From 9695cb098c784792af79c22d46a17f7bc7247d33 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 22 Sep 2026 17:43:57 -0300 Subject: Repaint PDF highlights by row, send rasters by shared memory, and animate messages PDF highlights (hover preview, search, selection) are baked into page rasters, and any change re-rendered the whole page with MuPDF; the TTY then re-sent it as base64 (4.7 MB a page), the GUI as a new texture. Worse, a pointer motion over a PDF invalidated the page even when no preview was shown, so every motion paid that. Now: - A raster whose baked highlight set equals the wanted one is left alone. - A highlighted page keeps its clean rows (before highlights and tint); a change repaints only the rows of quads that differ, running MuPDF's highlight pass (pardes_pdf_paint_highlights) over those clean rows and tinting them: the operations a full render performs, so the pixels are identical. MuPDF band renders are NOT bit-identical to a whole page (edge rows, resampled images), so they are never used to patch; the comment claiming otherwise is corrected. - ImagePlace.patch hands shells the changed rows; the GUI uploads just those rows into the texture it holds. - The TTY probes kitty shared memory (t=s) with an id vaxis never reaches and sends rasters as a /dev/shm name when the terminal reads it; direct base64 otherwise (ssh). - Shells that take row patches (GUI, TTY with shm) repaint a selection while it is dragged instead of only on release. Latency elsewhere: - TTY: an animating frame no longer sleeps 16 ms blind; a tick thread posts into the input queue, so input inside the frame is handled at once. - TTY and GUI: queued pointer motions coalesce to the last. - GUI: a skipped swapchain image re-arms the frame (3 retries); animations still tick while nothing presents. - Editing: the line index is carried across an edit instead of rebuilt from a scan of the whole file per keystroke. Messages fall into their row (ease-in; the GUI slides the band out from under the tagline, a terminal fades it), stay until the next input as before, linger MessageLinger ms (default 800), and dissolve (ease-out). MessageAnimation toggles it; both are settings, in Config and startup files. The snapshot harness pins the old behaviour. The detached server now ticks animations. A restored terminal comes back live: the old screen and scrollback (dumped as clean VT by ghostty's formatter, replayed at the new size; older dumps fall back to their rendered text), a dim "restored history" marker, then a new shell in the directory it was in. Right-click on a line number in a file pane looks at that line (a sticky context header's number included). Measured with an external pty driver (TTY), an in-process fence trace (GUI, PARDES_TEST_LATENCY), and test/pdf_pointer_bench.zig (pixel identity against the baseline and a whole-page oracle); balanced A/A/B rounds, paired per-round statistics. Messages stack: each event gets its own row and its own fall, linger and dissolve; a line keeps its row until it leaves and a new one fills the first free row. Announcements and statuses are replaced in place, not stacked. MessageFall, MessageDissolve and DumpDir are settings Config reports. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/detached/server.zig | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src/detached/server.zig') diff --git a/src/detached/server.zig b/src/detached/server.zig index 3e98fc9d..6959e577 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -536,7 +536,7 @@ pub const Session = struct { fn writeDump(ctx: ?*anyopaque, bytes: []const u8) void { const s = of(ctx); var pbuf: [1024:0]u8 = undefined; - const path = pardes.dump.outPath(&pbuf) orelse return; + const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return; filesystem.write(s.core, path, bytes) catch |err| return s.core.reportError(0, "dump", err); s.core.setLastDump(path); } @@ -809,8 +809,11 @@ pub const Session = struct { // The wait is the 9P connections' turn with the core. if (n == 0) { pardes.turn.rest(); - defer pardes.turn.wake(); - return nap(if (timeout_ms == 0) 16 else timeout_ms); + nap(if (timeout_ms == 0) 16 else timeout_ms); + pardes.turn.wake(); + // An animating frame's wait is the frame: tick it. + if (timeout_ms != 0) s.core.update(.tick); + return; } var timeout: c_int = if (timeout_ms == 0) -1 else @intCast(@min(timeout_ms, std.math.maxInt(c_int))); if (s.nextWake(now)) |due| timeout = if (timeout < 0) due else @min(timeout, due); @@ -819,6 +822,9 @@ pub const Session = struct { const ready = libc.poll(&fds, @intCast(n), timeout); pardes.turn.wake(); if (ready > 0) s.dispatch(fds[0..n], src[0..n]); + // Animating, the frame's time is up: advance it. There is no other + // tick in a detached session, and without it an animation never ends. + if (timeout_ms != 0 and monotonicMs() -| now >= timeout_ms) s.core.update(.tick); _ = s.drainCompletions(true); s.expire(monotonicMs()); s.harvest(); @@ -1156,7 +1162,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void try session.core.pump(h); if (session.core.quit) break; if (session.core.takeRestore()) |path| restore: { - const bytes = filesystem.readRestore(gpa, path) catch |err| { + const bytes = filesystem.readRestore(gpa, path, session.core.settings.dump_dir.get()) catch |err| { session.core.reportError(session.core.active, "Restore", err); break :restore; }; -- cgit v1.3