metal: GPU protocol + MetalGPU renders MSL triangle on iOS
Phase 8 step 3a of the Metal renderer port:
- New library/modules/gpu/ with types.sx (handles + ClearColor +
TextureFormat enum), api.sx (GPU :: protocol { ... } covering the
lifecycle / per-frame / resource / per-draw surface), and metal.sx
(MetalGPU backend implementing the protocol against CAMetalLayer).
Resource handles are 1-based indices into backend List(*void) tables.
MTL aggregates >16 bytes (MTLRegion, MTLScissorRect) pass via *T to
match arm64 Apple's indirect-by-reference ABI; MTLClearColor + CGSize
go through the HFA path as direct fn-pointer casts on objc_msgSend.
- UIKitPlatform got a gpu_mode: GpuMode toggle + sibling SxMetalView
class registration. In metal mode init skips EAGL context, the
did_finish_launching IMP skips the EAGL drawable-properties dict,
layoutSubviews reads the layer's bounds * dpi_scale into pixel_w/h
instead of allocating a GL renderbuffer, and end_frame is a no-op
(the MetalGPU owns its own present).
- examples/63-metal-clear.sx verifies the pipeline end-to-end on iOS
sim — compiles a pass-through MSL shader (packed_float2/packed_float4
to avoid alignment padding), uploads 3 vertices, draws a colored
triangle on a dark-blue clear.
Compiler fixes (filed-and-fixed in this branch):
- inline if X { return E; } followed by a fall-through final expression
no longer emits two terminators into the same basic block. Verified
by examples/83-inline-if-return-fallthrough.sx.
- Top-level type alias Name :: u32; now resolves correctly as the type
annotation on a global variable (was treated as ptr {}, breaking
comparisons + initializers). Verified by examples/84-global-type-alias.sx.
Issue->feature promotion:
- 16 historical examples/issue-NNNN.sx repros now confirmed-fixed and
renamed to focused feature names (67-82). Each gains a
tests/expected/*.txt + .exit pair so the regression suite covers them.
- 5 stale issue repros deleted (subsumed by broader tests).
Regression suite: 68 passing, 0 failed. macOS chess builds + runs; wasm
chess builds; iOS sim GLES chess still renders the full board; iOS sim
Metal demo renders the triangle.
This commit is contained in:
42
library/modules/gpu/api.sx
Normal file
42
library/modules/gpu/api.sx
Normal file
@@ -0,0 +1,42 @@
|
||||
#import "modules/std.sx";
|
||||
#import "modules/gpu/types.sx";
|
||||
|
||||
// GPU is the rendering-API abstraction. Concrete backends live as siblings
|
||||
// of this file: `metal.sx` (iOS, eventually macOS), `vulkan.sx` (Linux/
|
||||
// Android, plus macOS via MoltenVK), `webgpu.sx` (wasm). The SDL-backed
|
||||
// GL renderer used by the desktop+wasm path stays as-is until those
|
||||
// backends land.
|
||||
|
||||
GPU :: protocol {
|
||||
// Bind the GPU to a backend-specific render target (e.g. a
|
||||
// CAMetalLayer on iOS). pixel_w/pixel_h are the drawable's pixel
|
||||
// dimensions; call resize when they change.
|
||||
init :: (target: *void, pixel_w: s32, pixel_h: s32) -> bool;
|
||||
shutdown :: ();
|
||||
|
||||
resize :: (pixel_w: s32, pixel_h: s32);
|
||||
|
||||
begin_frame :: (clear: ClearColor) -> bool;
|
||||
|
||||
// target_time is the host clock time at which the drawable should be
|
||||
// presented (units match the platform's CADisplayLink.targetTimestamp
|
||||
// on Apple). Metal forwards it to presentDrawable:atTime: to cap the
|
||||
// pipeline at one frame so the inset slide lands on the same vsync as
|
||||
// UIKit's keyboard view. GL backends ignore it.
|
||||
end_frame :: (target_time: f64);
|
||||
|
||||
create_shader :: (vsrc: string, fsrc: string) -> ShaderHandle;
|
||||
create_buffer :: (size_bytes: s64) -> BufferHandle;
|
||||
update_buffer :: (buf: BufferHandle, data: *void, size_bytes: s64);
|
||||
create_texture :: (w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle;
|
||||
update_texture_region :: (tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void);
|
||||
|
||||
set_shader :: (sh: ShaderHandle);
|
||||
set_vertex_buffer :: (buf: BufferHandle);
|
||||
set_texture :: (slot: u32, tex: TextureHandle);
|
||||
set_vertex_constants :: (slot: u32, data: *void, size_bytes: s64);
|
||||
set_scissor :: (x: s32, y: s32, w: s32, h: s32);
|
||||
disable_scissor :: ();
|
||||
|
||||
draw_triangles :: (vertex_offset: s32, vertex_count: s32);
|
||||
}
|
||||
571
library/modules/gpu/metal.sx
Normal file
571
library/modules/gpu/metal.sx
Normal file
@@ -0,0 +1,571 @@
|
||||
// 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;
|
||||
|
||||
// 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 {
|
||||
init :: (self: *MetalGPU, target: *void, pixel_w: s32, pixel_h: s32) -> bool {
|
||||
inline if OS != .ios { return false; }
|
||||
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.
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
metal_init_ios :: (self: *MetalGPU) -> bool {
|
||||
inline if OS != .ios { return false; }
|
||||
if self.layer == null { return false; }
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
self.queue = msg_o(self.device, sel_registerName("newCommandQueue".ptr));
|
||||
if self.queue == null { return false; }
|
||||
|
||||
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; }
|
||||
|
||||
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);
|
||||
}
|
||||
24
library/modules/gpu/types.sx
Normal file
24
library/modules/gpu/types.sx
Normal file
@@ -0,0 +1,24 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
// Opaque GPU resource handles. Backends decide what the integer means
|
||||
// (GL: object name; Metal: 1-based index into a backend-owned table of
|
||||
// retained MTL* objects). Zero is reserved for "no handle".
|
||||
ShaderHandle :: u32;
|
||||
BufferHandle :: u32;
|
||||
TextureHandle :: u32;
|
||||
|
||||
ClearColor :: struct {
|
||||
r: f32;
|
||||
g: f32;
|
||||
b: f32;
|
||||
a: f32;
|
||||
|
||||
black :: () -> ClearColor => .{ r = 0.0, g = 0.0, b = 0.0, a = 1.0 };
|
||||
}
|
||||
|
||||
// Texture pixel format. The UI renderer only needs rgba8 (images, the
|
||||
// 1×1 white solid-rect texture) and r8 (font glyph atlas alpha).
|
||||
TextureFormat :: enum {
|
||||
rgba8;
|
||||
r8;
|
||||
}
|
||||
@@ -55,16 +55,26 @@ GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5;
|
||||
|
||||
g_uikit_plat : *UIKitPlatform = null;
|
||||
|
||||
// Which GPU API the UIKit backend wires up. `.gles` keeps the existing
|
||||
// CAEAGLLayer + EAGLContext + renderbuffer path; `.metal` swaps the view
|
||||
// for a CAMetalLayer-backed one and leaves rendering to MetalGPU. Set
|
||||
// before calling `init`; default `.gles` preserves prior behavior.
|
||||
GpuMode :: enum {
|
||||
gles;
|
||||
metal;
|
||||
}
|
||||
|
||||
UIKitPlatform :: struct {
|
||||
window: *void = null; // UIWindow*
|
||||
root_vc: *void = null; // UIViewController*
|
||||
gl_view: *void = null; // SxGLView*
|
||||
gl_layer: *void = null; // CAEAGLLayer* (= gl_view.layer)
|
||||
gl_ctx: *void = null; // EAGLContext*
|
||||
gl_view: *void = null; // SxGLView* OR SxMetalView* (depending on gpu_mode)
|
||||
gl_layer: *void = null; // CAEAGLLayer* OR CAMetalLayer* (= gl_view.layer)
|
||||
gl_ctx: *void = null; // EAGLContext* (null in metal mode)
|
||||
display_link: *void = null;
|
||||
color_renderbuffer: u32 = 0;
|
||||
framebuffer: u32 = 0;
|
||||
gl_initialized: bool = false;
|
||||
gpu_mode: GpuMode = .gles;
|
||||
|
||||
// Hidden UITextField; firstResponder ⇆ keyboard visibility.
|
||||
text_field: *void = null;
|
||||
@@ -121,7 +131,13 @@ impl Platform for UIKitPlatform {
|
||||
inline if OS == .ios {
|
||||
uikit_chdir_to_bundle();
|
||||
uikit_register_classes();
|
||||
uikit_create_gl_context(self);
|
||||
if self.gpu_mode == .gles {
|
||||
uikit_create_gl_context(self);
|
||||
} else {
|
||||
// Metal mode: skip EAGL. dpi_scale still needs to be known
|
||||
// before the window exists so callers can size resources.
|
||||
uikit_read_screen_scale(self);
|
||||
}
|
||||
}
|
||||
true;
|
||||
}
|
||||
@@ -159,7 +175,10 @@ impl Platform for UIKitPlatform {
|
||||
|
||||
end_frame :: (self: *UIKitPlatform) {
|
||||
inline if OS == .ios {
|
||||
uikit_present_renderbuffer(self);
|
||||
if self.gpu_mode == .gles {
|
||||
uikit_present_renderbuffer(self);
|
||||
}
|
||||
// Metal mode: caller's gpu.end_frame() handles present.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,9 +293,25 @@ uikit_register_classes :: () {
|
||||
objc_registerClassPair(SxAppDelegate);
|
||||
|
||||
uikit_register_gl_view_class();
|
||||
uikit_register_metal_view_class();
|
||||
}
|
||||
}
|
||||
|
||||
// Read [UIScreen mainScreen].nativeScale into plat.dpi_scale. Used by the
|
||||
// metal-mode init path which doesn't go through uikit_create_gl_context
|
||||
// (that's where the gles path picks the scale up).
|
||||
uikit_read_screen_scale :: (plat: *UIKitPlatform) {
|
||||
inline if OS != .ios { return; }
|
||||
UIScreen := objc_getClass("UIScreen".ptr);
|
||||
sel_main_screen := sel_registerName("mainScreen".ptr);
|
||||
sel_native_scale := sel_registerName("nativeScale".ptr);
|
||||
msg_o : (*void, *void) -> *void = xx objc_msgSend;
|
||||
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
|
||||
screen := msg_o(UIScreen, sel_main_screen);
|
||||
scale_d : f64 = msg_d(screen, sel_native_scale);
|
||||
plat.dpi_scale = xx scale_d;
|
||||
}
|
||||
|
||||
// NSNotification callback. The notification's userInfo dict has the
|
||||
// keyboard's end-frame and animation curve/duration.
|
||||
// UIKeyboardFrameEndUserInfoKey → NSValue wrapping CGRect (screen coords)
|
||||
@@ -390,6 +425,7 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
|
||||
UIWindow := objc_getClass("UIWindow".ptr);
|
||||
UIViewController := objc_getClass("UIViewController".ptr);
|
||||
SxGLView := objc_getClass("SxGLView".ptr);
|
||||
SxMetalView := objc_getClass("SxMetalView".ptr);
|
||||
EAGLContext := objc_getClass("EAGLContext".ptr);
|
||||
CADisplayLink := objc_getClass("CADisplayLink".ptr);
|
||||
NSRunLoop := objc_getClass("NSRunLoop".ptr);
|
||||
@@ -445,11 +481,12 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
|
||||
vc_raw := msg_o(UIViewController, sel_alloc);
|
||||
plat.root_vc = msg_o(vc_raw, sel_init);
|
||||
|
||||
// Allocate SxGLView and install it as the VC's view, so the standard
|
||||
// ViewController layout pipeline sizes the GL view to the window. Setting
|
||||
// it BEFORE setRootViewController avoids the VC lazy-loading a default
|
||||
// view first.
|
||||
glv_raw := msg_o(SxGLView, sel_alloc);
|
||||
// Allocate either SxGLView or SxMetalView based on gpu_mode and install
|
||||
// it as the VC's view. The view's +layerClass override gives us the
|
||||
// right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE
|
||||
// setRootViewController avoids the VC lazy-loading a default view.
|
||||
view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView;
|
||||
glv_raw := msg_o(view_class, sel_alloc);
|
||||
plat.gl_view = msg_o(glv_raw, sel_init);
|
||||
sel_set_view := sel_registerName("setView:".ptr);
|
||||
msg_oo(plat.root_vc, sel_set_view, plat.gl_view);
|
||||
@@ -458,35 +495,40 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
|
||||
|
||||
plat.gl_layer = msg_o(plat.gl_view, sel_layer);
|
||||
|
||||
// Mark the layer opaque (no compositor blend) + set the drawable properties
|
||||
// required by EAGLContext.renderbufferStorage:fromDrawable: (color format,
|
||||
// non-retained backing). Without this dict the renderbuffer allocation
|
||||
// silently fails and the framebuffer reports INCOMPLETE.
|
||||
// Mark the layer opaque (no compositor blend). Required for EAGL +
|
||||
// recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't
|
||||
// hurt to be explicit).
|
||||
sel_set_opaque := sel_registerName("setOpaque:".ptr);
|
||||
msg_obool : (*void, *void, u8) -> void = xx objc_msgSend;
|
||||
msg_obool(plat.gl_layer, sel_set_opaque, 1);
|
||||
|
||||
NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
|
||||
NSNumber := objc_getClass("NSNumber".ptr);
|
||||
sel_dictionary := sel_registerName("dictionary".ptr);
|
||||
sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr);
|
||||
sel_number_bool := sel_registerName("numberWithBool:".ptr);
|
||||
sel_set_drawable := sel_registerName("setDrawableProperties:".ptr);
|
||||
if plat.gpu_mode == .gles {
|
||||
// EAGL drawable properties dict required by
|
||||
// EAGLContext.renderbufferStorage:fromDrawable: (color format,
|
||||
// non-retained backing). Without this dict the renderbuffer
|
||||
// allocation silently fails and the framebuffer reports INCOMPLETE.
|
||||
NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
|
||||
NSNumber := objc_getClass("NSNumber".ptr);
|
||||
sel_dictionary := sel_registerName("dictionary".ptr);
|
||||
sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr);
|
||||
sel_number_bool := sel_registerName("numberWithBool:".ptr);
|
||||
sel_set_drawable := sel_registerName("setDrawableProperties:".ptr);
|
||||
|
||||
msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend;
|
||||
ns_no := msg_oio(NSNumber, sel_number_bool, 0);
|
||||
msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend;
|
||||
ns_no := msg_oio(NSNumber, sel_number_bool, 0);
|
||||
|
||||
// The EAGL dict keys/values must be the framework-provided NSString
|
||||
// constants (pointer identity is checked) — dlsym them from OpenGLES.
|
||||
retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
|
||||
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
|
||||
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
|
||||
// The EAGL dict keys/values must be the framework-provided NSString
|
||||
// constants (pointer identity is checked) — dlsym them from OpenGLES.
|
||||
retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
|
||||
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
|
||||
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
|
||||
|
||||
dict := msg_o(NSMutableDictionary, sel_dictionary);
|
||||
msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend;
|
||||
msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key);
|
||||
msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key);
|
||||
msg_oo(plat.gl_layer, sel_set_drawable, dict);
|
||||
dict := msg_o(NSMutableDictionary, sel_dictionary);
|
||||
msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend;
|
||||
msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key);
|
||||
msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key);
|
||||
msg_oo(plat.gl_layer, sel_set_drawable, dict);
|
||||
}
|
||||
|
||||
// EAGLContext + load_gl were already done in uikit_create_gl_context()
|
||||
// back when the game's main called plat.init() — so shaders/textures
|
||||
@@ -615,10 +657,38 @@ uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) {
|
||||
if g_uikit_plat == null { return; }
|
||||
plat := g_uikit_plat;
|
||||
if plat.gl_initialized { return; }
|
||||
uikit_setup_renderbuffer(plat);
|
||||
if plat.gpu_mode == .gles {
|
||||
uikit_setup_renderbuffer(plat);
|
||||
} else {
|
||||
uikit_compute_layer_pixel_size(plat);
|
||||
}
|
||||
plat.gl_initialized = true;
|
||||
}
|
||||
|
||||
// Metal mode equivalent of uikit_setup_renderbuffer's "tell me how big the
|
||||
// drawable is in pixels". Reads the layer's bounds in points and scales to
|
||||
// pixels via dpi_scale. CAMetalLayer.drawableSize is set by MetalGPU.init
|
||||
// based on these dims.
|
||||
uikit_compute_layer_pixel_size :: (plat: *UIKitPlatform) {
|
||||
inline if OS != .ios { return; }
|
||||
if plat.gl_view == null { return; }
|
||||
|
||||
sel_bounds := sel_registerName("bounds".ptr);
|
||||
msg_rect : (*void, *void) -> CGRect = xx objc_msgSend;
|
||||
b := msg_rect(plat.gl_view, sel_bounds);
|
||||
|
||||
w_pts : f64 = b.width;
|
||||
h_pts : f64 = b.height;
|
||||
plat.viewport_w = xx w_pts;
|
||||
plat.viewport_h = xx h_pts;
|
||||
|
||||
scale64 : f64 = xx plat.dpi_scale;
|
||||
pw : f64 = w_pts * scale64;
|
||||
ph : f64 = h_pts * scale64;
|
||||
plat.pixel_w = xx pw;
|
||||
plat.pixel_h = xx ph;
|
||||
}
|
||||
|
||||
// Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an
|
||||
// NSSet<UITouch *> + UIEvent. We take the first touch (single-touch model
|
||||
// matching the chess game's drag-and-tap UX) and push the resulting
|
||||
@@ -698,3 +768,45 @@ uikit_register_gl_view_class :: () {
|
||||
objc_registerClassPair(SxGLView);
|
||||
}
|
||||
}
|
||||
|
||||
// +layerClass IMP for SxMetalView. Class method, signature "#@:".
|
||||
uikit_metal_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
|
||||
objc_getClass("CAMetalLayer".ptr);
|
||||
}
|
||||
|
||||
// SxMetalView reuses the same tick/layout/touch IMPs as SxGLView. The IMPs
|
||||
// already branch on `plat.gpu_mode` for the GL-specific bits (renderbuffer
|
||||
// setup, etc.), so a single set of IMPs serves both view classes.
|
||||
uikit_register_metal_view_class :: () {
|
||||
inline if OS == .ios {
|
||||
UIView := objc_getClass("UIView".ptr);
|
||||
SxMetalView := objc_allocateClassPair(UIView, "SxMetalView".ptr, 0);
|
||||
|
||||
metaclass := object_getClass(SxMetalView);
|
||||
class_addMethod(metaclass,
|
||||
sel_registerName("layerClass".ptr),
|
||||
xx uikit_metal_view_layer_class, "#@:".ptr);
|
||||
|
||||
class_addMethod(SxMetalView,
|
||||
sel_registerName("sxTick:".ptr),
|
||||
xx uikit_gl_view_tick, "v@:@".ptr);
|
||||
class_addMethod(SxMetalView,
|
||||
sel_registerName("layoutSubviews".ptr),
|
||||
xx uikit_gl_view_layout, "v@:".ptr);
|
||||
|
||||
class_addMethod(SxMetalView,
|
||||
sel_registerName("touchesBegan:withEvent:".ptr),
|
||||
xx uikit_gl_view_touches_began, "v@:@@".ptr);
|
||||
class_addMethod(SxMetalView,
|
||||
sel_registerName("touchesMoved:withEvent:".ptr),
|
||||
xx uikit_gl_view_touches_moved, "v@:@@".ptr);
|
||||
class_addMethod(SxMetalView,
|
||||
sel_registerName("touchesEnded:withEvent:".ptr),
|
||||
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
|
||||
class_addMethod(SxMetalView,
|
||||
sel_registerName("touchesCancelled:withEvent:".ptr),
|
||||
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
|
||||
|
||||
objc_registerClassPair(SxMetalView);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user