a73x

73e2fd1c

cursor: extract cursorInstance helper with DECSCUSR shape dispatch

a73x   2026-04-19 06:57

Commit message
cursor: extract cursorInstance helper with DECSCUSR shape dispatch

Pure helper returning a renderer.Instance for block/underline/bar.
Replaces the inline instance build in main.zig so shape geometry
is unit-testable without a Vulkan device.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

build.zig
Old New
@@ -328,7 +328,9 @@ pub fn build(b: *std.Build) void {
328 }); 328 });
329 vk_sync_mod.addImport("vulkan", vulkan_module); 329 vk_sync_mod.addImport("vulkan", vulkan_module);
330 renderer_mod.addImport("vk_sync", vk_sync_mod); 330 renderer_mod.addImport("vk_sync", vk_sync_mod);
331 renderer_mod.addImport("font", font_mod);
331 renderer_test_mod.addImport("vk_sync", vk_sync_mod); 332 renderer_test_mod.addImport("vk_sync", vk_sync_mod);
333 renderer_test_mod.addImport("font", font_mod);
332 exe_mod.addImport("vk_sync", vk_sync_mod); 334 exe_mod.addImport("vk_sync", vk_sync_mod);
333 main_test_mod.addImport("vk_sync", vk_sync_mod); 335 main_test_mod.addImport("vk_sync", vk_sync_mod);
334 336
src/main.zig
Old New
@@ -554,26 +554,23 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
554 var cursor_instances_buf: [1]renderer.Instance = undefined; 554 var cursor_instances_buf: [1]renderer.Instance = undefined;
555 var cursor_instances: []const renderer.Instance = &.{}; 555 var cursor_instances: []const renderer.Instance = &.{};
556 if (term.render_state.cursor.viewport) |cursor| { 556 if (term.render_state.cursor.viewport) |cursor| {
557 const cursor_uv = atlas.cursorUV(); 557 const shape: renderer.CursorShape = switch (term.render_state.cursor.visual_style) {
558 cursor_instances_buf[0] = .{ 558 .block, .block_hollow => .block,
559 .cell_pos = .{ 559 .underline => .underline,
560 @floatFromInt(cursor.x), 560 .bar => .bar,
561 @floatFromInt(cursor.y), 561 };
562 }, 562 var inst = renderer.cursorInstance(
563 .glyph_size = .{ 563 shape,
564 @floatFromInt(cell_w), 564 cell_w,
565 @floatFromInt(cell_h), 565 cell_h,
566 }, 566 @as(u32, @intCast(geom.buffer_scale)),
567 .glyph_bearing = .{ 0, 0 }, 567 atlas.cursorUV(),
568 .uv_rect = .{ 568 );
569 cursor_uv.u0, 569 inst.cell_pos = .{
570 cursor_uv.v0, 570 @floatFromInt(cursor.x),
571 cursor_uv.u1, 571 @floatFromInt(cursor.y),
572 cursor_uv.v1,
573 },
574 .fg = .{ 1.0, 1.0, 1.0, 0.5 },
575 .bg = .{ 0, 0, 0, 0 },
576 }; 572 };
573 cursor_instances_buf[0] = inst;
577 cursor_instances = cursor_instances_buf[0..1]; 574 cursor_instances = cursor_instances_buf[0..1];
578 } 575 }
579 const previous_total_instance_count = render_cache.total_instance_count; 576 const previous_total_instance_count = render_cache.total_instance_count;
src/renderer.zig
Old New
@@ -2,6 +2,8 @@ const std = @import("std");
2 const vk = @import("vulkan"); 2 const vk = @import("vulkan");
3 const vk_sync = @import("vk_sync"); 3 const vk_sync = @import("vk_sync");
4 4
5 const GlyphUV = @import("font").GlyphUV;
6
5 const dl = @cImport({ 7 const dl = @cImport({
6 @cInclude("dlfcn.h"); 8 @cInclude("dlfcn.h");
7 }); 9 });
@@ -2192,3 +2194,79 @@ test "uploadInstanceRange contract reports invalid ranges explicitly" {
2192 const action = planUploadInstanceRangeAction(std.math.maxInt(u32), std.math.maxInt(u32), 1); 2194 const action = planUploadInstanceRangeAction(std.math.maxInt(u32), std.math.maxInt(u32), 1);
2193 try std.testing.expectEqual(InstanceRangeUploadAction.invalid_range, action); 2195 try std.testing.expectEqual(InstanceRangeUploadAction.invalid_range, action);
2194 } 2196 }
2197
2198 // --- cursor instance (pure, unit-tested) ---
2199
2200 pub const CursorShape = enum { block, underline, bar };
2201
2202 pub fn cursorInstance(
2203 shape: CursorShape,
2204 cell_w: u32,
2205 cell_h: u32,
2206 buffer_scale: u32,
2207 uv: GlyphUV,
2208 ) Instance {
2209 const cell_w_f: f32 = @floatFromInt(cell_w);
2210 const cell_h_f: f32 = @floatFromInt(cell_h);
2211 const line_w_f: f32 = @floatFromInt(2 * buffer_scale);
2212
2213 const size_xy: [2]f32 = switch (shape) {
2214 .block => .{ cell_w_f, cell_h_f },
2215 .underline => .{ cell_w_f, line_w_f },
2216 .bar => .{ line_w_f, cell_h_f },
2217 };
2218 const bearing_xy: [2]f32 = switch (shape) {
2219 .block, .bar => .{ 0, 0 },
2220 .underline => .{ 0, cell_h_f - line_w_f },
2221 };
2222
2223 return .{
2224 .cell_pos = .{ 0, 0 }, // caller overwrites with actual grid position
2225 .glyph_size = size_xy,
2226 .glyph_bearing = bearing_xy,
2227 .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 },
2228 .fg = .{ 1.0, 1.0, 1.0, 0.5 },
2229 .bg = .{ 0, 0, 0, 0 },
2230 };
2231 }
2232
2233 test "cursorInstance: block fills the whole cell at scale 1" {
2234 const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 };
2235 const inst = cursorInstance(.block, 10, 20, 1, uv);
2236 try std.testing.expectEqualSlices(f32, &[_]f32{ 10.0, 20.0 }, &inst.glyph_size);
2237 try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 0.0 }, &inst.glyph_bearing);
2238 }
2239
2240 test "cursorInstance: underline is a 2px bar at the cell bottom (scale 1)" {
2241 const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 };
2242 const inst = cursorInstance(.underline, 10, 20, 1, uv);
2243 try std.testing.expectEqualSlices(f32, &[_]f32{ 10.0, 2.0 }, &inst.glyph_size);
2244 try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 18.0 }, &inst.glyph_bearing);
2245 }
2246
2247 test "cursorInstance: bar is a 2px column at cell left (scale 1)" {
2248 const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 };
2249 const inst = cursorInstance(.bar, 10, 20, 1, uv);
2250 try std.testing.expectEqualSlices(f32, &[_]f32{ 2.0, 20.0 }, &inst.glyph_size);
2251 try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 0.0 }, &inst.glyph_bearing);
2252 }
2253
2254 test "cursorInstance: underline line width scales with buffer_scale" {
2255 const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 };
2256 const inst = cursorInstance(.underline, 20, 40, 2, uv);
2257 try std.testing.expectEqualSlices(f32, &[_]f32{ 20.0, 4.0 }, &inst.glyph_size);
2258 try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 36.0 }, &inst.glyph_bearing);
2259 }
2260
2261 test "cursorInstance: bar line width scales with buffer_scale" {
2262 const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 };
2263 const inst = cursorInstance(.bar, 20, 40, 2, uv);
2264 try std.testing.expectEqualSlices(f32, &[_]f32{ 4.0, 40.0 }, &inst.glyph_size);
2265 try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 0.0 }, &inst.glyph_bearing);
2266 }
2267
2268 test "cursorInstance: uv_rect comes from the provided UV" {
2269 const uv: GlyphUV = .{ .u0 = 0.1, .v0 = 0.2, .u1 = 0.3, .v1 = 0.4, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 };
2270 const inst = cursorInstance(.block, 10, 20, 1, uv);
2271 try std.testing.expectEqualSlices(f32, &[_]f32{ 0.1, 0.2, 0.3, 0.4 }, &inst.uv_rect);
2272 }