KWin's stored setup darkened the desk behind our back, and a TV remote could not leave the first settings tab #397

Merged
enricobuehler merged 4 commits from worktree-kwin-stored-config-strand into main 2026-08-26 14:44:47 +00:00
Owner

Two fixes and one follow-up feature from one Bazzite/KDE + Chromecast field report (0.31.4, BigRog).

1. Monitors cycling off/on, ending dark or at wrong resolutions — on Default and Shared Desktop

The report's log has the tell: an exclusive apply on a triple-monitor desk logged
also_disabled=[] — the topology apply found zero enabled physicals to disable, so the
teardown restore had nothing to re-enable.

KWin persists output state per monitor set (kwinoutputconfig.json). Creating our virtual
output changes the set, and a set that ever ran exclusive has "physicals disabled" stored — so KWin
disables the physicals itself, in the ~30 ms between our output's creation and the apply's
enumeration. From there the loop feeds itself:

  • exclusive captures nothing → teardown restores nothing → the desk ends dark, and KWin's own
    recovery improvises modes (the "some or all at different resolutions" the report describes);
  • under Shared Desktop / Extend, the same stored setup switches off screens those topologies
    promise never to touch ("previously never able to stay on, even with shared");
  • every session boundary flips the monitor set → modesets on every head → the off/on/off/on churn,
    which is also what keeps the virtual display renegotiating (and is when its stored 1080p entry
    can land on it — the existing actual_dims re-assert handles the create-time case).

The fix, all in the KWin backend (pf-vdisplay):

  • create() snapshots the enabled physicals before the virtual output exists — the only read
    KWin's reaction cannot have polluted.
  • Exclusive: the snapshot joins the restore list (union_restore), so an output KWin disabled
    before we could see it is still re-enabled at teardown.
  • Extend/Primary: reenable_stranded() undoes KWin's stored-config disable — immediately, and once
    more after 2 s for a late apply. One shot each, never a loop.
  • The group-teardown restore gains a delayed verify that re-asserts after the output reclaim,
    under the without-us set — a user-applied config KWin then persists, healing the stored setup
    instead of re-fighting it next session. It stands down if a quick reconnect's managed output
    already owns the topology.

