Skip to content

Engine Library

hifihedgehog edited this page Mar 3, 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 net8.0-windows.

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.

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.

Clone this wiki locally