diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4989da..5d6af0a4 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 ### Changed +- [#273] Preview system now calls `Refresh` to avoid double computation ### Removed diff --git a/Editor/PreviewSystem/IRenderFilter.cs b/Editor/PreviewSystem/IRenderFilter.cs index 44733304..29154790 100644 --- a/Editor/PreviewSystem/IRenderFilter.cs +++ b/Editor/PreviewSystem/IRenderFilter.cs @@ -26,6 +26,11 @@ internal RenderGroup(ImmutableList renderers) Renderers = renderers; } + internal RenderGroup WithoutData() + { + return new RenderGroup(Renderers); + } + public static RenderGroup For(IEnumerable renderers) { return new(renderers.OrderBy(r => r.GetInstanceID()).ToImmutableList()); @@ -116,6 +121,7 @@ public Task Instantiate(RenderGroup group, IEnumerable<(Rende ComputeContext context); } + [Flags] public enum RenderAspects { /// diff --git a/Editor/PreviewSystem/Rendering/NodeController.cs b/Editor/PreviewSystem/Rendering/NodeController.cs index c6ddb0fa..30b523e7 100644 --- a/Editor/PreviewSystem/Rendering/NodeController.cs +++ b/Editor/PreviewSystem/Rendering/NodeController.cs @@ -25,9 +25,11 @@ private class RefCount private readonly RefCount _refCount; private readonly ComputeContext _context; + internal RenderAspects WhatChanged = RenderAspects.Everything; - + internal RenderGroup Group => _group; internal Task OnInvalidate => _context.OnInvalidate; + internal bool IsInvalidated => OnInvalidate.IsCompleted; internal ProxyObjectController GetProxyFor(Renderer r) { @@ -35,6 +37,7 @@ internal ProxyObjectController GetProxyFor(Renderer r) } private NodeController( + IRenderFilter filter, RenderGroup group, IRenderFilterNode node, List<(Renderer, ProxyObjectController)> proxies, @@ -42,6 +45,7 @@ private NodeController( ComputeContext context ) { + _filter = filter; _group = group; _node = node; _proxies = proxies; @@ -77,7 +81,7 @@ public static async Task Create( context ); - return new NodeController(group, node, proxies, new RefCount(), context); + return new NodeController(filter, group, node, proxies, new RefCount(), context); } public async Task Refresh( @@ -87,11 +91,24 @@ RenderAspects changes { ComputeContext context = new ComputeContext(() => _node.ToString()); - var node = await _node.Refresh( - proxies.Select(p => (p.Item1, p.Item2.Renderer)), - context, - changes - ); + IRenderFilterNode node; + + if (changes == 0 && !IsInvalidated) + { + // Reuse the old node in its entirety + node = _node; + context = _context; + Debug.Log("=== Reusing node " + _node); + } + else + { + node = await _node.Refresh( + proxies.Select(p => (p.Item1, p.Item2.Renderer)), + context, + changes + ); + Debug.Log("=== Refreshing node " + _node + " with changes " + changes + "; success? " + (node != null) + " same? " + (node == _node)); + } RefCount refCount; if (node == _node) @@ -108,8 +125,14 @@ RenderAspects changes refCount = new RefCount(); } - var controller = new NodeController(_group, node, proxies, refCount, context); + var controller = new NodeController(_filter, _group, node, proxies, refCount, context); controller.WhatChanged = changes | node.WhatChanged; + + foreach (var proxy in proxies) + { + proxy.Item2.ChangeFlags |= node.WhatChanged; + } + return controller; } @@ -120,5 +143,10 @@ public void Dispose() _node.Dispose(); } } + + public override string ToString() + { + return "Node(" + _filter + " for group including " + _group.Renderers[0].gameObject.name + ")"; + } } } \ No newline at end of file diff --git a/Editor/PreviewSystem/Rendering/ProxyObjectCache.cs b/Editor/PreviewSystem/Rendering/ProxyObjectCache.cs new file mode 100644 index 00000000..430371c0 --- /dev/null +++ b/Editor/PreviewSystem/Rendering/ProxyObjectCache.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using nadena.dev.ndmf.rq; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace nadena.dev.ndmf.preview +{ + internal class ProxyObjectCache : IDisposable + { + private static HashSet _proxyObjectInstanceIds = new(); + + public static bool IsProxyObject(GameObject obj) + { + if (obj == null) return false; + + return _proxyObjectInstanceIds.Contains(obj.GetInstanceID()); + } + + private class RendererState + { + public Renderer InactiveProxy; + public int ActiveProxyCount = 0; + } + + private Dictionary _renderers = new(new ObjectIdentityComparer()); + private bool _cleanupPending = false; + + public Renderer GetOrCreate(Renderer original, Func create) + { + if (!_renderers.TryGetValue(original, out var state)) + { + state = new RendererState(); + _renderers.Add(original, state); + } + + Renderer proxy; + if (state.InactiveProxy != null) + { + proxy = state.InactiveProxy; + state.InactiveProxy = null; + state.ActiveProxyCount++; + return proxy; + } + + Debug.Log("=== Creating new proxy for " + original.gameObject.name); + proxy = create(); + if (proxy == null) + { + return null; + } + + state.ActiveProxyCount++; + _proxyObjectInstanceIds.Add(proxy.gameObject.GetInstanceID()); + + return proxy; + } + + public void ReturnProxy(Renderer original, Renderer proxy) + { + if (!_renderers.TryGetValue(original, out var state)) + { + Debug.Log("ProxyObjectCache: Renderer not found in cache"); + DestroyProxy(proxy); + return; + } + + if (!_cleanupPending) + { + EditorApplication.delayCall += Cleanup; + _cleanupPending = true; + } + + state.ActiveProxyCount--; + if (state.ActiveProxyCount > 0 && state.InactiveProxy == null) + { + state.InactiveProxy = proxy; + return; + } + + DestroyProxy(proxy); + + if (state.ActiveProxyCount == 0) + { + DestroyProxy(state.InactiveProxy); + _renderers.Remove(original); + } + } + + private static void DestroyProxy(Renderer proxy) + { + if (proxy == null) return; + + var gameObject = proxy.gameObject; + _proxyObjectInstanceIds.Remove(gameObject.GetInstanceID()); + Object.DestroyImmediate(gameObject); + } + + private void Cleanup() + { + _cleanupPending = false; + + foreach (var entry in _renderers.Where(kv => kv.Key == null).ToList()) + { + if (entry.Value.InactiveProxy != null) + { + Object.DestroyImmediate(entry.Value.InactiveProxy.gameObject); + } + _renderers.Remove(entry.Key); + } + } + + public void Dispose() + { + foreach (var entry in _renderers) + { + if (entry.Value.InactiveProxy != null) + { + Object.DestroyImmediate(entry.Value.InactiveProxy.gameObject); + } + } + _renderers.Clear(); + } + } +} \ No newline at end of file diff --git a/Editor/PreviewSystem/Rendering/ProxyObjectCache.cs.meta b/Editor/PreviewSystem/Rendering/ProxyObjectCache.cs.meta new file mode 100644 index 00000000..338342d6 --- /dev/null +++ b/Editor/PreviewSystem/Rendering/ProxyObjectCache.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 556c2c5fcf9f4c81bfd803cb979544be +timeCreated: 1718676067 \ No newline at end of file diff --git a/Editor/PreviewSystem/Rendering/ProxyObjectController.cs b/Editor/PreviewSystem/Rendering/ProxyObjectController.cs index 2914a477..51308317 100644 --- a/Editor/PreviewSystem/Rendering/ProxyObjectController.cs +++ b/Editor/PreviewSystem/Rendering/ProxyObjectController.cs @@ -1,7 +1,10 @@ #region using System; -using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using nadena.dev.ndmf.rq; +using nadena.dev.ndmf.rq.unity.editor; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; @@ -12,27 +15,109 @@ namespace nadena.dev.ndmf.preview { internal class ProxyObjectController : IDisposable { - private static HashSet _proxyObjectInstanceIds = new(); - + private readonly ProxyObjectCache _cache; private readonly Renderer _originalRenderer; private Renderer _replacementRenderer; internal Renderer Renderer => _replacementRenderer; public bool IsValid => _originalRenderer != null && _replacementRenderer != null; + internal RenderAspects ChangeFlags; + + internal Material[] _initialMaterials; + internal Mesh _initialSharedMesh; + internal ComputeContext _monitorRenderer, _monitorMaterials, _monitorMesh; + + internal Task OnInvalidate; + public static bool IsProxyObject(GameObject obj) { - if (obj == null) return false; - - return _proxyObjectInstanceIds.Contains(obj.GetInstanceID()); + return ProxyObjectCache.IsProxyObject(obj); } - public ProxyObjectController(Renderer originalRenderer) + public ProxyObjectController(ProxyObjectCache cache, Renderer originalRenderer, ProxyObjectController _priorController) { + _cache = cache; _originalRenderer = originalRenderer; + + SetupRendererMonitoring(originalRenderer); + + if (_priorController != null) + { + if (_priorController._monitorRenderer.OnInvalidate.IsCompleted) + { + ChangeFlags |= RenderAspects.Shapes; + + if (!_initialMaterials.SequenceEqual(_priorController._initialMaterials)) + { + ChangeFlags |= RenderAspects.Material | RenderAspects.Texture; + } + + if (_initialSharedMesh != _priorController._initialSharedMesh) + { + ChangeFlags |= RenderAspects.Mesh; + } + } + + if (_priorController._monitorMaterials.OnInvalidate.IsCompleted) + { + ChangeFlags |= RenderAspects.Material | RenderAspects.Texture; + } + + if (_priorController._monitorMesh.OnInvalidate.IsCompleted) + { + ChangeFlags |= RenderAspects.Mesh; + } + + if (ChangeFlags != 0) + { + Debug.Log("=== ProxyObjectController for " + originalRenderer.gameObject.name + " flags=" + + ChangeFlags); + } + } CreateReplacementObject(); } + private void SetupRendererMonitoring(Renderer r) + { + _monitorRenderer = new ComputeContext(() => "ProxyObjectController.Renderer"); + _monitorMaterials = new ComputeContext(() => "ProxyObjectController.Materials"); + _monitorMesh = new ComputeContext(() => "ProxyObjectController.Mesh"); + + _monitorRenderer.Observe(r); + if (r is SkinnedMeshRenderer smr) + { + _monitorMesh.Observe(smr.sharedMesh); + _initialSharedMesh = smr.sharedMesh; + } + else if (r is MeshRenderer mr) + { + var meshRenderer = _monitorMesh.GetComponent(r.gameObject); + if (meshRenderer != null) + { + _monitorMesh.Observe(meshRenderer.sharedMesh); + _initialSharedMesh = meshRenderer.sharedMesh; + } + } + + _initialMaterials = (Material[]) r.sharedMaterials.Clone(); + foreach (var material in r.sharedMaterials) + { + _monitorMaterials.Observe(material); + var texPropIds = material.GetTexturePropertyNameIDs(); + foreach (var texPropId in texPropIds) + { + var tex = material.GetTexture(texPropId); + if (tex != null) + { + _monitorMaterials.Observe(tex); + } + } + } + + OnInvalidate = Task.WhenAny(_monitorRenderer.OnInvalidate, _monitorMaterials.OnInvalidate, _monitorMesh.OnInvalidate); + } + internal bool OnPreFrame() { if (_replacementRenderer == null || _originalRenderer == null) @@ -97,45 +182,47 @@ internal bool OnPreFrame() return true; } - private bool CreateReplacementObject() + private void CreateReplacementObject() { - if (_originalRenderer == null) return false; - - var replacementGameObject = new GameObject("Proxy renderer for " + _originalRenderer.gameObject.name); - _proxyObjectInstanceIds.Add(replacementGameObject.GetInstanceID()); - replacementGameObject.hideFlags = HideFlags.DontSave; + if (_originalRenderer == null) return; + + _replacementRenderer = _cache.GetOrCreate(_originalRenderer, () => + { + var replacementGameObject = new GameObject("Proxy renderer for " + _originalRenderer.gameObject.name); + replacementGameObject.hideFlags = HideFlags.DontSave; #if MODULAR_AVATAR_DEBUG_HIDDEN - replacementGameObject.hideFlags = HideFlags.DontSave; + replacementGameObject.hideFlags = HideFlags.DontSave; #endif - replacementGameObject.AddComponent().KeepAlive = this; + replacementGameObject.AddComponent().KeepAlive = this; - if (_originalRenderer is SkinnedMeshRenderer smr) - { - _replacementRenderer = replacementGameObject.AddComponent(); - } - else if (_originalRenderer is MeshRenderer mr) - { - _replacementRenderer = replacementGameObject.AddComponent(); - replacementGameObject.AddComponent(); - } - else - { - Debug.Log("Unsupported renderer type: " + _replacementRenderer.GetType()); - Object.DestroyImmediate(replacementGameObject); - return true; - } + Renderer renderer; + if (_originalRenderer is SkinnedMeshRenderer smr) + { + renderer = replacementGameObject.AddComponent(); + } + else if (_originalRenderer is MeshRenderer mr) + { + renderer = replacementGameObject.AddComponent(); + replacementGameObject.AddComponent(); + } + else + { + Debug.Log("Unsupported renderer type: " + _originalRenderer.GetType()); + Object.DestroyImmediate(replacementGameObject); + return null; + } - return false; + return renderer; + }); } public void Dispose() { if (_replacementRenderer != null) { - _proxyObjectInstanceIds.Remove(_replacementRenderer.gameObject.GetInstanceID()); - Object.DestroyImmediate(_replacementRenderer.gameObject); + _cache.ReturnProxy(_originalRenderer, _replacementRenderer); _replacementRenderer = null; } } diff --git a/Editor/PreviewSystem/Rendering/ProxyPipeline.cs b/Editor/PreviewSystem/Rendering/ProxyPipeline.cs index 27e8fb17..1c924b5b 100644 --- a/Editor/PreviewSystem/Rendering/ProxyPipeline.cs +++ b/Editor/PreviewSystem/Rendering/ProxyPipeline.cs @@ -30,6 +30,8 @@ class StageDescriptor public StageDescriptor(IRenderFilter filter, ComputeContext context) { + Filter = filter; + context.TryObserve(filter.TargetGroups, out var unsorted); if (unsorted == null) unsorted = ImmutableList.Empty; @@ -89,14 +91,14 @@ internal void Invalidate() public IEnumerable<(Renderer, Renderer)> Renderers => _proxies.Select(kvp => (kvp.Key, kvp.Value.Renderer)); - public ProxyPipeline(IEnumerable filters, ProxyPipeline priorPipeline = null) + public ProxyPipeline(ProxyObjectCache proxyCache, IEnumerable filters, ProxyPipeline priorPipeline = null) { InvalidateAction = Invalidate; using (new SyncContextScope(ReactiveQueryScheduler.SynchronizationContext)) { _buildTask = Task.Factory.StartNew( - _ => Build(filters, priorPipeline), + _ => Build(proxyCache, filters, priorPipeline), null, CancellationToken.None, 0, @@ -105,7 +107,8 @@ public ProxyPipeline(IEnumerable filters, ProxyPipeline priorPipe } } - private async Task Build(IEnumerable filters, ProxyPipeline priorPipeline) + private async Task Build(ProxyObjectCache proxyCache, IEnumerable filters, + ProxyPipeline priorPipeline) { var context = new ComputeContext(() => "ProxyPipeline construction"); _ctx = context; // prevent GC @@ -141,7 +144,12 @@ private async Task Build(IEnumerable filters, ProxyPipeline prior } else { - var proxy = new ProxyObjectController(r); + ProxyObjectController priorProxy = null; + priorPipeline?._proxies.TryGetValue(r, out priorProxy); + + var proxy = new ProxyObjectController(proxyCache, r, priorProxy); + proxy.OnInvalidate.ContinueWith(_ => InvalidateAction(), + TaskContinuationOptions.ExecuteSynchronously); proxy.OnPreFrame(); _proxies.Add(r, proxy); @@ -153,12 +161,27 @@ private async Task Build(IEnumerable filters, ProxyPipeline prior } }); + var priorNode = prior?.NodeTasks.ElementAtOrDefault(groupIndex); + if (priorNode?.IsCompletedSuccessfully != true || !Equals(priorNode?.Result.Group, group)) + { + priorNode = null; + } - var node = Task.WhenAll(resolved).ContinueWith(items => + var node = Task.WhenAll(resolved).ContinueWith(async items => { - // TODO - prior node handling + var proxies = items.Result.ToList(); + var key = (group, filter); - return NodeController.Create(filter, group, items.Result.ToList()); + if (priorNode != null) + { + RenderAspects changeFlags = proxies.Select(p => p.Item2.ChangeFlags) + .Aggregate((a, b) => a | b); + + var node = await priorNode.Result.Refresh(proxies, changeFlags); + if (node != null) return node; + } + + return await NodeController.Create(filter, group, items.Result.ToList()); }) .Unwrap(); diff --git a/Editor/PreviewSystem/Rendering/ProxySession.cs b/Editor/PreviewSystem/Rendering/ProxySession.cs index 87c04665..5773fdab 100644 --- a/Editor/PreviewSystem/Rendering/ProxySession.cs +++ b/Editor/PreviewSystem/Rendering/ProxySession.cs @@ -28,6 +28,8 @@ internal class ProxySession : IObserver>, IDisposab internal ImmutableDictionary ProxyToOriginalObject => _active?.ProxyToOriginalObject ?? ImmutableDictionary.Empty; + internal ProxyObjectCache _proxyCache = new(); + public ProxySession(ReactiveValue> filters) { _filters = filters; @@ -53,6 +55,7 @@ public void Dispose() Reset(); EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; _unsubscribe?.Dispose(); + _proxyCache.Dispose(); } public void OnCompleted() @@ -84,7 +87,7 @@ public void OnNext(ImmutableList filters) if (activeNeedsReplacement && _next == null && _filters.TryGetValue(out var filters)) { - _next = new ProxyPipeline(filters.ToList(), _active); + _next = new ProxyPipeline(_proxyCache, filters.ToList(), _active); } if (activeNeedsReplacement && _next?.IsReady == true)