iOS lock step keyboard + metal
This commit is contained in:
@@ -9,6 +9,12 @@ FrameContext :: struct {
|
||||
pixel_h: s32;
|
||||
dpi_scale: f32;
|
||||
delta_time: f32;
|
||||
// The host clock time at which the next vsync will present the frame
|
||||
// we're about to render. On iOS this is CADisplayLink.targetTimestamp;
|
||||
// forward it to MetalGPU.end_frame() to schedule presentDrawable:atTime:
|
||||
// so our drawable hits the same vsync as UIKit's compositor. Other
|
||||
// platforms leave it 0 (Metal then falls back to immediate present).
|
||||
target_present_time: f64;
|
||||
}
|
||||
|
||||
KeyboardState :: struct {
|
||||
|
||||
@@ -19,6 +19,12 @@ UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_c
|
||||
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;
|
||||
|
||||
@@ -86,6 +92,9 @@ UIKitPlatform :: struct {
|
||||
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;
|
||||
@@ -100,16 +109,21 @@ UIKitPlatform :: struct {
|
||||
keyboard_visible: bool = false;
|
||||
keyboard_height: f32 = 0.0;
|
||||
|
||||
// Keyboard height SNAPS to its target value when the observer fires.
|
||||
// It does NOT interpolate in lockstep with iOS's keyboard animation.
|
||||
// Reason: with OpenGL ES + CAEAGLLayer, our renderbuffer is baked at
|
||||
// `presentRenderbuffer` time, while UIKit's keyboard view is composited
|
||||
// by CoreAnimation at vsync. We can't make the compositor interpolate
|
||||
// the renderbuffer's contents in lockstep with the keyboard's frame.
|
||||
// True lockstep requires a Metal renderer (CAMetalLayer +
|
||||
// `present(at: targetTimestamp)` keeps the pipeline at 1 frame) plus
|
||||
// curve-accurate prediction. Tracked as the Metal port in
|
||||
// current/CHECKPOINT.md.
|
||||
// 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;
|
||||
}
|
||||
@@ -170,6 +184,7 @@ impl Platform for UIKitPlatform {
|
||||
pixel_h = self.pixel_h,
|
||||
dpi_scale = self.dpi_scale,
|
||||
delta_time = self.delta_time,
|
||||
target_present_time = self.last_target_ts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -373,11 +388,17 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
|
||||
if end_value == null { return; }
|
||||
end_rect := msg_rect(end_value, sel_cg_rect_value);
|
||||
|
||||
// UIKeyboardAnimationDurationUserInfoKey is also in userInfo; reading it
|
||||
// and running our inset update inside a `[UIView animateWithDuration:...]`
|
||||
// block would put us in the same CoreAnimation transaction as the keyboard
|
||||
// (zero-lag sync). Blocks aren't yet expressible from sx, so we update the
|
||||
// inset synchronously — content snaps while the keyboard slides.
|
||||
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; }
|
||||
@@ -392,11 +413,34 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
|
||||
h := sh - kb_top;
|
||||
if h < 0.0 { h = 0.0; }
|
||||
if h > sh { h = sh; }
|
||||
target_h : f32 = xx h;
|
||||
|
||||
// SNAP to target. See comment on UIKitPlatform.keyboard_height for why
|
||||
// lockstep interpolation is deferred until the Metal renderer.
|
||||
plat.keyboard_height = 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) {
|
||||
@@ -603,6 +647,7 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
|
||||
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.)
|
||||
@@ -678,13 +723,54 @@ uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user