[client] spice: move protocol into transport

This commit is contained in:
Geoffrey McRae
2026-08-11 15:33:53 +10:00
parent 2bd0db1a26
commit 5b12685e3c
28 changed files with 2398 additions and 1058 deletions

View File

@@ -0,0 +1,46 @@
cmake_minimum_required(VERSION 3.10)
project(transport_SPICE LANGUAGES C)
set(SOURCES
clipboard.c
input.c
session.c
spice.c
surface.c
)
if(ENABLE_AUDIO)
list(APPEND SOURCES
audio.c
)
endif()
if(ENABLE_USB_AUDIO)
list(APPEND SOURCES
audio_usb.c
usbredir.c
${CMAKE_CURRENT_SOURCE_DIR}/../../src/usb_audio.c
)
endif()
add_library(transport_SPICE STATIC ${SOURCES})
target_include_directories(transport_SPICE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
)
target_link_libraries(transport_SPICE
lg_common
purespice
)
if(ENABLE_AUDIO)
target_compile_definitions(transport_SPICE PRIVATE ENABLE_AUDIO)
endif()
if(ENABLE_USB_AUDIO)
target_compile_definitions(transport_SPICE PRIVATE ENABLE_USB_AUDIO)
target_link_libraries(transport_SPICE
PkgConfig::USBREDIRPARSER
)
endif()

View File

