Files
codegirl-games/.github/issues/add-order-safe-texture-batching.md
T
2026-08-09 08:29:30 +00:00

117 lines
3.3 KiB
Markdown

# Add opt-in order-safe texture batching
## Summary
`end_frame` batches only consecutive sprites that use the same texture.
Alternating two textures therefore produces one sampler bind and draw call per
sprite even when some sprites could safely be regrouped.
Globally sorting transparent sprites by texture is not correct: overlapping
sprites may blend differently when submission order changes. Batching should
therefore be opt-in within explicit groups whose members are safe to reorder.
## Evidence
A temporary paired benchmark used:
- Odin `dev-2026-05-nightly:ea5175d` with `-debug -o:speed`
- SDL 3.4.12 and Vulkan/Lavapipe
- 128 animated sprites alternating between two equivalent textures
- Sorting cost included in the measured frame
- Ten order-alternated samples of 400 frames per mode
| Mode | Texture runs | Median frame time |
| --- | ---: | ---: |
| Submission order | 128 | 5.765 ms |
| Texture grouped | 2 | 5.654 ms |
Sorting and grouping improved median frame time by approximately **1.9%**.
Hardware drivers with higher draw-call overhead may show a different result.
## Reproduction harness
Run the committed alternating-texture workload on the baseline and candidate
commits:
```bash
make perf-frame \
PERF_FRAME_SCENARIO=2 \
PERF_ODIN_FLAGS="-debug -o:speed"
```
Keep all `PERF_FRAME_*` values unchanged. The baseline should produce one
texture run per sprite; the candidate should reduce runs only inside explicit
reorder-safe groups. Compare `median_ms_per_frame` and verify rendered output.
## Suggested fix
Add an explicit batch group to queued sprites. Group `0` keeps strict submission
order; nonzero groups may be reordered only when the caller guarantees that
their members are order-independent.
```odin
Queued_Sprite :: struct {
texture: ^sdl.GPUTexture,
verts: [SPRITE_VERT_COUNT]Vertex,
batch_group: u32, // 0 = strict order; nonzero = caller permits regrouping
}
```
Sort each contiguous, nonzero group by texture immediately before upload:
```odin
group_texture_runs :: proc(list: []Queued_Sprite) {
start := 0
for start < len(list) {
group := list[start].batch_group
if group == 0 {
start += 1
continue
}
end := start + 1
for end < len(list) && list[end].batch_group == group {
end += 1
}
// Stable insertion sort is sufficient while MAX_SPRITES is 128.
for i in start + 1 ..< end {
item := list[i]
j := i
for j > start {
if uintptr(list[j - 1].texture) <= uintptr(item.texture) {
break
}
list[j] = list[j - 1]
j -= 1
}
list[j] = item
}
start = end
}
}
```
Call it after all sprites are queued and before the transfer-buffer copy:
```odin
group_texture_runs(app.draw_list[:])
```
Expose batching through a separate API or explicit parameter so existing
`draw_sprite` calls remain strict-order by default:
```odin
draw_sprite_batched(&app, &sprite, batch_group = 1)
```
## Acceptance criteria
- Existing `draw_sprite` behavior preserves exact submission order.
- Reordering requires an explicit nonzero batch group.
- Sorting never moves a sprite across a strict-order entry or group boundary.
- Add tests for strict order, group boundaries, stable same-texture ordering,
and reduced texture-run count.
- Add a visual overlap test confirming default alpha compositing is unchanged.
- Benchmark sorting cost and draw-call reduction on a hardware GPU.