diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6610c67..a31e600 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ Improvements
- Improved EnumHelper.GetValues signature to return an array
- Allow using external gesture handling alongside InputHandler through ExternalGestureHandling
- Discard old data when updating a StaticSpriteBatch
+- Drastically improved StaticSpriteBatch batching performance
Fixes
- Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text
diff --git a/MLEM/Graphics/StaticSpriteBatch.cs b/MLEM/Graphics/StaticSpriteBatch.cs
index 914c5de..56ae232 100644
--- a/MLEM/Graphics/StaticSpriteBatch.cs
+++ b/MLEM/Graphics/StaticSpriteBatch.cs
@@ -23,7 +23,7 @@ namespace MLEM.Graphics {
///
/// The amount of vertices that are currently batched.
///
- public int Vertices => this.items.Count * 4;
+ public int Vertices => this.currentItems.Count * 4;
///
/// The amount of vertex buffers that this static sprite batch has.
/// To see the amount of buffers that are actually in use, see .
@@ -41,13 +41,16 @@ namespace MLEM.Graphics {
private readonly GraphicsDevice graphicsDevice;
private readonly SpriteEffect spriteEffect;
-
private readonly List vertexBuffers = new List();
private readonly List textures = new List();
- private readonly ISet- items = new HashSet
- ();
+ private readonly ISet
- currentItems = new HashSet
- ();
+ private readonly List
- sortedItems = new List
- ();
+
private IndexBuffer indices;
private bool batching;
private bool batchChanged;
+ private SpriteSortMode sortMode;
+ private Comparer
- comparer;
///
/// Creates a new static sprite batch with the given
@@ -62,10 +65,22 @@ namespace MLEM.Graphics {
/// Begins batching.
/// Call this method before calling Add or any of its overloads.
///
+ /// The drawing order for sprite drawing. by default, since it is the best in terms of rendering performance. Note that is not supported.
/// Thrown if this batch is currently batching already
- public void BeginBatch() {
+ /// Thrown if the is , which is not supported.
+ public void BeginBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
if (this.batching)
throw new InvalidOperationException("Already batching");
+ 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 (this.sortMode != sortMode) {
+ this.sortMode = sortMode;
+ this.comparer = Comparer
- .Create(StaticSpriteBatch.CreateComparison(sortMode));
+ this.sortedItems.Sort(this.comparer);
+ }
+
this.batching = true;
}
@@ -73,14 +88,10 @@ namespace MLEM.Graphics {
/// Ends batching.
/// Call this method after calling Add or any of its overloads the desired number of times to add batched items.
///
- /// The drawing order for sprite drawing. by default, since it is the best in terms of rendering performance. Note that is not supported.
/// Thrown if this method is called before was called.
- /// Thrown if the is , which is not supported.
- public void EndBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
+ public void EndBatch() {
if (!this.batching)
throw new InvalidOperationException("Not batching");
- if (sortMode == SpriteSortMode.Immediate)
- throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching");
this.batching = false;
// if we didn't add or remove any batch items, we don't have to recalculate anything
@@ -90,41 +101,32 @@ namespace MLEM.Graphics {
this.FilledBuffers = 0;
this.textures.Clear();
- // order items according to the sort mode
- IEnumerable- ordered = this.items;
- switch (sortMode) {
- case SpriteSortMode.Texture:
- // SortingKey is internal, but this will do for batching the same texture together
- ordered = ordered.OrderBy(i => i.Texture.GetHashCode());
- break;
- case SpriteSortMode.BackToFront:
- ordered = ordered.OrderBy(i => -i.Depth);
- break;
- case SpriteSortMode.FrontToBack:
- ordered = ordered.OrderBy(i => i.Depth);
- break;
- }
-
// fill vertex buffers
var dataIndex = 0;
Texture2D texture = null;
- foreach (var item in ordered) {
+ // 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 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
+ if (dataIndex > 0 && (i.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.items.Count, StaticSpriteBatch.MaxBatchItems);
+ var maxItems = Math.Min(this.currentItems.Count, 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];
@@ -148,6 +150,10 @@ 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");
}
///
@@ -178,7 +184,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.items.Count * 4 - totalIndex, buffer.VertexCount);
+ var verts = Math.Min(this.currentItems.Count * 4 - totalIndex, buffer.VertexCount);
this.graphicsDevice.SetVertexBuffer(buffer);
if (effect != null) {
@@ -362,8 +368,10 @@ namespace MLEM.Graphics {
public bool Remove(Item item) {
if (!this.batching)
throw new InvalidOperationException("Not batching");
- if (this.items.Remove(item)) {
+ if (!item.Removed && this.currentItems.Remove(item)) {
this.batchChanged = true;
+ // this item will only actually get removed from sortedItems in EndBatch for performance
+ item.Removed = true;
return true;
}
return false;
@@ -377,7 +385,8 @@ namespace MLEM.Graphics {
public void ClearBatch() {
if (!this.batching)
throw new InvalidOperationException("Not batching");
- this.items.Clear();
+ this.currentItems.Clear();
+ this.sortedItems.Clear();
this.textures.Clear();
this.FilledBuffers = 0;
this.batchChanged = true;
@@ -432,7 +441,10 @@ namespace MLEM.Graphics {
if (!this.batching)
throw new InvalidOperationException("Not batching");
var item = new Item(texture, depth, tl, tr, bl, br);
- this.items.Add(item);
+ 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.batchChanged = true;
return item;
}
@@ -452,6 +464,19 @@ 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;
+ }
+ }
+
///
/// A struct that represents an item added to a using Add or any of its overloads.
/// An item returned after adding can be removed using .
@@ -459,14 +484,17 @@ 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;
diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs
index e66e532..c062eb0 100644
--- a/Sandbox/GameImpl.cs
+++ b/Sandbox/GameImpl.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using FontStashSharp;
@@ -13,6 +14,7 @@ using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
+using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Startup;
@@ -351,6 +353,16 @@ public class GameImpl : MlemGame {
});
}
this.UiSystem.Add("WidthTest", widthPanel);
+
+ var batch = new StaticSpriteBatch(this.GraphicsDevice);
+ batch.BeginBatch(SpriteSortMode.FrontToBack);
+ 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();
+ this.OnDraw += (_, _) => batch.Draw(null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(3));
}
protected override void DoUpdate(GameTime gameTime) {