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/pdf.zig | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) (limited to 'src/pdf.zig') diff --git a/src/pdf.zig b/src/pdf.zig index 712750dc..de5e6d86 100644 --- a/src/pdf.zig +++ b/src/pdf.zig @@ -1017,6 +1017,73 @@ pub const Document = struct { ) != c.PARDES_PDF_OK) return error.RenderFailed; } + /// Only the highlight pass of `renderIntoAtWithPaper`, over `rgba`, which + /// holds `band` of this page as that render left it before highlighting + /// (opaque paper). Rendering with highlights equals rendering without and + /// then this, row for row: the render draws highlights over the finished + /// page, and so does this. + pub fn paintHighlightsAt( + document: *Document, + page: usize, + request: RenderRequest, + raster: Raster, + band: Raster.Band, + highlights: []const Highlight, + rgba: []u8, + ) !void { + if (rgba.len != band.len) return error.BadPixmap; + if (band.height == 0 or band.y + band.height > raster.height) + return error.BadPixmap; + const bounded = try boundedRequest(document, page, request); + if (highlights.len > c.PARDES_PDF_MAX_RESULT_QUADS) return error.RenderFailed; + if (c.pardes_pdf_paint_highlights( + document.handle, + @intCast(page), + bounded.dpi, + bounded.minimum_width, + bounded.minimum_height, + bounded.max_dimension, + if (highlights.len == 0) null else @ptrCast(highlights.ptr), + highlights.len, + rgba.ptr, + rgba.len, + @intCast(raster.width), + @intCast(raster.height), + @intCast(raster.stride), + @intCast(band.y), + @intCast(band.height), + ) != c.PARDES_PDF_OK) return error.RenderFailed; + } + + pub const Rows = struct { start: usize, end: usize }; + + /// The rows of `request`'s raster that painting `highlights` can touch, + /// by the transform the render itself uses (rotation included). Empty + /// when nothing would paint. + pub fn highlightRows( + document: *Document, + page: usize, + request: RenderRequest, + highlights: []const Highlight, + ) !Rows { + const bounded = try boundedRequest(document, page, request); + var row_start: c_int = 0; + var row_end: c_int = 0; + if (c.pardes_pdf_highlight_rows( + document.handle, + @intCast(page), + bounded.dpi, + bounded.minimum_width, + bounded.minimum_height, + bounded.max_dimension, + if (highlights.len == 0) null else @ptrCast(highlights.ptr), + highlights.len, + &row_start, + &row_end, + ) != c.PARDES_PDF_OK) return error.RenderFailed; + return .{ .start = @intCast(row_start), .end = @intCast(row_end) }; + } + const BoundedRequest = struct { dpi: c_int, minimum_width: c_int, -- cgit v1.3