Skip to content

Engine Library

hifihedgehog edited this page Mar 8, 2026 · 56 revisions

Engine Library

The PadForge.Engine assembly is a shared class library containing data types, interfaces, and enums used by both the Engine (input pipeline) and App (UI/ViewModel) assemblies. It has no UI dependencies and targets net10.0-windows10.0.26100.0.

Project file: PadForge.Engine/PadForge.Engine.csproj Namespace: PadForge.Engine


Gamepad

File: PadForge.Engine/Common/GamepadTypes.cs

Minimal struct matching the XInput XINPUT_GAMEPAD layout. Used as the output of the mapping pipeline (Step 3 -> Step 4 -> Step 5).

public struct Gamepad
{
    public ushort Buttons;
    public byte LeftTrigger;
    public byte RightTrigger;
    public short ThumbLX;
    public short ThumbLY;
    public short ThumbRX;
    public short ThumbRY;

    public bool IsButtonPressed(ushort flag);
    public void SetButton(ushort flag, bool pressed);
    public void Clear();
}

Button Flag Constants

Constant Value Description
DPAD_UP 0x0001 D-pad up
DPAD_DOWN 0x0002 D-pad down
DPAD_LEFT 0x0004 D-pad left
DPAD_RIGHT 0x0008 D-pad right
START 0x0010 Start button
BACK 0x0020 Back button
LEFT_THUMB 0x0040 Left stick click
RIGHT_THUMB 0x0080 Right stick click
LEFT_SHOULDER 0x0100 Left bumper
RIGHT_SHOULDER 0x0200 Right bumper
GUIDE 0x0400 Guide/home button
A 0x1000 A button
B 0x2000 B button
X 0x4000 X button
Y 0x8000 Y button

XInputState

File: PadForge.Engine/Common/GamepadTypes.cs

public struct XInputState
{
    public uint PacketNumber;
    public Gamepad Gamepad;
}

Matches the XINPUT_STATE struct layout (packet number + Gamepad).


VJoyRawState

File: PadForge.Engine/Common/GamepadTypes.cs

Raw vJoy output state for custom (non-gamepad) configurations. Bypasses the fixed Gamepad struct to support arbitrary axis/button/POV counts.

public struct VJoyRawState
{
    public short[] Axes;      // Up to 8 axes (signed short range -32768..32767)
    public uint[] Buttons;    // Button state as 4 x 32-bit words = 128 buttons max
    public int[] Povs;        // Up to 4 POV hat switches (-1=centered, 0-35900=direction)

    public static VJoyRawState Create(int nAxes, int nButtons, int nPovs);
    public void SetButton(int index, bool pressed);
    public bool IsButtonPressed(int index);
    public void Clear();
}

Create()

public static VJoyRawState Create(int nAxes, int nButtons, int nPovs)

Creates a zeroed state with specified capacities. Axes clamped to max 8, buttons to max 128 (stored as (N+31)/32 uint words), POVs to max 4.

Button Storage

Buttons use a 128-bit bitmask stored as uint[4]. Each uint holds 32 buttons. Index calculation:

  • word = index / 32
  • bit = index % 32

POV Values

POV values are in hundredths of degrees:

  • 0 = North
  • 4500 = NE
  • 9000 = East
  • 13500 = SE
  • 18000 = South
  • 22500 = SW
  • 27000 = West
  • 31500 = NW
  • 0xFFFFFFFF (-1) = Centered

Clear()

Resets axes to 0, buttons to 0, POVs to -1 (centered).


VirtualControllerType

File: PadForge.Engine/Common/VirtualControllerTypes.cs

public enum VirtualControllerType
{
    Xbox360 = 0,
    DualShock4 = 1,
    VJoy = 2
}

IVirtualController

File: PadForge.Engine/Common/VirtualControllerTypes.cs

public interface IVirtualController : IDisposable
{
    VirtualControllerType Type { get; }
    bool IsConnected { get; }
    void Connect();
    void Disconnect();
    void SubmitGamepadState(Gamepad gp);
    void RegisterFeedbackCallback(int padIndex, Vibration[] vibrationStates);
}

