mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 07:01:30 +00:00
[client] spice: move protocol into transport
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
|
||||
#include "interface/input.h"
|
||||
|
||||
extern const LG_InputOps LGI_Spice;
|
||||
|
||||
void lgInput_init(void);
|
||||
void lgInput_free(void);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 <purespice.h>
|
||||
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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <purespice.h>
|
||||
#include <stddef.h>
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include "common/event.h"
|
||||
#include "common/ll.h"
|
||||
|
||||
#include <purespice.h>
|
||||
#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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#define _H_LG_CLIENT_USB_AUDIO_
|
||||
|
||||
#include "interface/audio.h"
|
||||
#include "usbredir.h"
|
||||
#include "interface/usbredir.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -20,6 +20,7 @@ endfunction()
|
||||
|
||||
# Add/remove transports here!
|
||||
add_transport(LGMP)
|
||||
add_transport(SPICE)
|
||||
if(ENABLE_TESTS)
|
||||
add_transport(Test)
|
||||
endif()
|
||||
|
||||
46
client/transports/SPICE/CMakeLists.txt
Normal file
46
client/transports/SPICE/CMakeLists.txt
Normal file
@@ -0,0 +1,46 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(transport_SPICE LANGUAGES C)
|
||||
|
||||
set(SOURCES
|
||||
clipboard.c
|
||||
input.c
|
||||
session.c
|
||||
spice.c
|
||||
surface.c
|
||||
)
|
||||
|
||||
if(ENABLE_AUDIO)
|
||||
list(APPEND SOURCES
|
||||
audio.c
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ENABLE_USB_AUDIO)
|
||||
list(APPEND SOURCES
|
||||
audio_usb.c
|
||||
usbredir.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/usb_audio.c
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(transport_SPICE STATIC ${SOURCES})
|
||||
|
||||
target_include_directories(transport_SPICE PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
target_link_libraries(transport_SPICE
|
||||
lg_common
|
||||
purespice
|
||||
)
|
||||
|
||||
if(ENABLE_AUDIO)
|
||||
target_compile_definitions(transport_SPICE PRIVATE ENABLE_AUDIO)
|
||||
endif()
|
||||
|
||||
if(ENABLE_USB_AUDIO)
|
||||
target_compile_definitions(transport_SPICE PRIVATE ENABLE_USB_AUDIO)
|
||||
target_link_libraries(transport_SPICE
|
||||
PkgConfig::USBREDIRPARSER
|
||||
)
|
||||
endif()
|
||||
@@ -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 <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
54
client/transports/SPICE/audio.h
Normal file
54
client/transports/SPICE/audio.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifndef _H_LG_CLIENT_TRANSPORT_SPICE_AUDIO_
|
||||
#define _H_LG_CLIENT_TRANSPORT_SPICE_AUDIO_
|
||||
|
||||
#include "interface/audio.h"
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
typedef struct SpiceAudio SpiceAudio;
|
||||
|
||||
bool spiceAudio_init(SpiceAudio ** audio);
|
||||
void spiceAudio_free(SpiceAudio ** audio);
|
||||
|
||||
const LG_AudioOps * spiceAudio_getOps(void);
|
||||
void spiceAudio_setAvailable(SpiceAudio * audio, bool available);
|
||||
|
||||
void spiceAudio_playbackStart(SpiceAudio * audio,
|
||||
int channels, int sampleRate,
|
||||
PSAudioFormat format, uint32_t time);
|
||||
void spiceAudio_playbackStop(SpiceAudio * audio);
|
||||
void spiceAudio_playbackVolume(SpiceAudio * audio,
|
||||
int channels, const uint16_t volume[]);
|
||||
void spiceAudio_playbackMute(SpiceAudio * audio, bool mute);
|
||||
void spiceAudio_playbackData(SpiceAudio * audio,
|
||||
uint8_t * data, size_t size, uint32_t time);
|
||||
|
||||
void spiceAudio_recordStart(SpiceAudio * audio,
|
||||
int channels, int sampleRate,
|
||||
PSAudioFormat format);
|
||||
void spiceAudio_recordStop(SpiceAudio * audio);
|
||||
void spiceAudio_recordVolume(SpiceAudio * audio,
|
||||
int channels, const uint16_t volume[]);
|
||||
void spiceAudio_recordMute(SpiceAudio * audio, bool mute);
|
||||
|
||||
#endif
|
||||
@@ -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 <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
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);
|
||||
}
|
||||
45
client/transports/SPICE/clipboard.h
Normal file
45
client/transports/SPICE/clipboard.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifndef _H_LG_CLIENT_TRANSPORT_SPICE_CLIPBOARD_
|
||||
#define _H_LG_CLIENT_TRANSPORT_SPICE_CLIPBOARD_
|
||||
|
||||
#include "interface/clipboard.h"
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
typedef struct SpiceClipboard SpiceClipboard;
|
||||
|
||||
bool spiceClipboard_init(SpiceClipboard ** clipboard);
|
||||
void spiceClipboard_free(SpiceClipboard ** clipboard);
|
||||
|
||||
const LG_ClipboardOps * spiceClipboard_getOps(void);
|
||||
void spiceClipboard_setAvailable(SpiceClipboard * clipboard, bool available);
|
||||
|
||||
/* PureSpice's clipboard callbacks do not carry an opaque pointer. The active
|
||||
* callback target must be set before processing the corresponding session. */
|
||||
void spiceClipboard_setCallbackTarget(SpiceClipboard * clipboard);
|
||||
void spiceClipboard_notice(PSDataType type);
|
||||
void spiceClipboard_data(PSDataType type, uint8_t * buffer, uint32_t size);
|
||||
void spiceClipboard_release(void);
|
||||
void spiceClipboard_request(PSDataType type);
|
||||
void spiceClipboard_status(bool available);
|
||||
|
||||
#endif
|
||||
215
client/transports/SPICE/input.c
Normal file
215
client/transports/SPICE/input.c
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "input.h"
|
||||
|
||||
#include "kb.h"
|
||||
|
||||
#include "common/locking.h"
|
||||
|
||||
#include <purespice.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct SpiceInput
|
||||
{
|
||||
LG_Lock stateLock;
|
||||
LG_Lock statusDispatch;
|
||||
|
||||
bool available;
|
||||
uint32_t statusGeneration;
|
||||
LG_InputStatusFn statusCallback;
|
||||
void * statusOpaque;
|
||||
};
|
||||
|
||||
static uint32_t nextGeneration(uint32_t generation)
|
||||
{
|
||||
if (++generation == 0)
|
||||
++generation;
|
||||
return generation;
|
||||
}
|
||||
|
||||
static bool spiceSupports(void * opaque, LG_InputSupport support)
|
||||
{
|
||||
(void)opaque;
|
||||
(void)support;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void spiceSetStatusListener(void * opaque,
|
||||
LG_InputStatusFn callback, void * callbackOpaque)
|
||||
{
|
||||
SpiceInput * input = opaque;
|
||||
LG_LOCK(input->statusDispatch);
|
||||
|
||||
LG_LOCK(input->stateLock);
|
||||
input->statusCallback = callback;
|
||||
input->statusOpaque = callbackOpaque;
|
||||
const LG_InputStatus status =
|
||||
{
|
||||
.available = input->available,
|
||||
.generation = input->statusGeneration,
|
||||
};
|
||||
LG_UNLOCK(input->stateLock);
|
||||
|
||||
if (callback)
|
||||
callback(callbackOpaque, &status);
|
||||
LG_UNLOCK(input->statusDispatch);
|
||||
}
|
||||
|
||||
static bool spiceKeyDown(void * opaque, int key)
|
||||
{
|
||||
SpiceInput * input = opaque;
|
||||
const uint32_t ps2 = linux_to_ps2[key];
|
||||
|
||||
LG_LOCK(input->stateLock);
|
||||
const bool result = input->available &&
|
||||
(!ps2 || purespice_keyDown(ps2));
|
||||
LG_UNLOCK(input->stateLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool spiceKeyUp(void * opaque, int key)
|
||||
{
|
||||
SpiceInput * input = opaque;
|
||||
const uint32_t ps2 = linux_to_ps2[key];
|
||||
|
||||
LG_LOCK(input->stateLock);
|
||||
const bool result = !ps2 || purespice_keyUp(ps2);
|
||||
LG_UNLOCK(input->stateLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool spiceKeyboardLEDs(void * opaque, bool numLock, bool capsLock,
|
||||
bool scrollLock)
|
||||
{
|
||||
SpiceInput * input = opaque;
|
||||
const uint32_t modifiers =
|
||||
(scrollLock ? 1 /* SPICE_SCROLL_LOCK_MODIFIER */ : 0) |
|
||||
(numLock ? 2 /* SPICE_NUM_LOCK_MODIFIER */ : 0) |
|
||||
(capsLock ? 4 /* SPICE_CAPS_LOCK_MODIFIER */ : 0);
|
||||
|
||||
LG_LOCK(input->stateLock);
|
||||
const bool result = input->available &&
|
||||
purespice_keyModifiers(modifiers);
|
||||
LG_UNLOCK(input->stateLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool spiceMouseMotion(void * opaque, int32_t x, int32_t y)
|
||||
{
|
||||
SpiceInput * input = opaque;
|
||||
LG_LOCK(input->stateLock);
|
||||
const bool result = input->available && purespice_mouseMotion(x, y);
|
||||
LG_UNLOCK(input->stateLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool spiceMousePress(void * opaque, unsigned int button)
|
||||
{
|
||||
if (button > 7)
|
||||
return true;
|
||||
|
||||
SpiceInput * input = opaque;
|
||||
LG_LOCK(input->stateLock);
|
||||
const bool result = input->available && purespice_mousePress(button);
|
||||
LG_UNLOCK(input->stateLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool spiceMouseRelease(void * opaque, unsigned int button)
|
||||
{
|
||||
if (button > 7)
|
||||
return true;
|
||||
|
||||
SpiceInput * input = opaque;
|
||||
LG_LOCK(input->stateLock);
|
||||
const bool result = purespice_mouseRelease(button);
|
||||
LG_UNLOCK(input->stateLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static const LG_InputOps l_inputOps =
|
||||
{
|
||||
.name = "SPICE",
|
||||
.supports = spiceSupports,
|
||||
.setStatusListener = spiceSetStatusListener,
|
||||
.keyDown = spiceKeyDown,
|
||||
.keyUp = spiceKeyUp,
|
||||
.keyboardLEDs = spiceKeyboardLEDs,
|
||||
.mouseMotion = spiceMouseMotion,
|
||||
.mousePosition = NULL,
|
||||
.mousePress = spiceMousePress,
|
||||
.mouseRelease = spiceMouseRelease,
|
||||
.reset = NULL,
|
||||
};
|
||||
|
||||
bool spiceInput_init(SpiceInput ** input)
|
||||
{
|
||||
*input = calloc(1, sizeof(**input));
|
||||
if (!*input)
|
||||
return false;
|
||||
|
||||
LG_LOCK_INIT((*input)->stateLock);
|
||||
LG_LOCK_INIT((*input)->statusDispatch);
|
||||
return true;
|
||||
}
|
||||
|
||||
void spiceInput_free(SpiceInput ** input)
|
||||
{
|
||||
if (!input || !*input)
|
||||
return;
|
||||
|
||||
LG_LOCK_FREE((*input)->statusDispatch);
|
||||
LG_LOCK_FREE((*input)->stateLock);
|
||||
free(*input);
|
||||
*input = NULL;
|
||||
}
|
||||
|
||||
const LG_InputOps * spiceInput_getOps(void)
|
||||
{
|
||||
return &l_inputOps;
|
||||
}
|
||||
|
||||
void spiceInput_setAvailable(SpiceInput * input, bool available)
|
||||
{
|
||||
LG_LOCK(input->statusDispatch);
|
||||
LG_LOCK(input->stateLock);
|
||||
|
||||
const bool changed = input->available != available;
|
||||
if (changed)
|
||||
{
|
||||
input->available = available;
|
||||
input->statusGeneration = nextGeneration(input->statusGeneration);
|
||||
}
|
||||
|
||||
const LG_InputStatusFn callback = input->statusCallback;
|
||||
void * callbackOpaque = input->statusOpaque;
|
||||
const LG_InputStatus status =
|
||||
{
|
||||
.available = input->available,
|
||||
.generation = input->statusGeneration,
|
||||
};
|
||||
LG_UNLOCK(input->stateLock);
|
||||
|
||||
if (changed && callback)
|
||||
callback(callbackOpaque, &status);
|
||||
LG_UNLOCK(input->statusDispatch);
|
||||
}
|
||||
@@ -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 <purespice.h>
|
||||
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
|
||||
404
client/transports/SPICE/session.c
Normal file
404
client/transports/SPICE/session.c
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "spice.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/time.h"
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define USB_REDIR_DISCONNECT_TIMEOUT_US UINT64_C(500000)
|
||||
|
||||
static _Atomic(LG_Transport *) l_callbackTarget;
|
||||
|
||||
static LG_Transport * callbackTarget(void)
|
||||
{
|
||||
return atomic_load_explicit(&l_callbackTarget, memory_order_acquire);
|
||||
}
|
||||
|
||||
static void ready(void)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (!transport)
|
||||
return;
|
||||
|
||||
snprintf(transport->session.version,
|
||||
sizeof(transport->session.version), "SPICE");
|
||||
transport->session.os = LG_TRANSPORT_OS_OTHER;
|
||||
|
||||
purespice_mouseMode(true);
|
||||
|
||||
PSServerInfo info;
|
||||
if (purespice_getServerInfo(&info))
|
||||
{
|
||||
if (info.name)
|
||||
snprintf(transport->session.name,
|
||||
sizeof(transport->session.name), "%s", info.name);
|
||||
|
||||
for (unsigned int i = 0; i < sizeof(info.uuid); ++i)
|
||||
if (info.uuid[i])
|
||||
{
|
||||
transport->session.uuidValid = true;
|
||||
memcpy(transport->session.uuid, info.uuid, sizeof(info.uuid));
|
||||
break;
|
||||
}
|
||||
|
||||
purespice_freeServerInfo(&info);
|
||||
}
|
||||
else
|
||||
DEBUG_WARN("Failed to obtain SPICE server information");
|
||||
|
||||
if (transport->input)
|
||||
spiceInput_setAvailable(transport->input, true);
|
||||
#if ENABLE_AUDIO
|
||||
if (transport->audio && !transport->usbAudioEnabled)
|
||||
spiceAudio_setAvailable(transport->audio, true);
|
||||
#endif
|
||||
|
||||
transport->connectStatus = LG_TRANSPORT_OK;
|
||||
atomic_store_explicit(
|
||||
&transport->sessionValid, true, memory_order_release);
|
||||
lgSignalEvent(transport->connectEvent);
|
||||
}
|
||||
|
||||
static void surfaceCreate(unsigned int surfaceId, PSSurfaceFormat format,
|
||||
unsigned int width, unsigned int height)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_create(
|
||||
transport->surface, surfaceId, format, width, height);
|
||||
}
|
||||
|
||||
static void surfaceDestroy(unsigned int surfaceId)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_destroy(transport->surface, surfaceId);
|
||||
}
|
||||
|
||||
static void drawFill(unsigned int surfaceId, int x, int y,
|
||||
int width, int height, uint32_t color)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_drawFill(
|
||||
transport->surface, surfaceId, x, y, width, height, color);
|
||||
}
|
||||
|
||||
static void drawBitmap(unsigned int surfaceId, PSBitmapFormat format,
|
||||
bool topDown, int x, int y, int width, int height, int stride, void * data)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_drawBitmap(transport->surface, surfaceId, format,
|
||||
topDown, x, y, width, height, stride, data);
|
||||
}
|
||||
|
||||
static void setRGBAImage(int width, int height, int hx, int hy,
|
||||
const void * data)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_setRGBAImage(
|
||||
transport->surface, width, height, hx, hy, data);
|
||||
}
|
||||
|
||||
static void setMonoImage(int width, int height, int hx, int hy,
|
||||
const void * xorMask, const void * andMask)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_setMonoImage(transport->surface,
|
||||
width, height, hx, hy, xorMask, andMask);
|
||||
}
|
||||
|
||||
static void setColorImage(int width, int height, int hx, int hy,
|
||||
const void * data, const void * maskData)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_setColorImage(transport->surface,
|
||||
width, height, hx, hy, data, maskData);
|
||||
}
|
||||
|
||||
static void setPointerState(bool visible, int x, int y)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport)
|
||||
spiceSurface_setPointerState(transport->surface, visible, x, y);
|
||||
}
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
static void playbackStart(int channels, int sampleRate,
|
||||
PSAudioFormat format, uint32_t time)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_playbackStart(
|
||||
transport->audio, channels, sampleRate, format, time);
|
||||
}
|
||||
|
||||
static void playbackVolume(int channels, const uint16_t volume[])
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_playbackVolume(transport->audio, channels, volume);
|
||||
}
|
||||
|
||||
static void playbackMute(bool mute)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_playbackMute(transport->audio, mute);
|
||||
}
|
||||
|
||||
static void playbackStop(void)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_playbackStop(transport->audio);
|
||||
}
|
||||
|
||||
static void playbackData(uint8_t * data, size_t size, uint32_t time)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_playbackData(transport->audio, data, size, time);
|
||||
}
|
||||
|
||||
static void recordStart(int channels, int sampleRate, PSAudioFormat format)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_recordStart(
|
||||
transport->audio, channels, sampleRate, format);
|
||||
}
|
||||
|
||||
static void recordVolume(int channels, const uint16_t volume[])
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_recordVolume(transport->audio, channels, volume);
|
||||
}
|
||||
|
||||
static void recordMute(bool mute)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_recordMute(transport->audio, mute);
|
||||
}
|
||||
|
||||
static void recordStop(void)
|
||||
{
|
||||
LG_Transport * transport = callbackTarget();
|
||||
if (transport && transport->audio)
|
||||
spiceAudio_recordStop(transport->audio);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void disconnectProviders(LG_Transport * transport)
|
||||
{
|
||||
atomic_store_explicit(
|
||||
&transport->sessionValid, false, memory_order_release);
|
||||
|
||||
if (transport->input)
|
||||
spiceInput_setAvailable(transport->input, false);
|
||||
#if ENABLE_AUDIO
|
||||
if (transport->audio)
|
||||
spiceAudio_setAvailable(transport->audio, false);
|
||||
#endif
|
||||
if (transport->clipboard)
|
||||
spiceClipboard_setAvailable(transport->clipboard, false);
|
||||
spiceSurface_sessionStopped(transport->surface);
|
||||
}
|
||||
|
||||
int spiceSession_thread(void * opaque)
|
||||
{
|
||||
LG_Transport * transport = opaque;
|
||||
LG_Transport * expected = NULL;
|
||||
if (!atomic_compare_exchange_strong_explicit(&l_callbackTarget,
|
||||
&expected, transport, memory_order_acq_rel, memory_order_acquire))
|
||||
{
|
||||
DEBUG_ERROR("A SPICE session is already active");
|
||||
lgSignalEvent(transport->connectEvent);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (transport->clipboard)
|
||||
spiceClipboard_setCallbackTarget(transport->clipboard);
|
||||
|
||||
const PSConfig config =
|
||||
{
|
||||
.host = transport->host,
|
||||
.port = transport->port,
|
||||
.password = "",
|
||||
.ready = ready,
|
||||
.inputs =
|
||||
{
|
||||
.enable = transport->inputEnabled,
|
||||
.autoConnect = true,
|
||||
},
|
||||
.clipboard =
|
||||
{
|
||||
.enable = transport->clipboardEnabled,
|
||||
.notice = spiceClipboard_notice,
|
||||
.data = spiceClipboard_data,
|
||||
.release = spiceClipboard_release,
|
||||
.request = spiceClipboard_request,
|
||||
.status = spiceClipboard_status,
|
||||
},
|
||||
.display =
|
||||
{
|
||||
.enable = true,
|
||||
.autoConnect = false,
|
||||
.surfaceCreate = surfaceCreate,
|
||||
.surfaceDestroy = surfaceDestroy,
|
||||
.drawFill = drawFill,
|
||||
.drawBitmap = drawBitmap,
|
||||
},
|
||||
.cursor =
|
||||
{
|
||||
.enable = true,
|
||||
.autoConnect = false,
|
||||
.setRGBAImage = setRGBAImage,
|
||||
.setMonoImage = setMonoImage,
|
||||
.setColorImage = setColorImage,
|
||||
.setState = setPointerState,
|
||||
},
|
||||
#if ENABLE_AUDIO
|
||||
.playback =
|
||||
{
|
||||
.enable = transport->playbackEnabled &&
|
||||
!transport->usbAudioEnabled,
|
||||
.autoConnect = true,
|
||||
.start = playbackStart,
|
||||
.volume = playbackVolume,
|
||||
.mute = playbackMute,
|
||||
.stop = playbackStop,
|
||||
.data = playbackData,
|
||||
},
|
||||
.record =
|
||||
{
|
||||
.enable = transport->recordEnabled &&
|
||||
!transport->usbAudioEnabled,
|
||||
.autoConnect = true,
|
||||
.start = recordStart,
|
||||
.volume = recordVolume,
|
||||
.mute = recordMute,
|
||||
.stop = recordStop,
|
||||
},
|
||||
#endif
|
||||
#if ENABLE_USB_AUDIO
|
||||
.usbRedir =
|
||||
{
|
||||
.enable = transport->usbAudio != NULL,
|
||||
.autoConnect = false,
|
||||
.opaque = transport->usbRedir,
|
||||
.state = lgUsbRedir_state,
|
||||
.data = lgUsbRedir_data,
|
||||
},
|
||||
#endif
|
||||
};
|
||||
|
||||
PSStatus status = PS_STATUS_SHUTDOWN;
|
||||
if (!purespice_connect(&config))
|
||||
{
|
||||
DEBUG_ERROR("Failed to connect to SPICE server");
|
||||
goto done;
|
||||
}
|
||||
|
||||
transport->connected = true;
|
||||
status = PS_STATUS_RUN;
|
||||
int processTimeout = 100;
|
||||
|
||||
while (!atomic_load_explicit(&transport->stop, memory_order_acquire))
|
||||
{
|
||||
#if ENABLE_USB_AUDIO
|
||||
if (transport->usbRedir && !lgUsbRedir_process(transport->usbRedir))
|
||||
DEBUG_WARN("Failed to process USB audio redirection");
|
||||
if (transport->usbAudio)
|
||||
{
|
||||
const uint64_t delay = lgaUsb_processDelayNs(transport->usbAudio);
|
||||
if (delay == UINT64_MAX)
|
||||
processTimeout = 10;
|
||||
else
|
||||
{
|
||||
const uint64_t timeout = delay / UINT64_C(1000000) +
|
||||
(delay % UINT64_C(1000000) != 0);
|
||||
processTimeout = (int)min(timeout, UINT64_C(10));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
status = purespice_process(processTimeout);
|
||||
if (status != PS_STATUS_RUN)
|
||||
{
|
||||
if (status != PS_STATUS_SHUTDOWN)
|
||||
DEBUG_ERROR("Failed to process SPICE messages");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectProviders(transport);
|
||||
|
||||
#if ENABLE_USB_AUDIO
|
||||
if (status == PS_STATUS_RUN && transport->usbRedir)
|
||||
{
|
||||
if (!lgUsbRedir_process(transport->usbRedir))
|
||||
DEBUG_WARN("Failed to disconnect USB audio device");
|
||||
else
|
||||
{
|
||||
const uint64_t deadline =
|
||||
microtime() + USB_REDIR_DISCONNECT_TIMEOUT_US;
|
||||
while (lgUsbRedir_disconnectPending(transport->usbRedir) &&
|
||||
microtime() < deadline)
|
||||
{
|
||||
status = purespice_process(10);
|
||||
if (status != PS_STATUS_RUN)
|
||||
break;
|
||||
}
|
||||
|
||||
if (status == PS_STATUS_RUN &&
|
||||
lgUsbRedir_disconnectPending(transport->usbRedir))
|
||||
DEBUG_WARN("Timed out disconnecting USB audio device");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
purespice_disconnect();
|
||||
transport->connected = false;
|
||||
|
||||
done:
|
||||
disconnectProviders(transport);
|
||||
if (transport->clipboard)
|
||||
spiceClipboard_setCallbackTarget(NULL);
|
||||
|
||||
expected = transport;
|
||||
atomic_compare_exchange_strong_explicit(&l_callbackTarget,
|
||||
&expected, NULL, memory_order_acq_rel, memory_order_acquire);
|
||||
lgSignalEvent(transport->connectEvent);
|
||||
return 0;
|
||||
}
|
||||
277
client/transports/SPICE/spice.c
Normal file
277
client/transports/SPICE/spice.c
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "spice.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/option.h"
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
#include "../../src/audio.h"
|
||||
#endif
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void spiceDisconnect(LG_Transport * transport);
|
||||
|
||||
static void spiceSetup(void)
|
||||
{
|
||||
const PSInit init =
|
||||
{
|
||||
.log =
|
||||
{
|
||||
.info = debug_info,
|
||||
.warn = debug_warn,
|
||||
.error = debug_error,
|
||||
},
|
||||
};
|
||||
purespice_init(&init);
|
||||
}
|
||||
|
||||
static bool spiceCreate(LG_Transport ** result)
|
||||
{
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
LG_Transport * transport = calloc(1, sizeof(*transport));
|
||||
if (!transport)
|
||||
return false;
|
||||
|
||||
transport->host = option_get_string("spice", "host");
|
||||
transport->port = option_get_int("spice", "port");
|
||||
transport->inputEnabled = option_get_bool("spice", "input");
|
||||
transport->clipboardEnabled = option_get_bool("spice", "clipboard");
|
||||
transport->audioEnabled = option_get_bool("spice", "audio");
|
||||
transport->usbAudioEnabled =
|
||||
option_get_bool("spice", "usbAudio");
|
||||
transport->audioDebug = option_get_bool("audio", "debug");
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
transport->playbackEnabled =
|
||||
transport->audioEnabled && lgAudio_supportsPlayback();
|
||||
transport->recordEnabled =
|
||||
transport->audioEnabled && lgAudio_supportsRecord();
|
||||
#else
|
||||
transport->audioEnabled = false;
|
||||
transport->usbAudioEnabled = false;
|
||||
#endif
|
||||
|
||||
#if !ENABLE_USB_AUDIO
|
||||
transport->usbAudioEnabled = false;
|
||||
#endif
|
||||
|
||||
if (!spiceSurface_init(&transport->surface) ||
|
||||
(transport->inputEnabled &&
|
||||
!spiceInput_init(&transport->input)) ||
|
||||
(transport->clipboardEnabled &&
|
||||
!spiceClipboard_init(&transport->clipboard)))
|
||||
goto fail;
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
if (transport->audioEnabled && !spiceAudio_init(&transport->audio))
|
||||
goto fail;
|
||||
#endif
|
||||
|
||||
#if ENABLE_USB_AUDIO
|
||||
if (transport->audioEnabled && transport->usbAudioEnabled)
|
||||
{
|
||||
if (!transport->playbackEnabled)
|
||||
{
|
||||
DEBUG_WARN("USB audio requires a playback backend, using SPICE audio");
|
||||
transport->usbAudioEnabled = false;
|
||||
}
|
||||
else if (!(transport->usbAudio =
|
||||
lgaUsb_create(transport->audioDebug)))
|
||||
{
|
||||
DEBUG_WARN("Failed to initialize USB audio, using SPICE audio");
|
||||
transport->usbAudioEnabled = false;
|
||||
}
|
||||
else
|
||||
transport->usbRedir = lgaUsb_redir(transport->usbAudio);
|
||||
}
|
||||
#endif
|
||||
|
||||
transport->connectEvent = lgCreateEvent(false, 0);
|
||||
if (!transport->connectEvent)
|
||||
goto fail;
|
||||
|
||||
atomic_init(&transport->stop, false);
|
||||
atomic_init(&transport->sessionValid, false);
|
||||
|
||||
*result = transport;
|
||||
return true;
|
||||
|
||||
fail:
|
||||
if (transport->connectEvent)
|
||||
lgFreeEvent(transport->connectEvent);
|
||||
#if ENABLE_USB_AUDIO
|
||||
lgaUsb_destroy(transport->usbAudio);
|
||||
#endif
|
||||
#if ENABLE_AUDIO
|
||||
spiceAudio_free(&transport->audio);
|
||||
#endif
|
||||
spiceClipboard_free(&transport->clipboard);
|
||||
spiceInput_free(&transport->input);
|
||||
spiceSurface_free(&transport->surface);
|
||||
free(transport);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void spiceDestroy(LG_Transport ** transport)
|
||||
{
|
||||
if (!transport || !*transport)
|
||||
return;
|
||||
|
||||
spiceDisconnect(*transport);
|
||||
lgFreeEvent((*transport)->connectEvent);
|
||||
#if ENABLE_USB_AUDIO
|
||||
lgaUsb_destroy((*transport)->usbAudio);
|
||||
#endif
|
||||
#if ENABLE_AUDIO
|
||||
spiceAudio_free(&(*transport)->audio);
|
||||
#endif
|
||||
spiceClipboard_free(&(*transport)->clipboard);
|
||||
spiceInput_free(&(*transport)->input);
|
||||
spiceSurface_free(&(*transport)->surface);
|
||||
free(*transport);
|
||||
*transport = NULL;
|
||||
}
|
||||
|
||||
static LG_TransportStatus spiceConnect(LG_Transport * transport,
|
||||
LG_TransportSession * session)
|
||||
{
|
||||
if (transport->thread)
|
||||
return LG_TRANSPORT_ERROR;
|
||||
|
||||
memset(&transport->session, 0, sizeof(transport->session));
|
||||
transport->connectStatus = LG_TRANSPORT_ERROR;
|
||||
atomic_store_explicit(&transport->stop, false, memory_order_release);
|
||||
atomic_store_explicit(
|
||||
&transport->sessionValid, false, memory_order_release);
|
||||
lgResetEvent(transport->connectEvent);
|
||||
|
||||
if (!lgCreateThread(
|
||||
"spiceProcess", spiceSession_thread, transport, &transport->thread))
|
||||
return LG_TRANSPORT_ERROR;
|
||||
|
||||
lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE);
|
||||
if (transport->connectStatus != LG_TRANSPORT_OK)
|
||||
{
|
||||
lgJoinThread(transport->thread, NULL);
|
||||
transport->thread = NULL;
|
||||
return transport->connectStatus;
|
||||
}
|
||||
|
||||
*session = transport->session;
|
||||
return LG_TRANSPORT_OK;
|
||||
}
|
||||
|
||||
static void spiceDisconnect(LG_Transport * transport)
|
||||
{
|
||||
if (!transport->thread)
|
||||
return;
|
||||
|
||||
atomic_store_explicit(&transport->stop, true, memory_order_release);
|
||||
lgJoinThread(transport->thread, NULL);
|
||||
transport->thread = NULL;
|
||||
}
|
||||
|
||||
static bool spiceSessionValid(LG_Transport * transport)
|
||||
{
|
||||
return atomic_load_explicit(
|
||||
&transport->sessionValid, memory_order_acquire);
|
||||
}
|
||||
|
||||
static const LG_VideoOps * spiceGetVideoOps(LG_Transport * transport)
|
||||
{
|
||||
(void)transport;
|
||||
return spiceSurface_getVideoOps();
|
||||
}
|
||||
|
||||
static const LG_InputOps * spiceGetInputOps(LG_Transport * transport,
|
||||
void ** opaque)
|
||||
{
|
||||
*opaque = transport->input;
|
||||
return transport->input ? spiceInput_getOps() : NULL;
|
||||
}
|
||||
|
||||
static const LG_AudioOps * spiceGetAudioOps(LG_Transport * transport,
|
||||
void ** opaque)
|
||||
{
|
||||
#if ENABLE_USB_AUDIO
|
||||
if (transport->usbAudio)
|
||||
{
|
||||
*opaque = transport->usbAudio;
|
||||
return &LGA_USB;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
*opaque = transport->audio;
|
||||
return transport->audio ? spiceAudio_getOps() : NULL;
|
||||
#else
|
||||
*opaque = NULL;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static const LG_ClipboardOps * spiceGetClipboardOps(LG_Transport * transport,
|
||||
void ** opaque)
|
||||
{
|
||||
*opaque = transport->clipboard;
|
||||
return transport->clipboard ? spiceClipboard_getOps() : NULL;
|
||||
}
|
||||
|
||||
static LG_TransportStatus spiceSendControl(LG_Transport * transport,
|
||||
const LG_TransportControl * control, LG_TransportControlToken * token)
|
||||
{
|
||||
(void)control;
|
||||
(void)token;
|
||||
return spiceSessionValid(transport) ?
|
||||
LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED;
|
||||
}
|
||||
|
||||
static LG_TransportStatus spiceControlStatus(LG_Transport * transport,
|
||||
LG_TransportControlToken token)
|
||||
{
|
||||
(void)token;
|
||||
return spiceSessionValid(transport) ?
|
||||
LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED;
|
||||
}
|
||||
|
||||
const LG_TransportOps LGT_SPICE =
|
||||
{
|
||||
.name = "spice",
|
||||
.setup = spiceSetup,
|
||||
.create = spiceCreate,
|
||||
.destroy = spiceDestroy,
|
||||
.connect = spiceConnect,
|
||||
.disconnect = spiceDisconnect,
|
||||
.sessionValid = spiceSessionValid,
|
||||
.getVideoOps = spiceGetVideoOps,
|
||||
.getInputOps = spiceGetInputOps,
|
||||
.getAudioOps = spiceGetAudioOps,
|
||||
.getClipboardOps = spiceGetClipboardOps,
|
||||
.sendControl = spiceSendControl,
|
||||
.controlStatus = spiceControlStatus,
|
||||
};
|
||||
77
client/transports/SPICE/spice.h
Normal file
77
client/transports/SPICE/spice.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifndef _H_LG_CLIENT_TRANSPORT_SPICE_
|
||||
#define _H_LG_CLIENT_TRANSPORT_SPICE_
|
||||
|
||||
#include "interface/transport.h"
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "input.h"
|
||||
#include "surface.h"
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
#include "audio.h"
|
||||
#endif
|
||||
|
||||
#if ENABLE_USB_AUDIO
|
||||
#include "audio_usb.h"
|
||||
#endif
|
||||
|
||||
#include "common/event.h"
|
||||
#include "common/thread.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
struct LG_Transport
|
||||
{
|
||||
const char * host;
|
||||
unsigned int port;
|
||||
bool inputEnabled;
|
||||
bool clipboardEnabled;
|
||||
bool audioEnabled;
|
||||
bool usbAudioEnabled;
|
||||
bool playbackEnabled;
|
||||
bool recordEnabled;
|
||||
bool audioDebug;
|
||||
|
||||
SpiceInput * input;
|
||||
SpiceClipboard * clipboard;
|
||||
SpiceSurface * surface;
|
||||
#if ENABLE_AUDIO
|
||||
SpiceAudio * audio;
|
||||
#endif
|
||||
#if ENABLE_USB_AUDIO
|
||||
LGA_USBState * usbAudio;
|
||||
LG_USBRedir * usbRedir;
|
||||
#endif
|
||||
|
||||
LGThread * thread;
|
||||
LGEvent * connectEvent;
|
||||
atomic_bool stop;
|
||||
atomic_bool sessionValid;
|
||||
bool connected;
|
||||
LG_TransportStatus connectStatus;
|
||||
LG_TransportSession session;
|
||||
};
|
||||
|
||||
int spiceSession_thread(void * opaque);
|
||||
|
||||
#endif
|
||||
394
client/transports/SPICE/surface.c
Normal file
394
client/transports/SPICE/surface.c
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "surface.h"
|
||||
|
||||
#include "spice.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/locking.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct SpiceSurface
|
||||
{
|
||||
LG_RWLock eventsLock;
|
||||
LG_Lock activationLock;
|
||||
|
||||
const LG_SwSurfaceEventOps * events;
|
||||
void * eventOpaque;
|
||||
bool active;
|
||||
bool primaryValid;
|
||||
int hotX;
|
||||
int hotY;
|
||||
};
|
||||
|
||||
static bool surfaceAttach(LG_Transport * transport,
|
||||
const LG_SwSurfaceEventOps * events, void * opaque)
|
||||
{
|
||||
if (!events || !events->configure || !events->destroy ||
|
||||
!events->drawFill || !events->drawBitmap || !events->pointer)
|
||||
return false;
|
||||
|
||||
SpiceSurface * surface = transport->surface;
|
||||
LG_LOCK_EXCLUSIVE(surface->eventsLock);
|
||||
surface->events = events;
|
||||
surface->eventOpaque = opaque;
|
||||
LG_UNLOCK_EXCLUSIVE(surface->eventsLock);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void surfaceDetach(LG_Transport * transport)
|
||||
{
|
||||
SpiceSurface * surface = transport->surface;
|
||||
LG_LOCK_EXCLUSIVE(surface->eventsLock);
|
||||
surface->events = NULL;
|
||||
surface->eventOpaque = NULL;
|
||||
LG_UNLOCK_EXCLUSIVE(surface->eventsLock);
|
||||
}
|
||||
|
||||
static bool surfaceSetActive(LG_Transport * transport, bool active)
|
||||
{
|
||||
SpiceSurface * surface = transport->surface;
|
||||
LG_LOCK(surface->activationLock);
|
||||
|
||||
if (surface->active == active)
|
||||
{
|
||||
LG_UNLOCK(surface->activationLock);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result = false;
|
||||
if (active)
|
||||
{
|
||||
LG_LOCK_SHARED(surface->eventsLock);
|
||||
const bool attached = surface->events != NULL;
|
||||
LG_UNLOCK_SHARED(surface->eventsLock);
|
||||
if (!attached)
|
||||
goto done;
|
||||
|
||||
if (!atomic_load_explicit(
|
||||
&transport->sessionValid, memory_order_acquire) ||
|
||||
!purespice_hasChannel(PS_CHANNEL_DISPLAY) ||
|
||||
!purespice_hasChannel(PS_CHANNEL_CURSOR) ||
|
||||
!purespice_connectChannel(PS_CHANNEL_DISPLAY))
|
||||
goto done;
|
||||
|
||||
if (!purespice_connectChannel(PS_CHANNEL_CURSOR))
|
||||
{
|
||||
purespice_disconnectChannel(PS_CHANNEL_DISPLAY);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!purespice_disconnectChannel(PS_CHANNEL_DISPLAY))
|
||||
goto done;
|
||||
|
||||
if (!purespice_disconnectChannel(PS_CHANNEL_CURSOR))
|
||||
{
|
||||
purespice_connectChannel(PS_CHANNEL_DISPLAY);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
surface->active = active;
|
||||
result = true;
|
||||
|
||||
done:
|
||||
LG_UNLOCK(surface->activationLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static const LG_SwSurfaceOps swSurfaceOps =
|
||||
{
|
||||
.attach = surfaceAttach,
|
||||
.detach = surfaceDetach,
|
||||
.setActive = surfaceSetActive,
|
||||
};
|
||||
|
||||
static const LG_VideoOps videoOps =
|
||||
{
|
||||
.name = "SPICE",
|
||||
.type = LG_VIDEO_TYPE_SW_SURFACE,
|
||||
.swSurface = &swSurfaceOps,
|
||||
};
|
||||
|
||||
bool spiceSurface_init(SpiceSurface ** result)
|
||||
{
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
SpiceSurface * surface = calloc(1, sizeof(*surface));
|
||||
if (!surface)
|
||||
return false;
|
||||
|
||||
LG_RWLOCK_INIT(surface->eventsLock);
|
||||
LG_LOCK_INIT(surface->activationLock);
|
||||
*result = surface;
|
||||
return true;
|
||||
}
|
||||
|
||||
void spiceSurface_free(SpiceSurface ** surface)
|
||||
{
|
||||
if (!surface || !*surface)
|
||||
return;
|
||||
|
||||
LG_LOCK_FREE((*surface)->activationLock);
|
||||
LG_RWLOCK_FREE((*surface)->eventsLock);
|
||||
free(*surface);
|
||||
*surface = NULL;
|
||||
}
|
||||
|
||||
const LG_VideoOps * spiceSurface_getVideoOps(void)
|
||||
{
|
||||
return &videoOps;
|
||||
}
|
||||
|
||||
void spiceSurface_sessionStopped(SpiceSurface * surface)
|
||||
{
|
||||
LG_LOCK(surface->activationLock);
|
||||
surface->active = false;
|
||||
surface->primaryValid = false;
|
||||
LG_UNLOCK(surface->activationLock);
|
||||
}
|
||||
|
||||
void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId,
|
||||
PSSurfaceFormat format, unsigned int width, unsigned int height)
|
||||
{
|
||||
if (surfaceId != 0)
|
||||
{
|
||||
DEBUG_INFO("Ignoring secondary SPICE surface: id: %u, size: %ux%u",
|
||||
surfaceId, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case PS_SURFACE_FMT_32_xRGB:
|
||||
case PS_SURFACE_FMT_32_ARGB:
|
||||
break;
|
||||
|
||||
default:
|
||||
DEBUG_ERROR("Unsupported primary SPICE surface format: %d", format);
|
||||
surface->primaryValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Create primary SPICE surface: id: %u, size: %ux%u",
|
||||
surfaceId, width, height);
|
||||
surface->primaryValid = true;
|
||||
|
||||
LG_LOCK_SHARED(surface->eventsLock);
|
||||
if (surface->events)
|
||||
{
|
||||
surface->events->configure(surface->eventOpaque, width, height);
|
||||
surface->events->drawFill(
|
||||
surface->eventOpaque, 0, 0, width, height, 0);
|
||||
}
|
||||
LG_UNLOCK_SHARED(surface->eventsLock);
|
||||
}
|
||||
|
||||
void spiceSurface_destroy(SpiceSurface * surface, unsigned int surfaceId)
|
||||
{
|
||||
if (!surface->primaryValid || surfaceId != 0)
|
||||
{
|
||||
DEBUG_INFO("Ignoring destruction of inactive or secondary SPICE surface %u",
|
||||
surfaceId);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Destroy primary SPICE surface %u", surfaceId);
|
||||
surface->primaryValid = false;
|
||||
|
||||
LG_LOCK_SHARED(surface->eventsLock);
|
||||
if (surface->events)
|
||||
surface->events->destroy(surface->eventOpaque);
|
||||
LG_UNLOCK_SHARED(surface->eventsLock);
|
||||
}
|
||||
|
||||
void spiceSurface_drawFill(SpiceSurface * surface, unsigned int surfaceId,
|
||||
int x, int y, int width, int height, uint32_t color)
|
||||
{
|
||||
if (!surface->primaryValid || surfaceId != 0)
|
||||
return;
|
||||
|
||||
LG_LOCK_SHARED(surface->eventsLock);
|
||||
if (surface->events)
|
||||
surface->events->drawFill(
|
||||
surface->eventOpaque, x, y, width, height, color);
|
||||
LG_UNLOCK_SHARED(surface->eventsLock);
|
||||
}
|
||||
|
||||
void spiceSurface_drawBitmap(SpiceSurface * surface, unsigned int surfaceId,
|
||||
PSBitmapFormat format, bool topDown, int x, int y,
|
||||
int width, int height, int stride, void * data)
|
||||
{
|
||||
if (!surface->primaryValid || surfaceId != 0)
|
||||
return;
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case PS_BITMAP_FMT_32BIT:
|
||||
case PS_BITMAP_FMT_RGBA:
|
||||
break;
|
||||
|
||||
default:
|
||||
DEBUG_ERROR("Unsupported SPICE bitmap format: %d", format);
|
||||
return;
|
||||
}
|
||||
|
||||
LG_LOCK_SHARED(surface->eventsLock);
|
||||
if (surface->events)
|
||||
surface->events->drawBitmap(surface->eventOpaque, topDown,
|
||||
x, y, width, height, stride, data);
|
||||
LG_UNLOCK_SHARED(surface->eventsLock);
|
||||
}
|
||||
|
||||
static void pointerEvent(SpiceSurface * surface,
|
||||
const LG_TransportPointer * pointer)
|
||||
{
|
||||
LG_LOCK_SHARED(surface->eventsLock);
|
||||
if (surface->events)
|
||||
surface->events->pointer(surface->eventOpaque, pointer);
|
||||
LG_UNLOCK_SHARED(surface->eventsLock);
|
||||
}
|
||||
|
||||
void spiceSurface_setRGBAImage(SpiceSurface * surface,
|
||||
int width, int height, int hx, int hy, const void * data)
|
||||
{
|
||||
surface->hotX = hx;
|
||||
surface->hotY = hy;
|
||||
|
||||
const uint8_t * rgba = data;
|
||||
uint8_t * bgra = malloc((size_t)width * height * 4);
|
||||
if (!bgra)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < width * height; ++i)
|
||||
{
|
||||
bgra[i * 4 + 0] = rgba[i * 4 + 2];
|
||||
bgra[i * 4 + 1] = rgba[i * 4 + 1];
|
||||
bgra[i * 4 + 2] = rgba[i * 4 + 0];
|
||||
bgra[i * 4 + 3] = rgba[i * 4 + 3];
|
||||
}
|
||||
|
||||
const LG_TransportPointer pointer =
|
||||
{
|
||||
.flags = LG_TRANSPORT_POINTER_SHAPE,
|
||||
.type = CURSOR_TYPE_COLOR,
|
||||
.hx = hx,
|
||||
.hy = hy,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.pitch = width * 4,
|
||||
.shape = bgra,
|
||||
};
|
||||
pointerEvent(surface, &pointer);
|
||||
free(bgra);
|
||||
}
|
||||
|
||||
void spiceSurface_setMonoImage(SpiceSurface * surface,
|
||||
int width, int height, int hx, int hy,
|
||||
const void * xorMask, const void * andMask)
|
||||
{
|
||||
surface->hotX = hx;
|
||||
surface->hotY = hy;
|
||||
|
||||
const int stride = (width + 7) / 8;
|
||||
uint8_t * buffer = malloc((size_t)stride * height * 2);
|
||||
if (!buffer)
|
||||
return;
|
||||
|
||||
memcpy(buffer, andMask, (size_t)stride * height);
|
||||
memcpy(buffer + (size_t)stride * height,
|
||||
xorMask, (size_t)stride * height);
|
||||
|
||||
const LG_TransportPointer pointer =
|
||||
{
|
||||
.flags = LG_TRANSPORT_POINTER_SHAPE,
|
||||
.type = CURSOR_TYPE_MONOCHROME,
|
||||
.hx = hx,
|
||||
.hy = hy,
|
||||
.width = width,
|
||||
.height = height * 2,
|
||||
.pitch = stride,
|
||||
.shape = buffer,
|
||||
};
|
||||
pointerEvent(surface, &pointer);
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
void spiceSurface_setColorImage(SpiceSurface * surface,
|
||||
int width, int height, int hx, int hy,
|
||||
const void * data, const void * maskData)
|
||||
{
|
||||
surface->hotX = hx;
|
||||
surface->hotY = hy;
|
||||
|
||||
const uint8_t * rgba = data;
|
||||
const uint8_t * andMask = maskData;
|
||||
const int maskPitch = (width + 7) / 8;
|
||||
uint8_t * bgra = malloc((size_t)width * height * 4);
|
||||
if (!bgra)
|
||||
return;
|
||||
|
||||
for (int y = 0; y < height; ++y)
|
||||
for (int x = 0; x < width; ++x)
|
||||
{
|
||||
const int i = y * width + x;
|
||||
bgra[i * 4 + 0] = rgba[i * 4 + 2];
|
||||
bgra[i * 4 + 1] = rgba[i * 4 + 1];
|
||||
bgra[i * 4 + 2] = rgba[i * 4 + 0];
|
||||
bgra[i * 4 + 3] = andMask[y * maskPitch + x / 8] &
|
||||
(0x80U >> (x % 8)) ? 255 : 0;
|
||||
}
|
||||
|
||||
const LG_TransportPointer pointer =
|
||||
{
|
||||
.flags = LG_TRANSPORT_POINTER_SHAPE,
|
||||
.type = CURSOR_TYPE_MASKED_COLOR,
|
||||
.hx = hx,
|
||||
.hy = hy,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.pitch = width * 4,
|
||||
.shape = bgra,
|
||||
};
|
||||
pointerEvent(surface, &pointer);
|
||||
free(bgra);
|
||||
}
|
||||
|
||||
void spiceSurface_setPointerState(SpiceSurface * surface,
|
||||
bool visible, int x, int y)
|
||||
{
|
||||
const LG_TransportPointer pointer =
|
||||
{
|
||||
.flags = LG_TRANSPORT_POINTER_POSITION |
|
||||
LG_TRANSPORT_POINTER_VISIBLE_VALID |
|
||||
(visible ? LG_TRANSPORT_POINTER_VISIBLE : 0),
|
||||
.x = x,
|
||||
.y = y,
|
||||
.hx = surface->hotX,
|
||||
.hy = surface->hotY,
|
||||
};
|
||||
pointerEvent(surface, &pointer);
|
||||
}
|
||||
56
client/transports/SPICE/surface.h
Normal file
56
client/transports/SPICE/surface.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2026 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifndef _H_LG_CLIENT_TRANSPORT_SPICE_SURFACE_
|
||||
#define _H_LG_CLIENT_TRANSPORT_SPICE_SURFACE_
|
||||
|
||||
#include "interface/transport.h"
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
typedef struct SpiceSurface SpiceSurface;
|
||||
|
||||
bool spiceSurface_init(SpiceSurface ** surface);
|
||||
void spiceSurface_free(SpiceSurface ** surface);
|
||||
|
||||
const LG_VideoOps * spiceSurface_getVideoOps(void);
|
||||
void spiceSurface_sessionStopped(SpiceSurface * surface);
|
||||
|
||||
void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId,
|
||||
PSSurfaceFormat format, unsigned int width, unsigned int height);
|
||||
void spiceSurface_destroy(SpiceSurface * surface, unsigned int surfaceId);
|
||||
void spiceSurface_drawFill(SpiceSurface * surface, unsigned int surfaceId,
|
||||
int x, int y, int width, int height, uint32_t color);
|
||||
void spiceSurface_drawBitmap(SpiceSurface * surface, unsigned int surfaceId,
|
||||
PSBitmapFormat format, bool topDown, int x, int y,
|
||||
int width, int height, int stride, void * data);
|
||||
|
||||
void spiceSurface_setRGBAImage(SpiceSurface * surface,
|
||||
int width, int height, int hx, int hy, const void * data);
|
||||
void spiceSurface_setMonoImage(SpiceSurface * surface,
|
||||
int width, int height, int hx, int hy,
|
||||
const void * xorMask, const void * andMask);
|
||||
void spiceSurface_setColorImage(SpiceSurface * surface,
|
||||
int width, int height, int hx, int hy,
|
||||
const void * data, const void * maskData);
|
||||
void spiceSurface_setPointerState(SpiceSurface * surface,
|
||||
bool visible, int x, int y);
|
||||
|
||||
#endif
|
||||
@@ -21,36 +21,19 @@
|
||||
#ifndef _H_LG_CLIENT_USBREDIR_
|
||||
#define _H_LG_CLIENT_USBREDIR_
|
||||
|
||||
#include "interface/usbredir.h"
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user