feat(wire): mid-session shard-payload renegotiation — the black screen heals in seconds, jumbo behind an opt-in #41

Merged
enricobuehler merged 3 commits from worktree-shard-payload-reneg into main 2026-08-04 18:06:44 +00:00
Owner

Leg 2 of the MTU program (planning design/shard-payload-reneg.md), stacked on #37's verdict machinery. Leg 1 taught the host to diagnose a constrained path and clamp the next session; this makes the current session heal at the ~3–10 s verdict mark — and uses the same machinery, inverted, to grow a proven 9000-MTU LAN to ~8.9 KB shards (~6× fewer packets per frame).

Phase 0 — client-side groundwork (290d760e, separately shippable)

  • Per-frame geometry: the reassembler's strict shard_bytes firewall becomes per-frame pinning — a frame's first-arriving packet pins its shard size (bounds-checked to [MIN_SHARD_PAYLOAD, receive ceiling], even), later packets must match the pin, and the per-frame block ceiling derives from the pinned size. The reorder race between the ordered control stream and unordered video dies structurally: old-geometry frames in flight complete under their own pin while new frames arrive under the new one, and no cross-geometry splice can land in one buffer. The in-flight budget stays byte-based and exact.
  • MAX_DATAGRAM_BYTES 2048 → 9216: every receive path accepts sealed jumbo datagrams. Grep verdict: no embedder uses the constant directly ⇒ no C ABI bump; the regenerated header rides along. Static buffers won over resize-on-ack: the recvmmsg-ring delta is 128 × ~7 KiB ≈ 896 KiB per client session, lazily allocated, hosts unaffected.
  • Hello::max_shard_payload: u16 (trailing append, 0/absent = legacy): one field is both the renegotiation capability flag and the jumbo ceiling. Advertised by the shared pump (all client families) and the probe.

Phases 1–2 — the messages and the driver (34ad3cc6)

  • MSG_SHARD_PAYLOAD_CHANGED/_ACK (0x08/0x09). Asymmetric by design: a shrink re-keys at the next AU immediately after sending (pinning makes ordering irrelevant; the ack is telemetry); a grow emits nothing above the old size until the ack — the ack is the gate even though client buffers would fit.
  • Client: one dispatch arm in the shared pump control task — validate against the advertised bounds, ack; out-of-bounds gets silence, not an ack, so a buggy host can never read a granted grow out of garbage.
  • Host: the wire_mtu watcher grows a driver arm — a below-ceiling verdict still records the learned budget (session 2 starts right) and now shrinks session 1 live; with the jumbo opt-in (PUNKTFUNK_JUMBO=1, or PUNKTFUNK_WIRE_MTU > 1500 — one knob, derived) it raises the QUIC MTUD probe ceiling (per-endpoint; a few extra failed probes toward non-jumbo peers, zero cost otherwise), sends the ack-gated grow after a settled-at-sealed-jumbo proof, then stays alive as the revert guard: quinn's blackhole detection lowering current_mtu shrinks the wire back through the same path.
  • Apply point: Session::set_shard_payload drained in the send loop next to the adaptive-FEC target, gated on no open streamed AU.
  • PyroWave sessions are gated out (implementation finding beyond the plan): their clients parse chunk-aligned AUs in windows of the Welcome value read once over the C ABI, so a mid-stream re-key would corrupt the parse — they keep the leg-1 next-session clamp, which is codec-independent and fully consistent (everything derives from the one Welcome::shard_payload). This also settles the plan's open question on the two wire_chunk consumers — both are PyroWave-only. Lifting the gate later = per-frame window over the C ABI (v15), its own leg.
  • Legacy peers are inert both ways: no advertisement ⇒ no driver; an old host never sends the message.

