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:
2026-08-31 17:53:34 -07:00
parent 3bf053a3b1
commit 670fe3a10a
15 changed files with 618 additions and 54 deletions
+9 -1
View File
@@ -37,6 +37,11 @@ public partial class MainViewModel : ViewModelBase
// tier's FPS and paces frames into the encoder. The RTMP URL seam stays null
// until the live-stream create flow (TASK 5) supplies the reusable stream URL.
private readonly Func<string?> _rtmpUrlProvider;
// Scene graph (TASK 31): owns the scene collection + element inventory and the
// baked-crust compositor cache. The ViewModel exposes Scenes/StagedScene/LiveScene
// on top of it; the scene collection itself lives here.
private SceneGraph _sceneGraph = null!;
private readonly TransitionService _transition = new();
private WebView2Manager? _webView2Manager;
@@ -70,6 +75,7 @@ public partial class MainViewModel : ViewModelBase
{
if (e.PropertyName == nameof(SceneElement.UseDefaultBackground))
{
if (StagedScene != null) _sceneGraph.InvalidateBake(StagedScene);
UpdateBackgroundImage();
RefreshSnapshotsAsync();
ScheduleSave();
@@ -276,6 +282,7 @@ public partial class MainViewModel : ViewModelBase
_chatLayer = new ChatOverlayLayer(new ChatBoxRenderer());
_sceneGraph = new SceneGraph();
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
_framePump = new FramePump(
sceneProvider: () => StagedScene,
@@ -286,7 +293,8 @@ public partial class MainViewModel : ViewModelBase
log: message => AppLog.Write(message),
socialBar: () => (_socialBarFrame, _socials?.BarPosition ?? SocialBarPosition.Bottom),
transition: _transition,
fromSceneProvider: () => LiveScene);
fromSceneProvider: () => LiveScene,
sceneGraph: _sceneGraph);
_framePump.Failed += OnFramePumpFailed;
_framePump.HealthUpdated += OnFramePumpHealthUpdated;