Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5a5d809
feat(dice): dynamic sample-rate selection (probe caps + CLOCK_SELECT …
alicankaralar Jun 27, 2026
5a8e2cf
fix(dice): drive advertised sample rates from profile in audio graph
alicankaralar Jun 27, 2026
9b345f9
fix(dice): skip single-format bring-up policy for profiled devices
alicankaralar Jun 27, 2026
388e747
fix(dice): commit sample-rate change via SetSampleRate, not super
alicankaralar Jun 27, 2026
bb183d5
fix(dice): start streams at the nominal rate; disable format restore
alicankaralar Jun 27, 2026
35ca754
fix(dice): make RequestSampleRateChange a cross-process RPC
alicankaralar Jun 27, 2026
32d0d80
fix(dice): apply selected rate on start; gate live rate change
alicankaralar Jun 27, 2026
d01615a
feat(amdtp): rate-parametric blocking cadence (rational accumulator)
alicankaralar Jun 27, 2026
0c3492d
feat(amdtp): drive TX cadence + FDF from the live sample rate
alicankaralar Jun 27, 2026
b5dce90
fix(isoch): reset TX control-block cursors on StartIO
alicankaralar Jun 27, 2026
dafa5af
fix(dice): sync endpoint clock to the new rate (stop 44.1k StartIO ch…
alicankaralar Jun 27, 2026
7b2f85f
fix(dice): refresh direct-binding rate + generation on rate change
alicankaralar Jun 27, 2026
134c703
fix(dice): keep CLOCK_SELECT at the selected rate across StartIO
alicankaralar Jun 27, 2026
b86d6d3
chore(audio): log secondary TX stream rate at configure
alicankaralar Jun 27, 2026
1fc963b
fix(dice): wait for stable PLL lock before enabling streams
alicankaralar Jun 28, 2026
dfabe0d
checkpoint
alicankaralar Jul 2, 2026
15f5ad4
fix(dice): clear every stream's ISOC register on stop (not just strea…
alicankaralar Jul 2, 2026
85c3bc3
test(dice): adjust prepare reference window for HW-validated bring-up…
alicankaralar Jul 6, 2026
2060a19
fix(audio): rate-correct ZTS projection, SYT seed, and ZTS-period assert
alicankaralar Jul 10, 2026
be5a96a
feat(isoch): size TX packet budgets for the worst-case 44.1k cadence
alicankaralar Jul 10, 2026
446bbd1
refactor(amdtp): unify blocking cadence on the rational deadline engine
alicankaralar Jul 10, 2026
242345e
fix(audio): validate sample-rate changes against advertised rates and…
alicankaralar Jul 12, 2026
c276546
Stability improvements
alicankaralar Jul 12, 2026
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
10 changes: 8 additions & 2 deletions ASFWDriver/ASFWDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,12 @@ kern_return_t ASFWDriver::StartRuntime(IOService* provider) {

kern_return_t IMPL(ASFWDriver, Stop) {
QuiesceRuntime();
if (ivars && ivars->context && ivars->context->deps.interrupts) {
// Real stop: cancel the interrupt dispatch source now, while the
// provider is still attached, so the kernel-side unregisterInterrupt
// is ordered before termination instead of racing it.
ivars->context->deps.interrupts->Teardown();
}
if (ivars) {
if (ivars->wakeVerifyTimer) {
// Disable only, then release — Cancel() dispatches async and can
Expand Down Expand Up @@ -475,7 +481,7 @@ kern_return_t IMPL(ASFWDriver, SetPowerState) {
if (!ivars->runtimeSuspended && ivars->context) {
ASFW_LOG(Controller, "SetPowerState: quiescing runtime for sleep");
QuiesceRuntime();
ivars->context->Reset();
ivars->context->Reset(ServiceContext::ResetMode::ForSuspend);
ivars->runtimeSuspended = true;
}
} else {
Expand Down Expand Up @@ -566,7 +572,7 @@ void ASFWDriver::VerifyWakeRuntime(uint64_t attempt) {
}

QuiesceRuntime();
ctx.Reset();
ctx.Reset(ServiceContext::ResetMode::ForSuspend);
const kern_return_t kr = StartRuntime(ivars->powerProvider);
if (kr != kIOReturnSuccess) {
ASFW_LOG(Controller, "Wake verify: ❌ rebuild failed: 0x%08x", kr);
Expand Down
11 changes: 11 additions & 0 deletions ASFWDriver/Audio/Core/AudioCoordinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ IOReturn AudioCoordinator::RequestClockConfig(

const auto* record = registry_.FindByGuid(guid);
if (!record) {
ASFW_LOG_WARNING(Audio,
"AudioCoordinator: RequestDiceClockConfig no registry record for GUID=0x%016llx (device not registered yet?)",
guid);
return kIOReturnNotReady;
}

Expand All @@ -321,6 +324,14 @@ IOReturn AudioCoordinator::RequestClockConfig(
return kr;
}

// Keep the endpoint's clock in step with the new device rate so the next
// StartIO seeds the direct-binding/ZTS clock at the live rate. Without this
// the binding stays at the publish-time rate (48 kHz) while the device runs
// 44.1 kHz, and CoreAudio churns StartIO/StopIO on the clock mismatch.
if (auto endpoint = runtime_.EnsureEndpointRuntime(guid)) {
endpoint->SetCurrentSampleRate(desiredClock.sampleRateHz);
}

ASFW_LOG(Audio,
"AudioCoordinator: RequestClockConfig ok GUID=0x%016llx rate=%uHz reason=%u",
guid,
Expand Down
33 changes: 33 additions & 0 deletions ASFWDriver/Audio/Core/AudioEndpointRuntime.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ class AudioEndpointRuntime final : public Runtime::IDirectAudioBindingSource {
configValid_.store(true, std::memory_order_release);
}

// Update only the current sample rate. The DICE clock can change (Audio MIDI
// Setup / Logic) without a full config rebuild; the next StartIO seeds the
// direct-binding/ZTS clock from the live rate, so this must reflect it or
// CoreAudio sees the device clock advancing at the wrong rate and churns
// StartIO/StopIO.
//
// The binding the IR actually reads reports directSampleRateHz_, not
// config_.currentSampleRate. That field (and directGeneration_) is latched
// only when the direct-audio memory is allocated in
// EnsureDirectAudioMemoryLocked, which does not re-run on an idle rate change
// because the ring buffers are rate-independent (fixed kAudioRingBufferFrames)
// and are allocated once at bring-up then reused. So updating config_ alone
// leaves the binding (and the ZTS the HAL judges) stuck at the publish-time
// 48 kHz. Refresh directSampleRateHz_ and bump the generation here so
// CopyDirectAudioBinding reports the new rate and the IR re-arms the RX/ZTS
// clock at it.
void SetCurrentSampleRate(uint32_t sampleRateHz) noexcept {
if (sampleRateHz == 0) {
return;
}
if (lock_) {
IOLockLock(lock_);
}
config_.currentSampleRate = sampleRateHz;
if (directSampleRateHz_ != 0 && directSampleRateHz_ != sampleRateHz) {
directSampleRateHz_ = sampleRateHz;
++directGeneration_;
}
if (lock_) {
IOLockUnlock(lock_);
}
}

[[nodiscard]] bool CopyConfig(Model::ASFWAudioDevice& outConfig) const noexcept {
if (!configValid_.load(std::memory_order_acquire)) {
return false;
Expand Down
101 changes: 99 additions & 2 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) {
kr = failStart(kIOReturnError, "BuildDefaultTxStreamConfig");
return;
}
// The profile describes the wire geometry at its default (48 kHz);
// the live cadence/FDF follow the device's current nominal rate.
if (ivars.device.currentSampleRate > 0) {
txConfig.sampleRate =
static_cast<uint32_t>(ivars.device.currentSampleRate);
}

const uint32_t numSlots =
ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets;
Expand Down Expand Up @@ -212,6 +218,12 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) {
auto* metadataRing = reinterpret_cast<ASFW::IsochTransport::TxPacketMeta*>(ivars.txMetadataMap->GetAddress());
auto* controlBlock = reinterpret_cast<ASFW::IsochTransport::TxStreamControl*>(ivars.txControlMap->GetAddress());

// Clear stale runtime cursors before prefill: the shared slab can be
// reused across StartIO/StopIO probes (CoreAudio re-probes on a
// sample-rate change), and a carried-over exposeCursor fails the IT
// prime ("committed prefill > slots").
controlBlock->ResetForStart();

ivars.runtime.txSlotProvider.payloadBase = payloadBase;
ivars.runtime.txSlotProvider.metadataRing = metadataRing;
ivars.runtime.txSlotProvider.controlBlock = controlBlock;
Expand Down Expand Up @@ -260,6 +272,10 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) {
kr = failStart(kIOReturnError, "BuildDefaultTxStreamConfig2");
return;
}
if (ivars.device.currentSampleRate > 0) {
txConfig2.sampleRate =
static_cast<uint32_t>(ivars.device.currentSampleRate);
}
txConfig2.sourceChannelOffset = txConfig2.pcmChannels;

const uint32_t numSlots2 =
Expand Down Expand Up @@ -294,6 +310,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) {
auto* metadataRing2 = reinterpret_cast<ASFW::IsochTransport::TxPacketMeta*>(ivars.txMetadataMapSecondary->GetAddress());
auto* controlBlock2 = reinterpret_cast<ASFW::IsochTransport::TxStreamControl*>(ivars.txControlMapSecondary->GetAddress());

controlBlock2->ResetForStart();

ivars.runtime.txSlotProviderSecondary.payloadBase = payloadBase2;
ivars.runtime.txSlotProviderSecondary.metadataRing = metadataRing2;
ivars.runtime.txSlotProviderSecondary.controlBlock = controlBlock2;
Expand All @@ -312,8 +330,9 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) {
ivars.runtime.txSecondaryActive = true;

ASFW_LOG(Audio,
"ASFWAudioDevice: Allocated & configured SECONDARY TX stream offset=%u dbs=%u slots=%u slotSize=%u",
txConfig2.sourceChannelOffset, txConfig2.dbs, numSlots2, maxPacketBytes2);
"ASFWAudioDevice: Allocated & configured SECONDARY TX stream offset=%u dbs=%u slots=%u slotSize=%u rate=%u",
txConfig2.sourceChannelOffset, txConfig2.dbs, numSlots2, maxPacketBytes2,
txConfig2.sampleRate);
}
}

Expand Down Expand Up @@ -593,3 +612,81 @@ kern_return_t ASFWAudioDevice::StopIO(IOUserAudioStartStopFlags in_flags) {

return kr;
}

kern_return_t ASFWAudioDevice::HandleChangeSampleRate(double in_sample_rate) {
ASFW_LOG(Audio, "ASFWAudioDevice: HandleChangeSampleRate %.0f Hz (entry)", in_sample_rate);
if (!ivars || !ivars->driverIvars) {
ASFW_LOG(Audio,
"ASFWAudioDevice: HandleChangeSampleRate NOT READY (ivars=%p driverIvars=%p)",
static_cast<void*>(ivars),
static_cast<void*>(ivars ? ivars->driverIvars : nullptr));
return kIOReturnNotReady;
}
auto& ivars = *this->ivars->driverIvars;

const uint32_t rateHz = static_cast<uint32_t>(in_sample_rate);

// Only accept rates this device advertised; anything else would program a
// clock the transport can't honor and desync host from device.
bool rateSupported = false;
for (uint32_t i = 0; i < ivars.device.sampleRateCount; ++i) {
if (static_cast<uint32_t>(ivars.device.sampleRates[i]) == rateHz) {
rateSupported = true;
break;
}
}
if (!rateSupported) {
ASFW_LOG(Audio,
"ASFWAudioDevice: HandleChangeSampleRate %.0f Hz refused - unsupported rate",
in_sample_rate);
return kIOReturnUnsupported;
}

// Reject a rate change while IO is active. Live (hot) reconfiguration would
// restart the duplex transport underneath running IO across the cross-service
// seam, which currently desynchronizes the device clock from the host and
// leaves audio dead until a full stop/replug. Until hot-swap is supported,
// require the device to be stopped: returning an error makes CoreAudio keep
// the current rate rather than believe the hardware moved. The rate then
// changes cleanly on the next idle pick + StartIO.
if (ivars.runtime.isRunning.load(std::memory_order_acquire)) {
ASFW_LOG(Audio,
"ASFWAudioDevice: HandleChangeSampleRate %.0f Hz refused - IO active "
"(stop playback to change sample rate)",
in_sample_rate);
return kIOReturnBusy;
}

// Program the device's DICE clock to the new rate via the transport-side
// coordinator (CLOCK_SELECT + duplex reconfigure). The device is idle here,
// so this stores/applies the clock for the next StartIO. Reject the change if
// the transport can't apply it so CoreAudio does not believe the hardware moved.
if (!ivars.device.audioNub) {
// Without the nub the device clock can't be programmed; succeeding here
// would make CoreAudio believe the hardware moved when it didn't.
ASFW_LOG(Audio,
"ASFWAudioDevice: HandleChangeSampleRate %.0f Hz refused - no audio nub",
in_sample_rate);
return kIOReturnNotReady;
}
const kern_return_t kr = ivars.device.audioNub->RequestSampleRateChange(rateHz);
if (kr != kIOReturnSuccess) {
ASFW_LOG(Audio,
"ASFWAudioDevice: HandleChangeSampleRate transport reconfig failed: 0x%x",
kr);
return kr;
}
ivars.device.currentSampleRate = static_cast<double>(rateHz);

// Commit the rate to the ADK device. The validated ADK contract
// (ADKVirtualAudioLab) applies the change by calling SetSampleRate here, not
// by delegating to super — the base HandleChangeSampleRate does not move the
// active format, so without this the HAL reverts to the previous rate.
const kern_return_t setKr = SetSampleRate(in_sample_rate);
if (setKr != kIOReturnSuccess) {
ASFW_LOG(Audio,
"ASFWAudioDevice: HandleChangeSampleRate SetSampleRate(%.0f) failed: 0x%x",
in_sample_rate, setKr);
}
return setKr;
}
4 changes: 4 additions & 0 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDevice.iig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public:
virtual kern_return_t StartIO(IOUserAudioStartStopFlags in_flags) override LOCALONLY;
virtual kern_return_t StopIO(IOUserAudioStartStopFlags in_flags) override LOCALONLY;

// CoreAudio sample-rate selection: program the device's DICE clock to the
// new rate (via the transport-side coordinator) before accepting it.
virtual kern_return_t HandleChangeSampleRate(double in_sample_rate) override LOCALONLY;

void SetDriverIvars(ASFWAudioDriver_IVars* ivars) LOCALONLY;
};

Expand Down
59 changes: 53 additions & 6 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDriverGraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
ASFW_LOG(Audio, "ASFWAudioDriver: Using default device configuration (no nub properties)");
}

// Set once a resolved profile supplies its advertised sample-rate set, so the
// bring-up single-format policy below is skipped for profiled devices.
bool profileProvidedSampleRates = false;

// Resolve audio profile registry on startup
if (const auto* profile = ASFW::Isoch::Audio::AudioProfileRegistry::FindProfile(
parsedConfig.vendorId, parsedConfig.modelId, parsedConfig.guid)) {
Expand All @@ -154,14 +158,43 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
rxChannels,
txChannels);

// Sample rates come from the profile (same authoritative source as the
// channel counts above), so CoreAudio advertises the full set even if the
// nub property dict did not carry kSampleRates. The HAL builds one stream
// format per rate (see SetAvailableSampleRates below).
const auto profileRates = profile->SupportedSampleRates();
if (!profileRates.empty()) {
parsedConfig.sampleRateCount = 0;
bool currentRateInSet = false;
for (uint32_t hz : profileRates) {
if (parsedConfig.sampleRateCount >= ASFW::Isoch::Audio::kMaxSampleRates) {
break;
}
parsedConfig.sampleRates[parsedConfig.sampleRateCount++] =
static_cast<double>(hz);
if (static_cast<double>(hz) == parsedConfig.currentSampleRate) {
currentRateInSet = true;
}
}
if (!currentRateInSet) {
parsedConfig.currentSampleRate = parsedConfig.sampleRates[0];
}
profileProvidedSampleRates = true;
}

// Regenerate channel names for the updated channel counts. Prefers the
// device's per-channel labels (published by the core side) and falls
// back to synthesized "<plug> N" for any slot without a real label.
ASFW::Isoch::Audio::BuildChannelNamesFromPlugs(parsedConfig);
}

ASFW::Isoch::Audio::BuildFallbackBoolControls(parsedConfig);
ASFW::Isoch::Audio::ApplyBringupSingleFormatPolicy(parsedConfig);
// Profiled devices advertise their own validated rate set (DICE: 44.1/48 kHz);
// only fall back to the single-format bring-up policy for unprofiled devices
// whose multi-rate path is not yet validated end-to-end.
if (!profileProvidedSampleRates) {
ASFW::Isoch::Audio::ApplyBringupSingleFormatPolicy(parsedConfig);
}
ASFW::Isoch::Audio::ClampAudioDriverChannels(parsedConfig, ASFW::Encoding::kMaxPcmChannels);
CopyParsedConfigToDeviceState(parsedConfig, ivars.device);

Expand Down Expand Up @@ -213,7 +246,8 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
? "blocking" : "non-blocking");

ASFW_LOG(Audio,
"ASFWAudioDriver: Forcing single advertised format: input=Float32 output=Float32");
"ASFWAudioDriver: Advertising %u Float32 format(s) (input/output) across rates",
ivars.device.sampleRateCount);
ASFW_LOG(Audio,
"ASFWAudioDriver: Effective runtime channels: input=%u output=%u aggregate=%u",
ivars.device.inputChannelCount,
Expand Down Expand Up @@ -259,6 +293,12 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
}
ivars.audioDevice->SetDriverIvars(&ivars);

// Do not let the host save/restore a stale stream format from a prior
// session: the device must come up at the rate this graph selects below,
// and we drive rate changes explicitly through HandleChangeSampleRate.
// (Default behavior is restore-enabled; see IOUserAudioDevice header.)
ivars.audioDevice->SetWantsStreamFormatsRestored(false);

const uint32_t current_period = ivars.audioDevice->GetZeroTimestampPeriod();
ASFW_LOG(Audio, "ASFWAudioDriver: IOUserAudioDevice created. GetZeroTimestampPeriod() confirmed: %u frames", current_period);
ASFW_LOG(Audio,
Expand Down Expand Up @@ -293,16 +333,23 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
IOUserAudioStreamBasicDescription inputFormats[8] = {};
IOUserAudioStreamBasicDescription outputFormats[8] = {};
const uint32_t formatCount = ivars.device.sampleRateCount > 8 ? 8 : ivars.device.sampleRateCount;
uint32_t currentFormatIndex = 0;
for (uint32_t i = 0; i < formatCount; i++) {
FillFloat32Format(inputFormats[i], ivars.device.sampleRates[i], ivars.device.inputChannelCount);
FillFloat32Format(outputFormats[i], ivars.device.sampleRates[i], ivars.device.outputChannelCount);
if (ivars.device.sampleRates[i] == ivars.device.currentSampleRate) {
currentFormatIndex = i;
}
}

ASFW_LOG(Audio,
"ASFWAudioDriver: Created %u stream formats input=float32/%u ch output=float32/%u ch",
"ASFWAudioDriver: Created %u stream formats input=float32/%u ch output=float32/%u ch; "
"current format index=%u (%.0f Hz)",
formatCount,
ivars.device.inputChannelCount,
ivars.device.outputChannelCount);
ivars.device.outputChannelCount,
currentFormatIndex,
ivars.device.sampleRates[currentFormatIndex]);

IOMemoryDescriptor* rawOutputMemory = nullptr;
IOMemoryDescriptor* rawInputMemory = nullptr;
Expand Down Expand Up @@ -444,7 +491,7 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
if (!requireAdkSuccess(
"inputStream.SetCurrentStreamFormat",
ivars.inputStream->SetCurrentStreamFormat(
&inputFormats[0]))) {
&inputFormats[currentFormatIndex]))) {
return error;
}

