Perf: dedicated strip-parallel composite (4 threads, BelowNormal) — UNIT 5

A composite splits into four horizontal row strips across four DEDICATED
background threads; the calling thread takes a strip too (so four workers
never mean five-way oversubscription), and anything under 64 rows runs
inline. BlitContentRaw, BlitBakedElement and BlitOverlay now fan out.
Output is byte-identical — strips already partitioned by row.

The demotion deliberately does NOT come from Parallel.For. The pool is
shared, so Thread.CurrentThread.Priority = BelowNormal inside it leaks
into every unrelated work item that thread later runs — a full test run
after trying it broke AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_
KillsTheLoopback. Established guidance agrees: use your own producer/consumer
queue with your own Threads and set their priority yourself
(https://stackoverflow.com/questions/524483/net-backgroundworker-objects-thread-priority,
https://stackoverflow.com/questions/1578599/c-sharp-lower-thread-priority-in-thread-pool).
Helpers/StripPool.cs is that queue. Four threads is the measured sweet
spot: fewer leaves render over budget, more steals cycles from the game
and the encoder and made render worse.

The scaled blit's column LUT moves from a shared static buffer to
ArrayPool<float>.Shared with a finally — a single static buffer is a data
race the moment strips run in parallel.

Gate: SceneCompositorTests.StripParallel_RepeatedRenders_AreByteIdentical —
8 renders of a scaled round webcam + mirrored image + master-sized flash over
a short backdrop must be byte-for-byte identical, and must differ from a
backdrop-only render so it cannot pass vacuously. It caught a loop-variable
capture (new Thread(() => Worker(i)) saw i == Workers) that crashed the test
host on the first run.
This commit is contained in:
2026-10-05 18:54:47 -07:00
parent 3901cb8298
commit 6b6284d05c
7 changed files with 452 additions and 42 deletions
+70 -2
View File
@@ -1,4 +1,4 @@
using Xunit;
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Compositor;
@@ -28,7 +28,7 @@ public class SceneCompositorTests
}
/// <summary>Left half = leftColor, right half = rightColor.</summary>
private static VideoFrame Split(int w, int h, byte lr, byte lg, byte lb, byte rr, byte rg, byte rb)
public static VideoFrame Split(int w, int h, byte lr, byte lg, byte lb, byte rr, byte rg, byte rb)
{
var pixels = new byte[w * h * 4];
var half = w / 2;
@@ -509,6 +509,73 @@ public class SceneCompositorTests
AssertColor(bottom, 0, 1039, 255, 0, 0); // background just above the bar
AssertColor(bottom, 960, 540, 255, 0, 0); // bar region moved away from center
}
/// <summary>Unit 5 gate — the compositor now fans a composite out across four dedicated
/// BelowNormal strip workers (Helpers/StripPool), and the column LUT that the scaled blit
/// samples is read by those workers while the call is still in flight. Any strip writing
/// outside its own rows, or a LUT buffer shared between concurrent composites, corrupts a
/// handful of pixels NON-deterministICALLY, so comparing one render to the next is the
/// probe: the same scene must come out byte-for-byte identical every time. The canvas is
/// 240 rows tall (past the fan-out threshold) and carries a scaled round mirrored webcam, a
/// scaled image layer and a master-sized flash, so the three fanned-out paths
/// (BlitContentRaw, BlitBakedElement, BlitOverlay) all run. The color probe keeps the
/// test from passing vacuously on an all-black frame.</summary>
[Fact]
public void StripParallel_RepeatedRenders_AreByteIdentical()
{
const int W = 320, H = 240;
var backdrop = SceneCompositorTests.Solid(W, 200, 12, 34, 56); // shorter than the canvas: prefill bands
var webcamFrame = SceneCompositorTests.Split(64, 48, 255, 0, 0, 0, 255, 0);
var imageFrame = SceneCompositorTests.Split(48, 48, 0, 0, 255, 255, 255, 0);
var flash = new VideoFrame(W, H, new byte[W * H * 4]);
for (var i = 0; i < flash.BgraPixels.Length; i += 4)
{
flash.BgraPixels[i] = 200;
flash.BgraPixels[i + 1] = 200;
flash.BgraPixels[i + 2] = 200;
flash.BgraPixels[i + 3] = 32;
}
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var webcam = new WebcamSceneConfig { X = 20, Y = 30, Width = 200, Height = 120, ClipShape = ClipShape.Round, Opacity = 0.8 };
var image = new Source { Type = SourceType.Image, X = 180, Y = 140, Width = 100, Height = 70, IsMirrored = true };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(background);
scene.Elements.Add(webcam);
scene.Elements.Add(image);
VideoFrame? FrameFor(SceneElement e) => e switch
{
Source { IsBackground: true } => backdrop,
WebcamSceneConfig => webcamFrame,
Source { Type: SourceType.Image } => imageFrame,
_ => null,
};
var options = new CompositorOptions
{
SourceRectX = 0, SourceRectY = 0, SourceRectWidth = W, SourceRectHeight = H,
OutputWidth = W, OutputHeight = H,
};
var compositor = new SceneCompositor();
byte[]? reference = null;
for (var run = 0; run < 8; run++)
{
var output = compositor.Render(scene, FrameFor, flash, options);
if (reference == null) reference = output.BgraPixels.ToArray();
else Assert.True(reference.AsSpan().SequenceEqual(output.BgraPixels),
$"run {run} diverged from run 0 — strip writes or the shared column LUT raced");
}
// Not vacuous: drop the two layers and the render must change, so the byte-identity
// above is comparing a real composite and not an untouched backdrop.
var backdropOnly = new Scene { Name = "Live" };
backdropOnly.Elements.Add(background);
var baseline = compositor.Render(backdropOnly, FrameFor, flash, options).BgraPixels;
Assert.False(baseline.AsSpan().SequenceEqual(reference!),
"scene rendered identical to the backdrop alone — the test would prove nothing");
}
}
public class StretchMathTests
@@ -549,3 +616,4 @@ public class StretchMathTests
Assert.Equal(255, scaled.BgraPixels[i + 1]); // solid green survives the scale
}
}