Skip to content

Commit

Permalink
Add GetRawInputBuffer handling for RawKeyInput
Browse files Browse the repository at this point in the history
Slightly speeds up windows keyboard handling as it reduces syscalls
  • Loading branch information
CasualPokePlayer committed Dec 1, 2024
1 parent 9d3fc9c commit 6f0ae71
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 47 deletions.
171 changes: 127 additions & 44 deletions src/BizHawk.Bizware.Input/KeyInput/RawKeyInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ internal sealed class RawKeyInput : IKeyInput
private const int WM_INPUT = 0x00FF;

private IntPtr RawInputWindow;
private bool HandleAltKbLayouts;
private List<KeyEvent> KeyEvents = new();
private readonly object LockObj = new();
private bool _handleAltKbLayouts;
private List<KeyEvent> _keyEvents = [ ];
private readonly object _lockObj = new();
private bool _disposed;

private IntPtr RawInputBuffer;
private int RawInputBufferSize;
private readonly int RawInputBufferDataOffset;

private static readonly WNDPROC _wndProc = WndProc;

Expand Down Expand Up @@ -52,71 +57,120 @@ private static unsafe IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntP
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}

GCHandle handle;
RawKeyInput rawKeyInput;
if (uMsg != WM_INPUT)
{
if (uMsg == WM_CLOSE)
{
SetWindowLongPtrW(hWnd, GWLP_USERDATA, IntPtr.Zero);
GCHandle.FromIntPtr(ud).Free();
handle = GCHandle.FromIntPtr(ud);
rawKeyInput = (RawKeyInput)handle.Target;
Marshal.FreeCoTaskMem(rawKeyInput.RawInputBuffer);
handle.Free();
}

return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}

if (GetRawInputData(lParam, RID.INPUT, IntPtr.Zero,
out var size, Marshal.SizeOf<RAWINPUTHEADER>()) == -1)
out var size, sizeof(RAWINPUTHEADER)) == -1)
{
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}

// don't think size should ever be this big, but just in case
// also, make sure to align the buffer to a pointer boundary
var buffer = size > 1024
? new byte[size]
: stackalloc byte[size];
? new IntPtr[(size + sizeof(IntPtr) - 1) / sizeof(IntPtr)]
: stackalloc IntPtr[(size + sizeof(IntPtr) - 1) / sizeof(IntPtr)];

handle = GCHandle.FromIntPtr(ud);
rawKeyInput = (RawKeyInput)handle.Target;

fixed (byte* p = buffer)
fixed (IntPtr* p = buffer)
{
var input = (RAWINPUT*)p;

if (GetRawInputData(lParam, RID.INPUT, input,
ref size, Marshal.SizeOf<RAWINPUTHEADER>()) == -1)
ref size, sizeof(RAWINPUTHEADER)) == -1)
{
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}

if (input->header.dwType is RAWINPUTHEADER.RIM_TYPE.KEYBOARD
&& (input->data.keyboard.Flags & ~(RAWKEYBOARD.RI_KEY.E0 | RAWKEYBOARD.RI_KEY.BREAK)) is 0)
if (input->header.dwType == RAWINPUTHEADER.RIM_TYPE.KEYBOARD)
{
rawKeyInput.AddKeyInput(&input->data.keyboard);
}
}

