[client/idd] audio: support multiple playback clients

Replace the single-owner PCM queues with one shared LGMP SPMC stream.
Give each admitted client reliable state and an independent playback
cursor, while clients without clock authority resample locally.

Extend the audio protocol with per-client epochs and barriers.
Keep capture exclusive and bound authority backpressure so slow
secondary readers cannot affect the disciplined low-latency path.
This commit is contained in:
Geoffrey McRae
2026-09-05 21:38:50 +10:00
parent 95234cea86
commit 36e170d93a
14 changed files with 3485 additions and 1833 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,9 @@
#pragma once
#include "CSRWLock.h"
#include "common/KVMFRAudio.h"
#include "transport/IAudioSource.h"
#include <LGProtocol/KVMFRAudio.h>
#include <LGProtocol/LGMPConfig.h>
#include <Windows.h>
@@ -33,6 +33,7 @@
extern "C" {
#include "lgmp/host.h"
#include "lgmp/spmc.h"
#include "lgmp/stream.h"
}
@@ -40,21 +41,40 @@ class CLGMPControl;
class CLGMPHost;
class CLGMPTransport;
class CLGMPAudioTransport final : public IAudioSource
class CLGMPAudio final : public IAudioSource
{
private:
static constexpr unsigned RESPONSE_COUNT = LGMP_Q_POINTER_LEN;
static constexpr unsigned CLAIM_HISTORY_COUNT = 1024;
static constexpr uint32_t INVALID_SLOT = UINT32_MAX;
static constexpr unsigned INBOUND_DRAIN_MAX =
KVMFR_AUDIO_STREAM_SLOT_COUNT * 2U;
static constexpr uint64_t AUTHORITY_STALL_MS = 20U;
static constexpr uint64_t CAPTURE_REVOKE_ACK_MS =
KVMFR_AUDIO_OWNER_LEASE_MS;
enum class OwnerState
enum class SubscriberState
{
NONE,
BOUND,
RELEASING,
};
enum class PumpStage
{
IDLE,
STATUS,
PLAYBACK_STOP,
CAPTURE_STOP,
PLAYBACK_START,
PLAYBACK_VOLUME,
PLAYBACK_MUTE,
CAPTURE_START,
CAPTURE_VOLUME,
CAPTURE_MUTE,
BARRIER,
};
struct PendingResponse
{
bool valid = false;
@@ -85,6 +105,8 @@ private:
struct PendingCapture
{
bool valid = false;
uint32_t subscriber = INVALID_SLOT;
uint64_t subscriberSerial = 0;
LGMPStreamBuffer buffer = {};
AudioPacket packet = {};
const uint8_t * data = nullptr;
@@ -92,13 +114,65 @@ private:
uint64_t readySerial = 0;
};
struct Subscriber
{
PLGMPHostStream hostControl = nullptr;
PLGMPHostStream clientControl = nullptr;
KVMFRStreamDescriptor hostControlDescriptor = {};
KVMFRStreamDescriptor clientControlDescriptor = {};
SubscriberState state = SubscriberState::NONE;
uint32_t clientID = 0;
uint32_t requestGeneration = 0;
uint32_t clientCapabilities = 0;
KVMFRAudioSubscriptionFlags subscriptions = 0;
KVMFRAudioCapabilities grantedCapabilities = 0;
uint32_t bindingEpoch = 0;
uint32_t roleEpoch = 0;
uint32_t playbackReaderID = 0;
uint32_t playbackReaderEpoch = 0;
uint32_t hostControlEpoch = 0;
uint32_t clientControlEpoch = 0;
uint32_t captureEpoch = 0;
uint64_t claimToken = 0;
uint64_t serial = 0;
uint64_t deadline = 0;
bool responseAdmitted = false;
bool subscriptionReceived = false;
bool ready = false;
bool awaitingAck = false;
bool forceUnbind = false;
bool feedbackRevoked = false;
bool captureRevoking = false;
uint64_t captureRevokeRevision = 0;
uint64_t captureRevokeDeadline = 0;
uint64_t hostSequence = 0;
uint64_t nextControlSequence = 0;
bool controlSequenceKnown = false;
uint64_t nextCaptureSequence = 0;
bool captureSequenceKnown = false;
bool captureDiscontinuity = false;
uint64_t announcedPlayback = 0;
uint64_t announcedCapture = 0;
uint64_t stateRevision = 0;
uint64_t pumpRevision = 0;
PumpStage pumpStage = PumpStage::IDLE;
KVMFRAudioStatus pendingStatus = {};
KVMFRAudioStateBarrier barrier = {};
uint64_t barrierGeneration = 0;
KVMFRAudioStreamResponse response = {};
};
CLGMPHost& m_host;
CLGMPControl& m_control;
PLGMPHostStream m_hostToClientStream = nullptr;
PLGMPHostStream m_clientToHostStream = nullptr;
KVMFRStreamDescriptor m_hostToClientDescriptor = {};
KVMFRStreamDescriptor m_clientToHostDescriptor = {};
PLGMPHostSPMC m_playbackStream = nullptr;
KVMFRSPMCDescriptor m_playbackDescriptor = {};
PLGMPHostStream m_captureStream = nullptr;
KVMFRStreamDescriptor m_captureDescriptor = {};
Subscriber m_subscribers[LGMP_MAX_CLIENTS];
ResponseMemory m_responseMemory[RESPONSE_COUNT];
PendingResponse m_pendingResponse[LGMP_MAX_CLIENTS];
ClaimRecord m_claimRecord[CLAIM_HISTORY_COUNT];
@@ -113,41 +187,29 @@ private:
bool m_inboundStopping = false;
IAudioTarget * m_target = nullptr;
bool m_initialized = false;
bool m_claimResponderReady = 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 = {};
uint32_t m_feedbackAuthority = INVALID_SLOT;
uint32_t m_captureOwner = INVALID_SLOT;
uint32_t m_captureBinding = INVALID_SLOT;
uint32_t m_nextBindingEpoch = 0;
uint32_t m_nextRoleEpoch = 0;
uint64_t m_nextSubscriberSerial = 0;
bool m_activationCommanded = false;
AudioEndpointStatus m_state;
bool m_stateKnown = false;
bool m_statusDirty = false;
uint64_t m_announcedPlayback = 0;
uint64_t m_playbackRunGeneration = 0;
uint64_t m_announcedCapture = 0;
uint64_t m_hostSequence = 0;
uint64_t m_nextClientSequence = 0;
bool m_clientSequenceKnown = false;
bool m_captureDiscontinuity = false;
uint64_t m_previousPlaybackGeneration = 0;
bool m_captureCallbackInFlight = 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_authorityBlockedSince = 0;
uint64_t m_captureReadySerial = 0;
uint64_t m_inboundActivitySerial = 0;
PendingCapture m_pendingCapture;
@@ -158,9 +220,11 @@ private:
void StopInboundWorker();
void InboundThread();
static DWORD CALLBACK InboundThreadProc(void * context);
bool BindStreams(uint32_t clientID);
bool TryUnbindStreams();
void ForceUnbindStreams();
bool BindSubscriber(uint32_t index, uint32_t clientID,
const KVMFRAudioClaim& claim);
bool TryUnbindSubscriber(uint32_t index);
void ForceUnbindSubscriber(uint32_t index);
void ResetSubscriber(uint32_t index);
static bool SameFormat(const AudioFormat& left,
const AudioFormat& right);
@@ -171,6 +235,8 @@ private:
static KVMFRAudioStreamState WireState(AudioStreamState state);
static KVMFRStreamDescriptor WireDescriptor(
const LGMPStreamDescriptor& descriptor);
static KVMFRSPMCDescriptor WireDescriptor(
const LGMPSPMCDescriptor& descriptor);
bool ValidateClaim(const KVMFRAudioClaim& claim) const;
bool ResponsePending(uint32_t clientID,
@@ -180,58 +246,74 @@ private:
void CleanupResponseMemory();
void PumpResponses();
bool OwnerSubscribed() const;
void RenewLease();
void BeginRelease(const char * reason);
uint32_t FindSubscriber(uint32_t clientID) const;
uint32_t FindFreeSubscriber() const;
bool SubscriberPresent(uint32_t clientID,
const uint32_t clients[LGMP_MAX_CLIENTS], unsigned count) const;
void RenewLease(Subscriber& subscriber);
void BeginRelease(uint32_t index, const char * reason,
bool force = false);
void MaintainSubscribers(uint64_t now);
bool HasBoundSubscribers() const;
bool HasReadyPlayback() const;
bool PlaybackEligible(const Subscriber& subscriber) const;
bool FeedbackEligible(const Subscriber& subscriber) const;
bool CaptureEligible(const Subscriber& subscriber) const;
void RefreshGranted(uint32_t index, bool bumpRole);
void AssignFeedbackAuthority(uint32_t index);
void RevokeFeedbackAuthority(const char * reason);
void PromoteFeedbackAuthority(uint64_t playbackGeneration);
void AssignCaptureOwner();
void MaintainCaptureBinding();
void MarkStateDirty(uint32_t index);
void MarkAllStateDirty();
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,
KVMFRAudioStatus WireStatus(uint32_t index) const;
bool WriteControlMessage(uint32_t index,
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);
bool PlaybackCurrent(const KVMFRAudioStatus& status) const;
bool CaptureCurrent(const KVMFRAudioStatus& status) const;
void BeginStatePump(uint32_t index);
void PumpSubscriberState(uint32_t index);
void PumpState();
void CheckPlaybackCredit();
void CheckPlaybackBackpressure(uint64_t now);
bool ParseInbound(const LGMPStreamBuffer& buffer,
KVMFRAudioMessage& message) const;
bool PendingCaptureCurrent() const;
bool PendingCaptureCurrent(uint32_t index) const;
bool PrepareCapture(const KVMFRAudioMessage& message,
const LGMPStreamBuffer& buffer);
const LGMPStreamBuffer& buffer, uint32_t index);
void DrainControl(uint32_t index);
void DrainCapture();
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();
bool CanRespondToClaims();
void Process();
void TransportStop();
friend class CLGMPTransport;
public:
CLGMPAudioTransport(CLGMPHost& host, CLGMPControl& control) :
CLGMPAudio(CLGMPHost& host, CLGMPControl& control) :
m_host(host), m_control(control) {}
~CLGMPAudioTransport() override;
~CLGMPAudio() override;
CLGMPAudioTransport(const CLGMPAudioTransport&) = delete;
CLGMPAudioTransport& operator=(
const CLGMPAudioTransport&) = delete;
CLGMPAudio(const CLGMPAudio&) = delete;
CLGMPAudio& operator=(
const CLGMPAudio&) = delete;
bool Start(IAudioTarget& target) override;
void Stop() override;

View File

@@ -34,13 +34,13 @@
#include <memory>
class CLGMPTransport;
class CLGMPAudioTransport;
class CLGMPAudio;
class CLGMPControl final : public IControlSink
{
private:
friend class CLGMPTransport;
friend class CLGMPAudioTransport;
friend class CLGMPAudio;
static constexpr int POINTER_SHAPE_BUFFERS = 3;
static constexpr int COLOR_TRANSFORM_BUFFERS = 3;

View File

@@ -157,6 +157,12 @@ LGMP_STATUS CLGMPHost::CreateStream(
return lgmpHostStreamNew(m_host, config, stream);
}
LGMP_STATUS CLGMPHost::CreateSPMC(
const struct LGMPSPMCConfig& config, PLGMPHostSPMC * stream)
{
return lgmpHostSPMCNew(m_host, config, stream);
}
LGMP_STATUS CLGMPHost::Allocate(uint32_t size, PLGMPMemory * memory)
{
return lgmpHostMemAlloc(m_host, size, memory);

View File

@@ -29,6 +29,7 @@
extern "C" {
#include "lgmp/host.h"
#include "lgmp/spmc.h"
#include "lgmp/stream.h"
}
@@ -57,6 +58,8 @@ public:
PLGMPHostQueue * queue);
LGMP_STATUS CreateStream(const struct LGMPStreamConfig& config,
PLGMPHostStream * stream);
LGMP_STATUS CreateSPMC(const struct LGMPSPMCConfig& config,
PLGMPHostSPMC * stream);
LGMP_STATUS Allocate(uint32_t size, PLGMPMemory * memory);
LGMP_STATUS AllocateAligned(uint32_t size, uint32_t alignment,
PLGMPMemory * memory);

View File

@@ -102,7 +102,11 @@ bool CLGMPTransport::Initialize()
return false;
if (!m_audio.Initialize())
{
if (!m_audio.CanRespondToClaims())
return false;
DEBUG_WARN("LGMP audio transport is unavailable");
}
m_frames.SealMemoryLayout();
return true;

View File

@@ -40,7 +40,7 @@ private:
CIVSHMEM m_ivshmem;
CLGMPHost m_host;
CLGMPControl m_control;
CLGMPAudioTransport m_audio;
CLGMPAudio m_audio;
CLGMPFrameTransport m_frames;
CLGMPInputTransport m_input;
CLGMPClipboardTransport m_clipboard;