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
Owner

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 ceiling

The 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 at max_frame_bytes, which quic::handshake clamps 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:

wire frames admitted packets dropped
legacy streamed 12 0
0.23 slice 3 9 frames' worth, outright

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_dropped climbs, 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=4 while the packetizer was barely cutting a slice block.

Blast radius — only clients advertising VIDEO_CAP_MULTI_SLICE: Android (per-decoder probe) and the Linux/Windows punktfunk-session client (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=0 on the host.

The exact-multiple frame kill (moderate)

The same flush drained pending to empty whenever the AU's byte length was an exact multiple of shard_payload, leaving finish_streamed to 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::pending already documented but did not hold.


2 · 166c93c0 — the panel grid can be wrong in both directions, and the margin listens to the latch

The learner could only ever narrow. It is seeded from the display mode Kotlin asked for, and preferredDisplayModeId is 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 in punktfunk_core::phase::PanelGrid where 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. OnFrameRendered already 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. qWait and unconfirmed join the 1 Hz pf.present line — 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 AU

AMediaCodec_getInputBuffer returning 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_aus overflows 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 loop breaks rather than burning the whole backlog against a bad index.


Gates

  • cargo test -p punktfunk-core --all-features --lib278 passed (7 new PanelGrid cases, incl. the refused-mode regression; 2 new slice-wire regressions)
  • cargo test -p punktfunk-core --all-features --test loopback8 passed
  • cargo clippy -p punktfunk-core --all-features --all-targets -- -D warnings — clean
  • cargo ndk check — green on arm64-v8a and armeabi-v7a
  • Android cargo ndk clippy — the same 4 warnings as the base commit (audio.rs:138, mic.rs:89 type_complexity; vsync.rs:37 ×2 unnecessary_cast, the armv7-required cast), none new
  • cargo fmt --all -- --check — clean
  • --test c_abi fails 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 no FEATURE_PartialFrame now 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) and adb logcat -s pf.caps already prints prefer=4.

