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); } }