#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;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Burst;

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Rendering;
#endif

namespace UnityEngine.Rendering
{
    /// <summary>
    /// Static utility class for updating data post cull in begin camera rendering
    /// </summary>
    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

        /// <summary>
        /// Utility function to test if instance occlusion culling is enabled
        /// </summary>
        /// <returns>True if instance occlusion culling is enabled</returns>
        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;
        }

        /// <summary>
        /// Utility function for updating data post cull in begin camera rendering
        /// </summary>
        /// <param name="context">
        /// Context containing the data to be set
        /// </param>
        public static void PostCullBeginCameraRendering(RenderRequestBatcherContext context)
        {
            s_Instance?.batcher.PostCullBeginCameraRendering(context);
        }


        /// <summary>
        /// Utility function for updating probe data after global ambient probe is set up
        /// </summary>
        public static void OnSetupAmbientProbe()
        {
            s_Instance?.batcher.OnSetupAmbientProbe();
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="renderGraph">Render graph that will have a compute pass added.</param>
        /// <param name="settings">The view to update and occlusion test to use.</param>
        /// <param name="subviewOcclusionTests">Specifies the occluder subviews to use with each culling split index.</param>
        public static void InstanceOcclusionTest(RenderGraph renderGraph, in OcclusionCullingSettings settings, ReadOnlySpan<SubviewOcclusionTest> subviewOcclusionTests)
        {
            s_Instance?.batcher.InstanceOcclusionTest(renderGraph, settings, subviewOcclusionTests);
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="renderGraph">Render graph that will have a compute pass added.</param>
        /// <param name="occluderParameters">Parameter to specify the view and depth buffer to read.</param>
        /// <param name="occluderSubviewUpdates">Specifies which occluder subviews to update from slices of the input depth buffer.</param>
        public static void UpdateInstanceOccluders(RenderGraph renderGraph, in OccluderParameters occluderParameters, ReadOnlySpan<OccluderSubviewUpdate> occluderSubviewUpdates)
        {
            s_Instance?.batcher.UpdateInstanceOccluders(renderGraph, occluderParameters, occluderSubviewUpdates);
        }

        /// <summary>
        /// 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.
        /// </summary>
        public static void ReinitializeIfNeeded()
        {
#if UNITY_EDITOR
            if (!IsForcedOnViaCommandLine() && !MaintainContext && (IsProjectSupported() != IsEnabled()))
            {
                Reinitialize();
            }
#endif
        }

        #endregion

        #region Public Debug API

        /// <summary>
        /// Utility function to render an occlusion test heatmap debug overlay.
        /// </summary>
        /// <param name="renderGraph">Render graph that will have a compute pass added.</param>
        /// <param name="debugSettings">The rendering debugger debug settings to read parameters from.</param>
        /// <param name="viewInstanceID">The instance ID of the camera using a GPU occlusion test.</param>
        /// <param name="colorBuffer">The color buffer to render the overlay on.</param>
        public static void RenderDebugOcclusionTestOverlay(RenderGraph renderGraph, DebugDisplayGPUResidentDrawer debugSettings, EntityId viewInstanceID, TextureHandle colorBuffer)
        {
            s_Instance?.batcher.occlusionCullingCommon.RenderDebugOcclusionTestOverlay(renderGraph, debugSettings, viewInstanceID, colorBuffer);
        }

        /// <summary>
        /// Utility function visualise the occluder pyramid in a debug overlay.
        /// </summary>
        /// <param name="renderGraph">Render graph that will have a compute pass added.</param>
        /// <param name="debugSettings">The rendering debugger debug settings to read parameters from.</param>
        /// <param name="screenPos">The screen position to render the overlay at.</param>
        /// <param name="maxHeight">The maximum screen height of the overlay.</param>
        /// <param name="colorBuffer">The color buffer to render the overlay on.</param>
        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<PlayerLoopSystem>();
                    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<PlayerLoopSystem>();
                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() || ForceOcclusion)
                settings.enableOcclusionCulling = true;

            return settings;
        }

        /// <summary>
        /// Is GRD forced on via the command line via -force-gpuresidentdrawer. Editor only.
        /// </summary>
        /// <returns>true if forced on</returns>
        internal static bool IsForcedOnViaCommandLine()
        {
#if UNITY_EDITOR
            return s_IsForcedOnViaCommandLine;
#else
            return false;
#endif
        }

        /// <summary>
        /// Is occlusion culling forced on via the command line via -force-gpuocclusion. Editor only.
        /// </summary>
        /// <returns>true if forced on</returns>
        internal static bool IsOcclusionForcedOnViaCommandLine()
        {
#if UNITY_EDITOR
            return s_IsOcclusionForcedOnViaCommandLine;
#else
            return false;
#endif
        }

        internal static bool MaintainContext { get; set; } = false;
        internal static bool ForceOcclusion { get; set; } = false;

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

        IntPtr m_ContextIntPtr = IntPtr.Zero;

        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;


