using System; using System.Runtime.CompilerServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace MonoGame.Extended.Graphics { /// /// Minimizes draw calls to a by sorting them and attempting to merge them together /// before submitting them. /// /// The type of the information for a draw call. /// public abstract class Batcher : IDisposable where TDrawCallInfo : struct, IBatchDrawCallInfo, IComparable { 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; /// /// The array of structs currently enqueued. /// protected TDrawCallInfo[] DrawCalls; /// /// The number of structs currently enqueued. /// protected int EnqueuedDrawCallCount; /// /// Gets the associated with this . /// /// /// The associated with this . /// public GraphicsDevice GraphicsDevice { get; } /// /// Gets a value indicating whether batching is currently in progress by being within a and /// pair block of code. /// /// /// true if batching has begun; otherwise, false. /// public bool HasBegun { get; internal set; } /// /// Initializes a new instance of the class. /// /// The graphics device. /// The default effect. /// /// The maximum number of structs that can be enqueued before a /// /// is required. The default value is 2048. /// /// /// is /// null. /// /// /// is less than or equal /// 0. /// 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]; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// /// true to release both managed and unmanaged resources; false to release only /// unmanaged resources. /// 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."); } /// /// Begins the batch operation using an optional , , /// , , , world-to-view /// , or view-to-projection . /// /// /// /// The default objects for , , /// , and are /// , , /// and respectively. /// Passing /// null for any of the previously mentioned parameters result in using their default object. /// /// /// The to use for the , pair. /// /// The texture to use for the and /// pair. /// /// /// The to use for the and /// pair. /// /// /// The to use for the and /// pair. /// /// The to use for the and pair. /// /// The world-to-view transformation matrix to use for the and /// pair. /// /// /// The view-to-projection transformation matrix to use for the and /// pair. /// /// /// cannot be invoked again until has been invoked. /// /// /// /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for /// drawing, call . /// /// 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); } /// /// Begins the batch operation using an optional , , /// , , , world-to-view /// , or view-to-projection . /// /// /// /// The default objects for , , /// , and are /// , , /// and respectively. /// Passing /// null for any of the previously mentioned parameters result in using their default object. /// /// /// The to use for the , pair. /// /// The texture to use for the and /// pair. /// /// /// The to use for the and /// pair. /// /// /// The to use for the and /// pair. /// /// The to use for the and pair. /// /// The world-to-view transformation matrix to use for the and /// pair. /// /// /// The view-to-projection transformation matrix to use for the and /// pair. /// /// /// cannot be invoked again until has been invoked. /// /// /// /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for /// drawing, call . /// /// 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; } /// /// Flushes the batched geometry to the and restores it's state to how it was before /// was called. /// /// /// cannot be invoked until has been invoked. /// /// /// /// This method must be called after all enqueuing of draw calls. /// /// public void End() { EnsureHasBegun(); Flush(); HasBegun = false; } /// /// Sorts then submits the (sorted) enqueued draw calls to the for /// rendering without ending the and pair. /// protected void Flush() { if (EnqueuedDrawCallCount == 0) return; SortDrawCallsAndBindBuffers(); ApplyStates(); SubmitDrawCalls(); RestoreStates(); } /// /// Sorts the enqueued draw calls and binds any used or to the . /// 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; } /// /// Enqueues draw call information. /// /// The draw call information. /// /// /// If possible, the is merged with the last enqueued draw call information instead of /// being /// enqueued. /// /// /// If the enqueue buffer is full, a is invoked and then afterwards /// is enqueued. /// /// 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. */ /// /// Submits a draw operation to the using the specified . /// /// The draw call information. [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; } } }