worktree-pyrowave-wire-dead-code
2834
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cbfa03b7ad |
docs(host/pads): the SC2's bInterval is already 1 kHz — don't "fix" it to 250 Hz
Working G14/G18 turned up two sweep findings that do not survive contact with
the code. Neither is implemented; one is now guarded.
The 2026-08-07 sweep read the Triton (Steam Controller 2) usbip endpoint's
`bInterval: 1` as 125 µs — an 8 kHz duplicate storm — and the plan's G14 says to
raise it to 4 "like the Deck". That reading assumes a high-speed device, where
bInterval is the 2^(n-1) × 125 µs exponent. Both Triton devices declare
`UsbSpeed::Full`, and on a full-speed device the field is a plain frame count in
milliseconds: 1 means 1 ms, which is the 1 kHz the existing comment claims.
Raising it to 4 would mean 4 ms — a 4× cut to the motion rate a passed-through
SC2 delivers, in the name of fixing a problem it doesn't have. The endpoint now
carries the reasoning so the next reader doesn't repeat it.
G18's first bullet ("bound/rate-cap the host's rich-input channel; motion is
unbounded") is stale rather than wrong — it was true of the tree the sweep read.
Current main already routes rich input, motion included, through a 1024-deep
`sync_channel` whose `offer()` helper `try_send`s and drops on full, ending the
loop only on Disconnected. That is the same bounded-queue pattern the mic plane
adopted for security-review S6. Nothing owed.
G14's remaining bullet — DS/Deck neutral accel should read 1 g on the up axis
instead of 0 g free-fall — is deliberately NOT done here. Which axis is up is
precisely what G16's on-glass session measures: `switch_proto` documents the
wire as z-up and its neutral ships +Z, but the Deck's kernel negates Z/RZ, so
guessing would leave one backend confidently disagreeing with another. A wrong
constant is worse than the current obviously-unset 0.
Gate: fmt, build, clippy --all-targets -D warnings, and the test suites — green.
|
||
|
|
c010139e6e |
feat(client/apple): the gamepad UI moves and colours like the console it mirrors
ci / bun-nix (pull_request) Successful in 49s
ci / web (pull_request) Successful in 1m3s
ci / docs-site (pull_request) Successful in 1m16s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 4m38s
ci / rust (pull_request) Successful in 16m9s
Four reworks from the first palette-era on-glass review, all iOS-facing: - Surfaces carry the palette now, not just the text on them: ConsoleGlass washes every tier (Liquid Glass tint, pre-26 material, tvOS material) with ink.glass — the same colour the desktop console fills its panels with — and the close buttons move to an ink-aware consoleGlassBackground. The pre-26 branch also gains the focus tint it had silently dropped. Stray literals follow: ConnectOverlay text rides ink in the console takeover, card shadows soften on pale fields, the focused keycap reads onAccent. The online pip stays status-green on purpose. - The header breathes: title top padding 4/10 -> 10/18 plus shared header-spacing and title-bottom helpers mapped from the console shell's rhythm, applied to the launcher, settings and add-host alike, with the add-host close X re-anchored to the title row. - Settings, Add Host and the Library present IN PLACE on iOS: one persistent aurora whose calm is chased (the console's bg_mix), screens as transparent layers with the console's 0.26 s ease-out-cubic push/pop, an input drop for the transition, and the controller handed off through isActive — no more opaque bottom-up covers, no backdrop teardown. macOS keeps its sheets, tvOS its focus-engine covers. - The settings select is a real band: choice rows mount GamepadOptionBand, a spring-driven drum (Animatable body, ring-distance wrap, neighbours gated by focus and flight) whose retargeting spring accumulates rapid steps into one continuous spin. Reduce Motion falls back to a plain crossfade; toggles keep the quiet 14 pt slip. Verified: swift build (macOS), swift build --triple arm64-apple-ios17.0, swift test 208 passed / 0 failed. On-glass QA still owed: palette sweep on a pale palette, transition compositing over materials, drum feel on device. |
||
|
|
77797a9e20 |
feat(client/pads): stop streaming gyro into a session that cannot receive it
G8 of the gyro program, SDL-client half. The `Welcome` has always carried the backend the host actually RESOLVED, which is not necessarily the one the client asked for — Auto lands on Xbox 360 for anything not Sony/Valve/Xbox, and a Switch Pro on a Windows host folds to X360 too. No client read the field. So a player with an 8BitDo, or a Switch Pro on Windows, got a controller whose gyro did nothing, with nothing anywhere saying why: the client shipped ~250 Hz of Motion datagrams and the host parsed and discarded every one. `GamepadPref::has_motion()` answers whether a backend has a motion plane at all. The SDL client checks it on the first gyro sample: it logs one line naming the resolved backend and pointing at the fix (pick a DualSense-class controller type), then stops sending. Once per slot, not per sample — this path runs at the pad's sensor rate. `Auto` deliberately answers true. It means "unknown" — an old host that omitted the echo, which may well have resolved a DualSense — and suppressing motion on unknown would silently break working gyro, a worse failure than sending datagrams nobody reads. The predicate is an exhaustive match so a new backend has to state its answer rather than inherit one, and a table test pins both halves: a false negative kills working motion, a false positive keeps the void open, and both are silent. Owed: the plan wants this surfaced as a one-line UI hint, not just a log line. Apple already stores `resolvedGamepad` and Android needs the plumb; neither is done here, and both want their own gate. Gate (Linux CI image): fmt, build, clippy --all-targets -D warnings, and the test suites — green, with the new capability test observed running. |
||
|
|
c0f8f051c3 |
fix(client): the probe printed a device index the env var does not take
--probe-decode printed its DISPLAY position and called it the PUNKTFUNK_VK_DEVICE value. It is not. pick_device resolves that variable against the RAW vkEnumeratePhysicalDevices order (setup.rs, `devices.get(i)`) BEFORE any ranking runs, while the probe sorts discrete-first for readability. Those two orders disagree precisely on the hardware this flag exists to diagnose. pick_device's own comment records why the ranking is there: "enumeration order puts the iGPU FIRST on some hybrids (observed: Ryzen iGPU ahead of an RTX dGPU)". So on a hybrid laptop the number the probe printed for the iGPU could well be the number for the dGPU — a diagnostic handing out an actionable value that selects the other GPU, which is worse than printing none. Measured on the Arc + RTX 3500 Ada laptop, which is also where the first output went out with the wrong claim in it: three adapters, and the same Arc iGPU enumerated TWICE. So AdapterDecode now carries the raw enumeration index, captured before the sort, and the printer uses it; the "default presenter" marker stays on the first LISTED entry, because sorted-first is what pick_device lands on when nothing overrides. The duplicate is why the trailing hint names PUNKTFUNK_VK_ADAPTER as the safer knob and admits its limit: two adapters sharing a marketing name cannot be told apart by it, and a name match resolves to whichever enumerates first. The hint also states the thing this whole output invites a reader to get wrong — that a capable GPU in the list does not mean the decoder will use it, because Vulkan Video decodes on the presenter's device and PUNKTFUNK_DECODER does not move the presenter. Gates: fmt clean; clippy -D warnings on punktfunk-client-session and pf-presenter. |
||
|
|
43e713ecca |
fix(client): the probe now accounts for every bit it prints
First hardware run of --probe-decode, on the RTX 5070 Ti:
driver decode ops: H.264, H.265, AV1 (0xF)
Three names, four bits. 0xF is H.264|H.265|AV1|VP9 — bit 3 is
VK_VIDEO_CODEC_OPERATION_DECODE_VP9_BIT_KHR, a real decode operation this
client has no rung for, so the name table stopped short of it and the line
looked complete while silently dropping a codec the driver had advertised.
That is the exact failure this flag exists to prevent. The whole point of
--probe-decode is that a reader can trust the words to cover the number; a mask
with an unexplained bit asks them to trust it instead. VP9 is now named (marked
as having no punktfunk rung, because advertising it as decodable would be its
own lie), and any bit beyond the four we know prints as "unrecognised bits
0x…" rather than vanishing — so the next codec Khronos adds shows up as an
unknown rather than as nothing at all.
Gates: fmt clean; clippy -D warnings on punktfunk-client-session.
|
||
|
|
ce5047f3ad |
fix(host/pads): the Windows driver stops halving motion and stops serving torn reports
G6 + G15 of the gyro program. G6 — the UMDF gamepad driver's input path. Its timer ran at 8 ms and completed one pended READ_REPORT per tick, so a game could observe at most ~125 Hz while clients stream motion at ~250 Hz: every other sample was overwritten in the slot before anything read it, and the ones that survived carried up to 8 ms of extra latency. For gyro, a dropped sample is not a dropped frame — it is rotation that never reaches the game. The timer now ticks at 2 ms (about a real DualShock 4's Bluetooth cadence). Only the cheap half runs on every tick: read the input slot, complete one pended read. The channel handshake and the health marks stay on their historical ~8 ms, because they cost more, nothing wants them faster, and `driver_heartbeat`'s documented "+1 per ~8 ms tick" is what the host reads as liveness. The same slot is a single unqueued buffer that both sides touch without a lock, so a driver read landing mid-copy handed the game a report that was half the previous frame and half the next. For a button that is a one-tick glitch; for motion it is a spike in angular velocity, which an integrator turns into aim movement. `PadShm` gains an `input_gen` seqlock (v2.3, carved from reserved space inside the v2 legacy region): the host takes it odd, fences, writes the 64 bytes, and stores it even; the driver samples it either side of its read and retries once. The old code's own comment called this out as a known residual — it is now closed rather than documented. Version posture matches the ring's, with one simplification: no capability stamp is needed, because an old host never writes the field and a constant 0 is indistinguishable from "no write in flight", so a new driver against an old host behaves exactly as it does today, and an old driver ignores the field entirely. The Steam Deck write path had neither the seqlock nor even the trailing Release its DualSense sibling carried; all three Windows backends now publish through one `publish_input`. G15 — motion-cadence observability. The host already computed the measurement a "gyro feels floaty" report needs (client inter-arrival percentiles), but kept ONE global accumulator, so two motion-capable pads in a session interleaved into each other's gaps and produced a number describing neither. It also sat at `debug` behind a `tracing::enabled!` check, so a field log arrived with nothing in it and the only way to get the measurement was to ask for a re-run. Now per-pad and always on, summarized at `info` when the session ends — the moment a field report is being written. It costs one subtraction and one array increment per sample: percentiles come from a fixed log2 histogram instead of a growing sorted Vec, so there is no allocation, no per-window sort, and no way for a client streaming as fast as the link allows to make the instrument expensive. Percentiles are reported as bucket upper bounds (`_le`), which is a factor-of-two answer to a question whose answers are orders of magnitude apart. Gaps of 500 ms or more are counted as stalls rather than folded into the percentiles — an interruption is not a cadence, and averaging it in would report a healthy feed as a terrible one. Gates. Windows CI runner .133, the drivers workspace on the real WDK: cargo build, clippy -D warnings (which enforces the unsafe-audit lints), and fmt — all green, against a source whose SHA-256 matches this commit's. Linux CI image: fmt, build, clippy --all-targets -D warnings over pf-inject / punktfunk-core / punktfunk-probe / pf-client-core / pf-driver-proto / punktfunk-host, and the test suites including the 5 new motion-cadence tests — all green. Not measured on glass. G6's stated gate is a sensor-rate reading (SDL testcontroller or Steam's calibration screen) that matches the client's send rate; that is still owed, and a driver change only a compile has seen deserves it before anyone trusts the number. |
||
|
|
31bb3ca7f7 |
Merge pull request 'The console answers a mouse and a finger, and host cards get a menu' (#90) from worktree-console-tabs-pointer into main
ci / bun-nix (push) Successful in 24s
ci / docs-site (push) Successful in 1m17s
apple / swift (push) Successful in 1m32s
ci / web (push) Successful in 1m36s
deb / build-publish-client-arm64 (push) Successful in 2m42s
ci / rust-arm64 (push) Successful in 3m18s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m34s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m38s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 14s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 17s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 17s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m15s
docker / builders-arm64cross (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m34s
deb / build-publish (push) Successful in 4m51s
docker / deploy-docs (push) Successful in 34s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m51s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m2s
apple / screenshots (push) Successful in 5m53s
deb / build-publish-host (push) Successful in 7m16s
arch / build-publish (push) Successful in 7m59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m20s
android / android (push) Failing after 11m58s
flatpak / build-publish (push) Failing after 12m26s
ci / rust (push) Successful in 17m57s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 21m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m14s
Reviewed-on: #90 |
||
|
|
d49f955171 |
fix(console/host menu): the actions get their explainer line and room to breathe
ci / bun-nix (pull_request) Successful in 31s
ci / web (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 1m39s
ci / rust-arm64 (pull_request) Successful in 1m41s
apple / swift (pull_request) Successful in 1m36s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m2s
android / android (pull_request) Successful in 3m37s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m11s
ci / rust (pull_request) Successful in 5m6s
The rows sat hard against the pinned title — the menu is the one form screen with no subtitle, so the list started at the very top of the content band. It now wears Add Host's explainer, which both says what the menu is for and is the air that keeps the first row off the title. A pinned card explains what unpinning does and does not touch, the same wording the Android dialog uses. Caught by the screenshot pass, not by a test. |
||
|
|
507ea58da8 |
fix(host/audio): the silent-sink check recognizes the minted Speakers
Observed in the first real session on the substrate: the loopback ran on the minted 'Punktfunk Speakers' (silent on the host by construction), but have_silent name-matches only the Streaming Microphone — so the capture open logged 'desktop audio will also play on the host' (false) and re-attempted the Steam-pair install it doesn't need. The minted sink is recognized by id; its name honestly says Speakers, which the name rule must keep refusing for FOREIGN instances. |
||
|
|
2830cbe912 |
Merge pull request 'Say which GPU can do Vulkan Video, and why not when it can't' (#89) from worktree-vulkan-decode-probe into main
ci / rust-arm64 (push) Successful in 3m35s
ci / bun-nix (push) Successful in 29s
ci / web (push) Successful in 1m6s
ci / docs-site (push) Successful in 1m18s
apple / swift (push) Successful in 1m29s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m20s
deb / build-publish-host (push) Failing after 2m7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 34s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 32s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 26s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 15s
deb / build-publish-client-arm64 (push) Successful in 3m7s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 14s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m33s
android / android (push) Successful in 6m25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m2s
docker / builders-arm64cross (push) Successful in 10s
docker / deploy-docs (push) Successful in 31s
apple / screenshots (push) Successful in 6m4s
arch / build-publish (push) Successful in 7m45s
ci / rust (push) Successful in 8m13s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m7s
deb / build-publish (push) Successful in 8m36s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m30s
flatpak / build-publish (push) Successful in 17m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m4s
Reviewed-on: #89 |
||
|
|
4834c2ee51 |
fix(host/pads): DualShock 4 gyro ran 40× fast, and no pad ever stopped turning
Phase 1 of the gyro program (design/gyro-program.md, G1-G5) — the five correctness fixes under it. Gyro aim integrates angular velocity over time, so each of these is not a cosmetic wrongness: a wrong scale is every rotation being the wrong size, a wrong clock is every rotation being integrated against a fictional dt, and a stale sample is rotation that never happened. G1 — the DualShock 4 calibration blob. A Sony pad does not assume a motion scale, it reads one out of a fixed calibration feature report. Ours declared 0.5 LSB per °/s and 8192 LSB/g while the wire delivers 20 and 10000, so every DS4-type session decoded gyro 40× too fast and acceleration 1.22× hot — since the backend shipped. The blob now states the wire's own units (the DualSense blob's numbers, deliberately: both pads consume the identical wire sample). Its interleaved per-axis order is NOT a bug and stays: the virtual pad declares BUS_USB, where interleaved is the correct layout; grouped is Bluetooth's. The same blob lives a second time in the UMDF driver, which is a separate WDK workspace that cannot depend on pf-inject — one wrong table in two files, where fixing one reads as fixing it. Both are fixed, and the DS4 feature reports now live in dualshock4_proto beside the DualSense's rather than in the Linux backend, so there is one canonical copy to point at. Field hosts keep the old blob until they update the host package. G2 — the gate that would have caught it. Nothing pinned any backend's declaration against the wire, so tests/motion_contract.rs now applies the CONSUMER's arithmetic (the kernel's, and SDL's, which differ) to each backend and asserts the result lands back on the wire constants — for the DualSense and DS4 blobs, and for the Deck and Switch Pro rescales. It also parses the driver's Rust source and re-derives the units from THAT, so the two copies cannot drift. Verified non-vacuous both ways: re-introducing the old blob fails with "declares a fractional 32/64 LSB per °/s", and reverting only the driver's copy fails with "the UMDF driver's DS4_FEATURE_CALIBRATION has drifted from pf-inject's". The wire units themselves move to punktfunk_core::input::gamepad, referenced by the client's capture scale, the Deck/Switch rescales, and the probe — whose at-rest vector said 16384 (a driver's number, not the wire's) and now says 1 g. G3 — real sensor clocks. The DualSense advanced its sensor timestamp by +1 raw unit per report (0.33 µs — a frozen clock) and the DS4 by a flat +188 (~1 ms) regardless of the real 4-8 ms cadence. Anything integrating rate × dt off that field got nonsense. All four backends now stamp elapsed monotonic time in their own units via a shared SensorClock, anchored to the pad's first report so an irregular publish loop cannot make it drift, and truncated to the field width — which reproduces the wrap real hardware does. G4 — motion is level-triggered and had no watchdog. merge_frame preserves the last sample and the heartbeat re-emits it, so a feed that stops leaves the pad rotating forever — and with G3's honest clock, at a dt that keeps growing. Rumble and the pen plane each have an idle timeout; motion now has one too, at 100 ms. Angular velocity only: acceleration is kept, because gravity is legitimately persistent and blanking it reads as free-fall. The SDL client parks its gyro at zero when a slot closes, which is the case we can flush rather than wait out. (The Apple half of this rides in PR #88.) G5 — a pad returning inside the 300 ms replug grace keeps the same device and skips the create path, so a different controller inherits the previous one's touch contact and rotation — and a pad with no gyro never sends a sample to correct it. sweep() now reports re-claims separately from drops, and the manager clears the rich plane on one. Rich fields only: rumble and hidout dedup deliberately survive a removal. Gates (Linux, CI image): fmt, build, clippy --all-targets -D warnings over pf-inject/punktfunk-core/punktfunk-probe/pf-client-core, and the test suites — 110 pf-inject unit + 6 contract + 29 pf-client-core gamepad, all green. Not yet verified on glass; the on-glass sign/scale session is G16. |
||
|
|
413a3e3e76 |
feat(client): say which GPU can do Vulkan Video, and why not when it can't
ci / bun-nix (pull_request) Successful in 29s
ci / rust-arm64 (pull_request) Successful in 1m44s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m5s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 3m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m1s
ci / rust (pull_request) Successful in 4m27s
ci / docs-site (pull_request) Failing after 16m24s
ci / web (pull_request) Failing after 16m25s
Field report from an Intel Arc + NVIDIA laptop: pinning the Vulkan rung on the Arc iGPU silently produced D3D11VA, and there was no way to tell whether the build had tried at all. That ambiguity was ours, in three places. The "unavailable" log printed three of the FIVE conjuncts that gate Vulkan Video. A device with 1.3, the features and a decode queue family — but no codec extension — logged dev_is_13=true features_ok=true decode_family=true next to the word "unavailable" and named nothing actionable. It now prints all five, plus which base extensions are missing, which codec extensions are present, the decode family's own advertised codec operations, and the device name and vendor. It also no longer says "VAAPI/software" on Windows, where the rung below is D3D11VA. The native-vulkan PIN refusal logged `video_decode` alone. On a device that decodes something but not THIS codec, that reads as a contradiction: refused, yet video_decode=true. It now carries the caps mask and the codec bit that was wanted, so "your GPU can't" is distinguishable from "we asked for the wrong thing" — only the second is our bug. And `--probe-decode` is new: per-adapter Vulkan Video capability with no session, no surface and no logical device. For each GPU it answers usable yes/no, the driver's own decode ops, the extensions, and — when the answer is no — which conjunct failed, in words. Separate from --list-adapters, which the desktop shells parse line-by-line for their GPU picker and which therefore keeps printing bare names. The listing is ordered like pick_device (discrete first) and marks entry 0 as the default presenter, because that ordering is very likely the reporter's actual answer: pick_device ranks DISCRETE_GPU above INTEGRATED_GPU, Vulkan Video decodes on the PRESENTER's device by design (that is what makes it zero-copy), and PUNKTFUNK_DECODER does not move the presenter. So on a hybrid laptop, pinning the decoder while the dGPU presents probes the wrong GPU entirely — PUNKTFUNK_VK_DEVICE=<index> is the knob that moves it, and the index printed is that value. To keep the probe honest, VIDEO_BASE and VIDEO_CODECS moved to module scope and the five-way AND became video_decode_gate(), called by both the probe and device creation. A probe holding its own copy of the rule is one that eventually reports a capability the session then refuses — which reads to everyone as a decoder bug rather than a probe bug. Gates: fmt clean; clippy -D warnings over punktfunk-client-session and pf-presenter. The Linux container was unavailable (the host's disk filled and took the docker daemon with it), so this ran on the macOS host target only — the container leg is owed, and CI covers it on the PR. |
||
|
|
f06b3d9d04 |
feat(console): the console answers a mouse and a finger, and host cards get a menu
Two gaps, both found on the shared Linux/Windows console UI. **The settings tabs only moved for a gamepad.** They were bound to the shoulder buttons and to PgUp/PgDn, and the legend spells PgUp/PgDn out only when NO pad is attached — so with a controller plugged in a keyboard user had nothing to find, and a mouse or a touchscreen could not change section at all. The root cause was wider than the strip: `SkiaOverlay::handle_event` matched only `KeyDown` and `TextInput`, so every mouse button, wheel and touch contact fell past the console into the run loop, which routes pointer input exclusively at `stream.capture` — `None` while you are browsing. Nothing in the console had ever been clickable. Making just the pills answer would not have helped either: the settings screen is opened with X from home, so a mouse could not reach it. So the console gets a real pointer path: - `Overlay::handle_pointer` carries mouse/touch in SWAPCHAIN PIXELS. The run loop converts (it owns the window, hence the display scale, and mouse coordinates are logical while fingers are normalised); the console then hit-tests the very rects it drew last frame. Only DIRECT touch devices are offered — an indirect trackpad already drives the mouse. - Widgets act on the PRESS, not the release. The list and both carousels scroll the focused item toward the centre, so what you pressed has slid out from under your finger by the time it lifts; press-to-act has no such race and there is no drag gesture to compete with. - The hint bar became the pointer's button bar. It is already the console's only on-screen statement of what the face buttons do, and a pointer has none — so its Confirm/Back/Secondary/Tertiary pills are clickable on every screen, which is what puts Settings and Library within reach of a mouse at all. - Tab / Shift+Tab change section; PgUp/PgDn still do, and the keyboard legend now reads "Tab". - Right-click is Back everywhere, EXCEPT at the root: B there quits the launcher and a right-click is far easier to fire by accident. Quitting stays explicit. **Host cards had no menu.** Every other client hangs Wake / Copy link / Edit / Forget off a host card; the console could add a host and connect to one, and that was all — so a renamed machine or a fat-fingered address stayed wrong forever unless you opened a desktop shell. UP on a saved tile now opens that host's menu, the same gesture the Android console uses, on the one direction a horizontal carousel leaves free. - `ConsoleCmd::UpdateHost` edits the stored host IN PLACE. Removing and re-adding would silently drop the fingerprint, the learned MAC, the pinned cards and the profile binding — that is a rename, not a re-pair. - `ConsoleCmd::ForgetHost` drops it; if it is still advertising it returns as a discovered, unpaired row, which is the honest state. - Forget arms on the first press and fires on the second. The other clients forget outright; a console is driven by a thumbstick from across a room. - A pinned profile card offers only Unpin. It is a shortcut, not a second host, and offering to forget the host from it would blur exactly the distinction a pin draws. - "Edit…" REPLACES the menu on the stack rather than stacking over it, so Back from the editor doesn't land on a menu describing the host as it was before the edit. Verified in the pf-lxcheck2 container (this crate compiles to nothing on macOS — a bare `cargo check` there is vacuous): plain build and `clippy --all-targets` clean under `-D warnings`, 72 tests pass. Seven are new, and cover the reported bug directly — a press on a pill selects that tab, and each tab still keeps its own cursor when a pointer is what switched it. |
||
|
|
99a59fb5c7 |
fix(host/audio): the mic pump's first resolve waits for the minted endpoints
Measured on the target box: the pump wired 2 s before the provisioning worker latched, took the cable as its write target, and the next wiring pass would then have paired the default recording with the minted microphone — which nothing writes into: dead mic-air until a pump reopen. resolve_target now provisions synchronously (instant once latched; the opt-out env is honoured), so the pump's held device and the plan's verdict can never disagree. |
||
|
|
15dddf7ec0 |
feat(web): the Dashboard shows the audio wiring verdict
An Audio wiring card (Windows hosts) below the status tiles: a readiness badge (Ready / No microphone / No game audio / Not wired), the friendly names carrying each role, and the degradation notes that were previously visible only in the host log — mic withheld for game audio, the known- degraded last resort, a narrowing endpoint. api/openapi.json regenerated from the host build (AudioWiring + RuntimeStatus.audio); en+de messages. |
||
|
|
3870cdd1da |
feat(host/mgmt): the audio wiring verdict joins /status
RuntimeStatus gains an 'audio' object (Windows hosts): readiness (full/audio_only/mic_only/none), the friendly names carrying each role, and the three degradation flags (mic_withheld, last_resort, narrowing) — the verdicts that previously lived only in tracing logs. Snapshot of the last wiring pass (the mic pump wires at host start and on every reopen); a status poll never triggers COM work or IPolicyConfig writes. |
||
|
|
f7ef41b45b |
Merge pull request 'feat(clients): the phone's gyro can speak for a gyro-less pad' (#88) from worktree-gyro-phone-mirror into main
apple / swift (push) Successful in 1m30s
ci / web (push) Successful in 1m9s
ci / bun-nix (push) Successful in 53s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
ci / docs-site (push) Successful in 1m35s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 1m2s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m23s
ci / rust-arm64 (push) Successful in 6m21s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 25s
android / android (push) Successful in 8m5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5m18s
release / apple (push) Successful in 10m33s
docker / builders-arm64cross (push) Successful in 1m46s
docker / deploy-docs (push) Failing after 3m52s
ci / rust (push) Successful in 11m25s
apple / screenshots (push) Successful in 5m48s
Reviewed-on: #88 |
||
|
|
55aba3e936 |
feat(client/android): the phone's gyro can speak for a gyro-less pad
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m37s
ci / bun-nix (pull_request) Successful in 30s
android / android (pull_request) Successful in 4m5s
ci / rust-arm64 (pull_request) Successful in 4m1s
ci / docs-site (pull_request) Successful in 1m26s
ci / rust (pull_request) Successful in 10m54s
Opt-in "Gyro from this phone" (gyro_on_phone, off by default): this device's IMU sources wire pad 0's motion while that pad is a controller with no motion source of its own. On Android that gate is exact — the only pads that forward motion are the capture links (USB DualSense / SC2, claimed as ExternalPads), so the mirror stands down per sample whenever GamepadRouter.padHasOwnMotion(0) says a capture link holds the index, and sends nothing while pad 0 has no slot at all (motion never creates a host pad). "Rumble on this phone"'s sibling, data flowing the other way: same read-once-at-attach settings plumbing, same hardware-gated rows in the touch and controller settings (a TV box has no gyroscope to mirror from). DeviceGyro registers TYPE_GYROSCOPE + TYPE_ACCELEROMETER at ~200 Hz on a dedicated HandlerThread with batching disabled (maxReportLatencyUs = 0 — batching is poison for gyro aim), converts with the wire contract shared with pf-client-core (rad/s → 20 LSB/°·s, m/s² → g → 10000 LSB/g; Android's accelerometer already reads specific force, the DualSense report's own convention), and rotates each sample from the natural-portrait sensor frame into the controller frame by display rotation — a phone clipped landscape yaws when the player yaws instead of rolling. The remap matrix and unit constants are pinned by DeviceGyroTest. A stand-down edge (capture link claims pad 0, or session teardown) sends one zero-gyro sample so the host's virtual pad never keeps integrating an angular velocity this device stopped producing — the gyro sweep's stale-rotation latch, avoided by construction here. |
||
|
|
1f54b75c5c |
feat(client/apple): the phone's gyro can speak for a gyro-less pad
Opt-in "Gyro from this device" (DefaultsKey.gyroFromDevice, off by default, iOS only): while player 1's forwarded controller reports no rotation rate of its own — no GCMotion, or the gravity-only motion an Xbox pad exposes — this device's IMU sources pad 0's wire motion instead. The rumble-on-device mirror's sibling, data flowing the other way: same session-scoped UserDefaults read, same hardware-gated settings rows, same pad-0 rule. DeviceGyro wraps CMDeviceMotion at the ~100 Hz CoreMotion ceiling on a dedicated serial queue (not main — the controller path's main-queue delivery is a known jitter source), converts with the shared GamepadWire constants, and rotates each sample from the device's portrait frame into the controller frame by interface orientation, so a phone clipped landscape yaws when the player yaws instead of rolling. The remap matrix is derived and pinned by DeviceGyroRemapTests. GamepadCapture owns engage/stand-down (reconcile, suspend/resume, stop), and suppresses pad 0's controller-motion forwarding while the mirror runs — two writers on one pad's motion state would fight, and the accel-only stream would stomp the mirror's gyro with zeros. Also fixes the stale-motion latch from the gyro sweep on the controller path: flush now parks motion at zero (keeping the last accel, so gravity doesn't become free-fall), and the mirror's stop sends the same closing zero. The host holds motion as state and re-emits it — a nonzero angular velocity left behind read as endless rotation for as long as an overlay (Control Center pull-down) kept the app inactive. |
||
|
|
79c72fa64d |
fix(host/audio): capture endpoints carry the {0.0.1.…} id prefix
The capture-direction lookup built its endpoint id with the RENDER prefix
{0.0.0.00000000}., but WASAPI's enumeration returns capture ids as
{0.0.1.00000000}.{guid} — so the minted microphone's capture side never
string-matched the enumeration and the wiring plan paired no recording
device (audio-probe plan on the target box: mic_capture = '-'). Measured;
IMMDeviceEnumerator::GetDevice tolerated the wrong prefix, which is why
the S3 spike's direct open still passed.
|
||
|
|
64655c5275 |
fix(host/devtest): audio-probe plan provisions synchronously
A fresh CLI process has no startup worker to have finished, so the plan devtest raced its own background provisioning thread and printed the name ladder instead of tier-0. ensure_blocking() re-resolves existing marker devnodes in milliseconds before the wiring pass runs. |
||
|
|
c6b183450a |
Merge pull request 'docs: the 4:4:4 story catches up with the code that ships it' (#87) from worktree-docs-support-matrix-444 into main
ci / rust-arm64 (push) Successful in 2m14s
ci / web (push) Successful in 2m17s
ci / bun-nix (push) Successful in 24s
ci / docs-site (push) Successful in 1m59s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 5s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 2m54s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 4m31s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 3m25s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m50s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m14s
ci / rust (push) Successful in 13m30s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m29s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 4m41s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
Reviewed-on: #87 |
||
|
|
4a621de6b1 |
chore(packaging): retire VB-Cable — audio's substrate is Steam's drivers
The other half of the audio-substrate decision (spikes S2+S3 green, minted
endpoints landed in the previous commit): stop bundling a third-party
kernel driver the host no longer needs.
installer the VB-CABLE task, payload, silent-install run and the
donationware notice are gone; a suppressible notice tells
a Steam-less box that audio needs Steam INSTALLED (never
running) and that installing it later just works. A cable
from an older install is still deliberately not removed.
packer + CI -VbCableDir/VBCABLE_DIR, the staged-payload check and the
runner provisioning download are gone; SBOM drops the
redistributed-driver component.
winget the VB-Audio bundling-grant agreement becomes the honest
Steam requirement (surfaced on the unattended path where
no wizard is on screen).
docs windows-host/uninstall/security/echo say what actually
ships: no kernel-mode driver of our own, endpoints minted
from Valve's vendor-signed drivers, VB-CABLE mentioned
only as the historical fallback that keeps working.
host wording the mic-open guidance and module headers lead with Steam;
the NAME ladder itself is untouched — demoting 'cable
input' was considered and rejected (on a box where minting
transiently fails, the SSM would outrank an installed
cable, steal the silent sink, and make audio host-audible).
|
||
|
|
3a35773b70 |
Merge origin/main; 4:4:4 has no software floor, and the ABI is 17
Reconciling with #85 (FFmpeg is gone from the client). Two of my claims were true against the pre-merge tree and false against this one. Note 4 said the desktop clients need no 4:4:4 decode probe "because every rung can display full chroma — swscale converts for the software rung". There is no swscale any more. The CPU floor is openh264 + rav1d, it is 4:2:0 8-bit by contract and has no HEVC at all, so it refuses a 4:4:4 stream rather than converting one. The client still advertises the bit unprobed, which was the point of the original fix, but the honest reason is different: full chroma is a hardware path (Vulkan RExt, NVIDIA today), and what catches a box whose hardware 4:4:4 fails is note 2's codec reconnect, not a downgraded picture. Note 13 repeated the same wrong premise and now names both halves of its warning. The C ABI is 17, not the 14 I read before the merge. Textual side of the conflict: #85 rewrote the Codecs column and notes 1-3 of the same table while leaving note 4's stale 4:4:4 text alone. Theirs kept in full; only the 4:4:4 column and note 4 are mine. |
||
|
|
5aa1ca392e |
Merge pull request 'fix(host/encode): the Windows host build stops failing on unused split-encode helpers' (#86) from worktree-fix-winhost-clippy-dead-code into main
ci / web (push) Successful in 1m22s
ci / rust (push) Canceled after 1m39s
ci / rust-arm64 (push) Canceled after 1m38s
ci / docs-site (push) Canceled after 9s
ci / bun-nix (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Successful in 1m37s
android / android (push) Successful in 4m58s
deb / build-publish-host (push) Successful in 5m28s
apple / swift (push) Successful in 1m30s
arch / build-publish (push) Successful in 11m59s
apple / screenshots (push) Successful in 5m59s
deb / build-publish (push) Successful in 8m56s
windows-host / package (push) Successful in 15m28s
windows-host / winget-source (push) Skipped
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 24m1s
windows-host / canary-manifest (push) Successful in 23s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m43s
Reviewed-on: #86 |
||
|
|
0b160a4e22 |
feat(host/audio): minted Punktfunk endpoints become the wiring plan's tier-0
The audio-substrate program's Phase 2 (spikes S2+S3 measured green on the
target box): the host mints its OWN instances of Valve's streaming-audio
drivers and wires by IDENTITY instead of borrowing Steam's primaries —
minted.rs the provider: one devnode per role ('Punktfunk Speakers'
from SteamStreamingSpeakers.inf, 'Punktfunk Microphone'
from SteamStreamingMicrophone.inf), marker-matched across
restarts (PunktfunkAudioRole in Device Parameters — names
are NOT identity, a minted instance is name-identical to
the primaries), provisioned on a startup worker like pad
audio, retried with a 60 s cool-down from wiring passes,
defaults restored when a fresh endpoint grabs them.
wiring_plan MintedIds tier-0: the mic takes its minted device outright
(capture side paired by the provider's id — a name search
cannot tell it from the primary), the loopback prefers the
minted sink at the head of the silent tier, an operator
override still beats everything, a narrowing minted sink
demotes below real hardware, and stale ids fall back to
the ladder unchanged. Plus AudioReadiness — the
full/audio-only/mic-only/nothing classification, logged
with every plan change (§C4's seed).
audio-probe 'mint' runs the provider synchronously; 'plan' prints one
real wiring pass + readiness — the field-triage command.
Without Steam's drivers nothing changes: provisioning degrades to absent
ids and the plan keeps the name-based ladder (primaries → cable → real
hardware) exactly as before.
|
||
|
|
b245cb6b29 |
Merge pull request 'FFmpeg is gone from the client — native decode M0–M10' (#85) from worktree-native-decode-m0 into main
android / android (push) Canceled after 0s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Canceled after 33s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
audit / cargo-audit (push) Successful in 29s
audit / bun-audit (sdk) (push) Successful in 20s
audit / bun-audit (plugin-kit) (push) Successful in 23s
audit / bun-audit (web) (push) Failing after 25s
audit / docs-site-audit (push) Successful in 21s
audit / pnpm-audit (push) Successful in 24s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m53s
audit / license-gate (push) Successful in 4m48s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m5s
decky / build-publish (push) Successful in 23s
release / apple (push) Successful in 9m25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m4s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m5s
flatpak / build-publish (push) Failing after 14m52s
nix / flake (push) Successful in 16m42s
Reviewed-on: #85 |
||
|
|
f49f22a292 |
fix(host/encode): the Windows host build stops failing on unused split-encode helpers
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m26s
ci / docs-site (pull_request) Successful in 1m28s
apple / swift (pull_request) Successful in 1m37s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m31s
android / android (pull_request) Successful in 3m29s
ci / rust (pull_request) Successful in 9m51s
The Windows host job died in its clippy step: eight items in pf-encode's split-encode policy (`SPLIT_AUTO`..`SPLIT_DISABLE`, `resolve_split_mode`, `max_forced_split_mode`, `clamp_to_engines`) were reported as never used, and `-D warnings` turns that into a build failure. Nothing about the encoder was wrong — the items simply have no reader in one particular build of the crate, and nothing was telling the compiler that. `codec.rs` compiles on every platform, but the split policy only ever has a caller on Linux (the libav NVENC path reads it unconditionally) or on Windows with the `nvenc` feature (the direct-SDK backend). A featureless Windows build of pf-encode has neither, so every item in the cluster is genuinely dead there. Gate them on the union of their callers' cfgs, the way `forced_split_width` next door already is. The step lints pf-encode itself WITH `--features nvenc,amf-qsv,qsv`, where the items are live, which is why this was invisible there; the failure came from the next command in the same step, `clippy -p pf-vdisplay`, which pulls pf-encode in as a plain default-features dependency. Same item-level `dead_code` trap this crate has now hit five times. Verified: default-features pf-encode reproduces all eight errors before the change and none after (macOS default-features exercises the identical "cluster has no caller" arm as featureless Windows — the two remaining errors there, `vbv_frames_env` and a redundant closure call, are pre-existing and macOS-only; both items have real Windows callers). Linux default-features and Linux + nvenc `--all-targets` both stay clean, so the callers still see the policy. `cargo fmt` clean. |
||
|
|
f1e7ec3535 |
docs: the 4:4:4 story catches up with the code that ships it
The support matrix said the desktop clients' Full chroma switch "has no effect today" and that only the Apple client asks for 4:4:4. Both stopped being true in July: `clients/session/src/main.rs` advertises VIDEO_CAP_444 whenever the setting is on, deliberately with no client-side probe, because every desktop decode rung can display full chroma — the Vulkan presenter samples the 2-plane 4:4:4 pool formats and swscale converts for the software rung. So Linux, Windows and Apple all ask; Android is the one that genuinely doesn't implement it. The other half was HDR. `9f72a3b6` gave the Windows IDD-push capturer a packed 10-bit BT.2020 PQ RGB output, so NVENC encodes HEVC Main 4:4:4 10 and the two compose — the matrix still said "4:4:4 and HDR together is refused", and hdr.md still called PyroWave the only exception. Linux is the side that keeps the trade: handshake.rs resolves the depth back to 8 for a 4:4:4 session, so full chroma wins and the stream is SDR. Three cells move ❌ → ⚠️ rather than ✅ on purpose. The client half is unconditional, but the host half is not: HEVC 4:4:4 means an NVIDIA host, or PyroWave on any vendor. The notes say which, and point at the stats overlay's `4:4:4→4:2:0` tag — this negotiation is the one that fails loudly. Also: C ABI version 13 → 14; PyroWave's ≈8K 4:4:4 block-index ceiling now has a note; and the roadmap no longer calls Intel 4:4:4 a hardware limit, which the matrix and vaapi.rs both contradict — VCN can't, VAAPI hasn't. Spot-checked and left alone as still accurate: the Linux client clipboard stub, VAAPI declining 4:4:4, Android having no 4:4:4 at all, and the wire / driver / gamepad-channel versions. |
||
|
|
bbbcf321e5 |
Merge origin/main into worktree-native-decode-m0
ci / web (pull_request) Successful in 1m19s
apple / swift (pull_request) Successful in 1m32s
ci / docs-site (pull_request) Successful in 1m23s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 25s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m8s
android / android (pull_request) Successful in 3m31s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m23s
ci / rust-arm64 (pull_request) Successful in 5m36s
nix / flake (pull_request) Failing after 11m59s
ci / rust (pull_request) Successful in 14m27s
main moved 93 commits while this branch ran. Two conflicts, both where main's new
work sat next to M10's excision:
packaging/flatpak/io.unom.Punktfunk.yml — main added the vendored gamescope WSI
layer (the only route to HDR on a Deck) and, before it, a vulkan-headers module.
Took both: this branch predates them and deletes neither. But the headers module's
stated consumer was pf-ffvk's bindgen over FFmpeg's hwcontext_vulkan.h, and M10
deleted pf-ffvk — so it now reads as dead weight to the next person. It is not:
the WSI layer IS a Vulkan layer, compiles against those headers, and builds after
it, so module order is the dependency. Rewrote the rationale to say so, including
why dropping it would be expensive to discover — flatpak.yml has no pull_request:
trigger, so a manifest break reaches main invisibly and a tag then ships no Linux
flatpak. Also recorded that the native decoder needs nothing from there: pf-vkdecode
reaches Vulkan through ash, which is pure Rust bindings, no bindgen, no C headers.
crates/pf-console-ui/src/screens/settings.rs — main restructured the gamepad
settings into TABS, which removed the per-row section headers; this branch had left
Some("Video") untouched from the merge base and added the pre-M10 decoder migration
next to it. Git could not tell those apart. Took main's structure (no header, its
deliberate change) with this branch's migration layered on: a stored `vulkan`,
`vaapi` or `d3d11va` names no preset in the tabbed list and would render as "—",
then silently rewrite the user's preference on the next save.
Gates on the merged tree, Linux container: fmt clean; cargo check --workspace
--all-targets clean; clippy --workspace --all-targets -D warnings clean; tests
green across pf-vkdecode (187), pf-client-core (163), pf-console-ui (58) and
punktfunk-host (447 of 448 — the one failure is the pre-existing
gamestream::stream::tests::sender_delivers_batches, a UDP-loopback EINTR under
qemu that fails identically on a pristine HEAD).
|
||
|
|
03ee3e55d7 |
feat(host/devtest): audio-probe — the audio-substrate spike measurements
The S1-S3 spikes from windows-audio-endpoints-and-vbcable.md as one
runnable devtest (no game, no client, ssh-drivable):
audio-probe ssm S3, the decision gate: mint a SECOND devnode of
Valve's Steam Streaming Microphone driver and
prove the pair end to end (tone into its render
endpoint must come back out of its capture
endpoint). Pass = a punktfunk-owned virtual mic
needs no VB-Cable wherever Steam is installed.
audio-probe sink S2: mint a Speakers instance, park the DEFAULT
playback on it, tone through the default device,
WASAPI-loopback the instance - the desktop-audio
capture path minus the game.
audio-probe sss-primary S1: the primary Speakers' known-silent loopback,
re-measured, with mix format + steam.exe state.
audio-probe cleanup remove every probe-minted devnode (marker value
in Device Parameters, never name-guessing).
pad_endpoint grows the first slice of the design's §C1 shared minting
surface: create_media_devnode(desc, hwid, mark), bind_driver(hwid, inf),
find_capture_endpoint_for_devnode — the pad provisioner now calls the
same functions. The probe restores whatever default devices the minting
disturbed before it exits.
|
||
|
|
152047051c |
fix(host/windows): three SID unsafe blocks get their safety proofs
install.rs (landed 2026-08-05 with the security-review remediation, while the Windows CI runner was down) fails windows-host.yml's clippy gate: #![deny(clippy::undocumented_unsafe_blocks)] wants the SAFETY comment on the line preceding EACH unsafe block, and three blocks didn't have one — two sat behind a comment anchored to the enclosing closure/neighbouring statement, and EqualSid had none at all. Comments only; no behavior change. |
||
|
|
3608de25ed |
Merge pull request 'fix(ci): runner hygiene stops eating its own jobs' (#84) from worktree-ci-runner-hygiene into main
ci / bun-nix (push) Successful in 27s
ci / docs-site (push) Successful in 1m20s
apple / swift (push) Successful in 1m25s
ci / web (push) Successful in 1m28s
deb / build-publish-client-arm64 (push) Successful in 1m36s
ci / rust-arm64 (push) Successful in 3m31s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
decky / build-publish (push) Successful in 43s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
deb / build-publish (push) Successful in 3m53s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
deb / build-publish-host (push) Successful in 4m3s
docker / builders-arm64cross (push) Successful in 10s
android / android (push) Successful in 6m31s
apple / screenshots (push) Successful in 5m45s
windows-host / package (push) Failing after 7m11s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
arch / build-publish (push) Successful in 8m0s
ci / rust (push) Successful in 10m1s
docker / deploy-docs (push) Successful in 28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m33s
Reviewed-on: #84 |
||
|
|
ef8214415f |
fix(host/audio): game audio outranks the mic on cable-less boxes
The wiring plan reserved the mic target unconditionally first, so on a box without VB-Cable the mic took the Steam Streaming Microphone — the only working client-only loopback sink — and desktop audio fell to the known-silent Speakers last resort: a headless Steam-only host streamed SILENCE (the 2026-08 field case), and the installer's 'optional (mic passthrough)' wording never warned anyone. The mic may now hold the Streaming Microphone only while the loopback still gets a preferred (non-last-resort) pick without it — another silent sink or real hardware. Otherwise the loopback takes the endpoint and the mic falls to a lesser candidate or is honestly withheld (Wiring::mic_withheld), with the open error naming the trade and the remedy. An operator PUNKTFUNK_MIC_DEVICE override is exempt: an explicit choice may still strand the loopback on the last resort. Also: the Steam-pair auto-install latch is now once per INF-state instead of once per process — an attempt made while Steam was absent re-arms when its driver INFs later appear (files are invisible to the endpoint-set fingerprint, so nothing else would ever retry), and a withheld mic skips the pointless reinstall (the pair exists; the plan gave it to the loopback). |
||
|
|
75dfab1d35 |
fix(host): a reconnecting session inherits its launch instead of starting it again
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 2m48s
ci / web (pull_request) Successful in 2m34s
android / android (pull_request) Successful in 5m51s
ci / docs-site (pull_request) Successful in 1m48s
ci / rust (pull_request) Canceled after 6m55s
ci / rust-arm64 (pull_request) Canceled after 6m35s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 3m6s
Two defects, found while tracing M8's codec-fallback reconnect and recorded
verbatim in
|
||
|
|
138a1f1b2f |
fix(host/windows): the staging-dir SID checks document their unsafe blocks
ci / web (pull_request) Successful in 1m9s
ci / docs-site (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m34s
ci / bun-nix (pull_request) Successful in 22s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 4m4s
android / android (pull_request) Successful in 4m28s
ci / rust (pull_request) Successful in 9m12s
clippy's undocumented_unsafe_blocks (deny) flagged the three blocks that
|
||
|
|
39cfb7234c |
fix(ci/docker): a cache-hit builders job stops failing on a login it never uses
The LAN-registry docker login only serves the Push step (Reconcile and Tag-for-release authenticate via curl -u), but it ran unguarded — so a hit=true leg landing on a host with a misconfigured docker daemon failed at login with nothing to push (run 16044/16013 f44 leg). Gate it like Build/Push. |
||
|
|
c3cdee9bf5 |
fix(ci/prune): the 2-minute image prune stops deleting images mid-pull
docker image prune -af --filter until=2h keyed on image CREATION time, so a
base image built days ago that merely had no container at that instant was
"aged" — including one a job had just pulled and not yet created. Measured
2026-08-07: three job failures, each coinciding with a prune tick to the
second ("No such image: …punktfunk-rust-ci:latest", every step cancelled),
plus a 4-7 GB re-pull of every idle base image within minutes.
The routine tick now retires only what this host actually accretes — per-SHA
app tags older than 2h (their creation time IS the local build time) — then
sweeps dangling layers, which cannot touch a tagged image. The blanket -a
prune survives only in the near-ENOSPC burst guard, where one re-pull beats
every concurrent job dying.
docker-reclaim.{sh,service,timer} are the hourly leak reclaimer that so far
lived hand-installed on home-runner-1 only; home-runner-2 went without it and
re-accumulated 176 leaked volumes (~60 GB) until jobs died of ENOSPC on
2026-08-06/07. Checked in so both hosts install the same files from here.
|
||
|
|
dee97e893c |
fix(vkdecode): the address the driver keeps is now the address we keep
The AV1 use-after-free fix (
|
||
|
|
93b8528d09 |
Merge pull request 'fix(encode): NVENC split-frame encode never engaged for HDR — engage it, measured' (#83) from worktree-nvenc-s1-split-reconfigure into main
ci / bun-nix (push) Successful in 24s
ci / web (push) Successful in 1m4s
ci / docs-site (push) Successful in 1m16s
apple / swift (push) Successful in 1m35s
deb / build-publish-client-arm64 (push) Successful in 1m34s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 8s
windows-host / canary-manifest (push) Skipped
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 6s
ci / rust-arm64 (push) Successful in 3m30s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 6s
docker / builders-arm64cross (push) Skipped
deb / build-publish (push) Successful in 3m55s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m3s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m37s
deb / build-publish-host (push) Successful in 4m56s
android / android (push) Successful in 6m46s
apple / screenshots (push) Successful in 5m37s
windows-host / package (push) Failing after 5m23s
windows-host / winget-source (push) Skipped
arch / build-publish (push) Successful in 7m56s
ci / rust (push) Successful in 9m35s
docker / deploy-docs (push) Successful in 6m27s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m45s
Reviewed-on: #83 |
||
|
|
b5205fef52 |
Merge pull request 'fix(client/apple): audio stops crackling on lossy, bunching Wi-Fi' (#82) from worktree-audio-wifi-distortion into main
ci / rust-arm64 (push) Failing after 13s
arch / build-publish (push) Failing after 22s
ci / bun-nix (push) Successful in 21s
apple / swift (push) Successful in 1m32s
ci / web (push) Successful in 1m26s
ci / docs-site (push) Successful in 1m27s
deb / build-publish-host (push) Failing after 1m26s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 1m2s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 8s
docker / builders-arm64cross (push) Skipped
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m28s
deb / build-publish (push) Successful in 4m33s
android / android (push) Successful in 5m53s
docker / deploy-docs (push) Successful in 37s
deb / build-publish-client-arm64 (push) Successful in 4m31s
ci / rust (push) Successful in 6m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 4m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m47s
flatpak / build-publish (push) Failing after 4m28s
windows-host / package (push) Failing after 6m36s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
release / apple (push) Successful in 11m44s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m24s
apple / screenshots (push) Successful in 6m5s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m15s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m57s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 12m58s
Reviewed-on: #82 |
||
|
|
515a3c2912 |
feat(pf-encode): wire split arbitration on Windows too
ci / rust (pull_request) Failing after 26s
ci / bun-nix (pull_request) Successful in 46s
ci / web (pull_request) Successful in 1m19s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m35s
ci / rust-arm64 (pull_request) Successful in 2m10s
android / android (pull_request) Successful in 4m22s
The last coverage gap, and only worth building once S1 proved it possible: the
Windows backend drives NV_ENC_DEVICE_TYPE_DIRECTX, and an in-place splitEncodeMode
change had never been tested there. It works (
|
||
|
|
8c994965d4 |
docs(licensing): say what the licence gate cannot see
The plan's M10 checklist named "the about.toml carve-out that puts FFmpeg outside the automated licence gate". There is no such stanza — I looked, on this branch and on origin/main. The carve-out is structural, which is worse: cargo-about walks the CARGO graph, so a native library reached through a permissively-licensed -sys crate is invisible to it. ffmpeg-sys-next is WTFPL and passes the gate cleanly while the LGPL libavcodec it link-imports is never harvested at all. So about.toml's own claim to be "exactly the regression guard we want against a copyleft dependency silently entering the linked set" was overstated: it did not catch FFmpeg entering and would not catch the next one. The comment now says so, and says where the LGPL obligations are actually discharged instead. The one genuinely good piece of news is recorded too: since M10 the client links no FFmpeg, so for every client artifact the crate graph and the linked set coincide and the gate finally means what it appears to mean. The gap is the host's alone. Gate: cargo about generate about.hbs --fail — passes. |
||
|
|
b27135308f |
fix(scripts): the xcframework never absorbs a Homebrew libopus
ci / docs-site (pull_request) Failing after 2s
ci / web (pull_request) Failing after 5s
android / android (pull_request) Failing after 14s
ci / bun-nix (pull_request) Successful in 22s
apple / swift (pull_request) Successful in 1m41s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m52s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 5m45s
ci / rust (pull_request) Failing after 7m5s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m22s
On a Mac with brew's opus installed, audiopus_sys found it via pkg-config and statically linked it into the aarch64 slice — a lib built for the RUNNING macOS (minos 26.0, tripping the script's own version guard) and existing only for the host arch, so the x86_64 slice silently fell back to the vendored build and the two slices shipped different libopus builds. Force the vendored CMake build for every slice (OPUS_NO_PKG_CONFIG=1), with the CMake policy floor modern CMake (>=4) needs to accept libopus's old cmake_minimum_required. |
||
|
|
64c92da356 |
fix(client/apple): the jitter ring deepens on the sessions that actually starve
The shared JitterPolicy grew an adaptive target floor — clustered genuine underruns raise the live target a step at a time up to max_target_ms, a long quiet spell relaxes it back — and the three Rust rings all run it via note_read. The Apple ring is the one hand-written mirror, and it mirrored the shed half but not the growth half: its target was pinned at the 20 ms base forever. On Wi-Fi that bunches arrivals (power-save is the classic; the field MacBook report is the symptom), 20 ms is regularly shorter than one delivery stall, so the ring re-primed through every stall for the whole session — crackle that never got better, on exactly the client where a Moonlight with a deeper buffer sounds fine on the same host and network. The ring now carries the full mirror of note_read: 3 underruns inside a 5 s window grow the target 10 ms (capped at COREAUDIO's 70), 30 s of quiet gives a step back, and the write-side hard trim follows the grown target (including the Rust policy's target+quantum guard, which the mirror also lacked). New tests pin the mirror to the Rust suite's expectations — growth, relax, the cap — plus the field scenario end to end: bunched 60 ms deliveries with every fourth burst 30 ms late converge to a silence-free tail instead of crackling forever. |
||
|
|
81d257c7fa |
fix(client/audio): the in-core decoder conceals lost packets like every other client
A field report: game audio on a MacBook (M1) crackles over Wi-Fi against a host that plays clean to other clients. The Apple client is the one client whose Opus decode lives in core (punktfunk_connection_next_audio_pcm — AudioToolbox has no multistream path), and that decoder only ever decoded packets that ARRIVED. The Linux, Windows and Android decode loops all feed an AudioGapTracker and synthesize libopus packet-loss concealment for every packet the wire lost; the in-core path had the tracker sitting unused in the same crate. So on Apple every lost 5 ms datagram — at ~200 packets/s over Wi-Fi, a steady trickle — landed in the playout ring as a hard time-domain gap: a click per loss, sustained crackle under real loss. The redundant-plane recovery (0xD2) hides single losses when the host grants it, which is exactly why the survivors are the burstier gaps that need concealing most. The decode now runs through the same accounting as everyone else: concealed frames land in front of the arriving frame in one contiguous buffer (the embedder just writes it to its ring), a DTX marker advances the accounting without being decoded, and the output buffer is pre-sized for a full concealment run so the borrow-until-next-call pointer can never dangle. Unit-tested against real libopus: gaps, duplicates, DTX-after-loss, and the 50 ms cap. |
||
|
|
071358cbf7 |
test(pf-encode): S1 on WINDOWS/D3D11 — passes; Windows arbitration is buildable
Everything the split-encode programme rests on had been proven only on
Linux/CUDA. The Windows backend drives NV_ENC_DEVICE_TYPE_DIRECTX, so none of it
transferred by assumption -- and if the driver refused an in-place split change
there, Windows arbitration would simply not be buildable.
RESULT on the RTX Windows box (RTX 4090 / AD102, driver 610.88, D3D11):
engines=2, latched by query_caps (WP1.1's probe, validated on Windows
hardware rather than inferred from Linux)
DISABLE -> TWO_FORCED via nvEncReconfigureEncoder, resetEncoder=0: ACCEPTED,
ZERO IDRs, and the reverse likewise.
So the foundation now holds across three platform x arch x driver combinations:
Linux/CUDA Blackwell 610.57.04, Linux/CUDA Ada 610.43.03, Windows/D3D11 Ada
610.88.
⭐ UNBLOCKS ALL FUTURE WINDOWS ON-HARDWARE TESTING. pf-encode's nvenc test
binaries were believed unlinkable on Windows ("NvEncodeAPICreateInstance
unresolved", recorded as pre-existing and worked around by only ever running
clippy there). They link fine given the SDK import library:
RUSTFLAGS='-L native=C:\Users\Public\nvenc -l nvencodeapi'
`-L` alone is not enough -- without a `-l` nothing pulls the archive in, which is
why the earlier attempt still failed. ⚠ This is TEST-BINARY-LOCAL and must stay
that way: production deliberately dlopens NVENC rather than link-loading it, and
an unconditional link-load is the known crash class on non-NVIDIA Windows hosts.
⚠ Box note: the RTX Windows box answers on .158, not the .173 in its memory
entry, and `Administrator@` there resets the connection right after
SSH2_MSG_SERVICE_ACCEPT in a way that reads like the host being down -- the
working login is "Enrico Bühler"@192.168.1.158.
|
||
|
|
5a1ec6198e |
docs(ci): the Windows host's FFmpeg tree is no longer shared with the client
windows-host.yml called FFMPEG_DIR "the same BtbN lgpl-shared x64 tree the Windows CLIENT links against". Since M10 the client links no libav* at all and windows.yml sets no FFMPEG_DIR, so the sentence pointed a reader at a link that does not exist. The provisioning script still fetches the tree — for the host alone — which is the part worth saying out loud, because the next person to read it will wonder why a client-provisioning step still mentions FFmpeg. |
||
|
|
0430d907bb |
fix(pf-encode): gate forced_split_width to Linux — WP4 broke the Windows build
The verification gap flagged in
|
||
|
|
5c05246098 |
feat: M10 — FFmpeg is gone from the client
cargo tree -p punktfunk-client-session finds no ffmpeg. The host still does, which is the whole point: pf-encode keeps libavcodec unconditionally and no host workflow, packaging script or licence file was touched. Deleted: crates/pf-ffvk, video_vulkan.rs, video_vaapi.rs, video_libav.rs, the libavcodec half of video_d3d11.rs, the av_log machinery, ffmpeg::codec::Id as the decoder's vocabulary (the quic CODEC_* wire constants now serve, which is why the evidence table was keyed on them), DecodedImage::VkFrame and ::Dmabuf, the presenter's AVVkFrame lane, and the ffmpeg-fallback feature with everything behind it. DrmFrameGuard collapses from an enum to a newtype, which removes an unsafe impl Send. Roughly 25,000 lines. Then the CI, packaging, licensing and docs work the plan's §6 lists: the Windows workflows lose FFMPEG_DIR, PF_FFVK_VULKAN_INCLUDE and their PATH prepend; the MSIX loses its DLL wildcard; the client .deb stops emitting libav sonames on its own because depends come from dpkg-shlibdeps; arch, flatpak and nix drop the dependency; and the README's "FFmpeg 7 or 8" contract narrows to the host. Three defects reached users' machines in the first cut, and none was in the deletion itself. All three desktop Settings UIs offer vulkan, vaapi and d3d11va as stored decoder values, so those strings sit in shipped settings files today. Refusing them by name — which is the correct rule for a stale pin — would have bricked every upgraded client whose owner ever touched that dropdown. They now migrate onto the native rung for the same hardware family, at decoder construction AND at each dialog's lookup, because a legacy value that matches no preset displays as "Automatic" and silently rewrites the user's preference on the next save. M9's evidence filter was deleted on the argument that with no libavcodec twin below, barring an unproven rung removes hardware decode rather than moving down one rung. That is true on Windows and false on Linux for Intel and every unknown vendor id, where prefer_vulkan_first is false and the order is native-vaapi → native-vk: a rung that has decoded nothing anywhere sitting above one that is 250/250 on three drivers. Every Intel Linux desktop would have moved from libavcodec VAAPI, shipping for years, onto pf-vaadec by default — and a rung that constructs and then produces wrong pixels leaves only by the error-streak demotion, which this codebase already documents as not tripping on the B580's strobing. The filter is restored as a narrow, pure, testable rule: an unproven rung yields to a proven one, and to nothing else. Windows deliberately passes no rung below, because that vendor family is the one with a measured wrong-pixel report against Vulkan decode, and trading no evidence for evidence of corruption is the wrong direction. And the notices still said FFmpeg was bundled. The root file is what both desktop clients include_str! and what the MSIX ships, three lines under the new card saying no FFmpeg is bundled; Apple's Acknowledgements said it too, on iOS, tvOS and macOS. The generator now emits four per-client files scoped by transitive closure — 0 FFmpeg mentions in each, verified — while the root file keeps it for the host. That also ends the standing false attribution of ffmpeg-next, GTK4, windows-rs and the NVENC SDK to an iPhone. Windows has no reachable box, so it was compiled instead: a cross clippy at -D warnings on x86_64 and aarch64-pc-windows-msvc with the C toolchain stubbed so build scripts run without linking. That gate immediately caught an include_str! path one directory too deep, which nothing else could have. Gates: container clippy -D warnings, 160 tests, workspace check, both Windows targets clean, client ffmpeg count 0 and host 2. The four decode crates are untouched, so the hardware rungs' 250/250 stands. ⚠ Owed and unrun: no GPU has executed any of this milestone. M8's on-glass software check, M7's D3D11 and VAAPI AV1 hardware legs, and M9's field bake all still want hardware, and the bake window and criteria remain the user's. |