diff --git a/client/include/input.h b/client/include/input.h index ae0dd54b..830029a7 100644 --- a/client/include/input.h +++ b/client/include/input.h @@ -23,6 +23,9 @@ #include "interface/input.h" +typedef void (*LGInputKeyboardLEDsFn)(void * opaque, + bool valid, uint8_t leds); + void lgInput_init(void); void lgInput_free(void); @@ -35,6 +38,11 @@ void lgInput_useTransport(bool enable); bool lgInput_available(void); bool lgInput_supports(LG_InputSupport support); +/* The listener is called synchronously with the current guest LED state and + * later from transport threads whenever it changes. The callback must not + * call back into the input layer. */ +void lgInput_setKeyboardLEDsListener( + LGInputKeyboardLEDsFn callback, void * opaque); bool lgInput_keyDown(int key); bool lgInput_keyUp(int key); diff --git a/client/include/interface/input.h b/client/include/interface/input.h index 3d8ee4a1..99644db8 100644 --- a/client/include/interface/input.h +++ b/client/include/interface/input.h @@ -30,10 +30,22 @@ typedef enum LG_InputSupport } LG_InputSupport; +enum +{ + LG_KEYBOARD_LED_NUM_LOCK = 0x1, + LG_KEYBOARD_LED_CAPS_LOCK = 0x2, + LG_KEYBOARD_LED_SCROLL_LOCK = 0x4, + LG_KEYBOARD_LED_COMPOSE = 0x8, + LG_KEYBOARD_LED_KANA = 0x10, +}; + typedef struct LG_InputStatus { bool available; uint32_t generation; + /* USB HID keyboard LED bits reported by the guest. */ + bool keyboardLEDsValid; + uint8_t keyboardLEDs; } LG_InputStatus; diff --git a/client/src/input.c b/client/src/input.c index c6267c73..ccdf0355 100644 --- a/client/src/input.c +++ b/client/src/input.c @@ -32,6 +32,8 @@ struct InputBinding void * opaque; bool available; bool mouseAbsolute; + bool keyboardLEDsValid; + uint8_t keyboardLEDs; uint32_t epoch; }; @@ -39,8 +41,12 @@ static struct { LG_Lock bindingLock; LG_RWLock activeLock; + LG_Lock keyboardLEDsDispatch; uint32_t nextBindingEpoch; + LGInputKeyboardLEDsFn keyboardLEDsCallback; + void * keyboardLEDsOpaque; + struct InputBinding fallback; struct InputBinding transport; struct InputBinding active; @@ -95,6 +101,31 @@ static bool bindingEqual(const struct InputBinding * a, a->epoch == b->epoch; } +static bool keyboardLEDsEqual(const struct InputBinding * a, + const struct InputBinding * b) +{ + const bool aValid = a->ops && a->available && a->keyboardLEDsValid; + const bool bValid = b->ops && b->available && b->keyboardLEDsValid; + return aValid == bValid && (!aValid || a->keyboardLEDs == b->keyboardLEDs); +} + +static void dispatchKeyboardLEDs(void) +{ + LG_LOCK(l_input.keyboardLEDsDispatch); + LG_LOCK_SHARED(l_input.activeLock); + LGInputKeyboardLEDsFn callback = l_input.keyboardLEDsCallback; + void * opaque = l_input.keyboardLEDsOpaque; + const bool valid = l_input.active.ops && + l_input.active.available && + l_input.active.keyboardLEDsValid; + const uint8_t leds = l_input.active.keyboardLEDs; + LG_UNLOCK_SHARED(l_input.activeLock); + + if (callback) + callback(opaque, valid, leds); + LG_UNLOCK(l_input.keyboardLEDsDispatch); +} + static void releaseKeysNL(void) { for (int key = 0; key < KEY_MAX; ++key) @@ -132,7 +163,7 @@ static void clearStateNL(void) atomic_store_explicit(&l_input.buttons, 0, memory_order_relaxed); } -static void updateActiveNL(bool dropActive) +static bool updateActiveNL(bool dropActive) { const struct InputBinding next = l_input.useTransport && l_input.transport.available ? @@ -142,10 +173,15 @@ static void updateActiveNL(bool dropActive) if (bindingEqual(&next, &l_input.active)) { + const bool keyboardLEDsChanged = + !keyboardLEDsEqual(&next, &l_input.active); l_input.active = next; - return; + return keyboardLEDsChanged; } + const bool keyboardLEDsChanged = + !keyboardLEDsEqual(&next, &l_input.active); + if (dropActive) clearStateNL(); else @@ -160,11 +196,13 @@ static void updateActiveNL(bool dropActive) } else DEBUG_INFO("Input is unavailable"); + return keyboardLEDsChanged; } -static void updateStatusNL(struct InputBinding * binding, +static bool updateStatusNL(struct InputBinding * binding, const LG_InputStatus * status) { + const struct InputBinding oldActive = l_input.active; const bool wasActive = bindingEqual(&l_input.active, binding); if (wasActive && !status->available) @@ -178,7 +216,11 @@ static void updateStatusNL(struct InputBinding * binding, binding->ops->mousePosition && binding->ops->supports(binding->opaque, LG_INPUT_SUPPORT_MOUSE_ABSOLUTE); + binding->keyboardLEDsValid = status->available && + status->keyboardLEDsValid; + binding->keyboardLEDs = status->keyboardLEDs; updateActiveNL(false); + return !keyboardLEDsEqual(&oldActive, &l_input.active); } static void fallbackStatusChanged(void * opaque, @@ -188,10 +230,13 @@ static void fallbackStatusChanged(void * opaque, return; LG_LOCK_EXCLUSIVE(l_input.activeLock); + bool keyboardLEDsChanged = false; if (l_input.fallback.ops && l_input.fallback.epoch == (uint32_t)(uintptr_t)opaque) - updateStatusNL(&l_input.fallback, status); + keyboardLEDsChanged = updateStatusNL(&l_input.fallback, status); LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + if (keyboardLEDsChanged) + dispatchKeyboardLEDs(); } static void transportStatusChanged(void * opaque, @@ -201,10 +246,13 @@ static void transportStatusChanged(void * opaque, return; LG_LOCK_EXCLUSIVE(l_input.activeLock); + bool keyboardLEDsChanged = false; if (l_input.transport.ops && l_input.transport.epoch == (uint32_t)(uintptr_t)opaque) - updateStatusNL(&l_input.transport, status); + keyboardLEDsChanged = updateStatusNL(&l_input.transport, status); LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + if (keyboardLEDsChanged) + dispatchKeyboardLEDs(); } void lgInput_init(void) @@ -220,6 +268,7 @@ void lgInput_init(void) LG_LOCK_INIT(l_input.bindingLock); LG_RWLOCK_INIT(l_input.activeLock); + LG_LOCK_INIT(l_input.keyboardLEDsDispatch); } void lgInput_free(void) @@ -228,6 +277,7 @@ void lgInput_free(void) struct InputBinding transport; LG_LOCK(l_input.bindingLock); + LG_LOCK(l_input.keyboardLEDsDispatch); LG_LOCK_EXCLUSIVE(l_input.activeLock); resetActiveNL(); fallback = l_input.fallback; @@ -235,7 +285,10 @@ void lgInput_free(void) l_input.active = (struct InputBinding) { 0 }; l_input.fallback = (struct InputBinding) { 0 }; l_input.transport = (struct InputBinding) { 0 }; + l_input.keyboardLEDsCallback = NULL; + l_input.keyboardLEDsOpaque = NULL; LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + LG_UNLOCK(l_input.keyboardLEDsDispatch); if (fallback.ops && fallback.ops->setStatusListener) fallback.ops->setStatusListener(fallback.opaque, NULL, NULL); @@ -244,6 +297,7 @@ void lgInput_free(void) LG_UNLOCK(l_input.bindingLock); LG_RWLOCK_FREE(l_input.activeLock); + LG_LOCK_FREE(l_input.keyboardLEDsDispatch); LG_LOCK_FREE(l_input.bindingLock); } @@ -270,9 +324,12 @@ static void setBinding(struct InputBinding * target, const struct InputBinding next = makeBinding(ops, opaque); LG_LOCK_EXCLUSIVE(l_input.activeLock); *target = next; - updateActiveNL(false); + const bool keyboardLEDsChanged = updateActiveNL(false); LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + if (keyboardLEDsChanged) + dispatchKeyboardLEDs(); + if (next.ops && next.ops->setStatusListener) next.ops->setStatusListener(next.opaque, statusFn, (void *)(uintptr_t)next.epoch); @@ -285,8 +342,10 @@ static void dropBinding(struct InputBinding * target) LG_LOCK_EXCLUSIVE(l_input.activeLock); const bool wasActive = bindingEqual(&l_input.active, target); *target = (struct InputBinding) { 0 }; - updateActiveNL(wasActive); + const bool keyboardLEDsChanged = updateActiveNL(wasActive); LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + if (keyboardLEDsChanged) + dispatchKeyboardLEDs(); LG_UNLOCK(l_input.bindingLock); } @@ -314,8 +373,10 @@ void lgInput_useTransport(bool enable) { LG_LOCK_EXCLUSIVE(l_input.activeLock); l_input.useTransport = enable; - updateActiveNL(false); + const bool keyboardLEDsChanged = updateActiveNL(false); LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + if (keyboardLEDsChanged) + dispatchKeyboardLEDs(); } bool lgInput_available(void) @@ -344,6 +405,24 @@ bool lgInput_supports(LG_InputSupport support) return result; } +void lgInput_setKeyboardLEDsListener( + LGInputKeyboardLEDsFn callback, void * opaque) +{ + LG_LOCK(l_input.keyboardLEDsDispatch); + LG_LOCK_SHARED(l_input.activeLock); + l_input.keyboardLEDsCallback = callback; + l_input.keyboardLEDsOpaque = opaque; + const bool valid = l_input.active.ops && + l_input.active.available && + l_input.active.keyboardLEDsValid; + const uint8_t leds = l_input.active.keyboardLEDs; + LG_UNLOCK_SHARED(l_input.activeLock); + + if (callback) + callback(opaque, valid, leds); + LG_UNLOCK(l_input.keyboardLEDsDispatch); +} + bool lgInput_keyDown(int key) { if (key < 0 || key >= KEY_MAX) diff --git a/client/tests/spice_test.c b/client/tests/spice_test.c index 16b729c1..dda2cb09 100644 --- a/client/tests/spice_test.c +++ b/client/tests/spice_test.c @@ -41,6 +41,7 @@ extern const LG_TransportOps LGT_SPICE; struct SpiceInput { bool available; + uint32_t keyModifiers; unsigned int onN; unsigned int offN; unsigned int onOrder; @@ -304,6 +305,11 @@ void spiceInput_setAvailable(SpiceInput * input, bool available) } } +void spiceInput_setKeyboardLEDs(SpiceInput * input, uint32_t modifiers) +{ + input->keyModifiers = modifiers; +} + bool spiceClipboard_init(SpiceClipboard ** clipboard) { *clipboard = calloc(1, sizeof(**clipboard)); diff --git a/client/transports/LGMP/input.c b/client/transports/LGMP/input.c index e3e41a6c..62cc1e01 100644 --- a/client/transports/LGMP/input.c +++ b/client/transports/LGMP/input.c @@ -52,6 +52,18 @@ #define INPUT_MOUSE_DELTA_MIN (INT16_MIN * INPUT_MAX_SPLIT_REPORTS) #define INPUT_MOUSE_DELTA_MAX (INT16_MAX * INPUT_MAX_SPLIT_REPORTS) +_Static_assert( + (int)LG_KEYBOARD_LED_NUM_LOCK == + (int)KVMFR_INPUT_KEYBOARD_LED_NUM_LOCK && + (int)LG_KEYBOARD_LED_CAPS_LOCK == + (int)KVMFR_INPUT_KEYBOARD_LED_CAPS_LOCK && + (int)LG_KEYBOARD_LED_SCROLL_LOCK == + (int)KVMFR_INPUT_KEYBOARD_LED_SCROLL_LOCK && + (int)LG_KEYBOARD_LED_COMPOSE == + (int)KVMFR_INPUT_KEYBOARD_LED_COMPOSE && + (int)LG_KEYBOARD_LED_KANA == (int)KVMFR_INPUT_KEYBOARD_LED_KANA, + "Client and KVMFR keyboard LED bits differ"); + enum LGMPInputMouseMode { LGMP_INPUT_MOUSE_NONE, @@ -104,6 +116,8 @@ struct LGMPInput bool claimed; bool available; bool ownerBlocked; + bool keyboardLEDsValid; + uint8_t keyboardLEDs; uint32_t generation; uint32_t sequence; @@ -159,8 +173,10 @@ static LG_InputStatus inputStatus(const LGMPInput * input) { return (LG_InputStatus) { - .available = input->available, - .generation = input->endpointGeneration, + .available = input->available, + .generation = input->endpointGeneration, + .keyboardLEDsValid = input->available && input->keyboardLEDsValid, + .keyboardLEDs = input->keyboardLEDs, }; } @@ -227,12 +243,15 @@ static void connectionFailed(LGMPInput * input, LGMP_STATUS status) ++input->stats.counters.terminalFailures; } - if (input->available || input->endpointGeneration) + if (input->available || input->endpointGeneration || + input->keyboardLEDsValid) input->notifyStatus = true; input->connected = false; input->claimed = false; input->available = false; input->ownerBlocked = false; + input->keyboardLEDsValid = false; + input->keyboardLEDs = 0; input->pendingHead = 0; input->pendingCount = 0; input->mouseMode = LGMP_INPUT_MOUSE_NONE; @@ -551,11 +570,25 @@ static bool validInputStatus(const KVMFRInputStatus * status) KVMFR_INPUT_CAP_MOUSE_RELATIVE | KVMFR_INPUT_CAP_MOUSE_ABSOLUTE | KVMFR_INPUT_CAP_KEYBOARD; - static const uint32_t flags = + static const uint32_t commonFlags = KVMFR_INPUT_STATUS_AVAILABLE | KVMFR_INPUT_STATUS_HAS_OWNER; + static const uint8_t keyboardLEDs = + KVMFR_INPUT_KEYBOARD_LED_NUM_LOCK | + KVMFR_INPUT_KEYBOARD_LED_CAPS_LOCK | + KVMFR_INPUT_KEYBOARD_LED_SCROLL_LOCK | + KVMFR_INPUT_KEYBOARD_LED_COMPOSE | + KVMFR_INPUT_KEYBOARD_LED_KANA; - if (status->version != KVMFR_INPUT_VERSION || + const bool supportedVersion = status->version > 0 && + status->version <= KVMFR_INPUT_VERSION; + const bool keyboardLEDsVersion = + status->version >= KVMFR_INPUT_KEYBOARD_LEDS_VERSION; + const uint32_t flags = commonFlags | + (keyboardLEDsVersion ? + KVMFR_INPUT_STATUS_KEYBOARD_LEDS_VALID : 0); + + if (!supportedVersion || status->capabilities & ~capabilities || status->flags & ~flags || !status->generation || @@ -567,6 +600,12 @@ static bool validInputStatus(const KVMFRInputStatus * status) !status->streamGeneration) return false; + if (status->keyboardLEDs & ~keyboardLEDs) + return false; + for (size_t i = 0; i < sizeof(status->statusReserved); ++i) + if (status->statusReserved[i]) + return false; + for (size_t i = 0; i < sizeof(status->streamReserved) / sizeof(status->streamReserved[0]); ++i) if (status->streamReserved[i]) @@ -617,8 +656,14 @@ static bool validInputStatus(const KVMFRInputStatus * status) (status->flags & KVMFR_INPUT_STATUS_AVAILABLE) != 0; const bool hasOwner = (status->flags & KVMFR_INPUT_STATUS_HAS_OWNER) != 0; + const bool keyboardLEDsValid = keyboardLEDsVersion && + (status->flags & KVMFR_INPUT_STATUS_KEYBOARD_LEDS_VALID) != 0; if (!available && (status->capabilities || hasOwner)) return false; + if (!keyboardLEDsValid && status->keyboardLEDs) + return false; + if (!available && keyboardLEDsValid) + return false; if (available && (status->capabilities & (KVMFR_INPUT_CAP_MOUSE_RELATIVE | KVMFR_INPUT_CAP_KEYBOARD)) != @@ -714,10 +759,12 @@ static bool reconcileInputStream(LGMPInput * input, static void applyInputStatus(LGMPInput * input, const KVMFRInputStatus * status, uint32_t serial, bool * wake) { - const bool wasValid = input->statusValid; - const bool wasAvailable = input->available; - const uint32_t oldCapabilities = input->capabilities; - const uint32_t oldGeneration = input->endpointGeneration; + const bool wasValid = input->statusValid; + const bool wasAvailable = input->available; + const uint32_t oldCapabilities = input->capabilities; + const uint32_t oldGeneration = input->endpointGeneration; + const bool oldKeyboardLEDsValid = input->keyboardLEDsValid; + const uint8_t oldKeyboardLEDs = input->keyboardLEDs; const bool targetAvailable = (status->flags & KVMFR_INPUT_STATUS_AVAILABLE) != 0; const bool endpointChanged = wasValid && @@ -730,6 +777,9 @@ static void applyInputStatus(LGMPInput * input, input->endpointGeneration = status->generation; input->statusOwnerClientID = status->ownerClientID; input->statusOwnerGeneration = status->ownerGeneration; + input->keyboardLEDsValid = + (status->flags & KVMFR_INPUT_STATUS_KEYBOARD_LEDS_VALID) != 0; + input->keyboardLEDs = status->keyboardLEDs; const bool streamChanged = reconcileInputStream(input, status); const bool available = targetAvailable && input->streamEndpointBound; @@ -773,7 +823,9 @@ static void applyInputStatus(LGMPInput * input, if (!wasValid || wasAvailable != available || oldCapabilities != status->capabilities || - oldGeneration != status->generation) + oldGeneration != status->generation || + oldKeyboardLEDsValid != input->keyboardLEDsValid || + oldKeyboardLEDs != input->keyboardLEDs) input->notifyStatus = true; } @@ -1112,6 +1164,8 @@ bool lgmpInput_connect(LGMPInput * input, uint32_t clientID) input->claimed = false; input->available = false; input->ownerBlocked = false; + input->keyboardLEDsValid = false; + input->keyboardLEDs = 0; input->pendingHead = 0; input->pendingCount = 0; input->clientID = clientID; @@ -1170,11 +1224,14 @@ void lgmpInput_disconnect(LGMPInput * input) input->pendingCount = 0; input->connected = false; input->claimed = false; - if (input->available || input->endpointGeneration) + if (input->available || input->endpointGeneration || + input->keyboardLEDsValid) input->notifyStatus = true; - input->available = false; - input->ownerBlocked = false; - input->capabilities = 0; + input->available = false; + input->ownerBlocked = false; + input->keyboardLEDsValid = false; + input->keyboardLEDs = 0; + input->capabilities = 0; input->streamEndpointBound = false; memset(&input->streamEndpoint, 0, sizeof(input->streamEndpoint)); @@ -1196,6 +1253,8 @@ void lgmpInput_disconnect(LGMPInput * input) detachInputStream(input); input->available = false; input->ownerBlocked = false; + input->keyboardLEDsValid = false; + input->keyboardLEDs = 0; input->statusValid = false; input->ownerConfirmed = false; input->notifyStatus = false; diff --git a/client/transports/SPICE/input.c b/client/transports/SPICE/input.c index f1bea3bc..15196137 100644 --- a/client/transports/SPICE/input.c +++ b/client/transports/SPICE/input.c @@ -34,6 +34,8 @@ struct SpiceInput LG_Lock statusDispatch; bool available; + bool keyboardLEDsValid; + uint8_t keyboardLEDs; uint32_t statusGeneration; LG_InputStatusFn statusCallback; void * statusOpaque; @@ -64,8 +66,10 @@ static void spiceSetStatusListener(void * opaque, input->statusOpaque = callbackOpaque; const LG_InputStatus status = { - .available = input->available, - .generation = input->statusGeneration, + .available = input->available, + .generation = input->statusGeneration, + .keyboardLEDsValid = input->available && input->keyboardLEDsValid, + .keyboardLEDs = input->keyboardLEDs, }; LG_UNLOCK(input->stateLock); @@ -193,19 +197,58 @@ void spiceInput_setAvailable(SpiceInput * input, bool available) LG_LOCK(input->statusDispatch); LG_LOCK(input->stateLock); + const bool oldLEDsValid = + input->available && input->keyboardLEDsValid; const bool changed = input->available != available; if (changed) { input->available = available; input->statusGeneration = nextGeneration(input->statusGeneration); } + if (!available) + input->keyboardLEDsValid = false; + + const bool statusChanged = changed || oldLEDsValid != + (input->available && input->keyboardLEDsValid); const LG_InputStatusFn callback = input->statusCallback; void * callbackOpaque = input->statusOpaque; const LG_InputStatus status = { - .available = input->available, - .generation = input->statusGeneration, + .available = input->available, + .generation = input->statusGeneration, + .keyboardLEDsValid = input->available && input->keyboardLEDsValid, + .keyboardLEDs = input->keyboardLEDs, + }; + LG_UNLOCK(input->stateLock); + + if (statusChanged && callback) + callback(callbackOpaque, &status); + LG_UNLOCK(input->statusDispatch); +} + +void spiceInput_setKeyboardLEDs(SpiceInput * input, uint32_t modifiers) +{ + const uint8_t keyboardLEDs = + (modifiers & 2 ? LG_KEYBOARD_LED_NUM_LOCK : 0) | + (modifiers & 4 ? LG_KEYBOARD_LED_CAPS_LOCK : 0) | + (modifiers & 1 ? LG_KEYBOARD_LED_SCROLL_LOCK : 0); + + LG_LOCK(input->statusDispatch); + LG_LOCK(input->stateLock); + const bool changed = !input->keyboardLEDsValid || + input->keyboardLEDs != keyboardLEDs; + input->keyboardLEDsValid = true; + input->keyboardLEDs = keyboardLEDs; + + const LG_InputStatusFn callback = input->statusCallback; + void * callbackOpaque = input->statusOpaque; + const LG_InputStatus status = + { + .available = input->available, + .generation = input->statusGeneration, + .keyboardLEDsValid = input->available, + .keyboardLEDs = input->keyboardLEDs, }; LG_UNLOCK(input->stateLock); diff --git a/client/transports/SPICE/input.h b/client/transports/SPICE/input.h index 446693aa..877f84d6 100644 --- a/client/transports/SPICE/input.h +++ b/client/transports/SPICE/input.h @@ -30,5 +30,6 @@ void spiceInput_free(SpiceInput ** input); const LG_InputOps * spiceInput_getOps(void); void spiceInput_setAvailable(SpiceInput * input, bool available); +void spiceInput_setKeyboardLEDs(SpiceInput * input, uint32_t modifiers); #endif diff --git a/client/transports/SPICE/session.c b/client/transports/SPICE/session.c index 68c366e4..99162194 100644 --- a/client/transports/SPICE/session.c +++ b/client/transports/SPICE/session.c @@ -83,6 +83,13 @@ static void ready(void) lgSignalEvent(transport->connectEvent); } +static void keyModifiers(uint32_t modifiers) +{ + LG_Transport * transport = callbackTarget(); + if (transport && transport->input) + spiceInput_setKeyboardLEDs(transport->input, modifiers); +} + static void surfaceCreate(unsigned int surfaceId, PSSurfaceFormat format, unsigned int width, unsigned int height) { @@ -258,8 +265,9 @@ int spiceSession_thread(void * opaque) .ready = ready, .inputs = { - .enable = transport->inputEnabled, - .autoConnect = true, + .enable = transport->inputEnabled, + .autoConnect = true, + .keyModifiers = keyModifiers, }, .clipboard = { diff --git a/common/include/common/KVMFRInput.h b/common/include/common/KVMFRInput.h index 321c8691..0947a4c9 100644 --- a/common/include/common/KVMFRInput.h +++ b/common/include/common/KVMFRInput.h @@ -28,7 +28,8 @@ #include #include -#define KVMFR_INPUT_VERSION 2 +#define KVMFR_INPUT_VERSION 3 +#define KVMFR_INPUT_KEYBOARD_LEDS_VERSION 3 #define KVMFR_INPUT_STREAM_VERSION 1 #define KVMFR_INPUT_STREAM_ENDPOINT_COUNT 8 #define KVMFR_INPUT_STREAM_SLOT_COUNT 128 @@ -122,8 +123,20 @@ typedef uint32_t KVMFRInputCapabilityFlags; enum { - KVMFR_INPUT_STATUS_AVAILABLE = 0x1, - KVMFR_INPUT_STATUS_HAS_OWNER = 0x2, + KVMFR_INPUT_KEYBOARD_LED_NUM_LOCK = 0x1, + KVMFR_INPUT_KEYBOARD_LED_CAPS_LOCK = 0x2, + KVMFR_INPUT_KEYBOARD_LED_SCROLL_LOCK = 0x4, + KVMFR_INPUT_KEYBOARD_LED_COMPOSE = 0x8, + KVMFR_INPUT_KEYBOARD_LED_KANA = 0x10, +}; + +typedef uint8_t KVMFRInputKeyboardLEDFlags; + +enum +{ + KVMFR_INPUT_STATUS_AVAILABLE = 0x1, + KVMFR_INPUT_STATUS_HAS_OWNER = 0x2, + KVMFR_INPUT_STATUS_KEYBOARD_LEDS_VALID = 0x4, }; typedef uint32_t KVMFRInputStatusFlags; @@ -169,7 +182,10 @@ typedef struct KVMFRInputStatus uint32_t streamVersion; uint32_t streamEndpointCount; uint32_t streamGeneration; - uint32_t streamReserved[5]; + // USB HID keyboard LED bits; valid only when the status flag is set. + uint8_t keyboardLEDs; + uint8_t statusReserved[3]; + uint32_t streamReserved[4]; KVMFRInputStreamEndpoint streamEndpoint[ KVMFR_INPUT_STREAM_ENDPOINT_COUNT]; } @@ -192,6 +208,8 @@ static_assert(sizeof(KVMFRInputStreamEndpoint) == 48, "KVMFR input stream endpoint layout changed"); static_assert(offsetof(KVMFRInputStatus, streamVersion) == 32, "KVMFR input status stream layout changed"); +static_assert(offsetof(KVMFRInputStatus, keyboardLEDs) == 44, + "KVMFR input status keyboard LED layout changed"); static_assert(offsetof(KVMFRInputStatus, streamEndpoint) == 64, "KVMFR input stream discovery layout changed"); static_assert(sizeof(KVMFRInputStatus) == 448, @@ -215,6 +233,8 @@ _Static_assert(sizeof(KVMFRInputStreamEndpoint) == 48, "KVMFR input stream endpoint layout changed"); _Static_assert(offsetof(KVMFRInputStatus, streamVersion) == 32, "KVMFR input status stream layout changed"); +_Static_assert(offsetof(KVMFRInputStatus, keyboardLEDs) == 44, + "KVMFR input status keyboard LED layout changed"); _Static_assert(offsetof(KVMFRInputStatus, streamEndpoint) == 64, "KVMFR input stream discovery layout changed"); _Static_assert(sizeof(KVMFRInputStatus) == 448, diff --git a/idd/LGCommon/InputPipeProtocol.h b/idd/LGCommon/InputPipeProtocol.h index 4b76f1ae..0aba2bdd 100644 --- a/idd/LGCommon/InputPipeProtocol.h +++ b/idd/LGCommon/InputPipeProtocol.h @@ -29,7 +29,7 @@ static constexpr wchar_t LG_INPUT_PIPE_NAME[] = L"\\\\.\\pipe\\LookingGlassIDDInput"; static constexpr uint32_t LG_INPUT_PIPE_MAGIC = 0x5049474c; -static constexpr uint16_t LG_INPUT_PIPE_VERSION = 3; +static constexpr uint16_t LG_INPUT_PIPE_VERSION = 4; static constexpr size_t LG_INPUT_PIPE_MAX_PAYLOAD_SIZE = 64; static constexpr uint16_t LG_INPUT_MOUSE_ABSOLUTE_MAX = KVMFR_INPUT_MOUSE_ABSOLUTE_MAX; @@ -67,9 +67,12 @@ enum LGInputKeyboardModifier : uint8_t enum LGInputPipeMessageType : uint16_t { + // LGIdd to LGInput. LG_INPUT_PIPE_MESSAGE_MOUSE_ABSOLUTE = 1, LG_INPUT_PIPE_MESSAGE_MOUSE_RELATIVE = 2, LG_INPUT_PIPE_MESSAGE_KEYBOARD = 3, + // LGInput to LGIdd. + LG_INPUT_PIPE_MESSAGE_KEYBOARD_LEDS = 4, }; #pragma pack(push, 1) @@ -88,11 +91,18 @@ using LGInputPipeMouseRelative = KVMFRInputMouseRelative; using LGInputPipeMouseAbsolute = KVMFRInputMouseAbsolute; using LGInputPipeKeyboard = KVMFRInputKeyboard; +struct LGInputPipeKeyboardLEDs +{ + KVMFRInputKeyboardLEDFlags leds; +}; + static_assert(sizeof(LGInputPipeMouseRelative) == 16, "LGInputPipeMouseRelative wire layout changed"); static_assert(sizeof(LGInputPipeMouseAbsolute) == 16, "LGInputPipeMouseAbsolute wire layout changed"); static_assert(sizeof(LGInputPipeKeyboard) == 16, "LGInputPipeKeyboard wire layout changed"); +static_assert(sizeof(LGInputPipeKeyboardLEDs) == 1, + "LGInputPipeKeyboardLEDs wire layout changed"); static_assert(sizeof(LGInputPipeMessage) == 84, "LGInputPipeMessage wire layout changed"); diff --git a/idd/LGIdd/input/IInputSink.h b/idd/LGIdd/input/IInputSink.h index 31649a97..19f8ce6e 100644 --- a/idd/LGIdd/input/IInputSink.h +++ b/idd/LGIdd/input/IInputSink.h @@ -29,6 +29,8 @@ public: // Odd states are available; a state change invalidates in-flight input. virtual uint64_t GetState() const = 0; + // Returns false until the guest endpoint has reported its HID LED state. + virtual bool GetKeyboardLEDs(uint8_t& leds) const = 0; virtual bool SendMouseRelative(int32_t deltaX, int32_t deltaY, int32_t wheel, uint32_t buttons) = 0; virtual bool SendMouseAbsolute(uint16_t x, uint16_t y, diff --git a/idd/LGIdd/ipc/CInputPipeServer.cpp b/idd/LGIdd/ipc/CInputPipeServer.cpp index d2eb1f7e..0bf368f6 100644 --- a/idd/LGIdd/ipc/CInputPipeServer.cpp +++ b/idd/LGIdd/ipc/CInputPipeServer.cpp @@ -38,6 +38,8 @@ bool CInputPipeServer::Init() DeInit(); Atomic::Store(m_state, 0, std::memory_order_release); + Atomic::Store(m_keyboardLEDs, 0, std::memory_order_release); + Atomic::Store(m_keyboardLEDsValid, false, std::memory_order_release); m_performanceFrequency.QuadPart = 0; if (!QueryPerformanceFrequency(&m_performanceFrequency)) m_performanceFrequency.QuadPart = 0; @@ -551,6 +553,7 @@ void CInputPipeServer::Invalidate(uint64_t state, bool requireMatch) CSRWExclusiveLock queueLock(m_queueLock); m_queueHead = 0; m_queueCount = 0; + Atomic::Store(m_keyboardLEDsValid, false, std::memory_order_release); } DWORD WINAPI CInputPipeServer::ThreadProc(void * context) @@ -597,6 +600,8 @@ void CInputPipeServer::OnPipeConnected() ++state; ++state; m_sequence = 0; + m_feedbackSequence = 0; + Atomic::Store(m_keyboardLEDsValid, false, std::memory_order_release); m_queueHead = 0; m_queueCount = 0; @@ -623,8 +628,40 @@ bool CInputPipeServer::OnPipeMessage( const void * message, size_t size) { - UNREFERENCED_PARAMETER(message); - UNREFERENCED_PARAMETER(size); - DEBUG_WARN("LGInput sent an unexpected message"); - return false; + if (size != sizeof(LGInputPipeMessage)) + { + DEBUG_WARN("Received a malformed LGInput feedback message"); + return false; + } + + const LGInputPipeMessage& frame = + *static_cast(message); + if (frame.magic != LG_INPUT_PIPE_MAGIC || + frame.version != LG_INPUT_PIPE_VERSION || + frame.type != LG_INPUT_PIPE_MESSAGE_KEYBOARD_LEDS || + frame.payloadSize != sizeof(LGInputPipeKeyboardLEDs) || + !frame.sequence || frame.sequence != m_feedbackSequence + 1) + { + DEBUG_WARN("Received a malformed LGInput feedback message"); + return false; + } + + LGInputPipeKeyboardLEDs feedback = {}; + memcpy(&feedback, frame.payload, sizeof(feedback)); + const uint8_t mask = + KVMFR_INPUT_KEYBOARD_LED_NUM_LOCK | + KVMFR_INPUT_KEYBOARD_LED_CAPS_LOCK | + KVMFR_INPUT_KEYBOARD_LED_SCROLL_LOCK | + KVMFR_INPUT_KEYBOARD_LED_COMPOSE | + KVMFR_INPUT_KEYBOARD_LED_KANA; + if (feedback.leds & ~mask) + { + DEBUG_WARN("Received invalid LGInput keyboard LEDs"); + return false; + } + + m_feedbackSequence = frame.sequence; + Atomic::Store(m_keyboardLEDs, feedback.leds, std::memory_order_release); + Atomic::Store(m_keyboardLEDsValid, true, std::memory_order_release); + return true; } diff --git a/idd/LGIdd/ipc/CInputPipeServer.h b/idd/LGIdd/ipc/CInputPipeServer.h index c949f764..777498f1 100644 --- a/idd/LGIdd/ipc/CInputPipeServer.h +++ b/idd/LGIdd/ipc/CInputPipeServer.h @@ -66,6 +66,10 @@ private: size_t m_queueHead = 0; size_t m_queueCount = 0; uint64_t m_sequence = 0; + uint64_t m_feedbackSequence = 0; + + std::atomic m_keyboardLEDs { 0 }; + std::atomic m_keyboardLEDsValid { false }; MouseMode m_mouseMode = MouseMode::NONE; uint16_t m_absoluteX = 0; @@ -118,6 +122,14 @@ public: return Atomic::Load(m_state, std::memory_order_acquire); } + bool GetKeyboardLEDs(uint8_t& leds) const override + { + if (!Atomic::Load(m_keyboardLEDsValid, std::memory_order_acquire)) + return false; + leds = Atomic::Load(m_keyboardLEDs, std::memory_order_acquire); + return true; + } + bool SendMouseRelative( _In_ int32_t deltaX, _In_ int32_t deltaY, diff --git a/idd/LGIdd/transport/CInputHub.cpp b/idd/LGIdd/transport/CInputHub.cpp index 627f93d0..caffd930 100644 --- a/idd/LGIdd/transport/CInputHub.cpp +++ b/idd/LGIdd/transport/CInputHub.cpp @@ -551,6 +551,8 @@ InputTargetState CInputHub::GetState(const SourceKey& source) return result; if (!BindingValid(source)) return result; + result.keyboardLEDsValid = m_sink->GetKeyboardLEDs( + result.keyboardLEDs); if (!m_owner.backend) result.available = true; else if (source.client && source.generation && m_owner == source) diff --git a/idd/LGIdd/transport/IInputSource.h b/idd/LGIdd/transport/IInputSource.h index 4ec6fbe3..c25d5fdc 100644 --- a/idd/LGIdd/transport/IInputSource.h +++ b/idd/LGIdd/transport/IInputSource.h @@ -34,6 +34,8 @@ struct InputTargetState InputSourceId owner; bool available = false; bool owned = false; + bool keyboardLEDsValid = false; + uint8_t keyboardLEDs = 0; }; enum class InputResult diff --git a/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp index 256cffb6..ce106ed9 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp @@ -330,11 +330,13 @@ void CLGMPInputTransport::UpdateTargetState( const InputTargetState& state) { CSRWExclusiveLock lock(m_statusLock); - if (state.state == m_targetState.state && - state.available == m_targetState.available && - state.owned == m_targetState.owned && - state.owner.client == m_targetState.owner.client && - state.owner.generation == m_targetState.owner.generation) + if (state.state == m_targetState.state && + state.available == m_targetState.available && + state.owned == m_targetState.owned && + state.keyboardLEDsValid == m_targetState.keyboardLEDsValid && + state.keyboardLEDs == m_targetState.keyboardLEDs && + state.owner.client == m_targetState.owner.client && + state.owner.generation == m_targetState.owner.generation) return; if (state.state != m_targetState.state) @@ -389,6 +391,11 @@ bool CLGMPInputTransport::PublishStatus() status.ownerClientID = m_targetState.owner.client; status.ownerGeneration = m_targetState.owner.generation; } + if (available && m_targetState.keyboardLEDsValid) + { + status.flags |= KVMFR_INPUT_STATUS_KEYBOARD_LEDS_VALID; + status.keyboardLEDs = m_targetState.keyboardLEDs; + } status.generation = m_endpointGeneration; status.lease = static_cast(OWNER_LEASE_MS); status.maxButtons = KVMFR_INPUT_MOUSE_BUTTON_COUNT; diff --git a/idd/LGInput/CHIDDevice.cpp b/idd/LGInput/CHIDDevice.cpp index 5c4cbd89..5c1c7140 100644 --- a/idd/LGInput/CHIDDevice.cpp +++ b/idd/LGInput/CHIDDevice.cpp @@ -165,7 +165,10 @@ static NTSTATUS SetOutputReport( return STATUS_INVALID_PARAMETER; WDFDEVICE device = WdfIoQueueGetDevice(queue); - HIDGetDeviceContext(device)->keyboardLeds = report->leds; + HIDDeviceContext * context = HIDGetDeviceContext(device); + context->keyboardLeds = report->leds; + if (context->inputPipe) + context->inputPipe->UpdateKeyboardLEDs(report->leds); WdfRequestSetInformation(request, sizeof(*report)); return STATUS_SUCCESS; } diff --git a/idd/LGInput/ipc/CInputPipeClient.cpp b/idd/LGInput/ipc/CInputPipeClient.cpp index 038078d0..7e01d647 100644 --- a/idd/LGInput/ipc/CInputPipeClient.cpp +++ b/idd/LGInput/ipc/CInputPipeClient.cpp @@ -38,7 +38,15 @@ static constexpr uint16_t HID_CONSUMER_USAGE_VOLUME_DOWN = 0xea; bool CInputPipeClient::Start() { + { + CSRWExclusiveLock lock(m_feedbackLock); + m_feedbackReady = false; + } m_endpoint.Stop(); + { + CSRWExclusiveLock lock(m_feedbackLock); + m_feedbackSequence = 0; + } m_lastSequence = 0; m_statReceived = 0; m_statMalformed = 0; @@ -55,6 +63,10 @@ bool CInputPipeClient::Start() void CInputPipeClient::Stop() { const bool wasRunning = m_endpoint.IsRunning(); + { + CSRWExclusiveLock lock(m_feedbackLock); + m_feedbackReady = false; + } m_endpoint.Stop(); m_lastSequence = 0; if (wasRunning) @@ -67,11 +79,22 @@ void CInputPipeClient::Stop() void CInputPipeClient::OnPipeConnected() { m_lastSequence = 0; + { + CSRWExclusiveLock lock(m_feedbackLock); + m_feedbackSequence = 0; + m_feedbackReady = SendKeyboardLEDsLocked(); + if (!m_feedbackReady) + DEBUG_WARN("Failed to report LGInput keyboard LEDs"); + } DEBUG_INFO("Connected to the LGIdd input transport"); } void CInputPipeClient::OnPipeDisconnected() { + { + CSRWExclusiveLock lock(m_feedbackLock); + m_feedbackReady = false; + } m_lastSequence = 0; LogStatistics(true); CHIDDevice::ResetReports(); @@ -319,6 +342,46 @@ bool CInputPipeClient::SubmitReport( return true; } +void CInputPipeClient::UpdateKeyboardLEDs(uint8_t leds) +{ + const uint8_t mask = + KVMFR_INPUT_KEYBOARD_LED_NUM_LOCK | + KVMFR_INPUT_KEYBOARD_LED_CAPS_LOCK | + KVMFR_INPUT_KEYBOARD_LED_SCROLL_LOCK | + KVMFR_INPUT_KEYBOARD_LED_COMPOSE | + KVMFR_INPUT_KEYBOARD_LED_KANA; + leds &= mask; + + CSRWExclusiveLock lock(m_feedbackLock); + if (m_keyboardLEDs == leds) + return; + + m_keyboardLEDs = leds; + if (m_feedbackReady && !SendKeyboardLEDsLocked()) + { + m_feedbackReady = false; + DEBUG_WARN("Failed to report LGInput keyboard LEDs"); + } +} + +bool CInputPipeClient::SendKeyboardLEDsLocked() +{ + LGInputPipeMessage message = {}; + message.magic = LG_INPUT_PIPE_MAGIC; + message.version = LG_INPUT_PIPE_VERSION; + message.type = LG_INPUT_PIPE_MESSAGE_KEYBOARD_LEDS; + message.payloadSize = sizeof(LGInputPipeKeyboardLEDs); + message.sequence = ++m_feedbackSequence; + + const LGInputPipeKeyboardLEDs feedback = { m_keyboardLEDs }; + memcpy(message.payload, &feedback, sizeof(feedback)); + if (m_endpoint.Send(&message, sizeof(message))) + return true; + + --m_feedbackSequence; + return false; +} + void CInputPipeClient::LogStatistics(bool force) { const ULONGLONG now = GetTickCount64(); diff --git a/idd/LGInput/ipc/CInputPipeClient.h b/idd/LGInput/ipc/CInputPipeClient.h index 41c0beac..297cc0fc 100644 --- a/idd/LGInput/ipc/CInputPipeClient.h +++ b/idd/LGInput/ipc/CInputPipeClient.h @@ -21,6 +21,7 @@ #pragma once #include "CPipeEndpoint.h" +#include "CSRWLock.h" #include #include @@ -32,6 +33,7 @@ public: bool Start(); void Stop(); + void UpdateKeyboardLEDs(uint8_t leds); private: static constexpr ULONGLONG STATISTICS_INTERVAL_MS = 5000; @@ -43,9 +45,14 @@ private: bool HandleMouseAbsolute(const void * payload, size_t size); bool HandleKeyboard(const void * payload, size_t size); bool SubmitReport(const void * report, size_t size); + bool SendKeyboardLEDsLocked(); void LogStatistics(bool force); CPipeEndpoint m_endpoint; + CSRWLock m_feedbackLock; + bool m_feedbackReady = false; + uint64_t m_feedbackSequence = 0; + uint8_t m_keyboardLEDs = 0; uint64_t m_lastSequence = 0; uint64_t m_statReceived = 0; uint64_t m_statMalformed = 0; diff --git a/repos/PureSpice b/repos/PureSpice index 13695607..4e8583a4 160000 --- a/repos/PureSpice +++ b/repos/PureSpice @@ -1 +1 @@ -Subproject commit 13695607dec73aded4f7a17f41911bb30b521f80 +Subproject commit 4e8583a43731bcabc65b4457976601fa3ebb56ca