diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 9136b35..68da432 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -69,8 +69,8 @@ jobs: fail-fast: false matrix: include: - - { os: windows-latest, rid: win-x64, platform: windows, arch: x64 } - - { os: windows-latest, rid: win-x86, platform: windows, arch: x86 } + - { os: windows-2022, rid: win-x64, platform: windows, arch: x64 } + - { os: windows-2022, rid: win-x86, platform: windows, arch: x86 } - { os: ubuntu-latest, rid: linux-x64, platform: linux, arch: x64 } - { os: macos-15, rid: osx-x64, platform: macos, arch: x64 } - { os: macos-15, rid: osx-arm64, platform: macos, arch: arm64 } diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c65446..0792eae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ set(SOURCES # Platform-specific sources if(WIN32) list(APPEND SOURCES src/NativeInput_Windows.cpp) - set(PLATFORM_LIBS winmm) + set(PLATFORM_LIBS winmm hid setupapi cfgmgr32) elseif(APPLE) list(APPEND SOURCES src/NativeInput_Mac.mm) find_library(APPKIT_FRAMEWORK AppKit REQUIRED) @@ -34,9 +34,13 @@ else() set(PLATFORM_LIBS) endif() -# Create shared library -add_library(VpeNativeInput SHARED ${SOURCES}) - +# Create shared library +add_library(VpeNativeInput SHARED ${SOURCES}) + +if(WIN32) + target_compile_definitions(VpeNativeInput PRIVATE WINVER=0x0600 _WIN32_WINNT=0x0600) +endif() + # Link platform libraries target_link_libraries(VpeNativeInput PRIVATE ${PLATFORM_LIBS}) diff --git a/README.md b/README.md index c81b8b3..d3b08e2 100644 --- a/README.md +++ b/README.md @@ -99,11 +99,14 @@ You can override the output directory with: ```csharp using VisualPinball.Unity.Simulation; -// Initialize -NativeInputApi.VpeInputInit(); - -// Set bindings -var bindings = new[] { +// Initialize +NativeInputApi.VpeInputInit(); +if (NativeInputApi.VpeInputGetProtocolVersion() != 2) { + throw new InvalidOperationException("Unsupported native input protocol."); +} + +// Set bindings +var bindings = new[] { new NativeInputApi.InputBinding { Action = (int)NativeInputApi.InputAction.LeftFlipper, BindingType = (int)NativeInputApi.BindingType.Keyboard, diff --git a/src/NativeInput.h b/src/NativeInput.h index bbb477a..08d76c2 100644 --- a/src/NativeInput.h +++ b/src/NativeInput.h @@ -67,12 +67,29 @@ typedef enum { VPE_INPUT_MAX = 64 // Reserve space for future actions } VpeInputAction; -// Input binding type -typedef enum { - VPE_BINDING_KEYBOARD = 0, - VPE_BINDING_GAMEPAD = 1, - VPE_BINDING_MOUSE = 2 -} VpeBindingType; +// ABI protocol version. Increment when struct layouts change. +#define VPE_INPUT_PROTOCOL_VERSION 2 + +// Input binding type +typedef enum { + VPE_BINDING_KEYBOARD = 0, + VPE_BINDING_GAMEPAD = 1, + VPE_BINDING_MOUSE = 2 +} VpeBindingType; + +typedef enum { + VPE_INPUT_EVENT_ACTION = 0, + VPE_INPUT_EVENT_AXIS = 1, + // The set of available devices changed (hotplug); consumers should refresh + // their device list. Carries no payload besides the timestamp. + VPE_INPUT_EVENT_DEVICES_CHANGED = 2 +} VpeInputEventType; + +typedef enum { + VPE_AXIS_KIND_POSITION = 0, + VPE_AXIS_KIND_VELOCITY = 1, + VPE_AXIS_KIND_ACCELERATION = 2 +} VpeAxisKind; // Key codes (Windows virtual key codes, mapped on other platforms) typedef enum { @@ -117,6 +134,7 @@ typedef enum { VPE_KEY_P = 0x50, VPE_KEY_T = 0x54, VPE_KEY_Y = 0x59, + VPE_KEY_Z = 0x5A, VPE_KEY_NUMPAD1 = 0x61, VPE_KEY_A = 0x41, VPE_KEY_B = 0x42, @@ -124,17 +142,47 @@ typedef enum { VPE_KEY_D = 0x44, VPE_KEY_W = 0x57, VPE_KEY_MINUS = 0xBD, // VK_OEM_MINUS + VPE_KEY_SLASH = 0xBF, // VK_OEM_2 VPE_KEY_QUOTE = 0xDE, // VK_OEM_7 VPE_KEY_CARET = 0xC0, // VK_OEM_3 (layout dependent) } VpeKeyCode; - -// Input event structure (matches C# struct layout) -typedef struct { - int64_t timestampUsec; // Microsecond timestamp - int32_t action; // VpeInputAction - float value; // 0.0 (released) or 1.0 (pressed), or analog value - int32_t _padding; // Ensure 16-byte alignment -} VpeInputEvent; + +#define VPE_INPUT_DEVICE_ID_SIZE 260 +#define VPE_INPUT_DEVICE_NAME_SIZE 128 +#define VPE_INPUT_AXIS_NAME_SIZE 32 + +// Input device descriptor (matches C# struct layout) +typedef struct { + int32_t deviceIndex; + int32_t axisCount; + int32_t isConnected; + int32_t _padding; + char stableId[VPE_INPUT_DEVICE_ID_SIZE]; + char displayName[VPE_INPUT_DEVICE_NAME_SIZE]; +} VpeInputDeviceInfo; + +// Input axis descriptor (matches C# struct layout) +typedef struct { + int32_t axisId; + int32_t usagePage; + int32_t usage; + int32_t kind; + float rawValue; + int32_t _padding; + int64_t timestampUsec; + char name[VPE_INPUT_AXIS_NAME_SIZE]; +} VpeInputAxisInfo; + +// Input event structure (matches C# struct layout) +typedef struct { + int64_t timestampUsec; // Microsecond timestamp + int32_t eventType; // VpeInputEventType + int32_t action; // VpeInputAction for action events + int32_t deviceIndex; // device index for axis events + int32_t axisId; // axis id for axis events + float value; // 0.0/1.0 for buttons, -1.0..1.0 for axes + int32_t _padding; +} VpeInputEvent; // Input binding structure typedef struct { @@ -147,12 +195,17 @@ typedef struct { // Callback for input events typedef void (*VpeInputEventCallback)(const VpeInputEvent* event, void* userData); -// Initialize input system -// Returns 1 on success, 0 on failure -VPE_API int VpeInputInit(void); - -// Shutdown input system -VPE_API void VpeInputShutdown(void); +// Initialize input system +// Returns 1 on success, 0 on failure +VPE_API int VpeInputInit(void); + +// Native ABI protocol version. Call after VpeInputInit and verify the returned +// value before starting polling; VpeInputStartPolling fails until this has been +// acknowledged. +VPE_API int VpeInputGetProtocolVersion(void); + +// Shutdown input system +VPE_API void VpeInputShutdown(void); // Set input bindings // bindings: Array of VpeInputBinding @@ -165,11 +218,19 @@ VPE_API void VpeInputSetBindings(const VpeInputBinding* bindings, int count); // pollIntervalUs: Polling interval in microseconds (default 500) VPE_API int VpeInputStartPolling(VpeInputEventCallback callback, void* userData, int pollIntervalUs); -// Stop polling thread -VPE_API void VpeInputStopPolling(void); - -// Get high-resolution timestamp in microseconds -VPE_API int64_t VpeGetTimestampUsec(void); +// Stop polling thread +VPE_API void VpeInputStopPolling(void); + +// List HID joystick/gamepad-style devices with analog axes. +// Returns the total available device count. Copies up to maxDevices entries when devices is non-null. +VPE_API int VpeInputListDevices(VpeInputDeviceInfo* devices, int maxDevices); + +// List axes for a device index returned by VpeInputListDevices. +// Returns the total available axis count. Copies up to maxAxes entries when axes is non-null. +VPE_API int VpeInputListDeviceAxes(int deviceIndex, VpeInputAxisInfo* axes, int maxAxes); + +// Get high-resolution timestamp in microseconds +VPE_API int64_t VpeGetTimestampUsec(void); // Set thread priority to time-critical (best effort) VPE_API void VpeSetThreadPriority(void); diff --git a/src/NativeInput_Linux.cpp b/src/NativeInput_Linux.cpp index 679f069..16c2b6c 100644 --- a/src/NativeInput_Linux.cpp +++ b/src/NativeInput_Linux.cpp @@ -31,6 +31,7 @@ namespace { std::thread g_pollingThread; std::atomic g_running(false); + std::atomic g_protocolVersionChecked(false); std::condition_variable g_waitCondition; std::mutex g_waitMutex; VpeInputEventCallback g_callback = nullptr; @@ -139,8 +140,12 @@ namespace { return XK_w; case VPE_KEY_Y: return XK_y; + case VPE_KEY_Z: + return XK_z; case VPE_KEY_MINUS: return XK_minus; + case VPE_KEY_SLASH: + return XK_slash; case VPE_KEY_QUOTE: return XK_apostrophe; case VPE_KEY_CARET: @@ -261,6 +266,7 @@ namespace { VpeInputEvent event{}; event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_ACTION; event.action = static_cast(binding.action); event.value = 0.0f; g_callback(&event, g_userData); @@ -298,6 +304,7 @@ namespace { VpeInputEvent event{}; event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_ACTION; event.action = static_cast(binding.action); event.value = currentValue; g_callback(&event, g_userData); @@ -316,6 +323,7 @@ namespace { extern "C" { VPE_API int VpeInputInit(void) { + g_protocolVersionChecked.store(false, std::memory_order_release); g_startTime = Clock::now(); g_display = XOpenDisplay(nullptr); if (g_display == nullptr) { @@ -328,8 +336,14 @@ VPE_API int VpeInputInit(void) { return 1; } +VPE_API int VpeInputGetProtocolVersion(void) { + g_protocolVersionChecked.store(true, std::memory_order_release); + return VPE_INPUT_PROTOCOL_VERSION; +} + VPE_API void VpeInputShutdown(void) { VpeInputStopPolling(); + g_protocolVersionChecked.store(false, std::memory_order_release); g_bindings.clear(); g_wasForeground = false; @@ -371,7 +385,9 @@ VPE_API void VpeInputSetBindings(const VpeInputBinding* bindings, int count) { } VPE_API int VpeInputStartPolling(VpeInputEventCallback callback, void* userData, int pollIntervalUs) { - if (g_display == nullptr || g_running.load(std::memory_order_acquire)) { + if (!g_protocolVersionChecked.load(std::memory_order_acquire) || + g_display == nullptr || + g_running.load(std::memory_order_acquire)) { return 0; } @@ -402,6 +418,19 @@ VPE_API void VpeInputStopPolling(void) { } } +VPE_API int VpeInputListDevices(VpeInputDeviceInfo* devices, int maxDevices) { + (void)devices; + (void)maxDevices; + return 0; +} + +VPE_API int VpeInputListDeviceAxes(int deviceIndex, VpeInputAxisInfo* axes, int maxAxes) { + (void)deviceIndex; + (void)axes; + (void)maxAxes; + return 0; +} + VPE_API int64_t VpeGetTimestampUsec(void) { return GetTimestampUsecInternal(); } diff --git a/src/NativeInput_Mac.mm b/src/NativeInput_Mac.mm index c6231c3..f8b1262 100644 --- a/src/NativeInput_Mac.mm +++ b/src/NativeInput_Mac.mm @@ -28,6 +28,7 @@ std::thread g_pollingThread; std::atomic g_running(false); + std::atomic g_protocolVersionChecked(false); std::condition_variable g_waitCondition; std::mutex g_waitMutex; VpeInputEventCallback g_callback = nullptr; @@ -178,9 +179,15 @@ bool MapKeyCode(int keyCode, CGKeyCode* nativeKeyCode) { case VPE_KEY_Y: *nativeKeyCode = 16; return true; + case VPE_KEY_Z: + *nativeKeyCode = 6; + return true; case VPE_KEY_MINUS: *nativeKeyCode = 27; return true; + case VPE_KEY_SLASH: + *nativeKeyCode = 44; + return true; case VPE_KEY_QUOTE: *nativeKeyCode = 39; return true; @@ -225,6 +232,7 @@ void EmitReleaseForPressedKeys(int64_t timestampUsec) { VpeInputEvent event{}; event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_ACTION; event.action = static_cast(binding.action); event.value = 0.0f; g_callback(&event, g_userData); @@ -260,6 +268,7 @@ void PollingThreadFunc() { VpeInputEvent event{}; event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_ACTION; event.action = static_cast(binding.action); event.value = currentValue; g_callback(&event, g_userData); @@ -279,13 +288,20 @@ void PollingThreadFunc() { extern "C" { VPE_API int VpeInputInit(void) { + g_protocolVersionChecked.store(false, std::memory_order_release); g_startTime = Clock::now(); g_wasForeground = false; return 1; } +VPE_API int VpeInputGetProtocolVersion(void) { + g_protocolVersionChecked.store(true, std::memory_order_release); + return VPE_INPUT_PROTOCOL_VERSION; +} + VPE_API void VpeInputShutdown(void) { VpeInputStopPolling(); + g_protocolVersionChecked.store(false, std::memory_order_release); g_bindings.clear(); g_wasForeground = false; } @@ -317,7 +333,8 @@ VPE_API void VpeInputSetBindings(const VpeInputBinding* bindings, int count) { } VPE_API int VpeInputStartPolling(VpeInputEventCallback callback, void* userData, int pollIntervalUs) { - if (g_running.load(std::memory_order_acquire)) { + if (!g_protocolVersionChecked.load(std::memory_order_acquire) || + g_running.load(std::memory_order_acquire)) { return 0; } @@ -348,6 +365,19 @@ VPE_API void VpeInputStopPolling(void) { } } +VPE_API int VpeInputListDevices(VpeInputDeviceInfo* devices, int maxDevices) { + (void)devices; + (void)maxDevices; + return 0; +} + +VPE_API int VpeInputListDeviceAxes(int deviceIndex, VpeInputAxisInfo* axes, int maxAxes) { + (void)deviceIndex; + (void)axes; + (void)maxAxes; + return 0; +} + VPE_API int64_t VpeGetTimestampUsec(void) { return GetTimestampUsecInternal(); } diff --git a/src/NativeInput_Stub.cpp b/src/NativeInput_Stub.cpp index f7cd3d2..f999b1c 100644 --- a/src/NativeInput_Stub.cpp +++ b/src/NativeInput_Stub.cpp @@ -9,12 +9,16 @@ extern "C" { -VPE_API int VpeInputInit(void) { - // TODO: Implement for Linux (evdev) and macOS (IOKit) - return 0; // Not implemented -} - -VPE_API void VpeInputShutdown(void) { +VPE_API int VpeInputInit(void) { + // TODO: Implement for Linux (evdev) and macOS (IOKit) + return 0; // Not implemented +} + +VPE_API int VpeInputGetProtocolVersion(void) { + return VPE_INPUT_PROTOCOL_VERSION; +} + +VPE_API void VpeInputShutdown(void) { // TODO } @@ -27,11 +31,24 @@ VPE_API int VpeInputStartPolling(VpeInputEventCallback callback, void* userData, return 0; } -VPE_API void VpeInputStopPolling(void) { - // TODO -} - -VPE_API int64_t VpeGetTimestampUsec(void) { +VPE_API void VpeInputStopPolling(void) { + // TODO +} + +VPE_API int VpeInputListDevices(VpeInputDeviceInfo* devices, int maxDevices) { + (void)devices; + (void)maxDevices; + return 0; +} + +VPE_API int VpeInputListDeviceAxes(int deviceIndex, VpeInputAxisInfo* axes, int maxAxes) { + (void)deviceIndex; + (void)axes; + (void)maxAxes; + return 0; +} + +VPE_API int64_t VpeGetTimestampUsec(void) { auto now = std::chrono::high_resolution_clock::now(); auto duration = now.time_since_epoch(); return std::chrono::duration_cast(duration).count(); diff --git a/src/NativeInput_Windows.cpp b/src/NativeInput_Windows.cpp index 4c87dcc..a252db5 100644 --- a/src/NativeInput_Windows.cpp +++ b/src/NativeInput_Windows.cpp @@ -1,37 +1,111 @@ -// license:GPLv3+ -// Windows implementation of native input polling - -#ifdef _WIN32 - -#include "NativeInput.h" -#include -#include +// license:GPLv3+ +// Windows implementation of native input polling + +#ifdef _WIN32 + +#include "NativeInput.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include +#include + +#include #include +#include +#include +#include +#include +#include +#include +#include #include - + namespace { + constexpr USAGE UsagePageGenericDesktop = 0x01; + constexpr USAGE UsageJoystick = 0x04; + constexpr USAGE UsageGamepad = 0x05; + constexpr USAGE UsageMultiAxisController = 0x08; + constexpr USAGE UsageAxisMin = 0x30; + constexpr USAGE UsageAxisMax = 0x38; + struct KeyBindingState { int keyCode; VpeInputAction action; float previousValue; }; + struct AxisDescriptor { + int axisId; + USAGE usagePage; + USAGE usage; + HIDP_VALUE_CAPS valueCaps; + float previousValue; + float rawValue; + int64_t timestampUsec; + }; + + struct HidDevice { + int deviceIndex; + std::wstring path; + std::string stableId; + std::string displayName; + HANDLE handle = INVALID_HANDLE_VALUE; + PHIDP_PREPARSED_DATA preparsedData = nullptr; + HIDP_CAPS caps = {}; + std::vector axes; + std::vector inputReport; + OVERLAPPED readOverlap = {}; + HANDLE readEvent = NULL; + bool readPending = false; + bool isConnected = true; + }; + + struct PendingAxisEvent { + int deviceIndex; + int axisId; + float value; + }; + std::thread g_pollingThread; std::atomic g_running(false); + std::atomic g_protocolVersionChecked(false); VpeInputEventCallback g_callback = nullptr; void* g_userData = nullptr; int g_pollIntervalUs = 500; HANDLE g_timer = NULL; HANDLE g_stopEvent = NULL; - // Input bindings: one key can map to multiple actions. + std::mutex g_bindingsMutex; // guards g_bindings and g_wasForeground between API calls and the polling thread std::vector g_bindings; - - // High-resolution timing + std::mutex g_hidDevicesMutex; // guards the live polling device snapshot and axis telemetry + std::vector g_hidDevices; + + // Hotplug handling (polling thread only, except the atomic request flag): + // a cheap present-interface probe runs every couple of seconds, and a full + // reconciliation runs when the probe result changes or a read failed. + constexpr int64_t HotplugCheckIntervalUsec = 2000000; + std::atomic g_hotplugCheckRequested(false); + std::wstring g_lastHidInterfaceList; + LARGE_INTEGER g_frequency; LARGE_INTEGER g_startTime; bool g_wasForeground = false; + int64_t GetTimestampUsecInternal() { + if (g_frequency.QuadPart == 0) { + return 0; + } + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + return ((now.QuadPart - g_startTime.QuadPart) * 1000000LL) / g_frequency.QuadPart; + } + bool IsCurrentProcessForeground() { HWND foreground = GetForegroundWindow(); if (foreground == NULL) { @@ -43,6 +117,94 @@ namespace { return foregroundPid == GetCurrentProcessId(); } + std::string Narrow(const std::wstring& value) { + if (value.empty()) { + return {}; + } + const int size = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (size <= 1) { + return {}; + } + std::string result(static_cast(size - 1), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, result.data(), size, nullptr, nullptr); + return result; + } + + void CopyString(char* destination, int destinationSize, const std::string& value) { + if (destination == nullptr || destinationSize <= 0) { + return; + } + const size_t count = std::min(static_cast(destinationSize - 1), value.size()); + if (count > 0) { + memcpy(destination, value.data(), count); + } + destination[count] = '\0'; + } + + const char* AxisName(USAGE usage) { + switch (usage) { + case 0x30: return "X"; + case 0x31: return "Y"; + case 0x32: return "Z"; + case 0x33: return "RX"; + case 0x34: return "RY"; + case 0x35: return "RZ"; + case 0x36: return "Slider"; + case 0x37: return "Dial"; + case 0x38: return "Wheel"; + default: return "Axis"; + } + } + + bool IsSupportedAxis(USAGE usage) { + return usage >= UsageAxisMin && usage <= UsageAxisMax; + } + + bool DeviceLooksLikeJoystick(const HIDP_CAPS& caps) { + return caps.UsagePage == UsagePageGenericDesktop + && (caps.Usage == UsageJoystick || caps.Usage == UsageGamepad || caps.Usage == UsageMultiAxisController); + } + + void EmitActionEvent(int64_t timestampUsec, VpeInputAction action, float value) { + if (g_callback == nullptr) { + return; + } + + VpeInputEvent event{}; + event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_ACTION; + event.action = static_cast(action); + event.value = value; + g_callback(&event, g_userData); + } + + void EmitAxisEvent(int64_t timestampUsec, int deviceIndex, int axisId, float value) { + if (g_callback == nullptr) { + return; + } + + VpeInputEvent event{}; + event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_AXIS; + event.action = -1; + event.deviceIndex = deviceIndex; + event.axisId = axisId; + event.value = value; + g_callback(&event, g_userData); + } + + void EmitDevicesChangedEvent(int64_t timestampUsec) { + if (g_callback == nullptr) { + return; + } + + VpeInputEvent event{}; + event.timestampUsec = timestampUsec; + event.eventType = VPE_INPUT_EVENT_DEVICES_CHANGED; + event.action = -1; + g_callback(&event, g_userData); + } + void EmitReleaseForPressedKeys(int64_t timestampUsec) { for (auto& binding : g_bindings) { if (binding.previousValue <= 0.0f) { @@ -50,85 +212,542 @@ namespace { } binding.previousValue = 0.0f; - if (g_callback == nullptr) { + EmitActionEvent(timestampUsec, binding.action, 0.0f); + } + } + + void AddAxisDescriptors(HidDevice& device, const HIDP_VALUE_CAPS& valueCaps, int& axisId) { + if (valueCaps.UsagePage != UsagePageGenericDesktop) { + return; + } + + if (valueCaps.IsRange) { + for (USAGE usage = valueCaps.Range.UsageMin; usage <= valueCaps.Range.UsageMax; usage++) { + if (!IsSupportedAxis(usage)) { + continue; + } + device.axes.push_back({ + axisId++, + valueCaps.UsagePage, + usage, + valueCaps, + std::numeric_limits::quiet_NaN(), + 0.0f, + 0, + }); + } + } else if (IsSupportedAxis(valueCaps.NotRange.Usage)) { + device.axes.push_back({ + axisId++, + valueCaps.UsagePage, + valueCaps.NotRange.Usage, + valueCaps, + std::numeric_limits::quiet_NaN(), + 0.0f, + 0, + }); + } + } + + // Returns NaN for a null-state reading (device reports "no valid data" for the axis). + float NormalizeAxisValue(ULONG rawValue, const HIDP_VALUE_CAPS& valueCaps) { + const LONG logicalMin = valueCaps.LogicalMin; + const LONG logicalMax = valueCaps.LogicalMax; + if (logicalMax == logicalMin) { + return 0.0f; + } + + // HidP_GetUsageValue returns the raw report bits in a ULONG without sign + // extension; devices with a signed logical range (accelerometers such as + // the Pinscape KL25Z) need it done manually from the report field width. + LONG value; + if (logicalMin < 0 && valueCaps.BitSize > 0 && valueCaps.BitSize < 32) { + const ULONG signBit = 1UL << (valueCaps.BitSize - 1); + const ULONG mask = (signBit << 1) - 1UL; + const ULONG masked = rawValue & mask; + value = (masked & signBit) ? static_cast(masked | ~mask) : static_cast(masked); + } else { + value = static_cast(rawValue); + } + + if (valueCaps.HasNull && (value < logicalMin || value > logicalMax)) { + return std::numeric_limits::quiet_NaN(); + } + + const float normalized = ((static_cast(value) - static_cast(logicalMin)) + / (static_cast(logicalMax) - static_cast(logicalMin))) * 2.0f - 1.0f; + return std::clamp(normalized, -1.0f, 1.0f); + } + + void CloseHidDevice(HidDevice& device) { + if (device.handle != INVALID_HANDLE_VALUE) { + if (device.readPending) { + CancelIoEx(device.handle, &device.readOverlap); + // Cancellation is asynchronous and may be requested from a different + // thread than the ReadFile issuer, so use CancelIoEx and then wait for + // the IRP to stop touching the OVERLAPPED/report buffer. + DWORD bytesRead = 0; + GetOverlappedResult(device.handle, &device.readOverlap, &bytesRead, TRUE); + device.readPending = false; + } + CloseHandle(device.handle); + device.handle = INVALID_HANDLE_VALUE; + } + if (device.readEvent != NULL) { + CloseHandle(device.readEvent); + device.readEvent = NULL; + } + if (device.preparsedData != nullptr) { + HidD_FreePreparsedData(device.preparsedData); + device.preparsedData = nullptr; + } + } + + void CloseHidDevices(std::vector& devices) { + for (auto& device : devices) { + CloseHidDevice(device); + } + devices.clear(); + } + + // Releases the device's handles but keeps its identity (path, name, axes) and + // deviceIndex, so the slot can be reported as disconnected and reopened in + // place when the device comes back. + void DisconnectHidDevice(HidDevice& device) { + CloseHidDevice(device); + device.isConnected = false; + } + + std::wstring ToLower(const std::wstring& value) { + std::wstring result(value); + for (auto& c : result) { + c = towlower(c); + } + return result; + } + + // Returns the multi-sz list of present HID interface paths — much cheaper + // than a SetupDi enumeration, so it can serve as a periodic change probe. + std::wstring GetHidInterfaceList() { + GUID hidGuid; + HidD_GetHidGuid(&hidGuid); + for (int attempt = 0; attempt < 3; attempt++) { + ULONG length = 0; + if (CM_Get_Device_Interface_List_SizeW(&length, &hidGuid, nullptr, CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CR_SUCCESS || length <= 1) { + return {}; + } + std::wstring buffer(length, L'\0'); + const CONFIGRET result = CM_Get_Device_Interface_ListW(&hidGuid, nullptr, buffer.data(), length, CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (result == CR_SUCCESS) { + return buffer; + } + if (result != CR_BUFFER_SMALL) { // list grew between the two calls: retry + return {}; + } + } + return {}; + } + + std::vector ParseInterfaceList(const std::wstring& multiSz) { + std::vector paths; + size_t offset = 0; + while (offset < multiSz.size() && multiSz[offset] != L'\0') { + const size_t end = multiSz.find(L'\0', offset); + paths.emplace_back(multiSz.substr(offset, end - offset)); + offset = (end == std::wstring::npos) ? multiSz.size() : end + 1; + } + return paths; + } + + bool OpenHidDevice(const std::wstring& path, bool overlapped, HidDevice& device) { + const DWORD flags = FILE_ATTRIBUTE_NORMAL | (overlapped ? FILE_FLAG_OVERLAPPED : 0); + // Listing only needs to query HID metadata, which works with zero access + // rights and doesn't clash with devices opened exclusively elsewhere. + const DWORD desiredAccess = overlapped ? GENERIC_READ : 0; + device.handle = CreateFileW( + path.c_str(), + desiredAccess, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + flags, + NULL); + if (device.handle == INVALID_HANDLE_VALUE) { + return false; + } + + device.path = path; + device.stableId = Narrow(path); + + wchar_t productString[128] = {}; + if (HidD_GetProductString(device.handle, productString, sizeof(productString))) { + device.displayName = Narrow(productString); + } + if (device.displayName.empty()) { + HIDD_ATTRIBUTES attributes{}; + attributes.Size = sizeof(attributes); + if (HidD_GetAttributes(device.handle, &attributes)) { + char buffer[64] = {}; + snprintf(buffer, sizeof(buffer), "HID %04X:%04X", attributes.VendorID, attributes.ProductID); + device.displayName = buffer; + } else { + device.displayName = "HID joystick"; + } + } + + if (!HidD_GetPreparsedData(device.handle, &device.preparsedData)) { + CloseHidDevice(device); + return false; + } + + if (HidP_GetCaps(device.preparsedData, &device.caps) != HIDP_STATUS_SUCCESS) { + CloseHidDevice(device); + return false; + } + + std::vector valueCaps(device.caps.NumberInputValueCaps); + USHORT valueCapsLength = device.caps.NumberInputValueCaps; + if (valueCapsLength > 0 && HidP_GetValueCaps(HidP_Input, valueCaps.data(), &valueCapsLength, device.preparsedData) == HIDP_STATUS_SUCCESS) { + int axisId = 0; + for (USHORT i = 0; i < valueCapsLength; i++) { + AddAxisDescriptors(device, valueCaps[i], axisId); + } + } + + if (!DeviceLooksLikeJoystick(device.caps)) { + CloseHidDevice(device); + return false; + } + if (device.axes.empty() || device.caps.InputReportByteLength == 0) { + CloseHidDevice(device); + return false; + } + + device.inputReport.resize(device.caps.InputReportByteLength); + if (overlapped) { + device.readEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (device.readEvent == NULL) { + CloseHidDevice(device); + return false; + } + device.readOverlap.hEvent = device.readEvent; + } + return true; + } + + void EnumerateHidDevices(std::vector& devices, bool openForPolling) { + CloseHidDevices(devices); + + GUID hidGuid; + HidD_GetHidGuid(&hidGuid); + HDEVINFO deviceInfoSet = SetupDiGetClassDevsW(&hidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + return; + } + + for (DWORD index = 0;; index++) { + SP_DEVICE_INTERFACE_DATA interfaceData{}; + interfaceData.cbSize = sizeof(interfaceData); + if (!SetupDiEnumDeviceInterfaces(deviceInfoSet, nullptr, &hidGuid, index, &interfaceData)) { + break; + } + + DWORD requiredSize = 0; + SetupDiGetDeviceInterfaceDetailW(deviceInfoSet, &interfaceData, nullptr, 0, &requiredSize, nullptr); + if (requiredSize == 0) { + continue; + } + + std::vector detailBuffer(requiredSize); + auto* detailData = reinterpret_cast(detailBuffer.data()); + detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + if (!SetupDiGetDeviceInterfaceDetailW(deviceInfoSet, &interfaceData, detailData, requiredSize, nullptr, nullptr)) { continue; } - VpeInputEvent event; - event.timestampUsec = timestampUsec; - event.action = static_cast(binding.action); - event.value = 0.0f; - event._padding = 0; - g_callback(&event, g_userData); + HidDevice device{}; + if (!OpenHidDevice(detailData->DevicePath, openForPolling, device)) { + continue; + } + device.deviceIndex = static_cast(devices.size()); + devices.push_back(std::move(device)); + } + + SetupDiDestroyDeviceInfoList(deviceInfoSet); + } + + // Reopens a previously disconnected device in its existing slot, keeping the + // deviceIndex stable. Axis ids stay stable too, since the same descriptor + // yields the same axis enumeration order. + bool ReopenHidDevice(HidDevice& device) { + HidDevice fresh{}; + if (!OpenHidDevice(device.path, true, fresh)) { + return false; + } + fresh.deviceIndex = device.deviceIndex; + device = std::move(fresh); + return true; + } + + // Aligns g_hidDevices with the currently present HID interfaces: missing + // devices are disconnected (their slot and deviceIndex are kept), re-plugged + // devices are reopened in place, and new joystick-style devices are appended. + // Returns true if anything changed; sets retryPending when a present device + // could not be reopened (transient open failure) so the caller can try again. + // Caller holds g_hidDevicesMutex. + bool ReconcileHidDevices(const std::vector& presentPaths, bool& retryPending) { + bool changed = false; + retryPending = false; + + std::vector presentLower; + presentLower.reserve(presentPaths.size()); + for (const auto& path : presentPaths) { + presentLower.push_back(ToLower(path)); + } + + for (auto& device : g_hidDevices) { + const auto deviceLower = ToLower(device.path); + const bool present = std::find(presentLower.begin(), presentLower.end(), deviceLower) != presentLower.end(); + if (!present) { + if (device.isConnected || device.handle != INVALID_HANDLE_VALUE) { + DisconnectHidDevice(device); + changed = true; + } + } else if (device.handle == INVALID_HANDLE_VALUE) { + if (ReopenHidDevice(device)) { + changed = true; + } else { + retryPending = true; + } + } + } + + for (size_t i = 0; i < presentPaths.size(); i++) { + bool known = false; + for (const auto& device : g_hidDevices) { + if (ToLower(device.path) == presentLower[i]) { + known = true; + break; + } + } + if (known) { + continue; + } + HidDevice device{}; + if (!OpenHidDevice(presentPaths[i], true, device)) { + continue; // not a joystick-style device (or open failed) + } + device.deviceIndex = static_cast(g_hidDevices.size()); + g_hidDevices.push_back(std::move(device)); + changed = true; + } + + return changed; + } + + void CopyDeviceInfo(const HidDevice& source, VpeInputDeviceInfo& destination) { + destination = {}; + destination.deviceIndex = source.deviceIndex; + destination.axisCount = static_cast(source.axes.size()); + destination.isConnected = source.isConnected ? 1 : 0; + CopyString(destination.stableId, VPE_INPUT_DEVICE_ID_SIZE, source.stableId); + CopyString(destination.displayName, VPE_INPUT_DEVICE_NAME_SIZE, source.displayName); + } + + void CopyAxisInfo(const AxisDescriptor& source, VpeInputAxisInfo& destination) { + destination = {}; + destination.axisId = source.axisId; + destination.usagePage = source.usagePage; + destination.usage = source.usage; + destination.kind = VPE_AXIS_KIND_POSITION; + destination.rawValue = source.rawValue; + destination.timestampUsec = source.timestampUsec; + CopyString(destination.name, VPE_INPUT_AXIS_NAME_SIZE, AxisName(source.usage)); + } + + void ParseInputReport(HidDevice& device, int64_t timestampUsec, std::vector& pendingEvents) { + if (device.preparsedData == nullptr || device.inputReport.empty()) { + return; + } + + auto* report = reinterpret_cast(device.inputReport.data()); + const ULONG reportLength = static_cast(device.inputReport.size()); + for (auto& axis : device.axes) { + ULONG value = 0; + const NTSTATUS status = HidP_GetUsageValue( + HidP_Input, + axis.usagePage, + axis.valueCaps.LinkCollection, + axis.usage, + &value, + device.preparsedData, + report, + reportLength); + if (status != HIDP_STATUS_SUCCESS) { + continue; + } + + const float normalized = NormalizeAxisValue(value, axis.valueCaps); + if (std::isnan(normalized)) { + continue; + } + axis.rawValue = normalized; + axis.timestampUsec = timestampUsec; + if (std::isnan(axis.previousValue) || std::fabs(normalized - axis.previousValue) > 0.0001f) { + axis.previousValue = normalized; + pendingEvents.push_back({ device.deviceIndex, axis.axisId, normalized }); + } + } + } + + void StartHidRead(HidDevice& device, int64_t timestampUsec, std::vector& pendingEvents) { + if (device.handle == INVALID_HANDLE_VALUE || device.readPending || device.inputReport.empty()) { + return; + } + + ResetEvent(device.readEvent); + DWORD bytesRead = 0; + const BOOL ok = ReadFile( + device.handle, + device.inputReport.data(), + static_cast(device.inputReport.size()), + &bytesRead, + &device.readOverlap); + if (ok) { + ParseInputReport(device, timestampUsec, pendingEvents); + return; + } + + const DWORD error = GetLastError(); + if (error == ERROR_IO_PENDING) { + device.readPending = true; + } else { + // The device is gone (unplugged) or otherwise wedged; stop hammering + // the dead handle and let the hotplug reconciliation deal with it. + DisconnectHidDevice(device); + g_hotplugCheckRequested.store(true, std::memory_order_release); + } + } + + void CompleteHidRead(HidDevice& device, int64_t timestampUsec, std::vector& pendingEvents) { + if (!device.readPending || device.readEvent == NULL) { + return; + } + if (WaitForSingleObject(device.readEvent, 0) != WAIT_OBJECT_0) { + return; + } + + DWORD bytesRead = 0; + const BOOL ok = GetOverlappedResult(device.handle, &device.readOverlap, &bytesRead, FALSE); + device.readPending = false; + if (ok && bytesRead > 0) { + ParseInputReport(device, timestampUsec, pendingEvents); + } else if (!ok) { + // Read completed with failure — typically device removal mid-read. + DisconnectHidDevice(device); + g_hotplugCheckRequested.store(true, std::memory_order_release); + } + } + + void PollHidDevices(int64_t timestampUsec, std::vector& pendingEvents) { + pendingEvents.clear(); + { + std::lock_guard lock(g_hidDevicesMutex); + for (auto& device : g_hidDevices) { + CompleteHidRead(device, timestampUsec, pendingEvents); + StartHidRead(device, timestampUsec, pendingEvents); + } + } + + for (const auto& event : pendingEvents) { + EmitAxisEvent(timestampUsec, event.deviceIndex, event.axisId, event.value); + } + } + + // Runs on the polling thread: cheap present-interface probe on a fixed + // cadence; the expensive reconciliation only runs when the probe result + // changed or a device read failed. Emits the devices-changed event outside + // the device mutex. + void CheckHotplug(int64_t timestampUsec, int64_t& nextCheckUsec, bool& reconcilePending) { + if (g_hotplugCheckRequested.exchange(false, std::memory_order_acq_rel)) { + // A read failure asked for reconciliation. React quickly but debounced, + // so a wedged device can't turn this into a per-tick reconcile loop. + reconcilePending = true; + const int64_t soonUsec = timestampUsec + 250000; + if (soonUsec < nextCheckUsec) { + nextCheckUsec = soonUsec; + } + } + if (timestampUsec < nextCheckUsec) { + return; + } + nextCheckUsec = timestampUsec + HotplugCheckIntervalUsec; + + const std::wstring interfaceList = GetHidInterfaceList(); + if (interfaceList.empty()) { + return; // probe failed; try again next cycle + } + if (!reconcilePending && interfaceList == g_lastHidInterfaceList) { + return; + } + g_lastHidInterfaceList = interfaceList; + + bool changed; + bool retryPending; + { + std::lock_guard lock(g_hidDevicesMutex); + changed = ReconcileHidDevices(ParseInterfaceList(interfaceList), retryPending); + } + reconcilePending = retryPending; + if (changed) { + EmitDevicesChangedEvent(timestampUsec); } } - // Polling thread function void PollingThreadFunc() { SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST); - + const HANDLE handles[2] = { g_timer, g_stopEvent }; + std::vector pendingAxisEvents; + int64_t nextHotplugCheckUsec = 0; + bool hotplugReconcilePending = false; while (g_running.load(std::memory_order_acquire)) { - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - int64_t timestampUsec = ((now.QuadPart - g_startTime.QuadPart) * 1000000LL) / g_frequency.QuadPart; - - bool isForeground = IsCurrentProcessForeground(); + const int64_t timestampUsec = GetTimestampUsecInternal(); + CheckHotplug(timestampUsec, nextHotplugCheckUsec, hotplugReconcilePending); + const bool isForeground = IsCurrentProcessForeground(); if (!isForeground) { + std::lock_guard lock(g_bindingsMutex); if (g_wasForeground) { EmitReleaseForPressedKeys(timestampUsec); } g_wasForeground = false; + } else { + { + std::lock_guard lock(g_bindingsMutex); + g_wasForeground = true; - if (g_pollIntervalUs > 0 && g_timer != NULL) { - LARGE_INTEGER dueTime; - dueTime.QuadPart = -(static_cast(g_pollIntervalUs) * 10); - SetWaitableTimer(g_timer, &dueTime, 0, NULL, NULL, FALSE); - DWORD waitResult = WaitForMultipleObjects(2, handles, FALSE, INFINITE); - if (waitResult == WAIT_OBJECT_0 + 1) { - break; - } - } else { - DWORD waitResult = WaitForSingleObject(g_stopEvent, 1); - if (waitResult == WAIT_OBJECT_0) { - break; + for (auto& binding : g_bindings) { + SHORT keyState = GetAsyncKeyState(binding.keyCode); + bool isPressed = (keyState & 0x8000) != 0; + float currentValue = isPressed ? 1.0f : 0.0f; + + if (currentValue != binding.previousValue) { + binding.previousValue = currentValue; + EmitActionEvent(timestampUsec, binding.action, currentValue); + } } } - continue; - } - - g_wasForeground = true; - - // Poll all bound keys - for (auto& binding : g_bindings) { - SHORT keyState = GetAsyncKeyState(binding.keyCode); - bool isPressed = (keyState & 0x8000) != 0; - float currentValue = isPressed ? 1.0f : 0.0f; - if (currentValue != binding.previousValue) { - binding.previousValue = currentValue; + PollHidDevices(timestampUsec, pendingAxisEvents); + } - // Fire event - if (g_callback) { - VpeInputEvent event; - event.timestampUsec = timestampUsec; - event.action = static_cast(binding.action); - event.value = currentValue; - event._padding = 0; - g_callback(&event, g_userData); - } - } - } - - // Wait for next poll interval or stop event. if (g_pollIntervalUs > 0 && g_timer != NULL) { - // Relative due time in 100ns units (negative = relative) LARGE_INTEGER dueTime; dueTime.QuadPart = -(static_cast(g_pollIntervalUs) * 10); SetWaitableTimer(g_timer, &dueTime, 0, NULL, NULL, FALSE); DWORD waitResult = WaitForMultipleObjects(2, handles, FALSE, INFINITE); if (waitResult == WAIT_OBJECT_0 + 1) { - break; // stop event + break; } } else { DWORD waitResult = WaitForSingleObject(g_stopEvent, 1); @@ -139,15 +758,20 @@ namespace { } } } - -extern "C" { - + +extern "C" { + VPE_API int VpeInputInit(void) { - // Initialize high-resolution timer - if (!QueryPerformanceFrequency(&g_frequency)) { - return 0; + g_protocolVersionChecked.store(false, std::memory_order_release); + + // Only establish the timestamp epoch once; a re-init while a consumer is + // live must not make timestamps jump backwards. + if (g_frequency.QuadPart == 0) { + if (!QueryPerformanceFrequency(&g_frequency)) { + return 0; + } + QueryPerformanceCounter(&g_startTime); } - QueryPerformanceCounter(&g_startTime); if (g_timer == NULL) { g_timer = CreateWaitableTimer(NULL, FALSE, NULL); } @@ -156,10 +780,23 @@ VPE_API int VpeInputInit(void) { } return 1; } - + +VPE_API int VpeInputGetProtocolVersion(void) { + g_protocolVersionChecked.store(true, std::memory_order_release); + return VPE_INPUT_PROTOCOL_VERSION; +} + VPE_API void VpeInputShutdown(void) { VpeInputStopPolling(); - g_bindings.clear(); + g_protocolVersionChecked.store(false, std::memory_order_release); + { + std::lock_guard lock(g_bindingsMutex); + g_bindings.clear(); + } + { + std::lock_guard lock(g_hidDevicesMutex); + CloseHidDevices(g_hidDevices); + } if (g_timer != NULL) { CloseHandle(g_timer); g_timer = NULL; @@ -169,8 +806,9 @@ VPE_API void VpeInputShutdown(void) { g_stopEvent = NULL; } } - + VPE_API void VpeInputSetBindings(const VpeInputBinding* bindings, int count) { + std::lock_guard lock(g_bindingsMutex); g_bindings.clear(); g_wasForeground = false; @@ -182,15 +820,14 @@ VPE_API void VpeInputSetBindings(const VpeInputBinding* bindings, int count) { 0.0f, }); } - // TODO: Add gamepad support } } - + VPE_API int VpeInputStartPolling(VpeInputEventCallback callback, void* userData, int pollIntervalUs) { - if (g_running.load(std::memory_order_acquire)) { - return 0; // Already running + if (!g_protocolVersionChecked.load(std::memory_order_acquire) || g_running.load(std::memory_order_acquire)) { + return 0; } - + g_callback = callback; g_userData = userData; g_pollIntervalUs = pollIntervalUs; @@ -198,17 +835,28 @@ VPE_API int VpeInputStartPolling(VpeInputEventCallback callback, void* userData, if (g_stopEvent != NULL) { ResetEvent(g_stopEvent); } + { + std::lock_guard lock(g_hidDevicesMutex); + EnumerateHidDevices(g_hidDevices, true); + } + // Baseline for the hotplug probe; the polling thread reconciles against this. + g_lastHidInterfaceList = GetHidInterfaceList(); + g_hotplugCheckRequested.store(false, std::memory_order_release); g_running.store(true, std::memory_order_release); - - try { - g_pollingThread = std::thread(PollingThreadFunc); - return 1; - } catch (...) { - g_running.store(false, std::memory_order_release); - return 0; - } -} - + + try { + g_pollingThread = std::thread(PollingThreadFunc); + return 1; + } catch (...) { + g_running.store(false, std::memory_order_release); + { + std::lock_guard lock(g_hidDevicesMutex); + CloseHidDevices(g_hidDevices); + } + return 0; + } +} + VPE_API void VpeInputStopPolling(void) { if (g_running.load(std::memory_order_acquire)) { g_running.store(false, std::memory_order_release); @@ -219,18 +867,85 @@ VPE_API void VpeInputStopPolling(void) { g_pollingThread.join(); } } + { + std::lock_guard lock(g_hidDevicesMutex); + CloseHidDevices(g_hidDevices); + } +} + +VPE_API int VpeInputListDevices(VpeInputDeviceInfo* devices, int maxDevices) { + // While polling is active, serve the polling snapshot: it is the only set of + // device indices consistent with the indices carried by axis events, and its + // axis values are live. A fresh enumeration may order devices differently. + if (g_running.load(std::memory_order_acquire)) { + std::lock_guard lock(g_hidDevicesMutex); + const int count = static_cast(g_hidDevices.size()); + if (devices != nullptr && maxDevices > 0) { + const int copyCount = std::min(count, maxDevices); + for (int i = 0; i < copyCount; i++) { + CopyDeviceInfo(g_hidDevices[i], devices[i]); + } + } + return count; + } + + std::vector enumeratedDevices; + EnumerateHidDevices(enumeratedDevices, false); + const int count = static_cast(enumeratedDevices.size()); + if (devices != nullptr && maxDevices > 0) { + const int copyCount = std::min(count, maxDevices); + for (int i = 0; i < copyCount; i++) { + CopyDeviceInfo(enumeratedDevices[i], devices[i]); + } + } + CloseHidDevices(enumeratedDevices); + return count; +} + +VPE_API int VpeInputListDeviceAxes(int deviceIndex, VpeInputAxisInfo* axes, int maxAxes) { + if (g_running.load(std::memory_order_acquire)) { + std::lock_guard lock(g_hidDevicesMutex); + if (deviceIndex < 0 || deviceIndex >= static_cast(g_hidDevices.size())) { + return 0; + } + const auto& device = g_hidDevices[deviceIndex]; + const int count = static_cast(device.axes.size()); + if (axes != nullptr && maxAxes > 0) { + const int copyCount = std::min(count, maxAxes); + for (int i = 0; i < copyCount; i++) { + CopyAxisInfo(device.axes[i], axes[i]); + } + } + return count; + } + + std::vector enumeratedDevices; + EnumerateHidDevices(enumeratedDevices, false); + if (deviceIndex < 0 || deviceIndex >= static_cast(enumeratedDevices.size())) { + CloseHidDevices(enumeratedDevices); + return 0; + } + + const auto& device = enumeratedDevices[deviceIndex]; + const int count = static_cast(device.axes.size()); + if (axes != nullptr && maxAxes > 0) { + const int copyCount = std::min(count, maxAxes); + for (int i = 0; i < copyCount; i++) { + CopyAxisInfo(device.axes[i], axes[i]); + } + } + CloseHidDevices(enumeratedDevices); + return count; +} + +VPE_API int64_t VpeGetTimestampUsec(void) { + return GetTimestampUsecInternal(); } - -VPE_API int64_t VpeGetTimestampUsec(void) { - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - return ((now.QuadPart - g_startTime.QuadPart) * 1000000LL) / g_frequency.QuadPart; -} - -VPE_API void VpeSetThreadPriority(void) { - SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); -} - -} // extern "C" - -#endif // _WIN32 + +VPE_API void VpeSetThreadPriority(void) { + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); +} + +} // extern "C" + +#endif // _WIN32