mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-11-13 07:17:28 +00:00
Scope the legacy latency offset to classic SPICE and use the LGMP poll bound as its fixed arrival reserve. Keep learned jitter additive. Select provider clock feedback only when the current remote endpoint advertises it, avoiding a failed startup attempt on broker v1.
1236 lines
41 KiB
C
1236 lines
41 KiB
C
/**
|
|
* 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 <LGProtocol/KVMFR.h>
|
|
#include <LGProtocol/LGMPConfig.h>
|
|
#include "common/debug.h"
|
|
#include "common/event.h"
|
|
#include "common/time.h"
|
|
|
|
#include <lgmp/client.h>
|
|
#include <lgmp/host.h>
|
|
#include <lgmp/stream.h>
|
|
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#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);
|
|
CHECK(fixture->ops->playbackFlags == 0);
|
|
CHECK(fixture->ops->playbackArrivalReserveUs == 250);
|
|
CHECK(fixture->ops->clockFeedbackAvailable);
|
|
CHECK(!fixture->ops->clockFeedbackAvailable(fixture->audio));
|
|
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));
|
|
CHECK(!fixture.ops->clockFeedbackAvailable(fixture.audio));
|
|
|
|
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->clockFeedbackAvailable(fixture.audio));
|
|
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;
|
|
}
|