Backdrop scope implementation

This commit is contained in:
Zachary Levy
2026-05-01 17:46:49 -07:00
parent 5317b8f142
commit 05c6e6284c
5 changed files with 633 additions and 349 deletions
+63 -39
View File
@@ -392,16 +392,36 @@ frame_has_backdrop :: proc() -> bool {
return false
}
// Returns the absolute index of the first .Backdrop sub-batch in the layer's sub-batch range,
// or -1 if the layer has no backdrops. The index is into GLOB.tmp_sub_batches (not relative to
// layer.sub_batch_start), to match how draw_layer's render-range helpers consume it.
// Find the scissor that owns a given sub-batch index by linear scan over GLOB.scissors.
// Used by `run_backdrop_bracket`'s composite pass when the bracket loses its layer-pointer
// context: per-sub-batch scissor lookup is required to honor scissors set up upstream by
// `prepare_clay_batch`'s ScissorStart handling. O(scissors) per sub-batch is acceptable
// because scissor counts are small (single digits in typical UI frames).
//
// Panics if no scissor owns the index. The renderer's invariant is that the scissor list
// forms a contiguous, disjoint cover over `[0, len(tmp_sub_batches))` because every
// sub-batch is created via `append_or_extend_sub_batch` (which increments the active
// scissor's `sub_batch_len` in lockstep with the global array's growth) and scissors are
// only created at the current end-of-array. A miss here means that invariant is broken —
// either by a future code change that bypasses `append_or_extend_sub_batch`, by a scissor
// constructed with the wrong `sub_batch_start`, or by external corruption — and silent
// degradation would mask the bug. The panic message includes the offending index and the
// scissor list shape so the failure is locatable.
//INTERNAL
find_first_backdrop_in_layer :: proc(layer: ^Layer) -> int {
for i in 0 ..< layer.sub_batch_len {
abs_idx := layer.sub_batch_start + i
if GLOB.tmp_sub_batches[abs_idx].kind == .Backdrop do return int(abs_idx)
find_scissor_for_sub_batch :: proc(sub_batch_index: u32) -> sdl.Rect {
for scissor in GLOB.scissors {
if sub_batch_index >= scissor.sub_batch_start &&
sub_batch_index < scissor.sub_batch_start + scissor.sub_batch_len {
return scissor.bounds
}
}
return -1
log.panicf(
"find_scissor_for_sub_batch: no scissor owns sub-batch index %d (scissor count=%d, total sub-batches=%d); " +
"the scissor list must form a contiguous cover over all sub-batches",
sub_batch_index,
len(GLOB.scissors),
len(GLOB.tmp_sub_batches),
)
}
// ---------------------------------------------------------------------------------------------------------------------
@@ -754,14 +774,16 @@ compute_backdrop_group_work_region :: proc(
return
}
// Run the backdrop bracket for one layer. Assumes:
// - source_texture currently holds the pre-bracket frame contents (Pass A has already
// rendered everything that should appear behind the backdrop).
// Run one bracket over a contiguous range of pure-backdrop sub-batches. Assumes:
// - source_texture currently holds the pre-bracket frame contents (everything submitted
// ahead of this bracket on the same layer has already been rendered).
// - The caller has invoked ensure_backdrop_textures with current swapchain dimensions.
// - At least one .Backdrop sub-batch exists in the layer (caller checked).
// - The half-open range `[sub_batch_start, sub_batch_end)` is non-empty and every
// sub-batch in it has kind == .Backdrop. The caller (draw_layer) guarantees this by
// splitting the layer into runs.
//
// Per-sigma-group execution. The bracket walks the layer's sub-batches in submission order,
// grouping contiguous-same-sigma .Backdrop sub-batches. For each group:
// Per-sigma-group execution. The bracket walks the range in submission order, grouping
// contiguous-same-sigma .Backdrop sub-batches. For each group:
// 1. Pick a downsample factor using compute_backdrop_downsample_factor.
// 2. Compute that group's work region (primitives' AABB + 6σ halo, clamped).
// 3. Downsample: source_texture → downsample_texture, viewport-limited to
@@ -770,21 +792,22 @@ compute_backdrop_group_work_region :: proc(
// 5. V-blur (mode 0, direction=V): h_blur_texture → downsample_texture (ping-pong reuse;
// downsample_texture's data is no longer needed). Same viewport.
// 6. Composite (mode 1): downsample_texture (now holds H+V blur) → source_texture, full-
// target viewport, per-primitive SDF discard handles masking and applies the tint. Each
// sub-batch in the group is one instanced draw.
// target viewport, per-primitive SDF discard handles masking and applies the tint.
// Each sub-batch in the group issues an instanced draw under its own scissor (sub-
// batches inherit scissor state from the surrounding ScissorStart/End at submission).
//
// V-blur is run as its own working→working pass rather than folded into the composite. The
// folded variant produces a horizontal-vs-vertical asymmetry artifact (horizontal source
// features end up looking sharper than vertical ones inside the panel). Matching V's
// structure exactly to H's restores symmetry.
//
// On exit, source_texture contains the pre-bracket contents plus all backdrop primitives
// composited on top. The caller then runs Pass B (post-bracket non-backdrop sub-batches) on
// source_texture with LOAD.
// On exit, source_texture contains the pre-bracket contents plus all backdrop primitives in
// this range composited on top.
//INTERNAL
run_backdrop_bracket :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
layer: ^Layer,
sub_batch_start: u32,
sub_batch_end: u32,
swapchain_width, swapchain_height: u32,
) {
pipeline := &GLOB.backdrop
@@ -797,32 +820,23 @@ run_backdrop_bracket :: proc(
min_depth = 0,
max_depth = 1,
}
full_scissor := sdl.Rect {
x = 0,
y = 0,
w = i32(swapchain_width),
h = i32(swapchain_height),
}
// Working textures are at full swapchain resolution. Each per-group factor=N pass writes
// only to a sub-rect of dimensions (work_region_phys / N), via viewport-limited rendering.
layer_end := layer.sub_batch_start + layer.sub_batch_len
i := layer.sub_batch_start
layer_end := sub_batch_end
i := sub_batch_start
for i < layer_end {
// Caller guarantees this range is pure backdrop sub-batches.
assert(GLOB.tmp_sub_batches[i].kind == .Backdrop, "non-backdrop sub-batch inside bracket range")
batch := GLOB.tmp_sub_batches[i]
if batch.kind != .Backdrop {
i += 1
continue
}
// Find the contiguous run of .Backdrop sub-batches with this sigma.
sigma := batch.gaussian_sigma
group_start := i
group_end := i + 1
for group_end < layer_end {
next := GLOB.tmp_sub_batches[group_end]
if next.kind != .Backdrop || next.gaussian_sigma != sigma do break
if GLOB.tmp_sub_batches[group_end].gaussian_sigma != sigma do break
group_end += 1
}
@@ -997,6 +1011,10 @@ run_backdrop_bracket :: proc(
// upsamples (via bilinear filtering on the read), applies the SDF mask, and applies the
// tint. One render pass for the whole sigma group; each sub-batch issues its own draw
// call because non-contiguous-but-same-sigma sub-batches couldn't coalesce upstream.
//
// Per-sub-batch scissor: sub-batches inherit scissor state from ScissorStart/End that
// surrounded their submission. Switching scissors mid-pass is cheap; what matters is
// that the composite respects the same clipping the caller set up.
{
frag_uniforms.mode = 1
// direction is unused in mode 1 but keep it set so reading the uniform doesn't see
@@ -1011,7 +1029,6 @@ run_backdrop_bracket :: proc(
)
sdl.BindGPUGraphicsPipeline(pass, pipeline.blur_pipeline)
sdl.SetGPUViewport(pass, full_viewport)
sdl.SetGPUScissor(pass, full_scissor)
push_backdrop_vert_globals(cmd_buffer, f32(swapchain_width), f32(swapchain_height), 1)
push_backdrop_blur_frag_globals(cmd_buffer, &frag_uniforms)
sdl.BindGPUVertexStorageBuffers(pass, 0, ([^]^sdl.GPUBuffer)(&pipeline.primitive_buffer.gpu), 1)
@@ -1021,8 +1038,17 @@ run_backdrop_bracket :: proc(
&sdl.GPUTextureSamplerBinding{texture = pipeline.downsample_texture, sampler = pipeline.sampler},
1,
)
current_scissor: sdl.Rect = {0, 0, 0, 0}
scissor_set := false
for j in group_start ..< group_end {
grp := GLOB.tmp_sub_batches[j]
sub_batch_scissor := find_scissor_for_sub_batch(j)
if !scissor_set || sub_batch_scissor != current_scissor {
sdl.SetGPUScissor(pass, sub_batch_scissor)
current_scissor = sub_batch_scissor
scissor_set = true
}
sdl.DrawGPUPrimitives(pass, 6, grp.count, 0, grp.offset)
}
sdl.EndGPURenderPass(pass)
@@ -1129,10 +1155,8 @@ prepare_backdrop_primitive :: proc(layer: ^Layer, prim: Gaussian_Blur_Primitive,
// pass pair via sub-batch coalescing. Primitives with different sigmas in the same layer
// trigger separate blur passes (cost scales with the number of unique sigmas).
//
// Submission ordering is asymmetric: a non-backdrop draw submitted between two backdrops in
// the same layer renders *on top of* both backdrops, not between them. Use `draw.new_layer`
// to interleave. See README.md § "Backdrop pipeline" for the full bracket scheduling model.
gaussian_blur :: proc(
// Must be called inside a `begin_backdrop` / `end_backdrop` scope (or use `backdrop_scope`).
backdrop_blur :: proc(
layer: ^Layer,
rect: Rectangle,
gaussian_sigma: f32,