The retry loop stops eating the restore that re-lights the desk #430

Merged
enricobuehler merged 1 commits from worktree-hyprland-exclusive-restore-strand into main 2026-08-28 20:18:17 +00:00
Owner

Fixes the topology: exclusive dark-desk report from home-omarchy-1 (2026-08-28): after a failed session the operator's heads were never re-enabled, and every subsequent connect failed the same way because the desk it would stream was already gone.

The cause is not the registry

The report's two hypotheses both describe a registry entry stuck Active. For Hyprland that entry does not exist.

registry::acquire returns pass-through at its real.remote_fd.is_some() check (registry.rs:1285), which sits several statements above its vd.take_topology_restore() call (registry.rs:1305). Hyprland and sway both carry a portal fd, so the registry never takes their restore and never builds an Entry for them. The per-group hand_off_restore / take_expired machinery is real, unit-tested and correct — it just serves KWin only.

That makes Drop the only thing that ever runs the restore on these two backends. It was empty by the time it ran:

self.pending_restore = (!disabled.is_empty()).then(|| );   // unconditional assign

The host opens the backend once (native/stream.rs:4852) and lends it to build_pipeline_with_retry for up to eight attempts, so createapply_topology runs repeatedly against that one slot. Attempt 1 disables the heads and prepares the restore; attempts 2–8 correctly find nothing left to disable, prepare None, and assign it over attempt 1's closure — which is dropped, never called. Build fails → vd drops → Drop finds None → desk stays dark until a hand-run hyprctl reload.

This accounts for the journal exactly, including the part that misled the diagnosis: disabled=[…], then N× nothing to disable, and no restore_heads line of either polarity. That absence means the closure was discarded — not that the reload failed.

The change

backend::stash_topology_restore makes the slot first-wins; all three wlr-family backends write through it. First is also the right list, not merely the surviving one: attempt 1 looked at the desk while it was still lit, so its set is every head that was on — a later attempt can only see a subset.

KWin gets the same guard even though the registry drains its slot after every create and it cannot strand today. One of three identical sites left unguarded is how this comes back.

The pending_restore doc comments on Hyprland and sway are corrected too. They claimed the registry picks the restore up and Drop is a mere backstop; that claim is precisely what made a stranded restore read as a registry bug.

No change to restore_headshyprctl reload remains the measured restore, per the long comment there.

Verification

  • cargo fmt --check clean.
  • xcheck.sh linux clippy and xcheck.sh windows clippy clean, both verified non-vacuous (Checking pf-vdisplay present after a touch).
  • Also a plain Windows clippy without --all-targets: the new helper is only called from Linux-only modules, so this rules out the "dead outside tests" warning that --all-targets hides. Clean.
  • 263 tests pass / 0 fail / 5 ignored in rust:1.96; the 4 new tests confirmed running by name.
  • The regression test was proven to catch the bug, not merely to pass: with the guard backed out, eight_failed_attempts_do_not_strand_the_restore fails with "the retry loop stranded the restore — the desk stays dark".

Two things deliberately left alone

On-glass proof is owed. It needs a host build on home-omarchy-1 plus the induced failure (qm set 126 --vga virtio + reboot), which reconfigures and reboots the VM — not done unasked. Read-only check on 2026-08-28: the box is healthy, HDMI-A-1 reports "disabled": false.

The wlr-family restore is still per-session, not per-group. Because the registry never takes these, two concurrent exclusive sessions will have the first to end re-enable the heads under the second. Closing that means giving the pass-through path group bookkeeping it does not have today — #284's call, not a drive-by restructure. It is now written down at the field instead of contradicted there.

Refs #284.

