Skip to content

(DICE-3) Read Channel Names From the ROM#48

Open
alicankaralar wants to merge 16 commits into
mrmidi:mainfrom
alicankaralar:feat/dice-channel-names
Open

(DICE-3) Read Channel Names From the ROM#48
alicankaralar wants to merge 16 commits into
mrmidi:mainfrom
alicankaralar:feat/dice-channel-names

Conversation

@alicankaralar

@alicankaralar alicankaralar commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Populates the names of the channels for DICE devices, via the values retrieved from the ROM. In DICE devices, the channel names exist in some of the registers of the ROM. We're using those values where available. Tested on Venice F32.

It sits on DICE Venice Dual Stream PR and DICE Aggregate device fix which has to be merged before this branch.

alicankaralar and others added 8 commits July 2, 2026 21:00
Extend IsochService to manage a second (and up to 4th) isochronous capture
and playback stream for multi-stream DICE devices (Venice F32 = 2x16 ch),
without disturbing the single-stream path.

- Stream 0 stays the "master" context, owning the clock/ZTS/replay/TX-prep
  role exactly as before; secondary streams [1..4) layer on top, each on its
  own OHCI context (contextIndex == streamIndex).
- New PrepareReceiveStream/PrepareTransmitStream create + configure secondary
  contexts; StartPreparedReceive/Transmit, StopReceive/Transmit and StopAll
  iterate the secondaries. PrepareReceiveStream records the de-interleave
  channel offset (e.g. 16 for the 2nd 16-ch slice) for the audio-engine pass.
- Index-parameterized ReceiveContext/TransmitContext accessors.
- Surface both Prepare*Stream calls through IDiceHostTransport so the engine
  pass can drive them.

Production code does not call the new methods yet, so secondaries are always
null and every per-stream loop is a no-op: single-stream behavior is
byte-for-byte unchanged. Channel-offset application in the RX decoder and the
2nd TX payload slab are the next (audio-engine) pass.

Adds container/accessor unit tests; full C++ suite (1153) and the dext build
pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the receive half of multi-stream DICE capture: each wire stream's
16-ch CIP is decoded into the single shared 32-ch input buffer at a channel
offset, so CoreAudio sees one device while the bus carries 2×16 (Venice F32).

- RxAudioPacketProcessor::ProcessPacket gains channelOffset + publishTimeline:
  it writes its slice at frame+channelOffset and only the master advances the
  producer cursor/frame counters.
- IsochReceiveContext gains channelOffset_/streamChannels_/isSecondary_: a
  secondary context binds the shared input buffer and writes PCM only, skipping
  ALL master-only bookkeeping (clock/replay/ZTS/DBC reset + clock-publisher
  bind). The two halves are frame-locked by the device clock; the input safety
  offset absorbs sub-cycle inter-context skew.
- IsochService::PrepareReceive takes streamChannels (master slice width);
  PrepareReceiveStream binds the shared buffer + per-stream offset/slots/count.
- DiceDuplexRestartCoordinator prepares the master capture stream with its
  per-stream slot count, then loops the secondary capture streams at running
  channel offsets. Single-stream devices keep streamChannels==0 (full width)
  and the aggregate slot count — byte-for-byte unchanged.

NOTE: per-stream wire DBS (16) now flows to the RX geometry check, fixing the
previous aggregate-slots (32) mismatch that would have rejected every Venice
packet. TX de-interleave (2nd payload slab + writer) is the next pass; until
then host playback drives only the first 16 channels.

Full C++ suite (1153) and the dext build pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HW confirmed the 2×16 capture foundation works (ZTS fires, StartIO
completes, channels 1–16 correct), but channels 17–32 were misaligned:
the secondary IR context wrote its de-interleaved slice at a private
free-running cursor (from 0) with no relation to the master's ring
position. The two OHCI IR contexts arm/start at different times, so by
the time the secondary's binding is armed the master has already produced
N frames — leaving the second 16-ch half skewed by a large constant (plus
drift), which shows up as the wrong/garbage input on 17–32.

