platform: snap keyboard inset (lockstep deferred to Metal renderer)
Walked back the manual-interpolation + CABasicAnimation+presentationLayer
attempts at lockstep keyboard inset. Both leave a visible frame of lag
because the lockstep problem is structural, not implementation-detail:
- GL renderbuffer content is baked at presentRenderbuffer() time.
- The CoreAnimation compositor can interpolate the *position* of a
CALayer per-vsync but cannot reach into our renderbuffer's pixels.
- The GPU pipeline (CADisplayLink → command build → present →
compositor → display) is 2-3 frames deep on iOS GLES, so even
`targetTimestamp`-based prediction is one to two frames short.
The architectural escape that doesn't move the GL view (rejected for
edge cases) is to give CoreAnimation a renderable handle it can sync
on. That means **Metal**:
- CAMetalLayer + MTLDrawable.presentAtTime(_:) caps the pipeline at
exactly one frame.
- With targetTimestamp prediction + curve-accurate keyboard math,
our drawable lands at the same vsync as UIKit's keyboard.
- Renderer modernization (Metal/Vulkan/WebGPU per platform) was on
the roadmap anyway; lockstep is the forcing function.
This commit keeps the keyboard observer + show/hide_keyboard wiring
intact and SNAPS keyboard_height when the observer fires. Behavior:
the chess board doesn't shift during the keyboard animation; it shifts
in one step when the observer fires. Less smooth than the broken
attempt but honest.
Plan for the Metal port (next):
- library/modules/gpu/{metal,vulkan,webgpu}.sx + a `GPU` protocol
analogous to Platform.
- Port modules/ui/renderer.sx shaders from GLSL to MSL.
- SxGLView becomes SxMetalView; CAEAGLLayer becomes CAMetalLayer.
- Lockstep falls out of MTLDrawable.presentAtTime(targetTimestamp).
This commit is contained in:
@@ -5,13 +5,18 @@
|
||||
// on-screen keyboard so safe_insets.bottom can be observed growing /
|
||||
// shrinking under it.
|
||||
//
|
||||
// To visualize the safe-area / keyboard inset, the frame draws a red
|
||||
// bar at the bottom whose height equals `safe_insets.bottom`. The
|
||||
// platform interpolates `keyboard_height` over the keyboard's own
|
||||
// animation duration, so the bar slides in lockstep with iOS's
|
||||
// keyboard.
|
||||
//
|
||||
// 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
|
||||
// xcrun simctl launch booted co.swipelab.sxuikit
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/uikit.sx";
|
||||
@@ -22,12 +27,26 @@
|
||||
#import "modules/ui/events.sx";
|
||||
#import "modules/platform/uikit.sx";
|
||||
|
||||
GL_SCISSOR_TEST :u32: 0x0C11;
|
||||
glEnable_ : (u32) -> void = ---;
|
||||
glDisable_ : (u32) -> void = ---;
|
||||
glScissor_ : (s32, s32, s32, s32) -> void = ---;
|
||||
|
||||
g_color_index : s64 = 0;
|
||||
g_keyboard_up : bool = false;
|
||||
g_loaded : bool = false;
|
||||
|
||||
tap_frame :: () {
|
||||
fc := g_uikit_plat.begin_frame();
|
||||
|
||||
if !g_loaded {
|
||||
// Cache the GL fn-ptrs we use beyond what modules/opengl.sx loads.
|
||||
glEnable_ = xx ios_gl_proc("glEnable".ptr);
|
||||
glDisable_ = xx ios_gl_proc("glDisable".ptr);
|
||||
glScissor_ = xx ios_gl_proc("glScissor".ptr);
|
||||
g_loaded = true;
|
||||
}
|
||||
|
||||
events := g_uikit_plat.poll_events();
|
||||
i : s64 = 0;
|
||||
while i < events.len {
|
||||
@@ -51,9 +70,22 @@ tap_frame :: () {
|
||||
r : f32 = if phase == 0 then 0.8 else 0.1;
|
||||
g : f32 = if phase == 1 then 0.8 else 0.1;
|
||||
b : f32 = if phase == 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);
|
||||
|
||||
// Bottom bar = the interpolated safe-area bottom inset.
|
||||
insets := g_uikit_plat.safe_insets();
|
||||
bar_h_px : s32 = xx (insets.bottom * fc.dpi_scale);
|
||||
if bar_h_px > 0 {
|
||||
glEnable_(GL_SCISSOR_TEST);
|
||||
glScissor_(0, 0, fc.pixel_w, bar_h_px);
|
||||
glClearColor(0.95, 0.25, 0.25, 1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDisable_(GL_SCISSOR_TEST);
|
||||
}
|
||||
|
||||
g_uikit_plat.end_frame();
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,17 @@ 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.
|
||||
|
||||
saved_title: [*]u8 = null;
|
||||
}
|
||||
|
||||
@@ -316,6 +327,8 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
|
||||
if h < 0.0 { h = 0.0; }
|
||||
if h > sh { h = sh; }
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -587,11 +600,10 @@ uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
|
||||
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;
|
||||
dur_d : f64 = msg_d(link, sel_dur);
|
||||
plat.delta_time = xx dur_d;
|
||||
|
||||
fn := plat.frame_closure;
|
||||
fn();
|
||||
|
||||
Reference in New Issue
Block a user