From 858d691181be4fc43561c8e7861c0eec99d09e90 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 17 May 2026 15:51:57 +0300 Subject: [PATCH] platform: UIKit backend renders GLES3 via CAEAGLLayer + CADisplayLink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end on iOS sim: UIKitPlatform boots an SxAppDelegate, installs an SxGLView (UIView subclass overriding +layerClass to return CAEAGLLayer) as the root view controller's view, sets the drawable properties (EAGLColorFormatRGBA8, non-retained backing — looked up by dlsym so pointer-identity-checked constants match), creates an EAGLContext (GLES3), and registers a CADisplayLink that invokes the user's frame closure on every vsync. end_frame presents the renderbuffer via [EAGLContext presentRenderbuffer:]. The renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once the layer has its real on-screen bounds — allocating earlier (e.g. in didFinishLaunching) failed with INCOMPLETE_ATTACHMENT because the SxGLView's frame was still zero at that point. Setting the SxGLView as the VC's `view` (via setView:) lets the standard VC layout pipeline size it to the window without us having to read CGRect struct returns from objc_msgSend. EAGL drawableProperties dict keys/values are dlsym'd from OpenGLES — the framework checks them by pointer identity, so synthesized NSString literals with the same contents don't work. examples/66-uikit-platform.sx — runnable smoke test that cycles the screen color (red → green → blue every 30 frames) so you can confirm the display-link tick and present pipeline. modules/opengl.sx gains glGenFramebuffers, glGenRenderbuffers, glBindFramebuffer, glBindRenderbuffer, glFramebufferRenderbuffer, glGetRenderbufferParameteriv, glCheckFramebufferStatus — needed for the iOS GLES FBO-to-renderbuffer setup. They're wired into load_gl so SDL and the iOS dlsym loader both pick them up. Compiles cleanly on macOS / WASM / iOS-sim. Non-iOS targets never reference the unresolved UIKit/QuartzCore/OpenGLES symbols because every Obj-C touch lives inside `inline if OS == .ios`. Game's iOS path still goes through SDL3 for now. Touch events + game wire-up + keyboard observer = next steps. --- examples/66-uikit-platform.sx | 39 +++ library/modules/opengl.sx | 16 ++ library/modules/platform/uikit.sx | 455 ++++++++++++++++++++++++++++++ 3 files changed, 510 insertions(+) create mode 100644 examples/66-uikit-platform.sx create mode 100644 library/modules/platform/uikit.sx diff --git a/examples/66-uikit-platform.sx b/examples/66-uikit-platform.sx new file mode 100644 index 0000000..a613584 --- /dev/null +++ b/examples/66-uikit-platform.sx @@ -0,0 +1,39 @@ +// 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. +// +// Build + run: +// sx build --target ios-sim examples/66-uikit-platform.sx \ +// -o /tmp/SxUIKitBoot --bundle /tmp/SxUIKitBoot.app \ +// --bundle-id co.swipelab.sxuikit -F ~/Library/Frameworks +// xcrun simctl install booted /tmp/SxUIKitBoot.app +// xcrun simctl launch --console booted co.swipelab.sxuikit +// xcrun simctl io booted screenshot /tmp/screen.png + +#import "modules/std.sx"; +#import "modules/std/uikit.sx"; +#framework "OpenGLES"; +#framework "QuartzCore"; +#import "modules/opengl.sx"; +#import "modules/platform/uikit.sx"; + +g_frame_counter : s64 = 0; + +cycle_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; + glViewport(0, 0, fc.pixel_w, fc.pixel_h); + glClearColor(r, g, b, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + g_uikit_plat.end_frame(); +} + +main :: () -> void { + plat : *UIKitPlatform = xx malloc(size_of(UIKitPlatform)); + plat.init("SxUIKitPlatform", 0, 0); + plat.run_frame_loop(closure(cycle_frame)); +} diff --git a/library/modules/opengl.sx b/library/modules/opengl.sx index c1b3888..8ff7d1d 100644 --- a/library/modules/opengl.sx +++ b/library/modules/opengl.sx @@ -90,6 +90,14 @@ glPixelStorei : (u32, s32) -> void = ---; glTexSubImage2D : (u32, s32, s32, s32, s32, s32, u32, u32, *void) -> void = ---; glDeleteTextures : (s32, *u32) -> void = ---; +glGenFramebuffers : (s32, *u32) -> void = ---; +glGenRenderbuffers : (s32, *u32) -> void = ---; +glBindFramebuffer : (u32, u32) -> void = ---; +glBindRenderbuffer : (u32, u32) -> void = ---; +glFramebufferRenderbuffer : (u32, u32, u32, u32) -> void = ---; +glGetRenderbufferParameteriv : (u32, u32, *s32) -> void = ---; +glCheckFramebufferStatus : (u32) -> u32 = ---; + GL_TEXTURE_WRAP_S :u32: 0x2802; GL_TEXTURE_WRAP_T :u32: 0x2803; GL_CLAMP_TO_EDGE :u32: 0x812F; @@ -143,6 +151,14 @@ load_gl :: (get_proc: ([*]u8) -> *void) { glPixelStorei = xx get_proc("glPixelStorei"); glTexSubImage2D = xx get_proc("glTexSubImage2D"); glDeleteTextures = xx get_proc("glDeleteTextures"); + + glGenFramebuffers = xx get_proc("glGenFramebuffers"); + glGenRenderbuffers = xx get_proc("glGenRenderbuffers"); + glBindFramebuffer = xx get_proc("glBindFramebuffer"); + glBindRenderbuffer = xx get_proc("glBindRenderbuffer"); + glFramebufferRenderbuffer = xx get_proc("glFramebufferRenderbuffer"); + glGetRenderbufferParameteriv = xx get_proc("glGetRenderbufferParameteriv"); + glCheckFramebufferStatus = xx get_proc("glCheckFramebufferStatus"); } diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx new file mode 100644 index 0000000..f2e76a5 --- /dev/null +++ b/library/modules/platform/uikit.sx @@ -0,0 +1,455 @@ +// Pure UIKit + CAEAGLLayer + CADisplayLink backend for iOS. +// +// Linking is per-target via the game's build.sx (`opts.add_framework("UIKit")` +// + `opts.add_framework("OpenGLES")` + `opts.add_framework("QuartzCore")` on +// `.ios`). The file compiles cleanly on every target — the UIKit-touching +// bodies live behind `inline if OS == .ios` guards, so non-iOS builds never +// reach the unresolved Obj-C symbols. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; +#import "modules/opengl.sx"; +#import "modules/ui/types.sx"; +#import "modules/ui/events.sx"; +#import "modules/platform/types.sx"; +#import "modules/platform/api.sx"; + +UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_class: *void) -> s32 #foreign; +dlsym :: (handle: *void, name: [*]u8) -> *void #foreign; + +// kEAGLRenderingAPIOpenGLES3 = 3 +EAGL_API_GLES3 :: 3; + +// 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; +GL_FRAMEBUFFER :u32: 0x8D40; +GL_COLOR_ATTACHMENT0 :u32: 0x8CE0; +GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5; + +g_uikit_plat : *UIKitPlatform = null; + +UIKitPlatform :: struct { + window: *void = null; // UIWindow* + root_vc: *void = null; // UIViewController* + gl_view: *void = null; // SxGLView* + gl_layer: *void = null; // CAEAGLLayer* (= gl_view.layer) + gl_ctx: *void = null; // EAGLContext* + display_link: *void = null; + color_renderbuffer: u32 = 0; + framebuffer: u32 = 0; + gl_initialized: bool = false; + + viewport_w: f32 = 0.0; + viewport_h: f32 = 0.0; + pixel_w: s32 = 0; + pixel_h: s32 = 0; + dpi_scale: f32 = 1.0; + + delta_time: f32 = 0.016; + + frame_closure: Closure() = ---; + has_frame_closure: bool = false; + + events: List(Event) = .{}; + + safe_top: f32 = 0.0; + safe_left: f32 = 0.0; + safe_bottom: f32 = 0.0; + safe_right: f32 = 0.0; + + keyboard_visible: bool = false; + keyboard_height: f32 = 0.0; + + saved_title: [*]u8 = null; +} + +impl Platform for UIKitPlatform { + init :: (self: *UIKitPlatform, title: [:0]u8, w: s32, h: s32) -> bool { + self.dpi_scale = 1.0; + self.delta_time = 0.016; + self.has_frame_closure = false; + self.keyboard_visible = false; + self.keyboard_height = 0.0; + self.saved_title = title.ptr; + g_uikit_plat = self; + true; + } + + run_frame_loop :: (self: *UIKitPlatform, frame_fn: Closure()) { + self.frame_closure = frame_fn; + self.has_frame_closure = true; + g_uikit_plat = self; + inline if OS == .ios { + uikit_register_app_delegate_and_run(); + } + } + + poll_events :: (self: *UIKitPlatform) -> []Event { + result : []Event = ---; + result.ptr = self.events.items; + result.len = self.events.len; + self.events.len = 0; + result; + } + + begin_frame :: (self: *UIKitPlatform) -> FrameContext { + FrameContext.{ + viewport_w = self.viewport_w, + viewport_h = self.viewport_h, + pixel_w = self.pixel_w, + pixel_h = self.pixel_h, + dpi_scale = self.dpi_scale, + delta_time = self.delta_time, + }; + } + + end_frame :: (self: *UIKitPlatform) { + inline if OS == .ios { + uikit_present_renderbuffer(self); + } + } + + safe_insets :: (self: *UIKitPlatform) -> EdgeInsets { + bottom := self.safe_bottom; + if self.keyboard_visible { + if self.keyboard_height > bottom { bottom = self.keyboard_height; } + } + EdgeInsets.{ + top = self.safe_top, + left = self.safe_left, + bottom = bottom, + right = self.safe_right, + }; + } + + keyboard :: (self: *UIKitPlatform) -> KeyboardState { + KeyboardState.{ + visible = self.keyboard_visible, + height = self.keyboard_height, + }; + } + + show_keyboard :: (self: *UIKitPlatform) { } + hide_keyboard :: (self: *UIKitPlatform) { } + + shutdown :: (self: *UIKitPlatform) { } +} + +// dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2. +ios_gl_proc :: (name: [*]u8) -> *void { + rtld_default : *void = xx (0 - 2); + dlsym(rtld_default, name); +} + +// Read a `extern NSString * const k...` global from the loaded image. The +// extern variable holds a pointer to the NSString instance — dlsym returns +// the address of that variable, which we dereference. +uikit_extern_nsstring :: (name: [*]u8) -> *void { + rtld_default : *void = xx (0 - 2); + p := dlsym(rtld_default, name); + if p == null { return null; } + pp : **void = xx p; + pp.*; +} + +// ─────────────────────────────────────────────────────────────────────────── +// iOS-only helpers — only reachable from `inline if OS == .ios` call sites, +// so non-iOS builds never reference the unresolved UIKit symbols below. +// ─────────────────────────────────────────────────────────────────────────── + +uikit_register_app_delegate_and_run :: () { + inline if OS == .ios { + UIResponder := objc_getClass("UIResponder".ptr); + SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0); + + class_addMethod(SxAppDelegate, + sel_registerName("application:didFinishLaunchingWithOptions:".ptr), + xx uikit_did_finish_launching, "c@:@@".ptr); + class_addMethod(SxAppDelegate, + sel_registerName("window".ptr), + xx uikit_window_getter, "@@:".ptr); + class_addMethod(SxAppDelegate, + sel_registerName("setWindow:".ptr), + xx uikit_window_setter, "v@:@".ptr); + + objc_registerClassPair(SxAppDelegate); + + uikit_register_gl_view_class(); + + UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr)); + } +} + +uikit_window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) { + if g_uikit_plat == null { return xx 0; } + g_uikit_plat.window; +} + +uikit_window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) { + if g_uikit_plat == null { return; } + g_uikit_plat.window = w; +} + +uikit_did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) { + result : u8 = 1; + inline if OS == .ios { + result = uikit_did_finish_launching_ios(app); + } + result; +} + +uikit_did_finish_launching_ios :: (app: *void) -> u8 { + if g_uikit_plat == null { + NSLog(ns_string("[sx] no platform\n".ptr)); + return 0; + } + plat := g_uikit_plat; + + UIWindow := objc_getClass("UIWindow".ptr); + UIViewController := objc_getClass("UIViewController".ptr); + SxGLView := objc_getClass("SxGLView".ptr); + EAGLContext := objc_getClass("EAGLContext".ptr); + CADisplayLink := objc_getClass("CADisplayLink".ptr); + NSRunLoop := objc_getClass("NSRunLoop".ptr); + + sel_alloc := sel_registerName("alloc".ptr); + sel_init := sel_registerName("init".ptr); + sel_init_with_scene := sel_registerName("initWithWindowScene:".ptr); + sel_init_with_frame := sel_registerName("initWithFrame:".ptr); + sel_view := sel_registerName("view".ptr); + sel_set_root_vc := sel_registerName("setRootViewController:".ptr); + sel_make_key_visible := sel_registerName("makeKeyAndVisible".ptr); + sel_connected_scenes := sel_registerName("connectedScenes".ptr); + sel_any_object := sel_registerName("anyObject".ptr); + sel_add_subview := sel_registerName("addSubview:".ptr); + sel_set_frame := sel_registerName("setFrame:".ptr); + sel_bounds := sel_registerName("bounds".ptr); + sel_set_autoresizing := sel_registerName("setAutoresizingMask:".ptr); + sel_init_with_api := sel_registerName("initWithAPI:".ptr); + sel_set_current_ctx := sel_registerName("setCurrentContext:".ptr); + sel_layer := sel_registerName("layer".ptr); + sel_set_content_scale := sel_registerName("setContentScaleFactor:".ptr); + sel_screen := sel_registerName("screen".ptr); + sel_native_scale := sel_registerName("nativeScale".ptr); + sel_link_with_target := sel_registerName("displayLinkWithTarget:selector:".ptr); + sel_add_to_runloop := sel_registerName("addToRunLoop:forMode:".ptr); + sel_current_runloop := sel_registerName("currentRunLoop".ptr); + sel_tick := sel_registerName("sxTick:".ptr); + sel_safe_insets := sel_registerName("safeAreaInsets".ptr); + + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_v : (*void, *void) -> void = xx objc_msgSend; + 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; + + scenes := msg_o(app, sel_connected_scenes); + scene := msg_o(scenes, sel_any_object); + if scene == xx 0 { + NSLog(ns_string("[sx] no scene\n".ptr)); + return 0; + } + + win_raw := msg_o(UIWindow, sel_alloc); + plat.window = msg_ooo(win_raw, sel_init_with_scene, scene); + + vc_raw := msg_o(UIViewController, sel_alloc); + plat.root_vc = msg_o(vc_raw, sel_init); + + // Allocate SxGLView and install it as the VC's view, so the standard + // ViewController layout pipeline sizes the GL view to the window. Setting + // it BEFORE setRootViewController avoids the VC lazy-loading a default + // view first. + glv_raw := msg_o(SxGLView, sel_alloc); + plat.gl_view = msg_o(glv_raw, sel_init); + sel_set_view := sel_registerName("setView:".ptr); + msg_oo(plat.root_vc, sel_set_view, plat.gl_view); + + msg_oo(plat.window, sel_set_root_vc, plat.root_vc); + + plat.gl_layer = msg_o(plat.gl_view, sel_layer); + + // Mark the layer opaque (no compositor blend) + set the drawable properties + // required by EAGLContext.renderbufferStorage:fromDrawable: (color format, + // non-retained backing). Without this dict the renderbuffer allocation + // silently fails and the framebuffer reports INCOMPLETE. + sel_set_opaque := sel_registerName("setOpaque:".ptr); + msg_obool : (*void, *void, u8) -> void = xx objc_msgSend; + msg_obool(plat.gl_layer, sel_set_opaque, 1); + + NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr); + NSNumber := objc_getClass("NSNumber".ptr); + sel_dictionary := sel_registerName("dictionary".ptr); + sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr); + sel_number_bool := sel_registerName("numberWithBool:".ptr); + sel_set_drawable := sel_registerName("setDrawableProperties:".ptr); + + msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend; + ns_no := msg_oio(NSNumber, sel_number_bool, 0); + + // The EAGL dict keys/values must be the framework-provided NSString + // constants (pointer identity is checked) — dlsym them from OpenGLES. + retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr); + colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr); + rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr); + + dict := msg_o(NSMutableDictionary, sel_dictionary); + msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend; + msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key); + 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); + + // Match the layer's drawable scale to the screen's native scale so we get + // pixel-accurate rendering on retina displays. + 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); + + // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once + // the layer has its real on-screen bounds. makeKeyAndVisible triggers + // a layout pass; layoutSubviews calls uikit_setup_renderbuffer. + msg_v(plat.window, sel_make_key_visible); + + // Safe insets stay zero for now — struct-return ABI for `safeAreaInsets` + // (UIEdgeInsets = 4 CGFloats) isn't expressible without dedicated + // msgSend_stret plumbing. Revisit when keyboard sync needs it. + plat.safe_top = 0.0; + plat.safe_left = 0.0; + plat.safe_bottom = 0.0; + plat.safe_right = 0.0; + + // CADisplayLink: vsync-driven tick into our SxGLView. + plat.display_link = msg_oso(CADisplayLink, sel_link_with_target, plat.gl_view, sel_tick); + runloop := msg_o(NSRunLoop, sel_current_runloop); + mode := ns_string("kCFRunLoopDefaultMode".ptr); + msg_oso(plat.display_link, sel_add_to_runloop, runloop, mode); + + NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr)); + 1; +} + +// Allocate the color renderbuffer + framebuffer and bind them. The renderbuffer +// gets its pixel storage from the CAEAGLLayer via +// `[ctx renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]`, so the layer +// must already be in the view hierarchy at the right size when this runs. +uikit_setup_renderbuffer :: (plat: *UIKitPlatform) { + inline if OS != .ios { return; } + + glGenFramebuffers(1, @plat.framebuffer); + glGenRenderbuffers(1, @plat.color_renderbuffer); + + glBindRenderbuffer(GL_RENDERBUFFER, plat.color_renderbuffer); + + sel_renderbuffer_storage := sel_registerName("renderbufferStorage:fromDrawable:".ptr); + msg_o_u32_o : (*void, *void, u32, *void) -> u8 = xx objc_msgSend; + msg_o_u32_o(plat.gl_ctx, sel_renderbuffer_storage, GL_RENDERBUFFER, plat.gl_layer); + + glBindFramebuffer(GL_FRAMEBUFFER, plat.framebuffer); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, plat.color_renderbuffer); + + // Query the actual pixel dimensions from the renderbuffer. + pw : s32 = 0; + ph : s32 = 0; + GL_RENDERBUFFER_WIDTH :u32: 0x8D42; + GL_RENDERBUFFER_HEIGHT :u32: 0x8D43; + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, @pw); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, @ph); + plat.pixel_w = pw; + plat.pixel_h = ph; + plat.viewport_w = xx pw; + plat.viewport_h = xx ph; + if plat.dpi_scale > 0.0 { + plat.viewport_w = plat.viewport_w / plat.dpi_scale; + plat.viewport_h = plat.viewport_h / plat.dpi_scale; + } + + glViewport(0, 0, pw, ph); + + status := glCheckFramebufferStatus(GL_FRAMEBUFFER); + if status != GL_FRAMEBUFFER_COMPLETE { + NSLog(ns_string("[sx] framebuffer incomplete after renderbuffer setup\n".ptr)); + } +} + +uikit_present_renderbuffer :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + glBindRenderbuffer(GL_RENDERBUFFER, self.color_renderbuffer); + sel_present := sel_registerName("presentRenderbuffer:".ptr); + msg_ou : (*void, *void, u32) -> u8 = xx objc_msgSend; + msg_ou(self.gl_ctx, sel_present, GL_RENDERBUFFER); +} + +// ── SxGLView class ───────────────────────────────────────────────────────── +// UIView subclass overriding `+layerClass` to return [CAEAGLLayer class]. +// Instance method `sxTick:` is what CADisplayLink calls. + +uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) { + objc_getClass("CAEAGLLayer".ptr); +} + +uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) { + if g_uikit_plat == null { return; } + plat := g_uikit_plat; + if !plat.has_frame_closure { return; } + if !plat.gl_initialized { return; } + + // Pull this frame's duration from the display link. + sel_dur := sel_registerName("duration".ptr); + msg_d : (*void, *void) -> f64 = xx objc_msgSend; + d := msg_d(link, sel_dur); + plat.delta_time = xx d; + + fn := plat.frame_closure; + fn(); +} + +uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) { + // Call super first via objc_msgSendSuper would be cleaner, but UIView's + // default layoutSubviews is a no-op anyway. + if g_uikit_plat == null { return; } + plat := g_uikit_plat; + if plat.gl_initialized { return; } + uikit_setup_renderbuffer(plat); + plat.gl_initialized = true; +} + +uikit_register_gl_view_class :: () { + inline if OS == .ios { + UIView := objc_getClass("UIView".ptr); + SxGLView := objc_allocateClassPair(UIView, "SxGLView".ptr, 0); + + // +layerClass is a CLASS method — registered on the metaclass. + metaclass := object_getClass(SxGLView); + class_addMethod(metaclass, + sel_registerName("layerClass".ptr), + xx uikit_gl_view_layer_class, "#@:".ptr); + + // -sxTick: is the CADisplayLink callback. -layoutSubviews allocates + // the renderbuffer when the layer first gets non-zero bounds. + class_addMethod(SxGLView, + sel_registerName("sxTick:".ptr), + xx uikit_gl_view_tick, "v@:@".ptr); + class_addMethod(SxGLView, + sel_registerName("layoutSubviews".ptr), + xx uikit_gl_view_layout, "v@:".ptr); + + objc_registerClassPair(SxGLView); + } +}