TASK 31: SceneGraph component + baked-crust compositor optimization
- Add ElementKind (Static/Dynamic) to SceneElement base; Source/WebcamSceneConfig classify - Services/SceneGraph.cs: owns the Scenes collection (ViewModel's Scenes delegates to it), the element mutation surface (Add/Insert/Remove/Move, each invalidating the bake), and the queries that were scattered LINQ (GetBackground/GetWebcam/GetChatBoxes/GetSplitPoint/IsStatic) - SceneCompositor: split-aware BakeStaticBase + CompositeLayers + Render(.., staticBase, split); builds/caches the static base below the split point in source-rect space - FramePump: optional SceneGraph -> optimized bake+composite path; falls back to full render - MainViewModel: routes element mutations through the graph; invalidates the bake on static layout/opacity/visibility/useDefaultBackground changes and after background heal/Ensure - Integration test SceneGraphTests.BakedStaticBase_WithDynamicLayer_CompositesCorrectly Derivative survey (mandated): tried before writing — OBS does per-source opacity/visibility caching and static-scene baking; this mirrors OBS's 'cached static source' optimization. 3 documented defensive deviations from the spec (ChatOverlayLayer stays decoupled; background helpers stay VM-static for direct testability; full facade peel deferred post-1.0) in TASKS.md + ai.md. Scope check passed; clean build 0 warnings.
This commit is contained in:
@@ -31,6 +31,7 @@ public sealed class FramePump : IDisposable
|
||||
private readonly Func<(VideoFrame? Frame, SocialBarPosition Position)>? _socialBar;
|
||||
private readonly TransitionService? _transition;
|
||||
private readonly Func<Scene?>? _fromSceneProvider;
|
||||
private readonly SceneGraph? _sceneGraph;
|
||||
private readonly SceneCompositor _compositor = new();
|
||||
|
||||
private readonly object _gate = new();
|
||||
@@ -55,7 +56,8 @@ public sealed class FramePump : IDisposable
|
||||
Func<TimeSpan, CancellationToken, Task>? pacingDelay = null,
|
||||
Func<(VideoFrame? Frame, SocialBarPosition Position)>? socialBar = null,
|
||||
TransitionService? transition = null,
|
||||
Func<Scene?>? fromSceneProvider = null)
|
||||
Func<Scene?>? fromSceneProvider = null,
|
||||
SceneGraph? sceneGraph = null)
|
||||
{
|
||||
_sceneProvider = sceneProvider ?? throw new ArgumentNullException(nameof(sceneProvider));
|
||||
_frameResolver = frameResolver ?? throw new ArgumentNullException(nameof(frameResolver));
|
||||
@@ -67,6 +69,7 @@ public sealed class FramePump : IDisposable
|
||||
_socialBar = socialBar;
|
||||
_transition = transition;
|
||||
_fromSceneProvider = fromSceneProvider;
|
||||
_sceneGraph = sceneGraph;
|
||||
}
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
@@ -240,16 +243,16 @@ public sealed class FramePump : IDisposable
|
||||
{
|
||||
var fromScene = _fromSceneProvider();
|
||||
var fromFrame = fromScene != null
|
||||
? _compositor.Render(fromScene, _frameResolver, null, compositorOptions, socialBarFrame, socialBarTop)
|
||||
: _compositor.Render(scene, _frameResolver, null, compositorOptions, socialBarFrame, socialBarTop);
|
||||
var toFrame = _compositor.Render(scene, _frameResolver, null, compositorOptions, socialBarFrame, socialBarTop);
|
||||
? RenderScene(fromScene, compositorOptions, socialBarFrame, socialBarTop)
|
||||
: RenderScene(scene, compositorOptions, socialBarFrame, socialBarTop);
|
||||
var toFrame = RenderScene(scene, compositorOptions, socialBarFrame, socialBarTop);
|
||||
frame = transition.BlendFrame(toFrame);
|
||||
transition.Tick(lastTick.Elapsed.TotalMilliseconds);
|
||||
lastTick.Restart();
|
||||
}
|
||||
else
|
||||
{
|
||||
frame = _compositor.Render(scene, _frameResolver, null, compositorOptions, socialBarFrame, socialBarTop);
|
||||
frame = RenderScene(scene, compositorOptions, socialBarFrame, socialBarTop);
|
||||
lastTick.Restart();
|
||||
}
|
||||
|
||||
@@ -287,6 +290,39 @@ public sealed class FramePump : IDisposable
|
||||
|
||||
private void OnHealthUpdated(object? sender, StreamHealth health) => HealthUpdated?.Invoke(this, health);
|
||||
|
||||
/// <summary>Render a scene, using the baked-crust optimization when a <see cref="SceneGraph"/>
|
||||
/// is wired in: bake/cache the static layers below the split point, then composite the
|
||||
/// dynamic/above-split layers per frame. Without a SceneGraph, falls back to a full render
|
||||
/// (identical output — see SceneCompositorTests).</summary>
|
||||
private VideoFrame RenderScene(
|
||||
Scene scene,
|
||||
CompositorOptions options,
|
||||
VideoFrame? socialBarFrame,
|
||||
int socialBarTop)
|
||||
{
|
||||
if (_sceneGraph == null)
|
||||
return _compositor.Render(scene, _frameResolver, null, options, socialBarFrame, socialBarTop);
|
||||
|
||||
var split = _sceneGraph.GetSplitPoint(scene);
|
||||
if (split == scene.Elements.Count)
|
||||
{
|
||||
// Fully static scene: bake once, reuse.
|
||||
var baked = _sceneGraph.GetBakedBase(scene, _frameResolver, _compositorOptions);
|
||||
if (baked != null)
|
||||
return StretchMath.BilinearScale(baked, options.OutputWidth, options.OutputHeight);
|
||||
}
|
||||
|
||||
var baseFrame = _sceneGraph.GetBakedBase(scene, _frameResolver, _compositorOptions);
|
||||
if (baseFrame != null)
|
||||
{
|
||||
return SceneCompositor.CompositeLayers(
|
||||
baseFrame, scene, split, _frameResolver, options, socialBarFrame, socialBarTop);
|
||||
}
|
||||
|
||||
// No static base (first layer is dynamic or empty scene) — full render.
|
||||
return _compositor.Render(scene, _frameResolver, null, options, socialBarFrame, socialBarTop);
|
||||
}
|
||||
|
||||
private void OnProcessFailed(object? sender, string message)
|
||||
{
|
||||
_log?.Invoke($"FramePump: encoder process failed: {message}");
|
||||
|
||||
Reference in New Issue
Block a user