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