Anchor the secondary cursor to the master's published inputProducedEndFrame
on the first write of each replay epoch (re-anchored when rxReplayEpochResets
changes). The two streams are frame-locked by the device clock, so once
re-based to the master's leading edge they stay aligned; the input safety
offset absorbs the sub-drain phase. Packets are dropped (not written) until
the master has produced, so we never write a mis-anchored slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interrupt dispatcher only ever polled receive context 0 (master),
gated on isoRecvEvent & 0x01. A multi-stream DICE device (Venice F32 =
2×16) runs a secondary IR context (context 1, event bit 0x02) whose
interrupt fires but was never serviced — its DMA ring filled and drained
nothing, so the second 16-ch capture slice (channels 17–32) never reached
the input buffer. This is why the secondary-slice frame anchoring (02da155)
had no effect: the stream it anchored never ran.

Dispatch a poll for every context whose event bit is set in isoRecvEvent
(contextIndex == streamIndex, bit == 1 << contextIndex), master first so
the producer timeline is published before secondary slices anchor to it.
ReceiveContext(streamIndex) returns nullptr for unprepared/out-of-range
contexts, so single-stream devices are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Venice's device RX is 2 streams × 16ch (DBS=16), but the host transmit
engine was configured from the profile's aggregate (DBS=32/pcm=32), which
the device's 16-slot RX rejects — so playback was fully broken.

Decouple the per-stream wire geometry from the HAL aggregate:
- Add IDiceDeviceProfile::TxStreamCount()/RxStreamCount() (default 1).
- TxChannelCount()/RxChannelCount() now return per-stream PCM × stream
  count, so the HAL aggregate is unchanged for single-stream devices and
  stays 32-out for Venice (16 × 2).
- MidasVeniceProfile: BuildDefaultTxStreamConfig now describes ONE wire
  stream (16ch / DBS=16); TxStreamCount() == 2. RX is left as the
  aggregate (capture geometry comes from device runtime caps, not this
  profile — input is already 2×16 on the wire and unaffected).

StartIO builds the master TX engine from this per-stream config, so it now
transmits DBS=16 matching device RX[0]. This is the master half of the TX
de-interleave: it fixes playback channels 1–16. Channels 17–32 need the
second TX stream (2nd IT context/engine/slab), which follows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prep for the second TX stream: AmdtpStreamConfig / DiceStreamConfig carry
a sourceChannelOffset, and AmdtpPayloadWriter encodes host channels
[sourceChannelOffset, sourceChannelOffset + pcmChannels) of the shared
32-ch output buffer — the TX mirror of the RX channelOffset. Defaults to 0,
so single-stream and the current master path are byte-for-byte unchanged.
The second Venice stream will set offset=16 to encode channels 17–32.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…7–32)

Build the full second TX pipeline so a multi-stream DICE device (Venice
F32 = 2×16) drives both of its RX streams, delivering playback channels
17–32. Strictly additive: gated on TxStreamCount() > 1, the master
(stream 0) path is byte-for-byte unchanged, so single-stream devices are
unaffected.

Transport (IsochService):
- Per-stream TX shared resources (txPayloadSlab_/metadata/control are now
  arrays); AllocateTxIsochResources takes a streamIndex and frees only that
  stream. PrepareTransmitStream wires the secondary IT context to its own
  slab and fails closed (kIOReturnNotReady) if unallocated.
- IsochTxDmaRing gains opt-in channel stamping: a secondary ring overrides
  the packet-header channel [13:8] with its Configure() channel, so it rides
  its own iso channel without the audio-side slot provider knowing it. The
  master copies the producer header verbatim (unchanged; all-zero no-packet
  sentinel preserved).
- InterruptDispatcher services every signalled IT context, not just ctx 0
  (mirrors the IR fix).

Bringup (DiceDuplexRestartCoordinator): prepare a host IT context per
playback stream (PrepareTransmitStream loop), transmitting on each device
RX stream's iso channel.

