1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-22 12:58:33 +01:00

First pass at drastically improving StaticSpriteBatch batching performance

This commit is contained in:
Ell 2022-09-12 21:51:21 +02:00
parent d6e7c1086d
commit 742bc52437
3 changed files with 78 additions and 37 deletions

View file

@ -19,6 +19,7 @@ Improvements
- Improved EnumHelper.GetValues signature to return an array - Improved EnumHelper.GetValues signature to return an array
- Allow using external gesture handling alongside InputHandler through ExternalGestureHandling - Allow using external gesture handling alongside InputHandler through ExternalGestureHandling
- Discard old data when updating a StaticSpriteBatch - Discard old data when updating a StaticSpriteBatch
- Drastically improved StaticSpriteBatch batching performance
Fixes Fixes
- Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text - Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text

View file

@ -23,7 +23,7 @@ namespace MLEM.Graphics {
/// <summary> /// <summary>
/// The amount of vertices that are currently batched. /// The amount of vertices that are currently batched.
/// </summary> /// </summary>
public int Vertices => this.items.Count * 4; public int Vertices => this.currentItems.Count * 4;
/// <summary> /// <summary>
/// The amount of vertex buffers that this static sprite batch has. /// The amount of vertex buffers that this static sprite batch has.
/// To see the amount of buffers that are actually in use, see <see cref="FilledBuffers"/>. /// To see the amount of buffers that are actually in use, see <see cref="FilledBuffers"/>.
@ -41,13 +41,16 @@ namespace MLEM.Graphics {
private readonly GraphicsDevice graphicsDevice; private readonly GraphicsDevice graphicsDevice;
private readonly SpriteEffect spriteEffect; private readonly SpriteEffect spriteEffect;
private readonly List<DynamicVertexBuffer> vertexBuffers = new List<DynamicVertexBuffer>(); private readonly List<DynamicVertexBuffer> vertexBuffers = new List<DynamicVertexBuffer>();
private readonly List<Texture2D> textures = new List<Texture2D>(); private readonly List<Texture2D> textures = new List<Texture2D>();
private readonly ISet<Item> items = new HashSet<Item>(); private readonly ISet<Item> currentItems = new HashSet<Item>();
private readonly List<Item> sortedItems = new List<Item>();
private IndexBuffer indices; private IndexBuffer indices;
private bool batching; private bool batching;
private bool batchChanged; private bool batchChanged;
private SpriteSortMode sortMode;
private Comparer<Item> comparer;
/// <summary> /// <summary>
/// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/> /// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/>
@ -62,10 +65,22 @@ namespace MLEM.Graphics {
/// Begins batching. /// Begins batching.
/// Call this method before calling <c>Add</c> or any of its overloads. /// Call this method before calling <c>Add</c> or any of its overloads.
/// </summary> /// </summary>
/// <param name="sortMode">The drawing order for sprite drawing. <see cref="SpriteSortMode.Texture" /> by default, since it is the best in terms of rendering performance. Note that <see cref="SpriteSortMode.Immediate"/> is not supported.</param>
/// <exception cref="InvalidOperationException">Thrown if this batch is currently batching already</exception> /// <exception cref="InvalidOperationException">Thrown if this batch is currently batching already</exception>
public void BeginBatch() { /// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception>
public void BeginBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
if (this.batching) if (this.batching)
throw new InvalidOperationException("Already 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<Item>.Create(StaticSpriteBatch.CreateComparison(sortMode));
this.sortedItems.Sort(this.comparer);
}
this.batching = true; this.batching = true;
} }
@ -73,14 +88,10 @@ namespace MLEM.Graphics {
/// Ends batching. /// Ends batching.
/// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items. /// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items.
/// </summary> /// </summary>
/// <param name="sortMode">The drawing order for sprite drawing. <see cref="SpriteSortMode.Texture" /> by default, since it is the best in terms of rendering performance. Note that <see cref="SpriteSortMode.Immediate"/> is not supported.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called before <see cref="BeginBatch"/> was called.</exception> /// <exception cref="InvalidOperationException">Thrown if this method is called before <see cref="BeginBatch"/> was called.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception> public void EndBatch() {
public void EndBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
if (!this.batching) if (!this.batching)
throw new InvalidOperationException("Not 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; this.batching = false;
// if we didn't add or remove any batch items, we don't have to recalculate anything // 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.FilledBuffers = 0;
this.textures.Clear(); this.textures.Clear();
// order items according to the sort mode
IEnumerable<Item> 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 // fill vertex buffers
var dataIndex = 0; var dataIndex = 0;
Texture2D texture = null; 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 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); this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
dataIndex = 0; dataIndex = 0;
} }
StaticSpriteBatch.Data[dataIndex++] = item.TopLeft; StaticSpriteBatch.Data[dataIndex++] = i.TopLeft;
StaticSpriteBatch.Data[dataIndex++] = item.TopRight; StaticSpriteBatch.Data[dataIndex++] = i.TopRight;
StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft; StaticSpriteBatch.Data[dataIndex++] = i.BottomLeft;
StaticSpriteBatch.Data[dataIndex++] = item.BottomRight; StaticSpriteBatch.Data[dataIndex++] = i.BottomRight;
texture = item.Texture; texture = i.Texture;
} return false;
});
if (dataIndex > 0) if (dataIndex > 0)
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
// ensure we have enough indices // 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 // each item has 2 triangles which each have 3 indices
if (this.indices == null || this.indices.IndexCount < 6 * maxItems) { if (this.indices == null || this.indices.IndexCount < 6 * maxItems) {
var newIndices = new short[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 = new IndexBuffer(this.graphicsDevice, IndexElementSize.SixteenBits, newIndices.Length, BufferUsage.WriteOnly);
this.indices.SetData(newIndices); 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");
} }
/// <summary> /// <summary>
@ -178,7 +184,7 @@ namespace MLEM.Graphics {
for (var i = 0; i < this.FilledBuffers; i++) { for (var i = 0; i < this.FilledBuffers; i++) {
var buffer = this.vertexBuffers[i]; var buffer = this.vertexBuffers[i];
var texture = this.textures[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); this.graphicsDevice.SetVertexBuffer(buffer);
if (effect != null) { if (effect != null) {
@ -362,8 +368,10 @@ namespace MLEM.Graphics {
public bool Remove(Item item) { public bool Remove(Item item) {
if (!this.batching) if (!this.batching)
throw new InvalidOperationException("Not batching"); throw new InvalidOperationException("Not batching");
if (this.items.Remove(item)) { if (!item.Removed && this.currentItems.Remove(item)) {
this.batchChanged = true; this.batchChanged = true;
// this item will only actually get removed from sortedItems in EndBatch for performance
item.Removed = true;
return true; return true;
} }
return false; return false;
@ -377,7 +385,8 @@ namespace MLEM.Graphics {
public void ClearBatch() { public void ClearBatch() {
if (!this.batching) if (!this.batching)
throw new InvalidOperationException("Not batching"); throw new InvalidOperationException("Not batching");
this.items.Clear(); this.currentItems.Clear();
this.sortedItems.Clear();
this.textures.Clear(); this.textures.Clear();
this.FilledBuffers = 0; this.FilledBuffers = 0;
this.batchChanged = true; this.batchChanged = true;
@ -432,7 +441,10 @@ namespace MLEM.Graphics {
if (!this.batching) if (!this.batching)
throw new InvalidOperationException("Not batching"); throw new InvalidOperationException("Not batching");
var item = new Item(texture, depth, tl, tr, bl, br); 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; this.batchChanged = true;
return item; return item;
} }
@ -452,6 +464,19 @@ namespace MLEM.Graphics {
#endif #endif
} }
private static Comparison<Item> 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;
}
}
/// <summary> /// <summary>
/// A struct that represents an item added to a <see cref="StaticSpriteBatch"/> using <c>Add</c> or any of its overloads. /// A struct that represents an item added to a <see cref="StaticSpriteBatch"/> using <c>Add</c> or any of its overloads.
/// An item returned after adding can be removed using <see cref="Remove"/>. /// An item returned after adding can be removed using <see cref="Remove"/>.
@ -459,14 +484,17 @@ namespace MLEM.Graphics {
public class Item { public class Item {
internal readonly Texture2D Texture; internal readonly Texture2D Texture;
internal readonly int TextureHash;
internal readonly float Depth; internal readonly float Depth;
internal readonly VertexPositionColorTexture TopLeft; internal readonly VertexPositionColorTexture TopLeft;
internal readonly VertexPositionColorTexture TopRight; internal readonly VertexPositionColorTexture TopRight;
internal readonly VertexPositionColorTexture BottomLeft; internal readonly VertexPositionColorTexture BottomLeft;
internal readonly VertexPositionColorTexture BottomRight; internal readonly VertexPositionColorTexture BottomRight;
internal bool Removed;
internal Item(Texture2D texture, float depth, VertexPositionColorTexture topLeft, VertexPositionColorTexture topRight, VertexPositionColorTexture bottomLeft, VertexPositionColorTexture bottomRight) { internal Item(Texture2D texture, float depth, VertexPositionColorTexture topLeft, VertexPositionColorTexture topRight, VertexPositionColorTexture bottomLeft, VertexPositionColorTexture bottomRight) {
this.Texture = texture; this.Texture = texture;
this.TextureHash = texture.GetHashCode();
this.Depth = depth; this.Depth = depth;
this.TopLeft = topLeft; this.TopLeft = topLeft;
this.TopRight = topRight; this.TopRight = topRight;

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FontStashSharp; using FontStashSharp;
@ -13,6 +14,7 @@ using MLEM.Extensions;
using MLEM.Font; using MLEM.Font;
using MLEM.Formatting; using MLEM.Formatting;
using MLEM.Formatting.Codes; using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Input; using MLEM.Input;
using MLEM.Misc; using MLEM.Misc;
using MLEM.Startup; using MLEM.Startup;
@ -351,6 +353,16 @@ public class GameImpl : MlemGame {
}); });
} }
this.UiSystem.Add("WidthTest", widthPanel); this.UiSystem.Add("WidthTest", widthPanel);
var batch = new StaticSpriteBatch(this.GraphicsDevice);
batch.BeginBatch(SpriteSortMode.FrontToBack);
var depth = 0F;
var items = new List<StaticSpriteBatch.Item>();
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) { protected override void DoUpdate(GameTime gameTime) {