a73x

b0fdd27b

Guard offscreen renderer against shared-state hazards

a73x   2026-04-17 15:20

renderToOffscreen now waits on in_flight_fence before touching the
shared instance buffer, preventing the host from overwriting bytes
still being read by an in-flight swapchain draw.

readbackOffscreen now asserts target.format is BGRA8, since the
BGRA→RGBA swizzle assumes that layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git a/src/renderer.zig b/src/renderer.zig
index 118e483..0d67a9b 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -1781,6 +1781,12 @@ pub const Context = struct {
        instance_data: []const Instance,
        push: PushConstants,
    ) !void {
        // Wait for any in-flight swapchain frame to complete before touching the
        // 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));

        // 1. Upload instances (same path drawCells uses)
        try self.uploadInstances(instance_data);

@@ -1915,6 +1921,8 @@ pub const Context = struct {
        target: *const OffscreenTarget,
        out_rgba: []u8,
    ) !void {
        std.debug.assert(target.format == .b8g8r8a8_unorm); // swizzle below assumes BGRA8
        std.debug.assert(out_rgba.len == target.width * target.height * 4);
        if (out_rgba.len < target.readback_size) return error.BufferTooSmall;

        const mapped = try self.vkd.mapMemory(