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
+53 -7
View File
@@ -10,6 +10,40 @@ Unpushed: `5ad0106` broadcast diagnostic → `74da99c` + `0fb2f9c` privacy/Test
`8fbc370` diagnostic parse fix → `07a088a` **the explicit-bind fix** → `509ef36` window placement →
`b2f2197` handoff → `UNIT 0` frame-cadence measurement.
## 🚧 IN FLIGHT 2026-10-05 — UNITS 4+5 landed: the render-budget work (needs a creator measurement)
**The 141ms → 33ms mystery is SOLVED, and it was geometry, not the build.** The opaque-backdrop 1:1
row walk only engages when the source rect is **exactly** 1920×1080. On a 21:9 monitor the source
rect shrinks to 1920×803 ("fit", not "fill"), so every render paid a ~2M-byte opaque-black prefill
plus a ~1.54M-pixel per-pixel alpha-branch resample instead of one `BlockCopy`. 2560×1440 fits to
1920×1080 exactly and takes the fast path. **Debug-vs-Release was NOT the cause** — today's Debug
build with the game running on 16:9 measured 33ms average. Every recorded A/B matches.
**Shipped (all pixel-identical, all gated):**
- **Unit 4** — opaque 1:1 fast paths widened to the whole-canvas case; **prefill only the uncovered
letterbox rows**; a **per-column LUT** for the scaled blit (source column + 8.8 weights computed
once per column, not a divide per pixel). Commits `0464593`, `08290ec`, `77d5120`, `dc13438`.
- **Unit 5** — **`Helpers/StripPool.cs`**: one composite splits into four horizontal row strips
across four DEDICATED `BelowNormal` background threads (the caller takes a strip too; inline below
64 rows). ⛔ **Do not "simplify" this back to `Parallel.For` with a thread-priority poke** — the
pool is shared, the demotion leaks into unrelated work, and it broke an audio test for real. Two
sources + the failure are recorded in `MyMistakes.md`; the design note is in `ai.md` →
"Render budget". Commits `623d77e`, `3901cb8`, + the strip-pool commit.
- Gate test `SceneCompositorTests.StripParallel_RepeatedRenders_AreByteIdentical` — 8 renders of a
multi-layer scene 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 that crashed the test host on run 1.
**⏭ NEXT STEP — one Release recording, 60s, Live scene, Division 2 running.** This is the
measurement that decides whether Unit 5 was necessary. Read the `FramePump stats` line (it now
carries `social`/`ticker`/`flash`/`acq` per-stage timings and the pump-start line names the scene +
compositor rect):
- **avg render ≤12ms** → Units 4+5 did it; go straight to **Unit 6** (encoder backpressure:
`worst submit` has hit 53ms with ~357 cumulative drops — real frame loss on top of the padding).
- **still >16.67ms** → the remaining cost is NOT the blit loops; read the per-stage timings before
touching anything else (do not guess — that is exactly the UNIT 0 lesson).
- **Under ~30ms on 16:9 but 140ms on 21:9** → geometry is still costing us; the fix is the tier
source-rect, not the compositor.
## 🚨 ANSWERED 2026-10-05 — the judder is the COMPOSITOR, not the camera. Unit 1 is the wrong next move.
Take `ty-20261005-1201-556-0001.mp4` (75.3s, 1920×1080, 60fps) + `startup.log`, read together:
@@ -417,7 +451,7 @@ Detail and the open 11.12 re-check: [`TASKS/task-50-publish-recording.md`](TASKS
## Test state
**402/402, 0 warnings, verified 2026-10-05** (395 + 6 `FrameCadenceTests` + 1 pump-stats integration
test). Build the **test** project, not just the app project, or VSTest runs a stale DLL.
test + 1 strip-parallel gate). Re-verified after UNITS 4+5. Build the **test** project, not just the app project, or VSTest runs a stale DLL.
**Launch exe for the creator's test run: `bin/Debug/net8.0-windows10.0.19041.0/ytLive.exe`.**
**Confirmed environmental flake, NOT a regression (re-checked 2026-10-05, and it now fails
@@ -427,6 +461,15 @@ fails identically on a stashed clean tree**, so it has nothing to do with UNIT 0
interactive desktop session this shell does not have (the mouse drag never lands). Do not chase it;
it is listed under Landmines as the known interactive-session test.
**⚠️ A game-playing session breaks the audio tests — know this before debugging it.** On
2026-10-05 `AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` failed with
480/480 samples at `0.4` where `0` was expected, then passed in isolation minutes later. Cause: the
user was in a **Division 2 boss fight**, and that test captures **audio loopback** — the game's own
output landed in it. **An audio test failing during a gaming session is an environment fact, not a
regression.** Same bucket as the interactive-desktop flake above: re-run in a quiet session before
believing either. (It also briefly failed while I was probing `Parallel.For` thread priorities,
which sent me after the wrong culprit entirely — the game was the simpler explanation.)
**⚠️ New flake, do not chase it as a regression:** on the *first* full-suite run,
`AudioPipelineTests.MasterLimiter_CapsTheLiveMix_OnThePipe` failed with `Expected 0.891,
Actual 0` — a **zero**, not a slightly-off value, which is the signature of the pipe not being
@@ -449,31 +492,34 @@ whole-suite scheduling artifact of the same family as the `RealAppHost` STA land
1. **~PUSH, then the creator's 60-second Test** — see the FIXED box above. Packaging is NOT next;
the creator's ruling 2026-10-04: still in active development, so ship-time work waits and queued
features come first.
2. **Webcam cadence → Unit 1 (exclusive lock + 720p).** UNIT 0 is shipped and the counters are in
2. **⏭ Read the Unit 4+5 Release recording (see the IN FLIGHT box at the top).** Then Unit 6
(encoder submit backpressure: `worst submit` 53ms, ~357 cumulative drops) if render is inside
budget; if render is still over, the per-stage timings name the stage — do not guess.
3. **Webcam cadence → Unit 1 (exclusive lock + 720p).** UNIT 0 is shipped and the counters are in
`startup.log`; the reading needs one 60-second creator recording (details + the exact
read-the-log-together table in the IN FLIGHT box above). Then Unit 1: exclusive camera lock +
720p, and Unit 2: the blinking SD badge. **Creator to-do, not yet queued: long-run
resource/consumption/recycling benchmarking** (how long the app stays healthy under full burden) —
record only, do not start it.
2. **The packaging project + `Package.appxmanifest`** (TASK 48 items 2–3) — the next code unit.
4. **The packaging project + `Package.appxmanifest`** (TASK 48 items 2–3) — the next code unit.
Full trust, `webcam`/`microphone`, English only; Velopack deleted. Add the
`DefaultRecordFolder()` comment in the same unit, and the WebView2 framework dependency +
graceful-failure path.
3. **TASK 50 — one-button "Publish this video"** (`TASKS/task-50-publish-recording.md`). Queued
5. **TASK 50 — one-button "Publish this video"** (`TASKS/task-50-publish-recording.md`). Queued
behind the Store submission by its own precedence note ("MSIX first, publish second — unless
the creator reverses that"). Scoping is done; the build is not. ⛔ Do **not** flip
`enableAutomaticArchiving` until it ships. It also carries an **open 11.12 re-check**: a
publish feature makes the app *push* creator content to a third party, so the IARC answers
must not go to certification describing an app that cannot publish.
4. **Declare the IAP products in Partner Center** — the price is **settled** ($10/mo, $400
6. **Declare the IAP products in Partner Center** — the price is **settled** ($10/mo, $400
lifetime, no annual, 15% verified), so this is unblocked. **Blocks:** the Store listing and the
`OverlayHost.xaml` copy. ⛔ The Individual-vs-Company account question is still unanswered —
get it right before enrolling.
5. **Polar teardown** (TASK 48 item 0) — `PolarLicenseService`, `PolarLicense`,
7. **Polar teardown** (TASK 48 item 0) — `PolarLicenseService`, `PolarLicense`,
`MainViewModel.License.cs`, the `OverlayHost.xaml` string, the `csproj`/`App.xaml.cs`
Velopack sites. `IsPremium` ← Store entitlement. **Do this as one unit** — a half-torn-down
licensing path is worse than either end state.
6. **Vertical recording verification** — the compositor tier is proven by
8. **Vertical recording verification** — the compositor tier is proven by
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
been run. `FramePump.cs:645` flags off-size tiers as a known follow-up.
7. **WACK + certification notes + the privacy policy** (TASK 48 items 4–5) — the policy must