Wire-compatible with 0.30.x — everything you have already paired keeps working, and you can update one side at a time. Nothing in this release changes how a host and a client agree on what to send each other, so an old client on a new host, or the other way round, streams exactly as it does today.

Most of this release is things that were wrong in ways nothing announced. A DualSense's rumble and speaker never actually worked in a game streamed from a Linux host, for five separate reasons stacked on top of each other, and every one of them is fixed. On Windows, ending a session was silently killing the whole host, and launching a game from the library on 0.30 could drop your stream a second later. Android phones and TVs now show the same controller console the desktop does — one interface on three platforms — and the picture on a phone arrives markedly earlier with no dropped frames. On Linux hosts, another device on the box can no longer make your desktop audio stutter, and a Gaming Mode takeover on some machines was starving your controller with a login storm of our own making.

## TL;DR

- **DualSense rumble and speaker work in-game from a Linux host.** The controller was dying 400 ms after it appeared, every message the game sent it was reported as failed, its sound card was invisible to the sound system, and what got through arrived 48 dB too quiet. All fixed; the details are below.
- **Windows: ending a session no longer kills the host, and launching a game no longer drops your stream.** The first looked like a mystery reconnect; the second arrived with 0.30.
- **Android has the desktop's console, and a much faster picture.** One controller interface across Windows, Linux and Android — including 32-bit TV boxes — and on the reference phone end-to-end latency went from 30 ms to about 18 ms with dropped frames going from 40–50 a second to none.
- **Desktop audio on a Linux host no longer stutters because of somebody else's device.** In one 15-minute session, 15 % of what the listener heard was silence the host had papered over gaps with — because a controller's sound card, attached over the network and connected to nothing, was setting the pace for the whole box.
- **A Gaming Mode takeover on a machine that logs itself in was choking its own controller input** — the pad enumerated fine and then reported at about 1.4 times a second instead of 250. The cause was ours, and it is gone.
- **Hyprland and Sway users: the default display mode now really turns your desk monitors off during a session.** It said it did before and did nothing. Read *Before you update*.

## Before you update

- **Hyprland and Sway hosts:** the "exclusive" display setting — which is what the default resolves to on these desktops — now genuinely disables your own monitors for the length of a session and brings them back afterwards, exactly as it always has on KDE. Until now it was accepted, reported back as active, and quietly behaved as "extend", so your monitors stayed on. If you would rather keep them on, set the display topology to *extend*. Two things to know: on Hyprland, bringing the monitors back reloads your Hyprland configuration, which also drops any other runtime tweaks you have applied since login and re-runs its startup commands; and the Sway half is written to the same contract but has not been exercised on a live Sway machine, so if it misbehaves, please say so.
- **NixOS hosts:** there is now a binary cache, so a host no longer takes an hour to build. Add the cache and its public key to your configuration — the install guide has the snippet, and the cache serves its own public key. If your flake overrides Punktfunk's `nixpkgs` input, the cache cannot help you: every package changes and rebuilds. Separately, one of the controller-audio fixes below is a sound-configuration file the other Linux packages install into a system location NixOS does not have; it needs a package override there.
- **Other Linux hosts:** several controller-audio fixes arrive as system files inside the host package rather than as code — a device rule, a sound-system policy and a sound-card configuration. They take effect after the package is installed and the controller is reattached (or the machine rebooted).
- **Linux hosts, if you look in your sound settings mid-session:** you will now see a virtual output *and* a recording stream both named after Punktfunk. That is the host's own audio output, and it is not a leak — the troubleshooting guide has a new section on it and on what to do when it tells you another device is clocking your audio.
- **If you moved the host's management port and hand-edited the tray's start-up command to match, you can undo that.** The tray, every plugin and the plugin runner now follow the port the host actually bound, so nothing needs telling.

## New

