Hyprland/Sway black client — the wlr-family backends asserted a cursor mode instead of negotiating it #216

Merged
enricobuehler merged 1 commits from worktree-hyprland-cursor-mode-negotiation into main 2026-08-14 08:39:20 +00:00
Owner

Field report 2026-08-14: black client on Hyprland, pipeline build failed in the host log, unavailable cursor mode 4 from the portal. One failure, not three.

What was wrong

hyprland.rs and wlroots.rs both hardcoded portal CursorMode::Metadata whenever the session had negotiated the cursor channel, and never asked the backend what it supports:

let cursor_mode = if hw_cursor { CursorMode::Metadata } else { CursorMode::Embedded };

That is not a soft failure. xdg-desktop-portal's frontend validates the requested mode against the backend's AvailableCursorModes and fails the call with "Unavailable cursor mode %x" (%x → hex → the 4 in the report) before the backend ever sees it. So select_sources failed, build_pipeline gave up, and the client got a black screen.

Measured on glass (.21) — it is worse than the report suggested

Against a live Hyprland 0.56.2 session with xdg-desktop-portal-hyprland 1.4.1 and xdg-desktop-portal 1.22.1 — all current — with xdph properly attached ([screencopy] init successful):

AvailableCursorModes = u 3     # hidden(1) | embedded(2)

Read identically on the backend impl interface and on the frontend. xdph does not offer the metadata cursor at all, so this broke every cursor-forward session on current Hyprland — not just old installs, and updating the portal would not have helped.

Two things worth recording because they are easy to get wrong:

  • The rejecter is the frontend, not xdph. xdph 1.4.1 would itself have fallen back ([screencopy] unsupported cursor_mode {}, fallback to {}), but never gets the chance.
  • xdph PR #366 did not add metadata support — it fixed a cursor enum mishandled as a bool. "Update your portal" is not a fix.

xdpw is the same story from the other end: its screencast.c refuses METADATA outright ("dbus: unsupported cursor mode requested, cancelling"), so every cursor-forward session on Sway/wlroots cancelled its cast too.

The fix

New pf-vdisplay/src/vdisplay/linux/portal_cursor.rs: a negotiation ladder against the advertised bitfield, called by both backends after proxy creation.

pf-capture has always negotiated (portal::choose_cursor_mode), but pf-vdisplay may not depend on pf-capture — its Cargo.toml says "never on capture/inject or the orchestrator" — so the ladder is deliberately restated, with a comment on both copies to keep them in step.

The downgrade is graceful rather than merely survivable: with the portal on Embedded no SPA_META_Cursor arrives, so the host feeds the cursor channel nothing and a cursor-forward client draws nothing of its own — one pointer, not two.

Escape hatch

PUNKTFUNK_PORTAL_CURSOR_MODE=auto|hidden|embedded|metadata, for a backend that advertises a mode it implements badly (which negotiation cannot detect). A preference only: pins run the same ladder, so no value can re-create the refused request.

Verification

  • Module declared unconditionally so its ladder tests run on every CI leg, not just the one compiling mod hyprland.
  • Linux-only test pins our bit values against ashpd's enum; verified non-vacuous by planting a wrong discriminant (ashpd answers 4 for Metadata).
  • The regression test uses 3 — the bitfield measured on glass.
  • Linux: 225 tests pass, clippy --all-targets -D warnings clean, cargo fmt --check clean.

Not covered

The fixed host has not been run end-to-end against a Hyprland session — this verifies the root cause on glass and the ladder in tests, not the cure. Docs updated (hyprland.md, sway.md, configuration.md) with the verbatim error strings people will search for.

