938 lines
39 KiB
Plaintext
938 lines
39 KiB
Plaintext
// 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;
|
||
chdir :: (path: [*]u8) -> s32 #foreign;
|
||
|
||
// QuartzCore's wall-clock helper used by CoreAnimation. Seconds since boot,
|
||
// monotonic. We use it as the timebase for keyboard-inset lockstep so the
|
||
// per-frame interpolation lines up with UIKit's own animation timestamp.
|
||
CACurrentMediaTime :: () -> f64 #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; }
|
||
|
||
// UIEdgeInsets = {top, left, bottom, right} CGFloats — 32 bytes; exceeds the
|
||
// 16-byte registers cutoff so it returns via the x8 indirect-result-pointer
|
||
// convention. sx generates the right call sequence when the function-pointer
|
||
// type declares it as a by-value struct return.
|
||
UIEdgeInsets :: struct {
|
||
top: f64;
|
||
left: f64;
|
||
bottom: f64;
|
||
right: f64;
|
||
}
|
||
|
||
// CGRect for unwrapping NSValue-wrapped rects (e.g. keyboard end frame). Same
|
||
// 32-byte indirect-return shape as UIEdgeInsets.
|
||
CGRect :: struct {
|
||
x: f64;
|
||
y: f64;
|
||
width: f64;
|
||
height: 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;
|
||
GL_FRAMEBUFFER :u32: 0x8D40;
|
||
GL_COLOR_ATTACHMENT0 :u32: 0x8CE0;
|
||
GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5;
|
||
|
||
g_uikit_plat : *UIKitPlatform = null;
|
||
|
||
// Which GPU API the UIKit backend wires up. `.gles` keeps the existing
|
||
// CAEAGLLayer + EAGLContext + renderbuffer path; `.metal` swaps the view
|
||
// for a CAMetalLayer-backed one and leaves rendering to MetalGPU. Set
|
||
// before calling `init`; default `.gles` preserves prior behavior.
|
||
GpuMode :: enum {
|
||
gles;
|
||
metal;
|
||
}
|
||
|
||
UIKitPlatform :: struct {
|
||
window: *void = null; // UIWindow*
|
||
root_vc: *void = null; // UIViewController*
|
||
gl_view: *void = null; // SxGLView* OR SxMetalView* (depending on gpu_mode)
|
||
gl_layer: *void = null; // CAEAGLLayer* OR CAMetalLayer* (= gl_view.layer)
|
||
gl_ctx: *void = null; // EAGLContext* (null in metal mode)
|
||
display_link: *void = null;
|
||
color_renderbuffer: u32 = 0;
|
||
framebuffer: u32 = 0;
|
||
gl_initialized: bool = false;
|
||
gpu_mode: GpuMode = .gles;
|
||
|
||
// Hidden UITextField; firstResponder ⇆ keyboard visibility.
|
||
text_field: *void = null;
|
||
|
||
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;
|
||
// Latest CADisplayLink.targetTimestamp captured each tick — forwarded
|
||
// through FrameContext to MetalGPU.end_frame() for presentDrawable:atTime:.
|
||
last_target_ts: f64 = 0.0;
|
||
|
||
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;
|
||
|
||
// Keyboard inset lockstep: when willChangeFrame fires we read the
|
||
// animation duration from the notification's userInfo and interpolate
|
||
// `keyboard_height` over that window on each display-link tick. Each
|
||
// animation has a fresh start time and target — if a second event
|
||
// arrives mid-animation, the next interpolation starts from the
|
||
// currently-interpolated value (not from the previous animation's
|
||
// origin). The easing is `smoothstep` (cubic Hermite) which closely
|
||
// approximates UIKit's keyboard curve to within a frame at the
|
||
// standard 0.25s slide duration.
|
||
kb_anim_from: f32 = 0.0;
|
||
kb_anim_to: f32 = 0.0;
|
||
kb_anim_start: f64 = 0.0;
|
||
kb_anim_dur: f64 = 0.0;
|
||
kb_anim_curve: u64 = 0;
|
||
kb_animating: bool = false;
|
||
|
||
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.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();
|
||
if self.gpu_mode == .gles {
|
||
uikit_create_gl_context(self);
|
||
} else {
|
||
// Metal mode: skip EAGL. dpi_scale still needs to be known
|
||
// before the window exists so callers can size resources.
|
||
uikit_read_screen_scale(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 {
|
||
UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr));
|
||
}
|
||
}
|
||
|
||
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 {
|
||
inline if OS == .ios {
|
||
uikit_refresh_safe_insets(self);
|
||
}
|
||
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,
|
||
target_present_time = self.last_target_ts,
|
||
};
|
||
}
|
||
|
||
end_frame :: (self: *UIKitPlatform) {
|
||
inline if OS == .ios {
|
||
if self.gpu_mode == .gles {
|
||
uikit_present_renderbuffer(self);
|
||
}
|
||
// Metal mode: caller's gpu.end_frame() handles present.
|
||
}
|
||
}
|
||
|
||
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) {
|
||
inline if OS == .ios {
|
||
if self.text_field == null { return; }
|
||
sel_become := sel_registerName("becomeFirstResponder".ptr);
|
||
msg_b : (*void, *void) -> u8 = xx objc_msgSend;
|
||
msg_b(self.text_field, sel_become);
|
||
}
|
||
}
|
||
|
||
hide_keyboard :: (self: *UIKitPlatform) {
|
||
inline if OS == .ios {
|
||
if self.text_field == null { return; }
|
||
sel_resign := sel_registerName("resignFirstResponder".ptr);
|
||
msg_b : (*void, *void) -> u8 = xx objc_msgSend;
|
||
msg_b(self.text_field, sel_resign);
|
||
}
|
||
}
|
||
|
||
stop :: (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_refresh_safe_insets :: (plat: *UIKitPlatform) {
|
||
inline if OS != .ios { return; }
|
||
if plat.gl_view == null { return; }
|
||
|
||
sel_safe := sel_registerName("safeAreaInsets".ptr);
|
||
msg_insets : (*void, *void) -> UIEdgeInsets = xx objc_msgSend;
|
||
i := msg_insets(plat.gl_view, sel_safe);
|
||
plat.safe_top = xx i.top;
|
||
plat.safe_left = xx i.left;
|
||
plat.safe_bottom = xx i.bottom;
|
||
plat.safe_right = xx i.right;
|
||
}
|
||
|
||
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);
|
||
|
||
class_addMethod(SxAppDelegate,
|
||
sel_registerName("application:didFinishLaunchingWithOptions:".ptr),
|
||
xx uikit_did_finish_launching, "c@:@@".ptr);
|
||
class_addMethod(SxAppDelegate,
|
||
sel_registerName("sxKeyboardWillChangeFrame:".ptr),
|
||
xx uikit_keyboard_will_change_frame, "v@:@".ptr);
|
||
|
||
objc_registerClassPair(SxAppDelegate);
|
||
|
||
// SxSceneDelegate handles the per-scene UI setup. iOS 13+ scene-based
|
||
// lifecycle: didFinishLaunching is too early for the window — the
|
||
// UIWindowScene doesn't connect until `scene:willConnectTo:options:`.
|
||
// The class is named in Info.plist's UIApplicationSceneManifest →
|
||
// UISceneDelegateClassName.
|
||
SxSceneDelegate := objc_allocateClassPair(UIResponder, "SxSceneDelegate".ptr, 0);
|
||
|
||
class_addMethod(SxSceneDelegate,
|
||
sel_registerName("scene:willConnectToSession:options:".ptr),
|
||
xx uikit_scene_will_connect, "v@:@@@".ptr);
|
||
class_addMethod(SxSceneDelegate,
|
||
sel_registerName("window".ptr),
|
||
xx uikit_window_getter, "@@:".ptr);
|
||
class_addMethod(SxSceneDelegate,
|
||
sel_registerName("setWindow:".ptr),
|
||
xx uikit_window_setter, "v@:@".ptr);
|
||
|
||
// Formal protocol conformance is required for UISceneDelegate
|
||
// (iOS checks -[cls conformsToProtocol:@protocol(UISceneDelegate)]
|
||
// before instantiating; without it the class is silently rejected
|
||
// with "does not conform to the UISceneDelegate protocol" in the
|
||
// log and a default scene with no delegate is created instead).
|
||
// Add the protocol BEFORE registerClassPair — the runtime locks
|
||
// the class layout after registration.
|
||
UISceneDelegateProto := objc_getProtocol("UISceneDelegate".ptr);
|
||
UIWindowSceneDelegateProto := objc_getProtocol("UIWindowSceneDelegate".ptr);
|
||
if UISceneDelegateProto != null {
|
||
class_addProtocol(SxSceneDelegate, UISceneDelegateProto);
|
||
} else {
|
||
NSLog(ns_string("[sx] WARN: UISceneDelegate protocol not found (dead-stripped)\n".ptr));
|
||
}
|
||
if UIWindowSceneDelegateProto != null {
|
||
class_addProtocol(SxSceneDelegate, UIWindowSceneDelegateProto);
|
||
}
|
||
|
||
objc_registerClassPair(SxSceneDelegate);
|
||
|
||
uikit_register_gl_view_class();
|
||
uikit_register_metal_view_class();
|
||
}
|
||
}
|
||
|
||
// Read [UIScreen mainScreen].nativeScale into plat.dpi_scale. Used by the
|
||
// metal-mode init path which doesn't go through uikit_create_gl_context
|
||
// (that's where the gles path picks the scale up).
|
||
uikit_read_screen_scale :: (plat: *UIKitPlatform) {
|
||
inline if OS != .ios { return; }
|
||
UIScreen := objc_getClass("UIScreen".ptr);
|
||
sel_main_screen := sel_registerName("mainScreen".ptr);
|
||
sel_native_scale := sel_registerName("nativeScale".ptr);
|
||
msg_o : (*void, *void) -> *void = xx objc_msgSend;
|
||
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
|
||
screen := msg_o(UIScreen, sel_main_screen);
|
||
scale_d : f64 = msg_d(screen, sel_native_scale);
|
||
plat.dpi_scale = xx scale_d;
|
||
}
|
||
|
||
// NSNotification callback. The notification's userInfo dict has the
|
||
// keyboard's end-frame and animation curve/duration.
|
||
// UIKeyboardFrameEndUserInfoKey → NSValue wrapping CGRect (screen coords)
|
||
// UIKeyboardAnimationDurationUserInfoKey → NSNumber wrapping double
|
||
// Keyboard height = how much of the screen the keyboard covers from the bottom.
|
||
// If the keyboard's end Y >= screen.height, the keyboard is offscreen (hiding).
|
||
uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *void) callconv(.c) {
|
||
if g_uikit_plat == null { return; }
|
||
plat := g_uikit_plat;
|
||
|
||
sel_user_info := sel_registerName("userInfo".ptr);
|
||
sel_obj_for_key := sel_registerName("objectForKey:".ptr);
|
||
sel_cg_rect_value := sel_registerName("CGRectValue".ptr);
|
||
sel_double_value := sel_registerName("doubleValue".ptr);
|
||
sel_screen := sel_registerName("screen".ptr);
|
||
sel_bounds := sel_registerName("bounds".ptr);
|
||
|
||
msg_o : (*void, *void) -> *void = xx objc_msgSend;
|
||
msg_oo : (*void, *void, *void) -> *void = xx objc_msgSend;
|
||
msg_rect : (*void, *void) -> CGRect = xx objc_msgSend;
|
||
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
|
||
|
||
user_info := msg_o(notification, sel_user_info);
|
||
if user_info == null { return; }
|
||
|
||
end_value := msg_oo(user_info, sel_obj_for_key,
|
||
ns_string("UIKeyboardFrameEndUserInfoKey".ptr));
|
||
if end_value == null { return; }
|
||
end_rect := msg_rect(end_value, sel_cg_rect_value);
|
||
|
||
dur_value := msg_oo(user_info, sel_obj_for_key,
|
||
ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr));
|
||
anim_dur : f64 = 0.0;
|
||
if dur_value != null { anim_dur = msg_d(dur_value, sel_double_value); }
|
||
|
||
sel_unsigned_long_value := sel_registerName("unsignedLongValue".ptr);
|
||
msg_ul : (*void, *void) -> u64 = xx objc_msgSend;
|
||
curve_value := msg_oo(user_info, sel_obj_for_key,
|
||
ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr));
|
||
curve_int : u64 = 0;
|
||
if curve_value != null { curve_int = msg_ul(curve_value, sel_unsigned_long_value); }
|
||
|
||
// Screen height in points. The window lives on the connected scene's screen.
|
||
if plat.window == null { return; }
|
||
win_screen := msg_o(plat.window, sel_screen);
|
||
screen_bounds := msg_rect(win_screen, sel_bounds);
|
||
|
||
// Keyboard height = how much of the screen the keyboard covers from the
|
||
// bottom. When the keyboard is hiding, its end-frame.y == screen.height,
|
||
// so the height comes out 0 and visible becomes false.
|
||
kb_top : f64 = end_rect.y;
|
||
sh : f64 = screen_bounds.height;
|
||
h := sh - kb_top;
|
||
if h < 0.0 { h = 0.0; }
|
||
if h > sh { h = sh; }
|
||
target_h : f32 = xx h;
|
||
|
||
plat.keyboard_visible = h > 0.5;
|
||
|
||
if anim_dur <= 0.0 {
|
||
// No animation window — snap.
|
||
plat.keyboard_height = target_h;
|
||
plat.kb_animating = false;
|
||
return;
|
||
}
|
||
|
||
// Capture the animation params for the sx-side per-tick interpolation
|
||
// that drives `keyboard_height` (consumers like the chess UI's safe-area
|
||
// calc read it each frame). The interpolation uses cubic ease-out as a
|
||
// close approximation of UIKit's keyboard curve. For perfect lockstep
|
||
// on a UIView consumer in user code, drive a property via
|
||
// `[UIView animateWithDuration:plat.kb_anim_dur delay:0
|
||
// options:(plat.kb_anim_curve << 16) | 4
|
||
// animations:^{ ... }
|
||
// completion:nil]`
|
||
// — UIKit's internal options-to-CAMediaTimingFunction table handles
|
||
// even the private keyboard curve 7 correctly when packed this way.
|
||
plat.kb_anim_from = plat.keyboard_height;
|
||
plat.kb_anim_to = target_h;
|
||
plat.kb_anim_start = CACurrentMediaTime();
|
||
plat.kb_anim_dur = anim_dur;
|
||
plat.kb_anim_curve = curve_int;
|
||
plat.kb_animating = true;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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) {
|
||
inline if OS == .ios {
|
||
if g_uikit_plat != null {
|
||
uikit_subscribe_keyboard_notifications(self);
|
||
}
|
||
}
|
||
1;
|
||
}
|
||
|
||
uikit_subscribe_keyboard_notifications :: (delegate: *void) {
|
||
inline if OS != .ios { return; }
|
||
NSNotificationCenter := objc_getClass("NSNotificationCenter".ptr);
|
||
sel_default_center := sel_registerName("defaultCenter".ptr);
|
||
sel_add_observer := sel_registerName("addObserver:selector:name:object:".ptr);
|
||
msg_o : (*void, *void) -> *void = xx objc_msgSend;
|
||
msg_o5 : (*void, *void, *void, *void, *void, *void) -> void = xx objc_msgSend;
|
||
center := msg_o(NSNotificationCenter, sel_default_center);
|
||
msg_o5(center, sel_add_observer, delegate, sel_registerName("sxKeyboardWillChangeFrame:".ptr),
|
||
ns_string("UIKeyboardWillChangeFrameNotification".ptr), xx 0);
|
||
}
|
||
|
||
uikit_scene_will_connect :: (self: *void, _cmd: *void, scene: *void, session: *void, options: *void) callconv(.c) {
|
||
inline if OS == .ios {
|
||
uikit_scene_will_connect_ios(self, scene);
|
||
}
|
||
}
|
||
|
||
uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
|
||
if g_uikit_plat == null {
|
||
NSLog(ns_string("[sx] no platform\n".ptr));
|
||
return;
|
||
}
|
||
plat := g_uikit_plat;
|
||
|
||
UIWindow := objc_getClass("UIWindow".ptr);
|
||
UIViewController := objc_getClass("UIViewController".ptr);
|
||
SxGLView := objc_getClass("SxGLView".ptr);
|
||
SxMetalView := objc_getClass("SxMetalView".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_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_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend;
|
||
msg_oou64 : (*void, *void, u64) -> void = 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;
|
||
|
||
win_raw := msg_o(UIWindow, sel_alloc);
|
||
plat.window = msg_ooo(win_raw, sel_init_with_scene, scene);
|
||
|
||
// Make the scene delegate own the window so iOS retains it. Per the
|
||
// scene-based lifecycle, the scene delegate is expected to provide the
|
||
// UIWindow via -window/-setWindow:.
|
||
sel_set_window := sel_registerName("setWindow:".ptr);
|
||
msg_oo(delegate, sel_set_window, plat.window);
|
||
|
||
vc_raw := msg_o(UIViewController, sel_alloc);
|
||
plat.root_vc = msg_o(vc_raw, sel_init);
|
||
|
||
// Allocate either SxGLView or SxMetalView based on gpu_mode and install
|
||
// it as the VC's view. The view's +layerClass override gives us the
|
||
// right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE
|
||
// setRootViewController avoids the VC lazy-loading a default view.
|
||
view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView;
|
||
glv_raw := msg_o(view_class, 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). Required for EAGL +
|
||
// recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't
|
||
// hurt to be explicit).
|
||
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);
|
||
|
||
if plat.gpu_mode == .gles {
|
||
// EAGL drawable properties dict required by
|
||
// EAGLContext.renderbufferStorage:fromDrawable: (color format,
|
||
// non-retained backing). Without this dict the renderbuffer
|
||
// allocation silently fails and the framebuffer reports INCOMPLETE.
|
||
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 + 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. CGFloat is `double` on
|
||
// 64-bit Apple platforms; reading as f32 would clobber the value.
|
||
screen := msg_o(plat.window, sel_screen);
|
||
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
|
||
// a layout pass; layoutSubviews calls uikit_setup_renderbuffer.
|
||
msg_v(plat.window, sel_make_key_visible);
|
||
|
||
// Hidden UITextField as the firstResponder source for show_keyboard /
|
||
// hide_keyboard. Lives as a subview of the GL view so it's in the
|
||
// responder chain but is sized 0×0 so it can't be tapped.
|
||
UITextField := objc_getClass("UITextField".ptr);
|
||
sel_add_subview := sel_registerName("addSubview:".ptr);
|
||
tf_raw := msg_o(UITextField, sel_alloc);
|
||
plat.text_field = msg_o(tf_raw, sel_init);
|
||
msg_oo(plat.gl_view, sel_add_subview, plat.text_field);
|
||
|
||
|
||
// (Keyboard observer is registered in didFinishLaunching via
|
||
// uikit_subscribe_keyboard_notifications — it's app-level, not scene-
|
||
// level, so it doesn't belong here.)
|
||
|
||
// 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));
|
||
}
|
||
|
||
// 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;
|
||
|
||
// Keyboard-inset lockstep — sx-side cubic ease-out approximation of
|
||
// UIKit's private keyboard curve. Sample targetTimestamp so we
|
||
// interpolate at the time this frame will be visible. Lags by ~1
|
||
// frame behind UIKit because UIKit's keyboard is rendered in a
|
||
// separate process (UIRemoteKeyboardWindow) and we can't perfectly
|
||
// sync to it from outside that scene. Refinements tried:
|
||
// CATransaction.flush, CABasicAnimation, presentationLayer reading,
|
||
// and keyboardLayoutGuide — none eliminated the lag without
|
||
// cascade-breaking the GL view's frame.
|
||
if plat.kb_animating {
|
||
sel_target_ts := sel_registerName("targetTimestamp".ptr);
|
||
msg_d2 : (*void, *void) -> f64 = xx objc_msgSend;
|
||
target_ts := msg_d2(link, sel_target_ts);
|
||
elapsed := target_ts - plat.kb_anim_start;
|
||
// Negative elapsed can happen if the just-fired willChangeFrame
|
||
// set kb_anim_start to a wall time AFTER the tick already
|
||
// captured its targetTimestamp this frame. Without the clamp,
|
||
// t < 0 makes the cubic ease-out *overshoot* in the opposite
|
||
// direction (visible as the indicator briefly jumping past the
|
||
// keyboard on close, then animating back).
|
||
if elapsed < 0.0 { elapsed = 0.0; }
|
||
if elapsed >= plat.kb_anim_dur or plat.kb_anim_dur <= 0.0 {
|
||
plat.keyboard_height = plat.kb_anim_to;
|
||
plat.kb_animating = false;
|
||
} else {
|
||
t : f32 = xx (elapsed / plat.kb_anim_dur);
|
||
inv := 1.0 - t;
|
||
eased := 1.0 - inv * inv * inv;
|
||
plat.keyboard_height = plat.kb_anim_from + (plat.kb_anim_to - plat.kb_anim_from) * eased;
|
||
}
|
||
}
|
||
|
||
// Indicator's position is driven by UIView.animateWithDuration kicked
|
||
// off from willChangeFrame — it animates in lockstep with UIKit's
|
||
// keyboard using the same curve+duration. No per-tick setFrame here.
|
||
|
||
if !plat.has_frame_closure { return; }
|
||
if !plat.gl_initialized { return; }
|
||
|
||
sel_dur := sel_registerName("duration".ptr);
|
||
sel_tts := sel_registerName("targetTimestamp".ptr);
|
||
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
|
||
dur_d : f64 = msg_d(link, sel_dur);
|
||
plat.delta_time = xx dur_d;
|
||
// Stash the targetTimestamp so begin_frame can hand it down to the
|
||
// game in FrameContext for Metal presentDrawable:atTime:.
|
||
plat.last_target_ts = msg_d(link, sel_tts);
|
||
|
||
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; }
|
||
if plat.gpu_mode == .gles {
|
||
uikit_setup_renderbuffer(plat);
|
||
} else {
|
||
uikit_compute_layer_pixel_size(plat);
|
||
}
|
||
plat.gl_initialized = true;
|
||
}
|
||
|
||
// Metal mode equivalent of uikit_setup_renderbuffer's "tell me how big the
|
||
// drawable is in pixels". Reads the layer's bounds in points and scales to
|
||
// pixels via dpi_scale. CAMetalLayer.drawableSize is set by MetalGPU.init
|
||
// based on these dims.
|
||
uikit_compute_layer_pixel_size :: (plat: *UIKitPlatform) {
|
||
inline if OS != .ios { return; }
|
||
if plat.gl_view == null { return; }
|
||
|
||
sel_bounds := sel_registerName("bounds".ptr);
|
||
msg_rect : (*void, *void) -> CGRect = xx objc_msgSend;
|
||
b := msg_rect(plat.gl_view, sel_bounds);
|
||
|
||
w_pts : f64 = b.width;
|
||
h_pts : f64 = b.height;
|
||
plat.viewport_w = xx w_pts;
|
||
plat.viewport_h = xx h_pts;
|
||
|
||
scale64 : f64 = xx plat.dpi_scale;
|
||
pw : f64 = w_pts * scale64;
|
||
ph : f64 = h_pts * scale64;
|
||
plat.pixel_w = xx pw;
|
||
plat.pixel_h = xx ph;
|
||
}
|
||
|
||
// 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);
|
||
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);
|
||
|
||
// 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);
|
||
}
|
||
}
|
||
|
||
// +layerClass IMP for SxMetalView. Class method, signature "#@:".
|
||
uikit_metal_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
|
||
objc_getClass("CAMetalLayer".ptr);
|
||
}
|
||
|
||
// SxMetalView reuses the same tick/layout/touch IMPs as SxGLView. The IMPs
|
||
// already branch on `plat.gpu_mode` for the GL-specific bits (renderbuffer
|
||
// setup, etc.), so a single set of IMPs serves both view classes.
|
||
uikit_register_metal_view_class :: () {
|
||
inline if OS == .ios {
|
||
UIView := objc_getClass("UIView".ptr);
|
||
SxMetalView := objc_allocateClassPair(UIView, "SxMetalView".ptr, 0);
|
||
|
||
metaclass := object_getClass(SxMetalView);
|
||
class_addMethod(metaclass,
|
||
sel_registerName("layerClass".ptr),
|
||
xx uikit_metal_view_layer_class, "#@:".ptr);
|
||
|
||
class_addMethod(SxMetalView,
|
||
sel_registerName("sxTick:".ptr),
|
||
xx uikit_gl_view_tick, "v@:@".ptr);
|
||
class_addMethod(SxMetalView,
|
||
sel_registerName("layoutSubviews".ptr),
|
||
xx uikit_gl_view_layout, "v@:".ptr);
|
||
|
||
class_addMethod(SxMetalView,
|
||
sel_registerName("touchesBegan:withEvent:".ptr),
|
||
xx uikit_gl_view_touches_began, "v@:@@".ptr);
|
||
class_addMethod(SxMetalView,
|
||
sel_registerName("touchesMoved:withEvent:".ptr),
|
||
xx uikit_gl_view_touches_moved, "v@:@@".ptr);
|
||
class_addMethod(SxMetalView,
|
||
sel_registerName("touchesEnded:withEvent:".ptr),
|
||
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
|
||
class_addMethod(SxMetalView,
|
||
sel_registerName("touchesCancelled:withEvent:".ptr),
|
||
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
|
||
|
||
objc_registerClassPair(SxMetalView);
|
||
}
|
||
}
|