From e2ee9244ed05800394c3aec6207cefea894b8bea Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Thu, 20 Aug 2026 16:53:10 +1000 Subject: [PATCH] [client] recovery: add force keybind Add a configured escape key plus Shift+R command that requests display recovery from the LGMP transport and switches video to SPICE. Track the request on the main thread, report its asynchronous result, and return its producer instance with the request serial for exact correlation. Preserve normal escape+R rotation behavior, including custom Shift escape keys. --- client/include/interface/transport.h | 2 +- client/src/app.c | 62 ++++++++-- client/src/keybind.c | 12 +- client/src/main.c | 169 ++++++++++++++++++++++++++- client/src/main.h | 6 + client/src/overlay/help.c | 8 ++ client/transports/LGMP/lgmp.c | 4 +- doc/usage.rst | 2 + 8 files changed, 248 insertions(+), 17 deletions(-) diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index a8c1bdca..5c692327 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -485,7 +485,7 @@ typedef struct LG_TransportOps LG_TransportStatus (*getRecoveryInfo)(LG_Transport * transport, LG_RecoveryInfo * info); LG_TransportStatus (*requestRecovery)(LG_Transport * transport, - LG_RecoveryRequest request, uint32_t * serial); + LG_RecoveryRequest request, uint64_t * instance, uint32_t * serial); LG_TransportStatus (*sendControl)(LG_Transport * transport, const LG_TransportControl * control, LG_TransportControlToken * token); diff --git a/client/src/app.c b/client/src/app.c index ec42a401..23ad6d6d 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -47,6 +47,12 @@ #define SHADER_MOUSE_VALID (UINT32_C(1) << 31) +enum +{ + KEY_MODIFIER_LEFT_SHIFT = 0x1, + KEY_MODIFIER_RIGHT_SHIFT = 0x2, +}; + #define MTRACE(fmt, ...) \ do \ { \ @@ -273,6 +279,9 @@ void app_handleFocusEvent(bool focused) if (!focused) { + atomic_store_explicit( + &g_state.keyModifiers, 0, memory_order_release); + g_state.escapeActive = false; core_setGrabQuiet(false); core_setCursorInView(false); } @@ -289,8 +298,6 @@ void app_handleFocusEvent(bool focused) if (g_params.releaseKeysOnFocusLoss) lgInput_releaseKeys(); - g_state.escapeActive = false; - if (!g_params.showCursorDot) g_state.ds->setPointer(LG_POINTER_NONE); @@ -418,10 +425,20 @@ void app_handleWheelMotion(double motion) void app_handleKeyPressInternal(int sc) { + if (sc == KEY_LEFTSHIFT) + atomic_fetch_or_explicit(&g_state.keyModifiers, + KEY_MODIFIER_LEFT_SHIFT, memory_order_acq_rel); + else if (sc == KEY_RIGHTSHIFT) + atomic_fetch_or_explicit(&g_state.keyModifiers, + KEY_MODIFIER_RIGHT_SHIFT, memory_order_acq_rel); + if (!app_isOverlayMode() || !g_state.io->WantCaptureKeyboard) { if (sc == g_params.escapeKey && !g_state.escapeActive) { + memset(g_state.escapeKeys, 0, sizeof(g_state.escapeKeys)); + memset(g_state.escapeShiftKeys, 0, + sizeof(g_state.escapeShiftKeys)); g_state.escapeActive = true; g_state.escapeTime = microtime(); g_state.escapeAction = -1; @@ -431,6 +448,7 @@ void app_handleKeyPressInternal(int sc) if (g_state.escapeActive) { + const bool repeat = g_state.escapeKeys[sc]; g_state.escapeAction = sc; g_state.escapeKeys[sc] = true; KeybindHandle handle; @@ -438,7 +456,24 @@ void app_handleKeyPressInternal(int sc) { if (handle->sc == sc) { - handle->callback(sc, handle->opaque); + unsigned modifiers = atomic_load_explicit( + &g_state.keyModifiers, memory_order_acquire); + if (g_params.escapeKey == KEY_LEFTSHIFT) + modifiers &= ~(unsigned)KEY_MODIFIER_LEFT_SHIFT; + else if (g_params.escapeKey == KEY_RIGHTSHIFT) + modifiers &= ~(unsigned)KEY_MODIFIER_RIGHT_SHIFT; + const bool shift = modifiers != 0; + if (!repeat) + g_state.escapeShiftKeys[sc] = + shift && handle->shiftCallback; + + if (g_state.escapeShiftKeys[sc]) + { + if (!repeat) + handle->shiftCallback(sc, handle->shiftOpaque); + } + else + handle->callback(sc, handle->opaque); break; } } @@ -472,6 +507,13 @@ void app_handleKeyPressInternal(int sc) void app_handleKeyReleaseInternal(int sc) { + if (sc == KEY_LEFTSHIFT) + atomic_fetch_and_explicit(&g_state.keyModifiers, + ~(unsigned)KEY_MODIFIER_LEFT_SHIFT, memory_order_acq_rel); + else if (sc == KEY_RIGHTSHIFT) + atomic_fetch_and_explicit(&g_state.keyModifiers, + ~(unsigned)KEY_MODIFIER_RIGHT_SHIFT, memory_order_acq_rel); + if (g_state.escapeActive && sc == g_params.escapeKey) { if (g_state.escapeAction == -1) @@ -486,7 +528,8 @@ void app_handleKeyReleaseInternal(int sc) if (g_state.escapeKeys[sc]) { - g_state.escapeKeys[sc] = false; + g_state.escapeKeys[sc] = false; + g_state.escapeShiftKeys[sc] = false; return; } @@ -814,10 +857,13 @@ KeybindHandle app_registerKeybind(int sc, KeybindFn callback, void * opaque, return NULL; } - handle->sc = sc; - handle->callback = callback; - handle->description = description; - handle->opaque = opaque; + handle->sc = sc; + handle->callback = callback; + handle->shiftCallback = NULL; + handle->description = description; + handle->shiftDescription = NULL; + handle->opaque = opaque; + handle->shiftOpaque = NULL; ll_push(g_state.bindings, handle); return handle; diff --git a/client/src/keybind.c b/client/src/keybind.c index 66345a69..6da01f1b 100644 --- a/client/src/keybind.c +++ b/client/src/keybind.c @@ -50,6 +50,11 @@ static void bind_rotate(int sc, void * opaque) core_updatePositionInfo(); } +static void bind_forceRecovery(int sc, void * opaque) +{ + atomic_store_explicit(&g_state.forceRecovery, true, memory_order_release); +} + static void bind_input(int sc, void * opaque) { g_state.ignoreInput = !g_state.ignoreInput; @@ -150,8 +155,13 @@ void keybind_commonRegister(void) "Full screen toggle"); app_registerKeybind(KEY_V, bind_video , NULL, "Video stream toggle"); - app_registerKeybind(KEY_R, bind_rotate , NULL, + KeybindHandle rotate = app_registerKeybind(KEY_R, bind_rotate, NULL, "Rotate the output clockwise by 90° increments"); + if (rotate) + { + rotate->shiftCallback = bind_forceRecovery; + rotate->shiftDescription = "Force guest display recovery"; + } app_registerKeybind(KEY_Q, bind_quit , NULL, "Quit"); app_registerKeybind(KEY_O, bind_toggleOverlay, NULL, diff --git a/client/src/main.c b/client/src/main.c index 460399da..97af823d 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -2803,6 +2803,13 @@ struct RecoveryPrompt bool retryDeclined; }; +struct ForcedRecovery +{ + uint64_t instance; + uint32_t serial; + bool pending; +}; + static bool recoveryGetInfo(LG_RecoveryInfo * info) { if (!g_state.transport.ops->getRecoveryInfo) @@ -3043,6 +3050,152 @@ static bool recoverySerialNewer(uint32_t serial, uint32_t reference) return difference && difference < 0x80000000U; } +static bool recoveryRequestNewer(const LG_RecoveryInfo * info) +{ + return info->requestSerial != 0 && + (info->ackSerial == 0 || + recoverySerialNewer(info->requestSerial, info->ackSerial)); +} + +static void recoveryHandleForced(struct ForcedRecovery * recovery) +{ + const bool triggered = atomic_exchange_explicit( + &g_state.forceRecovery, false, memory_order_acq_rel); + if (!triggered && !recovery->pending) + return; + + LG_RecoveryInfo info = { 0 }; + if (!recoveryGetInfo(&info) || + !(info.capabilities & LG_RECOVERY_CAP_DISPLAY)) + { + if (triggered) + app_alert(LG_ALERT_WARNING, + "Guest display recovery is unavailable"); + return; + } + + if (info.uuidValid) + lgTransportFallback_setPrimaryUUID(g_state.fallback, info.uuid); + else + lgTransportFallback_clearPrimaryUUID(g_state.fallback); + + const bool requestNewer = recoveryRequestNewer(&info); + const LG_RecoveryRequest currentRequest = requestNewer ? + info.request : info.ackRequest; + const uint32_t currentSerial = requestNewer ? + info.requestSerial : info.ackSerial; + + if (recovery->pending) + { + const bool instanceChanged = recovery->instance != info.instance; + const bool currentRelevant = currentSerial && + (instanceChanged || currentSerial == recovery->serial || + recoverySerialNewer(currentSerial, recovery->serial)); + if (!currentRelevant && instanceChanged) + { + recovery->pending = false; + app_alert(LG_ALERT_WARNING, + "Guest display recovery was interrupted"); + if (!triggered) + return; + } + else if (currentRelevant && + currentRequest == LG_RECOVERY_REQ_RECOVERY) + { + recovery->instance = info.instance; + recovery->serial = currentSerial; + if (!requestNewer && info.state == LG_RECOVERY_STATE_ACTIVE) + { + recovery->pending = false; + fallbackRequestVideo(); + app_alert(LG_ALERT_SUCCESS, + "Guest display recovery is active"); + return; + } + + if (!requestNewer && info.state == LG_RECOVERY_STATE_FAILED) + { + recovery->pending = false; + app_alert(LG_ALERT_WARNING, "Recovery failed: %s", + recoveryErrorText(info.error)); + if (!triggered) + return; + } + else + { + fallbackRequestVideo(); + return; + } + } + else if (currentRelevant) + { + recovery->pending = false; + app_alert(LG_ALERT_WARNING, + "Guest display recovery was superseded"); + if (!triggered) + return; + } + else + { + fallbackRequestVideo(); + return; + } + } + + if (!triggered) + return; + + const bool pendingRecovery = requestNewer && + info.request == LG_RECOVERY_REQ_RECOVERY; + const bool switchingRecovery = !requestNewer && info.ackSerial && + info.ackRequest == LG_RECOVERY_REQ_RECOVERY && + info.state == LG_RECOVERY_STATE_SWITCHING; + const bool activeRecovery = !requestNewer && info.ackSerial && + info.ackRequest == LG_RECOVERY_REQ_RECOVERY && + info.state == LG_RECOVERY_STATE_ACTIVE; + if (activeRecovery) + { + fallbackRequestVideo(); + app_alert(LG_ALERT_SUCCESS, + "Guest display recovery is already active"); + return; + } + + if (pendingRecovery || switchingRecovery) + { + recovery->instance = info.instance; + recovery->serial = currentSerial; + recovery->pending = true; + fallbackRequestVideo(); + app_alert(LG_ALERT_INFO, + "Guest display recovery is already pending"); + return; + } + + if (!g_state.transport.ops->requestRecovery) + { + app_alert(LG_ALERT_WARNING, + "Guest display recovery is unavailable"); + return; + } + + uint32_t serial = 0; + const LG_TransportStatus status = + g_state.transport.ops->requestRecovery(g_state.transport.handle, + LG_RECOVERY_REQ_RECOVERY, &recovery->instance, &serial); + if (status != LG_TRANSPORT_OK) + { + app_alert(LG_ALERT_WARNING, + "Guest display recovery request failed (%d)", status); + return; + } + + recovery->serial = serial; + recovery->pending = true; + fallbackRequestVideo(); + app_alert(LG_ALERT_INFO, "Guest display recovery requested"); +} + static void recoveryHandleMismatch(struct RecoveryPrompt * prompt, const LG_VersionMismatch * mismatch) { @@ -3114,9 +3267,7 @@ static void recoveryHandleMismatch(struct RecoveryPrompt * prompt, else lgTransportFallback_clearPrimaryUUID(g_state.fallback); - const bool requestNewer = info.requestSerial != 0 && - (info.ackSerial == 0 || - recoverySerialNewer(info.requestSerial, info.ackSerial)); + const bool requestNewer = recoveryRequestNewer(&info); const LG_RecoveryRequest globalRequest = requestNewer ? info.request : info.ackRequest; const uint32_t globalSerial = requestNewer ? @@ -3218,7 +3369,8 @@ static void recoveryHandleMismatch(struct RecoveryPrompt * prompt, 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_RECOVERY_REQ_RECOVERY, &prompt->instance, + &prompt->serial) : LG_TRANSPORT_UNAVAILABLE; prompt->requested = status == LG_TRANSPORT_OK; prompt->owned = prompt->requested; @@ -3346,6 +3498,8 @@ static int lg_run(void) frameTimingInit(); lgInput_init(); lgAudio_init(); + atomic_init(&g_state.forceRecovery, false); + atomic_init(&g_state.keyModifiers, 0); if (!clipboard_init()) return -1; @@ -3688,6 +3842,7 @@ static int lg_run(void) MsgBoxHandle msgs[10]; int msgsCount; struct RecoveryPrompt recoveryPrompt = { 0 }; + struct ForcedRecovery forcedRecovery = { 0 }; atomic_init(&recoveryPrompt.choice, RECOVERY_PENDING); atomic_init(&recoveryPrompt.handle, 0); atomic_init(&recoveryPrompt.message, 0); @@ -3704,6 +3859,7 @@ restart: while(app_getState() == APP_STATE_RUNNING) { fallbackHandleEvents(); + recoveryHandleForced(&forcedRecovery); if (initialFallbackEnable && microtime() > initialFallbackEnable) { @@ -3795,7 +3951,7 @@ restart: recoveryClose(&recoveryPrompt); LG_RecoveryInfo recoveryInfo = { 0 }; - if (recoveryGetInfo(&recoveryInfo) && + if (!forcedRecovery.pending && recoveryGetInfo(&recoveryInfo) && (recoveryInfo.state == LG_RECOVERY_STATE_ACTIVE || recoveryInfo.state == LG_RECOVERY_STATE_SWITCHING || recoveryInfo.request == LG_RECOVERY_REQ_RECOVERY || @@ -3804,7 +3960,7 @@ restart: { const LG_TransportStatus status = g_state.transport.ops->requestRecovery(g_state.transport.handle, - LG_RECOVERY_REQ_NORMAL, NULL); + LG_RECOVERY_REQ_NORMAL, NULL, NULL); if (status != LG_TRANSPORT_OK) DEBUG_WARN("Failed to leave recovery mode: %d", status); } @@ -3931,6 +4087,7 @@ restart: while(likely(app_getState() == APP_STATE_RUNNING)) { fallbackHandleEvents(); + recoveryHandleForced(&forcedRecovery); if (unlikely(!g_state.transport.ops->sessionValid( g_state.transport.handle))) { diff --git a/client/src/main.h b/client/src/main.h index 11a5ac97..186db4ca 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -116,6 +116,7 @@ struct AppState atomic_bool fallbackEndpointMismatch; atomic_uint transportLost; + atomic_bool forceRecovery; uint8_t guestUUID[16]; bool guestUUIDValid; @@ -130,6 +131,8 @@ struct AppState uint64_t escapeTime; int escapeAction; bool escapeKeys[KEY_MAX]; + bool escapeShiftKeys[KEY_MAX]; + atomic_uint keyModifiers; bool escapeHelp; struct ll * bindings; bool haveSrcSize; @@ -265,8 +268,11 @@ struct KeybindHandle { int sc; KeybindFn callback; + KeybindFn shiftCallback; const char * description; + const char * shiftDescription; void * opaque; + void * shiftOpaque; }; enum WarpState diff --git a/client/src/overlay/help.c b/client/src/overlay/help.c index 592fe29e..fa224c75 100644 --- a/client/src/overlay/help.c +++ b/client/src/overlay/help.c @@ -82,6 +82,14 @@ static int help_render(void * udata, bool interactive, struct Rect * windowRects igText("%s+%s", escapeName, keyName); igTableNextColumn(); igTextUnformatted(handle->description, NULL); + + if (handle->shiftCallback && handle->shiftDescription) + { + igTableNextColumn(); + igText("%s+Shift+%s", escapeName, keyName); + igTableNextColumn(); + igTextUnformatted(handle->shiftDescription, NULL); + } } igEndTable(); diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 7e7aa37b..49fc96bc 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -1420,7 +1420,7 @@ static LG_TransportStatus lgmp_getRecoveryInfo(LG_Transport * this, } static LG_TransportStatus lgmp_requestRecovery(LG_Transport * this, - LG_RecoveryRequest request, uint32_t * serial) + LG_RecoveryRequest request, uint64_t * instance, uint32_t * serial) { uint32_t wireRequest; switch (request) @@ -1505,6 +1505,8 @@ static LG_TransportStatus lgmp_requestRecovery(LG_Transport * this, } if (serial) *serial = ticket; + if (instance) + *instance = header.session; LG_UNLOCK(this->recoveryLock); return LG_TRANSPORT_OK; diff --git a/doc/usage.rst b/doc/usage.rst index e6e14e83..fad6b063 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -46,6 +46,8 @@ not have :kbd:`ScrLk`. - Toggle the video stream * - :kbd:`ScrLk` + :kbd:`R` - Rotate clockwise by 90 degrees + * - :kbd:`ScrLk` + :kbd:`Shift` + :kbd:`R` + - Force guest display recovery mode * - :kbd:`ScrLk` + :kbd:`=` - Ask the IDD to match the client window resolution * - :kbd:`ScrLk` + :kbd:`I`