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}
\\