ffi 3.2 C3: migrate uikit.sx RunLoop + display-timing cluster

Third cluster: NSRunLoop and CADisplayLink move to declarative
`#objc_class` blocks.

Classes declared:

- NSRunLoop     → currentRunLoop (class)
- CADisplayLink → displayLinkWithTarget_selector (class),
                  addToRunLoop_forMode (instance),
                  targetTimestamp (instance), duration (instance)

The display-link instance is created with the new typed call shape:

    link := CADisplayLink.displayLinkWithTarget_selector(plat.gl_view, sel_tick);
    plat.display_link = xx link;  // keep the *void slot in the
                                   // platform struct for ABI parity
    runloop := NSRunLoop.currentRunLoop();
    link.addToRunLoop_forMode(runloop, mode_ns);

The `sxTick:` callback's `link: *void` param is cast to
`*CADisplayLink` at function entry so the body's `link.duration()` /
`link.targetTimestamp()` calls type-check.

Two now-redundant `objc_getClass(...)` lookups for CADisplayLink /
NSRunLoop are gone — the class slots come from the declarative
declarations via `__sx_objc_class_init`.

166/166 tests; chess builds clean on macOS / iOS / Android.
This commit is contained in:
agra
2026-05-25 17:15:27 +03:00
parent 17775b27a4
commit 2a7c8e0a6f
2 changed files with 36 additions and 12 deletions

View File

