Skip to content

Fix three iPad crashes and two silent freezes - #26

Merged
whoisaldo merged 1 commit into
mainfrom
fix/ios-crashes-and-stalls
Aug 27, 2026
Merged

Fix three iPad crashes and two silent freezes#26
whoisaldo merged 1 commit into
mainfrom
fix/ios-crashes-and-stalls

Conversation

@whoisaldo

Copy link
Copy Markdown
Owner

Findings from the /code-review pass over the merged revamp, each confirmed by reading the tree.

Crashes

On every host restart. The 4 Hz stats flush computed fragment deltas with wrapping subtraction (&-), but the assembler zeroes its counters whenever it resets — which is what a stream-epoch bump does. Two ~2^64 deltas were then combined with a trapping +, so the app died the moment the host restarted its pipeline (supervisor auto-restart, VDD toggle, settings change, encoder crash recovery). The client killed itself on the exact path #18 built to survive. Now re-baselines when a counter goes backwards.

On a fast swipe. TouchRelay squared Int32 deltas of coordinates spanning 0…65535. A flick across ~71% of the content width overflows, and Swift traps rather than wrapping. Widened to Int64.

On connect and disconnect. FrameSlot.onFrameStored was a plain stored closure, written on the MainActor by the Metal view's attach/detach while the VideoToolbox decode thread called it — a race on the closure's own retain/release. Now swapped under a lock and copied out before invocation.

Silent freezes

Auto-reconnect never fired. The flush loop continued when no datagrams had arrived, and the only call to the liveness watchdog sat below that guard. A dead host sends neither media nor heartbeats, so the watchdog was silenced in precisely the case it exists for: the status bar kept reading ON AIR over a frozen frame, and the reconnect cycle from #17 could never start. It now runs every tick.

Large frames were dropped forever. The assembler capped frames at 1024 fragments while the host fragments against — and the wire parser accepts — 3066. A 1440p/4K scene-change IDR lands in that gap: every fragment discarded, so no sync sample, so every later frame discarded too, and the keyframe the client requests comes back equally large. Permanent freeze with healthy heartbeats on both ends. The cap is now the protocol cap; maxPendingBytes remains the real memory guard.

Smaller

The three-finger HUD gesture could only ever show the HUD (it called scheduleHUDDismiss(), which unconditionally sets showHUD = true); both tap paths now share one toggle. Info.plist still declared Syne-Variable.ttf, deleted in #22.

Gates

67 iOS tests (2 new: protocol-cap frame assembly, full-screen flick), simulator E2E green.

All found by the code-review pass over the merged revamp and confirmed
against the tree.

Crash on every host restart. The 4 Hz stats flush computed fragment
deltas with wrapping subtraction, but the assembler zeroes its counters
whenever it resets — which is exactly what a stream-epoch bump does. Two
~2^64 deltas were then added with a trapping +, so the app died the
moment the host restarted its pipeline: supervisor auto-restart, VDD
toggle, settings change, encoder crash recovery. The client killed itself
on the very path M6 built to survive. It now re-baselines when a counter
goes backwards.

Crash on a fast swipe. TouchRelay squared Int32 deltas of coordinates
spanning 0...65535; a flick across ~71% of the width overflows and Swift
traps. Widened to Int64.

Crash on connect and disconnect. FrameSlot.onFrameStored was a plain
stored closure, written on the MainActor by the Metal view's attach and
detach while the VideoToolbox decode thread called it — a race on the
closure's own retain/release. It is now swapped under a lock and copied
out before being called.

Auto-reconnect never fired. The flush loop skipped the rest of the tick
when no datagrams had arrived, and the only call to the liveness watchdog
sat below that guard. A dead host sends neither media nor heartbeats, so
the watchdog was silenced in precisely the situation it exists for: the
bar kept reading ON AIR over a frozen frame and the reconnect cycle from
#17 could never start. The watchdog now runs every tick.

Large frames were dropped forever. The assembler capped frames at 1024
fragments while the host fragments against, and the wire parser accepts,
3066. A 1440p or 4K scene-change IDR lands in that gap: every fragment
discarded, no sync sample, so every later frame discarded too, and the
client asks for a keyframe that comes back equally large. Permanent
freeze with healthy heartbeats on both ends. The cap now IS the protocol
cap; maxPendingBytes remains the real memory guard.

Also: the three-finger HUD gesture could only ever show the HUD, because
it called scheduleHUDDismiss() which unconditionally sets showHUD = true;
both tap paths now share one toggle. And Info.plist still declared
Syne-Variable.ttf, deleted in #22.
@whoisaldo
whoisaldo merged commit 612ca11 into main Aug 27, 2026
4 checks passed
@whoisaldo
whoisaldo deleted the fix/ios-crashes-and-stalls branch August 27, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant