using Microsoft.Xna.Framework;
namespace MrGameEng.Graphics;
///
/// 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.
///
public struct SpriteChunkWriter
{
private readonly SpriteInstance[] _instances;
private readonly ulong[] _keys;
private readonly int _offset;
/// Sprites accepted into this chunk's range.
public int Count { get; private set; }
/// Sprites rejected by culling in this chunk.
public int Culled { get; private set; }
internal SpriteChunkWriter(SpriteInstance[] instances, ulong[] keys, int offset)
{
_instances = instances;
_keys = keys;
_offset = offset;
Count = 0;
Culled = 0;
}
/// Appends one accepted sprite.
public void Add(in SpriteInstance instance, ulong sortKey)
{
var index = _offset + Count;
_instances[index] = instance;
_keys[index] = sortKey;
Count++;
}
/// Counts one culled sprite.
public void AddCulled() => Culled++;
}
/// One sprite queued for rendering this frame.
public struct SpriteInstance
{
/// Texture region to draw. Never null for submitted instances.
public Texture2DRegion Region;
/// World-space (or screen-space) center of the quad.
public Vector2 Center;
/// Half extents after scaling, in pixels. May be negative for negative scale.
public Vector2 HalfSize;
/// Rotation in radians, clockwise.
public float Rotation;
/// Tint color.
public Color Color;
/// Mirroring flags.
public SpriteFlip Flip;
/// Render layer the instance belongs to.
public byte Layer;
}
///
/// CPU side of the renderer: collects 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).
///
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;
/// Creates a batcher with the given initial capacity.
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];
}
/// Number of sprites submitted this frame.
public int Count => _count;
/// Queues one sprite.
public void Submit(in SpriteInstance instance, ulong sortKey)
{
if (_count == _instances.Length)
{
Grow();
}
_instances[_count] = instance;
_keys[_count] = sortKey;
_count++;
}
///
/// Sorts all submitted sprites (stable: equal keys keep submission order) and returns
/// their indices in draw order. Valid until the next .
///
public ReadOnlySpan 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);
}
/// Returns the instance at (an index from ).
public ref readonly SpriteInstance this[int index] => ref _instances[index];
/// The sorted index array after (first entries are valid).
internal int[] SortedOrder => _order;
/// Resets the batcher for the next frame. Keeps allocated capacity.
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;
/// Total culled count reported by chunk writers in the last .
public int LastChunkCulled { get; private set; }
///
/// Prepares the scratch arena for chunked submission. are
/// the entity counts of each ECS chunk, in iteration order.
///
public void BeginChunks(ReadOnlySpan 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];
}
}
/// Returns the writer for chunk . Each chunk is written by one thread.
public SpriteChunkWriter GetChunkWriter(int chunkIndex) =>
new(_scratchInstances, _scratchKeys, _chunkOffsets[chunkIndex]);
/// Records the writer's results. Called by the same thread that filled the writer.
public void EndChunk(int chunkIndex, in SpriteChunkWriter writer)
{
_chunkVisible[chunkIndex] = writer.Count;
_chunkCulled[chunkIndex] = writer.Culled;
}
///
/// Merges all chunk ranges into the main arrays in chunk order (deterministic, keeps
/// sort stability) and returns the number of accepted sprites.
///
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);
}
}