Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,29 @@ ResolveRate(ASFWAudioDriver_IVars& driver, uint32_t rate, uint64_t revision,
.resolved = std::make_shared<const ResolvedAudioConfiguration>(*resolved)};
}

// Take the nub's catalog after it measured `rateHz` from the device's
// registers (CopyObservedRateCatalog). The device must be locked at the rate.
[[nodiscard]] kern_return_t AdoptObservedRateCatalog(ASFWAudioDriver_IVars& driver, uint32_t rateHz) {
OSDictionary* rawProperties = nullptr;
const auto status = driver.device.audioNub->CopyObservedRateCatalog(rateHz, &rawProperties);
const auto properties = OSSharedPtr(rawProperties, OSNoRetain);
if (status != kIOReturnSuccess) return status;
ASFW::Isoch::Audio::ParsedAudioDriverConfig catalog{};
ASFW::Isoch::Audio::InitializeAudioDriverConfigDefaults(catalog);
ASFW::Isoch::Audio::ParseAudioDriverConfigFromProperties(properties.get(), catalog);
if (catalog.guid != driver.device.guid ||
catalog.rateRouteIncarnation != driver.device.rateRouteIncarnation ||
std::ranges::find(catalog.rateFormationCandidates, rateHz,
&ASFW::Audio::Runtime::RateFormation::sampleRateHz) == catalog.rateFormationCandidates.end())
return kIOReturnAborted;
driver.device.rateFormationCandidates = std::move(catalog.rateFormationCandidates);
driver.device.sampleRateCount = catalog.sampleRateCount;
std::ranges::copy(std::span{catalog.sampleRates, catalog.sampleRateCount}, driver.device.sampleRates);
driver.device.rateRouteEpoch = catalog.rateRouteEpoch;
driver.device.rateBusGeneration = catalog.rateBusGeneration;
return kIOReturnSuccess;
}

