docs(release): the 0.25.0 notes cover the 37 commits since the bump
ci / docs-site (pull_request) Successful in 1m13s
ci / web (pull_request) Successful in 1m15s
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m14s
android / android (pull_request) Successful in 4m34s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 6m45s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m27s
ci / rust (pull_request) Successful in 14m5s

main moved from 8983ec04 to 35ba64ca while this branch sat open, taking the
release from 98 commits to 135. Merged in and folded the new work into the notes.

The largest addition is a new `## Before you update` section, because this batch
carries changes that need the reader to DO something and they were not going to
survive being buried in a Fixed bullet:

  * Linux users of the virtual Steam Deck pad must `usermod -aG punktfunk` and
    log back in, or it stops attaching — the capability moved off the `input`
    group (which every gamepad guide tells you to join) onto its own, because it
    can emulate arbitrary USB hardware.
  * Plugin UIs moved to their own origin on PORT+1, so a self-signed console
    needs the new port trusted once, and custom firewalls/proxies need it opened.
  * Saving a custom launch command re-confirms the console password, and add-ons
    may no longer set launch/pre-launch commands at all — a real break for any
    third-party add-on that populated them.
  * A fresh install now runs the plugin runner by default (upgrades untouched).
  * The Deck setup script used to leave the generated console password
    world-readable, so rotating it is worth a sentence.

The library-sources work is written as GROUNDWORK, deliberately. All six built-in
scanners still ship, still on by default, and nothing is removed — and none of
the replacement add-ons are published yet, so the migration banner only appears
as they arrive. Promising a user they can move Steam to an add-on today would be
the v0.22.3 mistake again: notes describing a build nobody is getting.

Two other honesty items. The Android HUD entry says outright that the stream did
not get faster and the headline number only got smaller because it stopped
counting the compositor's wait — otherwise every reader takes it for a speed-up.
The Windows non-C: settings entry says plainly that nothing is recoverable,
because the writes never reached disk, so there is no orphaned copy to restore
and the reader has to re-enter their preferences once.

`56adb470` (pad-audio WASAPI module path) is deliberately NOT a user-facing Fixed
entry: verified it is not an ancestor of v0.24.0, so it repairs a Windows build
break in code that has never shipped. It folds into the pad-audio feature. Same
for `19f637ea`, which is CI-only.

Under the hood gained the origin-isolation mechanism, the allowlist authorization
gate that fails the build on an unclassified route, store claims and the v2
library.json shape, the registry auth work, the config-writer fallback, send
pacing, and the vendored Deck WSI layer. The unverified list grew too: the origin
split has not been in a real browser, the packaging default-on changes have had
no installer run, and no launcher tile has ever been clicked.

Re-verified after the merge, all green: lock diff versions-only 32/32 against
origin/main, `cargo metadata --locked` resolves (35 members), `cargo fmt
--all --check` clean in both workspaces, doc lazy-continuation scanner 0 hits
over 521 files, notes body 0 internal-vocabulary hits above `## Under the hood`,
Play notes still 494/500 by android.yml's own gate logic. Wire 2, C ABI 16, and
the capability bytes are all unchanged from the bump commit — host_caps still has
exactly one free bit (0x80).

