Rubylight Download 2.2.10
On this page
  1. October 12 latency follow-up (issue 24)
  2. October 12 4:2:0 and pacing follow-up
  3. October 12 resource use (issue 23)
  4. October 10 PyroWave SDK 1.0 and a high-priority encode queue: shipped
  5. October 10 WGC helper textures read in place: on by default
  6. October 10 AV1 entropy-context (CDF) update: no default change
  7. October 10 per-frame send-wait budget: not built
  8. October 10 MMCSS boost on the media threads: not shipped
  9. October 10 HEVC reference invalidation after short losses: not shipped
  10. October 10 AMD reference invalidation, AV1 only
  11. October 10 item 5: the video sender thread, on paced links only
  12. October 10 Wi-Fi wave 2: a keyframe right after a frame the host could not send
  13. October 9 research triage: host verification of the five BUILD NOW items
  14. October 9 user report: constant jitter, and recovery requests
  15. October 9 held fixes: host A/B after the reboot
  16. October 9 AV1 padding at 1968x2184
  17. October 9 Vibepollo fixture failure
  18. October 9 AMF settings review
  19. October 9 job 7 split and stall diagnostics: host A/B, then a GPU reset
  20. October 9 a new virtual display hitches other streams
  21. October 9 job 4 split and two latency wakes: host A/B
  22. October 9 pacer claims at the stream rate (bug hunt item 1): host A/B
  23. October 8 repeats past the frame cap (PyroWave, VRR): host A/B
  24. October 8 user report: exclusive layout ignored, picture freezes
  25. October 8 real-client picture age over Wi-Fi (rc.24, laptop)
  26. October 8 encoder stall recovery backs off (RX 9070 XT report)
  27. October 8 rc.22 baseline at the owner's settings
  28. October 5 rc.3 release continuation
  29. October 5 continuation from OpenCode
  30. October 3 installed state (historical)
  31. October 3: multiple streams and display restoration
  32. October 3 evening: Vibepollo 2.0 parity
  33. October 4: latency beside a game
  34. Next work
  35. Operating rules
  36. Work log
  37. Handoff priorities and reproduction
  38. GPU reset recovery audit, 2026-10-09
  39. October 9 user report: physical monitor comes back on when a game loses focus
  40. October 9 user report: audio_loss warnings on an idle host
  41. Rename to Rubylight (2026-10-09)
  42. Rubylight 2.2.9 short release streams (2026-10-12)

Windows performance work — October 2, 2026 #

User objective: make the Rust host smoother and lower latency than Vibepollo, without reducing features or picture quality. Opus took over from Codex in the evening of October 2.

This is a dated engineering log, newest entry first. Each entry records what was and was not measured on its own date, so a "not measured" note in an older entry may have been measured in a later one. Results to a real laptop over Wi-Fi are in "October 8 real-client picture age over Wi-Fi"; the performance overview carries the current numbers.

October 12 latency follow-up (issue 24) #

Issue #24 reviews 2.2.7 (7ad9596). Its timing estimates and Linux models identify candidates; they are not Windows measurements. This pass implements the correctness and bounded latency follow-ups and records the experiments still requiring evidence.

Item Disposition
R1: reconnect GPU scheduling Implemented. The first streaming scope reapplies the last successfully configured GPU priority, including when capture is reused. The final scope still restores the prior class.
R2: host feedback Implemented. Gamepad motion/rumble/light/trigger reports and changed HDR metadata signal a connected loopback socket in the control wait. Reports are queued before the wake; no periodic wake was added. The driver's existing 8 ms feedback poll remains.
R3: ENet backlog Implemented. A pass that consumes its 512-event allowance uses a zero wait, so ENet's internal queue drains without an extra feedback interval between passes.
Active display leases Implemented. Game and retained remote-monitor leases check every 100 ms while a stream owns them, retaining 250 ms idle checks and existing faster recovery retries. Resume unparks the workers.
Display watcher completion Implemented. Journal ownership is released before waiting up to 100 ms for exit. A stalled owned watcher is terminated with a further bounded 100 ms wait; its completion event cannot be reused until exit is confirmed.
B1: WGC helper HIGH class Implemented after controlled idle/loaded comparisons. Only the helper process moves to HIGH; its capture thread runs at priority 14 instead of 9, distinct from MMCSS 18. The results and the initial idle outlier are recorded below.
B2: Wi-Fi pacing at 4× Deferred. The model predicts earlier packet delivery, but larger bursts can increase access-point/client loss. Keep 2× until real Wi-Fi ABBA runs, outage recovery and a ten-minute loss comparison establish the trade-off. No adaptive fallback was added without those measurements.
B3: newest image after encoder rebuild Implemented. A successful rebuild rereads the latest image, verifies device compatibility and refreshes source timing before encoding it. A picture published during initialization can replace the pre-rebuild picture.
B4: static-desktop helper repaint Implemented. The initial repaint deadline is 33 ms rather than 250 ms; retries stay 500 ms apart. This changes a deadline, not a measured launch-time result.
B5: interrupt a sending delta for an IDR Deferred after prototyping and testing. Cancellation/nonce model tests passed, but the live recovery check did not trigger in-flight cancellation and did not consistently improve IDR p95. Existing pending-delta dropping remains.
B6: standing microphone backlog Deferred after prototyping and testing. The queue/device model recovered within the proposed deadline, but baseline and candidate both failed the live tone probe, which never exercised trimming. The shorter device-buffer margin and speech-splice quality remain unvalidated. Microphone playout retains its existing behavior.
B7: final claim millisecond Implemented. When collection is no longer attempted inside the final millisecond, the loop precisely waits on the claim deadline instead of repeating short non-precise output polls.
B8: hybrid WGC arrival/poll Deferred. Explain the earlier excluded zero-frame run and obtain valid idle/loaded comparisons before changing capture wake policy. A theoretical earlier wake does not establish loaded scheduling behavior.
B9: paired-state save at startup Implemented. Client last-seen persistence runs on a background worker with the existing paired-state lock; timestamps remain monotonic across overlapping starts. A failed save or worker start is logged.
B10: capture during /launch Deferred. No first-frame measurement yet justifies the additional ownership path for abandoned launches, reconnects and differing negotiated modes. It also requires proving unused helpers are reclaimed within the proposed deadline.
B11: encoder backlog of one Deferred. The issue predicts a throughput reduction. Keep the existing bound until the saturated fixture establishes the quality/throughput trade-off; no new configuration option was added.
B12a: release replaced capture image Implemented. The old image is dropped after the publication lock is released and before notifying consumers.
B12b: grid pacing Deferred. Guarding collection near the existing opt-in grid deadline and switching its waits to precise waits still need frame-age, jitter and CPU comparisons on that path.
B12c: parallel audio/display preparation Deferred. No launch-stage measurement isolates its benefit, and concurrent preparation needs explicit failure, rollback and session-ownership validation.
B12d: skip pacing for short tails Deferred. Changing the last batch's pacing is a burst-size trade-off; the arithmetic estimate is not evidence of a real-link gain without increased drops.
B12e: audio idle waits Implemented. Event-driven capture waits account for the 50 ms silence threshold as well as the next packet deadline, avoiding an expired packet deadline forcing repeated 1 ms waits before silence is due. Capture events still wake immediately and the existing 5 ms wait cap remains.

The issue's measure-before-building items remain experiments:

Candidate Evidence still needed
GPU handoff fences/keyed mutex and helper conversion Per-hop GPU timestamps and an equivalent end-to-end comparison; the proposed sub-millisecond gain is unmeasured.
Separate encoder-output thread Show that encode time exceeds the useful claim window and that another thread reduces picture age without changing throughput or ordering.
Drop the AV1 RFI confirmation keyframe Establish decoder recovery correctness on independent clients before removing it.
Shorter hotplug quiet window Measure reconnect/reconfigure tails and prove competing topology changes settle safely before shortening the current 500 ms window.
Cached WGC HDR output detection Verify real HDR toggles through the cached output before changing cache or capture behavior.
Drain VHF feedback FIFO Establish whether the driver actually queues reports and how reads complete. The new control wake does not change driver polling semantics.

For C, measurement gaps, debug logs now record AMF encoder Init, encoder/context Terminate and helper connection/Ready elapsed times. A launch mode that differs from the negotiated mode emits an explicit display-preparation log. There is no new preparation counter, and interop.py/recovery_gaps.py have not been extended to aggregate all these stages. Launch-to-first-frame, reconnect, reconfigure switch time, HDR-toggle freeze and microphone end-to-end delay still need the issue's dedicated fixtures; log instrumentation alone does not close those measurement gaps.

The paths checked as sound in section D are preserved. Section E's excluded directions are not reopened: no slice/tile protocol, FEC-layout, wired-ceiling, Opus-complexity, microphone reorder-buffer or cross-reopen D3D-device change.

Validation #

Locked Windows workspace tests passed: 815 tests, zero failures and 51 explicitly ignored native/environment fixtures. Workspace/all-target Clippy with warnings denied, formatting, four release-tool tests and the 18-page documentation-site build passed. The native RX 7900 XT streaming-scope test passed, including a reconnect that reuses the still-live capture and restores the original GPU class afterwards.

An initial HEVC 1920x1080x60 helper-forced ABBA compared released 2.2.7 against the candidate that included HIGH helper priority, using a 5120x1440@240 SDR source and twelve-second streams. All four runs decoded every received frame, retained continuous audio and passed the existing stream checks. Baseline picture-age means were 13.420/13.154 ms; candidate means were 21.683/17.932 ms. Those values are outside the proposed idle tolerance, but a concurrent release test build in another workspace was subsequently observed. These runs are therefore unsuitable for latency acceptance or causal attribution. The priority change was removed for the subsequent controlled comparisons. An earlier fixture attempt used relative paths and exited before connecting; the recorded comparisons use absolute paths.

After the other release task finished, B1 was isolated on one candidate binary: only the initialized helper's process class changed, with wgc_helper_streaming_scope=false. HEVC 1080p60 at 20 Mbps used a 120 fps motion source on the same 5120x1440@240 SDR display and a loopback D3D11VA receiver. Source, receiver, tone and orchestration stayed HIGH in both arms to protect the fixture from the CPU load. Independent 30-second launches ran in ABBA order, discarding five seconds after priorities/load were established. input_path_probe --spin=highest supplied one busy HIGHEST thread per logical CPU in a NORMAL-class process. No other test or build ran concurrently. Picture age means source barcode timestamp to barcode extraction after decode, not presentation or input-to-photon delay.

Condition / run Helper Fresh FPS Mean age, ms p95, ms p99, ms
Idle A1 NORMAL 60.020 10.050 10.601 10.760
Idle B1 HIGH 60.000 11.769 19.063 19.240
Idle B2 HIGH 60.001 10.311 10.845 11.097
Idle A2 NORMAL 59.999 10.408 10.893 11.168
Loaded A1 NORMAL 40.449 44.373 63.597 81.423
Loaded B1 HIGH 59.919 9.425 10.091 10.426
Loaded B2 HIGH 59.997 10.785 11.278 12.387
Loaded A2 NORMAL 41.365 48.336 70.942 96.332

Both loaded HIGH runs passed the stream checks and retained over 99% of idle fresh FPS; both loaded NORMAL runs failed the moving-picture continuity check. All runs had zero decode errors. These are trimmed, post-control windows: loaded HIGH B2's broader E2E window still recorded a 552.240 ms picture-age p99, so the comparison does not establish elimination of startup/load transients. Independent idle launches averaged 0.810 ms higher at HIGH, outside the proposed 0.3 ms tolerance. That outlier remains unexplained, so a follow-up held the source, helper and receiver connected in one 140-second stream and changed only helper priority in four 30-second ABBA windows, again discarding five seconds after each assignment:

Helper Mean age, ms p99, ms Fresh FPS
NORMAL A1 10.039885 10.813 60.001
HIGH B1 10.053541 10.962 59.999
HIGH B2 10.076316 10.917 60.001
NORMAL A2 10.096460 10.839 59.999

The HIGH-minus-NORMAL mean was -0.003244 ms; adjacent comparisons were +0.013656 and -0.020144 ms. All 8,378 received frames decoded with continuous audio. This controls reconnect state and passes the idle criterion within this stream. It does not explain the independent-launch outlier or establish equivalence across sessions. B1 is accepted for the observed CPU-contention benefit with that uncertainty recorded; no real-game, Wi-Fi or other-hardware latency claim follows from these tests.

The combined candidate then passed four quiet 30-second HEVC 1080p60 streams against released 2.2.8 in ABBA order, with continuous audio, no decode errors and 59.999–60.000 fresh FPS. Mean picture age was 10.329/9.859/10.166/7.499 ms; p99 was 11.192/10.650/11.126/11.732 ms. These independently launched sessions do not establish an overall latency improvement.

H.264 1080p60 recovery at a 40,000 kbps pacing cap delivered and decoded all 20 requested IDRs in both builds, with no decode errors or unchanged pictures re-encoded. An earlier quiet pair had baseline/candidate IDR-arrival p95 of 39.236/36.427 ms; the final combined pair measured 38.303/40.993 ms. Per-frame and request CSVs found no missing wire IDs in any request-to-IDR bracket. Candidate brackets also had no missing source barcodes, but sender debug logs recorded no in-flight cancellations. Thus neither cancellation behavior on a real paced stream nor a consistent recovery gain was established. B5 and its prototype test were removed before release; no sender behavior changes ship.

The unshipped microphone prototype passed a deterministic queue/device model with 20/40 ms network holds at 5/10/20 ms packet cadence: mean delay returned within 5 ms of baseline by 900 ms without the hard queue limit or added silence. Quiet native ABBA runs nevertheless detected only 36/59, 36/59, 37/59 and 36/59 tones, with zero trimming in every run. The existing mic_probe generates tone pulses but has no --burst network option. These failures also occur in the baseline; they neither establish a regression nor validate the proposed drain. The prototype and its model tests were removed from the release.

After removing both prototypes, the rebuilt release passed another 30-second HEVC stream and H.264 stream with 20 keyframe requests at the same settings. Both decoded every received frame with zero decode errors and continuous audio; all 20 recovery IDRs decoded, with no unchanged picture re-encoded.

October 12 4:2:0 and pacing follow-up #

The accepted GPU change uses one compute dispatch for D3D11 NV12/P010 and PyroWave 4:2:0 fallback conversion, retaining pixel passes when resources are unavailable. Default AMD D3D12 conversion is unchanged. The conversion evidence records GPU timestamps, exact pixel comparisons, debug-layer validation and independent stream checks. These establish component performance and interoperability, not whole-stream latency or CPU savings.

Two pacing candidates remain unshipped. Timer-only waits for non-final batches, preserving the precise final-batch wait, consistently increased sender completion tails in the first ABBA matrix. At 150 Mbps/60 fps with an 800 Mbps wire target, final-packet p95 increased by 0.512 ms and keyframe p95 by 1.769 ms. At 80 Mbps/120 fps with a 160 Mbps wire target, the increases were 0.100 and 0.350 ms. CPU results varied across runs; there is no universal CPU-saving claim. The timer-only candidate was rejected.

The burst candidate sent each frame's second batch early, retaining its pacing debt. An SM-F966B phone on the same Wi-Fi connection compared 2.2.7 with the combined GPU-plus-burst candidate, using the same moving scene, 1920×1080/60 HEVC SDR and 62,988 kbps negotiated encoder rate. Each stable window lasted approximately 120 seconds. API counter deltas and raw per-frame traces gave:

Measurement 2.2.7 baseline GPU + burst candidate
Host sent FPS 60.371 60.369
Video payload Mbps 67.746 74.351
Send duration mean / p95, ms 0.731 / 1.603 0.823 / 1.613
Client-reported inferred unrecoverable frames 0 25
IDR requests 0 17
Reference invalidations 0 83

FEC reports are best effort, so zero reports do not prove zero loss. Adaptive FEC varied in both windows, and measured payload rates were unequal. This single pair does not prove causality: it combined GPU and pacing changes, and wireless conditions vary. It nevertheless fails the no-regression gate, so the burst change is dropped.

A separate GPU-only Wi-Fi check then completed in ABBA order: 2.2.7, 2.2.8, 2.2.8, 2.2.7. The same Radeon 780M laptop used official Moonlight 6.2.0 over WLAN, with identical 1920×1080/60 HEVC SDR settings, 80 Mbps requested and 62,988 kbps negotiated. The host used gpu_compute_conversion=false and the same full-screen 120 fps motion scene. Each client run lasted approximately 165 seconds; host counters and raw send traces cover an interior 120-second requested window. The GPU-only build contains no pacing changes.

Run Host sent FPS Video payload Mbps Send duration mean / p95, ms Send gap p95, ms
A1: 2.2.7 59.999 74.881 0.881 / 1.656 18.076
B1: 2.2.8 60.003 75.556 0.942 / 0.972 17.228
B2: 2.2.8 60.000 75.736 0.937 / 0.980 17.712
A2: 2.2.7 59.997 75.513 0.940 / 0.972 17.224

All four host windows had zero IDR requests, reference invalidations, frame replacements, sender drops and send gaps above 1.5 frame periods. Client statistics reported 60.04–60.06 incoming/decoded fps, 59.99 rendered fps, 0.00% network drops and 0.03% jitter drops in every run. Average reported host latency was 2.4/2.4/2.6/2.5 ms in run order. This short check found no observed regression at this workload; it does not establish exact packet loss, because client percentages are rounded and no FEC block reports were received. A2 matches the candidate's send p95, so A1's wider distribution does not establish an improvement. No end-to-end latency or CPU saving is claimed.

The laptop host evidence is in the same fixture directory below, in runs run-20261012T015057-514555Z, run-20261012T020813-526021Z, run-20261012T021320-288379Z and run-20261012T021915-058867Z, each with measurement.json and a host-logs snapshot including rotated traces. Client evidence is a1/b1/b2/a2 .raw.log and .stats.json under the laptop's C:\Users\ramaz\Documents\Codex\2026-10-11\lade\work.

The final GPU-only 2.2.8 build, with both pacing experiments removed, passed all 811 ordinary workspace tests, formatting and workspace/all-target Clippy with warnings denied. Four quiet 20-second loopback streams at 1080p60 and 40 Mbps, with gpu_compute_conversion=false, decoded every picture: HEVC 1198/1198, HEVC HDR 1198/1198, PyroWave SDR 1181/1181 and PyroWave HDR 1199/1199. Steady rates were 59.994–60.002 fps, with continuous audio and zero decode errors. Raw results are under work/gpu-only-e2e.

Evidence is in work/phone-tests/phone-20261012T010118-447124Z, under run-20261012T011159-475248Z/measurement.json and run-20261012T011751-458854Z/measurement.json.

The native C receiver matrix in work/pacing-native/20261012T012146-056459Z/result.json completed eight 30-second ABBA runs at an 800 Mbps wire target. Mean reported thread cycles per frame, baseline/burst, were 14.562M/12.769M at 150 Mbps/60 fps and 2.290M/1.597M at 80 Mbps/120 fps. These raw QueryThreadCycleTime counts are not converted to time. GetThreadTimes was inconsistent: the two 80 Mbps burst CPU readings were 12.66% and 0.31% despite similar cycle counts. Both 150 Mbps arms had kernel receive overflows with the unchanged 425,984-byte buffer. The 80 Mbps runs had no missing packets, but one baseline had 74 duplicates, so the matrix was not entirely clean. The phone gate failed; no ten-minute soak was run or claimed.

October 12 resource use (issue 23) #

Issue #23 separates estimated resource savings from measurements. Release 2.2.7 implements the following changes without changing the network pacer's waits or capture slot count:

Area Change
Ping and control workers Nonblocking media sockets wait for readability with a 250 ms idle bound. Control waits up to 250 ms without peers; connected peers use the next 8 ms feedback deadline or an earlier delayed mouse-release/key-repeat deadline. Input arrival wakes the socket immediately. Control MMCSS registration ends when its last peer leaves.
Background work Empty stall monitoring and inactive telemetry block. Idle housekeeping avoids spawning work or cloning settings. Service supervision waits on stop/session events and child exit, with timed restart and no-session retries. Display leases retain fast recovery checks and use 250 ms idle checks, with immediate teardown wake.
Helpers and logging Helpers dispatch before Tokio starts. The display watcher receives a completion event when the journal becomes empty; a subsequent display change starts a new watcher. Both log channels use 8192 lossy slots instead of the default 128,000.
Video transport ring, already present in the lockfile, seals video packets. Other nonce formats keep their existing crypto path. Conventional streams build and send one FEC block at a time, reuse shard allocations, and stop building unsent blocks after loss or cancellation.
GPU resources Cursor uploads retain their resources to a fence instead of blocking the CPU. AMF compute captures are retained through conversion rather than through encoder output. Capture and PyroWave owners forget imported texture references. Copy reuse waits for that texture's readers rather than unrelated conversions. Device shares its display metadata through Arc.
GPU and power policy The last streaming scope restores the previous process power-throttling and GPU scheduling settings. Nested streams retain the streaming policy until the final scope ends.
Graphics conversion Equal-sized PyroWave 4:4:4 planes use one MRT draw. Their SDR/HDR, scaling, letterboxing and cursor results are compared to the previous three-draw path by the native pixel fixture.
Queries and settings Known AMD-only/explicit-AMF startup probes skip unsupported standard-codec 4:4:4 modes. /serverinfo caches only native hardware/route facts for ten seconds; authorization and session fields stay live. mDNS checks addresses every thirty seconds. Effective settings rebuild after config/app/live-HDR changes, with a thirty-second refresh for selected ICC calibration. Cached releases retain consumed fields, and the update panel stops polling while hidden.

The ping constants imply about eight empty-socket timeout wakeups per second across both threads instead of about two thousand. That is a code-derived estimate, not a Windows context-switch or idle-power measurement. The smaller log queues similarly reduce reserved queue storage; no RSS reduction is claimed from the constants alone.

The Windows video_packet_performance probe compares encrypted and unencrypted 32-, 192- and 576-shard frames, including a reusable-buffer variant. The pre-change and new encrypted packets have identical counts, lengths and first frame SHA-256 values. Initial collected-frame medians were 41/353/1071 us before and 17/223/670 us after; concurrent builds introduce noise, so these are CPU microbenchmarks, not an end-to-end streaming claim. The reusable variant must be compared per workload; its largest-frame run did not uniformly beat collected packetization under that load.

Regression coverage includes legacy media PING, reconnects, delayed input deadlines, independent AES-GCM parity, lazy FEC blocks and nonce exhaustion, cache privacy/expiry, AMD probe selection, release metadata, display-watcher completion, GPU capture ownership and graphics pixel parity. On the RX 7900 XT, all eight native graphics tests passed, including exact MRT/three-pass pixel parity with SDR/HDR, scaling, letterboxing and cursors. Capture-fence ownership and both power/scheduling restoration tests also passed. Workspace Clippy with warnings denied and the web check/build passed. The local core suite passed 339 tests and the host suite passed 204; two Windows tests require pipe/firewall access denied by the local sandbox. The Playnite helper integration passed with a shorter temporary directory (the generated workspace path exceeded Win32's legacy path limit). These checks do not establish the issue's WPR/VRAM, saturated-stream or ten-minute network pass criteria.

Remaining experiments from the issue: timer-only or burstier pacing, reducing the four WGC helper slots to three, persistent codec-probe results keyed by full driver/AMF identity, and process/GPU resource readings in the timings/API. These need their own measurements or complete invalidation/measurement support. NV12/P010 and 4:2:0 graphics fallback targets have unequal sizes and cannot use the same MRT change; their existing conversion remains. No resource-saving estimate is presented as a measured latency, picture-quality or recovery gain.

October 10 PyroWave SDK 1.0 and a high-priority encode queue: shipped #

PyroWave moves from 186f0393 (API 0.6) to 502a3b52 (API 1.0, libpyrowave-shared-1.dll). The per-frame bitstream is the one upstream froze as v1 on 2026-10-03: the quantizer, dequantizer and block-packing shaders are unchanged, pyrowave_common.hpp only renames two enums and adds an on-disk header, and the decoder only gains an opt-in range scale. So Vibepollo 2.0 clients and Rubylight Android keep decoding it, and the host still advertises bitstream 186f0393. Vibepollo's encoder buffer-pool patch is dropped because upstream now pools those buffers per frame context; the 4:4:4 sizing and short-block patches still apply. Upstream also fixes an out-of-bounds read in the RDO analysis shader (efb6230).

New in 1.0 is a global priority for the Vulkan compute queue. The encode queue now asks for HIGH, or REALTIME when compute_queue_realtime is on, the same policy as the D3D12 copy and conversion queue, and falls back to the default when the driver refuses. The log line "PyroWave encode queue priority" records what was granted.

Host A/B on the RX 7900 XT (driver 26.9.2), base 2f53043 (SDK 186f0393, MEDIUM) against 957ace5 (SDK 1.0, HIGH; the log shows requested=512 granted=512), isolated test hosts and the loopback fixture client built against the old SDK, 20 s per run, three alternating repeats per cell, loaded runs beside gpu_load 60 1000 60 200:

Cell Picture age mean / p95 / p99 (ms) Encode mean / p95 (ms) Unique fps
1968x2184 HDR 4:4:4 120, idle, base 18.7 / 27.8 / 33.8 1.16 / 1.54 119.9
same, new 18.7 / 27.2 / 33.0 1.05 / 1.46 120.0
1968x2184 HDR 4:4:4 120, loaded, base 85.4 / 139.7 / 146.6 1.09 / 1.52 100.3
same, new 87.6 / 142.2 / 145.4 1.01 / 1.30 112.7
1080p120 SDR, idle, base 14.8 / 15.9 / 17.9 0.66 / 0.81 120.0
same, new 14.7 / 15.7 / 16.8 0.65 / 0.80 120.0
1080p120 SDR, loaded, base 20.2 / 26.6 / 32.2 0.72 / 0.90 115.8
same, new 20.6 / 24.8 / 29.2 0.70 / 0.88 116.3

Every run decoded every frame (0 decode errors, 0 replaced frames), so the old-SDK client reads the 1.0 host's stream. HIGH is no worse end to end: picture age is within run-to-run noise in every cell, and encoding is 0.1 ms faster at native size. The loaded native cells fail the e2e gap checks on both builds: the load also slows the loopback client, which decodes on the same GPU, so those cells compare the two builds but say little about a real client. No GPU watchdog dump during the runs.

October 10 WGC helper textures read in place: on by default #

On the service host WGC runs in a helper in the user's session, which copies each frame into one of its shared textures; the host then copied that into a texture of its own before the compute conversion. The host now lends the helper's texture to the conversion directly and hands it back, after the compute work submitted so far, when the last clone of the image is dropped (wgc_helper_zero_copy, AMD with compute copies only). The helper got a fourth texture so a held image does not starve it.

Host A/B on one build, 1440p120 AV1 to the loopback test client with motion_probe, three alternating idle pairs of 30 s: mean picture age 10.07 ms copied against 9.84 ms in place, host mean 2.843 against 2.774 ms, barcode coverage 1.0 and no decode errors in both. In place, a reconnect, a client kill and a 2-minute stream (119.6 fps) were clean. A pair beside gpu_load 45 1000 0 200 saturated the GPU and both arms fell to about 31 fps, so it could not tell them apart.

October 10 AV1 entropy-context (CDF) update: no default change #

Hidden settings amd_av1_cdf_update and amd_av1_cdf_frame_end (f546589) set AMF's Av1CdfUpdate and Av1CdfFrameEndUpdateMode for an offline sweep on the RX 7900 XT. The driver's defaults are already update on and frame-end mode 1. Update off cost 5-10% more bytes at the same request with PSNR 0.03-0.66 dB lower and VMAF about unchanged. Frame-end mode 0 was at best +0.024 VMAF with encode p99 within 0.3 ms either way; mode 2 is rejected (AMF error 4). VMAF was saturated (98.4-99.8) and AMF undershot the requested bitrate (35-55 Mb/s actual), so nothing here beats the driver. The settings stay hidden for experiments; the defaults don't change.

October 10 per-frame send-wait budget: not built #

The idea (triage L7/N2): a batch that finds the video socket full waits up to 4 ms (writable in net.rs), so a frame should share one wait budget of a frame period instead of 4 ms per batch. Counting with the real FEC layout and batch sizes (packet size 1392, 20% FEC, Wi-Fi pacing at twice the bitrate, wired at the 800 Mbps ceiling), a frame on a socket that stays full is already abandoned by send-loss after its first refused batch: one 4 ms wait for every P-frame from 20 Mbps up and every keyframe from 150 Mbps up, wired at any bitrate. Only keyframes on a Wi-Fi host at 10-80 Mbps take 2-9 refused batches (8-36 ms); a budget would cut that to about one period, and only during a host radio outage long enough to fill the 1 MiB socket buffer (about 100 ms or more). Under congestion, where the wait finds room, a budget would turn late frames into lost frames and keyframes. Not worth a host cycle; the draft (543614c, with a :full mode for BUTTERPOLLO_TEST_SEND_OUTAGE) was not merged.

October 10 MMCSS boost on the media threads: not shipped #

Priority::new registers a thread with MMCSS and then sets THREAD_PRIORITY_ABOVE_NORMAL, which cancels the boost (14 instead of 18). The input threads dropped that call in October (Priority::input, 3.5 ms to 17 us wake p50 beside TIME_CRITICAL spinners). Giving capture, encode, send and audio the same (4b88b6a, research item L2) made the stream worse with no load. Host ABBA, HEVC 2560x1440 at 120 fps, 50 Mb/s, A = c7e258a: picture-age mean 12.5 (A) against 14.3 ms (B), p95 15.4 / 18.3 against 17.9 / 18.9 ms; unique fps and host mean equal, 0 decode errors. An earlier no-load round agreed (11.7 against 13.1 ms). Loaded pairs were not run: TIME_CRITICAL spinners on every core freeze the desktop of an occupied PC.

Likely cause, not measured: every media thread ends its waits in a short spin (Timer::until, and until_precise yielding for 600 us), and at the MMCSS level of DWM's compositor those spins can hold a core the compositor or the game wants. A boost for these threads needs spin-free waits first. Dropped; the media threads keep Priority::new.

October 10 HEVC reference invalidation after short losses: not shipped #

The HEVC freeze above needs a long loss. H.265 8.3.1 derives a picture's order count from the previous picture's and reads a step of more than half the range as going backwards. AMF's range is 16, so after the 11-frame loss the recovery frame (12 after the last picture) was placed before its long-term reference, which then resolved to a missing picture. A branch (7ff31be, 58fa344) kept HEVC long-term references and invalidated only when the recovery frame was at most 8 frames after the last picture, else a keyframe. The native fixture passed: HEVC recovered 4-frame losses and took a keyframe for an 8-frame loss (recovery 9 after), AV1 recovered both.

Host A/B against 02509c0, HEVC 2560x1440 at 120 fps, 80 Mb/s, 160 Mb/s cap, no outage: B delivered 103.8 unique pictures/s against 119.6, picture age mean 14.6 against 8.8 ms (p95 18.4 against 9.5). The repeat B run stopped after 14 s in a fourth VIDEO_ENGINE_TIMEOUT (0x141, WATCHDOG-20261010-1020.dmp) after slow encoder call AMF QueryOutput 3000 ms; the GPU recovered without a reboot. The earlier steady-state pair with HEVC long-term references (9438e9c, set by config) was within noise, so the cost is not settled, but a steady-state loss and a hang on the first two runs fail the ship bar. HEVC keeps keyframe recovery; HEVC long-term references stay off. Not retried: marking long-term references only after a loss report cannot work, since recovery needs an anchor from before the loss.

October 10 AMD reference invalidation, AV1 only #

Moonlight can recover a lost frame by reference invalidation: the host encodes the next frame from an older long-term reference instead of sending a keyframe. AMF offered it only with amd_ltr_frames set (default 0). Fixture: 2560x1440 at 120 fps, 80 Mb/s under a 160 Mb/s pacing cap, an 80 ms outage every 1.5 s lost after the socket (BUTTERPOLLO_TEST_SEND_OUTAGE=1500:80:air, so only the client notices), test client with BUTTERPOLLO_TEST_RFI=1.

AV1 keyframe recovery invalidation
Stall mean / p95, set 1 106.0 / 106.5 ms 99.6 / 100.2 ms
Stall mean / p95, set 2 103.4 / 106.5 ms 96.4 / 100.0 ms
Stall mean / p95, set 3 96.1 / 99.6 ms 91.7 / 92.9 ms
Recovery frame 98-127 KB 15.5-24.6 KB
Steady state (no outage) picture age 9.35 ms, encode 2.90 ms, 17.8 KB 9.32 ms, 2.88 ms, 17.8 KB

Every AV1 frame decoded. HEVC and H.264 did not survive it. After the first HEVC invalidation the client's d3d11va decoder rejected every later picture ("Error constructing the frame RPS") for 58 s without asking for a keyframe. The header trace shows why: AMF writes a four-bit POC (it wraps every 16 frames), and after an 11-frame loss the long-term reference it names resolves to a missing picture. H.264 kept showing pictures up to 2.5 s old for the rest of each run.

Shipped (b78f853, 7c84750, 204ecdd): amd_ltr_frames defaults to 4 and applies to AV1 only. H.264 and HEVC answer an invalidation with a keyframe. After any AMF invalidation recovery a keyframe follows one second later, so a decoder that cannot follow it is frozen for about a second, not until the next loss. Moonlight-qt's D3D11VA renderer (Windows, the laptop client) offers invalidation for HEVC and AV1, not H.264 (getDecoderCapabilities in moonlight-qt master), so a real Windows client streaming AV1 uses this path, and an HEVC client now gets keyframes.

The first H.264 run of the last set, on the base build (no invalidation offered, so plain keyframe recovery), ended in a third RX 7900 XT VIDEO_ENGINE_TIMEOUT (0x141). The stall watch logged the session thread stuck in its encode call with the GPU query probe answering; the call never returned. All three timeouts (02:47 and 23:33 on October 9, 07:21 on October 10) came under keyframe-heavy recovery at high bitrate, two of them on base builds. A call blocked in the kernel driver cannot be ended by the host. The host's 07:21 session had LTRFrames=0 and no client invalidation in the 1.5 s before. Driver 32.0.31041.1004 (2026-08-17) left 24 WATCHDOG dumps over October 6, 8, 9 and 10 (October 6-8 predate this week's work); the 02:47 and 23:33 dumps share amdkmdag.sys+0x1d0fb0. The hang is chronic on this driver; troubleshooting.md now describes it for users.

The October 9 sender thread (encode and send overlap on their own threads) cut p95 by 1.8 ms under a 160 Mb/s pacing cap but added 1.8 ms on the default 800 Mb/s wired path. It now starts only when the pacer rate is within 4x the stream bitrate (network_pacing::paced): a pacing_max_bitrate_kbps cap or a wireless route (2x bitrate). Wired streams keep the inline send path unchanged. video_send_thread = false turns it off. The send-loss recovery above runs on both paths.

Host A/B, 2026-10-10 (RX 7900 XT, loopback): A = be942ba, B = 1984b63.

A B
160 Mb/s cap: picture-age p95, ms 19.4 16.0
160 Mb/s cap: claim-wait p95, ms 4.26 1.04
Default wired: picture age equal (inline path in both)
80 ms outage every 1.5 s: stalls equal

Watch item: on the paced path the recovery keyframe's p95 is 2.2 ms later (95% CI +0.1 to +4.3 ms). One B run failed "pictures missing"; the host reads it as fixture variance. Reconnect and client kill pass, 0 decode errors. Build check: 160 host tests, clippy -D warnings clean. Ships.

Follow-up 0d2ffd2: on a keyframe request the sender thread skips the delta frame waiting in its queue (the client discards it; its wire number is left as a gap, so reference invalidation numbering holds), and a keyframe replaces a waiting delta frame. Host A/B at the 160 Mb/s cap, A = c1c8470: recovery keyframe p95 65.1 / 64.6 / 65.2 ms (A) against 60.7 / 60.2 / 61.2 ms (B), paired -4.3 ms (CI -4.4 to -4.0, 3 of 3), back to the inline path's level. Pictures missing at most 1 per request (7 of 60), as designed. Picture age within noise. Outage-pair stalls 91.6 / 92.9 / 93.0 (A) against 92.1 / 97.2 / 100.5 ms (B), 0 decode errors. 161 host tests, clippy clean. Ships.

October 10 Wi-Fi wave 2: a keyframe right after a frame the host could not send #

On a host whose own Wi-Fi drops out (the October 9 user report: a Legion Go host whose socket refused 11-371 video packets at a time, each followed by a recovery request), the client loses the frame and, as Moonlight does, asks for a keyframe only after its next complete frame and a round trip. The host knows at once: Winsock refused the packets.

Change:

Test hook for the A/B: BUTTERPOLLO_TEST_SEND_OUTAGE=every_ms:length_ms refuses all video datagrams for length of every every (from the second interval on), in both builds. rust/tests/recovery_gaps.py reports the picture stalls in the test client's timing CSV.

