eb4c379b91
Take 10 (59a02a5b, slice 6) finally produced a self-contradicting stat: render 22.4ms + submit 2.5 against a 16.7ms deadline, yet avg wait 10ms — a rebasing pacer CANNOT sleep after a blown deadline. The wait was queue time: StartAsync fires from a UI command handler, and async continuations re-capture the current SynchronizationContext — the 'WPF-free, hermetic' frame pump had been rendering ON THE DISPATCHER behind the live preview the entire starvation saga. OBS keeps obs_graphics_thread/video_thread off-UI for exactly this reason (dedicated threads; see docs.obsproject.com/backend-design 'Libobs Threads'). - FramePump: _pumpTask = Task.Run(() => PumpAsync(...)) — null context inside, every continuation stays on the pool. - Audited, not ignored, what that exposes: StaticPixelCache.Get now locks (pool miss-decodes raced UI callers); ChatOverlayLayer.RenderFrame checks its cache off-thread but marshals the rare raster MISS to the dispatcher (DrawingVisual + RenderTargetBitmap are UI-thread objects) and re-validates there; pump events already marshal in the VM. - GCLatencyMode.SustainedLowLatency for the pump's life (restored in finally). - Stats gained 'worst render Xms' — bimodal averages hid per-tick spikes. - Webcam routes through the paste cache (the IsOpaque bypass re-sampled ~156k px every tick even between identical device frames). ONE integration test: Pump_Produces_OffTheStartingContext — an inline-pumping SynchronizationContext makes the old construction run the resolver on the starting thread by capture; the loop must never. 70/70 per-class green, clean build 0 warnings. Docs same commit (ai.md slice 7, TASKS take-11 gate, MyMistakes #6, HANDOFF). take 11: ~300/300 + honest wait -> saga closed, Unit B (two-line top bar spec, fully captured) starts.
54 lines
1.9 KiB
C#
54 lines
1.9 KiB
C#
using System.IO;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
|
|
namespace ytLive.Services.Compositor;
|
|
|
|
/// <summary>
|
|
/// Decodes a layout asset's bytes into a tightly-packed BGRA8 <see cref="VideoFrame"/>
|
|
/// once per content-hash asset id. The preview uses the WPF <c>BitmapImage</c> in
|
|
/// ImageCache; the output path needs raw pixels, so assets decode here instead.
|
|
/// </summary>
|
|
public static class StaticPixelCache
|
|
{
|
|
private static readonly Dictionary<string, VideoFrame> Cache = new(StringComparer.Ordinal);
|
|
|
|
public static VideoFrame? Get(string assetId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(assetId)) return null;
|
|
// Locked: the frame pump decodes from its own thread now (take-8 off-UI fix),
|
|
// so the dictionary genuinely races with UI-path callers (snapshots, heals).
|
|
lock (Cache)
|
|
{
|
|
if (Cache.TryGetValue(assetId, out var frame)) return frame;
|
|
|
|
var bytes = LayoutStore.Instance?.GetAssetBytes(assetId);
|
|
if (bytes == null || bytes.Length == 0) return null;
|
|
|
|
var decoded = Decode(bytes);
|
|
if (decoded != null) Cache[assetId] = decoded;
|
|
return decoded;
|
|
}
|
|
}
|
|
|
|
public static VideoFrame? Decode(byte[] bytes)
|
|
{
|
|
try
|
|
{
|
|
using var stream = new MemoryStream(bytes, writable: false);
|
|
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
|
|
var source = decoder.Frames[0];
|
|
var bgra = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
|
|
var width = bgra.PixelWidth;
|
|
var height = bgra.PixelHeight;
|
|
var pixels = new byte[width * height * 4];
|
|
bgra.CopyPixels(pixels, width * 4, 0);
|
|
return new VideoFrame(width, height, pixels);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|