Android vsync / allow_vrr settings. Apple has four presentation keys; Android has two (present_priority, smooth_buffer). vsync is structurally N/A on Android — SurfaceView/MediaCodec is always SurfaceFlinger-latched. allow_vrr is expressible (try_set_frame_rate hard-codes FIXED_SOURCE; the opt-out would be DEFAULT compatibility with the preferredDisplayModeId pin 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

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 ceiling ### The 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 at `max_frame_bytes`, which `quic::handshake` clamps 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: | wire | frames admitted | packets dropped | |---|---|---| | legacy streamed | 12 | **0** | | 0.23 slice | 3 | **9 frames' worth, outright** | 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_dropped` climbs, 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=4` while the packetizer was barely cutting a slice block. **Blast radius** — only clients advertising `VIDEO_CAP_MULTI_SLICE`: **Android** (per-decoder probe) and the **Linux/Windows `punktfunk-session` client** (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=0` on the host. ### The exact-multiple frame kill (moderate) The same flush drained `pending` to empty whenever the AU's byte length was an exact multiple of `shard_payload`, leaving `finish_streamed` to 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::pending` already documented but did not hold. --- ## 2 · `166c93c0` — the panel grid can be wrong in both directions, and the margin listens to the latch **The learner could only ever narrow.** It is seeded from the display mode Kotlin asked for, and `preferredDisplayModeId` is 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 in `punktfunk_core::phase::PanelGrid` where 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.** `OnFrameRendered` already 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. `qWait` and `unconfirmed` join the 1 Hz `pf.present` line — 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 AU `AMediaCodec_getInputBuffer` returning 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_aus` overflows 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 loop `break`s rather than burning the whole backlog against a bad index. --- ## Gates - `cargo test -p punktfunk-core --all-features --lib` — **278 passed** (7 new `PanelGrid` cases, incl. the refused-mode regression; 2 new slice-wire regressions) - `cargo test -p punktfunk-core --all-features --test loopback` — **8 passed** - `cargo clippy -p punktfunk-core --all-features --all-targets -- -D warnings` — clean - `cargo ndk check` — green on **arm64-v8a** and **armeabi-v7a** - Android `cargo ndk clippy` — the same **4** warnings as the base commit (`audio.rs:138`, `mic.rs:89` type_complexity; `vsync.rs:37` ×2 unnecessary_cast, the armv7-required cast), **none new** - `cargo fmt --all -- --check` — clean - ⚠ `--test c_abi` fails 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 no `FEATURE_PartialFrame` now 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) and `adb logcat -s pf.caps` already prints `prefer=4`. **Android `vsync` / `allow_vrr` settings.** Apple has four presentation keys; Android has two (`present_priority`, `smooth_buffer`). `vsync` is structurally N/A on Android — SurfaceView/MediaCodec is always SurfaceFlinger-latched. `allow_vrr` *is* expressible (`try_set_frame_rate` hard-codes `FIXED_SOURCE`; the opt-out would be `DEFAULT` compatibility with the `preferredDisplayModeId` pin 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](https://claude.com/claude-code)
enricobuehler added 1 commit 2026-08-02 17:36:24 +00:00
fix(core/packet): a slice-streamed frame costs its own size, not the whole frame ceiling
ci / web (pull_request) Successful in 1m4s
ci / rust-arm64 (pull_request) Successful in 1m42s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m5s
android / android (pull_request) Successful in 2m50s
ci / docs-site (pull_request) Successful in 2m45s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m2s
apple / swift (pull_request) Successful in 1m18s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 12m58s
8749bd1396
The 0.23.0 slice wire flushes a block every MIN_STREAM_BLOCK_SHARDS, so every
ordinary access unit is now opened by a SENTINEL — a header with no totals. The
reassembler sized those frames at `max_frame_bytes`, which the QUIC handshake
clamps to 8-64 MiB. That was survivable while sentinels were rare (the streamed
path emitted one only for an AU exceeding a whole FEC block, ~281 KB); it is not
survivable now that every frame is one.

Two consequences, both measured: each access unit allocated and ZEROED a
multi-megabyte buffer, and the in-flight budget (IN_FLIGHT_BUF_FACTOR x
max_frame_bytes) was spent after ~3 concurrent frames — with production geometry,
12 ordinary AUs in flight lost 9 of them outright, every packet dropped before it
could be placed. On a link with normal reorder that is a permanent loss storm:
frames never complete, the re-anchor gate freezes the picture, and the client begs
for keyframes. Only clients advertising VIDEO_CAP_MULTI_SLICE reach this path —
Android and the Linux/Windows session client; Apple and the Windows in-process
client never did, which is why it read as a platform-specific "video pipeline"
fault in the field.

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.

The same flush also drained `pending` to empty whenever the AU's length was an
exact multiple of the shard payload, leaving `finish_streamed` to seal a final
block of one zero-padded FILLER shard. Its derived base overlapped the block
flushed a moment earlier, retro-validation correctly read that as a lying header,
and the whole AU died — one frame in every 1408 on a 1500-MTU link, ~12 s apart at
120 fps, each costing a freeze and a recovery keyframe. A flush now keeps one
whole shard back, restoring the invariant `StreamedAu::pending` already documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enricobuehler added 1 commit 2026-08-02 17:52:49 +00:00
fix(android/present): the panel grid can be wrong in both directions, and the margin listens to the latch
android / android (pull_request) Canceled after 1m21s
apple / swift (pull_request) Successful in 1m18s
apple / screenshots (pull_request) Canceled after 0s
ci / rust (pull_request) Canceled after 1m32s
ci / rust-arm64 (pull_request) Canceled after 1m28s
ci / web (pull_request) Canceled after 0s
ci / docs-site (pull_request) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (pull_request) Canceled after 1m31s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 0s
166c93c079
Three defects in the 0.23.0 timeline presenter, all found while root-causing the
field report that turned out to be the slice wire. None of them is that bug; all
three are real, and the first is the one that would still bite once it is fixed.

The panel-period learner could only ever narrow. It is seeded from the display
mode Kotlin asked for — and `preferredDisplayModeId` is 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 with no way back, for the rest of the session: it then aims at
instants that never arrive and releases faster than the panel scans. The learner
moves both ways now, and lives in `punktfunk_core::phase::PanelGrid` where it is
host-testable and where the iOS and desktop presenters can share it. The
asymmetry is kept and made explicit — narrowing is immediate (a finer real grid
is always safe to subdivide onto, and it is 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, because one wide sample is a missed
callback and eight in a row is a display that really did slow down.

The glass budget was a prediction with nothing underneath it. `OnFrameRendered`
already reports what actually reached glass, but the budget never consulted it,
so a wrong grid could hand SurfaceFlinger frames indefinitely: BufferQueue fills,
MediaCodec runs out of output buffers, the decoder stalls, and the no-output
backstop starts begging for keyframes. Releases are now counted against their
confirms and the presenter holds back past six outstanding — loose on purpose,
since the callbacks are allowed to arrive batched and a held frame in the
newest-wins slot is a dropped one. It self-clears when the 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. `qWait` and `unconfirmed` join
the 1 Hz pf.present line, which is what would have made this visible from a log.

The adaptive latch margin widened on `paced_drops` — the newest-wins store's own
policy evictions, which happen whenever the stream out-runs the panel and say
nothing about SurfaceFlinger's latch lead. On a healthy device that walked the
margin to its 2.5 ms ceiling and re-imposed 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, which is what a missed vsync actually looks like.

Also corrects two doc comments that named `display.refreshRate` as the panel_hz
source; it has been the mode table since the A024 down-rate fix.

Gates: 278 punktfunk-core lib tests (7 new PanelGrid cases incl. the refused-mode
regression), clippy -D warnings and fmt clean, cargo ndk check green on arm64 and
armv7. Android clippy reports the same 4 warnings as the base commit and no new
ones. NOT yet confirmed on glass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enricobuehler added 1 commit 2026-08-02 17:54:14 +00:00
fix(android/decode): a codec input slot the feeder can't fill goes back, and so does the AU
apple / swift (pull_request) Successful in 1m19s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m7s
ci / rust-arm64 (pull_request) Successful in 2m20s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m6s
android / android (pull_request) Successful in 3m43s
ci / web (pull_request) Successful in 1m26s
ci / docs-site (pull_request) Successful in 2m6s
ci / rust (pull_request) Successful in 6m30s
61547b512a
`AMediaCodec_getInputBuffer` returning null for an index the input-available
callback had just handed us dropped both the slot and the access unit on the
floor. Every sibling path in this loop recycles the slot — the orphan-part
discard and the oversize drop both say so in as many words — because nothing was
written and nothing was queued, so it is still ours. Forgetting it leaks one of
the codec's input buffers per occurrence: we never use it again and the codec
never frees what it never received, so the pipeline runs out of input slots,
`pending_aus` overflows into its drop-oldest arm, and the resulting keyframe storm
reads as a decode fault rather than a bookkeeping one.

The AU went with it, silently — no keyframe request, no freeze gate, unlike every
other loss path here — leaving a hole in the reference chain whose concealment
was free to reach the screen.

Both go back now. `break` rather than `continue`, because a codec that cannot
hand out an input buffer it has just advertised is in no state to be fed the rest
of the parked queue on this pass, and retrying the same index against every
parked AU would burn the whole backlog for nothing; the loop comes round again on
the housekeeping wake within 5 ms if it was transient.

Gates: cargo ndk check green on arm64 and armv7, fmt clean, Android clippy at the
same 4 pre-existing warnings as the base commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enricobuehler changed title from WIP: fix(core/packet): a slice-streamed frame costs its own size, not the whole frame ceiling to fix(0.23.0): the slice wire starves the reassembler, and three presenter defects behind it 2026-08-02 17:54:52 +00:00
enricobuehler marked the pull request as work in progress 2026-08-02 17:55:07 +00:00
Author
Owner

Landed directly on main at the author's request — fast-forward 29473d62..c2a6d30d.

The three commits were rebased onto 29473d62 first (PR #19's iPad Esc merge had moved main), so the SHAs differ from the ones reviewed here:

here on main
8749bd13 97b2c01a — the slice wire
166c93c0 20de58a7 — the presenter
61547b51 c2a6d30d — the codec input slot

All gates were re-run on the rebased base, not carried over: 278 punktfunk-core lib tests, 8 loopback, clippy -D warnings clean, cargo fmt --check clean, cargo ndk check green on arm64-v8a and armeabi-v7a, Android clippy at the same 4 pre-existing warnings as the base commit. cargo check --workspace --all-targets reports 5 pf-client-core lib-test errors — verified byte-identical on 29473d62, 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.present for the new qWait / unconfirmed counters and the vsync: panel grid now … line.

Closing this in favour of the commits on main.

Landed directly on `main` at the author's request — fast-forward `29473d62..c2a6d30d`. The three commits were rebased onto `29473d62` first (PR #19's iPad Esc merge had moved main), so the SHAs differ from the ones reviewed here: | here | on main | |---|---| | `8749bd13` | `97b2c01a` — the slice wire | | `166c93c0` | `20de58a7` — the presenter | | `61547b51` | `c2a6d30d` — the codec input slot | All gates were re-run **on the rebased base**, not carried over: 278 `punktfunk-core` lib tests, 8 loopback, `clippy -D warnings` clean, `cargo fmt --check` clean, `cargo ndk check` green on arm64-v8a and armeabi-v7a, Android clippy at the same 4 pre-existing warnings as the base commit. `cargo check --workspace --all-targets` reports 5 `pf-client-core` lib-test errors — verified byte-identical on `29473d62`, 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.present` for the new `qWait` / `unconfirmed` counters and the `vsync: panel grid now …` line. Closing this in favour of the commits on `main`.
enricobuehler closed this pull request 2026-08-02 18:17:26 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
This pull request is marked as a work in progress.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#21