Skip to content

Input Precision

hifihedgehog edited this page May 4, 2026 · 19 revisions

Input Precision

This page describes the data characteristics of a custom controller built on a HIDMaestro Extended slot — the path most relevant to flight simulators, racing wheels, and HOTAS setups. It also covers the protocol-imposed bit depths of the Xbox and PlayStation virtual-controller types, so you can pick the slot type that matches your game's input requirements.

The page is organized as the input value's journey: SDL3 read, internal representation, deadzone math, then HIDMaestro output.


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 3-tier: HR waitable timer > multimedia timer > Thread.Sleep(1), all followed by SpinWait
Drift compensation Wall-clock cumulative tracking with per-cycle adjustment
Jitter Sub-millisecond. Spin-wait eliminates OS scheduler variance
GC pressure Zero allocations in the hot path. No garbage collection pauses during polling
Idle mode ~20 Hz when no slots created (CPU savings)

3-Tier Sleep Strategy

The polling loop selects the best available sleep mechanism at startup and falls through tiers if unavailable:

Tier 1: High-Resolution Waitable Timer (CreateWaitableTimerExW with CREATE_WAITABLE_TIMER_HIGH_RESOLUTION). Available on Windows 10 1803+. Achieves tighter clustering around the target interval than Thread.Sleep because the kernel scheduler services the timer at sub-ms resolution. No busy-wait CPU cost during the kernel sleep portion.

Tier 2: Multimedia Timer (timeSetEvent periodic callback signals a ManualResetEvent). The x360ce-style approach. The polling thread blocks with WaitOne() until the callback fires. Precision is ~1-2ms with timeBeginPeriod(1). Less accurate than the HR timer but much better than Thread.Sleep(1) alone.

Tier 3: Thread.Sleep(1) + SpinWait. Legacy fallback when both timers fail. Thread.Sleep(1) absorbs bulk wait when >1.5ms remains.

All three tiers finish with Thread.SpinWait(1) in a tight loop for the final sub-ms portion, ensuring precise cycle boundaries.

Wall-Clock Drift Compensation

Instead of tracking per-cycle overshoot, the loop maintains a cumulative expectedTicks counter incremented by targetTicks each iteration. The actual wallClock.ElapsedTicks is compared; if the loop is behind, the next cycle's sleep is shortened, and if ahead, it is lengthened. This converges the long-term average rate exactly to the target Hz.

If drift exceeds 10x the target interval (e.g., after system sleep/resume or exiting idle mode), the wall clock resets to prevent a burst of short catch-up cycles.


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 deadzone pipeline. Final on-the-wire bit depth depends on which HID protocol HIDMaestro encodes the report as. See Stage 5.

Stage 4: Deadzone Processing (64-bit floating point)

When deadzone, anti-deadzone, 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% deadzone, 0% anti-deadzone, 0% linear, 100% max range), the deadzone function returns immediately without any floating-point math. The axis value passes through bit-for-bit unchanged. Zero processing, zero rounding error.

Stage 5: HIDMaestro Output

Every virtual controller submits through HIDMaestro. PadForge converts the internal Gamepad struct (signed 16-bit sticks, unsigned 16-bit triggers) to normalized float at the SDK boundary:

LeftStickX  = gp.ThumbLX / 32767f      // -1.0 .. +1.0
LeftTrigger = gp.LeftTrigger / 65535f  //  0.0 .. +1.0

Single-precision float represents every integer in the [-32768, +32767] range exactly (float mantissa is 24 bits), so the conversion is lossless. HIDMaestro then encodes the report to whichever HID descriptor the slot's profile defines.

Extended slots (HIDMaestro custom HID) — the standard precision path

This is the path you use when building a custom controller in PadForge: an Extended slot backed by a HIDMaestro profile constructed via HMProfileBuilder (AddStick, AddTrigger, AddButtons, AddHat, FFB pages). It is the path with the fewest protocol constraints, and the standard data characteristics are:

Property Sticks Triggers
Type unsigned 16-bit unsigned 16-bit
Range 0 to 65535 0 to 65535
Resolution 65536 positions 65536 positions
Bits 16 effective bits 16 effective bits

