From 856d67b6cf5bcfce5cee955cec0359b2ba854eaf Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Mon, 12 Sep 2022 22:57:01 +0200 Subject: [PATCH] Second pass at StaticSpriteBatch optimizations --- MLEM/Graphics/StaticSpriteBatch.cs | 108 +++++++++++++++-------------- Sandbox/GameImpl.cs | 4 +- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/MLEM/Graphics/StaticSpriteBatch.cs b/MLEM/Graphics/StaticSpriteBatch.cs index 56ae232..daf437b 100644 --- a/MLEM/Graphics/StaticSpriteBatch.cs +++ b/MLEM/Graphics/StaticSpriteBatch.cs @@ -10,7 +10,7 @@ using System.IO; namespace MLEM.Graphics { /// - /// A static sprite batch is a variation of that keeps all batched items in a , allowing for them to be drawn multiple times. + /// A static sprite batch is a highly optimized variation of that keeps all batched items in a , allowing for them to be drawn multiple times. /// To add items to a static sprite batch, use to begin batching, to clear currently batched items, Add and its various overloads to add batch items, to remove them again, and to end batching. /// To draw the batched items, call . /// @@ -23,7 +23,7 @@ namespace MLEM.Graphics { /// /// The amount of vertices that are currently batched. /// - public int Vertices => this.currentItems.Count * 4; + public int Vertices => this.itemAmount * 4; /// /// The amount of vertex buffers that this static sprite batch has. /// To see the amount of buffers that are actually in use, see . @@ -43,14 +43,13 @@ namespace MLEM.Graphics { private readonly SpriteEffect spriteEffect; private readonly List vertexBuffers = new List(); private readonly List textures = new List(); - private readonly ISet currentItems = new HashSet(); - private readonly List sortedItems = new List(); + private readonly SortedList> items = new SortedList>(); private IndexBuffer indices; private bool batching; private bool batchChanged; private SpriteSortMode sortMode; - private Comparer comparer; + private int itemAmount; /// /// Creates a new static sprite batch with the given @@ -74,11 +73,16 @@ namespace MLEM.Graphics { if (sortMode == SpriteSortMode.Immediate) throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching"); - // update comparer and re-sort our list if our sort mode changed + // if the sort mode changed (which should be very rare in practice), we have to re-sort our list if (this.sortMode != sortMode) { this.sortMode = sortMode; - this.comparer = Comparer.Create(StaticSpriteBatch.CreateComparison(sortMode)); - this.sortedItems.Sort(this.comparer); + if (this.items.Count > 0) { + var tempItems = this.items.Values.SelectMany(s => s).ToArray(); + this.items.Clear(); + foreach (var item in tempItems) + this.AddItemToSet(item); + this.batchChanged = true; + } } this.batching = true; @@ -104,29 +108,25 @@ namespace MLEM.Graphics { // fill vertex buffers var dataIndex = 0; Texture2D texture = null; - // we use RemoveAll to iterate safely while being able to remove - this.sortedItems.RemoveAll(i => { - // we remove items in here to avoid having to search for them when removing them in Remove - if (i.Removed) - return true; - - // if the texture changes, we also have to start a new buffer! - if (dataIndex > 0 && (i.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) { - this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); - dataIndex = 0; + foreach (var itemSet in this.items.Values) { + foreach (var item in itemSet) { + // if the texture changes, we also have to start a new buffer! + if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) { + this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); + dataIndex = 0; + } + StaticSpriteBatch.Data[dataIndex++] = item.TopLeft; + StaticSpriteBatch.Data[dataIndex++] = item.TopRight; + StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft; + StaticSpriteBatch.Data[dataIndex++] = item.BottomRight; + texture = item.Texture; } - StaticSpriteBatch.Data[dataIndex++] = i.TopLeft; - StaticSpriteBatch.Data[dataIndex++] = i.TopRight; - StaticSpriteBatch.Data[dataIndex++] = i.BottomLeft; - StaticSpriteBatch.Data[dataIndex++] = i.BottomRight; - texture = i.Texture; - return false; - }); + } if (dataIndex > 0) this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); // ensure we have enough indices - var maxItems = Math.Min(this.currentItems.Count, StaticSpriteBatch.MaxBatchItems); + var maxItems = Math.Min(this.itemAmount, StaticSpriteBatch.MaxBatchItems); // each item has 2 triangles which each have 3 indices if (this.indices == null || this.indices.IndexCount < 6 * maxItems) { var newIndices = new short[6 * maxItems]; @@ -150,10 +150,6 @@ namespace MLEM.Graphics { this.indices = new IndexBuffer(this.graphicsDevice, IndexElementSize.SixteenBits, newIndices.Length, BufferUsage.WriteOnly); this.indices.SetData(newIndices); } - - // sanity check, this shouldn't be able to happen - if (this.currentItems.Count != this.sortedItems.Count) - throw new InvalidOperationException("Current and sorted items have mismatched sizes"); } /// @@ -184,7 +180,7 @@ namespace MLEM.Graphics { for (var i = 0; i < this.FilledBuffers; i++) { var buffer = this.vertexBuffers[i]; var texture = this.textures[i]; - var verts = Math.Min(this.currentItems.Count * 4 - totalIndex, buffer.VertexCount); + var verts = Math.Min(this.itemAmount * 4 - totalIndex, buffer.VertexCount); this.graphicsDevice.SetVertexBuffer(buffer); if (effect != null) { @@ -368,10 +364,12 @@ namespace MLEM.Graphics { public bool Remove(Item item) { if (!this.batching) throw new InvalidOperationException("Not batching"); - if (!item.Removed && this.currentItems.Remove(item)) { + var key = item.GetSortKey(this.sortMode); + if (this.items.TryGetValue(key, out var itemSet) && itemSet.Remove(item)) { + if (itemSet.Count <= 0) + this.items.Remove(key); + this.itemAmount--; this.batchChanged = true; - // this item will only actually get removed from sortedItems in EndBatch for performance - item.Removed = true; return true; } return false; @@ -385,10 +383,10 @@ namespace MLEM.Graphics { public void ClearBatch() { if (!this.batching) throw new InvalidOperationException("Not batching"); - this.currentItems.Clear(); - this.sortedItems.Clear(); + this.items.Clear(); this.textures.Clear(); this.FilledBuffers = 0; + this.itemAmount = 0; this.batchChanged = true; } @@ -441,10 +439,8 @@ namespace MLEM.Graphics { if (!this.batching) throw new InvalidOperationException("Not batching"); var item = new Item(texture, depth, tl, tr, bl, br); - this.currentItems.Add(item); - // add item in a sorted fashion - var pos = this.sortedItems.BinarySearch(item, this.comparer); - this.sortedItems.Insert(pos >= 0 ? pos : ~pos, item); + this.AddItemToSet(item); + this.itemAmount++; this.batchChanged = true; return item; } @@ -464,17 +460,13 @@ namespace MLEM.Graphics { #endif } - private static Comparison CreateComparison(SpriteSortMode sortMode) { - switch (sortMode) { - case SpriteSortMode.Texture: - return (i1, i2) => i1.TextureHash.CompareTo(i2.TextureHash); - case SpriteSortMode.BackToFront: - return (i1, i2) => i2.Depth.CompareTo(i1.Depth); - case SpriteSortMode.FrontToBack: - return (i1, i2) => i1.Depth.CompareTo(i2.Depth); - default: - return (i1, i2) => 0; + private void AddItemToSet(Item item) { + var sortKey = item.GetSortKey(this.sortMode); + if (!this.items.TryGetValue(sortKey, out var itemSet)) { + itemSet = new HashSet(); + this.items.Add(sortKey, itemSet); } + itemSet.Add(item); } /// @@ -484,17 +476,14 @@ namespace MLEM.Graphics { public class Item { internal readonly Texture2D Texture; - internal readonly int TextureHash; internal readonly float Depth; internal readonly VertexPositionColorTexture TopLeft; internal readonly VertexPositionColorTexture TopRight; internal readonly VertexPositionColorTexture BottomLeft; internal readonly VertexPositionColorTexture BottomRight; - internal bool Removed; internal Item(Texture2D texture, float depth, VertexPositionColorTexture topLeft, VertexPositionColorTexture topRight, VertexPositionColorTexture bottomLeft, VertexPositionColorTexture bottomRight) { this.Texture = texture; - this.TextureHash = texture.GetHashCode(); this.Depth = depth; this.TopLeft = topLeft; this.TopRight = topRight; @@ -502,6 +491,19 @@ namespace MLEM.Graphics { this.BottomRight = bottomRight; } + internal float GetSortKey(SpriteSortMode sortMode) { + switch (sortMode) { + case SpriteSortMode.Texture: + return this.Texture.GetHashCode(); + case SpriteSortMode.BackToFront: + return -this.Depth; + case SpriteSortMode.FrontToBack: + return this.Depth; + default: + return 0; + } + } + } #if FNA diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index c062eb0..f8e79c3 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -355,13 +355,15 @@ public class GameImpl : MlemGame { this.UiSystem.Add("WidthTest", widthPanel); var batch = new StaticSpriteBatch(this.GraphicsDevice); - batch.BeginBatch(SpriteSortMode.FrontToBack); + batch.BeginBatch(SpriteSortMode.Deferred); var depth = 0F; var items = new List(); foreach (var r in atlas.Regions) items.Add(batch.Add(r, new Vector2(50 + r.GetHashCode() % 200, 50), ColorHelper.FromHexRgb(r.GetHashCode()), 0, Vector2.Zero, 1, SpriteEffects.None, depth += 0.0001F)); batch.Remove(items[5]); batch.EndBatch(); + batch.BeginBatch(SpriteSortMode.BackToFront); + batch.EndBatch(); this.OnDraw += (_, _) => batch.Draw(null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(3)); }