55aba3e936ee4aef5058eb1878c01336e3408e60
2548
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
3608de25ed |
Merge pull request 'fix(ci): runner hygiene stops eating its own jobs' (#84) from worktree-ci-runner-hygiene into main
ci / bun-nix (push) Successful in 27s
ci / docs-site (push) Successful in 1m20s
apple / swift (push) Successful in 1m25s
ci / web (push) Successful in 1m28s
deb / build-publish-client-arm64 (push) Successful in 1m36s
ci / rust-arm64 (push) Successful in 3m31s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
decky / build-publish (push) Successful in 43s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
deb / build-publish (push) Successful in 3m53s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
deb / build-publish-host (push) Successful in 4m3s
docker / builders-arm64cross (push) Successful in 10s
android / android (push) Successful in 6m31s
apple / screenshots (push) Successful in 5m45s
windows-host / package (push) Failing after 7m11s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
arch / build-publish (push) Successful in 8m0s
ci / rust (push) Successful in 10m1s
docker / deploy-docs (push) Successful in 28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m33s
Reviewed-on: #84 |
||
|
|
138a1f1b2f |
fix(host/windows): the staging-dir SID checks document their unsafe blocks
ci / web (pull_request) Successful in 1m9s
ci / docs-site (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m34s
ci / bun-nix (pull_request) Successful in 22s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 4m4s
android / android (pull_request) Successful in 4m28s
ci / rust (pull_request) Successful in 9m12s
clippy's undocumented_unsafe_blocks (deny) flagged the three blocks that
|
||
|
|
39cfb7234c |
fix(ci/docker): a cache-hit builders job stops failing on a login it never uses
The LAN-registry docker login only serves the Push step (Reconcile and Tag-for-release authenticate via curl -u), but it ran unguarded — so a hit=true leg landing on a host with a misconfigured docker daemon failed at login with nothing to push (run 16044/16013 f44 leg). Gate it like Build/Push. |
||
|
|
c3cdee9bf5 |
fix(ci/prune): the 2-minute image prune stops deleting images mid-pull
docker image prune -af --filter until=2h keyed on image CREATION time, so a
base image built days ago that merely had no container at that instant was
"aged" — including one a job had just pulled and not yet created. Measured
2026-08-07: three job failures, each coinciding with a prune tick to the
second ("No such image: …punktfunk-rust-ci:latest", every step cancelled),
plus a 4-7 GB re-pull of every idle base image within minutes.
The routine tick now retires only what this host actually accretes — per-SHA
app tags older than 2h (their creation time IS the local build time) — then
sweeps dangling layers, which cannot touch a tagged image. The blanket -a
prune survives only in the near-ENOSPC burst guard, where one re-pull beats
every concurrent job dying.
docker-reclaim.{sh,service,timer} are the hourly leak reclaimer that so far
lived hand-installed on home-runner-1 only; home-runner-2 went without it and
re-accumulated 176 leaked volumes (~60 GB) until jobs died of ENOSPC on
2026-08-06/07. Checked in so both hosts install the same files from here.
|
||
|
|
93b8528d09 |
Merge pull request 'fix(encode): NVENC split-frame encode never engaged for HDR — engage it, measured' (#83) from worktree-nvenc-s1-split-reconfigure into main
ci / bun-nix (push) Successful in 24s
ci / web (push) Successful in 1m4s
ci / docs-site (push) Successful in 1m16s
apple / swift (push) Successful in 1m35s
deb / build-publish-client-arm64 (push) Successful in 1m34s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 8s
windows-host / canary-manifest (push) Skipped
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 6s
ci / rust-arm64 (push) Successful in 3m30s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 6s
docker / builders-arm64cross (push) Skipped
deb / build-publish (push) Successful in 3m55s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m3s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m37s
deb / build-publish-host (push) Successful in 4m56s
android / android (push) Successful in 6m46s
apple / screenshots (push) Successful in 5m37s
windows-host / package (push) Failing after 5m23s
windows-host / winget-source (push) Skipped
arch / build-publish (push) Successful in 7m56s
ci / rust (push) Successful in 9m35s
docker / deploy-docs (push) Successful in 6m27s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m45s
Reviewed-on: #83 |
||
|
|
b5205fef52 |
Merge pull request 'fix(client/apple): audio stops crackling on lossy, bunching Wi-Fi' (#82) from worktree-audio-wifi-distortion into main
ci / rust-arm64 (push) Failing after 13s
arch / build-publish (push) Failing after 22s
ci / bun-nix (push) Successful in 21s
apple / swift (push) Successful in 1m32s
ci / web (push) Successful in 1m26s
ci / docs-site (push) Successful in 1m27s
deb / build-publish-host (push) Failing after 1m26s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 1m2s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 8s
docker / builders-arm64cross (push) Skipped
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m28s
deb / build-publish (push) Successful in 4m33s
android / android (push) Successful in 5m53s
docker / deploy-docs (push) Successful in 37s
deb / build-publish-client-arm64 (push) Successful in 4m31s
ci / rust (push) Successful in 6m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 4m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m47s
flatpak / build-publish (push) Failing after 4m28s
windows-host / package (push) Failing after 6m36s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
release / apple (push) Successful in 11m44s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m24s
apple / screenshots (push) Successful in 6m5s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m15s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m57s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 12m58s
Reviewed-on: #82 |
||
|
|
515a3c2912 |
feat(pf-encode): wire split arbitration on Windows too
ci / rust (pull_request) Failing after 26s
ci / bun-nix (pull_request) Successful in 46s
ci / web (pull_request) Successful in 1m19s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m35s
ci / rust-arm64 (pull_request) Successful in 2m10s
android / android (pull_request) Successful in 4m22s
The last coverage gap, and only worth building once S1 proved it possible: the
Windows backend drives NV_ENC_DEVICE_TYPE_DIRECTX, and an in-place splitEncodeMode
change had never been tested there. It works (
|
||
|
|
b27135308f |
fix(scripts): the xcframework never absorbs a Homebrew libopus
ci / docs-site (pull_request) Failing after 2s
ci / web (pull_request) Failing after 5s
android / android (pull_request) Failing after 14s
ci / bun-nix (pull_request) Successful in 22s
apple / swift (pull_request) Successful in 1m41s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m52s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 5m45s
ci / rust (pull_request) Failing after 7m5s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m22s
On a Mac with brew's opus installed, audiopus_sys found it via pkg-config and statically linked it into the aarch64 slice — a lib built for the RUNNING macOS (minos 26.0, tripping the script's own version guard) and existing only for the host arch, so the x86_64 slice silently fell back to the vendored build and the two slices shipped different libopus builds. Force the vendored CMake build for every slice (OPUS_NO_PKG_CONFIG=1), with the CMake policy floor modern CMake (>=4) needs to accept libopus's old cmake_minimum_required. |
||
|
|
64c92da356 |
fix(client/apple): the jitter ring deepens on the sessions that actually starve
The shared JitterPolicy grew an adaptive target floor — clustered genuine underruns raise the live target a step at a time up to max_target_ms, a long quiet spell relaxes it back — and the three Rust rings all run it via note_read. The Apple ring is the one hand-written mirror, and it mirrored the shed half but not the growth half: its target was pinned at the 20 ms base forever. On Wi-Fi that bunches arrivals (power-save is the classic; the field MacBook report is the symptom), 20 ms is regularly shorter than one delivery stall, so the ring re-primed through every stall for the whole session — crackle that never got better, on exactly the client where a Moonlight with a deeper buffer sounds fine on the same host and network. The ring now carries the full mirror of note_read: 3 underruns inside a 5 s window grow the target 10 ms (capped at COREAUDIO's 70), 30 s of quiet gives a step back, and the write-side hard trim follows the grown target (including the Rust policy's target+quantum guard, which the mirror also lacked). New tests pin the mirror to the Rust suite's expectations — growth, relax, the cap — plus the field scenario end to end: bunched 60 ms deliveries with every fourth burst 30 ms late converge to a silence-free tail instead of crackling forever. |
||
|
|
81d257c7fa |
fix(client/audio): the in-core decoder conceals lost packets like every other client
A field report: game audio on a MacBook (M1) crackles over Wi-Fi against a host that plays clean to other clients. The Apple client is the one client whose Opus decode lives in core (punktfunk_connection_next_audio_pcm — AudioToolbox has no multistream path), and that decoder only ever decoded packets that ARRIVED. The Linux, Windows and Android decode loops all feed an AudioGapTracker and synthesize libopus packet-loss concealment for every packet the wire lost; the in-core path had the tracker sitting unused in the same crate. So on Apple every lost 5 ms datagram — at ~200 packets/s over Wi-Fi, a steady trickle — landed in the playout ring as a hard time-domain gap: a click per loss, sustained crackle under real loss. The redundant-plane recovery (0xD2) hides single losses when the host grants it, which is exactly why the survivors are the burstier gaps that need concealing most. The decode now runs through the same accounting as everyone else: concealed frames land in front of the arriving frame in one contiguous buffer (the embedder just writes it to its ring), a DTX marker advances the accounting without being decoded, and the output buffer is pre-sized for a full concealment run so the borrow-until-next-call pointer can never dangle. Unit-tested against real libopus: gaps, duplicates, DTX-after-loss, and the 50 ms cap. |
||
|
|
071358cbf7 |
test(pf-encode): S1 on WINDOWS/D3D11 — passes; Windows arbitration is buildable
Everything the split-encode programme rests on had been proven only on
Linux/CUDA. The Windows backend drives NV_ENC_DEVICE_TYPE_DIRECTX, so none of it
transferred by assumption -- and if the driver refused an in-place split change
there, Windows arbitration would simply not be buildable.
RESULT on the RTX Windows box (RTX 4090 / AD102, driver 610.88, D3D11):
engines=2, latched by query_caps (WP1.1's probe, validated on Windows
hardware rather than inferred from Linux)
DISABLE -> TWO_FORCED via nvEncReconfigureEncoder, resetEncoder=0: ACCEPTED,
ZERO IDRs, and the reverse likewise.
So the foundation now holds across three platform x arch x driver combinations:
Linux/CUDA Blackwell 610.57.04, Linux/CUDA Ada 610.43.03, Windows/D3D11 Ada
610.88.
⭐ UNBLOCKS ALL FUTURE WINDOWS ON-HARDWARE TESTING. pf-encode's nvenc test
binaries were believed unlinkable on Windows ("NvEncodeAPICreateInstance
unresolved", recorded as pre-existing and worked around by only ever running
clippy there). They link fine given the SDK import library:
RUSTFLAGS='-L native=C:\Users\Public\nvenc -l nvencodeapi'
`-L` alone is not enough -- without a `-l` nothing pulls the archive in, which is
why the earlier attempt still failed. ⚠ This is TEST-BINARY-LOCAL and must stay
that way: production deliberately dlopens NVENC rather than link-loading it, and
an unconditional link-load is the known crash class on non-NVIDIA Windows hosts.
⚠ Box note: the RTX Windows box answers on .158, not the .173 in its memory
entry, and `Administrator@` there resets the connection right after
SSH2_MSG_SERVICE_ACCEPT in a way that reads like the host being down -- the
working login is "Enrico Bühler"@192.168.1.158.
|
||
|
|
0430d907bb |
fix(pf-encode): gate forced_split_width to Linux — WP4 broke the Windows build
The verification gap flagged in
|
||
|
|
01294e3a53 |
refactor(pf-encode): WP4 — one split policy, shared with the libav path
The libav NVENC path carried its own inline copy of the split decision and had
already drifted from the direct-SDK selector: it hard-coded a 2-way split
regardless of engine count, and had no depth rule at all. That is the drift the
shared resolver was extracted to prevent, and the copy quietly reintroduced it.
Routing it through `resolve_split_mode` needed the policy to MOVE. `nvenc_core`
is gated on `feature = "nvenc"`, but the libav path is precisely the build where
that feature is OFF (`PUNKTFUNK_NVENC_DIRECT=0`, and the featureless packages --
the packaging gap this project has been bitten by before). So
resolve_split_mode / max_forced_split_mode / clamp_to_engines, plus a new
`forced_split_width`, now live in `codec.rs`, which is always compiled and
already owned SPLIT_FORCE_PIXEL_RATE.
That means the NV_ENC_SPLIT_ENCODE_MODE values had to be hand-written as plain
constants, since the SDK enum does not exist without the feature. They are
therefore pinned: `nvenc_split_constants_match_the_sdk` (feature-gated, the only
place both are visible at once) asserts all five against the real enum, so the
copies cannot rot.
⚠ Only the FORCED outcomes are actionable on the libav side -- libavcodec's
`split_encode_mode` AVOption is its own vocabulary and our DISABLE is the NVENC
enum's 15, which would be meaningless there. DISABLE/AUTO both map to "leave the
option unset", which is exactly today's behaviour (unset = the driver's auto).
`engines = 0` ("not probed") maps to 2-way, preserving what that site always did;
a 3-NVENC part gets the wider split only on the direct-SDK path, which is the one
that actually probes.
⚠⚠ VERIFICATION GAP: .133 went down mid-change (no ping), so the WINDOWS leg is
UNVERIFIED. This matters more than usual -- the Windows backend imported
resolve_split_mode from nvenc_core and that import had to move too, which a grep
caught rather than a compiler. Re-run before trusting it:
cargo clippy -p pf-encode --features nvenc --all-targets -- -D warnings
Verified .21: clippy -D warnings clean BOTH with and without the nvenc feature
(the featureless build is the whole point of the move) and with
nvenc,vulkan-encode; 65 unit tests incl. the new constant-parity test; 25/25
NVENC on-hardware; punktfunk-host clippy clean. fmt clean.
|
||
|
|
1062aa780f |
test(pf-encode): measure the bits/frame curve — no crossover, split always wins
WP0's real deliverable, and the hole every previous measurement in this
programme had. All prior timings ran against driver-zeroed buffers, so rate
control had nothing to code (~300 B/AU against an 833 KB quota) and only the
PIXEL-proportional half of the encode cost was ever exercised -- while the 4K60
HDR field report was a BITS/FRAME problem at 6.8 Mbit/frame.
Adds `pf_zerocopy::cuda::write_plane_from_host`, the exact mirror of the existing
read_plane_to_host. No new loader entry was needed: cuMemcpy2DAsync_v2 was
already in the table and CUDA_MEMCPY2D just needed the reverse memory types.
Linux-only by construction (pf-zerocopy's `imp` is cfg'd to linux).
⚠ Two harness mistakes found and fixed by looking at bytes/AU rather than
trusting the knob:
- Pure per-pixel noise is INCOMPRESSIBLE, so a low bitrate target does not
produce low bits/frame -- it OVERSHOOTS. At a nominal 50 Mbps the encoder
emitted 719 KB/AU against a 104 KB quota, and the three lowest rows of the
first sweep all sat at the same ~5.7 Mbit/frame. Sweeping nominal bitrate
measures nothing.
- So the sweep moves CONTENT DETAIL (block size) instead, and the x-axis is the
bits/frame the encoder ACTUALLY produced, never the one requested.
4K60 HEVC 8-bit, real content, single-engine vs forced-2:
bits/frame Ada 4090 Blackwell 5070 Ti
0.2-0.3 Mb 4567 -> 2381 1.92x 5549 -> 3552 1.56x
~1.1-1.2 Mb 5060 -> 2626 1.93x 5867 -> 4082 1.44x
~3.3 Mb 8478 -> 4455 1.90x 9286 -> 5862 1.58x
~9.6 Mb 16237 -> 8114 2.00x 16435 -> 9275 1.77x
RESULTS. (1) Encode time scales strongly with bits/frame -- 4.6 ms to 16.2 ms
across the range on Ada -- confirming the hypothesis' core claim. (2) There is NO
CROSSOVER: split wins at every point on both architectures (Ada ~1.9-2.0x and
notably flat, Blackwell 1.44-1.77x). So the arbitration's encode-side answer is
essentially always "split", which makes the sub-frame handicap the only decision
that actually matters -- exactly the part already built and unit-pinned.
(3) It corroborates the field capture: at ~6.8 Mbit/frame these curves put
single-engine 4K60 around 10-13 ms, and the field report was 10.3 ms on a 4090.
That reads as real ASIC time, not the retrieve-queue inflation it might have been.
⚠ Caveat the data itself shows: cost is NOT monotonic in bits/frame alone. The
1px row lands at the HIGHEST bits/frame yet encodes FASTER than the 4px row on
both boxes (Ada 10148 vs 16237 us) -- pure noise defeats motion estimation, which
gives up early, where semi-structured content makes it search hard. Content
structure is a real term, so "bits/frame" is a good axis but not a complete cost
model.
Verified .21: clippy -D warnings clean (pf-encode + pf-zerocopy), 64 unit tests,
25/25 NVENC on-hardware. Curves run on both Ada and Blackwell. fmt clean.
|
||
|
|
50b3fd1012 |
fix(pf-encode): drop the 10-bit short circuit — measured wrong on Ada, twice
WP1.3, and the measurement that justifies it. `resolve_split_mode`'s 10-bit rule sat ABOVE the pixel-rate arm and took no codec, so it (D1) vetoed 10-bit 4K120 -- the very case the pixel-rate arm exists for -- and (D2) applied an HEVC-Main10-on- Ada result to AV1 10-bit, which has no such measurement. Both fixed: the pixel-rate arm now comes first, and what remains is codec-scoped to HEVC and only applies BELOW that bar, where a second engine buys nothing anyway. The rule rested on one datapoint: 5120x1440@240 Main10 on Ada, forced-2 7.6 ms vs 2.8 ms single-engine -- split 2.7x SLOWER. Dropping the short circuit flips that exact configuration's behaviour, so it was re-measured on a 4090 (AD102, driver 610.43.03), 400 Mbps, sub-frame pinned off, via a new mode-parameterizable Main10 A/B test (PF_AB_MODE=WxHxFPS reproduces the original operating point). Ada 4090 single forced-2 ratio 3840x2160@60 4483 us 2178 us 2.06x split WINS 5120x1440@240 3689 us 2813 us 1.31x split WINS <- the veto's origin 3840x2160@120 4148 us 2189 us 1.89x split WINS Blackwell 5070 Ti 3840x2160@60 4216 us 2477 us 1.70x split WINS 5120x1440@240 4651 us 3894 us 1.19x split WINS Split wins for Main10 at every mode on BOTH architectures, including the config the veto came from. The original number does not reproduce. ⚠ Caveats, unchanged from the rest of this work: content is trivial (297-300 B/AU against an 833 KB CBR quota -- zeroed VRAM), so this is the pixel-proportional term and the bits/frame regime is still unmeasured; debug build; and the driver differs from whenever the original was taken. Also validated on Ada in the same session -- the whole spike set reproduces on a SECOND architecture and an OLDER driver (610.43.03 vs 610.57.04): S1a in-place split switch accepted with zero IDRs both directions; S1b takes effect (|C-B|=12 vs |C-A|=1921, the cleanest run yet); S1c pair flip passes; D5 confirmed (AUTO+sub-frame 4424 vs DISABLE 4409, 15 us apart -- and AUTO without sub-frame 2310 ~= TWO_FORCED 2314, so the arm stays); engines=2 with THREE_FORCED correctly clamped to mode 2; arbitration converged with exactly 1 keyframe. Verified: .21 clippy -D warnings clean + 64 unit tests; .133 Windows clippy -D warnings clean (the resolver signature grew a `codec` param, so both backends moved); Ada + Blackwell on-hardware as above. fmt clean. |
||
|
|
2366c4fe31 |
feat(pf-encode,host): price the HEVC sub-frame trade so arbitration can cover it
The named next step after WP3's first increment. That increment deliberately REFUSED to arbitrate HEVC-with-sub-frame -- the fleet default, and the reported field case -- because engaging split there gives up sub-frame readback, whose whole value is that the send overlaps the encode. An encoder measuring only encode time would see split as ~2x faster, take it, and make end-to-end latency worse while reporting a win. This supplies the missing number. The real comparison is encode_1eng + send_of_last_slice against encode_2eng + send_of_whole_AU, so the challenger owes roughly spread x (slices-1)/slices. Split across the two sides that can each see half: - Host: new `Encoder::set_send_spread_us` (defaulted, forwarded by TrackedEncoder -- same trap class as set_wire_chunking, and unforwarded it would fail SILENTLY IN THE SAFE DIRECTION, which is the hardest kind to notice). The send thread is the only place a paced send is observed and the encode loop the only place the encoder can be touched, so it goes over an AtomicU32 like encoder_ceiling_kbps, EWMA-smoothed 3:1 per completed AU: one content spike must not flip a verdict that then gets cached. - Encoder: turns the raw spread into the handicap, because only it knows `slices`. SplitArbiter::with_handicap charges it to the challenger before the comparison. A unit test runs identical encode numbers with a cheap and an expensive send and asserts the verdict REVERSES -- with an expensive send the arm that looks twice as fast is a loss end to end, and the incumbent must hold. That is precisely the regression an encode-only arbiter ships. Gate now opens for HEVC+sub-frame only when a spread has actually been reported (and slices >= 2); with no hint it still refuses, so behaviour is unchanged until the host feeds it. Two mechanics this needed: - apply_split_mode became a PAIR flip (split + sub-frame), routed through resolve_split_subframe and restoring from `subframe_opened_with` so a session that never had sub-frame can never gain it. It also recomputes `subframe_chunks`, which reconfigure_bitrate does NOT -- spike S1c's finding; leave it stale and supports_chunked_poll keeps saying yes while numSlices never advances, so poll_chunk busy-polls its whole budget every AU. - The arbiter is now fed from BOTH completion points. A sub-frame session finishes through poll_chunk, so the incumbent arm of an HEVC experiment would otherwise never deliver a sample -- only the challenger, with sub-frame dropped, comes through poll. Verified .21: clippy -D warnings clean for pf-encode AND punktfunk-host with nvenc, 63 unit tests (1 new), 23/23 NVENC on-hardware green. Verified .133: Windows clippy -D warnings clean, zero dead_code. fmt clean. |
||
|
|
f34f311c69 |
Merge pull request 'fix(brand): capitalize "Punktfunk" in user-facing text; name the legal copyright holder' (#81) from worktree-docs-title-casing into main
apple / swift (push) Successful in 1m35s
ci / bun-nix (push) Successful in 24s
ci / docs-site (push) Successful in 1m17s
ci / rust-arm64 (push) Successful in 2m23s
ci / web (push) Successful in 1m43s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
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 17s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
arch / build-publish (push) Failing after 4m37s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m36s
deb / build-publish-host (push) Successful in 4m21s
apple / screenshots (push) Successful in 6m3s
deb / build-publish-client-arm64 (push) Successful in 5m7s
nix / flake (push) Failing after 3m5s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 10s
docker / builders-arm64cross (push) Skipped
decky / build-publish (push) Successful in 45s
ci / rust (push) Failing after 7m22s
deb / build-publish (push) Successful in 5m11s
android / android (push) Successful in 14m37s
docker / deploy-docs (push) Successful in 6m48s
release / apple (push) Successful in 9m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m56s
windows-drivers / probe-and-proto (push) Successful in 2m0s
windows-drivers / driver-build (push) Successful in 9m47s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 17m8s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 6m42s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 11m31s
windows-host / package (push) Failing after 49m58s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 15m1s
Reviewed-on: #81 |
||
|
|
3b283dc26e |
feat(pf-encode): WP3 — live split arbitration, measured on the session, no IDR
The fix S1 unlocked. Rather than predict the right split mode at open — which
cannot work, because the decision depends on bits/frame and an Automatic client's
steady-state bitrate is unknown at open (ABR climbs in place afterwards) — the
encoder now measures both arms on the live session and keeps the winner. S1
proved nvEncReconfigureEncoder takes a changed splitEncodeMode with
resetEncoder=0, emits no IDR, and actually applies it, so the experiment is
invisible on the wire.
Deliberately measures instead of modelling: hard-coded per-arch constants are
exactly how the rule this replaces went wrong (one 5120x1440@240 Ada datapoint
generalised into a fleet-wide 10-bit veto). A measurement tracks driver updates
for free.
`SplitArbiter` (pure state machine, unit-tested without a GPU): measure incumbent
-> switch -> SETTLE -> measure challenger -> keep the winner, else switch back.
Verdicts cache per (gpu, codec, mode, depth, chroma) so later sessions open
straight into the winning arm; the key is CeilingKey minus split_mode, since the
split mode is the thing being decided.
⚠ SETTLE_FRAMES=16 is load-bearing, not padding: split-encode does not reach
steady state on the first frame (a FRESH TWO_FORCED session measured early-half
3280us vs late-half 1996), so judging an arm right after switching reads the
transient — intermittently, which would then be cached. A unit test feeds exactly
that transient and asserts the arbiter still sees the steady state.
Safety gates, all correctness conditions rather than preferences: opt-in
(PUNKTFUNK_NVENC_SPLIT_ARBITRATE=1) while it earns trust; an operator
PUNKTFUNK_SPLIT_ENCODE pin always wins; skip if a verdict is already cached; sync
depth-1 only (async_rt.is_none(), same gate chunked poll uses — under pipelined
retrieve the submit->AU span includes queue depth and the comparison is noise);
needs >=2 engines; never H.264.
⚠ And the one that bounds this increment: NO SUB-FRAME TRADE. For HEVC, forcing
split gives up sub-frame readback, which costs send/encode overlap the ENCODER
CANNOT SEE — it measures encode time only, so it would reliably prefer split and
silently make end-to-end latency worse. So arbitration runs only where nothing is
traded: sub-frame already off, or AV1 (both features legal). Pricing that trade
needs the host's send cost and is the next work package.
Challenger choice tests the question worth asking — anything not already the
widest forced split is challenged BY the widest ("are we leaving engines idle?").
The naive "challenge whatever we are not" spent the experiment re-proving that
splitting beats not-splitting, while parking the session on the slow arm to do
it, because 4K60 sits on the fallthrough AUTO.
⚠ Every new nvenc_core item is linux-gated: the arbiter is wired into the Linux
backend only for now and nvenc_core compiles on Windows too. Caught by the .133
check, not by reasoning — the first cut failed Windows clippy with 12 dead_code
errors, the exact item-level trap this file already carries a scar from.
Verified .21: clippy --features nvenc --all-targets -D warnings clean, 62 unit
tests (4 new arbiter tests), 23/23 NVENC on-hardware green including a new
end-to-end convergence test asserting ZERO extra IDRs and a cached verdict.
Verified .133: Windows clippy -D warnings clean, zero dead_code. fmt clean.
|
||
|
|
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. |
||
|
|
9c7409ea40 |
Merge pull request 'fix(web/plugins): a plugin's interface loads again, instead of an empty panel' (#80) from worktree-plugin-ui-frame-ancestors into main
ci / bun-nix (push) Successful in 29s
ci / docs-site (push) Successful in 1m10s
ci / web (push) Successful in 1m10s
ci / rust-arm64 (push) Successful in 1m25s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 14s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 22s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 23s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 58s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m35s
deb / build-publish-client-arm64 (push) Successful in 3m33s
deb / build-publish (push) Failing after 2m55s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 10s
docker / builders-arm64cross (push) Skipped
deb / build-publish-host (push) Successful in 5m37s
ci / rust (push) Successful in 6m37s
arch / build-publish (push) Successful in 8m50s
docker / deploy-docs (push) Failing after 6m26s
nix / flake (push) Canceled after 15m6s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14m48s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 14m28s
windows-host / package (push) Canceled after 17m47s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
Reviewed-on: #80 |
||
|
|
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. |
||
|
|
185a69c806 |
fix(web/plugins): a plugin's interface loads again, instead of an empty panel
ci / bun-nix (pull_request) Successful in 20s
ci / docs-site (pull_request) Successful in 1m5s
ci / web (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 1m49s
ci / rust (pull_request) Successful in 6m23s
nix / flake (pull_request) Failing after 12m37s
Two separate things had to be wrong for this, and both were. The frame's own policy locked it out. Plugin UIs moved to their own origin so a plugin cannot act as the logged-in operator, and the plugin origin names the console as the only page allowed to frame it. It built that name from the incoming request — but Nitro hands the app a synthetic request with no TLS socket, so an HTTPS console reads back as `http:`. The header said `frame-ancestors http://host:47992` while the operator was on `https://host:47992`, and the browser refused the frame outright (ERR_BLOCKED_BY_RESPONSE). Nothing on screen said so; the reason was only in devtools. The scheme now comes from the listener's own TLS state, stamped at bind time, with x-forwarded-proto winning when something in front terminated TLS for us — the one case where the browser's scheme is not ours. And the port was shut. 47993 was added to the firewall definitions, but an already-open firewall does not pick a new port up: ufw expands an app profile into rules when you allow it and keeps those, so editing the profile on upgrade changes nothing, and firewalld needs a reload. Every upgraded Linux host kept a 47992-only rule, silently. The packages now notice on upgrade and print the one command that fixes it, without touching the running firewall. The NixOS module and the container image never learned the port at all; both now open it. Also: the console no longer mounts the frame while it is still checking whether that origin is reachable. A firewalled port drops rather than refuses, so the check hangs for the browser's whole connect timeout, and mounting meanwhile is exactly the empty panel with no explanation. The card that follows now names both causes it can be — an untrusted certificate for that port, or a closed one — because from a browser the two are indistinguishable. The rule is now a pure function with tests, since its failure mode is a well-formed header that only a browser rejects. Verified on glass against home-worker-5 (.21) and its ROM Manager plugin: the frame was refused before, renders the plugin's UI after. |
||
|
|
9a1d8be4cc |
fix(pf-encode): AUTO split is conditional on sub-frame — do NOT retire the arm
Last change's docs concluded "AUTO never splits, retire the arm" from the sub-frame-ON measurement alone. Measured the missing leg before implementing it, and the conclusion was wrong. On .21 at 4K, plain AUTO (env unset, the resolver's fallthrough): sub-frame ON -> 5023/5157 us/frame ~= DISABLE 4979/5000 (does NOT split) sub-frame OFF -> 2401/2352 us/frame ~= TWO_FORCED 2319/2378 (DOES split) So AUTO is CONDITIONAL, not dead. Retiring it would have silently cost every sub-frame-off session its second engine -- a regression introduced while "cleaning up" an arm that looked inert. Split and sub-frame are mutually unsupported for HEVC, so the driver resolves AUTO to no-split only in that combination. Fix is disclosure, not removal: - resolve_split_subframe debug-logs the inert HEVC + AUTO + sub-frame case, which is the fleet default shape: "split_mode=AUTO" has meant "no split" for every default session and nothing said so. Deliberately NOT rewritten to DISABLE -- the mode we pass is what the driver was actually given, and the ceiling-cache key must keep describing that. - New unit test `auto_survives_the_arbitration_in_both_subframe_states` pins the contract so the arm cannot be simplified away later. - The resolver doc now records both measured legs instead of "AUTO is dead". Also in this change: - WP1.6: `resolve_subframe`'s doc said "Windows passes `false`". Stale since the 2026-07-31 .173 A/B flipped Windows to caps-gated default-on. It mattered: it made the AUTO-plus-sub-frame dead combination look Linux-only when it is fleet-wide. - Windows session-ready log parity: split_mode + engines + subframe. The Windows line had no split_mode at all, so a Windows field report could not answer "did this session actually split?" -- the question that started this whole thread. Verified: fmt clean; .21 clippy -p pf-encode --features nvenc --all-targets -D warnings clean, 58 unit tests (1 new), 22/22 NVENC on-hardware tests green; .133 Windows clippy --features nvenc --all-targets -D warnings clean (15m cold, zero errors or warnings) -- the Windows backend is cfg'd out on both macOS and the Linux box, so that leg needed a real Windows host. |
||
|
|
88f29a9411 |
feat(pf-encode): use every NVENC engine the GPU has, not a hard-coded two
WP1.1 plus the engine-count fix. `resolve_split_mode` forced TWO_FORCED at high pixel rate regardless of hardware, so a 3-NVENC part (GB202, AD102 workstation) left a third of its encode silicon idle, and a 1-NVENC part paid a wasted session open to discover it could not split. Probes NV_ENC_CAPS_NUM_ENCODER_ENGINES in both direct-SDK backends' query_caps (the cap is `= 49` in both linux_sys and windows_sys of the vendored SDK 0.4.0 -- the caps enum is cfg-selected per-OS, so that was checked) and latches it on a backend field. NOT on EncoderCaps: nine backends construct that struct as exhaustive literals, so a new field would be a 9-site change of which 7 are unrelated codecs passing a meaningless value, and the only consumer is the resolver. New `max_forced_split_mode(engines)`: 1 -> DISABLE, 2 -> TWO, 3 -> THREE, and >3 -> AUTO_FORCED, because NV_ENC_SPLIT_ENCODE_MODE cannot NAME more than three (NVENCAPI 12.1; values 4..14 are unallocated, so a future API may extend it) and AUTO_FORCED = "split, driver picks how many" is measurably a real split (2.01x vs disabled on .21). 0 = unprobed keeps the historical two-engine assumption. ⚠ WHY THE CLAMP EXISTS, measured on .21 (RTX 5070 Ti, 2 NVENC, 4K HEVC): requesting THREE_FORCED was HONOURED -- session opened in mode 3 -- and ran at 2303 us/frame, identical to TWO_FORCED's 2308. The driver does not reject an over-ask; it silently encodes narrower. So the rejection fallback cannot find the ceiling and PUNKTFUNK_SPLIT_ENCODE=3 on a 2-engine card would have logged a 3-way split over a 2-way encode. Operator overrides are now clamped with a warn. The ordering trap is covered by a test: on a >3-engine part hw_max is AUTO_FORCED (1), which is not "narrower than" TWO_FORCED (2) despite comparing smaller, so a naive min() would collapse a legitimate 3-way request to AUTO. Also adds `engines` and `subframe` to the Linux session-ready log: split_mode alone is ambiguous between "used both engines" and "left a third idle", and since the driver honours an over-wide request the mode cannot be read without the ceiling it was chosen from. This is the line a field report needs. --- and a correction to S1b, in the same change --- Re-running S1b afterwards flipped its verdict to "the driver appears to have IGNORED the in-place split change", contradicting the isolated runs that produced the |C-B|=34 figure already written into the design docs. Investigated rather than re-rolled. The switched leg was landing MIDWAY between the arms (~3600 us against A~5050, B~2300) and the nearest-neighbour verdict flipped on noise. Cause: split-encode does not reach steady state on the first frame -- a FRESH TWO_FORCED session shows it too (early-half 3280 us vs late-half 1996 in one run), so it is split warmup generally, not something specific to reconfiguring in place. A single median over the whole window cannot see that. The test now reports early-half vs late-half and gives a switched leg SETTLE=16 frames before its window opens, every leg the same length. With that, 4/4 runs agree: the switched leg reaches ~2030 us against a fresh-split ~2000 and a single-engine ~4900. ⚠ S1b's CONCLUSION stands (the switch does take effect) but the evidence behind the committed number did not reproduce; the docs are corrected rather than left implying a cleaner result than the harness could support. ⚠⚠ This is a WP3 REQUIREMENT, not just a test fix: a live-session arbitration that switches arms and immediately measures will misjudge the arm it just chose, because the encoder needs ~16 frames to settle. The settle window has to be part of the arbitration, and it is now a measured number rather than a guess. Verified on .21: clippy --features nvenc --all-targets -D warnings clean, 57 unit tests (3 new), all 23 NVENC on-hardware tests green, fmt clean. The 3 failing on-hw tests in a full --ignored run are VAAPI (no AMD/Intel GPU on that box -- their own ignore reason says so), pre-existing and unrelated. |
||
|
|
50fa228bab |
Merge pull request 'feat(client/android): copy a host's punktfunk:// link' (#79) from worktree-android-copy-link into main
ci / web (push) Failing after 28s
ci / bun-nix (push) Successful in 23s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 13s
ci / docs-site (push) Successful in 1m15s
docker / builders-arm64cross (push) Skipped
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m13s
android / android (push) Successful in 5m17s
ci / rust-arm64 (push) Successful in 6m17s
ci / rust (push) Failing after 6m15s
docker / deploy-docs (push) Successful in 6m29s
Reviewed-on: #79 |
||
|
|
70b81ac3d7 |
test(pf-encode): S1c + the D5 confirm — pair flips in place, AUTO really is dead
S1c `nvenc_cuda_split_subframe_pair_reconfigure`: the leg S1a/S1b excluded. Both pinned sub-frame OFF to isolate the split variable, but a real HEVC arbitration cannot -- split and sub-frame are mutually unsupported there, so engaging split means flipping enableSubFrameWrite in the same breath, a second init param and the one the reconfigure path deliberately pins. RESULT on .21: the PAIR moves in place, accepted, ZERO IDRs, both directions. It also pins the invariant that makes this safe to build on: `subframe_chunks` is latched ONLY in the init path (~line 1625) and is NOT recomputed by reconfigure_bitrate, so a caller flipping sub-frame in place must clear it too or supports_chunked_poll keeps reporting true and poll_chunk busy-polls its whole budget every AU against a numSlices that never advances. The test performs the correct sequence and asserts the state stays coherent, so WP3 has a worked example rather than a warning. `nvenc_cuda_auto_split_with_subframe`: the D5 confirm -- the one claim in the design's defect list that was only ever inferred. The driver reports no "mode I actually chose", so it is settled by timing, at 4K where the gap is ~2x. RESULT: AUTO (env unset) + sub-frame 4904 us/frame, DISABLE + sub-frame 5062, TWO_FORCED without sub-frame 3464. AUTO sits 158 us from DISABLE and 1440 from TWO_FORCED ⇒ D5 CONFIRMED: plain AUTO does not split while sub-frame is on, so the resolver's AUTO fallthrough reads as "let the driver decide" and means "never split". ⚠ TRAP, hit on this test's first run and now documented in it: the env knob CANNOT express plain AUTO. `0` is DISABLE and `1` is AUTO_FORCED, and resolve_split_subframe counts AUTO_FORCED as forced, so passing `1` silently disarms sub-frame and measures a different configuration entirely -- which produced a spurious "D5 REFUTED". Plain AUTO is only reachable as the resolver's fallthrough with the env unset. The leg now asserts sub-frame resolved TRUE, so the test can no longer answer the wrong question quietly. Verified on .21: clippy --features nvenc --all-targets -D warnings clean, all 4 spikes green, the normal 54-test suite unaffected, cargo fmt --all --check clean. |
||
|
|
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. |
||
|
|
4b57d11dd8 |
test(pf-encode): S1 spike — splitEncodeMode CAN change in place, no IDR
Two on-hardware spikes answering the gate on the split-encode engagement
program (design/nvenc-split-encode-engagement-implementation-plan.md).
S1a `nvenc_cuda_split_reconfigure_in_place`: can splitEncodeMode change via
nvEncReconfigureEncoder with resetEncoder=0, without an IDR? Our "reconfigure
must present the SAME init params as the open" rule (windows/nvenc.rs:620) is
our own invariant and had never been tested against a driver. It reports rather
than asserts the verdict -- both outcomes are legitimate findings -- and only
asserts what would invalidate the measurement (session live, engines >= 2, the
arms actually differ). Sub-frame is pinned off so the driver can't reject for
the wrong reason (HEVC forced-split and sub-frame are mutually unsupported).
S1b `nvenc_cuda_split_reconfigure_takes_effect`: the other half -- a driver that
accepts the parameter and quietly ignores it looks identical to one that honours
it. Three legs at 4K (fresh DISABLE / fresh TWO_FORCED / DISABLE->TWO in place);
if C tracks B and not A, the switch is real.
RESULT on .21 (RTX 5070 Ti, GB203 Blackwell, driver 610.57.04):
NV_ENC_CAPS_NUM_ENCODER_ENGINES = 2
S1a: accepted, ZERO IDRs, both directions.
S1b: A fresh DISABLE 5054 us/frame, B fresh TWO_FORCED 2453,
C switched in place 2419 -- |C-B|=34 vs |C-A|=2635. It takes effect,
and split is a clean ~2x at 4K.
Two limits, both recorded in the test docs rather than the commit only. The
frames come out at 427 B/AU against an 833 KB CBR quota: the driver hands back
zeroed VRAM, so the rotated buffers are identical and rate control skip-codes
everything. So this measures the PIXEL-proportional half of the cost only --
the bits/frame regime the field case lives in is untested here, and the test
prints an explicit INCONCLUSIVE-on-content line when it detects that. And this
is Blackwell 8-bit; the Ada Main10 question is untouched.
Verified on .21: clippy -p pf-encode --features nvenc --all-targets -D warnings
clean, both spikes green, cargo fmt --all --check clean.
|
||
|
|
442558e503 |
Merge pull request 'fix(plugin-kit): the SQLite reader never opened anything on Linux' (#78) from worktree-plugin-kit-sqlite-uri-fix into main
ci / bun-nix (push) Successful in 25s
ci / rust-arm64 (push) Successful in 1m19s
ci / docs-site (push) Successful in 1m25s
ci / web (push) Successful in 1m36s
plugin-kit-publish / publish (push) Successful in 33s
ci / rust (push) Successful in 8m0s
nix / flake (push) Successful in 11m19s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
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 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 15s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 26s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
Reviewed-on: #78plugin-kit-v0.3.1 |
||
|
|
4bcb3794c5 |
fix(plugin-kit): the SQLite reader never opened anything on Linux
nix / flake (pull_request) Failing after 1s
ci / bun-nix (pull_request) Successful in 22s
ci / web (pull_request) Successful in 58s
ci / docs-site (pull_request) Successful in 1m35s
ci / rust-arm64 (pull_request) Successful in 2m2s
ci / rust (pull_request) Successful in 6m15s
Found while running the lutris plugin's own release gate against a live host on
.21: `parity --compare` reported `1 missing` — the plugin produced NO entry for
the one game Lutris had, while the host's built-in scanner produced it fine.
`scan` agreed: "present: 0 games". `detect` still said "present", because detect
only stats the file.
`openReadOnly` builds a `file:<path>?immutable=1` URI — the right idea, since
`immutable=1` is what makes this a pure read that cannot lock a running
launcher's database or spawn WAL sidecars next to it. But it opened that name
with `{ readonly: true }`, and the options object does NOT enable SQLite's URI
filename parsing. Without SQLITE_OPEN_URI the name is taken literally, no such
file exists, and the open throws:
SQLiteError: unable to open database file
Every path here then degrades that to silence by design: `openReadOnly` returns
`undefined` for "this launcher isn't installed", `withReadOnlyDb` passes the
`undefined` through, and callers write `withReadOnlyDb(...) ?? []`. So a total,
permanent failure to read ANY database was indistinguishable from an empty
library. The lutris plugin reported 0 games on every Linux box, always.
It is platform-split, which is why it survived review and local runs: macOS
links Apple's system SQLite, which is built with URI filenames enabled, so the
same call succeeds there. Linux uses Bun's bundled SQLite, which is not. MEASURED
both ways on bun 1.3.14.
Fixed by passing the flags explicitly — SQLITE_OPEN_READONLY | SQLITE_OPEN_URI.
(`{ readonly: true, uri: true }` is not a supported option shape; also measured.)
And the second half, which is why nothing caught it: `parity --compare` sets
`process.exitCode = 1` on a mismatch and then returns NORMALLY — a red parity is
a finished comparison, not a crashed command. `runPluginCli` then assigned
`process.exitCode = 0` unconditionally after the effect resolved, overwriting it.
So the one verb both plugin READMEs document as the release gate — "exits
non-zero on any difference", "do not publish a version whose parity run is red" —
always exited 0, and any scripted use of it passed. Now `??= 0`, so a code a
command set deliberately survives.
Tests: the sqlite helper had NO coverage at all, which is the whole reason a
total failure shipped looking like an empty library. Added five cases against a
REAL database file — reads rows back, handles a path needing URI escaping,
withReadOnlyDb round-trips, an absent file is `undefined` not a throw, and a bad
query still degrades to []. Verified they actually catch it: against the shipped
code on Linux, 4 of the 5 fail; with the fix, 21/21 in that file and 61/61 across
the suite, typecheck and build clean.
End to end on .21 with the fix: lutris `scan` goes 0 -> 1 games and
`parity --compare` reports "parity OK — 1 entries identical". A deliberately
doctored baseline now exits 1 instead of 0.
0.3.0 -> 0.3.1.
|
||
|
|
214e846984 |
Merge pull request 'fix(ci/nix): the flake job could never start its container' (#77) from worktree-nix-flake-container-fix into main
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 13s
ci / bun-nix (push) Successful in 24s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
ci / web (push) Successful in 1m3s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 43s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 47s
ci / rust-arm64 (push) Successful in 1m23s
docker / builders-arm64cross (push) Skipped
ci / docs-site (push) Successful in 1m24s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
docker / deploy-docs (push) Successful in 6m30s
ci / rust (push) Successful in 8m11s
nix / flake (push) Successful in 11m42s
Reviewed-on: #77 |
||
|
|
e78906fb06 |
fix(ci/nix): the flake job could never start its container
ci / web (pull_request) Successful in 1m7s
ci / docs-site (pull_request) Successful in 1m11s
ci / rust-arm64 (pull_request) Successful in 1m18s
ci / bun-nix (pull_request) Successful in 16s
ci / rust (pull_request) Failing after 6m12s
nix / flake (pull_request) Failing after 4m54s
The nix gate landed in
|
||
|
|
d167e90ea1 |
Merge pull request 'A launcher tile's session stops depending on invisible state' (#76) from worktree-launcher-lease into main
arch / build-publish (push) Failing after 16s
ci / bun-nix (push) Successful in 42s
ci / web (push) Successful in 1m11s
ci / docs-site (push) Successful in 1m24s
apple / swift (push) Successful in 1m37s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 10s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
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 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
deb / build-publish-client-arm64 (push) Successful in 2m28s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 54s
android / android (push) Failing after 3m51s
ci / rust (push) Failing after 3m48s
docker / builders-arm64cross (push) Failing after 19s
deb / build-publish (push) Successful in 3m54s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m9s
ci / rust-arm64 (push) Successful in 5m32s
docker / deploy-docs (push) Successful in 32s
apple / screenshots (push) Successful in 5m58s
deb / build-publish-host (push) Successful in 7m57s
windows-host / package (push) Failing after 11m43s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 8m57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m49s
Reviewed-on: #76 |
||
|
|
e9da37aaf6 |
feat(host/gamelease): a launcher tile's session stops depending on invisible state
ci / web (pull_request) Failing after 2s
ci / rust-arm64 (pull_request) Failing after 3s
ci / bun-nix (pull_request) Successful in 1m15s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 2m16s
android / android (pull_request) Successful in 4m56s
ci / rust (pull_request) Successful in 9m1s
A launcher entry (design D4) has no "the game exited" moment to detect, but the lease was
still deciding its lifetime from whatever happened to be true at launch — and the two
outcomes disagree:
launcher NOT already running the spawned child stays alive -> `Child` lease
-> quitting the launcher ends the session
launcher ALREADY running the command forwards to the live instance and exits
inside SHIM_WINDOW, with no detect signals behind it
-> `Untracked` -> the session persists
Same tile, two lifetimes, chosen by something the user cannot see. Steam is the case that
settles which one is right: Big Picture is a *mode* of an already-running Steam client, not
a process — and on a Deck or SteamOS host Steam is always running — so no process signal can
express "the launcher's window closed". Heroic has the same shape for a different reason
(single-instance Electron: a second invocation forwards and exits).
So a launcher entry is now `LeaseKind::Untracked` unconditionally. The check sits AHEAD of
`nested`/`child`/`spec`, and that ordering is the fix rather than an implementation detail:
a launcher the host just started leaves a live child behind, and tracking that child is
precisely the inconsistency being removed.
`Untracked` already meant the right things downstream, so nothing else had to change: no
watcher thread, `terminate()` is a no-op ("nothing to end" — closing a session must not kill
the user's Steam), and no `GameExited` event, so a client does not bounce back to its library
when the launcher closes.
Threaded as `LaunchTarget::launcher` -> `LeaseRequest::launcher` from the entry's `role`.
Three call sites, one of which was the actual trap: the GameStream path does not build its
lease from a `LaunchTarget` at all, it goes through a `GsApp` intermediate that silently
dropped the field. An operator-typed `apps.json` command has no library entry behind it and
is never a launcher tile, so it passes `false` explicitly.
The test pins BOTH cases from the table above, plus the same request without the flag still
being `Matched` — so the assertions are the flag's doing and not an artifact of the fixture.
Gates: punktfunk-host 439 passed / 0 failed on .21 (+1).
Not covered here: nothing publishes launcher tiles until the plugins are released, so there
was no live exposure to fix — this is the semantics being made deliberate before the first
tile is ever clicked.
|
||
|
|
453b9850fa |
Merge pull request 'Gamepad UI: multi-tone palettes, and a UI that takes its colours from them' (#74) from worktree-gamepad-ui-polish into main
ci / bun-nix (push) Successful in 19s
ci / docs-site (push) Successful in 1m19s
apple / swift (push) Successful in 1m25s
deb / build-publish (push) Successful in 3m43s
ci / web (push) Successful in 5m20s
deb / build-publish-host (push) Successful in 4m10s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 13s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 15s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
ci / rust-arm64 (push) Successful in 6m3s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 9s
docker / builders-arm64cross (push) Skipped
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
deb / build-publish-client-arm64 (push) Successful in 2m14s
arch / build-publish (push) Successful in 7m7s
docker / deploy-docs (push) Failing after 9s
android / android (push) Failing after 10m28s
ci / rust (push) Failing after 10m7s
release / apple (push) Successful in 9m49s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m52s
apple / screenshots (push) Successful in 5m55s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m42s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m53s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m13s
flatpak / build-publish (push) Successful in 13m21s
Reviewed-on: #74 |
||
|
|
eb4b33ca49 |
Merge pull request 'An xbox launch kind a de-privileged plugin can publish' (#75) from worktree-xbox-launch-kind into main
android / android (push) Canceled after 0s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Failing after 11m41s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
Reviewed-on: #75 |
||
|
|
8e02f3a8e1 |
Merge pull request 'ci(nix): a stale bun.nix can no longer reach main unnoticed' (#73) from worktree-nix-bun2nix-drift-gate into main
apple / swift (push) Successful in 1m26s
ci / bun-nix (push) Successful in 1m20s
ci / rust-arm64 (push) Successful in 1m27s
android / android (push) Canceled after 1m44s
deb / build-publish-host (push) Failing after 3s
apple / screenshots (push) Canceled after 10s
arch / build-publish (push) Canceled after 1m47s
ci / rust (push) Canceled after 1m49s
ci / web (push) Canceled after 1m45s
ci / docs-site (push) Canceled after 1m45s
deb / build-publish (push) Canceled after 26s
deb / build-publish-client-arm64 (push) Canceled after 9s
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 3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
nix / flake (push) Failing after 18s
decky / build-publish (push) Successful in 50s
Reviewed-on: #73 |
||
|
|
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).
|
||
|
|
166e158afe |
feat(host/library): an xbox launch kind a de-privileged plugin can publish
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m22s
android / android (pull_request) Successful in 3m9s
ci / docs-site (pull_request) Successful in 1m9s
ci / web (pull_request) Successful in 3m20s
ci / rust (pull_request) Failing after 9m9s
The xbox scanner resolves each package's PackageFamilyName by enumerating %ProgramData%\Microsoft\Windows\AppRepository\Packages. Probed on .173 (design doc S1), that directory is: NT AUTHORITY\LocalService UnauthorizedAccessException <- the plugin runner LocalSystem count=348 <- the host service So an extracted xbox plugin cannot build an AUMID, and `aumid` — which takes a complete `<PFN>!<AppId>` — is unpublishable by one. The design doc offered two fallbacks: entries without `aumid` (they would list but not launch), or a one-shot elevated PFN resolve at install time. This is neither. The plugin sends `<Identity>!<AppId>`, both read straight out of MicrosoftGame.config — which IS readable de-privileged — and the host resolves the publisher hash at LAUNCH time, where it already owns command construction. That keeps D1 intact (the plugin supplies a validated value, the host builds the command), it is the same shape as the `playnite` kind, and resolving late means a package update that changes the publisher hash cannot leave a stale unlaunchable tile behind — which the install-time cache would have. `xbox_pfn` therefore moves from the scan path to the launch path rather than being deleted, and its doc records the privilege asymmetry so the next reader doesn't re-derive it. The charset guard is factored out (`aumid_part` / `valid_aumid`) and shared with the `aumid` kind. On `aumid` it was belt-and-braces — every value was host-derived. On `xbox` it is load-bearing: the Identity arrives over the wire from a plugin, and it is interpolated into a `shell:AppsFolder\…` argument. Validated inbound at reconcile too, so a malformed value is a 400 the plugin author can act on rather than a tile that fails on click. Gates: punktfunk-host 438 passed / 0 failed on .21, and 55/0 for the library tests on .173 (where these arms actually compile). Both counts +1 for the new guard test. |
||
|
|
fb707b4956 |
ci(nix): a stale bun.nix can no longer reach main unnoticed
ci / rust-arm64 (pull_request) Successful in 1m24s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Failing after 2m19s
ci / docs-site (pull_request) Successful in 1m17s
android / android (pull_request) Successful in 3m10s
ci / bun-nix (pull_request) Successful in 1m6s
nix / flake (pull_request) Failing after 1m6s
ci / rust (pull_request) Failing after 6m2s
Moving the bun packages to bun2nix ( |
||
|
|
65996621d8 |
Merge pull request 'fix(encode/pyrowave): stop stamping GPU scheduling priority over pf-frame's auto gate' (#72) from worktree-pyrowave-gpu-priority into main
ci / web (push) Successful in 1m10s
ci / docs-site (push) Successful in 1m9s
android / android (push) Successful in 6m26s
deb / build-publish-host (push) Failing after 15s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 1m19s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 17s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 10s
apple / swift (push) Successful in 1m27s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 12s
deb / build-publish-client-arm64 (push) Successful in 2m55s
docker / builders-arm64cross (push) Skipped
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
deb / build-publish (push) Successful in 4m9s
docker / deploy-docs (push) Successful in 42s
arch / build-publish (push) Successful in 10m31s
ci / rust-arm64 (push) Failing after 8m55s
apple / screenshots (push) Successful in 5m51s
windows-host / package (push) Failing after 6m15s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 9m13s
ci / rust (push) Successful in 20m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m50s
Reviewed-on: #72 |
||
|
|
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 |
||
|
|
70684e5079 |
fix(encode/pyrowave): stop stamping GPU scheduling priority over pf-frame's auto gate
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 4m41s
ci / rust-arm64 (pull_request) Successful in 2m35s
ci / web (pull_request) Successful in 2m27s
ci / docs-site (pull_request) Successful in 3m35s
ci / rust (pull_request) Successful in 8m25s
`windows/pyrowave.rs` raised the process's WDDM scheduling class to HIGH itself, once per process, at every session open. `pf-frame::dxgi::auto_priority_gate` already owns that policy for the whole process and runs from `create_device` — the call the Windows capture path always makes before any PyroWave texture exists. Two owners of one process-wide setting. The audit filed this as "downgrades REALTIME to HIGH", which undersells it. pf-frame's default `auto` mode starts at HIGH and then UPGRADES to REALTIME once it has established that is safe — HAGS off, or HAGS on with VRAM headroom — and leaves a monitor running that drops back when VRAM tightens, because REALTIME + NVIDIA + HAGS + near-full VRAM is a documented NVENC hang. Opening a PyroWave session after that upgrade stamped HIGH back over the class AND orphaned the monitor's decision, losing the ceiling-raise on exactly the GPU-saturated workload PyroWave exists to survive: it encodes on the shader cores a game saturates, where the measured spike is ~2 ms to 15-18 ms. Removed rather than reconciled. `PyroWaveEncoder::open` takes no device, so there was nothing session-specific to preserve, and the surviving owner is strictly better informed — it knows the adapter, HAGS state and VRAM headroom, none of which this call site had. The duplicated knob goes with it: `PUNKTFUNK_GPU_PRIORITY` is retired in favour of `PUNKTFUNK_GPU_PRIORITY_CLASS` (`off|normal|high|realtime|auto`, default `auto`), which is a superset — the removed knob could not express the auto gate at all. No other reference to it exists in the tree. Verified on .173: clippy -D warnings at nvenc,amf-qsv,qsv (host + pf-encode --all-targets), amf-qsv without qsv, qsv alone, no-features, cargo test --features qsv (34 passed), rustfmt — 7 legs green. Windows-only file, so the Linux legs do not compile it. |
||
|
|
3f8a70dd45 |
Merge pull request 'Launcher tiles reach the clients, and Playnite can publish again' (#70) from worktree-library-clients into main
apple / swift (push) Successful in 1m34s
ci / web (push) Successful in 1m7s
deb / build-publish-host (push) Failing after 4s
ci / docs-site (push) Successful in 1m23s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 22s
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 8s
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 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
deb / build-publish-client-arm64 (push) Successful in 2m36s
ci / rust-arm64 (push) Successful in 5m47s
arch / build-publish (push) Successful in 7m36s
docker / builders-arm64cross (push) Successful in 7s
docker / deploy-docs (push) Successful in 25s
android / android (push) Canceled after 9m43s
apple / screenshots (push) Canceled after 0s
ci / rust (push) Canceled after 9m54s
deb / build-publish (push) Canceled after 8m14s
release / apple (push) Canceled after 8m21s
flatpak / build-publish (push) Canceled after 5m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 5m16s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 4m13s
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 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
windows-host / package (push) Failing after 6m42s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
Reviewed-on: #70 |
||
|
|
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.
|
||
|
|
79ee308a7a |
build(rpm): declare all seven FFmpeg pkg-config modules, not three
`ffmpeg-next` is pulled with default features, so `ffmpeg-sys-next`'s build script
pkg-config-probes codec/device/filter/format/util/resampling/scaling and panics on
the first one missing. The spec named three.
RPM Fusion's `ffmpeg-devel` ships all seven in one package, which hid it. On a host
where those three instead resolve to Fedora's split `libav*-free-devel` packages,
`dnf builddep` installs exactly three and the build dies in a build script:
The system library `libavfilter` required by crate `ffmpeg-sys-next` was not found.
|
||
|
|
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 |