- **NixOS users get prebuilt binaries.** Every other install channel shipped binaries; Nix compiled the whole workspace and our patched compositor from source — roughly an hour, on the critical path of enabling the host at all. A signed binary cache is now published on every change to the main branch, so a build that once took an hour takes the time it takes to download.
- **Hyprland and Sway hosts can run a session on the virtual display alone.** With the display topology on *exclusive*, your own monitors are switched off while the session runs and switched back on when its display goes away — never all at once, so the desktop is never left with nothing to show, and never a monitor belonging to a second session or a second host on the same machine. *Primary* is still treated as *extend* on these desktops, and now says so on its own rather than sharing a warning with *exclusive*: Wayland has no notion of a primary output, only a focused one, and the streamed display already holds that.
- **The Android app's controller interface is the desktop's.** Plug in or pair a controller and the phone or TV shows the same console Windows and Linux do — same screens, same navigation, same motion — instead of an Android-only recreation of it that had to be fixed three times over. It runs on every Android device, including the 32-bit TV boxes that were previously left out. On a phone the console now takes the whole panel, tucking the system bars away for the duration and bringing them back on a swipe, and the library gives the sort bar's height back to the covers unless you have actually pulled it down. Connected controllers is a page of the console itself now, with the permission prompts, the rumble test and the controller-audio self-test where they were before.
- **The Apple gamepad library grows up.** On Mac, iPhone, iPad and Apple TV the controller-driven library gets the desktop console's poster grid alongside the shelf, a live sort and view bar (Default · A–Z · Platform · Store; Shelf · Grid) that writes the same setting the settings rows do so the two can never disagree, and **Collections** — group by platform or store, walk the groups, open one, and back out the same way — presented as tiles carrying a fanned deck of up to three covers. The plain touch library learned to sort and group from a toolbar menu too. Every title's platform, which the host had been sending all along and the app was throwing away, now shows in the detail band and drives the grouping.
- **The audio buffer can grow without going silent.** Every client keeps a small buffer of sound and adjusts its depth to what the link and the picture are doing. It could shrink gently, one crossfaded frame at a time — but the only way it could *grow* was to throw everything away and refill from silence. So whenever the picture's timing wandered and the sync loop asked for a little more audio depth, the very next late packet cost a 15–60 ms gap. This shape has been with us since roughly 0.24. The buffer now moves toward its target in both directions with the same gentle instrument, on Windows, Linux, Android, Mac, iPhone, iPad and Apple TV alike.
- **The audio threads that feed the speakers run at real priority on the client.** The device callback itself already did; the threads decoding audio and rendering controller audio did not, and on a Steam Deck decoding a 1440p120 stream on the same four cores that was a source of clicks. The previous attempt at this was a no-op on the Deck (its user account is not allowed to ask), so the client now goes through whichever door the machine actually offers — including from inside a Flatpak. Windows clients get the same through the system's pro-audio scheduling.
- **A host that is losing audio now says why.** The Linux host logs, by name, whichever device is setting the pace for its audio whenever that changes, and warns when it is not the host's own output — because those stalls become your holes. The capture summary also breaks its lost audio down by how long each gap was, so a periodic scheduler on the box and one long outage stop reading identically.
- **Plugins stop re-scanning your library while a game runs.** Steam writes to its folders the whole time a game is open, and every write was re-walking the library — 102 times in one 27-minute log. There is now a floor of one re-scan per half-minute per plugin, with nothing lost: whatever changed during the hold is picked up in exactly one trailing scan.

## Improved

- **Windows: the plugin runner writes a log file you can read.** A field report on a 0.30 host had plugins installed, the runner running, an empty library and "no logs at all" — and that was by design, since the runner's only way to speak was through the host it could not reach. It now writes a plain log file next to its plugin state, and the console's empty-library hint tells you where it is.
- **Apple gamepad screens move like the desktop's.** Screen transitions in the Mac, iPhone, iPad and Apple TV controller shell use the same spring the desktop console uses, and they can be interrupted — press B mid-flight and the same spring carries you back. Reduce Motion crossfades instead of snapping.
- **The Apple library fits a phone.** The grid fills the width instead of leaving a fifth of it empty on a phone; in a landscape phone's height it holds two rows instead of one; the shoulder-button hint hides on any phone and the sort bar has become a tray you pull down with ▲ and dismiss with ▼, A or B, so the field keeps every point of height it has. Navigating the grid no longer scrolls twice for one move, and a diagonal flick of the stick is one move, not two.
- **Mac: "Capture system shortcuts" can now capture the system's shortcuts.** 0.29 taught the Mac client to send ⌘ shortcuts to the host, but only the ones macOS lets an app see — ⌘Q, ⌘W and their like. ⌘Space, ⌘Tab and Mission Control never got that far: macOS hands them to Spotlight and the Dock before any app is asked, so a captured stream still opened Spotlight. With Accessibility access granted, those reach the host too while your input is captured; ⌘⎋ still releases capture and stays local. The grant is optional and only ever asked for from the setting itself, never at stream start — without it, the setting keeps doing what it did in 0.29 and its caption now says exactly that.
- **The on-screen keyboard on Mac and iPad behaves.** The row you are typing into flies from its place in the list to a seat directly above the keys and back, instead of the empty row doing the flying while the real one appeared from nowhere; a hardware keyboard types straight into the field while the tray is up, including characters the on-screen grid does not offer; and Esc means Done rather than closing the whole screen.
- **The controller speaker sounds like a speaker.** Both controller-audio lanes were compressed with the low-delay voice profile that suits rumble — on the speaker it sounded, in the words of the person who heard it, insanely compressed. The speaker lane now uses the full-quality music coder at a higher bitrate; the rumble lane is unchanged.
- **A silent controller speaker is no longer indistinguishable from broken hardware.** On Android the controller speaker is off by default — deliberately, it is a small loudspeaker in your hands — but nothing said so, and one field session spent an evening measuring the host for a speaker that was simply switched off on the phone. The setting now states its default, and the host's controller-audio test tells you up front whether your client would even be asked to play what the test is about to prove works.
- **The virtual controller looks exactly like a real one to the system.** It carried a placeholder serial number no real pad has, which leaked into every device name derived from it. Cleared, so anything that matches on those names sees the same text a physical pad produces.

