-
Notifications
You must be signed in to change notification settings - Fork 6
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.
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.
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.
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
|
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 |
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 byte (0–255) for XInput compatibility. This is a protocol constraint of the Xbox 360 controller format, not a PadForge limitation.
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.
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.
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.
PadForge's dead zone processing uses the same math as x360ce, validated over years of community use. All intermediate calculations use double precision.
| 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 |
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.
When any parameter is non-zero (or max range < 100%), the value enters the double-precision pipeline:
- Normalize to 0.0–1.0 range (double division)
- Apply dead zone — values below threshold map to 0.0
- Rescale remaining range to fill 0.0–1.0
- Apply anti-dead zone — shift minimum output up
- Apply linear curve — power function for response shaping
- Apply max range — scale down maximum
- 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.
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) |
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.
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.
| Concern | PadForge Behavior |
|---|---|
| Polling rate | 1000 Hz, sub-ms jitter |
| Axis resolution (sticks) | 65536 positions input, 32768 positions vJoy output |
| Axis resolution (triggers) | 256 positions (XInput protocol constraint) |
| 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.