summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/draw.zig38
-rw-r--r--src/gui/gui.zig18
-rw-r--r--src/pardes.zig18
3 files changed, 63 insertions, 11 deletions
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:
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 92bec92c..905caa48 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -2613,6 +2613,7 @@ fn localSession(
.fs = fs,
.test_mode = test_mode,
.test_clock = if (test_mode) host_io.testClock() else null,
+ .test_hz = if (std.c.getenv("PARDES_TEST_HZ")) |hz| @max(1, std.fmt.parseInt(u64, std.mem.span(hz), 10) catch 60) else 60,
};
defer shell.shutdownPtys();
const host = shell.host();
@@ -3898,6 +3899,8 @@ const Shell = struct {
retired_shells: [pardes.MAX_PANES]RetiredShell = @splat(.{}),
/// PARDES_TEST_CLOCK's virtual time (host_io.testClock), in test mode.
test_clock: ?u64 = null,
+ /// The display rate a virtual clock stands in for (PARDES_TEST_HZ).
+ test_hz: u64 = 60,
fn reap(s: *Shell) void {
const now = shellClock();
@@ -4150,10 +4153,17 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
pardes.turn.rest();
const got = c.SDL_WaitEventTimeout(&sev, ms);
pardes.turn.wake();
- // The core's wake ran out: a virtual clock moves exactly there.
- if (!got and timeout_ms != 0) if (s.test_clock) |*virtual| {
- virtual.* = @max(virtual.*, core.nextWake() orelse virtual.*);
- };
+ // A virtual clock: while a render-time animation moves, each loop
+ // is a presented frame and moves it by one display frame
+ // (PARDES_TEST_HZ), events or not, as a real display's present
+ // would; otherwise a wait that ran out moves it exactly to the
+ // core's wake.
+ if (s.test_clock) |*virtual| {
+ if (core.continuous()) {
+ const display = virtual.* + std.time.ns_per_s / s.test_hz;
+ virtual.* = if (core.nextWake()) |due| @max(virtual.*, @min(due, display)) else display;
+ } else if (!got and timeout_ms != 0) virtual.* = @max(virtual.*, core.nextWake() orelse virtual.*);
+ }
if (got) {
// A run of queued motions is one move to its last position (the
// handler reads only absolute x/y and held buttons): acting on
diff --git a/src/pardes.zig b/src/pardes.zig
index 449ef798..04ef4b36 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5040,7 +5040,9 @@ pub const Pardes = struct {
if (v.wait_input) |f| {
// Asleep until input or until core animation next needs a
// frame; 0 is no timeout at all.
- const timeout: u32 = if (p.nextWake()) |due| wake: {
+ // A render-time animation draws every loop: the wait only takes
+ // what input is there.
+ const timeout: u32 = if (p.continuous()) 1 else if (p.nextWake()) |due| wake: {
const now = if (v.now) |clock| clock(h.ctx) else p.now_ns;
const ms = ((due -| now) + std.time.ns_per_ms - 1) / std.time.ns_per_ms;
break :wake @intCast(std.math.clamp(ms, 1, std.math.maxInt(u32)));
@@ -5059,7 +5061,7 @@ pub const Pardes = struct {
// rendering and nothing else.
// A shell with a clock draws what `advance` stepped; one without
// steps its own `.tick`s and draws while anything animates.
- if (!p.needs_frame and (v.now != null or !p.animationActive())) return;
+ if (!p.needs_frame and !p.continuous() and (v.now != null or !p.animationActive())) return;
_ = p.frame_arena.reset(.retain_capacity);
const surface = try p.render(p.frame_arena.allocator());
if (v.present) |f| f(h.ctx, surface);
@@ -6217,7 +6219,6 @@ pub const Pardes = struct {
pub fn nextWake(p: *const Pardes) ?u64 {
const next = p.stepped_ns + animation.frame_ns;
if (p.chrome_animation.isActive() or p.presentation.animating()) return next;
- for (p.lifts) |spring| if (!spring.settled) return next;
var frames: ?u64 = null;
if (p.look_hover_wait) |waiting| {
const delay = config.look_preview_delay_frames orelse return next;
@@ -6230,10 +6231,19 @@ pub const Pardes = struct {
return if (frames) |n| p.stepped_ns + n * animation.frame_ns else null;
}
+ /// A render-time animation is moving (a pane's lift): it is sampled at
+ /// each frame's own `now`, so while it moves the shell draws every loop
+ /// and its present (vsync) paces it, at the display's rate. `nextWake`
+ /// is for the stepped animations, which count frames of `frame_ns`.
+ pub fn continuous(p: *const Pardes) bool {
+ for (p.lifts) |spring| if (!spring.settled) return true;
+ return false;
+ }
+
/// For shells that only need a yes or no: macOS and the board, which
/// keep their own frame banks.
pub fn animationActive(p: *const Pardes) bool {
- return p.nextWake() != null;
+ return p.nextWake() != null or p.continuous();
}
/// Bring core animation up to `now_ns`, the shell's monotonic clock: one