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.
The creator is DHH and lipreads their own recordings, so a webcam that
judders is an accessibility defect, not a polish item. Take
ty-20261005-1001-821-0001.mp4 holds 25.6 distinct webcam images/s against
the camera's 30fps with 281 gaps of 50-100ms - roughly one hitch in five -
in a file that muxes flawlessly. Nothing in the app could see that: rawvideo
carries no per-frame timestamps (ffmpeg stamps by ARRIVAL at the declared
60fps), and the stats line divided every average by the number of frames
SUBMITTED, which deadline padding always fills to the full target. So the
pump reported "300/300 frames, avg render 12.4ms" while producing 60
distinct composites.
Helpers/FrameCadence.cs is one pure counter fed from three points:
MediaCaptureFrameSource logs the device's own SystemRelativeTime stamps
against our arrival times (TryAcquireLatestFrame keeps only the LATEST
frame, so falling behind is silent by design), and FramePump logs the
distinct webcam Epoch values that reached a composite. Read together they
name the culprit - device/USB, our reader, or our compositor - instead of
guessing. Render/resolve/wait now divide by the real render count, and
"frames per 5s from R renders" is the duplicate-on-lag ratio stated
honestly.
An out-of-order timestamp counts as a frame but produces no gap and does not
become the next interval's reference, so one bad stamp cannot invent a
multi-second hitch twice.
Also corrects a stale ai.md line (the "webcam renders at ~18fps" figure
came from a different session and counted every newly rendered layer) and
re-records the background-removal ruling as permanently out. Records the
agreed-but-unbuilt Units 1-2, the no-fallback-camera ruling, and the
SetFormatAsync dead end found by probing the 19041 projection.
Suite 402/402, 0 warnings. The one red, LayerReorderPersistenceTests'
RealMouseDrag, fails identically on a stashed clean tree - it needs an
interactive desktop session this shell lacks.
Pre-1.0 the creator streams in one instance and screen-captures it from
another. Nothing prevented a second instance - there is no single-instance
mutex and no port anywhere. What broke it was SHARED STATE, and two of the
collisions were hard failures rather than annoyances:
- the layout DB. The model is read-whole-scene / write-whole-scene, so two
instances saving different layouts clobber each other.
- the WebView2 user data folder. Chromium takes an exclusive lock on it, so
the second instance of the same exe does not start at all.
- the auth token store. A test instance would overwrite the real YouTube
sign-in with its own.
- startup.log, where two appenders interleave and a crash in either instance
becomes ambiguous.
Set YTLIVE_INSTANCE=<id> and the process gets a private root at
%APPDATA%\ytLlive\instances/<id>/ for those four. Recording folder and the
ffmpeg tools cache stay shared on purpose - the cache should be shared, and
the creator picks the record folder. Global hotkeys stay un-namespaced: if
both instances register the same one, Windows refusing the second is the
correct answer.
An id that is not letters/digits/dash/underscore is rejected and degrades to
the primary profile, so the variable can never walk out of the profile
directory or name a UNC path.
The entire implementation is inside #if DEBUG. A Release build compiles to
DataRoot => DefaultRoot and WebViewDataFolder => null, and the call sites are
unconditional so Release cannot drift by forgetting an #if. The harness lives
with the tests: InstanceIsolationTests covers the two-identities contract,
the primary-instance no-op, per-instance WebView folders, path-traversal
rejection, that the real output paths actually move with the profile, and a
source-level assertion that the #else arm IS production behaviour.
Verified against a real Release build: the InstanceVariable field is absent
from its metadata and no "instances" path segment survives, while DataRoot
and WebViewDataFolder are present in both configurations. Grepping for
YTLIVE_INSTANCE proves nothing - a const is inlined at compile time and
appears in neither build, which cost one wasted verification round.
Docs for this unit (the InstanceProfile paragraph in ai.md, the 1.0 gate in
TASKS.md, and the MyMistakes/HANDOFF entries) landed in the previous commit,
because they share those files with the branding-credit work.
Creator: 'when I attempt to refresh my YPP page, I get an error about not being
able to reach YouTube. Seriously?' Real log: channels.list failed (403) x3.
Root cause #1 (the 403): channels.list?mine=true&part=statistics,auditDetails,
contentDetails returns 403 insufficientPermissions when the token lacks the
youtubepartner-channel-audit scope — which the auditDetails part ALONE requires,
per the docs ('A request that retrieves the auditDetails part ... must provide an
authorization token that contains the youtubepartner-channel-audit scope'). That
scope is MCN partner tooling with a 2-week token-revocation rule; the app must not
hold it. TASK-39's 'current scopes suffice, no re-consent' slice-1 claim was wrong
for this part; mock-fake tests never touched the real API, so it shipped green and
403'd every refresh since 2026-09-22.
https://developers.google.com/youtube/v3/docs/channels/list
Fix: part=statistics,contentDetails only; standing flags removed from
ChannelStatsService -> YppStatSnapshot surface -> YppTrackerViewModel -> drawer,
replaced by an honest deep-link row ('Channel standing isn't exposed to YouTube
apps — check the Earn page'). YppSnapshot standing columns stay (schema-stable,
always false). channels.list failures now log the response BODY — the bare code
could not name insufficientPermissions, which is what made this undiagnosable.
Root cause #2 (found by the new Good Dog, masked by the 403): statistics come back
as JSON STRINGS ('350'); raw GetInt64() throws. Tolerant ReadInt64 (ValueKind-first;
JsonElement.TryGetInt64 THROWS on strings — type-in, not try-type).
Good Dog: ChannelStatsServiceTests.CaptureCurrent_RequestsNoAuditDetails_AndStillParsesTheSnapshot
(URL asserts no auditDetails + snapshot parses); YppPullOutTests fixture updated.
Recipes for both 403/scope and statistics-strings entered in MyMistakes.md.
Also shipped in the same commit (shared PreviewPane.xaml + ai.md): the audio-sync
status dot removal from the #77 feedback round (creator: 'what is the point of the
status light? Lose it') — IntToSyncBrushConverter deleted with it.
verify.sh gate: 0 warnings, 316/316 pass, scope-check clean.
1. About box bg → #0A0C1A (matches icon bg)
2. Elements panel collapses when no element selected / scene changes
3. OpacityChip moved from preview overlay into Elements panel
4. Color picker fix: cast to SceneElement not Source (WebcamSceneConfig was silently blocked)
5. Red left accent bar on selected items in Scene/Source lists
6. ✓ character replaced with Path checkmark icon in Elements panel
7. Removed ToolTip='Applied' from green check buttons
8. Created bugs.md with first entry (iTunes audio on mic meter)
9. Full logo in README + About box uses ytLlive-logo.png