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; } };