Files
sx/library/modules/platform/uikit.sx
agra 3622993311 ui: chess UI renders on iOS sim via Metal (scene lifecycle + alias fix)
Four root causes for "chess UI shows white screen" — all fixed:

1. Hybrid legacy-app + scene-API path on iOS 26. Without
   UIApplicationSceneManifest in the Info.plist, iOS 26 booted us in
   [rb-legacy] mode and -[UIApplication connectedScenes] returned an
   empty set. didFinishLaunching's window-setup code bailed at "no scene"
   and the UIWindow never appeared on screen. Fix: emit the manifest in
   buildInfoPlist (src/target.zig) AND split the window/view/layer setup
   from didFinishLaunching into a new SxSceneDelegate's
   scene:willConnectToSession:options: IMP. didFinishLaunching now just
   subscribes the keyboard observer and returns YES.

2. UISceneDelegate formal protocol conformance. iOS 26 checks
   -[cls conformsToProtocol:@protocol(UISceneDelegate)] before
   instantiating the scene delegate; without it the runtime logs
   "SxSceneDelegate does not conform to the UISceneDelegate protocol"
   and silently uses a default delegate that does nothing. Fix:
   look up UISceneDelegate + UIWindowSceneDelegate via objc_getProtocol
   and class_addProtocol BEFORE objc_registerClassPair. The protocol
   metadata is present at link time (unlike UIApplicationDelegate per
   the long-standing legacy note in CHECKPOINT).

3. Protocol method return types via type aliases lowered as void.
   The GPU protocol declares `create_shader(...) -> ShaderHandle` where
   `ShaderHandle :: u32`. The protocol-decl lowering at lower.zig:7547
   passed the return AST node through type_bridge.resolveAstType which
   doesn't know about the type_alias_map. resolveTypeName fell through
   to its "assume named struct" branch and registered ShaderHandle as
   an empty struct ({ }). LLVM IR for the protocol call_indirect then
   read `call {} %fn_ptr(...)` — return value discarded; the
   subsequent abi.coerce load from a zero-init'd alloca yielded 0.
   Symptom: UIRenderer.mtl_shader = 0, set_shader sees state == null,
   the render-encoder fires draw with no pipeline state bound, GPU
   rejects the command buffer with MTLCommandBufferErrorInternal.
   Fix: at the protocol-decl method-type resolution sites in
   lower.zig, check type_alias_map BEFORE falling through to
   type_bridge.resolveAstType for both params and return type. A
   chess-side companion fix in /Users/agra/projects/game/main.sx
   (separate commit) memsets the MetalGPU struct after alloc so the
   List(*void) fields' len/cap/items aren't garbage.

After all four (this commit + memset companion in chess repo):
- 71/71 regression tests pass.
- Chess game now boots, scene-connects, ticks CADisplayLink, renders
  dark-gray clear + UI text + panel dividers every frame on iOS sim.
- Metal-clear example still renders.

Chess board + pieces visual contrast and faint-text-color are remaining
visual-polish items, not compiler/platform-setup issues.
2026-05-18 08:42:22 +03:00

852 lines
35 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("sxKeyboardWillChangeFrame:".ptr),
xx uikit_keyboard_will_change_frame, "v@:@".ptr);
objc_registerClassPair(SxAppDelegate);
// SxSceneDelegate handles the per-scene UI setup. iOS 13+ scene-based
// lifecycle: didFinishLaunching is too early for the window — the
// UIWindowScene doesn't connect until `scene:willConnectTo:options:`.
// The class is named in Info.plist's UIApplicationSceneManifest →
// UISceneDelegateClassName.
SxSceneDelegate := objc_allocateClassPair(UIResponder, "SxSceneDelegate".ptr, 0);
class_addMethod(SxSceneDelegate,
sel_registerName("scene:willConnectToSession:options:".ptr),
xx uikit_scene_will_connect, "v@:@@@".ptr);
class_addMethod(SxSceneDelegate,
sel_registerName("window".ptr),
xx uikit_window_getter, "@@:".ptr);
class_addMethod(SxSceneDelegate,
sel_registerName("setWindow:".ptr),
xx uikit_window_setter, "v@:@".ptr);
// Formal protocol conformance is required for UISceneDelegate
// (iOS checks -[cls conformsToProtocol:@protocol(UISceneDelegate)]
// before instantiating; without it the class is silently rejected
// with "does not conform to the UISceneDelegate protocol" in the
// log and a default scene with no delegate is created instead).
// Add the protocol BEFORE registerClassPair — the runtime locks
// the class layout after registration.
UISceneDelegateProto := objc_getProtocol("UISceneDelegate".ptr);
UIWindowSceneDelegateProto := objc_getProtocol("UIWindowSceneDelegate".ptr);
if UISceneDelegateProto != null {
class_addProtocol(SxSceneDelegate, UISceneDelegateProto);
} else {
NSLog(ns_string("[sx] WARN: UISceneDelegate protocol not found (dead-stripped)\n".ptr));
}
if UIWindowSceneDelegateProto != null {
class_addProtocol(SxSceneDelegate, UIWindowSceneDelegateProto);
}
objc_registerClassPair(SxSceneDelegate);
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) {
inline if OS == .ios {
if g_uikit_plat != null {
uikit_subscribe_keyboard_notifications(self);
}
}
1;
}
uikit_subscribe_keyboard_notifications :: (delegate: *void) {
inline if OS != .ios { return; }
NSNotificationCenter := objc_getClass("NSNotificationCenter".ptr);
sel_default_center := sel_registerName("defaultCenter".ptr);
sel_add_observer := sel_registerName("addObserver:selector:name:object:".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
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);
}
uikit_scene_will_connect :: (self: *void, _cmd: *void, scene: *void, session: *void, options: *void) callconv(.c) {
inline if OS == .ios {
uikit_scene_will_connect_ios(self, scene);
}
}
uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
if g_uikit_plat == null {
NSLog(ns_string("[sx] no platform\n".ptr));
return;
}
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_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;
win_raw := msg_o(UIWindow, sel_alloc);
plat.window = msg_ooo(win_raw, sel_init_with_scene, scene);
// Make the scene delegate own the window so iOS retains it. Per the
// scene-based lifecycle, the scene delegate is expected to provide the
// UIWindow via -window/-setWindow:.
sel_set_window := sel_registerName("setWindow:".ptr);
msg_oo(delegate, sel_set_window, plat.window);
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);
// (Keyboard observer is registered in didFinishLaunching via
// uikit_subscribe_keyboard_notifications — it's app-level, not scene-
// level, so it doesn't belong here.)
// 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));
}
// 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);
}
}