Host A/B, 2026-10-10 (RX 7900 XT, idle, loopback): A = 935b514 (test hook only), B = c2dbfc5. HEVC 2560x1440 at 120 fps, 50 Mb/s, strip at the stream rate, 3 alternating pairs of 30 s with an 80 ms outage every 1.5 s, then 2 pairs with no outage.

Outage runs A B
Picture stall mean / p95 / max, ms 97.7 / 101.7 / 101.8 88.4 / 92.6 / 93.2
Frames missing per stall 10.8 9.6
Client keyframe requests per run 17 1
Receiver fps 113.0-113.3 113.8-114.4

Per-run stall means do not overlap. Without outages both builds pass, host mean is 3.13-3.14 ms in both, picture-age p95 is 11.4 / 9.1 ms (A) against 9.3 / 9.3 ms (B), and B records no send_loss_recoveries. 0 decode errors, no GPU guard abort, no new dumps. Build check on the host: 153 host tests, clippy -D warnings clean. Not measured: a real Wi-Fi host, a real client, AV1 and H.264. Ships.

October 9 research triage: host verification of the five BUILD NOW items #

Codex implemented the five BUILD NOW items from the research triage as stacked commits on codex/triage-b (base 74c0d3cf). A verifier reviewed the code and measured on the host (RX 7900 XT, evening of October 9, installed host idle before every run). Only item 1 is on codex/verified. The review is in butterpollo-release/research/verify-review.md; it found no blocking code defect. Runs, scripts and logs are in research/verify/.

# Item Commit Verdict
1 Benchmark integrity gate and per-frame send trace 3605c124 ship
2 AMF ten-bit eligibility, applied-bitrate bookkeeping, HDR metadata test 91add468 needs more: the extended HDR test fails on the host
3 Link-speed lookup off the video threads 88505c51 drop: its own step-0 gate failed
4 Client FEC status telemetry (0x5502) 71344486 needs more: no real client report observed
5 Bounded, ordered sender for H.264/HEVC/AV1 735629df, 66897618 needs more: no picture-age gain beyond noise

What could not be measured. The isolated e2e fixture streams the primary monitor, an Odyssey G9 at 5120x1440 and 240 Hz with HDR off. Native 1968x2184 HDR120 needs the virtual display and the SYSTEM batch, which were out of scope. The proxy for it is HEVC 5120x1440 at 120 fps SDR (1:1 with the desktop, 7.4 Mpx). No Wi-Fi client, laptop or real Moonlight client was used, and loopback has no loss. So FEC reports, RFI after loss and link changes were not exercised.

The fixture: hardware-decoding receiver (BUTTERPOLLO_TEST_HW_DECODER=d3d11va), WGC, pacing=trace. Default pacing on loopback is the 800 Mbps wired ceiling (link_bps=0). The client always enables video encryption (ENCFLG_ALL), so every run checked the IV path.

For the sender cells the high-entropy source was a click-through noise window. It covered the right 34% of the screen above the motion strip and gave HEVC frames of about 71 packets at 80 Mb/s. The first full-screen version quit on any user input, and all 20 of its attempts were discarded while the owner worked. None of the partial-window runs was tainted.

1: trace cost. HEVC 5120x1440 at 120 fps, 80 Mb/s, 60 s runs, three pairs, host mean in ms:

Build Log Host mean per run Mean
base info 4.983 / 5.047 / 5.056 5.029
item 1 info 5.114 / 5.044 / 5.047 5.068
item 1 pacing=trace 5.163 / 5.152 / 5.057 5.124
base pacing=trace 5.103 / 5.063 / 5.089 5.085
item 1 pacing=trace 4.973 / 5.035 / 5.067 5.025

The send fields add nothing measurable on top of the existing claim trace: traced item 1 is -0.06 ms against traced base, with 11 late intervals against 21. Untraced item 1 is within the base spread. The 0.056 ms between info and trace is the cost of pacing=trace as a whole, which is diagnostic only. The gate labelled every run correctly as matched (wgc, 5120x1440, 240 Hz, Bgra8). Ship.

2: AMF. The default policy snapshots and unit tests pass. The extended ignored test hdr10_metadata_and_range_reach_the_bitstream fails on the host. A printing copy of it showed the mastering peaks follow every change (1015, then 600, then 1400 nits) and CLL known, then 0, then known on one encoder gives 1000/400, then 0/0, then 1200/500, for HEVC and AV1. AMF keeps the CLL SEI with zeros after a change to 0; a fresh encoder with CLL 0 omits it. 0/0 means unknown, so the stream is right and the assertion (absent after known to 0) is too strict. As committed, the test fails the hardware sweep. It needs the one-line test fix before shipping.

3: link lookup. The step-0 timing, 1000 routed_link calls to the laptop's address over wired 2.5 Gb/s: 0.051 ms mean, 0.081 ms p99, 0.561 ms max (Codex: 0.056 / 0.087 / 0.517). That is under the 0.1 ms p99 gate, so the item is dropped. Loopback cannot show a spike either: routed_link returns at once for loopback.

4: FEC telemetry. The parser is bounds-checked and has no panic path, and it changes no decision. The counters stayed at zero in all 73 e2e runs of builds that carry them, which is expected because loopback has no loss. Host timing equals base: host mean A against base in the noise cells is 5.13 against 5.13, 5.40 against 5.79, 2.29 against 2.42 and 2.87 against 2.85 ms. The triage's step 0 is still open: an installed Qt or Android client must be seen sending 0x5502 to this host, then 10 minutes on Wi-Fi.

5: sender. A is 71344486 (inline), B is 66897618, base is 74c0d3cf. Each run is 35 s with alternating order. Cells: (a) HEVC 5120x1440 at 120 fps, 80 Mb/s, default pacing, --recovery 20, RFI advertised; (b) the same with pacing_max_bitrate_kbps=160000; (c) H.264 1920x1080 at 60 fps, 20 Mb/s, --motion-at-rate; (d) AV1 2560x1440 at 120 fps, 50 Mb/s, --motion-at-rate. All with the noise source. The table gives means over runs; picture age is render to decode, in ms.

Cell Build Runs Picture age mean / p95 / p99 Claim wait mean / p95 Claims held by a send Fresh claims/s Claim gap max
a base 3 20.4 / 25.4 / 27.6 0.45 / 2.48 - 120.1 13.7
a A 7 20.6 / 22.6 / 25.1 0.59 / 1.57 0.1% 120.0 13.2
a B 7 21.6 / 24.6 / 25.6 0.44 / 2.06 0.2% 120.0 15.3
b base 3 25.7 / 30.7 / 33.0 1.41 / 4.09 - 118.6 44.5
b A 8 24.0 / 28.9 / 30.8 1.60 / 5.14 58% 118.9 41.0
b B 5 22.1 / 25.5 / 29.9 0.39 / 2.55 0.4% 119.9 25.8
c base 3 14.7 / 18.9 / 25.5 0.16 / 0.10 - 60.0 21.8
c A 3 15.0 / 16.6 / 17.2 0.21 / 0.33 0% 60.0 20.1
c B 3 15.9 / 16.9 / 17.4 0.14 / 0.10 0% 60.0 20.3
d base 3 18.0 / 22.1 / 23.6 0.89 / 4.27 - 120.2 12.5
d A 7 17.9 / 20.9 / 22.7 0.57 / 2.73 0% 120.2 12.1
d B 7 16.8 / 20.5 / 21.5 0.45 / 2.80 0% 120.1 11.5

"Claims held by a send" counts fresh claims made within 0.3 ms of the previous frame's last datagram after the picture had waited more than 0.3 ms. The baseline gate passes. In cell b, 48-64% of A's claims wait behind the inline send (send occupancy p95 3.9 ms at 160 Mb/s). B removes that wait: claim-wait p95 drops from 5.1 to 2.5 ms, fresh claims rise from 118.9 to 119.9 per second, and the longest claim gap falls from 41 to 26 ms.

The end-to-end requirement is not met. B's picture-age p95 and p99 in cell b (21.4-30.3 and 25.0-36.6 per run) overlap A's ranges (26.7-32.6 and 27.8-34.4). A verifier variant of B without items 3 and 4 (local commit 73181d10, link lookup kept on the sender thread) scored 24.0 / 28.0 / 30.5 over 3 runs. Picture age with the strip at the stream rate depends on the capture phase: claim-wait p95 is bimodal, 0.1-1 ms or 5-6 ms, in every build. Cells a, c and d are equal within the spread. In cell a, B's p95 is 2 ms above A but inside both ranges and the base's.

Keyframes at 160 Mb/s take as long either way: recovery p95 is 59.7 ms (A) against 59.7 ms (B). Cell b fails the e2e keyframe and gap limits for every build, including base, because a 5120x1440 keyframe takes about 10 ms on the wire at that rate.

Correctness checks:

The sender is correct and does what it was built to do. It needs a native 1968x2184 HDR or Wi-Fi measurement that shows a picture-age gain before it ships. To ship it without items 3 and 4, the cherry-pick needs the conflict resolution in 73181d10.

Items 1 and this section reached main as 10c8cea0 and d68fc79d.

Follow-up, October 9-10: item 2 fixed, item 5 settled #

Item 2: ships. The extended HDR test now passes on the RX 7900 XT for HEVC and AV1, both ranges, compute on and off, refs 1 and 4, and defaults and options. It now requires absence of the content light level only on a fresh encoder's first IDR. After a change it accepts the SEI/OBU at 0/0, which means unknown, and fails on any stale non-zero level. The scaling test was renamed to what it covers, and the invalidation check has a comment.

codex/verified is main plus 91add468 and the test fix. Its checks:

All passed.

Item 5: needs more. It wins on a paced link but regresses the default wired path. The comparison was paired A/B with the noise source: the standalone sender variant (73181d10) against its exact base (3605c124, the same tree as item 1 on 74c0d3cf). The runs are 35 s, alternating, --recovery 20. The CIs are 95%, from a bootstrap over pairs.

Cell (HEVC 5120x1440@120, 80 Mb/s) Pairs Picture age p95 diff, ms p99 diff, ms Claim-wait p95, base to sender Fresh claims/s diff Receiver unique fps diff
b, pacing capped at 160 Mb/s 13 -1.81 [-3.33, -0.26] -1.38 [-2.41, -0.35] 3.85 to 1.17 ms +0.77 [+0.54, +1.05] +1.48 [+0.43, +2.52]
a, default 800 Mb/s pacing 11 +1.84 [+0.35, +3.34] +1.12 [-0.44, +2.62] 1.61 to 3.02 ms +0.12 [-0.23, +0.46] -2.11 [-4.61, -0.31]

Picture-age p50 and mean are unchanged in both cells, with differences within ±0.4 ms.

So it fails the non-inferiority bar on the owner's default wired path. One plausible cause, not yet confirmed: the claim gate now counts the frame being sent (encoding + sending >= 2), so on a fast link it can defer a claim that the inline path would have made right after a 0.5 ms send.

Stability of the sender build:

After the reboot, 12 guarded runs (3 pairs per cell, at most 3 min each, aborting on any audio stall over 100 ms, device loss, stall or new watchdog dump) had no abort and no new dump.

The sender should not ship as is. Next step: make it neutral on fast links before re-measuring cell a. One option is to use it only when pacing is close to the bitrate, as on Wi-Fi routes or with pacing_max_bitrate_kbps; another is to count only pending, not sending, frames in the claim gate.

October 9 user report: constant jitter, and recovery requests #

Report (a user, relayed by the owner, 2026-10-09 08:54Z): "constant jitter". Their log (rc.28 host on a Legion Go, Radeon 890M, on Wi-Fi; AYN Odin 2 Portal client; 1080p60 H.264, virtual display at 120 Hz, RTSS limit 60; Hogwarts Legacy through Steam Big Picture) points at the game's frame rate, not at the host's pacing. In the 8.5 min game session only 26 of 101 five-second windows reach 58 fps or more; 37 sit at 39.7 fps and 10 near 30 fps, the 120 Hz V-Sync steps, with frames claimed on arrival (claim wait 0.8 ms mean in the 40 fps windows) and encoded in 3.0 ms. A 40 fps game in a 60 fps stream is shown for one or two refreshes in turn. Their rc.22 log from October 8 shows the same steps (12 of 28 windows at 40 fps), before the rc.27 and rc.28 changes. Recovery requests: 8 in the session, each within 5 s of the host's Wi-Fi socket dropping video packets (WSAEWOULDBLOCK, 11 to 371 packets at a time), and no reference invalidations.

Looking for the report turned up a regression that costs a hitch after each of those drops (from the code and the model below, not yet measured on the host):

Model (recovery_requests_do_not_hold_back_a_moving_picture, 10 s, requests at every phase of the frame period):

Source and requests Build Encodes Frames sent Game frame wait mean / p95 / p99 / max ms
120 fps game at the stream rate, 0.2-0.6 ms jitter, request every 1.5 s rc.28 1206 1200 of 1200 0.50 / 2.83 / 3.38 / 3.90
same, every 0.5 s rc.28 1208 1193 1.61 / 5.62 / 6.87 / 8.42
same, every 0.25 s rc.28 1213 1182 2.78 / 6.60 / 7.52 / 8.93
fix 1200 1200 0.02 / 0.05 / 0.05 / 0.05
60 fps strip on a 144 Hz display (14/21 ms gaps), 60 fps stream, every 0.25 s rc.28 601 581 of 600 6.39 / 10.45 / 11.86 / 11.87
fix 600 600 0.02 / 0.04 / 0.04 / 0.04

Fix: while the source moves, a recovery request waits for the next new picture, for two stream periods or 1.5 source frame intervals after the current one was presented, whichever is longer (stream_policy::reencode_at); a still or slow screen is still encoded again at once, keeping rc.28's 2.4 ms keyframe there. An unchanged picture encoded again for a recovery request spends no pacing credit (counts_toward_rate); static repeats still do, so the PyroWave VRR cap from rc.27 stays.

How it got past the tests: every host A/B ran over loopback without loss, so no keyframe was requested while the picture moved; the rc.28 wake was measured with requests only on a still desktop; rc.27's counting change was A/B'd with PyroWave (no keyframes) and AV1 without loss; the release check streamed without requests and with its strip at twice the stream rate; the laptop smoke checks only average fps and host latency. The release check (check.ps1) now also streams hevc-recovery: HEVC with the strip at the stream rate, the minimum frame rate at its default, and 16 keyframe requests 0.5 s apart (BUTTERPOLLO_TEST_IDR_PROBE_INTERVAL_MS), with the host tracing every claim (RUST_LOG=info,pacing=trace). It fails when the host encodes an unchanged picture again, when a request gets no decoded keyframe, or when the keyframe p95 exceeds three frame periods; one late frame per request is allowed beyond the usual 1%. The receiver's pictures seen twice or skipped and picture age are recorded only: with the strip at the stream rate the fixture repeats and skips 5-28 pictures in 12 s with no request at all (e2e.py --motion-at-rate, the control). claims_summary.py summarizes a traced host log.

Host A/B, 2026-10-09 (RX 7900 XT, idle, extended layout): rc.28 main a5884577 against the fix 58953c4f, isolated extended virtual display at 240 Hz, 1968x2184 HDR 120 fps HEVC at 80 Mb/s, hardware-decoding receiver, 20 keyframe requests per run, no dropped frames in any run.

Motion probe Build Picture age mean / p95 / p99 / max ms fps Pictures sent twice Claim wait p95 ms Keyframe mean / p95 / max ms
120 Hz (stream rate) rc.28 9.67 / 11.87 / 12.69 / 22.19 120.58 19 2.36 6.19 / 8.24 / 8.52
rc.28 10.74 / 12.79 / 13.85 / 24.73 120.51 19 2.11 6.29 / 7.75 / 8.05
fix 10.40 / 11.03 / 11.55 / 15.45 120.00 0 0.013 9.08 / 12.31 / 13.10
fix 9.35 / 9.95 / 10.37 / 13.99 120.00 0 0.014 9.08 / 12.25 / 13.27
240 Hz rc.28 equal within noise 8, 10 6.1, 5.8 mean
fix 0, 0 6.1, 6.7 mean

A keyframe on a moving picture now waits for the next new frame, about 3 ms later at 120 fps. The still 1080p120 desktop keeps the fast keyframe: 2.42 / 2.49 / 2.71 ms (fix) against 2.30 / 2.37 / 2.57 ms (rc.28).

Release-check stream, HEVC 2560x720 at 60 fps, strip at 60 fps, traced:

Build Requests Host claims / unchanged picture encoded again Receiver seen twice / skipped Keyframe mean / p95 / max ms Picture age mean / p95 / p99 ms
rc.28 16 711 / 10 42 / 40 3.37 / 4.77 / 5.45 13.27 / 24.18 / 31.44
fix 16 705 / 0 15 / 15 4.15 / 7.33 / 8.45 12.55 / 16.50 / 32.15
rc.28 none (control) all new 28 / 25 - 17.50 / 28.35 / 29.80
fix none (control) 706 / 0 5 / 5 - 21.28 / 23.45 / 25.18

October 9 held fixes: host A/B after the reboot #

RX 7900 XT, after the 06:27 reboot, rc.27 installed. Each fix was cherry-picked onto main 235c3fac and compared with main itself, A/B/A/B: isolated extended virtual display at 240 Hz, motion probe at 240 Hz, 1968x2184 HDR 120 fps at 80 Mb/s, hardware-decoding receiver, 35 s per run. Picture age mean / p95 / p99 ms.

Build HEVC r1 HEVC r2 AV1 r1 AV1 r2
main 11.58 / 11.99 / 12.23 11.21 / 12.03 / 12.29 10.90 / 11.57 / 11.91 10.92 / 11.55 / 11.94
resume waits for teardown (0a5a85f5) 11.34 / 12.05 / 12.43 11.10 / 11.93 / 12.11 10.94 / 11.66 / 12.05 10.88 / 11.58 / 11.92
no H.264 B-frame property on HEVC (e8af3a99) 11.04 / 11.69 / 12.11 11.61 / 12.06 / 12.29 - -

Both neutral (host mean 3.2-3.4 ms HEVC, 3.0 ms AV1 everywhere, 120.0 fps, no drops) and shipped. The resume fix is also covered by the GPU-free reconnect matrix (reconnect_tests.rs), now with no ignored cases.

Second batch, against main 33d4a11f plus the two fixes above (October 9 07:25-07:55), same setup:

Build HEVC r1 HEVC r2 AV1 r1 AV1 r2
main 11.23 / 11.72 / 12.09 11.22 / 11.64 / 11.92 10.61 / 11.19 / 11.51 11.36 / 11.74 / 11.85
GPU-reset recovery (c2fa482f, re-applied) 10.96 / 11.52 / 11.84 11.29 / 11.90 / 12.06 10.68 / 11.32 / 11.74 11.01 / 11.59 / 11.82
main (second pair) 11.14 / 11.79 / 12.05 10.91 / 11.51 / 11.90 10.87 / 11.48 / 11.74 10.85 / 11.50 / 11.81
recovery-request wake (81c8fb3a, re-applied) 11.10 / 11.74 / 12.03 11.34 / 11.66 / 11.85 10.79 / 11.43 / 15.43 10.66 / 11.19 / 11.41

Both neutral on moving pictures (the single 15.4 ms AV1 p99 did not repeat: a third wake run gave 10.85 / 11.45 / 11.73). The wake's target, measured with the receiver's IDR_PROBE on a still 1080p120 desktop (10 keyframe requests per run, request to complete IDR arrival, mean / p95 / max): main 5.80 / 7.50 / 8.83 and 6.02 / 7.35 / 8.35 ms, with the wake 2.44 / 2.51 / 2.83 and 2.39 / 2.46 / 2.77 ms. A client recovering from loss on a still or slow screen gets its keyframe about 3.5 ms sooner instead of waiting for the next frame period. GPU-reset recovery has no host trigger short of a real driver reset; its fault-injection tests cover it. Both together (the two commits touched the same capture subscription and were merged by hand): HEVC 11.21 / 11.63 / 11.79 ms, IDR 2.37 / 2.41 / 2.73 ms. Shipped as aebecdda and 5a7f7641.

Batched virtual display apply (d685d085, opt-in), same second-client fixture as on October 8 (WGC HEVC 1080p60, three other clients each creating a 1280x720 display): with the setting on, each creation made 1 whole-topology apply of 304-514 ms. Picture age on the first stream was not better: p99 / max 57.5 / 252 and 79.1 / 364 ms with it, 54.1 / 308 and 43.8 / 298 ms without. The hitch is the topology change itself, not the number of calls, so the change is not shipped; it stays on the local codex/hitchfix branch.

October 9 AV1 padding at 1968x2184 #

Investigation only; no encoder, shader, codec policy or service changes. The RX 7900 XT's extra coded area is generated inside AMF/VCN, outside the host's logical input texture. The host already reuses textures and converts only the requested picture. There is no demonstrated uninitialised host padding to fix, and no measured benefit that justifies changing allocation, alignment mode or AV1 headers. No new size/alignment logic or unit tests were added. H.264 and HEVC are unchanged.

This review used source, the installed SDK headers and saved October 2 fixtures. The GPU remains unavailable pending reboot: no hardware tests, capture, encoding, live streams or service operations were run. New evidence is CPU-only decoding of an existing synthetic D3D11 AV1 fixture, not a fresh host measurement. Review artifacts and check logs are in C:\Users\ramaz\.codex\artifacts\butterpollo-av1pad-20261009.

Alignment and the actual padded picture #

The SDK used by windows/build.rs includes AMF/components/VideoEncoderAV1.h from BUTTERPOLLO_FFMPEG_ROOT/include. Its Av1AlignmentMode enum is:

Value SDK suffix Meaning for this RX 7900 XT
1 64X16_ONLY Requires width divisible by 64 and height by 16; cannot accept 1968x2184.
2 64X16_1080P_CODED_1082 Same restriction, with a 1920x1080 exception producing 1920x1082.
3 NO_RESTRICTIONS Current public constructors; accepts the requested input but permits padded output.
4 8X2_ONLY Smaller alignment exists in the API; the saved RDNA3 experiment still produced the same padded output.

AMD documents black padding and the older hardware restriction in its AV1 guide. The SDK header also exposes Av1WidthAlignmentFactor and Av1HeightAlignmentFactor. AMD identifies newer hardware with smaller requirements in issue 423; setting a newer enum does not remove the RX 7900 XT's hardware limitation. Read back those capabilities after reboot rather than opening AMF now.

This is not simply width-to-64 and height-to-2 rounding: 2184 is already even. FFmpeg's AMF crop calculation uses the reported alignment factors (64x16 fallback for Navi3x), rounds each dimension up, then replaces an eight-row bottom pad with two rows because of a hardware special case. Thus the usual 16 extra columns remain, while 2184 would need eight rows to reach 2192 but gets two, reaching 2186. This explains more than the old guide's specifically named 1080p exception. It matches all saved SDR/HDR, mode-3/mode-4 cases in butterpollo-rust-20260930/day-work-20261002/av1-geometry/geometry.json: 1920x1080 -> 1920x1082; 1968x2184 -> 1984x2186; 2184x1968 -> 2240x1968. Property readback still reports the requested sizes.

The saved HDR header has use_128x128_superblock=0, so its coding superblocks are 64x64. That is distinct from input alignment and signalled frame dimensions: 2186 is not a multiple of 64. Both 1968x2184 and 1984x2186 occupy a 31x35 superblock grid, with partial edge superblocks. There is no additional whole superblock row or column here.

Who fills it, and what is converted each frame #

windows/src/amf.rs passes the requested width/height to FrameSize, AMF Init and the converters, and calls SetCrop(0, 0, width, height) before submission. That surface crop controls input; it has not produced an output crop in the saved bitstreams. In amf/gpu.rs, the native D3D11/D3D12 surface wrappers retain the converted texture without a host-side padded copy.

The black border is therefore supplied by AMF/VCN, not a host edge-replication or clear pass. To check actual pixels without the GPU, FFmpeg 9.0.2 decoded the existing av1-1968x2184-hdrtrue-align3.obu with -hwaccel none, -c:v libdav1d -threads 2 to planar 10-bit raw samples. In all eight frames, every pixel in the 16 right columns and two bottom rows has Y=3 and U=V=512. The adjacent visible right-edge luma is 431/615 in the fixture's two halves. This is stable neutral, below-limited-range black after lossy decoding, not replicated picture content or evidence of uninitialised host pixels. It does not establish a universal raw fill value for every driver/input path. The input SHA-256 is 7ab9991b6ba3ab7802a108c856183a379ea7f0b2801169b4ce6dd34e16c003d0; the sample counts are in border-samples.json in the review artifacts.

Cropping and the client #

The saved keyframe's sequence header says max_frame_width_minus_1=1983, max_frame_height_minus_1=2185. Its frame header has frame_size_override_flag=0, render_and_frame_size_different=0, and super-resolution is disabled. Consequently the decoded and render sizes are both 1984x2186. Render size belongs to the frame header, not the sequence header, and is not a crop rectangle. The AV1 specification, section 6.8.5 defines it as an application display hint with no effect on decoding. FFmpeg's libdav1d wrapper uses it to derive sample aspect ratio while retaining decoded dimensions. Rewriting it could therefore imply scaling/aspect changes rather than cutting off the border. The earlier render-size prototype below is not a general crop fix; changing coded dimensions without re-encoding the tiles is unsafe. FFmpeg's AMF container crop side data does not travel in this host's raw AV1 GameStream packets (amf.rs output -> host/src/stream.rs -> core/src/packet.rs).

Moonlight Qt's FFmpeg path has an explicit RDNA3 workaround: compare decoded dimensions with the original requested dimensions, and crop right/bottom if each excess is nonnegative and less than 64. Here that removes exactly 16 columns and two rows with av_frame_apply_cropping. Its D3D11 renderer uses the resulting frame dimensions for the source rectangle and texture coordinates. This path should show the original 1968x2184 picture without a padding bar or squeezing the padded image; ordinary scaling to the client window remains. Check for the client's cropping log and a border/grid image after reboot to establish actual client acceptance.

The owner's exact client/version was not supplied, so that conclusion cannot be applied to every Moonlight platform or fork. The inspected Android MediaCodec path configures the requested dimensions, uses scale-to-fit, and logs decoder output-format changes; it has no equivalent negotiated-size crop correction. Correct display there depends on the device decoder/output crop and needs a real client check. A requested-size overlay alone does not prove cropping.

Cost and post-reboot host A/B proposal #

Quantity Value
Visible luma pixels 1968 x 2184 = 4,298,112
Coded luma pixels 1984 x 2186 = 4,337,024
Extra per frame 38,912 = 0.9053% of visible pixels, not 1.4%
Extra at 120 fps 4,669,440 luma pixels/s
Equivalent extra tightly packed P010 storage 116,736 bytes/frame; 14.0 MB/s for one write at 120 fps

The storage/bandwidth figures are arithmetic, not measured transfers. There is no extra host conversion dispatch or host copy for this coded border. A hypothetical full 1984x2186 conversion would use 124x137 groups instead of 123x137. VCN still codes the border and decoders reconstruct it, but constant black, the unchanged superblock count, internal layouts and possible driver copies prevent deriving an encode-time or bitrate penalty from pixel count. AMF's closed implementation may do additional preparation; source inspection cannot establish whether it clears or copies its internal padding per frame.

Allocating an aligned surface once and clearing padding once would only help if it removed a measured AMF internal operation. The host already performs the proposed visible-only conversion and pool reuse. Edge replication would need updates when the edge changes. A one-time clear must initialize each pool surface, use correct YUV black (limited P010 Y=64<<6, U/V=512<<6; full-range Y=0), and finish before AMF reads it; clearing zero bytes would give incorrect chroma. Merely enlarging targetSize would scale the desktop and increase conversion work, so it is not a safe optimisation.

After reboot, first rerun the existing ignored amf::tests::native_av1_geometry_and_hdr_are_preserved fixture with a fresh BUTTERPOLLO_TEST_GEOMETRY_DIRECTORY and explicit executable paths in BUTTERPOLLO_TEST_FFMPEG and BUTTERPOLLO_TEST_FFPROBE. It compares both alignment modes in SDR/HDR and is expected to fail strict decoded dimensions on this driver. Preserve that failure criterion. Record alignment capability readback and the actual client's output crop, including a one-pixel border and square grid to distinguish cropping from squeezing/scaling.

Only if profiling identifies an internal padding/preparation cost, try a test-build A/B, not a default or a new flag:

  1. A: Current 1968x2184 HDR input, mode 3, 120 fps and the owner's normal bitrate/settings/capture path. Save the bitstream and conversion, AMF submit-to-output and end-to-end frame-age timings.
  2. B: AV1-only 1984x2192 (64x16) coded canvas and pool allocation, explicitly black-initialized once per surface, with the 1968x2184 picture at the top-left. Keep capture, negotiation, conversion coordinates and 123x137 dispatch unchanged; use the full canvas for AMF FrameSize/Init/input crop and mode 1. This requires a confirmed client crop of 16 columns/eight rows. It codes more pixels than A, so a win is not assumed. Do not substitute a larger negotiated desktop, which would change content and scaling.
  3. Before implementing B, add CPU unit tests separating visible size, storage size and dispatch: 1968x2184 -> 1984x2192 / 123x137; 1920x1080 -> 1920x1088 / 120x68; already-aligned 1984x2192 stays unchanged. Verify all visible edge pixels and both chroma planes, pool reuse, first frame initialization and crop on the real decoder in subsequent GPU tests.
  4. Alternate A/B/A/B with the same moving scene, refresh, HDR state, bitrate, warm-up and sample count. Compare p50/p95/p99 conversion and encode times, frame age, stalls/drops, frame bytes and cropped visible-image quality. Reject missing edge pixels, bars, scaling changes, colour contamination or gains inside run-to-run variance. Do not ship B without a repeatable win.

Validation passed using the supplied performance-probe/rust-env.ps1, CARGO_TARGET_DIR=D:\bp-build\av1pad-target and the serialized pwsh -NoProfile -File C:\src\cargo-one.ps1 wrapper:

October 9 Vibepollo fixture failure #

Read-only investigation and fixture repair; no host, receiver, GPU workload, service operation or build was run. Streaming validation must wait for the reboot described below. Artifacts are under C:\Users\ramaz\.codex\artifacts\butterpollo-rust-20260930\bench-rc21.

The zero-video failure is a missing WGC helper in the pinned benchmark bundle. The frz-wgc-* label makes the fixture write capture = wgc for both hosts. Vibepollo needs vibepollo-baseline-build\tools\sunshine_wgc_capture.exe, which is absent; that directory contains only the display helper executable and CMake files. The October 8 comparison used capture = ddx. The current sunshine.exe SHA-256 is a539dd0f4d5fdee4537b317efee07e96b201a0c229e050a6af8a18225edb46cf, identical to vp-vibepollo-nat-hevc-a/matched-settings.json.

The four attempts failed at different stages. Paths in this list are relative to frz-wgc-vibepollo-rN; host logs are under config/logs/logs:

The r4 first secondary starts at 01:47:44.489 (second-clients.json), after the first missing-helper error. Its second-0/result.log:6 returns 503 while the primary disconnects and tears down (host log:321,346-372). Vibepollo's src/nvhttp.cpp:6351-6364 admits only one blocking stream mutation at a time; this response does not diagnose a driver lock. Later secondaries also receive zero video and hit the same missing helper. The final motion_probe.exe timeouts in the run-level r2/r3/r4 logs are fixture teardown failures after these earlier errors, not the original streaming failure.

The logs do not support stopping the installed Butterpollo service. r4 creates its own display and selects it for capture after the temporary-lease warning. More decisively, the successful October 8 vp-vibepollo-nat-hevc-a/config/logs/logs/baseline-20261008-164312-585.log:66 has the identical warning; its result.log:56,67 reports 3,817 decoded frames and INTEROPERABILITY PASS. In the pinned source at day-work-20261002/vibepollo-baseline-source, src/platform/windows/virtual_display_sunshine.cpp:8257-8267 can return an existing physical display without acquiring a temporary probe lease; src/main.cpp:839-841 still emits this warning when there is no owned lease. It then validates encoders with synthetic surfaces. This is not evidence of exclusive driver ownership by rc.26. Likewise, the unrecognized wgc_slot_aligned_publish, gpu_compute_conversion and prep_cmd settings appear in both successful and failed stdout logs (October 8:49-51, r4:48-50). Omitting minimum_fps_target = 20 retains Vibepollo's default 20 (src/config.cpp:969).

Edited only the external run-motion-ab.py, preserving its original bytes as run-motion-ab.py.bak (SHA-256 3e39523eb6adec8db538e2755292119bda67ccf2c712c589600ae939676eafe7):

interop_vp_ab.py already contains the separate CSRF-token fetch, 60-second launch timeout and no-cancel handling; these were retained. Neither it nor run-one-ab.ps1, batch-ab.ps1, run-motion.py or the installed service was modified. CPU-only checks passed: Python syntax, PowerShell wrapper parsing, the backup hash, eight old/new Butterpollo config comparisons, unchanged Butterpollo secondary commands/environment/timing, Vibepollo capture and secondary isolation, helper durations, and the 15-column batch row below. No streaming success or new performance result is claimed before the reboot.

After reboot, with the installed host idle, use this fresh row in the RunsFile supplied to batch-ab.ps1 through the existing SYSTEM/session launcher (HEVC SDR 1080p60, 20 Mbps, 120 Hz source, D3D11VA, three secondary cycles, 60-second receiver and 70-second renderer):

frz-ddx-vibepollo-r5|vibepollo-baseline-build|hevc|1920x1080|60|20000|120|0||d3d11va|3|60|70||vibepollo

This is explicitly a DDX baseline. A WGC comparison needs the matching baseline WGC helper packaged and capture selection restored. Check actual source refresh and secondary display identities in the new artifacts before using the result as a matched hotplug comparison: Vibepollo can join another game client's existing output (src/remote_session.cpp:263-268), so per_client and three connections alone do not prove three new displays.

October 9 AMF settings review #

Source audit of 59bcc477 plus the two fixes below, for the RX 7900 XT (RDNA3, two VCN instances). No encoder workload, quality probe, ignored test or stream was run, and the installed service was untouched. The GPU still needs a reboot; the workspace test run encountered an additional non-ignored GPU-discovery test, documented under verification below. The target is 1968x2184 HDR 120 fps in HEVC/AV1, plus 1080p60 and 1440p120. Performance numbers in this section are earlier measurements, not new results.

The build takes its AMF 1.5.2.0 headers from C:\Users\ramaz\git\butterpollo-pw-build\build\_deps\ffmpeg-v2026.516.30821\ffmpeg\include\AMF. The three encoder headers and ColorSpace.h match upstream tag v1.5.2 (eae4a4b7efc35f8b0a3977a0984c0d642efc4e63) after normalizing line endings. The tables below use those exact contracts: H.264, HEVC, AV1, and colour definitions. SDK availability does not prove that this GPU/driver implements a property.

Findings and scope of the fixes #

AMD guidance and the measured baseline #

AMD distinguishes interactive game streaming from broadcasting gameplay. Its game-streaming recommendations include no B-frames, an infinite GOP, intra refresh, AQ, CBR and a 0.1-second VBV. At 120 fps that VBV is twelve frame budgets. Butterpollo leaves VBV driver-derived by default; its explicit buffer settings request 0.5-2 frame budgets. It agrees on no reordering and on-demand keyframes, but enables intra refresh only when negotiated and uses latency-constrained VBR. Those are deliberate streaming tradeoffs. AMD recommended settings.

The usage preset is applied first, then the explicit settings override it. Consequently amd_usage=high_quality alone still gets the host's speed preset, latency VBR and PA-off policy unless those are also changed. AMD identifies ultra-low latency and speed as appropriate to interactive use; PA/TAQ can override VBAQ, and adaptive mini-GOP can override a B-picture pattern. Changing usage therefore requires checking effective settings and the resulting bitstream, not just the dropdown label. AMD tuning and priorities.

Earlier evidence in AMF settings per codec: quality at a given bitrate below: speed to quality cost about 0.7 ms HEVC, 6.1 ms AV1 and 1.9 ms H.264 mean encode time in the SDR sweep, without consistent VMAF gain; low-latency usage cost 6-8 ms. CBR spent more of the target budget and enlarged p99 frames by 30-67% in the SDR clips and 10-42% in HDR. Turning H.264 VBAQ off improved VMAF by up to 9.42 at 20 Mbps and is already the default. The later 48 HDR encodes found no consistent reason to change HEVC/AV1 AQ, balanced preset or CBR; AQ-off ranged -0.30 to +0.90 VMAF for HEVC and -0.03 to +0.31 for AV1. Those HDR clips were SDR sources converted to PQ, not native HDR game scenes.