Expand Down Expand Up @@ -479,7 +526,7 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
if (!requireAdkSuccess(
"outputStream.SetCurrentStreamFormat",
ivars.outputStream->SetCurrentStreamFormat(
&outputFormats[0]))) {
&outputFormats[currentFormatIndex]))) {
return error;
}

Expand Down
19 changes: 11 additions & 8 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,16 +343,19 @@ uint32_t PrepareTransmitSlots(ASFWAudioDriver_IVars& ivars,
outputPresentationTicks,
sourcePresentationTicks);
if (presentationDeltaTicks >= 0) {
constexpr uint32_t kFramesPerPacket =
ASFW::IsochTransport::
AudioTimingGeometry::
kFramesPerDataPacket;
// ticks -> frames at the live rate. 44.1k has no integer
// ticks/sample (24576000/44100 ~= 557.28), so divide the
// tick*rate product instead of dividing by a per-sample
// constant (the old /512 overshot ~8.8% at 44.1k).
const auto& txConfig =
ivars.runtime.txStreamEngine.StreamConfig();
const uint32_t kFramesPerPacket =
txConfig.framesPerDataPacket;
const uint64_t projectedFrame =
replay.firstAudioFrame +
static_cast<uint64_t>(
presentationDeltaTicks /
ASFW::Timing::
kTicksPerSample48k);
(static_cast<uint64_t>(presentationDeltaTicks) *
txConfig.sampleRate) /
ASFW::Timing::kTicksPerSecond;
const uint64_t alignedFrame =
(projectedFrame / kFramesPerPacket) *
kFramesPerPacket;
Expand Down
Loading
Loading