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; + } }