[client] transport: manage persistent fallback sessions

This commit is contained in:
Geoffrey McRae
2026-08-11 16:16:19 +10:00
parent 5b12685e3c
commit 053d4e9d9b
8 changed files with 764 additions and 165 deletions

View File

@@ -187,6 +187,7 @@ set(SOURCES
src/render_queue.c src/render_queue.c
src/evdev.c src/evdev.c
src/transport.c src/transport.c
src/transport_fallback.c
src/input.c src/input.c
src/overlay/splash.c src/overlay/splash.c

View File

@@ -36,6 +36,8 @@
typedef struct LG_Transport LG_Transport; typedef struct LG_Transport LG_Transport;
typedef struct LG_RendererInterop LG_RendererInterop; typedef struct LG_RendererInterop LG_RendererInterop;
typedef bool (*LG_TransportCancelledFn)(void * opaque);
typedef enum LG_TransportStatus typedef enum LG_TransportStatus
{ {
LG_TRANSPORT_OK, LG_TRANSPORT_OK,
@@ -320,6 +322,11 @@ typedef struct LG_TransportOps
LG_TransportStatus (*connect)(LG_Transport * transport, LG_TransportStatus (*connect)(LG_Transport * transport,
LG_TransportSession * session); LG_TransportSession * session);
/* Optional cancellable form used by transports which may wait while
* establishing a session. */
LG_TransportStatus (*connectCancellable)(LG_Transport * transport,
LG_TransportSession * session, LG_TransportCancelledFn cancelled,
void * opaque);
void (*disconnect)(LG_Transport * transport); void (*disconnect)(LG_Transport * transport);
bool (*sessionValid)(LG_Transport * transport); bool (*sessionValid)(LG_Transport * transport);
/* Queried after create. The returned operations remain valid until the /* Queried after create. The returned operations remain valid until the

View File

@@ -1151,16 +1151,14 @@ void app_stopVideo(bool stop)
bool app_useSpiceDisplay(bool enable) bool app_useSpiceDisplay(bool enable)
{ {
if (!g_params.useSpice || !g_state.fallbackVideoOps || if (!g_params.useSpice || !g_state.fallback)
g_state.fallbackVideoOps->type != LG_VIDEO_TYPE_SW_SURFACE ||
!g_state.fallbackVideoOps->swSurface)
return false; return false;
atomic_store_explicit(&g_state.fallbackDisplayRequested, enable, atomic_store_explicit(&g_state.fallbackDisplayRequested, enable,
memory_order_release); memory_order_release);
// if the fallback is not yet ready, retain the requested state // if the fallback is not yet ready, retain the requested state
if (!atomic_load_explicit(&g_state.fallbackReady, memory_order_acquire)) if (!lgTransportFallback_ready(g_state.fallback))
return false; return false;
bool active = atomic_load_explicit(&g_state.fallbackDisplayActive, bool active = atomic_load_explicit(&g_state.fallbackDisplayActive,
@@ -1189,8 +1187,7 @@ bool app_useSpiceDisplay(bool enable)
if (active == enable) if (active == enable)
goto done; goto done;
if (!g_state.fallbackVideoOps->swSurface->setActive( if (!lgTransportFallback_setVideoActive(g_state.fallback, enable))
g_state.fallbackTransport.handle, enable))
goto fail; goto fail;
renderQueue_swSurfaceShow(enable); renderQueue_swSurfaceShow(enable);
@@ -1215,6 +1212,7 @@ done:
fail: fail:
DEBUG_ERROR("Failed to %s the SPICE display", DEBUG_ERROR("Failed to %s the SPICE display",
enable ? "enable" : "disable"); enable ? "enable" : "disable");
lgTransportFallback_setVideoActive(g_state.fallback, active);
atomic_store_explicit(&g_state.fallbackDisplayRequested, active, atomic_store_explicit(&g_state.fallbackDisplayRequested, active,
memory_order_release); memory_order_release);
goto done; goto done;

View File

@@ -1508,8 +1508,6 @@ static void fallbackSurfaceDestroy(void * opaque)
{ {
(void)opaque; (void)opaque;
g_state.fallbackSurfaceValid = false; g_state.fallbackSurfaceValid = false;
atomic_store_explicit(&g_state.fallbackDisplayRequested, false,
memory_order_release);
if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false, if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false,
memory_order_acq_rel)) memory_order_acq_rel))
{ {
@@ -1585,137 +1583,73 @@ static const LG_SwSurfaceEventOps fallbackSurfaceEvents =
.pointer = fallbackSurfacePointer, .pointer = fallbackSurfacePointer,
}; };
static void fallbackDisconnect(void) static void fallbackConnected(void * opaque,
const LG_TransportSession * session)
{ {
if (!g_state.fallbackTransport.ops) (void)opaque;
return; if (session->name[0] && !atomic_load_explicit(
&g_state.lgHostConnected, memory_order_acquire))
core_setTitle(session->name);
const bool connected = g_state.fallbackTransport.handle && if (atomic_load_explicit(&g_state.fallbackDisplayRequested,
g_state.fallbackTransport.ops->sessionValid( memory_order_acquire))
g_state.fallbackTransport.handle); app_useSpiceDisplay(true);
}
static void fallbackDisconnected(void * opaque)
{
fallbackSurfaceDestroy(opaque);
if (!atomic_load_explicit(
&g_state.lgHostConnected, memory_order_acquire))
overlaySplash_show(true);
}
static void fallbackUUIDMismatch(void * opaque, const uint8_t primary[16],
const uint8_t fallback[16])
{
(void)opaque;
(void)primary;
(void)fallback;
atomic_store_explicit( atomic_store_explicit(
&g_state.fallbackReady, false, memory_order_release); &g_state.fallbackUUIDMismatch, true, memory_order_release);
app_invalidateWindow(false);
}
if (connected) static const LG_TransportFallbackEventOps fallbackEvents =
{ {
lgInput_setFallback(NULL, NULL); .connected = fallbackConnected,
lgAudio_setFallback(NULL, NULL); .disconnected = fallbackDisconnected,
lgClipboard_setFallback(NULL, NULL); .uuidMismatch = fallbackUUIDMismatch,
} };
else
{
lgInput_dropFallback();
lgAudio_dropFallback();
lgClipboard_dropFallback();
}
if (g_state.fallbackVideoOps && static bool fallbackStart(void)
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) if (!g_params.useSpice || strcmp(g_params.transport, "spice") == 0)
return true; return true;
if (!lgTransport_create("spice", &g_state.fallbackTransport)) if (lgTransportFallback_start("spice", &fallbackSurfaceEvents, NULL,
{ &fallbackEvents, NULL, &g_state.fallback))
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; return true;
DEBUG_ERROR("Failed to start the SPICE fallback transport");
return false;
} }
static void checkUUID(void) static void fallbackStop(void)
{ {
if (!atomic_load_explicit(&g_state.fallbackReady, memory_order_acquire) || lgTransportFallback_stop(&g_state.fallback);
!g_state.fallbackSession.uuidValid || !g_state.guestUUIDValid) }
return;
if (memcmp(g_state.fallbackSession.uuid, g_state.guestUUID, static void fallbackHandleEvents(void)
sizeof(g_state.guestUUID)) == 0) {
if (!atomic_exchange_explicit(
&g_state.fallbackUUIDMismatch, false, memory_order_acq_rel))
return; return;
app_msgBox( app_msgBox(
"SPICE Configuration Error", "SPICE Configuration Error",
"You have connected SPICE to the wrong guest.\n" "You have connected SPICE to the wrong guest.\n"
"SPICE fallback services will not function until this is corrected."); "SPICE fallback services will not function until this is corrected.");
fallbackDisconnect();
} }
void intHandler(int sig) void intHandler(int sig)
@@ -1965,7 +1899,7 @@ static int lg_run(void)
g_state.micDefaultState = g_params.micDefaultState; g_state.micDefaultState = g_params.micDefaultState;
if (!fallbackConnect()) if (!fallbackStart())
return -1; return -1;
// select and init a renderer // select and init a renderer
@@ -2072,6 +2006,7 @@ static int lg_run(void)
} }
keybind_commonRegister(); keybind_commonRegister();
keybind_inputRegister();
if (g_state.jitRender) if (g_state.jitRender)
DEBUG_INFO("Using JIT render mode"); DEBUG_INFO("Using JIT render mode");
@@ -2127,15 +2062,7 @@ restart:
while(app_getState() == APP_STATE_RUNNING) while(app_getState() == APP_STATE_RUNNING)
{ {
if (atomic_load_explicit( fallbackHandleEvents();
&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) if (initialSpiceEnable && microtime() > initialSpiceEnable)
{ {
@@ -2158,7 +2085,10 @@ restart:
while (app_getState() == APP_STATE_RUNNING && while (app_getState() == APP_STATE_RUNNING &&
!atomic_load_explicit(&probe.done, memory_order_acquire)) !atomic_load_explicit(&probe.done, memory_order_acquire))
{
g_state.ds->wait(100); g_state.ds->wait(100);
fallbackHandleEvents();
}
if (!lgJoinThread(probeThread, NULL)) if (!lgJoinThread(probeThread, NULL))
{ {
@@ -2274,7 +2204,13 @@ restart:
g_state.guestOS = session.os; g_state.guestOS = session.os;
g_state.guestUUIDValid = session.uuidValid; g_state.guestUUIDValid = session.uuidValid;
if (session.uuidValid) if (session.uuidValid)
{
memcpy(g_state.guestUUID, session.uuid, sizeof(g_state.guestUUID)); memcpy(g_state.guestUUID, session.uuid, sizeof(g_state.guestUUID));
lgTransportFallback_setPrimaryUUID(
g_state.fallback, g_state.guestUUID);
}
else
lgTransportFallback_clearPrimaryUUID(g_state.fallback);
g_state.transportFeatures = session.features; g_state.transportFeatures = session.features;
frameScheduler_start(session.features); frameScheduler_start(session.features);
@@ -2297,9 +2233,6 @@ restart:
g_state.transport.handle, &clipboardOpaque) : NULL; g_state.transport.handle, &clipboardOpaque) : NULL;
lgClipboard_setTransport(clipboardOps, clipboardOpaque); lgClipboard_setTransport(clipboardOps, clipboardOpaque);
if (inputOps || lgInput_available())
keybind_inputRegister();
checkUUID();
DEBUG_INFO("Starting session"); DEBUG_INFO("Starting session");
atomic_store_explicit( atomic_store_explicit(
&g_state.lgHostConnected, true, memory_order_release); &g_state.lgHostConnected, true, memory_order_release);
@@ -2311,15 +2244,7 @@ restart:
while(likely(app_getState() == APP_STATE_RUNNING)) while(likely(app_getState() == APP_STATE_RUNNING))
{ {
if (atomic_load_explicit( fallbackHandleEvents();
&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( if (unlikely(!g_state.transport.ops->sessionValid(
g_state.transport.handle))) g_state.transport.handle)))
@@ -2342,6 +2267,11 @@ restart:
if (app_getState() == APP_STATE_RESTART) if (app_getState() == APP_STATE_RESTART)
{ {
atomic_store_explicit(
&g_state.lgHostConnected, false, memory_order_release);
g_state.guestUUIDValid = false;
lgTransportFallback_clearPrimaryUUID(g_state.fallback);
lgSignalEvent(e_startup); lgSignalEvent(e_startup);
lgSignalEvent(g_state.frameEvent); lgSignalEvent(g_state.frameEvent);
@@ -2368,8 +2298,6 @@ static void lg_shutdown(void)
if (e_cursorRepaint) if (e_cursorRepaint)
lgSignalEvent(e_cursorRepaint); lgSignalEvent(e_cursorRepaint);
fallbackDisconnect();
if (t_render) if (t_render)
{ {
if (g_state.jitRender && g_state.ds->stopWaitFrame) if (g_state.jitRender && g_state.ds->stopWaitFrame)
@@ -2379,6 +2307,8 @@ static void lg_shutdown(void)
lgJoinThread(t_render, NULL); lgJoinThread(t_render, NULL);
} }
fallbackStop();
if (t_cursorRepaint) if (t_cursorRepaint)
{ {
lgJoinThread(t_cursorRepaint, NULL); lgJoinThread(t_cursorRepaint, NULL);

View File

@@ -35,6 +35,7 @@
#include "cimgui.h" #include "cimgui.h"
#include "interface/transport.h" #include "interface/transport.h"
#include "transport_fallback.h"
enum RunState enum RunState
{ {
@@ -78,10 +79,10 @@ struct AppState
bool dsInitialized; bool dsInitialized;
bool jitRender; bool jitRender;
atomic_bool fallbackReady;
atomic_bool fallbackDisplayRequested; atomic_bool fallbackDisplayRequested;
atomic_bool fallbackDisplayActive; atomic_bool fallbackDisplayActive;
atomic_bool fallbackDisplayTransition; atomic_bool fallbackDisplayTransition;
atomic_bool fallbackUUIDMismatch;
bool fallbackSurfaceValid; bool fallbackSurfaceValid;
uint8_t guestUUID[16]; uint8_t guestUUID[16];
@@ -123,9 +124,7 @@ struct AppState
const LG_VideoOps * videoOps; const LG_VideoOps * videoOps;
LG_TransportFeatureFlags transportFeatures; LG_TransportFeatureFlags transportFeatures;
LG_TransportInstance fallbackTransport; LG_TransportFallback * fallback;
LG_TransportSession fallbackSession;
const LG_VideoOps * fallbackVideoOps;
LGThread * cursorThread; LGThread * cursorThread;
LGThread * frameThread; LGThread * frameThread;

View File

@@ -0,0 +1,582 @@
/**
* 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 "transport_fallback.h"
#include "audio.h"
#include "clipboard.h"
#include "input.h"
#include "common/debug.h"
#include "common/event.h"
#include "common/locking.h"
#include "common/thread.h"
#include "common/util.h"
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#define RETRY_INITIAL_MS 250U
#define RETRY_MAX_MS 5000U
#define SESSION_POLL_MS 100U
struct LG_TransportFallback
{
char * transportName;
LG_SwSurfaceEventOps surfaceEvents;
void * surfaceOpaque;
LG_TransportFallbackEventOps eventOps;
void * eventOpaque;
LGThread * thread;
LGEvent * wakeEvent;
LGEvent * videoIdleEvent;
LG_RWLock lock;
atomic_bool stop;
atomic_bool ready;
LG_TransportInstance transport;
LG_TransportSession session;
const LG_VideoOps * videoOps;
uint64_t connectionSerial;
bool attached;
bool connected;
bool providersPublished;
bool connectedReported;
bool closing;
bool videoRequested;
bool videoActive;
bool videoBusy;
bool primaryUUIDValid;
uint8_t primaryUUID[16];
bool mismatchReported;
uint8_t mismatchPrimary[16];
uint8_t mismatchFallback[16];
};
static bool connectCancelled(void * opaque)
{
const LG_TransportFallback * fallback = opaque;
return atomic_load_explicit(&fallback->stop, memory_order_acquire);
}
static bool uuidMismatchLocked(const LG_TransportFallback * fallback)
{
return fallback->primaryUUIDValid && fallback->session.uuidValid &&
memcmp(fallback->primaryUUID, fallback->session.uuid,
sizeof(fallback->primaryUUID)) != 0;
}
static bool recordMismatchLocked(LG_TransportFallback * fallback,
uint8_t primary[16], uint8_t remote[16])
{
if (!uuidMismatchLocked(fallback))
return false;
const bool duplicate = fallback->mismatchReported &&
memcmp(fallback->mismatchPrimary, fallback->primaryUUID,
sizeof(fallback->mismatchPrimary)) == 0 &&
memcmp(fallback->mismatchFallback, fallback->session.uuid,
sizeof(fallback->mismatchFallback)) == 0;
if (duplicate)
return false;
memcpy(fallback->mismatchPrimary, fallback->primaryUUID,
sizeof(fallback->mismatchPrimary));
memcpy(fallback->mismatchFallback, fallback->session.uuid,
sizeof(fallback->mismatchFallback));
memcpy(primary, fallback->primaryUUID, sizeof(fallback->primaryUUID));
memcpy(remote, fallback->session.uuid, sizeof(fallback->session.uuid));
fallback->mismatchReported = true;
return true;
}
static bool sessionLive(const LG_TransportFallback * fallback)
{
return fallback->connected && fallback->transport.ops &&
fallback->transport.ops->sessionValid(fallback->transport.handle);
}
static void unpublishProviders(LG_TransportFallback * fallback, bool live)
{
if (!fallback->providersPublished)
return;
if (live)
{
lgInput_setFallback(NULL, NULL);
lgAudio_setFallback(NULL, NULL);
lgClipboard_setFallback(NULL, NULL);
}
else
{
lgInput_dropFallback();
lgAudio_dropFallback();
lgClipboard_dropFallback();
}
fallback->providersPublished = false;
}
static void waitForVideoIdle(LG_TransportFallback * fallback)
{
for (;;)
{
LG_LOCK_EXCLUSIVE(fallback->lock);
atomic_store_explicit(&fallback->ready, false, memory_order_release);
fallback->closing = true;
const bool busy = fallback->videoBusy;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (!busy)
return;
lgWaitEvent(fallback->videoIdleEvent, TIMEOUT_INFINITE);
}
}
static bool cleanupConnection(LG_TransportFallback * fallback,
bool knownDead)
{
waitForVideoIdle(fallback);
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool reportDisconnected = fallback->connectedReported;
fallback->connectedReported = false;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
const bool live = !knownDead && sessionLive(fallback);
unpublishProviders(fallback, live);
if (fallback->attached)
{
if (live && fallback->videoActive)
fallback->videoOps->swSurface->setActive(
fallback->transport.handle, false);
fallback->videoActive = false;
fallback->videoOps->swSurface->detach(fallback->transport.handle);
fallback->attached = false;
}
if (fallback->transport.ops)
fallback->transport.ops->disconnect(fallback->transport.handle);
lgTransport_destroy(&fallback->transport);
LG_LOCK_EXCLUSIVE(fallback->lock);
fallback->session = (LG_TransportSession) { 0 };
fallback->videoOps = NULL;
fallback->connected = false;
fallback->closing = false;
++fallback->connectionSerial;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
return reportDisconnected;
}
static void publishProviders(LG_TransportFallback * fallback)
{
void * inputOpaque = NULL;
const LG_InputOps * inputOps = fallback->transport.ops->getInputOps ?
fallback->transport.ops->getInputOps(
fallback->transport.handle, &inputOpaque) : NULL;
lgInput_setFallback(inputOps, inputOpaque);
void * audioOpaque = NULL;
const LG_AudioOps * audioOps = fallback->transport.ops->getAudioOps ?
fallback->transport.ops->getAudioOps(
fallback->transport.handle, &audioOpaque) : NULL;
lgAudio_setFallback(audioOps, audioOpaque);
void * clipboardOpaque = NULL;
const LG_ClipboardOps * clipboardOps =
fallback->transport.ops->getClipboardOps ?
fallback->transport.ops->getClipboardOps(
fallback->transport.handle, &clipboardOpaque) : NULL;
lgClipboard_setFallback(clipboardOps, clipboardOpaque);
fallback->providersPublished = true;
}
static bool publishConnection(LG_TransportFallback * fallback,
bool * reportMismatch, uint8_t primaryUUID[16],
uint8_t fallbackUUID[16])
{
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool reject = uuidMismatchLocked(fallback);
if (reject)
*reportMismatch = recordMismatchLocked(
fallback, primaryUUID, fallbackUUID);
const bool stop = atomic_load_explicit(
&fallback->stop, memory_order_acquire);
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (reject || stop)
return false;
publishProviders(fallback);
for (;;)
{
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool reject = uuidMismatchLocked(fallback);
if (reject)
*reportMismatch = recordMismatchLocked(
fallback, primaryUUID, fallbackUUID);
const bool stop = atomic_load_explicit(
&fallback->stop, memory_order_acquire);
const bool requested = fallback->videoRequested;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (reject || stop)
return false;
if (requested == fallback->videoActive)
break;
if (!fallback->videoOps->swSurface->setActive(
fallback->transport.handle, requested))
break;
fallback->videoActive = requested;
}
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool finalReject = uuidMismatchLocked(fallback);
if (finalReject)
*reportMismatch = recordMismatchLocked(
fallback, primaryUUID, fallbackUUID);
const bool finalStop = atomic_load_explicit(
&fallback->stop, memory_order_acquire);
if (!finalReject && !finalStop)
{
fallback->mismatchReported = false;
atomic_store_explicit(&fallback->ready, true, memory_order_release);
}
LG_UNLOCK_EXCLUSIVE(fallback->lock);
return !finalReject && !finalStop;
}
static bool connectFallback(LG_TransportFallback * fallback)
{
LG_TransportInstance transport = { 0 };
if (!lgTransport_create(fallback->transportName, &transport))
{
DEBUG_ERROR("Failed to create fallback transport %s",
fallback->transportName);
return false;
}
const LG_VideoOps * videoOps = transport.ops->getVideoOps ?
transport.ops->getVideoOps(transport.handle) : NULL;
if (!videoOps || videoOps->type != LG_VIDEO_TYPE_SW_SURFACE ||
!videoOps->swSurface || !videoOps->swSurface->attach ||
!videoOps->swSurface->detach || !videoOps->swSurface->setActive)
{
DEBUG_ERROR("Fallback transport %s does not provide a software surface",
fallback->transportName);
lgTransport_destroy(&transport);
return false;
}
if (!videoOps->swSurface->attach(transport.handle,
&fallback->surfaceEvents, fallback->surfaceOpaque))
{
DEBUG_ERROR("Failed to attach fallback software surface");
lgTransport_destroy(&transport);
return false;
}
LG_LOCK_EXCLUSIVE(fallback->lock);
fallback->transport = transport;
fallback->videoOps = videoOps;
fallback->attached = true;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
LG_TransportSession session = { 0 };
const LG_TransportStatus status = transport.ops->connectCancellable ?
transport.ops->connectCancellable(transport.handle, &session,
connectCancelled, fallback) :
transport.ops->connect(transport.handle, &session);
if (status != LG_TRANSPORT_OK)
{
if (!atomic_load_explicit(&fallback->stop, memory_order_acquire))
DEBUG_ERROR("Fallback transport %s failed to connect: %d",
fallback->transportName, status);
cleanupConnection(fallback, false);
return false;
}
LG_LOCK_EXCLUSIVE(fallback->lock);
fallback->connected = true;
fallback->session = session;
++fallback->connectionSerial;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
bool reportMismatch = false;
uint8_t primaryUUID[16];
uint8_t fallbackUUID[16];
if (!publishConnection(fallback, &reportMismatch,
primaryUUID, fallbackUUID))
{
cleanupConnection(fallback, false);
if (reportMismatch && fallback->eventOps.uuidMismatch)
fallback->eventOps.uuidMismatch(
fallback->eventOpaque, primaryUUID, fallbackUUID);
return false;
}
LG_LOCK_EXCLUSIVE(fallback->lock);
fallback->connectedReported = true;
const LG_TransportSession reportedSession = fallback->session;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (fallback->eventOps.connected)
fallback->eventOps.connected(
fallback->eventOpaque, &reportedSession);
while (!atomic_load_explicit(&fallback->stop, memory_order_acquire))
{
lgWaitEvent(fallback->wakeEvent, SESSION_POLL_MS);
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool reject = uuidMismatchLocked(fallback);
if (reject)
reportMismatch = recordMismatchLocked(
fallback, primaryUUID, fallbackUUID);
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (reject)
break;
if (!sessionLive(fallback))
break;
}
const bool knownDead = !atomic_load_explicit(
&fallback->stop, memory_order_acquire) && !sessionLive(fallback);
const bool reportDisconnect = cleanupConnection(fallback, knownDead);
if (reportMismatch && fallback->eventOps.uuidMismatch)
fallback->eventOps.uuidMismatch(
fallback->eventOpaque, primaryUUID, fallbackUUID);
if (reportDisconnect && fallback->eventOps.disconnected)
fallback->eventOps.disconnected(fallback->eventOpaque);
return true;
}
static int fallbackThread(void * opaque)
{
LG_TransportFallback * fallback = opaque;
unsigned int retry = RETRY_INITIAL_MS;
while (!atomic_load_explicit(&fallback->stop, memory_order_acquire))
{
if (connectFallback(fallback))
retry = RETRY_INITIAL_MS;
if (atomic_load_explicit(&fallback->stop, memory_order_acquire))
break;
lgWaitEvent(fallback->wakeEvent, retry);
retry = min(retry * 2U, RETRY_MAX_MS);
}
const bool reportDisconnect = cleanupConnection(fallback, false);
if (reportDisconnect && fallback->eventOps.disconnected)
fallback->eventOps.disconnected(fallback->eventOpaque);
return 0;
}
bool lgTransportFallback_start(const char * transportName,
const LG_SwSurfaceEventOps * surfaceEvents, void * surfaceOpaque,
const LG_TransportFallbackEventOps * eventOps, void * eventOpaque,
LG_TransportFallback ** result)
{
if (!result)
return false;
*result = NULL;
if (!transportName || !*transportName || !surfaceEvents ||
!lgTransport_isValid(transportName))
return false;
LG_TransportFallback * fallback = calloc(1, sizeof(*fallback));
if (!fallback)
return false;
LG_RWLOCK_INIT(fallback->lock);
atomic_init(&fallback->stop, false);
atomic_init(&fallback->ready, false);
fallback->transportName = strdup(transportName);
if (!fallback->transportName)
goto fail;
fallback->surfaceEvents = *surfaceEvents;
fallback->surfaceOpaque = surfaceOpaque;
if (eventOps)
fallback->eventOps = *eventOps;
fallback->eventOpaque = eventOpaque;
fallback->wakeEvent = lgCreateEvent(true, 0);
if (!fallback->wakeEvent)
goto fail;
fallback->videoIdleEvent = lgCreateEvent(true, 0);
if (!fallback->videoIdleEvent)
goto fail;
*result = fallback;
if (!lgCreateThread("transportFallback", fallbackThread,
fallback, &fallback->thread))
goto fail;
return true;
fail:
*result = NULL;
if (fallback->videoIdleEvent)
lgFreeEvent(fallback->videoIdleEvent);
if (fallback->wakeEvent)
lgFreeEvent(fallback->wakeEvent);
LG_RWLOCK_FREE(fallback->lock);
free(fallback->transportName);
free(fallback);
return false;
}
void lgTransportFallback_stop(LG_TransportFallback ** fallbackPtr)
{
if (!fallbackPtr || !*fallbackPtr)
return;
LG_TransportFallback * fallback = *fallbackPtr;
*fallbackPtr = NULL;
atomic_store_explicit(&fallback->stop, true, memory_order_release);
lgSignalEvent(fallback->wakeEvent);
LGThread * thread = fallback->thread;
fallback->thread = NULL;
if (!lgJoinThread(thread, NULL))
{
DEBUG_ERROR("Failed to stop fallback transport worker; preserving state");
return;
}
lgFreeEvent(fallback->videoIdleEvent);
lgFreeEvent(fallback->wakeEvent);
LG_RWLOCK_FREE(fallback->lock);
free(fallback->transportName);
free(fallback);
}
bool lgTransportFallback_ready(const LG_TransportFallback * fallback)
{
return fallback && atomic_load_explicit(
&fallback->ready, memory_order_acquire);
}
bool lgTransportFallback_setVideoActive(
LG_TransportFallback * fallback, bool active)
{
if (!fallback)
return false;
uint64_t connectionSerial;
LG_Transport * transport;
const LG_SwSurfaceOps * surfaceOps;
for (;;)
{
LG_LOCK_EXCLUSIVE(fallback->lock);
fallback->videoRequested = active;
if (!atomic_load_explicit(&fallback->ready, memory_order_acquire) ||
fallback->closing)
{
LG_UNLOCK_EXCLUSIVE(fallback->lock);
return false;
}
if (fallback->videoActive == active)
{
LG_UNLOCK_EXCLUSIVE(fallback->lock);
return true;
}
if (!fallback->videoBusy)
{
fallback->videoBusy = true;
connectionSerial = fallback->connectionSerial;
transport = fallback->transport.handle;
surfaceOps = fallback->videoOps->swSurface;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
break;
}
LG_UNLOCK_EXCLUSIVE(fallback->lock);
lgWaitEvent(fallback->videoIdleEvent, TIMEOUT_INFINITE);
}
lgResetEvent(fallback->videoIdleEvent);
const bool result = surfaceOps->setActive(transport, active);
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool current = fallback->connectionSerial == connectionSerial;
if (current && result)
fallback->videoActive = active;
const bool accepted = current && !fallback->closing && result;
fallback->videoBusy = false;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
lgSignalEvent(fallback->videoIdleEvent);
return accepted;
}
void lgTransportFallback_setPrimaryUUID(
LG_TransportFallback * fallback, const uint8_t uuid[16])
{
if (!fallback || !uuid)
return;
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool changed = !fallback->primaryUUIDValid ||
memcmp(fallback->primaryUUID, uuid,
sizeof(fallback->primaryUUID)) != 0;
if (changed)
{
memcpy(fallback->primaryUUID, uuid, sizeof(fallback->primaryUUID));
fallback->primaryUUIDValid = true;
}
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (changed)
lgSignalEvent(fallback->wakeEvent);
}
void lgTransportFallback_clearPrimaryUUID(
LG_TransportFallback * fallback)
{
if (!fallback)
return;
LG_LOCK_EXCLUSIVE(fallback->lock);
const bool changed = fallback->primaryUUIDValid;
fallback->primaryUUIDValid = false;
LG_UNLOCK_EXCLUSIVE(fallback->lock);
if (changed)
lgSignalEvent(fallback->wakeEvent);
}

View File

@@ -0,0 +1,57 @@
/**
* 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_FALLBACK_
#define _H_LG_CLIENT_TRANSPORT_FALLBACK_
#include "interface/transport.h"
#include <stdbool.h>
#include <stdint.h>
typedef struct LG_TransportFallback LG_TransportFallback;
typedef struct LG_TransportFallbackEventOps
{
/* The session is valid only for the duration of this callback. */
void (*connected)(void * opaque, const LG_TransportSession * session);
void (*disconnected)(void * opaque);
void (*uuidMismatch)(void * opaque, const uint8_t primary[16],
const uint8_t fallback[16]);
}
LG_TransportFallbackEventOps;
bool lgTransportFallback_start(const char * transportName,
const LG_SwSurfaceEventOps * surfaceEvents, void * surfaceOpaque,
const LG_TransportFallbackEventOps * eventOps, void * eventOpaque,
LG_TransportFallback ** result);
void lgTransportFallback_stop(LG_TransportFallback ** fallback);
bool lgTransportFallback_ready(const LG_TransportFallback * fallback);
/* The requested state is retained across reconnects. */
bool lgTransportFallback_setVideoActive(
LG_TransportFallback * fallback, bool active);
void lgTransportFallback_setPrimaryUUID(
LG_TransportFallback * fallback, const uint8_t uuid[16]);
void lgTransportFallback_clearPrimaryUUID(
LG_TransportFallback * fallback);
#endif

View File

@@ -157,8 +157,9 @@ static void spiceDestroy(LG_Transport ** transport)
*transport = NULL; *transport = NULL;
} }
static LG_TransportStatus spiceConnect(LG_Transport * transport, static LG_TransportStatus spiceConnectCancellable(LG_Transport * transport,
LG_TransportSession * session) LG_TransportSession * session, LG_TransportCancelledFn cancelled,
void * opaque)
{ {
if (transport->thread) if (transport->thread)
return LG_TRANSPORT_ERROR; return LG_TRANSPORT_ERROR;
@@ -174,7 +175,24 @@ static LG_TransportStatus spiceConnect(LG_Transport * transport,
"spiceProcess", spiceSession_thread, transport, &transport->thread)) "spiceProcess", spiceSession_thread, transport, &transport->thread))
return LG_TRANSPORT_ERROR; return LG_TRANSPORT_ERROR;
bool cancel = false;
while (!lgWaitEvent(transport->connectEvent, 100))
if (cancelled && cancelled(opaque))
{
cancel = true;
atomic_store_explicit(&transport->stop, true, memory_order_release);
lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE); lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE);
break;
}
if (cancel || (cancelled && cancelled(opaque)))
{
atomic_store_explicit(&transport->stop, true, memory_order_release);
lgJoinThread(transport->thread, NULL);
transport->thread = NULL;
return LG_TRANSPORT_DISCONNECTED;
}
if (transport->connectStatus != LG_TRANSPORT_OK) if (transport->connectStatus != LG_TRANSPORT_OK)
{ {
lgJoinThread(transport->thread, NULL); lgJoinThread(transport->thread, NULL);
@@ -186,6 +204,12 @@ static LG_TransportStatus spiceConnect(LG_Transport * transport,
return LG_TRANSPORT_OK; return LG_TRANSPORT_OK;
} }
static LG_TransportStatus spiceConnect(LG_Transport * transport,
LG_TransportSession * session)
{
return spiceConnectCancellable(transport, session, NULL, NULL);
}
static void spiceDisconnect(LG_Transport * transport) static void spiceDisconnect(LG_Transport * transport)
{ {
if (!transport->thread) if (!transport->thread)
@@ -266,6 +290,7 @@ const LG_TransportOps LGT_SPICE =
.create = spiceCreate, .create = spiceCreate,
.destroy = spiceDestroy, .destroy = spiceDestroy,
.connect = spiceConnect, .connect = spiceConnect,
.connectCancellable = spiceConnectCancellable,
.disconnect = spiceDisconnect, .disconnect = spiceDisconnect,
.sessionValid = spiceSessionValid, .sessionValid = spiceSessionValid,
.getVideoOps = spiceGetVideoOps, .getVideoOps = spiceGetVideoOps,