8f9e9953
Extract cell_instance helpers into shared module
a73x 2026-04-17 15:44
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>
diff --git a/build.zig b/build.zig index ebc0043..b11cf46 100644 --- a/build.zig +++ b/build.zig @@ -302,6 +302,18 @@ pub fn build(b: *std.Build) void { const png_tests = b.addTest(.{ .root_module = png_test_mod }); test_step.dependOn(&b.addRunArtifact(png_tests).step); // cell_instance module — shared appendCellInstances / glyphTopOffset helpers const cell_instance_mod = b.createModule(.{ .root_source_file = b.path("src/cell_instance.zig"), .target = target, .optimize = optimize, }); cell_instance_mod.addImport("renderer", renderer_mod); cell_instance_mod.addImport("font", font_mod); cell_instance_mod.addImport("vt", vt_mod); exe_mod.addImport("cell_instance", cell_instance_mod); main_test_mod.addImport("cell_instance", cell_instance_mod); // capture module — --capture mode (render a VT script to PNG) const capture_mod = b.createModule(.{ .root_source_file = b.path("src/capture.zig"), @@ -317,5 +329,6 @@ pub fn build(b: *std.Build) void { capture_mod.addImport("config", config_mod); capture_mod.addImport("png", png_mod); capture_mod.addImport("vulkan", vulkan_module); capture_mod.addImport("cell_instance", cell_instance_mod); exe_mod.addImport("capture", capture_mod); } diff --git a/src/capture.zig b/src/capture.zig index 843f2f0..1c82e21 100644 --- a/src/capture.zig +++ b/src/capture.zig @@ -36,9 +36,13 @@ pub const CaptureError = error{ PngEncodeFailed, }; const cell_instance = @import("cell_instance"); const appendCellInstances = cell_instance.appendCellInstances; const glyphTopOffset = cell_instance.glyphTopOffset; const CAPTURE_COLS: u16 = 80; const CAPTURE_ROWS: u16 = 24; const VISIBILITY_DEADLINE_NS: i128 = 3 * std.time.ns_per_s; const VISIBILITY_TIMEOUT_NS: i128 = 3 * std.time.ns_per_s; /// Entry point. `argv[0]` is `--capture`; argv[1] = script path, argv[2] = out path. 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 { } // === play script through /bin/cat === try playScript(alloc, term, script_path); try playScript(term, script_path); // === snapshot + build instances === try term.snapshot(); @@ -195,14 +199,14 @@ pub fn run(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void { std.debug.print("capture: wrote {s} ({d}x{d})\n", .{ out_path, px_w, px_h }); } /// Wait up to VISIBILITY_DEADLINE_NS for the Wayland compositor to `configure` /// Wait up to VISIBILITY_TIMEOUT_NS for the Wayland compositor to `configure` /// the surface. For `--capture` we don't need the surface to actually be /// mapped onto an output (which would require committing a presentable /// buffer via the swapchain — we deliberately skip that since rendering is /// offscreen). A configured surface is enough to know our fixed 80x24 /// geometry was accepted. fn waitUntilVisible(conn: *wayland_client.Connection, window: *wayland_client.Window) !void { const deadline = @as(i128, std.time.nanoTimestamp()) + VISIBILITY_DEADLINE_NS; const deadline = @as(i128, std.time.nanoTimestamp()) + VISIBILITY_TIMEOUT_NS; while (std.time.nanoTimestamp() < deadline) { _ = conn.display.roundtrip(); if (window.state.configured) return; @@ -218,13 +222,13 @@ fn waitUntilVisible(conn: *wayland_client.Connection, window: *wayland_client.Wi /// Spawn `/bin/cat <script>` on a PTY; feed all its output into `term`. /// Returns once the child has exited AND two consecutive 20 ms polls /// produce no new bytes (drain). /// /// Note: spawns cat with script as argv rather than piping stdin+^D — /// avoids EOF-signalling races with VT escape sequences. fn playScript( alloc: std.mem.Allocator, term: *vt.Terminal, script_path: [:0]const u8, ) !void { _ = alloc; var p = try pty.Pty.spawn(.{ .cols = CAPTURE_COLS, .rows = CAPTURE_ROWS, @@ -320,52 +324,6 @@ fn buildInstancesForSnapshot( } } /// Mirror of main.zig's `appendCellInstances` (kept local to avoid making /// that function public just for this module). Appends 0-2 instances per /// cell: a filled-background quad if the bg differs from the terminal /// default, plus the glyph quad if the cell has a printable codepoint. fn appendCellInstances( alloc: std.mem.Allocator, instances: *std.ArrayListUnmanaged(renderer.Instance), row_idx: u32, col_idx: u32, cell_w: u32, cell_h: u32, baseline: u32, glyph_uv: ?font.GlyphUV, bg_uv: font.GlyphUV, colors: vt.CellColors, default_bg: [4]f32, ) !void { if (!std.meta.eql(colors.bg, default_bg)) { try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, .glyph_bearing = .{ 0, 0 }, .uv_rect = .{ bg_uv.u0, bg_uv.v0, bg_uv.u1, bg_uv.v1 }, .fg = colors.bg, .bg = colors.bg, }); } const uv = glyph_uv orelse return; try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) }, .glyph_bearing = .{ @floatFromInt(uv.bearing_x), glyphTopOffset(baseline, uv.bearing_y), }, .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 }, .fg = colors.fg, .bg = colors.bg, }); } fn glyphTopOffset(baseline: u32, bearing_y: i32) f32 { return @as(f32, @floatFromInt(baseline)) - @as(f32, @floatFromInt(bearing_y)); } /// Encode `rgba` as a PNG to a brand-new file at `path`. Buffers the full /// encoded byte stream in memory (fine for 80x24@16px: under 200 KB) and /// writes it in one shot. diff --git a/src/cell_instance.zig b/src/cell_instance.zig new file mode 100644 index 0000000..53a78e9 --- /dev/null +++ b/src/cell_instance.zig @@ -0,0 +1,58 @@ //! Shared cell-instance helpers used by both the live renderer (main.zig) //! and the capture renderer (capture.zig). const std = @import("std"); const renderer = @import("renderer"); const font = @import("font"); const vt = @import("vt"); /// Appends 0-2 `renderer.Instance` entries for a single terminal cell: /// - a filled-background quad when the cell's bg differs from the terminal /// default bg (so transparent cells don't draw a quad at all); /// - a glyph quad when `glyph_uv` is non-null (i.e. the cell has a /// printable codepoint that was found in the atlas). pub fn appendCellInstances( alloc: std.mem.Allocator, instances: *std.ArrayListUnmanaged(renderer.Instance), row_idx: u32, col_idx: u32, cell_w: u32, cell_h: u32, baseline: u32, glyph_uv: ?font.GlyphUV, bg_uv: font.GlyphUV, colors: vt.CellColors, default_bg: [4]f32, ) !void { if (!std.meta.eql(colors.bg, default_bg)) { try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, .glyph_bearing = .{ 0, 0 }, .uv_rect = .{ bg_uv.u0, bg_uv.v0, bg_uv.u1, bg_uv.v1 }, .fg = colors.bg, .bg = colors.bg, }); } const uv = glyph_uv orelse return; try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) }, .glyph_bearing = .{ @floatFromInt(uv.bearing_x), glyphTopOffset(baseline, uv.bearing_y), }, .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 }, .fg = colors.fg, .bg = colors.bg, }); } /// Returns the number of pixels from the top of the cell to the top of the /// glyph bitmap, given the cell `baseline` (pixels from cell top to the /// typographic baseline) and the glyph's `bearing_y` (pixels from baseline /// to the top of the glyph bitmap, positive = up). pub fn glyphTopOffset(baseline: u32, bearing_y: i32) f32 { return @as(f32, @floatFromInt(baseline)) - @as(f32, @floatFromInt(bearing_y)); } diff --git a/src/main.zig b/src/main.zig index 10dc1a1..868fca4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,6 +8,9 @@ const font = @import("font"); const config = @import("config"); const vk = @import("vulkan"); const bench_stats = @import("bench_stats"); const cell_instance = @import("cell_instance"); const appendCellInstances = cell_instance.appendCellInstances; const glyphTopOffset = cell_instance.glyphTopOffset; const FrameTiming = bench_stats.FrameTiming; const FrameTimingRing = bench_stats.FrameTimingRing; const SectionStats = bench_stats.SectionStats; @@ -1494,48 +1497,6 @@ fn cursorTouchesDirtyRow(dirty_rows: []const bool, cursor: CursorRefreshContext) return false; } fn appendCellInstances( alloc: std.mem.Allocator, instances: *std.ArrayListUnmanaged(renderer.Instance), row_idx: u32, col_idx: u32, cell_w: u32, cell_h: u32, baseline: u32, glyph_uv: ?font.GlyphUV, bg_uv: font.GlyphUV, colors: vt.CellColors, default_bg: [4]f32, ) !void { if (!std.meta.eql(colors.bg, default_bg)) { try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, .glyph_bearing = .{ 0, 0 }, .uv_rect = .{ bg_uv.u0, bg_uv.v0, bg_uv.u1, bg_uv.v1 }, .fg = colors.bg, .bg = colors.bg, }); } const uv = glyph_uv orelse return; try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) }, .glyph_bearing = .{ @floatFromInt(uv.bearing_x), glyphTopOffset(baseline, uv.bearing_y), }, .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 }, .fg = colors.fg, .bg = colors.bg, }); } fn glyphTopOffset(baseline: u32, bearing_y: i32) f32 { return @as(f32, @floatFromInt(baseline)) - @as(f32, @floatFromInt(bearing_y)); } fn encodeKeyboardEvent( term: *const vt.Terminal, ev: wayland_client.KeyboardEvent,