Abstraction over ViGEm/vJoy virtual controller operations. Concrete implementations live in the App assembly:

  • Xbox360VirtualController (ViGEm)
  • DS4VirtualController (ViGEm)
  • VJoyVirtualController (direct P/Invoke to vJoyInterface.dll)

CustomInputState

File: PadForge.Engine/Common/CustomInputState.cs

API-agnostic snapshot of a device's complete input state at a single point in time.

public class CustomInputState
{
    public const int MaxAxis = 24;
    public const int MaxSliders = 8;
    public const int MaxPovs = 4;
    public const int MaxButtons = 256;

    public int[] Axis;       // 0-65535, center = 32767
    public int[] Sliders;    // 0-65535
    public int[] Povs;       // centidegrees 0-35900, or -1 for centered
    public bool[] Buttons;   // true = pressed
    public float[] Gyro;     // [X, Y, Z] radians per second
    public float[] Accel;    // [X, Y, Z] meters per second squared
}

Constructors

public CustomInputState()
public CustomInputState(int[] axes, int[] sliders, int[] povs, bool[] buttons)

Default constructor creates zeroed arrays with default sizes. POVs initialize to -1 (centered). Copy constructor copies arrays up to max lengths (snapshot isolation).

Methods

public CustomInputState Clone()

Deep copy of all arrays.

public static void GetAxisMask(
    DeviceObjectItem[] items, int numAxes,
    out int axisMask, out int actuatorMask, out int actuatorCount)

Scans device object items to build axis and force-feedback actuator bitmasks. Bit N set = axis/actuator N exists.

public static int GetSlidersMask(DeviceObjectItem[] items, int numSliders)

Scans device object items to build a slider bitmask.

Value Conventions

Array Range Center Description
Axis 0-65535 32767 Indices 0-5 = X, Y, Z, Rx, Ry, Rz. 6-23 = additional
Sliders 0-65535 32767 Overflow or dedicated slider controls
Povs 0-35900 or -1 -1 Centidegrees. -1 = centered
Buttons bool false 256 max (covers full Windows VK code range)
Gyro float[3] 0.0 Radians/second. Only for gyro-capable devices
Accel float[3] 0.0 m/s^2. Only for accelerometer-capable devices

ISdlInputDevice

File: PadForge.Engine/Common/ISdlInputDevice.cs

Common interface for all SDL-based input device wrappers (joystick/gamepad, keyboard, mouse).

public interface ISdlInputDevice : IDisposable
{
    // Identity
    uint SdlInstanceId { get; }
    string Name { get; }
    Guid InstanceGuid { get; }
    Guid ProductGuid { get; }
    string DevicePath { get; }
    string SerialNumber { get; }
    ushort VendorId { get; }
    ushort ProductId { get; }

    // Capabilities
    int NumAxes { get; }
    int NumButtons { get; }
    int RawButtonCount { get; }
    int NumHats { get; }
    bool HasRumble { get; }
    bool HasHaptic { get; }
    bool HasGyro { get; }
    bool HasAccel { get; }
    bool IsAttached { get; }

    // Haptic
    HapticEffectStrategy HapticStrategy { get; }
    IntPtr HapticHandle { get; }
    uint HapticFeatures { get; }

    // State reading
    CustomInputState GetCurrentState();
    DeviceObjectItem[] GetDeviceObjects();
    int GetInputDeviceType();

    // Force feedback
    bool SetRumble(ushort low, ushort high, uint durationMs = uint.MaxValue);
    bool StopRumble();
}

HapticEffectStrategy

public enum HapticEffectStrategy
{
    None,
    LeftRight,
    Sine,
    Constant
}

Priority order chosen at haptic open time based on SDL_GetHapticFeatures:

  1. LeftRight — Best match for dual-motor rumble (low + high frequency).
  2. Sine — Periodic effect. Period varies by which motor is stronger.
  3. Constant — Fallback. Level from dominant motor.

WebControllerDevice

File: PadForge.Engine/Common/WebControllerDevice.cs

Virtual input device representing a browser-connected gamepad. Implements ISdlInputDevice so it integrates with the standard input pipeline.

