1c48849853
Take 5: render 58.9 -> 25.5ms (138/300, still ~2.2x). The blits were fixed; the resolver was not: ResolveOutputFrame -> RenderChatBox ran a FULL WPF raster (FormattedText + RenderTargetBitmap + CopyPixels + channel swap) EVERY tick whenever the chat buffer was non-empty — and the buffer survives sessions, so even a signed-out record-only take paid it. Established answer (OBS text sources): re-render on change, blit the cache every tick. ChatOverlayLayer: content version bumped from Messages.CollectionChanged (covers adds, the 500-cap removal, the fade Clear from any caller) + a config key (size + all Chat* appearance props); RenderFrame returns the cached VideoFrame by identity until either changes (compositor only reads cached frames). Conservative ordering (version latched BEFORE render) makes a mid-render message re-render next tick, never serve stale. ONE integration test: ChatOverlayLayerCacheTests (RealApp, real renderer): Same() for unchanged inputs, NotSame() on message/config change, null on empty. Full regression green (62 across touched classes), clean build 0 warnings. Accepted cost pending take 6: one ~15-25ms tick per arriving message; if live-chat bursts sag n/300, next slice = debounced off-tick re-render. Docs same commit: ai.md pipeline section, TASKS.md TASK 18, MyMistakes recipe (raster-on-change + session-surviving-buffer trap), HANDOFF (take 6 -> then Unit B, spec unchanged).
58 lines
2.2 KiB
C#
58 lines
2.2 KiB
C#
using System;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Take-5 starvation fix (slice 3): the chat layer is rendered ON CHANGE, not on
|
|
/// every pump tick — the per-tick WPF raster (FormattedText + RenderTargetBitmap
|
|
/// + CopyPixels) was the 25ms render. OBS text sources raster-on-change; this
|
|
/// pins that contract with the real renderer on the RealApp STA host: identical
|
|
/// inputs reuse the cached frame by identity, and content or config changes force
|
|
/// a fresh raster.
|
|
/// </summary>
|
|
[Collection("RealApp")]
|
|
public sealed class ChatOverlayLayerCacheTests
|
|
{
|
|
private readonly RealAppHost _app;
|
|
|
|
public ChatOverlayLayerCacheTests(RealAppHost app) => _app = app;
|
|
|
|
[Fact]
|
|
public void RenderFrame_RastersOnContentOrConfigChange_ReusesCacheOtherwise()
|
|
{
|
|
_app.Run(Run);
|
|
}
|
|
|
|
private void Run()
|
|
{
|
|
var layer = new ChatOverlayLayer(new ChatBoxRenderer());
|
|
var box = new Source { Type = SourceType.ChatBox, Name = "YouTube Chat", Width = 220, Height = 160 };
|
|
|
|
Assert.Null(layer.RenderFrame(box)); // empty buffer — nothing to rasterize
|
|
|
|
layer.Messages.Add(new ChatMessage { AuthorName = "a", Message = "hello", Timestamp = DateTime.Now });
|
|
var first = layer.RenderFrame(box);
|
|
Assert.NotNull(first);
|
|
|
|
Assert.Same(first, layer.RenderFrame(box)); // unchanged content + config → the SAME frame
|
|
Assert.Same(first, layer.RenderFrame(box)); // every tick hits the cache, not the raster
|
|
|
|
var resized = new Source { Type = SourceType.ChatBox, Width = 300, Height = 160 };
|
|
var reRendered = layer.RenderFrame(resized);
|
|
Assert.NotNull(reRendered);
|
|
Assert.NotSame(first, reRendered); // config change → fresh raster
|
|
|
|
layer.Messages.Add(new ChatMessage { AuthorName = "b", Message = "second", Timestamp = DateTime.Now });
|
|
var grown = layer.RenderFrame(box);
|
|
Assert.NotNull(grown);
|
|
Assert.NotSame(reRendered, grown); // new message → fresh raster
|
|
|
|
layer.Messages.Clear(); // the fade path
|
|
Assert.Null(layer.RenderFrame(box));
|
|
Assert.Null(layer.RenderFrame(box));
|
|
}
|
|
}
|