// Stylized Water 3 by Staggart Creations (http://staggart.xyz) // COPYRIGHT PROTECTED UNDER THE UNITY ASSET STORE EULA (https://unity.com/legal/as-terms) // • Copying or referencing source code for the production of new asset store, or public, content is strictly prohibited! // • Uploading this file to a public repository will subject it to an automated DMCA takedown request. using System; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.Rendering; using Unity.Mathematics; namespace StylizedWater3 { public static partial class HeightQuerySystem { public class Query { /// /// Maximum allowed number of sample positions allowed within a single height query /// A value too high may result in decreased performance, as the data payload needed to be retrieved from the GPU becomes too large. /// public const int MAX_SIZE = 128; //Input private NativeArray samplePositions; public readonly GraphicsBuffer inputPositionBuffer; //Output public NativeArray outputOffsets; public readonly GraphicsBuffer outputOffsetsBuffer; private int currentBufferIndex = 0; //Need to keep a buffer alive so it can be used the next frame private readonly NativeArray[] readbackBuffers = new NativeArray[2]; //Every request is issued with an ID public readonly Dictionary requests = new Dictionary(); //Index of the last request. public int sampleCount; private bool hasPendingRequest; public List availableIndices; public Query() { RecreateIndexPool(); //CPU input samplePositions = new NativeArray(MAX_SIZE, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); //GPU input inputPositionBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, MAX_SIZE, 3 * sizeof(float)); inputPositionBuffer.name = "Water Height Query: Sample Positions"; //GPU output outputOffsetsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, MAX_SIZE, sizeof(float)); inputPositionBuffer.name = "Water Height Query: Sampled Heights"; } public int GetNextAvailableIndex() { var index = availableIndices[0]; //Remove as it is now no longer available availableIndices.Remove(index); return index; } public void ReleaseIndex(int index) { availableIndices.Add(index); //Sorting optimizes the array occupancy availableIndices.Sort(); } private void RecreateIndexPool() { //Populate pool of available indices availableIndices = new List(); for (int i = 0; i < MAX_SIZE; i++) { availableIndices.Add(i); } } //Combine all the positions from the requests into one list private void PopulateSampleList() { //Copy all the sample positions in the queue to the summed array foreach (KeyValuePair request in requests) { for (int i = 0; i < request.Value.indices.Count; i++) { int index = request.Value.indices[i]; //NOTE: Setting items of a NativeArray is slow samplePositions[index] = request.Value.sampler.positions[i]; } } //Note: unused indices are not initialized. //Compute shader is configured not to sample them. Doing so otherwise causes VRAM corruption on Metal. } //Dispatch the compute shader. The "offsets" array will be populated based on the current GPU buffer contents. public void Dispatch(ComputeCommandBuffer cmd, ComputeShader cs, int kernel) { Profiler.BeginSample($"{PROFILER_PREFIX} Setup and dispatch"); PopulateSampleList(); cmd.SetBufferData(inputPositionBuffer, samplePositions); cmd.SetComputeIntParam(cs, "sampleCount", sampleCount); cmd.SetComputeBufferParam(cs, kernel, "positions", inputPositionBuffer); //Output cmd.SetComputeBufferParam(cs, kernel, "offsets", outputOffsetsBuffer); cmd.DispatchCompute(cs, kernel, RenderPass.THREAD_GROUPS, 1, 1); Profiler.EndSample(); } void ValidateNativeBuffer(ref NativeArray buffer) { if (!buffer.IsCreated || buffer.Length != MAX_SIZE) { if (buffer.IsCreated) buffer.Dispose(); buffer = new NativeArray(MAX_SIZE, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); } } public void Readback(UnsafeCommandBuffer cmd) { //Array was disposed of after readback request. Forced to recreate it //https://forum.unity.com/threads/asyncgpureadback-requestintonativearray-causes-invalidoperationexception-on-nativearray.1011955 //AllocateReadbackBuffer(); //Query may have been disposed, but an async readback was still pending... if (hasPendingRequest) { return; } hasPendingRequest = true; //After the readback request is complete Unity will dispose of this array automatically. Possibly when 'GetData' is called. //Hence a swap-buffer method is employed ValidateNativeBuffer(ref readbackBuffers[0]); ValidateNativeBuffer(ref readbackBuffers[1]); NativeArray nextBuffer = readbackBuffers[NextBufferIndex()]; #if UNITY_EDITOR //Unity dev: Remove when bug is fixed AtomicSafetyHandle ash = NativeArrayUnsafeUtility.GetAtomicSafetyHandle(nextBuffer); AtomicSafetyHandle.CheckReadAndThrow(ash); AtomicSafetyHandle.CheckDeallocateAndThrow(ash); #endif cmd.RequestAsyncReadbackIntoNativeArray(ref nextBuffer, outputOffsetsBuffer, MAX_SIZE * outputOffsetsBuffer.stride, 0, OnCompleteReadback); } private void SwapCurrentBuffer() { currentBufferIndex = (currentBufferIndex + 1) % 2; } int NextBufferIndex() { //return 0; return (currentBufferIndex + 1) % 2; } private void OnCompleteReadback(AsyncGPUReadbackRequest asyncGPUReadbackRequest) { if (asyncGPUReadbackRequest.hasError) { throw new Exception("Error reading GPU water height with AsyncGPUReadbackRequest."); } Profiler.BeginSample($"{PROFILER_PREFIX} Readback data"); outputOffsets = asyncGPUReadbackRequest.GetData(); foreach (KeyValuePair m_request in requests) { AsyncRequest request = m_request.Value; int queryLength = request.indices.Count; for (int i = 0; i < queryLength; i++) { //List of indices this request occupies in the query int index = request.indices[i]; var waterHeight = outputOffsets[index]; //Height value equals a void do not assign it if (request.invalidateMisses && EqualsVoid(waterHeight)) { //Debug.Log($"Height request for {request.label} at index {index} was invalidated (value={waterHeight})."); continue; } request.sampler.heightValues[i] = waterHeight; } //Issue a callback event for the external scripts that issued the request request.InvokeCallback(); } outputOffsets.Dispose(); SwapCurrentBuffer(); hasPendingRequest = false; Profiler.EndSample(); } public void Clear() { foreach (KeyValuePair request in requests) { request.Value.sampler.Dispose(); } requests.Clear(); RecreateIndexPool(); } public void Dispose() { Clear(); //Remove itself from the list of queries queries.Remove(this); QueryCount--; //Wait before we freeing the resources if (hasPendingRequest) AsyncGPUReadback.WaitAllRequests(); samplePositions.Dispose(); inputPositionBuffer.Dispose(); //Dispose any allocated arrays outputOffsetsBuffer.Dispose(); if (readbackBuffers[0].IsCreated) readbackBuffers[0].Dispose(); if (readbackBuffers[1].IsCreated) readbackBuffers[1].Dispose(); //If the query is being disposed, whilst no readback request was pending then this array would not be allocated if(outputOffsets.IsCreated) outputOffsets.Dispose(); } } } }