void InstallDriverFormation(ASFWAudioDriver_IVars& driver,
const ASFW::Audio::Runtime::ResolvedAudioConfiguration& resolved) {
auto& state = driver.device;
Expand Down Expand Up @@ -1493,24 +1516,8 @@ kern_return_t ASFWAudioDevice::RequestExternalRateResync(uint32_t nominalRateHz)
!std::holds_alternative<ASFW::Configuration::Uninitialized>(ivars->rateMachine.state);
IOLockUnlock(ivars->rateLock);
if (pending) return kIOReturnBusy;
OSDictionary* rawProperties = nullptr;
const auto status = driverIvars.device.audioNub->CopyObservedRateCatalog(nominalRateHz, &rawProperties);
const auto properties = OSSharedPtr(rawProperties, OSNoRetain);
const auto status = AdoptObservedRateCatalog(driverIvars, nominalRateHz);
if (status != kIOReturnSuccess) return status;
ASFW::Isoch::Audio::ParsedAudioDriverConfig catalog{};
ASFW::Isoch::Audio::InitializeAudioDriverConfigDefaults(catalog);
ASFW::Isoch::Audio::ParseAudioDriverConfigFromProperties(properties.get(), catalog);
if (catalog.guid != driverIvars.device.guid ||
catalog.rateRouteIncarnation != driverIvars.device.rateRouteIncarnation ||
std::ranges::find(catalog.rateFormationCandidates, nominalRateHz,
&ASFW::Audio::Runtime::RateFormation::sampleRateHz) == catalog.rateFormationCandidates.end())
return kIOReturnAborted;
driverIvars.device.rateFormationCandidates = std::move(catalog.rateFormationCandidates);
driverIvars.device.sampleRateCount = catalog.sampleRateCount;
std::ranges::copy(std::span{catalog.sampleRates, catalog.sampleRateCount},
driverIvars.device.sampleRates);
driverIvars.device.rateRouteEpoch = catalog.rateRouteEpoch;
driverIvars.device.rateBusGeneration = catalog.rateBusGeneration;
}
if (!driverIvars.device.rateFormationCandidates.empty())
return StageSampleRate(nominalRateHz, true);
Expand Down Expand Up @@ -1652,12 +1659,33 @@ kern_return_t ASFWAudioDevice::PerformDeviceConfigurationChange(
return status == kIOReturnSuccess ? kIOReturnNotReady : status;
}
auto commitConfiguration = transaction.candidate;
const bool requestedConfirmed = readback == kIOReturnSuccess && confirmed(transaction.candidate.sampleRate);
bool requestedConfirmed = readback == kIOReturnSuccess && confirmed(transaction.candidate.sampleRate);
bool otherConfirmed = false;
if (!requestedConfirmed && readback == kIOReturnSuccess && input && (output == input || output == 0)) {
const auto actual = ResolveRate(driver, input, transaction.prior.revision + 1);
if (actual) { commitConfiguration = *actual; otherConfirmed = true; }
}
// A provisional rate's layout is read now, with the clock locked at it,
// as TCAT's RestartStreaming reads TX/RX after CLOCK_ACCEPTED. The
// measured formation is never the staged placeholder, so the device
// confirmed another configuration than the candidate.
if ((requestedConfirmed || otherConfirmed) &&
commitConfiguration.resolved->formation.layoutProvisional) {
const uint32_t rate = commitConfiguration.sampleRate;
const auto adopted = AdoptObservedRateCatalog(driver, rate);
std::expected<DeviceConfiguration, kern_return_t> measured = std::unexpected(adopted);
if (adopted == kIOReturnSuccess) measured = ResolveRate(driver, rate, transaction.prior.revision + 1);
const bool learned = measured && !measured->resolved->formation.layoutProvisional;
if (learned) commitConfiguration = *measured;
else status = measured ? kIOReturnNotReady : measured.error();
otherConfirmed = learned;
requestedConfirmed = false;
ASFW_LOG(Audio, "[RateTxn] phase=learn token=%llu gen=%u rate=%u result=0x%x (%{public}s) in=%u out=%u",
transaction.identity.token, generation, rate, learned ? kIOReturnSuccess : status,
ASFW::Logging::IOReturnName(learned ? kIOReturnSuccess : status),
learned ? commitConfiguration.resolved->captureChannels : 0U,
learned ? commitConfiguration.resolved->playbackChannels : 0U);
}
ASFW_LOG(Audio, "[RateTxn] phase=confirm kind=%{public}s token=%llu gen=%u requested=%u out=%u in=%u kr=0x%x (%{public}s)",
requestedConfirmed ? "requested" : otherConfirmed ? "other" : "unknown",
transaction.identity.token, generation, transaction.candidate.sampleRate, output, input, readback, ASFW::Logging::IOReturnName(readback));
Expand Down
7 changes: 4 additions & 3 deletions ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1015,9 +1015,7 @@ kern_return_t IMPL(ASFWAudioNub, ReadRateClockState) {
[owner](auto done) { owner->ReadRateObservation(std::move(done)); }, 5000, kIOReturnTimeout);
if (result.status != kIOReturnSuccess) return result.status;
if (!binding.registry->IsCurrent(*route)) return kIOReturnAborted;
const auto actual = ASFW::Audio::Model::WithRateFormation(config, result.value.caps.sampleRateHz);
const bool known = result.value.clockConfirmed && actual &&
ASFW::Audio::RuntimeCapsMatchConfiguration(*actual, result.value.caps);
const bool known = ASFW::Audio::ObservationSettlesRate(config, result.value);
*outIncarnation = route->deviceIncarnation;
*outRouteEpoch = route->routeEpoch;
*outBusGeneration = route->generation.value;
Expand Down Expand Up @@ -1144,6 +1142,9 @@ kern_return_t IMPL(ASFWAudioNub, InstallRateFormation) {
if (!binding.registry->IsCurrent(route)) return kIOReturnAborted;
ASFW::Audio::Model::ASFWAudioDevice prior{};
if (!endpoint->CopyConfig(prior)) return kIOReturnNotReady;
// Only a layout read from the device is installed; the window replaces a
// placeholder through CopyObservedRateCatalog first.
if (ASFW::Audio::Model::LayoutIsProvisional(prior, sampleRateHz)) return kIOReturnNotReady;
const auto next = ASFW::Audio::Model::WithRateFormation(prior, sampleRateHz);
if (!next) return kIOReturnUnsupported;
if (prior.usesRateFormations && (!ivars->rateHardwareObservation ||
Expand Down
2 changes: 2 additions & 0 deletions ASFWDriver/Audio/DriverKit/Config/AudioDriverConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ void ParseAudioDriverConfigFromProperties(OSDictionary* properties,
? ::ASFW::Encoding::StreamMode::kBlocking : ::ASFW::Encoding::StreamMode::kNonBlocking;
formation.protocolSupported = protocol->unsigned32BitValue() != 0;
formation.hardwareValidated = validated->unsigned32BitValue() != 0;
if (auto* provisional = OSDynamicCast(OSNumber, entry->getObject(Keys::kFormationLayoutProvisional)))
formation.layoutProvisional = provisional->unsigned32BitValue() != 0;
if (auto* packed = OSDynamicCast(OSNumber, entry->getObject("ASFWMotuPackedPcm")))
formation.packedPcm = packed->unsigned32BitValue() != 0;
if (auto* value = OSDynamicCast(OSNumber, entry->getObject("ASFWMotuCaptureMessageChunks")))
Expand Down
1 change: 1 addition & 0 deletions ASFWDriver/Audio/Model/ASFWAudioDevice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ struct ASFWAudioDevice {
formation.mode == Encoding::StreamMode::kBlocking ? 1U : 0U) ||
!add(PropertyKeys::kFormationProtocolSupported, formation.protocolSupported) ||
!add(PropertyKeys::kFormationHardwareValidated, formation.hardwareValidated) ||
!add(PropertyKeys::kFormationLayoutProvisional, formation.layoutProvisional) ||
!add("ASFWMotuPackedPcm", formation.packedPcm) ||
!add("ASFWMotuCaptureMessageChunks", formation.packedCaptureMessageChunks) ||
!add("ASFWMotuPlaybackMessageChunks", formation.packedPlaybackMessageChunks) ||
Expand Down
2 changes: 2 additions & 0 deletions ASFWDriver/Audio/Model/AudioPropertyKeys.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ inline constexpr const char* kRateFormations = "ASFWRateFormations";
inline constexpr const char* kUsesRateFormations = "ASFWDiceRateFormations";
inline constexpr const char* kFormationProtocolSupported = "ProtocolSupported";
inline constexpr const char* kFormationHardwareValidated = "HardwareValidated";
// Optional: absent means the formation's layout was read from the device.
inline constexpr const char* kFormationLayoutProvisional = "LayoutProvisional";

// Keys within one stream entry.
inline constexpr const char* kStreamPcmChannels = "PCM";
Expand Down
14 changes: 14 additions & 0 deletions ASFWDriver/Audio/Model/DiscoveredRuntimeCaps.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "ASFWAudioDevice.hpp"
#include "RateConfiguration.hpp"
#include "../Protocols/AudioTypes.hpp"
#include <optional>
#include <algorithm>
Expand Down Expand Up @@ -32,6 +33,19 @@ namespace ASFW::Audio {
matches(config.captureStreams, caps.deviceToHostStreamCount, caps.deviceToHostStreams);
}

// Whether a hardware observation settles its rate for this endpoint: the clock
// is confirmed at a rate the catalog offers, and the registers match that
// rate's formation. A provisional formation has no layout to match yet; the
// registers read now are its layout (WithObservedRateCatalog).
[[nodiscard]] inline bool ObservationSettlesRate(const Model::ASFWAudioDevice& config,
const RateHardwareObservation& observed) noexcept {
if (!observed.clockConfirmed) return false;
const uint32_t rate = observed.caps.sampleRateHz;
if (Model::LayoutIsProvisional(config, rate)) return true;
const auto actual = Model::WithRateFormation(config, rate);
return actual && RuntimeCapsMatchConfiguration(*actual, observed.caps);
}

[[nodiscard]] inline std::optional<AudioStreamRuntimeCaps> DiscoveredCaps(
const Model::ASFWAudioDevice& config) noexcept {
if (config.playbackStreams.size() != 1 || config.captureStreams.size() != 1 ||
Expand Down
13 changes: 12 additions & 1 deletion ASFWDriver/Audio/Model/RateConfiguration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

namespace ASFW::Audio::Model {

// The device supports this rate, but its layout is still a placeholder.
[[nodiscard]] inline bool LayoutIsProvisional(const ASFWAudioDevice& config, uint32_t rateHz) noexcept {
const auto formation = std::ranges::find(config.rateFormationCandidates, rateHz,
&Runtime::RateFormation::sampleRateHz);
return formation != config.rateFormationCandidates.end() && formation->layoutProvisional;
}

// Resolve from retained descriptor formations, never by scaling channel counts.
// This is a value copy; callers install it only with both directions quiesced.
[[nodiscard]] inline std::expected<ASFWAudioDevice, Runtime::ConfigurationError>
Expand Down Expand Up @@ -44,15 +51,19 @@ WithRateFormation(const ASFWAudioDevice& prior, uint32_t rateHz) {

// Add one measured formation without changing the committed live format.
// A known formation must agree: stale registers must not overwrite an EAP
// catalog. The caller supplies a stable, route-checked hardware observation.
// catalog. A provisional one is a placeholder and takes the measured layout.
// The caller supplies a stable, route-checked hardware observation.
[[nodiscard]] inline std::expected<ASFWAudioDevice, Runtime::ConfigurationError>
WithObservedRateCatalog(const ASFWAudioDevice& prior, const Runtime::RateFormation& observed) {
if (observed.layoutProvisional) return std::unexpected(Runtime::ConfigurationError::InvalidFormation);
auto next = prior;
const auto existing = std::ranges::find(next.rateFormationCandidates,
observed.sampleRateHz, &Runtime::RateFormation::sampleRateHz);
if (existing == next.rateFormationCandidates.end()) {
next.rateFormationCandidates.push_back(observed);
std::ranges::sort(next.rateFormationCandidates, {}, &Runtime::RateFormation::sampleRateHz);
} else if (existing->layoutProvisional) {
*existing = observed;
}
const auto selected = WithRateFormation(next, observed.sampleRateHz);
if (!selected) return std::unexpected(selected.error());
Expand Down
32 changes: 31 additions & 1 deletion ASFWDriver/Audio/Protocols/DICE/Core/DiceRateFormats.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "../../../Runtime/ResolvedAudioConfiguration.hpp"
#include "../../../../Common/WireFormat.hpp"

#include <algorithm>
#include <array>
#include <optional>
#include <span>
Expand Down Expand Up @@ -81,7 +82,8 @@ DiceObservedFormat(const AudioStreamRuntimeCaps& caps) {
/// 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.
/// known here; the other rates are learned when the host selects them
/// (WithProvisionalRates). No probing at attach, no guessed scaling.
[[nodiscard]] inline DiceRateFormats SelectPublishedFormats(bool eapRead, DiceRateFormats eap,
const AudioStreamRuntimeCaps& caps) {
if (eapRead) return eap;
Expand Down Expand Up @@ -127,6 +129,34 @@ DiceFormations(uint32_t mask, const DiceRateFormats& formats, bool exposeCapture
return result;
}

/// Without EAP the registers describe only the current rate's mode, yet the
/// device runs every rate in CLOCK_CAPS. TCAT's kexts offer all of them and
/// read the layout after switching: Saffire.kext 4.3.0 performFormatChange
/// (0x5028) checks the rate against CLOCK_CAPS and requests a restart;
/// RestartStreaming (0xdb02) writes CLOCK_SELECT, waits for CLOCK_ACCEPTED,
/// reads TX/RX (PopulateDeviceStruct) and rebuilds the streams (CreateStreams).
/// Each such rate here carries the current rate's layout as a placeholder for
/// the HAL format list, marked layoutProvisional; the rate transaction reads
/// the real layout before anything is installed.
[[nodiscard]] inline std::vector<Runtime::RateFormation>
WithProvisionalRates(uint32_t mask, std::vector<Runtime::RateFormation> measured,
uint32_t currentRateHz) {
const auto current = std::ranges::find(measured, currentRateHz, &Runtime::RateFormation::sampleRateHz);
if (current == measured.end()) return measured;
const auto placeholder = *current;
for (const auto& rate : kDiceRateTable) {
if (!(mask & rate.capsBit) || std::ranges::find(measured, rate.hz,
&Runtime::RateFormation::sampleRateHz) != measured.end()) continue;
auto formation = placeholder;
formation.sampleRateHz = rate.hz;
formation.hardwareValidated = false;
formation.layoutProvisional = true;
measured.push_back(std::move(formation));
}
std::ranges::sort(measured, {}, &Runtime::RateFormation::sampleRateHz);
return measured;
}

// One observed rate is evidence for that rate only, even when the capability
// register describes just the current mode (#190).
[[nodiscard]] inline std::optional<Runtime::RateFormation>
Expand Down
Loading