@@ -560,9 +560,17 @@ declared as `#foreign #objc_class` blocks. The 4-keyword
`addObserver:selector:name:object:` selector derives cleanly from `addObserver:selector:name:object:` selector derives cleanly from
the underscore-separated sx name (`addObserver_selector_name_object`). the underscore-separated sx name (`addObserver_selector_name_object`).
Phase 3.2 C3 landed: RunLoop + display-timing cluster. NSRunLoop
(`currentRunLoop`) and CADisplayLink
(`displayLinkWithTarget_selector`, `addToRunLoop_forMode`,
`targetTimestamp`, `duration`) declared as `#foreign #objc_class`
blocks. The `link` parameter on the `sxTick:` callback is now cast
to `*CADisplayLink` at function entry so subsequent method calls
type-check.
Open work: Open work:
- **Phase 3 step 3.2 — C3..C5** — uikit.sx migration, one cluster - **Phase 3 step 3.2 — C4..C5** — uikit.sx migration, two clusters
per commit, chess regression after each. remaining (UIKit chrome, view tree + GL).
test for the default-mangling table. Escape hatch for selectors test for the default-mangling table. Escape hatch for selectors
that don't fit the underscore-split rule (e.g. `tableView_ that don't fit the underscore-split rule (e.g. `tableView_
numberOfRowsInSection_` with an asymmetric keyword count). numberOfRowsInSection_` with an asymmetric keyword count).

View File

@@ -100,6 +100,19 @@ NSNotificationCenter :: #foreign #objc_class("NSNotificationCenter") {
addObserver_selector_name_object :: (self: *Self, observer: *void, sel: *void, name: *void, obj: *void); addObserver_selector_name_object :: (self: *Self, observer: *void, sel: *void, name: *void, obj: *void);
} }
// ── RunLoop + display timing (Phase 3.2 C3) ────────────────────────────
NSRunLoop :: #foreign #objc_class("NSRunLoop") {
currentRunLoop :: () -> *NSRunLoop;
}
CADisplayLink :: #foreign #objc_class("CADisplayLink") {
displayLinkWithTarget_selector :: (target: *void, sel: *void) -> *CADisplayLink;
addToRunLoop_forMode :: (self: *Self, runloop: *NSRunLoop, mode: *void);
targetTimestamp :: (self: *Self) -> f64;
duration :: (self: *Self) -> f64;
}
// GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's // GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's
// loader path (they live on the framework's symbol table directly). // loader path (they live on the framework's symbol table directly).
GL_RENDERBUFFER :u32: 0x8D41; GL_RENDERBUFFER :u32: 0x8D41;
@@ -534,8 +547,9 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
UIViewController := objc_getClass("UIViewController".ptr); UIViewController := objc_getClass("UIViewController".ptr);
SxGLView := objc_getClass("SxGLView".ptr); SxGLView := objc_getClass("SxGLView".ptr);
SxMetalView := objc_getClass("SxMetalView".ptr); SxMetalView := objc_getClass("SxMetalView".ptr);
CADisplayLink := objc_getClass("CADisplayLink".ptr); // CADisplayLink and NSRunLoop class objects come from the
NSRunLoop := objc_getClass("NSRunLoop".ptr); // declarative `#objc_class` slots populated by emit_llvm's
// __sx_objc_class_init constructor — no local objc_getClass needed.
win_raw := #objc_call(*void)(UIWindow, "alloc"); win_raw := #objc_call(*void)(UIWindow, "alloc");
plat.window = #objc_call(*void)(win_raw, "initWithWindowScene:", scene); plat.window = #objc_call(*void)(win_raw, "initWithWindowScene:", scene);
@@ -619,10 +633,11 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
// a SEL value (not a dispatch selector), so it still goes through // a SEL value (not a dispatch selector), so it still goes through
// sel_registerName. // sel_registerName.
sel_tick := sel_registerName("sxTick:".ptr); sel_tick := sel_registerName("sxTick:".ptr);
plat.display_link = #objc_call(*void)(CADisplayLink, "displayLinkWithTarget:selector:", plat.gl_view, sel_tick); link := CADisplayLink.displayLinkWithTarget_selector(plat.gl_view, sel_tick);
runloop := #objc_call(*void)(NSRunLoop, "currentRunLoop"); plat.display_link = xx link;
mode := ns_string("kCFRunLoopDefaultMode".ptr); runloop := NSRunLoop.currentRunLoop();
#objc_call(void)(plat.display_link, "addToRunLoop:forMode:", runloop, mode); mode_ns := ns_string("kCFRunLoopDefaultMode".ptr);
link.addToRunLoop_forMode(runloop, mode_ns);
NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr)); NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr));
} }
@@ -682,7 +697,8 @@ uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
objc_getClass("CAEAGLLayer".ptr); objc_getClass("CAEAGLLayer".ptr);
} }
uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) { uikit_gl_view_tick :: (self: *void, _cmd: *void, link_raw: *void) callconv(.c) {
link : *CADisplayLink = xx link_raw;
if g_uikit_plat == null { return; } if g_uikit_plat == null { return; }
plat := g_uikit_plat; plat := g_uikit_plat;
@@ -696,7 +712,7 @@ uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
// and keyboardLayoutGuide — none eliminated the lag without // and keyboardLayoutGuide — none eliminated the lag without
// cascade-breaking the GL view's frame. // cascade-breaking the GL view's frame.
if plat.kb_animating { if plat.kb_animating {
target_ts := #objc_call(f64)(link, "targetTimestamp"); target_ts := link.targetTimestamp();
elapsed := target_ts - plat.kb_anim_start; elapsed := target_ts - plat.kb_anim_start;
// Negative elapsed can happen if the just-fired willChangeFrame // Negative elapsed can happen if the just-fired willChangeFrame
// set kb_anim_start to a wall time AFTER the tick already // set kb_anim_start to a wall time AFTER the tick already
@@ -723,11 +739,11 @@ uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
if !plat.has_frame_closure { return; } if !plat.has_frame_closure { return; }
if !plat.gl_initialized { return; } if !plat.gl_initialized { return; }
dur_d := #objc_call(f64)(link, "duration"); dur_d := link.duration();
plat.delta_time = xx dur_d; plat.delta_time = xx dur_d;
// Stash the targetTimestamp so begin_frame can hand it down to the // Stash the targetTimestamp so begin_frame can hand it down to the
// game in FrameContext for Metal presentDrawable:atTime:. // game in FrameContext for Metal presentDrawable:atTime:.
plat.last_target_ts = #objc_call(f64)(link, "targetTimestamp"); plat.last_target_ts = link.targetTimestamp();
fn := plat.frame_closure; fn := plat.frame_closure;
fn(); fn();