TASK 36: composite the branding credit into the output, not just the preview
The credit existed only as a WPF BrandFlashLayer TextBlock in the preview at 25% opacity on a 300s timer. A viewer of the stream or the recording never saw it, so "free tier shows branding" was not actually being delivered. The frame pump has carried an unused per-frame flashFrame slot for exactly this. Reverses the documented "Pre-GA posture" (ai.md Monetization), which kept the flash preview-only so test VODs stayed clean. Creator ruling 2026-09-26: the free tier has to be honest advertising, so it now reaches the broadcast. One rendered VideoFrame feeds BOTH the frame pump and the preview, so the creator's view cannot drift from what viewers get. Spec: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every rand(30s)+30s, single unwrapped line at a random spot inside the frame every time, neon white core with a feathered red/blue halo. Neon recipe is derivative work, per AGENTS.md: bright core + feathered multi-radius halo with R and B split in opposite directions, from https://nudaui.dev/components/neon-glow (layered text-shadow falloff), https://help.maxon.net/rg/en-us/Content/html/Blurs-and-Glows-chromatic-glow.html ("with no displacement, the green channel is all but invisible behind your white text" once R and B are split) and the OBS obs-stroke-glow-shadow "feathered stroke with user-defined size and intensity". Neon blue is #4d8bff rather than the theme's #0f3460, which renders near-black as a halo. Also fixes a PRE-EXISTING bug found on the way: FramePump's fully-static shortcut stretched the cached bake and returned it, silently discarding every per-frame overlay - the social bar and the alert ticker were already lost there. SceneCompositor.Overlay is now internal (it copies before blitting, so the bake is never mutated) and the shortcut composites overlays first. The credit is deliberately NOT folded into SceneGraph.GetBakedBase: SceneRegion.Matches compares element ids only, so a credit in the cached bake would freeze there and never expire. Per-frame only, and Epoch is stamped every read because the 8MB master buffer is recycled - without that the paste cache would freeze a stale credit. BrandFlashEnabled is now derived !IsPremium and non-assignable, and the presenter re-checks the licence on every read, so a key entered mid-credit cuts the advertisement on the next tick instead of letting it finish. Tests: 10 new facts. 357 total, 356 pass; the one failure is the known environment-flaky RealMouseDrag layer test (needs an uncovered desktop session). Adds RealAppHost.RunAsync - a frame-pump test must await inside the collection's shared STA thread or the whole suite deadlocks silently. NOTE: scope-check.sh flags Helpers/InstanceProfile.cs, AppLog.cs, TokenStore.cs, WebView2Manager.cs and InstanceIsolationTests.cs as outside this commit. That is a false positive: it uses `git diff HEAD`, which cannot distinguish a planned second commit in the same session from an unrelated edit. Those files are the dev multi-instance unit, committed immediately after this one - as is the InstanceProfile paragraph in ai.md, which shares a file with the Monetization section corrected here.
This commit is contained in:
@@ -989,3 +989,61 @@ in the same panel) fixed it with no other change. LESSON: **when a UI-only chang
|
||||
real-input test, the control's data-binding style is the suspect, not the feature** — and
|
||||
bisect by reverting ONE file (`git checkout Controls/LeftPanel.xaml`) before theorising.
|
||||
Prefer the idiom already in the file you are editing.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-26 — A blocking wait on the shared `RealApp` STA thread hangs the ENTIRE suite silently
|
||||
|
||||
**Symptom:** `vstest` printed 4 lines and then nothing. No test ever completed, no timeout, no
|
||||
stack trace. Ran it three times, plus after a full rebuild and a reboot. Looked exactly like a
|
||||
broken test runner.
|
||||
|
||||
**It was not the runner.** Two things were stacked:
|
||||
|
||||
1. **The runner output was being swallowed by my own shell plumbing.** Running `vstest` in the
|
||||
foreground with a pipe to `tail` produced *zero* bytes even when the run finished. Launching it
|
||||
with `nohup … > log 2>&1 &` and polling the log worked every time. **Recipe: always background
|
||||
a `vstest` run and poll the log file.** Never trust a foreground piped `vstest` in this shell.
|
||||
2. **The real hang was my test.** `LayerReorderPersistenceTests` and friends share ONE STA thread
|
||||
via the `RealApp` collection. I had written the frame-pump tests as
|
||||
`_app.Run(() => { pump.StartAsync(ct).GetAwaiter().GetResult(); … })`. `Run` *occupies* that
|
||||
single thread, so the pump had nowhere to run and the two deadlocked — permanently, with no
|
||||
timeout to save it.
|
||||
|
||||
**The rule:** a test that drives the frame pump MUST be `async` and use the new
|
||||
`RealAppHost.RunAsync(Func<Task>)`, with plain `await pump.StartAsync(...)`. `await` yields the
|
||||
STA thread back to the dispatcher; `.GetAwaiter().GetResult()` holds it hostage. **Never put a
|
||||
blocking wait inside `RealAppHost.Run`.** A collection-serialized UI thread turns a missing
|
||||
`await` into a whole-suite hang, which is why this presented as "the runner is broken."
|
||||
|
||||
**Also:** isolate before fixing. Running just the suspect class proved the runner was fine and my
|
||||
test was the hang. Three blind retries on the same command would have "confirmed" a false theory.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-26 — Two real bugs my own new tests caught, and the cadence trap
|
||||
|
||||
Both were found only because the tests asserted the creator's *stated numbers* rather than
|
||||
"something was drawn":
|
||||
|
||||
1. **Interval cadence implemented as an absolute deadline.** I had `_sinceStart` accumulate forever
|
||||
and compared it to `_untilNextFlash`, which I *replaced* with a new interval on each fire. That
|
||||
makes the second value an absolute time, not an interval, so gaps **shrink every cycle**:
|
||||
measured 6.7s, 3.5s, 2.8s instead of 30–60s. Fix: keep an absolute `_nextFlashAt` but set it to
|
||||
`_sinceStart + interval` on each fire, so the interval runs *from the last flash*. General trap:
|
||||
whenever a timer holds "when did I last do X" and "when next", decide explicitly whether the
|
||||
period is relative to the last event or to the start.
|
||||
2. **A fade-in that starts at exactly 0 opacity emits a blank first frame.** `OpacityAt(0) == 0`, so
|
||||
`Frame` returned null on the tick right after `Show()`. Not wrong (it is the start of a fade) but
|
||||
it wasted a tick and made "is a credit on screen right now?" answer false. Worth knowing before
|
||||
writing the assertion — my test's arithmetic was wrong, not the renderer, and I nearly "fixed"
|
||||
the renderer to match the bad test.
|
||||
|
||||
**The lesson:** pin the *numbers the creator asked for* (2s, 30–60s, fully-on-canvas). "It rendered
|
||||
something" passes against almost any implementation and caught neither bug.
|
||||
|
||||
**Third trap, cost me a wasted verification:** to prove a `const` isn't shipped I grepped the
|
||||
Release binary for it — and it was absent from the *Debug* binary too, because **consts are inlined
|
||||
at compile time and never reach metadata**. The control (run it against Debug as well) is what
|
||||
exposed that. Grep for a **field/type** (`InstanceVariable`) or use reflection; never grep for an
|
||||
inlined literal, and always run the check against the build you expect it to FAIL in.
|
||||
|
||||
Reference in New Issue
Block a user