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