Parallel rendering pipeline, ring vertex buffer, phase timings
Five optimizations measured on the 100k-entity stress scene (Release, vsync off): 103 FPS baseline -> 297 FPS. - Sprite submission and vertex building run on all cores above Renderer2DOptions.ParallelThreshold (default 8192). Work is sliced into 4096-entity segments: a Friflo chunk holds a whole archetype, so per-chunk parallelism degenerates to one thread. Segments merge in deterministic order, preserving radix sort stability. - Vertex buffer is ring-written with SetDataOptions.NoOverwrite (GPU buffer 2x frame size); Discard only on wrap-around. - Texture2DRegion precomputes UVs - four float divisions per sprite per frame removed. - Renderer2D exposes per-phase timings (submit/sort/build/upload/draw), shown in the sample HUD - all further optimization is data-driven. - Sample BounceSystem parallelized the same segmented way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
af319d1276
commit
e06f24a319
@@ -2,6 +2,44 @@ 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
|
||||
{
|
||||
@@ -147,9 +185,102 @@ public sealed class SpriteBatcher
|
||||
/// <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;
|
||||
|
||||
Reference in New Issue
Block a user