Skip to content

Instantly share code, notes, and snippets.

@kwalkerxxi
Forked from unitycoder/MouseInputManager.cs
Created February 6, 2025 12:32
Show Gist options
  • Save kwalkerxxi/7309e49d8604d1c8745f36710c4946b3 to your computer and use it in GitHub Desktop.
Save kwalkerxxi/7309e49d8604d1c8745f36710c4946b3 to your computer and use it in GitHub Desktop.

Revisions

  1. @unitycoder unitycoder revised this gist Jun 9, 2023. 1 changed file with 372 additions and 0 deletions.
    372 changes: 372 additions & 0 deletions RawInput.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,372 @@
    // https://pastebin.com/0Szi8ga6

    /* A RawInput implementation with HWND_MESSAGE window and Device State detection. Uses separate thread and accumulates deltas */

    #include <atomic>
    #include <Windows.h>
    #include "stdafx.h"
    #include "MessageToString.h"
    #include <iostream>
    #include <vector>
    #include <mutex>
    #include <map>
    #include <sstream>
    #include <deque>
    using namespace std;

    // State
    bool isInitialized = false;
    HANDLE runningThread = 0;
    HWND messageWindow = 0;

    // Multi threading
    using Mutex = std::mutex;
    using MutexLocker = std::lock_guard<std::mutex>;
    #define EnsureMutexLocked(T) { if(T.try_lock()) Crash(); }
    #define Crash() { std::cout << "CRASH()"; ((void(*)())0)(); }
    #define StrongAssert(T) { if(!(T)) cout << "Assertion Failed" << ##T << "\n"; Crash(); }

    // RawInput stuff
    const uint8_t RE_DEVICE_CONNECT = 0;
    const uint8_t RE_DEVICE_DISCONNECT = 1;
    const uint8_t RE_MOUSE = 2;

    struct RawInputEvent
    {
    int32_t devHandle;
    int32_t x, y, wheel;
    // pressed button
    uint8_t press;
    //released button
    uint8_t release;
    // event type
    uint8_t type;
    };
    vector<RawInputEvent> generatedEvents; // deltas must be accumulated
    Mutex dataMutex;

    // Mouse state for now
    struct DeviceState
    {
    int32_t x, y, z; // x,y are delts movement, z is WheelDelta
    uint8_t buttonStates; // bad idea for polling
    wstring name;
    };

    // Global buffer for worker thread
    std::vector<char> m_RawInputMessageData; // Buffer

    map<HANDLE, DeviceState> devices;

    void printEvent(RawInputEvent e)
    {
    stringstream ss;
    switch (e.type)
    {
    case RE_DEVICE_CONNECT: ss << "RE_DEVICE_CONNECT"; break;
    case RE_DEVICE_DISCONNECT: ss << "RE_DEVICE_DISCONNECT"; break;
    case RE_MOUSE: ss << "RE_MOUSE"; break;
    default: ss << "UNKNOWN(" << e.type << ")";
    }
    ss << " " << e.devHandle << " (" << e.x << "; " << e.y << ") DOWN=" << int(e.press) << " UP=" << int(e.release) << " w=" << e.wheel << "\n";
    cout << ss.str();
    }

    inline void AddEvent(uint8_t type, int32_t devHandle, uint8_t press, uint8_t release)
    {
    MutexLocker locker(dataMutex);
    RawInputEvent e;
    e.x = 0;
    e.y = 0;
    e.wheel = 0;
    e.type = type;
    e.devHandle = devHandle;
    e.press = press;
    e.release = release;
    generatedEvents.push_back(e);
    printEvent(e);
    }

    inline void AddEvent(RawInputEvent& ev)
    {
    MutexLocker locker(dataMutex);
    generatedEvents.push_back(ev);
    printEvent(ev);
    }

    void OnRawInput(HRAWINPUT handle)
    {
    // Determine the size
    UINT dataSize;
    GetRawInputData(handle, RID_INPUT, NULL, &dataSize, sizeof(RAWINPUTHEADER)); // get Size
    if (dataSize == 0) return;
    if (dataSize > m_RawInputMessageData.size()) m_RawInputMessageData.resize(dataSize);

    // Get the Data
    void* dataBuf = &m_RawInputMessageData[0];
    GetRawInputData(handle, RID_INPUT, dataBuf, &dataSize, sizeof(RAWINPUTHEADER)); // get Data
    const RAWINPUT *raw = (const RAWINPUT*)dataBuf;

    // Mouse
    //if (raw->header.dwType == RIM_TYPEMOUSE)

    HANDLE deviceHandle = raw->header.hDevice;
    const RAWMOUSE& mouseData = raw->data.mouse;

    USHORT flags = mouseData.usButtonFlags;
    short wheelDelta = (short)mouseData.usButtonData;
    LONG x = mouseData.lLastX, y = mouseData.lLastY;

    // Some events are critical
    if(flags & RI_MOUSE_LEFT_BUTTON_DOWN ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 1, 0);
    if(flags & RI_MOUSE_LEFT_BUTTON_UP ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 0, 1);
    if(flags & RI_MOUSE_MIDDLE_BUTTON_DOWN ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 3, 0);
    if(flags & RI_MOUSE_MIDDLE_BUTTON_UP ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 0, 3);
    if(flags & RI_MOUSE_RIGHT_BUTTON_DOWN ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 2, 0);
    if(flags & RI_MOUSE_RIGHT_BUTTON_UP ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 0, 2);


    // Some are to be accumulated
    auto& dev = devices[raw->header.hDevice];
    dev.x += x;
    dev.y += y;
    dev.z += wheelDelta;

    /*
    wprintf(
    L"Mouse: Device=0x%08X, Flags=%04x, WheelDelta=%d, X=%d, Y=%d\n",
    deviceHandle, flags, wheelDelta, x, y);
    /**/
    }



    void OnDeviceChange(HRAWINPUT handle, bool connected)
    {
    if (!connected)
    {
    RawInputEvent ev;
    ev.devHandle = int32_t(handle);
    ev.type = connected ? RE_DEVICE_CONNECT : RE_DEVICE_DISCONNECT;
    ev.x = 0;
    ev.y = 0;
    AddEvent(ev);
    MutexLocker locker(dataMutex);
    devices.erase(handle);
    return;
    }

    // Determine the size, Get Device Name
    std::vector<wchar_t> deviceNameData;
    wstring deviceName;
    UINT dataSize;
    SetLastError(0);
    GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, nullptr, &dataSize);
    if (GetLastError()) return;
    if (dataSize)
    {
    deviceNameData.resize(dataSize);
    UINT result = GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, &deviceNameData[0], &dataSize);
    if (result != UINT_MAX)
    {
    deviceName.assign(deviceNameData.begin(), deviceNameData.end());
    wprintf(L" Name=%s\n", deviceName.c_str());
    }
    }

    RID_DEVICE_INFO deviceInfo;
    deviceInfo.cbSize = sizeof deviceInfo;
    dataSize = sizeof deviceInfo;
    UINT result = GetRawInputDeviceInfo(handle, RIDI_DEVICEINFO, &deviceInfo, &dataSize);
    if (result != UINT_MAX)
    {
    wprintf(L" Id=%u, Buttons=%u, SampleRate=%u, HorizontalWheel=%s\n",
    deviceInfo.mouse.dwId,
    deviceInfo.mouse.dwNumberOfButtons,
    deviceInfo.mouse.dwSampleRate,
    deviceInfo.mouse.fHasHorizontalWheel ? L"1" : L"0");

    // At this perfect moment, add OR remove the device
    RawInputEvent ev;
    ev.devHandle = int32_t(handle);
    ev.type = RE_DEVICE_CONNECT;
    ev.x = 0;
    ev.y = 0;
    AddEvent(ev);
    MutexLocker locker(dataMutex);
    devices[handle].name = deviceName;
    }
    }

    LRESULT CALLBACK RawInputWndProc(HWND wh, UINT msg, WPARAM wp, LPARAM lp)
    {
    // Debugging Message pumps
    //cout << WMMessageToStr(msg, true) << ": W= " << wp << "; L= " << lp << "\n";

    // 254: WM_INPUT_DEVICE_CHANGE
    // wp = 1 GIDC_ARRIVAL
    // wp = 2 GIDC_REMOVAL

    // 255: WM_INPUT

    if (msg == WM_INPUT_DEVICE_CHANGE)
    {
    if (wp==1)
    {
    OnDeviceChange((HRAWINPUT)lp, true);
    }
    else if (wp == 2)
    {
    OnDeviceChange((HRAWINPUT)lp, false);
    }
    }
    else if (msg == WM_INPUT)
    {
    OnRawInput((HRAWINPUT)lp);
    }

    return DefWindowProc(wh, msg, wp, lp);
    }

    static const wchar_t* class_name = L"PI_DEV_RAWINPUT";
    void RawInputThread(LPVOID params)
    {
    WNDCLASSEX wx = {};
    wx.cbSize = sizeof(WNDCLASSEX);
    wx.lpfnWndProc = RawInputWndProc;
    wx.hInstance = NULL;
    wx.lpszClassName = class_name;
    HWND wh;
    if (RegisterClassEx(&wx))
    {
    wh = CreateWindowEx(0, class_name, L"Pi-Dev RawInput [NS]", 0, 0, 0, 0, 0, HWND_DESKTOP, NULL, NULL, NULL);
    messageWindow = wh;
    ShowWindow(wh, SW_SHOWMINNOACTIVE);

    RAWINPUTDEVICE device[4];

    // Mouse
    device[0].usUsagePage = 0x01;
    device[0].usUsage = 0x02;
    device[0].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
    device[0].hwndTarget = wh;

    // Gamepad
    device[1].usUsagePage = 0x01;
    device[1].usUsage = 0x05;
    device[1].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
    device[1].hwndTarget = wh;

    // Joystick
    device[2].usUsagePage = 0x01;
    device[2].usUsage = 0x04;
    device[2].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
    device[2].hwndTarget = wh;

    // Keyboard
    device[3].usUsagePage = 0x01;
    device[3].usUsage = 0x06;
    device[3].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
    device[3].hwndTarget = wh;

    // Register ONLY Mice
    RegisterRawInputDevices(device, 1, sizeof RAWINPUTDEVICE);


    MSG msg;
    while (GetMessage(&msg, 0, 0, 0) > 0)
    {
    DispatchMessage(&msg);
    }
    }
    }

    extern "C" __declspec(dllexport) int kill()
    {
    SetLastError(0);
    PostThreadMessage(GetThreadId(runningThread), WM_QUIT, 0, 0);
    cout << "PostThreadMessage = " << GetLastError() << "\n";

    SetLastError(0);
    UnregisterClass(class_name, NULL);
    cout << "UnregisterClass = " << GetLastError() << "\n";
    /**/

    messageWindow = 0;
    runningThread = 0;
    isInitialized = false;
    return GetLastError();
    }

    extern "C" __declspec(dllexport) bool init()
    {
    kill();
    // this is actually reinit()
    cout << "init()";
    MutexLocker locker(dataMutex);
    devices.clear();
    generatedEvents.clear();

    if (!isInitialized)
    {
    isInitialized = true;
    runningThread = CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(RawInputThread), NULL, 0, 0);
    messageWindow = 0;
    return true;
    }

    return false;
    }

    extern "C" __declspec(dllexport) void* poll()
    {
    MutexLocker locker(dataMutex);
    //cout << "==== Deltas =====\n";
    int numItems = generatedEvents.size();
    stringstream ss;
    for (auto& d : devices)
    {
    RawInputEvent e;
    e.devHandle = int32_t(d.first);
    auto& data = d.second;
    e.press = 0;
    e.release = 0;
    e.type = RE_MOUSE;
    e.wheel = data.z;
    e.x = data.x;
    e.y = data.y;
    if (e.x != 0 || e.y != 0)
    {
    ss.write((char*)&e, sizeof(RawInputEvent));
    ++numItems;
    }
    cout << e.x << "; " << e.y << "\n";
    // Zero accumulation fields
    data.x = 0;
    data.y = 0;
    data.z = 0;
    }
    ss.write((char*)generatedEvents.data(), sizeof(RawInputEvent)*generatedEvents.size());
    uint8_t* buf = (uint8_t*)CoTaskMemAlloc(4 + numItems*sizeof(RawInputEvent));
    memcpy(buf, &numItems, 4);
    memcpy(buf+4, ss.str().data(), numItems*sizeof(RawInputEvent));
    generatedEvents.clear();
    return buf;
    }

    int main()
    {
    cout << "sz = " << sizeof(RawInputEvent) << "\n";
    init();
    while (true)
    {
    if (GetAsyncKeyState(VK_HOME)) cout << "init() = " << init() << "\n";
    if (GetAsyncKeyState(VK_END)) cout << "kill() = " << kill() << "\n";
    Sleep(1000);
    void* data = poll();
    int d = 0;
    memcpy(&d, data, 4);
    //cout << d << "\n";
    }
    return 0;
    }
  2. @unitycoder unitycoder created this gist Jun 9, 2023.
    275 changes: 275 additions & 0 deletions MouseInputManager.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,275 @@
    // https://pastebin.com/4h3CqpYy

    // Unity PINVOKE interface for pastebin.com/0Szi8ga6
    // Handles multiple cursors
    // License: CC0

    using UnityEngine;
    using System.Collections;
    using System.Runtime.InteropServices;
    using System;
    using System.Collections.Generic;
    using UnityEngine.UI;

    public class MouseInputManager : MonoBehaviour
    {
    public static MouseInputManager instance;

    [DllImport("LibRawInput")]
    private static extern bool init();

    [DllImport("LibRawInput")]
    private static extern bool kill();

    [DllImport("LibRawInput")]
    private static extern IntPtr poll();

    public const byte RE_DEVICE_CONNECT = 0;
    public const byte RE_MOUSE = 2;
    public const byte RE_DEVICE_DISCONNECT = 1;
    public string getEventName(byte id)
    {
    switch (id)
    {
    case RE_DEVICE_CONNECT: return "RE_DEVICE_CONNECT";
    case RE_DEVICE_DISCONNECT: return "RE_DEVICE_DISCONNECT";
    case RE_MOUSE: return "RE_MOUSE";
    }
    return "UNKNOWN(" + id + ")";
    }

    public GameObject cursor;
    public Color[] colors;
    public float defaultMiceSensitivity = 1f;
    public float accelerationThreshold = 40;
    public float accelerationMultiplier = 2;
    public int screenBorderPixels = 16;

    [StructLayout(LayoutKind.Sequential)]
    public struct RawInputEvent
    {
    public int devHandle;
    public int x, y, wheel;
    public byte press;
    public byte release;
    public byte type;
    }

    public class MousePointer
    {
    public GameObject obj;
    public Vector2 position;
    public int deviceID;
    public int playerID;
    public float sensitivity;
    }
    Dictionary<int, MousePointer> pointersByDeviceId = new Dictionary<int, MousePointer>();
    Dictionary<int, MousePointer> pointersByPlayerId = new Dictionary<int, MousePointer>();
    int nextPlayerId = 1;
    int miceCount = 0;

    Canvas canvas;
    RectTransform canvasRect;
    float width, height;

    void Start()
    {
    instance = this;
    bool res = init();
    Debug.Log("Init() ==> " + res);
    Debug.Log(Marshal.SizeOf(typeof(RawInputEvent)));
    canvas = GetComponent<Canvas>();
    canvasRect = GetComponent<RectTransform>();
    //enterSingleMode();
    }

    public void OnDestroy()
    {
    instance = null;
    }

    int addCursor(int deviceId)
    {
    if(!isInit)
    {
    Debug.LogError("Not initialized");
    return -1;
    }

    MousePointer mp = null;
    pointersByDeviceId.TryGetValue(deviceId, out mp);
    if(mp != null)
    {
    Debug.LogError("This device already has a cursor");
    return -1;
    }

    Debug.Log("Adding DeviceID " + deviceId);
    mp = new MousePointer();
    mp.playerID = nextPlayerId++;
    pointersByDeviceId[deviceId] = mp;
    pointersByPlayerId[mp.playerID] = mp;
    mp.position = new Vector3(width / 2, height / 2, 0);

    mp.obj = Instantiate(cursor, transform) as GameObject;
    var rt = mp.obj.GetComponent<RectTransform>();
    rt.position = mp.position;

    var spriteComp = mp.obj.GetComponent<Image>();
    if (spriteComp) spriteComp.color = colors[mp.playerID % colors.Length];

    ++miceCount;
    return mp.playerID;
    }

    void deleteCursor(int deviceId)
    {
    --miceCount;
    var mp = pointersByDeviceId[deviceId];
    pointersByDeviceId.Remove(mp.deviceID);
    pointersByPlayerId.Remove(mp.playerID);
    Destroy(mp.obj);
    }

    bool _isMultiplayer = true;
    MousePointer _spPointer;

    [SerializeField]
    public bool isMultiplayer
    {
    set
    {
    if (!value) enterSingleMode(); else enterMultipleMode();
    _isMultiplayer = value;
    }
    get { return _isMultiplayer; }
    }

    void enterSingleMode()
    {
    clearCursorsAndDevices();
    --nextPlayerId;
    addCursor(0);
    _spPointer = pointersByDeviceId[0];
    Cursor.lockState = CursorLockMode.None;
    Cursor.visible = false;
    }

    void enterMultipleMode()
    {
    _spPointer = null;
    nextPlayerId = 0;
    clearCursorsAndDevices();
    Cursor.lockState = CursorLockMode.Locked;
    Cursor.visible = false;
    }

    void clearCursorsAndDevices()
    {
    pointersByDeviceId.Clear();
    pointersByPlayerId.Clear();
    nextPlayerId = 1;
    miceCount = 0;
    foreach (Transform t in transform) Destroy(t.gameObject);
    }

    public MousePointer getByPlayerId(int id)
    {
    MousePointer res = null;
    pointersByPlayerId.TryGetValue(id, out res);
    return res;
    }

    // Update is called once per frame
    int lastEvents = 0;
    bool isInit = true;
    void Update()
    {
    // Keyboard controls debug
    if (Input.GetKeyDown(KeyCode.R))
    {
    if (isInit)
    {
    clearCursorsAndDevices();
    kill();
    isInit = false;
    }
    else
    {
    init();
    isInit = true;
    }
    }

    if (Input.GetKeyDown(KeyCode.M))
    {
    isMultiplayer = !isMultiplayer;
    }

    // SP
    if (!_isMultiplayer)
    {
    var rt = _spPointer.obj.GetComponent<RectTransform>();
    rt.position = Input.mousePosition;
    }
    else
    {
    // MP
    width = canvasRect.rect.width;
    height = canvasRect.rect.height;
    var left = -width / 2;
    var right = width / 2;
    var top = -height / 2;
    var bottom = height / 2;
    //Debug.Log("left=" + left + ", right=" + right + ", top=" + top + ", bottom=" + bottom);

    // Poll the events and properly update whatever we need
    IntPtr data = poll();
    int numEvents = Marshal.ReadInt32(data);
    if (numEvents > 0) lastEvents = numEvents;
    for (int i = 0; i < numEvents; ++i)
    {
    var ev = new RawInputEvent();
    long offset = data.ToInt64() + sizeof(int) + i * Marshal.SizeOf(ev);
    ev.devHandle = Marshal.ReadInt32(new IntPtr(offset + 0));
    ev.x = Marshal.ReadInt32(new IntPtr(offset + 4));
    ev.y = Marshal.ReadInt32(new IntPtr(offset + 8));
    ev.wheel = Marshal.ReadInt32(new IntPtr(offset + 12));
    ev.press = Marshal.ReadByte(new IntPtr(offset + 16));
    ev.release = Marshal.ReadByte(new IntPtr(offset + 17));
    ev.type = Marshal.ReadByte(new IntPtr(offset + 18));
    //Debug.Log(getEventName(ev.type) + ": H=" + ev.devHandle + "; (" + ev.x + ";" + ev.y + ") Down=" + ev.press + " Up=" + ev.release);

    if (ev.type == RE_DEVICE_CONNECT) addCursor(ev.devHandle);
    else if (ev.type == RE_DEVICE_DISCONNECT) deleteCursor(ev.devHandle);
    else if (ev.type == RE_MOUSE)
    {
    MousePointer pointer = null;
    if (pointersByDeviceId.TryGetValue(ev.devHandle, out pointer))
    {
    float dx = ev.x * defaultMiceSensitivity;
    float dy = ev.y * defaultMiceSensitivity;
    if (Mathf.Abs(dx) > accelerationThreshold) dx *= accelerationMultiplier;
    if (Mathf.Abs(dy) > accelerationThreshold) dy *= accelerationMultiplier;
    pointer.position = new Vector2(
    Mathf.Clamp(pointer.position.x + dx, screenBorderPixels, width - screenBorderPixels),
    Mathf.Clamp(pointer.position.y - dy, screenBorderPixels, height - screenBorderPixels));
    RectTransform rt = pointer.obj.GetComponent<RectTransform>();
    rt.position = pointer.position;
    }
    else
    {
    Debug.Log("Unknown device found");
    addCursor(ev.devHandle);
    }
    }
    }
    Marshal.FreeCoTaskMem(data);
    }
    }

    void OnApplicationQuit()
    {
    kill();
    }
    }