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

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