#define GPU_RESIDENT_DRAWER_ALLOW_FORCE_ON
using System;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine.Assertions;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
using static UnityEngine.ObjectDispatcher;
using Unity.Jobs;
using static UnityEngine.Rendering.RenderersParameters;
using Unity.Jobs.LowLevel.Unsafe;
using UnityEngine.Rendering.RenderGraphModule;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Rendering;
#endif
namespace UnityEngine.Rendering
{
///
/// Static utility class for updating data post cull in begin camera rendering
///
public partial class GPUResidentDrawer
{
internal static GPUResidentDrawer instance { get => s_Instance; }
private static GPUResidentDrawer s_Instance = null;
////////////////////////////////////////
// Public API for rendering pipelines //
////////////////////////////////////////
#region Public API
///
/// Utility function to test if instance occlusion culling is enabled
///
/// True if instance occlusion culling is enabled
public static bool IsInstanceOcclusionCullingEnabled()
{
if (s_Instance == null)
return false;
if (s_Instance.settings.mode != GPUResidentDrawerMode.InstancedDrawing)
return false;
if (s_Instance.settings.enableOcclusionCulling)
return true;
return false;
}
///
/// Utility function for updating data post cull in begin camera rendering
///
///
/// Context containing the data to be set
///
public static void PostCullBeginCameraRendering(RenderRequestBatcherContext context)
{
s_Instance?.batcher.PostCullBeginCameraRendering(context);
}
///
/// Utility function for updating probe data after global ambient probe is set up
///
public static void OnSetupAmbientProbe()
{
s_Instance?.batcher.OnSetupAmbientProbe();
}
///
/// Utility function to run an occlusion test in compute to update indirect draws.
/// This function will dispatch compute shaders to run the given occlusion test and
/// update all indirect draws in the culling output for the given view.
/// The next time a renderer list that uses this culling output is drawn, these
/// indirect draw commands will contain only the instances that passed the given
/// occlusion test.
///
/// Render graph that will have a compute pass added.
/// The view to update and occlusion test to use.
/// Specifies the occluder subviews to use with each culling split index.
public static void InstanceOcclusionTest(RenderGraph renderGraph, in OcclusionCullingSettings settings, ReadOnlySpan subviewOcclusionTests)
{
s_Instance?.batcher.InstanceOcclusionTest(renderGraph, settings, subviewOcclusionTests);
}
///
/// Utility function used to update occluders using a depth buffer.
/// This function will dispatch compute shaders to read the given depth buffer
/// and build a mip pyramid of closest depths for use during occlusion culling.
/// The next time an occlusion test is issed for this view, instances will be
/// tested against the updated occluders.
///
/// Render graph that will have a compute pass added.
/// Parameter to specify the view and depth buffer to read.
/// Specifies which occluder subviews to update from slices of the input depth buffer.
public static void UpdateInstanceOccluders(RenderGraph renderGraph, in OccluderParameters occluderParameters, ReadOnlySpan occluderSubviewUpdates)
{
s_Instance?.batcher.UpdateInstanceOccluders(renderGraph, occluderParameters, occluderSubviewUpdates);
}
///
/// Enable or disable GPUResidentDrawer based on the project settings.
/// We call this every frame because GPUResidentDrawer can be enabled/disabled by the settings outside the render pipeline asset.
///
public static void ReinitializeIfNeeded()
{
#if UNITY_EDITOR
if (!IsForcedOnViaCommandLine() && (IsProjectSupported() != IsEnabled()))
{
Reinitialize();
}
#endif
}
#endregion
#region Public Debug API
///
/// Utility function to render an occlusion test heatmap debug overlay.
///
/// Render graph that will have a compute pass added.
/// The rendering debugger debug settings to read parameters from.
/// The instance ID of the camera using a GPU occlusion test.
/// The color buffer to render the overlay on.
public static void RenderDebugOcclusionTestOverlay(RenderGraph renderGraph, DebugDisplayGPUResidentDrawer debugSettings, int viewInstanceID, TextureHandle colorBuffer)
{
s_Instance?.batcher.occlusionCullingCommon.RenderDebugOcclusionTestOverlay(renderGraph, debugSettings, viewInstanceID, colorBuffer);
}
///
/// Utility function visualise the occluder pyramid in a debug overlay.
///
/// Render graph that will have a compute pass added.
/// The rendering debugger debug settings to read parameters from.
/// The screen position to render the overlay at.
/// The maximum screen height of the overlay.
/// The color buffer to render the overlay on.
public static void RenderDebugOccluderOverlay(RenderGraph renderGraph, DebugDisplayGPUResidentDrawer debugSettings, Vector2 screenPos, float maxHeight, TextureHandle colorBuffer)
{
s_Instance?.batcher.occlusionCullingCommon.RenderDebugOccluderOverlay(renderGraph, debugSettings, screenPos, maxHeight, colorBuffer);
}
#endregion
internal static DebugRendererBatcherStats GetDebugStats()
{
return s_Instance?.m_BatchersContext.debugStats;
}
private void InsertIntoPlayerLoop()
{
var rootLoop = LowLevel.PlayerLoop.GetCurrentPlayerLoop();
bool isAdded = false;
for (var i = 0; i < rootLoop.subSystemList.Length; i++)
{
var subSystem = rootLoop.subSystemList[i];
// We have to update inside the PostLateUpdate systems, because we have to be able to get previous matrices from renderers.
// Previous matrices are updated by renderer managers on UpdateAllRenderers which is part of PostLateUpdate.
if (!isAdded && subSystem.type == typeof(PostLateUpdate))
{
var subSubSystems = new List();
foreach (var subSubSystem in subSystem.subSystemList)
{
if (subSubSystem.type == typeof(PostLateUpdate.FinishFrameRendering))
{
PlayerLoopSystem s = default;
s.updateDelegate += PostPostLateUpdateStatic;
s.type = GetType();
subSubSystems.Add(s);
isAdded = true;
}
subSubSystems.Add(subSubSystem);
}
subSystem.subSystemList = subSubSystems.ToArray();
rootLoop.subSystemList[i] = subSystem;
}
}
LowLevel.PlayerLoop.SetPlayerLoop(rootLoop);
}
private void RemoveFromPlayerLoop()
{
var rootLoop = LowLevel.PlayerLoop.GetCurrentPlayerLoop();
for (int i = 0; i < rootLoop.subSystemList.Length; i++)
{
var subsystem = rootLoop.subSystemList[i];
if (subsystem.type != typeof(PostLateUpdate))
continue;
var newList = new List();
foreach (var subSubSystem in subsystem.subSystemList)
{
if (subSubSystem.type != GetType())
newList.Add(subSubSystem);
}
subsystem.subSystemList = newList.ToArray();
rootLoop.subSystemList[i] = subsystem;
}
LowLevel.PlayerLoop.SetPlayerLoop(rootLoop);
}
#if UNITY_EDITOR
private static void OnAssemblyReload()
{
if (s_Instance is not null)
s_Instance.Dispose();
}
#endif
internal static bool IsEnabled()
{
return s_Instance is not null;
}
internal static GPUResidentDrawerSettings GetGlobalSettingsFromRPAsset()
{
var renderPipelineAsset = GraphicsSettings.currentRenderPipeline;
if (renderPipelineAsset is not IGPUResidentRenderPipeline mbAsset)
return new GPUResidentDrawerSettings();
var settings = mbAsset.gpuResidentDrawerSettings;
if (IsForcedOnViaCommandLine())
settings.mode = GPUResidentDrawerMode.InstancedDrawing;
if (IsOcclusionForcedOnViaCommandLine())
settings.enableOcclusionCulling = true;
return settings;
}
///
/// Is GRD forced on via the command line via -force-gpuresidentdrawer. Editor only.
///
/// true if forced on
private static bool IsForcedOnViaCommandLine()
{
#if UNITY_EDITOR
return s_IsForcedOnViaCommandLine;
#else
return false;
#endif
}
///
/// Is occlusion culling forced on via the command line via -force-gpuocclusion. Editor only.
///
/// true if forced on
private static bool IsOcclusionForcedOnViaCommandLine()
{
#if UNITY_EDITOR
return s_IsOcclusionForcedOnViaCommandLine;
#else
return false;
#endif
}
internal static void Reinitialize()
{
var settings = GetGlobalSettingsFromRPAsset();
// When compiling in the editor, we include a try catch block around our initialization logic to avoid leaving the editor window in a broken state if something goes wrong.
// We can probably remove this in the future once the edit mode functionality stabilizes, but for now it's safest to have a fallback.
#if UNITY_EDITOR
try
#endif
{
Recreate(settings);
}
#if UNITY_EDITOR
catch (Exception exception)
{
Debug.LogError($"The GPU Resident Drawer encountered an error during initialization. The standard SRP path will be used instead. [Error: {exception.Message}]");
Debug.LogError($"GPU Resident drawer stack trace: {exception.StackTrace}");
CleanUp();
}
#endif
}
private static void CleanUp()
{
if (s_Instance == null)
return;
s_Instance.Dispose();
s_Instance = null;
}
private static void Recreate(GPUResidentDrawerSettings settings)
{
CleanUp();
if (IsGPUResidentDrawerSupportedBySRP(settings, out var message, out var severity))
{
s_Instance = new GPUResidentDrawer(settings, 4096, 0);
}
else
{
LogMessage(message, severity);
}
}
internal GPUResidentBatcher batcher { get => m_Batcher; }
internal GPUResidentDrawerSettings settings { get => m_Settings; }
private GPUResidentDrawerSettings m_Settings;
private GPUDrivenProcessor m_GPUDrivenProcessor = null;
private RenderersBatchersContext m_BatchersContext = null;
private GPUResidentBatcher m_Batcher = null;
private ObjectDispatcher m_Dispatcher;
private MeshRendererDrawer m_MeshRendererDrawer;
#if UNITY_EDITOR
private static readonly bool s_IsForcedOnViaCommandLine;
private static readonly bool s_IsOcclusionForcedOnViaCommandLine;
private NativeList m_FrameCameraIDs;
private bool m_FrameUpdateNeeded = false;
private bool m_SelectionChanged;
static GPUResidentDrawer()
{
Lightmapping.bakeCompleted += Reinitialize;
#if GPU_RESIDENT_DRAWER_ALLOW_FORCE_ON
foreach (var arg in Environment.GetCommandLineArgs())
{
if (arg.Equals("-force-gpuresidentdrawer", StringComparison.InvariantCultureIgnoreCase))
{
Debug.Log("GPU Resident Drawer forced on via commandline");
s_IsForcedOnViaCommandLine = true;
}
if (arg.Equals("-force-gpuocclusion", StringComparison.InvariantCultureIgnoreCase))
{
Debug.Log("GPU occlusion culling forced on via commandline");
s_IsOcclusionForcedOnViaCommandLine = true;
}
}
#endif
}
#endif
private GPUResidentDrawer(GPUResidentDrawerSettings settings, int maxInstanceCount, int maxTreeInstanceCount)
{
var resources = GraphicsSettings.GetRenderPipelineSettings();
var renderPipelineAsset = GraphicsSettings.currentRenderPipeline;
var mbAsset = renderPipelineAsset as IGPUResidentRenderPipeline;
Debug.Assert(mbAsset != null, "No compatible Render Pipeline found");
Assert.IsFalse(settings.mode == GPUResidentDrawerMode.Disabled);
m_Settings = settings;
var rbcDesc = RenderersBatchersContextDesc.NewDefault();
rbcDesc.instanceNumInfo = new InstanceNumInfo(meshRendererNum: maxInstanceCount, speedTreeNum: maxTreeInstanceCount);
rbcDesc.supportDitheringCrossFade = settings.supportDitheringCrossFade;
rbcDesc.smallMeshScreenPercentage = settings.smallMeshScreenPercentage;
rbcDesc.enableBoundingSpheresInstanceData = settings.enableOcclusionCulling;
rbcDesc.enableCullerDebugStats = true; // for now, always allow the possibility of reading counter stats from the cullers.
var instanceCullingBatcherDesc = InstanceCullingBatcherDesc.NewDefault();
#if UNITY_EDITOR
instanceCullingBatcherDesc.brgPicking = settings.pickingShader;
instanceCullingBatcherDesc.brgLoading = settings.loadingShader;
instanceCullingBatcherDesc.brgError = settings.errorShader;
#endif
m_GPUDrivenProcessor = new GPUDrivenProcessor();
m_BatchersContext = new RenderersBatchersContext(rbcDesc, m_GPUDrivenProcessor, resources);
m_Batcher = new GPUResidentBatcher(
m_BatchersContext,
instanceCullingBatcherDesc,
m_GPUDrivenProcessor);
m_Dispatcher = new ObjectDispatcher();
m_Dispatcher.EnableTypeTracking(TypeTrackingFlags.SceneObjects);
m_Dispatcher.EnableTypeTracking();
m_Dispatcher.EnableTypeTracking();
m_Dispatcher.EnableTransformTracking(TransformTrackingType.GlobalTRS);
m_MeshRendererDrawer = new MeshRendererDrawer(this, m_Dispatcher);
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload += OnAssemblyReload;
m_FrameCameraIDs = new NativeList(1, Allocator.Persistent);
#endif
SceneManager.sceneLoaded += OnSceneLoaded;
RenderPipelineManager.beginContextRendering += OnBeginContextRendering;
RenderPipelineManager.endContextRendering += OnEndContextRendering;
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
#if UNITY_EDITOR
Selection.selectionChanged += OnSelectionChanged;
#endif
// GPU Resident Drawer only supports legacy lightmap binding.
// Accordingly, we set the keyword globally across all shaders.
const string useLegacyLightmapsKeyword = "USE_LEGACY_LIGHTMAPS";
Shader.EnableKeyword(useLegacyLightmapsKeyword);
InsertIntoPlayerLoop();
}
private void Dispose()
{
Assert.IsNotNull(s_Instance);
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload -= OnAssemblyReload;
if (m_FrameCameraIDs.IsCreated)
m_FrameCameraIDs.Dispose();
#endif
SceneManager.sceneLoaded -= OnSceneLoaded;
RenderPipelineManager.beginContextRendering -= OnBeginContextRendering;
RenderPipelineManager.endContextRendering -= OnEndContextRendering;
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
#if UNITY_EDITOR
Selection.selectionChanged -= OnSelectionChanged;
#endif
RemoveFromPlayerLoop();
const string useLegacyLightmapsKeyword = "USE_LEGACY_LIGHTMAPS";
Shader.DisableKeyword(useLegacyLightmapsKeyword);
m_MeshRendererDrawer.Dispose();
m_MeshRendererDrawer = null;
m_Dispatcher.Dispose();
m_Dispatcher = null;
s_Instance = null;
m_Batcher?.Dispose();
m_BatchersContext.Dispose();
m_GPUDrivenProcessor.Dispose();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// Loaded scene might contain light probes that would affect existing objects. Hence we have to update all probes data.
if(mode == LoadSceneMode.Additive)
m_BatchersContext.UpdateAmbientProbeAndGpuBuffer(forceUpdate: true);
}
private static void PostPostLateUpdateStatic()
{
s_Instance?.PostPostLateUpdate();
}
private void OnBeginContextRendering(ScriptableRenderContext context, List cameras)
{
if (s_Instance is null)
return;
#if UNITY_EDITOR
EditorFrameUpdate(cameras);
#endif
m_Batcher.OnBeginContextRendering();
}
#if UNITY_EDITOR
// If running in the editor the player loop might not run
// In order to still have a single frame update we keep track of the camera ids
// A frame update happens in case the first camera is rendered again
private void EditorFrameUpdate(List cameras)
{
bool newFrame = false;
foreach (Camera camera in cameras)
{
int instanceID = camera.GetInstanceID();
if (m_FrameCameraIDs.Length == 0 || m_FrameCameraIDs.Contains(instanceID))
{
newFrame = true;
m_FrameCameraIDs.Clear();
}
m_FrameCameraIDs.Add(instanceID);
}
if (newFrame)
{
if (m_FrameUpdateNeeded)
m_Batcher.UpdateFrame();
else
m_FrameUpdateNeeded = true;
}
ProcessSelection();
}
private void OnSelectionChanged()
{
m_SelectionChanged = true;
}
private void ProcessSelection()
{
if(!m_SelectionChanged)
return;
m_SelectionChanged = false;
Object[] renderers = Selection.GetFiltered(typeof(MeshRenderer), SelectionMode.Deep);
var rendererIDs = new NativeArray(renderers.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < renderers.Length; ++i)
rendererIDs[i] = renderers[i] ? renderers[i].GetInstanceID() : 0;
m_Batcher.UpdateSelectedRenderers(rendererIDs);
rendererIDs.Dispose();
}
#endif
private void OnEndContextRendering(ScriptableRenderContext context, List cameras)
{
if (s_Instance is null)
return;
m_Batcher.OnEndContextRendering();
}
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
m_Batcher.OnBeginCameraRendering(camera);
}
private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera)
{
m_Batcher.OnEndCameraRendering(camera);
}
private void PostPostLateUpdate()
{
m_BatchersContext.UpdateAmbientProbeAndGpuBuffer(forceUpdate: false);
Profiler.BeginSample("GPUResidentDrawer.DispatchChanges");
var lodGroupTransformData = m_Dispatcher.GetTransformChangesAndClear(TransformTrackingType.GlobalTRS, Allocator.TempJob);
var lodGroupData = m_Dispatcher.GetTypeChangesAndClear(Allocator.TempJob, noScriptingArray: true);
var meshDataSorted = m_Dispatcher.GetTypeChangesAndClear(Allocator.TempJob, sortByInstanceID: true, noScriptingArray: true);
var materialData = m_Dispatcher.GetTypeChangesAndClear(Allocator.TempJob, noScriptingArray: true);
Profiler.EndSample();
Profiler.BeginSample("GPUResidentDrawer.ProcessMaterials");
ProcessMaterials(materialData.destroyedID);
Profiler.EndSample();
Profiler.BeginSample("GPUResidentDrawer.ProcessMeshes");
ProcessMeshes(meshDataSorted.destroyedID);
Profiler.EndSample();
Profiler.BeginSample("GPUResidentDrawer.ProcessLODGroups");
ProcessLODGroups(lodGroupData.changedID, lodGroupData.destroyedID, lodGroupTransformData.transformedID);
Profiler.EndSample();
lodGroupTransformData.Dispose();
lodGroupData.Dispose();
meshDataSorted.Dispose();
materialData.Dispose();
Profiler.BeginSample("GPUResidentDrawer.ProcessDraws");
m_MeshRendererDrawer.ProcessDraws();
// Add more drawers here ...
Profiler.EndSample();
m_BatchersContext.UpdateInstanceMotions();
m_Batcher.UpdateFrame();
#if UNITY_EDITOR
m_FrameUpdateNeeded = false;
#endif
}
private void ProcessMaterials(NativeArray destroyedID)
{
if(destroyedID.Length == 0)
return;
m_Batcher.DestroyMaterials(destroyedID);
}
private void ProcessMeshes(NativeArray destroyedID)
{
if (destroyedID.Length == 0)
return;
var destroyedMeshInstances = new NativeList(Allocator.TempJob);
ScheduleQueryMeshInstancesJob(destroyedID, destroyedMeshInstances).Complete();
m_Batcher.DestroyInstances(destroyedMeshInstances.AsArray());
destroyedMeshInstances.Dispose();
//@ Some rendererGroupID will not be invalidated when their mesh changed. We will need to update Mesh bounds, probes etc. manually for them.
m_Batcher.DestroyMeshes(destroyedID);
}
private void ProcessLODGroups(NativeArray changedID, NativeArray destroyed, NativeArray transformedID)
{
m_BatchersContext.DestroyLODGroups(destroyed);
m_BatchersContext.UpdateLODGroups(changedID);
m_BatchersContext.TransformLODGroups(transformedID);
}
internal void ProcessRenderers(NativeArray rendererGroupsID)
{
Profiler.BeginSample("GPUResidentDrawer.ProcessMeshRenderers");
var changedInstances = new NativeArray(rendererGroupsID.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
ScheduleQueryRendererGroupInstancesJob(rendererGroupsID, changedInstances).Complete();
m_Batcher.DestroyInstances(changedInstances);
changedInstances.Dispose();
m_Batcher.UpdateRenderers(rendererGroupsID);
Profiler.EndSample();
}
internal void TransformInstances(NativeArray instances, NativeArray localToWorldMatrices)
{
Profiler.BeginSample("GPUResidentDrawer.TransformInstances");
m_BatchersContext.UpdateInstanceTransforms(instances, localToWorldMatrices);
Profiler.EndSample();
}
internal void FreeInstances(NativeArray instances)
{
Profiler.BeginSample("GPUResidentDrawer.FreeInstances");
m_Batcher.DestroyInstances(instances);
m_BatchersContext.FreeInstances(instances);
Profiler.EndSample();
}
internal void FreeRendererGroupInstances(NativeArray rendererGroupIDs)
{
Profiler.BeginSample("GPUResidentDrawer.FreeRendererGroupInstances");
m_Batcher.FreeRendererGroupInstances(rendererGroupIDs);
Profiler.EndSample();
}
//@ Implement later...
internal InstanceHandle AppendNewInstance(int rendererGroupID, in Matrix4x4 instanceTransform)
{
throw new NotImplementedException();
}
//@ Additionally we need to implement the way to tie external transforms (not Transform components) with instances.
//@ So that an individual instance could be transformed externally and then updated in the drawer.
internal JobHandle ScheduleQueryRendererGroupInstancesJob(NativeArray rendererGroupIDs, NativeArray instances)
{
return m_BatchersContext.ScheduleQueryRendererGroupInstancesJob(rendererGroupIDs, instances);
}
internal JobHandle ScheduleQueryRendererGroupInstancesJob(NativeArray rendererGroupIDs, NativeList instances)
{
return m_BatchersContext.ScheduleQueryRendererGroupInstancesJob(rendererGroupIDs, instances);
}
internal JobHandle ScheduleQueryRendererGroupInstancesJob(NativeArray rendererGroupIDs, NativeArray instancesOffset, NativeArray instancesCount, NativeList instances)
{
return m_BatchersContext.ScheduleQueryRendererGroupInstancesJob(rendererGroupIDs, instancesOffset, instancesCount, instances);
}
internal JobHandle ScheduleQueryMeshInstancesJob(NativeArray sortedMeshIDs, NativeList instances)
{
return m_BatchersContext.ScheduleQueryMeshInstancesJob(sortedMeshIDs, instances);
}
}
}