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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
@@ -570,6 +575,23 @@ fn buildInfoPlist(allocator: std.mem.Allocator, exe_name: []const u8, bundle_id:
|
||||
\\ <true/>
|
||||
\\ <key>UILaunchScreen</key>
|
||||
\\ <dict/>
|
||||
\\ <key>UIApplicationSceneManifest</key>
|
||||
\\ <dict>
|
||||
\\ <key>UIApplicationSupportsMultipleScenes</key>
|
||||
\\ <false/>
|
||||
\\ <key>UISceneConfigurations</key>
|
||||
\\ <dict>
|
||||
\\ <key>UIWindowSceneSessionRoleApplication</key>
|
||||
\\ <array>
|
||||
\\ <dict>
|
||||
\\ <key>UISceneConfigurationName</key>
|
||||
\\ <string>Default Configuration</string>
|
||||
\\ <key>UISceneDelegateClassName</key>
|
||||
\\ <string>SxSceneDelegate</string>
|
||||
\\ </dict>
|
||||
\\ </array>
|
||||
\\ </dict>
|
||||
\\ </dict>
|
||||
\\ <key>DTPlatformName</key>
|
||||
\\ <string>{s}</string>
|
||||
\\</dict>
|
||||
|
||||
Reference in New Issue
Block a user