diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index f20a268e..911f9d5c 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -41,6 +41,11 @@ target_link_libraries(lgmp-transport-tests lg_common lgmp ) +if(ENABLE_AUDIO) + target_compile_definitions(lgmp-transport-tests PRIVATE + ENABLE_AUDIO + ) +endif() add_test(NAME lgmp-transport-tests COMMAND lgmp-transport-tests ) @@ -141,6 +146,31 @@ foreach(name IN LISTS LGMP_CLIPBOARD_CASES) ) endforeach() +if(ENABLE_AUDIO) + add_executable(lgmp-audio-tests + lgmp_audio_test.c + ../src/kb_hid.c + ) + target_compile_definitions(lgmp-audio-tests PRIVATE + ENABLE_AUDIO + ) + target_include_directories(lgmp-audio-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../transports/LGMP" + ) + target_link_libraries(lgmp-audio-tests + ${EXE_FLAGS} + transport_LGMP + lg_common + lgmp + ) + add_test(NAME lgmp-audio + COMMAND lgmp-audio-tests + ) + set_tests_properties(lgmp-audio PROPERTIES + TIMEOUT 20 + ) +endif() + add_executable(wayland-motion-tests motion_test.c ) diff --git a/client/tests/lgmp_audio_test.c b/client/tests/lgmp_audio_test.c new file mode 100644 index 00000000..977ed137 --- /dev/null +++ b/client/tests/lgmp_audio_test.c @@ -0,0 +1,1229 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "audio.h" + +#include +#include +#include "common/debug.h" +#include "common/event.h" +#include "common/time.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define RAM_SIZE (8U * 1024U * 1024U) +#define WAIT_MS 3000U +#define MAX_MEMORIES 64U +#define MAX_EVENTS 256U + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + fprintf(stderr, "check failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } \ + while (0) + +enum TraceKind +{ + TRACE_PLAY_START, + TRACE_PLAY_STOP, + TRACE_PLAY_DATA, + TRACE_REC_START, + TRACE_REC_STOP, +}; + +struct TraceEvent +{ + enum TraceKind kind; + uint32_t generation; +}; + +struct Trace +{ + pthread_mutex_t mutex; + atomic_uint statusCount; + atomic_uint playStartCount; + atomic_uint playStopCount; + atomic_uint playDataCount; + atomic_uint recStartCount; + atomic_uint recStopCount; + LG_AudioStatus status; + LG_AudioFormat playFormat; + LG_AudioFormat recFormat; + LG_AudioClock playClock; + bool playClockValid; + uint32_t playGeneration; + uint32_t recGeneration; + uint8_t playData[128]; + size_t playFrames; + struct TraceEvent events[MAX_EVENTS]; + unsigned eventCount; +}; + +static void traceEvent(struct Trace * trace, enum TraceKind kind, + uint32_t generation) +{ + pthread_mutex_lock(&trace->mutex); + if (trace->eventCount < MAX_EVENTS) + trace->events[trace->eventCount++] = + (struct TraceEvent){ kind, generation }; + pthread_mutex_unlock(&trace->mutex); +} + +static void statusCallback(void * opaque, const LG_AudioStatus * status) +{ + struct Trace * trace = opaque; + pthread_mutex_lock(&trace->mutex); + trace->status = *status; + pthread_mutex_unlock(&trace->mutex); + atomic_fetch_add_explicit(&trace->statusCount, 1, memory_order_release); +} + +static void playbackStart(void * opaque, uint32_t generation, + const LG_AudioFormat * format, const LG_AudioClock * clock) +{ + struct Trace * trace = opaque; + pthread_mutex_lock(&trace->mutex); + trace->playGeneration = generation; + trace->playFormat = *format; + trace->playClockValid = clock != NULL; + if (clock) + trace->playClock = *clock; + pthread_mutex_unlock(&trace->mutex); + traceEvent(trace, TRACE_PLAY_START, generation); + atomic_fetch_add_explicit(&trace->playStartCount, 1, + memory_order_release); +} + +static void playbackStop(void * opaque, uint32_t generation) +{ + struct Trace * trace = opaque; + traceEvent(trace, TRACE_PLAY_STOP, generation); + atomic_fetch_add_explicit(&trace->playStopCount, 1, + memory_order_release); +} + +static void playbackData(void * opaque, uint32_t generation, + const void * data, size_t frames, const LG_AudioClock * clock) +{ + struct Trace * trace = opaque; + pthread_mutex_lock(&trace->mutex); + trace->playGeneration = generation; + trace->playFrames = frames; + const size_t bytes = frames * trace->playFormat.channelCount * 3U; + const size_t copy = bytes < sizeof(trace->playData) ? + bytes : sizeof(trace->playData); + memset(trace->playData, 0, sizeof(trace->playData)); + memcpy(trace->playData, data, copy); + trace->playClockValid = clock != NULL; + if (clock) + trace->playClock = *clock; + pthread_mutex_unlock(&trace->mutex); + traceEvent(trace, TRACE_PLAY_DATA, generation); + atomic_fetch_add_explicit(&trace->playDataCount, 1, + memory_order_release); +} + +static void recordStart(void * opaque, uint32_t generation, + const LG_AudioFormat * format) +{ + struct Trace * trace = opaque; + pthread_mutex_lock(&trace->mutex); + trace->recGeneration = generation; + trace->recFormat = *format; + pthread_mutex_unlock(&trace->mutex); + traceEvent(trace, TRACE_REC_START, generation); + atomic_fetch_add_explicit(&trace->recStartCount, 1, + memory_order_release); +} + +static void recordStop(void * opaque, uint32_t generation) +{ + struct Trace * trace = opaque; + traceEvent(trace, TRACE_REC_STOP, generation); + atomic_fetch_add_explicit(&trace->recStopCount, 1, + memory_order_release); +} + +static const LG_AudioEventOps eventOps = +{ + .playbackStart = playbackStart, + .playbackStop = playbackStop, + .playbackData = playbackData, + .recordStart = recordStart, + .recordStop = recordStop, +}; + +struct Fixture +{ + void * ram; + PLGMPHost host; + PLGMPHostQueue pointerHost; + PLGMPClient client; + PLGMPClientQueue pointerClient; + uint32_t clientID; + LGEvent * pointerWake; + LGMPAudio * audio; + const LG_AudioOps * ops; + PLGMPHostStream h2c; + PLGMPHostStream c2h; + PLGMPMemory memories[MAX_MEMORIES]; + unsigned memoryCount; + KVMFRAudioClaim claim; + uint64_t hostSequence; + uint64_t endpointGeneration; + struct Trace trace; +}; + +struct SelfUnregister +{ + LGMPAudio * audio; + const LG_AudioOps * ops; + atomic_uint count; +}; + +struct RecordStress +{ + LGMPAudio * audio; + const LG_AudioOps * ops; + uint32_t generation; + atomic_bool stop; + atomic_uint successes; +}; + +static void * recordStress(void * opaque) +{ + struct RecordStress * stress = opaque; + const uint8_t pcm[18] = { 0 }; + while (!atomic_load_explicit(&stress->stop, memory_order_acquire)) + if (stress->ops->recordData(stress->audio, + stress->generation, pcm, 6, NULL)) + atomic_fetch_add_explicit( + &stress->successes, 1, memory_order_release); + return NULL; +} + +static void * removeStatusListener(void * opaque) +{ + struct RecordStress * stress = opaque; + stress->ops->setStatusListener(stress->audio, NULL, NULL); + return NULL; +} + +static void selfUnregisterStatus(void * opaque, + const LG_AudioStatus * status) +{ + struct SelfUnregister * state = opaque; + (void)status; + atomic_fetch_add_explicit(&state->count, 1, memory_order_release); + state->ops->setStatusListener(state->audio, NULL, NULL); +} + +static bool waitCount(struct Fixture * fixture, atomic_uint * value, + unsigned expected, unsigned timeoutMs) +{ + const uint64_t deadline = microtime() + (uint64_t)timeoutMs * 1000U; + while (microtime() < deadline) + { + lgmpHostProcess(fixture->host); + if (atomic_load_explicit(value, memory_order_acquire) >= expected) + return true; + struct timespec delay = { .tv_nsec = 1000000L }; + nanosleep(&delay, NULL); + } + return atomic_load_explicit(value, memory_order_acquire) >= expected; +} + +static bool fixtureInit(struct Fixture * fixture) +{ + memset(fixture, 0, sizeof(*fixture)); + CHECK(!posix_memalign(&fixture->ram, 64, RAM_SIZE)); + memset(fixture->ram, 0, RAM_SIZE); + CHECK(!pthread_mutex_init(&fixture->trace.mutex, NULL)); + + KVMFR session = { 0 }; + memcpy(session.magic, KVMFR_MAGIC, sizeof(session.magic)); + session.version = KVMFR_VERSION; + session.features = KVMFR_FEATURE_AUDIO; + CHECK(lgmpHostInit(fixture->ram, RAM_SIZE, &fixture->host, + sizeof(session), (uint8_t *)&session) == LGMP_OK); + const struct LGMPQueueConfig queueConfig = + { + .queueID = LGMP_Q_POINTER, + .numMessages = LGMP_Q_POINTER_LEN, + .subTimeout = 1000, + }; + CHECK(lgmpHostQueueNew(fixture->host, queueConfig, + &fixture->pointerHost) == LGMP_OK); + CHECK(lgmpClientInit(fixture->ram, RAM_SIZE, &fixture->client) == LGMP_OK); + struct timespec heartbeatDelay = { .tv_nsec = 300000000L }; + nanosleep(&heartbeatDelay, NULL); + CHECK(lgmpHostProcess(fixture->host) == LGMP_OK); + uint32_t udataSize = 0; + uint8_t * udata = NULL; + uint32_t remoteVersion = 0; + CHECK(lgmpClientSessionInit(fixture->client, &udataSize, &udata, + &fixture->clientID, &remoteVersion) == LGMP_OK); + CHECK(fixture->clientID != 0); + CHECK(lgmpClientSubscribe(fixture->client, LGMP_Q_POINTER, + &fixture->pointerClient) == LGMP_OK); + CHECK(lgmpHostProcess(fixture->host) == LGMP_OK); + fixture->pointerWake = lgCreateEvent(true, 0); + CHECK(fixture->pointerWake); + CHECK(lgmpAudio_create(fixture->client, fixture->pointerWake, + &fixture->audio)); + CHECK(lgmpAudio_connect(fixture->audio, fixture->clientID)); + fixture->ops = lgmpAudio_getOps(); + CHECK(fixture->ops); + fixture->endpointGeneration = UINT64_C(0x100000001); + return true; +} + +static void fixtureCleanup(struct Fixture * fixture) +{ + if (fixture->audio) + lgmpAudio_destroy(&fixture->audio); + if (fixture->pointerWake) + lgFreeEvent(fixture->pointerWake); + if (fixture->pointerClient) + lgmpClientUnsubscribe(&fixture->pointerClient); + if (fixture->client) + lgmpClientFree(&fixture->client); + if (fixture->h2c) + lgmpHostStreamFree(&fixture->h2c); + if (fixture->c2h) + lgmpHostStreamFree(&fixture->c2h); + for (unsigned i = 0; i < fixture->memoryCount; ++i) + lgmpHostMemFree(&fixture->memories[i]); + if (fixture->host) + lgmpHostFree(&fixture->host); + pthread_mutex_destroy(&fixture->trace.mutex); + free(fixture->ram); +} + +static bool allocateMemory(struct Fixture * fixture, size_t size, + PLGMPMemory * result) +{ + CHECK(fixture->memoryCount < MAX_MEMORIES); + CHECK(size <= UINT32_MAX); + CHECK(lgmpHostMemAlloc(fixture->host, (uint32_t)size, result) == LGMP_OK); + fixture->memories[fixture->memoryCount++] = *result; + return true; +} + +static bool readClaim(struct Fixture * fixture, KVMFRAudioClaim * claim, + unsigned timeoutMs) +{ + const uint64_t deadline = microtime() + (uint64_t)timeoutMs * 1000U; + while (microtime() < deadline) + { + size_t size = sizeof(*claim); + uint32_t clientID = 0; + const LGMP_STATUS status = lgmpHostReadDataWithSource( + fixture->pointerHost, claim, &size, &clientID); + if (status == LGMP_OK) + { + CHECK(size == sizeof(*claim)); + CHECK(clientID == fixture->clientID); + CHECK(lgmpHostAckData(fixture->pointerHost) == LGMP_OK); + return true; + } + CHECK(status == LGMP_ERR_QUEUE_EMPTY); + lgmpHostProcess(fixture->host); + struct timespec delay = { .tv_nsec = 1000000L }; + nanosleep(&delay, NULL); + } + return false; +} + +static bool createStreams(struct Fixture * fixture) +{ + const struct LGMPStreamConfig h2cConfig = + { + .direction = LGMP_STREAM_HOST_TO_CLIENT, + .policy = LGMP_STREAM_RELIABLE_FIFO, + .slotCount = KVMFR_AUDIO_STREAM_SLOT_COUNT, + .slotSize = KVMFR_AUDIO_STREAM_SLOT_BYTES, + }; + const struct LGMPStreamConfig c2hConfig = + { + .direction = LGMP_STREAM_CLIENT_TO_HOST, + .policy = LGMP_STREAM_RELIABLE_FIFO, + .slotCount = KVMFR_AUDIO_STREAM_SLOT_COUNT, + .slotSize = KVMFR_AUDIO_STREAM_SLOT_BYTES, + }; + CHECK(lgmpHostStreamNew(fixture->host, h2cConfig, + &fixture->h2c) == LGMP_OK); + CHECK(lgmpHostStreamNew(fixture->host, c2hConfig, + &fixture->c2h) == LGMP_OK); + return true; +} + +static void copyDescriptor(KVMFRStreamDescriptor * output, + PLGMPHostStream stream) +{ + struct LGMPStreamDescriptor descriptor; + lgmpHostStreamGetDescriptor(stream, &descriptor); + memcpy(output, &descriptor, sizeof(*output)); +} + +static bool bindStreams(struct Fixture * fixture) +{ + uint32_t epoch = 0; + CHECK(lgmpHostStreamBind(fixture->h2c, + fixture->clientID, &epoch) == LGMP_OK); + CHECK(epoch != 0); + CHECK(lgmpHostStreamBind(fixture->c2h, + fixture->clientID, &epoch) == LGMP_OK); + CHECK(epoch != 0); + return true; +} + +static bool postTagged(struct Fixture * fixture, uint64_t udata, + const void * data, size_t size, LGMPMessage * clientMessage) +{ + PLGMPMemory memory; + CHECK(allocateMemory(fixture, size, &memory)); + memcpy(lgmpHostMemPtr(memory), data, size); + unsigned recipients = 0; + CHECK(lgmpHostQueuePostForClients(fixture->pointerHost, udata, memory, + &fixture->clientID, 1, &recipients) == LGMP_OK); + CHECK(recipients == 1); + const uint64_t deadline = microtime() + WAIT_MS * 1000U; + LGMP_STATUS status; + do + { + status = lgmpClientProcess(fixture->pointerClient, clientMessage); + if (status == LGMP_OK) + return true; + CHECK(status == LGMP_ERR_QUEUE_EMPTY); + lgmpHostProcess(fixture->host); + } + while (microtime() < deadline); + return false; +} + +static bool deliverBoundResponse(struct Fixture * fixture, + const KVMFRAudioClaim * claim, bool correct) +{ + KVMFRAudioStreamResponse response = + { + .magic = KVMFR_AUDIO_RESPONSE_MAGIC, + .version = KVMFR_AUDIO_CONTROL_VERSION, + .size = sizeof(response), + .requestGeneration = claim->requestGeneration, + .boundClientID = fixture->clientID, + .result = KVMFR_AUDIO_RESPONSE_BOUND, + .protocolVersion = KVMFR_AUDIO_PROTOCOL_MAX, + .endpointGeneration = fixture->endpointGeneration, + .claimToken = correct ? claim->claimToken : claim->claimToken + 1, + }; + copyDescriptor(&response.hostToClient, fixture->h2c); + copyDescriptor(&response.clientToHost, fixture->c2h); + + LGMPMessage message; + CHECK(postTagged(fixture, KVMFR_AUDIO_CONTROL_UDATA, + &response, sizeof(response), &message)); + CHECK((uint32_t)message.udata == 0); + CHECK((uint32_t)(message.udata >> 32) == KVMFR_AUDIO_CONTROL_TAG); + CHECK(message.size == sizeof(response)); + KVMFRAudioStreamResponse copy; + memcpy(©, message.mem, sizeof(copy)); + CHECK(lgmpClientMessageDone(fixture->pointerClient) == LGMP_OK); + lgmpAudio_handleResponse(fixture->audio, ©); + return true; +} + +static KVMFRAudioFormat makeFormat(uint32_t channelMask, + KVMFRAudioSampleFormat sampleFormat) +{ + 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; + return (KVMFRAudioFormat) + { + .version = KVMFR_AUDIO_FORMAT_VERSION, + .size = sizeof(KVMFRAudioFormat), + .sampleFormat = sampleFormat, + .sampleRate = 192000, + .channelMask = channelMask, + .channelCount = (uint16_t)channels, + .validBits = (uint16_t)(bytes * 8U), + .containerBits = (uint16_t)(bytes * 8U), + .blockAlign = (uint16_t)(channels * bytes), + }; +} + +static bool writeH2C(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) +{ + LGMPStreamBuffer buffer = { 0 }; + CHECK(lgmpHostStreamWriteAcquire(fixture->h2c, &buffer) == LGMP_OK); + CHECK(buffer.capacity >= sizeof(KVMFRAudioMessage) + length); + const KVMFRAudioMessage message = + { + .version = KVMFR_AUDIO_MESSAGE_VERSION, + .size = sizeof(KVMFRAudioMessage), + .type = type, + .streamGeneration = generation, + .flags = flags, + .sequence = 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(lgmpHostStreamWriteCommit(fixture->h2c, &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) +{ + return writeH2C(fixture, type, generation, flags, position, timeNs, + rateQ32, frames, payload, length, fixture->hostSequence++); +} + +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, + 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); + if (status == LGMP_OK) + { + CHECK(buffer.size >= sizeof(*message)); + memcpy(message, buffer.data, sizeof(*message)); + 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); + return true; + } + CHECK(status == LGMP_ERR_STREAM_EMPTY); + lgmpHostProcess(fixture->host); + struct timespec delay = { .tv_nsec = 1000000L }; + nanosleep(&delay, NULL); + } + return false; +} + +static bool readC2HType(struct Fixture * fixture, uint32_t type, + KVMFRAudioMessage * message, uint8_t payload[KVMFR_AUDIO_PCM_BYTES], + unsigned timeoutMs) +{ + const uint64_t deadline = microtime() + (uint64_t)timeoutMs * 1000U; + while (microtime() < deadline) + { + const unsigned left = (unsigned)((deadline - microtime()) / 1000U + 1); + CHECK(readC2H(fixture, message, payload, left)); + if (message->type == type) + return true; + } + return false; +} + +static bool unbindBoth(struct Fixture * fixture) +{ + const uint64_t deadline = microtime() + WAIT_MS * 1000U; + bool h2cDone = false; + bool c2hDone = false; + while (microtime() < deadline && (!h2cDone || !c2hDone)) + { + if (!h2cDone) + { + const LGMP_STATUS status = lgmpHostStreamUnbind(fixture->h2c); + CHECK(status == LGMP_OK || status == LGMP_ERR_STREAM_BUSY); + h2cDone = status == LGMP_OK; + } + if (!c2hDone) + { + 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); + CHECK(status == LGMP_OK || status == LGMP_ERR_STREAM_BUSY); + c2hDone = status == LGMP_OK; + } + struct timespec delay = { .tv_nsec = 1000000L }; + nanosleep(&delay, NULL); + } + return h2cDone && c2hDone; +} + +static bool beginClaim(struct Fixture * fixture, bool retry) +{ + fixture->ops->setStatusListener(fixture->audio, + statusCallback, &fixture->trace); + CHECK(waitCount(fixture, &fixture->trace.statusCount, 1, WAIT_MS)); + pthread_mutex_lock(&fixture->trace.mutex); + const bool initiallyUnavailable = !fixture->trace.status.available; + pthread_mutex_unlock(&fixture->trace.mutex); + CHECK(initiallyUnavailable); + + lgmpAudio_pumpClaim(fixture->audio, fixture->pointerClient); + CHECK(readClaim(fixture, &fixture->claim, WAIT_MS)); + CHECK(fixture->claim.messageType == KVMFR_MESSAGE_AUDIO_CLAIM); + CHECK(fixture->claim.flags == KVMFR_AUDIO_CLAIM_BIND); + CHECK(fixture->claim.clientCapabilities == + (KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)); + + if (retry) + { + struct timespec delay = { .tv_nsec = 210000000L }; + nanosleep(&delay, NULL); + lgmpAudio_pumpClaim(fixture->audio, fixture->pointerClient); + KVMFRAudioClaim second; + CHECK(readClaim(fixture, &second, WAIT_MS)); + CHECK(!memcmp(&fixture->claim, &second, sizeof(second))); + } + return true; +} + +static bool testLateBoundRelease(void) +{ + struct Fixture fixture; + CHECK(fixtureInit(&fixture)); + + lgmpAudio_pumpClaim(fixture.audio, fixture.pointerClient); + KVMFRAudioClaim ignored; + CHECK(!readClaim(&fixture, &ignored, 30)); + + CHECK(beginClaim(&fixture, true)); + CHECK(createStreams(&fixture)); + CHECK(bindStreams(&fixture)); + + fixture.ops->setStatusListener(fixture.audio, NULL, NULL); + CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); + KVMFRAudioMessage message; + uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(message.type == KVMFR_AUDIO_MESSAGE_RELEASE); + CHECK(message.sequence == 0); + CHECK(unbindBoth(&fixture)); + const struct timespec settle = { .tv_nsec = 30000000L }; + nanosleep(&settle, NULL); + + fixtureCleanup(&fixture); + return true; +} + +static bool testRetiredClaim(void) +{ + struct Fixture fixture; + CHECK(fixtureInit(&fixture)); + + struct SelfUnregister self = + { + .audio = fixture.audio, + .ops = fixture.ops, + }; + fixture.ops->setStatusListener(fixture.audio, + selfUnregisterStatus, &self); + CHECK(atomic_load_explicit(&self.count, memory_order_acquire) == 1); + + CHECK(beginClaim(&fixture, false)); + const KVMFRAudioClaim retired = fixture.claim; + for (unsigned retry = 0; retry < 2; ++retry) + { + const struct timespec delay = { .tv_nsec = 210000000L }; + nanosleep(&delay, NULL); + lgmpAudio_pumpClaim(fixture.audio, fixture.pointerClient); + KVMFRAudioClaim duplicate; + CHECK(readClaim(&fixture, &duplicate, WAIT_MS)); + CHECK(!memcmp(&retired, &duplicate, sizeof(retired))); + } + + const struct timespec cooldownDelay = { .tv_nsec = 210000000L }; + nanosleep(&cooldownDelay, NULL); + lgmpAudio_pumpClaim(fixture.audio, fixture.pointerClient); + KVMFRAudioClaim unexpected; + CHECK(!readClaim(&fixture, &unexpected, 30)); + + fixture.ops->setStatusListener(fixture.audio, NULL, NULL); + const struct timespec retirementDelay = + { .tv_sec = 1, .tv_nsec = 250000000L }; + nanosleep(&retirementDelay, NULL); + fixture.ops->setStatusListener(fixture.audio, + statusCallback, &fixture.trace); + const struct timespec reclaimDelay = { .tv_nsec = 220000000L }; + nanosleep(&reclaimDelay, NULL); + lgmpAudio_pumpClaim(fixture.audio, fixture.pointerClient); + CHECK(readClaim(&fixture, &fixture.claim, WAIT_MS)); + CHECK(fixture.claim.requestGeneration != retired.requestGeneration); + CHECK(fixture.claim.claimToken != retired.claimToken); + + CHECK(createStreams(&fixture)); + CHECK(bindStreams(&fixture)); + CHECK(deliverBoundResponse(&fixture, &retired, true)); + KVMFRAudioMessage message; + uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(!readC2H(&fixture, &message, payload, 30)); + CHECK(unbindBoth(&fixture)); + + CHECK(bindStreams(&fixture)); + CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); + CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(message.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); + CHECK(message.sequence == 0); + + fixture.ops->setStatusListener(fixture.audio, NULL, NULL); + CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + &message, payload, WAIT_MS)); + CHECK(unbindBoth(&fixture)); + const struct timespec settle = { .tv_nsec = 30000000L }; + nanosleep(&settle, NULL); + + fixtureCleanup(&fixture); + return true; +} + +static bool testDataPlane(void) +{ + static const LG_AudioChannel expectedChannels[KVMFR_AUDIO_MAX_CHANNELS] = + { + LG_AUDIO_CH_FRONT_LEFT, + LG_AUDIO_CH_FRONT_RIGHT, + LG_AUDIO_CH_FRONT_CENTER, + LG_AUDIO_CH_LFE, + LG_AUDIO_CH_REAR_LEFT, + LG_AUDIO_CH_REAR_RIGHT, + LG_AUDIO_CH_FRONT_LEFT_CENTER, + LG_AUDIO_CH_FRONT_RIGHT_CENTER, + LG_AUDIO_CH_REAR_CENTER, + LG_AUDIO_CH_SIDE_LEFT, + LG_AUDIO_CH_SIDE_RIGHT, + LG_AUDIO_CH_TOP_CENTER, + LG_AUDIO_CH_TOP_FRONT_LEFT, + LG_AUDIO_CH_TOP_FRONT_CENTER, + LG_AUDIO_CH_TOP_FRONT_RIGHT, + LG_AUDIO_CH_TOP_REAR_LEFT, + LG_AUDIO_CH_TOP_REAR_CENTER, + LG_AUDIO_CH_TOP_REAR_RIGHT, + }; + + struct Fixture fixture; + CHECK(fixtureInit(&fixture)); + CHECK(beginClaim(&fixture, true)); + CHECK(createStreams(&fixture)); + CHECK(bindStreams(&fixture)); + + CHECK(deliverBoundResponse(&fixture, &fixture.claim, false)); + struct timespec shortDelay = { .tv_nsec = 30000000L }; + nanosleep(&shortDelay, NULL); + + CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); + KVMFRAudioMessage c2hMessage; + uint8_t c2hPayload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(c2hMessage.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); + CHECK(c2hMessage.sequence == 0); + + KVMFRAudioFormat playFormat = makeFormat( + KVMFR_AUDIO_CHANNEL_FRONT_LEFT, KVMFR_AUDIO_SAMPLE_S24_LE); + KVMFRAudioFormat captureFormat = makeFormat( + 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, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, + playWire, &playFormat, + captureWire, &captureFormat)); + CHECK(waitCount(&fixture, &fixture.trace.statusCount, 2, WAIT_MS)); + pthread_mutex_lock(&fixture.trace.mutex); + const bool available = fixture.trace.status.available; + const uint32_t endpointLocal = fixture.trace.status.generation; + pthread_mutex_unlock(&fixture.trace.mutex); + CHECK(available && endpointLocal != 0); + 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)); + + uint32_t previousPlayGeneration = 0; + for (unsigned bit = 0; bit < KVMFR_AUDIO_MAX_CHANNELS; ++bit) + { + const uint32_t startCount = + atomic_load_explicit(&fixture.trace.playStartCount, + memory_order_acquire); + 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(waitCount(&fixture, &fixture.trace.playStartCount, + startCount + 1, WAIT_MS)); + pthread_mutex_lock(&fixture.trace.mutex); + CHECK(fixture.trace.playFormat.sampleFormat == LG_AUDIO_FMT_S24_LE); + CHECK(fixture.trace.playFormat.channelCount == 1); + CHECK(fixture.trace.playFormat.channels[0] == expectedChannels[bit]); + CHECK(fixture.trace.playGeneration != 0); + CHECK(!previousPlayGeneration || + fixture.trace.playGeneration != previousPlayGeneration); + previousPlayGeneration = fixture.trace.playGeneration; + pthread_mutex_unlock(&fixture.trace.mutex); + } + + const uint32_t dataBefore = + atomic_load_explicit(&fixture.trace.playDataCount, memory_order_acquire); + 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, + UINT64_C(0x300000000) + KVMFR_AUDIO_MAX_CHANNELS - 1, + KVMFR_AUDIO_MESSAGE_CLOCK_VALID | + KVMFR_AUDIO_MESSAGE_CLOCK_STABLE, + 555, 987654321, UINT64_C(192000) << 32, 4, + pcm, sizeof(pcm))); + CHECK(waitCount(&fixture, &fixture.trace.playDataCount, + dataBefore + 1, WAIT_MS)); + pthread_mutex_lock(&fixture.trace.mutex); + CHECK(fixture.trace.playFrames == 4); + CHECK(!memcmp(fixture.trace.playData, pcm, sizeof(pcm))); + CHECK(fixture.trace.playClockValid); + CHECK(fixture.trace.playClock.discontinuity); + CHECK(fixture.trace.playClock.position == 555); + pthread_mutex_unlock(&fixture.trace.mutex); + + pthread_mutex_lock(&fixture.trace.mutex); + const uint32_t captureLocal = fixture.trace.recGeneration; + const uint32_t playbackLocal = fixture.trace.playGeneration; + pthread_mutex_unlock(&fixture.trace.mutex); + CHECK(captureLocal && playbackLocal); + const LG_AudioClock captureClock = + { + .position = 44, + .time = 777, + .rate = 192000.0, + .stable = true, + }; + uint8_t capturePcm[18]; + 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(c2hMessage.streamGeneration == captureWire); + CHECK(c2hMessage.frames == 6); + CHECK(c2hMessage.length == sizeof(capturePcm)); + CHECK(!memcmp(c2hPayload, capturePcm, sizeof(capturePcm))); + CHECK((c2hMessage.flags & (KVMFR_AUDIO_MESSAGE_CLOCK_VALID | + KVMFR_AUDIO_MESSAGE_CLOCK_STABLE)) == + (KVMFR_AUDIO_MESSAGE_CLOCK_VALID | + KVMFR_AUDIO_MESSAGE_CLOCK_STABLE)); + + const LG_AudioClock playbackClock = + { + .position = 888, + .time = 999, + .rate = 191999.75, + .stable = true, + .discontinuity = true, + }; + CHECK(!fixture.ops->clockFeedback(fixture.audio, playbackLocal, + &playbackClock, 192000.0)); + + const uint64_t currentPlayWire = + UINT64_C(0x300000000) + KVMFR_AUDIO_MAX_CHANNELS - 1; + KVMFRAudioFormat currentPlayFormat = makeFormat( + KVMFR_AUDIO_CHANNEL_TOP_BACK_RIGHT, + KVMFR_AUDIO_SAMPLE_S24_LE); + ++fixture.endpointGeneration; + const unsigned clockStatusBefore = atomic_load_explicit( + &fixture.trace.statusCount, memory_order_acquire); + CHECK(sendStatus(&fixture, true, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK, + currentPlayWire, ¤tPlayFormat, + captureWire, &captureFormat)); + CHECK(waitCount(&fixture, &fixture.trace.statusCount, + clockStatusBefore + 1, WAIT_MS)); + CHECK(fixture.ops->clockFeedback(fixture.audio, playbackLocal, + &playbackClock, 192000.0)); + CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK, + &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(c2hMessage.streamGeneration == + UINT64_C(0x300000000) + KVMFR_AUDIO_MAX_CHANNELS - 1); + CHECK(c2hMessage.length == sizeof(KVMFRAudioClockFeedback)); + CHECK(c2hMessage.flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY); + KVMFRAudioClockFeedback feedback; + memcpy(&feedback, c2hPayload, sizeof(feedback)); + CHECK(feedback.targetRateQ32 == (UINT64_C(192000) << 32)); + + while (readC2H(&fixture, &c2hMessage, c2hPayload, 2)) + { + } + unsigned successful = 0; + for (;;) + { + const uint64_t before = microtime(); + const bool ok = fixture.ops->recordData(fixture.audio, captureLocal, + capturePcm, 6, NULL); + const uint64_t elapsed = microtime() - before; + CHECK(elapsed < 20000U); + if (!ok) + break; + ++successful; + CHECK(successful <= KVMFR_AUDIO_STREAM_SLOT_COUNT); + } + CHECK(successful > 0 && successful <= KVMFR_AUDIO_STREAM_SLOT_COUNT); + uint64_t maxFullCall = 0; + for (unsigned i = 0; i < 100; ++i) + { + const uint64_t before = microtime(); + CHECK(!fixture.ops->recordData(fixture.audio, captureLocal, + capturePcm, 6, NULL)); + const uint64_t elapsed = microtime() - before; + if (elapsed > maxFullCall) + maxFullCall = elapsed; + } + CHECK(maxFullCall < 20000U); + + CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + uint8_t resumedPcm[18]; + memset(resumedPcm, 0xef, sizeof(resumedPcm)); + CHECK(fixture.ops->recordData(fixture.audio, captureLocal, + resumedPcm, 6, NULL)); + bool foundResumed = false; + uint64_t previousSequence = c2hMessage.sequence; + while (!foundResumed) + { + CHECK(readC2H(&fixture, &c2hMessage, c2hPayload, WAIT_MS)); + if (c2hMessage.type == KVMFR_AUDIO_MESSAGE_CAPTURE_DATA && + c2hMessage.length == sizeof(resumedPcm) && + !memcmp(c2hPayload, resumedPcm, sizeof(resumedPcm))) + { + CHECK(c2hMessage.sequence > previousSequence + 1); + CHECK(c2hMessage.flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY); + foundResumed = true; + } + 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(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(c2hMessage.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); + + const KVMFRAudioClaim originalClaim = fixture.claim; + fixture.endpointGeneration++; + playFormat = makeFormat(KVMFR_AUDIO_CHANNEL_STEREO, + 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, + KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE, + UINT64_C(0x500000001), + &playFormat, 0, NULL)); + CHECK(waitCount(&fixture, &fixture.trace.statusCount, + availableBefore + 1, WAIT_MS)); + pthread_mutex_lock(&fixture.trace.mutex); + CHECK(fixture.trace.status.available); + CHECK(fixture.trace.status.generation != endpointLocal); + pthread_mutex_unlock(&fixture.trace.mutex); + lgmpAudio_pumpClaim(fixture.audio, fixture.pointerClient); + KVMFRAudioClaim unexpectedClaim; + CHECK(!readClaim(&fixture, &unexpectedClaim, 30)); + CHECK(!memcmp(&fixture.claim, &originalClaim, sizeof(originalClaim))); + + const unsigned playBeforeDetach = atomic_load_explicit( + &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); + const KVMFRAudioMessage message = + { + .version = KVMFR_AUDIO_MESSAGE_VERSION, + .size = sizeof(KVMFRAudioMessage), + .type = KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA, + .streamGeneration = UINT64_C(0x500000001), + .sequence = fixture.hostSequence++, + .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, + sizeof(message) + message.length) == LGMP_OK); + } + fixture.ops->detach(fixture.audio); + const unsigned afterDetach = atomic_load_explicit( + &fixture.trace.playDataCount, memory_order_acquire); + CHECK(afterDetach >= playBeforeDetach); + fixture.ops->setStatusListener(fixture.audio, NULL, NULL); + CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + &c2hMessage, c2hPayload, WAIT_MS)); + CHECK(sendH2C(&fixture, KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP, + 1, 0, 0, 0, 0, 0, NULL, 0)); + CHECK(unbindBoth(&fixture)); + struct timespec after = { .tv_nsec = 30000000L }; + nanosleep(&after, NULL); + CHECK(atomic_load_explicit(&fixture.trace.playDataCount, + memory_order_acquire) == afterDetach); + + fixtureCleanup(&fixture); + printf("data-plane: full-ring-successes=%u max-full-call-us=%llu\n", + successful, (unsigned long long)maxFullCall); + return true; +} + +static bool testReleaseIsFinal(void) +{ + struct Fixture fixture; + CHECK(fixtureInit(&fixture)); + CHECK(beginClaim(&fixture, false)); + CHECK(createStreams(&fixture)); + CHECK(bindStreams(&fixture)); + CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); + + KVMFRAudioMessage message; + uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(readC2H(&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, + 0, NULL, captureWire, &captureFormat)); + CHECK(waitCount(&fixture, &fixture.trace.statusCount, 2, WAIT_MS)); + CHECK(fixture.ops->attach(fixture.audio, &eventOps, &fixture.trace)); + CHECK(waitCount(&fixture, &fixture.trace.recStartCount, 1, WAIT_MS)); + pthread_mutex_lock(&fixture.trace.mutex); + const uint32_t captureLocal = fixture.trace.recGeneration; + pthread_mutex_unlock(&fixture.trace.mutex); + CHECK(captureLocal != 0); + + struct RecordStress stress = + { + .audio = fixture.audio, + .ops = fixture.ops, + .generation = captureLocal, + }; + pthread_t stressThread; + CHECK(!pthread_create(&stressThread, NULL, recordStress, &stress)); + + CHECK(sendStatus(&fixture, true, KVMFR_AUDIO_CAP_CAPTURE, + 0, NULL, captureWire, &captureFormat)); + 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, + captureWire, 0, 0, 0, 0, 0, NULL, 0)); + CHECK(unbindBoth(&fixture)); + lgmpAudio_disconnect(fixture.audio); + + fixtureCleanup(&fixture); + return true; +} + +static bool testFaultReleaseReclaim(void) +{ + struct Fixture fixture; + CHECK(fixtureInit(&fixture)); + CHECK(beginClaim(&fixture, false)); + CHECK(createStreams(&fixture)); + CHECK(bindStreams(&fixture)); + CHECK(deliverBoundResponse(&fixture, &fixture.claim, true)); + + KVMFRAudioMessage message; + uint8_t payload[KVMFR_AUDIO_PCM_BYTES]; + CHECK(readC2H(&fixture, &message, payload, WAIT_MS)); + CHECK(message.type == KVMFR_AUDIO_MESSAGE_KEEPALIVE); + CHECK(message.sequence == 0); + + CHECK(sendH2C(&fixture, UINT32_C(0xffffffff), + 0, 0, 0, 0, 0, 0, NULL, 0)); + CHECK(readC2HType(&fixture, KVMFR_AUDIO_MESSAGE_RELEASE, + &message, payload, WAIT_MS)); + CHECK(unbindBoth(&fixture)); + + const KVMFRAudioClaim first = fixture.claim; + struct timespec retryDelay = { .tv_nsec = 250000000L }; + nanosleep(&retryDelay, NULL); + lgmpAudio_pumpClaim(fixture.audio, fixture.pointerClient); + CHECK(readClaim(&fixture, &fixture.claim, WAIT_MS)); + CHECK(fixture.claim.requestGeneration != first.requestGeneration); + CHECK(fixture.claim.claimToken != first.claimToken); + + fixture.ops->setStatusListener(fixture.audio, NULL, NULL); + fixtureCleanup(&fixture); + return true; +} + +int main(void) +{ + debug_init(); + + unsigned passed = 0; + if (!testLateBoundRelease()) + return 1; + ++passed; + if (!testRetiredClaim()) + return 1; + ++passed; + if (!testDataPlane()) + return 1; + ++passed; + if (!testReleaseIsFinal()) + return 1; + ++passed; + if (!testFaultReleaseReclaim()) + return 1; + ++passed; + printf("PASS %u/5 client LGMP audio integration groups\n", passed); + return 0; +} diff --git a/client/tests/lgmp_transport_test.c b/client/tests/lgmp_transport_test.c index f3741b3e..d0deca2d 100644 --- a/client/tests/lgmp_transport_test.c +++ b/client/tests/lgmp_transport_test.c @@ -21,6 +21,9 @@ #include "interface/transport.h" #include +#ifdef ENABLE_AUDIO +#include "common/KVMFRAudio.h" +#endif #include #include #include "common/debug.h" @@ -96,6 +99,42 @@ static bool waitForQueuesEmpty(PLGMPHost host, PLGMPHostQueue frameQueue, struct VideoStatusTrace; +#ifdef ENABLE_AUDIO +struct AudioStatusTrace +{ + atomic_uint count; + atomic_bool available; +}; + +static void audioStatusChanged(void * opaque, + const LG_AudioStatus * status) +{ + struct AudioStatusTrace * trace = opaque; + atomic_store_explicit( + &trace->available, status->available, memory_order_relaxed); + atomic_fetch_add_explicit(&trace->count, 1, memory_order_release); +} + +static bool readAudioClaim(PLGMPHost host, PLGMPHostQueue queue, + KVMFRAudioClaim * claim, uint32_t * clientID) +{ + for (unsigned i = 0; i < WAIT_TIMEOUT; ++i) + { + size_t size = sizeof(*claim); + const LGMP_STATUS status = lgmpHostReadDataWithSource( + queue, claim, &size, clientID); + if (status == LGMP_OK) + return size == sizeof(*claim) && *clientID != 0 && + lgmpHostAckData(queue) == LGMP_OK; + if (status != LGMP_ERR_QUEUE_EMPTY || + lgmpHostProcess(host) != LGMP_OK) + return false; + usleep(1000); + } + return false; +} +#endif + struct WaitState { LG_Transport * transport; @@ -330,12 +369,20 @@ int main(void) PLGMPMemory malformedFrameMemory = NULL; PLGMPMemory pointerMemory = NULL; PLGMPMemory malformedPointerMemory = NULL; +#ifdef ENABLE_AUDIO + PLGMPMemory audioResponseMemory = NULL; +#endif LG_Transport * transport = NULL; LG_Transport * secondTransport = NULL; LG_Transport * thirdTransport = NULL; LG_Transport * nestedTransport = NULL; LG_Transport * admittedTransport = NULL; const LG_FrameOps * frameOps = NULL; +#ifdef ENABLE_AUDIO + const LG_AudioOps * audioOps = NULL; + void * audioOpaque = NULL; + struct AudioStatusTrace audioTrace = { 0 }; +#endif struct VideoStatusTrace videoTrace; struct VideoStatusTrace secondVideoTrace; memset(&videoTrace, 0, sizeof(videoTrace)); @@ -364,6 +411,7 @@ int main(void) KVMFR session = { 0 }; memcpy(session.magic, KVMFR_MAGIC, sizeof(session.magic)); session.version = KVMFR_VERSION; + session.features = KVMFR_FEATURE_AUDIO; memcpy(session.hostver, "transport-test", sizeof("transport-test")); CHECK(lgmpHostInit(hostMemory, TEST_SHM_SIZE, &host, sizeof(session), @@ -415,6 +463,10 @@ int main(void) LGMP_OK); CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRCursor), &malformedPointerMemory) == LGMP_OK); +#ifdef ENABLE_AUDIO + CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRAudioStreamResponse), + &audioResponseMemory) == LGMP_OK); +#endif KVMFRCursor * malformedPointer = lgmpHostMemPtr(malformedPointerMemory); malformedPointer->height = 2; malformedPointer->pitch = UINT32_MAX; @@ -484,6 +536,14 @@ int main(void) LG_TransportSession admittedSession; CHECK(LGT_LGMP.connectCancellable( admittedTransport, &admittedSession, NULL, NULL) == LG_TRANSPORT_OK); + CHECK(transportSession.features & LG_TRANSPORT_FEATURE_AUDIO); +#ifdef ENABLE_AUDIO + CHECK(LGT_LGMP.getAudioOps); + audioOps = LGT_LGMP.getAudioOps(transport, &audioOpaque); + CHECK(audioOps && audioOpaque); +#else + CHECK(!LGT_LGMP.getAudioOps); +#endif videoTrace.transport = transport; videoTrace.ops = frameOps; frameOps->setStatusListener( @@ -560,6 +620,69 @@ int main(void) &videoTrace.pointerEpoch, memory_order_acquire)); frameOps->releasePointer(transport, &pointer); +#ifdef ENABLE_AUDIO + audioOps->setStatusListener(audioOpaque, + audioStatusChanged, &audioTrace); + CHECK(atomic_load_explicit(&audioTrace.count, memory_order_acquire) == 1); + CHECK(!atomic_load_explicit( + &audioTrace.available, memory_order_acquire)); + + struct WaitState audioPointerWait = + { + .transport = transport, + .ops = frameOps, + .status = LG_TRANSPORT_ERROR, + }; + pthread_t audioPointerThread; + CHECK(pthread_create(&audioPointerThread, NULL, + waitForVideo, &audioPointerWait) == 0); + + KVMFRAudioClaim audioClaim; + uint32_t audioClientID = 0; + CHECK(readAudioClaim(host, pointerQueue, + &audioClaim, &audioClientID)); + CHECK(audioClaim.messageType == KVMFR_MESSAGE_AUDIO_CLAIM); + CHECK(audioClaim.version == KVMFR_AUDIO_CONTROL_VERSION); + CHECK(audioClaim.size == sizeof(audioClaim)); + frameOps->cancelPointerWait(transport); + CHECK(pthread_join(audioPointerThread, NULL) == 0); + CHECK(audioPointerWait.status == LG_TRANSPORT_TIMEOUT); + + KVMFRAudioStreamResponse * audioResponse = + lgmpHostMemPtr(audioResponseMemory); + *audioResponse = (KVMFRAudioStreamResponse) + { + .magic = KVMFR_AUDIO_RESPONSE_MAGIC, + .version = KVMFR_AUDIO_CONTROL_VERSION, + .size = sizeof(*audioResponse), + .requestGeneration = audioClaim.requestGeneration, + .boundClientID = audioClientID, + .result = KVMFR_AUDIO_RESPONSE_BUSY, + .claimToken = audioClaim.claimToken, + }; + + unsigned audioRecipients = 0; + CHECK(lgmpHostQueuePostForClients(pointerQueue, + UINT64_C(0x7f7f7f7f) << 32, pointerMemory, + &audioClientID, 1, &audioRecipients) == LGMP_OK); + CHECK(audioRecipients == 1); + CHECK(lgmpHostQueuePostForClients(pointerQueue, + KVMFR_AUDIO_CONTROL_UDATA, pointerMemory, + &audioClientID, 1, &audioRecipients) == LGMP_OK); + CHECK(audioRecipients == 1); + CHECK(lgmpHostQueuePostForClients(pointerQueue, + KVMFR_AUDIO_CONTROL_UDATA, audioResponseMemory, + &audioClientID, 1, &audioRecipients) == LGMP_OK); + CHECK(audioRecipients == 1); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + CHECK(pointer.x == wirePointer->x && pointer.y == wirePointer->y); + frameOps->releasePointer(transport, &pointer); + CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue)); + audioOps->setStatusListener(audioOpaque, NULL, NULL); +#endif + const uint64_t frameEpoch = atomic_load_explicit( &videoTrace.frameEpoch, memory_order_acquire); const uint64_t pointerEpoch = atomic_load_explicit( @@ -1024,6 +1147,9 @@ cleanup: if (transport) LGT_LGMP.destroy(&transport); option_free(); +#ifdef ENABLE_AUDIO + lgmpHostMemFree(&audioResponseMemory); +#endif lgmpHostMemFree(&pointerMemory); lgmpHostMemFree(&malformedPointerMemory); lgmpHostMemFree(&malformedFrameMemory); diff --git a/client/transports/LGMP/CMakeLists.txt b/client/transports/LGMP/CMakeLists.txt index a0164bd6..d6801994 100644 --- a/client/transports/LGMP/CMakeLists.txt +++ b/client/transports/LGMP/CMakeLists.txt @@ -1,12 +1,22 @@ cmake_minimum_required(VERSION 3.10) project(transport_LGMP LANGUAGES C) -add_library(transport_LGMP STATIC +set(TRANSPORT_LGMP_SOURCES clipboard.c input.c lgmp.c ) +if(ENABLE_AUDIO) + list(APPEND TRANSPORT_LGMP_SOURCES audio.c) +endif() + +add_library(transport_LGMP STATIC ${TRANSPORT_LGMP_SOURCES}) + +if(ENABLE_AUDIO) + target_compile_definitions(transport_LGMP PRIVATE ENABLE_AUDIO) +endif() + target_include_directories(transport_LGMP PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../src ) diff --git a/client/transports/LGMP/audio.c b/client/transports/LGMP/audio.c new file mode 100644 index 00000000..1112fe8c --- /dev/null +++ b/client/transports/LGMP/audio.c @@ -0,0 +1,2336 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "audio.h" + +#include +#include "common/debug.h" +#include "common/locking.h" +#include "common/thread.h" +#include "common/time.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#define AUDIO_IDLE_POLL_MS 20U +#define AUDIO_ACTIVE_POLL_MS 2U +#define AUDIO_CLAIM_RETRY_US 200000U +#define AUDIO_OWNER_RETRY_US 1200000U +#define AUDIO_DRAIN_MAX 32U + +#define AUDIO_CAPABILITIES \ + (KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | \ + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) + +#define AUDIO_STATUS_FLAGS \ + (KVMFR_AUDIO_STATUS_AVAILABLE | KVMFR_AUDIO_STATUS_HAS_OWNER | \ + KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE | \ + KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE | \ + KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE | \ + KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) + +#define AUDIO_CLOCK_FLAGS \ + (KVMFR_AUDIO_MESSAGE_CLOCK_VALID | \ + KVMFR_AUDIO_MESSAGE_CLOCK_STABLE | \ + KVMFR_AUDIO_MESSAGE_DISCONTINUITY) + +#define AUDIO_DATA_FLAGS \ + (AUDIO_CLOCK_FLAGS | KVMFR_AUDIO_MESSAGE_SILENT) + +enum AudioPhase +{ + AUDIO_PHASE_IDLE, + AUDIO_PHASE_CLAIM_PENDING, + AUDIO_PHASE_CLAIM_SENT, + AUDIO_PHASE_BOUND, + AUDIO_PHASE_RELEASING, + AUDIO_PHASE_COOLDOWN, +}; + +struct AudioGeneration +{ + uint64_t wire; + uint32_t local; +}; + +struct AudioStreamState +{ + struct AudioGeneration generation; + KVMFRAudioFormat wireFormat; + LG_AudioFormat format; + bool active; + bool formatValid; + bool volumeValid; + bool muteValid; + bool mute; + bool clockValid; + LG_AudioClock clock; + uint8_t volumeChannels; + uint16_t volume[KVMFR_AUDIO_MAX_CHANNELS]; +}; + +struct AudioStatusCallbackFrame +{ + const LGMPAudio * audio; + struct AudioStatusCallbackFrame * previous; +}; + +struct AudioCallbackWaiter +{ + struct AudioCallbackWaiter * next; + LGEvent * event; + unsigned depth; +}; + +struct AudioCallbackWaitQueue +{ + LG_Lock lock; + atomic_uint count; + struct AudioCallbackWaiter * waiters; +}; + +struct LGMPAudio +{ + PLGMPClient client; + LGEvent * pointerWake; + LGEvent * event; + LGThread * thread; + LG_Lock lock; + LG_Lock eventLock; + atomic_flag writeGate; + atomic_bool stop; + atomic_bool dataPlaneLost; + atomic_bool captureDiscontinuity; + atomic_uint statusInFlight; + struct AudioCallbackWaitQueue statusWait; + + bool connected; + bool discover; + bool attached; + bool statusAvailable; + bool statusDirty; + bool responsePending; + bool claimPosting; + bool sequenceKnown; + bool playbackDiscontinuity; + bool releaseRequested; + bool releaseSent; + bool hostReadActive; + bool clientWriteActive; + enum AudioPhase phase; + uint32_t clientID; + uint32_t requestSerial; + uint32_t requestGeneration; + unsigned claimSends; + uint64_t claimToken; + uint64_t nextClaimAt; + uint64_t releaseDeadline; + uint64_t nextKeepalive; + uint64_t nextAttachAt; + uint64_t expectedSequence; + uint32_t generationSerial; + struct AudioGeneration endpointGeneration; + KVMFRAudioClaim claim; + KVMFRAudioStreamResponse response; + KVMFRAudioStatus remoteStatus; + struct AudioStreamState playback; + struct AudioStreamState capture; + + PLGMPClientStream hostToClientStream; + PLGMPClientStream clientToHostStream; + LGMPStreamPollState streamPoll; + + /* Protected by writeGate. */ + uint64_t clientSequence; + bool writeBound; + bool publicDataAllowed; + uint64_t writeCaptureGeneration; + uint32_t writeCaptureLocalGeneration; + KVMFRAudioFormat writeCaptureFormat; + uint64_t writePlaybackGeneration; + uint32_t writePlaybackLocalGeneration; + + const LG_AudioEventOps * events; + void * eventOpaque; + LG_AudioStatusFn statusCallback; + void * statusOpaque; +}; + +static _Thread_local struct AudioStatusCallbackFrame * l_statusFrames; + +_Static_assert(sizeof(struct LGMPStreamDescriptor) == + sizeof(KVMFRStreamDescriptor), + "LGMP and KVMFR stream 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(KVMFR_AUDIO_MAX_CHANNELS <= LG_AUDIO_MAX_CHANNELS, + "client audio cannot represent every KVMFR speaker position"); + +static uint32_t nextNonzero(uint32_t * value) +{ + if (++*value == 0) + ++*value; + return *value; +} + +static bool randomBytes(void * buffer, size_t size) +{ + uint8_t * output = buffer; + while (size) + { + const ssize_t received = getrandom(output, size, 0); + if (received < 0 && errno == EINTR) + continue; + if (received <= 0) + return false; + output += (size_t)received; + size -= (size_t)received; + } + return true; +} + +static bool tryWriteGate(LGMPAudio * audio) +{ + return !atomic_flag_test_and_set_explicit( + &audio->writeGate, memory_order_acquire); +} + +static void lockWriteGate(LGMPAudio * audio) +{ + while (!tryWriteGate(audio)) + { + } +} + +static void unlockWriteGate(LGMPAudio * audio) +{ + atomic_flag_clear_explicit(&audio->writeGate, memory_order_release); +} + +/* writeGate must be held. */ +static void closePublicDataLocked(LGMPAudio * audio) +{ + audio->publicDataAllowed = false; + audio->writeCaptureGeneration = 0; + audio->writeCaptureLocalGeneration = 0; + audio->writePlaybackGeneration = 0; + audio->writePlaybackLocalGeneration = 0; + memset(&audio->writeCaptureFormat, 0, + sizeof(audio->writeCaptureFormat)); +} + +static void signalWorker(LGMPAudio * audio) +{ + lgSignalEvent(audio->event); +} + +static unsigned statusCallbackDepth(const LGMPAudio * audio) +{ + unsigned depth = 0; + for (const struct AudioStatusCallbackFrame * frame = l_statusFrames; + frame; frame = frame->previous) + if (frame->audio == audio) + ++depth; + return depth; +} + +static void signalCallbackWaiters( + struct AudioCallbackWaitQueue * queue, unsigned remaining) +{ + if (!atomic_load_explicit(&queue->count, memory_order_acquire)) + return; + + LG_LOCK(queue->lock); + struct AudioCallbackWaiter ** link = &queue->waiters; + while (*link) + { + struct AudioCallbackWaiter * waiter = *link; + if (remaining > waiter->depth) + { + link = &waiter->next; + continue; + } + + *link = waiter->next; + atomic_fetch_sub_explicit( + &queue->count, 1, memory_order_release); + lgSignalEvent(waiter->event); + } + LG_UNLOCK(queue->lock); +} + +/* audio->lock must protect callback admission. */ +static void beginStatusCallback(LGMPAudio * audio, + struct AudioStatusCallbackFrame * frame) +{ + atomic_fetch_add_explicit( + &audio->statusInFlight, 1, memory_order_relaxed); + frame->audio = audio; + frame->previous = l_statusFrames; + l_statusFrames = frame; +} + +static void endStatusCallback(LGMPAudio * audio, + const struct AudioStatusCallbackFrame * frame) +{ + l_statusFrames = frame->previous; + const unsigned previous = atomic_fetch_sub_explicit( + &audio->statusInFlight, 1, memory_order_release); + DEBUG_ASSERT(previous > 0); + signalCallbackWaiters(&audio->statusWait, previous - 1); +} + +static void waitStatusCallbacks(LGMPAudio * audio) +{ + const unsigned depth = statusCallbackDepth(audio); + if (atomic_load_explicit( + &audio->statusInFlight, memory_order_acquire) <= depth) + return; + + struct AudioCallbackWaiter waiter = + { + .event = lgCreateEvent(true, 0), + .depth = depth, + }; + if (!waiter.event) + DEBUG_FATAL("Failed to create LGMP audio callback wait event"); + + bool queued = false; + LG_LOCK(audio->statusWait.lock); + atomic_fetch_add_explicit( + &audio->statusWait.count, 1, memory_order_release); + if (atomic_load_explicit( + &audio->statusInFlight, memory_order_acquire) > depth) + { + waiter.next = audio->statusWait.waiters; + audio->statusWait.waiters = &waiter; + queued = true; + } + else + atomic_fetch_sub_explicit( + &audio->statusWait.count, 1, memory_order_release); + LG_UNLOCK(audio->statusWait.lock); + + if (queued) + { + if (!lgWaitEvent(waiter.event, TIMEOUT_INFINITE)) + DEBUG_FATAL("Failed to wait for LGMP audio status callbacks"); + LG_LOCK(audio->statusWait.lock); + LG_UNLOCK(audio->statusWait.lock); + } + lgFreeEvent(waiter.event); +} + +static unsigned channelCount(uint32_t mask) +{ + unsigned count = 0; + while (mask) + { + count += mask & 1U; + mask >>= 1; + } + return count; +} + +static LG_AudioChannel channelFromBit(unsigned bit) +{ + static const LG_AudioChannel channels[KVMFR_AUDIO_MAX_CHANNELS] = + { + LG_AUDIO_CH_FRONT_LEFT, + LG_AUDIO_CH_FRONT_RIGHT, + LG_AUDIO_CH_FRONT_CENTER, + LG_AUDIO_CH_LFE, + LG_AUDIO_CH_REAR_LEFT, + LG_AUDIO_CH_REAR_RIGHT, + LG_AUDIO_CH_FRONT_LEFT_CENTER, + LG_AUDIO_CH_FRONT_RIGHT_CENTER, + LG_AUDIO_CH_REAR_CENTER, + LG_AUDIO_CH_SIDE_LEFT, + LG_AUDIO_CH_SIDE_RIGHT, + LG_AUDIO_CH_TOP_CENTER, + LG_AUDIO_CH_TOP_FRONT_LEFT, + LG_AUDIO_CH_TOP_FRONT_CENTER, + LG_AUDIO_CH_TOP_FRONT_RIGHT, + LG_AUDIO_CH_TOP_REAR_LEFT, + LG_AUDIO_CH_TOP_REAR_CENTER, + LG_AUDIO_CH_TOP_REAR_RIGHT, + }; + return bit < KVMFR_AUDIO_MAX_CHANNELS ? + channels[bit] : LG_AUDIO_CH_UNKNOWN; +} + +static bool validFormat(const KVMFRAudioFormat * wire, + LG_AudioFormat * result) +{ + if (!wire || wire->version != KVMFR_AUDIO_FORMAT_VERSION || + wire->size != sizeof(*wire) || !wire->sampleRate || + wire->sampleRate > KVMFR_AUDIO_MAX_SAMPLE_RATE || + !wire->channelMask || + (wire->channelMask & ~KVMFR_AUDIO_CHANNEL_POSITION_MASK) || + !wire->channelCount || + wire->channelCount > KVMFR_AUDIO_STREAM_MAX_CHANNELS || + channelCount(wire->channelMask) != wire->channelCount || + wire->reserved[0] || wire->reserved[1]) + return false; + + LG_AudioSampleFormat format; + unsigned bytesPerSample; + switch (wire->sampleFormat) + { + case KVMFR_AUDIO_SAMPLE_S16_LE: + format = LG_AUDIO_FMT_S16_LE; + bytesPerSample = 2; + if (wire->validBits != 16 || wire->containerBits != 16) + return false; + break; + + case KVMFR_AUDIO_SAMPLE_S24_LE: + format = LG_AUDIO_FMT_S24_LE; + bytesPerSample = 3; + if (wire->validBits != 24 || wire->containerBits != 24) + return false; + break; + + default: + return false; + } + + if (wire->blockAlign != wire->channelCount * bytesPerSample) + return false; + + *result = (LG_AudioFormat) + { + .sampleFormat = format, + .sampleRate = wire->sampleRate, + .channelCount = (uint8_t)wire->channelCount, + }; + unsigned channel = 0; + for (unsigned bit = 0; bit < KVMFR_AUDIO_MAX_CHANNELS; ++bit) + if (wire->channelMask & (UINT32_C(1) << bit)) + result->channels[channel++] = channelFromBit(bit); + return true; +} + +static bool sameFormat(const KVMFRAudioFormat * left, + const KVMFRAudioFormat * right) +{ + return !memcmp(left, right, sizeof(*left)); +} + +static bool emptyFormat(const KVMFRAudioFormat * format) +{ + const KVMFRAudioFormat empty = { 0 }; + return !memcmp(format, &empty, sizeof(empty)); +} + +static bool validClockFlags(uint32_t flags) +{ + return !(flags & ~AUDIO_CLOCK_FLAGS) && + (!(flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) || + (flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID)); +} + +static LG_AudioClock clockFromMessage(const KVMFRAudioMessage * message, + bool discontinuity) +{ + return (LG_AudioClock) + { + .position = message->position, + .time = message->timeNs, + .rate = (double)message->rateQ32 / 4294967296.0, + .stable = + (message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) != 0, + .discontinuity = discontinuity || + (message->flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY) != 0, + }; +} + +static uint32_t mapGeneration(LGMPAudio * audio, + struct AudioGeneration * generation, uint64_t wire) +{ + if (!wire) + return 0; + if (generation->wire != wire || !generation->local) + { + generation->wire = wire; + generation->local = nextNonzero(&audio->generationSerial); + } + return generation->local; +} + +static struct LGMPStreamDescriptor descriptorFromWire( + const KVMFRStreamDescriptor * wire) +{ + return (struct LGMPStreamDescriptor) + { + .magic = wire->magic, + .version = wire->version, + .size = wire->size, + .offset = wire->offset, + .regionSize = wire->regionSize, + .direction = wire->direction, + .policy = wire->policy, + .slotCount = wire->slotCount, + .slotSize = wire->slotSize, + }; +} + +static bool validDescriptor(const KVMFRStreamDescriptor * descriptor, + enum LGMPStreamDirection direction) +{ + return descriptor->magic == LGMP_STREAM_DESCRIPTOR_MAGIC && + descriptor->version == LGMP_STREAM_DESCRIPTOR_VERSION && + descriptor->size == sizeof(*descriptor) && descriptor->offset && + descriptor->regionSize && + 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; +} + +static void markStatusNL(LGMPAudio * audio, bool available, + uint64_t endpointGeneration) +{ + const uint32_t oldGeneration = audio->endpointGeneration.local; + if (endpointGeneration) + mapGeneration(audio, &audio->endpointGeneration, endpointGeneration); + if (audio->statusAvailable != available || + oldGeneration != audio->endpointGeneration.local) + audio->statusDirty = true; + audio->statusAvailable = available; +} + +static void publishStatus(LGMPAudio * audio) +{ + LG_LOCK(audio->lock); + if (!audio->statusDirty) + { + LG_UNLOCK(audio->lock); + return; + } + audio->statusDirty = false; + const LG_AudioStatusFn callback = audio->statusCallback; + void * opaque = audio->statusOpaque; + const LG_AudioStatus status = + { + .available = audio->connected && audio->statusAvailable, + .generation = audio->endpointGeneration.local, + }; + struct AudioStatusCallbackFrame frame; + if (callback) + beginStatusCallback(audio, &frame); + LG_UNLOCK(audio->lock); + if (callback) + { + callback(opaque, &status); + endStatusCallback(audio, &frame); + } +} + +static void updateWriteStreamState(LGMPAudio * audio) +{ + lockWriteGate(audio); + LG_LOCK(audio->lock); + const bool admitted = audio->writeBound && + audio->phase == AUDIO_PHASE_BOUND && !audio->releaseRequested; + const uint64_t captureGeneration = admitted && audio->capture.active ? + audio->capture.generation.wire : 0; + const uint32_t captureLocalGeneration = + admitted && audio->capture.active ? + audio->capture.generation.local : 0; + const bool captureChanged = + audio->writeCaptureGeneration != captureGeneration || + audio->writeCaptureLocalGeneration != captureLocalGeneration; + audio->writeCaptureGeneration = captureGeneration; + audio->writeCaptureLocalGeneration = captureLocalGeneration; + audio->writeCaptureFormat = audio->capture.wireFormat; + audio->writePlaybackGeneration = admitted && audio->playback.active ? + audio->playback.generation.wire : 0; + audio->writePlaybackLocalGeneration = + admitted && audio->playback.active ? + audio->playback.generation.local : 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); +} + +static void stopActiveStreams(LGMPAudio * audio) +{ + const LG_AudioEventOps * events; + void * opaque; + uint32_t playbackGeneration; + uint32_t captureGeneration; + + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + events = audio->attached ? audio->events : NULL; + opaque = audio->eventOpaque; + playbackGeneration = audio->playback.active ? + audio->playback.generation.local : 0; + captureGeneration = audio->capture.active ? + audio->capture.generation.local : 0; + memset(&audio->playback, 0, sizeof(audio->playback)); + memset(&audio->capture, 0, sizeof(audio->capture)); + audio->playbackDiscontinuity = false; + LG_UNLOCK(audio->lock); + updateWriteStreamState(audio); + + if (events && playbackGeneration && events->playbackStop) + events->playbackStop(opaque, playbackGeneration); + if (events && captureGeneration && events->recordStop) + events->recordStop(opaque, captureGeneration); + LG_UNLOCK(audio->eventLock); +} + +static void detachStreams(LGMPAudio * audio) +{ + lockWriteGate(audio); + audio->publicDataAllowed = false; + audio->writeBound = false; + audio->writeCaptureGeneration = 0; + audio->writeCaptureLocalGeneration = 0; + audio->writePlaybackGeneration = 0; + audio->writePlaybackLocalGeneration = 0; + memset(&audio->writeCaptureFormat, 0, + sizeof(audio->writeCaptureFormat)); + + lgmpClientStreamDetach(&audio->clientToHostStream); + lgmpClientStreamDetach(&audio->hostToClientStream); + audio->hostReadActive = false; + audio->clientWriteActive = false; + audio->clientSequence = 0; + atomic_store_explicit( + &audio->captureDiscontinuity, false, memory_order_release); + unlockWriteGate(audio); +} + +static void scheduleRetryNL(LGMPAudio * audio, uint64_t delay) +{ + audio->phase = AUDIO_PHASE_IDLE; + audio->requestGeneration = 0; + audio->claimToken = 0; + audio->claimSends = 0; + audio->claimPosting = false; + audio->releaseRequested = false; + audio->releaseSent = false; + memset(&audio->claim, 0, sizeof(audio->claim)); + audio->nextClaimAt = microtime() + delay; +} + +static void finishBinding(LGMPAudio * audio, uint64_t delay) +{ + stopActiveStreams(audio); + detachStreams(audio); + atomic_store_explicit( + &audio->dataPlaneLost, false, memory_order_release); + + LG_LOCK(audio->lock); + memset(&audio->remoteStatus, 0, sizeof(audio->remoteStatus)); + audio->sequenceKnown = false; + audio->expectedSequence = 0; + audio->releaseRequested = false; + audio->releaseSent = false; + audio->responsePending = false; + markStatusNL(audio, false, 0); + audio->endpointGeneration.wire = 0; + if (audio->connected && audio->discover) + scheduleRetryNL(audio, delay); + else + { + audio->phase = AUDIO_PHASE_IDLE; + audio->requestGeneration = 0; + audio->claimToken = 0; + audio->claimSends = 0; + audio->claimPosting = false; + } + LG_UNLOCK(audio->lock); + if (audio->pointerWake) + lgSignalEvent(audio->pointerWake); +} + +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)) + { + DEBUG_WARN("Invalid LGMP audio stream descriptors"); + return false; + } + + LGMP_STATUS hostStatus = LGMP_OK; + if (!audio->hostToClientStream) + { + const struct LGMPStreamDescriptor hostToClient = + descriptorFromWire(&response->hostToClient); + hostStatus = lgmpClientStreamAttach(audio->client, + &hostToClient, &audio->hostToClientStream); + } + if (hostStatus == LGMP_OK) + { + uint32_t epoch = 0; + hostStatus = lgmpClientStreamActivate( + audio->hostToClientStream, &epoch); + audio->hostReadActive = 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; + if (hostOwner) + { + uint32_t clientID = 0; + uint32_t epoch = 0; + hostOwner = lgmpClientStreamGetBinding( + audio->hostToClientStream, &clientID, &epoch) == LGMP_OK && + clientID == audio->clientID && epoch; + } + + lockWriteGate(audio); + LGMP_STATUS clientStatus = LGMP_OK; + if (!audio->clientToHostStream) + { + const struct LGMPStreamDescriptor clientToHost = + descriptorFromWire(&response->clientToHost); + clientStatus = lgmpClientStreamAttach(audio->client, + &clientToHost, &audio->clientToHostStream); + } + if (clientStatus == LGMP_OK) + { + uint32_t epoch = 0; + clientStatus = lgmpClientStreamActivate( + audio->clientToHostStream, &epoch); + audio->clientWriteActive = clientStatus == LGMP_OK; + } + bool clientOwner = audio->clientWriteActive; + if (clientOwner) + { + uint32_t clientID = 0; + uint32_t epoch = 0; + clientOwner = lgmpClientStreamGetBinding( + audio->clientToHostStream, &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); + + if (clientStatus != LGMP_OK) + DEBUG_WARN("Failed to attach/activate client-to-host audio stream: %s", + lgmpStatusString(clientStatus)); + audio->hostReadActive = hostOwner; + lgmpStreamPollActivity(&audio->streamPoll); + return hostOwner && clientOwner; +} + +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; +} + +static void requestRelease(LGMPAudio * audio, const char * reason) +{ + bool log = false; + lockWriteGate(audio); + LG_LOCK(audio->lock); + closePublicDataLocked(audio); + if (!audio->releaseRequested) + { + audio->releaseRequested = true; + audio->releaseDeadline = + microtime() + AUDIO_OWNER_RETRY_US; + markStatusNL(audio, false, 0); + log = true; + } + if (audio->phase == AUDIO_PHASE_BOUND) + audio->phase = AUDIO_PHASE_RELEASING; + LG_UNLOCK(audio->lock); + unlockWriteGate(audio); + if (log) + DEBUG_INFO("LGMP audio ownership releasing: %s", reason); + signalWorker(audio); +} + +static bool validEmptyMessage(const KVMFRAudioMessage * message, + uint32_t allowedFlags) +{ + return !(message->flags & ~allowedFlags) && + !message->position && !message->timeNs && !message->rateQ32 && + !message->frames && !message->length; +} + +static bool validVolume(const KVMFRAudioVolume * volume, + uint8_t channels) +{ + if (volume->version != KVMFR_AUDIO_VOLUME_VERSION || + volume->size != sizeof(*volume) || volume->reserved || + volume->channelCount != channels) + return false; + for (unsigned i = channels; i < KVMFR_AUDIO_MAX_CHANNELS; ++i) + if (volume->values[i]) + return false; + return true; +} + +static bool validStatus(const LGMPAudio * audio, + const KVMFRAudioStatus * status) +{ + if (status->version != KVMFR_AUDIO_STATUS_VERSION || + status->size != sizeof(*status) || + (status->flags & ~AUDIO_STATUS_FLAGS) || + 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->playbackState > KVMFR_AUDIO_STREAM_RUNNING || + status->captureState > KVMFR_AUDIO_STREAM_RUNNING) + return false; + + const bool playbackAvailable = + (status->flags & KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE) != 0; + const bool captureAvailable = + (status->flags & KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE) != 0; + if (playbackAvailable != + ((status->capabilities & KVMFR_AUDIO_CAP_PLAYBACK) != 0) || + captureAvailable != + ((status->capabilities & KVMFR_AUDIO_CAP_CAPTURE) != 0)) + return false; + + const bool playbackActive = + (status->flags & KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE) != 0; + const bool captureActive = + (status->flags & KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) != 0; + const bool available = + (status->flags & KVMFR_AUDIO_STATUS_AVAILABLE) != 0; + LG_AudioFormat ignored; + if (playbackActive && + (!available || !playbackAvailable || !status->playbackGeneration || + status->playbackState == KVMFR_AUDIO_STREAM_STOPPED || + !validFormat(&status->playbackFormat, &ignored))) + return false; + if (captureActive && + (!available || !captureAvailable || !status->captureGeneration || + status->captureState == KVMFR_AUDIO_STREAM_STOPPED || + !validFormat(&status->captureFormat, &ignored))) + return false; + if ((!emptyFormat(&status->playbackFormat) && + !validFormat(&status->playbackFormat, &ignored)) || + (!emptyFormat(&status->captureFormat) && + !validFormat(&status->captureFormat, &ignored))) + return false; + return true; +} + +static bool currentPlaybackNL(const LGMPAudio * audio, + uint64_t wireGeneration) +{ + return audio->playback.active && wireGeneration && + audio->playback.generation.wire == wireGeneration; +} + +static bool currentCaptureNL(const LGMPAudio * audio, + uint64_t wireGeneration) +{ + return audio->capture.active && wireGeneration && + audio->capture.generation.wire == wireGeneration; +} + +static void dispatchPlaybackStop(LGMPAudio * audio, + uint64_t wireGeneration) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentPlaybackNL(audio, wireGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + const uint32_t generation = audio->playback.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + memset(&audio->playback, 0, sizeof(audio->playback)); + audio->playbackDiscontinuity = false; + LG_UNLOCK(audio->lock); + updateWriteStreamState(audio); + if (events && events->playbackStop) + events->playbackStop(opaque, generation); + LG_UNLOCK(audio->eventLock); +} + +static void dispatchCaptureStop(LGMPAudio * audio, + uint64_t wireGeneration) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentCaptureNL(audio, wireGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + const uint32_t generation = audio->capture.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + memset(&audio->capture, 0, sizeof(audio->capture)); + LG_UNLOCK(audio->lock); + updateWriteStreamState(audio); + if (events && events->recordStop) + events->recordStop(opaque, generation); + LG_UNLOCK(audio->eventLock); +} + +static void dispatchPlaybackStart(LGMPAudio * audio, + const KVMFRAudioMessage * message, const KVMFRAudioFormat * wire, + const LG_AudioFormat * format, bool sequenceGap) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + const uint32_t oldGeneration = audio->playback.active ? + audio->playback.generation.local : 0; + const uint32_t generation = mapGeneration(audio, + &audio->playback.generation, message->streamGeneration); + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + audio->playback.wireFormat = *wire; + audio->playback.format = *format; + audio->playback.active = true; + audio->playback.formatValid = true; + audio->playback.volumeValid = false; + audio->playback.muteValid = false; + audio->playback.clockValid = + (message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0; + audio->playback.clock = clockFromMessage(message, sequenceGap); + audio->playbackDiscontinuity = sequenceGap || + (message->flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY); + LG_UNLOCK(audio->lock); + updateWriteStreamState(audio); + + if (events && oldGeneration && oldGeneration != generation && + events->playbackStop) + events->playbackStop(opaque, oldGeneration); + if (events && events->playbackStart) + { + const bool clockValid = + (message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0; + const LG_AudioClock clock = + clockFromMessage(message, sequenceGap); + events->playbackStart(opaque, generation, format, + clockValid ? &clock : NULL); + } + LG_UNLOCK(audio->eventLock); +} + +static void dispatchCaptureStart(LGMPAudio * audio, + const KVMFRAudioMessage * message, const KVMFRAudioFormat * wire, + const LG_AudioFormat * format) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + const uint32_t oldGeneration = audio->capture.active ? + audio->capture.generation.local : 0; + const uint32_t generation = mapGeneration(audio, + &audio->capture.generation, message->streamGeneration); + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + audio->capture.wireFormat = *wire; + audio->capture.format = *format; + audio->capture.active = true; + audio->capture.formatValid = true; + audio->capture.volumeValid = false; + audio->capture.muteValid = false; + LG_UNLOCK(audio->lock); + updateWriteStreamState(audio); + + if (events && oldGeneration && oldGeneration != generation && + events->recordStop) + events->recordStop(opaque, oldGeneration); + if (events && events->recordStart) + events->recordStart(opaque, generation, format); + LG_UNLOCK(audio->eventLock); +} + +static void dispatchPlaybackVolume(LGMPAudio * audio, + uint64_t wireGeneration, const KVMFRAudioVolume * volume) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentPlaybackNL(audio, wireGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + const uint32_t generation = audio->playback.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + audio->playback.volumeValid = true; + audio->playback.volumeChannels = (uint8_t)volume->channelCount; + memcpy(audio->playback.volume, volume->values, + sizeof(audio->playback.volume)); + LG_UNLOCK(audio->lock); + if (events && events->playbackVolume) + events->playbackVolume(opaque, generation, + (uint8_t)volume->channelCount, volume->values); + LG_UNLOCK(audio->eventLock); +} + +static void dispatchCaptureVolume(LGMPAudio * audio, + uint64_t wireGeneration, const KVMFRAudioVolume * volume) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentCaptureNL(audio, wireGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + const uint32_t generation = audio->capture.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + audio->capture.volumeValid = true; + audio->capture.volumeChannels = (uint8_t)volume->channelCount; + memcpy(audio->capture.volume, volume->values, + sizeof(audio->capture.volume)); + LG_UNLOCK(audio->lock); + if (events && events->recordVolume) + events->recordVolume(opaque, generation, + (uint8_t)volume->channelCount, volume->values); + LG_UNLOCK(audio->eventLock); +} + +static void dispatchPlaybackMute(LGMPAudio * audio, + uint64_t wireGeneration, bool mute) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentPlaybackNL(audio, wireGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + const uint32_t generation = audio->playback.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + audio->playback.muteValid = true; + audio->playback.mute = mute; + LG_UNLOCK(audio->lock); + if (events && events->playbackMute) + events->playbackMute(opaque, generation, mute); + LG_UNLOCK(audio->eventLock); +} + +static void dispatchCaptureMute(LGMPAudio * audio, + uint64_t wireGeneration, bool mute) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentCaptureNL(audio, wireGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + const uint32_t generation = audio->capture.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + audio->capture.muteValid = true; + audio->capture.mute = mute; + LG_UNLOCK(audio->lock); + if (events && events->recordMute) + events->recordMute(opaque, generation, mute); + LG_UNLOCK(audio->eventLock); +} + +static const uint8_t l_silence[KVMFR_AUDIO_PCM_BYTES]; + +static void dispatchPlaybackData(LGMPAudio * audio, + const KVMFRAudioMessage * message, const uint8_t * payload, + bool sequenceGap) +{ + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + if (!currentPlaybackNL(audio, message->streamGeneration)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + return; + } + + const bool silent = + (message->flags & KVMFR_AUDIO_MESSAGE_SILENT) != 0; + const size_t bytes = + (size_t)message->frames * audio->playback.wireFormat.blockAlign; + if (bytes > KVMFR_AUDIO_PCM_BYTES || + (silent ? message->length != 0 : message->length != bytes)) + { + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); + requestRelease(audio, "invalid playback audio length"); + return; + } + + const uint32_t generation = audio->playback.generation.local; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + const bool discontinuity = sequenceGap || + audio->playbackDiscontinuity; + const bool clockValid = + (message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0; + if (!events || !events->playbackData) + audio->playbackDiscontinuity = true; + else + audio->playbackDiscontinuity = false; + LG_UNLOCK(audio->lock); + + if (events && events->playbackData) + { + const LG_AudioClock clock = + clockFromMessage(message, discontinuity); + events->playbackData(opaque, generation, + silent ? l_silence : payload, message->frames, + clockValid ? &clock : NULL); + } + LG_UNLOCK(audio->eventLock); +} + +static bool processStatus(LGMPAudio * audio, + const KVMFRAudioMessage * message, const uint8_t * payload) +{ + if (message->streamGeneration || message->flags || + message->position || message->timeNs || message->rateQ32 || + message->frames || message->length != sizeof(KVMFRAudioStatus)) + return false; + + KVMFRAudioStatus status; + memcpy(&status, payload, sizeof(status)); + 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; + } + + audio->remoteStatus = status; + const bool available = + (status.flags & KVMFR_AUDIO_STATUS_AVAILABLE) != 0; + markStatusNL(audio, available, status.endpointGeneration); + } + LG_UNLOCK(audio->lock); + return valid; +} + +static bool processInboundMessage(LGMPAudio * audio, + const KVMFRAudioMessage * message, const uint8_t * payload, + bool sequenceGap) +{ + switch (message->type) + { + case KVMFR_AUDIO_MESSAGE_STATUS: + return processStatus(audio, message, payload); + + case KVMFR_AUDIO_MESSAGE_PLAYBACK_START: + case KVMFR_AUDIO_MESSAGE_CAPTURE_START: + { + if (!message->streamGeneration || message->frames || + message->length != sizeof(KVMFRAudioFormat) || + !validClockFlags(message->flags) || + (!(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) && + (message->position || message->timeNs || message->rateQ32))) + return false; + KVMFRAudioFormat wire; + LG_AudioFormat format; + memcpy(&wire, payload, sizeof(wire)); + if (!validFormat(&wire, &format)) + return false; + if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_START) + dispatchPlaybackStart( + audio, message, &wire, &format, sequenceGap); + else + dispatchCaptureStart(audio, message, &wire, &format); + return true; + } + + case KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP: + case KVMFR_AUDIO_MESSAGE_CAPTURE_STOP: + if (!message->streamGeneration || + !validEmptyMessage(message, 0)) + return false; + if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP) + dispatchPlaybackStop(audio, message->streamGeneration); + else + dispatchCaptureStop(audio, message->streamGeneration); + return true; + + case KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME: + case KVMFR_AUDIO_MESSAGE_CAPTURE_VOLUME: + { + if (!message->streamGeneration || message->flags || + message->position || message->timeNs || message->rateQ32 || + message->frames || + message->length != sizeof(KVMFRAudioVolume)) + return false; + KVMFRAudioVolume volume; + memcpy(&volume, payload, sizeof(volume)); + LG_LOCK(audio->lock); + const bool current = + message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME ? + currentPlaybackNL(audio, message->streamGeneration) : + currentCaptureNL(audio, message->streamGeneration); + const uint8_t channels = + message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME ? + audio->playback.format.channelCount : + audio->capture.format.channelCount; + LG_UNLOCK(audio->lock); + if (!current) + return true; + if (!validVolume(&volume, channels)) + return false; + if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME) + dispatchPlaybackVolume( + audio, message->streamGeneration, &volume); + else + dispatchCaptureVolume( + audio, message->streamGeneration, &volume); + return true; + } + + case KVMFR_AUDIO_MESSAGE_PLAYBACK_MUTE: + case KVMFR_AUDIO_MESSAGE_CAPTURE_MUTE: + if (!message->streamGeneration || + !validEmptyMessage(message, KVMFR_AUDIO_MESSAGE_MUTED)) + return false; + if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_MUTE) + dispatchPlaybackMute(audio, message->streamGeneration, + (message->flags & KVMFR_AUDIO_MESSAGE_MUTED) != 0); + else + dispatchCaptureMute(audio, message->streamGeneration, + (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; + } +} + +enum WriteResult +{ + WRITE_OK, + WRITE_FULL, + WRITE_LOST, +}; + +static enum WriteResult writeMessageLocked(LGMPAudio * audio, + 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) + return WRITE_LOST; + + LGMPStreamBuffer buffer = { 0 }; + LGMP_STATUS status = lgmpClientStreamWriteAcquire( + audio->clientToHostStream, &buffer); + if (status == LGMP_ERR_STREAM_FULL) + return WRITE_FULL; + if (status != LGMP_OK) + { + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + return WRITE_LOST; + } + + const uint32_t bytes = (uint32_t)sizeof(KVMFRAudioMessage) + length; + if (!buffer.data || buffer.capacity < bytes) + { + (void)lgmpClientStreamWriteCancel( + audio->clientToHostStream, &buffer); + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + return WRITE_LOST; + } + + const KVMFRAudioMessage message = + { + .version = KVMFR_AUDIO_MESSAGE_VERSION, + .size = sizeof(KVMFRAudioMessage), + .type = type, + .streamGeneration = streamGeneration, + .flags = flags, + .sequence = audio->clientSequence, + .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); + + status = lgmpClientStreamWriteCommit( + audio->clientToHostStream, &buffer, bytes); + if (status == LGMP_OK) + { + ++audio->clientSequence; + return WRITE_OK; + } + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + return WRITE_LOST; +} + +static bool validResponse(const LGMPAudio * audio, + const KVMFRAudioStreamResponse * response) +{ + if (response->magic != KVMFR_AUDIO_RESPONSE_MAGIC || + response->version != KVMFR_AUDIO_CONTROL_VERSION || + response->size != sizeof(*response) || + response->requestGeneration != audio->requestGeneration || + response->boundClientID != audio->clientID || + response->claimToken != audio->claimToken || + response->result < KVMFR_AUDIO_RESPONSE_BOUND || + response->result > KVMFR_AUDIO_RESPONSE_ERROR) + return false; + for (unsigned i = 0; i < sizeof(response->cursorCompatibility); ++i) + if (response->cursorCompatibility[i]) + return false; + + 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); + + const KVMFRStreamDescriptor empty = { 0 }; + return !response->protocolVersion && + !memcmp(&response->hostToClient, &empty, sizeof(empty)) && + !memcmp(&response->clientToHost, &empty, sizeof(empty)); +} + +static void processResponse(LGMPAudio * audio) +{ + KVMFRAudioStreamResponse response; + bool pending = false; + LG_LOCK(audio->lock); + if (audio->responsePending) + { + response = audio->response; + audio->responsePending = false; + pending = true; + } + LG_UNLOCK(audio->lock); + if (!pending) + return; + + LG_LOCK(audio->lock); + const bool current = audio->connected && + (audio->phase == AUDIO_PHASE_CLAIM_SENT || + audio->phase == AUDIO_PHASE_CLAIM_PENDING || + audio->phase == AUDIO_PHASE_COOLDOWN) && + response.requestGeneration == audio->requestGeneration && + response.claimToken == audio->claimToken; + const bool valid = current && validResponse(audio, &response); + LG_UNLOCK(audio->lock); + if (!current) + return; + if (!valid) + { + DEBUG_WARN("Ignoring malformed matching LGMP audio response"); + LG_LOCK(audio->lock); + audio->phase = AUDIO_PHASE_COOLDOWN; + audio->releaseDeadline = + microtime() + AUDIO_OWNER_RETRY_US; + LG_UNLOCK(audio->lock); + return; + } + + if (response.result != KVMFR_AUDIO_RESPONSE_BOUND) + { + if (response.result == KVMFR_AUDIO_RESPONSE_INCOMPATIBLE) + DEBUG_WARN("The host LGMP audio protocol is incompatible"); + LG_LOCK(audio->lock); + scheduleRetryNL(audio, response.result == + KVMFR_AUDIO_RESPONSE_INCOMPATIBLE ? + AUDIO_OWNER_RETRY_US : AUDIO_CLAIM_RETRY_US); + LG_UNLOCK(audio->lock); + if (audio->pointerWake) + lgSignalEvent(audio->pointerWake); + return; + } + + if (!attachStreams(audio, &response)) + { + LG_LOCK(audio->lock); + audio->phase = AUDIO_PHASE_RELEASING; + audio->releaseRequested = true; + audio->releaseSent = false; + audio->releaseDeadline = + microtime() + AUDIO_OWNER_RETRY_US; + audio->nextAttachAt = microtime() + AUDIO_CLAIM_RETRY_US; + LG_UNLOCK(audio->lock); + return; + } + + LG_LOCK(audio->lock); + if (!audio->connected || + response.requestGeneration != audio->requestGeneration || + response.claimToken != audio->claimToken) + { + audio->releaseRequested = true; + audio->phase = AUDIO_PHASE_RELEASING; + } + else + { + mapGeneration(audio, &audio->endpointGeneration, + response.endpointGeneration); + audio->phase = audio->releaseRequested || !audio->discover ? + AUDIO_PHASE_RELEASING : AUDIO_PHASE_BOUND; + } + audio->sequenceKnown = false; + audio->expectedSequence = 0; + audio->releaseSent = false; + audio->nextKeepalive = microtime(); + LG_UNLOCK(audio->lock); + DEBUG_INFO("LGMP audio streams attached for request %u", + response.requestGeneration); +} + +static unsigned drainInbound(LGMPAudio * audio) +{ + unsigned drained = 0; + while (audio->hostToClientStream && audio->hostReadActive && + drained < AUDIO_DRAIN_MAX) + { + LGMPStreamBuffer buffer = { 0 }; + LGMP_STATUS status = lgmpClientStreamReadPeek( + audio->hostToClientStream, &buffer); + if (status == LGMP_ERR_STREAM_EMPTY) + 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 audio stream: %s", + lgmpStatusString(status)); + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + break; + } + + ++drained; + LG_LOCK(audio->lock); + const bool releasing = audio->phase == AUDIO_PHASE_RELEASING; + LG_UNLOCK(audio->lock); + bool valid = true; + if (!releasing) + { + 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 : + message.sequence != 0; + audio->sequenceKnown = true; + audio->expectedSequence = message.sequence + 1; + LG_UNLOCK(audio->lock); + valid = processInboundMessage( + audio, &message, payload, sequenceGap); + } + } + + status = lgmpClientStreamReadRelease( + audio->hostToClientStream, &buffer); + if (status != LGMP_OK) + { + if (status != LGMP_ERR_STREAM_UNBOUND && + status != LGMP_ERR_STREAM_STALE) + DEBUG_WARN("Failed to release LGMP audio record: %s", + lgmpStatusString(status)); + atomic_store_explicit( + &audio->dataPlaneLost, true, memory_order_release); + break; + } + if (!valid) + { + requestRelease(audio, "malformed host-to-client record"); + break; + } + } + return drained; +} + +static bool bindingGone(LGMPAudio * audio) +{ + bool any = false; + const PLGMPClientStream streams[2] = + { + audio->hostToClientStream, + audio->clientToHostStream, + }; + for (unsigned i = 0; i < 2; ++i) + { + if (!streams[i]) + continue; + any = true; + uint32_t clientID = 0; + uint32_t epoch = 0; + if (i == 1) + lockWriteGate(audio); + const LGMP_STATUS status = lgmpClientStreamGetBinding( + streams[i], &clientID, &epoch); + if (i == 1) + unlockWriteGate(audio); + 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 audio binding: %s", + lgmpStatusString(status)); + return false; + } + } + return any; +} + +static void maintainBinding(LGMPAudio * audio) +{ + const uint64_t now = microtime(); + LG_LOCK(audio->lock); + const enum AudioPhase phase = audio->phase; + const bool releaseRequested = audio->releaseRequested; + const bool releaseSent = audio->releaseSent; + const uint64_t deadline = audio->releaseDeadline; + const uint64_t nextAttachAt = audio->nextAttachAt; + const bool keepalive = phase == AUDIO_PHASE_BOUND && + !releaseRequested && now >= audio->nextKeepalive; + LG_UNLOCK(audio->lock); + + if ((phase == AUDIO_PHASE_CLAIM_PENDING || + phase == AUDIO_PHASE_CLAIM_SENT || + phase == AUDIO_PHASE_COOLDOWN) && + releaseRequested && now >= deadline) + { + LG_LOCK(audio->lock); + if ((audio->phase == AUDIO_PHASE_CLAIM_PENDING || + audio->phase == AUDIO_PHASE_CLAIM_SENT || + audio->phase == AUDIO_PHASE_COOLDOWN) && + audio->releaseRequested && + !audio->claimPosting) + { + audio->responsePending = false; + scheduleRetryNL(audio, AUDIO_CLAIM_RETRY_US); + } + LG_UNLOCK(audio->lock); + return; + } + + if (phase == AUDIO_PHASE_COOLDOWN && now >= deadline) + { + bool wakePointer = false; + LG_LOCK(audio->lock); + if (audio->phase == AUDIO_PHASE_COOLDOWN && + now >= audio->releaseDeadline && + !audio->releaseRequested && audio->discover) + { + audio->phase = audio->claimSends ? + AUDIO_PHASE_CLAIM_SENT : AUDIO_PHASE_CLAIM_PENDING; + audio->claimSends = 0; + audio->nextClaimAt = 0; + wakePointer = true; + } + else if (audio->phase == AUDIO_PHASE_COOLDOWN && + !audio->claimSends && !audio->claimPosting) + scheduleRetryNL(audio, AUDIO_CLAIM_RETRY_US); + LG_UNLOCK(audio->lock); + if (wakePointer && audio->pointerWake) + lgSignalEvent(audio->pointerWake); + return; + } + + if (phase != AUDIO_PHASE_BOUND && + phase != AUDIO_PHASE_RELEASING) + return; + + if (phase == AUDIO_PHASE_RELEASING && + (!audio->hostReadActive || !audio->clientWriteActive) && + now >= nextAttachAt) + { + LG_LOCK(audio->lock); + const KVMFRAudioStreamResponse response = audio->response; + audio->nextAttachAt = now + AUDIO_CLAIM_RETRY_US; + LG_UNLOCK(audio->lock); + attachStreams(audio, &response); + } + + if (phase == AUDIO_PHASE_RELEASING && !releaseSent && + audio->clientWriteActive) + { + lockWriteGate(audio); + const enum WriteResult result = writeMessageLocked(audio, + KVMFR_AUDIO_MESSAGE_RELEASE, 0, 0, 0, 0, 0, 0, NULL, 0); + unlockWriteGate(audio); + if (result == WRITE_OK) + { + LG_LOCK(audio->lock); + if (audio->phase == AUDIO_PHASE_RELEASING) + audio->releaseSent = true; + LG_UNLOCK(audio->lock); + } + } + else if (keepalive) + { + lockWriteGate(audio); + const enum WriteResult result = writeMessageLocked(audio, + KVMFR_AUDIO_MESSAGE_KEEPALIVE, 0, 0, 0, 0, 0, 0, NULL, 0); + unlockWriteGate(audio); + if (result == WRITE_OK) + { + LG_LOCK(audio->lock); + if (audio->phase == AUDIO_PHASE_BOUND) + audio->nextKeepalive = + now + (uint64_t)KVMFR_AUDIO_KEEPALIVE_MS * 1000U; + LG_UNLOCK(audio->lock); + } + } + + if (phase == AUDIO_PHASE_RELEASING && bindingGone(audio)) + finishBinding(audio, AUDIO_CLAIM_RETRY_US); +} + +static void stopWorkerBinding(LGMPAudio * audio) +{ + LG_LOCK(audio->lock); + const bool active = audio->phase == AUDIO_PHASE_BOUND || + audio->phase == AUDIO_PHASE_RELEASING; + audio->phase = active ? AUDIO_PHASE_RELEASING : audio->phase; + LG_UNLOCK(audio->lock); + if (!active) + { + stopActiveStreams(audio); + detachStreams(audio); + return; + } + + for (;;) + { + drainInbound(audio); + maintainBinding(audio); + if (bindingGone(audio) || !lgmpClientSessionValid(audio->client)) + break; + lgWaitEvent(audio->event, 1); + } + stopActiveStreams(audio); + detachStreams(audio); +} + +static int audioThread(void * opaque) +{ + LGMPAudio * audio = opaque; + const struct LGMPStreamPollConfig pollConfig = + { + .spinCount = 32, + .minWaitUs = 100, + .maxWaitUs = AUDIO_ACTIVE_POLL_MS * 1000U, + }; + if (lgmpStreamPollInit(&audio->streamPoll, pollConfig) != LGMP_OK) + return -1; + + while (!atomic_load_explicit(&audio->stop, memory_order_acquire)) + { + processResponse(audio); + const unsigned drained = drainInbound(audio); + maintainBinding(audio); + if (atomic_exchange_explicit( + &audio->dataPlaneLost, false, memory_order_acq_rel)) + { + requestRelease(audio, "audio stream operation failed"); + if (bindingGone(audio)) + finishBinding(audio, AUDIO_CLAIM_RETRY_US); + } + publishStatus(audio); + + if (atomic_load_explicit(&audio->stop, memory_order_acquire)) + break; + LG_LOCK(audio->lock); + const bool active = audio->phase == AUDIO_PHASE_BOUND || + audio->phase == AUDIO_PHASE_RELEASING; + LG_UNLOCK(audio->lock); + if (active) + { + if (drained == AUDIO_DRAIN_MAX) + { + lgmpStreamPollActivity(&audio->streamPoll); + continue; + } + const uint32_t waitUs = lgmpStreamPollIdle(&audio->streamPoll); + if (!waitUs) + continue; + if (lgWaitEventNS(audio->event, waitUs * 1000U)) + lgmpStreamPollActivity(&audio->streamPoll); + } + else + lgWaitEvent(audio->event, AUDIO_IDLE_POLL_MS); + } + + stopWorkerBinding(audio); + publishStatus(audio); + return 0; +} + +bool lgmpAudio_create(PLGMPClient client, LGEvent * pointerWake, + LGMPAudio ** result) +{ + if (!client || !pointerWake || !result) + return false; + LGMPAudio * audio = calloc(1, sizeof(*audio)); + if (!audio) + return false; + audio->client = client; + audio->pointerWake = pointerWake; + audio->event = lgCreateEvent(true, 0); + if (!audio->event) + { + free(audio); + return false; + } + audio->generationSerial = 1; + if (!randomBytes(&audio->requestSerial, + sizeof(audio->requestSerial))) + { + lgFreeEvent(audio->event); + free(audio); + return false; + } + if (!audio->requestSerial) + audio->requestSerial = 1; + LG_LOCK_INIT(audio->lock); + LG_LOCK_INIT(audio->eventLock); + LG_LOCK_INIT(audio->statusWait.lock); + atomic_flag_clear(&audio->writeGate); + atomic_init(&audio->stop, false); + atomic_init(&audio->dataPlaneLost, false); + atomic_init(&audio->captureDiscontinuity, false); + atomic_init(&audio->statusInFlight, 0); + atomic_init(&audio->statusWait.count, 0); + *result = audio; + return true; +} + +void lgmpAudio_destroy(LGMPAudio ** audio) +{ + if (!audio || !*audio) + return; + lgmpAudio_disconnect(*audio); + lgFreeEvent((*audio)->event); + LG_LOCK_FREE((*audio)->statusWait.lock); + LG_LOCK_FREE((*audio)->eventLock); + LG_LOCK_FREE((*audio)->lock); + free(*audio); + *audio = NULL; +} + +bool lgmpAudio_connect(LGMPAudio * audio, uint32_t clientID) +{ + if (!audio || !clientID) + return false; + LG_LOCK(audio->lock); + if (audio->connected) + { + const bool same = audio->clientID == clientID; + LG_UNLOCK(audio->lock); + return same; + } + if (audio->thread) + { + LG_UNLOCK(audio->lock); + return false; + } + lgResetEvent(audio->event); + audio->connected = true; + audio->clientID = clientID; + audio->phase = AUDIO_PHASE_IDLE; + audio->statusAvailable = false; + audio->statusDirty = true; + audio->nextClaimAt = 0; + audio->releaseRequested = false; + audio->releaseSent = false; + memset(&audio->endpointGeneration, 0, + sizeof(audio->endpointGeneration)); + atomic_store_explicit(&audio->stop, false, memory_order_release); + atomic_store_explicit( + &audio->dataPlaneLost, false, memory_order_release); + + LGThread * thread; + if (lgCreateThread("lgmpAudio", audioThread, audio, &thread)) + { + audio->thread = thread; + LG_UNLOCK(audio->lock); + return true; + } + + audio->connected = false; + LG_UNLOCK(audio->lock); + return false; +} + +void lgmpAudio_disconnect(LGMPAudio * audio) +{ + if (!audio) + return; + + LG_LOCK(audio->eventLock); + lockWriteGate(audio); + LG_LOCK(audio->lock); + closePublicDataLocked(audio); + audio->events = NULL; + audio->eventOpaque = NULL; + audio->attached = false; + audio->discover = false; + audio->releaseRequested = true; + audio->releaseDeadline = + microtime() + AUDIO_OWNER_RETRY_US; + if (audio->phase == AUDIO_PHASE_BOUND) + audio->phase = AUDIO_PHASE_RELEASING; + markStatusNL(audio, false, 0); + audio->connected = false; + LGThread * thread = audio->thread; + LGEvent * event = audio->event; + atomic_store_explicit(&audio->stop, true, memory_order_release); + LG_UNLOCK(audio->lock); + unlockWriteGate(audio); + LG_UNLOCK(audio->eventLock); + + if (event) + lgSignalEvent(event); + if (thread) + lgJoinThread(thread, NULL); + + LG_LOCK(audio->lock); + audio->thread = NULL; + audio->clientID = 0; + audio->phase = AUDIO_PHASE_IDLE; + audio->responsePending = false; + audio->claimPosting = false; + audio->requestGeneration = 0; + audio->claimToken = 0; + audio->claimSends = 0; + audio->releaseRequested = false; + audio->releaseSent = 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)); + LG_UNLOCK(audio->lock); + waitStatusCallbacks(audio); +} + +static bool newClaimNL(LGMPAudio * audio) +{ + uint64_t token = 0; + if (!randomBytes(&token, sizeof(token))) + { + token = microtime() ^ ((uint64_t)audio->clientID << 32) ^ + audio->requestSerial; + } + if (!token) + token = 1; + + audio->requestGeneration = nextNonzero(&audio->requestSerial); + audio->claimToken = token; + audio->claim = (KVMFRAudioClaim) + { + .messageType = KVMFR_MESSAGE_AUDIO_CLAIM, + .version = KVMFR_AUDIO_CONTROL_VERSION, + .size = sizeof(KVMFRAudioClaim), + .requestGeneration = audio->requestGeneration, + .flags = KVMFR_AUDIO_CLAIM_BIND, + .claimToken = audio->claimToken, + .protocolMin = KVMFR_AUDIO_PROTOCOL_MIN, + .protocolMax = KVMFR_AUDIO_PROTOCOL_MAX, + .clientCapabilities = AUDIO_CAPABILITIES, + }; + audio->claimSends = 0; + audio->claimPosting = false; + audio->phase = AUDIO_PHASE_CLAIM_PENDING; + audio->nextClaimAt = 0; + return true; +} + +void lgmpAudio_pumpClaim(LGMPAudio * audio, + PLGMPClientQueue pointerQueue) +{ + if (!audio || !pointerQueue) + return; + + KVMFRAudioClaim claim; + bool send = false; + const uint64_t now = microtime(); + LG_LOCK(audio->lock); + if (audio->connected && audio->discover && + audio->phase == AUDIO_PHASE_IDLE && + now >= audio->nextClaimAt) + newClaimNL(audio); + + if (audio->connected && !audio->releaseRequested && + (audio->phase == AUDIO_PHASE_CLAIM_PENDING || + audio->phase == AUDIO_PHASE_CLAIM_SENT) && + now >= audio->nextClaimAt) + { + if (audio->claimSends >= 3) + { + audio->phase = AUDIO_PHASE_COOLDOWN; + audio->releaseDeadline = now + AUDIO_OWNER_RETRY_US; + } + else + { + claim = audio->claim; + send = true; + audio->claimPosting = true; + } + } + LG_UNLOCK(audio->lock); + if (!send) + return; + + uint32_t serial = 0; + const LGMP_STATUS status = lgmpClientSendData(pointerQueue, + &claim, sizeof(claim), &serial); + LG_LOCK(audio->lock); + if (claim.requestGeneration == audio->requestGeneration && + claim.claimToken == audio->claimToken) + { + audio->claimPosting = false; + if (audio->connected) + { + if (status == LGMP_OK) + { + audio->phase = AUDIO_PHASE_CLAIM_SENT; + ++audio->claimSends; + audio->nextClaimAt = now + AUDIO_CLAIM_RETRY_US; + } + else if (status == LGMP_ERR_QUEUE_FULL || + status == LGMP_ERR_QUEUE_BUSY) + audio->nextClaimAt = now + 1000U; + else + { + audio->nextClaimAt = now + AUDIO_CLAIM_RETRY_US; + DEBUG_WARN("Failed to send LGMP audio claim: %s", + lgmpStatusString(status)); + } + } + } + LG_UNLOCK(audio->lock); +} + +void lgmpAudio_handleResponse(LGMPAudio * audio, + const KVMFRAudioStreamResponse * response) +{ + if (!audio || !response) + return; + LG_LOCK(audio->lock); + if (audio->connected && + response->requestGeneration == audio->requestGeneration && + response->claimToken == audio->claimToken && + response->boundClientID == audio->clientID && + (audio->phase == AUDIO_PHASE_CLAIM_PENDING || + audio->phase == AUDIO_PHASE_CLAIM_SENT || + audio->phase == AUDIO_PHASE_COOLDOWN) && + !audio->responsePending) + { + audio->response = *response; + audio->responsePending = true; + } + LG_UNLOCK(audio->lock); + signalWorker(audio); +} + +static void setStatusListener(void * opaque, + LG_AudioStatusFn callback, void * callbackOpaque) +{ + LGMPAudio * audio = opaque; + if (!callback) + lockWriteGate(audio); + LG_LOCK(audio->lock); + if (!callback) + closePublicDataLocked(audio); + audio->statusCallback = callback; + audio->statusOpaque = callbackOpaque; + audio->discover = callback && audio->connected; + if (audio->discover && audio->phase == AUDIO_PHASE_IDLE) + { + audio->releaseRequested = false; + audio->nextClaimAt = 0; + } + if (!callback) + { + const bool ownsOrMayOwn = + audio->phase != AUDIO_PHASE_IDLE; + audio->releaseRequested = ownsOrMayOwn; + if (ownsOrMayOwn) + { + audio->releaseDeadline = + microtime() + AUDIO_OWNER_RETRY_US; + if (audio->phase == AUDIO_PHASE_BOUND) + audio->phase = AUDIO_PHASE_RELEASING; + } + markStatusNL(audio, false, 0); + } + const LG_AudioStatus status = + { + .available = audio->connected && audio->statusAvailable, + .generation = audio->endpointGeneration.local, + }; + struct AudioStatusCallbackFrame frame; + if (callback) + beginStatusCallback(audio, &frame); + audio->statusDirty = false; + LG_UNLOCK(audio->lock); + if (!callback) + unlockWriteGate(audio); + if (callback) + { + callback(callbackOpaque, &status); + endStatusCallback(audio, &frame); + } + else + waitStatusCallbacks(audio); + if (callback && audio->pointerWake) + lgSignalEvent(audio->pointerWake); + signalWorker(audio); +} + +static bool attach(void * opaque, const LG_AudioEventOps * events, + void * eventOpaque) +{ + LGMPAudio * audio = opaque; + if (!events) + return false; + + struct AudioStreamState playback; + struct AudioStreamState capture; + bool admitted; + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + admitted = audio->connected && audio->statusAvailable && + audio->phase == AUDIO_PHASE_BOUND && !audio->releaseRequested && + !audio->attached; + if (admitted) + { + audio->events = events; + audio->eventOpaque = eventOpaque; + audio->attached = true; + } + playback = audio->playback; + capture = audio->capture; + LG_UNLOCK(audio->lock); + + if (admitted && playback.active && events->playbackStart) + { + events->playbackStart(eventOpaque, playback.generation.local, + &playback.format, + playback.clockValid ? &playback.clock : NULL); + if (playback.volumeValid && events->playbackVolume) + events->playbackVolume(eventOpaque, playback.generation.local, + playback.volumeChannels, playback.volume); + if (playback.muteValid && events->playbackMute) + events->playbackMute(eventOpaque, playback.generation.local, + playback.mute); + } + if (admitted && capture.active && events->recordStart) + { + events->recordStart(eventOpaque, capture.generation.local, + &capture.format); + if (capture.volumeValid && events->recordVolume) + events->recordVolume(eventOpaque, capture.generation.local, + capture.volumeChannels, capture.volume); + if (capture.muteValid && events->recordMute) + events->recordMute(eventOpaque, capture.generation.local, + capture.mute); + } + LG_UNLOCK(audio->eventLock); + if (admitted) + signalWorker(audio); + return admitted; +} + +static void detach(void * opaque) +{ + LGMPAudio * audio = opaque; + LG_LOCK(audio->eventLock); + LG_LOCK(audio->lock); + audio->attached = false; + audio->events = NULL; + audio->eventOpaque = NULL; + LG_UNLOCK(audio->lock); + LG_UNLOCK(audio->eventLock); +} + +static bool rateToQ32(double rate, bool allowZero, uint64_t * result) +{ + if (!isfinite(rate) || rate < 0.0 || (!allowZero && rate == 0.0) || + rate > (double)KVMFR_AUDIO_MAX_SAMPLE_RATE * 2.0) + return false; + const long double scaled = (long double)rate * 4294967296.0L; + if (scaled > (long double)UINT64_MAX) + return false; + *result = (uint64_t)(scaled + 0.5L); + return allowZero || *result != 0; +} + +static bool recordData(void * opaque, uint32_t generation, + const void * data, size_t frames, const LG_AudioClock * sourceClock) +{ + LGMPAudio * audio = opaque; + if (!generation || !data || !frames || + frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET) + return false; + if (!tryWriteGate(audio)) + { + atomic_store_explicit( + &audio->captureDiscontinuity, true, memory_order_release); + return false; + } + + if (!audio->publicDataAllowed || !audio->writeBound || + generation != audio->writeCaptureLocalGeneration || + !audio->writeCaptureGeneration) + { + unlockWriteGate(audio); + return false; + } + const size_t bytes = frames * audio->writeCaptureFormat.blockAlign; + if (!audio->writeCaptureFormat.blockAlign || + bytes > KVMFR_AUDIO_PCM_BYTES) + { + unlockWriteGate(audio); + return false; + } + + uint32_t flags = 0; + uint64_t position = 0; + int64_t timeNs = 0; + uint64_t rateQ32 = 0; + if (sourceClock) + { + if (!rateToQ32(sourceClock->rate, true, &rateQ32)) + { + unlockWriteGate(audio); + return false; + } + flags |= KVMFR_AUDIO_MESSAGE_CLOCK_VALID; + position = sourceClock->position; + timeNs = sourceClock->time; + if (sourceClock->stable) + flags |= KVMFR_AUDIO_MESSAGE_CLOCK_STABLE; + if (sourceClock->discontinuity) + flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY; + } + if (atomic_exchange_explicit( + &audio->captureDiscontinuity, false, memory_order_acq_rel)) + flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY; + + const enum WriteResult result = writeMessageLocked(audio, + KVMFR_AUDIO_MESSAGE_CAPTURE_DATA, + audio->writeCaptureGeneration, flags, position, timeNs, rateQ32, + (uint32_t)frames, data, (uint32_t)bytes); + if (result != WRITE_OK) + { + if (result == WRITE_FULL) + ++audio->clientSequence; + atomic_store_explicit( + &audio->captureDiscontinuity, true, memory_order_release); + } + unlockWriteGate(audio); + return result == WRITE_OK; +} + +static bool clockFeedback(void * opaque, uint32_t generation, + const LG_AudioClock * playbackClock, double targetRate) +{ + LGMPAudio * audio = opaque; + uint64_t targetRateQ32; + uint64_t rateQ32; + if (!generation || !playbackClock || + !rateToQ32(targetRate, false, &targetRateQ32) || + !rateToQ32(playbackClock->rate, true, &rateQ32)) + return false; + + if (!tryWriteGate(audio)) + { + atomic_store_explicit( + &audio->captureDiscontinuity, true, memory_order_release); + return false; + } + LG_LOCK(audio->lock); + const bool supported = audio->phase == AUDIO_PHASE_BOUND && + !audio->releaseRequested && audio->statusAvailable && + (audio->remoteStatus.capabilities & + KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) != 0 && + audio->playback.active && + audio->playback.generation.local == generation; + LG_UNLOCK(audio->lock); + if (!supported) + { + unlockWriteGate(audio); + return false; + } + if (!audio->publicDataAllowed || !audio->writeBound || + generation != audio->writePlaybackLocalGeneration || + !audio->writePlaybackGeneration) + { + unlockWriteGate(audio); + return false; + } + + uint32_t flags = KVMFR_AUDIO_MESSAGE_CLOCK_VALID; + if (playbackClock->stable) + flags |= KVMFR_AUDIO_MESSAGE_CLOCK_STABLE; + if (playbackClock->discontinuity) + flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY; + const KVMFRAudioClockFeedback feedback = + { + .targetRateQ32 = targetRateQ32, + }; + const enum WriteResult result = writeMessageLocked(audio, + KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK, + audio->writePlaybackGeneration, flags, + playbackClock->position, playbackClock->time, rateQ32, 0, + &feedback, sizeof(feedback)); + if (result != WRITE_OK) + { + if (result == WRITE_FULL) + ++audio->clientSequence; + atomic_store_explicit( + &audio->captureDiscontinuity, true, memory_order_release); + } + unlockWriteGate(audio); + return result == WRITE_OK; +} + +static const LG_AudioOps l_audioOps = +{ + .name = "LGMP", + .setStatusListener = setStatusListener, + .attach = attach, + .detach = detach, + .recordData = recordData, + .clockFeedback = clockFeedback, +}; + +const LG_AudioOps * lgmpAudio_getOps(void) +{ + return &l_audioOps; +} diff --git a/client/transports/LGMP/audio.h b/client/transports/LGMP/audio.h new file mode 100644 index 00000000..ed6de3c2 --- /dev/null +++ b/client/transports/LGMP/audio.h @@ -0,0 +1,53 @@ +/** + * 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_CLIENT_TRANSPORT_LGMP_AUDIO_ +#define _H_LG_CLIENT_TRANSPORT_LGMP_AUDIO_ + +#include "interface/audio.h" + +#include "common/KVMFRAudio.h" +#include "common/event.h" + +#include + +#include +#include + +typedef struct LGMPAudio LGMPAudio; + +bool lgmpAudio_create(PLGMPClient client, LGEvent * pointerWake, + LGMPAudio ** result); +void lgmpAudio_destroy(LGMPAudio ** audio); + +bool lgmpAudio_connect(LGMPAudio * audio, uint32_t clientID); +void lgmpAudio_disconnect(LGMPAudio * audio); + +/* Called only while the transport owns the existing pointer subscription. */ +void lgmpAudio_pumpClaim(LGMPAudio * audio, + PLGMPClientQueue pointerQueue); + +/* The caller must copy the response and release its pointer message first. */ +void lgmpAudio_handleResponse(LGMPAudio * audio, + const KVMFRAudioStreamResponse * response); + +const LG_AudioOps * lgmpAudio_getOps(void); + +#endif diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 82b9e450..81cc1331 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -22,6 +22,9 @@ #include "clipboard.h" #include "input.h" +#ifdef ENABLE_AUDIO +#include "audio.h" +#endif #include #include @@ -120,6 +123,9 @@ struct LG_Transport PLGMPClientQueue pointerQueue; LGMPInput * input; LGMPClipboard * clipboard; +#ifdef ENABLE_AUDIO + LGMPAudio * audio; +#endif LG_Lock frameLock; LG_Lock pointerLock; LG_RWLock videoStatusLock; @@ -145,6 +151,9 @@ struct LG_Transport bool frameScheduleSupported; atomic_bool inputSupported; atomic_bool clipboardSupported; +#ifdef ENABLE_AUDIO + atomic_bool audioSupported; +#endif uint64_t frameLeaseHandle; uint32_t frameGeneration; uint32_t clientID; @@ -671,6 +680,9 @@ static bool lgmp_create(LG_Transport ** result) atomic_init(&this->connected, false); atomic_init(&this->inputSupported, false); atomic_init(&this->clipboardSupported, false); +#ifdef ENABLE_AUDIO + atomic_init(&this->audioSupported, false); +#endif atomic_init(&this->destroyPending, false); this->frameWake = lgCreateEvent(true, 0); @@ -884,6 +896,10 @@ static void lgmp_destroyNow(LG_Transport * this) { if (this->client) { + lgmp_stopPointer(this); +#ifdef ENABLE_AUDIO + lgmpAudio_destroy(&this->audio); +#endif lgmpClipboard_destroy(&this->clipboard); lgmpInput_destroy(&this->input); lgmp_closeQueues(this); @@ -986,6 +1002,9 @@ static bool lgmp_parseSession(struct LG_Transport * this, session->features |= LG_TRANSPORT_FEATURE_INPUT; if (header.features & KVMFR_FEATURE_CLIPBOARD) session->features |= LG_TRANSPORT_FEATURE_CLIPBOARD; + if (header.features & KVMFR_FEATURE_AUDIO) + session->features |= LG_TRANSPORT_FEATURE_AUDIO; + data += sizeof(header); size -= sizeof(header); while (size >= sizeof(KVMFRRecord)) @@ -1095,6 +1114,18 @@ static LG_TransportStatus lgmp_connectInternal(LG_Transport * this, if (!lgmp_parseSession(this, data, size, session)) return LG_TRANSPORT_INVALID_VERSION; +#ifdef ENABLE_AUDIO + const bool audioSupported = + (session->features & LG_TRANSPORT_FEATURE_AUDIO) != 0; + if (audioSupported && !this->audio && + !lgmpAudio_create(this->client, this->pointerWake, + &this->audio)) + DEBUG_WARN("Failed to create the optional LGMP audio transport"); + if (audioSupported && this->audio && + !lgmpAudio_connect(this->audio, this->clientID)) + DEBUG_WARN("Failed to start the optional LGMP audio transport"); +#endif + LGMPVideoStatusDispatch dispatch; LG_LOCK(this->frameLock); if (++this->frameGeneration == 0) @@ -1108,6 +1139,10 @@ static LG_TransportStatus lgmp_connectInternal(LG_Transport * this, atomic_store_explicit(&this->clipboardSupported, session->features & LG_TRANSPORT_FEATURE_CLIPBOARD, memory_order_release); +#ifdef ENABLE_AUDIO + atomic_store_explicit(&this->audioSupported, audioSupported, + memory_order_release); +#endif this->frameSerial = 0; this->frameSerialValid = false; this->formatValid = false; @@ -1169,6 +1204,10 @@ static void lgmp_disconnect(LG_Transport * this) lgmpInput_disconnect(this->input); lgmpClipboard_disconnect(this->clipboard); + lgmp_stopPointer(this); +#ifdef ENABLE_AUDIO + lgmpAudio_disconnect(this->audio); +#endif LG_LOCK(this->frameLock); if (++this->frameGeneration == 0) @@ -1180,6 +1219,10 @@ static void lgmp_disconnect(LG_Transport * this) &this->inputSupported, false, memory_order_release); atomic_store_explicit( &this->clipboardSupported, false, memory_order_release); +#ifdef ENABLE_AUDIO + atomic_store_explicit( + &this->audioSupported, false, memory_order_release); +#endif this->clientID = 0; LG_UNLOCK(this->frameLock); @@ -2346,6 +2389,10 @@ static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, &this->pointerQueue); } pointerQueue = this->pointerQueue; +#ifdef ENABLE_AUDIO + if (status == LG_TRANSPORT_OK && pointerQueue && this->audio) + lgmpAudio_pumpClaim(this->audio, pointerQueue); +#endif LG_UNLOCK(this->pointerLock); if (status == LG_TRANSPORT_TIMEOUT) lgmp_waitPoll(this->pointerWake, 1000); @@ -2353,10 +2400,56 @@ static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, { LGMPMessage message; PLGMPClientQueue processedQueue = pointerQueue; - status = lgmp_process(&processedQueue, this->cursorPollInterval, - this->pointerWake, &message); - if (!processedQueue) - lgmp_clearPointerQueue(this, pointerQueue); + const uint64_t deadline = this->cursorPollInterval ? + microtime() + this->cursorPollInterval : 0; + unsigned interval = this->cursorPollInterval; + for (unsigned skipped = 0;; ++skipped) + { + status = lgmp_process(&processedQueue, interval, + this->pointerWake, &message); + if (!processedQueue) + lgmp_clearPointerQueue(this, pointerQueue); + if (status != LG_TRANSPORT_OK) + break; + + const uint32_t tag = (uint32_t)(message.udata >> 32); + if (!tag) + break; + +#ifdef ENABLE_AUDIO + KVMFRAudioStreamResponse audioResponse; + const bool audioResponseValid = + tag == KVMFR_AUDIO_CONTROL_TAG && this->audio && + message.size == sizeof(audioResponse); + if (audioResponseValid) + memcpy(&audioResponse, message.mem, sizeof(audioResponse)); +#endif + status = lgmp_donePointerMessage(this, pointerQueue); + if (status != LG_TRANSPORT_OK) + break; +#ifdef ENABLE_AUDIO + if (audioResponseValid) + lgmpAudio_handleResponse(this->audio, &audioResponse); +#endif + + if (skipped + 1 >= LGMP_Q_POINTER_LEN) + { + status = LG_TRANSPORT_TIMEOUT; + break; + } + if (!deadline) + { + interval = 0; + continue; + } + const uint64_t now = microtime(); + if (now >= deadline) + { + status = LG_TRANSPORT_TIMEOUT; + break; + } + interval = (unsigned)(deadline - now); + } if (status == LG_TRANSPORT_OK) { if (!message.mem || message.size < sizeof(KVMFRCursor) || @@ -2623,6 +2716,26 @@ static const LG_ClipboardOps * lgmp_getClipboardOps( return lgmpClipboard_getOps(); } +#ifdef ENABLE_AUDIO +static const LG_AudioOps * lgmp_getAudioOps( + LG_Transport * this, void ** opaque) +{ + *opaque = NULL; + if (!atomic_load_explicit(&this->connected, memory_order_acquire) || + !atomic_load_explicit( + &this->audioSupported, memory_order_acquire)) + return NULL; + + if (!this->audio && + !lgmpAudio_create(this->client, this->pointerWake, &this->audio)) + return NULL; + if (!lgmpAudio_connect(this->audio, this->clientID)) + return NULL; + *opaque = this->audio; + return lgmpAudio_getOps(); +} +#endif + static void lgmp_probeVideoStatus(LG_Transport * this) { bool frameAvailable = false; @@ -2766,6 +2879,9 @@ const LG_TransportOps LGT_LGMP = .sessionValid = lgmp_sessionValid, .getVideoOps = lgmp_getVideoOps, .getInputOps = lgmp_getInputOps, +#ifdef ENABLE_AUDIO + .getAudioOps = lgmp_getAudioOps, +#endif .getClipboardOps = lgmp_getClipboardOps, .getRecoveryInfo = lgmp_getRecoveryInfo, .requestRecovery = lgmp_requestRecovery,