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
+3 -3
View File
@@ -53,7 +53,7 @@ public partial class MainViewModel
Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : 0,
Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : 0,
};
scene.Elements.Add(source);
_sceneGraph.AddElement(scene, source);
if (sourceType == SourceType.WebSource)
{
@@ -208,7 +208,7 @@ public partial class MainViewModel
source.Y = (1080 - source.Height) / 2;
}
scene.Elements.Add(source);
_sceneGraph.AddElement(scene, source);
SelectedElement = source;
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
@@ -233,7 +233,7 @@ public partial class MainViewModel
}
if (element is Source { Type: SourceType.WebSource } webSource)
_webView2Manager?.Unregister(webSource.Id);
scene.Elements.Remove(element);
_sceneGraph.RemoveElement(scene, element);
if (element is WebcamSceneConfig && _webcam != null
&& !Scenes.Any(s => s.Elements.OfType<WebcamSceneConfig>().Any()))