Two VCNs do not establish two-engine acceleration of this session. The October 7 split-frame sweep found no consistent gain from on/off/untouched across 720 runs, including sizes up to 7680x2160; readback was already on. Treat a split as unproven until timing or engine activity shows it. AMD's SAV description covers both multi-VCN GPUs and multi-device systems; SAV and the per-codec split-frame property are separate controls. AMD SAV primer.

Complete encoder-property inventory #

Sources: core/src/encoder_policy.rs::{amf,amf_rate_control,amf_split_frame}, windows/src/amf.rs::{create,configure_ltr,prepare_surface,set_bitrate,write_hdr_metadata} and web/src/lib/schema/video.ts. Names below are literal AMF strings, not the SDK macro identifiers. Unless noted, properties are set before Init. auto omits a write; unset below means policy does not write it, even if AMF encoder settings reads it. Policy writes are read back: required failures abort creation, optional failures warn. Direct property calls generally check only SetProperty success, with the exceptions specified below.

Setting H.264 HEVC AV1 Value, SDK comparison and verdict
Usage Usage HevcUsage Av1Usage Default ultra-low = 1 / 1 / 2. Transcoding 0; low-latency 2 / 2 / 1; webcam 3; high-quality 4; low-latency-high-quality 5. Correct, required unless auto. Other usages may buffer frames.
Quality QualityPreset HevcQualityPreset Av1QualityPreset Speed 1 / 10 / 100; balanced 0 / 5 / 70; quality 2 / 0 / 30. Correct; default speed, required unless auto. SDK also has high-quality presets, which the console does not offer; no new option proposed.
Rate control RateControlMethod HevcRateControlMethod Av1RateControlMethod Default latency VBR 3 / 1 / 1; CBR 1 / 3 / 3; CQP 0; peak VBR 2; QVBR 4; HQVBR 5; HQCBR 6. Correct. Explicit choice is required; default rejection warns. HDR demotes modes 4-6 to peak VBR. CQP ignores the stream bitrate budget.
QVBR target QvbrQualityLevel HevcQvbrQualityLevel Av1QvbrQualityLevel Only QVBR: explicit 1-51, zero leaves default (SDK 23). This is not AV1's 1-255 QIndex. Names/range correct. The headers do not substantiate the console's low/high quality endpoint labels.
Adaptive quantization EnableVBAQ HevcEnableVBAQ Av1AQMode Default false / true / 1 (CAQ); AV1 0 = none. Disabled for explicit CQP; auto omitted. Correct codec distinction, though the common console label says VBAQ for AV1 too. AQ may redistribute distortion rather than improve PSNR.
Pre-analysis EnablePreAnalysis HevcEnablePreAnalysis Av1EnablePreAnalysis False by default and for HDR; true on SDR when requested or RC >=4. Required when true. Correct names. The AVC/HEVC header comment saying peak VBR only is stale relative to AMD's documented PA-dependent quality RC modes.
PA lookahead PALookAheadBufferDepth PALookAheadBufferDepth PALookAheadBufferDepth Shared name, 1 only when PA is enabled, required. Adds analysis/buffering risk. No codec prefix belongs here.
HRD EnforceHRD HevcEnforceHRD Av1EnforceHRD False by default, configurable bool; explicit setting required. Correct. Off permits looser buffering; on may raise QP. It is not a network packet pacer.
RC frame skipping RateControlSkipFrameEnable HevcRateControlSkipFrameEnable Av1RateControlSkipFrameEnable Writes false optionally for AVC/HEVC. AV1 unset, now logged with the correct SDK name. Confirm actual value/support; do not infer it from the old log.
Input queue InputQueueSize HevcInputQueueSize Av1InputQueueSize amd_input_queue_size=0 writes nothing for all clients; positive requests are clamped to 1-32 and required. SDK default 16 is capacity, not a mandatory 16-frame delay. The VRR claim in console help is stale.
Query timeout QueryTimeout HevcQueryTimeout Av1QueryTimeout 1 ms, optional direct write; SDK default 0 = no wait. Correct units. QueryOutput may return earlier on output; this does not deliberately delay every frame by 1 ms. A pending query can still occupy the capture/encode thread until timeout.
Internal latency LowLatencyInternal LowLatencyInternal — Shared unprefixed bool for AVC/HEVC, explicit only, required. Correct exception to prefixing. AVC also selects POC mode 2. Under ultra-low usage the driver commonly already enables it; forcing on can be redundant.
AV1 latency — — Av1EncodingLatencyMode Auto omitted; none 0, power-saving 1, realtime 2, lowest 3. Correct. SDK targets respectively no deadline, 1/fps, 1/(2*fps), fastest possible; these are effort/power modes, not guaranteed timing.
SmartAccess Video EnableEncoderSmartAccessVideo HevcEnableEncoderSmartAccessVideo Av1EnableEncoderSmartAccessVideo Auto omitted; explicit bool, required only when enabling. Guard forces SAV off (required) when AVC/HEVC explicitly requests internal low latency. It does not inspect the usage-derived effective latency value; keep this driver-interaction question separate from a proven reset cause.
Split frame — HevcMultiHwInstanceEncode Av1MultiHwInstanceEncode Optional bool after other policy: only if HevcNumOfHwInstances / Av1CapNumOfHwInstances >1; auto asks only if not already on. Correct names; accepted hint is not proof of a split. On this host normally redundant.
High motion HighMotionQualityBoostEnable HevcHighMotionQualityBoostEnable Av1HighMotionQualityBoost Explicit bool only, optional. Correct AV1 exception. Existing sweep cost 0.2-1.1 ms without consistent quality gain.
Profile Profile HevcProfile — AVC 100 (High) always; HEVC 2 (Main10) for ten-bit, otherwise driver Main. AV1 profile left to driver (Main =1). Correct; AVC ten-bit/HDR and all AMF 4:4:4 are rejected. No level/tier override is made.
AVC entropy coding CABACEnable — — Auto omitted; CABAC 1, CAVLC 2. Correct enum despite the Enable name; do not replace with a bool. High profile's automatic coding is CABAC in the SDK.
Screen-content tools — — Av1ScreenContentTools Explicit bool only, required. SDK defaults true and gates palette/integer-MV tools; turning this on alone need not change anything. Av1PaletteMode and Av1ForceIntegerMv are not written.
Slices/tiles SlicesPerFrame HevcSlicesPerFrame Av1NumTilesPerFrame AVC/HEVC write negotiated count only if >1. AV1 explicit 1/2/4, or negotiated slices capped at 4 if >1; zero/one automatic leaves driver alone. Correct: tiles are a suggestion, and readback accepts any positive result. Extra partitions may cost coding efficiency; no slice/tile output mode is enabled.
References MaxNumRefFrames HevcMaxNumRefFrames Av1MaxNumRefFrames Negotiated positive count is required; zero leaves driver default. AVC intra refresh requires at least two; a one-reference client falls back to IDR. Correct budget handling; more slots are not evidence that the encoder actually uses extra predictors.
B-frame pattern BPicturesPattern No property Av1BPicturesPattern Optional zero for AVC/AV1 only after this fix. AV1 property is VCN5-only per header and may be absent here; HEVC had an invalid prefixed write. No B-reference, maximum-B, or adaptive-mini-GOP setting is written.
Size FrameSize HevcFrameSize Av1FrameSize Negotiated width/height as AMFSize, required. Correct.
Rate FrameRate HevcFrameRate Av1FrameRate fps_millihz()/1000 as AMFRate, required; preserves fractional refresh. Correct.
Target TargetBitrate HevcTargetBitrate Av1TargetBitrate Requested kbps *1000 bits/s, required initially; updated dynamically. Correct units. Failed live update warns and retains driver state.
Peak PeakBitrate HevcPeakBitrate Av1PeakBitrate Default unset; configured ratio 1-2 * target bits/s, required if requested. Not an individual-frame limit; its relevance depends on RC mode.
VBV VBVBufferSize HevcVBVBufferSize Av1VBVBufferSize Default unset; configured 0.5-2 * bits/frame, required if requested. Written after frame rate and target; bits, not bytes or milliseconds. Correct. Small buffers can hurt detail/scene changes.
Maximum AU/frame MaxAUSize HevcMaxAUSize Av1MaxCompressedFrameSize Default unset; configured 1-8 * bits/frame, required if requested. Correct names and bit units. Zero config means omit, not actively clear. HEVC header's default-60 comment disagrees with API guide's zero; read driver state instead of assuming either.
GOP/IDR IDRPeriod HevcGOPSize Av1GOPSize 0, AVC failure ignored, HEVC/AV1 required. No scheduled recovery IDRs; request on surfaces. HevcGOPSPerIDR, AVC IntraPeriod, Av1IntraPeriod and insertion cadence stay unwritten. Finite-GOP stalls are a retained host workaround, not an AMD guarantee for every driver.
Intra refresh IntraRefreshMBsNumberPerSlot HevcIntraRefreshCTBsNumberPerSlot Av1IntraRefreshMode, Av1IntraRefreshNumOfStripes Only when negotiated: AVC 16x16 MBs, HEVC 64x64 CTBs; N=ceil(w/block)*ceil(h/block), request ceil(N/clamp(N,1,299)) per slot. AV1 continuous=2, 300 stripes. Correct enums/block units; approximately 300 pictures means ~2.5 s at 120 fps or 5 s at 60, not immediate full recovery. Small pictures/granularity change the AVC/HEVC duration.
AV1 alignment — — Av1AlignmentMode 3 (no restrictions) on public constructors, required. Correct enum, but this driver still pads 1968x2184 to 1984x2186 without render-size signalling; already measured below. Changing alignment is not a proven fix.

LTR uses additional component and surface properties; all names and the reset-unused enum agree with the SDK. amd_ltr_frames=0 leaves them alone. A positive request (maximum four) is limited to negotiated references minus one rolling reference, to property limits and, for AV1, Av1CapMaxNumLTRFrames. Intra refresh disables LTR. Failed setup restores the maximum to zero and falls back to IDRs. This is loss recovery, not a free steady-state quality gain.

Purpose H.264 HEVC AV1 Value
LTR capacity MaxOfLTRFrames HevcMaxOfLTRFrames Av1MaxNumLTRFrames Limited positive count before Init, read back.
LTR lifetime LTRMode HevcLTRMode Av1LTRMode 0 = reset unused; core/src/ltr.rs drops other anchors when recovering, matching the driver rule.
Mark surface MarkCurrentWithLTRIndex HevcMarkCurrentWithLTRIndex Av1MarkCurrentWithLTRIndex Slot index on keyframe/selected every-fourth frames when LTR is active.
Recover surface ForceLTRReferenceBitfield HevcForceLTRReferenceBitfield Av1ForceLTRReferenceBitfield 1 << slot, not the slot number. Correct. Surface failure disables LTR and requests IDR.
Force recovery frame ForcePictureType HevcForcePictureType Av1ForceFrameType 2 = IDR / 2 = IDR / 1 = KEY on requested keyframes. Correct.
Repeat headers InsertSPS, InsertPPS HevcInsertHeader Av1ForceInsertSequenceHeader True on those keyframes so a reset decoder can start independently. Correct.

Not written: pre-encode (RateControlPreanalysisEnable, HevcRateControlPreAnalysisEnable, Av1RateControlPreEncode), filler (FillerDataEnable, HevcFillerDataEnable, Av1FillerData), and initial fullness (InitialVBVBufferFullness, HevcInitialVBVBufferFullness, Av1InitialVBVBufferFullness; SDK scale 0-64). The first two groups are logged only. PA-off does not explicitly switch pre-encode off. Filler can consume wire budget without picture detail, particularly under a CBR/usage change; confirm effective values before attributing an actual-bitrate gain to quality.

QP limits are also unset, not clamped by the host: AVC MinQP/MaxQP and QPI/QPP/QPB (0-51); HEVC HevcMinQP_I, HevcMaxQP_I, HevcMinQP_P, HevcMaxQP_P, HevcQP_I, HevcQP_P (QP scale 0-51); AV1 Av1MinQIndex_Intra, Av1MaxQIndex_Intra, Av1MinQIndex_Inter, Av1MaxQIndex_Inter, Av1MinQIndex_Inter_B, Av1MaxQIndex_Inter_B, Av1QIndex_Intra, Av1QIndex_Inter, Av1QIndex_Inter_B (SDK 1-255). Do not reuse an AVC QP number as an AV1 QIndex. These and colour/GOP details are not all in the settings log; an omitted field is not evidence of a default.

Live bitrate changes also scale positive read-back peak/VBV/AU values by the new/old bitrate ratio, including driver-derived values, clamped to property limits. Partial update failure leaves mixed driver state and a warning. Do rate-control comparisons on fresh sessions at a fixed bitrate.

The session admits two pending pictures; AMF's native ownership limit is eight. Neither equals the driver's input-queue capacity. poll queries only with work in flight and returns one result promptly. AMD recommends separate submission/output threads for overlap; this host uses a combined capture/encode loop with separate sending. A 1 ms query can therefore affect claim timing, but a threading rewrite is outside this review. AMD asynchronous application guide.

PA remains a low-priority experiment. AMD documents NV12 analysis and warns that PA adaptive quantization can supersede VBAQ. The code gates PA and quality RC on hdr, not ten_bit(), so optional ten-bit SDR can still request PA with P010; this needs a format-support check before using that combination. Auto-LTR/TAQ/adaptive mini-GOP are not explicitly controlled by this host. Keep PA off for the HDR baseline; no broader policy change is justified here. AMD PA guide. The HEVC API guide's rate-control section explicitly requires PA for QVBR/HQVBR/HQCBR, explaining the discrepancy with the older peak-VBR-only header comment; the same guide specifies MaxAUSize in bits with default zero.

HDR and colour from capture to Moonlight #

Negotiated::color_matrix() forces BT.2020 for HDR; ten_bit() selects P010 and full_range() takes bit zero of negotiated csc_mode. The D3D11 and D3D12 paths use the same gpu_color::constants and shaders/color.hlsl: scRGB FP16 (Rec.709 primaries, 1.0 = 80 nits) is converted to Rec.2020 and absolute ST.2084 PQ before 4:2:0 subsampling. An already-PQ Rgba10Pq source is not PQ-encoded twice. Limited P010 uses Y 64-940 and nominal chroma 64-960; full range uses 0-1023, with neutral chroma 512, stored in the high ten bits. CPU fallback in encoder.rs::Convert/color.rs uses the matching matrix/range and PQ conversion. Input AMF colour properties describe these already-converted NV12/P010 surfaces, not the original scRGB texture.

Property purpose H.264 HEVC AV1 Write and assessment
Bit depth No ten-bit write; rejected HevcColorBitDepth Av1ColorBitDepth 10 for HDR or ten-bit SDR; otherwise SDK default 8. Correct; HEVC also sets Main10.
Input profile InColorProfile HevcInColorProfile Av1InputColorProfile AMF profiles limited 601/709/2020 = 0/1/2, full = 3/7/8. Correct. Optional input description is largely redundant with our own conversion.
Output profile OutColorProfile HevcOutColorProfile Av1OutputColorProfile Same values; HDR uses 2 or 8. Correct names, including AV1's Input/Output rather than In/Out. Failed output writes abort HDR creation; SDR warns.
Input transfer InColorTransferChar HevcInColorTransferChar Av1InputColorTransferChar HDR 16 (PQ); SDR 601/709/2020 = 6/1/14. Input failure only debug-logged.
Output transfer OutColorTransferChar HevcOutColorTransferChar Av1OutputColorTransferChar Same values, correct per ColorSpace.h; output failure handled as above.
Input primaries InColorPrimaries HevcInColorPrimaries Av1InputColorPrimaries 601/709/2020 = 6/1/9; HDR always 9. Correct; input optional.
Output primaries OutColorPrimaries HevcOutColorPrimaries Av1OutputColorPrimaries Same values; HDR BT.2020. Correct, required for HDR.
Input range InputFullRangeColor HevcInputFullRangeColor Av1InputFullRangeColor Negotiated bool. Correct names/type; failures tolerated because some drivers reject the input hint.
Output range FullRangeColor HevcNominalRange Av1NominalRange Same bool. These literal names remain correct even though old macro aliases are deprecated. HEVC explicitly permits bool (legacy integer 0/1 also works); no enum/type fix needed. Failure is only a warning for full range, so verify actual bitstream range.
Matrix coefficient InMatrixCoeff, OutMatrixCoeff HevcInMatrixCoeff, HevcOutMatrixCoeff Av1InMatrixCoeff, Av1OutMatrixCoeff Unset. The driver derives matrix signalling from profile; expected 601/709/2020-NCL = 6/1/9, distinct from AMF profile numbers. The existing hardware test asserts BT.2020-NCL; no unconditional new property required without driver testing.
Static HDR payload None sent HevcInHDRMetadata Av1InHDRMetadata AMFBuffer interface containing AMFHDRMetadata, after Init and before first input; updated when changed. AV1 still uses InHDRMetadata, not InputHDRMetadata. HEVC's OutHDRMetadata is commented out in the SDK. Correct names.

capture.rs::Device::hdr_metadata() reads the selected output's DXGI maximum/minimum/full-frame luminance, with bounded finite conversion in core/src/hdr.rs. Primaries are synthesized as Rec.2020 and D65, not copied from DXGI panel primaries or a game's mastering metadata. Fallback is 1000 nits peak, 0.0001 nit minimum, unknown full-frame luminance. The live path leaves MaxCLL and MaxFALL at zero (unknown); the quality probe and tests can supply nonzero values. Display full-frame luminance is not content MaxFALL and is correctly kept separate. This is valid synthetic desktop metadata, not preservation of original HDR10/HDR10+ game metadata.

The same Metadata value is stored in the session and sent to the encoder before the first frame (stream.rs startup). It is refreshed once per second; encoder recreation sets metadata_due to now, so the first recovery frame also receives metadata. set_hdr_metadata skips SDR/AVC and duplicate values. On failure it warns and caches the attempted value rather than retrying it every frame; control metadata can therefore be correct while bitstream metadata is absent. Even a successful SetProperty/log line does not prove that every subsequent keyframe carries the metadata. The AV1 header labels the input HDR property static, while this implementation uses it after Init; retain the known driver path and check an independent keyframe after updates on any new runtime before claiming portable dynamic support.

Metadata units are intentionally codec-specific:

Field Internal/control value HEVC AMFHDRMetadata / SEI AV1 AMFHDRMetadata / metadata OBU
RGB primaries, D65 white Integer x/y in 1/50000 Unchanged floor(value *65536/50000), capped at 65535: 0.16 fixed point
Mastering peak Whole nits (u16) nits *10000 (u32) nits *256 (u32): 24.8 fixed point
Mastering minimum 1/10000 nit (u16) Unchanged in 1/10000 nit floor(value *16384/10000): 18.14 fixed point
MaxCLL / MaxFALL Whole nits, zero unknown Unchanged Unchanged
Display full-frame peak Whole nits, zero unknown No AMFHDRMetadata member; control only Control only

The shared AMF header describes HEVC units for AMFHDRMetadata. The retained AV1 implementation documents that this driver copies those fields straight into the OBU, so the host pre-scales them to the AV1 mastering-display syntax. For 1015 nits, minimum 0.005 nit and red x=0.708, the existing ignored test expects HEVC 10150000/10000, 50/10000, 35400/50000; AV1 259840/256, 81/16384, 46399/65536. Using HEVC's peak scale on AV1 would signal 39,062.5 nits for a 1000-nit source. The small AV1 truncation is quantization, not a factor-of-10,000 bug. Hardware tests were not rerun here.

HEVC carries mastering-display and, when known, content-light metadata in prefix SEI. AV1 carries HDR_MDCV (type 2) and HDR_CLL (type 1) metadata OBUs. amf.rs::poll copies these encoder bytes unchanged for HEVC/AV1; the host passes them to VideoPacketizer::encode_recovery, which frames/FECs/encrypts the payload without filtering SEI/OBUs. H.264 is SDR-only in this backend, with colour signalling in its SPS VUI and no HDR buffer.

The retained Moonlight fixture (performance-probe/moonlight-vrr-common, commit d6a11bc685b41037b352a96f29d08276fe5359ba) explains the apparent gap at the receiver: VideoDepacketizer.c strips H.264/HEVC leading AUD/prefix-SEI NALs, including HEVC NAL type 39, before delivering decode units. It does not parse the AV1 bitstream that way. Thus a Moonlight HEVC decode-unit dump may lose the metadata that was present on the wire; a dump is not sufficient to accuse the host of omitting SEI. The client's renderer/decoder can use the separate control metadata; actual tone mapping is client-specific.

