diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp index eb57e12d..5204129f 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp @@ -154,7 +154,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { const uint32_t numSlots = ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; - const uint32_t maxPacketBytes = 512; + const uint32_t maxPacketBytes = + 8u + static_cast(txConfig.framesPerDataPacket) * txConfig.dbs * 4u; const uint32_t interruptInterval = ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp index 15848b13..8fd2f689 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp @@ -7,16 +7,19 @@ #include "DiceProfileRegistry.hpp" #include "Isoch/Profiles/FocusriteSaffireProfile.hpp" #include "Isoch/Profiles/GenericDiceProfile.hpp" +#include "Isoch/Profiles/MidasVeniceProfile.hpp" namespace ASFW::Isoch::Audio::DICE { namespace { Profiles::GenericDiceProfile gGenericProfile{}; Profiles::FocusriteSaffireProfile gFocusriteProfile{}; +Profiles::MidasVeniceProfile gMidasVeniceProfile{}; } // namespace DiceProfileRegistry::DiceProfileRegistry() noexcept { (void)RegisterProfile(&gFocusriteProfile); + (void)RegisterProfile(&gMidasVeniceProfile); } bool DiceProfileRegistry::RegisterProfile(const IDiceDeviceProfile* profile) noexcept { diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.cpp new file mode 100644 index 00000000..822a13b0 --- /dev/null +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2026 ASFireWire Project +// +// MidasVeniceProfile.cpp +// Midas Venice F32 FireWire profile (DICE/TCAT). +// +// Venice F32: 32 analog inputs, 32 playback returns over FireWire. +// Channel counts need hardware verification — these are initial values derived +// from FFADO 2.4.9 TCAT device listings (vendor=0x0010c73f, model=0x000001). + +#include "MidasVeniceProfile.hpp" + +namespace ASFW::Isoch::Audio::DICE::Profiles { + +namespace { + +constexpr uint32_t kMidasVendorId = 0x10c73f; +constexpr uint32_t kMidasVeniceModelId = 0x000001; + +// Venice F32 stream geometry verified at runtime from the TCAT TX/RX STREAM +// FORMAT registers at 48 kHz: a SINGLE isochronous stream per direction carrying +// 32 PCM channels, 0 MIDI (DBS = 32). The device reports RX_NUMBER/TX_NUMBER = 1 +// with am824Slots = 32 (DICE DUPLEX START: inCh=32 inSlots=32, streams=1). Earlier +// values (33 = +MIDI, 16 = half) did not match the device's RX format, so the +// host CIP never locked the device and no hardware ZTS was produced. +// (At 2x sample rates DICE may split into multiple streams; that path is handled +// generically by the per-stream bringup/transport but is untested on this device.) +constexpr uint32_t kRxPcmChannels = 32; +constexpr uint32_t kTxPcmChannels = 32; +constexpr uint32_t kMidiSlots = 0; +constexpr uint32_t kRxDbs = kRxPcmChannels + kMidiSlots; +constexpr uint32_t kTxDbs = kTxPcmChannels + kMidiSlots; + +void FillStreamConfig(DiceStreamConfig& out, DiceStreamDirection direction) noexcept { + out = DiceStreamConfig{}; + out.direction = direction; + out.sampleRate = 48000; + out.streamMode = Encoding::StreamMode::kBlocking; + out.sid = 0; + out.framesPerDataPacket = 8; + out.fdf = 0x02; + out.fmt = 0x10; + + if (direction == DiceStreamDirection::HostToDevice) { + out.pcmChannels = kTxPcmChannels; + out.midiSlots = kMidiSlots; + out.dbs = kTxDbs; + } else { + out.pcmChannels = kRxPcmChannels; + out.midiSlots = kMidiSlots; + out.dbs = kRxDbs; + } +} + +} // namespace + +const char* MidasVeniceProfile::Name() const noexcept { + return "Midas Venice F32 (DICE)"; +} + +bool MidasVeniceProfile::Matches(const DiceDeviceIdentity& identity) const noexcept { + return identity.vendorId == kMidasVendorId && identity.modelId == kMidasVeniceModelId; +} + +DiceDeviceQuirks MidasVeniceProfile::Quirks() const noexcept { + DiceDeviceQuirks quirks{}; + // Same TCAT DICE chip family as Focusrite Saffire — same wire encoding. + quirks.tx.hostToDevicePcmEncoding = Encoding::AudioWireFormat::kRawPcm24In32; + quirks.tx.dbsPolicy = DbsPolicy::Constant; + quirks.tx.defaultNonAudioSlotWord = 0x80000000; + quirks.tx.initializeNonAudioSlots = true; + quirks.tx.preserveFdfInNoDataPackets = true; + quirks.rx.deviceToHostPcmEncoding = Encoding::AudioWireFormat::kAM824; + quirks.rx.dbsPolicy = DbsPolicy::Constant; + return quirks; +} + +bool MidasVeniceProfile::BuildDefaultTxStreamConfig(DiceStreamConfig& outConfig) const noexcept { + FillStreamConfig(outConfig, DiceStreamDirection::HostToDevice); + return true; +} + +bool MidasVeniceProfile::BuildDefaultRxStreamConfig(DiceStreamConfig& outConfig) const noexcept { + FillStreamConfig(outConfig, DiceStreamDirection::DeviceToHost); + return true; +} + +uint32_t MidasVeniceProfile::TxSafetyOffsetFrames(double sampleRate) const noexcept { + uint32_t framesPerPacket = 8; + uint32_t rateAddend = 0; + if (sampleRate > 96000.0) { + framesPerPacket = 32; + rateAddend = 4; + } else if (sampleRate > 48000.0) { + framesPerPacket = 16; + rateAddend = 2; + } + return (6 + rateAddend) * framesPerPacket; +} + +uint32_t MidasVeniceProfile::RxSafetyOffsetFrames(double sampleRate) const noexcept { + uint32_t framesPerPacket = 8; + uint32_t rateAddend = 0; + if (sampleRate > 96000.0) { + framesPerPacket = 32; + rateAddend = 4; + } else if (sampleRate > 48000.0) { + framesPerPacket = 16; + rateAddend = 2; + } + return (16 + rateAddend) * framesPerPacket; +} + +uint32_t MidasVeniceProfile::TxReportedLatencyFrames(double sampleRate) const noexcept { + if (sampleRate > 96000.0) return 119; + if (sampleRate > 48000.0) return 59; + return 29; +} + +uint32_t MidasVeniceProfile::RxReportedLatencyFrames(double sampleRate) const noexcept { + if (sampleRate > 96000.0) return 119; + if (sampleRate > 48000.0) return 59; + return 29; +} + +} // namespace ASFW::Isoch::Audio::DICE::Profiles diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.hpp new file mode 100644 index 00000000..386d3420 --- /dev/null +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.hpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2026 ASFireWire Project +// +// MidasVeniceProfile.hpp +// Midas Venice F32 FireWire profile (DICE/TCAT). + +#pragma once + +#include "../../DiceDeviceProfile.hpp" + +namespace ASFW::Isoch::Audio::DICE::Profiles { + +class MidasVeniceProfile final : public IDiceDeviceProfile { +public: + [[nodiscard]] const char* Name() const noexcept override; + + [[nodiscard]] bool Matches(const DiceDeviceIdentity& identity) const noexcept override; + + [[nodiscard]] DiceDeviceQuirks Quirks() const noexcept override; + + [[nodiscard]] bool BuildDefaultTxStreamConfig(DiceStreamConfig& outConfig) const noexcept override; + [[nodiscard]] bool BuildDefaultRxStreamConfig(DiceStreamConfig& outConfig) const noexcept override; + + [[nodiscard]] uint32_t TxSafetyOffsetFrames(double sampleRate) const noexcept override; + [[nodiscard]] uint32_t RxSafetyOffsetFrames(double sampleRate) const noexcept override; + + [[nodiscard]] uint32_t TxReportedLatencyFrames(double sampleRate) const noexcept override; + [[nodiscard]] uint32_t RxReportedLatencyFrames(double sampleRate) const noexcept override; +}; + +} // namespace ASFW::Isoch::Audio::DICE::Profiles diff --git a/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.cpp b/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.cpp index 546de4d1..6724d99e 100644 --- a/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.cpp +++ b/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.cpp @@ -15,7 +15,9 @@ RxAudioPacketProcessorResult RxAudioPacketProcessor::ProcessPacket(const uint8_t uint64_t absoluteFrame, uint32_t channels, uint32_t am824Slots, - ASFW::Encoding::AudioWireFormat format) noexcept { + ASFW::Encoding::AudioWireFormat format, + uint32_t channelOffset, + bool publishTimeline) noexcept { RxAudioPacketProcessorResult result{}; if (length < kIsochHeaderSize + 8) { @@ -82,12 +84,20 @@ RxAudioPacketProcessorResult RxAudioPacketProcessor::ProcessPacket(const uint8_t } const uint32_t* frameIn = dataBlocks + (i * cip->dataBlockSize); - DecodeDirectRxFrame(frameIn, channels, cip->dataBlockSize, format, frameOut); + // Write this stream's slice at its channel offset into the interleaved + // frame; the writer stride covers the buffer's full channel width. + DecodeDirectRxFrame(frameIn, channels, cip->dataBlockSize, format, + frameOut + channelOffset); + } + + // Only the master stream advances the producer cursor/frame counters; a + // secondary slice writes PCM into the same frames without re-publishing the + // timeline (the two streams are frame-locked by the device clock). + if (publishTimeline) { + const uint64_t producedEnd = absoluteFrame + eventCount; + writer_.PublishProducedEnd(producedEnd, static_cast(eventCount)); } - const uint64_t producedEnd = absoluteFrame + eventCount; - writer_.PublishProducedEnd(producedEnd, static_cast(eventCount)); - result.status = DirectRxWriteStatus::kAvailable; return result; } diff --git a/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.hpp b/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.hpp index b778dda4..fa3a69c6 100644 --- a/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.hpp +++ b/ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.hpp @@ -26,12 +26,20 @@ class RxAudioPacketProcessor final { explicit RxAudioPacketProcessor(DirectInputWriter& writer) noexcept : writer_(writer) {} + // `channels` is the number of PCM channels THIS stream decodes (its slice), + // written into the shared interleaved input buffer starting at `channelOffset` + // (e.g. 0 for the master/first 16-ch slice, 16 for the second). The buffer's + // full interleave width (stride) is owned by the writer's binding. + // `publishTimeline` advances the producer cursor/frame counters — only the + // master stream does this; secondary slices write PCM only. [[nodiscard]] RxAudioPacketProcessorResult ProcessPacket(const uint8_t* payload, size_t length, uint64_t absoluteFrame, uint32_t channels, uint32_t am824Slots, - ASFW::Encoding::AudioWireFormat format) noexcept; + ASFW::Encoding::AudioWireFormat format, + uint32_t channelOffset = 0, + bool publishTimeline = true) noexcept; private: DirectInputWriter& writer_; diff --git a/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp b/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp index 03449ea5..08c0d438 100644 --- a/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp +++ b/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp @@ -16,8 +16,11 @@ AMDTP::AmdtpStreamConfig DiceStreamConfigMapper::ToAmdtpConfig( config.fmt = diceConfig.fmt; config.fdf = diceConfig.fdf; config.framesPerDataPacket = diceConfig.framesPerDataPacket; - // maxPacketBytes keeps the AmdtpStreamConfig default (512), matching the - // lab slot capacity and the ASFW IT ring payload budget. + // Compute the true max packet size: CIP headers (8 bytes) + frames × DBS × 4 bytes/slot. + // Do not use the AmdtpStreamConfig default (512) — it is too small for high-channel + // devices (e.g. a 24-channel device with DBS=24 needs 776 bytes at 8 fpd). + config.maxPacketBytes = 8u + + static_cast(diceConfig.framesPerDataPacket) * diceConfig.dbs * 4u; return config; } diff --git a/ASFWDriver/Audio/Protocols/AudioTypes.hpp b/ASFWDriver/Audio/Protocols/AudioTypes.hpp index 0a38271f..9b6f1feb 100644 --- a/ASFWDriver/Audio/Protocols/AudioTypes.hpp +++ b/ASFWDriver/Audio/Protocols/AudioTypes.hpp @@ -9,9 +9,26 @@ namespace ASFW::Audio { +// Maximum isochronous streams per direction we support (DICE allows up to 4; +// the Venice F32 uses 2×16-channel streams per direction). +inline constexpr uint32_t kMaxAudioStreamsPerDirection = 4; + +// Per-stream wire geometry for one isochronous stream within a direction. +// The aggregate device (what CoreAudio sees) is the sum across active streams, +// but the transport layer must drive each stream on its own iso channel. +struct AudioStreamWireInfo { + static constexpr uint8_t kInvalidIsoChannel = 0xFF; + + uint8_t isoChannel{kInvalidIsoChannel}; // assigned/discovered iso channel + uint16_t pcmChannels{0}; // PCM channels carried by this stream + uint16_t am824Slots{0}; // AM824 data-block slots (DBS) for this stream + uint16_t midiPorts{0}; // MIDI ports muxed into this stream +}; + struct AudioStreamRuntimeCaps { static constexpr uint8_t kInvalidIsoChannel = 0xFF; + // ---- Aggregate (CoreAudio / HAL view): summed across active streams ---- // Host-facing channel counts (PCM only). uint32_t hostInputPcmChannels{0}; // Device -> host capture channels uint32_t hostOutputPcmChannels{0}; // Host -> device playback channels @@ -22,14 +39,49 @@ struct AudioStreamRuntimeCaps { uint32_t sampleRateHz{0}; - // Active DICE isochronous channels when discovered from stream entries. + // First active DICE isochronous channel per direction (stream[0]). uint8_t deviceToHostIsoChannel{kInvalidIsoChannel}; // DICE TX / host IR uint8_t hostToDeviceIsoChannel{kInvalidIsoChannel}; // DICE RX / host IT + + // ---- Per-stream (wire view): drives transport + device register programming ---- + // Stream count comes from the DICE TX_NUMBER/RX_NUMBER registers, NOT the + // count of currently-active streams — a freshly probed device may report + // streams with iso=-1 that we still must arm. + uint32_t deviceToHostStreamCount{0}; // # DICE TX streams (host IR) + uint32_t hostToDeviceStreamCount{0}; // # DICE RX streams (host IT) + AudioStreamWireInfo deviceToHostStreams[kMaxAudioStreamsPerDirection]{}; + AudioStreamWireInfo hostToDeviceStreams[kMaxAudioStreamsPerDirection]{}; }; struct AudioDuplexChannels { - uint8_t deviceToHostIsoChannel{0}; // DICE TX / host IR - uint8_t hostToDeviceIsoChannel{1}; // DICE RX / host IT + // Legacy single-channel accessors == stream[0] of each direction. Kept so + // the single-stream host path (and existing call sites) compile unchanged. + uint8_t deviceToHostIsoChannel{0}; // DICE TX / host IR (stream[0]) + uint8_t hostToDeviceIsoChannel{1}; // DICE RX / host IT (stream[0]) + + // Per-stream iso channels the host assigns and writes into the device's + // per-stream ISOC registers before GLOBAL_ENABLE. Counts default to 1 so an + // unpopulated value behaves like the legacy single-stream config. + // + // INVARIANT: stream[0] of each direction is the legacy scalar field above; + // these arrays carry the *additional* streams (index >= 1). Use the + // CaptureChannel()/PlaybackChannel() accessors so single-stream call sites + // that only populate the scalar fields stay byte-for-byte unchanged. + uint32_t captureStreamCount{1}; // device TX streams (host IR) + uint32_t playbackStreamCount{1}; // device RX streams (host IT) + uint8_t captureIsoChannels[kMaxAudioStreamsPerDirection]{0}; // device TX -> host IR + uint8_t playbackIsoChannels[kMaxAudioStreamsPerDirection]{1}; // host IT -> device RX + + // Iso channel for capture (device TX -> host IR) stream `i`. + [[nodiscard]] uint8_t CaptureChannel(uint32_t i) const noexcept { + return (i == 0) ? deviceToHostIsoChannel + : captureIsoChannels[i % kMaxAudioStreamsPerDirection]; + } + // Iso channel for playback (host IT -> device RX) stream `i`. + [[nodiscard]] uint8_t PlaybackChannel(uint32_t i) const noexcept { + return (i == 0) ? hostToDeviceIsoChannel + : playbackIsoChannels[i % kMaxAudioStreamsPerDirection]; + } }; } // namespace ASFW::Audio diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp b/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp index c4c5bf51..d7d1289a 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp @@ -386,10 +386,16 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { if (guid == 0) return; const auto* record = registry_.FindByGuid(guid); - if (!record) return; + if (!record) { + ASFW_LOG(Audio, "DiceAudioBackend::EnsureNubForGuid: no registry record for GUID=0x%016llx", guid); + return; + } const auto integration = DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId); if (integration != DeviceIntegrationMode::kHardcodedNub) { + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: skipping GUID=0x%016llx vendor=0x%06x model=0x%06x integration=%u (not hardcodedNub)", + guid, record->vendorId, record->modelId, static_cast(integration)); return; } @@ -402,12 +408,16 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { static ASFW::Isoch::Audio::DICE::DiceProfileRegistry diceRegistry{}; const auto* profile = diceRegistry.FindProfile(identity); if (!profile) { - // We neither retrieve stream geometry for unknown device nor fail loudly. - // Skip the logic of retrieving geometry and stick to only known profiles for now. - // TODO: Support dynamic stream geometry retrieval for unknown devices. + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: no isoch profile for GUID=0x%016llx vendor=0x%06x model=0x%06x (profileCount=%u)", + guid, record->vendorId, record->modelId, diceRegistry.ProfileCount()); return; } + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: matched profile=%{public}s for GUID=0x%016llx", + profile->Name(), guid); + auto protocol = runtime_.FindShared(guid); Model::ASFWAudioDevice dev{}; diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceDuplexRestartCoordinator.cpp b/ASFWDriver/Audio/Protocols/Backends/DiceDuplexRestartCoordinator.cpp index be7ed5b3..f9853b35 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceDuplexRestartCoordinator.cpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceDuplexRestartCoordinator.cpp @@ -129,15 +129,58 @@ struct DiceRecoveryDecision { AudioStreamRuntimeCaps caps{}; bool haveCaps = protocol && protocol->GetRuntimeAudioStreamCaps(caps); - if (haveCaps) { - if (IsValidIsoChannel(caps.deviceToHostIsoChannel)) { - channels.deviceToHostIsoChannel = caps.deviceToHostIsoChannel; - } - if (IsValidIsoChannel(caps.hostToDeviceIsoChannel)) { - channels.hostToDeviceIsoChannel = caps.hostToDeviceIsoChannel; + // Stream counts come from the device's TX_NUMBER/RX_NUMBER (includes streams + // currently reported with iso=-1 that the host must still arm). Single-stream + // devices report 1/1 and take exactly the legacy code path below. + auto clampCount = [](uint32_t n) noexcept -> uint32_t { + if (n == 0) return 1; + return (n > kMaxAudioStreamsPerDirection) ? kMaxAudioStreamsPerDirection : n; + }; + channels.captureStreamCount = + haveCaps ? clampCount(caps.deviceToHostStreamCount) : 1; + channels.playbackStreamCount = + haveCaps ? clampCount(caps.hostToDeviceStreamCount) : 1; + + // Assign distinct iso channels across both directions. stream[0] keeps the + // device-reported channel (or the legacy default) so the single-stream host + // path is byte-for-byte unchanged; additional streams get the lowest free + // channels. The host allocates these (it owns the IRM reservation) and writes + // them into the device's per-stream ISOC registers before GLOBAL_ENABLE. + uint64_t used = 0; // bitset over channels 0..63 + auto markUsed = [&](uint8_t ch) noexcept { + if (ch <= 0x3F) used |= (uint64_t{1} << ch); + }; + auto nextFree = [&]() noexcept -> uint8_t { + for (uint8_t ch = 0; ch <= 0x3F; ++ch) { + if ((used & (uint64_t{1} << ch)) == 0) { + used |= (uint64_t{1} << ch); + return ch; + } } + return AudioStreamWireInfo::kInvalidIsoChannel; + }; + + channels.captureIsoChannels[0] = + (haveCaps && IsValidIsoChannel(caps.deviceToHostIsoChannel)) + ? caps.deviceToHostIsoChannel + : kDefaultIrChannel; + channels.playbackIsoChannels[0] = + (haveCaps && IsValidIsoChannel(caps.hostToDeviceIsoChannel)) + ? caps.hostToDeviceIsoChannel + : kDefaultItChannel; + markUsed(channels.captureIsoChannels[0]); + markUsed(channels.playbackIsoChannels[0]); + + for (uint32_t i = 1; i < channels.captureStreamCount; ++i) { + channels.captureIsoChannels[i] = nextFree(); + } + for (uint32_t i = 1; i < channels.playbackStreamCount; ++i) { + channels.playbackIsoChannels[i] = nextFree(); } + // Legacy single-channel fields mirror stream[0]. + channels.deviceToHostIsoChannel = channels.captureIsoChannels[0]; + channels.hostToDeviceIsoChannel = channels.playbackIsoChannels[0]; return channels; } @@ -1247,6 +1290,30 @@ IOReturn DiceDuplexRestartCoordinator::RunDuplexStart( } const FW::Generation topologyGeneration = record.gen; auto runtimeProtocol = runtime_.FindShared(record.guid); + + // Pre-read the device's static stream format (DICE TX_NUMBER/RX_NUMBER + + // per-stream channels) so the channel resolution + IRM reservation below see + // the real stream count. EnsureRuntimeStreamGeometry publishes the per-stream + // caps that ResolveDuplexChannelsForRecord consumes via GetRuntimeAudioStreamCaps. + // Non-fatal: on failure we fall back to the legacy single-stream resolution + // and PrepareDuplex will surface any genuine device error. A multi-stream + // device (Venice F32 = 2×16) needs this to allocate a channel per stream; + // cross-validated with FFADO dice_avdevice.cpp prepare() (m_nb_rx/m_nb_tx). + const auto geometryLoad = WaitForAsyncResult( + [&](auto callback) { + diceProtocol.EnsureRuntimeStreamGeometry( + [callback = std::move(callback)](IOReturn st) mutable { callback(st, true); }); + }, + kSyncBridgeTimeoutMs, + kIOReturnTimeout, + cancel_); + if (geometryLoad.status != kIOReturnSuccess) { + ASFW_LOG(DICE, + "RunDuplexStart: stream-geometry pre-read failed (0x%x); " + "resolving channels with existing caps", + geometryLoad.status); + } + const AudioDuplexChannels channels = ResolveDuplexChannelsForRecord(record, runtimeProtocol.get()); const uint64_t restartId = AllocateRestartId(); @@ -1444,45 +1511,53 @@ IOReturn DiceDuplexRestartCoordinator::RunDuplexStart( SetSessionPhase(session, DiceRestartPhase::kPrepared); StoreSession(session); + // Reserve IRM bandwidth + channel for EVERY playback (host IT -> device RX) + // stream. A multi-stream DICE device (Venice F32) needs all of its RX iso + // channels reserved before GLOBAL_ENABLE; single-stream devices loop once. if (abortIfTeardown("ReservingPlaybackResources")) { return kIOReturnAborted; } - const kern_return_t reservePlaybackStatus = hostTransport_.ReservePlaybackResources( - guid, - *irmClient, - channels.hostToDeviceIsoChannel, - kPlaybackBandwidthUnits); - if (reservePlaybackStatus != kIOReturnSuccess) { - return rollbackToFailure(reservePlaybackStatus, - DiceRestartPhase::kReservingPlaybackResources, - DiceRestartFailureCause::kReservePlayback); - } - if (!IsRestartEpochCurrent(guid, restartId, topologyGeneration)) { - return rollbackToInvalidation(kIOReturnAborted, - DiceRestartPhase::kReservingPlaybackResources, - DiceRestartFailureCause::kReservePlayback); + for (uint32_t i = 0; i < channels.playbackStreamCount; ++i) { + const kern_return_t reservePlaybackStatus = hostTransport_.ReservePlaybackResources( + guid, + *irmClient, + channels.PlaybackChannel(i), + kPlaybackBandwidthUnits); + if (reservePlaybackStatus != kIOReturnSuccess) { + return rollbackToFailure(reservePlaybackStatus, + DiceRestartPhase::kReservingPlaybackResources, + DiceRestartFailureCause::kReservePlayback); + } + if (!IsRestartEpochCurrent(guid, restartId, topologyGeneration)) { + return rollbackToInvalidation(kIOReturnAborted, + DiceRestartPhase::kReservingPlaybackResources, + DiceRestartFailureCause::kReservePlayback); + } } SetSessionPhase(session, DiceRestartPhase::kReservingPlaybackResources); session.hostPlaybackReserved = true; StoreSession(session); + // Reserve IRM bandwidth + channel for EVERY capture (device TX -> host IR) stream. if (abortIfTeardown("ReservingCaptureResources")) { return kIOReturnAborted; } - const kern_return_t reserveCaptureStatus = hostTransport_.ReserveCaptureResources( - guid, - *irmClient, - channels.deviceToHostIsoChannel, - kCaptureBandwidthUnits); - if (reserveCaptureStatus != kIOReturnSuccess) { - return rollbackToFailure(reserveCaptureStatus, - DiceRestartPhase::kReservingCaptureResources, - DiceRestartFailureCause::kReserveCapture); - } - if (!IsRestartEpochCurrent(guid, restartId, topologyGeneration)) { - return rollbackToInvalidation(kIOReturnAborted, - DiceRestartPhase::kReservingCaptureResources, - DiceRestartFailureCause::kReserveCapture); + for (uint32_t i = 0; i < channels.captureStreamCount; ++i) { + const kern_return_t reserveCaptureStatus = hostTransport_.ReserveCaptureResources( + guid, + *irmClient, + channels.CaptureChannel(i), + kCaptureBandwidthUnits); + if (reserveCaptureStatus != kIOReturnSuccess) { + return rollbackToFailure(reserveCaptureStatus, + DiceRestartPhase::kReservingCaptureResources, + DiceRestartFailureCause::kReserveCapture); + } + if (!IsRestartEpochCurrent(guid, restartId, topologyGeneration)) { + return rollbackToInvalidation(kIOReturnAborted, + DiceRestartPhase::kReservingCaptureResources, + DiceRestartFailureCause::kReserveCapture); + } } SetSessionPhase(session, DiceRestartPhase::kReservingCaptureResources); session.hostCaptureReserved = true; @@ -1514,15 +1589,30 @@ IOReturn DiceDuplexRestartCoordinator::RunDuplexStart( // Saffire.kext allocates every local/remote isoch port before it reports // the assigned channels to DICE. Keep the expensive DMA setup on the // disabled side of GLOBAL_ENABLE as well. + // Multi-stream capture (e.g. Venice F32 = 2×16): each wire stream carries + // its own 16-ch CIP (per-stream DBS), de-interleaved into the single shared + // 32-ch input buffer at a channel offset. The master (stream 0) owns the + // clock/ZTS/replay; secondary streams write PCM only. Single-stream devices + // (captureStreamCount == 1) take the legacy path with streamChannels == 0 + // (full width) and the aggregate slot count. + const bool multiCapture = channels.captureStreamCount > 1; + const uint32_t masterCaptureSlots = + multiCapture ? session.runtimeCaps.deviceToHostStreams[0].am824Slots + : rxAm824Slots; + const uint32_t masterCaptureChannels = + multiCapture ? session.runtimeCaps.deviceToHostStreams[0].pcmChannels + : 0; + if (abortIfTeardown("PreparingHostReceive")) { return kIOReturnAborted; } const kern_return_t prepareReceiveStatus = hostTransport_.PrepareReceive( - channels.deviceToHostIsoChannel, + channels.CaptureChannel(0), hardware_, bindingSource, rxWireFormat, - rxAm824Slots); + masterCaptureSlots, + masterCaptureChannels); if (prepareReceiveStatus != kIOReturnSuccess) { return rollbackToFailure(prepareReceiveStatus, DiceRestartPhase::kStartingHostReceive, @@ -1534,6 +1624,36 @@ IOReturn DiceDuplexRestartCoordinator::RunDuplexStart( DiceRestartFailureCause::kStartReceive); } + // Prepare each secondary capture stream on its own OHCI IR context, writing + // its slice at the running channel offset. + uint32_t captureChannelOffset = masterCaptureChannels; + for (uint32_t i = 1; i < channels.captureStreamCount; ++i) { + const auto& streamInfo = session.runtimeCaps.deviceToHostStreams[i]; + if (abortIfTeardown("PreparingHostReceiveStream")) { + return kIOReturnAborted; + } + const kern_return_t status = hostTransport_.PrepareReceiveStream( + i, + channels.CaptureChannel(i), + hardware_, + bindingSource, + captureChannelOffset, + streamInfo.pcmChannels, + rxWireFormat, + streamInfo.am824Slots); + if (status != kIOReturnSuccess) { + return rollbackToFailure(status, + DiceRestartPhase::kStartingHostReceive, + DiceRestartFailureCause::kStartReceive); + } + if (!IsRestartEpochCurrent(guid, restartId, topologyGeneration)) { + return rollbackToInvalidation(kIOReturnAborted, + DiceRestartPhase::kStartingHostReceive, + DiceRestartFailureCause::kStartReceive); + } + captureChannelOffset += streamInfo.pcmChannels; + } + if (abortIfTeardown("PreparingHostTransmit")) { return kIOReturnAborted; } diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.cpp b/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.cpp index 9ecb528c..162b796b 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.cpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.cpp @@ -38,9 +38,11 @@ kern_return_t DiceIsochHostTransport::PrepareReceive( Driver::HardwareInterface& hardware, ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, Encoding::AudioWireFormat wireFormat, - uint32_t am824Slots) noexcept { + uint32_t am824Slots, + uint32_t streamChannels) noexcept { return isoch_.PrepareReceive( - channel, hardware, bindingSource, wireFormat, am824Slots); + channel, hardware, bindingSource, wireFormat, am824Slots, + /*packetCallback=*/nullptr, streamChannels); } kern_return_t DiceIsochHostTransport::PrepareTransmit( @@ -50,6 +52,28 @@ kern_return_t DiceIsochHostTransport::PrepareTransmit( return isoch_.PrepareTransmit(channel, hardware, sourceId); } +kern_return_t DiceIsochHostTransport::PrepareReceiveStream( + uint32_t streamIndex, + uint8_t channel, + Driver::HardwareInterface& hardware, + ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, + uint32_t channelOffset, + uint32_t streamChannels, + Encoding::AudioWireFormat wireFormat, + uint32_t am824Slots) noexcept { + return isoch_.PrepareReceiveStream( + streamIndex, channel, hardware, bindingSource, channelOffset, + streamChannels, wireFormat, am824Slots); +} + +kern_return_t DiceIsochHostTransport::PrepareTransmitStream( + uint32_t streamIndex, + uint8_t channel, + Driver::HardwareInterface& hardware, + uint8_t sourceId) noexcept { + return isoch_.PrepareTransmitStream(streamIndex, channel, hardware, sourceId); +} + kern_return_t DiceIsochHostTransport::StartPreparedReceive() noexcept { return isoch_.StartPreparedReceive(); } diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.hpp b/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.hpp index b03ccb43..66cc632f 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.hpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceHostTransport.hpp @@ -39,11 +39,28 @@ class IDiceHostTransport { Driver::HardwareInterface& hardware, ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, Encoding::AudioWireFormat wireFormat = Encoding::AudioWireFormat::kAM824, - uint32_t am824Slots = 0) noexcept = 0; + uint32_t am824Slots = 0, + uint32_t streamChannels = 0) noexcept = 0; [[nodiscard]] virtual kern_return_t PrepareTransmit( uint8_t channel, Driver::HardwareInterface& hardware, uint8_t sourceId) noexcept = 0; + // Secondary streams (streamIndex >= 1) for multi-stream DICE devices; the + // master stream uses PrepareReceive/PrepareTransmit above. + [[nodiscard]] virtual kern_return_t PrepareReceiveStream( + uint32_t streamIndex, + uint8_t channel, + Driver::HardwareInterface& hardware, + ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, + uint32_t channelOffset, + uint32_t streamChannels, + Encoding::AudioWireFormat wireFormat = Encoding::AudioWireFormat::kAM824, + uint32_t am824Slots = 0) noexcept = 0; + [[nodiscard]] virtual kern_return_t PrepareTransmitStream( + uint32_t streamIndex, + uint8_t channel, + Driver::HardwareInterface& hardware, + uint8_t sourceId) noexcept = 0; [[nodiscard]] virtual kern_return_t StartPreparedReceive() noexcept = 0; [[nodiscard]] virtual kern_return_t StartPreparedTransmit() noexcept = 0; [[nodiscard]] virtual kern_return_t StopAll() noexcept = 0; @@ -70,11 +87,26 @@ class DiceIsochHostTransport final : public IDiceHostTransport { Driver::HardwareInterface& hardware, ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, Encoding::AudioWireFormat wireFormat = Encoding::AudioWireFormat::kAM824, - uint32_t am824Slots = 0) noexcept override; + uint32_t am824Slots = 0, + uint32_t streamChannels = 0) noexcept override; [[nodiscard]] kern_return_t PrepareTransmit( uint8_t channel, Driver::HardwareInterface& hardware, uint8_t sourceId) noexcept override; + [[nodiscard]] kern_return_t PrepareReceiveStream( + uint32_t streamIndex, + uint8_t channel, + Driver::HardwareInterface& hardware, + ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, + uint32_t channelOffset, + uint32_t streamChannels, + Encoding::AudioWireFormat wireFormat = Encoding::AudioWireFormat::kAM824, + uint32_t am824Slots = 0) noexcept override; + [[nodiscard]] kern_return_t PrepareTransmitStream( + uint32_t streamIndex, + uint8_t channel, + Driver::HardwareInterface& hardware, + uint8_t sourceId) noexcept override; [[nodiscard]] kern_return_t StartPreparedReceive() noexcept override; [[nodiscard]] kern_return_t StartPreparedTransmit() noexcept override; [[nodiscard]] kern_return_t StopAll() noexcept override; diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp index 40196375..8fbee58f 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp @@ -65,6 +65,36 @@ void CacheRuntimeCaps(AudioStreamRuntimeCaps& caps, tx.FirstActiveIsoChannel(AudioStreamRuntimeCaps::kInvalidIsoChannel); caps.hostToDeviceIsoChannel = rx.FirstActiveIsoChannel(AudioStreamRuntimeCaps::kInvalidIsoChannel); + + // Per-stream wire geometry. Stream count comes from the DICE stream-format + // header (TX_NUMBER/RX_NUMBER), which includes streams the device reports + // with iso=-1 (disabled) that the host must still arm for a multi-stream + // device such as the Venice F32 (2×16 channels). The discovered isoChannel + // is carried through but the host reassigns it during channel resolution. + auto fillPerStream = [](const StreamConfig& sc, + uint32_t& outCount, + AudioStreamWireInfo* outStreams) noexcept { + const uint32_t count = + (sc.numStreams < kMaxAudioStreamsPerDirection) + ? sc.numStreams + : kMaxAudioStreamsPerDirection; + outCount = count; + for (uint32_t i = 0; i < count; ++i) { + const auto& entry = sc.streams[i]; + outStreams[i].isoChannel = + (entry.isoChannel >= 0 && entry.isoChannel <= 0x3F) + ? static_cast(entry.isoChannel) + : AudioStreamWireInfo::kInvalidIsoChannel; + outStreams[i].pcmChannels = + static_cast(entry.pcmChannels); + outStreams[i].am824Slots = + static_cast(entry.Am824Slots()); + outStreams[i].midiPorts = + static_cast(entry.midiPorts); + } + }; + fillPerStream(tx, caps.deviceToHostStreamCount, caps.deviceToHostStreams); + fillPerStream(rx, caps.hostToDeviceStreamCount, caps.hostToDeviceStreams); } } // namespace @@ -885,6 +915,8 @@ void DICEDuplexBringupController::DoDiscoverStreams( void DICEDuplexBringupController::DoProgramRx( AudioDuplexChannels channels, + uint32_t streamIndex, + uint32_t entrySizeBytes, VoidCallback cb) { if (!EnsureGenerationCurrent()) { DoRollback(kIOReturnOffline, std::move(cb)); @@ -892,51 +924,70 @@ void DICEDuplexBringupController::DoProgramRx( } restartSession_.phase = DiceRestartPhase::kProgrammingDeviceRx; - (void)io_.ReadQuadBE(MakeDICEAddress(sections_.rxStreamFormat.offset + RxOffset::kSize), - [this, channels, cb = std::move(cb)](Async::AsyncStatus transportStatus, uint32_t rxSize) mutable { - const IOReturn status = MapTransportStatus(transportStatus); - ASFW_LOG(DICE, - "DoProgramRx: RX_SIZE transport status=%u value=0x%08x", - static_cast(transportStatus), - rxSize); - if (status != kIOReturnSuccess) { - DoRollback(status, std::move(cb)); - return; - } - ASFW_LOG(DICE, - "DoProgramRx: RX_SIZE complete, entering RX program lambda value=0x%08x", - rxSize); - ASFW_LOG(DICE, - "DoProgramRx: writing RX isoch channel %u", - channels.hostToDeviceIsoChannel); - (void)io_.WriteQuadBE(MakeDICEAddress(sections_.rxStreamFormat.offset + RxOffset::kIsochronous), - channels.hostToDeviceIsoChannel, - [this, channels, cb = std::move(cb)](Async::AsyncStatus isoTransportStatus) mutable { - const IOReturn isoStatus = MapTransportStatus(isoTransportStatus); - if (isoStatus != kIOReturnSuccess) { - DoRollback(isoStatus, std::move(cb)); - return; - } + // At stream 0, read RX_SIZE first to learn the per-stream register stride, + // then re-enter this function with the resolved entry size for every stream. + if (streamIndex == 0 && entrySizeBytes == 0) { + (void)io_.ReadQuadBE(MakeDICEAddress(sections_.rxStreamFormat.offset + RxOffset::kSize), + [this, channels, cb = std::move(cb)](Async::AsyncStatus transportStatus, uint32_t rxSize) mutable { + const IOReturn status = MapTransportStatus(transportStatus); + ASFW_LOG(DICE, + "DoProgramRx: RX_SIZE transport status=%u value=0x%08x streams=%u", + static_cast(transportStatus), + rxSize, + channels.playbackStreamCount); + if (status != kIOReturnSuccess) { + DoRollback(status, std::move(cb)); + return; + } + const uint32_t stride = rxSize * 4u; + DoProgramRx(channels, 0, stride, std::move(cb)); + }); + return; + } - (void)io_.WriteQuadBE(MakeDICEAddress(sections_.rxStreamFormat.offset + RxOffset::kSeqStart), - kRxSeqStartDefault, - [this, channels, cb = std::move(cb)](Async::AsyncStatus seqTransportStatus) mutable { - const IOReturn seqStatus = MapTransportStatus(seqTransportStatus); - if (seqStatus != kIOReturnSuccess) { - DoRollback(seqStatus, std::move(cb)); - return; - } - restartSession_.deviceRxProgrammed = true; - restartSession_.phase = DiceRestartPhase::kDeviceRxProgrammed; - cb(kIOReturnSuccess); - }); - }); - }); + if (streamIndex >= channels.playbackStreamCount) { + // All RX streams programmed. + restartSession_.deviceRxProgrammed = true; + restartSession_.phase = DiceRestartPhase::kDeviceRxProgrammed; + cb(kIOReturnSuccess); + return; + } + + const uint8_t isoChannel = channels.PlaybackChannel(streamIndex); + const uint32_t streamBase = + sections_.rxStreamFormat.offset + streamIndex * entrySizeBytes; + ASFW_LOG(DICE, + "DoProgramRx: stream %u writing RX isoch channel %u (stride=%u)", + streamIndex, isoChannel, entrySizeBytes); + + (void)io_.WriteQuadBE(MakeDICEAddress(streamBase + RxOffset::kIsochronous), + isoChannel, + [this, channels, streamIndex, entrySizeBytes, streamBase, cb = std::move(cb)](Async::AsyncStatus isoTransportStatus) mutable { + const IOReturn isoStatus = MapTransportStatus(isoTransportStatus); + if (isoStatus != kIOReturnSuccess) { + DoRollback(isoStatus, std::move(cb)); + return; + } + + (void)io_.WriteQuadBE(MakeDICEAddress(streamBase + RxOffset::kSeqStart), + kRxSeqStartDefault, + [this, channels, streamIndex, entrySizeBytes, cb = std::move(cb)](Async::AsyncStatus seqTransportStatus) mutable { + const IOReturn seqStatus = MapTransportStatus(seqTransportStatus); + if (seqStatus != kIOReturnSuccess) { + DoRollback(seqStatus, std::move(cb)); + return; + } + // Next RX stream. + DoProgramRx(channels, streamIndex + 1, entrySizeBytes, std::move(cb)); + }); + }); } void DICEDuplexBringupController::DoProgramTx( AudioDuplexChannels channels, + uint32_t streamIndex, + uint32_t entrySizeBytes, VoidCallback cb) { if (!EnsureGenerationCurrent()) { DoRollback(kIOReturnOffline, std::move(cb)); @@ -944,46 +995,79 @@ void DICEDuplexBringupController::DoProgramTx( } restartSession_.phase = DiceRestartPhase::kProgrammingDeviceTx; - (void)io_.ReadQuadBE(MakeDICEAddress(sections_.txStreamFormat.offset + TxOffset::kSize), - [this, channels, cb = std::move(cb)](Async::AsyncStatus transportStatus, uint32_t) mutable { - const IOReturn status = MapTransportStatus(transportStatus); - if (status != kIOReturnSuccess) { - DoRollback(status, std::move(cb)); - return; - } - (void)io_.WriteQuadBE(MakeDICEAddress(sections_.txStreamFormat.offset + TxOffset::kIsochronous), - channels.deviceToHostIsoChannel, - [this, channels, cb = std::move(cb)](Async::AsyncStatus isoTransportStatus) mutable { - const IOReturn isoStatus = MapTransportStatus(isoTransportStatus); - if (isoStatus != kIOReturnSuccess) { - DoRollback(isoStatus, std::move(cb)); - return; - } + // At stream 0, read TX_SIZE first to learn the per-stream register stride. + if (streamIndex == 0 && entrySizeBytes == 0) { + (void)io_.ReadQuadBE(MakeDICEAddress(sections_.txStreamFormat.offset + TxOffset::kSize), + [this, channels, cb = std::move(cb)](Async::AsyncStatus transportStatus, uint32_t txSize) mutable { + const IOReturn status = MapTransportStatus(transportStatus); + if (status != kIOReturnSuccess) { + DoRollback(status, std::move(cb)); + return; + } + const uint32_t stride = txSize * 4u; + DoProgramTx(channels, 0, stride, std::move(cb)); + }); + return; + } - (void)io_.WriteQuadBE(MakeDICEAddress(sections_.txStreamFormat.offset + TxOffset::kSpeed), - kTxSpeedS400, - [this, cb = std::move(cb)](Async::AsyncStatus speedTransportStatus) mutable { - const IOReturn speedStatus = MapTransportStatus(speedTransportStatus); - if (speedStatus != kIOReturnSuccess) { - DoRollback(speedStatus, std::move(cb)); - return; - } - (void)io_.WriteQuadBE(MakeDICEAddress(sections_.global.offset + GlobalOffset::kEnable), - 1, - [this, cb = std::move(cb)](Async::AsyncStatus enableTransportStatus) mutable { - const IOReturn enableStatus = MapTransportStatus(enableTransportStatus); - if (enableStatus != kIOReturnSuccess) { - DoRollback(enableStatus, std::move(cb)); - return; - } - restartSession_.deviceTxArmed = true; - restartSession_.phase = DiceRestartPhase::kDeviceTxArmed; - cb(kIOReturnSuccess); - }); - }); - }); - }); + if (streamIndex >= channels.captureStreamCount) { + // All TX streams programmed; assert the single GLOBAL_ENABLE last. + DoEnableGlobal(channels, std::move(cb)); + return; + } + + const uint8_t isoChannel = channels.CaptureChannel(streamIndex); + const uint32_t streamBase = + sections_.txStreamFormat.offset + streamIndex * entrySizeBytes; + ASFW_LOG(DICE, + "DoProgramTx: stream %u writing TX isoch channel %u (stride=%u)", + streamIndex, isoChannel, entrySizeBytes); + + (void)io_.WriteQuadBE(MakeDICEAddress(streamBase + TxOffset::kIsochronous), + isoChannel, + [this, channels, streamIndex, entrySizeBytes, streamBase, cb = std::move(cb)](Async::AsyncStatus isoTransportStatus) mutable { + const IOReturn isoStatus = MapTransportStatus(isoTransportStatus); + if (isoStatus != kIOReturnSuccess) { + DoRollback(isoStatus, std::move(cb)); + return; + } + + (void)io_.WriteQuadBE(MakeDICEAddress(streamBase + TxOffset::kSpeed), + kTxSpeedS400, + [this, channels, streamIndex, entrySizeBytes, cb = std::move(cb)](Async::AsyncStatus speedTransportStatus) mutable { + const IOReturn speedStatus = MapTransportStatus(speedTransportStatus); + if (speedStatus != kIOReturnSuccess) { + DoRollback(speedStatus, std::move(cb)); + return; + } + // Next TX stream. + DoProgramTx(channels, streamIndex + 1, entrySizeBytes, std::move(cb)); + }); + }); +} + +void DICEDuplexBringupController::DoEnableGlobal( + AudioDuplexChannels channels, + VoidCallback cb) { + (void)channels; + if (!EnsureGenerationCurrent()) { + DoRollback(kIOReturnOffline, std::move(cb)); + return; + } + + (void)io_.WriteQuadBE(MakeDICEAddress(sections_.global.offset + GlobalOffset::kEnable), + 1, + [this, cb = std::move(cb)](Async::AsyncStatus enableTransportStatus) mutable { + const IOReturn enableStatus = MapTransportStatus(enableTransportStatus); + if (enableStatus != kIOReturnSuccess) { + DoRollback(enableStatus, std::move(cb)); + return; + } + restartSession_.deviceTxArmed = true; + restartSession_.phase = DiceRestartPhase::kDeviceTxArmed; + cb(kIOReturnSuccess); + }); } void DICEDuplexBringupController::DoFinishPrepare(VoidCallback cb) { @@ -1223,11 +1307,8 @@ void DICEDuplexBringupController::ProgramRxForDuplex48k(VoidCallback callback) { return; } - const AudioDuplexChannels channels{ - .deviceToHostIsoChannel = restartSession_.channels.deviceToHostIsoChannel, - .hostToDeviceIsoChannel = restartSession_.channels.hostToDeviceIsoChannel, - }; - DoProgramRx(channels, std::move(callback)); + // Pass the full per-stream channel set so every advertised stream is armed. + DoProgramRx(restartSession_.channels, 0, 0, std::move(callback)); } void DICEDuplexBringupController::ProgramTxAndEnableDuplex48k(VoidCallback callback) { @@ -1241,11 +1322,9 @@ void DICEDuplexBringupController::ProgramTxAndEnableDuplex48k(VoidCallback callb return; } - const AudioDuplexChannels channels{ - .deviceToHostIsoChannel = restartSession_.channels.deviceToHostIsoChannel, - .hostToDeviceIsoChannel = restartSession_.channels.hostToDeviceIsoChannel, - }; - DoProgramTx(channels, std::move(callback)); + // Pass the full per-stream channel set; GLOBAL_ENABLE is asserted once after + // every TX (and previously every RX) stream's ISOC register is written. + DoProgramTx(restartSession_.channels, 0, 0, std::move(callback)); } void DICEDuplexBringupController::ConfirmDuplex48kStart(VoidCallback callback) { diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp index 406edcd8..53c0a0c3 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp @@ -88,8 +88,16 @@ class DICEDuplexBringupController { void DoConfirmClockAccepted(AudioDuplexChannels channels, uint32_t observedNotify, VoidCallback cb); void DoReadGlobalAfterClockAccepted(AudioDuplexChannels channels, uint32_t observedNotify, IOReturn failureStatus, VoidCallback cb); void DoDiscoverStreams(AudioDuplexChannels channels, uint32_t step, VoidCallback cb); - void DoProgramRx(AudioDuplexChannels channels, VoidCallback cb); - void DoProgramTx(AudioDuplexChannels channels, VoidCallback cb); + // Per-stream device programming. A multi-stream DICE device (e.g. Venice F32, + // 2×16 channels) requires every advertised stream's ISOC register written + // before the single GLOBAL_ENABLE, so these recurse over the stream index. + // entrySizeBytes is the per-stream register stride (kSize*4), read once at + // streamIndex 0 and threaded through. + void DoProgramRx(AudioDuplexChannels channels, uint32_t streamIndex, + uint32_t entrySizeBytes, VoidCallback cb); + void DoProgramTx(AudioDuplexChannels channels, uint32_t streamIndex, + uint32_t entrySizeBytes, VoidCallback cb); + void DoEnableGlobal(AudioDuplexChannels channels, VoidCallback cb); void DoFinishPrepare(VoidCallback cb); void DoRollback(IOReturn error, VoidCallback cb); void DoCompleteClockApply(VoidCallback cb); diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/IDICEDuplexProtocol.hpp b/ASFWDriver/Audio/Protocols/DICE/Core/IDICEDuplexProtocol.hpp index ccaec69d..3aa547da 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/IDICEDuplexProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/IDICEDuplexProtocol.hpp @@ -24,9 +24,22 @@ class IDICEDuplexProtocol { using ConfirmCallback = std::function; using ClockApplyCallback = std::function; using HealthCallback = std::function; + using VoidCallback = std::function; virtual ~IDICEDuplexProtocol() = default; + // Pre-read the device's static stream format (DICE TX_NUMBER/RX_NUMBER + + // per-stream channel counts) and publish it through GetRuntimeAudioStreamCaps + // BEFORE duplex channel resolution. A multi-stream device (e.g. Venice F32 = + // 2×16) must allocate an iso channel + IRM reservation per stream; without + // this the first start resolves with empty caps (streamCount=1) and brings + // the device up as a single aggregate stream it rejects. Cross-validated with + // FFADO dice_avdevice.cpp prepare() (m_nb_rx/m_nb_tx loop). Default is a no-op + // success for protocols that derive geometry by other means. + virtual void EnsureRuntimeStreamGeometry(VoidCallback callback) { + callback(kIOReturnSuccess); + } + virtual void PrepareDuplex(const AudioDuplexChannels& channels, const DiceDesiredClockConfig& desiredClock, PrepareCallback callback) = 0; diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp index d484a034..91970eca 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp @@ -104,9 +104,23 @@ bool DICETcatProtocol::GetRuntimeAudioStreamCaps(AudioStreamRuntimeCaps& outCaps static_cast(deviceToHostIsoChannel_.load(std::memory_order_relaxed)); outCaps.hostToDeviceIsoChannel = static_cast(hostToDeviceIsoChannel_.load(std::memory_order_relaxed)); + + // Per-stream geometry: the runtimeCapsValid_ acquire-load above establishes + // happens-before with the writer's release-store, so the plain arrays are + // safe to read here. + outCaps.deviceToHostStreamCount = deviceToHostStreamCount_.load(std::memory_order_relaxed); + outCaps.hostToDeviceStreamCount = hostToDeviceStreamCount_.load(std::memory_order_relaxed); + for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { + outCaps.deviceToHostStreams[i] = deviceToHostStreams_[i]; + outCaps.hostToDeviceStreams[i] = hostToDeviceStreams_[i]; + } return true; } +void DICETcatProtocol::EnsureRuntimeStreamGeometry(VoidCallback callback) { + EnsureRuntimeCapsLoaded(std::move(callback)); +} + void DICETcatProtocol::SetTeardownCancelToken(const std::atomic* cancel) noexcept { teardownCancel_ = cancel; if (duplexCtrl_) { @@ -380,7 +394,7 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) { void DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, const StreamConfig& tx, const StreamConfig& rx) noexcept { - CacheRuntimeCaps(AudioStreamRuntimeCaps{ + AudioStreamRuntimeCaps caps{ .hostInputPcmChannels = tx.TotalPcmChannels(), .hostOutputPcmChannels = rx.TotalPcmChannels(), .deviceToHostAm824Slots = tx.TotalAm824Slots(), @@ -388,7 +402,34 @@ void DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, .sampleRateHz = global.sampleRate, .deviceToHostIsoChannel = tx.FirstActiveIsoChannel(AudioStreamRuntimeCaps::kInvalidIsoChannel), .hostToDeviceIsoChannel = rx.FirstActiveIsoChannel(AudioStreamRuntimeCaps::kInvalidIsoChannel), - }); + }; + + // Per-stream wire geometry from the DICE TX_NUMBER/RX_NUMBER headers. Stream + // count includes streams the device reports with iso=-1 (disabled) that the + // host must still arm for a multi-stream device such as the Venice F32 + // (2×16). Mirrors DICEDuplexBringupController's per-stream fill. + auto fillPerStream = [](const StreamConfig& sc, + uint32_t& outCount, + AudioStreamWireInfo* outStreams) noexcept { + const uint32_t count = (sc.numStreams < kMaxAudioStreamsPerDirection) + ? sc.numStreams + : kMaxAudioStreamsPerDirection; + outCount = count; + for (uint32_t i = 0; i < count; ++i) { + const auto& entry = sc.streams[i]; + outStreams[i].isoChannel = + (entry.isoChannel >= 0 && entry.isoChannel <= 0x3F) + ? static_cast(entry.isoChannel) + : AudioStreamWireInfo::kInvalidIsoChannel; + outStreams[i].pcmChannels = static_cast(entry.pcmChannels); + outStreams[i].am824Slots = static_cast(entry.Am824Slots()); + outStreams[i].midiPorts = static_cast(entry.midiPorts); + } + }; + fillPerStream(tx, caps.deviceToHostStreamCount, caps.deviceToHostStreams); + fillPerStream(rx, caps.hostToDeviceStreamCount, caps.hostToDeviceStreams); + + CacheRuntimeCaps(caps); } void DICETcatProtocol::CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept { @@ -399,6 +440,17 @@ void DICETcatProtocol::CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noex runtimeSampleRateHz_.store(caps.sampleRateHz, std::memory_order_relaxed); deviceToHostIsoChannel_.store(caps.deviceToHostIsoChannel, std::memory_order_relaxed); hostToDeviceIsoChannel_.store(caps.hostToDeviceIsoChannel, std::memory_order_relaxed); + + // Per-stream geometry: write the plain arrays + counts BEFORE the + // release-store of runtimeCapsValid_ so readers that pass the acquire-load + // observe a consistent snapshot. + deviceToHostStreamCount_.store(caps.deviceToHostStreamCount, std::memory_order_relaxed); + hostToDeviceStreamCount_.store(caps.hostToDeviceStreamCount, std::memory_order_relaxed); + for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { + deviceToHostStreams_[i] = caps.deviceToHostStreams[i]; + hostToDeviceStreams_[i] = caps.hostToDeviceStreams[i]; + } + runtimeCapsValid_.store(true, std::memory_order_release); LogRuntimeCaps("cache", caps); } @@ -412,6 +464,12 @@ void DICETcatProtocol::ResetRuntimeCaps() noexcept { hostToDeviceAm824Slots_.store(0, std::memory_order_relaxed); deviceToHostIsoChannel_.store(AudioStreamRuntimeCaps::kInvalidIsoChannel, std::memory_order_relaxed); hostToDeviceIsoChannel_.store(AudioStreamRuntimeCaps::kInvalidIsoChannel, std::memory_order_relaxed); + deviceToHostStreamCount_.store(0, std::memory_order_relaxed); + hostToDeviceStreamCount_.store(0, std::memory_order_relaxed); + for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { + deviceToHostStreams_[i] = AudioStreamWireInfo{}; + hostToDeviceStreams_[i] = AudioStreamWireInfo{}; + } } } // namespace ASFW::Audio::DICE::TCAT diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp index f74d372c..a6915792 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp @@ -54,6 +54,7 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, void ApplyClockConfig(const DiceDesiredClockConfig& desiredClock, ClockApplyCallback callback) override; void ReadDuplexHealth(HealthCallback callback) override; + void EnsureRuntimeStreamGeometry(VoidCallback callback) override; void SetTeardownCancelToken(const std::atomic* cancel) noexcept override; ::ASFW::IRM::IRMClient* GetIRMClient() const override { return irmClient_; } @@ -96,6 +97,16 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, std::atomic hostToDeviceAm824Slots_{0}; std::atomic deviceToHostIsoChannel_{AudioStreamRuntimeCaps::kInvalidIsoChannel}; std::atomic hostToDeviceIsoChannel_{AudioStreamRuntimeCaps::kInvalidIsoChannel}; + + // Per-stream wire geometry (DICE TX_NUMBER/RX_NUMBER + per-stream channels). + // Counts are atomic; the arrays are plain and published through the + // runtimeCapsValid_ release/acquire fence (written before the release-store, + // read after the acquire-load), mirroring the scalar fields above. + std::atomic deviceToHostStreamCount_{0}; + std::atomic hostToDeviceStreamCount_{0}; + AudioStreamWireInfo deviceToHostStreams_[kMaxAudioStreamsPerDirection]{}; + AudioStreamWireInfo hostToDeviceStreams_[kMaxAudioStreamsPerDirection]{}; + std::atomic runtimeCapsValid_{false}; }; diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp index 7144bfe1..1f506174 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp @@ -47,6 +47,15 @@ std::unique_ptr DeviceProtocolFactory::Create( return std::make_unique(busOps, busInfo, nodeId, irmClient); } + if (vendorId == kMidasVendorId && modelId == kMidasVeniceModelId) { + ASFW_LOG(DICE, + "Creating generic DICETcatProtocol for Midas Venice vendor=0x%06x model=0x%06x node=0x%04x", + vendorId, + modelId, + nodeId); + return std::make_unique(busOps, busInfo, nodeId, irmClient); + } + // Check for Apogee Duet FireWire (AV/C + vendor-dependent commands). if (vendorId == kApogeeVendorId && modelId == kApogeeDuetModelId) { ASFW_LOG(Audio, diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp index f541db22..a3b28b84 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp @@ -54,6 +54,8 @@ class DeviceProtocolFactory { static constexpr uint32_t kApogeeDuetModelId = DeviceProfiles::Audio::kApogeeDuetModelId; static constexpr uint32_t kAlesisVendorId = DeviceProfiles::Audio::kAlesisVendorId; static constexpr uint32_t kAlesisMultiMixModelId = DeviceProfiles::Audio::kAlesisMultiMixModelId; + static constexpr uint32_t kMidasVendorId = DeviceProfiles::Audio::kMidasVendorId; + static constexpr uint32_t kMidasVeniceModelId = DeviceProfiles::Audio::kMidasVeniceModelId; static constexpr uint32_t kFocusriteGuidModelSPro40Tcd3070 = DeviceProfiles::Audio::kFocusriteGuidModelSPro40Tcd3070; static constexpr const char* kFocusriteVendorName = DeviceProfiles::Audio::kFocusriteVendorName; @@ -70,6 +72,9 @@ class DeviceProtocolFactory { static constexpr const char* kAlesisVendorName = DeviceProfiles::Audio::kAlesisVendorName; static constexpr const char* kAlesisMultiMixModelName = DeviceProfiles::Audio::kAlesisMultiMixModelName; + static constexpr const char* kMidasVendorName = DeviceProfiles::Audio::kMidasVendorName; + static constexpr const char* kMidasVeniceModelName = + DeviceProfiles::Audio::kMidasVeniceModelName; struct KnownIdentity { uint32_t vendorId{0}; diff --git a/ASFWDriver/Audio/Protocols/DeviceStreamModeQuirks.cpp b/ASFWDriver/Audio/Protocols/DeviceStreamModeQuirks.cpp index a671ce8f..5019cfe3 100644 --- a/ASFWDriver/Audio/Protocols/DeviceStreamModeQuirks.cpp +++ b/ASFWDriver/Audio/Protocols/DeviceStreamModeQuirks.cpp @@ -16,6 +16,10 @@ constexpr uint32_t kFocusriteVendorId = 0x00130e; constexpr uint32_t kSPro14ModelId = 0x000009; constexpr uint32_t kSPro24ModelId = 0x000007; constexpr uint32_t kSPro24DspModelId = 0x000008; + +// Midas DICE devices — same rationale as Focusrite above. +constexpr uint32_t kMidasVendorId = 0x10c73f; +constexpr uint32_t kMidasVeniceModelId = 0x000001; } // namespace std::optional LookupForcedStreamMode( @@ -39,6 +43,10 @@ std::optional LookupForcedStreamMode( return Model::StreamMode::kBlocking; } + if (vendorId == kMidasVendorId && modelId == kMidasVeniceModelId) { + return Model::StreamMode::kBlocking; + } + return std::nullopt; } diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp index 0d91fa0a..b6b8b2a4 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp @@ -36,6 +36,10 @@ inline constexpr uint32_t kApogeeDuetModelId = 0x01dddd; inline constexpr uint32_t kAlesisVendorId = 0x000595; inline constexpr uint32_t kAlesisMultiMixModelId = 0x000000; +// ---- Midas (DICE / TCAT family) ---- +inline constexpr uint32_t kMidasVendorId = 0x10c73f; +inline constexpr uint32_t kMidasVeniceModelId = 0x000001; + // ---- Display names ---- inline constexpr const char* kFocusriteVendorName = "Focusrite"; inline constexpr const char* kSPro40ModelName = "Saffire Pro 40"; @@ -49,5 +53,7 @@ inline constexpr const char* kApogeeVendorName = "Apogee"; inline constexpr const char* kApogeeDuetModelName = "Duet"; inline constexpr const char* kAlesisVendorName = "Alesis"; inline constexpr const char* kAlesisMultiMixModelName = "MultiMix FireWire"; +inline constexpr const char* kMidasVendorName = "Midas"; +inline constexpr const char* kMidasVeniceModelName = "Venice F32"; } // namespace ASFW::DeviceProfiles::Audio diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp index b36d37db..39a2d783 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp @@ -17,6 +17,7 @@ #include "Vendors/AlesisAudioProfiles.hpp" #include "Vendors/ApogeeAudioProfiles.hpp" #include "Vendors/FocusriteAudioProfiles.hpp" +#include "Vendors/MidasAudioProfiles.hpp" #include @@ -31,6 +32,7 @@ class AudioProfileRegistry final { if (auto hint = Focusrite::LookupIdentity(query)) { return hint; } if (auto hint = Apogee::LookupIdentity(query)) { return hint; } if (auto hint = Alesis::LookupIdentity(query)) { return hint; } + if (auto hint = Midas::LookupIdentity(query)) { return hint; } if (auto hint = Focusrite::LookupIdentityByGuid(query)) { return hint; } return std::nullopt; } @@ -43,6 +45,7 @@ class AudioProfileRegistry final { if (auto hint = Focusrite::LookupAudioProfile(query)) { return hint; } if (auto hint = Apogee::LookupAudioProfile(query)) { return hint; } if (auto hint = Alesis::LookupAudioProfile(query)) { return hint; } + if (auto hint = Midas::LookupAudioProfile(query)) { return hint; } return std::nullopt; } }; diff --git a/ASFWDriver/DeviceProfiles/Audio/Vendors/MidasAudioProfiles.hpp b/ASFWDriver/DeviceProfiles/Audio/Vendors/MidasAudioProfiles.hpp new file mode 100644 index 00000000..4fa7bbba --- /dev/null +++ b/ASFWDriver/DeviceProfiles/Audio/Vendors/MidasAudioProfiles.hpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2026 ASFireWire Project +// +// MidasAudioProfiles.hpp - Midas FireWire audio device knowledge (DICE/TCAT). +// Knows ONLY Midas devices; performs no runtime protocol construction. + +#pragma once + +#include "../../Common/DeviceProfileTypes.hpp" +#include "../AudioDeviceIds.hpp" +#include "../AudioProfileTypes.hpp" + +#include + +namespace ASFW::DeviceProfiles::Audio::Midas { + +[[nodiscard]] constexpr std::optional +LookupIdentity(const DeviceProfileQuery& query) noexcept { + if (query.vendorId == kMidasVendorId && query.modelId == kMidasVeniceModelId) { + return DeviceIdentityHint{.vendorId = query.vendorId, + .modelId = query.modelId, + .vendorName = kMidasVendorName, + .modelName = kMidasVeniceModelName, + .source = MatchSource::VendorModel}; + } + return std::nullopt; +} + +[[nodiscard]] constexpr std::optional +LookupAudioProfile(const DeviceProfileQuery& query) noexcept { + if (query.vendorId == kMidasVendorId && query.modelId == kMidasVeniceModelId) { + // Cross-checked against local FFADO 2.4.9 configuration: Midas Venice F32 + // is listed as vendor 0x0010c73f, model 0x000001 in the DICE/TCAT set. + return AudioProfileHint{.family = AudioProtocolFamily::DICE, + .mode = AudioIntegrationMode::kHardcodedNub, + .source = MatchSource::VendorModel}; + } + return std::nullopt; +} + +} // namespace ASFW::DeviceProfiles::Audio::Midas diff --git a/ASFWDriver/Isoch/IsochService.cpp b/ASFWDriver/Isoch/IsochService.cpp index 94a232ad..7b5bea51 100644 --- a/ASFWDriver/Isoch/IsochService.cpp +++ b/ASFWDriver/Isoch/IsochService.cpp @@ -39,7 +39,8 @@ kern_return_t IsochService::PrepareReceive( ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, ASFW::Encoding::AudioWireFormat wireFormat, uint32_t am824Slots, - ASFW::Isoch::IsochReceiveCallback packetCallback) { + ASFW::Isoch::IsochReceiveCallback packetCallback, + uint32_t streamChannels) { if (!isochReceiveContext_) { ASFW::Isoch::Memory::IsochMemoryConfig config; config.numDescriptors = ASFW::Isoch::IsochReceiveContext::kNumDescriptors; @@ -73,7 +74,12 @@ kern_return_t IsochService::PrepareReceive( isochReceiveContext_->SetDirectAudioBindingSource(bindingSource); - const kern_return_t kr = isochReceiveContext_->Configure(channel, 0, wireFormat, am824Slots); + // Master stream: contextIndex 0, channelOffset 0, isSecondary false. + // streamChannels 0 == full binding width (single-stream back-compat); + // multi-stream devices pass their first slice's PCM count. + const kern_return_t kr = isochReceiveContext_->Configure( + channel, 0, wireFormat, am824Slots, + /*channelOffset=*/0, streamChannels, /*isSecondary=*/false); if (kr != kIOReturnSuccess) { ASFW_LOG(Isoch, "IsochService: IR Configure failed: 0x%08x", kr); return kr; @@ -87,12 +93,90 @@ kern_return_t IsochService::PrepareReceive( return kIOReturnSuccess; } +kern_return_t IsochService::PrepareReceiveStream( + uint32_t streamIndex, + uint8_t channel, + HardwareInterface& hardware, + ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, + uint32_t channelOffset, + uint32_t streamChannels, + ASFW::Encoding::AudioWireFormat wireFormat, + uint32_t am824Slots) { + // Stream 0 is the master; callers use PrepareReceive() for it. + if (streamIndex == 0 || streamIndex >= kMaxStreamsPerDirection) { + return kIOReturnBadArgument; + } + + auto& slot = secondaryReceiveContexts_[streamIndex - 1]; + if (!slot) { + ASFW::Isoch::Memory::IsochMemoryConfig config; + config.numDescriptors = ASFW::Isoch::IsochReceiveContext::kNumDescriptors; + config.packetSizeBytes = ASFW::Isoch::IsochReceiveContext::kMaxPacketSize; + config.descriptorAlignment = 16; + config.payloadPageAlignment = 16384; + + auto isochMem = ASFW::Isoch::Memory::IsochDMAMemoryManager::Create(config); + if (!isochMem || !isochMem->Initialize(hardware)) { + ASFW_LOG(Isoch, + "IsochService: secondary RX DMA memory init failed (stream %u)", + streamIndex); + return kIOReturnNoMemory; + } + + slot = IsochReceiveContext::Create(&hardware, isochMem); + if (!slot) { + ASFW_LOG(Isoch, + "IsochService: Failed to create secondary IR context (stream %u)", + streamIndex); + return kIOReturnNoMemory; + } + // Secondary streams do NOT own the clock/ZTS/replay role — those stay on + // the master context, so no ZTS/replay/timing-loss callbacks here. + } + + // Bind to the SAME shared input buffer as the master so this stream can + // write its de-interleaved slice; the context itself applies channelOffset. + slot->SetDirectAudioBindingSource(bindingSource); + + // contextIndex == streamIndex routes this stream to its own OHCI IR context. + // isSecondary=true makes it write PCM only (no clock/replay/ZTS). + const kern_return_t kr = slot->Configure( + channel, static_cast(streamIndex), wireFormat, am824Slots, + channelOffset, streamChannels, /*isSecondary=*/true); + if (kr != kIOReturnSuccess) { + ASFW_LOG(Isoch, + "IsochService: secondary IR Configure failed (stream %u): 0x%08x", + streamIndex, kr); + return kr; + } + + captureChannelOffset_[streamIndex] = channelOffset; + ASFW_LOG(Isoch, + "IsochService: Prepared secondary IR stream %u on channel %u (offset %u, %u ch)", + streamIndex, channel, channelOffset, streamChannels); + return kIOReturnSuccess; +} + kern_return_t IsochService::StartPreparedReceive() { if (!isochReceiveContext_) { return kIOReturnNotReady; } ASFW_LOG(Isoch, "IsochService: Starting prepared IR (Direct-Only)"); - return isochReceiveContext_->Start(); + const kern_return_t kr = isochReceiveContext_->Start(); + if (kr != kIOReturnSuccess) { + return kr; + } + for (auto& ctx : secondaryReceiveContexts_) { + if (ctx) { + const kern_return_t skr = ctx->Start(); + if (skr != kIOReturnSuccess) { + ASFW_LOG(Isoch, + "IsochService: secondary IR start failed: 0x%08x", skr); + return skr; + } + } + } + return kIOReturnSuccess; } kern_return_t IsochService::StopReceive() { @@ -102,6 +186,13 @@ kern_return_t IsochService::StopReceive() { // Safe after Stop(): Poll no longer runs, so no callback is in flight. isochReceiveContext_->SetCallback(nullptr); } + for (auto& ctx : secondaryReceiveContexts_) { + if (ctx) { + ctx->Stop(); + ctx->SetDirectAudioBindingSource(nullptr); + ctx->SetCallback(nullptr); + } + } if (dvCaptureActive_) { dvSink_.Detach(); @@ -269,23 +360,91 @@ kern_return_t IsochService::PrepareTransmit(uint8_t channel, return kIOReturnSuccess; } +kern_return_t IsochService::PrepareTransmitStream(uint32_t streamIndex, + uint8_t channel, + HardwareInterface& hardware, + uint8_t sid) { + // Stream 0 is the master; callers use PrepareTransmit() for it. + if (streamIndex == 0 || streamIndex >= kMaxStreamsPerDirection) { + return kIOReturnBadArgument; + } + + auto& slot = secondaryTransmitContexts_[streamIndex - 1]; + if (!slot) { + ASFW::Isoch::Memory::IsochMemoryConfig config; + config.numDescriptors = ASFW::Isoch::Tx::Layout::kRingBlocks; + config.packetSizeBytes = 0; + config.descriptorAlignment = ASFW::Isoch::Tx::Layout::kOHCIPageSize; + config.payloadPageAlignment = 16384; + config.allocatePayloadSlab = false; + + auto isochMem = ASFW::Isoch::Memory::IsochDMAMemoryManager::Create(config); + if (!isochMem || !isochMem->Initialize(hardware)) { + ASFW_LOG(Isoch, + "IsochService: secondary TX DMA memory init failed (stream %u)", + streamIndex); + return kIOReturnNoMemory; + } + + slot = IsochTransmitContext::Create(&hardware, isochMem); + if (!slot) { + ASFW_LOG(Isoch, + "IsochService: Failed to create secondary IT context (stream %u)", + streamIndex); + return kIOReturnNoMemory; + } + // No TX-preparation callback on secondaries; the master drives refill. + } + + const kern_return_t kr = slot->Configure(channel, sid); + if (kr != kIOReturnSuccess) { + ASFW_LOG(Isoch, + "IsochService: secondary IT Configure failed (stream %u): 0x%08x", + streamIndex, kr); + return kr; + } + // The secondary stream's shared payload slab is wired by the audio-engine + // pass; without it the context is configured but not yet startable. + ASFW_LOG(Isoch, + "IsochService: Prepared secondary IT stream %u on channel %u", + streamIndex, channel); + return kIOReturnSuccess; +} + kern_return_t IsochService::StartPreparedTransmit() { if (!isochTransmitContext_) { return kIOReturnNotReady; } - if (isochReceiveContext_ && - isochReceiveContext_->GetState() == - ASFW::Isoch::IRPolicy::State::Running && - !isochReceiveContext_->IsReplayEstablished()) { - txStartPending_ = true; - ASFW_LOG( - Isoch, - "IsochService: IT RUN deferred until IR cadence/replay is established"); - return kIOReturnSuccess; - } + // Start IT immediately — do NOT defer on IR cadence/replay. The TX producer + // already gates *data* packets on replay establishment (sending NO-DATA CIP + // until the device clock is recovered), so an early start only emits the + // NO-DATA "dry-run" packets that bootstrap the device's stream — matching + // FFADO's DICE streaming engine, which runs the transmit processor to drive + // the device rather than waiting on receive sync. + // + // FW: the old deferral deadlocked devices (e.g. Midas Venice F32) that won't + // transmit their capture stream until they see an active host playback + // stream: IT waited for IR cadence, IR cadence waited for device TX, device + // TX waited for host IT. Focusrite happens to transmit unconditionally so it + // never hit this, but the deferral was an ASFW-specific deviation from the + // reference and is removed. txStartPending_ = false; ASFW_LOG(Isoch, "IsochService: Starting prepared IT (Direct-Only)"); - return isochTransmitContext_->Start(); + const kern_return_t kr = isochTransmitContext_->Start(); + if (kr != kIOReturnSuccess) { + return kr; + } + for (auto& ctx : secondaryTransmitContexts_) { + if (ctx) { + const kern_return_t skr = ctx->Start(); + if (skr != kIOReturnSuccess) { + ASFW_LOG(Isoch, + "IsochService: secondary IT start failed: 0x%08x", skr); + return skr; + } + } + } + return kIOReturnSuccess; } kern_return_t IsochService::StopTransmit() { @@ -293,6 +452,11 @@ kern_return_t IsochService::StopTransmit() { if (isochTransmitContext_) { isochTransmitContext_->Stop(); } + for (auto& ctx : secondaryTransmitContexts_) { + if (ctx) { + ctx->Stop(); + } + } return kIOReturnSuccess; } diff --git a/ASFWDriver/Isoch/IsochService.hpp b/ASFWDriver/Isoch/IsochService.hpp index fc81dc7f..11db22fd 100644 --- a/ASFWDriver/Isoch/IsochService.hpp +++ b/ASFWDriver/Isoch/IsochService.hpp @@ -38,6 +38,12 @@ class IsochService { IsochService() = default; ~IsochService() = default; + // Maximum isochronous streams per direction. Stream 0 is the "master" + // (owns the clock/ZTS/replay role); streams 1+ are secondary slices used by + // multi-stream DICE devices (e.g. Venice F32 = 2×16 channels). A single OHCI + // IR/IT hardware context backs each stream (contextIndex == streamIndex). + static constexpr uint32_t kMaxStreamsPerDirection = 4; + kern_return_t StartReceive(uint8_t channel, HardwareInterface& hardware, ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, @@ -49,11 +55,30 @@ class IsochService { ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, ASFW::Encoding::AudioWireFormat wireFormat = ASFW::Encoding::AudioWireFormat::kAM824, uint32_t am824Slots = 0, - ASFW::Isoch::IsochReceiveCallback packetCallback = nullptr); + ASFW::Isoch::IsochReceiveCallback packetCallback = nullptr, + uint32_t streamChannels = 0); + // Prepare a secondary capture stream (streamIndex >= 1) on its own OHCI IR + // context. channelOffset is the first host input channel this stream writes + // (e.g. 16 for the second 16-ch slice of a 32-ch device); it is recorded for + // the audio-engine de-interleave pass and not yet applied to the decoder. + kern_return_t PrepareReceiveStream(uint32_t streamIndex, + uint8_t channel, + HardwareInterface& hardware, + ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, + uint32_t channelOffset, + uint32_t streamChannels, + ASFW::Encoding::AudioWireFormat wireFormat = ASFW::Encoding::AudioWireFormat::kAM824, + uint32_t am824Slots = 0); kern_return_t StartPreparedReceive(); kern_return_t StopReceive(); + [[nodiscard]] uint32_t CaptureStreamChannelOffset(uint32_t streamIndex) const noexcept { + return (streamIndex < kMaxStreamsPerDirection) + ? captureChannelOffset_[streamIndex] + : 0; + } + // Minimal DV (IEC 61883-2) capture tap: starts IR on the given channel with // no audio binding and streams raw DIF chunks into a shared ring the app // maps via CopyClientMemoryForType(type=1). @@ -67,6 +92,14 @@ class IsochService { kern_return_t PrepareTransmit(uint8_t channel, HardwareInterface& hardware, uint8_t sid); + // Prepare a secondary playback stream (streamIndex >= 1) on its own OHCI IT + // context. The shared payload slab for the secondary stream is wired by the + // audio-engine pass via SetSecondaryTransmitSharedMemory(); this only + // creates and configures the hardware context. + kern_return_t PrepareTransmitStream(uint32_t streamIndex, + uint8_t channel, + HardwareInterface& hardware, + uint8_t sid); kern_return_t StartPreparedTransmit(); kern_return_t StopTransmit(); @@ -119,15 +152,43 @@ class IsochService { ASFW::Isoch::IsochReceiveContext* ReceiveContext() const { return isochReceiveContext_.get(); } ASFW::Isoch::IsochTransmitContext* TransmitContext() const { return isochTransmitContext_.get(); } + // Per-stream accessors: index 0 == master, index 1+ == secondary streams. + ASFW::Isoch::IsochReceiveContext* ReceiveContext(uint32_t streamIndex) const { + if (streamIndex == 0) return isochReceiveContext_.get(); + if (streamIndex < kMaxStreamsPerDirection) + return secondaryReceiveContexts_[streamIndex - 1].get(); + return nullptr; + } + ASFW::Isoch::IsochTransmitContext* TransmitContext(uint32_t streamIndex) const { + if (streamIndex == 0) return isochTransmitContext_.get(); + if (streamIndex < kMaxStreamsPerDirection) + return secondaryTransmitContexts_[streamIndex - 1].get(); + return nullptr; + } + private: kern_return_t ClaimDuplexGuid(uint64_t guid); void RefreshReceiveTimingLossCallback() noexcept; void OnReceiveTimingLossDetected() noexcept; void StartDeferredTransmitIfReady() noexcept; + // Stream 0 (master) capture/playback contexts — own the clock/ZTS/replay + // role. Their lifecycle and callbacks are unchanged from the single-stream + // design; secondary streams layer on top without touching them. OSSharedPtr isochReceiveContext_; std::unique_ptr isochTransmitContext_; + // Secondary streams [1 .. kMaxStreamsPerDirection). Index i here maps to + // stream (i + 1); each runs on its own OHCI context (contextIndex == stream). + OSSharedPtr + secondaryReceiveContexts_[kMaxStreamsPerDirection - 1]; + std::unique_ptr + secondaryTransmitContexts_[kMaxStreamsPerDirection - 1]; + + // First host input channel each capture stream writes (de-interleave offset); + // recorded here for the audio-engine pass. Index 0 == master (offset 0). + uint32_t captureChannelOffset_[kMaxStreamsPerDirection]{0, 0, 0, 0}; + // DV capture shared ring (see Receive/DVCaptureSink.hpp) struct DVRingMapping { OSSharedPtr memory{}; diff --git a/ASFWDriver/Isoch/Receive/IsochReceiveContext.cpp b/ASFWDriver/Isoch/Receive/IsochReceiveContext.cpp index b3998b9f..9ba26a2f 100644 --- a/ASFWDriver/Isoch/Receive/IsochReceiveContext.cpp +++ b/ASFWDriver/Isoch/Receive/IsochReceiveContext.cpp @@ -62,7 +62,10 @@ IsochReceiveContext::Registers IsochReceiveContext::GetRegisters(uint8_t index) kern_return_t IsochReceiveContext::Configure(uint8_t channel, uint8_t contextIndex, Encoding::AudioWireFormat wireFormat, - uint32_t am824Slots) { + uint32_t am824Slots, + uint32_t channelOffset, + uint32_t streamChannels, + bool isSecondary) { if (!hardware_ || !dmaMemory_) { return kIOReturnNotReady; } @@ -76,6 +79,9 @@ kern_return_t IsochReceiveContext::Configure(uint8_t channel, registers_ = GetRegisters(contextIndex_); wireFormat_ = wireFormat; am824Slots_ = am824Slots; + channelOffset_ = channelOffset; + streamChannels_ = streamChannels; + isSecondary_ = isSecondary; return rxRing_.SetupRings(*dmaMemory_, kNumDescriptors, kMaxPacketSize); } @@ -119,6 +125,8 @@ kern_return_t IsochReceiveContext::Start() { rxRing_.ResetForStart(); absoluteFrameCursor_ = 0; cursorInitialized_ = false; + secondaryAnchored_ = false; + secondaryAnchorEpoch_ = 0; dbcInitialized_ = false; lastDbc_ = 0; rxZtsPublishCount_ = 0; @@ -197,7 +205,11 @@ uint32_t IsochReceiveContext::Poll() { directInputView_.streamMode = ASFW::Audio::Runtime::AudioStreamMode::kUnknown; directInputView_.hostToDeviceWireFormat = ASFW::Audio::Runtime::AudioWireFormat::kAM824; - if (!replayResetForStart_ && + // Master-only: reset the shared clock/replay timeline once + // on (re)bind. A secondary slice must never touch the shared + // control block's cadence/replay — the master owns it. + if (!isSecondary_ && + !replayResetForStart_ && directInputView_.control) { directInputView_.control->rxSytCadence.Reset(); directInputView_.control->rxSequenceReplay.Reset(); @@ -206,11 +218,13 @@ uint32_t IsochReceiveContext::Poll() { replayResetForStart_ = true; } - // Data plane (RX -> input buffer) and the controller-side clock - // publisher both arm. The clock publisher writes the shared - // timeline; the ADK side mirrors that timeline to HAL. + // Data plane (RX -> input buffer) arms for every stream. The + // controller-side clock publisher is master-only: it writes + // the shared timeline the ADK side mirrors to HAL. directInputWriter_.Bind(&directInputView_); - clockPublisher_.Bind(&directInputView_); + if (!isSecondary_) { + clockPublisher_.Bind(&directInputView_); + } } else { ASFW_LOG(Isoch, "IR: direct audio binding invalid or has no input (gen %llu -> %llu). Disarming valid=%d hasIn=%d control=%p rate=%u", @@ -257,16 +271,46 @@ uint32_t IsochReceiveContext::Poll() { const Rx::IsochRxDmaRing::CompletedPacket& pkt) { uint64_t callbackTimestamp = 0; if (pkt.payload) { + // Anchor a secondary slice to the master's published ring position + // before writing. The master owns inputProducedEndFrame; this stream + // is frame-locked to it but its OHCI context armed/started at a + // different time, so its private cursor must be re-based to the + // master's leading edge once per replay epoch. Until the master has + // produced (masterEnd != 0) we drop this stream's packets rather than + // write a mis-anchored slice. + if (isSecondary_ && directInputView_.control) { + const uint64_t epoch = directInputView_.control->rxReplayEpochResets.load( + std::memory_order_acquire); + if (!secondaryAnchored_ || epoch != secondaryAnchorEpoch_) { + const uint64_t masterEnd = + directInputView_.control->inputProducedEndFrame.load( + std::memory_order_acquire); + if (masterEnd == 0) { + return; + } + absoluteFrameCursor_ = masterEnd; + secondaryAnchored_ = true; + secondaryAnchorEpoch_ = epoch; + } + } + const uint64_t packetFirstAudioFrame = absoluteFrameCursor_; - const uint32_t channels = directInputView_.memory.inputChannels; + // This stream decodes its own slice width (streamChannels_), or the + // binding's full width for single-stream devices, and writes it at + // channelOffset_. Only the master publishes the producer timeline. + const uint32_t channels = streamChannels_ > 0 + ? streamChannels_ + : directInputView_.memory.inputChannels; const uint32_t slots = directInputView_.deviceToHostAm824Slots; const auto result = directProcessor_.ProcessPacket(pkt.payload, pkt.actualLength, packetFirstAudioFrame, channels, slots, - wireFormat_); + wireFormat_, + channelOffset_, + /*publishTimeline=*/!isSecondary_); const bool packetAccepted = result.status == AudioEngine::Direct::Rx:: @@ -274,6 +318,20 @@ uint32_t IsochReceiveContext::Poll() { result.status == AudioEngine::Direct::Rx:: DirectRxWriteStatus::kInvalidBinding; + + // Secondary slice: PCM is already written at its channel offset. + // Skip all master-only bookkeeping (clock/replay/ZTS/DBC/callback); + // just advance this stream's frame cursor in lockstep with the + // master (both are frame-locked by the device clock). Under packet + // loss the two halves can transiently skew until a discontinuity + // re-anchors — the input safety offset absorbs the sub-cycle case. + if (isSecondary_) { + if (packetAccepted) { + absoluteFrameCursor_ = + packetFirstAudioFrame + result.framesDecoded; + } + return; + } if (!packetAccepted) { ResetReplayEpochForDiscontinuity(); } else { diff --git a/ASFWDriver/Isoch/Receive/IsochReceiveContext.hpp b/ASFWDriver/Isoch/Receive/IsochReceiveContext.hpp index bf38c4d5..c2669149 100644 --- a/ASFWDriver/Isoch/Receive/IsochReceiveContext.hpp +++ b/ASFWDriver/Isoch/Receive/IsochReceiveContext.hpp @@ -87,10 +87,17 @@ class IsochReceiveContext : public OSObject, "IR descriptor ring must be an integer number of interrupt " "groups or the interrupt cadence breaks at the ring wrap"); + // channelOffset/streamChannels/isSecondary configure multi-stream + // de-interleave: this context writes `streamChannels` PCM channels (0 == the + // binding's full width) into the shared input buffer at `channelOffset`. A + // secondary context writes PCM only and does not own the clock/ZTS/replay. kern_return_t Configure(uint8_t channel, uint8_t contextIndex, Encoding::AudioWireFormat wireFormat = Encoding::AudioWireFormat::kAM824, - uint32_t am824Slots = 0); + uint32_t am824Slots = 0, + uint32_t channelOffset = 0, + uint32_t streamChannels = 0, + bool isSecondary = false); kern_return_t Start(); void Stop(); uint32_t Poll(); @@ -160,6 +167,24 @@ class IsochReceiveContext : public OSObject, Encoding::AudioWireFormat wireFormat_{Encoding::AudioWireFormat::kAM824}; uint32_t am824Slots_{0}; + // Multi-stream de-interleave: this context decodes `streamChannels_` PCM + // channels (0 == use the binding's full inputChannels) and writes them into + // the shared interleaved input buffer at `channelOffset_`. A secondary + // stream writes PCM only — the master (isSecondary_ == false) owns the + // clock/ZTS/replay timeline and the producer cursor. + uint32_t channelOffset_{0}; + uint32_t streamChannels_{0}; + bool isSecondary_{false}; + + // Secondary-slice frame anchoring. A secondary context runs on its own OHCI + // IR context that arms/starts independently of the master, so its private + // cursor (from 0) has no relation to the master's ring position. We anchor it + // to the master's published inputProducedEndFrame on the first write of each + // replay epoch so both halves of a frame land in the same ring slot; the two + // streams are frame-locked by the device clock and stay aligned thereafter. + bool secondaryAnchored_{false}; + uint64_t secondaryAnchorEpoch_{0}; + uint64_t absoluteFrameCursor_{0}; bool cursorInitialized_{false}; uint64_t rxZtsPublishCount_{0}; diff --git a/tests/audio/AudioProfileRegistryTests.cpp b/tests/audio/AudioProfileRegistryTests.cpp index a2370e9f..2a9c2348 100644 --- a/tests/audio/AudioProfileRegistryTests.cpp +++ b/tests/audio/AudioProfileRegistryTests.cpp @@ -49,6 +49,8 @@ TEST(AudioProfileRegistryTests, SelectsIntegrationModeForKnownDevices) { AudioIntegrationMode::kAVCDriven); EXPECT_EQ(ModeFor(ids::kAlesisVendorId, ids::kAlesisMultiMixModelId), AudioIntegrationMode::kHardcodedNub); + EXPECT_EQ(ModeFor(ids::kMidasVendorId, ids::kMidasVeniceModelId), + AudioIntegrationMode::kHardcodedNub); } TEST(AudioProfileRegistryTests, RejectsUnknownDevices) { @@ -87,6 +89,9 @@ TEST(AudioProfileRegistryTests, RecognizesKnownVendorModelPairs) { EXPECT_TRUE(AudioProfileRegistry::LookupIdentity( ByVendorModel(ids::kAlesisVendorId, ids::kAlesisMultiMixModelId)) .has_value()); + EXPECT_TRUE(AudioProfileRegistry::LookupIdentity( + ByVendorModel(ids::kMidasVendorId, ids::kMidasVeniceModelId)) + .has_value()); } TEST(AudioProfileRegistryTests, InfersFocusriteIdentityFromGuid) { @@ -132,4 +137,18 @@ TEST(AudioProfileRegistryTests, RecognizesAlesisMultiMixDiceProfile) { EXPECT_EQ(profile->family, AudioProtocolFamily::DICE); } +TEST(AudioProfileRegistryTests, RecognizesMidasVeniceDiceProfile) { + const auto identity = AudioProfileRegistry::LookupIdentity( + ByVendorModel(ids::kMidasVendorId, ids::kMidasVeniceModelId)); + ASSERT_TRUE(identity.has_value()); + EXPECT_STREQ(identity->vendorName, ids::kMidasVendorName); + EXPECT_STREQ(identity->modelName, ids::kMidasVeniceModelName); + + const auto profile = AudioProfileRegistry::LookupBestAudioProfile( + ByVendorModel(ids::kMidasVendorId, ids::kMidasVeniceModelId)); + ASSERT_TRUE(profile.has_value()); + EXPECT_EQ(profile->mode, AudioIntegrationMode::kHardcodedNub); + EXPECT_EQ(profile->family, AudioProtocolFamily::DICE); +} + } // namespace diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt index 5a4359a3..5718a36f 100644 --- a/tests/audio/CMakeLists.txt +++ b/tests/audio/CMakeLists.txt @@ -169,6 +169,7 @@ add_audio_test(DiceProfileTests "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp" "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/GenericDiceProfile.cpp" "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/FocusriteSaffireProfile.cpp" + "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.cpp" ) # IT Descriptor Layout Tests diff --git a/tests/audio/DiceProfileTests.cpp b/tests/audio/DiceProfileTests.cpp index 18b020ef..04460ff1 100644 --- a/tests/audio/DiceProfileTests.cpp +++ b/tests/audio/DiceProfileTests.cpp @@ -11,6 +11,7 @@ #include "Audio/DriverKit/Config/DICE/DiceProfileRegistry.hpp" #include "Audio/DriverKit/Config/DICE/Isoch/Profiles/FocusriteSaffireProfile.hpp" #include "Audio/DriverKit/Config/DICE/Isoch/Profiles/GenericDiceProfile.hpp" +#include "Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.hpp" namespace { @@ -78,6 +79,54 @@ TEST(DiceProfileTests, FocusriteAsymmetricSafetyOffsetsAndLatencies) { EXPECT_EQ(profile->RxReportedLatencyFrames(96000.0), 59); } +TEST(DiceProfileTests, ResolvesMidasVeniceProfileByVendorAndModel) { + const auto* profile = AudioProfileRegistry::FindProfile(0x10c73f, 0x000001, 0x10c73f04004011dfULL); + + ASSERT_NE(profile, nullptr); + EXPECT_STREQ(profile->Name(), "Midas Venice F32 (DICE)"); + EXPECT_EQ(profile->TxWireFormat(), ASFW::Encoding::AudioWireFormat::kRawPcm24In32); + EXPECT_EQ(profile->RxWireFormat(), ASFW::Encoding::AudioWireFormat::kAM824); + + // Venice F32 at 48 kHz: single 32-ch stream per direction, 0 MIDI, DBS=32. + EXPECT_EQ(profile->TxChannelCount(), 32); + EXPECT_EQ(profile->RxChannelCount(), 32); + EXPECT_EQ(profile->TxMidiSlots(), 0); + EXPECT_EQ(profile->RxMidiSlots(), 0); + EXPECT_EQ(profile->TxDbs(), 32); + EXPECT_EQ(profile->RxDbs(), 32); + + const auto* diceProfile = static_cast(profile); + EXPECT_TRUE(diceProfile->Quirks().tx.preserveFdfInNoDataPackets); + EXPECT_EQ(diceProfile->Quirks().tx.hostToDevicePcmEncoding, + ASFW::Encoding::AudioWireFormat::kRawPcm24In32); +} + +TEST(DiceProfileTests, MidasVeniceSafetyOffsetsAndLatencies) { + const auto* profile = AudioProfileRegistry::FindProfile(0x10c73f, 0x000001, 0x10c73f04004011dfULL); + ASSERT_NE(profile, nullptr); + + // 48 kHz: Tx = 6 * 8 = 48, Rx = 16 * 8 = 128 + EXPECT_EQ(profile->TxSafetyOffsetFrames(48000.0), 48); + EXPECT_EQ(profile->RxSafetyOffsetFrames(48000.0), 128); + EXPECT_EQ(profile->TxReportedLatencyFrames(48000.0), 29); + EXPECT_EQ(profile->RxReportedLatencyFrames(48000.0), 29); + + // 96 kHz: Tx = 8 * 16 = 128, Rx = 18 * 16 = 288 + EXPECT_EQ(profile->TxSafetyOffsetFrames(96000.0), 128); + EXPECT_EQ(profile->RxSafetyOffsetFrames(96000.0), 288); + EXPECT_EQ(profile->TxReportedLatencyFrames(96000.0), 59); + EXPECT_EQ(profile->RxReportedLatencyFrames(96000.0), 59); +} + +TEST(DiceProfileTests, MidasVendorWithWrongModelDoesNotMatchVeniceProfile) { + // Only vendor+model together should match — no vendor-only fallback for Midas. + const auto* profile = AudioProfileRegistry::FindProfile(0x10c73f, 0x999999, 0x0ULL); + // Should fall through to generic profile, not Venice. + if (profile != nullptr) { + EXPECT_STRNE(profile->Name(), "Midas Venice F32 (DICE)"); + } +} + TEST(DiceProfileTests, GenericDiceDefaultOffsetsAndLatencies) { const auto* profile = AudioProfileRegistry::FindProfile(0x999999, 0x000001, 0x123456789ULL); ASSERT_NE(profile, nullptr); diff --git a/tests/audio/IsochServiceTxPreparationTests.cpp b/tests/audio/IsochServiceTxPreparationTests.cpp index ce6076a8..f97d5377 100644 --- a/tests/audio/IsochServiceTxPreparationTests.cpp +++ b/tests/audio/IsochServiceTxPreparationTests.cpp @@ -119,4 +119,65 @@ TEST(IsochServiceTxPreparation, 1U); } +// Secondary-stream container: a multi-stream DICE device (Venice F32 = 2×16) +// needs IsochService to manage a second IR and second IT context on their own +// OHCI context indices, while the master (stream 0) is untouched. This pass only +// builds the container + records the de-interleave channel offset; the engine +// wires the decode/slab later. +TEST(IsochServiceTxPreparation, SecondaryStreamRejectsIndexZeroAndOutOfRange) { + IsochService service; + HardwareInterface hardware; + + // Index 0 is the master — must go through PrepareReceive/PrepareTransmit. + EXPECT_EQ(service.PrepareReceiveStream(0, /*channel=*/1, hardware, + /*binding=*/nullptr, /*offset=*/0, + /*streamChannels=*/16), + kIOReturnBadArgument); + EXPECT_EQ(service.PrepareTransmitStream(0, /*channel=*/0, hardware, /*sid=*/0x3f), + kIOReturnBadArgument); + // Out of range. + EXPECT_EQ(service.PrepareReceiveStream(IsochService::kMaxStreamsPerDirection, + 2, hardware, nullptr, 16, 16), + kIOReturnBadArgument); +} + +TEST(IsochServiceTxPreparation, SecondaryReceiveStreamCreatesContextAndRecordsOffset) { + IsochService service; + HardwareInterface hardware; + + EXPECT_EQ(service.ReceiveContext(1), nullptr); + + ASSERT_EQ( + service.PrepareReceiveStream(/*streamIndex=*/1, /*channel=*/2, hardware, + /*bindingSource=*/nullptr, + /*channelOffset=*/16, + /*streamChannels=*/16), + kIOReturnSuccess); + + // Master untouched; secondary now exists and its de-interleave offset is recorded. + EXPECT_EQ(service.ReceiveContext(0), nullptr); + EXPECT_NE(service.ReceiveContext(1), nullptr); + EXPECT_EQ(service.CaptureStreamChannelOffset(0), 0u); + EXPECT_EQ(service.CaptureStreamChannelOffset(1), 16u); + + // StopAll tears the whole service down without touching the (absent) master. + service.StopAll(); +} + +TEST(IsochServiceTxPreparation, SecondaryTransmitStreamCreatesIndependentContext) { + IsochService service; + HardwareInterface hardware; + + ASSERT_EQ( + service.PrepareTransmitStream(/*streamIndex=*/1, /*channel=*/4, hardware, + /*sid=*/0x3f), + kIOReturnSuccess); + + EXPECT_EQ(service.TransmitContext(0), nullptr); // master not created + EXPECT_NE(service.TransmitContext(1), nullptr); // secondary created + EXPECT_NE(service.TransmitContext(1), service.TransmitContext(0)); + + service.StopAll(); +} + } // namespace diff --git a/tests/common/DeviceProtocolFactoryTests.cpp b/tests/common/DeviceProtocolFactoryTests.cpp index 77c24bc4..d4c4a682 100644 --- a/tests/common/DeviceProtocolFactoryTests.cpp +++ b/tests/common/DeviceProtocolFactoryTests.cpp @@ -59,6 +59,11 @@ TEST(DeviceProtocolFactoryTests, SelectsIntegrationModeForKnownDevices) { DeviceProtocolFactory::kAlesisVendorId, DeviceProtocolFactory::kAlesisMultiMixModelId), DeviceIntegrationMode::kHardcodedNub); + + EXPECT_EQ(DeviceProtocolFactory::LookupIntegrationMode( + DeviceProtocolFactory::kMidasVendorId, + DeviceProtocolFactory::kMidasVeniceModelId), + DeviceIntegrationMode::kHardcodedNub); } TEST(DeviceProtocolFactoryTests, RejectsUnknownDevices) { @@ -103,6 +108,10 @@ TEST(DeviceProtocolFactoryTests, RecognizesKnownVendorModelPairs) { EXPECT_TRUE(DeviceProtocolFactory::IsKnownDevice( DeviceProtocolFactory::kAlesisVendorId, DeviceProtocolFactory::kAlesisMultiMixModelId)); + + EXPECT_TRUE(DeviceProtocolFactory::IsKnownDevice( + DeviceProtocolFactory::kMidasVendorId, + DeviceProtocolFactory::kMidasVeniceModelId)); } TEST(DeviceProtocolFactoryTests, InfersFocusriteIdentityFromGuid) { @@ -157,4 +166,13 @@ TEST(DeviceProtocolFactoryTests, RecognizesAlesisMultiMixDiceProfile) { EXPECT_STREQ(multiMix->modelName, DeviceProtocolFactory::kAlesisMultiMixModelName); } +TEST(DeviceProtocolFactoryTests, RecognizesMidasVeniceDiceProfile) { + const auto venice = DeviceProtocolFactory::LookupKnownIdentity( + DeviceProtocolFactory::kMidasVendorId, DeviceProtocolFactory::kMidasVeniceModelId); + ASSERT_TRUE(venice.has_value()); + EXPECT_EQ(venice->integrationMode, DeviceIntegrationMode::kHardcodedNub); + EXPECT_STREQ(venice->vendorName, DeviceProtocolFactory::kMidasVendorName); + EXPECT_STREQ(venice->modelName, DeviceProtocolFactory::kMidasVeniceModelName); +} + } // namespace diff --git a/tests/devices/DICEDuplexBringupControllerTests.cpp b/tests/devices/DICEDuplexBringupControllerTests.cpp index f91771a4..b672d6aa 100644 --- a/tests/devices/DICEDuplexBringupControllerTests.cpp +++ b/tests/devices/DICEDuplexBringupControllerTests.cpp @@ -1516,3 +1516,35 @@ TEST(DICEDuplexBringupControllerTests, IRMAllocateResourcesReturnsGenerationMism ASSERT_TRUE(status.has_value()); EXPECT_EQ(*status, ASFW::IRM::AllocationStatus::GenerationMismatch); } + +// AudioDuplexChannels invariant: stream[0] resolves to the legacy scalar field so +// single-stream call sites that only set the scalar stay byte-for-byte unchanged; +// the per-stream arrays carry the *additional* streams (Venice F32 = 2×16). +TEST(DICEDuplexBringupControllerTests, DuplexChannelsStreamZeroIsLegacyScalar) { + AudioDuplexChannels ch{}; + ch.deviceToHostIsoChannel = 7; // capture stream[0] (host IR) + ch.hostToDeviceIsoChannel = 5; // playback stream[0] (host IT) + // Arrays still at defaults; stream[0] must ignore them. + EXPECT_EQ(ch.CaptureChannel(0), 7); + EXPECT_EQ(ch.PlaybackChannel(0), 5); +} + +TEST(DICEDuplexBringupControllerTests, DuplexChannelsAdditionalStreamsUseArrays) { + AudioDuplexChannels ch{}; + ch.deviceToHostIsoChannel = 1; + ch.hostToDeviceIsoChannel = 0; + ch.captureStreamCount = 2; + ch.playbackStreamCount = 2; + ch.captureIsoChannels[1] = 2; // capture stream[1] + ch.playbackIsoChannels[1] = 3; // playback stream[1] + + EXPECT_EQ(ch.CaptureChannel(0), 1); + EXPECT_EQ(ch.CaptureChannel(1), 2); + EXPECT_EQ(ch.PlaybackChannel(0), 0); + EXPECT_EQ(ch.PlaybackChannel(1), 3); + // All four wire channels distinct — no bus collision across the duplex set. + EXPECT_NE(ch.CaptureChannel(0), ch.CaptureChannel(1)); + EXPECT_NE(ch.PlaybackChannel(0), ch.PlaybackChannel(1)); + EXPECT_NE(ch.CaptureChannel(0), ch.PlaybackChannel(0)); + EXPECT_NE(ch.CaptureChannel(1), ch.PlaybackChannel(1)); +} diff --git a/tests/devices/DiceDuplexRestartCoordinatorTests.cpp b/tests/devices/DiceDuplexRestartCoordinatorTests.cpp index 17769f11..e8c34d83 100644 --- a/tests/devices/DiceDuplexRestartCoordinatorTests.cpp +++ b/tests/devices/DiceDuplexRestartCoordinatorTests.cpp @@ -204,12 +204,14 @@ class FakeDiceHostTransport final : public IDiceHostTransport { ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, ASFW::Encoding::AudioWireFormat wireFormat = ASFW::Encoding::AudioWireFormat::kAM824, - uint32_t am824Slots = 0) noexcept override { + uint32_t am824Slots = 0, + uint32_t streamChannels = 0) noexcept override { log_.Add("host.prepare_receive"); lastReceiveChannel = channel; lastReceiveBindingSource = bindingSource; lastReceiveWireFormat = wireFormat; lastReceiveAm824Slots = am824Slots; + lastReceiveStreamChannels = streamChannels; ++prepareReceiveCalls; return prepareReceiveStatus; } @@ -224,6 +226,40 @@ class FakeDiceHostTransport final : public IDiceHostTransport { return prepareTransmitStatus; } + kern_return_t PrepareReceiveStream( + uint32_t streamIndex, + uint8_t channel, + HardwareInterface&, + ASFW::Audio::Runtime::IDirectAudioBindingSource* bindingSource, + uint32_t channelOffset, + uint32_t streamChannels, + ASFW::Encoding::AudioWireFormat wireFormat = + ASFW::Encoding::AudioWireFormat::kAM824, + uint32_t am824Slots = 0) noexcept override { + log_.Add("host.prepare_receive_stream"); + lastSecondaryReceiveIndex = streamIndex; + lastSecondaryReceiveChannel = channel; + lastSecondaryReceiveOffset = channelOffset; + lastSecondaryReceiveChannels = streamChannels; + lastSecondaryReceiveBindingSource = bindingSource; + (void)wireFormat; + (void)am824Slots; + ++prepareReceiveStreamCalls; + return prepareReceiveStatus; + } + + kern_return_t PrepareTransmitStream(uint32_t streamIndex, + uint8_t channel, + HardwareInterface&, + uint8_t sourceId) noexcept override { + log_.Add("host.prepare_transmit_stream"); + lastSecondaryTransmitIndex = streamIndex; + lastSecondaryTransmitChannel = channel; + lastSecondaryTransmitSourceId = sourceId; + ++prepareTransmitStreamCalls; + return prepareTransmitStatus; + } + kern_return_t StartPreparedReceive() noexcept override { log_.Add("host.start_receive"); ++startReceiveCalls; @@ -260,6 +296,7 @@ class FakeDiceHostTransport final : public IDiceHostTransport { ASFW::Audio::Runtime::IDirectAudioBindingSource* lastReceiveBindingSource{nullptr}; ASFW::Encoding::AudioWireFormat lastReceiveWireFormat{ASFW::Encoding::AudioWireFormat::kAM824}; uint32_t lastReceiveAm824Slots{0}; + uint32_t lastReceiveStreamChannels{0}; uint8_t lastTransmitChannel{0}; uint8_t lastTransmitSourceId{0}; uint32_t lastTransmitMode{0}; @@ -274,6 +311,16 @@ class FakeDiceHostTransport final : public IDiceHostTransport { int reserveCaptureCalls{0}; int prepareReceiveCalls{0}; int prepareTransmitCalls{0}; + int prepareReceiveStreamCalls{0}; + int prepareTransmitStreamCalls{0}; + uint32_t lastSecondaryReceiveIndex{0}; + uint8_t lastSecondaryReceiveChannel{0}; + uint32_t lastSecondaryReceiveOffset{0}; + uint32_t lastSecondaryReceiveChannels{0}; + ASFW::Audio::Runtime::IDirectAudioBindingSource* lastSecondaryReceiveBindingSource{nullptr}; + uint32_t lastSecondaryTransmitIndex{0}; + uint8_t lastSecondaryTransmitChannel{0}; + uint8_t lastSecondaryTransmitSourceId{0}; int startReceiveCalls{0}; int startTransmitCalls{0}; int stopCalls{0};