using System; using System.Collections.Generic; using System.Threading; using Unity.Collections; using UnityEngine; using UnityEngine.Rendering; namespace Assets.ThoMagic.Renderer { public static class RendererPool { public const int RESIZE_COUNT = 100000; public const int STREAMOUT_SIZE = 10000; public static bool isDisposed = true; private static ComputeShader instancingShader = null; private static CommandBuffer commandBuffer = null; private static int uploadInstancesShaderId = 0, resizeInstanceBufferShaderId = 0; private static object threadSecurity = new object(); private static int maxInstanceCount = 0; private static int instanceUploadCount = 0; private static readonly Dictionary cameras = new Dictionary(); public static readonly Dictionary objects = new Dictionary(); public static readonly Dictionary streamers = new Dictionary(); public static readonly List objectsList = new List(); public static readonly List prefabData = new List(); public static NativeArray uploadInstancesArray; public static readonly Queue uploadInstancesArrayQueue = new Queue(); public static readonly Queue prefabDataFreeSlot = new Queue(); public static readonly Queue instanceBufferFreeSlot = new Queue(); public static ComputeBuffer globalUpdateInstanceBuffer = null; public static ComputeBuffer globalInstanceBuffer = null; public static ComputeBuffer tempGlobalInstanceBuffer = null; private static Vector3 _origin; private static List _keysToRemove = new List(); private static bool _instanceCountChanged; public static bool externInstanceCountChanged { set { if (value == true) { foreach (var camera in cameras.Values) { camera.instanceCountChanged = value; } } } } public static bool instanceCountChanged { set { if (value == true && !_instanceCountChanged) { _instanceCountChanged = true; foreach (var camera in cameras.Values) { camera.instanceCountChanged = value; } } } } public static bool rebuildPrefabs { set { if (value == true) { foreach (var camera in cameras.Values) { camera.rebuildPrefabs = value; } } } } public static void Destroy() { if (isDisposed) return; isDisposed = true; _keysToRemove.Clear(); foreach (KeyValuePair camera in cameras) { _keysToRemove.Add(camera.Key); } foreach (int id in _keysToRemove) RemoveCamera(id); maxInstanceCount = 0; instanceUploadCount = 0; uploadInstancesArrayQueue.Clear(); globalInstanceBuffer?.Dispose(); globalInstanceBuffer = null; globalUpdateInstanceBuffer?.Dispose(); globalUpdateInstanceBuffer = null; tempGlobalInstanceBuffer?.Dispose(); tempGlobalInstanceBuffer = null; commandBuffer?.Dispose(); commandBuffer = null; prefabDataFreeSlot.Clear(); instanceBufferFreeSlot.Clear(); _keysToRemove.Clear(); InstanceStreamer.nextStreamerId = 1; CameraRenderer.nextCameraRendererId = 1; uploadInstancesArray.Dispose(); } internal static void BuildBuffers() { Monitor.Enter(threadSecurity); bool deleteOldBuffer = false; //Load shader TODO: (Move this to init?) if(instancingShader == null) { uploadInstancesArray = new NativeArray(STREAMOUT_SIZE, Allocator.Persistent); instancingShader = UnityEngine.Object.Instantiate(Resources.Load("ThoMagic Renderer Instancing")); instancingShader.hideFlags = HideFlags.HideAndDontSave; uploadInstancesShaderId = instancingShader.FindKernel("Upload_64"); resizeInstanceBufferShaderId = instancingShader.FindKernel("Resize_64"); instancingShader.name = $"ThoMagic Renderer Instancing - RendererPool"; commandBuffer = new CommandBuffer(); } commandBuffer.Clear(); //Are there queued instances ready for upload no current upload is planned? if (instanceUploadCount == 0 && uploadInstancesArrayQueue.Count > 0) { int take = Math.Min(STREAMOUT_SIZE, uploadInstancesArrayQueue.Count); for (int i = 0; i < take; i++) { uploadInstancesArray[instanceUploadCount++] = uploadInstancesArrayQueue.Dequeue(); } instanceCountChanged = true; } //Upload instances if (_instanceCountChanged && maxInstanceCount > 0) { //Create update buffer if null. TODO: Move to init? if(globalUpdateInstanceBuffer == null) { globalUpdateInstanceBuffer = new ComputeBuffer(STREAMOUT_SIZE, InstanceData.Stride, ComputeBufferType.Structured);//, ComputeBufferMode.SubUpdates); } //Resize and copy global instance buffer in gpu if (globalInstanceBuffer == null || globalInstanceBuffer.count < maxInstanceCount) { int nextCount = RESIZE_COUNT * ((maxInstanceCount - 1) / RESIZE_COUNT + 1); tempGlobalInstanceBuffer = new ComputeBuffer(nextCount, InstanceData.Stride, ComputeBufferType.Structured); tempGlobalInstanceBuffer.name = $"ThoMagic global instance buffer"; Debug.Log($"Upsized globalInstanceBuffer {nextCount.ToString("N")} / {(nextCount * InstanceData.Stride / 1024 / 1024)}mb"); if (globalInstanceBuffer != null) { deleteOldBuffer = true; commandBuffer.SetComputeBufferParam(instancingShader, resizeInstanceBufferShaderId, "tempGlobalInstances", tempGlobalInstanceBuffer); commandBuffer.SetComputeBufferParam(instancingShader, resizeInstanceBufferShaderId, "globalInstances", globalInstanceBuffer); commandBuffer.SetComputeIntParam(instancingShader, "_CountResize", globalInstanceBuffer.count); commandBuffer.DispatchCompute(instancingShader, resizeInstanceBufferShaderId, Mathf.CeilToInt(globalInstanceBuffer.count / 64.0f), 1, 1); /*instancingShader.SetBuffer(resizeInstanceBufferShaderId, "tempGlobalInstances", tempGlobalInstanceBuffer); instancingShader.SetBuffer(resizeInstanceBufferShaderId, "globalInstances", globalInstanceBuffer); instancingShader.SetInt("_Count", globalInstanceBuffer.count); instancingShader.Dispatch(resizeInstanceBufferShaderId, Mathf.CeilToInt(globalInstanceBuffer.count / 64.0f), 1, 1); globalInstanceBuffer.Dispose(); globalInstanceBuffer = tempGlobalInstanceBuffer; tempGlobalInstanceBuffer = null;*/ } else { globalInstanceBuffer = tempGlobalInstanceBuffer; //tempGlobalInstanceBuffer = null; } } else { tempGlobalInstanceBuffer = globalInstanceBuffer; } //Upload globalUpdateInstanceBuffer.SetData(uploadInstancesArray); /*var updateTarget = globalUpdateInstanceBuffer.BeginWrite(0, instanceUploadCount); NativeArray.Copy(uploadInstancesArray, updateTarget, instanceUploadCount); globalUpdateInstanceBuffer.EndWrite(instanceUploadCount);*/ /*instancingShader.SetBuffer(uploadInstancesShaderId, "globalUploadInstances", globalUpdateInstanceBuffer); instancingShader.SetBuffer(uploadInstancesShaderId, "globalInstances", globalInstanceBuffer); instancingShader.SetInt("_Count", instanceUploadCount); instancingShader.Dispatch(uploadInstancesShaderId, Mathf.CeilToInt(instanceUploadCount / 64.0f), 1, 1);*/ commandBuffer.SetComputeBufferParam(instancingShader, uploadInstancesShaderId, "globalUploadInstances", globalUpdateInstanceBuffer); commandBuffer.SetComputeBufferParam(instancingShader, uploadInstancesShaderId, "globalInstances", tempGlobalInstanceBuffer); commandBuffer.SetComputeIntParam(instancingShader, "_CountUpload", instanceUploadCount); commandBuffer.DispatchCompute(instancingShader, uploadInstancesShaderId, Mathf.CeilToInt(instanceUploadCount / 64.0f), 1, 1); Graphics.ExecuteCommandBuffer(commandBuffer); var fence = Graphics.CreateGraphicsFence(GraphicsFenceType.AsyncQueueSynchronisation, SynchronisationStageFlags.ComputeProcessing); Graphics.WaitOnAsyncGraphicsFence(fence); if(deleteOldBuffer) { globalInstanceBuffer.Dispose(); globalInstanceBuffer = tempGlobalInstanceBuffer; tempGlobalInstanceBuffer = null; } instanceUploadCount = 0; _instanceCountChanged = false; } Monitor.Exit(threadSecurity); } #region Instance handling internal static int AddInstance(int objectId, Vector3 pos, Quaternion orientation, float scaleXZ, float scaleY) { if (isDisposed) return -1; Monitor.Enter(threadSecurity); var myId = maxInstanceCount; var objectData = GetObjectData(objectId); objectData.count++; //Take free slot in instance buffer if (instanceBufferFreeSlot.Count > 0) { myId = instanceBufferFreeSlot.Dequeue(); } else { maxInstanceCount++; } //Directly upload if STREAMOUT_SIZE not reached, otherwise queue for upload if (instanceUploadCount < STREAMOUT_SIZE && uploadInstancesArrayQueue.Count == 0) uploadInstancesArray[instanceUploadCount++] = new InstanceData(InstanceData.Add, myId, objectData.prefabId, pos.x, pos.y, pos.z, orientation, scaleXZ, scaleY); else uploadInstancesArrayQueue.Enqueue(new InstanceData(InstanceData.Add, myId, objectData.prefabId, pos.x, pos.y, pos.z, orientation, scaleXZ, scaleY)); instanceCountChanged = true; Monitor.Exit(threadSecurity); return myId; } internal static void RemoveInstance(int objectId, int instanceId) { if (isDisposed) return; Monitor.Enter(threadSecurity); var objectData = GetObjectData(objectId); objectData.count--; //No need to reset instance data because the cullable index is removed, instance data will be reset on reuse. /*if (instanceUploadCount < STREAMOUT_SIZE && uploadInstancesArrayQueue.Count == 0) uploadInstancesArray[instanceUploadCount++] = new InstanceData(InstanceData.Delete, instanceId); else uploadInstancesArrayQueue.Enqueue(new InstanceData(InstanceData.Delete, instanceId));*/ externInstanceCountChanged = true; //Remember free slot in instance buffer instanceBufferFreeSlot.Enqueue(instanceId); Monitor.Exit(threadSecurity); } #endregion #region Camera handling public static IEnumerable GetCameras() { return cameras.Values; } public static int RegisterCamera(Camera camera) { int num = camera != null ? camera.GetHashCode() : throw new ArgumentNullException(nameof(camera)); if (!cameras.ContainsKey(num)) { CameraRenderer cameraRenderer = new CameraRenderer(camera); cameraRenderer.SetFloatingOrigin(in _origin); cameras.Add(num, cameraRenderer); } return num; } public static CameraRenderer GetCamera(int cameraId) { CameraRenderer cameraRenderer; return cameras.TryGetValue(cameraId, out cameraRenderer) ? cameraRenderer : null; } public static bool RemoveCamera(int cameraId) { CameraRenderer cameraRenderer; if (!cameras.TryGetValue(cameraId, out cameraRenderer)) return false; cameraRenderer?.Dispose(); return cameras.Remove(cameraId); } public static void RemoveDestroyedCameras() { _keysToRemove.Clear(); foreach (KeyValuePair camera in cameras) { if (camera.Value.Camera == null) _keysToRemove.Add(camera.Key); } foreach (int id in _keysToRemove) RemoveCamera(id); } #endregion #region Object handling public static int RegisterObject(GameObject gameObject, IInstanceRenderSettings settings, InstanceStreamer source, int ownerHash) { int num = gameObject != null ? /*gameObject.GetHashCode()*/ CalculateContentHash(gameObject) : throw new ArgumentNullException(nameof(gameObject)); if(!streamers.ContainsKey(ownerHash)) streamers.Add(ownerHash, source); if (!objects.ContainsKey(num)) { var obj = new ObjectData(gameObject, settings); objects.Add(num, obj); objectsList.Add(obj); obj.contentHashCache = CalculateContentHash(num, false); } else SetObjectSettings(gameObject, settings); if (!objects[num].Owners.Contains(ownerHash)) objects[num].Owners.Add(ownerHash); streamers[ownerHash].owners.Add(num); return num; } public static void RemoveObject(int objectId, int ownerHash) { if (objects.TryGetValue(objectId, out var objectData)) { objectData.Owners.Remove(ownerHash); if (objectData.Owners.Count == 0) { objects.Remove(objectId); objectsList.Remove(objectData); prefabDataFreeSlot.Enqueue(objectData.prefabId); } } if (streamers.TryGetValue(ownerHash, out var streamer)) { streamer.owners.Remove(objectId); if (streamer.owners.Count == 0) streamers.Remove(ownerHash); } rebuildPrefabs = true; } public static GameObject GetObject(int objectId) { return objects.TryGetValue(objectId, out var objectData) ? objectData.GameObject : null; } public static ObjectData GetObjectData(int objectId) { return objects[objectId]; } public static void SetObjectSettings(GameObject prefab, IInstanceRenderSettings instanceRenderSettings) { int hashCode = prefab.GetHashCode(); ObjectData objectData; if (!objects.TryGetValue(hashCode, out objectData)) return; if ((objectData.Settings == null && instanceRenderSettings != null) || (objectData.Settings != null && instanceRenderSettings == null) || instanceRenderSettings.Settings.GetHashCode() != objectData.Settings.Settings.GetHashCode()) { objectData.Settings = instanceRenderSettings; rebuildPrefabs = true; } } private static int CalculateContentHash(GameObject gameObject) { int num = 13; if (gameObject == null) return num; var lodGroup = gameObject.GetComponent(); LOD[] lodArray; if (lodGroup == null) lodArray = new LOD[1] { new LOD(0.0001f, gameObject.GetComponentsInChildren()) }; else lodArray = lodGroup.GetLODs(); foreach (LOD loD in lodArray) num = HashCode.Combine(num, CalculateContentHash(loD)); return num; } public static int CalculateContentHash(int objectId, bool reload = false) { var obj = objects[objectId]; int num = 13; if (obj.GameObject == null) return num; if (reload) { var lodGroup = obj.GameObject.GetComponent(); LOD[] lodArray; if (lodGroup == null) lodArray = new LOD[1] { new LOD(0.0001f, obj.GameObject.GetComponentsInChildren()) }; else lodArray = lodGroup.GetLODs(); obj.LODs = lodArray; } foreach (LOD loD in obj.LODs) num = HashCode.Combine(num, CalculateContentHash(loD)); return num; } private static int CalculateContentHash(LOD lod) { int num = 13; if (lod.renderers != null) { foreach (var renderer in lod.renderers) { if (renderer == null) { num = HashCode.Combine(num, 13); } else { MeshFilter component = renderer.GetComponent(); num = HashCode.Combine(HashCode.Combine( num, component == null || component.sharedMesh == null ? 13 : component.sharedMesh.GetHashCode(), renderer.shadowCastingMode.GetHashCode(), CalculateContentHash(renderer.sharedMaterials), renderer.motionVectorGenerationMode.GetHashCode(), renderer.receiveShadows.GetHashCode(), renderer.rendererPriority.GetHashCode(), renderer.renderingLayerMask.GetHashCode()), renderer.gameObject.layer.GetHashCode(), renderer.gameObject.transform.localPosition.GetHashCode(), lod.screenRelativeTransitionHeight.GetHashCode()); } } } return num; } private static int CalculateContentHash(Material[] materials) { int num = 13; if (materials != null) { foreach (Material material in materials) num = HashCode.Combine(num, material != null ? material.GetHashCode() : 13); } return num; } public static bool ContentHashChanged(int objectId) { if (!objects.ContainsKey(objectId)) return false; return objects[objectId].contentHashCache != CalculateContentHash(objectId, true); } internal static void Initialize() { isDisposed = false; BuildBuffers(); } #endregion } }