From 5b12685e3cac1d7ddc639e8e5e438c132b15ae8e Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 11 Aug 2026 15:33:53 +1000 Subject: [PATCH] [client] spice: move protocol into transport --- client/CMakeLists.txt | 15 - client/include/input.h | 2 - client/include/interface/transport.h | 36 +- .../interface/usbredir.h} | 35 +- client/src/app.c | 61 +- client/src/input_spice.c | 88 --- client/src/main.c | 689 ++++++------------ client/src/main.h | 20 +- client/src/render_queue.c | 2 +- client/src/render_queue.h | 2 +- client/src/usb_audio.h | 2 +- client/transports/CMakeLists.txt | 1 + client/transports/SPICE/CMakeLists.txt | 46 ++ .../SPICE/audio.c} | 525 ++++++------- client/transports/SPICE/audio.h | 54 ++ client/{src => transports/SPICE}/audio_usb.c | 0 client/{src => transports/SPICE}/audio_usb.h | 0 .../SPICE/clipboard.c} | 367 ++++++---- client/transports/SPICE/clipboard.h | 45 ++ client/transports/SPICE/input.c | 215 ++++++ .../SPICE/input.h} | 19 +- client/transports/SPICE/session.c | 404 ++++++++++ client/transports/SPICE/spice.c | 277 +++++++ client/transports/SPICE/spice.h | 77 ++ client/transports/SPICE/surface.c | 394 ++++++++++ client/transports/SPICE/surface.h | 56 ++ client/{src => transports/SPICE}/usbredir.c | 0 client/{src => transports/SPICE}/usbredir.h | 24 +- 28 files changed, 2398 insertions(+), 1058 deletions(-) rename client/{src/audio_spice.h => include/interface/usbredir.h} (51%) delete mode 100644 client/src/input_spice.c create mode 100644 client/transports/SPICE/CMakeLists.txt rename client/{src/audio_spice.c => transports/SPICE/audio.c} (58%) create mode 100644 client/transports/SPICE/audio.h rename client/{src => transports/SPICE}/audio_usb.c (100%) rename client/{src => transports/SPICE}/audio_usb.h (100%) rename client/{src/clipboard_spice.c => transports/SPICE/clipboard.c} (50%) create mode 100644 client/transports/SPICE/clipboard.h create mode 100644 client/transports/SPICE/input.c rename client/{src/clipboard_spice.h => transports/SPICE/input.h} (67%) create mode 100644 client/transports/SPICE/session.c create mode 100644 client/transports/SPICE/spice.c create mode 100644 client/transports/SPICE/spice.h create mode 100644 client/transports/SPICE/surface.c create mode 100644 client/transports/SPICE/surface.h rename client/{src => transports/SPICE}/usbredir.c (100%) rename client/{src => transports/SPICE}/usbredir.h (75%) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index fe179468..0f21b2d0 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -177,7 +177,6 @@ set(SOURCES src/font.c src/util.c src/clipboard.c - src/clipboard_spice.c src/kb.c src/kb_hid.c src/gl_dynprocs.c @@ -189,7 +188,6 @@ set(SOURCES src/evdev.c src/transport.c src/input.c - src/input_spice.c src/overlay/splash.c src/overlay/alert.c @@ -210,15 +208,6 @@ endif() if(ENABLE_AUDIO) list(APPEND SOURCES src/audio.c - src/audio_spice.c - ) -endif() - -if(ENABLE_USB_AUDIO) - list(APPEND SOURCES - src/audio_usb.c - src/usb_audio.c - src/usbredir.c ) endif() @@ -265,7 +254,6 @@ target_link_libraries(looking-glass-client lg_resources lg_common displayservers - purespice renderers transports audiodevs @@ -288,9 +276,6 @@ endif() if(ENABLE_USB_AUDIO) target_compile_definitions(looking-glass-client PRIVATE ENABLE_USB_AUDIO) - target_link_libraries(looking-glass-client - PkgConfig::USBREDIRPARSER - ) endif() install(TARGETS looking-glass-client diff --git a/client/include/input.h b/client/include/input.h index 0911777c..ae0dd54b 100644 --- a/client/include/input.h +++ b/client/include/input.h @@ -23,8 +23,6 @@ #include "interface/input.h" -extern const LG_InputOps LGI_Spice; - void lgInput_init(void); void lgInput_free(void); diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index 0f691e75..39be8c45 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -71,6 +71,7 @@ typedef uint32_t LG_TransportFeatureFlags; typedef struct LG_TransportSession { char version[32]; + char name[256]; LG_TransportFeatureFlags features; bool uuidValid; @@ -181,8 +182,8 @@ typedef struct LG_TransportPointer int16_t x; int16_t y; CursorType type; - int8_t hx; - int8_t hy; + int16_t hx; + int16_t hy; uint32_t width; uint32_t height; uint32_t pitch; @@ -267,9 +268,34 @@ typedef struct LG_FrameOps } LG_FrameOps; +typedef struct LG_SwSurfaceEventOps +{ + void (*configure)(void * opaque, unsigned int width, unsigned int height); + void (*destroy)(void * opaque); + void (*drawFill)(void * opaque, int x, int y, int width, int height, + uint32_t color); + /* Bitmap data is BGRA32 and remains valid only for the callback. */ + void (*drawBitmap)(void * opaque, bool topDown, int x, int y, + int width, int height, int stride, const void * data); + /* Pointer payloads remain valid only for the callback. */ + void (*pointer)(void * opaque, const LG_TransportPointer * pointer); +} +LG_SwSurfaceEventOps; + +typedef struct LG_SwSurfaceOps +{ + /* detach synchronously quiesces all event callbacks. */ + bool (*attach)(LG_Transport * transport, + const LG_SwSurfaceEventOps * events, void * opaque); + void (*detach)(LG_Transport * transport); + bool (*setActive)(LG_Transport * transport, bool active); +} +LG_SwSurfaceOps; + typedef enum LG_VideoType { LG_VIDEO_TYPE_FRAME, + LG_VIDEO_TYPE_SW_SURFACE, } LG_VideoType; @@ -277,7 +303,11 @@ typedef struct LG_VideoOps { const char * name; LG_VideoType type; - const LG_FrameOps * frame; + union + { + const LG_FrameOps * frame; + const LG_SwSurfaceOps * swSurface; + }; } LG_VideoOps; diff --git a/client/src/audio_spice.h b/client/include/interface/usbredir.h similarity index 51% rename from client/src/audio_spice.h rename to client/include/interface/usbredir.h index 55dfbe41..8ceb8dbd 100644 --- a/client/src/audio_spice.h +++ b/client/include/interface/usbredir.h @@ -18,28 +18,29 @@ * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#ifndef _H_LG_CLIENT_AUDIO_SPICE_ -#define _H_LG_CLIENT_AUDIO_SPICE_ +#ifndef _H_LG_CLIENT_USBREDIR_INTERFACE_ +#define _H_LG_CLIENT_USBREDIR_INTERFACE_ -#include "interface/audio.h" +struct usbredirparser; -#include +typedef struct LG_USBRedirDeviceOps +{ + /* Install the device packet callbacks on a newly-created parser. */ + void (*setup)(void * opaque, struct usbredirparser * parser); -extern const LG_AudioOps LGA_Spice; + /* Queue the device descriptors and connection announcement. */ + void (*plug)(void * opaque, struct usbredirparser * parser); -void lgaSpice_setAvailable(bool available); + /* Queue time-sensitive device data immediately before parser output is + * flushed. */ + void (*process)(void * opaque); -void lgaSpice_playbackStart(int channels, int sampleRate, - PSAudioFormat format, uint32_t time); -void lgaSpice_playbackStop(void); -void lgaSpice_playbackVolume(int channels, const uint16_t volume[]); -void lgaSpice_playbackMute(bool mute); -void lgaSpice_playbackData(uint8_t * data, size_t size, uint32_t time); + /* Stop all device activity. The bridge sends the disconnect packet. */ + void (*unplug)(void * opaque); +} +LG_USBRedirDeviceOps; -void lgaSpice_recordStart(int channels, int sampleRate, - PSAudioFormat format); -void lgaSpice_recordStop(void); -void lgaSpice_recordVolume(int channels, const uint16_t volume[]); -void lgaSpice_recordMute(bool mute); +/* Parser callbacks receive the bridge as their opaque value. */ +void * lgUsbRedir_device(void * parserOpaque); #endif diff --git a/client/src/app.c b/client/src/app.c index afd88974..3f89c02e 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -1151,17 +1151,19 @@ void app_stopVideo(bool stop) bool app_useSpiceDisplay(bool enable) { - if (!g_params.useSpice) + if (!g_params.useSpice || !g_state.fallbackVideoOps || + g_state.fallbackVideoOps->type != LG_VIDEO_TYPE_SW_SURFACE || + !g_state.fallbackVideoOps->swSurface) return false; - atomic_store_explicit(&g_state.spiceDisplayRequested, enable, + atomic_store_explicit(&g_state.fallbackDisplayRequested, enable, memory_order_release); - // if spice is not yet ready, flag the state we want for when it is - if (!atomic_load_explicit(&g_state.spiceReady, memory_order_acquire)) + // if the fallback is not yet ready, retain the requested state + if (!atomic_load_explicit(&g_state.fallbackReady, memory_order_acquire)) return false; - bool active = atomic_load_explicit(&g_state.spiceDisplayActive, + bool active = atomic_load_explicit(&g_state.fallbackDisplayActive, memory_order_acquire); if (active == enable) return active; @@ -1170,65 +1172,40 @@ bool app_useSpiceDisplay(bool enable) if (!enable && !atomic_load_explicit( &g_state.lgHostConnected, memory_order_acquire)) { - atomic_store_explicit(&g_state.spiceDisplayRequested, active, + atomic_store_explicit(&g_state.fallbackDisplayRequested, active, memory_order_release); return false; } bool expected = false; if (!atomic_compare_exchange_strong_explicit( - &g_state.spiceDisplayTransition, &expected, true, + &g_state.fallbackDisplayTransition, &expected, true, memory_order_acquire, memory_order_relaxed)) - return atomic_load_explicit(&g_state.spiceDisplayActive, + return atomic_load_explicit(&g_state.fallbackDisplayActive, memory_order_acquire); - active = atomic_load_explicit(&g_state.spiceDisplayActive, + active = atomic_load_explicit(&g_state.fallbackDisplayActive, memory_order_relaxed); if (active == enable) goto done; - if (enable) - { - if (!purespice_hasChannel(PS_CHANNEL_DISPLAY) || - !purespice_hasChannel(PS_CHANNEL_CURSOR)) - goto fail; + if (!g_state.fallbackVideoOps->swSurface->setActive( + g_state.fallbackTransport.handle, enable)) + goto fail; - if (!purespice_connectChannel(PS_CHANNEL_DISPLAY)) - goto fail; - - if (!purespice_connectChannel(PS_CHANNEL_CURSOR)) - { - purespice_disconnectChannel(PS_CHANNEL_DISPLAY); - goto fail; - } - - renderQueue_swSurfaceShow(true); - } - else - { - if (!purespice_disconnectChannel(PS_CHANNEL_DISPLAY)) - goto fail; - - if (!purespice_disconnectChannel(PS_CHANNEL_CURSOR)) - { - purespice_connectChannel(PS_CHANNEL_DISPLAY); - goto fail; - } - - renderQueue_swSurfaceShow(false); - } + renderQueue_swSurfaceShow(enable); active = enable; - atomic_store_explicit(&g_state.spiceDisplayActive, active, + atomic_store_explicit(&g_state.fallbackDisplayActive, active, memory_order_release); lgInput_useTransport(!active); overlayStatus_set(LG_USER_STATUS_SPICE, enable); done: - atomic_store_explicit(&g_state.spiceDisplayTransition, false, + atomic_store_explicit(&g_state.fallbackDisplayTransition, false, memory_order_release); - enable = atomic_load_explicit(&g_state.spiceDisplayRequested, + enable = atomic_load_explicit(&g_state.fallbackDisplayRequested, memory_order_acquire); if (enable != active) return app_useSpiceDisplay(enable); @@ -1238,7 +1215,7 @@ done: fail: DEBUG_ERROR("Failed to %s the SPICE display", enable ? "enable" : "disable"); - atomic_store_explicit(&g_state.spiceDisplayRequested, active, + atomic_store_explicit(&g_state.fallbackDisplayRequested, active, memory_order_release); goto done; } diff --git a/client/src/input_spice.c b/client/src/input_spice.c deleted file mode 100644 index 10acb715..00000000 --- a/client/src/input_spice.c +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Looking Glass - * Copyright © 2017-2026 The Looking Glass Authors - * https://looking-glass.io - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., 59 - * Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -#include "input.h" -#include "kb.h" - -#include -#include - -static bool spiceSupports(void * opaque, LG_InputSupport support) -{ - return false; -} - -static bool spiceKeyDown(void * opaque, int key) -{ - const uint32_t ps2 = linux_to_ps2[key]; - return !ps2 || purespice_keyDown(ps2); -} - -static bool spiceKeyUp(void * opaque, int key) -{ - const uint32_t ps2 = linux_to_ps2[key]; - return !ps2 || purespice_keyUp(ps2); -} - -static bool spiceKeyboardLEDs(void * opaque, bool numLock, bool capsLock, - bool scrollLock) -{ - 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); - - return purespice_keyModifiers(modifiers); -} - -static bool spiceMouseMotion(void * opaque, int32_t x, int32_t y) -{ - return purespice_mouseMotion(x, y); -} - -static bool spiceMousePress(void * opaque, unsigned int button) -{ - if (button > 7) - return true; - - return purespice_mousePress(button); -} - -static bool spiceMouseRelease(void * opaque, unsigned int button) -{ - if (button > 7) - return true; - - return purespice_mouseRelease(button); -} - -const LG_InputOps LGI_Spice = -{ - .name = "SPICE", - .supports = spiceSupports, - .keyDown = spiceKeyDown, - .keyUp = spiceKeyUp, - .keyboardLEDs = spiceKeyboardLEDs, - .mouseMotion = spiceMouseMotion, - .mousePosition = NULL, - .mousePress = spiceMousePress, - .mouseRelease = spiceMouseRelease, - .reset = NULL, -}; diff --git a/client/src/main.c b/client/src/main.c index b25cd806..d6edf10d 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -58,15 +58,8 @@ #include "core.h" #include "app.h" #include "audio.h" -#if ENABLE_AUDIO -#include "audio_spice.h" -#endif -#if ENABLE_USB_AUDIO -#include "audio_usb.h" -#endif #include "keybind.h" #include "clipboard.h" -#include "clipboard_spice.h" #include "kb.h" #include "egl_dynprocs.h" #include "gl_dynprocs.h" @@ -92,9 +85,7 @@ _Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F, static int renderThread(void * unused); static LGEvent *e_startup = NULL; -static LGEvent *e_spice = NULL; static LGEvent *e_cursorRepaint = NULL; -static LGThread *t_spice = NULL; static LGThread *t_render = NULL; static LGThread *t_cursorRepaint = NULL; @@ -1500,416 +1491,231 @@ int main_frameThread(void * unused) return 0; } -static void checkUUID(void) +static void fallbackSurfaceConfigure(void * opaque, + unsigned int width, unsigned int height) { - if (!atomic_load_explicit(&g_state.spiceReady, memory_order_acquire) || - !g_state.guestUUIDValid) + (void)opaque; + g_state.fallbackSurfaceValid = true; + g_state.srcSize.x = width; + g_state.srcSize.y = height; + g_state.haveSrcSize = true; + core_updatePositionInfo(); + + renderQueue_swSurfaceConfigure(width, height); +} + +static void fallbackSurfaceDestroy(void * opaque) +{ + (void)opaque; + g_state.fallbackSurfaceValid = false; + atomic_store_explicit(&g_state.fallbackDisplayRequested, false, + memory_order_release); + if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false, + memory_order_acq_rel)) + { + renderQueue_swSurfaceShow(false); + lgInput_useTransport(true); + overlayStatus_set(LG_USER_STATUS_SPICE, false); + } +} + +static void fallbackSurfaceDrawFill(void * opaque, int x, int y, + int width, int height, uint32_t color) +{ + (void)opaque; + renderQueue_swSurfaceDrawFill(x, y, width, height, color); +} + +static void fallbackSurfaceDrawBitmap(void * opaque, bool topDown, + int x, int y, int width, int height, int stride, const void * data) +{ + (void)opaque; + renderQueue_swSurfaceDrawBitmap( + x, y, width, height, stride, data, topDown); +} + +static void fallbackSurfacePointer(void * opaque, + const LG_TransportPointer * pointer) +{ + (void)opaque; + + if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) + { + LG_RendererCursor type; + switch(pointer->type) + { + case CURSOR_TYPE_COLOR: + type = LG_CURSOR_COLOR; + break; + + case CURSOR_TYPE_MONOCHROME: + type = LG_CURSOR_MONOCHROME; + break; + + case CURSOR_TYPE_MASKED_COLOR: + type = LG_CURSOR_MASKED_COLOR; + break; + + default: + return; + } + + const size_t size = (size_t)pointer->pitch * pointer->height; + uint8_t * shape = malloc(size); + if (!shape) + return; + memcpy(shape, pointer->shape, size); + renderQueue_cursorImage(type, pointer->width, pointer->height, + pointer->pitch, shape); + } + + if ((pointer->flags & LG_TRANSPORT_POINTER_POSITION) && + (pointer->flags & LG_TRANSPORT_POINTER_VISIBLE_VALID)) + renderQueue_cursorState( + pointer->flags & LG_TRANSPORT_POINTER_VISIBLE, + pointer->x, pointer->y, pointer->hx, pointer->hy); +} + +static const LG_SwSurfaceEventOps fallbackSurfaceEvents = +{ + .configure = fallbackSurfaceConfigure, + .destroy = fallbackSurfaceDestroy, + .drawFill = fallbackSurfaceDrawFill, + .drawBitmap = fallbackSurfaceDrawBitmap, + .pointer = fallbackSurfacePointer, +}; + +static void fallbackDisconnect(void) +{ + if (!g_state.fallbackTransport.ops) return; - if (memcmp(g_state.spiceUUID, g_state.guestUUID, - sizeof(g_state.spiceUUID)) == 0) + const bool connected = g_state.fallbackTransport.handle && + g_state.fallbackTransport.ops->sessionValid( + g_state.fallbackTransport.handle); + + atomic_store_explicit( + &g_state.fallbackReady, false, memory_order_release); + + if (connected) + { + lgInput_setFallback(NULL, NULL); + lgAudio_setFallback(NULL, NULL); + lgClipboard_setFallback(NULL, NULL); + } + else + { + lgInput_dropFallback(); + lgAudio_dropFallback(); + lgClipboard_dropFallback(); + } + + if (g_state.fallbackVideoOps && + g_state.fallbackVideoOps->type == LG_VIDEO_TYPE_SW_SURFACE && + g_state.fallbackVideoOps->swSurface) + { + if (connected) + g_state.fallbackVideoOps->swSurface->setActive( + g_state.fallbackTransport.handle, false); + g_state.fallbackVideoOps->swSurface->detach( + g_state.fallbackTransport.handle); + } + + if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false, + memory_order_acq_rel)) + { + renderQueue_swSurfaceShow(false); + lgInput_useTransport(true); + overlayStatus_set(LG_USER_STATUS_SPICE, false); + } + + g_state.fallbackTransport.ops->disconnect( + g_state.fallbackTransport.handle); + lgTransport_destroy(&g_state.fallbackTransport); + g_state.fallbackVideoOps = NULL; + g_state.fallbackSurfaceValid = false; +} + +static bool fallbackConnect(void) +{ + if (!g_params.useSpice || strcmp(g_params.transport, "spice") == 0) + return true; + + if (!lgTransport_create("spice", &g_state.fallbackTransport)) + { + DEBUG_ERROR("Failed to create the SPICE fallback transport"); + return false; + } + + g_state.fallbackVideoOps = + g_state.fallbackTransport.ops->getVideoOps( + g_state.fallbackTransport.handle); + if (!g_state.fallbackVideoOps || + g_state.fallbackVideoOps->type != LG_VIDEO_TYPE_SW_SURFACE || + !g_state.fallbackVideoOps->swSurface || + !g_state.fallbackVideoOps->swSurface->attach( + g_state.fallbackTransport.handle, &fallbackSurfaceEvents, NULL)) + { + DEBUG_ERROR("SPICE does not provide a software surface"); + fallbackDisconnect(); + return false; + } + + const LG_TransportStatus status = + g_state.fallbackTransport.ops->connect( + g_state.fallbackTransport.handle, &g_state.fallbackSession); + if (status != LG_TRANSPORT_OK) + { + DEBUG_ERROR("Failed to connect the SPICE fallback transport: %d", status); + fallbackDisconnect(); + return false; + } + + void * inputOpaque = NULL; + const LG_InputOps * inputOps = + g_state.fallbackTransport.ops->getInputOps ? + g_state.fallbackTransport.ops->getInputOps( + g_state.fallbackTransport.handle, &inputOpaque) : NULL; + lgInput_setFallback(inputOps, inputOpaque); + + void * audioOpaque = NULL; + const LG_AudioOps * audioOps = + g_state.fallbackTransport.ops->getAudioOps ? + g_state.fallbackTransport.ops->getAudioOps( + g_state.fallbackTransport.handle, &audioOpaque) : NULL; + lgAudio_setFallback(audioOps, audioOpaque); + + void * clipboardOpaque = NULL; + const LG_ClipboardOps * clipboardOps = + g_state.fallbackTransport.ops->getClipboardOps ? + g_state.fallbackTransport.ops->getClipboardOps( + g_state.fallbackTransport.handle, &clipboardOpaque) : NULL; + lgClipboard_setFallback(clipboardOps, clipboardOpaque); + + atomic_store_explicit( + &g_state.fallbackReady, true, memory_order_release); + if (g_state.fallbackSession.name[0]) + core_setTitle(g_state.fallbackSession.name); + if (inputOps) + keybind_inputRegister(); + return true; +} + +static void checkUUID(void) +{ + if (!atomic_load_explicit(&g_state.fallbackReady, memory_order_acquire) || + !g_state.fallbackSession.uuidValid || !g_state.guestUUIDValid) + return; + + if (memcmp(g_state.fallbackSession.uuid, g_state.guestUUID, + sizeof(g_state.guestUUID)) == 0) return; app_msgBox( "SPICE Configuration Error", "You have connected SPICE to the wrong guest.\n" - "SPICE input will not function until this is corrected."); - - g_params.useSpiceInput = false; - lgInput_setFallback(NULL, NULL); - lgcSpice_setAvailable(false); - lgClipboard_setFallback(NULL, NULL); - atomic_store_explicit(&g_state.spiceClose, true, memory_order_release); -} - -void spiceReady(void) -{ - atomic_store_explicit(&g_state.spiceReady, true, memory_order_release); - if (g_params.useSpiceInput) - lgInput_setFallback(&LGI_Spice, NULL); -#if ENABLE_AUDIO - if (g_params.useSpiceAudio && !g_params.useSpiceUSBAudio) - { - lgaSpice_setAvailable(true); - lgAudio_setFallback(&LGA_Spice, NULL); - } -#endif - - if (atomic_load_explicit(&g_state.spiceDisplayRequested, - memory_order_acquire)) - app_useSpiceDisplay(true); - - // set the intial mouse mode - purespice_mouseMode(true); - - PSServerInfo info; - if (purespice_getServerInfo(&info)) - { - core_setTitle(info.name); - - bool uuidValid = false; - for(int i = 0; i < sizeof(info.uuid); ++i) - if (info.uuid[i]) - { - uuidValid = true; - break; - } - - if (uuidValid) - { - memcpy(g_state.spiceUUID, info.uuid, sizeof(g_state.spiceUUID)); - checkUUID(); - } - purespice_freeServerInfo(&info); - } - else - DEBUG_WARN("Failed to obtain SPICE server information"); - - if (g_params.useSpiceClipboard && - !atomic_load_explicit(&g_state.spiceClose, memory_order_acquire)) - lgClipboard_setFallback(&LGC_Spice, NULL); - - if (g_params.useSpiceInput) - keybind_inputRegister(); - - lgSignalEvent(e_spice); -} - -static void spice_surfaceCreate(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); - g_state.spicePrimarySurfaceValid = false; - app_useSpiceDisplay(false); - return; - } - - DEBUG_INFO("Create primary SPICE surface: id: %u, size: %ux%u", - surfaceId, width, height); - - g_state.spicePrimarySurfaceValid = true; - - g_state.srcSize.x = width; - g_state.srcSize.y = height; - g_state.haveSrcSize = true; - core_updatePositionInfo(); - - renderQueue_swSurfaceConfigure(width, height); - renderQueue_swSurfaceDrawFill(0, 0, width, height, 0x0); -} - -static void spice_surfaceDestroy(unsigned int surfaceId) -{ - if (!g_state.spicePrimarySurfaceValid || surfaceId != 0) - { - DEBUG_INFO("Ignoring destruction of inactive or secondary SPICE surface %u", - surfaceId); - return; - } - - DEBUG_INFO("Destroy primary SPICE surface %u", surfaceId); - g_state.spicePrimarySurfaceValid = false; - app_useSpiceDisplay(false); -} - -static void spice_drawFill(unsigned int surfaceId, int x, int y, int width, - int height, uint32_t color) -{ - if (!g_state.spicePrimarySurfaceValid || surfaceId != 0) - return; - - renderQueue_swSurfaceDrawFill(x, y, width, height, color); -} - -static void spice_drawBitmap(unsigned int surfaceId, PSBitmapFormat format, - bool topDown, int x, int y, int width, int height, int stride, void * data) -{ - if (!g_state.spicePrimarySurfaceValid || 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; - } - - renderQueue_swSurfaceDrawBitmap(x, y, width, height, stride, data, topDown); -} - -static void spice_setCursorRGBAImage(int width, int height, int hx, int hy, - const void * data) -{ - g_state.spiceHotX = hx; - g_state.spiceHotY = hy; - - const uint8_t * rgba = data; - uint8_t * bgra = malloc(width * height * 4); - 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]; - } - renderQueue_cursorImage( - LG_CURSOR_COLOR, width, height, width * 4, bgra); -} - -static void spice_setCursorMonoImage(int width, int height, int hx, int hy, - const void * xorMask, const void * andMask) -{ - g_state.spiceHotX = hx; - g_state.spiceHotY = hy; - - int stride = (width + 7) / 8; - uint8_t * buffer = malloc(stride * height * 2); - memcpy(buffer, andMask, stride * height); - memcpy(buffer + stride * height, xorMask, stride * height); - renderQueue_cursorImage( - LG_CURSOR_MONOCHROME, width, height * 2, stride, buffer); -} - -static void spice_setCursorColorImage(int width, int height, int hx, int hy, - const void * data, const void * maskData) -{ - g_state.spiceHotX = hx; - g_state.spiceHotY = hy; - - const uint8_t * rgba = data; - const uint8_t * andMask = maskData; - const int maskStride = (width + 7) / 8; - uint8_t * bgra = malloc(width * height * 4); - 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 * maskStride + x / 8] & (0x80U >> (x % 8)) ? 255 : 0; - } - } - - renderQueue_cursorImage( - LG_CURSOR_MASKED_COLOR, width, height, width * 4, bgra); -} - -static void spice_setCursorState(bool visible, int x, int y) -{ - renderQueue_cursorState(visible, x, y, g_state.spiceHotX, g_state.spiceHotY); -} - -int spiceThread(void * arg) -{ -#if ENABLE_USB_AUDIO - LGA_USBState * usbAudio = NULL; - LG_USBRedir * usbRedir = NULL; - - if (g_params.useSpiceAudio && g_params.useSpiceUSBAudio) - { - if (!lgAudio_supportsPlayback()) - { - DEBUG_WARN("USB audio requires a playback backend, using SPICE audio"); - g_params.useSpiceUSBAudio = false; - } - else if (!(usbAudio = lgaUsb_create(g_params.audioDebug))) - { - DEBUG_WARN("Failed to initialize USB audio, using SPICE audio"); - g_params.useSpiceUSBAudio = false; - } - else - usbRedir = lgaUsb_redir(usbAudio); - } -#endif - - const struct PSConfig config = - { - .host = g_params.spiceHost, - .port = g_params.spicePort, - .password = "", - .ready = spiceReady, - .inputs = - { - .enable = g_params.useSpiceInput, - .autoConnect = true - }, - .clipboard = - { - .enable = g_params.useSpiceClipboard, - .notice = lgcSpice_notice, - .data = lgcSpice_data, - .release = lgcSpice_release, - .request = lgcSpice_request, - .status = lgcSpice_setAvailable, - }, - .display = - { - .enable = true, - .autoConnect = false, - .surfaceCreate = spice_surfaceCreate, - .surfaceDestroy = spice_surfaceDestroy, - .drawFill = spice_drawFill, - .drawBitmap = spice_drawBitmap - }, - .cursor = - { - .enable = true, - .autoConnect = false, - .setRGBAImage = spice_setCursorRGBAImage, - .setMonoImage = spice_setCursorMonoImage, - .setColorImage = spice_setCursorColorImage, - .setState = spice_setCursorState, - }, -#if ENABLE_AUDIO - .playback = - { - .enable = g_params.useSpiceAudio && - !g_params.useSpiceUSBAudio && lgAudio_supportsPlayback(), - .autoConnect = true, - .start = lgaSpice_playbackStart, - .volume = lgaSpice_playbackVolume, - .mute = lgaSpice_playbackMute, - .stop = lgaSpice_playbackStop, - .data = lgaSpice_playbackData - }, - .record = - { - .enable = g_params.useSpiceAudio && - !g_params.useSpiceUSBAudio && lgAudio_supportsRecord(), - .autoConnect = true, - .start = lgaSpice_recordStart, - .volume = lgaSpice_recordVolume, - .mute = lgaSpice_recordMute, - .stop = lgaSpice_recordStop - }, -#endif -#if ENABLE_USB_AUDIO - .usbRedir = - { - .enable = usbAudio != NULL, - .autoConnect = false, - .opaque = usbRedir, - .state = lgUsbRedir_state, - .data = lgUsbRedir_data, - }, -#endif - }; - - bool connected = false; - PSStatus status = PS_STATUS_SHUTDOWN; - if (!purespice_connect(&config)) - { - DEBUG_ERROR("Failed to connect to spice server"); - lgSignalEvent(e_spice); - goto end; - } - connected = true; - status = PS_STATUS_RUN; - - int processTimeout = 100; -#if ENABLE_USB_AUDIO - if (usbAudio) - { - lgAudio_setFallback(&LGA_USB, usbAudio); - processTimeout = 10; - } -#endif - - // process all spice messages - while(app_getState() != APP_STATE_SHUTDOWN && - !atomic_load_explicit(&g_state.spiceClose, memory_order_acquire)) - { -#if ENABLE_USB_AUDIO - if (usbRedir && !lgUsbRedir_process(usbRedir)) - DEBUG_WARN("Failed to process USB audio redirection"); - if (usbAudio) - { - const uint64_t delay = lgaUsb_processDelayNs(usbAudio); - if (delay == UINT64_MAX) - processTimeout = 10; - else - { - const uint64_t timeout = delay / UINT64_C(1000000) + - (delay % UINT64_C(1000000) != 0); - processTimeout = (int)min(timeout, UINT64_C(10)); - } - } -#endif - - if ((status = purespice_process(processTimeout)) != PS_STATUS_RUN) - { - if (status != PS_STATUS_SHUTDOWN) - DEBUG_ERROR("failed to process spice messages"); - goto end; - } - } - -end: - - lgInput_setFallback(NULL, NULL); - lgAudio_setFallback(NULL, NULL); - lgcSpice_setAvailable(false); - lgClipboard_setFallback(NULL, NULL); -#if ENABLE_AUDIO - lgaSpice_setAvailable(false); -#endif -#if ENABLE_USB_AUDIO - if (connected && status == PS_STATUS_RUN && usbRedir) - { - if (!lgUsbRedir_process(usbRedir)) - DEBUG_WARN("Failed to disconnect USB audio device"); - else - { - const uint64_t deadline = - microtime() + USB_REDIR_DISCONNECT_TIMEOUT_US; - while (lgUsbRedir_disconnectPending(usbRedir) && - microtime() < deadline) - { - status = purespice_process(10); - if (status != PS_STATUS_RUN) - break; - } - - if (status == PS_STATUS_RUN && - lgUsbRedir_disconnectPending(usbRedir)) - DEBUG_WARN("Timed out disconnecting USB audio device"); - } - } -#endif - if (connected) - purespice_disconnect(); -#if ENABLE_USB_AUDIO - lgaUsb_destroy(usbAudio); -#endif - - // if the connection was disconnected intentionally we don't want to shutdown - // so that the user can see the message box and take action - if (!atomic_load_explicit(&g_state.spiceClose, memory_order_acquire)) - app_setState(APP_STATE_SHUTDOWN); - - lgSignalEvent(e_spice); - return 0; + "SPICE fallback services will not function until this is corrected."); + fallbackDisconnect(); } void intHandler(int sig) @@ -2139,13 +1945,6 @@ static int lg_run(void) } DEBUG_INFO("Using Video: %s", g_state.videoOps->name); - // setup the spice startup condition - if (!(e_spice = lgCreateEvent(false, 0))) - { - DEBUG_ERROR("failed to create the spice startup event"); - return -1; - } - // setup the startup condition if (!(e_startup = lgCreateEvent(false, 0))) { @@ -2164,31 +1963,10 @@ static int lg_run(void) renderQueue_init(); frameScheduler_init(); - const PSInit psInit = - { - .log = - { - .info = debug_info, - .warn = debug_warn, - .error = debug_error, - } - }; - purespice_init(&psInit); - g_state.micDefaultState = g_params.micDefaultState; - if (g_params.useSpice) - { - if (!lgCreateThread("spiceThread", spiceThread, NULL, &t_spice)) - { - DEBUG_ERROR("spice create thread failed"); - return -1; - } - - lgWaitEvent(e_spice, TIMEOUT_INFINITE); - if (!atomic_load_explicit(&g_state.spiceReady, memory_order_acquire)) - return -1; - } + if (!fallbackConnect()) + return -1; // select and init a renderer bool needsOpenGL = false; @@ -2349,6 +2127,16 @@ restart: while(app_getState() == APP_STATE_RUNNING) { + if (atomic_load_explicit( + &g_state.fallbackReady, memory_order_acquire) && + !g_state.fallbackTransport.ops->sessionValid( + g_state.fallbackTransport.handle)) + { + DEBUG_ERROR("SPICE fallback transport disconnected"); + app_setState(APP_STATE_SHUTDOWN); + break; + } + if (initialSpiceEnable && microtime() > initialSpiceEnable) { app_useSpiceDisplay(true); @@ -2523,6 +2311,16 @@ restart: while(likely(app_getState() == APP_STATE_RUNNING)) { + if (atomic_load_explicit( + &g_state.fallbackReady, memory_order_acquire) && + !g_state.fallbackTransport.ops->sessionValid( + g_state.fallbackTransport.handle)) + { + DEBUG_ERROR("SPICE fallback transport disconnected"); + app_setState(APP_STATE_SHUTDOWN); + break; + } + if (unlikely(!g_state.transport.ops->sessionValid( g_state.transport.handle))) { @@ -2570,8 +2368,7 @@ static void lg_shutdown(void) if (e_cursorRepaint) lgSignalEvent(e_cursorRepaint); - if (t_spice) - lgJoinThread(t_spice, NULL); + fallbackDisconnect(); if (t_render) { @@ -2628,12 +2425,6 @@ static void lg_shutdown(void) e_startup = NULL; } - if (e_spice) - { - lgFreeEvent(e_spice); - e_startup = NULL; - } - if (g_state.ds) g_state.ds->shutdown(); lgClipboard_free(); diff --git a/client/src/main.h b/client/src/main.h index d1455b6c..738f8538 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -33,7 +33,6 @@ #include "common/event.h" #include "common/ll.h" -#include #include "cimgui.h" #include "interface/transport.h" @@ -79,12 +78,11 @@ struct AppState bool dsInitialized; bool jitRender; - uint8_t spiceUUID[16]; - atomic_bool spiceReady; - atomic_bool spiceDisplayRequested; - atomic_bool spiceDisplayActive; - atomic_bool spiceDisplayTransition; - bool spicePrimarySurfaceValid; + atomic_bool fallbackReady; + atomic_bool fallbackDisplayRequested; + atomic_bool fallbackDisplayActive; + atomic_bool fallbackDisplayTransition; + bool fallbackSurfaceValid; uint8_t guestUUID[16]; bool guestUUIDValid; @@ -112,8 +110,6 @@ struct AppState LG_RendererRect dstRect; bool posInfoValid; bool alignToGuest; - atomic_bool spiceClose; - atomic_uint_least64_t shaderMousePosition; atomic_uint_least32_t shaderMouseState; @@ -127,6 +123,10 @@ struct AppState const LG_VideoOps * videoOps; LG_TransportFeatureFlags transportFeatures; + LG_TransportInstance fallbackTransport; + LG_TransportSession fallbackSession; + const LG_VideoOps * fallbackVideoOps; + LGThread * cursorThread; LGThread * frameThread; LGEvent * frameEvent; @@ -152,8 +152,6 @@ struct AppState enum MicDefaultState micDefaultState; - int spiceHotX, spiceHotY; - // Set to true when setHDRImageDesc() returns false, indicating // the display server could not apply the HDR image description. // Checked by the renderer to fall back to software tone-mapping. diff --git a/client/src/render_queue.c b/client/src/render_queue.c index af1a74dc..53353215 100644 --- a/client/src/render_queue.c +++ b/client/src/render_queue.c @@ -131,7 +131,7 @@ void renderQueue_swSurfaceDrawFill(int x, int y, int width, int height, } void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, - int stride, void * data, bool topDown) + int stride, const void * data, bool topDown) { if (width <= 0 || height <= 0 || stride <= 0) { diff --git a/client/src/render_queue.h b/client/src/render_queue.h index 37fe2b1c..a4dde5f0 100644 --- a/client/src/render_queue.h +++ b/client/src/render_queue.h @@ -108,7 +108,7 @@ void renderQueue_swSurfaceDrawFill(int x, int y, int width, int height, uint32_t color); void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, - int stride, void * data, bool topDown); + int stride, const void * data, bool topDown); void renderQueue_swSurfaceShow(bool show); diff --git a/client/src/usb_audio.h b/client/src/usb_audio.h index dc852bfd..c897246c 100644 --- a/client/src/usb_audio.h +++ b/client/src/usb_audio.h @@ -22,7 +22,7 @@ #define _H_LG_CLIENT_USB_AUDIO_ #include "interface/audio.h" -#include "usbredir.h" +#include "interface/usbredir.h" #include #include diff --git a/client/transports/CMakeLists.txt b/client/transports/CMakeLists.txt index 26ca2b71..f2d2bcc9 100644 --- a/client/transports/CMakeLists.txt +++ b/client/transports/CMakeLists.txt @@ -20,6 +20,7 @@ endfunction() # Add/remove transports here! add_transport(LGMP) +add_transport(SPICE) if(ENABLE_TESTS) add_transport(Test) endif() diff --git a/client/transports/SPICE/CMakeLists.txt b/client/transports/SPICE/CMakeLists.txt new file mode 100644 index 00000000..5ebf9f84 --- /dev/null +++ b/client/transports/SPICE/CMakeLists.txt @@ -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() diff --git a/client/src/audio_spice.c b/client/transports/SPICE/audio.c similarity index 58% rename from client/src/audio_spice.c rename to client/transports/SPICE/audio.c index 1a359e99..c5184ac3 100644 --- a/client/src/audio_spice.c +++ b/client/transports/SPICE/audio.c @@ -18,13 +18,14 @@ * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#include "audio_spice.h" +#include "audio.h" #include "common/debug.h" #include "common/event.h" #include "common/locking.h" #include +#include #include #define SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS 2000 @@ -67,7 +68,7 @@ typedef struct SpiceAudioCallbackWaitQueue } SpiceAudioCallbackWaitQueue; -static struct +struct SpiceAudio { LG_RWLock lock; @@ -92,27 +93,6 @@ static struct int64_t playbackTime; uint64_t playbackPosition; LG_AudioClock playbackClock; -} -l_spice = -{ - .lock = - { - .readers = ATOMIC_VAR_INIT(0), - .writers = ATOMIC_VAR_INIT(0), - .writer = ATOMIC_FLAG_INIT, - }, - .statusInFlight = ATOMIC_VAR_INIT(0), - .statusWait = - { - .lock = ATOMIC_FLAG_INIT, - .count = ATOMIC_VAR_INIT(0), - }, - .inFlight = ATOMIC_VAR_INIT(0), - .eventWait = - { - .lock = ATOMIC_FLAG_INIT, - .count = ATOMIC_VAR_INIT(0), - }, }; static _Thread_local unsigned int l_eventDepth; @@ -215,53 +195,55 @@ static void waitCallbacks(atomic_uint * inFlight, lgFreeEvent(waiter.event); } -/* l_spice.lock must be held exclusively while admitting a callback so detach - * cannot invalidate the target between the snapshot and in-flight increment. */ -static bool beginEventNL(SpiceAudioEventTarget * target) +/* 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 (!l_spice.events) + if (!audio->events) return false; - target->events = l_spice.events; - target->opaque = l_spice.eventOpaque; - target->generation = l_spice.eventGeneration; - atomic_fetch_add_explicit(&l_spice.inFlight, 1, memory_order_relaxed); + 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(const SpiceAudioEventTarget * target, - uint32_t generation, bool active) +static bool playbackEventCurrent(SpiceAudio * audio, + const SpiceAudioEventTarget * target, uint32_t generation, bool active) { - LG_LOCK_SHARED(l_spice.lock); + LG_LOCK_SHARED(audio->lock); const bool result = - target->events == l_spice.events && - target->opaque == l_spice.eventOpaque && - target->generation == l_spice.eventGeneration && - l_spice.playback.generation == generation && - l_spice.playback.active == active; - LG_UNLOCK_SHARED(l_spice.lock); + 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(const SpiceAudioEventTarget * target, - uint32_t generation, bool active) +static bool recordEventCurrent(SpiceAudio * audio, + const SpiceAudioEventTarget * target, uint32_t generation, bool active) { - LG_LOCK_SHARED(l_spice.lock); + LG_LOCK_SHARED(audio->lock); const bool result = - target->events == l_spice.events && - target->opaque == l_spice.eventOpaque && - target->generation == l_spice.eventGeneration && - l_spice.record.generation == generation && - l_spice.record.active == active; - LG_UNLOCK_SHARED(l_spice.lock); + 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(void) +static void endEvent(SpiceAudio * audio) { --l_eventDepth; - endCallback(&l_spice.inFlight, &l_spice.eventWait); + endCallback(&audio->inFlight, &audio->eventWait); } static bool sampleFormat(PSAudioFormat source, @@ -378,75 +360,77 @@ static size_t sampleSize(LG_AudioSampleFormat format) return 0; } -static LG_AudioClock playbackClockNL(uint32_t time) +static LG_AudioClock playbackClockNL(SpiceAudio * audio, uint32_t time) { bool discontinuity = false; - if (!l_spice.playbackClockValid) + if (!audio->playbackClockValid) { - l_spice.playbackClockValid = true; - l_spice.playbackMediaTime = time; - l_spice.playbackTime = 0; + audio->playbackClockValid = true; + audio->playbackMediaTime = time; + audio->playbackTime = 0; } else { - const int32_t delta = (int32_t)(time - l_spice.playbackMediaTime); - l_spice.playbackMediaTime = time; + const int32_t delta = (int32_t)(time - audio->playbackMediaTime); + audio->playbackMediaTime = time; if (delta < 0 || delta > SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS) { - l_spice.playbackTime = 0; + audio->playbackTime = 0; discontinuity = true; } else - l_spice.playbackTime += (int64_t)delta * 1000000; + audio->playbackTime += (int64_t)delta * 1000000; } - l_spice.playbackClock = (LG_AudioClock) + audio->playbackClock = (LG_AudioClock) { - .position = l_spice.playbackPosition, - .time = l_spice.playbackTime, + .position = audio->playbackPosition, + .time = audio->playbackTime, .rate = 0.0, .stable = !discontinuity, .discontinuity = discontinuity, }; - return l_spice.playbackClock; + return audio->playbackClock; } static void spiceSetStatusListener(void * opaque, LG_AudioStatusFn callback, void * callbackOpaque) { + SpiceAudio * audio = opaque; LG_AudioStatus status; - LG_LOCK_EXCLUSIVE(l_spice.lock); - l_spice.statusCallback = callback; - l_spice.statusOpaque = callbackOpaque; + LG_LOCK_EXCLUSIVE(audio->lock); + audio->statusCallback = callback; + audio->statusOpaque = callbackOpaque; status = (LG_AudioStatus) { - .available = l_spice.available, - .generation = l_spice.statusGeneration, + .available = audio->available, + .generation = audio->statusGeneration, }; if (callback) { atomic_fetch_add_explicit( - &l_spice.statusInFlight, 1, memory_order_relaxed); + &audio->statusInFlight, 1, memory_order_relaxed); ++l_statusDepth; } - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); if (callback) { callback(callbackOpaque, &status); --l_statusDepth; - endCallback(&l_spice.statusInFlight, &l_spice.statusWait); + endCallback(&audio->statusInFlight, &audio->statusWait); } else - waitCallbacks(&l_spice.statusInFlight, - &l_spice.statusWait, l_statusDepth); + 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; @@ -456,41 +440,41 @@ static bool spiceAttach(void * opaque, const LG_AudioEventOps * events, LG_AudioClock playbackClock; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return false; } - l_spice.events = events; - l_spice.eventOpaque = eventOpaque; - l_spice.eventGeneration = - nextGeneration(l_spice.eventGeneration); - playback = l_spice.playback; - record = l_spice.record; - playbackClock = l_spice.playbackClock; - dispatch = beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, playback.generation, true) && + if (playbackEventCurrent(audio, &target, playback.generation, true) && target.events->playbackStart) { target.events->playbackStart(target.opaque, playback.generation, &playback.format, &playbackClock); if (playback.volumeValid && - playbackEventCurrent(&target, playback.generation, true) && + playbackEventCurrent(audio, &target, playback.generation, true) && target.events->playbackVolume) target.events->playbackVolume(target.opaque, playback.generation, playback.volumeChannels, playback.volume); if (playback.muteValid && - playbackEventCurrent(&target, playback.generation, true) && + playbackEventCurrent(audio, &target, playback.generation, true) && target.events->playbackMute) target.events->playbackMute(target.opaque, playback.generation, playback.mute); @@ -499,53 +483,55 @@ static bool spiceAttach(void * opaque, const LG_AudioEventOps * events, if (record.active) { - if (recordEventCurrent(&target, record.generation, true) && + if (recordEventCurrent(audio, &target, record.generation, true) && target.events->recordStart) { target.events->recordStart(target.opaque, record.generation, &record.format); if (record.volumeValid && - recordEventCurrent(&target, record.generation, true) && + recordEventCurrent(audio, &target, record.generation, true) && target.events->recordVolume) target.events->recordVolume(target.opaque, record.generation, record.volumeChannels, record.volume); if (record.muteValid && - recordEventCurrent(&target, record.generation, true) && + recordEventCurrent(audio, &target, record.generation, true) && target.events->recordMute) target.events->recordMute(target.opaque, record.generation, record.mute); } } - endEvent(); + endEvent(audio); return true; } static void spiceDetach(void * opaque) { - LG_LOCK_EXCLUSIVE(l_spice.lock); - l_spice.events = NULL; - l_spice.eventOpaque = NULL; - l_spice.eventGeneration = - nextGeneration(l_spice.eventGeneration); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&l_spice.inFlight, &l_spice.eventWait, l_eventDepth); + 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(l_spice.lock); - if (l_spice.available && l_spice.events && l_spice.record.active && - generation == l_spice.record.generation) + LG_LOCK_SHARED(audio->lock); + if (audio->available && audio->events && audio->record.active && + generation == audio->record.generation) { const size_t bytesPerSample = - sampleSize(l_spice.record.format.sampleFormat); - const size_t channels = l_spice.record.format.channelCount; + sampleSize(audio->record.format.sampleFormat); + const size_t channels = audio->record.format.channelCount; if (bytesPerSample && frames <= SIZE_MAX / bytesPerSample / channels && (frames == 0 || data)) { @@ -553,12 +539,12 @@ static bool spiceRecordData(void * opaque, uint32_t generation, valid = true; } } - LG_UNLOCK_SHARED(l_spice.lock); + LG_UNLOCK_SHARED(audio->lock); return valid && purespice_writeAudio((void *)data, size, 0); } -const LG_AudioOps LGA_Spice = +static const LG_AudioOps l_spiceAudioOps = { .name = "SPICE", .setStatusListener = spiceSetStatusListener, @@ -568,60 +554,106 @@ const LG_AudioOps LGA_Spice = .clockFeedback = NULL, }; -void lgaSpice_setAvailable(bool available) +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(l_spice.lock); - if (l_spice.available == available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (audio->available == available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - l_spice.available = available; - l_spice.statusGeneration = - nextGeneration(l_spice.statusGeneration); + audio->available = available; + audio->statusGeneration = + nextGeneration(audio->statusGeneration); if (!available) { - l_spice.playback.active = false; - l_spice.record.active = false; - l_spice.playbackClockValid = false; - l_spice.playbackPosition = 0; - l_spice.playbackClock = (LG_AudioClock) { 0 }; - l_spice.playback.volumeValid = false; - l_spice.playback.muteValid = false; - l_spice.record.volumeValid = false; - l_spice.record.muteValid = false; + 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 = l_spice.statusCallback; - callbackOpaque = l_spice.statusOpaque; + callback = audio->statusCallback; + callbackOpaque = audio->statusOpaque; status = (LG_AudioStatus) { .available = available, - .generation = l_spice.statusGeneration, + .generation = audio->statusGeneration, }; if (callback) { atomic_fetch_add_explicit( - &l_spice.statusInFlight, 1, memory_order_relaxed); + &audio->statusInFlight, 1, memory_order_relaxed); ++l_statusDepth; } - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); if (callback) { callback(callbackOpaque, &status); --l_statusDepth; - endCallback(&l_spice.statusInFlight, &l_spice.statusWait); + endCallback(&audio->statusInFlight, &audio->statusWait); } } -void lgaSpice_playbackStart(int channels, int sampleRate, - PSAudioFormat sourceFormat, uint32_t time) +void spiceAudio_playbackStart(SpiceAudio * audio, + int channels, int sampleRate, PSAudioFormat sourceFormat, uint32_t time) { LG_AudioFormat format; if (!makeFormat(channels, sampleRate, sourceFormat, &format)) @@ -636,78 +668,79 @@ void lgaSpice_playbackStart(int channels, int sampleRate, LG_AudioClock clock; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - l_spice.playback.active = true; - l_spice.playback.generation = - nextGeneration(l_spice.playback.generation); - l_spice.playback.format = format; - l_spice.playbackClockValid = false; - l_spice.playbackPosition = 0; - clock = playbackClockNL(time); + 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; - l_spice.playbackClock = clock; - playback = l_spice.playback; - dispatch = beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + audio->playbackClock = clock; + playback = audio->playback; + dispatch = beginEventNL(audio, &target); + LG_UNLOCK_EXCLUSIVE(audio->lock); if (!dispatch) return; - if (playbackEventCurrent(&target, playback.generation, true) && + if (playbackEventCurrent(audio, &target, playback.generation, true) && target.events->playbackStart) { target.events->playbackStart(target.opaque, playback.generation, &playback.format, &clock); if (playback.volumeValid && - playbackEventCurrent(&target, playback.generation, true) && + playbackEventCurrent(audio, &target, playback.generation, true) && target.events->playbackVolume) target.events->playbackVolume(target.opaque, playback.generation, playback.volumeChannels, playback.volume); if (playback.muteValid && - playbackEventCurrent(&target, playback.generation, true) && + playbackEventCurrent(audio, &target, playback.generation, true) && target.events->playbackMute) target.events->playbackMute(target.opaque, playback.generation, playback.mute); } - endEvent(); + endEvent(audio); } -void lgaSpice_playbackStop(void) +void spiceAudio_playbackStop(SpiceAudio * audio) { SpiceAudioEventTarget target; uint32_t generation; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.playback.active) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->playback.active) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - generation = l_spice.playback.generation; - l_spice.playback.active = false; - l_spice.playbackClockValid = false; - dispatch = beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, generation, false)) + playbackEventCurrent(audio, &target, generation, false)) target.events->playbackStop(target.opaque, generation); - endEvent(); + endEvent(audio); } -void lgaSpice_playbackVolume(int channels, const uint16_t volume[]) +void spiceAudio_playbackVolume(SpiceAudio * audio, + int channels, const uint16_t volume[]) { if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume) return; @@ -717,61 +750,62 @@ void lgaSpice_playbackVolume(int channels, const uint16_t volume[]) uint16_t snapshot[LG_AUDIO_MAX_CHANNELS]; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - memcpy(l_spice.playback.volume, volume, + memcpy(audio->playback.volume, volume, (size_t)channels * sizeof(*volume)); - l_spice.playback.volumeChannels = channels; - l_spice.playback.volumeValid = true; - generation = l_spice.playback.generation; + audio->playback.volumeChannels = channels; + audio->playback.volumeValid = true; + generation = audio->playback.generation; memcpy(snapshot, volume, (size_t)channels * sizeof(*volume)); - dispatch = l_spice.playback.active && beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + dispatch = audio->playback.active && beginEventNL(audio, &target); + LG_UNLOCK_EXCLUSIVE(audio->lock); if (!dispatch) return; if (target.events->playbackVolume && - playbackEventCurrent(&target, generation, true)) + playbackEventCurrent(audio, &target, generation, true)) target.events->playbackVolume( target.opaque, generation, channels, snapshot); - endEvent(); + endEvent(audio); } -void lgaSpice_playbackMute(bool mute) +void spiceAudio_playbackMute(SpiceAudio * audio, bool mute) { SpiceAudioEventTarget target; uint32_t generation; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - l_spice.playback.mute = mute; - l_spice.playback.muteValid = true; - generation = l_spice.playback.generation; - dispatch = l_spice.playback.active && beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, generation, true)) + playbackEventCurrent(audio, &target, generation, true)) target.events->playbackMute(target.opaque, generation, mute); - endEvent(); + endEvent(audio); } -void lgaSpice_playbackData(uint8_t * data, size_t size, uint32_t time) +void spiceAudio_playbackData(SpiceAudio * audio, + uint8_t * data, size_t size, uint32_t time) { SpiceAudioEventTarget target; uint32_t generation; @@ -779,43 +813,43 @@ void lgaSpice_playbackData(uint8_t * data, size_t size, uint32_t time) LG_AudioClock clock; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available || !l_spice.playback.active) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available || !audio->playback.active) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } const size_t bytesPerSample = - sampleSize(l_spice.playback.format.sampleFormat); + sampleSize(audio->playback.format.sampleFormat); const size_t stride = - bytesPerSample * l_spice.playback.format.channelCount; + bytesPerSample * audio->playback.format.channelCount; if (!stride || !size || !data || size % stride) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); DEBUG_ERROR("Invalid SPICE playback packet size: %zu", size); return; } frames = size / stride; - generation = l_spice.playback.generation; - clock = playbackClockNL(time); - l_spice.playbackPosition += frames; - dispatch = beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, generation, true)) + playbackEventCurrent(audio, &target, generation, true)) target.events->playbackData(target.opaque, generation, data, frames, &clock); - endEvent(); + endEvent(audio); } -void lgaSpice_recordStart(int channels, int sampleRate, - PSAudioFormat sourceFormat) +void spiceAudio_recordStart(SpiceAudio * audio, + int channels, int sampleRate, PSAudioFormat sourceFormat) { LG_AudioFormat format; if (!makeFormat(channels, sampleRate, sourceFormat, &format)) @@ -829,70 +863,71 @@ void lgaSpice_recordStart(int channels, int sampleRate, SpiceAudioStream record; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - l_spice.record.active = true; - l_spice.record.generation = nextGeneration(l_spice.record.generation); - l_spice.record.format = format; - record = l_spice.record; - dispatch = beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, record.generation, true) && + if (recordEventCurrent(audio, &target, record.generation, true) && target.events->recordStart) { target.events->recordStart( target.opaque, record.generation, &record.format); if (record.volumeValid && - recordEventCurrent(&target, record.generation, true) && + recordEventCurrent(audio, &target, record.generation, true) && target.events->recordVolume) target.events->recordVolume(target.opaque, record.generation, record.volumeChannels, record.volume); if (record.muteValid && - recordEventCurrent(&target, record.generation, true) && + recordEventCurrent(audio, &target, record.generation, true) && target.events->recordMute) target.events->recordMute( target.opaque, record.generation, record.mute); } - endEvent(); + endEvent(audio); } -void lgaSpice_recordStop(void) +void spiceAudio_recordStop(SpiceAudio * audio) { SpiceAudioEventTarget target; uint32_t generation; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.record.active) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->record.active) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - generation = l_spice.record.generation; - l_spice.record.active = false; - dispatch = beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, generation, false)) + recordEventCurrent(audio, &target, generation, false)) target.events->recordStop(target.opaque, generation); - endEvent(); + endEvent(audio); } -void lgaSpice_recordVolume(int channels, const uint16_t volume[]) +void spiceAudio_recordVolume(SpiceAudio * audio, + int channels, const uint16_t volume[]) { if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume) return; @@ -902,56 +937,56 @@ void lgaSpice_recordVolume(int channels, const uint16_t volume[]) uint16_t snapshot[LG_AUDIO_MAX_CHANNELS]; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - memcpy(l_spice.record.volume, volume, + memcpy(audio->record.volume, volume, (size_t)channels * sizeof(*volume)); - l_spice.record.volumeChannels = channels; - l_spice.record.volumeValid = true; - generation = l_spice.record.generation; + audio->record.volumeChannels = channels; + audio->record.volumeValid = true; + generation = audio->record.generation; memcpy(snapshot, volume, (size_t)channels * sizeof(*volume)); - dispatch = l_spice.record.active && beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + dispatch = audio->record.active && beginEventNL(audio, &target); + LG_UNLOCK_EXCLUSIVE(audio->lock); if (!dispatch) return; if (target.events->recordVolume && - recordEventCurrent(&target, generation, true)) + recordEventCurrent(audio, &target, generation, true)) target.events->recordVolume( target.opaque, generation, channels, snapshot); - endEvent(); + endEvent(audio); } -void lgaSpice_recordMute(bool mute) +void spiceAudio_recordMute(SpiceAudio * audio, bool mute) { SpiceAudioEventTarget target; uint32_t generation; bool dispatch; - LG_LOCK_EXCLUSIVE(l_spice.lock); - if (!l_spice.available) + LG_LOCK_EXCLUSIVE(audio->lock); + if (!audio->available) { - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + LG_UNLOCK_EXCLUSIVE(audio->lock); return; } - l_spice.record.mute = mute; - l_spice.record.muteValid = true; - generation = l_spice.record.generation; - dispatch = l_spice.record.active && beginEventNL(&target); - LG_UNLOCK_EXCLUSIVE(l_spice.lock); + 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(&target, generation, true)) + recordEventCurrent(audio, &target, generation, true)) target.events->recordMute(target.opaque, generation, mute); - endEvent(); + endEvent(audio); } diff --git a/client/transports/SPICE/audio.h b/client/transports/SPICE/audio.h new file mode 100644 index 00000000..6da9cae7 --- /dev/null +++ b/client/transports/SPICE/audio.h @@ -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 + +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 diff --git a/client/src/audio_usb.c b/client/transports/SPICE/audio_usb.c similarity index 100% rename from client/src/audio_usb.c rename to client/transports/SPICE/audio_usb.c diff --git a/client/src/audio_usb.h b/client/transports/SPICE/audio_usb.h similarity index 100% rename from client/src/audio_usb.h rename to client/transports/SPICE/audio_usb.h diff --git a/client/src/clipboard_spice.c b/client/transports/SPICE/clipboard.c similarity index 50% rename from client/src/clipboard_spice.c rename to client/transports/SPICE/clipboard.c index e0f37eb7..e2ccb519 100644 --- a/client/src/clipboard_spice.c +++ b/client/transports/SPICE/clipboard.c @@ -18,12 +18,13 @@ * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#include "clipboard_spice.h" +#include "clipboard.h" #include "common/debug.h" #include "common/locking.h" #include +#include typedef struct ClipboardEventTarget { @@ -40,7 +41,7 @@ typedef struct PendingRequest } PendingRequest; -static struct +struct SpiceClipboard { LG_Lock stateLock; LG_Lock eventDispatch; @@ -60,15 +61,10 @@ static struct PendingRequest write; LG_ClipboardRequest requestSerial; -} -l_spice = -{ - .stateLock = ATOMIC_FLAG_INIT, - .eventDispatch = ATOMIC_FLAG_INIT, - .statusDispatch = ATOMIC_FLAG_INIT, - .remoteType = LG_CLIPBOARD_DATA_NONE, }; +static _Atomic(SpiceClipboard *) l_callbackTarget; + static uint32_t nextGeneration(uint32_t generation) { if (++generation == 0) @@ -76,11 +72,11 @@ static uint32_t nextGeneration(uint32_t generation) return generation; } -static LG_ClipboardRequest nextRequestNL(void) +static LG_ClipboardRequest nextRequestNL(SpiceClipboard * clipboard) { - if (++l_spice.requestSerial == LG_CLIPBOARD_REQUEST_INVALID) - ++l_spice.requestSerial; - return l_spice.requestSerial; + if (++clipboard->requestSerial == LG_CLIPBOARD_REQUEST_INVALID) + ++clipboard->requestSerial; + return clipboard->requestSerial; } static bool spiceType(PSDataType source, LG_ClipboardData * type) @@ -116,76 +112,76 @@ static bool lgType(LG_ClipboardData source, PSDataType * type) static void spiceSetStatusListener(void * opaque, LG_ClipboardStatusFn callback, void * callbackOpaque) { - (void)opaque; - LG_LOCK(l_spice.statusDispatch); + SpiceClipboard * clipboard = opaque; + LG_LOCK(clipboard->statusDispatch); - LG_LOCK(l_spice.stateLock); - l_spice.statusCallback = callback; - l_spice.statusOpaque = callbackOpaque; + LG_LOCK(clipboard->stateLock); + clipboard->statusCallback = callback; + clipboard->statusOpaque = callbackOpaque; const LG_ClipboardStatus status = { - .available = l_spice.available, - .generation = l_spice.statusGeneration, + .available = clipboard->available, + .generation = clipboard->statusGeneration, }; - LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(clipboard->stateLock); if (callback) callback(callbackOpaque, &status); - LG_UNLOCK(l_spice.statusDispatch); + LG_UNLOCK(clipboard->statusDispatch); } static bool spiceAttach(void * opaque, const LG_ClipboardEventOps * events, void * eventOpaque) { - (void)opaque; + SpiceClipboard * clipboard = opaque; if (!events) return false; - LG_LOCK(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); - if (!l_spice.available) + LG_LOCK(clipboard->eventDispatch); + LG_LOCK(clipboard->stateLock); + if (!clipboard->available) { - LG_UNLOCK(l_spice.stateLock); - LG_UNLOCK(l_spice.eventDispatch); + LG_UNLOCK(clipboard->stateLock); + LG_UNLOCK(clipboard->eventDispatch); return false; } - l_spice.events = events; - l_spice.eventOpaque = eventOpaque; - const bool notice = l_spice.remoteNotice; - const LG_ClipboardData type = l_spice.remoteType; - LG_UNLOCK(l_spice.stateLock); + 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(l_spice.eventDispatch); + LG_UNLOCK(clipboard->eventDispatch); return true; } static void spiceDetach(void * opaque) { - (void)opaque; - LG_LOCK(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); - const bool failWrite = l_spice.available && l_spice.write.pending; - l_spice.events = NULL; - l_spice.eventOpaque = NULL; - l_spice.read = (PendingRequest) { 0 }; - l_spice.write = (PendingRequest) { 0 }; - LG_UNLOCK(l_spice.stateLock); + 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(l_spice.eventDispatch); + LG_UNLOCK(clipboard->eventDispatch); } static bool spiceRelease(void * opaque) { - (void)opaque; - LG_LOCK(l_spice.stateLock); - const bool available = l_spice.available; - const bool failWrite = l_spice.write.pending; - l_spice.write = (PendingRequest) { 0 }; - LG_UNLOCK(l_spice.stateLock); + 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; @@ -198,9 +194,9 @@ static bool spiceRelease(void * opaque) static bool spiceNotifyTypes(void * opaque, const LG_ClipboardData types[], size_t count) { - (void)opaque; + SpiceClipboard * clipboard = opaque; if (count == 0) - return spiceRelease(NULL); + return spiceRelease(clipboard); if (!types || count > LG_CLIPBOARD_DATA_NONE) return false; @@ -210,11 +206,11 @@ static bool spiceNotifyTypes(void * opaque, !lgType(types[i], &converted[i])) return false; - LG_LOCK(l_spice.stateLock); - const bool available = l_spice.available; - const bool failWrite = l_spice.write.pending; - l_spice.write = (PendingRequest) { 0 }; - LG_UNLOCK(l_spice.stateLock); + 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; @@ -227,20 +223,20 @@ static bool spiceNotifyTypes(void * opaque, static bool spiceData(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size) { - (void)opaque; + 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(l_spice.stateLock); - const bool valid = l_spice.available && l_spice.write.pending && - l_spice.write.request == request && - (type == LG_CLIPBOARD_DATA_NONE || l_spice.write.type == type); + 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) - l_spice.write = (PendingRequest) { 0 }; - LG_UNLOCK(l_spice.stateLock); + clipboard->write = (PendingRequest) { 0 }; + LG_UNLOCK(clipboard->stateLock); if (!valid || !purespice_clipboardDataStart(converted, size)) return false; @@ -252,23 +248,23 @@ static bool spiceData(void * opaque, LG_ClipboardRequest request, static bool spiceRequest(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type) { - (void)opaque; + SpiceClipboard * clipboard = opaque; PSDataType converted; if (request == LG_CLIPBOARD_REQUEST_INVALID || type == LG_CLIPBOARD_DATA_NONE || !lgType(type, &converted)) return false; - LG_LOCK(l_spice.stateLock); - const bool valid = l_spice.available && l_spice.events && - !l_spice.read.pending; + LG_LOCK(clipboard->stateLock); + const bool valid = clipboard->available && clipboard->events && + !clipboard->read.pending; if (valid) - l_spice.read = (PendingRequest) + clipboard->read = (PendingRequest) { - .pending = true, - .request = request, - .type = type, + .pending = true, + .request = request, + .type = type, }; - LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(clipboard->stateLock); if (!valid) return false; @@ -276,18 +272,18 @@ static bool spiceRequest(void * opaque, LG_ClipboardRequest request, if (purespice_clipboardRequest(converted)) return true; - LG_LOCK(l_spice.stateLock); - if (!l_spice.read.pending || l_spice.read.request != request) + LG_LOCK(clipboard->stateLock); + if (!clipboard->read.pending || clipboard->read.request != request) { - LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(clipboard->stateLock); return true; } - l_spice.read = (PendingRequest) { 0 }; - LG_UNLOCK(l_spice.stateLock); + clipboard->read = (PendingRequest) { 0 }; + LG_UNLOCK(clipboard->stateLock); return false; } -const LG_ClipboardOps LGC_Spice = +static const LG_ClipboardOps l_clipboardOps = { .name = "SPICE", .setStatusListener = spiceSetStatusListener, @@ -299,99 +295,152 @@ const LG_ClipboardOps LGC_Spice = .request = spiceRequest, }; -void lgcSpice_setAvailable(bool available) +bool spiceClipboard_init(SpiceClipboard ** clipboard) { - LG_LOCK(l_spice.statusDispatch); - LG_LOCK(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); + *clipboard = calloc(1, sizeof(**clipboard)); + if (!*clipboard) + return false; - const bool changed = l_spice.available != available; + 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) { - l_spice.available = available; - l_spice.statusGeneration = nextGeneration( - l_spice.statusGeneration); + clipboard->available = available; + clipboard->statusGeneration = nextGeneration( + clipboard->statusGeneration); } if (!available) { - l_spice.remoteNotice = false; - l_spice.remoteType = LG_CLIPBOARD_DATA_NONE; - l_spice.read = (PendingRequest) { 0 }; - l_spice.write = (PendingRequest) { 0 }; + clipboard->remoteNotice = false; + clipboard->remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard->read = (PendingRequest) { 0 }; + clipboard->write = (PendingRequest) { 0 }; } - const LG_ClipboardStatusFn callback = l_spice.statusCallback; - void * callbackOpaque = l_spice.statusOpaque; + const LG_ClipboardStatusFn callback = clipboard->statusCallback; + void * callbackOpaque = clipboard->statusOpaque; const LG_ClipboardStatus status = { .available = available, - .generation = l_spice.statusGeneration, + .generation = clipboard->statusGeneration, }; - LG_UNLOCK(l_spice.stateLock); - LG_UNLOCK(l_spice.eventDispatch); + LG_UNLOCK(clipboard->stateLock); + LG_UNLOCK(clipboard->eventDispatch); if (changed && callback) callback(callbackOpaque, &status); - LG_UNLOCK(l_spice.statusDispatch); + LG_UNLOCK(clipboard->statusDispatch); } -void lgcSpice_notice(PSDataType source) +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); - lgcSpice_release(); + spiceClipboard_release(); return; } - LG_LOCK(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); - const bool failWrite = l_spice.write.pending; - l_spice.remoteNotice = true; - l_spice.remoteType = type; - l_spice.read = (PendingRequest) { 0 }; - l_spice.write = (PendingRequest) { 0 }; + 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 = l_spice.available ? l_spice.events : NULL, - .opaque = l_spice.eventOpaque, + .events = clipboard->available ? clipboard->events : NULL, + .opaque = clipboard->eventOpaque, }; - LG_UNLOCK(l_spice.stateLock); + 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(l_spice.eventDispatch); + LG_UNLOCK(clipboard->eventDispatch); } -void lgcSpice_data(PSDataType source, uint8_t * buffer, uint32_t size) +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(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); - if (!l_spice.read.pending) + LG_LOCK(clipboard->eventDispatch); + LG_LOCK(clipboard->stateLock); + if (!clipboard->read.pending) { - LG_UNLOCK(l_spice.stateLock); - LG_UNLOCK(l_spice.eventDispatch); + LG_UNLOCK(clipboard->stateLock); + LG_UNLOCK(clipboard->eventDispatch); DEBUG_WARN("Ignoring unsolicited SPICE clipboard data"); return; } - const PendingRequest request = l_spice.read; - l_spice.read = (PendingRequest) { 0 }; + const PendingRequest request = clipboard->read; + clipboard->read = (PendingRequest) { 0 }; const ClipboardEventTarget target = { - .events = l_spice.available ? l_spice.events : NULL, - .opaque = l_spice.eventOpaque, + .events = clipboard->available ? clipboard->events : NULL, + .opaque = clipboard->eventOpaque, }; - LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(clipboard->stateLock); if (target.events && target.events->data) { @@ -416,30 +465,41 @@ void lgcSpice_data(PSDataType source, uint8_t * buffer, uint32_t size) type, buffer, size); } } - LG_UNLOCK(l_spice.eventDispatch); + LG_UNLOCK(clipboard->eventDispatch); } -void lgcSpice_release(void) +void spiceClipboard_release(void) { - LG_LOCK(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); - l_spice.remoteNotice = false; - l_spice.remoteType = LG_CLIPBOARD_DATA_NONE; - l_spice.read = (PendingRequest) { 0 }; + 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 = l_spice.available ? l_spice.events : NULL, - .opaque = l_spice.eventOpaque, + .events = clipboard->available ? clipboard->events : NULL, + .opaque = clipboard->eventOpaque, }; - LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(clipboard->stateLock); if (target.events && target.events->release) target.events->release(target.opaque); - LG_UNLOCK(l_spice.eventDispatch); + LG_UNLOCK(clipboard->eventDispatch); } -void lgcSpice_request(PSDataType source) +void spiceClipboard_request(PSDataType source) { + SpiceClipboard * clipboard = callbackTarget(); + if (!clipboard) + { + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + return; + } + LG_ClipboardData type; if (!spiceType(source, &type)) { @@ -448,44 +508,51 @@ void lgcSpice_request(PSDataType source) return; } - LG_LOCK(l_spice.eventDispatch); - LG_LOCK(l_spice.stateLock); - const bool busy = l_spice.write.pending; + LG_LOCK(clipboard->eventDispatch); + LG_LOCK(clipboard->stateLock); + const bool busy = clipboard->write.pending; const ClipboardEventTarget target = { - .events = l_spice.available && !busy ? - l_spice.events : NULL, - .opaque = l_spice.eventOpaque, + .events = clipboard->available && !busy ? + clipboard->events : NULL, + .opaque = clipboard->eventOpaque, }; LG_ClipboardRequest request = LG_CLIPBOARD_REQUEST_INVALID; if (target.events) { - request = nextRequestNL(); - l_spice.write = (PendingRequest) + request = nextRequestNL(clipboard); + clipboard->write = (PendingRequest) { - .pending = true, - .request = request, - .type = type, + .pending = true, + .request = request, + .type = type, }; } - LG_UNLOCK(l_spice.stateLock); + 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(l_spice.stateLock); - if (l_spice.write.pending && l_spice.write.request == request) + LG_LOCK(clipboard->stateLock); + if (clipboard->write.pending && clipboard->write.request == request) { - l_spice.write = (PendingRequest) { 0 }; + clipboard->write = (PendingRequest) { 0 }; fail = true; } - LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(clipboard->stateLock); if (fail) purespice_clipboardDataStart(SPICE_DATA_NONE, 0); else if (busy) DEBUG_WARN("Ignoring overlapping SPICE clipboard request"); } - LG_UNLOCK(l_spice.eventDispatch); + LG_UNLOCK(clipboard->eventDispatch); +} + +void spiceClipboard_status(bool available) +{ + SpiceClipboard * clipboard = callbackTarget(); + if (clipboard) + spiceClipboard_setAvailable(clipboard, available); } diff --git a/client/transports/SPICE/clipboard.h b/client/transports/SPICE/clipboard.h new file mode 100644 index 00000000..44c84bcb --- /dev/null +++ b/client/transports/SPICE/clipboard.h @@ -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 + +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 diff --git a/client/transports/SPICE/input.c b/client/transports/SPICE/input.c new file mode 100644 index 00000000..f1bea3bc --- /dev/null +++ b/client/transports/SPICE/input.c @@ -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 +#include +#include + +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); +} diff --git a/client/src/clipboard_spice.h b/client/transports/SPICE/input.h similarity index 67% rename from client/src/clipboard_spice.h rename to client/transports/SPICE/input.h index 1b288e07..446693aa 100644 --- a/client/src/clipboard_spice.h +++ b/client/transports/SPICE/input.h @@ -18,20 +18,17 @@ * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#ifndef _H_LG_CLIENT_CLIPBOARD_SPICE_ -#define _H_LG_CLIENT_CLIPBOARD_SPICE_ +#ifndef _H_LG_CLIENT_TRANSPORT_SPICE_INPUT_ +#define _H_LG_CLIENT_TRANSPORT_SPICE_INPUT_ -#include "interface/clipboard.h" +#include "interface/input.h" -#include +typedef struct SpiceInput SpiceInput; -extern const LG_ClipboardOps LGC_Spice; +bool spiceInput_init(SpiceInput ** input); +void spiceInput_free(SpiceInput ** input); -void lgcSpice_setAvailable(bool available); - -void lgcSpice_notice(PSDataType type); -void lgcSpice_data(PSDataType type, uint8_t * buffer, uint32_t size); -void lgcSpice_release(void); -void lgcSpice_request(PSDataType type); +const LG_InputOps * spiceInput_getOps(void); +void spiceInput_setAvailable(SpiceInput * input, bool available); #endif diff --git a/client/transports/SPICE/session.c b/client/transports/SPICE/session.c new file mode 100644 index 00000000..7b18280a --- /dev/null +++ b/client/transports/SPICE/session.c @@ -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 + +#include +#include +#include + +#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; +} diff --git a/client/transports/SPICE/spice.c b/client/transports/SPICE/spice.c new file mode 100644 index 00000000..4c4b7173 --- /dev/null +++ b/client/transports/SPICE/spice.c @@ -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 + +#include +#include + +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, +}; diff --git a/client/transports/SPICE/spice.h b/client/transports/SPICE/spice.h new file mode 100644 index 00000000..bd26087d --- /dev/null +++ b/client/transports/SPICE/spice.h @@ -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 + +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 diff --git a/client/transports/SPICE/surface.c b/client/transports/SPICE/surface.c new file mode 100644 index 00000000..a234ed09 --- /dev/null +++ b/client/transports/SPICE/surface.c @@ -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 +#include + +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); +} diff --git a/client/transports/SPICE/surface.h b/client/transports/SPICE/surface.h new file mode 100644 index 00000000..05e0c4e9 --- /dev/null +++ b/client/transports/SPICE/surface.h @@ -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 + +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 diff --git a/client/src/usbredir.c b/client/transports/SPICE/usbredir.c similarity index 100% rename from client/src/usbredir.c rename to client/transports/SPICE/usbredir.c diff --git a/client/src/usbredir.h b/client/transports/SPICE/usbredir.h similarity index 75% rename from client/src/usbredir.h rename to client/transports/SPICE/usbredir.h index e3503698..d65454e0 100644 --- a/client/src/usbredir.h +++ b/client/transports/SPICE/usbredir.h @@ -21,36 +21,19 @@ #ifndef _H_LG_CLIENT_USBREDIR_ #define _H_LG_CLIENT_USBREDIR_ +#include "interface/usbredir.h" + #include #include #include #include -struct usbredirparser; - typedef struct LG_USBRedir LG_USBRedir; /* Availability transitions are delivered on the PureSpice process thread. */ typedef void (*LG_USBRedirStatusFn)(void * opaque, bool available); -typedef struct LG_USBRedirDeviceOps -{ - /* Install the device packet callbacks on a newly-created parser. */ - void (*setup)(void * opaque, struct usbredirparser * parser); - - /* Queue the device descriptors and connection announcement. */ - void (*plug)(void * opaque, struct usbredirparser * parser); - - /* Queue time-sensitive device data. This runs on the PureSpice processing - * thread immediately before parser output is flushed. */ - void (*process)(void * opaque); - - /* Stop all device activity. The bridge sends the disconnect packet. */ - void (*unplug)(void * opaque); -} -LG_USBRedirDeviceOps; - LG_USBRedir * lgUsbRedir_create( const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque, LG_USBRedirStatusFn status, void * statusOpaque); @@ -74,7 +57,4 @@ void lgUsbRedir_state(PSUSBRedirChannel * channel, bool lgUsbRedir_data(PSUSBRedirChannel * channel, const uint8_t * data, size_t size, void * opaque); -/* Parser callbacks receive the bridge as their opaque value. */ -void * lgUsbRedir_device(void * parserOpaque); - #endif