Skip to content

Commit

Permalink
perf: cull preview nodes for disabled objects (#340)
Browse files Browse the repository at this point in the history
Closes: #317
  • Loading branch information
bdunderscore authored Aug 19, 2024
1 parent 20c98c3 commit 6fa2554
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 73 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

### Changed
- [#340] Render filters operating on disabled renderers are now culled (unless they declare that the might enable those
renderers) to save frametime.

### Removed

Expand Down
35 changes: 34 additions & 1 deletion Editor/PreviewSystem/IRenderFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace nadena.dev.ndmf.preview
public class RenderGroup
{
public ImmutableList<Renderer> Renderers { get; }

public bool IsEmpty => Renderers.IsEmpty;

internal RenderGroup(ImmutableList<Renderer> renderers)
{
Expand All @@ -47,6 +49,11 @@ public RenderGroup WithData<T>(T data)
return new RenderGroup<T>(Renderers, data);
}

internal virtual RenderGroup Filter(HashSet<Renderer> activeRenderers)
{
return new RenderGroup(Renderers.RemoveAll(r => !activeRenderers.Contains(r)));
}

public T GetData<T>()
{
return ((RenderGroup<T>)this).Context;
Expand Down Expand Up @@ -85,7 +92,12 @@ internal RenderGroup(ImmutableList<Renderer> renderers, T context) : base(render
{
Context = context;
}


internal override RenderGroup Filter(HashSet<Renderer> activeRenderers)
{
return new RenderGroup<T>(Renderers.RemoveAll(r => !activeRenderers.Contains(r)), Context);
}

private bool Equals(RenderGroup<T> other)
{
if (Context is IEnumerable l && other.Context is IEnumerable ol)
Expand Down Expand Up @@ -117,6 +129,27 @@ public override int GetHashCode()
[PublicAPI]
public interface IRenderFilter
{
/// <summary>
/// Set to true if this RenderFilter might enable a renderer which is otherwise disabled. If you do, this should
/// be performed by changing the `enabled` property of the proxy renderer.
///
/// Note that when enabling proxy renderers, it's up to you to consider the state of parent objects of the
/// original; if you set enabled to true, it'll be displayed.
///
/// If all render filters interacting with a particular renderer have CanEnableRenderers set to false, the
/// preview pipeline might skip all preview processing for that renderer.
/// </summary>
public virtual bool CanEnableRenderers => false;

/// <summary>
/// Indicates that the preview system must not remove renderers from a multi-node render group, even if they
/// are disabled. If this is set to true, then all renderers you return in a group from GetTargetGroups will be
/// included when that group is instantiated.
///
/// Note that even if this is true, the preview system can eliminate nodes where _all_ renderers are disabled.
/// </summary>
public virtual bool StrictRenderGroup => false;

public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context);

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions Editor/PreviewSystem/PreviewSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ public static void ClearCameraOverride(Camera target)

#endregion

internal IEnumerable<(Renderer, Renderer)> GetReplacements()
internal IEnumerable<(Renderer, Renderer)> OnPreCull(bool isSceneCamera)
{
return _proxySession?.OnPreCull() ?? Enumerable.Empty<(Renderer, Renderer)>();
return _proxySession?.OnPreCull(isSceneCamera) ?? Enumerable.Empty<(Renderer, Renderer)>();
}

internal ImmutableDictionary<Renderer, Renderer> OriginalToProxyRenderer =>
Expand Down
15 changes: 11 additions & 4 deletions Editor/PreviewSystem/ProxyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ private static void Initialize()

private static List<(Renderer, bool)> _resetActions = new();

private static bool IsSceneCamera(Camera cam)
{
return cam != null && SceneView.currentDrawingSceneView?.camera == cam;
}

private static bool ShouldHookCamera(Camera cam)
{
if (cam.name == "SceneCamera" && cam.gameObject.hideFlags == HideFlags.HideAndDontSave) return true;
if (cam.name == "TempCamera" && cam.targetTexture?.name == "ThumbnailCapture") return true;
if (cam.transform.parent == null) return true;

return false;
}
Expand All @@ -42,7 +47,8 @@ private static void OnPreCull(Camera cam)
{
ResetStates();

if (!ShouldHookCamera(cam)) return;
bool sceneCam = IsSceneCamera(cam);
if (!sceneCam && !ShouldHookCamera(cam)) return;

if (EditorApplication.isPlayingOrWillChangePlaymode) return;

Expand All @@ -52,7 +58,7 @@ private static void OnPreCull(Camera cam)
var sess = PreviewSession.Current;
if (sess == null) return;

foreach (var (original, replacement) in sess.GetReplacements())
foreach (var (original, replacement) in sess.OnPreCull(sceneCam))
{
// TODO: Optimize to cull meshes that don't have an active-state override registered
if (original == null || replacement == null || !original.enabled)
Expand All @@ -63,7 +69,8 @@ private static void OnPreCull(Camera cam)

_resetActions.Add((original, false));
_resetActions.Add((replacement, true));


// Note: don't set replacement.forceRenderingOff to false, as we might have culled it in sess.OnPreCull
replacement.forceRenderingOff = false;
original.forceRenderingOff = true;
}
Expand Down
34 changes: 33 additions & 1 deletion Editor/PreviewSystem/Rendering/ProxyObjectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
Expand All @@ -27,6 +28,10 @@ internal class ProxyObjectController : IDisposable
internal ComputeContext _monitorRenderer, _monitorMaterials, _monitorMesh;

internal Task OnInvalidate;

private bool _visibilityOffOriginal;
private bool _pickingOffOriginal, _pickingOffReplacement;
private long _lastVisibilityCheck = long.MinValue;

private static CustomSampler _onPreFrameSampler = CustomSampler.Create("ProxyObjectController.OnPreFrame");

Expand Down Expand Up @@ -130,7 +135,27 @@ internal bool OnPreFrame()
var target = _replacementRenderer;
var original = _originalRenderer;

target.gameObject.SetActive(original.enabled && original.gameObject.activeInHierarchy);
if (VisibilityMonitor.Sequence != _lastVisibilityCheck)
{
_pickingOffOriginal = SceneVisibilityManager.instance.IsPickingDisabled(original.gameObject);
_visibilityOffOriginal = SceneVisibilityManager.instance.IsHidden(original.gameObject);
}

target.enabled = original.enabled && original.gameObject.activeInHierarchy;

bool shouldDisablePicking = _pickingOffOriginal || _visibilityOffOriginal;

if (shouldDisablePicking != _pickingOffReplacement)
{
if (shouldDisablePicking)
{
SceneVisibilityManager.instance.DisablePicking(target.gameObject, false);
}
else
{
SceneVisibilityManager.instance.EnablePicking(target.gameObject, false);
}
}

SkinnedMeshRenderer smr = null;
if (_originalRenderer is SkinnedMeshRenderer smr_)
Expand Down Expand Up @@ -196,6 +221,11 @@ internal bool OnPreFrame()
}
}

internal void FinishPreFrame(bool isSceneViewCamera)
{
if (_replacementRenderer != null) _replacementRenderer.enabled &= !(isSceneViewCamera && _visibilityOffOriginal);
}

private void CreateReplacementObject()
{
if (_originalRenderer == null) return;
Expand Down Expand Up @@ -229,6 +259,8 @@ private void CreateReplacementObject()
return null;
}

renderer.forceRenderingOff = true;

return renderer;
});
}
Expand Down
85 changes: 22 additions & 63 deletions Editor/PreviewSystem/Rendering/ProxyPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,16 @@ class StageDescriptor

public readonly IRenderFilter Filter;
public readonly ImmutableList<RenderGroup> Originals;
public readonly ComputeContext Context;

public StageDescriptor(IRenderFilter filter, ComputeContext parentContext)
public StageDescriptor(TargetSet.Stage targetStage)
{
Filter = filter;

Context = new ComputeContext("StageDescriptor for " + Filter + " on " + parentContext);
Context.Invalidates(parentContext);

var unsorted = filter.GetTargetGroups(Context);

if (unsorted == null) unsorted = ImmutableList<RenderGroup>.Empty;
Originals = unsorted;

Originals = unsorted
.OrderBy(group => group.Renderers.First().GetInstanceID())
.ToImmutableList();
Filter = targetStage.Filter;
Originals = targetStage.Groups;
}

public StageDescriptor(StageDescriptor prior, ComputeContext parentContext)
public StageDescriptor(StageDescriptor prior)
{
Filter = prior.Filter;
Context = new ComputeContext("StageDescriptor for " + Filter + " on " + parentContext);
Context.Invalidates(parentContext);

Originals = prior.Originals;
}

Expand All @@ -67,6 +52,7 @@ public StageDescriptor(StageDescriptor prior, ComputeContext parentContext)
/// </summary>
internal class ProxyPipeline
{
private TargetSet _targetSet;
private List<StageDescriptor> _stages = new();
private Dictionary<Renderer, ProxyObjectController> _proxies = new();
private List<NodeController> _nodes = new(); // in OnFrame execution order
Expand Down Expand Up @@ -129,37 +115,29 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable<IRenderFilter>
System.Diagnostics.Debug.WriteLine($"Building pipeline {_generation}");
#endif

var filterList = filters.Where(f => f.IsEnabled(context)).ToList();
var filterList = filters.ToImmutableList();
_targetSet = priorPipeline?._targetSet?.Refresh(filterList) ?? new TargetSet(filterList);

var activeStages = _targetSet.ResolveActiveStages(context);

Dictionary<Renderer, Task<NodeController>> nodeTasks = new();
int total_nodes = 0;
int reused = 0;
int refresh_failed = 0;

for (int i = 0; i < filterList.Count(); i++)
for (int i = 0; i < activeStages.Count(); i++)
{
// TODO: Reuse logic

var filter = filterList[i];
var stageTemplate = activeStages[i];

Profiler.BeginSample("new StageDescriptor");
var priorStageDescriptor = priorPipeline?._stages.ElementAtOrDefault(i);
StageDescriptor stage;
if (priorStageDescriptor?.Filter == filter && (priorStageDescriptor?.Context.IsInvalidated == false))
{
stage = new StageDescriptor(priorStageDescriptor, context);
}
else
{
stage = new StageDescriptor(filter, context);
}
StageDescriptor stage = new StageDescriptor(stageTemplate);

Profiler.EndSample();

_stages.Add(stage);

var prior = priorPipeline?._stages.ElementAtOrDefault(i);
if (prior?.Filter != filter)
if (prior?.Filter != stage.Filter)
{
prior = null;
}
Expand All @@ -169,7 +147,6 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable<IRenderFilter>
{
total_nodes++;


groupIndex++;
var resolved = group.Renderers.Select(r =>
{
Expand All @@ -187,6 +164,8 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable<IRenderFilter>
proxy.OnInvalidate.ContinueWith(_ => InvalidateAction(),
TaskContinuationOptions.ExecuteSynchronously);
proxy.OnPreFrame();
// OnPreFrame can enable rendering, turn it off for now (until the pipeline goes active and
// we render for real).
_proxies.Add(r, proxy);

OriginalToProxyRenderer = OriginalToProxyRenderer.Add(r, proxy.Renderer);
Expand Down Expand Up @@ -231,7 +210,7 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable<IRenderFilter>
refresh_failed++;
}

return await NodeController.Create(filter, group, items.Result.ToList());
return await NodeController.Create(stage.Filter, group, items.Result.ToList());
})
.Unwrap();

Expand Down Expand Up @@ -270,35 +249,10 @@ await Task.WhenAll(_stages.SelectMany(s => s.NodeTasks))
}
}

public void OnFrame()
public void OnFrame(bool isSceneView)
{
if (!IsReady) return;

var manager = SceneVisibilityManager.instance;
foreach (var pair in _proxies)
{
var originalRenderer = pair.Key;
var proxyRenderer = pair.Value.Renderer;
if (originalRenderer == null || proxyRenderer == null) { continue; }
var originalObject = originalRenderer.gameObject;
var proxyObject = proxyRenderer.gameObject;

var isOriginHidden = manager.IsHidden(originalObject, true);
var needHiddenStateChange = isOriginHidden != manager.IsHidden(proxyObject, true);
if (needHiddenStateChange)
{
if (isOriginHidden) manager.Hide(proxyObject, true);
else manager.Show(proxyObject, true);
}

var isOriginPickingDisabled = manager.IsPickingDisabled(originalObject, true);
var needPickableStateChange = isOriginPickingDisabled != manager.IsPickingDisabled(proxyObject, true);
if (needPickableStateChange)
{
if (isOriginPickingDisabled) manager.DisablePicking(proxyObject, true);
else manager.EnablePicking(proxyObject, true);
}
}
foreach (var pair in _proxies)
{
pair.Value.OnPreFrame();
Expand All @@ -308,6 +262,11 @@ public void OnFrame()
{
node.OnFrame();
}

foreach (var pair in _proxies)
{
pair.Value.FinishPreFrame(isSceneView);
}
}

public void Dispose()
Expand Down
4 changes: 2 additions & 2 deletions Editor/PreviewSystem/Rendering/ProxySession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void Dispose()
_proxyCache.Dispose();
}

public IEnumerable<(Renderer, Renderer)> OnPreCull()
public IEnumerable<(Renderer, Renderer)> OnPreCull(bool isSceneCamera)
{
ShadowBoneManager.Instance.Update();

Expand Down Expand Up @@ -98,7 +98,7 @@ public void Dispose()

if (activeIsReady)
{
_active.OnFrame();
_active.OnFrame(isSceneCamera);
return _active.Renderers;
}
else
Expand Down
Loading

0 comments on commit 6fa2554

Please sign in to comment.