diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/DiceRateFormats.hpp b/ASFWDriver/Audio/Protocols/DICE/Core/DiceRateFormats.hpp index 0defc0842..e8200cd8f 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/DiceRateFormats.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/DiceRateFormats.hpp @@ -6,6 +6,7 @@ #include "../../../Runtime/ResolvedAudioConfiguration.hpp" #include "../../../../Common/WireFormat.hpp" +#include #include #include #include @@ -77,14 +78,50 @@ DiceObservedFormat(const AudioStreamRuntimeCaps& caps) { return format; } +// Measured ZED-R16 layouts (#223); require matching identity and live geometry. +[[nodiscard]] inline std::optional +ZedR16MeasuredFormats(uint64_t guid, const AudioStreamRuntimeCaps& caps) { + // Match OUI/category/product, excluding serial (Linux dice.c:34-68). + if ((guid >> 32U) != 0x0004c404U || (static_cast(guid) >> 22U) != 0) + return std::nullopt; + if (caps.sampleRateHz != 44100 && caps.sampleRateHz != 48000 && caps.sampleRateHz != 88200 && + caps.sampleRateHz != 96000) + return std::nullopt; + const auto observed = DiceObservedFormat(caps); + const auto mode = DiceRateMode(caps.sampleRateHz); + if (!observed || !mode) + return std::nullopt; + DiceRateFormats measured{}; + measured[0] = DiceModeFormat{{{16, 16, 0, {}, 0}, {10, 10, 0, {}, 0}}, + {{16, 17, 1, {}, 1}, {10, 10, 0, {}, 0}}}; + measured[1] = DiceModeFormat{{{16, 16, 0, {}, 0}, {2, 2, 0, {}, 0}}, + {{16, 17, 1, {}, 1}, {2, 2, 0, {}, 0}}}; + const auto matches = [](const auto& observedStreams, const auto& measuredStreams) { + return std::ranges::equal( + observedStreams, measuredStreams, + [](const auto& observedStream, const auto& measuredStream) { + return observedStream.pcmChannels == measuredStream.pcmChannels && + observedStream.dataBlockSize == measuredStream.dataBlockSize && + observedStream.midiPortCount == measuredStream.midiPortCount; + }); + }; + if (!matches(observed->playback, measured[*mode]->playback) || + !matches(observed->capture, measured[*mode]->capture)) + return std::nullopt; + return measured; +} + /// The per-mode formats a device publishes. When the EAP read succeeded its /// formats stand for every mode and the stream registers are not consulted /// (Linux dice-stream.c:618-621): registers read before a clock select can -/// describe another mode (:635-643). Without EAP only the observed mode is -/// known (Linux's fallback); no clock probing, no guessed scaling. +/// describe another mode (:635-643). Without EAP use matching ZED-R16 layouts +/// or only the observed mode; no clock probing or guessed scaling. [[nodiscard]] inline DiceRateFormats SelectPublishedFormats(bool eapRead, DiceRateFormats eap, - const AudioStreamRuntimeCaps& caps) { + const AudioStreamRuntimeCaps& caps, + uint64_t guid = 0) { if (eapRead) return eap; + if (const auto measured = ZedR16MeasuredFormats(guid, caps)) + return *measured; DiceRateFormats observedOnly{}; const auto mode = DiceRateMode(caps.sampleRateHz); const auto observed = DiceObservedFormat(caps); diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp index 6b345e490..beab6a4ce 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp @@ -1142,7 +1142,7 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) { // (2026-10-08) used to overwrite the EAP 2x entry // with 16 inputs, and every 2x switch rolled back. formats = SelectPublishedFormats(status == kIOReturnSuccess, - formats, caps); + formats, caps, guid_); auto formations = DiceFormations(caps.deviceRateMask, formats, runtimePolicy_.exposeDeviceToHostToCoreAudio); PublishRateFormations( diff --git a/tests/audio/DiceMultirateValidationTests.cpp b/tests/audio/DiceMultirateValidationTests.cpp index 6cceacffd..47fc1c181 100644 --- a/tests/audio/DiceMultirateValidationTests.cpp +++ b/tests/audio/DiceMultirateValidationTests.cpp @@ -60,3 +60,97 @@ TEST(DiceMultirateValidation, EapFormatsAreNotOverwrittenByStaleRegisters) { EXPECT_EQ(withoutEap[1]->capture[0].pcmChannels, 16U); EXPECT_FALSE(withoutEap[2].has_value()); } + +namespace { +constexpr uint64_t kZedGuid = 0x0004c404000186a1ULL; +ASFW::Audio::AudioStreamRuntimeCaps ZedCaps(uint32_t rate) { + ASFW::Audio::AudioStreamRuntimeCaps caps{}; + caps.sampleRateHz = rate; + caps.deviceRateMask = 0x1e; + caps.deviceToHostStreamCount = caps.hostToDeviceStreamCount = 2; + const auto second = static_cast(rate > 48000 ? 2 : 10); + caps.deviceToHostStreams[0] = {.pcmChannels = 16, .am824Slots = 17, .midiPorts = 1}; + caps.hostToDeviceStreams[0] = {.pcmChannels = 16, .am824Slots = 16, .midiPorts = 0}; + caps.deviceToHostStreams[1] = {.pcmChannels = second, .am824Slots = second, .midiPorts = 0}; + caps.hostToDeviceStreams[1] = caps.deviceToHostStreams[1]; + return caps; +} +} // namespace + +TEST(DiceMultirateValidation, ZedMeasuredModesResolveFromLowAndHighRateAttachment) { + using namespace ASFW::Audio; + for (const auto attachedRate : {44100U, 48000U, 88200U, 96000U}) { + const auto caps = ZedCaps(attachedRate); + const auto modes = DICE::SelectPublishedFormats(false, {}, caps, kZedGuid); + const auto formations = DICE::DiceFormations(caps.deviceRateMask, modes); + ASSERT_EQ(formations.size(), 4U); + const auto allocation = Runtime::MaximumFormationAllocation(formations, {49152, 26, 26, 0}); + ASSERT_TRUE(allocation); + for (const auto& formation : formations) { + EXPECT_FALSE(formation.hardwareValidated); + ASSERT_EQ(formation.capture.size(), 2U); + ASSERT_EQ(formation.playback.size(), 2U); + EXPECT_EQ(formation.capture[0].dataBlockSize, 17U); + EXPECT_EQ(formation.capture[0].midiPortCount, 1U); + EXPECT_EQ(formation.playback[0].dataBlockSize, 16U); + EXPECT_EQ(formation.playback[0].midiPortCount, 0U); + const auto config = Runtime::ResolveAudioConfiguration( + formation.sampleRateHz, formations, {64, 64, 128, 128}, *allocation, 1, + Runtime::ConfigurationValidationPolicy::HardwareBatch); + ASSERT_TRUE(config) << formation.sampleRateHz; + const auto expected = formation.sampleRateHz > 48000 ? 18U : 26U; + EXPECT_EQ(config->captureChannels, expected); + EXPECT_EQ(config->playbackChannels, expected); + } + EXPECT_EQ(DICE::DiceFormations(0x06, modes).size(), 2U); + EXPECT_EQ(DICE::DiceFormations(0x60, modes).size(), 0U); + } +} + +TEST(DiceMultirateValidation, ZedFallbackRejectsOtherIdentityAndUnmeasuredGeometry) { + using namespace ASFW::Audio; + auto caps = ZedCaps(48000); + for (const auto guid : + {0ULL, kZedGuid ^ (1ULL << 40), kZedGuid ^ (1ULL << 32), kZedGuid ^ (1ULL << 22)}) + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(guid, caps)); + EXPECT_TRUE(DICE::ZedR16MeasuredFormats(kZedGuid + 1, caps)); + caps.hostToDeviceStreams[0].am824Slots = 17; + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps)); + caps = ZedCaps(48000); + caps.hostToDeviceStreams[1].pcmChannels = 8; + caps.hostToDeviceStreams[1].am824Slots = 8; + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps)); + caps = ZedCaps(48000); + caps.deviceToHostStreams[0].midiPorts = 2; + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps)); + caps = ZedCaps(48000); + caps.sampleRateHz = 96000; // stale low-rate descriptors + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps)); + caps = ZedCaps(32000); // unsupported attachment rate + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps)); + caps = ZedCaps(48000); + caps.hostToDeviceStreamCount = 1; + EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps)); +} + +TEST(DiceMultirateValidation, ZedNeverOverridesSuccessfulEapEvenWhenEmpty) { + using namespace ASFW::Audio; + const auto caps = ZedCaps(48000); + DICE::DiceRateFormats eap{}; + EXPECT_FALSE(DICE::SelectPublishedFormats(true, eap, caps, kZedGuid)[0]); + eap[1] = DICE::DiceModeFormat{{{8, 8, 0, {}, 0}}, {{8, 8, 0, {}, 0}}}; + const auto modes = DICE::SelectPublishedFormats(true, eap, caps, kZedGuid); + EXPECT_FALSE(modes[0]); + ASSERT_TRUE(modes[1]); + EXPECT_EQ(modes[1]->capture[0].pcmChannels, 8U); +} + +TEST(DiceMultirateValidation, NonZedIdentityKeepsObservedModeOnly) { + using namespace ASFW::Audio; + const auto caps = ZedCaps(48000); + const auto modes = DICE::SelectPublishedFormats(false, {}, caps, kZedGuid ^ (1ULL << 22)); + ASSERT_TRUE(modes[0]); + EXPECT_FALSE(modes[1]); + EXPECT_FALSE(modes[2]); + EXPECT_EQ(DICE::DiceFormations(caps.deviceRateMask, modes).size(), 2U); +}