Both sticks and triggers are 16-bit unsigned (HID logical range [0..65535]) by default in PadForge's Extended profile builder. HIDMaestro requires axis bit depths to be a multiple of 8 — the only valid alternative is 8-bit. Non-aligned sizes (e.g. 10-bit) are rejected because they force a Const pad item that Chromium's RawInput parser surfaces as a phantom axis.

16-bit stick resolution exceeds the physical precision of every consumer controller on the market. Typical analog-stick ADCs report 10–12 bits of effective resolution; HIDMaestro's 16-bit output preserves every bit the hardware can produce, with 16x the headroom of vJoy's old 15-bit (32768-position) format. This is the precision floor you can rely on for flight sticks and HOTAS — small stick deflections drive long-throw control surfaces — and for racing wheels where smooth sub-degree steering input reaches the simulator without quantization.

POV hats use continuous values (0–35900 in hundredths of a degree) for full 8-way diagonal support. See POV Hat Support.

Xbox slots (Xbox 360 protocol)

Xbox slots emit the standard Xbox 360 HID report. Bit depth is fixed by the protocol, not by HIDMaestro:

Property Sticks Triggers
Type signed 16-bit unsigned 8-bit
Range -32768 to +32767 0 to 255
Resolution 65536 positions 256 positions

Sticks pass through at full input resolution. Triggers are downsampled to 8 bits because the Xbox 360 HID descriptor uses one byte per trigger.

PlayStation slots (DS4 protocol)

PlayStation slots emit the DualShock 4 HID report. Both sticks and triggers are 8-bit unsigned per the DS4 descriptor:

Property Sticks Triggers
Type unsigned 8-bit unsigned 8-bit
Range 0 to 255 0 to 255
Resolution 256 positions 256 positions

If you need 16-bit stick precision and PlayStation button labels at the same time, use an Extended slot with a custom DS4-style profile rather than an Xbox/PlayStation slot — Extended profiles are not constrained by the original protocol's bit depth.


Deadzone Math

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

Parameters

Parameter Range Default Effect
Deadzone 0–100% 0% Input below this threshold maps to zero
Anti-Deadzone 0–100% 0% Minimum output value. Eliminates the game's built-in deadzone
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 deadzone 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 deadzone. Values below threshold map to 0.0
  3. Rescale remaining range to fill 0.0–1.0
  4. Apply anti-deadzone. 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

HIDMaestro: Single-Call Submit

Every virtual controller submits its full state to HIDMaestro in one HMController.SubmitState call per frame, passing a complete HMGamepadState (or a custom-HID payload, for Extended profiles). HIDMaestro is consumer-driven — it forwards each call to the kernel as a single HID report write — so the throughput cost per frame is constant and independent of how many axes, buttons, or hats the profile uses.

There is no per-axis or per-button kernel call. The 1000 Hz polling loop submits one frame per controller per cycle, and that scales linearly with slot count without the per-element IOCTL overhead that vJoy's old SetAxis / SetBtn / SetDiscPov API forced on its callers.


Summary: standard data characteristics for custom HIDMaestro controllers

What you get from an Extended slot at default profile settings:

Concern Value
Polling rate 1000 Hz, sub-ms jitter (HR waitable timer preferred)
Stick axis resolution 65536 positions, unsigned 16-bit (HID logical range 0..65535)
Trigger axis resolution 65536 positions, unsigned 16-bit (HID logical range 0..65535)
POV hat 8-way continuous, hundredths of a degree (0..35900)
Buttons Up to 128 per profile
Deadzone at defaults Bit-perfect passthrough, zero processing
Deadzone precision Double-precision (64-bit) floating point
Response curve Configurable linear curve with double-precision math
Output latency Single HMController.SubmitState call per frame per controller
GC pauses None. Zero allocations in polling loop
Drift compensation Wall-clock cumulative tracking, exact long-term Hz
Thread priority AboveNormal with OS timer resolution set to 1 ms

Xbox (Xbox 360 protocol) keeps 16-bit sticks but downsamples triggers to 8-bit. PlayStation (DS4 protocol) is 8-bit on both. See Stage 5 for the full per-type breakdown.

For maximum fidelity, leave deadzone, anti-deadzone, and linear settings at their defaults (all zero). The deadzone function returns immediately at defaults — no floating-point math, no quantization — and the raw SDL axis value reaches HIDMaestro bit-for-bit identical to what the hardware reported.

Clone this wiki locally