From 6f7c79650857da578b9e6f8c49c772eb7c40bcae Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 08:17:38 -0300 Subject: GUI goldens settle on the window's idle, not two captures that agree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test/gui_golden.py hashed a scene once two captures 400 ms apart agreed; on a loaded machine a slow frame agreed with itself before the scene's change landed and every later hash moved up a scene. A test window now writes a status file into its capture directory each loop step (gui.zig writeTestStatus: step, frames captured, idle — nothing owes a frame, nothing moves, nothing waits on the core's clock), and the harness waits for an idle step two past the one current when it asks: a 9P write has been served by then, so the frame is the scene's. Two things kept a window from ever going idle, both fixed: - Look-hover re-armed forever: resting on a word with nothing to open, the hover wait ran its two frames, found nothing, cancelled, and the next present's pointer refresh started it again: a frame every other frame for as long as the pointer rested there, live too (CPU). The place last refused is remembered (Pardes.look_hover_refused) until the pointer moves off it. - The virtual clock moved only on a wait that timed out, so a steady trickle of events held it still; it now moves to the core's wake too when an event cut the wait short and nothing owes a frame. Captures go to a directory of their own, outside the session's (a file written there every frame is a change in the directory its panes look at). All 17 goldens match the file as recorded at oooopqyv; eight runs, five alongside a ReleaseFast build and three with all 16 cores saturated (load 13-19), were byte-identical, 14 s a run unloaded (was ~30). Tests: the resting pointer lets the core go idle. --- src/gui/gui.zig | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) (limited to 'src/gui') diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 6bde8b60..8d9c29ab 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -4273,7 +4273,13 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { 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.*); + } else if (timeout_ms != 0 and (!got or !core.needs_frame)) { + // A wait that ran out, or one a stray event cut short while + // nothing owes a frame: time moves to the core's wake all + // the same, so a steady trickle of events (a pty's, the + // test feed's) cannot hold a virtual clock still forever. + virtual.* = @max(virtual.*, core.nextWake() orelse virtual.*); + } } if (got) { // A run of queued motions is one move to its last position (the @@ -4348,6 +4354,7 @@ fn pollFrame(ctx: ?*anyopaque) void { if (updateCoreResize(core, geom.cols, geom.rows, g.cell_w, g.cell_h, g.tagline_width, g.tagline_height)) resetScroll(g); stepScroll(g, core, s.gpa); g.post.sync(s.gpa, s.io, g.device, g.swapchain_format, core); + if (s.test_mode) writeTestStatus(g, core, s.presented); // Between the core's frames, a chain that moves on its own redraws // alone, once a refresh of the display the window is on (a millisecond // early is on time: the wait that paces it is in whole milliseconds). @@ -8235,6 +8242,32 @@ fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, t g.captured +%= 1; } +/// A test window's state for a harness (test/gui_golden.py), in the +/// capture directory's `status`: " ". +/// Idle is 1 once the frame last captured is the one that stays: nothing +/// owes a frame, nothing moves, nothing waits on a clock. A harness that +/// sees a step past the one it acted on, idle, has its frame, however slow +/// the machine is. +var test_status_step: u64 = 0; +fn writeTestStatus(g: *const Gui, core: *const pardes.Pardes, presented: bool) void { + test_status_step +%= 1; + if (g.capture_dir.len == 0) return; + const idle = presented and !core.needs_frame and !core.continuous() and core.nextWake() == null and + !g.glide.moving() and g.scroll_lag == 0 and g.scroll_delta == 0 and g.scroll_notch == 0; + var line_buf: [64]u8 = undefined; + // What keeps it busy, after: owes a frame, moves, waits, not shown. + const line = std.fmt.bufPrint(&line_buf, "{d} {d} {d} {d}{d}{d}{d}\n", .{ test_status_step, g.captured, @intFromBool(idle), @intFromBool(core.needs_frame), @intFromBool(core.continuous()), @intFromBool(core.nextWake() != null), @intFromBool(!presented) }) catch return; + var tmp_buf: [4096]u8 = undefined; + var final_buf: [4096]u8 = undefined; + const tmp_path = std.fmt.bufPrintSentinel(&tmp_buf, "{s}/status.tmp", .{g.capture_dir}, 0) catch return; + const final_path = std.fmt.bufPrintSentinel(&final_buf, "{s}/status", .{g.capture_dir}, 0) catch return; + const fd = libc.open(tmp_path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644)); + if (fd < 0) return; + _ = libc.write(fd, line.ptr, line.len); + _ = libc.close(fd); + _ = libc.rename(tmp_path, final_path); +} + fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u32, height: u32, name: []const u8) !void { // A latency trace times frames, and an 8 MB file write per frame would // stall the loop it is timing. -- cgit v1.3