feat(scoreboard): it ticks — the marquee is the design, not an option

Creator, plainly: "the entire point of a ticker is to tick… so, tick." The static strip was my
reading of an earlier exchange; it is gone.

How it ticks without becoming the compositor's most expensive layer:
- `ScoreboardRenderer.RenderRun` rasterises the run ONCE per value change at its natural width plus
  a gap.
- `ScoreboardPresenter` crops a 1920-wide window at a moving offset each tick — a row-wise byte
  copy, no text layout, no re-raster.
- `TickScoreboard` runs on the existing 30 Hz meter clock and only while the strip is shown, so a
  hidden or disabled scoreboard costs nothing.

The dev simulation uses the same path, so what is previewed is what ships.

Test updated to the real contract: no tick → identical pixels; a tick → different pixels; a value
change → re-rasterised run. (The old test asserted frame-instance identity, which a cropped window
can never satisfy.)
This commit is contained in:
2026-10-30 15:17:55 -07:00
parent 882b8551d9
commit 460b36204d
5 changed files with 141 additions and 14 deletions
+1
View File
@@ -31,6 +31,7 @@ public partial class MainViewModel
RaiseMasterReadouts();
UpdateDuckState();
CheckTraxSilence();
TickScoreboard();
}
/// <summary>One honest diagnostic: "playing" with a flat player meter means the samples never
+20 -3
View File
@@ -89,8 +89,14 @@ public partial class MainViewModel
set
{
if (!SetProperty(ref _scoreboardSimulate, value)) return;
if (value) ScoreboardSample.Inject(_scoreboard);
else _scoreboard.StartSession(); // empty session: "nothing yet"
if (value)
{
ScoreboardSample.Inject(_scoreboard);
}
else
{
_scoreboard.StartSession(); // empty session: "nothing yet"
}
RefreshScoreboardPreviews();
OnPropertyChanged(nameof(ScoreboardFrame));
}
@@ -98,7 +104,8 @@ public partial class MainViewModel
/// <summary>What the cadence policy asks: the master switch AND at least one scene showing it.</summary>
internal bool ScoreboardPollsEnabled =>
_scoreboardEnabled && Scenes.Any(scene =>
!_scoreboardSimulate // dev data is made up: never poll for it
&& _scoreboardEnabled && Scenes.Any(scene =>
scene.Elements.OfType<Source>().Any(s => s.IsScoreboard && s.IsVisible && s.IsEnabled));
internal ScoreboardTiles ScoreboardTilesState => _scoreboardTiles;
@@ -215,6 +222,16 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ScoreboardStyleState));
}
/// <summary>One tick of the marquee: advance the window and republish. Runs only while the strip
/// is actually shown, so an idle/hidden scoreboard costs nothing.</summary>
private void TickScoreboard()
{
if (_scoreboardPresenter is null) return;
if (!_scoreboardEnabled && !_scoreboardSimulate) return;
_scoreboardPresenter.Advance(0.033);
RefreshScoreboardPreviews();
}
/// <summary>Publishes the strip to every scoreboard layer's preview element. The pump thread
/// must never touch a WriteableBitmap, so this runs on the UI thread only.</summary>
private void RefreshScoreboardPreviews()