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:
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field report 2026-08-14: black client on Hyprland,
pipeline build failedin the host log,unavailable cursor mode 4from the portal. One failure, not three.What was wrong
hyprland.rsandwlroots.rsboth hardcoded portalCursorMode::Metadatawhenever 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
AvailableCursorModesand fails the call with"Unavailable cursor mode %x"(%x→ hex → the4in the report) before the backend ever sees it. Soselect_sourcesfailed,build_pipelinegave 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):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:
[screencopy] unsupported cursor_mode {}, fallback to {}), but never gets the chance.xdpw is the same story from the other end: its
screencast.crefusesMETADATAoutright ("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-capturehas 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
EmbeddednoSPA_META_Cursorarrives, 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
mod hyprland.4forMetadata).3— the bitfield measured on glass.clippy --all-targets -D warningsclean,cargo fmt --checkclean.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.