48339326ae4e5e12ba3444bec61fe56698203ff1
2623
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
48339326ae |
fix(client/apple): the entrance keeps every card inside its own footprint
apple / swift (pull_request) Successful in 1m25s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m7s
ci / rust-arm64 (pull_request) Successful in 1m29s
ci / rust (pull_request) Canceled after 3m34s
ci / docs-site (pull_request) Canceled after 41s
ci / bun-nix (pull_request) Canceled after 29s
The focused card jumping into its correct state at the end of the entrance was the entrance's own geometry. The caller's .scrollTransition reads the geometry of the view underneath the entrance's transforms, so a card shoved 58 pt down and hinged on its leading edge spent the whole travel reported as far from centre - phase pinned at fully receded - and only collapsed to identity as the card came home. That collapse IS the jump, and it explains why it looked timing-dependent rather than simply broken. Now the rotation is about the card's centre (it turns in place instead of swinging sideways out of position) and the rise is 16 pt, inside the strip's own vertical slack, so nothing the entrance does moves a card away from where the scroll view thinks it is. The entrance also waits a couple of frames for real layout - the GeometryReader's first pass can report no width, so there is nothing to centre on yet - and the transaction override from the previous round is gone: it was not the cause, and nil-ing inherited animation could have made navigation snappier than intended. |
||
|
|
eaa87ca0cc |
fix(client/apple): the entrance stops dragging the scroll transition along with it
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m59s
ci / docs-site (pull_request) Successful in 2m6s
ci / rust (pull_request) Successful in 5m50s
The strip entrance is one animated progress value now, not a Bool behind per-card .animation modifiers. Those modifiers wrap the caller's card - INCLUDING its .scrollTransition - so a delayed spring flipping while the scroll view was still settling captured the transition's own per-frame phase updates and stranded the centred card half-receded until the next scroll re-drove it. That was the 'only navigating fixes it' report, and the race with load speed was the same thing. CardEntrance is now a ViewModifier + Animatable: it slices its own window out of one master clock the carousel animates 0 -> 1, so every transform is a pure function of an interpolated Double and no animation modifier wraps a card at all. Benign failure mode too - progress reaching 1 without animating leaves each card at exact identity rather than stranded. The entrance also moved inside .frame(width:) so a scroll target's geometry never depends on what its card is doing, and the non-tvOS branch states its .id explicitly. One leak remained after that: withAnimation sets its animation on the whole TRANSACTION, so the scroll view's initial centring still inherited the 1 s linear clock and the focused card only reached its correct look as that clock ran out - arriving as a jump. The card subtree now clears the inherited animation, so its phase lands per frame while the entrance's own transforms (driven by animatableData, not by the transaction) keep running. |
||
|
|
769a8c049d |
fix(client/apple): the entrance waits for the art, and every card swings
ci / bun-nix (pull_request) Successful in 29s
ci / web (pull_request) Successful in 59s
ci / docs-site (pull_request) Successful in 1m4s
ci / rust-arm64 (pull_request) Successful in 1m25s
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Successful in 4m21s
Three defects behind an entrance that read as a card sliding up: - The centred card never rotated. The stagger fans out from an anchor, and the anchor was given side 0 = no rotation - but the anchor IS the card the eye is on, so the single most visible card only rose. Side is never 0 now; every card turns. - The swing happened while the card was invisible. Opacity shared the transform's spring, so the card spent its whole rotation at near-zero alpha and only the last few degrees showed. The fade now runs on its own 0.22 s curve (a second .animation governs only the modifiers above it) while the transform springs over ~0.6 s. The travel is deeper too - 0.74 scale, 64 degrees, 58 pt - and the rotation sign now matches the coverflow's own recede, so a card unwinds INTO its resting angle instead of swinging against it. - It fired before the art existed. Cards swung in as grey placeholders and filled with artwork afterwards. PosterImage reports when a cover settles (art loaded, or candidates exhausted), the coverflow counts the first few, and GamepadCarousel holds its entrance on a contentReady gate - with a 700 ms backstop so a slow or artless library still animates. |
||
|
|
2fb80073e6 |
fix(client/apple): the library actually plays its entrance, and it swings in 3D
ci / bun-nix (pull_request) Successful in 54s
ci / docs-site (pull_request) Successful in 1m12s
ci / rust-arm64 (pull_request) Successful in 1m28s
apple / swift (pull_request) Successful in 1m28s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m56s
ci / rust (pull_request) Successful in 4m21s
The strip entrance never ran in the library, for two reasons: - The trigger was lost. Flipping the state inside onAppear puts the change in the SAME transaction as the view's insertion, where SwiftUI runs with animations disabled. The launcher got away with it; the library's strip mounts late - only once the fetch lands - and lost every time. The flip now defers one runloop turn, so it is an ordinary animated state change. - The art snapped in behind it. Covers hard-swapped from grey placeholder to image, so even a working entrance was followed by a run of cards popping to artwork after the strip had settled. PosterImage cross-fades now (the touch grid inherits it). And the entrance is 3D: a card starts turned away on the drum, small, low and invisible, then swings flat, grows and rises on an overshooting spring. Cards left of the anchor hinge on their trailing edge and cards right of it on their leading one, so the strip FANS OPEN from the cursor instead of sweeping past it - the same hinge-and-perspective language the coverflow's own recede speaks, so arriving and scrolling read as one object. Reduce Motion still drops every bit of travel. |
||
|
|
cf68d33e2c |
feat(client/apple): the strips assemble themselves, and the store chip stops flashing
ci / bun-nix (pull_request) Successful in 38s
ci / web (pull_request) Successful in 1m7s
ci / rust-arm64 (pull_request) Successful in 1m23s
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m48s
ci / rust (pull_request) Canceled after 6m16s
Two more from the on-glass pass: - The coverflow's store/source chip only showed its background on the centred cover. Same mechanism as the tray blur: a card rides a scrollTransition that composites it with opacity < 1 and a 3D rotation, and a material cannot sample a backdrop through an offscreen composite - so the frost stayed blank everywhere except the one card sitting at exactly full opacity. The coverflow's chip is a flat wash now (StoreBadge gains `solid`), which has no backdrop to sample and is therefore simply always there. The touch grid keeps its material - its cards carry no transform, so its frost samples fine. - Host cards and library covers now arrive with the strip instead of being there: each card rises out of a fade on a lightly overshooting spring, delayed by its distance from the cursor, so the strip assembles outward from where the eye already is. Implemented once in GamepadCarousel, so the launcher and the coverflow inherit it together. Transforms only - snapping, the callers' own scrollTransition and the tvOS focus engine are untouched - and Reduce Motion drops the travel for a plain unstaggered cross-fade. |
||
|
|
70de58f446 |
fix(client/apple): the tray blur goes full-bleed by layout, and its frost sinks to black
ci / bun-nix (pull_request) Successful in 30s
ci / web (pull_request) Successful in 1m6s
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m23s
ci / rust-arm64 (pull_request) Successful in 1m53s
ci / rust (pull_request) Successful in 4m54s
Round-3 field findings: the pop-in had retreated to the X axis alone - the one growth still left to safe-area resolution (the landscape side insets), which settles a beat after insertion, outside any geometry group and outside the view's own transaction. The scrim now reaches full-bleed purely by layout: a fixed 80 pt negative-padding overhang on the outer edge and both sides replaces ignoresSafeArea entirely, so every axis is deterministic from the first frame (and orientation no longer changes the gradient's proportions). The mask's strong region moves to 0.65 to account for the overhang leading the gradient. And the frost reads black now, not grey: an ink.shade(0.35) wash inside the mask sinks the material's luminance lift toward the palette's shade - black on a dark field, palette-honest on a pale one. |
||
|
|
f674a06a08 |
fix(client/apple): the tray blur keeps its shape, and the tab pill turns to glass
ci / bun-nix (pull_request) Successful in 27s
ci / docs-site (pull_request) Successful in 1m4s
ci / web (pull_request) Successful in 1m9s
ci / rust-arm64 (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Canceled after 4m39s
Two follow-ups from the second on-glass pass: - The tray blur's pop-in survived the geometryGroup: the full-bleed growth (negative padding + safe-area expansion) rode the push's transaction, and safe-area resolution sits outside a geometry group. The scrim now pins its own geometry out of any animation - the layer fade/slide still carries it, only its SHAPE can never animate. The bottom overshoot grows 32 -> 72 pt (the tray sits over scrolling rows plus the detail line; the blur influence starts well above the legend now) and the mask holds strength longer before dissolving. - The selected tab pill is a Liquid Glass surface (accent-tinted through consoleGlass, material fallback pre-26/tvOS) - the strip wears the same material language as the rows below it. The matched-geometry travel between pills is unchanged. |
||
|
|
49fa344c92 |
fix(client/apple): the gamepad UI answers its first field test
ci / rust-arm64 (pull_request) Successful in 1m29s
ci / bun-nix (pull_request) Successful in 19s
apple / swift (pull_request) Successful in 1m22s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m48s
ci / docs-site (pull_request) Successful in 1m53s
ci / rust (pull_request) Successful in 4m25s
Six findings from the on-device pass over #91, all iOS-facing: - The tray blurs no longer grow into place on a push: the screen layer resolves its internal layout (safe-area trays, the scrims' full-bleed) in a geometryGroup BEFORE the insertion animates. - The option band is LINEAR now, not a ring. A ring showed the first option waiting to the right of the last one - unreachable, since left/right clamps - and on a 2-option ring the unselected item flipped sides with every step (the 60/120 Hz row). Positions are fixed, the ends are the ends, and A's wrap travels back across the list. Options other than the facing one exist only while the drum is moving, so a long label never sits under a resting neighbour as overlapping text. - Toggles (and the pin rows) ride the band too: Off left of On, matching the left-off/right-on step semantics. - The close X is gone from settings, add-host and the library - a gamepad UI exits with B. A chromeless cancel button keeps hardware Esc and the macOS sheet working, and the library's loading/error/empty states gain a zero-size B listener so a controller-only user is never trapped where the coverflow (and its B) doesn't exist yet. - The heading is a real heading: leading-aligned with the 24 pt content inset, 24/34 pt (was 20/30), top margin 18/28 (was 10/18) - launcher, settings, add-host and library alike. The launcher's hidden-mirror chip trick died with the centred title that needed it. Verified: swift build (macOS), swift build --triple arm64-apple-ios17.0, swift test 208 passed / 0 failed. |
||
|
|
6c4df043fe |
Merge pull request 'Gamepad UI (iOS): palette-washed glass, room to breathe, in-place screens, and a real option drum' (#91) from worktree-gamepad-ios-polish into main
ci / bun-nix (push) Successful in 31s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 16s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 16s
ci / web (push) Successful in 1m12s
ci / docs-site (push) Successful in 1m20s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 14s
apple / swift (push) Successful in 1m32s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 15s
ci / rust-arm64 (push) Successful in 1m39s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 17s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 33s
docker / builders-arm64cross (push) Successful in 14s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m19s
apple / screenshots (push) Successful in 5m42s
docker / deploy-docs (push) Successful in 6m35s
ci / rust (push) Failing after 12m11s
release / apple (push) Successful in 9m15s
Reviewed-on: #91 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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).
|
||
|
|
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 |
||
|
|
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. |
||
|
|
01294e3a53 |
refactor(pf-encode): WP4 — one split policy, shared with the libav path
The libav NVENC path carried its own inline copy of the split decision and had
already drifted from the direct-SDK selector: it hard-coded a 2-way split
regardless of engine count, and had no depth rule at all. That is the drift the
shared resolver was extracted to prevent, and the copy quietly reintroduced it.
Routing it through `resolve_split_mode` needed the policy to MOVE. `nvenc_core`
is gated on `feature = "nvenc"`, but the libav path is precisely the build where
that feature is OFF (`PUNKTFUNK_NVENC_DIRECT=0`, and the featureless packages --
the packaging gap this project has been bitten by before). So
resolve_split_mode / max_forced_split_mode / clamp_to_engines, plus a new
`forced_split_width`, now live in `codec.rs`, which is always compiled and
already owned SPLIT_FORCE_PIXEL_RATE.
That means the NV_ENC_SPLIT_ENCODE_MODE values had to be hand-written as plain
constants, since the SDK enum does not exist without the feature. They are
therefore pinned: `nvenc_split_constants_match_the_sdk` (feature-gated, the only
place both are visible at once) asserts all five against the real enum, so the
copies cannot rot.
⚠ Only the FORCED outcomes are actionable on the libav side -- libavcodec's
`split_encode_mode` AVOption is its own vocabulary and our DISABLE is the NVENC
enum's 15, which would be meaningless there. DISABLE/AUTO both map to "leave the
option unset", which is exactly today's behaviour (unset = the driver's auto).
`engines = 0` ("not probed") maps to 2-way, preserving what that site always did;
a 3-NVENC part gets the wider split only on the direct-SDK path, which is the one
that actually probes.
⚠⚠ VERIFICATION GAP: .133 went down mid-change (no ping), so the WINDOWS leg is
UNVERIFIED. This matters more than usual -- the Windows backend imported
resolve_split_mode from nvenc_core and that import had to move too, which a grep
caught rather than a compiler. Re-run before trusting it:
cargo clippy -p pf-encode --features nvenc --all-targets -- -D warnings
Verified .21: clippy -D warnings clean BOTH with and without the nvenc feature
(the featureless build is the whole point of the move) and with
nvenc,vulkan-encode; 65 unit tests incl. the new constant-parity test; 25/25
NVENC on-hardware; punktfunk-host clippy clean. fmt clean.
|
||
|
|
38554c1c6e |
feat(client): M9's code half — native first, FFmpeg behind an off-by-default feature
`ffmpeg-fallback` on pf-client-core, default off on the crate. With it off the libavcodec rungs are not compiled, pf-ffvk leaves the dependency graph, and no ladder or demotion arm names them; with it on each sits exactly where it sits today, directly below its native twin. That is the switch which makes M10 a deletion rather than a redesign. The bake window and the regression criteria are the user's, per the plan, and nothing here claims the M9 gate is met. The hard part was not the feature, it was honesty. Two of the four native rungs have never decoded a frame on any hardware — native VAAPI at all, and native D3D11VA's AV1 leg — and making those the default would assert evidence that does not exist. So admission is per rung and per codec: a pair with hardware evidence joins `auto` always; a pair without it joins only when nothing proven is left below it (a build with no FFmpeg twin, where the alternative is not a proven rung but the CPU) or when the user asks with PUNKTFUNK_NATIVE_FIRST=1. Pins bypass it, so a lab run can still reach any rung. The shipping default therefore changes in exactly three ways, all evidence-backed: AV1 `auto` takes native Vulkan (250/250 bit-identical on an RTX 5070 Ti), Windows H.264/H.265 `auto` takes native D3D11VA above its FFmpeg twin (parity on two GPUs plus a 30-minute soak), and a failing Vulkan rung on Windows demotes to native D3D11VA first. Everything unproven is byte-for-byte as it was. The evidence state is written where it cannot rot: a table in video.rs's module docs, the same facts in code as `native_evidence()`, a test asserting them in both feature states, and a per-session log line carrying the rung, the codec, whether hardware has verified that pair and the evidence string — at WARN when it has not. A support engineer reading a log can now tell proven from assumed without asking anyone. Termination needed a new guarantee. With the FFmpeg twins gone, two native rungs in opposite per-vendor orders could hand a session back and forth forever, so a rung once entered is never re-entered and the walk is monotone to software. The never-delivered fall-through still works: with the feature on it is unchanged, and with it off it is redundant, because the next candidate already IS the rung below. ⚠ ffmpeg-next remains a hard dependency of pf-client-core, deliberately. What is left off-feature is three type-level residues — the codec-id vocabulary, the AVVkFrame guard that is pf-presenter's public import, and a pixel-format in one signature — every one of them an M10 §6 line item. Deleting them here would mean deleting the presenter's FFmpeg lane, 55 call sites, in a milestone whose gates cannot run a GPU. No libavcodec decoder is opened in a default build. ⚠ video_d3d11.rs was gated item by item rather than wholesale, and nothing in this tree compiles it — it needs a Windows check before anyone trusts it. Gates: both feature states, container clippy -D warnings and 158/159 tests, workspace check. The four decode crates are untouched, so the hardware rungs' 250/250 stands. |
||
|
|
d5e23146c0 |
feat(client): M8 — the software rung is openh264 and rav1d, and swscale is gone
The ladder's last rung no longer runs FFmpeg. H.264 decodes through openh264, AV1 through rav1d, and HEVC is refused outright: no permissively licensed software HEVC decoder exists, so an HEVC session that exhausts its hardware rungs now tears down and re-dials advertising HEVC-less caps, and the host picks H.264. The plan calls that a first-class path; it is one. swscale is deleted, and with it the BT.601 default that its correction code existed to undo. Colour on the H.264 lane now comes from the same pf-bitstream planner every hardware rung submits from — openh264 reports no VUI at all — and AV1's comes per-picture from the sequence header. One colour source, one CSC: the old default is unrepresentable rather than merely fixed. Frames reach the presenter as three tightly-packed planes through the planar CSC pass, which had to be un-gated from the pyrowave feature and its device probe, since the last rung must exist on devices that failed that probe. rav1d rather than the dav1d crate, deliberately and against the plan's literal wording: dav1d-sys is system-deps-only, so it would add a system library and a .pc file to every client package — in the milestone family whose excision checklist exists to delete exactly that. rav1d is the same decoder, same licence, statically linked. The cost is honest: no-asm builds on both decoders, and software throughput is still unmeasured. The colour test is the milestone's exit criterion, so it is built to fail. Three fixtures, and a mutation check: hardcoding the swscale default turns the red bar to [255,24,0], and swapping Cb/Cr turns red to blue — a silent error no metadata assertion could catch. Review then disproved the range half of it numerically: with eight saturated bars, decoding the full-range fixture with the wrong range gives max error ZERO, because a mismatch only pushes values outside [0,1] where the shader clamps. A mid-tone was added; the wrong range now costs 11, well past the tolerance. The exit criterion I set was satisfiable by a test that proved nothing. Two blocking defects, both emergent rather than local. Software AV1 on a 10-bit stream never reached its typed refusal: rav1d is built 8-bit-only and returns ENOPROTOOPT, which the send loop turned into a generic error, so the pump's typed downcast missed and every AU failed identically — a permanent freeze on precisely the shipping case, since AV1 is advertised only where hardware AV1 exists and hardware AV1 plus HDR is Main 10. The shape is now read from the sequence header before any byte reaches the decoder, exactly as the H.264 leg reads the active SPS. And the new Reconnecting phase was the first state that is not streaming, not connecting, and still holding a live stream — which opened all three guards that had made a second launch impossible. Pressing A assigned over `stream` where every other site shuts down first, and StreamState has no Drop, so the old pump was detached: a second live session still submitting to a Vulkan device that gets destroyed underneath it. Nothing about the reconnect was wrong in isolation; the defect lived between a new state and three guards nobody re-examined. Start is now defensive and the retry raises the connecting modal, so the UI matches the state and B can cancel. Also closed: retry_caps was computed, tested and never applied, so a shape refusal could end a session reporting no codec available while a working retry existed; the retry inherited force_software sticky-true, landing an HEVC→H.264 fallback on software H.264 with working hardware H.264; it re-dialled with a stale mode; the CPU present arm had no survivable-failure handling where the pyrowave arm — same pass — has it; HEVC is no longer advertised when the decoder is pinned to software; and the software rung now feeds the recovery-point SEI it already had in hand to the re-anchor gate. ⚠ Two host-side gaps found while tracing, neither in scope here: Hello::launch is NOT idempotent (gog:/custom: targets spawn a second copy on a retry; the field is kept verbatim because dropping it orphans the gamescope display whose reuse key includes the command), and a reconnected session can never adopt a game predating its own launch stamp, so it has no game-exit detection. ⚠ OWED: the on-glass software run. ~200 lines of new Vulkan on a path that only runs because the GPU already failed, and no driver has seen it. The review's minimum check is sync validation enabled, a non-multiple-of-16 mode, a mid-session resize and demotion, and both colour matrices. Gates: container clippy -D warnings over four crates, 236 tests, workspace check. pf-vkdecode and pf-bitstream are byte-for-byte untouched, so the hardware rungs' 250/250 stands. |
||
|
|
a20cd44ed4 |
feat(client): native VAAPI AV1 — the third rung, and two failure-path defects
The libva AV1 layouts, the AuPlan conversion and the Linux rung's AV1 arm, completing AV1 across all three hardware backends. Pin-only. Layouts measured, not transcribed: the committed probe grew the AV1 structures and every size and offset it printed against libva 2.23.0 is a compile-time assertion. Three that a hand-count gets wrong — the picture buffer is align 8 because anchor_frames_list is a pointer, inserting seven bytes of padding; seg_info and film_grain_info carry their own padding tails inside the parent; and THREE of AV1's six bit-field unions are narrower than a word (one uint8_t, two uint16_t), so a u32 packer over any of them writes through its neighbour. This is the fifth way this program has had to spell "which pictures does this frame use", and it is unlike the other four: ref_frame_map is indexed by SLOT and holds actual VASurfaceIDs rather than indices into anything, ref_frame_idx is indexed by NAME and holds slots taken from the header — not from the plan's refs, where a lost reference leaves a hole and a hole is not a slot — global motion is picture-level, and there is no per-reference size field at all. Established from va_dec_av1.h and libavcodec's vaapi_av1.c, and stated in the module docs so the next reader does not re-derive it. Review verified the whole happy path — every layout assertion re-measured, every packer width and bit position, the reference convention, the num_elements buffer shape — and found both defects on FAILURE paths, neither reachable on the vendored vector. A conversion refusal permanently desynced the ledger. The mutation block sat after the tile walk, so any tile-shape refusal left the planner holding a picture with no ledger slot — and the resulting UnresolvedReference fires before that block too, so it never repaired. Every later access unit hard-errored until a shown key frame: one lost packet costing a GOP. The arm's own doc already warned that skipping conversion would desynchronise the slot map; the refusal door did exactly what the skip door was written to avoid. The block is hoisted, and a tile-shape refusal on an already-damaged plan is now concealed rather than refused. Fixing that exposed a sharper edge: the conversion can release a slot and reassign it to the refused picture in one call, so the binding would still hold the PREVIOUS picture's surface — a wrong reference rather than a missing one, which nothing downstream could notice. The caller now clears the binding unconditionally on the refusal path. And a damaged frame's surface was never written yet was bound as a reference and left in pending, so a later clean show_existing_frame would claim it with damaged = false and ship uninitialised GPU memory to the presenter — on several drivers another client's framebuffer. The justification quoted half of va_dec_av1.h; its next sentence gives the remedy, which is to point the problematic index at an alternative buffer. Damaged frames now submit as they do on the other two arms, with live surfaces substituted for invalid entries and reported as a bitmask — preferring a reference that really decoded over the decode target, and keeping libavcodec's deliberate all-invalid map on a shown key frame. Film grain is refused rather than decoded wrong: libva wants two surfaces, one ungrained for prediction and one grained for output, and libavcodec allocates a second frame for exactly that. The gate now sits after the mutation block so a grained frame costs itself rather than the GOP, and stays per-AU rather than per-sequence because a stream that merely DECLARES the tool decodes here perfectly. ⚠ Residual, flagged not fixed: a picture decoded from substituted references can still be shown by a later show_existing_frame. It is decoded memory now rather than uninitialised, and it is what the H.264/H.265 arms do, but tracking "this was concealed" through to display needs new session state. Gates: macOS fmt/clippy/125 tests/cargo-doc, container clippy -D warnings over seven crates and 548 tests, workspace check. pf-bitstream's diff is comment-only — verified — so the Vulkan rung's 250/250 stands untouched. Nothing here has decoded a frame: no VAAPI hardware is reachable. |
||
|
|
ef40890c80 |
feat(client): native D3D11VA AV1 — wired, and four defects it exposed
The AV1 arm of the native D3D11VA rung, parity-required because today's FFmpeg d3d11va rung already decodes AV1 Profile 0 and the excision must not silently drop it. Pin-only, as that rung is today. decode() walks the temporal unit frame by frame; submit() splits into decode_into and present, because AV1 decodes frames that are never shown. The proven H.264/H.265 body is byte-for-byte unchanged — review diffed it against HEAD mechanically and found only a rename plus one refusal arm — and the VideoProcessorBlt hand-off is untouched. That mattered more than anything else here: those two codecs are hardware-proven, .173 is powered off, and no gate that runs could have caught a regression in them. Every descriptor value comes from libavcodec's dxva2_av1.c read verbatim, not from symmetry with the other codecs: three buffers and no qmatrix (AV1 transmits none), NumMBsInBuffer zero on all three, ConfigBitstreamRaw 1, surface alignment 128, pool +8, and the session sized from the SEQUENCE header's max frame size — sizing from the frame would rebuild the decoder and drop every reference the first time a stream legally resized downward. Two places where following the H.264/HEVC pattern would have been wrong. libav pads the bitstream buffer and grows only its descriptor's DataSize, never a tile's, because a tile's size is exact — charging padding to the last record is corruption, not filler. And the committed tile records were one per tile GROUP spanning the whole OBU, header and frame header included, where libav emits one per TILE addressing the payload past its tile_size_minus_1; the vendored vector is single-tile, so the old tests passed either way. Review then found four more defects in the already-committed conversion, each confirmed against libavcodec AND Chromium's D3D11 AV1 accelerator: Tile widths and heights were the coded minus-1 where the field is a superblock COUNT — every tile declared one superblock short, on every frame, with a comment asserting the opposite of the truth. StatusReportFeedbackNumber must be zero for AV1. Both reference implementations disable it specifically for this codec — libav's note reads "breaks decoding on some drivers (tested on NVIDIA 457.09)", Chromium's "it crashes :|" — while both set it for H.264 and HEVC, which is why this rung's proven codecs never showed it. It would likely have presented as a hang or a rejected submission rather than bad pixels, sending the next session after the tile records instead. frame_refs[].Index is an index INTO RefFrameMapTextureIndex, not a surface index; the neighbouring line already filled that map correctly. Measured: 1636 reference entries on the vendored vector where the two differ. qm_y/u/v need the 0xFF "no matrix" sentinel — 0 is a valid matrix index, and 274 of 274 frames transmit no quantiser matrix, so every one was being dequantized against matrix 0. Also closed: the slot leak the Vulkan rung had already found and documented (a frame refreshing no slot is never reported removed, so nine of them exhaust the ledger); a tile-grid check that could not fire, replaced with libav's own cols*rows guard; per-reference sizes now taken from the reference's own header via RefState rather than the current frame's; and the render size clamped against the decoded picture in both rungs, since AV1 permits a render size larger than the frame. The parity leg was rewired through the real decode path — it previously called the internals directly, so its hidden-frame assertion described the harness's own counter rather than production withholding anything. Gates: macOS fmt/clippy/383 tests, container clippy -D warnings over four crates and 499 tests, and on Windows .133 (.173 is powered off) clean checks plus 97 pf-dxvadec tests. All 8 Vulkan gpu_parity legs re-verified bit-exact on the RTX 5070 Ti after the shared-code change. No AV1 frame has been decoded through this rung anywhere: it needs .173 back. |
||
|
|
185332a866 |
fix(vkdecode): H.264 and H.265 session parameters own what the driver keeps
The same use-after-free the AV1 rung was just fixed for, closed in the two rungs that ship. session.rs and session_h265.rs handed their Std parameter sets to vkCreateVideoSessionParametersKHR and dropped the backings when the call returned; NVIDIA 610.57.04 was measured retaining such a pointer to decode-record time, which is what made AV1 diverge on 250 of 250 frames. Nothing was known to be broken here — both rungs are bit-exact on four drivers — but that was luck rather than correctness: the freed blocks happen to still hold the right bytes in that window. The native Vulkan rung sits in the auto ladder above FFmpeg-Vulkan on shipping clients, so this was live code, and its failure mode is silent wrong pixels rather than a crash. StoredParams and StoredParamsH265 hold the parameters object together with every wrapper it points at, so an object whose backing is gone cannot be built. create_parameters_object takes the wrappers by value; the Add arm adopts them only after a successful update, so a failed update drops what it never stored; the Recreate arm replaces, destroys the old object, then drops its backings, written explicitly so the ordering survives later edits. The Add-vs-Recreate decision table and the VPS ledger are untouched — only ownership moved. params.rs still carried the refuted claim as a type-level contract, that Vulkan "copies all parameter data before returning" and keeping the wrapper alive across the call "is the whole obligation". Corrected to the measured truth. The tests are what stop this returning, and each was verified by sabotage: inlining the H.264 PPS box fails at pps pScalingLists, inlining the H.265 SPS DPB box fails at sps pDecPicBufMgr, and making either adopt drop instead of store fails both session tests. Two lessons are recorded in them. Pointer equality cannot be the assertion, because the Std struct carries pointers by value and a stale one compares equal — the read-back is the discriminator, so the tests clobber the dead stack first to make a dangling read deterministic rather than lucky. And the first H.265 draft read six of eight pointers and let the sabotage through, so it now reads every one with a labelled assert. ⚠ One site of this class remains, deliberately: the VkVideoProfileInfoKHR chains, where wire()'s borrow dies with its enclosing block while the object created from it lives on — three session creates, an image, a buffer, and a query pool built from a raw pointer into a stack chain. It spans six modules and all three codecs, and a profile is enums a driver resolves at create time with no per-frame deref, so the risk is materially lower. It wants its own pass with its own hardware verification. Gates: macOS fmt/clippy/196 tests, container clippy -D warnings, pf-vkdecode 182/182 and pf-client-core 140/140. On the RTX 5070 Ti, all 8 gpu_parity legs re-verified green after the change — H.264, H.265, Main 10 and AV1 all still bit-identical to libavcodec. |
||
|
|
cdd1f3efce |
fix(vkdecode): AV1 is bit-exact — the bug was a use-after-free, not the driver
250/250 frames bit-identical to libavcodec on NVIDIA 610.57.04, and all four other parity legs (H.264, H.265, Main 10, both four-byte-prefix twins) still green. session_av1 built the sequence header, handed pStdSequenceHeader to vkCreateVideoSessionParametersKHR, and dropped the backing the instant the call returned — on the documented assumption that Vulkan copies parameter data before returning. NVIDIA does not. It keeps the pointer and dereferences pColorConfig when a decode is RECORDED. The freed block became our own next allocation, whose bytes read back as mono_chrome = 1, and a monochrome frame skips exactly loop_filter_level[2..3] (AV1 7.14). That is the whole fingerprint two earlier rounds chased: luma bit-exact, chroma off by small amounts, and rewriting the chroma levels in the bitstream changing nothing — the driver read them correctly and then discarded them, because it believed the stream had no chroma. StoredParamsAv1 now holds the parameters object and its Std backing in one value, so an object whose backing is gone is unrepresentable. The road there is worth recording, because two well-evidenced conclusions were wrong before this one was right. A software oracle reproduced the divergence exactly by disabling chroma deblocking, and a GPU probe showed chroma levels [8,12] and [63,63] producing byte-identical output — which looked conclusive and was not. libavcodec's own Vulkan AV1 hwaccel is bit-exact on this same driver, which proved the hardware fine and the defect ours. ffmpeg never hits it: with VK_KHR_video_maintenance2 it uses inline session parameters and never creates a parameters object at all. The proof is direct rather than inferred: a throwaway Vulkan capture layer dumped both submissions and every byte of our AV1 picture info already matched libavcodec's, including the loop filter block; only the session parameters layer differed. Watching the block's address showed correct bytes at create and our next allocation at decode. Ruled out on hardware, so nobody re-tests them: filmGrainSupport, maxCodedExtent, maxDpbSlots/maxActiveReferences, VkVideoDecodeUsageInfoKHR, the tile-start sentinel, the setup slot's SavedOrderHints, a NULL pTimingInfo, and heap luck. Two earlier fixes are confirmed against libavcodec's captured wire bytes and kept: CDEF secondary strengths carry the coded value rather than the spec's in-place fixup, and LoopRestorationSize is log2-based. The refuted driver-ignores-chroma-levels claim is corrected everywhere it was written down, and that probe test now passes and points at the lifetime of everything a submission points at before blaming a vendor. ⚠ Adjacent and NOT fixed: session.rs and session_h265.rs drop their Std backings the same way, and those sets carry embedded pointers too. Both are measured bit-exact on four drivers, so nothing is known to be wrong — but the contract now rests on a driver behaviour measured FALSE for AV1 on a shipping driver. The SAFETY comments asserting it have been corrected; the structure is deliberately untouched pending its own pass. Gates: macOS fmt/clippy/336 tests, container clippy -D warnings, all green; 8/8 gpu_parity and 3/3 gpu_smoke legs verified on the RTX 5070 Ti. |
||
|
|
1062aa780f |
test(pf-encode): measure the bits/frame curve — no crossover, split always wins
WP0's real deliverable, and the hole every previous measurement in this
programme had. All prior timings ran against driver-zeroed buffers, so rate
control had nothing to code (~300 B/AU against an 833 KB quota) and only the
PIXEL-proportional half of the encode cost was ever exercised -- while the 4K60
HDR field report was a BITS/FRAME problem at 6.8 Mbit/frame.
Adds `pf_zerocopy::cuda::write_plane_from_host`, the exact mirror of the existing
read_plane_to_host. No new loader entry was needed: cuMemcpy2DAsync_v2 was
already in the table and CUDA_MEMCPY2D just needed the reverse memory types.
Linux-only by construction (pf-zerocopy's `imp` is cfg'd to linux).
⚠ Two harness mistakes found and fixed by looking at bytes/AU rather than
trusting the knob:
- Pure per-pixel noise is INCOMPRESSIBLE, so a low bitrate target does not
produce low bits/frame -- it OVERSHOOTS. At a nominal 50 Mbps the encoder
emitted 719 KB/AU against a 104 KB quota, and the three lowest rows of the
first sweep all sat at the same ~5.7 Mbit/frame. Sweeping nominal bitrate
measures nothing.
- So the sweep moves CONTENT DETAIL (block size) instead, and the x-axis is the
bits/frame the encoder ACTUALLY produced, never the one requested.
4K60 HEVC 8-bit, real content, single-engine vs forced-2:
bits/frame Ada 4090 Blackwell 5070 Ti
0.2-0.3 Mb 4567 -> 2381 1.92x 5549 -> 3552 1.56x
~1.1-1.2 Mb 5060 -> 2626 1.93x 5867 -> 4082 1.44x
~3.3 Mb 8478 -> 4455 1.90x 9286 -> 5862 1.58x
~9.6 Mb 16237 -> 8114 2.00x 16435 -> 9275 1.77x
RESULTS. (1) Encode time scales strongly with bits/frame -- 4.6 ms to 16.2 ms
across the range on Ada -- confirming the hypothesis' core claim. (2) There is NO
CROSSOVER: split wins at every point on both architectures (Ada ~1.9-2.0x and
notably flat, Blackwell 1.44-1.77x). So the arbitration's encode-side answer is
essentially always "split", which makes the sub-frame handicap the only decision
that actually matters -- exactly the part already built and unit-pinned.
(3) It corroborates the field capture: at ~6.8 Mbit/frame these curves put
single-engine 4K60 around 10-13 ms, and the field report was 10.3 ms on a 4090.
That reads as real ASIC time, not the retrieve-queue inflation it might have been.
⚠ Caveat the data itself shows: cost is NOT monotonic in bits/frame alone. The
1px row lands at the HIGHEST bits/frame yet encodes FASTER than the 4px row on
both boxes (Ada 10148 vs 16237 us) -- pure noise defeats motion estimation, which
gives up early, where semi-structured content makes it search hard. Content
structure is a real term, so "bits/frame" is a good axis but not a complete cost
model.
Verified .21: clippy -D warnings clean (pf-encode + pf-zerocopy), 64 unit tests,
25/25 NVENC on-hardware. Curves run on both Ada and Blackwell. fmt clean.
|