175 commits since v0.29.0 (131 non-merge). Cut from origin/maina17a8917. Re-cut fromd92266a4(129/99) as main moved through PRs #270-#282. THE NUMBER: 0.30.0 is forced, not chosen. The C ABI moved 20 -> 24 in four steps this cycle — 21 connect_ex10 (the device_name an unpaired client knocks with, #244), 22 the per-client access accessors (#245), 23 connection_audio_plc (Apple's drought concealment), 24 connect_ex11 plus the resolved-format accessors (the lossless plane, #263) — and gamescope moved +pfhdr7 -> +pfhdr8 (patch 0010, the seat keymap the keyboard-layout fix depends on). Two DEFAULTS also flipped, either of which alone rules out a patch: the host now serves the lossless audio plane unless refused (#280, marked breaking by its author), and trust::Settings::library_enabled is deleted along with the "Show game library" row (#273, likewise). scripts/ci/pf-version.sh derives the canary base as latest stable + one minor, so canaries move 0.30.x -> 0.31.x after the tag. Every C addition is additive and WIRE_VERSION stays 2: the new Hello and Welcome fields ride the trailing-field append discipline, and the one new control message (PipelineGap, type byte 0x0A) took a free byte in the block it belongs to. No existing C function changed signature or behaviour, and no repr(C) struct grew a field — the lossless format is read through accessors precisely because PunktfunkAudioPcm and PunktfunkStats have no struct_size guard and are allocated by value by every C embedder. Version table re-measured ona17a8917, not carried forward from the first cut: ABI 24, wire 2, driver protocol 6 / min 3 (pf-driver-proto has no diff against the v0.29.0 tag), gamepad channel 3, plugin index schema 1, edition 2024, MSRV 1.85, 27 crate dirs, SDK 0.1.4, plugin-kit 0.4.2, gamescope 8, api/openapi.json stamped 0.29.0. ⚠ RE-SYNC OWED AND DONE: docs-site/public/openapi.json had drifted from api/openapi.json. #276's game-lifetime work regenerated the api/ copy alone (adding GameOnNewLaunch, game_on_new_launch and the `untracked` game state) and nothing catches it — the mgmt drift test compares the LIVE ROUTE TABLE against the generated document, not the two files — so CI stayed green while the published API reference described 0.29. Re-synced byte-identical here and called out in the CHANGELOG. Three claims were corrected against the code rather than shipped as drafted, all three in the first cut's favour of being too narrow or too broad: * lossless rungs are NOT uniform across clients. Apple and Android list the full ladder (441/48/882/96/1764); pf_client_core lists only 48/96, and pf-client-core still filters a surround request out before the wire, so the desktop and console clients stay on Opus under 5.1. The notes say exactly that instead of "stereo only". * "send logs to host" ships on the Gaming Mode console alone — the Apple, Android and webOS legs are follow-ups. * the masked-color I-beam fix arrived via a fork branch but is authored by the repo owner, so it is NOT credited as an outside contribution. Gates run on this tree: cargo fmt --all --check clean; cargo metadata --locked ok; Cargo.lock diff versions-only (36/36 lines) against the new main; cargo test -p punktfunk-core green including the c_abi harness (261 + 1 + 8, the ABI-24 header compiles and round-trips; macOS needs LIBRARY_PATH=/opt/homebrew/opt/opus/lib); the android.yml Play notes gate run verbatim — 475/500 characters and not byte- identical to any prior release's; notes voice scan finds no internal names outside the For developers section; both openapi copies diff clean.
25 KiB
Wire-compatible with 0.29.x — everything you have already paired keeps working, and you can update one side at a time. An old client on a new host, or the other way round, streams exactly as it does today.
Two big things arrive in this release. Your stream can now carry audio that is not compressed at all — and unlike when it first appeared, there is nothing to switch on at the host end; picking a lossless option in your client is the whole opt-in. And you can hand someone else access to your machine without handing them the machine: a guest can get a controller and nothing else, for four hours, and it ends by itself. Alongside them, every client learned to play frames out on the host's own timing instead of the moment they happen to arrive, and AV1 streaming — which has quietly never once worked on an AMD host — now does.
TL;DR
- Lossless audio, and the host no longer has to be talked into it. Pick a lossless option in your client's audio settings and you get uncompressed sound, up to 176.4 kHz and 24-bit, stereo through 7.1. The host serves it unless you tell it not to, and only when the session can actually pay for it.
- AV1 never decoded on an AMD host — every session silently fell back to H.265. It failed after about 287 frames, on the very first thing it tried to read, and nothing above the log said so. Fixed.
- Give someone limited, temporary access. Approve a device as a guest — controller only, four hours — instead of all-or-nothing forever. It warns them before it runs out and ends the session cleanly when it does.
- The picture is smoother, and variable-refresh displays finally earn their keep. Frames are played out on the host's cadence rather than the instant they arrive.
- Quitting a game no longer leaves the session up forever, and your library now remembers where you were, shows what is already running, and wakes a sleeping host while you browse it.
- A German — or any non-US — keyboard types the right characters now. Nothing was carrying your host's keyboard layout into the streamed session.
- If you edited your Windows host's configuration file by hand, read Before you update.
Before you update
- Windows hosts, if you hand-edited the host configuration file: the service now loads only Punktfunk's own settings and the logging switch out of it, and ignores everything else — an unrelated variable you added there will no longer reach the service. Separately, a configuration file or console-password file that was created by a normal user account rather than an administrator is now distrusted: the configuration file is set aside and replaced with the default, and the password is rotated to a fresh random one. Both changes close a way for someone without administrator rights to plant settings that the service would then run as the system account. If either file is yours and was written from a normal prompt, re-create it from an elevated one.
- The "Show game library" setting is gone, and the library is simply there. Nothing to do — but if you had it switched off, you now have a library. It never worked as a switch anyway: in Gaming Mode the button handed you the library whatever the setting said, while on the Linux and Windows desktop apps it genuinely hid the menu entry and defaulted to off, so most people had a library they had never seen. On Mac, iPhone, iPad and Android the equivalent toggle is untouched for now.
New
- Lossless audio, with nothing to turn on at the host. Until now every stream's sound was compressed, at one fixed sample rate, because the compressor Punktfunk uses only speaks that rate. There is now a second path that sends your audio untouched: 44.1 to 176.4 kHz, 16- or 24-bit, stereo through 7.1. Pick it in your client's audio settings — that choice is the opt-in, and it is the switch belonging to the person whose bandwidth it spends. When it first landed the host also needed an operator to enable it, which produced exactly one outcome: people picked "Lossless 96 kHz / 24-bit", silently got the compressed path, and the only explanation was a single line in a log on the other machine. So the host now allows it by default, with a setting to refuse it outright if you want.
- What still has to agree, and it is mechanical rather than a matter of consent: your capture device has to genuinely deliver the rate, and the cost has to fit within a quarter of that session's video bitrate. It runs outside the automatic bitrate control — taken off the top of the link, where that control can neither see it nor claw it back — so a 5 Mbps session cannot afford any of it. In stereo it costs 1.4–8.5 Mbps against the compressed path's 256 kbps, and up to 33.9 Mbps for 176.4 kHz 7.1.
- Be clear about what it buys. On game content it is very unlikely to be audible — the compressed path is already effectively transparent, and nothing above 24 kHz is hearable at all. The real win is bit-exactness: no lossy stage anywhere, and no resampling for a host whose interface genuinely runs at 96 kHz.
- ⚠ Two honest limits. The Mac, iPhone, iPad, Apple TV and Android menus list every rate; the Linux, Windows and Gaming Mode menus currently list 48 and 96 kHz, and on those clients choosing surround keeps you on the compressed path. And on a Linux host, if your sound server is configured to only ever run at the standard rate it will quietly resample a higher one instead of switching to it — which sounds like it worked and is not. The host cannot detect that, so add the rates you want to its allowed list.
- Access levels: let someone in without letting them in all the way. Approving a device used to be all-or-nothing and forever. Now the approval dialog offers what they may do and for how long, with three ready-made choices — full control, controller only, or watch-only — plus individual switches for the controller, mouse, keyboard, clipboard, microphone and the ability to launch games. There is a one-click "Approve as guest": a controller, four hours. The countdown is visible in their client, they are warned five minutes and one minute before it runs out, and when it does the session ends with a message that says so instead of looking like a crash. Their access is a column you can edit at any time, and a change takes effect in a session that is already running. Re-pairing can no longer be used to upgrade an access level that was cut back.
- Three things it honestly does not do, and they are documented rather than glossed: a limited guest still sees your whole desktop — this is permission, not isolation; a Moonlight-compatible client that has never been given a record is unrestricted; and an older client is genuinely restricted by the host but shows none of the on-screen countdown, so it will simply be disconnected.
- The library became a place you come back to. Opening a host's library now wakes it and keeps retrying while it boots, so it is warm by the time you pick something — waking used to start only once you connected, which is far too late to help. The catalog is remembered per host and drawn immediately, marked as possibly out of date, so a sleeping machine still shows you its titles instead of an error. Anything already running is badged Resume and sorted to the front. And the grid puts you back where you were — remembered as the last title you opened rather than a scroll position, so it survives a rotation, a resize, or the host gaining games. On Mac, iPad and Apple TV, tapping a paired host now opens its library, with "Stream the Desktop" moved into the card's menu.
- Close the last game when you launch a different one. A new host setting (off by default) ends the game this client had running before starting another. It will never touch another client's game, one you started at the machine yourself, or anything it is not certain about.
- Read your clients' logs in the web console, on the same timeline as the host's. Bundles sent up from a client used to sit in a table below the log viewer, where they could be downloaded and not read — which is a dead end on a phone, or on anything that is not the host. They are now part of the log view itself, with each device a chip you can switch on beside Host and Plugins. "The client stalled at 12:03:47 — what was the host doing?" is a question this page can finally answer.
- The web console tells you what is wrong with your host. The host has always known about things like a missing group membership or a driver that never got installed — and said so in a single log line nobody reads. A Troubleshooting page now lists those checks with what each one affects and what to do about it, and the dashboard shows the worst of them if there are any (and renders nothing at all on a healthy host). It knows the difference between two states that look identical and need opposite answers: being added to a group but not having logged out since, and not being in it at all. On the immutable Fedora-based systems it offers that system's own command rather than the one that silently does not stick there.
- Send your client's logs to the host. On a Steam Deck in Gaming Mode, an Apple TV or a TV app there is no realistic way to get a log off the device, so every fault report has arrived with only the host's half of the story. "Send logs to host" now sits in the Gaming Mode console's per-host menu. The Apple, Android and TV clients get the same action in a following release; the host side that receives it is already here.
- Shared clipboard on iPhone and iPad. It was written for the Mac and gated off everywhere else, so the iOS half of the same app had a switch it could not honour. Copy on your host and paste on your phone, and the other way round. It stays deliberately lazy — nothing is transferred until someone actually pastes — with one exception: an offer you have not pasted yet is pulled across when you leave the app, so switching to another app to paste actually works.
- Reach your game library from Android's touch interface, and copy a link to any game. The library was only reachable with a controller button, so a finger had no route to it at all. It now has the same poster grid the other clients draw. And every client's library hands out a game's own link, which boots straight into that title when opened.
- The Gaming Mode console got a proper visual pass, then a second one after it met a Steam Deck. Screens arrive with a staggered card entrance rather than popping into existence; the transition is an interruptible spring, so holding B backs out of a deep stack in one continuous motion instead of stuttering at every level; the library has a grid arrangement and can be grouped into collections by platform, which you can now start in; view and sort are switchable in the library itself rather than buried in settings; one context menu now serves the whole console; host tiles show which operating system they run, and controller battery; and a Reduce motion setting turns all of it off (and doubles as an OLED-friendly mode).
- Experimental: the virtual DualSense can present itself as a real USB controller on Linux hosts, so a game can finally pair "my controller" with "my controller's speaker". Games look for that pairing in a way our virtual pad could not satisfy, which is why some titles found both halves and then refused to open either. It is off by default and unproven on real hardware — it changes how the whole pad appears to the system — so it is there to be tried, not relied on.
Improved
- The picture is smoother, because frames are played out on the host's own cadence. Every client used to present a frame the instant it finished decoding, so any unevenness in delivery landed on your screen one-for-one. There is now a clock that works out the relationship between the host's timing and your display's, and plays each frame out when it is actually due, with a cushion sized to the unevenness it measures. It smooths the delivery, never the content — a game genuinely running at an irregular rate still looks exactly as irregular as it is. On a display with measured variable refresh the frame is presented at its due time with no snapping to the panel's grid, so variable refresh is now a real benefit rather than merely not a penalty.
- The host half matters as much: it was stamping each frame with the moment it reached Punktfunk, not the moment your desktop drew it — so on a host that delivers raggedly, that raggedness was baked into the timestamps. It now uses the desktop's own stamp where one is available and sane. One consequence worth knowing: the latency figures in the stats overlay now read higher, because they are counting a delay that was previously invisible. Nothing got slower; the number got truer.
- A brief hiccup on the host no longer costs you minutes of quality. On Windows, certain display changes make the host rebuild its capture and encoder in place — a few hundred milliseconds, entirely local, not a single packet lost. Your client measured that window, saw almost nothing arrive, and concluded the network was congested: one field session dropped its quality and then crawled back for the three minutes it had left. The host now tells the client when it stopped the stream itself, so that window is discarded instead of believed. Two related repairs: a window that carried almost nothing can no longer report the encoder as slow, and the automatic bitrate no longer climbs to a number the picture cannot use — on a gigabit network one session walked to 657 Mbps for a 1440p120 stream in 37 seconds and took the client's decoding time from under a millisecond to ten.
- A silent gap in the sound no longer turns into a much longer artifact. When audio simply stops arriving — a Wi-Fi link that bunches up, or a host whose capture stalls — the playback buffer used to drain empty and then rebuild from scratch, so a short gap became a much longer disturbance. The client now synthesizes the missing audio from what it last heard, for a bounded time, and reports how much it did so. On the host side, a hole in capture used to block everything behind it; it is now covered with silence on the normal schedule for up to half a second, so a 30 ms hole costs 30 ms.
- Your stream no longer gets stuck at the lowest quality on a game that runs below your display's refresh rate. A 120 Hz session running a game at 60 fps had every frame judged against the 120 Hz deadline, which most of them missed by definition — so the host decided the encoder could not keep up and refused every quality increase. One measured session sat at the lowest quality for 23 minutes — 94% of its length — on a connection with nothing whatsoever wrong with it. A frame's budget is now the arrival of the next real frame.
- Streaming from a host whose desktop delivers unevenly is steadier. A timing lock in the capture path re-engaged after a single good measurement, so a borderline host flapped in and out of it 41 times in 24 minutes — each flip a visible timing step. It now needs sustained evidence to engage, backs off further after each failure, and gives up for the session after repeated attempts.
- The host's audio counters answer the question they invite. A 16-second outage and a slowly starved stream used to report the same numbers. Pauses, gaps and their durations are now counted separately, and the send side reports itself for the first time — so "the host is pacing audio badly" is finally a claim that can be checked rather than a permanent suspicion.
- The Steam plugin finds far more cover art. Steam renamed the files it stores art under and changed where it puts them, and the scan knew only the old names and the old layout. Measured against a real 779-game library: portraits found locally went from 25 to 328, headers from 86 to 716. This showed up as newer games in particular showing a wide banner where a poster should be. ⚠ You need to update the Steam plugin itself to pick this up.
- The Apple client's settings stopped lecturing you. Every row carried its whole design rationale underneath it — the audio quality row could render ninety-five words under a single picker, and the Apple TV version of the same setting was a 102-word paragraph read from a sofa. Captions are now one clause of what the setting does and one of what it costs, with the numbers kept. Two-thirds of the words are gone and none of the actionable ones. One outright bug went with them: an Apple TV caption pointed at a context menu, and Apple TV has no context menus.
Fixed
- AV1 has never once decoded on an AMD host, and quietly pretended otherwise. Every AV1 session died after about 287 frames and fell back to H.265, so what you got was a working stream that was not the codec you chose — and the only trace was a parse error in the client's log on the very first thing it read. The cause is a bit reader in the video parser that refused to read a 32-bit value, on a comment that was true of a neighbouring function and wrong here. AMD's encoder includes a timing field that needs those 32 bits; NVIDIA's does not, which is exactly why this survived: everything that ever tested AV1 tested it on NVIDIA. Fixed, with the fault reproduced and pinned by test.
- A German keyboard typed US characters. Reported from an iPad streaming to a Linux host in Game Mode:
#arrived as\,-as/,äas'. Punktfunk sends the physical key you pressed and lets the host's session decide which character that is — which is right, and means the host's session has to actually know your layout. Nothing was telling it: the standard way of setting a keyboard layout on Linux writes a file that only one display server reads, and nothing else in the chain picks it up. The host now finds the layout your machine actually has recorded and hands it to the session, on every route including a session it merely attaches to. - Quitting a game left the session up and the console calling it "running" forever. No setting made any difference, and that was the tell: for a game the host had no way to recognise, it was asserting the game was running because it had just launched it — so "being watched", "quit and never noticed" and "cannot be seen at all" were the same state. Such a game now says so plainly instead. Windows hit this far more easily for a specific reason: it threw away the process id it was handed at launch, so a title that published no other identifying hint had nothing at all attached to it — which is also why clicking a game that was already running resumed it on Linux and started a second copy on Windows.
- DualSense haptics and the controller speaker were being thrown away on Linux. The haptic voice coils are two of the four channels of the controller's sound device, and a modern Linux audio setup splits that device up in a way that quietly folds those two channels into the speaker pair — so the haptics arrived and were discarded, with nothing erroring and nothing looking wrong. Both the host's virtual pad and the client's sending side now present and target what a real DualSense actually looks like. Separately: the pad's built-in speaker shares a channel with its headphone jack and powers up pointing at the jack, so with nothing plugged in it was silent even once the routing was right — the client now enables the speaker itself instead of waiting for a game to do it, and which value does that has since been confirmed on real hardware by a wide margin. There is also a self-test that plays a tone into the coils with no host and no game involved.
- Bazzite hosts could freeze mid-stream because of our virtual controller. Our virtual DualSense trips a Valve system service into scanning every process on the machine; the security layer denies that scan several hundred times a second, and the diagnostic daemon that reacts to those denials amplifies it into a machine-wide process storm that burns a core and keeps growing for fifteen minutes after the denials stop. The stream drops to nothing and the session dies. The trigger is ours, so the fix ships with us: the packages now install a rule that silences the denial, and the host warns in its own log when the conditions for the storm are present — because in the system log the storm looks like it is coming from us.
- Steam could not see a Steam Controller 2 attached through the host. The controller was captured, forwarded and enumerated correctly, and Steam's controller list was simply empty. Its device node was left readable only by the administrator because its identifiers were never added to our device rules — and for this controller that is fatal rather than degrading, since Steam is the only thing that can talk to it at all. On Android, the same controller paired over Bluetooth was invisible for a different reason: the app declared the permission it needed and never once asked for it, so the check answered "denied" forever in every shipped build.
- The picture went grey with moving artifacts for a second or two after starting a game. When a stream loses data it freezes rather than showing damage, and waits for a signal that it is safe to move again. One of those signals is the host asserting that the picture it just coded against is one your client still holds intact — but the host works that out from what your client received, not from what it managed to decode, and those differ precisely when the client had to paper over a loss. So it lifted the freeze onto a picture built from damage and left it lifted. The client now tracks which pictures are genuinely undamaged and refuses that claim when it can prove otherwise, which can only ever make it hold longer and ask for a clean picture sooner.
- The Windows text cursor disappeared. A text I-beam is drawn by inverting whatever is behind it rather than by being a shape with transparency, and it was being read as the latter — so it came across as fully transparent and the client drew nothing at all over every text field.
- Nothing in the Gaming Mode console was anti-aliased, which a Steam Deck's screen showed up immediately: smooth one-pixel outlines drawn on top of hard-stepped fills, a padlock with a stair-stepped body and a clean shackle, a blocky online dot inside a soft glow. Cover art was worse — every poster was being shrunk by simply discarding rows and columns, so box-art lettering broke up and edges crawled as the shelf scrolled, which reads as "low resolution" whatever the panel is. Both were the drawing library's defaults rather than anything chosen. Also from that first session on real hardware: collection covers that never loaded, a grid missing its last row, a sort bar that landed wherever there was room, headings that did not line up with every other client's, and a settings row whose value change repainted every other row.
- Every Apple device waiting for approval was called "This device". The name a client knocks with came from a system variable that does not exist in a Mac or iPad app, so a console with three of them pending showed three identical rows with no way to tell which was which.
- A reconnect could resurrect the login screen and start a login loop. On a host set to hand the TV back when you disconnect, a reconnect arriving while that hand-back was still in flight could race it — one field trace shows 1272 relogins in about four minutes, each starting Steam afresh, until the machine was stopped by hand.
- Opening a library shortcut while already streaming that same host popped an error on Mac and iPad instead of just bringing the app to the front. Android had it right from the start.
- The controller's audio device carried a name nothing could match. On Linux, the compatibility layer games use to reach a DualSense's speaker and haptics matches on specific text in the device's name, and ours had been named against an older reading of it — so neither half of the feature could attach to anything.
Thanks
Several fixes in this release exist because someone measured carefully rather than just reporting that something felt wrong: the smoothness work came out of a 24-minute log detailed enough to prove the network was blameless, which is the only way the real cause was ever going to be found; the keyboard report named exactly which characters came out wrong, and that set of characters is what identified the fault; the report behind the library and game-lifetime work described the whole loop — browse, play, quit, browse — rather than just the broken step; and the cover-art fix was measured against a real 779-game library rather than the one title that prompted it. Thank you — that detail is what makes a fault findable.
For developers
Protocol, ABI, driver and embedder detail — including the version table and the breaking-changes list — is in CHANGELOG.md.
The short version: the embedding interface moves forward four steps, all of them pure additions — nothing was removed, reshaped or given a new meaning, and no by-value structure grew a field. The streaming protocol itself does not move; one new control message took a free type byte. Four things are worth reading before you package or embed this release: the Windows configuration-file changes above, the two defaults that flipped (the host now serves lossless audio unless refused, and the client library setting is gone along with the field behind it), the fact that the compositor patches must be rebuilt for the keyboard-layout fix to take effect, and that the last free bit in one of the two capability bytes is now spent.