platform: UIKit backend renders GLES3 via CAEAGLLayer + CADisplayLink
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.
This commit is contained in:
39
examples/66-uikit-platform.sx
Normal file
39
examples/66-uikit-platform.sx
Normal file
@@ -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));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
455
library/modules/platform/uikit.sx
Normal file
455
library/modules/platform/uikit.sx
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user