platform: UIKitPlatform end-to-end — chess game runs on iOS sim
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 <f64-call-result>` 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`).
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 :: ();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<UITouch *> + 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user