From 71733433ed8861c7aa9881605c380a496bd014d3 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 21 Aug 2026 05:06:59 +1000 Subject: [PATCH] [client/idd] input: synchronize keyboard LEDs Report guest HID keyboard LED state from LGInput to LGIdd and publish it with the LGMP input endpoint status. Forward SPICE modifier updates through the same provider state so evdev can synchronize physical keyboard LEDs across transport changes and reconnects. Bump the versioned pipe and KVMFR input status contracts for the new feedback state. Accept earlier LGMP input status versions without LED feedback so the client retains input while the guest driver is being upgraded. --- client/include/input.h | 8 ++ client/include/interface/input.h | 12 +++ client/src/input.c | 95 +++++++++++++++++-- client/tests/spice_test.c | 6 ++ client/transports/LGMP/input.c | 87 ++++++++++++++--- client/transports/SPICE/input.c | 51 +++++++++- client/transports/SPICE/input.h | 1 + client/transports/SPICE/session.c | 12 ++- common/include/common/KVMFRInput.h | 28 +++++- idd/LGCommon/InputPipeProtocol.h | 12 ++- idd/LGIdd/input/IInputSink.h | 2 + idd/LGIdd/ipc/CInputPipeServer.cpp | 45 ++++++++- idd/LGIdd/ipc/CInputPipeServer.h | 12 +++ idd/LGIdd/transport/CInputHub.cpp | 2 + idd/LGIdd/transport/IInputSource.h | 2 + .../transport/lgmp/CLGMPInputTransport.cpp | 17 +++- idd/LGInput/CHIDDevice.cpp | 5 +- idd/LGInput/ipc/CInputPipeClient.cpp | 63 ++++++++++++ idd/LGInput/ipc/CInputPipeClient.h | 7 ++ repos/PureSpice | 2 +- 20 files changed, 425 insertions(+), 44 deletions(-) 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