Files
sx/library/modules/platform/sdl3.sx
agra 4e27a7e6c9 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`).
2026-05-17 16:52:03 +03:00

225 lines
6.9 KiB
Plaintext

#import "modules/std.sx";
#import "modules/allocators.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/sdl3.sx";
#import "modules/wasm.sx";
#import "modules/ui/types.sx";
#import "modules/ui/events.sx";
#import "modules/platform/types.sx";
#import "modules/platform/api.sx";
g_sdl_plat : *SdlPlatform = null;
SdlPlatform :: struct {
window: *void = null;
gl_ctx: *void = null;
running: bool = true;
width: s32 = 0;
height: s32 = 0;
pixel_w: s32 = 0;
pixel_h: s32 = 0;
dpi_scale: f32 = 1.0;
delta_time: f32 = 0.008;
last_perf: u64 = 0;
frame_closure: Closure() = ---;
has_frame_closure: bool = false;
events: List(Event) = .{};
}
impl Platform for SdlPlatform {
init :: (self: *SdlPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
self.running = true;
self.has_frame_closure = false;
self.delta_time = 0.008;
self.dpi_scale = 1.0;
SDL_Init(SDL_INIT_VIDEO);
inline if OS == {
case .wasm: {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
}
case .ios: {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
}
else: {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
}
}
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
init_w := w;
init_h := h;
inline if OS == .wasm {
init_w = emscripten_run_script_int("window.innerWidth");
init_h = emscripten_run_script_int("window.innerHeight");
} else {
display_id := SDL_GetPrimaryDisplay();
bounds : SDL_Rect = ---;
if SDL_GetDisplayUsableBounds(display_id, @bounds) {
init_w = bounds.w * 3 / 4;
init_h = bounds.h * 3 / 4;
}
}
self.window = SDL_CreateWindow(title, init_w, init_h,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
if self.window == null { return false; }
self.gl_ctx = SDL_GL_CreateContext(self.window);
SDL_GL_MakeCurrent(self.window, self.gl_ctx);
SDL_GL_SetSwapInterval(0);
load_gl(@SDL_GL_GetProcAddress);
SDL_GetWindowSize(self.window, @self.width, @self.height);
SDL_GetWindowSizeInPixels(self.window, @self.pixel_w, @self.pixel_h);
wf : f32 = xx self.width;
pw : f32 = xx self.pixel_w;
self.dpi_scale = if wf > 0.0 then pw / wf else 1.0;
glViewport(0, 0, self.pixel_w, self.pixel_h);
g_sdl_plat = self;
self.last_perf = SDL_GetPerformanceCounter();
true;
}
run_frame_loop :: (self: *SdlPlatform, frame_fn: Closure()) {
self.frame_closure = frame_fn;
self.has_frame_closure = true;
g_sdl_plat = self;
inline if OS != .wasm {
SDL_AddEventWatch(@sdl_event_watch, xx self);
}
inline if OS == .wasm {
emscripten_set_main_loop(@sdl_wasm_tick, 0, 1);
} else {
while self.running {
frame_fn();
}
}
}
poll_events :: (self: *SdlPlatform) -> []Event {
self.events.len = 0;
sdl_event : SDL_Event = .none;
while SDL_PollEvent(@sdl_event) {
if sdl_event == {
case .quit: { self.running = false; }
case .window_resized: (data) {
self.width = data.data1;
self.height = data.data2;
SDL_GetWindowSizeInPixels(self.window, @self.pixel_w, @self.pixel_h);
}
}
ui_event := translate_sdl_event(@sdl_event);
if ui_event != .none {
self.events.append(ui_event);
}
}
result : []Event = ---;
result.ptr = self.events.items;
result.len = self.events.len;
result;
}
begin_frame :: (self: *SdlPlatform) -> FrameContext {
current := SDL_GetPerformanceCounter();
freq := SDL_GetPerformanceFrequency();
if freq > 0 and self.last_perf > 0 {
diff : f32 = xx (current - self.last_perf);
fr : f32 = xx freq;
self.delta_time = diff / fr;
}
self.last_perf = current;
inline if OS == .wasm {
new_w : s32 = 0;
new_h : s32 = 0;
SDL_GetWindowSize(self.window, @new_w, @new_h);
if new_w != self.width or new_h != self.height {
self.width = new_w;
self.height = new_h;
SDL_GetWindowSizeInPixels(self.window, @self.pixel_w, @self.pixel_h);
}
}
FrameContext.{
viewport_w = xx self.width,
viewport_h = xx self.height,
pixel_w = self.pixel_w,
pixel_h = self.pixel_h,
dpi_scale = self.dpi_scale,
delta_time = self.delta_time,
};
}
end_frame :: (self: *SdlPlatform) {
SDL_GL_SwapWindow(self.window);
}
safe_insets :: (self: *SdlPlatform) -> EdgeInsets {
EdgeInsets.zero();
}
keyboard :: (self: *SdlPlatform) -> KeyboardState {
KeyboardState.zero();
}
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);
SDL_DestroyWindow(self.window);
SDL_Quit();
}
}
}
// SDL fires the watch synchronously when events are added — including during
// macOS's modal resize-drag, when SDL_PollEvent can't run. Re-invoking the
// frame closure here keeps content rendering at the new size during the drag.
sdl_event_watch :: (userdata: *void, event: *SDL_Event) -> bool callconv(.c) {
plat : *SdlPlatform = xx userdata;
if event.* == {
case .window_resized: (data) {
plat.width = data.data1;
plat.height = data.data2;
SDL_GetWindowSizeInPixels(plat.window, @plat.pixel_w, @plat.pixel_h);
if plat.has_frame_closure {
fn := plat.frame_closure;
fn();
}
}
}
true;
}
sdl_wasm_tick :: () callconv(.c) {
if g_sdl_plat == null { return; }
if !g_sdl_plat.has_frame_closure { return; }
fn := g_sdl_plat.frame_closure;
fn();
}