a73x

66ece494

renderer: reorder resetFences after acquire, add errdefer re-signal

a73x   2026-04-18 12:35

Preparatory refactor for bounded acquire timeouts. When acquireNextImageKHR
gains a finite timeout (next commit), the existing ordering (reset → acquire)
would leave in_flight_fence unsignaled with no submit pending on a timeout,
deadlocking future waits. Reorder to (acquire → reset) and add a private
resignalFence helper that the errdefer path uses to cover the tiny
post-reset / pre-submit failure window.

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 7ee6e29..b6fe31d 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -1267,15 +1267,31 @@ pub const Context = struct {
        self.instance_capacity = new_capacity;
    }

    /// Submit an empty command batch that signals `fence`. Used as an
    /// errdefer recovery path when acquire has succeeded, resetFences has
    /// run, but we failed before queueSubmit — we need to put the fence
    /// back in the signaled state so the next frame's wait succeeds.
    fn resignalFence(self: *Context, fence: vk.Fence) void {
        const submit_info = vk.SubmitInfo{};
        _ = self.vkd.queueSubmit(
            self.graphics_queue,
            1,
            @ptrCast(&submit_info),
            fence,
        ) catch |err| {
            std.log.warn("resignalFence: {s}", .{@errorName(err)});
        };
    }

    /// Record a command buffer that begins the render pass with the given clear color and presents.
    /// 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));
        try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));

        // Acquire next image
        // 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,
@@ -1289,6 +1305,9 @@ pub const Context = struct {
        if (swapchainNeedsRebuild(acquire.result)) return error.OutOfDateKHR;
        const image_index = acquire.image_index;

        try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
        errdefer self.resignalFence(self.in_flight_fence);

        // Record command buffer
        try self.vkd.resetCommandBuffer(self.command_buffer, .{});
        try self.vkd.beginCommandBuffer(self.command_buffer, &vk.CommandBufferBeginInfo{
@@ -1708,13 +1727,13 @@ pub const Context = struct {

        // Wait for previous frame to finish
        _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64));
        try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
        if (timing_out) |t| {
            t.wait_fences_us = readTimer(&timer);
            timer.reset();
        }

        // Acquire next image
        // 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,
@@ -1732,6 +1751,9 @@ pub const Context = struct {
            timer.reset();
        }

        try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence));
        errdefer self.resignalFence(self.in_flight_fence);

        // Record command buffer
        try self.vkd.resetCommandBuffer(self.command_buffer, .{});
        try self.vkd.beginCommandBuffer(self.command_buffer, &vk.CommandBufferBeginInfo{