while (true)
{
var rawInputBuffer = (RAWINPUT*)rawKeyInput.RawInputBuffer;
size = rawKeyInput.RawInputBufferSize;
var count = GetRawInputBuffer(rawInputBuffer, ref size, sizeof(RAWINPUTHEADER));
if (count == 0)
{
var handle = GCHandle.FromIntPtr(ud);
var rawKeyInput = (RawKeyInput)handle.Target;
break;
}

lock (rawKeyInput.LockObj)
if (count == -1)
{
// From testing, it appears this never actually occurs in practice
// As GetRawInputBuffer will succeed as long as the buffer has room for at least 1 packet
// As such, initial size is made very large to hopefully accommodate all packets at once
const int ERROR_INSUFFICIENT_BUFFER = 0x7A;
if (Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER)
{
var rawKey = (RawKey)(input->data.keyboard.MakeCode |
((input->data.keyboard.Flags & RAWKEYBOARD.RI_KEY.E0) != 0 ? 0x80 : 0));

// kind of a dumb hack, the Pause key is apparently special here
// keyboards would send scancode 0x1D with an E1 prefix, followed by 0x45 (which is NumLock!)
// this "NumLock" press will set the VKey to 255 (invalid VKey), so we can use that to know if this is actually a Pause press
// (note that DIK_PAUSE is just 0x45 with an E0 prefix, although this is likely just a conversion DirectInput does internally)
if (rawKey == RawKey.NUMLOCK && input->data.keyboard.VKey == VirtualKey.VK_NONE)
{
rawKey = RawKey.PAUSE;
}

if (rawKeyInput.HandleAltKbLayouts)
{
rawKey = MapToRealKeyViaScanCode(rawKey);
}

if (KeyEnumMap.TryGetValue(rawKey, out var key))
{
rawKeyInput.KeyEvents.Add(new(key, (input->data.keyboard.Flags & RAWKEYBOARD.RI_KEY.BREAK) == RAWKEYBOARD.RI_KEY.MAKE));
}
rawKeyInput.RawInputBufferSize *= 2;
rawKeyInput.RawInputBuffer = Marshal.ReAllocCoTaskMem(rawKeyInput.RawInputBuffer, rawKeyInput.RawInputBufferSize);
continue;
}

break;
}

for (var i = 0u; i < (uint)count; i++)
{
if (rawInputBuffer->header.dwType == RAWINPUTHEADER.RIM_TYPE.KEYBOARD)
{
var keyboard = (RAWKEYBOARD*)((byte*)&rawInputBuffer->data.keyboard + rawKeyInput.RawInputBufferDataOffset);
rawKeyInput.AddKeyInput(keyboard);
}

var packetSize = rawInputBuffer->header.dwSize;
var rawInputBufferUnaligned = (nuint)rawInputBuffer + packetSize;
var pointerAlignment = (nuint)sizeof(nuint) - 1;
rawInputBuffer = (RAWINPUT*)((rawInputBufferUnaligned + pointerAlignment) & ~pointerAlignment);
}
}

return IntPtr.Zero;
}

private unsafe void AddKeyInput(RAWKEYBOARD* keyboard)
{
if ((keyboard->Flags & ~(RAWKEYBOARD.RI_KEY.E0 | RAWKEYBOARD.RI_KEY.BREAK)) == 0)
{
var rawKey = (RawKey)(keyboard->MakeCode | ((keyboard->Flags & RAWKEYBOARD.RI_KEY.E0) != 0 ? 0x80 : 0));

// kind of a dumb hack, the Pause key is apparently special here
// keyboards would send scancode 0x1D with an E1 prefix, followed by 0x45 (which is NumLock!)
// this "NumLock" press will set the VKey to 255 (invalid VKey), so we can use that to know if this is actually a Pause press
// (note that DIK_PAUSE is just 0x45 with an E0 prefix, although this is likely just a conversion DirectInput does internally)
if (rawKey == RawKey.NUMLOCK && keyboard->VKey == VirtualKey.VK_NONE)
{
rawKey = RawKey.PAUSE;
}

if (_handleAltKbLayouts)
{
rawKey = MapToRealKeyViaScanCode(rawKey);
}

return DefRawInputProc(input, 0, Marshal.SizeOf<RAWINPUTHEADER>());
if (KeyEnumMap.TryGetValue(rawKey, out var key))
{
_keyEvents.Add(new(key, (keyboard->Flags & RAWKEYBOARD.RI_KEY.BREAK) == RAWKEYBOARD.RI_KEY.MAKE));
}
}
}

Expand Down Expand Up @@ -168,48 +222,77 @@ public RawKeyInput()
// but we can still test window creation
var testWindow = CreateRawInputWindow(); // this will throw if window creation or rawinput registering fails
DestroyWindow(testWindow);

