8f9e9953
Extract cell_instance helpers into shared module
a73x 2026-04-17 15:44
Commit message
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, |