Skip to content

Input Precision

hifihedgehog edited this page Mar 8, 2026 · 19 revisions

Input Precision

PadForge's input pipeline is designed for precision-critical applications like flight simulators, racing wheels, and HOTAS setups. This page details the polling architecture, axis value pipeline, dead zone math, and output fidelity at each stage.


Polling Architecture

PadForge polls all connected devices at 1000 Hz (1 ms interval) using a dedicated background thread.

Property Value
Target rate 1000 Hz (1 ms)
Thread priority AboveNormal
Timer resolution timeBeginPeriod(1) for OS-level 1 ms granularity
Sleep strategy Hybrid: Thread.Sleep(1) when >1.5 ms remaining, Thread.SpinWait for final sub-ms precision
Jitter Sub-millisecond — spin-wait eliminates OS scheduler variance
GC pressure Zero allocations in the hot path — no garbage collection pauses during polling

The hybrid sleep/spin-wait approach avoids both the CPU waste of pure spin-waiting and the ~15 ms granularity of unassisted Thread.Sleep. The result is a stable 1000 Hz loop with consistent sub-millisecond timing.


Axis Value Pipeline

Every axis value passes through a well-defined pipeline with known precision at each stage. At default settings, the pipeline provides bit-perfect passthrough with zero processing overhead.

Stage 1: SDL3 Input (16-bit signed)

SDL3 reads raw axis values from the OS HID driver and returns them as signed 16-bit integers.

Property Value
Type short (Int16)
Range -32768 to +32767
Resolution 65536 positions
Source SDL_GetJoystickAxis / SDL_GetGamepadAxis

Stage 2: CustomInputState (16-bit unsigned)

SDL values are converted to unsigned for internal processing:

unsigned = sdlValue + 32768
Property Value
Type int (stored as unsigned range)
Range 0 to 65535
Resolution 65536 positions
Conversion Lossless linear shift

Stage 3: Gamepad Struct (16-bit signed)

Mapped back to signed range for the XInput-compatible output struct:

signed = (short)(unsigned - 32768)
Property Value
Type short (Int16)
Range -32768 to +32767
Resolution 65536 positions
Conversion Lossless linear shift (inverse of Stage 2)

Triggers use ushort (0–65535), the same 16-bit resolution as stick axes. The full 16-bit range is preserved through the dead zone pipeline and output to vJoy (scaled to 15-bit 0–32767). Xbox 360 and DualShock 4 virtual controllers downscale to byte (0–255) at the final ViGEm output stage — this is a protocol constraint of those controller formats, not a PadForge limitation.

Stage 4: Dead Zone Processing (64-bit floating point)

When dead zone, anti-dead zone, or linear curve settings are non-default, axis values are processed through a double-precision floating-point pipeline:

normalized = value / 32767.0          // double precision
processed = ApplyDeadZone(normalized) // all math in double
output = (short)(processed * 32767.0) // back to integer
Property Value
Internal precision double (64-bit IEEE 754)
Significant digits ~15 decimal digits
Quantization error < 1 LSB when converted back to 16-bit

At default settings (0% dead zone, 0% anti-dead zone, 0% linear, 100% max range), the dead zone function returns immediately without any floating-point math. The axis value passes through bit-for-bit unchanged — zero processing, zero rounding error.

Stage 5: vJoy Output (15-bit unsigned)

For vJoy (custom DirectInput) controllers, the signed 16-bit value is converted to vJoy's HID logical range:

vjoyValue = (signedValue + 32768) / 2
Property Value
Type int (unsigned range)
Range 0 to 32767
Resolution 32768 positions
Bits 15 effective bits

The division by 2 maps the full signed 16-bit range into vJoy's standard HID logical range (0–32767). This is inherent to the vJoy HID descriptor format, not a PadForge design choice.

15-bit resolution exceeds the physical precision of all consumer controller hardware. Typical controller ADCs (analog-to-digital converters) provide 10–12 bits of effective resolution. The 15-bit output preserves every bit the hardware can produce.

Stage 5 (Alt): ViGEm Output (native)

For Xbox 360 and DualShock 4 virtual controllers via ViGEmBus, the Gamepad struct is submitted directly — no additional conversion. The signed 16-bit values pass through to the virtual device as-is.


Dead Zone Math

PadForge's dead zone processing uses the same math as x360ce, validated over years of community use. All intermediate calculations use double precision.

Parameters

Parameter Range Default Effect
Dead Zone 0–100% 0% Input below this threshold maps to zero
Anti-Dead Zone 0–100% 0% Minimum output value — eliminates the game's built-in dead zone
Linear -100–100% 0% Response curve: negative = more sensitive near center, positive = less sensitive
Max Range 0–100% 100% Limits maximum output

Default Settings = Bit-Perfect

When all parameters are at their defaults, the dead zone function detects this and returns the input value unchanged. There is no floating-point conversion, no rounding, and no precision loss. The raw SDL axis value arrives at the output driver bit-for-bit identical to what the hardware reported.

Non-Default Settings

When any parameter is non-zero (or max range < 100%), the value enters the double-precision pipeline:

  1. Normalize to 0.0–1.0 range (double division)
  2. Apply dead zone — values below threshold map to 0.0
  3. Rescale remaining range to fill 0.0–1.0
  4. Apply anti-dead zone — shift minimum output up
  5. Apply linear curve — power function for response shaping
  6. Apply max range — scale down maximum
  7. Convert back to integer output range

Every step uses double arithmetic. The only quantization occurs at the final integer conversion, introducing less than 1 LSB (least significant bit) of error — well below the noise floor of any physical controller.


POV Hat Support

PadForge uses continuous POV values (0–35900 in hundredths of degrees) rather than discrete 4-way POV. This provides full 8-way diagonal support:

Direction Value
North 0
Northeast 4500
East 9000
Southeast 13500
South 18000
Southwest 22500
West 27000
Northwest 31500
Centered -1 (0xFFFFFFFF)

Output Throughput

vJoy: Single-Call Batch Update

PadForge submits all axes, buttons, and POV values in a single UpdateVJD call per frame per virtual controller. This is a single kernel IOCTL that writes the entire JoystickPositionV2 struct atomically.

The alternative — calling individual SetAxis, SetBtn, SetDiscPov functions — would issue one kernel IOCTL per call (~1–2 ms each), degrading a 1000 Hz loop to ~11 Hz with just two controllers. PadForge avoids this entirely.

ViGEm: Direct Struct Submit

Xbox 360 and DualShock 4 virtual controllers receive their state via a single SubmitReport call per frame, passing the complete gamepad struct in one operation.


Summary for Flight Sim Users

Concern PadForge Behavior
Polling rate 1000 Hz, sub-ms jitter
Axis resolution (sticks) 65536 positions input, 32768 positions vJoy output
Axis resolution (triggers) 65536 positions input, 32768 positions vJoy output, 256 positions Xbox 360/DS4 output
Dead zone at defaults Bit-perfect passthrough, zero processing
Dead zone precision Double-precision (64-bit) floating point
Response curve Configurable linear curve with double-precision math
POV hat 8-way continuous (hundredths of degrees)
Output latency Single kernel call per frame per controller
GC pauses None — zero allocations in polling loop
Thread priority AboveNormal with OS timer resolution set to 1 ms

For maximum fidelity, leave dead zone, anti-dead zone, and linear settings at their defaults (all zero). This provides a completely transparent passthrough from your physical controller to the virtual device with no mathematical processing applied to the axis values.

Clone this wiki locally