Files
DoomRTX/neo/engine/opengl/DirectInputShim.cpp
T
Justin Marshall 647911d693 Added license.
2026-05-14 18:38:14 -07:00

1660 lines
44 KiB
C++

/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
IceTech Source Code 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 3 of the License, or
(at your option) any later version.
IceTech Source Code 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 IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
If you have questions concerning this license or the applicable additional terms, you may contact in writing Justin Marshall, justinmarshall20@gmail.com
===========================================================================
*/
#ifndef DIRECTINPUT_VERSION
#define DIRECTINPUT_VERSION 0x0800
#endif
#include "DirectInputShim.h"
#include <windows.h>
#include <dinput.h>
#include <algorithm>
#include <atomic>
#include <mutex>
#include <deque>
#include <vector>
#include <cstring>
#include <cstdio>
#include <stdint.h>
#ifndef ICE_DI_DEFAULT_BUFFER_SIZE
#define ICE_DI_DEFAULT_BUFFER_SIZE 256
#endif
#ifndef ICE_DI_CENTER_MOUSE_WHEN_ACTIVE
#define ICE_DI_CENTER_MOUSE_WHEN_ACTIVE 1
#endif
#ifndef ICE_DI_CENTER_WARP_IGNORE_MS
#define ICE_DI_CENTER_WARP_IGNORE_MS 120
#endif
#ifndef ICE_DI_CENTER_PIXEL_TOLERANCE
#define ICE_DI_CENTER_PIXEL_TOLERANCE 1
#endif
// Forward declarations
class IceDirectInputDevice8;
class IceDirectInput8;
bool IceDirectInputShim_RegisterRawInput(HWND hwnd);
bool IceDirectInputShim_ForceMouseCenter();
void IceDirectInputShim_SetMouseTargetWindow(HWND hwnd);
void IceDirectInputShim_SetMouseCenterLock(bool enabled);
// Globals
static HWND g_rawInputHwnd = NULL;
static HWND g_keyboardTargetHwnd = NULL;
static HWND g_mouseTargetHwnd = NULL;
static IceDirectInputDevice8* g_keyboardDevice = NULL;
static IceDirectInputDevice8* g_mouseDevice = NULL;
static std::mutex g_deviceMutex;
static std::mutex g_windowMutex;
static std::atomic<bool> g_iceDI_GameMouseActive(true);
static std::atomic<bool> g_iceDI_MouseAcquired(false);
static std::atomic<bool> g_iceDI_CenterMouseWhenActive(ICE_DI_CENTER_MOUSE_WHEN_ACTIVE != 0);
// The shim owns the clip only while the game mouse is active.
static bool g_mouseClipOwned = false;
static RECT g_lastClipRect = { 0, 0, 0, 0 };
// Used only to ignore absolute raw-mouse packets generated by our own recentering.
static POINT g_lastCenterWarpPoint = { 0, 0 };
static DWORD g_lastCenterWarpTick = 0;
// ------------------------------------------------------------
// Helpers
// ------------------------------------------------------------
static DWORD IceDI_GetTimeStamp() {
return GetTickCount();
}
static LONG IceDI_AbsLong(LONG v) {
return v < 0 ? -v : v;
}
static bool IceDI_IsDInputProp(REFGUID rguidProp, DWORD propId) {
// DirectInput DIPROP_* values are not real GUID objects.
// They are encoded as invalid pointer-sized constants:
// #define MAKEDIPROP(prop) (*(const GUID *)(prop))
//
// So do NOT compare DIPROP_* with operator== / IsEqualGUID / memcmp.
return (DWORD)(uintptr_t)&rguidProp == propId;
}
static bool IceDI_IsPressedRawKeyboard(const RAWKEYBOARD& kb) {
return (kb.Flags & RI_KEY_BREAK) == 0;
}
static bool IceDI_IsUsableWindow(HWND hwnd) {
return hwnd != NULL && ::IsWindow(hwnd) != FALSE;
}
static bool IceDI_IsVisibleUsableWindow(HWND hwnd) {
return IceDI_IsUsableWindow(hwnd) && ::IsWindowVisible(hwnd) != FALSE;
}
static bool IceDI_ForegroundOwnsWindow(HWND hwnd) {
if (!IceDI_IsUsableWindow(hwnd)) {
return false;
}
HWND foreground = ::GetForegroundWindow();
if (!IceDI_IsUsableWindow(foreground)) {
return false;
}
if (foreground == hwnd) {
return true;
}
// Embedded case: the Doom/game HWND can be a child of the editor's
// foreground top-level HWND. This is the case that needs live client-to-
// screen conversion instead of stale cached x/y/resolution.
if (::IsChild(foreground, hwnd)) {
return true;
}
HWND root = ::GetAncestor(hwnd, GA_ROOT);
if (root != NULL && root == foreground) {
return true;
}
// Rare, but allow it when a child of our target became foreground.
if (::IsChild(hwnd, foreground)) {
return true;
}
return false;
}
static bool IceDI_GetClientScreenRect(HWND hwnd, RECT* outRect) {
if (outRect == NULL || !IceDI_IsUsableWindow(hwnd)) {
return false;
}
RECT client;
if (!::GetClientRect(hwnd, &client)) {
return false;
}
if ((client.right - client.left) <= 0 || (client.bottom - client.top) <= 0) {
return false;
}
POINT pts[2];
pts[0].x = client.left;
pts[0].y = client.top;
pts[1].x = client.right;
pts[1].y = client.bottom;
if (!::ClientToScreen(hwnd, &pts[0])) {
return false;
}
if (!::ClientToScreen(hwnd, &pts[1])) {
return false;
}
outRect->left = std::min<LONG>(pts[0].x, pts[1].x);
outRect->top = std::min<LONG>(pts[0].y, pts[1].y);
outRect->right = std::max<LONG>(pts[0].x, pts[1].x);
outRect->bottom = std::max<LONG>(pts[0].y, pts[1].y);
return (outRect->right > outRect->left) && (outRect->bottom > outRect->top);
}
static bool IceDI_RectEquals(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right && a.bottom == b.bottom;
}
static HWND IceDI_GetFallbackRawInputWindow_NoLock() {
if (IceDI_IsUsableWindow(g_rawInputHwnd)) {
return g_rawInputHwnd;
}
if (IceDI_IsUsableWindow(g_mouseTargetHwnd)) {
return g_mouseTargetHwnd;
}
if (IceDI_IsUsableWindow(g_keyboardTargetHwnd)) {
return g_keyboardTargetHwnd;
}
return NULL;
}
static HWND IceDI_GetMouseTargetWindow() {
HWND target = NULL;
HWND fallback = NULL;
{
std::lock_guard<std::mutex> lock(g_windowMutex);
target = g_mouseTargetHwnd;
fallback = IceDI_GetFallbackRawInputWindow_NoLock();
}
if (IceDI_IsUsableWindow(target)) {
return target;
}
if (IceDI_IsUsableWindow(fallback)) {
return fallback;
}
// Last-ditch fallback for code paths that acquire before setting
// cooperative level. GetFocus only works reliably inside this thread,
// but it is better than retaining a stale hwnd.
HWND focus = ::GetFocus();
if (IceDI_IsUsableWindow(focus)) {
return focus;
}
return NULL;
}
static bool IceDI_ShouldLockMouseToWindow(HWND hwnd) {
if (!g_iceDI_CenterMouseWhenActive.load()) {
return false;
}
if (!g_iceDI_GameMouseActive.load()) {
return false;
}
if (!g_iceDI_MouseAcquired.load()) {
return false;
}
if (!IceDI_IsVisibleUsableWindow(hwnd)) {
return false;
}
if (!IceDI_ForegroundOwnsWindow(hwnd)) {
return false;
}
return true;
}
static void IceDI_RememberCenterWarp(const POINT& center) {
std::lock_guard<std::mutex> lock(g_windowMutex);
g_lastCenterWarpPoint = center;
g_lastCenterWarpTick = IceDI_GetTimeStamp();
}
static bool IceDI_IsRecentCenterWarpPoint(const POINT& pt) {
POINT lastPoint;
DWORD lastTick;
{
std::lock_guard<std::mutex> lock(g_windowMutex);
lastPoint = g_lastCenterWarpPoint;
lastTick = g_lastCenterWarpTick;
}
if (lastTick == 0) {
return false;
}
DWORD now = IceDI_GetTimeStamp();
if ((DWORD)(now - lastTick) > ICE_DI_CENTER_WARP_IGNORE_MS) {
return false;
}
return IceDI_AbsLong(pt.x - lastPoint.x) <= 2 && IceDI_AbsLong(pt.y - lastPoint.y) <= 2;
}
static void IceDI_ClearMouseClipIfOwned() {
bool shouldClear = false;
{
std::lock_guard<std::mutex> lock(g_windowMutex);
shouldClear = g_mouseClipOwned;
g_mouseClipOwned = false;
g_lastClipRect.left = 0;
g_lastClipRect.top = 0;
g_lastClipRect.right = 0;
g_lastClipRect.bottom = 0;
}
if (shouldClear) {
::ClipCursor(NULL);
}
}
static bool IceDI_ApplyMouseClip(const RECT& rect) {
bool needsClip = true;
{
std::lock_guard<std::mutex> lock(g_windowMutex);
needsClip = !g_mouseClipOwned || !IceDI_RectEquals(g_lastClipRect, rect);
g_mouseClipOwned = true;
g_lastClipRect = rect;
}
// Calling ClipCursor repeatedly is okay, but avoid doing it every poll when
// the rect has not changed. If the editor moves/reparents/resizes the child
// window, the live rect changes and this is applied immediately.
if (needsClip) {
if (!::ClipCursor(&rect)) {
return false;
}
}
return true;
}
static bool IceDI_SkipCenter = false;
void IceDI_ForceOverrideSkipCenter(bool shouldSkip) {
IceDI_SkipCenter = shouldSkip;
}
static bool IceDI_ForceMouseCenterInternal(bool forceWarp) {
HWND hwnd = IceDI_GetMouseTargetWindow();
if (!IceDI_ShouldLockMouseToWindow(hwnd) || IceDI_SkipCenter) {
IceDI_ClearMouseClipIfOwned();
return false;
}
RECT clientScreenRect;
if (!IceDI_GetClientScreenRect(hwnd, &clientScreenRect)) {
IceDI_ClearMouseClipIfOwned();
return false;
}
POINT center;
center.x = clientScreenRect.left + ((clientScreenRect.right - clientScreenRect.left) / 2);
center.y = clientScreenRect.top + ((clientScreenRect.bottom - clientScreenRect.top) / 2);
IceDI_ApplyMouseClip(clientScreenRect);
POINT cursor;
bool haveCursor = (::GetCursorPos(&cursor) != FALSE);
bool outside = !haveCursor || !::PtInRect(&clientScreenRect, cursor);
bool offCenter = !haveCursor ||
IceDI_AbsLong(cursor.x - center.x) > ICE_DI_CENTER_PIXEL_TOLERANCE ||
IceDI_AbsLong(cursor.y - center.y) > ICE_DI_CENTER_PIXEL_TOLERANCE;
if (forceWarp || outside || offCenter) {
IceDI_RememberCenterWarp(center);
::SetCursorPos(center.x, center.y);
}
return true;
}
static bool IceDI_RawAbsoluteMouseToScreen(const RAWMOUSE& rm, POINT* outPoint) {
if (outPoint == NULL) {
return false;
}
int left = 0;
int top = 0;
int width = ::GetSystemMetrics(SM_CXSCREEN);
int height = ::GetSystemMetrics(SM_CYSCREEN);
if (rm.usFlags & MOUSE_VIRTUAL_DESKTOP) {
left = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
top = ::GetSystemMetrics(SM_YVIRTUALSCREEN);
width = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
height = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
}
if (width <= 0 || height <= 0) {
return false;
}
// RAWMOUSE absolute packets are normalized to 0..65535 over the selected
// desktop rectangle. Treating these values as relative deltas is a classic
// cause of huge mouse jumps/skips.
outPoint->x = left + ::MulDiv(rm.lLastX, width - 1, 65535);
outPoint->y = top + ::MulDiv(rm.lLastY, height - 1, 65535);
return true;
}
static DWORD IceDI_RawKeyboardToDIK(const RAWKEYBOARD& kb) {
USHORT makeCode = kb.MakeCode;
USHORT flags = kb.Flags;
USHORT vkey = kb.VKey;
if (makeCode == 0) {
if (vkey == VK_SNAPSHOT) {
return DIK_SYSRQ;
}
if (vkey == VK_PAUSE) {
return DIK_PAUSE;
}
return 0;
}
if (vkey == VK_PAUSE) {
return DIK_PAUSE;
}
if (vkey == VK_SNAPSHOT) {
return DIK_SYSRQ;
}
DWORD dik = makeCode & 0x7F;
if (flags & RI_KEY_E0) {
dik |= 0x80;
}
switch (vkey) {
case VK_RCONTROL:
return DIK_RCONTROL;
case VK_RMENU:
return DIK_RMENU;
case VK_LWIN:
return DIK_LWIN;
case VK_RWIN:
return DIK_RWIN;
case VK_APPS:
return DIK_APPS;
default:
break;
}
return dik;
}
enum iceDIShimDeviceType {
ICE_DI_DEVICE_KEYBOARD,
ICE_DI_DEVICE_MOUSE
};
static bool IceDI_RegisterRawInputForCurrentTargets(HWND fallbackHwnd) {
HWND keyboardTarget = NULL;
HWND mouseTarget = NULL;
{
std::lock_guard<std::mutex> lock(g_windowMutex);
if (IceDI_IsUsableWindow(g_keyboardTargetHwnd)) {
keyboardTarget = g_keyboardTargetHwnd;
}
if (IceDI_IsUsableWindow(g_mouseTargetHwnd)) {
mouseTarget = g_mouseTargetHwnd;
}
if (!IceDI_IsUsableWindow(keyboardTarget) && IceDI_IsUsableWindow(fallbackHwnd)) {
keyboardTarget = fallbackHwnd;
g_keyboardTargetHwnd = fallbackHwnd;
}
if (!IceDI_IsUsableWindow(mouseTarget) && IceDI_IsUsableWindow(fallbackHwnd)) {
mouseTarget = fallbackHwnd;
g_mouseTargetHwnd = fallbackHwnd;
}
if (IceDI_IsUsableWindow(fallbackHwnd)) {
g_rawInputHwnd = fallbackHwnd;
}
else {
g_rawInputHwnd = IceDI_GetFallbackRawInputWindow_NoLock();
}
}
if (!IceDI_IsUsableWindow(keyboardTarget) && !IceDI_IsUsableWindow(mouseTarget)) {
return false;
}
RAWINPUTDEVICE rid[2];
UINT count = 0;
memset(rid, 0, sizeof(rid));
if (IceDI_IsUsableWindow(keyboardTarget)) {
rid[count].usUsagePage = 0x01;
rid[count].usUsage = 0x06;
rid[count].dwFlags = 0;
rid[count].hwndTarget = keyboardTarget;
count++;
}
if (IceDI_IsUsableWindow(mouseTarget)) {
rid[count].usUsagePage = 0x01;
rid[count].usUsage = 0x02;
rid[count].dwFlags = 0;
rid[count].hwndTarget = mouseTarget;
count++;
}
if (count == 0) {
return false;
}
if (!::RegisterRawInputDevices(rid, count, sizeof(RAWINPUTDEVICE))) {
OutputDebugStringA("IceDirectInputShim: RegisterRawInputDevices failed\n");
return false;
}
return true;
}
static void IceDI_SetDeviceTargetWindow(iceDIShimDeviceType type, HWND hwnd) {
if (!IceDI_IsUsableWindow(hwnd)) {
return;
}
{
std::lock_guard<std::mutex> lock(g_windowMutex);
if (type == ICE_DI_DEVICE_KEYBOARD) {
g_keyboardTargetHwnd = hwnd;
}
else {
g_mouseTargetHwnd = hwnd;
}
g_rawInputHwnd = hwnd;
}
IceDI_RegisterRawInputForCurrentTargets(hwnd);
}
// ------------------------------------------------------------
// IceDirectInputDevice8
// ------------------------------------------------------------
class IceDirectInputDevice8 : public IDirectInputDevice8A {
public:
IceDirectInputDevice8(iceDIShimDeviceType type) :
refCount(1),
deviceType(type),
acquired(false),
bufferSize(ICE_DI_DEFAULT_BUFFER_SIZE),
coopFlags(0),
hwnd(NULL),
haveLastAbsoluteMouse(false) {
memset(keyboardState, 0, sizeof(keyboardState));
memset(&mouseState, 0, sizeof(mouseState));
lastAbsoluteMouse.x = 0;
lastAbsoluteMouse.y = 0;
}
virtual ~IceDirectInputDevice8() {
std::lock_guard<std::mutex> lock(g_deviceMutex);
if (g_keyboardDevice == this) {
g_keyboardDevice = NULL;
}
if (g_mouseDevice == this) {
g_mouseDevice = NULL;
g_iceDI_MouseAcquired.store(false);
}
}
// --------------------------------------------------------
// IUnknown
// --------------------------------------------------------
STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) override {
if (ppvObj == NULL) {
return E_POINTER;
}
*ppvObj = NULL;
if (riid == IID_IUnknown || riid == IID_IDirectInputDevice8A) {
*ppvObj = static_cast<IDirectInputDevice8A*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHOD_(ULONG, AddRef)() override {
return ++refCount;
}
STDMETHOD_(ULONG, Release)() override {
ULONG r = --refCount;
if (r == 0) {
delete this;
}
return r;
}
// --------------------------------------------------------
// IDirectInputDevice8A
// --------------------------------------------------------
STDMETHOD(GetCapabilities)(LPDIDEVCAPS lpDIDevCaps) override {
if (lpDIDevCaps == NULL) {
return E_POINTER;
}
if (lpDIDevCaps->dwSize < sizeof(DIDEVCAPS)) {
return DIERR_INVALIDPARAM;
}
DWORD oldSize = lpDIDevCaps->dwSize;
memset(lpDIDevCaps, 0, oldSize);
lpDIDevCaps->dwSize = oldSize;
if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
lpDIDevCaps->dwDevType = DI8DEVTYPE_KEYBOARD;
lpDIDevCaps->dwButtons = 256;
}
else {
lpDIDevCaps->dwDevType = DI8DEVTYPE_MOUSE;
lpDIDevCaps->dwAxes = 3;
lpDIDevCaps->dwButtons = 8;
}
return DI_OK;
}
STDMETHOD(EnumObjects)(LPDIENUMDEVICEOBJECTSCALLBACKA lpCallback, LPVOID pvRef, DWORD dwFlags) override {
if (lpCallback == NULL) {
return DIERR_INVALIDPARAM;
}
if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
for (DWORD i = 0; i < 256; i++) {
DIDEVICEOBJECTINSTANCEA inst;
memset(&inst, 0, sizeof(inst));
inst.dwSize = sizeof(inst);
inst.dwType = DIDFT_BUTTON | DIDFT_MAKEINSTANCE(i);
inst.dwOfs = i;
sprintf_s(inst.tszName, "Key %u", i);
if (lpCallback(&inst, pvRef) == DIENUM_STOP) {
break;
}
}
}
else {
static const DWORD mouseObjects[] = {
DIMOFS_X,
DIMOFS_Y,
DIMOFS_Z,
DIMOFS_BUTTON0,
DIMOFS_BUTTON1,
DIMOFS_BUTTON2,
DIMOFS_BUTTON3,
DIMOFS_BUTTON4,
DIMOFS_BUTTON5,
DIMOFS_BUTTON6,
DIMOFS_BUTTON7
};
for (int i = 0; i < (int)(sizeof(mouseObjects) / sizeof(mouseObjects[0])); i++) {
DIDEVICEOBJECTINSTANCEA inst;
memset(&inst, 0, sizeof(inst));
inst.dwSize = sizeof(inst);
inst.dwOfs = mouseObjects[i];
if (mouseObjects[i] == DIMOFS_X || mouseObjects[i] == DIMOFS_Y || mouseObjects[i] == DIMOFS_Z) {
inst.dwType = DIDFT_AXIS | DIDFT_MAKEINSTANCE(i);
}
else {
inst.dwType = DIDFT_BUTTON | DIDFT_MAKEINSTANCE(i - 3);
}
sprintf_s(inst.tszName, "Mouse Object %d", i);
if (lpCallback(&inst, pvRef) == DIENUM_STOP) {
break;
}
}
}
return DI_OK;
}
STDMETHOD(GetProperty)(REFGUID rguidProp, LPDIPROPHEADER pdiph) override {
if (pdiph == NULL) {
return E_POINTER;
}
if (IceDI_IsDInputProp(rguidProp, 1)) { // DIPROP_BUFFERSIZE
if (pdiph->dwSize < sizeof(DIPROPDWORD)) {
return DIERR_INVALIDPARAM;
}
DIPROPDWORD* prop = reinterpret_cast<DIPROPDWORD*>(pdiph);
prop->dwData = bufferSize;
return DI_OK;
}
return DIERR_UNSUPPORTED;
}
STDMETHOD(SetProperty)(REFGUID rguidProp, LPCDIPROPHEADER pdiph) override {
if (pdiph == NULL) {
return E_POINTER;
}
if (IceDI_IsDInputProp(rguidProp, 1)) { // DIPROP_BUFFERSIZE
if (pdiph->dwSize < sizeof(DIPROPDWORD)) {
return DIERR_INVALIDPARAM;
}
const DIPROPDWORD* prop = reinterpret_cast<const DIPROPDWORD*>(pdiph);
std::lock_guard<std::mutex> lock(mutex);
bufferSize = prop->dwData > 0 ? prop->dwData : ICE_DI_DEFAULT_BUFFER_SIZE;
TrimQueue_NoLock();
return DI_OK;
}
// Ignore unsupported DirectInput properties instead of failing old code.
return DI_OK;
}
STDMETHOD(Acquire)() override {
acquired = true;
if (hwnd != NULL && ::IsWindow(hwnd)) {
IceDI_SetDeviceTargetWindow(deviceType, hwnd);
}
else {
HWND fallback = NULL;
{
std::lock_guard<std::mutex> lock(g_windowMutex);
fallback = IceDI_GetFallbackRawInputWindow_NoLock();
}
if (fallback != NULL && ::IsWindow(fallback)) {
IceDI_RegisterRawInputForCurrentTargets(fallback);
}
}
if (deviceType == ICE_DI_DEVICE_MOUSE) {
g_iceDI_MouseAcquired.store(true);
ResetMouseMotion();
IceDI_ForceMouseCenterInternal(true);
}
return DI_OK;
}
STDMETHOD(Unacquire)() override {
acquired = false;
if (deviceType == ICE_DI_DEVICE_MOUSE) {
g_iceDI_MouseAcquired.store(false);
ResetMouseMotion();
IceDI_ClearMouseClipIfOwned();
}
return DI_OK;
}
STDMETHOD(GetDeviceState)(DWORD cbData, LPVOID lpvData) override {
if (lpvData == NULL) {
return E_POINTER;
}
if (!acquired) {
return DIERR_NOTACQUIRED;
}
if (deviceType == ICE_DI_DEVICE_MOUSE) {
IceDI_ForceMouseCenterInternal(false);
}
std::lock_guard<std::mutex> lock(mutex);
if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
if (cbData < 256) {
return DIERR_INVALIDPARAM;
}
memcpy(lpvData, keyboardState, 256);
return DI_OK;
}
if (cbData >= sizeof(DIMOUSESTATE2)) {
memcpy(lpvData, &mouseState, sizeof(DIMOUSESTATE2));
// Relative mouse state is consumed per poll.
mouseState.lX = 0;
mouseState.lY = 0;
mouseState.lZ = 0;
return DI_OK;
}
if (cbData >= sizeof(DIMOUSESTATE)) {
DIMOUSESTATE out;
memset(&out, 0, sizeof(out));
out.lX = mouseState.lX;
out.lY = mouseState.lY;
out.lZ = mouseState.lZ;
memcpy(out.rgbButtons, mouseState.rgbButtons, 4);
memcpy(lpvData, &out, sizeof(out));
mouseState.lX = 0;
mouseState.lY = 0;
mouseState.lZ = 0;
return DI_OK;
}
return DIERR_INVALIDPARAM;
}
STDMETHOD(GetDeviceData)(
DWORD cbObjectData,
LPDIDEVICEOBJECTDATA rgdod,
LPDWORD pdwInOut,
DWORD dwFlags
) override {
if (pdwInOut == NULL) {
return E_POINTER;
}
if (cbObjectData < sizeof(DIDEVICEOBJECTDATA)) {
return DIERR_INVALIDPARAM;
}
if (!acquired) {
return DIERR_NOTACQUIRED;
}
if (deviceType == ICE_DI_DEVICE_MOUSE) {
IceDI_ForceMouseCenterInternal(false);
}
std::lock_guard<std::mutex> lock(mutex);
DWORD requested = *pdwInOut;
// Doom/idTech uses rgdod == NULL to clear buffered garbage.
if (rgdod == NULL) {
DWORD count = 0;
if (requested == 0) {
count = (DWORD)queue.size();
queue.clear();
}
else {
count = std::min<DWORD>(requested, (DWORD)queue.size());
for (DWORD i = 0; i < count; i++) {
queue.pop_front();
}
}
*pdwInOut = count;
return DI_OK;
}
DWORD count = std::min<DWORD>(requested, (DWORD)queue.size());
for (DWORD i = 0; i < count; i++) {
rgdod[i] = queue.front();
queue.pop_front();
}
*pdwInOut = count;
return DI_OK;
}
STDMETHOD(SetDataFormat)(LPCDIDATAFORMAT lpdf) override {
if (lpdf == NULL) {
return DIERR_INVALIDPARAM;
}
// Existing Doom path passes c_dfDIKeyboard / c_dfDIMouse2.
// The shim always emits standard DirectInput offsets.
return DI_OK;
}
STDMETHOD(SetEventNotification)(HANDLE hEvent) override {
return DI_OK;
}
STDMETHOD(SetCooperativeLevel)(HWND hwndOwner, DWORD dwFlags) override {
hwnd = hwndOwner;
coopFlags = dwFlags;
if (hwndOwner != NULL && ::IsWindow(hwndOwner)) {
IceDI_SetDeviceTargetWindow(deviceType, hwndOwner);
if (deviceType == ICE_DI_DEVICE_MOUSE) {
IceDI_ForceMouseCenterInternal(true);
}
}
return DI_OK;
}
STDMETHOD(GetObjectInfo)(LPDIDEVICEOBJECTINSTANCEA pdidoi, DWORD dwObj, DWORD dwHow) override {
if (pdidoi == NULL) {
return E_POINTER;
}
DWORD oldSize = pdidoi->dwSize;
memset(pdidoi, 0, oldSize);
pdidoi->dwSize = oldSize;
pdidoi->dwOfs = dwObj;
sprintf_s(pdidoi->tszName, "Ice RawInput Object %u", dwObj);
return DI_OK;
}
STDMETHOD(GetDeviceInfo)(LPDIDEVICEINSTANCEA pdidi) override {
if (pdidi == NULL) {
return E_POINTER;
}
DWORD oldSize = pdidi->dwSize;
memset(pdidi, 0, oldSize);
pdidi->dwSize = oldSize;
if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
pdidi->guidInstance = GUID_SysKeyboard;
pdidi->guidProduct = GUID_SysKeyboard;
pdidi->dwDevType = DI8DEVTYPE_KEYBOARD;
strcpy_s(pdidi->tszInstanceName, "Ice RawInput Keyboard");
strcpy_s(pdidi->tszProductName, "Ice RawInput Keyboard");
}
else {
pdidi->guidInstance = GUID_SysMouse;
pdidi->guidProduct = GUID_SysMouse;
pdidi->dwDevType = DI8DEVTYPE_MOUSE;
strcpy_s(pdidi->tszInstanceName, "Ice RawInput Mouse");
strcpy_s(pdidi->tszProductName, "Ice RawInput Mouse");
}
return DI_OK;
}
STDMETHOD(RunControlPanel)(HWND hwndOwner, DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(Initialize)(HINSTANCE hinst, DWORD dwVersion, REFGUID rguid) override {
return DI_OK;
}
STDMETHOD(CreateEffect)(REFGUID rguid, LPCDIEFFECT lpeff, LPDIRECTINPUTEFFECT* ppdeff, LPUNKNOWN punkOuter) override {
if (ppdeff != NULL) {
*ppdeff = NULL;
}
return DIERR_UNSUPPORTED;
}
STDMETHOD(EnumEffects)(LPDIENUMEFFECTSCALLBACKA lpCallback, LPVOID pvRef, DWORD dwEffType) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(GetEffectInfo)(LPDIEFFECTINFOA pdei, REFGUID rguid) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(GetForceFeedbackState)(LPDWORD pdwOut) override {
if (pdwOut != NULL) {
*pdwOut = 0;
}
return DIERR_UNSUPPORTED;
}
STDMETHOD(SendForceFeedbackCommand)(DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(EnumCreatedEffectObjects)(LPDIENUMCREATEDEFFECTOBJECTSCALLBACK lpCallback, LPVOID pvRef, DWORD fl) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(Escape)(LPDIEFFESCAPE pesc) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(Poll)() override {
if (deviceType == ICE_DI_DEVICE_MOUSE && acquired) {
IceDI_ForceMouseCenterInternal(false);
}
return DI_OK;
}
STDMETHOD(SendDeviceData)(DWORD cbObjectData, LPCDIDEVICEOBJECTDATA rgdod, LPDWORD pdwInOut, DWORD fl) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(EnumEffectsInFile)(LPCSTR lpszFileName, LPDIENUMEFFECTSINFILECALLBACK pec, LPVOID pvRef, DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(WriteEffectToFile)(LPCSTR lpszFileName, DWORD dwEntries, LPDIFILEEFFECT rgDiFileEft, DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(BuildActionMap)(LPDIACTIONFORMATA lpdiaf, LPCSTR lpszUserName, DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(SetActionMap)(LPDIACTIONFORMATA lpdiActionFormat, LPCSTR lptszUserName, DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(GetImageInfo)(LPDIDEVICEIMAGEINFOHEADERA lpdiDevImageInfoHeader) override {
return DIERR_UNSUPPORTED;
}
// --------------------------------------------------------
// Raw Input entry points
// --------------------------------------------------------
void PushKeyboardRaw(const RAWKEYBOARD& kb) {
DWORD dik = IceDI_RawKeyboardToDIK(kb);
if (dik == 0 || dik >= 256) {
return;
}
const bool down = IceDI_IsPressedRawKeyboard(kb);
const BYTE newState = down ? 0x80 : 0x00;
std::lock_guard<std::mutex> lock(mutex);
keyboardState[dik] = newState;
DIDEVICEOBJECTDATA od;
memset(&od, 0, sizeof(od));
od.dwOfs = dik;
od.dwData = newState;
od.dwTimeStamp = IceDI_GetTimeStamp();
od.dwSequence = 0;
od.uAppData = 0;
PushEvent_NoLock(od);
}
void PushMouseRaw(const RAWMOUSE& rm) {
std::lock_guard<std::mutex> lock(mutex);
const DWORD time = IceDI_GetTimeStamp();
LONG dx = 0;
LONG dy = 0;
if (rm.usFlags & MOUSE_MOVE_ABSOLUTE) {
POINT absolutePoint;
if (IceDI_RawAbsoluteMouseToScreen(rm, &absolutePoint)) {
// SetCursorPos can surface as an absolute packet on some mouse
// stacks / remote-desktop / wrapped-window setups. Reset the
// baseline instead of generating a giant negative/positive delta.
if (IceDI_IsRecentCenterWarpPoint(absolutePoint)) {
lastAbsoluteMouse = absolutePoint;
haveLastAbsoluteMouse = true;
}
else if (haveLastAbsoluteMouse) {
dx = absolutePoint.x - lastAbsoluteMouse.x;
dy = absolutePoint.y - lastAbsoluteMouse.y;
lastAbsoluteMouse = absolutePoint;
}
else {
lastAbsoluteMouse = absolutePoint;
haveLastAbsoluteMouse = true;
}
}
}
else {
haveLastAbsoluteMouse = false;
dx = rm.lLastX;
dy = rm.lLastY;
}
// Raw Input relative deltas only. The absolute packet conversion above
// converts absolute devices into sane relative deltas; do not mix this
// with GetCursorPos/WM_MOUSEMOVE deltas.
if (dx != 0) {
mouseState.lX += dx;
PushMouseAxis_NoLock(DIMOFS_X, dx, time);
}
if (dy != 0) {
mouseState.lY += dy;
PushMouseAxis_NoLock(DIMOFS_Y, dy, time);
}
const USHORT flags = rm.usButtonFlags;
if (flags & RI_MOUSE_WHEEL) {
SHORT wheel = (SHORT)rm.usButtonData;
mouseState.lZ += wheel;
PushMouseAxis_NoLock(DIMOFS_Z, wheel, time);
}
if (flags & RI_MOUSE_BUTTON_1_DOWN) {
PushMouseButton_NoLock(0, true, time);
}
if (flags & RI_MOUSE_BUTTON_1_UP) {
PushMouseButton_NoLock(0, false, time);
}
if (flags & RI_MOUSE_BUTTON_2_DOWN) {
PushMouseButton_NoLock(1, true, time);
}
if (flags & RI_MOUSE_BUTTON_2_UP) {
PushMouseButton_NoLock(1, false, time);
}
if (flags & RI_MOUSE_BUTTON_3_DOWN) {
PushMouseButton_NoLock(2, true, time);
}
if (flags & RI_MOUSE_BUTTON_3_UP) {
PushMouseButton_NoLock(2, false, time);
}
if (flags & RI_MOUSE_BUTTON_4_DOWN) {
PushMouseButton_NoLock(3, true, time);
}
if (flags & RI_MOUSE_BUTTON_4_UP) {
PushMouseButton_NoLock(3, false, time);
}
if (flags & RI_MOUSE_BUTTON_5_DOWN) {
PushMouseButton_NoLock(4, true, time);
}
if (flags & RI_MOUSE_BUTTON_5_UP) {
PushMouseButton_NoLock(4, false, time);
}
}
void ClearQueue() {
std::lock_guard<std::mutex> lock(mutex);
queue.clear();
}
void ResetMouseMotion() {
if (deviceType != ICE_DI_DEVICE_MOUSE) {
return;
}
std::lock_guard<std::mutex> lock(mutex);
queue.clear();
memset(&mouseState, 0, sizeof(mouseState));
haveLastAbsoluteMouse = false;
lastAbsoluteMouse.x = 0;
lastAbsoluteMouse.y = 0;
}
private:
void PushMouseAxis_NoLock(DWORD ofs, LONG value, DWORD time) {
DIDEVICEOBJECTDATA od;
memset(&od, 0, sizeof(od));
od.dwOfs = ofs;
od.dwData = (DWORD)value;
od.dwTimeStamp = time;
od.dwSequence = 0;
od.uAppData = 0;
PushEvent_NoLock(od);
}
void PushMouseButton_NoLock(int button, bool down, DWORD time) {
if (button < 0 || button >= 8) {
return;
}
mouseState.rgbButtons[button] = down ? 0x80 : 0x00;
DIDEVICEOBJECTDATA od;
memset(&od, 0, sizeof(od));
od.dwOfs = DIMOFS_BUTTON0 + button;
od.dwData = down ? 0x80 : 0x00;
od.dwTimeStamp = time;
od.dwSequence = 0;
od.uAppData = 0;
PushEvent_NoLock(od);
}
void PushEvent_NoLock(const DIDEVICEOBJECTDATA& od) {
queue.push_back(od);
TrimQueue_NoLock();
}
void TrimQueue_NoLock() {
while (queue.size() > bufferSize) {
queue.pop_front();
}
}
private:
std::atomic<ULONG> refCount;
iceDIShimDeviceType deviceType;
bool acquired;
DWORD bufferSize;
DWORD coopFlags;
HWND hwnd;
std::mutex mutex;
std::deque<DIDEVICEOBJECTDATA> queue;
BYTE keyboardState[256];
DIMOUSESTATE2 mouseState;
bool haveLastAbsoluteMouse;
POINT lastAbsoluteMouse;
};
// ------------------------------------------------------------
// IceDirectInput8
// ------------------------------------------------------------
class IceDirectInput8 : public IDirectInput8A {
public:
IceDirectInput8() : refCount(1) {
}
virtual ~IceDirectInput8() {
}
STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) override {
if (ppvObj == NULL) {
return E_POINTER;
}
*ppvObj = NULL;
if (riid == IID_IUnknown || riid == IID_IDirectInput8A) {
*ppvObj = static_cast<IDirectInput8A*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHOD_(ULONG, AddRef)() override {
return ++refCount;
}
STDMETHOD_(ULONG, Release)() override {
ULONG r = --refCount;
if (r == 0) {
delete this;
}
return r;
}
STDMETHOD(CreateDevice)(REFGUID rguid, LPDIRECTINPUTDEVICE8A* lplpDirectInputDevice, LPUNKNOWN pUnkOuter) override {
if (lplpDirectInputDevice == NULL) {
return E_POINTER;
}
*lplpDirectInputDevice = NULL;
if (pUnkOuter != NULL) {
return CLASS_E_NOAGGREGATION;
}
IceDirectInputDevice8* dev = NULL;
IceDirectInputDevice8* oldDev = NULL;
if (rguid == GUID_SysKeyboard) {
dev = new IceDirectInputDevice8(ICE_DI_DEVICE_KEYBOARD);
{
std::lock_guard<std::mutex> lock(g_deviceMutex);
oldDev = g_keyboardDevice;
g_keyboardDevice = dev;
g_keyboardDevice->AddRef();
}
}
else if (rguid == GUID_SysMouse) {
dev = new IceDirectInputDevice8(ICE_DI_DEVICE_MOUSE);
{
std::lock_guard<std::mutex> lock(g_deviceMutex);
oldDev = g_mouseDevice;
g_mouseDevice = dev;
g_mouseDevice->AddRef();
}
}
else {
return DIERR_DEVICENOTREG;
}
// Do not Release while holding g_deviceMutex. The device destructor also
// takes g_deviceMutex to clear the globals.
if (oldDev != NULL) {
oldDev->Release();
}
*lplpDirectInputDevice = dev;
return DI_OK;
}
STDMETHOD(EnumDevices)(DWORD dwDevType, LPDIENUMDEVICESCALLBACKA lpCallback, LPVOID pvRef, DWORD dwFlags) override {
if (lpCallback == NULL) {
return DIERR_INVALIDPARAM;
}
if (dwDevType == 0 || dwDevType == DI8DEVCLASS_KEYBOARD || dwDevType == DI8DEVTYPE_KEYBOARD) {
DIDEVICEINSTANCEA di;
memset(&di, 0, sizeof(di));
di.dwSize = sizeof(di);
di.guidInstance = GUID_SysKeyboard;
di.guidProduct = GUID_SysKeyboard;
di.dwDevType = DI8DEVTYPE_KEYBOARD;
strcpy_s(di.tszInstanceName, "Ice RawInput Keyboard");
strcpy_s(di.tszProductName, "Ice RawInput Keyboard");
if (lpCallback(&di, pvRef) == DIENUM_STOP) {
return DI_OK;
}
}
if (dwDevType == 0 || dwDevType == DI8DEVCLASS_POINTER || dwDevType == DI8DEVTYPE_MOUSE) {
DIDEVICEINSTANCEA di;
memset(&di, 0, sizeof(di));
di.dwSize = sizeof(di);
di.guidInstance = GUID_SysMouse;
di.guidProduct = GUID_SysMouse;
di.dwDevType = DI8DEVTYPE_MOUSE;
strcpy_s(di.tszInstanceName, "Ice RawInput Mouse");
strcpy_s(di.tszProductName, "Ice RawInput Mouse");
lpCallback(&di, pvRef);
}
return DI_OK;
}
STDMETHOD(GetDeviceStatus)(REFGUID rguidInstance) override {
if (rguidInstance == GUID_SysKeyboard || rguidInstance == GUID_SysMouse) {
return DI_OK;
}
return DIERR_NOTFOUND;
}
STDMETHOD(RunControlPanel)(HWND hwndOwner, DWORD dwFlags) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(Initialize)(HINSTANCE hinst, DWORD dwVersion) override {
return DI_OK;
}
STDMETHOD(FindDevice)(REFGUID rguidClass, LPCSTR ptszName, LPGUID pguidInstance) override {
if (pguidInstance == NULL) {
return E_POINTER;
}
if (ptszName != NULL) {
if (_stricmp(ptszName, "keyboard") == 0) {
*pguidInstance = GUID_SysKeyboard;
return DI_OK;
}
if (_stricmp(ptszName, "mouse") == 0) {
*pguidInstance = GUID_SysMouse;
return DI_OK;
}
}
return DIERR_NOTFOUND;
}
STDMETHOD(EnumDevicesBySemantics)(
LPCSTR ptszUserName,
LPDIACTIONFORMATA lpdiActionFormat,
LPDIENUMDEVICESBYSEMANTICSCBA lpCallback,
LPVOID pvRef,
DWORD dwFlags
) override {
return DIERR_UNSUPPORTED;
}
STDMETHOD(ConfigureDevices)(
LPDICONFIGUREDEVICESCALLBACK lpdiCallback,
LPDICONFIGUREDEVICESPARAMSA lpdiCDParams,
DWORD dwFlags,
LPVOID pvRefData
) override {
return DIERR_UNSUPPORTED;
}
private:
std::atomic<ULONG> refCount;
};
// ------------------------------------------------------------
// Public API
// ------------------------------------------------------------
HRESULT WINAPI IceDirectInput8Create(
HINSTANCE hinst,
DWORD dwVersion,
REFIID riidltf,
LPVOID* ppvOut,
LPUNKNOWN punkOuter
) {
if (ppvOut == NULL) {
return E_POINTER;
}
*ppvOut = NULL;
if (punkOuter != NULL) {
return CLASS_E_NOAGGREGATION;
}
IceDirectInput8* di = new IceDirectInput8();
HRESULT hr = di->QueryInterface(riidltf, ppvOut);
di->Release();
return hr;
}
bool IceDirectInputShim_RegisterRawInput(HWND hwnd) {
if (hwnd == NULL || !::IsWindow(hwnd)) {
return false;
}
{
std::lock_guard<std::mutex> lock(g_windowMutex);
g_rawInputHwnd = hwnd;
g_keyboardTargetHwnd = hwnd;
g_mouseTargetHwnd = hwnd;
}
bool ok = IceDI_RegisterRawInputForCurrentTargets(hwnd);
if (ok) {
IceDI_ForceMouseCenterInternal(false);
}
return ok;
}
void IceDirectInputShim_SetMouseTargetWindow(HWND hwnd) {
if (hwnd == NULL || !::IsWindow(hwnd)) {
return;
}
{
std::lock_guard<std::mutex> lock(g_windowMutex);
g_mouseTargetHwnd = hwnd;
g_rawInputHwnd = hwnd;
}
IceDI_RegisterRawInputForCurrentTargets(hwnd);
IceDI_ForceMouseCenterInternal(true);
}
void IceDirectInputShim_SetMouseCenterLock(bool enabled) {
g_iceDI_CenterMouseWhenActive.store(enabled);
if (!enabled) {
IceDI_ClearMouseClipIfOwned();
}
else {
IceDI_ForceMouseCenterInternal(true);
}
}
bool IceDirectInputShim_ForceMouseCenter() {
return IceDI_ForceMouseCenterInternal(true);
}
bool IceDirectInputShim_HandleRawInput(LPARAM lParam) {
UINT size = 0;
if (::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)) != 0) {
return false;
}
if (size == 0) {
return false;
}
std::vector<BYTE> buffer;
buffer.resize(size);
UINT result = ::GetRawInputData(
(HRAWINPUT)lParam,
RID_INPUT,
buffer.data(),
&size,
sizeof(RAWINPUTHEADER)
);
if (result == (UINT)-1 || result == 0) {
return false;
}
RAWINPUT* raw = reinterpret_cast<RAWINPUT*>(buffer.data());
// RAWINPUTHEADER intentionally has no target HWND field. The target window
// is chosen when registering RAWINPUTDEVICE::hwndTarget, in SetCooperativeLevel,
// or through IceDirectInputShim_SetMouseTargetWindow(). Do not try to read
// the target HWND from the raw-input header; that member does not exist.
if (raw->header.dwType == RIM_TYPEKEYBOARD) {
IceDirectInputDevice8* kbd = NULL;
{
std::lock_guard<std::mutex> lock(g_deviceMutex);
kbd = g_keyboardDevice;
if (kbd != NULL) {
kbd->AddRef();
}
}
if (kbd != NULL) {
kbd->PushKeyboardRaw(raw->data.keyboard);
kbd->Release();
return true;
}
}
if (raw->header.dwType == RIM_TYPEMOUSE) {
if (!g_iceDI_GameMouseActive.load()) {
IceDI_ClearMouseClipIfOwned();
return false;
}
IceDirectInputDevice8* mouse = NULL;
{
std::lock_guard<std::mutex> lock(g_deviceMutex);
mouse = g_mouseDevice;
if (mouse != NULL) {
mouse->AddRef();
}
}
if (mouse != NULL) {
mouse->PushMouseRaw(raw->data.mouse);
mouse->Release();
IceDI_ForceMouseCenterInternal(false);
return true;
}
}
return false;
}
void IceDirectInputShim_SetGameMouseActive(bool active) {
g_iceDI_GameMouseActive.store(active);
IceDirectInputDevice8* mouse = NULL;
{
std::lock_guard<std::mutex> lock(g_deviceMutex);
mouse = g_mouseDevice;
if (mouse != NULL) {
mouse->AddRef();
}
}
if (mouse != NULL) {
mouse->ResetMouseMotion();
mouse->Release();
}
if (active) {
IceDI_ForceMouseCenterInternal(true);
}
else {
IceDI_ClearMouseClipIfOwned();
}
}
void IceDirectInputShim_Shutdown() {
IceDirectInputDevice8* keyboard = NULL;
IceDirectInputDevice8* mouse = NULL;
{
std::lock_guard<std::mutex> lock(g_deviceMutex);
keyboard = g_keyboardDevice;
mouse = g_mouseDevice;
g_keyboardDevice = NULL;
g_mouseDevice = NULL;
}
if (keyboard != NULL) {
keyboard->Release();
}
if (mouse != NULL) {
mouse->Release();
}
{
std::lock_guard<std::mutex> lock(g_windowMutex);
g_rawInputHwnd = NULL;
g_keyboardTargetHwnd = NULL;
g_mouseTargetHwnd = NULL;
g_lastCenterWarpTick = 0;
}
g_iceDI_MouseAcquired.store(false);
g_iceDI_GameMouseActive.store(false);
IceDI_ClearMouseClipIfOwned();
}