From 0ebba0be3bb78473c1cabfa63bfacf60ea06d76b Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Tue, 16 Jun 2026 20:06:54 +0200 Subject: [PATCH] Fix: Stop scene aging in finished playback state Added - State caching mechanism to RenderStateAssembler and ExtensionStatisticsAssembler - Snapshot caching to RenderEventBuffer for unchanged scene states Changed - SceneFrameUpdater now properly freezes delta time for `Finished` state, matching `Idle` and `Paused` - Re-enabled rendering cache logic to utilize identical snapshots instead of full rebuilds Result Cache hits now properly resolve during idle and finished playback states. Rendering interaction benchmarks (100000 events) demonstrate significant improvements: - SweepHoverAndRenderFrames: 726.256 ms -> 64.122 ms - PanZoomAndRenderFrames: 758.712 ms -> 44.596 ms - ToggleLayoutAndRenderFrames: 763.527 ms -> 59.904 ms Allocations per frame dropped drastically from ~179.5 MB to ~14 KB. --- .../Interfaces/IRenderStateAssembler.cs | 5 +- src/Rendering/Pipeline/RenderEventBuffer.cs | 11 +++- .../Pipeline/RenderFrameAssembler.cs | 8 ++- src/Rendering/Pipeline/RenderingPipeline.cs | 17 ++++-- src/Rendering/Pipeline/SceneFrameUpdater.cs | 20 +++++-- .../Hud/ExtensionStatisticsAssembler.cs | 18 +++++- src/Rendering/States/RenderStateAssembler.cs | 60 +++++++++++++++---- 7 files changed, 106 insertions(+), 33 deletions(-) diff --git a/src/Rendering/Interfaces/IRenderStateAssembler.cs b/src/Rendering/Interfaces/IRenderStateAssembler.cs index 448ef8c..51ee51b 100644 --- a/src/Rendering/Interfaces/IRenderStateAssembler.cs +++ b/src/Rendering/Interfaces/IRenderStateAssembler.cs @@ -30,10 +30,11 @@ RenderState Assemble( PlayerDiagnostics diagnostics, SceneNode? hoveredNode, HoveredPodHud? hoveredPod, - LayoutMode mode); + LayoutMode mode, + bool sceneUnchanged = false); /// /// Clears accumulated internal state. /// void Reset(); -} \ No newline at end of file +} diff --git a/src/Rendering/Pipeline/RenderEventBuffer.cs b/src/Rendering/Pipeline/RenderEventBuffer.cs index 80cadef..a007950 100644 --- a/src/Rendering/Pipeline/RenderEventBuffer.cs +++ b/src/Rendering/Pipeline/RenderEventBuffer.cs @@ -15,6 +15,7 @@ internal sealed class RenderEventBuffer(RenderEventKinds renderEvents) : IDispos renderEvents); private RenderEventKinds _renderEvents = renderEvents; + private bool _hasBufferedEvents; /// /// Updates enabled render event kinds and resets aggregation state. @@ -46,17 +47,21 @@ public void Add( { _aggregation.Process( evt); + _hasBufferedEvents = true; } } /// /// Flushes buffered events into the rendering pipeline. /// - public void FlushTo( + public bool FlushTo( RenderingPipeline pipeline) { lock (_sync) { + if (!_hasBufferedEvents) + return false; + _aggregation.Flush(); foreach (var t in RenderEventDispatchTable.Table) @@ -72,6 +77,8 @@ public void FlushTo( } _aggregation.Clear(); + _hasBufferedEvents = false; + return true; } } @@ -85,4 +92,4 @@ public void Dispose() _aggregation.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/Rendering/Pipeline/RenderFrameAssembler.cs b/src/Rendering/Pipeline/RenderFrameAssembler.cs index 0ff0941..aec7fd4 100644 --- a/src/Rendering/Pipeline/RenderFrameAssembler.cs +++ b/src/Rendering/Pipeline/RenderFrameAssembler.cs @@ -25,7 +25,8 @@ public void SubmitFrame( PlayerDiagnostics diagnostics, SceneNode? hoveredNode, HoveredPodHud? hoveredPod, - LayoutMode layoutMode) + LayoutMode layoutMode, + bool sceneUnchanged = false) { var state = assembler.Assemble( @@ -38,9 +39,10 @@ public void SubmitFrame( diagnostics, hoveredNode, hoveredPod, - layoutMode); + layoutMode, + sceneUnchanged); output.Submit( state); } -} \ No newline at end of file +} diff --git a/src/Rendering/Pipeline/RenderingPipeline.cs b/src/Rendering/Pipeline/RenderingPipeline.cs index 18be3d9..40c9cf1 100644 --- a/src/Rendering/Pipeline/RenderingPipeline.cs +++ b/src/Rendering/Pipeline/RenderingPipeline.cs @@ -146,23 +146,27 @@ internal void OnProgress( ClearSceneState(); } - _events.FlushTo( - this); + var hadNewEvents = + _events.FlushTo( + this); - UpdateFrame(); + UpdateFrame( + hadNewEvents); } /// /// Updates simulation, hover state, diagnostics, and frame output. /// - private void UpdateFrame() + private void UpdateFrame( + bool hadNewEvents) { var diagnostics = Player.GetDiagnostics(); float dt = _frameUpdater.Tick( - diagnostics); + diagnostics, + hadNewEvents); _hover.Tick(); @@ -183,7 +187,8 @@ private void UpdateFrame() diagnostics, _hover.HoveredNode, _hover.HoveredPod, - _options.Mode); + _options.Mode, + sceneUnchanged: !hadNewEvents && dt == 0f); } /// diff --git a/src/Rendering/Pipeline/SceneFrameUpdater.cs b/src/Rendering/Pipeline/SceneFrameUpdater.cs index 441715d..43193b4 100644 --- a/src/Rendering/Pipeline/SceneFrameUpdater.cs +++ b/src/Rendering/Pipeline/SceneFrameUpdater.cs @@ -22,14 +22,16 @@ internal sealed class SceneFrameUpdater( /// Ticks all frame-level scene systems and returns clamped delta time. /// public float Tick( - PlayerDiagnostics diagnostics) + PlayerDiagnostics diagnostics, + bool hadNewEvents = false) { float dt = CalculateDeltaTime( diagnostics); StepLayout( diagnostics, - dt); + dt, + hadNewEvents); anim.Tick( dt); @@ -68,7 +70,8 @@ private float CalculateDeltaTime( currentWallTime; if (diagnostics.State == PlayerState.Idle || - diagnostics.State == PlayerState.Paused) + diagnostics.State == PlayerState.Paused || + diagnostics.State == PlayerState.Finished) { return 0f; } @@ -83,11 +86,18 @@ private float CalculateDeltaTime( /// private void StepLayout( PlayerDiagnostics diagnostics, - float dt) + float dt, + bool hadNewEvents) { if (dt <= 0f) return; + if (!hadNewEvents && + layout.Energy <= 0f) + { + return; + } + float speedBoost = (float)Math.Max( 1.0, @@ -126,4 +136,4 @@ private void StepLayout( /// public void ResetTime() => _lastWallTime = 0; -} \ No newline at end of file +} diff --git a/src/Rendering/States/Hud/ExtensionStatisticsAssembler.cs b/src/Rendering/States/Hud/ExtensionStatisticsAssembler.cs index f5af838..9904db4 100644 --- a/src/Rendering/States/Hud/ExtensionStatisticsAssembler.cs +++ b/src/Rendering/States/Hud/ExtensionStatisticsAssembler.cs @@ -9,11 +9,18 @@ namespace ChangeTrace.Rendering.States.Hud; /// internal sealed class ExtensionStatisticsAssembler { + private IReadOnlyList _cached = []; + /// /// Collects and ranks the most common file extensions in the scene. /// - public IReadOnlyList Assemble(ISceneGraph scene) + public IReadOnlyList Assemble( + ISceneGraph scene, + bool sceneUnchanged) { + if (sceneUnchanged) + return _cached; + var counts = new Dictionary(); foreach (var node in scene.Nodes.Values) @@ -28,10 +35,15 @@ public IReadOnlyList Assemble(ISceneGraph scene) counts.GetValueOrDefault(node.Extension) + 1; } - return counts + _cached = counts .OrderByDescending(x => x.Value) .Take(8) .Select(x => new ExtensionStat(x.Key, x.Value)) .ToList(); + + return _cached; } -} \ No newline at end of file + + public void Reset() => + _cached = []; +} diff --git a/src/Rendering/States/RenderStateAssembler.cs b/src/Rendering/States/RenderStateAssembler.cs index 12dfebc..101e436 100644 --- a/src/Rendering/States/RenderStateAssembler.cs +++ b/src/Rendering/States/RenderStateAssembler.cs @@ -29,6 +29,9 @@ internal sealed class RenderStateAssembler : IRenderStateAssembler private readonly LeaderboardAssembler _leaderboard = new(); private readonly HudStateAssembler _hud = new(); + private ISceneSnapshot _cachedSceneSnapshot = SceneSnapshot.Empty; + private bool _hasCachedSceneSnapshot; + /// /// Records contributor activity for leaderboard tracking. /// @@ -39,7 +42,15 @@ public void RecordActorEvent(string actor, string commitSha) => /// Clears accumulated render-related state. /// public void Reset() => + ResetCaches(); + + private void ResetCaches() + { _leaderboard.Reset(); + _extensions.Reset(); + _cachedSceneSnapshot = SceneSnapshot.Empty; + _hasCachedSceneSnapshot = false; + } private static Dictionary BuildNodeIndex( IReadOnlyList nodes) @@ -65,14 +76,15 @@ public RenderState Assemble( PlayerDiagnostics diagnostics, SceneNode? hoveredNode, HoveredPodHud? hoveredPod, - LayoutMode layoutMode) + LayoutMode layoutMode, + bool sceneUnchanged = false) { - var nodeSnapshots = _nodes.Assemble(scene.Nodes); - var nodeIndex = BuildNodeIndex(nodeSnapshots); - var avatarSnapshots = _avatars.Assemble(scene.Avatars, out var activeAvatarsCount); - var edgeSnapshots = _edges.Assemble(scene, nodeIndex); - var particleSnapshots = _particles.Assemble(animationSystem); - var extensions = _extensions.Assemble(scene); + int activeAvatarsCount = + scene.Avatars.Count(static avatar => avatar.Value.ActivityLevel > 0.1f); + + var extensions = _extensions.Assemble( + scene, + sceneUnchanged); var leaderboard = _leaderboard.Assemble(); var hudState = @@ -88,11 +100,10 @@ public RenderState Assemble( leaderboard); var sceneSnapshot = - SceneSnapshotMaterializer.Create( - nodeSnapshots, - avatarSnapshots, - edgeSnapshots, - particleSnapshots); + GetOrCreateSceneSnapshot( + scene, + animationSystem, + sceneUnchanged); return new RenderState( virtualTime, @@ -103,4 +114,29 @@ public RenderState Assemble( layoutMode, diagnostics.ManagedMemoryMb); } + + private ISceneSnapshot GetOrCreateSceneSnapshot( + ISceneGraph scene, + IAnimationSystem animationSystem, + bool sceneUnchanged) + { + if (sceneUnchanged && _hasCachedSceneSnapshot) + return _cachedSceneSnapshot; + + var nodeSnapshots = _nodes.Assemble(scene.Nodes); + var nodeIndex = BuildNodeIndex(nodeSnapshots); + var avatarSnapshots = _avatars.Assemble(scene.Avatars, out _); + var edgeSnapshots = _edges.Assemble(scene, nodeIndex); + var particleSnapshots = _particles.Assemble(animationSystem); + + _cachedSceneSnapshot = + SceneSnapshotMaterializer.Create( + nodeSnapshots, + avatarSnapshots, + edgeSnapshots, + particleSnapshots); + + _hasCachedSceneSnapshot = true; + return _cachedSceneSnapshot; + } }