feat(ux)+fix(rings): release counter #N in the wordmark; all shared-frame rings 4->8 (#11)
Roll-forward of today-slices 6fd1d9c onto the slice-8 base, two-loop hunk dropped. Release counter (per-build GUID read as noise; +1 per commit from git rev-list, baseline 241 -> #13, generated by GenerateBuildStamp; GUID demotes to startup.log). Tests pinned to Label/#N >= 13; wordmark display test asserts the Label. Flash fix (take 14 finding): consumer holds must never outlive depth x source period — 4 slots at high refresh lap ~27ms vs a <=50ms compositor read, so a recycled slot flashed its new frame over the lagged old one. All shared rings 4->8 (OBS/overlay precedent for ring discipline). Camera producer now rotates an 8-deep ring + Epoch instead of a fresh ~3.7MB array per device frame (110-220MB/s LOH churn); WebView2 capture reuses a canvas scratch + 8-deep output ring + a reused WriteableBitmap instead of two fresh arrays + a fresh bitmap per 10Hz tick. Paste cache stays identity-keyed (Epoch).
This commit is contained in:
@@ -171,7 +171,7 @@ C# / WPF (.NET 8) following MVVM:
|
||||
| `Models/` | Plain data types — Scene, Source (incl. `ClipShape`, `IsMirrored`, `VideoImageSource`), QualityOption, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage, **Socials (`SocialService` enum + `SocialEntry`/`SocialsConfig` + `SocialServiceIcons`) — the social bar** |
|
||||
| `ViewModels/` | MainViewModel — `public partial class`, one file per functional area (Scenes, Background, Webcam, Audio, Trax, Socials, Streaming, Chat, Overlays, Account, License, Recording — split complete, see `ViewModels/index.md`); **Chat.cs is a thin delegating facade over `Services/ChatOverlayLayer.cs` (Commit G, first true decomposition)**; GoLiveViewModel, ReuseImageViewModel, CameraPickerViewModel, **SocialsDialogViewModel** |
|
||||
| `Services/` | YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite), **SocialValidator (`ISocialValidator` seam + `HttpSocialValidator` default)**, **webcam: `VideoFrame` seam + `CameraDeviceInfo`/`ICameraEnumerator`/`ICameraFrameSource` interfaces + `MediaCaptureCameraEnumerator`/`MediaCaptureFrameSource` (WinRT) + `CameraManager`**, **screen capture: `IFullScreenDetector`/`Win32FullScreenDetector` + `IScreenCaptureSource`/`ScreenCaptureFrameSource` (WinRT GraphicsCapture) + `ScreenCaptureManager` + `ScreenCaptureSourceFactory` + `Direct3D11Helper`/`CaptureInterop` (COM bridges)**, **media source: `IMediaFrameSource` + `MediaVideoSource` (spawns ffmpeg `rawvideo` BGRA decode) + `MediaVideoSourceManager` (refcount-by-path session owner) + pure `RawVideoFrameReader` + `IDecodeProcess`/`FfmpegDecodeProcess` process seam (binary-stdout mirror of `IEncoderProcess`; see "Media source")**, **compositor: `SceneCompositor` + `CompositorOptions` + pure `StretchMath` + `StaticPixelCache` (see "Scene compositor")**, **audio: `IAudioSource` seam + `WasapiLoopbackAudioSource`/`WasapiMicAudioSource` (NAudio WASAPI) + `AudioMixer` + pure `AudioLevelMeter`/`WaveToFloat`/`VoiceFilterChain`/`LowShelfFilter`/`HighShelfFilter`/`NoiseGate`/`Compressor`/`AutoDucker`/`AudioRingBuffer`/`TinyResampler`/`AudioSyncDelay` + `MusicPlayer` + `IAudioPipeWriter`/`NamedPipeAudioWriter` (see "Live audio capture")**, **encoder: `IFfmpegEncoder`/`FfmpegEncoder` + `IEncoderProcess`/`FfmpegEncoderProcess` + `IFfmpegLocator`/`FfmpegLocator` + pure `FfmpegArgs`/`FfmpegProgressParser`/`FfmpegEncoderPicker` + the `FramePump` frame producer (see "Live encoder" + "Live frame pipeline")**, **notifications: `INotificationService` seam (`AppNotificationSeverity` Info/Success/Warning/Error) + `NotificationService` (Notification.Wpf toasts, see "Toast notifications")** |
|
||||
| `Helpers/` | ViewModelBase (INotifyPropertyChanged), RelayCommand, ImageCache, AppLog (file logger), FocusPreservingListBox, OAuthCredentials, **TokenStore (DPAPI session persistence)**, **BuildStamp (per-build GUID generated by `GenerateBuildStamp` in ytLive.csproj — wordmark superscript + startup.log line, 2026-09-04)**, visibility converters |
|
||||
| `Helpers/` | ViewModelBase (INotifyPropertyChanged), RelayCommand, ImageCache, AppLog (file logger), FocusPreservingListBox, OAuthCredentials, **TokenStore (DPAPI session persistence)**, **BuildStamp (wordmark release counter `#N`, +1 per commit, generated by `GenerateBuildStamp` in ytLive.csproj via git rev-list; the per-build GUID now logs to startup.log only — 2026-09-04)**, visibility converters |
|
||||
| `Themes/` | `Controls.xaml` — the single dark-theme source, merged once in `App.xaml` (see `Themes/index.md`) |
|
||||
| `MainWindow.xaml` | Dark theme; layout: top bar (controls), center (preview + live controls below), left (scenes/sources), right (chat), bottom (gear + stream stats + resolution) |
|
||||
|
||||
@@ -680,7 +680,7 @@ and paces frames into the encoder at the tier's FPS. **rawvideo is stamped by AR
|
||||
pts from frame order at the declared fps — supply rate = output speed. A starved producer (pump < fps)
|
||||
ships a time-lapse, truncated file with NO error (take-2 lesson, 2026-09-01); the pump therefore logs
|
||||
`FramePump stats: n/target frames per 5s, avg render Xms (resolve R), avg submit Yms` so a slow stage names itself — and since 2026-09-04 so does the BUILD: every build gets a GUID stamped
|
||||
into `Helpers/BuildStamp` (csproj `GenerateBuildStamp` target, fresh per compile — no lying incremental build), shown as the wordmark superscript and logged at startup
|
||||
into `Helpers/BuildStamp` (csproj `GenerateBuildStamp` target, fresh per compile — no lying incremental build), logged at startup
|
||||
("Build xxxxxxxx (compiled ...)"). **Take 6 (stamp-less, ambiguous): render 35-41ms with slice 3 built or not unknown — attribution is why the stamp exists; takes 7+ are
|
||||
readable.** If the breakdown says resolve dominates, the suspects are pre-cached chat/config thrash or the web frame; if blit dominates, the general-path element sizes. **Pattern — everything is a constructor-injected
|
||||
seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<CompositorOptions>`,
|
||||
@@ -799,13 +799,17 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
(212/300). The whole remaining gap is PERIODIC 35-65ms worst-render spikes that worsened across
|
||||
the take (189→147 frames/5s) — the signature of gen2 GC pauses, now provable via `gen2 +N` in
|
||||
every stats window. Biggest churn was structural: `ScreenCaptureFrameSource` minted a fresh
|
||||
~8.3MB `byte[]` per DWM frame (~500MB/s LOH). It now rotates a 4-deep ring (a ≤17ms consumer can
|
||||
never be lapped at 60Hz), size-matched per slot, with reused downscale row scratch. Recycled
|
||||
arrays would poison the paste cache (it keys on array IDENTITY), so `VideoFrame.Epoch` —
|
||||
monotonic per producer frame, 0 for fresh-array producers — joins the key. Regression test
|
||||
`PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits` fails on the old key by
|
||||
construction. Next suspect if gen2 stays hot: the 10Hz WebView2 capture loop (full-canvas PNG
|
||||
decode + fresh arrays on the UI thread) — recorded as follow-up, untouched this slice.
|
||||
~8.3MB `byte[]` per DWM frame (~500MB/s LOH). It now rotates a shared-frame ring (4-deep at slice 8,
|
||||
deepened to 8-deep after the "flash" finding — consumer holds must never outlive depth × source
|
||||
period), size-matched per slot, with reused downscale row scratch. Recycled
|
||||
arrays would poison the paste cache (it keys on array IDENTITY), so `VideoFrame.Epoch` —
|
||||
monotonic per producer frame, 0 for fresh-array producers — joins the key. Regression test
|
||||
`PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits` fails on the old key by
|
||||
construction. The camera producer carried the same fresh-array churn (110-220MB/s at 30-60fps):
|
||||
it now rotates its own 8-deep ring + Epoch (same identity rule), and the WebView2 capture
|
||||
reuses a canvas scratch + 8-deep output ring + a reused WriteableBitmap instead of minting two
|
||||
fresh arrays + a new bitmap per 10Hz tick. Next suspect if gen2 stays hot: the WPF preview load
|
||||
itself driving gen2 — recorded as follow-up, untouched this slice.
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder (closes stdin → EOF → ffmpeg finalizes+exits)
|
||||
**before** awaiting the loop, because closing stdin unblocks a write stuck on pipe backpressure — the
|
||||
reverse order would deadlock. `ProcessFailed` self-stops the pump. `Failed` while live flips
|
||||
|
||||
Reference in New Issue
Block a user