feat(33): shed our own capture cost first; the task is done

Last slice. 590 green, 0 warnings.

- **Shed before you charge the viewer.** On machine evidence the policy's first act is
  `ShedCaptureCadence`: `ScreenCaptureFrameSource` widens its conversion interval 10 ms → 33 ms (≈100
  → 30 conversions/s) through a new `ScreenCaptureManager.ShedLoad`. The output tier, the declared fps
  and the viewer's picture are untouched — we stop paying for readbacks we were throwing away. It
  costs no step budget; if the starvation persists, the fps ladder runs on the next clean window.
- The shed is reset on every stop path, so a cheap session cannot leave a later one capped.
- **The session line is a Logs-tab line, not a session-report line.** The session report is the money
  summary (Super Chats, per currency); quality chatter there would bury the numbers it exists for. The
  cause-tagged line already lands in the log at the moment of the step.

TASK 33 is complete: two ladders, budgets, floors, ambiguity, one-way with recovery behind a flag, the
relaunch, the copy, and the cost shed. 18 tests pin the ladders, the copy and the restart.
This commit is contained in:
2026-11-09 13:05:44 -08:00
parent 134103c08b
commit 0fa3006da7
7 changed files with 73 additions and 10 deletions
+9 -1
View File
@@ -64,10 +64,14 @@ public sealed class ScreenCaptureFrameSource : IScreenCaptureSource
// accommodate in-flight frames) and publishes only monotonically newer frames
// (MonotonicGate — a slow older completion must never overwrite a newer LatestFrame).
private static readonly TimeSpan MinConvertInterval = TimeSpan.FromMilliseconds(10);
/// <summary>TASK 33: the "shed our own load first" cadence — 30 conversions/s instead of ~100.
/// The viewer's tier does not change; only how often we pay for a readback.</summary>
private static readonly TimeSpan ShedConvertInterval = TimeSpan.FromMilliseconds(33);
private static readonly TimeSpan TelemetryInterval = TimeSpan.FromSeconds(2);
private const int MaxConcurrentConversions = 3;
private const int PoolBufferCount = 5;
private DateTime _lastConvertAt = DateTime.MinValue;
private TimeSpan _convertInterval = MinConvertInterval;
private DateTime _telemetryFrom = DateTime.UtcNow;
private DateTime _lastTelemetry = DateTime.UtcNow;
private long _skippedCadence;
@@ -79,6 +83,10 @@ public sealed class ScreenCaptureFrameSource : IScreenCaptureSource
public string Key { get; }
public event Action<VideoFrame>? FrameAvailable;
/// <summary>TASK 33: halve our own capture cost before asking the creator to accept a lower tier.
/// Idempotent; the pump keeps its declared fps either way (it pads), so no output contract changes.</summary>
public void ShedLoad(bool shed) => _convertInterval = shed ? ShedConvertInterval : MinConvertInterval;
internal ScreenCaptureFrameSource(string key, GraphicsCaptureItem item)
{
Key = key;
@@ -171,7 +179,7 @@ public sealed class ScreenCaptureFrameSource : IScreenCaptureSource
return;
}
var now = DateTime.UtcNow;
if (now - _lastConvertAt < MinConvertInterval)
if (now - _lastConvertAt < _convertInterval)
{
_skippedCadence++;
frame.Dispose();