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