Property Value
VID / PID 0xBEEF / 0xCA7E
Axes 6 (LX, LY, LT, RX, RY, RT — 0-65535 range)
Buttons 11 (standard Xbox layout: A, B, X, Y, LB, RB, Back, Start, LS, RS, Guide)
POV Hats 1
HasRumble true (via browser Vibration API)

State is written by the WebSocket thread and read by the polling thread via volatile reference swaps.


DeviceObjectItem

File: PadForge.Engine/Common/DeviceObjectItem.cs

Describes a single input object (axis, button, hat, slider) on a device. Used by the mapping UI and pipeline.

public class DeviceObjectItem
{
    // Identity
    public string Name { get; set; }
    public Guid ObjectTypeGuid { get; set; }
    public DeviceObjectTypeFlags ObjectType { get; set; }

    // Position
    public int InputIndex { get; set; }
    public int Offset { get; set; }

    // Aspect
    public ObjectAspect Aspect { get; set; }

    // Computed helpers
    public bool IsForceActuator { get; }
    public bool IsAxis { get; }
    public bool IsButton { get; }
    public bool IsPov { get; }
    public bool IsSlider { get; }
}

Property Details

Property Type Default Description
Name string "" Human-readable name (e.g., "X Axis", "Button 3")
ObjectTypeGuid Guid Guid.Empty Well-known GUID from ObjectGuid
ObjectType DeviceObjectTypeFlags All Classification flags
InputIndex int 0 Zero-based index into CustomInputState arrays
Offset int 0 Byte offset for legacy mapping compatibility
Aspect ObjectAspect Position Position, Velocity, Acceleration, or Force

Computed Properties

  • IsForceActuator: (ObjectType & ForceFeedbackActuator) != 0
  • IsAxis: (ObjectType & Axis) != 0
  • IsButton: (ObjectType & Button) != 0
  • IsPov: (ObjectType & PointOfViewController) != 0
  • IsSlider: ObjectTypeGuid == ObjectGuid.Slider

ToString()

Returns "{Name} ({TypeLabel}, Index {InputIndex})" where TypeLabel is "Axis", "Slider", "POV", "Button", or "Object".


InputTypes

File: PadForge.Engine/Common/InputTypes.cs

DeviceObjectTypeFlags

[Flags]
public enum DeviceObjectTypeFlags : int
{
    All = 0,
    RelativeAxis = 1,
    AbsoluteAxis = 2,
    Axis = 3,                        // RelativeAxis | AbsoluteAxis
    PushButton = 4,
    ToggleButton = 8,
    Button = 12,                     // PushButton | ToggleButton
    PointOfViewController = 16,
    Collection = 64,
    NoData = 128,
    ForceFeedbackActuator = 0x01000000,
    ForceFeedbackEffectTrigger = 0x02000000
}

ObjectAspect

[Flags]
public enum ObjectAspect : int
{
    Position = 0x100,
    Velocity = 0x200,
    Acceleration = 0x300,
    Force = 0x400
}

EffectParameterFlags

[Flags]
public enum EffectParameterFlags : int
{
    None = 0,
    Duration = 1,
    SamplePeriod = 2,
    Gain = 4,
    TriggerButton = 8,
    TriggerRepeatInterval = 16,
    Axes = 32,
    Direction = 64,
    Envelope = 128,
    TypeSpecificParameters = 256,
    StartDelay = 512,
    AllParameters = 0x3FF,
    Start = 0x20000000,
    NoRestart = 0x40000000,
    NoDownload = unchecked((int)0x80000000)
}

ObjectGuid

Static class providing well-known GUIDs for device object types. Values match DirectInput GUID constants.

public static class ObjectGuid
{
    public static readonly Guid XAxis;          // {A36D02E0-C9F3-11CF-BFC7-444553540000}
    public static readonly Guid YAxis;          // {A36D02E1-...}
    public static readonly Guid ZAxis;          // {A36D02E2-...}
    public static readonly Guid RxAxis;         // {A36D02F4-...}
    public static readonly Guid RyAxis;         // {A36D02F5-...}
    public static readonly Guid RzAxis;         // {A36D02E3-...}
    public static readonly Guid Slider;         // {A36D02E4-...}
    public static readonly Guid Button;         // {A36D02F0-...}
    public static readonly Guid Key;            // {55728220-D33C-11CF-BFC7-444553540000}
    public static readonly Guid PovController;  // {A36D02F2-...}
    public static readonly Guid Unknown;        // Guid.Empty
}

