From d3f6460fdf95bf7549f4aabe2f2ddad65264f107 Mon Sep 17 00:00:00 2001 From: Babib3l Date: Tue, 24 Mar 2026 22:45:37 +0100 Subject: [PATCH] Fix macOS Caps Lock rebinding by buffering one-shot key-down events during keyboard assignment --- .../Assigner/KeyboardKeyAssigner.cs | 29 +++++--- src/Ryujinx.Input/IKeyboard.cs | 11 +++ src/Ryujinx/Input/AvaloniaKeyboard.cs | 13 ++++ src/Ryujinx/Input/AvaloniaKeyboardDriver.cs | 67 ++++++++++++++++--- 4 files changed, 104 insertions(+), 16 deletions(-) diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index 1f626625f..186bbdd55 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -8,22 +8,28 @@ namespace Ryujinx.Input.Assigner private readonly IKeyboard _keyboard; private KeyboardStateSnapshot _keyboardState; + private Button? _pressedButton; public KeyboardKeyAssigner(IKeyboard keyboard) { _keyboard = keyboard; } - public void Initialize() { } + public void Initialize() + { + _pressedButton = null; + } public void ReadInput() { _keyboardState = _keyboard.GetKeyboardStateSnapshot(); + + _pressedButton ??= GetPressedButtonFromState() ?? GetPressedButtonFromBufferedPress(); } public bool IsAnyButtonPressed() { - return GetPressedButton() is not null; + return _pressedButton is not null; } public bool ShouldCancel() @@ -32,26 +38,33 @@ namespace Ryujinx.Input.Assigner } public Button? GetPressedButton() + { + return !ShouldCancel() ? _pressedButton : null; + } + + private Button? GetPressedButtonFromState() { Key aliasedKey = GetAliasedPressedKey(); if (aliasedKey != Key.Unknown) { - return !ShouldCancel() ? new Button(aliasedKey) : null; + return new Button(aliasedKey); } - Button? keyPressed = null; - for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = new(key); - break; + return new Button(key); } } - return !ShouldCancel() ? keyPressed : null; + return null; + } + + private Button? GetPressedButtonFromBufferedPress() + { + return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null; } private Key GetAliasedPressedKey() diff --git a/src/Ryujinx.Input/IKeyboard.cs b/src/Ryujinx.Input/IKeyboard.cs index 74ac50457..9476fb1aa 100644 --- a/src/Ryujinx.Input/IKeyboard.cs +++ b/src/Ryujinx.Input/IKeyboard.cs @@ -44,5 +44,16 @@ namespace Ryujinx.Input return new KeyboardStateSnapshot(_keyState); } + + /// + /// Try to consume a recently pressed key. + /// + /// The pressed key, if available. + /// True if a key press was consumed. + bool TryConsumePressedKey(out Key key) + { + key = Key.Unknown; + return false; + } } } diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index bc36d357e..c31a317d1 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -98,6 +98,19 @@ namespace Ryujinx.Ava.Input } } + public bool TryConsumePressedKey(out Key key) + { + try + { + return _driver.TryConsumePressedKey(_mode, out key); + } + catch + { + key = Key.Unknown; + return false; + } + } + public void SetConfiguration(InputConfig configuration) { lock (_userMappingLock) diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs index f7f9858ec..9925330f3 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs @@ -4,6 +4,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Input; using System; using System.Collections.Generic; +using System.Threading; using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; using Key = Ryujinx.Input.Key; @@ -16,6 +17,9 @@ namespace Ryujinx.Ava.Input private readonly HashSet _semanticPressedKeys; private readonly HashSet _physicalPressedKeys; private readonly Dictionary _observedPhysicalKeysBySemanticKey; + private readonly Queue _semanticPressedKeyQueue; + private readonly Queue _physicalPressedKeyQueue; + private readonly Lock _pressedKeyQueueLock; private readonly KeyboardInputMode _defaultMode; public event EventHandler KeyPressed; @@ -31,6 +35,9 @@ namespace Ryujinx.Ava.Input _semanticPressedKeys = []; _physicalPressedKeys = []; _observedPhysicalKeysBySemanticKey = []; + _semanticPressedKeyQueue = []; + _physicalPressedKeyQueue = []; + _pressedKeyQueueLock = new(); _defaultMode = defaultMode; _control.KeyDown += OnKeyPress; @@ -108,20 +115,46 @@ namespace Ryujinx.Ava.Input internal void Clear(KeyboardInputMode mode) { - if (mode == KeyboardInputMode.Physical) + lock (_pressedKeyQueueLock) { - _physicalPressedKeys.Clear(); - } - else - { - _semanticPressedKeys.Clear(); + if (mode == KeyboardInputMode.Physical) + { + _physicalPressedKeys.Clear(); + _physicalPressedKeyQueue.Clear(); + } + else + { + _semanticPressedKeys.Clear(); + _semanticPressedKeyQueue.Clear(); + } } } public void Clear() { - _semanticPressedKeys.Clear(); - _physicalPressedKeys.Clear(); + lock (_pressedKeyQueueLock) + { + _semanticPressedKeys.Clear(); + _physicalPressedKeys.Clear(); + _semanticPressedKeyQueue.Clear(); + _physicalPressedKeyQueue.Clear(); + } + } + + internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key) + { + lock (_pressedKeyQueueLock) + { + Queue queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue; + + if (queue.TryDequeue(out key)) + { + return true; + } + } + + key = Key.Unknown; + return false; } private static void UpdateKeyState(HashSet pressedKeys, Key key, bool isPressed) @@ -161,10 +194,28 @@ namespace Ryujinx.Ava.Input Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key); Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key); ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey); + bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey); + bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey); UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed); UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed); + if (isPressed) + { + lock (_pressedKeyQueueLock) + { + if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound) + { + _semanticPressedKeyQueue.Enqueue(resolvedSemanticKey); + } + + if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound) + { + _physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey); + } + } + } + if (isPressed && semanticKey is not Key.Unknown and not Key.Unbound && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)