a73x

c1de8b6f

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

a73x   2026-04-18 15:16

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>

diff --git a/src/renderer.zig b/src/renderer.zig
index b30d897..5c15fc4 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -1292,7 +1292,10 @@ pub const Context = struct {
        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);
        // Re-signal only on errors before queueSubmit; once submit succeeds the
        // GPU will signal the fence on completion and a manual signal would race.
        var submit_done = false;
        errdefer if (!submit_done) self.resignalFence(self.in_flight_fence);

        // Record command buffer
        try self.vkd.resetCommandBuffer(self.command_buffer, .{});
@@ -1331,6 +1334,7 @@ pub const Context = struct {
            .signal_semaphore_count = 1,
            .p_signal_semaphores = @ptrCast(&self.render_finished),
        }), self.in_flight_fence);
        submit_done = true;

        // Present
        const present_result = self.vkd.queuePresentKHR(self.present_queue, &vk.PresentInfoKHR{
@@ -1725,7 +1729,10 @@ pub const Context = struct {
        }

        try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
        errdefer self.resignalFence(self.in_flight_fence);
        // Re-signal only on errors before queueSubmit; once submit succeeds the
        // GPU will signal the fence on completion and a manual signal would race.
        var submit_done = false;
        errdefer if (!submit_done) self.resignalFence(self.in_flight_fence);

        // Record command buffer
        try self.vkd.resetCommandBuffer(self.command_buffer, .{});
@@ -1774,6 +1781,7 @@ pub const Context = struct {
            .signal_semaphore_count = 1,
            .p_signal_semaphores = @ptrCast(&self.render_finished),
        }), self.in_flight_fence);
        submit_done = true;
        if (timing_out) |t| {
            t.submit_us = readTimer(&timer);
            timer.reset();
@@ -1818,7 +1826,12 @@ pub const Context = struct {
        // 2. Reset + begin capture command buffer
        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);
        // Re-signal only on errors before queueSubmit; once submit succeeds the
        // GPU will signal the fence on completion and a manual signal would race
        // (in particular, the post-submit waitFenceBounded below could time out
        // on a driver flake while the GPU work is still legitimately in flight).
        var submit_done = false;
        errdefer if (!submit_done) self.resignalFence(self.capture_fence);

        try self.vkd.resetCommandBuffer(self.capture_cmd, .{});
        try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{
@@ -1934,6 +1947,7 @@ pub const Context = struct {
            .command_buffer_count = 1,
            .p_command_buffers = @ptrCast(&self.capture_cmd),
        }), self.capture_fence);
        submit_done = true;
        try vk_sync.waitFenceBounded(self.vkd, self.device, self.capture_fence);
    }