Field report 2026-08-14: black client on Hyprland, `pipeline build failed` in the host log, `unavailable cursor mode 4` from the portal. One failure, not three. ## What was wrong `hyprland.rs` and `wlroots.rs` both hardcoded portal `CursorMode::Metadata` whenever the session had negotiated the cursor channel, and never asked the backend what it supports: ```rust let cursor_mode = if hw_cursor { CursorMode::Metadata } else { CursorMode::Embedded }; ``` That is not a soft failure. **xdg-desktop-portal's frontend** validates the requested mode against the backend's `AvailableCursorModes` and fails the call with `"Unavailable cursor mode %x"` (`%x` → hex → the `4` in the report) before the backend ever sees it. So `select_sources` failed, `build_pipeline` gave up, and the client got a black screen. ## Measured on glass (.21) — it is worse than the report suggested Against a **live Hyprland 0.56.2** session with **xdg-desktop-portal-hyprland 1.4.1** and **xdg-desktop-portal 1.22.1** — all current — with xdph properly attached (`[screencopy] init successful`): ``` AvailableCursorModes = u 3 # hidden(1) | embedded(2) ``` Read identically on the backend impl interface and on the frontend. **xdph does not offer the metadata cursor at all**, so this broke *every* cursor-forward session on current Hyprland — not just old installs, and updating the portal would not have helped. Two things worth recording because they are easy to get wrong: - The rejecter is the **frontend**, not xdph. xdph 1.4.1 would itself have fallen back (`[screencopy] unsupported cursor_mode {}, fallback to {}`), but never gets the chance. - xdph PR #366 did **not** add metadata support — it fixed a cursor enum mishandled as a bool. "Update your portal" is not a fix. xdpw is the same story from the other end: its `screencast.c` refuses `METADATA` outright (`"dbus: unsupported cursor mode requested, cancelling"`), so every cursor-forward session on Sway/wlroots cancelled its cast too. ## The fix New `pf-vdisplay/src/vdisplay/linux/portal_cursor.rs`: a negotiation ladder against the advertised bitfield, called by both backends after proxy creation. `pf-capture` has always negotiated (`portal::choose_cursor_mode`), but pf-vdisplay may not depend on pf-capture — its Cargo.toml says "never on capture/inject or the orchestrator" — so the ladder is deliberately restated, with a comment on both copies to keep them in step. The downgrade is graceful rather than merely survivable: with the portal on `Embedded` no `SPA_META_Cursor` arrives, so the host feeds the cursor channel nothing and a cursor-forward client draws nothing of its own — one pointer, not two. ## Escape hatch `PUNKTFUNK_PORTAL_CURSOR_MODE=auto|hidden|embedded|metadata`, for a backend that advertises a mode it implements badly (which negotiation cannot detect). A preference only: pins run the same ladder, so no value can re-create the refused request. ## Verification - Module declared unconditionally so its ladder tests run on **every** CI leg, not just the one compiling `mod hyprland`. - Linux-only test pins our bit values against ashpd's enum; **verified non-vacuous** by planting a wrong discriminant (ashpd answers `4` for `Metadata`). - The regression test uses `3` — the bitfield measured on glass. - Linux: **225 tests pass**, `clippy --all-targets -D warnings` clean, `cargo fmt --check` clean. ## Not covered The fixed host has **not** been run end-to-end against a Hyprland session — this verifies the root cause on glass and the ladder in tests, not the cure. Docs updated (`hyprland.md`, `sway.md`, `configuration.md`) with the verbatim error strings people will search for.
enricobuehler added 1 commit 2026-08-14 08:28:06 +00:00
fix(vdisplay): the wlr-family backends asserted a cursor mode instead of negotiating it, so the portal refused the call
ci / bun-nix (pull_request) Successful in 24s
ci / docs-site (pull_request) Successful in 1m13s
ci / web (pull_request) Successful in 3m27s
android / android (pull_request) Successful in 4m2s
ci / rust-arm64 (pull_request) Successful in 4m40s
ci / rust (pull_request) Successful in 10m47s
13aa59c575
Hyprland and wlroots both hardcoded portal `CursorMode::Metadata` whenever the
session had negotiated the cursor channel, and never asked the backend what it
supports. That is not a soft failure: xdg-desktop-portal's FRONTEND validates the
requested mode against the backend's `AvailableCursorModes` and fails the call
with `"Unavailable cursor mode %x"` before the backend ever sees it.

So a cursor-forward session (desktop mouse mode) died at `select_sources`,
surfacing as "pipeline build failed" and a black client, with
`unavailable cursor mode 4` in the portal log. Field report 2026-08-14.

MEASURED on .21 the same day, and it is worse than the report suggested: against
a LIVE Hyprland 0.56.2 with xdg-desktop-portal-hyprland 1.4.1 and
xdg-desktop-portal 1.22.1 — all current — `AvailableCursorModes` reads **3**
(Hidden|Embedded) on both the backend impl interface and the frontend. xdph does
not offer the metadata cursor at all, so this broke EVERY cursor-forward session
on current Hyprland, not merely on old installs. Updating the portal would not
have helped. xdpw is the same from the other end: its screencast.c refuses
METADATA outright.

pf-capture's own portal path has always negotiated (`choose_cursor_mode`); this
restates that ladder in pf-vdisplay, which may not depend on pf-capture. The
downgrade is graceful rather than merely survivable: with the portal on Embedded
no `SPA_META_Cursor` arrives, so the host feeds the cursor channel nothing and a
cursor-forward client draws nothing of its own — one pointer, not two.

`PUNKTFUNK_PORTAL_CURSOR_MODE=auto|hidden|embedded|metadata` pins the preference
for a backend that advertises a mode it implements badly, which negotiation
cannot detect. It is a preference only: pins run the same ladder, so no value can
re-create the refused request.

The module is declared unconditionally so its ladder tests run on every CI leg
rather than only the one that compiles `mod hyprland` — including a Linux-only
test pinning our bit values against ashpd's enum, verified non-vacuous by
planting a wrong discriminant (ashpd answers 4 for Metadata, the number in the
report). The regression test uses 3, the bitfield measured on glass. Linux: 225
tests pass, clippy --all-targets -D warnings clean.
enricobuehler merged commit 8ca4c6eb0e into main 2026-08-14 08:39:20 +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#216