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;
}
}
}