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 { /// /// Same as Time.time, but not updated as frequently. /// Used since burst jobs cannot access Time.time. /// public static readonly Unity.Burst.SharedStatic BurstTime = Unity.Burst.SharedStatic.GetOrCreate(4); private class BurstTimeKey {} } /// /// 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. /// /// /// 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(); /// } /// /// /// See: /// 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; /// /// ID of the scope. /// Zero means no or invalid scope. /// internal int id; static int idCounter = 1; /// True if the scope has been created 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++; } /// /// 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. /// internal void Draw () { if (gizmos.IsAllocated) { if (gizmos.Target is DrawingData gizmosTarget) gizmosTarget.Draw(this); } } /// /// 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. /// 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); } /// /// 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. /// public void Dispose () { if (gizmos.IsAllocated) { if (gizmos.Target is DrawingData gizmosTarget) gizmosTarget.DisposeRedrawScope(this); } gizmos = default; id = 0; } }; /// /// 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 /// /// /// 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); /// } /// } /// /// public class DrawingData { /// Combines hashes into a single hash value public struct Hasher : IEquatable { ulong hash; public static Hasher NotSupplied => new Hasher { hash = ulong.MaxValue }; public static Hasher Create(T init) { var h = new Hasher(); h.Add(init); return h; } public void Add(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 temporaryMeshBuffers; JobHandle buildJob, splitterJob; public List meshes; public bool isValid { get { return 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(); if (!temporaryMeshBuffers.IsCreated) { temporaryMeshBuffers = new NativeArray(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 meshes) { var itemMeshes = this.meshes; var customMeshIndex = 0; var capturedState = temporaryMeshBuffers[0].capturedState; var maxCustomMeshes = capturedState.Length / UnsafeUtility.SizeOf(); 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; /// Used to invalidate gizmos when the scene mode changes 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 meshes; public NativeArray 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(); if (!commandBuffers.IsCreated) { #if UNITY_2022_3_OR_NEWER commandBuffers = new NativeArray(JobsUtility.ThreadIndexCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); #else commandBuffers = new NativeArray(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(AnyBuffersWrittenTo).Invoke; unsafe delegate void ResetAllBuffersToDelegate(UnsafeAppendBuffer* buffers, int numBuffers); private readonly unsafe static ResetAllBuffersToDelegate ResetAllBuffersToInvoke = BurstCompiler.CompileFunctionPointer(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 > hash2index; Stack freeSlots; Stack > 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(); freeLists = new Stack >(); hash2index = new Dictionary >(); } 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 ls; if (!hash2index.TryGetValue(meta.hasher.Hash, out ls)) { if (freeLists.Count == 0) freeLists.Push(new List()); 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(); } /// /// 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. /// 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 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 meshes = new List(); List cachedMeshes = new List(); List stagingCachedMeshes = new List(); #if USE_RAW_GRAPHICS_BUFFERS List stagingCachedMeshesDelay = new List(); #endif int lastTimeLargestCachedMeshWasUsed = 0; internal SDFLookupData fontData; int currentDrawOrderIndex = 0; /// /// 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. /// internal int sceneModeVersion = 0; /// /// Slightly adjusted scene mode version. /// This takes into account `Application.isPlaying` too. It is possible for 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. /// 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; } /// /// Get an empty builder for queuing drawing commands. /// /// /// // 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); /// } /// /// See: /// /// 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. 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); } /// /// Get an empty builder for queuing drawing commands. /// /// See: /// /// If true, this builder will be rendered in standalone games and in the editor even if gizmos are disabled. public CommandBuilder GetBuilder (RedrawScope redrawScope, bool renderInGame = false) { UpdateTime(); return new CommandBuilder(this, Hasher.NotSupplied, frameRedrawScope, redrawScope, !renderInGame, false, adjustedSceneModeVersion); } /// /// Get an empty builder for queuing drawing commands. /// /// See: /// /// If true, this builder will be rendered in standalone games and in the editor even if gizmos are disabled. 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); } /// Material to use for surfaces public Material surfaceMaterial; /// Material to use for lines public Material lineMaterial; /// Material to use for text 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 persistentRedrawScopes = new Dictionary(); #if ALINE_TRACK_REDRAW_SCOPE_LEAKS Dictionary persistentRedrawScopeInfos = new Dictionary(); #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 cameraVersions = new Dictionary(); 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 } /// /// 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. /// public bool Draw (Hasher hasher) { if (hasher.Equals(Hasher.NotSupplied)) throw new System.ArgumentException("Invalid hash value"); return processedData.SetVersion(hasher, version); } /// /// 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. /// 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); } /// Schedules all meshes that were drawn the last frame with this redraw scope to be drawn again 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); } } void CleanupOldCameras () { // Remove cameras that have not been used for a while, to avoid memory leaks. // We keep them for a few frames for debugging purposes. foreach (var item in cameraVersions) { if (item.Value.end < lastTickVersion - 10) { cameraVersions.Remove(item.Key); // Break to avoid modifying the collection while iterating over it // In the rare case that multiple cameras needed to be removed, we can continue removing them next frame. break; } } } 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); CleanupOldCameras(); 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 { 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("aline_surface"); } if (lineMaterial == null) { lineMaterial = Resources.Load("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)); } /// /// 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. /// 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 } } /// Call after all commands for the frame have been done to draw everything. /// Indicates if built-in command builders and custom ones without a custom CommandBuilder.cameraTargets should render to this camera. 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; } /// Returns a new axis aligned bounding box that contains the given bounding box after being transformed by the matrix 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; } /// /// Destroys all cached meshes. /// Used to make sure that no memory leaks happen in the Unity Editor. /// 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(); } } }