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; }