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:
@@ -1,5 +1,5 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ytLive.Helpers;
|
||||
using ytLive.Models;
|
||||
|
||||
@@ -33,7 +33,6 @@ public sealed class SceneCompositor
|
||||
private static readonly Dictionary<SceneElement, VideoFrame> _elementRasters = new(ReferenceEqualityComparer.Instance);
|
||||
private static readonly object _pasteGate = new();
|
||||
private const int MaxPasteEntries = 256;
|
||||
private static float[] _sxLutCache = Array.Empty<float>();
|
||||
|
||||
/// <summary>
|
||||
/// Composite <paramref name="scene"/> into the tier's output frame. Layer order (back →
|
||||
@@ -397,8 +396,7 @@ public sealed class SceneCompositor
|
||||
var y1 = System.Math.Min(cropH, py + dh);
|
||||
if (x0 >= x1 || y0 >= y1) return;
|
||||
var s = raster.BgraPixels;
|
||||
var parallelOpts = new ParallelOptions { MaxDegreeOfParallelism = 4 };
|
||||
Parallel.For(y0, y1, parallelOpts, y =>
|
||||
StripPool.For(y0, y1, y =>
|
||||
{
|
||||
var si = ((y - py) * dw + (x0 - px)) * 4;
|
||||
var len = x1 - x0;
|
||||
@@ -547,37 +545,37 @@ public sealed class SceneCompositor
|
||||
var s = src.BgraPixels;
|
||||
var srcStride = src.Width; // canvas stride in pixels (not bytes)
|
||||
|
||||
// Build horizontal LUT for scaled blit to reduce per-pixel divides
|
||||
// Build horizontal LUT for scaled blit to reduce per-pixel divides. The table is
|
||||
// READ by the strip workers while it is being sampled, so it must not be shared
|
||||
// mutable state: it comes from the thread-safe ArrayPool and goes back in a finally
|
||||
// (the static single-buffer version raced the moment the strips went parallel).
|
||||
var lutCount = x1 - x0 + 1;
|
||||
if (_sxLutCache.Length < lutCount)
|
||||
var sxLut = ArrayPool<float>.Shared.Rent(lutCount);
|
||||
try
|
||||
{
|
||||
_sxLutCache = new float[Math.Max(lutCount, 4096)];
|
||||
}
|
||||
var sxLut = _sxLutCache;
|
||||
var pxBase = x0 - ex;
|
||||
for (var xi = 0; xi < lutCount; xi++)
|
||||
{
|
||||
var px = pxBase + xi;
|
||||
float sxCanvas;
|
||||
if (px < ox || px > ox + drawnW)
|
||||
var pxBase = x0 - ex;
|
||||
for (var xi = 0; xi < lutCount; xi++)
|
||||
{
|
||||
sxCanvas = float.NaN;
|
||||
var px = pxBase + xi;
|
||||
float sxCanvas;
|
||||
if (px < ox || px > ox + drawnW)
|
||||
{
|
||||
sxCanvas = float.NaN;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sxNorm = (px - ox) / drawnW;
|
||||
sxNorm = sxNorm < 0f ? 0f : sxNorm > 1f ? 1f : sxNorm;
|
||||
var sxCrop = sxNorm * cbW;
|
||||
sxCanvas = cbX + sxCrop;
|
||||
if (sxCanvas < 0f) sxCanvas = 0f;
|
||||
if (sxCanvas > src.Width - 1f) sxCanvas = src.Width - 1f;
|
||||
if (isMirror) sxCanvas = src.Width - 1f - sxCanvas;
|
||||
}
|
||||
sxLut[xi] = sxCanvas;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sxNorm = (px - ox) / drawnW;
|
||||
sxNorm = sxNorm < 0f ? 0f : sxNorm > 1f ? 1f : sxNorm;
|
||||
var sxCrop = sxNorm * cbW;
|
||||
sxCanvas = cbX + sxCrop;
|
||||
if (sxCanvas < 0f) sxCanvas = 0f;
|
||||
if (sxCanvas > src.Width - 1f) sxCanvas = src.Width - 1f;
|
||||
if (isMirror) sxCanvas = src.Width - 1f - sxCanvas;
|
||||
}
|
||||
sxLut[xi] = sxCanvas;
|
||||
}
|
||||
|
||||
var parallelOpts = new ParallelOptions { MaxDegreeOfParallelism = 4 };
|
||||
Parallel.For(y0, y1 + 1, parallelOpts, y =>
|
||||
StripPool.For(y0, y1 + 1, y =>
|
||||
{
|
||||
var py = y - ey;
|
||||
if (py < oy || py > oy + drawnH) return;
|
||||
@@ -658,7 +656,12 @@ public sealed class SceneCompositor
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<float>.Shared.Return(sxLut);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Two-stage bilinear in 8.8 fixed point; result within ±1 of the
|
||||
@@ -823,8 +826,7 @@ public sealed class SceneCompositor
|
||||
var dy0 = Math.Max(0, sy0 - options.SourceRectY);
|
||||
var dx1 = Math.Min(dstW, sx0 - options.SourceRectX + overlay.Width);
|
||||
var dy1 = Math.Min(dstH, sy0 - options.SourceRectY + overlay.Height);
|
||||
var parallelOpts = new ParallelOptions { MaxDegreeOfParallelism = 4 };
|
||||
Parallel.For(dy0, dy1, parallelOpts, y =>
|
||||
StripPool.For(dy0, dy1, y =>
|
||||
{
|
||||
var si = ((y + options.SourceRectY - sy0) * overlay.Width
|
||||
+ dx0 + options.SourceRectX - sx0) * 4;
|
||||
|
||||
Reference in New Issue
Block a user