1731 lines
63 KiB
C#

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