Verification (the 0.23.0 lesson: geometry changes breed sizing bugs)

  • Slice-wire regression suite re-run at shard 512 / 1216 / 1408 / 8908 (exact-multiple sweep, lossy + reversed roundtrips, sentinel path, in-flight budget).
  • Mixed-geometry reorder-torture proptest (48 cases): frames of different sizes and wire shapes shuffled into one delivery with per-frame recoverable loss — every frame must arrive byte-identical and the in-flight budget must return to exactly zero.
  • Sealed-loopback session tests asserting delivered frames (never absence of errors): mid-stream shrink → jumbo grow → revert through one continuous crypto/replay stream; the old-geometry reorder race; cross-geometry splice rejection; firewall bounds non-vacuous both ways.
  • The low-MTU chunk-aligned guarantee pinned at the clamp shapes a constrained path actually produces (1216 WARP budget, 512 floor) with real loss (d1c4cb18).
  • Gates: mac core 297/297 --features quic + clippy -D warnings + fmt; pf-lxcheck container host clippy -D warnings + host native:: + core 297/297 Linux; cargo ndk arm64-v8a + armeabi-v7a; header drift committed.

Still owed (on-glass)

The field reporter's VPN box for the shrink leg (expect black → WARN → heals in-session), a 9000-MTU NIC+switch pair for the grow leg (check whether the .42/.173 encode boxes support jumbo first), and the Android on-device memory reading.

Leg 2 of the MTU program (planning `design/shard-payload-reneg.md`), stacked on #37's verdict machinery. Leg 1 taught the host to *diagnose* a constrained path and clamp the **next** session; this makes the **current** session heal at the ~3–10 s verdict mark — and uses the same machinery, inverted, to grow a proven 9000-MTU LAN to ~8.9 KB shards (~6× fewer packets per frame). ### Phase 0 — client-side groundwork (`290d760e`, separately shippable) - **Per-frame geometry**: the reassembler's strict `shard_bytes` firewall becomes per-frame **pinning** — a frame's first-arriving packet pins its shard size (bounds-checked to `[MIN_SHARD_PAYLOAD, receive ceiling]`, even), later packets must match the pin, and the per-frame block ceiling derives from the pinned size. The reorder race between the ordered control stream and unordered video dies structurally: old-geometry frames in flight complete under their own pin while new frames arrive under the new one, and no cross-geometry splice can land in one buffer. The in-flight budget stays byte-based and exact. - **`MAX_DATAGRAM_BYTES` 2048 → 9216**: every receive path accepts sealed jumbo datagrams. Grep verdict: no embedder uses the constant directly ⇒ **no C ABI bump**; the regenerated header rides along. Static buffers won over resize-on-ack: the recvmmsg-ring delta is 128 × ~7 KiB ≈ **896 KiB per client session**, lazily allocated, hosts unaffected. - **`Hello::max_shard_payload: u16`** (trailing append, 0/absent = legacy): one field is both the renegotiation capability flag and the jumbo ceiling. Advertised by the shared pump (all client families) and the probe. ### Phases 1–2 — the messages and the driver (`34ad3cc6`) - `MSG_SHARD_PAYLOAD_CHANGED`/`_ACK` (0x08/0x09). Asymmetric by design: a **shrink** re-keys at the next AU immediately after sending (pinning makes ordering irrelevant; the ack is telemetry); a **grow** emits nothing above the old size until the ack — the ack is the gate even though client buffers would fit. - Client: one dispatch arm in the shared pump control task — validate against the advertised bounds, ack; out-of-bounds gets **silence**, not an ack, so a buggy host can never read a granted grow out of garbage. - Host: the `wire_mtu` watcher grows a driver arm — a below-ceiling verdict still records the learned budget (session 2 starts right) **and now shrinks session 1 live**; with the jumbo opt-in (`PUNKTFUNK_JUMBO=1`, or `PUNKTFUNK_WIRE_MTU` > 1500 — one knob, derived) it raises the QUIC MTUD probe ceiling (per-endpoint; a few extra failed probes toward non-jumbo peers, zero cost otherwise), sends the ack-gated grow after a settled-at-sealed-jumbo proof, then stays alive as the **revert guard**: quinn's blackhole detection lowering `current_mtu` shrinks the wire back through the same path. - Apply point: `Session::set_shard_payload` drained in the send loop next to the adaptive-FEC target, gated on **no open streamed AU**. - **PyroWave sessions are gated out** (implementation finding beyond the plan): their clients parse chunk-aligned AUs in windows of the `Welcome` value read **once** over the C ABI, so a mid-stream re-key would corrupt the parse — they keep the leg-1 next-session clamp, which is codec-independent and fully consistent (everything derives from the one `Welcome::shard_payload`). This also settles the plan's open question on the two `wire_chunk` consumers — both are PyroWave-only. Lifting the gate later = per-frame window over the C ABI (v15), its own leg. - Legacy peers are inert both ways: no advertisement ⇒ no driver; an old host never sends the message. ### Verification (the 0.23.0 lesson: geometry changes breed sizing bugs) - Slice-wire regression suite re-run at shard **512 / 1216 / 1408 / 8908** (exact-multiple sweep, lossy + reversed roundtrips, sentinel path, in-flight budget). - Mixed-geometry **reorder-torture proptest** (48 cases): frames of different sizes and wire shapes shuffled into one delivery with per-frame recoverable loss — every frame must arrive byte-identical and the in-flight budget must return to exactly zero. - Sealed-loopback session tests asserting **delivered frames** (never absence of errors): mid-stream shrink → jumbo grow → revert through one continuous crypto/replay stream; the old-geometry reorder race; cross-geometry splice rejection; firewall bounds non-vacuous both ways. - The low-MTU chunk-aligned guarantee pinned at the clamp shapes a constrained path actually produces (1216 WARP budget, 512 floor) with real loss (`d1c4cb18`). - Gates: mac core **297/297** `--features quic` + clippy `-D warnings` + fmt; pf-lxcheck container host clippy `-D warnings` + host `native::` + core 297/297 Linux; `cargo ndk` arm64-v8a + armeabi-v7a; header drift committed. ### Still owed (on-glass) The field reporter's VPN box for the shrink leg (expect black → WARN → heals in-session), a 9000-MTU NIC+switch pair for the grow leg (check whether the .42/.173 encode boxes support jumbo first), and the Android on-device memory reading.
enricobuehler added 3 commits 2026-08-04 18:05:08 +00:00
Phase 0 of mid-session shard-payload renegotiation (planning
design/shard-payload-reneg.md), stacked on the leg-1 MTU resilience. All
three legs are client-side and forward-compatible: deployed clients that
carry them accept a mid-session shard change the moment a future host
sends one, and nothing changes on the wire until then.