Play's "What's new" is left as it stands: at 494/500 there is no room, and the
only Android-facing additions here (the stats-overlay measurement change and a
certificate-strictness fix) are both worth less to a phone user than any line
already in it.
This commit is contained in:
2026-08-06 00:45:25 +02:00
parent 745864423b
commit 1535d69852
+29 -2
View File
@@ -2,11 +2,24 @@ Wire-compatible with 0.24.x — everything you have already paired keeps working
The headline is that a **DualSense plugged in by USB can now play a game's fine-grained haptics — the textured detail in the grips, not just the rumble motors — and its own speaker, streamed from the host**. That needs a Windows host with Steam installed and either the Android app or the desktop session client; everywhere else, nothing changes.
Behind it, three fronts. **Controllers** were swept end to end: rumble that faded on a Steam Deck, died for good after one hiccup on a phone, or kept buzzing after you quit; adaptive triggers and lightbars left stuck in a game's last state on your desk after the stream ended; player-number lights that never lit on anything but a DualSense — more than twenty separate faults, across every client and both hosts. **Sound** got the same treatment: desktop audio is encoded at roughly double the bitrate, hosts stopped routing the entire game mix through Steam's voice channel on PCs that had it installed, and audio that drifts behind the picture now pulls itself back instead of staying late for the rest of the session. And the **black screen** that some people hit on VPN-shaped networks — a session that connects, reports every gauge healthy, and then shows nothing at all, forever — is finally diagnosed, explained in the log, and healed on its own. Alongside those: the Steam Deck plugin is rebuilt as a launcher into the app, holding Select on any controller presses the host's Guide button, and saved settings profiles can be pinned to hosts without a mouse.
Behind it, three fronts. **Controllers** were swept end to end: rumble that faded on a Steam Deck, died for good after one hiccup on a phone, or kept buzzing after you quit; adaptive triggers and lightbars left stuck in a game's last state on your desk after the stream ended; player-number lights that never lit on anything but a DualSense — more than twenty separate faults, across every client and both hosts. **Sound** got the same treatment: desktop audio is encoded at roughly double the bitrate, hosts stopped routing the entire game mix through Steam's voice channel on PCs that had it installed, and audio that drifts behind the picture now pulls itself back instead of staying late for the rest of the session. And the **black screen** that some people hit on VPN-shaped networks — a session that connects, reports every gauge healthy, and then shows nothing at all, forever — is finally diagnosed, explained in the log, and healed on its own. Alongside those: a security review closed 37 of 38 findings — a few of which need a moment of your attention, so the list is right below this; HDR finally works on a Steam Deck out of the box; the Windows client stops forgetting your settings when it isn't installed on C:; the library settings become one **Game sources** list with a Launchers row above your games; the Steam Deck plugin is rebuilt as a launcher into the app; holding Select on any controller presses the host's Guide button; and saved settings profiles can be pinned to hosts without a mouse.
## Before you update
A security review closed 37 of 38 findings this release, and a few of them change behaviour in ways you need to know about. Most people need to do nothing — but check this list if any of it applies to you.
- **Linux, if you use the virtual Steam Deck controller: you must join a new group.** The permission that lets the host create that emulated pad used to ride along with the `input` group, which every gamepad guide tells you to join — but it can emulate arbitrary USB hardware, so it now has a group of its own. Run `sudo usermod -aG punktfunk "$USER"` and log back in, or the virtual Deck pad will stop attaching after this update. Ordinary virtual gamepads are unaffected, and you should only join this group on a machine you trust.
- **Add-on interfaces now load on their own port.** An add-on's own interface used to run on the web console's address, which meant it could act with your logged-in permissions; it now runs on a separate port next to the console's (47993 by default). If you reach your console over a self-signed certificate, your browser needs to trust the new port once — the console shows a card explaining this with a link that opens it in a new tab. If you use a custom firewall or a reverse proxy, open or forward that second port. Fresh installs open it automatically on Windows, and the Linux firewall profiles include it. If the port cannot be opened at all, add-on interfaces switch off and say so rather than quietly moving back.
- **Saving a game with a custom launch command asks for your console password again.** A custom command runs on your machine as you, so it now re-confirms. Ordinary edits — title, artwork, platform, a normal Steam launch — are untouched.
- **Add-ons can no longer set launch or pre-launch commands themselves.** Those two fields run through a shell and are yours alone now; an add-on that tries is refused. If you use a third-party add-on that filled them in, it will need updating by its author, who should use the new "opens a launcher" entry type instead.
- **A new install now runs the add-on runner by default.** Game sources depend on it, and a host without it would show an empty library. Upgrades are untouched — if you deliberately switched it off, it stays off. You can still disable it and keep streaming; you only lose add-on game sources and automation.
- **If you installed on a Steam Deck with the setup script, consider rotating your console password.** It was written to a world-readable file. That is fixed, and the Fedora/RHEL hint that told you to read the password out of the system log — where anyone able to read logs could see it — is gone too.
## New
- **Your DualSense's own haptics, carried from the host.** Games that drive the DualSense's fine-grained voice coils — the detailed, textured feedback in the grips, as distinct from the coarse rumble motors — now carry that across the stream to the controller in your hands, and the pad's built-in speaker can be carried with it. It needs all of: a DualSense or DualSense Edge **plugged into your device by USB** (over Bluetooth the pad exposes no audio device to play into, so there is nothing this can do), a **Windows host with Steam installed** (the per-controller audio device is built on Valve's Remote Play streaming-speakers driver), and either the Android app or the desktop session client. Anywhere else — a Linux host, the iPhone/iPad/Mac app, the ordinary Windows or Linux desktop app, a Bluetooth pad — nothing changes at all. A game that uses only ordinary rumble keeps rumbling exactly as it does today. On Android the controls are **Controller haptics** and **Controller speaker** under Controllers, alongside a **Test haptics** button that checks your phone can drive the pad at all without needing a stream running. Expect a new playback device named "DualSense Wireless Controller" to appear in the host's Windows sound settings — that is this feature, it is how games find the controller's speaker, and it will not take over as your default output.
- **One "Game sources" list, and a Launchers row above your games.** The library settings used to show two separate boxes — a list of toggles for the built-in launcher scanners, and a second card for anything an add-on had synced in. They are now one list, where every source (Steam, Lutris, Heroic, Epic, GOG, Xbox, or one from an add-on) is a row with the same controls: on/off, how many games it contributes, a "show only these" filter, its own settings, and a remove option that offers to take its games with it. Add-on sources show a Running or Stopped badge so a dead one is obvious at a glance. Separately, entries can now be marked as opening a launcher rather than a game — Steam Big Picture, Steam desktop, Heroic, Lutris — and those group into a Launchers row above the game grid instead of getting lost among your titles. You can tick that yourself when adding or editing an entry. Update the host and console.
- **Game sources are becoming add-ons, and nothing about that is forced.** Each launcher is being turned into its own add-on, so you install only the ones you use and each gets its own settings. **All six built-in scanners still ship in the host, still switched on, and still find your games with nothing installed** — no one has to install anything to keep what they have. As replacement add-ons become available the console offers to move a source over, one button per source, never all at once and never automatically; a source keeps the same identity either way, so switching does not renumber your games, lose their artwork, or break pinned shortcuts. Half-moved is a perfectly valid state. In 0.25.0 this is groundwork: the replacement add-ons are not published yet, so expect the offer to appear as they arrive rather than on update day.
- **Hold Select to press the host's Guide button.** Hold Select (Back / View) on its own for about a third of a second and the host sees its Guide button go down — and it stays down while you hold, so a longer hold reads as a long-press on the host, which is how a big-screen host opens its Quick Access Menu. A quick tap of Select still goes to the game, and Select as part of a combo — including the leave chord — passes through untouched. It is on by default on iPhone, iPad and Apple TV, where the system keeps the controller's own Home press for itself and this is the only reliable route to the host's overlay. Everywhere else the raw press already reaches the host, so the gesture stays off by default and Select keeps its exact timing. Update the client.
- **Get onto a host by asking, instead of typing a PIN.** From the Steam Deck panel, tapping a locked host now offers **Request access**: the stream opens and waits while whoever is at the host approves your Deck in its console, then the picture comes up by itself. It gives up after about three minutes like any failed connection. Offered only for hosts visible on your network — one you saved by typing an address has no advertised identity to check against, so those still use a PIN, and the sheet says why. Update the plugin; hosts already knew how to approve.
- **Pin a settings profile to a host from a controller.** Every controller-driven settings screen — the Deck and Linux console home, the Apple app's gamepad UI including Apple TV, and the Android app's controller UI including Android TV — gains a **Profiles** section showing each profile and where it is pinned ("Not pinned", "Pinned to 2 hosts"). Open one and press A on a host to pin or unpin. On Apple TV this is the only profile management there has ever been; on Android, pinning previously needed a touchscreen. Creating and editing profiles is still a desktop or phone job. Update the client.
@@ -25,6 +38,7 @@ Behind it, three fronts. **Controllers** were swept end to end: rumble that fade
- **Hosts you reach over a VPN show as online on the Steam Deck.** The panel's list merges what it finds on the network with the hosts you have saved and probes the saved ones directly, so a box that never advertises itself — over Tailscale, or on another subnet — reads as up instead of unreachable. Rows sort online first, then most recently streamed.
- **Waking a sleeping host from the Deck waits for it properly.** The panel used to send the wake-up and then guess how long to wait before dialling. It now waits for the host to actually answer.
- **Two new troubleshooting sections on audio.** One explains what the host actually captures and why streamed sound can be worse than what you hear on the host itself — naming the Steam Streaming Microphone trap explicitly and showing the log line that identifies it. The other covers audio that lags the picture, why it should now correct itself, and what to check when it does not.
- **The Android stats overlay stops charging your screen's own delay to the stream.** Its headline latency used to include the time Android itself takes to put a finished frame on the panel — a floor no streaming app can undercut, and easily over 30 ms on a 60 Hz phone. That now sits on its own line instead of inside the headline, matching how the iPhone, iPad and Apple TV clients have always reported it. **Your stream is exactly as fast as it was** — the headline number gets smaller because it finally measures only the part Punktfunk controls, which also makes it comparable across devices. The floor is measured on your device rather than assumed, and the full unshaved figures are still in the client's log. Update the client.
## Fixed
@@ -47,6 +61,12 @@ Behind it, three fronts. **Controllers** were swept end to end: rumble that fade
- **A controller driver that failed to attach no longer stalls the stream while the host works out why.** The diagnosis ran a slow system lookup on the very thread feeding controller input and rumble — up to two seconds per affected pad, at exactly the moment a session was already going wrong. It now runs in the background, and because it is off the critical path it can afford to wait long enough to report what it actually found. Update the host.
- **The Steam Deck keeps its trackpad mouse when a stream starts.** Starting a stream killed the built-in trackpad-as-mouse system-wide, and it only returned seconds later when the controller's own firmware watchdog restored it. Update the client.
- **Controller settings you cannot use no longer look live.** With "Forward controllers" off, the rows beneath it have nothing to act on, but on the Windows app and both controller-driven settings screens they stayed fully interactive — so you could sit there changing settings that did nothing. They are now dimmed until forwarding is back on. On Apple devices, starting a stream with forwarding off also stopped claiming every button's system gesture (which took away your screenshot and Home presses) and stopped powering up the controller's motion sensors for a stream that was not forwarding anything. Update the client.
- **HDR works on a Steam Deck straight out of the box.** Streaming an HDR game to a Deck gave a washed-out, tone-mapped picture with the overlay reporting a fall back to SDR. Punktfunk now ships everything it needs to talk to the Deck's Game Mode display pipeline, so a plain install is all it takes — there is no longer a separate piece to install by hand. Honest about what came before: that manual step was documented only in a comment inside the packaging, and even people who found it still got SDR, because the layer loaded and looked healthy while silently never engaging. One thing is still yours to do: HDR has to be switched on in Steam's own display settings, or nothing on the Deck gets it. Update the Flatpak client.
- **The Windows client saves your settings when it isn't installed on the C: drive.** On a PC set to install new apps to a second drive, the client streamed perfectly and then quietly forgot everything on restart — settings, connection profiles and your saved hosts all came back empty each launch, while the app showed the toggle you had just moved as though it had stuck. Every save was failing silently. Saving now works on those installs, and if the folder genuinely can't be written the client says so in a banner naming it rather than pretending. An update you declined also used to be offered again forever on these installs, for the same reason; that is fixed too. **You will need to set your preferences once more — nothing can be recovered, because it never reached the disk — and this time they will stay.** Update the client.
- **Recovering from a brief freeze no longer makes it worse.** When the host stalled for a moment — some AMD systems do this when a display drops to standby — the very large catch-up frame was pushed out in one burst that overflowed the network buffer and was lost, costing another round trip and another freeze. That frame is now sent at a pace the connection has already proven it can carry. Update the host.
- **A momentary stall no longer pins your stream at a low bitrate for minutes afterwards.** A window in which almost nothing arrived looked, to the quality logic, exactly like your device's decoder giving up — so it recorded a ceiling that was never real and then spent minutes climbing back toward it. Nearly-empty windows are no longer treated as evidence about your decoder. The stream still backs off for genuine trouble; it just stops drawing that conclusion from an interruption. Update the client.
- **The Steam Deck panel shows host names instead of addresses.** A host you saved by typing its address in was listed as that address, printed twice — once as the title and once underneath. Saved hosts that are online now show the name the machine actually advertises, and a name you chose yourself still wins and is never overwritten. Update the plugin.
- **"Recreate shortcuts" on the Deck actually recreates them.** After a plugin reinstall the Punktfunk entry could vanish from your Steam library and never return: the plugin always believed the old entry still existed, so recreating it reported success while doing nothing, and "Open Punktfunk" answered with "Game configuration unavailable". A stale entry is now detected and rebuilt on the next launch. The plugin also lists itself in Decky as "Punktfunk", capitalised properly. Update the plugin.
- **A leftover folder from an uninstalled Sunshine or Apollo is no longer treated as a conflict.** Both uninstallers leave a settings folder behind, and Punktfunk counted any trace at all — a leftover folder, a file on disk, a registered but switched-off background service — as a live clash. Affected machines warned on every start and showed a red card in the web console reading that another streaming server was running, when nothing was. Only a server that is genuinely running, or set to start on its own, counts now; the console names exactly what it saw, and leftovers appear in the full report under a heading saying they need no action. Update the host.
- **A crashed host gives you your screen back.** In Exclusive display mode the host switches your own monitors off for the length of a session and back on when it ends. If the host crashed or was killed mid-session that never happened — the desk simply stayed dark, no timeout brought it back, and the way out was Windows' own display shortcut or a reboot. The host now records which screens it is about to switch off before switching them off, and forces every connected display back on the next time it starts. Recovery happens at that next start, not on a timer: if the host stays down, the screen stays dark until it runs again. Update the host.
- **Camera look survives pressing Escape on an iPad.** Pressing Escape mid-stream made iPadOS hand the pointer back to the system, and Punktfunk never took it back. Clicks kept landing exactly where you aimed, so input looked fine — but the game stopped receiving mouse movement, so camera look was dead for the rest of the session. Clicking back into the video now takes the pointer again, and if the system refuses the first time, the next click tries again. Update the client.
@@ -78,4 +98,11 @@ The Decky plugin has been rebuilt as a **launcher**. It no longer contains a sec
- **Mid-session shard renegotiation is gated off for PyroWave sessions**, which parse the video stream in windows fixed at session start — re-sizing mid-stream would corrupt the parse. Those sessions get the next-session clamp only, and are excluded from jumbo. The decode-cap latch fix likewise does not apply to PyroWave, where adaptive bitrate is open-loop by design.
- **The Deck plugin's Python backend is now four thin shells over the `punktfunk` CLI** (`discover`, `hosts list --probe --json`, `pair`, `hosts add`); it parses no client data files and re-implements no client rules, and an outdated client reports itself deterministically as exit 5 + `unknown command "<verb>"` rather than being inferred from GTK startup noise. Host identity is matched by fingerprint first and address second in exactly one place, so a host that changed DHCP lease still matches its record while a different box inheriting the address does not inherit its pairing. `KnownHosts::read()` was split out of `load()` so `discover` can annotate against the store without minting-and-saving ids, which two parallel invocations could otherwise race.
- **The hold-Select gesture is one state machine** with unit tests in the shared client core, re-implemented to the same rules in the Apple capture layer and Android's router. A tapped Select is delivered on release with its release scheduled 50 ms behind, because a back-to-back down+up can otherwise fold into a single sequenced snapshot and vanish. `punktfunk-session` gained a per-user Unix control socket (`$XDG_RUNTIME_DIR[/app/$FLATPAK_ID]/punktfunk-session-ctl.sock`) with two verbs, `guide` and `qam` — the one runtime path a flatpak and the outside-the-sandbox Decky backend see identically.
- **Verification is build-level.** Clippy and test gates on Linux, the Windows runner and macOS; the desktop-audio, packet-sizing and iPad pointer work has not been confirmed on glass in these commits. **Controller audio in particular has never run on a real DualSense** — it is a hardware feature whose entire verification to date is unit tests and compile checks, and its rumble arbitration rests on an explicitly retracted assumption about whether the voice coils and the rumble motors are the same actuators (the evidence-based 500 ms idle window is correct either way, but the underlying exclusivity is unsettled). Android's arbiter is the evidence-based one; the desktop twin and the coil restore on Android's stop path are both still owed. Some Android OEM kernels also refuse the isochronous claim outright, which degrades to ordinary rumble and is reported by the self test.
- **Origin isolation for plugin UIs.** A second listener (default `PORT + 1`, `PUNKTFUNK_UI_PLUGIN_PORT`) serves `/plugin-ui/**` and nothing else; the console origin refuses those paths and the plugin origin refuses everything else, `/api/**` above all. Different origin (scheme+host+port) so same-origin policy *is* the boundary, same site so the `SameSite=Lax` session cookie still flows. Bind failure disables plugin UIs rather than falling back. `x-pf-listener` is stripped inbound and set by the entry, active ports are republished as `*_PORT_ACTIVE`, the plugin origin's CSP names the console as its only `frame-ancestors`, and the proxy allowlist drops the plugin's `Clear-Site-Data`, `Access-Control-Allow-Origin` and `Set-Cookie`. ⚠ The kit's `postMessage(..., "*")` is now load-bearing — narrowing it to `location.origin` would target the plugin's own origin and drop every message.
- **Authorization is an allowlist with a build-time gate.** `plugin_may_access` is a list of permitted `(method, path)` pairs with `{}` segment matching, enforced by a test that walks the live route table and **fails the build on any unclassified route** — the "block these" list it replaces let new endpoints through silently. Field authority is tracked separately from route reachability: requests carry the lane that authorized them, and `prep` / `launch.kind = "command"` are operator-token only. Art serving gained an extension whitelist plus magic-byte sniffing, canonicalize-or-refuse, UNC refusal, config-dir exclusion and root checking, with `file://` percent-decoded *before* canonicalization so `%2e%2e` cannot hide; roots default to the Windows users base and, new on POSIX, `$HOME` (`PUNKTFUNK_LIBRARY_ART_ROOTS`). Validation also runs at write time, so an unservable path can no longer be persisted.
- **Store claims keep identity across the scanner-to-plugin handover.** `library.json` gains a v2 `{entries, claims}` shape that reads the old bare array unchanged and rewrites on first mutation. `PUT /library/provider/{p}?store=<s>` claims a store; entries then surface as `<store>:<external_id>` rather than `custom:<id>`, so entry ids, GameStream app ids, client art caches and Moonlight pins all survive. One provider per store (409 otherwise); while a claim is held the matching built-in scanner is skipped, so the two never double-list. `GET/PUT /library/scanners` is now a sources endpoint over the same disabled-set file. New entry fields: `role: game|launcher`, and launch kinds `steam_ui` (`bigpicture|desktop`) and `launcher_ui` (platform-gated, 400 on invalid). Plugin kit 0.3.0 adds a `./library` subpath — `defineLibraryPlugin`, ported total parsers (text VDF/ACF, the binary `shortcuts.vdf` walker with CRC-32 appid derivation, read-only immutable SQLite, a registry wrapper that refuses HKCU, path-confinement joins), and `GET/PUT /__config` so a plugin with settings need not ship an SPA.
- **Build-container images now push to an authenticated registry endpoint**, and `:latest` is reconciled against the content key on every push to main — an out-of-band tag move is detected and repaired rather than silently inherited. This also fixes a long-standing bug where reverting a CI change left `:latest` pointing at the newer build forever.
- **The client's config writer** falls back to an in-place write when the atomic replace is unavailable, verifies it by reading the bytes back, and records the last persistence failure centrally so the UI can surface it. Scratch files are now per-process, closing a real collision between the five processes that write these stores (shell, session, console UI, CLI, Decky) — one could previously rename its half-written temp over another's target. The update-check bookkeeping was hand-rolling the same dance and now goes through that one writer.
- **Host send pacing** gained a pure, unit-tested budget function: oversized frames are budgeted at the pacing rate with a 100 ms absolute ceiling instead of being compressed into a single frame interval. Steady-state schedules are byte-identical, the legacy behaviour stays reachable via an environment escape hatch, and the GameStream-compatible path is untouched. The ABR decode-cap latch now ignores windows that delivered under a quarter of target without erasing a reference a genuine choke had set.
- **The Deck's Vulkan compatibility layer is built from source**, pinned to the same upstream revision as the host's own packaged build, so client and host come from one tree — bump both together. It is ~4 MB of app content in place of a 94 MB external extension users had to fetch themselves, and Flathub is no longer needed at install time. The old search-path override was deliberately dropped so two same-named layers cannot both load.
- **Verification is build-level.** Clippy and test gates on Linux, the Windows runner and macOS; the desktop-audio, packet-sizing and iPad pointer work has not been confirmed on glass in these commits. **Controller audio in particular has never run on a real DualSense** — it is a hardware feature whose entire verification to date is unit tests and compile checks, and its rumble arbitration rests on an explicitly retracted assumption about whether the voice coils and the rumble motors are the same actuators (the evidence-based 500 ms idle window is correct either way, but the underlying exclusivity is unsettled). Android's arbiter is the evidence-based one; the desktop twin and the coil restore on Android's stop path are both still owed. Some Android OEM kernels also refuse the isochronous claim outright, which degrades to ordinary rumble and is reported by the self test. Three more things in this release are reasoned-and-tested rather than observed: the plugin-UI origin split is validated against a fake console and a fake plugin, not yet in a real browser; the packaging default-on changes have had no installer run or package build; and no launcher tile has been clicked on a real host, the first source that would publish one not existing yet.