using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Barotrauma { sealed class SpriteRecorder : ISpriteBatch, IDisposable { public readonly record struct Command( Texture2D Texture, VertexPositionColorTexture VertexBL, VertexPositionColorTexture VertexBR, VertexPositionColorTexture VertexTL, VertexPositionColorTexture VertexTR, float Depth, Vector2 Min, Vector2 Max, int Index) { public static Vector2 GetMinPosition(params VertexPositionColorTexture[] vertices) => new Vector2( MathUtils.Min(vertices.Select(v => v.Position.X).ToArray()), MathUtils.Min(vertices.Select(v => v.Position.Y).ToArray())); public static Vector2 GetMaxPosition(params VertexPositionColorTexture[] vertices) => new Vector2( MathUtils.Max(vertices.Select(v => v.Position.X).ToArray()), MathUtils.Max(vertices.Select(v => v.Position.Y).ToArray())); public static Command FromTransform( Texture2D texture, Vector2 pos, Rectangle srcRect, Color color, float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth, int index) { int srcRectLeft = srcRect.Left; int srcRectRight = srcRect.Right; int srcRectTop = srcRect.Top; int srcRectBottom = srcRect.Bottom; if (effects.HasFlag(SpriteEffects.FlipHorizontally)) { (srcRectRight, srcRectLeft) = (srcRectLeft, srcRectRight); } if (effects.HasFlag(SpriteEffects.FlipVertically)) { (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); } float sin = (float)Math.Sin(rotationRad); float cos = (float)Math.Cos(rotationRad); var size = srcRect.Size.ToVector2() * scale; Vector2 wAdd = new Vector2(size.X * cos, size.X * sin); Vector2 hAdd = new Vector2(-size.Y * sin, size.Y * cos); pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; var vertexTl = new VertexPositionColorTexture { Color = color, Position = new Vector3(pos.X, pos.Y, 0f), TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height) }; var vertexTr = new VertexPositionColorTexture { Color = color, Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f), TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height) }; var vertexBl = new VertexPositionColorTexture { Color = color, Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f), TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) }; var vertexBr = new VertexPositionColorTexture { Color = color, Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f), TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) }; var min = GetMinPosition( vertexTl, vertexTr, vertexBl, vertexBr); var max = GetMaxPosition( vertexTl, vertexTr, vertexBl, vertexBr); return new Command( texture, vertexBl, vertexBr, vertexTl, vertexTr, depth, min, max, index); } public bool Overlaps(Command other) { return Min.X <= other.Max.X && Max.X >= other.Min.X && Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; } } private struct RecordedBuffer { public readonly Texture2D Texture; public readonly VertexBuffer VertexBuffer; public readonly int PolyCount; public RecordedBuffer(List commandList, int startIndex, int count) { Texture = commandList[startIndex].Texture; VertexBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, count * 4, BufferUsage.WriteOnly); VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[count * 4]; for (int i = 0; i < count; i++) { vertices[(i * 4) + 0] = commandList[startIndex + i].VertexBL; vertices[(i * 4) + 1] = commandList[startIndex + i].VertexBR; vertices[(i * 4) + 2] = commandList[startIndex + i].VertexTL; vertices[(i * 4) + 3] = commandList[startIndex + i].VertexTR; } VertexBuffer.SetData(vertices); PolyCount = count * 2; } } public static BasicEffect BasicEffect = null; private readonly List recordedBuffers = new List(); private readonly List commandList = new List(); private SpriteSortMode currentSortMode; private IndexBuffer indexBuffer = null; private int maxSpriteCount = 0; public volatile bool ReadyToRender = false; private volatile bool isDisposed = false; public Vector2 Min { get; private set; } public Vector2 Max { get; private set; } public void Begin(SpriteSortMode sortMode) { ReadyToRender = false; currentSortMode = sortMode; } private void AppendCommand(Command command) { if (isDisposed) { return; } if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); commandList.Add(command); } public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) { if (isDisposed) { return; } var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotationRad, origin, scale, effects, depth, commandList.Count); AppendCommand(command); } public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null) { if (isDisposed) { return; } int iters = count ?? (vertices.Length / 4); for (int i=0;i { return c1.Depth < c2.Depth ? -1 : c1.Depth > c2.Depth ? 1 : c1.Index < c2.Index ? 1 : c1.Index > c2.Index ? -1 : 0; }); break; case SpriteSortMode.BackToFront: commandList.Sort((c1, c2) => { return c1.Depth < c2.Depth ? 1 : c1.Depth > c2.Depth ? -1 : c1.Index < c2.Index ? 1 : c1.Index > c2.Index ? -1 : 0; }); break; } //try to place commands of the same texture //contiguously for optimal buffer generation //while maintaining the same visual result for (int i = 1; i < commandList.Count; i++) { if (commandList[i].Texture != commandList[i - 1].Texture) { for (int j = i - 1; j >= 0; j--) { if (commandList[j].Texture == commandList[i].Texture) { //no commands between i and j overlap with //i, therefore we can safely sift i down to //make a contiguous block commandList.SiftElement(i, j + 1); break; } else if (commandList[j].Overlaps(commandList[i])) { //an overlapping command was found, therefore //attempting to sift this one down would change //the visual result break; } } } } if (isDisposed) { return; } //each contiguous block of commands of the same texture //requires a vertex buffer to be rendered CrossThread.RequestExecutionOnMainThread(() => { if (isDisposed) { return; } if (commandList.Count == 0) { return; } int startIndex = 0; for (int i = 1; i < commandList.Count; i++) { if (commandList[i].Texture != commandList[startIndex].Texture) { maxSpriteCount = Math.Max(maxSpriteCount, i - startIndex); recordedBuffers.Add(new RecordedBuffer(commandList, startIndex, i - startIndex)); startIndex = i; } } recordedBuffers.Add(new RecordedBuffer(commandList, startIndex, commandList.Count - startIndex)); maxSpriteCount = Math.Max(maxSpriteCount, commandList.Count - startIndex); }); commandList.Clear(); ReadyToRender = true; } public void Render(Camera cam) { if (!ReadyToRender) { return; } var gfxDevice = GameMain.Instance.GraphicsDevice; BasicEffect ??= new BasicEffect(gfxDevice); BasicEffect.Projection = Matrix.CreateOrthographicOffCenter(new Rectangle(0, 0, cam.Resolution.X, cam.Resolution.Y), -1f, 1f); BasicEffect.View = cam.Transform; BasicEffect.World = Matrix.Identity; BasicEffect.TextureEnabled = true; BasicEffect.VertexColorEnabled = true; BasicEffect.Alpha = 1f; int requiredIndexCount = maxSpriteCount * 6; if (requiredIndexCount > 0 && (indexBuffer == null || indexBuffer.IndexCount < requiredIndexCount)) { indexBuffer?.Dispose(); indexBuffer = new IndexBuffer(gfxDevice, IndexElementSize.SixteenBits, requiredIndexCount * 2, BufferUsage.WriteOnly); ushort[] indices = new ushort[requiredIndexCount * 2]; for (int i = 0; i < indices.Length; i += 6) { indices[i + 0] = (ushort)((i / 6) * 4 + 1); indices[i + 1] = (ushort)((i / 6) * 4 + 0); indices[i + 2] = (ushort)((i / 6) * 4 + 2); indices[i + 3] = (ushort)((i / 6) * 4 + 1); indices[i + 4] = (ushort)((i / 6) * 4 + 2); indices[i + 5] = (ushort)((i / 6) * 4 + 3); } indexBuffer.SetData(indices); } gfxDevice.Indices = indexBuffer; for (int i = 0; i < recordedBuffers.Count; i++) { gfxDevice.SetVertexBuffer(recordedBuffers[i].VertexBuffer); BasicEffect.Texture = recordedBuffers[i].Texture; BasicEffect.CurrentTechnique.Passes[0].Apply(); gfxDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, recordedBuffers[i].PolyCount); } } public void Dispose() { isDisposed = true; foreach (var buffer in recordedBuffers) { buffer.VertexBuffer.Dispose(); } recordedBuffers.Clear(); commandList.Clear(); indexBuffer?.Dispose(); indexBuffer = null; ReadyToRender = false; } } }