fix(pf-vdisplay): verify the size KWin actually built the virtual output at (≤60 Hz path) #194

Merged
enricobuehler merged 1 commits from worktree-moonlight-4k60-dims into main 2026-08-13 13:52:32 +00:00
1 Commits
Author SHA1 Message Date
enricobuehler 6237e3d0a3 fix(pf-vdisplay): the KWin ≤60 Hz path never checked what KWin actually built, and the log reported the request as if it were a readback
ci / bun-nix (pull_request) Successful in 37s
ci / web (pull_request) Successful in 1m14s
ci / docs-site (pull_request) Successful in 2m31s
ci / rust-arm64 (pull_request) Successful in 4m14s
android / android (pull_request) Successful in 4m53s
ci / rust (pull_request) Successful in 25m8s
A 4K60 GameStream session captured 1920x1080. `create()` asked KWin for
3840x2160, KWin built something else, and nothing compared the two: only the
>60 Hz arm read anything back, and it gets that for free because it installs a
custom mode. The ≤60 Hz arm installs nothing, which is exactly why it never
noticed.

The line that should have caught it was the one that hid it. `spawn_vout`
returns a node id, never a size, so

    tracing::info!(node_id, width, height, "KWin virtual output ready")

was echoing the REQUEST — the field log stated 3840x2160 while the output was
1080p, and the first pass at diagnosing this was done against that number. It
now logs `requested_w`/`requested_h`, and the readback sits under it.

Unverified, the mismatch was silent and total. `final_dims` carried the request
forward, so `apply_topology`, `clear_replication_source` and
`resolve_kscreen_addr` — all of which resolve by dims — quietly missed their own
output, leaving the stream neither primary nor de-mirrored; and the encoder
opened at the captured size, handing the client a bitstream that disagreed with
the resolution it had configured its decoder from.

Suspected trigger is KWin restoring per-output mode/scale from
kwinoutputconfig.json, which is keyed by output NAME — and ours is deliberately
stable across sessions so KDE reapplies that client's scaling (Stage 3). The
feature and the failure are the same mechanism.

- `kwin_output_mgmt::actual_dims()` reads the output's real mode + scale.
  Resolution is by name alone, so it declines unless EXACTLY one output carries
  our prefix: two means a supersede is in flight, and the dims filter is the
  only thing that can tell the replacement from the predecessor whose name it
  reuses. Failing closed keeps this a pure addition.
- On a mismatch, re-assert the requested mode through the same
  `set_custom_mode` install+select the sacrificial birth already uses (an output
  at a size we don't want, moved to one we do) and arm `expect_exact_dims` so
  the capturer holds frames until the screencast renegotiates. 60 Hz is
  requested, not `mode.refresh_hz`: only the size is wrong here, and asking for
  the client's rate would install a 30 Hz mode for a 30 fps client.
- If KWin refuses the correction, report the size that is REALLY there rather
  than the request, so the dims-keyed resolves and the encoder key on reality,
  and say in the log how to clear the stored entry.
- Scale is logged, never corrected — a non-unity scale here is the Stage 3
  feature working, not a fault.
- `mode_satisfies()` extracts the acceptance predicate both arms now share, so
  they cannot drift into disagreeing about what "we got what we asked for"
  means. Tested: a restored 1080p does not pass for a 4K request, a CVT-aligned
  width does, and the slack is bounded, one-sided and width-only.

The stream-side warning is reworded but deliberately still NOT fatal: mirroring
a pinned monitor streams a size the client never negotiated BY DESIGN (§7.3 — a
panel runs at the mode its owner set and the client scales), so refusing the
mismatch would break every mirror session. It now names both causes and states
what the client actually does with the stream.

Does not claim to close the Xbox Moonlight disconnect it was found through: that
client's IDR storm begins ~4.6 s after the first frame, which a decoder simply
unable to handle the size would not do. The 1080p-instead-of-4K is a real defect
on its own terms and is what this fixes.
2026-08-13 15:26:35 +02:00