From f495e5b7baf33dc07e15353bc9d9ba696f74fc3f Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 8 Aug 2026 21:23:32 +1000 Subject: [PATCH] [client] input: abstract guest input backends Introduce LG_InputOps and route keyboard and mouse input through the active video transport when it provides an input backend. Keep SPICE as the fallback and force it while the SPICE display is active. Add atomic shared/exclusive locking for safe backend changes. --- client/CMakeLists.txt | 2 + client/include/input.h | 50 ++++ client/include/interface/input.h | 60 +++++ client/include/interface/transport.h | 5 + client/src/app.c | 52 +--- client/src/config.c | 14 +- client/src/core.c | 11 +- client/src/input.c | 362 +++++++++++++++++++++++++++ client/src/input_spice.c | 82 ++++++ client/src/keybind.c | 39 ++- client/src/keybind.h | 2 +- client/src/main.c | 68 +++-- client/src/main.h | 2 - client/tests/mouse_test.c | 7 +- client/transports/LGMP/lgmp.c | 8 + client/transports/Test/test.c | 8 + common/include/common/locking.h | 44 ++++ 17 files changed, 711 insertions(+), 105 deletions(-) create mode 100644 client/include/input.h create mode 100644 client/include/interface/input.h create mode 100644 client/src/input.c create mode 100644 client/src/input_spice.c diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 57d4c792..7d64458c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -167,6 +167,8 @@ set(SOURCES src/render_queue.c src/evdev.c src/transport.c + src/input.c + src/input_spice.c src/overlay/splash.c src/overlay/alert.c diff --git a/client/include/input.h b/client/include/input.h new file mode 100644 index 00000000..3c05adf5 --- /dev/null +++ b/client/include/input.h @@ -0,0 +1,50 @@ +/** + * 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 + */ + +#ifndef _H_LG_CLIENT_INPUT_ +#define _H_LG_CLIENT_INPUT_ + +#include "interface/input.h" + +extern const LG_InputOps LGI_Spice; + +void lgInput_init(void); +void lgInput_free(void); + +void lgInput_setFallback(const LG_InputOps * ops, void * opaque); +void lgInput_setTransport(const LG_InputOps * ops, void * opaque); +void lgInput_dropTransport(void); +void lgInput_useTransport(bool enable); + +bool lgInput_available(void); +bool lgInput_supports(LG_InputSupport support); + +bool lgInput_keyDown(int key); +bool lgInput_keyUp(int key); +bool lgInput_keyboardLEDs(bool numLock, bool capsLock, bool scrollLock); +void lgInput_releaseKeys(void); + +bool lgInput_mouseMotion(int32_t x, int32_t y); +bool lgInput_mousePosition(uint32_t x, uint32_t y, uint32_t width, + uint32_t height); +bool lgInput_mousePress(unsigned int button); +bool lgInput_mouseRelease(unsigned int button); + +#endif diff --git a/client/include/interface/input.h b/client/include/interface/input.h new file mode 100644 index 00000000..95090ae4 --- /dev/null +++ b/client/include/interface/input.h @@ -0,0 +1,60 @@ +/** + * 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 + */ + +#ifndef _H_LG_CLIENT_INPUT_INTERFACE_ +#define _H_LG_CLIENT_INPUT_INTERFACE_ + +#include +#include + +typedef enum LG_InputSupport +{ + LG_INPUT_SUPPORT_MOUSE_ABSOLUTE, +} +LG_InputSupport; + +typedef struct LG_InputOps +{ + const char * name; + + /* Operations must fail safely if a remote endpoint disappears. */ + bool (*supports)(void * opaque, LG_InputSupport support); + + /* Keyboard codes use the Linux input-event KEY_* values. */ + bool (*keyDown)(void * opaque, int key); + bool (*keyUp)(void * opaque, int key); + bool (*keyboardLEDs)(void * opaque, bool numLock, bool capsLock, + bool scrollLock); + + bool (*mouseMotion)(void * opaque, int32_t x, int32_t y); + /* Position is in guest pixels and must be within the supplied dimensions. */ + bool (*mousePosition)(void * opaque, uint32_t x, uint32_t y, + uint32_t width, uint32_t height); + /* Button order: left, middle, right, wheel up/down, side, extra. */ + bool (*mousePress)(void * opaque, unsigned int button); + bool (*mouseRelease)(void * opaque, unsigned int button); + + /* Restore all devices owned by this implementation to a neutral state. + * This must safely tolerate the remote input endpoint becoming unavailable. */ + void (*reset)(void * opaque); +} +LG_InputOps; + +#endif diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index d90b12b5..8d65e5a6 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -27,6 +27,7 @@ #include "common/framebuffer.h" #include "common/types.h" +#include "interface/input.h" #define LG_TRANSPORT_MAX_DAMAGE_RECTS LG_MAX_FRAME_DAMAGE_RECTS @@ -244,6 +245,10 @@ typedef struct LG_TransportOps void (*disconnect)(LG_Transport * transport); bool (*sessionValid)(LG_Transport * transport); bool (*supportsDMA)(LG_Transport * transport); + /* Queried after connect. The returned operations and opaque value remain + * valid until disconnect; NULL indicates that this session has no input. */ + const LG_InputOps *(*getInputOps)(LG_Transport * transport, + void ** opaque); bool (*attachRenderer)(LG_Transport * transport, const LG_RendererInterop * interop); void (*detachRenderer)(LG_Transport * transport); diff --git a/client/src/app.c b/client/src/app.c index 181c5ce8..aff6d6a3 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -26,6 +26,7 @@ #include "clipboard.h" #include "render_queue.h" #include "evdev.h" +#include "input.h" #include "kb.h" @@ -236,9 +237,7 @@ void app_handleFocusEvent(bool focused) core_setCursorInView(false); if (g_params.releaseKeysOnFocusLoss) - for (int key = 0; key < KEY_MAX; key++) - if (atomic_load_explicit(&g_state.keyDown[key], memory_order_relaxed)) - app_handleKeyReleaseInternal(key); + lgInput_releaseKeys(); g_state.escapeActive = false; @@ -472,7 +471,7 @@ void app_handleButtonPress(int button) if (!core_inputEnabled() || !g_cursor.inView || !g_cursor.viewReq) return; - if (!purespice_mousePress(button)) + if (!lgInput_mousePress(button)) DEBUG_ERROR("app_handleButtonPress: failed to send message"); } @@ -495,7 +494,7 @@ void app_handleButtonRelease(int button) if (!core_inputEnabled()) return; - if (!purespice_mouseRelease(button)) + if (!lgInput_mouseRelease(button)) DEBUG_ERROR("app_handleButtonRelease: failed to send message"); } @@ -551,20 +550,8 @@ void app_handleKeyPressInternal(int sc) if (g_params.ignoreWindowsKeys && (sc == KEY_LEFTMETA || sc == KEY_RIGHTMETA)) return; - if (!atomic_load_explicit(&g_state.keyDown[sc], memory_order_relaxed)) - { - uint32_t ps2 = linux_to_ps2[sc]; - if (!ps2) - return; - - if (purespice_keyDown(ps2)) - atomic_store_explicit(&g_state.keyDown[sc], true, memory_order_relaxed); - else - { - DEBUG_ERROR("app_handleKeyPress: failed to send message"); - return; - } - } + if (!lgInput_keyDown(sc)) + DEBUG_ERROR("app_handleKeyPress: failed to send message"); } void app_handleKeyReleaseInternal(int sc) @@ -573,7 +560,7 @@ void app_handleKeyReleaseInternal(int sc) { if (g_state.escapeAction == -1) { - if (!g_state.escapeHelp && g_params.useSpiceInput && + if (!g_state.escapeHelp && lgInput_available() && !app_isOverlayMode()) core_setGrab(!g_cursor.grab); } @@ -592,24 +579,11 @@ void app_handleKeyReleaseInternal(int sc) if (!core_inputEnabled()) return; - // avoid sending key up events when we didn't send a down - if (!atomic_load_explicit(&g_state.keyDown[sc], memory_order_relaxed)) - return; - if (g_params.ignoreWindowsKeys && (sc == KEY_LEFTMETA || sc == KEY_RIGHTMETA)) return; - uint32_t ps2 = linux_to_ps2[sc]; - if (!ps2) - return; - - if (purespice_keyUp(ps2)) - atomic_store_explicit(&g_state.keyDown[sc], false, memory_order_relaxed); - else - { + if (!lgInput_keyUp(sc)) DEBUG_ERROR("app_handleKeyRelease: failed to send message"); - return; - } } void app_handleKeyPress(int sc) @@ -643,12 +617,7 @@ void app_handleKeyboardLEDs(bool numLock, bool capsLock, bool scrollLock) if (!core_inputEnabled()) return; - uint32_t modifiers = - (scrollLock ? 1 /* SPICE_SCROLL_LOCK_MODIFIER */ : 0) | - (numLock ? 2 /* SPICE_NUM_LOCK_MODIFIER */ : 0) | - (capsLock ? 4 /* SPICE_CAPS_LOCK_MODIFIER */ : 0); - - if (!purespice_keyModifiers(modifiers)) + if (!lgInput_keyboardLEDs(numLock, capsLock, scrollLock)) DEBUG_ERROR("app_handleKeyboardLEDs: failed to send message"); } @@ -716,7 +685,7 @@ void app_handleMouseBasic(void) g_cursor.projected.x += x; g_cursor.projected.y += y; - if (!purespice_mouseMotion(x, y)) + if (!lgInput_mouseMotion(x, y)) DEBUG_ERROR("failed to send mouse motion message"); } @@ -1395,6 +1364,7 @@ bool app_useSpiceDisplay(bool enable) active = enable; atomic_store_explicit(&g_state.spiceDisplayActive, active, memory_order_release); + lgInput_useTransport(!active); overlayStatus_set(LG_USER_STATUS_SPICE, enable); done: diff --git a/client/src/config.c b/client/src/config.c index c702a507..43f24a8e 100644 --- a/client/src/config.c +++ b/client/src/config.c @@ -419,7 +419,7 @@ static struct Option options[] = { .module = "input", .name = "captureOnly", - .description = "Only enable input via SPICE if in capture mode", + .description = "Only enable guest input while in capture mode", .type = OPTION_TYPE_BOOL, .value.x_bool = false }, @@ -746,6 +746,12 @@ bool config_load(int argc, char * argv[]) g_params.helpMenuDelayUs = option_get_int("input", "helpMenuDelay") * (uint64_t) 1000; + g_params.scaleMouseInput = option_get_bool("spice", "scaleCursor"); + g_params.captureOnStart = option_get_bool("spice", "captureOnStart"); + g_params.alwaysShowCursor = option_get_bool("spice", "alwaysShowCursor"); + g_params.showCursorDot = option_get_bool("spice", "showCursorDot"); + g_params.largeCursorDot = option_get_bool("spice", "largeCursorDot"); + g_params.minimizeOnFocusLoss = option_get_bool("win", "minimizeOnFocusLoss"); g_params.setGuestRes = option_get_bool("win", "setGuestRes" ); @@ -769,12 +775,6 @@ bool config_load(int argc, char * argv[]) g_params.clipboardToVM = false; g_params.clipboardToLocal = false; } - - g_params.scaleMouseInput = option_get_bool("spice", "scaleCursor"); - g_params.captureOnStart = option_get_bool("spice", "captureOnStart"); - g_params.alwaysShowCursor = option_get_bool("spice", "alwaysShowCursor"); - g_params.showCursorDot = option_get_bool("spice", "showCursorDot"); - g_params.largeCursorDot = option_get_bool("spice", "largeCursorDot"); } g_params.audioDebug = option_get_bool("audio", "debug"); diff --git a/client/src/core.c b/client/src/core.c index 637d2be8..0ba73775 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -24,6 +24,7 @@ #include "util.h" #include "kb.h" #include "message.h" +#include "input.h" #include "common/time.h" #include "common/debug.h" @@ -132,7 +133,7 @@ static bool moveExit(double ex, double ey) bool core_inputEnabled(void) { - return g_params.useSpiceInput && !g_state.ignoreInput && + return lgInput_available() && !g_state.ignoreInput && ((g_cursor.grab && g_params.captureInputOnly) || !g_params.captureInputOnly); } @@ -719,7 +720,7 @@ void core_handleMouseGrabbed(double ex, double ey) if (x == 0 && y == 0) return; - if (!purespice_mouseMotion(x, y)) + if (!lgInput_mouseMotion(x, y)) DEBUG_ERROR("failed to send mouse motion message"); } @@ -737,9 +738,9 @@ void core_handleMouseNormal(double ex, double ey) // wiggle the mouse when the guest has not provided any information, we need // to do this because windows doesn't enable a cursor at all until it has // been moved for the first time. - if (!purespice_mouseMotion(1, 1)) + if (!lgInput_mouseMotion(1, 1)) DEBUG_ERROR("failed to send mouse motion message"); - if (!purespice_mouseMotion(-1, -1)) + if (!lgInput_mouseMotion(-1, -1)) DEBUG_ERROR("failed to send mouse motion message"); } return; @@ -985,7 +986,7 @@ fallback: x, y, g_cursor.guest.x, g_cursor.guest.y, didExit, testExit, g_cursor.warpState); - if (!purespice_mouseMotion(x, y)) + if (!lgInput_mouseMotion(x, y)) DEBUG_ERROR("failed to send mouse motion message"); } diff --git a/client/src/input.c b/client/src/input.c new file mode 100644 index 00000000..bba6f824 --- /dev/null +++ b/client/src/input.c @@ -0,0 +1,362 @@ +/** + * 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 "input.h" + +#include "common/debug.h" +#include "common/locking.h" + +#include +#include + +struct InputBinding +{ + const LG_InputOps * ops; + void * opaque; + bool mouseAbsolute; +}; + +static struct +{ + LG_RWLock activeLock; + + struct InputBinding fallback; + struct InputBinding transport; + struct InputBinding active; + bool useTransport; + + _Atomic(bool) keys[KEY_MAX]; + _Atomic(uint32_t) buttons; +} +l_input; + +static bool validOps(const LG_InputOps * ops) +{ + return + ops && + ops->name && + ops->supports && + ops->keyDown && + ops->keyUp && + ops->mouseMotion && + ops->mousePress && + ops->mouseRelease; +} + +static struct InputBinding makeBinding(const LG_InputOps * ops, + void * opaque) +{ + return (struct InputBinding) + { + .ops = ops, + .opaque = opaque, + .mouseAbsolute = ops && ops->mousePosition && + ops->supports(opaque, LG_INPUT_SUPPORT_MOUSE_ABSOLUTE), + }; +} + +static void releaseKeysNL(void) +{ + for (int key = 0; key < KEY_MAX; ++key) + if (atomic_exchange_explicit(&l_input.keys[key], false, + memory_order_relaxed) && l_input.active.ops) + l_input.active.ops->keyUp(l_input.active.opaque, key); +} + +static void releaseButtonsNL(void) +{ + const uint32_t buttons = atomic_exchange_explicit( + &l_input.buttons, 0, memory_order_relaxed); + + if (!l_input.active.ops) + return; + + for (unsigned int button = 1; button < 32; ++button) + if (buttons & (UINT32_C(1) << button)) + l_input.active.ops->mouseRelease(l_input.active.opaque, button); +} + +static void resetActiveNL(void) +{ + releaseKeysNL(); + releaseButtonsNL(); + if (l_input.active.ops && l_input.active.ops->reset) + l_input.active.ops->reset(l_input.active.opaque); +} + +static void clearStateNL(void) +{ + for (int key = 0; key < KEY_MAX; ++key) + atomic_store_explicit(&l_input.keys[key], false, memory_order_relaxed); + + atomic_store_explicit(&l_input.buttons, 0, memory_order_relaxed); +} + +static void updateActiveNL(void) +{ + const struct InputBinding next = + l_input.useTransport && l_input.transport.ops ? + l_input.transport : l_input.fallback; + + if (next.ops == l_input.active.ops && + next.opaque == l_input.active.opaque) + return; + + resetActiveNL(); + l_input.active = next; + + if (l_input.active.ops) + { + if (l_input.active.ops->reset) + l_input.active.ops->reset(l_input.active.opaque); + DEBUG_INFO("Using Input: %s", l_input.active.ops->name); + } + else + DEBUG_INFO("Input is unavailable"); +} + +void lgInput_init(void) +{ + l_input.fallback = (struct InputBinding) { 0 }; + l_input.transport = (struct InputBinding) { 0 }; + l_input.active = (struct InputBinding) { 0 }; + l_input.useTransport = true; + + for (int key = 0; key < KEY_MAX; ++key) + atomic_init(&l_input.keys[key], false); + atomic_init(&l_input.buttons, 0); + + LG_RWLOCK_INIT(l_input.activeLock); +} + +void lgInput_free(void) +{ + LG_LOCK_EXCLUSIVE(l_input.activeLock); + resetActiveNL(); + l_input.active = (struct InputBinding) { 0 }; + l_input.fallback = (struct InputBinding) { 0 }; + l_input.transport = (struct InputBinding) { 0 }; + LG_UNLOCK_EXCLUSIVE(l_input.activeLock); + LG_RWLOCK_FREE(l_input.activeLock); +} + +void lgInput_setFallback(const LG_InputOps * ops, void * opaque) +{ + if (ops && !validOps(ops)) + { + DEBUG_ERROR("Invalid fallback input operations"); + ops = NULL; + opaque = NULL; + } + + LG_LOCK_EXCLUSIVE(l_input.activeLock); + l_input.fallback = makeBinding(ops, opaque); + updateActiveNL(); + LG_UNLOCK_EXCLUSIVE(l_input.activeLock); +} + +void lgInput_setTransport(const LG_InputOps * ops, void * opaque) +{ + if (ops && !validOps(ops)) + { + DEBUG_ERROR("Invalid transport input operations"); + ops = NULL; + opaque = NULL; + } + + LG_LOCK_EXCLUSIVE(l_input.activeLock); + l_input.transport = makeBinding(ops, opaque); + updateActiveNL(); + LG_UNLOCK_EXCLUSIVE(l_input.activeLock); +} + +void lgInput_dropTransport(void) +{ + LG_LOCK_EXCLUSIVE(l_input.activeLock); + if (l_input.useTransport && l_input.active.ops == l_input.transport.ops && + l_input.active.opaque == l_input.transport.opaque) + { + l_input.active = (struct InputBinding) { 0 }; + clearStateNL(); + } + + l_input.transport = (struct InputBinding) { 0 }; + updateActiveNL(); + LG_UNLOCK_EXCLUSIVE(l_input.activeLock); +} + +void lgInput_useTransport(bool enable) +{ + LG_LOCK_EXCLUSIVE(l_input.activeLock); + l_input.useTransport = enable; + updateActiveNL(); + LG_UNLOCK_EXCLUSIVE(l_input.activeLock); +} + +bool lgInput_available(void) +{ + LG_LOCK_SHARED(l_input.activeLock); + const bool result = l_input.active.ops != NULL; + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_supports(LG_InputSupport support) +{ + LG_LOCK_SHARED(l_input.activeLock); + bool result; + switch (support) + { + case LG_INPUT_SUPPORT_MOUSE_ABSOLUTE: + result = l_input.active.mouseAbsolute; + break; + + default: + result = false; + break; + } + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_keyDown(int key) +{ + if (key < 0 || key >= KEY_MAX) + return false; + + LG_LOCK_SHARED(l_input.activeLock); + if (atomic_exchange_explicit( + &l_input.keys[key], true, memory_order_relaxed)) + { + LG_UNLOCK_SHARED(l_input.activeLock); + return true; + } + + const bool result = l_input.active.ops && + l_input.active.ops->keyDown(l_input.active.opaque, key); + if (!result) + atomic_store_explicit(&l_input.keys[key], false, memory_order_relaxed); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_keyUp(int key) +{ + if (key < 0 || key >= KEY_MAX) + return false; + + LG_LOCK_SHARED(l_input.activeLock); + if (!atomic_exchange_explicit( + &l_input.keys[key], false, memory_order_relaxed)) + { + LG_UNLOCK_SHARED(l_input.activeLock); + return true; + } + + const bool result = l_input.active.ops && + l_input.active.ops->keyUp(l_input.active.opaque, key); + if (!result) + atomic_store_explicit(&l_input.keys[key], true, memory_order_relaxed); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_keyboardLEDs(bool numLock, bool capsLock, bool scrollLock) +{ + LG_LOCK_SHARED(l_input.activeLock); + const bool result = l_input.active.ops && + (!l_input.active.ops->keyboardLEDs || + l_input.active.ops->keyboardLEDs(l_input.active.opaque, + numLock, capsLock, scrollLock)); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +void lgInput_releaseKeys(void) +{ + LG_LOCK_EXCLUSIVE(l_input.activeLock); + releaseKeysNL(); + LG_UNLOCK_EXCLUSIVE(l_input.activeLock); +} + +bool lgInput_mouseMotion(int32_t x, int32_t y) +{ + LG_LOCK_SHARED(l_input.activeLock); + const bool result = l_input.active.ops && + l_input.active.ops->mouseMotion(l_input.active.opaque, x, y); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_mousePosition(uint32_t x, uint32_t y, uint32_t width, + uint32_t height) +{ + LG_LOCK_SHARED(l_input.activeLock); + const bool result = l_input.active.mouseAbsolute && + l_input.active.ops->mousePosition(l_input.active.opaque, + x, y, width, height); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_mousePress(unsigned int button) +{ + if (button == 0 || button >= 32) + return false; + + const uint32_t mask = UINT32_C(1) << button; + LG_LOCK_SHARED(l_input.activeLock); + if (atomic_fetch_or_explicit( + &l_input.buttons, mask, memory_order_relaxed) & mask) + { + LG_UNLOCK_SHARED(l_input.activeLock); + return true; + } + + const bool result = l_input.active.ops && + l_input.active.ops->mousePress(l_input.active.opaque, button); + if (!result) + atomic_fetch_and_explicit( + &l_input.buttons, ~mask, memory_order_relaxed); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} + +bool lgInput_mouseRelease(unsigned int button) +{ + if (button == 0 || button >= 32) + return false; + + const uint32_t mask = UINT32_C(1) << button; + LG_LOCK_SHARED(l_input.activeLock); + if (!(atomic_fetch_and_explicit( + &l_input.buttons, ~mask, memory_order_relaxed) & mask)) + { + LG_UNLOCK_SHARED(l_input.activeLock); + return true; + } + + const bool result = l_input.active.ops && + l_input.active.ops->mouseRelease(l_input.active.opaque, button); + if (!result) + atomic_fetch_or_explicit(&l_input.buttons, mask, memory_order_relaxed); + LG_UNLOCK_SHARED(l_input.activeLock); + return result; +} diff --git a/client/src/input_spice.c b/client/src/input_spice.c new file mode 100644 index 00000000..efaec2c9 --- /dev/null +++ b/client/src/input_spice.c @@ -0,0 +1,82 @@ +/** + * 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 "input.h" +#include "kb.h" + +#include +#include + +static bool spiceSupports(void * opaque, LG_InputSupport support) +{ + return false; +} + +static bool spiceKeyDown(void * opaque, int key) +{ + const uint32_t ps2 = linux_to_ps2[key]; + return !ps2 || purespice_keyDown(ps2); +} + +static bool spiceKeyUp(void * opaque, int key) +{ + const uint32_t ps2 = linux_to_ps2[key]; + return !ps2 || purespice_keyUp(ps2); +} + +static bool spiceKeyboardLEDs(void * opaque, bool numLock, bool capsLock, + bool scrollLock) +{ + const uint32_t modifiers = + (scrollLock ? 1 /* SPICE_SCROLL_LOCK_MODIFIER */ : 0) | + (numLock ? 2 /* SPICE_NUM_LOCK_MODIFIER */ : 0) | + (capsLock ? 4 /* SPICE_CAPS_LOCK_MODIFIER */ : 0); + + return purespice_keyModifiers(modifiers); +} + +static bool spiceMouseMotion(void * opaque, int32_t x, int32_t y) +{ + return purespice_mouseMotion(x, y); +} + +static bool spiceMousePress(void * opaque, unsigned int button) +{ + return purespice_mousePress(button); +} + +static bool spiceMouseRelease(void * opaque, unsigned int button) +{ + return purespice_mouseRelease(button); +} + +const LG_InputOps LGI_Spice = +{ + .name = "SPICE", + .supports = spiceSupports, + .keyDown = spiceKeyDown, + .keyUp = spiceKeyUp, + .keyboardLEDs = spiceKeyboardLEDs, + .mouseMotion = spiceMouseMotion, + .mousePosition = NULL, + .mousePress = spiceMousePress, + .mouseRelease = spiceMouseRelease, + .reset = NULL, +}; diff --git a/client/src/keybind.c b/client/src/keybind.c index e949a76c..e28bb30e 100644 --- a/client/src/keybind.c +++ b/client/src/keybind.c @@ -24,10 +24,10 @@ #include "app.h" #include "audio.h" #include "core.h" +#include "input.h" #include "kb.h" #include "message.h" -#include #include static void bind_fullscreen(int sc, void * opaque) @@ -96,23 +96,19 @@ static void bind_mouseSens(int sc, void * opaque) static void bind_ctrlAltFn(int sc, void * opaque) { - const uint32_t ctrl = linux_to_ps2[KEY_LEFTCTRL]; - const uint32_t alt = linux_to_ps2[KEY_LEFTALT ]; - const uint32_t fn = linux_to_ps2[sc]; - purespice_keyDown(ctrl); - purespice_keyDown(alt ); - purespice_keyDown(fn ); + lgInput_keyDown(KEY_LEFTCTRL); + lgInput_keyDown(KEY_LEFTALT); + lgInput_keyDown(sc); - purespice_keyUp(ctrl); - purespice_keyUp(alt ); - purespice_keyUp(fn ); + lgInput_keyUp(KEY_LEFTCTRL); + lgInput_keyUp(KEY_LEFTALT); + lgInput_keyUp(sc); } static void bind_passthrough(int sc, void * opaque) { - sc = linux_to_ps2[sc]; - purespice_keyDown(sc); - purespice_keyUp (sc); + lgInput_keyDown(sc); + lgInput_keyUp (sc); } static void bind_toggleOverlay(int sc, void * opaque) @@ -122,8 +118,9 @@ static void bind_toggleOverlay(int sc, void * opaque) static void bind_toggleKey(int sc, void * opaque) { - purespice_keyDown((uintptr_t) opaque); - purespice_keyUp((uintptr_t) opaque); + const int key = (uintptr_t) opaque; + lgInput_keyDown(key); + lgInput_keyUp(key); } static void bind_setGuestRes(int sc, void * opaque) @@ -183,25 +180,25 @@ static void bind_toggleMicDefault(int sc, void * opaque) } #endif -void keybind_spiceRegister(void) +void keybind_inputRegister(void) { - /* register the common keybinds for spice */ + /* register the common input keybinds */ static bool firstTime = true; if (firstTime) { app_registerKeybind(KEY_I, bind_input, NULL, - "Spice keyboard & mouse toggle"); + "Keyboard & mouse toggle"); app_registerKeybind(KEY_INSERT, bind_mouseSens, (void *) true, "Increase mouse sensitivity in capture mode"); app_registerKeybind(KEY_DELETE, bind_mouseSens, (void *) false, "Decrease mouse sensitivity in capture mode"); - app_registerKeybind(KEY_UP, bind_toggleKey, (void *) PS2_VOLUME_UP, + app_registerKeybind(KEY_UP, bind_toggleKey, (void *) KEY_VOLUMEUP, "Send volume up to the guest"); - app_registerKeybind(KEY_DOWN, bind_toggleKey, (void *) PS2_VOLUME_DOWN, + app_registerKeybind(KEY_DOWN, bind_toggleKey, (void *) KEY_VOLUMEDOWN, "Send volume down to the guest"); - app_registerKeybind(KEY_M, bind_toggleKey, (void *) PS2_MUTE, + app_registerKeybind(KEY_M, bind_toggleKey, (void *) KEY_MUTE, "Send mute to the guest"); app_registerKeybind(KEY_LEFTMETA, bind_passthrough, NULL, diff --git a/client/src/keybind.h b/client/src/keybind.h index 2fe50e89..473a14c5 100644 --- a/client/src/keybind.h +++ b/client/src/keybind.h @@ -22,6 +22,6 @@ #define _H_LG_KEYBIND_ void keybind_commonRegister(void); -void keybind_spiceRegister(void); +void keybind_inputRegister(void); #endif diff --git a/client/src/main.c b/client/src/main.c index 2e30390b..95980278 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -69,6 +69,7 @@ #include "render_queue.h" #include "evdev.h" #include "frame_scheduler.h" +#include "input.h" #ifdef ENABLE_TESTS #include "interface/test_capture.h" @@ -150,8 +151,8 @@ static void lgInit(void) g_cursor.surfaceExit = false; g_cursor.guest.valid = false; - // if spice is not in use, hide the local cursor - if ((!g_params.useSpiceInput && g_params.hideMouse) || !g_params.showCursorDot) + // if guest input is not in use, hide the local cursor + if ((!lgInput_available() && g_params.hideMouse) || !g_params.showCursorDot) g_state.ds->setPointer(LG_POINTER_NONE); else g_state.ds->setPointer(LG_POINTER_SQUARE); @@ -1022,6 +1023,12 @@ static int renderThread(void * unused) lgTimerDestroy(tickTimer); lgTimerDestroy(fpsTimer); + if (g_state.transport && + g_state.transportOps->sessionValid(g_state.transport)) + lgInput_setTransport(NULL, NULL); + else + lgInput_dropTransport(); + core_stopCursorThread(); core_stopFrameThread(); @@ -1058,7 +1065,7 @@ int main_cursorThread(void * unused) g_cursor.redraw = false; RENDERER(onMouseEvent, g_cursor.guest.visible && - (g_cursor.draw || !g_params.useSpiceInput), + (g_cursor.draw || !lgInput_available()), g_cursor.guest.x, g_cursor.guest.y, g_cursor.guest.hx, g_cursor.guest.hy); if (!g_state.stopVideo) @@ -1067,6 +1074,8 @@ int main_cursorThread(void * unused) continue; } + if (status == LG_TRANSPORT_DISCONNECTED) + lgInput_dropTransport(); app_setState(status == LG_TRANSPORT_DISCONNECTED ? APP_STATE_RESTART : APP_STATE_SHUTDOWN); if (status != LG_TRANSPORT_DISCONNECTED) @@ -1075,7 +1084,7 @@ int main_cursorThread(void * unused) } const bool wasRendered = g_cursor.guest.visible && - (g_cursor.draw || !g_params.useSpiceInput); + (g_cursor.draw || !lgInput_available()); bool hotspotChanged = false; if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) @@ -1156,12 +1165,12 @@ int main_cursorThread(void * unused) app_updateMouseState(); g_cursor.redraw = false; RENDERER(onMouseEvent, - g_cursor.guest.visible && (g_cursor.draw || !g_params.useSpiceInput), + g_cursor.guest.visible && (g_cursor.draw || !lgInput_available()), g_cursor.guest.x, g_cursor.guest.y, g_cursor.guest.hx, g_cursor.guest.hy); const bool isRendered = g_cursor.guest.visible && - (g_cursor.draw || !g_params.useSpiceInput); + (g_cursor.draw || !lgInput_available()); const bool contentChanged = (pointer.flags & (LG_TRANSPORT_POINTER_SHAPE | LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) || whiteLevelChanged; @@ -1211,7 +1220,10 @@ int main_frameThread(void * unused) continue; } if (status == LG_TRANSPORT_DISCONNECTED) + { + lgInput_dropTransport(); app_setState(APP_STATE_RESTART); + } else if (status == LG_TRANSPORT_END) app_setState(APP_STATE_SHUTDOWN); else @@ -1484,9 +1496,10 @@ static void checkUUID(void) app_msgBox( "SPICE Configuration Error", "You have connected SPICE to the wrong guest.\n" - "Input will not function until this is corrected."); + "SPICE input will not function until this is corrected."); g_params.useSpiceInput = false; + lgInput_setFallback(NULL, NULL); atomic_store_explicit(&g_state.spiceClose, true, memory_order_release); purespice_disconnect(); } @@ -1494,6 +1507,9 @@ static void checkUUID(void) void spiceReady(void) { atomic_store_explicit(&g_state.spiceReady, true, memory_order_release); + if (g_params.useSpiceInput) + lgInput_setFallback(&LGI_Spice, NULL); + if (atomic_load_explicit(&g_state.spiceDisplayRequested, memory_order_acquire)) app_useSpiceDisplay(true); @@ -1525,7 +1541,7 @@ void spiceReady(void) DEBUG_WARN("Failed to obtain SPICE server information"); if (g_params.useSpiceInput) - keybind_spiceRegister(); + keybind_inputRegister(); lgSignalEvent(e_spice); } @@ -1759,27 +1775,12 @@ int spiceThread(void * arg) } } - // send key up events for any pressed keys - if (g_params.useSpiceInput) - { - for(int scancode = 0; scancode < KEY_MAX; ++scancode) - if (atomic_load_explicit( - &g_state.keyDown[scancode], memory_order_relaxed)) - { - const uint32_t ps2 = linux_to_ps2[scancode]; - if (ps2 && purespice_keyUp(ps2)) - atomic_store_explicit( - &g_state.keyDown[scancode], false, memory_order_relaxed); - else - DEBUG_ERROR("Failed to release key %d during SPICE shutdown", - scancode); - } - } - + lgInput_setFallback(NULL, NULL); purespice_disconnect(); end: + lgInput_setFallback(NULL, NULL); audio_free(); // if the connection was disconnected intentionally we don't want to shutdown @@ -1890,6 +1891,7 @@ static int lg_run(void) { LG_LOCK_INIT(l_cursorRepaint.lock); frameTimingInit(); + lgInput_init(); #ifdef ENABLE_TESTS memset(&l_testCapture, 0, sizeof(l_testCapture)); @@ -2355,8 +2357,13 @@ restart: g_state.transportFeatures = session.features; frameScheduler_start(session.features); - if (g_state.spiceReady && g_params.useSpiceInput) - keybind_spiceRegister(); + void * inputOpaque = NULL; + const LG_InputOps * inputOps = g_state.transportOps->getInputOps ? + g_state.transportOps->getInputOps(g_state.transport, &inputOpaque) : NULL; + lgInput_setTransport(inputOps, inputOpaque); + + if (lgInput_available()) + keybind_inputRegister(); checkUUID(); DEBUG_INFO("Starting session"); atomic_store_explicit( @@ -2371,6 +2378,7 @@ restart: { if (unlikely(!g_state.transportOps->sessionValid(g_state.transport))) { + lgInput_dropTransport(); atomic_store_explicit( &g_state.lgHostConnected, false, memory_order_release); DEBUG_INFO("Waiting for the host to restart..."); @@ -2391,6 +2399,7 @@ restart: core_stopFrameThread(); core_stopCursorThread(); + lgInput_dropTransport(); g_state.transportOps->disconnect(g_state.transport); app_setState(APP_STATE_RUNNING); @@ -2435,7 +2444,12 @@ static void lg_shutdown(void) LG_LOCK_FREE(l_cursorRepaint.lock); if (g_state.transportOps) + { + lgInput_dropTransport(); g_state.transportOps->destroy(&g_state.transport); + } + + lgInput_free(); if (g_state.frameEvent) { diff --git a/client/src/main.h b/client/src/main.h index 32b95c7d..b56811d3 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -99,8 +99,6 @@ struct AppState int escapeAction; bool escapeHelp; struct ll * bindings; - atomic_bool keyDown[KEY_MAX]; - bool haveSrcSize; struct Point windowPos; int windowW, windowH; diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index c7d3e088..9b3764fe 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -241,7 +241,12 @@ void app_mouseTrace(const char * file, unsigned int line, (void)format; } -bool purespice_mouseMotion(int32_t x, int32_t y) +bool lgInput_available(void) +{ + return g_params.useSpiceInput; +} + +bool lgInput_mouseMotion(int32_t x, int32_t y) { push(EV_MOTION, x, y, false); return true; diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 458aa5cd..a24e7b3b 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -1327,6 +1327,13 @@ static LG_TransportStatus lgmp_controlStatus(LG_Transport * this, LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE; } +static const LG_InputOps * lgmp_getInputOps(LG_Transport * this, + void ** opaque) +{ + *opaque = NULL; + return NULL; +} + const LG_TransportOps LGT_LGMP = { .name = "lgmp", @@ -1337,6 +1344,7 @@ const LG_TransportOps LGT_LGMP = .disconnect = lgmp_disconnect, .sessionValid = lgmp_sessionValid, .supportsDMA = lgmp_supportsDMA, + .getInputOps = lgmp_getInputOps, .attachRenderer = lgmp_attachRenderer, .detachRenderer = lgmp_detachRenderer, .nextFrame = lgmp_nextFrame, diff --git a/client/transports/Test/test.c b/client/transports/Test/test.c index 4858cb68..4cf8a1e1 100644 --- a/client/transports/Test/test.c +++ b/client/transports/Test/test.c @@ -727,6 +727,13 @@ static LG_TransportStatus test_controlStatus(LG_Transport * this, LG_TRANSPORT_DISCONNECTED; } +static const LG_InputOps * test_getInputOps(LG_Transport * this, + void ** opaque) +{ + *opaque = NULL; + return NULL; +} + const LG_TransportOps LGT_Test = { .name = "test", @@ -737,6 +744,7 @@ const LG_TransportOps LGT_Test = .disconnect = test_disconnect, .sessionValid = test_sessionValid, .supportsDMA = test_supportsDMA, + .getInputOps = test_getInputOps, .attachRenderer = test_attachRenderer, .detachRenderer = test_detachRenderer, .nextFrame = test_nextFrame, diff --git a/common/include/common/locking.h b/common/include/common/locking.h index e2274870..a75b1977 100644 --- a/common/include/common/locking.h +++ b/common/include/common/locking.h @@ -34,6 +34,50 @@ typedef atomic_flag LG_Lock; atomic_flag_clear_explicit(&(x), memory_order_release); #define LG_LOCK_FREE(x) +typedef struct LG_RWLock +{ + atomic_uint readers; + atomic_uint writers; + atomic_flag writer; +} +LG_RWLock; + +#define LG_RWLOCK_INIT(x) do { \ + atomic_init(&(x).readers, 0); \ + atomic_init(&(x).writers, 0); \ + atomic_flag_clear(&(x).writer); \ +} while (0) + +#define LG_LOCK_SHARED(x) do { \ + for (;;) \ + { \ + while (atomic_load_explicit( \ + &(x).writers, memory_order_seq_cst)) { ; } \ + atomic_fetch_add_explicit(&(x).readers, 1, memory_order_seq_cst); \ + if (!atomic_load_explicit(&(x).writers, memory_order_seq_cst)) \ + break; \ + atomic_fetch_sub_explicit(&(x).readers, 1, memory_order_seq_cst); \ + } \ +} while (0) + +#define LG_UNLOCK_SHARED(x) \ + atomic_fetch_sub_explicit(&(x).readers, 1, memory_order_seq_cst) + +#define LG_LOCK_EXCLUSIVE(x) do { \ + atomic_fetch_add_explicit(&(x).writers, 1, memory_order_seq_cst); \ + while (atomic_flag_test_and_set_explicit( \ + &(x).writer, memory_order_seq_cst)) { ; } \ + while (atomic_load_explicit( \ + &(x).readers, memory_order_seq_cst)) { ; } \ +} while (0) + +#define LG_UNLOCK_EXCLUSIVE(x) do { \ + atomic_flag_clear_explicit(&(x).writer, memory_order_seq_cst); \ + atomic_fetch_sub_explicit(&(x).writers, 1, memory_order_seq_cst); \ +} while (0) + +#define LG_RWLOCK_FREE(x) + #define INTERLOCKED_INC(x) atomic_fetch_add((x), 1) #define INTERLOCKED_DEC(x) atomic_fetch_sub((x), 1)