a73x

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(&section_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`.