From 5681dcdbe7254888b26bc44269f7820b8f6e88f7 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 5 Sep 2026 06:32:14 +1000 Subject: [PATCH] [idd] audio: discipline playback clock from client Forward coalesced feedback through the broker to the render clock. Keep capture independent and apply the rate to WaveRT timing. Bound and slew targets, then restore unity when feedback stalls. Withdraw feedback after the watchdog makes fallback safe. --- idd/LGAudio/Adapter.cpp | 61 +++++- idd/LGAudio/Adapter.h | 10 +- idd/LGAudio/AudioClock.cpp | 182 +++++++++++++++++- idd/LGAudio/AudioClock.h | 41 +++- idd/LGAudio/BrokerDevice.cpp | 49 ++++- idd/LGAudio/MiniportWaveRTStream.cpp | 173 ++++++++++++++--- idd/LGAudio/MiniportWaveRTStream.h | 1 + idd/LGCommon/LGAudioBroker.h | 19 ++ idd/LGIdd/audio/CLGAudioDevice.cpp | 176 ++++++++++++++++- .../transport/lgmp/CLGMPAudioTransport.cpp | 27 ++- 10 files changed, 679 insertions(+), 60 deletions(-) diff --git a/idd/LGAudio/Adapter.cpp b/idd/LGAudio/Adapter.cpp index 28946e70..7a95ef63 100644 --- a/idd/LGAudio/Adapter.cpp +++ b/idd/LGAudio/Adapter.cpp @@ -132,7 +132,9 @@ CAudioAdapter::~CAudioAdapter() NTSTATUS CAudioAdapter::Initialize() { - NTSTATUS status = m_clock.Initialize(); + NTSTATUS status = m_renderClock.Initialize(); + if (NT_SUCCESS(status)) + status = m_captureClock.Initialize(); if (NT_SUCCESS(status)) status = m_renderRing.Init(); if (NT_SUCCESS(status)) @@ -149,6 +151,8 @@ NTSTATUS CAudioAdapter::ConfigureStream( m_renderRing : m_captureRing; KIRQL transportIrql; KeAcquireSpinLock(&m_transportLock, &transportIrql); + if (kind == EndpointKind::Render) + m_renderClock.ResetRate(); const NTSTATUS status = ring.Configure(format); if (!NT_SUCCESS(status)) { @@ -194,7 +198,10 @@ void CAudioAdapter::SetStreamState( if (reset) { if (kind == EndpointKind::Render) + { + m_renderClock.ResetRate(); m_renderRing.Reset(); + } else m_captureRing.Reset(); } @@ -232,6 +239,7 @@ void CAudioAdapter::SetBrokerActive(_In_ bool active) m_captureRing.Reset(); m_renderRing.Reset(); + m_renderClock.ResetRate(); InterlockedExchange(&m_renderDiscontinuity, active ? 1 : 0); KeReleaseSpinLock(&m_transportLock, oldIrql); } @@ -396,9 +404,56 @@ NTSTATUS CAudioAdapter::WriteCaptureFrames( } _IRQL_requires_max_(DISPATCH_LEVEL) -AudioClockSnapshot CAudioAdapter::GetClockSnapshot() const +NTSTATUS CAudioAdapter::SetRenderClockRate( + _In_ ULONGLONG streamGeneration, + _In_ ULONGLONG targetRateQ32) { - return m_clock.GetSnapshot(); + NTSTATUS status = STATUS_SUCCESS; + KIRQL transportIrql; + KeAcquireSpinLock(&m_transportLock, &transportIrql); + + KIRQL stateIrql; + KeAcquireSpinLock(&m_streamStateLock, &stateIrql); + const AudioStreamSnapshot render = m_renderState; + const bool active = + InterlockedCompareExchange(&m_brokerActive, 0, 0) != 0; + KeReleaseSpinLock(&m_streamStateLock, stateIrql); + + if (!active || render.state != KSSTATE_RUN || !render.formatValid) + status = STATUS_DEVICE_NOT_READY; + else if (render.generation != streamGeneration) + status = STATUS_RETRY; + else + { + const ULONGLONG nominalRateQ32 = + static_cast(render.format.sampleRate) << 32; + const ULONGLONG maximumDelta = nominalRateQ32 * + kAudioClockMaximumCorrectionPpm / 1000000; + if (!render.format.sampleRate || + targetRateQ32 < nominalRateQ32 - maximumDelta || + targetRateQ32 > nominalRateQ32 + maximumDelta) + { + status = STATUS_INVALID_PARAMETER; + } + else + { + const ULONGLONG rateQ32 = + (targetRateQ32 + render.format.sampleRate / 2) / + render.format.sampleRate; + if (!m_renderClock.SetRate(rateQ32)) + status = STATUS_INVALID_PARAMETER; + } + } + + KeReleaseSpinLock(&m_transportLock, transportIrql); + return status; +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +AudioClockSnapshot CAudioAdapter::GetClockSnapshot(_In_ EndpointKind kind) +{ + return kind == EndpointKind::Render ? + m_renderClock.GetSnapshot() : m_captureClock.GetSnapshot(); } NTSTATUS CAudioAdapter::QueryInterface( diff --git a/idd/LGAudio/Adapter.h b/idd/LGAudio/Adapter.h index 276b825a..311c514f 100644 --- a/idd/LGAudio/Adapter.h +++ b/idd/LGAudio/Adapter.h @@ -100,7 +100,12 @@ public: _Out_ PULONG droppedBytes); _IRQL_requires_max_(DISPATCH_LEVEL) - AudioClockSnapshot GetClockSnapshot() const; + NTSTATUS SetRenderClockRate( + _In_ ULONGLONG streamGeneration, + _In_ ULONGLONG targetRateQ32); + + _IRQL_requires_max_(DISPATCH_LEVEL) + AudioClockSnapshot GetClockSnapshot(_In_ EndpointKind kind); STDMETHODIMP_(NTSTATUS) QueryInterface( _In_ REFIID interfaceId, @@ -116,7 +121,8 @@ private: KSPIN_LOCK m_streamStateLock; AudioStreamSnapshot m_renderState; AudioStreamSnapshot m_captureState; - CAudioClock m_clock; + CAudioClock m_renderClock; + CAudioClock m_captureClock; CAudioRingBuffer m_renderRing; CAudioRingBuffer m_captureRing; }; diff --git a/idd/LGAudio/AudioClock.cpp b/idd/LGAudio/AudioClock.cpp index 99aaf20d..0816c0c6 100644 --- a/idd/LGAudio/AudioClock.cpp +++ b/idd/LGAudio/AudioClock.cpp @@ -20,11 +20,60 @@ #include "AudioClock.h" +#include + +namespace +{ + // Revert before the client activates local rate control after 100 ms. + constexpr ULONGLONG kFeedbackTimeoutNumerator = 3; + constexpr ULONGLONG kFeedbackTimeoutDivisor = 40; + constexpr ULONGLONG kFractionMask = (1ULL << 32) - 1; + + bool Divide128( + ULONGLONG high, + ULONGLONG low, + ULONGLONG divisor, + ULONGLONG *quotient) + { + if (!divisor || !quotient || high >= divisor) + return false; + if (!high) + { + *quotient = low / divisor; + return true; + } + + ULONGLONG result = 0; + ULONGLONG remainder = high; + for (ULONG bit = 64; bit; --bit) + { + const ULONG index = bit - 1; + const bool carry = (remainder & (1ULL << 63)) != 0; + remainder = (remainder << 1) | ((low >> index) & 1); + if (carry || remainder >= divisor) + { + remainder -= divisor; + result |= 1ULL << index; + } + } + + *quotient = result; + return true; + } +} + namespace LGAudio { CAudioClock::CAudioClock() : - m_frequency(0) + m_frequency(0), + m_position(0), + m_positionFractionQ32(0), + m_lastQpcPosition(0), + m_rateQ32(kAudioClockRateOneQ32), + m_lastRateUpdateQpc(0), + m_feedbackDeadlineQpc(0) { + KeInitializeSpinLock(&m_lock); } _IRQL_requires_max_(PASSIVE_LEVEL) @@ -35,23 +84,140 @@ namespace LGAudio if (frequency.QuadPart <= 0) return STATUS_DEVICE_HARDWARE_ERROR; + const LARGE_INTEGER qpc = KeQueryPerformanceCounter(nullptr); m_frequency = static_cast(frequency.QuadPart); + m_position = static_cast(qpc.QuadPart); + m_lastQpcPosition = m_position; + m_lastRateUpdateQpc = m_position; return STATUS_SUCCESS; } + _Requires_lock_held_(m_lock) + void CAudioClock::AdvanceLocked(_In_ ULONGLONG qpcPosition) + { + if (qpcPosition <= m_lastQpcPosition) + return; + + const ULONGLONG elapsed = qpcPosition - m_lastQpcPosition; + ULONGLONG productHigh = 0; + ULONGLONG productLow = _umul128(elapsed, m_rateQ32, &productHigh); + const ULONGLONG oldLow = productLow; + productLow += m_positionFractionQ32; + if (productLow < oldLow) + ++productHigh; + + const ULONGLONG positionDelta = + (productHigh << 32) | (productLow >> 32); + m_position += positionDelta; + m_positionFractionQ32 = productLow & kFractionMask; + m_lastQpcPosition = qpcPosition; + } + + _Requires_lock_held_(m_lock) + void CAudioClock::UpdateLocked(_In_ ULONGLONG qpcPosition) + { + if (m_feedbackDeadlineQpc && + m_feedbackDeadlineQpc > m_lastQpcPosition && + m_feedbackDeadlineQpc <= qpcPosition) + { + AdvanceLocked(m_feedbackDeadlineQpc); + m_rateQ32 = kAudioClockRateOneQ32; + m_lastRateUpdateQpc = m_feedbackDeadlineQpc; + m_feedbackDeadlineQpc = 0; + } + else if (m_feedbackDeadlineQpc && + m_feedbackDeadlineQpc <= m_lastQpcPosition) + { + m_rateQ32 = kAudioClockRateOneQ32; + m_lastRateUpdateQpc = m_lastQpcPosition; + m_feedbackDeadlineQpc = 0; + } + + AdvanceLocked(qpcPosition); + } + _IRQL_requires_max_(DISPATCH_LEVEL) - AudioClockSnapshot CAudioClock::GetSnapshot() const + AudioClockSnapshot CAudioClock::GetSnapshot() { const LARGE_INTEGER qpc = KeQueryPerformanceCounter(nullptr); - const ULONGLONG position = static_cast(qpc.QuadPart); + const ULONGLONG qpcPosition = static_cast(qpc.QuadPart); - // The fallback domains are identical, making the correlation exact. - return + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + UpdateLocked(qpcPosition); + const AudioClockSnapshot result = { - position, + m_position, m_frequency, - position, - m_frequency + qpcPosition, + m_frequency, + m_rateQ32 }; + KeReleaseSpinLock(&m_lock, oldIrql); + return result; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + bool CAudioClock::SetRate(_In_ ULONGLONG rateQ32) + { + const ULONGLONG maximumDelta = + kAudioClockRateOneQ32 * kAudioClockMaximumCorrectionPpm / 1000000; + if (rateQ32 < kAudioClockRateOneQ32 - maximumDelta || + rateQ32 > kAudioClockRateOneQ32 + maximumDelta) + return false; + + const LARGE_INTEGER qpc = KeQueryPerformanceCounter(nullptr); + const ULONGLONG qpcPosition = static_cast(qpc.QuadPart); + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + UpdateLocked(qpcPosition); + if (!m_feedbackDeadlineQpc) + m_lastRateUpdateQpc = qpcPosition; + + const ULONGLONG elapsed = qpcPosition > m_lastRateUpdateQpc ? + qpcPosition - m_lastRateUpdateQpc : 0; + const ULONGLONG slewQ32PerSecond = + kAudioClockRateOneQ32 * kAudioClockMaximumSlewPpmPerSecond / + 1000000; + ULONGLONG productHigh = 0; + const ULONGLONG productLow = + _umul128(elapsed, slewQ32PerSecond, &productHigh); + ULONGLONG maximumStep = MAXULONGLONG; + if (m_frequency && productHigh < m_frequency) + Divide128(productHigh, productLow, m_frequency, &maximumStep); + + const ULONGLONG minimumRate = m_rateQ32 > maximumStep ? + m_rateQ32 - maximumStep : 0; + const ULONGLONG maximumRate = + m_rateQ32 <= MAXULONGLONG - maximumStep ? + m_rateQ32 + maximumStep : MAXULONGLONG; + if (rateQ32 < minimumRate) + m_rateQ32 = minimumRate; + else if (rateQ32 > maximumRate) + m_rateQ32 = maximumRate; + else + m_rateQ32 = rateQ32; + m_lastRateUpdateQpc = qpcPosition; + const ULONGLONG timeoutTicks = + m_frequency * kFeedbackTimeoutNumerator / + kFeedbackTimeoutDivisor; + m_feedbackDeadlineQpc = qpcPosition <= MAXULONGLONG - timeoutTicks ? + qpcPosition + timeoutTicks : MAXULONGLONG; + KeReleaseSpinLock(&m_lock, oldIrql); + return true; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + void CAudioClock::ResetRate() + { + const LARGE_INTEGER qpc = KeQueryPerformanceCounter(nullptr); + const ULONGLONG qpcPosition = static_cast(qpc.QuadPart); + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + UpdateLocked(qpcPosition); + m_rateQ32 = kAudioClockRateOneQ32; + m_lastRateUpdateQpc = qpcPosition; + m_feedbackDeadlineQpc = 0; + KeReleaseSpinLock(&m_lock, oldIrql); } } diff --git a/idd/LGAudio/AudioClock.h b/idd/LGAudio/AudioClock.h index 4c0c65ae..e4876d66 100644 --- a/idd/LGAudio/AudioClock.h +++ b/idd/LGAudio/AudioClock.h @@ -35,17 +35,18 @@ namespace LGAudio ULONGLONG Frequency; ULONGLONG QpcPosition; ULONGLONG QpcFrequency; + // Audio-position ticks advanced per QPC tick in unsigned Q32.32. + ULONGLONG RateQ32; }; + constexpr ULONGLONG kAudioClockRateOneQ32 = 1ULL << 32; + constexpr ULONG kAudioClockMaximumCorrectionPpm = 5000; + constexpr ULONG kAudioClockMaximumSlewPpmPerSecond = 5000; + /** - * Adapter-owned clock shared by all render and capture streams. - * - * The local fallback uses QPC for both domains. A transport-disciplined - * implementation can later synthesize a continuous, monotonic Position - * while preserving its correlation with the local QPC fields. Before a - * non-unity clock rate is enabled, the stream deadline and timestamp - * conversions must consume an explicit fixed-point affine mapping between - * the two domains. + * Adapter-owned affine clock. Position is continuous and monotonic while + * RateQ32 may be updated to discipline a render endpoint to a remote sink. + * QpcPosition always remains in the local Windows QPC domain. */ class CAudioClock final { @@ -59,12 +60,34 @@ namespace LGAudio NTSTATUS Initialize(); _IRQL_requires_max_(DISPATCH_LEVEL) - AudioClockSnapshot GetSnapshot() const; + AudioClockSnapshot GetSnapshot(); + + _IRQL_requires_max_(DISPATCH_LEVEL) + bool SetRate(_In_ ULONGLONG rateQ32); + + _IRQL_requires_max_(DISPATCH_LEVEL) + void ResetRate(); private: + _Requires_lock_held_(m_lock) + void AdvanceLocked(_In_ ULONGLONG qpcPosition); + + _Requires_lock_held_(m_lock) + void UpdateLocked(_In_ ULONGLONG qpcPosition); + + KSPIN_LOCK m_lock; ULONGLONG m_frequency; + ULONGLONG m_position; + ULONGLONG m_positionFractionQ32; + ULONGLONG m_lastQpcPosition; + ULONGLONG m_rateQ32; + ULONGLONG m_lastRateUpdateQpc; + ULONGLONG m_feedbackDeadlineQpc; }; } using LGAudio::AudioClockSnapshot; using LGAudio::CAudioClock; +using LGAudio::kAudioClockMaximumCorrectionPpm; +using LGAudio::kAudioClockMaximumSlewPpmPerSecond; +using LGAudio::kAudioClockRateOneQ32; diff --git a/idd/LGAudio/BrokerDevice.cpp b/idd/LGAudio/BrokerDevice.cpp index f660755e..b874855d 100644 --- a/idd/LGAudio/BrokerDevice.cpp +++ b/idd/LGAudio/BrokerDevice.cpp @@ -860,7 +860,8 @@ namespace caps->maximumVersion = LGAUDIO_BROKER_ABI_VERSION; caps->capabilities = LGAUDIO_BROKER_CAP_CONTROL | LGAUDIO_BROKER_CAP_RENDER_READ | - LGAUDIO_BROKER_CAP_CAPTURE_WRITE; + LGAUDIO_BROKER_CAP_CAPTURE_WRITE | + LGAUDIO_BROKER_CAP_CLOCK_CONTROL; caps->maximumRenderTransfer = LGAUDIO_BROKER_MAX_PAYLOAD_BYTES; caps->maximumCaptureTransfer = LGAUDIO_BROKER_MAX_PAYLOAD_BYTES; return CompleteIrp(irp, STATUS_SUCCESS, sizeof(*caps)); @@ -1028,6 +1029,48 @@ namespace return CompleteIrp(irp, status); } + NTSTATUS SetClockControl( + _In_ BrokerControlExtension *extension, + _In_ PFILE_OBJECT fileObject, + _Inout_ PIRP irp, + _In_ ULONG inputLength) + { + const void *input = irp->AssociatedIrp.SystemBuffer; + if (!ValidateVersionedInput(input, inputLength, + sizeof(LGAudioBrokerClockControl))) + return CompleteIrp(irp, STATUS_INVALID_PARAMETER); + + const auto *control = + static_cast(input); + if (control->flags || control->reserved0 || + control->reserved[0] || control->reserved[1] || + !control->adapterGeneration || !control->bindingGeneration || + !control->streamGeneration || !control->targetRateQ32) + { + return CompleteIrp(irp, STATUS_INVALID_PARAMETER); + } + + BindingSnapshot binding{}; + NTSTATUS status = AcquireBinding(extension, fileObject, &binding); + if (NT_SUCCESS(status) && + (control->adapterGeneration != binding.adapterGeneration || + control->bindingGeneration != binding.bindingGeneration)) + { + status = STATUS_RETRY; + } + if (NT_SUCCESS(status) && !IsCurrentAdapter(binding)) + status = STATUS_DEVICE_NOT_CONNECTED; + if (NT_SUCCESS(status)) + { + status = binding.adapter->SetRenderClockRate( + control->streamGeneration, control->targetRateQ32); + } + + if (binding.adapter) + ExReleaseRundownProtection(&extension->bindingRundown); + return CompleteIrp(irp, status); + } + NTSTATUS QueryStats( _Inout_ PIRP irp, _In_ ULONG outputLength) @@ -1492,6 +1535,10 @@ namespace status = QueryStats(irp, outputLength); break; + case IOCTL_LGAUDIO_BROKER_SET_CLOCK_CONTROL: + status = SetClockControl(extension, fileObject, irp, inputLength); + break; + default: status = CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); break; diff --git a/idd/LGAudio/MiniportWaveRTStream.cpp b/idd/LGAudio/MiniportWaveRTStream.cpp index 6605a448..f3108607 100644 --- a/idd/LGAudio/MiniportWaveRTStream.cpp +++ b/idd/LGAudio/MiniportWaveRTStream.cpp @@ -21,14 +21,112 @@ #include "MiniportWaveRTStream.h" #include "MiniportWaveRT.h" +#include + namespace { constexpr ULONG kRenderPublicationRateHz = 4000; +constexpr ULONGLONG kMaximumTimeInterval = MAXULONGLONG >> 1; ULONGLONG MinUlonglong(ULONGLONG left, ULONGLONG right) { return left < right ? left : right; } + +bool Divide128( + ULONGLONG high, + ULONGLONG low, + ULONGLONG divisor, + ULONGLONG *quotient) +{ + if (!divisor || !quotient || high >= divisor) + return false; + if (!high) + { + *quotient = low / divisor; + return true; + } + + ULONGLONG result = 0; + ULONGLONG remainder = high; + for (ULONG bit = 64; bit; --bit) + { + const ULONG index = bit - 1; + const bool carry = (remainder & (1ULL << 63)) != 0; + remainder = (remainder << 1) | ((low >> index) & 1); + if (carry || remainder >= divisor) + { + remainder -= divisor; + result |= 1ULL << index; + } + } + + *quotient = result; + return true; +} + +ULONGLONG MultiplyDivide( + ULONGLONG value, + ULONGLONG multiplier, + ULONGLONG divisor, + bool roundUp) +{ + if (!divisor) + return 0; + + ULONGLONG high = 0; + ULONGLONG low = _umul128(value, multiplier, &high); + if (roundUp) + { + const ULONGLONG add = divisor - 1; + const ULONGLONG oldLow = low; + low += add; + if (low < oldLow) + { + if (high == MAXULONGLONG) + return kMaximumTimeInterval; + ++high; + } + } + + ULONGLONG result = 0; + if (!Divide128(high, low, divisor, &result)) + return kMaximumTimeInterval; + + return result < kMaximumTimeInterval ? + result : kMaximumTimeInterval; +} + +ULONGLONG AudioTicksToQpcTicks( + ULONGLONG audioTicks, + ULONGLONG audioFrequency, + ULONGLONG qpcFrequency, + ULONGLONG rateQ32, + bool roundUp) +{ + if (!audioFrequency || !qpcFrequency || !rateQ32) + return 0; + + const ULONGLONG unityQpcTicks = MultiplyDivide( + audioTicks, qpcFrequency, audioFrequency, roundUp); + return MultiplyDivide( + unityQpcTicks, kAudioClockRateOneQ32, rateQ32, roundUp); +} + +ULONGLONG FramesToQpcTicks( + ULONGLONG frames, + ULONG sampleRate, + ULONGLONG qpcFrequency, + ULONGLONG rateQ32) +{ + if (!sampleRate || !qpcFrequency || !rateQ32) + return 0; + + const ULONGLONG unityQpcTicks = MultiplyDivide( + frames, qpcFrequency, sampleRate, false); + return MultiplyDivide( + unityQpcTicks, kAudioClockRateOneQ32, rateQ32, false); +} } CMiniportWaveRTStream::CMiniportWaveRTStream( @@ -57,6 +155,7 @@ CMiniportWaveRTStream::CMiniportWaveRTStream( m_notificationEventCount(0), m_clockFrequency(0), m_qpcFrequency(0), + m_clockRateQ32(kAudioClockRateOneQ32), m_lastClockPosition(0), m_lastQpcPosition(0), m_lastPresentationQpc(0), @@ -211,8 +310,9 @@ NTSTATUS CMiniportWaveRTStream::SetState(_In_ KSSTATE state) { KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); - if (!clock.Frequency || !clock.QpcFrequency) + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); + if (!clock.Frequency || !clock.QpcFrequency || !clock.RateQ32) { KeReleaseSpinLock(&m_positionLock, oldIrql); return STATUS_DEVICE_HARDWARE_ERROR; @@ -238,6 +338,7 @@ NTSTATUS CMiniportWaveRTStream::SetState(_In_ KSSTATE state) m_kind == EndpointKind::Capture && !m_hasRun; m_clockFrequency = clock.Frequency; m_qpcFrequency = clock.QpcFrequency; + m_clockRateQ32 = clock.RateQ32; m_lastClockPosition = clock.Position; m_lastQpcPosition = clock.QpcPosition; m_lastPresentationQpc = clock.QpcPosition; @@ -259,7 +360,8 @@ NTSTATUS CMiniportWaveRTStream::SetState(_In_ KSSTATE state) KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); if (m_state == KSSTATE_RUN) AdvanceLocked(clock); m_state = state; @@ -268,6 +370,7 @@ NTSTATUS CMiniportWaveRTStream::SetState(_In_ KSSTATE state) { m_clockFrequency = 0; m_qpcFrequency = 0; + m_clockRateQ32 = kAudioClockRateOneQ32; m_lastClockPosition = 0; m_lastQpcPosition = 0; m_lastPresentationQpc = 0; @@ -299,7 +402,8 @@ NTSTATUS CMiniportWaveRTStream::GetPosition( KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); AdvanceLocked(clock); ULONG reportedPosition = m_cyclicPosition; if (m_notificationCount && m_bufferSize) @@ -443,7 +547,8 @@ NTSTATUS CMiniportWaveRTStream::GetReadPacket( KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); if (!m_notificationCount) { KeReleaseSpinLock(&m_positionLock, oldIrql); @@ -482,8 +587,9 @@ NTSTATUS CMiniportWaveRTStream::GetReadPacket( availablePacket * (m_packetSize / blockAlign); const ULONGLONG currentFrame = m_linearPosition / blockAlign; const ULONGLONG elapsedFrames = currentFrame - packetStartFrame; - const ULONGLONG elapsedTicks = - elapsedFrames * m_qpcFrequency / sampleRate; + const ULONGLONG elapsedTicks = FramesToQpcTicks( + elapsedFrames, static_cast(sampleRate), + m_qpcFrequency, m_clockRateQ32); *performanceCounterValue = elapsedTicks < m_lastPresentationQpc ? m_lastPresentationQpc - elapsedTicks : 0; *moreData = FALSE; @@ -505,7 +611,8 @@ NTSTATUS CMiniportWaveRTStream::SetWritePacket( KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); if (!m_notificationCount) { KeReleaseSpinLock(&m_positionLock, oldIrql); @@ -570,7 +677,8 @@ NTSTATUS CMiniportWaveRTStream::GetOutputStreamPresentationPosition( KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); if (!m_notificationCount) { KeReleaseSpinLock(&m_positionLock, oldIrql); @@ -585,10 +693,11 @@ NTSTATUS CMiniportWaveRTStream::GetOutputStreamPresentationPosition( const ULONG sampleRate = m_format.WaveFormatExt.Format.nSamplesPerSec; - const ULONGLONG ticksSinceReportedFrame = - sampleRate && clock.Frequency && clock.QpcFrequency ? - (m_frameRemainder * clock.QpcFrequency / clock.Frequency) / - sampleRate : 0; + const ULONGLONG fractionalAudioTicks = + sampleRate ? m_frameRemainder / sampleRate : 0; + const ULONGLONG ticksSinceReportedFrame = AudioTicksToQpcTicks( + fractionalAudioTicks, clock.Frequency, clock.QpcFrequency, + clock.RateQ32, false); if (m_pendingBytes || m_state == KSSTATE_RUN) { position->u64QPCPosition = @@ -609,7 +718,8 @@ NTSTATUS CMiniportWaveRTStream::GetPacketCount( KIRQL oldIrql; KeAcquireSpinLock(&m_positionLock, &oldIrql); - const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + const AudioClockSnapshot clock = + m_adapter->GetClockSnapshot(m_kind); if (!m_notificationCount) { KeReleaseSpinLock(&m_positionLock, oldIrql); @@ -755,7 +865,8 @@ void CMiniportWaveRTStream::StopTimer() void CMiniportWaveRTStream::ArmTimerLocked() { if (!m_timer || m_state != KSSTATE_RUN || !m_notificationCount || - !m_packetSize || !m_clockFrequency) + !m_packetSize || !m_clockFrequency || !m_qpcFrequency || + !m_clockRateQ32) return; const ULONG blockAlign = @@ -798,15 +909,18 @@ void CMiniportWaveRTStream::ArmTimerLocked() const ULONGLONG frameTicks = framesUntilTarget * frequency; const ULONGLONG ticksNumerator = frameTicks > m_frameRemainder ? frameTicks - m_frameRemainder : 0; - ULONGLONG qpcTicks = + const ULONGLONG audioTicks = (ticksNumerator + sampleRate - 1) / sampleRate; + ULONGLONG qpcTicks = AudioTicksToQpcTicks( + audioTicks, m_clockFrequency, m_qpcFrequency, + m_clockRateQ32, true); if (!qpcTicks) qpcTicks = 1; constexpr ULONGLONG kHundredNanosecondsPerSecond = 10000000; ULONGLONG dueTime = - (qpcTicks * kHundredNanosecondsPerSecond + frequency - 1) / - frequency; + MultiplyDivide(qpcTicks, kHundredNanosecondsPerSecond, + m_qpcFrequency, true); if (!dueTime) dueTime = 1; @@ -817,7 +931,7 @@ void CMiniportWaveRTStream::AdvanceLocked( _In_ const AudioClockSnapshot &clock) { if (m_state != KSSTATE_RUN || !m_buffer || !m_bufferSize || - !clock.Frequency || !clock.QpcFrequency) + !clock.Frequency || !clock.QpcFrequency || !clock.RateQ32) return; const bool clockDiscontinuity = @@ -827,6 +941,7 @@ void CMiniportWaveRTStream::AdvanceLocked( { m_clockFrequency = clock.Frequency; m_qpcFrequency = clock.QpcFrequency; + m_clockRateQ32 = clock.RateQ32; m_lastClockPosition = clock.Position; m_lastQpcPosition = clock.QpcPosition; m_lastPresentationQpc = clock.QpcPosition; @@ -837,6 +952,7 @@ void CMiniportWaveRTStream::AdvanceLocked( if (clock.Position == m_lastClockPosition) { m_qpcFrequency = clock.QpcFrequency; + m_clockRateQ32 = clock.RateQ32; m_lastQpcPosition = clock.QpcPosition; return; } @@ -849,6 +965,7 @@ void CMiniportWaveRTStream::AdvanceLocked( m_frameRemainder = numerator % clock.Frequency; m_clockFrequency = clock.Frequency; m_qpcFrequency = clock.QpcFrequency; + m_clockRateQ32 = clock.RateQ32; m_lastClockPosition = clock.Position; m_lastQpcPosition = clock.QpcPosition; @@ -1023,19 +1140,21 @@ void CMiniportWaveRTStream::UpdatePresentationQpcLocked( m_format.WaveFormatExt.Format.nBlockAlign; const ULONG sampleRate = m_format.WaveFormatExt.Format.nSamplesPerSec; - if (!clock.Frequency || !clock.QpcFrequency || !blockAlign || - !sampleRate) + if (!clock.Frequency || !clock.QpcFrequency || !clock.RateQ32 || + !blockAlign || !sampleRate) { m_lastPresentationQpc = clock.QpcPosition; return; } const ULONGLONG pendingFrames = m_pendingBytes / blockAlign; - const ULONGLONG wholeQpcTicks = - pendingFrames * clock.QpcFrequency / sampleRate; - const ULONGLONG fractionalQpcTicks = - (m_frameRemainder * clock.QpcFrequency / clock.Frequency) / - sampleRate; + const ULONGLONG wholeQpcTicks = FramesToQpcTicks( + pendingFrames, sampleRate, clock.QpcFrequency, clock.RateQ32); + const ULONGLONG fractionalAudioTicks = + m_frameRemainder / sampleRate; + const ULONGLONG fractionalQpcTicks = AudioTicksToQpcTicks( + fractionalAudioTicks, clock.Frequency, clock.QpcFrequency, + clock.RateQ32, false); const ULONGLONG ticksSinceBoundary = wholeQpcTicks + fractionalQpcTicks; m_lastPresentationQpc = ticksSinceBoundary < clock.QpcPosition ? @@ -1132,7 +1251,7 @@ void CMiniportWaveRTStream::TimerCallback( KeAcquireSpinLockAtDpcLevel(&stream->m_positionLock); const AudioClockSnapshot clock = - stream->m_adapter->GetClockSnapshot(); + stream->m_adapter->GetClockSnapshot(stream->m_kind); stream->AdvanceLocked(clock); if (stream->m_state == KSSTATE_RUN) stream->ArmTimerLocked(); diff --git a/idd/LGAudio/MiniportWaveRTStream.h b/idd/LGAudio/MiniportWaveRTStream.h index fb7a892b..0cca94ac 100644 --- a/idd/LGAudio/MiniportWaveRTStream.h +++ b/idd/LGAudio/MiniportWaveRTStream.h @@ -152,6 +152,7 @@ private: ULONGLONG m_clockFrequency; ULONGLONG m_qpcFrequency; + ULONGLONG m_clockRateQ32; ULONGLONG m_lastClockPosition; ULONGLONG m_lastQpcPosition; ULONGLONG m_lastPresentationQpc; diff --git a/idd/LGCommon/LGAudioBroker.h b/idd/LGCommon/LGAudioBroker.h index 1ab7f380..bf89bf31 100644 --- a/idd/LGCommon/LGAudioBroker.h +++ b/idd/LGCommon/LGAudioBroker.h @@ -102,6 +102,9 @@ static constexpr UINT32 IOCTL_LGAUDIO_BROKER_UNBIND = static constexpr UINT32 IOCTL_LGAUDIO_BROKER_QUERY_STATS = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x904, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS); +static constexpr UINT32 IOCTL_LGAUDIO_BROKER_SET_CLOCK_CONTROL = + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x905, METHOD_BUFFERED, + FILE_READ_ACCESS | FILE_WRITE_ACCESS); #pragma pack(push, 8) @@ -209,6 +212,20 @@ struct LGAudioBrokerStats UINT64 reserved[4]; }; +struct LGAudioBrokerClockControl +{ + UINT32 size; + UINT32 version; + UINT32 flags; + UINT32 reserved0; + UINT64 adapterGeneration; + UINT64 bindingGeneration; + UINT64 streamGeneration; + // Absolute render frames per second in unsigned Q32.32 format. + UINT64 targetRateQ32; + UINT64 reserved[2]; +}; + #pragma pack(pop) static_assert(sizeof(LGAudioBrokerHeader) == 8, @@ -227,6 +244,8 @@ static_assert(sizeof(LGAudioBrokerBind) == 48, "LGAudio broker binding layout changed"); static_assert(sizeof(LGAudioBrokerStats) == 160, "LGAudio broker statistics layout changed"); +static_assert(sizeof(LGAudioBrokerClockControl) == 64, + "LGAudio broker clock-control layout changed"); static_assert(LGAUDIO_BROKER_MAX_PAYLOAD_BYTES >= (192000U / 100U) * 8U * 3U, "LGAudio broker payload cannot hold 10 ms at the maximum format"); diff --git a/idd/LGIdd/audio/CLGAudioDevice.cpp b/idd/LGIdd/audio/CLGAudioDevice.cpp index 847cbafd..293ebb9d 100644 --- a/idd/LGIdd/audio/CLGAudioDevice.cpp +++ b/idd/LGIdd/audio/CLGAudioDevice.cpp @@ -45,6 +45,9 @@ namespace static constexpr DWORD OPEN_RETRY_MS = 500; static constexpr DWORD ACTIVE_POLL_MS = 10; static constexpr DWORD IDLE_POLL_MS = 100; + // Let the driver's 75 ms watchdog restore unity before local fallback. + static constexpr DWORD CLOCK_CONTROL_FAILURE_GRACE_MS = 100; + static constexpr uint32_t CLOCK_MAXIMUM_CORRECTION_PPM = 5000; static constexpr size_t BROKER_PACKET_BYTES = sizeof(LGAudioBrokerPacketHeader) + AUDIO_MAX_PCM_BYTES; @@ -176,6 +179,19 @@ namespace error == ERROR_RETRY || error == ERROR_DEVICE_NOT_CONNECTED; } + + bool ValidClockTarget(uint64_t targetRateQ32, uint32_t sampleRate) + { + if (!targetRateQ32 || !sampleRate) + return false; + + const uint64_t nominalRateQ32 = + static_cast(sampleRate) << 32; + const uint64_t maximumDelta = nominalRateQ32 * + CLOCK_MAXIMUM_CORRECTION_PPM / 1000000; + return targetRateQ32 >= nominalRateQ32 - maximumDelta && + targetRateQ32 <= nominalRateQ32 + maximumDelta; + } } class CLGAudioDevice::CState @@ -224,6 +240,10 @@ private: bool m_bound = false; ActivationState m_activation = ActivationState::IDLE; bool m_controlDirty = true; + AudioClockFeedback m_clockFeedback; + bool m_clockFeedbackPending = false; + bool m_clockControlFailureActive = false; + uint64_t m_clockControlFailureSince = 0; AudioEndpointStatus m_status; uint64_t m_endpointGeneration = 1; @@ -277,6 +297,30 @@ private: return m_playbackBlocked; } + void ClearClockFeedbackLocked() + { + m_clockFeedback = {}; + m_clockFeedbackPending = false; + } + + void ClearClockControlFailureLocked() + { + m_clockControlFailureActive = false; + m_clockControlFailureSince = 0; + } + + void ResetClockControlLocked() + { + ClearClockFeedbackLocked(); + ClearClockControlFailureLocked(); + } + + void ResetClockControl() + { + CSRWExclusiveLock lock(m_lock); + ResetClockControlLocked(); + } + static DWORD CALLBACK ThreadProc(void * opaque) { static_cast(opaque)->Thread(); @@ -342,6 +386,14 @@ private: if (advance) m_endpointGeneration = NextGeneration(m_endpointGeneration); next.generation = m_endpointGeneration; + if (next.generation != m_status.generation || + next.capabilities != m_status.capabilities || + next.available != m_status.available || + next.active != m_status.active || + !SameStream(next.playback, m_status.playback)) + { + ResetClockControlLocked(); + } if (SameStatus(next, m_status)) return; m_status = next; @@ -535,6 +587,7 @@ private: void CloseBroker(bool publish = true) { + ResetClockControl(); if (m_handle == INVALID_HANDLE_VALUE) return; @@ -575,6 +628,7 @@ private: m_activation != ActivationState::DEACTIVATING) return false; m_activation = ActivationState::IDLE; + ResetClockControlLocked(); m_captureHead = 0; m_captureCount = 0; } @@ -651,6 +705,7 @@ private: else if (!bound && m_activation == ActivationState::DEACTIVATING) { m_activation = ActivationState::IDLE; + ResetClockControlLocked(); return true; } return false; @@ -733,6 +788,95 @@ private: return true; } + void ProcessClockControl() + { + AudioClockFeedback feedback; + { + CSRWExclusiveLock lock(m_lock); + if (!m_clockFeedbackPending) + return; + + feedback = m_clockFeedback; + ClearClockFeedbackLocked(); + if (!m_started || m_stopping || + m_activation != ActivationState::ACTIVE || + !(m_status.capabilities & AUDIO_CAP_CLOCK_FEEDBACK) || + !m_status.available || !m_status.active || + !m_status.playback.formatValid || + m_status.playback.state != AudioStreamState::RUNNING || + feedback.endpointGeneration != m_status.generation || + feedback.playbackGeneration != m_status.playback.generation) + { + return; + } + } + + const uint32_t requiredFlags = LGAUDIO_BROKER_STATE_BOUND | + LGAUDIO_BROKER_STATE_DATA_PLANE_ACTIVE; + if (m_handle == INVALID_HANDLE_VALUE || + !(m_caps.capabilities & LGAUDIO_BROKER_CAP_CLOCK_CONTROL) || + (m_brokerState.flags & requiredFlags) != requiredFlags || + m_brokerState.render.state != LGAUDIO_BROKER_STREAM_RUNNING || + feedback.playbackGeneration != m_brokerState.render.generation) + { + return; + } + + LGAudioBrokerClockControl control = {}; + control.size = sizeof(control); + control.version = LGAUDIO_BROKER_ABI_VERSION; + control.adapterGeneration = m_brokerState.adapterGeneration; + control.bindingGeneration = m_brokerState.bindingGeneration; + control.streamGeneration = feedback.playbackGeneration; + control.targetRateQ32 = feedback.targetRateQ32; + + DWORD returned = 0; + DWORD error = ERROR_SUCCESS; + if (Ioctl(IOCTL_LGAUDIO_BROKER_SET_CLOCK_CONTROL, + &control, sizeof(control), nullptr, 0, returned, error) && + returned == 0) + { + CSRWExclusiveLock lock(m_lock); + ClearClockControlFailureLocked(); + return; + } + + if (error == ERROR_SUCCESS) + error = ERROR_INVALID_DATA; + if (Stopping()) + return; + + if (RecoverableIoError(error)) + m_nextPollAt = 0; + + const uint64_t now = GetTickCount64(); + bool sustained = false; + { + CSRWExclusiveLock lock(m_lock); + if (!m_clockControlFailureActive) + { + m_clockControlFailureActive = true; + m_clockControlFailureSince = now; + } + else + { + sustained = now - m_clockControlFailureSince >= + CLOCK_CONTROL_FAILURE_GRACE_MS; + } + } + if (!sustained) + return; + + DEBUG_WARN("Looking Glass Audio clock control failed: %lu", error); + m_caps.capabilities &= ~LGAUDIO_BROKER_CAP_CLOCK_CONTROL; + { + CSRWExclusiveLock lock(m_lock); + m_lastError = error; + ResetClockControlLocked(); + } + PublishBrokerState(true); + } + bool ValidateRender(const LGAudioBrokerPacketHeader& header, DWORD transferred, AudioPacket& packet) const { @@ -1111,6 +1255,7 @@ private: if (!Reconcile(false)) continue; + ProcessClockControl(); DeliverRender(); StartWriteIo(); StartRenderIo(); @@ -1128,6 +1273,7 @@ private: { CSRWExclusiveLock lock(m_lock); m_activation = ActivationState::DEACTIVATING; + ResetClockControlLocked(); } if (m_handle != INVALID_HANDLE_VALUE) { @@ -1209,6 +1355,7 @@ public: m_events = &events; m_started = true; m_stopping = false; + ResetClockControlLocked(); m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr); if (!m_thread) { @@ -1228,6 +1375,7 @@ public: if (!m_started) return; m_stopping = true; + ResetClockControlLocked(); thread = m_thread; SetEvent(m_stopEvent); SetEvent(m_wakeEvent); @@ -1248,6 +1396,7 @@ public: m_captureCount = 0; m_activation = ActivationState::IDLE; m_controlDirty = true; + ResetClockControlLocked(); m_status = {}; m_status.generation = m_endpointGeneration; CloseEvents(); @@ -1290,12 +1439,16 @@ public: if (m_activation == ActivationState::DEACTIVATING) return AudioResult::BUSY; if (m_activation == ActivationState::IDLE) + { + ResetClockControlLocked(); m_activation = ActivationState::ACTIVATING; + } } else if (m_activation == ActivationState::ACTIVATING || m_activation == ActivationState::ACTIVE) { m_activation = ActivationState::DEACTIVATING; + ResetClockControlLocked(); m_controlDirty = true; SetEvent(m_wakeEvent); } @@ -1362,16 +1515,29 @@ public: AUDIO_CLOCK_VALID | AUDIO_CLOCK_STABLE | AUDIO_CLOCK_DISCONTINUITY; if ((feedback.flags & ~allowedFlags) || - !(feedback.flags & AUDIO_CLOCK_VALID)) + !(feedback.flags & AUDIO_CLOCK_VALID) || + !feedback.endpointGeneration || + !feedback.playbackGeneration || !feedback.targetRateQ32) return AudioResult::REJECTED; - CSRWSharedLock lock(m_lock); - if (!m_started || m_stopping || !m_status.active) + CSRWExclusiveLock lock(m_lock); + if (!m_started || m_stopping || + m_activation != ActivationState::ACTIVE || + !(m_status.capabilities & AUDIO_CAP_CLOCK_FEEDBACK) || + !m_status.available || !m_status.active || + !m_status.playback.formatValid || + m_status.playback.state != AudioStreamState::RUNNING) return AudioResult::UNAVAILABLE; if (feedback.endpointGeneration != m_status.generation || feedback.playbackGeneration != m_status.playback.generation) return AudioResult::STALE; - // The v1 broker advertises no clock-control capability or IOCTL yet. - return AudioResult::UNAVAILABLE; + if (!ValidClockTarget(feedback.targetRateQ32, + m_status.playback.format.sampleRate)) + return AudioResult::REJECTED; + + m_clockFeedback = feedback; + m_clockFeedbackPending = true; + SetEvent(m_wakeEvent); + return AudioResult::ACCEPTED; } void PlaybackReceiveReady() diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp index 29f1dc05..1a001b50 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp @@ -1188,8 +1188,23 @@ void CLGMPAudioTransport::PumpState() m_announcedCapture = 0; } - if (playbackCurrent && - m_announcedPlayback != m_state.playback.generation) + const bool startPlayback = playbackCurrent && + m_announcedPlayback != m_state.playback.generation; + const bool startCapture = captureCurrent && + m_announcedCapture != m_state.capture.generation; + + // Publish capabilities before starts so the client selects the correct + // rate-control path. A second status below records the announced streams. + if (m_statusDirty && (startPlayback || startCapture)) + { + const KVMFRAudioStatus status = WireStatus(); + if (!WriteMessage(KVMFR_AUDIO_MESSAGE_STATUS, + 0, 0, 0, 0, 0, 0, &status, sizeof(status))) + return; + m_statusDirty = false; + } + + if (startPlayback) { const KVMFRAudioFormat format = WireFormat(m_state.playback.format); if (!WriteMessage(KVMFR_AUDIO_MESSAGE_PLAYBACK_START, @@ -1197,10 +1212,10 @@ void CLGMPAudioTransport::PumpState() &format, sizeof(format))) return; m_announcedPlayback = m_state.playback.generation; + m_statusDirty = true; } - if (captureCurrent && - m_announcedCapture != m_state.capture.generation) + if (startCapture) { const KVMFRAudioFormat format = WireFormat(m_state.capture.format); if (!WriteMessage(KVMFR_AUDIO_MESSAGE_CAPTURE_START, @@ -1208,6 +1223,7 @@ void CLGMPAudioTransport::PumpState() &format, sizeof(format))) return; m_announcedCapture = m_state.capture.generation; + m_statusDirty = true; } if (!m_statusDirty) @@ -1658,7 +1674,8 @@ void CLGMPAudioTransport::DrainInbound() } if (callClock) { - if (result == AudioResult::FAILED) + if (result == AudioResult::FAILED || + result == AudioResult::REJECTED) BeginRelease("audio clock target failed"); continue; }