@@ -0,0 +1,992 @@
/**
* 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 "common/debug.h"
#include "common/event.h"
#include "common/locking.h"
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#define SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS 2000
typedef struct SpiceAudioStream
{
bool active;
uint32_t generation;
LG_AudioFormat format;
bool volumeValid;
uint8_t volumeChannels;
uint16_t volume[LG_AUDIO_MAX_CHANNELS];
bool muteValid;
bool mute;
}
SpiceAudioStream;
typedef struct SpiceAudioEventTarget
{
const LG_AudioEventOps * events;
void * opaque;
uint32_t generation;
}
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;
struct SpiceAudio
{
LG_RWLock lock;
bool available;
uint32_t statusGeneration;
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;
bool playbackClockValid;
uint32_t playbackMediaTime;
int64_t playbackTime;
uint64_t playbackPosition;
LG_AudioClock playbackClock;
};
static _Thread_local unsigned int l_eventDepth;
static _Thread_local unsigned int l_statusDepth;
static uint32_t nextGeneration(uint32_t generation)
{
if (++generation == 0)
++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);
}
/* The instance 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(SpiceAudio * audio,
SpiceAudioEventTarget * target)
{
if (!audio->events)
return false;
target->events = audio->events;
target->opaque = audio->eventOpaque;
target->generation = audio->eventGeneration;
atomic_fetch_add_explicit(&audio->inFlight, 1, memory_order_relaxed);
++l_eventDepth;
return true;
}
static bool playbackEventCurrent(SpiceAudio * audio,
const SpiceAudioEventTarget * target, uint32_t generation, bool active)
{
LG_LOCK_SHARED(audio->lock);
const bool result =
target->events == audio->events &&
target->opaque == audio->eventOpaque &&
target->generation == audio->eventGeneration &&
audio->playback.generation == generation &&
audio->playback.active == active;
LG_UNLOCK_SHARED(audio->lock);
return result;
}
static bool recordEventCurrent(SpiceAudio * audio,
const SpiceAudioEventTarget * target, uint32_t generation, bool active)
{
LG_LOCK_SHARED(audio->lock);
const bool result =
target->events == audio->events &&
target->opaque == audio->eventOpaque &&
target->generation == audio->eventGeneration &&
audio->record.generation == generation &&
audio->record.active == active;
LG_UNLOCK_SHARED(audio->lock);
return result;
}
static void endEvent(SpiceAudio * audio)
{
--l_eventDepth;
endCallback(&audio->inFlight, &audio->eventWait);
}
static bool sampleFormat(PSAudioFormat source,
LG_AudioSampleFormat * format)
{
switch (source)
{
case PS_AUDIO_FMT_S16:
*format = LG_AUDIO_FMT_S16_LE;
return true;
default:
return false;
}
}
static void channelLayout(LG_AudioFormat * format)
{
memset(format->channels, 0, sizeof(format->channels));
switch (format->channelCount)
{
case 1:
format->channels[0] = LG_AUDIO_CH_MONO;
break;
case 2:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
break;
case 3:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
break;
case 4:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
format->channels[2] = LG_AUDIO_CH_REAR_LEFT;
format->channels[3] = LG_AUDIO_CH_REAR_RIGHT;
break;
case 5:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
format->channels[3] = LG_AUDIO_CH_REAR_LEFT;
format->channels[4] = LG_AUDIO_CH_REAR_RIGHT;
break;
case 6:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
format->channels[3] = LG_AUDIO_CH_LFE;
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
break;
case 7:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
format->channels[3] = LG_AUDIO_CH_LFE;
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
format->channels[6] = LG_AUDIO_CH_REAR_CENTER;
break;
case 8:
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
format->channels[3] = LG_AUDIO_CH_LFE;
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
format->channels[6] = LG_AUDIO_CH_SIDE_LEFT;
format->channels[7] = LG_AUDIO_CH_SIDE_RIGHT;
break;
default:
break;
}
}
static bool makeFormat(int channels, int sampleRate, PSAudioFormat source,
LG_AudioFormat * format)
{
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || sampleRate < 1 ||
!sampleFormat(source, &format->sampleFormat))
return false;
format->sampleRate = sampleRate;
format->channelCount = channels;
channelLayout(format);
return true;
}
static size_t sampleSize(LG_AudioSampleFormat format)
{
switch (format)
{
case LG_AUDIO_FMT_U8: return 1;
case LG_AUDIO_FMT_S16_LE: return 2;
case LG_AUDIO_FMT_S24_LE: return 3;
case LG_AUDIO_FMT_S32_LE: return 4;
case LG_AUDIO_FMT_F32_LE: return 4;
case LG_AUDIO_FMT_F32_NE: return 4;
case LG_AUDIO_FMT_F64_LE: return 8;
}
return 0;
}
static LG_AudioClock playbackClockNL(SpiceAudio * audio, uint32_t time)
{
bool discontinuity = false;
if (!audio->playbackClockValid)
{
audio->playbackClockValid = true;
audio->playbackMediaTime = time;
audio->playbackTime = 0;
}
else
{
const int32_t delta = (int32_t)(time - audio->playbackMediaTime);
audio->playbackMediaTime = time;
if (delta < 0 || delta > SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS)
{
audio->playbackTime = 0;
discontinuity = true;
}
else
audio->playbackTime += (int64_t)delta * 1000000;
}
audio->playbackClock = (LG_AudioClock)
{
.position = audio->playbackPosition,
.time = audio->playbackTime,
.rate = 0.0,
.stable = !discontinuity,
.discontinuity = discontinuity,
};
return audio->playbackClock;
}
static void spiceSetStatusListener(void * opaque,
LG_AudioStatusFn callback, void * callbackOpaque)
{
SpiceAudio * audio = opaque;
LG_AudioStatus status;
LG_LOCK_EXCLUSIVE(audio->lock);
audio->statusCallback = callback;
audio->statusOpaque = callbackOpaque;
status = (LG_AudioStatus)
{
.available = audio->available,
.generation = audio->statusGeneration,
};
if (callback)
{
atomic_fetch_add_explicit(
&audio->statusInFlight, 1, memory_order_relaxed);
++l_statusDepth;
}
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (callback)
{
callback(callbackOpaque, &status);
--l_statusDepth;
endCallback(&audio->statusInFlight, &audio->statusWait);
}
else
waitCallbacks(&audio->statusInFlight,
&audio->statusWait, l_statusDepth);
}
static bool spiceAttach(void * opaque, const LG_AudioEventOps * events,
void * eventOpaque)
{
SpiceAudio * audio = opaque;
if (!events)
return false;
SpiceAudioEventTarget target;
SpiceAudioStream playback;
SpiceAudioStream record;
LG_AudioClock playbackClock;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return false;
}
audio->events = events;
audio->eventOpaque = eventOpaque;
audio->eventGeneration =
nextGeneration(audio->eventGeneration);
playback = audio->playback;
record = audio->record;
playbackClock = audio->playbackClock;
dispatch = beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return true;
if (playback.active)
{
if (playbackEventCurrent(audio, &target, playback.generation, true) &&
target.events->playbackStart)
{
target.events->playbackStart(target.opaque,
playback.generation, &playback.format, &playbackClock);
if (playback.volumeValid &&
playbackEventCurrent(audio, &target, playback.generation, true) &&
target.events->playbackVolume)
target.events->playbackVolume(target.opaque,
playback.generation,
playback.volumeChannels, playback.volume);
if (playback.muteValid &&
playbackEventCurrent(audio, &target, playback.generation, true) &&
target.events->playbackMute)
target.events->playbackMute(target.opaque,
playback.generation, playback.mute);
}
}
if (record.active)
{
if (recordEventCurrent(audio, &target, record.generation, true) &&
target.events->recordStart)
{
target.events->recordStart(target.opaque,
record.generation, &record.format);
if (record.volumeValid &&
recordEventCurrent(audio, &target, record.generation, true) &&
target.events->recordVolume)
target.events->recordVolume(target.opaque, record.generation,
record.volumeChannels, record.volume);
if (record.muteValid &&
recordEventCurrent(audio, &target, record.generation, true) &&
target.events->recordMute)
target.events->recordMute(target.opaque,
record.generation, record.mute);
}
}
endEvent(audio);
return true;
}
static void spiceDetach(void * opaque)
{
SpiceAudio * audio = opaque;
LG_LOCK_EXCLUSIVE(audio->lock);
audio->events = NULL;
audio->eventOpaque = NULL;
audio->eventGeneration =
nextGeneration(audio->eventGeneration);
LG_UNLOCK_EXCLUSIVE(audio->lock);
waitCallbacks(&audio->inFlight, &audio->eventWait, l_eventDepth);
}
static bool spiceRecordData(void * opaque, uint32_t generation,
const void * data, size_t frames, const LG_AudioClock * sourceClock)
{
SpiceAudio * audio = opaque;
size_t size = 0;
bool valid = false;
LG_LOCK_SHARED(audio->lock);
if (audio->available && audio->events && audio->record.active &&
generation == audio->record.generation)
{
const size_t bytesPerSample =
sampleSize(audio->record.format.sampleFormat);
const size_t channels = audio->record.format.channelCount;
if (bytesPerSample && frames <= SIZE_MAX / bytesPerSample / channels &&
(frames == 0 || data))
{
size = frames * bytesPerSample * channels;
valid = true;
}
}
LG_UNLOCK_SHARED(audio->lock);
return valid && purespice_writeAudio((void *)data, size, 0);
}
static const LG_AudioOps l_spiceAudioOps =
{
.name = "SPICE",
.setStatusListener = spiceSetStatusListener,
.attach = spiceAttach,
.detach = spiceDetach,
.recordData = spiceRecordData,
.clockFeedback = NULL,
};
bool spiceAudio_init(SpiceAudio ** result)
{
if (!result)
return false;
SpiceAudio * audio = calloc(1, sizeof(*audio));
if (!audio)
return false;
LG_RWLOCK_INIT(audio->lock);
atomic_init(&audio->statusInFlight, 0);
LG_LOCK_INIT(audio->statusWait.lock);
atomic_init(&audio->statusWait.count, 0);
atomic_init(&audio->inFlight, 0);
LG_LOCK_INIT(audio->eventWait.lock);
atomic_init(&audio->eventWait.count, 0);
*result = audio;
return true;
}
void spiceAudio_free(SpiceAudio ** audio)
{
if (!audio || !*audio)
return;
SpiceAudio * instance = *audio;
spiceAudio_setAvailable(instance, false);
spiceSetStatusListener(instance, NULL, NULL);
spiceDetach(instance);
LG_LOCK_FREE(instance->statusWait.lock);
LG_LOCK_FREE(instance->eventWait.lock);
LG_RWLOCK_FREE(instance->lock);
free(instance);
*audio = NULL;
}
const LG_AudioOps * spiceAudio_getOps(void)
{
return &l_spiceAudioOps;
}
void spiceAudio_setAvailable(SpiceAudio * audio, bool available)
{
LG_AudioStatusFn callback;
void * callbackOpaque;
LG_AudioStatus status;
LG_LOCK_EXCLUSIVE(audio->lock);
if (audio->available == available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
audio->available = available;
audio->statusGeneration =
nextGeneration(audio->statusGeneration);
if (!available)
{
audio->playback.active = false;
audio->record.active = false;
audio->playbackClockValid = false;
audio->playbackPosition = 0;
audio->playbackClock = (LG_AudioClock) { 0 };
audio->playback.volumeValid = false;
audio->playback.muteValid = false;
audio->record.volumeValid = false;
audio->record.muteValid = false;
}
callback = audio->statusCallback;
callbackOpaque = audio->statusOpaque;
status = (LG_AudioStatus)
{
.available = available,
.generation = audio->statusGeneration,
};
if (callback)
{
atomic_fetch_add_explicit(
&audio->statusInFlight, 1, memory_order_relaxed);
++l_statusDepth;
}
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (callback)
{
callback(callbackOpaque, &status);
--l_statusDepth;
endCallback(&audio->statusInFlight, &audio->statusWait);
}
}
void spiceAudio_playbackStart(SpiceAudio * audio,
int channels, int sampleRate, PSAudioFormat sourceFormat, uint32_t time)
{
LG_AudioFormat format;
if (!makeFormat(channels, sampleRate, sourceFormat, &format))
{
DEBUG_ERROR("Invalid SPICE playback format: %d channels, %d Hz, %d",
channels, sampleRate, sourceFormat);
return;
}
SpiceAudioEventTarget target;
SpiceAudioStream playback;
LG_AudioClock clock;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
audio->playback.active = true;
audio->playback.generation =
nextGeneration(audio->playback.generation);
audio->playback.format = format;
audio->playbackClockValid = false;
audio->playbackPosition = 0;
clock = playbackClockNL(audio, time);
clock.stable = false;
clock.discontinuity = true;
audio->playbackClock = clock;
playback = audio->playback;
dispatch = beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (playbackEventCurrent(audio, &target, playback.generation, true) &&
target.events->playbackStart)
{
target.events->playbackStart(target.opaque,
playback.generation, &playback.format, &clock);
if (playback.volumeValid &&
playbackEventCurrent(audio, &target, playback.generation, true) &&
target.events->playbackVolume)
target.events->playbackVolume(target.opaque,
playback.generation, playback.volumeChannels, playback.volume);
if (playback.muteValid &&
playbackEventCurrent(audio, &target, playback.generation, true) &&
target.events->playbackMute)
target.events->playbackMute(target.opaque,
playback.generation, playback.mute);
}
endEvent(audio);
}
void spiceAudio_playbackStop(SpiceAudio * audio)
{
SpiceAudioEventTarget target;
uint32_t generation;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->playback.active)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
generation = audio->playback.generation;
audio->playback.active = false;
audio->playbackClockValid = false;
dispatch = beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->playbackStop &&
playbackEventCurrent(audio, &target, generation, false))
target.events->playbackStop(target.opaque, generation);
endEvent(audio);
}
void spiceAudio_playbackVolume(SpiceAudio * audio,
int channels, const uint16_t volume[])
{
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume)
return;
SpiceAudioEventTarget target;
uint32_t generation;
uint16_t snapshot[LG_AUDIO_MAX_CHANNELS];
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
memcpy(audio->playback.volume, volume,
(size_t)channels * sizeof(*volume));
audio->playback.volumeChannels = channels;
audio->playback.volumeValid = true;
generation = audio->playback.generation;
memcpy(snapshot, volume, (size_t)channels * sizeof(*volume));
dispatch = audio->playback.active && beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->playbackVolume &&
playbackEventCurrent(audio, &target, generation, true))
target.events->playbackVolume(
target.opaque, generation, channels, snapshot);
endEvent(audio);
}
void spiceAudio_playbackMute(SpiceAudio * audio, bool mute)
{
SpiceAudioEventTarget target;
uint32_t generation;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
audio->playback.mute = mute;
audio->playback.muteValid = true;
generation = audio->playback.generation;
dispatch = audio->playback.active && beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->playbackMute &&
playbackEventCurrent(audio, &target, generation, true))
target.events->playbackMute(target.opaque, generation, mute);
endEvent(audio);
}
void spiceAudio_playbackData(SpiceAudio * audio,
uint8_t * data, size_t size, uint32_t time)
{
SpiceAudioEventTarget target;
uint32_t generation;
size_t frames;
LG_AudioClock clock;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available || !audio->playback.active)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
const size_t bytesPerSample =
sampleSize(audio->playback.format.sampleFormat);
const size_t stride =
bytesPerSample * audio->playback.format.channelCount;
if (!stride || !size || !data || size % stride)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
DEBUG_ERROR("Invalid SPICE playback packet size: %zu", size);
return;
}
frames = size / stride;
generation = audio->playback.generation;
clock = playbackClockNL(audio, time);
audio->playbackPosition += frames;
dispatch = beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->playbackData &&
playbackEventCurrent(audio, &target, generation, true))
target.events->playbackData(target.opaque, generation,
data, frames, &clock);
endEvent(audio);
}
void spiceAudio_recordStart(SpiceAudio * audio,
int channels, int sampleRate, PSAudioFormat sourceFormat)
{
LG_AudioFormat format;
if (!makeFormat(channels, sampleRate, sourceFormat, &format))
{
DEBUG_ERROR("Invalid SPICE record format: %d channels, %d Hz, %d",
channels, sampleRate, sourceFormat);
return;
}
SpiceAudioEventTarget target;
SpiceAudioStream record;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
audio->record.active = true;
audio->record.generation = nextGeneration(audio->record.generation);
audio->record.format = format;
record = audio->record;
dispatch = beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (recordEventCurrent(audio, &target, record.generation, true) &&
target.events->recordStart)
{
target.events->recordStart(
target.opaque, record.generation, &record.format);
if (record.volumeValid &&
recordEventCurrent(audio, &target, record.generation, true) &&
target.events->recordVolume)
target.events->recordVolume(target.opaque, record.generation,
record.volumeChannels, record.volume);
if (record.muteValid &&
recordEventCurrent(audio, &target, record.generation, true) &&
target.events->recordMute)
target.events->recordMute(
target.opaque, record.generation, record.mute);
}
endEvent(audio);
}
void spiceAudio_recordStop(SpiceAudio * audio)
{
SpiceAudioEventTarget target;
uint32_t generation;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->record.active)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
generation = audio->record.generation;
audio->record.active = false;
dispatch = beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->recordStop &&
recordEventCurrent(audio, &target, generation, false))
target.events->recordStop(target.opaque, generation);
endEvent(audio);
}
void spiceAudio_recordVolume(SpiceAudio * audio,
int channels, const uint16_t volume[])
{
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume)
return;
SpiceAudioEventTarget target;
uint32_t generation;
uint16_t snapshot[LG_AUDIO_MAX_CHANNELS];
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
memcpy(audio->record.volume, volume,
(size_t)channels * sizeof(*volume));
audio->record.volumeChannels = channels;
audio->record.volumeValid = true;
generation = audio->record.generation;
memcpy(snapshot, volume, (size_t)channels * sizeof(*volume));
dispatch = audio->record.active && beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->recordVolume &&
recordEventCurrent(audio, &target, generation, true))
target.events->recordVolume(
target.opaque, generation, channels, snapshot);
endEvent(audio);
}
void spiceAudio_recordMute(SpiceAudio * audio, bool mute)
{
SpiceAudioEventTarget target;
uint32_t generation;
bool dispatch;
LG_LOCK_EXCLUSIVE(audio->lock);
if (!audio->available)
{
LG_UNLOCK_EXCLUSIVE(audio->lock);
return;
}
audio->record.mute = mute;
audio->record.muteValid = true;
generation = audio->record.generation;
dispatch = audio->record.active && beginEventNL(audio, &target);
LG_UNLOCK_EXCLUSIVE(audio->lock);
if (!dispatch)
return;
if (target.events->recordMute &&
recordEventCurrent(audio, &target, generation, true))
target.events->recordMute(target.opaque, generation, mute);
endEvent(audio);
}

View File

@@ -0,0 +1,54 @@
/**
* 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_SPICE_AUDIO_
#define _H_LG_CLIENT_TRANSPORT_SPICE_AUDIO_
#include "interface/audio.h"
#include <purespice.h>
typedef struct SpiceAudio SpiceAudio;
bool spiceAudio_init(SpiceAudio ** audio);
void spiceAudio_free(SpiceAudio ** audio);
const LG_AudioOps * spiceAudio_getOps(void);
void spiceAudio_setAvailable(SpiceAudio * audio, bool available);
void spiceAudio_playbackStart(SpiceAudio * audio,
int channels, int sampleRate,
PSAudioFormat format, uint32_t time);
void spiceAudio_playbackStop(SpiceAudio * audio);
void spiceAudio_playbackVolume(SpiceAudio * audio,
int channels, const uint16_t volume[]);
void spiceAudio_playbackMute(SpiceAudio * audio, bool mute);
void spiceAudio_playbackData(SpiceAudio * audio,
uint8_t * data, size_t size, uint32_t time);
void spiceAudio_recordStart(SpiceAudio * audio,
int channels, int sampleRate,
PSAudioFormat format);
void spiceAudio_recordStop(SpiceAudio * audio);
void spiceAudio_recordVolume(SpiceAudio * audio,
int channels, const uint16_t volume[]);
void spiceAudio_recordMute(SpiceAudio * audio, bool mute);
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
/**
* 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_AUDIO_USB_
#define _H_LG_CLIENT_AUDIO_USB_
#include "interface/audio.h"
#include "usbredir.h"
#include <stdint.h>
typedef struct LGA_USBState LGA_USBState;
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);
uint64_t lgaUsb_processDelayNs(const LGA_USBState * state);
extern const LG_AudioOps LGA_USB;
#endif

View File

@@ -0,0 +1,558 @@
/**
* 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 "clipboard.h"
#include "common/debug.h"
#include "common/locking.h"
#include <stdatomic.h>
#include <stdlib.h>
typedef struct ClipboardEventTarget
{
const LG_ClipboardEventOps * events;
void * opaque;
}
ClipboardEventTarget;
typedef struct PendingRequest
{
bool pending;
LG_ClipboardRequest request;
LG_ClipboardData type;
}
PendingRequest;
struct SpiceClipboard
{
LG_Lock stateLock;
LG_Lock eventDispatch;
LG_Lock statusDispatch;
bool available;
uint32_t statusGeneration;
LG_ClipboardStatusFn statusCallback;
void * statusOpaque;
const LG_ClipboardEventOps * events;
void * eventOpaque;
bool remoteNotice;
LG_ClipboardData remoteType;
PendingRequest read;
PendingRequest write;
LG_ClipboardRequest requestSerial;
};
static _Atomic(SpiceClipboard *) l_callbackTarget;
static uint32_t nextGeneration(uint32_t generation)
{
if (++generation == 0)
++generation;
return generation;
}
static LG_ClipboardRequest nextRequestNL(SpiceClipboard * clipboard)
{
if (++clipboard->requestSerial == LG_CLIPBOARD_REQUEST_INVALID)
++clipboard->requestSerial;
return clipboard->requestSerial;
}
static bool spiceType(PSDataType source, LG_ClipboardData * type)
{
switch (source)
{
case SPICE_DATA_TEXT : *type = LG_CLIPBOARD_DATA_TEXT; return true;
case SPICE_DATA_PNG : *type = LG_CLIPBOARD_DATA_PNG ; return true;
case SPICE_DATA_BMP : *type = LG_CLIPBOARD_DATA_BMP ; return true;
case SPICE_DATA_TIFF : *type = LG_CLIPBOARD_DATA_TIFF; return true;
case SPICE_DATA_JPEG : *type = LG_CLIPBOARD_DATA_JPEG; return true;
case SPICE_DATA_NONE : break;
}
return false;
}
static bool lgType(LG_ClipboardData source, PSDataType * type)
{
switch (source)
{
case LG_CLIPBOARD_DATA_TEXT : *type = SPICE_DATA_TEXT; return true;
case LG_CLIPBOARD_DATA_PNG : *type = SPICE_DATA_PNG ; return true;
case LG_CLIPBOARD_DATA_BMP : *type = SPICE_DATA_BMP ; return true;
case LG_CLIPBOARD_DATA_TIFF : *type = SPICE_DATA_TIFF; return true;
case LG_CLIPBOARD_DATA_JPEG : *type = SPICE_DATA_JPEG; return true;
case LG_CLIPBOARD_DATA_NONE : *type = SPICE_DATA_NONE; return true;
}
return false;
}
static void spiceSetStatusListener(void * opaque,
LG_ClipboardStatusFn callback, void * callbackOpaque)
{
SpiceClipboard * clipboard = opaque;
LG_LOCK(clipboard->statusDispatch);
LG_LOCK(clipboard->stateLock);
clipboard->statusCallback = callback;
clipboard->statusOpaque = callbackOpaque;
const LG_ClipboardStatus status =
{
.available = clipboard->available,
.generation = clipboard->statusGeneration,
};
LG_UNLOCK(clipboard->stateLock);
if (callback)
callback(callbackOpaque, &status);
LG_UNLOCK(clipboard->statusDispatch);
}
static bool spiceAttach(void * opaque,
const LG_ClipboardEventOps * events, void * eventOpaque)
{
SpiceClipboard * clipboard = opaque;
if (!events)
return false;
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
if (!clipboard->available)
{
LG_UNLOCK(clipboard->stateLock);
LG_UNLOCK(clipboard->eventDispatch);
return false;
}
clipboard->events = events;
clipboard->eventOpaque = eventOpaque;
const bool notice = clipboard->remoteNotice;
const LG_ClipboardData type = clipboard->remoteType;
LG_UNLOCK(clipboard->stateLock);
if (notice && events->notice)
events->notice(eventOpaque, &type, 1);
LG_UNLOCK(clipboard->eventDispatch);
return true;
}
static void spiceDetach(void * opaque)
{
SpiceClipboard * clipboard = opaque;
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
const bool failWrite = clipboard->available && clipboard->write.pending;
clipboard->events = NULL;
clipboard->eventOpaque = NULL;
clipboard->read = (PendingRequest) { 0 };
clipboard->write = (PendingRequest) { 0 };
LG_UNLOCK(clipboard->stateLock);
if (failWrite)
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
LG_UNLOCK(clipboard->eventDispatch);
}
static bool spiceRelease(void * opaque)
{
SpiceClipboard * clipboard = opaque;
LG_LOCK(clipboard->stateLock);
const bool available = clipboard->available;
const bool failWrite = clipboard->write.pending;
clipboard->write = (PendingRequest) { 0 };
LG_UNLOCK(clipboard->stateLock);
if (!available)
return false;
const bool failed = !failWrite ||
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
return purespice_clipboardRelease() && failed;
}
static bool spiceNotifyTypes(void * opaque,
const LG_ClipboardData types[], size_t count)
{
SpiceClipboard * clipboard = opaque;
if (count == 0)
return spiceRelease(clipboard);
if (!types || count > LG_CLIPBOARD_DATA_NONE)
return false;
PSDataType converted[LG_CLIPBOARD_DATA_NONE];
for (size_t i = 0; i < count; ++i)
if (types[i] == LG_CLIPBOARD_DATA_NONE ||
!lgType(types[i], &converted[i]))
return false;
LG_LOCK(clipboard->stateLock);
const bool available = clipboard->available;
const bool failWrite = clipboard->write.pending;
clipboard->write = (PendingRequest) { 0 };
LG_UNLOCK(clipboard->stateLock);
if (!available)
return false;
const bool failed = !failWrite ||
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
return purespice_clipboardGrab(converted, (int)count) && failed;
}
static bool spiceData(void * opaque, LG_ClipboardRequest request,
LG_ClipboardData type, const void * data, size_t size)
{
SpiceClipboard * clipboard = opaque;
PSDataType converted;
if (request == LG_CLIPBOARD_REQUEST_INVALID || !lgType(type, &converted) ||
(type == LG_CLIPBOARD_DATA_NONE && size != 0) ||
(size && !data))
return false;
LG_LOCK(clipboard->stateLock);
const bool valid = clipboard->available && clipboard->write.pending &&
clipboard->write.request == request &&
(type == LG_CLIPBOARD_DATA_NONE || clipboard->write.type == type);
if (valid)
clipboard->write = (PendingRequest) { 0 };
LG_UNLOCK(clipboard->stateLock);
if (!valid || !purespice_clipboardDataStart(converted, size))
return false;
return !size || purespice_clipboardData(
converted, (uint8_t *)data, size);
}
static bool spiceRequest(void * opaque, LG_ClipboardRequest request,
LG_ClipboardData type)
{
SpiceClipboard * clipboard = opaque;
PSDataType converted;
if (request == LG_CLIPBOARD_REQUEST_INVALID ||
type == LG_CLIPBOARD_DATA_NONE || !lgType(type, &converted))
return false;
LG_LOCK(clipboard->stateLock);
const bool valid = clipboard->available && clipboard->events &&
!clipboard->read.pending;
if (valid)
clipboard->read = (PendingRequest)
{
.pending = true,
.request = request,
.type = type,
};
LG_UNLOCK(clipboard->stateLock);
if (!valid)
return false;
if (purespice_clipboardRequest(converted))
return true;
LG_LOCK(clipboard->stateLock);
if (!clipboard->read.pending || clipboard->read.request != request)
{
LG_UNLOCK(clipboard->stateLock);
return true;
}
clipboard->read = (PendingRequest) { 0 };
LG_UNLOCK(clipboard->stateLock);
return false;
}
static const LG_ClipboardOps l_clipboardOps =
{
.name = "SPICE",
.setStatusListener = spiceSetStatusListener,
.attach = spiceAttach,
.detach = spiceDetach,
.release = spiceRelease,
.notifyTypes = spiceNotifyTypes,
.data = spiceData,
.request = spiceRequest,
};
bool spiceClipboard_init(SpiceClipboard ** clipboard)
{
*clipboard = calloc(1, sizeof(**clipboard));
if (!*clipboard)
return false;
LG_LOCK_INIT((*clipboard)->stateLock);
LG_LOCK_INIT((*clipboard)->eventDispatch);
LG_LOCK_INIT((*clipboard)->statusDispatch);
(*clipboard)->remoteType = LG_CLIPBOARD_DATA_NONE;
return true;
}
void spiceClipboard_free(SpiceClipboard ** clipboard)
{
if (!clipboard || !*clipboard)
return;
SpiceClipboard * expected = *clipboard;
atomic_compare_exchange_strong_explicit(&l_callbackTarget,
&expected, NULL, memory_order_acq_rel, memory_order_acquire);
LG_LOCK_FREE((*clipboard)->statusDispatch);
LG_LOCK_FREE((*clipboard)->eventDispatch);
LG_LOCK_FREE((*clipboard)->stateLock);
free(*clipboard);
*clipboard = NULL;
}
const LG_ClipboardOps * spiceClipboard_getOps(void)
{
return &l_clipboardOps;
}
void spiceClipboard_setAvailable(SpiceClipboard * clipboard, bool available)
{
LG_LOCK(clipboard->statusDispatch);
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
const bool changed = clipboard->available != available;
if (changed)
{
clipboard->available = available;
clipboard->statusGeneration = nextGeneration(
clipboard->statusGeneration);
}
if (!available)
{
clipboard->remoteNotice = false;
clipboard->remoteType = LG_CLIPBOARD_DATA_NONE;
clipboard->read = (PendingRequest) { 0 };
clipboard->write = (PendingRequest) { 0 };
}
const LG_ClipboardStatusFn callback = clipboard->statusCallback;
void * callbackOpaque = clipboard->statusOpaque;
const LG_ClipboardStatus status =
{
.available = available,
.generation = clipboard->statusGeneration,
};
LG_UNLOCK(clipboard->stateLock);
LG_UNLOCK(clipboard->eventDispatch);
if (changed && callback)
callback(callbackOpaque, &status);
LG_UNLOCK(clipboard->statusDispatch);
}
void spiceClipboard_setCallbackTarget(SpiceClipboard * clipboard)
{
atomic_store_explicit(
&l_callbackTarget, clipboard, memory_order_release);
}
static SpiceClipboard * callbackTarget(void)
{
return atomic_load_explicit(&l_callbackTarget, memory_order_acquire);
}
void spiceClipboard_notice(PSDataType source)
{
SpiceClipboard * clipboard = callbackTarget();
if (!clipboard)
return;
LG_ClipboardData type;
if (!spiceType(source, &type))
{
if (source != SPICE_DATA_NONE)
DEBUG_ERROR("Invalid SPICE clipboard notice type: %d", source);
spiceClipboard_release();
return;
}
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
const bool failWrite = clipboard->write.pending;
clipboard->remoteNotice = true;
clipboard->remoteType = type;
clipboard->read = (PendingRequest) { 0 };
clipboard->write = (PendingRequest) { 0 };
const ClipboardEventTarget target =
{
.events = clipboard->available ? clipboard->events : NULL,
.opaque = clipboard->eventOpaque,
};
LG_UNLOCK(clipboard->stateLock);
if (failWrite)
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
if (target.events && target.events->notice)
target.events->notice(target.opaque, &type, 1);
LG_UNLOCK(clipboard->eventDispatch);
}
void spiceClipboard_data(PSDataType source, uint8_t * buffer, uint32_t size)
{
SpiceClipboard * clipboard = callbackTarget();
if (!clipboard)
return;
LG_ClipboardData type = LG_CLIPBOARD_DATA_NONE;
const bool converted = spiceType(source, &type);
const bool validData = source == SPICE_DATA_NONE ||
(converted && (!size || buffer));
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
if (!clipboard->read.pending)
{
LG_UNLOCK(clipboard->stateLock);
LG_UNLOCK(clipboard->eventDispatch);
DEBUG_WARN("Ignoring unsolicited SPICE clipboard data");
return;
}
const PendingRequest request = clipboard->read;
clipboard->read = (PendingRequest) { 0 };
const ClipboardEventTarget target =
{
.events = clipboard->available ? clipboard->events : NULL,
.opaque = clipboard->eventOpaque,
};
LG_UNLOCK(clipboard->stateLock);
if (target.events && target.events->data)
{
if (!validData || (source != SPICE_DATA_NONE && type != request.type))
{
DEBUG_ERROR("Invalid SPICE clipboard response");
target.events->data(target.opaque, request.request,
LG_CLIPBOARD_DATA_NONE, NULL, 0);
}
else
{
if (type == LG_CLIPBOARD_DATA_TEXT && size)
{
uint8_t * output = buffer;
for (uint32_t i = 0; i < size; ++i)
if (buffer[i] != '\r')
*output++ = buffer[i];
size = (uint32_t)(output - buffer);
}
target.events->data(target.opaque, request.request,
type, buffer, size);
}
}
LG_UNLOCK(clipboard->eventDispatch);
}
void spiceClipboard_release(void)
{
SpiceClipboard * clipboard = callbackTarget();
if (!clipboard)
return;
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
clipboard->remoteNotice = false;
clipboard->remoteType = LG_CLIPBOARD_DATA_NONE;
clipboard->read = (PendingRequest) { 0 };
const ClipboardEventTarget target =
{
.events = clipboard->available ? clipboard->events : NULL,
.opaque = clipboard->eventOpaque,
};
LG_UNLOCK(clipboard->stateLock);
if (target.events && target.events->release)
target.events->release(target.opaque);
LG_UNLOCK(clipboard->eventDispatch);
}
void spiceClipboard_request(PSDataType source)
{
SpiceClipboard * clipboard = callbackTarget();
if (!clipboard)
{
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
return;
}
LG_ClipboardData type;
if (!spiceType(source, &type))
{
DEBUG_ERROR("Invalid SPICE clipboard request type: %d", source);
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
return;
}
LG_LOCK(clipboard->eventDispatch);
LG_LOCK(clipboard->stateLock);
const bool busy = clipboard->write.pending;
const ClipboardEventTarget target =
{
.events = clipboard->available && !busy ?
clipboard->events : NULL,
.opaque = clipboard->eventOpaque,
};
LG_ClipboardRequest request = LG_CLIPBOARD_REQUEST_INVALID;
if (target.events)
{
request = nextRequestNL(clipboard);
clipboard->write = (PendingRequest)
{
.pending = true,
.request = request,
.type = type,
};
}
LG_UNLOCK(clipboard->stateLock);
const bool accepted = target.events && target.events->request &&
target.events->request(target.opaque, request, type);
if (!accepted)
{
bool fail = !busy && request == LG_CLIPBOARD_REQUEST_INVALID;
LG_LOCK(clipboard->stateLock);
if (clipboard->write.pending && clipboard->write.request == request)
{
clipboard->write = (PendingRequest) { 0 };
fail = true;
}
LG_UNLOCK(clipboard->stateLock);
if (fail)
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
else if (busy)
DEBUG_WARN("Ignoring overlapping SPICE clipboard request");
}
LG_UNLOCK(clipboard->eventDispatch);
}
void spiceClipboard_status(bool available)
{
SpiceClipboard * clipboard = callbackTarget();
if (clipboard)
spiceClipboard_setAvailable(clipboard, available);
}

View File

@@ -0,0 +1,45 @@
/**
* 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_SPICE_CLIPBOARD_
#define _H_LG_CLIENT_TRANSPORT_SPICE_CLIPBOARD_
#include "interface/clipboard.h"
#include <purespice.h>
typedef struct SpiceClipboard SpiceClipboard;
bool spiceClipboard_init(SpiceClipboard ** clipboard);
void spiceClipboard_free(SpiceClipboard ** clipboard);
const LG_ClipboardOps * spiceClipboard_getOps(void);
void spiceClipboard_setAvailable(SpiceClipboard * clipboard, bool available);
/* PureSpice's clipboard callbacks do not carry an opaque pointer. The active
* callback target must be set before processing the corresponding session. */
void spiceClipboard_setCallbackTarget(SpiceClipboard * clipboard);
void spiceClipboard_notice(PSDataType type);
void spiceClipboard_data(PSDataType type, uint8_t * buffer, uint32_t size);
void spiceClipboard_release(void);
void spiceClipboard_request(PSDataType type);
void spiceClipboard_status(bool available);
#endif