InputDeviceType

Integer constants matching DirectInput device type values. Used for UserDevice.CapType.

public static class InputDeviceType
{
    public const int Device = 17;
    public const int Mouse = 18;
    public const int Keyboard = 19;
    public const int Joystick = 20;
    public const int Gamepad = 21;
    public const int Driving = 22;
    public const int Flight = 23;
    public const int FirstPerson = 24;
    public const int Supplemental = 25;
}

MapType

public enum MapType : int
{
    None = 0,
    Axis = 1,
    Button = 2,
    Slider = 3,
    POV = 4
}

ForceFeedbackState

File: PadForge.Engine/Common/ForceFeedbackState.cs

Manages force feedback (rumble) state for a single device with change detection.

public class ForceFeedbackState
{
    // Public state
    public ushort LeftMotorSpeed { get; }    // 0-65535
    public ushort RightMotorSpeed { get; }   // 0-65535
    public bool IsActive { get; }

    // Methods
    public void SetDeviceForces(UserDevice ud, ISdlInputDevice device, PadSetting ps, Vibration v);
    public void StopDeviceForces(ISdlInputDevice device);
    public bool Changed(PadSetting ps);
}

SetDeviceForces()

public void SetDeviceForces(UserDevice ud, ISdlInputDevice device, PadSetting ps, Vibration v)
  1. Reads gain settings from PadSetting: ForceOverall, LeftMotorStrength, RightMotorStrength (all 0-100).
  2. Applies per-motor and overall gain scaling to raw XInput motor speeds.
  3. Swaps motors if ForceSwapMotor is configured.
  4. Change detection: Only sends to hardware when values differ from cached values. Each SDL_RumbleJoystick call restarts the hardware rumble, causing brief gaps. By only sending on change with uint.MaxValue duration (~49 days), rumble stays continuous.
  5. Routes to either SetHapticForces() (for haptic devices) or SetRumble() (for rumble devices).

SetHapticForces()

private bool SetHapticForces(ISdlInputDevice device, ushort left, ushort right)

Translates dual-motor rumble to SDL haptic effects based on the device's HapticEffectStrategy:

Strategy SDL Effect Large Motor Small Motor
LeftRight SDL_HAPTIC_LEFTRIGHT large_magnitude = left small_magnitude = right
Sine SDL_HAPTIC_SINE magnitude = max/2, period = 120 period = 40
Constant SDL_HAPTIC_CONSTANT level = max/2 N/A

Creates effect on first call via SDL_CreateHapticEffect, updates in-place on subsequent calls via SDL_UpdateHapticEffect.

Changed()

public bool Changed(PadSetting ps)

Returns true if any force feedback setting in the PadSetting has changed since last call. Caches: ForceType, ForceSwapMotor, LeftMotorStrength, RightMotorStrength, ForceOverall.


Vibration

File: PadForge.Engine/Common/ForceFeedbackState.cs

public class Vibration
{
    public ushort LeftMotorSpeed { get; set; }   // Low-frequency, heavy rumble. 0-65535
    public ushort RightMotorSpeed { get; set; }  // High-frequency, light buzz. 0-65535

    public Vibration();
    public Vibration(ushort leftMotor, ushort rightMotor);
}

Matches the XINPUT_VIBRATION layout. Populated from ViGEm FeedbackReceived callback on the ViGEm thread; read by Step 2 on the polling thread.


RumbleLogger

File: PadForge.Engine/Common/RumbleLogger.cs

public static class RumbleLogger
{
    public static bool Enabled { get; set; }
    public static void Log(string message);
}

Diagnostic logger for force feedback debugging. Disabled by default. Set Enabled = true in InputService.Start() to activate. Thread-safe. Writes timestamped messages to a log file using Stopwatch for high-resolution timing.


