Stabilize TX streaming: reconcile IT ring lap loss, fault dead IRQ path, classify replay misses#80
Stabilize TX streaming: reconcile IT ring lap loss, fault dead IRQ path, classify replay misses#80mrmidi wants to merge 6 commits into
Conversation
AR Request drains before AR Response, so a target's FCP response block write can arrive before our own command-write acknowledgement has been dispatched. The response itself proves the target received and processed the command; dropping it made the Duet 48 kHz prepublish sequence time out and deferred the audio nub entirely. Accept a validated response for the active write attempt and treat the later local write callback as a harmless stale event. Documented in AVC_RECOVERY_AND_SYNC_ALGO_AND_BUGS.md (committed with the stabilization change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The IT descriptor ring is free-running and completion accounting measured hardware progress modulo the 48-packet ring, so any refill gap over 6 ms silently discarded whole laps: the wire re-transmitted stale packets at full rate while completionCursor dilated against bus time, dragging the TX producer and the RX replay reader into a permanent all-zero-payload state (2026-07-19 Duet incident: interrupt path died mid-session, the watchdog carried the stream, and 71% of wire packets were stale re-sends). - Recover true progress from the OUTPUT_LAST completion timestamps (ComputeLostLapPackets, one packet per cycle), including the pointer-unmoved whole-lap stall, and reconcile completionCursor, softwareFillAbsIdx_, and completion stamps forward. New lapLossEvents/lapLossPacketsTotal counters and an IT LAP LOSS record. - Fault the transmit context after 16 consecutive IRQ-silent watchdog kicks and log the first watchdog engagement; watchdog-carried streaming re-sends stale ring laps between kicks and is never a valid steady state. - Classify TX replay read failures: re-anchor the reader in place on overwritten history (no frame-cursor realign; skipped entries only shift NODATA placement, legal under IEC 61883-6 blocking), hold the reader on ahead-of-producer, and reserve reader reset plus frame-cursor realignment for genuine RX-domain transitions. Documented in AVC_RECOVERY_AND_SYNC_ALGO_AND_BUGS.md as TX-LAPLOSS-001 (fixed, hardware-validated on a live Duet run) and TX-IRQ-001 (open: why the interrupt path died). Scheme file is xcodegen regeneration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware validation status — 2026-07-19 Duet endurance runLive run on the Apogee Duet with this branch installed: 31m50s of continuous duplex streaming, audibly clean end to end. Verified from three independent sources. FireBug wire capture (
|
| Signal | Count |
|---|---|
[TxAlign] realignments after stream start |
0 (one continuous host-frame mapping) |
[TxReplay] fail=overwritten / reclamped |
0 (reader never left its window) |
[TxReplay] fail=ahead |
4 (transient, absorbed in place — no reset, no realign) |
[PayloadWriter] frame losses |
0 |
Watchdog engagements (TX-IRQ-001) |
0 (interrupt path alive all session) |
Notes
- IT completion-delta high-water reached 43/48 packets between lap-loss events — this machine's scheduling load rides close to the 6 ms ring depth, so the reconciliation path is load-bearing, not theoretical.
TX-IRQ-001(the interrupt-path death that enabled yesterday's zombie) did not recur this session; if it does, the watchdog-engagement log will timestamp it and the 16-kick fatal bounds it to ~1 s.- Known cosmetic issue (follow-up, not this PR): a healthy run continuously rate-limit-logs
[TxPrepFrame] frame=1because the 400-cycle exposure target is structurally unreachable within the 678-packet preparation limit; actual exposure lead is ~27–35 ms, ~3× the safety floor, and no frames are lost.
🤖 Generated with Claude Code
The Saffire Pro 24 DSP run showed the lap-loss detector's blind spot: completion timestamps cannot distinguish free-running stale re-transmission from a stalled context whose sparse crawl stamps late timestamps (FireBug counted 13 wire packets where the timestamps implied 64k). Classify by coverage: unaccounted progress within the producer's committed lead is reconciled as before; progress beyond it can only end at an uncommitted slot, so fault immediately as context-stalled with the completion accounting untouched. Record ContextControl and the latched IntEvent bits (visible even when masked) in the lap-loss, context-stalled, and watchdog paths as first-fault evidence for the still-open freeze trigger (TX-STALL-001). Document the Saffire incident, TX-STALL-001, and the confirmed fault-propagation gap TX-FAULT-PROP-001. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Saffire run showed the lap-loss detector phantom-killing a healthy stream (all mod-48 deltas < 48; FireBug 2.24M continuous packets), and DICE was stable before the 2026-07-18 refactors (merge 1bbb70e, no lap-loss logic). Root cause is cross-backend leakage: a shared timingLossCallback_ (AV/C overwrites DICE) and cross-backend RecoverStreaming. references/linux-sound-firewire-stack proves the boundary (per-family recovery, family-blind mechanism, one content callback with errors polled upward); the Focusrite DICE kext corroborates DICE-notification-driven recovery. Records the corrective boundary and withdraws TX-LAPLOSS-001. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Status update — direction change, do not merge as-isHardware testing on a Saffire Pro 24 DSP (DICE) reversed the conclusion of the TX lap-loss work. Summary before this branch gets rebuilt. The lap-loss detector was the regression, not the fixOn the Saffire the detector phantom-killed a healthy stream. Every Reinforcing data point: DICE was stable before any of this — merge Real root cause: cross-backend leakage (
|
libffado-2.5.0 confirms dice != avc != bebob in ASFW's own idiom: every device is a FFADODevice subclass with per-family streaming/recovery virtuals (lock/unlock/prepare/resetForStreaming), a family-blind libstreaming manager, and one StreamProcessor-per-stream seam. Directs the corrective to per-backend virtual/override recovery rather than a shared callback slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware proved the timestamp-based lap-loss/context-stall detector phantom-kills healthy streams on both devices: Saffire and Duet each ran clean (completion cursor tracking the wire exactly, 0 silent packets) until the detector's stale-baseline double-count fabricated a multi- hundred-lap 'stall' on a single coalesced delta=0 read and faulted the context. It also corrupts audio in real time via the sub-fatal reconciliations that jump softwareFillAbsIdx_. Remove ComputeLostLapPackets, DetectLostLapPackets, the Refill reconcile/context-stalled path, the counters, the ContextStalled reason, and their tests. Kept: the backend-agnostic pieces — failure-class replay handling (hold on ahead, no realign) and the watchdog-silence fatal + ctrl/intEvent snapshot. Root cause is elsewhere (TX-IRQ-001 and backend boundary leakage); see AVC_RECOVERY_AND_SYNC_ALGO_AND_BUGS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Symptom
A Duet duplex stream could degrade into a "transport-alive, audio-dead" zombie: FireBug showed both isoch channels running at one packet per cycle with zero silent cycles, TX SYT tracking RX SYT exactly — and every PCM sample in the outgoing DATA packets zero. The 2026-07-19 incident ran in this state for ~35 minutes with no fault raised.
Root cause (confirmed from the live MCP driver ring)
The measured chain, each link verified in code and in the retained ring (17.3-minute window, zero dropped records):
IsochTxDmaRing::ComputeDeltaConsumed()measured progress from the command pointer — which is only defined modulo 48. Any refill gap longer than 6 ms silently discarded whole laps: the wire re-transmitted the stale 48 slots (a DBC seam per lap, invisible in SYT because 48 ≡ 0 mod 16 cycles) whilecompletionCursorunder-advanced with no fault raised.Poll()watchdog (~68 ms cadence) carried the stream, losing ~10 ring laps per kick. Net: 71% of the session's wire packets were stale re-transmissions;completionCursoradvanced at ~575 packets/s against the wire's 8,000/s.completionCursor, so producer time dilated to ~7% of bus time. The RX replay reader consumed ~575 entries/s against RX's healthy 8,000/s and fell out of its 512-entry history window every few tens of milliseconds ([TxReplay] fail=overwritten,d=-547…-920, epoch unchanged — no RX reset involved).AmdtpPayloadWriterclassified essentially every host frameoutsideand the pre-zeroed DATA payloads shipped as silence. The loop is self-sustaining.This was not the FW-64 callback overwrite, the unsafe AV/C background restart, or RX/TX SYT phase — those remain separate documented defects and were not required to produce this incident.
Changes
IsochTxDmaRing— true-cycle completion accounting (TX-LAPLOSS-001)ComputeLostLapPackets()recovers true progress from the OUTPUT_LAST completion timestamps (the context transmits exactly one packet per cycle), detects whole lost laps — including the pointer-unmoved exactly-one-lap stall, which was previously invisible — and reconcilescompletionCursor,softwareFillAbsIdx_, and the completion stamps forward. The producer's committed lead (678 packets) already covers the reconciliation jump, so a stall degrades to a bounded ≤6 ms glitch instead of permanent time dilation. NewlapLossEvents/lapLossPacketsTotalcounters plus anIT LAP LOSSring record make every event attributable.IsochTransmitContext::Poll()— sustained IRQ-silence fault (bounds TX-IRQ-001)The watchdog now logs its first engagement and, after 16 consecutive IRQ-silent kicks (~1 s), stops the context via the existing TX-fault path. Watchdog-carried streaming re-transmits stale ring laps between kicks and is never a valid steady state — a dead interrupt path now produces a clean, attributable fault within a second instead of a half-hour zombie.
PrepareTransmitSlots()— failure-class replay handlingReplay read failures are no longer uniformly "reset + re-arm alignment":
overwritten→ re-anchor the reader viaBegin()and retry in place ([TxReplay] reclamped). Skipped entries only shift NODATA placement, which IEC 61883-6 blocking permits; DBC continuity is packetizer-owned and the SYT offset drifts sub-tick across the skipped span. No alignment re-arm.ahead→ hold the reader and ship one NODATA; the same cursor reads successfully once RX catches up.This removes the realign churn that orphaned host frames.
FCPTransport— accept response delivered before local write completion (separate commit)AR Request drains before AR Response, so a target's FCP response can arrive before our own command-write acknowledgement. Dropping it made the Duet 48 kHz prepublish time out and deferred the audio nub. The response itself proves delivery; it is now accepted for the active write attempt, and the later local write callback is a harmless stale event.
Validation
IsochTxLapLossMath.*(timestamp math incl. 8-second wrap and jitter tolerance),IsochTxDmaRingTest.RefillReconcilesLostLapForward,IsochTxDmaRingTest.RefillDetectsWholeLapStallWithUnmovedPointer,RxDrivenTimingTests.ReaderReBeginReanchorsAfterHistoryOverwritten, and the updatedFCPTransportTests.AcceptsResponseBeforeCommandWriteCompletion../build.sh --no-bumpsucceeds (the 6 analyzer warnings are pre-existing).[TxAlign]at stream start, zero[TxReplay]failures after startup, zero payload-writer losses — and one real ~8.5 ms refill stall at ~3 min was caught and reconciled (IT LAP LOSS: ring lapped 48 packets … delta=20,committed − completion = 678) with no audible consequence and no downstream thrash. On the old code that exact event class started the death spiral.Still open (documented in
AVC_RECOVERY_AND_SYNC_ALGO_AND_BUGS.md)AUDIO-RECOVERY-ROUTING-001callback overwrite,AVC-RECOVERY-002unsafe background restart) are unchanged by this PR.[TxPrepFrame] frame=1continuously because the 400-cycle exposure target is structurally unreachable within the 678-packet preparation limit (actual exposure lead ~27–35 ms, ~3× the safety floor — no frames lost). TheframeShortgate should move to the real safety floor.🤖 Generated with Claude Code