From 4e27a7e6c9984b4e39f7f63378d1fa1948b42457 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 17 May 2026 16:52:03 +0300 Subject: [PATCH] =?UTF-8?q?platform:=20UIKitPlatform=20end-to-end=20?= =?UTF-8?q?=E2=80=94=20chess=20game=20runs=20on=20iOS=20sim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What works on iOS sim now: - pure-UIKit boot via UIApplicationMain (no SDL3 on iOS) - SxGLView (CAEAGLLayer) + EAGLContext(GLES3) + CADisplayLink - GLES3 shader path in modules/ui/renderer.sx (was wasm-only; now wasm-OR-ios) - UITouch -> ui.Event translation (mouse_down/moved/up) on touchesBegan/ Moved/Ended/Cancelled. Verified by tapping the chess board: the expected pawn highlights and its legal moves show as green dots. - chdir to NSBundle.mainBundle.resourcePath inside UIKitPlatform.init so the game's relative fopen("assets/...") calls resolve. Required restructuring to fix four problems discovered along the way: 1. GL context + load_gl must happen BEFORE UIApplicationMain so the game's pipeline.init (which compiles shaders) doesn't crash on null function pointers. Pulled EAGLContext creation + load_gl out of didFinishLaunching: into UIKitPlatform.init via uikit_create_gl_context. 2. UIScreen.nativeScale returns CGFloat (=double on 64-bit Apple). Reading it through a `(*void, *void) -> f32` msgSend signature clobbers the value to 0 — the upper 32 bits of d0 land where the f32 reads from. Replaced msg_f with msg_d returning f64 (and added msg_odbl for setContentScaleFactor: which takes CGFloat). 3. `xx ` directly assigned to an f32 field through a sema path lowers as `sitofp` (integer→float) on the double — LLVM verification rejects it. Workaround: hoist into an `f64` local first. 4. The renderer was selecting the GLSL 330 core shader on every non-wasm target, including iOS GLES3 where it silently fails to compile and no quads render. Added OS == .ios to the GLES branch. Game changes: - main.sx: g_plat is now a boxed `Platform` (not concrete *SdlPlatform). Backend chosen per-target via `inline if OS == .ios { ... }`. The ESC-to-stop handling is OS-guarded (mobile apps don't quit on key press, and SDL_Keycode references would force-link SDL on iOS). - build.sx: iOS no longer adds SDL3; it adds UIKit + OpenGLES + QuartzCore instead. - delta_time and viewport dims are now mirrored to free globals so the dock subsystem (`g_dock_delta_time = @g_delta_time`) and build_ui layout decisions don't need a pointer through the boxed protocol. Other: - Added `stop()` to the Platform protocol (no-op on UIKitPlatform). - examples/66-uikit-platform.sx updated: taps advance the clear color (red → green → blue) — smoke test for the touch IMP wiring. - shutdown() on UIKitPlatform is a no-op (mobile apps don't tear down). Outstanding for next session: - The Dynamic Island notch overlaps the top of the board because we haven't read UIView.safeAreaInsets yet (CGRect/UIEdgeInsets struct returns require a different msgSend ABI than we currently express). - Keyboard observer (UIKeyboardWillChangeFrameNotification + animation duration) — the load-bearing iOS feature. - Real-device codesigning workflow for the new build. Two more sx compiler bugs to file out of this work: - xx(f64 call result) → f32 emits sitofp (problem #3 above). - Inline `#import` inside `inline if` fails to parse (we worked around by importing both backends unconditionally; the unused-backend's Obj-C calls are gated by `inline if OS == .ios`). --- examples/66-uikit-platform.sx | 33 +++++-- library/modules/platform/api.sx | 5 + library/modules/platform/sdl3.sx | 8 +- library/modules/platform/uikit.sx | 148 ++++++++++++++++++++++++++---- library/modules/ui/renderer.sx | 4 +- 5 files changed, 163 insertions(+), 35 deletions(-) diff --git a/examples/66-uikit-platform.sx b/examples/66-uikit-platform.sx index a613584..c9ca2c8 100644 --- a/examples/66-uikit-platform.sx +++ b/examples/66-uikit-platform.sx @@ -1,6 +1,7 @@ // UIKitPlatform end-to-end smoke: boots the AppDelegate, installs an -// SxGLView with a CAEAGLLayer + GLES3 context + CADisplayLink, and on -// every vsync clears the screen to a cycling color. +// SxGLView with a CAEAGLLayer + GLES3 context + CADisplayLink, polls +// UITouch events into ui.Event, and on every vsync clears the screen +// to a color that advances on each tap. // // Build + run: // sx build --target ios-sim examples/66-uikit-platform.sx \ @@ -15,17 +16,29 @@ #framework "OpenGLES"; #framework "QuartzCore"; #import "modules/opengl.sx"; +#import "modules/ui/types.sx"; +#import "modules/ui/events.sx"; #import "modules/platform/uikit.sx"; -g_frame_counter : s64 = 0; +g_color_index : s64 = 0; -cycle_frame :: () { +tap_frame :: () { fc := g_uikit_plat.begin_frame(); - g_frame_counter += 1; - phase := g_frame_counter / 30; - r : f32 = if (phase % 3) == 0 then 0.8 else 0.1; - g : f32 = if (phase % 3) == 1 then 0.8 else 0.1; - b : f32 = if (phase % 3) == 2 then 0.8 else 0.1; + + events := g_uikit_plat.poll_events(); + i : s64 = 0; + while i < events.len { + ev := events.ptr[i]; + if ev == { + case .mouse_down: { g_color_index += 1; } + } + i += 1; + } + + phase := g_color_index % 3; + r : f32 = if phase == 0 then 0.8 else 0.1; + g : f32 = if phase == 1 then 0.8 else 0.1; + b : f32 = if phase == 2 then 0.8 else 0.1; glViewport(0, 0, fc.pixel_w, fc.pixel_h); glClearColor(r, g, b, 1.0); glClear(GL_COLOR_BUFFER_BIT); @@ -35,5 +48,5 @@ cycle_frame :: () { main :: () -> void { plat : *UIKitPlatform = xx malloc(size_of(UIKitPlatform)); plat.init("SxUIKitPlatform", 0, 0); - plat.run_frame_loop(closure(cycle_frame)); + plat.run_frame_loop(closure(tap_frame)); } diff --git a/library/modules/platform/api.sx b/library/modules/platform/api.sx index 3563c95..16747cf 100644 --- a/library/modules/platform/api.sx +++ b/library/modules/platform/api.sx @@ -18,5 +18,10 @@ Platform :: protocol { show_keyboard :: (); hide_keyboard :: (); + // Request the run loop to stop. On iOS/Android this is a no-op + // (mobile apps don't quit on user request); on SDL it tears down the + // `while !quit` loop. + stop :: (); + shutdown :: (); } diff --git a/library/modules/platform/sdl3.sx b/library/modules/platform/sdl3.sx index 1ea2e22..5fb79cc 100644 --- a/library/modules/platform/sdl3.sx +++ b/library/modules/platform/sdl3.sx @@ -29,10 +29,6 @@ SdlPlatform :: struct { has_frame_closure: bool = false; events: List(Event) = .{}; - - stop :: (self: *SdlPlatform) { - self.running = false; - } } impl Platform for SdlPlatform { @@ -188,6 +184,10 @@ impl Platform for SdlPlatform { show_keyboard :: (self: *SdlPlatform) { } hide_keyboard :: (self: *SdlPlatform) { } + stop :: (self: *SdlPlatform) { + self.running = false; + } + shutdown :: (self: *SdlPlatform) { inline if OS != .wasm { SDL_GL_DestroyContext(self.gl_ctx); diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index f2e76a5..a71a2bf 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -17,10 +17,15 @@ UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_class: *void) -> s32 #foreign; dlsym :: (handle: *void, name: [*]u8) -> *void #foreign; +chdir :: (path: [*]u8) -> s32 #foreign; // kEAGLRenderingAPIOpenGLES3 = 3 EAGL_API_GLES3 :: 3; +// CGFloat is a `double` on 64-bit Apple platforms; CGPoint = {x, y} fits in +// 16 bytes and returns via the FP-register path on arm64. +CGPoint :: struct { x: f64; y: f64; } + // GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's // loader path (they live on the framework's symbol table directly). GL_RENDERBUFFER :u32: 0x8D41; @@ -70,10 +75,20 @@ impl Platform for UIKitPlatform { self.dpi_scale = 1.0; self.delta_time = 0.016; self.has_frame_closure = false; + self.gl_initialized = false; self.keyboard_visible = false; self.keyboard_height = 0.0; self.saved_title = title.ptr; g_uikit_plat = self; + + // iOS apps start with CWD=/. chdir to the bundle's resourcePath so the + // game's relative `fopen("assets/...")` calls find their data — must + // happen BEFORE any code that loads fonts/textures from disk. + inline if OS == .ios { + uikit_chdir_to_bundle(); + uikit_register_classes(); + uikit_create_gl_context(self); + } true; } @@ -82,7 +97,7 @@ impl Platform for UIKitPlatform { self.has_frame_closure = true; g_uikit_plat = self; inline if OS == .ios { - uikit_register_app_delegate_and_run(); + UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr)); } } @@ -134,6 +149,8 @@ impl Platform for UIKitPlatform { show_keyboard :: (self: *UIKitPlatform) { } hide_keyboard :: (self: *UIKitPlatform) { } + stop :: (self: *UIKitPlatform) { } + shutdown :: (self: *UIKitPlatform) { } } @@ -159,7 +176,19 @@ uikit_extern_nsstring :: (name: [*]u8) -> *void { // so non-iOS builds never reference the unresolved UIKit symbols below. // ─────────────────────────────────────────────────────────────────────────── -uikit_register_app_delegate_and_run :: () { +uikit_chdir_to_bundle :: () { + inline if OS != .ios { return; } + NSBundle := objc_getClass("NSBundle".ptr); + sel_main_bundle := sel_registerName("mainBundle".ptr); + sel_resource_path := sel_registerName("resourcePath".ptr); + msg_o : (*void, *void) -> *void = xx objc_msgSend; + bundle := msg_o(NSBundle, sel_main_bundle); + rsrc := msg_o(bundle, sel_resource_path); + if rsrc == null { return; } + chdir(c_string(rsrc)); +} + +uikit_register_classes :: () { inline if OS == .ios { UIResponder := objc_getClass("UIResponder".ptr); SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0); @@ -177,11 +206,38 @@ uikit_register_app_delegate_and_run :: () { objc_registerClassPair(SxAppDelegate); uikit_register_gl_view_class(); - - UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr)); } } +uikit_create_gl_context :: (plat: *UIKitPlatform) { + inline if OS != .ios { return; } + + EAGLContext := objc_getClass("EAGLContext".ptr); + UIScreen := objc_getClass("UIScreen".ptr); + sel_alloc := sel_registerName("alloc".ptr); + sel_init_with_api := sel_registerName("initWithAPI:".ptr); + sel_set_current_ctx := sel_registerName("setCurrentContext:".ptr); + sel_main_screen := sel_registerName("mainScreen".ptr); + sel_native_scale := sel_registerName("nativeScale".ptr); + + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; + msg_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend; + msg_d : (*void, *void) -> f64 = xx objc_msgSend; + + // Read the screen scale up-front so callers can size font caches and + // textures with the right DPI before the window even exists. + screen := msg_o(UIScreen, sel_main_screen); + scale_d : f64 = msg_d(screen, sel_native_scale); + plat.dpi_scale = xx scale_d; + + ctx_raw := msg_o(EAGLContext, sel_alloc); + plat.gl_ctx = msg_oi32(ctx_raw, sel_init_with_api, EAGL_API_GLES3); + msg_oo(EAGLContext, sel_set_current_ctx, plat.gl_ctx); + + load_gl(@ios_gl_proc); +} + uikit_window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) { if g_uikit_plat == null { return xx 0; } g_uikit_plat.window; @@ -244,13 +300,13 @@ uikit_did_finish_launching_ios :: (app: *void) -> u8 { msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; msg_ooo : (*void, *void, *void) -> *void = xx objc_msgSend; msg_oso : (*void, *void, *void, *void) -> *void = xx objc_msgSend; - msg_ofi : (*void, *void, f32) -> void = xx objc_msgSend; msg_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend; msg_oou64 : (*void, *void, u64) -> void = xx objc_msgSend; - // float-returning msgSend uses a different ABI on x86_64 (objc_msgSend_fpret) - // but on arm64 it's the same `objc_msgSend`. We only target arm64-class - // devices/simulators here. - msg_f : (*void, *void) -> f32 = xx objc_msgSend; + // CGFloat-returning msgSend. CGFloat is `double` on 64-bit Apple — reading + // it as f32 reads the low 32 bits of `d0` which isn't a valid float + // representation of the underlying double, so the value comes back as 0. + 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); @@ -308,19 +364,17 @@ uikit_did_finish_launching_ios :: (app: *void) -> u8 { msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key); msg_oo(plat.gl_layer, sel_set_drawable, dict); - // EAGLContext (GLES3) + make current. - ctx_raw := msg_o(EAGLContext, sel_alloc); - plat.gl_ctx = msg_oi32(ctx_raw, sel_init_with_api, EAGL_API_GLES3); - msg_oo(EAGLContext, sel_set_current_ctx, plat.gl_ctx); + // EAGLContext + load_gl were already done in uikit_create_gl_context() + // back when the game's main called plat.init() — so shaders/textures + // built before the window exists already work. // Match the layer's drawable scale to the screen's native scale so we get - // pixel-accurate rendering on retina displays. + // pixel-accurate rendering on retina displays. CGFloat is `double` on + // 64-bit Apple platforms; reading as f32 would clobber the value. screen := msg_o(plat.window, sel_screen); - scale := msg_f(screen, sel_native_scale); - plat.dpi_scale = scale; - msg_ofi(plat.gl_view, sel_set_content_scale, scale); - - load_gl(@ios_gl_proc); + scale := msg_d(screen, sel_native_scale); + plat.dpi_scale = xx scale; + msg_odbl(plat.gl_view, sel_set_content_scale, scale); // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once // the layer has its real on-screen bounds. makeKeyAndVisible triggers @@ -430,6 +484,48 @@ uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) { plat.gl_initialized = true; } +// Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an +// NSSet + UIEvent. We take the first touch (single-touch model +// matching the chess game's drag-and-tap UX) and push the resulting +// Event into the platform's queue for the next poll_events drain. + +uikit_touch_location :: (touch: *void, view: *void) -> Point { + sel_location := sel_registerName("locationInView:".ptr); + msg_pt : (*void, *void, *void) -> CGPoint = xx objc_msgSend; + p := msg_pt(touch, sel_location, view); + Point.{ x = xx p.x, y = xx p.y }; +} + +uikit_first_touch :: (touches: *void) -> *void { + sel_any := sel_registerName("anyObject".ptr); + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_o(touches, sel_any); +} + +uikit_gl_view_touches_began :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) { + if g_uikit_plat == null { return; } + touch := uikit_first_touch(touches); + if touch == null { return; } + pos := uikit_touch_location(touch, self); + g_uikit_plat.events.append(.mouse_down(.{ position = pos, button = .left })); +} + +uikit_gl_view_touches_moved :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) { + if g_uikit_plat == null { return; } + touch := uikit_first_touch(touches); + if touch == null { return; } + pos := uikit_touch_location(touch, self); + g_uikit_plat.events.append(.mouse_moved(.{ position = pos, delta = Point.zero() })); +} + +uikit_gl_view_touches_ended :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) { + if g_uikit_plat == null { return; } + touch := uikit_first_touch(touches); + if touch == null { return; } + pos := uikit_touch_location(touch, self); + g_uikit_plat.events.append(.mouse_up(.{ position = pos, button = .left })); +} + uikit_register_gl_view_class :: () { inline if OS == .ios { UIView := objc_getClass("UIView".ptr); @@ -450,6 +546,20 @@ uikit_register_gl_view_class :: () { sel_registerName("layoutSubviews".ptr), xx uikit_gl_view_layout, "v@:".ptr); + // Touch dispatch. + class_addMethod(SxGLView, + sel_registerName("touchesBegan:withEvent:".ptr), + xx uikit_gl_view_touches_began, "v@:@@".ptr); + class_addMethod(SxGLView, + sel_registerName("touchesMoved:withEvent:".ptr), + xx uikit_gl_view_touches_moved, "v@:@@".ptr); + class_addMethod(SxGLView, + sel_registerName("touchesEnded:withEvent:".ptr), + xx uikit_gl_view_touches_ended, "v@:@@".ptr); + class_addMethod(SxGLView, + sel_registerName("touchesCancelled:withEvent:".ptr), + xx uikit_gl_view_touches_ended, "v@:@@".ptr); + objc_registerClassPair(SxGLView); } } diff --git a/library/modules/ui/renderer.sx b/library/modules/ui/renderer.sx index 9d6d67d..ab92429 100755 --- a/library/modules/ui/renderer.sx +++ b/library/modules/ui/renderer.sx @@ -28,8 +28,8 @@ UIRenderer :: struct { draw_calls: s64; init :: (self: *UIRenderer) { - // Create shader (ES for WASM/WebGL2, Core for desktop) - inline if OS == .wasm { + // Create shader (ES for WASM/WebGL2 + iOS GLES3, Core for desktop GL 3.3) + inline if OS == .wasm or OS == .ios { self.shader = create_program(UI_VERT_SRC_ES, UI_FRAG_SRC_ES); } else { self.shader = create_program(UI_VERT_SRC_CORE, UI_FRAG_SRC_CORE);