summaryrefslogtreecommitdiff
path: root/test/image_harness.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-16 15:49:12 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:44:42 -0300
commit1551e409c31992437cb2fa864f576d45c8433801 (patch)
treee2fae8451f87b735a1360c7c2e383fdc40165789 /test/image_harness.zig
parentbe2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff)
downloadpardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz
pardes-1551e409c31992437cb2fa864f576d45c8433801.zip
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'test/image_harness.zig')
-rw-r--r--test/image_harness.zig134
1 files changed, 99 insertions, 35 deletions
diff --git a/test/image_harness.zig b/test/image_harness.zig
index 601bce49..21ec706e 100644
--- a/test/image_harness.zig
+++ b/test/image_harness.zig
@@ -27,6 +27,9 @@ const fixture_rgb = [3]u8{ 13, 77, 231 };
// filtered-tint results for the fixture's saturated red and green pages.
const pdf_filtered_red = [3]u8{ 255, 77, 77 };
const pdf_filtered_green = [3]u8{ 0, 162, 0 };
+const pdf_filtered_blue = [3]u8{ 147, 147, 255 };
+
+const PdfFirstPage = enum { red, blue };
pub fn main(init: std.process.Init) !void {
var arena_state: std.heap.ArenaAllocator = .init(gpa);
@@ -101,9 +104,10 @@ fn appendFmt(out: *BoundedPdf, comptime fmt: []const u8, args: anytype) !void {
/// trailing pages make skipped native renders visible in Kitty's transmit
/// headers. Building it here keeps the backend test independent of mutool,
/// fonts, and checked-in binary fixtures while the xref offsets remain exact.
-fn writePdf(arena: std.mem.Allocator, path: [:0]const u8) !void {
+fn writePdf(arena: std.mem.Allocator, path: [:0]const u8, first_page: PdfFirstPage) !void {
_ = arena;
- const first_stream =
+ const first_stream = switch (first_page) {
+ .red =>
\\1 0 0 rg
\\0 0 72 48 re
\\f
@@ -113,7 +117,19 @@ fn writePdf(arena: std.mem.Allocator, path: [:0]const u8) !void {
\\4 20 Td
\\(FIRST NEEDLE) Tj
\\ET
- ;
+ ,
+ .blue =>
+ \\0 0 1 rg
+ \\0 0 72 48 re
+ \\f
+ \\0 0 0 rg
+ \\BT
+ \\/F1 10 Tf
+ \\4 20 Td
+ \\(FIRST NEEDLE) Tj
+ \\ET
+ ,
+ };
const second_stream =
\\0 1 0 rg
\\0 0 72 48 re
@@ -257,10 +273,10 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
// Keep the generated location token short enough that the 38-column PDF
// pane can visibly include each outline title after `path:page:ordinal`.
const pdf_path = try std.fmt.allocPrintSentinel(arena, "/tmp/p{d}.pdf", .{libc.getpid()}, 0);
- try writePdf(arena, pdf_path);
+ try writePdf(arena, pdf_path, .red);
defer _ = libc.unlink(pdf_path);
- var h = try eh.Harness.initArgs(gpa, exe, 16, 80, pdf_path);
+ var h = try eh.Harness.initArgs(gpa, exe, 16, 40, pdf_path);
defer h.deinit();
try h.pump(150);
try h.expectRawContains("\x1b_Gi=1,a=q\x1b\\", "app did not issue the Kitty graphics capability query");
@@ -334,9 +350,13 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
// A viewport step crosses the boundary continuously: page one leaves and
// is deleted, page two remains cached, and newly intersecting page three
- // is transmitted. Distinct fixture heights identify the latter on wire.
+ // is transmitted as the 64-row grain covering its visible head. The new
+ // post-step transmit identifies that page without requiring an offscreen
+ // whole-page raster during a viewport-sized fling.
const page_step_start = h.raw.items.len;
- const page_three_transmit = "\x1b_Gf=32,s=96,v=67,i=";
+ const all_transmits = "\x1b_Gf=32,s=";
+ const transmits_before_step = std.mem.count(u8, h.raw.items, all_transmits);
+ const page_three_transmit = "\x1b_Gf=32,s=96,v=64,i=";
try h.send("\x06");
try h.expectWaitFor("pdf 2/4 width PdfFit", 5000, "continuous page-down did not enter PDF page two");
const page_step_deadline = eh.nowMs() + 5000;
@@ -347,29 +367,31 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
return error.KittyPdfOffscreenPageNotReleased;
if (std.mem.indexOf(u8, h.raw.items[page_step_start..], page_three_transmit) == null)
return error.KittyPdfNewVisiblePageMissing;
- if (std.mem.count(u8, h.raw.items, transmit) != initial_transmits)
+ if (std.mem.count(u8, h.raw.items, all_transmits) != transmits_before_step + 1)
return error.KittyPdfRetainedNeighborRetransmitted;
// Search still addresses semantic pages inside the strip. Returning one
- // viewport, then revealing page two's hit, creates a decorated revision.
+ // viewport, then submitting the bare search selects and Looks its first
+ // hit through the shared walk, creating a decorated page-two revision.
try h.send("\x02");
try h.expectWaitFor("pdf 1/4 width PdfFit", 5000, "continuous page-up did not return to PDF page one");
+ const transmits_before_search = std.mem.count(u8, h.raw.items, all_transmits);
try h.send("/SECOND\r");
const search_row = try std.fmt.allocPrint(arena, "{s}:2:1 SECOND", .{std.fs.path.basename(pdf_path)});
try h.expectWaitFor(search_row, 5000, "PDF search did not preserve the exact page-two query occurrence");
- const before_look = std.mem.count(u8, h.raw.items, transmit);
- try h.send("n");
const look_deadline = eh.nowMs() + 5000;
while (eh.nowMs() < look_deadline) {
_ = try h.pumpOnce(50);
- if (std.mem.count(u8, h.raw.items, transmit) > before_look and h.contains("pdf 2/4 width PdfFit")) break;
+ if (std.mem.count(u8, h.raw.items, all_transmits) > transmits_before_search and
+ h.contains("pdf 2/4 width PdfFit")) break;
}
- try h.expectContains("pdf 2/4 width PdfFit", "n did not reveal the PDF search result at page two");
- if (std.mem.count(u8, h.raw.items, transmit) <= before_look) return error.KittyPdfLookRetransmitMissing;
+ try h.expectContains("pdf 2/4 width PdfFit", "bare search did not reveal its first PDF hit at page two");
+ if (std.mem.count(u8, h.raw.items, all_transmits) <= transmits_before_search)
+ return error.KittyPdfLookRetransmitMissing;
// PdfFit changes only placement. The active decorated raster remains the
// same terminal image while height fit supplies an explicit row extent.
- const before_fit = std.mem.count(u8, h.raw.items, transmit);
+ const before_fit = std.mem.count(u8, h.raw.items, all_transmits);
const fit_raw_start = h.raw.items.len;
try h.send(" tz");
try h.expectWaitFor("pdf 2/4 height PdfFit", 5000, "PdfFit did not expose height fit");
@@ -382,12 +404,12 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
const height_crop_w = kittyParam(height_apc, 'w') orelse return error.KittyPdfHeightPlacementWrong;
if (height_crop_w == 0 or height_crop_w > 96 or kittyParam(height_apc, 'h') != 64)
return error.KittyPdfHeightPlacementWrong;
- if (std.mem.count(u8, h.raw.items, transmit) != before_fit)
+ if (std.mem.count(u8, h.raw.items, all_transmits) != before_fit)
return error.KittyPdfFitRetransmitted;
// The generated PDF carries a real nested outline. PdfSections must show
- // all three DFS rows in its output pane while leaving n/N ownership on
- // the PDF. The first two destinations share page one but have distinct
+ // all three DFS rows in its output pane. n/N only select rows; Enter Looks
+ // the selected destination. The first two share page one but have distinct
// page-space y coordinates; their placement APCs must differ while the
// second reveal reuses exactly the rasters made visible by the first.
try h.send(" ts");
@@ -396,8 +418,8 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
try h.expectContains("Page two", "PdfSections did not expose the other-page outline row");
const upper_start = h.raw.items.len;
- try h.send("n");
- try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "first PdfSections n did not reveal page one");
+ try h.send("n\r");
+ try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "first PdfSections n+Look did not reveal page one");
const upper_deadline = eh.nowMs() + 3000;
while (eh.nowMs() < upper_deadline and kittyPlacementApc(h.raw.items, upper_start) == null)
_ = try h.pumpOnce(50);
@@ -408,8 +430,8 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
const same_page_transmits = std.mem.count(u8, h.raw.items, "\x1b_Gf=32,");
const lower_start = h.raw.items.len;
- try h.send("n");
- try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "nested PdfSections n left page one");
+ try h.send("n\r");
+ try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "nested PdfSections n+Look left page one");
const lower_deadline = eh.nowMs() + 3000;
while (eh.nowMs() < lower_deadline and kittyPlacementApc(h.raw.items, lower_start) == null)
_ = try h.pumpOnce(50);
@@ -420,12 +442,32 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
if (std.mem.count(u8, h.raw.items, "\x1b_Gf=32,") != same_page_transmits)
return error.KittyPdfSectionsSamePageRetransmitted;
- try h.send("n");
- try h.expectWaitFor("pdf 2/4 height PdfFit", 5000, "third PdfSections n did not reveal page two");
- try h.send("N");
- try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "PdfSections N did not return to the nested page-one row");
+ try h.send("n\r");
+ try h.expectWaitFor("pdf 2/4 height PdfFit", 5000, "third PdfSections n+Look did not reveal page two");
+ try h.send("N\r");
+ try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "PdfSections N+Look did not return to the nested page-one row");
- std.debug.print("PDF harness ok: Kitty continuous cache + search/fit + nested PdfSections n/N\n", .{});
+ // Editor-style save: build a same-sized blue first page on another inode,
+ // atomically rename it over the live pathname, and send NO input. The
+ // inotify wake alone must reopen MuPDF and transmit a fresh raster.
+ const replacement_path = try std.fmt.allocPrintSentinel(arena, "{s}.replacement", .{pdf_path}, 0);
+ defer _ = libc.unlink(replacement_path);
+ try writePdf(arena, replacement_path, .blue);
+ try h.pump(250);
+ const before_reload = std.mem.count(u8, h.raw.items, all_transmits);
+ try h.pump(250);
+ if (std.mem.count(u8, h.raw.items, all_transmits) != before_reload)
+ return error.KittyPdfNavigationDidNotSettle;
+ if (libc.rename(replacement_path, pdf_path) != 0) return error.PdfLiveRenameFailed;
+ const reload_deadline = eh.nowMs() + 5000;
+ while (eh.nowMs() < reload_deadline and
+ std.mem.count(u8, h.raw.items, all_transmits) <= before_reload)
+ _ = try h.pumpOnce(50);
+ if (std.mem.count(u8, h.raw.items, all_transmits) <= before_reload)
+ return error.KittyPdfLiveReloadMissing;
+ try h.expectWaitFor("reloaded", 5000, "PDF inotify reload did not report a committed transaction");
+
+ std.debug.print("PDF harness ok: Kitty continuous cache + search/fit + sections + idle live reload\n", .{});
}
fn kittyPlacementApc(raw: []const u8, start: usize) ?[]const u8 {
@@ -498,7 +540,7 @@ fn runGui(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !void
fn runGuiPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !void {
const pdf_path = try std.fmt.allocPrintSentinel(arena, "{s}/pages.pdf", .{base}, 0);
const capture_path = try std.fmt.allocPrintSentinel(arena, "{s}/latest.ppm", .{base}, 0);
- try writePdf(arena, pdf_path);
+ try writePdf(arena, pdf_path, .red);
_ = libc.unlink(capture_path);
_ = setenv("PARDES_TEST", "1", 1);
_ = setenv("PARDES_TEST_CAPTURE_DIR", base, 1);
@@ -532,7 +574,10 @@ fn runGuiPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !v
fit_width.width() - fit_height.width()
else
fit_height.width() - fit_width.width();
- if (fit_height.height() < fit_width.height() * 2 or width_delta > 8) {
+ // A positional PDF now owns the full single column, so width fit already
+ // occupies much of this short test window. Height fit must still extend
+ // the colored page by at least a third while preserving its clipped width.
+ if (fit_height.height() * 3 < fit_width.height() * 4 or width_delta > 8) {
std.debug.print("SDL PDF fit extents did not change as expected: width={any}, height={any}\n", .{ fit_width, fit_height });
return error.SdlPdfFitGeometryWrong;
}
@@ -554,7 +599,8 @@ fn runGuiPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !v
const final_pixels = ppmPixels(final_capture) orelse return error.BadCapture;
if (countColor(final_pixels, pdf_filtered_red) != 0) return error.SdlPdfOffscreenPageStillDrawn;
- // Exercise the actual PdfSections output + Look stepping path in SDL too.
+ // Exercise the actual PdfSections output + n/N selection + Look path in
+ // SDL too.
// The first two nested outline rows stay on red page one at different y
// positions, the third moves to green page two, and N returns exactly to
// the nested page-one destination.
@@ -565,19 +611,37 @@ fn runGuiPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !v
try h.send(" ts");
_ = try waitForCaptureBounds(&h, capture_path, pdf_filtered_red, 500, 5000);
_ = libc.unlink(capture_path);
- try h.send("n");
+ try h.send("n\r");
_ = try waitForCaptureBounds(&h, capture_path, pdf_filtered_red, 200, 5000);
_ = libc.unlink(capture_path);
- try h.send("n");
+ try h.send("n\r");
_ = try waitForCaptureBounds(&h, capture_path, pdf_filtered_red, 100, 5000);
_ = libc.unlink(capture_path);
- try h.send("n");
+ try h.send("n\r");
_ = try waitForCaptureBounds(&h, capture_path, pdf_filtered_green, 200, 5000);
_ = libc.unlink(capture_path);
- try h.send("N");
+ try h.send("N\r");
_ = try waitForCaptureBounds(&h, capture_path, pdf_filtered_red, 100, 5000);
- std.debug.print("PDF harness ok: SDL continuous strip + nested PdfSections red->green->red ({d}->{d} pixels)\n", .{ fit_width.count, green.count });
+ // The GUI host receives the same parent-directory rename while idle. A
+ // newly captured blue first page proves the wake reached MuPDF, replaced
+ // the pane raster, and reached the actual GPU output without a key event.
+ const replacement_path = try std.fmt.allocPrintSentinel(arena, "{s}/pages.replacement.pdf", .{base}, 0);
+ defer _ = libc.unlink(replacement_path);
+ try writePdf(arena, replacement_path, .blue);
+ _ = libc.unlink(capture_path);
+ if (libc.rename(replacement_path, pdf_path) != 0) return error.PdfLiveRenameFailed;
+ const blue = try waitForCaptureBounds(&h, capture_path, pdf_filtered_blue, 100, 5000);
+ const reloaded_capture = try readFile(gpa, capture_path);
+ defer gpa.free(reloaded_capture);
+ const reloaded_pixels = ppmPixels(reloaded_capture) orelse return error.BadCapture;
+ if (countColor(reloaded_pixels, pdf_filtered_red) != 0)
+ return error.SdlPdfLiveReloadRetainedOldPixels;
+
+ std.debug.print(
+ "PDF harness ok: SDL continuous strip + sections + idle red->blue reload ({d}->{d}->{d} pixels)\n",
+ .{ fit_width.count, green.count, blue.count },
+ );
}
const ColorBounds = struct {