diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 23c0d28e4aa..e6680a0ba22 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,7 @@ END TEMPLATE--> ### New features +* Add batched Box2 / Box2Rotated drawing methods to Clyde WorldHandle. * Completion filter now works by Contains instead of StartsWith ### Bugfixes diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs index 57b4c038eac..82efebf4bac 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs @@ -19,6 +19,8 @@ internal sealed class RenderHandle : IRenderHandle { private readonly Clyde _clyde; private readonly IEntityManager _entities; + private readonly ClydeTexture _whiteClydeTexture; + private readonly Box2 _whiteUv; public DrawingHandleScreen DrawingHandleScreen { get; } public DrawingHandleWorld DrawingHandleWorld { get; } @@ -29,6 +31,8 @@ public RenderHandle(Clyde clyde, IEntityManager entities) _entities = entities; var white = _clyde.GetStockTexture(ClydeStockTexture.White); + _whiteClydeTexture = ExtractTexture(white, null, out var whiteBounds); + _whiteUv = WorldTextureBoundsToUV(_whiteClydeTexture, whiteBounds); DrawingHandleScreen = new DrawingHandleScreenImpl(white, this); DrawingHandleWorld = new DrawingHandleWorldImpl(white, this); } @@ -93,6 +97,30 @@ public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl _clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr); } + public void DrawTextureWorldBatch(Texture texture, ReadOnlySpan rects, Color modulate) + { + var clydeTexture = ExtractTexture(texture, null, out var csr); + var sr = WorldTextureBoundsToUV(clydeTexture, csr); + _clyde.DrawTextureBatch(clydeTexture.TextureId, rects, modulate, in sr); + } + + public void DrawTextureWorldBatchUnmodulated(Texture texture, ReadOnlySpan rects) + { + var clydeTexture = ExtractTexture(texture, null, out var csr); + var sr = WorldTextureBoundsToUV(clydeTexture, csr); + _clyde.DrawTextureBatchUnmodulated(clydeTexture.TextureId, rects, in sr); + } + + public void DrawRectWorldBatch(ReadOnlySpan rects, Color modulate) + { + _clyde.DrawRectBatch(_whiteClydeTexture.TextureId, rects, modulate, in _whiteUv); + } + + public void DrawRectWorldBatchUnmodulated(ReadOnlySpan rects) + { + _clyde.DrawRectBatchUnmodulated(_whiteClydeTexture.TextureId, rects, in _whiteUv); + } + internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr) { var (w, h) = texture.Size; @@ -488,6 +516,16 @@ public override void DrawRect(Box2 rect, Color color, bool filled = true) } } + public override void DrawRects(ReadOnlySpan rects) + { + _renderHandle.DrawRectWorldBatch(rects, Modulate); + } + + public override void DrawRectsUnmodulated(ReadOnlySpan rects) + { + _renderHandle.DrawRectWorldBatchUnmodulated(rects); + } + public override void DrawRect(in Box2Rotated rect, Color color, bool filled = true) { if (filled) @@ -539,6 +577,18 @@ public override void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, quad.TopLeft, quad.TopRight, color, in subRegion); } + /// + public override void DrawTextureRects(Texture texture, ReadOnlySpan rects) + { + _renderHandle.DrawTextureWorldBatch(texture, rects, Modulate); + } + + /// + public override void DrawTextureRectsUnmodulated(Texture texture, ReadOnlySpan rects) + { + _renderHandle.DrawTextureWorldBatchUnmodulated(texture, rects); + } + public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture, ReadOnlySpan vertices) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs index f7d419161d9..7d6f67220c2 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs @@ -620,16 +620,191 @@ private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl // TODO: split batch if necessary. var vIdx = BatchVertexIndex; - BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate); - BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate); - BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate); - BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate); + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, modulate); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, modulate); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, modulate); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, modulate); BatchVertexIndex += 4; QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); _debugStats.LastClydeDrawCalls += 1; } + private void DrawTextureBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords) + { + DrawTextureBatch(texture, rects, modulate, in texCoords, true); + } + + private void DrawTextureBatchUnmodulated( + ClydeHandle texture, + ReadOnlySpan rects, + in Box2 texCoords) + { + DrawTextureBatch(texture, rects, Color.White, in texCoords, false); + } + + private void DrawTextureBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords, + bool applyModulation) + { + if (rects.Length == 0) + return; + + // Avoids calling EnsureBatchSpaceAvailable / EnsureBatchState for each individual quad. + var primitiveType = GetQuadBatchPrimitiveType(); + var indexCount = GetQuadBatchIndexCount(); + var rectIndex = 0; + + while (rectIndex < rects.Length) + { + var availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + EnsureBatchState(texture, true, primitiveType, _queuedShader); + + availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + var count = Math.Min(availableQuads, rects.Length - rectIndex); + + for (var i = 0; i < count; i++) + { + ref readonly var rect = ref rects[rectIndex + i]; + var color = rect.Modulate ?? Color.White; + + if (applyModulation) + color *= modulate; + + // Can probably SIMD this more somehow but future concern. + var quad = rect.Quad; + var transform = quad.Transform * _currentMatrixModel; + + var bl = Vector2.Transform(quad.Box.BottomLeft, transform); + var br = Vector2.Transform(quad.Box.BottomRight, transform); + var tr = Vector2.Transform(quad.Box.TopRight, transform); + var tl = tr + bl - br; + + var vIdx = BatchVertexIndex; + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, color); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, color); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, color); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, color); + BatchVertexIndex += 4; + QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); + } + + rectIndex += count; + _debugStats.LastClydeDrawCalls += count; + } + } + + private void DrawRectBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords) + { + DrawRectBatch(texture, rects, modulate, in texCoords, true); + } + + private void DrawRectBatchUnmodulated( + ClydeHandle texture, + ReadOnlySpan rects, + in Box2 texCoords) + { + DrawRectBatch(texture, rects, Color.White, in texCoords, false); + } + + private void DrawRectBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords, + bool applyModulation) + { + if (rects.Length == 0) + return; + + var primitiveType = GetQuadBatchPrimitiveType(); + var indexCount = GetQuadBatchIndexCount(); + var rectIndex = 0; + + while (rectIndex < rects.Length) + { + var availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + EnsureBatchState(texture, true, primitiveType, _queuedShader); + + availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + var count = Math.Min(availableQuads, rects.Length - rectIndex); + + for (var i = 0; i < count; i++) + { + ref readonly var rect = ref rects[rectIndex + i]; + var color = rect.Color; + + if (applyModulation) + color *= modulate; + + var box = rect.Rect; + + var bl = Vector2.Transform(box.BottomLeft, _currentMatrixModel); + var br = Vector2.Transform(box.BottomRight, _currentMatrixModel); + var tr = Vector2.Transform(box.TopRight, _currentMatrixModel); + var tl = tr + bl - br; + + var vIdx = BatchVertexIndex; + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, color); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, color); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, color); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, color); + BatchVertexIndex += 4; + QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); + } + + rectIndex += count; + _debugStats.LastClydeDrawCalls += count; + } + } + + private int GetAvailableBatchQuads(int indexCount) + { + // The vertex size is so comically high you're probably never hitting it. + var availableVertices = Math.Max(0, BatchVertexData.Length - BatchVertexIndex - 1); + var availableIndices = Math.Max(0, BatchIndexData.Length - BatchIndexIndex); + return Math.Min(availableVertices / 4, availableIndices / indexCount); + } + private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId, ReadOnlySpan indices, ReadOnlySpan vertices) { diff --git a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs index 9fba50a0506..af7d88d03da 100644 --- a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs +++ b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Robust.Shared.Graphics; using Robust.Shared.Maths; @@ -22,6 +23,19 @@ protected DrawingHandleWorld(Texture white) : base(white) /// Is it filled with color, or just the border lines? public abstract void DrawRect(Box2 rect, Color color, bool filled = true); + /// + /// Draws multiple filled, untextured colored rectangles to the world. All rectangles use the current transform. + /// + /// + /// This is the batched equivalent of repeated filled calls. + /// + public abstract void DrawRects(ReadOnlySpan rects); + + /// + /// Draws multiple filled, untextured colored rectangles without multiplying by the handle modulation color. + /// + public abstract void DrawRectsUnmodulated(ReadOnlySpan rects); + /// /// Draws an untextured colored rectangle to the world.The coordinate system is right handed. /// Make sure to set @@ -58,6 +72,16 @@ public abstract void DrawTextureRectRegion(Texture texture, Box2 quad, public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, Color? modulate = null, UIBox2? subRegion = null); + /// + /// Draws multiple rotated rectangles for the same texture. + /// + public abstract void DrawTextureRects(Texture texture, ReadOnlySpan rects); + + /// + /// Draws multiple rotated rectangles for the same texture without multiplying by the handle modulation color. + /// + public abstract void DrawTextureRectsUnmodulated(Texture texture, ReadOnlySpan rects); + private Box2 GetQuad(Texture texture, Vector2 position) { return Box2.FromDimensions(position, texture.Size / (float)Ppm); @@ -133,4 +157,14 @@ public void DrawTextureRect(Texture texture, in Box2Rotated quad, Color? modulat DrawTextureRectRegion(texture, in quad, modulate); } } + + /// + /// A rotated rectangle for batched world texture drawing. + /// + public readonly record struct WorldTextureRect(Box2Rotated Quad, Color? Modulate = null); + + /// + /// An axis-aligned rectangle for batched world rectangle drawing. + /// + public readonly record struct WorldRect(Box2 Rect, Color Color); }