From b4781b334a06d16aedb0b6a3d604cb249eb619de Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 17:56:10 -0300 Subject: Lift moves at the display's rate: render-time animation Review (and the coordinator): Lift stepped with the core's 16 ms ticks, so a 144 Hz display held each shadow for 2-3 refreshes. The lift springs are now sampled at each frame's own time (p.now_ns), and Pardes.continuous() reports a render-time animation in motion: while it is, the pump draws every loop (a 1 ms wait takes only the input there is) and the present's vsync paces it at the display's rate. They left nextWake; the stepped, frame-indexed animations (transitions, message fades, the theme fade) and every golden are unchanged. With PARDES_TEST_CLOCK, each loop of a moving render-time animation moves the virtual clock by a display frame at PARDES_TEST_HZ (default 60), so a 144 Hz series is deterministic: a focus switch is 51 distinct frames at 144 Hz. Test: renders 6.9 ms apart give rising lifts; settled, no frames. The tty never lifts (GUI capability). Shared files touched: pardes.zig (continuous, pump), gui.zig. --- src/draw.zig | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) (limited to 'src/draw.zig') diff --git a/src/draw.zig b/src/draw.zig index 0998db24..70b629a5 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -127,11 +127,12 @@ pub fn place(p: *Pardes, s: *Surface) void { serial.* = pane.serial; spring.* = .{}; } - spring.retarget(if (p.settings.lift and id == p.active) 1 else 0, p.stepped_ns); - _ = spring.step(p.stepped_ns); + // At this frame's own time, not the stepped animations' 16 ms grid. + spring.retarget(if (p.settings.lift and id == p.active) 1 else 0, p.now_ns); + _ = spring.step(p.now_ns); } for (s.regions[0..s.nregions]) |*region| switch (region.kind) { - .grip, .tag, .body, .rail => region.lift = p.lifts[region.owner].value(p.stepped_ns), + .grip, .tag, .body, .rail => region.lift = p.lifts[region.owner].value(p.now_ns), .notice => region.lift = if (p.settings.lift) 1 else 0, else => {}, }; @@ -1122,6 +1123,37 @@ test "a notice wider than its band, as a shader's compile error is, draws its en } } +test "a lift is sampled at each frame's own time, and asks for frames only while it moves" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const ms = std.time.ns_per_ms; + p.advance(1000 * ms); + _ = try p.render(arena.allocator()); + try std.testing.expect(!p.continuous()); + p.settings.lift = true; + var lifts: [3]f32 = undefined; + // 144 Hz: 6.9 ms apart, none of them on a 16 ms step of its own. + for (&lifts, 0..) |*lift, i| { + p.advance(1000 * ms + @as(u64, @intCast(i)) * 6_944_444); + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + lift.* = for (s.regionList()) |region| { + if (region.kind == .grip and region.owner == p.active) break region.lift; + } else return error.TestExpectedGrip; + try std.testing.expect(p.continuous()); + } + try std.testing.expect(lifts[0] < lifts[1] and lifts[1] < lifts[2]); + // Settled, it is exactly up, and the core is idle again. + p.advance(2000 * ms); + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + try std.testing.expect(!p.continuous()); + try std.testing.expectEqual(@as(?u64, null), p.nextWake()); +} + test "a monkey over notices, prompts, tags and tiny panes at a gui's metrics never panics" { // The snapshot harness runs the tty, whose tagline pitch is the body's, // so it never reaches what a narrower tagline does to a notice's band: -- cgit v1.3