a73x

b0fdd27b

Guard offscreen renderer against shared-state hazards

a73x   2026-04-17 15:20

Commit message
Guard offscreen renderer against shared-state hazards

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>

src/renderer.zig
Old New
@@ -1781,6 +1781,12 @@ pub const Context = struct {
1781 instance_data: []const Instance, 1781 instance_data: []const Instance,
1782 push: PushConstants, 1782 push: PushConstants,
1783 ) !void { 1783 ) !void {
1784 // Wait for any in-flight swapchain frame to complete before touching the
1785 // shared instance buffer. (drawCells and renderToOffscreen share
1786 // self.instance_memory; without this wait the host would overwrite bytes
1787 // the GPU is still reading.)
1788 _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64));
1789
1784 // 1. Upload instances (same path drawCells uses) 1790 // 1. Upload instances (same path drawCells uses)
1785 try self.uploadInstances(instance_data); 1791 try self.uploadInstances(instance_data);
1786 1792
@@ -1915,6 +1921,8 @@ pub const Context = struct {
1915 target: *const OffscreenTarget, 1921 target: *const OffscreenTarget,
1916 out_rgba: []u8, 1922 out_rgba: []u8,
1917 ) !void { 1923 ) !void {
1924 std.debug.assert(target.format == .b8g8r8a8_unorm); // swizzle below assumes BGRA8
1925 std.debug.assert(out_rgba.len == target.width * target.height * 4);
1918 if (out_rgba.len < target.readback_size) return error.BufferTooSmall; 1926 if (out_rgba.len < target.readback_size) return error.BufferTooSmall;
1919 1927
1920 const mapped = try self.vkd.mapMemory( 1928 const mapped = try self.vkd.mapMemory(