feat(text): composite shared text rasters in output

Raster static text on UI-thread changes and reuse the same transparent BGRA frame in preview and all compositor paths. Cache text frame identity in the frame-pump signature and invalidate static bakes on edits. Follow OBS text-source conventions: https://obsproject.com/kb/text-sources
This commit is contained in:
2026-11-04 15:01:47 -08:00
parent 58eb640c4e
commit 5c4acf141b
19 changed files with 622 additions and 81 deletions
+4 -1
View File
@@ -47,6 +47,7 @@ public partial class MainViewModel : ViewModelBase
// born yet (startup crash regression from TASK 31, 2026-09-01; headless RealApp
// tests never ran it, so it waited for the first native launch).
private SceneGraph _sceneGraph = new();
internal SceneGraph SceneGraphForTests => _sceneGraph;
/// <summary>Alert audio plays on the default device (creator ruling 2026-11-01): the WASAPI
/// loopback then picks it up, so it is audible AND visible in the desktop sound bar.</summary>
@@ -497,6 +498,7 @@ public partial class MainViewModel : ViewModelBase
}
ClearPreviewHistory();
StagedScene = Scenes.FirstOrDefault(s => SceneCatalog.Is(s.Name, SceneCatalog.Starting)) ?? Scenes.FirstOrDefault();
UpdateConfiguredTextOverlays();
UpdateActiveBackground();
UpdateActiveChatBoxPreviews();
UpdateActiveAlertBoxPreviews();
@@ -585,7 +587,7 @@ public partial class MainViewModel : ViewModelBase
// Scene-element → latest frame, for the live compositor. The map mirrors the
// preview: webcam by DeviceId, live captures (the background) by CaptureKey,
// images/background by AssetId. A null frame leaves the element transparent.
private VideoFrame? ResolveOutputFrame(SceneElement element)
internal VideoFrame? ResolveOutputFrame(SceneElement element)
{
var frame = ResolveOutputFrameCore(element);
// Output-side eyes (2026-09-01): preview showed the webcam but the recording
@@ -644,6 +646,7 @@ public partial class MainViewModel : ViewModelBase
// (caught by the resolver logging, 2026-09-01 take two).
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(DeviceKeyForWebcam(webcam.WebcamId), EffectKeyFor(webcam)),
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
Source { Type: SourceType.TextOverlay } text => _textOverlayRenderer.RenderFrame(text),
Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox),
Source { Type: SourceType.Ticker } ticker => _alertLayer.RenderSceneTicker(ticker),
Source { Type: SourceType.Scoreboard } => _scoreboardPresenter?.Frame,