a73x

8f9e9953

Extract cell_instance helpers into shared module

a73x   2026-04-17 15:44

Commit message
Extract cell_instance helpers into shared module

appendCellInstances and glyphTopOffset were duplicated between
main.zig and capture.zig. Move both to src/cell_instance.zig so
both callers (and future test_render) share one implementation.

Also drop unused alloc param from playScript and rename the
visibility constant to VISIBILITY_TIMEOUT_NS to reflect it is
a duration, not a deadline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

build.zig
Old New
@@ -302,6 +302,18 @@ pub fn build(b: *std.Build) void {
302 const png_tests = b.addTest(.{ .root_module = png_test_mod }); 302 const png_tests = b.addTest(.{ .root_module = png_test_mod });
303 test_step.dependOn(&b.addRunArtifact(png_tests).step); 303 test_step.dependOn(&b.addRunArtifact(png_tests).step);
304 304
305 // cell_instance module — shared appendCellInstances / glyphTopOffset helpers
306 const cell_instance_mod = b.createModule(.{
307 .root_source_file = b.path("src/cell_instance.zig"),
308 .target = target,
309 .optimize = optimize,
310 });
311 cell_instance_mod.addImport("renderer", renderer_mod);
312 cell_instance_mod.addImport("font", font_mod);
313 cell_instance_mod.addImport("vt", vt_mod);
314 exe_mod.addImport("cell_instance", cell_instance_mod);
315 main_test_mod.addImport("cell_instance", cell_instance_mod);
316
305 // capture module — --capture mode (render a VT script to PNG) 317 // capture module — --capture mode (render a VT script to PNG)
306 const capture_mod = b.createModule(.{ 318 const capture_mod = b.createModule(.{
307 .root_source_file = b.path("src/capture.zig"), 319 .root_source_file = b.path("src/capture.zig"),
@@ -317,5 +329,6 @@ pub fn build(b: *std.Build) void {
317 capture_mod.addImport("config", config_mod); 329 capture_mod.addImport("config", config_mod);
318 capture_mod.addImport("png", png_mod); 330 capture_mod.addImport("png", png_mod);
319 capture_mod.addImport("vulkan", vulkan_module); 331 capture_mod.addImport("vulkan", vulkan_module);
332 capture_mod.addImport("cell_instance", cell_instance_mod);
320 exe_mod.addImport("capture", capture_mod); 333 exe_mod.addImport("capture", capture_mod);
321 } 334 }
src/capture.zig
Old New
@@ -36,9 +36,13 @@ pub const CaptureError = error{
36 PngEncodeFailed, 36 PngEncodeFailed,
37 }; 37 };
38 38
39 const cell_instance = @import("cell_instance");
40 const appendCellInstances = cell_instance.appendCellInstances;
41 const glyphTopOffset = cell_instance.glyphTopOffset;
42
39 const CAPTURE_COLS: u16 = 80; 43 const CAPTURE_COLS: u16 = 80;
40 const CAPTURE_ROWS: u16 = 24; 44 const CAPTURE_ROWS: u16 = 24;
41 const VISIBILITY_DEADLINE_NS: i128 = 3 * std.time.ns_per_s; 45 const VISIBILITY_TIMEOUT_NS: i128 = 3 * std.time.ns_per_s;
42 46
43 /// Entry point. `argv[0]` is `--capture`; argv[1] = script path, argv[2] = out path. 47 /// Entry point. `argv[0]` is `--capture`; argv[1] = script path, argv[2] = out path.
44 pub fn run(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void { 48 pub fn run(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void {
@@ -147,7 +151,7 @@ pub fn run(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void {
147 } 151 }
148 152
149 // === play script through /bin/cat === 153 // === play script through /bin/cat ===
150 try playScript(alloc, term, script_path); 154 try playScript(term, script_path);
151 155
152 // === snapshot + build instances === 156 // === snapshot + build instances ===
153 try term.snapshot(); 157 try term.snapshot();
@@ -195,14 +199,14 @@ pub fn run(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void {
195 std.debug.print("capture: wrote {s} ({d}x{d})\n", .{ out_path, px_w, px_h }); 199 std.debug.print("capture: wrote {s} ({d}x{d})\n", .{ out_path, px_w, px_h });
196 } 200 }
197 201
198 /// Wait up to VISIBILITY_DEADLINE_NS for the Wayland compositor to `configure` 202 /// Wait up to VISIBILITY_TIMEOUT_NS for the Wayland compositor to `configure`
199 /// the surface. For `--capture` we don't need the surface to actually be 203 /// the surface. For `--capture` we don't need the surface to actually be
200 /// mapped onto an output (which would require committing a presentable 204 /// mapped onto an output (which would require committing a presentable
201 /// buffer via the swapchain — we deliberately skip that since rendering is 205 /// buffer via the swapchain — we deliberately skip that since rendering is
202 /// offscreen). A configured surface is enough to know our fixed 80x24 206 /// offscreen). A configured surface is enough to know our fixed 80x24
203 /// geometry was accepted. 207 /// geometry was accepted.
204 fn waitUntilVisible(conn: *wayland_client.Connection, window: *wayland_client.Window) !void { 208 fn waitUntilVisible(conn: *wayland_client.Connection, window: *wayland_client.Window) !void {
205 const deadline = @as(i128, std.time.nanoTimestamp()) + VISIBILITY_DEADLINE_NS; 209 const deadline = @as(i128, std.time.nanoTimestamp()) + VISIBILITY_TIMEOUT_NS;
206 while (std.time.nanoTimestamp() < deadline) { 210 while (std.time.nanoTimestamp() < deadline) {
207 _ = conn.display.roundtrip(); 211 _ = conn.display.roundtrip();
208 if (window.state.configured) return; 212 if (window.state.configured) return;
@@ -218,13 +222,13 @@ fn waitUntilVisible(conn: *wayland_client.Connection, window: *wayland_client.Wi
218 /// Spawn `/bin/cat <script>` on a PTY; feed all its output into `term`. 222 /// Spawn `/bin/cat <script>` on a PTY; feed all its output into `term`.
219 /// Returns once the child has exited AND two consecutive 20 ms polls 223 /// Returns once the child has exited AND two consecutive 20 ms polls
220 /// produce no new bytes (drain). 224 /// produce no new bytes (drain).
225 ///
226 /// Note: spawns cat with script as argv rather than piping stdin+^D —
227 /// avoids EOF-signalling races with VT escape sequences.
221 fn playScript( 228 fn playScript(
222 alloc: std.mem.Allocator,
223 term: *vt.Terminal, 229 term: *vt.Terminal,
224 script_path: [:0]const u8, 230 script_path: [:0]const u8,
225 ) !void { 231 ) !void {
226 _ = alloc;
227
228 var p = try pty.Pty.spawn(.{ 232 var p = try pty.Pty.spawn(.{
229 .cols = CAPTURE_COLS, 233 .cols = CAPTURE_COLS,
230 .rows = CAPTURE_ROWS, 234 .rows = CAPTURE_ROWS,
@@ -320,52 +324,6 @@ fn buildInstancesForSnapshot(
320 } 324 }
321 } 325 }
322 326
323 /// Mirror of main.zig's `appendCellInstances` (kept local to avoid making
324 /// that function public just for this module). Appends 0-2 instances per
325 /// cell: a filled-background quad if the bg differs from the terminal
326 /// default, plus the glyph quad if the cell has a printable codepoint.
327 fn appendCellInstances(
328 alloc: std.mem.Allocator,
329 instances: *std.ArrayListUnmanaged(renderer.Instance),
330 row_idx: u32,
331 col_idx: u32,
332 cell_w: u32,
333 cell_h: u32,
334 baseline: u32,
335 glyph_uv: ?font.GlyphUV,
336 bg_uv: font.GlyphUV,
337 colors: vt.CellColors,
338 default_bg: [4]f32,
339 ) !void {
340 if (!std.meta.eql(colors.bg, default_bg)) {
341 try instances.append(alloc, .{
342 .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) },
343 .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h) },
344 .glyph_bearing = .{ 0, 0 },
345 .uv_rect = .{ bg_uv.u0, bg_uv.v0, bg_uv.u1, bg_uv.v1 },
346 .fg = colors.bg,
347 .bg = colors.bg,
348 });
349 }
350
351 const uv = glyph_uv orelse return;
352 try instances.append(alloc, .{
353 .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) },
354 .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) },
355 .glyph_bearing = .{
356 @floatFromInt(uv.bearing_x),
357 glyphTopOffset(baseline, uv.bearing_y),
358 },
359 .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 },
360 .fg = colors.fg,
361 .bg = colors.bg,
362 });
363 }
364
365 fn glyphTopOffset(baseline: u32, bearing_y: i32) f32 {
366 return @as(f32, @floatFromInt(baseline)) - @as(f32, @floatFromInt(bearing_y));
367 }
368
369 /// Encode `rgba` as a PNG to a brand-new file at `path`. Buffers the full 327 /// Encode `rgba` as a PNG to a brand-new file at `path`. Buffers the full
370 /// encoded byte stream in memory (fine for 80x24@16px: under 200 KB) and 328 /// encoded byte stream in memory (fine for 80x24@16px: under 200 KB) and
371 /// writes it in one shot. 329 /// writes it in one shot.
src/cell_instance.zig
Old New
@@ -0,0 +1,58 @@
1 //! Shared cell-instance helpers used by both the live renderer (main.zig)
2 //! and the capture renderer (capture.zig).
3
4 const std = @import("std");
5 const renderer = @import("renderer");
6 const font = @import("font");
7 const vt = @import("vt");
8
9 /// Appends 0-2 `renderer.Instance` entries for a single terminal cell:
10 /// - a filled-background quad when the cell's bg differs from the terminal
11 /// default bg (so transparent cells don't draw a quad at all);
12 /// - a glyph quad when `glyph_uv` is non-null (i.e. the cell has a
13 /// printable codepoint that was found in the atlas).
14 pub fn appendCellInstances(
15 alloc: std.mem.Allocator,
16 instances: *std.ArrayListUnmanaged(renderer.Instance),
17 row_idx: u32,
18 col_idx: u32,
19 cell_w: u32,
20 cell_h: u32,
21 baseline: u32,
22 glyph_uv: ?font.GlyphUV,
23 bg_uv: font.GlyphUV,
24 colors: vt.CellColors,
25 default_bg: [4]f32,
26 ) !void {
27 if (!std.meta.eql(colors.bg, default_bg)) {
28 try instances.append(alloc, .{
29 .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) },
30 .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h) },
31 .glyph_bearing = .{ 0, 0 },
32 .uv_rect = .{ bg_uv.u0, bg_uv.v0, bg_uv.u1, bg_uv.v1 },
33 .fg = colors.bg,
34 .bg = colors.bg,
35 });
36 }
37
38 const uv = glyph_uv orelse return;
39 try instances.append(alloc, .{
40 .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) },
41 .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) },
42 .glyph_bearing = .{
43 @floatFromInt(uv.bearing_x),
44 glyphTopOffset(baseline, uv.bearing_y),
45 },
46 .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 },
47 .fg = colors.fg,
48 .bg = colors.bg,
49 });
50 }
51
52 /// Returns the number of pixels from the top of the cell to the top of the
53 /// glyph bitmap, given the cell `baseline` (pixels from cell top to the
54 /// typographic baseline) and the glyph's `bearing_y` (pixels from baseline
55 /// to the top of the glyph bitmap, positive = up).
56 pub fn glyphTopOffset(baseline: u32, bearing_y: i32) f32 {
57 return @as(f32, @floatFromInt(baseline)) - @as(f32, @floatFromInt(bearing_y));
58 }
src/main.zig
Old New
@@ -8,6 +8,9 @@ const font = @import("font");
8 const config = @import("config"); 8 const config = @import("config");
9 const vk = @import("vulkan"); 9 const vk = @import("vulkan");
10 const bench_stats = @import("bench_stats"); 10 const bench_stats = @import("bench_stats");
11 const cell_instance = @import("cell_instance");
12 const appendCellInstances = cell_instance.appendCellInstances;
13 const glyphTopOffset = cell_instance.glyphTopOffset;
11 const FrameTiming = bench_stats.FrameTiming; 14 const FrameTiming = bench_stats.FrameTiming;
12 const FrameTimingRing = bench_stats.FrameTimingRing; 15 const FrameTimingRing = bench_stats.FrameTimingRing;
13 const SectionStats = bench_stats.SectionStats; 16 const SectionStats = bench_stats.SectionStats;
@@ -1494,48 +1497,6 @@ fn cursorTouchesDirtyRow(dirty_rows: []const bool, cursor: CursorRefreshContext)
1494 return false; 1497 return false;
1495 } 1498 }
1496 1499
1497 fn appendCellInstances(
1498 alloc: std.mem.Allocator,
1499 instances: *std.ArrayListUnmanaged(renderer.Instance),
1500 row_idx: u32,
1501 col_idx: u32,
1502 cell_w: u32,
1503 cell_h: u32,
1504 baseline: u32,
1505 glyph_uv: ?font.GlyphUV,
1506 bg_uv: font.GlyphUV,
1507 colors: vt.CellColors,
1508 default_bg: [4]f32,
1509 ) !void {
1510 if (!std.meta.eql(colors.bg, default_bg)) {
1511 try instances.append(alloc, .{
1512 .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) },
1513 .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h) },
1514 .glyph_bearing = .{ 0, 0 },
1515 .uv_rect = .{ bg_uv.u0, bg_uv.v0, bg_uv.u1, bg_uv.v1 },
1516 .fg = colors.bg,
1517 .bg = colors.bg,
1518 });
1519 }
1520
1521 const uv = glyph_uv orelse return;
1522 try instances.append(alloc, .{
1523 .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) },
1524 .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) },
1525 .glyph_bearing = .{
1526 @floatFromInt(uv.bearing_x),
1527 glyphTopOffset(baseline, uv.bearing_y),
1528 },
1529 .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 },
1530 .fg = colors.fg,
1531 .bg = colors.bg,
1532 });
1533 }
1534
1535 fn glyphTopOffset(baseline: u32, bearing_y: i32) f32 {
1536 return @as(f32, @floatFromInt(baseline)) - @as(f32, @floatFromInt(bearing_y));
1537 }
1538
1539 fn encodeKeyboardEvent( 1500 fn encodeKeyboardEvent(
1540 term: *const vt.Terminal, 1501 term: *const vt.Terminal,
1541 ev: wayland_client.KeyboardEvent, 1502 ev: wayland_client.KeyboardEvent,