// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. // Potential improvements // - Half return values // - Half minGridSize using System.Collections.Generic; using Unity.Collections; using UnityEngine; using UnityEngine.Rendering; using WaveHarmonic.Crest.Internal; namespace WaveHarmonic.Crest { /// /// Base interface for providers. /// public interface IQueryProvider { // NOTE: Here for documentation reuse. /// Unique ID for calling code. Typically acquired by calling GetHashCode. /// The minimum spatial length of the object, such as the width of a boat. Useful for filtering out detail when not needed. Set to zero to get full available detail. /// The world space points that will be queried. /// The layer this query targets. /// The center of all the query positions. Used to choose the closest query provider. /// The status of the query. internal static int Query(int hash, float minimumLength, Vector3[] points, int layer, Vector3? center) => throw new System.NotImplementedException("Crest: this method is for documentation reuse only. Do not invoke."); /// /// Check if the query results could be retrieved successfully using the return code /// from Query method. /// /// The query status returned from Query. /// Whether the retrieve was successful. bool RetrieveSucceeded(int status) { return (status & (int)QueryBase.QueryStatus.RetrieveFailed) == 0; } } interface IQueryable { int ResultGuidCount { get; } int RequestCount { get; } int QueryCount { get; } void UpdateQueries(WaterRenderer water); void SendReadBack(WaterRenderer water); void CleanUp(); void Initialize(WaterRenderer water); } interface IQueryableSimple : IQueryable { int Query(int hash, float minimumLength, Vector3[] queries, Vector3[] results, Vector3? center); } /// /// Provides heights and other physical data about the water surface. Works by uploading query positions to GPU and computing /// the data and then transferring back the results asynchronously. An exception to this is water surface velocities - these can /// not be computed on the GPU and are instead computed on the CPU by retaining last frames' query results and computing finite diffs. /// abstract class QueryBase : IQueryable { protected abstract int Kernel { get; } // 4 was enough for a long time, but Linux setups seems to demand 7 const int k_MaximumRequests = 7; const int k_MaximumGuids = 1024; // We need only two additional queries to compute normals. const int k_NormalAdditionalQueryCount = 2; readonly WaterRenderer _Water; readonly IQueryableLod _Lod; readonly PropertyWrapperCompute _Wrapper; readonly System.Action _DataArrivedAction; // Must match value in compute shader const int k_ComputeGroupSize = 64; static class ShaderIDs { public static readonly int s_QueryPositions_MinimumGridSizes = Shader.PropertyToID("_Crest_QueryPositions_MinimumGridSizes"); } const float k_FiniteDifferenceDx = 0.1f; readonly ComputeBuffer _ComputeBufferQueries; readonly ComputeBuffer _ComputeBufferResults; internal const int k_DefaultMaximumQueryCount = 4096; readonly int _MaximumQueryCount; readonly Vector3[] _QueryPositionXZ_MinimumGridSize; /// /// Holds information about all query points. Maps from unique hash code to position in point array. /// sealed class SegmentRegistrar { // Map from guids to (segment start index, segment end index, frame number when query was made) public Dictionary _Segments = new(); public int _QueryCount = 0; } /// /// Since query results return asynchronously and may not return at all (in theory), we keep a ringbuffer /// of the registrars of the last frames so that when data does come back it can be interpreted correctly. /// sealed class SegmentRegistrarRingBuffer { // Requests in flight plus 2 held values, plus one current static readonly int s_PoolSize = k_MaximumRequests + 2 + 1; readonly SegmentRegistrar[] _Segments = new SegmentRegistrar[s_PoolSize]; public int _SegmentRelease = 0; public int _SegmentAcquire = 0; public SegmentRegistrar Current => _Segments[_SegmentAcquire]; public SegmentRegistrarRingBuffer() { for (var i = 0; i < _Segments.Length; i++) { _Segments[i] = new(); } } public void AcquireNew() { var lastIndex = _SegmentAcquire; { var newSegmentAcquire = (_SegmentAcquire + 1) % _Segments.Length; if (newSegmentAcquire == _SegmentRelease) { // The last index has incremented and landed on the first index. This shouldn't happen normally, but // can happen if the Scene and Game view are not visible, in which case async readbacks dont get processed // and the pipeline blocks up. #if !UNITY_EDITOR Debug.LogError("Crest: Query ring buffer exhausted. Please report this to developers."); #endif return; } _SegmentAcquire = newSegmentAcquire; } // Copy the registrations across from the previous frame. This makes queries persistent. This is needed because // queries are often made from FixedUpdate(), and at high framerates this may not be called, which would mean // the query would get lost and this leads to stuttering and other artifacts. { _Segments[_SegmentAcquire]._QueryCount = 0; _Segments[_SegmentAcquire]._Segments.Clear(); foreach (var segment in _Segments[lastIndex]._Segments) { var age = Time.frameCount - segment.Value.z; // Don't keep queries around if they have not be active in the last 10 frames if (age < 10) { // Compute a new segment range - we may have removed some segments that were too old, so this ensures // we have a nice compact array of queries each frame rather than accumulating persistent air bubbles var newSegment = segment.Value; newSegment.x = _Segments[_SegmentAcquire]._QueryCount; newSegment.y = newSegment.x + (segment.Value.y - segment.Value.x); _Segments[_SegmentAcquire]._QueryCount = newSegment.y + 1; _Segments[_SegmentAcquire]._Segments.Add(segment.Key, newSegment); } } } } public void ReleaseLast() { _SegmentRelease = (_SegmentRelease + 1) % _Segments.Length; } public void RemoveRegistrations(int key) { // Remove the guid for all of the next spare segment registrars. However, don't touch the ones that are being // used for active requests. var i = _SegmentAcquire; while (true) { if (_Segments[i]._Segments.ContainsKey(key)) { _Segments[i]._Segments.Remove(key); } i = (i + 1) % _Segments.Length; if (i == _SegmentRelease) { break; } } } public void ClearAvailable() { // Extreme approach - flush all segments for next spare registrars (but don't touch ones being used for active requests) var i = _SegmentAcquire; while (true) { _Segments[i]._Segments.Clear(); _Segments[i]._QueryCount = 0; i = (i + 1) % _Segments.Length; if (i == _SegmentRelease) { break; } } } public void ClearAll() { for (var i = 0; i < _Segments.Length; i++) { _Segments[i]._QueryCount = 0; _Segments[i]._Segments.Clear(); } } } readonly SegmentRegistrarRingBuffer _SegmentRegistrarRingBuffer = new(); NativeArray _QueryResults; float _QueryResultsTime = -1f; Dictionary _ResultSegments; NativeArray _QueryResultsLast; float _QueryResultsTimeLast = -1f; Dictionary _ResultSegmentsLast; struct ReadbackRequest { public AsyncGPUReadbackRequest _Request; public float _DataTimestamp; public Dictionary _Segments; } readonly List _Requests = new(); public enum QueryStatus { OK = 0, RetrieveFailed = 1, PostFailed = 2, NotEnoughDataForVels = 4, VelocityDataInvalidated = 8, InvalidDtForVelocity = 16, } public QueryBase(IQueryableLod lod) { _Water = lod.Water; _Lod = lod; _DataArrivedAction = new(DataArrived); _MaximumQueryCount = lod.MaximumQueryCount; _QueryPositionXZ_MinimumGridSize = new Vector3[_MaximumQueryCount]; _ComputeBufferQueries = new(_MaximumQueryCount, 12, ComputeBufferType.Default); _ComputeBufferResults = new(_MaximumQueryCount, 12, ComputeBufferType.Default); _QueryResults = new(_MaximumQueryCount, Allocator.Persistent, NativeArrayOptions.ClearMemory); _QueryResultsLast = new(_MaximumQueryCount, Allocator.Persistent, NativeArrayOptions.ClearMemory); var shader = WaterResources.Instance.Compute._Query; if (shader == null) { Debug.LogError($"Crest: Could not load Query compute shader"); return; } _Wrapper = new(_Water.SimulationBuffer, shader, Kernel); } void LogMaximumQueryCountExceededError() { Debug.LogError($"Crest: Maximum query count ({_MaximumQueryCount}) exceeded, increase the {nameof(WaterRenderer)} > Simulations > {_Lod.Name} > {nameof(_Lod.MaximumQueryCount)} to support a higher number of queries.", _Water); } /// /// Takes a unique request ID and some world space XZ positions, and computes the displacement vector that lands at this position, /// to a good approximation. The world space height of the water at that position is then SeaLevel + displacement.y. /// protected bool UpdateQueryPoints(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] queryNormals) { if (queryPoints.Length + _SegmentRegistrarRingBuffer.Current._QueryCount > _MaximumQueryCount) { LogMaximumQueryCountExceededError(); return false; } var segmentRetrieved = false; // We'll send in 2 points to get normals var countPts = queryPoints != null ? queryPoints.Length : 0; var countNorms = queryNormals != null ? queryNormals.Length : 0; var countTotal = countPts + countNorms * k_NormalAdditionalQueryCount; if (_SegmentRegistrarRingBuffer.Current._Segments.TryGetValue(ownerHash, out var segment)) { var segmentSize = segment.y - segment.x + 1; if (segmentSize == countTotal) { // Update frame count segment.z = Time.frameCount; _SegmentRegistrarRingBuffer.Current._Segments[ownerHash] = segment; segmentRetrieved = true; } else { _SegmentRegistrarRingBuffer.Current._Segments.Remove(ownerHash); } } if (countTotal == 0) { // No query data return false; } if (!segmentRetrieved) { if (_SegmentRegistrarRingBuffer.Current._Segments.Count >= k_MaximumGuids) { Debug.LogError("Crest: Too many guids registered with CollProviderCompute. Increase s_maxGuids."); return false; } segment.x = _SegmentRegistrarRingBuffer.Current._QueryCount; segment.y = segment.x + countTotal - 1; segment.z = Time.frameCount; _SegmentRegistrarRingBuffer.Current._Segments.Add(ownerHash, segment); _SegmentRegistrarRingBuffer.Current._QueryCount += countTotal; //Debug.Log("Crest: Added points for " + guid); } // The smallest wavelengths should repeat no more than twice across the smaller spatial length. Unless we're // in the last LOD - then this is the best we can do. var minWavelength = minSpatialLength / 2f; var samplesPerWave = 2f; var minGridSize = minWavelength / samplesPerWave; if (countPts + segment.x > _QueryPositionXZ_MinimumGridSize.Length) { LogMaximumQueryCountExceededError(); return false; } for (var pointi = 0; pointi < countPts; pointi++) { _QueryPositionXZ_MinimumGridSize[pointi + segment.x].x = queryPoints[pointi].x; _QueryPositionXZ_MinimumGridSize[pointi + segment.x].y = queryPoints[pointi].z; _QueryPositionXZ_MinimumGridSize[pointi + segment.x].z = minGridSize; } // To compute each normal, post 2 query points (reuse point above) for (var normi = 0; normi < countNorms; normi++) { var arrIdx = segment.x + countPts + k_NormalAdditionalQueryCount * normi; _QueryPositionXZ_MinimumGridSize[arrIdx].x = queryNormals[normi].x + k_FiniteDifferenceDx; _QueryPositionXZ_MinimumGridSize[arrIdx].y = queryNormals[normi].z; _QueryPositionXZ_MinimumGridSize[arrIdx].z = minGridSize; arrIdx += 1; _QueryPositionXZ_MinimumGridSize[arrIdx].x = queryNormals[normi].x; _QueryPositionXZ_MinimumGridSize[arrIdx].y = queryNormals[normi].z + k_FiniteDifferenceDx; _QueryPositionXZ_MinimumGridSize[arrIdx].z = minGridSize; } return true; } /// /// Signal that we're no longer servicing queries. Note this leaves an air bubble in the query buffer. /// void RemoveQueryPoints(int guid) { _SegmentRegistrarRingBuffer.RemoveRegistrations(guid); } /// /// Remove air bubbles from the query array. Currently this lazily just nukes all the registered /// query IDs so they'll be recreated next time (generating garbage). /// void CompactQueryStorage() { _SegmentRegistrarRingBuffer.ClearAvailable(); } /// /// Copy out displacements, heights, normals. Pass null if info is not required. /// protected bool RetrieveResults(int guid, Vector3[] displacements, float[] heights, Vector3[] normals) { if (_ResultSegments == null) { return false; } // Check if there are results that came back for this guid if (!_ResultSegments.TryGetValue(guid, out var segment)) { // Guid not found - no result return false; } var countPoints = 0; if (displacements != null) countPoints = displacements.Length; if (heights != null) countPoints = heights.Length; if (displacements != null && heights != null) Debug.Assert(displacements.Length == heights.Length); var countNorms = normals != null ? normals.Length : 0; if (countPoints > 0) { // Retrieve Results if (displacements != null) _QueryResults.Slice(segment.x, countPoints).CopyTo(displacements); // Retrieve Result heights if (heights != null) { var seaLevel = _Water.SeaLevel; for (var i = 0; i < countPoints; i++) { heights[i] = seaLevel + _QueryResults[i + segment.x].y; } } } if (countNorms > 0) { var firstNorm = segment.x + countPoints; var dx = -Vector3.right * k_FiniteDifferenceDx; var dz = -Vector3.forward * k_FiniteDifferenceDx; for (var i = 0; i < countNorms; i++) { var p = _QueryResults[i + segment.x]; var px = dx + _QueryResults[firstNorm + k_NormalAdditionalQueryCount * i]; var pz = dz + _QueryResults[firstNorm + k_NormalAdditionalQueryCount * i + 1]; normals[i] = Vector3.Cross(p - px, p - pz).normalized; normals[i].y *= -1f; } } return true; } /// /// Compute time derivative of the displacements by calculating difference from last query. More complicated than it would seem - results /// may not be available in one or both of the results, or the query locations in the array may change. /// protected int CalculateVelocities(int ownerHash, Vector3[] results) { // Need at least 2 returned results to do finite difference if (_QueryResultsTime < 0f || _QueryResultsTimeLast < 0f) { return 1; } if (!_ResultSegments.TryGetValue(ownerHash, out var segment)) { return (int)QueryStatus.RetrieveFailed; } if (!_ResultSegmentsLast.TryGetValue(ownerHash, out var segmentLast)) { return (int)QueryStatus.NotEnoughDataForVels; } if ((segment.y - segment.x) != (segmentLast.y - segmentLast.x)) { // Number of queries changed - can't handle that return (int)QueryStatus.VelocityDataInvalidated; } var dt = _QueryResultsTime - _QueryResultsTimeLast; if (dt < 0.0001f) { return (int)QueryStatus.InvalidDtForVelocity; } var count = results.Length; for (var i = 0; i < count; i++) { results[i] = (_QueryResults[i + segment.x] - _QueryResultsLast[i + segmentLast.x]) / dt; } return 0; } /// /// Per-frame update callback. /// /// The current . public void UpdateQueries(WaterRenderer water) { #if UNITY_EDITOR // Seems to be a terrible memory leak coming from creating async GPU readbacks. // This was marked as resolved by Unity and confirmed fixed by forum posts. // May be worth keeping. See issue #630 for more details. if (!water._HeightQueries && !Application.isPlaying) return; #endif if (_SegmentRegistrarRingBuffer.Current._QueryCount > 0) { ExecuteQueries(); } } public void SendReadBack(WaterRenderer water) { #if UNITY_EDITOR // Seems to be a terrible memory leak coming from creating async GPU readbacks. // This was marked as resolved by Unity and confirmed fixed by forum posts. // May be worth keeping. See issue #630 for more details. if (!water._HeightQueries && !Application.isPlaying) return; #endif if (_SegmentRegistrarRingBuffer.Current._QueryCount > 0) { // Remove oldest requests if we have hit the limit while (_Requests.Count >= k_MaximumRequests) { _Requests.RemoveAt(0); } ReadbackRequest request; request._DataTimestamp = Time.time - Time.deltaTime; request._Request = AsyncGPUReadback.Request(_ComputeBufferResults, _DataArrivedAction); request._Segments = _SegmentRegistrarRingBuffer.Current._Segments; _Requests.Add(request); _SegmentRegistrarRingBuffer.AcquireNew(); } } void ExecuteQueries() { _ComputeBufferQueries.SetData(_QueryPositionXZ_MinimumGridSize, 0, 0, _SegmentRegistrarRingBuffer.Current._QueryCount); _Wrapper.SetBuffer(ShaderIDs.s_QueryPositions_MinimumGridSizes, _ComputeBufferQueries); _Wrapper.SetBuffer(Crest.ShaderIDs.s_Target, _ComputeBufferResults); var numGroups = (_SegmentRegistrarRingBuffer.Current._QueryCount + k_ComputeGroupSize - 1) / k_ComputeGroupSize; _Wrapper.Dispatch(numGroups, 1, 1); } /// /// Called when a compute buffer has been read back from the GPU to the CPU. /// void DataArrived(AsyncGPUReadbackRequest req) { // Can get callbacks after disable, so detect this. if (!_QueryResults.IsCreated) { _Requests.Clear(); return; } // Remove any error requests for (var i = _Requests.Count - 1; i >= 0; --i) { if (_Requests[i]._Request.hasError) { _Requests.RemoveAt(i); _SegmentRegistrarRingBuffer.ReleaseLast(); } } // Find the last request that was completed var lastDoneIndex = _Requests.Count - 1; while (lastDoneIndex >= 0 && !_Requests[lastDoneIndex]._Request.done) { --lastDoneIndex; } // If there is a completed request, process it if (lastDoneIndex >= 0) { // Update "last" results (_QueryResults, _QueryResultsLast) = (_QueryResultsLast, _QueryResults); _QueryResultsTimeLast = _QueryResultsTime; _ResultSegmentsLast = _ResultSegments; var data = _Requests[lastDoneIndex]._Request.GetData(); data.CopyTo(_QueryResults); _QueryResultsTime = _Requests[lastDoneIndex]._DataTimestamp; _ResultSegments = _Requests[lastDoneIndex]._Segments; } // Remove all the requests up to the last completed one for (var i = lastDoneIndex; i >= 0; --i) { _Requests.RemoveAt(i); _SegmentRegistrarRingBuffer.ReleaseLast(); } } /// /// On destroy, to clean up resources. /// public void CleanUp() { _ComputeBufferQueries.Dispose(); _ComputeBufferResults.Dispose(); if (_QueryResults.IsCreated) _QueryResults.Dispose(); if (_QueryResultsLast.IsCreated) _QueryResultsLast.Dispose(); _SegmentRegistrarRingBuffer.ClearAll(); } public virtual void Initialize(WaterRenderer water) { } public int ResultGuidCount => _ResultSegments != null ? _ResultSegments.Count : 0; public int RequestCount => _Requests != null ? _Requests.Count : 0; public int QueryCount => _SegmentRegistrarRingBuffer != null ? _SegmentRegistrarRingBuffer.Current._QueryCount : 0; } abstract class QueryBaseSimple : QueryBase, IQueryableSimple { protected QueryBaseSimple(IQueryableLod lod) : base(lod) { } public virtual int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] results, Vector3? center) { var result = (int)QueryStatus.OK; if (!UpdateQueryPoints(ownerHash, minSpatialLength, queryPoints, null)) { result |= (int)QueryStatus.PostFailed; } if (!RetrieveResults(ownerHash, results, null, null)) { result |= (int)QueryStatus.RetrieveFailed; } return result; } } abstract class QueryPerCamera : IQueryable where T : IQueryable, new() { internal readonly WaterRenderer _Water; internal readonly Dictionary _Providers = new(); public QueryPerCamera(WaterRenderer water) { _Water = water; Initialize(water); } public int ResultGuidCount { get { var total = 0; foreach (var (camera, provider) in _Providers) { if (_Water.ShouldExecuteQueries(camera)) total += provider.ResultGuidCount; } return total; } } public int RequestCount { get { var total = 0; foreach (var (camera, provider) in _Providers) { if (_Water.ShouldExecuteQueries(camera)) total += provider.RequestCount; } return total; } } public int QueryCount { get { var total = 0; foreach (var (camera, provider) in _Providers) { if (_Water.ShouldExecuteQueries(camera)) total += provider.QueryCount; } return total; } } public void CleanUp() { foreach (var provider in _Providers.Values) { provider?.CleanUp(); } } public void Initialize(WaterRenderer water) { var camera = water.CurrentCamera; if (camera == null) { camera = water.Viewer; } if (camera == null) { return; } if (!_Providers.ContainsKey(camera)) { // Cannot use parameters. We could use System.Activator.CreateInstance to get // around that, but instead we just use WaterRenderer.Instance. _Providers.Add(camera, new()); } } public void SendReadBack(WaterRenderer water) { _Providers[water.CurrentCamera].SendReadBack(water); } public void UpdateQueries(WaterRenderer water) { _Providers[water.CurrentCamera].UpdateQueries(water); } public Vector2 FindCenter(Vector3[] queries, Vector3? center) { if (center != null) { return ((Vector3)center).XZ(); } // Calculate center if none provided. var sum = Vector2.zero; foreach (var point in queries) { sum += point.XZ(); } return new(sum.x / queries.Length, sum.y / queries.Length); } } abstract class QueryPerCameraSimple : QueryPerCamera, IQueryableSimple where T : IQueryableSimple, new() { protected QueryPerCameraSimple(WaterRenderer water) : base(water) { } public int Query(int id, float length, Vector3[] queries, Vector3[] results, Vector3? center = null) { if (_Water._InCameraLoop) { return _Providers[_Water.CurrentCamera].Query(id, length, queries, results, center); } var lastStatus = -1; var lastDistance = Mathf.Infinity; var newCenter = FindCenter(queries, center); foreach (var provider in _Providers) { var camera = provider.Key; if (!_Water.ShouldExecuteQueries(camera)) { continue; } var distance = (newCenter - camera.transform.position.XZ()).sqrMagnitude; if (lastStatus == (int)QueryBase.QueryStatus.OK && lastDistance < distance) { continue; } var status = provider.Value.Query(id, length, queries, results, center); if (lastStatus < 0 || status == (int)QueryBase.QueryStatus.OK) { lastStatus = status; lastDistance = distance; } } return lastStatus; } } static partial class Extensions { public static void UpdateQueries(this IQueryProvider self, WaterRenderer water) => (self as IQueryable)?.UpdateQueries(water); public static void CleanUp(this IQueryProvider self) => (self as IQueryable)?.CleanUp(); } }