diff options
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs')
-rw-r--r-- | Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs new file mode 100644 index 0000000..3cb5704 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs @@ -0,0 +1,387 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics +{ + /// <summary> + /// Minimizes draw calls to a <see cref="GraphicsDevice" /> by sorting them and attempting to merge them together + /// before submitting them. + /// </summary> + /// <typeparam name="TDrawCallInfo">The type of the information for a draw call.</typeparam> + /// <seealso cref="IDisposable" /> + public abstract class Batcher<TDrawCallInfo> : IDisposable + where TDrawCallInfo : struct, IBatchDrawCallInfo<TDrawCallInfo>, IComparable<TDrawCallInfo> + { + internal const int DefaultBatchMaximumDrawCallsCount = 2048; + private BlendState _blendState; + private SamplerState _samplerState; + private DepthStencilState _depthStencilState; + private RasterizerState _rasterizerState; + private readonly Effect _defaultEffect; + private Effect _currentEffect; + private Matrix? _viewMatrix; + private Matrix? _projectionMatrix; + + /// <summary> + /// The array of <see cref="TDrawCallInfo" /> structs currently enqueued. + /// </summary> + protected TDrawCallInfo[] DrawCalls; + + /// <summary> + /// The number of <see cref="TDrawCallInfo" /> structs currently enqueued. + /// </summary> + protected int EnqueuedDrawCallCount; + + /// <summary> + /// Gets the <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />. + /// </summary> + /// <value> + /// The <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />. + /// </value> + public GraphicsDevice GraphicsDevice { get; } + + /// <summary> + /// Gets a value indicating whether batching is currently in progress by being within a <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair block of code. + /// </summary> + /// <value> + /// <c>true</c> if batching has begun; otherwise, <c>false</c>. + /// </value> + public bool HasBegun { get; internal set; } + + /// <summary> + /// Initializes a new instance of the <see cref="Batcher{TDrawCallInfo}" /> class. + /// </summary> + /// <param name="graphicsDevice">The graphics device.</param> + /// <param name="defaultEffect">The default effect.</param> + /// <param name="maximumDrawCallsCount"> + /// The maximum number of <see cref="TDrawCallInfo" /> structs that can be enqueued before a + /// <see cref="Batcher{TDrawCallInfo}.Flush" /> + /// is required. The default value is <code>2048</code>. + /// </param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="graphicsDevice" /> is + /// null. + /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="maximumDrawCallsCount" /> is less than or equal + /// <code>0</code>. + /// </exception> + protected Batcher(GraphicsDevice graphicsDevice, Effect defaultEffect, + int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount) + { + if (graphicsDevice == null) + throw new ArgumentNullException(nameof(graphicsDevice)); + + if (maximumDrawCallsCount <= 0) + throw new ArgumentOutOfRangeException(nameof(maximumDrawCallsCount)); + + GraphicsDevice = graphicsDevice; + _defaultEffect = defaultEffect; + DrawCalls = new TDrawCallInfo[maximumDrawCallsCount]; + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="diposing"> + /// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only + /// unmanaged resources. + /// </param> + protected virtual void Dispose(bool diposing) + { + if (!diposing) + return; + _defaultEffect.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void EnsureHasBegun([CallerMemberName] string callerMemberName = null) + { + if (!HasBegun) + throw new InvalidOperationException( + $"The {nameof(Begin)} method must be called before the {callerMemberName} method can be called."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void EnsureHasNotBegun([CallerMemberName] string callerMemberName = null) + { + if (HasBegun) + throw new InvalidOperationException( + $"The {nameof(End)} method must be called before the {callerMemberName} method can be called."); + } + + /// <summary> + /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />, + /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view + /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />. + /// </summary> + /// <remarks> + /// <para> + /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />, + /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are + /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />, + /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively. + /// Passing + /// <code>null</code> for any of the previously mentioned parameters result in using their default object. + /// </para> + /// </remarks> + /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param> + /// <param name="samplerState"> + /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="depthStencilState"> + /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="rasterizerState"> + /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param> + /// <param name="viewMatrix"> + /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="projectionMatrix"> + /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <exception cref="InvalidOperationException"> + /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked. + /// </exception> + /// <remarks> + /// <para> + /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for + /// drawing, call <see cref="End" />. + /// </para> + /// </remarks> + public virtual void Begin(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, BlendState blendState = null, SamplerState samplerState = null, + DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null) + { + var viewMatrix1 = viewMatrix ?? Matrix.Identity; + var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 0, -1); + Begin(ref viewMatrix1, ref projectionMatrix1, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + /// <summary> + /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />, + /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view + /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />. + /// </summary> + /// <remarks> + /// <para> + /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />, + /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are + /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />, + /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively. + /// Passing + /// <code>null</code> for any of the previously mentioned parameters result in using their default object. + /// </para> + /// </remarks> + /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param> + /// <param name="samplerState"> + /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="depthStencilState"> + /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="rasterizerState"> + /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param> + /// <param name="viewMatrix"> + /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="projectionMatrix"> + /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <exception cref="InvalidOperationException"> + /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked. + /// </exception> + /// <remarks> + /// <para> + /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for + /// drawing, call <see cref="End" />. + /// </para> + /// </remarks> + public virtual void Begin(ref Matrix viewMatrix, ref Matrix projectionMatrix, BlendState blendState = null, SamplerState samplerState = null, + DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null) + { + EnsureHasNotBegun(); + HasBegun = true; + + // Store the states to be applied on End() + // This ensures that two or more batchers will not affect each other + _blendState = blendState ?? BlendState.AlphaBlend; + _samplerState = samplerState ?? SamplerState.PointClamp; + _depthStencilState = depthStencilState ?? DepthStencilState.None; + _rasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise; + _currentEffect = effect ?? _defaultEffect; + _projectionMatrix = projectionMatrix; + _viewMatrix = viewMatrix; + } + + /// <summary> + /// Flushes the batched geometry to the <see cref="GraphicsDevice" /> and restores it's state to how it was before + /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> was called. + /// </summary> + /// <exception cref="InvalidOperationException"> + /// <see cref="End" /> cannot be invoked until <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> has been invoked. + /// </exception> + /// <remarks> + /// <para> + /// This method must be called after all enqueuing of draw calls. + /// </para> + /// </remarks> + public void End() + { + EnsureHasBegun(); + Flush(); + HasBegun = false; + } + + /// <summary> + /// Sorts then submits the (sorted) enqueued draw calls to the <see cref="GraphicsDevice" /> for + /// rendering without ending the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair. + /// </summary> + protected void Flush() + { + if (EnqueuedDrawCallCount == 0) + return; + SortDrawCallsAndBindBuffers(); + ApplyStates(); + SubmitDrawCalls(); + RestoreStates(); + } + + /// <summary> + /// Sorts the enqueued draw calls and binds any used <see cref="VertexBuffer" /> or <see cref="IndexBuffer" /> to the <see cref="GraphicsDevice" />. + /// </summary> + protected abstract void SortDrawCallsAndBindBuffers(); + + private void ApplyStates() + { + var oldBlendState = GraphicsDevice.BlendState; + var oldSamplerState = GraphicsDevice.SamplerStates[0]; + var oldDepthStencilState = GraphicsDevice.DepthStencilState; + var oldRasterizerState = GraphicsDevice.RasterizerState; + + GraphicsDevice.BlendState = _blendState; + + GraphicsDevice.SamplerStates[0] = _samplerState; + GraphicsDevice.DepthStencilState = _depthStencilState; + GraphicsDevice.RasterizerState = _rasterizerState; + + _blendState = oldBlendState; + _samplerState = oldSamplerState; + _depthStencilState = oldDepthStencilState; + _rasterizerState = oldRasterizerState; + + var viewMatrix = _viewMatrix ?? Matrix.Identity; + var projectionMatrix = _projectionMatrix ?? + Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, + GraphicsDevice.Viewport.Height, 0, 0f, -1f); + + var matrixChainEffect = _currentEffect as IMatrixChainEffect; + if (matrixChainEffect != null) + { + matrixChainEffect.World = Matrix.Identity; + matrixChainEffect.SetView(ref viewMatrix); + matrixChainEffect.SetProjection(ref projectionMatrix); + } + else + { + var effectMatrices = _currentEffect as IEffectMatrices; + if (effectMatrices == null) + return; + effectMatrices.World = Matrix.Identity; + effectMatrices.View = viewMatrix; + effectMatrices.Projection = projectionMatrix; + } + } + + private void RestoreStates() + { + GraphicsDevice.BlendState = _blendState; + GraphicsDevice.SamplerStates[0] = _samplerState; + GraphicsDevice.DepthStencilState = _depthStencilState; + GraphicsDevice.RasterizerState = _rasterizerState; + } + + /// <summary> + /// Enqueues draw call information. + /// </summary> + /// <param name="drawCall">The draw call information.</param> + /// <remarks> + /// <para> + /// If possible, the <paramref name="drawCall" /> is merged with the last enqueued draw call information instead of + /// being + /// enqueued. + /// </para> + /// <para> + /// If the enqueue buffer is full, a <see cref="Flush" /> is invoked and then afterwards + /// <paramref name="drawCall" /> is enqueued. + /// </para> + /// </remarks> + protected void Enqueue(ref TDrawCallInfo drawCall) + { + if (EnqueuedDrawCallCount > 0 && drawCall.TryMerge(ref DrawCalls[EnqueuedDrawCallCount - 1])) + return; + if (EnqueuedDrawCallCount >= DrawCalls.Length) + Flush(); + DrawCalls[EnqueuedDrawCallCount++] = drawCall; + } + + /* It might be better to have derived classes just implement the for loop instead of having this virtual method call... + * However, if the derived class is only going to override this method once and the code is short, which should both be + * true, then maybe we can get away with this virtual method call by having it inlined. So tell the JIT or AOT compiler + * we would like it be so. This does NOT guarantee the compiler will respect our wishes. + */ + + /// <summary> + /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="TDrawCallInfo"/>. + /// </summary> + /// <param name="drawCall">The draw call information.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected abstract void InvokeDrawCall(ref TDrawCallInfo drawCall); + + private void SubmitDrawCalls() + { + if (EnqueuedDrawCallCount == 0) + return; + + for (var i = 0; i < EnqueuedDrawCallCount; i++) + { + DrawCalls[i].SetState(_currentEffect); + + foreach (var pass in _currentEffect.CurrentTechnique.Passes) + { + pass.Apply(); + InvokeDrawCall(ref DrawCalls[i]); + } + } + + Array.Clear(DrawCalls, 0, EnqueuedDrawCallCount); + EnqueuedDrawCallCount = 0; + } + } +} +
\ No newline at end of file |