diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index 2c3141de..d4930589 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -79,6 +79,7 @@
+
@@ -158,6 +159,7 @@
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index 5811185e..4c43e19a 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -256,6 +256,9 @@
Transport\LGMP
+
+ Transport\LGMP
+
Transport\LGMP
@@ -429,6 +432,9 @@
Transport\LGMP
+
+ Transport\LGMP
+
Transport\LGMP
diff --git a/idd/LGIdd/audio/CLGAudioDevice.cpp b/idd/LGIdd/audio/CLGAudioDevice.cpp
index 16dd3abf..b87c0bcb 100644
--- a/idd/LGIdd/audio/CLGAudioDevice.cpp
+++ b/idd/LGIdd/audio/CLGAudioDevice.cpp
@@ -1348,7 +1348,8 @@ public:
AudioResult SetClockFeedback(const AudioClockFeedback& feedback)
{
const uint32_t allowedFlags =
- AUDIO_CLOCK_VALID | AUDIO_CLOCK_STABLE;
+ AUDIO_CLOCK_VALID | AUDIO_CLOCK_STABLE |
+ AUDIO_CLOCK_DISCONTINUITY;
if ((feedback.flags & ~allowedFlags) ||
!(feedback.flags & AUDIO_CLOCK_VALID))
return AudioResult::REJECTED;
diff --git a/idd/LGIdd/transport/IAudioSource.h b/idd/LGIdd/transport/IAudioSource.h
index 1f5bff2f..2c4931bd 100644
--- a/idd/LGIdd/transport/IAudioSource.h
+++ b/idd/LGIdd/transport/IAudioSource.h
@@ -49,8 +49,9 @@ enum AudioPacketFlag : uint32_t
enum AudioClockFlag : uint32_t
{
- AUDIO_CLOCK_VALID = 1U << 0,
- AUDIO_CLOCK_STABLE = 1U << 1,
+ AUDIO_CLOCK_VALID = 1U << 0,
+ AUDIO_CLOCK_STABLE = 1U << 1,
+ AUDIO_CLOCK_DISCONTINUITY = 1U << 2,
};
enum class AudioStreamState : uint32_t
@@ -118,6 +119,7 @@ struct AudioClockFeedback
uint64_t playbackGeneration = 0;
uint64_t position = 0;
int64_t timeNs = 0;
+ uint64_t rateQ32 = 0;
uint64_t targetRateQ32 = 0;
uint32_t flags = 0;
};
diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp
new file mode 100644
index 00000000..3d79cbcb
--- /dev/null
+++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp
@@ -0,0 +1,1758 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include
+
+#include "transport/lgmp/CLGMPAudioTransport.h"
+
+#include
+
+#include "CDebug.h"
+#include "WCWrite.h"
+#include "transport/lgmp/CLGMPControl.h"
+#include "transport/lgmp/CLGMPHost.h"
+
+#include
+#include
+#include
+
+namespace
+{
+ static_assert(AUDIO_MAX_CHANNELS == KVMFR_AUDIO_MAX_CHANNELS,
+ "audio channel limits differ");
+ static_assert(AUDIO_STREAM_MAX_CHANNELS ==
+ KVMFR_AUDIO_STREAM_MAX_CHANNELS,
+ "audio stream channel limits differ");
+ static_assert(AUDIO_MAX_FRAMES_PER_PACKET ==
+ KVMFR_AUDIO_MAX_FRAMES_PER_PACKET,
+ "audio packet limits differ");
+ static_assert(AUDIO_MAX_PCM_BYTES == KVMFR_AUDIO_PCM_BYTES,
+ "audio PCM limits differ");
+ static_assert(static_cast(AUDIO_CAP_PLAYBACK) ==
+ static_cast(KVMFR_AUDIO_CAP_PLAYBACK) &&
+ static_cast(AUDIO_CAP_CAPTURE) ==
+ static_cast(KVMFR_AUDIO_CAP_CAPTURE) &&
+ static_cast(AUDIO_CAP_CLOCK_FEEDBACK) ==
+ static_cast(KVMFR_AUDIO_CAP_CLOCK_FEEDBACK),
+ "audio capability bits differ");
+ static_assert(sizeof(LGMPStreamDescriptor) ==
+ sizeof(KVMFRStreamDescriptor),
+ "LGMP and KVMFR stream descriptor sizes differ");
+
+ constexpr uint32_t AUDIO_CAPABILITY_MASK =
+ KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE |
+ KVMFR_AUDIO_CAP_CLOCK_FEEDBACK;
+ constexpr uint32_t AUDIO_DIRECTION_MASK =
+ KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE;
+ constexpr uint32_t AUDIO_PACKET_FLAG_MASK =
+ AUDIO_PACKET_DISCONTINUITY | AUDIO_PACKET_SILENT |
+ AUDIO_PACKET_CLOCK_VALID | AUDIO_PACKET_CLOCK_STABLE;
+ constexpr uint32_t WIRE_PACKET_FLAG_MASK =
+ KVMFR_AUDIO_MESSAGE_DISCONTINUITY |
+ KVMFR_AUDIO_MESSAGE_SILENT |
+ KVMFR_AUDIO_MESSAGE_CLOCK_VALID |
+ KVMFR_AUDIO_MESSAGE_CLOCK_STABLE;
+ constexpr size_t AUDIO_SHARED_MEMORY_PREFLIGHT =
+ 2U * 1024U * 1024U;
+
+ uint32_t ChannelCount(uint32_t mask)
+ {
+ uint32_t count = 0;
+ while (mask)
+ {
+ count += mask & 1U;
+ mask >>= 1;
+ }
+ return count;
+ }
+
+ uint64_t NextSerial(uint64_t serial)
+ {
+ ++serial;
+ return serial ? serial : 1;
+ }
+
+ bool RequestNewer(uint32_t candidate, uint32_t previous)
+ {
+ return candidate != previous &&
+ static_cast(candidate - previous) > 0;
+ }
+}
+
+CLGMPAudioTransport::~CLGMPAudioTransport()
+{
+ TransportStop();
+
+ CSRWExclusiveLock lock(m_lock);
+ for (ResponseMemory& response : m_responseMemory)
+ lgmpHostMemFree(&response.memory);
+ FreeStreams();
+ m_initialized = false;
+}
+
+bool CLGMPAudioTransport::SameFormat(
+ const AudioFormat& left, const AudioFormat& right)
+{
+ return left.sampleRate == right.sampleRate &&
+ left.channelMask == right.channelMask &&
+ left.channels == right.channels &&
+ left.containerBits == right.containerBits &&
+ left.validBits == right.validBits &&
+ left.blockAlign == right.blockAlign;
+}
+
+bool CLGMPAudioTransport::SamePacket(
+ const AudioPacket& left, const AudioPacket& right)
+{
+ return left.endpointGeneration == right.endpointGeneration &&
+ left.streamGeneration == right.streamGeneration &&
+ left.flags == right.flags && left.frames == right.frames &&
+ left.position == right.position && left.timeNs == right.timeNs &&
+ left.rateQ32 == right.rateQ32 &&
+ SameFormat(left.format, right.format);
+}
+
+bool CLGMPAudioTransport::ValidFormat(const AudioFormat& format)
+{
+ if (!format.sampleRate ||
+ format.sampleRate > KVMFR_AUDIO_MAX_SAMPLE_RATE ||
+ !format.channels ||
+ format.channels > KVMFR_AUDIO_STREAM_MAX_CHANNELS ||
+ !format.channelMask ||
+ (format.channelMask & ~KVMFR_AUDIO_CHANNEL_POSITION_MASK) ||
+ ChannelCount(format.channelMask) != format.channels)
+ return false;
+
+ const bool s16 = format.containerBits == 16 && format.validBits == 16;
+ const bool s24 = format.containerBits == 24 && format.validBits == 24;
+ const uint16_t bytes = static_cast(
+ format.channels * (format.containerBits / 8U));
+ return (s16 || s24) && format.blockAlign == bytes;
+}
+
+KVMFRAudioFormat CLGMPAudioTransport::WireFormat(
+ const AudioFormat& format)
+{
+ KVMFRAudioFormat result = {};
+ result.version = KVMFR_AUDIO_FORMAT_VERSION;
+ result.size = sizeof(result);
+ result.sampleFormat = format.containerBits == 16 ?
+ KVMFR_AUDIO_SAMPLE_S16_LE : KVMFR_AUDIO_SAMPLE_S24_LE;
+ result.sampleRate = format.sampleRate;
+ result.channelMask = format.channelMask;
+ result.channelCount = format.channels;
+ result.validBits = format.validBits;
+ result.containerBits = format.containerBits;
+ result.blockAlign = format.blockAlign;
+ return result;
+}
+
+KVMFRAudioStreamState CLGMPAudioTransport::WireState(
+ AudioStreamState state)
+{
+ switch (state)
+ {
+ case AudioStreamState::ACQUIRED:
+ return KVMFR_AUDIO_STREAM_ACQUIRED;
+ case AudioStreamState::PAUSED:
+ return KVMFR_AUDIO_STREAM_PAUSED;
+ case AudioStreamState::RUNNING:
+ return KVMFR_AUDIO_STREAM_RUNNING;
+ default:
+ return KVMFR_AUDIO_STREAM_STOPPED;
+ }
+}
+
+KVMFRStreamDescriptor CLGMPAudioTransport::WireDescriptor(
+ const LGMPStreamDescriptor& descriptor)
+{
+ KVMFRStreamDescriptor wire = {};
+ wire.magic = descriptor.magic;
+ wire.version = descriptor.version;
+ wire.size = descriptor.size;
+ wire.offset = descriptor.offset;
+ wire.regionSize = descriptor.regionSize;
+ wire.direction = descriptor.direction;
+ wire.policy = descriptor.policy;
+ wire.slotCount = descriptor.slotCount;
+ wire.slotSize = descriptor.slotSize;
+ return wire;
+}
+
+bool CLGMPAudioTransport::InitializeStreams()
+{
+ const LGMPStreamConfig hostToClient =
+ {
+ LGMP_STREAM_HOST_TO_CLIENT,
+ LGMP_STREAM_RELIABLE_FIFO,
+ KVMFR_AUDIO_STREAM_SLOT_COUNT,
+ static_cast(KVMFR_AUDIO_STREAM_SLOT_BYTES),
+ };
+ LGMP_STATUS status = m_host.CreateStream(
+ hostToClient, &m_hostToClientStream);
+ if (status != LGMP_OK)
+ {
+ DEBUG_ERROR("Failed to create host-to-client audio stream: %s",
+ lgmpStatusString(status));
+ return false;
+ }
+
+ const LGMPStreamConfig clientToHost =
+ {
+ LGMP_STREAM_CLIENT_TO_HOST,
+ LGMP_STREAM_RELIABLE_FIFO,
+ KVMFR_AUDIO_STREAM_SLOT_COUNT,
+ static_cast(KVMFR_AUDIO_STREAM_SLOT_BYTES),
+ };
+ status = m_host.CreateStream(clientToHost, &m_clientToHostStream);
+ if (status != LGMP_OK)
+ {
+ DEBUG_ERROR("Failed to create client-to-host audio stream: %s",
+ lgmpStatusString(status));
+ lgmpHostStreamFree(&m_hostToClientStream);
+ return false;
+ }
+
+ LGMPStreamDescriptor descriptor = {};
+ lgmpHostStreamGetDescriptor(m_hostToClientStream, &descriptor);
+ m_hostToClientDescriptor = WireDescriptor(descriptor);
+ lgmpHostStreamGetDescriptor(m_clientToHostStream, &descriptor);
+ m_clientToHostDescriptor = WireDescriptor(descriptor);
+ return true;
+}
+
+void CLGMPAudioTransport::FreeStreams()
+{
+ lgmpHostStreamFree(&m_clientToHostStream);
+ lgmpHostStreamFree(&m_hostToClientStream);
+ m_clientToHostDescriptor = {};
+ m_hostToClientDescriptor = {};
+}
+
+bool CLGMPAudioTransport::Initialize()
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ CSRWExclusiveLock lock(m_lock);
+ if (m_initialized)
+ return true;
+
+ // LGMP shared allocations are monotonic. Reserve conservatively before
+ // the first one so ordinary shared-memory exhaustion cannot strand a
+ // partial optional layout. A later local handle allocation can still fail
+ // with LGMP_ERR_NO_MEM, but that is independent of shared capacity.
+ if (m_host.Available() < AUDIO_SHARED_MEMORY_PREFLIGHT)
+ {
+ DEBUG_WARN("LGMP audio requires at least %zu bytes of available "
+ "shared memory", AUDIO_SHARED_MEMORY_PREFLIGHT);
+ return false;
+ }
+ if (!InitializeStreams())
+ return false;
+
+ LGMP_STATUS status = LGMP_OK;
+ for (ResponseMemory& response : m_responseMemory)
+ {
+ status = m_host.Allocate(sizeof(KVMFRAudioStreamResponse),
+ &response.memory);
+ if (status != LGMP_OK)
+ break;
+ memset(lgmpHostMemPtr(response.memory), 0,
+ sizeof(KVMFRAudioStreamResponse));
+ }
+ if (status != LGMP_OK)
+ {
+ DEBUG_ERROR("Failed to allocate audio response memory: %s",
+ lgmpStatusString(status));
+ for (ResponseMemory& response : m_responseMemory)
+ lgmpHostMemFree(&response.memory);
+ FreeStreams();
+ return false;
+ }
+
+ m_state.generation = 1;
+ m_initialized = true;
+ return true;
+}
+
+bool CLGMPAudioTransport::BindStreams(uint32_t clientID)
+{
+ LGMP_STATUS status = lgmpHostStreamBind(
+ m_hostToClientStream, clientID, nullptr);
+ if (status != LGMP_OK)
+ {
+ DEBUG_WARN("Failed to bind host-to-client audio stream for client "
+ "%u: %s", clientID, lgmpStatusString(status));
+ return false;
+ }
+
+ status = lgmpHostStreamBind(m_clientToHostStream, clientID, nullptr);
+ if (status == LGMP_OK)
+ return true;
+
+ DEBUG_WARN("Failed to bind client-to-host audio stream for client "
+ "%u: %s", clientID, lgmpStatusString(status));
+ const LGMP_STATUS rollback =
+ lgmpHostStreamUnbind(m_hostToClientStream);
+ if (rollback != LGMP_OK && rollback != LGMP_ERR_STREAM_UNBOUND)
+ DEBUG_WARN("Failed to roll back the audio stream binding: %s",
+ lgmpStatusString(rollback));
+ return false;
+}
+
+bool CLGMPAudioTransport::TryUnbindStreams()
+{
+ bool complete = true;
+ for (PLGMPHostStream stream :
+ { m_clientToHostStream, m_hostToClientStream })
+ {
+ if (!stream)
+ continue;
+ const LGMP_STATUS status = lgmpHostStreamUnbind(stream);
+ if (status == LGMP_ERR_STREAM_BUSY)
+ complete = false;
+ else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
+ {
+ Fail("lgmpHostStreamUnbind", status);
+ complete = false;
+ }
+ }
+ return complete;
+}
+
+void CLGMPAudioTransport::ForceUnbindStreams()
+{
+ for (PLGMPHostStream stream :
+ { m_clientToHostStream, m_hostToClientStream })
+ {
+ if (!stream)
+ continue;
+ const LGMP_STATUS status = lgmpHostStreamForceUnbind(stream);
+ if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
+ DEBUG_WARN("Failed to force-unbind an audio stream: %s",
+ lgmpStatusString(status));
+ }
+}
+
+bool CLGMPAudioTransport::ValidateClaim(
+ const KVMFRAudioClaim& claim) const
+{
+ if (claim.messageType != KVMFR_MESSAGE_AUDIO_CLAIM ||
+ claim.version != KVMFR_AUDIO_CONTROL_VERSION ||
+ claim.size != sizeof(claim) || !claim.requestGeneration ||
+ claim.flags != KVMFR_AUDIO_CLAIM_BIND || !claim.claimToken ||
+ !claim.protocolMin || claim.protocolMin > claim.protocolMax ||
+ claim.protocolMin > KVMFR_AUDIO_PROTOCOL_MAX ||
+ claim.protocolMax < KVMFR_AUDIO_PROTOCOL_MIN ||
+ (claim.clientCapabilities & ~AUDIO_CAPABILITY_MASK) ||
+ !(claim.clientCapabilities & AUDIO_DIRECTION_MASK) ||
+ claim.reserved0)
+ return false;
+
+ for (uint64_t reserved : claim.reserved)
+ if (reserved)
+ return false;
+ return true;
+}
+
+bool CLGMPAudioTransport::ResponsePending(uint32_t clientID,
+ const KVMFRAudioStreamResponse& candidate) const
+{
+ for (const PendingResponse& pending : m_pendingResponse)
+ if (pending.valid && pending.clientID == clientID &&
+ pending.response.requestGeneration ==
+ candidate.requestGeneration &&
+ pending.response.claimToken == candidate.claimToken &&
+ pending.response.result == candidate.result)
+ return true;
+
+ for (const ResponseMemory& response : m_responseMemory)
+ if (response.valid && response.clientID == clientID &&
+ response.requestGeneration == candidate.requestGeneration &&
+ response.claimToken == candidate.claimToken &&
+ response.result == candidate.result)
+ return true;
+ return false;
+}
+
+bool CLGMPAudioTransport::QueueResponse(uint32_t clientID,
+ const KVMFRAudioStreamResponse& response)
+{
+ if (ResponsePending(clientID, response))
+ return true;
+
+ PendingResponse * available = nullptr;
+ PendingResponse * sameClient = nullptr;
+ for (PendingResponse& pending : m_pendingResponse)
+ {
+ if (pending.valid && pending.clientID == clientID)
+ {
+ if (pending.response.result == KVMFR_AUDIO_RESPONSE_BOUND)
+ return false;
+ if (!sameClient)
+ sameClient = &pending;
+ continue;
+ }
+ if (!pending.valid && !available)
+ available = &pending;
+ }
+ if (sameClient)
+ available = sameClient;
+ if (!available)
+ return false;
+
+ available->valid = true;
+ available->clientID = clientID;
+ available->response = response;
+ return true;
+}
+
+void CLGMPAudioTransport::CleanupResponseMemory()
+{
+ for (ResponseMemory& response : m_responseMemory)
+ {
+ if (!response.valid ||
+ m_control.PayloadPending(response.memory))
+ continue;
+ response.valid = false;
+ response.clientID = 0;
+ response.requestGeneration = 0;
+ response.claimToken = 0;
+ response.result = 0;
+ }
+}
+
+void CLGMPAudioTransport::PumpResponses()
+{
+ CleanupResponseMemory();
+ for (PendingResponse& pending : m_pendingResponse)
+ {
+ if (!pending.valid)
+ continue;
+
+ ResponseMemory * available = nullptr;
+ for (ResponseMemory& response : m_responseMemory)
+ if (response.memory && !response.valid)
+ {
+ available = &response;
+ break;
+ }
+ if (!available)
+ return;
+
+ memcpy(lgmpHostMemPtr(available->memory), &pending.response,
+ sizeof(pending.response));
+ WCWrite::Flush();
+ unsigned recipients = 0;
+ const LGMP_STATUS status = m_control.PostForClient(
+ KVMFR_AUDIO_CONTROL_UDATA, available->memory,
+ pending.clientID, recipients);
+ if (status == LGMP_ERR_QUEUE_FULL)
+ return;
+ if (status != LGMP_OK)
+ {
+ Fail("lgmpHostQueuePostForClients(audio)", status);
+ return;
+ }
+
+ const bool bound =
+ pending.response.result == KVMFR_AUDIO_RESPONSE_BOUND;
+ if (recipients)
+ {
+ available->valid = true;
+ available->clientID = pending.clientID;
+ available->requestGeneration =
+ pending.response.requestGeneration;
+ available->claimToken = pending.response.claimToken;
+ available->result = pending.response.result;
+ if (bound && m_ownerState == OwnerState::BOUND &&
+ m_ownerClientID == pending.clientID &&
+ m_ownerGeneration == pending.response.requestGeneration &&
+ m_ownerToken == pending.response.claimToken)
+ {
+ m_responseAdmitted = true;
+ m_statusDirty = true;
+ }
+ }
+ else if (bound && m_ownerState == OwnerState::BOUND &&
+ m_ownerClientID == pending.clientID &&
+ m_ownerGeneration == pending.response.requestGeneration &&
+ m_ownerToken == pending.response.claimToken)
+ BeginRelease("audio claimant disappeared");
+
+ pending = {};
+ }
+}
+
+bool CLGMPAudioTransport::OwnerSubscribed() const
+{
+ if (!m_ownerClientID)
+ return false;
+
+ uint32_t clients[LGMP_MAX_CLIENTS] = {};
+ unsigned count = 0;
+ const LGMP_STATUS status = m_control.GetClientIDs(clients, count);
+ if (status != LGMP_OK)
+ {
+ DEBUG_WARN("Failed to query audio owner subscription: %s",
+ lgmpStatusString(status));
+ return true;
+ }
+ for (unsigned index = 0; index < count; ++index)
+ if (clients[index] == m_ownerClientID)
+ return true;
+ return false;
+}
+
+void CLGMPAudioTransport::RenewLease()
+{
+ m_ownerDeadline = GetTickCount64() + KVMFR_AUDIO_OWNER_LEASE_MS;
+}
+
+void CLGMPAudioTransport::ReleasePendingCapture()
+{
+ if (!m_pendingCapture.valid)
+ return;
+ const LGMP_STATUS status = lgmpHostStreamReadRelease(
+ m_clientToHostStream, &m_pendingCapture.buffer);
+ if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND &&
+ status != LGMP_ERR_STREAM_STALE)
+ Fail("lgmpHostStreamReadRelease(audio)", status);
+ m_pendingCapture = {};
+}
+
+void CLGMPAudioTransport::CancelPlaybackCredit()
+{
+ if (!m_playbackCreditValid)
+ return;
+
+ const LGMP_STATUS status = lgmpHostStreamWriteCancel(
+ m_hostToClientStream, &m_playbackCredit);
+ if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND &&
+ status != LGMP_ERR_STREAM_STALE)
+ {
+ DEBUG_WARN("Failed to cancel an audio playback reservation: %s",
+ lgmpStatusString(status));
+ m_failed = true;
+ }
+ m_playbackCredit = {};
+ m_playbackCreditValid = false;
+}
+
+void CLGMPAudioTransport::ClearBlockedPlayback(bool notify)
+{
+ const bool blocked = m_playbackBlocked;
+ CancelPlaybackCredit();
+ m_playbackBlocked = false;
+ m_blockedPlayback = {};
+ m_blockedPlaybackSize = 0;
+ if (notify && blocked)
+ m_playbackReadyPending = true;
+}
+
+bool CLGMPAudioTransport::RememberBlockedPlayback(
+ const AudioPacket& packet, size_t size)
+{
+ if (m_playbackBlocked)
+ return size == m_blockedPlaybackSize &&
+ SamePacket(packet, m_blockedPlayback);
+
+ m_playbackBlocked = true;
+ m_blockedPlayback = packet;
+ m_blockedPlaybackSize = size;
+ return true;
+}
+
+void CLGMPAudioTransport::BeginRelease(const char * reason)
+{
+ if (m_ownerState == OwnerState::NONE)
+ return;
+ if (m_ownerState != OwnerState::RELEASING)
+ {
+ DEBUG_INFO("Audio owner %u generation %u releasing: %s",
+ m_ownerClientID, m_ownerGeneration, reason);
+ m_ownerState = OwnerState::RELEASING;
+ m_releaseGeneration = m_state.generation;
+ m_deactivationIssued = false;
+ m_statusDirty = false;
+ m_announcedPlayback = 0;
+ m_announcedCapture = 0;
+ ClearBlockedPlayback(true);
+ ReleasePendingCapture();
+
+ for (PendingResponse& pending : m_pendingResponse)
+ if (pending.valid && pending.clientID == m_ownerClientID &&
+ pending.response.result == KVMFR_AUDIO_RESPONSE_BOUND)
+ pending = {};
+ }
+}
+
+void CLGMPAudioTransport::ResetOwner()
+{
+ ClearBlockedPlayback(false);
+ for (ClaimRecord& record : m_claimRecord)
+ if (record.valid && record.clientID == m_ownerClientID &&
+ record.requestGeneration == m_ownerGeneration &&
+ record.claimToken == m_ownerToken)
+ {
+ record.closed = true;
+ break;
+ }
+
+ m_ownerState = OwnerState::NONE;
+ m_ownerClientID = 0;
+ m_ownerGeneration = 0;
+ m_clientCapabilities = 0;
+ m_ownerToken = 0;
+ m_ownerDeadline = 0;
+ m_releaseGeneration = 0;
+ m_responseAdmitted = false;
+ m_clientReady = false;
+ m_activationIssued = false;
+ m_deactivationIssued = false;
+ m_ownerResponse = {};
+ m_statusDirty = false;
+ m_announcedPlayback = 0;
+ m_announcedCapture = 0;
+ m_hostSequence = 0;
+ m_nextClientSequence = 0;
+ m_clientSequenceKnown = false;
+ m_captureDiscontinuity = false;
+ m_pendingCapture = {};
+ m_ownerSerial = NextSerial(m_ownerSerial);
+}
+
+void CLGMPAudioTransport::MaintainOwner(uint64_t now)
+{
+ if (m_ownerState == OwnerState::NONE)
+ return;
+ if (m_ownerState != OwnerState::RELEASING)
+ {
+ if (!OwnerSubscribed())
+ BeginRelease("audio pointer subscriber disappeared");
+ else if (m_pendingCapture.valid)
+ RenewLease();
+ else if (now >= m_ownerDeadline)
+ BeginRelease("audio owner lease expired");
+ }
+
+ if (m_ownerState != OwnerState::RELEASING)
+ return;
+
+ const bool streamsUnbound = TryUnbindStreams();
+
+ const bool inactive = !m_started || !m_activationIssued ||
+ (m_stateKnown && !m_state.active &&
+ m_state.generation != m_releaseGeneration);
+ if (streamsUnbound && inactive)
+ {
+ DEBUG_INFO("Audio owner %u generation %u released",
+ m_ownerClientID, m_ownerGeneration);
+ ResetOwner();
+ }
+}
+
+void CLGMPAudioTransport::InitializeOwner(uint32_t clientID,
+ const KVMFRAudioClaim& claim)
+{
+ m_ownerState = OwnerState::BOUND;
+ m_ownerClientID = clientID;
+ m_ownerGeneration = claim.requestGeneration;
+ m_clientCapabilities = claim.clientCapabilities;
+ m_ownerToken = claim.claimToken;
+ m_responseAdmitted = false;
+ m_clientReady = false;
+ m_activationIssued = false;
+ m_deactivationIssued = false;
+ m_statusDirty = true;
+ m_announcedPlayback = 0;
+ m_announcedCapture = 0;
+ m_hostSequence = 0;
+ m_nextClientSequence = 0;
+ m_clientSequenceKnown = false;
+ m_captureDiscontinuity = false;
+ ClearBlockedPlayback(false);
+ m_pendingCapture = {};
+ m_ownerSerial = NextSerial(m_ownerSerial);
+ RenewLease();
+}
+
+void CLGMPAudioTransport::HandleClaim(
+ uint32_t clientID, const KVMFRAudioClaim& claim)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_initialized)
+ return;
+
+ KVMFRAudioStreamResponse response = {};
+ response.magic = KVMFR_AUDIO_RESPONSE_MAGIC;
+ response.version = KVMFR_AUDIO_CONTROL_VERSION;
+ response.size = sizeof(response);
+ response.requestGeneration = claim.requestGeneration;
+ response.boundClientID = clientID;
+ response.endpointGeneration = m_state.generation;
+ response.claimToken = claim.claimToken;
+
+ ClaimRecord * claimRecord = nullptr;
+ ClaimRecord * claimSlot = nullptr;
+ for (ClaimRecord& record : m_claimRecord)
+ {
+ if (record.valid && record.clientID == clientID)
+ claimRecord = &record;
+ if (!record.valid && !claimSlot)
+ claimSlot = &record;
+ }
+ if (claimRecord)
+ claimSlot = claimRecord;
+
+ const bool claimValid = ValidateClaim(claim);
+ if (!claimValid)
+ response.result = KVMFR_AUDIO_RESPONSE_INCOMPATIBLE;
+ else if (m_failed || !m_started || !m_stateKnown)
+ response.result = KVMFR_AUDIO_RESPONSE_ERROR;
+ else if (m_ownerState != OwnerState::NONE)
+ {
+ if (m_ownerState != OwnerState::RELEASING &&
+ m_ownerClientID == clientID &&
+ m_ownerGeneration == claim.requestGeneration &&
+ m_ownerToken == claim.claimToken)
+ {
+ RenewLease();
+ response = m_ownerResponse;
+ }
+ else
+ response.result = KVMFR_AUDIO_RESPONSE_BUSY;
+ }
+ else if (claimRecord &&
+ claimRecord->requestGeneration == claim.requestGeneration &&
+ claimRecord->claimToken == claim.claimToken)
+ {
+ if (claimRecord->response.result == KVMFR_AUDIO_RESPONSE_ERROR)
+ response = claimRecord->response;
+ else
+ response.result = KVMFR_AUDIO_RESPONSE_BUSY;
+ }
+ else if (claimRecord && !RequestNewer(
+ claim.requestGeneration, claimRecord->requestGeneration))
+ response.result = KVMFR_AUDIO_RESPONSE_BUSY;
+ else if (!claimSlot)
+ {
+ DEBUG_WARN("Audio claim history is full for client %u", clientID);
+ response.result = KVMFR_AUDIO_RESPONSE_ERROR;
+ }
+ else if (BindStreams(clientID))
+ {
+ InitializeOwner(clientID, claim);
+ response.result = KVMFR_AUDIO_RESPONSE_BOUND;
+ response.protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX;
+ response.hostToClient = m_hostToClientDescriptor;
+ response.clientToHost = m_clientToHostDescriptor;
+ m_ownerResponse = response;
+ claimSlot->valid = true;
+ claimSlot->closed = false;
+ claimSlot->clientID = clientID;
+ claimSlot->requestGeneration = claim.requestGeneration;
+ claimSlot->claimToken = claim.claimToken;
+ claimSlot->response = response;
+ DEBUG_INFO("Audio owner %u generation %u admitted",
+ m_ownerClientID, m_ownerGeneration);
+ }
+ else
+ response.result = KVMFR_AUDIO_RESPONSE_ERROR;
+
+ if (claimValid && claimSlot &&
+ response.result == KVMFR_AUDIO_RESPONSE_ERROR &&
+ (!claimRecord || RequestNewer(claim.requestGeneration,
+ claimRecord->requestGeneration)))
+ {
+ claimSlot->valid = true;
+ claimSlot->closed = true;
+ claimSlot->clientID = clientID;
+ claimSlot->requestGeneration = claim.requestGeneration;
+ claimSlot->claimToken = claim.claimToken;
+ claimSlot->response = response;
+ }
+
+ if (!QueueResponse(clientID, response))
+ {
+ DEBUG_WARN("Audio response queue is full for client %u", clientID);
+ if (response.result == KVMFR_AUDIO_RESPONSE_BOUND &&
+ m_ownerClientID == clientID)
+ BeginRelease("audio response queue exhausted");
+ }
+ PumpResponses();
+}
+
+KVMFRAudioStatus CLGMPAudioTransport::WireStatus() const
+{
+ KVMFRAudioStatus status = {};
+ status.version = KVMFR_AUDIO_STATUS_VERSION;
+ status.size = sizeof(status);
+ status.protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX;
+ status.requestGeneration = m_ownerGeneration;
+ status.capabilities =
+ m_state.capabilities & m_clientCapabilities;
+ status.ownerClientID = m_ownerClientID;
+ status.ownerGeneration = m_ownerGeneration;
+ status.endpointGeneration = m_state.generation;
+ status.playbackGeneration = m_state.playback.generation;
+ status.captureGeneration = m_state.capture.generation;
+ status.playbackState = WireState(m_state.playback.state);
+ status.captureState = WireState(m_state.capture.state);
+ if (m_state.playback.formatValid &&
+ ValidFormat(m_state.playback.format))
+ status.playbackFormat = WireFormat(m_state.playback.format);
+ if (m_state.capture.formatValid &&
+ ValidFormat(m_state.capture.format))
+ status.captureFormat = WireFormat(m_state.capture.format);
+
+ if (m_state.available)
+ status.flags |= KVMFR_AUDIO_STATUS_AVAILABLE;
+ if (m_ownerState != OwnerState::NONE)
+ status.flags |= KVMFR_AUDIO_STATUS_HAS_OWNER;
+ if (status.capabilities & KVMFR_AUDIO_CAP_PLAYBACK)
+ status.flags |= KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE;
+ if (status.capabilities & KVMFR_AUDIO_CAP_CAPTURE)
+ status.flags |= KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE;
+ if ((status.capabilities & KVMFR_AUDIO_CAP_PLAYBACK) &&
+ m_state.available && m_state.active &&
+ m_state.playback.state != AudioStreamState::UNAVAILABLE &&
+ m_state.playback.state != AudioStreamState::STOPPED)
+ status.flags |= KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE;
+ if ((status.capabilities & KVMFR_AUDIO_CAP_CAPTURE) &&
+ m_state.available && m_state.active &&
+ m_state.capture.state != AudioStreamState::UNAVAILABLE &&
+ m_state.capture.state != AudioStreamState::STOPPED)
+ status.flags |= KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE;
+ return status;
+}
+
+bool CLGMPAudioTransport::CommitMessage(LGMPStreamBuffer& buffer,
+ KVMFRAudioMessageType type, uint64_t streamGeneration,
+ KVMFRAudioMessageFlags flags, uint64_t position, int64_t timeNs,
+ uint64_t rateQ32, uint32_t frames, const void * payload,
+ uint32_t length)
+{
+ const uint32_t bytes = static_cast(
+ sizeof(KVMFRAudioMessage) + length);
+ if (!buffer.data || buffer.capacity < bytes)
+ {
+ const LGMP_STATUS cancel = lgmpHostStreamWriteCancel(
+ m_hostToClientStream, &buffer);
+ if (cancel != LGMP_OK)
+ DEBUG_WARN("Failed to cancel an undersized audio stream "
+ "reservation: %s", lgmpStatusString(cancel));
+ Fail("undersized host-to-client audio stream slot",
+ LGMP_ERR_INVALID_SIZE);
+ return false;
+ }
+
+ KVMFRAudioMessage message = {};
+ message.version = KVMFR_AUDIO_MESSAGE_VERSION;
+ message.size = sizeof(message);
+ message.type = type;
+ message.streamGeneration = streamGeneration;
+ message.flags = flags;
+ message.sequence = m_hostSequence;
+ message.position = position;
+ message.timeNs = timeNs;
+ message.rateQ32 = rateQ32;
+ message.frames = frames;
+ message.length = length;
+ memcpy(buffer.data, &message, sizeof(message));
+ if (length)
+ WCWrite::Copy(static_cast(buffer.data) + sizeof(message),
+ payload, length);
+
+ const LGMP_STATUS status = lgmpHostStreamWriteCommit(
+ m_hostToClientStream, &buffer, bytes);
+ if (status == LGMP_OK)
+ {
+ ++m_hostSequence;
+ return true;
+ }
+ if (status == LGMP_ERR_STREAM_UNBOUND ||
+ status == LGMP_ERR_STREAM_STALE)
+ {
+ BeginRelease("host-to-client audio stream binding lost");
+ return false;
+ }
+ Fail("lgmpHostStreamWriteCommit(audio)", status);
+ return false;
+}
+
+bool CLGMPAudioTransport::WriteMessage(KVMFRAudioMessageType type,
+ uint64_t streamGeneration, KVMFRAudioMessageFlags flags,
+ uint64_t position, int64_t timeNs, uint64_t rateQ32,
+ uint32_t frames, const void * payload, uint32_t length)
+{
+ if (m_playbackCreditValid)
+ return false;
+
+ LGMPStreamBuffer buffer = {};
+ const LGMP_STATUS status = lgmpHostStreamWriteAcquire(
+ m_hostToClientStream, &buffer);
+ if (status == LGMP_ERR_STREAM_FULL)
+ return false;
+ if (status == LGMP_ERR_STREAM_UNBOUND ||
+ status == LGMP_ERR_STREAM_STALE)
+ {
+ BeginRelease("host-to-client audio stream binding lost");
+ return false;
+ }
+ if (status != LGMP_OK)
+ {
+ Fail("lgmpHostStreamWriteAcquire(audio)", status);
+ return false;
+ }
+
+ return CommitMessage(buffer, type, streamGeneration, flags,
+ position, timeNs, rateQ32, frames, payload, length);
+}
+
+void CLGMPAudioTransport::PumpState()
+{
+ if (m_ownerState != OwnerState::BOUND || !m_responseAdmitted ||
+ !m_clientReady || !m_stateKnown || m_failed ||
+ m_playbackCreditValid)
+ return;
+
+ const bool playbackCurrent = m_state.available && m_state.active &&
+ (m_clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) &&
+ (m_state.capabilities & AUDIO_CAP_PLAYBACK) &&
+ m_state.playback.formatValid &&
+ ValidFormat(m_state.playback.format) &&
+ m_state.playback.state != AudioStreamState::UNAVAILABLE &&
+ m_state.playback.state != AudioStreamState::STOPPED;
+ if (m_announcedPlayback &&
+ (!playbackCurrent ||
+ m_announcedPlayback != m_state.playback.generation))
+ {
+ if (!WriteMessage(KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP,
+ m_announcedPlayback, 0, 0, 0, 0, 0, nullptr, 0))
+ return;
+ m_announcedPlayback = 0;
+ }
+
+ const bool captureCurrent = m_state.available && m_state.active &&
+ (m_clientCapabilities & KVMFR_AUDIO_CAP_CAPTURE) &&
+ (m_state.capabilities & AUDIO_CAP_CAPTURE) &&
+ m_state.capture.formatValid &&
+ ValidFormat(m_state.capture.format) &&
+ m_state.capture.state != AudioStreamState::UNAVAILABLE &&
+ m_state.capture.state != AudioStreamState::STOPPED;
+ if (m_announcedCapture &&
+ (!captureCurrent ||
+ m_announcedCapture != m_state.capture.generation))
+ {
+ if (!WriteMessage(KVMFR_AUDIO_MESSAGE_CAPTURE_STOP,
+ m_announcedCapture, 0, 0, 0, 0, 0, nullptr, 0))
+ return;
+ m_announcedCapture = 0;
+ }
+
+ if (playbackCurrent &&
+ m_announcedPlayback != m_state.playback.generation)
+ {
+ const KVMFRAudioFormat format = WireFormat(m_state.playback.format);
+ if (!WriteMessage(KVMFR_AUDIO_MESSAGE_PLAYBACK_START,
+ m_state.playback.generation, 0, 0, 0, 0, 0,
+ &format, sizeof(format)))
+ return;
+ m_announcedPlayback = m_state.playback.generation;
+ }
+
+ if (captureCurrent &&
+ m_announcedCapture != m_state.capture.generation)
+ {
+ const KVMFRAudioFormat format = WireFormat(m_state.capture.format);
+ if (!WriteMessage(KVMFR_AUDIO_MESSAGE_CAPTURE_START,
+ m_state.capture.generation, 0, 0, 0, 0, 0,
+ &format, sizeof(format)))
+ return;
+ m_announcedCapture = m_state.capture.generation;
+ }
+
+ if (!m_statusDirty)
+ return;
+ const KVMFRAudioStatus status = WireStatus();
+ if (WriteMessage(KVMFR_AUDIO_MESSAGE_STATUS,
+ 0, 0, 0, 0, 0, 0, &status, sizeof(status)))
+ m_statusDirty = false;
+}
+
+void CLGMPAudioTransport::CheckPlaybackCredit()
+{
+ if (!m_playbackBlocked || m_playbackCreditValid ||
+ m_playbackReadyPending || m_statusDirty ||
+ m_ownerState != OwnerState::BOUND || !m_responseAdmitted ||
+ !m_clientReady || !m_stateKnown || !m_state.available ||
+ !m_state.active ||
+ !(m_clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) ||
+ !(m_state.capabilities & AUDIO_CAP_PLAYBACK) ||
+ m_blockedPlayback.endpointGeneration != m_state.generation ||
+ m_blockedPlayback.streamGeneration !=
+ m_state.playback.generation ||
+ !m_state.playback.formatValid ||
+ m_state.playback.state != AudioStreamState::RUNNING ||
+ !SameFormat(m_blockedPlayback.format,
+ m_state.playback.format) ||
+ m_announcedPlayback != m_blockedPlayback.streamGeneration)
+ return;
+
+ m_playbackCredit = {};
+ const LGMP_STATUS status = lgmpHostStreamWriteAcquire(
+ m_hostToClientStream, &m_playbackCredit);
+ if (status == LGMP_ERR_STREAM_FULL)
+ return;
+ if (status == LGMP_ERR_STREAM_UNBOUND ||
+ status == LGMP_ERR_STREAM_STALE)
+ {
+ BeginRelease("host-to-client audio credit binding lost");
+ return;
+ }
+ if (status != LGMP_OK)
+ {
+ Fail("lgmpHostStreamWriteAcquire(audio credit)", status);
+ return;
+ }
+
+ const size_t required = sizeof(KVMFRAudioMessage) +
+ m_blockedPlaybackSize;
+ if (!m_playbackCredit.data ||
+ m_playbackCredit.capacity < required)
+ {
+ const LGMP_STATUS cancel = lgmpHostStreamWriteCancel(
+ m_hostToClientStream, &m_playbackCredit);
+ m_playbackCredit = {};
+ if (cancel != LGMP_OK)
+ DEBUG_WARN("Failed to cancel an undersized audio credit: %s",
+ lgmpStatusString(cancel));
+ Fail("undersized host-to-client audio stream slot",
+ LGMP_ERR_INVALID_SIZE);
+ return;
+ }
+
+ m_playbackCreditValid = true;
+ m_playbackReadyPending = true;
+}
+
+bool CLGMPAudioTransport::ParseInbound(
+ const LGMPStreamBuffer& buffer, KVMFRAudioMessage& message) const
+{
+ if (!buffer.data || buffer.size < sizeof(message) ||
+ buffer.size > buffer.capacity)
+ return false;
+ memcpy(&message, buffer.data, sizeof(message));
+ if (message.version != KVMFR_AUDIO_MESSAGE_VERSION ||
+ message.size != sizeof(message) || message.reserved0 ||
+ message.length > KVMFR_AUDIO_PCM_BYTES ||
+ message.length >
+ (std::numeric_limits::max)() - sizeof(message))
+ return false;
+ return buffer.size == sizeof(message) + message.length;
+}
+
+bool CLGMPAudioTransport::PendingCaptureCurrent() const
+{
+ if (!m_pendingCapture.valid)
+ return false;
+
+ const AudioPacket& packet = m_pendingCapture.packet;
+ return m_stateKnown && m_state.available && m_state.active &&
+ (m_clientCapabilities & KVMFR_AUDIO_CAP_CAPTURE) &&
+ (m_state.capabilities & AUDIO_CAP_CAPTURE) &&
+ m_state.capture.formatValid &&
+ ValidFormat(m_state.capture.format) &&
+ m_state.capture.state == AudioStreamState::RUNNING &&
+ packet.endpointGeneration == m_state.generation &&
+ packet.streamGeneration == m_state.capture.generation &&
+ SameFormat(packet.format, m_state.capture.format);
+}
+
+bool CLGMPAudioTransport::PrepareCapture(
+ const KVMFRAudioMessage& message, const LGMPStreamBuffer& buffer)
+{
+ if (!m_stateKnown || !m_state.available || !m_state.active ||
+ !(m_state.capabilities & AUDIO_CAP_CAPTURE) ||
+ !m_state.capture.formatValid ||
+ !ValidFormat(m_state.capture.format) ||
+ m_state.capture.state != AudioStreamState::RUNNING ||
+ message.streamGeneration != m_state.capture.generation)
+ return false;
+
+ const bool silent =
+ (message.flags & KVMFR_AUDIO_MESSAGE_SILENT) != 0;
+ const uint32_t bytes =
+ message.frames * m_state.capture.format.blockAlign;
+ if (bytes > KVMFR_AUDIO_PCM_BYTES ||
+ (silent ? message.length != 0 : message.length != bytes))
+ return false;
+
+ AudioPacket packet;
+ packet.endpointGeneration = m_state.generation;
+ packet.streamGeneration = message.streamGeneration;
+ if (message.flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY)
+ packet.flags |= AUDIO_PACKET_DISCONTINUITY;
+ if (message.flags & KVMFR_AUDIO_MESSAGE_SILENT)
+ packet.flags |= AUDIO_PACKET_SILENT;
+ if (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID)
+ packet.flags |= AUDIO_PACKET_CLOCK_VALID;
+ if (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE)
+ packet.flags |= AUDIO_PACKET_CLOCK_STABLE;
+ if (m_captureDiscontinuity ||
+ (m_clientSequenceKnown &&
+ message.sequence != m_nextClientSequence) ||
+ (!m_clientSequenceKnown && message.sequence != 0))
+ packet.flags |= AUDIO_PACKET_DISCONTINUITY;
+ packet.frames = message.frames;
+ packet.position = message.position;
+ packet.timeNs = message.timeNs;
+ packet.rateQ32 = message.rateQ32;
+ packet.format = m_state.capture.format;
+
+ m_pendingCapture.valid = true;
+ m_pendingCapture.buffer = buffer;
+ m_pendingCapture.packet = packet;
+ m_pendingCapture.data = silent ? nullptr :
+ static_cast(buffer.data) +
+ sizeof(KVMFRAudioMessage);
+ m_pendingCapture.size = message.length;
+ m_pendingCapture.readySerial = m_captureReadySerial;
+ m_nextClientSequence = message.sequence + 1;
+ m_clientSequenceKnown = true;
+ return true;
+}
+
+void CLGMPAudioTransport::DrainInbound()
+{
+ for (unsigned count = 0; count < INBOUND_DRAIN_MAX; ++count)
+ {
+ CSRWSharedLock callbackLock(m_callbackLock);
+ IAudioTarget * target = nullptr;
+ AudioClockFeedback feedback;
+ bool callCapture = false;
+ bool callClock = false;
+ uint64_t ownerSerial = 0;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_ownerState != OwnerState::BOUND ||
+ !m_responseAdmitted || m_failed)
+ return;
+
+ target = m_started ? m_target : nullptr;
+ ownerSerial = m_ownerSerial;
+ if (m_pendingCapture.valid)
+ {
+ if (!PendingCaptureCurrent())
+ {
+ ReleasePendingCapture();
+ m_captureDiscontinuity = true;
+ continue;
+ }
+ if (m_pendingCapture.readySerial == m_captureReadySerial)
+ return;
+ m_pendingCapture.readySerial = m_captureReadySerial;
+ callCapture = true;
+ }
+ else
+ {
+ LGMPStreamBuffer buffer = {};
+ const LGMP_STATUS status = lgmpHostStreamReadPeek(
+ m_clientToHostStream, &buffer);
+ if (status == LGMP_ERR_STREAM_EMPTY)
+ return;
+ if (status == LGMP_ERR_STREAM_UNBOUND ||
+ status == LGMP_ERR_STREAM_STALE)
+ {
+ BeginRelease("client-to-host audio stream binding lost");
+ return;
+ }
+ if (status != LGMP_OK)
+ {
+ Fail("lgmpHostStreamReadPeek(audio)", status);
+ return;
+ }
+
+ KVMFRAudioMessage message = {};
+ if (!ParseInbound(buffer, message))
+ {
+ const LGMP_STATUS release = lgmpHostStreamReadRelease(
+ m_clientToHostStream, &buffer);
+ if (release != LGMP_OK)
+ Fail("lgmpHostStreamReadRelease(audio invalid)", release);
+ BeginRelease("malformed client-to-host audio record");
+ return;
+ }
+
+ const bool sequenceGap =
+ (m_clientSequenceKnown &&
+ message.sequence != m_nextClientSequence) ||
+ (!m_clientSequenceKnown && message.sequence != 0);
+ const auto consume = [&]() -> bool
+ {
+ const LGMP_STATUS release = lgmpHostStreamReadRelease(
+ m_clientToHostStream, &buffer);
+ if (release != LGMP_OK)
+ {
+ Fail("lgmpHostStreamReadRelease(audio)", release);
+ return false;
+ }
+ m_nextClientSequence = message.sequence + 1;
+ m_clientSequenceKnown = true;
+ return true;
+ };
+
+ if (!m_clientReady &&
+ message.type != KVMFR_AUDIO_MESSAGE_KEEPALIVE &&
+ message.type != KVMFR_AUDIO_MESSAGE_RELEASE)
+ {
+ if (!consume())
+ return;
+ BeginRelease("audio data received before client readiness");
+ return;
+ }
+
+ switch (message.type)
+ {
+ case KVMFR_AUDIO_MESSAGE_CAPTURE_DATA:
+ {
+ const bool clockStable =
+ (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) != 0;
+ const bool clockValid =
+ (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0;
+ if (!(m_clientCapabilities & KVMFR_AUDIO_CAP_CAPTURE) ||
+ (message.flags & ~WIRE_PACKET_FLAG_MASK) ||
+ (clockStable && !clockValid) ||
+ (!clockValid && (message.position || message.timeNs ||
+ message.rateQ32)) || !message.streamGeneration ||
+ !message.frames ||
+ message.frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET)
+ {
+ consume();
+ BeginRelease("invalid capture audio record");
+ return;
+ }
+ RenewLease();
+
+ const bool captureCurrent = m_stateKnown &&
+ m_state.available && m_state.active &&
+ (m_state.capabilities & AUDIO_CAP_CAPTURE) &&
+ m_state.capture.formatValid &&
+ ValidFormat(m_state.capture.format) &&
+ m_state.capture.state == AudioStreamState::RUNNING &&
+ message.streamGeneration ==
+ m_state.capture.generation;
+ if (captureCurrent)
+ {
+ const bool silent = (message.flags &
+ KVMFR_AUDIO_MESSAGE_SILENT) != 0;
+ const uint32_t bytes = message.frames *
+ m_state.capture.format.blockAlign;
+ if (bytes > KVMFR_AUDIO_PCM_BYTES ||
+ (silent ? message.length != 0 :
+ message.length != bytes))
+ {
+ consume();
+ BeginRelease("invalid capture audio payload");
+ return;
+ }
+ }
+
+ if (!PrepareCapture(message, buffer))
+ {
+ consume();
+ m_captureDiscontinuity = true;
+ continue;
+ }
+ if (sequenceGap)
+ m_pendingCapture.packet.flags |=
+ AUDIO_PACKET_DISCONTINUITY;
+ callCapture = true;
+ break;
+ }
+
+ case KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK:
+ {
+ const uint32_t validFlags =
+ KVMFR_AUDIO_MESSAGE_CLOCK_VALID |
+ KVMFR_AUDIO_MESSAGE_CLOCK_STABLE |
+ KVMFR_AUDIO_MESSAGE_DISCONTINUITY;
+ KVMFRAudioClockFeedback wire = {};
+ if (message.length == sizeof(wire))
+ memcpy(&wire, static_cast(buffer.data) +
+ sizeof(message), sizeof(wire));
+ const bool valid =
+ (m_clientCapabilities &
+ KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) &&
+ message.flags && !(message.flags & ~validFlags) &&
+ (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) &&
+ (!(message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) ||
+ (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID)) &&
+ message.streamGeneration && !message.frames &&
+ message.length == sizeof(wire) && !wire.reserved &&
+ wire.targetRateQ32;
+ if (!valid)
+ {
+ consume();
+ BeginRelease("invalid audio clock feedback");
+ return;
+ }
+
+ const bool current = m_stateKnown && m_state.available &&
+ m_state.active && (m_state.capabilities &
+ AUDIO_CAP_CLOCK_FEEDBACK) &&
+ message.streamGeneration ==
+ m_state.playback.generation;
+ if (!consume())
+ return;
+ RenewLease();
+ if (sequenceGap)
+ m_captureDiscontinuity = true;
+ if (!current)
+ continue;
+ feedback.endpointGeneration = m_state.generation;
+ feedback.playbackGeneration = message.streamGeneration;
+ feedback.position = message.position;
+ feedback.timeNs = message.timeNs;
+ feedback.rateQ32 = message.rateQ32;
+ feedback.targetRateQ32 = wire.targetRateQ32;
+ feedback.flags = AUDIO_CLOCK_VALID;
+ if (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE)
+ feedback.flags |= AUDIO_CLOCK_STABLE;
+ if (sequenceGap ||
+ (message.flags &
+ KVMFR_AUDIO_MESSAGE_DISCONTINUITY))
+ feedback.flags |= AUDIO_CLOCK_DISCONTINUITY;
+ callClock = true;
+ break;
+ }
+
+ case KVMFR_AUDIO_MESSAGE_KEEPALIVE:
+ case KVMFR_AUDIO_MESSAGE_RELEASE:
+ {
+ const bool empty = !message.streamGeneration &&
+ !message.flags && !message.position && !message.timeNs &&
+ !message.rateQ32 && !message.frames && !message.length;
+ if (!consume())
+ return;
+ if (!empty)
+ {
+ BeginRelease("invalid audio owner control record");
+ return;
+ }
+ if (message.type == KVMFR_AUDIO_MESSAGE_RELEASE)
+ {
+ BeginRelease("client audio release");
+ return;
+ }
+ if (!m_clientReady)
+ {
+ if (sequenceGap)
+ {
+ BeginRelease("invalid initial audio keepalive sequence");
+ return;
+ }
+ m_clientReady = true;
+ DEBUG_INFO("Audio owner %u generation %u is ready",
+ m_ownerClientID, m_ownerGeneration);
+ }
+ if (sequenceGap)
+ m_captureDiscontinuity = true;
+ RenewLease();
+ continue;
+ }
+
+ default:
+ consume();
+ BeginRelease("unsupported client-to-host audio record");
+ return;
+ }
+ }
+ }
+
+ AudioResult result = AudioResult::UNAVAILABLE;
+ if (target && callCapture)
+ result = target->SendCapture(m_pendingCapture.packet,
+ m_pendingCapture.data, m_pendingCapture.size);
+ else if (target && callClock)
+ result = target->SetClockFeedback(feedback);
+
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (ownerSerial != m_ownerSerial ||
+ m_ownerState != OwnerState::BOUND)
+ return;
+ if (callClock)
+ {
+ if (result == AudioResult::FAILED)
+ BeginRelease("audio clock target failed");
+ continue;
+ }
+ if (!callCapture || !m_pendingCapture.valid)
+ continue;
+ if (!PendingCaptureCurrent())
+ {
+ ReleasePendingCapture();
+ m_captureDiscontinuity = true;
+ continue;
+ }
+ if (result == AudioResult::BUSY)
+ return;
+
+ const LGMP_STATUS release = lgmpHostStreamReadRelease(
+ m_clientToHostStream, &m_pendingCapture.buffer);
+ if (release != LGMP_OK)
+ {
+ m_pendingCapture = {};
+ Fail("lgmpHostStreamReadRelease(audio capture)", release);
+ return;
+ }
+ m_pendingCapture = {};
+ m_captureDiscontinuity = result != AudioResult::ACCEPTED;
+ if (result == AudioResult::FAILED ||
+ result == AudioResult::REJECTED)
+ {
+ BeginRelease("audio capture target failed");
+ return;
+ }
+ }
+ }
+}
+
+void CLGMPAudioTransport::ProcessActivation()
+{
+ CSRWSharedLock callbackLock(m_callbackLock);
+ IAudioTarget * target = nullptr;
+ bool activate = false;
+ uint64_t ownerSerial = 0;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (!m_started || !m_target || m_failed ||
+ m_ownerState == OwnerState::NONE)
+ return;
+ if (m_ownerState == OwnerState::BOUND &&
+ m_responseAdmitted && m_clientReady &&
+ !m_activationIssued)
+ activate = true;
+ else if (m_ownerState != OwnerState::RELEASING ||
+ !m_activationIssued || m_deactivationIssued)
+ return;
+ target = m_target;
+ ownerSerial = m_ownerSerial;
+ }
+
+ const AudioResult result = target->SetAudioActive(activate);
+ CSRWExclusiveLock lock(m_lock);
+ if (ownerSerial != m_ownerSerial ||
+ m_ownerState == OwnerState::NONE)
+ return;
+ if (activate)
+ {
+ if (m_ownerState != OwnerState::BOUND || m_activationIssued)
+ return;
+ if (result == AudioResult::ACCEPTED)
+ m_activationIssued = true;
+ else if (result == AudioResult::FAILED ||
+ result == AudioResult::REJECTED)
+ BeginRelease("audio endpoint activation failed");
+ }
+ else if (m_ownerState == OwnerState::RELEASING &&
+ (result == AudioResult::ACCEPTED ||
+ result == AudioResult::UNAVAILABLE ||
+ result == AudioResult::STALE))
+ m_deactivationIssued = true;
+}
+
+void CLGMPAudioTransport::NotifyPlaybackReady()
+{
+ CSRWSharedLock callbackLock(m_callbackLock);
+ IAudioTarget * target = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_playbackReadyPending)
+ return;
+ m_playbackReadyPending = false;
+ if (m_started)
+ target = m_target;
+ }
+ if (target)
+ target->PlaybackReceiveReady();
+}
+
+void CLGMPAudioTransport::NotifyFailure()
+{
+ CSRWSharedLock callbackLock(m_callbackLock);
+ IAudioTarget * target = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_failed || m_failureNotified)
+ return;
+ m_failureNotified = true;
+ if (m_started)
+ target = m_target;
+ }
+ if (target)
+ target->AudioFailed();
+}
+
+void CLGMPAudioTransport::Fail(
+ const char * operation, LGMP_STATUS status)
+{
+ if (!m_failed)
+ DEBUG_ERROR("%s Failed (Audio): %s",
+ operation, lgmpStatusString(status));
+ ClearBlockedPlayback(true);
+ m_failed = true;
+}
+
+void CLGMPAudioTransport::Process()
+{
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_initialized)
+ return;
+ PumpResponses();
+ }
+
+ DrainInbound();
+
+ {
+ CSRWExclusiveLock lock(m_lock);
+ MaintainOwner(GetTickCount64());
+ PumpResponses();
+ PumpState();
+ CheckPlaybackCredit();
+ }
+ ProcessActivation();
+ NotifyPlaybackReady();
+ NotifyFailure();
+}
+
+bool CLGMPAudioTransport::Start(IAudioTarget& target)
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_initialized || m_failed)
+ return false;
+ if (m_started)
+ return m_target == ⌖
+ m_target = ⌖
+ m_started = true;
+ m_stateKnown = false;
+ m_failureNotified = false;
+ return true;
+}
+
+void CLGMPAudioTransport::Stop()
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_started && !m_target)
+ return;
+ if (m_ownerState != OwnerState::NONE)
+ BeginRelease("audio source stopped");
+ else
+ ClearBlockedPlayback(false);
+ m_started = false;
+ m_target = nullptr;
+ m_playbackReadyPending = false;
+}
+
+void CLGMPAudioTransport::TransportStop()
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ CSRWExclusiveLock lock(m_lock);
+ m_started = false;
+ m_target = nullptr;
+ m_playbackReadyPending = false;
+ ClearBlockedPlayback(false);
+ ReleasePendingCapture();
+ ForceUnbindStreams();
+ ResetOwner();
+ for (PendingResponse& response : m_pendingResponse)
+ response = {};
+ for (ClaimRecord& record : m_claimRecord)
+ record = {};
+}
+
+void CLGMPAudioTransport::AudioState(
+ const AudioEndpointStatus& state)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_started)
+ return;
+
+ if (m_playbackBlocked)
+ {
+ const bool playbackCurrent = state.available && state.active &&
+ (m_clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) &&
+ (state.capabilities & AUDIO_CAP_PLAYBACK) &&
+ state.playback.formatValid &&
+ ValidFormat(state.playback.format) &&
+ state.playback.state == AudioStreamState::RUNNING &&
+ state.generation == m_blockedPlayback.endpointGeneration &&
+ state.playback.generation ==
+ m_blockedPlayback.streamGeneration &&
+ SameFormat(state.playback.format, m_blockedPlayback.format);
+ if (!playbackCurrent)
+ ClearBlockedPlayback(true);
+ }
+
+ m_state = state;
+ m_stateKnown = true;
+ if (m_ownerState == OwnerState::BOUND && m_responseAdmitted)
+ m_statusDirty = true;
+}
+
+AudioResult CLGMPAudioTransport::SendPlayback(
+ const AudioPacket& packet, const uint8_t * data, size_t size)
+{
+ if ((packet.flags & ~AUDIO_PACKET_FLAG_MASK) ||
+ ((packet.flags & AUDIO_PACKET_CLOCK_STABLE) &&
+ !(packet.flags & AUDIO_PACKET_CLOCK_VALID)) ||
+ !packet.frames ||
+ packet.frames > AUDIO_MAX_FRAMES_PER_PACKET ||
+ !ValidFormat(packet.format))
+ return AudioResult::REJECTED;
+
+ const size_t pcmBytes = static_cast(packet.frames) *
+ packet.format.blockAlign;
+ const bool silent = (packet.flags & AUDIO_PACKET_SILENT) != 0;
+ if (pcmBytes > AUDIO_MAX_PCM_BYTES ||
+ (silent ? size != 0 : (size != pcmBytes || !data)))
+ return AudioResult::REJECTED;
+
+ CSRWExclusiveLock lock(m_lock);
+ if (m_playbackBlocked &&
+ (size != m_blockedPlaybackSize ||
+ !SamePacket(packet, m_blockedPlayback)))
+ {
+ DEBUG_WARN("Audio playback retry did not match the retained packet");
+ ClearBlockedPlayback(false);
+ return AudioResult::REJECTED;
+ }
+ if (!m_started)
+ {
+ ClearBlockedPlayback(false);
+ return AudioResult::UNAVAILABLE;
+ }
+ if (m_failed)
+ {
+ ClearBlockedPlayback(false);
+ return AudioResult::FAILED;
+ }
+ if (m_ownerState != OwnerState::BOUND ||
+ !(m_clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK))
+ {
+ ClearBlockedPlayback(false);
+ return AudioResult::ACCEPTED;
+ }
+ if (!m_responseAdmitted || !m_clientReady || !m_stateKnown)
+ {
+ if (!RememberBlockedPlayback(packet, size))
+ return AudioResult::REJECTED;
+ return AudioResult::BUSY;
+ }
+ if (!m_state.available || !m_state.active)
+ {
+ ClearBlockedPlayback(false);
+ return AudioResult::UNAVAILABLE;
+ }
+ if (packet.endpointGeneration != m_state.generation ||
+ packet.streamGeneration != m_state.playback.generation)
+ {
+ ClearBlockedPlayback(false);
+ return AudioResult::STALE;
+ }
+ if (!(m_state.capabilities & AUDIO_CAP_PLAYBACK) ||
+ !m_state.playback.formatValid ||
+ m_state.playback.state != AudioStreamState::RUNNING ||
+ !SameFormat(packet.format, m_state.playback.format))
+ {
+ ClearBlockedPlayback(false);
+ return AudioResult::UNAVAILABLE;
+ }
+
+ KVMFRAudioMessageFlags flags = 0;
+ if (packet.flags & AUDIO_PACKET_DISCONTINUITY)
+ flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY;
+ if (packet.flags & AUDIO_PACKET_SILENT)
+ flags |= KVMFR_AUDIO_MESSAGE_SILENT;
+ if (packet.flags & AUDIO_PACKET_CLOCK_VALID)
+ flags |= KVMFR_AUDIO_MESSAGE_CLOCK_VALID;
+ if (packet.flags & AUDIO_PACKET_CLOCK_STABLE)
+ flags |= KVMFR_AUDIO_MESSAGE_CLOCK_STABLE;
+
+ if (m_playbackCreditValid)
+ {
+ LGMPStreamBuffer buffer = m_playbackCredit;
+ m_playbackCredit = {};
+ m_playbackCreditValid = false;
+ const bool committed = CommitMessage(buffer,
+ KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, packet.streamGeneration,
+ flags, packet.position, packet.timeNs, packet.rateQ32,
+ packet.frames, data, static_cast(size));
+ ClearBlockedPlayback(false);
+ if (committed)
+ return AudioResult::ACCEPTED;
+ if (m_failed)
+ return AudioResult::FAILED;
+ return m_ownerState == OwnerState::BOUND ?
+ AudioResult::BUSY : AudioResult::ACCEPTED;
+ }
+
+ PumpState();
+ if (m_failed)
+ return AudioResult::FAILED;
+ if (m_ownerState != OwnerState::BOUND)
+ return AudioResult::ACCEPTED;
+ if (m_statusDirty ||
+ m_announcedPlayback != packet.streamGeneration)
+ {
+ if (!RememberBlockedPlayback(packet, size))
+ return AudioResult::REJECTED;
+ return AudioResult::BUSY;
+ }
+
+ if (!WriteMessage(KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA,
+ packet.streamGeneration, flags, packet.position, packet.timeNs,
+ packet.rateQ32, packet.frames, data, static_cast(size)))
+ {
+ if (m_failed)
+ return AudioResult::FAILED;
+ if (m_ownerState != OwnerState::BOUND)
+ return AudioResult::ACCEPTED;
+ if (!RememberBlockedPlayback(packet, size))
+ return AudioResult::REJECTED;
+ return AudioResult::BUSY;
+ }
+ ClearBlockedPlayback(false);
+ return AudioResult::ACCEPTED;
+}
+
+void CLGMPAudioTransport::CaptureReceiveReady()
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (m_started)
+ m_captureReadySerial = NextSerial(m_captureReadySerial);
+}
diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h
new file mode 100644
index 00000000..5462aa4a
--- /dev/null
+++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h
@@ -0,0 +1,230 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include "CSRWLock.h"
+#include "common/KVMFRAudio.h"
+#include "transport/IAudioSource.h"
+
+#include
+
+#include
+
+#include
+#include
+
+extern "C" {
+ #include "lgmp/host.h"
+ #include "lgmp/stream.h"
+}
+
+class CLGMPControl;
+class CLGMPHost;
+class CLGMPTransport;
+
+class CLGMPAudioTransport final : public IAudioSource
+{
+private:
+ static constexpr unsigned RESPONSE_COUNT = LGMP_Q_POINTER_LEN;
+ static constexpr unsigned CLAIM_HISTORY_COUNT = 1024;
+ static constexpr unsigned INBOUND_DRAIN_MAX =
+ KVMFR_AUDIO_STREAM_SLOT_COUNT * 2U;
+
+ enum class OwnerState
+ {
+ NONE,
+ BOUND,
+ RELEASING,
+ };
+
+ struct PendingResponse
+ {
+ bool valid = false;
+ uint32_t clientID = 0;
+ KVMFRAudioStreamResponse response = {};
+ };
+
+ struct ResponseMemory
+ {
+ PLGMPMemory memory = nullptr;
+ bool valid = false;
+ uint32_t clientID = 0;
+ uint32_t requestGeneration = 0;
+ uint64_t claimToken = 0;
+ KVMFRAudioResponseResult result = 0;
+ };
+
+ struct ClaimRecord
+ {
+ bool valid = false;
+ bool closed = false;
+ uint32_t clientID = 0;
+ uint32_t requestGeneration = 0;
+ uint64_t claimToken = 0;
+ KVMFRAudioStreamResponse response = {};
+ };
+
+ struct PendingCapture
+ {
+ bool valid = false;
+ LGMPStreamBuffer buffer = {};
+ AudioPacket packet = {};
+ const uint8_t * data = nullptr;
+ size_t size = 0;
+ uint64_t readySerial = 0;
+ };
+
+ CLGMPHost& m_host;
+ CLGMPControl& m_control;
+
+ PLGMPHostStream m_hostToClientStream = nullptr;
+ PLGMPHostStream m_clientToHostStream = nullptr;
+ KVMFRStreamDescriptor m_hostToClientDescriptor = {};
+ KVMFRStreamDescriptor m_clientToHostDescriptor = {};
+ ResponseMemory m_responseMemory[RESPONSE_COUNT];
+ PendingResponse m_pendingResponse[LGMP_MAX_CLIENTS];
+ ClaimRecord m_claimRecord[CLAIM_HISTORY_COUNT];
+
+ CSRWLock m_lifecycleLock;
+ CSRWLock m_callbackLock;
+ CSRWLock m_lock;
+ IAudioTarget * m_target = nullptr;
+ bool m_initialized = false;
+ bool m_started = false;
+ bool m_failed = false;
+ bool m_failureNotified = false;
+
+ OwnerState m_ownerState = OwnerState::NONE;
+ uint32_t m_ownerClientID = 0;
+ uint32_t m_ownerGeneration = 0;
+ uint32_t m_clientCapabilities = 0;
+ uint64_t m_ownerToken = 0;
+ uint64_t m_ownerSerial = 0;
+ uint64_t m_ownerDeadline = 0;
+ uint64_t m_releaseGeneration = 0;
+ bool m_responseAdmitted = false;
+ bool m_clientReady = false;
+ bool m_activationIssued = false;
+ bool m_deactivationIssued = false;
+ KVMFRAudioStreamResponse m_ownerResponse = {};
+
+ AudioEndpointStatus m_state;
+ bool m_stateKnown = false;
+ bool m_statusDirty = false;
+ uint64_t m_announcedPlayback = 0;
+ uint64_t m_announcedCapture = 0;
+ uint64_t m_hostSequence = 0;
+ uint64_t m_nextClientSequence = 0;
+ bool m_clientSequenceKnown = false;
+ bool m_captureDiscontinuity = false;
+ bool m_playbackBlocked = false;
+ bool m_playbackReadyPending = false;
+ bool m_playbackCreditValid = false;
+ LGMPStreamBuffer m_playbackCredit = {};
+ AudioPacket m_blockedPlayback = {};
+ size_t m_blockedPlaybackSize = 0;
+ uint64_t m_captureReadySerial = 0;
+ PendingCapture m_pendingCapture;
+
+ bool InitializeStreams();
+ void FreeStreams();
+ bool BindStreams(uint32_t clientID);
+ bool TryUnbindStreams();
+ void ForceUnbindStreams();
+
+ static bool SameFormat(const AudioFormat& left,
+ const AudioFormat& right);
+ static bool SamePacket(const AudioPacket& left,
+ const AudioPacket& right);
+ static bool ValidFormat(const AudioFormat& format);
+ static KVMFRAudioFormat WireFormat(const AudioFormat& format);
+ static KVMFRAudioStreamState WireState(AudioStreamState state);
+ static KVMFRStreamDescriptor WireDescriptor(
+ const LGMPStreamDescriptor& descriptor);
+
+ bool ValidateClaim(const KVMFRAudioClaim& claim) const;
+ bool ResponsePending(uint32_t clientID,
+ const KVMFRAudioStreamResponse& response) const;
+ bool QueueResponse(uint32_t clientID,
+ const KVMFRAudioStreamResponse& response);
+ void CleanupResponseMemory();
+ void PumpResponses();
+
+ bool OwnerSubscribed() const;
+ void RenewLease();
+ void BeginRelease(const char * reason);
+ void ReleasePendingCapture();
+ void CancelPlaybackCredit();
+ void ClearBlockedPlayback(bool notify);
+ bool RememberBlockedPlayback(const AudioPacket& packet, size_t size);
+ void MaintainOwner(uint64_t now);
+ void ResetOwner();
+
+ KVMFRAudioStatus WireStatus() const;
+ bool CommitMessage(LGMPStreamBuffer& buffer,
+ KVMFRAudioMessageType type, uint64_t streamGeneration,
+ KVMFRAudioMessageFlags flags, uint64_t position, int64_t timeNs,
+ uint64_t rateQ32, uint32_t frames, const void * payload,
+ uint32_t length);
+ bool WriteMessage(KVMFRAudioMessageType type,
+ uint64_t streamGeneration, KVMFRAudioMessageFlags flags,
+ uint64_t position, int64_t timeNs, uint64_t rateQ32,
+ uint32_t frames, const void * payload, uint32_t length);
+ void PumpState();
+ void CheckPlaybackCredit();
+
+ bool ParseInbound(const LGMPStreamBuffer& buffer,
+ KVMFRAudioMessage& message) const;
+ bool PendingCaptureCurrent() const;
+ bool PrepareCapture(const KVMFRAudioMessage& message,
+ const LGMPStreamBuffer& buffer);
+ void DrainInbound();
+ void ProcessActivation();
+ void NotifyPlaybackReady();
+ void NotifyFailure();
+ void Fail(const char * operation, LGMP_STATUS status);
+
+ void InitializeOwner(uint32_t clientID,
+ const KVMFRAudioClaim& claim);
+ void HandleClaim(uint32_t clientID,
+ const KVMFRAudioClaim& claim);
+ bool Initialize();
+ void Process();
+ void TransportStop();
+
+ friend class CLGMPTransport;
+
+public:
+ CLGMPAudioTransport(CLGMPHost& host, CLGMPControl& control) :
+ m_host(host), m_control(control) {}
+ ~CLGMPAudioTransport() override;
+
+ CLGMPAudioTransport(const CLGMPAudioTransport&) = delete;
+ CLGMPAudioTransport& operator=(
+ const CLGMPAudioTransport&) = delete;
+
+ bool Start(IAudioTarget& target) override;
+ void Stop() override;
+ void AudioState(const AudioEndpointStatus& state) override;
+ AudioResult SendPlayback(const AudioPacket& packet,
+ const uint8_t * data, size_t size) override;
+ void CaptureReceiveReady() override;
+};
diff --git a/idd/LGIdd/transport/lgmp/CLGMPControl.cpp b/idd/LGIdd/transport/lgmp/CLGMPControl.cpp
index 2a5bcc1f..65bf8939 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPControl.cpp
+++ b/idd/LGIdd/transport/lgmp/CLGMPControl.cpp
@@ -128,6 +128,31 @@ LGMP_STATUS CLGMPControl::AckData()
return lgmpHostAckData(m_pointerQueue);
}
+LGMP_STATUS CLGMPControl::PostForClient(uint64_t udata,
+ PLGMPMemory memory, uint32_t clientID, unsigned& recipients)
+{
+ recipients = 0;
+ if (!m_pointerQueue || !memory || !clientID)
+ return LGMP_ERR_INVALID_ARGUMENT;
+ return lgmpHostQueuePostForClients(m_pointerQueue, udata, memory,
+ &clientID, 1, &recipients);
+}
+
+bool CLGMPControl::PayloadPending(PLGMPMemory memory) const
+{
+ return m_pointerQueue && memory &&
+ lgmpHostQueuePayloadPending(m_pointerQueue, memory);
+}
+
+LGMP_STATUS CLGMPControl::GetClientIDs(
+ uint32_t clientIDs[LGMP_MAX_CLIENTS], unsigned& count) const
+{
+ count = 0;
+ if (!m_pointerQueue || !clientIDs)
+ return LGMP_ERR_INVALID_ARGUMENT;
+ return lgmpHostGetClientIDs(m_pointerQueue, clientIDs, &count);
+}
+
bool CLGMPControl::HasNewSubscribers()
{
return lgmpHostQueueNewSubs(m_pointerQueue) != 0;
diff --git a/idd/LGIdd/transport/lgmp/CLGMPControl.h b/idd/LGIdd/transport/lgmp/CLGMPControl.h
index d96a0c1b..80163791 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPControl.h
+++ b/idd/LGIdd/transport/lgmp/CLGMPControl.h
@@ -34,11 +34,13 @@
#include
class CLGMPTransport;
+class CLGMPAudioTransport;
class CLGMPControl final : public IControlSink
{
private:
friend class CLGMPTransport;
+ friend class CLGMPAudioTransport;
static constexpr int POINTER_SHAPE_BUFFERS = 3;
static constexpr int COLOR_TRANSFORM_BUFFERS = 3;
@@ -66,6 +68,11 @@ private:
LGMP_STATUS ReadDataWithSource(void * data, size_t * size,
uint32_t * sourceClientID);
LGMP_STATUS AckData();
+ LGMP_STATUS PostForClient(uint64_t udata, PLGMPMemory memory,
+ uint32_t clientID, unsigned& recipients);
+ bool PayloadPending(PLGMPMemory memory) const;
+ LGMP_STATUS GetClientIDs(
+ uint32_t clientIDs[LGMP_MAX_CLIENTS], unsigned& count) const;
bool HasNewSubscribers();
void RequestReplay();
diff --git a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp
index e4f7e971..fed79f65 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp
+++ b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp
@@ -53,7 +53,8 @@ bool CLGMPHost::Initialize(CIVSHMEM& ivshmem)
KVMFR_FEATURE_WINDOWSIZE |
KVMFR_FEATURE_FRAME_SCHEDULE |
KVMFR_FEATURE_INPUT |
- KVMFR_FEATURE_CLIPBOARD;
+ KVMFR_FEATURE_CLIPBOARD |
+ KVMFR_FEATURE_AUDIO;
strncpy_s(kvmfr.hostver, LG_VERSION_STR, sizeof(kvmfr.hostver) - 1);
ss.write(reinterpret_cast(&kvmfr), sizeof(kvmfr));
}
diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp
index 00accb3f..3b62b855 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp
+++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp
@@ -25,6 +25,8 @@
#include
#include
+#include
+
static bool TranslateFrameScheduleFlags(
uint32_t source, uint32_t& destination)
{
@@ -66,6 +68,7 @@ static const FrameProfile FRAME_PROFILES[] =
CLGMPTransport::CLGMPTransport(const TransportInstance& config) :
m_config(config),
m_control(m_host),
+ m_audio(m_host, m_control),
m_frames(m_host, m_ivshmem),
m_input(m_host),
m_clipboard(m_host)
@@ -91,12 +94,16 @@ bool CLGMPTransport::Initialize()
if (!m_host.Initialize(m_ivshmem))
return false;
- // Preserve the existing shared-memory layout by appending input and then
- // clipboard after the frame/pointer queues and retained pointer state.
+ // Preserve the existing shared-memory layout by appending input,
+ // clipboard, and optional audio after the frame/pointer queues and
+ // retained pointer state.
if (!m_frames.Initialize() || !m_control.Initialize() ||
!m_input.Initialize() || !m_clipboard.Initialize())
return false;
+ if (!m_audio.Initialize())
+ DEBUG_WARN("LGMP audio transport is unavailable");
+
m_frames.SealMemoryLayout();
return true;
}
@@ -122,12 +129,6 @@ ITransport::ProcessResult CLGMPTransport::Process(ITransportEvents& events)
recovery.active, admission.state, admission.error);
}
- // Before the swap chain establishes the frame-buffer alignment, service
- // only the protocol-independent recovery channel. This preserves the old
- // transport startup boundary while keeping recovery available immediately.
- if (!Atomic::Load(m_ready, std::memory_order_acquire))
- return ProcessResult::OK;
-
const LGMP_STATUS processStatus = m_host.Process();
if (processStatus != LGMP_OK)
{
@@ -143,14 +144,16 @@ ITransport::ProcessResult CLGMPTransport::Process(ITransportEvents& events)
return ProcessResult::FAILURE;
}
- m_input.FlushStatus();
-
+ const bool ready = Atomic::Load(m_ready, std::memory_order_acquire);
const uint64_t now = CFrameScheduler::Nanotime();
// Take the frame subscriber snapshot before processing scheduling messages,
// then publish both updates together just as the original timer did.
- const CLGMPFrameTransport::SubscriberSnapshot subscribers =
- m_frames.SnapshotSubscribers();
+ CLGMPFrameTransport::SubscriberSnapshot subscribers;
+ if (ready)
+ subscribers = m_frames.SnapshotSubscribers();
+
+ m_audio.Process();
uint8_t data[LGMP_MSGS_SIZE];
size_t size;
@@ -225,12 +228,35 @@ ITransport::ProcessResult CLGMPTransport::Process(ITransportEvents& events)
DEBUG_WARN("Ignoring invalid KVMFR frame schedule");
break;
}
+
+ case KVMFR_MESSAGE_AUDIO_CLAIM:
+ {
+ KVMFRAudioClaim claim = {};
+ const bool valid = size == sizeof(claim);
+ if (valid)
+ memcpy(&claim, data, sizeof(claim));
+ else
+ DEBUG_WARN("Ignoring invalid KVMFR audio claim");
+
+ // Do not let stream binding or a full response queue hold the
+ // shared pointer/control receive head.
+ m_control.AckData();
+ if (valid)
+ m_audio.HandleClaim(sourceClientID, claim);
+ continue;
+ }
}
m_control.AckData();
}
- m_frames.FinalizeSubscribers(subscribers, now);
+ m_audio.Process();
+
+ if (ready)
+ {
+ m_input.FlushStatus();
+ m_frames.FinalizeSubscribers(subscribers, now);
+ }
if (m_control.HasNewSubscribers())
m_control.RequestReplay();
@@ -245,6 +271,7 @@ void CLGMPTransport::Stop()
m_hasActive = false;
m_clipboard.Stop();
m_input.Stop();
+ m_audio.TransportStop();
}
void CLGMPTransport::SyncRecovery()
diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.h b/idd/LGIdd/transport/lgmp/CLGMPTransport.h
index 3b392446..ac75e44f 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPTransport.h
+++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.h
@@ -23,6 +23,7 @@
#include "Atomic.h"
#include "transport/ITransport.h"
#include "transport/lgmp/CIVSHMEM.h"
+#include "transport/lgmp/CLGMPAudioTransport.h"
#include "transport/lgmp/CLGMPClipboardTransport.h"
#include "transport/lgmp/CLGMPControl.h"
#include "transport/lgmp/CLGMPFrameTransport.h"
@@ -39,6 +40,7 @@ private:
CIVSHMEM m_ivshmem;
CLGMPHost m_host;
CLGMPControl m_control;
+ CLGMPAudioTransport m_audio;
CLGMPFrameTransport m_frames;
CLGMPInputTransport m_input;
CLGMPClipboardTransport m_clipboard;
@@ -76,6 +78,7 @@ public:
DirectFrameBufferMemory GetDirectMemory() const override;
IFrameSink * FrameSink() override { return &m_frames; }
+ IAudioSource * Audio() override { return &m_audio; }
IControlSink * Control() override { return &m_control; }
IInputSource * Input() override { return &m_input; }
IClipboardSource * Clipboard() override { return &m_clipboard; }