a73x

e2859d88

feat(renderer): glyph atlas upload + instanced draw

a73x   2026-04-08 09:22

Commit message
feat(renderer): glyph atlas upload + instanced draw

Add GPU glyph atlas texture (R8, 1024x1024), unit-quad vertex buffer,
per-instance buffer, uploadAtlas/uploadInstances/drawCells methods, and
a --draw-smoke-test that renders a single 'M' glyph to the window center.
Fix FIFO swapchain hang by preferring MAILBOX/IMMEDIATE present modes.

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

src/main.zig
Old New
@@ -3,6 +3,7 @@ const vt = @import("vt");
3 const pty = @import("pty"); 3 const pty = @import("pty");
4 const wayland_client = @import("wayland-client"); 4 const wayland_client = @import("wayland-client");
5 const renderer = @import("renderer"); 5 const renderer = @import("renderer");
6 const font = @import("font");
6 7
7 pub fn main() !void { 8 pub fn main() !void {
8 var gpa: std.heap.DebugAllocator(.{}) = .init; 9 var gpa: std.heap.DebugAllocator(.{}) = .init;
@@ -28,9 +29,92 @@ pub fn main() !void {
28 return runRenderSmokeTest(alloc); 29 return runRenderSmokeTest(alloc);
29 } 30 }
30 31
32 if (args.len >= 2 and std.mem.eql(u8, args[1], "--draw-smoke-test")) {
33 return runDrawSmokeTest(alloc);
34 }
35
31 std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); 36 std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{});
32 } 37 }
33 38
39 fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
40 var conn = try wayland_client.Connection.init();
41 defer conn.deinit();
42 std.debug.print("wayland connected\n", .{});
43
44 const window = try conn.createWindow(alloc, "waystty-draw-smoke");
45 defer window.deinit();
46 std.debug.print("window created (w={d} h={d})\n", .{ window.width, window.height });
47
48 _ = conn.display.roundtrip();
49
50 var ctx = try renderer.Context.init(
51 alloc,
52 @ptrCast(conn.display),
53 @ptrCast(window.surface),
54 window.width,
55 window.height,
56 );
57 defer ctx.deinit();
58 std.debug.print("vulkan context created\n", .{});
59
60 // Load monospace font and create atlas
61 var font_lookup = try font.lookupMonospace(alloc);
62 defer font_lookup.deinit(alloc);
63
64 const px_size: u32 = 16;
65 var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, px_size);
66 defer face.deinit();
67
68 var atlas = try font.Atlas.init(alloc, 1024, 1024);
69 defer atlas.deinit();
70
71 // Rasterize 'M' into the atlas
72 const glyph_uv = try atlas.getOrInsert(&face, 'M');
73 std.debug.print("glyph 'M': uv=({d:.3},{d:.3})->({d:.3},{d:.3}) size={d}x{d}\n", .{
74 glyph_uv.u0, glyph_uv.v0, glyph_uv.u1, glyph_uv.v1,
75 glyph_uv.width, glyph_uv.height,
76 });
77
78 // Upload atlas pixels to GPU
79 try ctx.uploadAtlas(atlas.pixels);
80 std.debug.print("atlas uploaded\n", .{});
81
82 // Cell size from font metrics
83 const cell_w: f32 = @floatFromInt(face.cellWidth());
84 const cell_h: f32 = @floatFromInt(face.cellHeight());
85 std.debug.print("cell size: {d}x{d}\n", .{ cell_w, cell_h });
86
87 // One Instance: 'M' at cell position (40, 12) — near center of 80x24 grid
88 const instances = [_]renderer.Instance{
89 .{
90 .cell_pos = .{ 40.0, 12.0 },
91 .uv_rect = .{ glyph_uv.u0, glyph_uv.v0, glyph_uv.u1, glyph_uv.v1 },
92 .fg = .{ 1.0, 1.0, 1.0, 1.0 },
93 .bg = .{ 0.0, 0.0, 0.0, 1.0 },
94 },
95 };
96
97 try ctx.uploadInstances(&instances);
98 std.debug.print("instances uploaded, rendering 60 frames...\n", .{});
99
100 var i: u32 = 0;
101 while (i < 60) : (i += 1) {
102 // Non-blocking Wayland event read: prepare + read + dispatch.
103 // The Vulkan WSI (FIFO) needs wl_buffer.release events from the compositor
104 // to be read off the socket before it can release an acquire slot.
105 _ = conn.display.flush();
106 if (conn.display.prepareRead()) {
107 _ = conn.display.readEvents();
108 }
109 _ = conn.display.dispatchPending();
110 try ctx.drawCells(1, .{ cell_w, cell_h });
111 _ = conn.display.flush();
112 }
113
114 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
115 std.debug.print("done\n", .{});
116 }
117
34 fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { 118 fn runRenderSmokeTest(alloc: std.mem.Allocator) !void {
35 var conn = try wayland_client.Connection.init(); 119 var conn = try wayland_client.Connection.init();
36 defer conn.deinit(); 120 defer conn.deinit();
src/renderer.zig
Old New
@@ -119,6 +119,23 @@ fn createSwapchain(
119 image_count = caps.max_image_count; 119 image_count = caps.max_image_count;
120 } 120 }
121 121
122 // Prefer MAILBOX (non-blocking, no tearing) over IMMEDIATE, fall back to FIFO.
123 // FIFO blocks until compositor releases a buffer, requiring a pumped Wayland event loop.
124 var pm_count: u32 = 0;
125 _ = try vki.getPhysicalDeviceSurfacePresentModesKHR(pd_info.physical, surface, &pm_count, null);
126 const present_modes = try alloc.alloc(vk.PresentModeKHR, pm_count);
127 defer alloc.free(present_modes);
128 _ = try vki.getPhysicalDeviceSurfacePresentModesKHR(pd_info.physical, surface, &pm_count, present_modes.ptr);
129 var present_mode: vk.PresentModeKHR = .fifo_khr;
130 for (present_modes[0..pm_count]) |pm| {
131 if (pm == .mailbox_khr) { present_mode = .mailbox_khr; break; }
132 }
133 if (present_mode == .fifo_khr) {
134 for (present_modes[0..pm_count]) |pm| {
135 if (pm == .immediate_khr) { present_mode = .immediate_khr; break; }
136 }
137 }
138
122 const same_family = pd_info.graphics_queue_family == pd_info.present_queue_family; 139 const same_family = pd_info.graphics_queue_family == pd_info.present_queue_family;
123 const families = [_]u32{ pd_info.graphics_queue_family, pd_info.present_queue_family }; 140 const families = [_]u32{ pd_info.graphics_queue_family, pd_info.present_queue_family };
124 141
@@ -135,7 +152,7 @@ fn createSwapchain(
135 .p_queue_family_indices = if (same_family) null else &families, 152 .p_queue_family_indices = if (same_family) null else &families,
136 .pre_transform = caps.current_transform, 153 .pre_transform = caps.current_transform,
137 .composite_alpha = .{ .opaque_bit_khr = true }, 154 .composite_alpha = .{ .opaque_bit_khr = true },
138 .present_mode = .fifo_khr, 155 .present_mode = present_mode,
139 .clipped = .true, 156 .clipped = .true,
140 }, null); 157 }, null);
141 158
@@ -200,6 +217,60 @@ pub const Instance = extern struct {
200 bg: [4]f32, // location 4 217 bg: [4]f32, // location 4
201 }; 218 };
202 219
220 const BufferResult = struct {
221 buffer: vk.Buffer,
222 memory: vk.DeviceMemory,
223 };
224
225 fn createHostVisibleBuffer(
226 vki: vk.InstanceWrapper,
227 physical_device: vk.PhysicalDevice,
228 vkd: vk.DeviceWrapper,
229 device: vk.Device,
230 size: vk.DeviceSize,
231 usage: vk.BufferUsageFlags,
232 ) !BufferResult {
233 const buffer = try vkd.createBuffer(device, &vk.BufferCreateInfo{
234 .size = size,
235 .usage = usage,
236 .sharing_mode = .exclusive,
237 }, null);
238 errdefer vkd.destroyBuffer(device, buffer, null);
239
240 const reqs = vkd.getBufferMemoryRequirements(device, buffer);
241 const mem_idx = try findMemoryType(
242 vki,
243 physical_device,
244 reqs.memory_type_bits,
245 .{ .host_visible_bit = true, .host_coherent_bit = true },
246 );
247 const memory = try vkd.allocateMemory(device, &vk.MemoryAllocateInfo{
248 .allocation_size = reqs.size,
249 .memory_type_index = mem_idx,
250 }, null);
251 errdefer vkd.freeMemory(device, memory, null);
252
253 try vkd.bindBufferMemory(device, buffer, memory, 0);
254 return .{ .buffer = buffer, .memory = memory };
255 }
256
257 fn findMemoryType(
258 vki: vk.InstanceWrapper,
259 physical_device: vk.PhysicalDevice,
260 type_filter: u32,
261 properties: vk.MemoryPropertyFlags,
262 ) !u32 {
263 const mem_props = vki.getPhysicalDeviceMemoryProperties(physical_device);
264 var i: u32 = 0;
265 while (i < mem_props.memory_type_count) : (i += 1) {
266 if ((type_filter & (@as(u32, 1) << @intCast(i))) != 0) {
267 const type_props = mem_props.memory_types[i].property_flags;
268 if (type_props.contains(properties)) return i;
269 }
270 }
271 return error.NoSuitableMemoryType;
272 }
273
203 pub const Context = struct { 274 pub const Context = struct {
204 alloc: std.mem.Allocator, 275 alloc: std.mem.Allocator,
205 vkb: vk.BaseWrapper, 276 vkb: vk.BaseWrapper,
@@ -235,6 +306,20 @@ pub const Context = struct {
235 image_available: vk.Semaphore, 306 image_available: vk.Semaphore,
236 render_finished: vk.Semaphore, 307 render_finished: vk.Semaphore,
237 in_flight_fence: vk.Fence, 308 in_flight_fence: vk.Fence,
309 // Static unit-quad vertex buffer
310 quad_vertex_buffer: vk.Buffer,
311 quad_vertex_memory: vk.DeviceMemory,
312 // Per-frame instance buffer
313 instance_buffer: vk.Buffer,
314 instance_memory: vk.DeviceMemory,
315 instance_capacity: u32,
316 // GPU glyph atlas texture
317 atlas_image: vk.Image,
318 atlas_memory: vk.DeviceMemory,
319 atlas_view: vk.ImageView,
320 atlas_sampler: vk.Sampler,
321 atlas_width: u32,
322 atlas_height: u32,
238 323
239 pub fn init( 324 pub fn init(
240 alloc: std.mem.Allocator, 325 alloc: std.mem.Allocator,
@@ -582,6 +667,117 @@ pub const Context = struct {
582 }, null); 667 }, null);
583 errdefer vkd.destroyFence(device, in_flight_fence, null); 668 errdefer vkd.destroyFence(device, in_flight_fence, null);
584 669
670 // --- Quad vertex buffer ---
671 const quad_verts = [_]Vertex{
672 .{ .unit_pos = .{ 0, 0 } },
673 .{ .unit_pos = .{ 1, 0 } },
674 .{ .unit_pos = .{ 1, 1 } },
675 .{ .unit_pos = .{ 0, 0 } },
676 .{ .unit_pos = .{ 1, 1 } },
677 .{ .unit_pos = .{ 0, 1 } },
678 };
679 const quad_vb_size: vk.DeviceSize = @sizeOf(@TypeOf(quad_verts));
680 const quad = try createHostVisibleBuffer(vki, pd_info.physical, vkd, device, quad_vb_size, .{ .vertex_buffer_bit = true });
681 errdefer {
682 vkd.destroyBuffer(device, quad.buffer, null);
683 vkd.freeMemory(device, quad.memory, null);
684 }
685 {
686 const mapped = try vkd.mapMemory(device, quad.memory, 0, quad_vb_size, .{});
687 @memcpy(
688 @as([*]Vertex, @ptrCast(@alignCast(mapped)))[0..quad_verts.len],
689 &quad_verts,
690 );
691 vkd.unmapMemory(device, quad.memory);
692 }
693
694 // --- Instance buffer ---
695 const max_instances: u32 = 200 * 80;
696 const instance_size: vk.DeviceSize = @sizeOf(Instance) * max_instances;
697 const inst = try createHostVisibleBuffer(vki, pd_info.physical, vkd, device, instance_size, .{ .vertex_buffer_bit = true });
698 errdefer {
699 vkd.destroyBuffer(device, inst.buffer, null);
700 vkd.freeMemory(device, inst.memory, null);
701 }
702
703 // --- Atlas texture ---
704 const atlas_width: u32 = 1024;
705 const atlas_height: u32 = 1024;
706
707 const atlas_image = try vkd.createImage(device, &vk.ImageCreateInfo{
708 .image_type = .@"2d",
709 .format = .r8_unorm,
710 .extent = .{ .width = atlas_width, .height = atlas_height, .depth = 1 },
711 .mip_levels = 1,
712 .array_layers = 1,
713 .samples = .{ .@"1_bit" = true },
714 .tiling = .optimal,
715 .usage = .{ .transfer_dst_bit = true, .sampled_bit = true },
716 .sharing_mode = .exclusive,
717 .initial_layout = .undefined,
718 }, null);
719 errdefer vkd.destroyImage(device, atlas_image, null);
720
721 const img_reqs = vkd.getImageMemoryRequirements(device, atlas_image);
722 const img_mem_idx = try findMemoryType(vki, pd_info.physical, img_reqs.memory_type_bits, .{ .device_local_bit = true });
723 const atlas_memory = try vkd.allocateMemory(device, &vk.MemoryAllocateInfo{
724 .allocation_size = img_reqs.size,
725 .memory_type_index = img_mem_idx,
726 }, null);
727 errdefer vkd.freeMemory(device, atlas_memory, null);
728 try vkd.bindImageMemory(device, atlas_image, atlas_memory, 0);
729
730 const atlas_view = try vkd.createImageView(device, &vk.ImageViewCreateInfo{
731 .image = atlas_image,
732 .view_type = .@"2d",
733 .format = .r8_unorm,
734 .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity },
735 .subresource_range = .{
736 .aspect_mask = .{ .color_bit = true },
737 .base_mip_level = 0,
738 .level_count = 1,
739 .base_array_layer = 0,
740 .layer_count = 1,
741 },
742 }, null);
743 errdefer vkd.destroyImageView(device, atlas_view, null);
744
745 const atlas_sampler = try vkd.createSampler(device, &vk.SamplerCreateInfo{
746 .mag_filter = .nearest,
747 .min_filter = .nearest,
748 .mipmap_mode = .nearest,
749 .address_mode_u = .clamp_to_edge,
750 .address_mode_v = .clamp_to_edge,
751 .address_mode_w = .clamp_to_edge,
752 .mip_lod_bias = 0,
753 .anisotropy_enable = .false,
754 .max_anisotropy = 1,
755 .compare_enable = .false,
756 .compare_op = .always,
757 .min_lod = 0,
758 .max_lod = 0,
759 .border_color = .int_opaque_black,
760 .unnormalized_coordinates = .false,
761 }, null);
762 errdefer vkd.destroySampler(device, atlas_sampler, null);
763
764 // Bind atlas to descriptor set
765 const img_info = vk.DescriptorImageInfo{
766 .sampler = atlas_sampler,
767 .image_view = atlas_view,
768 .image_layout = .shader_read_only_optimal,
769 };
770 vkd.updateDescriptorSets(device, 1, @ptrCast(&vk.WriteDescriptorSet{
771 .dst_set = descriptor_set,
772 .dst_binding = 0,
773 .dst_array_element = 0,
774 .descriptor_count = 1,
775 .descriptor_type = .combined_image_sampler,
776 .p_image_info = @ptrCast(&img_info),
777 .p_buffer_info = undefined,
778 .p_texel_buffer_view = undefined,
779 }), 0, null);
780
585 return .{ 781 return .{
586 .alloc = alloc, 782 .alloc = alloc,
587 .vkb = vkb, 783 .vkb = vkb,
@@ -612,6 +808,17 @@ pub const Context = struct {
612 .image_available = image_available, 808 .image_available = image_available,
613 .render_finished = render_finished, 809 .render_finished = render_finished,
614 .in_flight_fence = in_flight_fence, 810 .in_flight_fence = in_flight_fence,
811 .quad_vertex_buffer = quad.buffer,
812 .quad_vertex_memory = quad.memory,
813 .instance_buffer = inst.buffer,
814 .instance_memory = inst.memory,
815 .instance_capacity = max_instances,
816 .atlas_image = atlas_image,
817 .atlas_memory = atlas_memory,
818 .atlas_view = atlas_view,
819 .atlas_sampler = atlas_sampler,
820 .atlas_width = atlas_width,
821 .atlas_height = atlas_height,
615 }; 822 };
616 } 823 }
617 824
@@ -619,6 +826,16 @@ pub const Context = struct {
619 // Wait for device to be idle before destroying anything 826 // Wait for device to be idle before destroying anything
620 _ = self.vkd.deviceWaitIdle(self.device) catch {}; 827 _ = self.vkd.deviceWaitIdle(self.device) catch {};
621 828
829 // Atlas + buffers (in reverse order of creation)
830 self.vkd.destroySampler(self.device, self.atlas_sampler, null);
831 self.vkd.destroyImageView(self.device, self.atlas_view, null);
832 self.vkd.destroyImage(self.device, self.atlas_image, null);
833 self.vkd.freeMemory(self.device, self.atlas_memory, null);
834 self.vkd.destroyBuffer(self.device, self.instance_buffer, null);
835 self.vkd.freeMemory(self.device, self.instance_memory, null);
836 self.vkd.destroyBuffer(self.device, self.quad_vertex_buffer, null);
837 self.vkd.freeMemory(self.device, self.quad_vertex_memory, null);
838
622 // Sync objects 839 // Sync objects
623 self.vkd.destroyFence(self.device, self.in_flight_fence, null); 840 self.vkd.destroyFence(self.device, self.in_flight_fence, null);
624 self.vkd.destroySemaphore(self.device, self.render_finished, null); 841 self.vkd.destroySemaphore(self.device, self.render_finished, null);
@@ -718,6 +935,246 @@ pub const Context = struct {
718 .p_image_indices = @ptrCast(&image_index), 935 .p_image_indices = @ptrCast(&image_index),
719 }); 936 });
720 } 937 }
938
939 /// Upload CPU R8 pixels into the GPU atlas image.
940 /// Uses a staging buffer + one-shot command buffer.
941 /// Transitions: UNDEFINED -> TRANSFER_DST -> SHADER_READ_ONLY.
942 pub fn uploadAtlas(self: *Context, pixels: []const u8) !void {
943 const size: vk.DeviceSize = @intCast(pixels.len);
944
945 // Create staging buffer
946 const staging = try createHostVisibleBuffer(
947 self.vki,
948 self.physical_device,
949 self.vkd,
950 self.device,
951 size,
952 .{ .transfer_src_bit = true },
953 );
954 defer {
955 self.vkd.destroyBuffer(self.device, staging.buffer, null);
956 self.vkd.freeMemory(self.device, staging.memory, null);
957 }
958
959 // Copy pixels into staging buffer
960 const mapped = try self.vkd.mapMemory(self.device, staging.memory, 0, size, .{});
961 @memcpy(@as([*]u8, @ptrCast(mapped))[0..pixels.len], pixels);
962 self.vkd.unmapMemory(self.device, staging.memory);
963
964 // One-shot command buffer
965 var cb: vk.CommandBuffer = undefined;
966 try self.vkd.allocateCommandBuffers(self.device, &vk.CommandBufferAllocateInfo{
967 .command_pool = self.command_pool,
968 .level = .primary,
969 .command_buffer_count = 1,
970 }, @ptrCast(&cb));
971 defer self.vkd.freeCommandBuffers(self.device, self.command_pool, 1, @ptrCast(&cb));
972
973 try self.vkd.beginCommandBuffer(cb, &vk.CommandBufferBeginInfo{
974 .flags = .{ .one_time_submit_bit = true },
975 });
976
977 // Barrier: UNDEFINED -> TRANSFER_DST
978 const barrier_to_transfer = vk.ImageMemoryBarrier{
979 .src_access_mask = .{},
980 .dst_access_mask = .{ .transfer_write_bit = true },
981 .old_layout = .undefined,
982 .new_layout = .transfer_dst_optimal,
983 .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
984 .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
985 .image = self.atlas_image,
986 .subresource_range = .{
987 .aspect_mask = .{ .color_bit = true },
988 .base_mip_level = 0,
989 .level_count = 1,
990 .base_array_layer = 0,
991 .layer_count = 1,
992 },
993 };
994 self.vkd.cmdPipelineBarrier(
995 cb,
996 .{ .top_of_pipe_bit = true },
997 .{ .transfer_bit = true },
998 .{},
999 0, null,
1000 0, null,
1001 1, @ptrCast(&barrier_to_transfer),
1002 );
1003
1004 // Copy buffer -> image
1005 const region = vk.BufferImageCopy{
1006 .buffer_offset = 0,
1007 .buffer_row_length = 0,
1008 .buffer_image_height = 0,
1009 .image_subresource = .{
1010 .aspect_mask = .{ .color_bit = true },
1011 .mip_level = 0,
1012 .base_array_layer = 0,
1013 .layer_count = 1,
1014 },
1015 .image_offset = .{ .x = 0, .y = 0, .z = 0 },
1016 .image_extent = .{ .width = self.atlas_width, .height = self.atlas_height, .depth = 1 },
1017 };
1018 self.vkd.cmdCopyBufferToImage(cb, staging.buffer, self.atlas_image, .transfer_dst_optimal, 1, @ptrCast(&region));
1019
1020 // Barrier: TRANSFER_DST -> SHADER_READ_ONLY
1021 const barrier_to_shader = vk.ImageMemoryBarrier{
1022 .src_access_mask = .{ .transfer_write_bit = true },
1023 .dst_access_mask = .{ .shader_read_bit = true },
1024 .old_layout = .transfer_dst_optimal,
1025 .new_layout = .shader_read_only_optimal,
1026 .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
1027 .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
1028 .image = self.atlas_image,
1029 .subresource_range = .{
1030 .aspect_mask = .{ .color_bit = true },
1031 .base_mip_level = 0,
1032 .level_count = 1,
1033 .base_array_layer = 0,
1034 .layer_count = 1,
1035 },
1036 };
1037 self.vkd.cmdPipelineBarrier(
1038 cb,
1039 .{ .transfer_bit = true },
1040 .{ .fragment_shader_bit = true },
1041 .{},
1042 0, null,
1043 0, null,
1044 1, @ptrCast(&barrier_to_shader),
1045 );
1046
1047 try self.vkd.endCommandBuffer(cb);
1048
1049 try self.vkd.queueSubmit(self.graphics_queue, 1, @ptrCast(&vk.SubmitInfo{
1050 .command_buffer_count = 1,
1051 .p_command_buffers = @ptrCast(&cb),
1052 }), .null_handle);
1053 try self.vkd.queueWaitIdle(self.graphics_queue);
1054 }
1055
1056 /// Map the instance buffer, copy instances in, unmap.
1057 pub fn uploadInstances(self: *Context, instances: []const Instance) !void {
1058 if (instances.len > self.instance_capacity) return error.TooManyInstances;
1059 const size: vk.DeviceSize = @sizeOf(Instance) * instances.len;
1060 const mapped = try self.vkd.mapMemory(self.device, self.instance_memory, 0, size, .{});
1061 @memcpy(@as([*]Instance, @ptrCast(@alignCast(mapped)))[0..instances.len], instances);
1062 self.vkd.unmapMemory(self.device, self.instance_memory);
1063 }
1064
1065 /// Full draw pass: bind pipeline, push constants, vertex + instance buffers, draw, present.
1066 pub fn drawCells(self: *Context, instance_count: u32, cell_size: [2]f32) !void {
1067 // Wait for previous frame to finish
1068 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64));
1069 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
1070
1071 // Acquire next image
1072 const acquire = try self.vkd.acquireNextImageKHR(
1073 self.device,
1074 self.swapchain,
1075 std.math.maxInt(u64),
1076 self.image_available,
1077 .null_handle,
1078 );
1079 const image_index = acquire.image_index;
1080
1081 // Record command buffer
1082 try self.vkd.resetCommandBuffer(self.command_buffer, .{});
1083 try self.vkd.beginCommandBuffer(self.command_buffer, &vk.CommandBufferBeginInfo{
1084 .flags = .{ .one_time_submit_bit = true },
1085 });
1086
1087 const clear_value = vk.ClearValue{
1088 .color = .{ .float_32 = .{ 0.0, 0.0, 0.0, 1.0 } },
1089 };
1090
1091 self.vkd.cmdBeginRenderPass(self.command_buffer, &vk.RenderPassBeginInfo{
1092 .render_pass = self.render_pass,
1093 .framebuffer = self.framebuffers[image_index],
1094 .render_area = .{
1095 .offset = .{ .x = 0, .y = 0 },
1096 .extent = self.swapchain_extent,
1097 },
1098 .clear_value_count = 1,
1099 .p_clear_values = @ptrCast(&clear_value),
1100 }, .@"inline");
1101
1102 self.vkd.cmdBindPipeline(self.command_buffer, .graphics, self.pipeline);
1103
1104 // Dynamic viewport + scissor
1105 const viewport = vk.Viewport{
1106 .x = 0.0,
1107 .y = 0.0,
1108 .width = @floatFromInt(self.swapchain_extent.width),
1109 .height = @floatFromInt(self.swapchain_extent.height),
1110 .min_depth = 0.0,
1111 .max_depth = 1.0,
1112 };
1113 self.vkd.cmdSetViewport(self.command_buffer, 0, 1, @ptrCast(&viewport));
1114
1115 const scissor = vk.Rect2D{
1116 .offset = .{ .x = 0, .y = 0 },
1117 .extent = self.swapchain_extent,
1118 };
1119 self.vkd.cmdSetScissor(self.command_buffer, 0, 1, @ptrCast(&scissor));
1120
1121 // Push constants
1122 const pc = PushConstants{
1123 .viewport_size = .{
1124 @floatFromInt(self.swapchain_extent.width),
1125 @floatFromInt(self.swapchain_extent.height),
1126 },
1127 .cell_size = cell_size,
1128 };
1129 self.vkd.cmdPushConstants(
1130 self.command_buffer,
1131 self.pipeline_layout,
1132 .{ .vertex_bit = true },
1133 0,
1134 @sizeOf(PushConstants),
1135 @ptrCast(&pc),
1136 );
1137
1138 // Bind descriptor set (atlas sampler)
1139 self.vkd.cmdBindDescriptorSets(
1140 self.command_buffer,
1141 .graphics,
1142 self.pipeline_layout,
1143 0, 1, @ptrCast(&self.descriptor_set),
1144 0, null,
1145 );
1146
1147 // Bind vertex buffers: binding 0 = quad, binding 1 = instances
1148 const buffers = [_]vk.Buffer{ self.quad_vertex_buffer, self.instance_buffer };
1149 const offsets = [_]vk.DeviceSize{ 0, 0 };
1150 self.vkd.cmdBindVertexBuffers(self.command_buffer, 0, 2, &buffers, &offsets);
1151
1152 self.vkd.cmdDraw(self.command_buffer, 6, instance_count, 0, 0);
1153
1154 self.vkd.cmdEndRenderPass(self.command_buffer);
1155 try self.vkd.endCommandBuffer(self.command_buffer);
1156
1157 // Submit
1158 const wait_stage = vk.PipelineStageFlags{ .color_attachment_output_bit = true };
1159 try self.vkd.queueSubmit(self.graphics_queue, 1, @ptrCast(&vk.SubmitInfo{
1160 .wait_semaphore_count = 1,
1161 .p_wait_semaphores = @ptrCast(&self.image_available),
1162 .p_wait_dst_stage_mask = @ptrCast(&wait_stage),
1163 .command_buffer_count = 1,
1164 .p_command_buffers = @ptrCast(&self.command_buffer),
1165 .signal_semaphore_count = 1,
1166 .p_signal_semaphores = @ptrCast(&self.render_finished),
1167 }), self.in_flight_fence);
1168
1169 // Present
1170 _ = try self.vkd.queuePresentKHR(self.present_queue, &vk.PresentInfoKHR{
1171 .wait_semaphore_count = 1,
1172 .p_wait_semaphores = @ptrCast(&self.render_finished),
1173 .swapchain_count = 1,
1174 .p_swapchains = @ptrCast(&self.swapchain),
1175 .p_image_indices = @ptrCast(&image_index),
1176 });
1177 }
721 }; 1178 };
722 1179
723 test "vulkan module imports" { 1180 test "vulkan module imports" {