From e0b5564f04aa0ae6666f29ef29c768d800fbabce Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 13:15:41 -0300 Subject: Step core animation by the shell's clock and sleep to the next wake A shell now answers Host.now, its monotonic clock in ns, and the pump advances core animation to it: one .tick per whole 16 ms frame since core time last stood still. The same animation therefore takes the same time at 60, 120 and 144 Hz, over ssh and after a slow frame (the GUI's re-armed clock ran them ~25% slow at 144 Hz; the tty's post-frame sleep drifted). nextWake() lists every core animation in one place: a frame from now while anything moves, the end of the wait while something only waits (a message lingering, the look-hover delay), null when idle. Shells sleep exactly that long, and a wait is jumped to its end in one step, so an 800 ms linger costs one frame instead of fifty. An overshoot under 1.5 ms after a step is let go: at 60 and 120 Hz every display frame takes exactly one step (a 16 ms frame against a 16.67 ms vsync would otherwise double-step every 24th). The six tick drivers are gone: tty's timer thread only times the wait, interruptibly (a newer, shorter request cuts short a sleep still timing a longer one); the GUI's AnimationClock, web's JS tick bank (pardes_tick now takes rAF's timestamp), the detached server's and the board's ticks; grid mode steps a virtual clock straight to each wake. PARDES_TEST_CLOCK, set by the snapshot harness, gives tty and the detached server the same virtual clock. Every stepped frame is drawn. The old pump never drew the last frame of a fade (a .tick asks for no frame, and the fade was over by the check), so a theme switch stopped at 9/10 of the way until the next input: theme.golden, themesel.golden and the GUI's acme-light scene move to the theme's true colours, and nothing else changes. The frozen previous grid is captured only while a panel transition is chosen, not on every idle frame. --- src/web/app.mjs | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) (limited to 'src/web') diff --git a/src/web/app.mjs b/src/web/app.mjs index 11d8f228..280239d5 100644 --- a/src/web/app.mjs +++ b/src/web/app.mjs @@ -1,7 +1,5 @@ const CELL_SIZE = 20; const ATTR_TAGLINE = 1 << 7; -const ANIMATION_FRAME_MS = 1000 / 60; -const MAX_ANIMATION_TICKS = 4; const DEFAULT_FG = "#cccccc"; const DEFAULT_BG = "#121212"; const decoder = new TextDecoder(); @@ -325,7 +323,6 @@ export class PardesRuntime { this.running = false; this.pointer = null; this.frameHandle = 0; - this.animationClock = { last: null, lag: 0 }; this.bindEvents(); } @@ -338,7 +335,6 @@ export class PardesRuntime { this.wasm.pardes_tick(); this.renderer.render(); this.running = true; - this.animationClock = { last: null, lag: 0 }; this.frameHandle = requestAnimationFrame((timestamp) => this.frame(timestamp)); this.root.focus({ preventScroll: true }); } @@ -355,13 +351,9 @@ export class PardesRuntime { this.resize(); this.root.style.setProperty("--pardes-tagline-font-size", `${this.measuredTaglinePercent}%`); } - // JS owns the animation clock; the pump that follows applies those ticks, - // runs the core's effects and presents one frame however fast the display - // is. Both halves are needed: an idle core still has input and effects. - for (let ticks = animationTicks(this.animationClock, timestamp); ticks > 0; ticks -= 1) { - this.wasm.pardes_animation_tick(); - } - this.wasm.pardes_tick(); + // The core's clock is this timestamp: it steps its own animations + // by it, however fast the display refreshes. + this.wasm.pardes_tick(timestamp); if (this.wasm.pardes_should_quit()) this.running = false; this.renderer.render(); this.frameHandle = requestAnimationFrame((nextTimestamp) => this.frame(nextTimestamp)); @@ -559,28 +551,6 @@ export class PardesRuntime { } } -/// Spend monotonic requestAnimationFrame time as fixed 60 Hz core ticks. A -/// high-refresh display therefore renders repeated states rather than making -/// frame-count transitions run twice as fast. Long suspension is capped so a -/// returning tab cannot execute an unbounded loop before drawing. -export function animationTicks(clock, timestamp) { - if (!Number.isFinite(timestamp)) return 0; - if (clock.last === null || timestamp < clock.last) { - clock.last = timestamp; - clock.lag = 0; - return 0; - } - const elapsed = Math.min(timestamp - clock.last, ANIMATION_FRAME_MS * MAX_ANIMATION_TICKS); - clock.last = timestamp; - clock.lag += elapsed; - const ticks = Math.min(MAX_ANIMATION_TICKS, Math.floor((clock.lag + 1e-7) / ANIMATION_FRAME_MS)); - clock.lag -= ticks * ANIMATION_FRAME_MS; - return ticks; -} - -/// The four things the core asks the page for, as the module's imports. The -/// holder exists because the import object has to be built before the instance -/// whose memory those imports read. export function browserHost() { const host = { wasm: null }; const view = (ptr, len) => new Uint8Array(host.wasm.memory.buffer, ptr, len); -- cgit v1.3