Stable radix sprite sort and render hot-path optimizations
SpriteBatcher now sorts with a stable LSD radix sort: equal-key sprites keep submission order across frames (no flicker) and passes over digits identical in all keys are skipped, making the common single-layer case nearly free. Hot paths avoid per-sprite trig and square roots: SinCos is skipped for unrotated sprites and the culling radius comes from the region's precomputed diagonal. Stress scene (100k entities, ~61k on screen, Release): 103 -> 124 FPS. Sample now runs with VSync off to show real frame rates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2b7d4c4fef
commit
a3e6d3bb0a
@@ -29,14 +29,22 @@ public struct SpriteInstance
|
||||
|
||||
/// <summary>
|
||||
/// CPU side of the renderer: collects <see cref="SpriteInstance"/>s with their sort keys
|
||||
/// and orders them layer → depth → texture. Allocation-free after warm-up
|
||||
/// (arrays grow geometrically and are reused across frames).
|
||||
/// and orders them layer → depth → texture using a stable LSD radix sort — sprites with
|
||||
/// equal keys keep their submission order across frames (no flicker), and sorting stays
|
||||
/// O(n) on large counts. Allocation-free after warm-up (arrays grow geometrically and are
|
||||
/// reused across frames).
|
||||
/// </summary>
|
||||
public sealed class SpriteBatcher
|
||||
{
|
||||
private const int RadixBits = 16;
|
||||
private const int RadixSize = 1 << RadixBits;
|
||||
|
||||
private SpriteInstance[] _instances;
|
||||
private ulong[] _keys;
|
||||
private ulong[] _keysTemp;
|
||||
private int[] _order;
|
||||
private int[] _orderTemp;
|
||||
private readonly int[] _histogram = new int[RadixSize];
|
||||
private int _count;
|
||||
|
||||
/// <summary>Creates a batcher with the given initial capacity.</summary>
|
||||
@@ -44,7 +52,9 @@ public sealed class SpriteBatcher
|
||||
{
|
||||
_instances = new SpriteInstance[initialCapacity];
|
||||
_keys = new ulong[initialCapacity];
|
||||
_keysTemp = new ulong[initialCapacity];
|
||||
_order = new int[initialCapacity];
|
||||
_orderTemp = new int[initialCapacity];
|
||||
}
|
||||
|
||||
/// <summary>Number of sprites submitted this frame.</summary>
|
||||
@@ -64,18 +74,74 @@ public sealed class SpriteBatcher
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts all submitted sprites and returns their indices in draw order.
|
||||
/// Valid until the next <see cref="Clear"/>.
|
||||
/// Sorts all submitted sprites (stable: equal keys keep submission order) and returns
|
||||
/// their indices in draw order. Valid until the next <see cref="Clear"/>.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<int> Sort()
|
||||
{
|
||||
for (var i = 0; i < _count; i++)
|
||||
var n = _count;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
_order[i] = i;
|
||||
}
|
||||
|
||||
Array.Sort(_keys, _order, 0, _count);
|
||||
return _order.AsSpan(0, _count);
|
||||
if (n < 2)
|
||||
{
|
||||
return _order.AsSpan(0, n);
|
||||
}
|
||||
|
||||
// Биты, различающиеся хотя бы у одной пары ключей: проходы по одинаковым
|
||||
// разрядам (один слой, одна глубина) пропускаются целиком.
|
||||
ulong orBits = 0, andBits = ~0UL;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
orBits |= _keys[i];
|
||||
andBits &= _keys[i];
|
||||
}
|
||||
|
||||
var differing = orBits ^ andBits;
|
||||
var keys = _keys;
|
||||
var order = _order;
|
||||
var keysOut = _keysTemp;
|
||||
var orderOut = _orderTemp;
|
||||
|
||||
for (var shift = 0; shift < 64; shift += RadixBits)
|
||||
{
|
||||
if ((differing >> shift & (RadixSize - 1)) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Array.Clear(_histogram, 0, RadixSize);
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
_histogram[(int)(keys[i] >> shift & (RadixSize - 1))]++;
|
||||
}
|
||||
|
||||
var running = 0;
|
||||
for (var digit = 0; digit < RadixSize; digit++)
|
||||
{
|
||||
var bucket = _histogram[digit];
|
||||
_histogram[digit] = running;
|
||||
running += bucket;
|
||||
}
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var position = _histogram[(int)(keys[i] >> shift & (RadixSize - 1))]++;
|
||||
keysOut[position] = keys[i];
|
||||
orderOut[position] = order[i];
|
||||
}
|
||||
|
||||
(keys, keysOut) = (keysOut, keys);
|
||||
(order, orderOut) = (orderOut, order);
|
||||
}
|
||||
|
||||
_keys = keys;
|
||||
_keysTemp = keysOut;
|
||||
_order = order;
|
||||
_orderTemp = orderOut;
|
||||
return _order.AsSpan(0, n);
|
||||
}
|
||||
|
||||
/// <summary>Returns the instance at <paramref name="index"/> (an index from <see cref="Sort"/>).</summary>
|
||||
@@ -89,6 +155,8 @@ public sealed class SpriteBatcher
|
||||
var capacity = _instances.Length * 2;
|
||||
Array.Resize(ref _instances, capacity);
|
||||
Array.Resize(ref _keys, capacity);
|
||||
Array.Resize(ref _keysTemp, capacity);
|
||||
Array.Resize(ref _order, capacity);
|
||||
Array.Resize(ref _orderTemp, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user