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`