diff --git a/client/include/app.h b/client/include/app.h index d0b150f1..9fbfc18d 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -182,6 +182,10 @@ void app_alert(LG_MsgAlert type, const char * fmt, ...); typedef struct MsgBoxHandle * MsgBoxHandle; MsgBoxHandle app_msgBox(const char * caption, const char * fmt, ...); +typedef void (*MsgBoxCloseCallback)(MsgBoxHandle handle, void * opaque); +MsgBoxHandle app_msgBoxWithClose(const char * caption, + MsgBoxCloseCallback callback, void * opaque, const char * fmt, ...); + typedef void (*MsgBoxConfirmCallback)(bool yes, void * opaque); MsgBoxHandle app_confirmMsgBox(const char * caption, MsgBoxConfirmCallback callback, void * opaque, const char * fmt, ...); diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index 8b41cb0d..b54b76da 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -70,6 +70,79 @@ enum typedef uint32_t LG_TransportFeatureFlags; +typedef struct LG_VersionMismatch +{ + bool valid; + char component[16]; + uint32_t expectedVersion; + uint32_t currentVersion; +} +LG_VersionMismatch; + +#define LG_RECOVERY_MAX_VERSIONS 4 + +enum +{ + LG_RECOVERY_CAP_DISPLAY = 0x1, +}; + +typedef uint32_t LG_RecoveryCaps; + +typedef enum LG_RecoveryRequest +{ + LG_RECOVERY_REQ_NONE, + LG_RECOVERY_REQ_NORMAL, + LG_RECOVERY_REQ_RECOVERY, +} +LG_RecoveryRequest; + +typedef enum LG_RecoveryState +{ + LG_RECOVERY_STATE_UNKNOWN, + LG_RECOVERY_STATE_NORMAL, + LG_RECOVERY_STATE_SWITCHING, + LG_RECOVERY_STATE_ACTIVE, + LG_RECOVERY_STATE_FAILED, +} +LG_RecoveryState; + +typedef enum LG_RecoveryError +{ + LG_RECOVERY_ERR_NONE, + LG_RECOVERY_ERR_UNSUPPORTED, + LG_RECOVERY_ERR_HELPER_UNAVAILABLE, + LG_RECOVERY_ERR_TOPOLOGY_FAILED, + LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY, +} +LG_RecoveryError; + +typedef struct LG_RecoveryVersion +{ + char component[16]; + uint32_t version; +} +LG_RecoveryVersion; + +typedef struct LG_RecoveryInfo +{ + uint32_t abiVersion; + LG_RecoveryCaps capabilities; + uint64_t instance; + uint32_t heartbeat; + bool uuidValid; + uint8_t uuid[16]; + char producerVersion[64]; + uint32_t versionCount; + LG_RecoveryVersion versions[LG_RECOVERY_MAX_VERSIONS]; + uint32_t requestSerial; + LG_RecoveryRequest request; + uint32_t ackSerial; + LG_RecoveryRequest ackRequest; + LG_RecoveryState state; + LG_RecoveryError error; +} +LG_RecoveryInfo; + typedef struct LG_TransportSession { char version[32]; @@ -87,7 +160,7 @@ typedef struct LG_TransportSession uint8_t cores; uint8_t sockets; - uint32_t remoteVersion; + LG_VersionMismatch versionMismatch; } LG_TransportSession; @@ -346,6 +419,13 @@ typedef struct LG_TransportOps const LG_ClipboardOps *(*getClipboardOps)(LG_Transport * transport, void ** opaque); + /* Recovery operations are independent of a transport session and may be + * used after create whenever the backing transport advertises them. */ + LG_TransportStatus (*getRecoveryInfo)(LG_Transport * transport, + LG_RecoveryInfo * info); + LG_TransportStatus (*requestRecovery)(LG_Transport * transport, + LG_RecoveryRequest request, uint32_t * serial); + LG_TransportStatus (*sendControl)(LG_Transport * transport, const LG_TransportControl * control, LG_TransportControlToken * token); LG_TransportStatus (*controlStatus)(LG_Transport * transport, diff --git a/client/src/app.c b/client/src/app.c index 75cb52f5..8531b373 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -712,7 +712,20 @@ MsgBoxHandle app_msgBox(const char * caption, const char * fmt, ...) { va_list args; va_start(args, fmt); - MsgBoxHandle handle = overlayMsg_show(caption, NULL, NULL, fmt, args); + MsgBoxHandle handle = overlayMsg_show( + caption, NULL, NULL, NULL, NULL, fmt, args); + va_end(args); + + return handle; +} + +MsgBoxHandle app_msgBoxWithClose(const char * caption, + MsgBoxCloseCallback callback, void * opaque, const char * fmt, ...) +{ + va_list args; + va_start(args, fmt); + MsgBoxHandle handle = overlayMsg_show( + caption, NULL, NULL, callback, opaque, fmt, args); va_end(args); return handle; @@ -723,7 +736,8 @@ MsgBoxHandle app_confirmMsgBox(const char * caption, { va_list args; va_start(args, fmt); - MsgBoxHandle handle = overlayMsg_show(caption, callback, opaque, fmt, args); + MsgBoxHandle handle = overlayMsg_show( + caption, callback, opaque, NULL, NULL, fmt, args); va_end(args); return handle; diff --git a/client/src/main.c b/client/src/main.c index 8014d531..6462d2f1 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -86,6 +86,7 @@ static int renderThread(void * unused); static RenderQueueSource renderQueueSource(LG_VideoSource source); static bool videoSourceInvalidate(LG_VideoSource source); static void videoSourceShowSplashIfNeeded(void); +static bool fallbackRequestVideo(void); static LGEvent *e_startup = NULL; static LGEvent *e_cursorRepaint = NULL; @@ -1608,7 +1609,7 @@ int main_frameThread(void * unused) &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, false, memory_order_release); - if (!app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK)) + if (!fallbackRequestVideo()) videoSourceShowSplashIfNeeded(); } @@ -1645,13 +1646,12 @@ static RenderQueueSource renderQueueSource(LG_VideoSource source) return RENDER_QUEUE_SOURCE_NONE; } -static void videoSourceBegin(LG_VideoSource source) +static void videoSourceBeginLocked(LG_VideoSource source) { struct VideoSourceState * state = &g_state.videoSource[source]; const uint64_t generation = renderQueue_sourceBegin(renderQueueSource(source)); - LG_LOCK(g_state.videoSourceLock); atomic_store_explicit(&state->ready, false, memory_order_release); atomic_store_explicit( &state->transitionSerial, 0, memory_order_relaxed); @@ -1664,6 +1664,12 @@ static void videoSourceBegin(LG_VideoSource source) atomic_store_explicit( &state->generation, generation, memory_order_release); state->configurePending = false; +} + +static void videoSourceBegin(LG_VideoSource source) +{ + LG_LOCK(g_state.videoSourceLock); + videoSourceBeginLocked(source); LG_UNLOCK(g_state.videoSourceLock); } @@ -1883,15 +1889,27 @@ static void videoSourceApplied(void * opaque, RenderQueueSource queueSource, app_refreshVideoSource(); } +static bool swSurfaceEventAdmitted(LG_VideoSource source) +{ + return source != LG_VIDEO_SOURCE_FALLBACK || + lgTransportFallback_admitted(g_state.fallback); +} + static void swSurfaceConfigure(LG_VideoSource source, unsigned int width, unsigned int height) { struct VideoSourceState * state = &g_state.videoSource[source]; - videoSourceBegin(source); + LG_LOCK(g_state.videoSourceLock); + if (!swSurfaceEventAdmitted(source)) + { + LG_UNLOCK(g_state.videoSourceLock); + return; + } + + videoSourceBeginLocked(source); atomic_store_explicit(&state->width, width, memory_order_relaxed); atomic_store_explicit(&state->height, height, memory_order_relaxed); atomic_store_explicit(&state->rotate, LG_ROTATE_0, memory_order_release); - LG_LOCK(g_state.videoSourceLock); state->configurePending = true; atomic_store_explicit(&state->ready, true, memory_order_release); LG_UNLOCK(g_state.videoSourceLock); @@ -1924,30 +1942,41 @@ static void swSurfaceDestroy(LG_VideoSource source) static void swSurfaceDrawFill(LG_VideoSource source, int x, int y, int width, int height, uint32_t color) { + LG_LOCK(g_state.videoSourceLock); + if (!swSurfaceEventAdmitted(source)) + { + LG_UNLOCK(g_state.videoSourceLock); + return; + } + const uint64_t generation = atomic_load_explicit( &g_state.videoSource[source].generation, memory_order_acquire); renderQueue_sourceSwSurfaceDrawFill(renderQueueSource(source), generation, x, y, width, height, color); + LG_UNLOCK(g_state.videoSourceLock); } static void swSurfaceDrawBitmap(LG_VideoSource source, bool topDown, int x, int y, int width, int height, int stride, const void * data) { + LG_LOCK(g_state.videoSourceLock); + if (!swSurfaceEventAdmitted(source)) + { + LG_UNLOCK(g_state.videoSourceLock); + return; + } + const uint64_t generation = atomic_load_explicit( &g_state.videoSource[source].generation, memory_order_acquire); renderQueue_sourceSwSurfaceDrawBitmap(renderQueueSource(source), generation, x, y, width, height, stride, data, topDown); + LG_UNLOCK(g_state.videoSourceLock); } static void swSurfacePointer(LG_VideoSource source, const LG_TransportPointer * pointer) { struct VideoSourceState * state = &g_state.videoSource[source]; - const uint64_t generation = atomic_load_explicit( - &state->generation, memory_order_acquire); - if (!generation) - return; - LG_RendererCursor type = LG_CURSOR_COLOR; if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) switch (pointer->type) @@ -1971,8 +2000,15 @@ static void swSurfacePointer(LG_VideoSource source, const bool drawCursor = source != LG_VIDEO_SOURCE_PRIMARY || g_cursor.draw || !lgInput_available(); LG_LOCK(g_state.videoSourceLock); - if (atomic_load_explicit(&state->generation, memory_order_acquire) != - generation) + if (!swSurfaceEventAdmitted(source)) + { + LG_UNLOCK(g_state.videoSourceLock); + return; + } + + const uint64_t generation = atomic_load_explicit( + &state->generation, memory_order_acquire); + if (!generation) { LG_UNLOCK(g_state.videoSourceLock); return; @@ -2036,7 +2072,6 @@ static void swSurfacePointer(LG_VideoSource source, g_cursor.guest.hy = hy; g_cursor.guest.valid = valid; } - LG_UNLOCK(g_state.videoSourceLock); if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) renderQueue_sourceCursorImage(renderQueueSource(source), generation, @@ -2055,6 +2090,7 @@ static void swSurfacePointer(LG_VideoSource source, LG_TRANSPORT_POINTER_VISIBLE_VALID | LG_TRANSPORT_POINTER_SHAPE))) renderQueue_sourceCursorState(renderQueueSource(source), generation, visible, x, y, hx, hy); + LG_UNLOCK(g_state.videoSourceLock); if (sourceApplied) { @@ -2094,8 +2130,7 @@ static void swSurfaceEventDestroy(void * opaque) static void swSurfaceEventDrawFill(void * opaque, int x, int y, int width, int height, uint32_t color) { - swSurfaceDrawFill( - swSurfaceSource(opaque), x, y, width, height, color); + swSurfaceDrawFill(swSurfaceSource(opaque), x, y, width, height, color); } static void swSurfaceEventDrawBitmap(void * opaque, bool topDown, @@ -2120,6 +2155,18 @@ static const LG_SwSurfaceEventOps swSurfaceEvents = .pointer = swSurfaceEventPointer, }; +static bool fallbackRequestVideo(void) +{ + if (!g_state.fallback) + return false; + + lgTransportFallback_requestVideoActive(g_state.fallback, true); + if (!lgTransportFallback_admitted(g_state.fallback)) + return false; + + return app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK); +} + static void fallbackConnected(void * opaque, const LG_TransportSession * session) { @@ -2131,7 +2178,10 @@ static void fallbackConnected(void * opaque, &g_state.lgHostConnected, memory_order_acquire)) core_setTitle(session->name); - app_refreshVideoSource(); + if (lgTransportFallback_videoRequested(g_state.fallback)) + fallbackRequestVideo(); + else + app_refreshVideoSource(); } static void fallbackDisconnected(void * opaque) @@ -2162,7 +2212,7 @@ static const LG_TransportFallbackEventOps fallbackEvents = .uuidMismatch = fallbackUUIDMismatch, }; -static bool fallbackStart(void) +static bool fallbackStart(const uint8_t primaryUUID[16]) { if (!option_get_bool("spice", "enable") || strcmp(g_params.transport, "spice") == 0) @@ -2170,7 +2220,7 @@ static bool fallbackStart(void) if (lgTransportFallback_start("spice", &swSurfaceEvents, (void *)(uintptr_t)LG_VIDEO_SOURCE_FALLBACK, - &fallbackEvents, NULL, &g_state.fallback)) + &fallbackEvents, NULL, primaryUUID, &g_state.fallback)) return true; DEBUG_ERROR("Failed to start the SPICE fallback transport"); @@ -2251,12 +2301,522 @@ static bool tryRenderer(const int index, const LG_RendererParams lgrParams, return true; } -static void reportBadVersion(void) +static void reportBadVersion(const LG_VersionMismatch * mismatch) { DEBUG_BREAK(); - DEBUG_ERROR("The host application is not compatible with this client"); - DEBUG_ERROR("This is not a Looking Glass error, do not report this"); - DEBUG_ERROR("Please install the matching host application for this client"); + if (mismatch->valid) + { + DEBUG_ERROR("Incompatible %s version", mismatch->component); + DEBUG_ERROR("Expected version: %u", mismatch->expectedVersion); + DEBUG_ERROR("Current version : %u", mismatch->currentVersion); + } + else + DEBUG_ERROR("The transport is not compatible with this client"); + DEBUG_ERROR("Please install matching Looking Glass components"); +} + +static const int RECOVERY_PENDING = -1; +static const int RECOVERY_NO = 0; +static const int RECOVERY_YES = 1; + +struct RecoveryPrompt +{ + atomic_int choice; + atomic_uintptr_t handle; + atomic_uintptr_t message; + atomic_bool messageClosed; + uint64_t instance; + uint32_t serial; + uint32_t failedSerial; + LG_RecoveryState reportedState; + bool shown; + bool requested; + bool owned; + bool retryPrompt; + bool retryDeclined; +}; + +static bool recoveryGetInfo(LG_RecoveryInfo * info) +{ + if (!g_state.transport.ops->getRecoveryInfo) + return false; + + return g_state.transport.ops->getRecoveryInfo( + g_state.transport.handle, info) == LG_TRANSPORT_OK; +} + +static void recoveryConfirm(bool yes, void * opaque) +{ + struct RecoveryPrompt * prompt = opaque; + atomic_store_explicit(&prompt->choice, + yes ? RECOVERY_YES : RECOVERY_NO, memory_order_release); +} + +static void recoveryClosePrompt(struct RecoveryPrompt * prompt) +{ + if (atomic_load_explicit( + &prompt->choice, memory_order_acquire) != RECOVERY_PENDING) + { + atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed); + return; + } + + MsgBoxHandle handle = (MsgBoxHandle)atomic_exchange_explicit( + &prompt->handle, 0, memory_order_acq_rel); + app_msgBoxClose(handle); +} + +static void recoveryCloseMessage(struct RecoveryPrompt * prompt) +{ + MsgBoxHandle handle = (MsgBoxHandle)atomic_exchange_explicit( + &prompt->message, 0, memory_order_acq_rel); + app_msgBoxClose(handle); +} + +static void recoveryMessageClosed(MsgBoxHandle handle, void * opaque) +{ + struct RecoveryPrompt * prompt = opaque; + atomic_store_explicit( + &prompt->messageClosed, true, memory_order_release); + + uintptr_t expected = (uintptr_t)handle; + atomic_compare_exchange_strong_explicit(&prompt->message, + &expected, 0, memory_order_acq_rel, memory_order_acquire); +} + +static void recoveryBeginMessage(struct RecoveryPrompt * prompt) +{ + recoveryCloseMessage(prompt); + atomic_store_explicit( + &prompt->messageClosed, false, memory_order_relaxed); +} + +static void recoveryStoreMessage(struct RecoveryPrompt * prompt, + MsgBoxHandle handle) +{ + atomic_store_explicit( + &prompt->message, (uintptr_t)handle, memory_order_release); + if (!atomic_load_explicit( + &prompt->messageClosed, memory_order_acquire)) + return; + + uintptr_t expected = (uintptr_t)handle; + atomic_compare_exchange_strong_explicit(&prompt->message, + &expected, 0, memory_order_acq_rel, memory_order_acquire); +} + +static void recoveryClose(struct RecoveryPrompt * prompt) +{ + recoveryClosePrompt(prompt); + recoveryCloseMessage(prompt); +} + +static int recoveryExit(struct RecoveryPrompt * prompt, int result) +{ + recoveryClose(prompt); + return result; +} + +static void retainMessage(MsgBoxHandle * messages, size_t capacity, + int * count, MsgBoxHandle message) +{ + if (!message) + return; + + if ((size_t)*count >= capacity) + { + app_msgBoxClose(message); + return; + } + + messages[(*count)++] = message; +} + +static void recoveryShowPrompt(struct RecoveryPrompt * prompt, + const LG_VersionMismatch * mismatch, bool hasFallback) +{ + const char * fallback = hasFallback ? + "The client will then switch to its SPICE fallback." : + "SPICE is disabled in this client, so use another SPICE viewer."; + + recoveryCloseMessage(prompt); + prompt->shown = true; + prompt->retryPrompt = false; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + MsgBoxHandle handle; + if (mismatch->valid) + { + const char * component = mismatch->component[0] ? + mismatch->component : "transport"; + handle = app_confirmMsgBox( + "Incompatible Transport Version", recoveryConfirm, prompt, + "Expected %s version: %u\n" + "Current %s version: %u\n" + "\n" + "Enable recovery mode to restore the guest display topology?\n" + "%s", + component, mismatch->expectedVersion, + component, mismatch->currentVersion, fallback); + } + else + { + handle = app_confirmMsgBox( + "Incompatible Transport Version", recoveryConfirm, prompt, + "The transport is not compatible with this client.\n" + "\n" + "Enable recovery mode to restore the guest display topology?\n" + "%s", fallback); + } + atomic_store_explicit( + &prompt->handle, (uintptr_t)handle, memory_order_release); + if (atomic_load_explicit( + &prompt->choice, memory_order_acquire) != RECOVERY_PENDING) + atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed); +} + +static const char * recoveryErrorText(LG_RecoveryError error) +{ + switch (error) + { + case LG_RECOVERY_ERR_HELPER_UNAVAILABLE: + return "The IDD helper is unavailable"; + + case LG_RECOVERY_ERR_TOPOLOGY_FAILED: + return "Windows could not restore the saved display topology"; + + case LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY: + return "No fallback display became active"; + + case LG_RECOVERY_ERR_UNSUPPORTED: + return "Recovery is not supported by the guest driver"; + + case LG_RECOVERY_ERR_NONE: + break; + } + + return "The guest could not enter recovery mode"; +} + +static int recoveryTakeChoice(struct RecoveryPrompt * prompt) +{ + const int choice = atomic_exchange_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_acq_rel); + if (choice != RECOVERY_PENDING) + atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed); + return choice; +} + +static void recoveryShowRetry(struct RecoveryPrompt * prompt, + const char * reason, uint32_t failedSerial) +{ + recoveryClose(prompt); + if (failedSerial) + prompt->failedSerial = failedSerial; + prompt->serial = 0; + prompt->reportedState = LG_RECOVERY_STATE_FAILED; + prompt->shown = true; + prompt->requested = false; + prompt->owned = false; + prompt->retryPrompt = true; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + MsgBoxHandle handle = app_confirmMsgBox( + "Recovery Failed", recoveryConfirm, prompt, + "%s.\n\nRetry guest display recovery?", reason); + atomic_store_explicit( + &prompt->handle, (uintptr_t)handle, memory_order_release); + if (atomic_load_explicit( + &prompt->choice, memory_order_acquire) != RECOVERY_PENDING) + atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed); +} + +static void recoveryShowPending(struct RecoveryPrompt * prompt, + const LG_VersionMismatch * mismatch) +{ + prompt->shown = true; + prompt->reportedState = LG_RECOVERY_STATE_SWITCHING; + prompt->retryPrompt = false; + if (mismatch->valid) + { + const char * component = mismatch->component[0] ? + mismatch->component : "transport"; + recoveryBeginMessage(prompt); + recoveryStoreMessage(prompt, app_msgBoxWithClose( + "Incompatible Transport Version", + recoveryMessageClosed, prompt, + "Expected %s version: %u\n" + "Current %s version: %u\n" + "\n" + "Recovery mode is pending. Waiting for the guest driver.", + component, mismatch->expectedVersion, + component, mismatch->currentVersion)); + } + else + { + recoveryBeginMessage(prompt); + recoveryStoreMessage(prompt, app_msgBoxWithClose( + "Incompatible Transport Version", + recoveryMessageClosed, prompt, + "The transport is not compatible with this client.\n" + "\n" + "Recovery mode is pending. Waiting for the guest driver.")); + } +} + +static bool recoverySerialNewer(uint32_t serial, uint32_t reference) +{ + const uint32_t difference = serial - reference; + return difference && difference < 0x80000000U; +} + +static void recoveryHandleMismatch(struct RecoveryPrompt * prompt, + const LG_VersionMismatch * mismatch) +{ + LG_RecoveryInfo info = { 0 }; + const bool available = recoveryGetInfo(&info) && + (info.capabilities & LG_RECOVERY_CAP_DISPLAY); + if (!available) + { + if (prompt->requested && !prompt->retryPrompt && + !prompt->retryDeclined) + { + recoveryShowRetry(prompt, + "The guest recovery channel is unavailable", 0); + return; + } + + if (prompt->shown) + return; + + prompt->shown = true; + if (mismatch->valid) + { + const char * component = mismatch->component[0] ? + mismatch->component : "transport"; + recoveryBeginMessage(prompt); + recoveryStoreMessage(prompt, app_msgBoxWithClose( + "Incompatible Transport Version", + recoveryMessageClosed, prompt, + "Expected %s version: %u\n" + "Current %s version: %u\n" + "\n" + "This guest driver does not support remote recovery.\n" + "Use a guest console to install matching components.", + component, mismatch->expectedVersion, + component, mismatch->currentVersion)); + } + else + { + recoveryBeginMessage(prompt); + recoveryStoreMessage(prompt, app_msgBoxWithClose( + "Incompatible Transport Version", + recoveryMessageClosed, prompt, + "The transport is not compatible with this client.\n" + "\n" + "This guest driver does not support remote recovery.\n" + "Use a guest console to install matching components.")); + } + return; + } + + if (prompt->instance != info.instance) + { + recoveryClose(prompt); + prompt->instance = info.instance; + prompt->serial = 0; + prompt->failedSerial = 0; + prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN; + prompt->shown = false; + prompt->requested = false; + prompt->owned = false; + prompt->retryPrompt = false; + prompt->retryDeclined = false; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + } + + if (info.uuidValid) + lgTransportFallback_setPrimaryUUID(g_state.fallback, info.uuid); + else + lgTransportFallback_clearPrimaryUUID(g_state.fallback); + + const bool requestNewer = info.requestSerial != 0 && + (info.ackSerial == 0 || + recoverySerialNewer(info.requestSerial, info.ackSerial)); + const LG_RecoveryRequest globalRequest = requestNewer ? + info.request : info.ackRequest; + const uint32_t globalSerial = requestNewer ? + info.requestSerial : info.ackSerial; + if (prompt->requested && globalSerial && + globalRequest == LG_RECOVERY_REQ_NORMAL && + (globalSerial == prompt->serial || + recoverySerialNewer(globalSerial, prompt->serial))) + { + recoveryClose(prompt); + prompt->serial = 0; + prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN; + prompt->shown = prompt->retryDeclined; + prompt->requested = false; + prompt->owned = false; + prompt->retryPrompt = false; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + } + + const bool pendingRecovery = requestNewer && + info.request == LG_RECOVERY_REQ_RECOVERY; + const bool failedRecovery = !requestNewer && info.ackSerial != 0 && + info.ackRequest == LG_RECOVERY_REQ_RECOVERY && + info.state == LG_RECOVERY_STATE_FAILED; + const bool statusRecovery = !requestNewer && info.ackSerial != 0 && + info.ackRequest == LG_RECOVERY_REQ_RECOVERY && + (info.state == LG_RECOVERY_STATE_SWITCHING || + info.state == LG_RECOVERY_STATE_ACTIVE); + if (failedRecovery && prompt->requested && + prompt->failedSerial != info.ackSerial && + (info.ackSerial == prompt->serial || + recoverySerialNewer(info.ackSerial, prompt->serial))) + { + if (prompt->owned && info.ackSerial == prompt->serial) + { + recoveryShowRetry(prompt, recoveryErrorText(info.error), + info.ackSerial); + return; + } + + recoveryClose(prompt); + prompt->serial = 0; + prompt->failedSerial = info.ackSerial; + prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN; + prompt->shown = prompt->retryDeclined; + prompt->requested = false; + prompt->owned = false; + prompt->retryPrompt = false; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + } + + if (pendingRecovery) + { + if (!prompt->requested || prompt->serial != info.requestSerial) + { + recoveryClose(prompt); + prompt->requested = true; + prompt->serial = info.requestSerial; + prompt->failedSerial = 0; + prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN; + prompt->shown = false; + prompt->owned = false; + prompt->retryPrompt = false; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + } + } + else if (statusRecovery && + (!prompt->requested || prompt->serial != info.ackSerial)) + { + recoveryClose(prompt); + prompt->requested = true; + prompt->serial = info.ackSerial; + prompt->failedSerial = 0; + prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN; + prompt->shown = false; + prompt->owned = false; + prompt->retryPrompt = false; + atomic_store_explicit( + &prompt->choice, RECOVERY_PENDING, memory_order_relaxed); + } + + const bool retryChoice = prompt->retryPrompt; + const int choice = recoveryTakeChoice(prompt); + if (choice == RECOVERY_NO) + { + prompt->retryPrompt = false; + if (retryChoice) + prompt->retryDeclined = true; + return; + } + + if (choice == RECOVERY_YES) + { + prompt->retryPrompt = false; + prompt->retryDeclined = false; + const LG_TransportStatus status = + g_state.transport.ops->requestRecovery ? + g_state.transport.ops->requestRecovery(g_state.transport.handle, + LG_RECOVERY_REQ_RECOVERY, &prompt->serial) : + LG_TRANSPORT_UNAVAILABLE; + prompt->requested = status == LG_TRANSPORT_OK; + prompt->owned = prompt->requested; + prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN; + if (!prompt->requested) + { + recoveryShowRetry(prompt, + "The recovery request could not be sent to the guest driver", 0); + return; + } + + prompt->failedSerial = 0; + fallbackRequestVideo(); + recoveryShowPending(prompt, mismatch); + app_alert(LG_ALERT_INFO, "Guest display recovery requested"); + return; + } + + const bool matchingAck = prompt->requested && statusRecovery && + info.ackSerial == prompt->serial; + if (matchingAck && info.state == LG_RECOVERY_STATE_ACTIVE) + { + fallbackRequestVideo(); + if (prompt->reportedState != LG_RECOVERY_STATE_ACTIVE) + { + prompt->shown = true; + prompt->reportedState = LG_RECOVERY_STATE_ACTIVE; + if (mismatch->valid) + { + const char * component = mismatch->component[0] ? + mismatch->component : "transport"; + recoveryBeginMessage(prompt); + recoveryStoreMessage(prompt, app_msgBoxWithClose( + "Incompatible Transport Version", + recoveryMessageClosed, prompt, + "Expected %s version: %u\n" + "Current %s version: %u\n" + "\n" + "Recovery mode is active. SPICE video is available.", + component, mismatch->expectedVersion, + component, mismatch->currentVersion)); + } + else + { + recoveryBeginMessage(prompt); + recoveryStoreMessage(prompt, app_msgBoxWithClose( + "Incompatible Transport Version", + recoveryMessageClosed, prompt, + "The transport is not compatible with this client.\n" + "\n" + "Recovery mode is active. SPICE video is available.")); + } + app_alert(LG_ALERT_SUCCESS, "Guest display recovery is active"); + } + return; + } + + const bool pending = prompt->requested && + ((pendingRecovery && + info.requestSerial == prompt->serial) || + (matchingAck && info.state == LG_RECOVERY_STATE_SWITCHING)); + if (pending) + { + fallbackRequestVideo(); + if (prompt->reportedState != LG_RECOVERY_STATE_SWITCHING) + recoveryShowPending(prompt, mismatch); + return; + } + + if (!prompt->shown) + recoveryShowPrompt(prompt, mismatch, g_state.fallback != NULL); } static MsgBoxHandle showSpiceInputHelp(void) @@ -2412,6 +2972,9 @@ static int lg_run(void) } DEBUG_INFO("Using Transport: %s", g_state.transport.ops->name); + LG_RecoveryInfo initialRecovery = { 0 }; + const bool initialRecoveryValid = recoveryGetInfo(&initialRecovery); + g_state.videoOps = g_state.transport.ops->getVideoOps(g_state.transport.handle); if (!g_state.videoOps || @@ -2460,7 +3023,9 @@ static int lg_run(void) g_state.micDefaultState = g_params.micDefaultState; - if (!fallbackStart()) + const uint8_t * fallbackUUID = initialRecoveryValid && + initialRecovery.uuidValid ? initialRecovery.uuid : NULL; + if (!fallbackStart(fallbackUUID)) return -1; // select and init a renderer @@ -2621,6 +3186,11 @@ static int lg_run(void) LG_TransportSession session; MsgBoxHandle msgs[10]; int msgsCount; + struct RecoveryPrompt recoveryPrompt = { 0 }; + atomic_init(&recoveryPrompt.choice, RECOVERY_PENDING); + atomic_init(&recoveryPrompt.handle, 0); + atomic_init(&recoveryPrompt.message, 0); + atomic_init(&recoveryPrompt.messageClosed, false); restart: frameTimingReset(); @@ -2636,8 +3206,8 @@ restart: if (initialFallbackEnable && microtime() > initialFallbackEnable) { - app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK); - initialFallbackEnable = 0; + if (fallbackRequestVideo()) + initialFallbackEnable = 0; } struct TransportSessionProbe probe = { @@ -2650,7 +3220,7 @@ restart: &probeThread)) { DEBUG_ERROR("Failed to create transport session probe thread"); - return -1; + return recoveryExit(&recoveryPrompt, -1); } while (app_getState() == APP_STATE_RUNNING && @@ -2663,11 +3233,11 @@ restart: if (!lgJoinThread(probeThread, NULL)) { DEBUG_ERROR("Failed to join transport session probe thread"); - return -1; + return recoveryExit(&recoveryPrompt, -1); } if (app_getState() != APP_STATE_RUNNING) - return -1; + return recoveryExit(&recoveryPrompt, -1); if (probe.status == LG_TRANSPORT_OK) { @@ -2679,14 +3249,10 @@ restart: if (probe.status == LG_TRANSPORT_INVALID_VERSION) { if (waitCount++ == 0) - { - reportBadVersion(); - msgs[msgsCount++] = app_msgBox( - "Incompatible Transport Version", - "The selected transport source is not compatible with this client.\n" - "Please install matching versions."); - DEBUG_INFO("Remote transport version: %u", probe.session.remoteVersion); - } + reportBadVersion(&probe.session.versionMismatch); + + recoveryHandleMismatch( + &recoveryPrompt, &probe.session.versionMismatch); g_state.ds->wait(1000); continue; @@ -2703,22 +3269,50 @@ restart: } if (waitCount == 30 && !g_params.disableWaitingMessage) { - msgs[msgsCount++] = app_msgBox( - "Transport Source Not Available", - "The selected transport source is not available.\n" - "Continuing to wait..."); - msgs[msgsCount++] = showSpiceInputHelp(); + const size_t msgCapacity = sizeof(msgs) / sizeof(*msgs); + retainMessage(msgs, msgCapacity, &msgsCount, + app_msgBox("Transport Source Not Available", + "The selected transport source is not available.\n" + "Continuing to wait...")); + retainMessage(msgs, msgCapacity, &msgsCount, + showSpiceInputHelp()); } g_state.ds->wait(1000); continue; } DEBUG_ERROR("Transport connection failed with status %d", probe.status); - return -1; + return recoveryExit(&recoveryPrompt, -1); } if (app_getState() != APP_STATE_RUNNING) - return -1; + return recoveryExit(&recoveryPrompt, -1); + + recoveryClose(&recoveryPrompt); + LG_RecoveryInfo recoveryInfo = { 0 }; + if (recoveryGetInfo(&recoveryInfo) && + (recoveryInfo.state == LG_RECOVERY_STATE_ACTIVE || + recoveryInfo.state == LG_RECOVERY_STATE_SWITCHING || + recoveryInfo.request == LG_RECOVERY_REQ_RECOVERY || + recoveryInfo.ackRequest == LG_RECOVERY_REQ_RECOVERY) && + g_state.transport.ops->requestRecovery) + { + const LG_TransportStatus status = + g_state.transport.ops->requestRecovery(g_state.transport.handle, + LG_RECOVERY_REQ_NORMAL, NULL); + if (status != LG_TRANSPORT_OK) + DEBUG_WARN("Failed to leave recovery mode: %d", status); + } + recoveryPrompt.serial = 0; + recoveryPrompt.failedSerial = 0; + recoveryPrompt.reportedState = LG_RECOVERY_STATE_UNKNOWN; + recoveryPrompt.shown = false; + recoveryPrompt.requested = false; + recoveryPrompt.owned = false; + recoveryPrompt.retryPrompt = false; + recoveryPrompt.retryDeclined = false; + atomic_store_explicit( + &recoveryPrompt.choice, RECOVERY_PENDING, memory_order_relaxed); waitCount = 100; for (int i = 0; i < msgsCount; ++i) @@ -2811,7 +3405,7 @@ restart: { videoSourceBegin(LG_VIDEO_SOURCE_PRIMARY); if (!core_startCursorThread() || !core_startFrameThread()) - return -1; + return recoveryExit(&recoveryPrompt, -1); } else { @@ -2819,7 +3413,7 @@ restart: g_state.transport.handle, true)) { DEBUG_ERROR("Failed to activate the primary software surface"); - return -1; + return recoveryExit(&recoveryPrompt, -1); } app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); } @@ -2852,7 +3446,6 @@ restart: atomic_store_explicit( &g_state.lgHostConnected, false, memory_order_release); g_state.guestUUIDValid = false; - lgTransportFallback_clearPrimaryUUID(g_state.fallback); lgSignalEvent(e_startup); lgSignalEvent(g_state.frameEvent); @@ -2877,7 +3470,7 @@ restart: goto restart; } - return 0; + return recoveryExit(&recoveryPrompt, 0); } static void lg_shutdown(void) diff --git a/client/src/overlay/msg.c b/client/src/overlay/msg.c index f5b14d1f..539b96f1 100644 --- a/client/src/overlay/msg.c +++ b/client/src/overlay/msg.c @@ -33,11 +33,13 @@ struct Msg { - char * caption; - char * message; - StringList lines; + char * caption; + char * message; + StringList lines; MsgBoxConfirmCallback confirm; - void * opaque; + void * confirmOpaque; + MsgBoxCloseCallback close; + void * closeOpaque; }; struct MsgState @@ -62,6 +64,9 @@ static bool msg_init(void ** udata, const void * params) static void freeMsg(struct Msg * msg) { + if (msg->close) + msg->close((MsgBoxHandle)msg, msg->closeOpaque); + free(msg->caption); free(msg->message); stringlist_free(&msg->lines); @@ -144,14 +149,14 @@ static int msg_render(void * udata, bool interactive, struct Rect * windowRects, if (igButton("Yes", textSize)) { destroy = true; - msg->confirm(true, msg->opaque); + msg->confirm(true, msg->confirmOpaque); } igSameLine(0.0f, -1.0f); if (igButton("No", textSize)) { destroy = true; - msg->confirm(false, msg->opaque); + msg->confirm(false, msg->confirmOpaque); } } else @@ -202,7 +207,9 @@ bool overlayMsg_modal(void) } MsgBoxHandle overlayMsg_show( - const char * caption, MsgBoxConfirmCallback confirm, void * opaque, + const char * caption, + MsgBoxConfirmCallback confirm, void * confirmOpaque, + MsgBoxCloseCallback close, void * closeOpaque, const char * fmt, va_list args) { struct Msg * msg = calloc(1, sizeof(*msg)); @@ -228,8 +235,6 @@ MsgBoxHandle overlayMsg_show( return NULL; } - msg->confirm = confirm; - msg->opaque = opaque; if (valloc_sprintf(&msg->message, fmt ? fmt : "", args) < 0) { DEBUG_ERROR("failed to format message"); @@ -256,6 +261,10 @@ MsgBoxHandle overlayMsg_show( } } + msg->confirm = confirm; + msg->confirmOpaque = confirmOpaque; + msg->close = close; + msg->closeOpaque = closeOpaque; ll_push(l_msg.messages, msg); app_invalidateOverlay(false); diff --git a/client/src/overlay/msg.h b/client/src/overlay/msg.h index 8bef7121..e83acd6b 100644 --- a/client/src/overlay/msg.h +++ b/client/src/overlay/msg.h @@ -29,7 +29,9 @@ bool overlayMsg_modal(void); MsgBoxHandle overlayMsg_show( - const char * caption, MsgBoxConfirmCallback confirm, void * opaque, + const char * caption, + MsgBoxConfirmCallback confirm, void * confirmOpaque, + MsgBoxCloseCallback close, void * closeOpaque, const char * fmt, va_list args); void overlayMsg_close(MsgBoxHandle handle); diff --git a/client/src/transport_fallback.c b/client/src/transport_fallback.c index 2152cea0..7e1b4b3d 100644 --- a/client/src/transport_fallback.c +++ b/client/src/transport_fallback.c @@ -50,9 +50,12 @@ struct LG_TransportFallback LGThread * thread; LGEvent * wakeEvent; LG_RWLock lock; + LG_Lock providerLock; + LG_Lock eventLock; atomic_bool stop; atomic_bool ready; + atomic_bool admitted; LG_TransportInstance transport; LG_TransportSession session; @@ -62,6 +65,7 @@ struct LG_TransportFallback bool connected; bool providersPublished; bool connectedReported; + bool disconnectedReported; bool closing; bool videoRequested; bool videoActive; @@ -89,6 +93,11 @@ static bool uuidMismatchLocked(const LG_TransportFallback * fallback) sizeof(fallback->primaryUUID)) != 0; } +static bool uuidAdmittedLocked(const LG_TransportFallback * fallback) +{ + return !uuidMismatchLocked(fallback); +} + static bool recordMismatchLocked(LG_TransportFallback * fallback, uint8_t primary[16], uint8_t remote[16]) { @@ -119,10 +128,50 @@ static bool sessionLive(const LG_TransportFallback * fallback) fallback->transport.ops->sessionValid(fallback->transport.handle); } +static void notifyConnected(LG_TransportFallback * fallback) +{ + LG_LOCK(fallback->eventLock); + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool report = + atomic_load_explicit(&fallback->ready, memory_order_acquire) && + atomic_load_explicit(&fallback->admitted, memory_order_acquire) && + uuidAdmittedLocked(fallback) && !fallback->closing && + !fallback->disconnectedReported; + fallback->connectedReported = report; + const LG_TransportSession session = fallback->session; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (report && fallback->eventOps.connected) + fallback->eventOps.connected(fallback->eventOpaque, &session); + LG_UNLOCK(fallback->eventLock); +} + +static void notifyDisconnected( + LG_TransportFallback * fallback, bool requested) +{ + if (!requested) + return; + + LG_LOCK(fallback->eventLock); + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool report = !fallback->disconnectedReported; + if (report) + fallback->disconnectedReported = true; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (report && fallback->eventOps.disconnected) + fallback->eventOps.disconnected(fallback->eventOpaque); + LG_UNLOCK(fallback->eventLock); +} + static void unpublishProviders(LG_TransportFallback * fallback, bool live) { + LG_LOCK(fallback->providerLock); if (!fallback->providersPublished) + { + LG_UNLOCK(fallback->providerLock); return; + } if (live) { @@ -137,12 +186,14 @@ static void unpublishProviders(LG_TransportFallback * fallback, bool live) lgClipboard_dropFallback(); } fallback->providersPublished = false; + LG_UNLOCK(fallback->providerLock); } static void closeVideoAdmission(LG_TransportFallback * fallback) { LG_LOCK_EXCLUSIVE(fallback->lock); atomic_store_explicit(&fallback->ready, false, memory_order_release); + atomic_store_explicit(&fallback->admitted, false, memory_order_release); fallback->closing = true; LG_UNLOCK_EXCLUSIVE(fallback->lock); } @@ -184,8 +235,20 @@ static bool cleanupConnection(LG_TransportFallback * fallback, return reportDisconnected; } -static void publishProviders(LG_TransportFallback * fallback) +static bool publishProviders(LG_TransportFallback * fallback) { + LG_LOCK(fallback->providerLock); + LG_LOCK_SHARED(fallback->lock); + const bool admitted = atomic_load_explicit( + &fallback->admitted, memory_order_acquire) && + uuidAdmittedLocked(fallback) && !fallback->closing; + if (!admitted) + { + LG_UNLOCK_SHARED(fallback->lock); + LG_UNLOCK(fallback->providerLock); + return false; + } + void * inputOpaque = NULL; const LG_InputOps * inputOps = fallback->transport.ops->getInputOps ? fallback->transport.ops->getInputOps( @@ -205,9 +268,12 @@ static void publishProviders(LG_TransportFallback * fallback) fallback->transport.handle, &clipboardOpaque) : NULL; lgClipboard_setFallback(clipboardOps, clipboardOpaque); fallback->providersPublished = true; + LG_UNLOCK_SHARED(fallback->lock); + LG_UNLOCK(fallback->providerLock); + return true; } -static bool publishConnection(LG_TransportFallback * fallback, +static bool admitConnection(LG_TransportFallback * fallback, bool * reportMismatch, uint8_t primaryUUID[16], uint8_t fallbackUUID[16]) { @@ -218,11 +284,24 @@ static bool publishConnection(LG_TransportFallback * fallback, fallback, primaryUUID, fallbackUUID); const bool stop = atomic_load_explicit( &fallback->stop, memory_order_acquire); + const bool admitted = !reject && !stop && !fallback->closing; + if (admitted) + atomic_store_explicit( + &fallback->admitted, true, memory_order_release); LG_UNLOCK_EXCLUSIVE(fallback->lock); - if (reject || stop) + return admitted; +} + +static bool publishConnection(LG_TransportFallback * fallback, + bool * reportMismatch, uint8_t primaryUUID[16], + uint8_t fallbackUUID[16]) +{ + if (!admitConnection(fallback, reportMismatch, + primaryUUID, fallbackUUID)) return false; - publishProviders(fallback); + if (!publishProviders(fallback)) + return false; for (;;) { @@ -231,12 +310,14 @@ static bool publishConnection(LG_TransportFallback * fallback, if (reject) *reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); + const bool admitted = uuidAdmittedLocked(fallback); const bool stop = atomic_load_explicit( &fallback->stop, memory_order_acquire); + const bool closing = fallback->closing; const bool requested = fallback->videoRequested; LG_UNLOCK_EXCLUSIVE(fallback->lock); - if (reject || stop) + if (reject || !admitted || stop || closing) return false; if (requested == fallback->videoActive) break; @@ -252,15 +333,18 @@ static bool publishConnection(LG_TransportFallback * fallback, if (finalReject) *reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); + const bool finalAdmitted = uuidAdmittedLocked(fallback); const bool finalStop = atomic_load_explicit( &fallback->stop, memory_order_acquire); - if (!finalReject && !finalStop) + if (!finalReject && finalAdmitted && !finalStop && !fallback->closing) { fallback->mismatchReported = false; atomic_store_explicit(&fallback->ready, true, memory_order_release); } + const bool published = !finalReject && finalAdmitted && !finalStop && + !fallback->closing; LG_UNLOCK_EXCLUSIVE(fallback->lock); - return !finalReject && !finalStop; + return published; } static bool connectFallback(LG_TransportFallback * fallback) @@ -315,8 +399,9 @@ static bool connectFallback(LG_TransportFallback * fallback) } LG_LOCK_EXCLUSIVE(fallback->lock); - fallback->connected = true; - fallback->session = session; + fallback->connected = true; + fallback->session = session; + fallback->disconnectedReported = false; ++fallback->connectionSerial; LG_UNLOCK_EXCLUSIVE(fallback->lock); @@ -333,13 +418,7 @@ static bool connectFallback(LG_TransportFallback * fallback) 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); + notifyConnected(fallback); while (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) { @@ -351,8 +430,10 @@ static bool connectFallback(LG_TransportFallback * fallback) if (reject) reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); + const bool admitted = uuidAdmittedLocked(fallback); + const bool closing = fallback->closing; LG_UNLOCK_EXCLUSIVE(fallback->lock); - if (reject) + if (reject || !admitted || closing) break; if (!sessionLive(fallback)) break; @@ -365,8 +446,7 @@ static bool connectFallback(LG_TransportFallback * fallback) if (reportMismatch && fallback->eventOps.uuidMismatch) fallback->eventOps.uuidMismatch( fallback->eventOpaque, primaryUUID, fallbackUUID); - if (reportDisconnect && fallback->eventOps.disconnected) - fallback->eventOps.disconnected(fallback->eventOpaque); + notifyDisconnected(fallback, reportDisconnect); return true; } @@ -388,14 +468,14 @@ static int fallbackThread(void * opaque) } const bool reportDisconnect = cleanupConnection(fallback, false); - if (reportDisconnect && fallback->eventOps.disconnected) - fallback->eventOps.disconnected(fallback->eventOpaque); + notifyDisconnected(fallback, reportDisconnect); return 0; } bool lgTransportFallback_start(const char * transportName, const LG_SwSurfaceEventOps * surfaceEvents, void * surfaceOpaque, const LG_TransportFallbackEventOps * eventOps, void * eventOpaque, + const uint8_t primaryUUID[16], LG_TransportFallback ** result) { if (!result) @@ -411,8 +491,11 @@ bool lgTransportFallback_start(const char * transportName, return false; LG_RWLOCK_INIT(fallback->lock); + LG_LOCK_INIT(fallback->providerLock); + LG_LOCK_INIT(fallback->eventLock); atomic_init(&fallback->stop, false); atomic_init(&fallback->ready, false); + atomic_init(&fallback->admitted, false); fallback->transportName = strdup(transportName); if (!fallback->transportName) @@ -423,6 +506,12 @@ bool lgTransportFallback_start(const char * transportName, if (eventOps) fallback->eventOps = *eventOps; fallback->eventOpaque = eventOpaque; + if (primaryUUID) + { + memcpy(fallback->primaryUUID, primaryUUID, + sizeof(fallback->primaryUUID)); + fallback->primaryUUIDValid = true; + } fallback->wakeEvent = lgCreateEvent(true, 0); if (!fallback->wakeEvent) @@ -439,6 +528,8 @@ fail: *result = NULL; if (fallback->wakeEvent) lgFreeEvent(fallback->wakeEvent); + LG_LOCK_FREE(fallback->eventLock); + LG_LOCK_FREE(fallback->providerLock); LG_RWLOCK_FREE(fallback->lock); free(fallback->transportName); free(fallback); @@ -465,6 +556,8 @@ void lgTransportFallback_stop(LG_TransportFallback ** fallbackPtr) } lgFreeEvent(fallback->wakeEvent); + LG_LOCK_FREE(fallback->eventLock); + LG_LOCK_FREE(fallback->providerLock); LG_RWLOCK_FREE(fallback->lock); free(fallback->transportName); free(fallback); @@ -476,6 +569,24 @@ bool lgTransportFallback_ready(const LG_TransportFallback * fallback) &fallback->ready, memory_order_acquire); } +bool lgTransportFallback_admitted(const LG_TransportFallback * fallback) +{ + return fallback && atomic_load_explicit( + &fallback->admitted, memory_order_acquire); +} + +bool lgTransportFallback_videoRequested( + LG_TransportFallback * fallback) +{ + if (!fallback) + return false; + + LG_LOCK_SHARED(fallback->lock); + const bool requested = fallback->videoRequested; + LG_UNLOCK_SHARED(fallback->lock); + return requested; +} + static bool applyVideoRequest(LG_TransportFallback * fallback) { for (;;) @@ -533,12 +644,30 @@ void lgTransportFallback_requestVideoActive( lgSignalEvent(fallback->wakeEvent); } +static bool revokeAdmissionLocked(LG_TransportFallback * fallback) +{ + atomic_store_explicit(&fallback->ready, false, memory_order_release); + atomic_store_explicit(&fallback->admitted, false, memory_order_release); + if (!fallback->connected) + return false; + + fallback->connectedReported = false; + fallback->closing = true; + return true; +} + void lgTransportFallback_setPrimaryUUID( LG_TransportFallback * fallback, const uint8_t uuid[16]) { if (!fallback || !uuid) return; + bool revoked = false; + bool notifyDisconnect = false; + bool reportMismatch = false; + uint8_t primaryUUID[16]; + uint8_t fallbackUUID[16]; + LG_LOCK_EXCLUSIVE(fallback->lock); const bool changed = !fallback->primaryUUIDValid || memcmp(fallback->primaryUUID, uuid, @@ -547,11 +676,24 @@ void lgTransportFallback_setPrimaryUUID( { memcpy(fallback->primaryUUID, uuid, sizeof(fallback->primaryUUID)); fallback->primaryUUIDValid = true; + if (uuidMismatchLocked(fallback)) + { + revoked = true; + notifyDisconnect = revokeAdmissionLocked(fallback); + reportMismatch = recordMismatchLocked( + fallback, primaryUUID, fallbackUUID); + } } LG_UNLOCK_EXCLUSIVE(fallback->lock); if (changed) lgSignalEvent(fallback->wakeEvent); + if (revoked) + unpublishProviders(fallback, false); + notifyDisconnected(fallback, notifyDisconnect); + if (reportMismatch && fallback->eventOps.uuidMismatch) + fallback->eventOps.uuidMismatch( + fallback->eventOpaque, primaryUUID, fallbackUUID); } void lgTransportFallback_clearPrimaryUUID( @@ -562,7 +704,11 @@ void lgTransportFallback_clearPrimaryUUID( LG_LOCK_EXCLUSIVE(fallback->lock); const bool changed = fallback->primaryUUIDValid; - fallback->primaryUUIDValid = false; + if (changed) + { + fallback->primaryUUIDValid = false; + fallback->mismatchReported = false; + } LG_UNLOCK_EXCLUSIVE(fallback->lock); if (changed) diff --git a/client/src/transport_fallback.h b/client/src/transport_fallback.h index 2d228c9a..e1bfa118 100644 --- a/client/src/transport_fallback.h +++ b/client/src/transport_fallback.h @@ -41,10 +41,14 @@ LG_TransportFallbackEventOps; bool lgTransportFallback_start(const char * transportName, const LG_SwSurfaceEventOps * surfaceEvents, void * surfaceOpaque, const LG_TransportFallbackEventOps * eventOps, void * eventOpaque, + const uint8_t primaryUUID[16], LG_TransportFallback ** result); void lgTransportFallback_stop(LG_TransportFallback ** fallback); bool lgTransportFallback_ready(const LG_TransportFallback * fallback); +bool lgTransportFallback_admitted(const LG_TransportFallback * fallback); +bool lgTransportFallback_videoRequested( + LG_TransportFallback * fallback); /* The requested state is retained across reconnects. */ void lgTransportFallback_requestVideoActive( LG_TransportFallback * fallback, bool active); diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 92a5f23b..85713376 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -23,12 +23,14 @@ #include "input.h" #include "common/KVMFR.h" +#include "common/KVMFRRecovery.h" #include "common/LGMPConfig.h" #include "common/debug.h" #include "common/ivshmem.h" #include "common/locking.h" #include "common/option.h" #include "common/stringutils.h" +#include "common/time.h" #include @@ -39,6 +41,11 @@ #include #define LGMP_TIMING_SPIN_COUNT 4096 +#define LGMP_RECOVERY_PROBE_INTERVAL_US 10000U +#define LGMP_RECOVERY_PROBE_TIMEOUT_US \ + ((KVMFR_R_HEARTBEAT_MS * 3U) * 1000U) +#define LGMP_RECOVERY_LIVE_TIMEOUT_US \ + ((KVMFR_R_HEARTBEAT_MS * 4U) * 1000U) struct DMAFrameInfo { @@ -91,6 +98,16 @@ struct LG_Transport struct DMAFrameInfo dma[LGMP_Q_FRAME_BUFFER_LEN]; uint8_t * pointerData; size_t pointerDataSize; + + size_t lgmpSize; + KVMFRR * recovery; + LG_Lock recoveryLock; + uint64_t recoveryCandidateSession; + uint64_t recoverySession; + uint64_t recoveryHeartbeatTime; + uint32_t recoveryCandidateHeartbeat; + uint32_t recoveryLGMPVersion; + bool recoveryLive; }; static bool lgmp_deviceValidator(struct Option * opt, const char ** error) @@ -186,6 +203,130 @@ static void lgmp_setup(void) option_register(options); } +static bool lgmp_recoveryMagic(const KVMFRR * recovery) +{ + return recovery && memcmp(recovery->header.magic, KVMFR_R_MAGIC, + sizeof(recovery->header.magic)) == 0; +} + +static bool lgmp_recoverySnapshot(const struct LG_Transport * this, + KVMFRRHeader * header, KVMFRRInfo * info) +{ + const KVMFRR * recovery = this->recovery; + if (!recovery || + __atomic_load_n(&recovery->header.ready, __ATOMIC_ACQUIRE) != + KVMFR_R_READY) + return false; + + memcpy(header, &recovery->header, sizeof(*header)); + memcpy(info, &recovery->info, sizeof(*info)); + header->heartbeat = __atomic_load_n(&recovery->header.heartbeat, + __ATOMIC_ACQUIRE); + + if (__atomic_load_n(&recovery->header.ready, __ATOMIC_ACQUIRE) != + KVMFR_R_READY || + memcmp(header->magic, KVMFR_R_MAGIC, + sizeof(header->magic)) != 0 || + header->abiVersion != KVMFR_R_VERSION || + header->structSize < sizeof(KVMFRR) || + header->session == 0) + return false; + + return true; +} + +static bool lgmp_refreshRecoveryLocked(struct LG_Transport * this, bool wait) +{ + const uint64_t deadline = wait ? + microtime() + LGMP_RECOVERY_PROBE_TIMEOUT_US : 0; + + do + { + KVMFRRHeader header; + KVMFRRInfo info; + const bool valid = lgmp_recoverySnapshot(this, &header, &info); + const uint64_t now = microtime(); + + if (valid) + { + if (this->recoveryCandidateSession != header.session) + { + this->recoveryCandidateSession = header.session; + this->recoveryCandidateHeartbeat = header.heartbeat; + this->recoveryLive = false; + this->lgmpSize = this->shm.size; + } + else if (this->recoveryCandidateHeartbeat != header.heartbeat) + { + this->recoveryCandidateHeartbeat = header.heartbeat; + this->recoverySession = header.session; + this->recoveryHeartbeatTime = now; + this->recoveryLive = true; + this->lgmpSize = this->shm.size - KVMFR_R_REGION_SIZE; + } + + if (this->recoveryLive && this->recoverySession == header.session && + now - this->recoveryHeartbeatTime <= + LGMP_RECOVERY_LIVE_TIMEOUT_US) + { + this->recoveryLGMPVersion = header.lgmpVersion; + return true; + } + } + else + { + this->recoveryLive = false; + this->lgmpSize = this->shm.size; + } + + if (!wait || now >= deadline) + break; + usleep(LGMP_RECOVERY_PROBE_INTERVAL_US); + } + while (true); + + this->recoveryLive = false; + this->lgmpSize = this->shm.size; + return false; +} + +static bool lgmp_recoveryVersions(struct LG_Transport * this, + uint32_t * lgmpVersion, uint32_t * kvmfrVersion) +{ + LG_LOCK(this->recoveryLock); + const bool live = lgmp_refreshRecoveryLocked(this, + lgmp_recoveryMagic(this->recovery)); + KVMFRRHeader header; + KVMFRRInfo info; + const bool valid = live && lgmp_recoverySnapshot(this, &header, &info) && + header.session == this->recoverySession; + if (valid) + { + if (lgmpVersion) + *lgmpVersion = header.lgmpVersion; + if (kvmfrVersion) + *kvmfrVersion = header.kvmfrVersion; + } + LG_UNLOCK(this->recoveryLock); + return valid; +} + +static LGMP_STATUS lgmp_initializeClient(struct LG_Transport * this) +{ + LGMP_STATUS status = lgmpClientInit(this->shm.mem, this->lgmpSize, + &this->client); + if (status != LGMP_OK) + return status; + + if (!lgmpInput_create(this->client, &this->input)) + { + lgmpClientFree(&this->client); + return LGMP_ERR_NO_MEM; + } + + return LGMP_OK; +} + static bool lgmp_create(LG_Transport ** result) { struct LG_Transport * this = calloc(1, sizeof(*this)); @@ -209,6 +350,7 @@ static bool lgmp_create(LG_Transport ** result) LG_LOCK_INIT(this->frameLock); LG_LOCK_INIT(this->pointerLock); + LG_LOCK_INIT(this->recoveryLock); this->frameGeneration = 1; this->frameLease[0].subscription = &this->frameQueue; @@ -220,28 +362,43 @@ static bool lgmp_create(LG_Transport ** result) { LG_LOCK_FREE(this->frameLock); LG_LOCK_FREE(this->pointerLock); + LG_LOCK_FREE(this->recoveryLock); free(this); return false; } - LGMP_STATUS status = lgmpClientInit(this->shm.mem, this->shm.size, - &this->client); + this->lgmpSize = this->shm.size; + if (this->shm.size >= KVMFR_R_REGION_SIZE + sizeof(KVMFRR)) + { + this->recovery = (KVMFRR *)((uint8_t *)this->shm.mem + + this->shm.size - KVMFR_R_REGION_SIZE); + if (lgmp_recoveryMagic(this->recovery)) + { + LG_LOCK(this->recoveryLock); + lgmp_refreshRecoveryLocked(this, true); + LG_UNLOCK(this->recoveryLock); + } + } + + LGMP_STATUS status = lgmp_initializeClient(this); if (status != LGMP_OK) { + if (this->recoveryLive && + (status == LGMP_ERR_INVALID_MAGIC || + status == LGMP_ERR_INVALID_VERSION)) + { + DEBUG_WARN("LGMP is unavailable (%s), recovery remains available", + lgmpStatusString(status)); + lgmpClientFree(&this->client); + *result = this; + return true; + } + DEBUG_ERROR("lgmpClientInit failed: %s", lgmpStatusString(status)); ivshmemClose(&this->shm); LG_LOCK_FREE(this->frameLock); LG_LOCK_FREE(this->pointerLock); - free(this); - return false; - } - - if (!lgmpInput_create(this->client, &this->input)) - { - lgmpClientFree(&this->client); - ivshmemClose(&this->shm); - LG_LOCK_FREE(this->frameLock); - LG_LOCK_FREE(this->pointerLock); + LG_LOCK_FREE(this->recoveryLock); free(this); return false; } @@ -371,29 +528,66 @@ static void lgmp_destroy(LG_Transport ** transport) return; struct LG_Transport * this = *transport; - lgmpInput_destroy(&this->input); - lgmp_closeQueues(this); + if (this->client) + { + lgmpInput_destroy(&this->input); + lgmp_closeQueues(this); + lgmpClientFree(&this->client); + } lgmp_closeDMA(this); free(this->pointerData); - lgmpClientFree(&this->client); ivshmemClose(&this->shm); LG_LOCK_FREE(this->frameLock); LG_LOCK_FREE(this->pointerLock); + LG_LOCK_FREE(this->recoveryLock); free(this); *transport = NULL; } -static bool lgmp_parseSession(const uint8_t * data, uint32_t size, - LG_TransportSession * session) +static void lgmp_setVersionMismatch(LG_TransportSession * session, + const char * component, uint32_t expected, uint32_t current) { - if (size < sizeof(KVMFR)) + session->versionMismatch.valid = true; + session->versionMismatch.expectedVersion = expected; + session->versionMismatch.currentVersion = current; + str_copy(session->versionMismatch.component, + sizeof(session->versionMismatch.component), component, + strlen(component)); +} + +static bool lgmp_parseSession(struct LG_Transport * this, + const uint8_t * data, uint32_t size, LG_TransportSession * session) +{ + if (!data || size < sizeof(KVMFR)) + { + uint32_t current = 0; + bool currentValid = data && + size >= offsetof(KVMFR, version) + sizeof(uint32_t); + if (currentValid) + { + memcpy(¤t, data + offsetof(KVMFR, version), sizeof(current)); + } + else + currentValid = lgmp_recoveryVersions(this, NULL, ¤t); + if (currentValid && current != KVMFR_VERSION) + lgmp_setVersionMismatch(session, "KVMFR", KVMFR_VERSION, current); return false; + } const KVMFR * header = (const KVMFR *)data; - if (memcmp(header->magic, KVMFR_MAGIC, sizeof(header->magic)) != 0 || - header->version != KVMFR_VERSION) + if (memcmp(header->magic, KVMFR_MAGIC, sizeof(header->magic)) != 0) { - session->remoteVersion = header->version; + uint32_t current; + if (lgmp_recoveryVersions(this, NULL, ¤t) && + current != KVMFR_VERSION) + lgmp_setVersionMismatch(session, "KVMFR", KVMFR_VERSION, current); + return false; + } + + if (header->version != KVMFR_VERSION) + { + lgmp_setVersionMismatch(session, "KVMFR", KVMFR_VERSION, + header->version); return false; } @@ -466,16 +660,43 @@ static LG_TransportStatus lgmp_connect(LG_Transport * this, memset(session, 0, sizeof(*session)); session->os = LG_TRANSPORT_OS_OTHER; + if (!this->client) + { + LG_LOCK(this->recoveryLock); + lgmp_refreshRecoveryLocked(this, + lgmp_recoveryMagic(this->recovery)); + LG_UNLOCK(this->recoveryLock); + + const LGMP_STATUS status = lgmp_initializeClient(this); + if (status != LGMP_OK) + { + uint32_t remoteVersion = 0; + const bool versionKnown = + lgmp_recoveryVersions(this, &remoteVersion, NULL); + if (status == LGMP_ERR_INVALID_VERSION || + (status == LGMP_ERR_INVALID_MAGIC && versionKnown && + remoteVersion != LGMP_PROTOCOL_VERSION)) + { + if (versionKnown && remoteVersion != LGMP_PROTOCOL_VERSION) + lgmp_setVersionMismatch(session, "LGMP", + LGMP_PROTOCOL_VERSION, remoteVersion); + return LG_TRANSPORT_INVALID_VERSION; + } + + return status == LGMP_ERR_INVALID_MAGIC ? + LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_ERROR; + } + } + uint32_t size; uint8_t * data; - uint32_t remoteVersion; + uint32_t remoteVersion = 0; LGMP_STATUS status = lgmpClientSessionInit(this->client, &size, &data, &this->clientID, &remoteVersion); - session->remoteVersion = remoteVersion; switch (status) { case LGMP_OK: - if (!lgmp_parseSession(data, size, session)) + if (!lgmp_parseSession(this, data, size, session)) return LG_TRANSPORT_INVALID_VERSION; LG_LOCK(this->frameLock); @@ -494,10 +715,23 @@ static LG_TransportStatus lgmp_connect(LG_Transport * this, return LG_TRANSPORT_OK; case LGMP_ERR_INVALID_VERSION: + if (!remoteVersion) + lgmp_recoveryVersions(this, &remoteVersion, NULL); + lgmp_setVersionMismatch(session, "LGMP", LGMP_PROTOCOL_VERSION, + remoteVersion); return LG_TRANSPORT_INVALID_VERSION; case LGMP_ERR_INVALID_SESSION: + return LG_TRANSPORT_UNAVAILABLE; + case LGMP_ERR_INVALID_MAGIC: + if (lgmp_recoveryVersions(this, &remoteVersion, NULL) && + remoteVersion != LGMP_PROTOCOL_VERSION) + { + lgmp_setVersionMismatch(session, "LGMP", LGMP_PROTOCOL_VERSION, + remoteVersion); + return LG_TRANSPORT_INVALID_VERSION; + } return LG_TRANSPORT_UNAVAILABLE; default: @@ -508,6 +742,9 @@ static LG_TransportStatus lgmp_connect(LG_Transport * this, static void lgmp_disconnect(LG_Transport * this) { + if (!this->client) + return; + lgmpInput_disconnect(this->input); lgmp_closeQueues(this); @@ -528,12 +765,295 @@ static void lgmp_disconnect(LG_Transport * this) static bool lgmp_sessionValid(LG_Transport * this) { - return this->connected && lgmpClientSessionValid(this->client); + return this->client && this->connected && + lgmpClientSessionValid(this->client); +} + +static LG_RecoveryRequest lgmp_recoveryRequestType(uint32_t request) +{ + switch (request) + { + case KVMFR_R_REQ_NORMAL: + return LG_RECOVERY_REQ_NORMAL; + case KVMFR_R_REQ_RECOVERY: + return LG_RECOVERY_REQ_RECOVERY; + default: + return LG_RECOVERY_REQ_NONE; + } +} + +static LG_RecoveryState lgmp_recoveryState(uint32_t state) +{ + switch (state) + { + case KVMFR_R_STATE_NORMAL: + return LG_RECOVERY_STATE_NORMAL; + case KVMFR_R_STATE_SWITCHING: + return LG_RECOVERY_STATE_SWITCHING; + case KVMFR_R_STATE_ACTIVE: + return LG_RECOVERY_STATE_ACTIVE; + case KVMFR_R_STATE_FAILED: + return LG_RECOVERY_STATE_FAILED; + default: + return LG_RECOVERY_STATE_UNKNOWN; + } +} + +static LG_RecoveryError lgmp_recoveryError(uint32_t error) +{ + switch (error) + { + case KVMFR_R_ERR_NONE: + return LG_RECOVERY_ERR_NONE; + case KVMFR_R_ERR_UNSUPPORTED: + return LG_RECOVERY_ERR_UNSUPPORTED; + case KVMFR_R_ERR_HELPER_UNAVAILABLE: + return LG_RECOVERY_ERR_HELPER_UNAVAILABLE; + case KVMFR_R_ERR_TOPOLOGY_FAILED: + return LG_RECOVERY_ERR_TOPOLOGY_FAILED; + case KVMFR_R_ERR_NO_FALLBACK_DISPLAY: + return LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY; + default: + return LG_RECOVERY_ERR_UNSUPPORTED; + } +} + +static bool lgmp_recoveryRequestSnapshot(const KVMFRRRequest * source, + KVMFRRRequest * result) +{ + for (unsigned i = 0; i < 4; ++i) + { + const uint32_t serial = __atomic_load_n(&source->serial, + __ATOMIC_ACQUIRE); + if (serial & KVMFR_R_REQ_WRITING) + continue; + + result->request = source->request; + result->session = source->session; + __atomic_thread_fence(__ATOMIC_ACQUIRE); + if (__atomic_load_n(&source->serial, __ATOMIC_RELAXED) == serial) + { + result->serial = serial; + return true; + } + } + + return false; +} + +static bool lgmp_recoveryStatusSnapshot(const KVMFRRStatus * source, + KVMFRRStatus * result) +{ + for (unsigned i = 0; i < 4; ++i) + { + const uint32_t serial = __atomic_load_n(&source->serial, + __ATOMIC_ACQUIRE); + if (!serial || (serial & 1U)) + continue; + + result->ackSerial = source->ackSerial; + result->ackRequest = source->ackRequest; + result->state = source->state; + result->error = source->error; + result->session = source->session; + __atomic_thread_fence(__ATOMIC_ACQUIRE); + if (__atomic_load_n(&source->serial, __ATOMIC_RELAXED) == serial) + { + result->serial = serial; + return true; + } + } + + return false; +} + +static bool lgmp_recoverySerialNewer(uint32_t serial, uint32_t reference) +{ + const uint32_t difference = serial - reference; + return difference && difference < 0x80000000U; +} + +static bool lgmp_recoveryLatestRequest(const KVMFRR * recovery, + uint64_t session, KVMFRRRequest * result) +{ + bool found = false; + for (unsigned i = 0; i < KVMFR_R_REQ_SLOTS; ++i) + { + KVMFRRRequest request; + if (!lgmp_recoveryRequestSnapshot(&recovery->requests[i], &request) || + !request.serial || request.session != session) + continue; + + if (!found || lgmp_recoverySerialNewer(request.serial, result->serial)) + { + *result = request; + found = true; + } + } + + return found; +} + +static LG_TransportStatus lgmp_getRecoveryInfo(LG_Transport * this, + LG_RecoveryInfo * info) +{ + if (!info) + return LG_TRANSPORT_ERROR; + + memset(info, 0, sizeof(*info)); + LG_LOCK(this->recoveryLock); + const bool live = lgmp_refreshRecoveryLocked(this, + lgmp_recoveryMagic(this->recovery)); + if (!live) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_UNAVAILABLE; + } + + KVMFRRHeader header; + KVMFRRInfo wireInfo; + if (!lgmp_recoverySnapshot(this, &header, &wireInfo) || + header.session != this->recoverySession) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_UNAVAILABLE; + } + + info->abiVersion = header.abiVersion; + info->instance = header.session; + info->heartbeat = header.heartbeat; + if (header.capabilities & KVMFR_R_CAP_DISPLAY) + info->capabilities |= LG_RECOVERY_CAP_DISPLAY; + + memcpy(info->uuid, header.uuid, sizeof(info->uuid)); + for (unsigned i = 0; i < sizeof(info->uuid); ++i) + info->uuidValid |= info->uuid[i] != 0; + str_copy(info->producerVersion, sizeof(info->producerVersion), + wireInfo.version, sizeof(wireInfo.version)); + + info->versionCount = 2; + str_copy(info->versions[0].component, + sizeof(info->versions[0].component), "LGMP", sizeof("LGMP")); + info->versions[0].version = header.lgmpVersion; + str_copy(info->versions[1].component, + sizeof(info->versions[1].component), "KVMFR", sizeof("KVMFR")); + info->versions[1].version = header.kvmfrVersion; + + KVMFRRRequest request = {0}; + if (lgmp_recoveryLatestRequest(this->recovery, header.session, &request)) + { + info->requestSerial = request.serial; + info->request = lgmp_recoveryRequestType(request.request); + } + + KVMFRRStatus status; + if (lgmp_recoveryStatusSnapshot(&this->recovery->status, &status) && + status.session == header.session) + { + info->ackSerial = status.ackSerial; + info->ackRequest = lgmp_recoveryRequestType(status.ackRequest); + info->state = lgmp_recoveryState(status.state); + info->error = lgmp_recoveryError(status.error); + } + + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_OK; +} + +static LG_TransportStatus lgmp_requestRecovery(LG_Transport * this, + LG_RecoveryRequest request, uint32_t * serial) +{ + uint32_t wireRequest; + switch (request) + { + case LG_RECOVERY_REQ_NORMAL: + wireRequest = KVMFR_R_REQ_NORMAL; + break; + case LG_RECOVERY_REQ_RECOVERY: + wireRequest = KVMFR_R_REQ_RECOVERY; + break; + default: + return LG_TRANSPORT_ERROR; + } + + LG_LOCK(this->recoveryLock); + if (!lgmp_refreshRecoveryLocked(this, + lgmp_recoveryMagic(this->recovery))) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_UNAVAILABLE; + } + + KVMFRRHeader header; + KVMFRRInfo wireInfo; + if (!lgmp_recoverySnapshot(this, &header, &wireInfo) || + header.session != this->recoverySession || + !(header.capabilities & KVMFR_R_CAP_DISPLAY)) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_UNAVAILABLE; + } + + uint32_t ticket = __atomic_add_fetch(&this->recovery->req.ticket, 2U, + __ATOMIC_RELAXED); + if (!ticket) + ticket = __atomic_add_fetch(&this->recovery->req.ticket, 2U, + __ATOMIC_RELAXED); + if (!ticket || (ticket & KVMFR_R_REQ_WRITING)) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_UNAVAILABLE; + } + + KVMFRRRequest * destination = NULL; + const uint32_t claimed = ticket | KVMFR_R_REQ_WRITING; + for (unsigned i = 0; i < KVMFR_R_REQ_SLOTS; ++i) + { + uint32_t expected = 0; + if (__atomic_compare_exchange_n(&this->recovery->requests[i].serial, + &expected, claimed, false, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) + { + destination = &this->recovery->requests[i]; + break; + } + } + if (!destination) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_TIMEOUT; + } + + destination->request = wireRequest; + destination->session = header.session; + + KVMFRRHeader currentHeader; + if (!lgmp_recoverySnapshot(this, ¤tHeader, &wireInfo) || + currentHeader.session != header.session) + { + uint32_t expected = claimed; + __atomic_compare_exchange_n(&destination->serial, &expected, 0, false, + __ATOMIC_RELEASE, __ATOMIC_RELAXED); + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_UNAVAILABLE; + } + + uint32_t expected = claimed; + if (!__atomic_compare_exchange_n(&destination->serial, &expected, ticket, + false, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) + { + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_TIMEOUT; + } + if (serial) + *serial = ticket; + + LG_UNLOCK(this->recoveryLock); + return LG_TRANSPORT_OK; } static bool lgmp_supportsDMA(LG_Transport * this) { - return this->allowDMA && ivshmemHasDMA(&this->shm); + return this->client && this->allowDMA && ivshmemHasDMA(&this->shm); } static bool lgmp_attachRenderer(LG_Transport * this, @@ -801,10 +1321,23 @@ static int lgmp_getDMA(struct LG_Transport * this, const KVMFRFrame * frame, if (dma->fd >= 0) return dma->fd; - const uintptr_t position = (uintptr_t)frame - (uintptr_t)this->shm.mem; - const uintptr_t offset = frame->offset + sizeof(FrameBuffer); + const uintptr_t base = (uintptr_t)this->shm.mem; + const uintptr_t address = (uintptr_t)frame; + if (address < base) + return -1; + + const size_t position = address - base; + if (position > this->lgmpSize || + frame->offset > this->lgmpSize - position || + sizeof(FrameBuffer) > this->lgmpSize - position - frame->offset) + return -1; + + const size_t offset = position + frame->offset + sizeof(FrameBuffer); + if (dataSize > this->lgmpSize - offset) + return -1; + dma->dataSize = dataSize; - dma->fd = ivshmemGetDMABuf(&this->shm, position + offset, dataSize); + dma->fd = ivshmemGetDMABuf(&this->shm, offset, dataSize); return dma->fd; } @@ -1388,15 +1921,17 @@ static const LG_VideoOps * lgmp_getVideoOps(LG_Transport * this) const LG_TransportOps LGT_LGMP = { - .name = "lgmp", - .setup = lgmp_setup, - .create = lgmp_create, - .destroy = lgmp_destroy, - .connect = lgmp_connect, - .disconnect = lgmp_disconnect, - .sessionValid = lgmp_sessionValid, - .getVideoOps = lgmp_getVideoOps, - .getInputOps = lgmp_getInputOps, - .sendControl = lgmp_sendControl, - .controlStatus = lgmp_controlStatus, + .name = "lgmp", + .setup = lgmp_setup, + .create = lgmp_create, + .destroy = lgmp_destroy, + .connect = lgmp_connect, + .disconnect = lgmp_disconnect, + .sessionValid = lgmp_sessionValid, + .getVideoOps = lgmp_getVideoOps, + .getInputOps = lgmp_getInputOps, + .getRecoveryInfo = lgmp_getRecoveryInfo, + .requestRecovery = lgmp_requestRecovery, + .sendControl = lgmp_sendControl, + .controlStatus = lgmp_controlStatus, };