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:
@@ -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