## Fixed

- **Ending a session killed the entire host on Windows.** The service restarted it about six seconds later, so it read as a mystery reconnect rather than a crash — four times on one machine, every one of them a session teardown. The cause was a single log line written at a moment when the logging machinery could already be gone, and the crash handler then hid the evidence by trying to log the same way and failing the same way. Neither happens now, and if anything of that shape ever recurs, the message naming it will actually be written down.
- **Launching a game from a Windows host on 0.30 could drop your stream a second later**, with the console reporting nothing running. Almost every Windows launch is a hand-off to Steam, Playnite or the shell — a process that quits a second after the launcher takes over. 0.30 learned to track the process it started, and for a title without any way to recognise its window it took that hand-off's exit as the game's and closed the connection. It would also, in one shape, have asked your whole Steam client to close when the game ended. Both fixed; a title the host cannot track shows as untracked instead of "launching" forever.
- **DualSense rumble and speaker in a game streamed from a Linux host — five faults, in order:**
  - **The controller vanished 400 ms after it appeared** when the newer USB-style transport was on. One of its setup reports was one byte longer than the driver asks for, and USB treats an over-long reply as hostile and tears the whole device down. The report is the right length now, every reply is trimmed to what was asked for, and the host waits for the system to actually adopt the pad before it calls the pad ready.
  - **Every message the game sent to the pad was reported as failed**, so the one command that switches on rumble, adaptive triggers and the speaker never took, and nothing downstream could show a result. The pad was acknowledging every write with "0 bytes accepted". It acknowledges the bytes it took now — and the Steam Controller 2 shares that path, so its writes were being reported wrong too.
  - **The pad's sound card was only readable by the administrator**, because it appears at a moment when no desktop session is active to grant access, so the sound system never saw it and games had nowhere to route controller audio. A device rule fixes it, for virtual and physically plugged pads alike.
  - **What reached the voice coils was 48 dB too quiet.** The sound system starts every new device at what a mixer shows as 40 %, which in real amplitude is about −24 dB — and both the host and the client minted one, so the two stacked. Both ends now pin the controller's volume to unity.
  - **The controller's only playback route was a mono output that at least one game overran and crashed on**, about 74 seconds in. The pad now presents its proper multi-channel profile from the moment it appears, so that route — and the crash path — never exists, and the coils get their own channels into the bargain.
  - Also on this path: the speaker channel was being forwarded to the wrong side, so you felt rumble but heard nothing; the pad's audio clock ran about 26 % slow under load, backing its own audio up into dropouts and dragging desktop capture down to half delivery with it; and the sound-system policy that keeps a controller from setting the pace for the whole box was set to a value that made it merely last in line rather than excluded, which on a Punktfunk host is still elected. All corrected.
