[client] spice: move protocol into transport

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

View File

@@ -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();