a73x

c1de8b6f

renderer: gate fence re-signal on pre-submit errors only

a73x   2026-04-18 15:16

Commit message
renderer: gate fence re-signal on pre-submit errors only

Code review of the prior commit caught that the errdefer re-signal in
drawClear, drawCells, and renderToOffscreen was scoped too widely. It
remained active through the post-submit operations (queuePresentKHR for
draw paths, waitFenceBounded for capture path), so a failure or timeout
*after* a successful queueSubmit would re-signal a fence the GPU was
about to signal naturally — racing the driver and corrupting fence state.

Add a submit_done sentinel and gate the errdefer on it. Once
queueSubmit succeeds, submit_done flips true and the errdefer becomes
a no-op for any subsequent error.

uploadAtlasRegion already ends immediately after queueSubmit, so its
errdefer needed no change.

Part of issue ab6c92f0.

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

src/renderer.zig
Old New
@@ -1292,7 +1292,10 @@ pub const Context = struct {
1292 const image_index = try vk_sync.acquireImageBounded(self.vkd, self.device, self.swapchain, self.image_available); 1292 const image_index = try vk_sync.acquireImageBounded(self.vkd, self.device, self.swapchain, self.image_available);
1293 1293
1294 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));
1295 errdefer self.resignalFence(self.in_flight_fence); 1295 // Re-signal only on errors before queueSubmit; once submit succeeds the
1296 // GPU will signal the fence on completion and a manual signal would race.
1297 var submit_done = false;
1298 errdefer if (!submit_done) self.resignalFence(self.in_flight_fence);
1296 1299
1297 // Record command buffer 1300 // Record command buffer
1298 try self.vkd.resetCommandBuffer(self.command_buffer, .{}); 1301 try self.vkd.resetCommandBuffer(self.command_buffer, .{});
@@ -1331,6 +1334,7 @@ pub const Context = struct {
1331 .signal_semaphore_count = 1, 1334 .signal_semaphore_count = 1,
1332 .p_signal_semaphores = @ptrCast(&self.render_finished), 1335 .p_signal_semaphores = @ptrCast(&self.render_finished),
1333 }), self.in_flight_fence); 1336 }), self.in_flight_fence);
1337 submit_done = true;
1334 1338
1335 // Present 1339 // Present
1336 const present_result = self.vkd.queuePresentKHR(self.present_queue, &vk.PresentInfoKHR{ 1340 const present_result = self.vkd.queuePresentKHR(self.present_queue, &vk.PresentInfoKHR{
@@ -1725,7 +1729,10 @@ pub const Context = struct {
1725 } 1729 }
1726 1730
1727 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence)); 1731 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
1728 errdefer self.resignalFence(self.in_flight_fence); 1732 // Re-signal only on errors before queueSubmit; once submit succeeds the
1733 // GPU will signal the fence on completion and a manual signal would race.
1734 var submit_done = false;
1735 errdefer if (!submit_done) self.resignalFence(self.in_flight_fence);
1729 1736
1730 // Record command buffer 1737 // Record command buffer
1731 try self.vkd.resetCommandBuffer(self.command_buffer, .{}); 1738 try self.vkd.resetCommandBuffer(self.command_buffer, .{});
@@ -1774,6 +1781,7 @@ pub const Context = struct {
1774 .signal_semaphore_count = 1, 1781 .signal_semaphore_count = 1,
1775 .p_signal_semaphores = @ptrCast(&self.render_finished), 1782 .p_signal_semaphores = @ptrCast(&self.render_finished),
1776 }), self.in_flight_fence); 1783 }), self.in_flight_fence);
1784 submit_done = true;
1777 if (timing_out) |t| { 1785 if (timing_out) |t| {
1778 t.submit_us = readTimer(&timer); 1786 t.submit_us = readTimer(&timer);
1779 timer.reset(); 1787 timer.reset();
@@ -1818,7 +1826,12 @@ pub const Context = struct {
1818 // 2. Reset + begin capture command buffer 1826 // 2. Reset + begin capture command buffer
1819 try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence); 1827 try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence);
1820 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.capture_fence)); 1828 try self.vkd.resetFences(self.device, 1, @ptrCast(&self.capture_fence));
1821 errdefer self.resignalFence(self.capture_fence); 1829 // Re-signal only on errors before queueSubmit; once submit succeeds the
1830 // GPU will signal the fence on completion and a manual signal would race
1831 // (in particular, the post-submit waitFenceBounded below could time out
1832 // on a driver flake while the GPU work is still legitimately in flight).
1833 var submit_done = false;
1834 errdefer if (!submit_done) self.resignalFence(self.capture_fence);
1822 1835
1823 try self.vkd.resetCommandBuffer(self.capture_cmd, .{}); 1836 try self.vkd.resetCommandBuffer(self.capture_cmd, .{});
1824 try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{ 1837 try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{
@@ -1934,6 +1947,7 @@ pub const Context = struct {
1934 .command_buffer_count = 1, 1947 .command_buffer_count = 1,
1935 .p_command_buffers = @ptrCast(&self.capture_cmd), 1948 .p_command_buffers = @ptrCast(&self.capture_cmd),
1936 }), self.capture_fence); 1949 }), self.capture_fence);
1950 submit_done = true;
1937 try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence); 1951 try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence);
1938 } 1952 }
1939 1953