WIP: fix(0.23.0): the slice wire starves the reassembler, and three presenter defects behind it #21
Closed
enricobuehler
wants to merge 3 commits from
worktree-android-presenter-023-triage into main
pull from: worktree-android-presenter-023-triage
merge into: :main
:main
:worktree-host-discovery-refresh
:worktree-native-decode-m0
:worktree-release-0250
:worktree-presenter-cadence-wp1
:worktree-decky-brand-name
:worktree-win-amd-decoder-order
:worktree-stall-ride-through
:fix/pad-audio-wasapi-module-path
:feat/android-pad-audio
:worktree-adl-emul-probe
:worktree-haptics-m12-dry
:worktree-haptics-m9-richfb
:worktree-haptics-m11-settings
:worktree-apple-store-screenshots
:worktree-decky-slim-rework
:worktree-appstore-copy
:worktree-haptics-m10-wire
:worktree-haptics-m8-proto
:worktree-haptics-m7-windows
:worktree-haptics-m6-presenter
:worktree-wire-mtu-resilience
:worktree-haptics-m5-android
:worktree-haptics-m4-apple
:worktree-haptics-m3-rumble-engine
:worktree-android-presenter-023-triage
:worktree-vdisplay-resume-recovery
:worktree-kwin-vout-demirror
:fix/web-console-sweep
:fix/windows-web-console-lifecycle
:chore/windows-rerender-semantics
:fix/pf-encode-phase8
:fix/pf-encode-phase7
:feat/cursor-channel
:feat/desktop-mouse-mode
:feat/plugin-kit
:fix/network-split-standing-latency
:fix/windows-msix-arm64-and-manifest
:fix/encode-medium-tier
:fix/encode-rfi-taint-and-oob
:fix/encode-init-leak-and-twin-drift
:fix/encode-teardown-uaf
:feat/pyrowave-windows
:fix/ubuntu-2404-host-deb
:fix/ios-archive-provisioning
:merge/first-frame-latency
:feat/shared-clipboard-v2
:feat/apple-live-activities-widgets
:debug/touch-finger-logging
:feat/shared-clipboard
:midstream-resize
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Triage of the 0.23.0 field report — "massive amounts of skipped frames which leads to the screen just completely freezing for seconds at a time then randomly updating, completely unusable" (Linux host; Pixel 9 Pro "doesn't even render", Lenovo Y700 "renders but freezes"), plus a second reporter seeing it as hit-or-miss.
Root cause is not the new Android presenter. It is the P2b slice wire (
440cb8d1). The presenter defects are real but separate, and the first of them is what would still bite once the wire is fixed.1 ·
8749bd13— a slice-streamed frame costs its own size, not the whole frame ceilingThe starvation (critical)
The slice wire cuts a block every
MIN_STREAM_BLOCK_SHARDS(16 shards ≈ 22.5 KB), so any AU at or above that size is now opened by a sentinel header — one with no totals. The reassembler sized sentinel-opened frames atmax_frame_bytes, whichquic::handshakeclamps to 8–64 MiB. That was fine while sentinels were near-impossible: the legacy streamed path emitted one only for an AU exceeding a whole FEC block (~281 KB). It is not fine now that ordinary frames are sentinels.Measured with production geometry (1408-byte shards, the 8 MiB floor), 12 ordinary 40 KB AUs in flight:
Plus a multi-megabyte
vec![0; …]per access unit — ~960 MB/s of zeroing at 120 fps, at the lowest negotiated ceiling.On any link with normal reorder the in-flight budget is permanently exhausted: frames never complete,
frames_droppedclimbs, the re-anchor gate freezes the picture, and the client begs for keyframes.It is bitrate-dependent, which is the reported "hit or miss": 20 Mbps at 120 fps gives ~20 KB AUs, just under the 16-shard floor and safe; 20 Mbps at 60 fps (~41 KB) or 50 Mbps at 120 fps (~52 KB) are over it and fatal. The same threshold is why the P2e on-glass A/B missed this — the host logged
slices=4while the packetizer was barely cutting a slice block.Blast radius — only clients advertising
VIDEO_CAP_MULTI_SLICE: Android (per-decoder probe) and the Linux/Windowspunktfunk-sessionclient (unconditional,main.rs:262). Apple and the Windows in-process client never advertise it, which is why the Windows-host/Windows-client field report a day earlier never saw it — and why this one reads "Linux/Android".Fix: a sentinel carries no total but does pin its own block's extent — a slice sentinel by its wire base, a legacy one by its full-K position. Size the buffer to that and grow as later blocks (or the final block's totals) reveal more. The budget is re-checked on growth for the same reason it is checked at open.
Workaround for anyone on 0.23.0 today:
PUNKTFUNK_SLICE_STREAM=0on the host.The exact-multiple frame kill (moderate)
The same flush drained
pendingto empty whenever the AU's byte length was an exact multiple ofshard_payload, leavingfinish_streamedto seal a final block of one zero-padded filler shard. Its derived base (total_data − 1) overlapped the block flushed a moment earlier; retro-validation correctly read that as a lying header and killed the whole AU. ~1 frame in 1408 — about every 12 s at 120 fps — each costing a re-anchor freeze and a recovery keyframe.Fix: a flush keeps one whole shard back, restoring the invariant
StreamedAu::pendingalready documented but did not hold.2 ·
166c93c0— the panel grid can be wrong in both directions, and the margin listens to the latchThe learner could only ever narrow. It is seeded from the display mode Kotlin asked for, and
preferredDisplayModeIdis a request the system may refuse (Smooth Display off, battery saver, thermal, an OEM governor). Ask for 120 Hz on a panel that stays at 60 and the presenter pins an 8.33 ms grid on a 16.67 ms display for the rest of the session, aiming at instants that never arrive and releasing faster than the panel scans. The learner now moves both ways, and lives inpunktfunk_core::phase::PanelGridwhere it is host-testable and where the iOS and desktop presenters can share it. The asymmetry is kept and made explicit: narrowing is immediate (the per-uid down-rate case the seed most often gets wrong), widening needs eight consecutive agreeing observations and then takes the narrowest of them.The glass budget was a prediction with nothing underneath it.
OnFrameRenderedalready reports what reached glass; the budget never consulted it. Releases are now counted against their confirms and the presenter holds back past six outstanding — loose on purpose, since callbacks arrive batched and a held frame in the newest-wins slot is a dropped one. It self-clears when confirms catch up and writes the ledger off after the same 100 ms the stale reopen uses, so a platform that stops confirming can never wedge the stream.qWaitandunconfirmedjoin the 1 Hzpf.presentline — what would have made this visible from a log.The adaptive latch margin widened on
paced_drops— newest-wins policy evictions, which happen whenever the stream out-runs the panel and say nothing about SF's latch lead. That walked the margin to its 2.5 ms ceiling on healthy devices, re-imposing the display latency the P2e sweep had just measured away. It now widens on the measured latch exceeding one panel period plus the live margin.3 ·
61547b51— a codec input slot the feeder can't fill goes back, and so does the AUAMediaCodec_getInputBufferreturning null for an index the callback had just handed us dropped both the slot and the AU. Every sibling path recycles the slot; forgetting it leaks one of the codec's input buffers per occurrence — the pipeline runs out of input slots,pending_ausoverflows into its drop-oldest arm, and the keyframe storm reads as a decode fault. The AU went too, silently, with no keyframe request behind it. Both go back now, and the loopbreaks rather than burning the whole backlog against a bad index.Gates
cargo test -p punktfunk-core --all-features --lib— 278 passed (7 newPanelGridcases, incl. the refused-mode regression; 2 new slice-wire regressions)cargo test -p punktfunk-core --all-features --test loopback— 8 passedcargo clippy -p punktfunk-core --all-features --all-targets -- -D warnings— cleancargo ndk check— green on arm64-v8a and armeabi-v7acargo ndk clippy— the same 4 warnings as the base commit (audio.rs:138,mic.rs:89type_complexity;vsync.rs:37×2 unnecessary_cast, the armv7-required cast), none newcargo fmt --all -- --check— clean--test c_abifails on this machine for want of a C toolchain; it fails identically on a clean tree.⏳ On-glass confirmation owed (Pixel 9 Pro / Y700 against a Linux host). The presenter changes in particular are reasoned + compile-checked, not measured.
Deliberately not here
0.23.0's Automatic-AV1 (
f9c56eaf): under codec=auto, any Android device with a hardware AV1 decoder and noFEATURE_PartialFramenow requests AV1. The Pixel 9 Pro has hardware AV1; the Y700 (SD 8+ Gen 1) does not — the single thing differing between "doesn't even render" and "renders but freezes". Changing a user-facing codec default on a suspicion is worse than leaving it; the reporter can test it in one tap (Settings → Codec → HEVC) andadb logcat -s pf.capsalready printsprefer=4.Android
vsync/allow_vrrsettings. Apple has four presentation keys; Android has two (present_priority,smooth_buffer).vsyncis structurally N/A on Android — SurfaceView/MediaCodec is always SurfaceFlinger-latched.allow_vrris expressible (try_set_frame_ratehard-codesFIXED_SOURCE; the opt-out would beDEFAULTcompatibility with thepreferredDisplayModeIdpin skipped) but it is a new user-facing setting across Kotlin UI, profiles and the JNI seam, and it changes frame-rate behaviour that wants on-glass validation — not something to bundle into a regression fix.🤖 Generated with Claude Code
WIP: fix(core/packet): a slice-streamed frame costs its own size, not the whole frame ceilingto fix(0.23.0): the slice wire starves the reassembler, and three presenter defects behind itLanded directly on
mainat the author's request — fast-forward29473d62..c2a6d30d.The three commits were rebased onto
29473d62first (PR #19's iPad Esc merge had moved main), so the SHAs differ from the ones reviewed here:8749bd1397b2c01a— the slice wire166c93c020de58a7— the presenter61547b51c2a6d30d— the codec input slotAll gates were re-run on the rebased base, not carried over: 278
punktfunk-corelib tests, 8 loopback,clippy -D warningsclean,cargo fmt --checkclean,cargo ndk checkgreen on arm64-v8a and armeabi-v7a, Android clippy at the same 4 pre-existing warnings as the base commit.cargo check --workspace --all-targetsreports 5pf-client-corelib-test errors — verified byte-identical on29473d62, so pre-existing and not from this work.⏳ Still owed: on-glass confirmation. The wire fix is proven by test; the presenter changes are reasoned and compile-checked only, never run on a device. Wants a session against the Pixel 9 Pro / Y700 with a Linux host, reading
adb logcat -s pf.presentfor the newqWait/unconfirmedcounters and thevsync: panel grid now …line.Closing this in favour of the commits on
main.Pull request closed