Skip to content

Commit c18b15a

Browse files
authored
refactor: decompose Renderer2D and optimize RenderBuffer2D sort (#7)
* refactor: decompose Renderer2D and optimize RenderBuffer2D sort * fix: stable sort and clearArray in RenderBuffer2D - Pack (layer << 32) | index into int64 keys for stable sort - Add clearArray=true when returning items to ArrayPool - Update stability test with 20-item input (would fail with unstable sort)
1 parent 3041478 commit c18b15a

4 files changed

Lines changed: 268 additions & 205 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
- **Breaking:** `LitSprite` command signature changed — now carries `LightContext2D * SpriteState` instead of 8 individual fields. Consumers must update pattern matches and `LightDraw.litSprite` call sites to use the new `SpriteState` type.
2626
- **Breaking:** `IRenderPipeline3D.Execute` signature changed from curried (`gameCtx -> buffer -> rtPool -> unit`) to tupled (`gameCtx * buffer * rtPool -> unit`). All implementations and call sites must update.
2727
- `SpriteState` moved from `Command2D` module to top-level `Mibo.Elmish.Graphics2D` namespace.
28+
- `Renderer2D` refactored: extracted command dispatch into `module private CommandHandlers` with `RendererState` struct threaded `byref`. Post-processing extracted into `PostProcess2D` module. Class reduced from ~530 LOC to ~60 LOC of orchestration.
29+
- `RenderBuffer2D.Sort` optimized: layer keys are now precomputed during `Add` (O(n) pattern matches) and sort uses `Array.Sort(keys, items, ...)` with primitive int comparisons, eliminating O(n log n) repeated pattern matching over the 37-case `Command2D` union. Sort is now stable — same-layer commands preserve insertion order via packed `int64` keys (layer in high 32 bits, insertion index in low 32 bits).
2830
- Shadow rendering: `collectMeshDraws` now partitions draws (non-skinned first, skinned second) to minimize shader switches in the shadow pass.
2931
- Shadow rendering: `renderShadowRegion` skips `computeNormalMatrix` and `SetShaderValueMatrix` when consecutive meshes share the same transform.
3032
- Removed `lightsDirty` class field from `ForwardPbrPipeline`; handlers now check only `ShaderVariant.LightsDirty`. `handleLightCommand` sets all three variants' dirty flags directly.

src/Mibo.Raylib.Tests/Graphics2DTests.fs

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -840,34 +840,26 @@ let renderBuffer2DTests =
840840
test "Sort preserves insertion order for same layer" {
841841
let buf = RenderBuffer2D()
842842

843-
let cmdA =
844-
Command2D.fillCircle
845-
(5<RenderLayer>, Color.Red)
846-
(Vector2(1.0f, 0.0f), 10.0f)
847-
848-
let cmdB =
849-
Command2D.fillCircle
850-
(5<RenderLayer>, Color.Blue)
851-
(Vector2(2.0f, 0.0f), 10.0f)
852-
853-
let cmdC =
854-
Command2D.fillCircle
855-
(5<RenderLayer>, Color.Green)
856-
(Vector2(3.0f, 0.0f), 10.0f)
857-
858-
buf.Add(cmdA)
859-
buf.Add(cmdB)
860-
buf.Add(cmdC)
843+
// Use enough items on the same layer that an unstable sort would
844+
// reorder them — 3 items is too few to trigger swaps in most algorithms.
845+
let colors = [|
846+
for i in 0..19 -> Color(byte i, byte(i * 10), 0uy, 255uy)
847+
|]
848+
849+
for c in colors do
850+
buf.Add(
851+
Command2D.fillCircle
852+
(5<RenderLayer>, c)
853+
(Vector2(float32 c.R, 0.0f), 10.0f)
854+
)
855+
861856
buf.Sort()
862857

863-
match buf.Item 0, buf.Item 1, buf.Item 2 with
864-
| Command2D.FillCircle(_, _, c1, _),
865-
Command2D.FillCircle(_, _, c2, _),
866-
Command2D.FillCircle(_, _, c3, _) ->
867-
Expect.equal c1 Color.Red "First should be Red"
868-
Expect.equal c2 Color.Blue "Second should be Blue"
869-
Expect.equal c3 Color.Green "Third should be Green"
870-
| _ -> Tests.failtest "Expected FillCircle commands"
858+
for i = 0 to colors.Length - 1 do
859+
match buf.Item i with
860+
| Command2D.FillCircle(_, _, c, _) ->
861+
Expect.equal c colors[i] $"Item {i} should match insertion order"
862+
| _ -> Tests.failtest "Expected FillCircle"
871863
}
872864

873865
test "Buffer expands capacity when full" {

src/Mibo.Raylib/Graphics2D/RenderBuffer.fs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace Mibo.Elmish.Graphics2D
22

33
open System
44
open System.Buffers
5-
open System.Collections.Generic
65

76
/// <summary>
87
/// An allocation-free buffer for 2D render commands, sorted by layer.
@@ -22,8 +21,9 @@ type RenderBuffer2D
2221
/// <summary>Initial capacity. Defaults to 1024 if not specified.</summary>
2322
?capacity: int) =
2423

25-
let mutable items = ArrayPool<Command2D>.Shared.Rent(defaultArg capacity 1024)
26-
24+
let initialCapacity = defaultArg capacity 1024
25+
let mutable items = ArrayPool<Command2D>.Shared.Rent(initialCapacity)
26+
let mutable keys = ArrayPool<int64>.Shared.Rent(initialCapacity)
2727
let mutable count = 0
2828

2929
let getLayer(cmd: Command2D) =
@@ -76,20 +76,19 @@ type RenderBuffer2D
7676
| Command2D.DisableShadows(_, layer) -> layer
7777
| Command2D.Particle(_, _, _, layer) -> layer
7878

79-
let layerComparer =
80-
{ new IComparer<Command2D> with
81-
member _.Compare(a, b) = int(getLayer a) - int(getLayer b)
82-
}
83-
8479
let ensureCapacity(needed: int) =
8580
if count + needed > items.Length then
8681
let newSize = max (items.Length * 2) (count + needed)
8782

88-
let newArr = ArrayPool<Command2D>.Shared.Rent(newSize)
83+
let newItems = ArrayPool<Command2D>.Shared.Rent(newSize)
84+
let newKeys = ArrayPool<int64>.Shared.Rent(newSize)
8985

90-
Array.Copy(items, newArr, count)
91-
ArrayPool<Command2D>.Shared.Return(items)
92-
items <- newArr
86+
Array.Copy(items, newItems, count)
87+
Array.Copy(keys, newKeys, count)
88+
ArrayPool<Command2D>.Shared.Return(items, true)
89+
ArrayPool<int64>.Shared.Return(keys)
90+
items <- newItems
91+
keys <- newKeys
9392

9493
/// <summary>The number of commands currently in the buffer.</summary>
9594
member _.Count = count
@@ -101,6 +100,7 @@ type RenderBuffer2D
101100
member _.Add(cmd: Command2D) =
102101
ensureCapacity 1
103102
items[count] <- cmd
103+
keys[count] <- (int64(int(getLayer cmd)) <<< 32) ||| int64 count
104104
count <- count + 1
105105

106106
/// <summary>
@@ -110,9 +110,10 @@ type RenderBuffer2D
110110
member _.Clear() = count <- 0
111111

112112
/// <summary>
113-
/// Sorts commands by layer in ascending order.
113+
/// Sorts commands by layer in ascending order, preserving insertion order for same-layer commands.
114+
/// Uses precomputed int64 keys (layer in high 32 bits, insertion index in low 32 bits) to avoid
115+
/// repeated pattern matching during comparisons and guarantee stable sort.
114116
/// Must be called after <see cref="M:Mibo.Elmish.Graphics2D.RenderBuffer2D.Clear"/>
115117
/// and population, before iteration.
116118
/// </summary>
117-
member _.Sort() =
118-
Array.Sort(items, 0, count, layerComparer)
119+
member _.Sort() = Array.Sort(keys, items, 0, count)

0 commit comments

Comments
 (0)