View File

@@ -0,0 +1,215 @@
/**
* 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 "input.h"
#include "kb.h"
#include "common/locking.h"
#include <purespice.h>
#include <stddef.h>
#include <stdlib.h>
struct SpiceInput
{
LG_Lock stateLock;
LG_Lock statusDispatch;
bool available;
uint32_t statusGeneration;
LG_InputStatusFn statusCallback;
void * statusOpaque;
};
static uint32_t nextGeneration(uint32_t generation)
{
if (++generation == 0)
++generation;
return generation;
}
static bool spiceSupports(void * opaque, LG_InputSupport support)
{
(void)opaque;
(void)support;
return false;
}
static void spiceSetStatusListener(void * opaque,
LG_InputStatusFn callback, void * callbackOpaque)
{
SpiceInput * input = opaque;
LG_LOCK(input->statusDispatch);
LG_LOCK(input->stateLock);
input->statusCallback = callback;
input->statusOpaque = callbackOpaque;
const LG_InputStatus status =
{
.available = input->available,
.generation = input->statusGeneration,
};
LG_UNLOCK(input->stateLock);
if (callback)
callback(callbackOpaque, &status);
LG_UNLOCK(input->statusDispatch);
}
static bool spiceKeyDown(void * opaque, int key)
{
SpiceInput * input = opaque;
const uint32_t ps2 = linux_to_ps2[key];
LG_LOCK(input->stateLock);
const bool result = input->available &&
(!ps2 || purespice_keyDown(ps2));
LG_UNLOCK(input->stateLock);
return result;
}
static bool spiceKeyUp(void * opaque, int key)
{
SpiceInput * input = opaque;
const uint32_t ps2 = linux_to_ps2[key];
LG_LOCK(input->stateLock);
const bool result = !ps2 || purespice_keyUp(ps2);
LG_UNLOCK(input->stateLock);
return result;
}
static bool spiceKeyboardLEDs(void * opaque, bool numLock, bool capsLock,
bool scrollLock)
{
SpiceInput * input = opaque;
const uint32_t modifiers =
(scrollLock ? 1 /* SPICE_SCROLL_LOCK_MODIFIER */ : 0) |
(numLock ? 2 /* SPICE_NUM_LOCK_MODIFIER */ : 0) |
(capsLock ? 4 /* SPICE_CAPS_LOCK_MODIFIER */ : 0);
LG_LOCK(input->stateLock);
const bool result = input->available &&
purespice_keyModifiers(modifiers);
LG_UNLOCK(input->stateLock);
return result;
}
static bool spiceMouseMotion(void * opaque, int32_t x, int32_t y)
{
SpiceInput * input = opaque;
LG_LOCK(input->stateLock);
const bool result = input->available && purespice_mouseMotion(x, y);
LG_UNLOCK(input->stateLock);
return result;
}
static bool spiceMousePress(void * opaque, unsigned int button)
{
if (button > 7)
return true;
SpiceInput * input = opaque;
LG_LOCK(input->stateLock);
const bool result = input->available && purespice_mousePress(button);
LG_UNLOCK(input->stateLock);
return result;
}
static bool spiceMouseRelease(void * opaque, unsigned int button)
{
if (button > 7)
return true;
SpiceInput * input = opaque;
LG_LOCK(input->stateLock);
const bool result = purespice_mouseRelease(button);
LG_UNLOCK(input->stateLock);
return result;
}
static const LG_InputOps l_inputOps =
{
.name = "SPICE",
.supports = spiceSupports,
.setStatusListener = spiceSetStatusListener,
.keyDown = spiceKeyDown,
.keyUp = spiceKeyUp,
.keyboardLEDs = spiceKeyboardLEDs,
.mouseMotion = spiceMouseMotion,
.mousePosition = NULL,
.mousePress = spiceMousePress,
.mouseRelease = spiceMouseRelease,
.reset = NULL,
};
bool spiceInput_init(SpiceInput ** input)
{
*input = calloc(1, sizeof(**input));
if (!*input)
return false;
LG_LOCK_INIT((*input)->stateLock);
LG_LOCK_INIT((*input)->statusDispatch);
return true;
}
void spiceInput_free(SpiceInput ** input)
{
if (!input || !*input)
return;
LG_LOCK_FREE((*input)->statusDispatch);
LG_LOCK_FREE((*input)->stateLock);
free(*input);
*input = NULL;
}
const LG_InputOps * spiceInput_getOps(void)
{
return &l_inputOps;
}
void spiceInput_setAvailable(SpiceInput * input, bool available)
{
LG_LOCK(input->statusDispatch);
LG_LOCK(input->stateLock);
const bool changed = input->available != available;
if (changed)
{
input->available = available;
input->statusGeneration = nextGeneration(input->statusGeneration);
}
const LG_InputStatusFn callback = input->statusCallback;
void * callbackOpaque = input->statusOpaque;
const LG_InputStatus status =
{
.available = input->available,
.generation = input->statusGeneration,
};
LG_UNLOCK(input->stateLock);
if (changed && callback)
callback(callbackOpaque, &status);
LG_UNLOCK(input->statusDispatch);
}

