From 81ea3fd42c9e2bdb2a79acf1f591d7ebc9508d72 Mon Sep 17 00:00:00 2001 From: Zachary Levy Date: Fri, 8 May 2026 16:39:25 -0700 Subject: [PATCH] GPU correctness fixes and optimizations --- draw/backdrop.odin | 8 +++++++- draw/core_2d.odin | 10 +++++++--- draw/examples/hellope.odin | 30 +++++++++++++++--------------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/draw/backdrop.odin b/draw/backdrop.odin index 5b65b18..fb22c79 100644 --- a/draw/backdrop.odin +++ b/draw/backdrop.odin @@ -684,7 +684,13 @@ upload_backdrop_primitives :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPas sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, ) - prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, false) + // cycle=true: this is a persistent per-frame streaming transfer buffer. The previous + // frame's UploadToGPUBuffer is almost certainly still in flight when we map here + // (allowedFramesInFlight defaults to 2 on Metal). Without cycling, the CPU memcpy below + // races the GPU's blit read on the same MTLBuffer.contents. Cycling rebinds the + // container's active internal buffer to an unbound one (or allocates a new one) — O(1) + // in steady state, no fence wait. See SDL_gpu_metal.m's METAL_INTERNAL_PrepareBufferForWrite. + prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, true) if prim_array == nil { log.panicf("Failed to map backdrop primitive transfer buffer: %s", sdl.GetError()) } diff --git a/draw/core_2d.odin b/draw/core_2d.odin index 96b1fa0..b3f3879 100644 --- a/draw/core_2d.odin +++ b/draw/core_2d.odin @@ -538,7 +538,9 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { sdl.GPUBufferUsageFlags{.VERTEX}, ) - vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, false) + // cycle=true: see backdrop.odin upload_backdrop_primitives. Persistent per-frame + // streaming buffer; previous frame's blit is still in flight at map time. + vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, true) if vert_array == nil { log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError()) } @@ -569,7 +571,8 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { grow_buffer_if_needed(device, &GLOB.core_2d.index_buffer, index_size, sdl.GPUBufferUsageFlags{.INDEX}) - idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, false) + // cycle=true: see vertex_buffer above. + idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, true) if idx_array == nil { log.panicf("Failed to map index transfer buffer: %s", sdl.GetError()) } @@ -596,7 +599,8 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, ) - prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, false) + // cycle=true: see vertex_buffer above. + prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, true) if prim_array == nil { log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError()) } diff --git a/draw/examples/hellope.odin b/draw/examples/hellope.odin index 63fe227..27f248f 100644 --- a/draw/examples/hellope.odin +++ b/draw/examples/hellope.odin @@ -279,12 +279,14 @@ hellope_custom :: proc() { } gauge := Gauge { - value = 0.73, - color = {50, 200, 100, 255}, + value = 0.73, + color = {50, 200, 100, 255}, + bg_color = {80, 80, 80, 255}, } gauge2 := Gauge { - value = 0.45, - color = {200, 100, 50, 255}, + value = 0.45, + color = {200, 100, 50, 255}, + bg_color = {80, 80, 80, 255}, } // `clay.CustomElementConfig.customData` is a rawptr; the Clay integration in `draw` @@ -342,11 +344,11 @@ hellope_custom :: proc() { // reflection inside the strip), and gauge2 is deferred-replayed by // `prepare_clay_batch` after the bracket closes (renders crisp on top of the // bracket output — unrelated to the strip since they don't overlap). + // `backgroundColor` is omitted on the gauges; bg lives on `Gauge.bg_color`. See `draw_custom`. if clay.UI(clay.ID("gauge"))( { layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, custom = {customData = &gauge_custom}, - backgroundColor = {80, 80, 80, 255}, }, ) { if clay.UI(clay.ID("backdrop"))( @@ -362,7 +364,6 @@ hellope_custom :: proc() { { layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, custom = {customData = &gauge2_custom}, - backgroundColor = {80, 80, 80, 255}, }, ) {} } @@ -376,8 +377,9 @@ hellope_custom :: proc() { } Gauge :: struct { - value: f32, - color: draw.Color, + value: f32, + color: draw.Color, + bg_color: draw.Color, } draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) { @@ -386,14 +388,12 @@ hellope_custom :: proc() { // before the union refactor. gauge := cast(^Gauge)render_data.customData + // `gauge.bg_color` instead of `render_data.backgroundColor`: under Clay master, an + // element with both `custom.customData` and `backgroundColor` emits a Custom AND a + // Rectangle for the same bounds, in that order — the Rectangle paints over the + // callback's output. Carrying bg on user data sidesteps it. border_width: f32 = 2 - draw.rectangle( - layer, - bounds, - draw.color_from_clay(render_data.backgroundColor), - outline_color = draw.WHITE, - outline_width = border_width, - ) + draw.rectangle(layer, bounds, gauge.bg_color, outline_color = draw.WHITE, outline_width = border_width) fill := draw.Rectangle { x = bounds.x,