[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.
This commit is contained in:
Geoffrey McRae
2026-08-21 05:06:59 +10:00
parent 1a5bdc14aa
commit 71733433ed
20 changed files with 425 additions and 44 deletions

View File

@@ -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,

View File

@@ -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<const LGInputPipeMessage *>(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;
}

View File

@@ -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<uint8_t> m_keyboardLEDs { 0 };
std::atomic<bool> 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,

View File

@@ -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)

View File

@@ -34,6 +34,8 @@ struct InputTargetState
InputSourceId owner;
bool available = false;
bool owned = false;
bool keyboardLEDsValid = false;
uint8_t keyboardLEDs = 0;
};
enum class InputResult

View File

@@ -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<uint32_t>(OWNER_LEASE_MS);
status.maxButtons = KVMFR_INPUT_MOUSE_BUTTON_COUNT;