Files
sx/library/modules/platform/uikit.sx
agra a938c4f900 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.
2026-05-17 19:36:37 +03:00

813 lines
33 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.
// Pure UIKit + CAEAGLLayer + CADisplayLink backend for iOS.
//
// Linking is per-target via the game's build.sx (`opts.add_framework("UIKit")`
// + `opts.add_framework("OpenGLES")` + `opts.add_framework("QuartzCore")` on
// `.ios`). The file compiles cleanly on every target — the UIKit-touching
// bodies live behind `inline if OS == .ios` guards, so non-iOS builds never
// reach the unresolved Obj-C symbols.
#import "modules/std.sx";
#import "modules/std/objc.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/ui/types.sx";
#import "modules/ui/events.sx";
#import "modules/platform/types.sx";
#import "modules/platform/api.sx";
UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_class: *void) -> s32 #foreign;
dlsym :: (handle: *void, name: [*]u8) -> *void #foreign;
chdir :: (path: [*]u8) -> s32 #foreign;
// kEAGLRenderingAPIOpenGLES3 = 3
EAGL_API_GLES3 :: 3;
// CGFloat is a `double` on 64-bit Apple platforms; CGPoint = {x, y} fits in
// 16 bytes and returns via the FP-register path on arm64.
CGPoint :: struct { x: f64; y: f64; }
// UIEdgeInsets = {top, left, bottom, right} CGFloats — 32 bytes; exceeds the
// 16-byte registers cutoff so it returns via the x8 indirect-result-pointer
// convention. sx generates the right call sequence when the function-pointer
// type declares it as a by-value struct return.
UIEdgeInsets :: struct {
top: f64;
left: f64;
bottom: f64;
right: f64;
}
// CGRect for unwrapping NSValue-wrapped rects (e.g. keyboard end frame). Same
// 32-byte indirect-return shape as UIEdgeInsets.
CGRect :: struct {
x: f64;
y: f64;
width: f64;
height: f64;
}
// GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's
// loader path (they live on the framework's symbol table directly).
GL_RENDERBUFFER :u32: 0x8D41;
GL_FRAMEBUFFER :u32: 0x8D40;
GL_COLOR_ATTACHMENT0 :u32: 0x8CE0;
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* 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;
viewport_w: f32 = 0.0;
viewport_h: f32 = 0.0;
pixel_w: s32 = 0;
pixel_h: s32 = 0;
dpi_scale: f32 = 1.0;
delta_time: f32 = 0.016;
frame_closure: Closure() = ---;
has_frame_closure: bool = false;
events: List(Event) = .{};
safe_top: f32 = 0.0;
safe_left: f32 = 0.0;
safe_bottom: f32 = 0.0;
safe_right: f32 = 0.0;
keyboard_visible: bool = false;
keyboard_height: f32 = 0.0;
// Keyboard height SNAPS to its target value when the observer fires.
// It does NOT interpolate in lockstep with iOS's keyboard animation.
// Reason: with OpenGL ES + CAEAGLLayer, our renderbuffer is baked at
// `presentRenderbuffer` time, while UIKit's keyboard view is composited
// by CoreAnimation at vsync. We can't make the compositor interpolate
// the renderbuffer's contents in lockstep with the keyboard's frame.
// True lockstep requires a Metal renderer (CAMetalLayer +
// `present(at: targetTimestamp)` keeps the pipeline at 1 frame) plus
// curve-accurate prediction. Tracked as the Metal port in
// current/CHECKPOINT.md.
saved_title: [*]u8 = null;
}
impl Platform for UIKitPlatform {
init :: (self: *UIKitPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
self.dpi_scale = 1.0;
self.delta_time = 0.016;
self.has_frame_closure = false;
self.gl_initialized = false;
self.keyboard_visible = false;
self.keyboard_height = 0.0;
self.saved_title = title.ptr;
g_uikit_plat = self;
// iOS apps start with CWD=/. chdir to the bundle's resourcePath so the
// game's relative `fopen("assets/...")` calls find their data — must
// happen BEFORE any code that loads fonts/textures from disk.
inline if OS == .ios {
uikit_chdir_to_bundle();
uikit_register_classes();
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;
}
run_frame_loop :: (self: *UIKitPlatform, frame_fn: Closure()) {
self.frame_closure = frame_fn;
self.has_frame_closure = true;
g_uikit_plat = self;
inline if OS == .ios {
UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr));
}
}
poll_events :: (self: *UIKitPlatform) -> []Event {
result : []Event = ---;
result.ptr = self.events.items;
result.len = self.events.len;
self.events.len = 0;
result;
}
begin_frame :: (self: *UIKitPlatform) -> FrameContext {
inline if OS == .ios {
uikit_refresh_safe_insets(self);
}
FrameContext.{
viewport_w = self.viewport_w,
viewport_h = self.viewport_h,
pixel_w = self.pixel_w,
pixel_h = self.pixel_h,
dpi_scale = self.dpi_scale,
delta_time = self.delta_time,
};
}
end_frame :: (self: *UIKitPlatform) {
inline if OS == .ios {
if self.gpu_mode == .gles {
uikit_present_renderbuffer(self);
}
// Metal mode: caller's gpu.end_frame() handles present.
}
}
safe_insets :: (self: *UIKitPlatform) -> EdgeInsets {
bottom := self.safe_bottom;
if self.keyboard_visible {
if self.keyboard_height > bottom { bottom = self.keyboard_height; }
}
EdgeInsets.{
top = self.safe_top,
left = self.safe_left,
bottom = bottom,
right = self.safe_right,
};
}
keyboard :: (self: *UIKitPlatform) -> KeyboardState {
KeyboardState.{
visible = self.keyboard_visible,
height = self.keyboard_height,
};
}
show_keyboard :: (self: *UIKitPlatform) {
inline if OS == .ios {
if self.text_field == null { return; }
sel_become := sel_registerName("becomeFirstResponder".ptr);
msg_b : (*void, *void) -> u8 = xx objc_msgSend;
msg_b(self.text_field, sel_become);
}
}
hide_keyboard :: (self: *UIKitPlatform) {
inline if OS == .ios {
if self.text_field == null { return; }
sel_resign := sel_registerName("resignFirstResponder".ptr);
msg_b : (*void, *void) -> u8 = xx objc_msgSend;
msg_b(self.text_field, sel_resign);
}
}
stop :: (self: *UIKitPlatform) { }
shutdown :: (self: *UIKitPlatform) { }
}
// dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2.
ios_gl_proc :: (name: [*]u8) -> *void {
rtld_default : *void = xx (0 - 2);
dlsym(rtld_default, name);
}
// Read a `extern NSString * const k...` global from the loaded image. The
// extern variable holds a pointer to the NSString instance — dlsym returns
// the address of that variable, which we dereference.
uikit_extern_nsstring :: (name: [*]u8) -> *void {
rtld_default : *void = xx (0 - 2);
p := dlsym(rtld_default, name);
if p == null { return null; }
pp : **void = xx p;
pp.*;
}
// ───────────────────────────────────────────────────────────────────────────
// iOS-only helpers — only reachable from `inline if OS == .ios` call sites,
// so non-iOS builds never reference the unresolved UIKit symbols below.
// ───────────────────────────────────────────────────────────────────────────
uikit_refresh_safe_insets :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
if plat.gl_view == null { return; }
sel_safe := sel_registerName("safeAreaInsets".ptr);
msg_insets : (*void, *void) -> UIEdgeInsets = xx objc_msgSend;
i := msg_insets(plat.gl_view, sel_safe);
plat.safe_top = xx i.top;
plat.safe_left = xx i.left;
plat.safe_bottom = xx i.bottom;
plat.safe_right = xx i.right;
}
uikit_chdir_to_bundle :: () {
inline if OS != .ios { return; }
NSBundle := objc_getClass("NSBundle".ptr);
sel_main_bundle := sel_registerName("mainBundle".ptr);
sel_resource_path := sel_registerName("resourcePath".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
bundle := msg_o(NSBundle, sel_main_bundle);
rsrc := msg_o(bundle, sel_resource_path);
if rsrc == null { return; }
chdir(c_string(rsrc));
}
uikit_register_classes :: () {
inline if OS == .ios {
UIResponder := objc_getClass("UIResponder".ptr);
SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0);
class_addMethod(SxAppDelegate,
sel_registerName("application:didFinishLaunchingWithOptions:".ptr),
xx uikit_did_finish_launching, "c@:@@".ptr);
class_addMethod(SxAppDelegate,
sel_registerName("window".ptr),
xx uikit_window_getter, "@@:".ptr);
class_addMethod(SxAppDelegate,
sel_registerName("setWindow:".ptr),
xx uikit_window_setter, "v@:@".ptr);
class_addMethod(SxAppDelegate,
sel_registerName("sxKeyboardWillChangeFrame:".ptr),
xx uikit_keyboard_will_change_frame, "v@:@".ptr);
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)
// UIKeyboardAnimationDurationUserInfoKey → NSNumber wrapping double
// Keyboard height = how much of the screen the keyboard covers from the bottom.
// If the keyboard's end Y >= screen.height, the keyboard is offscreen (hiding).
uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *void) callconv(.c) {
if g_uikit_plat == null { return; }
plat := g_uikit_plat;
sel_user_info := sel_registerName("userInfo".ptr);
sel_obj_for_key := sel_registerName("objectForKey:".ptr);
sel_cg_rect_value := sel_registerName("CGRectValue".ptr);
sel_double_value := sel_registerName("doubleValue".ptr);
sel_screen := sel_registerName("screen".ptr);
sel_bounds := sel_registerName("bounds".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> *void = xx objc_msgSend;
msg_rect : (*void, *void) -> CGRect = xx objc_msgSend;
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
user_info := msg_o(notification, sel_user_info);
if user_info == null { return; }
end_value := msg_oo(user_info, sel_obj_for_key,
ns_string("UIKeyboardFrameEndUserInfoKey".ptr));
if end_value == null { return; }
end_rect := msg_rect(end_value, sel_cg_rect_value);
// UIKeyboardAnimationDurationUserInfoKey is also in userInfo; reading it
// and running our inset update inside a `[UIView animateWithDuration:...]`
// block would put us in the same CoreAnimation transaction as the keyboard
// (zero-lag sync). Blocks aren't yet expressible from sx, so we update the
// inset synchronously — content snaps while the keyboard slides.
// Screen height in points. The window lives on the connected scene's screen.
if plat.window == null { return; }
win_screen := msg_o(plat.window, sel_screen);
screen_bounds := msg_rect(win_screen, sel_bounds);
// Keyboard height = how much of the screen the keyboard covers from the
// bottom. When the keyboard is hiding, its end-frame.y == screen.height,
// so the height comes out 0 and visible becomes false.
kb_top : f64 = end_rect.y;
sh : f64 = screen_bounds.height;
h := sh - kb_top;
if h < 0.0 { h = 0.0; }
if h > sh { h = sh; }
// SNAP to target. See comment on UIKitPlatform.keyboard_height for why
// lockstep interpolation is deferred until the Metal renderer.
plat.keyboard_height = xx h;
plat.keyboard_visible = h > 0.5;
}
uikit_create_gl_context :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
EAGLContext := objc_getClass("EAGLContext".ptr);
UIScreen := objc_getClass("UIScreen".ptr);
sel_alloc := sel_registerName("alloc".ptr);
sel_init_with_api := sel_registerName("initWithAPI:".ptr);
sel_set_current_ctx := sel_registerName("setCurrentContext:".ptr);
sel_main_screen := sel_registerName("mainScreen".ptr);
sel_native_scale := sel_registerName("nativeScale".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend;
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
// Read the screen scale up-front so callers can size font caches and
// textures with the right DPI before the window even exists.
screen := msg_o(UIScreen, sel_main_screen);
scale_d : f64 = msg_d(screen, sel_native_scale);
plat.dpi_scale = xx scale_d;
ctx_raw := msg_o(EAGLContext, sel_alloc);
plat.gl_ctx = msg_oi32(ctx_raw, sel_init_with_api, EAGL_API_GLES3);
msg_oo(EAGLContext, sel_set_current_ctx, plat.gl_ctx);
load_gl(@ios_gl_proc);
}
uikit_window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) {
if g_uikit_plat == null { return xx 0; }
g_uikit_plat.window;
}
uikit_window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) {
if g_uikit_plat == null { return; }
g_uikit_plat.window = w;
}
uikit_did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) {
result : u8 = 1;
inline if OS == .ios {
result = uikit_did_finish_launching_ios(self, app);
}
result;
}
uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
if g_uikit_plat == null {
NSLog(ns_string("[sx] no platform\n".ptr));
return 0;
}
plat := g_uikit_plat;
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);
sel_alloc := sel_registerName("alloc".ptr);
sel_init := sel_registerName("init".ptr);
sel_init_with_scene := sel_registerName("initWithWindowScene:".ptr);
sel_init_with_frame := sel_registerName("initWithFrame:".ptr);
sel_view := sel_registerName("view".ptr);
sel_set_root_vc := sel_registerName("setRootViewController:".ptr);
sel_make_key_visible := sel_registerName("makeKeyAndVisible".ptr);
sel_connected_scenes := sel_registerName("connectedScenes".ptr);
sel_any_object := sel_registerName("anyObject".ptr);
sel_add_subview := sel_registerName("addSubview:".ptr);
sel_set_frame := sel_registerName("setFrame:".ptr);
sel_bounds := sel_registerName("bounds".ptr);
sel_set_autoresizing := sel_registerName("setAutoresizingMask:".ptr);
sel_init_with_api := sel_registerName("initWithAPI:".ptr);
sel_set_current_ctx := sel_registerName("setCurrentContext:".ptr);
sel_layer := sel_registerName("layer".ptr);
sel_set_content_scale := sel_registerName("setContentScaleFactor:".ptr);
sel_screen := sel_registerName("screen".ptr);
sel_native_scale := sel_registerName("nativeScale".ptr);
sel_link_with_target := sel_registerName("displayLinkWithTarget:selector:".ptr);
sel_add_to_runloop := sel_registerName("addToRunLoop:forMode:".ptr);
sel_current_runloop := sel_registerName("currentRunLoop".ptr);
sel_tick := sel_registerName("sxTick:".ptr);
sel_safe_insets := sel_registerName("safeAreaInsets".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_v : (*void, *void) -> void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_ooo : (*void, *void, *void) -> *void = xx objc_msgSend;
msg_oso : (*void, *void, *void, *void) -> *void = xx objc_msgSend;
msg_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend;
msg_oou64 : (*void, *void, u64) -> void = xx objc_msgSend;
// CGFloat-returning msgSend. CGFloat is `double` on 64-bit Apple — reading
// it as f32 reads the low 32 bits of `d0` which isn't a valid float
// representation of the underlying double, so the value comes back as 0.
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
msg_odbl : (*void, *void, f64) -> void = xx objc_msgSend;
scenes := msg_o(app, sel_connected_scenes);
scene := msg_o(scenes, sel_any_object);
if scene == xx 0 {
NSLog(ns_string("[sx] no scene\n".ptr));
return 0;
}
win_raw := msg_o(UIWindow, sel_alloc);
plat.window = msg_ooo(win_raw, sel_init_with_scene, scene);
vc_raw := msg_o(UIViewController, sel_alloc);
plat.root_vc = msg_o(vc_raw, sel_init);
// 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);
msg_oo(plat.window, sel_set_root_vc, plat.root_vc);
plat.gl_layer = msg_o(plat.gl_view, sel_layer);
// 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);
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);
// 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);
}
// EAGLContext + load_gl were already done in uikit_create_gl_context()
// back when the game's main called plat.init() — so shaders/textures
// built before the window exists already work.
// Match the layer's drawable scale to the screen's native scale so we get
// pixel-accurate rendering on retina displays. CGFloat is `double` on
// 64-bit Apple platforms; reading as f32 would clobber the value.
screen := msg_o(plat.window, sel_screen);
scale := msg_d(screen, sel_native_scale);
plat.dpi_scale = xx scale;
msg_odbl(plat.gl_view, sel_set_content_scale, scale);
// Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once
// the layer has its real on-screen bounds. makeKeyAndVisible triggers
// a layout pass; layoutSubviews calls uikit_setup_renderbuffer.
msg_v(plat.window, sel_make_key_visible);
// Hidden UITextField as the firstResponder source for show_keyboard /
// hide_keyboard. Lives as a subview of the GL view so it's in the
// responder chain but is sized 0×0 so it can't be tapped.
UITextField := objc_getClass("UITextField".ptr);
sel_add_subview := sel_registerName("addSubview:".ptr);
tf_raw := msg_o(UITextField, sel_alloc);
plat.text_field = msg_o(tf_raw, sel_init);
msg_oo(plat.gl_view, sel_add_subview, plat.text_field);
// Subscribe SxAppDelegate to UIKeyboardWillChangeFrameNotification. We
// use the AppDelegate as the observer since it already exists; the IMP
// updates g_uikit_plat directly so we don't need ivar storage.
NSNotificationCenter := objc_getClass("NSNotificationCenter".ptr);
sel_default_center := sel_registerName("defaultCenter".ptr);
sel_add_observer := sel_registerName("addObserver:selector:name:object:".ptr);
msg_o5 : (*void, *void, *void, *void, *void, *void) -> void = xx objc_msgSend;
center := msg_o(NSNotificationCenter, sel_default_center);
msg_o5(center, sel_add_observer, delegate, sel_registerName("sxKeyboardWillChangeFrame:".ptr),
ns_string("UIKeyboardWillChangeFrameNotification".ptr), xx 0);
// CADisplayLink: vsync-driven tick into our SxGLView.
plat.display_link = msg_oso(CADisplayLink, sel_link_with_target, plat.gl_view, sel_tick);
runloop := msg_o(NSRunLoop, sel_current_runloop);
mode := ns_string("kCFRunLoopDefaultMode".ptr);
msg_oso(plat.display_link, sel_add_to_runloop, runloop, mode);
NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr));
1;
}
// Allocate the color renderbuffer + framebuffer and bind them. The renderbuffer
// gets its pixel storage from the CAEAGLLayer via
// `[ctx renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]`, so the layer
// must already be in the view hierarchy at the right size when this runs.
uikit_setup_renderbuffer :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
glGenFramebuffers(1, @plat.framebuffer);
glGenRenderbuffers(1, @plat.color_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, plat.color_renderbuffer);
sel_renderbuffer_storage := sel_registerName("renderbufferStorage:fromDrawable:".ptr);
msg_o_u32_o : (*void, *void, u32, *void) -> u8 = xx objc_msgSend;
msg_o_u32_o(plat.gl_ctx, sel_renderbuffer_storage, GL_RENDERBUFFER, plat.gl_layer);
glBindFramebuffer(GL_FRAMEBUFFER, plat.framebuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, plat.color_renderbuffer);
// Query the actual pixel dimensions from the renderbuffer.
pw : s32 = 0;
ph : s32 = 0;
GL_RENDERBUFFER_WIDTH :u32: 0x8D42;
GL_RENDERBUFFER_HEIGHT :u32: 0x8D43;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, @pw);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, @ph);
plat.pixel_w = pw;
plat.pixel_h = ph;
plat.viewport_w = xx pw;
plat.viewport_h = xx ph;
if plat.dpi_scale > 0.0 {
plat.viewport_w = plat.viewport_w / plat.dpi_scale;
plat.viewport_h = plat.viewport_h / plat.dpi_scale;
}
glViewport(0, 0, pw, ph);
status := glCheckFramebufferStatus(GL_FRAMEBUFFER);
if status != GL_FRAMEBUFFER_COMPLETE {
NSLog(ns_string("[sx] framebuffer incomplete after renderbuffer setup\n".ptr));
}
}
uikit_present_renderbuffer :: (self: *UIKitPlatform) {
inline if OS != .ios { return; }
glBindRenderbuffer(GL_RENDERBUFFER, self.color_renderbuffer);
sel_present := sel_registerName("presentRenderbuffer:".ptr);
msg_ou : (*void, *void, u32) -> u8 = xx objc_msgSend;
msg_ou(self.gl_ctx, sel_present, GL_RENDERBUFFER);
}
// ── SxGLView class ─────────────────────────────────────────────────────────
// UIView subclass overriding `+layerClass` to return [CAEAGLLayer class].
// Instance method `sxTick:` is what CADisplayLink calls.
uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
objc_getClass("CAEAGLLayer".ptr);
}
uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
if g_uikit_plat == null { return; }
plat := g_uikit_plat;
if !plat.has_frame_closure { return; }
if !plat.gl_initialized { return; }
sel_dur := sel_registerName("duration".ptr);
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
dur_d : f64 = msg_d(link, sel_dur);
plat.delta_time = xx dur_d;
fn := plat.frame_closure;
fn();
}
uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) {
// Call super first via objc_msgSendSuper would be cleaner, but UIView's
// default layoutSubviews is a no-op anyway.
if g_uikit_plat == null { return; }
plat := g_uikit_plat;
if plat.gl_initialized { return; }
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
// Event into the platform's queue for the next poll_events drain.
uikit_touch_location :: (touch: *void, view: *void) -> Point {
sel_location := sel_registerName("locationInView:".ptr);
msg_pt : (*void, *void, *void) -> CGPoint = xx objc_msgSend;
p := msg_pt(touch, sel_location, view);
Point.{ x = xx p.x, y = xx p.y };
}
uikit_first_touch :: (touches: *void) -> *void {
sel_any := sel_registerName("anyObject".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_o(touches, sel_any);
}
uikit_gl_view_touches_began :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {
if g_uikit_plat == null { return; }
touch := uikit_first_touch(touches);
if touch == null { return; }
pos := uikit_touch_location(touch, self);
g_uikit_plat.events.append(.mouse_down(.{ position = pos, button = .left }));
}
uikit_gl_view_touches_moved :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {
if g_uikit_plat == null { return; }
touch := uikit_first_touch(touches);
if touch == null { return; }
pos := uikit_touch_location(touch, self);
g_uikit_plat.events.append(.mouse_moved(.{ position = pos, delta = Point.zero() }));
}
uikit_gl_view_touches_ended :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {
if g_uikit_plat == null { return; }
touch := uikit_first_touch(touches);
if touch == null { return; }
pos := uikit_touch_location(touch, self);
g_uikit_plat.events.append(.mouse_up(.{ position = pos, button = .left }));
}
uikit_register_gl_view_class :: () {
inline if OS == .ios {
UIView := objc_getClass("UIView".ptr);
SxGLView := objc_allocateClassPair(UIView, "SxGLView".ptr, 0);
// +layerClass is a CLASS method — registered on the metaclass.
metaclass := object_getClass(SxGLView);
class_addMethod(metaclass,
sel_registerName("layerClass".ptr),
xx uikit_gl_view_layer_class, "#@:".ptr);
// -sxTick: is the CADisplayLink callback. -layoutSubviews allocates
// the renderbuffer when the layer first gets non-zero bounds.
class_addMethod(SxGLView,
sel_registerName("sxTick:".ptr),
xx uikit_gl_view_tick, "v@:@".ptr);
class_addMethod(SxGLView,
sel_registerName("layoutSubviews".ptr),
xx uikit_gl_view_layout, "v@:".ptr);
// Touch dispatch.
class_addMethod(SxGLView,
sel_registerName("touchesBegan:withEvent:".ptr),
xx uikit_gl_view_touches_began, "v@:@@".ptr);
class_addMethod(SxGLView,
sel_registerName("touchesMoved:withEvent:".ptr),
xx uikit_gl_view_touches_moved, "v@:@@".ptr);
class_addMethod(SxGLView,
sel_registerName("touchesEnded:withEvent:".ptr),
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
class_addMethod(SxGLView,
sel_registerName("touchesCancelled:withEvent:".ptr),
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
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);
}
}