a73x

0c5b68af

Migrate 10 Vulkan wait/acquire sites to bounded helpers

a73x   2026-04-18 15:12

Commit message
Migrate 10 Vulkan wait/acquire sites to bounded helpers

Replaces unbounded waitForFences/acquireNextImageKHR calls in drawClear,
drawCells, uploadAtlasRegion, renderToOffscreen (three waits), and
drawTextCoverageCompareFrame with vk_sync.waitFenceBounded /
acquireImageBounded. Adds VkWaitTimeout/VkAcquireTimeout error arms in
main.zig that log via vk_sync.logVkTimeout, mark the frame dirty, and
retry on the next loop iteration. Atlas upload timeouts propagate the
dirty flag via the existing pre-assign guard.

Part of issue ab6c92f0.

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

src/main.zig
Old New
@@ -7,6 +7,7 @@ const renderer = @import("renderer");
7 const font = @import("font"); 7 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 vk_sync = @import("vk_sync");
10 const bench_stats = @import("bench_stats"); 11 const bench_stats = @import("bench_stats");
11 const cell_instance = @import("cell_instance"); 12 const cell_instance = @import("cell_instance");
12 const appendCellInstances = cell_instance.appendCellInstances; 13 const appendCellInstances = cell_instance.appendCellInstances;
@@ -592,12 +593,19 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
592 const y_start = atlas.last_uploaded_y; 593 const y_start = atlas.last_uploaded_y;
593 const y_end = atlas.cursor_y + atlas.row_height; 594 const y_end = atlas.cursor_y + atlas.row_height;
594 if (y_start < y_end) { 595 if (y_start < y_end) {
595 try ctx.uploadAtlasRegion( 596 ctx.uploadAtlasRegion(
596 atlas.pixels, 597 atlas.pixels,
597 y_start, 598 y_start,
598 y_end, 599 y_end,
599 atlas.needs_full_upload, 600 atlas.needs_full_upload,
600 ); 601 ) catch |err| switch (err) {
602 error.VkWaitTimeout => {
603 vk_sync.logVkTimeout(@src(), .atlas);
604 render_pending = true;
605 continue;
606 },
607 else => return err,
608 };
601 atlas.last_uploaded_y = atlas.cursor_y; 609 atlas.last_uploaded_y = atlas.cursor_y;
602 atlas.needs_full_upload = false; 610 atlas.needs_full_upload = false;
603 render_cache.layout_dirty = true; 611 render_cache.layout_dirty = true;
@@ -686,6 +694,11 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
686 render_pending = true; 694 render_pending = true;
687 continue; 695 continue;
688 }, 696 },
697 error.VkWaitTimeout, error.VkAcquireTimeout => {
698 vk_sync.logVkTimeout(@src(), .fence);
699 render_pending = true;
700 continue;
701 },
689 else => return err, 702 else => return err,
690 }; 703 };
691 frame_timing.gpu_submit_us = usFromTimer(&section_timer); 704 frame_timing.gpu_submit_us = usFromTimer(&section_timer);
@@ -1361,27 +1374,11 @@ fn drawTextCoverageCompareFrame(
1361 cell_h_px: u32, 1374 cell_h_px: u32,
1362 clear_color: [4]f32, 1375 clear_color: [4]f32,
1363 ) !void { 1376 ) !void {
1364 _ = try ctx.vkd.waitForFences( 1377 try vk_sync.waitFenceBounded(ctx.vkd, ctx.device, ctx.in_flight_fence);
1365 ctx.device,
1366 1,
1367 @ptrCast(&ctx.in_flight_fence),
1368 .true,
1369 std.math.maxInt(u64),
1370 );
1371 try ctx.vkd.resetFences(ctx.device, 1, @ptrCast(&ctx.in_flight_fence));
1372 1378
1373 const acquire = ctx.vkd.acquireNextImageKHR( 1379 const image_index = try vk_sync.acquireImageBounded(ctx.vkd, ctx.device, ctx.swapchain, ctx.image_available);
1374 ctx.device, 1380
1375 ctx.swapchain, 1381 try ctx.vkd.resetFences(ctx.device, 1, @ptrCast(&ctx.in_flight_fence));
1376 std.math.maxInt(u64),
1377 ctx.image_available,
1378 .null_handle,
1379 ) catch |err| switch (err) {
1380 error.OutOfDateKHR => return error.OutOfDateKHR,
1381 else => return err,
1382 };
1383 if (acquire.result == .suboptimal_khr) return error.OutOfDateKHR;
1384 const image_index = acquire.image_index;
1385 1382
1386 try ctx.vkd.resetCommandBuffer(ctx.command_buffer, .{}); 1383 try ctx.vkd.resetCommandBuffer(ctx.command_buffer, .{});
1387 try ctx.vkd.beginCommandBuffer(ctx.command_buffer, &vk.CommandBufferBeginInfo{ 1384 try ctx.vkd.beginCommandBuffer(ctx.command_buffer, &vk.CommandBufferBeginInfo{
src/renderer.zig
Old New
@@ -1,5 +1,6 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const vk = @import("vulkan"); 2 const vk = @import("vulkan");
3 const vk_sync = @import("vk_sync");
3 4
4 const dl = @cImport({ 5 const dl = @cImport({
5 @cInclude("dlfcn.h"); 6 @cInclude("dlfcn.h");
@@ -1287,23 +1288,8 @@ pub const Context = struct {
1287 /// Does not bind the pipeline or draw — just clear + present. 1288 /// Does not bind the pipeline or draw — just clear + present.
1288 /// Blocks until the previous frame's fence signals. 1289 /// Blocks until the previous frame's fence signals.
1289 pub fn drawClear(self: *Context, clear_color: [4]f32) !void { 1290 pub fn drawClear(self: *Context, clear_color: [4]f32) !void {
1290 // Wait for previous frame to finish 1291 try vk_sync.waitFenceBounded(self.vkd, self.device, self.in_flight_fence);
1291 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64)); 1292 const image_index = try vk_sync.acquireImageBounded(self.vkd, self.device, self.swapchain, self.image_available);
1292
1293 // Acquire next image BEFORE reset, so an acquire failure leaves the
1294 // fence in a safe state (signaled from the prior frame).
1295 const acquire = self.vkd.acquireNextImageKHR(
1296 self.device,
1297 self.swapchain,
1298 std.math.maxInt(u64),
1299 self.image_available,
1300 .null_handle,
1301 ) catch |err| switch (err) {
1302 error.OutOfDateKHR => return error.OutOfDateKHR,
1303 else => return err,
1304 };
1305 if (swapchainNeedsRebuild(acquire.result)) return error.OutOfDateKHR;
1306 const image_index = acquire.image_index;
1307 1293
1308 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence)); 1294 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
1309 errdefer self.resignalFence(self.in_flight_fence); 1295 errdefer self.resignalFence(self.in_flight_fence);
@@ -1494,8 +1480,9 @@ pub const Context = struct {
1494 const byte_len: usize = @as(usize, y_end - y_start) * self.atlas_width; 1480 const byte_len: usize = @as(usize, y_end - y_start) * self.atlas_width;
1495 1481
1496 // Wait for any prior atlas transfer to finish before reusing staging buffer 1482 // Wait for any prior atlas transfer to finish before reusing staging buffer
1497 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.atlas_transfer_fence), .true, std.math.maxInt(u64)); 1483 try vk_sync.waitFenceBounded(self.vkd, self.device, self.atlas_transfer_fence);
1498 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.atlas_transfer_fence)); 1484 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.atlas_transfer_fence));
1485 errdefer self.resignalFence(self.atlas_transfer_fence);
1499 1486
1500 // Copy dirty band into staging buffer 1487 // Copy dirty band into staging buffer
1501 const mapped = try self.vkd.mapMemory(self.device, self.atlas_staging_memory, 0, @intCast(byte_len), .{}); 1488 const mapped = try self.vkd.mapMemory(self.device, self.atlas_staging_memory, 0, @intCast(byte_len), .{});
@@ -1725,27 +1712,13 @@ pub const Context = struct {
1725 } 1712 }
1726 }.read; 1713 }.read;
1727 1714
1728 // Wait for previous frame to finish 1715 try vk_sync.waitFenceBounded(self.vkd, self.device, self.in_flight_fence);
1729 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64));
1730 if (timing_out) |t| { 1716 if (timing_out) |t| {
1731 t.wait_fences_us = readTimer(&timer); 1717 t.wait_fences_us = readTimer(&timer);
1732 timer.reset(); 1718 timer.reset();
1733 } 1719 }
1734 1720
1735 // Acquire next image BEFORE reset, so an acquire failure leaves the 1721 const image_index = try vk_sync.acquireImageBounded(self.vkd, self.device, self.swapchain, self.image_available);
1736 // fence in a safe state (signaled from the prior frame).
1737 const acquire = self.vkd.acquireNextImageKHR(
1738 self.device,
1739 self.swapchain,
1740 std.math.maxInt(u64),
1741 self.image_available,
1742 .null_handle,
1743 ) catch |err| switch (err) {
1744 error.OutOfDateKHR => return error.OutOfDateKHR,
1745 else => return err,
1746 };
1747 if (swapchainNeedsRebuild(acquire.result)) return error.OutOfDateKHR;
1748 const image_index = acquire.image_index;
1749 if (timing_out) |t| { 1722 if (timing_out) |t| {
1750 t.acquire_us = readTimer(&timer); 1723 t.acquire_us = readTimer(&timer);
1751 timer.reset(); 1724 timer.reset();
@@ -1837,14 +1810,15 @@ pub const Context = struct {
1837 // shared instance buffer. (drawCells and renderToOffscreen share 1810 // shared instance buffer. (drawCells and renderToOffscreen share
1838 // self.instance_memory; without this wait the host would overwrite bytes 1811 // self.instance_memory; without this wait the host would overwrite bytes
1839 // the GPU is still reading.) 1812 // the GPU is still reading.)
1840 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64)); 1813 try vk_sync.waitFenceBounded(self.vkd, self.device, self.in_flight_fence);
1841 1814
1842 // 1. Upload instances (same path drawCells uses) 1815 // 1. Upload instances (same path drawCells uses)
1843 try self.uploadInstances(instance_data); 1816 try self.uploadInstances(instance_data);
1844 1817
1845 // 2. Reset + begin capture command buffer 1818 // 2. Reset + begin capture command buffer
1846 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.capture_fence), .true, std.math.maxInt(u64)); 1819 try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence);
1847 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.capture_fence)); 1820 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.capture_fence));
1821 errdefer self.resignalFence(self.capture_fence);
1848 1822
1849 try self.vkd.resetCommandBuffer(self.capture_cmd, .{}); 1823 try self.vkd.resetCommandBuffer(self.capture_cmd, .{});
1850 try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{ 1824 try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{
@@ -1960,7 +1934,7 @@ pub const Context = struct {
1960 .command_buffer_count = 1, 1934 .command_buffer_count = 1,
1961 .p_command_buffers = @ptrCast(&self.capture_cmd), 1935 .p_command_buffers = @ptrCast(&self.capture_cmd),
1962 }), self.capture_fence); 1936 }), self.capture_fence);
1963 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.capture_fence), .true, std.math.maxInt(u64)); 1937 try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence);
1964 } 1938 }
1965 1939
1966 /// Read the offscreen target's readback buffer into `out_rgba`. 1940 /// Read the offscreen target's readback buffer into `out_rgba`.