commit d6d4dfeaf98ef73310e931708f610c309db77dc2
parent acf91fce8cecdf5ac8d5aabb9e20601124513252
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Mon, 1 Jun 2026 16:18:08 -0500
test: offline mux round-trip suite disproves 4 corruption theories
Adds MuxState::roundtrip_decode + tests proving the encoding is structurally
sound (faithful round-trip on the user's real routing incl ADAT 7/8, all ADAT
dests present, no dup dests, per-table lengths match assignment ≤77). The 0x3
on SET_MUX is therefore not in our encoding logic — it's live-wire framing or a
device-state precondition. Documented as the key open bug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat:
1 file changed, 97 insertions(+), 0 deletions(-)
diff --git a/scarlett-core/src/mux.rs b/scarlett-core/src/mux.rs
@@ -217,6 +217,23 @@ impl MuxState {
}
out
}
+
+ /// Round-trip a set of device entries through encode and decode the result
+ /// back to `dest_id -> source_id`, for offline verification that our encoding
+ /// reproduces the device's routing without loss. Pure; no device.
+ pub fn roundtrip_decode(entries: &[MuxEntry], pc: [(u16, u16); 6]) -> Vec<(u16, u16)> {
+ let st = MuxState::from_entries(pc, entries);
+ let assign = mux_assignment_18i20_gen3();
+ let mut out = Vec::new();
+ for v in st.encode_table(&assign[0]) {
+ let dst = (v & 0xfff) as u16;
+ let src = ((v >> 12) & 0xfff) as u16;
+ if dst != 0 {
+ out.push((dst, src));
+ }
+ }
+ out
+ }
}
/// A left/right grouping of ports: `right` is None for a lone mono port.
@@ -318,6 +335,86 @@ mod tests {
const PC: [(u16, u16); 6] = PORT_COUNT_18I20_GEN3;
#[test]
+ fn roundtrip_preserves_real_device_routing() {
+ // The user's actual routing (from adatverify), incl. ADAT 7/8 ← PCM 19/20.
+ // The round-trip MUST reproduce every entry — if any drops to Off, that's
+ // the corruption that zeroed ADAT 7/8 on hardware.
+ let e = |dst: u16, src: u16| MuxEntry { dest: dst, source: src };
+ let entries = vec![
+ // ADAT Out 1-8 (0x200..0x207) ← PCM 13-20 (0x60c..0x613)
+ e(0x200, 0x60c), e(0x201, 0x60d), e(0x202, 0x60e), e(0x203, 0x60f),
+ e(0x204, 0x610), e(0x205, 0x611), e(0x206, 0x612), e(0x207, 0x613),
+ // Analogue Out 1-2 ← PCM 1-2
+ e(0x080, 0x600), e(0x081, 0x601),
+ // Mixer In 1-2 ← Analogue 1-2
+ e(0x300, 0x080), e(0x301, 0x081),
+ ];
+ let got = MuxState::roundtrip_decode(&entries, PC);
+ for me in &entries {
+ let (dst, src) = (me.dest, me.source);
+ let found = got.iter().find(|(d, _)| *d == dst).map(|(_, s)| *s);
+ assert_eq!(
+ found,
+ Some(src),
+ "dest {dst:#05x} should map to src {src:#05x}, got {found:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn per_table_entry_counts_match_assignment() {
+ // Each encoded table's length must equal the sum of its assignment
+ // entry counts (the kernel sends exactly that many words). A mismatch —
+ // especially an over-long table 1/2 — is a prime 0x3 suspect.
+ let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] };
+ let assign = mux_assignment_18i20_gen3();
+ for (t, a) in assign.iter().enumerate() {
+ let expected: usize = a.iter().map(|e| e.count as usize).sum();
+ let got = st.encode_table(a).len();
+ assert_eq!(got, expected, "table {t}: encoded {got} != assignment sum {expected}");
+ }
+ // Print the actual per-table sizes for the record.
+ let sizes: Vec<usize> = assign.iter().map(|a| st.encode_table(a).len()).collect();
+ // Kernel/Focusrite: tables are 77, 75, 36-ish depending on band. Just
+ // assert table 0 is the largest and all are <= SCARLETT2_MUX_MAX (77).
+ assert!(sizes.iter().all(|&s| s <= 77), "a table exceeds MUX_MAX 77: {sizes:?}");
+ }
+
+ #[test]
+ fn encoded_table_covers_all_adat_destinations() {
+ // Every ADAT Out (0x200..0x207) must appear as a destination in the
+ // encoded table 0. If 7/8 are missing, the assignment table is wrong and
+ // a write would corrupt/zero them — the suspected cause.
+ let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] };
+ let table = st.encode_table(&mux_assignment_18i20_gen3()[0]);
+ let dests: std::collections::HashSet<u16> =
+ table.iter().map(|v| (v & 0xfff) as u16).collect();
+ for i in 0..8u16 {
+ assert!(
+ dests.contains(&(0x200 + i)),
+ "ADAT Out {} (id {:#05x}) missing from encoded table",
+ i + 1,
+ 0x200 + i
+ );
+ }
+ }
+
+ #[test]
+ fn encoded_table_has_no_duplicate_destinations() {
+ // Each destination must appear at most once; a dup means two entries
+ // fight over one dest (device may reject → 0x3, or last-wins corrupts).
+ let st = MuxState { pc: PC, mux: vec![0u16; num_dsts(&PC)] };
+ let table = st.encode_table(&mux_assignment_18i20_gen3()[0]);
+ let mut seen = std::collections::HashSet::new();
+ for v in &table {
+ let dst = (v & 0xfff) as u16;
+ if dst != 0 {
+ assert!(seen.insert(dst), "duplicate destination {dst:#05x} in table");
+ }
+ }
+ }
+
+ #[test]
fn dest_pairs_group_stereo_and_keep_mono() {
let dp = dest_pairs(&PC);
// First row: Analogue Out 1-2 (paired).