stream.rs sends encrypted reliable ENet control type 0x010e once the encoder is configured with the display's metadata, and again whenever the 27-byte metadata payload changes. Sunshine also sends it once. It used to go out as soon as an output was selected, with placeholder metadata, then again with the real values; Moonlight for Xbox sets the TV's HDMI mode on every message, so it switched twice (issue #11). Metadata::wire and Moonlight ControlStream.c agree exactly:

Payload offsets Field Representation
0 HDR enabled 0 or 1 from negotiated config.hdr, including 0 for SDR
1-12 R, G, B x/y Six little-endian u16, normalized to 50000
13-16 White x/y Two little-endian u16, normalized to 50000
17-18 Mastering peak Little-endian u16, whole nits
19-20 Mastering minimum Little-endian u16, 1/10000 nit
21-24 MaxCLL, MaxFALL Two little-endian u16, whole nits
25-26 Display full-frame peak Little-endian u16, whole nits

Moonlight saves those fields, updates HDR state and invokes setHdrMode; LiGetHdrMetadata exposes the copy. Decode units get hdrActive and Rec.2020 from that state (SDR uses negotiated colour space); the payload does not separately carry transfer/matrix/full-range flags. Bitstream signalling and negotiation still matter. tests/moonlight_client.c checks HDR notifications, metadata validity and decoded ten-bit/PQ/BT.2020 frames. The existing ignored hdr10_metadata_and_range_reach_the_bitstream checks encoder output directly, both codecs, both ranges, D3D11/D3D12 and independently decodable keyframes 0 and 4, including nonzero/unknown CLL. It is the post-reboot validation to use, not a test to execute while the GPU is down.

Ranked host A/B plan, after reboot #

Keep ultra-low usage, speed, latency VBR, PA off, the existing per-codec AQ default, automatic queue/latency/SAV/split-frame and negotiated references as the control. Use separate fresh sessions; change one setting at a time. These rankings are expected opportunities, not claims of measured gains:

Rank Concrete A/B values Expected benefit and required observations
1 amd_max_frame_size=0,4,2 (1 only if the others help) Best candidate for recovery/scene-change picture-age tails on a constrained link. In amf_quality compare IDR bytes, p99 P-frame bytes, actual bitrate and detail loss; in streaming request recovery and measure sender/FEC time, picture-age p95/p99, decode errors and held pictures. A cap may reduce quality or be ignored; readback alone is insufficient.
2 amd_ltr_frames=0,1,2, only with a client reference budget allowing it, intra refresh off Potentially large loss-recovery benefit, little expected idle gain. Probe steady-state quality/bytes/encode cost first; then controlled packet loss and repeat recovery on a separate client. Measure recovery-frame bytes, time to clean picture and picture-age p99. One-reference clients cannot test LTR; amf_quality does not simulate loss.
3 HEVC/AV1 amd_vbaq=enabled,disabled; H.264 retain disabled as control Cheap quality opportunity on native HDR gradients, particles, foliage and UI. Earlier synthetic HDR changes were small/mixed, max +0.90/+0.31 VMAF for HEVC/AV1. Compare PSNR/SSIM-style plane metrics, visual temporal stability and actual bytes, with no regression in encode p99 or picture age. Do not assume AV1 CAQ is identical to AVC VBAQ.
4 First amd_rc=vbr_latency,vbr_peak,cbr; then, separately with the selected RC, amd_peak_bitrate_ratio=0,1,1.5, amd_vbv_buffer_frames=0,2,1, amd_enforce_hrd=false,true Better use of bitrate or smaller bursts, with a quality tradeoff. Existing CBR gains mostly came from spending more bits and produced larger frames. Measure actual bitrate, scene-cut/IDR sizes, PSNR/SSIM/VMAF, encode p99 and on-wire picture-age tails; compare both equal requested and equal actual bitrate. Do not switch all four knobs together.
5 AV1 amd_av1_tiles=0,1,2,4; separately amd_av1_screen_content=auto,disabled,enabled Possible coding-efficiency or client decode improvement, especially if automatic tiles exceed one. Inspect effective tile count and screen-tool use; quality-probe bits/text edges and separate-client decode time/picture age. Extra tiles do not make this full-frame sender transmit early and did not unlock split-frame gain in earlier sweeps. AVC/HEVC slice 1/2/4 requires a negotiated test-client change; no host config knob exists.
6 amd_input_queue_size=0,4,2, then 1 only in an isolated run; separate source-only QueryTimeout=1 vs 0 Possible submission/claim-tail gain under game load; near-zero expected idle gain when backlog is already small. Measure INPUT_FULL/NEED_MORE_INPUT counts, claim wait, output gaps, CPU time, encode p99 and picture age. Stop at a stall. amf_quality submits one picture at a time and cannot establish a queue-depth win. QueryTimeout has no console setting; an experimental build is needed, not a new shipped flag.
7 amd_quality=speed,balanced,quality, starting at 1080p60/low bitrate; optionally amd_high_motion_quality_boost=auto,enabled,disabled in separate cells Low expected return at the owner's normal rates; quality already cost up to 6.1 ms in AV1. Only keep a slower choice if new native content shows a visible gain and encode/picture-age tails still meet 16.67 ms at 60 or 8.33 ms at 120 fps. This is a frame budget, not a guarantee that the full pipeline fits in one frame.
8 AVC/HEVC amd_lowlatency_mode=auto,enabled; AV1 amd_av1_latency_mode=auto,lowest,realtime; split auto,disabled,enabled Mostly redundancy checks: effective internal latency/AV1 lowest and split were already selected by the default usage/driver. Read settings first and skip pairs with identical effective state. Compare encode/picture-age p99 and power. Revisit split primarily after a driver change; two reported VCNs alone predict no gain.

SAV and PA/HQ are last, separate investigations, not recommended changes to the HDR baseline. Prior SAV/split sweeps were neutral. Do not combine explicit SAV-on with forced internal low latency; the existing guard documents past HEVC HDR resets. An 8-bit SDR-only PA comparison would use amd_rc=vbr_peak with amd_preanalysis=false,true, then QVBR at 18/23/28 or HQVBR separately. Inspect PA's effective tools and reference/reorder behavior. The current one-picture-at-a-time quality probe may time out if a mode needs future input; such a timeout does not measure quality or throughput. A pipelined experiment would be separate work. HDR requests for these quality RC modes currently become peak VBR, so they are not genuine HDR QVBR A/Bs.

For every feasible cell, use 1968x2184/120 at 80 Mbps (also 30 for a quality-stressed cell), 2560x1440/120 at 50 Mbps (also 20), and 1920x1080/60 at 20 Mbps (also 10). HEVC/AV1 get HDR and SDR; AVC gets SDR. At the normal rates one-frame budgets are respectively 666667, 416667 and 333333 bits, not bytes. E.g. native HDR VBV=1 requests about 83.3 kB and max-frame=2 requests about 166.7 kB before network/FEC overhead.

Use amf_quality's existing --codec, --width, --height, --fps, --bitrate (kbps), --frames, --config, --out and HDR --hdr 1 options only after reboot. It writes the bitstream and reports non-IDR encode mean/p95/p99, frame bytes mean/p99 and separate IDR byte sizes; it does not compute PSNR, SSIM or VMAF itself. Score a matched decode against the exact converted reference with the existing quality harness/FFmpeg. Preserve bit depth, matrix, range, chroma siting and native-size cropping in both inputs. Its SDR path requests CSC 0 (601 limited), while HDR uses CSC 4 (2020 limited); references must match. HDR input is planar linear gbrpf32le scaled to scRGB at 203-nit SDR white, so SDR clips merely converted to HDR do not test native highlights or wide-gamut content. Include native HDR material with known source/reference handling; default VMAF alone is not an HDR appearance metric. Score several clips and scene transitions, not only the original two upscaled 180-frame samples.

Then alternate A/B/B/A streaming runs, at least three per cell, with the same display/stream refresh, bitrate/FEC, client and game load. Record rendered-picture-to-decoded-picture age mean/p95/p99, new pictures/s, host encode/claim/send timing, IDR/recovery spikes, decoder time and errors. This fixture age excludes client scanout and input latency. Use a separate hardware-decoding client under game load: loopback decoder starvation was already measured here and can dwarf encoder differences. For native AV1, check the known 1984x2186 padding/crop behavior before comparing scores. Archive effective settings after Init and independently inspect HDR headers and keyframes; do not call a lower requested rate or absent metadata a quality/latency improvement.

Verification of this review's changes #

All cargo commands used C:\src\cargo-one.ps1, the supplied rust-env.ps1 and CARGO_TARGET_DIR=D:\bp-build\amfreview-target.

Tuning proposals, encoder/HDR bitstream tests and hardware validation remain deferred until the GPU is healthy. The requested single-skip command needs a post-reboot rerun before it can be called green.

October 9 job 7 split and stall diagnostics: host A/B, then a GPU reset #

Same setup as the job 4 A/B below (base 92afcce5, 1968x2184 HDR 120, 240 Hz virtual display, hardware-decoding receiver), October 9 02:32-02:49. Picture age mean / p95 / p99 ms:

Build HEVC rounds AV1 rounds
base 12.40 / 13.12 / 13.39, 12.41 / 13.16 / 13.42, 13.82 / 17.84 / 19.03, 12.42 / 13.24 / 13.44, 12.27 / 12.92 / 13.36 11.90 / 12.52 / 12.80, 12.08 / 12.74 / 13.36
job 7 (display/input split) 12.31 / 12.92 / 13.33, 12.37 / 13.16 / 13.39 11.90 / 12.53 / 13.00, 12.02 / 12.66 / 13.15
stall diagnostics 13.46 / 17.72 / 18.36, 12.38 / 13.14 / 13.40, 12.38 / 13.18 / 13.39, 12.36 / 13.05 / 13.34 13.12 / 17.48 / 17.84, 12.01 / 12.63 / 13.20

Both neutral: the 17-18 ms p95 tails appear in single runs of both the base and the stall build (a phase the fixture sometimes locks into), not systematically. Host mean was 3.6-3.7 ms (HEVC) and 3.2-3.3 ms (AV1) in every run. Shipped: job 7 as 66547532 and 68273f43, the stall injection, tests and stall-time logging as c35bcd28.

GPU reset: 25 s into the fifth base run (02:47:11 and again 02:48:03 local) the Radeon driver timed out (C:\Windows\LiveKernelReports\WATCHDOG\ WATCHDOG-20261009-0247.dmp and -0248.dmp; the AMD Bug Report Tool opened). The host log shows audio late reads of up to 0.8 s at that moment, and the next run's host could not open an encoder within 60 s. The driver did not recover: the RX 7900 XT shows Code 31 and both virtual display adapters Code 43, Windows runs on the basic display, and every build's --diagnostics fails with 0x80070057. Over 60 identical runs earlier that night had no reset; the previous watchdog dumps on this PC are from October 8 00:31-00:43. Cause unknown: the base build predates every change measured here. It needs a reboot before any GPU work; the dumps are kept for analysis.

October 9 a new virtual display hitches other streams #

Owner's observation: RX 7900 XT, Windows 11, extended layout, one client rendering and streaming on its own virtual display. Creating another client's display raises the first stream's render-to-decode picture age to 500-600 ms for about 1.3 s. Removing the second display does not hitch. Windows also reactivates a deliberately inactive physical TV during creation. This is the reported measurement, not a measurement from this worktree.

Source audit: 59caf6a0. Plan only; no display code changed. The logs do not establish how many mode/layout applies actually happened, and the existing code already skips satisfied settings. Combining the remaining operations crosses identity checks, HDR settling and arrangement recovery bookkeeping; it is not a clearly safe, contained change without Windows transition tests. Single-stream and multi-stream behavior therefore remain unchanged.

Call trace and scope #

Entry: host/src/display_session.rs::Prepared::create captures the original layout, calls Guard::new_virtual_options, sets virtual DPI, acquires an arrangement lease, then calls Guard::apply_virtual_mode("after layout"). GoldenLease records a restoration snapshot; it does not apply one at launch. The physical Activation::acquire branch is skipped for an owned VDD.

All SetDisplayConfig calls below submit the desired whole active path set, even when the code changes just one source/target. They can disturb DWM/capture on the first display. This is a plausible source of the hitch, not proof that every call pauses every display or that the driver hotplug itself is innocent. A device-scoped request likewise is not a guarantee of uninterrupted composition on other outputs.

In the table, A = SDC_APPLY, U = SDC_USE_SUPPLIED_DISPLAY_CONFIG, C = SDC_ALLOW_CHANGES. Counts are display-changing native call attempts, including fallbacks, per invocation of the indicated step. Read-only queries are not included in apply totals.

Order / source Native operation and flags Scope and count
Before arrival: Snapshot::capture, hotplug::Protection::capture GetDisplayConfigBufferSizes / QueryDisplayConfig (QDC_ONLY_ACTIVE_PATHS or QDC_ALL_PATHS); device-name, color, DPI and current-mode queries Read only; 0 applies. ALL_PATHS includes inactive alternative routes to active targets; these are not dormant monitors.
display_lease -> VirtualDisplay::create_options Driver version query DeviceIoControl(0x900); one create request (0x901 for protocol 3.5, 0x90c for 3.6+) carrying requested dimensions/rate 1 driver creation, separate from Win32 configuration counts. Windows/driver arrival and saved-topology recall can reconfigure the desktop before our first apply. Internal commit count is unknown. Shared identity reuse creates no new display.
VirtualDisplay::resolve -> activate_target, only if Windows leaves the new target inactive `SetDisplayConfig(A U
check_hotplug("created") -> Protection::check Remove reactivated dormant targets, compact referenced modes, `SetDisplayConfig(A U); on failure retry A
Guard::new_virtual_options -> lease_hdr -> set_hdr -> color_state::set DisplayConfigSetDeviceInfo(SET_HDR_STATE=16) on supported Windows 11 API, otherwise legacy SET_ADVANCED_COLOR_STATE 0-1 HDR write, addressed to the new adapter/target. Already satisfied or unsupported enable requests are skipped. Successful writes are polled up to three seconds; a timed-out transition can issue 1 additional rollback write and fail setup. Modern SET failure does not also invoke legacy SET.
finish_hotplug("after settings") -> Protection::settle Repeated Protection::check, same strict/prune/fallback sequence as above Normally 0 applies, despite polling: 500 ms quiet period, 50 ms sleeps, 1500 ms enforcement deadline. Each additional reactivation episode adds 1-2 applies. Sleeps delay this launch; they are not themselves DWM modesets or a demonstrated cause of the other stream's freeze.
End of that same finish_hotplug -> VirtualDisplay::apply_mode("after settings") -> Topology::set_mode_rate If current dimensions/rate differ, edit new source width/height and path refresh, invalidate its target timing index, then Topology::restore -> `SetDisplayConfig(A U
Mode-list fallback inside that set_mode_rate If CCD succeeded but readback still mismatches, ChangeDisplaySettingsExW(new_display, ..., flags=0) with `DM_PELSWIDTH DM_PELSHEIGHT
virtual_scale -> set_dpi_scale DisplayConfigSetDeviceInfo(type=-4), adapter/source 0-1 DPI write for the new source; default configuration 0 and matching scale skip it. Separate from CCD topology applies.
display_arrangement::Lease::acquire, existing owner -> apply -> Arrangement::compose_all Capture current layout, inherit the first stream's arrangement, journal desired layout; Topology::set_active only if active ID sets differ For the stated extended case after successful TV pruning: 0 active-set applies. In other layouts/state changes: usually 1-2 `SetDisplayConfig(A
Same arrangement apply -> Topology::set_positions -> restore Place subsequent streams to the right, aligned with the first; `SetDisplayConfig(A U
Guard::apply_virtual_mode("after layout") Same conditional CCD mode set and GDI fallback as the earlier mode pass 0-1 CCD + 0-1 GDI. Needed only if Windows changed/refused the requested mode; commonly just a readback after the earlier mode pass.
Verification / optional hdr_profile::Lease::acquire Mode/color queries; optional ColorProfileAddDisplayAssociation for a selected differing ICC profile Queries: 0 applies. Profile: normally 0-1 association write to new adapter/source, separate from HDR SET and CCD; failure can restore the prior association.

The two INFO messages virtual display mode applied are emitted by readback in VirtualDisplay::apply_mode, including when set_mode_rate took its no-op return. They are not evidence of two native mode changes. The inactive-target warning is before the strict apply (and its last deadline check); it does not report whether the strict call succeeded or needed the permissive fallback. The excerpt alone cannot establish a measured count.

Both mode passes target the new VDD, not the first stream's display. The arrangement records a restoration snapshot but does not restore every monitor's pre-arrival timing during this second launch. If arrival or an ALLOW_CHANGES fallback retimes the first display, the new display's final mode check does not repair it; verify the first display separately.

There is no SDC_SAVE_TO_DATABASE, SDC_NO_OPTIMIZATION, SDC_FORCE_MODE_ENUMERATION, SDC_TOPOLOGY_*, CDS_UPDATEREGISTRY, CDS_NORESET or CDS_RESET on this launch path. apply_mode with CDS_FULLSCREEN exists for the separate public integer-Hz set_mode helper; the virtual launch uses set_mode_rate, whose GDI fallback has flags 0. Snapshot rotation/clone restoration and restoration of every saved monitor are teardown/recovery operations, not an unconditional second-client launch step. If arrival actually disables the first VDD, its independent heartbeat can run activation, HDR/mode and arrangement recovery too; those extra calls must be counted separately when recovery messages occur.

Counts per second-display creation #

Assume one automatic successful VDD activation, one TV reactivation at created, no later reactivation, an extended arrangement with unchanged active set, and no error unwind or concurrent recovery. Let M1 and M2 be 0/1 for the two actual CCD mode changes, P be 0/1 for a position change, and F be 0/1 for the TV restore's permissive retry:

SetDisplayConfig attempts = 1 + F + M1 + P + M2.

Removal is asymmetric: it has no create-time Windows topology recall, startup guard or new-display HDR/mode setup. It can still change the topology: Lease::drop may deactivate the departing target with set_active, then VirtualDisplay::drop sends removal IOCTL 0x902 and lease release 0x904. No measured hitch on removal does not imply no global call there.

Minimum-change proposal and why it is deferred #

Two corrections to the suggested one-call approach are essential:

The first candidate should be restricted to a new, independently sourced owned VDD joining an existing extended stream arrangement:

  1. Retain creation/resolve and the immediate strict dormant-TV correction. Do not leave the TV on through a possibly three-second HDR transition. Keep identity checks, mode compaction and the bounded startup guard.
  2. Keep required HDR/DPI setup and settling. For this joining-display case only, defer the after settings mode write to arrangement. Keep the first/single-stream, reused-display, exclusive/primary/isolated and recovery paths as they are.
  3. From one fresh active topology, model the new display at its requested size/rate before composing its final position. Plan the mode and position together, retain every unrelated source/target mode, path order and clone relationship, discard unreferenced modes and remap indices. Reject a shared/clone source rather than editing another display through it. If the modeled result already matches, issue no call. Otherwise apply that full surviving configuration once with A|U.
  4. Journal the same intended arrangement before mutation; recheck ownership and topology immediately before apply and verify afterwards. A refused strict plan needs a fresh query before using the existing fallback sequence, never a replay of a snapshot taken before HDR/hotplug. Keep readback and a conditional rate correction if the driver does not honor the requested timing; do not drop the final verification just to save a log line. ALLOW_CHANGES remains an explicit compatibility fallback, whose potential to retime the first display must be measured.

For the three-CCD example this aims at two: immediate TV prune, then combined new-display mode/position. It cannot remove Windows' own arrival reconfiguration or a necessary HDR transition. Folding the prune into the same final call could reach one when HDR is already satisfied and the final arrangement is known at created, but currently that information and its recovery journal belong to a later lease. Delaying protection or moving that lease into creation is a larger behavior/lifetime change, not a safe flag substitution. The one-CCD already-satisfied case has no redundant host apply to remove while still preserving the TV's inactive state.

Risks requiring host validation: known driver rejection of exact layouts during arrival (the current guard has a fallback for it); missing/invalid target timings when changing refresh; cloned/shared source IDs; renumbered GDI names after pruning; HDR recalling another topology; stale snapshots overwriting a concurrent user layout change; preserving recovery journals and lock ordering across DISPLAYS, SETTINGS and the arrangement lease. CCD has no atomic compare-and-set, so even a final requery cannot eliminate the last race. Blindly removing ALLOW_CHANGES from Topology::restore would also alter physical-display restoration and single-stream behavior.

Before implementing that candidate, add pure planner tests over synthetic paths/modes and arrangement nodes: all settings already satisfied -> no apply; TV reactivation alone -> one prune with other modes preserved; new mode plus position -> one apply; untouched first-stream rational timing, primary, negative coordinates and clone relationships; compaction of orphan modes with shared mode indices; owned/replaced/missing target identity; reject the new target sharing an existing source; changed snapshot -> replan; requested dimensions used for placement; unchanged single-stream and non-extended fallback selection. The existing display/hotplug.rs tests cover pruning, compaction, identities and stale-layout detection, but cannot prove DWM/driver latency or acceptance of a new combined apply. No unused planner or speculative runtime change was added here.

Apollo/Vibepollo comparison: this checkout has no src/ C++ host and no Apollo/Vibepollo display implementation under third-party/ (only moonlight-common-c and nanors). docs/butterpollo-cpp.md records the C++ source removal after rc.23. The Rust resolve comment credits Vibepollo's delayed inactive-target activation, but that is not a source audit of its current flags or call counts. No comparison numbers are invented.

Verification and owner's next measurement #

Validation used the requested performance-probe\rust-env.ps1, CARGO_TARGET_DIR=D:\bp-build\hitch-target and C:\src\cargo-one.ps1 for every cargo invocation:

The required unfiltered test gate is not green; neither the unrelated tests nor the machine-wide wrapper were changed. The wrapper returned exit code 0 even for the failed test run, so results above come from Cargo's test summary, not that wrapper status. Logs are in D:\bp-build\hitch-target\clippy.log, test.log and test-without-hardware.log.

No displays or installed service were changed and no streams were started. Ignored hardware fixtures stayed skipped. The requested default suite attempted the two unignored probes above; the follow-up explicitly excluded them. This commit has no runtime difference to A/B.

For the next host run, keep the first client's renderer, codec, HDR, rate, resolution and capture backend fixed; repeatedly join/leave the second client with the TV connected but inactive. Record first-stream picture-age maximum and p95/p99 in a short window around creation, longest interval with no new rendered/captured picture, and time to recover to baseline. Whole-run averages can hide the reported 1.3-second event. Compare creation with removal and, when convenient, a TV-disconnected control to separate arrival cost from the corrective prune. Test SDR/HDR separately and verify both displays' actual refresh/size/position/HDR plus the TV's inactive state.

Correlate arrival, the guard warning, both mode stages and layout with timestamped native call entry/exit, flags, return codes and before/after topologies (API tracing or temporary measurement instrumentation). The current INFO logs cannot supply this. A DWM/DxgKrnl trace can distinguish a composition/driver pause from capture reinitialization or downstream delay. After a candidate exists, A/B/A/B the same launch sequence and verify that actual CCD commits decrease and first-stream frame gaps/picture age improve without retiming the first display or re-enabling the TV. If only one strict prune already occurs, focus the experiment on driver arrival, HDR and capture recovery before implementing batching.

October 9 job 4 split and two latency wakes: host A/B #

A/B/A/B on the RX 7900 XT host, October 9 02:05-02:16, against their common base 92afcce5 (which predates the pacer fix, so some runs claim 121 fps on both sides): isolated extended virtual display at 240 Hz, motion probe at 240 Hz, 1968x2184 HDR 120 fps at 80 Mb/s, hardware-decoding receiver, 35 s per run. Picture age is mean / p95 / p99 in ms; detect is the host's capture detection mean / p95.

Case Build Picture age r1 Picture age r2 Host mean ms Detect ms
HEVC, DDX base 12.16 / 12.67 / 13.16 12.10 / 12.63 / 12.95 3.64, 3.51 0.49 / 0.82, 0.53 / 1.11
job 4 12.22 / 12.83 / 13.31 12.77 / 14.78 / 17.08 3.59, 3.45 0.55 / 1.12, 0.47 / 0.99
AV1, DDX base 11.86 / 12.50 / 12.80 12.39 / 13.43 / 16.69 3.22, 3.09 0.56 / 1.12, 0.45 / 0.86
job 4 11.85 / 12.46 / 12.70 12.40 / 13.36 / 16.70 3.22, 3.09 0.55 / 1.12, 0.43 / 0.74
HEVC, WGC base 12.89 / 13.65 / 16.90 12.86 / 13.51 / 15.16 3.43, 3.44 0.12 / 0.16, 0.12 / 0.16
wakes (dfc865c8) 12.88 / 13.56 / 15.24 12.89 / 13.56 / 16.94 3.43, 3.44 0.12 / 0.16, 0.12 / 0.16

October 9 pacer claims at the stream rate (bug hunt item 1): host A/B #

stream_policy.rs refilled pacing credit at 1.01x the stream rate, so a source faster than the stream (the default 2x virtual display, a 165 Hz game) was claimed at 121.2 fps on a 120 fps stream and 60.6 on 60. 3309053 (on main as 75414f78) refills at exactly 1.0 when the source interval is under 0.95 periods. A/B/A/B on the RX 7900 XT host, October 9 01:24-01:31, 0b1967d against 3309053: loopback with the hardware-decoding receiver, isolated extended virtual display at 2x the stream rate with the motion probe at 2x, DDX, HEVC, 35 s per run, minimum_fps_target unset.

Case Build Receiver steady fps Host fps Picture age mean / p95 / p99 ms claim_wait p95 ms Dropped
1968x2184 HDR 120, 80 Mb/s before r1 / r2 121.09 / 121.00 120.75 / 120.79 12.96 / 13.95 / 17.05, 12.94 / 13.90 / 17.12 1.04 / 0.99 0 / 0
after r1 / r2 120.02 / 120.00 119.72 / 119.79 13.07 / 13.72 / 17.14, 12.94 / 15.18 / 17.11 0.96 / 0.97 0 / 0
1080p60, 20 Mb/s before r1 / r2 60.52 / 60.30 60.32 / 59.97 7.95 / 12.10 / 19.76, 7.50 / 8.11 / 11.08 2.19 / 0.03 0 / 0
after r1 / r2 60.00 / 60.00 59.56 / 59.65 7.62 / 8.26 / 8.75, 10.54 / 11.11 / 11.49 0.02 / 1.76 0 / 0

Result: the stream now leaves at exactly the stream rate (120.0 and 60.0 instead of 121.0 and 60.3-60.5); picture age is unchanged within run-to-run noise (the 1080p60 runs differ by phase, in both directions). Shipped for the rate: a client showing 120 Hz received 1.2 extra frames a second, which it has to drop or queue (Moonlight's 121.0-121.2 received fps in the laptop runs above). Not measured: Moonlight's own dropped-frame count on a real client after the change; the loopback receiver decodes but does not present.

October 8 repeats past the frame cap (PyroWave, VRR): host A/B #

Measured on the RX 7900 XT host on October 9 (01:05-01:16), A/B/A/B, 5e16d1c (before; same Rust as 9cf57ce) against 88d1dec (after). Loopback with the independent receiver (decoder plus per-frame size in its timing CSV), not the laptop: an isolated extended virtual display at 200 Hz (2x the stream rate), 1920x1080 at 100 fps with VRR requested, minimum_fps_target unset, PyroWave at 400 Mb/s and AV1 at 50 Mb/s, motion probe at 90 Hz on the 200 Hz display (presents on a 5 ms grid, so 10 and 15 ms gaps alternate), 35 s per run, 3 s warm-up dropped. Sent fps is counted per one-second window from client arrival times.

Case Build Sent fps p50 / max New frames KB p5 / p50 Picture age p50 / p95 / p99 ms Worst gap ms
PyroWave, 90 Hz game before r1 144 / 151 54.8 / 216.6 15.52 / 18.59 / 19.51 13.5
before r2 150 / 151 63.9 / 130.4 15.06 / 17.09 / 17.93 13.1
after r1 90 / 91 340.2 / 494.2 14.87 / 17.94 / 18.56 18.6
after r2 90 / 91 377.5 / 494.2 14.67 / 16.90 / 17.63 15.9
PyroWave, still desktop before r1 / r2 97 / 100, 97 / 101 all 193 KB repeats - 20.1, 12.0
after r1 / r2 20 / 67, 20 / 82 all 193 KB repeats - 51.4, 51.3
AV1, 90 Hz game before r1 / r2 90 / 91 1.3 / 4.4 10.76 / 13.83 / 14.74, 10.26 / 12.70 / 13.39 17.6, 15.5
after r1 / r2 90 / 91 1.3 / 4.4 10.58 / 13.04 / 13.78, 10.47 / 12.92 / 13.64 15.8, 17.7

Result: the report reproduces on the old build and is fixed. Before, a 90 fps game on a 100 fps PyroWave VRR stream went out at 144-151 fps, because about 40% of the frames were repeats, and new frames were budgeted from the short gap after a repeat: median 130-217 KB against 494 KB after. After, exactly the game's 90 fps goes out, every frame new, at full size; picture age is the same or 0.3-0.7 ms lower. A still PyroWave desktop goes from 97 fps of repeats to the 20 fps minimum, so the worst gap there is now 51 ms by design (max 67 and 82 fps are the first second, before the picture settles). AV1 is unchanged within noise, as expected: it already had a 20 fps minimum and no repeats while the game ran. Not measured: a real client over Wi-Fi (the laptop), and a real game.

Report (a user, relayed by the owner; rc.25, PyroWave with VRR at 100 fps): motion windows at 105 to 136 fps. Saving a minimum frame rate of 30 brought motion to just under 100, a still screen toward 30 and a 34 ms worst gap. Latency-relevant code change for rc.27; nothing measured on a host yet.

Host A/B to run (idle host, never exclusive display mode): PyroWave, VRR, 100 fps to the laptop client with a renderer below the stream rate with uneven frame times, then a still desktop. Compare the commit before this change with this one: sent fps per one-second window (p50 and max), size of new frames (p5 and p50), picture age, and the worst gap on a still screen. Repeat once with AV1 for the counting change alone.

October 8 user report: exclusive layout ignored, picture freezes #

Report (relayed by the owner): with the virtual display layout on exclusive and Remote Monitor chosen on the client, the first physical display stayed on and primary; and sometimes the picture stops while the stream and its audio go on. Code changes only; nothing here has run on a host yet.

To confirm on the host (never in exclusive mode on the owner's PC): a stream on a virtual display while another device's virtual display arrives and leaves, checking that capture reopened follows any left the desktop error and the client keeps fresh pictures. Not addressed: a session thread stuck in a driver call (encoder terminate or init) would also freeze the picture with audio running; there is no watchdog for it yet.

Host check, October 9 01:36-01:41, build 9cf57ce (includes 2180d8f and b193b88), isolated host on the RX 7900 XT with the extended layout, WGC, HEVC 1920x1080 at 60 fps to the hardware-decoding receiver on a per-client virtual display at 120 Hz with the motion probe at 120 Hz, 60 s:

October 8 real-client picture age over Wi-Fi (rc.24, laptop) #

First render-to-decode measurement from a second machine instead of loopback. Host: RX 7900 XT, 2.0.0-rc.24, Desktop app on the Extended virtual display, motion_probe drawing a QPC barcode (240 Hz source on the 120 fps runs, 120 Hz on the 60 fps runs). Client: IdeaPad, Radeon 780M, driver 32.0.31035.1003, on Wi-Fi only (802.11ax, 5 GHz, 2402 Mbit/s link); host on Ethernet. Decoder: the independent rust/tests/moonlight_client.c built on the host from e5520ac0 (FFmpeg D3D11VA for HEVC and AV1, decoding on its own thread, reading back only a 640x8 barcode strip), launched by a separately paired test client ("lap-measure", Extended layout). Clock: the host pings the laptop every 200 ms over UDP; the 20% of pings with the lowest round trip give a linear host-to-laptop QPC map (1,682 pings, minimum RTT 1.16 ms so offset error is within about +-0.6 ms, fitted drift 17.5 ppm, fit residual at most 0.3 ms). One 30 s run per row, the first 3 s dropped, first decode of each picture only. HDR requested on every row; all decoded as 10-bit P010, BT.2020, PQ.

"Received" is when the client's decode thread starts on the picture (fully received, after its queue). "Decoded" adds the client's hardware decode plus the synchronous strip readback; Moonlight-qt reports 0.3-0.7 ms hardware decode on the same laptop, so a Moonlight user's picture age lies between the two columns.

Config (bitrate) Fresh fps Render to received avg / p95 / p99 Render to decoded avg / p95 / p99 Host processing avg / p95 Client decode avg
1968x2184@120 AV1 HDR (80 Mbps) 120.0 13.54 / 14.33 / 14.93 ms 18.76 / 19.61 / 20.28 ms 3.04 / 3.3 ms 5.21 ms
1968x2184@120 HEVC HDR (80 Mbps) 120.0 14.13 / 14.97 / 15.90 ms 19.32 / 20.35 / 21.35 ms 3.70 / 4.1 ms 5.19 ms
2560x1440@120 AV1 HDR (50 Mbps), repeat 1 120.0 13.34 / 14.15 / 16.12 ms 18.58 / 19.76 / 21.49 ms 2.77 / 3.0 ms 5.24 ms
2560x1440@120 AV1 HDR (50 Mbps), repeat 2 120.0 13.28 / 14.07 / 15.63 ms 18.10 / 18.92 / 20.91 ms 2.76 / 3.0 ms 4.82 ms
2560x1440@120 AV1 HDR (50 Mbps), first run (noisy) 119.5 14.78 / 21.87 / 34.85 ms 20.25 / 29.51 / 45.16 ms 2.74 / 3.0 ms 5.47 ms
2560x1440@120 HEVC HDR (50 Mbps) 120.0 13.67 / 14.69 / 19.87 ms 18.80 / 20.22 / 24.97 ms 3.11 / 3.2 ms 5.13 ms
1920x1080@60 AV1 HDR (20 Mbps) 60.6 12.91 / 15.05 / 17.89 ms 16.88 / 19.40 / 22.18 ms 2.16 / 2.5 ms 3.97 ms
1920x1080@60 HEVC HDR (20 Mbps) 60.5 12.99 / 14.15 / 16.62 ms 16.51 / 17.69 / 20.28 ms 2.39 / 2.7 ms 3.51 ms

Every run received all frames with zero decode failures; unique fresh pictures matched the stream rate (1440p AV1 skipped 17 source frames and had 183 intervals over 1.5 frame periods, which is its wider p95/p99). Two repeats at 15:42Z and 15:43Z (14 and 24 late intervals, no skipped source frames) measured 13.3 ms average and 14.1 ms p95 to received; the host side of the first run was clean, so its late frames came from Wi-Fi or the client. AV1 is 0.6 ms faster than HEVC to the client at native resolution, mostly host processing (3.0 vs 3.7 ms).

A first batch at 15:04Z (older client build) is discarded for 120 fps and AV1: the client decoded and read back whole frames on its receive thread (8-10 ms a frame), shed about 11-15% of frames and queued for seconds, and its FFmpeg chose libdav1d without D3D11VA for AV1. Its valid 1080p60 HEVC row (12.91 / 14.82 / 17.22 ms to received) agrees with the table.

Outside this table: a game load on the host and Vibepollo on the same fixture were measured on the same-PC loopback fixture (see "rc.24 against Vibepollo 2.0 on the same GPU"). Also out of scope here: Ethernet on the client, display scanout, and PyroWave (stock and fixture clients lack it on this laptop). Moonlight-qt averages on the same laptop and host (rc.21-rc.24): host processing 2.6-3.6 ms, network 1-3 ms, decode 0.3-0.7 ms. Raw files: laptop C:\Users\ramaz\bp-measure\phase2b, host clock pings and rendered-frame maps in the project files.

October 8 encoder stall recovery backs off (RX 9070 XT report) #

Report: RX 9070 XT host, HD 630 client on Streamlight, 4K60 HEVC 80 Mbps, rc.22: freeze, then the stream ends, with encoder_recovery "returned no frame for 100 ms". That rule dates from rc.19 (07a8a55e); rc.22 only moved the message onto the stream card. When two frames sit in AMF for 100 ms the encoder is recreated, and a recreated encoder that was silent for another 100 ms was recreated again, about ten create/destroy cycles a second, until the 5 s budget ended the session ("the encoder stopped returning frames"). A fresh encoder's first 4K keyframe on the 9070 XT's single VCN beside a game can outlast 100 ms, so the loop could never let it finish.

Change: the first stall still recreates after 100 ms; each further recreation without a frame doubles the wait (200, 400, 800 ms), and any frame out resets the stall timer even if the in-flight count did not drop. The 5 s budget is unchanged and streams without stalls take the same path. Follow-up: an automatic AMF "safe profile" after repeated stalls (low-latency usage, no SmartAccess Video, LTR or forced queue) was tried and removed: it raised latency for the rest of the session on an unproven RDNA4 link, and overrode the owner's settings. Compute conversion still moves to the graphics queue only after a second separate failure, as before.

Second follow-up, from the reporter's log: the last timings before the stall were healthy (59.4 fps, encode 4.6 ms mean, 5.4 ms p99, host max 6.1 ms), the GPU was near 99% with a game, and network_fec had fired. The stall was sudden, not a slow encoder, and Sunshine on the same PC only got choppy when the GPU was saturated. So a stall now waits 250 ms before the first recreation (then 500 ms, 1 s, 2 s), the budget is 20 s, and a frame too large for full FEC in Moonlight's four blocks keeps the parity that still fits instead of none (849-1019 shards; 900 shards at 20% now get 13%). wgc_stamp_future_frames 302/303 with WGC's stamp about 4 ms ahead is a capture clock offset the timings keep signed, not a cause. Not measured: the RX 9070 XT is not available, and the RX 7900 XT has not reproduced the stall. The debug soak's encoder-failure fault exercises the encode-error path, not this one; a stall injection is still to be written.

October 9 follow-up: the debug soak fault directory now accepts encoder stall, containing a duration in milliseconds (for example 300), or an empty encoder stall.persistent, which withholds output until removed. The finite request is consumed once. The fault holds completed frames at the session's encoder boundary, counts them as backlog, and survives recreation; pictures from the old encoder are discarded on recreation. It is compiled only with debug assertions or tests, uses the existing BUTTERPOLLO_TEST_FAULT_DIR, and has no release setting or CLI switch.

GPU-free tests in host/src/stream/encoder_tests.rs use a mock at the encoder-output trait boundary, a virtual clock, the session's actual recovery helpers and IDR request, and a packetized frame sink. They cover 300 ms of silence followed by a keyframe and more packets, a permanent stall, the 250/500/1000/2000 ms recreation waits (then capped at 2000), a short stall released by polling, resetting backoff for a later failure, and output progress with an unchanged backlog. The 20 s budget starts at the first recovery attempt, not at the start of the fault. Polling normally ends it with the encoder did not produce a frame during recovery; if the stall deadline check runs first, the existing message is the encoder stopped returning frames. Both paths are tested, including the failed session's 0x80004005 termination reason. The sink verifies packet delivery resumes; this does not exercise a real GPU, network stream or decoder.

Code and local SDK audit (AMF headers version 1.5.2 in %BUTTERPOLLO_FFMPEG_ROOT%/include/AMF; third-party/ has no AMF docs in this worktree):

The SDK contracts agree with AMD's asynchronous encoder guide and HEVC header. Ranked hypotheses below are inferences, not an RX 9070 XT reproduction:

Rank Hypothesis Evidence that would distinguish it
1 AMF/driver or D3D12 handoff wedges under contention after accepting input. Healthy timings followed by abrupt silence fit better than a steady throughput limit. submit_result=AMF_OK, repeated REPEAT/OK-null with two old inputs, healthy devices, conversion fences completed but AMF's surface fence/ownership not advancing. If all fences complete, the wedge is farther inside AMF/output processing. A WPR/GPUView trace and driver-version comparison are needed to separate these.
2 Capture-copy or colour-conversion work is starved by the 99% game load, so VCN has no ready input. High queue priority still shares GPU execution/memory resources. conversion_completed < conversion_submitted or a surface's completed < conversion_value. Trace the capture fence dependency and compute scheduling with GPUView; a ready conversion weakens this hypothesis. D3D11 input instead needs a graphics-queue trace.
3 A queue/latency/analysis configuration exposes driver buffering or a driver bug. A two-picture host cap can also conflict with a mode needing more input. Compare effective InputQueueSize, Usage, LowLatencyInternal, SAV, PA/lookahead, pre-encode and LTR against defaults. Repeated NEED_MORE_INPUT/EOF is distinct from ordinary REPEAT. INPUT_FULL with the explicit 100 ms submit error identifies submission backpressure. The earlier forced-queue RDNA4 report is a lead, not proof about this reporter's settings.
4 The single VCN is delayed by scheduling, another encoder, or expensive recovery keyframes/pre-encode. All input fences ready, devices healthy, output eventually resumes as the waits grow. Correlate VCN engine occupancy/other sessions in GPUView and the first recovered IDR; collect under-load encode latency. The pre-stall 4.6 ms mean weakens sustained 4K60 throughput exhaustion. One hardware instance leaves no second instance for split-frame relief.
5 Device removal/TDR, or surface retention/accounting failure. Non-success d3d11_removed/d3d12_removed, a fence completed value of u64::MAX, Windows display-driver reset events, or native eight-surface capacity errors. retained far above in_flight and completed surface fences with excess owners suggest retention; correlate with encoder_dropped and queue-drain errors.

New diagnostics, only on startup, failure/recreation or recovered output:

The release frame path only retains AMF result codes and a retry counter for these snapshots; it adds no timer reads, GPU waits, property writes or per-frame logs. Timing, queue limits, defaults and recovery decisions remain unchanged. The software deadlines still require AMF calls to return: a driver blocking inside QueryOutput, SubmitInput, Init or Terminate can overrun them. If logs stop entirely, capture the streaming thread's stack/ETW trace; the synthetic output fault does not simulate a hung driver call. No hardware/ignored tests, streams or installed-service changes were used for this investigation.

Validation: cargo fmt --all -- --check, cargo clippy --workspace --all-targets --locked -j 2 -- -D warnings and cargo test --workspace --locked -j 2 all passed through C:\src\cargo-one.ps1, using D:\bp-build\stall-target. Workspace result: 546 passed, 0 failed, 44 ignored; all six new stall tests passed. No ignored tests were run.

October 8 rc.22 baseline at the owner's settings #

The goal is now a finished product measured at the owner's own settings (1968×2184 HDR, 120 fps, AV1 and HEVC) plus 1080p60 and 1440p120. This is the host-side baseline every later latency change is compared with.

Fixture: run-motion.py from bench-rc17 copied to bench-rc21 in the artifacts folder, unchanged except for size, rate, bitrate and decoder options. Isolated host started as SYSTEM in the signed-in session, virtual HDR display at twice the stream rate, WGC capture (the default), compute conversion on, native AMF at ultra-low latency with speed. Picture age is the timestamped rendered picture to independent decoding on loopback. Bitrates requested: 80 Mbps native, 50 Mbps 1440p, 20 Mbps 1080p. rc.22 is the installed release (3adaa1c22345b6fd). Three runs per row; the numbers are the means of the runs' mean, p95 and p99.

The independent client must decode in hardware at these sizes: with FFmpeg's software HEVC decoder (moonlight-motion-client-dump.exe, 8 threads) native 120 fps took 7.8-8.1 ms per frame, the queue grew and picture age reached 340 ms mean with a 950 ms p99. The client from rust/tests/moonlight_client.c built with BUTTERPOLLO_TEST_HW_DECODER=d3d11va decodes in about 5 ms. For AV1 it must pick FFmpeg's av1 decoder (the default, libdav1d, has no hardware path); rust/tests/moonlight_client.c now does that when a hardware decoder is set.

rc.22, idle Picture age mean / p95 / p99 (ms) New pictures/s Host latency
1968×2184 HEVC HDR 120 fps 13.71 / 14.40 / 15.10 120.3 3.5 ms
1968×2184 AV1 HDR 120 fps 13.45 / 14.08 / 15.01 120.5 3.1 ms
2560×1440 HEVC HDR 120 fps 12.48 / 13.12 / 14.03 120.4 3.0 ms
2560×1440 AV1 HDR 120 fps 12.22 / 12.90 / 13.73 120.4 2.8 ms
1920×1080 HEVC HDR 60 fps 14.60 / 15.83 / 17.57 60.3 2.3 ms
1920×1080 HEVC HDR 60 fps beside gpu_load 45 1000 0 200, software decode 33.14 / 41.94 / 44.38 59.1 2.0 ms

Native AV1 still decodes as 1984×2186 (AMD's AV1 alignment), so its runs fail the fixture's strict size check; the timings are complete. The load row uses the software client as on October 7 (33.4 ms for rc.17 WGC then): with a hardware decoder on the same GPU as the load, decoding starves and picture age reaches seconds, which measures the client, not the host. Native-size rows beside a load therefore need the separate laptop client. An earlier rc.21 batch matched these idle numbers within 0.3 ms, but some of its runs overlapped the rc.22 release session's own e2e streams and it is not used.

Artifacts: bench-rc21\r22-* (baseline), bench-rc21\ab-* (below).

Reconnect and issue #6 fixes, no latency change #

The sign-in, listener and control-timeout fixes committed alongside this entry were checked against rc.22 in one alternating batch, three runs each:

rc.22 With the fixes
1968×2184 HEVC 120 fps idle, mean / p95 / p99 13.66 / 14.39 / 15.15 13.76 / 14.49 / 15.21
1080p60 beside the load, mean / p95 / p99 33.25 / 42.02 / 44.76 32.94 / 42.02 / 44.39

Every run decoded all received frames with zero failures. The differences are within the spread of the runs. The fixes themselves (a reset during accept, a session that loses its control peer, a 5-10 s Wi-Fi drop, a launch before sign-in) are covered by code review and unit tests; they still need a network test on 192.168.4.10 and a signed-out launch on this host.

Native size beside a game, and two rejected settings #

The October 7 load (gpu_load 45 1000 0 200, uncapped) starves the loopback client's hardware decoder at 1968×2184 120 fps: decoding took 8.3-12.6 ms a frame, above the 8.3 ms period, and picture age grew to seconds. Lighter uncapped loads (fewer draws or iterations) did the same. A game capped at 60 fps (gpu_load 45 1000 60 200, 5.3-5.6 ms of GPU work per frame) leaves the decoder at 4.7-6.8 ms and is the native loaded cell from now on. A game capped at 120 fps beside 1440p120 sits at the decoder's limit (8.2 ms) and its runs spread from 17 to 172 ms, so that cell is not used.

rc.22, 1968×2184 HEVC HDR 120 fps beside the 60 fps game, seven default runs from three batches: picture age 17.2-18.7 ms mean, p95 18.8-21.7, p99 19.3-22.6, 120 new pictures a second. The host's own split shows where the load costs time: claim_wait (copy submitted to encoder claim) rises from 0.06 ms idle to 1.0-3.0 ms mean, p95 6 ms, and frame_age from 0.2 to 1.6-3.5 ms. Encoding stays at 3.3 ms. claim_wait is bimodal by run (about 1 or 2.8 ms), which follows the phase between the 60 fps game and the 120 fps stream.

Two settings were tried against that wait, alternating with the default in one batch each, three runs per row:

Beside the 60 fps game Picture age mean / p95 / p99 (ms) claim_wait mean
Default (compute queue priority HIGH) 18.11 / 20.15 / 21.51 2.81 ms
compute_queue_realtime=true (GLOBAL_REALTIME granted) 17.93 / 19.96 / 21.40 2.71 ms
Default, second batch 17.64 / 19.08 / 19.93 2.22 ms
frame_pacing_source_phase=false 18.09 / 20.23 / 21.77 2.23 ms
also frame_pacing_predictive=false 17.87 / 22.62 / 24.54 2.72 ms

A realtime copy queue changes nothing, so the wait is not the copy queuing behind the game's compute. Turning off source-phase or predictive pacing does not remove it either and makes the 95th and 99th percentiles worse. Both stay at their defaults. Next: trace where the claim waits (the copy fence, WGC delivery, or the session thread busy in QueryOutput), which is what the sender-thread and AMF-poll work would change.

Artifacts: bench-rc21\lp-*, rt-*, pc-*.

First remote client: the laptop over Wi-Fi #

Moonlight-qt 6.1.0 on a Radeon 780M laptop (driver 32.0.31035.1003, D3D11VA, panel not HDR) on 5 GHz Wi-Fi, against the installed rc.22 host on Ethernet, October 8 07:04-07:12 UTC. Desktop app, motion_probe drawing on the streamed display for every run, client "lap" on the Extended layout so the owner's monitor stays on. Moonlight's whole-session statistics, one 35 s run per row (averages only, startup included):

Run Received fps Host processing min / max / avg Decode
1968×2184 AV1 HDR 120 fps, 80 Mbps 121.2 2.6 / 4.5 / 2.9 ms 0.37 ms
1968×2184 HEVC HDR 120 fps, 80 Mbps 121.0 3.1 / 13.9 / 3.5 ms 0.38 ms
2560×1440 HEVC HDR 120 fps, 50 Mbps 121.0 2.8 / 14.9 / 3.0 ms 0.51 ms
2560×1440 AV1 HDR 120 fps, 50 Mbps 121.2 2.4 / 4.3 / 2.6 ms 0.62 ms
1920×1080 HEVC HDR 60 fps, 20 Mbps 59.5 1.9 avg, 9.2 max
1920×1080 AV1 HDR 60 fps, 20 Mbps 59.8 1.6 avg, 8.5 max

Network jitter drops stayed at or below 0.13% at 120 fps. The HEVC maxima of 14-15 ms are each stream's first frames, not a steady-state difference between the codecs: in the host's own 5 s windows during these runs HEVC peaked at 4.1-4.7 ms native and 3.4-3.8 ms at 1440p, with p99 3.3-4.2 ms, as AV1. In the 18 loopback runs above, the first keyframe took 12.7-17.7 ms on both codecs and frames 2-5 sometimes 7-17 ms; after that no frame exceeded 6 ms. Warming the encoder before the first frame would only shorten stream start. A first 1080p60 pair overlapped the owner changing his monitor mode (one 48.7 ms frame) and was repeated.

Render-to-decode delay over the network was measured later the same day (see "October 8 real-client picture age over Wi-Fi" above): the independent client launches from a paired identity on the laptop, since the host ties a session to the address that launched it.

The laptop's smoke test on the installed rc.24 (13:17-13:18 UTC, motion on the streamed display, Extended layout) received 118.7 fps for 1968×2184 AV1 HDR and 120.1 fps for HEVC, with 3.0 and 3.6 ms average host processing. Moonlight's AV1 maximum was 20.6 ms against 13.4 ms on rc.23. It is again the stream's first frames: every 5 s host window of that AV1 session peaked at 3.82-3.93 ms (p99 3.36-3.81 ms), and the HEVC session's at 4.51-4.53 ms. A first keyframe's time varies from start to start (12.7-17.7 ms in the loopback runs), so one start does not show a regression.

rc.24 against Vibepollo 2.0 on the same GPU #

The loopback fixture with the pinned Vibepollo 2.0 build (day-work-20261002\vibepollo-baseline-build, VIBEPOLLO_ISOLATED_BENCHMARK) and the installed rc.24, alternating in one batch on October 8 (16:40-16:57 local), two runs per cell, identical settings: native AMF at ultra-low latency, Desktop Duplication capture for both (as on October 4), a virtual HDR display, Extended layout, hardware decoding in the client. No game load: the large October 4 gap (42.4 against 96.4 ms) was beside a GPU load, where Vibepollo fed its encoder about 24 fresh pictures a second; its idle gap was 13.8 against 16.0 ms, in line with these. The owner's monitor now runs at 2560×1440 120 Hz, and the virtual display came up at 120 Hz instead of twice the stream rate in every run of both hosts, so the source drew at 120 fps; these numbers compare only with each other. The monitor's mode was checked before and after (2560×1440 at 120 Hz both times); Vibepollo's display recovery changed nothing this time.

Picture age mean / p95 / p99 (ms), host latency rc.24 Vibepollo 2.0
1968×2184 HEVC HDR 120 fps, 80 Mbps 17.81 / 18.94 / 19.56, 3.50 ms 19.97 / 20.43 / 20.87, 4.04 ms
1968×2184 AV1 HDR 120 fps, 80 Mbps 17.42 / 18.10 / 18.62, 3.20 ms 18.88 / 19.54 / 19.90, 3.89 ms
2560×1440 HEVC HDR 120 fps, 50 Mbps 17.09 / 17.99 / 18.46, 3.08 ms 17.19 / 17.68 / 17.93, 3.52 ms
1920×1080 HEVC HDR 60 fps, 20 Mbps 14.67 / 15.50 / 15.82, 2.46 ms 21.05 / 21.25 / 21.43, 1.30 ms (one run)

At the owner's native size rc.24 delivers the picture 1.5-2.2 ms sooner at every percentile, with 0.5-0.7 ms less host latency; at 1440p the two are equal. At 1080p60 one Vibepollo run's renderer never started and the other was 6.4 ms slower, although Vibepollo reports less host latency there (its host latency excludes part of the path Butterpollo's includes). Every run delivered all source pictures except one rc.24 AV1 run: 9.5 s in, WGC's shared texture failed (0x887A0026, keyed mutex abandoned), capture reopened after a 258 ms gap, and afterwards the client read only 53% of the pictures while the host still sent 119 fps. Its picture age (17.41 ms) is from the pictures it did read. That recovery path needs a look: the picture after reopening may not have been the virtual display's. [Later traced to the fixture: the virtual display moved, Desktop Duplication lost access and reopened correctly, but motion_probe stayed at the old coordinates; fixed in aff04350.]

Artifacts: bench-rc21\vp-*, displays-before-vibepollo.json.

Beside a game #

The same comparison with the 60 fps-capped game beside the stream (gpu_load 45 1000 60 200, which leaves the client's decoder room), two alternating runs per cell, 18:41-18:55 local, Desktop Duplication for both:

Beside the game, picture age mean / p95 / p99 (ms), host latency rc.24 Vibepollo 2.0
1968×2184 HEVC HDR 120 fps 19.61 / 21.56 / 23.10, 3.4 ms 26.96 / 37.62 / 39.25, 8.3 ms
1968×2184 AV1 HDR 120 fps 17.15 / 18.96 / 22.18, 3.1 ms 24.53 / 36.77 / 38.05, 7.4 ms
1920×1080 HEVC HDR 60 fps 17.84 / 21.78 / 22.84, 2.2 ms 20.55 / 22.55 / 22.66, 5.7 ms (one run)

Beside the game rc.24 delivers the native picture 7.4-7.8 ms sooner on average and about 16-18 ms sooner at the 95th and 99th percentiles; its host latency stays at 3.1-3.4 ms while Vibepollo's rises to 7-10 ms. Vibepollo also delivered fewer new pictures (103-118 a second for HEVC, 106-111 for AV1, against 117-120). At 1080p60 the two are close. One Vibepollo 1080p run is left out: its source ran at 60 Hz instead of 120. This matches the October 4 result that the gap opens under GPU load, though the load and absolute values differ. Artifacts: bench-rc21\vl-*.

AV1 at 1968×2184: padding, and why the render-size rewrite is not shipped #

The RX 7900 XT (driver 32.0.31041.1004) encodes a 1968×2184 AV1 stream as 1984×2186 even with AlignmentMode set to no restrictions, and the frame header says render_and_frame_size_different = 0 (FFmpeg trace_headers on a native rc.22 keyframe). Every client therefore receives the padding unless it crops to the negotiated size, as Moonlight-qt does. The host now logs a warning once per encoder at such sizes.

Setting the render size in the frame header is possible without touching the tile data: the flag gains render_width_minus_1 and render_height_minus_1, 32 bits, so everything after it moves by exactly four bytes and the byte alignment before the tiles is unchanged. A prototype that parses the sequence header and the uncompressed frame header up to render_size() (bench-rc21\av1-render\av1_render.py) rewrote the keyframe correctly: trace_headers reads 1967 / 2183, and libdav1d decodes a picture byte-identical to the original. But FFmpeg 9.0.2 still reports and outputs 1984×2186 for it with libdav1d: it does not crop to the render size. The fix would only help a decoder that honours render_size(), and none available here does, so it is not shipped. Check whether the Android client's MediaCodec AV1 decoder crops to it, or crops to the negotiated size itself, before revisiting.

AMF settings per codec: quality at a given bitrate #

windows/examples/amf_quality.rs encodes clip frames with the stream's encoder (compute conversion, the host's default settings plus one change) and writes the bitstream; FFmpeg 9.0.2 then scores it with VMAF and PSNR-Y against the same frames (bench-rc21\quality\run-quality.ps1). Each frame is encoded on its own, so encode time is submission to output without queueing. Clips: a 2D arcade game (Teenage Mutant Ninja Turtles HD, 1080p60) and a 3D shooter (GoldenEye XBLA, 720p30), scaled to 1968×2184 or 2560×1440, 180 frames each, SDR 8-bit. The rows are 1968×2184 at 30 and 80 Mbps (HEVC, AV1) and 2560×1440 at 20 and 50 Mbps (HEVC, AV1, H.264), all at 120 fps. Defaults reach VMAF 96.4-99.0 except H.264 at 20 Mbps (93.5 and 79.7).

Range of VMAF change against the default per codec, and the change in mean and 99th-percentile encode time and in 99th-percentile frame size, first clip (second clip in brackets where run):

Setting HEVC ΔVMAF AV1 ΔVMAF H.264 ΔVMAF Encode mean / p99 p99 frame size
amd_quality=balanced -0.56 to +0.11 -0.29 to 0 -0.05 to 0 same same
amd_quality=quality -0.32 to -0.05 -0.31 to +0.07 -0.03 to +0.02 +0.7 / +1.1 ms HEVC, +6.1 ms AV1, +1.9 ms H.264 same
amd_rc=cbr +0.31 to +0.52 (-0.05 to +0.03) -0.04 to +0.34 (-0.02 to +0.24) +0.34 to +0.98 (-0.75 to +0.04) same +37-67% (+30-58%)
amd_vbaq=disabled -0.13 to +0.24 (-0.03 to +0.40) -0.22 to +0.07 (+0.01 to +0.19) +0.25 to +1.03 (+0.40 to +9.42) same +1% (+6%)
amd_high_motion_quality_boost=enabled -0.22 to 0 -0.29 to +0.22 0 to +0.12 +0.2 to +1.1 ms same
amd_usage=lowlatency -0.22 to +0.23 -0.29 to 0 -0.11 to +0.10 +6 to +8 ms same

Only one change pays at no cost: adaptive quantization (VBAQ) off for H.264. It raised VMAF in all four H.264 rows, by 1.0 and 9.4 at 20 Mbps, with the same encode time. H.264 now defaults to it off; HEVC and AV1 keep it on, where the effect is within ±0.4 either way. Constant bitrate scores slightly higher on the first clip only because it spends more of the budget (VBR with low latency stays 5-20% under the target), and its largest frames grow by a third to two thirds, which costs pacing time on the network; it stays off. The slower quality presets and the low-latency usage buy nothing at these bitrates and cost up to 8 ms an encode.

Limits: SDR 8-bit sources (the owner streams 10-bit HDR; the encoder tools are the same, but the gains were not measured there), two clips upscaled from 720p and 1080p, and VMAF's default model. A 1 to 2 VMAF difference is barely visible; the H.264 row at 20 Mbps is the one a viewer would notice.

The same probe then ran in 10-bit HDR (--hdr 1, run-quality-hdr.ps1): the clips become linear light with SDR white at 203 nits, are uploaded as scRGB FP16 like a captured HDR desktop, and the stream's encoder writes PQ BT.2020; the reference is FFmpeg's zscale conversion of the same frames. HEVC and AV1 at 1968×2184 (30 and 80 Mbps) and 2560×1440 (20 Mbps), 120 fps, both clips, 48 encodes. Defaults reach VMAF 95.8-98.8.

HDR, against the default HEVC ΔVMAF AV1 ΔVMAF p99 frame size
amd_vbaq=disabled -0.30 to +0.90 -0.03 to +0.31 -5% to +5%
amd_quality=balanced -0.40 to -0.13 -0.07 to +0.29 same
amd_rc=cbr -0.23 to +0.48 -0.11 to +0.36 +10% to +42%

Encode time did not change with any of them (native HEVC 5.5 ms mean, AV1 4.9 ms). No change is consistent in HDR either, so HEVC and AV1 keep their defaults; VBAQ stays on for both.

rc.23 against rc.22 #

Main at bad6076c (c4a2d51393496851) against the installed rc.22, the same fixture, alternating in one batch, Extended layout, hardware decoding. Since the morning batches the owner's physical monitor runs in a half-width mode and the fixture adds its display beside it rather than alone; every cell sits 4-6 ms above the morning's and the runs spread over about 2 ms, so compare only within this table.

Picture age mean / p95 / p99 (ms) rc.22 rc.23 candidate
1968×2184 HEVC 120 fps (3 runs) 19.69 / 20.77 / 21.75 19.75 / 20.92 / 21.65
1968×2184 AV1 120 fps (3) 19.00 / 20.23 / 21.06 19.22 / 20.26 / 21.01
2560×1440 HEVC 120 fps (7) 17.70 / 18.80 / 19.33 18.14 / 19.18 / 20.63
1920×1080 HEVC 60 fps (3) 15.40 / 17.56 / 19.08 14.49 / 15.76 / 17.28
1968×2184 HEVC beside the 60 fps game (7) 21.94 / 24.82 / 30.01 23.61 / 36.11 / 40.40

Run means at 1440p fall into two groups, about 16.6 and 18.6 ms, for both builds. Beside the game one rc.23 run averaged 32.2 ms; its host log is healthy (claim wait at most 1.1 ms, encode p99 4.2 ms), so the time went in the client's decoding next to the load, and without it rc.23 averages 22.2 ms against rc.22's 21.9. No regression is visible on the host side; every run decoded all received frames without failures. Artifacts: bench-rc21\r23-*.

October 5 rc.3 release continuation #

The user approved WGC compute by default for the next test release after reviewing the latency/freshness tradeoff below, and authorized publication. wgc_compute_copy=false and the global compute switch remain available. Shared captures now respect those settings. The fixed 150 ms recovery wait has been replaced by acknowledgements after each stream releases old GPU resources; failed reopen attempts still back off.

The reporter can run the candidate but is only reachable through the user on Reddit, with no reply time known. There is no access to the RX 9070 XT. Do not close or advertise a fix for the exact 4.7 versus 3.9 ms latency report based on local RX 7900 XT/wired tests. Validation and release records follow in PERFORMANCE.md; earlier dated records below describe their original state.

October 5 continuation from OpenCode #

The active OpenCode work moved from vibepollo into this butterpollo-rust worktree. Opus shipped rc.2, fixed stream termination on transient UDP send errors, compared the DDX path with Vibepollo 2.0, and prepared the launch posts. Its final unfinished task was WGC measurement and optimization, followed by investigating packet-burst pacing on slower client links.

The last WGC smoke test succeeded as the interactive user but failed under SYSTEM at CreateForMonitor with 0x80070424. Opus left additional error contexts in windows/src/capture.rs; those are preserved. The service's host runs as SYSTEM in the interactive session, so this is a host limitation, not just an isolated-harness failure.

The continuation on codex/wgc-capture-recovery shares the startup/recovery fallback to DDX and investigates event-driven WGC wakeups. Explicit capture probes stay strict: requesting WGC cannot silently benchmark DDX. Native reconnect testing also found that revoking FrameArrived after the pool closes aborts inside Windows; notification removal now precedes pool closure and is idempotent. Pure notification waits improved idle detection but regressed under GPU load, so notification registration remains opt-in for the probe; production capture keeps polling. These changes are local and unreleased. The installed host remains rc.2.

Validation: all 177 workspace tests pass (19 hardware-specific tests remain ignored by the default suite), Clippy passes with warnings denied, and the release host builds. The explicit native WGC test passes 16 reconnect/COM teardown cycles. An isolated user-mode WGC stream decodes 715/715 HEVC frames at 1080p60 with zero failures and nonzero audio. No SYSTEM-context runtime retest or installation was performed. The quantitative wakeup experiment and its limitations are recorded in PERFORMANCE.md.

Follow-up: service-mode WGC needs a capture helper running as the signed-in user. DDX fallback does not establish equivalent VRR or generated-frame behavior. The subsequent WGC compute-copy follow-up below validates content and synchronization independently before measuring full-stream latency. The subsequent follow-up adds completion-based packet pacing with wire overhead and a cap for known local Ethernet links. A late send no longer creates a catch-up burst. Three core tests cover the cap, delayed sends and per-datagram overhead. Independent UDP stress tests on the NUC at 192.168.4.10 cover integrity, real socket overruns and a separately labeled modeled bottleneck. This wired fixture does not reproduce the reporter's Wi-Fi.

The user identifies the reporter's GPU as RX 9070 XT, with the latest driver. Their rc.2 log reports one HEVC instance and transient UDP errors 10055/10035. The 4.7 versus 3.9 ms comparison remains unresolved. Local 7900 XT tests confirm the existing compute conversion helps under GPU load; disabling multi-instance encoding has no useful local improvement. An output-wait optimization improved component and idle results but regressed loaded LAN host mean from 6.102 to 6.501 ms, so it was reverted. Do not promote it based only on the favorable component measurement.

A native DDX failure led to finding a missing display-awake request in Rust's capture worker. Vibepollo holds this request. The workstation's idle timeout is three minutes; the unchanged snapshot test passed once with a temporary request, but later failed again even with a moving source and that request. Its failure remains unresolved; do not attribute it solely to display sleep. The worker now holds a thread-bound guard which preserves and restores prior requirements. This does not establish the cause of the reporter's restarts.

The 230-second wired LAN pair crossed the workstation's 180-second display timeout. The control recorded no fresh capture claims in all ten samples after 180 seconds; the revised worker recorded fresh claims in all ten. Both decoded every received picture and the test tone, with zero codec failures. Both still required two startup DDX restarts. Preserve that open startup issue and the failing standalone DDX test in the handoff.

Before the compute-copy follow-up, the workspace/native run passed 198 checks (180 ordinary and 18 native), excluding the known failing AV1 geometry check and unavailable NVIDIA hardware. The DDX test passes in that active-desktop state; earlier inactive-desktop failures remain open. Final LAN checks pass H.264, HEVC HDR, aligned AV1 and explicit WGC at approximately 60 FPS, with exact geometry, nonblank pictures, test-tone audio and zero decode errors.

The independent C receiver now runs on Linux as well as Windows, optionally uses hardware decoding with readback of every picture, checks pixel contrast, and can require a minimum steady frame rate. The NUC passed 1080p60 HEVC but could only deliver 27.588 FPS in the 4K60 readback case; that is a failed performance gate, not a successful 4K60 test. Exact unaligned AMD AV1 geometry was rechecked and still fails for all twelve SDR/HDR/alignment combinations.

Local validation artifacts are in C:\Users\ramaz\.codex\artifacts\butterpollo-wgc-20261005; the NUC fixtures are in /home/rambo/butterpollo-tests-20261005. Complete measurements and reproduction are in PERFORMANCE.md.

That earlier release host built and Clippy/format/diff checks passed. Its SHA-256 is ff9da9483253a3e5b70737ad7e77006e689effc24ebc4519e0a719dcd66880d2. The final 4K60 HEVC loopback check decodes 985/985 received pictures at 60.585 steady FPS, but includes startup blank pictures and recovery time. No installed service or profile was replaced; test-owned processes and receiver containers are stopped. Changes remain uncommitted on codex/wgc-capture-recovery. validation-summary.json records the checks, limitations and source hashes.

Subsequent WGC compute-copy and startup investigation #

The user asked which optimizations were reverted before continuing. The two reversions remain default WGC notifications and the shorter encoder-output wait. Neither was re-enabled. Supported AMD WGC capture can use DDX's fenced compute handoff and compute AMF conversion with wgc_compute_copy=true, with graceful graphics fallback. Default activation was subsequently reverted after the corrected comparison below; the option remains experimental.

Native WGC verification compares the candidate before the reference: 120 exact frames and 119 changing pictures at idle, then the same under GPU load, including a snapshot retained after teardown. Eight complete HEVC streams in alternating order found loaded host mean 8.557 → 1.953 ms and decoded picture age 49.052 → 40.630 ms. Idle picture age was 31.432 → 31.835 ms. The strict distinct-frame gate still fails in several control/candidate runs; do not equate approximately 60.6 transport FPS with 60 distinct pictures. See the full table and limits in PERFORMANCE.md. This is not a 9070 XT retest.

A second native regression test reproduced compute-sharing fallback losing the only available frame. It now copies that same frame through D3D11, and the test passes. The original DDX arrival probe also falsely reported one frame when it had no samples; actual frame and presentation counts are now separate. The new startup probe found no DDX frames while Windows reported the display off, while WGC supplied one cached image. During a cold stream, raw duplication observed 5120×1440 BGRA → 3840×2160 FP16 → 5120×1440 BGRA, coincident with two access-loss events. The initiator remains unidentified; display changes were disabled in the fixture. A warm repeat had no restart. Do not add a blind delay or reject legitimate dark frames to hide this.

The motion probe's numeric rate previously changed physical refresh. It now paces only the animation, with before/during/after checks confirming the physical 5120×1440 output remains at 240 Hz. The initial comparison above used the old 60-Hz fixture and forced static repeats at 60 FPS. A four-run reversed comparison found that using production's existing 20-FPS repeat floor restores 59.9–60.0 distinct FPS at idle instead of 44–46, without changing production.

Eight further streams with production's repeat floor and the corrected fixture found compute reduces loaded picture age from 52.859 to 34.603 ms, but distinct FPS drops from 51.423 to 49.955. All four loaded runs fail the 58.2-FPS gates; all four idle runs pass. This prompted reverting the compute default too. Keep the option available, the failures visible, and the user's RX 9070 XT acceptance open. Reports: repeat-cadence, wgc-compute-abba3.

The moving-desktop workspace/native run passes 200 checks, including 20 native checks. The unavailable NVIDIA test and known failing AV1 geometry test remain excluded. The final retained build and validation are recorded in validation-summary.json; the installed service and profile remain unchanged. Final retained host SHA-256: c8341cefcb1cdfc50f8038e735c412175571222a743da5f0ba6ec6a388366959. The final run again passes all 200 available tests. Wired default WGC HEVC, opt-in compute HEVC and opt-in compute HEVC HDR all deliver approximately 60 distinct FPS with zero decode errors. An absent-motion negative check correctly fails despite decoding all 724 received pictures. Reports: retained-native-final and retained-lan-final. All workspace release binaries and Clippy pass.

October 3 installed state (historical) #

Installed revision: 8db29eee4 (October 3, 07:37 UTC; butterpollo.exe SHA-256 DB543650FC4824E0CDA61919E619E6419106E7B5D45A815AD0C541CB816ACA77, 58 package files, profile preserved). It contains the scheduling work below and the multi-stream and display fixes of October 3. Push only to the owned butterpollo remote. Draft PR: https://github.com/RamazanKara/Rubylight/pull/1.

Why the customer saw higher host latency than Vibepollo #

  1. Different counters. The C++ host sends Moonlight frame_processing_latency = send - host_processing_timestamp, taken when it picks the frame up (src/stream.cpp:2037-2046, display_wgc.cpp:384). The Rust host sent send - presentation, which adds the 2-3 ms a frame waits after Windows presents it. The Rust host now reports claim to packet, as the C++ host did, and logs the waiting separately (frame_age, split into detect and claim_wait), so it stays visible.
  2. Real waiting. The encoder claimed frames on a fixed 120 Hz grid unrelated to presentation, so frames aged 2-3 ms before encoding (phase lottery: the value was fixed per session). Frames are now claimed on arrival (frame_pacing = arrival, default; grid keeps the old scheduler).
  3. Coarse waits. Waitable timers on this PC wake 0.3-0.5 ms late for short waits (windows/examples/timer_probe.rs), so the 100 us AMF output poll was really ~0.5 ms and deadline claims were late. AMF now waits in the driver (QueryTimeout = 1, only while a frame is in flight); streams raise the timer resolution, opt out of power throttling and use high priority, as the C++ host did.

Measured (October 2 evening, same binary, A/B) #

Fixture: day-work-20261002/run-motion.py av1 <label> rust physical-strip-motion with BUTTERPOLLO_TEST_STREAM_SCALE=0.5 (physical 5120x1440 at 240 Hz streamed at 2560x720/120 so the local software decoder is not the bottleneck), BUTTERPOLLO_TEST_DECODER_THREADS=4, BUTTERPOLLO_TEST_FRAME_PACING. received_age.py reports picture age minus client decode time (renderer to fully received frame), which excludes the loopback decoder's CPU contention.

Run (v6, final) Picture age mean / p99 Received age mean / p99 Present to send Frame age
grid 12.55 / 13.66 ms 9.37 / 9.86 ms 4.8 ms 2.9 ms
arrival a 10.40 / 11.74 ms 7.10 / 7.85 ms 2.5-2.6 ms 0.40 ms
arrival b 10.29 / 11.51 ms 7.08 / 7.84 ms 2.55-2.6 ms 0.41 ms
arrival c (stopped when the customer connected) - 7.10 / 7.88 ms 2.5 ms 0.41 ms

Zero late intervals and 120.0 unique fps in every run. Arrival pacing is 2.3 ms faster end to end than the grid on the same binary. The 1 ms timer also helps the local renderer and client, so do not compare these absolute values with runs before d9a04bbb0.

Three pacer defects were found with per-claim traces (BUTTERPOLLO_TEST_RUST_LOG=info,pacing=trace, claims.py) and are covered by tests: the session-sampled cadence estimate drifted (now the capture worker's median interval); a fresh frame presented just before the claim slot but detected after it lost to the older one; and a credit deficit from startup persisted for a whole session at exactly the stream rate (credit now refills 1 % faster, claims need 7/8 of a frame of credit).

Against Vibepollo 2.0 (pinned C++ baseline) #

Same fixture, same scaled 2560x720 AV1 stream and client, run alternately after the Rust build above was installed (22:32 local). The C++ baseline's first run failed (its virtual display restart was denied, the renderer timed out) and its display recovery re-enabled the HISENSE monitor and set the Odyssey from 240 Hz to 120 Hz; the second pair therefore ran on a 120 Hz source. The displays were put back afterwards (Odyssey 5120x1440 at 240 Hz, HISENSE detached, as before the test). Do not run the C++ baseline fixture on this machine again without isolating its display recovery.

120 Hz source Rust Vibepollo 2.0
Moonlight host latency (claim to send, both) 2.05 ms 2.47 ms
Picture age mean / p99 15.6 / 18.7 ms 18.2 / 29.9 ms
Received age mean / p99 11.6 / 12.5 ms 13.2 / 22.6 ms
Intervals over 1.5 periods in 30 s 1 17

One pair only; repeat on the customer's real sessions before claiming it in release notes.

Build loop warning #

The WSL clock ran 43 s behind Windows, so cargo on Windows skipped rebuilding files edited in WSL within that window. systemd-timesyncd in WSL was stopped and the WSL clock set from Windows. If edits seem to have no effect, compare wsl date with Windows time and touch the sources.

October 3: multiple streams and display restoration #

The customer reported that multiple streams did not work. Fixed in c23384eae..8db29eee4:

  1. Reconnect lockout. A client launching again after an abandoned launch or stream got "client already has a session" until the old one timed out (four failures in a row at 05:09 UTC). A launch now supersedes the same client's launch or stream in that role and waits up to 5 s for its teardown.
  2. Second client refused. A different display mode ("another stream owns a different display mode") or a running arrangement refused the second stream, and clients shared one retained display. Each client now has its own retained display, a mode set by one stream is kept (the other stream scales), and the arrangement is shared and re-laid out as streams come and go.
  3. A second client's virtual display broke the first client's DDX capture: 0x887A0026 on every re-created duplication, 117 restarts, never recovered. Windows keeps returning the stale adapter while any device on it is alive (multi_ddx_probe reproduces this). The capture worker now drops the lost capture, withdraws the frame and waits 150 ms for streams to release their encoder before re-creating it. The first stream recovers after one or two restarts, about a second without frames.
  4. RTSP refusals and failures were logged at debug; now warnings.
  5. Display restoration. Topology::set_mode_rate trusted SetDisplayConfig: the HISENSE TV asked for 1080p120 came back at 60 Hz with no error, and re-applying the unchanged layout (set_positions) did the same. It now skips a display already in the mode, verifies the result, and falls back to the display's mode list. The layout recorded for restore no longer contains the stream's own virtual display (a retained one stayed as an invisible monitor beside the Odyssey; restoring its settings after it was removed is the likely source of the customer's "os error 31"). Restore continues past a failing display, names the failed step, re-checks rates after moving displays, and retries briefly while Windows applies another change ("cannot read display mode").

Fixtures (in day-work-20261002): multi_stream.py runs two paired clients against an isolated host (concurrent join, retry after an unconnected launch, resume after a client crash). run-system-multi.ps1 runs it as SYSTEM so each client gets its own virtual display (-Layout extended). The check capture_restarts_bounded allows up to four capture restarts when a display arrives. restore_probe re-applies the current layout, optionally step by step, and prints every display's rate.

Run Result
Physical display, all scenarios PASS, 0 capture restarts
SYSTEM, per-client virtual displays, before the fix FAIL, 117 restarts
SYSTEM, per-client virtual displays, final PASS, 1 restart; A 58.1 fps at 60, B 114 fps at 120; layout and rates unchanged; no restore warning

The arrival fixture could not be compared with the 240 Hz runs above: the Odyssey was at 120 Hz and the phone's retained 240 Hz virtual display was attached. Same display state, installed f44de5dd8 against the new build: received age 13.6 / 19.8 ms against 13.2 / 20.2 ms (mean / p99), host p99 2.6 ms for both. No pacing regression.

Display state found on October 3 (not changed back without the customer): Odyssey 5120x1440 at 120 Hz (240 Hz on October 2), HISENSE active at 120 Hz until restore_probe reproduced the 60 Hz bug on it several times; it was returned to 120 Hz and then went inactive about a minute later, while no probe ran (TV standby or switched off; still connected). After the update restarted the service it was active again at 1920x1080 at 120 Hz in its usual place, and the retained virtual display was gone. The Odyssey is still at 120 Hz.

Open: the customer's own configuration (exclusive layout, HDR virtual display) was not run here because it turns the physical monitors off. If error 31 still occurs, the warning now names the display and step.

October 3 evening: Vibepollo 2.0 parity #

Goal from the customer: Butterpollo replaces Vibepollo 2.0 for its users, with the latency wins kept and Vibepollo's behaviour everywhere else. docs/features.md is the current list. Done since 967fe6a87:

Still missing: the virtual display render GPU and reclaim after restart, Playnite focus retries and fullscreen relaunch, /api/browse.

Unverified here: the secure desktop during a stream, streaming the sign-in screen after a reboot, a Steam game ending its stream, the restore hotkey.

October 4: latency beside a game #

Customer request: the lowest possible encode and end-to-end latency. Encode is at the VCN floor when idle; the remaining cost was D3D11 work queued behind a game on the graphics engine. Captures are now copied and converted on D3D12 compute queues and AMF encodes from D3D12 (cc05d5018, details and numbers in PERFORMANCE.md). Idle picture age fell 0.6 ms; beside a heavy GPU load it fell 7-9 ms with more new pictures per second. gpu_compute_conversion = false restores the graphics-queue path.

Probes (156c9f348): gpu_load, d3d12_probe, copy_probe, ddx_sync_probe, and performance --live-capture --arrival for present-to-bitstream timing. Scripts in <artifact>\day-work-20261004 (load_matrix.py, e2e-latency.ps1, ddx-sync.ps1, live-latency.ps1, guarded.py, quiet.py).

Lessons for test runs:

Next: AMD AV1 still pads 1968×2184 to 1984×2186 (AMF issue 423). Only AMD GPUs use the compute queues; NVIDIA and Intel keep the graphics-queue copy and conversion until the path is tested there.

Waiting in Desktop Duplication instead of polling was measured and rejected (examples/ddx_arrival_probe.rs, day-work-20261004\ddx-wait, e2e11, e2e12). A blocking AcquireNextFrame returns a frame 0.08 ms after its present (p95 0.11 ms) against 0.39 ms (p95 0.89 ms) when polling every 0.5 ms, but it holds the device's immediate-context lock: another thread's Flush meanwhile took 1.1 ms at the median and up to 56 ms (texture GetDesc and output GetDesc1 are unaffected). With AMF on compute queues nothing else used that context, and idle streams gained 0.16 ms of picture age over four runs. Beside the game load, waiting was worse in all three pairs: 30.6 against 29.2 ms picture age, 91 against 94 new pictures per second, 0.7 ms more host time. Capture keeps polling.

Next work #

  1. Install the current source while the host is idle (package with build.ps1 -Dependencies <artifact>\bootstrap-sdk -TargetDirectory <artifact>\target -NvidiaRoot <artifact>\ngx-sdk -MsvcSdk <artifact>\msvc-sdk -Package after dot-sourcing performance-probe/rust-env.ps1). Check the customer's next 1968x2184 HDR sessions: frame_age, host_mean (now claim to packet) and the Artemis HDR10 warning after a host restart.
  2. Repeat the A/B against the pinned Vibepollo baseline with the scaled fixture (picture and received age), now that the counters agree.
  3. Encode is 0.2-0.3 ms slower when claiming right after composition (the conversion waits for the capture copy). Investigate converting directly from the duplicated surface.
  4. AMD AV1 padded decoded size remains open (see below).

Operating rules #

Work log #

Measurements from this pass #

Artifacts are in the day-work-20261002 directory above. These are local test results, not acceptance of a better customer experience or release readiness.

Case Mean / p95 / p99 / max host ms Mean / p95 / p99 / max picture age ms Result
5d5 HEVC, low-delay decoder 6.158 / 9.5 / 9.8 / 13.5 16.307 / 20.220 / 21.968 / 27.133 120.001 fps, strict decode passed; startup probe overlapped
5d5 AV1 5.630 / 8.9 / 9.3 / 12.5 14.498 / 18.305 / 18.859 / 21.749 119.999 fps; wrong decoded dimensions
AMF 8×2 request, AV1 5.542 / 8.9 / 9.3 / 12.7 14.386 / 17.954 / 18.453 / 21.995 120.000 fps; wrong decoded dimensions; full startup probe awaited
Same-size shader and explicit AMF crop, AV1 6.785 / 8.7 / 8.8 / 9.2 15.490 / 18.013 / 18.338 / 19.066 120.002 fps; wrong decoded dimensions; full startup probe awaited

Picture age uses the timestamp rendered into the picture and the decode completion QPC on this machine. It includes local rendering, DWM, capture, encoding, loopback and software decoding, but excludes remote display scanout. Vibepollo starts its reported host timer after acquiring a frame; Rust uses its source timestamp. Those counters alone cannot establish a fair comparison.

Before the same-size shader change, 64 GPU samples measured HDR FP16-to-P010 conversion at native size: 0.335 ms mean, 0.337 ms p95, 0.343 ms p99. Resizing to 3840 × 2160: 0.520 ms mean, 0.619 ms p95. These isolate conversion and exclude capture, encoding and delivery; any component gain must be checked in a stream.

After the same-size shader change, native conversion measured 0.170 ms mean, 0.175 ms p95 and 0.179 ms p99. Scaled conversion measured 0.532 ms mean and 0.641 ms p95, versus 0.520/0.619 before; verify this small variance if further shader changes affect resizing. All six native GPU checks passed, including absolute HDR luminance/gamut/linear resizing, SDR range/matrix, cursor blending, 4:4:4 chroma, retained texture ownership and conversion timestamps.

The stream result still depends strongly on capture/timer phase: the last AV1 test had a lower maximum but a worse mean. Do not call it a host-latency win. Its capture-age estimate p95 remained approximately 5.1 ms; encoding p95 was 3.7 ms. The 8×2 alignment experiment was reverted. Explicit surface crop was ported from the C++ AMF backend but did not fix decoded dimensions on this GPU.

The archived C++ baseline was built successfully with streaming/capture/codec paths unchanged. Test-only patches guard four machine-wide startup recovery operations and add an isolated-display creation marker. PyroWave/WebRTC and driver packaging are disabled in this comparison build; native AMF/DDX remain the compared paths. Its initial launches failed before streaming because the portable asset working directory and log parent were missing; fix the fixture, not the installed host. Preserve both patch files and the pinned inventory.

The GPU process scheduling class was already ported, but per-device GPU thread priority and maximum render-queue latency were missing. Added the previous host's relative priority 7 and queue hint 1 as independent best-effort settings. These compile but await a measured full stream. They are rendering hints, not evidence that the capture/codec pipeline previously queued three frames.

Later work, 14:36–15:12 UTC #

Continued active coding and testing. No candidate has been installed or pushed.

Regression and package validation, 15:12–15:36 UTC #

Continued active work; no candidate has been installed or pushed.

Installed candidate and capture wait investigation, 15:36–15:52 UTC #

DDX lifecycle and normal-user motion work, 15:52–16:20 UTC #

Matched SDR motion and customer regressions, 16:20–17:03 UTC #

Final source validation and handoff, 17:03–17:42 UTC #

Matched physical-output motion results below use a 128-pixel strip without changing the existing display mode. Picture age includes rendering, DWM, capture, encoding, loopback and independent software decoding; it excludes remote display scanout. The C++ host counter excludes pre-acquisition capture age while Rust includes it, so compare picture age and delivery together. These runs do not establish native virtual-display or full-game acceptance.

Case Unique fps / late intervals Mean / p95 / p99 / max picture age ms
C++ AV1 SDR strip, second run 119.765 / 3 16.424 / 21.132 / 22.022 / 46.623
Rust AV1 SDR, embedded shaders + freshness 119.998 / 0 16.173 / 16.819 / 17.286 / 17.906
C++ HEVC native physical HDR, first pair 119.798 / 4 15.677 / 16.057 / 17.058 / 32.182
Rust HEVC physical HDR + freshness, first pair 120.000 / 0 16.663 / 17.116 / 17.344 / 20.370
Rust HEVC HDR + fixed 100 µs polling 119.386 / 16 15.111 / 15.585 / 16.009 / 24.123
C++ HEVC HDR, final pair without session API polling 118.645 / 31 18.633 / 24.091 / 83.201 / 111.426
Rust HEVC HDR + predictive polling, final pair 119.355 / 14 15.841 / 18.115 / 19.406 / 28.169

The final pair's source and delivery cadence are worse than the earlier pair. Renderer interval medians are near 119.918 Hz but do not account for every missed vblank. Repeat under controlled CPU/GPU load and measure the producer's overall rate before promoting any scheduling change. Do not select only the favorable tail result or claim a whole-product victory.

At 17:49 UTC the final locked release workspace test run passes all 134 ordinary tests (host 16, core 78, Windows 39, Vulkan 1); fifteen native tests remain opt-in. Formatting, git diff --check and all-target Clippy with warnings denied pass. Logs: handoff-tests.log and handoff-clippy.log. The current normal host release build passes. Previous native checks validate embedded shaders; no GPU workload was started during the final wrap. The current source is not packaged or installed.

Active work through 17:42 covers about five hours, with the 13:20–13:29 gap excluded; this was not a full day of active work. The user-requested wrap records results and checkpoints code rather than continuing experiments.

Handoff priorities and reproduction #

  1. Review the final source checkpoint; package/install it only when the actual installed host is idle. Existing package/ZIP and installed host still belong to 36bff835a. Preserve configuration and pairings. Verify the negative-key launch and complete startup HDR flags with Artemis after installation.
  2. Establish a repeated matched full-stream baseline at native 1968×2184 HDR 120 fps, same scene, codec, bitrate and client. Diagnose capture age, send intervals and mouse smoothness. Keep every scheduling experiment disabled until both picture age and cadence improve reliably.
  3. Resolve AMD AV1 decoded geometry: requested 1968×2184 becomes 1984×2186; 1920×1080 becomes 1920×1082; 2184×1968 becomes 2240×1968. Driver FrameSize, alignment and explicit crop attempts have not fixed it. Do not weaken exact checks, falsify headers or silently change the requested picture.
  4. Reproduce the HDR10 warning and display restoration error 31. NVIDIA/Intel, NGX, VHF feedback and full VRR/HDR client acceptance still need appropriate hardware. Keep the PR draft; neither latency acceptance nor release readiness is established.

All fixtures and raw results are in the artifact directory stated above. installed_state.py reads the real connection ledger with a quiet interval; guard-idle.py, run-native.py, run-motion.py and probe-startup.py are normal-user runners. The physical motion runner autodetects the existing source, stores host SHA/settings, uses a test-owned tone and saves no desktop frame dump. BUTTERPOLLO_TEST_POLL_SESSION_API=0 disables intrusive fixture API sampling. The final Rust motion binary SHA-256 is 5c6689313bff0ccb09d9d4ccaf180be4193900a4a07ec1242a1c358ad1631022.

The pinned C++ baseline is 8a8c4b03a280ab9f567beb380110abb80f5220b8 in vibepollo-baseline-source/vibepollo-baseline-build, with its exact modules and startup-only test isolation patches. Its capture/codec/stream hot path is unchanged. Do not run a globally installed C++ host for these isolated fixtures: its startup recovery can affect the real display arrangement.

Native Rust environment: dot-source the sibling performance-probe/rust-env.ps1 and put C:\msys64\ucrt64\bin first on PATH. Use cargo test --locked --release --workspace, cargo clippy --locked --workspace --all-targets -- -D warnings and cargo fmt --all -- --check. Native GPU checks require the idle guard. Artifact Python is ..\test-python\Scripts\python.exe; fixture commands and matched settings are retained alongside their logs.

Automatic approval review rejected the elevated SYSTEM benchmark with "blocked by policy". It was not retried through another privilege mechanism. Subsequent tests used normal-user physical outputs. Routine idle-safe updating was a separate accepted action. Customer streams and credentials were untouched.

Update this file with concrete changes, measurements, failures and next steps. Record active work intervals separately from elapsed wall time and scheduled idle gaps. A schedule running for a day does not prove a day of active effort.

GPU reset recovery audit, 2026-10-09 #

Audited base ec6b0db0 without running a stream, GPU workload, installed service, or driver operation. Line references in this paragraph refer to that base: host/src/stream.rs:440,1772 joined capture/audio without deadlines; windows/src/display/recovery.rs:1007 likewise joined the display lease worker; windows/src/timing.rs:185 used INFINITE despite arming a deadline timer; host/src/state.rs:351 awaited codec readiness indefinitely on serverinfo/applist; windows/src/encoder.rs:583 drained FFmpeg output without an iteration bound. These now have five-second joins, a timer wait ceiling, ten-second readiness timeout, and a 64-packet drain limit respectively. Capture reopen checked its deadline only after failed opens (host/src/stream.rs:810), so display-wait branches could bypass it; the deadline now covers every retry branch.

Compute CPU fences already had two-second waits (windows/src/compute.rs:92), including handoff/converter destruction; removal's UINT64_MAX is now typed and failure paths query the device's removal reason. AMF already requested QueryTimeout=1 ms (amf.rs:336), queried once per poll (amf.rs:1055), and bounded its three submission/capacity loops at 100 ms (amf.rs:1201,1322,1407). There is no Present in the stream path; DDX acquisition uses timeout zero. WGC pool errors retain HRESULTs, including across helper IPC, and its existing one-second health check now detects a removed device even with an empty pool.

Previously generic encoder retries could reuse a dead capture device. Typed REMOVED/RESET/HUNG/DRIVER_INTERNAL_ERROR now starts one shared recovery incident: log the reason once through the stream-card warning, release all consumer GPU leases, back off 150 ms, recreate capture/device and the pinned encoder, request an IDR, and clear recovery only after encoded output. Repeated removal and successful capture opens cannot restart the 30-second budget. A supervisor created only on loss ends all affected clients even if vendor teardown stalls; the terminal warning explains unavailable adapters/Code 31 and rebooting. Healthy frames add no allocations or system calls; reason queries happen on errors/stalls or the existing WGC health interval. No service restart is needed.

Synchronous driver calls cannot be forcibly cancelled safely in this process: D3D11 Map (capture.rs:892), AMF QueryOutput and Terminate (amf.rs:1448,1454), and COM/FFmpeg releases can still strand a worker until the driver or process exits. Bounded joins detach such workers while their owned resources remain alive. Unrelated lifetime waits remain: display_recovery.rs:524 watches host exit in a separate process; crash.rs:58,66 waits for the crash reporter process; crash.rs:407,411,440 are crash-test synchronization. These are not GPU fences. This change contains resets; it does not establish or fix the cause of the TDR.

GPU-free tests cover HRESULT/context classification, fence-removal sentinels, WGC IPC/fallback, one-shot/persistent debug fault files, resource release, recovery keyframes, fixed recovery deadlines, and client termination independent of a blocked worker. Native DXGI adapter/display-topology tests are now opt-in like the other hardware tests. The first ordinary workspace run unexpectedly ran the unmarked topology test and failed with 0x80070057 in the current environment; its assertions remain unchanged. Final validation passes: fmt check, workspace/all-target Clippy with warnings denied, and locked workspace tests (553 passed, 46 ignored), all through C:\src\cargo-one.ps1 with CARGO_TARGET_DIR=D:\bp-build\tdr-target. After reboot, verify normal SDR/HDR streams, one-shot and persistent DXGI_ERROR_DEVICE_REMOVED[.persistent] files in an isolated debug host's BUTTERPOLLO_TEST_FAULT_DIR, the stream-card warning/IDR, and responsive console and serverinfo throughout recovery. Actual TDR/Code 31 recovery remains a hardware acceptance check; no forced TDR was attempted here.

October 9 user report: physical monitor comes back on when a game loses focus #

Report (the owner): streaming with the game in exclusive fullscreen, pressing the Win key turns the physical monitor back on. Borderless and windowed games do not. Cause, inferred from code and not yet reproduced: the exclusive layout is applied without SDC_SAVE_TO_DATABASE, so the Windows display database for "physical + virtual display" still has the physical monitor on. An exclusive-fullscreen game losing focus (Win key, Alt+Tab, Ctrl+Alt+Del) makes Windows apply that saved layout. The virtual display stays on, so the heartbeat's recovery (which only starts when the virtual display is switched off or recreated) never noticed.

To confirm on the host (needs the exclusive layout, so only with the owner's OK): one virtual display stream with the exclusive layout, then rust/tests/layout_recall.ps1, which makes Windows apply its saved layout the way a focus loss does and times how long the physical monitor stays on (pass: never on with the saved layout; off again within 1.5 s if only the watchdog acts; a588457 should show it staying on). After the stream, the physical monitor must come back as before, and a following extended-layout stream must leave it on. Then the real case: an exclusive-fullscreen game, press Win, Alt+Tab and Ctrl+Alt+Del; the host log shows "displays the stream layout switched off came back on; reapplying it".

October 9 user report: audio_loss warnings on an idle host #

Report (the owner, any playback device, idle PC, video perfect): the log fills with Audio lost on the host before sending: 4 late reads, 5.6 ms beyond the capture buffer, 0 unsent packets (longest wait 26.6 ms, buffer 22.0 ms), a new line every 5 s.

Cause, from code and not yet measured on the host:

Change:

To confirm on the host (idle): rust/release/e2e.py streams with the virtual-speaker tone; run 60 s HEVC 1080p60 twice on each of eef9754 and this change with BUTTERPOLLO_E2E_RUST_LOG=info,butterpollo=debug. Pass: base shows audio_loss lines as the owner sees; the change shows none and few or no late passes. Positive control on a debug build: write 40 to <fault dir>/audio stall every 2 s during a 30 s stream; pass: an audio_loss warning with about one late read per stall and 10-20 ms lost each. If it stays silent, Windows does not skip the loopback position on overflow and the detector is deaf; the debug lines then show what it saw.

Host A/B, 2026-10-09 (local release builds in the rc.28 package, HEVC 1080p60, 60 s, 2 runs each): a164f72 and eef9754 both passed with the tone decoded at 60.0 fps and 0 audio_loss lines; the branch logged no late reads and no audio_default/audio_device_query warnings. The base never showed the warning because the e2e profile sets keep_sink_default=false, auto_capture_sink=false and a fixed virtual sink, so the once-a-second upkeep never asked Windows anything: consistent with the upkeep being the stall, but its cost is not measured. rc.29 shipped a164f72 (380c177).

Positive control (debug a164f72, 14 injected 40 ms stalls): 0 warnings. Each stall logged waited_ms=41-45 skipped_ms=0, then 5-10 ms later a read with waited_ms~1 skipped_ms=18 (28 three times). The late read still finds the packets that fit, in order; the skip shows on the next packet. rc.29's detector only credited a skip to the read it arrived on, so it missed every real drop. Fix: a late read stays pending for one buffer, and the first skip in that time counts against it (HostLoss::read, which now also takes whether Windows delivered anything; a late read with an empty buffer lost nothing). audio_probe --default-cost [rounds] times the upkeep's Windows calls directly (fresh enumerator, three default endpoints), read-only.

To confirm: the control again (pass: about one late read per stall, 18-28 ms lost each), and audio_probe --default-cost 200 on the host.

Host re-test of 0863e14, 2026-10-09: all pass.

Draft for the rc.30 notes: "rc.29's audio loss warning missed real drops: Windows reports a dropped packet on the read after the late one. It now counts them. On the host, 15 of 16 injected 40 ms stalls were reported, each with the 18-28 ms Windows dropped; normal streams stay silent. The device check rc.29 moved off the audio thread took 7.8 ms on average (max 11 ms) on the host."

Rename to Rubylight (2026-10-09) #

Ramazan picked Rubylight as the new name (analysis/rename-research.md in the project files). Everything people see is renamed: console, tray, setup, Start menu entry, Apps & features entry, the service's display name and the firewall rule; setup and in-app updates replace the old Start menu entry and firewall rule once the new ones exist. The service key (ApolloService), the profile (%ProgramData%\Butterpollo\config), binary names, pipe, mutex and task names, the Vulkan layer, the NVIDIA profile, the UPnP mapping description and the virtual display label keep their names, so upgrades stay in place and rolling back to rc.29 still works. Fresh installs default to C:\Program Files\Rubylight.

The in-app updater of rc.29 and older asks api.github.com/repos/RamazanKara/Rubylight and only accepts butterpollo-setup-<v>.exe from that repository's download URL; after a repo rename GitHub answers with a redirect to api.github.com/repositories/<id>, which it refuses. So releases now carry the installer under both names, the updater asks RamazanKara/Rubylight first and falls back to the old name, and the repository is renamed only after rc.30 is out, so rc.29 hosts can update to it in-app first.

Draft for the rc.30 notes: "Butterpollo is now Rubylight. The name nods to Ruby, the red-haired mascot of ATI's Radeon cards. Updating keeps your settings, paired devices, apps, service and install folder as they are; only the names Windows shows change. The installer is now rubylight-setup-<version>.exe; releases keep a butterpollo-setup copy so older hosts can update in-app. Links to the old GitHub address keep working."

Rubylight 2.2.9 short release streams (2026-10-12) #

The published 2.2.9 package at source commit d0bfcb5e764095250b8f0e22fd5a0cb3656d5ea6 passed all 36 short release acceptance streams: two runs of nine codec/output profiles on each of WGC and Desktop Duplication (DDX). These are new measurements under the conditions below; the earlier measurements in this work log remain historical.

Every numerical cell below preserves run 1 / run 2; no percentiles are pooled or averaged. Rates are the steady fresh-picture FPS (unique_fps, identical to steady_fps in every run). Picture age is the motion renderer's QPC timestamp through capture, encoding, loopback delivery and decoded-picture inspection on the same PC, before any client display presentation. Host mean/p99 are the steady host-processing latency reported in received frames, not total picture age or an encode-only timer. Decoder mean is the receiver's all-frame callback mean, including warmup, unlike the steady columns. All latency columns are milliseconds.

Capture Codec/output Fresh FPS, r1 / r2 Picture age mean, r1 / r2 Picture age p95, r1 / r2 Host mean, r1 / r2 Host p99, r1 / r2 Decoder mean, r1 / r2
WGC H.264 SDR 60.006 / 60.000 11.292 / 9.919 12.021 / 17.404 2.410 / 2.330 2.700 / 2.600 1.657 / 1.796
WGC HEVC SDR 60.002 / 60.002 11.283 / 12.579 12.251 / 17.498 2.133 / 2.487 2.600 / 2.700 2.383 / 2.458
WGC HEVC HDR 60.001 / 60.000 14.730 / 12.626 15.399 / 13.913 2.477 / 2.180 3.000 / 2.900 2.136 / 2.319
WGC AV1 SDR 60.004 / 59.999 15.025 / 9.745 16.062 / 10.635 1.926 / 1.808 2.500 / 2.500 1.279 / 1.163
WGC AV1 HDR 59.995 / 59.998 14.977 / 13.985 15.943 / 15.066 1.914 / 1.825 2.500 / 2.500 1.206 / 1.251
WGC PyroWave SDR 4:2:0 60.006 / 59.996 20.011 / 15.826 24.095 / 16.575 0.947 / 0.987 1.200 / 1.300 1.537 / 1.612
WGC PyroWave SDR 4:4:4 59.994 / 59.997 18.342 / 23.988 19.052 / 24.900 0.926 / 0.983 1.400 / 1.400 1.947 / 2.075
WGC PyroWave HDR 4:2:0 59.996 / 60.033 19.929 / 16.787 20.626 / 20.752 0.785 / 0.727 1.100 / 1.100 1.321 / 1.252
WGC PyroWave HDR 4:4:4 60.001 / 59.997 16.954 / 19.804 17.560 / 20.576 1.001 / 0.912 1.300 / 1.300 2.093 / 2.022
DDX H.264 SDR 60.001 / 60.004 13.155 / 9.936 13.955 / 11.852 2.236 / 2.307 2.600 / 2.800 1.719 / 1.735
DDX HEVC SDR 59.998 / 59.999 10.984 / 15.564 14.469 / 16.589 2.418 / 2.448 2.900 / 2.700 2.201 / 2.307
DDX HEVC HDR 60.001 / 59.995 10.139 / 14.121 11.041 / 15.064 2.429 / 2.455 2.800 / 3.000 2.143 / 2.212
DDX AV1 SDR 59.997 / 60.004 12.708 / 12.944 13.480 / 13.824 1.895 / 1.933 2.600 / 2.600 1.217 / 1.196
DDX AV1 HDR 60.004 / 60.000 7.313 / 9.589 8.317 / 10.378 1.839 / 1.949 2.500 / 2.600 1.139 / 1.149
DDX PyroWave SDR 4:2:0 59.997 / 59.998 22.378 / 20.871 22.970 / 23.022 1.012 / 1.063 1.500 / 1.500 1.530 / 1.525
DDX PyroWave SDR 4:4:4 59.996 / 60.002 20.376 / 23.396 23.619 / 24.157 1.064 / 1.067 1.600 / 1.600 2.034 / 2.049
DDX PyroWave HDR 4:2:0 60.000 / 60.002 16.299 / 19.265 17.019 / 19.877 0.788 / 0.877 1.200 / 1.200 1.360 / 1.380
DDX PyroWave HDR 4:4:4 60.008 / 59.997 17.557 / 17.030 18.754 / 18.749 1.065 / 1.068 1.300 / 1.400 2.122 / 2.171

All 36 results have passed=true, zero decoder failures, every received frame decoded, continuous audio tone, zero source underruns, 100% motion coverage, and zero steady assembly intervals over 1.5 frame periods. PyroWave reported zero partial frames. These short acceptance results do not measure remote network behavior, real-client presentation/display latency, or long-session stability; they do not replace the older native-HDR or real-client figures. No competitor measurements were changed.

Provenance: the public package's butterpollo.exe SHA-256 is f38f3a0056dd64fc015af864b1bcf83c7e56bf388f97d6a6548398030665f4b7. The existing conventional and PyroWave receiver fixture SHA-256 values are, respectively, 629c0503e8816189043853ab13a6f83ea8e8236a8675a93cd779117791cbb57f and 72f01704a962cc6ab39e113f1acec1fcbc9fab7c57f17de798590a6702b58df0. The run used the unchanged release e2e.py, e2e_host.py, e2e_result.py and tests/interop.py acceptance checks; their hashes and the motion/audio fixture hashes are in metadata.json. Raw evidence is at C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\current-e2e-20261012T045200344567Z (started 04:52:00 UTC, ended 05:03:19 UTC), with per-run commands, capture source, host/client logs and result JSON. The extracted 36-result set is C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\e2e-completed.json.

Current VRR, motion-rate and keyframe controls #

The same release, host, physical SDR source and software HEVC receiver also passed 12/12 control streams, two runs per capture/variant. Output was 2560x720 at 60 FPS and 20,000 kbps, with 12 seconds requested and the same 3-second warmup. VRR request asks for VRR through the client protocol; it is not a measurement of physical-panel presentation. At-rate motion renders the timestamp strip at 60 Hz. 16 keyframes uses that same motion rate plus 16 recovery/keyframe requests. Values remain r1 / r2, in ms except FPS.

Capture Variant Steady received FPS Fresh FPS Picture age mean Picture age p95 Host mean Host p99
WGC VRR request 60.000 / 60.003 60.000 / 60.003 11.866 / 17.785 12.709 / 18.649 2.056 / 2.062 2.300 / 2.600
WGC At-rate motion 59.995 / 60.005 59.995 / 60.005 21.185 / 16.160 22.113 / 17.018 2.478 / 2.438 2.700 / 2.600
WGC 16 keyframes 59.997 / 59.999 58.629 / 59.999 17.006 / 22.973 18.978 / 24.703 2.469 / 2.469 2.700 / 3.000
DDX VRR request 59.999 / 60.000 59.999 / 60.000 14.358 / 15.599 17.981 / 16.226 2.268 / 2.062 2.700 / 2.300
DDX At-rate motion 60.000 / 60.004 60.000 / 60.004 18.041 / 13.033 18.923 / 13.964 2.456 / 2.421 3.000 / 2.800
DDX 16 keyframes 60.008 / 60.031 58.309 / 58.559 10.684 / 12.449 12.266 / 15.812 2.456 / 2.407 3.000 / 2.800

All four request runs received and decoded all 16 requested keyframes, and host claim traces reported zero unchanged pictures encoded again. Request latency below is the existing receiver's request-to-keyframe-arrival timer; it does not include display presentation.

Capture Request mean, r1 / r2 Request p95, r1 / r2 Request max, r1 / r2
WGC 3.780 / 4.068 6.514 / 6.572 7.061 / 8.039
DDX 3.898 / 3.581 6.540 / 4.899 7.963 / 5.657

Receiver picture repeats/skips were 12/12 then 0/0 for WGC and 15/15 then 13/13 for DDX. The at-rate fixture's phase can itself repeat/skip pictures; these counts do not override the host claim trace or identify a host defect. The request runs had 10 / 10 (WGC) and 7 / 5 (DDX) assembly intervals above 25 ms and still passed the unchanged keyframe-specific gap allowance. The run-to-run picture-age variation is visible rather than folded into one mean. Raw stage: C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\current-controls-20261012T050341865344Z.

Current compute-conversion setting under GPU load #

All 8/8 DDX HEVC HDR-output streams passed at 1920x1080, 60 FPS and 20,000 kbps, with 20 seconds requested and 3 seconds of warmup. The source remained physical 5120x1440 SDR at 240 Hz, converted into HDR output. This differs from the historical native-HDR virtual display at 120 Hz; these settings comparisons cannot establish a before/after release change against that older measurement. The setting is gpu_compute_conversion; run 1 tested false then true, and run 2 reversed that order within each load condition. Values are r1 / r2, with steady fresh FPS and ms distributions.

Compute setting Load Fresh FPS Picture age mean Picture age p95 Host mean Host p99
false Idle 59.999 / 60.000 13.186 / 14.745 18.991 / 15.549 2.595 / 2.663 3.200 / 3.200
true Idle 60.001 / 59.999 13.615 / 15.475 14.475 / 16.155 2.509 / 2.487 3.000 / 2.900
false GPU load 60.003 / 60.000 21.226 / 19.821 25.302 / 22.031 4.855 / 2.612 10.400 / 4.000
true GPU load 60.000 / 60.003 19.398 / 18.082 25.269 / 21.286 2.290 / 4.575 2.700 / 5.600

The established load command was gpu_load.exe 45 1000 60 200: a 45-second 2560x1440 render target, 1,000 total shader iterations split over 200 draws, capped at 60 FPS. It started after host capability probing, stayed alive through the stream, and exited naturally before the next case. Its own final summary recorded 2,701 frames and 60.0 FPS in each of the four runs; this is load-fixture throughput, not a GPU-utilization percentage.

Compute setting under load Load frame p50, r1 / r2 (ms) Load frame p95, r1 / r2 (ms)
false 9.551 / 9.794 10.111 / 10.458
true 9.358 / 9.871 9.994 / 10.032

The loaded host-time ordering reversed between runs: false/true means were 4.855/2.290 ms in run 1 and 2.612/4.575 ms in run 2. Picture-age tails also varied. These two repeats support the recorded acceptance and current conditions, not a stable general performance gain. The false/load first run had one assembly interval above 25 ms; the other seven had none. All eight fully decoded received frames, retained continuous audio and 100% motion coverage, with zero decoder failures or tone-source underruns. Raw stage: C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\current-compute-20261012T050652545434Z.

Physical-source 240 FPS saturation attempts #

Both final DDX HEVC SDR runs at 5120x1440, 240 FPS, 150,000 kbps failed the unchanged release acceptance gates. Each requested 20 seconds with the same 3-second warmup and software receiver, using the physical 5120x1440 SDR 240 Hz source. No synthetic GPU load was added. These are complete loopback capture/encode/transport/decode measurements; the result does not isolate a host-only or decoder-only bottleneck and is not a real-client capacity limit.

Run Steady received FPS Fresh FPS Picture age mean (ms) Picture age p95 (ms) Host mean (ms) Host p99 (ms) Decoder all-frame mean (ms)
1 107.922 107.507 391.645 595.534 8.791 9.500 8.784
2 106.570 105.799 393.770 605.422 8.834 9.500 8.917

Picture age reached 645.547 / 669.399 ms maximum; assembly-interval p99 was 44.074 / 44.533 ms and maximum 51.602 / 52.893 ms. The receiver exited 1 in both cases: steady-rate, delivery-gap and fresh-picture gates failed. All 2,218 / 2,189 received frames decoded without decoder errors, and audio remained continuous with no tone-source underruns; those checks do not turn the failed 240 FPS attempt into a pass. Only final result files were included, after both cases and the stage metadata recorded completion. Raw stage: C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\current-saturation-20261012T051136906993Z.

Supplemental capacity and 120 FPS streams #

The completed supplemental stage recorded 10/12 passing checks: both repeated-frame capacity probes completed, and eight of ten loopback streams passed. Both 4K120 HEVC streams failed. This remains release 2.2.9 on the same physical 5120x1440 SDR 240 Hz source and driver recorded above. HDR continues to mean SDR-source conversion to HDR output. Source and driver conditions differ from historical measurements, so no historical speedup or regression is calculated.

The additional HEVC HDR-output capacity probes used WGC, 2560x1440, 40,000 kbps and a configured 120 FPS, but ran unpaced on a repeated frame for approximately eight seconds after 20 warmup frames. Each path has one run, not a pair. The --cpu path still uses AMF hardware encoding and includes CPU conversion/upload in its frame-processing call; it is not the isolated CPU color-conversion benchmark. These figures exclude live capture, network, decode and display. The submission-to-observed-output column is the probe's local pipeline observation, not client picture age.

Input path Completed frames Measured seconds Encoded FPS Encode-call mean / p99 (ms) Submission-to-observed-output mean / p99 (ms)
GPU 3299 8.000453 412.352 2.425 / 4.037 20.741 / 21.707
--cpu 486 8.015478 60.633 16.493 / 17.552 32.941 / 34.837

The ten WGC streams each requested 20 seconds at 120 FPS, with the same 3-second receiver warmup. Conventional HEVC/AV1 used the software decoders and four-thread request described above; PyroWave used its Vulkan decoder with synchronous 8-bit CPU readback. The profiles were PyroWave HDR 4:4:4 at 1080p/200,000 kbps, HEVC/AV1 HDR at 1440p/50,000 kbps, and HEVC/AV1 HDR at 4K/80,000 kbps. Values are r1 / r2. Age and host columns are steady milliseconds; decoder mean includes all frames and warmup.

Output profile Outcome Steady received FPS Fresh FPS Picture age mean Picture age p95 Host mean Host p99 Decoder mean
PyroWave HDR 4:4:4 1080p120 pass / pass 119.993 / 120.000 119.993 / 120.000 15.680 / 15.638 16.639 / 16.907 0.675 / 0.675 1.000 / 1.000 1.874 / 1.874
HEVC HDR 1440p120 pass / pass 120.000 / 119.996 120.000 / 119.996 14.656 / 14.524 15.485 / 15.819 3.108 / 3.070 3.500 / 3.600 4.182 / 4.055
AV1 HDR 1440p120 pass / pass 120.000 / 119.998 120.000 / 119.998 12.322 / 13.012 13.266 / 14.723 2.587 / 2.580 3.100 / 3.000 2.412 / 2.584
HEVC HDR 4K120 fail / fail 97.516 / 95.425 97.457 / 95.425 548.206 / 525.010 887.134 / 892.375 5.777 / 5.716 6.400 / 6.300 10.113 / 10.235
AV1 HDR 4K120 pass / pass 120.003 / 119.997 120.003 / 119.997 17.257 / 16.817 18.962 / 18.712 4.893 / 4.720 5.500 / 5.200 4.769 / 4.697

Both HEVC 4K runs exited 1 and failed receiver interoperability, steady-rate, delivery-gap and fresh-picture gates. Their picture-age maximum was 957.890 / 967.767 ms; assembly-interval p99 was 14.967 / 17.407 ms and maximum 45.352 / 44.905 ms, with 58 / 85 intervals over 1.5 frame periods. Those are full-loop saturation results, not a measurement isolating the encoder or software decoder, and not a real-client display/capacity limit. All ten streams decoded every received frame without decoder failures and kept continuous audio with zero source underruns. PyroWave reported zero partial or host-replaced frames. The existing acceptance gates were unchanged; frame completeness and audio do not erase the two failed rate/gap outcomes. Full-precision capacity JSON and per-run commands, source checks, logs and final results are retained in C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\current-supplemental-20261012T051226067741Z.

Current AMF default quality #

The October 12 release-source AMF quality batch completed 32/32 cases: 16 settings for each of two clips, with 180 encoded and scored frames per case, starting three seconds into each clip. It uses the release 2.2.9 source commit and RX 7900 XT/driver recorded above. The exact-source amf_quality probe used current defaults with no overrides, and the existing fixture's Lanczos fill resize/crop and configured 120 FPS. These are single runs per clip and setting, not repeated trials or confidence intervals. They measure file encode/decode quality and local non-IDR encode latency; they do not include capture, transport, presentation or encoder queueing.

Quality and actual-bitrate cells below are TMNT / GoldenEye, referring to Teenage Mutant Ninja Turtles HD.mp4 and GoldenEye XBLA.mp4, respectively. The two timing columns give mean / p95 / p99 milliseconds separately for each clip and exclude IDR frames. Actual Mbps includes all 180 encoded frames, including the IDR, scaled to the configured 120 FPS; it is not a wall-clock throughput measurement. Requested and actual rates can differ substantially for these short clips. Full precision remains in the raw JSON.

Output / codec Resolution Requested Mbps Actual Mbps (TMNT / GoldenEye) VMAF (TMNT / GoldenEye) PSNR-Y dB (TMNT / GoldenEye) TMNT encode mean / p95 / p99 (ms) GoldenEye encode mean / p95 / p99 (ms)
SDR HEVC 1968x2184 30 28.139 / 28.780 93.917 / 99.192 46.655 / 43.843 4.741 / 5.647 / 12.684 4.767 / 5.361 / 12.588
SDR AV1 1968x2184 30 24.264 / 26.167 93.833 / 98.739 46.816 / 45.482 4.113 / 4.928 / 12.104 4.158 / 5.297 / 12.097
SDR HEVC 2560x1440 20 22.718 / 19.598 92.602 / 97.663 42.274 / 39.211 4.140 / 4.894 / 11.239 4.221 / 4.907 / 12.848
SDR AV1 2560x1440 20 21.195 / 17.630 92.830 / 97.716 42.830 / 40.533 3.673 / 4.243 / 11.622 3.646 / 4.281 / 10.831
SDR H264 2560x1440 20 22.538 / 19.285 90.460 / 85.368 41.434 / 36.310 4.067 / 5.005 / 11.299 4.093 / 4.977 / 11.411
SDR HEVC 1968x2184 80 74.412 / 80.896 94.905 / 99.841 48.324 / 48.737 4.783 / 5.457 / 12.460 4.881 / 6.019 / 12.714
SDR AV1 1968x2184 80 39.234 / 62.251 94.548 / 99.387 47.898 / 50.089 4.214 / 4.747 / 13.126 4.203 / 5.412 / 12.259
SDR HEVC 2560x1440 50 55.221 / 51.447 94.301 / 99.063 44.289 / 42.876 4.157 / 4.610 / 11.074 4.187 / 5.160 / 11.283
SDR AV1 2560x1440 50 40.662 / 41.199 94.105 / 98.782 44.314 / 44.092 3.708 / 4.466 / 11.072 3.666 / 4.738 / 10.948
SDR H264 2560x1440 50 48.120 / 45.155 93.663 / 98.488 43.870 / 41.397 4.159 / 4.902 / 11.496 4.124 / 4.805 / 11.370
HDR HEVC 1968x2184 30 16.052 / 30.373 97.168 / 98.437 54.178 / 48.750 5.544 / 6.083 / 16.215 5.610 / 5.887 / 16.971
HDR AV1 1968x2184 30 14.054 / 29.668 96.726 / 98.243 53.963 / 49.804 4.976 / 5.362 / 16.519 5.011 / 5.273 / 16.348
HDR HEVC 2560x1440 20 16.640 / 20.291 97.112 / 96.691 51.011 / 44.471 4.980 / 5.358 / 14.594 5.032 / 5.507 / 14.888
HDR AV1 2560x1440 20 13.464 / 20.016 96.575 / 97.008 50.548 / 45.615 4.478 / 4.867 / 14.148 4.477 / 5.045 / 14.232
HDR HEVC 1968x2184 80 43.232 / 80.063 98.280 / 99.925 58.884 / 53.664 5.623 / 5.876 / 16.634 5.758 / 6.527 / 16.723
HDR AV1 1968x2184 80 23.933 / 67.338 97.708 / 99.556 56.855 / 54.202 4.993 / 5.443 / 16.146 5.015 / 5.254 / 15.779
SDR references use limited-range BT.601 yuv420p. The HDR rows use the
existing SDR-to-HDR 203-nit-white fixture: the SDR clips are linearized,
converted to scRGB FP16 with white scaled by 203/80, and scored against a
BT.2020 PQ limited-range 10-bit reference. They are not native-HDR footage.
Default VMAF applied to PQ code values, and PSNR-Y in that representation,
are fixture scores rather than perceptual HDR validation; no SDR/HDR score
advantage or historical quality delta is inferred from them.

Scoring used FFmpeg 9.0.2-full_build-www.gyan.dev, built with GCC 16.2.0, and its default libvmaf model. The preserved legacy graph could repeat the last decoded picture past the intended endpoint and scored 183 pictures for 180 encoded frames in the first attempt. The corrected graph trims both decoded and reference branches with trim=end_frame=180, aligns their time bases, uses shortest=1:repeatlast=0 for VMAF and PSNR, and explicitly maps both video outputs without audio or output frame duplication. Both the SDR and HDR validation cases passed in amf-scoring-validation, followed by a fresh complete batch. Every case was checked for exactly 180 encoded frames, 180 VMAF records and 180 PSNR per-frame records. The aborted first batch amf-default-quality-20261012T052453Z remains as diagnostic evidence and is excluded from this table. Historical scalar and competitor results remain unchanged; the scoring correction and current source/driver prevent a causal before/after claim from these measurements.

The probe SHA-256 is 6ae7ccb1001620d3224bd60e1b3871ebbe379a536d844d869441504ae86cc99a; FFmpeg SHA-256 is 589e50b766d251afdf181dd664d40bd94407e200b019989fd7468c7d118a28d0. The runner hash is 4dd91d1977bd0677766c68955bd7eceb831fb0a7fcbf44f1c7d6bfe52464fd86. The manifest preserves both input-clip hashes, the original SDR/HDR case CSV and filter-script hashes, every command, per-case logs and bitstream hashes. Raw batch: C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\amf-default-quality-20261012T052917Z; scoring validation: C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\amf-scoring-validation.

Current AMF quality settings comparison #

The October 12 release-source supplement completed 116/116 settings cases, compared with the 32/32 current defaults above. Each arm encoded and scored exactly 180 pictures with the same feed and scoring commands as its saved default: 20,880 supplemental pictures and 5,760 baseline pictures, with matching VMAF and PSNR record counts. Passing means successful encoding, decoding, scoring and applicable setting readback; the fixture has no minimum VMAF or PSNR threshold.

All cases use the RX 7900 XT and release 2.2.9 source/driver recorded above, at 120 FPS. SDR covers 1968x2184 HEVC/AV1 at 30 and 80 Mbps and 2560x1440 HEVC/AV1/H.264 at 20 and 50 Mbps. TMNT has six alternatives per case (60 runs); GoldenEye has CBR and the opposite AQ state (20 runs). Converted HDR covers HEVC/AV1 at 1968x2184, 30/80 Mbps, and 2560x1440, 20 Mbps: both clips have balanced, CBR and AQ-off alternatives (36 runs).

Every range below is alternative minus its corresponding current default across the stated cases. Requested bitrate is fixed; actual bitrate is measured rather than normalized. Frame-size percentages use the p99 of all 180 encoded pictures, including IDR. The default is speed, ultra-low-latency usage, latency-constrained VBR, AQ off for H.264 and AQ on for HEVC/AV1. The four historical H.264 AQ-off alternatives therefore become explicit AQ-on alternatives in this current batch.

SDR alternative Cases HEVC delta VMAF AV1 delta VMAF H.264 delta VMAF All-frame p99 size change, all tested codecs
amd_quality=balanced 10 -0.418 to -0.080 0 +0.031 to +0.143 -2.79% to +1.95%
amd_quality=quality 10 -0.416 to -0.081 -0.017 to +0.072 +0.067 to +0.251 -3.45% to +1.59%
amd_rc=cbr 20 -0.045 to +0.434 -0.003 to +0.318 +0.130 to +1.785 +7.73% to +135.89%
amd_vbaq=disabled 16 -0.002 to +0.436 +0.001 to +0.230 not run; already default -1.24% to +11.58%
amd_vbaq=enabled 4 not run; already default not run; already default -11.887 to -0.060 -10.25% to +2.11%
amd_high_motion_quality_boost=enabled request 10 0 to +0.051 0 to +0.036 0 -0.53% to 0%
amd_usage=lowlatency 10 0 0 -0.007 to +0.056 -0.08% to +1.04%

The 10-case SDR alternatives cover TMNT only (four HEVC, four AV1 and two H.264 cases). CBR and AQ cover both clips (eight HEVC, eight AV1 and four H.264 cases). Each 12-case HDR alternative below has six cases per codec, covering both clips.

Converted-HDR alternative Cases HEVC delta VMAF AV1 delta VMAF All-frame p99 size change, both codecs
amd_quality=balanced 12 -0.403 to -0.047 -0.000045 to +0.052 -3.25% to +4.25%
amd_rc=cbr 12 -0.274 to +0.719 -0.156 to +0.362 +9.54% to +44.50%
amd_vbaq=disabled 12 -0.078 to +1.086 +0.005 to +0.482 -4.34% to +3.62%

AQ conclusions: H.264 AQ-on reduced VMAF in all four cases: at 20 Mbps, by 1.437 TMNT / 11.887 GoldenEye points, and at 50 Mbps by 0.060 / 0.409. It also reduced PSNR-Y by 0.264-2.169 dB despite using 1.49-9.32% more actual bitrate. This supports keeping H.264's current AQ-off default for these clips. HEVC AQ-off was mixed: seven of eight SDR scores and four of six converted-HDR scores increased, with the full ranges above. AV1 AQ-off increased VMAF in all 14 tested cases, by 0.0009-0.2301 SDR and 0.0051-0.4822 converted-HDR points. These small AV1 score gains must not be described as an unchanged score or erased by the older findings; they also do not establish a universal visual optimum. Converted-HDR PSNR-Y changes with AV1 AQ-off ranged from -0.141 to +0.225 dB, so the metrics do not uniformly favour the same choice.

CBR increased all-frame p99 size in every case. Its actual-bitrate changes were -2.31% to +17.29% SDR and +2.03% to +9.69% converted HDR; VMAF changes were codec/content dependent. The observed larger encoded pictures do not by themselves measure network pacing or picture age.

For timing context, the quality preset's SDR non-IDR encode-mean deltas were +0.705 to +0.844 ms HEVC, +5.595 to +6.718 ms AV1 and +2.002 to +2.067 ms H.264; corresponding p99 deltas were +0.495 to +0.843, +4.823 to +6.747 and +1.663 to +1.808 ms. Low-latency usage had observed mean deltas of +5.489 to +8.245 ms and p99 deltas of +5.299 to +9.088 ms across codecs, with the small or zero score changes above. CBR SDR means ranged from -0.123 to +0.169 ms, but one AV1 p99 delta reached +7.379 ms. Full per-case mean/p95/p99 values and deltas are retained rather than interpreting that tail as a repeatable setting effect. Baselines ran before the supplement, without randomization or interleaved controls: none of these timing deltas is a matched performance comparison.

Effective-setting logs verify 106/116 requested properties. The other 10 HMQB requests have no property readback in this release; none logged an AMF-setting-unavailable warning. Their completed scores describe the requested configuration, but neither completion nor absent warnings prove HMQB applied. The summary preserves the exact property, requested value, observed value, full effective-settings line and warnings for every case. There were no setting mismatches, driver-unavailable warnings or interrupted attempts in this batch.

These remain file encode/decode measurements, excluding capture, transport and display. The converted-HDR input is SDR with 203-nit white, scored in PQ code values with the default VMAF model; it is not native-HDR perceptual validation. All current arms start at 3 seconds, including GoldenEye HDR, whose historical fixture started at 5 seconds. Together with the corrected 180-picture scorer, this prevents a matched historical quality delta. The October 8 tables and competitor results remain unchanged.

Full-precision results for all 116 alternatives and their exact 32 control matches are indexed in work\amf-settings-quality-summary.json, generated by work\summarize_amf_settings_quality.py. Raw evidence is under work\measurements\amf-settings-quality-20261012T093917Z; the controls are work\measurements\amf-default-quality-20261012T052917Z. The summary verifies manifest/run/result agreement, control hashes, effective-setting log hashes and both 180-record score files. It retains all commands through the manifests, clip/probe/FFmpeg/fixture identities, per-case recorded evidence hashes, actual bitrate, VMAF, PSNR-Y, non-IDR encode timings and both IDR-inclusive and non-IDR frame-size p99 values.

Current default pacing in complete loopback streams #

The published 2.2.9 host passed 4/4 default-pacing streams on October 12, 07:43–07:45 CEST: two runs each of 2560x720 HEVC SDR at 60 FPS / 60,000 kbps and 2560x720 PyroWave SDR at 120 FPS / 800,000 kbps. These use the same host, driver and physical 5120x1440 SDR / 240 Hz source recorded above, with WGC, the fixture's minimum-FPS target matching the stream rate, and no pacing overrides or synthetic load. Runs alternate HEVC then PyroWave in each repeat. Each requests 30 seconds; the receiver excludes three seconds of warmup from steady rate, picture-age and host distributions. Observed steady windows were 26.748–26.917 seconds.

These are independent loopback capture/encode/transport/decode streams. HEVC uses the FFmpeg software decoder with four requested decoder threads. PyroWave uses its Vulkan decoder, bitstream 186f0393, encrypted record framing and synchronous 8-bit CPU-buffer readback. Picture age includes the timestamped source picture through decoding/readback on the same PC; it excludes physical network transit and client display. Decoder means below include warmup and all frames, unlike the steady picture-age/host metrics. They do not establish real-client or Wi-Fi latency, native-HDR correctness, or a causal change from a historical pacing build.

Profile / run Decoded / received frames Steady received FPS Fresh FPS Picture age mean / p95 / p99 / max (ms) Host mean / p99 (ms) Decoder mean (ms)
HEVC / 1 1792 / 1792 60.007 60.007 13.206 / 15.298 / 15.740 / 22.480 2.396 / 2.900 2.145
HEVC / 2 1787 / 1787 60.006 60.006 13.084 / 18.426 / 18.976 / 19.668 2.392 / 2.600 2.751
PyroWave / 1 3593 / 3593 120.002 119.742 14.342 / 15.262 / 17.120 / 22.974 0.864 / 1.300 1.197
PyroWave / 2 3593 / 3593 119.999 119.999 14.423 / 15.248 / 17.669 / 22.704 0.865 / 1.200 1.230

All four passed the unchanged release/interop acceptance gates, with 100% motion coverage, no video decode failures, continuous audio and zero tone-source underruns. No steady assembly interval exceeded 1.5 frame periods. Assembly-interval p99 / maximum were 20.957 / 21.384 ms and 20.359 / 21.370 ms for HEVC, then 10.357 / 12.005 ms and 9.874 / 11.535 ms for PyroWave. Both PyroWave runs reported zero partial frames and zero host-replaced frames; the first run's lower fresh-picture rate remains visible in the table. This short check does not establish long-session stability or a display-presentation bound.

The package and both receiver hashes match the 36-stream release batch above. The task-local runner work\run_default_pacing_streams.py has SHA-256 46ecc91de62b8814191cf16fae6a799fd229502fffcfba35734ed5be932ac240. It uses the unmodified release e2e.py and tests/interop.py with the modes, bitrates and 30-second duration above. Exact per-case commands, source checks, host/client logs and final result.json files are retained alongside the stage manifest at C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\current-default-pacing-streams-20261012T054303698009Z.

Audio under load and 80 ms outage recovery #

The published 2.2.9 host completed 11 serial 30-second loopback tests on October 12, 07:50–07:57 CEST. Audio was continuous in all 11, with nonzero received packets and tone blocks, peaks of 0.012907–0.012912, and zero tone-source underruns. Full-stream acceptance was 6 passed / 5 failed; audio continuity does not turn a video failure into a pass. All received video frames decoded without reported decoder failures, with 100% motion coverage. This is the independent FFmpeg software receiver with four decoder threads, the existing virtual-speaker tone and moving strip, and WGC on the same 5120x1440 SDR / 240 Hz physical source. It does not measure a laptop, Wi-Fi, client presentation or native HDR.

The eight audio load cases use 2560x720 SDR at 60 FPS / 20,000 kbps, with minimum_fps_target=60 and default pacing. CPU load means 16 separate normal-priority Python busy processes, each bounded to 180 seconds and stopped after stream teardown. The historical batch.py is unavailable, so this substitute does not establish an exact historical CPU-load comparison. GPU load is the archived 2560x1440 fixture, gpu_load.exe 45 1000 0 200: 45 seconds, 1,000 total iterations, uncapped and 200 draws. Load starts only after encoder capability detection; every requested CPU/GPU load remained alive through stream teardown. GPU loads then completed naturally with exit status zero. Combined means both loads, not an equal GPU workload rate.

There is one run per condition. Steady rates and picture age exclude three seconds of warmup; audio packet/tone-block counts cover the full receiver run. Picture age is measured only for received, decoded pictures and does not include the time spent displaying an old picture across a lost-frame gap. The gap measurements below retain that separate failure mode.

Case Full-stream result Audio packets / tone blocks Steady received / fresh FPS Picture age p95 / max (ms)
H264 / idle passed 5831 / 10862 60.000 / 60.000 14.884 / 16.849
H264 / cpu passed 5709 / 10618 60.055 / 59.753 19.546 / 39.749
H264 / gpu failed 5831 / 10862 60.003 / 48.859 67.954 / 96.313
H264 / combined passed 5728 / 10656 59.993 / 57.981 26.802 / 69.849
HEVC / idle passed 5811 / 10822 60.000 / 60.000 15.218 / 16.890
HEVC / cpu passed 5731 / 10662 60.021 / 59.490 20.980 / 51.216
HEVC / gpu failed 5805 / 10810 60.002 / 47.508 70.125 / 113.416
HEVC / combined passed 5747 / 10694 59.957 / 58.441 27.749 / 47.315
H264 / 80 ms outage failed 5833 / 10866 113.671 / 113.671 14.008 / 32.977
HEVC / 80 ms outage failed 5827 / 10854 113.080 / 113.080 15.309 / 39.876
AV1 / 80 ms outage failed 5809 / 10818 113.573 / 113.573 13.392 / 31.367

Both GPU-only cases failed “moving pictures were missing or repeated too often”: fresh-picture rates of 48.859 and 47.508 FPS were below the unchanged 90%-of-requested-rate gate, despite receiving about 60 FPS. Their assembly intervals exceeded 1.5 frame periods 4 and 8 times respectively; this did not independently fail the existing gap gate. The passing combined cases are not evidence that CPU contention improves the stream: their GPU load completed substantially fewer frames. Actual whole-load throughput and frame timings were:

Stream / load Load frames Load fps Load frame p50 / p95 (ms)
H264 / GPU only 7699 171.1 5.714 / 6.535
H264 / combined 3001 66.7 10.299 / 37.254
HEVC / GPU only 7723 171.6 5.694 / 6.498
HEVC / combined 3051 67.8 11.384 / 35.441

The three recovery cases use 2560x1440 SDR at 120 FPS / 80,000 kbps, minimum_fps_target=120 and pacing_max_bitrate_kbps=160000, without the CPU/GPU load. The existing BUTTERPOLLO_TEST_SEND_OUTAGE=1500:80:air fixture simulates an 80 ms video-send outage every 1.5 seconds; BUTTERPOLLO_TEST_RFI=1 advertises reference invalidation. This is an injected loopback failure, not measured radio loss. No normal acceptance gate was relaxed or given a forced-outage allowance. All three retained the same failures: receiver interoperability failed, steady frame rate is outside 97–103% of the requested rate, and frame delivery has excessive gaps. Their steady long-interval counts were 33 / 36 / 64 for H264 / HEVC / AV1, with maximum intervals of 97.080 / 107.072 / 95.806 ms.

recovery_gaps.py analyzes the complete timing CSV, including warmup. A stall requires both missing wire-frame indices and an arrival gap exceeding twice the median frame period. Thus its stall count differs from the receiver's steady intervals-over-1.5-period counter. Gap length includes the outage and recovery; missing frames are gaps in wire-frame indices, not UDP packet-loss counts. Frame sizes below use 1,024 bytes per KiB.

Codec Decoded / received Stalls Gap mean / p95 / max (ms) Missing frames Stalls ended by keyframe Median / recovery-mean frame size (KiB)
H264 3395 / 3395 19 95.7 / 97.0 / 97.1 190 19 14.8 / 148.8
HEVC 3369 / 3369 19 105.2 / 106.9 / 107.1 208 19 12.8 / 184.4
AV1 3381 / 3381 19 88.0 / 88.1 / 95.8 191 0 7.4 / 21.0

H264 and HEVC ended all 19 detected stalls with a keyframe. AV1 ended none with a keyframe; its client log records post-invalidation P-frames. That observed recovery behavior does not make AV1's failed normal full-stream result a pass or establish a historical recovery improvement.

Raw evidence is retained in C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\audio-starvation-recovery-20261012T055019Z. It includes all 11 run.json and unmodified e2e result.json files, source checks, host/client/tone/load logs, three recovery CSVs and their gap reports, and the manifest with exact commands and fixture hashes. The package and receiver hashes match the current release stream batch above. Reproduce with the task-local work\run_audio_starvation.py, SHA-256 c93062ba316e84421c4aff2ca76184c5eef4f68597d4b2e3dee04ffe9c9fd0a8.

IDR requests on the physical desktop without imposed motion #

All 12/12 IDR/interoperability checks passed on the published 2.2.9 host on October 12, 07:58–08:04 CEST: two repeats of H264, HEVC and AV1 through each of WGC and DDX. The same physical 5120x1440 SDR / 240 Hz desktop remained live, with no imposed motion fixture and no guarantee that its content was frozen. Applications could redraw during a run. These are not matched reproductions of an earlier static scene or measurements of native HDR, laptop/Wi-Fi delivery or display presentation.

Each run requests 25 seconds, 60 FPS / 20,000 kbps SDR, with minimum_fps_target=20. H264/HEVC output is 1920x1080; AV1 output is 2560x1440. The independent FFmpeg software receiver requests four decoder threads, excludes two seconds of warmup, and requests 10 IDRs at the fixture's default 1.5-second spacing. Every run logged 10 requested, 10 sent, 10 corresponding keyframes observed and 10 decoded, with no video decode failures. The arrival statistics below measure request to complete keyframe enqueue at the receiver, excluding keyframes already in flight; they are not request-to-display latency. The existing ten-sample percentile calculation is retained.

Pass here means the unchanged interop.py and receiver completed their IDR/decode/audio checks with INTEROPERABILITY PASS. The task-local worker does not run the release e2e_result.py motion/freshness evaluator and does not request minimum-FPS or moving-picture checks for this source. Observed cadence is reported without relabelling this as a complete moving-stream acceptance test. Steady windows were 22.800–22.900 seconds; frame and audio packet totals cover the full receiver run. Long intervals exceed 1.5 frame periods, and their counts and tails remain visible below.

Capture / codec / repeat Decoded / received frames Steady FPS IDR arrival mean / p95 / max (ms) Long intervals Arrival interval p99 / max (ms) Audio packets
WGC / H264 / r1 1491 / 1491 60.000 3.795 / 5.541 / 5.581 4 18.194 / 28.128 4823
WGC / HEVC / r1 1492 / 1492 60.001 3.903 / 6.426 / 7.307 3 21.357 / 27.679 4827
WGC / AV1 / r1 1492 / 1492 59.999 3.978 / 5.277 / 6.091 2 18.054 / 27.913 4823
DDX / H264 / r1 1496 / 1496 60.001 4.181 / 6.043 / 6.804 5 17.918 / 28.545 4829
DDX / HEVC / r1 1491 / 1491 59.999 3.960 / 5.828 / 6.714 4 17.813 / 28.426 4807
DDX / AV1 / r1 1496 / 1496 60.000 3.731 / 3.658 / 5.643 4 17.774 / 29.186 4827
WGC / H264 / r2 1492 / 1492 59.999 3.993 / 5.467 / 6.183 5 17.852 / 28.377 4827
WGC / HEVC / r2 1493 / 1493 60.000 3.369 / 3.244 / 6.414 4 17.599 / 28.389 4827
WGC / AV1 / r2 1492 / 1492 59.999 4.072 / 5.349 / 6.246 3 20.242 / 33.106 4827
DDX / H264 / r2 1495 / 1495 60.000 3.945 / 5.696 / 7.121 4 17.868 / 28.812 4823
DDX / HEVC / r2 1497 / 1497 60.000 3.484 / 3.409 / 6.840 4 17.889 / 28.669 4831
DDX / AV1 / r2 1496 / 1496 60.000 4.246 / 6.623 / 7.639 5 20.515 / 28.770 4827

All 12 runs recorded continuous audio, 8,814–8,862 measured tone blocks and zero tone-source underruns. No synthetic CPU or GPU load was added. The capture backends and SDR source dimensions were confirmed in host logs. Neither fresh-picture rate nor picture age is established by this fixture.

Raw evidence is retained in C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\remaining-static-idr-20261012T055813241409Z: per-case run/source/result records, host/client/tone logs, receiver timing and timing.csv.idr.csv request/arrival/decode records, plus stage metadata, hardware and the 12-entry summary. Package and conventional receiver hashes match the current release batch above. Reproduce with work\run_remaining_streams.py --stages static-idr; the runner SHA-256 is 2660304689116b0e6f7f0e1abb2d6336bee86033ce818693f3e5672731ec554f.

Ninety-second control and 20 ms outage streams #

Eight published-2.2.9 runs completed on October 12, 08:04–08:16 CEST: two repeats of HEVC and AV1, each with a control and an injected-outage run. All four controls passed; all four outage runs failed the unchanged normal-stream gate with “frame delivery has excessive gaps”. Every run used 2560x1440 SDR at 120 FPS / 50,000 kbps, WGC on the same physical 5120x1440 SDR / 240 Hz source, minimum_fps_target=120, default pacing and the moving-strip/audio-tone fixtures. No synthetic CPU or GPU load was added.

Both controls and outage runs advertise reference invalidation through BUTTERPOLLO_TEST_RFI=1. Only outage runs set BUTTERPOLLO_TEST_SEND_OUTAGE=1500:20:air, simulating 20 ms of video-send loss every 1.5 seconds. The independent FFmpeg software receiver uses four decoder threads over loopback. This does not measure real Wi-Fi loss, laptop hardware decoding, client display or native HDR. The 90-second runs have 86.797–86.842-second steady windows after three seconds of warmup.

All received frames decoded with zero reported decoder failures, 100% motion coverage, continuous audio and zero tone-source underruns. Received and fresh FPS are identical in these results. Frame/audio totals cover the entire receiver run; cadence statistics are steady-state. Long intervals exceed 1.5 frame periods. The outage rates remain within the normal 97–103% FPS gate; their gaps fail independently. No forced-outage exception was added.

Codec / condition / repeat Full-stream result Decoded / received frames Steady received = fresh FPS Long intervals Arrival interval p99 / max (ms) Audio packets / tone blocks
HEVC / control / r1 passed 10780 / 10780 120.000 0 9.576 / 11.965 17823 / 34846
HEVC / outage / r1 failed 10564 / 10564 117.549 116 13.544 / 40.318 17831 / 34862
AV1 / control / r1 passed 10782 / 10782 120.001 0 8.904 / 10.334 17829 / 34858
AV1 / outage / r1 failed 10600 / 10600 117.999 157 13.161 / 38.329 17825 / 34850
HEVC / control / r2 passed 10784 / 10784 120.000 0 9.500 / 10.580 17831 / 34862
HEVC / outage / r2 failed 10570 / 10570 117.659 116 13.496 / 41.199 17821 / 34842
AV1 / control / r2 passed 10781 / 10781 120.002 0 11.195 / 11.936 17829 / 34858
AV1 / outage / r2 failed 10567 / 10567 117.608 139 12.815 / 38.419 17825 / 34850

The separate recovery_gaps.py reports use the complete timing CSV, including warmup, and the same missing-wire-frame plus two-median-period stall definition as the 80 ms check above. All controls recorded zero detected stalls. Every outage run recorded 59 stalls; gap duration includes loss and recovery. Missing frames below count gaps in wire-frame indices, not missing UDP packets, and frame sizes are KiB (1,024 bytes).

Codec / repeat Stalls Gap mean / p95 / max (ms) Missing frames Stalls ended by keyframe Median / recovery-mean frame size (KiB)
HEVC / r1 59 36.8 / 40.0 / 40.3 219 59 14.8 / 142.9
HEVC / r2 59 35.6 / 39.8 / 41.2 211 59 17.7 / 138.2
AV1 / r1 59 29.1 / 29.7 / 38.3 180 0 12.3 / 13.3
AV1 / r2 59 34.5 / 38.0 / 38.4 214 0 13.2 / 19.4

HEVC ended every detected stall with a keyframe. AV1 ended none with a keyframe, with 59 post-invalidation P-frames logged in each repeat. This shows recovery in this injected-loss fixture while preserving all four normal-stream failures; it does not establish a historical recovery gain or real-network performance.

Raw evidence is retained in C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\remaining-loss-20261012T060405396678Z: all eight run/source/e2e result records, host/client/tone logs, timing CSVs and recovery reports, plus stage metadata, hardware and summary. The host, receiver and runner hashes match the current static-IDR batch above. Reproduce with work\run_remaining_streams.py --stages loss; each recorded command retains the 90-second duration, 50,000 kbps bitrate and outage environment where applicable.

Two-minute decoded-audio continuity and video windows #

Both 120-second published-2.2.9 streams passed the unmodified release e2e gates on October 12, 08:16–08:21 CEST. HEVC uses 2560x720 SDR / 60 FPS / 20,000 kbps; PyroWave uses 1920x1080 HDR-output 4:4:4 / 60 FPS / 400,000 kbps with encrypted record framing. Both use WGC on the same physical 5120x1440 SDR / 240 Hz source, minimum_fps_target=60, default pacing and no added CPU/GPU load. PyroWave's HDR output is converted from SDR, and its Vulkan receiver uses synchronous 8-bit readback; this is not native-HDR capture or full-precision HDR pixel validation. HEVC uses the independent FFmpeg software receiver with four decoder threads.

These are loopback decode/continuity measurements, excluding real-network transit, laptop performance and audio/display presentation. All received frames decoded, with zero decode failures, 100% motion coverage, continuous audio and zero tone-source underruns. PyroWave reported 7,195 complete HDR records, zero partial frames and zero host-replaced frames. Steady received and fresh FPS were identical. Frame/audio totals cover the full receiver run; picture age and cadence exclude three seconds of warmup, leaving 116.796 seconds for HEVC and 116.867 seconds for PyroWave.

Profile Decoded / received frames Steady received = fresh FPS Picture age mean / p95 / p99 / max (ms) Audio packets / tone blocks
HEVC SDR 7190 / 7190 60.002 14.240 / 17.236 / 17.857 / 25.826 23819 / 46838
PyroWave HDR output 4:4:4 7195 / 7195 60.000 18.444 / 19.356 / 19.846 / 20.626 23855 / 46910

Neither run had a steady assembly interval over 1.5 frame periods. The whole-steady interval p99 / maximum was 20.411 / 22.127 ms for HEVC and 19.991 / 20.934 ms for PyroWave.

Offline analysis uses the unchanged soak_result.py audio_windows and video_windows functions. Each audio timeline has 12 nominal ten-second windows, including its partial final window; all 24/24 were continuous under the existing tone RMS check. In each case, the first eleven audio windows contain 2,000–2,002 packets and 480,000–480,480 decoded samples per channel at 48 kHz. Final-window counts are reported separately below, not normalized to a full ten seconds. The callback-gap range is the minimum to maximum of the 12 per-window maximum gaps, not a percentile or speaker-output gap measurement.

Profile Continuous audio windows Window maximum callback-gap range (ms) Partial final audio packets / decoded samples per channel
HEVC SDR 12 / 12 19.026–19.841 1807 / 433680
PyroWave HDR output 4:4:4 12 / 12 10.818–11.474 1843 / 442320

The video analyzer independently creates 12 windows per case after its three-second warmup. Each has 100% picture-age coverage and zero assembly gaps exceeding two frame periods (33.333 ms), a separate criterion from the e2e 1.5-period counter. Ranges below include the partial final window and retain per-window statistics; they are not pooled percentiles.

Profile Window fresh-FPS range Window decoder-mean range (ms) Window picture-age p99 range (ms) Partial final video frames / span (s)
HEVC SDR 59.982–60.014 2.193–2.425 14.053–18.274 403 / 6.700
PyroWave HDR output 4:4:4 59.983–60.016 2.096–2.184 19.429–20.159 406 / 6.750

Audio timestamps are receiver QPC after Opus decoding; these video timestamps are QPC at decode-callback start. Their windows have independent origins and must not be aligned by window index. No source-audio timestamp, RTP clock mapping or synchronized audiovisual marker is present. These results establish decoded-tone continuity and video cadence only, not lip-sync, A/V clock drift or glass-to-glass timing.

Raw evidence is retained in C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\remaining-audio-20261012T061645540435Z; the offline windows and evidence hashes are in the task-local work\audio-windows-summary.json. Host/receiver identities match the current release batch above. Reproduce the measurements with work\run_remaining_streams.py --stages audio, then analyze the retained CSVs with work\summarize_audio_windows.py. The analyzer wrapper SHA-256 is 82c08a2231bfe774c7e5748000aa25ba3cb337dcf2330d76d01cfe9b58662ad6; the unchanged soak_result.py SHA-256 is ac98ec52e34580dfbd83b9b8cee1d590a3ea81829afa0e3f9ce327ee6a127289.

Short soak, switching, reconnects and WGC-helper recovery #

The valid published-2.2.9 short soak ran on October 12, 08:27–08:36 CEST, for 564.789 seconds overall. Of 67 cases, 64 passed and three failed: all three idle steady streams, eight codec/mode switches, 52 launch/resume cycles and the WGC-helper recovery passed; all three loaded steady streams failed freshness checks. The overall soak result remains failed. This roughly nine-minute matrix contains short individual sessions and does not establish multi-hour stability.

It uses the same released host and receiver identities above, an isolated loopback host with wgc_user_helper=true, the moving-strip/tone fixtures and the physical 5120x1440 SDR / 240 Hz display snapshot. Conventional codecs use the FFmpeg software receiver; PyroWave uses its Vulkan receiver and 8-bit readback. HDR switch cases convert the SDR source and do not establish native-HDR correctness. The stock soak calls its receiver evaluator without the e2e source-comparison arguments, so receiver records retain comparison=unverified; the display snapshot and host logs are separate source evidence, not a matched historical comparison.

The six steady cases each request 25 seconds, 1920x1080 SDR and 20,000 kbps. Both 120 FPS cases request VRR; minimum-FPS targets match each stream rate. After three seconds of warmup, steady windows span 21.498–21.550 seconds. There is one run per condition, in the order below. All six decoded every received frame, had continuous audio with zero tone-source underruns, and recorded 100% picture-age coverage. Decoder means include all frames; rates and picture-age statistics exclude warmup.

Profile / condition Result Decoded / received Steady received / fresh FPS Picture age mean / p95 / p99 / max (ms) Decoder mean (ms)
HEVC 60 FPS / idle passed 1476 / 1476 59.999 / 59.999 14.740 / 19.822 / 20.418 / 21.294 3.929
HEVC 60 FPS / load failed 1475 / 1475 59.997 / 42.304 54.182 / 81.100 / 93.041 / 122.201 3.837
AV1 60 FPS / load failed 1474 / 1474 60.006 / 41.492 51.745 / 80.482 / 94.876 / 108.285 2.336
AV1 60 FPS / idle passed 1474 / 1474 60.000 / 60.000 11.365 / 13.658 / 14.004 / 18.934 2.102
HEVC 120 FPS / idle passed 2949 / 2949 120.004 / 120.004 15.213 / 16.200 / 16.539 / 21.553 2.890
HEVC 120 FPS / load failed 2962 / 2962 120.630 / 49.349 52.510 / 81.078 / 94.680 / 117.468 2.831

Each loaded case retains both failures: “moving pictures were missing or repeated too often” and “a video window lost fresh pictures”. Its two full video windows delivered 42.202 / 42.207 fresh FPS for HEVC at 60 FPS, 42.009 / 40.567 for AV1 at 60 FPS, and 48.430 / 49.307 for HEVC at 120 FPS, below the existing 90%-of-requested-rate threshold. These were not reclassified as decoder-limited cases. The stock load command is gpu_load.exe 55 1000 0 200 (uncapped, 200 draws); the soak stops it after the 25-second stream. All three load logs are empty, so completed load throughput is unavailable; measurements from other suites are not substituted.

All eight ten-second switches passed in this order: H264, HEVC, AV1, PyroWave, HEVC HDR output, 1280x720 HEVC, PyroWave HDR-output 4:4:4, then H264. Other outputs are 1920x1080, all at 60 FPS; requested bitrate is 20,000 kbps for conventional codecs and 400,000 kbps for PyroWave. They decoded 571–581 frames each, sustained 59.983–60.035 fresh FPS, and retained continuous audio with zero source underruns.

All 52 three-second HEVC 1080p60 churn cases passed their existing short-session checks: 26 launches alternating with 26 resumes on one host. They decoded all 153–156 received frames per case, received 405–413 audio packets, and reported 59.969–60.139 fresh FPS after 0.5-second warmup. These checks require decoded video and audible audio, but deliberately omit the tone-continuity test; audio_continuous is null, so these are not 52 further continuity measurements. No teardown failures or leftover-device/helper failures were reported. The 26 disconnect-to-next-resume-request intervals were 0.504–0.534 seconds. This is request-dispatch timing, not time to the first decoded frame. The existing quick-reconnect check passed.

Resource growth compares 26 settled churn snapshots after each resume, discards the first five, then compares the first/last five medians of the remaining 21 samples. The host working-set, handle and thread checks passed:

Host resource First median Last median Delta Allowed growth
Working set, bytes 155529216 153108480 -2420736 67108864
Handles 391 389 -2 16
Threads 40 39 -1 4

GPU memory is outside that resource-growth gate. For the same host and adapter, the first/last settled churn snapshots (churn-02 / churn-52) recorded the per-process DedicatedUsage counter at 4,365,221,888 / 17,118,527,488 bytes, an increase of 12,753,305,600 bytes. These original readings remain preserved as counter observations. The follow-up below reproduced the growing counter without sustained adapter-wide VRAM growth, identifying the large staircase as a reporting artifact. It must not be presented as 12.75 GB of leaked VRAM.

The final 20-second HEVC 1080p60 case terminates only the isolated host's owned WGC helper at 8.442 seconds. The host logged capture_recovery; the existing bounded-recovery evaluator reported recovered, observed by the end of a valid video window 11.121 seconds after injection. That post-injection window spans 6.550 seconds, with 60.002 fresh FPS and 100% picture-age coverage. This observation time is not the outage duration: the receiver's maximum assembly gap was 496.039 ms. It decoded all 1,148 frames and reported continuous tone with zero source underruns; both audio windows were continuous. The fault case uses its recovery predicate, not normal steady-stream cadence acceptance. The retained host log also contains an audio_stopped warning near teardown.

Three debug-only fault cases remain excluded from this published-binary measurement: DXGI_ERROR_ACCESS_LOST, encoder failure and encoder failure.persistent. The stock fixture also skips controller/VHF streaming because its receiver has no controller sender, and secure-desktop tests because it does not change Windows desktop/security state. The excluded debug access-lost case is not coverage for the secure desktop.

An earlier 152.134-second invalid fixture attempt is preserved at work\measurements\remaining-soak-short-20261012T062104519442Z and excluded from product pass/failure counts. Both directly launched receivers exited before main with -1073741515 because the DLL search path omitted C:\msys64\ucrt64\bin, which interop.py normally supplies. The retained no-argument check work\soak-dll-startup-check.json reproduces that exit, then obtains exit 2 with “session URL required” from the same binaries after adding that directory; no stream was launched by the check. The task-local rerun prepends the directory only to the receiver environment. The released host, source matrix and evaluators remain unchanged apart from excluding the three unsupported debug faults. The initial startup/cascading resume errors are not 67 product failures.

Valid raw evidence is retained at C:\Users\ramaz\Documents\Codex\2026-10-12\wait\work\measurements\remaining-soak-short-20261012T062718020881Z\soak-short\soak\20261012T062721-072655Z. It includes all 67 case records and timelines, host/client/tone logs, settled resource snapshots and the final result.json; stage metadata and release-limitations.json record the exact package and fixture adaptation. Reproduce with work\run_remaining_streams.py --stages soak-short; this runner version has SHA-256 c9e16755be55449132c47952982e1cbcad768cc0f173e12ae79ed04a57b57563.

GPU-memory counter follow-up (October 12) #

A focused diagnostic on the same published 2.2.9 host, RX 7900 XT, driver and physical desktop ran at 14:06–14:10 UTC. It reused the first 30 HEVC 1920x1080p60 reconnect cases from the existing soak fixture, alternating launch/resume with three-second streams through the WGC helper. All 30/30 cases and the existing CPU resource-growth checks passed. The eight-second pairing stream accounts for the additional disconnect: the log contains 31 completed disconnects and no stuck-worker teardown warnings. This diagnostic adds separate adapter-wide counters and a 30-second final idle period; it does not repeat the mixed-codec or GPU-load parts of the original soak.

The original sampler reads Win32_PerfFormattedData_GPUPerformanceCounters_GPUProcessMemory, filtered by host PID. The follow-up also reads GPUAdapterMemory for the same RX 7900 XT adapter (luid_0x00000000_0x00016702_phys_0). Each snapshot has one matching process instance; the increase is not a duplicated-row sum or a running total calculated by the fixture. GB below means decimal GB.

Observation Process dedicated counter (GB) Process GPU commitment (bytes) Adapter dedicated usage (GB)
Before test host starts not running not running 2.664
After 30 reconnects, host still running 7.910 4,096 2.786
After 30 seconds idle, host still running 7.910 4,096 2.659
After test host exits not running not running 2.659

All 15 settled reconnect snapshots report 4,096 bytes of process GPU commitment. Adapter dedicated usage returns from 2,664,321,024 bytes before launch to 2,659,160,064 bytes by 15 seconds idle, remaining there at 30 seconds and after host exit. Adapter commitment changes from 3,152,412,672 to 3,152,408,576 bytes on exit, a decrease of exactly 4,096 bytes. There is no corresponding 7.91 GB of adapter consumption to release. The original soak also returned process commitment to 4,096 bytes in all 41 settled snapshots while its dedicated counter accumulated.

The reproduced staircase is a per-process counter/accounting artifact; no sustained adapter VRAM growth was observed in this HEVC/WGC reconnect test. Microsoft documents false growth in GPU Process Memory counters during resource recreation. That Windows 10 report is supporting context, not confirmation of the underlying Windows/driver defect on this Windows 11 build. These results resolve the large counter-growth warning; they do not establish every codec, capture path or longer session as leak-free.

Raw evidence is retained under work\leak-analysis\repro-20261012T140648Z, including the original independent-counters.jsonl, fixture metadata and the soak results in soak\20261012T140650-275038Z. The counter timeline has SHA-256 f2e69f58ec3195e03d5f626e9631c4ae8a43267771d8be53f5d18d48b03f2521; the soak result.json has 90e3306e5f0fc5d398884003baeb428bb6058ddf7bcff6c64992a052c1224f03. work\leak-analysis\reproduction-summary.json indexes the comparison and hashes; soak-counter-audit.json retains the original 92-snapshot audit. The task-local diagnostic is work\leak-analysis\reproduce_counter.py, with its sampler and released-host identity pinned in the metadata. Original benchmark outcomes and competitor measurements remain unchanged.

Real Wi-Fi hardware-receiver streams (October 12) #

The laptop connection was restored, and all 20 independent-receiver streams passed their raw receiver gates: 12 HDR-output conversion cases and eight SDR control/recovery cases. These completed between 07:28 and 07:58 UTC. They use the published 2.2.9 host above, not the historical rc.24 host. The two completed stock Moonlight sessions are reported separately with their original cleanup-check failures.

The source remains the physical 5120x1440 SDR display at 240 Hz on the RX 7900 XT. A timestamped motion strip renders at twice the requested stream rate; GPU compute conversion is enabled and minimum_fps_target matches the requested stream rate. HDR outputs therefore measure SDR-to-HDR conversion, not native-HDR capture or HDR pixel fidelity. The receiver is on the actual Lenovo IdeaPad (model 83D3), Radeon 780M, driver 32.0.31041.1004, Windows build 26200, using its RZ616 Wi-Fi 6E adapter with a reported 2.4 Gbps link. Link rate is not measured payload throughput. The laptop was on AC power with the Balanced power plan.

The task-owned independent receiver uses D3D11VA/D3D11 hardware decoding and reads back an eight-row barcode strip from NV12/P010 surfaces. Its executable SHA-256 is 68a6e78ece95d7ac48719da5772bafb179f0bdb06ea3ee827e15d469ffd50a67; the retained fixture source reference is e5520ac0:rust/tests/moonlight_client.c, not an attestation of a reproducible build. This is distinct from installed Moonlight 6.2.0 and does not measure client display presentation. Receiver metadata retains its actual executable identity, GPU/driver, and CSV/log hashes for each run.

Family Output and requested rate Requested bitrate Duration per run Runs / raw gates
HEVC and AV1 HDR output 1920x1080, 60 FPS 20 Mbps 30 s 4 / 4 passed
HEVC and AV1 HDR output 2560x1440, 120 FPS 50 Mbps 30 s 4 / 4 passed
HEVC and AV1 HDR output 1968x2184, 120 FPS 80 Mbps 30 s 4 / 4 passed
HEVC and AV1 SDR controls 2560x1440, 120 FPS 50 Mbps 90 s 4 / 4 passed
HEVC and AV1 SDR with sender outage 2560x1440, 120 FPS 50 Mbps 90 s 4 / 4 passed

Each condition has two separate runs, with the second pass in reverse condition order. The qualification HEVC HDR 1080p60 run is run 1 of that condition, not an extra 21st stream. Recovery cases request reference-frame invalidation support; outage cases use the existing 1500:20:air sender fixture (20 ms every 1,500 ms). Steady receiver gates exclude three seconds of warmup and request a minimum received rate of 97% of the stream rate. These are the pinned receiver's audio, picture, motion and FPS gates; they are not the local software-receiver release evaluator or a matched historical comparison.

Final analysis accepted 19 of 20 timing records. Host render QPC is mapped onto the laptop clock using 2,777 of 13,883 replies (the lowest-RTT 20%, rounded up) and one affine fit. Both QPC frequencies are 10 MHz. The fitted drift is 16.929 ppm, selected maximum RTT 1.631 ms and maximum absolute fit residual 0.362 ms. Selected half-RTT plus maximum fit residual gives a 1.177 ms sample-based diagnostic, not a confidence interval or a proven error bound; network asymmetry remains unknown. Every accepted source/arrival timestamp lies inside the selected calibration span; no clock extrapolation is used.

For this hardware fixture, arrival_ms estimates completed frame assembly/enqueue time in the laptop QPC epoch, excluding decoder-queue waiting. Render-to-received timing therefore excludes decoder work and display scanout. The separately reported decoder callback includes barcode readback; adding it to arrival age would omit decoder-queue wait. The CSV's local picture_age_ms is invalid across machines and is not used.

The table retains each repeat separately; no percentiles are pooled or averaged. All latency columns are milliseconds. Distributions use the first received copy of each unique readable source picture after three seconds of warmup; fresh FPS counts unique-picture transitions over the complete post-warmup arrival window. Host processing is reported by received frames, not an encode-only timer. Decoder mean below uses the same selected pictures, unlike the receiver log's all-frame callback mean.

Codec/output and condition Output / FPS Run Fresh FPS Render-to-received mean / p95 / p99 Host mean / p99 Decoder mean
HEVC HDR output 1920x1080 / 60 1 60.018 14.123 / 15.295 / 16.834 2.454 / 3.000 3.463
HEVC HDR output 1920x1080 / 60 2 60.001 9.025 / 15.818 / 17.103 2.368 / 2.700 3.311
AV1 HDR output 1920x1080 / 60 1 59.997 10.697 / 12.103 / 13.128 1.977 / 2.600 3.452
AV1 HDR output 1920x1080 / 60 2 unavailable unavailable unavailable unavailable
HEVC HDR output 2560x1440 / 120 1 120.009 11.764 / 13.138 / 16.199 3.115 / 3.600 4.608
HEVC HDR output 2560x1440 / 120 2 119.996 11.503 / 13.594 / 16.078 3.060 / 3.600 4.668
AV1 HDR output 2560x1440 / 120 1 120.003 11.544 / 14.531 / 15.945 2.660 / 3.100 4.700
AV1 HDR output 2560x1440 / 120 2 119.988 10.463 / 11.486 / 12.621 2.581 / 3.000 4.599
HEVC HDR output 1968x2184 / 120 1 119.998 12.057 / 14.152 / 16.333 3.523 / 4.100 5.269
HEVC HDR output 1968x2184 / 120 2 119.999 15.202 / 39.992 / 70.001 3.594 / 3.900 5.265
AV1 HDR output 1968x2184 / 120 1 119.961 11.069 / 11.972 / 13.559 2.914 / 3.300 5.228
AV1 HDR output 1968x2184 / 120 2 119.998 11.024 / 11.901 / 13.369 2.947 / 3.400 5.278
HEVC SDR control 2560x1440 / 120 1 119.999 12.170 / 13.765 / 47.960 3.086 / 3.600 4.616
HEVC SDR control 2560x1440 / 120 2 119.999 11.469 / 12.794 / 14.495 3.095 / 3.600 4.587
HEVC SDR outage 2560x1440 / 120 1 117.878 11.234 / 12.576 / 14.954 3.112 / 3.600 4.602
HEVC SDR outage 2560x1440 / 120 2 117.931 11.458 / 12.707 / 14.285 3.076 / 3.600 4.593
AV1 SDR control 2560x1440 / 120 1 119.998 10.622 / 11.540 / 12.842 2.661 / 3.100 4.621
AV1 SDR control 2560x1440 / 120 2 120.003 10.852 / 12.225 / 14.890 2.603 / 3.000 4.652
AV1 SDR outage 2560x1440 / 120 1 117.771 10.849 / 12.250 / 15.181 2.644 / 3.100 4.652
AV1 SDR outage 2560x1440 / 120 2 117.722 10.772 / 11.835 / 15.791 2.607 / 3.100 4.623

AV1 HDR 1080p60 run 2 passed its receiver gates (1,795/1,795 decoded frames, zero decode failures, continuous tone and 60.002 raw steady FPS), but three consecutive decoded barcode ID/QPC pairs did not match the exact source ledger. Its entire calibrated timing record remains unavailable; no samples were filtered or substituted. The raw log's 100% readable-barcode coverage does not prove timestamp identity. The mismatch diagnostic is retained as source-pair-mismatch-details.json beside that case. All 20 runs reported continuous tone and zero decoder failures. The HEVC portrait run 2 tail (70.001 ms p99) and the HEVC SDR control run 1 tail (47.960 ms p99) are retained; ordinary receiver acceptance does not imply low latency tails, and these runs do not establish a causal change from the older source/driver fixture.

Recovery analysis uses the existing recovery_gaps.py criterion over the whole CSV, including warmup: missing wire-frame numbers and an adjacent arrival gap exceeding twice the median period. Stall length includes the outage and recovery; a stall ending in frame_type=1 ends in a keyframe. RFI/IDR counters come separately from observed host sessions. The pinned 11-column CSV has no frame-byte field, so median/recovery-frame byte sizes are unavailable and must not be reported as zero.

SDR codec / condition Run Detected stalls Gap mean / p95 / max (ms) Missing wire frames in stalls Keyframe-ended stalls Last observed RFI / IDR / send-loss counters
HEVC control 1 0 not applicable 0 0 0 / 1 / 0
HEVC control 2 0 not applicable 0 0 0 / 1 / 0
HEVC outage 1 59 33.783 / 40.574 / 41.651 192 59 117 / 60 / 0
HEVC outage 2 59 32.281 / 39.524 / 40.444 183 59 118 / 60 / 0
AV1 control 1 0 not applicable 0 0 0 / 1 / 0
AV1 control 2 0 not applicable 0 0 0 / 1 / 0
AV1 outage 1 59 32.523 / 38.470 / 40.425 198 0 117 / 1 / 0
AV1 outage 2 58 33.761 / 39.543 / 40.636 203 0 117 / 1 / 0

These host counters are cumulative at the last observed session sample, not guaranteed teardown totals, and are not summed across polling samples. They retain the difference between reference feedback and IDR requests; the number of RFI requests is not a count of independently recovered outages. All detected HEVC outage stalls ended in keyframes; none of the detected AV1 outage stalls did. Missing-wire counts apply only to detected stalls, not every lost packet or frame on the network.

Raw evidence is retained in task-local work\measurements\wifi-20261012T072600-000003Z (qualification) and work\measurements\wifi-20261012T073200-000004Z (remaining 19 cases). Each includes its plan, release/fixture hashes, isolated host lifecycle, source motion ledger and original laptop evidence archive. Final timing analysis is retained in work\wifi-summary.json, using the stopped collector's wifi-clock-20261012T070500\clock-pings.csv and stopped responder's laptop-20261012T075905-clock-final.json. Per-case calibrated-timing-final.json files retain accepted results or the explicit validation failure and their evidence hashes. Historical Wi-Fi and competitor measurements above remain unchanged.

Native virtual-HDR headline streams (October 12) #

The published 2.2.9 host passed 28/28 native virtual-HDR headline streams between 08:26 and 08:52 UTC. All 28 also passed exact source ID/QPC membership, timestamp ordering and CSV/statistic accounting checks; no mismatching samples were filtered. These are two repeats of seven conditions through each of DDX and WGC. The host is the same Ryzen 7 5800X3D / RX 7900 XT, driver 32.0.32015.2008, Windows build 26200 used above. Published host SHA-256: f38f3a0056dd64fc015af864b1bcf83c7e56bf388f97d6a6548398030665f4b7; release source d0bfcb5e764095250b8f0e22fd5a0cb3656d5ea6.

The captured source is an extended native HDR virtual display at 120 Hz, with the requested output dimensions and confirmed RgbaF16 capture. The physical 5120x1440 SDR / 240 Hz Odyssey G93SC remains active. The historical October 8 setup instead had a 2560x1440 / 120 Hz physical display. These current results do not establish a matched improvement over the earlier host or competitor measurements. The renderer runs for 40 seconds; the independent receiver requests 35 seconds and excludes the first five seconds from steady statistics. Host frame pacing remains default, with 1,000 us capture polling and predictive/freshness polling disabled.

The local independent receiver uses D3D11VA hardware decoding on the RX 7900 XT, with the same pinned executable as the Wi-Fi fixture (68a6e78ece95d7ac48719da5772bafb179f0bdb06ea3ee827e15d469ffd50a67; retained source reference e5520ac0:rust/tests/moonlight_client.c, not a reproducible-build attestation). Picture age here is the renderer's QPC timestamp to the local decoded-barcode measurement, including hardware decode and eight-row barcode readback. It excludes client presentation and physical display scanout. Both processes use the same 10 MHz QPC clock, so there is no Wi-Fi clock-fit adjustment. This endpoint differs from the Wi-Fi table's render-to-received endpoint. The hardware CSV's separate arrival_ms estimates frame assembly/enqueue, not decode start; arrival_ms + decode_ms omits decoder-queue waiting and is not a decode-completion timestamp. Its 11 columns contain no frame-byte field.

Each row below retains one repeat, with no pooled percentiles. Latencies are milliseconds. Picture-age statistics use readable post-warmup decoded pictures; host and callback means use post-warmup received frames. Fresh FPS counts unique-picture transitions over the post-warmup arrival window. Ordinary acceptance is the unchanged fixture's independent stream gate, separate from the exact-source timing validation.

Capture Codec Load Output / FPS / Mbps Run Fresh FPS Picture age mean / p95 / p99 Host mean / p99 Decoder callback mean
DDX HEVC HDR idle 1968x2184 / 120 / 80 1 120.001 10.213 / 10.721 / 11.138 3.247 / 3.600 2.198
DDX HEVC HDR idle 1968x2184 / 120 / 80 2 119.985 9.086 / 11.014 / 13.191 3.344 / 4.000 2.319
DDX AV1 HDR idle 1968x2184 / 120 / 80 1 119.998 8.962 / 9.586 / 10.005 3.038 / 3.500 2.321
DDX AV1 HDR idle 1968x2184 / 120 / 80 2 119.999 12.042 / 12.926 / 13.214 3.026 / 3.500 2.263
DDX HEVC HDR idle 2560x1440 / 120 / 50 1 120.000 9.716 / 10.231 / 10.543 2.984 / 3.600 1.954
DDX HEVC HDR idle 2560x1440 / 120 / 50 2 119.999 9.053 / 9.553 / 9.844 2.956 / 3.500 2.069
DDX HEVC HDR idle 1920x1080 / 60 / 20 1 60.000 9.218 / 9.683 / 9.883 2.179 / 2.600 1.638
DDX HEVC HDR idle 1920x1080 / 60 / 20 2 59.999 10.966 / 11.431 / 11.643 2.099 / 2.600 1.549
DDX HEVC HDR load 1968x2184 / 120 / 80 1 119.980 15.261 / 17.579 / 18.074 3.450 / 4.400 3.796
DDX HEVC HDR load 1968x2184 / 120 / 80 2 120.000 13.418 / 14.473 / 18.227 3.244 / 4.100 2.753
DDX AV1 HDR load 1968x2184 / 120 / 80 1 119.997 14.895 / 19.201 / 19.886 5.145 / 8.600 2.671
DDX AV1 HDR load 1968x2184 / 120 / 80 2 119.971 12.395 / 16.789 / 18.016 5.352 / 8.600 2.439
DDX HEVC HDR load 1920x1080 / 60 / 20 1 59.999 11.661 / 21.844 / 22.270 2.192 / 3.000 1.446
DDX HEVC HDR load 1920x1080 / 60 / 20 2 60.006 9.616 / 14.687 / 14.890 2.281 / 2.800 1.581
WGC HEVC HDR idle 1968x2184 / 120 / 80 1 119.965 12.950 / 15.449 / 19.332 3.260 / 3.700 2.356
WGC HEVC HDR idle 1968x2184 / 120 / 80 2 120.000 11.526 / 12.018 / 12.488 3.264 / 3.600 2.406
WGC AV1 HDR idle 1968x2184 / 120 / 80 1 120.001 9.566 / 10.309 / 13.414 3.040 / 3.500 2.513
WGC AV1 HDR idle 1968x2184 / 120 / 80 2 120.015 11.577 / 13.240 / 13.627 3.035 / 3.400 2.482
WGC HEVC HDR idle 2560x1440 / 120 / 50 1 119.998 12.135 / 12.824 / 13.054 2.954 / 3.200 2.129
WGC HEVC HDR idle 2560x1440 / 120 / 50 2 120.004 10.447 / 11.168 / 11.351 2.990 / 3.400 2.124
WGC HEVC HDR idle 1920x1080 / 60 / 20 1 60.000 8.826 / 9.475 / 9.845 2.270 / 2.600 1.685
WGC HEVC HDR idle 1920x1080 / 60 / 20 2 60.001 9.892 / 10.379 / 10.611 2.275 / 2.600 1.652
WGC HEVC HDR load 1968x2184 / 120 / 80 1 119.926 15.450 / 18.708 / 19.151 3.368 / 4.000 4.083
WGC HEVC HDR load 1968x2184 / 120 / 80 2 119.964 18.043 / 20.070 / 20.349 3.397 / 4.000 4.624
WGC AV1 HDR load 1968x2184 / 120 / 80 1 119.760 17.212 / 19.436 / 24.054 3.033 / 3.600 4.474
WGC AV1 HDR load 1968x2184 / 120 / 80 2 119.976 17.428 / 19.520 / 19.812 3.053 / 3.500 4.647
WGC HEVC HDR load 1920x1080 / 60 / 20 1 59.998 13.258 / 13.965 / 19.763 2.289 / 2.500 1.490
WGC HEVC HDR load 1920x1080 / 60 / 20 2 60.000 10.618 / 11.069 / 11.212 2.453 / 2.800 2.449

Loaded cases use the established gpu_load 45 1000 60 200 fixture: a 2560x1440 render target, 1,000 total iterations across 200 draws, capped at 60 FPS for 45 seconds. Every one of the 12 load processes completed naturally with 2,701 frames / 60.000 FPS; observed per-run frame-time p50 ranged 5.419–6.179 ms, and p95 10.022–10.344 ms. This is a specified synthetic workload, not a claim of maximum GPU saturation. Variation between repeats, including the 24.054 ms WGC AV1 loaded p99, is retained. Two repeats do not establish confidence intervals or causal improvements over a different display/driver setup.

Raw streams and controls are under work\measurements\headline-ddx and work\measurements\headline-wgc, with batch suffixes 20261012t082617-467057z and 20261012t083911-689666z. Each retains the source ledger, original receiver CSV/log, capture integrity and before/after active-monitor snapshots. The snapshots verified original active monitor identities, layout, mode, refresh and HDR restoration; they are not a full QDC topology snapshot. Final source validation, per-run results and hashes are in work\native-summary.json. Earlier launcher and pairing setup failures are retained separately, not counted as product stream failures. Stock Moonlight validation remains separate from this instrumented receiver.

Native HDR pixel roundtrip (October 12) #

All 4/4 WGC native-HDR pixel checks passed: two HEVC and two AV1 runs, 1280x720 / 60 FPS / 40 Mbps, 35 seconds each, using a 120 Hz extended virtual HDR source. These differ from the historical 22-second / 60 Hz source fixture. This check uses the software-decoding independent receiver (629c0503e8816189043853ab13a6f83ea8e8236a8675a93cd779117791cbb57f), not the hardware receiver's reduced barcode strip. The existing colour_check.py black, white, luma, chroma, contrast and saturation gates all passed; each run retained 16 native-HDR observations. Code errors below are in limited-range ten-bit code values; slope and saturation are ratios.

Codec Run Black Y White Y Luma mean / mean absolute / p99 absolute error Chroma mean absolute error Contrast slope Saturation ratio
HEVC HDR 1 64.01 509.00 -0.073 / 0.423 / 1.88 0.408 0.9996 1.0059
HEVC HDR 2 64.00 509.00 -0.068 / 0.392 / 1.74 0.397 0.9993 1.0061
AV1 HDR 1 64.00 509.00 0.000 / 0.311 / 1.36 0.354 0.9990 1.0051
AV1 HDR 2 64.00 509.00 -0.046 / 0.372 / 1.72 0.574 0.9978 0.9917

Expected black/white Y are 64.00 / 509.08. Raw evidence and unchanged gates are retained under work\measurements\hdr-pixels, with executable, fixture and colour-check hashes in work\native-summary.json. Active monitor state was restored in all four cases. This validates the native HDR capture/conversion/codec pixel path; it is not physical-panel calibration, full-game perceptual validation or a display-latency test.

SDR VRR capture-only measurements (October 12) #

The current-source capture_phase_probe attempted 32 runs across eight requested mode groups, with no added GPU load. 20 runs in five groups matched the requested Windows display mode and source rate; all 20 captured every presented picture in the retained window. 12 runs in three groups have unavailable requested-mode timings because Windows selected a different active refresh rate. All raw attempts remain recorded.

Each group orders WGC helper / DDX / DDX / WGC helper, ten seconds each, excluding the first second and final 250 ms. The source is SDR BGRA8. This measures renderer Present to a distinct owned capture texture after GPU copy, with no encoder, receiver, network or scanout. The executable is built against exact release source d0bfcb5e, SHA-256 792ff4e1ed774815a460e7266a1851b42c3ecc01fa2daacf78ce2976400d77e2; work\vrr-fixtures\build.json pins the source and runtime. It is a source-built component fixture, not a downloaded release executable or the unavailable private HDR/RTSS variant.

Each paired value below preserves the first / second occurrence of that backend, not an average. Latency columns are milliseconds; all listed runs have 100% picture coverage. The 116 FPS runs contain 1,015 retained presented/captured pictures each, and 120 FPS runs 1,050 each.

Output / active Hz / source FPS Capture Observed source FPS, run 1 / 2 Present-to-copy mean, run 1 / 2 p95, run 1 / 2 p99, run 1 / 2
1920x1080 / 1000 / 116 WGC helper 115.997 / 115.996 8.108 / 8.240 10.374 / 10.592 10.893 / 11.337
1920x1080 / 1000 / 116 DDX 116.015 / 115.995 8.014 / 7.965 10.203 / 10.161 10.624 / 10.734
1920x1080 / 1000 / 120 WGC helper 120.003 / 120.000 8.630 / 8.826 9.919 / 10.132 10.411 / 10.387
1920x1080 / 1000 / 120 DDX 119.998 / 120.001 8.606 / 8.564 9.503 / 9.491 10.050 / 10.029
3840x2160 / 1000 / 116 WGC helper 115.996 / 115.999 8.001 / 8.234 10.486 / 10.615 11.040 / 11.219
3840x2160 / 1000 / 116 DDX 116.002 / 115.997 7.675 / 7.632 9.823 / 10.047 10.338 / 10.603
3840x2160 / 1000 / 120 WGC helper 120.004 / 119.998 8.709 / 8.508 9.874 / 9.708 10.767 / 10.105
3840x2160 / 1000 / 120 DDX 120.000 / 119.998 7.891 / 8.019 8.996 / 8.979 9.178 / 9.207
1920x1080 / 232 / 116 WGC helper 116.006 / 116.000 6.497 / 6.484 8.490 / 8.438 8.918 / 9.071
1920x1080 / 232 / 116 DDX 115.998 / 115.999 6.277 / 6.229 8.333 / 8.300 8.780 / 8.886
Requested output / Hz / source FPS Observed active Hz Attempts Requested-mode timing
1920x1080 / 240 / 120 120 4 unavailable
3840x2160 / 232 / 116 928 4 unavailable
3840x2160 / 240 / 120 960 4 unavailable

The virtual-display API requested lease rates, but this retained probe did not force Windows active-mode selection. The mismatched groups were not retried or relabelled as requested-mode passes. Their captured-picture coverage does not override the mode mismatch. Observed active Hz is a Windows virtual-display mode, not measured physical-panel refresh. All eight groups restored the recorded active-monitor state. Raw group directories are work\measurements\vrr-capture-* from 085538473358Z through 090444056183Z on October 12; the exact eight paths, all 32 raw reports and hashes are indexed by work\native-summary.json. The two remaining 4K groups ran separately after the first batch stopped on its 1080p240 mismatch. The historical private native-HDR capture-only and RTSS fixture sources remain unavailable; no replacement experiment or matched historical improvement is claimed.

Stock Moonlight 6.2.0 over Wi-Fi (October 12) #

Two published 2.2.9 sessions used the stock Moonlight Qt 6.2.0 client on the Radeon 780M laptop over its RZ616 Wi-Fi connection. The stream used HEVC SDR 1920x1080 at 60 FPS, requested 80 Mbps, WGC, gpu_compute_conversion=false, and minimum_fps_target=20. The encoder negotiated 62,988 kbps with 20% FEC. Each client ran for 165 seconds with hardware decoding, V-sync and frame pacing disabled. The physical source was 5120x1440 SDR at 240 Hz, with a full-screen 120 FPS motion probe completing naturally after 190 seconds (22,800 presents per run), without a synthetic audio tone. These are two current release repeats of the historical stock-client settings, not a new ABBA comparison or a matched improvement claim against earlier releases.

Host windows begin five seconds after the first verified RUNNING API sample and request 120 seconds. The unchanged analyzer uses the first and last samples inside that interval, yielding the effective durations below. Each run has 237 API samples at about 0.506-second intervals; per-frame trace counts exactly match the frame-counter deltas.

Run Effective seconds Frames / host sent FPS Host video-byte counter Mbps Send duration mean / p95 / p99 ms Send gap p95 / p99 ms
1 119.329 7,160 / 60.002 75.128 0.928 / 0.971 / 0.991 17.218 / 17.769
2 119.304 7,158 / 59.998 75.225 0.879 / 1.655 / 1.670 18.021 / 18.600

Both host windows had zero recorded replacements, IDR requests, reference invalidations, loss recoveries, send stutters, trace drops, writable waits, and send gaps longer than 1.5 frame periods. There were no FEC reports, so exact packet loss is unavailable. The client-reported global statistics cover its longer 165.017 / 165.045-second sessions:

Stock client statistic Run 1 Run 2
Incoming frame rate 60.05 FPS 59.99 FPS
Decoding frame rate 60.05 FPS 59.99 FPS
Rendering frame rate 59.99 FPS 59.99 FPS
Host processing min / max / average 2.1 / 3.6 / 2.5 ms 2.1 / 3.8 / 2.5 ms
Network-dropped frames 0.00% 0.00%
Jitter-dropped frames 0.03% 0.01%
Average network latency (reported variance) 1 ms (0 ms) 1 ms (5 ms)
Average decoding time 0.59 ms 0.50 ms
Average frame queue delay 0.02 ms 0.02 ms
Average rendering time, including monitor V-sync latency 0.48 ms 0.47 ms

These client statistics and host send measurements have distinct windows and endpoints. They must not be added together or presented as render-to-decoded picture age or glass-to-glass latency. The rounded 0.00% network-drop statistic is not proof of zero packet loss.

Both hosts stopped cleanly after the source completed and the stream session cleared. Both original client wrappers failed their immediate cleanup check, recording Client cleanup or preservation check failed. and processExited=false. Separate checks at 09:23:48 and 09:30:26 UTC confirmed that the owned client processes had exited without a second close or force kill. Original preferences, all four original pairings, the original GUI and Codex remained intact; the new benchmark pairing was retained. The original wrapper failures remain in the evidence and are not relabelled as passes. Final cleanup at 09:30:27 UTC found no owned test processes, stream UDP endpoints or clock UDP endpoints.

work\stock-summary.json indexes the complete statistics, original wrapper failures, later cleanup observations and verified evidence hashes. Host evidence is under work\measurements\stock-laptop\stock-20261012T090903-553635Z, in run-20261012T091735-588476Z and run-20261012T092522-181430Z; each retains its window, source ledger, samples, analyzer result and all host log rotations archived after shutdown. Client evidence is in work\laptop-exchange\incoming\stock-r1-20261012T092445.zip (SHA-256 87d6e69ec17a6e5693e54fea7614736b3cb393678bf253423eb1cd7d3ef729d3) and stock-r2-20261012T093027.zip (f47cbaeb8b8ff5a7ba79ccb5101daff6ad1686c7fc456b6d16a189107ebdca0d), with adjacent metadata/acknowledgements and stock-final-cleanup-20261012T093027.json. Each archive retains all ten stock statistics and a sanitized log; unsanitized client logs stayed on the laptop. The summary verifies the upload, sanitized-log, source ledger and archived-host-log hashes and preserves the fixture hashes.

AV1 host-processing p95 diagnosis (October 12) #

The morning DDX AV1 HDR result was 5.249 ms host mean, with 8.4 ms p95 in both repeats, versus 3.109 ms mean / 3.2 and 3.4 ms p95 in the retained October 8 rc.24 runs. At 120 FPS, 8.4 ms spans approximately one 8.33 ms frame interval. The following investigation separates the reported timer, release version, load phase and GPU dependencies.

Timer boundary. Native AMF starts the timer before capacity checking and GPU conversion submission, then stops it when QueryOutput returns the matching PTS. Unfinished capture-copy and conversion fences are inside that interval. The sender reconstructs a claim timestamp from this duration; it does not retain an independently sampled claim-to-packetization timer. Output-buffer copying occurs after the AMF sample and before that reconstruction. The inline sender then adds the small interval to its pre-packetization sample; the separate paced sender also includes residence in its sending queue and waiting for sender capacity. Only the frame-header value rounds to 0.1 ms. These loopback runs used inline sending. In these runs, the reported host duration matched the AMF stage duration within the wire rounding. Therefore the observed increase cannot be called slower hardware compression from the host counter alone.

Released-binary control. Run the original rc.24 and 2.2.9 packages in A/B/B/A order on the same current desktop: RX 7900 XT, physical 5120×1440 SDR at 240 Hz, native 1968×2184 HDR source at 120 Hz, AV1 HDR 120 FPS requesting 80 Mbps, identical AMF settings and four requested decoder threads. The established load is gpu_load 45 1000 60 200. Each receiver runs for 35 seconds with five seconds excluded as warmup.

Execution order Host mean / p95 (ms) Render-to-decoded picture age mean (ms)
rc.24 A1 3.444 / 7.1 12.362
2.2.9 B1 3.198 / 4.1 10.477
2.2.9 B2 3.019 / 3.3 9.123
rc.24 A2 3.043 / 3.3 11.198

The old release also develops the slow tail. These controls do not reproduce a persistent 2.2.9 increase from 3.11 to 5.25 ms. The historical desktop and run phase changed, so the earlier difference is not an isolated code-regression measurement. Reducing the current receiver's requested decoder threads to one gave 3.117 / 3.4 ms host mean / p95; both thread settings reported active_thread_type=0 and delay=0.

Load phase. Temporary tracing builds preserve each release's algorithms and add per-frame stage observations. A current DDX trace reproduced 4.909 / 8.0 ms host mean / p95: all 1,318 receiver samples at or above 6 ms belonged to even source IDs. The morning slow phase primarily affected odd IDs. Changing only the load cadence from 60 to 59 FPS moved the host-duration periodic component to 59 Hz and made slow-frame parity switch on a one-second beat. The two 59 FPS runs measured 3.347 / 6.5 ms and 3.405 / 6.3 ms. This ties the slow tail to the relative timing of the GPU workload and capture, rather than fixed odd/even image complexity.

Where the time goes. Receiver-aligned stages in the slow 60 FPS trace averaged 7.507 ms, versus 3.388 ms for frames below 6 ms. Every one of its 1,317 stage samples at or above 6 ms had an unfinished capture-copy fence at submission. CPU conversion submission averaged 0.047 ms, AMF submission 0.120 ms, and the p95 of each slow frame's longest post-submission polling gap was 0.587 ms. The old traced release's slow frames showed the same pending-copy pattern. The one-sample difference between the stage and receiver counts above comes from applying the 6 ms threshold before versus after 0.1 ms wire rounding.

A second tracing build also sampled the producer/acquired fence and the destination texture's prior-reader fence. Across three DDX runs, all 149 slow steady frames had an unfinished producer fence at submission and an already completed prior-reader fence. There were zero slow frames with a pending prior-reader fence, and each previous capture copy was complete at submission. Per-run means of the first observations of producer completion were 4.831 / 5.068 / 5.238 ms after encoder entry; copy completion was first observed at 4.995 / 5.263 / 5.542 ms, and encoded output at 7.182 / 7.381 / 7.828 ms. These are CPU-observed completion bounds, not exact GPU execution timestamps. They locate the dominant added wait at the D3D11 producer/acquisition dependency, before the compute copy, conversion and AMF compression. These CPU observations do not separate DWM execution from scheduling delay; the deeper ETW analysis below follows that dependency.

The independent GPU ETW trace corroborates this ordering. For slow PTS 2583 (capture-copy fence 2590), the host's Compute0 queue waited 3.541 ms for a producer signal from its graphics queue. The copy packet was released after that wait and completed in 0.284 ms. The multi-millisecond stall therefore precedes execution of the compute copy itself. All 13 slow frames in the selected 120-frame trace window joined to this producer dependency: wait mean 3.073 ms (range 2.465–3.541 ms), followed by copy DMA release-to-completion mean 0.152 ms. The other 107 frames' copy interval averaged 0.160 ms. These packet intervals are not the whole AMF frame duration.

The producer wait preserves correctness: Desktop Duplication can return before DWM has finished the image. The retained October 4 synchronization probe found 316/325 mismatches under load without that wait, versus 0/267 with it. Removing the fence or starting the reported timer after it would not be a demonstrated latency fix. Faster output polling and different AQ settings do not target the multi-millisecond dependency identified here.

Existing capture alternative. Repeat DDX/WGC/WGC/DDX with the 59 FPS load so that relative phase walks through the full cycle. Both use the same dependency-tracing binary and stream settings. Values below average the two runs' statistics; they are not pooled percentiles.

Capture Host mean / p95 (ms) Picture age mean / p95 (ms) Fresh pictures/s
DDX 3.126 / 3.7 10.597 / 15.116 118.924
WGC 3.038 / 3.4 11.345 / 15.580 119.936

WGC had no host samples at or above 6 ms in either repeat, but its picture age mean was 0.747 ms higher. The first DDX run delivered 117.840 fresh FPS with 64 source discontinuities; the second delivered 120.009 with none. This establishes a capture/cadence tradeoff, not a general end-to-end latency improvement. A future DDX optimization must reduce the producer dependency while retaining synchronized pixels, picture freshness and throughput. Moving the producer signal earlier to overlap command-list preparation was tested in the four-run experiment below. It did not establish a real picture-age improvement. No production algorithm or default was changed by this diagnosis.

Evidence and validation. All 17 diagnostic cases completed their ordinary stream checks and restored the physical display. All 70,535 nonzero source-ID/QPC pairs matched their original render ledgers exactly; 136 unreadable rows remain in the original denominators. The one GPU/CPU WPR case had zero lost trace events/buffers, but delivered 117.49 fresh FPS with additional source discontinuities; its performance is kept separate. Diagnostic builds and their logging are not replacement released-package headline measurements. The original morning and competitor tables remain unchanged.

Local evidence is under work\host-mean-trace: the twelve-case run-20261012T150121Z, five-case dependencies-20261012T152710Z, receiver-analysis.json, phase-analysis.json, stage-cause-summary.json, receiver-analysis-dependencies-20261012T152710Z.json and the second batch's analysis\stage-cause-summary.json. Each batch retains source ledgers, receiver CSVs, stage logs, source/binary hashes and display snapshots. The matched GPU queue records are in etw\gpu-window-analysis.json. The release sources are rc.24 ec84f48abbea67e0e3c3f8fc0c7c71e090831580 and 2.2.9 d0bfcb5e764095250b8f0e22fd5a0cb3656d5ea6. Temporary source changes, builds, raw evidence and credentials remain outside the repository.

DWM dependency and early-signal experiment #

Following the producer fence upstream in the same retained ETW window identified unfinished dwm.exe graphics work in all 13 slow frames. After DWM's fence completed, the host wait completed within 6–74 µs; its producer signal then completed 3–24 µs later. The multi-millisecond delay is therefore upstream of host signal completion.

For PTS 2583, DWM submitted its preceding graphics DMA packet at trace time 37.115663 s. That packet was released at 37.118722 s and completed at 37.119220 s: 3.059 ms before release, followed by 0.498 ms to completion. DWM's signal was queued at 37.115673 s but only completed at 37.119237 s. The host wait completed at 37.119271 s, and its producer signal at 37.119295 s. Queuing a signal must not be mistaken for completing it.

Twelve of the thirteen slow frames' preceding DWM packets spent 2.423–3.412 ms between submission and release. The remaining packet spent 3.358 ms between release and completion. All thirteen overlapped two graphics packets from the synthetic GPU load. No explicit wait packet on DWM's queue appeared in the retained window. An adjacent fast DWM packet took 31 µs from submission to release and 104 µs from release to completion. These events identify DWM graphics scheduling/completion as the upstream dependency; they do not identify a particular DWM shader or measure pure GPU execution time. The command-buffer byte count is not an image-copy size.

Candidate and acceptance rule. A scratch build moved the D3D11 producer Signal and Flush before shared-resource opening and copy-command-list preparation. Every synchronization wait, resource lifetime and timing boundary remained intact. Before running it, the plan required repeated reductions in host p95 and producer wait, without deterioration in picture age or fresh-picture cadence. Both arms used identical stage tracing, native HDR settings and the 59 FPS load described above. Four serial runs used unchanged/candidate/candidate/unchanged order:

Run Host mean / p95 / p99 (ms) Picture age mean / p95 (ms) Fresh pictures/s Host samples ≥6 ms
Unchanged A1 3.103 / 3.4 / 7.0 11.240 / 15.225 119.969 54/3582
Earlier signal B1 3.061 / 3.3 / 4.6 10.859 / 15.003 120.003 27/3571
Earlier signal B2 3.089 / 3.3 / 6.9 11.208 / 15.245 119.778 50/3576
Unchanged A2 3.138 / 3.4 / 7.5 10.896 / 14.962 119.979 81/3581

Host p95 decreased by 0.1 ms in both candidate runs. However, the picture-age improvement did not repeat: B1 versus A1 improved its mean by 0.382 ms, while B2 versus A2 increased it by 0.312 ms. Averaging each arm's two run statistics, picture-age mean changed by −0.035 ms and p95 by +0.031 ms; these are not pooled percentiles. B2 omitted six source pictures, versus one in either control. This does not establish candidate-caused degradation, but it fails the combined acceptance rule.

All 208 stage samples at or above 6 ms still had a pending producer fence and a completed prior-reader fence. Per-run first-observed producer completion means were 4.944 / 5.062 / 4.776 / 5.007 ms after encoder entry. These CPU-observed bounds show no consistent shortening of the slow dependency. The counts at or above 6 ms differ between stage and wire measurements because of 0.1 ms wire rounding. The candidate was not adopted, and the conditional 60 FPS confirmation was not run. Acquisition timing remains an untested optimization direction; neither bypassing the correctness fence nor moving the timer past it would resolve this measured delay.

Validation and provenance. All four stream gates, binary-hash checks, native-source modes and physical-display restoration checks passed. All 16,646 nonzero source-ID/QPC pairs matched exactly; 49 zero-ID startup rows remain in the raw evidence. All 14,310 steady frames mapped to stage records. No production source, installed binary, capture default or migration behavior changed.

The batch is work\host-mean-trace\early-signal-20261012T163740Z, with analysis\early-signal-comparison.json and adjacent raw logs and ledgers. The parent directory retains early-signal-plan.json, early-signal-receiver-judgment.json and receiver-analysis-early-signal-20261012T163740Z.json. Exact DWM fence/value joins are in etw\producer-chain-analysis.json and etw\producer-chain-summary.json. The instrumented unchanged host SHA-256 is 9e11727043573faecdaf536f3bedb785407852a5e888267f4eefd04f8b51fc1a; the candidate is 3a001534068d0756ee8b742eccc53aa0a8c0c2c939fb2edaeae1aa8c04277182.

DDX post-publication hold experiment #

A second scratch candidate tested frame ownership timing. After publishing each DDX snapshot and waking encoders, it requested one existing polling interval (500 µs) before the next release/acquire. The timer can overshoot; 500 µs is not a measured maximum hold. The current picture still publishes immediately, and all GPU waits, texture-ownership rules and encoder timer boundaries remain unchanged. The earlier-signal change was removed. This tests whether holding the duplication surface briefly changes subsequent DWM work; it does not assume that the retained trace contains redundant desktop copies.

The same instrumented baseline, native HDR settings and 59 FPS load were run in A/B/B/A order. Acceptance required repeatable picture-age and host-tail improvement with fresh-picture cadence preserved, before any 60 FPS confirmation or production change.

Run Host mean / p95 / p99 (ms) Picture age mean / p95 (ms) Fresh pictures/s Omitted source pictures
Unchanged A1 3.109 / 3.5 / 6.6 10.808 / 14.853 118.880 33
Hold B1 3.058 / 3.3 / 4.4 10.136 / 14.636 120.001 0
Hold B2 3.166 / 4.1 / 7.5 10.411 / 14.616 119.979 1
Unchanged A2 3.498 / 7.2 / 8.0 10.342 / 13.740 119.998 0

The first candidate run improved on the first control, but the second increased picture-age mean by 0.069 ms and p95 by 0.876 ms against the final control. Both candidate picture-age p95 values exceeded the final control, despite its higher host p95. Across all four runs, 544/544 stage samples at or above 6 ms still had a pending producer fence, a completed prior-reader fence and a completed previous copy. Slow-frame first-observed producer completion means were 4.648 / 4.843 / 4.688 / 4.709 ms after encoder entry: the same multi-millisecond dependency remained. These are CPU-observed bounds, not GPU execution durations.

The acceptance rule was not met, so the hold was not adopted and the conditional 60 FPS confirmation was not run. Baseline variation also prevents claiming that the candidate caused a regression. No production code, capture defaults or installed settings changed.

All four ordinary stream checks, binary hashes, native source modes and physical-display restoration checks passed. All 16,643 nonzero source ID/QPC pairs matched their render ledgers, and 14,293 steady frames aligned to stage records without flags. The 47 zero-ID startup rows remain in the evidence. Raw source omissions were retained in the results.

Evidence is work\host-mean-trace\posthold-20261012T174145Z, including analysis\posthold-comparison.json, the predeclared plan, source hashes, logs and ledgers. The parent directory retains receiver-analysis-posthold-20261012T174145Z.json. The baseline host hash is unchanged from the earlier-signal experiment; the hold candidate SHA-256 is 97a570378033c0cbbedf33c7785903bfb2072b69c9ed847b036074e961823612.

View this page on GitHub