Fixes the `topology: exclusive` dark-desk report from `home-omarchy-1` (2026-08-28): after a **failed** session the operator's heads were never re-enabled, and every subsequent connect failed the same way because the desk it would stream was already gone. ## The cause is not the registry The report's two hypotheses both describe a registry entry stuck `Active`. For Hyprland that entry **does not exist**. `registry::acquire` returns pass-through at its `real.remote_fd.is_some()` check (`registry.rs:1285`), which sits several statements *above* its `vd.take_topology_restore()` call (`registry.rs:1305`). Hyprland and sway both carry a portal fd, so the registry never takes their restore and never builds an `Entry` for them. The per-group `hand_off_restore` / `take_expired` machinery is real, unit-tested and correct — it just serves **KWin only**. That makes `Drop` the *only* thing that ever runs the restore on these two backends. It was empty by the time it ran: ```rust self.pending_restore = (!disabled.is_empty()).then(|| …); // unconditional assign ``` The host opens the backend **once** (`native/stream.rs:4852`) and lends it to `build_pipeline_with_retry` for up to eight attempts, so `create` → `apply_topology` runs repeatedly against that one slot. Attempt 1 disables the heads and prepares the restore; attempts 2–8 *correctly* find nothing left to disable, prepare `None`, and assign it over attempt 1's closure — which is dropped, never called. Build fails → `vd` drops → `Drop` finds `None` → desk stays dark until a hand-run `hyprctl reload`. This accounts for the journal exactly, including the part that misled the diagnosis: `disabled=[…]`, then N× `nothing to disable`, and **no `restore_heads` line of either polarity**. That absence means the closure was discarded — not that the reload failed. ## The change `backend::stash_topology_restore` makes the slot first-wins; all three wlr-family backends write through it. First is also the *right* list, not merely the surviving one: attempt 1 looked at the desk while it was still lit, so its set is every head that was on — a later attempt can only see a subset. KWin gets the same guard even though the registry drains its slot after every `create` and it cannot strand today. One of three identical sites left unguarded is how this comes back. The `pending_restore` doc comments on Hyprland and sway are corrected too. They claimed the registry picks the restore up and `Drop` is a mere backstop; that claim is precisely what made a stranded restore read as a registry bug. No change to `restore_heads` — `hyprctl reload` remains the measured restore, per the long comment there. ## Verification - `cargo fmt --check` clean. - `xcheck.sh linux clippy` and `xcheck.sh windows clippy` clean, both verified **non-vacuous** (`Checking pf-vdisplay` present after a `touch`). - Also a plain Windows clippy **without** `--all-targets`: the new helper is only called from Linux-only modules, so this rules out the "dead outside tests" warning that `--all-targets` hides. Clean. - 263 tests pass / 0 fail / 5 ignored in `rust:1.96`; the 4 new tests confirmed running by name. - **The regression test was proven to catch the bug**, not merely to pass: with the guard backed out, `eight_failed_attempts_do_not_strand_the_restore` fails with *"the retry loop stranded the restore — the desk stays dark"*. ## Two things deliberately left alone **On-glass proof is owed.** It needs a host build on `home-omarchy-1` *plus* the induced failure (`qm set 126 --vga virtio` + reboot), which reconfigures and reboots the VM — not done unasked. Read-only check on 2026-08-28: the box is healthy, `HDMI-A-1` reports `"disabled": false`. **The wlr-family restore is still per-session, not per-group.** Because the registry never takes these, two concurrent `exclusive` sessions will have the first to end re-enable the heads under the second. Closing that means giving the pass-through path group bookkeeping it does not have today — #284's call, not a drive-by restructure. It is now written down at the field instead of contradicted there. Refs #284.
enricobuehler added 1 commit 2026-08-28 19:37:07 +00:00
The retry loop stops eating the restore that re-lights the desk
ci / web (pull_request) Successful in 55s
ci / docs-site (pull_request) Successful in 1m1s
ci / bun-nix (pull_request) Successful in 22s
ci / docs-drift (pull_request) Successful in 25s
ci / rust-arm64 (pull_request) Successful in 2m11s
android / android (pull_request) Successful in 5m30s
ci / rust (pull_request) Successful in 7m48s
aa8c5d0e0e
`topology: exclusive` on Hyprland left the operator's heads disabled after a
failed session, recoverable only by a hand-run `hyprctl reload`. The cause is
neither a failing reload nor the registry: it is one line in `apply_topology`.

The host opens the vdisplay backend ONCE and lends it to
`build_pipeline_with_retry` for up to eight attempts, so `create` — and with it
`apply_topology` — runs repeatedly against one instance's `pending_restore`
slot. Attempt 1 disables the heads and prepares the restore; attempts 2..n then
correctly find nothing left to disable (attempt 1 already darkened everything)
and prepare `None`. The slot was assigned unconditionally, so attempt 2 dropped
attempt 1's closure on the floor. When the build finally failed and the backend
dropped, its `Drop` had nothing to run — which is exactly why the field journal
shows `disabled=[...]`, ten `nothing to disable`, and no `restore_heads` line of
either polarity.

`stash_topology_restore` makes the slot first-wins. First is also the RIGHT
list, not merely the surviving one: attempt 1 looked at the desk while it was
still lit, so its set is every head that was on; a later attempt can only see a
subset.

Two corrections to what the code claimed about itself, since they are what made
this read as a registry bug. The Hyprland and sway `pending_restore` docs said
the registry picks the restore up and `Drop` is a mere backstop. It does not:
both backends carry a portal fd, so `registry::acquire` returns them as
pass-through at the `remote_fd.is_some()` check, several statements ABOVE its
`take_topology_restore()` call. Nothing ever lifts these into a display group,
and `Drop` is the only thing that runs them. The per-group hand-off machinery
is real, tested and correct — it just serves KWin, not these two.

That leaves a genuine per-session restore on the wlr-family backends (two
concurrent exclusive sessions: the first to end re-enables the heads under the
second), which is now written down at the field rather than contradicted there.
Closing it needs group bookkeeping the pass-through path does not have, so it
stays #284's call, not a drive-by restructure.

KWin gets the same guard although its slot is drained by the registry after
every create, so it cannot strand today — one of three identical sites left
unguarded is how this comes back.

Four tests in `backend.rs` cover the failure shape, the one that matters being
eight attempts with only the first having heads to disable. Verified by backing
the guard out: it fails with "the retry loop stranded the restore — the desk
stays dark".
enricobuehler merged commit 37902ebac8 into main 2026-08-28 20:18:17 +00:00
enricobuehler deleted branch worktree-hyprland-exclusive-restore-strand 2026-08-28 20:18:27 +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#430