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:
agra
2026-05-18 08:42:22 +03:00
parent 63565e41ff
commit 3622993311
3 changed files with 111 additions and 36 deletions

View File

@@ -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;

View File

@@ -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>