One-time manual step for an already-stranded box (the reporter's current state): the snapshot
cannot see outputs that were dark before the update. Re-enable the monitors once in System
Settings → Display (or kscreen-doctor output.<name>.enable) while no stream is live; that
writes the healthy without-us setup back and the loop cannot re-form.

2. Chromecast: settings tabs unreachable with just the TV remote

The tab split put section switching on L1/R1, Tab and PgUp/PgDn — a Chromecast remote has none of
those, and D-pad left/right already mean "adjust the focused row". With the pad dropped (as
Chromecast pads do), every tab but the first was unreachable.

Up from the list's top row now steps onto the tab strip instead of recoiling: left/right travel the
ring (wrapping, like the shoulders), Down or A drop back into the rows, B still leaves the screen.
The strip shows the handoff (brighter pill, ‹ › chevrons), the rows rest their focus ring while the
strip holds it, and the hint bar names the D-pad's meaning up there. The gamepad path is untouched.

3. The legend speaks the device in hand

The hint legend only ever spoke gamepad (or desktop-keyboard keycaps), whatever was driving: a TV
remote got Enter/Esc/Tab, and a Switch pad's badges read the Xbox letters — "A Select" over the
button engraved B.

The shell now notes what drove last at every input seam (the Android host loop labels its two menu
paths — discrete events are the remote, the pad sampler is the pad; the desktop overlay's menu
channel is pad by construction; the keyboard path notes itself), and the legend resolves per frame:

  • a pad speaks its own family — PlayStation shapes, Nintendo letters (both pairs swapped, the
    way the pad is engraved), ABXY for the rest;
  • keys on Android are a TV remote: an OK badge, the ↩ return arrow for back, the section hint
    pointing at the D-pad path (▲) from fix 2, and the Y/X hints hidden outright — a remote has
    neither, and advertising a button the device cannot press is worse than silence;
  • keys on the desktop stay keyboard keycaps; an untouched console follows the connected pad, else
    the platform's key device.

The controller chip grows a remote mark and says "TV remote" when that is what is driving. A
pointer press changes nothing — a tap says nothing about the buttons in the user's other hand.

Verification

  • cargo clippy --all-targets -- -D warnings clean for pf-vdisplay, pf-console-ui, and
    punktfunk-client-android (arm64 Linux container); cargo test -p pf-vdisplay: 256 passed.
  • New unit tests: union_restore coverage; dpad_alone_reaches_every_tab; the glyph-style matrix
    (the_legend_follows_what_drives), Nintendo letter mapping, and remote hint hiding — the
    pf-console-ui suites run in ci / rust (the local arm64 image has no SDL3 to link tests).
  • The screen dump gains 11-home-remote / 11b-settings-remote for the eyeball pass.
  • Not run: a live KWin session against the stored-config scenario, and the dump render itself
    (Linux-only harness).
Two fixes and one follow-up feature from one Bazzite/KDE + Chromecast field report (0.31.4, `BigRog`). ## 1. Monitors cycling off/on, ending dark or at wrong resolutions — on Default *and* Shared Desktop The report's log has the tell: an exclusive apply on a triple-monitor desk logged `also_disabled=[]` — the topology apply found **zero enabled physicals to disable**, so the teardown restore had nothing to re-enable. KWin persists output state per **monitor set** (`kwinoutputconfig.json`). Creating our virtual output changes the set, and a set that ever ran exclusive has "physicals disabled" stored — so KWin disables the physicals **itself**, in the ~30 ms between our output's creation and the apply's enumeration. From there the loop feeds itself: - exclusive captures nothing → teardown restores nothing → the desk ends dark, and KWin's own recovery improvises modes (the "some or all at different resolutions" the report describes); - under Shared Desktop / Extend, the same stored setup switches off screens those topologies promise never to touch ("previously never able to stay on, even with shared"); - every session boundary flips the monitor set → modesets on every head → the off/on/off/on churn, which is also what keeps the virtual display renegotiating (and is when its stored 1080p entry can land on it — the existing `actual_dims` re-assert handles the create-time case). The fix, all in the KWin backend (`pf-vdisplay`): - `create()` snapshots the enabled physicals **before** the virtual output exists — the only read KWin's reaction cannot have polluted. - Exclusive: the snapshot joins the restore list (`union_restore`), so an output KWin disabled before we could see it is still re-enabled at teardown. - Extend/Primary: `reenable_stranded()` undoes KWin's stored-config disable — immediately, and once more after 2 s for a late apply. One shot each, never a loop. - The group-teardown restore gains a delayed verify that re-asserts **after** the output reclaim, under the without-us set — a user-applied config KWin then persists, healing the stored setup instead of re-fighting it next session. It stands down if a quick reconnect's managed output already owns the topology. **One-time manual step for an already-stranded box** (the reporter's current state): the snapshot cannot see outputs that were dark before the update. Re-enable the monitors once in System Settings → Display (or `kscreen-doctor output.<name>.enable`) **while no stream is live**; that writes the healthy without-us setup back and the loop cannot re-form. ## 2. Chromecast: settings tabs unreachable with just the TV remote The tab split put section switching on L1/R1, Tab and PgUp/PgDn — a Chromecast remote has none of those, and D-pad left/right already mean "adjust the focused row". With the pad dropped (as Chromecast pads do), every tab but the first was unreachable. Up from the list's top row now steps onto the tab strip instead of recoiling: left/right travel the ring (wrapping, like the shoulders), Down or A drop back into the rows, B still leaves the screen. The strip shows the handoff (brighter pill, ‹ › chevrons), the rows rest their focus ring while the strip holds it, and the hint bar names the D-pad's meaning up there. The gamepad path is untouched. ## 3. The legend speaks the device in hand The hint legend only ever spoke gamepad (or desktop-keyboard keycaps), whatever was driving: a TV remote got Enter/Esc/Tab, and a Switch pad's badges read the Xbox letters — "A Select" over the button engraved B. The shell now notes what drove last at every input seam (the Android host loop labels its two menu paths — discrete events are the remote, the pad sampler is the pad; the desktop overlay's menu channel is pad by construction; the keyboard path notes itself), and the legend resolves per frame: - a pad speaks its own family — PlayStation shapes, **Nintendo letters** (both pairs swapped, the way the pad is engraved), ABXY for the rest; - keys on Android are a **TV remote**: an OK badge, the ↩ return arrow for back, the section hint pointing at the D-pad path (▲) from fix 2, and the Y/X hints hidden outright — a remote has neither, and advertising a button the device cannot press is worse than silence; - keys on the desktop stay keyboard keycaps; an untouched console follows the connected pad, else the platform's key device. The controller chip grows a remote mark and says "TV remote" when that is what is driving. A pointer press changes nothing — a tap says nothing about the buttons in the user's other hand. ## Verification - `cargo clippy --all-targets -- -D warnings` clean for `pf-vdisplay`, `pf-console-ui`, and `punktfunk-client-android` (arm64 Linux container); `cargo test -p pf-vdisplay`: 256 passed. - New unit tests: `union_restore` coverage; `dpad_alone_reaches_every_tab`; the glyph-style matrix (`the_legend_follows_what_drives`), Nintendo letter mapping, and remote hint hiding — the pf-console-ui suites run in `ci / rust` (the local arm64 image has no SDL3 to link tests). - The screen dump gains `11-home-remote` / `11b-settings-remote` for the eyeball pass. - Not run: a live KWin session against the stored-config scenario, and the dump render itself (Linux-only harness).
enricobuehler added 2 commits 2026-08-26 07:23:41 +00:00
A Bazzite triple-monitor field report: monitors cycling off/on around every
session, sessions ending with some or all monitors dark or at the wrong
resolution, and an exclusive apply logging also_disabled=[] on a desk with
three enabled physicals.

KWin persists output state per MONITOR SET (kwinoutputconfig.json). Creating
our virtual output changes the set, and a set that ever ran exclusive has
'physicals disabled' stored — so KWin disables them ITSELF, in the window
between our output's creation and the topology apply's enumeration. The apply
then finds nothing enabled, captures nothing for restore, and teardown
re-enables nothing: the desk stays dark, and KWin's own recovery improvises
modes. Under Extend/Primary the same stored setup switched screens off that
those topologies promise never to touch.

Three moves, all in the KWin backend:

* create() snapshots the enabled physicals BEFORE the virtual output exists —
  the only read KWin's reaction cannot have polluted.
* Exclusive: the snapshot joins the restore list (union_restore) — an output
  KWin disabled before we could see it is still re-enabled at teardown.
  Extend/Primary: reenable_stranded() puts the stored-config casualties back
  on, immediately and once more after a short delay (KWin can apply the
  stored setup after our first read). One shot each, never a loop.
* The group-teardown restore gains a delayed verify that re-asserts AFTER our
  output is reclaimed — under the without-us set, so KWin persists the heal
  into the stored setup instead of re-fighting us next session. It stands
  down if a new session's managed output already owns the topology.

A box stranded BEFORE this fix stays stranded once: the snapshot cannot see
outputs that were already dark. Re-enabling them once in System Settings
while no stream is live writes the healthy without-us setup back, and the
loop cannot re-form.
fix(console-ui): the settings tabs answered only to shoulders — Up from the top row puts a TV remote's D-pad on the strip
ci / web (pull_request) Successful in 1m4s
ci / rust-arm64 (pull_request) Successful in 1m25s
ci / docs-drift (pull_request) Successful in 24s
ci / docs-site (pull_request) Successful in 1m6s
ci / bun-nix (pull_request) Successful in 1m11s
ci / rust (pull_request) Failing after 4m4s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 4m27s
android / android (pull_request) Canceled after 3m42s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Canceled after 1m17s
b64ac3cb32
A Chromecast field report: with only the TV remote — the pad had dropped off,
as Chromecast pads do — every settings tab but the first was unreachable. The
split of the one 30-row scroll into tabs put section switching on L1/R1, Tab
and PgUp/PgDn, and a remote has none of those; the D-pad's left/right already
mean 'adjust the focused row'.

Up from the list's top row now steps onto the tab strip instead of recoiling:
left/right travel the ring (wrapping, like the shoulders), Down or A drop
back into the rows of the tab that's showing, B still leaves the screen, and
the shoulders keep working from either focus. The strip shows the handoff —
brighter pill, ‹ › chevrons — the rows rest their focus ring while it holds
focus, and the hint bar names the D-pad's meaning up there. A pointer press
on the rows takes the focus back.

The gamepad path is untouched: nothing changes until an Up is pressed on the
top row, which used to be a boundary recoil.
enricobuehler added 1 commit 2026-08-26 07:29:41 +00:00
style: the two folds rustfmt wanted (ci / rust fmt gate)
ci / web (pull_request) Successful in 1m16s
ci / docs-site (pull_request) Successful in 1m21s
ci / bun-nix (pull_request) Successful in 21s
ci / docs-drift (pull_request) Successful in 20s
ci / rust-arm64 (pull_request) Successful in 4m25s
android / android (pull_request) Successful in 8m37s
ci / rust (pull_request) Successful in 9m9s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m0s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m57s
8c4b1b8c62
enricobuehler added 1 commit 2026-08-26 14:42:16 +00:00
feat(console-ui): the legend speaks the device in hand — remote marks on TV, Nintendo's own letters, keys vs pad tracked at the source
ci / docs-drift (pull_request) Successful in 37s
ci / bun-nix (pull_request) Successful in 1m6s
ci / web (pull_request) Successful in 1m24s
ci / docs-site (pull_request) Successful in 1m26s
ci / rust-arm64 (pull_request) Successful in 1m54s
android / android (pull_request) Successful in 6m7s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m54s
ci / rust (pull_request) Successful in 6m40s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m14s
4399664217
Follow-up from the same Chromecast field report: the hint legend only ever
spoke gamepad (or desktop-keyboard keycaps), whatever was actually driving.
A TV remote got Enter/Esc/Tab — keys it does not have — and a Switch pad's
badges read the Xbox letters, so 'A Select' sat over the button engraved B.

The shell now notes WHAT drove last at every input seam — the Android host
loop labels its two menu paths (discrete events are the remote, the pad
sampler is the pad), the desktop overlay's menu channel is pad by
construction, and the keyboard path notes itself. Each frame the legend
resolves from that:

* a pad speaks its own family — PlayStation shapes, Nintendo letters (both
  pairs swapped, the way the pad is engraved), ABXY for the rest;
* keys on Android are a TV remote: an OK badge for select, the ↩ return
  arrow for back, the section hint pointing at the D-pad path (▲) that
  reaches the tab strip, and the Y/X hints hidden outright — a remote has
  neither, and advertising a button the device cannot press is worse than
  silence;
* keys on the desktop stay keyboard keycaps;
* nothing has driven yet: the connected pad's family, else the platform's
  key device — so an untouched console reads right on every device.

The controller chip grows a remote mark and says 'TV remote' when that is
what is driving; a pointer press changes nothing, since a tap says nothing
about the buttons in the user's other hand. The screen dump gains the two
remote frames (Home + Settings) for the eyeball pass.
enricobuehler marked the pull request as ready for review 2026-08-26 14:44:27 +00:00
enricobuehler scheduled this pull request to auto merge when all checks succeed 2026-08-26 14:44:40 +00:00
enricobuehler merged commit fc6060f274 into main 2026-08-26 14:44:47 +00:00
enricobuehler deleted branch worktree-kwin-stored-config-strand 2026-08-26 14:44:51 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#397