Files
FlaxEngine/Source/Engine/Renderer/RenderList.h
T
mafiesto4 bc36168318 Optimize Animated Model rendering with hardware instancing
All models are using the same global buffer for skinned bones which allows to share shader binding for instancing.
Refactor draw call for batching skinned mesh draws.
Remove `SkinnedMeshDrawData` and merge it into `AnimatedModel` internals.
2026-06-15 17:59:41 +02:00

725 lines
25 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Memory/ArenaAllocation.h"
#include "Engine/Core/Math/Quaternion.h"
#include "Engine/Graphics/PostProcessSettings.h"
#include "Engine/Graphics/DynamicBuffer.h"
#include "Engine/Scripting/ScriptingObject.h"
#include "DrawCall.h"
#include "RenderListBuffer.h"
#include "RendererAllocation.h"
#include "RenderSetup.h"
enum class StaticFlags;
class RenderBuffers;
class PostProcessEffect;
class SceneRendering;
class LightWithShadow;
class IPostFxSettingsProvider;
class CubeTexture;
struct RenderContext;
struct RenderContextBatch;
struct Half4;
struct RenderLightData
{
Guid ID;
Float3 Position;
float MinRoughness;
Float3 Color;
float ShadowsStrength;
Float3 Direction;
float ShadowsFadeDistance;
float ShadowsNormalOffsetScale;
float ShadowsDepthBias;
float ShadowsSharpness;
float ShadowsDistance;
StaticFlags StaticFlags;
ShadowsCastingMode ShadowsMode;
float IndirectLightingIntensity;
uint8 HasShadow : 1;
uint8 CastVolumetricShadow : 1;
uint8 UseInverseSquaredFalloff : 1;
uint8 IsDirectionalLight : 1;
uint8 IsPointLight : 1;
uint8 IsSpotLight : 1;
uint8 IsSkyLight : 1;
float VolumetricScatteringIntensity;
float ContactShadowsLength;
float ScreenSize;
uint32 ShadowsBufferAddress;
float ShadowsUpdateRate;
float ShadowsUpdateRateAtDistance;
uint32 ShadowFrame;
int32 ShadowsResolution;
bool CanRenderShadow(const RenderView& view) const;
#if !BUILD_RELEASE
// Development-only, gets the actor that produced this light (from ID).
Light* GetActor() const;
#endif
};
struct RenderDirectionalLightData : RenderLightData
{
float Cascade1Spacing;
float Cascade2Spacing;
float Cascade3Spacing;
float Cascade4Spacing;
PartitionMode PartitionMode;
int32 CascadeCount;
float CascadeBlendSize;
float SourceAngle;
RenderDirectionalLightData()
{
Platform::MemoryClear(this, sizeof(RenderDirectionalLightData));
IsDirectionalLight = 1;
}
POD_COPYABLE(RenderDirectionalLightData);
void SetShaderData(ShaderLightData& data, bool useShadow) const;
};
struct RenderLocalLightData : RenderLightData
{
GPUTexture* IESTexture;
float Radius;
float SourceRadius;
bool CanRenderShadow(const RenderView& view) const;
};
struct RenderSpotLightData : RenderLocalLightData
{
Float3 UpVector;
float OuterConeAngle;
float CosOuterCone;
float InvCosConeDifference;
float FallOffExponent;
RenderSpotLightData()
{
Platform::MemoryClear(this, sizeof(RenderSpotLightData));
IsSpotLight = 1;
}
POD_COPYABLE(RenderSpotLightData);
void SetShaderData(ShaderLightData& data, bool useShadow) const;
};
struct RenderPointLightData : RenderLocalLightData
{
float FallOffExponent;
float SourceLength;
RenderPointLightData()
{
Platform::MemoryClear(this, sizeof(RenderPointLightData));
IsPointLight = 1;
}
POD_COPYABLE(RenderPointLightData);
void SetShaderData(ShaderLightData& data, bool useShadow) const;
};
struct RenderSkyLightData : RenderLightData
{
Float3 AdditiveColor;
float Radius;
CubeTexture* Image;
RenderSkyLightData()
{
Platform::MemoryClear(this, sizeof(RenderSkyLightData));
IsSkyLight = 1;
}
POD_COPYABLE(RenderSkyLightData);
void SetShaderData(ShaderLightData& data, bool useShadow) const;
};
struct RenderEnvironmentProbeData
{
GPUTexture* Texture;
Float3 Position;
float Radius; // unscaled for box
float Brightness;
int32 SortOrder;
uint32 HashID;
bool BoxProjection;
Float3 Scale; // box-only
float BlendDistance;
Quaternion Orientation; // box-only
void SetShaderData(ShaderEnvProbeData& data) const;
};
struct RenderDecalData
{
Matrix World;
MaterialBase* Material;
int32 SortOrder;
uint32 RenderLayersMask;
};
struct RenderFogData
{
IFogRenderer* Renderer;
GPUTextureView* VolumetricFogTexture;
ShaderExponentialHeightFogData ExponentialHeightFogData;
ShaderVolumetricFogData VolumetricFogData;
VolumetricFogOptions VolumetricFog;
RenderFogData();
void Init(const RenderView& view, IFogRenderer* renderer);
};
/// <summary>
/// The draw calls list types.
/// </summary>
API_ENUM() enum class DrawCallsListType
{
/// <summary>
/// Hardware depth rendering.
/// </summary>
Depth,
/// <summary>
/// GBuffer rendering.
/// </summary>
GBuffer,
/// <summary>
/// GBuffer rendering after decals.
/// </summary>
GBufferNoDecals,
/// <summary>
/// Transparency rendering.
/// </summary>
Forward,
/// <summary>
/// Distortion accumulation rendering.
/// </summary>
Distortion,
/// <summary>
/// Motion vectors rendering.
/// </summary>
MotionVectors,
MAX,
};
/// <summary>
/// Represents a patch of draw calls that can be submitted to rendering.
/// </summary>
struct DrawBatch
{
/// <summary>
/// Draw calls sorting key (shared by the all draw calls withing a patch).
/// </summary>
uint64 SortKey;
/// <summary>
/// The first draw call index.
/// </summary>
uint16 StartIndex;
/// <summary>
/// A number of draw calls to be submitted at once.
/// </summary>
uint16 BatchSize;
/// <summary>
/// The total amount of instances (sum from all draw calls in this batch).
/// </summary>
uint32 InstanceCount;
bool operator<(const DrawBatch& other) const
{
return SortKey < other.SortKey;
}
};
struct BatchedDrawCall
{
DrawCall DrawCall;
uint16 ObjectsStartIndex = 0; // Index of the instances start in the ObjectsBuffer (set internally).
Array<struct ShaderObjectData, ConcurrentArenaAllocation> Instances;
BatchedDrawCall() { CRASH; } // Don't use it
BatchedDrawCall(RenderList* list);
BatchedDrawCall(BatchedDrawCall&& other) noexcept;
};
/// <summary>
/// Represents a list of draw calls.
/// </summary>
struct DrawCallsList
{
/// <summary>
/// The list of draw calls indices to render.
/// </summary>
RenderListBuffer<int32> Indices;
/// <summary>
/// The list of external draw calls indices to render.
/// </summary>
RenderListBuffer<int32> PreBatchedDrawCalls;
/// <summary>
/// The draw calls batches (for instancing).
/// </summary>
Array<DrawBatch> Batches;
/// <summary>
/// True if draw calls batches list can be rendered using hardware instancing, otherwise false.
/// </summary>
bool CanUseInstancing = true;
void Clear();
bool IsEmpty() const;
};
// Small utility for allocating memory from RenderList arena pool with automatic fallback to shared RendererAllocation for larger memory blocks.
struct RenderListAlloc
{
void* Data = nullptr;
uint32 Size = 0;
bool NeedFree = false;
~RenderListAlloc();
void* Init(RenderList* list, uint32 size, uint32 alignment = 1);
template<typename T>
FORCE_INLINE T* Init(RenderList* list, int32 count, uint32 alignment = 1)
{
return (T*)Init(list, count * sizeof(T), alignment);
}
template<typename T>
FORCE_INLINE T* Get()
{
return (T*)Data;
}
};
/// <summary>
/// Rendering cache container object for the draw calls collecting, sorting and executing.
/// </summary>
API_CLASS(Sealed) class FLAXENGINE_API RenderList : public ScriptingObject
{
DECLARE_SCRIPTING_TYPE(RenderList);
/// <summary>
/// Allocates the new renderer list object or reuses already allocated one.
/// </summary>
/// <returns>The cache object.</returns>
API_FUNCTION() static RenderList* GetFromPool();
/// <summary>
/// Frees the list back to the pool.
/// </summary>
/// <param name="cache">The cache.</param>
API_FUNCTION() static void ReturnToPool(RenderList* cache);
/// <summary>
/// Cleanups the static data cache used to accelerate draw calls sorting. Use it to reduce memory pressure.
/// </summary>
static void CleanupCache();
/// <summary>
/// The rendering extension interface for custom drawing/effects linked to RenderList. Can be used during async scene drawing and further drawing/processing for more optimized rendering.
/// </summary>
class FLAXENGINE_API IExtension
{
public:
IExtension();
virtual ~IExtension();
// Event called when GPU Device is shutting down and the extension is being disposed. Used to clean up GPU resources (before scripting might destroy extension).
virtual void Dispose() {}
// Event called before collecting draw calls. Can be used for initialization.
virtual void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) {}
// Event called after collecting draw calls. Can be used for cleanup or to perform additional drawing using collected draw calls data such as batched data processing.
virtual void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) {}
};
public:
/// <summary>
/// Memory storage with all draw-related data that lives during a single frame rendering time. Thread-safe to allocate memory during rendering jobs.
/// </summary>
ConcurrentArenaAllocator Memory;
/// <summary>
/// All scenes for rendering.
/// </summary>
Array<SceneRendering*> Scenes;
/// <summary>
/// Draw calls list (for all draw passes).
/// </summary>
RenderListBuffer<DrawCall> DrawCalls;
/// <summary>
/// Draw calls list with pre-batched instances (for all draw passes).
/// </summary>
RenderListBuffer<BatchedDrawCall> BatchedDrawCalls;
/// <summary>
/// The draw calls lists. Each for the separate draw pass.
/// </summary>
DrawCallsList DrawCallsLists[(int32)DrawCallsListType::MAX];
/// <summary>
/// The additional draw calls list for Depth drawing into Shadow Projections that use DrawCalls from main render context. This assumes that RenderContextBatch contains main context and shadow projections only.
/// </summary>
DrawCallsList ShadowDepthDrawCallsList;
/// <summary>
/// Light pass members - directional lights
/// </summary>
Array<RenderDirectionalLightData> DirectionalLights;
/// <summary>
/// Light pass members - point lights
/// </summary>
RenderListBuffer<RenderPointLightData> PointLights;
/// <summary>
/// Light pass members - spot lights
/// </summary>
RenderListBuffer<RenderSpotLightData> SpotLights;
/// <summary>
/// Light pass members - sky lights
/// </summary>
Array<RenderSkyLightData> SkyLights;
/// <summary>
/// Environment probes to use for rendering reflections
/// </summary>
Array<RenderEnvironmentProbeData> EnvironmentProbes;
/// <summary>
/// Decals registered for the rendering.
/// </summary>
Array<RenderDecalData> Decals;
/// <summary>
/// Local volumetric fog particles registered for the rendering.
/// </summary>
RenderListBuffer<DrawCall> VolumetricFogParticles;
/// <summary>
/// Sky/skybox renderer proxy to use (only one per frame)
/// </summary>
ISkyRenderer* Sky;
/// <summary>
/// Atmospheric fog renderer proxy to use (only one per frame)
/// </summary>
IAtmosphericFogRenderer* AtmosphericFog;
/// <summary>
/// Fog rendering data.
/// </summary>
RenderFogData Fog;
/// <summary>
/// Post effects to render.
/// </summary>
Array<PostProcessEffect*> PostFx;
/// <summary>
/// The renderer setup for the frame drawing.
/// </summary>
RenderSetup Setup;
/// <summary>
/// The post process settings.
/// </summary>
PostProcessSettings Settings;
struct FLAXENGINE_API BlendableSettings
{
IPostFxSettingsProvider* Provider;
float Weight;
int32 Priority;
float VolumeSizeSqr;
bool operator<(const BlendableSettings& other) const;
};
/// <summary>
/// The blendable postFx volumes collected during frame draw calls gather pass.
/// </summary>
Array<BlendableSettings> Blendable;
void AddSettingsBlend(IPostFxSettingsProvider* provider, float weight, int32 priority, float volumeSizeSqr);
/// <summary>
/// Camera frustum corners in World Space
/// </summary>
Float3 FrustumCornersWs[8];
/// <summary>
/// Camera frustum corners in View Space
/// </summary>
Float3 FrustumCornersVs[8];
/// <summary>
/// Objects buffer that contains ShaderObjectData for each DrawCall.
/// </summary>
DynamicTypedBuffer ObjectBuffer;
/// <summary>
/// Temporary objects buffer that contains ShaderObjectData for each DrawCall reused during scene rendering (eg. by skybox).
/// </summary>
DynamicTypedBuffer TempObjectBuffer;
typedef Function<void(GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex)> DelayedDraw;
void AddDelayedDraw(DelayedDraw&& func);
void DrainDelayedDraws(GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex);
/// <summary>
/// Adds custom callback (eg. lambda) to invoke after scene draw calls are collected on a main thread (some async draw tasks might be active). Allows for safe usage of GPUContext for draw preparations or to perform GPU-driven drawing.
/// </summary>
/// <remarks>Can be called in async during scene rendering (thread-safe internally). Lambda is allocated by concurrent arena allocator owned by the RenderList.</remarks>
template<typename T>
FORCE_INLINE void AddDelayedDraw(const T& lambda)
{
DelayedDraw func;
func.Bind<ConcurrentArenaAllocation>(&Memory, lambda);
AddDelayedDraw(MoveTemp(func));
}
// IExtension implementation
void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch);
void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch);
private:
DynamicVertexBuffer _instanceBuffer;
RenderListBuffer<DelayedDraw> _delayedDraws;
public:
/// <summary>
/// Blends the postprocessing settings into the final options.
/// </summary>
void BlendSettings();
/// <summary>
/// Runs the post fx materials pass. Uses input/output buffer to render all materials. Uses temporary render target as a ping pong buffer if required (the same format and description).
/// </summary>
/// <param name="context">The context.</param>
/// <param name="renderContext">The rendering context.</param>
/// <param name="locationA">The material postFx location.</param>
/// <param name="locationB">The custom postFx location.</param>
/// <param name="inputOutput">The input and output texture.</param>
void RunPostFxPass(GPUContext* context, RenderContext& renderContext, MaterialPostFxLocation locationA, PostProcessEffectLocation locationB, GPUTexture*& inputOutput);
/// <summary>
/// Runs the material post fx pass. Uses input and output buffers as a ping pong to render all materials.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="renderContext">The rendering context.</param>
/// <param name="location">The material postFx location.</param>
/// <param name="input">The input texture.</param>
/// <param name="output">The output texture.</param>
void RunMaterialPostFxPass(GPUContext* context, RenderContext& renderContext, MaterialPostFxLocation location, GPUTexture*& input, GPUTexture*& output);
/// <summary>
/// Runs the custom post fx pass. Uses input and output buffers as a ping pong to render all effects.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="renderContext">The rendering context.</param>
/// <param name="location">The custom postFx location.</param>
/// <param name="input">The input texture.</param>
/// <param name="output">The output texture.</param>
void RunCustomPostFxPass(GPUContext* context, RenderContext& renderContext, PostProcessEffectLocation location, GPUTexture*& input, GPUTexture*& output);
/// <summary>
/// Determines whether any Custom PostFx specified by given type. Used to pick a faster rendering path by the frame rendering module.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="postProcess">The PostFx location to check (for scripts).</param>
/// <returns>True if render any postFx of the given type, otherwise false.</returns>
bool HasAnyPostFx(const RenderContext& renderContext, PostProcessEffectLocation postProcess) const;
/// <summary>
/// Determines whether any Material PostFx specified by given type. Used to pick a faster rendering path by the frame rendering module.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="materialPostFx">The PostFx location to check (for materials).</param>
/// <returns>True if render any postFx of the given type, otherwise false.</returns>
bool HasAnyPostFx(const RenderContext& renderContext, MaterialPostFxLocation materialPostFx) const;
/// <summary>
/// Determines whether any Custom PostFx or Material PostFx specified by given type. Used to pick a faster rendering path by the frame rendering module.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="postProcess">The PostFx location to check (for scripts).</param>
/// <param name="materialPostFx">The PostFx location to check (for materials).</param>
/// <returns>True if render any postFx of the given type, otherwise false.</returns>
bool HasAnyPostFx(const RenderContext& renderContext, PostProcessEffectLocation postProcess, MaterialPostFxLocation materialPostFx) const
{
return HasAnyPostFx(renderContext, postProcess) || HasAnyPostFx(renderContext, materialPostFx);
}
public:
/// <summary>
/// Init cache for given task
/// </summary>
/// <param name="renderContext">The rendering context.</param>
void Init(RenderContext& renderContext);
/// <summary>
/// Clear cached data
/// </summary>
void Clear();
public:
/// <summary>
/// Adds the draw call to the draw lists.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="drawModes">The object draw modes.</param>
/// <param name="staticFlags">The object static flags.</param>
/// <param name="drawCall">The draw call data.</param>
/// <param name="receivesDecals">True if the rendered mesh can receive decals.</param>
/// <param name="sortOrder">Object sorting key.</param>
void AddDrawCall(const RenderContext& renderContext, DrawPass drawModes, StaticFlags staticFlags, DrawCall& drawCall, bool receivesDecals = true, int8 sortOrder = 0);
/// <summary>
/// Adds the draw call to the draw lists and references it in other render contexts. Performs additional per-context frustum culling.
/// </summary>
/// <param name="renderContextBatch">The rendering context batch. This assumes that RenderContextBatch contains main context and shadow projections only.</param>
/// <param name="drawModes">The object draw modes.</param>
/// <param name="staticFlags">The object static flags.</param>
/// <param name="shadowsMode">The object shadows casting mode.</param>
/// <param name="bounds">The object bounds.</param>
/// <param name="drawCall">The draw call data.</param>
/// <param name="receivesDecals">True if the rendered mesh can receive decals.</param>
/// <param name="sortOrder">Object sorting key.</param>
void AddDrawCall(const RenderContextBatch& renderContextBatch, DrawPass drawModes, StaticFlags staticFlags, ShadowsCastingMode shadowsMode, const BoundingSphere& bounds, DrawCall& drawCall, bool receivesDecals = true, int8 sortOrder = 0);
/// <summary>
/// Writes all draw calls into large objects buffer (used for random-access object data access on a GPU). Can be executed in async.
/// </summary>
void BuildObjectsBuffer();
/// <summary>
/// Sorts the collected draw calls list.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="reverseDistance">If set to <c>true</c> reverse draw call distance to the view. Results in back to front sorting.</param>
/// <param name="listType">The collected draw calls list type.</param>
/// <param name="pass">The draw pass (optional).</param>
API_FUNCTION() FORCE_INLINE void SortDrawCalls(API_PARAM(Ref) const RenderContext& renderContext, bool reverseDistance, DrawCallsListType listType, DrawPass pass = DrawPass::All)
{
SortDrawCalls(renderContext, reverseDistance, DrawCallsLists[(int32)listType], DrawCalls, listType);
}
/// <summary>
/// Sorts the collected draw calls list.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="reverseDistance">If set to <c>true</c> reverse draw call distance to the view. Results in back to front sorting.</param>
/// <param name="list">The collected draw calls indices list.</param>
/// <param name="drawCalls">The collected draw calls list.</param>
/// <param name="listType">The hint about draw calls list type (optional).</param>
/// <param name="pass">The draw pass (optional).</param>
void SortDrawCalls(const RenderContext& renderContext, bool reverseDistance, DrawCallsList& list, const RenderListBuffer<DrawCall>& drawCalls, DrawCallsListType listType = DrawCallsListType::GBuffer, DrawPass pass = DrawPass::All);
/// <summary>
/// Executes the collected draw calls.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="listType">The collected draw calls list type.</param>
/// <param name="input">The input scene color. It's optional and used in forward/postFx rendering.</param>
API_FUNCTION() FORCE_INLINE void ExecuteDrawCalls(API_PARAM(Ref) const RenderContext& renderContext, DrawCallsListType listType, GPUTextureView* input = nullptr)
{
ExecuteDrawCalls(renderContext, DrawCallsLists[(int32)listType], this, input);
}
/// <summary>
/// Executes the collected draw calls.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="list">The collected draw calls indices list.</param>
/// <param name="input">The input scene color. It's optional and used in forward/postFx rendering.</param>
FORCE_INLINE void ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsList& list, GPUTextureView* input = nullptr)
{
ExecuteDrawCalls(renderContext, list, this, input);
}
/// <summary>
/// Executes the collected draw calls.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="list">The collected draw calls indices list.</param>
/// <param name="drawCallsList">The collected draw calls list owner.</param>
/// <param name="input">The input scene color. It's optional and used in forward/postFx rendering.</param>
void ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsList& list, RenderList* drawCallsList, GPUTextureView* input);
};
/// <summary>
/// Represents a single object information for GPU rendering.
/// </summary>
GPU_CB_STRUCT(ShaderObjectData
{
Float4 Raw[8];
void FLAXENGINE_API Store(const Matrix& worldMatrix, const Matrix& prevWorldMatrix, const Half4& lightmapUVsAreaPacked, const Float3& geometrySize, float perInstanceRandom = 0.0f, float worldDeterminantSign = 1.0f, byte lodDitherFactor = 0, uint32 skinningOffset = 0, int16 skinningPrevOffset = 0);
void FLAXENGINE_API Load(Matrix& worldMatrix, Matrix& prevWorldMatrix, Half4& lightmapUVsArea, Float3& geometrySize, float& perInstanceRandom, float& worldDeterminantSign, byte& lodDitherFactor, uint32& skinningOffset, int16& prevBonesOffset) const;
FORCE_INLINE void Store(const DrawCall& drawCall)
{
Store(drawCall.World, drawCall.Surface.PrevWorld, drawCall.Surface.LightmapUVsArea, drawCall.Surface.GeometrySize, drawCall.PerInstanceRandom, drawCall.WorldDeterminant ? -1.0f : 1.0f, drawCall.Surface.LODDitherFactor, drawCall.Surface.SkinningBonesOffset, drawCall.Surface.PrevBonesOffset);
}
FORCE_INLINE void Load(DrawCall& drawCall) const
{
float worldDeterminantSign;
Load(drawCall.World, drawCall.Surface.PrevWorld, drawCall.Surface.LightmapUVsArea, drawCall.Surface.GeometrySize, drawCall.PerInstanceRandom, worldDeterminantSign, drawCall.Surface.LODDitherFactor, drawCall.Surface.SkinningBonesOffset, drawCall.Surface.PrevBonesOffset);
drawCall.ObjectPosition = drawCall.World.GetTranslation();
drawCall.WorldDeterminant = worldDeterminantSign < 0 ? 1 : 0;
}
});
/// <summary>
/// Represents data passed to Vertex Shader used for instanced rendering (per-instance element).
/// </summary>
PACK_STRUCT(struct ShaderObjectDrawInstanceData
{
uint32 ObjectIndex;
});
struct SurfaceDrawCallHandler
{
static void GetHash(const DrawCall& drawCall, uint32& batchKey);
static bool CanBatch(const DrawCall& a, const DrawCall& b, DrawPass pass);
};