From 362299331113fe1aba35400e26acc1608c59cc36 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 18 May 2026 08:42:22 +0300 Subject: [PATCH] ui: chess UI renders on iOS sim via Metal (scene lifecycle + alias fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- library/modules/platform/uikit.sx | 101 +++++++++++++++++++++--------- src/ir/lower.zig | 24 +++++-- src/target.zig | 22 +++++++ 3 files changed, 111 insertions(+), 36 deletions(-) diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index 0592ac4..bf1c6f9 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -280,18 +280,49 @@ uikit_register_classes :: () { 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); + // 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(); } @@ -408,17 +439,36 @@ uikit_window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) { } 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); + if g_uikit_plat != null { + uikit_subscribe_keyboard_notifications(self); + } } - result; + 1; } -uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { +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 0; + return; } plat := g_uikit_plat; @@ -437,8 +487,6 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { 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); @@ -468,16 +516,15 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { 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); + // 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); @@ -556,16 +603,9 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { 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); + // (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); @@ -574,7 +614,6 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { 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 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4055907..57ac0c3 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -7535,18 +7535,32 @@ pub const Lowering = struct { for (pd.methods) |method| { var ptypes = std.ArrayList(TypeId).empty; for (method.params) |p| { - // Resolve param type; Self → *void for protocol context + // Resolve param type; Self → *void for protocol context. + // Type aliases (e.g. `ShaderHandle :: u32`) need to be + // resolved through type_alias_map before falling through + // to type_bridge — otherwise they're treated as named + // empty structs and the LLVM call gets `{}` parameters. const pty = blk: { - if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) { - break :blk void_ptr_ty; + if (p.data == .type_expr) { + if (std.mem.eql(u8, p.data.type_expr.name, "Self")) { + break :blk void_ptr_ty; + } + if (self.type_alias_map.get(p.data.type_expr.name)) |aliased| { + break :blk aliased; + } } break :blk type_bridge.resolveAstType(p, table); }; ptypes.append(self.alloc, pty) catch unreachable; } const ret = if (method.return_type) |rt| blk: { - if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) { - break :blk void_ptr_ty; + if (rt.data == .type_expr) { + if (std.mem.eql(u8, rt.data.type_expr.name, "Self")) { + break :blk void_ptr_ty; + } + if (self.type_alias_map.get(rt.data.type_expr.name)) |aliased| { + break :blk aliased; + } } break :blk type_bridge.resolveAstType(rt, table); } else .void; diff --git a/src/target.zig b/src/target.zig index 122af20..be5661b 100644 --- a/src/target.zig +++ b/src/target.zig @@ -543,6 +543,11 @@ fn buildInfoPlist(allocator: std.mem.Allocator, exe_name: []const u8, bundle_id: const min_os: []const u8 = "14.0"; const is_sim = target_config.isIOSSimulator(); const platform_key: []const u8 = if (is_sim) "iPhoneSimulator" else "iPhoneOS"; + // UIApplicationSceneManifest opts the app into the iOS 13+ scene-based + // lifecycle. Without it, iOS 26 boots the app in `[rb-legacy]` mode and + // the CAMetalLayer never reaches the compositor. With the manifest + // declared (no UISceneDelegate listed), iOS auto-connects an implicit + // scene that our SxAppDelegate can find via `[app connectedScenes]`. return std.fmt.allocPrint(allocator, \\ \\ @@ -570,6 +575,23 @@ fn buildInfoPlist(allocator: std.mem.Allocator, exe_name: []const u8, bundle_id: \\ \\ UILaunchScreen \\ + \\ UIApplicationSceneManifest + \\ + \\ UIApplicationSupportsMultipleScenes + \\ + \\ UISceneConfigurations + \\ + \\ UIWindowSceneSessionRoleApplication + \\ + \\ + \\ UISceneConfigurationName + \\ Default Configuration + \\ UISceneDelegateClassName + \\ SxSceneDelegate + \\ + \\ + \\ + \\ \\ DTPlatformName \\ {s} \\