- W0.1 — the reassembler's strict shard_bytes firewall becomes per-frame
  pinning: a frame's first-arriving packet pins that frame's shard size
  (bounds-checked to [min_shard_bytes, max_shard_bytes], even), later
  packets must match the pin, and the per-frame block ceiling derives
  from the pinned size (a session-level cap would reject legitimate
  post-shrink frames). The reorder race between an ordered control
  message and unordered video dies structurally: old-geometry frames in
  flight complete under their own pin while new frames arrive under the
  new one, and no cross-geometry splice can land in one buffer. The
  in-flight budget stays byte-based and exact.
- W0.2 — MAX_DATAGRAM_BYTES 2048 → 9216: every receive path (transport
  RECV_BUF, the recvmmsg ring) now accepts sealed jumbo datagrams
  (9000-MTU LAN ≈ 8908-byte shards). Static buffers over resize-on-ack:
  the ring delta is 128 × ~7 KiB ≈ 896 KiB per client session, lazily
  allocated, hosts unaffected. Grep verdict: no embedder uses the
  constant directly, so no C ABI bump — the regenerated header rides
  along (drift gate).
- W0.3 — trailing Hello field max_shard_payload: u16 (0/absent =
  legacy), the append-with-placeholder discipline of video_caps/
  client_caps. One field is both the renegotiation capability flag and
  the jumbo ceiling; core's pump advertises it for all client families,
  the probe too.
- Host seam for Phase 1, dead until wired: Packetizer::set_shard_payload
  (re-derives the block ceilings; construction delegates to it) +
  Session::set_shard_payload (host-only, Config::validate parity).

