a9e7c033c3b972ebcb48b85da354e45ccac3dfd6
761
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3a4c94ad79 |
fix(dxvadec): the review round — a vacuous predicate, an overstated claim, and the H.264 twin of this defect
Five findings from the adversarial pass, all real. **The deferral predicate was vacuous.** `plan.dpb.removed` is ALWAYS a subset of `plan.dpb_refs`: `Av1Planner::plan_frame` snapshots `dpb_refs` before any mutation and `refresh_slots` can only report a picture that was in `self.slots` at that moment. So `filter(|id| dpb_refs.contains(id))` was a condition that is never false, the eager-release loop beside it could never release anything, and the test assertion "only a picture the submission points at earns the reprieve" could never fire. Now: defer every removal, say why in terms of the planner, and assert the PLANNER's property (`removed ⊆ dpb_refs`) — which is falsifiable, and whose failure would mean the conversion is releasing a surface `ref_frame_map` points at. **The failure-path claim was overstated.** Holding the decode's `Result` closes this frame's leak, not the unit's: `decode_av1` returns on the first failing frame and abandons the rest of the temporal unit's plans, so their removals are never released. 24 of 250 units carry a second frame. Named rather than fixed — what to do with the frames after a failure is the pump's question. **⚠⚠ The H.264 leg plausibly has the same defect, and the comment this change added said it could not.** `pic.rs` builds `RefFrameList` from `plan.dpb_refs`, and `H264Planner` snapshots that in `begin_picture` — BEFORE 8.2.5 marking and the DPB bump. The vendored bump drops a picture the sliding window just unmarked once it has been output, so a picture can land in both `RefFrameList` and `dpb.removed`: the AV1 aliasing shape exactly. Measured zero on the vendored vector — but that vector REORDERS, which is precisely what keeps an unmarked picture alive past the AU that unmarked it. A punktfunk host emits LOW-DELAY H.264, where output happens as each picture is decoded, which is the condition that makes eviction and unmarking land in the same access unit. Traced end to end in source, not reproduced (no low-delay vector). NOT fixed: changing a hardware-proven codec on an unreproduced suspicion is the worse risk two commits before a release. Instead `no_au_removes_a_picture_its_own_reference_list_names` makes the assumption falsifiable, and its message says what to do when it fires. HEVC is structurally safe and now says why: `H265Planner` snapshots `dpb_refs` AFTER `decode_rps`. **Four more stale promotion sites**, past the four already fixed: `Backend:: NativeD3d11va`'s variant doc, `Decoder::new`'s Windows rung comment, `lib.rs`'s module note and `clients/session/README.md`. Two sites that used the AV1 leg as the live EXAMPLE of an unproven rung are marked as expired rather than deleted — the reasoning is what the next bad-evidence leg will need. **The AV1 dump was missing.** `PF_DXVA_DUMP` wrote h264 and hevc only, for the one codec whose libavcodec capture has never been taken and where the dump is therefore the only tool. |
||
|
|
7f822d9c41 |
Merge pull request 'Apple mic uplink: the microphone was never in the audio graph, so nothing ever pulled it' (#94) from fix/apple-mic-uplink into main
apple / swift (push) Successful in 1m31s
ci / web (push) Successful in 2m15s
ci / bun-nix (push) Successful in 21s
ci / docs-site (push) Successful in 1m14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 37s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 24s
ci / rust-arm64 (push) Successful in 3m41s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 32s
docker / builders-arm64cross (push) Successful in 11s
docker / deploy-docs (push) Successful in 36s
ci / rust (push) Successful in 6m49s
release / apple (push) Successful in 9m41s
apple / screenshots (push) Successful in 5m53s
Reviewed-on: #94 |
||
|
|
9db2c06140 |
Merge pull request 'Gamepad UI (iOS): the field pass — a linear drum, no close chrome, a heading that leads, and strips that assemble themselves' (#93) from worktree-gamepad-ios-polish-2 into main
apple / swift (push) Canceled after 28s
apple / screenshots (push) Canceled after 0s
ci / rust (push) Canceled after 34s
ci / rust-arm64 (push) Canceled after 33s
ci / web (push) Canceled after 33s
ci / docs-site (push) Canceled after 0s
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
release / apple (push) Canceled after 0s
Reviewed-on: #93 |
||
|
|
d2d5058d0b |
fix(client/apple): the mic was never in the graph, so nothing ever pulled it
ci / web (pull_request) Successful in 1m1s
ci / docs-site (pull_request) Successful in 1m15s
ci / bun-nix (pull_request) Successful in 20s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m45s
ci / rust (pull_request) Successful in 5m43s
ROOT CAUSE, from the reporter's device log: 16:25:49.093 mic capture: 48000 Hz, 1 ch <- tap installed, format fine 16:25:49.235 audio engines joined - voice processing active ... 13 s of session, no errors, and the 10 s silence verdict NEVER fires The engine started clean and the tap was installed against a valid format - so neither the format timing nor the encoder was the fault. The tripwire fires after ten seconds of CAPTURED frames and never fired across a 13-second session: the tap received nothing at all. Because the capture side must be pulled, and only the render graph pulls anything. On the combined engine the input node carried a tap and no connection, so it was not in the graph and nobody drove it: the IO unit came up (the recording indicator lit for a beat, then went out as the input went idle) and not one buffer ever reached the tap. No error, no failed start - a session that quietly sent no microphone. The input now runs through a silent sink into the main mixer, which is what Apple's own voice-processing sample does. outputVolume = 0 because the mic must reach the graph and never the speaker. The split path never needed this - a capture-only engine has the input node AS its graph - so this broke exactly when the combined topology became the default. Verified: swift build (macOS), swift build --triple arm64-apple-ios17.0, swift test 208 passed. Awaiting the reporter's on-device confirmation. |
||
|
|
428377eaa1 |
Merge pull request 'Intel Arc never used Vulkan Video because our pNext order fed the decode caps to the wrong struct' (#92) from worktree-probe-codec-names into main
audit / bun-audit (plugin-kit) (push) Successful in 19s
audit / bun-audit (sdk) (push) Successful in 21s
audit / bun-audit (web) (push) Failing after 20s
audit / cargo-audit (push) Successful in 32s
audit / docs-site-audit (push) Successful in 15s
audit / pnpm-audit (push) Failing after 39s
apple / swift (push) Successful in 1m28s
ci / rust-arm64 (push) Successful in 1m25s
ci / web (push) Successful in 1m37s
ci / bun-nix (push) Successful in 27s
ci / docs-site (push) Successful in 1m19s
ci / rust (push) Successful in 4m47s
audit / license-gate (push) Successful in 5m7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
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 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
deb / build-publish-client-arm64 (push) Successful in 2m33s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m21s
android / android (push) Successful in 8m3s
deb / build-publish (push) Successful in 7m40s
deb / build-publish-host (push) Successful in 7m22s
docker / builders-arm64cross (push) Successful in 5s
release / apple (push) Successful in 9m44s
windows-host / package (push) Successful in 11m14s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 16s
docker / deploy-docs (push) Failing after 1m41s
arch / build-publish (push) Successful in 13m59s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m27s
flatpak / build-publish (push) Successful in 8m26s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m37s
apple / screenshots (push) Successful in 5m47s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m6s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m18s
nix / flake (push) Successful in 15m19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m14s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 24m25s
Reviewed-on: #92 |
||
|
|
7e34ca3d5e |
fix(client/apple): a session that can't voice-process still sends its microphone
ci / bun-nix (pull_request) Successful in 25s
ci / docs-site (pull_request) Successful in 1m7s
ci / web (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m29s
ci / rust (pull_request) Successful in 5m44s
Field report: mic uplink dead on iOS, iPadOS and macOS alike, while Android on the same host works - so the host and the wire are fine. Two defects in the combined (voice-processing) engine, which became the default on all three Apple platforms a week ago and has never run on a device - CI only runs swift test on macOS, and the loopback test counts datagrams without decoding them. - The tap read the input format before the engine was prepared. Enabling voice processing swaps the engine's IO unit for the VPIO one and renegotiates its formats; until prepare() the input node can still report the pre-swap state, 0 Hz / 0 channels included, which installMicTap correctly refuses as 'no usable input device'. Both topologies now prepare first, so the chain is built against what the voice processor actually emits. - A mic chain that failed on the voice-processed engine took the whole uplink down for the session: that arm fell back to playback ONLY. The sibling failure a few lines above - the voice processor refusing to engage at all - already falls back to the split path, which is a working mic without echo cancellation. Both arms do that now. The mic outranks the AEC. Not reproduced locally (no Punktfunk entries in this Mac's log store, and collecting the device's log needs root), so this is a strong inference plus one proven logic defect rather than a confirmed fix. If it persists, Console filtered to subsystem io.unom.punktfunk / category audio names the stage: 'mic capture: N Hz' then 'audio engines joined' then, 10 s in, either 'mic uplink OK - peak ...' or the SILENCE warning. Follow-up worth doing separately: nothing reports whether the uplink actually opened, so the HUD offers a Mute Microphone button over a session sending nothing. Android gates that on a real micRunning signal. |
||
|
|
adf92939ec |
fix(client/apple): the stats card's corner stops eating its own text
ci / bun-nix (pull_request) Successful in 28s
ci / web (pull_request) Successful in 1m21s
apple / swift (pull_request) Successful in 1m28s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m33s
ci / rust-arm64 (pull_request) Successful in 1m53s
ci / rust (pull_request) Successful in 4m39s
Making the HUD concentric with the physical display corner had no upper bound, so a modern phone (~62 pt of display radius) asked for a 48 pt corner on a card whose lines sit 10 pt from the edge. A corner of radius r pulls the edge inward by r - sqrt(r^2 - (r-y)^2) at distance y below the top: at the first line that is ~19 pt, so the top and bottom lines rendered INSIDE the arc. Concentricity is only a virtue while the radius is small next to the card. The radius is now capped at 28 (devices asking for less still get a truly concentric corner) and the iOS content padding scales with it at 0.45*r, which leaves ~4.6 pt of arc against 12.6 pt of padding at the cap. The card grows by under 3 pt a side; the compact pill is unchanged. |
||
|
|
b166c53cc2 |
fix(vkdecode): a refused device says what the refusal costs, and about which format
ci / web (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 1m33s
ci / docs-site (pull_request) Successful in 2m2s
ci / rust-arm64 (pull_request) Successful in 2m25s
android / android (pull_request) Successful in 3m1s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m13s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m3s
ci / rust (pull_request) Successful in 11m15s
nix / flake (pull_request) Successful in 12m49s
Three things the Intel Arc measurement showed were wrong or unhelpful in the refusal path. The message named NV12 whatever the stream was. A Main 10 session refused over P010 was told about NV12, which sends the reader to look up the wrong format's support. Both variants now carry the format the driver's own entry reported. A missing SAMPLED now says what it costs. "does not advertise usage SAMPLED" is accurate and tells a field reporter nothing: the consequence is that no shader can read this device's decoded pictures, so the zero-copy path cannot exist on it at all — which is a different conversation from a device that is merely slower. The line points at --probe-decode for the driver's own words. And the probe's second opinion no longer claims to be one. Measured on both vendors, vkGetPhysicalDeviceImageFormatProperties2 answers "creatable" for combinations the video-format query rejects — on NVIDIA too, for SAMPLED alone, which is not a legal video image usage at all. So it does not honour the chained profile list and must not be read as permission; it is still printed, because otherwise everyone who reads a refusal asks the question again, but it is labelled as not authority. Also names the three video ENCODE usage bits, which NVIDIA advertises on decode pictures and the probe was printing as "unrecognised 0xC000". |
||
|
|
9582da07b4 |
fix(client/apple): the entrance goes underneath the scroll transition
apple / swift (pull_request) Successful in 1m31s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m15s
ci / rust-arm64 (pull_request) Successful in 1m29s
ci / bun-nix (pull_request) Successful in 19s
ci / web (pull_request) Successful in 1m41s
ci / rust (pull_request) Canceled after 10m43s
Structural, because tuning the transform values was treating a symptom. A scroll transition derives its phase from the geometry of the view it wraps, and the entrance was wrapping each card on the OUTSIDE - so it moved the very thing the transition measures. Every card read as far from centre for the whole travel, phase pinned at fully receded, and the centred card only collapsed into its focused look as the entrance ended. That collapse was the jump; shrinking the offset last round only made it smaller. The card builder now hands each caller its own CardEntrance and both the launcher and the coverflow apply it BENEATH their .scrollTransition. The transition measures a card that never moves and composes its scale and rotation on top of the entrance's, so the two can no longer fight - and the fuller travel is back (34 pt rise) now that the geometry constraint that forced it down to 16 is gone. |
||
|
|
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. |
||
|
|
c06ee55b61 |
diag(vkdecode): --probe-decode reports what the driver says about video images
ci / bun-nix (pull_request) Successful in 38s
apple / swift (pull_request) Successful in 1m31s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m11s
ci / docs-site (pull_request) Successful in 2m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m44s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 6m29s
android / android (pull_request) Successful in 7m44s
ci / rust (pull_request) Canceled after 9m40s
nix / flake (pull_request) Canceled after 9m38s
The Intel Arc refusal moved one step down the caps query and stopped again: the coincide NV12 entry does not advertise SAMPLED. That sentence is punktfunk's, not the driver's, and the last two times a conclusion was drawn from a sentence of ours the conclusion was wrong. So --probe-decode now prints the driver's own answers instead. For every profile the client can negotiate (H.264 High, H.265 Main and Main 10, AV1 Main 8- and 10-bit) it asks vkGetPhysicalDeviceVideoFormatPropertiesKHR in six usage combinations — the three the image pools really create with, plus DPB|DST without sampling, SAMPLED alone and DST alone, which are what localise a refusal to a half. Each answer is printed as the driver gave it: format, usage and create flags named AND in hex with unrecognised bits called out, image type, tiling. A failed query prints its VkResult rather than vanishing into an empty list. It goes through pf-vkdecode's own query rather than a copy of it, which meant splitting query_formats into a physical-device form — the call never needed the VkDevice the old signature demanded. VideoFormat gains imageType and imageTiling to carry the whole record; VUID-VkImageCreateInfo-pNext-06811 compares both for equality, so they were being assumed rather than read. And because a driver that under-reports usage would be indistinguishable from one that genuinely lacks it, the probe asks a second, independent question — vkGetPhysicalDeviceImageFormatProperties2 over the same profile list — and prints it only where the two disagree. A disagreement is the finding. No behaviour change to any decode path: derivation reads the same fields it did. |
||
|
|
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. |
||
|
|
c0f8f051c3 |
fix(client): the probe printed a device index the env var does not take
--probe-decode printed its DISPLAY position and called it the PUNKTFUNK_VK_DEVICE value. It is not. pick_device resolves that variable against the RAW vkEnumeratePhysicalDevices order (setup.rs, `devices.get(i)`) BEFORE any ranking runs, while the probe sorts discrete-first for readability. Those two orders disagree precisely on the hardware this flag exists to diagnose. pick_device's own comment records why the ranking is there: "enumeration order puts the iGPU FIRST on some hybrids (observed: Ryzen iGPU ahead of an RTX dGPU)". So on a hybrid laptop the number the probe printed for the iGPU could well be the number for the dGPU — a diagnostic handing out an actionable value that selects the other GPU, which is worse than printing none. Measured on the Arc + RTX 3500 Ada laptop, which is also where the first output went out with the wrong claim in it: three adapters, and the same Arc iGPU enumerated TWICE. So AdapterDecode now carries the raw enumeration index, captured before the sort, and the printer uses it; the "default presenter" marker stays on the first LISTED entry, because sorted-first is what pick_device lands on when nothing overrides. The duplicate is why the trailing hint names PUNKTFUNK_VK_ADAPTER as the safer knob and admits its limit: two adapters sharing a marketing name cannot be told apart by it, and a name match resolves to whichever enumerates first. The hint also states the thing this whole output invites a reader to get wrong — that a capable GPU in the list does not mean the decoder will use it, because Vulkan Video decodes on the presenter's device and PUNKTFUNK_DECODER does not move the presenter. Gates: fmt clean; clippy -D warnings on punktfunk-client-session and pf-presenter. |
||
|
|
43e713ecca |
fix(client): the probe now accounts for every bit it prints
First hardware run of --probe-decode, on the RTX 5070 Ti:
driver decode ops: H.264, H.265, AV1 (0xF)
Three names, four bits. 0xF is H.264|H.265|AV1|VP9 — bit 3 is
VK_VIDEO_CODEC_OPERATION_DECODE_VP9_BIT_KHR, a real decode operation this
client has no rung for, so the name table stopped short of it and the line
looked complete while silently dropping a codec the driver had advertised.
That is the exact failure this flag exists to prevent. The whole point of
--probe-decode is that a reader can trust the words to cover the number; a mask
with an unexplained bit asks them to trust it instead. VP9 is now named (marked
as having no punktfunk rung, because advertising it as decodable would be its
own lie), and any bit beyond the four we know prints as "unrecognised bits
0x…" rather than vanishing — so the next codec Khronos adds shows up as an
unknown rather than as nothing at all.
Gates: fmt clean; clippy -D warnings on punktfunk-client-session.
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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).
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
e6bcac8770 |
chore(license): name the copyright holder "unom - Enrico Bühler"
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m6s
ci / rust-arm64 (pull_request) Successful in 1m31s
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m23s
android / android (pull_request) Successful in 3m58s
ci / rust (pull_request) Successful in 8m28s
nix / flake (pull_request) Successful in 13m20s
windows-drivers / probe-and-proto (pull_request) Successful in 2m56s
windows-drivers / driver-build (pull_request) Successful in 10m19s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 18m19s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 24m1s
"unom" alone is not the legal entity name. Updated the copyright line in all nine first-party license files -- the root MIT/Apache pair and the hand-maintained copies under clients/apple, clients/decky, packaging/windows/drivers and packaging/windows/pf-vkhdr-layer (there is no script that syncs these, so each is edited directly). The Linux and Windows clients' About screens pick this up automatically: both `include_str!` the root LICENSE-MIT / LICENSE-APACHE at compile time. Deliberately untouched: - Third-party license texts (Geist OFL, FFmpeg, VB-CABLE, the vendored pyrowave/Granite tree, KDE protocol XMLs, the os-icon licenses) -- those are other parties' copyrights. - Publisher/author/maintainer metadata, which is identity rather than license text and is reported separately for a decision. |
||
|
|
b0ec759fad |
fix(brand): capitalize "Punktfunk" in user-facing text
The docs site's page title read "punktfunk docs". Fixed that and swept the rest of the tree for the same defect, capitalizing the brand wherever it is shown to a human and leaving it lowercase where it is a technical identifier (CLI/package names, `punktfunk://` scheme, PnP enumerator, TLS SNI, logcat tag, config paths, CMS tenant id). User-visible fixes: - docs-site: page title -> "Punktfunk Docs"; API reference title, meta description and the branded bar's aria-label; the BrandMark/Wordmark SVG accessible names (the web console already had these capitalized -- the docs site had drifted from it). - Android: six strings of live UI copy -- the local-network permission dialog (x2), the connect-screen error banner, and the no-controller explainer. - Apple: the "No Hosts" empty-state text and the fallback display name for a host that advertises no instance name. - Windows client: the `--discover` progress line. - KWin fake-input: the application name passed to `authenticate()` (the grant is cached per-exe, so the string is display-only). - THIRD-PARTY-NOTICES: fixed in both generators (about.hbs and gen-third-party-notices.py) and applied to the three checked-in outputs so they match what a regeneration now produces. Every changed line differs from the original only by letter case, so line lengths are unchanged and no formatter width rule is affected. `cargo fmt --all --check` passes. |
||
|
|
f33e442b43 |
feat(client/android): copy a host's punktfunk:// link
ci / bun-nix (pull_request) Successful in 25s
ci / web (pull_request) Successful in 1m8s
ci / docs-site (pull_request) Successful in 1m16s
ci / rust-arm64 (pull_request) Successful in 1m41s
android / android (pull_request) Successful in 3m22s
ci / rust (pull_request) Failing after 7m33s
Android was the one app that could open a punktfunk:// link but never hand one out, so every Android link had to be typed by hand — and the host's stable record id, which is the part that keeps a link working after the box changes address, isn't shown anywhere in the UI to type. Both homes now offer Copy link: the touch grid's card overflow menu, and the controller home's host options (Up on a tile). A pinned card copies its own profile with it, matching Linux and Apple; a host card copies none and so keeps honouring the host's binding, exactly like tapping it does. The URL is the shared self-emitted form (DeepLinks.forHost), already covered by the cross-language vector tests, so the three emitters stay in step. Android 13+ draws its own clipboard confirmation and we add nothing on top of it; below that we say so ourselves, as a toast in the console home, which renders neither banner. |
||
|
|
a6e51215fd |
feat(client): M6's rung is wired — libva, dlopen'd, no libavcodec
The native VAAPI decoder now runs end to end: pf-vaadec's plans go into libva's buffers, the surface comes back as DRM-PRIME dmabufs, and the presenter imports them exactly as it does the FFmpeg rung's. Pin-only — `PUNKTFUNK_DECODER=native-vaapi` — for the reason M5's D3D11VA rung was: `auto` admission is earned with hardware parity and a soak, and this rung has decoded nothing yet. libva is dlopen'd rather than linked, so the pf-lxcheck2 container compiles and clippies the whole thing without libva-dev, and a machine without a VAAPI runtime gets a clean refusal instead of a packaging dependency. The surface pool is not the slot map. `SlotMap::assign` hands out the lowest free slot, and a slot freed by an access unit's own removals is free by the time that unit's picture takes it — measured at 225 of the vendored vector's 250 access units. A surface bound by slot index would therefore decode, on nine frames in ten, into the surface still holding the picture on screen. So `plan_to_va` now takes the decode target as a parameter, bound by the caller at activation time the way pf-vkdecode binds a pool image, and a surface is free only when no live picture is bound to it, no output is owed for it, and no consumer holds it. Measured rather than transcribed, as everywhere else here: layout-probe.c grew the export descriptor (312 bytes, objects[4]/layers[4]), the buffer-type enumerators — VASliceParameterBufferType is 4 and VASliceDataBufferType is 5, not the 3 and 4 that counting off the header suggests — and the config, attribute and generic-value layouts. All pinned as compile-time assertions, which is how the 12-byte VAGenericValue in the first draft was caught: the C union holds a pointer, so it is 8-aligned and 16 bytes. The plane walk lives in pf-vaadec, pure and unit-tested on macOS, because it is the one structure the DRIVER writes and we read: SEPARATE_LAYERS returns NV12 as two layers, and taking layers[0] is the green screen this project has already paid for. It also refuses what it cannot express rather than guessing — a bogus object count, a plane naming an object that is not there, objects disagreeing on tiling. Own DecodedImage variant, same payload type. The physical hand-off is identical to the FFmpeg rung's, so the presenter keeps ONE arm and one demotion streak; the variant exists so the compiler asks which rung decoded wherever that matters. Both D3D11VA rungs share a variant and `1573a987` had to fix the consequence afterwards — a "native" soak that could silently have been an FFmpeg soak. Here the four uncovered matches were compile errors. Buffers are destroyed by us, not by vaEndPicture: va.h is explicit that the user must call vaDestroyBuffer, and the libva 0.x behaviour is long gone. Leaking two per picture at 60 fps exhausts the driver's store in minutes. pf-vaadec's presenter headroom was 4, written against no consumer. The Vulkan rung had already measured the client pipeline at four to seven held frames; it is 8 now, pinned to that crate's constant so a re-measurement moves both. Gates: macOS fmt/clippy/341 tests/cargo doc, and in the container clippy -D warnings over six crates, 795 tests, workspace check. Hardware legs are still owed — no AMD/Mesa or Intel box was reachable. |
||
|
|
4a9a1c3ed4 |
feat(clients/gamepad-ui): multi-tone palettes, and a UI that takes its colours from them
ci / docs-site (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 1m18s
apple / swift (pull_request) Successful in 1m25s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m59s
android / android (pull_request) Successful in 5m53s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 6m36s
ci / rust (pull_request) Successful in 9m57s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m41s
The first pass built each palette by rotating ONE colour field's hue, and it
showed: every option was a single tone at several brightnesses, which reads flat
next to any real gradient. A palette is now an ordered ramp of DISTINCT hues.
The 4×4 mesh samples that ramp along the diagonal with a fixed per-cell offset
table, so neighbouring cells land on different parts of it and the colours pool
and swirl instead of banding; the control points' existing drift then moves the
pools around. Violet keeps its explicit sixteen colours, so the default is
untouched.
Twelve of them now, dark first then pale: Violet, Nebula, Abyss, Ember, Moss,
Graphite, then Holo, Sunset, Bloom, Dawn, Mint, Opal. Holo and Sunset are
straight takes on the two reference gradients — foil and poster.
`every_palette_is_multi_tone` measures the hue spread across all sixteen cells
and fails under 45° (20° for Graphite and Opal, which are meant to be
restrained). It caught Ember at 35°, all reds and oranges — the very flatness
this rework exists to remove — and Graphite at 3° despite a comment claiming it
drifted cool to warm. Both were rebuilt until the numbers matched the prose.
The UI follows the palette now, rather than wearing brand violet over whatever
happens to be behind it. Each palette carries an accent and a light flag, and an
Ink derived from those (foreground, accent, on-accent, glass, scrim and its
strength) is published to the whole tree — a thread-local in the console, an
environment value on Apple, a CompositionLocal on Android. Pale palettes flip
the ink: dark text on white frost, with the materials, tray scrims and every
wash that sits under text following suit.
Three things only the renders could have told us:
- Additive blending blows out over a pale ground. Android's blobs and Apple's
legacy field composite with Plus/plusLighter, which over near-white
saturates every blob to white — Holo rendered as a grey wash. Pale palettes
blend normally.
- A white scrim at the dark field's strength BLEACHES the gradient. Mixing
toward black at 0.4 reads as depth; toward white at 0.4 destroys the chroma
it is drawn over. The scrim now carries a per-palette strength.
- White glass over a bright field has far less separating it from its backdrop
than dark glass over a dark one, and needed more body.
Verified: console build + clippy -D warnings + 173 tests, Apple build + 200
tests + an iOS-triple typecheck, Android compile + 62 tests, and eyeball passes
on real renders of both the vivid and the pale ends (console CPU rasters; a new
Roborazzi light-palette scene, which is what exposed the blend-mode bug).
|
||
|
|
c48e60fbb7 |
Merge pull request 'Two July fixes that were never merged: rpm FFmpeg modules + the released-pointer double cursor' (#71) from worktree-july-rpm-and-cursor 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
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-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m56s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m48s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m14s
release / apple (push) Successful in 9m31s
flatpak / build-publish (push) Successful in 10m3s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m17s
Reviewed-on: #71 |
||
|
|
2a67c02f7e |
fix(clients/cursor): the host must not composite a pointer under a released client's own cursor
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 1m9s
android / android (pull_request) Successful in 4m19s
windows / build (x86_64-pc-windows-msvc) (pull_request) Failing after 1m36s
ci / web (pull_request) Successful in 4m31s
ci / docs-site (pull_request) Successful in 4m43s
ci / rust-arm64 (pull_request) Successful in 6m50s
ci / rust (pull_request) Failing after 12m8s
Streaming a KDE desktop showed two cursors: the one the user was moving, and a
second one sitting underneath it that never moved. It was not KDE's — KWin 6.7.3
in cursor-as-metadata mode calls `setRenderCursor(false)` on every recorded buffer
and hands the cursor item to an exclusive `ItemTreeView`, so `shouldRenderItem()`
skips it and no pointer is ever painted into that stream. It was ours.
Both clients declared the render model as `captured && desktop`, so ANY released
pointer handed compositing back to the host. But releasing does not remove the
local cursor — it restores the ordinary window arrow over the video. The host then
blends its own pointer in underneath, and since a released client forwards no
motion, nothing drives it: it stays frozen wherever the host pointer was last left.
Caught live on the host with the render-model diag:
cursor diag: client_draws=false blended=true live=Some((-1, 622, true))
x = -1 — parked on the streamed output's left edge, unchanged sample after sample,
while the user moved their own cursor around freely. Engaging capture flipped it to
`client_draws=true blended=false` and the duplicate vanished, which is why it only
looked "stuck when not dragging": dragging means engaged, and engaged was the one
state that behaved.
The host may composite ONLY while the client holds a grabbed, hidden pointer — the
capture model, engaged — which is the single state with no local cursor on screen.
Released now counts as "the client draws it": the host stops compositing and keeps
forwarding shape/state over the channel (the forwarder ticks on this side of the
flip), so re-engaging is seamless and the client's cached shape stays warm.
|
||
|
|
58ee74cb58 |
fix(clients/windows): clippy's manual_is_multiple_of on the rescan tick
android / android (pull_request) Successful in 6m8s
ci / web (pull_request) Successful in 4m50s
ci / docs-site (pull_request) Successful in 1m14s
ci / rust-arm64 (pull_request) Successful in 5m50s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Successful in 12m37s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m18s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m13s
`cargo clippy -p punktfunk-client-windows -- -D warnings` fails on main with the pinned 1.96.0 toolchain: `ticks % 5 == 0` trips `manual_is_multiple_of`. Pre-existing and not from this branch — found while gating the launcher work on .173, because neither macOS nor Linux ever compiles this crate. Clippy's own suggestion, applied verbatim. |
||
|
|
6ae2ea6708 | Merge remote-tracking branch 'origin/main' into worktree-library-clients | ||
|
|
00d4026054 |
Merge pull request 'Worktree field kleisty triage' (#69) from worktree-field-kleisty-triage into main
arch / build-publish (push) Failing after 40s
apple / swift (push) Successful in 1m26s
ci / web (push) Successful in 1m10s
ci / docs-site (push) Successful in 2m30s
deb / build-publish (push) Successful in 3m43s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
deb / build-publish-client-arm64 (push) Successful in 2m23s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
ci / rust-arm64 (push) Successful in 6m51s
docker / builders-arm64cross (push) Failing after 25s
docker / deploy-docs (push) Failing after 1m57s
release / apple (push) Successful in 9m17s
deb / build-publish-host (push) Successful in 7m58s
android / android (push) Successful in 12m19s
ci / rust (push) Successful in 12m1s
flatpak / build-publish (push) Successful in 9m40s
apple / screenshots (push) Successful in 5m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 15m55s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 15m50s
windows-host / package (push) Canceled after 2m58s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 1s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reviewed-on: #69 |
||
|
|
883c317872 |
feat(clients/library): a launcher tile looks like one, on every client
The host has been able to describe a launcher entry since M2 — `role: "launcher"`,
the `steam_ui` and `launcher_ui` kinds — and the web console has grouped them into
their own rail since M4. No other client ever looked. `pf-client-core` decoded
`role` into an `is_launcher()` helper with zero call sites, and the shared console
model dropped the field entirely on its way to the renderer.
So a launcher tile arrived everywhere else as an ordinary game with no cover art:
indistinguishable from a title whose poster failed to load, sorted into the middle
of the alphabet, and captioned "Play".
One contract, implemented in each client's own idiom:
* launchers never interleave with titles — they lead, and each group keeps the
host's title order
* grid surfaces get a labelled section; a coverflow keeps its single carousel and
names the group the cursor is in, changing as it crosses the boundary. A second
focus rail would mean a new up/down nav model in three renderers for two or
three tiles
* an art-less launcher gets an accent face naming its launcher, not a title
monogram on the neutral one — "opens Steam", not "a cover that didn't load"
* anything that is not `"launcher"` is a game, and a host that omits the field
renders exactly as before (design D4's intended degradation)
* launching is unchanged: the client sends an id, the host resolves the recipe
The grouping is enforced once per client stack rather than per screen. In the
console UI it is an invariant of `LibraryShared::set_games`, so the cursor
arithmetic, the art pump and every future consumer inherit it; on Apple and Android
it is applied where the library is fetched/parsed.
Fixed in passing: the Apple and Android store badges were hard-coded
`isCustom ? "Custom" : "Steam"`, so every Lutris, GOG, Heroic, Epic and Xbox title
was labelled "Steam". Both now carry the same store table the Rust clients use.
The CLI's `--library` gains a fourth column (`game`/`launcher`), appended rather
than folded into an existing one so anything reading the first three is untouched.
Gates: punktfunk-host 436 passed / 0 failed and pf-console-ui 49 passed / 0 failed
on .21 (three new tests), workspace clippy -D warnings and cargo fmt --check clean
there; `swift build` of the full PunktfunkClient and `:app:compileDebugKotlin` clean
on macOS; `cargo check` + `clippy -D warnings` for the Windows client on .173.
Still unproven on hardware: no launcher tile has been clicked on a real host — that
needs the plugins published, which needs this branch's base merged first.
|
||
|
|
72777119fd |
fix(client/android): stop reporting every disconnect as a lost connection
ci / web (pull_request) Successful in 1m6s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m50s
android / android (pull_request) Successful in 3m43s
ci / rust-arm64 (pull_request) Successful in 4m26s
ci / rust (pull_request) Successful in 7m12s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 12m38s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 8m23s
The stream watchdog polled a bare "has the session ended" boolean, so it had exactly one thing it could say and said it every time: "Connection lost — the host may be asleep. Wake it to reconnect." That ran when the player quit their game, when an operator ended the session from the console, and when they pressed Back themselves — telling them to go wake a host that was never asleep. It now reads the end reason. Only a connection that actually died gets that line, a host-side failure gets its own, and the three deliberate endings say nothing at all: leaving the stream is already the feedback, and a toast on top of it is just noise. A game launched from a library also returns to that library instead of host selection, which needs the intent hoisted out of the console shell: the stream replaces that shell in the composition, discarding the `remember`s holding its screen and host, so by the time the session ends there is nothing left to navigate back with. The parent holds it across the gap and the shell consumes it on the way in. The touch UI has no library — only the console shell does — so there it is the toast fix alone. |
||
|
|
81b4f76c4d |
fix(client): a session ending on purpose stops reading as a failure
The desktop clients turned every host-side close into "Host ended the session", and a reason string means "abnormal" to everything downstream: the GTK and Windows shells raised a banner, the console overlay drew a status strip. Quitting a game you launched yourself produced all of that. Now only a host error or a lost connection carries a message; the deliberate endings return the silence those shells already give a clean exit, which is also what puts the console back in its library with nothing in the way. The Apple client gains the same distinction. It had one line for every ending — "Session ended by <host>." — which is fine for an operator stopping the session and wrong for a link that died, so each now says what happened. A game exiting stays silent and returns to the library it was launched from. Both read the reason while the connection is still up, because tearing it down is what makes it unreadable, and both fall back to their previous wording when there is no verdict — an older core, or a close that raced the read — rather than inventing a new one for a case they cannot see. |
||
|
|
d74639de70 |
Merge pull request 'A safe-area resolution that keeps the picture out of the notch' (#68) from worktree-launchers-safearea-exclusions into main
apple / swift (push) Successful in 1m28s
ci / rust-arm64 (push) Successful in 1m53s
android / android (push) Successful in 5m48s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
ci / web (push) Successful in 1m4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / docs-site (push) Successful in 1m12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
docker / builders-arm64cross (push) Successful in 7s
docker / deploy-docs (push) Successful in 34s
ci / rust (push) Failing after 9m39s
release / apple (push) Successful in 9m11s
apple / screenshots (push) Successful in 5m42s
Reviewed-on: #68 |
||
|
|
d4dd5f7a3d |
feat(client): a game exiting takes you back to its library
Quit a game you launched from a host's library and the stream ended with "Session ended by <host>." on the host-selection screen — an error report for something you had just done on purpose, and several taps away from starting the next title. The host has always said what happened: it closes the connection with APP_EXITED when the game it launched for a session exits, and that code's own documentation describes this feature. Nothing ever read it — a search across every client found zero consumers. (It also could not reach anyone until the previous commit, since the close only happens once the lease declares the game gone.) The core now records the reason as it observes the close, latched before the shutdown flag because different threads watch the two, and exposes it as punktfunk_connection_game_exited. Purely additive: a client that never asks behaves exactly as before, the host sends identical bytes, and the wire version is untouched — ABI 17. The Apple client asks while the connection is still up, then treats a game exit as the normal finish it is: no error banner, and if the session began as a library launch it reopens that library so the next title is one tap away. Any other ending — a stop, the host going away, network loss — is unchanged. The other clients keep their existing end-of-session behaviour; the call is there when they want it. |
||
|
|
ea762b849d |
fix(client/ios): Escape stays in the game instead of freeing the pointer
Pressing Escape mid-stream on an iPad handed the mouse back to iPadOS: the captured cursor was swapped for the system one and the game stopped receiving relative motion, so aiming died until you clicked back in. Two previous attempts treated that release as unavoidable and built recovery around it — a re-lock burst, then a click that re-asks. Both came back from the field unchanged, because both fought the release after it had already happened, inside the cooldown the platform applies straight after its own "let me out" gesture. The release was never unavoidable. This app had no UIKit key handling at all: every key arrives on the GameController path, which is a parallel HID feed that does not consume the UIKit event, and the only thing that ever became first responder was the video view, and only to summon the soft keyboard. So every hardware Escape reached UIKit unclaimed — and an unclaimed key press is precisely what lets the system apply its own default for that key. Apps that read a hardware keyboard the ordinary way consume the event as a side effect and never see this. So claim it. The stream controller becomes first responder while capture is engaged and takes Escape in pressesBegan/pressesEnded, passing every other press to super untouched. Escape still reaches the host on the GameController path, so in-game menus open exactly as before; only the system's own interpretation is suppressed. Scoped to captured input, so Escape keeps dismissing sheets and leaving full screen whenever the stream doesn't own the keyboard, and the deliberate ways out are untouched — Cmd-Escape and Ctrl-Opt-Shift-Q are read off the same GameController path and clear capture themselves. The recovery path stays as a backstop and is retimed to match what was measured: the old burst spent its entire budget within ~0.6 s of the drop, i.e. wholly inside the cooldown, where the answer can only be no. Retries now continue at 1.2 s and 2.4 s, and quietly — they don't hide the cursor or mute pointer motion the way the burst does, so a longer recovery costs nothing when it fails. |