diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index cdf953a8..38165c86 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -65,6 +65,7 @@
+
@@ -74,6 +75,7 @@
+
@@ -107,6 +109,7 @@
+
@@ -114,6 +117,7 @@
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index dd16de15..6eff672f 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -8,6 +8,9 @@
{6A9D97A5-1A6C-4A95-A2D0-A7C2D6BE459D}
+
+ {B37D983B-12F1-41AE-8375-1B9B90A64F87}
+
{D2C16E51-6087-4E08-8DA4-5AD1A60EF58E}
@@ -61,6 +64,9 @@
Driver
+
+ Input
+
IPC
@@ -160,6 +166,9 @@
Transport
+
+ Transport
+
Transport
@@ -181,6 +190,9 @@
Transport\LGMP
+
+ Transport\LGMP
+
Transport\LGMP
@@ -291,6 +303,9 @@
Transport\LGMP
+
+ Transport\LGMP
+
Transport\LGMP
diff --git a/idd/LGIdd/display/CDeviceContext.cpp b/idd/LGIdd/display/CDeviceContext.cpp
index 9152fab6..49abb765 100644
--- a/idd/LGIdd/display/CDeviceContext.cpp
+++ b/idd/LGIdd/display/CDeviceContext.cpp
@@ -21,8 +21,10 @@
#include "display/CDeviceContext.h"
#include "display/IddCxCompat.h"
+#include "ipc/CInputPipeServer.h"
#include "ipc/CPipeServer.h"
#include "transport/IFrameTransport.h"
+#include "transport/IInputTransport.h"
#include "transport/TransportFactory.h"
#include "CDebug.h"
@@ -55,6 +57,9 @@ CDeviceContext::~CDeviceContext()
WdfTimerStop(m_transportTimer, TRUE);
m_transportTimer = nullptr;
}
+
+ if (m_transport && m_transport->Input())
+ m_transport->Input()->Stop();
}
void CDeviceContext::QueryIddCxCapabilities()
@@ -462,41 +467,49 @@ bool CDeviceContext::SetupTransport(size_t alignSize)
{
// Frame buffers cannot be allocated until the GPU-specific alignment is
// known. The swap-chain path may call this again after setup completed.
- if (m_transport->Frames().GetMaxFrameSize())
- return true;
-
- if (!InitializeTransport() || !m_transport->Setup(alignSize))
- return false;
-
- WDF_TIMER_CONFIG config;
- WDF_TIMER_CONFIG_INIT_PERIODIC(&config,
- [](WDFTIMER timer) -> void
- {
- WDFOBJECT parent = WdfTimerGetParentObject(timer);
- auto wrapper = WdfObjectGet_CDeviceContextWrapper(parent);
- wrapper->context->TransportTimer();
- },
- 10);
- config.AutomaticSerialization = FALSE;
-
- /**
- * Documentation states that Dispatch is not available under UMDF, however
- * using Passive returns a not-supported error and Dispatch works.
- */
- WDF_OBJECT_ATTRIBUTES attribs;
- WDF_OBJECT_ATTRIBUTES_INIT(&attribs);
- attribs.ParentObject = m_wdfDevice;
- attribs.ExecutionLevel = WdfExecutionLevelDispatch;
-
- NTSTATUS status = WdfTimerCreate(
- &config, &attribs, &m_transportTimer);
- if (!NT_SUCCESS(status))
+ if (!m_transport->Frames().GetMaxFrameSize())
{
- DEBUG_ERROR_HR(status, "Timer creation failed");
+ if (!InitializeTransport() || !m_transport->Setup(alignSize))
+ return false;
+
+ WDF_TIMER_CONFIG config;
+ WDF_TIMER_CONFIG_INIT_PERIODIC(&config,
+ [](WDFTIMER timer) -> void
+ {
+ WDFOBJECT parent = WdfTimerGetParentObject(timer);
+ auto wrapper = WdfObjectGet_CDeviceContextWrapper(parent);
+ wrapper->context->TransportTimer();
+ },
+ 10);
+ config.AutomaticSerialization = FALSE;
+
+ /**
+ * Documentation states that Dispatch is not available under UMDF,
+ * however using Passive returns a not-supported error and Dispatch works.
+ */
+ WDF_OBJECT_ATTRIBUTES attribs;
+ WDF_OBJECT_ATTRIBUTES_INIT(&attribs);
+ attribs.ParentObject = m_wdfDevice;
+ attribs.ExecutionLevel = WdfExecutionLevelDispatch;
+
+ NTSTATUS status = WdfTimerCreate(
+ &config, &attribs, &m_transportTimer);
+ if (!NT_SUCCESS(status))
+ {
+ DEBUG_ERROR_HR(status, "Timer creation failed");
+ return false;
+ }
+
+ WdfTimerStart(m_transportTimer, WDF_REL_TIMEOUT_IN_MS(10));
+ }
+
+ IInputTransport * input = m_transport->Input();
+ if (input && !input->Start(g_inputPipeServer))
+ {
+ DEBUG_ERROR("Failed to start input transport");
return false;
}
- WdfTimerStart(m_transportTimer, WDF_REL_TIMEOUT_IN_MS(10));
return true;
}
diff --git a/idd/LGIdd/input/IInputSink.h b/idd/LGIdd/input/IInputSink.h
new file mode 100644
index 00000000..6ccda3ce
--- /dev/null
+++ b/idd/LGIdd/input/IInputSink.h
@@ -0,0 +1,39 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include
+
+class IInputSink
+{
+public:
+ virtual ~IInputSink() = default;
+
+ virtual bool IsAvailable() const = 0;
+ virtual uint64_t GetGeneration() 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,
+ int32_t wheel, uint32_t buttons) = 0;
+ virtual bool SendKeyboard(
+ uint8_t modifiers, const uint8_t * keys) = 0;
+ virtual bool Reset() = 0;
+};
diff --git a/idd/LGIdd/ipc/CInputPipeServer.cpp b/idd/LGIdd/ipc/CInputPipeServer.cpp
index 41651ca9..87cef5a8 100644
--- a/idd/LGIdd/ipc/CInputPipeServer.cpp
+++ b/idd/LGIdd/ipc/CInputPipeServer.cpp
@@ -21,17 +21,34 @@
#include "ipc/CInputPipeServer.h"
#include "CDebug.h"
+#include "CSRWLock.h"
#include "InputPipeProtocol.h"
#include
CInputPipeServer g_inputPipeServer;
+static constexpr int32_t MAX_SPLIT_REPORTS = 4;
+static constexpr int32_t MAX_MOUSE_DELTA =
+ INT16_MAX * MAX_SPLIT_REPORTS;
+static constexpr int32_t MIN_MOUSE_DELTA =
+ INT16_MIN * MAX_SPLIT_REPORTS;
+static constexpr int32_t MAX_MOUSE_WHEEL =
+ INT8_MAX * MAX_SPLIT_REPORTS;
+static constexpr int32_t MIN_MOUSE_WHEEL =
+ LG_INPUT_MOUSE_WHEEL_MIN * MAX_SPLIT_REPORTS;
+
bool CInputPipeServer::Init()
{
- AcquireSRWLockExclusive(&m_sendLock);
- m_sequence = 0;
- ReleaseSRWLockExclusive(&m_sendLock);
+ m_state.store(0, std::memory_order_release);
+ {
+ CSRWExclusiveLock lock(&m_sendLock);
+ m_sequence = 0;
+ m_mouseMode = MouseMode::NONE;
+ m_absoluteValid = false;
+ m_absoluteX = 0;
+ m_absoluteY = 0;
+ }
m_endpoint.SetHandler(this);
return m_endpoint.Start(
@@ -42,53 +59,134 @@ bool CInputPipeServer::Init()
void CInputPipeServer::DeInit()
{
+ Invalidate();
m_endpoint.Stop();
}
bool CInputPipeServer::SendMouseRelative(
- int16_t deltaX,
- int16_t deltaY,
- int8_t wheel,
- uint8_t buttons)
+ int32_t deltaX,
+ int32_t deltaY,
+ int32_t wheel,
+ uint32_t buttons)
{
- if (wheel < LG_INPUT_MOUSE_WHEEL_MIN ||
- (buttons & ~LG_INPUT_MOUSE_BUTTON_MASK))
+ if (deltaX < MIN_MOUSE_DELTA || deltaX > MAX_MOUSE_DELTA ||
+ deltaY < MIN_MOUSE_DELTA || deltaY > MAX_MOUSE_DELTA ||
+ wheel < MIN_MOUSE_WHEEL || wheel > MAX_MOUSE_WHEEL ||
+ (buttons & ~static_cast(LG_INPUT_MOUSE_BUTTON_MASK)))
return false;
- const LGInputPipeMouseRelative payload = {
- buttons,
- deltaX,
- deltaY,
- wheel,
- };
- return SendMessage(
- LG_INPUT_PIPE_MESSAGE_MOUSE_RELATIVE,
- &payload,
- sizeof(payload));
+ CSRWExclusiveLock lock(&m_sendLock);
+ if (!IsAvailable())
+ return false;
+
+ if (m_mouseMode == MouseMode::ABSOLUTE)
+ {
+ const LGInputPipeMouseAbsolute neutral = {
+ 0,
+ m_absoluteX,
+ m_absoluteY,
+ 0,
+ };
+ if (!SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_MOUSE_ABSOLUTE,
+ &neutral,
+ sizeof(neutral)))
+ {
+ Invalidate();
+ return false;
+ }
+ m_mouseMode = MouseMode::NONE;
+ }
+
+ do
+ {
+ const int16_t x = deltaX > INT16_MAX ? INT16_MAX :
+ deltaX < INT16_MIN ? INT16_MIN : static_cast(deltaX);
+ const int16_t y = deltaY > INT16_MAX ? INT16_MAX :
+ deltaY < INT16_MIN ? INT16_MIN : static_cast(deltaY);
+ const int8_t wheelDelta = wheel > INT8_MAX ? INT8_MAX :
+ wheel < LG_INPUT_MOUSE_WHEEL_MIN ? LG_INPUT_MOUSE_WHEEL_MIN :
+ static_cast(wheel);
+
+ const LGInputPipeMouseRelative payload = {
+ static_cast(buttons),
+ x,
+ y,
+ wheelDelta,
+ };
+ if (!SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_MOUSE_RELATIVE, &payload, sizeof(payload)))
+ {
+ Invalidate();
+ return false;
+ }
+
+ m_mouseMode = MouseMode::RELATIVE;
+ deltaX -= x;
+ deltaY -= y;
+ wheel -= wheelDelta;
+ }
+ while (deltaX || deltaY || wheel);
+
+ return true;
}
bool CInputPipeServer::SendMouseAbsolute(
uint16_t x,
uint16_t y,
- int8_t wheel,
- uint8_t buttons)
+ int32_t wheel,
+ uint32_t buttons)
{
if (x > LG_INPUT_MOUSE_ABSOLUTE_MAX ||
y > LG_INPUT_MOUSE_ABSOLUTE_MAX ||
- wheel < LG_INPUT_MOUSE_WHEEL_MIN ||
- (buttons & ~LG_INPUT_MOUSE_BUTTON_MASK))
+ wheel < MIN_MOUSE_WHEEL || wheel > MAX_MOUSE_WHEEL ||
+ (buttons & ~static_cast(LG_INPUT_MOUSE_BUTTON_MASK)))
return false;
- const LGInputPipeMouseAbsolute payload = {
- buttons,
- x,
- y,
- wheel,
- };
- return SendMessage(
- LG_INPUT_PIPE_MESSAGE_MOUSE_ABSOLUTE,
- &payload,
- sizeof(payload));
+ CSRWExclusiveLock lock(&m_sendLock);
+ if (!IsAvailable())
+ return false;
+
+ if (m_mouseMode == MouseMode::RELATIVE)
+ {
+ const LGInputPipeMouseRelative neutral = {};
+ if (!SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_MOUSE_RELATIVE,
+ &neutral,
+ sizeof(neutral)))
+ {
+ Invalidate();
+ return false;
+ }
+ m_mouseMode = MouseMode::NONE;
+ }
+
+ do
+ {
+ const int8_t wheelDelta = wheel > INT8_MAX ? INT8_MAX :
+ wheel < LG_INPUT_MOUSE_WHEEL_MIN ? LG_INPUT_MOUSE_WHEEL_MIN :
+ static_cast(wheel);
+ const LGInputPipeMouseAbsolute payload = {
+ static_cast(buttons),
+ x,
+ y,
+ wheelDelta,
+ };
+ if (!SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_MOUSE_ABSOLUTE, &payload, sizeof(payload)))
+ {
+ Invalidate();
+ return false;
+ }
+ m_mouseMode = MouseMode::ABSOLUTE;
+ m_absoluteValid = true;
+ m_absoluteX = x;
+ m_absoluteY = y;
+ wheel -= wheelDelta;
+ }
+ while (wheel);
+
+ return true;
}
bool CInputPipeServer::SendKeyboard(
@@ -107,13 +205,58 @@ bool CInputPipeServer::SendKeyboard(
payload.keys[i] = keys[i];
}
- return SendMessage(
- LG_INPUT_PIPE_MESSAGE_KEYBOARD,
- &payload,
- sizeof(payload));
+ CSRWExclusiveLock lock(&m_sendLock);
+ const bool sent = IsAvailable() &&
+ SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_KEYBOARD,
+ &payload,
+ sizeof(payload));
+ if (!sent)
+ Invalidate();
+ return sent;
}
-bool CInputPipeServer::SendMessage(
+bool CInputPipeServer::Reset()
+{
+ CSRWExclusiveLock lock(&m_sendLock);
+ const bool reset = IsAvailable() &&
+ ResetLocked();
+ if (!reset)
+ Invalidate();
+ return reset;
+}
+
+bool CInputPipeServer::ResetLocked()
+{
+ const LGInputPipeMouseRelative relative = {};
+ const LGInputPipeMouseAbsolute absolute = {
+ 0,
+ m_absoluteX,
+ m_absoluteY,
+ 0,
+ };
+ const LGInputPipeKeyboard keyboard = {};
+
+ if (!SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_MOUSE_RELATIVE,
+ &relative,
+ sizeof(relative)) ||
+ (m_absoluteValid &&
+ !SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_MOUSE_ABSOLUTE,
+ &absolute,
+ sizeof(absolute))) ||
+ !SendMessageLocked(
+ LG_INPUT_PIPE_MESSAGE_KEYBOARD,
+ &keyboard,
+ sizeof(keyboard)))
+ return false;
+
+ m_mouseMode = MouseMode::NONE;
+ return true;
+}
+
+bool CInputPipeServer::SendMessageLocked(
LGInputPipeMessageType type,
const void * payload,
size_t size)
@@ -128,13 +271,40 @@ bool CInputPipeServer::SendMessage(
message.payloadSize = static_cast(size);
memcpy(message.payload, payload, size);
- AcquireSRWLockExclusive(&m_sendLock);
message.sequence = ++m_sequence;
const bool sent = m_endpoint.Send(&message, sizeof(message));
- ReleaseSRWLockExclusive(&m_sendLock);
return sent;
}
+void CInputPipeServer::Invalidate()
+{
+ uint64_t state = m_state.load(std::memory_order_acquire);
+ while ((state & 1) && !m_state.compare_exchange_weak(
+ state, state + 1, std::memory_order_acq_rel))
+ {
+ }
+}
+
+void CInputPipeServer::OnPipeConnected()
+{
+ Invalidate();
+
+ CSRWExclusiveLock lock(&m_sendLock);
+ const bool ready = ResetLocked();
+ if (!ready)
+ {
+ DEBUG_WARN("Failed to neutralize the LGInput endpoint");
+ return;
+ }
+
+ m_state.fetch_add(1, std::memory_order_acq_rel);
+}
+
+void CInputPipeServer::OnPipeDisconnected()
+{
+ Invalidate();
+}
+
bool CInputPipeServer::OnPipeMessage(
const void * message,
size_t size)
diff --git a/idd/LGIdd/ipc/CInputPipeServer.h b/idd/LGIdd/ipc/CInputPipeServer.h
index 053bcda8..416c2756 100644
--- a/idd/LGIdd/ipc/CInputPipeServer.h
+++ b/idd/LGIdd/ipc/CInputPipeServer.h
@@ -22,11 +22,13 @@
#include "CPipeEndpoint.h"
#include "InputPipeProtocol.h"
+#include "input/IInputSink.h"
+#include
#include
#include
-class CInputPipeServer : private IPipeEndpointHandler
+class CInputPipeServer : public IInputSink, private IPipeEndpointHandler
{
public:
~CInputPipeServer() { DeInit(); }
@@ -34,34 +36,61 @@ public:
bool Init();
void DeInit();
- // Mouse buttons use LGInputMouseButton bits. Wheel values are -127..127.
+ bool IsAvailable() const override
+ {
+ return (m_state.load(std::memory_order_acquire) & 1) != 0;
+ }
+ uint64_t GetGeneration() const override
+ {
+ return m_state.load(std::memory_order_acquire);
+ }
+
+ // Mouse buttons use LGInputMouseButton bits. Values outside the pipe report
+ // ranges are split into multiple reports without losing motion.
bool SendMouseRelative(
- _In_ int16_t deltaX,
- _In_ int16_t deltaY,
- _In_ int8_t wheel,
- _In_ uint8_t buttons);
+ _In_ int32_t deltaX,
+ _In_ int32_t deltaY,
+ _In_ int32_t wheel,
+ _In_ uint32_t buttons) override;
// Absolute coordinates are normalized to 0..32767 on each axis.
bool SendMouseAbsolute(
_In_range_(0, LG_INPUT_MOUSE_ABSOLUTE_MAX) uint16_t x,
_In_range_(0, LG_INPUT_MOUSE_ABSOLUTE_MAX) uint16_t y,
- _In_ int8_t wheel,
- _In_ uint8_t buttons);
+ _In_ int32_t wheel,
+ _In_ uint32_t buttons) override;
// Keys are USB HID Keyboard/Keypad usage IDs; zero marks an empty slot.
bool SendKeyboard(
_In_ uint8_t modifiers,
- _In_reads_(LG_INPUT_KEYBOARD_KEY_COUNT) const uint8_t * keys);
+ _In_reads_(LG_INPUT_KEYBOARD_KEY_COUNT) const uint8_t * keys) override;
+ bool Reset() override;
bool IsConnected() const { return m_endpoint.IsConnected(); }
private:
- bool SendMessage(
+ bool SendMessageLocked(
_In_ LGInputPipeMessageType type,
_In_reads_bytes_(size) const void * payload,
_In_ size_t size);
+ bool ResetLocked();
+ void Invalidate();
+ void OnPipeConnected() override;
+ void OnPipeDisconnected() override;
bool OnPipeMessage(const void * message, size_t size) override;
- CPipeEndpoint m_endpoint;
- SRWLOCK m_sendLock = SRWLOCK_INIT;
- uint64_t m_sequence = 0;
+ CPipeEndpoint m_endpoint;
+ // Odd states are available. Each endpoint transition advances the state.
+ std::atomic m_state { 0 };
+ SRWLOCK m_sendLock = SRWLOCK_INIT;
+ uint64_t m_sequence = 0;
+ enum class MouseMode
+ {
+ NONE,
+ RELATIVE,
+ ABSOLUTE,
+ };
+ MouseMode m_mouseMode = MouseMode::NONE;
+ bool m_absoluteValid = false;
+ uint16_t m_absoluteX = 0;
+ uint16_t m_absoluteY = 0;
};
extern CInputPipeServer g_inputPipeServer;
diff --git a/idd/LGIdd/transport/IInputTransport.h b/idd/LGIdd/transport/IInputTransport.h
new file mode 100644
index 00000000..71a420c1
--- /dev/null
+++ b/idd/LGIdd/transport/IInputTransport.h
@@ -0,0 +1,32 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+class IInputSink;
+
+class IInputTransport
+{
+public:
+ virtual ~IInputTransport() = default;
+
+ virtual bool Start(IInputSink& sink) = 0;
+ virtual void Stop() = 0;
+};
diff --git a/idd/LGIdd/transport/ITransport.h b/idd/LGIdd/transport/ITransport.h
index 4cdd567e..b093789c 100644
--- a/idd/LGIdd/transport/ITransport.h
+++ b/idd/LGIdd/transport/ITransport.h
@@ -28,6 +28,7 @@
class IControlTransport;
class IFrameTransport;
+class IInputTransport;
class ITransportEvents
{
@@ -60,4 +61,5 @@ public:
virtual IFrameTransport& Frames() = 0;
virtual IControlTransport& Control() = 0;
+ virtual IInputTransport * Input() { return nullptr; }
};
diff --git a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp
index 8419fe3e..8420b174 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp
+++ b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp
@@ -49,9 +49,10 @@ bool CLGMPHost::Initialize(CIVSHMEM& ivshmem)
memcpy_s(kvmfr.magic, sizeof(kvmfr.magic), KVMFR_MAGIC, sizeof(KVMFR_MAGIC) - 1);
kvmfr.version = KVMFR_VERSION;
kvmfr.features =
- KVMFR_FEATURE_SETCURSORPOS |
- KVMFR_FEATURE_WINDOWSIZE |
- KVMFR_FEATURE_FRAME_SCHEDULE;
+ KVMFR_FEATURE_SETCURSORPOS |
+ KVMFR_FEATURE_WINDOWSIZE |
+ KVMFR_FEATURE_FRAME_SCHEDULE |
+ KVMFR_FEATURE_INPUT;
strncpy_s(kvmfr.hostver, LG_VERSION_STR, sizeof(kvmfr.hostver) - 1);
ss.write(reinterpret_cast(&kvmfr), sizeof(kvmfr));
}
diff --git a/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp
new file mode 100644
index 00000000..43ff60ec
--- /dev/null
+++ b/idd/LGIdd/transport/lgmp/CLGMPInputTransport.cpp
@@ -0,0 +1,518 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "transport/lgmp/CLGMPInputTransport.h"
+
+#include "input/IInputSink.h"
+#include "transport/lgmp/CLGMPHost.h"
+#include "CDebug.h"
+#include "CSRWLock.h"
+
+#include "common/KVMFRInput.h"
+#include "common/LGMPConfig.h"
+
+#include
+#include
+
+#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
+ #define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
+#endif
+
+static const LGMPQueueConfig INPUT_QUEUE_CONFIG =
+{
+ LGMP_Q_INPUT,
+ LGMP_Q_INPUT_LEN,
+ 1000,
+};
+
+static constexpr int32_t MAX_SPLIT_REPORTS = 4;
+static constexpr int32_t MAX_MOUSE_DELTA =
+ INT16_MAX * MAX_SPLIT_REPORTS;
+static constexpr int32_t MIN_MOUSE_DELTA =
+ INT16_MIN * MAX_SPLIT_REPORTS;
+static constexpr int32_t MAX_MOUSE_WHEEL =
+ INT8_MAX * MAX_SPLIT_REPORTS;
+static constexpr int32_t MIN_MOUSE_WHEEL =
+ -INT8_MAX * MAX_SPLIT_REPORTS;
+
+static bool IsZero(const void * data, size_t size)
+{
+ const uint8_t * byte = static_cast(data);
+ for (size_t i = 0; i < size; ++i)
+ if (byte[i])
+ return false;
+ return true;
+}
+
+static bool ArmPollTimer(HANDLE timer, bool active)
+{
+ LARGE_INTEGER due = {};
+ due.QuadPart = active ? -2500 : -10000;
+ return SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE) != FALSE;
+}
+
+CLGMPInputTransport::~CLGMPInputTransport()
+{
+ DeInit();
+}
+
+bool CLGMPInputTransport::Initialize()
+{
+ if (m_queue)
+ return true;
+
+ const LGMP_STATUS status = m_host.CreateQueue(
+ INPUT_QUEUE_CONFIG, &m_queue);
+ if (status != LGMP_OK)
+ {
+ DEBUG_ERROR("lgmpHostQueueCreate Failed (Input): %s",
+ lgmpStatusString(status));
+ return false;
+ }
+
+ return true;
+}
+
+void CLGMPInputTransport::DeInit()
+{
+ Stop();
+ m_queue = nullptr;
+}
+
+bool CLGMPInputTransport::Start(IInputSink& sink)
+{
+ CSRWExclusiveLock lock(&m_lifecycleLock);
+ if (m_thread)
+ {
+ const DWORD state = WaitForSingleObject(m_thread, 0);
+ if (state == WAIT_TIMEOUT)
+ return true;
+ if (state != WAIT_OBJECT_0)
+ {
+ DEBUG_ERROR_HR(GetLastError(),
+ "Failed to inspect LGMP input worker");
+ return false;
+ }
+
+ CloseHandle(m_thread);
+ CloseHandle(m_pollTimer);
+ CloseHandle(m_stopEvent);
+ m_thread = nullptr;
+ m_pollTimer = nullptr;
+ m_stopEvent = nullptr;
+ m_sink = nullptr;
+ }
+
+ if (!m_queue)
+ return false;
+
+ m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ m_pollTimer = CreateWaitableTimerExW(nullptr, nullptr,
+ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
+ if (!m_pollTimer)
+ m_pollTimer = CreateWaitableTimerExW(
+ nullptr, nullptr, 0, TIMER_ALL_ACCESS);
+
+ if (!m_stopEvent || !m_pollTimer)
+ {
+ DEBUG_ERROR_HR(GetLastError(),
+ "Failed to create LGMP input worker resources");
+ if (m_pollTimer)
+ CloseHandle(m_pollTimer);
+ if (m_stopEvent)
+ CloseHandle(m_stopEvent);
+ m_pollTimer = nullptr;
+ m_stopEvent = nullptr;
+ return false;
+ }
+
+ m_sink = &sink;
+ m_sinkGeneration = sink.GetGeneration();
+ m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
+ if (!m_thread)
+ {
+ DEBUG_ERROR_HR(GetLastError(), "Failed to create LGMP input worker");
+ m_sink = nullptr;
+ m_sinkGeneration = 0;
+ CloseHandle(m_pollTimer);
+ CloseHandle(m_stopEvent);
+ m_pollTimer = nullptr;
+ m_stopEvent = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+void CLGMPInputTransport::Stop()
+{
+ HANDLE thread;
+ {
+ CSRWExclusiveLock lock(&m_lifecycleLock);
+ thread = m_thread;
+ if (m_stopEvent)
+ SetEvent(m_stopEvent);
+ }
+
+ if (thread)
+ WaitForSingleObject(thread, INFINITE);
+
+ CSRWExclusiveLock lock(&m_lifecycleLock);
+ if (m_thread)
+ {
+ CloseHandle(m_thread);
+ m_thread = nullptr;
+ }
+ if (m_pollTimer)
+ {
+ CloseHandle(m_pollTimer);
+ m_pollTimer = nullptr;
+ }
+ if (m_stopEvent)
+ {
+ CloseHandle(m_stopEvent);
+ m_stopEvent = nullptr;
+ }
+ m_sink = nullptr;
+ m_ownerClientID = 0;
+ m_ownerGeneration = 0;
+ m_ownerSequence = 0;
+ m_ownerDeadline = 0;
+ m_sinkGeneration = 0;
+}
+
+bool CLGMPInputTransport::IsOwner(
+ uint32_t sourceClientID, uint32_t generation) const
+{
+ return m_ownerClientID == sourceClientID &&
+ m_ownerGeneration == generation;
+}
+
+bool CLGMPInputTransport::Claim(
+ uint32_t sourceClientID, const KVMFRInputMessage& message)
+{
+ if (message.sequence != 1 || !m_sink || !m_sink->IsAvailable())
+ return false;
+
+ const uint64_t sinkGeneration = m_sink->GetGeneration();
+ if (sinkGeneration != m_sinkGeneration || !m_sink->Reset() ||
+ !m_sink->IsAvailable() ||
+ m_sink->GetGeneration() != sinkGeneration)
+ return false;
+
+ m_ownerClientID = sourceClientID;
+ m_ownerGeneration = message.generation;
+ m_ownerSequence = message.sequence;
+ RenewLease();
+ DEBUG_INFO("Input owner %u generation %u acquired",
+ m_ownerClientID, m_ownerGeneration);
+ return true;
+}
+
+void CLGMPInputTransport::RenewLease()
+{
+ m_ownerDeadline = GetTickCount64() + OWNER_LEASE_MS;
+}
+
+void CLGMPInputTransport::ReleaseOwner(
+ bool reset, const char * reason)
+{
+ if (!m_ownerClientID)
+ return;
+
+ const uint32_t clientID = m_ownerClientID;
+ const uint32_t generation = m_ownerGeneration;
+ if (reset && m_sink)
+ m_sink->Reset();
+
+ m_ownerClientID = 0;
+ m_ownerGeneration = 0;
+ m_ownerSequence = 0;
+ m_ownerDeadline = 0;
+ DEBUG_INFO("Input owner %u generation %u released (%s)",
+ clientID, generation, reason);
+}
+
+void CLGMPInputTransport::CheckOwner()
+{
+ if (!m_sink)
+ return;
+
+ const uint64_t generation = m_sink->GetGeneration();
+ if (generation != m_sinkGeneration)
+ {
+ m_sinkGeneration = generation;
+ ReleaseOwner(true, "input endpoint changed");
+ return;
+ }
+
+ if (!m_ownerClientID)
+ return;
+
+ if (!m_sink->IsAvailable())
+ {
+ ReleaseOwner(true, "input unavailable");
+ return;
+ }
+
+ if (GetTickCount64() >= m_ownerDeadline)
+ ReleaseOwner(true, "lease expired");
+}
+
+bool CLGMPInputTransport::ValidatePayload(
+ const KVMFRInputMessage& message) const
+{
+ switch (message.type)
+ {
+ case KVMFR_INPUT_MESSAGE_CLAIM:
+ case KVMFR_INPUT_MESSAGE_RELEASE:
+ case KVMFR_INPUT_MESSAGE_KEEPALIVE:
+ case KVMFR_INPUT_MESSAGE_RESET:
+ return IsZero(&message.payload, sizeof(message.payload));
+
+ case KVMFR_INPUT_MESSAGE_MOUSE_RELATIVE:
+ return message.payload.mouseRelative.deltaX >= MIN_MOUSE_DELTA &&
+ message.payload.mouseRelative.deltaX <= MAX_MOUSE_DELTA &&
+ message.payload.mouseRelative.deltaY >= MIN_MOUSE_DELTA &&
+ message.payload.mouseRelative.deltaY <= MAX_MOUSE_DELTA &&
+ message.payload.mouseRelative.wheel >= MIN_MOUSE_WHEEL &&
+ message.payload.mouseRelative.wheel <= MAX_MOUSE_WHEEL;
+
+ case KVMFR_INPUT_MESSAGE_MOUSE_ABSOLUTE:
+ return message.payload.mouseAbsolute.x <=
+ KVMFR_INPUT_MOUSE_ABSOLUTE_MAX &&
+ message.payload.mouseAbsolute.y <=
+ KVMFR_INPUT_MOUSE_ABSOLUTE_MAX &&
+ message.payload.mouseAbsolute.wheel >= MIN_MOUSE_WHEEL &&
+ message.payload.mouseAbsolute.wheel <= MAX_MOUSE_WHEEL &&
+ !message.payload.mouseAbsolute.reserved;
+
+ case KVMFR_INPUT_MESSAGE_KEYBOARD:
+ if (!IsZero(message.payload.keyboard.reserved,
+ sizeof(message.payload.keyboard.reserved)))
+ return false;
+ for (size_t i = 0; i < KVMFR_INPUT_KEYBOARD_KEY_COUNT; ++i)
+ if (message.payload.keyboard.keys[i] >
+ KVMFR_INPUT_KEYBOARD_USAGE_MAX)
+ return false;
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool CLGMPInputTransport::ProcessMessage(
+ uint32_t sourceClientID, const KVMFRInputMessage& message)
+{
+ const bool owner = IsOwner(sourceClientID, message.generation);
+ const uint64_t sinkGeneration = m_sink ?
+ m_sink->GetGeneration() : m_sinkGeneration;
+ if (sinkGeneration != m_sinkGeneration)
+ {
+ m_sinkGeneration = sinkGeneration;
+ if (m_ownerClientID)
+ {
+ ReleaseOwner(true, "input endpoint changed");
+ return false;
+ }
+ }
+
+ if (!message.generation || !message.sequence || message.reserved ||
+ !ValidatePayload(message))
+ {
+ if (owner)
+ ReleaseOwner(true, "invalid input message");
+ return false;
+ }
+
+ if (message.type == KVMFR_INPUT_MESSAGE_CLAIM)
+ {
+ if (m_ownerClientID)
+ {
+ if (!owner)
+ return true;
+ if (message.sequence == 1 && m_ownerSequence == 1)
+ return true;
+
+ ReleaseOwner(true, "sequence discontinuity");
+ return false;
+ }
+ return Claim(sourceClientID, message);
+ }
+
+ if (!owner)
+ return true;
+
+ uint32_t expectedSequence = m_ownerSequence + 1;
+ if (!expectedSequence)
+ expectedSequence = 1;
+ if (message.sequence != expectedSequence)
+ {
+ ReleaseOwner(true, "sequence discontinuity");
+ return false;
+ }
+
+ bool accepted = false;
+ switch (message.type)
+ {
+ case KVMFR_INPUT_MESSAGE_RELEASE:
+ ReleaseOwner(true, "client release");
+ return true;
+
+ case KVMFR_INPUT_MESSAGE_KEEPALIVE:
+ accepted = true;
+ break;
+
+ case KVMFR_INPUT_MESSAGE_RESET:
+ accepted = m_sink && m_sink->Reset();
+ break;
+
+ case KVMFR_INPUT_MESSAGE_MOUSE_RELATIVE:
+ accepted = m_sink && m_sink->SendMouseRelative(
+ message.payload.mouseRelative.deltaX,
+ message.payload.mouseRelative.deltaY,
+ message.payload.mouseRelative.wheel,
+ message.payload.mouseRelative.buttons);
+ break;
+
+ case KVMFR_INPUT_MESSAGE_MOUSE_ABSOLUTE:
+ accepted = m_sink && m_sink->SendMouseAbsolute(
+ message.payload.mouseAbsolute.x,
+ message.payload.mouseAbsolute.y,
+ message.payload.mouseAbsolute.wheel,
+ message.payload.mouseAbsolute.buttons);
+ break;
+
+ case KVMFR_INPUT_MESSAGE_KEYBOARD:
+ accepted = m_sink && m_sink->SendKeyboard(
+ message.payload.keyboard.modifiers,
+ message.payload.keyboard.keys);
+ break;
+
+ default:
+ break;
+ }
+
+ if (!accepted)
+ {
+ ReleaseOwner(true, "input delivery failed");
+ return false;
+ }
+
+ const uint64_t deliveredGeneration = m_sink->GetGeneration();
+ if (deliveredGeneration != m_sinkGeneration)
+ {
+ m_sinkGeneration = deliveredGeneration;
+ ReleaseOwner(true, "input endpoint changed");
+ return false;
+ }
+
+ m_ownerSequence = message.sequence;
+ RenewLease();
+ return true;
+}
+
+bool CLGMPInputTransport::DrainMessages()
+{
+ bool received = false;
+ for (unsigned count = 0; count < 256; ++count)
+ {
+ uint8_t data[LGMP_MSGS_SIZE] = {};
+ size_t size = 0;
+ uint32_t sourceClientID = 0;
+ const LGMP_STATUS status = lgmpHostReadDataWithSource(
+ m_queue, data, &size, &sourceClientID);
+ if (status == LGMP_ERR_QUEUE_EMPTY)
+ break;
+ if (status != LGMP_OK)
+ {
+ DEBUG_ERROR("lgmpHostReadData Failed (Input): %s",
+ lgmpStatusString(status));
+ break;
+ }
+
+ received = true;
+ if (size != sizeof(KVMFRInputMessage))
+ {
+ DEBUG_WARN("Ignoring invalid KVMFR input message size");
+ if (sourceClientID == m_ownerClientID)
+ ReleaseOwner(true, "invalid input message");
+ }
+ else
+ {
+ KVMFRInputMessage message = {};
+ memcpy(&message, data, sizeof(message));
+ ProcessMessage(sourceClientID, message);
+ }
+
+ lgmpHostAckData(m_queue);
+ }
+ return received;
+}
+
+DWORD CALLBACK CLGMPInputTransport::ThreadProc(void * context)
+{
+ static_cast(context)->Thread();
+ return 0;
+}
+
+void CLGMPInputTransport::Thread()
+{
+ DWORD avTask = 0;
+ HANDLE avTaskHandle =
+ AvSetMmThreadCharacteristicsW(L"Distribution", &avTask);
+ if (avTaskHandle &&
+ !AvSetMmThreadPriority(avTaskHandle, AVRT_PRIORITY_HIGH))
+ DEBUG_WARN("Failed to raise input MMCSS priority: %lu",
+ GetLastError());
+
+ ULONGLONG activeUntil = 0;
+ const HANDLE waitHandles[] = { m_stopEvent, m_pollTimer };
+ for (;;)
+ {
+ CheckOwner();
+ if (DrainMessages())
+ activeUntil = GetTickCount64() + ACTIVE_POLL_MS;
+
+ const bool active = GetTickCount64() < activeUntil;
+ if (!ArmPollTimer(m_pollTimer, active))
+ {
+ DEBUG_ERROR_HR(GetLastError(), "Failed to arm LGMP input timer");
+ if (WaitForSingleObject(m_stopEvent, 1) != WAIT_TIMEOUT)
+ break;
+ continue;
+ }
+
+ const DWORD wait = WaitForMultipleObjects(
+ _countof(waitHandles), waitHandles, FALSE, INFINITE);
+ if (wait == WAIT_OBJECT_0)
+ break;
+ if (wait != WAIT_OBJECT_0 + 1)
+ {
+ DEBUG_ERROR_HR(GetLastError(), "LGMP input worker wait failed");
+ break;
+ }
+ }
+
+ ReleaseOwner(true, "transport stopped");
+ if (avTaskHandle)
+ AvRevertMmThreadCharacteristics(avTaskHandle);
+}
diff --git a/idd/LGIdd/transport/lgmp/CLGMPInputTransport.h b/idd/LGIdd/transport/lgmp/CLGMPInputTransport.h
new file mode 100644
index 00000000..96894c1f
--- /dev/null
+++ b/idd/LGIdd/transport/lgmp/CLGMPInputTransport.h
@@ -0,0 +1,87 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include "transport/IInputTransport.h"
+
+#include
+
+#include
+
+extern "C" {
+ #include "lgmp/host.h"
+}
+
+class CLGMPHost;
+class IInputSink;
+struct KVMFRInputMessage;
+
+class CLGMPInputTransport final : public IInputTransport
+{
+private:
+ static constexpr ULONGLONG OWNER_LEASE_MS = 500;
+ static constexpr ULONGLONG ACTIVE_POLL_MS = 50;
+
+ CLGMPHost& m_host;
+
+ PLGMPHostQueue m_queue = nullptr;
+ IInputSink * m_sink = nullptr;
+
+ SRWLOCK m_lifecycleLock = SRWLOCK_INIT;
+ HANDLE m_stopEvent = nullptr;
+ HANDLE m_pollTimer = nullptr;
+ HANDLE m_thread = nullptr;
+
+ uint32_t m_ownerClientID = 0;
+ uint32_t m_ownerGeneration = 0;
+ uint32_t m_ownerSequence = 0;
+ ULONGLONG m_ownerDeadline = 0;
+ uint64_t m_sinkGeneration = 0;
+
+ bool Initialize();
+ void DeInit();
+ bool DrainMessages();
+ bool ProcessMessage(uint32_t sourceClientID,
+ const KVMFRInputMessage& message);
+ bool ValidatePayload(const KVMFRInputMessage& message) const;
+ bool IsOwner(uint32_t sourceClientID, uint32_t generation) const;
+ bool Claim(uint32_t sourceClientID,
+ const KVMFRInputMessage& message);
+ void RenewLease();
+ void ReleaseOwner(bool reset, const char * reason);
+ void CheckOwner();
+ void Thread();
+
+ static DWORD CALLBACK ThreadProc(void * context);
+
+ friend class CLGMPTransport;
+
+public:
+ explicit CLGMPInputTransport(CLGMPHost& host) :
+ m_host(host) {}
+ ~CLGMPInputTransport() override;
+
+ CLGMPInputTransport(const CLGMPInputTransport&) = delete;
+ CLGMPInputTransport& operator=(const CLGMPInputTransport&) = delete;
+
+ bool Start(IInputSink& sink) override;
+ void Stop() override;
+};
diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp
index a407e068..bc28b387 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp
+++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp
@@ -49,7 +49,8 @@ static bool TranslateFrameScheduleFlags(
CLGMPTransport::CLGMPTransport() :
m_control(m_host),
- m_frames(m_host, m_ivshmem)
+ m_frames(m_host, m_ivshmem),
+ m_input(m_host)
{
}
@@ -69,9 +70,10 @@ bool CLGMPTransport::Initialize()
if (!m_host.Initialize(m_ivshmem))
return false;
- // Preserve the shared-memory layout: frame queues precede the pointer queue
- // and its retained cursor and color-transform allocations.
- if (!m_frames.Initialize() || !m_control.Initialize())
+ // Preserve the existing shared-memory layout by appending input after the
+ // frame and pointer queues and retained pointer state allocations.
+ if (!m_frames.Initialize() || !m_control.Initialize() ||
+ !m_input.Initialize())
return false;
m_frames.SealMemoryLayout();
diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.h b/idd/LGIdd/transport/lgmp/CLGMPTransport.h
index 12cbacfc..204f2e28 100644
--- a/idd/LGIdd/transport/lgmp/CLGMPTransport.h
+++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.h
@@ -25,6 +25,7 @@
#include "transport/lgmp/CLGMPControl.h"
#include "transport/lgmp/CLGMPFrameTransport.h"
#include "transport/lgmp/CLGMPHost.h"
+#include "transport/lgmp/CLGMPInputTransport.h"
class CLGMPTransport final : public ITransport
{
@@ -35,6 +36,7 @@ private:
CLGMPHost m_host;
CLGMPControl m_control;
CLGMPFrameTransport m_frames;
+ CLGMPInputTransport m_input;
public:
CLGMPTransport();
@@ -53,4 +55,5 @@ public:
IFrameTransport& Frames() override { return m_frames; }
IControlTransport& Control() override { return m_control; }
+ IInputTransport * Input() override { return &m_input; }
};