Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
12 changes: 8 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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})

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
111 changes: 86 additions & 25 deletions src/NativeInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -117,24 +134,55 @@ 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,
VPE_KEY_S = 0x53,
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;
Comment thread
freezy marked this conversation as resolved.

// Input binding structure
typedef struct {
Expand All @@ -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
Expand All @@ -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);
Expand Down
31 changes: 30 additions & 1 deletion src/NativeInput_Linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace {

std::thread g_pollingThread;
std::atomic<bool> g_running(false);
std::atomic<bool> g_protocolVersionChecked(false);
std::condition_variable g_waitCondition;
std::mutex g_waitMutex;
VpeInputEventCallback g_callback = nullptr;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -261,6 +266,7 @@ namespace {

VpeInputEvent event{};
event.timestampUsec = timestampUsec;
event.eventType = VPE_INPUT_EVENT_ACTION;
event.action = static_cast<int32_t>(binding.action);
event.value = 0.0f;
g_callback(&event, g_userData);
Expand Down Expand Up @@ -298,6 +304,7 @@ namespace {

VpeInputEvent event{};
event.timestampUsec = timestampUsec;
event.eventType = VPE_INPUT_EVENT_ACTION;
event.action = static_cast<int32_t>(binding.action);
event.value = currentValue;
g_callback(&event, g_userData);
Expand All @@ -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) {
Expand All @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
}
Expand Down
32 changes: 31 additions & 1 deletion src/NativeInput_Mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

std::thread g_pollingThread;
std::atomic<bool> g_running(false);
std::atomic<bool> g_protocolVersionChecked(false);
std::condition_variable g_waitCondition;
std::mutex g_waitMutex;
VpeInputEventCallback g_callback = nullptr;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -225,6 +232,7 @@ void EmitReleaseForPressedKeys(int64_t timestampUsec) {

VpeInputEvent event{};
event.timestampUsec = timestampUsec;
event.eventType = VPE_INPUT_EVENT_ACTION;
event.action = static_cast<int32_t>(binding.action);
event.value = 0.0f;
g_callback(&event, g_userData);
Expand Down Expand Up @@ -260,6 +268,7 @@ void PollingThreadFunc() {

VpeInputEvent event{};
event.timestampUsec = timestampUsec;
event.eventType = VPE_INPUT_EVENT_ACTION;
event.action = static_cast<int32_t>(binding.action);
event.value = currentValue;
g_callback(&event, g_userData);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
}
Expand Down
Loading
Loading