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:
agra
2026-05-17 17:46:17 +03:00
parent 1af8e1ffd5
commit 2ff24e29cc
2 changed files with 49 additions and 5 deletions

View File

@@ -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();