From 11477eac9da463849dcdef51ac64f4f0573a17f1 Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 03:43:18 -0300 Subject: [PATCH 1/7] WIP: backup RME Fireface 800 protocol + Isoch transmit changes (Stage 5G/5H) Safety commit of uncommitted RME work before continuing Stage 5H development, per user request. Scope is intentionally narrow: RMEFireface800Protocol (new) and the Isoch transmit-path changes it depends on. Related supporting edits (DeviceProtocolFactory, AudioProfileRegistry, device IDs, RMEAudioProfiles.hpp, etc.) remain uncommitted in the working tree and were left out of this snapshot. --- .../Protocols/RME/RMEFireface800Protocol.cpp | 1457 +++++++++++++++++ .../Protocols/RME/RMEFireface800Protocol.hpp | 93 ++ ASFWDriver/Isoch/IsochService.cpp | 103 ++ ASFWDriver/Isoch/IsochService.hpp | 12 + .../Isoch/Transmit/IsochTransmitContext.cpp | 1057 ++++++++++++ .../Isoch/Transmit/IsochTransmitContext.hpp | 62 + ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp | 973 ++++++++++- ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp | 110 +- 8 files changed, 3847 insertions(+), 20 deletions(-) create mode 100644 ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp create mode 100644 ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp new file mode 100644 index 00000000..25ecdc33 --- /dev/null +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp @@ -0,0 +1,1457 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 ASFireWire Project +// +// RMEFireface800Protocol.cpp - Stage 5H bounded silent playback integration. +// +// This stage preserves the verified device start/stop handshake and the Stage +// 5G D-D-D-S packet shape, then runs the immutable 48-cycle silence program as +// a circular ring for exactly 100 ms. OHCI is stopped before the device and IRM +// cleanup. Core Audio StartIO remains rejected; refill and interrupts stay off. + +#include "RMEFireface800Protocol.hpp" + +#include "../../../Logging/Logging.hpp" +#include "../../../Bus/IRM/IRMClient.hpp" +#include "../../Wire/AMDTP/AmdtpTxPacketizer.hpp" +#include "../../Wire/AMDTP/AmdtpPacketTimeline.hpp" +#include "../../../Shared/Isoch/IsochAudioTransport.hpp" +#include "../../../Hardware/OHCIDescriptors.hpp" +#include "../Backends/SyncAsyncBridge.hpp" + +#include +#include +#include +#include +#include + +namespace ASFW::Audio::RME { +namespace { + +constexpr uint64_t kStfAddress = 0x0000fc88f000ULL; +constexpr uint64_t kSyncStatusAddress = 0x0000801c0000ULL; +constexpr uint64_t kClockConfigAddress = 0x0000801c0004ULL; +constexpr uint64_t kTxIsochChannelAddress = 0x0000801c0008ULL; +constexpr uint64_t kRxPacketFormatAddress = 0x0000fc88f004ULL; +constexpr uint64_t kAllocTxStreamAddress = 0x0000fc88f008ULL; +constexpr uint64_t kIsochCommStartAddress = 0x0000fc88f00cULL; +constexpr uint64_t kIsochCommStopAddress = 0x0000fc88f010ULL; + +Async::FWAddress MakeAddress(uint64_t value) noexcept { + return Async::FWAddress{Async::FWAddress::AddressParts{ + .addressHi = static_cast((value >> 32U) & 0xFFFFU), + .addressLo = static_cast(value & 0xFFFFFFFFU), + }}; +} + +} // namespace + +RMEFireface800Protocol::RMEFireface800Protocol( + Protocols::Ports::FireWireBusOps& busOps, + Protocols::Ports::FireWireBusInfo& busInfo, + uint16_t nodeId, + uint64_t deviceGuid, + ::ASFW::IRM::IRMClient* irmClient) noexcept + : busOps_(busOps), + busInfo_(busInfo), + nodeId_(nodeId), + deviceGuid_(deviceGuid), + irmClient_(irmClient) {} + +RMEFireface800Protocol::~RMEFireface800Protocol() { + BestEffortStopDeviceCommunication(); + active_.store(false, std::memory_order_release); + ReleaseReservedResources(); + if (probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } +} + +IOReturn RMEFireface800Protocol::Initialize() { + playbackPreflightRoute_.store(0U, std::memory_order_release); + stage5hInFlight_.store(false, std::memory_order_release); + active_.store(true, std::memory_order_release); + ASFW_LOG(Audio, + "[RME] Stage 5F no-CIP wire-format preflight starting GUID=0x%016llx node=%u gen=%u", + deviceGuid_, + static_cast(nodeId_ & 0x3FU), + busInfo_.GetGeneration().value); + ProbeClockConfig(); + return kIOReturnSuccess; +} + +IOReturn RMEFireface800Protocol::Shutdown() { + BestEffortStopDeviceCommunication(); + active_.store(false, std::memory_order_release); + stage5hInFlight_.store(false, std::memory_order_release); + ReleaseReservedResources(); + if (probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } + ASFW_LOG(Audio, "[RME] Stage 5F preflight stopped GUID=0x%016llx", deviceGuid_); + return kIOReturnSuccess; +} + +void RMEFireface800Protocol::UpdateRuntimeContext( + uint16_t nodeId, + Protocols::AVC::FCPTransport* transport) { + (void)transport; + nodeId_ = nodeId; +} + +bool RMEFireface800Protocol::GetPlaybackPreflightRoute( + PlaybackPreflightRoute& outRoute) const { + if (!active_.load(std::memory_order_acquire)) { + return false; + } + const uint64_t snapshot = + playbackPreflightRoute_.load(std::memory_order_acquire); + if (snapshot == 0U) { + return false; + } + + outRoute.channel = static_cast(snapshot & 0xFFU); + outRoute.bandwidthUnits = + static_cast((snapshot >> 8U) & 0x00FFFFFFU); + outRoute.sampleRateHz = static_cast(snapshot >> 32U); + outRoute.deviceCommunicationStopped = true; + return outRoute.channel <= 63U && outRoute.bandwidthUnits != 0U && + outRoute.sampleRateHz != 0U; +} + +IOReturn RMEFireface800Protocol::RunBoundedPlaybackIntegrationPreflight( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep runHostBurst, + BoundedPlaybackStep cleanupHost) { + if (!active_.load(std::memory_order_acquire) || !prepareHost || + !runHostBurst || !cleanupHost || + route.channel > 63U || route.bandwidthUnits != 1286U || + route.sampleRateHz != 192000U || + !route.deviceCommunicationStopped) { + return kIOReturnBadArgument; + } + + PlaybackPreflightRoute heldRoute{}; + if (!GetPlaybackPreflightRoute(heldRoute) || + heldRoute.channel != route.channel || + heldRoute.bandwidthUnits != route.bandwidthUnits || + heldRoute.sampleRateHz != route.sampleRateHz || + !heldRoute.deviceCommunicationStopped) { + return kIOReturnNotReady; + } + + bool expected = false; + if (!stage5hInFlight_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) { + ASFW_LOG_WARNING(Audio, + "[RME] Stage 5H bounded integration already in flight GUID=0x%016llx", + deviceGuid_); + return kIOReturnBusy; + } + + // Consume the held route so repeated StartIO retries cannot enter while the + // synchronous bounded test owns the FF800 engine and its IRM reservation. + playbackPreflightRoute_.store(0U, std::memory_order_release); + + IOReturn finalStatus = prepareHost(); + bool hostPrepared = finalStatus == kIOReturnSuccess; + bool startAccepted = false; + bool stopAccepted = false; + bool releaseAccepted = false; + + if (!hostPrepared) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H host cadence preparation failed kr=0x%x; proceeding with bounded cleanup", + finalStatus); + } + + const uint32_t dataBlockQuadlets = DecodeChannelCount(route.sampleRateHz); + if (hostPrepared && dataBlockQuadlets == 0U) { + finalStatus = kIOReturnBadArgument; + } else if (hostPrepared) { + // Exact Stage 4D/5F FF800 begin-session value: communication enable, + // S800 flag, and the 192 kHz data-block-quadlet count. + const uint32_t startValue = + 0x80000000U | 0x00000800U | dataBlockQuadlets; + const std::array startLE = { + static_cast(startValue & 0xFFU), + static_cast((startValue >> 8U) & 0xFFU), + static_cast((startValue >> 16U) & 0xFFU), + static_cast((startValue >> 24U) & 0xFFU), + }; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const auto startResult = ASFW::Audio::WaitForAsyncResult( + [this, generation, node, startLE](auto done) { + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStartAddress), + std::span{startLE}, + FW::FwSpeed::S400, + [done](Async::AsyncStatus status, + std::span payload) mutable { + (void)payload; + const bool accepted = + status == Async::AsyncStatus::kSuccess; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + if (!probeHandle_.IsValid()) { + done(kIOReturnNoResources, false); + } + }, + 500U, + kIOReturnTimeout); + startAccepted = + startResult.status == kIOReturnSuccess && startResult.value; + if (!startAccepted) { + if (startResult.status == kIOReturnTimeout && + probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } + finalStatus = startResult.status == kIOReturnSuccess + ? kIOReturnIOError + : startResult.status; + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H FF800 playback-engine start failed kr=0x%x gen=%u value=0x%08x", + startResult.status, + generation.value, + startValue); + } else { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H FF800 playback engine started channel=%u rate=%u value=0x%08x", + route.channel, + route.sampleRateHz, + startValue); + finalStatus = runHostBurst(); + } + } + + // Cleanup is deliberately ordered on success and every failure path: + // host RUN clear/ACTIVE drain first, then FF800 stop, then IRM release. + const IOReturn hostCleanupStatus = cleanupHost(); + if (hostCleanupStatus != kIOReturnSuccess && + finalStatus == kIOReturnSuccess) { + finalStatus = hostCleanupStatus; + } + if (hostCleanupStatus != kIOReturnSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H host cleanup failed kr=0x%x before FF800 stop", + hostCleanupStatus); + } + + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; + const auto stopGeneration = busInfo_.GetGeneration(); + const auto stopNode = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const auto stopResult = ASFW::Audio::WaitForAsyncResult( + [this, stopGeneration, stopNode, stopLE](auto done) { + probeHandle_ = busOps_.WriteBlock( + stopGeneration, + stopNode, + MakeAddress(kIsochCommStopAddress), + std::span{stopLE}, + FW::FwSpeed::S400, + [done](Async::AsyncStatus status, + std::span payload) mutable { + (void)payload; + const bool accepted = + status == Async::AsyncStatus::kSuccess; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + if (!probeHandle_.IsValid()) { + done(kIOReturnNoResources, false); + } + }, + 500U, + kIOReturnTimeout); + stopAccepted = + stopResult.status == kIOReturnSuccess && stopResult.value; + if (!stopAccepted && finalStatus == kIOReturnSuccess) { + finalStatus = stopResult.status == kIOReturnSuccess + ? kIOReturnIOError + : stopResult.status; + } + if (!stopAccepted) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H FF800 stop command failed kr=0x%x gen=%u", + stopResult.status, + stopGeneration.value); + } + + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + + const bool hadReservation = playbackResourcesReserved_; + const uint8_t reservedChannel = reservedPlaybackChannel_; + const uint32_t reservedBandwidth = reservedPlaybackBandwidthUnits_; + playbackResourcesReserved_ = false; + reservedPlaybackChannel_ = 0xFFU; + reservedPlaybackBandwidthUnits_ = 0U; + + if (!hadReservation || irmClient_ == nullptr || reservedChannel > 63U || + reservedBandwidth == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H cleanup missing held IRM route expectedChannel=%u expectedBandwidth=%u actualChannel=%u actualBandwidth=%u", + route.channel, + route.bandwidthUnits, + reservedChannel, + reservedBandwidth); + if (finalStatus == kIOReturnSuccess) { + finalStatus = kIOReturnNotReady; + } + } else { + const auto releaseResult = ASFW::Audio::WaitForAsyncResult( + [this, reservedChannel, reservedBandwidth](auto done) { + irmClient_->ReleaseResources( + reservedChannel, + reservedBandwidth, + [done](IRM::AllocationStatus status) mutable { + const bool accepted = + status == IRM::AllocationStatus::Success; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + }, + 1000U, + kIOReturnTimeout); + releaseAccepted = + releaseResult.status == kIOReturnSuccess && releaseResult.value; + if (!releaseAccepted && finalStatus == kIOReturnSuccess) { + finalStatus = releaseResult.status == kIOReturnSuccess + ? kIOReturnIOError + : releaseResult.status; + } + if (!releaseAccepted) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H IRM release failed kr=0x%x channel=%u bandwidth=%u", + releaseResult.status, + reservedChannel, + reservedBandwidth); + } + } + + stage5hInFlight_.store(false, std::memory_order_release); + + if (stopAccepted && releaseAccepted) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H FF800 playback engine stopped and IRM route released channel=%u bandwidth=%u", + reservedChannel, + reservedBandwidth); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H cleanup completed with errors startAccepted=%u stopAccepted=%u releaseAccepted=%u", + startAccepted ? 1U : 0U, + stopAccepted ? 1U : 0U, + releaseAccepted ? 1U : 0U); + } + + if (finalStatus == kIOReturnSuccess) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H bounded circular silent playback integration passed; Core Audio StartIO remains rejected"); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H bounded circular silent playback integration failed kr=0x%x; Core Audio StartIO remains rejected", + finalStatus); + } + return finalStatus; +} + +void RMEFireface800Protocol::ProbeClockConfig() noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kClockConfigAddress), + 4, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] Clock probe failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + const uint32_t rate = DecodeSampleRate(raw); + currentRate_ = rate; + const char* source = DecodeClockSource(raw); + + ASFW_LOG(Audio, + "[RME] ✅ Clock raw=0x%08x rate=%u source=%{public}s gen=%u", + raw, + rate, + source, + generation.value); + ASFW_LOG(Audio, + "[RME] Clock options: spdifOut=%{public}s emphasis=%{public}s opticalOut=%{public}s wordSingleSpeed=%{public}s spdifIn=%{public}s", + (raw & 0x00000020U) != 0U ? "professional" : "consumer", + (raw & 0x00000040U) != 0U ? "on" : "off", + (raw & 0x00000100U) != 0U ? "S/PDIF" : "ADAT", + (raw & 0x00002000U) != 0U ? "on" : "off", + (raw & 0x00000200U) != 0U ? "optical" : "coaxial"); + + LogStreamCaps(rate); + if (rate == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F STF write skipped: decoded rate is zero"); + ProbeSyncStatus(); + return; + } + ValidateStfWritePath(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Clock probe could not be submitted"); + } +} + +void RMEFireface800Protocol::ValidateStfWritePath(uint32_t rate) noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const std::array rateLE = { + static_cast(rate & 0xFFU), + static_cast((rate >> 8U) & 0xFFU), + static_cast((rate >> 16U) & 0xFFU), + static_cast((rate >> 24U) & 0xFFU), + }; + + ASFW_LOG(Audio, + "[RME] Stage 5F writing current STF=%u Hz (no rate change) gen=%u", + rate, + generation.value); + + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kStfAddress), + std::span{rateLE}, + FW::FwSpeed::S400, + [this, generation, rate](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F STF write failed status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + ProbeSyncStatus(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F STF write accepted: %u Hz; waiting 100 ms before verification", + rate); + IOSleep(100); + VerifyClockAfterStfWrite(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F STF write could not be submitted"); + ProbeSyncStatus(); + } +} + +void RMEFireface800Protocol::VerifyClockAfterStfWrite(uint32_t expectedRate) noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kClockConfigAddress), + 4, + FW::FwSpeed::S400, + [this, generation, expectedRate](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F clock verification failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + ProbeSyncStatus(); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + const uint32_t actualRate = DecodeSampleRate(raw); + if (actualRate == expectedRate) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F STF write-path verified: expected=%u actual=%u raw=0x%08x", + expectedRate, + actualRate, + raw); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F STF verification mismatch: expected=%u actual=%u raw=0x%08x", + expectedRate, + actualRate, + raw); + } + ProbeSyncStatus(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F clock verification could not be submitted"); + ProbeSyncStatus(); + } +} + +void RMEFireface800Protocol::ProbeSyncStatus() noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kSyncStatusAddress), + 8, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess || payload.size() < 8U) { + ASFW_LOG_ERROR(Audio, + "[RME] Sync-status probe failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + return; + } + + const uint32_t status0 = ReadLE32(payload.data()); + const uint32_t status1 = ReadLE32(payload.data() + 4U); + const char* referred = DecodeReferredClockSource(status0, status1); + const uint32_t referredRate = DecodeReferredRate(status0); + + ASFW_LOG(Audio, + "[RME] ✅ Sync status raw0=0x%08x raw1=0x%08x gen=%u", + status0, + status1, + generation.value); + ASFW_LOG(Audio, + "[RME] External sync: word=%{public}s spdif=%{public}s adat1=%{public}s adat2=%{public}s", + DecodeExternalState(status0, 0x40000000U, 0x20000000U), + DecodeExternalState(status0, 0x00080000U, 0x00040000U), + DecodeExternalState(status0, 0x00000400U, 0x00001000U), + DecodeExternalState(status0, 0x00000800U, 0x00002000U)); + ASFW_LOG(Audio, + "[RME] Referred clock: source=%{public}s rate=%u", + referred, + referredRate); + + ProbeTxIsochChannel(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Sync-status probe could not be submitted"); + } +} + +void RMEFireface800Protocol::ProbeTxIsochChannel() noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kTxIsochChannelAddress), + 4, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] TX isoch-channel probe failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + if (raw == 0xFFFFFFFFU) { + ASFW_LOG(Audio, + "[RME] ✅ TX isoch channel: inactive/not allocated raw=0xffffffff gen=%u", + generation.value); + } else { + ASFW_LOG(Audio, + "[RME] ✅ TX isoch channel reported raw=0x%08x channel=%u gen=%u", + raw, + raw & 0x3FU, + generation.value); + } + ReservePlaybackResourcesPreflight(currentRate_); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] TX isoch-channel probe could not be submitted"); + } +} + + +void RMEFireface800Protocol::ReservePlaybackResourcesPreflight(uint32_t rate) noexcept { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (playbackResourcesReserved_) { + ASFW_LOG(Audio, + "[RME] Stage 5F playback resources already reserved channel=%u bandwidth=%u", + reservedPlaybackChannel_, + reservedPlaybackBandwidthUnits_); + return; + } + if (irmClient_ == nullptr) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F cannot reserve playback resources: IRM client unavailable"); + return; + } + + const uint32_t bandwidthUnits = CalculatePlaybackBandwidthUnits(rate); + if (bandwidthUnits == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F cannot reserve playback resources: invalid rate=%u", + rate); + return; + } + + irmClient_->ReadResourcesSnapshot( + [this, rate, bandwidthUnits](IRM::AllocationStatus status, IRM::ResourceSnapshot snapshot) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != IRM::AllocationStatus::Success) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F IRM snapshot failed status=%{public}s", + IRM::ToString(status)); + return; + } + + const uint8_t channel = + SelectAvailableChannel(snapshot.channelsAvailable31_0, + snapshot.channelsAvailable63_32); + ASFW_LOG(Audio, + "[RME] Stage 5F IRM snapshot bandwidthAvailable=%u requested=%u selectedChannel=%u", + snapshot.bandwidthAvailable, + bandwidthUnits, + channel); + + if (channel == 0xFFU || snapshot.bandwidthAvailable < bandwidthUnits) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F insufficient IRM resources bandwidthAvailable=%u requested=%u channel=%u", + snapshot.bandwidthAvailable, + bandwidthUnits, + channel); + return; + } + + irmClient_->AllocateResources( + channel, + bandwidthUnits, + [this, rate, channel, bandwidthUnits](IRM::AllocationStatus allocateStatus) { + if (allocateStatus != IRM::AllocationStatus::Success) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F IRM reservation failed status=%{public}s channel=%u bandwidth=%u", + IRM::ToString(allocateStatus), + channel, + bandwidthUnits); + return; + } + + if (!active_.load(std::memory_order_acquire)) { + irmClient_->ReleaseResources( + channel, bandwidthUnits, [](IRM::AllocationStatus) {}); + return; + } + + reservedPlaybackChannel_ = channel; + reservedPlaybackBandwidthUnits_ = bandwidthUnits; + playbackResourcesReserved_ = true; + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F IRM playback reservation acquired channel=%u bandwidth=%u", + channel, + bandwidthUnits); + ProgramRxPacketFormat(rate, channel, bandwidthUnits); + }); + }); +} + +void RMEFireface800Protocol::ProgramRxPacketFormat(uint32_t rate, + uint8_t channel, + uint32_t bandwidthUnits) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + if (dataBlockQuadlets == 0U || channel > 63U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F RX packet-format skipped rate=%u dbq=%u channel=%u", + rate, + dataBlockQuadlets, + channel); + ReleaseReservedResources(); + return; + } + + // Linux ff-protocol-former.c programs ((DBQ << 3) << 8) | channel. + const uint32_t format = ((dataBlockQuadlets << 3U) << 8U) | channel; + const std::array formatLE = { + static_cast(format & 0xFFU), + static_cast((format >> 8U) & 0xFFU), + static_cast((format >> 16U) & 0xFFU), + static_cast((format >> 24U) & 0xFFU), + }; + + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kRxPacketFormatAddress), + std::span{formatLE}, + FW::FwSpeed::S400, + [this, generation, rate, channel, bandwidthUnits, format]( + Async::AsyncStatus status, std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F RX packet-format write failed status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + ReleaseReservedResources(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F playback route armed rate=%u dbq=%u channel=%u bandwidth=%u format=0x%08x", + rate, + DecodeChannelCount(rate), + channel, + bandwidthUnits, + format); + RequestDeviceTxAllocation(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F RX packet-format write could not be submitted"); + ReleaseReservedResources(); + } +} + +void RMEFireface800Protocol::RequestDeviceTxAllocation(uint32_t rate) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + if (dataBlockQuadlets == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F TX allocation skipped: invalid rate=%u", + rate); + ReleaseReservedResources(); + return; + } + + const std::array dbqLE = { + static_cast(dataBlockQuadlets & 0xFFU), + static_cast((dataBlockQuadlets >> 8U) & 0xFFU), + static_cast((dataBlockQuadlets >> 16U) & 0xFFU), + static_cast((dataBlockQuadlets >> 24U) & 0xFFU), + }; + + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kAllocTxStreamAddress), + std::span{dbqLE}, + FW::FwSpeed::S400, + [this, generation, rate, dataBlockQuadlets]( + Async::AsyncStatus status, std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F device TX allocation request failed status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + ReleaseReservedResources(); + return; + } + + deviceTxAllocationRequested_ = true; + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F device TX allocation request accepted dbq=%u; polling channel", + dataBlockQuadlets); + PollDeviceTxIsochChannel(rate, 1U); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F device TX allocation request could not be submitted"); + ReleaseReservedResources(); + } +} + +void RMEFireface800Protocol::PollDeviceTxIsochChannel(uint32_t rate, + uint32_t attempt) noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kTxIsochChannelAddress), + 4, + FW::FwSpeed::S400, + [this, generation, rate, attempt](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F TX channel poll failed status=%{public}s bytes=%zu attempt=%u gen=%u", + Async::ToString(status), + payload.size(), + attempt, + generation.value); + StopDeviceCommunicationPreflight(); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + if (raw == 0xFFFFFFFFU) { + if (attempt >= 10U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F device TX allocation timed out after %u polls", + attempt); + StopDeviceCommunicationPreflight(); + return; + } + + IOSleep(50); + PollDeviceTxIsochChannel(rate, attempt + 1U); + return; + } + + const uint8_t channel = static_cast(raw & 0x3FU); + deviceTxChannel_ = channel; + deviceTxAllocated_ = true; + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F device TX channel allocated raw=0x%08x channel=%u polls=%u rate=%u", + raw, + channel, + attempt, + rate); + StartDeviceCommunicationPreflight(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F TX channel poll could not be submitted attempt=%u", + attempt); + StopDeviceCommunicationPreflight(); + } +} + +void RMEFireface800Protocol::StartDeviceCommunicationPreflight(uint32_t rate) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + if (dataBlockQuadlets == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F start command skipped: invalid rate=%u", + rate); + StopDeviceCommunicationPreflight(); + return; + } + + // Linux ff800_begin_session(): 0x80000000 | DBQ | S800 flag. + // This machine is linked at S800, verified by topology/self-ID. + const uint32_t startValue = 0x80000000U | 0x00000800U | dataBlockQuadlets; + const std::array startLE = { + static_cast(startValue & 0xFFU), + static_cast((startValue >> 8U) & 0xFFU), + static_cast((startValue >> 16U) & 0xFFU), + static_cast((startValue >> 24U) & 0xFFU), + }; + + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStartAddress), + std::span{startLE}, + FW::FwSpeed::S400, + [this, generation, rate, startValue](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F start command failed status=%{public}s gen=%u value=0x%08x", + Async::ToString(status), + generation.value, + startValue); + StopDeviceCommunicationPreflight(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F device isoch start accepted rate=%u dbq=%u txChannel=%u playbackChannel=%u value=0x%08x; running 100 ms without OHCI contexts", + rate, + DecodeChannelCount(rate), + deviceTxChannel_, + reservedPlaybackChannel_, + startValue); + IOSleep(100); + StopDeviceCommunicationPreflight(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F start command could not be submitted"); + StopDeviceCommunicationPreflight(); + } +} + +void RMEFireface800Protocol::StopDeviceCommunicationPreflight() noexcept { + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStopAddress), + std::span{stopLE}, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F stop command failed status=%{public}s gen=%u; releasing host IRM reservation", + Async::ToString(status), + generation.value); + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + ReleaseReservedResources(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F stop command accepted; FF800 TX channel may remain latched by design (channel=%u)", + deviceTxChannel_); + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + const uint64_t routeSnapshot = + (static_cast(currentRate_) << 32U) | + (static_cast( + reservedPlaybackBandwidthUnits_ & 0x00FFFFFFU) + << 8U) | + static_cast(reservedPlaybackChannel_); + playbackPreflightRoute_.store( + playbackResourcesReserved_ ? routeSnapshot : 0U, + std::memory_order_release); + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F playback route held for bounded host packet test channel=%u bandwidth=%u deviceStopped=1", + reservedPlaybackChannel_, + reservedPlaybackBandwidthUnits_); + RunNoCipWireFormatSelfTest(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F stop command could not be submitted; releasing host IRM reservation"); + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + ReleaseReservedResources(); + } +} + +void RMEFireface800Protocol::RunNoCipWireFormatSelfTest() noexcept { + using namespace ASFW::Protocols::Audio::AMDTP; + + constexpr uint32_t kCycles = 8U; + constexpr uint32_t kSlotCount = 4U; + constexpr uint32_t kFramesPerDataPacket = 32U; + constexpr uint32_t kDbq = 12U; + constexpr uint32_t kExpectedDataPackets = 6U; + constexpr uint32_t kExpectedEmptyPackets = 2U; + constexpr uint32_t kExpectedFrames = 192U; + constexpr uint32_t kDataPacketBytes = kFramesPerDataPacket * kDbq * 4U; + + std::array slots{}; + AmdtpPacketTimeline timeline{}; + if (!timeline.AttachSlots(slots.data(), static_cast(slots.size()))) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F wire self-test failed: timeline attach"); + return; + } + + auto packetStorage = std::unique_ptr( + new (std::nothrow) uint8_t[kSlotCount * kDataPacketBytes]); + if (!packetStorage) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F wire self-test failed: packet storage allocation"); + return; + } + + AmdtpStreamConfig config{}; + config.sampleRate = 192000U; + config.streamMode = StreamMode::Blocking; + config.sid = 1U; + config.dbs = static_cast(kDbq); + config.pcmChannels = static_cast(kDbq); + config.midiSlots = 0U; + config.framesPerDataPacket = static_cast(kFramesPerDataPacket); + config.maxPacketBytes = kDataPacketBytes; + config.includeCipHeader = false; + + AmdtpTxPolicy policy{}; + policy.hostToDevicePcmEncoding = PcmSlotEncoding::RawSigned24In32BE; + policy.initializeNonAudioSlots = false; + policy.emptyPacketsDuringIdle = true; + + AmdtpTxPacketizer packetizer{}; + packetizer.BindTimeline(&timeline); + if (!packetizer.Configure(config, policy)) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F wire self-test failed: packetizer configure"); + return; + } + packetizer.Reset(0U, 0U); + + uint32_t dataPackets = 0U; + uint32_t emptyPackets = 0U; + uint32_t frames = 0U; + bool geometryValid = true; + bool payloadZero = true; + bool ohciMetadataValid = true; + uint32_t metadataCommits = 0U; + uint32_t skipCycles = 0U; + + Async::HW::OHCIDescriptor skipDescriptor{}; + Async::HW::ITDescriptorBuilder::BuildOutputLast( + skipDescriptor, + { + .dataIOVA = 0U, + .payloadSize = 0U, + .branchIOVA = 0x00001000U, + .zValue = 4U, + .interruptBits = Async::HW::OHCIDescriptor::kIntNever, + }); + const uint32_t skipControlHigh = + skipDescriptor.control >> + Async::HW::OHCIDescriptor::kControlHighShift; + const bool skipDescriptorValid = + ((skipControlHigh >> Async::HW::OHCIDescriptor::kCmdShift) & 0xFU) == + Async::HW::OHCIDescriptor::kCmdOutputLast && + ((skipControlHigh >> Async::HW::OHCIDescriptor::kKeyShift) & 0x7U) == + Async::HW::OHCIDescriptor::kKeyStandard && + ((skipControlHigh >> Async::HW::OHCIDescriptor::kBranchShift) & 0x3U) == + Async::HW::OHCIDescriptor::kBranchAlways && + (skipDescriptor.control & 0xFFFFU) == 0U && + skipDescriptor.dataAddress == 0U && + Async::HW::DecodeBranchPhys32_AT(skipDescriptor.branchWord) == + 0x00001000U && + (skipDescriptor.branchWord & 0xFU) == 4U && + !Async::HW::IsImmediate(skipDescriptor); + + for (uint32_t cycle = 0; cycle < kCycles; ++cycle) { + uint8_t* packetBytes = packetStorage.get() + + (cycle % kSlotCount) * kDataPacketBytes; + for (uint32_t i = 0; i < kDataPacketBytes; ++i) { + packetBytes[i] = 0xA5U; + } + + TxPacketSlotView slot{ + .packetIndex = cycle, + .bytes = packetBytes, + .capacityBytes = kDataPacketBytes, + }; + AmdtpTimingState timing{}; + timing.disposition = AmdtpPacketDisposition::Data; + + PreparedTxPacket packet{}; + if (!packetizer.PrepareNextPacket(slot, timing, packet)) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F wire self-test failed: packet preparation cycle=%u", + cycle); + return; + } + + if (packet.isData) { + ++dataPackets; + frames += packet.framesInPacket; + if (packet.byteCount != kDataPacketBytes || + packet.framesInPacket != kFramesPerDataPacket || + packet.dbs != kDbq) { + geometryValid = false; + } + for (uint32_t i = 0; i < packet.byteCount; ++i) { + if (packetBytes[i] != 0U) { + payloadZero = false; + break; + } + } + } else { + ++emptyPackets; + if (packet.byteCount != 0U || packet.framesInPacket != 0U) { + geometryValid = false; + } + } + + // Exercise the same plain helper used by DextTxSlotProvider to publish + // OHCI metadata. FF800 packets must use tag 0 (no CIP), S400, tcode A, + // and data_length equal to the raw payload size (or zero when idle). + const uint32_t headerQ0 = ASFW::IsochTransport::BuildIsochTxHeaderQ0( + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader); + const uint32_t headerQ1 = + ASFW::IsochTransport::BuildIsochTxHeaderQ1(packet.byteCount); + const uint32_t expectedLength = packet.isData ? kDataPacketBytes : 0U; + const auto decodedTag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + const bool shouldSkip = + ASFW::IsochTransport::ShouldSkipIsochTxPacket( + decodedTag, packet.byteCount); + if (shouldSkip) { + ++skipCycles; + } + if (decodedTag != + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(headerQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(headerQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(headerQ1) != + expectedLength || + shouldSkip != !packet.isData) { + ohciMetadataValid = false; + } else { + ++metadataCommits; + } + } + + if (dataPackets == kExpectedDataPackets && + emptyPackets == kExpectedEmptyPackets && + frames == kExpectedFrames && geometryValid && payloadZero && + ohciMetadataValid && metadataCommits == kCycles && + skipCycles == kExpectedEmptyPackets && skipDescriptorValid) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F FF800 no-CIP wire self-test passed cycles=%u data=%u empty=%u frames=%u dataBytes=%u dbq=%u headerBytes=0 payload=silence", + kCycles, + dataPackets, + emptyPackets, + frames, + kDataPacketBytes, + kDbq); + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F FF800 OHCI metadata/skip self-test passed cycles=%u tag=0 speed=S400 tcode=0x%x dataLength=%u skipCycles=%u descriptor=OUTPUT_LAST/standard/zero/Z4 commits=%u", + kCycles, + ASFW::IsochTransport::kIsochDataBlockTCode, + kDataPacketBytes, + skipCycles, + metadataCommits); + ASFW_LOG(Audio, + "[RME] Stage 5F complete: raw no-CIP packet bytes, OHCI tag/data-length metadata, and true idle skip semantics are verified; OHCI contexts and Core Audio streaming remain disabled"); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F wire self-test mismatch data=%u/%u empty=%u/%u frames=%u/%u geometry=%u zero=%u", + dataPackets, + kExpectedDataPackets, + emptyPackets, + kExpectedEmptyPackets, + frames, + kExpectedFrames, + geometryValid ? 1U : 0U, + payloadZero ? 1U : 0U); + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F OHCI metadata/skip mismatch valid=%u descriptor=%u skip=%u/%u commits=%u/%u", + ohciMetadataValid ? 1U : 0U, + skipDescriptorValid ? 1U : 0U, + skipCycles, + kExpectedEmptyPackets, + metadataCommits, + kCycles); + } +} + +void RMEFireface800Protocol::BestEffortStopDeviceCommunication() noexcept { + if (!deviceTxAllocationRequested_ && !deviceTxAllocated_) { + return; + } + + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + + const auto handle = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStopAddress), + std::span{stopLE}, + FW::FwSpeed::S400, + [generation](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (status == Async::AsyncStatus::kSuccess) { + ASFW_LOG(Audio, + "[RME] Stage 5F best-effort stop submitted during shutdown gen=%u", + generation.value); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F best-effort stop failed during shutdown status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + } + }); + + if (!handle.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F best-effort stop could not be submitted during shutdown"); + } +} + +void RMEFireface800Protocol::ReleaseReservedResources() noexcept { + playbackPreflightRoute_.store(0U, std::memory_order_release); + stage5hInFlight_.store(false, std::memory_order_release); + if (!playbackResourcesReserved_ || irmClient_ == nullptr) { + return; + } + + const uint8_t channel = reservedPlaybackChannel_; + const uint32_t bandwidthUnits = reservedPlaybackBandwidthUnits_; + playbackResourcesReserved_ = false; + reservedPlaybackChannel_ = 0xFFU; + reservedPlaybackBandwidthUnits_ = 0U; + + irmClient_->ReleaseResources( + channel, + bandwidthUnits, + [channel, bandwidthUnits](IRM::AllocationStatus status) { + if (status == IRM::AllocationStatus::Success) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F released playback IRM reservation channel=%u bandwidth=%u", + channel, + bandwidthUnits); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F playback IRM release status=%{public}s channel=%u bandwidth=%u", + IRM::ToString(status), + channel, + bandwidthUnits); + } + }); +} + +void RMEFireface800Protocol::LogStreamCaps(uint32_t rate) const noexcept { + const uint32_t channels = DecodeChannelCount(rate); + ASFW_LOG(Audio, + "[RME] Stream geometry: mode=%{public}s rate=%u captureChannels=%u playbackChannels=%u sampleFormat=PCM24-in-32", + DecodeStreamMode(rate), + rate, + channels, + channels); + ASFW_LOG(Audio, + "[RME] Supported FF800 geometry: low=28x28 mid=20x20 high=12x12 MIDI=1-in/1-out"); +} + +uint32_t RMEFireface800Protocol::ReadLE32(const uint8_t* bytes) noexcept { + return static_cast(bytes[0]) | + (static_cast(bytes[1]) << 8U) | + (static_cast(bytes[2]) << 16U) | + (static_cast(bytes[3]) << 24U); +} + +uint32_t RMEFireface800Protocol::DecodeSampleRate(uint32_t value) noexcept { + switch (value & 0x0000001EU) { + case 0x00000002U: return 32000; + case 0x00000000U: return 44100; + case 0x00000006U: return 48000; + case 0x0000000AU: return 64000; + case 0x00000008U: return 88200; + case 0x0000000EU: return 96000; + case 0x00000012U: return 128000; + case 0x00000010U: return 176400; + case 0x00000016U: return 192000; + default: return 0; + } +} + +const char* RMEFireface800Protocol::DecodeClockSource(uint32_t value) noexcept { + if ((value & 0x00000001U) != 0U) { + return "internal"; + } + + switch (value & 0x00001C00U) { + case 0x00000000U: return "ADAT1"; + case 0x00000400U: return "ADAT2"; + case 0x00000C00U: return "S/PDIF"; + case 0x00001000U: return "word clock"; + case 0x00001800U: return "LTC/TCO"; + default: return "unknown external"; + } +} + +const char* RMEFireface800Protocol::DecodeExternalState(uint32_t value, + uint32_t lockedMask, + uint32_t syncedMask) noexcept { + if ((value & lockedMask) == 0U) { + return "none"; + } + return (value & syncedMask) != 0U ? "sync" : "lock"; +} + +const char* RMEFireface800Protocol::DecodeReferredClockSource(uint32_t status0, + uint32_t status1) noexcept { + if ((status1 & 0x00000001U) != 0U) { + return "internal"; + } + + switch (status0 & 0x01C00000U) { + case 0x00000000U: return "ADAT1"; + case 0x00400000U: return "ADAT2"; + case 0x00C00000U: return "S/PDIF"; + case 0x01000000U: return "word clock"; + case 0x01400000U: return "TCO"; + default: return "none"; + } +} + +uint32_t RMEFireface800Protocol::DecodeReferredRate(uint32_t status0) noexcept { + switch (status0 & 0x1E000000U) { + case 0x02000000U: return 32000; + case 0x04000000U: return 44100; + case 0x06000000U: return 48000; + case 0x08000000U: return 64000; + case 0x0A000000U: return 88200; + case 0x0C000000U: return 96000; + case 0x0E000000U: return 128000; + case 0x10000000U: return 176400; + case 0x12000000U: return 192000; + default: return 0; + } +} + +const char* RMEFireface800Protocol::DecodeStreamMode(uint32_t rate) noexcept { + switch (rate) { + case 32000: + case 44100: + case 48000: + return "low/single-speed"; + case 64000: + case 88200: + case 96000: + return "mid/double-speed"; + case 128000: + case 176400: + case 192000: + return "high/quad-speed"; + default: + return "unknown"; + } +} + +uint32_t RMEFireface800Protocol::DecodeChannelCount(uint32_t rate) noexcept { + switch (rate) { + case 32000: + case 44100: + case 48000: + return 28; + case 64000: + case 88200: + case 96000: + return 20; + case 128000: + case 176400: + case 192000: + return 12; + default: + return 0; + } +} + +uint32_t RMEFireface800Protocol::DecodeSytInterval(uint32_t rate) noexcept { + switch (rate) { + case 32000: + case 44100: + case 48000: + return 8U; + case 64000: + case 88200: + case 96000: + return 16U; + case 128000: + case 176400: + case 192000: + return 32U; + default: + return 0U; + } +} + +uint32_t RMEFireface800Protocol::CalculatePlaybackBandwidthUnits(uint32_t rate) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + const uint32_t sytInterval = DecodeSytInterval(rate); + if (dataBlockQuadlets == 0U || sytInterval == 0U) { + return 0U; + } + + // FF800 uses blocking raw PCM with no CIP header. The maximum payload is + // SYT interval * data-block quadlets * 4 bytes. Match the project's + // conservative Linux iso-resources fallback: 12-byte packet overhead, + // S800 scaling, then 512 allocation units of gap-count fallback margin. + const uint32_t maxPayloadBytes = sytInterval * dataBlockQuadlets * 4U; + const uint32_t alignedPayloadBytes = (maxPayloadBytes + 3U) & ~3U; + const uint32_t packetBytesAtS800 = (12U + alignedPayloadBytes + 1U) / 2U; + return packetBytesAtS800 + 512U; +} + +uint8_t RMEFireface800Protocol::SelectAvailableChannel(uint32_t channels31_0, + uint32_t channels63_32) noexcept { + for (uint8_t channel = 0; channel < 64U; ++channel) { + const uint32_t reg = channel < 32U ? channels31_0 : channels63_32; + const uint32_t mask = uint32_t{1} << (31U - (channel % 32U)); + if ((reg & mask) != 0U) { + return channel; + } + } + return 0xFFU; +} + +} // namespace ASFW::Audio::RME diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp new file mode 100644 index 00000000..c3ef2e3e --- /dev/null +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 ASFireWire Project +// +// RMEFireface800Protocol.hpp - Stage 5H bounded circular silent playback integration. + +#pragma once + +#include "../IDeviceProtocol.hpp" +#include "../../../Protocols/Ports/FireWireBusPort.hpp" + +#include +#include + +namespace ASFW::IRM { +class IRMClient; +} + +namespace ASFW::Audio::RME { + +class RMEFireface800Protocol final : public IDeviceProtocol { +public: + RMEFireface800Protocol(Protocols::Ports::FireWireBusOps& busOps, + Protocols::Ports::FireWireBusInfo& busInfo, + uint16_t nodeId, + uint64_t deviceGuid, + ::ASFW::IRM::IRMClient* irmClient) noexcept; + ~RMEFireface800Protocol() override; + + IOReturn Initialize() override; + IOReturn Shutdown() override; + const char* GetName() const override { return "RME Fireface 800 (Stage 5H bounded circular silent playback integration)"; } + + void UpdateRuntimeContext(uint16_t nodeId, + Protocols::AVC::FCPTransport* transport) override; + bool GetPlaybackPreflightRoute(PlaybackPreflightRoute& outRoute) const override; + IOReturn RunBoundedPlaybackIntegrationPreflight( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep runHostBurst, + BoundedPlaybackStep cleanupHost) override; + +private: + void ProbeClockConfig() noexcept; + void ValidateStfWritePath(uint32_t rate) noexcept; + void VerifyClockAfterStfWrite(uint32_t expectedRate) noexcept; + void ProbeSyncStatus() noexcept; + void ProbeTxIsochChannel() noexcept; + void ReservePlaybackResourcesPreflight(uint32_t rate) noexcept; + void ProgramRxPacketFormat(uint32_t rate, uint8_t channel, uint32_t bandwidthUnits) noexcept; + void RequestDeviceTxAllocation(uint32_t rate) noexcept; + void PollDeviceTxIsochChannel(uint32_t rate, uint32_t attempt) noexcept; + void StartDeviceCommunicationPreflight(uint32_t rate) noexcept; + void StopDeviceCommunicationPreflight() noexcept; + void RunNoCipWireFormatSelfTest() noexcept; + void BestEffortStopDeviceCommunication() noexcept; + void ReleaseReservedResources() noexcept; + void LogStreamCaps(uint32_t rate) const noexcept; + + static uint32_t ReadLE32(const uint8_t* bytes) noexcept; + static uint32_t DecodeSampleRate(uint32_t value) noexcept; + static const char* DecodeClockSource(uint32_t value) noexcept; + static const char* DecodeExternalState(uint32_t value, + uint32_t lockedMask, + uint32_t syncedMask) noexcept; + static const char* DecodeReferredClockSource(uint32_t status0, + uint32_t status1) noexcept; + static uint32_t DecodeReferredRate(uint32_t status0) noexcept; + static const char* DecodeStreamMode(uint32_t rate) noexcept; + static uint32_t DecodeChannelCount(uint32_t rate) noexcept; + static uint32_t DecodeSytInterval(uint32_t rate) noexcept; + static uint32_t CalculatePlaybackBandwidthUnits(uint32_t rate) noexcept; + static uint8_t SelectAvailableChannel(uint32_t channels31_0, + uint32_t channels63_32) noexcept; + + Protocols::Ports::FireWireBusOps& busOps_; + Protocols::Ports::FireWireBusInfo& busInfo_; + uint16_t nodeId_{0}; + uint64_t deviceGuid_{0}; + ::ASFW::IRM::IRMClient* irmClient_{nullptr}; + Async::AsyncHandle probeHandle_{}; + std::atomic active_{false}; + std::atomic playbackPreflightRoute_{0U}; + uint32_t currentRate_{0}; + uint8_t reservedPlaybackChannel_{0xFF}; + uint32_t reservedPlaybackBandwidthUnits_{0}; + bool playbackResourcesReserved_{false}; + bool deviceTxAllocationRequested_{false}; + bool deviceTxAllocated_{false}; + uint8_t deviceTxChannel_{0xFF}; + std::atomic stage5hInFlight_{false}; +}; + +} // namespace ASFW::Audio::RME diff --git a/ASFWDriver/Isoch/IsochService.cpp b/ASFWDriver/Isoch/IsochService.cpp index 804d8cb0..11e584f9 100644 --- a/ASFWDriver/Isoch/IsochService.cpp +++ b/ASFWDriver/Isoch/IsochService.cpp @@ -403,6 +403,109 @@ kern_return_t IsochService::PrepareTransmitStream(uint32_t streamIndex, uint8_t return kIOReturnSuccess; } +kern_return_t IsochService::PrimePreparedTransmitForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5E priming prepared IT before inert CommandPtr and all-skip RUN tests"); + return isochTransmitContext_->PrimeForPreflight(); +} + +kern_return_t IsochService::ProgramPreparedTransmitCommandPtrForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5E programming inert IT CommandPtr with RUN clear"); + return isochTransmitContext_->ProgramCommandPtrForPreflight(); +} + +kern_return_t IsochService::RunPreparedTransmitAllSkipForPreflight( + uint32_t durationMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5E running prepared IT with 48 skip-only descriptors for %u ms", + durationMs); + return isochTransmitContext_->RunAllSkipForPreflight(durationMs); +} + +kern_return_t IsochService::RunPreparedTransmitSingleSilencePacketForPreflight( + uint32_t timeoutMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5F running one finite silent no-CIP packet; completion timeout=%u ms", + timeoutMs); + return isochTransmitContext_->RunSingleSilencePacketForPreflight(timeoutMs); +} + +kern_return_t IsochService::PrepareTransmitFiniteSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5G preparing finite 48-cycle D-D-D-S silent cadence with RUN clear"); + return isochTransmitContext_->PrepareFiniteSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::RunPreparedTransmitFiniteSilenceCadenceForPreflight( + uint32_t timeoutMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5G executing prepared finite silent cadence; completion timeout=%u ms", + timeoutMs); + return isochTransmitContext_->RunPreparedFiniteSilenceCadenceForPreflight( + timeoutMs); +} + +kern_return_t +IsochService::CleanupPreparedTransmitFiniteSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnSuccess; + } + return isochTransmitContext_->CleanupFiniteSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::PrepareTransmitBoundedCircularSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5H preparing immutable circular 48-cycle D-D-D-S silence ring with RUN clear"); + return isochTransmitContext_ + ->PrepareBoundedCircularSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::RunPreparedTransmitBoundedCircularSilenceCadenceForPreflight( + uint32_t durationMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5H executing bounded circular silence ring durationMs=%u interrupts=0 refill=0", + durationMs); + return isochTransmitContext_ + ->RunPreparedBoundedCircularSilenceCadenceForPreflight(durationMs); +} + +kern_return_t +IsochService::CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnSuccess; + } + return isochTransmitContext_ + ->CleanupBoundedCircularSilenceCadenceForPreflight(); +} + kern_return_t IsochService::StartPreparedTransmit() { if (!isochTransmitContext_) { return kIOReturnNotReady; diff --git a/ASFWDriver/Isoch/IsochService.hpp b/ASFWDriver/Isoch/IsochService.hpp index cd7e823c..e394dc7f 100644 --- a/ASFWDriver/Isoch/IsochService.hpp +++ b/ASFWDriver/Isoch/IsochService.hpp @@ -89,6 +89,18 @@ class IsochService { kern_return_t PrepareTransmitStream(uint32_t streamIndex, uint8_t channel, HardwareInterface& hardware, uint8_t sid); kern_return_t StartPreparedTransmit(); + kern_return_t PrimePreparedTransmitForPreflight(); + kern_return_t ProgramPreparedTransmitCommandPtrForPreflight(); + kern_return_t RunPreparedTransmitAllSkipForPreflight(uint32_t durationMs); + kern_return_t RunPreparedTransmitSingleSilencePacketForPreflight(uint32_t timeoutMs); + kern_return_t PrepareTransmitFiniteSilenceCadenceForPreflight(); + kern_return_t RunPreparedTransmitFiniteSilenceCadenceForPreflight( + uint32_t timeoutMs); + kern_return_t CleanupPreparedTransmitFiniteSilenceCadenceForPreflight(); + kern_return_t PrepareTransmitBoundedCircularSilenceCadenceForPreflight(); + kern_return_t RunPreparedTransmitBoundedCircularSilenceCadenceForPreflight( + uint32_t durationMs); + kern_return_t CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight(); kern_return_t StopTransmit(); diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp index 78db2f9c..89480886 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp @@ -10,6 +10,7 @@ #include "../../Logging/LogConfig.hpp" #include "../../Common/TimingUtils.hpp" +#include #include #include @@ -58,6 +59,10 @@ kern_return_t IsochTransmitContext::Configure(uint8_t channel, uint8_t sid) noex channel_ = channel; ring_.SetChannel(channel_); + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; if (dmaMemory_) { // Allocate-once policy @@ -98,6 +103,10 @@ kern_return_t IsochTransmitContext::SetSharedMemoryDescriptors( payloadDmaCmd_ = nullptr; } payloadDmaMap_.Reset(); + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; // 1. Map Payload Slab IOMemoryMap* pMap = nullptr; @@ -249,6 +258,1050 @@ kern_return_t IsochTransmitContext::SetSharedMemoryDescriptors( return kIOReturnSuccess; } +kern_return_t IsochTransmitContext::PrimeForPreflight() noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, "IT: Stage 5E descriptor preflight missing hardware/ring"); + return kIOReturnNoResources; + } + if (!payloadBase_ || !payloadDmaMap_.IsValid() || !metadataRing_ || !controlBlock_ || + controlBlock_->numSlots == 0 || controlBlock_->slotStrideBytes == 0 || + controlBlock_->maxPacketBytes == 0) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight shared TX contract incomplete"); + return kIOReturnNotReady; + } + + ring_.ResetForStart(); + ring_.SeedCycleTracking(*hardware_); + + const uint64_t exposeCursor = + controlBlock_->exposeCursor.load(std::memory_order_acquire); + const auto primeStats = ring_.Prime(payloadDmaMap_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + exposeCursor); + if (primeStats.packetsAssembled != Tx::Layout::kNumPackets) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight prime failed assembled=%llu expected=%u expose=%llu", + primeStats.packetsAssembled, + Tx::Layout::kNumPackets, + exposeCursor); + return kIOReturnInternalError; + } + + constexpr uint32_t kExpectedDataPackets = 36U; + constexpr uint32_t kExpectedSkipPackets = 12U; + constexpr uint32_t kExpectedDataBytes = 1536U; + + uint32_t dataPackets = 0; + uint32_t skipPackets = 0; + bool metadataValid = true; + bool payloadShapeValid = true; + for (uint32_t packet = 0; packet < Tx::Layout::kNumPackets; ++packet) { + const uint32_t slot = packet % controlBlock_->numSlots; + const auto& meta = metadataRing_[slot]; + const uint32_t q0 = OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(q0); + const bool skip = ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength); + if (skip) { + ++skipPackets; + payloadShapeValid = payloadShapeValid && meta.payloadLength == 0U; + } else { + ++dataPackets; + payloadShapeValid = payloadShapeValid && + meta.payloadLength == kExpectedDataBytes; + } + if (tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(q0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(q0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength( + OSSwapLittleToHostInt32(meta.immediateHeader[1])) != + meta.payloadLength) { + metadataValid = false; + break; + } + } + + const bool cadenceValid = + dataPackets == kExpectedDataPackets && + skipPackets == kExpectedSkipPackets; + const uint64_t descriptorIOVA = ring_.Slab().DescriptorRegion().deviceBase; + if (!metadataValid || !payloadShapeValid || !cadenceValid || + descriptorIOVA == 0 || descriptorIOVA > 0xFFFFFFFFULL) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight validation failed metadata=%u payloadShape=%u cadence=%u data=%u/%u skip=%u/%u descriptorIOVA=0x%llx", + metadataValid ? 1U : 0U, + payloadShapeValid ? 1U : 0U, + cadenceValid ? 1U : 0U, + dataPackets, + kExpectedDataPackets, + skipPackets, + kExpectedSkipPackets, + descriptorIOVA); + return kIOReturnInternalError; + } + + packetsAssembled_ = primeStats.packetsAssembled; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E FF800 descriptor ring primed packets=%u data=%u skip=%u descriptorIOVA=0x%08x expose=%llu noCommandPtr=1 noRun=1", + Tx::Layout::kNumPackets, + dataPackets, + skipPackets, + static_cast(descriptorIOVA), + exposeCursor); + ring_.DumpDescriptorRing(0, 8); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::ProgramCommandPtrForPreflight() noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5E CommandPtr preflight rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, "IT: Stage 5E CommandPtr preflight missing hardware/ring"); + return kIOReturnNoResources; + } + + const uint64_t descriptorIOVA = ring_.Slab().DescriptorRegion().deviceBase; + if (descriptorIOVA == 0 || descriptorIOVA > 0xFFFFFFFFULL) { + ASFW_LOG(Isoch, + "IT: Stage 5E invalid descriptor IOVA 0x%llx", + descriptorIOVA); + return kIOReturnInternalError; + } + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + // Make the safety invariant explicit before touching CommandPtr. No IT + // interrupt is enabled and RUN is forced clear; a CommandPtr by itself is + // inert and cannot launch DMA. + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + hardware_->Write(Register32::kIsoXmitIntMaskClear, (1u << contextIndex_)); + + const uint32_t expectedCmd = + static_cast(descriptorIOVA) | Tx::Layout::kBlocksPerPacket; + hardware_->Write(cmdPtrReg, expectedCmd); + + // The readbacks also flush posted MMIO writes. + const uint32_t readCmd = hardware_->Read(cmdPtrReg); + const uint32_t readCtl = hardware_->Read(ctrlReg); + const bool commandMatches = readCmd == expectedCmd; + const bool runClear = (readCtl & Driver::ContextControl::kRun) == 0; + const bool activeClear = (readCtl & Driver::ContextControl::kActive) == 0; + const bool deadClear = (readCtl & Driver::ContextControl::kDead) == 0; + + if (!commandMatches || !runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5E CommandPtr validation failed expected=0x%08x actual=0x%08x control=0x%08x runClear=%u activeClear=%u deadClear=%u", + expectedCmd, + readCmd, + readCtl, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E inert CommandPtr programmed expected=0x%08x actual=0x%08x descriptorIOVA=0x%08x Z=%u control=0x%08x noRun=1 noInterrupt=1 noPacket=1", + expectedCmd, + readCmd, + static_cast(descriptorIOVA), + Tx::Layout::kBlocksPerPacket, + readCtl); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::RunAllSkipForPreflight( + uint32_t durationMs) noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion missing hardware/ring"); + return kIOReturnNoResources; + } + if (durationMs == 0U || durationMs > 20U) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion invalid timeout=%u ms", + durationMs); + return kIOReturnBadArgument; + } + if (!ring_.ProgramAllSkipPacketsForRunPreflight()) { + ASFW_LOG(Isoch, + "IT: Stage 5E could not construct/publish finite 48-skip descriptor chain"); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E finite all-skip descriptor chain published descriptors=48 terminalBranch=0 bytes=%zu dmaPublish=1", + ring_.Slab().DescriptorRegion().size); + + const uint64_t descriptorIOVA = ring_.Slab().DescriptorRegion().deviceBase; + if (descriptorIOVA == 0U || descriptorIOVA > 0xFFFFFFFFULL) { + ASFW_LOG(Isoch, + "IT: Stage 5E invalid descriptor IOVA 0x%llx", + descriptorIOVA); + return kIOReturnInternalError; + } + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + // The finite skip program begins with one descriptor, not the four + // physical slots reserved for a normal FF800 packet program. + const uint32_t expectedCmd = + static_cast(descriptorIOVA) | 1U; + hardware_->Write(cmdPtrReg, expectedCmd); + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != expectedCmd || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + expectedCmd, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + const uint32_t maxPolls = durationMs * 100U; // 10 us per poll. + bool activeObserved = false; + uint32_t completionPolls = 0U; + uint32_t controlAfterRun = hardware_->Read(ctrlReg); + auto completion = ring_.FetchAllSkipCompletionForRunPreflight(); + for (; + completionPolls < maxPolls && + completion.completedDescriptors < Tx::Layout::kNumPackets && + (controlAfterRun & Driver::ContextControl::kDead) == 0U; + ++completionPolls) { + activeObserved = activeObserved || + ((controlAfterRun & Driver::ContextControl::kActive) != 0U); + IODelay(10U); + controlAfterRun = hardware_->Read(ctrlReg); + completion = ring_.FetchAllSkipCompletionForRunPreflight(); + } + activeObserved = activeObserved || + ((controlAfterRun & Driver::ContextControl::kActive) != 0U); + + const bool deadSet = + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + const bool completionProved = + completion.completedDescriptors == Tx::Layout::kNumPackets && + completion.lastTransferStatus != 0U; + if (deadSet || !completionProved) { + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + (void)hardware_->Read(ctrlReg); + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion failed control=0x%08x dead=%u completed=%u/48 lastStatus=0x%04x lastTimestamp=0x%04x activeObserved=%u polls=%u", + controlAfterRun, + deadSet ? 1U : 0U, + completion.completedDescriptors, + completion.lastTransferStatus, + completion.lastTimestamp, + activeObserved ? 1U : 0U, + completionPolls); + return deadSet ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E finite all-skip DMA completion proved cmd=0x%08x control=0x%08x completed=48/48 lastStatus=0x%04x lastTimestamp=0x%04x activeObserved=%u dormantAccepted=%u polls=%u noInterrupt=1 noPacketDescriptor=1", + commandBeforeRun, + controlAfterRun, + completion.lastTransferStatus, + completion.lastTimestamp, + activeObserved ? 1U : 0U, + (controlAfterRun & Driver::ContextControl::kActive) == 0U ? 1U : 0U, + completionPolls); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0; + poll < 1000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnTimeout; + } + + state_ = State::Stopped; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E finite all-skip context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1 completed=48 busPacketDescriptors=0", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::RunSingleSilencePacketForPreflight( + const uint32_t timeoutMs) noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings() || !payloadBase_ || + !metadataRing_ || !controlBlock_) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet missing hardware/ring/shared memory"); + return kIOReturnNoResources; + } + if (timeoutMs == 0U || timeoutMs > 20U) { + return kIOReturnBadArgument; + } + + Tx::IsochTxDmaRing::SingleSilencePacketProgram program{}; + if (!ring_.ProgramSingleSilencePacketForRunPreflight( + payloadBase_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + program)) { + ASFW_LOG(Isoch, + "IT: Stage 5F could not construct verified finite silent packet program"); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5F finite silent packet published packet=%u producerSlot=%u cmd=0x%08x channel=%u payloadBytes=%u tag=0 noCIP=1 allZero=1 terminalBranch=0 dmaPublish=1", + program.packetSlot, + program.producerSlot, + program.commandPtr, + channel_, + program.payloadLength); + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + hardware_->Write(cmdPtrReg, program.commandPtr); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + const uint32_t maxPolls = timeoutMs * 100U; // 10 us per poll. + uint32_t polls = 0U; + uint32_t controlAfterRun = hardware_->Read(ctrlReg); + auto completion = + ring_.FetchSingleSilencePacketCompletionForRunPreflight( + program.packetSlot); + while (polls < maxPolls && completion.transferStatus == 0U && + (controlAfterRun & Driver::ContextControl::kDead) == 0U) { + IODelay(10U); + ++polls; + controlAfterRun = hardware_->Read(ctrlReg); + completion = + ring_.FetchSingleSilencePacketCompletionForRunPreflight( + program.packetSlot); + } + + const bool dead = + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + const uint8_t eventCode = + static_cast(completion.transferStatus & 0x1FU); + const bool ackComplete = eventCode == 0x11U; + if (dead || completion.transferStatus == 0U || !ackComplete) { + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + (void)hardware_->Read(ctrlReg); + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet completion failed control=0x%08x dead=%u xferStatus=0x%04x event=0x%02x timestamp=0x%04x polls=%u", + controlAfterRun, + dead ? 1U : 0U, + completion.transferStatus, + eventCode, + completion.timestamp, + polls); + return dead ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5F finite silent packet DMA/transmit completed cmd=0x%08x control=0x%08x xferStatus=0x%04x event=ack_complete timestamp=0x%04x packet=%u channel=%u payloadBytes=%u actualBusPackets=1 payload=silence deviceEngineStopped=1 interrupts=0", + commandBeforeRun, + controlAfterRun, + completion.transferStatus, + completion.timestamp, + program.packetSlot, + channel_, + program.payloadLength); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0U; + poll < 1000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnTimeout; + } + + state_ = State::Stopped; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5F finite silent packet context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1 actualBusPackets=1", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::PrepareFiniteSilenceCadenceForPreflight() + noexcept { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence preparation rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence preparation missing hardware/ring"); + return kIOReturnNoResources; + } + if (!payloadBase_ || !metadataRing_ || !controlBlock_ || + controlBlock_->numSlots == 0U || + controlBlock_->slotStrideBytes == 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence preparation shared TX contract incomplete"); + return kIOReturnNotReady; + } + + Tx::IsochTxDmaRing::FiniteSilenceCadenceProgram program{}; + if (!ring_.ProgramFiniteSilenceCadenceForRunPreflight( + payloadBase_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + program)) { + ASFW_LOG(Isoch, + "IT: Stage 5G could not construct verified finite D-D-D-S silence cadence"); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G descriptor chain published descriptors=%u dataPackets=%u skip=%u payloadBytes=%u pattern=DDD-S terminalBranch=0 dmaPublish=1", + program.descriptorCount, + program.dataPacketCount, + program.skipPacketCount, + program.payloadLength); + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + hardware_->Write(cmdPtrReg, program.commandPtr); + + const uint32_t commandReadback = hardware_->Read(cmdPtrReg); + const uint32_t controlReadback = hardware_->Read(ctrlReg); + const bool runClear = + (controlReadback & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlReadback & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlReadback & Driver::ContextControl::kDead) == 0U; + if (commandReadback != program.commandPtr || !runClear || + !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x runClear=%u activeClear=%u deadClear=%u", + program.commandPtr, + commandReadback, + controlReadback, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnInternalError; + } + + finiteCadenceProgram_ = program; + finiteCadencePrepared_ = true; + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::RunPreparedFiniteSilenceCadenceForPreflight( + const uint32_t timeoutMs) noexcept { + if (!finiteCadencePrepared_ || + (state_ != State::Configured && state_ != State::Stopped)) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence run rejected prepared=%u state=%{public}s", + finiteCadencePrepared_ ? 1U : 0U, + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || timeoutMs == 0U || timeoutMs > 50U) { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + return hardware_ ? kIOReturnBadArgument : kIOReturnNoResources; + } + + const auto program = finiteCadenceProgram_; + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence RUN gate failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + const uint32_t maxPolls = timeoutMs * 100U; // 10 us per poll. + uint32_t polls = 0U; + bool deadObserved = false; + bool activeObserved = false; + uint32_t controlAfterRun = hardware_->Read(ctrlReg); + auto completion = + ring_.FetchFiniteSilenceCadenceCompletionForRunPreflight( + program.startPacketSlot); + while (polls < maxPolls && + completion.completedDescriptors < program.descriptorCount) { + deadObserved = deadObserved || + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + activeObserved = activeObserved || + (controlAfterRun & Driver::ContextControl::kActive) != 0U; + if (deadObserved) { + break; + } + IODelay(10U); + ++polls; + controlAfterRun = hardware_->Read(ctrlReg); + completion = + ring_.FetchFiniteSilenceCadenceCompletionForRunPreflight( + program.startPacketSlot); + } + deadObserved = deadObserved || + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + activeObserved = activeObserved || + (controlAfterRun & Driver::ContextControl::kActive) != 0U; + + const bool completionValid = + !deadObserved && + completion.completedDescriptors == program.descriptorCount && + completion.completedDataDescriptors == program.dataPacketCount && + completion.actualBusPackets == program.dataPacketCount && + completion.eventErrors == 0U && + completion.cadenceValid; + + if (completionValid) { + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G finite cadence burst completed descriptors=%u/%u dataCompleted=%u/%u actualBusPackets=%u eventErrors=0 deadObserved=0 activeObserved=%u polls=%u", + completion.completedDescriptors, + program.descriptorCount, + completion.completedDataDescriptors, + program.dataPacketCount, + completion.actualBusPackets, + activeObserved ? 1U : 0U, + polls); + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G cadence verification passed timestampPattern=1,1,2"); + } else { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence burst failed control=0x%08x descriptors=%u/%u dataCompleted=%u/%u actualBusPackets=%u eventErrors=%u deadObserved=%u cadenceValid=%u lastStatus=0x%04x lastTimestamp=0x%04x polls=%u", + controlAfterRun, + completion.completedDescriptors, + program.descriptorCount, + completion.completedDataDescriptors, + program.dataPacketCount, + completion.actualBusPackets, + completion.eventErrors, + deadObserved ? 1U : 0U, + completion.cadenceValid ? 1U : 0U, + completion.lastTransferStatus, + completion.lastTimestamp, + polls); + } + + const kern_return_t cleanupKr = + CleanupFiniteSilenceCadenceForPreflight(); + if (cleanupKr != kIOReturnSuccess) { + return cleanupKr; + } + if (!completionValid) { + return deadObserved ? kIOReturnNotPermitted : kIOReturnTimeout; + } + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::CleanupFiniteSilenceCadenceForPreflight() noexcept { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + + if (!hardware_) { + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + return kIOReturnNoResources; + } + + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0U; + poll < 1000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + refillInProgress_.clear(std::memory_order_release); + + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return deadClear ? kIOReturnTimeout : kIOReturnNotPermitted; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G transmit context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::PrepareBoundedCircularSilenceCadenceForPreflight() + noexcept { + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence preparation rejected state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence preparation missing hardware/ring"); + return kIOReturnNoResources; + } + if (!payloadBase_ || !metadataRing_ || !controlBlock_ || + controlBlock_->numSlots == 0U || + controlBlock_->slotStrideBytes == 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence shared TX contract incomplete"); + return kIOReturnNotReady; + } + + Tx::IsochTxDmaRing::BoundedCircularSilenceCadenceProgram program{}; + if (!ring_.ProgramBoundedCircularSilenceCadenceForPreflight( + payloadBase_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + program)) { + ASFW_LOG(Isoch, + "IT: Stage 5H could not construct verified circular D-D-D-S silence cadence"); + return kIOReturnInternalError; + } + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + hardware_->Write(cmdPtrReg, program.commandPtr); + + const uint32_t commandReadback = hardware_->Read(cmdPtrReg); + const uint32_t controlReadback = hardware_->Read(ctrlReg); + const bool runClear = + (controlReadback & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlReadback & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlReadback & Driver::ContextControl::kDead) == 0U; + if (commandReadback != program.commandPtr || !runClear || + !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x runClear=%u activeClear=%u deadClear=%u", + program.commandPtr, + commandReadback, + controlReadback, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnInternalError; + } + + boundedCircularCadenceProgram_ = program; + boundedCircularCadencePrepared_ = true; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H circular cadence armed with RUN clear cmd=0x%08x startSlot=%u durationPending=1", + program.commandPtr, + program.startPacketSlot); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::RunPreparedBoundedCircularSilenceCadenceForPreflight( + const uint32_t durationMs) noexcept { + if (!boundedCircularCadencePrepared_ || + (state_ != State::Configured && state_ != State::Stopped)) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence run rejected prepared=%u state=%{public}s", + boundedCircularCadencePrepared_ ? 1U : 0U, + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || durationMs < 50U || durationMs > 250U) { + return hardware_ ? kIOReturnBadArgument : kIOReturnNoResources; + } + + const auto program = boundedCircularCadenceProgram_; + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence RUN gate failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + constexpr uint32_t kPollDelayUs = 250U; + const uint32_t maxPolls = (durationMs * 1000U) / kPollDelayUs; + uint32_t polls = 0U; + bool deadObserved = false; + bool activeObserved = false; + uint32_t consecutiveInactiveAfterActive = 0U; + uint32_t maxInactiveAfterActive = 0U; + uint32_t anchorExecutions = 0U; + uint32_t anchorEventErrors = 0U; + uint16_t previousAnchorTimestamp = 0U; + bool anchorSeen = false; + + for (; polls < maxPolls; ++polls) { + const uint32_t control = hardware_->Read(ctrlReg); + const bool active = + (control & Driver::ContextControl::kActive) != 0U; + const bool dead = + (control & Driver::ContextControl::kDead) != 0U; + deadObserved = deadObserved || dead; + if (active) { + activeObserved = true; + consecutiveInactiveAfterActive = 0U; + } else if (activeObserved) { + ++consecutiveInactiveAfterActive; + maxInactiveAfterActive = std::max( + maxInactiveAfterActive, consecutiveInactiveAfterActive); + } + + const auto anchor = + ring_.FetchCadenceAnchorCompletionForPreflight( + program.startPacketSlot); + if (anchor.transferStatus != 0U) { + const uint8_t eventCode = static_cast( + anchor.transferStatus & 0x1FU); + if (eventCode != 0x11U) { + ++anchorEventErrors; + } else if (!anchorSeen) { + anchorSeen = true; + previousAnchorTimestamp = anchor.timestamp; + anchorExecutions = 1U; + } else if (anchor.timestamp != previousAnchorTimestamp) { + previousAnchorTimestamp = anchor.timestamp; + ++anchorExecutions; + } + } + + if (dead) { + break; + } + IODelay(kPollDelayUs); + } + + // Stop the immutable ring synchronously before the protocol sends the + // FF800 stop command. The cleanup callback remains idempotent and will + // repeat this gate on every partial-failure path. + const kern_return_t stopStatus = + CleanupBoundedCircularSilenceCadenceForPreflight(); + const auto completion = + ring_.FetchFiniteSilenceCadenceCompletionForRunPreflight( + program.startPacketSlot); + + const uint32_t completedSweeps = + anchorExecutions > 0U ? anchorExecutions - 1U : 0U; + const uint32_t minimumBusPackets = + completedSweeps * program.dataPacketCount; + const uint32_t nominalSweeps = durationMs / 6U; + const uint32_t minimumRequiredSweeps = + std::max(4U, nominalSweeps / 2U); + + // Tolerate at most three consecutive inactive samples (750 us) to avoid + // treating a single asynchronous register read as a lost circular run. + const bool activeContinuous = maxInactiveAfterActive < 4U; + const bool repeatedRunValid = + stopStatus == kIOReturnSuccess && + !deadObserved && + activeObserved && + activeContinuous && + anchorEventErrors == 0U && + completedSweeps >= minimumRequiredSweeps && + completion.completedDescriptors == program.descriptorCount && + completion.completedDataDescriptors == program.dataPacketCount && + completion.actualBusPackets == program.dataPacketCount && + completion.eventErrors == 0U; + + if (!repeatedRunValid) { + ASFW_LOG(Isoch, + "IT: Stage 5H bounded circular run failed durationMs=%u controlPolls=%u activeObserved=%u maxInactivePolls=%u deadObserved=%u anchorExecutions=%u completedSweeps=%u/%u minimumBusPackets=%u anchorEventErrors=%u descriptors=%u/%u dataCompleted=%u/%u eventErrors=%u stopKr=0x%x", + durationMs, + polls, + activeObserved ? 1U : 0U, + maxInactiveAfterActive, + deadObserved ? 1U : 0U, + anchorExecutions, + completedSweeps, + minimumRequiredSweeps, + minimumBusPackets, + anchorEventErrors, + completion.completedDescriptors, + program.descriptorCount, + completion.completedDataDescriptors, + program.dataPacketCount, + completion.eventErrors, + stopStatus); + if (stopStatus != kIOReturnSuccess) { + return stopStatus; + } + return deadObserved ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H bounded circular silence sustained durationMs=%u anchorExecutions=%u completedSweeps=%u minimumBusPackets=%u activeContinuous=1 eventErrors=0 deadObserved=0 interrupts=0 refill=0", + durationMs, + anchorExecutions, + completedSweeps, + minimumBusPackets); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::CleanupBoundedCircularSilenceCadenceForPreflight() + noexcept { + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; + + if (!hardware_) { + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + return kIOReturnNoResources; + } + + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0U; + poll < 2000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + refillInProgress_.clear(std::memory_order_release); + + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular context stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return deadClear ? kIOReturnTimeout : kIOReturnNotPermitted; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H circular transmit context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1", + controlStopped); + return kIOReturnSuccess; +} + kern_return_t IsochTransmitContext::Start() noexcept { if (state_ != State::Configured && state_ != State::Stopped) { ASFW_LOG(Isoch, "IT: Start rejected - state=%{public}s", TxStateName(state_)); @@ -348,6 +1401,10 @@ kern_return_t IsochTransmitContext::Start() noexcept { } void IsochTransmitContext::Stop() noexcept { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; if (state_ == State::Running && hardware_) { Register32 ctrlClrReg = static_cast(DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp index 52b0ff08..74e18185 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp @@ -87,6 +87,60 @@ class IsochTransmitContext final { uint32_t ztsPeriodFrames) noexcept; kern_return_t Start() noexcept; + + // Stage 5E descriptor safety seam: build and validate the complete OHCI IT descriptor + // program against the already-prefilled shared packet ring, but do not + // write CommandPtr, set RUN, enable interrupts, or transmit a bus packet. + kern_return_t PrimeForPreflight() noexcept; + + // Stage 5E safety seam: program and read back the inert OHCI CommandPtr for + // the already-validated descriptor ring. RUN and interrupts remain clear, + // so the controller cannot fetch or transmit a packet. + kern_return_t ProgramCommandPtrForPreflight() noexcept; + + // Stage 5E: replace the validated packet ring with a finite chain of 48 + // true skip descriptors, start the OHCI IT context with interrupts masked, + // and prove execution from descriptor completion status. ACTIVE is sampled + // only for diagnostics because a completed finite program normally becomes + // dormant with RUN=1, ACTIVE=0 and DEAD=0. No bus packet descriptor exists. + kern_return_t RunAllSkipForPreflight(uint32_t durationMs) noexcept; + + // Stage 5F: reduce the already-validated FF800 ring to one finite, silent, + // no-CIP packet, run it with interrupts masked, require ack_complete, and + // stop the context. This emits exactly one isochronous packet on the + // protocol-held IRM channel; the FF800 communication engine remains stopped. + kern_return_t RunSingleSilencePacketForPreflight(uint32_t timeoutMs) noexcept; + + // Stage 5G: prepare one finite 48-cycle D-D-D-S silent cadence while RUN + // is clear, publish all payload/descriptor DMA state, and program/read back + // CommandPtr. The caller starts the FF800 engine only after this succeeds. + kern_return_t PrepareFiniteSilenceCadenceForPreflight() noexcept; + + // Stage 5G: execute the previously prepared finite cadence once with + // interrupts masked, validate descriptor writeback and 1,1,2 cycle timing, + // then stop the OHCI context cleanly. No descriptor refill or live stream. + kern_return_t RunPreparedFiniteSilenceCadenceForPreflight( + uint32_t timeoutMs) noexcept; + + // Stage 5G unconditional teardown seam. Clear RUN even when preparation or + // device start failed before the normal RUN path, wait boundedly for ACTIVE + // to drain, verify DEAD is clear, and discard all finite-cadence state. + kern_return_t CleanupFiniteSilenceCadenceForPreflight() noexcept; + + // Stage 5H: prepare the same verified 48-cycle D-D-D-S silence cadence as + // a circular immutable ring. RUN, interrupts, and refill remain disabled + // until the FF800 engine has been started by the protocol layer. + kern_return_t PrepareBoundedCircularSilenceCadenceForPreflight() noexcept; + + // Stage 5H: run the immutable circular ring for a strictly bounded window, + // prove repeated wraps from anchor-descriptor timestamp changes, reject + // DEAD/ACTIVE loss, and stop synchronously. + kern_return_t RunPreparedBoundedCircularSilenceCadenceForPreflight( + uint32_t durationMs) noexcept; + + // Stage 5H unconditional teardown seam used on success and every partial + // failure before the protocol stops the device and releases IRM. + kern_return_t CleanupBoundedCircularSilenceCadenceForPreflight() noexcept; void Stop() noexcept; void Poll() noexcept; @@ -146,6 +200,14 @@ class IsochTransmitContext final { Tx::TxPayloadDmaMap payloadDmaMap_{}; OSSharedPtr payloadDmaCmd_{nullptr}; + + Tx::IsochTxDmaRing::FiniteSilenceCadenceProgram + finiteCadenceProgram_{}; + bool finiteCadencePrepared_{false}; + + Tx::IsochTxDmaRing::BoundedCircularSilenceCadenceProgram + boundedCircularCadenceProgram_{}; + bool boundedCircularCadencePrepared_{false}; }; } // namespace Isoch diff --git a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp index 6b0c52b7..21b6ec20 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp +++ b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp @@ -5,6 +5,7 @@ #include "../../Common/TimingUtils.hpp" #include +#include #include namespace ASFW::Isoch::Tx { @@ -28,6 +29,21 @@ namespace { (static_cast(channel & 0x3F) << 8); return OSSwapHostToLittleInt32(h); } + +// The generic AT helper intentionally rejects Z=1 because the normal ASFW +// packet program always occupies four descriptor blocks. Stage 5E proved that +// a true OHCI skip program is exactly one descriptor, so Stage 5G needs a +// narrowly-scoped encoder that permits only the two program sizes used by the +// finite cadence: Z=4 for data and Z=1 for skip. +[[nodiscard]] inline uint32_t MakeFiniteCadenceBranchWord( + const uint32_t descriptorIOVA, + const uint32_t zBlocks) noexcept { + if (descriptorIOVA == 0U || (descriptorIOVA & 0xFU) != 0U || + (zBlocks != 1U && zBlocks != Layout::kBlocksPerPacket)) { + return 0U; + } + return (descriptorIOVA & 0xFFFFFFF0U) | (zBlocks & 0xFU); +} } // namespace void IsochTxDmaRing::ResetForStart() noexcept { @@ -55,11 +71,16 @@ void IsochTxDmaRing::ResetForStart() noexcept { void IsochTxDmaRing::GaugeWirePayload(uint64_t fillAbsIdx, const uint8_t* packetBytes, - uint32_t payloadLength) noexcept { + uint32_t payloadLength, + uint32_t immediateHeaderQ0LE) noexcept { constexpr uint32_t kCipHeaderBytes = 8; constexpr uint32_t kIdleSlotWord = 0x80000000u; // AM824 no-info / idle MIDI - if (payloadLength <= kCipHeaderBytes) { - return; // NO-DATA packet: CIP header only + const uint32_t headerQ0 = OSSwapLittleToHostInt32(immediateHeaderQ0LE); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + const uint32_t streamHeaderBytes = + tag == ASFW::IsochTransport::IsochPacketTag::kCip ? kCipHeaderBytes : 0U; + if (payloadLength <= streamHeaderBytes) { + return; // CIP header-only or a true empty no-CIP cycle. } const uint64_t dataPackets = @@ -75,8 +96,8 @@ void IsochTxDmaRing::GaugeWirePayload(uint64_t fillAbsIdx, counters_.wireLastInfoQuad.load(std::memory_order_relaxed)); } - const uint32_t quadCount = (payloadLength - kCipHeaderBytes) / 4; - const uint8_t* quadBytes = packetBytes + kCipHeaderBytes; + const uint32_t quadCount = (payloadLength - streamHeaderBytes) / 4; + const uint8_t* quadBytes = packetBytes + streamHeaderBytes; uint32_t infoQuads = 0; uint32_t lastInfoQuad = 0; uint32_t maxAbs24 = 0; @@ -183,18 +204,26 @@ void IsochTxDmaRing::ResyncCycleTracking(Driver::HardwareInterface& hw, const uint32_t lastProcessedPkt = (hwPacketIndex + Layout::kNumPackets - 1) % Layout::kNumPackets; out.completedPacketIndex = lastProcessedPkt; out.completedPacketCount = deltaConsumed; - auto* processedOL = slab_.GetDescriptorPtr( - lastProcessedPkt * Layout::kBlocksPerPacket + - Layout::kCompletionBlock); - + auto* processedFirst = slab_.GetDescriptorPtr( + lastProcessedPkt * Layout::kBlocksPerPacket); if (dmaMemory_) { - dmaMemory_->FetchFromDevice(reinterpret_cast(processedOL), sizeof(*processedOL)); + dmaMemory_->FetchFromDevice( + reinterpret_cast(processedFirst), + sizeof(*processedFirst)); + } + auto* processedCompletion = + CompletionDescriptorForPacket(lastProcessedPkt); + if (dmaMemory_ && processedCompletion != processedFirst) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(processedCompletion), + sizeof(*processedCompletion)); } - const uint16_t hwTimestamp = static_cast(processedOL->statusWord & 0xFFFF); + const uint16_t hwTimestamp = static_cast( + processedCompletion->statusWord & 0xFFFFU); out.hwTimestamp = hwTimestamp; - if (processedOL->statusWord == 0) { + if (processedCompletion->statusWord == 0) { return; } @@ -218,6 +247,861 @@ void IsochTxDmaRing::CommitRefill(const uint32_t toFill) noexcept { counters_.packetsRefilled.fetch_add(toFill, std::memory_order_relaxed); } +void IsochTxDmaRing::ProgramSkipPacket(const uint32_t packetSlot) noexcept { + const uint32_t descBase = packetSlot * Layout::kBlocksPerPacket; + for (uint32_t block = 0; block < Layout::kBlocksPerPacket; ++block) { + auto* desc = slab_.GetDescriptorPtr(descBase + block); + std::memset(desc, 0, sizeof(OHCIDescriptor)); + } + + // A no-CIP idle cycle is a true OHCI skip: one zero-length standard + // OUTPUT_LAST descriptor and no immediate isoch header. Keep the fixed + // four-block slot geometry by branching directly to the next slot with + // Z=4; the remaining three descriptors in this slot stay zeroed. + auto* skip = slab_.GetDescriptorPtr(descBase); + const uint32_t nextPacketSlot = + (packetSlot + 1U) % Layout::kNumPackets; + ITDescriptorBuilder::BuildOutputLast( + *skip, + { + .dataIOVA = 0, + .payloadSize = 0, + .branchIOVA = slab_.GetDescriptorIOVA( + nextPacketSlot * Layout::kBlocksPerPacket), + .zValue = Layout::kBlocksPerPacket, + .interruptBits = + Core::IsTimingGroupBoundary(packetSlot) + ? OHCIDescriptor::kIntAlways + : OHCIDescriptor::kIntNever, + }); +} + +bool IsochTxDmaRing::ProgramSingleSilencePacketForRunPreflight( + uint8_t* payloadBase, + const uint32_t numSlots, + const uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + SingleSilencePacketProgram& outProgram) noexcept { + outProgram = {}; + if (!slab_.IsValid() || payloadBase == nullptr || metadataRing == nullptr || + numSlots == 0U || slotStrideBytes == 0U) { + return false; + } + + constexpr uint32_t kExpectedPayloadBytes = 1536U; + for (uint32_t packetSlot = 0U; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + const uint32_t producerSlot = packetSlot % numSlots; + const auto& meta = metadataRing[producerSlot]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength)) { + continue; + } + if (tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + meta.payloadLength != kExpectedPayloadBytes || + meta.payloadLength > slotStrideBytes) { + return false; + } + + const uint8_t* payload = payloadBase + + static_cast(producerSlot) * slotStrideBytes; + for (uint32_t byte = 0U; byte < meta.payloadLength; ++byte) { + if (payload[byte] != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5F single-packet safety check rejected nonzero payload packet=%u slot=%u byte=%u value=0x%02x", + packetSlot, + producerSlot, + byte, + payload[byte]); + return false; + } + } + + const uint32_t descBase = packetSlot * Layout::kBlocksPerPacket; + auto* immDesc = reinterpret_cast( + slab_.GetDescriptorPtr(descBase)); + auto* desc2 = slab_.GetDescriptorPtr( + descBase + Layout::kFirstPayloadBlock); + auto* completion = slab_.GetDescriptorPtr( + descBase + Layout::kCompletionBlock); + + const uint32_t wireQ0 = + OSSwapLittleToHostInt32(immDesc->immediateData[0]); + const uint32_t wireQ1 = + OSSwapLittleToHostInt32(immDesc->immediateData[1]); + const uint8_t wireChannel = + static_cast((wireQ0 >> 8U) & 0x3FU); + if (ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0) != + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1) != + meta.payloadLength || + wireChannel != channel_) { + ASFW_LOG(Isoch, + "IT: Stage 5F single-packet header validation failed packet=%u tag=%u speed=%u tcode=0x%x channel=%u/%u dataLength=%u/%u", + packetSlot, + static_cast( + ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0)), + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0), + wireChannel, + channel_, + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1), + meta.payloadLength); + return false; + } + + // Convert the normal circular packet program to a finite one-packet + // program. The immediate descriptor's self-link remains the OHCI skip + // address for a missed cycle; the OUTPUT_LAST branch is the queue tail. + const uint32_t intMask = 0x3U << + (OHCIDescriptor::kIntShift + OHCIDescriptor::kControlHighShift); + completion->control &= ~intMask; + completion->branchWord = 0U; + AR_init_status(*completion, 0U); + desc2->statusWord = 0U; + immDesc->common.statusWord = 0U; + + if (dmaMemory_) { + dmaMemory_->PublishToDevice( + reinterpret_cast(payload), + meta.payloadLength); + dmaMemory_->PublishBarrier(); + dmaMemory_->PublishToDevice( + reinterpret_cast(immDesc), + sizeof(OHCIDescriptorImmediate)); + dmaMemory_->PublishToDevice( + reinterpret_cast(desc2), + sizeof(OHCIDescriptor)); + dmaMemory_->PublishToDevice( + reinterpret_cast(completion), + sizeof(OHCIDescriptor)); + } else { + ASFW::Driver::WriteBarrier(); + } + + const uint64_t descriptorIOVA = + slab_.GetDescriptorIOVA(descBase); + if (descriptorIOVA == 0U || descriptorIOVA > 0xFFFFFFFFULL) { + return false; + } + + outProgram.packetSlot = packetSlot; + outProgram.producerSlot = producerSlot; + outProgram.commandPtr = + static_cast(descriptorIOVA) | + Layout::kBlocksPerPacket; + outProgram.payloadLength = meta.payloadLength; + return true; + } + return false; +} + +IsochTxDmaRing::SingleSilencePacketCompletion +IsochTxDmaRing::FetchSingleSilencePacketCompletionForRunPreflight( + const uint32_t packetSlot) noexcept { + SingleSilencePacketCompletion out{}; + if (!slab_.IsValid() || packetSlot >= Layout::kNumPackets) { + return out; + } + + auto* completion = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(completion), + sizeof(*completion)); + } else { + ASFW::Driver::ReadBarrier(); + } + out.transferStatus = AT_xferStatus(*completion); + out.timestamp = AT_timeStamp(*completion); + return out; +} + +bool IsochTxDmaRing::ProgramFiniteSilenceCadenceForRunPreflight( + uint8_t* payloadBase, + const uint32_t numSlots, + const uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + FiniteSilenceCadenceProgram& outProgram) noexcept { + outProgram = {}; + if (!slab_.IsValid() || payloadBase == nullptr || metadataRing == nullptr || + numSlots == 0U || slotStrideBytes == 0U) { + return false; + } + + static_assert(Layout::kNumPackets == 48U); + constexpr uint32_t kExpectedPayloadBytes = 1536U; + constexpr uint32_t kExpectedDataPackets = 36U; + constexpr uint32_t kExpectedSkipPackets = 12U; + + // The FF800 packetizer's valid 192 kHz cadence may be committed with any + // of the four phase rotations. Stage 5G requires the finite command list + // itself to begin D-D-D-S, so select the phase whose first three physical + // slots are data and whose fourth is a skip. The previous implementation + // assumed physical slot zero was always data; on the real run it was the + // skip phase (S-D-D-D), causing packet 0 to be rejected before DMA. + uint32_t cadenceStartSlot = Layout::kNumPackets; + for (uint32_t candidate = 0U; candidate < 4U; ++candidate) { + bool candidateValid = true; + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const uint32_t physicalSlot = + (candidate + logicalSlot) % Layout::kNumPackets; + const uint32_t producerSlot = physicalSlot % numSlots; + const auto& meta = metadataRing[producerSlot]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const uint32_t metaQ1 = + OSSwapLittleToHostInt32(meta.immediateHeader[1]); + const auto tag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + const bool actualSkip = + ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength); + const bool expectedSkip = (logicalSlot & 0x3U) == 0x3U; + if (actualSkip != expectedSkip || + tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(metaQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(metaQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(metaQ1) != + meta.payloadLength) { + candidateValid = false; + break; + } + } + if (candidateValid) { + cadenceStartSlot = candidate; + break; + } + } + + if (cadenceStartSlot >= Layout::kNumPackets) { + uint32_t observedSkipMask = 0U; + for (uint32_t physicalSlot = 0U; physicalSlot < 4U; ++physicalSlot) { + const auto& meta = metadataRing[physicalSlot % numSlots]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto tag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength)) { + observedSkipMask |= (1U << physicalSlot); + } + } + ASFW_LOG(Isoch, + "IT: Stage 5G could not phase-align committed cadence observedFirst4SkipMask=0x%x expectedRotatedDDD-S=1", + observedSkipMask); + return false; + } + + ASFW_LOG(Isoch, + "IT: Stage 5G cadence phase aligned physicalStartSlot=%u logicalPattern=DDD-S", + cadenceStartSlot); + + uint32_t dataPackets = 0U; + uint32_t skipPackets = 0U; + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool expectedSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (cadenceStartSlot + logicalSlot) % Layout::kNumPackets; + const uint32_t producerSlot = physicalSlot % numSlots; + const auto& meta = metadataRing[producerSlot]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const uint32_t metaQ1 = + OSSwapLittleToHostInt32(meta.immediateHeader[1]); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + const bool actualSkip = + ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength); + + if (actualSkip != expectedSkip || + tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(metaQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(metaQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(metaQ1) != + meta.payloadLength) { + ASFW_LOG(Isoch, + "IT: Stage 5G cadence metadata rejected logical=%u physical=%u expectedSkip=%u actualSkip=%u tag=%u speed=%u tcode=0x%x dataLength=%u/%u", + logicalSlot, + physicalSlot, + expectedSkip ? 1U : 0U, + actualSkip ? 1U : 0U, + static_cast(tag), + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(metaQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(metaQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(metaQ1), + meta.payloadLength); + return false; + } + + const uint32_t descBase = + physicalSlot * Layout::kBlocksPerPacket; + const bool isLast = logicalSlot + 1U == Layout::kNumPackets; + const bool nextIsSkip = !isLast && + (((logicalSlot + 1U) & 0x3U) == 0x3U); + const uint32_t nextZ = + nextIsSkip ? 1U : Layout::kBlocksPerPacket; + const uint32_t nextPhysicalSlot = isLast + ? 0U + : ((cadenceStartSlot + logicalSlot + 1U) % + Layout::kNumPackets); + const uint32_t nextDescriptorIOVA = isLast + ? 0U + : slab_.GetDescriptorIOVA( + nextPhysicalSlot * Layout::kBlocksPerPacket); + + if (expectedSkip) { + ++skipPackets; + if (meta.payloadLength != 0U) { + return false; + } + + for (uint32_t block = 0U; + block < Layout::kBlocksPerPacket; + ++block) { + auto* desc = slab_.GetDescriptorPtr(descBase + block); + std::memset(desc, 0, sizeof(OHCIDescriptor)); + } + + auto* skip = slab_.GetDescriptorPtr(descBase); + skip->control = OHCIDescriptor::BuildControl({ + .reqCount = 0U, + .command = OHCIDescriptor::kCmdOutputLast, + .key = OHCIDescriptor::kKeyStandard, + .interruptBits = OHCIDescriptor::kIntNever, + .branchBits = OHCIDescriptor::kBranchAlways, + }); + skip->control |= + (1U << (OHCIDescriptor::kStatusShift + + OHCIDescriptor::kControlHighShift)); + skip->dataAddress = 0U; + skip->branchWord = isLast + ? 0U + : MakeFiniteCadenceBranchWord(nextDescriptorIOVA, nextZ); + if (!isLast && skip->branchWord == 0U) { + return false; + } + AR_init_status(*skip, 0U); + continue; + } + + ++dataPackets; + if (meta.payloadLength != kExpectedPayloadBytes || + meta.payloadLength > slotStrideBytes) { + ASFW_LOG(Isoch, + "IT: Stage 5G data shape rejected logical=%u physical=%u producer=%u payloadBytes=%u/%u stride=%u", + logicalSlot, + physicalSlot, + producerSlot, + meta.payloadLength, + kExpectedPayloadBytes, + slotStrideBytes); + return false; + } + + const uint8_t* payload = payloadBase + + static_cast(producerSlot) * slotStrideBytes; + for (uint32_t byte = 0U; byte < meta.payloadLength; ++byte) { + if (payload[byte] != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5G silence safety check rejected logical=%u physical=%u producer=%u byte=%u value=0x%02x", + logicalSlot, + physicalSlot, + producerSlot, + byte, + payload[byte]); + return false; + } + } + + auto* immDesc = reinterpret_cast( + slab_.GetDescriptorPtr(descBase)); + auto* payloadDesc = slab_.GetDescriptorPtr( + descBase + Layout::kFirstPayloadBlock); + auto* completion = slab_.GetDescriptorPtr( + descBase + Layout::kCompletionBlock); + const uint32_t wireQ0 = + OSSwapLittleToHostInt32(immDesc->immediateData[0]); + const uint32_t wireQ1 = + OSSwapLittleToHostInt32(immDesc->immediateData[1]); + const uint8_t wireChannel = + static_cast((wireQ0 >> 8U) & 0x3FU); + const uint32_t firstFragmentBytes = + payloadDesc->control & 0xFFFFU; + const uint32_t lastFragmentBytes = + completion->control & 0xFFFFU; + if (ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0) != + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1) != + kExpectedPayloadBytes || + wireChannel != channel_ || + firstFragmentBytes + lastFragmentBytes != kExpectedPayloadBytes || + payloadDesc->dataAddress == 0U || + completion->dataAddress == 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5G primed descriptor validation failed logical=%u physical=%u tag=%u speed=%u tcode=0x%x channel=%u/%u dataLength=%u fragments=%u+%u", + logicalSlot, + physicalSlot, + static_cast( + ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0)), + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0), + wireChannel, + channel_, + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1), + firstFragmentBytes, + lastFragmentBytes); + return false; + } + + const uint32_t intMask = 0x3U << + (OHCIDescriptor::kIntShift + + OHCIDescriptor::kControlHighShift); + completion->control &= ~intMask; + completion->branchWord = isLast + ? 0U + : MakeFiniteCadenceBranchWord(nextDescriptorIOVA, nextZ); + if (!isLast && completion->branchWord == 0U) { + return false; + } + AR_init_status(*completion, 0U); + payloadDesc->statusWord = 0U; + immDesc->common.statusWord = 0U; + + if (dmaMemory_) { + dmaMemory_->PublishToDevice( + reinterpret_cast(payload), + meta.payloadLength); + } + } + + if (dataPackets != kExpectedDataPackets || + skipPackets != kExpectedSkipPackets) { + return false; + } + + if (dmaMemory_) { + dmaMemory_->PublishBarrier(); + dmaMemory_->PublishToDevice( + slab_.DescriptorRegion().virtualBase, + slab_.DescriptorRegion().size); + } else { + ASFW::Driver::WriteBarrier(); + } + + const uint64_t descriptorIOVA = slab_.GetDescriptorIOVA( + cadenceStartSlot * Layout::kBlocksPerPacket); + if (descriptorIOVA == 0U || descriptorIOVA > 0xFFFFFFFFULL) { + return false; + } + + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool isSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (cadenceStartSlot + logicalSlot) % Layout::kNumPackets; + const auto* branchOwner = isSkip + ? slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket) + : slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + const bool isLast = logicalSlot + 1U == Layout::kNumPackets; + const bool nextIsSkip = !isLast && + (((logicalSlot + 1U) & 0x3U) == 0x3U); + const uint32_t nextPhysicalSlot = isLast + ? 0U + : ((cadenceStartSlot + logicalSlot + 1U) % + Layout::kNumPackets); + const uint32_t expectedBranch = isLast + ? 0U + : MakeFiniteCadenceBranchWord( + slab_.GetDescriptorIOVA( + nextPhysicalSlot * Layout::kBlocksPerPacket), + nextIsSkip ? 1U : Layout::kBlocksPerPacket); + if (branchOwner->branchWord != expectedBranch || + (!isLast && expectedBranch == 0U)) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite branch validation failed logical=%u physical=%u skip=%u actual=0x%08x expected=0x%08x", + logicalSlot, + physicalSlot, + isSkip ? 1U : 0U, + branchOwner->branchWord, + expectedBranch); + return false; + } + } + + outProgram.commandPtr = + static_cast(descriptorIOVA) | + Layout::kBlocksPerPacket; + outProgram.startPacketSlot = cadenceStartSlot; + outProgram.descriptorCount = Layout::kNumPackets; + outProgram.dataPacketCount = dataPackets; + outProgram.skipPacketCount = skipPackets; + outProgram.payloadLength = kExpectedPayloadBytes; + return true; +} + +bool IsochTxDmaRing::ProgramBoundedCircularSilenceCadenceForPreflight( + uint8_t* payloadBase, + const uint32_t numSlots, + const uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + BoundedCircularSilenceCadenceProgram& outProgram) noexcept { + outProgram = {}; + + FiniteSilenceCadenceProgram finite{}; + if (!ProgramFiniteSilenceCadenceForRunPreflight( + payloadBase, + numSlots, + slotStrideBytes, + metadataRing, + finite)) { + ASFW_LOG(Isoch, + "IT: Stage 5H could not construct the Stage 5G-validated silence cadence"); + return false; + } + + if (finite.descriptorCount != Layout::kNumPackets || + finite.dataPacketCount != 36U || + finite.skipPacketCount != 12U || + finite.payloadLength != 1536U || + finite.startPacketSlot >= Layout::kNumPackets) { + return false; + } + + // Logical slot 47 is always the skip in the twelfth D-D-D-S group. Its + // one-descriptor program must advertise Z=4 when branching back to the + // first logical slot, which is guaranteed to be a normal data program. + const uint32_t lastPhysicalSlot = + (finite.startPacketSlot + Layout::kNumPackets - 1U) % + Layout::kNumPackets; + auto* lastSkip = slab_.GetDescriptorPtr( + lastPhysicalSlot * Layout::kBlocksPerPacket); + const uint32_t firstDescriptorIOVA = slab_.GetDescriptorIOVA( + finite.startPacketSlot * Layout::kBlocksPerPacket); + const uint32_t circularBranch = MakeFiniteCadenceBranchWord( + firstDescriptorIOVA, Layout::kBlocksPerPacket); + if (circularBranch == 0U) { + return false; + } + lastSkip->branchWord = circularBranch; + + if (dmaMemory_) { + dmaMemory_->PublishBarrier(); + dmaMemory_->PublishToDevice( + slab_.DescriptorRegion().virtualBase, + slab_.DescriptorRegion().size); + } else { + ASFW::Driver::WriteBarrier(); + } + + // Re-read every branch owner after publication. The first 47 links retain + // Stage 5G's phase-correct Z values; only the final skip is now circular. + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool isSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (finite.startPacketSlot + logicalSlot) % Layout::kNumPackets; + const auto* branchOwner = isSkip + ? slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket) + : slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + const uint32_t nextLogicalSlot = + (logicalSlot + 1U) % Layout::kNumPackets; + const uint32_t nextPhysicalSlot = + (finite.startPacketSlot + nextLogicalSlot) % + Layout::kNumPackets; + const bool nextIsSkip = (nextLogicalSlot & 0x3U) == 0x3U; + const uint32_t expectedBranch = MakeFiniteCadenceBranchWord( + slab_.GetDescriptorIOVA( + nextPhysicalSlot * Layout::kBlocksPerPacket), + nextIsSkip ? 1U : Layout::kBlocksPerPacket); + if (expectedBranch == 0U || + branchOwner->branchWord != expectedBranch) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular branch validation failed logical=%u physical=%u skip=%u actual=0x%08x expected=0x%08x", + logicalSlot, + physicalSlot, + isSkip ? 1U : 0U, + branchOwner->branchWord, + expectedBranch); + return false; + } + } + + outProgram.commandPtr = finite.commandPtr; + outProgram.startPacketSlot = finite.startPacketSlot; + outProgram.descriptorCount = finite.descriptorCount; + outProgram.dataPacketCount = finite.dataPacketCount; + outProgram.skipPacketCount = finite.skipPacketCount; + outProgram.payloadLength = finite.payloadLength; + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H bounded circular descriptor ring published descriptors=%u dataPackets=%u skip=%u payloadBytes=%u pattern=DDD-S finalBranchToStart=1 interrupts=0 refill=0 dmaPublish=1", + outProgram.descriptorCount, + outProgram.dataPacketCount, + outProgram.skipPacketCount, + outProgram.payloadLength); + return true; +} + +IsochTxDmaRing::CadenceAnchorCompletion +IsochTxDmaRing::FetchCadenceAnchorCompletionForPreflight( + const uint32_t startPacketSlot) noexcept { + CadenceAnchorCompletion out{}; + if (!slab_.IsValid() || startPacketSlot >= Layout::kNumPackets) { + return out; + } + const auto* completion = slab_.GetDescriptorPtr( + startPacketSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(completion), + sizeof(*completion)); + } else { + ASFW::Driver::ReadBarrier(); + } + out.transferStatus = AT_xferStatus(*completion); + out.timestamp = AT_timeStamp(*completion); + return out; +} + +IsochTxDmaRing::FiniteSilenceCadenceCompletion +IsochTxDmaRing::FetchFiniteSilenceCadenceCompletionForRunPreflight( + const uint32_t startPacketSlot) noexcept { + FiniteSilenceCadenceCompletion out{}; + if (!slab_.IsValid() || startPacketSlot >= Layout::kNumPackets) { + return out; + } + + const auto region = slab_.DescriptorRegion(); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice(region.virtualBase, region.size); + } else { + ASFW::Driver::ReadBarrier(); + } + + uint32_t dataIndex = 0U; + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool isSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (startPacketSlot + logicalSlot) % Layout::kNumPackets; + const auto* completion = isSkip + ? slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket) + : slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + const uint16_t transferStatus = AT_xferStatus(*completion); + const uint16_t timestamp = AT_timeStamp(*completion); + if (transferStatus != 0U) { + ++out.completedDescriptors; + const uint8_t eventCode = + static_cast(transferStatus & 0x1FU); + if (eventCode != 0x11U) { + ++out.eventErrors; + } + if (!isSkip) { + ++out.completedDataDescriptors; + ++out.actualBusPackets; + if (dataIndex < out.dataTimestamps.size()) { + out.dataTimestamps[dataIndex] = timestamp; + } + ++dataIndex; + } + } + if (logicalSlot + 1U == Layout::kNumPackets) { + out.lastTransferStatus = transferStatus; + out.lastTimestamp = timestamp; + } + } + + if (out.completedDataDescriptors == out.dataTimestamps.size()) { + out.cadenceValid = true; + uint32_t previousCycle = + (out.dataTimestamps[0] & 0x1FFFU) % 8000U; + for (uint32_t data = 1U; + data < out.dataTimestamps.size(); + ++data) { + const uint32_t cycle = + (out.dataTimestamps[data] & 0x1FFFU) % 8000U; + const uint32_t gap = + (cycle + 8000U - previousCycle) % 8000U; + const uint32_t expectedGap = + (data % 3U) == 0U ? 2U : 1U; + if (gap != expectedGap) { + out.cadenceValid = false; + break; + } + previousCycle = cycle; + } + } + return out; +} + +bool IsochTxDmaRing::ProgramAllSkipPacketsForRunPreflight() noexcept { + if (!slab_.IsValid()) { + return false; + } + + for (uint32_t packetSlot = 0; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + const uint32_t descBase = packetSlot * Layout::kBlocksPerPacket; + for (uint32_t block = 0; block < Layout::kBlocksPerPacket; ++block) { + auto* desc = slab_.GetDescriptorPtr(descBase + block); + std::memset(desc, 0, sizeof(OHCIDescriptor)); + } + + auto* skip = slab_.GetDescriptorPtr(descBase); + const bool isLast = packetSlot + 1U == Layout::kNumPackets; + const uint32_t nextDescriptorIOVA = isLast + ? 0U + : slab_.GetDescriptorIOVA( + (packetSlot + 1U) * Layout::kBlocksPerPacket); + + // A queued OHCI transmit skip is a one-descriptor program (Z=1), + // even though ASFW reserves four physical descriptor slots per audio + // cycle. Advertising Z=4 made the controller consume the first + // OUTPUT_LAST and then interpret the three zero padding descriptors as + // part of the same program, so only descriptor 0 ever completed. + skip->control = OHCIDescriptor::BuildControl({ + .reqCount = 0, + .command = OHCIDescriptor::kCmdOutputLast, + .key = OHCIDescriptor::kKeyStandard, + .interruptBits = OHCIDescriptor::kIntNever, + .branchBits = OHCIDescriptor::kBranchAlways, + }); + skip->control |= + (1u << (OHCIDescriptor::kStatusShift + + OHCIDescriptor::kControlHighShift)); + skip->dataAddress = 0U; + skip->branchWord = isLast + ? 0U + : ((nextDescriptorIOVA & 0xFFFFFFF0U) | 1U); + AR_init_status(*skip, 0U); + } + + // Publish the complete finite descriptor chain before CommandPtr/RUN. + if (dmaMemory_) { + dmaMemory_->PublishToDevice( + slab_.DescriptorRegion().virtualBase, + slab_.DescriptorRegion().size); + } else { + ASFW::Driver::WriteBarrier(); + } + + for (uint32_t packetSlot = 0; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + if (!IsSkipDescriptor(packetSlot)) { + return false; + } + const auto* desc = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + const bool isLast = packetSlot + 1U == Layout::kNumPackets; + if (isLast) { + if (desc->branchWord != 0U) { + return false; + } + } else if (desc->branchWord == 0U || + (desc->branchWord & 0xFU) != 1U) { + return false; + } + } + return true; +} + +IsochTxDmaRing::AllSkipCompletionSnapshot +IsochTxDmaRing::FetchAllSkipCompletionForRunPreflight() noexcept { + AllSkipCompletionSnapshot out{}; + if (!slab_.IsValid()) { + return out; + } + + const auto region = slab_.DescriptorRegion(); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice(region.virtualBase, region.size); + } else { + ASFW::Driver::ReadBarrier(); + } + + for (uint32_t packetSlot = 0; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + const auto* desc = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + if (AT_xferStatus(*desc) != 0U) { + ++out.completedDescriptors; + } + if (packetSlot + 1U == Layout::kNumPackets) { + out.lastTransferStatus = AT_xferStatus(*desc); + out.lastTimestamp = AT_timeStamp(*desc); + } + } + return out; +} + +bool IsochTxDmaRing::IsSkipDescriptor( + const uint32_t packetSlot) const noexcept { + const auto* desc = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + const uint32_t controlHigh = + desc->control >> OHCIDescriptor::kControlHighShift; + const uint8_t command = static_cast( + (controlHigh >> OHCIDescriptor::kCmdShift) & 0xFU); + const uint8_t key = static_cast( + (controlHigh >> OHCIDescriptor::kKeyShift) & 0x7U); + const uint16_t requestCount = + static_cast(desc->control & 0xFFFFU); + return command == OHCIDescriptor::kCmdOutputLast && + key == OHCIDescriptor::kKeyStandard && + requestCount == 0U; +} + +IsochTxDmaRing::OHCIDescriptor* +IsochTxDmaRing::CompletionDescriptorForPacket( + const uint32_t packetSlot) noexcept { + if (IsSkipDescriptor(packetSlot)) { + return slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + } + return slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); +} + IsochTxDmaRing::PrimeStats IsochTxDmaRing::Prime( const TxPayloadDmaMap& payloadDmaMap, const uint32_t numSlots, @@ -277,6 +1161,16 @@ IsochTxDmaRing::PrimeStats IsochTxDmaRing::Prime( return stats; } + const uint32_t headerQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto packetTag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + packetTag, meta.payloadLength)) { + ProgramSkipPacket(pktIdx); + continue; + } + std::array payloadFragments{}; const uint64_t payloadOffset = static_cast(producerSlot) * slotStrideBytes; @@ -525,15 +1419,23 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( const uint64_t currentAbsIdx = completedAbsIdx + i; const uint32_t completedPktSlot = static_cast(currentAbsIdx % Layout::kNumPackets); - auto* desc2 = slab_.GetDescriptorPtr( - completedPktSlot * Layout::kBlocksPerPacket + - Layout::kCompletionBlock); + auto* firstDesc = slab_.GetDescriptorPtr( + completedPktSlot * Layout::kBlocksPerPacket); if (dmaMemory_) { - dmaMemory_->FetchFromDevice(reinterpret_cast(desc2), sizeof(*desc2)); + dmaMemory_->FetchFromDevice( + reinterpret_cast(firstDesc), + sizeof(*firstDesc)); + } + auto* completionDesc = + CompletionDescriptorForPacket(completedPktSlot); + if (dmaMemory_ && completionDesc != firstDesc) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(completionDesc), + sizeof(*completionDesc)); } - const uint16_t hwTimestamp = - static_cast(desc2->statusWord & 0xFFFF); + const uint16_t hwTimestamp = static_cast( + completionDesc->statusWord & 0xFFFFU); // OHCI OUTPUT_LAST reports sec[2:0]:cycle[12:0] and omits the // intra-cycle offset. Reconstruct the packet cycle at offset zero. @@ -671,10 +1573,33 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( return out; } + const uint32_t headerQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto packetTag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + packetTag, payloadLength)) { + ProgramSkipPacket(hwSlot); + if (dmaMemory_) { + for (uint32_t block = 0; + block < Layout::kBlocksPerPacket; + ++block) { + const auto* desc = slab_.GetDescriptorPtr( + hwSlot * Layout::kBlocksPerPacket + block); + dmaMemory_->PublishToDevice( + reinterpret_cast(desc), + sizeof(*desc)); + } + } + ++packetsFilled; + continue; + } + if (payloadBase) { GaugeWirePayload(fillAbsIdx, payloadBase + payloadOffset, - payloadLength); + payloadLength, + meta.immediateHeader[0]); } std::array payloadFragments{}; @@ -703,6 +1628,14 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( const uint32_t descBase = hwSlot * Layout::kBlocksPerPacket; auto* immDesc = reinterpret_cast( slab_.GetDescriptorPtr(descBase)); + std::memset(immDesc, 0, sizeof(OHCIDescriptorImmediate)); + immDesc->common.control = OHCIDescriptor::BuildControl({ + .reqCount = 8, + .command = OHCIDescriptor::kCmdOutputMore, + .key = OHCIDescriptor::kKeyImmediate, + .interruptBits = OHCIDescriptor::kIntNever, + .branchBits = OHCIDescriptor::kBranchNever, + }); immDesc->immediateData[0] = StampHeaderChannel(meta.immediateHeader[0], channel_); immDesc->immediateData[1] = meta.immediateHeader[1]; @@ -712,6 +1645,7 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( auto* desc2 = slab_.GetDescriptorPtr(descBase + Layout::kFirstPayloadBlock); + std::memset(desc2, 0, sizeof(OHCIDescriptor)); desc2->control = OHCIDescriptor::BuildControl({ .reqCount = static_cast(payloadFragments[0].length), .command = OHCIDescriptor::kCmdOutputMore, @@ -725,6 +1659,7 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( auto* desc3 = slab_.GetDescriptorPtr(descBase + Layout::kCompletionBlock); + std::memset(desc3, 0, sizeof(OHCIDescriptor)); desc3->control = OHCIDescriptor::BuildControl({ .reqCount = static_cast(payloadFragments[1].length), .command = OHCIDescriptor::kCmdOutputLast, diff --git a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp index 9abd1948..9adc17bb 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp +++ b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp @@ -135,6 +135,109 @@ class IsochTxDmaRing final { const ASFW::IsochTransport::TxPacketMeta* metadataRing, uint64_t preFillCount) noexcept; + struct SingleSilencePacketProgram final { + uint32_t packetSlot{0U}; + uint32_t producerSlot{0U}; + uint32_t commandPtr{0U}; + uint32_t payloadLength{0U}; + }; + + struct SingleSilencePacketCompletion final { + uint16_t transferStatus{0U}; + uint16_t timestamp{0U}; + }; + + // Stage 5F safety seam: select one already-primed no-CIP FF800 data packet, + // verify that its 1536-byte payload is entirely zero, terminate its normal + // four-descriptor OHCI program, publish payload + descriptors, and return + // the exact CommandPtr. This is the first preflight that can emit a packet. + [[nodiscard]] bool ProgramSingleSilencePacketForRunPreflight( + uint8_t* payloadBase, + uint32_t numSlots, + uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + SingleSilencePacketProgram& outProgram) noexcept; + [[nodiscard]] SingleSilencePacketCompletion + FetchSingleSilencePacketCompletionForRunPreflight( + uint32_t packetSlot) noexcept; + + struct FiniteSilenceCadenceProgram final { + uint32_t commandPtr{0U}; + uint32_t startPacketSlot{0U}; + uint32_t descriptorCount{0U}; + uint32_t dataPacketCount{0U}; + uint32_t skipPacketCount{0U}; + uint32_t payloadLength{0U}; + }; + + struct FiniteSilenceCadenceCompletion final { + uint32_t completedDescriptors{0U}; + uint32_t completedDataDescriptors{0U}; + uint32_t actualBusPackets{0U}; + uint32_t eventErrors{0U}; + uint16_t lastTransferStatus{0U}; + uint16_t lastTimestamp{0U}; + bool cadenceValid{false}; + std::array dataTimestamps{}; + }; + + // Stage 5G safety seam: convert the already-primed 48-cycle FF800 ring + // into one finite D-D-D-S cadence burst. All 36 data payloads must be + // 1536-byte no-CIP silence packets; all 12 idle cycles remain true OHCI + // skip programs. Queue branches are finite and the final skip terminates. + [[nodiscard]] bool ProgramFiniteSilenceCadenceForRunPreflight( + uint8_t* payloadBase, + uint32_t numSlots, + uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + FiniteSilenceCadenceProgram& outProgram) noexcept; + [[nodiscard]] FiniteSilenceCadenceCompletion + FetchFiniteSilenceCadenceCompletionForRunPreflight( + uint32_t startPacketSlot) noexcept; + + struct BoundedCircularSilenceCadenceProgram final { + uint32_t commandPtr{0U}; + uint32_t startPacketSlot{0U}; + uint32_t descriptorCount{0U}; + uint32_t dataPacketCount{0U}; + uint32_t skipPacketCount{0U}; + uint32_t payloadLength{0U}; + }; + + struct CadenceAnchorCompletion final { + uint16_t transferStatus{0U}; + uint16_t timestamp{0U}; + }; + + // Stage 5H safety seam: reuse the Stage 5G-validated 48-cycle D-D-D-S + // silence program, then connect its final skip back to the first data + // descriptor. The ring remains immutable while RUN is set; the caller + // must stop it after a strictly bounded interval. + [[nodiscard]] bool ProgramBoundedCircularSilenceCadenceForPreflight( + uint8_t* payloadBase, + uint32_t numSlots, + uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + BoundedCircularSilenceCadenceProgram& outProgram) noexcept; + [[nodiscard]] CadenceAnchorCompletion + FetchCadenceAnchorCompletionForPreflight( + uint32_t startPacketSlot) noexcept; + + struct AllSkipCompletionSnapshot final { + uint32_t completedDescriptors{0}; + uint16_t lastTransferStatus{0}; + uint16_t lastTimestamp{0}; + }; + + // Stage 5E safety seam: replace every packet program with a true OHCI + // transmit skip descriptor. The chain is deliberately finite: descriptors + // 0..46 branch forward and descriptor 47 terminates with branchAddress=0. + // This mirrors the normal OHCI queue model and lets completion status, + // rather than the transient ACTIVE bit, prove that DMA executed. + [[nodiscard]] bool ProgramAllSkipPacketsForRunPreflight() noexcept; + [[nodiscard]] AllSkipCompletionSnapshot + FetchAllSkipCompletionForRunPreflight() noexcept; + [[nodiscard]] RefillOutcome Refill(Driver::HardwareInterface& hw, uint8_t contextIndex, ASFW::IsochTransport::TxPacketMeta* metadataRing, @@ -164,13 +267,18 @@ class IsochTxDmaRing final { uint32_t deltaConsumed, RefillOutcome& out) noexcept; void CommitRefill(uint32_t toFill) noexcept; + void ProgramSkipPacket(uint32_t packetSlot) noexcept; + [[nodiscard]] bool IsSkipDescriptor(uint32_t packetSlot) const noexcept; + [[nodiscard]] OHCIDescriptor* CompletionDescriptorForPacket( + uint32_t packetSlot) noexcept; [[nodiscard]] bool DecodeHardwarePacketIndex(Driver::HardwareInterface& hw, uint8_t contextIndex, uint32_t& outPacketIndex, uint32_t& outCmdPtr) noexcept; void GaugeWirePayload(uint64_t fillAbsIdx, const uint8_t* packetBytes, - uint32_t payloadLength) noexcept; + uint32_t payloadLength, + uint32_t immediateHeaderQ0LE) noexcept; uint8_t channel_{0}; IsochTxDescriptorSlab slab_{}; From 7397d194d5f9614e21e0759275445fb0b65c9d54 Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 04:04:26 -0300 Subject: [PATCH 2/7] RME Fireface 800: consolidate Stage 5C-5H wiring, no-CIP wire format, and test build fix Rounds out the earlier RME/Isoch backup commit with everything that feature depends on to actually compile and run: - No-CIP (tag=0) isoch header support end to end: IsochAudioTransport header build/decode helpers, includeCipHeader plumbed through AmdtpStreamConfig/AmdtpTxPacketizer/AudioStreamConfig/ DiceStreamConfigMapper, and the OHCI TX descriptor path. - Core Audio wiring for the RME read-only nub: AudioCoordinator publishes/tears down the Stage 5C endpoint, ASFWAudioDevice enters the Stage 5C preflight-only StartIO/StopIO path, ASFWAudioDriverZts primes the FF800 blocking cadence instead of NO-DATA. - Discovery: FWDevice parses Model_ID (0x17) from the Unit Directory, DeviceRegistry recovers model_id from the raw Config ROM as a fallback (RME publishes it in the Unit Directory, not the root), DeviceManager refreshes identity on rediscovery. - DeviceProtocolFactory/IDeviceProtocol/AudioProfileRegistry/ AudioDeviceIds/AudioProfileTypes/RMEAudioProfiles: RME identity, kReadOnlyNub integration mode, and the bounded playback preflight seam (PlaybackPreflightRoute / RunBoundedPlaybackIntegrationPreflight) the Stage 5G/5H protocol implementation calls into. - Info.plist: IOPCIMatch for this bench's TI 104C:823F controller, ASFWAutoStartAudioStreams=false (StartIO stays rejected per the Stage 5G/5H invariant). DriverContext.cpp: interrupt setup gets MSI-X/MSI/fallback diagnostics for bring-up on this board. - project.pbxproj/ASFW.xcscheme: adds the new RME/RMEAudioProfiles file references so Xcode builds them. Hand-edited because xcodegen isn't installed in this environment -- re-run `xcodegen generate` once it is, to get a canonical regenerated project and diff-check it against this. - tests/{protocols,devices}/CMakeLists.txt: add ConfigROMParser.cpp to the three test targets that compile DeviceRegistry.cpp, which now calls ConfigROMParser::ParseDirectory (previously an unresolved symbol at link time for SessionRegistryTests/SBP2HandlerTests/ AudioDuplexCoordinatorTests). --- ASFW.xcodeproj/project.pbxproj | 34 ++++- .../xcshareddata/xcschemes/ASFW.xcscheme | 1 + .../xcschemes/ASFWDriver.xcscheme | 87 ------------ ASFW.xctestplan | 9 +- ASFWDriver/Audio/Core/AudioCoordinator.cpp | 76 ++++++++++- ASFWDriver/Audio/Core/AudioCoordinator.hpp | 1 + .../Audio/DriverKit/ASFWAudioDevice.cpp | 42 +++++- .../DriverKit/ASFWAudioDriverPrivate.hpp | 30 ++-- .../Audio/DriverKit/ASFWAudioDriverZts.cpp | 21 ++- ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp | 128 +++++++++++++++++- .../DriverKit/Config/AudioProfileRegistry.cpp | 103 ++++++++++++++ .../DriverKit/Config/AudioStreamProfile.hpp | 6 + .../Engine/Direct/Tx/DiceTxStreamEngine.cpp | 10 +- .../Audio/Protocols/DeviceProtocolFactory.cpp | 10 ++ .../Audio/Protocols/DeviceProtocolFactory.hpp | 4 + .../Audio/Protocols/IDeviceProtocol.hpp | 32 +++++ .../Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp | 3 +- .../Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp | 26 +++- .../Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp | 1 + ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp | 6 + .../DeviceProfiles/Audio/AudioDeviceIds.hpp | 6 + .../Audio/AudioProfileRegistry.hpp | 3 + .../Audio/AudioProfileTypes.hpp | 1 + .../Audio/Vendors/RMEAudioProfiles.hpp | 46 +++++++ ASFWDriver/Discovery/DeviceManager.cpp | 5 + ASFWDriver/Discovery/DeviceRegistry.cpp | 67 ++++++++- ASFWDriver/Discovery/FWDevice.cpp | 30 ++++ ASFWDriver/Discovery/FWDevice.hpp | 7 +- ASFWDriver/Info.plist | 8 +- ASFWDriver/Service/DriverContext.cpp | 25 +++- .../Shared/Isoch/IsochAudioTransport.hpp | 62 ++++++++- tests/devices/CMakeLists.txt | 1 + tests/protocols/CMakeLists.txt | 2 + 33 files changed, 739 insertions(+), 154 deletions(-) delete mode 100644 ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme create mode 100644 ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index 47bc3d01..5b17196d 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A1B2C3D4E5F60718293A4B5C /* RMEFireface800Protocol.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B1C2D3E4F5061728394A5B6C /* RMEFireface800Protocol.cpp */; }; 0267190E6597A8DA11482D7C /* DriverConnector+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264D4CD41FF948792F95E023 /* DriverConnector+Status.swift */; }; 02D9A096E4DC8704CF3B4DE4 /* MetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B20C10AC8C1594C410D4D6F /* MetricsView.swift */; }; 04D9E2DA1F4128063BC1E28E /* FocusriteSaffireProfile.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7B882FD0DA3D8C7A7845DA54 /* FocusriteSaffireProfile.cpp */; }; @@ -381,6 +382,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + B1C2D3E4F5061728394A5B6C /* RMEFireface800Protocol.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RMEFireface800Protocol.cpp; sourceTree = ""; }; + C1D2E3F40516273849A5B6C7 /* RMEFireface800Protocol.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEFireface800Protocol.hpp; sourceTree = ""; }; + E1F2031425364758697A8B9C /* RMEAudioProfiles.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEAudioProfiles.hpp; sourceTree = ""; }; 00744EB824D48D9F716E153A /* PostResetTimingCoordinator.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PostResetTimingCoordinator.hpp; sourceTree = ""; }; 00979778212761BF904386E6 /* AudioEndpointRuntime.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AudioEndpointRuntime.hpp; sourceTree = ""; }; 00D5EC5AEB7989BC5DBE3E15 /* BusResetHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BusResetHandler.cpp; sourceTree = ""; }; @@ -1650,6 +1654,7 @@ 4D2DB64009F93C974F2DF20F /* DICE */, FD14F207887135CE102F8ACA /* Duplex */, 6DF35E1146D265DA8C2B7611 /* Oxford */, + D1E2F3041526374859A6B7C8 /* RME */, ); path = Protocols; sourceTree = ""; @@ -2623,6 +2628,15 @@ path = Diagnostics; sourceTree = ""; }; + D1E2F3041526374859A6B7C8 /* RME */ = { + isa = PBXGroup; + children = ( + B1C2D3E4F5061728394A5B6C /* RMEFireface800Protocol.cpp */, + C1D2E3F40516273849A5B6C7 /* RMEFireface800Protocol.hpp */, + ); + path = RME; + sourceTree = ""; + }; E08412C570C729DF6E083569 /* Vendors */ = { isa = PBXGroup; children = ( @@ -2631,6 +2645,7 @@ F1BF9794C22F3E1693A6CCF0 /* FocusriteAudioProfiles.hpp */, ADA183665D23E96057546B82 /* MidasAudioProfiles.hpp */, 0C844DC3BD4E0AE7D1081F52 /* PreSonusAudioProfiles.hpp */, + E1F2031425364758697A8B9C /* RMEAudioProfiles.hpp */, C9474A3E174E5254247327C6 /* TerraTecAudioProfiles.hpp */, ); path = Vendors; @@ -3034,6 +3049,7 @@ DB27C439E148A97E29A0D466 /* DeviceDiscoveryHandler.cpp in Sources */, 971AC3A1D14A2A8C0F6E1939 /* DeviceManager.cpp in Sources */, 8482325128986FFEE1C854CF /* DeviceProtocolFactory.cpp in Sources */, + A1B2C3D4E5F60718293A4B5C /* RMEFireface800Protocol.cpp in Sources */, 713D5C0BC0201DFBDA15B69D /* DeviceRegistry.cpp in Sources */, 6DB7521C088B8C7AE091D4D3 /* DeviceStreamModeQuirks.cpp in Sources */, D5D557B2656B33436FB6738E /* DiagnosticLogger.cpp in Sources */, @@ -3303,7 +3319,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = F6YA6B56LR; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; @@ -3324,7 +3340,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = F6YA6B56LR; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; @@ -3399,6 +3415,9 @@ 5871D362E349A3418E269976 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_CODE_COVERAGE = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = NO; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; AD_HOC_CODE_SIGNING_ALLOWED = YES; ASFW_DEXT_ENTITLEMENTS_NO = ASFWDriver/ASFWDriver.entitlements; ASFW_DEXT_ENTITLEMENTS_YES = "ASFWDriver/ASFWDriver+SCSI.entitlements"; @@ -3410,7 +3429,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3448,7 +3467,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = F6YA6B56LR; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -3481,6 +3500,9 @@ 8AE4F9EA5D2C9D98D94E9AD2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_CODE_COVERAGE = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = NO; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; AD_HOC_CODE_SIGNING_ALLOWED = YES; ASFW_DEXT_ENTITLEMENTS_NO = ASFWDriver/ASFWDriver.entitlements; ASFW_DEXT_ENTITLEMENTS_YES = "ASFWDriver/ASFWDriver+SCSI.entitlements"; @@ -3492,7 +3514,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3595,7 +3617,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = F6YA6B56LR; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; diff --git a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme index 49d88a40..04fd1619 100644 --- a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme +++ b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme @@ -28,6 +28,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "NO" onlyGenerateCoverageForSpecifiedTargets = "NO"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ASFW.xctestplan b/ASFW.xctestplan index d5250085..5c2c6f93 100644 --- a/ASFW.xctestplan +++ b/ASFW.xctestplan @@ -1,7 +1,7 @@ { "configurations" : [ { - "id" : "68101470-79D0-48C5-ACA4-8865D9C3B598", + "id" : "50727156-0551-43F8-87AB-D75879B2CA5B", "name" : "Test Scheme Action", "options" : { @@ -9,9 +9,9 @@ } ], "defaultOptions" : { - "codeCoverage" : false, - "performanceAntipatternCheckerEnabled" : true, - "targetForVariableExpansion" : { + "codeCoverage" : false, + "performanceAntipatternCheckerEnabled" : true, + "targetForVariableExpansion" : { "containerPath" : "container:ASFW.xcodeproj", "identifier" : "EE5A446B3669D0171F9606EE", "name" : "ASFW" @@ -19,7 +19,6 @@ }, "testTargets" : [ { - "parallelizable" : true, "target" : { "containerPath" : "container:ASFW.xcodeproj", "identifier" : "348672D607701677ABCB567C", diff --git a/ASFWDriver/Audio/Core/AudioCoordinator.cpp b/ASFWDriver/Audio/Core/AudioCoordinator.cpp index 5e9a8b72..460d4438 100644 --- a/ASFWDriver/Audio/Core/AudioCoordinator.cpp +++ b/ASFWDriver/Audio/Core/AudioCoordinator.cpp @@ -46,6 +46,15 @@ void AudioCoordinator::SetCMPClient(ASFW::CMP::CMPClient* client) noexcept { void AudioCoordinator::OnDeviceAdded(std::shared_ptr device) { if (!device) return; const uint64_t guid = device->GetGUID(); + + const auto* record = registry_.FindByGuid(guid); + if (record && + DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) == + DeviceIntegrationMode::kReadOnlyNub) { + EnsureRMEReadOnlyNub(guid); + return; + } + if (BackendForGuid(guid) == &dice_) { dice_.OnDeviceRecordUpdated(guid); } @@ -55,7 +64,12 @@ void AudioCoordinator::OnDeviceResumed(std::shared_ptr devi if (!device) return; const uint64_t guid = device->GetGUID(); auto* backend = BackendForGuid(guid); - if (backend == &dice_) { + const auto* record = registry_.FindByGuid(guid); + if (record && + DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) == + DeviceIntegrationMode::kReadOnlyNub) { + EnsureRMEReadOnlyNub(guid); + } else if (backend == &dice_) { dice_.OnDeviceRecordUpdated(guid); } @@ -105,8 +119,15 @@ void AudioCoordinator::OnDeviceSuspended(std::shared_ptr de void AudioCoordinator::OnDeviceRemoved(Discovery::Guid64 guid) { if (guid == 0) return; + const auto* record = registry_.FindByGuid(guid); + const auto integration = record + ? DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) + : DeviceIntegrationMode::kNone; + auto* backend = BackendForGuid(guid); - if (backend == &dice_) { + if (integration == DeviceIntegrationMode::kReadOnlyNub) { + publisher_.TerminateNub(guid, "RME-Stage3-Removed"); + } else if (backend == &dice_) { dice_.OnDeviceRemoved(guid); } else if (backend == &avc_) { avc_.OnDeviceRemoved(guid); @@ -168,6 +189,44 @@ void AudioCoordinator::HandleCycleInconsistent() noexcept { dice_.HandleRecoveryEvent(guid, DICE::DiceRestartReason::kRecoverAfterCycleInconsistent); } + +void AudioCoordinator::EnsureRMEReadOnlyNub(uint64_t guid) noexcept { + const auto* record = registry_.FindByGuid(guid); + if (!record || + record->vendorId != DeviceProfiles::Audio::kRMEVendorId || + record->modelId != DeviceProfiles::Audio::kFireface800ModelId) { + return; + } + + Model::ASFWAudioDevice dev{}; + dev.guid = record->guid; + dev.vendorId = record->vendorId; + dev.modelId = record->modelId; + dev.deviceName = "RME Fireface 800 (Stage 5C)"; + dev.inputChannelCount = 12; + dev.outputChannelCount = 12; + dev.channelCount = 12; + dev.inputPlugName = "Fireface 800 Inputs"; + dev.outputPlugName = "Fireface 800 Outputs"; + dev.sampleRates = {192000u}; + dev.currentSampleRate = 192000u; + dev.streamMode = Model::StreamMode::kBlocking; + + if (auto endpoint = runtime_.EnsureEndpointRuntime(guid)) { + endpoint->UpdateConfig(dev); + } + + if (publisher_.EnsureNub(guid, dev, "RME-Stage5C")) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5C Core Audio endpoint published: 192000 Hz, 12 inputs, 12 outputs; streaming disabled GUID=0x%016llx", + guid); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5C failed to publish Core Audio endpoint GUID=0x%016llx", + guid); + } +} + IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { if (guid == 0) return nullptr; @@ -180,6 +239,9 @@ IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { if (integration == DeviceIntegrationMode::kHardcodedNub) { return &dice_; } + if (integration == DeviceIntegrationMode::kReadOnlyNub) { + return nullptr; + } return &avc_; } @@ -187,6 +249,16 @@ IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { IOReturn AudioCoordinator::StartStreaming(uint64_t guid) noexcept { if (guid == 0) return kIOReturnBadArgument; + const auto* record = registry_.FindByGuid(guid); + if (record && + DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) == + DeviceIntegrationMode::kReadOnlyNub) { + ASFW_LOG_WARNING(Audio, + "[RME] Stage 5C endpoint is preflight-only; no-CIP wire format is verified but OHCI streaming remains blocked GUID=0x%016llx", + guid); + return kIOReturnUnsupported; + } + bool setActive = false; if (lock_) { IOLockLock(lock_); diff --git a/ASFWDriver/Audio/Core/AudioCoordinator.hpp b/ASFWDriver/Audio/Core/AudioCoordinator.hpp index 647e4cd6..54a5e750 100644 --- a/ASFWDriver/Audio/Core/AudioCoordinator.hpp +++ b/ASFWDriver/Audio/Core/AudioCoordinator.hpp @@ -69,6 +69,7 @@ class AudioCoordinator final : public Discovery::IDeviceObserver, private: [[nodiscard]] IAudioBackend* BackendForGuid(uint64_t guid) noexcept; + void EnsureRMEReadOnlyNub(uint64_t guid) noexcept; AudioNubPublisher publisher_; DiceAudioBackend dice_; diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp index ddbb7317..40ddacb3 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp @@ -13,6 +13,7 @@ #include "Config/AudioProfileRegistry.hpp" #include "../../Common/DriverKitOwnership.hpp" #include "../../Shared/Isoch/IsochAudioTransport.hpp" +#include "../../DeviceProfiles/Audio/AudioDeviceIds.hpp" #include #include @@ -64,6 +65,21 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { static_cast(in_flags)); auto& ivars = *this->ivars->driverIvars; + + // Stage 5C deliberately lets the FF800 enter the generic TX allocation, + // mapping, packetizer and prefill path. The Dext-side StartAudioStreaming + // branch then primes descriptors only and returns unsupported before any + // OHCI CommandPtr/RUN write or device communication start. + const bool isRmeStage5C = + ivars.device.vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + ivars.device.modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId; + if (isRmeStage5C) { + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5C: Core Audio StartIO entering host TX DMA/descriptor preflight; live streaming remains blocked GUID=0x%016llx", + ivars.device.guid); + } + __block kern_return_t kr = kIOReturnSuccess; ivars.workQueue->DispatchSync(^{ @@ -177,7 +193,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { const uint32_t numSlots = ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; const uint32_t maxPacketBytes = - 8u + static_cast(txConfig.framesPerDataPacket) * txConfig.dbs * 4u; + (txConfig.includeCipHeader ? 8u : 0u) + + static_cast(txConfig.framesPerDataPacket) * txConfig.dbs * 4u; const uint32_t interruptInterval = ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; @@ -231,6 +248,10 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { ivars.runtime.txSlotProvider.controlBlock = controlBlock; ivars.runtime.txSlotProvider.numSlots = numSlots; ivars.runtime.txSlotProvider.slotStrideBytes = maxPacketBytes; + ivars.runtime.txSlotProvider.isochTag = + txConfig.includeCipHeader + ? ASFW::IsochTransport::IsochPacketTag::kCip + : ASFW::IsochTransport::IsochPacketTag::kNoCipHeader; ivars.runtime.txExecutionTimeline.controlBlock = controlBlock; @@ -282,7 +303,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { const uint32_t numSlots2 = ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; const uint32_t maxPacketBytes2 = - 8u + static_cast(txConfig2.framesPerDataPacket) * txConfig2.dbs * 4u; + (txConfig2.includeCipHeader ? 8u : 0u) + + static_cast(txConfig2.framesPerDataPacket) * txConfig2.dbs * 4u; const uint32_t interruptInterval2 = ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; @@ -318,6 +340,10 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { ivars.runtime.txSlotProviderSecondary.controlBlock = controlBlock2; ivars.runtime.txSlotProviderSecondary.numSlots = numSlots2; ivars.runtime.txSlotProviderSecondary.slotStrideBytes = maxPacketBytes2; + ivars.runtime.txSlotProviderSecondary.isochTag = + txConfig2.includeCipHeader + ? ASFW::IsochTransport::IsochPacketTag::kCip + : ASFW::IsochTransport::IsochPacketTag::kNoCipHeader; if (!ivars.runtime.txStreamEngineSecondary.Configure(*profile, txConfig2)) { kr = failStart(kIOReturnError, "ConfigureTxStreamEngine2"); @@ -545,6 +571,18 @@ kern_return_t ASFWAudioDevice::StopIO(IOUserAudioStartStopFlags in_flags) { static_cast(in_flags)); auto& ivars = *this->ivars->driverIvars; + + // Matching Stage 4A no-op teardown for a StartIO request that was rejected + // before the generic transport or AudioDriverKit state was entered. + if (ivars.device.vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + ivars.device.modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId) { + ASFW_LOG( + Audio, + "[RME] Stage 4A: StopIO acknowledged with no active stream GUID=0x%016llx", + ivars.device.guid); + return kIOReturnSuccess; + } + __block kern_return_t kr = kIOReturnSuccess; ivars.workQueue->DispatchSync(^{ diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp index 30235828..157c2c4f 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp @@ -105,6 +105,8 @@ class DextTxSlotProvider final : public ASFW::Protocols::Audio::AMDTP::IAmdtpTxS ASFW::IsochTransport::TxStreamControl* controlBlock{nullptr}; uint32_t numSlots{0}; uint32_t slotStrideBytes{0}; + ASFW::IsochTransport::IsochPacketTag isochTag{ + ASFW::IsochTransport::IsochPacketTag::kCip}; bool AcquireWritableSlot( uint32_t packetIndex, @@ -132,27 +134,19 @@ class DextTxSlotProvider final : public ASFW::Protocols::Audio::AMDTP::IAmdtpTxS meta.packetIndex = packet.packetIndex; meta.payloadLength = packet.byteCount; - // immediateData[0] = isoch packet header: spd=2 (S400) at [18:16], - // tag=1 (standard CIP) at [15:14], tcode=0xA (isoch data block - // transmit) at [7:4], sy=0. The channel at [13:8] is deliberately - // left as a placeholder: the owning transport ring always stamps its - // configured channel immediately before publishing the descriptor. - // The speed field is mandatory — omitting it transmits at S100 and - // produces a header the device/analyzer treats as malformed. - // Cross-validated with Linux: firewire/ohci.h:277-286 and - // firewire/ohci.c:3377-3381. - const uint32_t isochHeaderQ0 = (static_cast(2 & 0x7) << 16) | - (static_cast(1 & 0x3) << 14) | - (static_cast(0xA & 0xF) << 4); + // immediateData[0] = OHCI isoch packet header. Tag 1 identifies a + // standard CIP packet; tag 0 identifies raw payload with no CIP header + // (the Fireface 800 former-generation wire format). The owning ring + // stamps the configured channel immediately before DMA publication. + const uint32_t isochHeaderQ0 = + ASFW::IsochTransport::BuildIsochTxHeaderQ0(isochTag); meta.immediateHeader[0] = OSSwapHostToLittleInt32(isochHeaderQ0); - // immediateData[1] = data_length (payload bytes) in bits [31:16]. The - // CIP header is the first 8 bytes of the payload buffer and is shipped - // by the OUTPUT_LAST descriptor — it does NOT belong in the packet - // header immediate. Cross-validated with Linux: - // firewire/ohci.h:287-288 and firewire/ohci.c:3383. + // immediateData[1] = data_length (all bytes transmitted after the OHCI + // immediate packet header) in bits [31:16]. This is 0 for a true empty + // no-CIP idle cycle, raw PCM bytes for tag 0, or CIP+payload for tag 1. meta.immediateHeader[1] = OSSwapHostToLittleInt32( - static_cast(packet.byteCount & 0xFFFF) << 16); + ASFW::IsochTransport::BuildIsochTxHeaderQ1(packet.byteCount)); // Compute expectedGen and release-store commitGen const uint64_t gen = ASFW::IsochTransport::ExpectedCommitGen(packet.packetIndex, numSlots); diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp index 6a9ad5a5..3eabcc4a 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp @@ -12,6 +12,7 @@ #include "ASFWAudioDriverPrivate.hpp" #include "../../Common/TimingUtils.hpp" #include "../../Logging/Logging.hpp" +#include "../../DeviceProfiles/Audio/AudioDeviceIds.hpp" #include @@ -502,11 +503,20 @@ void PrefillTxRingBeforeStart(ASFWAudioDriver_IVars& ivars) noexcept { // the producer gets its first turn. Steady state still targets completion // + kTxPreparationLeadPackets. ASFW::Protocols::Audio::AMDTP::AmdtpTimingState timing{}; - timing.replayValid = true; timing.txClockValid = false; - timing.disposition = - ASFW::Protocols::Audio::AMDTP:: - AmdtpPacketDisposition::NoData; + const bool isRmeFireface800 = + ivars.device.vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + ivars.device.modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId; + + // Normal devices intentionally prefill NO-DATA through the replay override. + // The FF800 is different: it needs its native 192 kHz blocking cadence + // (6 data packets / 2 true empty cycles per 8 bus cycles). Leaving + // replayValid=true with replayDataBlocks=0 forces every packet empty and + // bypasses the cadence engine entirely. + timing.replayValid = !isRmeFireface800; + timing.disposition = isRmeFireface800 + ? ASFW::Protocols::Audio::AMDTP::AmdtpPacketDisposition::Data + : ASFW::Protocols::Audio::AMDTP::AmdtpPacketDisposition::NoData; uint32_t prepared = 0; for (uint64_t packetIndex = 0; @@ -527,9 +537,10 @@ void PrefillTxRingBeforeStart(ASFWAudioDriver_IVars& ivars) noexcept { } ASFW_LOG(DirectAudio, - "ADK DBG TX prefill seeded %u/%u committed NO-DATA packets before isoch start (steadyLead=%u)", + "ADK DBG TX prefill seeded %u/%u committed %{public}s packets before isoch start (steadyLead=%u)", prepared, numSlots, + isRmeFireface800 ? "FF800 blocking cadence" : "NO-DATA", ASFW::IsochTransport::AudioTimingGeometry:: kTxPreparationLeadPackets); } diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp index 3c681590..87ff4525 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp @@ -496,7 +496,106 @@ kern_return_t IMPL(ASFWAudioNub, StartAudioStreaming) return kIOReturnNotReady; } + // Resolve the runtime binding before the global auto-start gate. Stage 5H + // is a bounded integration test: prepare one immutable circular 48-cycle + // D-D-D-S silence ring with RUN clear, start the already-verified FF800 + // communication engine, sustain the ring for exactly 100 ms, stop OHCI + // first, then stop the device and release the held IRM route. Core Audio + // streaming remains rejected after the bounded cleanup completes. + ProtocolRuntimeBinding binding{}; + const kern_return_t bindingStatus = ResolveProtocolRuntimeBinding(ivars, binding); + if (bindingStatus == kIOReturnSuccess && binding.device != nullptr) { + const auto integration = ASFW::Audio::DeviceProtocolFactory::LookupIntegrationMode( + binding.device->vendorId, binding.device->modelId); + if (integration == ASFW::Audio::DeviceIntegrationMode::kReadOnlyNub && + binding.device->vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + binding.device->modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId) { + ASFWDriver* parent = GetParentASFWDriver(ivars); + auto* ctx = parent + ? static_cast(parent->GetServiceContext()) + : nullptr; + ASFW::Audio::PlaybackPreflightRoute route{}; + if (!ctx || !ctx->deps.hardware || !binding.protocol || + !binding.protocol->GetPlaybackPreflightRoute(route)) { + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5H bounded circular silent cadence run deferred: verified IRM route is not ready GUID=0x%016llx", + ivars->guid); + return kIOReturnUnsupported; + } + if (route.channel > 63U || route.bandwidthUnits != 1286U || + route.sampleRateHz != 192000U || + !route.deviceCommunicationStopped) { + ASFW_LOG_ERROR( + Audio, + "[RME] Stage 5H bounded circular silent cadence run rejected invalid route channel=%u bandwidth=%u/1286 rate=%u deviceStopped=%u", + route.channel, + route.bandwidthUnits, + route.sampleRateHz, + route.deviceCommunicationStopped ? 1U : 0U); + return kIOReturnUnsupported; + } + + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5H bounded circular silent cadence run starting GUID=0x%016llx channel=%u bandwidth=%u rate=%u descriptors=48 dataPacketsPerSweep=36 skipPerSweep=12 durationMs=100 deviceEngineStarting=1", + ivars->guid, + route.channel, + route.bandwidthUnits, + route.sampleRateHz); + + const IOReturn integrationKr = + binding.protocol->RunBoundedPlaybackIntegrationPreflight( + route, + [ctx, route]() -> IOReturn { + IOReturn kr = ctx->isoch.PrepareTransmit( + route.channel, *ctx->deps.hardware, 0U); + if (kr == kIOReturnSuccess) { + kr = ctx->isoch.PrimePreparedTransmitForPreflight(); + } + if (kr == kIOReturnSuccess) { + kr = ctx->isoch + .PrepareTransmitBoundedCircularSilenceCadenceForPreflight(); + } + return kr; + }, + [ctx]() -> IOReturn { + // Stage 5H keeps the immutable 6 ms ring active for exactly + // 100 ms, proving repeated wraps without refill. + return ctx->isoch + .RunPreparedTransmitBoundedCircularSilenceCadenceForPreflight(100U); + }, + [ctx]() -> IOReturn { + // Idempotent final host cleanup. The RUN callback already + // clears RUN and waits for ACTIVE; this also handles every + // preparation/start failure path before device stop. + return ctx->isoch + .CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight(); + }); + + // The protocol call above is synchronous: OHCI, FF800 and IRM + // cleanup all finish before StartAudioStreaming returns and before + // AudioDriverKit unwinds/frees the shared TX slabs. + if (integrationKr == kIOReturnSuccess) { + ASFW_LOG( + Audio, + "[RME] ✅ Stage 5H bounded circular silent playback integration completed GUID=0x%016llx channel=%u durationMs=100; Core Audio StartIO remains rejected", + ivars->guid, + route.channel); + } else { + ASFW_LOG_ERROR( + Audio, + "[RME] Stage 5H bounded circular silent playback integration failed GUID=0x%016llx channel=%u kr=0x%x; Core Audio StartIO remains rejected", + ivars->guid, + route.channel, + integrationKr); + } + return kIOReturnUnsupported; + } + } + // Auto-start gating (Info.plist + runtime), useful for debugging discovery without streams. + // Stage 5H has already been handled above; it executes one bounded 100 ms circular silence run and never enables live Core Audio streaming. if (!ASFW::LogConfig::Shared().IsAudioAutoStartEnabled()) { ASFW_LOG(Audio, "ASFWAudioNub: StartAudioStreaming skipped (auto-start disabled) GUID=0x%016llx", @@ -509,11 +608,15 @@ kern_return_t IMPL(ASFWAudioNub, StartAudioStreaming) // Linux resets FCP before its OXFW stream restart (oxfw.c:279-287), and // Apple likewise waits for its resumed state before reconnecting. DICE's // hardcoded-nub path does not use FCP and remains independent. - ProtocolRuntimeBinding binding{}; - const kern_return_t bindingStatus = ResolveProtocolRuntimeBinding(ivars, binding); if (bindingStatus == kIOReturnSuccess && binding.device != nullptr) { const auto integration = ASFW::Audio::DeviceProtocolFactory::LookupIntegrationMode( binding.device->vendorId, binding.device->modelId); + if (integration == ASFW::Audio::DeviceIntegrationMode::kReadOnlyNub) { + ASFW_LOG_WARNING(Audio, + "ASFWAudioNub: read-only audio endpoint transport remains disabled GUID=0x%016llx", + ivars->guid); + return kIOReturnUnsupported; + } if (integration != ASFW::Audio::DeviceIntegrationMode::kHardcodedNub) { auto* transport = binding.avcDiscovery ? binding.avcDiscovery->GetFCPTransportForNodeID(binding.device->nodeId) @@ -554,6 +657,27 @@ kern_return_t IMPL(ASFWAudioNub, StopAudioStreaming) return kIOReturnNotReady; } + ProtocolRuntimeBinding binding{}; + if (ResolveProtocolRuntimeBinding(ivars, binding) == kIOReturnSuccess && + binding.device != nullptr && + binding.device->vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + binding.device->modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId) { + ASFWDriver* parent = GetParentASFWDriver(ivars); + auto* ctx = parent + ? static_cast(parent->GetServiceContext()) + : nullptr; + if (ctx) { + (void)ctx->isoch.StopTransmit(); + } + if (auto endpoint = FindEndpointRuntime(ivars)) { + endpoint->MarkStreaming(false); + } + ASFW_LOG(Audio, + "[RME] Stage 5H stop cleanup complete; bounded circular transmit context is stopped and no live audio stream exists GUID=0x%016llx", + ivars->guid); + return kIOReturnSuccess; + } + auto* coordinator = GetAudioCoordinator(ivars); if (!coordinator) { return kIOReturnNotReady; diff --git a/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp b/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp index 6a6bef2f..780a0ca9 100644 --- a/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp @@ -5,12 +5,109 @@ // Global profile registry dispatcher. #include "AudioProfileRegistry.hpp" +#include "AudioStreamProfile.hpp" #include "AVC/ApogeeDuetProfile.hpp" #include "AVC/Phase88Profile.hpp" #include "DICE/DiceProfileRegistry.hpp" #include "../../../DeviceProfiles/Audio/AudioDeviceIds.hpp" +namespace { + +class RMEFireface800Stage3Profile final : public ASFW::Isoch::Audio::IAudioStreamProfile { +public: + [[nodiscard]] const char* Name() const noexcept override { + return "RME Fireface 800 (Stage 5C)"; + } + + [[nodiscard]] ASFW::Encoding::AudioWireFormat TxWireFormat() const noexcept override { + return ASFW::Encoding::AudioWireFormat::kRawPcm24In32; + } + + [[nodiscard]] ASFW::Encoding::AudioWireFormat RxWireFormat() const noexcept override { + return ASFW::Encoding::AudioWireFormat::kRawPcm24In32; + } + + [[nodiscard]] uint32_t TxChannelCount() const noexcept override { return 12; } + [[nodiscard]] uint32_t RxChannelCount() const noexcept override { return 12; } + [[nodiscard]] uint32_t TxMidiSlots() const noexcept override { return 0; } + [[nodiscard]] uint32_t RxMidiSlots() const noexcept override { return 0; } + [[nodiscard]] uint32_t TxDbs() const noexcept override { return 12; } + [[nodiscard]] uint32_t RxDbs() const noexcept override { return 12; } + + [[nodiscard]] uint32_t TxSafetyOffsetFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 64; + } + + [[nodiscard]] uint32_t RxSafetyOffsetFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 64; + } + + [[nodiscard]] uint32_t TxReportedLatencyFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 128; + } + + [[nodiscard]] uint32_t RxReportedLatencyFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 128; + } + + [[nodiscard]] std::vector SupportedSampleRates() const override { + // Stage 5C intentionally publishes only the hardware-verified current mode. + return {192000u}; + } + + [[nodiscard]] bool BuildDefaultTxStreamConfig( + ASFW::Isoch::Audio::AudioStreamConfig& outConfig) const noexcept override { + outConfig = {}; + outConfig.direction = ASFW::Isoch::Audio::AudioStreamDirection::HostToDevice; + outConfig.sampleRate = 192000u; + outConfig.streamMode = ASFW::Encoding::StreamMode::kBlocking; + outConfig.sid = 0u; + outConfig.pcmChannels = 12u; + outConfig.dbs = 12u; + outConfig.midiSlots = 0u; + outConfig.framesPerDataPacket = 32u; + outConfig.fdf = 0u; + outConfig.fmt = 0u; + outConfig.includeCipHeader = false; + return true; + } + + [[nodiscard]] bool BuildDefaultRxStreamConfig( + ASFW::Isoch::Audio::AudioStreamConfig& outConfig) const noexcept override { + outConfig = {}; + outConfig.direction = ASFW::Isoch::Audio::AudioStreamDirection::DeviceToHost; + outConfig.sampleRate = 192000u; + outConfig.streamMode = ASFW::Encoding::StreamMode::kBlocking; + outConfig.sid = 0u; + outConfig.pcmChannels = 12u; + outConfig.dbs = 12u; + outConfig.midiSlots = 0u; + outConfig.framesPerDataPacket = 32u; + outConfig.fdf = 0u; + outConfig.fmt = 0u; + outConfig.includeCipHeader = false; + return true; + } + + [[nodiscard]] ASFW::Isoch::Audio::AudioStreamTxPolicy + TxStreamPolicy() const noexcept override { + ASFW::Isoch::Audio::AudioStreamTxPolicy policy{}; + policy.hostToDevicePcmEncoding = ASFW::Encoding::AudioWireFormat::kRawPcm24In32; + policy.variableDbs = false; + policy.initializeNonAudioSlots = false; + policy.preserveFdfInNoDataPackets = false; + policy.emptyPacketsDuringIdle = true; + return policy; + } +}; + +} // namespace + namespace ASFW::Isoch::Audio { const IAudioDeviceProfile* AudioProfileRegistry::FindProfile(uint32_t vendorId, @@ -28,6 +125,12 @@ const IAudioDeviceProfile* AudioProfileRegistry::FindProfile(uint32_t vendorId, return &phase88Profile; } + static RMEFireface800Stage3Profile fireface800Profile{}; + if (vendorId == DeviceProfiles::Audio::kRMEVendorId && + modelId == DeviceProfiles::Audio::kFireface800ModelId) { + return &fireface800Profile; + } + // Map identity to the DICE family structures DICE::DiceDeviceIdentity identity{ .guid = guid, diff --git a/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp index d36458cf..a7cb83d0 100644 --- a/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp +++ b/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp @@ -27,6 +27,12 @@ struct AudioStreamConfig final { uint8_t framesPerDataPacket{8}; uint8_t fdf{0x02}; uint8_t fmt{0x10}; + + // Most AMDTP streams carry the standard 8-byte CIP header. + // Former-generation RME Fireface streams use raw no-CIP payloads. + // Default true preserves every existing profile. + bool includeCipHeader{true}; + uint8_t sourceChannelOffset{0}; }; diff --git a/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp b/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp index 56524acd..afe5c2ac 100644 --- a/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp +++ b/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp @@ -16,11 +16,13 @@ AMDTP::AmdtpStreamConfig DiceStreamConfigMapper::ToAmdtpConfig( config.fmt = streamConfig.fmt; config.fdf = streamConfig.fdf; config.framesPerDataPacket = streamConfig.framesPerDataPacket; + config.includeCipHeader = streamConfig.includeCipHeader; config.sourceChannelOffset = streamConfig.sourceChannelOffset; - // 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 + + // Compute the true max packet size. Standard AMDTP uses an 8-byte CIP + // header; former-generation RME Fireface streams use raw no-CIP payloads. + // Do not use the AmdtpStreamConfig default (512) — it is too small for + // high-channel devices. + config.maxPacketBytes = (streamConfig.includeCipHeader ? 8u : 0u) + static_cast(streamConfig.framesPerDataPacket) * streamConfig.dbs * 4u; return config; } diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp index 44f96a00..025b51ff 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp @@ -8,6 +8,7 @@ #include "DICE/TCAT/DICETcatProtocol.hpp" #include "Oxford/Apogee/ApogeeDuetProtocol.hpp" #include "BeBoB/Phase88Protocol.hpp" +#include "RME/RMEFireface800Protocol.hpp" #include "../../Logging/Logging.hpp" namespace ASFW::Audio { @@ -79,6 +80,15 @@ std::unique_ptr DeviceProtocolFactory::Create( busOps, busInfo, nodeId, nullptr, irmClient, cmpClient, deviceGuid); } + if (vendorId == DeviceProfiles::Audio::kRMEVendorId && + modelId == DeviceProfiles::Audio::kFireface800ModelId) { + ASFW_LOG(Audio, + "Creating Stage 5C RME Fireface 800 no-CIP wire-format preflight vendor=0x%06x model=0x%06x node=0x%04x", + vendorId, modelId, nodeId); + return std::make_unique( + busOps, busInfo, nodeId, deviceGuid, irmClient); + } + if (vendorId == kTerraTecVendorId && modelId == kPhase88RackFwModelId) { ASFW_LOG(Audio, "Creating Phase88Protocol BeBoB/CMP backend vendor=0x%06x model=0x%06x node=0x%04x", diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp index 167c7918..dfc14343 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp @@ -46,6 +46,8 @@ class DeviceProtocolFactory { // Identity constants are defined in DeviceProfiles/Audio/AudioDeviceIds.hpp (single // source of truth) and re-exported here so existing DeviceProtocolFactory::kX call // sites keep resolving. + static constexpr uint32_t kRMEVendorId = DeviceProfiles::Audio::kRMEVendorId; + static constexpr uint32_t kFireface800ModelId = DeviceProfiles::Audio::kFireface800ModelId; static constexpr uint32_t kFocusriteVendorId = DeviceProfiles::Audio::kFocusriteVendorId; static constexpr uint32_t kSPro40ModelId = DeviceProfiles::Audio::kSPro40ModelId; static constexpr uint32_t kLiquidS56ModelId = DeviceProfiles::Audio::kLiquidS56ModelId; @@ -68,6 +70,8 @@ class DeviceProtocolFactory { DeviceProfiles::Audio::kStudioLive1602ModelId; static constexpr uint32_t kFocusriteGuidModelSPro40Tcd3070 = DeviceProfiles::Audio::kFocusriteGuidModelSPro40Tcd3070; + static constexpr const char* kRMEVendorName = DeviceProfiles::Audio::kRMEVendorName; + static constexpr const char* kFireface800ModelName = DeviceProfiles::Audio::kFireface800ModelName; static constexpr const char* kFocusriteVendorName = DeviceProfiles::Audio::kFocusriteVendorName; static constexpr const char* kSPro40ModelName = DeviceProfiles::Audio::kSPro40ModelName; static constexpr const char* kLiquidS56ModelName = DeviceProfiles::Audio::kLiquidS56ModelName; diff --git a/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp b/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp index 7396b938..9529fec4 100644 --- a/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp @@ -27,6 +27,13 @@ class IDuplexDeviceControl; namespace ASFW::Audio { +struct PlaybackPreflightRoute final { + uint8_t channel{0xFFU}; + uint32_t bandwidthUnits{0U}; + uint32_t sampleRateHz{0U}; + bool deviceCommunicationStopped{false}; +}; + /// Interface for device-specific protocol handlers /// /// Device protocols are instantiated by DeviceProtocolFactory when a @@ -74,6 +81,7 @@ class IDeviceProtocol { } using VoidCallback = std::function; + using BoundedPlaybackStep = std::function; /// Optional bring-up hook to prepare device-side duplex state at 48kHz. /// Drivers can call this before any IRM reservation or host IR/IT startup. @@ -109,6 +117,30 @@ class IDeviceProtocol { return nullptr; } + /// Optional bounded bring-up route held by a device protocol. This is only + /// for staged transport validation; it is not a general streaming API. + virtual bool GetPlaybackPreflightRoute(PlaybackPreflightRoute& outRoute) const { + (void)outRoute; + return false; + } + + /// Optional staged integration hook. The protocol starts its already- + /// verified device communication engine, invokes one bounded host transmit + /// validation window, then stops the device and releases the held playback + /// IRM route. + /// This is not a live streaming API and must never make StartIO succeed. + virtual IOReturn RunBoundedPlaybackIntegrationPreflight( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep runHostBurst, + BoundedPlaybackStep cleanupHost) { + (void)route; + (void)prepareHost; + (void)runHostBurst; + (void)cleanupHost; + return kIOReturnUnsupported; + } + /// Optional protocol-neutral duplex control interface used by the audio lifecycle. virtual IDuplexDeviceControl* AsDuplexDeviceControl() noexcept { return nullptr; diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp index 3865a672..d5b45625 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp @@ -123,7 +123,8 @@ void AmdtpPayloadWriter::WriteFloat32Interleaved( // field set, so this cannot underflow. const uint32_t frameInPacket = static_cast(absoluteFrame - snap.firstAudioFrame); - uint8_t* dest = snap.packetBytes + kCipHeaderBytes + + const uint32_t headerBytes = streamConfig_.includeCipHeader ? kCipHeaderBytes : 0U; + uint8_t* dest = snap.packetBytes + headerBytes + frameInPacket * snap.dbs * kBytesPerSlot; const uint32_t pcmSlots = (streamConfig_.pcmChannels < snap.dbs) diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp index 1ebe88dc..8194df3a 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp @@ -67,8 +67,9 @@ bool AmdtpTxPacketizer::Configure(const AmdtpStreamConfig& streamConfig, } const uint32_t dataPacketBytes = - kCipHeaderBytes + static_cast(config.framesPerDataPacket) * - config.dbs * kBytesPerSlot; + (config.includeCipHeader ? kCipHeaderBytes : 0U) + + static_cast(config.framesPerDataPacket) * + config.dbs * kBytesPerSlot; if (dataPacketBytes > config.maxPacketBytes) { return false; } @@ -156,10 +157,13 @@ bool AmdtpTxPacketizer::PrepareNextPacket(TxPacketSlotView slot, const uint32_t payloadBytes = static_cast(frames) * streamConfig_.dbs * kBytesPerSlot; - const bool isEmptyPacket = !isData && txPolicy_.emptyPacketsDuringIdle; + // CIP_NO_HEADER streams have no representation for a header-only no-data + // packet; their idle cycles are genuine zero-length packets. + const bool isEmptyPacket = + !isData && (txPolicy_.emptyPacketsDuringIdle || !streamConfig_.includeCipHeader); const uint32_t byteCount = - isEmptyPacket ? 0 : (isData ? (kCipHeaderBytes + payloadBytes) : kCipHeaderBytes); + isEmptyPacket ? 0 : (isData ? (HeaderBytes() + payloadBytes) : HeaderBytes()); if (slot.capacityBytes < byteCount) { return false; // no state advanced; caller may retry @@ -181,7 +185,9 @@ bool AmdtpTxPacketizer::PrepareNextPacket(TxPacketSlotView slot, ? timing.nextDataSyt : IEC61883::SytFormatter::kNoInfo; - WriteCipHeader(slot.bytes, cipBuilder_.BuildData(dbc, outPacket.syt)); + if (streamConfig_.includeCipHeader) { + WriteCipHeader(slot.bytes, cipBuilder_.BuildData(dbc, outPacket.syt)); + } WriteDataPacketDefaults(slot.bytes, slot.capacityBytes, payloadBytes); if (!timeline_->ExposeDataPacket(outPacket, slot.bytes, @@ -199,6 +205,8 @@ bool AmdtpTxPacketizer::PrepareNextPacket(TxPacketSlotView slot, timeline_->MarkNoDataPacket(slot.packetIndex); } else { // CIP-header-only: no payload, even as padding (DICE-II rejects it). + // This branch is unreachable for CIP_NO_HEADER streams because + // those force isEmptyPacket above. WriteCipHeader(slot.bytes, cipBuilder_.BuildNoData(dbc)); timeline_->MarkNoDataPacket(slot.packetIndex); // DBC deliberately not advanced. @@ -226,7 +234,7 @@ void AmdtpTxPacketizer::WriteDataPacketDefaults(uint8_t* packetBytes, uint32_t payloadBytes) noexcept { (void)packetCapacityBytes; // capacity validated by the caller - uint8_t* payload = packetBytes + kCipHeaderBytes; + uint8_t* payload = packetBytes + HeaderBytes(); if (txPolicy_.clearPayloadBeforeExposure) { for (uint32_t i = 0; i < payloadBytes; ++i) { @@ -253,8 +261,12 @@ void AmdtpTxPacketizer::WriteCipHeader( WriteBE32(packetBytes + 4, header.q1); } +uint32_t AmdtpTxPacketizer::HeaderBytes() const noexcept { + return streamConfig_.includeCipHeader ? kCipHeaderBytes : 0U; +} + uint32_t AmdtpTxPacketizer::DataPacketBytes() const noexcept { - return kCipHeaderBytes + PayloadBytes(); + return HeaderBytes() + PayloadBytes(); } uint32_t AmdtpTxPacketizer::PayloadBytes() const noexcept { diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp index 0936beca..26762b41 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp @@ -55,6 +55,7 @@ class AmdtpTxPacketizer final { void WriteCipHeader(uint8_t* packetBytes, const IEC61883::CipHeaderWords& header) noexcept; + [[nodiscard]] uint32_t HeaderBytes() const noexcept; [[nodiscard]] uint32_t DataPacketBytes() const noexcept; [[nodiscard]] uint32_t PayloadBytes() const noexcept; diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp index beca2ee9..3b662376 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp @@ -35,6 +35,12 @@ struct AmdtpStreamConfig final { uint8_t framesPerDataPacket{8}; uint32_t maxPacketBytes{512}; + // Most IEC 61883-6/AMDTP streams carry the standard 8-byte CIP header. + // RME Fireface former-generation streams use CIP_NO_HEADER: the isoch + // payload is raw PCM data blocks and no-data cycles are genuine empty + // packets. Keep the default true so every existing device is unchanged. + bool includeCipHeader{true}; + // First host buffer channel this stream encodes. For a multi-stream device // (Venice F32 = 2×16) the 32-ch host output buffer is split across streams: // stream 0 reads channels [0, pcmChannels), stream 1 reads [16, 16+pcmChannels), diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp index 4a6ed946..c4566f51 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp @@ -56,7 +56,13 @@ inline constexpr uint32_t kStudioLive1642ModelId = 0x000010; inline constexpr uint32_t kStudioLive2442ModelId = 0x000012; inline constexpr uint32_t kStudioLive3242ModelId = 0x000014; +// ---- RME (vendor-specific Fireface family) ---- +inline constexpr uint32_t kRMEVendorId = 0x000a35; +inline constexpr uint32_t kFireface800ModelId = 0x101800; + // ---- Display names ---- +inline constexpr const char* kRMEVendorName = "RME"; +inline constexpr const char* kFireface800ModelName = "Fireface 800"; inline constexpr const char* kFocusriteVendorName = "Focusrite"; inline constexpr const char* kSPro40ModelName = "Saffire Pro 40"; inline constexpr const char* kLiquidS56ModelName = "Liquid Saffire 56"; diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp index 2f8b1ee3..04f44290 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp @@ -19,6 +19,7 @@ #include "Vendors/FocusriteAudioProfiles.hpp" #include "Vendors/MidasAudioProfiles.hpp" #include "Vendors/PreSonusAudioProfiles.hpp" +#include "Vendors/RMEAudioProfiles.hpp" #include "Vendors/TerraTecAudioProfiles.hpp" #include @@ -36,6 +37,7 @@ class AudioProfileRegistry final { if (auto hint = Alesis::LookupIdentity(query)) { return hint; } if (auto hint = Midas::LookupIdentity(query)) { return hint; } if (auto hint = PreSonus::LookupIdentity(query)) { return hint; } + if (auto hint = RME::LookupIdentity(query)) { return hint; } if (auto hint = TerraTec::LookupIdentity(query)) { return hint; } if (auto hint = Focusrite::LookupIdentityByGuid(query)) { return hint; } return std::nullopt; @@ -51,6 +53,7 @@ class AudioProfileRegistry final { if (auto hint = Alesis::LookupAudioProfile(query)) { return hint; } if (auto hint = Midas::LookupAudioProfile(query)) { return hint; } if (auto hint = PreSonus::LookupAudioProfile(query)) { return hint; } + if (auto hint = RME::LookupAudioProfile(query)) { return hint; } if (auto hint = TerraTec::LookupAudioProfile(query)) { return hint; } return std::nullopt; } diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp index f594802d..c8cd911d 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp @@ -21,6 +21,7 @@ enum class AudioIntegrationMode : uint8_t { kNone = 0, // Recognized but not driven (deferred multistream models). kHardcodedNub, // Vendor-specific audio backend (DICE/TCAT), no AV/C. kAVCDriven, // AV/C discovery drives topology; vendor protocol adds extra controls. + kReadOnlyNub, // Publish a Core Audio endpoint, but deliberately block streaming. }; /// Audio protocol family a device belongs to. Forward-looking: consumed today only for diff --git a/ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp b/ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp new file mode 100644 index 00000000..5b8a4cbf --- /dev/null +++ b/ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 ASFireWire Project +// +// RMEAudioProfiles.hpp - RME FireWire audio device identity and staged support policy. +// +// Stage 4B recognizes the Fireface 800, publishes a Core Audio endpoint, verifies STF, +// reserves the host-to-device IRM route, and programs RX packet geometry. Streaming +// remains deliberately blocked: no device TX allocation or isoch start is performed. + +#pragma once + +#include "../../Common/DeviceProfileTypes.hpp" +#include "../AudioDeviceIds.hpp" +#include "../AudioProfileTypes.hpp" + +#include + +namespace ASFW::DeviceProfiles::Audio::RME { + +[[nodiscard]] constexpr std::optional +LookupIdentity(const DeviceProfileQuery& query) noexcept { + if (query.vendorId != kRMEVendorId || query.modelId != kFireface800ModelId) { + return std::nullopt; + } + + return DeviceIdentityHint{.vendorId = query.vendorId, + .modelId = query.modelId, + .vendorName = kRMEVendorName, + .modelName = kFireface800ModelName, + .source = MatchSource::VendorModel}; +} + +[[nodiscard]] constexpr std::optional +LookupAudioProfile(const DeviceProfileQuery& query) noexcept { + if (query.vendorId != kRMEVendorId || query.modelId != kFireface800ModelId) { + return std::nullopt; + } + + // Publish a Core Audio endpoint with the verified 192 kHz / 12x12 geometry, + // but keep transport disabled until the RME-specific isoch backend is implemented. + return AudioProfileHint{.family = AudioProtocolFamily::VendorSpecific, + .mode = AudioIntegrationMode::kReadOnlyNub, + .source = MatchSource::VendorModel}; +} + +} // namespace ASFW::DeviceProfiles::Audio::RME diff --git a/ASFWDriver/Discovery/DeviceManager.cpp b/ASFWDriver/Discovery/DeviceManager.cpp index baae1ae9..3f5ff458 100644 --- a/ASFWDriver/Discovery/DeviceManager.cpp +++ b/ASFWDriver/Discovery/DeviceManager.cpp @@ -553,6 +553,11 @@ std::shared_ptr DeviceManager::ResumeExistingDevice(const std::shared_ return nullptr; } + // A rediscovery may enrich identity after the initial minimal ROM pass (for + // example, model_id found in a Unit Directory). Refresh even when the device + // is already Ready so the UserClient and protocol selection see the new IDs. + device->RefreshIdentity(record); + if (device->IsSuspended()) { device->Resume(record.gen, record.nodeId, record.link); UpdateOperationalIndex(record.guid, record.gen, record.nodeId, "update"); diff --git a/ASFWDriver/Discovery/DeviceRegistry.cpp b/ASFWDriver/Discovery/DeviceRegistry.cpp index 9d0450d2..318f26d4 100644 --- a/ASFWDriver/Discovery/DeviceRegistry.cpp +++ b/ASFWDriver/Discovery/DeviceRegistry.cpp @@ -3,6 +3,9 @@ #include #include "../Logging/Logging.hpp" #include "../DeviceProfiles/Audio/AudioProfileRegistry.hpp" +#include "../ConfigROM/Parse/ConfigROMParser.hpp" + +#include namespace ASFW::Discovery { @@ -17,6 +20,39 @@ constexpr uint32_t kUnitSwVersion_SBP2 = 0x010483; // SBP-2 Unit_Sw_Version namespace { +[[nodiscard]] std::optional FindUnitModelIdInRawROM(const ConfigROM& rom) { + if (rom.rawQuadlets.empty()) { + return std::nullopt; + } + + const uint32_t rootDirStart = 1U + static_cast(rom.bib.busInfoLength); + for (const auto& rootEntry : rom.rootDirMinimal) { + if (rootEntry.key != CfgKey::Unit_Directory || rootEntry.leafOffsetQuadlets == 0) { + continue; + } + + const uint32_t absoluteOffset = rootDirStart + rootEntry.leafOffsetQuadlets; + if (absoluteOffset >= rom.rawQuadlets.size()) { + continue; + } + + const auto raw = std::span(rom.rawQuadlets.data(), rom.rawQuadlets.size()); + const auto parsed = ConfigROMParser::ParseDirectory(raw.subspan(absoluteOffset), 32); + if (!parsed) { + continue; + } + + for (const auto& entry : *parsed) { + if (entry.keyType == ASFW::FW::EntryType::kImmediate && + entry.keyId == ASFW::FW::ConfigKey::kModelId) { + return entry.value; + } + } + } + + return std::nullopt; +} + void PopulateDeviceIdentity(DeviceRecord& device, const ConfigROM& rom) { for (const auto& entry : rom.rootDirMinimal) { if (entry.key == CfgKey::VendorId) { @@ -35,13 +71,37 @@ void PopulateDeviceIdentity(DeviceRecord& device, const ConfigROM& rom) { if (unit.unitSwVersion != 0) { device.unitSwVersion = unit.unitSwVersion; } - if (device.unitSpecId.has_value() && device.unitSwVersion.has_value()) { - break; + + // Some devices, including the RME Fireface 800, publish model_id in the + // Unit Directory instead of the Root Directory. Preserve a root-level + // model_id when present, otherwise use the first usable unit model_id. + if (device.modelId == 0 && unit.modelId.has_value()) { + device.modelId = *unit.modelId; + } + } + + // Details discovery can complete with the raw Unit Directory present while the + // higher-level UnitDirectory metadata is still incomplete. Parse the raw unit + // directory as a final, generic fallback so immediate model_id entries are not lost. + if (device.modelId == 0) { + if (const auto rawModelId = FindUnitModelIdInRawROM(rom); rawModelId.has_value()) { + device.modelId = *rawModelId; + ASFW_LOG(Discovery, + "Recovered unit-directory model_id from raw Config ROM: 0x%06x", + device.modelId); } } device.vendorName = rom.vendorName; device.modelName = rom.modelName; + if (device.modelName.empty()) { + for (const auto& unit : rom.unitDirectories) { + if (unit.modelName.has_value() && !unit.modelName->empty()) { + device.modelName = *unit.modelName; + break; + } + } + } } void MaybeInferKnownIdentityFromGuid(DeviceRecord& device, Guid64 guid) { @@ -137,7 +197,8 @@ DeviceRecord& DeviceRegistry::UpsertFromROM(const ConfigROM& rom, const LinkPoli device.vendorId, device.modelId, static_cast(integrationMode)); - if (integrationMode == DeviceProfiles::Audio::AudioIntegrationMode::kHardcodedNub) { + if (integrationMode == DeviceProfiles::Audio::AudioIntegrationMode::kHardcodedNub || + integrationMode == DeviceProfiles::Audio::AudioIntegrationMode::kReadOnlyNub) { device.kind = DeviceKind::VendorSpecificAudio; device.isAudioCandidate = true; } else { diff --git a/ASFWDriver/Discovery/FWDevice.cpp b/ASFWDriver/Discovery/FWDevice.cpp index 541a6fa5..169b7c7c 100644 --- a/ASFWDriver/Discovery/FWDevice.cpp +++ b/ASFWDriver/Discovery/FWDevice.cpp @@ -116,6 +116,11 @@ std::vector FWDevice::ExtractUnitDirectory( entries.push_back(RomEntry{CfgKey::Unit_Sw_Version, value, keyType, 0}); } break; + case 0x17: // Model_ID + if (keyType == 0) { // Immediate + entries.push_back(RomEntry{CfgKey::ModelId, value, keyType, 0}); + } + break; case 0x14: // Logical_Unit_Number if (keyType == 0) { // Immediate entries.push_back(RomEntry{CfgKey::Logical_Unit_Number, value, keyType, 0}); @@ -222,6 +227,31 @@ void FWDevice::Resume(Generation newGen, uint16_t newNodeId, const LinkPolicy& n } } +void FWDevice::RefreshIdentity(const DeviceRecord& record) +{ + if (record.guid != guid_) { + return; + } + + const uint32_t previousVendorId = vendorId_; + const uint32_t previousModelId = modelId_; + + vendorId_ = record.vendorId; + modelId_ = record.modelId; + kind_ = record.kind; + vendorName_ = record.vendorName; + modelName_ = record.modelName; + isAudioCandidate_ = record.isAudioCandidate; + supportsAMDTP_ = record.supportsAMDTP; + + if (previousVendorId != vendorId_ || previousModelId != modelId_) { + ASFW_LOG(Discovery, + "FWDevice identity refreshed GUID=0x%016llx vendor=0x%06x->0x%06x " + "model=0x%06x->0x%06x", + guid_, previousVendorId, vendorId_, previousModelId, modelId_); + } +} + void FWDevice::Terminate() { if (state_ == State::Terminated) { diff --git a/ASFWDriver/Discovery/FWDevice.hpp b/ASFWDriver/Discovery/FWDevice.hpp index 05aad4b0..88017aa4 100644 --- a/ASFWDriver/Discovery/FWDevice.hpp +++ b/ASFWDriver/Discovery/FWDevice.hpp @@ -56,6 +56,7 @@ class FWDevice : public std::enable_shared_from_this { void Publish(); void Suspend(); void Resume(Generation newGen, uint16_t newNodeId, const LinkPolicy& newLink); + void RefreshIdentity(const DeviceRecord& record); void Terminate(); private: @@ -69,9 +70,9 @@ class FWDevice : public std::enable_shared_from_this { ) const; const Guid64 guid_; - const uint32_t vendorId_; - const uint32_t modelId_; - const DeviceKind kind_; + uint32_t vendorId_; + uint32_t modelId_; + DeviceKind kind_; std::string vendorName_; std::string modelName_; diff --git a/ASFWDriver/Info.plist b/ASFWDriver/Info.plist index d8d388bf..12b6fa36 100644 --- a/ASFWDriver/Info.plist +++ b/ASFWDriver/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 42 + 17 IOKitPersonalities ASFWAudioDriverService @@ -51,7 +51,7 @@ ASFWAudioNub ASFWAutoStartAudioStreams - + ASFWControllerVerbosity 1 ASFWDICEVerbosity @@ -87,7 +87,7 @@ IOClass IOUserService IOPCIMatch - 0x590111c1 + 0x823f104c IOPCITunnelCompatible IOProbeScore @@ -113,7 +113,7 @@ IOMatchCategory ASFWSCSIController IOPCIMatch - 0x590111c1 + 0x823f104c IOPCITunnelCompatible IOProviderClass diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index 48f0873e..19697406 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -303,11 +303,23 @@ kern_return_t DriverWiring::PrepareInterrupts(ASFWDriver& service, IOService* pr return kIOReturnBadArgument; } - auto status = pci->ConfigureInterrupts(kIOInterruptTypePCIMessagedX, 1, 1, 0); + auto status = pci->ConfigureInterrupts( + kIOInterruptTypePCIMessagedX, 1, 1, 0); + ASFW_LOG(Controller, + "PrepareInterrupts: MSI-X ConfigureInterrupts -> 0x%08x", + status); + if (status != kIOReturnSuccess) { - status = pci->ConfigureInterrupts(kIOInterruptTypePCIMessaged, 1, 1, 0); + status = pci->ConfigureInterrupts( + kIOInterruptTypePCIMessaged, 1, 1, 0); + ASFW_LOG(Controller, + "PrepareInterrupts: MSI ConfigureInterrupts -> 0x%08x", + status); + if (status != kIOReturnSuccess) { - return status; + ASFW_LOG(Controller, + "PrepareInterrupts: MSI-X/MSI unavailable; " + "trying existing interrupt index 0"); } } } @@ -320,7 +332,12 @@ kern_return_t DriverWiring::PrepareInterrupts(ASFWDriver& service, IOService* pr ctx.interruptAction = OSSharedPtr(action, OSNoRetain); } - auto kr = intrMgr->Initialise(provider, ctx.workQueue, ctx.interruptAction); + auto kr = intrMgr->Initialise( + provider, ctx.workQueue, ctx.interruptAction); + ASFW_LOG(Controller, + "PrepareInterrupts: InterruptManager::Initialise(index=0) " + "-> 0x%08x", + kr); if (kr != kIOReturnSuccess) { ctx.interruptAction.reset(); return kr; diff --git a/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp b/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp index 971963e0..98da3e07 100644 --- a/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp +++ b/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp @@ -29,6 +29,66 @@ namespace ASFW::IsochTransport { inline constexpr uint32_t kTransportAbiVersion = 4; + +// ============================================================================= +// OHCI isochronous transmit packet-header contract. +// ============================================================================= +// IEEE-1394 isoch tag 0 carries a stream with no CIP header; tag 1 carries a +// standard CIP packet. The Fireface 800 former-generation protocol uses tag 0 +// because its payload is raw PCM24-in-32 (Linux AMDTP CIP_NO_HEADER). +enum class IsochPacketTag : uint8_t { + kNoCipHeader = 0, + kCip = 1, +}; + +inline constexpr uint8_t kIsochSpeedS400 = 2; +inline constexpr uint8_t kIsochDataBlockTCode = 0xA; + +[[nodiscard]] constexpr uint32_t BuildIsochTxHeaderQ0( + IsochPacketTag tag, + uint8_t speedCode = kIsochSpeedS400, + uint8_t sy = 0) noexcept { + return (static_cast(speedCode & 0x7U) << 16U) | + (static_cast(tag) << 14U) | + (static_cast(kIsochDataBlockTCode) << 4U) | + static_cast(sy & 0xFU); +} + +[[nodiscard]] constexpr uint32_t BuildIsochTxHeaderQ1( + uint32_t payloadLength) noexcept { + return (payloadLength & 0xFFFFU) << 16U; +} + +[[nodiscard]] constexpr uint8_t DecodeIsochTxHeaderSpeed( + uint32_t headerQ0) noexcept { + return static_cast((headerQ0 >> 16U) & 0x7U); +} + +[[nodiscard]] constexpr IsochPacketTag DecodeIsochTxHeaderTag( + uint32_t headerQ0) noexcept { + return static_cast((headerQ0 >> 14U) & 0x3U); +} + +[[nodiscard]] constexpr uint8_t DecodeIsochTxHeaderTCode( + uint32_t headerQ0) noexcept { + return static_cast((headerQ0 >> 4U) & 0xFU); +} + +[[nodiscard]] constexpr uint32_t DecodeIsochTxHeaderDataLength( + uint32_t headerQ1) noexcept { + return (headerQ1 >> 16U) & 0xFFFFU; +} + +/// A no-CIP idle cycle is represented by no isochronous packet at all. The +/// OHCI core must program a zero-length standard OUTPUT_LAST skip descriptor, +/// not an immediate isoch header with data_length=0. CIP streams retain their +/// normal header-only packet semantics. +[[nodiscard]] constexpr bool ShouldSkipIsochTxPacket( + IsochPacketTag tag, + uint32_t payloadLength) noexcept { + return tag == IsochPacketTag::kNoCipHeader && payloadLength == 0U; +} + // ============================================================================= // Shared queue / buffer sizing (ADK §3.3 / §6.4). // These constants are the "Iron Rule" ground truth for both sides. @@ -57,7 +117,7 @@ struct alignas(64) TxPacketMeta final { uint32_t immediateHeader[2]; ///< Ready-to-blit OHCI IT header quadlets ///< (OUTPUT_MORE_IMMEDIATE immediate data). ///< The core never interprets these. - uint32_t payloadLength; ///< Bytes: 8 (NO-DATA) or 8 + frames·dbs·4. + uint32_t payloadLength; ///< Bytes: 0 for a true empty no-CIP cycle, raw payload for tag 0, or CIP header + payload for tag 1. uint32_t reserved0; uint64_t packetIndex; ///< Absolute index this entry describes. std::atomic commitGen; ///< Lap-numbered commit marker; 0 = never diff --git a/tests/devices/CMakeLists.txt b/tests/devices/CMakeLists.txt index 193fd7c9..571517b7 100644 --- a/tests/devices/CMakeLists.txt +++ b/tests/devices/CMakeLists.txt @@ -53,6 +53,7 @@ add_device_test(AudioDuplexCoordinatorTests "${ASFW_DRIVER_DIR}/Audio/Core/AudioRuntimeRegistry.cpp" "${ASFW_DRIVER_DIR}/Discovery/DeviceRegistry.cpp" "${ASFW_DRIVER_DIR}/Bus/IRM/IRMClient.cpp" + "${ASFW_DRIVER_DIR}/ConfigROM/Parse/ConfigROMParser.cpp" ) # FW-74: resolver-owned duplex stream geometry, wire-format quirks, and host recipe. diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index b96252eb..2610230a 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -242,6 +242,7 @@ add_protocols_test(SessionRegistryTests "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" "${ASFW_DRIVER_DIR}/Discovery/SpeedPolicy.cpp" + "${ASFW_DRIVER_DIR}/ConfigROM/Parse/ConfigROMParser.cpp" ) # SBP2Handler Tests (user-client boundary: session state + command ABI hardening) @@ -258,4 +259,5 @@ add_protocols_test(SBP2HandlerTests "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" "${ASFW_DRIVER_DIR}/Discovery/SpeedPolicy.cpp" + "${ASFW_DRIVER_DIR}/ConfigROM/Parse/ConfigROMParser.cpp" ) From ec49c0eef7036b0df767b85b3a390a43b90212f3 Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 04:11:09 -0300 Subject: [PATCH 3/7] Regenerate Xcode project via xcodegen (Homebrew now installed) Replaces the hand-edited project.pbxproj from the previous commit with a canonical `xcodegen generate` output now that Homebrew + xcodegen are installed on this machine. Also restores ASFWDriver.xcscheme, which project.yml already declares (schemes.ASFWDriver) -- its earlier deletion just meant every future `xcodegen generate` would keep recreating it as a diff. The stray backup/OLD artifacts (Info.plist.original, DriverContext.cpp.backup-*, DMAMemoryManager.cpp.backup-dma, ASFWDriverOLD.zip, ASFW.xcodeprojOLD/, projectOLD.pbxproj) were deleted before regenerating -- they lived under ASFWDriver/, which project.yml globs in full, so a prior generate had folded them into the dext target as build Resources. That is almost certainly what forced the earlier hand-edit workaround in the first place. --- ASFW.xcodeproj/project.pbxproj | 52 +++++----- .../xcshareddata/xcschemes/ASFW.xcscheme | 1 - .../xcschemes/ASFWDriver.xcscheme | 95 +++++++++++++++++++ 3 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index 5b17196d..8f248fa7 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - A1B2C3D4E5F60718293A4B5C /* RMEFireface800Protocol.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B1C2D3E4F5061728394A5B6C /* RMEFireface800Protocol.cpp */; }; 0267190E6597A8DA11482D7C /* DriverConnector+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264D4CD41FF948792F95E023 /* DriverConnector+Status.swift */; }; 02D9A096E4DC8704CF3B4DE4 /* MetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B20C10AC8C1594C410D4D6F /* MetricsView.swift */; }; 04D9E2DA1F4128063BC1E28E /* FocusriteSaffireProfile.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7B882FD0DA3D8C7A7845DA54 /* FocusriteSaffireProfile.cpp */; }; @@ -115,6 +114,7 @@ 47C7D2797F3974490450A45A /* AVCNestedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB92C50A509F45F51EFD3F03 /* AVCNestedChannelTests.swift */; }; 47F41CED6EB2EE793A31B6E5 /* ASFWAudioDriverControls.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 018751CF8CA051FDDE9B2B06 /* ASFWAudioDriverControls.cpp */; }; 48359CD69DF9A8F5868E12C0 /* ASFWMCPLiveDriverControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1391A71639068D8291B32AF /* ASFWMCPLiveDriverControl.swift */; }; + 49F6698714AAE6DA7A1CE7AE /* RMEFireface800Protocol.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B80BA7172EA0EC366CA7C8EC /* RMEFireface800Protocol.cpp */; }; 4A7F808CC9DBBBED16B2F07A /* ConfigROMBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 290CF8A5D9D6CABD1B3BF37A /* ConfigROMBuilder.cpp */; }; 4B85BC02C027180D070F4DFF /* DICETcatProtocol.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3B6A6E8C021FEF7B9569D79A /* DICETcatProtocol.cpp */; }; 4C587E86B66E99AA77CF17A1 /* ASFWDriverUserClient.iig in Sources */ = {isa = PBXBuildFile; fileRef = AF46E21DCF97D1A3F7DFDA09 /* ASFWDriverUserClient.iig */; }; @@ -382,9 +382,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - B1C2D3E4F5061728394A5B6C /* RMEFireface800Protocol.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RMEFireface800Protocol.cpp; sourceTree = ""; }; - C1D2E3F40516273849A5B6C7 /* RMEFireface800Protocol.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEFireface800Protocol.hpp; sourceTree = ""; }; - E1F2031425364758697A8B9C /* RMEAudioProfiles.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEAudioProfiles.hpp; sourceTree = ""; }; 00744EB824D48D9F716E153A /* PostResetTimingCoordinator.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PostResetTimingCoordinator.hpp; sourceTree = ""; }; 00979778212761BF904386E6 /* AudioEndpointRuntime.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AudioEndpointRuntime.hpp; sourceTree = ""; }; 00D5EC5AEB7989BC5DBE3E15 /* BusResetHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BusResetHandler.cpp; sourceTree = ""; }; @@ -561,6 +558,7 @@ 3B0597EE0A4E4C496E0C2EA6 /* ASFWAudioDriverZts.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ASFWAudioDriverZts.cpp; sourceTree = ""; }; 3B294E73813DAE348F5D045A /* IDeviceManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = IDeviceManager.hpp; sourceTree = ""; }; 3B6A6E8C021FEF7B9569D79A /* DICETcatProtocol.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DICETcatProtocol.cpp; sourceTree = ""; }; + 3BA650C15D778F327D36918D /* RMEAudioProfiles.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEAudioProfiles.hpp; sourceTree = ""; }; 3BC1BCBC9E52B53B87DEEA69 /* CIPHeader.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CIPHeader.hpp; sourceTree = ""; }; 3CCCE7A077DAD59020AD49F2 /* BusResetCoordinatorActions.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BusResetCoordinatorActions.cpp; sourceTree = ""; }; 3CF6AA538A8321AEB7DDB655 /* MCPHardwareSmokeRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPHardwareSmokeRunnerTests.swift; sourceTree = ""; }; @@ -917,6 +915,7 @@ B7E03EB5573EA30EB8F2D42B /* PingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingView.swift; sourceTree = ""; }; B7E5492B36B350E48E9214A8 /* AudioNubPublisher.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AudioNubPublisher.cpp; sourceTree = ""; }; B808B46CF106706A04C126DB /* ASFWMCPAvcFcpTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASFWMCPAvcFcpTools.swift; sourceTree = ""; }; + B80BA7172EA0EC366CA7C8EC /* RMEFireface800Protocol.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RMEFireface800Protocol.cpp; sourceTree = ""; }; B8B02346383E39CA66366AAE /* AudioDriverConfig.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AudioDriverConfig.cpp; sourceTree = ""; }; B9E220CDF43AD389BD345A8F /* DriverKitOwnership.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DriverKitOwnership.hpp; sourceTree = ""; }; BA6781D8E0522CE83CECCD4F /* Error.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Error.hpp; sourceTree = ""; }; @@ -930,6 +929,7 @@ BCD6B302DCC686B9FD46CE06 /* RolePolicy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RolePolicy.cpp; sourceTree = ""; }; BD88A8D5D9A4D4FDC7E5A71A /* ConfigROMParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ConfigROMParser.hpp; sourceTree = ""; }; BD92B955A8BFE30B33FB1F45 /* ATManagerImpl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ATManagerImpl.hpp; sourceTree = ""; }; + BDCF2C69EC4FAB205B581D26 /* RMEFireface800Protocol.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEFireface800Protocol.hpp; sourceTree = ""; }; BE308A43E548024E6623D4A8 /* bump.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bump.sh; sourceTree = ""; }; BF49A4417B0371208665ECA7 /* StreamFormatParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = StreamFormatParser.hpp; sourceTree = ""; }; BF9FE813F630AD0832D9E674 /* FWDevice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FWDevice.hpp; sourceTree = ""; }; @@ -1654,7 +1654,7 @@ 4D2DB64009F93C974F2DF20F /* DICE */, FD14F207887135CE102F8ACA /* Duplex */, 6DF35E1146D265DA8C2B7611 /* Oxford */, - D1E2F3041526374859A6B7C8 /* RME */, + 83A156BB194E66B40C582BD8 /* RME */, ); path = Protocols; sourceTree = ""; @@ -1998,6 +1998,15 @@ path = Discovery; sourceTree = ""; }; + 83A156BB194E66B40C582BD8 /* RME */ = { + isa = PBXGroup; + children = ( + B80BA7172EA0EC366CA7C8EC /* RMEFireface800Protocol.cpp */, + BDCF2C69EC4FAB205B581D26 /* RMEFireface800Protocol.hpp */, + ); + path = RME; + sourceTree = ""; + }; 8416E8873F248937DC1F725E /* Config */ = { isa = PBXGroup; children = ( @@ -2628,15 +2637,6 @@ path = Diagnostics; sourceTree = ""; }; - D1E2F3041526374859A6B7C8 /* RME */ = { - isa = PBXGroup; - children = ( - B1C2D3E4F5061728394A5B6C /* RMEFireface800Protocol.cpp */, - C1D2E3F40516273849A5B6C7 /* RMEFireface800Protocol.hpp */, - ); - path = RME; - sourceTree = ""; - }; E08412C570C729DF6E083569 /* Vendors */ = { isa = PBXGroup; children = ( @@ -2645,7 +2645,7 @@ F1BF9794C22F3E1693A6CCF0 /* FocusriteAudioProfiles.hpp */, ADA183665D23E96057546B82 /* MidasAudioProfiles.hpp */, 0C844DC3BD4E0AE7D1081F52 /* PreSonusAudioProfiles.hpp */, - E1F2031425364758697A8B9C /* RMEAudioProfiles.hpp */, + 3BA650C15D778F327D36918D /* RMEAudioProfiles.hpp */, C9474A3E174E5254247327C6 /* TerraTecAudioProfiles.hpp */, ); path = Vendors; @@ -2899,8 +2899,8 @@ projectDirPath = ""; projectRoot = ""; targets = ( - EE5A446B3669D0171F9606EE /* ASFW */, 0EB9A8DA75D08971084A440A /* ASFWDriver */, + EE5A446B3669D0171F9606EE /* ASFW */, 348672D607701677ABCB567C /* ASFWTests */, ); }; @@ -3049,7 +3049,6 @@ DB27C439E148A97E29A0D466 /* DeviceDiscoveryHandler.cpp in Sources */, 971AC3A1D14A2A8C0F6E1939 /* DeviceManager.cpp in Sources */, 8482325128986FFEE1C854CF /* DeviceProtocolFactory.cpp in Sources */, - A1B2C3D4E5F60718293A4B5C /* RMEFireface800Protocol.cpp in Sources */, 713D5C0BC0201DFBDA15B69D /* DeviceRegistry.cpp in Sources */, 6DB7521C088B8C7AE091D4D3 /* DeviceStreamModeQuirks.cpp in Sources */, D5D557B2656B33436FB6738E /* DiagnosticLogger.cpp in Sources */, @@ -3115,6 +3114,7 @@ 3ECDDF5924BE520E1750A8FF /* PostResetTimingCoordinator.cpp in Sources */, 270027C27155433C83E72998 /* PowerLinkPolicyCoordinator.cpp in Sources */, AE6B45BB973CF543649B851E /* PreSonusStudioLiveProfile.cpp in Sources */, + 49F6698714AAE6DA7A1CE7AE /* RMEFireface800Protocol.cpp in Sources */, 3455F76072D6ED0ED7E7B77E /* ROMReader.cpp in Sources */, 55D2ED0B592528E5950795D3 /* ROMScanSession.cpp in Sources */, 3125B234BDC6F30DF047EC94 /* ROMScanSessionDetails.cpp in Sources */, @@ -3319,7 +3319,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = F6YA6B56LR; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; @@ -3340,7 +3340,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = F6YA6B56LR; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; @@ -3415,9 +3415,6 @@ 5871D362E349A3418E269976 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_CODE_COVERAGE = NO; - GCC_GENERATE_TEST_COVERAGE_FILES = NO; - GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; AD_HOC_CODE_SIGNING_ALLOWED = YES; ASFW_DEXT_ENTITLEMENTS_NO = ASFWDriver/ASFWDriver.entitlements; ASFW_DEXT_ENTITLEMENTS_YES = "ASFWDriver/ASFWDriver+SCSI.entitlements"; @@ -3429,7 +3426,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3467,7 +3464,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = F6YA6B56LR; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -3500,9 +3497,6 @@ 8AE4F9EA5D2C9D98D94E9AD2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_CODE_COVERAGE = NO; - GCC_GENERATE_TEST_COVERAGE_FILES = NO; - GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; AD_HOC_CODE_SIGNING_ALLOWED = YES; ASFW_DEXT_ENTITLEMENTS_NO = ASFWDriver/ASFWDriver.entitlements; ASFW_DEXT_ENTITLEMENTS_YES = "ASFWDriver/ASFWDriver+SCSI.entitlements"; @@ -3514,7 +3508,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3617,7 +3611,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = F6YA6B56LR; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; diff --git a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme index 04fd1619..49d88a40 100644 --- a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme +++ b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFW.xcscheme @@ -28,7 +28,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "NO" onlyGenerateCoverageForSpecifiedTargets = "NO"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b351596ca7b385fbce3e8cabdf5296924036cf48 Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 05:22:28 -0300 Subject: [PATCH 4/7] RME: retry IRM resource snapshot on transient NotFound after bus reset Hardware testing on the Fireface 800 (Stage 5H build) surfaced a real bug: right after a bus reset, ReservePlaybackResourcesPreflight's IRM snapshot read can race IRMClient's own IRM-node resolution -- IRMClient::ReadIRMWindow returns NotFound while irmNodeId_ is still 0xFF, before bus manager/Self-ID processing has caught up. There was no retry, so playbackPreflightRoute_ stayed at 0 forever and every subsequent StartIO logged "verified IRM route is not ready" until the next full protocol reinit. Reproduced live: a mid-session bus reset left the route permanently unarmed on the unpatched build. Mirrors the existing PollDeviceTxIsochChannel bounded retry idiom in this same file: up to 10 attempts, 50ms apart, before giving up. Confirmed on hardware after the fix: Stage 5H's bounded circular silent playback burst (48 descriptors, 100ms, clean OHCI/FF800/IRM teardown) passes cleanly on a fresh dext load. The IRM snapshot happened to succeed on first read this run (no NotFound race hit), so the retry branch itself is unexercised by this specific run; still pending is forcing another bus reset while the dext is live to confirm the retry path recovers instead of stranding the route again. --- .../Protocols/RME/RMEFireface800Protocol.cpp | 28 ++++++++++++++++--- .../Protocols/RME/RMEFireface800Protocol.hpp | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp index 25ecdc33..06977d4c 100644 --- a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp @@ -616,7 +616,8 @@ void RMEFireface800Protocol::ProbeTxIsochChannel() noexcept { } -void RMEFireface800Protocol::ReservePlaybackResourcesPreflight(uint32_t rate) noexcept { +void RMEFireface800Protocol::ReservePlaybackResourcesPreflight(uint32_t rate, + uint32_t attempt) noexcept { if (!active_.load(std::memory_order_acquire)) { return; } @@ -641,14 +642,33 @@ void RMEFireface800Protocol::ReservePlaybackResourcesPreflight(uint32_t rate) no } irmClient_->ReadResourcesSnapshot( - [this, rate, bandwidthUnits](IRM::AllocationStatus status, IRM::ResourceSnapshot snapshot) { + [this, rate, bandwidthUnits, attempt](IRM::AllocationStatus status, + IRM::ResourceSnapshot snapshot) { if (!active_.load(std::memory_order_acquire)) { return; } if (status != IRM::AllocationStatus::Success) { + // Right after a bus reset the IRM node hasn't been re-resolved + // yet (IRMClient::ReadIRMWindow reports NotFound while + // irmNodeId_ == 0xFF) — this is a transient race against bus + // manager/Self-ID processing, not a permanent failure. Retry + // with the same bounded backoff PollDeviceTxIsochChannel uses + // below, instead of stranding playbackPreflightRoute_ at 0 + // until the next full rediscovery. + if (status == IRM::AllocationStatus::NotFound && attempt < 10U) { + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5F IRM snapshot not ready (IRM node not yet resolved) status=%{public}s attempt=%u; retrying", + IRM::ToString(status), + attempt); + IOSleep(50); + ReservePlaybackResourcesPreflight(rate, attempt + 1U); + return; + } ASFW_LOG_ERROR(Audio, - "[RME] Stage 5F IRM snapshot failed status=%{public}s", - IRM::ToString(status)); + "[RME] Stage 5F IRM snapshot failed status=%{public}s attempt=%u", + IRM::ToString(status), + attempt); return; } diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp index c3ef2e3e..32d4f1ff 100644 --- a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp @@ -45,7 +45,7 @@ class RMEFireface800Protocol final : public IDeviceProtocol { void VerifyClockAfterStfWrite(uint32_t expectedRate) noexcept; void ProbeSyncStatus() noexcept; void ProbeTxIsochChannel() noexcept; - void ReservePlaybackResourcesPreflight(uint32_t rate) noexcept; + void ReservePlaybackResourcesPreflight(uint32_t rate, uint32_t attempt = 0U) noexcept; void ProgramRxPacketFormat(uint32_t rate, uint8_t channel, uint32_t bandwidthUnits) noexcept; void RequestDeviceTxAllocation(uint32_t rate) noexcept; void PollDeviceTxIsochChannel(uint32_t rate, uint32_t attempt) noexcept; From 4aa5d3221392f220e1a13f1c86ad27205d418049 Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 06:39:32 -0300 Subject: [PATCH 5/7] RME Stage 6: dev-only continuous silent isoch TX streaming Turns the bounded 100ms Stage 5H circular silence burst into a maintained continuous stream, reachable only through a new developer-gated UserClient/MCP path -- never through real Core Audio StartIO, which keeps returning kIOReturnUnsupported unchanged. Reuses the Stage 5H self-looping 48-descriptor D-D-D-S ring builder verbatim (it was already duration-agnostic; only the run/cleanup entry points were bounded). New transport-layer methods start the ring and return immediately instead of polling for a fixed window, plus a non-blocking health check (DEAD/event-error/anchor-advance) reused by both a new Watchdog heartbeat (~1s, anomaly-only logging) and an on-demand UserClient query. RMEFireface800Protocol gains Start/Stop/Health hooks mirroring RunBoundedPlaybackIntegrationPreflight's shape, guarded by a separate continuousPlaybackInFlight_ flag checked alongside stage5hInFlight_ (same OHCI context/IRM route, must never run both at once). Extracted the FF800-stop+IRM-release tail into StopDeviceEngineAndReleaseIrm so it's shared instead of duplicated between the bounded and continuous paths. IsochTransmitContext::Stop() now stops an in-flight continuous cadence too, since it deliberately never enters State::Running and would otherwise be missed by driver teardown. New UserClient selectors 64-66 (GUID-keyed, full 64-bit -- the existing kMethodStartAudioStreaming=62/63 are an unrelated generic AV/C developer path and hard-reject RME already) plumbed through to new MCP tools asfw_continuous_tx_{start,stop,health}_dev, mirroring the existing asfw_bus_reset_dev developer-trigger pattern. Verified: full build (dext + Swift app) succeeds, ./build.sh --test-only stays at 1334/1335 (same pre-existing unrelated failure, no regression). Hardware verification of the continuous run itself is still pending -- blocked on a stuck systemextensionsctl uninstall requiring a reboot. --- ASFW.xcodeproj/project.pbxproj | 4 + ASFW/ASFWDriverConnector.swift | 4 + ASFW/DriverConnector+Status.swift | 93 ++++++ ASFW/MCP/ASFWMCPContinuousTxTools.swift | 81 +++++ ASFW/MCP/ASFWMCPDriverControl.swift | 22 ++ ASFW/MCP/ASFWMCPLiveDriverControl.swift | 31 ++ ASFW/MCP/ASFWMCPToolCatalog.swift | 1 + ASFW/MCP/ASFWMCPToolDispatch.swift | 62 ++++ ASFWDriver/ASFWDriver.cpp | 111 +++++++ ASFWDriver/ASFWDriver.iig | 13 + .../Audio/Protocols/IDeviceProtocol.hpp | 45 +++ .../Protocols/RME/RMEFireface800Protocol.cpp | 313 +++++++++++++++--- .../Protocols/RME/RMEFireface800Protocol.hpp | 31 ++ ASFWDriver/Isoch/IsochService.cpp | 26 ++ ASFWDriver/Isoch/IsochService.hpp | 7 + .../Isoch/Transmit/IsochTransmitContext.cpp | 143 ++++++++ .../Isoch/Transmit/IsochTransmitContext.hpp | 37 +++ ASFWDriver/Scheduling/WatchdogCoordinator.cpp | 31 ++ ASFWDriver/Scheduling/WatchdogCoordinator.hpp | 1 + .../UserClient/Core/ASFWDriverUserClient.cpp | 45 +++ 20 files changed, 1051 insertions(+), 50 deletions(-) create mode 100644 ASFW/MCP/ASFWMCPContinuousTxTools.swift diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index 8f248fa7..817000ae 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -335,6 +335,7 @@ F118D01B178AD54981507B90 /* ROMScanSessionIRM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D1606D8DEE1120C1646AB8D /* ROMScanSessionIRM.cpp */; }; F193984E9950C4C137BFAD6D /* MCPDiceTcatToolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B7F3D2FEC0DA11D8D9F0AC /* MCPDiceTcatToolsTests.swift */; }; F302759145BDF336C186D2BC /* HardwareInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E564F41599A92B7D55B12 /* HardwareInterface.cpp */; }; + F34A587E2B103749D7490B97 /* ASFWMCPContinuousTxTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE56C6DD9B345116DD0699B /* ASFWMCPContinuousTxTools.swift */; }; F3EC20E3CDECF9FD90E56995 /* ASFWSCSIController.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3558EAA4E19D859BC8594170 /* ASFWSCSIController.cpp */; }; F4A99690858D37ACCC9CECB6 /* SystemLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB2E35051CCCFD3E4CD6E2F /* SystemLogsView.swift */; }; F576426B7995BB3474228911 /* ASFWDriverUserClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5364BA023A0C10D7791261A1 /* ASFWDriverUserClient.cpp */; }; @@ -561,6 +562,7 @@ 3BA650C15D778F327D36918D /* RMEAudioProfiles.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEAudioProfiles.hpp; sourceTree = ""; }; 3BC1BCBC9E52B53B87DEEA69 /* CIPHeader.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CIPHeader.hpp; sourceTree = ""; }; 3CCCE7A077DAD59020AD49F2 /* BusResetCoordinatorActions.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BusResetCoordinatorActions.cpp; sourceTree = ""; }; + 3CE56C6DD9B345116DD0699B /* ASFWMCPContinuousTxTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASFWMCPContinuousTxTools.swift; sourceTree = ""; }; 3CF6AA538A8321AEB7DDB655 /* MCPHardwareSmokeRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPHardwareSmokeRunnerTests.swift; sourceTree = ""; }; 3D1606D8DEE1120C1646AB8D /* ROMScanSessionIRM.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ROMScanSessionIRM.cpp; sourceTree = ""; }; 3D26D51E676FCE0D7C7747FD /* InterruptDispatcher.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = InterruptDispatcher.hpp; sourceTree = ""; }; @@ -1812,6 +1814,7 @@ 077468963C2F7E7BB91E28CB /* ASFWMCPBeBoBTools.swift */, 9D6A93FFD51132B2A4F8B00E /* ASFWMCPBusResetTools.swift */, 969C58B339AB3E30DB3B3E45 /* ASFWMCPCmpTools.swift */, + 3CE56C6DD9B345116DD0699B /* ASFWMCPContinuousTxTools.swift */, 0417EE4C83805D8B15818D95 /* ASFWMCPCore.swift */, EE503DDF07D0510DECCEBD3B /* ASFWMCPDiceTcatTools.swift */, 42FD0F9803524954295461C0 /* ASFWMCPDriverControl.swift */, @@ -3207,6 +3210,7 @@ 86AAAB1DA5BB95E96BAFFBB7 /* ASFWMCPBeBoBTools.swift in Sources */, 6FA31DB6C2C615B5E0AFBB7C /* ASFWMCPBusResetTools.swift in Sources */, 963574C5C0B791D8C9BC3322 /* ASFWMCPCmpTools.swift in Sources */, + F34A587E2B103749D7490B97 /* ASFWMCPContinuousTxTools.swift in Sources */, 7A904CEDE54313921506AFCB /* ASFWMCPControlViewModel.swift in Sources */, EE252C976DEC6C30E860638C /* ASFWMCPCore.swift in Sources */, 960F055B6D7B32200D416CDB /* ASFWMCPDiceTcatTools.swift in Sources */, diff --git a/ASFW/ASFWDriverConnector.swift b/ASFW/ASFWDriverConnector.swift index 99d79b31..c7cc81dd 100644 --- a/ASFW/ASFWDriverConnector.swift +++ b/ASFW/ASFWDriverConnector.swift @@ -60,6 +60,10 @@ final class ASFWDriverConnector: ObservableObject { case requestUserBusReset = 61 case startAudioStreaming = 62 case stopAudioStreaming = 63 + // Stage 6 (dev-only): continuous silent isoch TX cadence. + case startContinuousIsochTxSilence = 64 + case stopContinuousIsochTxSilence = 65 + case getContinuousIsochTxHealth = 66 } // MARK: - Re-exported Models diff --git a/ASFW/DriverConnector+Status.swift b/ASFW/DriverConnector+Status.swift index 2a9471d8..37c5d364 100644 --- a/ASFW/DriverConnector+Status.swift +++ b/ASFW/DriverConnector+Status.swift @@ -221,4 +221,97 @@ extension ASFWDriverConnector { } return message } + + // MARK: - Stage 6 (dev-only continuous silence) + + /// Starts a continuous (not bounded-to-100ms) silent isoch TX cadence for + /// a device protocol that implements it (currently only RME Fireface + /// 800). Never reachable through real CoreAudio StartIO. + func startContinuousIsochTxSilence(guid: UInt64) -> Bool { + guard isConnected else { + log("startContinuousIsochTxSilence: Not connected", level: .warning) + return false + } + + var input: [UInt64] = [guid] + let kr = IOConnectCallScalarMethod( + connection, + Method.startContinuousIsochTxSilence.rawValue, + &input, + UInt32(input.count), + nil, + nil + ) + guard kr == KERN_SUCCESS else { + let message = "startContinuousIsochTxSilence failed: \(interpretIOReturn(kr))" + log(message, level: .error) + lastError = message + return false + } + return true + } + + func stopContinuousIsochTxSilence(guid: UInt64) -> Bool { + guard isConnected else { + log("stopContinuousIsochTxSilence: Not connected", level: .warning) + return false + } + + var input: [UInt64] = [guid] + let kr = IOConnectCallScalarMethod( + connection, + Method.stopContinuousIsochTxSilence.rawValue, + &input, + UInt32(input.count), + nil, + nil + ) + guard kr == KERN_SUCCESS else { + let message = "stopContinuousIsochTxSilence failed: \(interpretIOReturn(kr))" + log(message, level: .error) + lastError = message + return false + } + return true + } + + struct ContinuousIsochTxHealth: Equatable { + let running: Bool + let dead: Bool + let eventError: Bool + let anchorExecutions: UInt32 + let lastAnchorTimestamp: UInt16 + } + + func getContinuousIsochTxHealth(guid: UInt64) -> ContinuousIsochTxHealth? { + guard isConnected else { + log("getContinuousIsochTxHealth: Not connected", level: .warning) + return nil + } + + var input: [UInt64] = [guid] + var output = [UInt64](repeating: 0, count: 5) + var outputCount: UInt32 = 5 + let kr = IOConnectCallScalarMethod( + connection, + Method.getContinuousIsochTxHealth.rawValue, + &input, + UInt32(input.count), + &output, + &outputCount + ) + guard kr == KERN_SUCCESS, outputCount == 5 else { + let message = "getContinuousIsochTxHealth failed: \(interpretIOReturn(kr))" + log(message, level: .error) + lastError = message + return nil + } + return ContinuousIsochTxHealth( + running: output[0] != 0, + dead: output[1] != 0, + eventError: output[2] != 0, + anchorExecutions: UInt32(truncatingIfNeeded: output[3]), + lastAnchorTimestamp: UInt16(truncatingIfNeeded: output[4]) + ) + } } diff --git a/ASFW/MCP/ASFWMCPContinuousTxTools.swift b/ASFW/MCP/ASFWMCPContinuousTxTools.swift new file mode 100644 index 00000000..aa8041a9 --- /dev/null +++ b/ASFW/MCP/ASFWMCPContinuousTxTools.swift @@ -0,0 +1,81 @@ +import Foundation + +// Stage 6 (dev-only): continuous (not bounded-to-100ms) silent isoch TX +// cadence, for device protocols that implement IDeviceProtocol's continuous- +// playback hooks (currently only RME Fireface 800). Reachable only through +// this developer-gated MCP path -- never through real CoreAudio StartIO, +// which must keep returning kIOReturnUnsupported. + +extension ASFWMCPToolCatalog { + static let continuousTxTools: [ASFWMCPToolDefinition] = [ + ASFWMCPToolDefinition( + name: "asfw_continuous_tx_start_dev", + group: "isoch_dev", + visibility: .developerWrite, + readOnly: false, + idempotent: false, + summary: "Start a continuous (unbounded) silent isoch TX cadence on the held playback route (targetGuid required). Never enables real Core Audio streaming." + ), + ASFWMCPToolDefinition( + name: "asfw_continuous_tx_stop_dev", + group: "isoch_dev", + visibility: .developerWrite, + readOnly: false, + idempotent: true, + summary: "Stop an active continuous silent isoch TX cadence and release its held IRM route (targetGuid required)." + ), + ASFWMCPToolDefinition( + name: "asfw_continuous_tx_health_dev", + group: "isoch_dev", + visibility: .readOnly, + readOnly: true, + idempotent: false, + summary: "Non-blocking health snapshot of an active continuous silent isoch TX cadence (targetGuid required)." + ) + ] +} + +struct ASFWMCPContinuousTxReceipt: Equatable { + let targetGuid: UInt64 + let started: Bool + let status: Int32 + + var ok: Bool { status == 0 } + + var mcpValue: ASFWMCPValue { + .object([ + "targetGuid": .string(String(format: "0x%016llX", targetGuid)), + "action": .string(started ? "start" : "stop"), + "status": .int(Int(status)), + "ok": .bool(ok), + ]) + } +} + +struct ASFWMCPContinuousTxHealthReceipt: Equatable { + let targetGuid: UInt64 + let health: ASFWDriverConnector.ContinuousIsochTxHealth? + let status: Int32 + + var ok: Bool { status == 0 } + + var mcpValue: ASFWMCPValue { + guard let health else { + return .object([ + "targetGuid": .string(String(format: "0x%016llX", targetGuid)), + "status": .int(Int(status)), + "ok": .bool(false), + ]) + } + return .object([ + "targetGuid": .string(String(format: "0x%016llX", targetGuid)), + "status": .int(Int(status)), + "ok": .bool(ok), + "running": .bool(health.running), + "dead": .bool(health.dead), + "eventError": .bool(health.eventError), + "anchorExecutions": .int(Int(health.anchorExecutions)), + "lastAnchorTimestamp": .int(Int(health.lastAnchorTimestamp)), + ]) + } +} diff --git a/ASFW/MCP/ASFWMCPDriverControl.swift b/ASFW/MCP/ASFWMCPDriverControl.swift index 1dae5c86..f3606ac5 100644 --- a/ASFW/MCP/ASFWMCPDriverControl.swift +++ b/ASFW/MCP/ASFWMCPDriverControl.swift @@ -13,6 +13,8 @@ protocol ASFWDriverControlling { func executeCompareSwap(_ request: ASFWMCPCompareSwapRequest) async -> ASFWMCPTransactionResult func executeFCPCommand(_ request: ASFWMCPFcpCommandRequest) async -> ASFWMCPFcpCommandReceipt func executePhase88Streaming(targetGuid: UInt64, start: Bool) async -> ASFWMCPPhase88StreamingReceipt + func executeContinuousTxSilence(targetGuid: UInt64, start: Bool) async -> ASFWMCPContinuousTxReceipt + func executeContinuousTxHealth(targetGuid: UInt64) async -> ASFWMCPContinuousTxHealthReceipt func executeBusReset(_ request: ASFWMCPBusResetRequest) async -> ASFWMCPBusResetReceipt func executeIRMSnapshot(_ request: ASFWMCPIrmSnapshotRequest) async -> ASFWMCPIrmResourceSnapshot } @@ -31,6 +33,7 @@ actor MockASFWDriverControl: ASFWDriverControlling { private var duetInputFdf: UInt8 = 0x02 private var duetOutputFdf: UInt8 = 0x02 private var phase88Streaming = false + private var continuousTxSilenceRunning = false init( generation: UInt32 = 17, @@ -375,6 +378,25 @@ actor MockASFWDriverControl: ASFWDriverControlling { return ASFWMCPPhase88StreamingReceipt(targetGuid: targetGuid, started: start, status: 0) } + func executeContinuousTxSilence(targetGuid: UInt64, start: Bool) async -> ASFWMCPContinuousTxReceipt { + attemptedWriteCount += 1 + continuousTxSilenceRunning = start + return ASFWMCPContinuousTxReceipt(targetGuid: targetGuid, started: start, status: 0) + } + + func executeContinuousTxHealth(targetGuid: UInt64) async -> ASFWMCPContinuousTxHealthReceipt { + guard continuousTxSilenceRunning else { + return ASFWMCPContinuousTxHealthReceipt(targetGuid: targetGuid, health: nil, status: -536_870_213) + } + return ASFWMCPContinuousTxHealthReceipt( + targetGuid: targetGuid, + health: ASFWDriverConnector.ContinuousIsochTxHealth( + running: true, dead: false, eventError: false, + anchorExecutions: 42, lastAnchorTimestamp: 1234), + status: 0 + ) + } + func executeBusReset(_ request: ASFWMCPBusResetRequest) async -> ASFWMCPBusResetReceipt { let correlationId = "mock-bus-reset" guard request.generation == generation else { diff --git a/ASFW/MCP/ASFWMCPLiveDriverControl.swift b/ASFW/MCP/ASFWMCPLiveDriverControl.swift index 31bb3a39..ee2b4544 100644 --- a/ASFW/MCP/ASFWMCPLiveDriverControl.swift +++ b/ASFW/MCP/ASFWMCPLiveDriverControl.swift @@ -22,6 +22,8 @@ protocol ASFWLiveDriverBackend: AnyObject { func mcpSendRawFCPCommand(guid: UInt64, frame: Data, timeoutMs: UInt32) -> Data? func mcpSetAudioStreaming(guid: UInt64, enabled: Bool) -> Int32 func mcpRequestUserBusReset(expectedGeneration: UInt32, shortReset: Bool) -> UInt32? + func mcpSetContinuousIsochTxSilence(guid: UInt64, enabled: Bool) -> Int32 + func mcpGetContinuousIsochTxHealth(guid: UInt64) -> ASFWDriverConnector.ContinuousIsochTxHealth? } extension ASFWDriverConnector: ASFWLiveDriverBackend { @@ -113,6 +115,16 @@ extension ASFWDriverConnector: ASFWLiveDriverBackend { func mcpRequestUserBusReset(expectedGeneration: UInt32, shortReset: Bool) -> UInt32? { requestUserBusReset(expectedGeneration: expectedGeneration, shortReset: shortReset) } + + func mcpSetContinuousIsochTxSilence(guid: UInt64, enabled: Bool) -> Int32 { + let ok = enabled ? startContinuousIsochTxSilence(guid: guid) + : stopContinuousIsochTxSilence(guid: guid) + return ok ? 0 : -1 + } + + func mcpGetContinuousIsochTxHealth(guid: UInt64) -> ContinuousIsochTxHealth? { + getContinuousIsochTxHealth(guid: guid) + } } @MainActor @@ -386,6 +398,25 @@ final class LiveASFWDriverControl: ASFWDriverControlling { ) } + func executeContinuousTxSilence(targetGuid: UInt64, start: Bool) async -> ASFWMCPContinuousTxReceipt { + guard backend.mcpIsConnected else { + return ASFWMCPContinuousTxReceipt(targetGuid: targetGuid, started: start, status: -536_870_201) + } + return ASFWMCPContinuousTxReceipt( + targetGuid: targetGuid, + started: start, + status: backend.mcpSetContinuousIsochTxSilence(guid: targetGuid, enabled: start) + ) + } + + func executeContinuousTxHealth(targetGuid: UInt64) async -> ASFWMCPContinuousTxHealthReceipt { + guard backend.mcpIsConnected, + let health = backend.mcpGetContinuousIsochTxHealth(guid: targetGuid) else { + return ASFWMCPContinuousTxHealthReceipt(targetGuid: targetGuid, health: nil, status: -536_870_201) + } + return ASFWMCPContinuousTxHealthReceipt(targetGuid: targetGuid, health: health, status: 0) + } + func executePhase88Streaming(targetGuid: UInt64, start: Bool) async -> ASFWMCPPhase88StreamingReceipt { guard backend.mcpIsConnected else { return ASFWMCPPhase88StreamingReceipt(targetGuid: targetGuid, started: start, status: -536_870_201) diff --git a/ASFW/MCP/ASFWMCPToolCatalog.swift b/ASFW/MCP/ASFWMCPToolCatalog.swift index 91eec616..b1d91e3f 100644 --- a/ASFW/MCP/ASFWMCPToolCatalog.swift +++ b/ASFW/MCP/ASFWMCPToolCatalog.swift @@ -21,6 +21,7 @@ enum ASFWMCPToolCatalog { + sbp2Tools + diceTcatTools + bebobTools + + continuousTxTools } static let coreTools: [ASFWMCPToolDefinition] = [ diff --git a/ASFW/MCP/ASFWMCPToolDispatch.swift b/ASFW/MCP/ASFWMCPToolDispatch.swift index b50de4c4..e90061ae 100644 --- a/ASFW/MCP/ASFWMCPToolDispatch.swift +++ b/ASFW/MCP/ASFWMCPToolDispatch.swift @@ -115,6 +115,11 @@ extension ASFWMCPCore { case "asfw_phase88_start_48k", "asfw_phase88_stop": return await phase88StreamingResult(toolName: name, decoder: decoder, start: name == "asfw_phase88_start_48k") + case "asfw_continuous_tx_start_dev", "asfw_continuous_tx_stop_dev": + return await continuousTxStreamingResult(toolName: name, decoder: decoder, + start: name == "asfw_continuous_tx_start_dev") + case "asfw_continuous_tx_health_dev": + return await continuousTxHealthResult(toolName: name, decoder: decoder) default: return notImplementedToolResult(name, reason: "Catalog tool \(name) has no dispatch arm.") } @@ -780,6 +785,63 @@ private extension ASFWMCPCore { } } + func continuousTxStreamingResult( + toolName: String, + decoder: ASFWMCPToolArgumentDecoder, + start: Bool + ) async -> ASFWMCPToolCallResult { + do { + let targetGuid = try decoder.uint64("targetGuid") + let currentGen = await currentGeneration() + // No bus-generation pin applies to this GUID-keyed dev control (it + // drives a held OHCI/IRM session, not a node address) -- pin + // requested == current so the policy engine's staleness check is a + // no-op and only the developer-mode/address-space gate applies. + let policy = evaluateWritePolicy(ASFWMCPPolicyRequest( + operationType: .write, + addressSpace: .ohciController, + requestedGeneration: currentGen, + currentGeneration: currentGen, + dryRun: try decoder.bool("dryRun", default: false) + )) + guard policy.reachesDriverWritePath else { + return .failure(toolName: toolName, + code: policy.isDryRun ? .dryRunOnly : (policy.errorCode ?? .policyDenied), + reason: policy.reason, + data: .object(["policy": policy.mcpValue])) + } + let receipt = await driver.executeContinuousTxSilence(targetGuid: targetGuid, start: start) + return receipt.ok + ? .success(toolName: toolName, data: .object([ + "kind": .string("continuousTxSilenceCadence"), + "receipt": receipt.mcpValue, + "policy": policy.mcpValue, + ])) + : .failure(toolName: toolName, code: .rcodeError, + reason: "The driver rejected the continuous TX silence \(start ? "start" : "stop") request.", + data: .object(["receipt": receipt.mcpValue, "policy": policy.mcpValue])) + } catch { + return malformedToolResult(toolName, reason: error.localizedDescription) + } + } + + func continuousTxHealthResult( + toolName: String, + decoder: ASFWMCPToolArgumentDecoder + ) async -> ASFWMCPToolCallResult { + do { + let targetGuid = try decoder.uint64("targetGuid") + let receipt = await driver.executeContinuousTxHealth(targetGuid: targetGuid) + return receipt.ok + ? .success(toolName: toolName, data: receipt.mcpValue) + : .failure(toolName: toolName, code: .capabilityUnavailable, + reason: "No continuous TX silence cadence is active for this GUID.", + data: receipt.mcpValue) + } catch { + return malformedToolResult(toolName, reason: error.localizedDescription) + } + } + func cmpReadPcrResult( toolName: String, decoder: ASFWMCPToolArgumentDecoder diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index f77bb43e..f531aec9 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -1022,6 +1022,117 @@ kern_return_t ASFWDriver::StopAudioStreaming(uint64_t guid) { return ivars->context->audioCoordinator->StopStreaming(guid); } +kern_return_t ASFWDriver::StartContinuousIsochTxSilence(uint64_t guid) { + if (!ivars || !ivars->context) { + return kIOReturnNotReady; + } + auto& ctx = *ivars->context; + if (!ctx.deps.deviceRegistry || !ctx.deps.audioRuntimeRegistry || + !ctx.deps.hardware) { + return kIOReturnNotReady; + } + auto* record = ctx.deps.deviceRegistry->FindByGuid(guid); + auto protocol = ctx.deps.audioRuntimeRegistry->FindShared(guid); + if (!record || !protocol) { + return kIOReturnNotReady; + } + + ASFW::Audio::PlaybackPreflightRoute route{}; + if (!protocol->GetPlaybackPreflightRoute(route)) { + ASFW_LOG_WARNING(Audio, + "[RME] Continuous TX silence start refused: no verified playback route GUID=0x%016llx", + guid); + return kIOReturnNotReady; + } + + ASFW_LOG(Audio, "[RME] Continuous TX silence start (dev) GUID=0x%016llx", guid); + return protocol->StartContinuousPlaybackIntegration( + route, + [&ctx, route]() -> IOReturn { + IOReturn kr = ctx.isoch.PrepareTransmit( + route.channel, *ctx.deps.hardware, 0U); + if (kr == kIOReturnSuccess) { + kr = ctx.isoch.PrimePreparedTransmitForPreflight(); + } + if (kr == kIOReturnSuccess) { + kr = ctx.isoch + .PrepareTransmitBoundedCircularSilenceCadenceForPreflight(); + } + return kr; + }, + [&ctx]() -> IOReturn { + return ctx.isoch.StartTransmitContinuousCircularSilenceCadence(); + }); +} + +kern_return_t ASFWDriver::StopContinuousIsochTxSilence(uint64_t guid) { + if (!ivars || !ivars->context) { + return kIOReturnNotReady; + } + auto& ctx = *ivars->context; + if (!ctx.deps.audioRuntimeRegistry) { + return kIOReturnNotReady; + } + auto protocol = ctx.deps.audioRuntimeRegistry->FindShared(guid); + if (!protocol) { + return kIOReturnNotReady; + } + + ASFW_LOG(Audio, "[RME] Continuous TX silence stop (dev) GUID=0x%016llx", guid); + return protocol->StopContinuousPlaybackIntegration( + [&ctx]() -> IOReturn { + return ctx.isoch.StopTransmitContinuousCircularSilenceCadence(); + }); +} + +kern_return_t ASFWDriver::GetContinuousIsochTxHealth( + uint64_t guid, + bool* running, + bool* dead, + bool* eventError, + uint32_t* anchorExecutions, + uint16_t* lastAnchorTimestamp) { + if (!running || !dead || !eventError || !anchorExecutions || + !lastAnchorTimestamp) { + return kIOReturnBadArgument; + } + *running = false; + *dead = false; + *eventError = false; + *anchorExecutions = 0U; + *lastAnchorTimestamp = 0U; + + if (!ivars || !ivars->context) { + return kIOReturnNotReady; + } + auto& ctx = *ivars->context; + if (!ctx.deps.audioRuntimeRegistry) { + return kIOReturnNotReady; + } + auto protocol = ctx.deps.audioRuntimeRegistry->FindShared(guid); + if (!protocol) { + return kIOReturnNotReady; + } + + return protocol->GetContinuousPlaybackIntegrationHealth( + [&ctx, running, dead, eventError, anchorExecutions, + lastAnchorTimestamp](ASFW::Audio::ContinuousPlaybackHealth& health) -> IOReturn { + const auto txHealth = + ctx.isoch.PollTransmitContinuousCircularSilenceCadenceHealth(); + health.running = txHealth.running; + health.dead = txHealth.dead; + health.eventError = txHealth.eventError; + health.anchorExecutions = txHealth.anchorExecutions; + health.lastAnchorTimestamp = txHealth.lastAnchorTimestamp; + *running = health.running; + *dead = health.dead; + *eventError = health.eventError; + *anchorExecutions = health.anchorExecutions; + *lastAnchorTimestamp = health.lastAnchorTimestamp; + return kIOReturnSuccess; + }); +} + kern_return_t ASFWDriver::StartIsochReceive(uint8_t channel, uint32_t wireFormatRaw, uint32_t am824Slots) { if (!ivars || !ivars->context) { return kIOReturnNotReady; diff --git a/ASFWDriver/ASFWDriver.iig b/ASFWDriver/ASFWDriver.iig index 65f7fcf1..d0a98602 100644 --- a/ASFWDriver/ASFWDriver.iig +++ b/ASFWDriver/ASFWDriver.iig @@ -111,6 +111,19 @@ public: kern_return_t StartAudioStreaming(uint64_t guid) LOCALONLY; kern_return_t StopAudioStreaming(uint64_t guid) LOCALONLY; + // Stage 6 (dev-only): continuous silent isoch TX cadence for a device + // protocol that implements IDeviceProtocol's continuous-playback hooks + // (currently only RME Fireface 800). Never reachable from Core Audio + // StartIO -- reached only via this UserClient/MCP path. + kern_return_t StartContinuousIsochTxSilence(uint64_t guid) LOCALONLY; + kern_return_t StopContinuousIsochTxSilence(uint64_t guid) LOCALONLY; + kern_return_t GetContinuousIsochTxHealth(uint64_t guid, + bool* running, + bool* dead, + bool* eventError, + uint32_t* anchorExecutions, + uint16_t* lastAnchorTimestamp) LOCALONLY; + // Isochronous Receive Control kern_return_t StartIsochReceive(uint8_t channel, uint32_t wireFormatRaw, uint32_t am824Slots) LOCALONLY; kern_return_t StopIsochReceive() LOCALONLY; diff --git a/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp b/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp index 9529fec4..56d11e40 100644 --- a/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp @@ -34,6 +34,17 @@ struct PlaybackPreflightRoute final { bool deviceCommunicationStopped{false}; }; +/// Protocol-neutral snapshot of a continuous host transmit ring's health. +/// Mirrors (but does not include/depend on) the transport-layer health +/// struct -- IDeviceProtocol must not reach into Isoch/OHCI types. +struct ContinuousPlaybackHealth final { + bool running{false}; + bool dead{false}; + bool eventError{false}; + uint32_t anchorExecutions{0U}; + uint16_t lastAnchorTimestamp{0U}; +}; + /// Interface for device-specific protocol handlers /// /// Device protocols are instantiated by DeviceProtocolFactory when a @@ -141,6 +152,40 @@ class IDeviceProtocol { return kIOReturnUnsupported; } + using ContinuousHealthStep = std::function; + + /// Optional dev-only continuous variant of the staged integration hook + /// above: arms the device engine and starts the host ring, then returns + /// immediately -- the ring keeps running until StopContinuousPlaybackIntegration + /// is called. Never reachable from Core Audio StartIO; must never make + /// StartIO succeed. + virtual IOReturn StartContinuousPlaybackIntegration( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep startHostRing) { + (void)route; + (void)prepareHost; + (void)startHostRing; + return kIOReturnUnsupported; + } + + /// Stops a continuous stream started above: stops the host ring, then + /// stops the device engine and releases its held IRM route. Idempotent + /// -- returns kIOReturnNotReady if nothing is in flight. + virtual IOReturn StopContinuousPlaybackIntegration( + BoundedPlaybackStep stopHostRing) { + (void)stopHostRing; + return kIOReturnUnsupported; + } + + /// Non-blocking health snapshot of an in-flight continuous stream. + /// Returns kIOReturnNotReady if nothing is in flight. + virtual IOReturn GetContinuousPlaybackIntegrationHealth( + ContinuousHealthStep pollHealth) { + (void)pollHealth; + return kIOReturnUnsupported; + } + /// Optional protocol-neutral duplex control interface used by the audio lifecycle. virtual IDuplexDeviceControl* AsDuplexDeviceControl() noexcept { return nullptr; diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp index 06977d4c..93b20e54 100644 --- a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp @@ -241,6 +241,53 @@ IOReturn RMEFireface800Protocol::RunBoundedPlaybackIntegrationPreflight( hostCleanupStatus); } + uint8_t reservedChannel = 0xFFU; + uint32_t reservedBandwidth = 0U; + const IOReturn stopReleaseStatus = StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, reservedChannel, reservedBandwidth); + if (stopReleaseStatus != kIOReturnSuccess && + finalStatus == kIOReturnSuccess) { + finalStatus = stopReleaseStatus; + } + + stage5hInFlight_.store(false, std::memory_order_release); + + if (stopAccepted && releaseAccepted) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H FF800 playback engine stopped and IRM route released channel=%u bandwidth=%u", + reservedChannel, + reservedBandwidth); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H cleanup completed with errors startAccepted=%u stopAccepted=%u releaseAccepted=%u", + startAccepted ? 1U : 0U, + stopAccepted ? 1U : 0U, + releaseAccepted ? 1U : 0U); + } + + if (finalStatus == kIOReturnSuccess) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H bounded circular silent playback integration passed; Core Audio StartIO remains rejected"); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H bounded circular silent playback integration failed kr=0x%x; Core Audio StartIO remains rejected", + finalStatus); + } + return finalStatus; +} + +IOReturn RMEFireface800Protocol::StopDeviceEngineAndReleaseIrm( + bool& outStopAccepted, + bool& outReleaseAccepted, + uint8_t& outReleasedChannel, + uint32_t& outReleasedBandwidth) noexcept { + outStopAccepted = false; + outReleaseAccepted = false; + outReleasedChannel = 0xFFU; + outReleasedBandwidth = 0U; + + IOReturn status = kIOReturnSuccess; + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; const auto stopGeneration = busInfo_.GetGeneration(); const auto stopNode = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; @@ -266,16 +313,14 @@ IOReturn RMEFireface800Protocol::RunBoundedPlaybackIntegrationPreflight( }, 500U, kIOReturnTimeout); - stopAccepted = + outStopAccepted = stopResult.status == kIOReturnSuccess && stopResult.value; - if (!stopAccepted && finalStatus == kIOReturnSuccess) { - finalStatus = stopResult.status == kIOReturnSuccess + if (!outStopAccepted) { + status = stopResult.status == kIOReturnSuccess ? kIOReturnIOError : stopResult.status; - } - if (!stopAccepted) { ASFW_LOG_ERROR(Audio, - "[RME] Stage 5H FF800 stop command failed kr=0x%x gen=%u", + "[RME] FF800 stop command failed kr=0x%x gen=%u", stopResult.status, stopGeneration.value); } @@ -290,73 +335,240 @@ IOReturn RMEFireface800Protocol::RunBoundedPlaybackIntegrationPreflight( playbackResourcesReserved_ = false; reservedPlaybackChannel_ = 0xFFU; reservedPlaybackBandwidthUnits_ = 0U; + outReleasedChannel = reservedChannel; + outReleasedBandwidth = reservedBandwidth; if (!hadReservation || irmClient_ == nullptr || reservedChannel > 63U || reservedBandwidth == 0U) { ASFW_LOG_ERROR(Audio, - "[RME] Stage 5H cleanup missing held IRM route expectedChannel=%u expectedBandwidth=%u actualChannel=%u actualBandwidth=%u", - route.channel, - route.bandwidthUnits, + "[RME] cleanup missing held IRM route actualChannel=%u actualBandwidth=%u", reservedChannel, reservedBandwidth); - if (finalStatus == kIOReturnSuccess) { - finalStatus = kIOReturnNotReady; + if (status == kIOReturnSuccess) { + status = kIOReturnNotReady; } - } else { - const auto releaseResult = ASFW::Audio::WaitForAsyncResult( - [this, reservedChannel, reservedBandwidth](auto done) { - irmClient_->ReleaseResources( - reservedChannel, - reservedBandwidth, - [done](IRM::AllocationStatus status) mutable { - const bool accepted = - status == IRM::AllocationStatus::Success; - done(accepted ? kIOReturnSuccess : kIOReturnIOError, - accepted); - }); - }, - 1000U, - kIOReturnTimeout); - releaseAccepted = - releaseResult.status == kIOReturnSuccess && releaseResult.value; - if (!releaseAccepted && finalStatus == kIOReturnSuccess) { - finalStatus = releaseResult.status == kIOReturnSuccess + return status; + } + + const auto releaseResult = ASFW::Audio::WaitForAsyncResult( + [this, reservedChannel, reservedBandwidth](auto done) { + irmClient_->ReleaseResources( + reservedChannel, + reservedBandwidth, + [done](IRM::AllocationStatus status) mutable { + const bool accepted = + status == IRM::AllocationStatus::Success; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + }, + 1000U, + kIOReturnTimeout); + outReleaseAccepted = + releaseResult.status == kIOReturnSuccess && releaseResult.value; + if (!outReleaseAccepted) { + if (status == kIOReturnSuccess) { + status = releaseResult.status == kIOReturnSuccess ? kIOReturnIOError : releaseResult.status; } - if (!releaseAccepted) { - ASFW_LOG_ERROR(Audio, - "[RME] Stage 5H IRM release failed kr=0x%x channel=%u bandwidth=%u", - releaseResult.status, - reservedChannel, - reservedBandwidth); + ASFW_LOG_ERROR(Audio, + "[RME] IRM release failed kr=0x%x channel=%u bandwidth=%u", + releaseResult.status, + reservedChannel, + reservedBandwidth); + } + + return status; +} + +IOReturn RMEFireface800Protocol::StartContinuousPlaybackIntegration( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep startHostRing) { + if (!active_.load(std::memory_order_acquire) || !prepareHost || + !startHostRing || + route.channel > 63U || route.bandwidthUnits != 1286U || + route.sampleRateHz != 192000U || + !route.deviceCommunicationStopped) { + return kIOReturnBadArgument; + } + + PlaybackPreflightRoute heldRoute{}; + if (!GetPlaybackPreflightRoute(heldRoute) || + heldRoute.channel != route.channel || + heldRoute.bandwidthUnits != route.bandwidthUnits || + heldRoute.sampleRateHz != route.sampleRateHz || + !heldRoute.deviceCommunicationStopped) { + return kIOReturnNotReady; + } + + bool expectedStage5h = false; + bool expectedContinuous = false; + if (stage5hInFlight_.load(std::memory_order_acquire) || + !continuousPlaybackInFlight_.compare_exchange_strong( + expectedContinuous, true, std::memory_order_acq_rel)) { + ASFW_LOG_WARNING(Audio, + "[RME] Continuous integration start refused: already in flight (stage5h=%u continuous=%u) GUID=0x%016llx", + expectedStage5h ? 1U : 0U, + expectedContinuous ? 1U : 0U, + deviceGuid_); + return kIOReturnBusy; + } + + // Consume the held route, mirroring RunBoundedPlaybackIntegrationPreflight: + // repeated Start calls while a session is live must not re-enter here. + playbackPreflightRoute_.store(0U, std::memory_order_release); + + IOReturn finalStatus = prepareHost(); + const bool hostPrepared = finalStatus == kIOReturnSuccess; + if (!hostPrepared) { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration host cadence preparation failed kr=0x%x", + finalStatus); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return finalStatus; + } + + const uint32_t dataBlockQuadlets = DecodeChannelCount(route.sampleRateHz); + if (dataBlockQuadlets == 0U) { + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return kIOReturnBadArgument; + } + + // Exact Stage 4D/5F/5H FF800 begin-session value: communication enable, + // S800 flag, and the 192 kHz data-block-quadlet count. + const uint32_t startValue = + 0x80000000U | 0x00000800U | dataBlockQuadlets; + const std::array startLE = { + static_cast(startValue & 0xFFU), + static_cast((startValue >> 8U) & 0xFFU), + static_cast((startValue >> 16U) & 0xFFU), + static_cast((startValue >> 24U) & 0xFFU), + }; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const auto startResult = ASFW::Audio::WaitForAsyncResult( + [this, generation, node, startLE](auto done) { + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStartAddress), + std::span{startLE}, + FW::FwSpeed::S400, + [done](Async::AsyncStatus status, + std::span payload) mutable { + (void)payload; + const bool accepted = + status == Async::AsyncStatus::kSuccess; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + if (!probeHandle_.IsValid()) { + done(kIOReturnNoResources, false); + } + }, + 500U, + kIOReturnTimeout); + const bool startAccepted = + startResult.status == kIOReturnSuccess && startResult.value; + if (!startAccepted) { + if (startResult.status == kIOReturnTimeout && + probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); } + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration FF800 playback-engine start failed kr=0x%x gen=%u value=0x%08x", + startResult.status, + generation.value, + startValue); + bool stopAccepted = false; + bool releaseAccepted = false; + uint8_t releasedChannel = 0xFFU; + uint32_t releasedBandwidth = 0U; + (void)StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, releasedChannel, releasedBandwidth); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return startResult.status == kIOReturnSuccess ? kIOReturnIOError + : startResult.status; } - stage5hInFlight_.store(false, std::memory_order_release); + ASFW_LOG(Audio, + "[RME] ✅ Continuous integration FF800 playback engine started channel=%u rate=%u value=0x%08x", + route.channel, + route.sampleRateHz, + startValue); + + finalStatus = startHostRing(); + if (finalStatus != kIOReturnSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration host ring start failed kr=0x%x; stopping device engine", + finalStatus); + bool stopAccepted = false; + bool releaseAccepted = false; + uint8_t releasedChannel = 0xFFU; + uint32_t releasedBandwidth = 0U; + (void)StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, releasedChannel, releasedBandwidth); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return finalStatus; + } + + ASFW_LOG(Audio, + "[RME] ✅ Continuous circular silent playback integration started; Core Audio StartIO remains rejected GUID=0x%016llx", + deviceGuid_); + return kIOReturnSuccess; +} + +IOReturn RMEFireface800Protocol::StopContinuousPlaybackIntegration( + BoundedPlaybackStep stopHostRing) { + bool expected = true; + if (!continuousPlaybackInFlight_.compare_exchange_strong( + expected, false, std::memory_order_acq_rel)) { + return kIOReturnNotReady; + } + + const IOReturn hostStopStatus = stopHostRing ? stopHostRing() + : kIOReturnUnsupported; + if (hostStopStatus != kIOReturnSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration host ring stop failed kr=0x%x; proceeding with device/IRM teardown", + hostStopStatus); + } + + bool stopAccepted = false; + bool releaseAccepted = false; + uint8_t releasedChannel = 0xFFU; + uint32_t releasedBandwidth = 0U; + const IOReturn stopReleaseStatus = StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, releasedChannel, releasedBandwidth); if (stopAccepted && releaseAccepted) { ASFW_LOG(Audio, - "[RME] ✅ Stage 5H FF800 playback engine stopped and IRM route released channel=%u bandwidth=%u", - reservedChannel, - reservedBandwidth); + "[RME] ✅ Continuous integration FF800 playback engine stopped and IRM route released channel=%u bandwidth=%u", + releasedChannel, + releasedBandwidth); } else { ASFW_LOG_ERROR(Audio, - "[RME] Stage 5H cleanup completed with errors startAccepted=%u stopAccepted=%u releaseAccepted=%u", - startAccepted ? 1U : 0U, + "[RME] Continuous integration cleanup completed with errors stopAccepted=%u releaseAccepted=%u", stopAccepted ? 1U : 0U, releaseAccepted ? 1U : 0U); } - if (finalStatus == kIOReturnSuccess) { - ASFW_LOG(Audio, - "[RME] ✅ Stage 5H bounded circular silent playback integration passed; Core Audio StartIO remains rejected"); - } else { - ASFW_LOG_ERROR(Audio, - "[RME] Stage 5H bounded circular silent playback integration failed kr=0x%x; Core Audio StartIO remains rejected", - finalStatus); + return hostStopStatus != kIOReturnSuccess ? hostStopStatus + : stopReleaseStatus; +} + +IOReturn RMEFireface800Protocol::GetContinuousPlaybackIntegrationHealth( + ContinuousHealthStep pollHealth) { + if (!continuousPlaybackInFlight_.load(std::memory_order_acquire)) { + return kIOReturnNotReady; } - return finalStatus; + if (!pollHealth) { + return kIOReturnBadArgument; + } + ContinuousPlaybackHealth health{}; + return pollHealth(health); } void RMEFireface800Protocol::ProbeClockConfig() noexcept { @@ -1270,6 +1482,7 @@ void RMEFireface800Protocol::BestEffortStopDeviceCommunication() noexcept { void RMEFireface800Protocol::ReleaseReservedResources() noexcept { playbackPreflightRoute_.store(0U, std::memory_order_release); stage5hInFlight_.store(false, std::memory_order_release); + continuousPlaybackInFlight_.store(false, std::memory_order_release); if (!playbackResourcesReserved_ || irmClient_ == nullptr) { return; } diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp index 32d4f1ff..e1b55ae2 100644 --- a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp @@ -39,6 +39,15 @@ class RMEFireface800Protocol final : public IDeviceProtocol { BoundedPlaybackStep runHostBurst, BoundedPlaybackStep cleanupHost) override; + IOReturn StartContinuousPlaybackIntegration( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep startHostRing) override; + IOReturn StopContinuousPlaybackIntegration( + BoundedPlaybackStep stopHostRing) override; + IOReturn GetContinuousPlaybackIntegrationHealth( + ContinuousHealthStep pollHealth) override; + private: void ProbeClockConfig() noexcept; void ValidateStfWritePath(uint32_t rate) noexcept; @@ -56,6 +65,17 @@ class RMEFireface800Protocol final : public IDeviceProtocol { void ReleaseReservedResources() noexcept; void LogStreamCaps(uint32_t rate) const noexcept; + // Shared FF800-stop + IRM-release tail used by both the Stage 5H bounded + // burst and the Stage 6 continuous stream. Sends the FF800 isoch-comm + // stop command, clears device-TX-allocation state, then releases the + // held IRM reservation (consuming playbackResourcesReserved_/ + // reservedPlaybackChannel_/reservedPlaybackBandwidthUnits_). Returns + // kIOReturnSuccess only if both steps were accepted. + IOReturn StopDeviceEngineAndReleaseIrm(bool& outStopAccepted, + bool& outReleaseAccepted, + uint8_t& outReleasedChannel, + uint32_t& outReleasedBandwidth) noexcept; + static uint32_t ReadLE32(const uint8_t* bytes) noexcept; static uint32_t DecodeSampleRate(uint32_t value) noexcept; static const char* DecodeClockSource(uint32_t value) noexcept; @@ -88,6 +108,17 @@ class RMEFireface800Protocol final : public IDeviceProtocol { bool deviceTxAllocated_{false}; uint8_t deviceTxChannel_{0xFF}; std::atomic stage5hInFlight_{false}; + + // Stage 6 (dev-only continuous silence): separate in-flight guard from + // stage5hInFlight_ so Stage 5H's existing guard/call sites stay + // untouched. RunBoundedPlaybackIntegrationPreflight and + // StartContinuousPlaybackIntegration each check BOTH flags before + // proceeding -- they drive the same OHCI context/IRM route and must + // never run concurrently. Reuses reservedPlaybackChannel_/ + // reservedPlaybackBandwidthUnits_/playbackResourcesReserved_ above to + // hold state across the Start/Stop call gap (safe: mutually exclusive + // with Stage 5H by the two-flag guard). + std::atomic continuousPlaybackInFlight_{false}; }; } // namespace ASFW::Audio::RME diff --git a/ASFWDriver/Isoch/IsochService.cpp b/ASFWDriver/Isoch/IsochService.cpp index 11e584f9..9f1b6083 100644 --- a/ASFWDriver/Isoch/IsochService.cpp +++ b/ASFWDriver/Isoch/IsochService.cpp @@ -506,6 +506,32 @@ IsochService::CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight() ->CleanupBoundedCircularSilenceCadenceForPreflight(); } +kern_return_t +IsochService::StartTransmitContinuousCircularSilenceCadence() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: starting continuous circular silence ring (dev) interrupts=0 refill=0"); + return isochTransmitContext_->StartContinuousCircularSilenceCadence(); +} + +IsochTransmitContext::ContinuousCadenceHealth +IsochService::PollTransmitContinuousCircularSilenceCadenceHealth() { + if (!isochTransmitContext_) { + return {}; + } + return isochTransmitContext_ + ->PollContinuousCircularSilenceCadenceHealth(); +} + +kern_return_t IsochService::StopTransmitContinuousCircularSilenceCadence() { + if (!isochTransmitContext_) { + return kIOReturnSuccess; + } + return isochTransmitContext_->StopContinuousCircularSilenceCadence(); +} + kern_return_t IsochService::StartPreparedTransmit() { if (!isochTransmitContext_) { return kIOReturnNotReady; diff --git a/ASFWDriver/Isoch/IsochService.hpp b/ASFWDriver/Isoch/IsochService.hpp index e394dc7f..5f9ae11c 100644 --- a/ASFWDriver/Isoch/IsochService.hpp +++ b/ASFWDriver/Isoch/IsochService.hpp @@ -102,6 +102,13 @@ class IsochService { uint32_t durationMs); kern_return_t CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight(); + // Stage 6 (dev-only continuous silence): same immutable ring as above, + // run indefinitely instead of for a bounded duration. + kern_return_t StartTransmitContinuousCircularSilenceCadence(); + ASFW::Isoch::IsochTransmitContext::ContinuousCadenceHealth + PollTransmitContinuousCircularSilenceCadenceHealth(); + kern_return_t StopTransmitContinuousCircularSilenceCadence(); + kern_return_t StopTransmit(); kern_return_t BeginSplitDuplex(uint64_t guid); diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp index 89480886..b5b4dbbc 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp @@ -1302,6 +1302,140 @@ IsochTransmitContext::CleanupBoundedCircularSilenceCadenceForPreflight() return kIOReturnSuccess; } +kern_return_t +IsochTransmitContext::StartContinuousCircularSilenceCadence() noexcept { + if (!boundedCircularCadencePrepared_ || + (state_ != State::Configured && state_ != State::Stopped)) { + ASFW_LOG(Isoch, + "IT: Continuous cadence start rejected prepared=%u state=%{public}s", + boundedCircularCadencePrepared_ ? 1U : 0U, + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_) { + return kIOReturnNoResources; + } + + const auto program = boundedCircularCadenceProgram_; + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Continuous cadence RUN gate failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + continuousAnchorSeen_ = false; + continuousAnchorExecutions_ = 0U; + continuousPreviousAnchorTimestamp_ = 0U; + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + // One settle delay + single readback/anchor-fetch to confirm the ring + // actually started -- not a sustain-and-verify loop like Stage 5H's run + // method. The hardware keeps looping on its own from here. + IODelay(250U); + const uint32_t controlAfterRun = hardware_->Read(ctrlReg); + const bool active = + (controlAfterRun & Driver::ContextControl::kActive) != 0U; + const bool dead = + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + const auto anchor = ring_.FetchCadenceAnchorCompletionForPreflight( + program.startPacketSlot); + if (anchor.transferStatus != 0U) { + const uint8_t eventCode = + static_cast(anchor.transferStatus & 0x1FU); + if (eventCode == 0x11U) { + continuousAnchorSeen_ = true; + continuousAnchorExecutions_ = 1U; + continuousPreviousAnchorTimestamp_ = anchor.timestamp; + } + } + + if (dead || !active) { + ASFW_LOG(Isoch, + "IT: Continuous cadence start failed to sustain control=0x%08x active=%u dead=%u", + controlAfterRun, + active ? 1U : 0U, + dead ? 1U : 0U); + (void)CleanupBoundedCircularSilenceCadenceForPreflight(); + return dead ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + continuousCadenceRunning_ = true; + ASFW_LOG(Isoch, + "IT: ✅ Continuous circular silence cadence started channel/descriptors=%u dataPackets=%u skip=%u anchorConfirmed=%u", + program.descriptorCount, + program.dataPacketCount, + program.skipPacketCount, + continuousAnchorSeen_ ? 1U : 0U); + return kIOReturnSuccess; +} + +IsochTransmitContext::ContinuousCadenceHealth +IsochTransmitContext::PollContinuousCircularSilenceCadenceHealth() noexcept { + ContinuousCadenceHealth health{}; + if (!continuousCadenceRunning_ || !hardware_) { + return health; + } + health.running = true; + + const auto program = boundedCircularCadenceProgram_; + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const uint32_t control = hardware_->Read(ctrlReg); + health.dead = (control & Driver::ContextControl::kDead) != 0U; + + const auto anchor = ring_.FetchCadenceAnchorCompletionForPreflight( + program.startPacketSlot); + if (anchor.transferStatus != 0U) { + const uint8_t eventCode = + static_cast(anchor.transferStatus & 0x1FU); + if (eventCode != 0x11U) { + health.eventError = true; + } else if (!continuousAnchorSeen_) { + continuousAnchorSeen_ = true; + continuousAnchorExecutions_ = 1U; + continuousPreviousAnchorTimestamp_ = anchor.timestamp; + } else if (anchor.timestamp != continuousPreviousAnchorTimestamp_) { + continuousPreviousAnchorTimestamp_ = anchor.timestamp; + ++continuousAnchorExecutions_; + } + } + + health.anchorExecutions = continuousAnchorExecutions_; + health.lastAnchorTimestamp = continuousPreviousAnchorTimestamp_; + return health; +} + +kern_return_t +IsochTransmitContext::StopContinuousCircularSilenceCadence() noexcept { + const bool wasRunning = continuousCadenceRunning_; + continuousCadenceRunning_ = false; + const kern_return_t status = + CleanupBoundedCircularSilenceCadenceForPreflight(); + if (wasRunning) { + ASFW_LOG(Isoch, + "IT: Continuous circular silence cadence stopped anchorExecutions=%u stopKr=0x%x", + continuousAnchorExecutions_, + status); + } + return status; +} + kern_return_t IsochTransmitContext::Start() noexcept { if (state_ != State::Configured && state_ != State::Stopped) { ASFW_LOG(Isoch, "IT: Start rejected - state=%{public}s", TxStateName(state_)); @@ -1401,6 +1535,15 @@ kern_return_t IsochTransmitContext::Start() noexcept { } void IsochTransmitContext::Stop() noexcept { + // Safety net: a Stage 6 continuous cadence deliberately never enters + // State::Running (see StartContinuousCircularSilenceCadence), so the + // RUN-clear block below would otherwise never fire for it. Without this, + // a driver teardown/unload while a continuous stream is active would + // leave the OHCI IT context spinning in RUN state. + if (continuousCadenceRunning_) { + (void)StopContinuousCircularSilenceCadence(); + } + finiteCadencePrepared_ = false; finiteCadenceProgram_ = {}; boundedCircularCadencePrepared_ = false; diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp index 74e18185..50591b3d 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp @@ -141,6 +141,35 @@ class IsochTransmitContext final { // Stage 5H unconditional teardown seam used on success and every partial // failure before the protocol stops the device and releases IRM. kern_return_t CleanupBoundedCircularSilenceCadenceForPreflight() noexcept; + + // Stage 6 (dev-only continuous silence): reuses the exact same immutable + // self-looping ring PrepareBoundedCircularSilenceCadenceForPreflight() + // already built for Stage 5H -- that ring has no notion of "bounded", the + // duration limit lives entirely in the Stage 5H run/poll loop below. This + // sets RUN once, confirms one anchor execution, and returns immediately; + // the hardware keeps looping the ring with zero interrupts/software + // involvement until StopContinuousCircularSilenceCadence() is called. + kern_return_t StartContinuousCircularSilenceCadence() noexcept; + + struct ContinuousCadenceHealth final { + bool running{false}; + bool dead{false}; + bool eventError{false}; + uint32_t anchorExecutions{0U}; + uint16_t lastAnchorTimestamp{0U}; + }; + + // Non-blocking single-shot status read (one register read + one anchor + // fetch, no IODelay/poll loop) -- safe to call from a periodic watchdog + // tick. Returns a default (running=false) snapshot when no continuous + // cadence is active. + [[nodiscard]] ContinuousCadenceHealth + PollContinuousCircularSilenceCadenceHealth() noexcept; + + // Stops the ring via the same CleanupBoundedCircularSilenceCadenceForPreflight() + // teardown Stage 5H already uses (RUN clear, ACTIVE-drain poll, DEAD check). + kern_return_t StopContinuousCircularSilenceCadence() noexcept; + void Stop() noexcept; void Poll() noexcept; @@ -208,6 +237,14 @@ class IsochTransmitContext final { Tx::IsochTxDmaRing::BoundedCircularSilenceCadenceProgram boundedCircularCadenceProgram_{}; bool boundedCircularCadencePrepared_{false}; + + // Stage 6 continuous-silence state. Mutually exclusive with the Stage 5H + // bounded run by construction (one OHCI IT context); reuses + // boundedCircularCadenceProgram_/boundedCircularCadencePrepared_ above. + bool continuousCadenceRunning_{false}; + bool continuousAnchorSeen_{false}; + uint32_t continuousAnchorExecutions_{0U}; + uint16_t continuousPreviousAnchorTimestamp_{0U}; }; } // namespace Isoch diff --git a/ASFWDriver/Scheduling/WatchdogCoordinator.cpp b/ASFWDriver/Scheduling/WatchdogCoordinator.cpp index f4a2e45c..f201e137 100644 --- a/ASFWDriver/Scheduling/WatchdogCoordinator.cpp +++ b/ASFWDriver/Scheduling/WatchdogCoordinator.cpp @@ -10,6 +10,7 @@ #include "../Isoch/IsochReceiveContext.hpp" #include "../Isoch/Transmit/IsochTransmitContext.hpp" #include "../Logging/LogConfig.hpp" +#include "../Logging/Logging.hpp" namespace ASFW::Driver { namespace { @@ -27,6 +28,10 @@ constexpr uint32_t kPayloadWriterDrainIntervalTicks = 100; // so log the most recent decision once per ~1 s (1000 ticks). The line carries // the total decision count so collapsed updates are still visible. constexpr uint32_t kTxSytTraceIntervalTicks = 1000; +// Stage 6 (dev-only continuous silence) coarse heartbeat. The ring runs with +// zero interrupts by design, so this is the only liveness signal for it; +// anomalies (DEAD/event error) log immediately and independently below. +constexpr uint32_t kContinuousCadenceHeartbeatIntervalTicks = 1000; uint64_t MicrosecondsToMachTicks(uint64_t usec) { static mach_timebase_info_data_t timebase{0, 0}; @@ -189,6 +194,32 @@ void WatchdogCoordinator::TickIsochTransmit( isochTransmitContext->Poll(); } + // Stage 6 continuous silence cadence never enters State::Running (see + // IsochTransmitContext::StartContinuousCircularSilenceCadence), so it is + // independent of the isRunning/Poll() path above. Cheap no-op when no + // continuous cadence is active (single bool check inside the poll call). + const auto continuousHealth = + isochTransmitContext->PollContinuousCircularSilenceCadenceHealth(); + if (continuousHealth.running) { + if (continuousHealth.dead || continuousHealth.eventError) { + ASFW_LOG(Isoch, + "IT: ⚠️ Continuous cadence anomaly dead=%u eventError=%u anchorExecutions=%u", + continuousHealth.dead ? 1U : 0U, + continuousHealth.eventError ? 1U : 0U, + continuousHealth.anchorExecutions); + } + if (++continuousCadenceHeartbeatDivider_ >= + kContinuousCadenceHeartbeatIntervalTicks) { + continuousCadenceHeartbeatDivider_ = 0; + ASFW_LOG(Isoch, + "IT: Continuous cadence heartbeat anchorExecutions=%u lastAnchorTimestamp=%u", + continuousHealth.anchorExecutions, + continuousHealth.lastAnchorTimestamp); + } + } else { + continuousCadenceHeartbeatDivider_ = 0; + } + if (++itLogDivider_ < 1000) { return; } diff --git a/ASFWDriver/Scheduling/WatchdogCoordinator.hpp b/ASFWDriver/Scheduling/WatchdogCoordinator.hpp index 7f9c4112..4626fba1 100644 --- a/ASFWDriver/Scheduling/WatchdogCoordinator.hpp +++ b/ASFWDriver/Scheduling/WatchdogCoordinator.hpp @@ -56,6 +56,7 @@ class WatchdogCoordinator { uint32_t ztsLogDivider_{0}; uint32_t payloadWriterLogDivider_{0}; uint32_t txSytTraceDivider_{0}; + uint32_t continuousCadenceHeartbeatDivider_{0}; }; } // namespace ASFW::Driver diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index cf406100..48f60851 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -73,6 +73,12 @@ enum { kMethodRequestUserBusReset = 61, kMethodStartAudioStreaming = 62, kMethodStopAudioStreaming = 63, + + // Stage 6 (dev-only): continuous silent isoch TX cadence, reached only + // via this UserClient/MCP path -- never through real CoreAudio StartIO. + kMethodStartContinuousIsochTxSilence = 64, + kMethodStopContinuousIsochTxSilence = 65, + kMethodGetContinuousIsochTxHealth = 66, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, @@ -340,6 +346,45 @@ MethodDispatchResult DispatchDriverControlMethods(ASFWDriver& driver, ? driver.StartAudioStreaming(*guid) : driver.StopAudioStreaming(*guid)}; } + case kMethodStartContinuousIsochTxSilence: + case kMethodStopContinuousIsochTxSilence: { + // Full 64-bit GUID -- GetFirstScalarInput() above truncates to + // uint32_t, which real Fireface 800 GUIDs (e.g. 0x000a3500f021a1b4) + // do not fit in. + if (!arguments || !arguments->scalarInput || + arguments->scalarInputCount < 1 || arguments->scalarInput[0] == 0) { + return MethodDispatchResult{kIOReturnBadArgument}; + } + const uint64_t guid = arguments->scalarInput[0]; + return MethodDispatchResult{ + selector == kMethodStartContinuousIsochTxSilence + ? driver.StartContinuousIsochTxSilence(guid) + : driver.StopContinuousIsochTxSilence(guid)}; + } + case kMethodGetContinuousIsochTxHealth: { + if (!arguments || !arguments->scalarInput || + arguments->scalarInputCount < 1 || arguments->scalarInput[0] == 0) { + return MethodDispatchResult{kIOReturnBadArgument}; + } + const uint64_t guid = arguments->scalarInput[0]; + bool running = false; + bool dead = false; + bool eventError = false; + uint32_t anchorExecutions = 0U; + uint16_t lastAnchorTimestamp = 0U; + const kern_return_t kr = driver.GetContinuousIsochTxHealth( + guid, &running, &dead, &eventError, &anchorExecutions, + &lastAnchorTimestamp); + if (arguments->scalarOutput && arguments->scalarOutputCount >= 5) { + arguments->scalarOutput[0] = running ? 1U : 0U; + arguments->scalarOutput[1] = dead ? 1U : 0U; + arguments->scalarOutput[2] = eventError ? 1U : 0U; + arguments->scalarOutput[3] = anchorExecutions; + arguments->scalarOutput[4] = lastAnchorTimestamp; + arguments->scalarOutputCount = 5; + } + return MethodDispatchResult{kr}; + } case kMethodGetAudioAutoStart: return HandleGetAudioAutoStart(driver, arguments); case kMethodGetLogConfig: From 54a5e9e6737b81a344b37b1b4f084a86f22778ea Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 07:05:16 -0300 Subject: [PATCH 6/7] RME: fix continuous TX silence dev path missing shared TX buffer allocation StartContinuousIsochTxSilence's prepareHost step called IsochService:: PrepareTransmit directly, which requires the shared payload/metadata/ control memory to already exist. Stage 5H never had this problem because it only ever runs from inside a real Core Audio StartIO, which allocates that memory via AllocateTxIsochResources before ever reaching the RME branch. This dev-only path is driven straight from UserClient with no CoreAudio session, so the buffer was never allocated -- every start attempt failed with kIOReturnNotReady (confirmed live: kr=0xe00002d8). Fix: allocate it explicitly with the same fixed no-CIP RME wire geometry validated since Stage 5A (1536 bytes/packet: 32 frames x 12 dbs x 4 bytes, no CIP header) before calling PrepareTransmit, and free it symmetrically on stop. Also bumps ASFWDriver's CURRENT_PROJECT_VERSION from 2 to a fixed 100. Every iteration during Stage 6 development produced a dext with the same CFBundleVersion as whatever was already active system-extension-side; OSSystemExtensionRequest won't replace an equal-or-lower version, so the only path forward was systemextensionsctl uninstall -- which got stuck in "terminating for uninstall but still running" (recoverable only by a full reboot) three separate times this session. A comfortably high fixed value means future installs are a normal automatic upgrade instead. --- ASFW.xcodeproj/project.pbxproj | 4 ++-- ASFWDriver/ASFWDriver.cpp | 41 +++++++++++++++++++++++++++++++--- project.yml | 10 ++++++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index 817000ae..fdd59b77 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -3430,7 +3430,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 100; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3512,7 +3512,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 100; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index f531aec9..9da67fb1 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -1049,8 +1049,37 @@ kern_return_t ASFWDriver::StartContinuousIsochTxSilence(uint64_t guid) { return protocol->StartContinuousPlaybackIntegration( route, [&ctx, route]() -> IOReturn { - IOReturn kr = ctx.isoch.PrepareTransmit( - route.channel, *ctx.deps.hardware, 0U); + // Unlike Stage 5H (triggered from inside a real Core Audio + // StartIO, which already allocated the shared TX slab before + // ever reaching the RME branch), this dev-only path is driven + // straight from UserClient with no CoreAudio session -- the + // shared payload/metadata/control memory PrepareTransmit expects + // was never allocated. Allocate it here with the same fixed + // no-CIP RME wire geometry validated since Stage 5A: 1536 + // bytes/packet (32 frames x 12 dbs x 4 bytes, no CIP header). + IOMemoryDescriptor* rawPayload = nullptr; + IOMemoryDescriptor* rawMetadata = nullptr; + IOMemoryDescriptor* rawControl = nullptr; + const uint32_t numSlots = + ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; + const uint32_t interruptInterval = + ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; + IOReturn kr = ctx.isoch.AllocateTxIsochResources( + 0U, numSlots, 1536U, interruptInterval, + &rawPayload, &rawMetadata, &rawControl); + if (rawPayload) { + rawPayload->release(); + } + if (rawMetadata) { + rawMetadata->release(); + } + if (rawControl) { + rawControl->release(); + } + if (kr == kIOReturnSuccess) { + kr = ctx.isoch.PrepareTransmit( + route.channel, *ctx.deps.hardware, 0U); + } if (kr == kIOReturnSuccess) { kr = ctx.isoch.PrimePreparedTransmitForPreflight(); } @@ -1079,10 +1108,16 @@ kern_return_t ASFWDriver::StopContinuousIsochTxSilence(uint64_t guid) { } ASFW_LOG(Audio, "[RME] Continuous TX silence stop (dev) GUID=0x%016llx", guid); - return protocol->StopContinuousPlaybackIntegration( + const IOReturn kr = protocol->StopContinuousPlaybackIntegration( [&ctx]() -> IOReturn { return ctx.isoch.StopTransmitContinuousCircularSilenceCadence(); }); + // Symmetric with the dev-only allocation in StartContinuousIsochTxSilence + // (this path never had a real CoreAudio StartIO/StopIO to own the shared + // slab's lifetime). Safe to free unconditionally: FreeTxIsochResources() + // clears all stream slots and no other consumer holds this dev slab. + ctx.isoch.FreeTxIsochResources(); + return kr; } kern_return_t ASFWDriver::GetContinuousIsochTxHealth( diff --git a/project.yml b/project.yml index 3048aafb..2ee77c6d 100644 --- a/project.yml +++ b/project.yml @@ -157,7 +157,15 @@ targets: SWIFT_EMIT_LOC_STRINGS: YES SWIFT_VERSION: "6.0" MARKETING_VERSION: "0.2.0" - CURRENT_PROJECT_VERSION: 2 + # Bumped from 2: every rebuild-during-iteration otherwise produces a + # dext with the same CFBundleVersion as whatever is already active, + # and OSSystemExtensionRequest won't replace an equal-or-lower + # version -- the only way forward was an explicit + # systemextensionsctl uninstall, which got stuck in "terminating for + # uninstall but still running" (fixed only by a full reboot) + # repeatedly during Stage 6 development. A comfortably high fixed + # value means every future install is a normal automatic upgrade. + CURRENT_PROJECT_VERSION: 100 configs: Debug: SWIFT_OPTIMIZATION_LEVEL: -Onone From 30c63a3ba24aae1e237122e98bd70668bb90013b Mon Sep 17 00:00:00 2001 From: MM Date: Mon, 20 Jul 2026 08:31:07 -0300 Subject: [PATCH 7/7] RME: document Stage 6 continuous TX pending hardware verification The buffer-allocation fix in StartContinuousIsochTxSilence (previous commit) has not been exercised end to end on hardware yet. Every live attempt this session failed earlier, at GetPlaybackPreflightRoute() returning false, before ever reaching this function's body -- something on the system (observed pattern consistent with coreaudiod) wins the race for the single-use Stage 5F playback route within about a second of every bus reset, ahead of this dev-only MCP-triggered path's three sequential HTTP round trips. Not a Stage 6 regression: Stage 5H shows the identical symptom whenever something beats it to the route. Documented in place so the next session doesn't have to rediscover this; two candidate next steps noted in the comment. --- ASFWDriver/ASFWDriver.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index 9da67fb1..a361db68 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -1022,6 +1022,21 @@ kern_return_t ASFWDriver::StopAudioStreaming(uint64_t guid) { return ivars->context->audioCoordinator->StopStreaming(guid); } +// PENDING HARDWARE VERIFICATION (as of this commit): the buffer-allocation +// fix below is believed correct by inspection but has not been exercised on +// real hardware end to end. Every live attempt so far failed earlier, at +// GetPlaybackPreflightRoute() returning false ("no verified playback +// route"), before ever reaching this function's body. Root cause: something +// on the system (observed pattern is consistent with coreaudiod) calls the +// real Core Audio StartIO -> Stage 5H path within about a second of every +// bus reset, consuming the single-use route RMEFireface800Protocol arms +// once per Stage 5F completion, before this dev-only MCP-triggered path +// (three sequential HTTP round trips: initialize, notifications/initialized, +// tools/call) can win the race. This is a pre-existing route-contention +// characteristic (Stage 5H has the identical symptom when something beats +// it to the route), not a Stage 6 regression. Next attempt should either +// (a) find and quiesce whatever is auto-probing the device, or (b) give this +// dev path its own independent IRM reservation instead of racing Stage 5H's. kern_return_t ASFWDriver::StartContinuousIsochTxSilence(uint64_t guid) { if (!ivars || !ivars->context) { return kIOReturnNotReady;