Skip to content

Instantly share code, notes, and snippets.

@petersvp
Created September 1, 2022 12:24
Show Gist options
  • Save petersvp/5e0a60c3d6e1ba6b7685807abf4ad799 to your computer and use it in GitHub Desktop.
Save petersvp/5e0a60c3d6e1ba6b7685807abf4ad799 to your computer and use it in GitHub Desktop.
Unity + RawInput: Multiple mice + pointers (C++ DLL + C# MonoBehaviour)
/* A RawInput implementation with HWND_MESSAGE window and Device State detection. Uses separate thread and accumulates deltas */
/* You need to compile this single cpp to "LibRawInput.dll" using C++ compiler of choice and Windows SDK. */
/* Then add the DLL to your Unity project as a native plugin. */
#include <atomic>
#include <Windows.h>
#include "stdafx.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
// 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_MESSAGE, 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;
}
// 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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment