Files
mrgameeng/src/MrGameEng.Graphics/SpriteBatcher.cs
T
Leonid Pershin fd6343bd09
CI / build-test (push) Failing after 1m8s
@
Add MrGameEng.AI utility-AI module; format codebase with CSharpier

New MrGameEng.AI module (ResponseCurve, Consideration, UtilityAction,
UtilityAi selector, Blackboard) plus CSharpier formatting applied across
the whole engine. Documents the CSharpier convention in CLAUDE.md.
@
2026-06-12 07:19:10 +03:00

295 lines
9.6 KiB
C#

using Microsoft.Xna.Framework;
namespace MrGameEng.Graphics;
/// <summary>
/// Writer for one ECS chunk during parallel submission: appends accepted sprites compactly
/// into the chunk's scratch range and counts culled ones. Used by exactly one thread.
/// </summary>
public struct SpriteChunkWriter
{
private readonly SpriteInstance[] _instances;
private readonly ulong[] _keys;
private readonly int _offset;
/// <summary>Sprites accepted into this chunk's range.</summary>
public int Count { get; private set; }
/// <summary>Sprites rejected by culling in this chunk.</summary>
public int Culled { get; private set; }
internal SpriteChunkWriter(SpriteInstance[] instances, ulong[] keys, int offset)
{
_instances = instances;
_keys = keys;
_offset = offset;
Count = 0;
Culled = 0;
}
/// <summary>Appends one accepted sprite.</summary>
public void Add(in SpriteInstance instance, ulong sortKey)
{
var index = _offset + Count;
_instances[index] = instance;
_keys[index] = sortKey;
Count++;
}
/// <summary>Counts one culled sprite.</summary>
public void AddCulled() => Culled++;
}
/// <summary>One sprite queued for rendering this frame.</summary>
public struct SpriteInstance
{
/// <summary>Texture region to draw. Never null for submitted instances.</summary>
public Texture2DRegion Region;
/// <summary>World-space (or screen-space) center of the quad.</summary>
public Vector2 Center;
/// <summary>Half extents after scaling, in pixels. May be negative for negative scale.</summary>
public Vector2 HalfSize;
/// <summary>Rotation in radians, clockwise.</summary>
public float Rotation;
/// <summary>Tint color.</summary>
public Color Color;
/// <summary>Mirroring flags.</summary>
public SpriteFlip Flip;
/// <summary>Render layer the instance belongs to.</summary>
public byte Layer;
}
/// <summary>
/// CPU side of the renderer: collects <see cref="SpriteInstance"/>s with their sort keys
/// 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>
public SpriteBatcher(int initialCapacity = 2048)
{
_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>
public int Count => _count;
/// <summary>Queues one sprite.</summary>
public void Submit(in SpriteInstance instance, ulong sortKey)
{
if (_count == _instances.Length)
{
Grow();
}
_instances[_count] = instance;
_keys[_count] = sortKey;
_count++;
}
/// <summary>
/// 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()
{
var n = _count;
for (var i = 0; i < n; i++)
{
_order[i] = i;
}
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>
public ref readonly SpriteInstance this[int index] => ref _instances[index];
/// <summary>The sorted index array after <see cref="Sort"/> (first <see cref="Count"/> entries are valid).</summary>
internal int[] SortedOrder => _order;
/// <summary>Resets the batcher for the next frame. Keeps allocated capacity.</summary>
public void Clear() => _count = 0;
// --- Параллельная по-чанковая подача -------------------------------------------------
// Каждый чанк ECS пишет в свой непересекающийся диапазон scratch-арены, затем диапазоны
// сливаются в порядке чанков — порядок детерминирован, стабильность сортировки сохраняется.
private SpriteInstance[] _scratchInstances = [];
private ulong[] _scratchKeys = [];
private int[] _chunkOffsets = [];
private int[] _chunkVisible = [];
private int[] _chunkCulled = [];
private int _chunkCount;
/// <summary>Total culled count reported by chunk writers in the last <see cref="CommitChunks"/>.</summary>
public int LastChunkCulled { get; private set; }
/// <summary>
/// Prepares the scratch arena for chunked submission. <paramref name="chunkLengths"/> are
/// the entity counts of each ECS chunk, in iteration order.
/// </summary>
public void BeginChunks(ReadOnlySpan<int> chunkLengths)
{
_chunkCount = chunkLengths.Length;
if (_chunkOffsets.Length < _chunkCount)
{
Array.Resize(ref _chunkOffsets, _chunkCount);
Array.Resize(ref _chunkVisible, _chunkCount);
Array.Resize(ref _chunkCulled, _chunkCount);
}
var total = 0;
for (var i = 0; i < _chunkCount; i++)
{
_chunkOffsets[i] = total;
total += chunkLengths[i];
}
if (_scratchInstances.Length < total)
{
_scratchInstances = new SpriteInstance[total];
_scratchKeys = new ulong[total];
}
}
/// <summary>Returns the writer for chunk <paramref name="chunkIndex"/>. Each chunk is written by one thread.</summary>
public SpriteChunkWriter GetChunkWriter(int chunkIndex) =>
new(_scratchInstances, _scratchKeys, _chunkOffsets[chunkIndex]);
/// <summary>Records the writer's results. Called by the same thread that filled the writer.</summary>
public void EndChunk(int chunkIndex, in SpriteChunkWriter writer)
{
_chunkVisible[chunkIndex] = writer.Count;
_chunkCulled[chunkIndex] = writer.Culled;
}
/// <summary>
/// Merges all chunk ranges into the main arrays in chunk order (deterministic, keeps
/// sort stability) and returns the number of accepted sprites.
/// </summary>
public int CommitChunks()
{
var visible = 0;
var culled = 0;
for (var i = 0; i < _chunkCount; i++)
{
visible += _chunkVisible[i];
culled += _chunkCulled[i];
}
while (_count + visible > _instances.Length)
{
Grow();
}
for (var i = 0; i < _chunkCount; i++)
{
var length = _chunkVisible[i];
if (length == 0)
{
continue;
}
Array.Copy(_scratchInstances, _chunkOffsets[i], _instances, _count, length);
Array.Copy(_scratchKeys, _chunkOffsets[i], _keys, _count, length);
_count += length;
}
LastChunkCulled = culled;
_chunkCount = 0;
return visible;
}
private void Grow()
{
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);
}
}