View File

@@ -0,0 +1,34 @@
/**
* 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_SPICE_INPUT_
#define _H_LG_CLIENT_TRANSPORT_SPICE_INPUT_
#include "interface/input.h"
typedef struct SpiceInput SpiceInput;
bool spiceInput_init(SpiceInput ** input);
void spiceInput_free(SpiceInput ** input);
const LG_InputOps * spiceInput_getOps(void);
void spiceInput_setAvailable(SpiceInput * input, bool available);
#endif

View File

@@ -0,0 +1,404 @@
/**
* 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 "spice.h"
#include "common/debug.h"
#include "common/time.h"
#include <purespice.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#define USB_REDIR_DISCONNECT_TIMEOUT_US UINT64_C(500000)
static _Atomic(LG_Transport *) l_callbackTarget;
static LG_Transport * callbackTarget(void)
{
return atomic_load_explicit(&l_callbackTarget, memory_order_acquire);
}
static void ready(void)
{
LG_Transport * transport = callbackTarget();
if (!transport)
return;
snprintf(transport->session.version,
sizeof(transport->session.version), "SPICE");
transport->session.os = LG_TRANSPORT_OS_OTHER;
purespice_mouseMode(true);
PSServerInfo info;
if (purespice_getServerInfo(&info))
{
if (info.name)
snprintf(transport->session.name,
sizeof(transport->session.name), "%s", info.name);
for (unsigned int i = 0; i < sizeof(info.uuid); ++i)
if (info.uuid[i])
{
transport->session.uuidValid = true;
memcpy(transport->session.uuid, info.uuid, sizeof(info.uuid));
break;
}
purespice_freeServerInfo(&info);
}
else
DEBUG_WARN("Failed to obtain SPICE server information");
if (transport->input)
spiceInput_setAvailable(transport->input, true);
#if ENABLE_AUDIO
if (transport->audio && !transport->usbAudioEnabled)
spiceAudio_setAvailable(transport->audio, true);
#endif
transport->connectStatus = LG_TRANSPORT_OK;
atomic_store_explicit(
&transport->sessionValid, true, memory_order_release);
lgSignalEvent(transport->connectEvent);
}
static void surfaceCreate(unsigned int surfaceId, PSSurfaceFormat format,
unsigned int width, unsigned int height)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_create(
transport->surface, surfaceId, format, width, height);
}
static void surfaceDestroy(unsigned int surfaceId)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_destroy(transport->surface, surfaceId);
}
static void drawFill(unsigned int surfaceId, int x, int y,
int width, int height, uint32_t color)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_drawFill(
transport->surface, surfaceId, x, y, width, height, color);
}
static void drawBitmap(unsigned int surfaceId, PSBitmapFormat format,
bool topDown, int x, int y, int width, int height, int stride, void * data)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_drawBitmap(transport->surface, surfaceId, format,
topDown, x, y, width, height, stride, data);
}
static void setRGBAImage(int width, int height, int hx, int hy,
const void * data)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_setRGBAImage(
transport->surface, width, height, hx, hy, data);
}
static void setMonoImage(int width, int height, int hx, int hy,
const void * xorMask, const void * andMask)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_setMonoImage(transport->surface,
width, height, hx, hy, xorMask, andMask);
}
static void setColorImage(int width, int height, int hx, int hy,
const void * data, const void * maskData)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_setColorImage(transport->surface,
width, height, hx, hy, data, maskData);
}
static void setPointerState(bool visible, int x, int y)
{
LG_Transport * transport = callbackTarget();
if (transport)
spiceSurface_setPointerState(transport->surface, visible, x, y);
}
#if ENABLE_AUDIO
static void playbackStart(int channels, int sampleRate,
PSAudioFormat format, uint32_t time)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_playbackStart(
transport->audio, channels, sampleRate, format, time);
}
static void playbackVolume(int channels, const uint16_t volume[])
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_playbackVolume(transport->audio, channels, volume);
}
static void playbackMute(bool mute)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_playbackMute(transport->audio, mute);
}
static void playbackStop(void)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_playbackStop(transport->audio);
}
static void playbackData(uint8_t * data, size_t size, uint32_t time)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_playbackData(transport->audio, data, size, time);
}
static void recordStart(int channels, int sampleRate, PSAudioFormat format)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_recordStart(
transport->audio, channels, sampleRate, format);
}
static void recordVolume(int channels, const uint16_t volume[])
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_recordVolume(transport->audio, channels, volume);
}
static void recordMute(bool mute)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_recordMute(transport->audio, mute);
}
static void recordStop(void)
{
LG_Transport * transport = callbackTarget();
if (transport && transport->audio)
spiceAudio_recordStop(transport->audio);
}
#endif
static void disconnectProviders(LG_Transport * transport)
{
atomic_store_explicit(
&transport->sessionValid, false, memory_order_release);
if (transport->input)
spiceInput_setAvailable(transport->input, false);
#if ENABLE_AUDIO
if (transport->audio)
spiceAudio_setAvailable(transport->audio, false);
#endif
if (transport->clipboard)
spiceClipboard_setAvailable(transport->clipboard, false);
spiceSurface_sessionStopped(transport->surface);
}
int spiceSession_thread(void * opaque)
{
LG_Transport * transport = opaque;
LG_Transport * expected = NULL;
if (!atomic_compare_exchange_strong_explicit(&l_callbackTarget,
&expected, transport, memory_order_acq_rel, memory_order_acquire))
{
DEBUG_ERROR("A SPICE session is already active");
lgSignalEvent(transport->connectEvent);
return 0;
}
if (transport->clipboard)
spiceClipboard_setCallbackTarget(transport->clipboard);
const PSConfig config =
{
.host = transport->host,
.port = transport->port,
.password = "",
.ready = ready,
.inputs =
{
.enable = transport->inputEnabled,
.autoConnect = true,
},
.clipboard =
{
.enable = transport->clipboardEnabled,
.notice = spiceClipboard_notice,
.data = spiceClipboard_data,
.release = spiceClipboard_release,
.request = spiceClipboard_request,
.status = spiceClipboard_status,
},
.display =
{
.enable = true,
.autoConnect = false,
.surfaceCreate = surfaceCreate,
.surfaceDestroy = surfaceDestroy,
.drawFill = drawFill,
.drawBitmap = drawBitmap,
},
.cursor =
{
.enable = true,
.autoConnect = false,
.setRGBAImage = setRGBAImage,
.setMonoImage = setMonoImage,
.setColorImage = setColorImage,
.setState = setPointerState,
},
#if ENABLE_AUDIO
.playback =
{
.enable = transport->playbackEnabled &&
!transport->usbAudioEnabled,
.autoConnect = true,
.start = playbackStart,
.volume = playbackVolume,
.mute = playbackMute,
.stop = playbackStop,
.data = playbackData,
},
.record =
{
.enable = transport->recordEnabled &&
!transport->usbAudioEnabled,
.autoConnect = true,
.start = recordStart,
.volume = recordVolume,
.mute = recordMute,
.stop = recordStop,
},
#endif
#if ENABLE_USB_AUDIO
.usbRedir =
{
.enable = transport->usbAudio != NULL,
.autoConnect = false,
.opaque = transport->usbRedir,
.state = lgUsbRedir_state,
.data = lgUsbRedir_data,
},
#endif
};
PSStatus status = PS_STATUS_SHUTDOWN;
if (!purespice_connect(&config))
{
DEBUG_ERROR("Failed to connect to SPICE server");
goto done;
}
transport->connected = true;
status = PS_STATUS_RUN;
int processTimeout = 100;
while (!atomic_load_explicit(&transport->stop, memory_order_acquire))
{
#if ENABLE_USB_AUDIO
if (transport->usbRedir && !lgUsbRedir_process(transport->usbRedir))
DEBUG_WARN("Failed to process USB audio redirection");
if (transport->usbAudio)
{
const uint64_t delay = lgaUsb_processDelayNs(transport->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
status = purespice_process(processTimeout);
if (status != PS_STATUS_RUN)
{
if (status != PS_STATUS_SHUTDOWN)
DEBUG_ERROR("Failed to process SPICE messages");
break;
}
}
disconnectProviders(transport);
#if ENABLE_USB_AUDIO
if (status == PS_STATUS_RUN && transport->usbRedir)
{
if (!lgUsbRedir_process(transport->usbRedir))
DEBUG_WARN("Failed to disconnect USB audio device");
else
{
const uint64_t deadline =
microtime() + USB_REDIR_DISCONNECT_TIMEOUT_US;
while (lgUsbRedir_disconnectPending(transport->usbRedir) &&
microtime() < deadline)
{
status = purespice_process(10);
if (status != PS_STATUS_RUN)
break;
}
if (status == PS_STATUS_RUN &&
lgUsbRedir_disconnectPending(transport->usbRedir))
DEBUG_WARN("Timed out disconnecting USB audio device");
}
}
#endif
purespice_disconnect();
transport->connected = false;
done:
disconnectProviders(transport);
if (transport->clipboard)
spiceClipboard_setCallbackTarget(NULL);
expected = transport;
atomic_compare_exchange_strong_explicit(&l_callbackTarget,
&expected, NULL, memory_order_acq_rel, memory_order_acquire);
lgSignalEvent(transport->connectEvent);
return 0;
}

View File

@@ -0,0 +1,277 @@
/**
* 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 "spice.h"
#include "common/debug.h"
#include "common/option.h"
#if ENABLE_AUDIO
#include "../../src/audio.h"
#endif
#include <purespice.h>
#include <stdlib.h>
#include <string.h>
static void spiceDisconnect(LG_Transport * transport);
static void spiceSetup(void)
{
const PSInit init =
{
.log =
{
.info = debug_info,
.warn = debug_warn,
.error = debug_error,
},
};
purespice_init(&init);
}
static bool spiceCreate(LG_Transport ** result)
{
if (!result)
return false;
LG_Transport * transport = calloc(1, sizeof(*transport));
if (!transport)
return false;
transport->host = option_get_string("spice", "host");
transport->port = option_get_int("spice", "port");
transport->inputEnabled = option_get_bool("spice", "input");
transport->clipboardEnabled = option_get_bool("spice", "clipboard");
transport->audioEnabled = option_get_bool("spice", "audio");
transport->usbAudioEnabled =
option_get_bool("spice", "usbAudio");
transport->audioDebug = option_get_bool("audio", "debug");
#if ENABLE_AUDIO
transport->playbackEnabled =
transport->audioEnabled && lgAudio_supportsPlayback();
transport->recordEnabled =
transport->audioEnabled && lgAudio_supportsRecord();
#else
transport->audioEnabled = false;
transport->usbAudioEnabled = false;
#endif
#if !ENABLE_USB_AUDIO
transport->usbAudioEnabled = false;
#endif
if (!spiceSurface_init(&transport->surface) ||
(transport->inputEnabled &&
!spiceInput_init(&transport->input)) ||
(transport->clipboardEnabled &&
!spiceClipboard_init(&transport->clipboard)))
goto fail;
#if ENABLE_AUDIO
if (transport->audioEnabled && !spiceAudio_init(&transport->audio))
goto fail;
#endif
#if ENABLE_USB_AUDIO
if (transport->audioEnabled && transport->usbAudioEnabled)
{
if (!transport->playbackEnabled)
{
DEBUG_WARN("USB audio requires a playback backend, using SPICE audio");
transport->usbAudioEnabled = false;
}
else if (!(transport->usbAudio =
lgaUsb_create(transport->audioDebug)))
{
DEBUG_WARN("Failed to initialize USB audio, using SPICE audio");
transport->usbAudioEnabled = false;
}
else
transport->usbRedir = lgaUsb_redir(transport->usbAudio);
}
#endif
transport->connectEvent = lgCreateEvent(false, 0);
if (!transport->connectEvent)
goto fail;
atomic_init(&transport->stop, false);
atomic_init(&transport->sessionValid, false);
*result = transport;
return true;
fail:
if (transport->connectEvent)
lgFreeEvent(transport->connectEvent);
#if ENABLE_USB_AUDIO
lgaUsb_destroy(transport->usbAudio);
#endif
#if ENABLE_AUDIO
spiceAudio_free(&transport->audio);
#endif
spiceClipboard_free(&transport->clipboard);
spiceInput_free(&transport->input);
spiceSurface_free(&transport->surface);
free(transport);
return false;
}
static void spiceDestroy(LG_Transport ** transport)
{
if (!transport || !*transport)
return;
spiceDisconnect(*transport);
lgFreeEvent((*transport)->connectEvent);
#if ENABLE_USB_AUDIO
lgaUsb_destroy((*transport)->usbAudio);
#endif
#if ENABLE_AUDIO
spiceAudio_free(&(*transport)->audio);
#endif
spiceClipboard_free(&(*transport)->clipboard);
spiceInput_free(&(*transport)->input);
spiceSurface_free(&(*transport)->surface);
free(*transport);
*transport = NULL;
}
static LG_TransportStatus spiceConnect(LG_Transport * transport,
LG_TransportSession * session)
{
if (transport->thread)
return LG_TRANSPORT_ERROR;
memset(&transport->session, 0, sizeof(transport->session));
transport->connectStatus = LG_TRANSPORT_ERROR;
atomic_store_explicit(&transport->stop, false, memory_order_release);
atomic_store_explicit(
&transport->sessionValid, false, memory_order_release);
lgResetEvent(transport->connectEvent);
if (!lgCreateThread(
"spiceProcess", spiceSession_thread, transport, &transport->thread))
return LG_TRANSPORT_ERROR;
lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE);
if (transport->connectStatus != LG_TRANSPORT_OK)
{
lgJoinThread(transport->thread, NULL);
transport->thread = NULL;
return transport->connectStatus;
}
*session = transport->session;
return LG_TRANSPORT_OK;
}
static void spiceDisconnect(LG_Transport * transport)
{
if (!transport->thread)
return;
atomic_store_explicit(&transport->stop, true, memory_order_release);
lgJoinThread(transport->thread, NULL);
transport->thread = NULL;
}
static bool spiceSessionValid(LG_Transport * transport)
{
return atomic_load_explicit(
&transport->sessionValid, memory_order_acquire);
}
static const LG_VideoOps * spiceGetVideoOps(LG_Transport * transport)
{
(void)transport;
return spiceSurface_getVideoOps();
}
static const LG_InputOps * spiceGetInputOps(LG_Transport * transport,
void ** opaque)
{
*opaque = transport->input;
return transport->input ? spiceInput_getOps() : NULL;
}
static const LG_AudioOps * spiceGetAudioOps(LG_Transport * transport,
void ** opaque)
{
#if ENABLE_USB_AUDIO
if (transport->usbAudio)
{
*opaque = transport->usbAudio;
return &LGA_USB;
}
#endif
#if ENABLE_AUDIO
*opaque = transport->audio;
return transport->audio ? spiceAudio_getOps() : NULL;
#else
*opaque = NULL;
return NULL;
#endif
}
static const LG_ClipboardOps * spiceGetClipboardOps(LG_Transport * transport,
void ** opaque)
{
*opaque = transport->clipboard;
return transport->clipboard ? spiceClipboard_getOps() : NULL;
}
static LG_TransportStatus spiceSendControl(LG_Transport * transport,
const LG_TransportControl * control, LG_TransportControlToken * token)
{
(void)control;
(void)token;
return spiceSessionValid(transport) ?
LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED;
}
static LG_TransportStatus spiceControlStatus(LG_Transport * transport,
LG_TransportControlToken token)
{
(void)token;
return spiceSessionValid(transport) ?
LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED;
}
const LG_TransportOps LGT_SPICE =
{
.name = "spice",
.setup = spiceSetup,
.create = spiceCreate,
.destroy = spiceDestroy,
.connect = spiceConnect,
.disconnect = spiceDisconnect,
.sessionValid = spiceSessionValid,
.getVideoOps = spiceGetVideoOps,
.getInputOps = spiceGetInputOps,
.getAudioOps = spiceGetAudioOps,
.getClipboardOps = spiceGetClipboardOps,
.sendControl = spiceSendControl,
.controlStatus = spiceControlStatus,
};

View File

@@ -0,0 +1,77 @@
/**
* 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_SPICE_
#define _H_LG_CLIENT_TRANSPORT_SPICE_
#include "interface/transport.h"
#include "clipboard.h"
#include "input.h"
#include "surface.h"
#if ENABLE_AUDIO
#include "audio.h"
#endif
#if ENABLE_USB_AUDIO
#include "audio_usb.h"
#endif
#include "common/event.h"
#include "common/thread.h"
#include <stdatomic.h>
struct LG_Transport
{
const char * host;
unsigned int port;
bool inputEnabled;
bool clipboardEnabled;
bool audioEnabled;
bool usbAudioEnabled;
bool playbackEnabled;
bool recordEnabled;
bool audioDebug;
SpiceInput * input;
SpiceClipboard * clipboard;
SpiceSurface * surface;
#if ENABLE_AUDIO
SpiceAudio * audio;
#endif
#if ENABLE_USB_AUDIO
LGA_USBState * usbAudio;
LG_USBRedir * usbRedir;
#endif
LGThread * thread;
LGEvent * connectEvent;
atomic_bool stop;
atomic_bool sessionValid;
bool connected;
LG_TransportStatus connectStatus;
LG_TransportSession session;
};
int spiceSession_thread(void * opaque);
#endif

View File

@@ -0,0 +1,394 @@
/**
* 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 "surface.h"
#include "spice.h"
#include "common/debug.h"
#include "common/locking.h"
#include <stdlib.h>
#include <string.h>
struct SpiceSurface
{
LG_RWLock eventsLock;
LG_Lock activationLock;
const LG_SwSurfaceEventOps * events;
void * eventOpaque;
bool active;
bool primaryValid;
int hotX;
int hotY;
};
static bool surfaceAttach(LG_Transport * transport,
const LG_SwSurfaceEventOps * events, void * opaque)
{
if (!events || !events->configure || !events->destroy ||
!events->drawFill || !events->drawBitmap || !events->pointer)
return false;
SpiceSurface * surface = transport->surface;
LG_LOCK_EXCLUSIVE(surface->eventsLock);
surface->events = events;
surface->eventOpaque = opaque;
LG_UNLOCK_EXCLUSIVE(surface->eventsLock);
return true;
}
static void surfaceDetach(LG_Transport * transport)
{
SpiceSurface * surface = transport->surface;
LG_LOCK_EXCLUSIVE(surface->eventsLock);
surface->events = NULL;
surface->eventOpaque = NULL;
LG_UNLOCK_EXCLUSIVE(surface->eventsLock);
}
static bool surfaceSetActive(LG_Transport * transport, bool active)
{
SpiceSurface * surface = transport->surface;
LG_LOCK(surface->activationLock);
if (surface->active == active)
{
LG_UNLOCK(surface->activationLock);
return true;
}
bool result = false;
if (active)
{
LG_LOCK_SHARED(surface->eventsLock);
const bool attached = surface->events != NULL;
LG_UNLOCK_SHARED(surface->eventsLock);
if (!attached)
goto done;
if (!atomic_load_explicit(
&transport->sessionValid, memory_order_acquire) ||
!purespice_hasChannel(PS_CHANNEL_DISPLAY) ||
!purespice_hasChannel(PS_CHANNEL_CURSOR) ||
!purespice_connectChannel(PS_CHANNEL_DISPLAY))
goto done;
if (!purespice_connectChannel(PS_CHANNEL_CURSOR))
{
purespice_disconnectChannel(PS_CHANNEL_DISPLAY);
goto done;
}
}
else
{
if (!purespice_disconnectChannel(PS_CHANNEL_DISPLAY))
goto done;
if (!purespice_disconnectChannel(PS_CHANNEL_CURSOR))
{
purespice_connectChannel(PS_CHANNEL_DISPLAY);
goto done;
}
}
surface->active = active;
result = true;
done:
LG_UNLOCK(surface->activationLock);
return result;
}
static const LG_SwSurfaceOps swSurfaceOps =
{
.attach = surfaceAttach,
.detach = surfaceDetach,
.setActive = surfaceSetActive,
};
static const LG_VideoOps videoOps =
{
.name = "SPICE",
.type = LG_VIDEO_TYPE_SW_SURFACE,
.swSurface = &swSurfaceOps,
};
bool spiceSurface_init(SpiceSurface ** result)
{
if (!result)
return false;
SpiceSurface * surface = calloc(1, sizeof(*surface));
if (!surface)
return false;
LG_RWLOCK_INIT(surface->eventsLock);
LG_LOCK_INIT(surface->activationLock);
*result = surface;
return true;
}
void spiceSurface_free(SpiceSurface ** surface)
{
if (!surface || !*surface)
return;
LG_LOCK_FREE((*surface)->activationLock);
LG_RWLOCK_FREE((*surface)->eventsLock);
free(*surface);
*surface = NULL;
}
const LG_VideoOps * spiceSurface_getVideoOps(void)
{
return &videoOps;
}
void spiceSurface_sessionStopped(SpiceSurface * surface)
{
LG_LOCK(surface->activationLock);
surface->active = false;
surface->primaryValid = false;
LG_UNLOCK(surface->activationLock);
}
void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId,
PSSurfaceFormat format, unsigned int width, unsigned int height)
{
if (surfaceId != 0)
{
DEBUG_INFO("Ignoring secondary SPICE surface: id: %u, size: %ux%u",
surfaceId, width, height);
return;
}
switch (format)
{
case PS_SURFACE_FMT_32_xRGB:
case PS_SURFACE_FMT_32_ARGB:
break;
default:
DEBUG_ERROR("Unsupported primary SPICE surface format: %d", format);
surface->primaryValid = false;
return;
}
DEBUG_INFO("Create primary SPICE surface: id: %u, size: %ux%u",
surfaceId, width, height);
surface->primaryValid = true;
LG_LOCK_SHARED(surface->eventsLock);
if (surface->events)
{
surface->events->configure(surface->eventOpaque, width, height);
surface->events->drawFill(
surface->eventOpaque, 0, 0, width, height, 0);
}
LG_UNLOCK_SHARED(surface->eventsLock);
}
void spiceSurface_destroy(SpiceSurface * surface, unsigned int surfaceId)
{
if (!surface->primaryValid || surfaceId != 0)
{
DEBUG_INFO("Ignoring destruction of inactive or secondary SPICE surface %u",
surfaceId);
return;
}
DEBUG_INFO("Destroy primary SPICE surface %u", surfaceId);
surface->primaryValid = false;
LG_LOCK_SHARED(surface->eventsLock);
if (surface->events)
surface->events->destroy(surface->eventOpaque);
LG_UNLOCK_SHARED(surface->eventsLock);
}
void spiceSurface_drawFill(SpiceSurface * surface, unsigned int surfaceId,
int x, int y, int width, int height, uint32_t color)
{
if (!surface->primaryValid || surfaceId != 0)
return;
LG_LOCK_SHARED(surface->eventsLock);
if (surface->events)
surface->events->drawFill(
surface->eventOpaque, x, y, width, height, color);
LG_UNLOCK_SHARED(surface->eventsLock);
}
void spiceSurface_drawBitmap(SpiceSurface * surface, unsigned int surfaceId,
PSBitmapFormat format, bool topDown, int x, int y,
int width, int height, int stride, void * data)
{
if (!surface->primaryValid || surfaceId != 0)
return;
switch (format)
{
case PS_BITMAP_FMT_32BIT:
case PS_BITMAP_FMT_RGBA:
break;
default:
DEBUG_ERROR("Unsupported SPICE bitmap format: %d", format);
return;
}
LG_LOCK_SHARED(surface->eventsLock);
if (surface->events)
surface->events->drawBitmap(surface->eventOpaque, topDown,
x, y, width, height, stride, data);
LG_UNLOCK_SHARED(surface->eventsLock);
}
static void pointerEvent(SpiceSurface * surface,
const LG_TransportPointer * pointer)
{
LG_LOCK_SHARED(surface->eventsLock);
if (surface->events)
surface->events->pointer(surface->eventOpaque, pointer);
LG_UNLOCK_SHARED(surface->eventsLock);
}
void spiceSurface_setRGBAImage(SpiceSurface * surface,
int width, int height, int hx, int hy, const void * data)
{
surface->hotX = hx;
surface->hotY = hy;
const uint8_t * rgba = data;
uint8_t * bgra = malloc((size_t)width * height * 4);
if (!bgra)
return;
for (int i = 0; i < width * height; ++i)
{
bgra[i * 4 + 0] = rgba[i * 4 + 2];
bgra[i * 4 + 1] = rgba[i * 4 + 1];
bgra[i * 4 + 2] = rgba[i * 4 + 0];
bgra[i * 4 + 3] = rgba[i * 4 + 3];
}
const LG_TransportPointer pointer =
{
.flags = LG_TRANSPORT_POINTER_SHAPE,
.type = CURSOR_TYPE_COLOR,
.hx = hx,
.hy = hy,
.width = width,
.height = height,
.pitch = width * 4,
.shape = bgra,
};
pointerEvent(surface, &pointer);
free(bgra);
}
void spiceSurface_setMonoImage(SpiceSurface * surface,
int width, int height, int hx, int hy,
const void * xorMask, const void * andMask)
{
surface->hotX = hx;
surface->hotY = hy;
const int stride = (width + 7) / 8;
uint8_t * buffer = malloc((size_t)stride * height * 2);
if (!buffer)
return;
memcpy(buffer, andMask, (size_t)stride * height);
memcpy(buffer + (size_t)stride * height,
xorMask, (size_t)stride * height);
const LG_TransportPointer pointer =
{
.flags = LG_TRANSPORT_POINTER_SHAPE,
.type = CURSOR_TYPE_MONOCHROME,
.hx = hx,
.hy = hy,
.width = width,
.height = height * 2,
.pitch = stride,
.shape = buffer,
};
pointerEvent(surface, &pointer);
free(buffer);
}
void spiceSurface_setColorImage(SpiceSurface * surface,
int width, int height, int hx, int hy,
const void * data, const void * maskData)
{
surface->hotX = hx;
surface->hotY = hy;
const uint8_t * rgba = data;
const uint8_t * andMask = maskData;
const int maskPitch = (width + 7) / 8;
uint8_t * bgra = malloc((size_t)width * height * 4);
if (!bgra)
return;
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x)
{
const int i = y * width + x;
bgra[i * 4 + 0] = rgba[i * 4 + 2];
bgra[i * 4 + 1] = rgba[i * 4 + 1];
bgra[i * 4 + 2] = rgba[i * 4 + 0];
bgra[i * 4 + 3] = andMask[y * maskPitch + x / 8] &
(0x80U >> (x % 8)) ? 255 : 0;
}
const LG_TransportPointer pointer =
{
.flags = LG_TRANSPORT_POINTER_SHAPE,
.type = CURSOR_TYPE_MASKED_COLOR,
.hx = hx,
.hy = hy,
.width = width,
.height = height,
.pitch = width * 4,
.shape = bgra,
};
pointerEvent(surface, &pointer);
free(bgra);
}
void spiceSurface_setPointerState(SpiceSurface * surface,
bool visible, int x, int y)
{
const LG_TransportPointer pointer =
{
.flags = LG_TRANSPORT_POINTER_POSITION |
LG_TRANSPORT_POINTER_VISIBLE_VALID |
(visible ? LG_TRANSPORT_POINTER_VISIBLE : 0),
.x = x,
.y = y,
.hx = surface->hotX,
.hy = surface->hotY,
};
pointerEvent(surface, &pointer);
}

View File

@@ -0,0 +1,56 @@
/**
* 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_SPICE_SURFACE_
#define _H_LG_CLIENT_TRANSPORT_SPICE_SURFACE_
#include "interface/transport.h"
#include <purespice.h>
typedef struct SpiceSurface SpiceSurface;
bool spiceSurface_init(SpiceSurface ** surface);
void spiceSurface_free(SpiceSurface ** surface);
const LG_VideoOps * spiceSurface_getVideoOps(void);
void spiceSurface_sessionStopped(SpiceSurface * surface);
void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId,
PSSurfaceFormat format, unsigned int width, unsigned int height);
void spiceSurface_destroy(SpiceSurface * surface, unsigned int surfaceId);
void spiceSurface_drawFill(SpiceSurface * surface, unsigned int surfaceId,
int x, int y, int width, int height, uint32_t color);
void spiceSurface_drawBitmap(SpiceSurface * surface, unsigned int surfaceId,
PSBitmapFormat format, bool topDown, int x, int y,
int width, int height, int stride, void * data);
void spiceSurface_setRGBAImage(SpiceSurface * surface,
int width, int height, int hx, int hy, const void * data);
void spiceSurface_setMonoImage(SpiceSurface * surface,
int width, int height, int hx, int hy,
const void * xorMask, const void * andMask);
void spiceSurface_setColorImage(SpiceSurface * surface,
int width, int height, int hx, int hy,
const void * data, const void * maskData);
void spiceSurface_setPointerState(SpiceSurface * surface,
bool visible, int x, int y);
#endif

View File

@@ -0,0 +1,466 @@
/**
* 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 "usbredir.h"
#include "common/debug.h"
#include "common/time.h"
#include <usbredirparser.h>
#include <stdatomic.h>
#include <stdlib.h>
#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
{
const LG_USBRedirDeviceOps * deviceOps;
void * deviceOpaque;
LG_USBRedirStatusFn status;
void * statusOpaque;
PSUSBRedirChannel * channels[USB_REDIR_CHANNEL_COUNT];
PSUSBRedirChannel * channel;
struct usbredirparser * parser;
const uint8_t * readData;
size_t readSize;
size_t readOffset;
atomic_bool desiredPlugged;
atomic_bool available;
bool plugged;
bool disconnectPending;
int64_t disconnectDeadline;
int64_t reconnectDeadline;
};
static void setAvailable(LG_USBRedir * usbredir, bool available)
{
const bool previous = atomic_exchange_explicit(
&usbredir->available, available, memory_order_acq_rel);
if (previous != available && usbredir->status)
usbredir->status(usbredir->statusOpaque, available);
}
static int readUSBRedir(void * opaque, uint8_t * data, int count)
{
LG_USBRedir * usbredir = opaque;
const size_t remaining = usbredir->readSize - usbredir->readOffset;
if (remaining < (size_t)count)
count = (int)remaining;
if (!count)
return 0;
memcpy(data, usbredir->readData + usbredir->readOffset, count);
usbredir->readOffset += count;
return count;
}
static int writeUSBRedir(void * opaque, uint8_t * data, int count)
{
LG_USBRedir * usbredir = opaque;
if (!usbredir->channel ||
!purespice_usbRedirWrite(usbredir->channel, data, count))
return -1;
return count;
}
static void logUSBRedir(void * opaque, int level, const char * message)
{
(void)opaque;
switch (level)
{
case usbredirparser_error:
DEBUG_ERROR("USB redirection: %s", message);
break;
case usbredirparser_warning:
DEBUG_WARN("USB redirection: %s", message);
break;
case usbredirparser_info:
DEBUG_INFO("USB redirection: %s", message);
break;
case usbredirparser_debug:
case usbredirparser_debug_data:
DEBUG_TRACE("USB redirection: %s", message);
break;
case usbredirparser_none:
break;
}
}
static void helloUSBRedir(void * opaque,
struct usb_redir_hello_header * hello)
{
(void)hello;
LG_USBRedir * usbredir = opaque;
setAvailable(usbredir, true);
}
static void deviceDisconnectAck(void * opaque)
{
LG_USBRedir * usbredir = opaque;
usbredir->disconnectPending = false;
usbredir->disconnectDeadline = 0;
}
static void unplugDevice(LG_USBRedir * usbredir)
{
if (!usbredir->plugged)
return;
usbredir->deviceOps->unplug(usbredir->deviceOpaque);
usbredir->plugged = false;
}
static void destroyParser(LG_USBRedir * usbredir)
{
setAvailable(usbredir, false);
unplugDevice(usbredir);
usbredir->disconnectPending = false;
usbredir->disconnectDeadline = 0;
if (!usbredir->parser)
return;
usbredirparser_destroy(usbredir->parser);
usbredir->parser = NULL;
}
static bool flushUSBRedir(LG_USBRedir * usbredir)
{
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,
PSUSBRedirChannel * channel)
{
usbredir->channel = channel;
if (purespice_usbRedirConnect(channel))
return true;
DEBUG_WARN("Failed to connect USB redirection channel %u",
purespice_usbRedirChannelId(channel));
usbredir->channel = NULL;
return false;
}
static bool selectChannel(LG_USBRedir * usbredir)
{
if (usbredir->channel)
return true;
for (unsigned int i = 0; i < USB_REDIR_CHANNEL_COUNT; ++i)
{
PSUSBRedirChannel * channel = usbredir->channels[i];
if (!channel || !purespice_usbRedirAvailable(channel))
continue;
if (connectChannel(usbredir, channel))
{
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)
{
if (usbredir->parser)
return true;
usbredir->parser = usbredirparser_create();
if (!usbredir->parser)
{
DEBUG_ERROR("Failed to create the USB redirection parser");
return false;
}
usbredir->deviceOps->setup(usbredir->deviceOpaque, usbredir->parser);
usbredir->parser->priv = usbredir;
usbredir->parser->log_func = logUSBRedir;
usbredir->parser->read_func = readUSBRedir;
usbredir->parser->write_func = writeUSBRedir;
usbredir->parser->hello_func = helloUSBRedir;
usbredir->parser->device_disconnect_ack_func = deviceDisconnectAck;
uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 };
usbredirparser_caps_set_cap(caps,
usb_redir_cap_device_disconnect_ack);
usbredirparser_caps_set_cap(caps,
usb_redir_cap_connect_device_version);
usbredirparser_caps_set_cap(caps,
usb_redir_cap_ep_info_max_packet_size);
usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids);
usbredirparser_caps_set_cap(caps, usb_redir_cap_32bits_bulk_length);
usbredirparser_init(usbredir->parser, "Looking Glass", caps,
USB_REDIR_CAPS_SIZE, usbredirparser_fl_usb_host);
if (flushUSBRedir(usbredir))
return true;
DEBUG_ERROR("Failed to send the USB redirection hello packet");
destroyParser(usbredir);
return false;
}
LG_USBRedir * lgUsbRedir_create(
const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque,
LG_USBRedirStatusFn status, void * statusOpaque)
{
if (!deviceOps || !deviceOps->setup || !deviceOps->plug ||
!deviceOps->unplug)
return NULL;
LG_USBRedir * usbredir = calloc(1, sizeof(*usbredir));
if (!usbredir)
return NULL;
usbredir->deviceOps = deviceOps;
usbredir->deviceOpaque = deviceOpaque;
usbredir->status = status;
usbredir->statusOpaque = statusOpaque;
atomic_init(&usbredir->desiredPlugged, false);
atomic_init(&usbredir->available, false);
return usbredir;
}
void lgUsbRedir_destroy(LG_USBRedir * usbredir)
{
if (!usbredir)
return;
destroyParser(usbredir);
free(usbredir);
}
void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged)
{
atomic_store_explicit(&usbredir->desiredPlugged, plugged,
memory_order_release);
}
bool lgUsbRedir_available(const LG_USBRedir * usbredir)
{
return atomic_load_explicit(&usbredir->available, memory_order_acquire);
}
bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir)
{
return usbredir->disconnectPending;
}
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))
{
const bool result = flushUSBRedir(usbredir);
if (!result && recover)
resetChannel(usbredir);
return result;
}
if (usbredir->disconnectPending)
{
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);
if (desired != usbredir->plugged)
{
if (desired)
{
usbredir->deviceOps->plug(
usbredir->deviceOpaque, usbredir->parser);
usbredir->plugged = true;
}
else
{
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);
}
}
if (usbredir->plugged && usbredir->deviceOps->process)
usbredir->deviceOps->process(usbredir->deviceOpaque);
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,
PSUSBRedirState state, void * opaque)
{
LG_USBRedir * usbredir = opaque;
const uint8_t id = purespice_usbRedirChannelId(channel);
switch (state)
{
case PS_USB_REDIR_AVAILABLE:
usbredir->channels[id] = channel;
selectChannel(usbredir);
break;
case PS_USB_REDIR_CONNECTED:
if (channel != usbredir->channel)
{
purespice_usbRedirDisconnect(channel);
break;
}
if (!createParser(usbredir))
purespice_usbRedirDisconnect(channel);
break;
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:
if (usbredir->channels[id] == channel)
usbredir->channels[id] = NULL;
if (channel == usbredir->channel)
{
destroyParser(usbredir);
usbredir->channel = NULL;
selectChannel(usbredir);
}
break;
}
}
bool lgUsbRedir_data(PSUSBRedirChannel * channel,
const uint8_t * data, size_t size, void * opaque)
{
LG_USBRedir * usbredir = opaque;
if (channel != usbredir->channel || !usbredir->parser ||
usbredir->readData)
return false;
usbredir->readData = data;
usbredir->readSize = size;
usbredir->readOffset = 0;
const int result = usbredirparser_do_read(usbredir->parser);
const bool consumed = usbredir->readOffset == usbredir->readSize;
usbredir->readData = NULL;
usbredir->readSize = 0;
usbredir->readOffset = 0;
if (result == usbredirparser_read_parse_error)
{
DEBUG_ERROR("Invalid USB redirection packet");
return false;
}
if (!consumed)
{
DEBUG_ERROR("USB redirection parser did not consume its input");
return false;
}
/* 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)
{
LG_USBRedir * usbredir = parserOpaque;
return usbredir->deviceOpaque;
}

View File

@@ -0,0 +1,60 @@
/**
* 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_USBREDIR_
#define _H_LG_CLIENT_USBREDIR_
#include "interface/usbredir.h"
#include <purespice.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct LG_USBRedir LG_USBRedir;
/* Availability transitions are delivered on the PureSpice process thread. */
typedef void (*LG_USBRedirStatusFn)(void * opaque, bool available);
LG_USBRedir * lgUsbRedir_create(
const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque,
LG_USBRedirStatusFn status, void * statusOpaque);
/* The PureSpice session must be stopped before destroying the bridge. */
void lgUsbRedir_destroy(LG_USBRedir * usbredir);
/* This may be called from another thread. The request is applied by
* lgUsbRedir_process on the PureSpice processing thread. */
void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged);
bool lgUsbRedir_available(const LG_USBRedir * usbredir);
/* True after a device disconnect has been sent and until the guest has
* completed the detach. This must be queried on the processing thread. */
bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir);
/* Apply pending device state and flush parser output. This must be called on
* the PureSpice processing thread. */
bool lgUsbRedir_process(LG_USBRedir * usbredir);
void lgUsbRedir_state(PSUSBRedirChannel * channel,
PSUSBRedirState state, void * opaque);
bool lgUsbRedir_data(PSUSBRedirChannel * channel,
const uint8_t * data, size_t size, void * opaque);
#endif