Audio engine: a secondary DiceTxStreamEngine + slot provider + shared
slab in the runtime/ivars. StartIO allocates/maps/configures it with
sourceChannelOffset = pcmChannels (encodes host channels 17–32). The RT
pump and IO write shadow the master in lockstep (same packetIndex/SYT/
disposition, payload from offset 16); prefill seeds both rings. AMDTP
payload writer + Dice/AmdtpStreamConfig carry sourceChannelOffset.

Teardown drops txSecondaryActive before releasing the secondary slab on
every path (StopIO, loud teardown).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The secondary transmit context defaulted to OHCI IT context 0 — the same
hardware context as the master — because IsochTransmitContext::Configure
never set contextIndex_. On HW both streams wrote context 0's CommandPtr/
ContextControl registers and enabled the context-0 interrupt; the
secondary's Start() clobbered the master's program and the first refill hit
a command-pointer-decode fault → IT FATAL STOP (log: both contexts
"Enabled IT interrupt for context 0", secondary cmdPtr decode exitDecode=2).

Add SetContextIndex() and assign contextIndex_ = streamIndex for the
secondary in PrepareTransmitStream, mirroring the IR side (contextIndex ==
streamIndex). The secondary now runs on IT context 1, enables interrupt
bit 0x02, and is serviced by the multi-context dispatcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alicankaralar and others added 8 commits July 7, 2026 13:41
kRxPcmChannels 32 -> 16 with RxStreamCount() == 2, so both directions
describe one wire stream (DBS=16) and the HAL aggregate stays 16 x 2 = 32.
Behavior-neutral: capture geometry comes from device runtime caps, and
RxDbs() has no consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eardown

IsochReceiveContext was an OSObject created via plain `new` (MakeOSObject)
instead of OSTypeAlloc. In DriverKit, OSMetaClassBase has no constructor
setting refcount — only OSObjectAllocate() (the OSTypeAlloc path) does — so
the object was born with refcount 0. The single release() in ~IsochService
then underflowed and hit the DriverKit over-release assert, SIGABRT-ing the
whole dext during ASFWDriver::free() (crash report 2026-07-03 13:21). A
crashed dext is not relaunched, so the device never re-published until
reboot; this also explains recurring shutdown stalls.

Nothing needs OSObject semantics here (no OSAction, no cast, no IIG), so
make it a plain final class owned by std::unique_ptr, matching
IsochTransmitContext, and delete the MakeOSObject helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ammed the AR path

OHCI §8.4.2: AR bufferFill appends a status/timestamp trailer quadlet to
every received packet. ARPacketParser treated it as optional: a packet whose
header+payload ended flush with the received-byte frontier parsed as
"complete" with totalLength 4 bytes short. The trailer then arrived as an
orphan quadlet at the head of the unread stream, every subsequent parse was
misaligned (responses completed against unknown tLabels, then an unparseable
byte pattern), and the AR response path jammed permanently — all async on
the bus timed out (ackCode=0x1: devices kept ACKing) until reboot.

Field trace 2026-07-03 13:53:28: tLabel=6's 104-byte read-block response
consumed as 120 bytes, its trailer left as the "offset 0/4" tail in
buffer[5]; three real responses destroyed ("No transaction for key"),
parser stuck at offset 56 forever. Chunked DICE section reads made the
flush-boundary case likely enough to hit on the third bring-up.

Fix: no trailer in the window means the packet is incomplete — return
nullopt so the tail is preserved and the stitched cross-buffer retry picks
the packet up whole once the trailer lands.

Adds a parser-level regression test and a ring-level reproduction of the
exact trailer-only straddle; fixes the tLabel-extraction test fixture to be
a valid AR DMA image (trailer included).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DICE stores global-section strings little-endian: the first character of
each quadlet lives in the least-significant byte. The wire transmits
quadlets big-endian, so reading characters MSB-first byte-reversed every
quadlet ("Veni" -> "ineV"). Read LSB-first instead.

Extracted the decode into DecodeDiceNickname() (header-only) and added
DiceNicknameTests covering the Venice regression, short names, payload
bounds, and the empty case.