Verification (the 0.23.0 lesson — geometry changes breed sizing bugs):
the slice-wire suite re-runs at shard 512/1216/1408/8908 (exact-multiple
sweep, lossy + reversed roundtrips, sentinel path, in-flight budget);
mid-stream shrink→grow→revert delivery; the old-geometry reorder race;
cross-geometry splice rejection; firewall bounds non-vacuous both ways;
a 48-case mixed-geometry reorder-torture proptest asserting per-frame
byte-identical DELIVERY and an exactly-zero final budget; and a sealed
loopback session test (continuous crypto/replay) delivering frames
across live re-keys — every test asserts delivered frames, never the
absence of errors.

core: 294/294 --features quic + clippy -D warnings (macOS), fmt.
Phases 1-2 of design/shard-payload-reneg.md, on top of the Phase 0
per-frame geometry. The leg-1 watcher stops merely diagnosing the
constrained path and heals the CURRENT session; the same machinery,
inverted, takes a proven jumbo LAN up to ~8.9 KB shards.

- Messages: MSG_SHARD_PAYLOAD_CHANGED (0x08, host→client, {shard_payload
  u16}) and MSG_SHARD_PAYLOAD_ACK (0x09, the echo). Asymmetric by
  design: a shrink re-keys the packetizer at the next AU immediately
  after sending (per-frame pinning makes ordering irrelevant; the ack is
  telemetry), a grow emits nothing above the old size until the ack —
  the ack is the gate even though client buffers are statically sized.
- Client: one dispatch arm in the shared pump control task (all client
  families) — validate against the advertised receive bounds, ack;
  out-of-bounds requests get SILENCE, not an ack, so a buggy host can
  never read a granted grow out of garbage.
- Host driver: the wire_mtu watcher grows a ShardReneg arm — on a
  below-ceiling verdict it still records the learned budget (session 2
  starts right) and now also shrinks session 1 at the ~3-10 s verdict
  mark; with the jumbo opt-in (PUNKTFUNK_JUMBO=1, or PUNKTFUNK_WIRE_MTU
  > 1500 — one knob, derived) it sends the ack-gated grow after a
  settled-at-sealed-jumbo proof and then stays alive as the revert
  guard: quinn's blackhole detection lowering current_mtu shrinks the
  wire back through the same path. The QUIC MTUD probe ceiling rises
  from 1472 to the sealed jumbo size with the opt-in (per-ENDPOINT: a
  few extra failed probes toward non-jumbo peers, zero cost otherwise).
- Apply point: Session::set_shard_payload drained in the send loop next
  to the adaptive-FEC target, gated on no open streamed AU (a streamed
  frame's shard-aligned tiling derives from the size it began with).
- Renegotiation is gated OFF for PyroWave sessions: their clients parse
  chunk-aligned AUs in windows of the Welcome value pinned at session
  start (read once over the C ABI), so a mid-stream re-key would corrupt
  the parse — those sessions keep the leg-1 next-session clamp. This
  also settles the plan's open question on the two wire_chunk consumers:
  both are PyroWave-only, so the gate covers them entirely.
- Legacy peers are inert both ways: no Hello advertisement → the host
  never constructs the driver; an old host never sends the message.

core: 296/296 --features quic + clippy -D warnings (macOS), fmt; the
regenerated header carries the new message ids (drift gate).
test(core/session): pin the low-MTU chunk-aligned guarantee at clamped shard sizes
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m44s
ci / web (pull_request) Successful in 1m2s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m6s
ci / docs-site (pull_request) Successful in 2m3s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 1m51s
android / android (pull_request) Successful in 5m21s
ci / rust (pull_request) Successful in 8m33s
d1c4cb18dd
PyroWave sessions are gated out of mid-session renegotiation, so a
constrained path serves them through the leg-1 SESSION-START clamp. This
pins the consistency that guarantee rests on: everything chunk-aligned
derives from the one Welcome::shard_payload number — the host
packetizes at it, the client's C-ABI parse window reads it back, and
partial delivery zero-fills exact windows of it — verified at the two
clamp shapes a constrained path actually produces (1216, the
WARP/Tailscale budget, and the 512 floor) over the sealed loopback wire
with real loss.
enricobuehler merged commit 9f1f23eb40 into main 2026-08-04 18:06:44 +00:00
enricobuehler deleted branch worktree-shard-payload-reneg 2026-08-04 18:06:48 +00:00
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#41