Files
sx/library/modules/gpu/metal.sx
agra a1647eab9b metal: pause step 3b pending sx-side fixes (filed 0024-0030)
Step 3b code is wired across UIRenderer + GlyphCache + UIPipeline +
chess game (gpu_mode = .metal on iOS, MetalGPU bound via the GPU
protocol). macOS GL chess, iOS-sim GLES chess, and iOS-sim Metal
triangle (63-metal-clear.sx) all still render.

iOS-sim Metal chess crashes inside replaceRegion uploading the 1MB
font atlas. Bisecting that crash exposed several sx-language issues
where mid-bisect tracers (NSLog inside if/else branch bodies) didn't
produce output, blocking further investigation.

Filing each finding as examples/issue-NNNN.sx rather than working
around piecemeal:

Bugs:
- 0024 NSLog/foreign-call inside if/else body not producing output
- 0025 C-ABI param coercion incomplete for composites >16B
       (combined direct-call abiCoerceParamType TODO + call_indirect
        path that doesn't apply C-ABI coercion at all)
- 0026 replaceRegion 1MB upload crash (likely downstream of 0025)

Features needed for step 4 + cleanup:
- 0027 Obj-C block bridge (^{...}) for animateWithDuration:
- 0028 Optional protocol box (?GPU = null) replaces T = ---; has_T: bool
- 0029 destroy_texture/buffer/shader on GPU protocol
- 0030 extern cross-file globals

Library-side: renderer.sx + glyph_cache.sx + pipeline.sx gain a
`gpu: GPU = ---; has_gpu: bool` field pair + branches that route every
GL touchpoint through the protocol when has_gpu. glyph_cache.init
saves/restores those fields around its memset. pipeline.set_gpu()
propagates to renderer + font. Renderer's MSL shader source added as
UI_MSL_SRC using packed_float2/packed_float4 to keep the 12-float
interleaved vertex layout tight (48 bytes).

metal.sx: dual-phase init (init(null, 0, 0) for eager device+queue,
re-init with the layer once UIKit installs the SxMetalView).
setStorageMode:.shared on every texture descriptor to ensure CPU-
writable atlas pixels on Apple Silicon iOS-sim.

Regression suite: 68 passing, 0 failed. WASM chess build currently
broken under step 3b state (silent compiler crash); documented in
CHECKPOINT.md, likely fallout from one of the filed issues (probably
0028 — the verbose protocol-box pattern). Step 3b resumes after
0024-0030 land.
2026-05-17 21:17:17 +03:00

602 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Metal backend for the GPU protocol. iOS-only for now; macOS later.
//
// Linking is per-target via the consumer's build.sx:
// opts.add_framework("Metal")
// opts.add_framework("QuartzCore") // CAMetalLayer lives here
// `#framework "Metal"` below adds it to iOS-target link lines automatically;
// non-iOS targets don't reach the Metal-touching code paths.
#import "modules/std.sx";
#import "modules/std/objc.sx";
#import "modules/compiler.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";
#framework "Metal";
// MTLCreateSystemDefaultDevice lives in the Metal framework as a plain C
// function. Returns id<MTLDevice> retained +1; we leak it for now since
// the device lives for the whole process.
MTLCreateSystemDefaultDevice :: () -> *void #foreign;
// Pixel formats.
MTL_PIXEL_FORMAT_BGRA8_UNORM :u64: 80;
MTL_PIXEL_FORMAT_RGBA8_UNORM :u64: 70;
MTL_PIXEL_FORMAT_R8_UNORM :u64: 10;
// MTLLoadAction / MTLStoreAction.
MTL_LOAD_ACTION_CLEAR :u64: 2;
MTL_STORE_ACTION_STORE :u64: 1;
// MTLStorageMode. For UI atlases + sprites the CPU needs to write pixels
// and the GPU needs to sample — `.shared` is the safe default. On iOS-sim
// under Apple Silicon the convenience class method's default storage
// isn't reliably shared, so we set it explicitly in metal_create_texture_ios.
MTL_STORAGE_MODE_SHARED :u64: 0;
// MTLPrimitiveType.
MTL_PRIMITIVE_TYPE_TRIANGLE :u64: 3;
// MTLBlendFactor — the subset used for normal alpha blending.
MTL_BLEND_FACTOR_SRC_ALPHA :u64: 4;
MTL_BLEND_FACTOR_ONE_MINUS_SRC_A :u64: 5;
// CGSize is a 2-element f64 HFA. arm64 Apple ABI puts it in d0,d1 — direct
// fn-pointer cast on objc_msgSend with a CGSize arg does the right thing.
CGSize :: struct { width: f64; height: f64; }
// MTLClearColor is a 4-element f64 HFA. Same ABI story — passes in d0..d3.
MTLClearColor :: struct {
red: f64;
green: f64;
blue: f64;
alpha: f64;
}
// MTLOrigin / MTLSize / MTLRegion / MTLScissorRect — integer aggregates.
// MTLRegion is 48 bytes and MTLScissorRect is 32 bytes; arm64 Apple ABI
// passes >16-byte composites by reference (address in the next register).
// We declare the call shapes with `*MTLRegion` etc., construct a local on
// the stack, and pass `@local` — the machine state matches what the Obj-C
// method expects.
MTLOrigin :: struct { x: u64; y: u64; z: u64; }
MTLSize :: struct { width: u64; height: u64; depth: u64; }
MTLRegion :: struct { origin: MTLOrigin; size: MTLSize; }
MTLScissorRect :: struct { x: u64; y: u64; width: u64; height: u64; }
// Pixel sub-format storage for textures. Tracks the bytes-per-pixel for the
// upload path (replaceRegion needs bytesPerRow which is bpp × width).
TextureSlot :: struct {
tex: *void = null;
bytes_per_pixel: u32 = 0;
}
MetalGPU :: struct {
device: *void = null; // id<MTLDevice>
queue: *void = null; // id<MTLCommandQueue>
layer: *void = null; // CAMetalLayer*
pixel_w: s32 = 0;
pixel_h: s32 = 0;
// Per-frame transients. Live only between begin_frame and end_frame.
drawable: *void = null; // id<CAMetalDrawable>
cmd_buffer: *void = null; // id<MTLCommandBuffer>
encoder: *void = null; // id<MTLRenderCommandEncoder>
// Resource tables. Handles are 1-based indices (0 = invalid).
shaders: List(*void) = .{}; // MTLRenderPipelineState*
buffers: List(*void) = .{}; // MTLBuffer*
textures: List(TextureSlot) = .{};
}
impl GPU for MetalGPU {
// Two-phase init: callers can `init(null, 0, 0)` first to allocate
// device + queue eagerly (lets the UI pipeline compile shaders before
// UIKit hands us a layer), then re-call `init(layer, w, h)` once the
// CAMetalLayer is available. The second call only updates the layer
// ref + dims; device/queue are preserved.
init :: (self: *MetalGPU, target: *void, pixel_w: s32, pixel_h: s32) -> bool {
inline if OS != .ios { return false; }
if target != null {
self.layer = target;
self.pixel_w = pixel_w;
self.pixel_h = pixel_h;
}
metal_init_ios(self);
}
shutdown :: (self: *MetalGPU) {
// Metal objects clean up at process exit on iOS. A real shutdown
// would send `release` to queue + device.
}
resize :: (self: *MetalGPU, pixel_w: s32, pixel_h: s32) {
self.pixel_w = pixel_w;
self.pixel_h = pixel_h;
inline if OS == .ios {
metal_resize_ios(self);
}
}
begin_frame :: (self: *MetalGPU, clear: ClearColor) -> bool {
inline if OS != .ios { return false; }
metal_begin_frame_ios(self, clear);
}
end_frame :: (self: *MetalGPU, target_time: f64) {
inline if OS == .ios {
metal_end_frame_ios(self, target_time);
}
}
// ── Resources ────────────────────────────────────────────────────────
// Handle = 1-based index into the backing List (0 = invalid). The bulk
// of each method lives in an iOS-only helper for readability — the impl
// method just guards non-iOS and delegates.
create_shader :: (self: *MetalGPU, vsrc: string, fsrc: string) -> ShaderHandle {
inline if OS != .ios { return 0; }
metal_create_shader_ios(self, vsrc);
}
create_buffer :: (self: *MetalGPU, size_bytes: s64) -> BufferHandle {
inline if OS != .ios { return 0; }
metal_create_buffer_ios(self, size_bytes);
}
update_buffer :: (self: *MetalGPU, buf: BufferHandle, data: *void, size_bytes: s64) {
inline if OS == .ios {
metal_update_buffer_ios(self, buf, data, size_bytes);
}
}
create_texture :: (self: *MetalGPU, w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle {
inline if OS != .ios { return 0; }
metal_create_texture_ios(self, w, h, format, pixels);
}
update_texture_region :: (self: *MetalGPU, tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void) {
inline if OS == .ios {
metal_update_texture_region_ios(self, tex, x, y, w, h, pixels);
}
}
// ── Per-draw state ───────────────────────────────────────────────────
// All operate on `self.encoder`, which is live only between begin_frame
// and end_frame. Calling these outside that window is a silent no-op.
set_shader :: (self: *MetalGPU, sh: ShaderHandle) {
inline if OS == .ios {
metal_set_shader_ios(self, sh);
}
}
set_vertex_buffer :: (self: *MetalGPU, buf: BufferHandle) {
inline if OS == .ios {
metal_set_vertex_buffer_ios(self, buf);
}
}
set_texture :: (self: *MetalGPU, slot: u32, tex: TextureHandle) {
inline if OS == .ios {
metal_set_texture_ios(self, slot, tex);
}
}
set_vertex_constants :: (self: *MetalGPU, slot: u32, data: *void, size_bytes: s64) {
inline if OS == .ios {
metal_set_vertex_constants_ios(self, slot, data, size_bytes);
}
}
set_scissor :: (self: *MetalGPU, x: s32, y: s32, w: s32, h: s32) {
inline if OS == .ios {
metal_set_scissor_ios(self, x, y, w, h);
}
}
disable_scissor :: (self: *MetalGPU) {
inline if OS == .ios {
metal_disable_scissor_ios(self);
}
}
draw_triangles :: (self: *MetalGPU, vertex_offset: s32, vertex_count: s32) {
inline if OS == .ios {
metal_draw_triangles_ios(self, vertex_offset, vertex_count);
}
}
}
// ───────────────────────────────────────────────────────────────────────────
// iOS-only helpers — only reachable from `inline if OS == .ios` call sites,
// so non-iOS builds never reference the unresolved Metal symbols below.
// ───────────────────────────────────────────────────────────────────────────
// init() may be called twice: once with target==null to create device +
// queue eagerly (so the UI pipeline can compile shaders before UIKit
// has a layer for us), then again with target=CAMetalLayer once
// `-[SxAppDelegate didFinishLaunching:]` has installed the view.
// Both calls go through this helper; it's idempotent on the device/queue
// and only touches the layer when one's been supplied.
metal_init_ios :: (self: *MetalGPU) -> bool {
inline if OS != .ios { return false; }
if self.device == null {
self.device = MTLCreateSystemDefaultDevice();
if self.device == null { return false; }
}
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void = xx objc_msgSend;
msg_osize : (*void, *void, CGSize) -> void = xx objc_msgSend;
msg_o : (*void, *void) -> *void = xx objc_msgSend;
if self.queue == null {
self.queue = msg_o(self.device, sel_registerName("newCommandQueue".ptr));
if self.queue == null { return false; }
}
if self.layer != null {
msg_oo(self.layer, sel_registerName("setDevice:".ptr), self.device);
msg_ou(self.layer, sel_registerName("setPixelFormat:".ptr), MTL_PIXEL_FORMAT_BGRA8_UNORM);
msg_ob(self.layer, sel_registerName("setFramebufferOnly:".ptr), 1);
size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h };
msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size);
}
true;
}
metal_resize_ios :: (self: *MetalGPU) {
inline if OS != .ios { return; }
if self.layer == null { return; }
msg_osize : (*void, *void, CGSize) -> void = xx objc_msgSend;
size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h };
msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size);
}
metal_begin_frame_ios :: (self: *MetalGPU, clear: ClearColor) -> bool {
inline if OS != .ios { return false; }
if self.layer == null { return false; }
if self.queue == null { return false; }
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_oo_ret : (*void, *void, *void) -> *void = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ouret : (*void, *void, u64) -> *void = xx objc_msgSend;
msg_oclear : (*void, *void, MTLClearColor) -> void = xx objc_msgSend;
// drawable = [layer nextDrawable]
self.drawable = msg_o(self.layer, sel_registerName("nextDrawable".ptr));
if self.drawable == null { return false; }
// tex = [drawable texture]
drawable_texture := msg_o(self.drawable, sel_registerName("texture".ptr));
// pass = [MTLRenderPassDescriptor renderPassDescriptor] (autoreleased)
MTLRenderPassDescriptor := objc_getClass("MTLRenderPassDescriptor".ptr);
pass := msg_o(MTLRenderPassDescriptor, sel_registerName("renderPassDescriptor".ptr));
// color0 = pass.colorAttachments[0]
attachments := msg_o(pass, sel_registerName("colorAttachments".ptr));
color0 := msg_ouret(attachments, sel_registerName("objectAtIndexedSubscript:".ptr), 0);
msg_oo(color0, sel_registerName("setTexture:".ptr), drawable_texture);
msg_ou(color0, sel_registerName("setLoadAction:".ptr), MTL_LOAD_ACTION_CLEAR);
msg_ou(color0, sel_registerName("setStoreAction:".ptr), MTL_STORE_ACTION_STORE);
mtl_clear := MTLClearColor.{
red = xx clear.r,
green = xx clear.g,
blue = xx clear.b,
alpha = xx clear.a,
};
msg_oclear(color0, sel_registerName("setClearColor:".ptr), mtl_clear);
// cmd = [queue commandBuffer] (autoreleased)
self.cmd_buffer = msg_o(self.queue, sel_registerName("commandBuffer".ptr));
if self.cmd_buffer == null { return false; }
// encoder = [cmd renderCommandEncoderWithDescriptor:pass] (autoreleased)
self.encoder = msg_oo_ret(self.cmd_buffer,
sel_registerName("renderCommandEncoderWithDescriptor:".ptr), pass);
if self.encoder == null { return false; }
true;
}
metal_end_frame_ios :: (self: *MetalGPU, target_time: f64) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if self.cmd_buffer == null { return; }
if self.drawable == null { return; }
msg_v : (*void, *void) -> void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_ood : (*void, *void, *void, f64) -> void = xx objc_msgSend;
msg_v(self.encoder, sel_registerName("endEncoding".ptr));
// target_time > 0 → presentDrawable:atTime: (lockstep path).
// target_time == 0 → fall back to presentDrawable: (immediate).
if target_time > 0.0 {
msg_ood(self.cmd_buffer, sel_registerName("presentDrawable:atTime:".ptr),
self.drawable, target_time);
} else {
msg_oo(self.cmd_buffer, sel_registerName("presentDrawable:".ptr), self.drawable);
}
msg_v(self.cmd_buffer, sel_registerName("commit".ptr));
self.encoder = null;
self.cmd_buffer = null;
self.drawable = null;
}
// ── Shader (MSL pipeline state) ──────────────────────────────────────────
// Compile the MSL source, look up the conventional entry points `vmain`
// (vertex) and `fmain` (fragment), and produce an `MTLRenderPipelineState`
// targeted at the layer's BGRA8 surface with standard alpha blending.
// The fsrc parameter is ignored — Metal's library is one MSL file with
// both functions; pass the combined source as vsrc.
metal_create_shader_ios :: (self: *MetalGPU, src: string) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_oo_r : (*void, *void, *void) -> *void = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ouret: (*void, *void, u64) -> *void = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void = xx objc_msgSend;
// [device newLibraryWithSource:src options:nil error:&err]
msg_lib : (*void, *void, *void, *void, **void) -> *void = xx objc_msgSend;
src_ns := ns_string(src.ptr);
err : *void = null;
library := msg_lib(self.device,
sel_registerName("newLibraryWithSource:options:error:".ptr),
src_ns, xx 0, @err);
if library == null {
NSLog(ns_string("[metal] MSL compile failed\n".ptr));
return 0;
}
vfn := msg_oo_r(library, sel_registerName("newFunctionWithName:".ptr),
ns_string("vmain".ptr));
ffn := msg_oo_r(library, sel_registerName("newFunctionWithName:".ptr),
ns_string("fmain".ptr));
if vfn == null { NSLog(ns_string("[metal] missing vmain in MSL\n".ptr)); return 0; }
if ffn == null { NSLog(ns_string("[metal] missing fmain in MSL\n".ptr)); return 0; }
MTLRenderPipelineDescriptor := objc_getClass("MTLRenderPipelineDescriptor".ptr);
desc := msg_o(MTLRenderPipelineDescriptor, sel_registerName("alloc".ptr));
desc = msg_o(desc, sel_registerName("init".ptr));
msg_oo(desc, sel_registerName("setVertexFunction:".ptr), vfn);
msg_oo(desc, sel_registerName("setFragmentFunction:".ptr), ffn);
// colorAttachments[0]: pixel format + alpha blending.
atts := msg_o(desc, sel_registerName("colorAttachments".ptr));
att0 := msg_ouret(atts, sel_registerName("objectAtIndexedSubscript:".ptr), 0);
msg_ou(att0, sel_registerName("setPixelFormat:".ptr), MTL_PIXEL_FORMAT_BGRA8_UNORM);
msg_ob(att0, sel_registerName("setBlendingEnabled:".ptr), 1);
msg_ou(att0, sel_registerName("setSourceRGBBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA);
msg_ou(att0, sel_registerName("setDestinationRGBBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A);
msg_ou(att0, sel_registerName("setSourceAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA);
msg_ou(att0, sel_registerName("setDestinationAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A);
msg_pipe : (*void, *void, *void, **void) -> *void = xx objc_msgSend;
err2 : *void = null;
state := msg_pipe(self.device,
sel_registerName("newRenderPipelineStateWithDescriptor:error:".ptr),
desc, @err2);
if state == null {
NSLog(ns_string("[metal] pipeline state creation failed\n".ptr));
return 0;
}
self.shaders.append(state);
xx self.shaders.len;
}
// ── Buffers ──────────────────────────────────────────────────────────────
// Shared-memory MTLBuffer (CPU + GPU visible on UMA hardware). `contents`
// returns the mapped pointer for memcpy uploads.
metal_create_buffer_ios :: (self: *MetalGPU, size_bytes: s64) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
if size_bytes <= 0 { return 0; }
// MTLResourceStorageModeShared is the default (option value 0).
msg_buf : (*void, *void, u64, u64) -> *void = xx objc_msgSend;
buf := msg_buf(self.device,
sel_registerName("newBufferWithLength:options:".ptr),
xx size_bytes, 0);
if buf == null { return 0; }
self.buffers.append(buf);
xx self.buffers.len;
}
metal_update_buffer_ios :: (self: *MetalGPU, handle: u32, data: *void, size_bytes: s64) {
inline if OS != .ios { return; }
buf := metal_lookup_buffer(self, handle);
if buf == null { return; }
if data == null { return; }
if size_bytes <= 0 { return; }
msg_o : (*void, *void) -> *void = xx objc_msgSend;
dst := msg_o(buf, sel_registerName("contents".ptr));
if dst == null { return; }
memcpy(dst, data, size_bytes);
}
metal_lookup_buffer :: (self: *MetalGPU, handle: u32) -> *void {
inline if OS != .ios { return null; }
if handle == 0 { return null; }
h64 : s64 = xx handle;
if h64 > self.buffers.len { return null; }
self.buffers.items[handle - 1];
}
metal_lookup_shader :: (self: *MetalGPU, handle: u32) -> *void {
inline if OS != .ios { return null; }
if handle == 0 { return null; }
h64 : s64 = xx handle;
if h64 > self.shaders.len { return null; }
self.shaders.items[handle - 1];
}
// ── Textures ─────────────────────────────────────────────────────────────
metal_create_texture_ios :: (self: *MetalGPU, w: s32, h: s32, format: TextureFormat, pixels: *void) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
if w <= 0 { return 0; }
if h <= 0 { return 0; }
pixel_format : u64 = 0;
bytes_per_pixel : u32 = 0;
if format == .rgba8 {
pixel_format = MTL_PIXEL_FORMAT_RGBA8_UNORM;
bytes_per_pixel = 4;
} else {
pixel_format = MTL_PIXEL_FORMAT_R8_UNORM;
bytes_per_pixel = 1;
}
// [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:width:height:mipmapped:]
MTLTextureDescriptor := objc_getClass("MTLTextureDescriptor".ptr);
msg_desc : (*void, *void, u64, u64, u64, u8) -> *void = xx objc_msgSend;
desc := msg_desc(MTLTextureDescriptor,
sel_registerName("texture2DDescriptorWithPixelFormat:width:height:mipmapped:".ptr),
pixel_format, xx w, xx h, 0);
if desc == null { return 0; }
// Force shared storage so the CPU can keep writing pixels (atlas updates,
// sprite uploads). On iOS-sim under Apple Silicon the convenience class
// method's default storage isn't reliably shared for every format.
msg_ou_void : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ou_void(desc, sel_registerName("setStorageMode:".ptr), MTL_STORAGE_MODE_SHARED);
msg_oo : (*void, *void, *void) -> *void = xx objc_msgSend;
tex := msg_oo(self.device, sel_registerName("newTextureWithDescriptor:".ptr), desc);
if tex == null { return 0; }
slot : TextureSlot = .{ tex = tex, bytes_per_pixel = bytes_per_pixel };
self.textures.append(slot);
if pixels != null {
handle : u32 = xx self.textures.len;
metal_update_texture_region_ios(self, handle, 0, 0, w, h, pixels);
}
xx self.textures.len;
}
metal_update_texture_region_ios :: (self: *MetalGPU, handle: u32, x: s32, y: s32, w: s32, h: s32, pixels: *void) {
inline if OS != .ios { return; }
if handle == 0 { return; }
h64 : s64 = xx handle;
if h64 > self.textures.len { return; }
slot := self.textures.items[handle - 1];
if slot.tex == null { return; }
if pixels == null { return; }
if w <= 0 { return; }
if h <= 0 { return; }
region : MTLRegion = .{
origin = .{ x = xx x, y = xx y, z = 0 },
size = .{ width = xx w, height = xx h, depth = 1 },
};
bytes_per_row : u64 = xx (slot.bytes_per_pixel * cast(u32) w);
// [tex replaceRegion:region mipmapLevel:0 withBytes:pixels bytesPerRow:bytes_per_row]
msg_replace : (*void, *void, *MTLRegion, u64, *void, u64) -> void = xx objc_msgSend;
msg_replace(slot.tex,
sel_registerName("replaceRegion:mipmapLevel:withBytes:bytesPerRow:".ptr),
@region, 0, pixels, bytes_per_row);
}
// ── Per-draw state ───────────────────────────────────────────────────────
metal_set_shader_ios :: (self: *MetalGPU, sh: u32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
state := metal_lookup_shader(self, sh);
if state == null { return; }
msg : (*void, *void, *void) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setRenderPipelineState:".ptr), state);
}
metal_set_vertex_buffer_ios :: (self: *MetalGPU, h: u32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
buf := metal_lookup_buffer(self, h);
if buf == null { return; }
// [encoder setVertexBuffer:buf offset:0 atIndex:0]
msg : (*void, *void, *void, u64, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setVertexBuffer:offset:atIndex:".ptr), buf, 0, 0);
}
metal_set_texture_ios :: (self: *MetalGPU, slot: u32, h: u32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if h == 0 { return; }
h64 : s64 = xx h;
if h64 > self.textures.len { return; }
tex := self.textures.items[h - 1].tex;
if tex == null { return; }
// [encoder setFragmentTexture:tex atIndex:slot]
msg : (*void, *void, *void, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setFragmentTexture:atIndex:".ptr), tex, xx slot);
}
metal_set_vertex_constants_ios :: (self: *MetalGPU, slot: u32, data: *void, size_bytes: s64) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if data == null { return; }
if size_bytes <= 0 { return; }
// [encoder setVertexBytes:data length:size_bytes atIndex:slot]
msg : (*void, *void, *void, u64, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setVertexBytes:length:atIndex:".ptr),
data, xx size_bytes, xx slot);
}
metal_set_scissor_ios :: (self: *MetalGPU, x: s32, y: s32, w: s32, h: s32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
rect : MTLScissorRect = .{ x = xx x, y = xx y, width = xx w, height = xx h };
// [encoder setScissorRect:rect] (MTLScissorRect is 32 bytes → indirect)
msg : (*void, *void, *MTLScissorRect) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setScissorRect:".ptr), @rect);
}
metal_disable_scissor_ios :: (self: *MetalGPU) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
// Metal has no "disable scissor" — set the rect to cover the full
// drawable so subsequent draws aren't clipped.
rect : MTLScissorRect = .{ x = 0, y = 0, width = xx self.pixel_w, height = xx self.pixel_h };
msg : (*void, *void, *MTLScissorRect) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setScissorRect:".ptr), @rect);
}
metal_draw_triangles_ios :: (self: *MetalGPU, vertex_offset: s32, vertex_count: s32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if vertex_count <= 0 { return; }
// [encoder drawPrimitives:.triangle vertexStart:offset vertexCount:count]
msg : (*void, *void, u64, u64, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("drawPrimitives:vertexStart:vertexCount:".ptr),
MTL_PRIMITIVE_TYPE_TRIANGLE, xx vertex_offset, xx vertex_count);
}