cross-validated with FFADO dice_avdevice.cpp:696
("Strings from the device are always little-endian").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CopyLabelBlob had the same byte-order bug as the nickname: it read each
quadlet big-endian and emitted MSB-first, byte-reversing every 4-char
group. DICE stores text fields little-endian (first char in the LSB), so
emit LSB-first.

Add SplitDiceLabels() to split a decoded TX/RX name blob into per-channel
names (separator '\', list terminated by '\\'), mirroring FFADO
splitNameString/splitString. Preserves a leading empty token so the
result index stays aligned with the channel index. Covered by
DiceLabelTests; the nickname/label byte order is locked by DiceNicknameTests.

cross-validated with FFADO dice_avdevice.cpp:1527,1547 (TX/RX name strings
little-endian) + splitNameString + ffadotypes.h splitString.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the plumbing to surface real per-channel names (not just "In N"/"Out N")
without changing behavior when no labels are present:

- Widen kMaxNamedChannels 8 -> 32 (Venice F32 is 32x32); the ivars name
  arrays and the SetElementName loops follow the constant instead of a
  hardcoded 8, so all elements can be named.
- Model::ASFWAudioDevice gains input/outputChannelNames vectors, published
  as OSArray<OSString> under kInput/kOutputChannelNames.
- The audio side parses them into deviceInput/OutputChannelNames; a single
  BuildChannelNamesFromPlugs() now prefers a device label per slot and falls
  back to the synthesized "<plug> N" for empty slots. Both the initial parse
  and BuildAudioGraph's post-profile regeneration route through it, so they
  agree.

Inert until a backend populates the names (next commit wires DICE). No
behavior change for devices that publish no labels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the DICE device-truth source into the channel-name carry path:

- DICETcatProtocol retains the per-channel labels parsed from the TX/RX
  stream-format name sections, flattened across each direction's streams in
  channel order (input == device TX, output == device RX per AudioTypes.hpp),
  published through the existing runtimeCapsValid_ release/acquire fence.
  Exposed via a new IDeviceProtocol::GetChannelLabels().
- DiceAudioBackend::EnsureNubForGuid eagerly loads the runtime caps (which
  reads global + TX + RX, including the name sections) once before the first
  nub publish, then fills Model::ASFWAudioDevice input/outputChannelNames from
  GetChannelLabels. The load early-returns if caps are already cached, is
  guarded against teardown, and publishes regardless of outcome so names fall
  back to synthesized "<plug> N" when unavailable.

CoreAudio (Logic / Audio MIDI Setup) now shows the device's real channel
names. Covered by DICETcatProtocolTests.ChannelLabelsFlattenAcrossStreams...;
the wire decode is locked by DiceLabelTests/DiceNicknameTests.

cross-validated with FFADO dice_avdevice.cpp (capture==TX, playback==RX).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…els parse

ReadTx/RxStreamConfig issued a single block read capped at 512 bytes (the safe
single-read payload for DICE max_rec). A multi-stream section is larger: the
Venice F32 needs 8 + 2*280 = 568 bytes, and stream 1's 256-byte label blob
starts at byte 304 -- so ParseStreamConfig's bounds guard silently skipped it
and only stream 0's channel names (first 16 channels per direction) reached
CoreAudio; channels 17-32 fell back to synthesized names.

Add ReadSectionChunked: chain <=512-byte reads into one buffer covering
min(section size, 4KB), then parse once. A failure after the first chunk
delivers the partial buffer (parser guards each region), preserving the old
best-effort behavior; only a failed first chunk is a hard error.

(The PrepareSequenceMatchesReferenceWindow expectation update that rode along
in the original commit stays with the sample-rate series it belongs to.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alicankaralar alicankaralar force-pushed the feat/dice-channel-names branch from beb0c92 to 7bc4870 Compare July 7, 2026 11:46
@alicankaralar alicankaralar changed the title (DICE) Read Channel Names From the ROM (DICE-3) Read Channel Names From the ROM Jul 7, 2026
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