-
Notifications
You must be signed in to change notification settings - Fork 6
Virtual Controllers
Developer reference for all three IVirtualController implementations.
Source files:
-
PadForge.Engine/Common/VirtualControllerTypes.cs-- interface + enum PadForge.App/Common/Input/Xbox360VirtualController.csPadForge.App/Common/Input/DS4VirtualController.csPadForge.App/Common/Input/VJoyVirtualController.cs
namespace PadForge.Engine
{
public enum VirtualControllerType
{
Xbox360 = 0,
DualShock4 = 1,
VJoy = 2
}
public interface IVirtualController : IDisposable
{
VirtualControllerType Type { get; }
bool IsConnected { get; }
void Connect();
void Disconnect();
void SubmitGamepadState(Gamepad gp);
void RegisterFeedbackCallback(int padIndex, Vibration[] vibrationStates);
}
}The Gamepad struct uses the XInput layout (signed short axes, byte triggers, ushort button bitmask). All three implementations receive the same Gamepad and translate it to their output format.
Namespace: PadForge.Common.Input
Visibility: internal sealed
NuGet dependency: Nefarius.ViGEm.Client
| Field | Type | Description |
|---|---|---|
_controller |
IXbox360Controller |
ViGEm Xbox 360 controller instance (readonly) |
_disposed |
bool |
Dispose guard |
| Property | Type | Description |
|---|---|---|
Type |
VirtualControllerType |
Always VirtualControllerType.Xbox360
|
IsConnected |
bool |
True after Connect(), false after Disconnect()
|
public Xbox360VirtualController(ViGEmClient client)Creates an Xbox 360 virtual controller via client.CreateXbox360Controller(). The ViGEmClient is owned externally (by InputManager); multiple controllers share one client.
public void Connect()Calls _controller.Connect() on the ViGEm bus. After this call, Windows sees a new Xbox 360 controller. Sets IsConnected = true.
public void Disconnect()Calls _controller.Disconnect(). The virtual device is removed from Windows. Sets IsConnected = false.
public void Dispose()Guarded by _disposed. Calls Disconnect() if still connected, then disposes the underlying controller via (IDisposable)?.Dispose().
public void SubmitGamepadState(Gamepad gp)Maps the Gamepad struct to Xbox 360 report format and submits:
Buttons (direct bitmask check against Gamepad constants):
| Gamepad Flag | Xbox360Button |
|---|---|
Gamepad.A |
Xbox360Button.A |
Gamepad.B |
Xbox360Button.B |
Gamepad.X |
Xbox360Button.X |
Gamepad.Y |
Xbox360Button.Y |
Gamepad.LEFT_SHOULDER |
Xbox360Button.LeftShoulder |
Gamepad.RIGHT_SHOULDER |
Xbox360Button.RightShoulder |
Gamepad.BACK |
Xbox360Button.Back |
Gamepad.START |
Xbox360Button.Start |
Gamepad.LEFT_THUMB |
Xbox360Button.LeftThumb |
Gamepad.RIGHT_THUMB |
Xbox360Button.RightThumb |
Gamepad.GUIDE |
Xbox360Button.Guide |
Gamepad.DPAD_UP |
Xbox360Button.Up |
Gamepad.DPAD_DOWN |
Xbox360Button.Down |
Gamepad.DPAD_LEFT |
Xbox360Button.Left |
Gamepad.DPAD_RIGHT |
Xbox360Button.Right |
Axes (signed short, direct passthrough):
| Gamepad Field | Xbox360Axis |
|---|---|
ThumbLX |
LeftThumbX |
ThumbLY |
LeftThumbY |
ThumbRX |
RightThumbX |
ThumbRY |
RightThumbY |
Triggers (byte, direct passthrough):
| Gamepad Field | Xbox360Slider |
|---|---|
LeftTrigger |
LeftTrigger |
RightTrigger |
RightTrigger |
Finishes with _controller.SubmitReport().
public void RegisterFeedbackCallback(int padIndex, Vibration[] vibrationStates)Subscribes to _controller.FeedbackReceived. The callback:
- Captures
padIndexin a closure (capturedIndex). - Reads
args.LargeMotorandargs.SmallMotor(byte 0-255 from ViGEm). - Scales to ushort:
(ushort)(args.LargeMotor * 257)and(ushort)(args.SmallMotor * 257). - Writes to
vibrationStates[capturedIndex].LeftMotorSpeed/.RightMotorSpeed. - Logs via
RumbleLogger.Log()on change detection.
The callback runs on the ViGEm thread (not the polling thread). The Vibration struct fields are ushort, and writes are atomic. Step 2 reads these values on the polling thread for rumble forwarding.
Namespace: PadForge.Common.Input
Visibility: internal sealed
NuGet dependency: Nefarius.ViGEm.Client
| Field | Type | Description |
|---|---|---|
_controller |
IDualShock4Controller |
ViGEm DualShock 4 controller instance (readonly) |
_disposed |
bool |
Dispose guard |
| Property | Type | Description |
|---|---|---|
Type |
VirtualControllerType |
Always VirtualControllerType.DualShock4
|
IsConnected |
bool |
True after Connect(), false after Disconnect()
|
public DS4VirtualController(ViGEmClient client)Creates a DualShock 4 virtual controller via client.CreateDualShock4Controller().
Same pattern as Xbox360VirtualController.
public void SubmitGamepadState(Gamepad gp)Maps the Gamepad struct to DS4 report format. Key differences from Xbox 360:
Face buttons (different naming convention):
| Gamepad Flag | DualShock4Button |
|---|---|
Gamepad.A |
DualShock4Button.Cross |
Gamepad.B |
DualShock4Button.Circle |
Gamepad.X |
DualShock4Button.Square |
Gamepad.Y |
DualShock4Button.Triangle |
Shoulder buttons:
| Gamepad Flag | DualShock4Button |
|---|---|
Gamepad.LEFT_SHOULDER |
DualShock4Button.ShoulderLeft |
Gamepad.RIGHT_SHOULDER |
DualShock4Button.ShoulderRight |
Center buttons:
| Gamepad Flag | DualShock4Button |
|---|---|
Gamepad.BACK |
DualShock4Button.Share |
Gamepad.START |
DualShock4Button.Options |
Thumbstick clicks: ThumbLeft, ThumbRight
Special buttons:
| Gamepad Flag | DS4 Button |
|---|---|
Gamepad.GUIDE |
DualShock4SpecialButton.Ps |
Digital trigger buttons (DS4-specific -- pressed when analog trigger > 0):
_controller.SetButtonState(DualShock4Button.TriggerLeft, gp.LeftTrigger > 0);
_controller.SetButtonState(DualShock4Button.TriggerRight, gp.RightTrigger > 0);D-Pad (mapped to hat switch via GetDPadDirection()):
private static DualShock4DPadDirection GetDPadDirection(bool up, bool down, bool left, bool right)Returns one of 9 values: None, North, Northeast, East, Southeast, South, Southwest, West, Northwest. Priority order: diagonals first (up+right, up+left, down+right, down+left), then cardinals.
Axes (signed short to unsigned byte conversion):
private static byte ShortToByte(short value) => (byte)((value + 32768) >> 8);
private static byte ShortToByteInvertY(short value) => (byte)((32767 - value) >> 8);DS4 axes use byte 0-255 with center at 128. Y-axes are inverted (0=up in DS4 vs positive=up in Xbox).
| Gamepad Field | DualShock4Axis | Conversion |
|---|---|---|
ThumbLX |
LeftThumbX |
ShortToByte |
ThumbLY |
LeftThumbY |
ShortToByteInvertY |
ThumbRX |
RightThumbX |
ShortToByte |
ThumbRY |
RightThumbY |
ShortToByteInvertY |
Triggers (byte direct passthrough):
| Gamepad Field | DualShock4Slider |
|---|---|
LeftTrigger |
LeftTrigger |
RightTrigger |
RightTrigger |
Finishes with _controller.SubmitReport().
public void RegisterFeedbackCallback(int padIndex, Vibration[] vibrationStates)Same pattern as Xbox360. Uses #pragma warning disable CS0618 because FeedbackReceived is marked obsolete but still functional in the ViGEm client library.
Namespace: PadForge.Common.Input
Visibility: internal sealed
No NuGet dependency -- uses direct P/Invoke to vJoyInterface.dll.
This is the most complex implementation. It manages driver-level device nodes, HID report descriptors, registry entries, and force feedback routing in addition to standard gamepad output.
| Field | Type | Description |
|---|---|---|
_dllLoaded |
bool |
Whether vJoyInterface.dll has been successfully loaded |
_currentDescriptorCount |
int |
Number of DeviceNN registry descriptors currently written |
_driverStoreChecked |
bool |
Whether driver store check has run this session |
_generation |
int |
Incremented on device node restart; triggers handle re-acquire |
_ffbLock |
object |
Lock protecting FFB state |
_ffbCallbackRegistered |
bool |
Whether the global FFB callback is registered |
_ffbCallbackDelegate |
VJoyNative.FfbGenCB |
Strong reference to prevent GC of the callback delegate |
_ffbDeviceMap |
Dictionary<uint, (int padIndex, Vibration[] states)> |
Routes vJoy device ID to vibration output |
_ffbDeviceStates |
Dictionary<uint, FfbDeviceState> |
Per-device FFB effect tracking |
_lastDeviceConfigs |
VJoyDeviceConfig[] |
Cached per-device configs from last EnsureDevicesAvailable call |
| Property | Type | Description |
|---|---|---|
CurrentDescriptorCount |
int |
Read-only accessor for _currentDescriptorCount. Used by Step 5 |
IsDllLoaded |
bool |
Whether vJoyInterface.dll is loaded |
| Field | Type | Description |
|---|---|---|
_deviceId |
uint |
vJoy device ID (1-16), readonly |
_connected |
bool |
Whether this controller is connected |
_connectedGeneration |
int |
Generation at time of Connect()
|
_submitCallCount |
int |
Diagnostic counter for SubmitGamepadState calls |
_submitFailCount |
int |
Diagnostic counter for failed UpdateVJD calls |
| Property | Type | Description |
|---|---|---|
Type |
VirtualControllerType |
Always VirtualControllerType.VJoy
|
IsConnected |
bool |
Read from _connected
|
DeviceId |
uint |
The vJoy device ID (1-16) |
public VJoyVirtualController(uint deviceId)Validates deviceId is in range 1-16. Throws ArgumentOutOfRangeException if not.
internal static void EnsureDllLoaded()Preloads vJoyInterface.dll into the process. Strategy:
- If
_dllLoadedis true, return immediately. - Try
NativeLibrary.TryLoad("vJoyInterface.dll")(default search paths). - Try
C:\Program Files\vJoy\vJoyInterface.dll. - Try
C:\Program Files\vJoy\x64\vJoyInterface.dll(legacy installs). - Only sets
_dllLoaded = trueon success. Retries on next call if not found.
IMPORTANT: Do NOT use NativeLibrary.SetDllImportResolver -- it hijacks the entire assembly's DLL resolution.
internal static void ResetState()Resets all cached static state: _dllLoaded = false, _currentDescriptorCount = 0, _driverStoreChecked = false, increments _generation. Called after driver reinstall.
public void Connect()- Calls
EnsureDllLoaded(). - Checks
GetVJDStatus(_deviceId)-- must beVJD_STAT_FREE. - Calls
AcquireVJD(_deviceId). - Calls
ResetVJD(_deviceId). - Sets
_connected = true, captures_connectedGeneration = _generation. - Sends a test frame via
UpdateVJDwith non-zero axes and centered POV hats (0xFFFF_FFFF).
Throws InvalidOperationException if device is not free or acquisition fails.
public void Disconnect()- Removes FFB routing for this device from
_ffbDeviceMapand_ffbDeviceStates(under_ffbLock). - Calls
ResetVJD(_deviceId)andRelinquishVJD(_deviceId). - Sets
_connected = false.
public void ReAcquireIfNeeded()Called by Step 5 after EnsureDevicesAvailable to ensure existing controllers re-claim their device IDs BEFORE FindFreeDeviceId() runs for new controllers. If _connectedGeneration != _generation, relinquishes and re-acquires the device. Non-fatal on failure (retries next cycle).
public void SubmitGamepadState(Gamepad gp)Uses a single UpdateVJD(rID, ref JoystickPositionV2) call per frame (1 kernel IOCTL). NEVER use individual SetAxis/SetBtn/SetDiscPov calls -- each is a separate kernel IOCTL (~1-2ms), causing 1000Hz to drop to 11Hz with 2 controllers.
Generation check: If _connectedGeneration != _generation, transparently re-acquires the device handle before submitting.
Axis conversion (signed short to vJoy unsigned range 0-32767):
int lx = (gp.ThumbLX + 32768) / 2;
int ly = 32767 - (gp.ThumbLY + 32768) / 2; // Y inverted (HID Y-down=max)
int rx = (gp.ThumbRX + 32768) / 2;
int ry = 32767 - (gp.ThumbRY + 32768) / 2; // Y inverted
int lt = gp.LeftTrigger * 32767 / 255;
int rt = gp.RightTrigger * 32767 / 255;Y-axis inversion: HID convention is Y-down=max value. Formula: 32767 - (value + 32768) / 2.
Axis mapping to JoystickPositionV2 fields:
| Gamepad | JoystickPositionV2 Field |
|---|---|
ThumbLX |
wAxisX |
ThumbLY |
wAxisY (inverted) |
LeftTrigger |
wAxisZ |
ThumbRX |
wAxisXRot |
ThumbRY |
wAxisYRot (inverted) |
RightTrigger |
wAxisZRot |
Button bitmask (11 buttons, positions 0-10): A, B, X, Y, LB, RB, Back, Start, LS, RS, Guide.
D-Pad to continuous POV hat (centidegrees):
| Direction | Value |
|---|---|
| North | 0 |
| Northeast | 4500 |
| East | 9000 |
| Southeast | 13500 |
| South | 18000 |
| Southwest | 22500 |
| West | 27000 |
| Northwest | 31500 |
| Centered | -1 (stored as 0xFFFF_FFFF) |
Unused POV hats (bHatsEx1, bHatsEx2, bHatsEx3) are set to 0xFFFF_FFFF (centered).
public void SubmitRawState(VJoyRawState raw)Submits a VJoyRawState directly, bypassing the Gamepad struct. Used for custom vJoy configurations with arbitrary axis/button/POV counts.
Axis mapping (supports up to 16 axes):
| Index | JoystickPositionV2 Field |
|---|---|
| 0 | wAxisX |
| 1 | wAxisY |
| 2 | wAxisZ |
| 3 | wAxisXRot |
| 4 | wAxisYRot |
| 5 | wAxisZRot |
| 6 | wSlider |
| 7 | wDial |
| 8 | wWheel |
| 9 | wAxisVX |
| 10 | wAxisVY |
| 11 | wAxisVZ |
| 12 | wAxisVBRX |
| 13 | wAileron |
| 14 | wRudder |
| 15 | wThrottle |
Button mapping: raw.Buttons is uint[] where each uint is 32 button bits. Maps to lButtons, lButtonsEx1, lButtonsEx2, lButtonsEx3 (128 buttons total).
POV mapping: raw.Povs is int[]. Value -1 = centered (0xFFFFFFFF), else direct centidegree value. Maps to bHats, bHatsEx1, bHatsEx2, bHatsEx3.
public void RegisterFeedbackCallback(int padIndex, Vibration[] vibrationStates)Registers this device for FFB routing:
- Under
_ffbLock, adds_deviceId -> (padIndex, vibrationStates)to_ffbDeviceMap. - If
_ffbCallbackRegisteredis false, registersFfbCallbackviaVJoyNative.FfbRegisterGenCB(). - Keeps a strong reference to the delegate in
_ffbCallbackDelegateto prevent GC. - Catches
DllNotFoundExceptiongracefully.
The global callback is shared across all vJoy devices -- it routes by device ID.
private class FfbDeviceState
{
public byte DeviceGain = 255; // 0-255, default 100%
public Dictionary<byte, FfbEffectState> Effects = new(); // keyed by EffectBlockIndex
}private class FfbEffectState
{
public FFBEType Type;
public int Magnitude; // absolute, 0-10000
public byte Gain = 255; // per-effect gain (0-255)
public ushort Duration; // ms, 0xFFFF=infinite
public bool Running;
public ushort Direction; // polar direction 0-32767
}private static void FfbCallback(IntPtr data, IntPtr userData)Global callback invoked by vJoyInterface.dll on its thread pool. Routes FFB packets by device ID. Handles these packet types:
| Packet Type | Handler | Description |
|---|---|---|
PT_EFFREP |
Ffb_h_Eff_Report |
Set Effect Report: type, gain, direction, duration |
PT_CONSTREP |
Ffb_h_Eff_Constant |
Set Constant Force: signed magnitude (-10000..+10000) |
PT_PRIDREP |
Ffb_h_Eff_Period |
Set Periodic (Sine/Square/Triangle): unsigned magnitude (0..10000) |
PT_RAMPREP |
Ffb_h_Eff_Ramp |
Set Ramp Force: uses max of abs(Start), abs(End) |
PT_CONDREP |
Ffb_h_Eff_Cond |
Set Condition (Spring/Damper/Friction/Inertia): uses max of pos/neg coefficients |
PT_EFOPREP |
Ffb_h_EffOp |
Effect Operation: EFF_START, EFF_SOLO, EFF_STOP |
PT_GAINREP |
Ffb_h_DevGain |
Device Gain (0-255) |
PT_CTRLREP |
Ffb_h_DevCtrl |
Device Control: CTRL_STOPALL, CTRL_DEVRST, CTRL_DISACT |
PT_BLKFRREP |
Ffb_h_EffectBlockIndex |
Block Free: deletes effect |
private static void ApplyMotorOutput(uint deviceId, FfbDeviceState devState)Computes aggregate motor output from all running effects:
- Sums
magnitude * (effectGain / 255.0)for all running effects with non-zero magnitude. - Applies device-level gain:
motorSum *= deviceGain / 255.0. - Scales from 0-10000 to 0-65535 (ushort).
- Writes equal value to both
LeftMotorSpeedandRightMotorSpeed(Xbox/DS4 don't have directional rumble).
public static bool CheckVJoyInstalled()Returns true if the vJoy driver is installed and enabled. Catches DllNotFoundException gracefully.
public static uint FindFreeDeviceId()Scans device IDs 1-16, returns the first with VJD_STAT_FREE status. Returns 0 if none available. Fast, non-blocking -- safe for the engine thread.
public static bool IsServiceStuck()Checks if the vjoy service is in STOP_PENDING state via sc.exe query vjoy. This zombie state occurs when a previous uninstall removed the service before device nodes -- only a full restart clears it.
public static int CountExistingDevices()Counts existing vJoy device nodes via pnputil /enum-devices /class HIDClass. More reliable than GetVJDStatus which returns stale data.
public static bool EnsureDevicesAvailable(int requiredCount, VJoyDeviceConfig[] perDeviceConfigs)
public static bool EnsureDevicesAvailable(int requiredCount = 1)Ensures the specified number of vJoy virtual joysticks are available. Architecture: ONE device node + N registry descriptor keys.
VJoyDeviceConfig struct:
public struct VJoyDeviceConfig
{
public int Axes;
public int Buttons;
public int Povs;
public int Sticks; // thumbsticks (each uses 2 axes)
public int Triggers; // each uses 1 axis
}Flow:
- First call:
EnsureDriverInStore()andEnsureFfbRegistryKeys(). - Fast path: if count and configs match cached state, skip expensive operations.
- Writes registry descriptors via
WriteDeviceDescriptors(). - If
requiredCount == 0: disables the device node (not remove -- avoids stale DLL handles). - If no device node exists: creates one via
CreateVJoyDevices(1), waits up to 5s for PnP binding. - If excess nodes exist: removes extras, keeps one.
- If descriptors changed: restarts node (disable/enable via pnputil), waits up to 5s.
- Increments
_generationon restart so connected controllers know to re-acquire.
totalVJoyNeeded race fix: Count must use SlotControllerTypes[i] == VJoy && SlotCreated[i], NOT check VC lifecycle state. During EnsureTypeGroupOrder bubble sort (UI thread), the polling thread can catch transient state where a vJoy slot's type was swapped mid-sort, causing undercount.
internal static bool CreateVJoyDevices(int count = 1)Creates device nodes using SetupAPI in an elevated PowerShell script. Steps:
SetupDiCreateDeviceInfoList(HIDClass GUID)-
SetupDiCreateDeviceInfoW("HIDClass", DICD_GENERATE_ID)-- Critical: DeviceName must be"HIDClass"(class name), NOT the hardware ID. SetupDiSetDeviceRegistryPropertyW(SPDRP_HARDWAREID, "root\VID_1234&PID_BEAD&REV_0222")SetupDiCallClassInstaller(DIF_REGISTERDEVICE)-
UpdateDriverForPlugAndPlayDevicesWwith flag 0 (no INSTALLFLAG_FORCE).
internal static bool RemoveDeviceNode(string instanceId)
internal static bool RemoveAllDeviceNodes()Removes device nodes via pnputil /remove-device "{instanceId}" /subtree. Resets _dllLoaded and _currentDescriptorCount after removal.
private static bool WriteDeviceDescriptors(int requiredCount, VJoyDeviceConfig[] perDeviceConfigs)Writes HID report descriptors to HKLM\SYSTEM\CurrentControlSet\services\vjoy\Parameters\DeviceNN. Only writes when descriptors differ from existing (avoids disturbing live devices). Excess DeviceNN keys beyond required count are removed. Returns true if any registry changes occurred.
private static byte[] BuildHidDescriptor(byte reportId, int nAxes, int nButtons, int nPovs)Builds a HID Report Descriptor matching vJoyConf format. Fixed 97-byte report layout. See vJoy Deep Dive for full descriptor format.
private static void AppendFfbDescriptor(List<byte> d, byte reportId)Appends the full PID (Physical Interface Device) HID descriptor for force feedback. Report IDs are offset by 0x10 * reportId (1-based). See vJoy Deep Dive for full FFB descriptor.
private static void EnsureFfbRegistryKeys()Writes OEMForceFeedback registry keys to HKCU\...\OEM\VID_1234&PID_BEAD\OEMForceFeedback. Required for DirectInput to enumerate the device as FFB-capable. Keys include CLSID, Attributes, and 11 effect GUIDs (ConstantForce, RampForce, Square, Sine, Triangle, SawtoothUp, SawtoothDown, Spring, Damper, Inertia, Friction).
internal static class VJoyNative
{
bool vJoyEnabled();
VjdStat GetVJDStatus(uint rID);
bool AcquireVJD(uint rID);
void RelinquishVJD(uint rID);
bool ResetVJD(uint rID);
bool UpdateVJD(uint rID, ref JoystickPositionV2 pData);
bool SetAxis(int value, uint rID, uint axis);
bool SetBtn(bool value, uint rID, byte nBtn);
bool SetDiscPov(int value, uint rID, byte nPov);
// FFB
delegate void FfbGenCB(IntPtr data, IntPtr userData);
void FfbRegisterGenCB(FfbGenCB cb, IntPtr data);
uint Ffb_h_DeviceID(IntPtr packet, ref uint deviceId);
uint Ffb_h_Type(IntPtr packet, ref FFBPType type);
uint Ffb_h_EffectBlockIndex(IntPtr packet, ref uint index);
uint Ffb_h_Eff_Report(IntPtr packet, ref FFB_EFF_REPORT effect);
uint Ffb_h_Eff_Constant(IntPtr packet, ref FFB_EFF_CONSTANT effect);
uint Ffb_h_Eff_Ramp(IntPtr packet, ref FFB_EFF_RAMP effect);
uint Ffb_h_Eff_Period(IntPtr packet, ref FFB_EFF_PERIOD effect);
uint Ffb_h_Eff_Cond(IntPtr packet, ref FFB_EFF_COND effect);
uint Ffb_h_EffOp(IntPtr packet, ref FFB_EFF_OP operation);
uint Ffb_h_DevCtrl(IntPtr packet, ref FFB_CTRL control);
uint Ffb_h_DevGain(IntPtr packet, ref byte gain);
}All functions use CallingConvention.Cdecl and DllImport("vJoyInterface.dll").
[StructLayout(LayoutKind.Explicit, Size = 108)]
internal struct JoystickPositionV2
{
[FieldOffset(0)] byte bDevice; // 1-based device index
[FieldOffset(4)] int wThrottle;
[FieldOffset(8)] int wRudder;
[FieldOffset(12)] int wAileron;
[FieldOffset(16)] int wAxisX;
[FieldOffset(20)] int wAxisY;
[FieldOffset(24)] int wAxisZ;
[FieldOffset(28)] int wAxisXRot;
[FieldOffset(32)] int wAxisYRot;
[FieldOffset(36)] int wAxisZRot;
[FieldOffset(40)] int wSlider;
[FieldOffset(44)] int wDial;
[FieldOffset(48)] int wWheel;
[FieldOffset(52)] int wAxisVX;
[FieldOffset(56)] int wAxisVY;
[FieldOffset(60)] int wAxisVZ;
[FieldOffset(64)] int wAxisVBRX;
[FieldOffset(68)] int wAxisVBRY;
[FieldOffset(72)] int wAxisVBRZ;
[FieldOffset(76)] int lButtons; // Buttons 1-32 bitmask
[FieldOffset(80)] uint bHats; // POV hat 1 (continuous: centidegrees, centered=0xFFFFFFFF)
[FieldOffset(84)] uint bHatsEx1; // POV hat 2
[FieldOffset(88)] uint bHatsEx2; // POV hat 3
[FieldOffset(92)] uint bHatsEx3; // POV hat 4
[FieldOffset(96)] int lButtonsEx1; // Buttons 33-64
[FieldOffset(100)] int lButtonsEx2; // Buttons 65-96
[FieldOffset(104)] int lButtonsEx3; // Buttons 97-128
}Matches public.h _JOYSTICK_POSITION_V2 struct. Total size: 108 bytes.