From 36e170d93a297a66ae20eecde8dde5002d9d7f2e Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 5 Sep 2026 21:38:50 +1000 Subject: [PATCH] [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. --- client/tests/lgmp_audio_test.c | 749 +++-- client/tests/lgmp_transport_test.c | 2 +- client/transports/LGMP/audio.c | 1108 +++++-- client/transports/LGMP/audio.h | 2 +- client/transports/LGMP/lgmp.c | 3 +- common/include/common/KVMFRAudio.h | 447 --- .../transport/lgmp/CLGMPAudioTransport.cpp | 2796 +++++++++++------ .../transport/lgmp/CLGMPAudioTransport.h | 190 +- idd/LGIdd/transport/lgmp/CLGMPControl.h | 4 +- idd/LGIdd/transport/lgmp/CLGMPHost.cpp | 6 + idd/LGIdd/transport/lgmp/CLGMPHost.h | 3 + idd/LGIdd/transport/lgmp/CLGMPTransport.cpp | 4 + idd/LGIdd/transport/lgmp/CLGMPTransport.h | 2 +- repos/LGProtocol | 2 +- 14 files changed, 3485 insertions(+), 1833 deletions(-) delete mode 100644 common/include/common/KVMFRAudio.h diff --git a/client/tests/lgmp_audio_test.c b/client/tests/lgmp_audio_test.c index 7d1d22b9..25d8ee2d 100644 --- a/client/tests/lgmp_audio_test.c +++ b/client/tests/lgmp_audio_test.c @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -136,6 +137,18 @@ static void playbackStop(void * opaque, uint32_t generation) memory_order_release); } +static unsigned audioSampleBytes(LG_AudioSampleFormat format) +{ + switch (format) + { + case LG_AUDIO_FMT_S16_LE: return 2; + case LG_AUDIO_FMT_S24_LE: return 3; + case LG_AUDIO_FMT_S32_LE: + case LG_AUDIO_FMT_F32_LE: return 4; + default: return 0; + } +} + static void playbackData(void * opaque, uint32_t generation, const void * data, size_t frames, const LG_AudioClock * clock) { @@ -143,7 +156,8 @@ static void playbackData(void * opaque, uint32_t generation, pthread_mutex_lock(&trace->mutex); trace->playGeneration = generation; trace->playFrames = frames; - const size_t bytes = frames * trace->playFormat.channelCount * 3U; + const size_t bytes = frames * trace->playFormat.channelCount * + audioSampleBytes(trace->playFormat.sampleFormat); const size_t copy = bytes < sizeof(trace->playData) ? bytes : sizeof(trace->playData); memset(trace->playData, 0, sizeof(trace->playData)); @@ -198,13 +212,26 @@ struct Fixture LGEvent * pointerWake; LGMPAudio * audio; const LG_AudioOps * ops; - PLGMPHostStream h2c; - PLGMPHostStream c2h; + PLGMPHostSPMC playback; + PLGMPHostStream hostControl; + PLGMPHostStream clientControl; + PLGMPHostStream capture; + uint32_t playbackReaderID; + uint32_t bindingEpoch; + uint32_t roleEpoch; PLGMPMemory memories[MAX_MEMORIES]; unsigned memoryCount; KVMFRAudioClaim claim; - uint64_t hostSequence; + uint64_t hostControlSequence; + uint64_t stateSerial; uint64_t endpointGeneration; + bool available; + KVMFRAudioCapabilities capabilities; + KVMFRAudioCapabilities grantedCapabilities; + uint64_t playbackGeneration; + uint64_t captureGeneration; + KVMFRAudioFormat playbackFormat; + KVMFRAudioFormat captureFormat; struct Trace trace; }; @@ -355,10 +382,14 @@ static void fixtureCleanup(struct Fixture * fixture) lgmpClientUnsubscribe(&fixture->pointerClient); if (fixture->client) lgmpClientFree(&fixture->client); - if (fixture->h2c) - lgmpHostStreamFree(&fixture->h2c); - if (fixture->c2h) - lgmpHostStreamFree(&fixture->c2h); + if (fixture->playback) + lgmpHostSPMCFree(&fixture->playback); + if (fixture->hostControl) + lgmpHostStreamFree(&fixture->hostControl); + if (fixture->clientControl) + lgmpHostStreamFree(&fixture->clientControl); + if (fixture->capture) + lgmpHostStreamFree(&fixture->capture); for (unsigned i = 0; i < fixture->memoryCount; ++i) lgmpHostMemFree(&fixture->memories[i]); if (fixture->host) @@ -404,28 +435,45 @@ static bool readClaim(struct Fixture * fixture, KVMFRAudioClaim * claim, static bool createStreams(struct Fixture * fixture) { - const struct LGMPStreamConfig h2cConfig = + const struct LGMPSPMCConfig playbackConfig = + { + .slotCount = KVMFR_AUDIO_STREAM_SLOT_COUNT, + .slotSize = KVMFR_AUDIO_PLAYBACK_SLOT_BYTES, + .maxReaders = KVMFR_AUDIO_MAX_PLAYBACK_READERS, + }; + const struct LGMPStreamConfig hostControlConfig = { .direction = LGMP_STREAM_HOST_TO_CLIENT, .policy = LGMP_STREAM_RELIABLE_FIFO, .slotCount = KVMFR_AUDIO_STREAM_SLOT_COUNT, - .slotSize = KVMFR_AUDIO_STREAM_SLOT_BYTES, + .slotSize = KVMFR_AUDIO_HOST_CONTROL_SLOT_BYTES, }; - const struct LGMPStreamConfig c2hConfig = + const struct LGMPStreamConfig clientControlConfig = { .direction = LGMP_STREAM_CLIENT_TO_HOST, .policy = LGMP_STREAM_RELIABLE_FIFO, .slotCount = KVMFR_AUDIO_STREAM_SLOT_COUNT, - .slotSize = KVMFR_AUDIO_STREAM_SLOT_BYTES, + .slotSize = KVMFR_AUDIO_CLIENT_CONTROL_SLOT_BYTES, }; - CHECK(lgmpHostStreamNew(fixture->host, h2cConfig, - &fixture->h2c) == LGMP_OK); - CHECK(lgmpHostStreamNew(fixture->host, c2hConfig, - &fixture->c2h) == LGMP_OK); + const struct LGMPStreamConfig captureConfig = + { + .direction = LGMP_STREAM_CLIENT_TO_HOST, + .policy = LGMP_STREAM_RELIABLE_FIFO, + .slotCount = KVMFR_AUDIO_STREAM_SLOT_COUNT, + .slotSize = KVMFR_AUDIO_CAPTURE_SLOT_BYTES, + }; + CHECK(lgmpHostSPMCNew(fixture->host, playbackConfig, + &fixture->playback) == LGMP_OK); + CHECK(lgmpHostStreamNew(fixture->host, hostControlConfig, + &fixture->hostControl) == LGMP_OK); + CHECK(lgmpHostStreamNew(fixture->host, clientControlConfig, + &fixture->clientControl) == LGMP_OK); + CHECK(lgmpHostStreamNew(fixture->host, captureConfig, + &fixture->capture) == LGMP_OK); return true; } -static void copyDescriptor(KVMFRStreamDescriptor * output, +static void copyStreamDescriptor(KVMFRStreamDescriptor * output, PLGMPHostStream stream) { struct LGMPStreamDescriptor descriptor; @@ -433,15 +481,43 @@ static void copyDescriptor(KVMFRStreamDescriptor * output, memcpy(output, &descriptor, sizeof(*output)); } +static void copySPMCDescriptor(KVMFRSPMCDescriptor * output, + PLGMPHostSPMC stream) +{ + struct LGMPSPMCDescriptor descriptor; + lgmpHostSPMCGetDescriptor(stream, &descriptor); + memcpy(output, &descriptor, sizeof(*output)); +} + static bool bindStreams(struct Fixture * fixture) { uint32_t epoch = 0; - CHECK(lgmpHostStreamBind(fixture->h2c, + CHECK(lgmpHostSPMCReaderBind(fixture->playback, + fixture->clientID, &fixture->playbackReaderID, + &epoch) == LGMP_OK); + CHECK(epoch != 0); + CHECK(lgmpHostStreamBind(fixture->hostControl, fixture->clientID, &epoch) == LGMP_OK); CHECK(epoch != 0); - CHECK(lgmpHostStreamBind(fixture->c2h, + CHECK(lgmpHostStreamBind(fixture->clientControl, fixture->clientID, &epoch) == LGMP_OK); CHECK(epoch != 0); + CHECK(lgmpHostStreamBind(fixture->capture, + fixture->clientID, &epoch) == LGMP_OK); + CHECK(epoch != 0); + if (++fixture->bindingEpoch == 0) + ++fixture->bindingEpoch; + if (++fixture->roleEpoch == 0) + ++fixture->roleEpoch; + fixture->hostControlSequence = 0; + fixture->stateSerial = 0; + fixture->grantedCapabilities = + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK; + fixture->playbackGeneration = 0; + fixture->captureGeneration = 0; + memset(&fixture->playbackFormat, 0, sizeof(fixture->playbackFormat)); + memset(&fixture->captureFormat, 0, sizeof(fixture->captureFormat)); return true; } @@ -483,9 +559,17 @@ static bool deliverBoundResponse(struct Fixture * fixture, .protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX, .endpointGeneration = fixture->endpointGeneration, .claimToken = correct ? claim->claimToken : claim->claimToken + 1, + .grantedCapabilities = + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK, + .bindingEpoch = fixture->bindingEpoch, + .roleEpoch = fixture->roleEpoch, + .playbackReaderID = fixture->playbackReaderID, }; - copyDescriptor(&response.hostToClient, fixture->h2c); - copyDescriptor(&response.clientToHost, fixture->c2h); + copySPMCDescriptor(&response.playback, fixture->playback); + copyStreamDescriptor(&response.hostControl, fixture->hostControl); + copyStreamDescriptor(&response.clientControl, fixture->clientControl); + copyStreamDescriptor(&response.capture, fixture->capture); LGMPMessage message; CHECK(postTagged(fixture, KVMFR_AUDIO_CONTROL_UDATA, @@ -506,8 +590,15 @@ static KVMFRAudioFormat makeFormat(uint32_t channelMask, unsigned channels = 0; for (uint32_t mask = channelMask; mask; mask >>= 1) channels += mask & 1U; - const unsigned bytes = - sampleFormat == KVMFR_AUDIO_SAMPLE_S24_LE ? 3U : 2U; + unsigned bytes = 0; + switch (sampleFormat) + { + case KVMFR_AUDIO_SAMPLE_S16_LE: bytes = 2; break; + case KVMFR_AUDIO_SAMPLE_S24_LE: bytes = 3; break; + case KVMFR_AUDIO_SAMPLE_S32_LE: + case KVMFR_AUDIO_SAMPLE_F32_LE: bytes = 4; break; + default: break; + } return (KVMFRAudioFormat) { .version = KVMFR_AUDIO_FORMAT_VERSION, @@ -522,13 +613,14 @@ static KVMFRAudioFormat makeFormat(uint32_t channelMask, }; } -static bool writeH2C(struct Fixture * fixture, uint32_t type, +static bool writeHostControl(struct Fixture * fixture, uint32_t type, uint64_t generation, uint32_t flags, uint64_t position, int64_t timeNs, uint64_t rateQ32, uint32_t frames, - const void * payload, uint32_t length, uint64_t sequence) + const void * payload, uint32_t length) { LGMPStreamBuffer buffer = { 0 }; - CHECK(lgmpHostStreamWriteAcquire(fixture->h2c, &buffer) == LGMP_OK); + CHECK(lgmpHostStreamWriteAcquire( + fixture->hostControl, &buffer) == LGMP_OK); CHECK(buffer.capacity >= sizeof(KVMFRAudioMessage) + length); const KVMFRAudioMessage message = { @@ -537,7 +629,7 @@ static bool writeH2C(struct Fixture * fixture, uint32_t type, .type = type, .streamGeneration = generation, .flags = flags, - .sequence = sequence, + .sequence = fixture->hostControlSequence, .position = position, .timeNs = timeNs, .rateQ32 = rateQ32, @@ -547,92 +639,98 @@ static bool writeH2C(struct Fixture * fixture, uint32_t type, memcpy(buffer.data, &message, sizeof(message)); if (length) memcpy((uint8_t *)buffer.data + sizeof(message), payload, length); - CHECK(lgmpHostStreamWriteCommit(fixture->h2c, &buffer, + CHECK(lgmpHostStreamWriteCommit(fixture->hostControl, &buffer, + (uint32_t)sizeof(message) + length) == LGMP_OK); + ++fixture->hostControlSequence; + return true; +} + +static bool publishPlayback(struct Fixture * fixture, uint64_t generation, + uint32_t flags, uint64_t position, int64_t timeNs, uint64_t rateQ32, + uint32_t frames, const void * payload, uint32_t length) +{ + LGMPSPMCBuffer buffer = { 0 }; + CHECK(lgmpHostSPMCWriteAcquire(fixture->playback, &buffer) == LGMP_OK); + CHECK(buffer.capacity >= sizeof(KVMFRAudioMessage) + length); + const KVMFRAudioMessage message = + { + .version = KVMFR_AUDIO_MESSAGE_VERSION, + .size = sizeof(KVMFRAudioMessage), + .type = KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, + .streamGeneration = generation, + .flags = flags, + .sequence = buffer.sequence, + .position = position, + .timeNs = timeNs, + .rateQ32 = rateQ32, + .frames = frames, + .length = length, + }; + memcpy(buffer.data, &message, sizeof(message)); + if (length) + memcpy((uint8_t *)buffer.data + sizeof(message), payload, length); + CHECK(lgmpHostSPMCWriteCommit(fixture->playback, &buffer, (uint32_t)sizeof(message) + length) == LGMP_OK); return true; } -static bool sendH2C(struct Fixture * fixture, uint32_t type, - uint64_t generation, uint32_t flags, uint64_t position, - int64_t timeNs, uint64_t rateQ32, uint32_t frames, - const void * payload, uint32_t length) +static bool skipPlaybackRecord(struct Fixture * fixture) { - return writeH2C(fixture, type, generation, flags, position, timeNs, - rateQ32, frames, payload, length, fixture->hostSequence++); + LGMPSPMCBuffer buffer = { 0 }; + CHECK(lgmpHostSPMCWriteAcquire(fixture->playback, &buffer) == LGMP_OK); + CHECK(lgmpHostSPMCWriteCancel(fixture->playback, &buffer) == LGMP_OK); + return true; } -static bool sendStatus(struct Fixture * fixture, bool available, - uint32_t capabilities, - uint64_t playGeneration, const KVMFRAudioFormat * playFormat, - uint64_t captureGeneration, const KVMFRAudioFormat * captureFormat) -{ - uint32_t flags = KVMFR_AUDIO_STATUS_HAS_OWNER; - if (capabilities & KVMFR_AUDIO_CAP_PLAYBACK) - flags |= KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE; - if (capabilities & KVMFR_AUDIO_CAP_CAPTURE) - flags |= KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE; - if (available) - flags |= KVMFR_AUDIO_STATUS_AVAILABLE; - if (playGeneration) - flags |= KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE; - if (captureGeneration) - flags |= KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE; - KVMFRAudioStatus status = - { - .version = KVMFR_AUDIO_STATUS_VERSION, - .size = sizeof(KVMFRAudioStatus), - .flags = flags, - .protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX, - .requestGeneration = fixture->claim.requestGeneration, - .capabilities = capabilities, - .ownerClientID = fixture->clientID, - .ownerGeneration = fixture->claim.requestGeneration, - .endpointGeneration = fixture->endpointGeneration, - .playbackGeneration = playGeneration, - .captureGeneration = captureGeneration, - .playbackState = playGeneration ? - KVMFR_AUDIO_STREAM_RUNNING : KVMFR_AUDIO_STREAM_STOPPED, - .captureState = captureGeneration ? - KVMFR_AUDIO_STREAM_RUNNING : KVMFR_AUDIO_STREAM_STOPPED, - }; - if (playFormat) - status.playbackFormat = *playFormat; - if (captureFormat) - status.captureFormat = *captureFormat; - return sendH2C(fixture, KVMFR_AUDIO_MESSAGE_STATUS, 0, 0, 0, 0, 0, - 0, &status, sizeof(status)); -} - -static bool readC2H(struct Fixture * fixture, KVMFRAudioMessage * message, +static bool readStream(struct Fixture * fixture, PLGMPHostStream stream, + KVMFRAudioMessage * message, uint8_t payload[KVMFR_AUDIO_PCM_BYTES], unsigned timeoutMs) { const uint64_t deadline = microtime() + (uint64_t)timeoutMs * 1000U; while (microtime() < deadline) { LGMPStreamBuffer buffer = { 0 }; - const LGMP_STATUS status = - lgmpHostStreamReadPeek(fixture->c2h, &buffer); + const LGMP_STATUS status = lgmpHostStreamReadPeek(stream, &buffer); if (status == LGMP_OK) { CHECK(buffer.size >= sizeof(*message)); memcpy(message, buffer.data, sizeof(*message)); + CHECK(message->version == KVMFR_AUDIO_MESSAGE_VERSION); + CHECK(message->size == sizeof(*message)); + CHECK(!message->reserved0); CHECK(message->length <= KVMFR_AUDIO_PCM_BYTES); CHECK(buffer.size == sizeof(*message) + message->length); if (message->length) memcpy(payload, (uint8_t *)buffer.data + sizeof(*message), message->length); - CHECK(lgmpHostStreamReadRelease(fixture->c2h, &buffer) == LGMP_OK); + CHECK(lgmpHostStreamReadRelease(stream, &buffer) == LGMP_OK); return true; } CHECK(status == LGMP_ERR_STREAM_EMPTY); lgmpHostProcess(fixture->host); - struct timespec delay = { .tv_nsec = 1000000L }; + const struct timespec delay = { .tv_nsec = 1000000L }; nanosleep(&delay, NULL); } return false; } -static bool readC2HType(struct Fixture * fixture, uint32_t type, +static bool readClientControl(struct Fixture * fixture, + KVMFRAudioMessage * message, uint8_t payload[KVMFR_AUDIO_PCM_BYTES], + unsigned timeoutMs) +{ + return readStream(fixture, fixture->clientControl, + message, payload, timeoutMs); +} + +static bool readCapture(struct Fixture * fixture, + KVMFRAudioMessage * message, uint8_t payload[KVMFR_AUDIO_PCM_BYTES], + unsigned timeoutMs) +{ + return readStream(fixture, fixture->capture, + message, payload, timeoutMs); +} + +static bool readClientControlType(struct Fixture * fixture, uint32_t type, KVMFRAudioMessage * message, uint8_t payload[KVMFR_AUDIO_PCM_BYTES], unsigned timeoutMs) { @@ -640,47 +738,285 @@ static bool readC2HType(struct Fixture * fixture, uint32_t type, while (microtime() < deadline) { const unsigned left = (unsigned)((deadline - microtime()) / 1000U + 1); - CHECK(readC2H(fixture, message, payload, left)); + CHECK(readClientControl(fixture, message, payload, left)); if (message->type == type) return true; } return false; } -static bool unbindBoth(struct Fixture * fixture) +static KVMFRAudioStatus makeStatus(const struct Fixture * fixture) +{ + uint32_t flags = KVMFR_AUDIO_STATUS_BOUND; + if (fixture->capabilities & KVMFR_AUDIO_CAP_PLAYBACK) + flags |= KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE; + if (fixture->capabilities & KVMFR_AUDIO_CAP_CAPTURE) + flags |= KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE; + if (fixture->available) + flags |= KVMFR_AUDIO_STATUS_AVAILABLE; + if (fixture->available && fixture->playbackGeneration && + (fixture->grantedCapabilities & KVMFR_AUDIO_CAP_PLAYBACK)) + flags |= KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE; + if (fixture->available && fixture->captureGeneration && + (fixture->grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE)) + flags |= KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE; + + KVMFRAudioStatus status = + { + .version = KVMFR_AUDIO_STATUS_VERSION, + .size = sizeof(KVMFRAudioStatus), + .flags = flags, + .protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX, + .requestGeneration = fixture->claim.requestGeneration, + .capabilities = fixture->capabilities, + .grantedCapabilities = fixture->grantedCapabilities, + .bindingEpoch = fixture->bindingEpoch, + .roleEpoch = fixture->roleEpoch, + .boundClientID = fixture->clientID, + .feedbackAuthorityClientID = + fixture->grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK ? + fixture->clientID : 0, + .feedbackAuthorityEpoch = + fixture->grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK ? + fixture->roleEpoch : 0, + .captureOwnerClientID = + fixture->grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE ? + fixture->clientID : 0, + .endpointGeneration = fixture->endpointGeneration, + .playbackGeneration = fixture->playbackGeneration, + .captureGeneration = fixture->captureGeneration, + .playbackState = fixture->playbackGeneration ? + KVMFR_AUDIO_STREAM_RUNNING : KVMFR_AUDIO_STREAM_STOPPED, + .captureState = fixture->captureGeneration ? + KVMFR_AUDIO_STREAM_RUNNING : KVMFR_AUDIO_STREAM_STOPPED, + .playbackFormat = fixture->playbackFormat, + .captureFormat = fixture->captureFormat, + }; + return status; +} + +static bool sendCurrentStatus(struct Fixture * fixture) +{ + const KVMFRAudioStatus status = makeStatus(fixture); + return writeHostControl(fixture, KVMFR_AUDIO_MESSAGE_STATUS, + 0, 0, 0, 0, 0, 0, &status, sizeof(status)); +} + +static bool readInitialSubscription(struct Fixture * fixture) +{ + KVMFRAudioMessage message; + uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(readClientControl(fixture, &message, payload, WAIT_MS)); + CHECK(message.type == KVMFR_AUDIO_MESSAGE_PLAYBACK_SUBSCRIBE); + CHECK(message.sequence == 0); + CHECK(!message.streamGeneration && !message.flags && + !message.position && !message.timeNs && !message.rateQ32 && + !message.frames && + message.length == sizeof(KVMFRAudioSubscription)); + KVMFRAudioSubscription subscription; + memcpy(&subscription, payload, sizeof(subscription)); + CHECK(subscription.bindingEpoch == fixture->bindingEpoch); + CHECK(subscription.roleEpoch == fixture->roleEpoch); + CHECK(subscription.subscriptions == + (KVMFR_AUDIO_SUBSCRIBE_PLAYBACK | + KVMFR_AUDIO_SUBSCRIBE_CAPTURE)); + CHECK(!subscription.reserved); + return true; +} + +struct PlaybackStartClock +{ + uint64_t position; + int64_t timeNs; + uint64_t rateQ32; + bool stable; +}; + +static bool sendStateSnapshotWithPlaybackClock( + struct Fixture * fixture, bool available, + KVMFRAudioCapabilities capabilities, + KVMFRAudioCapabilities grantedCapabilities, + uint64_t playbackGeneration, const KVMFRAudioFormat * playbackFormat, + uint64_t captureGeneration, const KVMFRAudioFormat * captureFormat, + const struct PlaybackStartClock * playbackClock) +{ + CHECK(!(grantedCapabilities & ~capabilities)); + CHECK(!playbackGeneration || playbackFormat); + CHECK(!captureGeneration || captureFormat); + CHECK(!playbackGeneration || + (available && + (grantedCapabilities & KVMFR_AUDIO_CAP_PLAYBACK))); + CHECK(!captureGeneration || + (available && + (grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE))); + + const uint64_t oldPlaybackGeneration = fixture->playbackGeneration; + const uint64_t oldCaptureGeneration = fixture->captureGeneration; + const bool playbackChanged = + oldPlaybackGeneration != playbackGeneration || + (playbackGeneration && + memcmp(&fixture->playbackFormat, playbackFormat, + sizeof(*playbackFormat))); + const bool captureChanged = + oldCaptureGeneration != captureGeneration || + (captureGeneration && + memcmp(&fixture->captureFormat, captureFormat, + sizeof(*captureFormat))); + CHECK(!playbackClock || (playbackChanged && playbackGeneration)); + CHECK(!playbackClock || playbackClock->rateQ32); + + if (fixture->grantedCapabilities != grantedCapabilities) + { + if (++fixture->roleEpoch == 0) + ++fixture->roleEpoch; + } + fixture->available = available; + fixture->capabilities = capabilities; + fixture->grantedCapabilities = grantedCapabilities; + fixture->playbackGeneration = playbackGeneration; + fixture->captureGeneration = captureGeneration; + if (playbackFormat) + fixture->playbackFormat = *playbackFormat; + else + memset(&fixture->playbackFormat, 0, sizeof(fixture->playbackFormat)); + if (captureFormat) + fixture->captureFormat = *captureFormat; + else + memset(&fixture->captureFormat, 0, sizeof(fixture->captureFormat)); + + CHECK(sendCurrentStatus(fixture)); + if (oldPlaybackGeneration && playbackChanged) + CHECK(writeHostControl(fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, + oldPlaybackGeneration, 0, 0, 0, 0, 0, NULL, 0)); + if (oldCaptureGeneration && captureChanged) + CHECK(writeHostControl(fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_STOP, + oldCaptureGeneration, 0, 0, 0, 0, 0, NULL, 0)); + if (playbackGeneration && playbackChanged) + { + const uint32_t flags = playbackClock ? + KVMFR_AUDIO_MESSAGE_CLOCK_VALID | + (playbackClock->stable ? + KVMFR_AUDIO_MESSAGE_CLOCK_STABLE : 0) : 0; + CHECK(writeHostControl(fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, + playbackGeneration, flags, + playbackClock ? playbackClock->position : 0, + playbackClock ? playbackClock->timeNs : 0, + playbackClock ? playbackClock->rateQ32 : 0, 0, + playbackFormat, sizeof(*playbackFormat))); + } + if (captureGeneration && captureChanged) + CHECK(writeHostControl(fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_START, + captureGeneration, 0, 0, 0, 0, 0, + captureFormat, sizeof(*captureFormat))); + + if (++fixture->stateSerial == 0) + ++fixture->stateSerial; + const KVMFRAudioStateBarrier barrier = + { + .bindingEpoch = fixture->bindingEpoch, + .roleEpoch = fixture->roleEpoch, + .stateSerial = fixture->stateSerial, + }; + CHECK(writeHostControl(fixture, KVMFR_AUDIO_MESSAGE_STATE_BARRIER, + playbackGeneration, 0, 0, 0, 0, 0, + &barrier, sizeof(barrier))); + + KVMFRAudioMessage message; + uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(readClientControlType(fixture, KVMFR_AUDIO_MESSAGE_STATE_ACK, + &message, payload, WAIT_MS)); + CHECK(message.streamGeneration == playbackGeneration); + CHECK(!message.flags && !message.position && !message.timeNs && + !message.rateQ32 && !message.frames && + message.length == sizeof(KVMFRAudioStateBarrier)); + KVMFRAudioStateBarrier ack; + memcpy(&ack, payload, sizeof(ack)); + CHECK(!memcmp(&ack, &barrier, sizeof(ack))); + return true; +} + +static bool sendStateSnapshot(struct Fixture * fixture, bool available, + KVMFRAudioCapabilities capabilities, + KVMFRAudioCapabilities grantedCapabilities, + uint64_t playbackGeneration, const KVMFRAudioFormat * playbackFormat, + uint64_t captureGeneration, const KVMFRAudioFormat * captureFormat) +{ + return sendStateSnapshotWithPlaybackClock(fixture, available, + capabilities, grantedCapabilities, + playbackGeneration, playbackFormat, + captureGeneration, captureFormat, NULL); +} + +static bool sendHostControl(struct Fixture * fixture, uint32_t type, + uint64_t generation, uint32_t flags, uint64_t position, + int64_t timeNs, uint64_t rateQ32, uint32_t frames, + const void * payload, uint32_t length) +{ + return writeHostControl(fixture, type, generation, flags, position, + timeNs, rateQ32, frames, payload, length); +} + +static bool drainHostStream(PLGMPHostStream stream) +{ + LGMPStreamBuffer buffer = { 0 }; + LGMP_STATUS status; + while ((status = lgmpHostStreamReadPeek(stream, &buffer)) == LGMP_OK) + { + CHECK(lgmpHostStreamReadRelease(stream, &buffer) == LGMP_OK); + memset(&buffer, 0, sizeof(buffer)); + } + CHECK(status == LGMP_ERR_STREAM_EMPTY || + status == LGMP_ERR_STREAM_UNBOUND || + status == LGMP_ERR_STREAM_STALE); + return true; +} + +static bool unbindStreams(struct Fixture * fixture) { const uint64_t deadline = microtime() + WAIT_MS * 1000U; - bool h2cDone = false; - bool c2hDone = false; - while (microtime() < deadline && (!h2cDone || !c2hDone)) + bool playbackDone = false; + bool hostControlDone = false; + bool clientControlDone = false; + bool captureDone = false; + while (microtime() < deadline && + (!playbackDone || !hostControlDone || + !clientControlDone || !captureDone)) { - if (!h2cDone) + if (!playbackDone) { - const LGMP_STATUS status = lgmpHostStreamUnbind(fixture->h2c); + const LGMP_STATUS status = lgmpHostSPMCReaderUnbind( + fixture->playback, fixture->playbackReaderID); CHECK(status == LGMP_OK || status == LGMP_ERR_STREAM_BUSY); - h2cDone = status == LGMP_OK; + playbackDone = status == LGMP_OK; } - if (!c2hDone) + if (!hostControlDone) { - LGMPStreamBuffer buffer = { 0 }; - LGMP_STATUS status; - while ((status = lgmpHostStreamReadPeek(fixture->c2h, - &buffer)) == LGMP_OK) - { - CHECK(lgmpHostStreamReadRelease(fixture->c2h, &buffer) == LGMP_OK); - memset(&buffer, 0, sizeof(buffer)); - } - CHECK(status == LGMP_ERR_STREAM_EMPTY || - status == LGMP_ERR_STREAM_UNBOUND || - status == LGMP_ERR_STREAM_STALE); - status = lgmpHostStreamUnbind(fixture->c2h); + const LGMP_STATUS status = + lgmpHostStreamUnbind(fixture->hostControl); CHECK(status == LGMP_OK || status == LGMP_ERR_STREAM_BUSY); - c2hDone = status == LGMP_OK; + hostControlDone = status == LGMP_OK; } + if (!clientControlDone) + { + CHECK(drainHostStream(fixture->clientControl)); + const LGMP_STATUS status = + lgmpHostStreamUnbind(fixture->clientControl); + CHECK(status == LGMP_OK || status == LGMP_ERR_STREAM_BUSY); + clientControlDone = status == LGMP_OK; + } + if (!captureDone) + { + CHECK(drainHostStream(fixture->capture)); + const LGMP_STATUS status = lgmpHostStreamUnbind(fixture->capture); + CHECK(status == LGMP_OK || status == LGMP_ERR_STREAM_BUSY); + captureDone = status == LGMP_OK; + } + lgmpHostProcess(fixture->host); struct timespec delay = { .tv_nsec = 1000000L }; nanosleep(&delay, NULL); } - return h2cDone && c2hDone; + return playbackDone && hostControlDone && + clientControlDone && captureDone; } static bool beginClaim(struct Fixture * fixture, bool retry) @@ -696,10 +1032,21 @@ static bool beginClaim(struct Fixture * fixture, bool retry) lgmpAudio_pumpClaim(fixture->audio, fixture->pointerClient); CHECK(readClaim(fixture, &fixture->claim, WAIT_MS)); CHECK(fixture->claim.messageType == KVMFR_MESSAGE_AUDIO_CLAIM); + CHECK(fixture->claim.version == KVMFR_AUDIO_CONTROL_VERSION); + CHECK(fixture->claim.size == sizeof(fixture->claim)); CHECK(fixture->claim.flags == KVMFR_AUDIO_CLAIM_BIND); + CHECK(fixture->claim.protocolMin == KVMFR_AUDIO_PROTOCOL_MIN); + CHECK(fixture->claim.protocolMax == KVMFR_AUDIO_PROTOCOL_MAX); CHECK(fixture->claim.clientCapabilities == (KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)); + CHECK(fixture->claim.subscriptions == + (KVMFR_AUDIO_SUBSCRIBE_PLAYBACK | + KVMFR_AUDIO_SUBSCRIBE_CAPTURE)); + for (unsigned i = 0; + i < sizeof(fixture->claim.reserved) / + sizeof(fixture->claim.reserved[0]); ++i) + CHECK(!fixture->claim.reserved[i]); if (retry) { @@ -730,10 +1077,10 @@ static bool testLateBoundRelease(void) CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); KVMFRAudioMessage message; uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(readClientControl(&fixture, &message, payload, WAIT_MS)); CHECK(message.type == KVMFR_AUDIO_MESSAGE_RELEASE); CHECK(message.sequence == 0); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); const struct timespec settle = { .tv_nsec = 30000000L }; nanosleep(&settle, NULL); @@ -791,19 +1138,20 @@ static bool testRetiredClaim(void) CHECK(deliverBoundResponse(&fixture, &retired, true)); KVMFRAudioMessage message; uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(!readC2H(&fixture, &message, payload, 30)); - CHECK(unbindBoth(&fixture)); + CHECK(!readClientControl(&fixture, &message, payload, 30)); + CHECK(unbindStreams(&fixture)); CHECK(bindStreams(&fixture)); CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); - CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(readInitialSubscription(&fixture)); + CHECK(readClientControl(&fixture, &message, payload, WAIT_MS)); CHECK(message.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); - CHECK(message.sequence == 0); + CHECK(message.sequence == 1); fixture.ops->setStatusListener(fixture.audio, NULL, NULL); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, &message, payload, WAIT_MS)); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); const struct timespec settle = { .tv_nsec = 30000000L }; nanosleep(&settle, NULL); @@ -822,16 +1170,15 @@ static bool testPlaybackRestartSameGeneration(void) KVMFRAudioMessage message; uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_KEEPALIVE, + CHECK(readInitialSubscription(&fixture)); + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_KEEPALIVE, &message, payload, WAIT_MS)); const uint64_t wireGeneration = UINT64_C(0x300000001); const KVMFRAudioFormat format = makeFormat( KVMFR_AUDIO_CHANNEL_FRONT_LEFT, KVMFR_AUDIO_SAMPLE_S24_LE); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, - wireGeneration, 0, 0, 0, 0, 0, - &format, sizeof(format))); - CHECK(sendStatus(&fixture, true, KVMFR_AUDIO_CAP_PLAYBACK, + CHECK(sendStateSnapshot(&fixture, true, + KVMFR_AUDIO_CAP_PLAYBACK, KVMFR_AUDIO_CAP_PLAYBACK, wireGeneration, &format, 0, NULL)); CHECK(waitCount(&fixture, &fixture.trace.statusCount, 2, WAIT_MS)); CHECK(fixture.ops->attach(fixture.audio, &eventOps, &fixture.trace)); @@ -847,13 +1194,13 @@ static bool testPlaybackRestartSameGeneration(void) for (unsigned i = 0; i < sizeof(pcm); ++i) pcm[i] = (uint8_t)(0xa0U + i); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, + CHECK(sendHostControl(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, wireGeneration, 0, 0, 0, 0, 0, NULL, 0)); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, + CHECK(sendHostControl(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, wireGeneration, 0, 0, 0, 0, 0, &format, sizeof(format))); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, - wireGeneration, 0, 0, 0, 0, 4, pcm, sizeof(pcm))); + CHECK(publishPlayback(&fixture, wireGeneration, + 0, 0, 0, 0, 4, pcm, sizeof(pcm))); CHECK(waitCount(&fixture, &fixture.trace.playStopCount, 1, WAIT_MS)); CHECK(waitCount(&fixture, &fixture.trace.playStartCount, 2, WAIT_MS)); @@ -884,9 +1231,9 @@ static bool testPlaybackRestartSameGeneration(void) fixture.ops->detach(fixture.audio); fixture.ops->setStatusListener(fixture.audio, NULL, NULL); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, &message, payload, WAIT_MS)); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); const struct timespec settle = { .tv_nsec = 30000000L }; nanosleep(&settle, NULL); @@ -931,9 +1278,10 @@ static bool testDataPlane(void) CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); KVMFRAudioMessage c2hMessage; uint8_t c2hPayload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(readInitialSubscription(&fixture)); + CHECK(readClientControl(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); CHECK(c2hMessage.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); - CHECK(c2hMessage.sequence == 0); + CHECK(c2hMessage.sequence == 1); KVMFRAudioFormat playFormat = makeFormat( KVMFR_AUDIO_CHANNEL_FRONT_LEFT, KVMFR_AUDIO_SAMPLE_S24_LE); @@ -941,18 +1289,18 @@ static bool testDataPlane(void) KVMFR_AUDIO_CHANNEL_TOP_BACK_RIGHT, KVMFR_AUDIO_SAMPLE_S24_LE); const uint64_t playWire = UINT64_C(0x100000002); const uint64_t captureWire = UINT64_C(0x200000003); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, - playWire, KVMFR_AUDIO_MESSAGE_CLOCK_VALID | - KVMFR_AUDIO_MESSAGE_CLOCK_STABLE, - 99, 123456, UINT64_C(192000) << 32, 0, - &playFormat, sizeof(playFormat))); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_START, - captureWire, 0, 0, 0, 0, 0, - &captureFormat, sizeof(captureFormat))); - CHECK(sendStatus(&fixture, true, + const struct PlaybackStartClock startClock = + { + .position = 99, + .timeNs = 123456, + .rateQ32 = UINT64_C(192000) << 32, + .stable = true, + }; + CHECK(sendStateSnapshotWithPlaybackClock(&fixture, true, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, playWire, &playFormat, - captureWire, &captureFormat)); + captureWire, &captureFormat, &startClock)); CHECK(waitCount(&fixture, &fixture.trace.statusCount, 2, WAIT_MS)); pthread_mutex_lock(&fixture.trace.mutex); const bool available = fixture.trace.status.available; @@ -962,6 +1310,13 @@ static bool testDataPlane(void) CHECK(fixture.ops->attach(fixture.audio, &eventOps, &fixture.trace)); CHECK(waitCount(&fixture, &fixture.trace.playStartCount, 1, WAIT_MS)); CHECK(waitCount(&fixture, &fixture.trace.recStartCount, 1, WAIT_MS)); + pthread_mutex_lock(&fixture.trace.mutex); + CHECK(fixture.trace.playClockValid); + CHECK(fixture.trace.playClock.stable); + CHECK(fixture.trace.playClock.position == startClock.position); + CHECK(fixture.trace.playClock.time == startClock.timeNs); + CHECK(fixture.trace.playClock.rate == 192000.0); + pthread_mutex_unlock(&fixture.trace.mutex); uint32_t previousPlayGeneration = 0; for (unsigned bit = 0; bit < KVMFR_AUDIO_MAX_CHANNELS; ++bit) @@ -972,8 +1327,10 @@ static bool testDataPlane(void) KVMFRAudioFormat format = makeFormat(UINT32_C(1) << bit, KVMFR_AUDIO_SAMPLE_S24_LE); const uint64_t wire = UINT64_C(0x300000000) + bit; - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, - wire, 0, 0, 0, 0, 0, &format, sizeof(format))); + CHECK(sendStateSnapshot(&fixture, true, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, + wire, &format, captureWire, &captureFormat)); CHECK(waitCount(&fixture, &fixture.trace.playStartCount, startCount + 1, WAIT_MS)); pthread_mutex_lock(&fixture.trace.mutex); @@ -992,8 +1349,8 @@ static bool testDataPlane(void) uint8_t pcm[12]; for (unsigned i = 0; i < sizeof(pcm); ++i) pcm[i] = (uint8_t)(0x40U + i); - ++fixture.hostSequence; - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, + CHECK(skipPlaybackRecord(&fixture)); + CHECK(publishPlayback(&fixture, UINT64_C(0x300000000) + KVMFR_AUDIO_MAX_CHANNELS - 1, KVMFR_AUDIO_MESSAGE_CLOCK_VALID | KVMFR_AUDIO_MESSAGE_CLOCK_STABLE, @@ -1025,8 +1382,8 @@ static bool testDataPlane(void) memset(capturePcm, 0x5a, sizeof(capturePcm)); CHECK(fixture.ops->recordData(fixture.audio, captureLocal, capturePcm, 6, &captureClock)); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_DATA, - &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(readCapture(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(c2hMessage.type == KVMFR_AUDIO_MESSAGE_CAPTURE_DATA); CHECK(c2hMessage.streamGeneration == captureWire); CHECK(c2hMessage.frames == 6); CHECK(c2hMessage.length == sizeof(capturePcm)); @@ -1056,7 +1413,9 @@ static bool testDataPlane(void) ++fixture.endpointGeneration; const unsigned clockStatusBefore = atomic_load_explicit( &fixture.trace.statusCount, memory_order_acquire); - CHECK(sendStatus(&fixture, true, + CHECK(sendStateSnapshot(&fixture, true, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK, KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | KVMFR_AUDIO_CAP_CLOCK_FEEDBACK, currentPlayWire, ¤tPlayFormat, @@ -1066,7 +1425,8 @@ static bool testDataPlane(void) CHECK(fixture.ops->clockFeedbackAvailable(fixture.audio)); CHECK(fixture.ops->clockFeedback(fixture.audio, playbackLocal, &playbackClock, 192000.0)); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK, + CHECK(readClientControlType(&fixture, + KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK, &c2hMessage, c2hPayload, WAIT_MS)); CHECK(c2hMessage.streamGeneration == UINT64_C(0x300000000) + KVMFR_AUDIO_MAX_CHANNELS - 1); @@ -1075,8 +1435,10 @@ static bool testDataPlane(void) KVMFRAudioClockFeedback feedback; memcpy(&feedback, c2hPayload, sizeof(feedback)); CHECK(feedback.targetRateQ32 == (UINT64_C(192000) << 32)); + CHECK(feedback.bindingEpoch == fixture.bindingEpoch); + CHECK(feedback.roleEpoch == fixture.roleEpoch); - while (readC2H(&fixture, &c2hMessage, c2hPayload, 2)) + while (readCapture(&fixture, &c2hMessage, c2hPayload, 2)) { } unsigned successful = 0; @@ -1105,7 +1467,7 @@ static bool testDataPlane(void) } CHECK(maxFullCall < 20000U); - CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(readCapture(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); uint8_t resumedPcm[18]; memset(resumedPcm, 0xef, sizeof(resumedPcm)); CHECK(fixture.ops->recordData(fixture.audio, captureLocal, @@ -1114,7 +1476,7 @@ static bool testDataPlane(void) uint64_t previousSequence = c2hMessage.sequence; while (!foundResumed) { - CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(readCapture(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); if (c2hMessage.type == KVMFR_AUDIO_MESSAGE_CAPTURE_DATA && c2hMessage.length == sizeof(resumedPcm) && !memcmp(c2hPayload, resumedPcm, sizeof(resumedPcm))) @@ -1126,19 +1488,16 @@ static bool testDataPlane(void) previousSequence = c2hMessage.sequence; } - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, - currentPlayWire, 0, 0, 0, 0, 0, NULL, 0)); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_STOP, - captureWire, 0, 0, 0, 0, 0, NULL, 0)); const unsigned statusBefore = atomic_load_explicit( &fixture.trace.statusCount, memory_order_acquire); - CHECK(sendStatus(&fixture, false, 0, 0, NULL, 0, NULL)); + CHECK(sendStateSnapshot(&fixture, false, 0, 0, + 0, NULL, 0, NULL)); CHECK(waitCount(&fixture, &fixture.trace.statusCount, statusBefore + 1, WAIT_MS)); pthread_mutex_lock(&fixture.trace.mutex); CHECK(!fixture.trace.status.available); pthread_mutex_unlock(&fixture.trace.mutex); - CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, 350)); + CHECK(readClientControl(&fixture, &c2hMessage, c2hPayload, 350)); CHECK(c2hMessage.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); const KVMFRAudioClaim originalClaim = fixture.claim; @@ -1147,10 +1506,8 @@ static bool testDataPlane(void) KVMFR_AUDIO_SAMPLE_S16_LE); const unsigned availableBefore = atomic_load_explicit( &fixture.trace.statusCount, memory_order_acquire); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_START, - UINT64_C(0x500000001), 0, 0, 0, 0, 0, - &playFormat, sizeof(playFormat))); - CHECK(sendStatus(&fixture, true, + CHECK(sendStateSnapshot(&fixture, true, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, UINT64_C(0x500000001), &playFormat, 0, NULL)); @@ -1169,25 +1526,23 @@ static bool testDataPlane(void) &fixture.trace.playDataCount, memory_order_acquire); for (unsigned i = 0; i < KVMFR_AUDIO_STREAM_SLOT_COUNT; ++i) { - LGMPStreamBuffer buffer = { 0 }; - LGMP_STATUS status = lgmpHostStreamWriteAcquire(fixture.h2c, &buffer); - if (status == LGMP_ERR_STREAM_FULL) - break; - CHECK(status == LGMP_OK); + LGMPSPMCBuffer buffer = { 0 }; + CHECK(lgmpHostSPMCWriteAcquire( + fixture.playback, &buffer) == LGMP_OK); const KVMFRAudioMessage message = { .version = KVMFR_AUDIO_MESSAGE_VERSION, .size = sizeof(KVMFRAudioMessage), .type = KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, .streamGeneration = UINT64_C(0x500000001), - .sequence = fixture.hostSequence++, + .sequence = buffer.sequence, .frames = 1, .length = playFormat.blockAlign, }; memcpy(buffer.data, &message, sizeof(message)); memset((uint8_t *)buffer.data + sizeof(message), 0x77, message.length); - CHECK(lgmpHostStreamWriteCommit(fixture.h2c, &buffer, + CHECK(lgmpHostSPMCWriteCommit(fixture.playback, &buffer, sizeof(message) + message.length) == LGMP_OK); } fixture.ops->detach(fixture.audio); @@ -1195,11 +1550,11 @@ static bool testDataPlane(void) &fixture.trace.playDataCount, memory_order_acquire); CHECK(afterDetach >= playBeforeDetach); fixture.ops->setStatusListener(fixture.audio, NULL, NULL); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, &c2hMessage, c2hPayload, WAIT_MS)); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, + CHECK(sendHostControl(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, 1, 0, 0, 0, 0, 0, NULL, 0)); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); struct timespec after = { .tv_nsec = 30000000L }; nanosleep(&after, NULL); CHECK(atomic_load_explicit(&fixture.trace.playDataCount, @@ -1222,16 +1577,15 @@ static bool testReleaseIsFinal(void) KVMFRAudioMessage message; uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(readInitialSubscription(&fixture)); + CHECK(readClientControl(&fixture, &message, payload, WAIT_MS)); CHECK(message.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); const uint64_t captureWire = UINT64_C(0x700000001); const KVMFRAudioFormat captureFormat = makeFormat( KVMFR_AUDIO_CHANNEL_TOP_BACK_RIGHT, KVMFR_AUDIO_SAMPLE_S24_LE); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_START, - captureWire, 0, 0, 0, 0, 0, - &captureFormat, sizeof(captureFormat))); - CHECK(sendStatus(&fixture, true, KVMFR_AUDIO_CAP_CAPTURE, + CHECK(sendStateSnapshot(&fixture, true, + KVMFR_AUDIO_CAP_CAPTURE, KVMFR_AUDIO_CAP_CAPTURE, 0, NULL, captureWire, &captureFormat)); CHECK(waitCount(&fixture, &fixture.trace.statusCount, 2, WAIT_MS)); CHECK(fixture.ops->attach(fixture.audio, &eventOps, &fixture.trace)); @@ -1250,38 +1604,27 @@ static bool testReleaseIsFinal(void) pthread_t stressThread; CHECK(!pthread_create(&stressThread, NULL, recordStress, &stress)); - CHECK(sendStatus(&fixture, true, KVMFR_AUDIO_CAP_CAPTURE, - 0, NULL, captureWire, &captureFormat)); + CHECK(sendCurrentStatus(&fixture)); pthread_t listenerThread; CHECK(!pthread_create(&listenerThread, NULL, removeStatusListener, &stress)); CHECK(!pthread_join(listenerThread, NULL)); - bool released = false; - const uint64_t releaseDeadline = microtime() + WAIT_MS * 1000U; - while (microtime() < releaseDeadline && !released) - { - CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); - released = message.type == KVMFR_AUDIO_MESSAGE_RELEASE; - } - CHECK(released); - - const uint64_t trailingDeadline = microtime() + 30000U; - while (microtime() < trailingDeadline) - { - if (!readC2H(&fixture, &message, payload, 2)) - continue; - CHECK(message.type != KVMFR_AUDIO_MESSAGE_CAPTURE_DATA); - } - CHECK(!fixture.ops->recordData(fixture.audio, - captureLocal, payload, 6, NULL)); atomic_store_explicit(&stress.stop, true, memory_order_release); CHECK(!pthread_join(stressThread, NULL)); CHECK(atomic_load_explicit( &stress.successes, memory_order_acquire) > 0); - CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_STOP, + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + &message, payload, WAIT_MS)); + while (readCapture(&fixture, &message, payload, 2)) + CHECK(message.type == KVMFR_AUDIO_MESSAGE_CAPTURE_DATA); + CHECK(!fixture.ops->recordData(fixture.audio, + captureLocal, payload, 6, NULL)); + CHECK(!readCapture(&fixture, &message, payload, 30)); + + CHECK(sendHostControl(&fixture, KVMFR_AUDIO_MESSAGE_CAPTURE_STOP, captureWire, 0, 0, 0, 0, 0, NULL, 0)); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); lgmpAudio_disconnect(fixture.audio); fixtureCleanup(&fixture); @@ -1299,15 +1642,16 @@ static bool testFaultReleaseReclaim(void) KVMFRAudioMessage message; uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(readInitialSubscription(&fixture)); + CHECK(readClientControl(&fixture, &message, payload, WAIT_MS)); CHECK(message.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); - CHECK(message.sequence == 0); + CHECK(message.sequence == 1); - CHECK(sendH2C(&fixture, UINT32_C(0xffffffff), + CHECK(sendHostControl(&fixture, UINT32_C(0xffffffff), 0, 0, 0, 0, 0, 0, NULL, 0)); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, &message, payload, WAIT_MS)); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); const KVMFRAudioClaim first = fixture.claim; struct timespec retryDelay = { .tv_nsec = 250000000L }; @@ -1333,17 +1677,18 @@ static bool testBoundDisconnect(void) KVMFRAudioMessage message; uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_KEEPALIVE, + CHECK(readInitialSubscription(&fixture)); + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_KEEPALIVE, &message, payload, WAIT_MS)); struct DisconnectTask task = { .audio = fixture.audio }; atomic_init(&task.done, false); pthread_t thread; CHECK(!pthread_create(&thread, NULL, disconnectAudio, &task)); - CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + CHECK(readClientControlType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, &message, payload, WAIT_MS)); CHECK(!atomic_load_explicit(&task.done, memory_order_acquire)); - CHECK(unbindBoth(&fixture)); + CHECK(unbindStreams(&fixture)); CHECK(waitDone(&fixture, &task.done, WAIT_MS)); CHECK(!pthread_join(thread, NULL)); diff --git a/client/tests/lgmp_transport_test.c b/client/tests/lgmp_transport_test.c index d0deca2d..e95ed181 100644 --- a/client/tests/lgmp_transport_test.c +++ b/client/tests/lgmp_transport_test.c @@ -22,7 +22,7 @@ #include #ifdef ENABLE_AUDIO -#include "common/KVMFRAudio.h" +#include #endif #include #include diff --git a/client/transports/LGMP/audio.c b/client/transports/LGMP/audio.c index e60b02c1..dc2c3f74 100644 --- a/client/transports/LGMP/audio.c +++ b/client/transports/LGMP/audio.c @@ -27,6 +27,7 @@ #include "common/time.h" #include +#include #include #include @@ -48,8 +49,11 @@ (KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | \ KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) +#define AUDIO_SUBSCRIPTIONS \ + (KVMFR_AUDIO_SUBSCRIBE_PLAYBACK | KVMFR_AUDIO_SUBSCRIBE_CAPTURE) + #define AUDIO_STATUS_FLAGS \ - (KVMFR_AUDIO_STATUS_AVAILABLE | KVMFR_AUDIO_STATUS_HAS_OWNER | \ + (KVMFR_AUDIO_STATUS_AVAILABLE | KVMFR_AUDIO_STATUS_BOUND | \ KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE | \ KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE | \ KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE | \ @@ -123,7 +127,8 @@ struct LGMPAudio LGThread * thread; LG_Lock lock; LG_Lock eventLock; - atomic_flag writeGate; + atomic_flag controlWriteGate; + atomic_flag captureWriteGate; atomic_bool stop; atomic_bool dataPlaneLost; atomic_bool captureDiscontinuity; @@ -138,12 +143,18 @@ struct LGMPAudio bool statusDirty; bool responsePending; bool claimPosting; - bool sequenceKnown; + bool controlSequenceKnown; + bool playbackSequenceKnown; bool playbackDiscontinuity; + bool playbackReadActive; + bool stateAcknowledged; + bool stateAckPending; + bool subscriptionPending; bool releaseRequested; bool releaseSent; - bool hostReadActive; - bool clientWriteActive; + bool hostControlActive; + bool clientControlActive; + bool captureWriteActive; enum AudioPhase phase; uint32_t clientID; uint32_t requestSerial; @@ -154,8 +165,15 @@ struct LGMPAudio uint64_t releaseDeadline; uint64_t nextKeepalive; uint64_t nextAttachAt; - uint64_t expectedSequence; + uint64_t expectedControlSequence; + uint64_t expectedPlaybackSequence; + uint64_t stateAckGeneration; + uint32_t bindingEpoch; + uint32_t roleEpoch; + uint32_t playbackReaderEpoch; uint32_t generationSerial; + KVMFRAudioStateBarrier stateAck; + KVMFRAudioSubscription subscription; struct AudioGeneration endpointGeneration; KVMFRAudioClaim claim; KVMFRAudioStreamResponse response; @@ -163,14 +181,23 @@ struct LGMPAudio struct AudioStreamState playback; struct AudioStreamState capture; - PLGMPClientStream hostToClientStream; - PLGMPClientStream clientToHostStream; + PLGMPClientSPMC playbackStream; + PLGMPClientStream hostControlStream; + PLGMPClientStream clientControlStream; + PLGMPClientStream captureStream; LGMPStreamPollState streamPoll; + uint8_t playbackRecord[KVMFR_AUDIO_PLAYBACK_SLOT_BYTES]; - /* Protected by writeGate. */ - uint64_t clientSequence; - bool writeBound; + /* Protected by the corresponding write gate. Shared admission fields are + * changed only while both gates are held. */ + uint64_t clientControlSequence; + uint64_t captureSequence; + bool controlWriteBound; + bool captureWriteBound; bool publicDataAllowed; + KVMFRAudioCapabilities writeGrantedCapabilities; + uint32_t writeBindingEpoch; + uint32_t writeRoleEpoch; uint64_t writeCaptureGeneration; uint32_t writeCaptureLocalGeneration; KVMFRAudioFormat writeCaptureFormat; @@ -185,15 +212,26 @@ struct LGMPAudio static _Thread_local struct AudioStatusCallbackFrame * l_statusFrames; +_Static_assert(sizeof(KVMFRCursor) == KVMFR_AUDIO_CURSOR_COMPAT_BYTES, + "KVMFR cursor compatibility layout changed"); _Static_assert(sizeof(struct LGMPStreamDescriptor) == sizeof(KVMFRStreamDescriptor), "LGMP and KVMFR stream descriptor sizes differ"); +_Static_assert(sizeof(struct LGMPSPMCDescriptor) == + sizeof(KVMFRSPMCDescriptor), + "LGMP and KVMFR SPMC descriptor sizes differ"); _Static_assert(offsetof(struct LGMPStreamDescriptor, offset) == offsetof(KVMFRStreamDescriptor, offset), "LGMP and KVMFR stream descriptor offsets differ"); _Static_assert(offsetof(struct LGMPStreamDescriptor, slotSize) == offsetof(KVMFRStreamDescriptor, slotSize), "LGMP and KVMFR stream descriptor geometry differs"); +_Static_assert(offsetof(struct LGMPSPMCDescriptor, offset) == + offsetof(KVMFRSPMCDescriptor, offset), + "LGMP and KVMFR SPMC descriptor offsets differ"); +_Static_assert(offsetof(struct LGMPSPMCDescriptor, maxReaders) == + offsetof(KVMFRSPMCDescriptor, maxReaders), + "LGMP and KVMFR SPMC descriptor geometry differs"); _Static_assert(KVMFR_AUDIO_MAX_CHANNELS <= LG_AUDIO_MAX_CHANNELS, "client audio cannot represent every KVMFR speaker position"); @@ -220,28 +258,60 @@ static bool randomBytes(void * buffer, size_t size) return true; } -static bool tryWriteGate(LGMPAudio * audio) +static bool tryControlWriteGate(LGMPAudio * audio) { return !atomic_flag_test_and_set_explicit( - &audio->writeGate, memory_order_acquire); + &audio->controlWriteGate, memory_order_acquire); } -static void lockWriteGate(LGMPAudio * audio) +static void lockControlWriteGate(LGMPAudio * audio) { - while (!tryWriteGate(audio)) + while (!tryControlWriteGate(audio)) { } } -static void unlockWriteGate(LGMPAudio * audio) +static void unlockControlWriteGate(LGMPAudio * audio) { - atomic_flag_clear_explicit(&audio->writeGate, memory_order_release); + atomic_flag_clear_explicit( + &audio->controlWriteGate, memory_order_release); } -/* writeGate must be held. */ +static void lockCaptureWriteGate(LGMPAudio * audio) +{ + while (atomic_flag_test_and_set_explicit( + &audio->captureWriteGate, memory_order_acquire)) + { + } +} + +static void unlockCaptureWriteGate(LGMPAudio * audio) +{ + atomic_flag_clear_explicit( + &audio->captureWriteGate, memory_order_release); +} + +/* Lifecycle updates acquire the capture gate before the control gate. This + * keeps capture backpressure from suppressing clock feedback. */ +static void lockWriteGates(LGMPAudio * audio) +{ + lockCaptureWriteGate(audio); + lockControlWriteGate(audio); +} + +static void unlockWriteGates(LGMPAudio * audio) +{ + unlockControlWriteGate(audio); + unlockCaptureWriteGate(audio); +} + +/* Both write gates must be held. */ static void closePublicDataLocked(LGMPAudio * audio) { audio->publicDataAllowed = false; + audio->writeGrantedCapabilities = 0; + audio->writeBindingEpoch = 0; + audio->writeRoleEpoch = 0; audio->writeCaptureGeneration = 0; audio->writeCaptureLocalGeneration = 0; audio->writePlaybackGeneration = 0; @@ -520,8 +590,25 @@ static struct LGMPStreamDescriptor descriptorFromWire( }; } +static struct LGMPSPMCDescriptor spmcDescriptorFromWire( + const KVMFRSPMCDescriptor * wire) +{ + return (struct LGMPSPMCDescriptor) + { + .magic = wire->magic, + .version = wire->version, + .size = wire->size, + .offset = wire->offset, + .regionSize = wire->regionSize, + .slotCount = wire->slotCount, + .slotSize = wire->slotSize, + .maxReaders = wire->maxReaders, + .reserved = wire->reserved, + }; +} + static bool validDescriptor(const KVMFRStreamDescriptor * descriptor, - enum LGMPStreamDirection direction) + enum LGMPStreamDirection direction, uint32_t slotSize) { return descriptor->magic == LGMP_STREAM_DESCRIPTOR_MAGIC && descriptor->version == LGMP_STREAM_DESCRIPTOR_VERSION && @@ -530,7 +617,19 @@ static bool validDescriptor(const KVMFRStreamDescriptor * descriptor, descriptor->direction == (uint32_t)direction && descriptor->policy == LGMP_STREAM_RELIABLE_FIFO && descriptor->slotCount == KVMFR_AUDIO_STREAM_SLOT_COUNT && - descriptor->slotSize == KVMFR_AUDIO_STREAM_SLOT_BYTES; + descriptor->slotSize == slotSize; +} + +static bool validSPMCDescriptor(const KVMFRSPMCDescriptor * descriptor) +{ + return descriptor->magic == LGMP_SPMC_DESCRIPTOR_MAGIC && + descriptor->version == LGMP_SPMC_DESCRIPTOR_VERSION && + descriptor->size == sizeof(*descriptor) && descriptor->offset && + descriptor->regionSize && + descriptor->slotCount == KVMFR_AUDIO_STREAM_SLOT_COUNT && + descriptor->slotSize == KVMFR_AUDIO_PLAYBACK_SLOT_BYTES && + descriptor->maxReaders == KVMFR_AUDIO_MAX_PLAYBACK_READERS && + !descriptor->reserved; } static void markStatusNL(LGMPAudio * audio, bool available, @@ -544,7 +643,7 @@ static void markStatusNL(LGMPAudio * audio, bool available, audio->statusDirty = true; audio->statusAvailable = available; atomic_store_explicit(&audio->clockFeedbackAvailable, - available && (audio->remoteStatus.capabilities & + available && (audio->remoteStatus.grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK), memory_order_release); } @@ -577,14 +676,24 @@ static void publishStatus(LGMPAudio * audio) static void updateWriteStreamState(LGMPAudio * audio) { - lockWriteGate(audio); + lockWriteGates(audio); LG_LOCK(audio->lock); - const bool admitted = audio->writeBound && + const bool admitted = audio->controlWriteBound && audio->phase == AUDIO_PHASE_BOUND && !audio->releaseRequested; - const uint64_t captureGeneration = admitted && audio->capture.active ? + const bool playbackAdmitted = admitted && audio->stateAcknowledged; + const KVMFRAudioCapabilities granted = + admitted && audio->statusAvailable ? + audio->remoteStatus.grantedCapabilities : 0; + const bool captureGranted = + (granted & KVMFR_AUDIO_CAP_CAPTURE) != 0 && + (audio->subscription.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_CAPTURE) != 0; + const uint64_t captureGeneration = admitted && captureGranted && + audio->captureWriteBound && audio->capture.active ? audio->capture.generation.wire : 0; const uint32_t captureLocalGeneration = - admitted && audio->capture.active ? + admitted && captureGranted && audio->captureWriteBound && + audio->capture.active ? audio->capture.generation.local : 0; const bool captureChanged = audio->writeCaptureGeneration != captureGeneration || @@ -592,18 +701,29 @@ static void updateWriteStreamState(LGMPAudio * audio) audio->writeCaptureGeneration = captureGeneration; audio->writeCaptureLocalGeneration = captureLocalGeneration; audio->writeCaptureFormat = audio->capture.wireFormat; - audio->writePlaybackGeneration = admitted && audio->playback.active ? + audio->writePlaybackGeneration = playbackAdmitted && + (granted & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + (audio->subscription.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_PLAYBACK) && + audio->playback.active ? audio->playback.generation.wire : 0; audio->writePlaybackLocalGeneration = - admitted && audio->playback.active ? + playbackAdmitted && + (granted & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + (audio->subscription.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_PLAYBACK) && + audio->playback.active ? audio->playback.generation.local : 0; + audio->writeGrantedCapabilities = granted; + audio->writeBindingEpoch = admitted ? audio->bindingEpoch : 0; + audio->writeRoleEpoch = admitted ? audio->roleEpoch : 0; audio->publicDataAllowed = admitted && (audio->writeCaptureGeneration || audio->writePlaybackGeneration); LG_UNLOCK(audio->lock); if (captureChanged) atomic_store_explicit( &audio->captureDiscontinuity, false, memory_order_release); - unlockWriteGate(audio); + unlockWriteGates(audio); } static void stopActiveStreams(LGMPAudio * audio) @@ -636,9 +756,13 @@ static void stopActiveStreams(LGMPAudio * audio) static void detachStreams(LGMPAudio * audio) { - lockWriteGate(audio); + lockWriteGates(audio); audio->publicDataAllowed = false; - audio->writeBound = false; + audio->controlWriteBound = false; + audio->captureWriteBound = false; + audio->writeGrantedCapabilities = 0; + audio->writeBindingEpoch = 0; + audio->writeRoleEpoch = 0; audio->writeCaptureGeneration = 0; audio->writeCaptureLocalGeneration = 0; audio->writePlaybackGeneration = 0; @@ -646,14 +770,21 @@ static void detachStreams(LGMPAudio * audio) memset(&audio->writeCaptureFormat, 0, sizeof(audio->writeCaptureFormat)); - lgmpClientStreamDetach(&audio->clientToHostStream); - lgmpClientStreamDetach(&audio->hostToClientStream); - audio->hostReadActive = false; - audio->clientWriteActive = false; - audio->clientSequence = 0; + lgmpClientStreamDetach(&audio->captureStream); + lgmpClientStreamDetach(&audio->clientControlStream); + lgmpClientStreamDetach(&audio->hostControlStream); + lgmpClientSPMCDetach(&audio->playbackStream); + audio->hostControlActive = false; + audio->clientControlActive = false; + audio->captureWriteActive = false; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + audio->playbackReaderEpoch = 0; + audio->clientControlSequence = 0; + audio->captureSequence = 0; atomic_store_explicit( &audio->captureDiscontinuity, false, memory_order_release); - unlockWriteGate(audio); + unlockWriteGates(audio); } static void scheduleRetryNL(LGMPAudio * audio, uint64_t delay) @@ -678,8 +809,18 @@ static void finishBinding(LGMPAudio * audio, uint64_t delay) LG_LOCK(audio->lock); memset(&audio->remoteStatus, 0, sizeof(audio->remoteStatus)); - audio->sequenceKnown = false; - audio->expectedSequence = 0; + audio->controlSequenceKnown = false; + audio->playbackSequenceKnown = false; + audio->expectedControlSequence = 0; + audio->expectedPlaybackSequence = 0; + audio->stateAckPending = false; + audio->stateAcknowledged = false; + audio->subscriptionPending = false; + audio->bindingEpoch = 0; + audio->roleEpoch = 0; + audio->stateAckGeneration = 0; + memset(&audio->stateAck, 0, sizeof(audio->stateAck)); + memset(&audio->subscription, 0, sizeof(audio->subscription)); audio->releaseRequested = false; audio->releaseSent = false; audio->responsePending = false; @@ -703,104 +844,220 @@ static void finishBinding(LGMPAudio * audio, uint64_t delay) static bool attachStreams(LGMPAudio * audio, const KVMFRAudioStreamResponse * response) { - if (!validDescriptor(&response->hostToClient, - LGMP_STREAM_HOST_TO_CLIENT) || - !validDescriptor(&response->clientToHost, - LGMP_STREAM_CLIENT_TO_HOST)) + if (!validSPMCDescriptor(&response->playback) || + !validDescriptor(&response->hostControl, + LGMP_STREAM_HOST_TO_CLIENT, + KVMFR_AUDIO_HOST_CONTROL_SLOT_BYTES) || + !validDescriptor(&response->clientControl, + LGMP_STREAM_CLIENT_TO_HOST, + KVMFR_AUDIO_CLIENT_CONTROL_SLOT_BYTES) || + !validDescriptor(&response->capture, + LGMP_STREAM_CLIENT_TO_HOST, KVMFR_AUDIO_CAPTURE_SLOT_BYTES)) { DEBUG_WARN("Invalid LGMP audio stream descriptors"); return false; } - LGMP_STATUS hostStatus = LGMP_OK; - if (!audio->hostToClientStream) + LGMP_STATUS playbackStatus = LGMP_OK; + if (!audio->playbackStream) { - const struct LGMPStreamDescriptor hostToClient = - descriptorFromWire(&response->hostToClient); + const struct LGMPSPMCDescriptor playback = + spmcDescriptorFromWire(&response->playback); + playbackStatus = lgmpClientSPMCAttach(audio->client, &playback, + response->playbackReaderID, &audio->playbackStream); + } + uint32_t playbackEpoch = 0; + if (playbackStatus == LGMP_OK) + playbackStatus = lgmpClientSPMCActivate( + audio->playbackStream, &playbackEpoch); + bool playbackOwner = playbackStatus == LGMP_OK; + if (playbackOwner) + { + uint32_t clientID = 0; + uint32_t epoch = 0; + playbackOwner = lgmpClientSPMCGetBinding(audio->playbackStream, + &clientID, &epoch) == LGMP_OK && clientID == audio->clientID && + epoch && epoch == playbackEpoch; + } + if (playbackStatus != LGMP_OK) + DEBUG_WARN("Failed to attach/activate playback audio stream: %s", + lgmpStatusString(playbackStatus)); + audio->playbackReaderEpoch = playbackOwner ? playbackEpoch : 0; + audio->playbackReadActive = false; + + LGMP_STATUS hostStatus = LGMP_OK; + if (!audio->hostControlStream) + { + const struct LGMPStreamDescriptor descriptor = + descriptorFromWire(&response->hostControl); hostStatus = lgmpClientStreamAttach(audio->client, - &hostToClient, &audio->hostToClientStream); + &descriptor, &audio->hostControlStream); } if (hostStatus == LGMP_OK) { uint32_t epoch = 0; hostStatus = lgmpClientStreamActivate( - audio->hostToClientStream, &epoch); - audio->hostReadActive = hostStatus == LGMP_OK; + audio->hostControlStream, &epoch); + audio->hostControlActive = hostStatus == LGMP_OK; } - if (hostStatus != LGMP_OK) - DEBUG_WARN("Failed to attach/activate host-to-client audio stream: %s", - lgmpStatusString(hostStatus)); - - bool hostOwner = audio->hostReadActive; + bool hostOwner = audio->hostControlActive; if (hostOwner) { uint32_t clientID = 0; uint32_t epoch = 0; hostOwner = lgmpClientStreamGetBinding( - audio->hostToClientStream, &clientID, &epoch) == LGMP_OK && + audio->hostControlStream, &clientID, &epoch) == LGMP_OK && clientID == audio->clientID && epoch; } + if (hostStatus != LGMP_OK) + DEBUG_WARN("Failed to attach/activate host audio control stream: %s", + lgmpStatusString(hostStatus)); + audio->hostControlActive = hostOwner; - lockWriteGate(audio); + lockWriteGates(audio); LGMP_STATUS clientStatus = LGMP_OK; - if (!audio->clientToHostStream) + if (!audio->clientControlStream) { - const struct LGMPStreamDescriptor clientToHost = - descriptorFromWire(&response->clientToHost); + const struct LGMPStreamDescriptor descriptor = + descriptorFromWire(&response->clientControl); clientStatus = lgmpClientStreamAttach(audio->client, - &clientToHost, &audio->clientToHostStream); + &descriptor, &audio->clientControlStream); } if (clientStatus == LGMP_OK) { uint32_t epoch = 0; clientStatus = lgmpClientStreamActivate( - audio->clientToHostStream, &epoch); - audio->clientWriteActive = clientStatus == LGMP_OK; + audio->clientControlStream, &epoch); + audio->clientControlActive = clientStatus == LGMP_OK; } - bool clientOwner = audio->clientWriteActive; + bool clientOwner = audio->clientControlActive; if (clientOwner) { uint32_t clientID = 0; uint32_t epoch = 0; clientOwner = lgmpClientStreamGetBinding( - audio->clientToHostStream, &clientID, &epoch) == LGMP_OK && + audio->clientControlStream, &clientID, &epoch) == LGMP_OK && clientID == audio->clientID && epoch; } - audio->clientWriteActive = clientOwner; - const bool wasWriteBound = audio->writeBound; - audio->writeBound = clientOwner; - if (audio->writeBound && !wasWriteBound) - audio->clientSequence = 0; - unlockWriteGate(audio); + audio->clientControlActive = clientOwner; + const bool wasControlBound = audio->controlWriteBound; + audio->controlWriteBound = clientOwner; + if (audio->controlWriteBound && !wasControlBound) + audio->clientControlSequence = 0; + + LGMP_STATUS captureStatus = LGMP_OK; + if (!audio->captureStream) + { + const struct LGMPStreamDescriptor descriptor = + descriptorFromWire(&response->capture); + captureStatus = lgmpClientStreamAttach(audio->client, + &descriptor, &audio->captureStream); + } + const bool captureGranted = + (response->grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE) != 0; + if (captureStatus == LGMP_OK && captureGranted) + { + uint32_t epoch = 0; + captureStatus = lgmpClientStreamActivate( + audio->captureStream, &epoch); + audio->captureWriteActive = captureStatus == LGMP_OK; + } + else + audio->captureWriteActive = false; + bool captureOwner = captureStatus == LGMP_OK && + (!captureGranted || audio->captureWriteActive); + if (captureGranted && captureOwner) + { + uint32_t clientID = 0; + uint32_t epoch = 0; + captureOwner = lgmpClientStreamGetBinding(audio->captureStream, + &clientID, &epoch) == LGMP_OK && clientID == audio->clientID && epoch; + } + audio->captureWriteActive = captureGranted && captureOwner; + const bool wasCaptureBound = audio->captureWriteBound; + audio->captureWriteBound = audio->captureWriteActive; + if (audio->captureWriteBound && !wasCaptureBound) + audio->captureSequence = 0; + unlockWriteGates(audio); if (clientStatus != LGMP_OK) - DEBUG_WARN("Failed to attach/activate client-to-host audio stream: %s", + DEBUG_WARN("Failed to attach/activate client audio control stream: %s", lgmpStatusString(clientStatus)); - audio->hostReadActive = hostOwner; + if (captureStatus != LGMP_OK) + DEBUG_WARN("Failed to attach/activate capture audio stream: %s", + lgmpStatusString(captureStatus)); lgmpStreamPollActivity(&audio->streamPoll); - return hostOwner && clientOwner; + return playbackOwner && hostOwner && clientOwner && captureOwner; +} + +static bool refreshCaptureBinding(LGMPAudio * audio, bool granted) +{ + lockCaptureWriteGate(audio); + if (!granted) + { + audio->captureWriteActive = false; + audio->captureWriteBound = false; + unlockCaptureWriteGate(audio); + return true; + } + + LGMP_STATUS status = audio->captureStream ? + LGMP_OK : LGMP_ERR_STREAM_UNBOUND; + if (status == LGMP_OK) + { + uint32_t epoch = 0; + status = lgmpClientStreamActivate(audio->captureStream, &epoch); + } + bool owner = status == LGMP_OK; + if (owner) + { + uint32_t clientID = 0; + uint32_t epoch = 0; + status = lgmpClientStreamGetBinding(audio->captureStream, + &clientID, &epoch); + owner = status == LGMP_OK && clientID == audio->clientID && epoch; + if (status == LGMP_OK && !owner) + status = LGMP_ERR_STREAM_STALE; + } + const bool wasBound = audio->captureWriteBound; + audio->captureWriteActive = owner; + audio->captureWriteBound = owner; + if (owner && !wasBound) + audio->captureSequence = 0; + unlockCaptureWriteGate(audio); + + if (!owner) + DEBUG_WARN("Failed to activate granted capture audio stream: %s", + lgmpStatusString(status)); + return owner; +} + +static bool parseRecord(const void * data, uint32_t size, uint32_t capacity, + KVMFRAudioMessage * message, const uint8_t ** payload) +{ + if (!data || size < sizeof(*message) || size > capacity) + return false; + memcpy(message, data, sizeof(*message)); + if (message->version != KVMFR_AUDIO_MESSAGE_VERSION || + message->size != sizeof(*message) || message->reserved0 || + message->length > KVMFR_AUDIO_PCM_BYTES || + size != sizeof(*message) + message->length) + return false; + *payload = (const uint8_t *)data + sizeof(*message); + return true; } static bool parseMessage(const LGMPStreamBuffer * buffer, KVMFRAudioMessage * message, const uint8_t ** payload) { - 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 || - buffer->size != sizeof(*message) + message->length) - return false; - *payload = (const uint8_t *)buffer->data + sizeof(*message); - return true; + return parseRecord(buffer->data, buffer->size, buffer->capacity, + message, payload); } static void requestRelease(LGMPAudio * audio, const char * reason) { bool log = false; - lockWriteGate(audio); + lockWriteGates(audio); LG_LOCK(audio->lock); closePublicDataLocked(audio); if (!audio->releaseRequested) @@ -811,12 +1068,16 @@ static void requestRelease(LGMPAudio * audio, const char * reason) markStatusNL(audio, false, 0); log = true; } + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + audio->stateAckPending = false; + audio->subscriptionPending = false; if (audio->phase == AUDIO_PHASE_BOUND) audio->phase = AUDIO_PHASE_RELEASING; LG_UNLOCK(audio->lock); - unlockWriteGate(audio); + unlockWriteGates(audio); if (log) - DEBUG_INFO("LGMP audio ownership releasing: %s", reason); + DEBUG_INFO("LGMP audio binding releasing: %s", reason); signalWorker(audio); } @@ -828,14 +1089,15 @@ static bool validEmptyMessage(const KVMFRAudioMessage * message, !message->frames && !message->length; } -static bool validVolume(const KVMFRAudioVolume * volume, - uint8_t channels) +static bool validVolumePayload(const KVMFRAudioVolume * volume) { if (volume->version != KVMFR_AUDIO_VOLUME_VERSION || volume->size != sizeof(*volume) || volume->reserved || - volume->channelCount != channels) + !volume->channelCount || + volume->channelCount > KVMFR_AUDIO_STREAM_MAX_CHANNELS) return false; - for (unsigned i = channels; i < KVMFR_AUDIO_MAX_CHANNELS; ++i) + for (unsigned i = volume->channelCount; + i < KVMFR_AUDIO_MAX_CHANNELS; ++i) if (volume->values[i]) return false; return true; @@ -850,14 +1112,44 @@ static bool validStatus(const LGMPAudio * audio, status->protocolVersion != KVMFR_AUDIO_PROTOCOL_MAX || status->requestGeneration != audio->requestGeneration || (status->capabilities & ~AUDIO_CAPABILITIES) || - status->ownerClientID != audio->clientID || - status->ownerGeneration != audio->requestGeneration || - !(status->flags & KVMFR_AUDIO_STATUS_HAS_OWNER) || - status->reserved0 || !status->endpointGeneration || + (status->grantedCapabilities & ~status->capabilities) || + (status->grantedCapabilities & ~audio->claim.clientCapabilities) || + status->bindingEpoch != audio->bindingEpoch || + !status->roleEpoch || + status->boundClientID != audio->clientID || + !(status->flags & KVMFR_AUDIO_STATUS_BOUND) || + status->reserved0 || status->reserved1 || + !status->endpointGeneration || status->playbackState > KVMFR_AUDIO_STREAM_RUNNING || status->captureState > KVMFR_AUDIO_STREAM_RUNNING) return false; + if (audio->roleEpoch && + (int32_t)(status->roleEpoch - audio->roleEpoch) < 0) + return false; + + const bool playbackGranted = + (status->grantedCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) != 0; + const bool captureGranted = + (status->grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE) != 0; + const bool feedbackGranted = + (status->grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) != 0; + if ((feedbackGranted && !playbackGranted) || + (!(status->capabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + (status->feedbackAuthorityClientID || + status->feedbackAuthorityEpoch)) || + (!(status->capabilities & KVMFR_AUDIO_CAP_CAPTURE) && + status->captureOwnerClientID) || + (!!status->feedbackAuthorityClientID != + !!status->feedbackAuthorityEpoch) || + (feedbackGranted != + (status->feedbackAuthorityClientID == audio->clientID)) || + (status->feedbackAuthorityClientID == audio->clientID && + status->feedbackAuthorityEpoch != status->roleEpoch) || + (captureGranted != + (status->captureOwnerClientID == audio->clientID))) + return false; + const bool playbackAvailable = (status->flags & KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE) != 0; const bool captureAvailable = @@ -876,12 +1168,14 @@ static bool validStatus(const LGMPAudio * audio, (status->flags & KVMFR_AUDIO_STATUS_AVAILABLE) != 0; LG_AudioFormat ignored; if (playbackActive && - (!available || !playbackAvailable || !status->playbackGeneration || + (!available || !playbackAvailable || !playbackGranted || + !status->playbackGeneration || status->playbackState == KVMFR_AUDIO_STREAM_STOPPED || !validFormat(&status->playbackFormat, &ignored))) return false; if (captureActive && - (!available || !captureAvailable || !status->captureGeneration || + (!available || !captureAvailable || !captureGranted || + !status->captureGeneration || status->captureState == KVMFR_AUDIO_STREAM_STOPPED || !validFormat(&status->captureFormat, &ignored))) return false; @@ -893,6 +1187,35 @@ static bool validStatus(const LGMPAudio * audio, return true; } +static bool statusRequiresBarrier(const KVMFRAudioStatus * current, + const KVMFRAudioStatus * next) +{ + if (current->version != KVMFR_AUDIO_STATUS_VERSION || + current->flags != next->flags || + current->protocolVersion != next->protocolVersion || + current->requestGeneration != next->requestGeneration || + current->capabilities != next->capabilities || + current->grantedCapabilities != next->grantedCapabilities || + current->bindingEpoch != next->bindingEpoch || + current->roleEpoch != next->roleEpoch || + current->endpointGeneration != next->endpointGeneration || + current->playbackGeneration != next->playbackGeneration || + current->captureGeneration != next->captureGeneration || + current->playbackState != next->playbackState || + current->captureState != next->captureState || + !sameFormat(¤t->playbackFormat, &next->playbackFormat) || + !sameFormat(¤t->captureFormat, &next->captureFormat)) + return true; + + if ((next->grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + (current->feedbackAuthorityClientID != + next->feedbackAuthorityClientID || + current->feedbackAuthorityEpoch != next->feedbackAuthorityEpoch)) + return true; + + return false; +} + static bool currentPlaybackNL(const LGMPAudio * audio, uint64_t wireGeneration) { @@ -907,6 +1230,34 @@ static bool currentCaptureNL(const LGMPAudio * audio, audio->capture.generation.wire == wireGeneration; } +static bool startMatchesStatus(LGMPAudio * audio, + KVMFRAudioMessageType type, uint64_t generation, + const KVMFRAudioFormat * format) +{ + LG_LOCK(audio->lock); + const bool playback = type == KVMFR_AUDIO_MESSAGE_PLAYBACK_START; + const KVMFRAudioCapabilities capability = playback ? + KVMFR_AUDIO_CAP_PLAYBACK : KVMFR_AUDIO_CAP_CAPTURE; + const KVMFRAudioStatusFlags activeFlag = playback ? + KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE : + KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE; + const uint64_t statusGeneration = playback ? + audio->remoteStatus.playbackGeneration : + audio->remoteStatus.captureGeneration; + const KVMFRAudioFormat * statusFormat = playback ? + &audio->remoteStatus.playbackFormat : + &audio->remoteStatus.captureFormat; + const bool result = + audio->remoteStatus.version == KVMFR_AUDIO_STATUS_VERSION && + audio->remoteStatus.bindingEpoch == audio->bindingEpoch && + audio->remoteStatus.roleEpoch == audio->roleEpoch && + (audio->remoteStatus.grantedCapabilities & capability) && + (audio->remoteStatus.flags & activeFlag) && + generation == statusGeneration && sameFormat(format, statusFormat); + LG_UNLOCK(audio->lock); + return result; +} + static void dispatchPlaybackStop(LGMPAudio * audio, uint64_t wireGeneration) { @@ -1187,44 +1538,158 @@ static bool processStatus(LGMPAudio * audio, KVMFRAudioStatus status; memcpy(&status, payload, sizeof(status)); + lockWriteGates(audio); LG_LOCK(audio->lock); const bool valid = validStatus(audio, &status); if (valid) { - const bool playbackActive = - (status.flags & KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE) != 0; - const bool captureActive = - (status.flags & KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) != 0; - if (playbackActive && - (!currentPlaybackNL(audio, status.playbackGeneration) || - !sameFormat(&audio->playback.wireFormat, - &status.playbackFormat))) - { - LG_UNLOCK(audio->lock); - return false; - } - if (captureActive && - (!currentCaptureNL(audio, status.captureGeneration) || - !sameFormat(&audio->capture.wireFormat, - &status.captureFormat))) - { - LG_UNLOCK(audio->lock); - return false; - } - if ((!playbackActive && audio->playback.active) || - (!captureActive && audio->capture.active)) - { - LG_UNLOCK(audio->lock); - return false; - } - + const bool stateUpdate = + statusRequiresBarrier(&audio->remoteStatus, &status); audio->remoteStatus = status; + audio->roleEpoch = status.roleEpoch; + if (audio->subscriptionPending) + { + audio->subscription.bindingEpoch = status.bindingEpoch; + audio->subscription.roleEpoch = status.roleEpoch; + } + if (stateUpdate) + { + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + audio->stateAckPending = false; + audio->writePlaybackGeneration = 0; + audio->writePlaybackLocalGeneration = 0; + if (!audio->writeCaptureGeneration) + audio->publicDataAllowed = false; + } const bool available = (status.flags & KVMFR_AUDIO_STATUS_AVAILABLE) != 0; markStatusNL(audio, available, status.endpointGeneration); + audio->writeGrantedCapabilities = + available ? status.grantedCapabilities : 0; + audio->writeBindingEpoch = available ? status.bindingEpoch : 0; + audio->writeRoleEpoch = available ? status.roleEpoch : 0; + if (!(status.grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE)) + { + audio->captureWriteActive = false; + audio->captureWriteBound = false; + audio->writeCaptureGeneration = 0; + audio->writeCaptureLocalGeneration = 0; + } + if (!(status.grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)) + { + audio->writePlaybackGeneration = 0; + audio->writePlaybackLocalGeneration = 0; + } } LG_UNLOCK(audio->lock); - return valid; + unlockWriteGates(audio); + if (!valid) + return false; + + const bool captureGranted = + (status.grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE) != 0; + if (!refreshCaptureBinding(audio, captureGranted)) + return false; + updateWriteStreamState(audio); + return true; +} + +static bool processStateBarrier(LGMPAudio * audio, + const KVMFRAudioMessage * message, const uint8_t * payload) +{ + if (message->flags || message->position || message->timeNs || + message->rateQ32 || message->frames || + message->length != sizeof(KVMFRAudioStateBarrier)) + return false; + + KVMFRAudioStateBarrier barrier; + memcpy(&barrier, payload, sizeof(barrier)); + if (!barrier.bindingEpoch || !barrier.roleEpoch || !barrier.stateSerial) + return false; + + lockControlWriteGate(audio); + LG_LOCK(audio->lock); + const bool statusPlaybackActive = + (audio->remoteStatus.flags & + KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE) != 0; + const bool statusCaptureActive = + (audio->remoteStatus.flags & + KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) != 0; + const bool stateInstalled = + (statusPlaybackActive ? + currentPlaybackNL(audio, + audio->remoteStatus.playbackGeneration) && + sameFormat(&audio->playback.wireFormat, + &audio->remoteStatus.playbackFormat) : + !audio->playback.active) && + (statusCaptureActive ? + currentCaptureNL(audio, audio->remoteStatus.captureGeneration) && + sameFormat(&audio->capture.wireFormat, + &audio->remoteStatus.captureFormat) : + !audio->capture.active); + const bool sameBarrier = + barrier.bindingEpoch == audio->stateAck.bindingEpoch && + barrier.roleEpoch == audio->stateAck.roleEpoch && + barrier.stateSerial == audio->stateAck.stateSerial && + message->streamGeneration == audio->stateAckGeneration; + const bool current = audio->phase == AUDIO_PHASE_BOUND && + audio->remoteStatus.version == KVMFR_AUDIO_STATUS_VERSION && + barrier.bindingEpoch == audio->bindingEpoch && + barrier.roleEpoch == audio->roleEpoch && + stateInstalled && + ((statusPlaybackActive && message->streamGeneration == + audio->remoteStatus.playbackGeneration) || + (!statusPlaybackActive && !message->streamGeneration)) && + audio->playbackStream && audio->playbackReaderEpoch && + (sameBarrier || !audio->stateAck.stateSerial || + barrier.stateSerial > audio->stateAck.stateSerial); + if (current && sameBarrier) + audio->stateAckPending = true; + else if (current) + { + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + } + LG_UNLOCK(audio->lock); + if (!current) + { + unlockControlWriteGate(audio); + return false; + } + if (sameBarrier) + { + unlockControlWriteGate(audio); + return true; + } + + uint64_t skipped = 0; + const LGMP_STATUS status = + lgmpClientSPMCSync(audio->playbackStream, &skipped); + if (status != LGMP_OK) + { + DEBUG_WARN("Failed to synchronize playback audio stream: %s", + lgmpStatusString(status)); + unlockControlWriteGate(audio); + return false; + } + + LG_LOCK(audio->lock); + const bool stillCurrent = audio->phase == AUDIO_PHASE_BOUND && + barrier.bindingEpoch == audio->bindingEpoch && + barrier.roleEpoch == audio->roleEpoch; + if (stillCurrent) + { + audio->stateAck = barrier; + audio->stateAckGeneration = message->streamGeneration; + audio->stateAckPending = true; + audio->playbackSequenceKnown = false; + audio->expectedPlaybackSequence = 0; + audio->playbackDiscontinuity = skipped != 0 || audio->playback.active; + } + LG_UNLOCK(audio->lock); + unlockControlWriteGate(audio); + return stillCurrent; } static bool processInboundMessage(LGMPAudio * audio, @@ -1236,6 +1701,9 @@ static bool processInboundMessage(LGMPAudio * audio, case KVMFR_AUDIO_MESSAGE_STATUS: return processStatus(audio, message, payload); + case KVMFR_AUDIO_MESSAGE_STATE_BARRIER: + return processStateBarrier(audio, message, payload); + case KVMFR_AUDIO_MESSAGE_PLAYBACK_START: case KVMFR_AUDIO_MESSAGE_CAPTURE_START: { @@ -1250,6 +1718,9 @@ static bool processInboundMessage(LGMPAudio * audio, memcpy(&wire, payload, sizeof(wire)); if (!validFormat(&wire, &format)) return false; + if (!startMatchesStatus( + audio, message->type, message->streamGeneration, &wire)) + return false; if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_START) dispatchPlaybackStart( audio, message, &wire, &format, sequenceGap); @@ -1279,6 +1750,8 @@ static bool processInboundMessage(LGMPAudio * audio, return false; KVMFRAudioVolume volume; memcpy(&volume, payload, sizeof(volume)); + if (!validVolumePayload(&volume)) + return false; LG_LOCK(audio->lock); const bool current = message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME ? @@ -1291,7 +1764,7 @@ static bool processInboundMessage(LGMPAudio * audio, LG_UNLOCK(audio->lock); if (!current) return true; - if (!validVolume(&volume, channels)) + if (volume.channelCount != channels) return false; if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME) dispatchPlaybackVolume( @@ -1315,22 +1788,27 @@ static bool processInboundMessage(LGMPAudio * audio, (message->flags & KVMFR_AUDIO_MESSAGE_MUTED) != 0); return true; - case KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA: - if (!message->streamGeneration || !message->frames || - message->frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET || - (message->flags & ~AUDIO_DATA_FLAGS) || - !validClockFlags(message->flags & AUDIO_CLOCK_FLAGS) || - (!(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) && - (message->position || message->timeNs || message->rateQ32))) - return false; - dispatchPlaybackData(audio, message, payload, sequenceGap); - return true; - default: return false; } } +static bool processPlaybackMessage(LGMPAudio * audio, + const KVMFRAudioMessage * message, const uint8_t * payload, + bool discontinuity) +{ + if (message->type != KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA || + !message->streamGeneration || !message->frames || + message->frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET || + (message->flags & ~AUDIO_DATA_FLAGS) || + !validClockFlags(message->flags & AUDIO_CLOCK_FLAGS) || + (!(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) && + (message->position || message->timeNs || message->rateQ32))) + return false; + dispatchPlaybackData(audio, message, payload, discontinuity); + return true; +} + enum WriteResult { WRITE_OK, @@ -1339,17 +1817,18 @@ enum WriteResult }; static enum WriteResult writeMessageLocked(LGMPAudio * audio, + PLGMPClientStream stream, uint64_t * sequence, 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 (!audio->writeBound || !audio->clientToHostStream) + if (!stream || !sequence) return WRITE_LOST; LGMPStreamBuffer buffer = { 0 }; LGMP_STATUS status = lgmpClientStreamWriteAcquire( - audio->clientToHostStream, &buffer); + stream, &buffer); if (status == LGMP_ERR_STREAM_FULL) return WRITE_FULL; if (status != LGMP_OK) @@ -1363,7 +1842,7 @@ static enum WriteResult writeMessageLocked(LGMPAudio * audio, if (!buffer.data || buffer.capacity < bytes) { (void)lgmpClientStreamWriteCancel( - audio->clientToHostStream, &buffer); + stream, &buffer); atomic_store_explicit( &audio->dataPlaneLost, true, memory_order_release); return WRITE_LOST; @@ -1376,7 +1855,7 @@ static enum WriteResult writeMessageLocked(LGMPAudio * audio, .type = type, .streamGeneration = streamGeneration, .flags = flags, - .sequence = audio->clientSequence, + .sequence = *sequence, .position = position, .timeNs = timeNs, .rateQ32 = rateQ32, @@ -1388,10 +1867,10 @@ static enum WriteResult writeMessageLocked(LGMPAudio * audio, memcpy((uint8_t *)buffer.data + sizeof(message), payload, length); status = lgmpClientStreamWriteCommit( - audio->clientToHostStream, &buffer, bytes); + stream, &buffer, bytes); if (status == LGMP_OK) { - ++audio->clientSequence; + ++*sequence; return WRITE_OK; } atomic_store_explicit( @@ -1418,15 +1897,34 @@ static bool validResponse(const LGMPAudio * audio, if (response->result == KVMFR_AUDIO_RESPONSE_BOUND) return response->protocolVersion == KVMFR_AUDIO_PROTOCOL_MAX && response->endpointGeneration && - validDescriptor(&response->hostToClient, - LGMP_STREAM_HOST_TO_CLIENT) && - validDescriptor(&response->clientToHost, - LGMP_STREAM_CLIENT_TO_HOST); + response->bindingEpoch && response->roleEpoch && + response->playbackReaderID < KVMFR_AUDIO_MAX_PLAYBACK_READERS && + !(response->grantedCapabilities & ~AUDIO_CAPABILITIES) && + !(response->grantedCapabilities & + ~audio->claim.clientCapabilities) && + (response->grantedCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) && + (!(response->grantedCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) || + (response->grantedCapabilities & KVMFR_AUDIO_CAP_PLAYBACK)) && + validSPMCDescriptor(&response->playback) && + validDescriptor(&response->hostControl, + LGMP_STREAM_HOST_TO_CLIENT, + KVMFR_AUDIO_HOST_CONTROL_SLOT_BYTES) && + validDescriptor(&response->clientControl, + LGMP_STREAM_CLIENT_TO_HOST, + KVMFR_AUDIO_CLIENT_CONTROL_SLOT_BYTES) && + validDescriptor(&response->capture, + LGMP_STREAM_CLIENT_TO_HOST, KVMFR_AUDIO_CAPTURE_SLOT_BYTES); const KVMFRStreamDescriptor empty = { 0 }; + const KVMFRSPMCDescriptor emptySPMC = { 0 }; return !response->protocolVersion && - !memcmp(&response->hostToClient, &empty, sizeof(empty)) && - !memcmp(&response->clientToHost, &empty, sizeof(empty)); + !response->grantedCapabilities && !response->bindingEpoch && + !response->roleEpoch && !response->playbackReaderID && + !memcmp(&response->playback, &emptySPMC, sizeof(emptySPMC)) && + !memcmp(&response->hostControl, &empty, sizeof(empty)) && + !memcmp(&response->clientControl, &empty, sizeof(empty)) && + !memcmp(&response->capture, &empty, sizeof(empty)); } static void processResponse(LGMPAudio * audio) @@ -1505,11 +2003,27 @@ static void processResponse(LGMPAudio * audio) { mapGeneration(audio, &audio->endpointGeneration, response.endpointGeneration); + audio->bindingEpoch = response.bindingEpoch; + audio->roleEpoch = response.roleEpoch; + audio->subscription = (KVMFRAudioSubscription) + { + .bindingEpoch = response.bindingEpoch, + .roleEpoch = response.roleEpoch, + .subscriptions = audio->claim.subscriptions, + }; + audio->subscriptionPending = true; audio->phase = audio->releaseRequested || !audio->discover ? AUDIO_PHASE_RELEASING : AUDIO_PHASE_BOUND; } - audio->sequenceKnown = false; - audio->expectedSequence = 0; + audio->controlSequenceKnown = false; + audio->playbackSequenceKnown = false; + audio->expectedControlSequence = 0; + audio->expectedPlaybackSequence = 0; + audio->stateAckPending = false; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + memset(&audio->stateAck, 0, sizeof(audio->stateAck)); + audio->stateAckGeneration = 0; audio->releaseSent = false; audio->nextKeepalive = microtime(); LG_UNLOCK(audio->lock); @@ -1517,15 +2031,15 @@ static void processResponse(LGMPAudio * audio) response.requestGeneration); } -static unsigned drainInbound(LGMPAudio * audio) +static unsigned drainControl(LGMPAudio * audio) { unsigned drained = 0; - while (audio->hostToClientStream && audio->hostReadActive && + while (audio->hostControlStream && audio->hostControlActive && drained < AUDIO_DRAIN_MAX) { LGMPStreamBuffer buffer = { 0 }; LGMP_STATUS status = lgmpClientStreamReadPeek( - audio->hostToClientStream, &buffer); + audio->hostControlStream, &buffer); if (status == LGMP_ERR_STREAM_EMPTY) break; if (status == LGMP_ERR_STREAM_UNBOUND || @@ -1554,23 +2068,22 @@ static unsigned drainInbound(LGMPAudio * audio) KVMFRAudioMessage message; const uint8_t * payload; valid = parseMessage(&buffer, &message, &payload); - bool sequenceGap = false; if (valid) { LG_LOCK(audio->lock); - sequenceGap = audio->sequenceKnown ? - message.sequence != audio->expectedSequence : + const bool sequenceGap = audio->controlSequenceKnown ? + message.sequence != audio->expectedControlSequence : message.sequence != 0; - audio->sequenceKnown = true; - audio->expectedSequence = message.sequence + 1; + audio->controlSequenceKnown = true; + audio->expectedControlSequence = message.sequence + 1; LG_UNLOCK(audio->lock); - valid = processInboundMessage( - audio, &message, payload, sequenceGap); + valid = !sequenceGap && processInboundMessage( + audio, &message, payload, false); } } status = lgmpClientStreamReadRelease( - audio->hostToClientStream, &buffer); + audio->hostControlStream, &buffer); if (status != LGMP_OK) { if (status != LGMP_ERR_STREAM_UNBOUND && @@ -1590,13 +2103,77 @@ static unsigned drainInbound(LGMPAudio * audio) return drained; } +static unsigned drainPlayback(LGMPAudio * audio) +{ + unsigned drained = 0; + while (audio->playbackStream && audio->playbackReadActive && + drained < AUDIO_DRAIN_MAX) + { + struct LGMPSPMCRecord record = { 0 }; + const LGMP_STATUS status = lgmpClientSPMCRead(audio->playbackStream, + audio->playbackRecord, + (uint32_t)sizeof(audio->playbackRecord), &record); + if (status == LGMP_ERR_STREAM_EMPTY) + break; + if (status == LGMP_ERR_STREAM_BUSY) + { + LG_LOCK(audio->lock); + audio->playbackDiscontinuity = true; + LG_UNLOCK(audio->lock); + break; + } + if (status == LGMP_ERR_STREAM_UNBOUND || + status == LGMP_ERR_STREAM_STALE) + { + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + break; + } + if (status != LGMP_OK) + { + DEBUG_WARN("Failed to read LGMP playback audio stream: %s", + lgmpStatusString(status)); + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + break; + } + + ++drained; + KVMFRAudioMessage message; + const uint8_t * payload; + bool valid = !record.reserved && record.size && + parseRecord(audio->playbackRecord, record.size, + (uint32_t)sizeof(audio->playbackRecord), &message, &payload) && + message.sequence == record.sequence; + bool discontinuity = record.skipped != 0; + if (valid) + { + LG_LOCK(audio->lock); + if (audio->playbackSequenceKnown && + record.sequence != audio->expectedPlaybackSequence) + discontinuity = true; + audio->playbackSequenceKnown = true; + audio->expectedPlaybackSequence = record.sequence + 1; + LG_UNLOCK(audio->lock); + valid = processPlaybackMessage( + audio, &message, payload, discontinuity); + } + if (!valid) + { + requestRelease(audio, "malformed playback SPMC record"); + break; + } + } + return drained; +} + static bool bindingGone(LGMPAudio * audio) { bool any = false; const PLGMPClientStream streams[2] = { - audio->hostToClientStream, - audio->clientToHostStream, + audio->hostControlStream, + audio->clientControlStream, }; for (unsigned i = 0; i < 2; ++i) { @@ -1606,11 +2183,11 @@ static bool bindingGone(LGMPAudio * audio) uint32_t clientID = 0; uint32_t epoch = 0; if (i == 1) - lockWriteGate(audio); + lockControlWriteGate(audio); const LGMP_STATUS status = lgmpClientStreamGetBinding( streams[i], &clientID, &epoch); if (i == 1) - unlockWriteGate(audio); + unlockControlWriteGate(audio); if (status == LGMP_OK && clientID == audio->clientID && epoch) return false; if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND && @@ -1621,6 +2198,24 @@ static bool bindingGone(LGMPAudio * audio) return false; } } + + if (audio->playbackStream) + { + any = true; + uint32_t clientID = 0; + uint32_t epoch = 0; + const LGMP_STATUS status = lgmpClientSPMCGetBinding( + audio->playbackStream, &clientID, &epoch); + if (status == LGMP_OK && clientID == audio->clientID && epoch) + return false; + if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND && + status != LGMP_ERR_STREAM_STALE) + { + DEBUG_WARN("Failed to query LGMP playback binding: %s", + lgmpStatusString(status)); + return false; + } + } return any; } @@ -1637,6 +2232,8 @@ static void maintainBinding(LGMPAudio * audio) const enum AudioPhase phase = audio->phase; const bool releaseRequested = audio->releaseRequested; const bool releaseSent = audio->releaseSent; + const bool subscriptionPending = audio->subscriptionPending; + const bool stateAckPending = audio->stateAckPending; const uint64_t deadline = audio->releaseDeadline; const uint64_t nextAttachAt = audio->nextAttachAt; const bool keepalive = phase == AUDIO_PHASE_BOUND && @@ -1690,7 +2287,8 @@ static void maintainBinding(LGMPAudio * audio) return; if (phase == AUDIO_PHASE_RELEASING && - (!audio->hostReadActive || !audio->clientWriteActive) && + (!audio->hostControlActive || !audio->clientControlActive || + !audio->playbackStream) && now >= nextAttachAt) { LG_LOCK(audio->lock); @@ -1701,12 +2299,13 @@ static void maintainBinding(LGMPAudio * audio) } if (phase == AUDIO_PHASE_RELEASING && !releaseSent && - audio->clientWriteActive) + audio->clientControlActive) { - lockWriteGate(audio); + lockControlWriteGate(audio); const enum WriteResult result = writeMessageLocked(audio, + audio->clientControlStream, &audio->clientControlSequence, KVMFR_AUDIO_MESSAGE_RELEASE, 0, 0, 0, 0, 0, 0, NULL, 0); - unlockWriteGate(audio); + unlockControlWriteGate(audio); if (result == WRITE_OK) { LG_LOCK(audio->lock); @@ -1715,12 +2314,72 @@ static void maintainBinding(LGMPAudio * audio) LG_UNLOCK(audio->lock); } } + else if (phase == AUDIO_PHASE_BOUND && !releaseRequested && + subscriptionPending && audio->clientControlActive) + { + LG_LOCK(audio->lock); + const KVMFRAudioSubscription subscription = audio->subscription; + LG_UNLOCK(audio->lock); + lockControlWriteGate(audio); + const enum WriteResult result = writeMessageLocked(audio, + audio->clientControlStream, &audio->clientControlSequence, + KVMFR_AUDIO_MESSAGE_PLAYBACK_SUBSCRIBE, 0, 0, 0, 0, 0, 0, + &subscription, sizeof(subscription)); + unlockControlWriteGate(audio); + if (result == WRITE_OK) + { + LG_LOCK(audio->lock); + if (!memcmp(&audio->subscription, &subscription, + sizeof(subscription))) + audio->subscriptionPending = false; + LG_UNLOCK(audio->lock); + } + } + else if (phase == AUDIO_PHASE_BOUND && !releaseRequested && + stateAckPending && audio->clientControlActive) + { + LG_LOCK(audio->lock); + const KVMFRAudioStateBarrier ack = audio->stateAck; + const uint64_t generation = audio->stateAckGeneration; + LG_UNLOCK(audio->lock); + lockControlWriteGate(audio); + const enum WriteResult result = writeMessageLocked(audio, + audio->clientControlStream, &audio->clientControlSequence, + KVMFR_AUDIO_MESSAGE_STATE_ACK, generation, 0, 0, 0, 0, 0, + &ack, sizeof(ack)); + unlockControlWriteGate(audio); + if (result == WRITE_OK) + { + bool acknowledged = false; + LG_LOCK(audio->lock); + if (!memcmp(&audio->stateAck, &ack, sizeof(ack)) && + audio->stateAckGeneration == generation) + { + audio->stateAckPending = false; + acknowledged = audio->phase == AUDIO_PHASE_BOUND && + !audio->releaseRequested && + ack.bindingEpoch == audio->bindingEpoch && + ack.roleEpoch == audio->roleEpoch; + audio->stateAcknowledged = acknowledged; + audio->playbackReadActive = acknowledged && + audio->statusAvailable && + (audio->remoteStatus.grantedCapabilities & + KVMFR_AUDIO_CAP_PLAYBACK) && + (audio->subscription.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_PLAYBACK); + } + LG_UNLOCK(audio->lock); + if (acknowledged) + updateWriteStreamState(audio); + } + } else if (keepalive) { - lockWriteGate(audio); + lockControlWriteGate(audio); const enum WriteResult result = writeMessageLocked(audio, + audio->clientControlStream, &audio->clientControlSequence, KVMFR_AUDIO_MESSAGE_KEEPALIVE, 0, 0, 0, 0, 0, 0, NULL, 0); - unlockWriteGate(audio); + unlockControlWriteGate(audio); if (result == WRITE_OK) { LG_LOCK(audio->lock); @@ -1737,9 +2396,17 @@ static void maintainBinding(LGMPAudio * audio) static void stopWorkerBinding(LGMPAudio * audio) { + const uint64_t now = microtime(); LG_LOCK(audio->lock); const bool active = bindingPhaseActiveNL(audio); audio->phase = active ? AUDIO_PHASE_RELEASING : audio->phase; + if (active && !audio->releaseDeadline) + audio->releaseDeadline = now + AUDIO_OWNER_RETRY_US; + const uint64_t releaseDeadline = audio->releaseDeadline; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + audio->stateAckPending = false; + audio->subscriptionPending = false; LG_UNLOCK(audio->lock); if (!active) { @@ -1750,7 +2417,8 @@ static void stopWorkerBinding(LGMPAudio * audio) for (;;) { - drainInbound(audio); + drainControl(audio); + drainPlayback(audio); maintainBinding(audio); LG_LOCK(audio->lock); @@ -1758,6 +2426,11 @@ static void stopWorkerBinding(LGMPAudio * audio) LG_UNLOCK(audio->lock); if (!bindingActive || !lgmpClientSessionValid(audio->client)) break; + if (microtime() >= releaseDeadline) + { + DEBUG_WARN("Timed out waiting for LGMP audio binding release"); + break; + } lgWaitEvent(audio->event, 1); } stopActiveStreams(audio); @@ -1779,7 +2452,9 @@ static int audioThread(void * opaque) while (!atomic_load_explicit(&audio->stop, memory_order_acquire)) { processResponse(audio); - const unsigned drained = drainInbound(audio); + const unsigned controlDrained = drainControl(audio); + const unsigned playbackDrained = drainPlayback(audio); + const unsigned drained = controlDrained + playbackDrained; if (drained) lgmpStreamPollActivity(&audio->streamPoll); maintainBinding(audio); @@ -1799,7 +2474,8 @@ static int audioThread(void * opaque) LG_UNLOCK(audio->lock); if (active) { - if (drained == AUDIO_DRAIN_MAX) + if (controlDrained == AUDIO_DRAIN_MAX || + playbackDrained == AUDIO_DRAIN_MAX) { lgmpStreamPollActivity(&audio->streamPoll); continue; @@ -1848,7 +2524,8 @@ bool lgmpAudio_create(PLGMPClient client, LGEvent * pointerWake, LG_LOCK_INIT(audio->lock); LG_LOCK_INIT(audio->eventLock); LG_LOCK_INIT(audio->statusWait.lock); - atomic_flag_clear(&audio->writeGate); + atomic_flag_clear(&audio->controlWriteGate); + atomic_flag_clear(&audio->captureWriteGate); atomic_init(&audio->stop, false); atomic_init(&audio->dataPlaneLost, false); atomic_init(&audio->captureDiscontinuity, false); @@ -1897,6 +2574,13 @@ bool lgmpAudio_connect(LGMPAudio * audio, uint32_t clientID) audio->nextClaimAt = 0; audio->releaseRequested = false; audio->releaseSent = false; + audio->bindingEpoch = 0; + audio->roleEpoch = 0; + audio->playbackReaderEpoch = 0; + audio->stateAckPending = false; + audio->subscriptionPending = false; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; memset(&audio->endpointGeneration, 0, sizeof(audio->endpointGeneration)); atomic_store_explicit(&audio->stop, false, memory_order_release); @@ -1922,7 +2606,7 @@ void lgmpAudio_disconnect(LGMPAudio * audio) return; LG_LOCK(audio->eventLock); - lockWriteGate(audio); + lockWriteGates(audio); LG_LOCK(audio->lock); closePublicDataLocked(audio); audio->events = NULL; @@ -1930,6 +2614,10 @@ void lgmpAudio_disconnect(LGMPAudio * audio) audio->attached = false; audio->discover = false; audio->releaseRequested = true; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + audio->stateAckPending = false; + audio->subscriptionPending = false; audio->releaseDeadline = microtime() + AUDIO_OWNER_RETRY_US; if (audio->phase == AUDIO_PHASE_BOUND) @@ -1940,7 +2628,7 @@ void lgmpAudio_disconnect(LGMPAudio * audio) LGEvent * event = audio->event; atomic_store_explicit(&audio->stop, true, memory_order_release); LG_UNLOCK(audio->lock); - unlockWriteGate(audio); + unlockWriteGates(audio); LG_UNLOCK(audio->eventLock); if (event) @@ -1959,11 +2647,20 @@ void lgmpAudio_disconnect(LGMPAudio * audio) audio->claimSends = 0; audio->releaseRequested = false; audio->releaseSent = false; + audio->bindingEpoch = 0; + audio->roleEpoch = 0; + audio->playbackReaderEpoch = 0; + audio->stateAckPending = false; + audio->subscriptionPending = false; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; audio->statusCallback = NULL; audio->statusOpaque = NULL; memset(&audio->claim, 0, sizeof(audio->claim)); memset(&audio->response, 0, sizeof(audio->response)); memset(&audio->remoteStatus, 0, sizeof(audio->remoteStatus)); + memset(&audio->stateAck, 0, sizeof(audio->stateAck)); + memset(&audio->subscription, 0, sizeof(audio->subscription)); LG_UNLOCK(audio->lock); waitStatusCallbacks(audio); } @@ -1992,6 +2689,7 @@ static bool newClaimNL(LGMPAudio * audio) .protocolMin = KVMFR_AUDIO_PROTOCOL_MIN, .protocolMax = KVMFR_AUDIO_PROTOCOL_MAX, .clientCapabilities = AUDIO_CAPABILITIES, + .subscriptions = AUDIO_SUBSCRIPTIONS, }; audio->claimSends = 0; audio->claimPosting = false; @@ -2093,7 +2791,7 @@ static void setStatusListener(void * opaque, { LGMPAudio * audio = opaque; if (!callback) - lockWriteGate(audio); + lockWriteGates(audio); LG_LOCK(audio->lock); if (!callback) closePublicDataLocked(audio); @@ -2110,6 +2808,10 @@ static void setStatusListener(void * opaque, const bool ownsOrMayOwn = audio->phase != AUDIO_PHASE_IDLE; audio->releaseRequested = ownsOrMayOwn; + audio->playbackReadActive = false; + audio->stateAcknowledged = false; + audio->stateAckPending = false; + audio->subscriptionPending = false; if (ownsOrMayOwn) { audio->releaseDeadline = @@ -2130,7 +2832,7 @@ static void setStatusListener(void * opaque, audio->statusDirty = false; LG_UNLOCK(audio->lock); if (!callback) - unlockWriteGate(audio); + unlockWriteGates(audio); if (callback) { callback(callbackOpaque, &status); @@ -2228,20 +2930,22 @@ static bool recordData(void * opaque, uint32_t generation, if (!generation || !data || !frames || frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET) return false; - lockWriteGate(audio); + lockCaptureWriteGate(audio); - if (!audio->publicDataAllowed || !audio->writeBound || + if (!audio->publicDataAllowed || !audio->captureWriteBound || + !(audio->writeGrantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE) || + !audio->writeBindingEpoch || !audio->writeRoleEpoch || generation != audio->writeCaptureLocalGeneration || !audio->writeCaptureGeneration) { - unlockWriteGate(audio); + unlockCaptureWriteGate(audio); return false; } const size_t bytes = frames * audio->writeCaptureFormat.blockAlign; if (!audio->writeCaptureFormat.blockAlign || bytes > KVMFR_AUDIO_PCM_BYTES) { - unlockWriteGate(audio); + unlockCaptureWriteGate(audio); return false; } @@ -2253,7 +2957,7 @@ static bool recordData(void * opaque, uint32_t generation, { if (!rateToQ32(sourceClock->rate, true, &rateQ32)) { - unlockWriteGate(audio); + unlockCaptureWriteGate(audio); return false; } flags |= KVMFR_AUDIO_MESSAGE_CLOCK_VALID; @@ -2269,6 +2973,7 @@ static bool recordData(void * opaque, uint32_t generation, flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY; enum WriteResult result = writeMessageLocked(audio, + audio->captureStream, &audio->captureSequence, KVMFR_AUDIO_MESSAGE_CAPTURE_DATA, audio->writeCaptureGeneration, flags, position, timeNs, rateQ32, (uint32_t)frames, data, (uint32_t)bytes); @@ -2300,6 +3005,7 @@ static bool recordData(void * opaque, uint32_t generation, nsleep((uint64_t)waitUs * 1000U); result = writeMessageLocked(audio, + audio->captureStream, &audio->captureSequence, KVMFR_AUDIO_MESSAGE_CAPTURE_DATA, audio->writeCaptureGeneration, flags, position, timeNs, rateQ32, (uint32_t)frames, data, (uint32_t)bytes); @@ -2308,11 +3014,11 @@ static bool recordData(void * opaque, uint32_t generation, if (result != WRITE_OK) { if (result == WRITE_FULL) - ++audio->clientSequence; + ++audio->captureSequence; atomic_store_explicit( &audio->captureDiscontinuity, true, memory_order_release); } - unlockWriteGate(audio); + unlockCaptureWriteGate(audio); return result == WRITE_OK; } @@ -2327,26 +3033,39 @@ static bool clockFeedback(void * opaque, uint32_t generation, !rateToQ32(playbackClock->rate, true, &rateQ32)) return false; - if (!tryWriteGate(audio)) + if (!tryControlWriteGate(audio)) return false; LG_LOCK(audio->lock); const bool supported = audio->phase == AUDIO_PHASE_BOUND && !audio->releaseRequested && audio->statusAvailable && - (audio->remoteStatus.capabilities & + audio->stateAcknowledged && + audio->stateAck.bindingEpoch == audio->bindingEpoch && + audio->stateAck.roleEpoch == audio->roleEpoch && + audio->stateAckGeneration == + audio->remoteStatus.playbackGeneration && + (audio->remoteStatus.grantedCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) != 0 && + audio->remoteStatus.feedbackAuthorityClientID == audio->clientID && + audio->remoteStatus.feedbackAuthorityEpoch && + audio->remoteStatus.bindingEpoch == audio->bindingEpoch && + audio->remoteStatus.roleEpoch == audio->roleEpoch && audio->playback.active && audio->playback.generation.local == generation; LG_UNLOCK(audio->lock); if (!supported) { - unlockWriteGate(audio); + unlockControlWriteGate(audio); return false; } - if (!audio->publicDataAllowed || !audio->writeBound || + if (!audio->publicDataAllowed || !audio->controlWriteBound || + !(audio->writeGrantedCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) || + audio->writeBindingEpoch != audio->bindingEpoch || + audio->writeRoleEpoch != audio->roleEpoch || generation != audio->writePlaybackLocalGeneration || !audio->writePlaybackGeneration) { - unlockWriteGate(audio); + unlockControlWriteGate(audio); return false; } @@ -2358,13 +3077,16 @@ static bool clockFeedback(void * opaque, uint32_t generation, const KVMFRAudioClockFeedback feedback = { .targetRateQ32 = targetRateQ32, + .bindingEpoch = audio->writeBindingEpoch, + .roleEpoch = audio->writeRoleEpoch, }; const enum WriteResult result = writeMessageLocked(audio, + audio->clientControlStream, &audio->clientControlSequence, KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK, audio->writePlaybackGeneration, flags, playbackClock->position, playbackClock->time, rateQ32, 0, &feedback, sizeof(feedback)); - unlockWriteGate(audio); + unlockControlWriteGate(audio); return result == WRITE_OK; } diff --git a/client/transports/LGMP/audio.h b/client/transports/LGMP/audio.h index ed6de3c2..09203f6d 100644 --- a/client/transports/LGMP/audio.h +++ b/client/transports/LGMP/audio.h @@ -23,7 +23,7 @@ #include "interface/audio.h" -#include "common/KVMFRAudio.h" +#include #include "common/event.h" #include diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 81cc1331..73527cf3 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -2419,7 +2419,8 @@ static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, #ifdef ENABLE_AUDIO KVMFRAudioStreamResponse audioResponse; const bool audioResponseValid = - tag == KVMFR_AUDIO_CONTROL_TAG && this->audio && + tag == KVMFR_AUDIO_CONTROL_TAG && + message.udata == KVMFR_AUDIO_CONTROL_UDATA && this->audio && message.size == sizeof(audioResponse); if (audioResponseValid) memcpy(&audioResponse, message.mem, sizeof(audioResponse)); diff --git a/common/include/common/KVMFRAudio.h b/common/include/common/KVMFRAudio.h deleted file mode 100644 index 994e03e0..00000000 --- a/common/include/common/KVMFRAudio.h +++ /dev/null @@ -1,447 +0,0 @@ -/** - * 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 - */ - -#ifndef _H_LG_COMMON_KVMFR_AUDIO_ -#define _H_LG_COMMON_KVMFR_AUDIO_ - -#pragma once - -#include -#include - -#include -#include - -enum -{ - /* Static protocol support only; KVMFRAudioStatus reports availability. */ - KVMFR_FEATURE_AUDIO = 1U << 5, -}; - -enum -{ - KVMFR_MESSAGE_AUDIO_CLAIM = 3, -}; - -#define KVMFR_AUDIO_PROTOCOL_MIN 1U -#define KVMFR_AUDIO_PROTOCOL_MAX 1U -#define KVMFR_AUDIO_CONTROL_VERSION 1U -#define KVMFR_AUDIO_MESSAGE_VERSION 1U -#define KVMFR_AUDIO_FORMAT_VERSION 1U -#define KVMFR_AUDIO_STATUS_VERSION 1U -#define KVMFR_AUDIO_VOLUME_VERSION 1U - -#define KVMFR_AUDIO_MAX_CHANNELS 18U -#define KVMFR_AUDIO_MAX_SAMPLE_RATE 192000U -#define KVMFR_AUDIO_MAX_FRAMES_PER_PACKET 2048U -#define KVMFR_AUDIO_MAX_BYTES_PER_SAMPLE 4U -#define KVMFR_AUDIO_STREAM_MAX_CHANNELS 8U -#define KVMFR_AUDIO_STREAM_SLOT_COUNT 16U -#define KVMFR_AUDIO_OWNER_LEASE_MS 1000U -#define KVMFR_AUDIO_KEEPALIVE_MS 200U -#define KVMFR_AUDIO_PCM_BYTES \ - (KVMFR_AUDIO_MAX_FRAMES_PER_PACKET * \ - KVMFR_AUDIO_STREAM_MAX_CHANNELS * \ - KVMFR_AUDIO_MAX_BYTES_PER_SAMPLE) - -enum -{ - KVMFR_AUDIO_CAP_PLAYBACK = 1U << 0, - KVMFR_AUDIO_CAP_CAPTURE = 1U << 1, - KVMFR_AUDIO_CAP_CLOCK_FEEDBACK = 1U << 2, -}; - -typedef uint32_t KVMFRAudioCapabilities; - -enum -{ - KVMFR_AUDIO_SAMPLE_S16_LE = 1, - /* Signed little-endian samples packed into exactly three bytes. */ - KVMFR_AUDIO_SAMPLE_S24_LE = 2, - KVMFR_AUDIO_SAMPLE_S32_LE = 3, - /* IEEE 754 little-endian floating-point samples. */ - KVMFR_AUDIO_SAMPLE_F32_LE = 4, -}; - -typedef uint32_t KVMFRAudioSampleFormat; - -enum -{ - KVMFR_AUDIO_SAMPLE_MASK_S16_LE = 1U << 0, - KVMFR_AUDIO_SAMPLE_MASK_S24_LE = 1U << 1, - KVMFR_AUDIO_SAMPLE_MASK_S32_LE = 1U << 2, - KVMFR_AUDIO_SAMPLE_MASK_F32_LE = 1U << 3, -}; - -typedef uint32_t KVMFRAudioSampleFormatFlags; - -/* These values match the conventional Windows speaker-position mask. */ -enum -{ - KVMFR_AUDIO_CHANNEL_FRONT_LEFT = 1U << 0, - KVMFR_AUDIO_CHANNEL_FRONT_RIGHT = 1U << 1, - KVMFR_AUDIO_CHANNEL_FRONT_CENTER = 1U << 2, - KVMFR_AUDIO_CHANNEL_LFE = 1U << 3, - KVMFR_AUDIO_CHANNEL_BACK_LEFT = 1U << 4, - KVMFR_AUDIO_CHANNEL_BACK_RIGHT = 1U << 5, - KVMFR_AUDIO_CHANNEL_FRONT_LEFT_OF_CENTER = 1U << 6, - KVMFR_AUDIO_CHANNEL_FRONT_RIGHT_OF_CENTER = 1U << 7, - KVMFR_AUDIO_CHANNEL_BACK_CENTER = 1U << 8, - KVMFR_AUDIO_CHANNEL_SIDE_LEFT = 1U << 9, - KVMFR_AUDIO_CHANNEL_SIDE_RIGHT = 1U << 10, - KVMFR_AUDIO_CHANNEL_TOP_CENTER = 1U << 11, - KVMFR_AUDIO_CHANNEL_TOP_FRONT_LEFT = 1U << 12, - KVMFR_AUDIO_CHANNEL_TOP_FRONT_CENTER = 1U << 13, - KVMFR_AUDIO_CHANNEL_TOP_FRONT_RIGHT = 1U << 14, - KVMFR_AUDIO_CHANNEL_TOP_BACK_LEFT = 1U << 15, - KVMFR_AUDIO_CHANNEL_TOP_BACK_CENTER = 1U << 16, - KVMFR_AUDIO_CHANNEL_TOP_BACK_RIGHT = 1U << 17, -}; - -typedef uint32_t KVMFRAudioChannelMask; - -#define KVMFR_AUDIO_CHANNEL_MONO \ - KVMFR_AUDIO_CHANNEL_FRONT_CENTER -#define KVMFR_AUDIO_CHANNEL_STEREO \ - (KVMFR_AUDIO_CHANNEL_FRONT_LEFT | KVMFR_AUDIO_CHANNEL_FRONT_RIGHT) -#define KVMFR_AUDIO_CHANNEL_5POINT1 \ - (KVMFR_AUDIO_CHANNEL_STEREO | KVMFR_AUDIO_CHANNEL_FRONT_CENTER | \ - KVMFR_AUDIO_CHANNEL_LFE | KVMFR_AUDIO_CHANNEL_SIDE_LEFT | \ - KVMFR_AUDIO_CHANNEL_SIDE_RIGHT) -#define KVMFR_AUDIO_CHANNEL_7POINT1 \ - (KVMFR_AUDIO_CHANNEL_5POINT1 | KVMFR_AUDIO_CHANNEL_BACK_LEFT | \ - KVMFR_AUDIO_CHANNEL_BACK_RIGHT) -#define KVMFR_AUDIO_CHANNEL_POSITION_MASK UINT32_C(0x0003FFFF) - -/* Interleaved samples and volume entries use ascending set-bit order from - * channelMask. channelCount must equal the number of set mask bits and may - * cover all 18 standard positions even when an implementation advertises a - * lower active channel limit. */ -typedef struct KVMFRAudioFormat -{ - uint16_t version; - uint16_t size; - KVMFRAudioSampleFormat sampleFormat; - uint32_t sampleRate; - KVMFRAudioChannelMask channelMask; - uint16_t channelCount; - uint16_t validBits; - uint16_t containerBits; - uint16_t blockAlign; - uint32_t reserved[2]; -} -KVMFRAudioFormat; - -enum -{ - KVMFR_AUDIO_MESSAGE_STATUS = 1, - - KVMFR_AUDIO_MESSAGE_PLAYBACK_START, - KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, - KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, - KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME, - KVMFR_AUDIO_MESSAGE_PLAYBACK_MUTE, - - KVMFR_AUDIO_MESSAGE_CAPTURE_START, - KVMFR_AUDIO_MESSAGE_CAPTURE_STOP, - KVMFR_AUDIO_MESSAGE_CAPTURE_DATA, - KVMFR_AUDIO_MESSAGE_CAPTURE_VOLUME, - KVMFR_AUDIO_MESSAGE_CAPTURE_MUTE, - - KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK, - KVMFR_AUDIO_MESSAGE_KEEPALIVE, - KVMFR_AUDIO_MESSAGE_RELEASE, -}; - -typedef uint32_t KVMFRAudioMessageType; - -enum -{ - KVMFR_AUDIO_MESSAGE_DISCONTINUITY = 1U << 0, - KVMFR_AUDIO_MESSAGE_SILENT = 1U << 1, - KVMFR_AUDIO_MESSAGE_CLOCK_STABLE = 1U << 2, - KVMFR_AUDIO_MESSAGE_MUTED = 1U << 3, - KVMFR_AUDIO_MESSAGE_CLOCK_VALID = 1U << 4, -}; - -typedef uint32_t KVMFRAudioMessageFlags; - -/* - * Every record uses one of the two ordinary LGMP streams: - * - * host-to-client: STATUS, PLAYBACK_*, and CAPTURE start/stop/control; - * client-to-host: CAPTURE_DATA, CLOCK_FEEDBACK, KEEPALIVE, and RELEASE. - * - * Any payload immediately follows this fixed header. START carries one - * KVMFRAudioFormat, VOLUME carries KVMFRAudioVolume, and DATA - * carries frames * blockAlign bytes. The position and timeNs fields describe - * the first frame of DATA or the correlated clock sample in CLOCK_FEEDBACK. - * Times are monotonic nanoseconds in the publisher's clock domain and may be - * compared only by difference unless that domain is otherwise known. - */ -typedef struct KVMFRAudioMessage -{ - uint16_t version; - uint16_t size; - KVMFRAudioMessageType type; - uint64_t streamGeneration; - KVMFRAudioMessageFlags flags; - uint32_t reserved0; - uint64_t sequence; - uint64_t position; - int64_t timeNs; - /* Unsigned 32.32 frames per second; zero means unavailable. */ - uint64_t rateQ32; - uint32_t frames; - uint32_t length; -} -KVMFRAudioMessage; - -/* - * Type-specific payload and header rules: - * - * STATUS: - * host-to-client, KVMFRAudioStatus payload, zero streamGeneration/frames. - * PLAYBACK_START / CAPTURE_START: - * host-to-client, KVMFRAudioFormat payload, nonzero streamGeneration and - * zero frames. CLOCK_VALID marks an optional source-clock correlation. - * PLAYBACK_STOP / CAPTURE_STOP: - * host-to-client, no payload, nonzero streamGeneration and zero frames. - * PLAYBACK_DATA / CAPTURE_DATA: - * the direction implied by the name, nonzero streamGeneration, and between - * one and KVMFR_AUDIO_MAX_FRAMES_PER_PACKET frames. Without SILENT, length - * is frames * blockAlign, does not exceed KVMFR_AUDIO_PCM_BYTES, and the - * payload is interleaved PCM. With SILENT, length is zero and no PCM payload - * is present. - * PLAYBACK_VOLUME / CAPTURE_VOLUME: - * host-to-client, KVMFRAudioVolume payload, nonzero streamGeneration and - * zero frames. - * PLAYBACK_MUTE / CAPTURE_MUTE: - * host-to-client, no payload, nonzero streamGeneration, zero frames, and - * MUTED set or clear for the new state. - * CLOCK_FEEDBACK: - * client-to-host, KVMFRAudioClockFeedback payload, the playback generation, - * zero frames, and CLOCK_VALID set. - * KEEPALIVE / RELEASE: - * client-to-host, no payload and zero streamGeneration/frames. - * - * Sequence starts at zero and increments for every record on each LGMP stream - * while bound. A producer that drops a full realtime record leaves a sequence - * gap and sets DISCONTINUITY on its next record. - */ - -/* Payload for CLOCK_FEEDBACK. The message clock describes the playback - * device; targetRateQ32 is the requested source rate. */ -typedef struct KVMFRAudioClockFeedback -{ - uint64_t targetRateQ32; - uint64_t reserved; -} -KVMFRAudioClockFeedback; - -/* Fixed-size payload for PLAYBACK_VOLUME and CAPTURE_VOLUME. Values use the - * full unsigned 16-bit range, where zero is silent and UINT16_MAX is unity. - * Entries beyond channelCount must be zero. */ -typedef struct KVMFRAudioVolume -{ - uint16_t version; - uint16_t size; - uint16_t channelCount; - uint16_t reserved; - uint16_t values[KVMFR_AUDIO_MAX_CHANNELS]; -} -KVMFRAudioVolume; - -#define KVMFR_AUDIO_STREAM_SLOT_BYTES \ - (sizeof(KVMFRAudioMessage) + KVMFR_AUDIO_PCM_BYTES) - -enum -{ - KVMFR_AUDIO_STATUS_AVAILABLE = 1U << 0, - KVMFR_AUDIO_STATUS_HAS_OWNER = 1U << 1, - KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE = 1U << 2, - KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE = 1U << 3, - KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE = 1U << 4, - KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE = 1U << 5, -}; - -typedef uint32_t KVMFRAudioStatusFlags; - -enum -{ - KVMFR_AUDIO_STREAM_STOPPED = 0, - KVMFR_AUDIO_STREAM_ACQUIRED = 1, - KVMFR_AUDIO_STREAM_PAUSED = 2, - KVMFR_AUDIO_STREAM_RUNNING = 3, -}; - -typedef uint32_t KVMFRAudioStreamState; - -typedef struct KVMFRAudioStatus -{ - uint16_t version; - uint16_t size; - KVMFRAudioStatusFlags flags; - uint32_t protocolVersion; - uint32_t requestGeneration; - KVMFRAudioCapabilities capabilities; - uint32_t ownerClientID; - uint32_t ownerGeneration; - uint32_t reserved0; - uint64_t endpointGeneration; - uint64_t playbackGeneration; - uint64_t captureGeneration; - KVMFRAudioStreamState playbackState; - KVMFRAudioStreamState captureState; - KVMFRAudioFormat playbackFormat; - KVMFRAudioFormat captureFormat; -} -KVMFRAudioStatus; - -enum -{ - KVMFR_AUDIO_CLAIM_BIND = 1U << 0, -}; - -typedef uint32_t KVMFRAudioClaimFlags; - -/* Sent through the existing LGMP client-control message path. flags must be - * exactly KVMFR_AUDIO_CLAIM_BIND in protocol v1. Audio support is statically - * advertised in KVMFR features; runtime availability is not a prerequisite - * for binding these streams. */ -typedef struct KVMFRAudioClaim -{ - uint32_t messageType; - uint16_t version; - uint16_t size; - uint32_t requestGeneration; - KVMFRAudioClaimFlags flags; - uint64_t claimToken; - uint32_t protocolMin; - uint32_t protocolMax; - KVMFRAudioCapabilities clientCapabilities; - uint32_t reserved0; - uint64_t reserved[3]; -} -KVMFRAudioClaim; - -/* - * Descriptor responses share the existing pointer queue. The high udata word - * identifies this record; the low word remains zero for cursor compatibility. - */ -#define KVMFR_AUDIO_CONTROL_TAG UINT32_C(0x41554431) -#define KVMFR_AUDIO_CONTROL_UDATA (UINT64_C(0x41554431) << 32) -#define KVMFR_AUDIO_RESPONSE_MAGIC UINT32_C(0x41554452) -#define KVMFR_AUDIO_CURSOR_COMPAT_BYTES 28U - -#if defined(__cplusplus) -static_assert(sizeof(KVMFRCursor) == KVMFR_AUDIO_CURSOR_COMPAT_BYTES, - "KVMFR cursor compatibility layout changed"); -#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L -_Static_assert(sizeof(KVMFRCursor) == KVMFR_AUDIO_CURSOR_COMPAT_BYTES, - "KVMFR cursor compatibility layout changed"); -#endif - -enum -{ - KVMFR_AUDIO_RESPONSE_BOUND = 1, - KVMFR_AUDIO_RESPONSE_BUSY = 2, - KVMFR_AUDIO_RESPONSE_INCOMPATIBLE = 3, - KVMFR_AUDIO_RESPONSE_ERROR = 4, -}; - -typedef uint16_t KVMFRAudioResponseResult; - -/* Exactly one response result is returned. A BOUND response selects a - * nonzero protocolVersion and makes both descriptors valid. */ -typedef struct KVMFRAudioStreamResponse -{ - uint8_t cursorCompatibility[28]; - uint32_t magic; - uint16_t version; - uint16_t size; - uint32_t requestGeneration; - uint32_t boundClientID; - KVMFRAudioResponseResult result; - uint16_t protocolVersion; - uint64_t endpointGeneration; - uint64_t claimToken; - KVMFRStreamDescriptor hostToClient; - KVMFRStreamDescriptor clientToHost; -} -KVMFRAudioStreamResponse; - -#if defined(__cplusplus) -static_assert(sizeof(KVMFRAudioFormat) == 32, - "KVMFR audio format layout changed"); -static_assert(sizeof(KVMFRAudioMessage) == 64, - "KVMFR audio message layout changed"); -static_assert(sizeof(KVMFRAudioClockFeedback) == 16, - "KVMFR audio clock feedback layout changed"); -static_assert(sizeof(KVMFRAudioVolume) == 44, - "KVMFR audio volume layout changed"); -static_assert(sizeof(KVMFRAudioStatus) == 128, - "KVMFR audio status layout changed"); -static_assert(offsetof(KVMFRAudioStatus, endpointGeneration) == 32, - "KVMFR audio endpoint generation layout changed"); -static_assert(offsetof(KVMFRAudioStatus, playbackFormat) == 64, - "KVMFR audio status format layout changed"); -static_assert(sizeof(KVMFRAudioClaim) == 64, - "KVMFR audio claim must fit one LGMP control message"); -static_assert(offsetof(KVMFRAudioStreamResponse, magic) == 28, - "KVMFR audio pointer compatibility layout changed"); -static_assert(offsetof(KVMFRAudioStreamResponse, endpointGeneration) == 48, - "KVMFR audio response generation layout changed"); -static_assert(offsetof(KVMFRAudioStreamResponse, claimToken) == 56, - "KVMFR audio response token layout changed"); -static_assert(offsetof(KVMFRAudioStreamResponse, hostToClient) == 64, - "KVMFR audio stream discovery layout changed"); -static_assert(sizeof(KVMFRAudioStreamResponse) == 128, - "KVMFR audio stream response layout changed"); -#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L -_Static_assert(sizeof(KVMFRAudioFormat) == 32, - "KVMFR audio format layout changed"); -_Static_assert(sizeof(KVMFRAudioMessage) == 64, - "KVMFR audio message layout changed"); -_Static_assert(sizeof(KVMFRAudioClockFeedback) == 16, - "KVMFR audio clock feedback layout changed"); -_Static_assert(sizeof(KVMFRAudioVolume) == 44, - "KVMFR audio volume layout changed"); -_Static_assert(sizeof(KVMFRAudioStatus) == 128, - "KVMFR audio status layout changed"); -_Static_assert(offsetof(KVMFRAudioStatus, endpointGeneration) == 32, - "KVMFR audio endpoint generation layout changed"); -_Static_assert(offsetof(KVMFRAudioStatus, playbackFormat) == 64, - "KVMFR audio status format layout changed"); -_Static_assert(sizeof(KVMFRAudioClaim) == 64, - "KVMFR audio claim must fit one LGMP control message"); -_Static_assert(offsetof(KVMFRAudioStreamResponse, magic) == 28, - "KVMFR audio pointer compatibility layout changed"); -_Static_assert(offsetof(KVMFRAudioStreamResponse, endpointGeneration) == 48, - "KVMFR audio response generation layout changed"); -_Static_assert(offsetof(KVMFRAudioStreamResponse, claimToken) == 56, - "KVMFR audio response token layout changed"); -_Static_assert(offsetof(KVMFRAudioStreamResponse, hostToClient) == 64, - "KVMFR audio stream discovery layout changed"); -_Static_assert(sizeof(KVMFRAudioStreamResponse) == 128, - "KVMFR audio stream response layout changed"); -#endif - -#endif diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp index 3f306047..44a05aae 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.cpp @@ -29,6 +29,8 @@ #include "transport/lgmp/CLGMPControl.h" #include "transport/lgmp/CLGMPHost.h" +#include + #include #include #include @@ -40,6 +42,8 @@ namespace { + static_assert(sizeof(KVMFRCursor) == KVMFR_AUDIO_CURSOR_COMPAT_BYTES, + "KVMFR cursor compatibility layout changed"); static_assert(AUDIO_MAX_CHANNELS == KVMFR_AUDIO_MAX_CHANNELS, "audio channel limits differ"); static_assert(AUDIO_STREAM_MAX_CHANNELS == @@ -70,12 +74,18 @@ namespace static_assert(sizeof(LGMPStreamDescriptor) == sizeof(KVMFRStreamDescriptor), "LGMP and KVMFR stream descriptor sizes differ"); + static_assert(sizeof(LGMPSPMCDescriptor) == + sizeof(KVMFRSPMCDescriptor), + "LGMP and KVMFR SPMC descriptor sizes differ"); + static_assert(LGMP_MAX_CLIENTS == + KVMFR_AUDIO_MAX_PLAYBACK_READERS, + "audio reader and LGMP client limits 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_SUBSCRIPTION_MASK = + KVMFR_AUDIO_SUBSCRIBE_PLAYBACK | KVMFR_AUDIO_SUBSCRIBE_CAPTURE; constexpr uint32_t AUDIO_PACKET_FLAG_MASK = AUDIO_PACKET_DISCONTINUITY | AUDIO_PACKET_SILENT | AUDIO_PACKET_CLOCK_VALID | AUDIO_PACKET_CLOCK_STABLE; @@ -111,17 +121,29 @@ namespace return serial ? serial : 1; } + uint32_t NextEpoch(uint32_t epoch) + { + ++epoch; + return epoch ? epoch : 1; + } + bool RequestNewer(uint32_t candidate, uint32_t previous) { return candidate != previous && static_cast(candidate - previous) > 0; } + + bool EmptyControl(const KVMFRAudioMessage& message) + { + return !message.streamGeneration && !message.flags && + !message.position && !message.timeNs && !message.rateQ32 && + !message.frames && !message.length; + } } -CLGMPAudioTransport::~CLGMPAudioTransport() +CLGMPAudio::~CLGMPAudio() { TransportStop(); - CSRWExclusiveLock lock(m_lock); for (ResponseMemory& response : m_responseMemory) lgmpHostMemFree(&response.memory); @@ -129,7 +151,7 @@ CLGMPAudioTransport::~CLGMPAudioTransport() m_initialized = false; } -bool CLGMPAudioTransport::SameFormat( +bool CLGMPAudio::SameFormat( const AudioFormat& left, const AudioFormat& right) { return left.sampleFormat == right.sampleFormat && @@ -141,18 +163,17 @@ bool CLGMPAudioTransport::SameFormat( left.blockAlign == right.blockAlign; } -bool CLGMPAudioTransport::SamePacket( +bool CLGMPAudio::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); + left.rateQ32 == right.rateQ32 && SameFormat(left.format, right.format); } -bool CLGMPAudioTransport::ValidFormat(const AudioFormat& format) +bool CLGMPAudio::ValidFormat(const AudioFormat& format) { if (!format.sampleRate || format.sampleRate > KVMFR_AUDIO_MAX_SAMPLE_RATE || @@ -188,14 +209,13 @@ bool CLGMPAudioTransport::ValidFormat(const AudioFormat& format) return validSampleFormat && format.blockAlign == bytes; } -KVMFRAudioFormat CLGMPAudioTransport::WireFormat( +KVMFRAudioFormat CLGMPAudio::WireFormat( const AudioFormat& format) { KVMFRAudioFormat result = {}; result.version = KVMFR_AUDIO_FORMAT_VERSION; result.size = sizeof(result); - result.sampleFormat = - static_cast(format.sampleFormat); + result.sampleFormat = static_cast(format.sampleFormat); result.sampleRate = format.sampleRate; result.channelMask = format.channelMask; result.channelCount = format.channels; @@ -205,7 +225,7 @@ KVMFRAudioFormat CLGMPAudioTransport::WireFormat( return result; } -KVMFRAudioStreamState CLGMPAudioTransport::WireState( +KVMFRAudioStreamState CLGMPAudio::WireState( AudioStreamState state) { switch (state) @@ -221,7 +241,7 @@ KVMFRAudioStreamState CLGMPAudioTransport::WireState( } } -KVMFRStreamDescriptor CLGMPAudioTransport::WireDescriptor( +KVMFRStreamDescriptor CLGMPAudio::WireDescriptor( const LGMPStreamDescriptor& descriptor) { KVMFRStreamDescriptor wire = {}; @@ -237,63 +257,120 @@ KVMFRStreamDescriptor CLGMPAudioTransport::WireDescriptor( return wire; } -bool CLGMPAudioTransport::InitializeStreams() +KVMFRSPMCDescriptor CLGMPAudio::WireDescriptor( + const LGMPSPMCDescriptor& descriptor) { - const LGMPStreamConfig hostToClient = + KVMFRSPMCDescriptor wire = {}; + wire.magic = descriptor.magic; + wire.version = descriptor.version; + wire.size = descriptor.size; + wire.offset = descriptor.offset; + wire.regionSize = descriptor.regionSize; + wire.slotCount = descriptor.slotCount; + wire.slotSize = descriptor.slotSize; + wire.maxReaders = descriptor.maxReaders; + wire.reserved = descriptor.reserved; + return wire; +} + +bool CLGMPAudio::InitializeStreams() +{ + const LGMPSPMCConfig playback = { - LGMP_STREAM_HOST_TO_CLIENT, - LGMP_STREAM_RELIABLE_FIFO, KVMFR_AUDIO_STREAM_SLOT_COUNT, - static_cast(KVMFR_AUDIO_STREAM_SLOT_BYTES), + static_cast(KVMFR_AUDIO_PLAYBACK_SLOT_BYTES), + KVMFR_AUDIO_MAX_PLAYBACK_READERS, }; - LGMP_STATUS status = m_host.CreateStream( - hostToClient, &m_hostToClientStream); + LGMP_STATUS status = m_host.CreateSPMC(playback, &m_playbackStream); if (status != LGMP_OK) { - DEBUG_ERROR("Failed to create host-to-client audio stream: %s", + DEBUG_ERROR("Failed to create playback audio stream: %s", lgmpStatusString(status)); return false; } - const LGMPStreamConfig clientToHost = + const LGMPStreamConfig capture = { LGMP_STREAM_CLIENT_TO_HOST, LGMP_STREAM_RELIABLE_FIFO, KVMFR_AUDIO_STREAM_SLOT_COUNT, - static_cast(KVMFR_AUDIO_STREAM_SLOT_BYTES), + static_cast(KVMFR_AUDIO_CAPTURE_SLOT_BYTES), }; - status = m_host.CreateStream(clientToHost, &m_clientToHostStream); + status = m_host.CreateStream(capture, &m_captureStream); if (status != LGMP_OK) { - DEBUG_ERROR("Failed to create client-to-host audio stream: %s", + DEBUG_ERROR("Failed to create capture audio stream: %s", lgmpStatusString(status)); - lgmpHostStreamFree(&m_hostToClientStream); + FreeStreams(); return false; } - LGMPStreamDescriptor descriptor = {}; - lgmpHostStreamGetDescriptor(m_hostToClientStream, &descriptor); - m_hostToClientDescriptor = WireDescriptor(descriptor); - lgmpHostStreamGetDescriptor(m_clientToHostStream, &descriptor); - m_clientToHostDescriptor = WireDescriptor(descriptor); + const LGMPStreamConfig hostControl = + { + LGMP_STREAM_HOST_TO_CLIENT, + LGMP_STREAM_RELIABLE_FIFO, + KVMFR_AUDIO_STREAM_SLOT_COUNT, + static_cast(KVMFR_AUDIO_HOST_CONTROL_SLOT_BYTES), + }; + const LGMPStreamConfig clientControl = + { + LGMP_STREAM_CLIENT_TO_HOST, + LGMP_STREAM_RELIABLE_FIFO, + KVMFR_AUDIO_STREAM_SLOT_COUNT, + static_cast(KVMFR_AUDIO_CLIENT_CONTROL_SLOT_BYTES), + }; + for (Subscriber& subscriber : m_subscribers) + { + status = m_host.CreateStream(hostControl, &subscriber.hostControl); + if (status == LGMP_OK) + status = m_host.CreateStream( + clientControl, &subscriber.clientControl); + if (status != LGMP_OK) + { + DEBUG_ERROR("Failed to create audio control stream: %s", + lgmpStatusString(status)); + FreeStreams(); + return false; + } + + LGMPStreamDescriptor descriptor = {}; + lgmpHostStreamGetDescriptor(subscriber.hostControl, &descriptor); + subscriber.hostControlDescriptor = WireDescriptor(descriptor); + lgmpHostStreamGetDescriptor(subscriber.clientControl, &descriptor); + subscriber.clientControlDescriptor = WireDescriptor(descriptor); + } + + LGMPSPMCDescriptor playbackDescriptor = {}; + lgmpHostSPMCGetDescriptor(m_playbackStream, &playbackDescriptor); + m_playbackDescriptor = WireDescriptor(playbackDescriptor); + LGMPStreamDescriptor captureDescriptor = {}; + lgmpHostStreamGetDescriptor(m_captureStream, &captureDescriptor); + m_captureDescriptor = WireDescriptor(captureDescriptor); return true; } -void CLGMPAudioTransport::FreeStreams() +void CLGMPAudio::FreeStreams() { - lgmpHostStreamFree(&m_clientToHostStream); - lgmpHostStreamFree(&m_hostToClientStream); - m_clientToHostDescriptor = {}; - m_hostToClientDescriptor = {}; + for (Subscriber& subscriber : m_subscribers) + { + lgmpHostStreamFree(&subscriber.clientControl); + lgmpHostStreamFree(&subscriber.hostControl); + subscriber.clientControlDescriptor = {}; + subscriber.hostControlDescriptor = {}; + } + lgmpHostStreamFree(&m_captureStream); + lgmpHostSPMCFree(&m_playbackStream); + m_captureDescriptor = {}; + m_playbackDescriptor = {}; } -DWORD CALLBACK CLGMPAudioTransport::InboundThreadProc(void * context) +DWORD CALLBACK CLGMPAudio::InboundThreadProc(void * context) { - static_cast(context)->InboundThread(); + static_cast(context)->InboundThread(); return 0; } -bool CLGMPAudioTransport::StartInboundWorker() +bool CLGMPAudio::StartInboundWorker() { m_inboundStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); m_inboundWakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); @@ -337,7 +414,7 @@ bool CLGMPAudioTransport::StartInboundWorker() return true; } -void CLGMPAudioTransport::StopInboundWorker() +void CLGMPAudio::StopInboundWorker() { HANDLE thread = nullptr; { @@ -365,7 +442,7 @@ void CLGMPAudioTransport::StopInboundWorker() m_inboundStopEvent = nullptr; } -void CLGMPAudioTransport::InboundThread() +void CLGMPAudio::InboundThread() { DWORD avTask = 0; HANDLE avTaskHandle = @@ -386,6 +463,11 @@ void CLGMPAudioTransport::InboundThread() { DEBUG_ERROR("Failed to initialize LGMP audio polling: %s", lgmpStatusString(pollStatus)); + { + CSRWExclusiveLock lock(m_lock); + if (!m_inboundStopping) + Fail("lgmpStreamPollInit(audio)", pollStatus); + } if (avTaskHandle) AvRevertMmThreadCharacteristics(avTaskHandle); return; @@ -395,20 +477,31 @@ void CLGMPAudioTransport::InboundThread() { m_inboundStopEvent, m_inboundWakeEvent }; const HANDLE activeWaits[] = { m_inboundStopEvent, m_inboundWakeEvent, m_inboundPollTimer }; + bool workerFailed = false; for (;;) { - if (WaitForSingleObject(m_inboundStopEvent, 0) == WAIT_OBJECT_0) + const DWORD stopped = WaitForSingleObject(m_inboundStopEvent, 0); + if (stopped == WAIT_OBJECT_0) break; + if (stopped == WAIT_FAILED) + { + DEBUG_ERROR_HR(GetLastError(), + "LGMP audio stop-event poll failed"); + workerFailed = true; + break; + } uint64_t before = 0; - uint64_t hostBefore = 0; bool active = false; - bool playbackCreditAcquired = false; { CSRWSharedLock lock(m_lock); before = m_inboundActivitySerial; - hostBefore = m_hostSequence; - active = m_ownerState != OwnerState::NONE; + for (const Subscriber& subscriber : m_subscribers) + if (subscriber.state != SubscriberState::NONE) + { + active = true; + break; + } } if (active) @@ -416,21 +509,23 @@ void CLGMPAudioTransport::InboundThread() DrainInbound(); { CSRWExclusiveLock lock(m_lock); - const bool hadPlaybackCredit = m_playbackCreditValid; PumpState(); - CheckPlaybackCredit(); - playbackCreditAcquired = !hadPlaybackCredit && - m_playbackCreditValid; + CheckPlaybackBackpressure(GetTickCount64()); } NotifyPlaybackReady(); } { CSRWSharedLock lock(m_lock); - if (before != m_inboundActivitySerial || - hostBefore != m_hostSequence || playbackCreditAcquired) + if (before != m_inboundActivitySerial) lgmpStreamPollActivity(&streamPoll); - active = m_ownerState != OwnerState::NONE; + active = false; + for (const Subscriber& subscriber : m_subscribers) + if (subscriber.state != SubscriberState::NONE) + { + active = true; + break; + } } if (!active) @@ -443,6 +538,7 @@ void CLGMPAudioTransport::InboundThread() { DEBUG_ERROR_HR(GetLastError(), "LGMP audio worker wait failed"); + workerFailed = true; break; } lgmpStreamPollActivity(&streamPoll); @@ -456,6 +552,7 @@ void CLGMPAudioTransport::InboundThread() { DEBUG_ERROR_HR(GetLastError(), "Failed to arm LGMP audio timer"); + workerFailed = true; break; } @@ -472,25 +569,54 @@ void CLGMPAudioTransport::InboundThread() { DEBUG_ERROR_HR(GetLastError(), "LGMP audio worker wait failed"); + workerFailed = true; break; } } + if (workerFailed) + { + CSRWExclusiveLock lock(m_lock); + if (!m_inboundStopping) + Fail("audio ingress worker", LGMP_ERR_CLOCK_FAILURE); + } if (avTaskHandle) AvRevertMmThreadCharacteristics(avTaskHandle); } -bool CLGMPAudioTransport::Initialize() +bool CLGMPAudio::Initialize() { CSRWExclusiveLock lifecycleLock(m_lifecycleLock); CSRWExclusiveLock lock(m_lock); if (m_initialized) return true; + if (!m_claimResponderReady) + { + 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); + return false; + } + m_claimResponderReady = 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. + // the first data-plane allocation; the small response pool above remains + // available when the optional layout cannot be created. if (m_host.Available() < AUDIO_SHARED_MEMORY_PREFLIGHT) { DEBUG_WARN("LGMP audio requires at least %zu bytes of available " @@ -500,100 +626,212 @@ bool CLGMPAudioTransport::Initialize() 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; m_inboundStopping = false; if (!StartInboundWorker()) { m_initialized = false; - for (ResponseMemory& response : m_responseMemory) - lgmpHostMemFree(&response.memory); FreeStreams(); return false; } return true; } -bool CLGMPAudioTransport::BindStreams(uint32_t clientID) +bool CLGMPAudio::BindSubscriber(uint32_t index, + uint32_t clientID, const KVMFRAudioClaim& claim) { + if (index >= LGMP_MAX_CLIENTS || + m_subscribers[index].state != SubscriberState::NONE) + return false; + + Subscriber& subscriber = m_subscribers[index]; LGMP_STATUS status = lgmpHostStreamBind( - m_hostToClientStream, clientID, nullptr); + subscriber.hostControl, clientID, &subscriber.hostControlEpoch); if (status != LGMP_OK) { - DEBUG_WARN("Failed to bind host-to-client audio stream for client " - "%u: %s", clientID, lgmpStatusString(status)); + DEBUG_WARN("Failed to bind host audio control for client %u: %s", + clientID, lgmpStatusString(status)); return false; } - status = lgmpHostStreamBind(m_clientToHostStream, clientID, nullptr); - if (status == LGMP_OK) - return true; + status = lgmpHostStreamBind( + subscriber.clientControl, clientID, &subscriber.clientControlEpoch); + if (status != LGMP_OK) + { + DEBUG_WARN("Failed to bind client audio control for client %u: %s", + clientID, lgmpStatusString(status)); + (void)lgmpHostStreamUnbind(subscriber.hostControl); + return false; + } - 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; + status = lgmpHostSPMCReaderBind(m_playbackStream, clientID, + &subscriber.playbackReaderID, &subscriber.playbackReaderEpoch); + if (status != LGMP_OK) + { + DEBUG_WARN("Failed to bind playback reader for client %u: %s", + clientID, lgmpStatusString(status)); + (void)lgmpHostStreamUnbind(subscriber.clientControl); + (void)lgmpHostStreamUnbind(subscriber.hostControl); + return false; + } + + subscriber.state = SubscriberState::BOUND; + subscriber.clientID = clientID; + subscriber.requestGeneration = claim.requestGeneration; + subscriber.clientCapabilities = claim.clientCapabilities; + subscriber.subscriptions = claim.subscriptions; + subscriber.bindingEpoch = + m_nextBindingEpoch = NextEpoch(m_nextBindingEpoch); + subscriber.roleEpoch = m_nextRoleEpoch = NextEpoch(m_nextRoleEpoch); + subscriber.claimToken = claim.claimToken; + subscriber.serial = + m_nextSubscriberSerial = NextSerial(m_nextSubscriberSerial); + subscriber.stateRevision = 1; + RenewLease(subscriber); + RefreshGranted(index, false); + return true; } -bool CLGMPAudioTransport::TryUnbindStreams() +bool CLGMPAudio::TryUnbindSubscriber(uint32_t index) { + if (index >= LGMP_MAX_CLIENTS) + return true; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state == SubscriberState::NONE) + return true; + if (m_captureCallbackInFlight && m_captureBinding == index) + return false; + + if (m_pendingCapture.valid && + m_pendingCapture.subscriber == index) + ReleasePendingCapture(); + bool complete = true; - for (PLGMPHostStream stream : - { m_clientToHostStream, m_hostToClientStream }) + if (m_captureBinding == index) + { + const LGMP_STATUS status = lgmpHostStreamUnbind(m_captureStream); + if (status == LGMP_ERR_STREAM_BUSY) + complete = false; + else if (status == LGMP_OK || status == LGMP_ERR_STREAM_UNBOUND) + { + m_captureBinding = INVALID_SLOT; + if (m_captureOwner == index) + m_captureOwner = INVALID_SLOT; + subscriber.captureEpoch = 0; + subscriber.captureRevoking = false; + subscriber.captureRevokeDeadline = 0; + AssignCaptureOwner(); + } + else + { + Fail("lgmpHostStreamUnbind(audio capture)", status); + complete = false; + } + } + + for (PLGMPHostStream stream : + { subscriber.clientControl, subscriber.hostControl }) { - 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); + Fail("lgmpHostStreamUnbind(audio control)", status); complete = false; } } + + const LGMP_STATUS readerStatus = lgmpHostSPMCReaderUnbind( + m_playbackStream, subscriber.playbackReaderID); + if (readerStatus == LGMP_ERR_STREAM_BUSY) + complete = false; + else if (readerStatus != LGMP_OK && + readerStatus != LGMP_ERR_STREAM_UNBOUND) + { + Fail("lgmpHostSPMCReaderUnbind(audio)", readerStatus); + complete = false; + } return complete; } -void CLGMPAudioTransport::ForceUnbindStreams() +void CLGMPAudio::ForceUnbindSubscriber(uint32_t index) { - for (PLGMPHostStream stream : - { m_clientToHostStream, m_hostToClientStream }) + if (index >= LGMP_MAX_CLIENTS) + return; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state == SubscriberState::NONE) + return; + + if (m_pendingCapture.valid && + m_pendingCapture.subscriber == index) + ReleasePendingCapture(); + if (m_captureBinding == index) { - if (!stream) - continue; - const LGMP_STATUS status = lgmpHostStreamForceUnbind(stream); + const LGMP_STATUS status = + lgmpHostStreamForceUnbind(m_captureStream); if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND) - DEBUG_WARN("Failed to force-unbind an audio stream: %s", + DEBUG_WARN("Failed to force-unbind audio capture: %s", lgmpStatusString(status)); } + + for (PLGMPHostStream stream : + { subscriber.clientControl, subscriber.hostControl }) + { + const LGMP_STATUS status = lgmpHostStreamForceUnbind(stream); + if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND) + DEBUG_WARN("Failed to force-unbind audio control: %s", + lgmpStatusString(status)); + } + + const LGMP_STATUS status = lgmpHostSPMCReaderForceUnbind( + m_playbackStream, subscriber.playbackReaderID); + if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND) + DEBUG_WARN("Failed to force-unbind audio reader: %s", + lgmpStatusString(status)); } -bool CLGMPAudioTransport::ValidateClaim( +void CLGMPAudio::ResetSubscriber(uint32_t index) +{ + if (index >= LGMP_MAX_CLIENTS) + return; + Subscriber& subscriber = m_subscribers[index]; + const uint32_t clientID = subscriber.clientID; + const uint32_t requestGeneration = subscriber.requestGeneration; + const uint64_t claimToken = subscriber.claimToken; + PLGMPHostStream hostControl = subscriber.hostControl; + PLGMPHostStream clientControl = subscriber.clientControl; + const KVMFRStreamDescriptor hostDescriptor = + subscriber.hostControlDescriptor; + const KVMFRStreamDescriptor clientDescriptor = + subscriber.clientControlDescriptor; + + for (ClaimRecord& record : m_claimRecord) + if (record.valid && record.clientID == clientID && + record.requestGeneration == requestGeneration && + record.claimToken == claimToken) + { + record.closed = true; + break; + } + + if (m_feedbackAuthority == index) + m_feedbackAuthority = INVALID_SLOT; + if (m_captureOwner == index) + m_captureOwner = INVALID_SLOT; + if (m_captureBinding == index) + m_captureBinding = INVALID_SLOT; + + subscriber = {}; + subscriber.hostControl = hostControl; + subscriber.clientControl = clientControl; + subscriber.hostControlDescriptor = hostDescriptor; + subscriber.clientControlDescriptor = clientDescriptor; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); +} + +bool CLGMPAudio::ValidateClaim( const KVMFRAudioClaim& claim) const { if (claim.messageType != KVMFR_MESSAGE_AUDIO_CLAIM || @@ -604,8 +842,12 @@ bool CLGMPAudioTransport::ValidateClaim( claim.protocolMin > KVMFR_AUDIO_PROTOCOL_MAX || claim.protocolMax < KVMFR_AUDIO_PROTOCOL_MIN || (claim.clientCapabilities & ~AUDIO_CAPABILITY_MASK) || - !(claim.clientCapabilities & AUDIO_DIRECTION_MASK) || - claim.reserved0) + !(claim.clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) || + ((claim.clientCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + !(claim.clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK)) || + (claim.subscriptions & ~AUDIO_SUBSCRIPTION_MASK) || + ((claim.subscriptions & KVMFR_AUDIO_SUBSCRIBE_CAPTURE) && + !(claim.clientCapabilities & KVMFR_AUDIO_CAP_CAPTURE))) return false; for (uint64_t reserved : claim.reserved) @@ -614,7 +856,7 @@ bool CLGMPAudioTransport::ValidateClaim( return true; } -bool CLGMPAudioTransport::ResponsePending(uint32_t clientID, +bool CLGMPAudio::ResponsePending(uint32_t clientID, const KVMFRAudioStreamResponse& candidate) const { for (const PendingResponse& pending : m_pendingResponse) @@ -634,7 +876,7 @@ bool CLGMPAudioTransport::ResponsePending(uint32_t clientID, return false; } -bool CLGMPAudioTransport::QueueResponse(uint32_t clientID, +bool CLGMPAudio::QueueResponse(uint32_t clientID, const KVMFRAudioStreamResponse& response) { if (ResponsePending(clientID, response)) @@ -660,28 +902,27 @@ bool CLGMPAudioTransport::QueueResponse(uint32_t clientID, if (!available) return false; - available->valid = true; + available->valid = true; available->clientID = clientID; available->response = response; return true; } -void CLGMPAudioTransport::CleanupResponseMemory() +void CLGMPAudio::CleanupResponseMemory() { for (ResponseMemory& response : m_responseMemory) { - if (!response.valid || - m_control.PayloadPending(response.memory)) + if (!response.valid || m_control.PayloadPending(response.memory)) continue; - response.valid = false; - response.clientID = 0; + response.valid = false; + response.clientID = 0; response.requestGeneration = 0; - response.claimToken = 0; - response.result = 0; + response.claimToken = 0; + response.result = 0; } } -void CLGMPAudioTransport::PumpResponses() +void CLGMPAudio::PumpResponses() { CleanupResponseMemory(); for (PendingResponse& pending : m_pendingResponse) @@ -716,248 +957,442 @@ void CLGMPAudioTransport::PumpResponses() const bool bound = pending.response.result == KVMFR_AUDIO_RESPONSE_BOUND; + const uint32_t index = bound ? + FindSubscriber(pending.clientID) : INVALID_SLOT; if (recipients) { - available->valid = true; - available->clientID = pending.clientID; + 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) + available->claimToken = pending.response.claimToken; + available->result = pending.response.result; + if (index != INVALID_SLOT) { - m_responseAdmitted = true; - m_statusDirty = true; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.requestGeneration == + pending.response.requestGeneration && + subscriber.claimToken == pending.response.claimToken) + { + subscriber.responseAdmitted = true; + RenewLease(subscriber); + MarkStateDirty(index); + } } } - 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"); + else if (index != INVALID_SLOT) + BeginRelease(index, "audio claimant disappeared"); pending = {}; } } -bool CLGMPAudioTransport::OwnerSubscribed() const +uint32_t CLGMPAudio::FindSubscriber(uint32_t clientID) const { - if (!m_ownerClientID) - return false; + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + if (m_subscribers[index].state != SubscriberState::NONE && + m_subscribers[index].clientID == clientID) + return index; + return INVALID_SLOT; +} - 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; - } +uint32_t CLGMPAudio::FindFreeSubscriber() const +{ + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + if (m_subscribers[index].state == SubscriberState::NONE) + return index; + return INVALID_SLOT; +} + +bool CLGMPAudio::SubscriberPresent(uint32_t clientID, + const uint32_t clients[LGMP_MAX_CLIENTS], unsigned count) const +{ for (unsigned index = 0; index < count; ++index) - if (clients[index] == m_ownerClientID) + if (clients[index] == clientID) return true; return false; } -void CLGMPAudioTransport::RenewLease() +void CLGMPAudio::RenewLease(Subscriber& subscriber) { - m_ownerDeadline = GetTickCount64() + KVMFR_AUDIO_OWNER_LEASE_MS; + subscriber.deadline = + GetTickCount64() + KVMFR_AUDIO_OWNER_LEASE_MS; } -void CLGMPAudioTransport::ReleasePendingCapture() +void CLGMPAudio::ReleasePendingCapture() { if (!m_pendingCapture.valid) return; const LGMP_STATUS status = lgmpHostStreamReadRelease( - m_clientToHostStream, &m_pendingCapture.buffer); + m_captureStream, &m_pendingCapture.buffer); if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND && status != LGMP_ERR_STREAM_STALE) - Fail("lgmpHostStreamReadRelease(audio)", status); + Fail("lgmpHostStreamReadRelease(audio capture)", status); m_pendingCapture = {}; m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); } -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) +void CLGMPAudio::ClearBlockedPlayback(bool notify) { const bool blocked = m_playbackBlocked; - CancelPlaybackCredit(); m_playbackBlocked = false; m_blockedPlayback = {}; m_blockedPlaybackSize = 0; + m_authorityBlockedSince = 0; if (notify && blocked) m_playbackReadyPending = true; } -bool CLGMPAudioTransport::RememberBlockedPlayback( +bool CLGMPAudio::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) +bool CLGMPAudio::HasBoundSubscribers() const { - 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); - if (!m_captureCallbackInFlight) - ReleasePendingCapture(); - if (!m_inboundStopping && m_inboundWakeEvent) - SetEvent(m_inboundWakeEvent); - - for (PendingResponse& pending : m_pendingResponse) - if (pending.valid && pending.clientID == m_ownerClientID && - pending.response.result == KVMFR_AUDIO_RESPONSE_BOUND) - pending = {}; - } + for (const Subscriber& subscriber : m_subscribers) + if (subscriber.state == SubscriberState::BOUND && + subscriber.responseAdmitted && + subscriber.subscriptionReceived && + (((subscriber.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_PLAYBACK) && + (subscriber.clientCapabilities & + KVMFR_AUDIO_CAP_PLAYBACK)) || + ((subscriber.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_CAPTURE) && + (subscriber.clientCapabilities & + KVMFR_AUDIO_CAP_CAPTURE)))) + return true; + return false; } -void CLGMPAudioTransport::ResetOwner() +bool CLGMPAudio::PlaybackEligible( + const Subscriber& subscriber) const { - 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_captureCallbackInFlight = false; - m_pendingCapture = {}; - m_ownerSerial = NextSerial(m_ownerSerial); + return subscriber.state == SubscriberState::BOUND && + (subscriber.clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) && + (subscriber.subscriptions & KVMFR_AUDIO_SUBSCRIBE_PLAYBACK); } -void CLGMPAudioTransport::MaintainOwner(uint64_t now) +bool CLGMPAudio::FeedbackEligible( + const Subscriber& subscriber) const { - 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(); - } + return PlaybackEligible(subscriber) && + (subscriber.clientCapabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK); } -void CLGMPAudioTransport::InitializeOwner(uint32_t clientID, - const KVMFRAudioClaim& claim) +bool CLGMPAudio::CaptureEligible( + const Subscriber& subscriber) const { - 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(); + return subscriber.state == SubscriberState::BOUND && + (subscriber.clientCapabilities & KVMFR_AUDIO_CAP_CAPTURE) && + (subscriber.subscriptions & KVMFR_AUDIO_SUBSCRIBE_CAPTURE); +} + +bool CLGMPAudio::HasReadyPlayback() const +{ + for (const Subscriber& subscriber : m_subscribers) + if (PlaybackEligible(subscriber) && subscriber.ready && + subscriber.announcedPlayback == + m_state.playback.generation) + return true; + return false; +} + +void CLGMPAudio::MarkStateDirty(uint32_t index) +{ + if (index >= LGMP_MAX_CLIENTS || + m_subscribers[index].state != SubscriberState::BOUND) + return; + Subscriber& subscriber = m_subscribers[index]; + subscriber.stateRevision = NextSerial(subscriber.stateRevision); + subscriber.pumpStage = PumpStage::IDLE; + subscriber.ready = false; + subscriber.awaitingAck = false; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); if (!m_inboundStopping && m_inboundWakeEvent) SetEvent(m_inboundWakeEvent); } -void CLGMPAudioTransport::HandleClaim( - uint32_t clientID, const KVMFRAudioClaim& claim) +void CLGMPAudio::MarkAllStateDirty() +{ + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + MarkStateDirty(index); +} + +void CLGMPAudio::RefreshGranted(uint32_t index, bool bumpRole) +{ + if (index >= LGMP_MAX_CLIENTS) + return; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state != SubscriberState::BOUND) + return; + + KVMFRAudioCapabilities granted = 0; + if (m_stateKnown && + (m_state.capabilities & KVMFR_AUDIO_CAP_PLAYBACK) && + (subscriber.clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK)) + granted |= KVMFR_AUDIO_CAP_PLAYBACK; + if (m_captureOwner == index && CaptureEligible(subscriber) && + (m_state.capabilities & KVMFR_AUDIO_CAP_CAPTURE)) + granted |= KVMFR_AUDIO_CAP_CAPTURE; + if (m_feedbackAuthority == index && FeedbackEligible(subscriber) && + (m_state.capabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)) + granted |= KVMFR_AUDIO_CAP_CLOCK_FEEDBACK; + if (granted == subscriber.grantedCapabilities) + return; + + subscriber.grantedCapabilities = granted; + if (bumpRole && subscriber.subscriptionReceived) + subscriber.roleEpoch = + m_nextRoleEpoch = NextEpoch(m_nextRoleEpoch); + subscriber.response.grantedCapabilities = + granted | KVMFR_AUDIO_CAP_PLAYBACK; + subscriber.response.roleEpoch = subscriber.roleEpoch; + MarkStateDirty(index); +} + +void CLGMPAudio::AssignFeedbackAuthority(uint32_t index) +{ + if (m_feedbackAuthority != INVALID_SLOT || + index >= LGMP_MAX_CLIENTS || + !FeedbackEligible(m_subscribers[index])) + return; + m_feedbackAuthority = index; + RefreshGranted(index, true); + DEBUG_INFO("Audio feedback authority assigned to client %u", + m_subscribers[index].clientID); +} + +void CLGMPAudio::RevokeFeedbackAuthority(const char * reason) +{ + if (m_feedbackAuthority == INVALID_SLOT) + return; + const uint32_t index = m_feedbackAuthority; + m_feedbackAuthority = INVALID_SLOT; + m_subscribers[index].feedbackRevoked = true; + DEBUG_INFO("Audio feedback authority client %u revoked: %s", + m_subscribers[index].clientID, reason); + RefreshGranted(index, true); + m_authorityBlockedSince = 0; + if (m_playbackBlocked) + ClearBlockedPlayback(true); +} + +void CLGMPAudio::PromoteFeedbackAuthority( + uint64_t playbackGeneration) +{ + if (!playbackGeneration || m_feedbackAuthority != INVALID_SLOT) + return; + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + if (FeedbackEligible(m_subscribers[index]) && + !m_subscribers[index].feedbackRevoked) + { + AssignFeedbackAuthority(index); + return; + } +} + +void CLGMPAudio::AssignCaptureOwner() +{ + if (m_captureOwner != INVALID_SLOT || + m_captureBinding != INVALID_SLOT) + return; + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + { + Subscriber& subscriber = m_subscribers[index]; + if (!CaptureEligible(subscriber)) + continue; + uint32_t epoch = 0; + const LGMP_STATUS status = lgmpHostStreamBind( + m_captureStream, subscriber.clientID, &epoch); + if (status == LGMP_ERR_STREAM_BUSY) + return; + if (status != LGMP_OK) + { + Fail("lgmpHostStreamBind(audio capture)", status); + return; + } + m_captureOwner = index; + m_captureBinding = index; + subscriber.captureEpoch = epoch; + subscriber.nextCaptureSequence = 0; + subscriber.captureSequenceKnown = false; + subscriber.captureDiscontinuity = true; + subscriber.captureRevokeDeadline = 0; + RefreshGranted(index, true); + DEBUG_INFO("Audio capture assigned to client %u", + subscriber.clientID); + return; + } +} + +void CLGMPAudio::MaintainCaptureBinding() +{ + if (m_captureBinding == INVALID_SLOT) + { + AssignCaptureOwner(); + return; + } + + const uint32_t index = m_captureBinding; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state != SubscriberState::BOUND || + !subscriber.captureRevoking || m_captureCallbackInFlight) + return; + + const bool acknowledged = + subscriber.pumpRevision >= subscriber.captureRevokeRevision && + !subscriber.awaitingAck && subscriber.ready; + const uint64_t now = GetTickCount64(); + const bool timedOut = subscriber.captureRevokeDeadline && + now >= subscriber.captureRevokeDeadline; + if (!acknowledged && !timedOut) + return; + if (!acknowledged && subscriber.captureRevokeDeadline != 1) + { + DEBUG_WARN("Audio capture revoke acknowledgement timed out for " + "client %u", subscriber.clientID); + subscriber.captureRevokeDeadline = 1; + } + + if (m_pendingCapture.valid && + m_pendingCapture.subscriber == index) + ReleasePendingCapture(); + const LGMP_STATUS status = lgmpHostStreamUnbind(m_captureStream); + if (status == LGMP_ERR_STREAM_BUSY) + return; + if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND) + { + Fail("lgmpHostStreamUnbind(audio capture role)", status); + return; + } + + m_captureBinding = INVALID_SLOT; + subscriber.captureEpoch = 0; + subscriber.captureRevoking = false; + subscriber.captureRevokeDeadline = 0; + AssignCaptureOwner(); +} + +void CLGMPAudio::BeginRelease(uint32_t index, + const char * reason, bool force) +{ + if (index >= LGMP_MAX_CLIENTS) + return; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state == SubscriberState::NONE) + return; + subscriber.forceUnbind = subscriber.forceUnbind || force; + if (subscriber.state == SubscriberState::RELEASING) + return; + + DEBUG_INFO("Audio client %u generation %u releasing: %s", + subscriber.clientID, subscriber.requestGeneration, reason); + subscriber.state = SubscriberState::RELEASING; + subscriber.ready = false; + subscriber.awaitingAck = false; + subscriber.pumpStage = PumpStage::IDLE; + if (m_feedbackAuthority == index) + RevokeFeedbackAuthority(reason); + if (m_captureOwner == index) + m_captureOwner = INVALID_SLOT; + if (m_pendingCapture.valid && + m_pendingCapture.subscriber == index && + !m_captureCallbackInFlight) + ReleasePendingCapture(); + + for (PendingResponse& pending : m_pendingResponse) + if (pending.valid && pending.clientID == subscriber.clientID && + pending.response.result == KVMFR_AUDIO_RESPONSE_BOUND) + pending = {}; + if (!HasReadyPlayback()) + ClearBlockedPlayback(true); + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); +} + +void CLGMPAudio::MaintainSubscribers(uint64_t now) +{ + MaintainCaptureBinding(); + uint32_t clients[LGMP_MAX_CLIENTS] = {}; + unsigned count = 0; + const LGMP_STATUS query = m_control.GetClientIDs(clients, count); + const bool clientsKnown = query == LGMP_OK; + if (!clientsKnown) + DEBUG_WARN("Failed to query audio pointer subscribers: %s", + lgmpStatusString(query)); + + bool assignCapture = false; + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + { + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state == SubscriberState::NONE) + continue; + if (clientsKnown && !SubscriberPresent( + subscriber.clientID, clients, count)) + subscriber.forceUnbind = true; + if (subscriber.state == SubscriberState::BOUND) + { + if (subscriber.forceUnbind) + BeginRelease(index, + "audio pointer subscriber disappeared", true); + else if (m_pendingCapture.valid && + m_pendingCapture.subscriber == index) + RenewLease(subscriber); + else if (now >= subscriber.deadline) + BeginRelease(index, "audio client lease expired"); + } + + if (subscriber.state != SubscriberState::RELEASING) + continue; + if (m_captureCallbackInFlight && m_captureBinding == index) + continue; + if (subscriber.forceUnbind) + { + ForceUnbindSubscriber(index); + const bool wasCapture = m_captureOwner == index; + ResetSubscriber(index); + assignCapture = assignCapture || wasCapture; + continue; + } + if (TryUnbindSubscriber(index)) + { + DEBUG_INFO("Audio client %u generation %u released", + subscriber.clientID, subscriber.requestGeneration); + const bool wasCapture = m_captureOwner == index; + ResetSubscriber(index); + assignCapture = assignCapture || wasCapture; + } + } + if (assignCapture || (m_captureOwner == INVALID_SLOT && + m_captureBinding == INVALID_SLOT)) + AssignCaptureOwner(); +} + +void CLGMPAudio::HandleClaim(uint32_t clientID, + const KVMFRAudioClaim& claim) { CSRWExclusiveLock lock(m_lock); - if (!m_initialized) + if (!m_claimResponderReady) return; KVMFRAudioStreamResponse response = {}; - response.magic = KVMFR_AUDIO_RESPONSE_MAGIC; - response.version = KVMFR_AUDIO_CONTROL_VERSION; - response.size = sizeof(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.boundClientID = clientID; response.endpointGeneration = m_state.generation; - response.claimToken = claim.claimToken; + response.claimToken = claim.claimToken; ClaimRecord * claimRecord = nullptr; ClaimRecord * claimSlot = nullptr; @@ -972,19 +1407,20 @@ void CLGMPAudioTransport::HandleClaim( claimSlot = claimRecord; const bool claimValid = ValidateClaim(claim); + const uint32_t existing = FindSubscriber(clientID); if (!claimValid) response.result = KVMFR_AUDIO_RESPONSE_INCOMPATIBLE; - else if (m_failed || !m_started || !m_stateKnown) + else if (!m_initialized || m_failed) response.result = KVMFR_AUDIO_RESPONSE_ERROR; - else if (m_ownerState != OwnerState::NONE) + else if (existing != INVALID_SLOT) { - if (m_ownerState != OwnerState::RELEASING && - m_ownerClientID == clientID && - m_ownerGeneration == claim.requestGeneration && - m_ownerToken == claim.claimToken) + Subscriber& subscriber = m_subscribers[existing]; + if (subscriber.state == SubscriberState::BOUND && + subscriber.requestGeneration == claim.requestGeneration && + subscriber.claimToken == claim.claimToken) { - RenewLease(); - response = m_ownerResponse; + RenewLease(subscriber); + response = subscriber.response; } else response.result = KVMFR_AUDIO_RESPONSE_BUSY; @@ -1006,65 +1442,117 @@ void CLGMPAudioTransport::HandleClaim( 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; + { + const uint32_t index = FindFreeSubscriber(); + if (index == INVALID_SLOT) + response.result = KVMFR_AUDIO_RESPONSE_BUSY; + else if (!BindSubscriber(index, clientID, claim)) + response.result = KVMFR_AUDIO_RESPONSE_ERROR; + else + { + Subscriber& subscriber = m_subscribers[index]; + // Admission grants playback independently from the live subscription. + // The subscription controls whether this reader consumes PCM. + if (m_feedbackAuthority == INVALID_SLOT && + (claim.subscriptions & KVMFR_AUDIO_SUBSCRIBE_PLAYBACK) && + (claim.clientCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)) + AssignFeedbackAuthority(index); + AssignCaptureOwner(); + RefreshGranted(index, false); + + response.result = KVMFR_AUDIO_RESPONSE_BOUND; + response.protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX; + response.grantedCapabilities = + subscriber.grantedCapabilities | KVMFR_AUDIO_CAP_PLAYBACK; + response.bindingEpoch = subscriber.bindingEpoch; + response.roleEpoch = subscriber.roleEpoch; + response.playbackReaderID = subscriber.playbackReaderID; + response.playback = m_playbackDescriptor; + response.hostControl = subscriber.hostControlDescriptor; + response.clientControl = subscriber.clientControlDescriptor; + response.capture = m_captureDescriptor; + subscriber.response = response; + + claimSlot->valid = true; + claimSlot->closed = false; + claimSlot->clientID = clientID; + claimSlot->requestGeneration = claim.requestGeneration; + claimSlot->claimToken = claim.claimToken; + claimSlot->response = response; + DEBUG_INFO("Audio client %u generation %u admitted in slot %u", + clientID, claim.requestGeneration, index); + } + } 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->valid = true; + claimSlot->closed = true; + claimSlot->clientID = clientID; claimSlot->requestGeneration = claim.requestGeneration; - claimSlot->claimToken = claim.claimToken; - claimSlot->response = response; + 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"); + if (response.result == KVMFR_AUDIO_RESPONSE_BOUND) + { + const uint32_t index = FindSubscriber(clientID); + if (index != INVALID_SLOT) + BeginRelease(index, "audio response queue exhausted"); + } } PumpResponses(); } -KVMFRAudioStatus CLGMPAudioTransport::WireStatus() const +KVMFRAudioStatus CLGMPAudio::WireStatus(uint32_t index) 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; + if (index >= LGMP_MAX_CLIENTS) + return status; + const Subscriber& subscriber = m_subscribers[index]; + + status.version = KVMFR_AUDIO_STATUS_VERSION; + status.size = sizeof(status); + status.protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX; + status.requestGeneration = subscriber.requestGeneration; + status.capabilities = + m_state.capabilities & subscriber.clientCapabilities; + status.grantedCapabilities = + subscriber.grantedCapabilities & status.capabilities; + status.bindingEpoch = subscriber.bindingEpoch; + status.roleEpoch = subscriber.roleEpoch; + status.boundClientID = subscriber.clientID; 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); + status.captureGeneration = m_state.capture.generation; + status.playbackState = WireState(m_state.playback.state); + status.captureState = WireState(m_state.capture.state); + + if ((status.capabilities & KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + m_feedbackAuthority != INVALID_SLOT) + { + const Subscriber& authority = + m_subscribers[m_feedbackAuthority]; + if (authority.state == SubscriberState::BOUND) + { + status.feedbackAuthorityClientID = authority.clientID; + status.feedbackAuthorityEpoch = authority.roleEpoch; + } + } + if ((status.capabilities & KVMFR_AUDIO_CAP_CAPTURE) && + m_captureOwner != INVALID_SLOT && + m_subscribers[m_captureOwner].state == SubscriberState::BOUND) + status.captureOwnerClientID = + m_subscribers[m_captureOwner].clientID; + if (m_state.playback.formatValid && ValidFormat(m_state.playback.format)) status.playbackFormat = WireFormat(m_state.playback.format); @@ -1074,252 +1562,396 @@ KVMFRAudioStatus CLGMPAudioTransport::WireStatus() const if (m_state.available) status.flags |= KVMFR_AUDIO_STATUS_AVAILABLE; - if (m_ownerState != OwnerState::NONE) - status.flags |= KVMFR_AUDIO_STATUS_HAS_OWNER; + if (subscriber.state == SubscriberState::BOUND) + status.flags |= KVMFR_AUDIO_STATUS_BOUND; 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 (m_announcedPlayback) - status.flags |= KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE; - if (m_announcedCapture) - 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; - - if (m_state.playback.state == AudioStreamState::RUNNING) - m_playbackRunGeneration = m_state.playback.generation; const bool playbackPrewarm = (m_state.playback.state == AudioStreamState::ACQUIRED || m_state.playback.state == AudioStreamState::PAUSED) && m_playbackRunGeneration != m_state.playback.generation; - 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::RUNNING || - playbackPrewarm); - 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; - } + if (m_state.available && m_state.active && + PlaybackEligible(subscriber) && + (status.grantedCapabilities & KVMFR_AUDIO_CAP_PLAYBACK) && + m_state.playback.generation && + m_state.playback.formatValid && + ValidFormat(m_state.playback.format) && + (m_state.playback.state == AudioStreamState::RUNNING || + playbackPrewarm)) + status.flags |= KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE; - 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::RUNNING; - 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; - } - - const bool startPlayback = playbackCurrent && - m_announcedPlayback != m_state.playback.generation; - const bool startCapture = captureCurrent && - m_announcedCapture != m_state.capture.generation; - - // Publish capabilities before starts so the client selects the correct - // rate-control path. A second status below records the announced streams. - if (m_statusDirty && (startPlayback || startCapture)) - { - const KVMFRAudioStatus status = WireStatus(); - if (!WriteMessage(KVMFR_AUDIO_MESSAGE_STATUS, - 0, 0, 0, 0, 0, 0, &status, sizeof(status))) - return; - m_statusDirty = false; - } - - if (startPlayback) - { - const KVMFRAudioFormat format = WireFormat(m_state.playback.format); - if (!WriteMessage(KVMFR_AUDIO_MESSAGE_PLAYBACK_START, - m_state.playback.generation, 0, 0, 0, 0, 0, - &format, sizeof(format))) - return; - m_announcedPlayback = m_state.playback.generation; - m_statusDirty = true; - } - - if (startCapture) - { - 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; - m_statusDirty = true; - } - - 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; + if (m_state.available && m_state.active && + CaptureEligible(subscriber) && + (status.grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE) && + m_state.capture.generation && + m_state.capture.formatValid && + ValidFormat(m_state.capture.format) && + m_state.capture.state == AudioStreamState::RUNNING) + status.flags |= KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE; + return status; } -void CLGMPAudioTransport::CheckPlaybackCredit() +bool CLGMPAudio::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) { - 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) || + if (index >= LGMP_MAX_CLIENTS) + return false; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state != SubscriberState::BOUND) + return false; + + LGMPStreamBuffer buffer = {}; + LGMP_STATUS status = lgmpHostStreamWriteAcquire( + subscriber.hostControl, &buffer); + if (status == LGMP_ERR_STREAM_FULL) + return false; + if (status == LGMP_ERR_STREAM_UNBOUND || + status == LGMP_ERR_STREAM_STALE) + { + BeginRelease(index, "host audio control binding lost"); + return false; + } + if (status != LGMP_OK) + { + Fail("lgmpHostStreamWriteAcquire(audio control)", status); + return false; + } + + const uint32_t bytes = + static_cast(sizeof(KVMFRAudioMessage) + length); + if (!buffer.data || buffer.capacity < bytes || (length && !payload)) + { + const LGMP_STATUS cancel = lgmpHostStreamWriteCancel( + subscriber.hostControl, &buffer); + if (cancel != LGMP_OK) + DEBUG_WARN("Failed to cancel audio control reservation: %s", + lgmpStatusString(cancel)); + Fail("undersized host audio control 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 = subscriber.hostSequence; + message.position = position; + message.timeNs = timeNs; + message.rateQ32 = rateQ32; + message.frames = frames; + message.length = length; + WCWrite::Copy(buffer.data, &message, sizeof(message)); + if (length) + WCWrite::Copy(static_cast(buffer.data) + + sizeof(message), payload, length); + WCWrite::Flush(); + + status = lgmpHostStreamWriteCommit( + subscriber.hostControl, &buffer, bytes); + if (status == LGMP_OK) + { + ++subscriber.hostSequence; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); + return true; + } + if (status == LGMP_ERR_STREAM_UNBOUND || + status == LGMP_ERR_STREAM_STALE) + BeginRelease(index, "host audio control binding lost"); + else + Fail("lgmpHostStreamWriteCommit(audio control)", status); + return false; +} + +bool CLGMPAudio::PlaybackCurrent( + const KVMFRAudioStatus& status) const +{ + return (status.flags & KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE) != 0; +} + +bool CLGMPAudio::CaptureCurrent( + const KVMFRAudioStatus& status) const +{ + return (status.flags & KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) != 0; +} + +void CLGMPAudio::BeginStatePump(uint32_t index) +{ + Subscriber& subscriber = m_subscribers[index]; + subscriber.pendingStatus = WireStatus(index); + subscriber.pumpRevision = subscriber.stateRevision; + subscriber.pumpStage = PumpStage::STATUS; + subscriber.ready = false; + subscriber.awaitingAck = false; +} + +void CLGMPAudio::PumpSubscriberState(uint32_t index) +{ + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state != SubscriberState::BOUND || + !subscriber.responseAdmitted || + !subscriber.subscriptionReceived || !m_stateKnown || m_failed) + return; + if (subscriber.pumpRevision != subscriber.stateRevision) + BeginStatePump(index); + else if (subscriber.pumpStage == PumpStage::IDLE) + return; + + for (unsigned step = 0; step < 12U; ++step) + { + if (subscriber.state != SubscriberState::BOUND || + subscriber.pumpRevision != subscriber.stateRevision) + return; + const KVMFRAudioStatus& status = subscriber.pendingStatus; + switch (subscriber.pumpStage) + { + case PumpStage::STATUS: + if (!WriteControlMessage(index, KVMFR_AUDIO_MESSAGE_STATUS, + 0, 0, 0, 0, 0, 0, &status, sizeof(status))) + return; + subscriber.pumpStage = PumpStage::PLAYBACK_STOP; + break; + + case PumpStage::PLAYBACK_STOP: + if (subscriber.announcedPlayback && + (!PlaybackCurrent(status) || + subscriber.announcedPlayback != + status.playbackGeneration)) + { + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, + subscriber.announcedPlayback, 0, 0, 0, 0, 0, + nullptr, 0)) + return; + subscriber.announcedPlayback = 0; + } + subscriber.pumpStage = PumpStage::CAPTURE_STOP; + break; + + case PumpStage::CAPTURE_STOP: + if (subscriber.announcedCapture && + (!CaptureCurrent(status) || + subscriber.announcedCapture != status.captureGeneration)) + { + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_CAPTURE_STOP, + subscriber.announcedCapture, 0, 0, 0, 0, 0, + nullptr, 0)) + return; + subscriber.announcedCapture = 0; + } + subscriber.pumpStage = PumpStage::PLAYBACK_START; + break; + + case PumpStage::PLAYBACK_START: + if (PlaybackCurrent(status) && + subscriber.announcedPlayback != status.playbackGeneration) + { + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_PLAYBACK_START, + status.playbackGeneration, 0, 0, 0, 0, 0, + &status.playbackFormat, sizeof(status.playbackFormat))) + return; + subscriber.announcedPlayback = status.playbackGeneration; + subscriber.pumpStage = PumpStage::PLAYBACK_VOLUME; + } + else + subscriber.pumpStage = PumpStage::CAPTURE_START; + break; + + case PumpStage::PLAYBACK_VOLUME: + { + KVMFRAudioVolume volume = {}; + volume.version = KVMFR_AUDIO_VOLUME_VERSION; + volume.size = sizeof(volume); + volume.channelCount = status.playbackFormat.channelCount; + for (unsigned channel = 0; + channel < volume.channelCount; ++channel) + volume.values[channel] = UINT16_MAX; + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME, + status.playbackGeneration, 0, 0, 0, 0, 0, + &volume, sizeof(volume))) + return; + subscriber.pumpStage = PumpStage::PLAYBACK_MUTE; + break; + } + + case PumpStage::PLAYBACK_MUTE: + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_PLAYBACK_MUTE, + status.playbackGeneration, 0, 0, 0, 0, 0, + nullptr, 0)) + return; + subscriber.pumpStage = PumpStage::CAPTURE_START; + break; + + case PumpStage::CAPTURE_START: + if (CaptureCurrent(status) && + subscriber.announcedCapture != status.captureGeneration) + { + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_CAPTURE_START, + status.captureGeneration, 0, 0, 0, 0, 0, + &status.captureFormat, sizeof(status.captureFormat))) + return; + subscriber.announcedCapture = status.captureGeneration; + subscriber.pumpStage = PumpStage::CAPTURE_VOLUME; + } + else + subscriber.pumpStage = PumpStage::BARRIER; + break; + + case PumpStage::CAPTURE_VOLUME: + { + KVMFRAudioVolume volume = {}; + volume.version = KVMFR_AUDIO_VOLUME_VERSION; + volume.size = sizeof(volume); + volume.channelCount = status.captureFormat.channelCount; + for (unsigned channel = 0; + channel < volume.channelCount; ++channel) + volume.values[channel] = UINT16_MAX; + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_CAPTURE_VOLUME, + status.captureGeneration, 0, 0, 0, 0, 0, + &volume, sizeof(volume))) + return; + subscriber.pumpStage = PumpStage::CAPTURE_MUTE; + break; + } + + case PumpStage::CAPTURE_MUTE: + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_CAPTURE_MUTE, + status.captureGeneration, 0, 0, 0, 0, 0, + nullptr, 0)) + return; + subscriber.pumpStage = PumpStage::BARRIER; + break; + + case PumpStage::BARRIER: + { + KVMFRAudioStateBarrier barrier = {}; + barrier.bindingEpoch = subscriber.bindingEpoch; + barrier.roleEpoch = subscriber.roleEpoch; + barrier.stateSerial = NextSerial(subscriber.barrier.stateSerial); + const uint64_t generation = PlaybackCurrent(status) ? + status.playbackGeneration : 0; + if (!WriteControlMessage(index, + KVMFR_AUDIO_MESSAGE_STATE_BARRIER, generation, + 0, 0, 0, 0, 0, &barrier, sizeof(barrier))) + return; + subscriber.barrier = barrier; + subscriber.barrierGeneration = generation; + subscriber.awaitingAck = true; + subscriber.pumpStage = PumpStage::IDLE; + break; + } + + case PumpStage::IDLE: + return; + } + } +} + +void CLGMPAudio::PumpState() +{ + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + PumpSubscriberState(index); +} + +void CLGMPAudio::CheckPlaybackBackpressure(uint64_t now) +{ + if (!m_playbackBlocked) + return; + if (!m_started || m_failed || !m_stateKnown || + !m_state.available || !m_state.active || 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) + m_state.playback.format)) { - BeginRelease("host-to-client audio credit binding lost"); - return; - } - if (status != LGMP_OK) - { - Fail("lgmpHostStreamWriteAcquire(audio credit)", status); + ClearBlockedPlayback(true); return; } - const size_t required = sizeof(KVMFRAudioMessage) + - m_blockedPlaybackSize; - if (!m_playbackCredit.data || - m_playbackCredit.capacity < required) + bool eligible = false; + for (const Subscriber& subscriber : m_subscribers) + if (subscriber.responseAdmitted && + subscriber.subscriptionReceived && + PlaybackEligible(subscriber)) + { + eligible = true; + break; + } + if (!eligible) { - 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); + ClearBlockedPlayback(true); + return; + } + if (!HasReadyPlayback()) + return; + + if (m_feedbackAuthority == INVALID_SLOT) + { + ClearBlockedPlayback(true); + return; + } + Subscriber& authority = m_subscribers[m_feedbackAuthority]; + if (!authority.ready || !PlaybackEligible(authority) || + !(authority.grantedCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)) + { + ClearBlockedPlayback(true); return; } - m_playbackCreditValid = true; - m_playbackReadyPending = true; + LGMPSPMCReaderState reader = {}; + const LGMP_STATUS status = lgmpHostSPMCReaderGetState( + m_playbackStream, authority.playbackReaderID, &reader); + if (status == LGMP_ERR_STREAM_BUSY) + return; + bool stalled = false; + if (status == LGMP_OK) + { + if (reader.state != LGMP_SPMC_READER_READY || + reader.clientID != authority.clientID || + reader.epoch != authority.playbackReaderEpoch) + { + RevokeFeedbackAuthority("playback reader binding lost"); + return; + } + stalled = reader.producerSequence - reader.consumerSequence >= + KVMFR_AUDIO_STREAM_SLOT_COUNT; + } + else + { + Fail("lgmpHostSPMCReaderGetState(audio)", status); + return; + } + + if (!stalled) + { + ClearBlockedPlayback(true); + return; + } + if (!m_authorityBlockedSince) + m_authorityBlockedSince = now; + else if (now - m_authorityBlockedSince >= AUTHORITY_STALL_MS) + RevokeFeedbackAuthority("playback reader stalled"); } -bool CLGMPAudioTransport::ParseInbound( +bool CLGMPAudio::ParseInbound( const LGMPStreamBuffer& buffer, KVMFRAudioMessage& message) const { if (!buffer.data || buffer.size < sizeof(message) || @@ -1335,15 +1967,21 @@ bool CLGMPAudioTransport::ParseInbound( return buffer.size == sizeof(message) + message.length; } -bool CLGMPAudioTransport::PendingCaptureCurrent() const +bool CLGMPAudio::PendingCaptureCurrent(uint32_t index) const { - if (!m_pendingCapture.valid) + if (!m_pendingCapture.valid || index >= LGMP_MAX_CLIENTS || + m_pendingCapture.subscriber != index) return false; - + const Subscriber& subscriber = m_subscribers[index]; 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) && + return subscriber.state == SubscriberState::BOUND && + subscriber.serial == m_pendingCapture.subscriberSerial && + m_captureOwner == index && m_captureBinding == index && + CaptureEligible(subscriber) && + (subscriber.grantedCapabilities & KVMFR_AUDIO_CAP_CAPTURE) && + subscriber.announcedCapture == packet.streamGeneration && + m_stateKnown && m_state.available && m_state.active && + (m_state.capabilities & KVMFR_AUDIO_CAP_CAPTURE) && m_state.capture.formatValid && ValidFormat(m_state.capture.format) && m_state.capture.state == AudioStreamState::RUNNING && @@ -1352,17 +1990,23 @@ bool CLGMPAudioTransport::PendingCaptureCurrent() const SameFormat(packet.format, m_state.capture.format); } -bool CLGMPAudioTransport::PrepareCapture( - const KVMFRAudioMessage& message, const LGMPStreamBuffer& buffer) +bool CLGMPAudio::PrepareCapture( + const KVMFRAudioMessage& message, const LGMPStreamBuffer& buffer, + uint32_t index) { - if (!m_stateKnown || !m_state.available || !m_state.active || - !(m_state.capabilities & AUDIO_CAP_CAPTURE) || + if (index >= LGMP_MAX_CLIENTS || + m_subscribers[index].state != SubscriberState::BOUND || + m_captureOwner != index || m_captureBinding != index || + !m_stateKnown || + !m_state.available || !m_state.active || + !(m_state.capabilities & KVMFR_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; + Subscriber& subscriber = m_subscribers[index]; const bool silent = (message.flags & KVMFR_AUDIO_MESSAGE_SILENT) != 0; const uint32_t bytes = @@ -1371,9 +2015,9 @@ bool CLGMPAudioTransport::PrepareCapture( (silent ? message.length != 0 : message.length != bytes)) return false; - AudioPacket packet; + AudioPacket packet = {}; packet.endpointGeneration = m_state.generation; - packet.streamGeneration = message.streamGeneration; + packet.streamGeneration = message.streamGeneration; if (message.flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY) packet.flags |= AUDIO_PACKET_DISCONTINUITY; if (message.flags & KVMFR_AUDIO_MESSAGE_SILENT) @@ -1382,345 +2026,526 @@ bool CLGMPAudioTransport::PrepareCapture( 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)) + if (subscriber.captureDiscontinuity) packet.flags |= AUDIO_PACKET_DISCONTINUITY; - packet.frames = message.frames; + packet.frames = message.frames; packet.position = message.position; - packet.timeNs = message.timeNs; - packet.rateQ32 = message.rateQ32; - packet.format = m_state.capture.format; + 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.valid = true; + m_pendingCapture.subscriber = index; + m_pendingCapture.subscriberSerial = subscriber.serial; + 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.size = message.length; m_pendingCapture.readySerial = m_captureReadySerial; - m_nextClientSequence = message.sequence + 1; - m_clientSequenceKnown = true; m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); return true; } -void CLGMPAudioTransport::DrainInbound() +void CLGMPAudio::DrainControl(uint32_t index) { for (unsigned count = 0; count < INBOUND_DRAIN_MAX; ++count) { CSRWSharedLock callbackLock(m_callbackLock); IAudioTarget * target = nullptr; - AudioClockFeedback feedback; - bool callCapture = false; + AudioClockFeedback feedback = {}; bool callClock = false; - uint64_t ownerSerial = 0; + uint64_t subscriberSerial = 0; + uint32_t feedbackRoleEpoch = 0; { CSRWExclusiveLock lock(m_lock); - if (m_ownerState == OwnerState::RELEASING) - { - ReleasePendingCapture(); - LGMPStreamBuffer buffer = {}; - const LGMP_STATUS status = lgmpHostStreamReadPeek( - m_clientToHostStream, &buffer); - if (status == LGMP_ERR_STREAM_EMPTY || - status == LGMP_ERR_STREAM_UNBOUND || - status == LGMP_ERR_STREAM_STALE) - return; - if (status != LGMP_OK) - { - Fail("lgmpHostStreamReadPeek(audio release)", status); - return; - } - const LGMP_STATUS release = lgmpHostStreamReadRelease( - m_clientToHostStream, &buffer); - if (release != LGMP_OK) - { - Fail("lgmpHostStreamReadRelease(audio release)", release); - return; - } - m_inboundActivitySerial = - NextSerial(m_inboundActivitySerial); - continue; - } - if (m_ownerState != OwnerState::BOUND || - !m_responseAdmitted || m_failed) + if (index >= LGMP_MAX_CLIENTS) + return; + Subscriber& subscriber = m_subscribers[index]; + if (subscriber.state == SubscriberState::NONE) return; - target = m_started ? m_target : nullptr; - ownerSerial = m_ownerSerial; + LGMPStreamBuffer buffer = {}; + const LGMP_STATUS peek = lgmpHostStreamReadPeek( + subscriber.clientControl, &buffer); + if (peek == LGMP_ERR_STREAM_EMPTY) + return; + if (peek == LGMP_ERR_STREAM_UNBOUND || + peek == LGMP_ERR_STREAM_STALE) + { + if (subscriber.state == SubscriberState::BOUND) + BeginRelease(index, "client audio control binding lost"); + return; + } + if (peek != LGMP_OK) + { + Fail("lgmpHostStreamReadPeek(audio control)", peek); + return; + } + + const auto release = [&]() -> bool + { + const LGMP_STATUS status = lgmpHostStreamReadRelease( + subscriber.clientControl, &buffer); + if (status != LGMP_OK) + { + Fail("lgmpHostStreamReadRelease(audio control)", status); + return false; + } + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); + return true; + }; + + if (subscriber.state == SubscriberState::RELEASING) + { + if (!release()) + return; + continue; + } + + KVMFRAudioMessage message = {}; + if (!ParseInbound(buffer, message)) + { + (void)release(); + BeginRelease(index, "malformed client audio control record"); + return; + } + + const bool sequenceValid = subscriber.controlSequenceKnown ? + message.sequence == subscriber.nextControlSequence : + message.sequence == 0; + if (!sequenceValid) + { + (void)release(); + BeginRelease(index, "client audio control sequence gap"); + return; + } + subscriber.nextControlSequence = message.sequence + 1; + subscriber.controlSequenceKnown = true; + + const uint8_t * payload = + static_cast(buffer.data) + sizeof(message); + switch (message.type) + { + case KVMFR_AUDIO_MESSAGE_PLAYBACK_SUBSCRIBE: + { + KVMFRAudioSubscription subscription = {}; + if (message.length == sizeof(subscription)) + memcpy(&subscription, payload, sizeof(subscription)); + const bool valid = !message.streamGeneration && + !message.flags && !message.position && !message.timeNs && + !message.rateQ32 && !message.frames && + message.length == sizeof(subscription) && + subscription.bindingEpoch == subscriber.bindingEpoch && + subscription.roleEpoch && + !(subscription.subscriptions & + ~AUDIO_SUBSCRIPTION_MASK) && !subscription.reserved && + (!(subscription.subscriptions & + KVMFR_AUDIO_SUBSCRIBE_CAPTURE) || + (subscriber.clientCapabilities & + KVMFR_AUDIO_CAP_CAPTURE)); + const bool initial = !subscriber.subscriptionReceived; + if (!valid || (initial && + subscription.subscriptions != + subscriber.subscriptions)) + { + (void)release(); + BeginRelease(index, "invalid audio subscription"); + return; + } + + if (!release()) + return; + RenewLease(subscriber); + if (subscription.roleEpoch != subscriber.roleEpoch) + { + if (static_cast(subscription.roleEpoch - + subscriber.roleEpoch) > 0) + { + BeginRelease(index, + "invalid future audio subscription"); + return; + } + if (initial) + { + subscriber.subscriptionReceived = true; + MarkStateDirty(index); + if (m_captureOwner == INVALID_SLOT) + AssignCaptureOwner(); + } + continue; + } + + const KVMFRAudioSubscriptionFlags previous = + subscriber.subscriptions; + subscriber.subscriptions = subscription.subscriptions; + subscriber.subscriptionReceived = true; + + if (m_feedbackAuthority == index && + !FeedbackEligible(subscriber)) + RevokeFeedbackAuthority("playback subscription removed"); + if (m_captureOwner == index && + !CaptureEligible(subscriber)) + { + m_captureOwner = INVALID_SLOT; + subscriber.captureRevoking = true; + subscriber.captureRevokeDeadline = + GetTickCount64() + CAPTURE_REVOKE_ACK_MS; + RefreshGranted(index, true); + } + if (previous != subscriber.subscriptions || + !subscriber.pumpRevision) + MarkStateDirty(index); + if (subscriber.captureRevoking) + subscriber.captureRevokeRevision = + subscriber.stateRevision; + if (m_captureOwner == INVALID_SLOT && + m_captureBinding == INVALID_SLOT) + AssignCaptureOwner(); + continue; + } + + case KVMFR_AUDIO_MESSAGE_STATE_ACK: + { + KVMFRAudioStateBarrier barrier = {}; + if (message.length == sizeof(barrier)) + memcpy(&barrier, payload, sizeof(barrier)); + const bool valid = !message.flags && !message.position && + !message.timeNs && !message.rateQ32 && !message.frames && + message.length == sizeof(barrier) && + barrier.bindingEpoch == subscriber.bindingEpoch && + barrier.stateSerial; + if (!valid) + { + (void)release(); + BeginRelease(index, "invalid audio state acknowledgement"); + return; + } + if (!release()) + return; + RenewLease(subscriber); + if (barrier.roleEpoch != subscriber.roleEpoch) + { + if (static_cast(barrier.roleEpoch - + subscriber.roleEpoch) < 0) + continue; + BeginRelease(index, + "invalid future audio state acknowledgement"); + return; + } + if (subscriber.awaitingAck && + message.streamGeneration == + subscriber.barrierGeneration && + !memcmp(&barrier, &subscriber.barrier, + sizeof(barrier))) + { + subscriber.awaitingAck = false; + subscriber.ready = true; + DEBUG_INFO("Audio client %u generation %u is ready", + subscriber.clientID, + subscriber.requestGeneration); + } + continue; + } + + case KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK: + { + KVMFRAudioClockFeedback wire = {}; + if (message.length == sizeof(wire)) + memcpy(&wire, payload, sizeof(wire)); + const uint32_t validFlags = + KVMFR_AUDIO_MESSAGE_CLOCK_VALID | + KVMFR_AUDIO_MESSAGE_CLOCK_STABLE | + KVMFR_AUDIO_MESSAGE_DISCONTINUITY; + const bool valid = !(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.targetRateQ32 && + wire.bindingEpoch == subscriber.bindingEpoch && + wire.roleEpoch; + if (!valid) + { + (void)release(); + BeginRelease(index, "invalid audio clock feedback"); + return; + } + + if (!release()) + return; + RenewLease(subscriber); + if (wire.roleEpoch != subscriber.roleEpoch) + { + if (static_cast(wire.roleEpoch - + subscriber.roleEpoch) < 0) + continue; + BeginRelease(index, + "invalid future audio clock feedback"); + return; + } + const bool current = m_feedbackAuthority == index && + subscriber.ready && + (subscriber.grantedCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + m_stateKnown && m_state.available && m_state.active && + (m_state.capabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) && + message.streamGeneration == + m_state.playback.generation; + 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 (message.flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY) + feedback.flags |= AUDIO_CLOCK_DISCONTINUITY; + target = m_started ? m_target : nullptr; + subscriberSerial = subscriber.serial; + feedbackRoleEpoch = subscriber.roleEpoch; + callClock = target != nullptr; + break; + } + + case KVMFR_AUDIO_MESSAGE_KEEPALIVE: + case KVMFR_AUDIO_MESSAGE_RELEASE: + if (!EmptyControl(message)) + { + (void)release(); + BeginRelease(index, "invalid audio client control"); + return; + } + if (!release()) + return; + if (message.type == KVMFR_AUDIO_MESSAGE_RELEASE) + { + BeginRelease(index, "client audio release"); + return; + } + RenewLease(subscriber); + continue; + + default: + (void)release(); + BeginRelease(index, "unsupported client audio control"); + return; + } + } + + if (!callClock) + continue; + const AudioResult result = target->SetClockFeedback(feedback); + { + CSRWExclusiveLock lock(m_lock); + if (index >= LGMP_MAX_CLIENTS || + m_subscribers[index].state != SubscriberState::BOUND || + m_subscribers[index].serial != subscriberSerial) + return; + if (result == AudioResult::FAILED || + result == AudioResult::REJECTED) + { + Subscriber& subscriber = m_subscribers[index]; + if (m_feedbackAuthority != index || !subscriber.ready || + subscriber.roleEpoch != feedbackRoleEpoch || + !(subscriber.grantedCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) || + !m_stateKnown || m_state.playback.generation != + feedback.playbackGeneration) + continue; + RevokeFeedbackAuthority("audio clock target failed"); + } + } + } +} + +void CLGMPAudio::DrainCapture() +{ + for (unsigned count = 0; count < INBOUND_DRAIN_MAX; ++count) + { + CSRWSharedLock callbackLock(m_callbackLock); + IAudioTarget * target = nullptr; + bool callCapture = false; + uint32_t index = INVALID_SLOT; + uint64_t subscriberSerial = 0; + { + CSRWExclusiveLock lock(m_lock); + if (m_captureBinding == INVALID_SLOT) + return; + index = m_captureBinding; + Subscriber& subscriber = m_subscribers[index]; + if (m_pendingCapture.valid) { - if (!PendingCaptureCurrent()) + if (!PendingCaptureCurrent(index)) { ReleasePendingCapture(); - m_captureDiscontinuity = true; + subscriber.captureDiscontinuity = true; continue; } if (m_pendingCapture.readySerial == m_captureReadySerial) return; m_pendingCapture.readySerial = m_captureReadySerial; - callCapture = true; + target = m_started ? m_target : nullptr; + subscriberSerial = subscriber.serial; + callCapture = target != nullptr; } else { LGMPStreamBuffer buffer = {}; - const LGMP_STATUS status = lgmpHostStreamReadPeek( - m_clientToHostStream, &buffer); - if (status == LGMP_ERR_STREAM_EMPTY) + const LGMP_STATUS peek = lgmpHostStreamReadPeek( + m_captureStream, &buffer); + if (peek == LGMP_ERR_STREAM_EMPTY) return; - if (status == LGMP_ERR_STREAM_UNBOUND || - status == LGMP_ERR_STREAM_STALE) + if (peek == LGMP_ERR_STREAM_UNBOUND || + peek == LGMP_ERR_STREAM_STALE) { - BeginRelease("client-to-host audio stream binding lost"); + if (subscriber.state == SubscriberState::BOUND) + BeginRelease(index, "capture audio binding lost"); return; } - if (status != LGMP_OK) + if (peek != LGMP_OK) { - Fail("lgmpHostStreamReadPeek(audio)", status); + Fail("lgmpHostStreamReadPeek(audio capture)", peek); return; } - KVMFRAudioMessage message = {}; - if (!ParseInbound(buffer, message)) + const auto release = [&]() -> bool { - 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) + const LGMP_STATUS status = lgmpHostStreamReadRelease( + m_captureStream, &buffer); + if (status != LGMP_OK) { - Fail("lgmpHostStreamReadRelease(audio)", release); + Fail("lgmpHostStreamReadRelease(audio capture)", status); return false; } - m_nextClientSequence = message.sequence + 1; - m_clientSequenceKnown = true; m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); return true; }; - if (!m_clientReady && - message.type != KVMFR_AUDIO_MESSAGE_KEEPALIVE && - message.type != KVMFR_AUDIO_MESSAGE_RELEASE) + if (subscriber.state == SubscriberState::RELEASING || + subscriber.captureRevoking) { - if (!consume()) + if (!release()) return; - BeginRelease("audio data received before client readiness"); + continue; + } + + KVMFRAudioMessage message = {}; + if (!ParseInbound(buffer, message)) + { + (void)release(); + BeginRelease(index, "malformed capture audio record"); return; } + const bool sequenceGap = + (subscriber.captureSequenceKnown && + message.sequence != subscriber.nextCaptureSequence) || + (!subscriber.captureSequenceKnown && message.sequence != 0); + subscriber.nextCaptureSequence = message.sequence + 1; + subscriber.captureSequenceKnown = true; + if (sequenceGap) + subscriber.captureDiscontinuity = true; - switch (message.type) + const bool clockStable = + (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) != 0; + const bool clockValid = + (message.flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0; + const bool structurallyValid = + message.type == KVMFR_AUDIO_MESSAGE_CAPTURE_DATA && + !(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; + if (!structurallyValid) { - 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; + (void)release(); + BeginRelease(index, "invalid capture audio record"); + return; } + RenewLease(subscriber); + const bool current = m_captureOwner == index && + (subscriber.grantedCapabilities & + KVMFR_AUDIO_CAP_CAPTURE) && + subscriber.announcedCapture == message.streamGeneration && + m_stateKnown && m_state.available && m_state.active && + (m_state.capabilities & KVMFR_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 (!current) + { + if (!release()) + return; + subscriber.captureDiscontinuity = true; + continue; + } + + 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)) + { + (void)release(); + BeginRelease(index, "invalid capture audio payload"); + return; + } + if (!PrepareCapture(message, buffer, index)) + { + if (!release()) + return; + subscriber.captureDiscontinuity = true; + continue; + } + target = m_started ? m_target : nullptr; + subscriberSerial = subscriber.serial; + callCapture = target != nullptr; } - m_captureCallbackInFlight = target && callCapture; + m_captureCallbackInFlight = callCapture; } AudioResult result = AudioResult::UNAVAILABLE; - if (target && callCapture) + if (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 (callCapture) - m_captureCallbackInFlight = false; - if (ownerSerial != m_ownerSerial || - m_ownerState != OwnerState::BOUND) + m_captureCallbackInFlight = false; + if (index >= LGMP_MAX_CLIENTS || + m_subscribers[index].serial != subscriberSerial || + !m_pendingCapture.valid) { if (m_pendingCapture.valid) ReleasePendingCapture(); return; } - if (callClock) - { - if (result == AudioResult::FAILED || - result == AudioResult::REJECTED) - BeginRelease("audio clock target failed"); - continue; - } - if (!callCapture || !m_pendingCapture.valid) - continue; - if (!PendingCaptureCurrent()) + Subscriber& subscriber = m_subscribers[index]; + if (!PendingCaptureCurrent(index)) { ReleasePendingCapture(); - m_captureDiscontinuity = true; + subscriber.captureDiscontinuity = true; continue; } if (result == AudioResult::BUSY) return; const LGMP_STATUS release = lgmpHostStreamReadRelease( - m_clientToHostStream, &m_pendingCapture.buffer); + m_captureStream, &m_pendingCapture.buffer); if (release != LGMP_OK) { m_pendingCapture = {}; @@ -1728,64 +2553,55 @@ void CLGMPAudioTransport::DrainInbound() return; } m_pendingCapture = {}; - m_inboundActivitySerial = - NextSerial(m_inboundActivitySerial); - m_captureDiscontinuity = result != AudioResult::ACCEPTED; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); + subscriber.captureDiscontinuity = + result != AudioResult::ACCEPTED; if (result == AudioResult::FAILED || result == AudioResult::REJECTED) { - BeginRelease("audio capture target failed"); + BeginRelease(index, "audio capture target failed"); return; } } } } -void CLGMPAudioTransport::ProcessActivation() +void CLGMPAudio::DrainInbound() +{ + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + DrainControl(index); + DrainCapture(); +} + +void CLGMPAudio::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) + if (!m_started || !m_target || m_failed || !m_stateKnown) return; - if (m_ownerState == OwnerState::BOUND && - m_responseAdmitted && m_clientReady && - !m_activationIssued) - activate = true; - else if (m_ownerState != OwnerState::RELEASING || - !m_activationIssued || m_deactivationIssued) + activate = HasBoundSubscribers(); + if (activate == m_activationCommanded) return; - target = m_target; - ownerSerial = m_ownerSerial; + target = m_target; } const AudioResult result = target->SetAudioActive(activate); CSRWExclusiveLock lock(m_lock); - if (ownerSerial != m_ownerSerial || - m_ownerState == OwnerState::NONE) + if (!m_started || m_target != target) 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; + if (result == AudioResult::ACCEPTED || + (!activate && (result == AudioResult::UNAVAILABLE || + result == AudioResult::STALE))) + m_activationCommanded = activate; + else if (result == AudioResult::FAILED || + result == AudioResult::REJECTED) + Fail("audio endpoint activation", LGMP_ERR_CORRUPTED); } -void CLGMPAudioTransport::NotifyPlaybackReady() +void CLGMPAudio::NotifyPlaybackReady() { CSRWSharedLock callbackLock(m_callbackLock); IAudioTarget * target = nullptr; @@ -1801,7 +2617,7 @@ void CLGMPAudioTransport::NotifyPlaybackReady() target->PlaybackReceiveReady(); } -void CLGMPAudioTransport::NotifyFailure() +void CLGMPAudio::NotifyFailure() { CSRWSharedLock callbackLock(m_callbackLock); IAudioTarget * target = nullptr; @@ -1817,7 +2633,7 @@ void CLGMPAudioTransport::NotifyFailure() target->AudioFailed(); } -void CLGMPAudioTransport::Fail( +void CLGMPAudio::Fail( const char * operation, LGMP_STATUS status) { if (!m_failed) @@ -1827,28 +2643,32 @@ void CLGMPAudioTransport::Fail( m_failed = true; } -void CLGMPAudioTransport::Process() +void CLGMPAudio::Process() { { CSRWExclusiveLock lock(m_lock); - if (!m_initialized) + if (!m_claimResponderReady) return; PumpResponses(); - } - - { - CSRWExclusiveLock lock(m_lock); - MaintainOwner(GetTickCount64()); + if (!m_initialized) + return; + MaintainSubscribers(GetTickCount64()); PumpResponses(); PumpState(); - CheckPlaybackCredit(); + CheckPlaybackBackpressure(GetTickCount64()); } ProcessActivation(); NotifyPlaybackReady(); NotifyFailure(); } -bool CLGMPAudioTransport::Start(IAudioTarget& target) +bool CLGMPAudio::CanRespondToClaims() +{ + CSRWSharedLock lock(m_lock); + return m_claimResponderReady; +} + +bool CLGMPAudio::Start(IAudioTarget& target) { CSRWExclusiveLock lifecycleLock(m_lifecycleLock); CSRWExclusiveLock callbackLock(m_callbackLock); @@ -1861,58 +2681,68 @@ bool CLGMPAudioTransport::Start(IAudioTarget& target) m_started = true; m_stateKnown = false; m_failureNotified = false; + m_activationCommanded = false; return true; } -void CLGMPAudioTransport::Stop() +void CLGMPAudio::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); + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + BeginRelease(index, "audio source stopped"); + ClearBlockedPlayback(false); m_started = false; - m_target = nullptr; + m_target = nullptr; m_playbackReadyPending = false; + m_activationCommanded = false; } -void CLGMPAudioTransport::TransportStop() +void CLGMPAudio::TransportStop() { CSRWExclusiveLock lifecycleLock(m_lifecycleLock); StopInboundWorker(); CSRWExclusiveLock callbackLock(m_callbackLock); CSRWExclusiveLock lock(m_lock); m_started = false; - m_target = nullptr; + m_target = nullptr; m_playbackReadyPending = false; ClearBlockedPlayback(false); ReleasePendingCapture(); - ForceUnbindStreams(); - ResetOwner(); + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + { + if (m_subscribers[index].state == SubscriberState::NONE) + continue; + ForceUnbindSubscriber(index); + ResetSubscriber(index); + } + m_feedbackAuthority = INVALID_SLOT; + m_captureOwner = INVALID_SLOT; + m_captureBinding = INVALID_SLOT; + m_activationCommanded = false; for (PendingResponse& response : m_pendingResponse) response = {}; for (ClaimRecord& record : m_claimRecord) record = {}; } -void CLGMPAudioTransport::AudioState( +void CLGMPAudio::AudioState( const AudioEndpointStatus& state) { CSRWExclusiveLock lock(m_lock); if (!m_started) return; + const uint64_t previousPlayback = m_previousPlaybackGeneration; if (state.playback.state == AudioStreamState::RUNNING) m_playbackRunGeneration = state.playback.generation; 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) && @@ -1925,20 +2755,34 @@ void CLGMPAudioTransport::AudioState( ClearBlockedPlayback(true); } - m_state = state; + m_state = state; m_stateKnown = true; - if (m_ownerState == OwnerState::BOUND && m_responseAdmitted) - m_statusDirty = true; + m_previousPlaybackGeneration = state.playback.generation; + for (uint32_t index = 0; index < LGMP_MAX_CLIENTS; ++index) + RefreshGranted(index, true); + MarkAllStateDirty(); + if (state.playback.generation && + state.playback.generation != previousPlayback) + { + for (Subscriber& subscriber : m_subscribers) + if (subscriber.state == SubscriberState::BOUND) + subscriber.feedbackRevoked = false; + PromoteFeedbackAuthority(state.playback.generation); + } + if (m_captureOwner == INVALID_SLOT) + AssignCaptureOwner(); if (!m_inboundStopping && m_inboundWakeEvent) SetEvent(m_inboundWakeEvent); } -AudioResult CLGMPAudioTransport::SendPlayback( +AudioResult CLGMPAudio::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.flags & AUDIO_PACKET_CLOCK_VALID) && + (packet.position || packet.timeNs || packet.rateQ32)) || !packet.frames || packet.frames > AUDIO_MAX_FRAMES_PER_PACKET || !ValidFormat(packet.format)) @@ -1956,10 +2800,11 @@ AudioResult CLGMPAudioTransport::SendPlayback( (size != m_blockedPlaybackSize || !SamePacket(packet, m_blockedPlayback))) { - DEBUG_WARN("Audio playback retry did not match the retained packet"); + DEBUG_WARN("Audio playback retry did not match retained packet"); ClearBlockedPlayback(false); return AudioResult::REJECTED; } + const bool retainedRetry = m_playbackBlocked; if (!m_started) { ClearBlockedPlayback(false); @@ -1970,13 +2815,23 @@ AudioResult CLGMPAudioTransport::SendPlayback( ClearBlockedPlayback(false); return AudioResult::FAILED; } - if (m_ownerState != OwnerState::BOUND || - !(m_clientCapabilities & KVMFR_AUDIO_CAP_PLAYBACK)) + + bool hasPlaybackSubscriber = false; + for (const Subscriber& subscriber : m_subscribers) + if (subscriber.responseAdmitted && + subscriber.subscriptionReceived && + PlaybackEligible(subscriber)) + { + hasPlaybackSubscriber = true; + break; + } + if (!hasPlaybackSubscriber) { ClearBlockedPlayback(false); return AudioResult::ACCEPTED; } - if (!m_responseAdmitted || !m_clientReady || !m_stateKnown) + + if (!m_stateKnown) { if (!RememberBlockedPlayback(packet, size)) return AudioResult::REJECTED; @@ -2002,6 +2857,101 @@ AudioResult CLGMPAudioTransport::SendPlayback( return AudioResult::UNAVAILABLE; } + PumpState(); + if (m_failed) + return AudioResult::FAILED; + if (!HasReadyPlayback()) + { + if (!RememberBlockedPlayback(packet, size)) + return AudioResult::REJECTED; + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); + return AudioResult::BUSY; + } + + if (m_feedbackAuthority != INVALID_SLOT) + { + Subscriber& authority = m_subscribers[m_feedbackAuthority]; + if (authority.ready && PlaybackEligible(authority) && + (authority.grantedCapabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)) + { + LGMPSPMCReaderState reader = {}; + const LGMP_STATUS query = lgmpHostSPMCReaderGetState( + m_playbackStream, authority.playbackReaderID, &reader); + if (query == LGMP_ERR_STREAM_BUSY) + { + if (!RememberBlockedPlayback(packet, size)) + return AudioResult::REJECTED; + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); + return AudioResult::BUSY; + } + bool stalled = false; + if (query == LGMP_OK) + { + if (reader.state != LGMP_SPMC_READER_READY || + reader.clientID != authority.clientID || + reader.epoch != authority.playbackReaderEpoch) + RevokeFeedbackAuthority("playback reader binding lost"); + else + stalled = reader.producerSequence - + reader.consumerSequence >= KVMFR_AUDIO_STREAM_SLOT_COUNT; + } + else + { + Fail("lgmpHostSPMCReaderGetState(audio)", query); + return AudioResult::FAILED; + } + + if (stalled && m_feedbackAuthority != INVALID_SLOT) + { + const uint64_t now = GetTickCount64(); + if (!m_authorityBlockedSince) + m_authorityBlockedSince = now; + if (now - m_authorityBlockedSince >= AUTHORITY_STALL_MS) + RevokeFeedbackAuthority("playback reader stalled"); + else + { + if (!RememberBlockedPlayback(packet, size)) + return AudioResult::REJECTED; + if (!m_inboundStopping && m_inboundWakeEvent) + SetEvent(m_inboundWakeEvent); + return AudioResult::BUSY; + } + } + else + m_authorityBlockedSince = 0; + } + } + + LGMPSPMCBuffer buffer = {}; + LGMP_STATUS status = lgmpHostSPMCWriteAcquire( + m_playbackStream, &buffer); + if (status == LGMP_ERR_STREAM_BUSY) + { + if (!RememberBlockedPlayback(packet, size)) + return AudioResult::REJECTED; + return AudioResult::BUSY; + } + if (status != LGMP_OK) + { + Fail("lgmpHostSPMCWriteAcquire(audio)", status); + return AudioResult::FAILED; + } + + const size_t required = sizeof(KVMFRAudioMessage) + size; + if (!buffer.data || buffer.capacity < required) + { + const LGMP_STATUS cancel = + lgmpHostSPMCWriteCancel(m_playbackStream, &buffer); + if (cancel != LGMP_OK) + DEBUG_WARN("Failed to cancel audio playback reservation: %s", + lgmpStatusString(cancel)); + Fail("undersized playback audio slot", LGMP_ERR_INVALID_SIZE); + return AudioResult::FAILED; + } + KVMFRAudioMessageFlags flags = 0; if (packet.flags & AUDIO_PACKET_DISCONTINUITY) flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY; @@ -2012,54 +2962,40 @@ AudioResult CLGMPAudioTransport::SendPlayback( 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; - } + KVMFRAudioMessage message = {}; + message.version = KVMFR_AUDIO_MESSAGE_VERSION; + message.size = sizeof(message); + message.type = KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA; + message.streamGeneration = packet.streamGeneration; + message.flags = flags; + message.sequence = buffer.sequence; + message.position = packet.position; + message.timeNs = packet.timeNs; + message.rateQ32 = packet.rateQ32; + message.frames = packet.frames; + message.length = static_cast(size); + WCWrite::Copy(buffer.data, &message, sizeof(message)); + if (size) + WCWrite::Copy(static_cast(buffer.data) + + sizeof(message), data, size); + WCWrite::Flush(); - PumpState(); - if (m_failed) + status = lgmpHostSPMCWriteCommit(m_playbackStream, &buffer, + static_cast(required)); + if (status != LGMP_OK) + { + Fail("lgmpHostSPMCWriteCommit(audio)", status); 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); + if (retainedRetry) + m_playbackReadyPending = false; + m_inboundActivitySerial = NextSerial(m_inboundActivitySerial); return AudioResult::ACCEPTED; } -void CLGMPAudioTransport::CaptureReceiveReady() +void CLGMPAudio::CaptureReceiveReady() { CSRWExclusiveLock lock(m_lock); if (m_started) diff --git a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h index 63036689..7b3d0c56 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h +++ b/idd/LGIdd/transport/lgmp/CLGMPAudioTransport.h @@ -21,9 +21,9 @@ #pragma once #include "CSRWLock.h" -#include "common/KVMFRAudio.h" #include "transport/IAudioSource.h" +#include #include #include @@ -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; diff --git a/idd/LGIdd/transport/lgmp/CLGMPControl.h b/idd/LGIdd/transport/lgmp/CLGMPControl.h index 80163791..eebfe0c1 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPControl.h +++ b/idd/LGIdd/transport/lgmp/CLGMPControl.h @@ -34,13 +34,13 @@ #include 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; diff --git a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp index fed79f65..31882b00 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp @@ -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); diff --git a/idd/LGIdd/transport/lgmp/CLGMPHost.h b/idd/LGIdd/transport/lgmp/CLGMPHost.h index eaafd967..22a0845a 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPHost.h +++ b/idd/LGIdd/transport/lgmp/CLGMPHost.h @@ -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); diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp index 3b62b855..89cdf7c2 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp @@ -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; diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.h b/idd/LGIdd/transport/lgmp/CLGMPTransport.h index ac75e44f..e7f5b482 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.h +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.h @@ -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; diff --git a/repos/LGProtocol b/repos/LGProtocol index 62b52724..c9659b31 160000 --- a/repos/LGProtocol +++ b/repos/LGProtocol @@ -1 +1 @@ -Subproject commit 62b52724b63b6e179b8fb7e0c7c03e9a8b4e1636 +Subproject commit c9659b314a0fa95ebaccb14b7343af483cd70d1d