diff --git a/CHANGELOG.md b/CHANGELOG.md index 6463feff..eae9fd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [#320] Render nodes are not correctly reused across frames - [#321] Fix GetTargetGroup being called on every pipeline invalidation +- [#327] Z-fighting occurs in prefab isolation view ### Changed diff --git a/Editor/ChangeStream/ListenerSet.cs b/Editor/ChangeStream/ListenerSet.cs index 5c28dbac..4eed89d4 100644 --- a/Editor/ChangeStream/ListenerSet.cs +++ b/Editor/ChangeStream/ListenerSet.cs @@ -1,6 +1,7 @@ #region using System; +using System.Collections.Generic; using nadena.dev.ndmf.preview; using UnityEditor; @@ -25,6 +26,13 @@ ComputeContext ctx _ctx = ctx == null ? null : new WeakReference(ctx); } + public override string ToString() + { + if (_ctx.TryGetTarget(out var target)) + return $"Listener for {target}"; + return "Listener (GC'd)"; + } + public void Dispose() { if (_next != null) @@ -41,6 +49,9 @@ internal void MaybePrune() { if (!_ctx.TryGetTarget(out var ctx) || ctx.IsInvalidated) { +#if NDMF_DEBUG + System.Diagnostics.Debug.WriteLine($"{this} is invalid, disposing"); +#endif Dispose(); } } @@ -50,10 +61,17 @@ internal void MaybeFire(T info) { if (!_ctx.TryGetTarget(out var ctx) || ctx.IsInvalidated) { +#if NDMF_DEBUG + System.Diagnostics.Debug.WriteLine($"{this} is invalid, disposing"); +#endif Dispose(); } else if (_filter(info)) { +#if NDMF_DEBUG + System.Diagnostics.Debug.WriteLine($"{this} is firing"); +#endif + ctx.Invalidate(); // We need to wait two frames before repainting: One to process task callbacks, then one to actually // repaint (and update previews). @@ -116,5 +134,15 @@ public void Prune() listener = next; } } + + internal IEnumerable GetListeners() + { + var ptr = _head._next; + while (ptr != _head) + { + yield return ptr.ToString(); + ptr = ptr._next; + } + } } } \ No newline at end of file diff --git a/Editor/ChangeStream/ShadowGameObject.cs b/Editor/ChangeStream/ShadowGameObject.cs index cd60b199..d0c71a3d 100644 --- a/Editor/ChangeStream/ShadowGameObject.cs +++ b/Editor/ChangeStream/ShadowGameObject.cs @@ -1,5 +1,8 @@ #region +#if NDMF_DEBUG +using System.Text; +#endif using System; using System.Collections.Generic; using System.Threading; @@ -56,6 +59,91 @@ internal class ShadowHierarchy int lastPruned = Int32.MinValue; +#if NDMF_DEBUG + [MenuItem("Tools/NDM Framework/Debug: Dump shadow hierarchy")] + static void StaticDumpShadowHierarchy() + { + ObjectWatcher.Instance.Hierarchy.DumpShadowHierarchy(); + } + + void DumpShadowHierarchy() + { + int indent = 0; + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("[Shadow Hierarchy Dump]"); + sb.AppendLine("Root set listeners:"); + indent += 2; + DumpListenerSet(_rootSetListener); + sb.AppendLine(); + indent -= 2; + + sb.AppendLine("GameObjects:"); + foreach (var obj in _gameObjects.Values) + { + if (obj.Parent == null) + { + DumpShadowGameObject(obj); + } + } + + sb.AppendLine("Other objects:"); + foreach (var obj in _otherObjects.Values) + { + DumpShadowObject(obj); + } + + sb.AppendLine("[End Shadow Hierarchy Dump]"); + + Debug.Log(sb.ToString()); + + + void DumpShadowObject(ShadowObject obj) + { + sb.Append(' ', indent); + sb.Append("+ "); + if (obj.Object == null) + { + sb.AppendLine("<" + obj.InstanceID + ">"); + } + else if (obj.Object is Component c) + { + sb.AppendLine(c.gameObject.name + " (" + c.GetType().Name + ")"); + } + else + { + sb.AppendLine(obj.Object.name); + } + } + + void DumpShadowGameObject(ShadowGameObject obj) + { + sb.Append(' ', indent); + sb.Append("+ "); + sb.AppendLine(obj.GameObject == null ? "<" + obj.InstanceID + ">" : obj.GameObject.name); + indent += 2; + DumpListenerSet(obj._listeners); + sb.AppendLine(); + sb.Append(' ', indent); + sb.AppendLine("Children:"); + foreach (var child in obj.Children) + { + DumpShadowGameObject(child); + } + indent -= 2; + } + + void DumpListenerSet(ListenerSet set) + { + foreach (var listener in set.GetListeners()) + { + sb.Append(' ', indent); + sb.AppendLine(listener.ToString()); + } + } + } +#endif + internal IDisposable RegisterRootSetListener(ListenerSet.Filter filter, ComputeContext ctx) { if (ctx.IsInvalidated) return new NullDisposable(); @@ -290,6 +378,10 @@ internal void FireDestroyNotification(int instanceId) void ForceInvalidateHierarchy(ShadowGameObject obj) { +#if NDMF_DEBUG + Debug.Log("=== Force invalidate: " + (obj.GameObject?.name ?? "")); +#endif + obj._listeners.Fire(HierarchyEvent.ForceInvalidate); _gameObjects.Remove(obj.InstanceID); @@ -322,6 +414,10 @@ internal void FireStructureChangeEvent(int instanceId) internal void InvalidateAll() { +#if NDMF_DEBUG + Debug.Log("=== Invalidate all ==="); +#endif + var oldDict = _gameObjects; _gameObjects = new Dictionary(); diff --git a/Editor/PreviewSystem/ComputeContext.cs b/Editor/PreviewSystem/ComputeContext.cs index 2602d580..0c7edb34 100644 --- a/Editor/PreviewSystem/ComputeContext.cs +++ b/Editor/PreviewSystem/ComputeContext.cs @@ -19,9 +19,11 @@ public sealed class ComputeContext static ComputeContext() { - NullContext = new ComputeContext(null); + NullContext = new ComputeContext("null", null); } + internal string Description { get; } + /// /// An Action which can be used to invalidate this compute context (possibly triggering a recompute). /// @@ -35,16 +37,18 @@ static ComputeContext() public bool IsInvalidated => OnInvalidate.IsCompleted; - internal ComputeContext() + internal ComputeContext(string description) { Invalidate = () => _invalidater.TrySetResult(null); OnInvalidate = _invalidater.Task; + Description = description; } - private ComputeContext(object nullToken) + private ComputeContext(string description, object nullToken) { Invalidate = () => { }; OnInvalidate = Task.CompletedTask; + Description = description; } /// @@ -55,5 +59,10 @@ internal void Invalidates(ComputeContext other) { OnInvalidate.ContinueWith(_ => other.Invalidate()); } + + public override string ToString() + { + return ""; + } } } \ No newline at end of file diff --git a/Editor/PreviewSystem/ProxyManager.cs b/Editor/PreviewSystem/ProxyManager.cs index 1626059c..6c9824c5 100644 --- a/Editor/PreviewSystem/ProxyManager.cs +++ b/Editor/PreviewSystem/ProxyManager.cs @@ -36,6 +36,9 @@ private static void OnPreCull(Camera cam) if (EditorApplication.isPlayingOrWillChangePlaymode) return; + // TODO: fully support prefab isolation view + if (PrefabStageUtility.GetCurrentPrefabStage() != null) return; + var sess = PreviewSession.Current; if (sess == null) return; diff --git a/Editor/PreviewSystem/Rendering/NodeController.cs b/Editor/PreviewSystem/Rendering/NodeController.cs index 34c8151c..e9837650 100644 --- a/Editor/PreviewSystem/Rendering/NodeController.cs +++ b/Editor/PreviewSystem/Rendering/NodeController.cs @@ -81,9 +81,11 @@ public static async Task Create( IRenderFilter filter, RenderGroup group, ObjectRegistry registry, - List<(Renderer, ProxyObjectController, ObjectRegistry)> proxies) + List<(Renderer, ProxyObjectController, ObjectRegistry)> proxies + ) { - ComputeContext context = new ComputeContext(); + var context = + new ComputeContext("NodeController for " + filter + " on " + group.Renderers[0].gameObject.name); IRenderFilterNode node; using (var scope = new ObjectRegistryScope(registry)) @@ -104,7 +106,8 @@ RenderAspects changes ) { var registry = ObjectRegistry.Merge(null, proxies.Select(p => p.Item3)); - ComputeContext context = new ComputeContext(); + var context = new ComputeContext("NodeController (refresh) for " + _filter + " on " + + _group.Renderers[0].gameObject.name); IRenderFilterNode node; diff --git a/Editor/PreviewSystem/Rendering/ProxyObjectController.cs b/Editor/PreviewSystem/Rendering/ProxyObjectController.cs index 8e0136df..0b53fdfe 100644 --- a/Editor/PreviewSystem/Rendering/ProxyObjectController.cs +++ b/Editor/PreviewSystem/Rendering/ProxyObjectController.cs @@ -72,9 +72,10 @@ public ProxyObjectController(ProxyObjectCache cache, Renderer originalRenderer, private void SetupRendererMonitoring(Renderer r) { - _monitorRenderer = new ComputeContext(); - _monitorMaterials = new ComputeContext(); - _monitorMesh = new ComputeContext(); + var gameObjectName = r.gameObject.name; + _monitorRenderer = new ComputeContext("Renderer Monitor for " + gameObjectName); + _monitorMaterials = new ComputeContext("Material Monitor for " + gameObjectName); + _monitorMesh = new ComputeContext("Mesh Monitor for " + gameObjectName); _monitorRenderer.Observe(r); if (r is SkinnedMeshRenderer smr) diff --git a/Editor/PreviewSystem/Rendering/ProxyPipeline.cs b/Editor/PreviewSystem/Rendering/ProxyPipeline.cs index 2cdb40df..b77bb370 100644 --- a/Editor/PreviewSystem/Rendering/ProxyPipeline.cs +++ b/Editor/PreviewSystem/Rendering/ProxyPipeline.cs @@ -35,7 +35,7 @@ public StageDescriptor(IRenderFilter filter, ComputeContext parentContext) { Filter = filter; - Context = new ComputeContext(); + Context = new ComputeContext("StageDescriptor for " + Filter + " on " + parentContext); Context.Invalidates(parentContext); var unsorted = filter.GetTargetGroups(Context); @@ -51,7 +51,7 @@ public StageDescriptor(IRenderFilter filter, ComputeContext parentContext) public StageDescriptor(StageDescriptor prior, ComputeContext parentContext) { Filter = prior.Filter; - Context = new ComputeContext(); + Context = new ComputeContext("StageDescriptor for " + Filter + " on " + parentContext); Context.Invalidates(parentContext); Originals = prior.Originals; @@ -84,9 +84,11 @@ internal class ProxyPipeline internal ImmutableDictionary ProxyToOriginalObject = ImmutableDictionary.Empty; + private readonly long _generation; + // ReSharper disable once NotAccessedField.Local // needed to prevent GC of the ComputeContext - private ComputeContext _ctx = new(); + private ComputeContext _ctx; internal bool IsInvalidated => _ctx.OnInvalidate.IsCompleted; internal void Invalidate() @@ -104,6 +106,7 @@ internal void Invalidate() public ProxyPipeline(ProxyObjectCache proxyCache, IEnumerable filters, ProxyPipeline priorPipeline = null) { + _generation = (priorPipeline?._generation ?? 0) + 1; InvalidateAction = Invalidate; _buildTask = Task.Factory.StartNew( @@ -119,9 +122,13 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable ProxyPipeline priorPipeline) { Profiler.BeginSample("ProxyPipeline.Build.Synchronous"); - var context = new ComputeContext(); + var context = new ComputeContext($"ProxyPipeline {_generation}"); _ctx = context; // prevent GC +#if NDMF_DEBUG + System.Diagnostics.Debug.WriteLine($"Building pipeline {_generation}"); +#endif + var filterList = filters.Where(f => f.IsEnabled(context)).ToList(); Dictionary> nodeTasks = new(); @@ -205,6 +212,10 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable { var proxies = items.Result.ToList(); +#if NDMF_DEBUG + System.Diagnostics.Debug.WriteLine($"Creating node for {filter} on {group.Renderers[0].gameObject.name} for generation {_generation}"); +#endif + if (priorNode != null) { RenderAspects changeFlags = proxies.Select(p => p.Item2.ChangeFlags) @@ -244,6 +255,10 @@ await Task.WhenAll(_stages.SelectMany(s => s.NodeTasks)) //Debug.WriteLine($"Total nodes: {total_nodes}, reused: {reused}, refresh failed: {refresh_failed}"); +#if NDMF_DEBUG + System.Diagnostics.Debug.WriteLine($"Pipeline {_generation} is ready"); +#endif + foreach (var stage in _stages) { foreach (var node in stage.NodeTasks) diff --git a/UnitTests~/RQ-EditorTests/PublishedValueTest.cs b/UnitTests~/RQ-EditorTests/PublishedValueTest.cs index fa438d65..bb797b00 100644 --- a/UnitTests~/RQ-EditorTests/PublishedValueTest.cs +++ b/UnitTests~/RQ-EditorTests/PublishedValueTest.cs @@ -9,7 +9,7 @@ public class PublishedValueTest public void BasicObserve() { PublishedValue val = new PublishedValue("foo"); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); string observed = ctx.Observe(val); Assert.AreEqual("foo", observed); @@ -25,7 +25,7 @@ public void BasicObserve() public void ObserveWithExtract() { PublishedValue val = new PublishedValue("foo"); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); int len = ctx.Observe(val, s => s.Length); Assert.AreEqual(3, len); @@ -43,7 +43,7 @@ public void ObserveWithExtract() public void ObserveWithExtractAndEquals() { PublishedValue val = new PublishedValue("foo"); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); string observed = ctx.Observe(val, a => a, (a, b) => a.ToLowerInvariant().Equals(b.ToLowerInvariant())); Assert.AreEqual("foo", observed); diff --git a/UnitTests~/RQ-EditorTests/ShadowHierarchyTest.cs b/UnitTests~/RQ-EditorTests/ShadowHierarchyTest.cs index 9ec5416d..53f50f20 100644 --- a/UnitTests~/RQ-EditorTests/ShadowHierarchyTest.cs +++ b/UnitTests~/RQ-EditorTests/ShadowHierarchyTest.cs @@ -41,7 +41,7 @@ public void TestBasic() var gameObject = c(new GameObject("tmp")); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); bool wasFired = false; bool doInvalidate = false; @@ -75,7 +75,7 @@ public void ListenerDeregisteredWhenContextInvalidated() var gameObject = c(new GameObject("tmp")); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); bool wasFired = false; shadow.RegisterGameObjectListener(gameObject, e => @@ -98,7 +98,7 @@ public void ListenerDeregisteredAfterTrueReturn() var gameObject = c(new GameObject("tmp")); int count = 0; - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); shadow.RegisterGameObjectListener(gameObject, (e) => { @@ -118,7 +118,7 @@ void MakeListener__WhenTargetGCd_ListenerIsRemoved(ShadowHierarchy h, GameObject { wasFired[0] = true; return false; - }, new ComputeContext()); + }, new ComputeContext("")); } [Test] public void WhenTargetGCd_ListenerIsRemoved() @@ -148,7 +148,7 @@ public void WhenDisposed_ListenerIsRemoved() var gameObject = c(new GameObject("tmp")); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); bool wasFired = false; var listener = shadow.RegisterGameObjectListener(gameObject, e => @@ -170,7 +170,7 @@ public void PathNotifications_GeneratedWhenImmediateParentChanged() var p1 = c("p1"); var p2 = c("p2"); - var target = new ComputeContext(); + var target = new ComputeContext(""); bool wasFired = false; shadow.RegisterGameObjectListener(p2, e => @@ -199,7 +199,7 @@ public void PathNotifications_GeneratedWhenGrandparentChanged() var p2 = c("p2"); var p3 = c("p3"); - var target = new ComputeContext(); + var target = new ComputeContext(""); bool wasFired = false; p1.transform.SetParent(p2.transform); @@ -228,7 +228,7 @@ public void ComponentChangeNotifications_GeneratedWhenObjectItselfChanges() var shadow = new ShadowHierarchy(); var obj = c("obj"); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); List events = new List(); shadow.RegisterGameObjectListener(obj, e => @@ -252,7 +252,7 @@ public void ComponentChangeNotifications_GeneratedWhenChildChanges() child.transform.SetParent(parent.transform); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); List events = new List(); shadow.RegisterGameObjectListener(parent, e => @@ -278,7 +278,7 @@ public void ComponentChangeNotifications_FiredAfterReparents() p3.transform.SetParent(p2.transform); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); List events = new List(); shadow.RegisterGameObjectListener(p1, e => @@ -313,7 +313,7 @@ public void ComponentChangeNotification_FiredAfterReorderEvent() c1.transform.SetParent(p.transform); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); List events = new List(); shadow.RegisterGameObjectListener(p, e => @@ -342,7 +342,7 @@ public void OnDestroy_NotificationsBlasted() o2.transform.SetParent(o1.transform); o3.transform.SetParent(o2.transform); - ComputeContext ctx = new ComputeContext(); + ComputeContext ctx = new ComputeContext(""); List<(int, HierarchyEvent)> events = new List<(int, HierarchyEvent)>(); shadow.RegisterGameObjectListener(o1, e => { @@ -386,7 +386,7 @@ public void OnReparentDestroyedObject_NotificationsBlasted() { var shadow = new ShadowHierarchy(); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); var o1 = c("o1"); var o2 = c("o2"); @@ -417,7 +417,7 @@ public void OnInvalidateAll_EverythingIsInvalidated() { var shadow = new ShadowHierarchy(); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); var o1 = c("o1"); var o2 = c("o2"); @@ -445,7 +445,7 @@ public void ComponentMonitoringTest() { var shadow = new ShadowHierarchy(); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); var o1 = c("o1"); var component = o1.AddComponent(); @@ -463,7 +463,7 @@ public void ComponentReorder_TriggersStructureChange() { var shadow = new ShadowHierarchy(); - var ctx = new ComputeContext(); + var ctx = new ComputeContext(""); var o1 = c("o1"); var o2 = c("o2");