diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/DrawingData.cs | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/DrawingData.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/DrawingData.cs | 1712 |
1 files changed, 1712 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/DrawingData.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/DrawingData.cs new file mode 100644 index 0000000..646edec --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/DrawingData.cs @@ -0,0 +1,1712 @@ +using UnityEngine; +using System.Collections.Generic; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using System; +using Unity.Jobs; +using Unity.Collections; +using Unity.Burst; +using UnityEngine.Rendering; +using System.Diagnostics; +using Unity.Jobs.LowLevel.Unsafe; +using UnityEngine.Profiling; +using System.Linq; + +namespace Pathfinding.Drawing { + using Pathfinding.Drawing.Text; + using Unity.Profiling; + + public static class SharedDrawingData { + /// <summary> + /// Same as Time.time, but not updated as frequently. + /// Used since burst jobs cannot access Time.time. + /// </summary> + public static readonly Unity.Burst.SharedStatic<float> BurstTime = Unity.Burst.SharedStatic<float>.GetOrCreate<DrawingManager, BurstTimeKey>(4); + + private class BurstTimeKey {} + } + + /// <summary> + /// Used to cache drawing data over multiple frames. + /// This is useful as a performance optimization when you are drawing the same thing over multiple consecutive frames. + /// + /// <code> + /// private RedrawScope redrawScope; + /// + /// void Start () { + /// redrawScope = DrawingManager.GetRedrawScope(); + /// using (var builder = DrawingManager.GetBuilder(redrawScope)) { + /// builder.WireSphere(Vector3.zero, 1.0f, Color.red); + /// } + /// } + /// + /// void OnDestroy () { + /// redrawScope.Dispose(); + /// } + /// </code> + /// + /// See: <see cref="DrawingManager.GetRedrawScope"/> + /// </summary> + public struct RedrawScope : System.IDisposable { + // Stored as a GCHandle to allow storing this struct in an unmanaged ECS component or system + internal System.Runtime.InteropServices.GCHandle gizmos; + /// <summary> + /// ID of the scope. + /// Zero means no or invalid scope. + /// </summary> + internal int id; + + static int idCounter = 1; + + /// <summary>True if the scope has been created</summary> + public bool isValid => id != 0; + + internal RedrawScope (DrawingData gizmos, int id) { + this.gizmos = gizmos.gizmosHandle; + this.id = id; + } + + internal RedrawScope (DrawingData gizmos) { + this.gizmos = gizmos.gizmosHandle; + // Should be enough with 4 billion ids before they wrap around. + id = idCounter++; + } + + /// <summary> + /// Everything rendered with this scope and which is not older than one frame is drawn again. + /// This is useful if you for some reason cannot draw some items during a frame (e.g. some asynchronous process is modifying the contents) + /// but you still want to draw the same thing as the last frame to at least draw *something*. + /// + /// Note: The items age will be reset. So the next frame you can call + /// this method again to draw the items yet again. + /// </summary> + internal void Draw () { + if (gizmos.IsAllocated) { + if (gizmos.Target is DrawingData gizmosTarget) gizmosTarget.Draw(this); + } + } + + /// <summary> + /// Stops keeping all previously rendered items alive, and starts a new scope. + /// Equivalent to first calling Dispose on the old scope and then creating a new one. + /// </summary> + public void Rewind () { + GameObject associatedGameObject = null; + if (gizmos.IsAllocated) { + if (gizmos.Target is DrawingData gizmosTarget) associatedGameObject = gizmosTarget.GetAssociatedGameObject(this); + } + Dispose(); + this = DrawingManager.GetRedrawScope(associatedGameObject); + } + + internal void DrawUntilDispose (GameObject associatedGameObject) { + if (gizmos.Target is DrawingData gizmosTarget) gizmosTarget.DrawUntilDisposed(this, associatedGameObject); + } + + /// <summary> + /// Dispose the redraw scope to stop rendering the items. + /// + /// You must do this when you are done with the scope, even if it was never used to actually render anything. + /// The items will stop rendering immediately: the next camera to render will not render the items unless kept alive in some other way. + /// For example items are always rendered at least once. + /// </summary> + public void Dispose () { + if (gizmos.IsAllocated) { + if (gizmos.Target is DrawingData gizmosTarget) gizmosTarget.DisposeRedrawScope(this); + } + gizmos = default; + id = 0; + } + }; + + /// <summary> + /// Helper for drawing Gizmos in a performant way. + /// This is a replacement for the Unity Gizmos class as that is not very performant + /// when drawing very large amounts of geometry (for example a large grid graph). + /// These gizmos can be persistent, so if the data does not change, the gizmos + /// do not need to be updated. + /// + /// How to use + /// - Create a Hasher object and hash whatever data you will be using to draw the gizmos + /// Could be for example the positions of the vertices or something. Just as long as + /// if the gizmos should change, then the hash changes as well. + /// - Check if a cached mesh exists for that hash + /// - If not, then create a Builder object and call the drawing methods until you are done + /// and then call Finalize with a reference to a gizmos class and the hash you calculated before. + /// - Call gizmos.Draw with the hash. + /// - When you are done with drawing gizmos for this frame, call gizmos.FinalizeDraw + /// + /// <code> + /// var a = Vector3.zero; + /// var b = Vector3.one; + /// var color = Color.red; + /// var hasher = DrawingData.Hasher.Create(this); + /// + /// hasher.Add(a); + /// hasher.Add(b); + /// hasher.Add(color); + /// var gizmos = DrawingManager.instance.gizmos; + /// if (!gizmos.Draw(hasher)) { + /// using (var builder = gizmos.GetBuilder(hasher)) { + /// // Ideally something very complex, not just a single line + /// builder.Line(a, b, color); + /// } + /// } + /// </code> + /// </summary> + public class DrawingData { + /// <summary>Combines hashes into a single hash value</summary> + public struct Hasher : IEquatable<Hasher> { + ulong hash; + + public static Hasher NotSupplied => new Hasher { hash = ulong.MaxValue }; + + public static Hasher Create<T>(T init) { + var h = new Hasher(); + + h.Add(init); + return h; + } + + public void Add<T>(T hash) { + // Just a regular hash function. The + 12289 is to make sure that hashing zeros doesn't just produce a zero (and generally that hashing one X doesn't produce a hash of X) + // (with a struct we can't provide default initialization) + this.hash = (1572869UL * this.hash) ^ (ulong)hash.GetHashCode() + 12289; + } + + public ulong Hash { + get { + return hash; + } + } + + public override int GetHashCode () { + return (int)hash; + } + + public bool Equals (Hasher other) { + return hash == other.hash; + } + } + + internal struct ProcessedBuilderData { + public enum Type { + Invalid = 0, + Static, + Dynamic, + Persistent, + } + + public Type type; + public BuilderData.Meta meta; + bool submitted; + + // A single instance of a MeshBuffers struct. + // This needs to be stored in a NativeArray because we will use it as a pointer + // and it needs to be guaranteed to stay in the same position in memory. + public NativeArray<MeshBuffers> temporaryMeshBuffers; + JobHandle buildJob, splitterJob; + public List<MeshWithType> meshes; + + public bool isValid => type != Type.Invalid; + + public struct CapturedState { + public Matrix4x4 matrix; + public Color color; + } + + public struct MeshBuffers { + public UnsafeAppendBuffer splitterOutput, vertices, triangles, solidVertices, solidTriangles, textVertices, textTriangles, capturedState; + public Bounds bounds; + + public MeshBuffers(Allocator allocator) { + splitterOutput = new UnsafeAppendBuffer(0, 4, allocator); + vertices = new UnsafeAppendBuffer(0, 4, allocator); + triangles = new UnsafeAppendBuffer(0, 4, allocator); + solidVertices = new UnsafeAppendBuffer(0, 4, allocator); + solidTriangles = new UnsafeAppendBuffer(0, 4, allocator); + textVertices = new UnsafeAppendBuffer(0, 4, allocator); + textTriangles = new UnsafeAppendBuffer(0, 4, allocator); + capturedState = new UnsafeAppendBuffer(0, 4, allocator); + bounds = new Bounds(); + } + + public void Dispose () { + splitterOutput.Dispose(); + vertices.Dispose(); + triangles.Dispose(); + solidVertices.Dispose(); + solidTriangles.Dispose(); + textVertices.Dispose(); + textTriangles.Dispose(); + capturedState.Dispose(); + } + + static void DisposeIfLarge (ref UnsafeAppendBuffer ls) { + if (ls.Length*3 < ls.Capacity && ls.Capacity > 1024) { + var alloc = ls.Allocator; + ls.Dispose(); + ls = new UnsafeAppendBuffer(0, 4, alloc); + } + } + + public void DisposeIfLarge () { + DisposeIfLarge(ref splitterOutput); + DisposeIfLarge(ref vertices); + DisposeIfLarge(ref triangles); + DisposeIfLarge(ref solidVertices); + DisposeIfLarge(ref solidTriangles); + DisposeIfLarge(ref textVertices); + DisposeIfLarge(ref textTriangles); + DisposeIfLarge(ref capturedState); + } + } + + public unsafe UnsafeAppendBuffer* splitterOutputPtr => & ((MeshBuffers*)temporaryMeshBuffers.GetUnsafePtr())->splitterOutput; + + public void Init (Type type, BuilderData.Meta meta) { + submitted = false; + this.type = type; + this.meta = meta; + + if (meshes == null) meshes = new List<MeshWithType>(); + if (!temporaryMeshBuffers.IsCreated) { + temporaryMeshBuffers = new NativeArray<MeshBuffers>(1, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + temporaryMeshBuffers[0] = new MeshBuffers(Allocator.Persistent); + } + } + + static int SubmittedJobs = 0; + + public void SetSplitterJob (DrawingData gizmos, JobHandle splitterJob) { + this.splitterJob = splitterJob; + if (type == Type.Static) { + var cameraInfo = new GeometryBuilder.CameraInfo(null); + unsafe { + buildJob = GeometryBuilder.Build(gizmos, (MeshBuffers*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(temporaryMeshBuffers), ref cameraInfo, splitterJob); + } + + SubmittedJobs++; + // ScheduleBatchedJobs is expensive, so only do it once in a while + if (SubmittedJobs % 8 == 0) { + MarkerScheduleJobs.Begin(); + JobHandle.ScheduleBatchedJobs(); + MarkerScheduleJobs.End(); + } + } + } + + public void SchedulePersistFilter (int version, int lastTickVersion, float time, int sceneModeVersion) { + if (type != Type.Persistent) throw new System.InvalidOperationException(); + + // If data was from a different game mode then it shouldn't live any longer. + // E.g. editor mode => game mode + if (meta.sceneModeVersion != sceneModeVersion) { + meta.version = -1; + return; + } + + // Guarantee that all drawing commands survive at least one frame + // Don't filter them until they have had the opportunity to be drawn once at least. + // (they may not actually have been drawn because no cameras may be active) + if (meta.version < lastTickVersion || submitted) { + splitterJob.Complete(); + meta.version = version; + + // If the command buffer is empty then this instance should not live longer + var splitterOutput = temporaryMeshBuffers[0].splitterOutput; + if (splitterOutput.Length == 0) { + meta.version = -1; + return; + } + + buildJob.Complete(); + unsafe { + splitterJob = new PersistentFilterJob { + buffer = &((MeshBuffers*)NativeArrayUnsafeUtility.GetUnsafePtr(temporaryMeshBuffers))->splitterOutput, + time = time, + }.Schedule(splitterJob); + } + } + } + + public bool IsValidForCamera (Camera camera, bool allowGizmos, bool allowCameraDefault) { + if (!allowGizmos && meta.isGizmos) return false; + + if (meta.cameraTargets != null) { + return meta.cameraTargets.Contains(camera); + } else { + return allowCameraDefault; + } + } + + public void Schedule (DrawingData gizmos, ref GeometryBuilder.CameraInfo cameraInfo) { + // The job for Static will already have been scheduled in SetSplitterJob + if (type != Type.Static) { + unsafe { + buildJob = GeometryBuilder.Build(gizmos, (MeshBuffers*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(temporaryMeshBuffers), ref cameraInfo, splitterJob); + } + } + } + + public void BuildMeshes (DrawingData gizmos) { + if (type == Type.Static && submitted) return; + buildJob.Complete(); + unsafe { + GeometryBuilder.BuildMesh(gizmos, meshes, (MeshBuffers*)temporaryMeshBuffers.GetUnsafePtr()); + } + submitted = true; + } + + public void CollectMeshes (List<RenderedMeshWithType> meshes) { + var itemMeshes = this.meshes; + var customMeshIndex = 0; + var capturedState = temporaryMeshBuffers[0].capturedState; + var maxCustomMeshes = capturedState.Length / UnsafeUtility.SizeOf<CapturedState>(); + + for (int i = 0; i < itemMeshes.Count; i++) { + Color color; + Matrix4x4 matrix; + int drawOrderIndex; + if ((itemMeshes[i].type & MeshType.Custom) != 0) { + UnityEngine.Assertions.Assert.IsTrue(customMeshIndex < maxCustomMeshes); + + // The color and orientation of custom meshes are stored in the captured state array. + // It is indexed in the same order as the custom meshes in the #meshes list. + unsafe { + var state = *((CapturedState*)capturedState.Ptr + customMeshIndex); + color = state.color; + matrix = state.matrix; + customMeshIndex += 1; + } + // Custom meshes are rendered *after* all similar builders. + // In practice this means all custom meshes are drawn after all dynamic items. + drawOrderIndex = meta.drawOrderIndex + 1; + } else { + // All other meshes use default colors and identity matrices + // since their data is already baked into the vertex colors and positions + color = Color.white; + matrix = Matrix4x4.identity; + drawOrderIndex = meta.drawOrderIndex; + } + meshes.Add(new RenderedMeshWithType { + mesh = itemMeshes[i].mesh, + type = itemMeshes[i].type, + drawingOrderIndex = drawOrderIndex, + color = color, + matrix = matrix, + }); + } + } + + void PoolMeshes (DrawingData gizmos, bool includeCustom) { + if (!isValid) throw new System.InvalidOperationException(); + var outIndex = 0; + for (int i = 0; i < meshes.Count; i++) { + // Custom meshes should only be pooled if the Pool flag is set. + // Otherwise they are supplied by the user and it's up to them how to handle it. + if ((meshes[i].type & MeshType.Custom) == 0 || (includeCustom && (meshes[i].type & MeshType.Pool) != 0)) { + gizmos.PoolMesh(meshes[i].mesh); + } else { + // Retain custom meshes + meshes[outIndex] = meshes[i]; + outIndex += 1; + } + } + meshes.RemoveRange(outIndex, meshes.Count - outIndex); + } + + public void PoolDynamicMeshes (DrawingData gizmos) { + if (type == Type.Static && submitted) return; + PoolMeshes(gizmos, false); + } + + public void Release (DrawingData gizmos) { + if (!isValid) throw new System.InvalidOperationException(); + PoolMeshes(gizmos, true); + // Clear custom meshes too + meshes.Clear(); + type = Type.Invalid; + splitterJob.Complete(); + buildJob.Complete(); + var bufs = this.temporaryMeshBuffers[0]; + bufs.DisposeIfLarge(); + this.temporaryMeshBuffers[0] = bufs; + } + + public void Dispose () { + if (isValid) throw new System.InvalidOperationException(); + splitterJob.Complete(); + buildJob.Complete(); + if (temporaryMeshBuffers.IsCreated) { + temporaryMeshBuffers[0].Dispose(); + temporaryMeshBuffers.Dispose(); + } + } + } + + internal struct SubmittedMesh { + public Mesh mesh; + public bool temporary; + } + + [BurstCompile] + internal struct BuilderData : IDisposable { + public enum State { + Free, + Reserved, + Initialized, + WaitingForSplitter, + WaitingForUserDefinedJob, + } + + public struct Meta { + public Hasher hasher; + public RedrawScope redrawScope1; + public RedrawScope redrawScope2; + public int version; + public bool isGizmos; + /// <summary>Used to invalidate gizmos when the scene mode changes</summary> + public int sceneModeVersion; + public int drawOrderIndex; + public Camera[] cameraTargets; + } + + public struct BitPackedMeta { + uint flags; + + const int UniqueIDBitshift = 17; + const int IsBuiltInFlagIndex = 16; + const int IndexMask = (1 << IsBuiltInFlagIndex) - 1; + const int MaxDataIndex = IndexMask; + public const int UniqueIdMask = (1 << (32 - UniqueIDBitshift)) - 1; + + + public BitPackedMeta (int dataIndex, int uniqueID, bool isBuiltInCommandBuilder) { + // Important to make ensure bitpacking doesn't collide + if (dataIndex > MaxDataIndex) throw new System.Exception("Too many command builders active. Are some command builders not being disposed?"); + UnityEngine.Assertions.Assert.IsTrue(uniqueID <= UniqueIdMask && uniqueID >= 0); + + flags = (uint)(dataIndex | uniqueID << UniqueIDBitshift | (isBuiltInCommandBuilder ? 1 << IsBuiltInFlagIndex : 0)); + } + + public int dataIndex { + get { + return (int)(flags & IndexMask); + } + } + + public int uniqueID { + get { + return (int)(flags >> UniqueIDBitshift); + } + } + + public bool isBuiltInCommandBuilder { + get { + return (flags & (1 << IsBuiltInFlagIndex)) != 0; + } + } + + public static bool operator== (BitPackedMeta lhs, BitPackedMeta rhs) { + return lhs.flags == rhs.flags; + } + + public static bool operator!= (BitPackedMeta lhs, BitPackedMeta rhs) { + return lhs.flags != rhs.flags; + } + + public override bool Equals (object obj) { + if (obj is BitPackedMeta meta) { + return flags == meta.flags; + } + return false; + } + + public override int GetHashCode () { + return (int)flags; + } + } + + public BitPackedMeta packedMeta; + public List<SubmittedMesh> meshes; + public NativeArray<UnsafeAppendBuffer> commandBuffers; + public State state { get; private set; } + // TODO? + public bool preventDispose; + JobHandle splitterJob; + JobHandle disposeDependency; + AllowedDelay disposeDependencyDelay; + System.Runtime.InteropServices.GCHandle disposeGCHandle; + public Meta meta; + + public void Reserve (int dataIndex, bool isBuiltInCommandBuilder) { + if (state != State.Free) throw new System.InvalidOperationException(); + state = BuilderData.State.Reserved; + packedMeta = new BitPackedMeta(dataIndex, (UniqueIDCounter++) & BitPackedMeta.UniqueIdMask, isBuiltInCommandBuilder); + } + + static int UniqueIDCounter = 0; + + public void Init (Hasher hasher, RedrawScope frameRedrawScope, RedrawScope customRedrawScope, bool isGizmos, int drawOrderIndex, int sceneModeVersion) { + if (state != State.Reserved) throw new System.InvalidOperationException(); + + meta = new Meta { + hasher = hasher, + redrawScope1 = frameRedrawScope, + redrawScope2 = customRedrawScope, + isGizmos = isGizmos, + version = 0, // Will be filled in later + drawOrderIndex = drawOrderIndex, + sceneModeVersion = sceneModeVersion, + cameraTargets = null, + }; + + if (meshes == null) meshes = new List<SubmittedMesh>(); + if (!commandBuffers.IsCreated) { +#if UNITY_2022_3_OR_NEWER + commandBuffers = new NativeArray<UnsafeAppendBuffer>(JobsUtility.ThreadIndexCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); +#else + commandBuffers = new NativeArray<UnsafeAppendBuffer>(JobsUtility.MaxJobThreadCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); +#endif + for (int i = 0; i < commandBuffers.Length; i++) commandBuffers[i] = new UnsafeAppendBuffer(0, 4, Allocator.Persistent); + } + + state = State.Initialized; + } + + public unsafe UnsafeAppendBuffer* bufferPtr { + get { + return (UnsafeAppendBuffer*)commandBuffers.GetUnsafePtr(); + } + } + + [BurstCompile] + [AOT.MonoPInvokeCallback(typeof(AnyBuffersWrittenToDelegate))] + unsafe static bool AnyBuffersWrittenTo (UnsafeAppendBuffer* buffers, int numBuffers) { + bool any = false; + + for (int i = 0; i < numBuffers; i++) { + any |= buffers[i].Length > 0; + } + return any; + } + + [BurstCompile] + [AOT.MonoPInvokeCallback(typeof(AnyBuffersWrittenToDelegate))] + unsafe static void ResetAllBuffers (UnsafeAppendBuffer* buffers, int numBuffers) { + for (int i = 0; i < numBuffers; i++) { + buffers[i].Reset(); + } + } + + unsafe delegate bool AnyBuffersWrittenToDelegate(UnsafeAppendBuffer* buffers, int numBuffers); + private readonly unsafe static AnyBuffersWrittenToDelegate AnyBuffersWrittenToInvoke = BurstCompiler.CompileFunctionPointer<AnyBuffersWrittenToDelegate>(AnyBuffersWrittenTo).Invoke; + unsafe delegate void ResetAllBuffersToDelegate(UnsafeAppendBuffer* buffers, int numBuffers); + private readonly unsafe static ResetAllBuffersToDelegate ResetAllBuffersToInvoke = BurstCompiler.CompileFunctionPointer<ResetAllBuffersToDelegate>(ResetAllBuffers).Invoke; + + public void SubmitWithDependency (System.Runtime.InteropServices.GCHandle gcHandle, JobHandle dependency, AllowedDelay allowedDelay) { + state = State.WaitingForUserDefinedJob; + disposeDependency = dependency; + disposeDependencyDelay = allowedDelay; + disposeGCHandle = gcHandle; + } + + public void Submit (DrawingData gizmos) { + if (state != State.Initialized) throw new System.InvalidOperationException(); + + unsafe { + // There are about 128 buffers we need to check and it's faster to do that using Burst + if (meshes.Count == 0 && !AnyBuffersWrittenToInvoke((UnsafeAppendBuffer*)commandBuffers.GetUnsafeReadOnlyPtr(), commandBuffers.Length)) { + // If no buffers have been written to then simply discard this builder + Release(); + return; + } + } + + meta.version = gizmos.version; + + // Command stream + // split to static, dynamic and persistent + // render static + // render dynamic per camera + // render persistent per camera + const int PersistentDrawOrderOffset = 1000000; + var tmpMeta = meta; + // Reserve some buffers. + // We need to set a deterministic order in which things are drawn to avoid flickering. + // The shaders use the z buffer most of the time, but there are still + // things which are not order independent. + // Static stuff is drawn first + tmpMeta.drawOrderIndex = meta.drawOrderIndex*3 + 0; + int staticBuffer = gizmos.processedData.Reserve(ProcessedBuilderData.Type.Static, tmpMeta); + // Dynamic stuff is drawn directly after the static stuff + // Note that any custom meshes will get this draw order index + 1. + tmpMeta.drawOrderIndex = meta.drawOrderIndex*3 + 1; + int dynamicBuffer = gizmos.processedData.Reserve(ProcessedBuilderData.Type.Dynamic, tmpMeta); + // Persistent stuff is always drawn after everything else + tmpMeta.drawOrderIndex = meta.drawOrderIndex + PersistentDrawOrderOffset; + int persistentBuffer = gizmos.processedData.Reserve(ProcessedBuilderData.Type.Persistent, tmpMeta); + + unsafe { + splitterJob = new StreamSplitter { + inputBuffers = commandBuffers, + staticBuffer = gizmos.processedData.Get(staticBuffer).splitterOutputPtr, + dynamicBuffer = gizmos.processedData.Get(dynamicBuffer).splitterOutputPtr, + persistentBuffer = gizmos.processedData.Get(persistentBuffer).splitterOutputPtr, + }.Schedule(); + } + + gizmos.processedData.Get(staticBuffer).SetSplitterJob(gizmos, splitterJob); + gizmos.processedData.Get(dynamicBuffer).SetSplitterJob(gizmos, splitterJob); + gizmos.processedData.Get(persistentBuffer).SetSplitterJob(gizmos, splitterJob); + + if (meshes.Count > 0) { + // Custom meshes may be affected by matrices and colors that are set in the command builders. + // Matrices may in theory be dynamic per camera (though this functionality is not used at the moment). + // The Command.CaptureState commands are marked as Dynamic so captured state will be written to + // the meshBuffers.capturedState array in the #dynamicBuffer. + var customMeshes = gizmos.processedData.Get(dynamicBuffer).meshes; + + // Copy meshes to render + for (int i = 0; i < meshes.Count; i++) customMeshes.Add(new MeshWithType { mesh = meshes[i].mesh, type = MeshType.Solid | MeshType.Custom | (meshes[i].temporary ? MeshType.Pool : 0) }); + meshes.Clear(); + } + + // TODO: Allocate 3 output objects and pipe splitter to them + + // Only meshes valid for all cameras have been submitted. + // Meshes that depend on the specific camera will be submitted just before rendering + // that camera. Line drawing depends on the exact camera. + // In particular when drawing circles different number of segments + // are used depending on the distance to the camera. + state = State.WaitingForSplitter; + } + + public void CheckJobDependency (DrawingData gizmos, bool allowBlocking) { + if (state == State.WaitingForUserDefinedJob && (disposeDependency.IsCompleted || (allowBlocking && disposeDependencyDelay == AllowedDelay.EndOfFrame))) { + disposeDependency.Complete(); + disposeDependency = default; + disposeGCHandle.Free(); + state = State.Initialized; + Submit(gizmos); + } + } + + public void Release () { + if (state == State.Free) throw new System.InvalidOperationException(); + state = BuilderData.State.Free; + ClearData(); + } + + void ClearData () { + // Wait for any jobs that might be running + // This is important to avoid memory corruption bugs + disposeDependency.Complete(); + splitterJob.Complete(); + meta = default; + disposeDependency = default; + preventDispose = false; + meshes.Clear(); + unsafe { + // There are about 128 buffers we need to reset and it's faster to do that using Burst + ResetAllBuffers((UnsafeAppendBuffer*)commandBuffers.GetUnsafePtr(), commandBuffers.Length); + } + } + + public void Dispose () { + if (state == State.WaitingForUserDefinedJob) { + disposeDependency.Complete(); + disposeGCHandle.Free(); + // We would call Submit here, but we are deleting the data anyway, so who cares. + state = State.WaitingForSplitter; + } + + if (state == State.Reserved || state == State.Initialized || state == State.WaitingForUserDefinedJob) { + UnityEngine.Debug.LogError("Drawing data is being destroyed, but a drawing instance is still active. Are you sure you have called Dispose on all drawing instances? This will cause a memory leak!"); + return; + } + + splitterJob.Complete(); + if (commandBuffers.IsCreated) { + for (int i = 0; i < commandBuffers.Length; i++) { + commandBuffers[i].Dispose(); + } + commandBuffers.Dispose(); + } + } + } + + internal struct BuilderDataContainer : IDisposable { + BuilderData[] data; + + public int memoryUsage { + get { + int sum = 0; + if (data != null) { + for (int i = 0; i < data.Length; i++) { + var cmds = data[i].commandBuffers; + for (int j = 0; j < cmds.Length; j++) { + sum += cmds[j].Capacity; + } + unsafe { + sum += data[i].commandBuffers.Length * sizeof(UnsafeAppendBuffer); + } + } + } + return sum; + } + } + + + public BuilderData.BitPackedMeta Reserve (bool isBuiltInCommandBuilder) { + if (data == null) data = new BuilderData[1]; + for (int i = 0; i < data.Length; i++) { + if (data[i].state == BuilderData.State.Free) { + data[i].Reserve(i, isBuiltInCommandBuilder); + return data[i].packedMeta; + } + } + + var newData = new BuilderData[data.Length * 2]; + data.CopyTo(newData, 0); + data = newData; + return Reserve(isBuiltInCommandBuilder); + } + + public void Release (BuilderData.BitPackedMeta meta) { + data[meta.dataIndex].Release(); + } + + public bool StillExists (BuilderData.BitPackedMeta meta) { + int index = meta.dataIndex; + + if (data == null || index >= data.Length) return false; + return data[index].packedMeta == meta; + } + + public ref BuilderData Get (BuilderData.BitPackedMeta meta) { + int index = meta.dataIndex; + + if (data[index].state == BuilderData.State.Free) throw new System.ArgumentException("Data is not reserved"); + if (data[index].packedMeta != meta) throw new System.ArgumentException("This command builder has already been disposed"); + return ref data[index]; + } + + public void DisposeCommandBuildersWithJobDependencies (DrawingData gizmos) { + if (data == null) return; + for (int i = 0; i < data.Length; i++) data[i].CheckJobDependency(gizmos, false); + MarkerAwaitUserDependencies.Begin(); + for (int i = 0; i < data.Length; i++) data[i].CheckJobDependency(gizmos, true); + MarkerAwaitUserDependencies.End(); + } + + public void ReleaseAllUnused () { + if (data == null) return; + for (int i = 0; i < data.Length; i++) { + if (data[i].state == BuilderData.State.WaitingForSplitter) { + data[i].Release(); + } + } + } + + public void Dispose () { + if (data != null) { + for (int i = 0; i < data.Length; i++) data[i].Dispose(); + } + // Ensures calling Dispose multiple times is a NOOP + data = null; + } + } + + internal struct ProcessedBuilderDataContainer { + ProcessedBuilderData[] data; + Dictionary<ulong, List<int> > hash2index; + Stack<int> freeSlots; + Stack<List<int> > freeLists; + + public int memoryUsage { + get { + int sum = 0; + if (data != null) { + for (int i = 0; i < data.Length; i++) { + var bufs = data[i].temporaryMeshBuffers; + for (int j = 0; j < bufs.Length; j++) { + var psum = 0; + psum += bufs[j].textVertices.Capacity; + psum += bufs[j].textTriangles.Capacity; + psum += bufs[j].solidVertices.Capacity; + psum += bufs[j].solidTriangles.Capacity; + psum += bufs[j].vertices.Capacity; + psum += bufs[j].triangles.Capacity; + psum += bufs[j].capturedState.Capacity; + psum += bufs[j].splitterOutput.Capacity; + sum += psum; + UnityEngine.Debug.Log(i + ":" + j + " " + psum); + } + } + } + return sum; + } + } + + public int Reserve (ProcessedBuilderData.Type type, BuilderData.Meta meta) { + if (data == null) { + data = new ProcessedBuilderData[0]; + freeSlots = new Stack<int>(); + freeLists = new Stack<List<int> >(); + hash2index = new Dictionary<ulong, List<int> >(); + } + if (freeSlots.Count == 0) { + var newData = new ProcessedBuilderData[math.max(4, data.Length*2)]; + data.CopyTo(newData, 0); + for (int i = data.Length; i < newData.Length; i++) freeSlots.Push(i); + data = newData; + } + int index = freeSlots.Pop(); + data[index].Init(type, meta); + if (!meta.hasher.Equals(Hasher.NotSupplied)) { + List<int> ls; + if (!hash2index.TryGetValue(meta.hasher.Hash, out ls)) { + if (freeLists.Count == 0) freeLists.Push(new List<int>()); + ls = hash2index[meta.hasher.Hash] = freeLists.Pop(); + } + ls.Add(index); + } + return index; + } + + public ref ProcessedBuilderData Get (int index) { + if (!data[index].isValid) throw new System.ArgumentException(); + return ref data[index]; + } + + void Release (DrawingData gizmos, int i) { + var h = data[i].meta.hasher.Hash; + + if (!data[i].meta.hasher.Equals(Hasher.NotSupplied)) { + if (hash2index.TryGetValue(h, out var ls)) { + ls.Remove(i); + if (ls.Count == 0) { + freeLists.Push(ls); + hash2index.Remove(h); + } + } + } + data[i].Release(gizmos); + freeSlots.Push(i); + } + + public void SubmitMeshes (DrawingData gizmos, Camera camera, int versionThreshold, bool allowGizmos, bool allowCameraDefault) { + if (data == null) return; + MarkerSchedule.Begin(); + var cameraInfo = new GeometryBuilder.CameraInfo(camera); + int c = 0; + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && data[i].meta.version >= versionThreshold && data[i].IsValidForCamera(camera, allowGizmos, allowCameraDefault)) { + c++; + data[i].Schedule(gizmos, ref cameraInfo); + } + } + + MarkerSchedule.End(); + + // Ensure all jobs start to be executed on the worker threads now + JobHandle.ScheduleBatchedJobs(); + + MarkerBuild.Begin(); + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && data[i].meta.version >= versionThreshold && data[i].IsValidForCamera(camera, allowGizmos, allowCameraDefault)) { + data[i].BuildMeshes(gizmos); + } + } + MarkerBuild.End(); + } + + /// <summary> + /// Remove any existing dynamic meshes since we know we will not need them after this frame. + /// We do not remove custom meshes or static ones because those may be kept between frames and cameras. + /// </summary> + public void PoolDynamicMeshes (DrawingData gizmos) { + if (data == null) return; + MarkerPool.Begin(); + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid) { + data[i].PoolDynamicMeshes(gizmos); + } + } + MarkerPool.End(); + } + + public void CollectMeshes (int versionThreshold, List<RenderedMeshWithType> meshes, Camera camera, bool allowGizmos, bool allowCameraDefault) { + if (data == null) return; + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && data[i].meta.version >= versionThreshold && data[i].IsValidForCamera(camera, allowGizmos, allowCameraDefault)) { + data[i].CollectMeshes(meshes); + } + } + } + + public void FilterOldPersistentCommands (int version, int lastTickVersion, float time, int sceneModeVersion) { + if (data == null) return; + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && data[i].type == ProcessedBuilderData.Type.Persistent) { + data[i].SchedulePersistFilter(version, lastTickVersion, time, sceneModeVersion); + } + } + } + + public bool SetVersion (Hasher hasher, int version) { + if (data == null) return false; + + if (hash2index.TryGetValue(hasher.Hash, out var indices)) { + UnityEngine.Assertions.Assert.IsTrue(indices.Count > 0); + for (int id = 0; id < indices.Count; id++) { + var i = indices[id]; + UnityEngine.Assertions.Assert.IsTrue(data[i].isValid); + UnityEngine.Assertions.Assert.AreEqual(data[i].meta.hasher.Hash, hasher.Hash); + data[i].meta.version = version; + } + return true; + } else { + return false; + } + } + + public bool SetVersion (RedrawScope scope, int version) { + if (data == null) return false; + bool found = false; + + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && (data[i].meta.redrawScope1.id == scope.id || data[i].meta.redrawScope2.id == scope.id)) { + data[i].meta.version = version; + found = true; + } + } + return found; + } + + public bool SetCustomScope (Hasher hasher, RedrawScope scope) { + if (data == null) return false; + + if (hash2index.TryGetValue(hasher.Hash, out var indices)) { + UnityEngine.Assertions.Assert.IsTrue(indices.Count > 0); + for (int id = 0; id < indices.Count; id++) { + var i = indices[id]; + UnityEngine.Assertions.Assert.IsTrue(data[i].isValid); + UnityEngine.Assertions.Assert.AreEqual(data[i].meta.hasher.Hash, hasher.Hash); + data[i].meta.redrawScope2 = scope; + } + return true; + } else { + return false; + } + } + + public void ReleaseDataOlderThan (DrawingData gizmos, int version) { + if (data == null) return; + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && data[i].meta.version < version) { + Release(gizmos, i); + } + } + } + + public void ReleaseAllWithHash (DrawingData gizmos, Hasher hasher) { + if (data == null) return; + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid && data[i].meta.hasher.Hash == hasher.Hash) { + Release(gizmos, i); + } + } + } + + public void Dispose (DrawingData gizmos) { + if (data == null) return; + for (int i = 0; i < data.Length; i++) { + if (data[i].isValid) Release(gizmos, i); + data[i].Dispose(); + } + // Ensures calling Dispose multiple times is a NOOP + data = null; + } + } + + [System.Flags] + internal enum MeshType { + Solid = 1 << 0, + Lines = 1 << 1, + Text = 1 << 2, + // Set if the mesh is not a built-in mesh. These may have non-identity matrices set. + Custom = 1 << 3, + // If set for a custom mesh, the mesh will be pooled. + // This is used for temporary custom meshes that are created by ALINE + Pool = 1 << 4, + BaseType = Solid | Lines | Text, + } + + internal struct MeshWithType { + public Mesh mesh; + public MeshType type; + } + + internal struct RenderedMeshWithType { + public Mesh mesh; + public MeshType type; + public int drawingOrderIndex; + // May only be set to non-white if type contains MeshType.Custom + public Color color; + // May only be set to a non-identity matrix if type contains MeshType.Custom + public Matrix4x4 matrix; + } + + internal BuilderDataContainer data; + internal ProcessedBuilderDataContainer processedData; + List<RenderedMeshWithType> meshes = new List<RenderedMeshWithType>(); + List<Mesh> cachedMeshes = new List<Mesh>(); + List<Mesh> stagingCachedMeshes = new List<Mesh>(); +#if USE_RAW_GRAPHICS_BUFFERS + List<Mesh> stagingCachedMeshesDelay = new List<Mesh>(); +#endif + int lastTimeLargestCachedMeshWasUsed = 0; + internal SDFLookupData fontData; + int currentDrawOrderIndex = 0; + + /// <summary> + /// Incremented every time the editor goes from play mode -> edit mode, or edit mode -> play mode. + /// Used to ensure that no WithDuration scopes survive this transition. + /// + /// Normally it is not important, but when Unity's enter play mode settings have reload domain disabled + /// then it can become important since this manager will survive the transition. + /// </summary> + internal int sceneModeVersion = 0; + + /// <summary> + /// Slightly adjusted scene mode version. + /// This takes into account `Application.isPlaying` too. It is possible for <see cref="sceneModeVersion"/> to be modified + /// and then some gizmos are drawn before the actual play mode change happens (with the old Application.isPlaying) mode. + /// + /// More precisely, what could happen without this adjustment is + /// 1. EditorApplication.playModeStateChanged (PlayModeStateChange.ExitingPlayMode) fires which increments sceneModeVersion. + /// 2. A final update loop runs with Application.isPlaying = true. + /// 3. During this loop, a new command builder is created with the new sceneModeVersion and Application.isPlaying=true and is drawn to using a WithDuration scope. + /// 4. The play mode changes to editor mode. + /// 5. The WithDuration scope survives! + /// + /// We cannot increment sceneModeVersion on PlayModeStateChange.ExitedPlayMode (not Exiting) instead, because some gizmos which we want to keep may + /// be drawn before that event fires. Yay, Unity is so helpful. + /// </summary> + int adjustedSceneModeVersion { + get { + return sceneModeVersion + (Application.isPlaying ? 1000 : 0); + } + } + + internal int GetNextDrawOrderIndex () { + currentDrawOrderIndex++; + return currentDrawOrderIndex; + } + + internal void PoolMesh (Mesh mesh) { + // Note: clearing the mesh here will deallocate the vertex/index buffers + // This is not good for performance as it will have to be allocated again (likely with the same size) in the next frame + //mesh.Clear(); + stagingCachedMeshes.Add(mesh); + } + + void SortPooledMeshes () { + // TODO: Is accessing the vertex count slow? + cachedMeshes.Sort((a, b) => b.vertexCount - a.vertexCount); + } + + internal Mesh GetMesh (int desiredVertexCount) { + if (cachedMeshes.Count > 0) { + // Do a binary search to find the smallest cached mesh which is larger or equal to the desired vertex count + // TODO: We should actually compare the byte size of the vertex buffer, not the number of vertices because + // the vertex size can change depending on the mesh attribute layout. + int mn = 0; + int mx = cachedMeshes.Count; + while (mx > mn + 1) { + int mid = (mn+mx)/2; + if (cachedMeshes[mid].vertexCount < desiredVertexCount) { + mx = mid; + } else { + mn = mid; + } + } + + var res = cachedMeshes[mn]; + if (mn == 0) lastTimeLargestCachedMeshWasUsed = version; + cachedMeshes.RemoveAt(mn); + return res; + } else { + var mesh = new Mesh { + hideFlags = HideFlags.DontSave + }; + mesh.MarkDynamic(); + return mesh; + } + } + + internal void LoadFontDataIfNecessary () { + if (fontData.material == null) { + var font = DefaultFonts.LoadDefaultFont(); + fontData.Dispose(); + fontData = new SDFLookupData(font); + } + } + + static float CurrentTime { + get { + return Application.isPlaying ? Time.time : Time.realtimeSinceStartup; + } + } + + static void UpdateTime () { + // Time.time cannot be accessed in the job system, so create a global variable which *can* be accessed. + // It's not updated as frequently, but it's only used for the WithDuration method, so it should be ok + SharedDrawingData.BurstTime.Data = CurrentTime; + } + + /// <summary> + /// Get an empty builder for queuing drawing commands. + /// + /// <code> + /// // Create a new CommandBuilder + /// using (var draw = DrawingManager.GetBuilder()) { + /// // Use the exact same API as the global Draw class + /// draw.WireBox(Vector3.zero, Vector3.one); + /// } + /// </code> + /// See: <see cref="Drawing.CommandBuilder"/> + /// </summary> + /// <param name="renderInGame">If true, this builder will be rendered in standalone games and in the editor even if gizmos are disabled. + /// If false, it will only be rendered in the editor when gizmos are enabled.</param> + public CommandBuilder GetBuilder (bool renderInGame = false) { + UpdateTime(); + return new CommandBuilder(this, Hasher.NotSupplied, frameRedrawScope, default, !renderInGame, false, adjustedSceneModeVersion); + } + + internal CommandBuilder GetBuiltInBuilder (bool renderInGame = false) { + UpdateTime(); + return new CommandBuilder(this, Hasher.NotSupplied, frameRedrawScope, default, !renderInGame, true, adjustedSceneModeVersion); + } + + /// <summary> + /// Get an empty builder for queuing drawing commands. + /// + /// See: <see cref="Drawing.CommandBuilder"/> + /// </summary> + /// <param name="renderInGame">If true, this builder will be rendered in standalone games and in the editor even if gizmos are disabled.</param> + public CommandBuilder GetBuilder (RedrawScope redrawScope, bool renderInGame = false) { + UpdateTime(); + return new CommandBuilder(this, Hasher.NotSupplied, frameRedrawScope, redrawScope, !renderInGame, false, adjustedSceneModeVersion); + } + + /// <summary> + /// Get an empty builder for queuing drawing commands. + /// + /// See: <see cref="Drawing.CommandBuilder"/> + /// </summary> + /// <param name="renderInGame">If true, this builder will be rendered in standalone games and in the editor even if gizmos are disabled.</param> + public CommandBuilder GetBuilder (Hasher hasher, RedrawScope redrawScope = default, bool renderInGame = false) { + // The user is going to rebuild the data with the given hash + // Let's clear the previous data with that hash since we know it is not needed any longer. + // Do not do this if a hash is not given. + if (!hasher.Equals(Hasher.NotSupplied)) DiscardData(hasher); + UpdateTime(); + return new CommandBuilder(this, hasher, frameRedrawScope, redrawScope, !renderInGame, false, adjustedSceneModeVersion); + } + + /// <summary>Material to use for surfaces</summary> + public Material surfaceMaterial; + + /// <summary>Material to use for lines</summary> + public Material lineMaterial; + + /// <summary>Material to use for text</summary> + public Material textMaterial; + + public DrawingSettings.Settings settings; + + public DrawingSettings.Settings settingsRef { + get { + if (settings == null) { + settings = DrawingSettings.DefaultSettings; + } + + return settings; + } + } + + public int version { get; private set; } = 1; + int lastTickVersion; + int lastTickVersion2; + Dictionary<int, GameObject> persistentRedrawScopes = new Dictionary<int, GameObject>(); +#if ALINE_TRACK_REDRAW_SCOPE_LEAKS + Dictionary<int, String> persistentRedrawScopeInfos = new Dictionary<int, String>(); +#endif + internal System.Runtime.InteropServices.GCHandle gizmosHandle; + + public RedrawScope frameRedrawScope; + + public GameObject GetAssociatedGameObject (RedrawScope scope) { + if (persistentRedrawScopes.TryGetValue(scope.id, out var go)) return go; + return null; + } + + struct Range { + public int start; + public int end; + } + + Dictionary<Camera, Range> cameraVersions = new Dictionary<Camera, Range>(); + + internal static readonly ProfilerMarker MarkerScheduleJobs = new ProfilerMarker("ScheduleJobs"); + internal static readonly ProfilerMarker MarkerAwaitUserDependencies = new ProfilerMarker("Await user dependencies"); + internal static readonly ProfilerMarker MarkerSchedule = new ProfilerMarker("Schedule"); + internal static readonly ProfilerMarker MarkerBuild = new ProfilerMarker("Build"); + internal static readonly ProfilerMarker MarkerPool = new ProfilerMarker("Pool"); + internal static readonly ProfilerMarker MarkerRelease = new ProfilerMarker("Release"); + internal static readonly ProfilerMarker MarkerBuildMeshes = new ProfilerMarker("Build Meshes"); + internal static readonly ProfilerMarker MarkerCollectMeshes = new ProfilerMarker("Collect Meshes"); + internal static readonly ProfilerMarker MarkerSortMeshes = new ProfilerMarker("Sort Meshes"); + internal static readonly ProfilerMarker LeakTracking = new ProfilerMarker("RedrawScope Leak Tracking"); + + void DiscardData (Hasher hasher) { + processedData.ReleaseAllWithHash(this, hasher); + } + + internal void OnChangingPlayMode () { + sceneModeVersion++; + +#if UNITY_EDITOR + // If we are in the editor, we schedule a callback to check if any RedrawScope objects were not disposed. + // OnChangingPlayMode will run before the scene is destroyed. So we know that any persistent redraw scopes + // that are alive right now should be destroyed soon. + // We wait a few updates to allow the scene to be destroyed before we check for leaks. + // EditorApplication.delayCall may be called before the scene has actually been destroyed. + // Usually it has, but in particular if the user double-clicks the play button to start and then immediately + // stop the game, then it may run before the scene has been destroyed. + var shouldBeDestroyed = this.persistentRedrawScopes.Keys.ToArray(); + UnityEditor.EditorApplication.CallbackFunction checkLeaks = null; + int remainingFrames = 2; + checkLeaks = () => { + if (remainingFrames > 0) { + remainingFrames--; + return; + } + UnityEditor.EditorApplication.delayCall -= checkLeaks; + int leaked = 0; + foreach (var v in shouldBeDestroyed) { + if (persistentRedrawScopes.ContainsKey(v)) leaked++; + } + if (leaked > 0) { +#if ALINE_TRACK_REDRAW_SCOPE_LEAKS + UnityEngine.Debug.LogError(leaked + " RedrawScope objects were not disposed. Make sure to dispose them when you are done with them, otherwise this will lead to a memory leak and potentially a performance issue."); + foreach (var v in shouldBeDestroyed) { + if (persistentRedrawScopes.ContainsKey(v)) { + UnityEngine.Debug.LogError("RedrawScope leaked. Allocated from:\n" + persistentRedrawScopeInfos[v]); + } + } +#else + UnityEngine.Debug.LogError(leaked + " RedrawScope objects were not disposed. Make sure to dispose them when you are done with them, otherwise this will lead to a memory leak and potentially a performance issue.\nEnable ALINE_TRACK_REDRAW_SCOPE_LEAKS in the scripting define symbols to track the leaks more accurately."); +#endif + foreach (var v in shouldBeDestroyed) { + persistentRedrawScopes.Remove(v); +#if ALINE_TRACK_REDRAW_SCOPE_LEAKS + persistentRedrawScopeInfos.Remove(v); +#endif + } + } + }; + UnityEditor.EditorApplication.delayCall += checkLeaks; +#endif + } + + /// <summary> + /// Schedules the meshes for the specified hash to be drawn. + /// Returns: False if there is no cached mesh for this hash, you may want to + /// submit one in that case. The draw command will be issued regardless of the return value. + /// </summary> + public bool Draw (Hasher hasher) { + if (hasher.Equals(Hasher.NotSupplied)) throw new System.ArgumentException("Invalid hash value"); + return processedData.SetVersion(hasher, version); + } + + /// <summary> + /// Schedules the meshes for the specified hash to be drawn. + /// Returns: False if there is no cached mesh for this hash, you may want to + /// submit one in that case. The draw command will be issued regardless of the return value. + /// + /// This overload will draw all meshes within the specified redraw scope. + /// Note that if they had been drawn with another redraw scope earlier they will be removed from that scope. + /// </summary> + public bool Draw (Hasher hasher, RedrawScope scope) { + if (hasher.Equals(Hasher.NotSupplied)) throw new System.ArgumentException("Invalid hash value"); + processedData.SetCustomScope(hasher, scope); + return processedData.SetVersion(hasher, version); + } + + /// <summary>Schedules all meshes that were drawn the last frame with this redraw scope to be drawn again</summary> + internal void Draw (RedrawScope scope) { + if (scope.isValid) processedData.SetVersion(scope, version); + } + + internal void DrawUntilDisposed (RedrawScope scope, GameObject associatedGameObject) { + if (scope.isValid) { + Draw(scope); + persistentRedrawScopes.Add(scope.id, associatedGameObject); +#if ALINE_TRACK_REDRAW_SCOPE_LEAKS && UNITY_EDITOR + LeakTracking.Begin(); + persistentRedrawScopeInfos[scope.id] = new System.Diagnostics.StackTrace().ToString(); + LeakTracking.End(); +#endif + } + } + + internal void DisposeRedrawScope (RedrawScope scope) { + if (scope.isValid) { + processedData.SetVersion(scope, -1); + persistentRedrawScopes.Remove(scope.id); +#if ALINE_TRACK_REDRAW_SCOPE_LEAKS && UNITY_EDITOR + persistentRedrawScopeInfos.Remove(scope.id); +#endif + } + } + + void RefreshRedrawScopes () { +#if UNITY_EDITOR && UNITY_2020_1_OR_NEWER + var currentStage = UnityEditor.SceneManagement.StageUtility.GetCurrentStage(); + var isInNonMainStage = currentStage != UnityEditor.SceneManagement.StageUtility.GetMainStage(); +#endif + foreach (var scope in persistentRedrawScopes) { +#if UNITY_EDITOR && UNITY_2020_1_OR_NEWER + // True if the scene is in isolation mode (e.g. focusing on a single prefab) and this object is not part of that sub-stage + var disabledDueToIsolationMode = isInNonMainStage && scope.Value != null && UnityEditor.SceneManagement.StageUtility.GetStage(scope.Value) != currentStage; + if (disabledDueToIsolationMode) continue; +#endif + processedData.SetVersion(new RedrawScope(this, scope.Key), version); + } + } + + public void TickFramePreRender () { + data.DisposeCommandBuildersWithJobDependencies(this); + // Remove persistent commands that have timed out. + // When not playing then persistent commands are never drawn twice + processedData.FilterOldPersistentCommands(version, lastTickVersion, CurrentTime, adjustedSceneModeVersion); + + RefreshRedrawScopes(); + + // All cameras rendered between the last tick and this one will have + // a version that is at least lastTickVersion + 1. + // However the user may want to reuse meshes from the previous frame (see Draw(Hasher)). + // This requires us to keep data from one more frame and thus we use lastTickVersion2 + 1 + // TODO: One frame should be enough, right? + processedData.ReleaseDataOlderThan(this, lastTickVersion2 + 1); + lastTickVersion2 = lastTickVersion; + lastTickVersion = version; + currentDrawOrderIndex = 0; + + // Pooled meshes from two frames ago can now be used. +#if USE_RAW_GRAPHICS_BUFFERS + // One would think that pooled meshes from only one frame ago can be used. + // And yes, Unity will allow this, but the GPU may still be working on the meshes from the previous frame. + // Therefore, when we try to write to the raw mesh vertex buffers Unity will block until the previous + // frame's GPU work is done, which may take a long time. + // Using "double buffering" for the meshes that are updated every frame is more efficient. + // When we use simplified methods for setting the vertex/index data we don't have to do this + // because Unity seems to manage an upload buffer or something for us. + cachedMeshes.AddRange(stagingCachedMeshesDelay); + // Move stagingCachedMeshes to stagingCachedMeshesDelay, and make stagingCachedMeshes an empty list. + stagingCachedMeshesDelay.Clear(); + var tmp = stagingCachedMeshesDelay; + stagingCachedMeshesDelay = stagingCachedMeshes; + stagingCachedMeshes = tmp; +#else + cachedMeshes.AddRange(stagingCachedMeshes); + stagingCachedMeshes.Clear(); +#endif + SortPooledMeshes(); + + // If the largest cached mesh hasn't been used in a while, then remove it to free up the memory + if (version - lastTimeLargestCachedMeshWasUsed > 60 && cachedMeshes.Count > 0) { + Mesh.DestroyImmediate(cachedMeshes[0]); + cachedMeshes.RemoveAt(0); + lastTimeLargestCachedMeshWasUsed = version; + } + + // TODO: Filter cameraVersions to avoid memory leak + } + + public void PostRenderCleanup () { + MarkerRelease.Begin(); + data.ReleaseAllUnused(); + MarkerRelease.End(); + version++; + } + + class MeshCompareByDrawingOrder : IComparer<RenderedMeshWithType> { + public int Compare (RenderedMeshWithType a, RenderedMeshWithType b) { + // Extract if the meshes are Solid/Lines/Text + var ta = (int)a.type & 0x7; + var tb = (int)b.type & 0x7; + return ta != tb ? ta - tb : a.drawingOrderIndex - b.drawingOrderIndex; + } + } + + static readonly MeshCompareByDrawingOrder meshSorter = new MeshCompareByDrawingOrder(); + // Temporary array, cached to avoid allocations + Plane[] frustrumPlanes = new Plane[6]; + // Temporary block, cached to avoid allocations + MaterialPropertyBlock customMaterialProperties = new MaterialPropertyBlock(); + + int totalMemoryUsage => this.data.memoryUsage + this.processedData.memoryUsage; + + void LoadMaterials () { + // Make sure the material references are correct + + // Note: When importing the package for the first time the asset database may not be up to date and Resources.Load may return null. + + if (surfaceMaterial == null) { + surfaceMaterial = Resources.Load<Material>("aline_surface"); + } + if (lineMaterial == null) { + lineMaterial = Resources.Load<Material>("aline_outline"); + } + if (fontData.material == null) { + var font = DefaultFonts.LoadDefaultFont(); + fontData.Dispose(); + fontData = new SDFLookupData(font); + } + } + + public DrawingData() { + gizmosHandle = System.Runtime.InteropServices.GCHandle.Alloc(this, System.Runtime.InteropServices.GCHandleType.Weak); + LoadMaterials(); + } + + static int CeilLog2 (int x) { + // Should use `math.ceillog2` whenever we next raise the minimum compatible version of the mathematics package. + // This variant is prone to floating point errors. + return (int)math.ceil(math.log2(x)); + } + + /// <summary> + /// Wrapper for different kinds of commands buffers. + /// + /// Annoyingly, they all use a CommandBuffer in the end, but the universal render pipeline wraps it in a RasterCommandBuffer, + /// and it's not possible to get the underlaying CommandBuffer. + /// </summary> + public struct CommandBufferWrapper { + public CommandBuffer cmd; +#if MODULE_RENDER_PIPELINES_UNIVERSAL_17_0_0_OR_NEWER + public bool allowDisablingWireframe; + public RasterCommandBuffer cmd2; +#endif + +#if UNITY_2023_1_OR_NEWER + public void SetWireframe (bool enable) { + if (cmd != null) { + cmd.SetWireframe(enable); + } +#if MODULE_RENDER_PIPELINES_UNIVERSAL_17_0_0_OR_NEWER + else if (cmd2 != null) { + if (allowDisablingWireframe) cmd2.SetWireframe(enable); + } +#endif + } +#endif + + public void DrawMesh (Mesh mesh, Matrix4x4 matrix, Material material, int submeshIndex, int shaderPass, MaterialPropertyBlock properties) { + if (cmd != null) { + cmd.DrawMesh(mesh, matrix, material, submeshIndex, shaderPass, properties); + } +#if MODULE_RENDER_PIPELINES_UNIVERSAL_17_0_0_OR_NEWER + else if (cmd2 != null) { + cmd2.DrawMesh(mesh, matrix, material, submeshIndex, shaderPass, properties); + } +#endif + } + } + + /// <summary>Call after all <see cref="Draw"/> commands for the frame have been done to draw everything.</summary> + /// <param name="allowCameraDefault">Indicates if built-in command builders and custom ones without a custom CommandBuilder.cameraTargets should render to this camera.</param> + public void Render (Camera cam, bool allowGizmos, CommandBufferWrapper commandBuffer, bool allowCameraDefault) { + LoadMaterials(); + + // Warn if the materials could not be found + if (surfaceMaterial == null || lineMaterial == null) { + // Note that when the package is installed Unity may start rendering things and call this method before it has initialized the Resources folder with the materials. + // We don't want to throw exceptions in that case because once the import finishes everything will be good. + // UnityEngine.Debug.LogWarning("Looks like you just installed ALINE. The ALINE package will start working after the next script recompilation."); + return; + } + + var planes = frustrumPlanes; + GeometryUtility.CalculateFrustumPlanes(cam, planes); + + if (!cameraVersions.TryGetValue(cam, out Range cameraRenderingRange)) { + cameraRenderingRange = new Range { start = int.MinValue, end = int.MinValue }; + } + + // Check if the last time the camera was rendered + // was during the current frame. + if (cameraRenderingRange.end > lastTickVersion) { + // In some cases a camera is rendered multiple times per frame. + // In this case we just extend the end of the drawing range up to the current version. + // The reasoning is that all times the camera is rendered in a frame + // all things should be drawn. + // If we did update the start of the range then things would only be drawn + // the first time the camera was rendered in the frame. + + // Sometimes the scene view will be rendered twice in a single frame + // due to some internal Unity tooltip code. + // Without this fix the scene view camera may end up showing no gizmos + // for a single frame. + cameraRenderingRange.end = version + 1; + } else { + // This is the common case: the previous time the camera was rendered + // it rendered all versions lower than cameraRenderingRange.end. + // So now we start by rendering from that version. + cameraRenderingRange = new Range { start = cameraRenderingRange.end, end = version + 1 }; + } + + // Don't show anything rendered before the last frame. + // If the camera has been turned off for a while and then suddenly starts rendering again + // we want to make sure that we don't render meshes from multiple frames. + // This happens often in the unity editor as the scene view and game view often skip + // rendering many frames when outside of play mode. + cameraRenderingRange.start = Mathf.Max(cameraRenderingRange.start, lastTickVersion2 + 1); + + var settings = settingsRef; + +#if UNITY_2023_1_OR_NEWER + bool skipDueToWireframe = false; + commandBuffer.SetWireframe(false); +#else + // If GL.wireframe is enabled (the Wireframe mode in the scene view settings) + // then I have found no way to draw gizmos in a good way. + // It's best to disable gizmos altogether to avoid drawing wireframe versions of gizmo meshes. + bool skipDueToWireframe = GL.wireframe; +#endif + + if (!skipDueToWireframe) { + MarkerBuildMeshes.Begin(); + processedData.SubmitMeshes(this, cam, cameraRenderingRange.start, allowGizmos, allowCameraDefault); + MarkerBuildMeshes.End(); + MarkerCollectMeshes.Begin(); + meshes.Clear(); + processedData.CollectMeshes(cameraRenderingRange.start, meshes, cam, allowGizmos, allowCameraDefault); + processedData.PoolDynamicMeshes(this); + MarkerCollectMeshes.End(); + MarkerSortMeshes.Begin(); + // Note that a stable sort is required as some meshes may have the same sorting index + // but those meshes will have a consistent ordering between them in the list + meshes.Sort(meshSorter); + MarkerSortMeshes.End(); + + int colorID = Shader.PropertyToID("_Color"); + int colorFadeID = Shader.PropertyToID("_FadeColor"); + var solidBaseColor = new Color(1, 1, 1, settings.solidOpacity); + var solidFadeColor = new Color(1, 1, 1, settings.solidOpacityBehindObjects); + var lineBaseColor = new Color(1, 1, 1, settings.lineOpacity); + var lineFadeColor = new Color(1, 1, 1, settings.lineOpacityBehindObjects); + var textBaseColor = new Color(1, 1, 1, settings.textOpacity); + var textFadeColor = new Color(1, 1, 1, settings.textOpacityBehindObjects); + + // The meshes list is already sorted as first surfaces, then lines, then text + for (int i = 0; i < meshes.Count;) { + int meshEndIndex = i+1; + var tp = meshes[i].type & MeshType.BaseType; + while (meshEndIndex < meshes.Count && (meshes[meshEndIndex].type & MeshType.BaseType) == tp) meshEndIndex++; + + Material mat; + customMaterialProperties.Clear(); + switch (tp) { + case MeshType.Solid: + mat = surfaceMaterial; + customMaterialProperties.SetColor(colorID, solidBaseColor); + customMaterialProperties.SetColor(colorFadeID, solidFadeColor); + break; + case MeshType.Lines: + mat = lineMaterial; + customMaterialProperties.SetColor(colorID, lineBaseColor); + customMaterialProperties.SetColor(colorFadeID, lineFadeColor); + break; + case MeshType.Text: + mat = fontData.material; + customMaterialProperties.SetColor(colorID, textBaseColor); + customMaterialProperties.SetColor(colorFadeID, textFadeColor); + break; + default: + throw new System.InvalidOperationException("Invalid mesh type"); + } + + for (int pass = 0; pass < mat.passCount; pass++) { + for (int j = i; j < meshEndIndex; j++) { + var mesh = meshes[j]; + if ((mesh.type & MeshType.Custom) != 0) { + // This mesh type may have a matrix set. So we need to handle that + if (GeometryUtility.TestPlanesAABB(planes, TransformBoundingBox(mesh.matrix, mesh.mesh.bounds))) { + // Custom meshes may have different colors + customMaterialProperties.SetColor(colorID, solidBaseColor * mesh.color); + commandBuffer.DrawMesh(mesh.mesh, mesh.matrix, mat, 0, pass, customMaterialProperties); + customMaterialProperties.SetColor(colorID, solidBaseColor); + } + } else if (GeometryUtility.TestPlanesAABB(planes, mesh.mesh.bounds)) { + // This mesh is drawn with an identity matrix + commandBuffer.DrawMesh(mesh.mesh, Matrix4x4.identity, mat, 0, pass, customMaterialProperties); + } + } + } + + i = meshEndIndex; + } + + meshes.Clear(); + } + + cameraVersions[cam] = cameraRenderingRange; + } + + /// <summary>Returns a new axis aligned bounding box that contains the given bounding box after being transformed by the matrix</summary> + static Bounds TransformBoundingBox (Matrix4x4 matrix, Bounds bounds) { + var mn = bounds.min; + var mx = bounds.max; + // Create the bounding box from the bounding box of the transformed + // 8 points of the original bounding box. + var newBounds = new Bounds(matrix.MultiplyPoint(mn), Vector3.zero); + + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mn.x, mn.y, mx.z))); + + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mn.x, mx.y, mn.z))); + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mn.x, mx.y, mx.z))); + + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mx.x, mn.y, mn.z))); + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mx.x, mn.y, mx.z))); + + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mx.x, mx.y, mn.z))); + newBounds.Encapsulate(matrix.MultiplyPoint(new Vector3(mx.x, mx.y, mx.z))); + return newBounds; + } + + /// <summary> + /// Destroys all cached meshes. + /// Used to make sure that no memory leaks happen in the Unity Editor. + /// </summary> + public void ClearData () { + gizmosHandle.Free(); + data.Dispose(); + processedData.Dispose(this); + + for (int i = 0; i < cachedMeshes.Count; i++) { + Mesh.DestroyImmediate(cachedMeshes[i]); + } + cachedMeshes.Clear(); + + UnityEngine.Assertions.Assert.IsTrue(meshes.Count == 0); + fontData.Dispose(); + } + } +} |