From b576c238fb0e3da3ac8d22baf3f29245a5a6f2bc Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 23:48:38 -0300 Subject: add colorscheme-aware PDF tint modes --- test/pdf_bench.zig | 97 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 12 deletions(-) (limited to 'test/pdf_bench.zig') diff --git a/test/pdf_bench.zig b/test/pdf_bench.zig index 1325def5..fb911bee 100644 --- a/test/pdf_bench.zig +++ b/test/pdf_bench.zig @@ -7,9 +7,11 @@ //! //! The benchmark links the real src/pdf.zig wrapper and C exception bridge in //! ReleaseFast. Timed regions include MuPDF rasterization directly into the -//! allocator-owned RGBA output; checksum/free/reporting stay outside. "cold" -//! means a fresh Document and MuPDF page cache (the OS filesystem cache is -//! deliberately uncontrolled), while warm scenarios retain one Document. +//! allocator-owned RGBA output; tint scenarios additionally include the exact +//! in-place PDF transform used by the core. Checksum/free/reporting stay +//! outside. "cold" means a fresh Document and MuPDF page cache (the OS +//! filesystem cache is deliberately uncontrolled), while warm scenarios +//! retain one Document. const std = @import("std"); const pdf = @import("mupdf"); @@ -69,13 +71,29 @@ const Scenario = struct { id: []const u8, lifecycle: Lifecycle, decoration: Decoration, + tint: pdf.TintMode = .disabled, }; +// Fixed benchmark colors, independent of user config and host defaults. They +// are Pardes's helix page background/foreground and make run-to-run tint +// checksums a stable optimization target. +const tint_colors: pdf.TintColors = .{ + .background = .{ 0x08, 0x08, 0x08 }, + .foreground = .{ 0xbd, 0xbd, 0xbd }, +}; + +// Append only: baseline readers and the cross-scenario checks below rely on +// the original four scenarios retaining their indices and meaning. const scenarios = [_]Scenario{ .{ .id = "cold-document-plain", .lifecycle = .cold_document_page, .decoration = .plain }, .{ .id = "warm-page-plain", .lifecycle = .warm_page, .decoration = .plain }, .{ .id = "warm-page-highlighted", .lifecycle = .warm_page, .decoration = .highlighted }, .{ .id = "warm-document-pass", .lifecycle = .warm_document_pass, .decoration = .plain }, + // The checked-in design page is grayscale. Reuse the original deterministic + // highlight decoration here so filtered/full exercise source chroma and + // must retain distinct correctness checksums. + .{ .id = "warm-page-filtered", .lifecycle = .warm_page, .decoration = .highlighted, .tint = .filtered }, + .{ .id = "warm-page-full", .lifecycle = .warm_page, .decoration = .highlighted, .tint = .full }, }; const highlights = [_]pdf.Highlight{ @@ -232,7 +250,13 @@ fn measure( if (scenario.lifecycle == .cold_document_page) { for (0..rounds) |round| { - const timed = try timeCold(config.path, config.page, profile.request, scenario.decoration); + const timed = try timeCold( + config.path, + config.page, + profile.request, + scenario.decoration, + scenario.tint, + ); try verifyIdentity(&expected, timed.identity, profile, scenario, round); if (round >= config.warmup) { samples[sample_at] = timed.ns; @@ -250,12 +274,14 @@ fn measure( config.page, profile.request, scenario.decoration, + scenario.tint, ), .warm_document_pass => try timeDocumentPass( &document, multi_pages, profile.request, scenario.decoration, + scenario.tint, ), .cold_document_page => unreachable, }; @@ -288,11 +314,12 @@ fn timeCold( page: usize, request: pdf.RenderRequest, decoration: Decoration, + tint: pdf.TintMode, ) !Timed { const started = nowNs(); var document = try pdf.Document.open(path); errdefer document.deinit(); - const rendered = try renderPage(&document, page, request, decoration); + const rendered = try renderPage(&document, page, request, decoration, tint); const elapsed = @max(@as(u64, 1), nowNs() -| started); const identity = try consumeRender(rendered, page, .{}); document.deinit(); @@ -304,9 +331,10 @@ fn timePage( page: usize, request: pdf.RenderRequest, decoration: Decoration, + tint: pdf.TintMode, ) !Timed { const started = nowNs(); - const rendered = try renderPage(document, page, request, decoration); + const rendered = try renderPage(document, page, request, decoration, tint); const elapsed = @max(@as(u64, 1), nowNs() -| started); return .{ .ns = elapsed, @@ -319,6 +347,7 @@ fn timeDocumentPass( page_count: usize, request: pdf.RenderRequest, decoration: Decoration, + tint: pdf.TintMode, ) !Timed { const rendered = try gpa.alloc(pdf.Render, page_count); defer gpa.free(rendered); @@ -328,7 +357,7 @@ fn timeDocumentPass( errdefer for (rendered[0..initialized]) |item| gpa.free(item.rgba); const started = nowNs(); for (rendered, 0..) |*item, page| { - item.* = try renderPage(document, page, request, decoration); + item.* = try renderPage(document, page, request, decoration, tint); initialized += 1; } elapsed = @max(@as(u64, 1), nowNs() -| started); @@ -344,11 +373,18 @@ fn renderPage( page: usize, request: pdf.RenderRequest, decoration: Decoration, + tint: pdf.TintMode, ) !pdf.Render { - return switch (decoration) { + const rendered = switch (decoration) { .plain => document.renderAt(gpa, page, request), .highlighted => document.renderWithHighlightsAt(gpa, page, request, &highlights), }; + const fresh = try rendered; + errdefer gpa.free(fresh.rgba); + // Keep the original scenarios instruction-for-instruction identical. + // Only the appended tint scenarios enter the linear pixel pass. + if (tint != .disabled) try pdf.tintRgba(fresh.rgba, tint, tint_colors); + return fresh; } /// Hash every output byte after the timer stops. Reporting and cross-round @@ -400,12 +436,20 @@ fn checkProfileResults(results: []const Result, profile: Profile) void { const cold = results[0].identity; const warm = results[1].identity; const marked = results[2].identity; + const filtered = results[4].identity; + const full = results[5].identity; if (!cold.eql(warm)) fatal("{s}: cold and warm plain renders produced different pixels", .{profile.id}); if (warm.pixels != marked.pixels or warm.rgba_bytes != marked.rgba_bytes) fatal("{s}: highlighted render changed raster dimensions", .{profile.id}); if (warm.checksum == marked.checksum) fatal("{s}: highlighted render did not change pixels", .{profile.id}); + if (marked.pixels != filtered.pixels or marked.rgba_bytes != filtered.rgba_bytes or + marked.pixels != full.pixels or marked.rgba_bytes != full.rgba_bytes) + fatal("{s}: tint changed raster dimensions", .{profile.id}); + if (marked.checksum == filtered.checksum or marked.checksum == full.checksum or + filtered.checksum == full.checksum) + fatal("{s}: tint did not change pixels", .{profile.id}); } fn medianMpixPerSecond(result: Result) f64 { @@ -439,8 +483,22 @@ fn reportText( gpa, "pardes PDF render benchmark (ReleaseFast)\n" ++ "path: {s} ({d} pages), selected page: {d}, multi-page pass: {d} pages\n" ++ - "warmup: {d}, sampled reps: {d}; cold = fresh Document (OS cache uncontrolled)\n\n", - .{ config.path, document_pages, config.page + 1, multi_pages, config.warmup, config.reps }, + "warmup: {d}, sampled reps: {d}; cold = fresh Document (OS cache uncontrolled)\n" ++ + "tint colors: background=#{x:0>2}{x:0>2}{x:0>2}, foreground=#{x:0>2}{x:0>2}{x:0>2}\n\n", + .{ + config.path, + document_pages, + config.page + 1, + multi_pages, + config.warmup, + config.reps, + tint_colors.background[0], + tint_colors.background[1], + tint_colors.background[2], + tint_colors.foreground[0], + tint_colors.foreground[1], + tint_colors.foreground[2], + }, ) catch return; out.appendSlice( gpa, @@ -501,8 +559,23 @@ fn reportJson( gpa, "{{\"benchmark\":\"pardes-pdf-render\",\"build\":\"ReleaseFast\"," ++ "\"path\":{s},\"document_pages\":{d},\"selected_page\":{d}," ++ - "\"multi_pages\":{d},\"warmup\":{d},\"reps\":{d},\"profiles\":[", - .{ path_json, document_pages, config.page + 1, multi_pages, config.warmup, config.reps }, + "\"multi_pages\":{d},\"warmup\":{d},\"reps\":{d}," ++ + "\"tint_colors\":{{\"background\":\"{x:0>2}{x:0>2}{x:0>2}\"," ++ + "\"foreground\":\"{x:0>2}{x:0>2}{x:0>2}\"}},\"profiles\":[", + .{ + path_json, + document_pages, + config.page + 1, + multi_pages, + config.warmup, + config.reps, + tint_colors.background[0], + tint_colors.background[1], + tint_colors.background[2], + tint_colors.foreground[0], + tint_colors.foreground[1], + tint_colors.foreground[2], + }, ) catch return; for (profiles, 0..) |profile, i| { out.print( -- cgit v1.3