InputHookManager

File: PadForge.Engine/Common/InputHookManager.cs

Manages WH_KEYBOARD_LL and WH_MOUSE_LL low-level Windows hooks for suppressing mapped inputs from keyboards and mice. Only suppresses inputs that are in the active suppression sets — non-mapped keys/buttons pass through normally.

public class InputHookManager : IDisposable
{
    void Start();
    void Stop();
    void SetSuppressedKeys(HashSet<int> vkCodes);
    void SetSuppressedMouseButtons(HashSet<int> buttons);
    bool HasAnySuppression { get; }
}

Threading

Hooks require a thread with a message pump. InputHookManager creates a dedicated background thread running a GetMessage loop. Hooks are installed on this thread via SetWindowsHookExW.

  • Start() creates the thread and waits (via ManualResetEventSlim) until hooks are installed before returning.
  • Stop() posts WM_QUIT to the hook thread to exit the message loop, then joins the thread.

Suppression Sets

Suppression sets use volatile reference swap for thread safety — the hook callbacks read the current set reference without locking, and updates replace the entire HashSet atomically.

void SetSuppressedKeys(HashSet<int> vkCodes)       // Virtual key codes to suppress
void SetSuppressedMouseButtons(HashSet<int> buttons) // 0=Left, 1=Right, 2=Middle, 3=XButton1, 4=XButton2

Hook Callbacks

  • Keyboard: Intercepts WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP. Reads KBDLLHOOKSTRUCT.vkCode and checks against the suppression set. Returns (IntPtr)1 to suppress, or CallNextHookEx to pass through.
  • Mouse: Intercepts button messages (WM_LBUTTONDOWN/UP, WM_RBUTTONDOWN/UP, WM_MBUTTONDOWN/UP, WM_XBUTTONDOWN/UP). Converts the message to a button ID via MouseMessageToButtonId() and checks against the suppression set. Mouse movement and wheel events always pass through.

Button ID Mapping

Mouse Message Button ID
WM_LBUTTONDOWN/UP 0 (Left)
WM_RBUTTONDOWN/UP 1 (Right)
WM_MBUTTONDOWN/UP 2 (Middle)
WM_XBUTTONDOWN/UP (XBUTTON1) 3
WM_XBUTTONDOWN/UP (XBUTTON2) 4
Other (move, wheel) -1 (pass through)

P/Invoke

Function DLL Purpose
SetWindowsHookExW user32.dll Install low-level hook
UnhookWindowsHookEx user32.dll Remove hook
CallNextHookEx user32.dll Pass input to next hook
GetModuleHandleW kernel32.dll Get module handle for hook registration
GetMessageW user32.dll Message pump loop
PostThreadMessageW user32.dll Post WM_QUIT to hook thread
GetCurrentThreadId kernel32.dll Get hook thread ID

Stick Calibration Fields (PadSetting)

Field Type Description
LeftThumbCenterOffsetX string Left stick X center offset (-100 to 100%). Default "0".
LeftThumbCenterOffsetY string Left stick Y center offset. Default "0".
RightThumbCenterOffsetX string Right stick X center offset. Default "0".
RightThumbCenterOffsetY string Right stick Y center offset. Default "0".
LeftThumbMaxRangeX string Left stick X max range (1-100%). Default "100".
LeftThumbMaxRangeY string Left stick Y max range. Default "100".
RightThumbMaxRangeX string Right stick X max range. Default "100".
RightThumbMaxRangeY string Right stick Y max range. Default "100".

Center offset is applied in Step 3 before dead zone processing. Max range scales the output range.


Input Hiding Fields (UserDevice)

Field Type Description
HidHideEnabled bool Whether this device is hidden from games via HidHide
ConsumeInputEnabled bool Whether mapped keyboard/mouse inputs are suppressed via hooks
ForceRawJoystickMode bool Bypass SDL3 gamepad remapping, read raw joystick indices
HidHideInstanceIds List<string> Cached HID instance IDs for blacklisting (persisted for offline devices)

Clone this wiki locally