// 32-bit dumb: GetRawInputBuffer packets are directly copied from the kernel
// so on WOW64 they will have 64-bit headers (i.e. 64 bit handles, not 32 bit)
if (IntPtr.Size == 4)
{
var currentProccess = Win32Imports.GetCurrentProcess();
if (!Win32Imports.IsWow64Process(currentProccess, out var isWow64))
{
throw new InvalidOperationException("Failed to query WOW64 status");
}

RawInputBufferDataOffset = isWow64 ? 8 : 0;
}

RawInputBufferSize = (Marshal.SizeOf<RAWINPUT>() + RawInputBufferDataOffset) * 16;
RawInputBuffer = Marshal.AllocCoTaskMem(RawInputBufferSize);
}

public void Dispose()
{
lock (LockObj)
lock (_lockObj)
{
if (RawInputWindow != IntPtr.Zero)
{
// Can't use DestroyWindow, that's only allowed in the thread that created the window!
PostMessageW(RawInputWindow, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
RawInputWindow = IntPtr.Zero;
}
else
{
// We'll free RawInputBuffer the raw input window message handler if it's around
// Otherwise, just do it here
Marshal.FreeCoTaskMem(RawInputBuffer);
}

_disposed = true;
}
}

public IEnumerable<KeyEvent> Update(bool handleAltKbLayouts)
{
lock (LockObj)
lock (_lockObj)
{
if (_disposed)
{
return [ ];
}

if (RawInputWindow == IntPtr.Zero)
{
RawInputWindow = CreateRawInputWindow();
var handle = GCHandle.Alloc(this, GCHandleType.Normal);
SetWindowLongPtrW(RawInputWindow, GWLP_USERDATA, GCHandle.ToIntPtr(handle));
}

HandleAltKbLayouts = handleAltKbLayouts;
_handleAltKbLayouts = handleAltKbLayouts;

while (PeekMessageW(out var msg, RawInputWindow, 0, 0, PM_REMOVE))
{
TranslateMessage(ref msg);
DispatchMessageW(ref msg);
}

var ret = KeyEvents;
KeyEvents = new();
var ret = _keyEvents;
_keyEvents = [ ];
return ret;
}
}

private static readonly RawKey[] _rawKeysNoTranslation =
{
[
RawKey.NUMPAD0,
RawKey.NUMPAD1,
RawKey.NUMPAD2,
Expand All @@ -230,7 +313,7 @@ public IEnumerable<KeyEvent> Update(bool handleAltKbLayouts)
RawKey.INTL4,
RawKey.LANG3,
RawKey.LANG4
};
];

private static RawKey MapToRealKeyViaScanCode(RawKey key)
{
Expand Down
6 changes: 3 additions & 3 deletions src/BizHawk.Common/Win32/RawInputImports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -481,15 +481,15 @@ public struct RAWINPUT
public RAWINPUTDATA data;
}

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
public static extern unsafe IntPtr DefRawInputProc(RAWINPUT* paRawInput, int nInput, int cbSizeHeader);

[DllImport("user32.dll", ExactSpelling = true)]
public static extern int GetRawInputData(IntPtr hRawInput, RID uiCommand, IntPtr pData, out int bSize, int cbSizeHeader);

[DllImport("user32.dll", ExactSpelling = true)]
public static extern unsafe int GetRawInputData(IntPtr hRawInput, RID uiCommand, RAWINPUT* pData, ref int bSize, int cbSizeHeader);

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
public static extern unsafe int GetRawInputBuffer(RAWINPUT* pData, ref int bSize, int cbSizeHeader);

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RegisterRawInputDevices(ref RAWINPUTDEVICE pRawInputDevice, uint uiNumDevices, int cbSize);
Expand Down
7 changes: 7 additions & 0 deletions src/BizHawk.Common/Win32/Win32Imports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,12 @@ public enum TPM

[DllImport("user32.dll", ExactSpelling = true)]
public static extern int TrackPopupMenuEx(IntPtr hmenu, TPM fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm);

[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GetCurrentProcess();

[DllImport("kernel32.dll", ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWow64Process(IntPtr hProcess, [MarshalAs(UnmanagedType.Bool)] out bool Wow64Process);
}
}

0 comments on commit 6f0ae71

Please sign in to comment.