1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
|
//! Pointer gestures over a native PDF pane: hovering words (the Look preview
//! highlight), and dragging a selection to its release.
//!
//! zig build pdf-pointer-bench -Doptimize=ReleaseFast -- [--label L] [--reps N] [--path FILE]
//! [--cell WxH] [--scroll NOTCHES] [--verify 1] [--live 1]
//!
//! Every frame is timed the way a shell pump spends it: the event through
//! `Pardes.update`, one `render()` into a reset arena, the effect drain. A
//! hover step is its motion frame plus every tick frame the preview animates
//! before settling, which is when the highlight lands. Stdout is CSV,
//! `label,scenario:kind,round,ns`, one row per frame and one per step
//! (`kind=step`), for an external statistics pass.
//!
//! `visual` hashes every presented raster in full, with its placement, on the
//! frame each step settles on. It is the no-regression proof: a change to how highlights
//! are painted may change what it costs, never one presented pixel.
const std = @import("std");
const pardes = @import("pardes");
const pdf = @import("mupdf");
pub const std_options: std.Options = .{ .log_level = .err };
const gpa = std.heap.smp_allocator;
const cols: u16 = 120;
const rows: u16 = 40;
const Config = struct {
path: []const u8 = "docs/design.pdf",
label: []const u8 = "run",
reps: usize = 5,
/// Wheel notches before the gestures, off the sparse title page.
scroll: usize = 40,
/// 8x16 is the Kitty policy's cell; the SDL shell's is closer to 16x35.
cell_w: u16 = 8,
cell_h: u16 = 16,
/// Re-render every raster that claims to hold the current highlights and
/// compare it row by row: exactness without a baseline build.
verify: bool = false,
/// A shell that takes row patches: selections repaint while dragged.
live: bool = false,
};
pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(init.arena.allocator());
var config: Config = .{};
var i: usize = 1;
while (i + 1 < args.len) : (i += 2) {
const value = args[i + 1];
if (std.mem.eql(u8, args[i], "--path")) config.path = value else if (std.mem.eql(u8, args[i], "--label")) config.label = value else if (std.mem.eql(u8, args[i], "--reps")) config.reps = try std.fmt.parseUnsigned(usize, value, 10) else if (std.mem.eql(u8, args[i], "--scroll")) config.scroll = try std.fmt.parseUnsigned(usize, value, 10) else if (std.mem.eql(u8, args[i], "--verify")) config.verify = std.mem.eql(u8, value, "1") else if (std.mem.eql(u8, args[i], "--live")) config.live = std.mem.eql(u8, value, "1") else if (std.mem.eql(u8, args[i], "--cell")) {
var it = std.mem.splitScalar(u8, value, 'x');
config.cell_w = try std.fmt.parseUnsigned(u16, it.next() orelse "", 10);
config.cell_h = try std.fmt.parseUnsigned(u16, it.next() orelse "", 10);
} else return error.UnknownOption;
}
if (i != args.len) return error.MissingValue;
var visual: ?u64 = null;
for (0..config.reps) |round| {
const got = try measure(config, round);
if (visual) |want| if (want != got) {
std.debug.print("pdf-pointer-bench: visual changed between rounds: {x} vs {x}\n", .{ want, got });
return error.Unstable;
};
visual = got;
}
std.debug.print("pdf-pointer-bench {s}: visual={x:0>16}\n", .{ config.label, visual.? });
}
const Run = struct {
core: *pardes.Pardes,
arena: std.heap.ArenaAllocator,
config: Config,
round: usize,
visual: u64 = 0x7061_7264_6573_5050,
step_ns: u64 = 0,
frames: usize = 0,
steps: usize = 0,
last: ?*const pardes.Surface = null,
fn frame(run: *Run, scenario: []const u8, kind: []const u8, event: pardes.Event) !void {
const started = nowNs();
run.core.update(event);
while (run.core.nextEffect()) |_| {}
_ = run.arena.reset(.retain_capacity);
const surface = try run.core.render(run.arena.allocator());
while (run.core.nextEffect()) |_| {}
const elapsed = nowNs() - started;
run.step_ns += elapsed;
run.frames += 1;
print("{s},{s}:{s},{d},{d}\n", .{ run.config.label, scenario, kind, run.round, elapsed });
run.last = surface;
if (run.config.verify) try verify(run.core, run.arena.allocator());
}
/// The step, then the tick frames its animation asks for (a hover's
/// preview arrives on one of them), as a pump with nothing else queued.
fn step(run: *Run, scenario: []const u8, kind: []const u8, event: pardes.Event) !void {
run.step_ns = 0;
try run.frame(scenario, kind, event);
var ticks: usize = 0;
while (run.core.animationActive() and ticks < 32) : (ticks += 1)
try run.frame(scenario, "tick", .tick);
print("{s},{s}:step,{d},{d}\n", .{ run.config.label, scenario, run.round, run.step_ns });
// Only the settled frame is the invariant: how many frames a preview
// takes to arrive is a setting (look_preview_delay_frames).
run.observe(run.last.?);
run.steps += 1;
}
fn observe(run: *Run, surface: *const pardes.Surface) void {
for (surface.images[0..surface.nimages]) |maybe| {
const place = maybe orelse continue;
run.visual = mix(run.visual, [_]u64{ place.pane, place.x, place.y, place.w, place.h, place.native.page, place.iw, place.ih });
if (place.native.geometry) |g| run.visual = mix(run.visual, [_]u64{ g.src.x, g.src.y, g.src.w, g.src.h, g.dst.x, g.dst.y, g.dst.w, g.dst.h });
run.visual = std.hash.Wyhash.hash(run.visual, place.rgba);
}
}
};
var verify_failures: usize = 0;
fn verify(core: *pardes.Pardes, arena: std.mem.Allocator) !void {
const Pdf = pardes.panes.Pdf;
const pane = core.panes[0] orelse return;
const state = &(pane.pdf orelse return);
const highlights = try Pdf.buildHighlights(state, arena, Pdf.highlightInput(core, 0, pane));
for (state.rasters[0..state.rasters_len]) |*r| {
if (!r.valid or r.rgba.len == 0 or r.tint_key == null) continue;
const want = highlights.forPage(r.page);
if (comptime !@hasField(@TypeOf(r.*), "baked")) return else if (!std.mem.eql(u8, std.mem.sliceAsBytes(want), std.mem.sliceAsBytes(r.baked))) continue;
const shape = try state.document.measureRenderAt(r.page, r.request);
// The oracle is the WHOLE page: a band's own edge rows can differ.
const whole = try arena.alloc(u8, shape.len);
try state.document.renderIntoAtWithPaper(r.page, r.request, shape, shape.wholePage(), want, whole, false);
try pdf.tintRgba(whole, r.tint_key.?.mode, r.tint_key.?.colors);
const fresh = whole[r.band_y * shape.stride ..][0 .. r.band_h * shape.stride];
var first: ?usize = null;
var last: usize = 0;
var count: usize = 0;
for (0..r.band_h) |y| {
const row = fresh[y * shape.stride ..][0..shape.stride];
if (std.mem.eql(u8, row, r.rgba[y * shape.stride ..][0..shape.stride])) continue;
if (first == null) first = y;
last = y;
count += 1;
}
if (first) |f| {
verify_failures += 1;
if (verify_failures <= 20) std.debug.print("verify: page {d} rows {d}..{d} ({d} differ), highlights {d}, band {d}+{d}\n", .{ r.page, f + r.band_y, last + r.band_y, count, want.len, r.band_y, r.band_h });
}
}
}
fn measure(config: Config, round: usize) !u64 {
const core = try pardes.Pardes.init(gpa, .{ .file = config.path, .cols = cols, .rows = rows });
defer core.deinit();
core.native_images = true;
if (comptime @hasField(pardes.Pardes, "native_image_patches")) core.native_image_patches = config.live;
var run: Run = .{ .core = core, .arena = .init(gpa), .config = config, .round = round };
defer run.arena.deinit();
const cell: pardes.CellPixels = .{ .w = config.cell_w, .h = config.cell_h };
core.update(.{ .resize = .{ .cols = cols, .rows = rows, .cell_pixels = cell } });
_ = try core.render(run.arena.allocator());
const pane = core.panes[0] orelse return error.NoPane;
if (pane.pdf == null) return error.NotPdf;
const rect = core.rects[0];
const mid_col = rect.x + rect.w / 2;
const mid_row = rect.y + rect.h / 2;
for (0..config.scroll) |_| {
core.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mid_col, .row = mid_row } });
_ = run.arena.reset(.retain_capacity);
_ = try core.render(run.arena.allocator());
while (core.nextEffect()) |_| {}
}
const top = rect.y + 4;
const bottom = rect.y + rect.h -| 3;
const left = rect.x + 4;
const right = rect.x + rect.w -| 4;
// Hover: walk four rows of the body, a word-ish stride at a time.
var row: u16 = top;
while (row < bottom) : (row += (bottom - top) / 4) {
var col: u16 = left;
while (col < right) : (col += 3)
try run.step("hover", "motion", .{ .mouse = .{ .button = .none, .kind = .motion, .col = col, .row = row } });
}
// Leave the page so the last preview is withdrawn like a real exit.
try run.step("hover", "leave", .pointer_leave);
// Select: press, drag across three lines, release; five times down the page.
row = top;
while (row + 3 < bottom) : (row += (bottom - top) / 5) {
try run.step("select", "press", .{ .mouse = .{ .button = .left, .kind = .press, .col = left, .row = row } });
var n: u16 = 1;
while (n <= 24) : (n += 1) {
const col = left + (right - left) * (n % 8) / 8;
try run.step("select", "drag", .{ .mouse = .{ .button = .left, .kind = .drag, .col = col, .row = row + n / 8 } });
}
try run.step("select", "release", .{ .mouse = .{ .button = .left, .kind = .release, .col = right, .row = row + 3 } });
}
return mix(run.visual, @as(u64, run.steps));
}
fn print(comptime format: []const u8, args: anytype) void {
var buf: [256]u8 = undefined;
const line = std.fmt.bufPrint(&buf, format, args) catch return;
_ = std.c.write(1, line.ptr, line.len);
}
fn mix(seed: u64, value: anytype) u64 {
var copy = value;
return std.hash.Wyhash.hash(seed, std.mem.asBytes(©));
}
fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}
comptime {
_ = pdf.Document;
}
|