0c5b68af
Migrate 10 Vulkan wait/acquire sites to bounded helpers
a73x 2026-04-18 15:12
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>
diff --git a/src/main.zig b/src/main.zig index cea6884..441da56 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,6 +7,7 @@ const renderer = @import("renderer"); const font = @import("font"); const config = @import("config"); const vk = @import("vulkan"); const vk_sync = @import("vk_sync"); const bench_stats = @import("bench_stats"); const cell_instance = @import("cell_instance"); const appendCellInstances = cell_instance.appendCellInstances; @@ -592,12 +593,19 @@ fn runTerminal(alloc: std.mem.Allocator) !void { const y_start = atlas.last_uploaded_y; const y_end = atlas.cursor_y + atlas.row_height; if (y_start < y_end) { try ctx.uploadAtlasRegion( ctx.uploadAtlasRegion( atlas.pixels, y_start, y_end, atlas.needs_full_upload, ); ) catch |err| switch (err) { error.VkWaitTimeout => { vk_sync.logVkTimeout(@src(), .atlas); render_pending = true; continue; }, else => return err, }; atlas.last_uploaded_y = atlas.cursor_y; atlas.needs_full_upload = false; render_cache.layout_dirty = true; @@ -686,6 +694,11 @@ fn runTerminal(alloc: std.mem.Allocator) !void { render_pending = true; continue; }, error.VkWaitTimeout, error.VkAcquireTimeout => { vk_sync.logVkTimeout(@src(), .fence); render_pending = true; continue; }, else => return err, }; frame_timing.gpu_submit_us = usFromTimer(§ion_timer); @@ -1361,27 +1374,11 @@ fn drawTextCoverageCompareFrame( cell_h_px: u32, clear_color: [4]f32, ) !void { _ = try ctx.vkd.waitForFences( ctx.device, 1, @ptrCast(&ctx.in_flight_fence), .true, std.math.maxInt(u64), ); try ctx.vkd.resetFences(ctx.device, 1, @ptrCast(&ctx.in_flight_fence)); try vk_sync.waitFenceBounded(ctx.vkd, ctx.device, ctx.in_flight_fence); const acquire = ctx.vkd.acquireNextImageKHR( ctx.device, ctx.swapchain, std.math.maxInt(u64), ctx.image_available, .null_handle, ) catch |err| switch (err) { error.OutOfDateKHR => return error.OutOfDateKHR, else => return err, }; if (acquire.result == .suboptimal_khr) return error.OutOfDateKHR; const image_index = acquire.image_index; const image_index = try vk_sync.acquireImageBounded(ctx.vkd, ctx.device, ctx.swapchain, ctx.image_available); try ctx.vkd.resetFences(ctx.device, 1, @ptrCast(&ctx.in_flight_fence)); try ctx.vkd.resetCommandBuffer(ctx.command_buffer, .{}); try ctx.vkd.beginCommandBuffer(ctx.command_buffer, &vk.CommandBufferBeginInfo{ diff --git a/src/renderer.zig b/src/renderer.zig index b6fe31d..b30d897 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -1,5 +1,6 @@ const std = @import("std"); const vk = @import("vulkan"); const vk_sync = @import("vk_sync"); const dl = @cImport({ @cInclude("dlfcn.h"); @@ -1287,23 +1288,8 @@ pub const Context = struct { /// Does not bind the pipeline or draw — just clear + present. /// Blocks until the previous frame's fence signals. pub fn drawClear(self: *Context, clear_color: [4]f32) !void { // Wait for previous frame to finish _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64)); // Acquire next image BEFORE reset, so an acquire failure leaves the // fence in a safe state (signaled from the prior frame). const acquire = self.vkd.acquireNextImageKHR( self.device, self.swapchain, std.math.maxInt(u64), self.image_available, .null_handle, ) catch |err| switch (err) { error.OutOfDateKHR => return error.OutOfDateKHR, else => return err, }; if (swapchainNeedsRebuild(acquire.result)) return error.OutOfDateKHR; const image_index = acquire.image_index; try vk_sync.waitFenceBounded(self.vkd, self.device, self.in_flight_fence); const image_index = try vk_sync.acquireImageBounded(self.vkd, self.device, self.swapchain, self.image_available); try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence)); errdefer self.resignalFence(self.in_flight_fence); @@ -1494,8 +1480,9 @@ pub const Context = struct { const byte_len: usize = @as(usize, y_end - y_start) * self.atlas_width; // Wait for any prior atlas transfer to finish before reusing staging buffer _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.atlas_transfer_fence), .true, std.math.maxInt(u64)); try vk_sync.waitFenceBounded(self.vkd, self.device, self.atlas_transfer_fence); try self.vkd.resetFences(self.device, 1, @ptrCast(&self.atlas_transfer_fence)); errdefer self.resignalFence(self.atlas_transfer_fence); // Copy dirty band into staging buffer const mapped = try self.vkd.mapMemory(self.device, self.atlas_staging_memory, 0, @intCast(byte_len), .{}); @@ -1725,27 +1712,13 @@ pub const Context = struct { } }.read; // Wait for previous frame to finish _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64)); try vk_sync.waitFenceBounded(self.vkd, self.device, self.in_flight_fence); if (timing_out) |t| { t.wait_fences_us = readTimer(&timer); timer.reset(); } // Acquire next image BEFORE reset, so an acquire failure leaves the // fence in a safe state (signaled from the prior frame). const acquire = self.vkd.acquireNextImageKHR( self.device, self.swapchain, std.math.maxInt(u64), self.image_available, .null_handle, ) catch |err| switch (err) { error.OutOfDateKHR => return error.OutOfDateKHR, else => return err, }; if (swapchainNeedsRebuild(acquire.result)) return error.OutOfDateKHR; const image_index = acquire.image_index; const image_index = try vk_sync.acquireImageBounded(self.vkd, self.device, self.swapchain, self.image_available); if (timing_out) |t| { t.acquire_us = readTimer(&timer); timer.reset(); @@ -1837,14 +1810,15 @@ pub const Context = struct { // shared instance buffer. (drawCells and renderToOffscreen share // self.instance_memory; without this wait the host would overwrite bytes // the GPU is still reading.) _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64)); try vk_sync.waitFenceBounded(self.vkd, self.device, self.in_flight_fence); // 1. Upload instances (same path drawCells uses) try self.uploadInstances(instance_data); // 2. Reset + begin capture command buffer _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.capture_fence), .true, std.math.maxInt(u64)); try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence); try self.vkd.resetFences(self.device, 1, @ptrCast(&self.capture_fence)); errdefer self.resignalFence(self.capture_fence); try self.vkd.resetCommandBuffer(self.capture_cmd, .{}); try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{ @@ -1960,7 +1934,7 @@ pub const Context = struct { .command_buffer_count = 1, .p_command_buffers = @ptrCast(&self.capture_cmd), }), self.capture_fence); _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.capture_fence), .true, std.math.maxInt(u64)); try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence); } /// Read the offscreen target's readback buffer into `out_rgba`.