From 405233bfb96c01f373ef9b6b4bb2682a7687c0c0 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 2 Sep 2026 03:38:27 +1000 Subject: [PATCH] [idd] audio: poll LGMP streams at low latency Move audio ingress and playback-credit recovery off the display timer. Poll through a dedicated MMCSS worker at sub-millisecond intervals. Wake capture retries immediately and drain queued client records during release so backpressure cannot stall recording or shutdown. --- idd/LGIdd/audio/CLGAudioDevice.cpp | 11 + .../transport/lgmp/CLGMPAudioTransport.cpp | 270 +++++++++++++++++- .../transport/lgmp/CLGMPAudioTransport.h | 11 + 3 files changed, 289 insertions(+), 3 deletions(-) diff --git a/idd/LGIdd/audio/CLGAudioDevice.cpp b/idd/LGIdd/audio/CLGAudioDevice.cpp index b87c0bcb..847cbafd 100644 --- a/idd/LGIdd/audio/CLGAudioDevice.cpp +++ b/idd/LGIdd/audio/CLGAudioDevice.cpp @@ -26,6 +26,7 @@ #include +#include #include #include #include @@ -1078,6 +1079,14 @@ private: void Thread() { + DWORD avTask = 0; + HANDLE avTaskHandle = + AvSetMmThreadCharacteristicsW(L"Pro Audio", &avTask); + if (avTaskHandle && + !AvSetMmThreadPriority(avTaskHandle, AVRT_PRIORITY_HIGH)) + DEBUG_WARN("Failed to raise audio broker MMCSS priority: %lu", + GetLastError()); + while (!Stopping()) { if (m_handle == INVALID_HANDLE_VALUE) @@ -1131,6 +1140,8 @@ private: CloseBroker(false); } FinishAbsentDeactivation(); + if (avTaskHandle) + AvRevertMmThreadCharacteristics(avTaskHandle); } void CloseEvents() diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp index 3d79cbcb..c4d3babe 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp @@ -29,10 +29,15 @@ #include "transport/lgmp/CLGMPControl.h" #include "transport/lgmp/CLGMPHost.h" +#include #include #include #include +#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION + #define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 +#endif + namespace { static_assert(AUDIO_MAX_CHANNELS == KVMFR_AUDIO_MAX_CHANNELS, @@ -72,6 +77,13 @@ namespace constexpr size_t AUDIO_SHARED_MEMORY_PREFLIGHT = 2U * 1024U * 1024U; + bool ArmAudioPollTimer(HANDLE timer, uint32_t waitUs) + { + LARGE_INTEGER due = {}; + due.QuadPart = -static_cast(waitUs) * 10; + return SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE) != FALSE; + } + uint32_t ChannelCount(uint32_t mask) { uint32_t count = 0; @@ -246,6 +258,199 @@ void CLGMPAudioTransport::FreeStreams() m_hostToClientDescriptor = {}; } +DWORD CALLBACK CLGMPAudioTransport::InboundThreadProc(void * context) +{ + static_cast(context)->InboundThread(); + return 0; +} + +bool CLGMPAudioTransport::StartInboundWorker() +{ + m_inboundStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + m_inboundWakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + m_inboundPollTimer = CreateWaitableTimerExW(nullptr, nullptr, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + if (!m_inboundPollTimer) + m_inboundPollTimer = CreateWaitableTimerExW( + nullptr, nullptr, 0, TIMER_ALL_ACCESS); + + if (!m_inboundStopEvent || !m_inboundWakeEvent || + !m_inboundPollTimer) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create LGMP audio worker resources"); + if (m_inboundPollTimer) + CloseHandle(m_inboundPollTimer); + if (m_inboundWakeEvent) + CloseHandle(m_inboundWakeEvent); + if (m_inboundStopEvent) + CloseHandle(m_inboundStopEvent); + m_inboundPollTimer = nullptr; + m_inboundWakeEvent = nullptr; + m_inboundStopEvent = nullptr; + return false; + } + + m_inboundThread = CreateThread(nullptr, 0, InboundThreadProc, + this, 0, nullptr); + if (!m_inboundThread) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create LGMP audio worker"); + CloseHandle(m_inboundPollTimer); + CloseHandle(m_inboundWakeEvent); + CloseHandle(m_inboundStopEvent); + m_inboundPollTimer = nullptr; + m_inboundWakeEvent = nullptr; + m_inboundStopEvent = nullptr; + return false; + } + return true; +} + +void CLGMPAudioTransport::StopInboundWorker() +{ + HANDLE thread = nullptr; + { + CSRWExclusiveLock lock(m_lock); + m_inboundStopping = true; + thread = m_inboundThread; + if (m_inboundStopEvent) + SetEvent(m_inboundStopEvent); + } + if (thread) + WaitForSingleObject(thread, INFINITE); + + CSRWExclusiveLock lock(m_lock); + if (m_inboundThread) + CloseHandle(m_inboundThread); + if (m_inboundPollTimer) + CloseHandle(m_inboundPollTimer); + if (m_inboundWakeEvent) + CloseHandle(m_inboundWakeEvent); + if (m_inboundStopEvent) + CloseHandle(m_inboundStopEvent); + m_inboundThread = nullptr; + m_inboundPollTimer = nullptr; + m_inboundWakeEvent = nullptr; + m_inboundStopEvent = nullptr; +} + +void CLGMPAudioTransport::InboundThread() +{ + DWORD avTask = 0; + HANDLE avTaskHandle = + AvSetMmThreadCharacteristicsW(L"Pro Audio", &avTask); + if (avTaskHandle && + !AvSetMmThreadPriority(avTaskHandle, AVRT_PRIORITY_HIGH)) + DEBUG_WARN("Failed to raise audio ingress MMCSS priority: %lu", + GetLastError()); + + LGMPStreamPollState streamPoll = {}; + LGMPStreamPollConfig pollConfig = {}; + pollConfig.spinCount = 64U; + pollConfig.minWaitUs = 25U; + pollConfig.maxWaitUs = 250U; + const LGMP_STATUS pollStatus = lgmpStreamPollInit(&streamPoll, + pollConfig); + if (pollStatus != LGMP_OK) + { + DEBUG_ERROR("Failed to initialize LGMP audio polling: %s", + lgmpStatusString(pollStatus)); + if (avTaskHandle) + AvRevertMmThreadCharacteristics(avTaskHandle); + return; + } + + const HANDLE idleWaits[] = + { m_inboundStopEvent, m_inboundWakeEvent }; + const HANDLE activeWaits[] = + { m_inboundStopEvent, m_inboundWakeEvent, m_inboundPollTimer }; + for (;;) + { + if (WaitForSingleObject(m_inboundStopEvent, 0) == WAIT_OBJECT_0) + break; + + uint64_t before = 0; + uint64_t hostBefore = 0; + bool active = false; + bool playbackCreditAcquired = false; + { + CSRWSharedLock lock(m_lock); + before = m_inboundActivitySerial; + hostBefore = m_hostSequence; + active = m_ownerState != OwnerState::NONE; + } + + if (active) + { + DrainInbound(); + { + CSRWExclusiveLock lock(m_lock); + const bool hadPlaybackCredit = m_playbackCreditValid; + PumpState(); + CheckPlaybackCredit(); + playbackCreditAcquired = !hadPlaybackCredit && + m_playbackCreditValid; + } + NotifyPlaybackReady(); + } + + { + CSRWSharedLock lock(m_lock); + if (before != m_inboundActivitySerial || + hostBefore != m_hostSequence || playbackCreditAcquired) + lgmpStreamPollActivity(&streamPoll); + active = m_ownerState != OwnerState::NONE; + } + + if (!active) + { + const DWORD wait = WaitForMultipleObjects( + _countof(idleWaits), idleWaits, FALSE, INFINITE); + if (wait == WAIT_OBJECT_0) + break; + if (wait != WAIT_OBJECT_0 + 1) + { + DEBUG_ERROR_HR(GetLastError(), + "LGMP audio worker wait failed"); + break; + } + lgmpStreamPollActivity(&streamPoll); + continue; + } + + const uint32_t waitUs = lgmpStreamPollIdle(&streamPoll); + if (!waitUs) + continue; + if (!ArmAudioPollTimer(m_inboundPollTimer, waitUs)) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to arm LGMP audio timer"); + break; + } + + const DWORD wait = WaitForMultipleObjects( + _countof(activeWaits), activeWaits, FALSE, INFINITE); + if (wait == WAIT_OBJECT_0) + break; + if (wait == WAIT_OBJECT_0 + 1) + { + lgmpStreamPollActivity(&streamPoll); + continue; + } + if (wait != WAIT_OBJECT_0 + 2) + { + DEBUG_ERROR_HR(GetLastError(), + "LGMP audio worker wait failed"); + break; + } + } + + if (avTaskHandle) + AvRevertMmThreadCharacteristics(avTaskHandle); +} + bool CLGMPAudioTransport::Initialize() { CSRWExclusiveLock lifecycleLock(m_lifecycleLock); @@ -288,6 +493,15 @@ bool CLGMPAudioTransport::Initialize() m_state.generation = 1; m_initialized = true; + m_inboundStopping = false; + if (!StartInboundWorker()) + { + m_initialized = false; + for (ResponseMemory& response : m_responseMemory) + lgmpHostMemFree(&response.memory); + FreeStreams(); + return false; + } return true; } @@ -535,6 +749,7 @@ void CLGMPAudioTransport::ReleasePendingCapture() status != LGMP_ERR_STREAM_STALE) Fail("lgmpHostStreamReadRelease(audio)", status); m_pendingCapture = {}; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); } void CLGMPAudioTransport::CancelPlaybackCredit() @@ -594,7 +809,10 @@ void CLGMPAudioTransport::BeginRelease(const char * reason) m_announcedPlayback = 0; m_announcedCapture = 0; ClearBlockedPlayback(true); - ReleasePendingCapture(); + if (!m_captureCallbackInFlight) + ReleasePendingCapture(); + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); for (PendingResponse& pending : m_pendingResponse) if (pending.valid && pending.clientID == m_ownerClientID && @@ -634,6 +852,7 @@ void CLGMPAudioTransport::ResetOwner() m_nextClientSequence = 0; m_clientSequenceKnown = false; m_captureDiscontinuity = false; + m_captureCallbackInFlight = false; m_pendingCapture = {}; m_ownerSerial = NextSerial(m_ownerSerial); } @@ -691,6 +910,8 @@ void CLGMPAudioTransport::InitializeOwner(uint32_t clientID, m_pendingCapture = {}; m_ownerSerial = NextSerial(m_ownerSerial); RenewLease(); + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); } void CLGMPAudioTransport::HandleClaim( @@ -1137,6 +1358,7 @@ bool CLGMPAudioTransport::PrepareCapture( m_pendingCapture.readySerial = m_captureReadySerial; m_nextClientSequence = message.sequence + 1; m_clientSequenceKnown = true; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); return true; } @@ -1152,6 +1374,32 @@ void CLGMPAudioTransport::DrainInbound() uint64_t ownerSerial = 0; { CSRWExclusiveLock lock(m_lock); + if (m_ownerState == OwnerState::RELEASING) + { + ReleasePendingCapture(); + LGMPStreamBuffer buffer = {}; + const LGMP_STATUS status = lgmpHostStreamReadPeek( + m_clientToHostStream, &buffer); + if (status == LGMP_ERR_STREAM_EMPTY || + status == LGMP_ERR_STREAM_UNBOUND || + status == LGMP_ERR_STREAM_STALE) + return; + if (status != LGMP_OK) + { + Fail("lgmpHostStreamReadPeek(audio release)", status); + return; + } + const LGMP_STATUS release = lgmpHostStreamReadRelease( + m_clientToHostStream, &buffer); + if (release != LGMP_OK) + { + Fail("lgmpHostStreamReadRelease(audio release)", release); + return; + } + m_inboundActivitySerial = + NextSerial(m_inboundActivitySerial); + continue; + } if (m_ownerState != OwnerState::BOUND || !m_responseAdmitted || m_failed) return; @@ -1216,6 +1464,8 @@ void CLGMPAudioTransport::DrainInbound() } m_nextClientSequence = message.sequence + 1; m_clientSequenceKnown = true; + m_inboundActivitySerial = + NextSerial(m_inboundActivitySerial); return true; }; @@ -1385,6 +1635,7 @@ void CLGMPAudioTransport::DrainInbound() return; } } + m_captureCallbackInFlight = target && callCapture; } AudioResult result = AudioResult::UNAVAILABLE; @@ -1396,9 +1647,15 @@ void CLGMPAudioTransport::DrainInbound() { CSRWExclusiveLock lock(m_lock); + if (callCapture) + m_captureCallbackInFlight = false; if (ownerSerial != m_ownerSerial || m_ownerState != OwnerState::BOUND) + { + if (m_pendingCapture.valid) + ReleasePendingCapture(); return; + } if (callClock) { if (result == AudioResult::FAILED) @@ -1425,6 +1682,8 @@ void CLGMPAudioTransport::DrainInbound() return; } m_pendingCapture = {}; + m_inboundActivitySerial = + NextSerial(m_inboundActivitySerial); m_captureDiscontinuity = result != AudioResult::ACCEPTED; if (result == AudioResult::FAILED || result == AudioResult::REJECTED) @@ -1531,8 +1790,6 @@ void CLGMPAudioTransport::Process() PumpResponses(); } - DrainInbound(); - { CSRWExclusiveLock lock(m_lock); MaintainOwner(GetTickCount64()); @@ -1580,6 +1837,7 @@ void CLGMPAudioTransport::Stop() void CLGMPAudioTransport::TransportStop() { CSRWExclusiveLock lifecycleLock(m_lifecycleLock); + StopInboundWorker(); CSRWExclusiveLock callbackLock(m_callbackLock); CSRWExclusiveLock lock(m_lock); m_started = false; @@ -1622,6 +1880,8 @@ void CLGMPAudioTransport::AudioState( m_stateKnown = true; if (m_ownerState == OwnerState::BOUND && m_responseAdmitted) m_statusDirty = true; + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); } AudioResult CLGMPAudioTransport::SendPlayback( @@ -1754,5 +2014,9 @@ void CLGMPAudioTransport::CaptureReceiveReady() { CSRWExclusiveLock lock(m_lock); if (m_started) + { m_captureReadySerial = NextSerial(m_captureReadySerial); + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); + } } diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h index 5462aa4a..af0c3aee 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h +++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h @@ -106,6 +106,11 @@ private: CSRWLock m_lifecycleLock; CSRWLock m_callbackLock; CSRWLock m_lock; + HANDLE m_inboundStopEvent = nullptr; + HANDLE m_inboundWakeEvent = nullptr; + HANDLE m_inboundPollTimer = nullptr; + HANDLE m_inboundThread = nullptr; + bool m_inboundStopping = false; IAudioTarget * m_target = nullptr; bool m_initialized = false; bool m_started = false; @@ -135,6 +140,7 @@ private: uint64_t m_nextClientSequence = 0; bool m_clientSequenceKnown = false; bool m_captureDiscontinuity = false; + bool m_captureCallbackInFlight = false; bool m_playbackBlocked = false; bool m_playbackReadyPending = false; bool m_playbackCreditValid = false; @@ -142,10 +148,15 @@ private: AudioPacket m_blockedPlayback = {}; size_t m_blockedPlaybackSize = 0; uint64_t m_captureReadySerial = 0; + uint64_t m_inboundActivitySerial = 0; PendingCapture m_pendingCapture; bool InitializeStreams(); void FreeStreams(); + bool StartInboundWorker(); + void StopInboundWorker(); + void InboundThread(); + static DWORD CALLBACK InboundThreadProc(void * context); bool BindStreams(uint32_t clientID); bool TryUnbindStreams(); void ForceUnbindStreams();