- **Desktop audio on a Linux host stuttered because another device was setting its pace.** The host's audio output was a stream pretending to be an output, and a stream never keeps its own time — so the sound system handed the job to the highest-priority running device on the box. On one reporter's host that was a DualSense forwarded over the network: connected to nothing, never sleeping, and unable to keep steady time for something arriving over a link. Every cycle that ran was healthy; the loss was in the gaps between them — 3.9 holes a second, the worst 142 ms, and 15.4 % of a 15-minute session filled with silence the host generated to cover them. The host now creates a real virtual output that keeps its own time inside the sound system, so no hardware or network device can ever be chosen for it. It also turns out the host's audio callback has been running at 2.67 ms rather than the 5 ms it was designed around on every stock Linux host since it was written, because of a rounding rule nobody had accounted for; that is corrected on the same path.
- **A Gaming Mode takeover on a machine that logs itself in flooded the box with logins.** On a host set to log its user in automatically, the way the takeover held the desktop's login manager back sat squarely in that manager's retry path — so every automatic login failed in milliseconds and it tried again, with no back-off: 962 logins in under four minutes, the system's buttons re-scanned 5,688 times, a load of 26 on 12 cores, and every program that listens for new devices drowning in the noise. To you it looked like "my DualSense is not detected, or only with an insane delay": the pad enumerated perfectly and then delivered input at about 1.4 Hz. That method of holding back is gone; the takeover now stops the login manager first and only then steps in. If it cannot stop it, it no longer tries anyway — it falls back to streaming the desktop's own session, which is a working stream, rather than fighting a login loop that costs you your controller.
- **Audio hiccups on the client that nothing counted.** On the Linux desktop client and the Steam Deck the audio callback was running on the wrong thread at ordinary priority, and when that thread was late the sound system played silence for us and moved on — an underrun no counter ever saw. On the host, the audio pacer measured its schedule against the wall clock while the audio it carried did not, so every small hole left it a little further behind until a bigger hole repaid the debt as a burst of silence frames — one field log showed 33–72 % of departures late and the worst nearly 100 ms behind. Both fixed, and holes no longer open and close with a click.
- **A Steam Deck's support bundle covered three seconds instead of the whole session.** The video decoder writes a dozen bookkeeping lines per frame, and at 120 fps that flushed the entire log ring — 2,037,456 lines evicted from a 27-minute session, including the one audio line three rounds of investigation had been waiting for. The chatter is filtered before it reaches the ring.
- **Android: the picture arrived late and dropped frames on a phone that could easily keep up.** The reference phone decodes a frame in 4–5 ms and was still showing 30 ms end to end with 40–50 skipped frames a second, because it was pacing against a display clock that Android quietly slows down for game processes. Frames are now composited straight onto the display on the panel's real timing, and each one reports back exactly when it landed. On glass: end-to-end 30 ms → about 18 ms, skipped frames 40–50 a second → 0. Whether the panel *holds* 120 Hz turns out to be the phone maker's power policy, not the app's — nothing an app can ask lifts it — so if you want 120 on such a phone, set your phone's minimum refresh rate.
- **Android: the controller went dead after opening the Controllers or Licences page from the console** — every press was dropped until you force-stopped the app. Also, plugging in a controller could leave you on a grey screen for the rest of the session if the console could not draw; it now hands you back to the touch interface instead.
- **Mac, iPad and Apple TV: clicking a host connects to it again.** A change earlier in this cycle had made the host card open the game library instead, with "connect" pushed into the menu — the opposite of every other client. Reverted: tap to connect, "Browse Library…" back in the menu, everything else the library work landed kept.
- **The Apple grid's first day on real hardware.** A single stick flick was read as up-then-right on the way out of the dead zone; the grid drew over the pinned title; rows vanished while still in view; one step down scrolled the row above half away; and "Copy link" was a face button on a gamepad interface. All addressed — X now opens a small options menu for the title.
- **RPM builds were broken by one stray line**, so for a short window the main branch produced no Fedora packages at all. Fixed the same day; no release was affected.

## Thanks

Several of these were found because someone sent a log detailed enough to disprove the obvious. The Steam Deck bundle that turned out to hold three seconds of decoder chatter is what made the log-ring bug visible; the audio-clock investigation went through four field logs and a purpose-built probe before a single column in a diagnostic tool named a controller's sound card; and the report of a Windows host with plugins installed, a runner running and nothing to show for it described exactly the failure the runner could not report on its own. Thank you.

## For developers

Protocol, ABI, driver and embedder detail — including the version table and the notes on what moved — is in [CHANGELOG.md](https://git.unom.io/unom/punktfunk/src/tag/v0.31.0/CHANGELOG.md).

The short version: nothing versioned moves — no wire, ABI, driver-protocol or plugin-contract change — and the C header is byte-identical to 0.30.0's. Three things are worth reading before you package or embed this release: the Linux host package now installs three new system files (a device rule, a sound-system policy and a sound-card configuration) that the controller-audio fixes depend on; the Linux desktop-audio capture changed topology by default, with a one-release escape hatch back to the 0.30 shape; and the Android app's Compose console is deleted outright, which removes its screenshot scenes.
