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