#if UNITY_EDITOR
        private static readonly bool s_IsForcedOnViaCommandLine;
        private static readonly bool s_IsOcclusionForcedOnViaCommandLine;

        private NativeList<EntityId> m_FrameCameraIDs;
        private bool m_FrameUpdateNeeded = false;

        private bool m_IsSelectionDirty;

        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<GPUResidentDrawerResources>();
            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<LODGroup>(TypeTrackingFlags.SceneObjects);
            m_Dispatcher.EnableTypeTracking<Mesh>();
            m_Dispatcher.EnableTypeTracking<Material>();
            m_Dispatcher.EnableTypeTracking<MeshRenderer>(TypeTrackingFlags.SceneObjects);
            m_Dispatcher.EnableTypeTracking<Camera>(TypeTrackingFlags.SceneObjects | TypeTrackingFlags.EditorOnlyObjects);

            m_Dispatcher.EnableTransformTracking<MeshRenderer>(TransformTrackingType.GlobalTRS);
            m_Dispatcher.EnableTransformTracking<LODGroup>(TransformTrackingType.GlobalTRS);

#if UNITY_EDITOR
            AssemblyReloadEvents.beforeAssemblyReload += OnAssemblyReload;
            m_FrameCameraIDs = new NativeList<EntityId>(1, Allocator.Persistent);
            m_IsSelectionDirty = true; // Force at least one selection update
