From c97a17f85b20d1495b33f22dfae6c08fd4b783b2 Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Fri, 14 Aug 2026 23:08:00 -0700 Subject: [PATCH] Speed group_texture_runs for two-texture batches. --- engine/app.odin | 85 +++++++++++++++++++++++++++++++++------ engine/batching_test.odin | 45 +++++++++++++++++++++ 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/engine/app.odin b/engine/app.odin index 412e67b..9a64c7b 100644 --- a/engine/app.odin +++ b/engine/app.odin @@ -504,19 +504,78 @@ group_texture_runs :: proc(list: []Queued_Sprite) { 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 - } + stable_group_by_texture(list[start:end]) start = end } } + +// Stably orders window by texture pointer. Fast path for 1–2 textures +// (common); insertion sort for 3+. +stable_group_by_texture :: proc(window: []Queued_Sprite) { + n := len(window) + if n <= 1 do return + + already := true + for i in 1 ..< n { + if uintptr(window[i].texture) < uintptr(window[i - 1].texture) { + already = false + break + } + } + if already do return + + first := uintptr(window[0].texture) + second: uintptr + has_second := false + third := false + for i in 1 ..< n { + t := uintptr(window[i].texture) + if t == first do continue + if !has_second { + second = t + has_second = true + continue + } + if t != second { + third = true + break + } + } + + if !has_second do return + + if third { + for i in 1 ..< n { + item := window[i] + j := i + for j > 0 { + if uintptr(window[j - 1].texture) <= uintptr(item.texture) { + break + } + window[j] = window[j - 1] + j -= 1 + } + window[j] = item + } + return + } + + lo, hi := first, second + if lo > hi do lo, hi = hi, lo + + tmp: [MAX_SPRITES]Queued_Sprite + w := 0 + for q in window { + if uintptr(q.texture) == lo { + tmp[w] = q + w += 1 + } + } + for q in window { + if uintptr(q.texture) == hi { + tmp[w] = q + w += 1 + } + } + copy(window, tmp[:w]) +} diff --git a/engine/batching_test.odin b/engine/batching_test.odin index a378054..917f484 100644 --- a/engine/batching_test.odin +++ b/engine/batching_test.odin @@ -190,6 +190,51 @@ group_texture_runs_already_optimal :: proc(t: ^testing.T) { testing.expect_value(t, marker_of(list[3]), f32(40)) } +@(test) +group_texture_runs_alternating_many_stable :: proc(t: ^testing.T) { + // Characterization: large alternating group must collapse to two runs + // while preserving same-texture submission order (markers). + N :: 64 + list := make([]Queued_Sprite, N) + defer delete(list) + for i in 0 ..< N { + tex: uintptr = 2 if (i % 2) == 0 else 1 + list[i] = queued(tex, 1, f32(i + 1)) + } + testing.expect_value(t, texture_run_count(list), N) + group_texture_runs(list) + testing.expect_value(t, texture_run_count(list), 2) + testing.expect(t, list[0].texture == fake_tex(1)) + testing.expect(t, list[N / 2 - 1].texture == fake_tex(1)) + testing.expect(t, list[N / 2].texture == fake_tex(2)) + testing.expect(t, list[N - 1].texture == fake_tex(2)) + for i in 0 ..< N / 2 { + testing.expect_value(t, marker_of(list[i]), f32(2 * i + 2)) // odd markers: 2,4,...,N + testing.expect_value(t, marker_of(list[N / 2 + i]), f32(2 * i + 1)) // even markers: 1,3,...,N-1 + } +} + +@(test) +group_texture_runs_three_textures_stable :: proc(t: ^testing.T) { + list := []Queued_Sprite { + queued(3, 1, 1), + queued(1, 1, 2), + queued(2, 1, 3), + queued(3, 1, 4), + queued(1, 1, 5), + queued(2, 1, 6), + } + testing.expect_value(t, texture_run_count(list), 6) + group_texture_runs(list) + testing.expect_value(t, texture_run_count(list), 3) + testing.expect_value(t, marker_of(list[0]), f32(2)) + testing.expect_value(t, marker_of(list[1]), f32(5)) + testing.expect_value(t, marker_of(list[2]), f32(3)) + testing.expect_value(t, marker_of(list[3]), f32(6)) + testing.expect_value(t, marker_of(list[4]), f32(1)) + testing.expect_value(t, marker_of(list[5]), f32(4)) +} + @(test) group_texture_runs_noncontiguous_same_group_id :: proc(t: ^testing.T) { // Same nonzero id split by group 0: each window regroups alone.