diag(build): per-build GUID stamp + resolve/blit timing split (take-6 attribution failure)
Take 6 measured render 35-41ms — WORSE than take 5's 25.5 — and the run could not be attributed to a binary: exe mtime != build contents (incremental builds serve stale exes; a source edit without rebuild is a silent old binary). Three takes of a perf saga had been judged against builds nobody could prove. - ytLive.csproj GenerateBuildStamp target: fresh GUID per compile (writes obj/BuildStamp.g.cs -> Helpers/BuildStamp.Id/BuiltLocal). Deliberately defeats incremental lies: every 'dotnet build' recompiles the app project. - Wordmark shows the id as a superscript (TopBar.xaml, x:Static, 9px grey BaselineAlignment=Superscript); startup.log records 'Build <id> (compiled <time>)' so every take is cross-readable with the visible UI. - FramePump stats split the tick: 'avg render Xms (resolve Y), avg submit Z' — the resolver is timed separately (wrapper resolver on per-tick paths; bake keeps the raw one) so take 7 names the hot half of 'render' with data. - Fixed a latent transition-clock bug found on the way: lastTick now restarts every frame (the branch rework had restarted it only during transitions, letting a transition begun after idle complete instantly on its first Tick). ONE integration test family: BuildStampTests (unit: shape) + BuildStampDisplayTests (RealApp, namescoped FindName on TopBar proves the wordmark SHOWS the id). 47/47 per-class green, clean build 0 warnings. Docs same commit. User's top-bar/session spec (re-sent twice) + settled Q&A decisions folded into HANDOFF Unit B — next work unit after take 7 verdict.
This commit is contained in:
@@ -26,6 +26,9 @@ public partial class App : Application
|
||||
AppLog.Write(args.Exception, "Dispatcher unhandled exception");
|
||||
|
||||
AppLog.Write("App OnStartup begin");
|
||||
// Attribution (2026-09-04): every take must be traceable to the exact binary
|
||||
// that produced it — the wordmark superscript shows this same id.
|
||||
AppLog.Write($"Build {ytLive.Helpers.BuildStamp.Id} (compiled {ytLive.Helpers.BuildStamp.BuiltLocal})");
|
||||
|
||||
// Velopack auto-update bootstrap — checks for updates on startup and
|
||||
// applies them (the app restarts automatically if an update is found).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<UserControl x:Class="ytLive.Controls.TopBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:helpers="clr-namespace:ytLive.Helpers"
|
||||
x:Name="TopBarRoot">
|
||||
<!-- LOGO / BRAND — clicking opens the in-app About overlay -->
|
||||
<Grid>
|
||||
@@ -20,7 +21,10 @@
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<TextBlock FontSize="22" FontWeight="Bold">
|
||||
<Run Text="LlamaCas" Foreground="#e0e0e0"/><Run Text="ty" Foreground="#e94560"/>
|
||||
<Run Text="LlamaCas" Foreground="#e0e0e0"/><Run Text="ty" Foreground="#e94560"/><Run
|
||||
x:Name="BuildIdStamp" Text="{x:Static helpers:BuildStamp.Id}" FontSize="9"
|
||||
BaselineAlignment="Superscript" Foreground="#8888aa"
|
||||
ToolTipService.ToolTip="{x:Static helpers:BuildStamp.Id}"/><Run Text=" " FontSize="4"/>
|
||||
</TextBlock>
|
||||
</Button>
|
||||
|
||||
|
||||
+18
-5
@@ -44,11 +44,18 @@ RealApp boot-smoke. Scope-check passed.
|
||||
raster on message/config change, blit the cached frame every tick (OBS text-source pattern).
|
||||
Tests: `ChatOverlayLayerCacheTests` (the ONE, RealApp) + full regression green (62 across
|
||||
touched classes), clean build 0 warnings.
|
||||
2. **Take 6 (user, ~30s record-only):** expect `≈300/300 frames per 5s, avg render ≤ ~10ms`. If
|
||||
chat-burst drops one frame per message (avg fine but `n/300` sags during live chat): debounced
|
||||
off-tick re-render is the next slice. If steady at ~300 with render <10 → recording saga
|
||||
CLOSED, go Unit B (below).
|
||||
3. **Unit B — the top bar + session logic (user spec 2026-09-04, decisions settled):**
|
||||
2. **BUILD STAMP shipped (slice 4, same day):** take 6 came back 35-41ms — WORSE than take 5 —
|
||||
and attribution was impossible (exe timestamp ≠ binary contents; incremental builds served
|
||||
unverified exes). Every build now stamps a fresh GUID (`ytLive.csproj GenerateBuildStamp` →
|
||||
`Helpers/BuildStamp`), shown as the wordmark superscript (build id) + `Build xxxxxxxx (compiled
|
||||
...)` in startup.log; stats report `avg render Xms (resolve Y)` so the hot half of the tick is
|
||||
named. Tests `BuildStampTests` + `BuildStampDisplayTests` (real window, namescoped FindName).
|
||||
3. **Take 7 (user, ~30s record-only):** FIRST read the superscript + startup.log Build line — the
|
||||
take is meaningless without it. Expect slice 3 in any post-stamp build: `avg render ≤ ~10ms
|
||||
(resolve ≪ render), ≈300/300`. If resolve dominates → resolver-side surprise (chat config thrash
|
||||
/ web frame); if blit dominates → measure the general-path element sizes. If chat-burst sags
|
||||
`n/300`: debounced off-tick re-render slice. If clean → recording saga CLOSED, Unit B.
|
||||
4. **Unit B — the top bar + session logic (user spec 2026-09-04 re-sent twice + decisions settled in Q&A):**
|
||||
- Two-line top bar. Line 1: center = REC + **LIVE** pills (text renamed from ON-AIR; pills become
|
||||
mutually-exclusive RADIOS — record-OR-stream ruling), right = avatar + **Login/Logout** button
|
||||
(no account status light). Line 2: centered primary **Start** (grayed while NO pill armed —
|
||||
@@ -67,6 +74,12 @@ RealApp boot-smoke. Scope-check passed.
|
||||
live is scary — confirmation allows back-out + testing up to go-live").
|
||||
- Bottom-bar metrics init/maintain: `ResetHealth` + `HealthUpdated` exist — verify on take 6.
|
||||
- F6 "start/end" hotkey routes through `HandleHotkey` — check it honors the new grayed-Start gate.
|
||||
- Login button text: "Login" (disconnected, LIVE pill greyed) → "Logout" (connected, avatar
|
||||
appears left of it). The account status LIGHT is deleted per spec 2a.
|
||||
- Primary button: grayed "Start" when NO pill armed (inverts 2026-09-01 rule — fix the map in
|
||||
the landing commit); enabled when either armed; REC path = file dialog flow; LIVE path =
|
||||
go-live after dialog confirm; becomes the Stop/End face while active (user's "Stop button
|
||||
never active" complaint gets a hermetic test pinning visibility+CanExecute).
|
||||
- ONE integration test (hermetic): pills↔button state machine + record-path seam (an
|
||||
`internal static Func<SaveFileDialog-ish prompt>` override seam mirroring `RegistrarOverride`
|
||||
— never pop real dialogs in tests).
|
||||
|
||||
@@ -213,3 +213,18 @@ one-line provenance — *who asked, what triggered it* ("creator: OBS delay-filt
|
||||
Features without attribution become roadmap orphans that get punted, removed, or re-litigated. Same
|
||||
disease as an unexplained "known failure" label — a fact recorded without its reason is a future
|
||||
argument.
|
||||
|
||||
## An un-attributed build invalidated three takes of a perf saga — stamp the binary
|
||||
|
||||
(2026-09-04, takes 4–6) After each render-perf fix the creator "exed the code" and re-recorded, but
|
||||
the exe timestamp ≠ binary contents (incremental builds reuse whatever compiles clean; a source edit
|
||||
with no rebuild serves the OLD exe). Take 6 measured render WORSE than take 5 (35-41ms) and there was
|
||||
no honest way to tell "the chat cache fix doesn't work" from "the fix was never running" — three
|
||||
hours of diagnosis on an unattributable sample. Rule: if takes measure the app, EVERY build carries
|
||||
an id and EVERY log line traces to it — `GenerateBuildStamp` (csproj) writes a fresh GUID per
|
||||
compile (deliberately defeating incremental lies), the wordmark shows it as a superscript, startup.log
|
||||
records `Build <id> (compiled <time>)`. A perf claim without build attribution is a guess; ask for the
|
||||
stamp BEFORE theorizing. (Also this session: a sentinel-byte test where the source pattern could
|
||||
GENERATE the sentinel, and a fixed-point bilinear that shifted BOTH stages and silently drew nothing —
|
||||
see the rawvideo recipe.)
|
||||
|
||||
|
||||
@@ -265,7 +265,11 @@ public sealed class FramePump : IDisposable
|
||||
// Log the render/submit split every 5s so the next take names the stage.
|
||||
var renderSw = new System.Diagnostics.Stopwatch();
|
||||
var submitSw = new System.Diagnostics.Stopwatch();
|
||||
long renderTicks = 0, submitTicks = 0;
|
||||
// Resolve-vs-composite split (2026-09-04, take-6 ambiguity): "render" was a
|
||||
// black box — the stats line now reports resolver time separately so a take
|
||||
// names the stage (get-frame vs blit) instead of feeding another guess.
|
||||
var resolveSw = new System.Diagnostics.Stopwatch();
|
||||
long renderTicks = 0, submitTicks = 0, resolveTicks = 0;
|
||||
int statFrames = 0;
|
||||
var statsNext = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
||||
void ReportStats()
|
||||
@@ -275,13 +279,25 @@ public sealed class FramePump : IDisposable
|
||||
_log?.Invoke(statFrames == 0
|
||||
? "FramePump stats: NO frames produced in 5s (loop stalled?)"
|
||||
: $"FramePump stats: {statFrames}/{target:F0} frames per 5s, " +
|
||||
$"avg render {renderTicks / (double)System.Diagnostics.Stopwatch.Frequency * 1000 / statFrames:F1}ms, " +
|
||||
$"avg render {renderTicks / (double)System.Diagnostics.Stopwatch.Frequency * 1000 / statFrames:F1}ms " +
|
||||
$"(resolve {resolveTicks / (double)System.Diagnostics.Stopwatch.Frequency * 1000 / statFrames:F1}), " +
|
||||
$"avg submit {submitTicks / (double)System.Diagnostics.Stopwatch.Frequency * 1000 / statFrames:F1}ms");
|
||||
renderTicks = submitTicks = 0;
|
||||
renderTicks = submitTicks = resolveTicks = 0;
|
||||
statFrames = 0;
|
||||
statsNext = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
||||
}
|
||||
|
||||
// One wrapper shared by every render of the run — resolve time accumulates
|
||||
// inside the render measurement, and the stats line reports the split.
|
||||
VideoFrame? TimedResolver(SceneElement element)
|
||||
{
|
||||
resolveSw.Restart();
|
||||
var frame = _frameResolver(element);
|
||||
resolveSw.Stop();
|
||||
resolveTicks += resolveSw.ElapsedTicks;
|
||||
return frame;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
@@ -312,12 +328,15 @@ public sealed class FramePump : IDisposable
|
||||
// pooling change because its buffer's only consumer was its own release.
|
||||
renderSw.Restart();
|
||||
var scratch = AcquireScratch(scratchSize);
|
||||
frame = RenderScene(scene, compositorOptions, socialBarFrame, socialBarTop, scratch);
|
||||
frame = RenderScene(scene, compositorOptions, socialBarFrame, socialBarTop, scratch, TimedResolver);
|
||||
if (_transition is { Active: true } transition)
|
||||
{
|
||||
frame = transition.BlendFrame(frame);
|
||||
transition.Tick(lastTick.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
// Restarted EVERY frame (transition or not) so a transition's first
|
||||
// Tick sees per-frame time, not the pump's whole uptime.
|
||||
lastTick.Restart();
|
||||
// Restarted EVERY frame (transition or not) — the old per-frame reset
|
||||
// is what stops a transition that begins after idle from inheriting
|
||||
// a giant ElapsedMs and completing instantly on its first tick.
|
||||
@@ -394,10 +413,14 @@ public sealed class FramePump : IDisposable
|
||||
CompositorOptions options,
|
||||
VideoFrame? socialBarFrame,
|
||||
int socialBarTop,
|
||||
byte[]? scratch = null)
|
||||
byte[]? scratch = null,
|
||||
Func<SceneElement, VideoFrame?>? resolver = null)
|
||||
{
|
||||
// per-tick composites use the (timed) resolver; the rare bake uses the raw one
|
||||
// so bake cost lands in "render" but not "resolve".
|
||||
resolver ??= _frameResolver;
|
||||
if (_sceneGraph == null)
|
||||
return _compositor.Render(scene, _frameResolver, null, options, socialBarFrame, socialBarTop, scratch: scratch);
|
||||
return _compositor.Render(scene, resolver, null, options, socialBarFrame, socialBarTop, scratch: scratch);
|
||||
|
||||
var split = _sceneGraph.GetSplitPoint(scene);
|
||||
if (split == scene.Elements.Count)
|
||||
@@ -412,11 +435,11 @@ public sealed class FramePump : IDisposable
|
||||
if (baseFrame != null)
|
||||
{
|
||||
return SceneCompositor.CompositeLayers(
|
||||
baseFrame, scene, split, _frameResolver, options, socialBarFrame, socialBarTop, scratch: scratch);
|
||||
baseFrame, scene, split, resolver, options, socialBarFrame, socialBarTop, scratch: scratch);
|
||||
}
|
||||
|
||||
// No static base (first layer is dynamic or empty scene) — full render.
|
||||
return _compositor.Render(scene, _frameResolver, null, options, socialBarFrame, socialBarTop, scratch: scratch);
|
||||
return _compositor.Render(scene, resolver, null, options, socialBarFrame, socialBarTop, scratch: scratch);
|
||||
}
|
||||
|
||||
private void OnProcessFailed(object? sender, string message)
|
||||
|
||||
@@ -973,7 +973,7 @@ click (volume sliders keep their manual `SetSliderValueFromClick`, harmless dupl
|
||||
|
||||
**Goal:** record the stream output to a local file, with or without simultaneously streaming.
|
||||
|
||||
### Status: ✅ Shipped `a9eb360` (2026-08-29) — code done (incl. manual-rename modal), build 0 warnings, 244/246 tests; **running-app verification (takes 1–5 done)**: files land (ffmpeg re-pinned month-end), **webcam-in-output + social bar visually CONFIRMED from take 3's extracted frame**, rename modal used for real (take 3 was named via it); take 3 exposed the ~2fps producer starvation and the pump stage-timing (`97ffc42`) named it in one line — `avg render 258.1ms` — **slice 1 fixed 2026-09-03** (deadline pacing per OBS video-io.c + libyuv-style row blits in `SceneCompositor`, test `Pump_Paces_To_The_Deadline_Compensating_Render_Cost`); **take 4: pacing held but render stayed 58.9ms** (2M-iteration row walk + 8.3MB/tick LOH) — **slice 2 shipped 2026-09-04**: `VideoFrame.IsOpaque` producer-contract flag → full-cover backdrop is one `Buffer.BlockCopy`; integer fixed-point bilinear general path; pump scratch pool (release strictly post-submit, owned-by-reference so cache frames are untouchable); dead per-tick `fromScene` render + the `fromSceneProvider` seam removed (BlendFrame uses `TransitionService.FromFrame` — the old render fed nothing). Tests `Pump_Pools_ScratchBuffers_Across_Frames_Without_Stale_Pixels` (the ONE) + `Composite_OpaqueFullCover_Backdrop_CopiesEveryPixel_Into_Scratch`; 59/59 per-class green, clean build 0 warnings. **take 5 ran: render 58.9→25.5ms (`138/300` ≈ 2.2x still) — cause: the per-tick chat raster (`RenderFrame` hit `RenderTargetBitmap` every tick whenever the message buffer was non-empty — the buffer survives sessions); slice 3 shipped 2026-09-04: `ChatOverlayLayer` rasters on message/config change and blits a cached frame every tick (OBS text-source pattern; test `ChatOverlayLayerCacheTests`).** **take 6 pending**: stats must show `avg render ≤ ~10ms, ≈300/300`. Known remaining churn (follow-ups, not silently done): vertical tier's final `BilinearScale` still allocates per frame; one slow tick (~15-25ms) per arriving chat message — debounced off-tick re-render if take 6 shows burst loss. **User UX spec (2026-09-04) queued behind this**: two-line top bar (LIVE rename, radio pills, Login/Logout button + avatar right-click Change Account, no account light; line 2 centered Start↔Stop grayed-until-armed) + up-front SaveFileDialog for REC (native overwrite prompt; retires the stop-time rename modal) + Go-Live dialog KEPT as preflight confirmation prefilled from the Text drawer — decisions captured in HANDOFF
|
||||
### Status: ✅ Shipped `a9eb360` (2026-08-29) — code done (incl. manual-rename modal), build 0 warnings, 244/246 tests; **running-app verification (takes 1–5 done)**: files land (ffmpeg re-pinned month-end), **webcam-in-output + social bar visually CONFIRMED from take 3's extracted frame**, rename modal used for real (take 3 was named via it); take 3 exposed the ~2fps producer starvation and the pump stage-timing (`97ffc42`) named it in one line — `avg render 258.1ms` — **slice 1 fixed 2026-09-03** (deadline pacing per OBS video-io.c + libyuv-style row blits in `SceneCompositor`, test `Pump_Paces_To_The_Deadline_Compensating_Render_Cost`); **take 4: pacing held but render stayed 58.9ms** (2M-iteration row walk + 8.3MB/tick LOH) — **slice 2 shipped 2026-09-04**: `VideoFrame.IsOpaque` producer-contract flag → full-cover backdrop is one `Buffer.BlockCopy`; integer fixed-point bilinear general path; pump scratch pool (release strictly post-submit, owned-by-reference so cache frames are untouchable); dead per-tick `fromScene` render + the `fromSceneProvider` seam removed (BlendFrame uses `TransitionService.FromFrame` — the old render fed nothing). Tests `Pump_Pools_ScratchBuffers_Across_Frames_Without_Stale_Pixels` (the ONE) + `Composite_OpaqueFullCover_Backdrop_CopiesEveryPixel_Into_Scratch`; 59/59 per-class green, clean build 0 warnings. **take 5 ran: render 58.9→25.5ms (`138/300` ≈ 2.2x still) — cause: the per-tick chat raster (`RenderFrame` hit `RenderTargetBitmap` every tick whenever the message buffer was non-empty — the buffer survives sessions); slice 3 shipped 2026-09-04: `ChatOverlayLayer` rasters on message/config change and blits a cached frame every tick (OBS text-source pattern; test `ChatOverlayLayerCacheTests`).** **take 6 ran WITHOUT attribution (35-41ms — build provenance unknown; slice 3 effectiveness UNCONFIRMED) → build-stamp shipped instead of guessing again: `Helpers/BuildStamp` GUID per build (csproj GenerateBuildStamp; incremental builds can no longer lie), wordmark superscript + startup.log line; stats split `render (resolve)` so the next take names the stage. **take 7 pending**: read stamp → confirm slice 3 build → stats must show `avg render ≤ ~10ms (resolve ≪), ≈300/300`. Known remaining churn (follow-ups, not silently done): vertical tier's final `BilinearScale` still allocates per frame; one slow tick (~15-25ms) per arriving chat message — debounced off-tick re-render if take 6 shows burst loss. **User UX spec (2026-09-04) queued behind this**: two-line top bar (LIVE rename, radio pills, Login/Logout button + avatar right-click Change Account, no account light; line 2 centered Start↔Stop grayed-until-armed) + up-front SaveFileDialog for REC (native overwrite prompt; retires the stop-time rename modal) + Go-Live dialog KEPT as preflight confirmation prefilled from the Text drawer — decisions captured in HANDOFF
|
||||
|
||||
1. ✅ `EncoderOptions` extended with `StreamEnabled` / `RecordEnabled` / `RecordPath` (independent intent flags)
|
||||
2. ✅ `FfmpegArgs.Build` reworked into per-output blocks (stream `-f flv`, record `-f mp4`) via `AddVideoTags`
|
||||
|
||||
@@ -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)**, visibility converters |
|
||||
| `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 |
|
||||
| `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) |
|
||||
|
||||
@@ -679,7 +679,10 @@ scene each tick, resolves every element to its latest frame, composites it into
|
||||
and paces frames into the encoder at the tier's FPS. **rawvideo is stamped by ARRIVAL:** ffmpeg assigns
|
||||
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 submit Yms` so a slow stage names itself. **Pattern — everything is a constructor-injected
|
||||
`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
|
||||
("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>`,
|
||||
`Func<EncoderOptions?>`, `Func<IFfmpegEncoder>`, `Action<string>` log, and an injectable pacing delay
|
||||
(default `Task.Delay`; tests inject `Task.Yield`). The pump is free of WPF and of the capture managers.
|
||||
|
||||
@@ -19,6 +19,30 @@
|
||||
<Page Remove="ytLive.Tests\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Per-build identity stamp (2026-09-04): takes 1–6 could not be attributed to an
|
||||
exact binary — render perf "fixes" were being judged against builds we could not
|
||||
prove were running. A fresh GUID per build makes an incremental compile impossible
|
||||
(that is the point: every exe you launch was compiled NOW), the wordmark shows the
|
||||
id, and startup.log records id + build time. -->
|
||||
<Target Name="GenerateBuildStamp" BeforeTargets="CoreCompile">
|
||||
<PropertyGroup>
|
||||
<BuildStampFile>$(IntermediateOutputPath)BuildStamp.g.cs</BuildStampFile>
|
||||
<BuildStampId>$([System.Guid]::NewGuid().ToString('N').Substring(0,8))</BuildStampId>
|
||||
<BuildStampTime>$([System.DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))</BuildStampTime>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<BuildStampLines Include="// <auto-generated> Per-build identity (ytLive.csproj GenerateBuildStamp)."/>
|
||||
<BuildStampLines Include="// Shown as the wordmark superscript and logged at startup: proof of which"/>
|
||||
<BuildStampLines Include="// binary produced a test recording (2026-09-04). Never edit by hand."/>
|
||||
<BuildStampLines Include="namespace ytLive.Helpers%3B"/>
|
||||
<BuildStampLines Include="public static class BuildStamp { public const string Id = "$(BuildStampId)"%3B public const string BuiltLocal = "$(BuildStampTime)"%3B }"/>
|
||||
</ItemGroup>
|
||||
<WriteLinesToFile File="$(BuildStampFile)" Lines="@(BuildStampLines)" Overwrite="true" WriteOnlyWhenDifferent="false" />
|
||||
<ItemGroup>
|
||||
<Compile Include="$(BuildStampFile)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\llamacasty-icon.png" />
|
||||
<Resource Include="Assets\llamacasty-logo.jpg" />
|
||||
|
||||
Reference in New Issue
Block a user