[client] audio: harden low-latency streaming

Rework audio provider and backend lifecycles so playback and capture
callbacks quiesce without blocking real-time threads. Move activation,
teardown, controls, retries, and diagnostics onto bounded workers.

Harden USB audio cadence, feedback, and capture recovery.
Preserve source clocks through recording and pace packets from the
device clock. Bound queues, waits, conversion buffers, and packet sizes.

Make PipeWire and PulseAudio stream control thread-safe and recoverable.
Correct latency clock domains, coalesce rate updates, preserve recent
capture under overload, and keep logging outside real-time callbacks.
This commit is contained in:
Geoffrey McRae
2026-08-10 16:36:12 +10:00
parent 4434985aa3
commit 87aa61510c
13 changed files with 4795 additions and 1217 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
#include "audio_spice.h"
#include "common/debug.h"
#include "common/event.h"
#include "common/locking.h"
#include <stdatomic.h>
@@ -50,6 +51,22 @@ typedef struct SpiceAudioEventTarget
}
SpiceAudioEventTarget;
typedef struct SpiceAudioCallbackWaiter
{
struct SpiceAudioCallbackWaiter * next;
LGEvent * event;
unsigned int depth;
}
SpiceAudioCallbackWaiter;
typedef struct SpiceAudioCallbackWaitQueue
{
LG_Lock lock;
atomic_uint count;
SpiceAudioCallbackWaiter * waiters;
}
SpiceAudioCallbackWaitQueue;
static struct
{
LG_RWLock lock;
@@ -59,11 +76,13 @@ static struct
LG_AudioStatusFn statusCallback;
void * statusOpaque;
atomic_uint statusInFlight;
SpiceAudioCallbackWaitQueue statusWait;
const LG_AudioEventOps * events;
void * eventOpaque;
uint32_t eventGeneration;
atomic_uint inFlight;
SpiceAudioCallbackWaitQueue eventWait;
SpiceAudioStream playback;
SpiceAudioStream record;
@@ -83,7 +102,17 @@ l_spice =
.writer = ATOMIC_FLAG_INIT,
},
.statusInFlight = ATOMIC_VAR_INIT(0),
.statusWait =
{
.lock = ATOMIC_FLAG_INIT,
.count = ATOMIC_VAR_INIT(0),
},
.inFlight = ATOMIC_VAR_INIT(0),
.eventWait =
{
.lock = ATOMIC_FLAG_INIT,
.count = ATOMIC_VAR_INIT(0),
},
};
static _Thread_local unsigned int l_eventDepth;
@@ -96,6 +125,96 @@ static uint32_t nextGeneration(uint32_t generation)
return generation;
}
static LGEvent * createWaitEvent(void)
{
LGEvent * event = lgCreateEvent(true, 0);
if (!event)
DEBUG_FATAL("Failed to create SPICE audio wait event");
return event;
}
static void waitEvent(LGEvent * event)
{
if (!lgWaitEvent(event, TIMEOUT_INFINITE))
DEBUG_FATAL("Failed to wait for SPICE audio event");
}
static void signalWaitEvent(LGEvent * event)
{
if (!lgSignalEvent(event))
DEBUG_FATAL("Failed to signal SPICE audio event");
}
static void signalCallbackWaiters(
SpiceAudioCallbackWaitQueue * queue, unsigned int remaining)
{
if (!atomic_load_explicit(&queue->count, memory_order_acquire))
return;
LG_LOCK(queue->lock);
SpiceAudioCallbackWaiter ** link = &queue->waiters;
while (*link)
{
SpiceAudioCallbackWaiter * waiter = *link;
if (remaining > waiter->depth)
{
link = &waiter->next;
continue;
}
*link = waiter->next;
atomic_fetch_sub_explicit(
&queue->count, 1, memory_order_release);
signalWaitEvent(waiter->event);
}
LG_UNLOCK(queue->lock);
}
static void endCallback(atomic_uint * inFlight,
SpiceAudioCallbackWaitQueue * waitQueue)
{
const unsigned int previous = atomic_fetch_sub_explicit(
inFlight, 1, memory_order_release);
DEBUG_ASSERT(previous > 0);
signalCallbackWaiters(waitQueue, previous - 1);
}
static void waitCallbacks(atomic_uint * inFlight,
SpiceAudioCallbackWaitQueue * waitQueue, unsigned int depth)
{
if (atomic_load_explicit(inFlight, memory_order_acquire) <= depth)
return;
SpiceAudioCallbackWaiter waiter =
{
.event = createWaitEvent(),
.depth = depth,
};
bool queued = false;
LG_LOCK(waitQueue->lock);
if (atomic_load_explicit(inFlight, memory_order_acquire) > depth)
{
waiter.next = waitQueue->waiters;
waitQueue->waiters = &waiter;
atomic_fetch_add_explicit(
&waitQueue->count, 1, memory_order_release);
queued = true;
}
LG_UNLOCK(waitQueue->lock);
if (queued)
{
waitEvent(waiter.event);
/* The signaler owns the waiter until it releases the queue lock. */
LG_LOCK(waitQueue->lock);
LG_UNLOCK(waitQueue->lock);
}
lgFreeEvent(waiter.event);
}
/* l_spice.lock must be held exclusively while admitting a callback so detach
* cannot invalidate the target between the snapshot and in-flight increment. */
static bool beginEventNL(SpiceAudioEventTarget * target)
@@ -142,7 +261,7 @@ static bool recordEventCurrent(const SpiceAudioEventTarget * target,
static void endEvent(void)
{
--l_eventDepth;
atomic_fetch_sub_explicit(&l_spice.inFlight, 1, memory_order_release);
endCallback(&l_spice.inFlight, &l_spice.eventWait);
}
static bool sampleFormat(PSAudioFormat source,
@@ -318,13 +437,11 @@ static void spiceSetStatusListener(void * opaque,
{
callback(callbackOpaque, &status);
--l_statusDepth;
atomic_fetch_sub_explicit(
&l_spice.statusInFlight, 1, memory_order_release);
endCallback(&l_spice.statusInFlight, &l_spice.statusWait);
}
else
while (atomic_load_explicit(
&l_spice.statusInFlight, memory_order_acquire) > l_statusDepth)
;
waitCallbacks(&l_spice.statusInFlight,
&l_spice.statusWait, l_statusDepth);
}
static bool spiceAttach(void * opaque, const LG_AudioEventOps * events,
@@ -413,9 +530,7 @@ static void spiceDetach(void * opaque)
nextGeneration(l_spice.eventGeneration);
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
while (atomic_load_explicit(
&l_spice.inFlight, memory_order_acquire) > l_eventDepth)
;
waitCallbacks(&l_spice.inFlight, &l_spice.eventWait, l_eventDepth);
}
static bool spiceRecordData(void * opaque, uint32_t generation,
@@ -501,8 +616,7 @@ void lgaSpice_setAvailable(bool available)
{
callback(callbackOpaque, &status);
--l_statusDepth;
atomic_fetch_sub_explicit(
&l_spice.statusInFlight, 1, memory_order_release);
endCallback(&l_spice.statusInFlight, &l_spice.statusWait);
}
}

View File

@@ -22,11 +22,15 @@
#include "usb_audio.h"
#include "common/debug.h"
#include "common/event.h"
#include "common/locking.h"
#include "common/time.h"
#include <stdalign.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#define USB_AUDIO_NS_PER_SECOND INT64_C(1000000000)
@@ -37,49 +41,94 @@ typedef struct USBAudioCallbackFrame
}
USBAudioCallbackFrame;
typedef struct USBAudioCallbackWaiter
{
struct USBAudioCallbackWaiter * next;
LGEvent * event;
unsigned int depth;
}
USBAudioCallbackWaiter;
typedef struct USBAudioCallbackWaitQueue
{
LG_Lock lock;
atomic_uint count;
USBAudioCallbackWaiter * waiters;
}
USBAudioCallbackWaitQueue;
typedef struct USBAudioCallbackGate
{
atomic_uint inFlight;
USBAudioCallbackWaitQueue wait;
}
USBAudioCallbackGate;
typedef struct USBAudioEventTarget
{
const LG_AudioEventOps * events;
void * opaque;
uint32_t attachmentGeneration;
USBAudioCallbackFrame frame;
const LG_AudioEventOps * events;
void * opaque;
uint32_t attachmentGeneration;
USBAudioCallbackFrame frame;
USBAudioCallbackFrame ** frames;
USBAudioCallbackGate * gate;
}
USBAudioEventTarget;
typedef struct USBAudioOperationWaiter
{
struct USBAudioOperationWaiter * next;
LGEvent * event;
}
USBAudioOperationWaiter;
typedef struct USBAudioOperationQueue
{
LG_Lock lock;
bool active;
USBAudioOperationWaiter * head;
USBAudioOperationWaiter * tail;
}
USBAudioOperationQueue;
struct LGA_USBState
{
LG_USBAudio * device;
LG_USBRedir * redir;
LG_Lock statusLock;
atomic_bool available;
uint32_t statusGeneration;
LG_AudioStatusFn statusCallback;
void * statusOpaque;
atomic_uint statusInFlight;
atomic_uint_fast64_t statusNextTicket;
atomic_uint_fast64_t statusServingTicket;
LG_Lock statusLock;
atomic_bool available;
uint32_t statusGeneration;
LG_AudioStatusFn statusCallback;
void * statusOpaque;
atomic_uint statusInFlight;
USBAudioCallbackWaitQueue statusWait;
USBAudioOperationQueue statusOperation;
LG_Lock stateLock;
bool attached;
bool detaching;
uint32_t attachmentGeneration;
const LG_AudioEventOps * events;
void * eventOpaque;
LG_Lock stateLock;
LGEvent * detachEvent;
bool attached;
bool detaching;
uint32_t attachmentGeneration;
const LG_AudioEventOps * events;
void * eventOpaque;
LG_AudioFormat playbackFormat;
uint32_t playbackGeneration;
LG_AudioFormat recordFormat;
uint32_t recordGeneration;
uint32_t generationSerial;
int64_t playbackClockOrigin;
atomic_uint_fast64_t playbackPosition;
atomic_uint playbackDeliveryGeneration;
atomic_uint recordDeliveryGeneration;
atomic_uint inFlight;
LG_AudioFormat playbackFormat;
uint32_t playbackGeneration;
LG_AudioFormat recordFormat;
uint32_t recordGeneration;
uint32_t generationSerial;
int64_t playbackClockOrigin;
atomic_uint_fast64_t playbackPosition;
atomic_uint playbackDeliveryGeneration;
atomic_uint recordDeliveryGeneration;
alignas(64) USBAudioCallbackGate playbackGate;
alignas(64) USBAudioCallbackGate recordGate;
USBAudioOperationQueue recordOperation;
};
static _Thread_local USBAudioCallbackFrame * l_eventFrames;
static _Thread_local USBAudioCallbackFrame * l_playbackFrames;
static _Thread_local USBAudioCallbackFrame * l_recordFrames;
static _Thread_local USBAudioCallbackFrame * l_statusFrames;
static const LG_AudioFormat l_formatTemplate =
@@ -139,6 +188,51 @@ static unsigned int callbackDepth(
return depth;
}
static LGEvent * createWaitEvent(void)
{
LGEvent * event = lgCreateEvent(true, 0);
if (!event)
DEBUG_FATAL("Failed to create USB audio wait event");
return event;
}
static void waitEvent(LGEvent * event)
{
if (!lgWaitEvent(event, TIMEOUT_INFINITE))
DEBUG_FATAL("Failed to wait for USB audio event");
}
static void signalWaitEvent(LGEvent * event)
{
if (!lgSignalEvent(event))
DEBUG_FATAL("Failed to signal USB audio event");
}
static void signalCallbackWaiters(
USBAudioCallbackWaitQueue * queue, unsigned int remaining)
{
if (!atomic_load_explicit(&queue->count, memory_order_seq_cst))
return;
LG_LOCK(queue->lock);
USBAudioCallbackWaiter ** link = &queue->waiters;
while (*link)
{
USBAudioCallbackWaiter * waiter = *link;
if (remaining > waiter->depth)
{
link = &waiter->next;
continue;
}
*link = waiter->next;
atomic_fetch_sub_explicit(
&queue->count, 1, memory_order_release);
signalWaitEvent(waiter->event);
}
LG_UNLOCK(queue->lock);
}
static void beginCallback(LGA_USBState * state,
USBAudioCallbackFrame * frame, USBAudioCallbackFrame ** frames,
atomic_uint * inFlight)
@@ -150,10 +244,59 @@ static void beginCallback(LGA_USBState * state,
}
static void endCallback(USBAudioCallbackFrame * frame,
USBAudioCallbackFrame ** frames, atomic_uint * inFlight)
USBAudioCallbackFrame ** frames, atomic_uint * inFlight,
USBAudioCallbackWaitQueue * waitQueue)
{
*frames = frame->previous;
atomic_fetch_sub_explicit(inFlight, 1, memory_order_seq_cst);
const unsigned int previous = atomic_fetch_sub_explicit(
inFlight, 1, memory_order_seq_cst);
DEBUG_ASSERT(previous > 0);
signalCallbackWaiters(waitQueue, previous - 1);
}
static void waitCallbacks(LGA_USBState * state,
USBAudioCallbackFrame * frames, atomic_uint * inFlight,
USBAudioCallbackWaitQueue * waitQueue)
{
const unsigned int depth = callbackDepth(frames, state);
if (atomic_load_explicit(inFlight, memory_order_seq_cst) <= depth)
return;
USBAudioCallbackWaiter waiter =
{
.event = createWaitEvent(),
.depth = depth,
};
bool queued = false;
LG_LOCK(waitQueue->lock);
/* Publish the waiter before rechecking inFlight. This prevents the final
* callback from taking the lock-free path while a waiter is being queued. */
atomic_fetch_add_explicit(
&waitQueue->count, 1, memory_order_seq_cst);
if (atomic_load_explicit(inFlight, memory_order_seq_cst) > depth)
{
waiter.next = waitQueue->waiters;
waitQueue->waiters = &waiter;
queued = true;
}
else
atomic_fetch_sub_explicit(
&waitQueue->count, 1, memory_order_seq_cst);
LG_UNLOCK(waitQueue->lock);
if (queued)
{
waitEvent(waiter.event);
/* The signaler owns the waiter until it releases the queue lock. */
LG_LOCK(waitQueue->lock);
LG_UNLOCK(waitQueue->lock);
}
lgFreeEvent(waiter.event);
}
static LG_AudioClock makePlaybackClock(
@@ -178,7 +321,8 @@ static LG_AudioClock makePlaybackClock(
/* stateLock must be held while admitting a control event. The data event
* path performs the equivalent admission using deliveryGeneration. */
static bool beginEventNL(
LGA_USBState * state, USBAudioEventTarget * target)
LGA_USBState * state, USBAudioEventTarget * target,
USBAudioCallbackFrame ** frames, USBAudioCallbackGate * gate)
{
if (!state->attached || !state->events)
return false;
@@ -186,11 +330,27 @@ static bool beginEventNL(
target->events = state->events;
target->opaque = state->eventOpaque;
target->attachmentGeneration = state->attachmentGeneration;
target->frames = frames;
target->gate = gate;
beginCallback(
state, &target->frame, &l_eventFrames, &state->inFlight);
state, &target->frame, frames, &gate->inFlight);
return true;
}
static bool beginPlaybackEventNL(
LGA_USBState * state, USBAudioEventTarget * target)
{
return beginEventNL(
state, target, &l_playbackFrames, &state->playbackGate);
}
static bool beginRecordEventNL(
LGA_USBState * state, USBAudioEventTarget * target)
{
return beginEventNL(
state, target, &l_recordFrames, &state->recordGate);
}
static bool attachmentCurrentNL(const LGA_USBState * state,
const USBAudioEventTarget * target)
{
@@ -214,37 +374,84 @@ static bool recordEventCurrentNL(const LGA_USBState * state,
state->recordGeneration == streamGeneration;
}
static void endEvent(LGA_USBState * state, USBAudioEventTarget * target)
static void endEvent(USBAudioEventTarget * target)
{
endCallback(&target->frame, &l_eventFrames, &state->inFlight);
endCallback(&target->frame, target->frames,
&target->gate->inFlight, &target->gate->wait);
}
static void waitEvents(const LGA_USBState * state)
static void waitPlaybackEvents(LGA_USBState * state)
{
const unsigned int depth = callbackDepth(l_eventFrames, state);
while (atomic_load_explicit(
&state->inFlight, memory_order_seq_cst) > depth)
;
waitCallbacks(state, l_playbackFrames,
&state->playbackGate.inFlight, &state->playbackGate.wait);
}
static bool beginStatusOperation(LGA_USBState * state)
static void waitRecordEvents(LGA_USBState * state)
{
if (callbackDepth(l_statusFrames, state))
waitCallbacks(state, l_recordFrames,
&state->recordGate.inFlight, &state->recordGate.wait);
}
static bool beginOperation(LGA_USBState * state,
USBAudioOperationQueue * operation,
const USBAudioCallbackFrame * callbackFrames)
{
/* Reentrant control must run inline: the current owner may be waiting for
* this callback to finish before it can release the operation. */
if (callbackDepth(callbackFrames, state))
return false;
const uint_fast64_t ticket = atomic_fetch_add_explicit(
&state->statusNextTicket, 1, memory_order_relaxed);
while (atomic_load_explicit(
&state->statusServingTicket, memory_order_acquire) != ticket)
;
USBAudioOperationWaiter waiter =
{
.event = createWaitEvent(),
};
bool queued = false;
LG_LOCK(operation->lock);
if (operation->active)
{
if (operation->tail)
operation->tail->next = &waiter;
else
operation->head = &waiter;
operation->tail = &waiter;
queued = true;
}
else
operation->active = true;
LG_UNLOCK(operation->lock);
if (queued)
{
waitEvent(waiter.event);
/* The previous owner owns the waiter until it releases the queue lock. */
LG_LOCK(operation->lock);
LG_UNLOCK(operation->lock);
}
lgFreeEvent(waiter.event);
return true;
}
static void endStatusOperation(LGA_USBState * state, bool owner)
static void endOperation(
USBAudioOperationQueue * operation, bool owner)
{
if (owner)
atomic_fetch_add_explicit(
&state->statusServingTicket, 1, memory_order_release);
if (!owner)
return;
LG_LOCK(operation->lock);
USBAudioOperationWaiter * waiter = operation->head;
if (waiter)
{
operation->head = waiter->next;
if (!operation->head)
operation->tail = NULL;
signalWaitEvent(waiter->event);
}
else
operation->active = false;
LG_UNLOCK(operation->lock);
}
static void beginStatusCallback(
@@ -257,15 +464,32 @@ static void beginStatusCallback(
static void endStatusCallback(
LGA_USBState * state, USBAudioCallbackFrame * frame)
{
endCallback(frame, &l_statusFrames, &state->statusInFlight);
endCallback(frame, &l_statusFrames,
&state->statusInFlight, &state->statusWait);
}
static void waitStatusCallbacks(const LGA_USBState * state)
static void waitStatusCallbacks(LGA_USBState * state)
{
const unsigned int depth = callbackDepth(l_statusFrames, state);
while (atomic_load_explicit(
&state->statusInFlight, memory_order_acquire) > depth)
;
waitCallbacks(state, l_statusFrames,
&state->statusInFlight, &state->statusWait);
}
/* Returns with stateLock held. A callback must not wait for a detach which
* may itself be waiting for that callback to return. */
static bool lockStateAfterDetach(LGA_USBState * state)
{
for (;;)
{
LG_LOCK(state->stateLock);
if (!state->detaching)
return true;
LG_UNLOCK(state->stateLock);
if (callbackDepth(l_playbackFrames, state) ||
callbackDepth(l_recordFrames, state))
return false;
waitEvent(state->detachEvent);
}
}
static void usbPlaybackStart(
@@ -294,10 +518,10 @@ static void usbPlaybackStart(
atomic_store_explicit(
&state->playbackDeliveryGeneration, 0, memory_order_seq_cst);
clock = makePlaybackClock(state, 0);
const bool admitted = beginEventNL(state, &target);
const bool admitted = beginPlaybackEventNL(state, &target);
dispatch = admitted && target.events->playbackStart;
if (admitted && !dispatch)
endEvent(state, &target);
endEvent(&target);
LG_UNLOCK(state->stateLock);
if (!dispatch)
@@ -311,7 +535,7 @@ static void usbPlaybackStart(
atomic_store_explicit(&state->playbackDeliveryGeneration,
generation, memory_order_seq_cst);
LG_UNLOCK(state->stateLock);
endEvent(state, &target);
endEvent(&target);
}
static void usbPlaybackStop(void * opaque)
@@ -330,10 +554,10 @@ static void usbPlaybackStop(void * opaque)
state->playbackGeneration = 0;
atomic_store_explicit(
&state->playbackDeliveryGeneration, 0, memory_order_seq_cst);
const bool admitted = beginEventNL(state, &target);
const bool admitted = beginPlaybackEventNL(state, &target);
LG_UNLOCK(state->stateLock);
waitEvents(state);
waitPlaybackEvents(state);
if (!admitted)
return;
@@ -345,7 +569,7 @@ static void usbPlaybackStop(void * opaque)
if (dispatch)
target.events->playbackStop(target.opaque, generation);
endEvent(state, &target);
endEvent(&target);
}
static void usbPlaybackData(
@@ -353,7 +577,8 @@ static void usbPlaybackData(
{
LGA_USBState * state = opaque;
USBAudioCallbackFrame frame;
beginCallback(state, &frame, &l_eventFrames, &state->inFlight);
beginCallback(state, &frame, &l_playbackFrames,
&state->playbackGate.inFlight);
const uint64_t position = atomic_fetch_add_explicit(
&state->playbackPosition, frames, memory_order_relaxed);
@@ -370,7 +595,8 @@ static void usbPlaybackData(
target, generation, data, frames, &clock);
}
}
endCallback(&frame, &l_eventFrames, &state->inFlight);
endCallback(&frame, &l_playbackFrames,
&state->playbackGate.inFlight, &state->playbackGate.wait);
}
static void usbRecordStart(
@@ -378,11 +604,15 @@ static void usbRecordStart(
{
LGA_USBState * state = opaque;
USBAudioEventTarget target;
LG_AudioFormat format;
bool dispatch;
uint32_t generation;
const bool recordOwner = beginOperation(
state, &state->recordOperation, l_recordFrames);
LG_LOCK(state->stateLock);
setStreamFormat(&state->recordFormat, sampleRate, channelMask);
format = state->recordFormat;
generation = state->recordGeneration;
if (!generation)
{
@@ -392,49 +622,58 @@ static void usbRecordStart(
}
atomic_store_explicit(
&state->recordDeliveryGeneration, 0, memory_order_seq_cst);
const bool admitted = beginEventNL(state, &target);
const bool admitted = beginRecordEventNL(state, &target);
dispatch = admitted && target.events->recordStart;
if (dispatch)
atomic_store_explicit(&state->recordDeliveryGeneration,
generation, memory_order_seq_cst);
if (admitted && !dispatch)
endEvent(state, &target);
endEvent(&target);
LG_UNLOCK(state->stateLock);
if (!dispatch)
{
endOperation(&state->recordOperation, recordOwner);
return;
}
target.events->recordStart(
target.opaque, generation, &state->recordFormat);
LG_LOCK(state->stateLock);
if (recordEventCurrentNL(state, &target, generation))
atomic_store_explicit(&state->recordDeliveryGeneration,
generation, memory_order_seq_cst);
LG_UNLOCK(state->stateLock);
endEvent(state, &target);
target.opaque, generation, &format);
endEvent(&target);
endOperation(&state->recordOperation, recordOwner);
}
static void usbRecordStop(void * opaque)
{
LGA_USBState * state = opaque;
USBAudioEventTarget target;
const bool recordOwner = beginOperation(
state, &state->recordOperation, l_recordFrames);
LG_LOCK(state->stateLock);
const uint32_t generation = state->recordGeneration;
if (!generation)
{
LG_UNLOCK(state->stateLock);
endOperation(&state->recordOperation, recordOwner);
return;
}
state->recordGeneration = 0;
atomic_store_explicit(
&state->recordDeliveryGeneration, 0, memory_order_seq_cst);
const bool admitted = beginEventNL(state, &target);
const bool admitted = beginRecordEventNL(state, &target);
LG_UNLOCK(state->stateLock);
waitEvents(state);
/* A reentrant stop cannot quiesce callbacks for the same reason that it
* cannot wait for ownership above. */
waitRecordEvents(state);
if (!admitted)
{
endOperation(&state->recordOperation, recordOwner);
return;
}
LG_LOCK(state->stateLock);
const bool dispatch = attachmentCurrentNL(state, &target) &&
@@ -443,7 +682,8 @@ static void usbRecordStop(void * opaque)
if (dispatch)
target.events->recordStop(target.opaque, generation);
endEvent(state, &target);
endEvent(&target);
endOperation(&state->recordOperation, recordOwner);
}
static const LG_USBAudioEventOps l_usbAudioEvents =
@@ -458,7 +698,8 @@ static const LG_USBAudioEventOps l_usbAudioEvents =
static void usbSetAvailable(void * opaque, bool available)
{
LGA_USBState * state = opaque;
const bool statusOwner = beginStatusOperation(state);
const bool statusOwner = beginOperation(
state, &state->statusOperation, l_statusFrames);
LG_LOCK(state->statusLock);
const bool changed = atomic_load_explicit(
@@ -488,14 +729,15 @@ static void usbSetAvailable(void * opaque, bool available)
callback(callbackOpaque, &status);
endStatusCallback(state, &frame);
}
endStatusOperation(state, statusOwner);
endOperation(&state->statusOperation, statusOwner);
}
static void usbSetStatusListener(void * opaque,
LG_AudioStatusFn callback, void * callbackOpaque)
{
LGA_USBState * state = opaque;
const bool statusOwner = beginStatusOperation(state);
const bool statusOwner = beginOperation(
state, &state->statusOperation, l_statusFrames);
LG_LOCK(state->statusLock);
state->statusCallback = callback;
@@ -518,7 +760,7 @@ static void usbSetStatusListener(void * opaque,
}
else
waitStatusCallbacks(state);
endStatusOperation(state, statusOwner);
endOperation(&state->statusOperation, statusOwner);
}
static bool usbAttach(void * opaque, const LG_AudioEventOps * events,
@@ -529,25 +771,21 @@ static bool usbAttach(void * opaque, const LG_AudioEventOps * events,
&state->available, memory_order_acquire))
return false;
USBAudioEventTarget target;
USBAudioEventTarget playbackTarget;
USBAudioEventTarget recordTarget;
LG_AudioFormat playbackFormat;
LG_AudioFormat recordFormat;
LG_AudioClock playbackClock;
uint32_t attachmentGeneration;
uint32_t playbackGeneration;
uint32_t recordGeneration;
bool playbackDispatch;
bool recordDispatch;
bool admitted;
bool playbackAdmitted;
bool recordAdmitted;
for (;;)
{
LG_LOCK(state->stateLock);
if (!state->detaching)
break;
LG_UNLOCK(state->stateLock);
if (callbackDepth(l_eventFrames, state))
return false;
}
if (!lockStateAfterDetach(state))
return false;
if (state->attached || !atomic_load_explicit(
&state->available, memory_order_acquire))
@@ -561,61 +799,72 @@ static bool usbAttach(void * opaque, const LG_AudioEventOps * events,
nextGeneration(state->attachmentGeneration);
state->events = events;
state->eventOpaque = eventOpaque;
attachmentGeneration = state->attachmentGeneration;
playbackGeneration = state->playbackGeneration;
recordGeneration = state->recordGeneration;
playbackDispatch = playbackGeneration && events->playbackStart;
recordDispatch = recordGeneration && events->recordStart;
admitted = (playbackDispatch || recordDispatch) &&
beginEventNL(state, &target);
playbackDispatch = playbackDispatch && admitted;
recordDispatch = recordDispatch && admitted;
playbackAdmitted = playbackDispatch &&
beginPlaybackEventNL(state, &playbackTarget);
playbackDispatch = playbackDispatch && playbackAdmitted;
recordAdmitted = false;
if (playbackDispatch)
{
playbackFormat = state->playbackFormat;
playbackClock = makePlaybackClock(state, atomic_load_explicit(
&state->playbackPosition, memory_order_relaxed));
}
if (recordDispatch)
recordFormat = state->recordFormat;
lgUsbRedir_setPlugged(state->redir, true);
LG_UNLOCK(state->stateLock);
if (playbackDispatch)
{
target.events->playbackStart(
target.opaque, playbackGeneration,
playbackTarget.events->playbackStart(
playbackTarget.opaque, playbackGeneration,
&playbackFormat, &playbackClock);
LG_LOCK(state->stateLock);
if (playbackEventCurrentNL(
state, &target, playbackGeneration))
state, &playbackTarget, playbackGeneration))
atomic_store_explicit(&state->playbackDeliveryGeneration,
playbackGeneration, memory_order_seq_cst);
LG_UNLOCK(state->stateLock);
}
if (playbackAdmitted)
endEvent(&playbackTarget);
if (recordDispatch)
{
const bool recordOwner = beginOperation(
state, &state->recordOperation, l_recordFrames);
LG_LOCK(state->stateLock);
recordDispatch = recordEventCurrentNL(
state, &target, recordGeneration);
recordDispatch = state->attached &&
state->attachmentGeneration == attachmentGeneration &&
state->events == events &&
state->eventOpaque == eventOpaque &&
state->recordGeneration == recordGeneration;
recordAdmitted = recordDispatch &&
beginRecordEventNL(state, &recordTarget);
recordDispatch = recordAdmitted &&
recordEventCurrentNL(state, &recordTarget, recordGeneration);
if (recordDispatch)
{
recordFormat = state->recordFormat;
atomic_store_explicit(&state->recordDeliveryGeneration,
recordGeneration, memory_order_seq_cst);
}
LG_UNLOCK(state->stateLock);
if (recordDispatch)
target.events->recordStart(
target.opaque, recordGeneration, &recordFormat);
recordTarget.events->recordStart(
recordTarget.opaque, recordGeneration, &recordFormat);
LG_LOCK(state->stateLock);
if (recordDispatch &&
recordEventCurrentNL(state, &target, recordGeneration))
atomic_store_explicit(&state->recordDeliveryGeneration,
recordGeneration, memory_order_seq_cst);
LG_UNLOCK(state->stateLock);
if (recordAdmitted)
endEvent(&recordTarget);
endOperation(&state->recordOperation, recordOwner);
}
if (admitted)
endEvent(state, &target);
return true;
}
@@ -623,17 +872,11 @@ static void usbDetach(void * opaque)
{
LGA_USBState * state = opaque;
for (;;)
{
LG_LOCK(state->stateLock);
if (!state->detaching)
break;
LG_UNLOCK(state->stateLock);
if (callbackDepth(l_eventFrames, state))
return;
}
if (!lockStateAfterDetach(state))
return;
state->detaching = true;
lgResetEvent(state->detachEvent);
state->attached = false;
state->attachmentGeneration =
nextGeneration(state->attachmentGeneration);
@@ -646,7 +889,8 @@ static void usbDetach(void * opaque)
lgUsbRedir_setPlugged(state->redir, false);
LG_UNLOCK(state->stateLock);
waitEvents(state);
waitPlaybackEvents(state);
waitRecordEvents(state);
LG_LOCK(state->stateLock);
if (!state->attached &&
@@ -655,6 +899,7 @@ static void usbDetach(void * opaque)
state->events = NULL;
state->eventOpaque = NULL;
state->detaching = false;
signalWaitEvent(state->detachEvent);
}
LG_UNLOCK(state->stateLock);
}
@@ -666,7 +911,8 @@ static bool usbClockFeedback(void * opaque, uint32_t generation,
LG_LOCK(state->stateLock);
if (!state->attached ||
state->playbackGeneration != generation)
state->playbackGeneration != generation ||
!lgUsbAudio_feedbackActive(state->device))
{
LG_UNLOCK(state->stateLock);
return false;
@@ -686,17 +932,19 @@ static bool usbRecordData(void * opaque, uint32_t generation,
const void * data, size_t frames,
const LG_AudioClock * sourceClock)
{
(void)sourceClock;
LGA_USBState * state = opaque;
USBAudioCallbackFrame frame;
beginCallback(state, &frame, &l_eventFrames, &state->inFlight);
beginCallback(state, &frame, &l_recordFrames,
&state->recordGate.inFlight);
const bool valid = generation && generation == atomic_load_explicit(
&state->recordDeliveryGeneration, memory_order_seq_cst);
const bool result = valid &&
lgUsbAudio_recordData(state->device, data, frames);
lgUsbAudio_recordData(
state->device, data, frames, sourceClock);
endCallback(&frame, &l_eventFrames, &state->inFlight);
endCallback(&frame, &l_recordFrames,
&state->recordGate.inFlight, &state->recordGate.wait);
return result;
}
@@ -710,28 +958,47 @@ const LG_AudioOps LGA_USB =
.clockFeedback = usbClockFeedback,
};
LGA_USBState * lgaUsb_create(void)
LGA_USBState * lgaUsb_create(bool debug)
{
LGA_USBState * state = calloc(1, sizeof(*state));
LGA_USBState * state = aligned_alloc(
alignof(LGA_USBState), sizeof(*state));
if (!state)
return NULL;
memset(state, 0, sizeof(*state));
LG_LOCK_INIT(state->statusLock);
LG_LOCK_INIT(state->statusWait.lock);
LG_LOCK_INIT(state->statusOperation.lock);
LG_LOCK_INIT(state->stateLock);
LG_LOCK_INIT(state->playbackGate.wait.lock);
LG_LOCK_INIT(state->recordGate.wait.lock);
LG_LOCK_INIT(state->recordOperation.lock);
atomic_init(&state->available, false);
atomic_init(&state->playbackPosition, 0);
atomic_init(&state->playbackDeliveryGeneration, 0);
atomic_init(&state->recordDeliveryGeneration, 0);
atomic_init(&state->inFlight, 0);
atomic_init(&state->playbackGate.inFlight, 0);
atomic_init(&state->recordGate.inFlight, 0);
atomic_init(&state->statusInFlight, 0);
atomic_init(&state->statusNextTicket, 0);
atomic_init(&state->statusServingTicket, 0);
atomic_init(&state->playbackGate.wait.count, 0);
atomic_init(&state->recordGate.wait.count, 0);
atomic_init(&state->statusWait.count, 0);
state->playbackFormat = l_formatTemplate;
state->recordFormat = l_formatTemplate;
state->device = lgUsbAudio_create(&l_usbAudioEvents, state);
state->detachEvent = lgCreateEvent(false, 0);
if (!state->detachEvent || !lgSignalEvent(state->detachEvent))
{
if (state->detachEvent)
lgFreeEvent(state->detachEvent);
free(state);
return NULL;
}
state->device = lgUsbAudio_create(&l_usbAudioEvents, state, debug);
if (!state->device)
{
lgFreeEvent(state->detachEvent);
free(state);
return NULL;
}
@@ -741,6 +1008,7 @@ LGA_USBState * lgaUsb_create(void)
if (!state->redir)
{
lgUsbAudio_destroy(state->device);
lgFreeEvent(state->detachEvent);
free(state);
return NULL;
}
@@ -757,7 +1025,13 @@ void lgaUsb_destroy(LGA_USBState * state)
usbDetach(state);
lgUsbRedir_destroy(state->redir);
lgUsbAudio_destroy(state->device);
lgFreeEvent(state->detachEvent);
LG_LOCK_FREE(state->recordOperation.lock);
LG_LOCK_FREE(state->recordGate.wait.lock);
LG_LOCK_FREE(state->playbackGate.wait.lock);
LG_LOCK_FREE(state->stateLock);
LG_LOCK_FREE(state->statusOperation.lock);
LG_LOCK_FREE(state->statusWait.lock);
LG_LOCK_FREE(state->statusLock);
free(state);
}
@@ -767,7 +1041,7 @@ LG_USBRedir * lgaUsb_redir(LGA_USBState * state)
return state ? state->redir : NULL;
}
bool lgaUsb_recording(const LGA_USBState * state)
uint64_t lgaUsb_processDelayNs(const LGA_USBState * state)
{
return state && lgUsbAudio_recording(state->device);
return state ? lgUsbAudio_processDelayNs(state->device) : UINT64_MAX;
}

View File

@@ -24,14 +24,16 @@
#include "interface/audio.h"
#include "usbredir.h"
#include <stdint.h>
typedef struct LGA_USBState LGA_USBState;
LGA_USBState * lgaUsb_create(void);
LGA_USBState * lgaUsb_create(bool debug);
/* Detach this provider and stop PureSpice before destroying its state. */
void lgaUsb_destroy(LGA_USBState * state);
LG_USBRedir * lgaUsb_redir(LGA_USBState * state);
bool lgaUsb_recording(const LGA_USBState * state);
uint64_t lgaUsb_processDelayNs(const LGA_USBState * state);
extern const LG_AudioOps LGA_USB;

View File

@@ -1728,7 +1728,7 @@ int spiceThread(void * arg)
DEBUG_WARN("USB audio requires a playback backend, using SPICE audio");
g_params.useSpiceUSBAudio = false;
}
else if (!(usbAudio = lgaUsb_create()))
else if (!(usbAudio = lgaUsb_create(g_params.audioDebug)))
{
DEBUG_WARN("Failed to initialize USB audio, using SPICE audio");
g_params.useSpiceUSBAudio = false;
@@ -1839,7 +1839,17 @@ int spiceThread(void * arg)
if (usbRedir && !lgUsbRedir_process(usbRedir))
DEBUG_WARN("Failed to process USB audio redirection");
if (usbAudio)
processTimeout = lgaUsb_recording(usbAudio) ? 1 : 10;
{
const uint64_t delay = lgaUsb_processDelayNs(usbAudio);
if (delay == UINT64_MAX)
processTimeout = 10;
else
{
const uint64_t timeout = delay / UINT64_C(1000000) +
(delay % UINT64_C(1000000) != 0);
processTimeout = (int)min(timeout, UINT64_C(10));
}
}
#endif
if ((status = purespice_process(processTimeout)) != PS_STATUS_RUN)

View File

@@ -21,6 +21,7 @@
#include "interface/overlay.h"
#include "math.h"
#include "cimgui.h"
#include <stdatomic.h>
#include "../overlays.h"
#include "../main.h"
@@ -32,7 +33,7 @@
//TODO: Make this user configurable?
#define ICON_SIZE 32
static bool l_state[LG_USER_STATUS_MAX] = { 0 };
static atomic_bool l_state[LG_USER_STATUS_MAX] = { 0 };
static OverlayImage l_image[LG_USER_STATUS_MAX] = { 0 };
static bool l_recordToggle;
static double l_scale = 1.0;
@@ -92,7 +93,8 @@ static int status_render(void * udata, bool interactive, struct Rect * windowRec
for(int i = 0; i < LG_USER_STATUS_MAX; ++i)
{
OverlayImage * img = &l_image[i];
if (!l_state[i] || !img->tex)
if (!atomic_load_explicit(&l_state[i], memory_order_relaxed) ||
!img->tex)
continue;
// if the recording indicator is off, don't draw but reserve space
@@ -147,9 +149,9 @@ struct LG_OverlayOps LGOverlayStatus =
void overlayStatus_set(LGUserStatus status, bool value)
{
if (l_state[status] == value)
if (atomic_exchange_explicit(
&l_state[status], value, memory_order_relaxed) == value)
return;
l_state[status] = value;
app_invalidateOverlay(true);
};

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
#ifndef _H_LG_CLIENT_USB_AUDIO_
#define _H_LG_CLIENT_USB_AUDIO_
#include "interface/audio.h"
#include "usbredir.h"
#include <stddef.h>
@@ -49,21 +50,25 @@ typedef struct LG_USBAudioEventOps
LG_USBAudioEventOps;
LG_USBAudio * lgUsbAudio_create(
const LG_USBAudioEventOps * events, void * eventOpaque);
const LG_USBAudioEventOps * events, void * eventOpaque, bool debug);
/* Destroy the LG_USBRedir using this device before destroying the device. */
void lgUsbAudio_destroy(LG_USBAudio * audio);
/* Publish the requested source rate without touching usbredir from the audio
* feedback thread. */
void lgUsbAudio_setFeedbackRate(LG_USBAudio * audio, double sampleRate);
bool lgUsbAudio_feedbackActive(const LG_USBAudio * audio);
/* Queue interleaved packed signed 24-bit microphone frames. This may be
* called from the audio recording thread. */
/* Queue interleaved packed signed 24-bit microphone frames. sourceClock is
* borrowed for the call and identifies the first frame when present. This may
* be called from the audio recording thread. */
bool lgUsbAudio_recordData(
LG_USBAudio * audio, const void * data, size_t frames);
LG_USBAudio * audio, const void * data, size_t frames,
const LG_AudioClock * sourceClock);
/* This must be queried on the PureSpice processing thread. */
bool lgUsbAudio_recording(const LG_USBAudio * audio);
/* Return the time until ISO-IN processing is needed. This must be queried on
* the PureSpice processing thread. */
uint64_t lgUsbAudio_processDelayNs(const LG_USBAudio * audio);
const LG_USBRedirDeviceOps * lgUsbAudio_deviceOps(void);

View File

@@ -21,6 +21,7 @@
#include "usbredir.h"
#include "common/debug.h"
#include "common/time.h"
#include <usbredirparser.h>
@@ -29,6 +30,9 @@
#include <string.h>
#define USB_REDIR_CHANNEL_COUNT 256
#define USB_REDIR_DISCONNECT_TIMEOUT_NS INT64_C(500000000)
#define USB_REDIR_RECONNECT_DELAY_NS INT64_C(250000000)
#define USB_REDIR_MAX_OUTPUT_BYTES UINT64_C(1048576)
struct LG_USBRedir
{
@@ -49,6 +53,8 @@ struct LG_USBRedir
atomic_bool available;
bool plugged;
bool disconnectPending;
int64_t disconnectDeadline;
int64_t reconnectDeadline;
};
static void setAvailable(LG_USBRedir * usbredir, bool available)
@@ -123,6 +129,7 @@ static void deviceDisconnectAck(void * opaque)
{
LG_USBRedir * usbredir = opaque;
usbredir->disconnectPending = false;
usbredir->disconnectDeadline = 0;
}
static void unplugDevice(LG_USBRedir * usbredir)
@@ -139,6 +146,7 @@ static void destroyParser(LG_USBRedir * usbredir)
setAvailable(usbredir, false);
unplugDevice(usbredir);
usbredir->disconnectPending = false;
usbredir->disconnectDeadline = 0;
if (!usbredir->parser)
return;
@@ -149,8 +157,20 @@ static void destroyParser(LG_USBRedir * usbredir)
static bool flushUSBRedir(LG_USBRedir * usbredir)
{
return !usbredir->parser ||
usbredirparser_do_write(usbredir->parser) == 0;
if (!usbredir->parser)
return true;
if (usbredirparser_do_write(usbredir->parser) != 0)
return false;
const uint64_t buffered =
usbredirparser_get_bufferered_output_size(usbredir->parser);
if (buffered <= USB_REDIR_MAX_OUTPUT_BYTES)
return true;
DEBUG_ERROR("USB redirection output queue exceeded %u bytes",
(unsigned int)USB_REDIR_MAX_OUTPUT_BYTES);
return false;
}
static bool connectChannel(LG_USBRedir * usbredir,
@@ -166,10 +186,10 @@ static bool connectChannel(LG_USBRedir * usbredir,
return false;
}
static void selectChannel(LG_USBRedir * usbredir)
static bool selectChannel(LG_USBRedir * usbredir)
{
if (usbredir->channel)
return;
return true;
for (unsigned int i = 0; i < USB_REDIR_CHANNEL_COUNT; ++i)
{
@@ -178,8 +198,28 @@ static void selectChannel(LG_USBRedir * usbredir)
continue;
if (connectChannel(usbredir, channel))
return;
{
usbredir->reconnectDeadline = 0;
return true;
}
}
usbredir->reconnectDeadline =
(int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
return false;
}
static void resetChannel(LG_USBRedir * usbredir)
{
PSUSBRedirChannel * channel = usbredir->channel;
usbredir->channel = NULL;
destroyParser(usbredir);
if (channel && purespice_usbRedirConnected(channel))
purespice_usbRedirDisconnect(channel);
usbredir->reconnectDeadline =
(int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
}
static bool createParser(LG_USBRedir * usbredir)
@@ -268,14 +308,37 @@ bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir)
return usbredir->disconnectPending;
}
bool lgUsbRedir_process(LG_USBRedir * usbredir)
static bool processUSBRedir(LG_USBRedir * usbredir, bool recover)
{
const int64_t now = (int64_t)nanotime();
if (!usbredir->channel &&
now >= usbredir->reconnectDeadline)
selectChannel(usbredir);
if (!usbredir->parser ||
!atomic_load_explicit(&usbredir->available, memory_order_acquire))
return flushUSBRedir(usbredir);
{
const bool result = flushUSBRedir(usbredir);
if (!result && recover)
resetChannel(usbredir);
return result;
}
if (usbredir->disconnectPending)
return flushUSBRedir(usbredir);
{
if (usbredir->disconnectDeadline &&
now >= usbredir->disconnectDeadline)
{
DEBUG_WARN("USB redirection disconnect acknowledgement timed out");
resetChannel(usbredir);
return true;
}
const bool result = flushUSBRedir(usbredir);
if (!result && recover)
resetChannel(usbredir);
return result;
}
const bool desired = atomic_load_explicit(&usbredir->desiredPlugged,
memory_order_acquire);
@@ -292,6 +355,8 @@ bool lgUsbRedir_process(LG_USBRedir * usbredir)
unplugDevice(usbredir);
usbredir->disconnectPending = usbredirparser_peer_has_cap(
usbredir->parser, usb_redir_cap_device_disconnect_ack);
usbredir->disconnectDeadline = usbredir->disconnectPending ?
now + USB_REDIR_DISCONNECT_TIMEOUT_NS : 0;
usbredirparser_send_device_disconnect(usbredir->parser);
}
}
@@ -299,7 +364,15 @@ bool lgUsbRedir_process(LG_USBRedir * usbredir)
if (usbredir->plugged && usbredir->deviceOps->process)
usbredir->deviceOps->process(usbredir->deviceOpaque);
return flushUSBRedir(usbredir);
const bool result = flushUSBRedir(usbredir);
if (!result && recover)
resetChannel(usbredir);
return result;
}
bool lgUsbRedir_process(LG_USBRedir * usbredir)
{
return processUSBRedir(usbredir, true);
}
void lgUsbRedir_state(PSUSBRedirChannel * channel,
@@ -328,7 +401,13 @@ void lgUsbRedir_state(PSUSBRedirChannel * channel,
case PS_USB_REDIR_DISCONNECTED:
if (channel == usbredir->channel)
{
destroyParser(usbredir);
usbredir->channel = NULL;
if (purespice_usbRedirAvailable(channel))
usbredir->reconnectDeadline =
(int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
}
break;
case PS_USB_REDIR_UNAVAILABLE:
@@ -375,7 +454,9 @@ bool lgUsbRedir_data(PSUSBRedirChannel * channel,
return false;
}
return lgUsbRedir_process(usbredir);
/* Returning false lets PureSpice disconnect after this callback unwinds;
* resetting it here would destroy the channel during its own dispatch. */
return processUSBRedir(usbredir, false);
}
void * lgUsbRedir_device(void * parserOpaque)