de1bfdc0c5e84fd49bad13cb03c6a180d8cc0b99
2696
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de1bfdc0c5 |
Merge pull request 'The console stops borrowing the browser's controls — and its preset tiles stop landing all at once' (#100) from worktree-web-console-sweep into main
ci / bun-nix (push) Successful in 32s
ci / docs-site (push) Successful in 1m12s
ci / web (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 8s
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 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
ci / rust-arm64 (push) Successful in 2m28s
windows-host / package (push) Failing after 1m31s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m37s
deb / build-publish-client-arm64 (push) Successful in 4m16s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
deb / build-publish-host (push) Successful in 5m27s
docker / builders-arm64cross (push) Successful in 6s
deb / build-publish (push) Successful in 5m37s
ci / rust (push) Successful in 7m27s
arch / build-publish (push) Successful in 8m48s
docker / deploy-docs (push) Failing after 6m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m44s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m22s
Reviewed-on: #100 |
||
|
|
3be7d1d4f8 |
feat(web): the console asks its own questions
Follow-up to
|
||
|
|
a85e845255 |
fix(web): the console stops falling out of its own design system
A pre-release sweep of the management console for two things that no type check
and no diff can catch: primitives that were never @unom/ui's, and animation
that a nested motion parent quietly cancelled.
THE PRESET TILES ALL LANDED ON THE SAME FRAME. @unom/ui's <Section> sets
`delayChildren: stagger(...)`, so a page whose cards are direct descendants of
it staggers for free — which is why every page but one looked right. An
<AnimatedCard> is ALSO a motion element and sets no `delayChildren`, and the
Virtual displays preset tiles are cards nested INSIDE that page's config card,
so that card became their timing group. Measured in a headless browser: the
opacity spread between the first and last tile was 0.00 across the whole
animation (six tiles in lockstep), and is 0.98 now — a ~100 ms cascade matching
the rest of the console. The four hand-rolled copies of the stagger container
collapse into one `<Stagger>` that carries the explanation.
FIVE FILES IMPORTED THE WRONG BUTTON. `@unom/ui/button` exports both a plain
`Button` and the `AnimatedButton` that this console's wrapper re-exports under
the same name — so `import { Button } from "@unom/ui/button"` compiles, renders,
and silently opts out of the mount animation and the hover/tap response.
Displays, SessionGame, GPU, Update and PendingDevices had dead buttons sitting
next to live ones.
THREE PRIMITIVES HAD NO WRAPPER, SO NOBODY REACHED FOR THEM. @unom/ui ships
form/select, form/textarea and form/checkbox; components/ui did not, and the
gap was filled with browser-chrome `<select>`, `<textarea>` and
`<input type="checkbox">` in the add-hook modal and both library forms. Select
needs the same token correction Tabs needed — upstream `text-secondary` is a
text colour, but here `--secondary` is a SURFACE, so the trigger's chevron and
placeholder rendered at near-zero contrast on the card behind them.
The hook timeout also stops accepting a value the host rejects: `min`/`max` on
a controlled `<input type="number">` are decoration (no form validation ever
runs), so 900 went into a field capped at 600 and failed later, at run time.
@unom/ui's InputNumber clamps on blur and lets the field be empty while you
retype instead of snapping to the fallback.
Storybook gains the page that had no story at all — the console's largest
config surface, and the reason this shipped unseen. Its <Card> wrapper is load
bearing: it reproduces the motion nesting that IS the bug.
|
||
|
|
42e4d47743 |
Merge pull request 'Gyro: the pipeline was wrong end to end — measured against a real controller, and fixed' (#99) from worktree-gyro-p0-correctness into main
ci / bun-nix (push) Successful in 30s
windows-drivers / probe-and-proto (push) Successful in 24s
ci / web (push) Successful in 1m11s
ci / docs-site (push) Successful in 1m23s
ci / rust-arm64 (push) Successful in 1m31s
apple / swift (push) Successful in 1m30s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
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 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
windows-drivers / driver-build (push) Successful in 1m37s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
deb / build-publish-client-arm64 (push) Successful in 2m34s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m3s
windows-host / package (push) Failing after 1m37s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m18s
deb / build-publish-host (push) Successful in 4m14s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m27s
deb / build-publish (push) Successful in 6m52s
docker / builders-arm64cross (push) Successful in 14s
android / android (push) Successful in 9m13s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m40s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m15s
ci / rust (push) Successful in 11m20s
release / apple (push) Successful in 9m47s
flatpak / build-publish (push) Successful in 7m44s
arch / build-publish (push) Successful in 12m13s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m8s
docker / deploy-docs (push) Failing after 6m20s
apple / screenshots (push) Successful in 5m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m45s
Reviewed-on: #99 |
||
|
|
ee61e8c9ba |
fix(clients/pads): the phone mirror never needed the controller path's frame change
ci / bun-nix (pull_request) Successful in 30s
ci / docs-site (pull_request) Successful in 1m16s
ci / web (pull_request) Successful in 1m22s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
windows-drivers / driver-build (pull_request) Successful in 1m38s
ci / rust-arm64 (pull_request) Successful in 2m12s
windows-drivers / probe-and-proto (pull_request) Successful in 21s
android / android (pull_request) Successful in 3m39s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m11s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m14s
ci / rust (pull_request) Successful in 6m32s
Reverts half of |
||
|
|
7a4cdac5b7 |
fix(client/android): a Bluetooth pad's gyro obeys the same reachability gate as the rest
ci / bun-nix (pull_request) Successful in 26s
ci / web (pull_request) Successful in 1m8s
ci / docs-site (pull_request) Successful in 1m9s
ci / rust-arm64 (pull_request) Successful in 1m34s
windows-drivers / driver-build (pull_request) Successful in 1m34s
android / android (pull_request) Successful in 3m30s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m15s
windows-drivers / probe-and-proto (pull_request) Successful in 18s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m6s
ci / rust (pull_request) Successful in 6m34s
apple / swift (pull_request) Failing after 11m43s
apple / screenshots (pull_request) Skipped
Follow-up to the G10 merge. The new sensor path was written against main, which does not carry this branch's G8 work, so it forwarded motion unconditionally — the one thing G8 exists to stop. `deviceMotion` checked `forwarding` and nothing else. A Bluetooth DualSense in a session that resolved to an X-Box backend would stream ~200 Hz of samples the host parses and discards, for the whole session, exactly as the USB capture path did before G8. Not a regression against shipped behaviour — the path is new — but it would have shipped the defect back into a client that had just been taught not to have it. `Slot` now carries `motionReaches`, asked once at open off the kind that pad DECLARED, in the same shape `ExternalPad` already used. Per pad, not per session: under Automatic the handshake carries the active pad's kind, so a couch with an X-Box pad on slot 0 and a DualSense on slot 1 must not have slot 1's working gyro suppressed by slot 0's answer. The notice moved to where the truth is known. `openSlot` knows only what kind a pad declared, not whether it physically has a gyro — that is discovered later, when `PadSensors` finds a gyroscope and calls `setDeviceHasSensorMotion`. Raising it there is the only placement that both tells a player whose gyro is being dropped and stays silent for the pads that never had one. Also unified the last duplicate scale in the module. G10 hoisted the wire units into `Gamepad` and pointed `DeviceGyro` at them, but `DsDevice` kept its own `20L` / `10000L` — and `Gamepad`'s new comment claims every sender goes through one place, which was not yet true. Two copies of a unit constant in one module is precisely the defect this program opened with (a DualShock 4 blob 40× hot because a second copy had drifted), so the claim and the code now agree. `val` rather than `const val` only because widening to Long is not a constant expression; Long is deliberate, since the calibration arithmetic overflows an Int before it divides. Proven non-vacuous rather than assumed: changing `Gamepad.MOTION_GYRO_LSB_PER_DEG_S` from 20 to 16 now fails four named cases across three classes — `DsDeviceTest.calibrationRescalesRawCountsOntoTheWireUnits`, `.theHostsOwnBlobIsAPassthrough`, `.parseStateAppliesTheCalibration` and `DeviceGyroTest.wireUnitConstants`. Before this change `DsDevice` would not have noticed. The gate itself has no test, for the reason the surrounding code already documents: `GamepadRouter` needs Android plus a live JNI handle, there is no Robolectric in this module, and a mock would test the mock. It is argued at the call sites instead. Gate: `:kit:compileDebugKotlin`, `:kit:testDebugUnitTest`, `:app:compileDebugKotlin`, `:app:testDebugUnitTest` — kit 75 / app 67, 0 failures, counts read out of the JUnit XML. The merge reconciles: 62 on this branch, plus 6 from main's DeviceGyroTest, plus G10's 7. |
||
|
|
e81ab1ff2b | Merge branch 'worktree-agent-a6f74e8ea7824fb56' into worktree-gyro-p0-correctness | ||
|
|
8f1081719f |
feat(client/android): a Bluetooth controller's gyro stops going nowhere
Android had two motion sources and both of them are USB claims. DsCapture takes a Sony pad's HID interface away from the kernel; Sc2Capture does the same for a Steam Controller 2. Everything else — a DualSense, a DualShock 4, a Switch Pro, an 8BitDo, paired over Bluetooth — arrives as an ordinary InputDevice. Its buttons worked, its sticks worked, and its gyro was dead, silently, with no log line and nothing in the UI to suggest the pad had a sensor at all. That is not one controller, it is the whole class of controllers people actually pair to a phone. The platform has had the answer since Android 12: InputDevice.getSensorManager hands back a SensorManager scoped to that one controller, carrying its TYPE_GYROSCOPE and TYPE_ACCELEROMETER. PadSensors registers a listener per forwarded pad that has a gyroscope and sends the samples on that pad's wire index. Below API 31 it registers nothing and the pads behave exactly as they did. It is built on DeviceGyro's shape, because the phone mirror had already paid for these lessons. One dedicated HandlerThread, never the main one. Batching off (maxReportLatencyUs = 0) — batching would trade away precisely the latency gyro aim exists to avoid. 200 Hz requested, which is also the ceiling the framework grants an app without HIGH_SAMPLING_RATE_SENSORS, so asking for more would only be capped. And a feed that lets go of a pad still alive parks its rotation at zero first: the host holds motion as state and re-emits it in every virtual-pad report, so an angular velocity left behind is a pad that rotates forever. Two writers on one pad's motion is the failure this program has spent the day unpicking, so the coordination is explicit in three places. A USB capture wins: DsCapture.startUsb already calls releaseDevice at claim time, that closes the slot, and the close now also takes the sensor listeners off — the claim makes the InputDevice vanish anyway, but going through the explicit teardown is what makes the ordering deterministic instead of a race against the platform's own removal callback. The phone-gyro mirror stands down: registering flips a bit the router reports through padHasOwnMotion, which DeviceGyro re-reads on every sample and answers with its own zero park. And a pad with an accelerometer but no gyroscope is deliberately NOT taken — it could only send gravity while pinning rotation at zero, on a pad the mirror is otherwise entitled to speak for, which is the same fight in a quieter costume. The wire units are measured fact (punktfunk_core::input::gamepad: 20 LSB/deg·s, 10000 LSB/g), and they now live in exactly one place on this client: Gamepad.motionGyroWire / motionAccelWire, which DeviceGyro was hand-inlining a second copy of. The gyro program's first finding was a client sending 40x hot because a second copy of a number had drifted, and the merge that followed found a sender nobody remembered to correct. One function, both callers. THE AXIS FRAME ON THIS PATH IS NOT VERIFIED, and the mapping is deliberately straight through rather than guessed at. What is known: the wire is a unit passthrough into a virtual DualSense report, and that report's frame was measured over raw HID on 2026-08-07 as (Right, Up, Backward-toward-the-player) carrying (pitch, yaw, roll), right-handed — which is why the USB path forwards the pad's own order un-remapped and is correct to. Android documents its sensor frame for a handheld device as +x right, +y up, +z out of the face, the same frame once "the face" is read as the one the player looks at. So straight through is what the documentation implies. What nobody has done is put a Bluetooth DualSense in front of the platform sensor framework and compare — those numbers come through a HID driver and InputFlinger's sensor mapper, either of which could permute or negate without saying so. A plausible-looking wrong remap is exactly the bug this program keeps finding, so the code says unverified and names the measurement that settles it, and each feed logs its first converted sample so the cheapest half of that measurement — which slot gravity lands on with the pad flat and still — costs a logcat line. PadSensorsTest pins the scale, the clamp, the rounding and the straight-through order, mutation-checked four ways: 20 to 16 fails gyroScaleFromRadiansPerSecond and straightThroughFrame, reversing the axis order fails straightThroughFrame, truncating instead of rounding fails roundsToNearestNotTowardZero, and negating the accel fails restingPadIsTheHostNeutral. Its frame expectations are written to change together with any remap that lands, not to be edited around one. GamepadRouter needs Android and a live JNI handle and there is no Robolectric here, so its half is argued in comments beside the code, as DsCapture's claim ordering already is. Gates: kit 65 tests (58 before, plus 7), app 67 unchanged, 0 failures, read out of the JUnit XML rather than off a green build. |
||
|
|
979ed8f426 |
docs(clients/pads): say that an X-Box virtual pad has nowhere to put motion
G17's motion half. The docs described what the CLIENT sends and stopped there, which
made a promise the host does not always keep.
The support matrix said a desktop client forwards motion from any pad SDL exposes a gyro
on "and the host injects it into the matching virtual pad". The first clause is true; the
second is only true when the virtual pad has a motion plane. The X-Box 360 and One
backends do not — no gyro in their HID contract — so the host parses every sample and
discards it. That is where *Automatic* lands anything it does not recognise as Sony or
Valve, an 8BitDo with a perfectly good gyro included, and where a Switch Pro lands on a
Windows host with no `hid-nintendo` backend to fold it into.
A reader following the old text would conclude their gyro was broken. The failure has no
other symptom: motion just does nothing.
So both pages now say what to do about it — pick a DualSense-class type — and the
client-settings page says it where the choice is actually made, next to the degrade
paragraph that explains why a session ends up on an X-Box pad in the first place.
The Deck's Steam-Input requirement moves out of Decky's settings blurb, which is the one
place a Deck user streaming FROM the Deck would never look. With Steam Input on, Steam
hands the app its own virtual X-Box pad, so no controller-type choice can help: there is
no gyro on the pad the client can see.
The picker help text now mentions motion on GTK and Android, which is where it was
missing — Windows already said it and Apple says it in its own words. One sentence, the
same sentence, so the four clients answer the question the same way.
This is the doc side of the on-screen notice that shipped earlier in this branch. The two
exist for the same reason and now agree: the client says it when it detects the case, the
docs say it when someone goes looking.
Not covered: the preset COUNTS in note 1 ("Android and the console home offer six …
Windows and Apple offer five") are still unverified against the four pickers, and the
Apple picker's missing Steam Deck entry is a code gap rather than a doc one. Both are
noted in the plan and left for their own change rather than guessed at here.
Gate: Linux CI image fmt + `clippy --locked --all-targets -D warnings` on
punktfunk-client-linux (the GTK string is compiled) plus the core crates and their tests;
Android `:app:compileDebugKotlin` + `:app:testDebugUnitTest`. Green.
|
||
|
|
1eab4b6626 |
fix(client/apple): the phone-gyro mirror was left on the old motion convention
ci / docs-site (pull_request) Successful in 1m3s
apple / swift (pull_request) Successful in 1m37s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 1m30s
windows-drivers / driver-build (pull_request) Successful in 1m39s
ci / web (pull_request) Successful in 2m5s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 3m21s
android / android (pull_request) Successful in 4m54s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m6s
ci / rust (pull_request) Successful in 9m4s
windows-drivers / probe-and-proto (pull_request) Failing after 10m10s
Surfaced by the merge. `DeviceGyro`'s header states the contract plainly — "units and axis semantics match `GamepadCapture.forwardMotion` exactly … the same convention, so a future sign/scale correction lands in one place for both sources" — and this branch made two such corrections in only one of the two places. That is a promise the code stopped keeping the moment the controller path was fixed. Both were true parity when #88 was written; both broke here. **The negation.** `GamepadCapture` sends `-(gravity + userAcceleration)` because Apple reports the gravity VECTOR, pointing down, while an accelerometer measures proper acceleration, pointing up at rest — and the wire carries the latter. The mirror sent it un-negated, so a phone lying still told the host it was accelerating downward at 1 g. The comment above that line even claimed the convention matched. **The frame.** The mirror's remap targets the controller frame its own header describes — x right, y up, z out of the screen — which is exactly GameController's frame, and that is not the DualSense report frame the wire is defined in. So the same change of basis the controller path now takes applies here, after the orientation remap rather than instead of it: the remap resolves which way the phone is being held, and the basis change translates the result into the pad's language. Two different jobs that happen to compose. Order matters for the closing sample too. `stop` replays `lastAccel` beside a zero gyro so "rotation stopped" does not also read as free fall; `lastAccel` is recorded after both conversions, so what gets parked is what was actually sent. Left alone deliberately: `DeviceGyroRemap` itself and `DeviceGyroRemapTests`. The orientation matrices answer a different question — which way is the phone being held — and nothing measured this evening bears on them. They remain derived-not-verified, as their own doc says, and the on-glass pass that owes the controller path a check owes them one too, in all four orientations. Gate: macOS `swift build` + full suite (215 tests, 5 skipped, 0 failures) and the iOS-triple typecheck green — the latter is what actually compiles this file, since the whole thing is `#if os(iOS)`. |
||
|
|
5a4305c072 |
merge: bring current main into the gyro correctness branch
main moved ~60 commits while this branch was in progress, and one of them matters here: PR #88 (the phone-gyro mirror) landed, touching the same motion path. One conflicted file, `GamepadCapture.swift`, in three places — all of them the two changes meeting rather than disagreeing: - **Slot fields.** #88 added `motionSent` + `lastAccel` for its flush-parks-motion fix; this branch removed `lastMotionNs` with the 4 ms drop-throttle. Kept both decisions: the parking state stays, the throttle field goes. - **forwardMotion's head.** #88 added the mirror stand-down (`pad 0` yields while the phone speaks for it); this branch deleted the throttle guard. Kept the stand-down, dropped the guard. - **The send.** This branch converts into the DualSense report frame; #88 records what went out so `flush` can replay it beside a zero gyro. Both, with the recording placed AFTER the conversion — `flush` replays `lastAccel`, so it has to be the vector that actually went on the wire, or a still pad's gravity gets parked in the wrong axis. The two features compose exactly, which is worth stating because it is not luck: this branch gates motion capture on `hasRotationRate`, and #88 engages the phone mirror when `hasRotationRate != true`. They are complements — a pad either drives its own gyro or the phone mirrors for it, never both and never neither. Everything else auto-merged. Note `DeviceGyroRemapTests` is `#if os(iOS)`, so the macOS suite reports the same 215 as before the merge rather than gaining #88's six — checked, not assumed. Gates re-run against the merged tree rather than trusting either side's: Linux fmt + build + `clippy --locked --all-targets -D warnings` + punktfunk-core and pf-inject suites; Apple 215 tests and the iOS-triple typecheck; Android kit + app compile and tests. All green. |
||
|
|
0170da2a5f |
fix(client/apple): stop dropping rotation, and stop inventing it
ci / bun-nix (pull_request) Successful in 46s
ci / web (pull_request) Successful in 1m3s
ci / docs-site (pull_request) Successful in 1m38s
apple / swift (pull_request) Successful in 1m37s
apple / screenshots (pull_request) Skipped
windows-drivers / driver-build (pull_request) Successful in 1m42s
ci / rust-arm64 (pull_request) Successful in 2m19s
windows-drivers / probe-and-proto (pull_request) Successful in 33s
android / android (pull_request) Successful in 3m33s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m15s
ci / rust (pull_request) Successful in 4m50s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m9s
G13 — the three capture-fidelity findings from the gyro sweep, two fixed and one argued. **The 4 ms floor was a DROP, and it was shedding real rotation.** A sample arriving 3.9 ms after the last one was discarded outright. That is the wrong shape for this signal: buttons and sticks are absolute state, so a dropped frame costs nothing — the next one says everything it would have. Angular velocity is a RATE, and a consumer integrates it into an angle, so a dropped sample is rotation that happened and can never be recovered. GameController's delivery jitters around the pad's own ~250 Hz, so a floor set AT that rate does not shed a rare extra sample; it sheds a steady fraction of every turn. And the error is one-signed, so it accumulates — aim drifting short, which reads as bad sensitivity rather than as a bug. Nothing needed the ceiling. GC delivers at the sensor's rate rather than faster, the SDL client has always forwarded every sample, and the host's idle watchdog is a 100 ms timeout this cannot outpace. The throttle's two fields went with it: `lastMotionNs` was left set-but-never-read once the guard was gone, and `motionIntervalNs` had no other consumer. (Notes elsewhere say `flush` parks motion and reads it — that is PR #88's branch, not this one. Checked rather than assumed.) **An X-Box pad was streaming gyro it does not have.** Capture attached to any `GCMotion`, and an X-Box controller exposes one that reports gravity and NOTHING else. So the client sent a permanently-zero `rotationRate` to the host as authoritative gyro, under a declaration saying this pad has one. That is worse than having no motion plane at all: a game sees a controller being held perfectly still forever, and there is nothing to fall back to and nothing to notice. Now gated on `hasRotationRate`, which is GameController's own answer to the question we actually mean. The settings badge had the same bug from the same cause — `hasMotion` was `motion != nil`, so an X-Box pad got a gyroscope icon. It now reads `hasRotationRate` too. One wrong predicate was driving both the UI promise and the wire behaviour, which is why they were wrong together. That also simplifies G8's "your gyro can't reach this session" notice, which had to test `hasRotationRate` itself to avoid nagging about a gyro the pad never had. With the attach gated on it, the notice is just the else-branch. **Motion stays on the main queue, and this is the argument for why.** GameController's `handlerQueue` is a property of the CONTROLLER, not of an element, so moving motion off main moves buttons, sticks, the touchpad and the escape chord with it. This class is `@MainActor` throughout — eight `assumeIsolated` sites, the slot table, the gesture timers — so that is a rewrite of the isolation model rather than a queue assignment, and it would put the tvOS escape chord (the only controller way out of a stream there) on a background queue. That is a real risk for a speculative gain. The comment says so at the call site, and names the measurement to make first if it ever does bite: the host's per-pad motion inter-arrival histogram already reports exactly this and would say whether the delay is client-side or on the wire. Gate: macOS `swift build` + the full suite (215 tests, 5 skipped, 0 failures) and the iOS-triple typecheck green. No test pins the throttle removal or the capability gate: both are properties of live `GCMotion` delivery, which this module cannot fake — there is no injectable seam, and inventing one to assert "we called sendMotion twice" would test the mock. They are argued at the call sites instead, in the same spirit as the parts of `DsCapture` that are not unit-testable in their module either. On-glass verification is owed with the two already outstanding on that rig. |
||
|
|
d996449a82 |
fix(host/pads): a virtual pad at rest said it was in free fall
G14, unblocked by the frame measurement in
|
||
|
|
efb7f99129 |
fix(client/apple): motion arrived in the wrong frame — measured against a real pad
G16 step 1, and the second half of what |
||
|
|
a8a4b11f5c |
Merge pull request 'HEVC died at 720p and 1080p because we sized the DPB from the level's ceiling, not the stream's need' (#96) from fix/hevc-dpb-level-ceiling into main
ci / web (push) Successful in 1m6s
ci / bun-nix (push) Successful in 50s
apple / swift (push) Successful in 1m32s
ci / docs-site (push) Successful in 1m15s
ci / rust-arm64 (push) Successful in 2m46s
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 8s
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 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
ci / rust (push) Failing after 4m2s
deb / build-publish-client-arm64 (push) Successful in 3m0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m4s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m18s
deb / build-publish (push) Successful in 5m30s
android / android (push) Successful in 7m28s
docker / builders-arm64cross (push) Successful in 10s
deb / build-publish-host (push) Successful in 5m58s
apple / screenshots (push) Successful in 6m1s
docker / deploy-docs (push) Successful in 32s
arch / build-publish (push) Successful in 10m30s
flatpak / build-publish (push) Successful in 7m11s
windows-host / package (push) Successful in 14m24s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 22s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m37s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m2s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m30s
Reviewed-on: #96 |
||
|
|
8805a18375 |
Merge pull request 'A damaged AV1 frame stops killing the whole client (and a BOM stops erasing every setting)' (#97) from worktree-rav1d-single-frame-context-abort into main
android / android (push) Canceled after 0s
apple / swift (push) Canceled after 16s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 4s
deb / build-publish-client-arm64 (push) Canceled after 4s
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
flatpak / build-publish (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, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 4s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reviewed-on: #97 |
||
|
|
e9ddc3c561 |
Merge pull request 'AV1 stops shipping half a frame' (#95) from worktree-av1-subframe-truncation into main
android / android (push) Canceled after 22s
apple / swift (push) Canceled after 57s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 46s
ci / rust (push) Canceled after 54s
ci / rust-arm64 (push) Canceled after 28s
ci / docs-site (push) Canceled after 14s
ci / web (push) Canceled after 21s
ci / bun-nix (push) Canceled after 15s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 39s
deb / build-publish-client-arm64 (push) Canceled after 35s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 17s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 16s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 17s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 9s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 8s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (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 32s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
windows-host / package (push) Canceled after 2m28s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
Reviewed-on: #95 |
||
|
|
a1b286245c |
Merge pull request 'Windows audio substrate: minted Punktfunk endpoints retire VB-Cable' (#98) from worktree-audio-substrate into main
android / android (push) Canceled after 32s
ci / bun-nix (push) Successful in 22s
apple / swift (push) Canceled after 33s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 36s
ci / rust (push) Canceled after 35s
ci / rust-arm64 (push) Canceled after 45s
ci / web (push) Canceled after 45s
ci / docs-site (push) Canceled after 44s
deb / build-publish (push) Canceled after 31s
deb / build-publish-host (push) Canceled after 27s
deb / build-publish-client-arm64 (push) Canceled after 12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 21s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 17s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 5s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 17s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 20s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 3s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 6s
docker / builders-arm64cross (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 5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 4s
windows-host / package (push) Canceled after 59s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
decky / build-publish (push) Failing after 10m34s
Reviewed-on: #98 |
||
|
|
8551e88fcb |
merge: bring current main into the audio-substrate branch
ci / bun-nix (pull_request) Successful in 47s
ci / web (pull_request) Successful in 1m19s
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m27s
ci / rust-arm64 (pull_request) Successful in 4m25s
android / android (pull_request) Successful in 5m59s
ci / rust (pull_request) Successful in 7m19s
Two conflicts, both unions of independent removals/fixes: main fixed the same three install.rs SAFETY comments this branch fixed (main's phrasing kept), and the runner provisioning drops BOTH env lines — main removed PF_FFVK_VULKAN_INCLUDE (pf-ffvk is gone since the FFmpeg replacement), this branch removed VBCABLE_DIR (the retirement). |
||
|
|
bca63cd9ff |
docs(client/video): name the upstream rav1d issue next to the workaround
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m53s
ci / bun-nix (pull_request) Successful in 25s
ci / docs-site (pull_request) Successful in 1m23s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m36s
ci / web (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 3m41s
ci / rust (pull_request) Successful in 6m21s
memorysafety/rav1d#1497, filed with the one-line fix and a reproducer that needs no capture — any AV1 stream with one temporal unit removed. Written down where the setting is, because the next person to read `av1_settings` and wonder whether the floor is still needed should be able to check rather than re-derive it. |
||
|
|
eb9203b115 |
fix(client/settings): a byte order mark stops silently erasing every setting
ci / bun-nix (pull_request) Successful in 40s
ci / web (pull_request) Successful in 1m14s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m12s
ci / docs-site (pull_request) Successful in 1m27s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m56s
android / android (pull_request) Canceled after 2m30s
ci / rust (pull_request) Canceled after 2m17s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 1m8s
`Set-Content -Encoding UTF8` writes a UTF-8 BOM, and every Windows how-to
reaches for it, so `%APPDATA%\punktfunk\client-windows-settings.json` edited
from a shell arrives with `EF BB BF` in front of the `{`. serde_json rejects
that at byte 0 — correctly, JSON has no BOM — and
`.and_then(|s| serde_json::from_str(&s).ok())` turned the refusal into
`Default`. Every setting in the file, gone, with the file plainly correct on
screen and not one word anywhere about why.
Cost an hour on 08-07: a `codec: "av1"` edit was ignored and the client
negotiated HEVC. The obvious suspects — the negotiation, the caps, the host —
were all working exactly as designed.
So the mark is stripped, which is what every other JSON consumer on Windows
does. But the BOM is only the instance; the bug is the `.ok()`, which hides a
trailing comma, a truncated write and a hand-edit typo just as completely.
Those now cost one `warn!` naming the file and serde's own line and column. A
file that cannot be READ at all is reported too, and for the same reason: PowerShell's
`-Encoding Unicode` writes UTF-16LE, `read_to_string` rejects it as invalid
UTF-8, and that lands in exactly the same hole.
The RESULT is deliberately unchanged — `Default`, never an error. Nothing about
streaming may hinge on a settings file being readable, and refusing to start
because one is malformed would be a worse failure than the one being fixed. A
missing file stays silent, because that is just first run.
All three of this client's JSON stores share the loader, because all three had
the identical line: the settings file, the known-hosts store (where a BOM
silently unpairs every host) and the profiles catalog.
|
||
|
|
32ff93199f |
fix(client/video): a damaged AV1 frame stops killing the whole client
The software rung aborted the process — not the session, the process — the
first time a 4K AV1 stream lost a frame. Reproduced on .21 twice on 08-07,
`SIGABRT` a few hundred milliseconds after "first frame decoded".
It was never about 4K, and it was never our bitstream.
rav1d 1.1.0 kills the process on ANY decode error while it holds a single
frame context. `rav1d_submit_frame`'s `c.fc.len() == 1` branch calls
`rav1d_decode_frame` inline; that always finishes in
`rav1d_decode_frame_exit`, which does an unconditional
`mem::take(&mut f.frame_hdr)` (decode.rs:4873); and then, only if the decode
returned `Err`, the same branch re-enters a local `on_error` whose first act is
`f.frame_hdr.as_ref().unwrap()` (decode.rs:4997) — on the `None` the teardown
just left. The panic unwinds into `dav1d_send_data`, which is `extern "C"`, so
it is `panic_cannot_unwind` → `abort()`: no `catch_unwind` at our call site, no
rung demotion and no `NoSoftwareRung` refusal can catch it. The same code is in
upstream `main` today, and 1.1.0 is the newest release, so there is no version
to bump to.
4K was only where an error first HAPPENED. The CPU rung cannot keep up at
3840x2160 (35-39 fps against a 60 fps stream), so the receive backlog stopped
draining, `pump::data` flushed it and jumped to live, and the next AU
referenced frames nobody had decoded. libdav1d gives the identical verdict on
the identical capture — 13 frames, then "Invalid data found when processing
input" — and simply carries on. At 1080p the rung keeps up, nothing is ever
flushed, no AU is ever damaged, and the same code ran for years without
anybody seeing this.
So the fix is to stop asking rav1d for the configuration whose error path is
broken. `c.fc.len() > 1` never calls `rav1d_decode_frame` at all: it hands the
frame to `rav1d_task_frame_init` and errors come back through `cached_error` /
`task_thread.retval` as ordinary `EINVAL`s, which the pump already answers with
a keyframe request. Measured, against the captured 4K stream:
n_threads=8 max_frame_delay=1 -> n_fc=1 -> ABORT
n_threads=1 max_frame_delay=1 -> n_fc=1 -> ABORT
n_threads=1 max_frame_delay=2 -> n_fc=1 -> ABORT <- proves the rule
n_threads=8 max_frame_delay=2 -> n_fc=2 -> 13 pictures, EINVAL, survives
n_threads=8 max_frame_delay=0 -> n_fc=3 -> survives
The third row is why `n_threads` grows a floor of two as well as the delay:
`n_fc` is `min(max_frame_delay, n_threads)`, so one decode thread silently puts
the whole thing back on the aborting path. That row is also what rules out the
theory this investigation started with — pinning threads to 1 was the suspected
trigger, and it makes things WORSE, so the tile workers are innocent and the
single frame context is the entire defect.
Two frame contexts would normally cost a frame of latency, and this does not,
because `decode` now drains PAST the first `EAGAIN`. `rav1d_get_picture` only
reaches its blocking `drain_picture` on a call whose own `drain` flag is already
set, and that flag is set by the PREVIOUS `get_picture` and cleared by every
`send_data` that carried bytes — so the first `EAGAIN` after a send does not
mean "no picture for this AU", it means "ask again", and this AU's frame comes
out of the second call. Stopping at the first `None` is what a
single-frame-context reading of dav1d's API teaches, and it would have put the
pipeline two frames behind while looking perfectly healthy. Measured over 14
temporal units at `n_fc = 2`: stopping at the first `None` produces nothing at
all for units 0 and 1; draining past it produces one frame per unit from unit 0,
at 20-42 ms per unit against `n_fc = 1`'s 21-53 ms. Not a trade — same cadence,
slightly faster, because the tile workers overlap the drain.
`Av1Software::new` then asks rav1d itself, through `dav1d_get_frame_delay`,
what those settings actually bought, and refuses to open a decoder that would
run with one frame context. That is not a restatement of the arithmetic: it is
`get_num_threads`' own answer, so it stays right if rav1d's derivation changes.
It is there because the failure it guards is uniquely quiet — an edit that
reinstates `n_fc = 1` costs nothing at build time, nothing in the tests and
nothing on a clean link, and then kills the client the first time a frame
arrives damaged. Losing the rung is recoverable; `abort()` is not.
On glass, .21, 35-second sessions, `PUNKTFUNK_DECODER=software`:
4K60 AV1 before: SIGABRT on the second frame, every run
after: exit 0, 0 panics, 35-39 fps, 1204 frames, decode_failed=0,
and 13 decode errors recovered from across 17 backlog
flushes — the exact condition that used to abort, survived
thirteen times in one session
1080p AV1 after: 40 fps, decode p50 2.2 ms (2.1 ms before the change)
What this does NOT buy: rav1d has other `unwrap()`s, and because its whole
public surface is dav1d's `extern "C"` ABI — every internal `rav1d_*` entry
point is `pub(crate)` — no in-process guard can turn one of them into anything
but an abort. This removes the one we hit and can prove; it does not make the
CPU rung panic-proof, and the evidence table says so.
Reported upstream with a self-contained reproducer: the in-tree
`test-25fps.ivf.av1` vector with one temporal unit dropped aborts rav1d at
`n_fc = 1`, survives at `n_fc = 2`, and libdav1d decodes it with 145 error
reports and no crash.
|
||
|
|
0ee690f8bc |
fix(client/hevc): the DPB we demanded was the level's ceiling, not the stream's need
ci / bun-nix (pull_request) Successful in 34s
ci / web (pull_request) Successful in 1m12s
ci / docs-site (pull_request) Successful in 1m12s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m18s
ci / rust-arm64 (pull_request) Successful in 2m30s
android / android (pull_request) Successful in 3m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m21s
ci / rust (pull_request) Successful in 5m42s
A punktfunk client streaming HEVC from .21 (RTX 5070 Ti) refused every access
unit with "stream needs 17 DPB slots, device caps at 16", flushed, waited for an
IRAP, got a fresh IDR that needed 17 too, exhausted the decode ladder and
reconnected with HEVC excluded. On a build with no software HEVC decoder — there
is no permissively licensed one — that is not a slower path, it is losing the
codec.
The host was blameless. Reading the SPS it actually emitted: general_level_idc
153 (L5.1 High, which NVENC autoselects at hevcConfig.level = 0 because a
130 Mbps target does not fit L5.0's 100 Mbps ceiling) and
sps_max_dec_pic_buffering_minus1 = 5 — six pictures, RFI_DPB references plus the
current one. Six, at every resolution. That is already the minimum the encoder
can honestly declare, and the only host-side lever, the level, cannot be lowered
without signalling a bitrate the stream exceeds. There was nothing to fix there.
dpb_limit was reading equation A-2 instead. A-2 is a CEILING on what an SPS may
signal — 7.4.3.2.1 constrains sps_max_dec_pic_buffering_minus1 to
0..=MaxDpbSize-1 — not a statement of what a stream needs, and it branches on
picture size against the LEVEL's MaxLumaPs. At 1080p the coded 1920x1088 =
2 088 960 luma samples fall under MaxLumaPs(L5.1) >> 2 = 2 228 224, taking the
first branch for min(4 * MaxDpbPicBuf, 16) = 16. max(A-2, buffering) then
reported 16 where the stream had asked for 6, the backends added one slot for the
picture in flight, and 17 is one more than NVIDIA's maxDpbSlots.
A resolution sweep on the box drew A-2's branch table exactly, and it is the two
commonest streaming resolutions that lost the codec:
720p 1280x720 = 921 600 branch 1 -> 16 frames, 17 slots 82 refusals, HEVC dropped
1080p 1920x1088 = 2 088 960 branch 1 -> 16 frames, 17 slots 41 refusals, HEVC dropped
1440p 2560x1440 = 3 686 400 branch 2 -> 12 frames, 13 slots clean
4K 3840x2176 = 8 355 840 else -> 6 frames, 7 slots clean, decode 1.9 ms
One host, one level, one six-picture requirement. Only which branch the picture
size landed in decided whether HEVC worked. That is also why this hid for so
long: 4K was the resolution it was exercised at, and 4K is the one size that
falls through to the honest answer. H.264 escaped for an unrelated reason — its
own level-derived ceiling happened to land at 13 for 1080p L5.0 and 5 for 4K
L5.2 — but it is the same shape of derivation and would fail the same way if
NVENC ever picked a higher level for a smaller picture.
So dpb_limit now returns the stream's own sps_max_dec_pic_buffering_minus1 + 1,
capped at 16. That is not a workaround, it is what the number means: it is
exactly the bound C.5.2.2's fullness clause bumps against, and A.4.1 bounds the
total RPS entries by the same value, so `buffering` pictures hold `buffering - 1`
references plus the current one with nothing left over.
The max() that produced the 16 was written to be generous to malformed streams —
"storing their pictures beats erroring the AU" — but it never did that either.
Dpb::needs_bumping (C.5.2.2) already keys on the signalled buffering, not on
max_num_pics, so a stream referencing more pictures than it declared was ALREADY
being bumped below its own declared depth before every store. The widened limit
bought no tolerance at all; all it ever did was over-allocate hardware surfaces,
by ten pictures per session at 1080p, and on NVIDIA take HEVC away entirely.
The fix moves 720p and 1080p onto the pool shape 4K has been running in the field
all along (7 slots, 6 references), so it is not a new operating point — it is the
one already proven. max_active_references drops from 15 to 6, still above the 5
an RFI_DPB stream can name. The per-AU level gate in pf-vkdecode reads
plan.picture.level_idc directly, so dropping A-2 out of NegotiationInfo costs no
sensitivity to a mid-stream level change.
Two regression tests pin the arithmetic from both ends, because either end
drifting back reproduces this:
- h265: the field SPS synthesized byte for byte on the fields that matter must
plan 6 frames / 7 slots, all four resolutions must agree because the stream
does, and every depth the envelope gate admits must leave room for the picture
in flight. The one honest residue is pinned too and deliberately left
refusing: A.4 does let a conforming stream declare a full 16-picture DPB, and
17 slots genuinely do not fit 16, so that stream is still refused rather than
decoded with too few slots and silently corrupted references.
- pf-encode: RFI_DPB + 2 <= 16, guarding the producer end. RFI is a real
latency win and this does not cap it at today's value — there are nine slots
of headroom — it just stops it being raised past the point where clients can
no longer decode us at all.
|
||
|
|
6348334eff |
docs(client/video): the evidence table stops saying AV1 never decoded
ci / bun-nix (pull_request) Successful in 27s
ci / web (pull_request) Successful in 1m15s
ci / docs-site (pull_request) Successful in 1m19s
ci / rust-arm64 (pull_request) Successful in 1m40s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m17s
android / android (pull_request) Successful in 3m33s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m4s
ci / rust (pull_request) Successful in 5m32s
Two of its notes became false the moment the host stopped truncating AV1. native D3D11VA / AV1 said "NEVER decoded a frame on any hardware". It has now decoded 4K60 on an RTX 3500 Ada — and the same run is why the note matters: its warn line named the rung as unproven moments before it failed 72 access units running with "reference picture N holds no DPB slot". That was the host shipping half of every frame, not the rung, so the M7 wiring was right all along. It stays UNVERIFIED regardless. `verified` gates `native_rung_admitted` — whether `auto` may pick this rung ahead of Vulkan Video — and one 25-second session with no frame-hash parity and no soak does not buy that. Promoting it wants a deliberate gpu_parity-style run. The note now says what is true instead of what is convenient. software / AV1 said rav1d had "CPU unit tests only". rav1d has now run on glass: 1080p AV1 decodes, and 4K ABORTS THE PROCESS. It takes an internal error path and panics inside its own on_error (rav1d 1.1.0 decode.rs:4997, unwrap on a None frame header); the panic crosses the extern "C" boundary in dav1d_send_data, so it is panic_cannot_unwind and no rung demotion or NoSoftwareRung refusal can catch it. libdav1d decodes the same 4K stream 715/715, so this is rav1d's own defect and is recorded where the next person to reach that rung will see it. |
||
|
|
7cab7ae6bc |
feat(client/android): say when a captured pad's gyro can't reach the session
G8's Android half, and the last of the three clients. Same failure as the other two: a controller with a gyro, in a session whose virtual pad has no motion plane, does nothing when tilted — silently, with no way from the couch to tell that apart from a broken sensor. The fix is the Controller type setting, so the notice names it. Android read neither the requested nor the resolved backend, so this needed a plumb. What it did NOT need was a third copy of the rule. `nativePadMotionReaches` takes the kind a pad declared and answers off `pad_motion_reaches` in punktfunk-core, where the argument and the tests already live. The rule is subtler than it looks — the host builds each pad from its OWN declaration and folds what it cannot build, so neither the declaration nor the session echo answers it alone — and every way of getting it wrong is silent. A Kotlin transcription would have been a third thing to keep in step with the host, which is exactly how the SDL half got it wrong the first time. Asked once per pad, at claim, in `openExternal` — where the pad's kind is already being declared to the host — and the answer held for the pad's lifetime on the `ExternalPad`. Not per sample: this runs at a DualSense's full report rate. `hasGyro` gates only the NOTICE, and defaults to false. `DsCapture` passes true — every pad it captures is a Sony one whose IMU is a headline feature, forwarded on the rich plane. `Sc2Capture` keeps the default, because the Steam Controller 2's motion rides inside the opaque passthrough report that `hidReport` carries, which nothing here may second-guess: warning about motion for a pad that never calls `motion()` would be a notice about a feature the player never lost. The suppression itself is on `motion()` regardless, where it costs a dead pad nothing and stops a live one paying to send samples the host will decode and discard. The notice sits at the BOTTOM of the stream overlay, unlike the mic-chord confirmation at the top. The two can coincide — a pad is claimed at roughly the moment someone might be muting — and one landing on the other would cost the user both. It holds 6 s rather than the mic chord's 1.6: that one confirms something the user just did, this one explains something they did not, in a sentence they have to read. Nulled at teardown beside `onExitArmed`/`onMicChord`, for the same reason those are — a slot closing during release must not poke Compose state on the way out. Not covered by tests, and this is a limit of the module rather than a choice: `GamepadRouter` needs Android plus a live JNI handle, there is no Robolectric here, and the predicate it defers to is pure Rust that already has its table. So the parts that carry the reasoning are argued in comments, as `DsCapture`'s claim/teardown ordering already is. What IS mechanically verified is the piece that a compiler cannot catch and a device would fail on: the JNI symbol `Java_io_unom_punktfunk_kit_NativeBridge_nativePadMotionReaches` is present and global in the built arm64-v8a `.so`, so the `external fun` resolves rather than throwing `UnsatisfiedLinkError` at the first pad. Gate: `:kit:compileDebugKotlin`, `:kit:testDebugUnitTest` (62 cases, 0 failed, read out of the JUnit XML rather than inferred from a green build — unchanged from this branch's previous count), `:app:compileDebugKotlin` and `:app:testDebugUnitTest` (67 cases, 0 failed), with `:kit:cargoNdkRelease` rebuilding the JNI crate clean across all three ABIs, plus `cargo fmt --check` on it. On-glass verification is owed on the rig the earlier legs used, and is worth doing as one pass with the two already owed there. |
||
|
|
aaa58ad817 |
feat(client/apple): say when a pad's gyro can't reach the session, and stop powering it
G8's Apple half — the UI hint
|
||
|
|
ebc2f1cf92 |
feat(host/audio): the minted microphone returns to tier-0 — pitch-true
ci / web (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 2m18s
android / android (pull_request) Successful in 3m27s
ci / rust-arm64 (pull_request) Successful in 4m24s
ci / rust (pull_request) Canceled after 7m47s
The revert un-reverts, on measurement: with the per-direction stamp sets (render = the pad-proven PCM16-device/float-mix stereo split, capture = device-format only), micpitch reads 440 Hz in as 440 Hz out at exact peak. The octave-low voice was the driver DEFAULT endpoints disagreeing (stereo render vs mono capture), never a raw-crossing design. The user called the wrong verdict — the pad program 4ch success was the counter-evidence that reopened the case. |
||
|
|
c16e07d746 |
fix(encode/nvenc): AV1 stops shipping half a frame
Every 4K AV1 frame this host encoded reached the wire truncated to its first tile, and had since AV1 was wired up. Measured on .21 (RTX 5070 Ti, 4K60, split AUTO): each access unit carried a frame header declaring two tile rows and a single Tile Group OBU with tg_start = tg_end = 0, so libdav1d rejected 835 of 836 AUs with "Error parsing frame header". NVIDIA's hardware decoder accepts the truncated stream, which is why native Vulkan Video looked healthy at 60 fps while both conformant software decoders — rav1d in-tree and libdav1d out-of-tree — refused every frame and clients fell to a black screen. The two halves of sub-frame readback are armed by different conditions. build_init_params arms the WRITER (enableSubFrameWrite + reportSliceOffsets) from subframe_on alone; the chunked READER additionally requires slices >= 2, and resolve_slices returns 1 for AV1 unconditionally — before the PUNKTFUNK_NVENC_SLICES override is even read, because AV1 partitions via tiles rather than slices. So an AV1 session asked the driver to publish its output tile by tile and then took only the first tile with one blocking lock_bitstream. resolve_split_subframe — the one arbitration point both direct-SDK backends already call — now disarms sub-frame for AV1 and returns split_mode untouched, so AV1 keeps every engine split encode gives it. Arming the reader instead is not a drop-in alternative: poll_chunk cuts at bitstreamSizeInBytes on the reasoning that "slices are contiguous Annex-B", which AV1's OBUs are not. With sub-frame disarmed and split still AUTO, the same session decodes 654/654 frames clean through libdav1d. The test that pinned this as correct (av1_untouched, "both features are legal together") is replaced by one that pins the disarm, and by one that checks the reader's gate against the writer's — the comparison nothing made. The Linux latch comment claiming the two "can't disagree" is corrected; that claim is what made this invisible. |
||
|
|
52cd42e911 |
fix(host/audio): capture endpoints get the device format only
Live bisect on a fresh endpoint: the mix/host format keys are RENDER-engine properties — stamped onto a capture endpoint they broke its shared-mode graph (IsFormatSupported reported 2ch/48k OK while Initialize failed 0x88890008 on a once-stamped fresh endpoint; unstamped it opened fine, S3). The capture now gets ONLY the device-format key — the knob mmsys.cpl itself writes — declaring the stereo the pins actually accept. |
||
|
|
5c4969fd6b |
fix(client/pads): the gyro cut-off asked about the session, not the pad
Supersedes the check |
||
|
|
8c274d6256 |
fix(host/devtest): the probe asks stereo — its mono ask WAS the unopenable endpoint
Measured resolution of the 0x88890008 mystery: IsFormatSupported said the capture accepts 2ch/48k shared while Initialize kept failing — because the probe itself had switched to a MONO ask for frequency counting, and this stack does not bridge channel counts on capture even under autoconvert. Every unopenable-endpoint verdict after that switch was the instrument, not the endpoint. Stereo ask restored; crossings counted on channel 0. |
||
|
|
16e506f943 |
feat(host/devtest): audio-probe micpins — the driver-capability map
Exclusive+shared IsFormatSupported across {1,2}ch x {16,32}bit x
{44.1,48,96}kHz on both minted mic pins. Interrogates the DRIVER,
bypassing every endpoint-store stamping question: what the pins truly
accept decides whether the mic leg has any coherent configuration, and
whether an exclusive-mode mono open is an escape hatch. (The pad program
made its own breakthrough with exactly this instrument on the sibling
SSS driver.)
|
||
|
|
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 |
||
|
|
2eed9823e5 |
fix(host/audio): the mic pair gets the pad-proven coherent stereo stamp set
The user challenged the format-locked-pins verdict, and the pad program is the counter-evidence: it hit the SAME 0x88890008 unopenable-endpoint signature and cured it with a COHERENT stamp set, after which the same driver family served 4ch happily. This branch previous attempts were contaminated twice over — a float device-format (the pad bisect proved the split must be PCM16 device / float mix+host) and no AudioEndpointBuilder restart (Restart-Service Audiosrv never touches its dependency, so endpoint configs were never rebuilt). Both mic endpoints now get one identical coherent stereo set; the octave-low hypothesis shifts from "raw crossing by design" to "the two endpoint stores disagreed (stereo render default vs mono capture default)". |
||
|
|
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. |
||
|
|
8e8d30202c |
fix(client/android): a Sony pad's buttons no longer wait on its calibration
Supersedes the parse gate in |
||
|
|
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 |
||
|
|
26b0819f5c |
fix(client/android): plugging in a Sony pad could hitch the interface
Supersedes the synchronous calibration read |
||
|
|
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. |
||
|
|
cc53b3d6b0 |
fix(host/audio): unwire the minted microphone — the driver mic path is unusable
Final pitch-probe verdict on the SSM driver pair: the render pin is stereo-only, the capture pin mono-only (stamping either differently makes the endpoint unopenable), and the crossing between them is a RAW byte pass — so voice fed through the render endpoint reads back an octave low and no format stamp can fix it. S3 peak-based PASS = false pass; per the design doc revert clause the mic falls back to the name ladder (a virtual cable), pending the user re-decision. The SPEAKERS substrate keeps tier-0 (no driver crossing — a plain engine loopback tap, measured clean). minted_ids() publishes speakers only; the mic endpoints stay minted and recorded (provisioned()) for the micpitch probe and a possible future non-render transport, and their format stamps now pin each side to its pin one true format — healing the endpoints this branch earlier mis-stamped. |
||
|
|
5922cbe325 |
fix(host/audio): the minted mic pair declares stereo on BOTH sides
Second measurement round: the driver render pin is STEREO-ONLY — the mono render stamp turned the endpoint unopenable (0x88890008 on every open, the incoherent-stamp signature the pad program documented). Since the crossing is raw, the coherent choice inverts: the CAPTURE side now declares the stereo float stream that actually crosses (fixing the octave-low voice), and the render has its stereo float default stamped explicitly — pinning the pair AND healing any endpoint a previous build left mono-stamped. |
||
|
|
ed98814145 |
fix(host/audio): the minted microphone renders MONO — voice was an octave low
Measured with the new pitch probe: 440 Hz into the minted mic render came back as 220 Hz off its capture side. The driver forwards the render stream RAW into its mono capture, so a stereo-declared render (the driver-default we inherited) turns every stereo frame into two mono samples — half speed, octave down, exactly the field report. The mic render now gets a coherent MONO 48 kHz format set stamped alongside its name (PCM16 device format + float mix/host formats), making the engine downmix before the driver crossing. The mic pump keeps pushing stereo; shared-mode autoconvert handles the rest. |
||
|
|
0cf76af0c2 |
feat(host/devtest): audio-probe measures pitch, not just peaks
Field report through the minted microphone: voice plays back an octave low. Peaks are pitch-blind — S3 passed while a potential half-rate link hid in the numbers (288k samples fits both the honest and the half-speed story). Every probe measurement now estimates the dominant frequency by zero crossings over the signal span, and `audio-probe micpitch` runs the decisive experiment against the LIVE minted pair: 440 Hz in, frequency out — ~440 = pair innocent, ~220 = the stereo render stream is forwarded raw into the mono capture. |
||
|
|
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. |
||
|
|
f6de620f34 |
fix(client/android): a captured Sony pad's gyro turned the wrong amount
G14/G16 leg 3. This supersedes the nominal constant |
||
|
|
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. |