The console UI died on any Vulkan loader newer than the version we asked for #204

Merged
enricobuehler merged 1 commits from worktree-deck-skia-browse-fix into main 2026-08-13 20:26:26 +00:00
Owner

Field report, 2026-08-13: "I updated my Deck to 0.28.0 and now I can't open the client anymore via the Decky plugin or the shortcut in game mode."

Neither the plugin nor the shortcut is at fault — both launch PF_BROWSE=1 (clients/decky/src/steam.ts:435), and it is the client that dies about a second after start. Steam's own log is where the truth is; the Decky log shows nothing wrong:

punktfunkrun: gamepad UI io.unom.Punktfunk --browse (console home)
… Gamescope WSI Created swapchain … Destroying swapchain …
console: console UI init (required for --browse): Skia DirectContext over the shared device
Game Recording - game stopped

That second-to-last line is an anyhow chain (run.rs's context plus the anyhow! in skia_overlay.rs), not prose: make_vulkan returned None.

Root cause

1fb081a1 chore(deps): move pf-console-ui from skia-safe 0.87 to 0.99 (the currency wave, #193, tagged into v0.28.0) replaced BackendContext::new with new_builder(…, None) and justified the None in a comment as "byte-for-byte what the (now removed) BackendContext::new did".

That is true of the value and false of the behaviour. None leaves Skia's fMaxAPIVersion at its 0 sentinel, and the newer Skia acts on that sentinel by falling back to vkEnumerateInstanceVersion() — the loader's ceiling, not ours:

  • pf-presenter/src/vk/setup.rs creates the instance with API_VERSION_1_3.
  • SteamOS 3.7's loader answers 1.4.321 — on the host and inside the flatpak sandbox (libvulkan.so.1.4.321).
  • ⇒ Skia validates a 1.4 function table against a 1.3 instance, vkGetDeviceProcAddr returns null for the entry points in between, validation fails, make_vulkanNone.

At 0.87 the same sentinel was inert, because that Skia knew nothing of Vulkan 1.4. That is why this surfaced the moment 0.28.0 landed rather than being long-standing.

Blast radius, wider than the report

  • run.rs makes an overlay that cannot init fatal for --browse — so the console home is simply dead.
  • In a stream the same failure only warns (console-UI overlay init failed — continuing without it), so those sessions quietly lost their stats OSD and capture HUD. "My OSD vanished in 0.28.0" is this same bug.
  • pf-presenter's vk module is cfg(any(linux, windows)), so this was never Deck-specific: any client on a loader newer than 1.3 loses the console home, Windows included.

The fix

The presenter publishes the version an overlay may size its function table to as SharedDevice::api_version, and SkiaOverlay::init passes it instead of None.

It is min(what we declared, what the loader reports), and both halves are load-bearing in opposite directions:

  • taking the loader's number alone is this bug;
  • taking ours alone would break the mirror case — a 1.1+ loader accepts our 1.3 apiVersion as intent even when it cannot deliver 1.3, so pinning 1.3 outright would newly break an old-loader box that works today.

Three unit tests pin both directions and the no-answer (1.0 loader) case. The three API_VERSION_1_3 spellings in setup.rs now read the one constant, so the number the overlay is told can no longer drift from the number we asked for.

Measured, not inferred

A standalone repro (ash 0.38 + skia-safe 0.99) built in distrobox pf2 and run on the Deck itself (RADV VANGOGH, loader 1.4.321) — no client build needed to see it:

vkEnumerateInstanceVersion() -> 1.4.321 ; our VkApplicationInfo.apiVersion -> 1.3.0
max_api_version = None      => DirectContext NULL   <-- FAILS
max_api_version = Some(1.3) => DirectContext OK

Field before/after on the same hardware: the Deck's log says Skia console UI on the presenter's device repeatedly on 2026-08-11 (skia 0.87), and never again after today's update.

Verification

Gate Result
cargo fmt --all --check clean
cargo build -p pf-console-ui (x86_64 Linux container) RC=0
cargo build -p pf-presenter RC=0
cargo test -p pf-presenter 46 passed, incl. the 3 new tests
cargo clippy --all-targets -p pf-console-ui -- -D warnings RC=0
cargo clippy --all-targets -p pf-presenter -- -D warnings RC=0

cargo check -p pf-console-ui on macOS is vacuous — every mod in that crate is cfg(any(target_os = "linux", windows)), so it compiles nothing and passes with anything. The pf-lxcheck2 container is the only place this code compiles at all; both crates genuinely recompiled there (Compiling pf-console-ui) and the three new tests are listed by name in the run.

Not yet confirmed on glass end-to-end — the Deck went to sleep mid-session. The remaining check is the obvious one: once this merges and the flatpak canary publishes, open the console home from the Decky panel.

Field report, 2026-08-13: *"I updated my Deck to 0.28.0 and now I can't open the client anymore via the Decky plugin or the shortcut in game mode."* Neither the plugin nor the shortcut is at fault — both launch `PF_BROWSE=1` (`clients/decky/src/steam.ts:435`), and it is the **client** that dies about a second after start. Steam's own log is where the truth is; the Decky log shows nothing wrong: ``` punktfunkrun: gamepad UI io.unom.Punktfunk --browse (console home) … Gamescope WSI Created swapchain … Destroying swapchain … console: console UI init (required for --browse): Skia DirectContext over the shared device Game Recording - game stopped ``` That second-to-last line is an anyhow chain (`run.rs`'s context plus the `anyhow!` in `skia_overlay.rs`), not prose: `make_vulkan` returned `None`. ## Root cause `1fb081a1 chore(deps): move pf-console-ui from skia-safe 0.87 to 0.99` (the currency wave, #193, tagged into v0.28.0) replaced `BackendContext::new` with `new_builder(…, None)` and justified the `None` in a comment as *"byte-for-byte what the (now removed) `BackendContext::new` did"*. That is true of the **value** and false of the **behaviour**. `None` leaves Skia's `fMaxAPIVersion` at its `0` sentinel, and the newer Skia acts on that sentinel by falling back to `vkEnumerateInstanceVersion()` — the **loader's** ceiling, not ours: * `pf-presenter/src/vk/setup.rs` creates the instance with `API_VERSION_1_3`. * SteamOS 3.7's loader answers **1.4.321** — on the host *and* inside the flatpak sandbox (`libvulkan.so.1.4.321`). * ⇒ Skia validates a **1.4** function table against a **1.3** instance, `vkGetDeviceProcAddr` returns null for the entry points in between, validation fails, `make_vulkan` → `None`. At 0.87 the same sentinel was inert, because that Skia knew nothing of Vulkan 1.4. That is why this surfaced the moment 0.28.0 landed rather than being long-standing. ## Blast radius, wider than the report * `run.rs` makes an overlay that cannot init **fatal for `--browse`** — so the console home is simply dead. * In a **stream** the same failure only warns (`console-UI overlay init failed — continuing without it`), so those sessions quietly lost their **stats OSD and capture HUD**. "My OSD vanished in 0.28.0" is this same bug. * `pf-presenter`'s `vk` module is `cfg(any(linux, windows))`, so this was never Deck-specific: **any client on a loader newer than 1.3 loses the console home, Windows included.** ## The fix The presenter publishes the version an overlay may size its function table to as `SharedDevice::api_version`, and `SkiaOverlay::init` passes it instead of `None`. It is `min(what we declared, what the loader reports)`, and both halves are load-bearing in opposite directions: * taking the **loader's** number alone is this bug; * taking **ours** alone would break the mirror case — a 1.1+ loader accepts our 1.3 `apiVersion` as *intent* even when it cannot deliver 1.3, so pinning 1.3 outright would newly break an old-loader box that works today. Three unit tests pin both directions and the no-answer (1.0 loader) case. The three `API_VERSION_1_3` spellings in `setup.rs` now read the one constant, so the number the overlay is told can no longer drift from the number we asked for. ## Measured, not inferred A standalone repro (ash 0.38 + skia-safe 0.99) built in distrobox `pf2` and **run on the Deck itself** (RADV VANGOGH, loader 1.4.321) — no client build needed to see it: ``` vkEnumerateInstanceVersion() -> 1.4.321 ; our VkApplicationInfo.apiVersion -> 1.3.0 max_api_version = None => DirectContext NULL <-- FAILS max_api_version = Some(1.3) => DirectContext OK ``` Field before/after on the same hardware: the Deck's log says `Skia console UI on the presenter's device` repeatedly on 2026-08-11 (skia 0.87), and never again after today's update. ## Verification | Gate | Result | |---|---| | `cargo fmt --all --check` | clean | | `cargo build -p pf-console-ui` (x86_64 Linux container) | RC=0 | | `cargo build -p pf-presenter` | RC=0 | | `cargo test -p pf-presenter` | **46 passed**, incl. the 3 new tests | | `cargo clippy --all-targets -p pf-console-ui -- -D warnings` | RC=0 | | `cargo clippy --all-targets -p pf-presenter -- -D warnings` | RC=0 | ⚠ `cargo check -p pf-console-ui` on macOS is **vacuous** — every `mod` in that crate is `cfg(any(target_os = "linux", windows))`, so it compiles nothing and passes with anything. The `pf-lxcheck2` container is the only place this code compiles at all; both crates genuinely recompiled there (`Compiling pf-console-ui`) and the three new tests are listed by name in the run. ⏳ Not yet confirmed on glass end-to-end — the Deck went to sleep mid-session. The remaining check is the obvious one: once this merges and the flatpak canary publishes, open the console home from the Decky panel.
enricobuehler added 1 commit 2026-08-13 18:53:05 +00:00
fix(console-ui): Skia sized its function table to the loader, not to what we promised
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 1m17s
ci / web (pull_request) Successful in 3m33s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 5m15s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m58s
ci / rust (pull_request) Successful in 16m40s
android / android (pull_request) Successful in 5m18s
fbbfce9b0e
The skia-safe 0.87 -> 0.99 move swapped `BackendContext::new` for
`new_builder(..., None)` and recorded the `None` as "byte-for-byte what the
(now removed) `BackendContext::new` did". That is true of the VALUE and false
of the BEHAVIOUR. `None` leaves Skia's `fMaxAPIVersion` at its `0` sentinel,
and the newer Skia acts on that sentinel by falling back to
`vkEnumerateInstanceVersion()` -- the LOADER's ceiling, not ours. The presenter
declares 1.3; a current Mesa answers 1.4 (1.4.321 on SteamOS 3.7, host and
inside the flatpak sandbox alike). Skia then validates a 1.4 function table
against an instance that only ever promised 1.3, `vkGetDeviceProcAddr` returns
null for the entry points in between, validation fails, and `make_vulkan` hands
back `None`. At 0.87 the same sentinel was inert, because that Skia knew nothing
of Vulkan 1.4 -- which is why this surfaced the moment 0.28.0 landed.

`run.rs` makes an overlay that cannot init fatal for `--browse`, so on the Steam
Deck the console home died on update: the Decky panel's button and the
gamepad-UI library shortcut both launch `PF_BROWSE=1`, and neither would open.
In a stream the same failure only warns, so those sessions quietly lost their
stats OSD and capture HUD instead. `pf-presenter`'s `vk` module is
`cfg(any(linux, windows))`, so this was never Deck-specific.

The presenter now publishes the version an overlay may size itself to as
`SharedDevice::api_version`, and `SkiaOverlay::init` passes it instead of `None`.
It is `min(what we declared, what the loader reports)`: taking the loader's
number alone is this bug, and taking ours alone would break the mirror case,
where a 1.1+ loader accepts our 1.3 `apiVersion` as intent even when it cannot
deliver 1.3. Three unit tests pin both directions and the no-answer case. The
three `API_VERSION_1_3` spellings in setup.rs now read the one constant, so the
number the overlay is told can no longer drift from the number we asked for.

Measured on the Deck (RADV VANGOGH, loader 1.4.321) with a standalone repro
against the shipped crate -- the client build is not needed to see it:

  vkEnumerateInstanceVersion() -> 1.4.321 ; VkApplicationInfo -> 1.3.0
  max_api_version = None      => DirectContext NULL
  max_api_version = Some(1.3) => DirectContext OK

Verified: cargo fmt --all --check; and in the pf-lxcheck2 x86_64 container,
cargo build + cargo clippy --all-targets -- -D warnings for pf-console-ui and
pf-presenter, plus cargo test -p pf-presenter (46 passed). Note that
`cargo check -p pf-console-ui` on macOS is vacuous -- every mod in that crate is
cfg(linux|windows), so it compiles nothing there.
enricobuehler merged commit fdf48fcaa1 into main 2026-08-13 20:26:26 +00:00
enricobuehler deleted branch worktree-deck-skia-browse-fix 2026-08-13 20:26:28 +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#204