#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();

            string extraText = IsForcedOnViaCommandLine() ? " (forced on via commandline)" : "";
            extraText = MaintainContext ? " (forced on via MaintainContext)" : extraText;
            if (settings.enableOcclusionCulling)
            {
                string occlusionText = IsOcclusionForcedOnViaCommandLine() ? " (forced on via commandline)" : "";
                occlusionText = ForceOcclusion ? " (forced on via ForceOcclusion)" : occlusionText;
                extraText = $"{extraText} with GPU Occlusion Culling{occlusionText}";
            }
            Console.WriteLine($"GPU Resident Drawer created{extraText}.");
        }

        private void Dispose()
        {
            Assert.IsNotNull(s_Instance);

#if UNITY_EDITOR
            AssemblyReloadEvents.beforeAssemblyReload -= OnAssemblyReload;
            if (m_FrameCameraIDs.IsCreated)
                m_FrameCameraIDs.Dispose();
#endif
            SceneManager.sceneLoaded -= OnSceneLoaded;

            // Note: Those RenderPipelineManager callbacks do not run when using built-in editor debug views such as lightmap, shadowmask etc
            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_Dispatcher.Dispose();
            m_Dispatcher = null;

            s_Instance = null;

            m_Batcher?.Dispose();

            m_BatchersContext.Dispose();
            m_GPUDrivenProcessor.Dispose();

            m_ContextIntPtr = IntPtr.Zero;

            Console.WriteLine($"GPU Resident Drawer disposed.");
        }

        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<Camera> cameras)
        {
            if (s_Instance is null)
                return;

            // This logic ensures that EditorFrameUpdate is not called more than once after calling BeginContextRendering, unless EndContextRendering has also been called.
            if (m_ContextIntPtr == IntPtr.Zero)
            {
                m_ContextIntPtr = context.Internal_GetPtr();
#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
        // Note: This doesn't run when using built-in debug views such as lightmaps, shadowmask and etc
        private void EditorFrameUpdate(List<Camera> cameras)
        {
            bool newFrame = false;
            foreach (Camera camera in cameras)
            {
                EntityId instanceID = camera.GetEntityId();
                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;
            }
        }

        private void OnSelectionChanged()
        {
            m_IsSelectionDirty = true;
        }

        private void UpdateSelection()
        {
            Profiler.BeginSample("GPUResidentDrawer.UpdateSelection");

            Object[] renderers = Selection.GetFiltered(typeof(MeshRenderer), SelectionMode.Deep);

            var rendererIDs = new NativeArray<EntityId>(renderers.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);

            for (int i = 0; i < renderers.Length; ++i)
                rendererIDs[i] = renderers[i] ? renderers[i].GetEntityId() : EntityId.None;

            m_Batcher.UpdateSelectedRenderers(rendererIDs);

            rendererIDs.Dispose();

            Profiler.EndSample();
        }
#endif

        private void OnEndContextRendering(ScriptableRenderContext context, List<Camera> cameras)
        {
            if (s_Instance is null)
                return;

            if (m_ContextIntPtr == context.Internal_GetPtr())
            {
                m_ContextIntPtr = IntPtr.Zero;
                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<LODGroup>(TransformTrackingType.GlobalTRS, Allocator.TempJob);
            var lodGroupData = m_Dispatcher.GetTypeChangesAndClear<LODGroup>(Allocator.TempJob, noScriptingArray: true);
            var meshDataSorted = m_Dispatcher.GetTypeChangesAndClear<Mesh>(Allocator.TempJob, sortByInstanceID: true, noScriptingArray: true);
            var cameraChanges = m_Dispatcher.GetTypeChangesAndClear<Camera>(Allocator.TempJob, noScriptingArray: true);
            var materialData = m_Dispatcher.GetTypeChangesAndClear<Material>(Allocator.TempJob, noScriptingArray: true);
            var rendererData = m_Dispatcher.GetTypeChangesAndClear<MeshRenderer>(Allocator.TempJob, noScriptingArray: true);
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ClassifyMaterials");
            ClassifyMaterials(materialData.changedID, out NativeList<EntityId> unsupportedChangedMaterials,
                out NativeList<EntityId> supportedChangedMaterials,
                out NativeList<GPUDrivenPackedMaterialData> supportedChangedPackedMaterialDatas, Allocator.TempJob);
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.FindUnsupportedRenderers");
            NativeList<EntityId> unsupportedRenderers = FindUnsupportedRenderers(unsupportedChangedMaterials.AsArray());
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ProcessMaterials");
            ProcessMaterials(materialData.destroyedID, unsupportedChangedMaterials.AsArray());
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ProcessMeshes");
            ProcessMeshes(meshDataSorted.destroyedID);
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ProcessLODGroups");
            ProcessLODGroups(lodGroupData.changedID, lodGroupData.destroyedID, lodGroupTransformData.transformedID);
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ProcessCameras");
            ProcessCameras(cameraChanges.changedID, cameraChanges.destroyedID);
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ProcessRenderers");
            ProcessRenderers(rendererData, unsupportedRenderers.AsArray());
            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.ProcessRendererMaterialAndMeshChanges");
            ProcessRendererMaterialAndMeshChanges(rendererData.changedID, supportedChangedMaterials.AsArray(), supportedChangedPackedMaterialDatas.AsArray(), meshDataSorted.changedID);
            Profiler.EndSample();

            lodGroupTransformData.Dispose();
            lodGroupData.Dispose();
            meshDataSorted.Dispose();
            materialData.Dispose();
            cameraChanges.Dispose();
            rendererData.Dispose();
            unsupportedChangedMaterials.Dispose();
            unsupportedRenderers.Dispose();
            supportedChangedMaterials.Dispose();
            supportedChangedPackedMaterialDatas.Dispose();

            m_BatchersContext.UpdateInstanceMotions();
            m_Batcher.UpdateFrame();

#if UNITY_EDITOR
            if (m_IsSelectionDirty)
            {
                UpdateSelection();
                m_IsSelectionDirty = false;
            }

            m_FrameUpdateNeeded = false;
#endif
        }

        private void ProcessMaterials(NativeArray<EntityId> destroyedID, NativeArray<EntityId> unsupportedMaterials)
        {
            if (destroyedID.Length > 0)
                m_Batcher.DestroyMaterials(destroyedID);

            if (unsupportedMaterials.Length > 0)
                m_Batcher.DestroyMaterials(unsupportedMaterials);
        }

        private void ProcessCameras(NativeArray<EntityId> changedIDs, NativeArray<EntityId> destroyedIDs)
        {
            m_BatchersContext.UpdateCameras(changedIDs);
            m_BatchersContext.FreePerCameraInstanceData(destroyedIDs);
        }

        private void ProcessMeshes(NativeArray<EntityId> destroyedID)
        {
            if (destroyedID.Length == 0)
                return;

            var destroyedMeshInstances = new NativeList<InstanceHandle>(Allocator.TempJob);
            ScheduleQueryMeshInstancesJob(destroyedID, destroyedMeshInstances).Complete();
            m_Batcher.DestroyDrawInstances(destroyedMeshInstances.AsArray());
            destroyedMeshInstances.Dispose();

            //@ Check if we need to update instance bounds and light probe sampling positions after mesh is destroyed.
            m_Batcher.DestroyMeshes(destroyedID);
        }

        private void ProcessLODGroups(NativeArray<EntityId> changedID, NativeArray<EntityId> destroyed, NativeArray<EntityId> transformedID)
        {
            m_BatchersContext.DestroyLODGroups(destroyed);
            m_BatchersContext.UpdateLODGroups(changedID);
            m_BatchersContext.TransformLODGroups(transformedID);
        }

        private void ProcessRendererMaterialAndMeshChanges(NativeArray<EntityId> excludedRenderers, NativeArray<EntityId> changedMaterials, NativeArray<GPUDrivenPackedMaterialData> changedPackedMaterialDatas, NativeArray<EntityId> changedMeshes)
        {
            if (changedMaterials.Length == 0 && changedMeshes.Length == 0)
                return;

            Profiler.BeginSample("GPUResidentDrawer.GetMaterialsWithChangedPackedMaterial");

            var filteredMaterials = GetMaterialsWithChangedPackedMaterial(changedMaterials, changedPackedMaterialDatas, Allocator.TempJob);
            // Make sure we update the packed material cache AFTER filtering the materials as we need to compare the packed materials
            // with their previous values.
            var updatePackedMaterialCacheJob = m_Batcher.SchedulePackedMaterialCacheUpdate(changedMaterials, changedPackedMaterialDatas);

            Profiler.EndSample();

            if (filteredMaterials.Count == 0 && changedMeshes.Length == 0)
            {
                filteredMaterials.Dispose();
                updatePackedMaterialCacheJob.Complete();
                return;
            }

            var sortedExcludedRenderers = new NativeArray<EntityId>(excludedRenderers, Allocator.TempJob);
            if (sortedExcludedRenderers.Length > 0)
            {
                Profiler.BeginSample("ProcessRendererMaterialAndMeshChanges.Sort");
                sortedExcludedRenderers.SortJob().Schedule().Complete();
                Profiler.EndSample();
            }

            Profiler.BeginSample("GPUResidentDrawer.FindRenderersFromMaterialsOrMeshes");

            var (renderersWithChangedMaterials, renderersWithChangedMeshes) = FindRenderersFromMaterialsOrMeshes(sortedExcludedRenderers, filteredMaterials, changedMeshes, Allocator.TempJob);
            filteredMaterials.Dispose();

            Profiler.EndSample();

            sortedExcludedRenderers.Dispose();
            updatePackedMaterialCacheJob.Complete();

            if (renderersWithChangedMaterials.Length == 0 && renderersWithChangedMeshes.Length == 0)
            {
                renderersWithChangedMaterials.Dispose();
                renderersWithChangedMeshes.Dispose();
                return;
            }

            Profiler.BeginSample("GPUResidentDrawer.UpdateRenderers");
            {
                var changedMaterialsCount = renderersWithChangedMaterials.Length;
                var changedMeshesCount = renderersWithChangedMeshes.Length;
                var totalCount = changedMaterialsCount + changedMeshesCount;

                var changedInstances = new NativeArray<InstanceHandle>(totalCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                var changedRenderers = new NativeArray<EntityId>(totalCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);

                NativeArray<EntityId>.Copy(renderersWithChangedMaterials.AsArray(), changedRenderers, changedMaterialsCount);
                NativeArray<EntityId>.Copy(renderersWithChangedMeshes.AsArray(), changedRenderers.GetSubArray(changedMaterialsCount, changedMeshesCount), changedMeshesCount);

                ScheduleQueryRendererGroupInstancesJob(changedRenderers, changedInstances).Complete();

                m_Batcher.DestroyDrawInstances(changedInstances);
                m_Batcher.UpdateRenderers(renderersWithChangedMaterials.AsArray(), true);
                m_Batcher.UpdateRenderers(renderersWithChangedMeshes.AsArray(), false);

                changedInstances.Dispose();
                changedRenderers.Dispose();

                renderersWithChangedMaterials.Dispose();
                renderersWithChangedMeshes.Dispose();
            }
            Profiler.EndSample();
        }

        private void ProcessRenderers(TypeDispatchData rendererChanges, NativeArray<EntityId> unsupportedRenderers)
        {
            Profiler.BeginSample("GPUResidentDrawer.ProcessRenderers");

            var changedInstances = new NativeArray<InstanceHandle>(rendererChanges.changedID.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
            ScheduleQueryRendererGroupInstancesJob(rendererChanges.changedID, changedInstances).Complete();

            m_Batcher.DestroyDrawInstances(changedInstances);
            changedInstances.Dispose();
            m_Batcher.UpdateRenderers(rendererChanges.changedID);

            FreeRendererGroupInstances(rendererChanges.destroyedID, unsupportedRenderers);

            Profiler.EndSample();

            Profiler.BeginSample("GPUResidentDrawer.TransformMeshRenderers");
            var transformChanges = m_Dispatcher.GetTransformChangesAndClear<MeshRenderer>(TransformTrackingType.GlobalTRS, Allocator.TempJob);
            var transformedInstances = new NativeArray<InstanceHandle>(transformChanges.transformedID.Length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
            ScheduleQueryRendererGroupInstancesJob(transformChanges.transformedID, transformedInstances).Complete();
            // We can pull localToWorldMatrices directly from the renderers if we are doing update after PostLatUpdate.
            // This will save us transform re computation as matrices are ready inside renderer's TransformInfo.
            TransformInstances(transformedInstances, transformChanges.localToWorldMatrices);
            transformedInstances.Dispose();
            transformChanges.Dispose();

            Profiler.EndSample();
        }

        private void TransformInstances(NativeArray<InstanceHandle> instances, NativeArray<Matrix4x4> localToWorldMatrices)
        {
            Profiler.BeginSample("GPUResidentDrawer.TransformInstances");

            m_BatchersContext.UpdateInstanceTransforms(instances, localToWorldMatrices);

            Profiler.EndSample();
        }

        private void FreeInstances(NativeArray<InstanceHandle> instances)
        {
            Profiler.BeginSample("GPUResidentDrawer.FreeInstances");

            m_Batcher.DestroyDrawInstances(instances);
            m_BatchersContext.FreeInstances(instances);

            Profiler.EndSample();
        }

        private void FreeRendererGroupInstances(NativeArray<EntityId> rendererGroupIDs, NativeArray<EntityId> unsupportedRendererGroupIDs)
        {
            Profiler.BeginSample("GPUResidentDrawer.FreeRendererGroupInstances");

            m_Batcher.FreeRendererGroupInstances(rendererGroupIDs);

            if (unsupportedRendererGroupIDs.Length > 0)
            {
                m_Batcher.FreeRendererGroupInstances(unsupportedRendererGroupIDs);
                m_GPUDrivenProcessor.DisableGPUDrivenRendering(unsupportedRendererGroupIDs);
            }

            Profiler.EndSample();
        }

        //@ Implement later...
        private 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.

        private JobHandle ScheduleQueryRendererGroupInstancesJob(NativeArray<EntityId> rendererGroupIDs, NativeArray<InstanceHandle> instances)
        {
            return m_BatchersContext.ScheduleQueryRendererGroupInstancesJob(rendererGroupIDs, instances);
        }

        private JobHandle ScheduleQueryRendererGroupInstancesJob(NativeArray<EntityId> rendererGroupIDs, NativeList<InstanceHandle> instances)
        {
            return m_BatchersContext.ScheduleQueryRendererGroupInstancesJob(rendererGroupIDs, instances);
        }

        private JobHandle ScheduleQueryRendererGroupInstancesJob(NativeArray<EntityId> rendererGroupIDs, NativeArray<int> instancesOffset, NativeArray<int> instancesCount, NativeList<InstanceHandle> instances)
        {
            return m_BatchersContext.ScheduleQueryRendererGroupInstancesJob(rendererGroupIDs, instancesOffset, instancesCount, instances);
        }

        private JobHandle ScheduleQueryMeshInstancesJob(NativeArray<EntityId> sortedMeshIDs, NativeList<InstanceHandle> instances)
        {
            return m_BatchersContext.ScheduleQueryMeshInstancesJob(sortedMeshIDs, instances);
        }

        private void ClassifyMaterials(NativeArray<EntityId> materials, out NativeList<EntityId> unsupportedMaterials,
            out NativeList<EntityId> supportedMaterials, out NativeList<GPUDrivenPackedMaterialData> supportedPackedMaterialDatas, Allocator allocator)
        {
            supportedMaterials = new NativeList<EntityId>(materials.Length, allocator);
            unsupportedMaterials = new NativeList<EntityId>(materials.Length, allocator);
            supportedPackedMaterialDatas = new NativeList<GPUDrivenPackedMaterialData>(materials.Length, allocator);

            if (materials.Length > 0)
            {
                GPUResidentDrawerBurst.ClassifyMaterials(materials, m_Batcher.instanceCullingBatcher.batchMaterialHash.AsReadOnly(),
                                                         ref supportedMaterials, ref unsupportedMaterials, ref supportedPackedMaterialDatas);
            }
        }

        private NativeList<EntityId> FindUnsupportedRenderers(NativeArray<EntityId> unsupportedMaterials)
        {
            NativeList<EntityId> unsupportedRenderers = new NativeList<EntityId>(Allocator.TempJob);

            if (unsupportedMaterials.Length > 0)
            {
                GPUResidentDrawerBurst.FindUnsupportedRenderers(unsupportedMaterials, m_BatchersContext.sharedInstanceData.materialIDArrays,
                    m_BatchersContext.sharedInstanceData.rendererGroupIDs, ref unsupportedRenderers);
            }

            return unsupportedRenderers;
        }

        private NativeHashSet<EntityId> GetMaterialsWithChangedPackedMaterial(NativeArray<EntityId> materials, NativeArray<GPUDrivenPackedMaterialData> packedMaterialDatas, Allocator allocator)
        {
            NativeHashSet<EntityId> filteredMaterials = new NativeHashSet<EntityId>(materials.Length, allocator);

            GPUResidentDrawerBurst.GetMaterialsWithChangedPackedMaterial(materials, packedMaterialDatas,
                batcher.instanceCullingBatcher.packedMaterialHash.AsReadOnly(), ref filteredMaterials);

            return filteredMaterials;
        }

        private (NativeList<EntityId> renderersWithMaterials, NativeList<EntityId> renderersWithMeshes) FindRenderersFromMaterialsOrMeshes(NativeArray<EntityId> sortedExcludeRenderers, NativeHashSet<EntityId> materials, NativeArray<EntityId> meshes, Allocator rendererListAllocator)
        {
            var sharedInstanceData = m_BatchersContext.sharedInstanceData;
            var renderersWithMaterials = new NativeList<EntityId>(sharedInstanceData.rendererGroupIDs.Length, rendererListAllocator);
            var renderersWithMeshes = new NativeList<EntityId>(sharedInstanceData.rendererGroupIDs.Length, rendererListAllocator);

            var jobHandle = new FindRenderersFromMaterialOrMeshJob
            {
                materialIDs = materials.AsReadOnly(),
                materialIDArrays = sharedInstanceData.materialIDArrays,
                meshIDs = meshes.AsReadOnly(),
                meshIDArray = sharedInstanceData.meshIDs,
                rendererGroupIDs = sharedInstanceData.rendererGroupIDs,
                sortedExcludeRendererIDs = sortedExcludeRenderers.AsReadOnly(),
                selectedRenderGroupsForMaterials = renderersWithMaterials.AsParallelWriter(),
                selectedRenderGroupsForMeshes = renderersWithMeshes.AsParallelWriter()
            }.ScheduleBatch(sharedInstanceData.rendererGroupIDs.Length, FindRenderersFromMaterialOrMeshJob.k_BatchSize);
            jobHandle.Complete();

            return (renderersWithMaterials, renderersWithMeshes);
        }

        [BurstCompile(DisableSafetyChecks = true, OptimizeFor = OptimizeFor.Performance)]
        private unsafe struct FindRenderersFromMaterialOrMeshJob : IJobParallelForBatch
        {
            public const int k_BatchSize = 128;

            [ReadOnly] public NativeHashSet<EntityId>.ReadOnly materialIDs;
            [ReadOnly] public NativeArray<SmallEntityIdArray>.ReadOnly materialIDArrays;
            [ReadOnly] public NativeArray<EntityId>.ReadOnly meshIDs;
            [ReadOnly] public NativeArray<EntityId>.ReadOnly meshIDArray;
            [ReadOnly] public NativeArray<EntityId>.ReadOnly rendererGroupIDs;
            [ReadOnly] public NativeArray<EntityId>.ReadOnly sortedExcludeRendererIDs;

            [WriteOnly] public NativeList<EntityId>.ParallelWriter selectedRenderGroupsForMaterials;
            [WriteOnly] public NativeList<EntityId>.ParallelWriter selectedRenderGroupsForMeshes;

            public void Execute(int startIndex, int count)
            {
                EntityId* renderersToAddForMaterialsPtr = stackalloc EntityId[k_BatchSize];
                var renderersToAddForMaterials = new UnsafeList<EntityId>(renderersToAddForMaterialsPtr, k_BatchSize);
                renderersToAddForMaterials.Length = 0;

                EntityId* renderersToAddForMeshesPtr = stackalloc EntityId[k_BatchSize];
                var renderersToAddForMeshes = new UnsafeList<EntityId>(renderersToAddForMeshesPtr, k_BatchSize);
                renderersToAddForMeshes.Length = 0;

                for (int index = 0; index < count; index++)
                {
                    var rendererIndex = startIndex + index;
                    var rendererID = rendererGroupIDs[rendererIndex];

                    // We ignore this renderer if it is in the excluded list.
                    if (sortedExcludeRendererIDs.BinarySearch(rendererID) >= 0)
                        continue;

                    {
                        var meshID = meshIDArray[rendererIndex];
                        if (meshIDs.Contains(meshID))
                        {
                            renderersToAddForMeshes.AddNoResize(rendererID);
                            // We can skip the material check if we found a mesh match since at this point
                            // the renderer is already added and will be processed by the mesh branch
                            continue;
                        }
                    }
                    {
                        var rendererMaterials = materialIDArrays[rendererIndex];

                        for (int materialIndex = 0; materialIndex < rendererMaterials.Length; materialIndex++)
                        {
                            var materialID = rendererMaterials[materialIndex];
                            if (materialIDs.Contains(materialID))
                            {

#pragma warning disable 618 // todo @emilie.thaulow make renderID an EntityId
                                renderersToAddForMaterials.AddNoResize(rendererID);
#pragma warning restore 618
                                break;
                            }
                        }
                    }
                }

                selectedRenderGroupsForMaterials.AddRangeNoResize(renderersToAddForMaterialsPtr, renderersToAddForMaterials.Length);
                selectedRenderGroupsForMeshes.AddRangeNoResize(renderersToAddForMeshesPtr, renderersToAddForMeshes.Length);
            }
        }
    }
}