Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.

Interface control document

1. Identification

ItemValue
DocumentICD-CMCTRL-001 rev 2
ProductGame controller input module
Product versions covered0ea243c
AssemblyHEAT.GameControllerInput, net10.0
Settings sectionHEAT.GameControllerInput
PeersWindows DirectInput 8 and XInput, via Vortice.DirectInput 3.8.3 and Vortice.XInput 3.8.3
HostHEAT Capture, HEAT.Capture.ModuleSDK 2.0.5
Data modelSimulationFrame carrying one EntityState per device plus EntityEvent entries
Date2026-09-02
OwnerVRAI Simulation

2. Scope

This document defines the two Windows device APIs the module reads, the hidden top-level window it creates inside the host process to satisfy DirectInput, the enumeration and identity rules for each API, the polling behaviour, and the entities and events published to HEAT Capture.

The module is read-only with respect to the controllers. It never writes to a device: no rumble, no force feedback, no LED or configuration output.

The two APIs are not alternatives layered behind one abstraction. They are separate interfaces with different identity schemes, different button naming, different axis field names and different failure behaviour, and a single physical Xbox-class pad is normally visible on both. Sections 4 and 5 are therefore specified independently, and section 7.3 documents where their published output diverges.

3. Interface inventory

IDNameDirectionTransportPeer
IF-CMCTRL-01DirectInput device stateDevice to moduleDirectInput 8, non-exclusive background acquisition against a window handleAny attached HID game controller
IF-CMCTRL-02XInput device stateDevice to moduleXInput GetCapabilities and GetState on player slots 0 to 3XInput-class pad
IF-CMCTRL-03Hidden Win32 input windowModule to operating systemRegisterClassW and CreateWindowExW inside the host processWindows window manager
IF-CMCTRL-04Frame publicationModule to hostModule SDK publication pathHEAT Capture

4. IF-CMCTRL-01 DirectInput device state

4.1 Enumeration and identity

Enumeration happens once, in InitialiseModule. The module creates a DirectInput 8 instance and, for each DeviceType in the DeviceTypes setting, enumerates with DeviceEnumerationFlags.AttachedOnly.

ItemValue
Device categoriesThe DeviceTypes setting; default Gamepad, Joystick, Driving, Flight
Selection filterIf DeviceId parses as a GUID, only the device whose InstanceGuid equals it is kept. A value that does not parse, including empty, applies no filter and every enumerated device is kept.
Device identityThe DirectInput InstanceGuid
Device nameThe DirectInput InstanceName, as reported by the driver
Button identityGetObjects(DeviceObjectTypeFlags.Button) enumerated in order; the ordinal position of each object is used as the key
Log on discoveryDirectInput controller: {Name} ({Guid}) at information level

Button identity depends on the enumeration ordinal matching the index of the same button in JoystickState.Buttons. The module does not read each object’s offset or usage to confirm that correspondence, so a device whose button objects enumerate in a different order than they appear in the state array reports correct button names against the wrong physical buttons.

Because enumeration is bound to InitialiseModule, which the host calls once per module lifetime, a controller connected after that call is never captured, however many capture sessions start afterwards. There is no re-enumeration on capture start and no hot-plug detection.

4.2 Acquisition lifecycle

Acquisition happens on every capture start, in StartCapture, and only when at least one DirectInput device was enumerated. The module creates the hidden window described in section 6 and then, for each device in turn:

  1. SetCooperativeLevel(hwnd, CooperativeLevel.NonExclusive | CooperativeLevel.Background)
  2. SetDataFormat with the RawJoystickState type argument
  3. Acquire()

Non-exclusive means the device stays available to every other application, including a simulator running alongside HEAT Capture. The module never takes exclusive ownership of a controller.

A per-device failure at any of the three steps logs Failed to acquire {Device} at error level and that device is skipped; the remaining devices are still attempted. A successful acquisition seeds the device’s retained state to null and, when DebugLogs is set, logs Acquired DirectInput: {Device}.

If the hidden window cannot be created the module logs Failed to create the input window; DirectInput devices unavailable at error level and no DirectInput device is acquired. If no DirectInput device was acquired and there are also no XInput devices, StartCapture logs No controller could be acquired and returns false, which fails the capture start for this module.

Capture end cancels the poll loop, awaits its task, calls Unacquire() on every device with any exception swallowed, then disposes the hidden window in that order, so the window outlives the devices that cooperate with it. The retained device states and the previous-button-state map are cleared. The enumeration itself, the device list and the device and button name maps are not cleared, so a subsequent capture start reuses the devices found at initialisation.

InitialiseModule and StartCapture are both bounded by the SDK StartupTimeout. This module does not override it, so the default of 5 seconds applies to enumeration, window creation and acquisition together. See the Module SDK contract.

4.3 Authentication

None. Any attached HID game controller matching the configured device types is enumerated and acquired. There is no pairing, no key and no allow-list beyond the optional single-GUID DeviceId filter.

4.4 Polling and state format

A background task started at capture start runs a loop that polls every device, checks the stuck detector described in 4.5, and then waits PollIntervalMs. The setting is re-read on every iteration, so a change takes effect on the next wait without restarting the capture.

ItemValue
Poll intervalPollIntervalMs, default 4 ms, approximately 250 polls per second
Readdevice.Poll() followed by GetCurrentState using JoystickState, RawJoystickState and JoystickUpdate as type arguments
RetentionThe result replaces the device’s retained snapshot; nothing is queued

The poll loop only refreshes the retained snapshot. No event is derived at poll rate. Everything published is derived from the snapshot present at the moment of a capture cycle, so intermediate polls are discarded by design. Section 7.5 states the consequence.

The state fields read from JoystickState are the button array, X, Y, Z, RotationX, RotationY, RotationZ, every element of Sliders, and every element of PointOfViewControllers. No other field of the state is used.

The module sets no axis range property on the device. Axis normalisation therefore hard-codes a 0 to 65535 raw range with 32767 as centre. A device that reports a different raw range is normalised against the wrong centre and scale, with no warning.

4.5 Foreground gating of XInput-class pads

DirectInput does not deliver changing data for an XInput-class pad to a process that does not own the foreground window, regardless of CooperativeLevel.Background. The module’s own repository records the measurement: on an Xbox One pad with sticks moving throughout, five-second phases at 4 ms polls produced 322 samples with 1 distinct value while the window was invisible, 322 samples with 1 distinct value while it was visible but not activated, and 322 samples with 322 distinct values only while it was visible and in the foreground. The same probe against a T.Flight Hotas X, an ordinary HID device, produced changing data in every phase including invisible, which is why the DirectInput path is kept rather than removed.

A background capture host never owns the foreground window, so this interface cannot deliver live data for an XInput-class pad. The module detects the symptom rather than leaving it silent. Once at least 500 DirectInput samples have been taken and no more than one distinct X, Y, RotationX, RotationY sample value has been seen, it emits one warning naming the sample count and every enumerated device name, and advising that the XInput entry be selected for that device instead. The warning is emitted at most once per capture session and the detector then disarms.

5. IF-CMCTRL-02 XInput device state

This interface is present only when UseXInput is set, default true.

5.1 Enumeration and identity

Enumeration happens once, in InitialiseModule, alongside the DirectInput enumeration.

ItemValue
Slots probedUser indices 0, 1, 2, 3
Presence testXInput.GetCapabilities(userIndex, DeviceQueryType.Any, out caps); a false return skips the slot
Device identityA synthetic GUID per slot: first component userIndex + 1, second and third components zero, trailing bytes 0x58 0x49 0x4E 0x50, which spell XINP. Deterministic and disjoint from real DirectInput instance GUIDs.
Device nameXInput Gamepad (Player N) when caps.SubType is Gamepad, otherwise XInput Controller (Player N), with N = userIndex + 1
Selection filterIf DeviceId parses as a GUID, the slot is kept only when its synthetic GUID equals it
Log on discoveryXInput controller: {Name} (User {Index}) at information level, with Index the zero-based user index

XInput exposes no device GUID, serial number or driver-reported name, so identity is positional. A pad moved to a different player slot between sessions is a different entity with a different id and name. Nothing in the interface can tie the two together.

Because a single physical Xbox-class pad is visible to both APIs, the settings UI lists it twice and DeviceId matches exactly one GUID. Selecting the DirectInput entry for such a pad filters the XInput slot out, which is the combination section 4.5 warns about.

If neither API produced a device, InitialiseModule logs No game controllers found at warning level and still returns true. The module then starts, publishes nothing, and section 7.1 applies.

5.2 Lifecycle

There is no connection or acquisition step. XInput needs no window handle and no cooperative level, which is why it works from a background host.

Capture start seeds each enumerated slot’s retained gamepad state to the default all-zero value and, when DebugLogs is set, logs Ready XInput: {Name} (User {Index}). Capture end clears the retained states and the previous-button map. The slot enumeration itself is retained across capture sessions.

The seeded all-zero state is indistinguishable from a real reading of a centred pad with no button pressed. Section 7.1 states what that means for a consumer.

5.3 Authentication

None. Any pad the XInput driver presents on slots 0 to 3 is read.

5.4 Polling and state format

The same background poll loop reads every enumerated slot with XInput.GetState(userIndex, out state) at PollIntervalMs cadence and stores state.Gamepad on success.

The fields read from Gamepad are Buttons, LeftThumbX, LeftThumbY, RightThumbX, RightThumbY, LeftTrigger and RightTrigger. Gamepad reports no per-sample packet number or timestamp to the module, so there is no way to tell a repeated identical reading from a stale one.

A false return from GetState, which is how XInput reports a disconnected slot, leaves the previously retained state in place and is logged as XInput GetState failed for {Name} only when DebugLogs is set. The module keeps publishing that retained state and its axis event for the rest of the session. See section 8.

6. IF-CMCTRL-03 Hidden Win32 input window

DirectInput requires a top-level window handle for SetCooperativeLevel, even for non-exclusive background acquisition. When the module is hosted out of process the host is launched with CREATE_NO_WINDOW, so it has neither a main window nor a console window and there is no handle to borrow. The module therefore creates one of its own, purely to satisfy that call.

ItemValue
Window classHEAT.GameControllerInput.HiddenWindow, registered once per process
Window styleWS_POPUP, zero size at position 0,0, no parent, never shown
Window titleHEAT Game Controller Input
ThreadA dedicated background thread named GameControllerInput.HiddenWindow running its own GetMessageW pump
CreationSynchronous from the caller’s perspective: creation is awaited, and a RegisterClassW or CreateWindowExW failure surfaces as an exception carrying the Win32 error code
TeardownWM_CLOSE is posted, DefWindowProc destroys the window, WM_DESTROY posts WM_QUIT, the pump exits, and the pump thread is joined with a 2 second bound

Implications for the host process that a deployment needs to know:

  • The host process acquires a registered window class and one top-level window it did not previously have. The class name string is intentionally leaked because the class lives for the process lifetime, and the window procedure delegate is held in a static field because Win32 retains the raw function pointer.
  • A message pump thread runs for the duration of every capture session that has at least one DirectInput device. The thread is a background thread, so it does not keep the process alive.
  • The window is created only when at least one DirectInput device was enumerated. A session with XInput devices only creates no window and starts no pump thread.
  • The window is never made visible or activated, which is deliberate but is also exactly the condition under which section 4.5 applies.

7. IF-CMCTRL-04 Frame publication and data model reference

7.1 Frame and entities

HandleCapture returns null, publishing nothing for that cycle, only when both retained state collections are empty. That is the case when no device was enumerated at all, and for a DirectInput-only session it remains the case until the first successful poll, because a DirectInput device’s retained state starts as null and a null state is skipped.

An XInput session is different: capture start seeds every slot with an all-zero gamepad state, so a frame containing that entity, its axis event and no button event is published from the first capture cycle onwards even if GetState has never once succeeded. A consumer cannot distinguish that seeded frame from a real reading of a centred, untouched pad.

Each published frame carries:

ItemValue
Simulation tickA counter starting at 0, reset to 0 on every capture start, incremented once per capture cycle including cycles that publish nothing new
Event timestampDateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), taken once per cycle and applied identically to every event in the frame
EntitiesOne EntityState per DirectInput device with a non-null retained state, plus one per enumerated XInput slot

An entity carries only an identity, with no components:

FieldValue
Entity.NameThe DirectInput InstanceName, or the synthetic XInput Gamepad (Player N) / XInput Controller (Player N)
Entity.IdThe 32-bit GetHashCode() of the device GUID, cast to an unsigned 64-bit value. A negative hash sign-extends, so ids for some devices appear as values above 2^63. Ids are stable for the same GUID across sessions and across processes.

The entity is published every cycle whether or not the device produced anything, so an untouched controller still occupies a slot in every frame.

The SDK wraps the frame as a DataPacket whose TimestampUnixMs is taken at publication. That value and the per-event timestamp in the frame are separate readings of the host clock taken at slightly different moments in the same cycle.

7.2 Button events

Each button transition is one EntityEvent carrying a DataEvent with the entity id, the frame timestamp and a ButtonPress payload of a button name and a state.

Edge computation compares the button bits in the state retained at this capture cycle against those retained at the previous capture cycle:

PreviousCurrentInternal statePublished state
not pressedpressedDownPressed
pressedpressedHoldHold
pressednot pressedReleasedReleased
not pressednot pressedUpnot published

A button held across cycles therefore produces one Hold event per cycle for as long as it is held, not one event per press. A consumer that wants rising edges only must filter on Pressed.

Button naming differs by API.

DirectInput: the button count is the number of enumerated button objects when that is non-zero, otherwise the length of the state’s button array. The name is the driver-reported object name, with Button {i} as the fallback when no name was enumerated for that ordinal.

XInput: a fixed 14-entry table, evaluated in this order.

BitPublished name
AA
BB
XX
YY
LeftShoulderLB
RightShoulderRB
BackBack
StartStart
LeftThumbLS
RightThumbRS
DPadUpDPad Up
DPadDownDPad Down
DPadLeftDPad Left
DPadRightDPad Right

The XInput Guide button is not in the table and is never published. The analogue triggers are not buttons on this path; they appear only in the axis event.

7.3 Axis events

An axis event is one EntityEvent carrying a DataEvent with a JsonEvent whose event name is ControllerAxes and whose data is a JSON object serialised as a string. The event is emitted with the device’s current axis values whether or not any axis moved.

The two APIs publish under the same event name with disjoint field sets. A consumer must key its parsing off which entity the event belongs to; the two schemas share no field name.

DirectInput fields:

FieldSourceValue
x, y, zX, Y, Z(raw - 32767) / 32767, rounded to 4 decimal places
rx, ry, rzRotationX, RotationY, RotationZsame normalisation
slider0, slider1, …each element of Sliderssame normalisation
pov0, pov1, …each element of PointOfViewControllers-1 when the raw value is negative, otherwise the raw value divided by 100

DirectInput reports a point-of-view control in hundredths of a degree clockwise from forward and a negative value when it is centred, so pov0 is degrees in the range 0 to 359, and -1 means centred. It is not normalised to the -1 to 1 range the other fields use.

The axis fields are always all present for every axis the state structure carries, whether or not the physical device has that axis. An axis the device does not report reads as its unset raw value and is normalised like any other, so a device without a Z axis still publishes a z field.

XInput fields, always exactly these six:

FieldSourceValue
left_x, left_yLeftThumbX, LeftThumbYraw / 32767, rounded to 4 decimal places
right_x, right_yRightThumbX, RightThumbYsame normalisation
left_trigger, right_triggerLeftTrigger, RightTriggerraw / 255, rounded to 4 decimal places

XInput thumb values are signed 16-bit, so the negative extreme is a raw -32768 against a divisor of 32767. The exact quotient is -1.00003, which rounds to -1.0000 at the four decimal places used, so the published range is -1 to 1 on both signs. No dead zone is applied on either path: the raw resting jitter of a stick is published as-is, which is why an untouched pad can still produce a changing ControllerAxes payload every cycle.

7.4 Axis suppression, per API

SendAxisData, default true, is documented as one switch for analogue data across all devices. It is applied on one path only.

PathSendAxisData = trueSendAxisData = false
DirectInputControllerAxes published every cycle per deviceNo ControllerAxes event published
XInputControllerAxes published every cycle per deviceControllerAxes still published every cycle per device

This is audit finding CTRL-01 and it is a property of this revision. The XInput publication is not guarded by the setting, so clearing SendAxisData does not reduce XInput axis event volume at all. For an XInput-only deployment the setting has no observable effect. The setting is read fresh from the settings monitor on each cycle on the DirectInput path, so a change there takes effect on the next cycle without restarting the capture.

TrackEdgeDetection, default true, is declared in the settings and is intended to gate button-state computation. This revision never reads it: edge computation and button event publication are unconditional on both paths. Clearing it changes nothing.

7.5 Timing and cadence

Two independent rates are involved and they do different things.

RateSettingDefaultEffect
Device pollPollIntervalMs4 msRefreshes the retained snapshot per device. Derives no events.
Publication and edge detectionSDK Interval1000 msBuilds and publishes the frame, and computes every button edge and axis value.

This module does not override the SDK Interval default, so the effective resolution of a button event is 1000 ms unless the operator lowers it. Edge detection compares the snapshot at this capture cycle against the snapshot at the previous capture cycle, not against the previous poll. A press and release that both occur inside one Interval window are not delayed, they are never observed: the button read as not pressed at both cycle boundaries, so no Pressed and no Released event exists. The 4 ms poll rate does not change this; it only decides how fresh the value sampled at the cycle boundary is.

A deployment that must catch brief taps has to lower Interval towards PollIntervalMs, accepting the corresponding rise in frame and event volume. Raising PollIntervalMs has no effect on tap capture.

7.6 Connection events

The module publishes no connection, disconnection or device-status event of any kind. Nothing enqueues one, and there is no event name reserved for one. Device presence is observable to a consumer only indirectly, by which entities appear in a frame, and that signal is weak: the entity set is fixed at initialisation and a device that stops responding continues to appear.

Device arrival and departure are visible only in the host log, through the discovery lines in 4.1 and 5.1 at initialisation and the poll error warnings in section 8 during capture.

8. Error and disconnect behaviour

ConditionDetectionBehaviour
No device found by either APIBoth enumerations empty at initialisationLogs No game controllers found at warning level. InitialiseModule still returns true, so the module starts and publishes nothing.
Hidden window cannot be createdException from RegisterClassW or CreateWindowExWLogs Failed to create the input window; DirectInput devices unavailable at error level. No DirectInput device is acquired. XInput is unaffected.
One DirectInput device fails to acquireException from SetCooperativeLevel, SetDataFormat or AcquireLogs Failed to acquire {Device} at error level. That device is skipped; the others are still attempted.
Nothing acquirable at allNo DirectInput device acquired and no XInput device enumeratedLogs No controller could be acquired and StartCapture returns false, failing the module’s capture start.
DirectInput poll failure, including unplugException from Poll or GetCurrentStateAn immediate Acquire() retry is attempted and any failure from it is swallowed, so the next loop iteration retries. Logs DirectInput poll error for {Device} at warning level on the first failure, and on every failure when DebugLogs is set. The retained snapshot is not cleared, so the last successful state keeps being published.
DirectInput polls cleanly but never changes500 or more samples with at most one distinct axis sample valueLogs one warning per session naming the sample count and every device name, advising selection of the XInput entry. See 4.5.
XInput slot disconnectedGetState returns falseLogged as XInput GetState failed for {Name} at warning level only when DebugLogs is set. The retained state is not cleared, so the last successful state and its ControllerAxes event keep being published for the rest of the session.
XInput poll exceptionException from GetStateLogs XInput poll error for {Name} at warning level on the first failure, and on every failure when DebugLogs is set.

Two properties of this table matter more than the individual rows.

Neither path clears its retained state on failure. A controller unplugged mid-session leaves its last value in place and the module keeps republishing it, with no event and, on the XInput path with default settings, no log line either. A consumer that treats a ControllerAxes payload as evidence of a live device will be wrong for the remainder of the session.

The poll error counter that gates first-failure logging is shared between the two paths and is reset to zero on every successful DirectInput poll. With a healthy DirectInput device present, an XInput failure is therefore logged as a first failure repeatedly rather than once.

9. Units and conventions

ConceptConvention
DirectInput axes and slidersDimensionless, nominally -1 to 1, centre 0, derived from an assumed 0 to 65535 raw range with centre 32767. Rounded to 4 decimal places.
DirectInput point of viewDegrees clockwise from forward, 0 to 359, or -1 for centred. Not normalised.
XInput thumbsticksDimensionless, -1 to 1, centre 0, from a signed 16-bit raw value divided by 32767. Rounded to 4 decimal places.
XInput triggersDimensionless, 0 to 1, from an unsigned 8-bit raw value divided by 255. Rounded to 4 decimal places.
Dead zoneNone applied on either path. Raw resting jitter is published.
Button stateThree published values: Pressed, Hold, Released. Up is computed internally and never published.
Sign and axis directionWhatever the device driver reports. The module applies no axis inversion and no per-device calibration.
Time baseHost wall clock, UTC Unix milliseconds, taken once per capture cycle. Neither API supplies a device timestamp, so none is carried.
Simulation tickPer-session cycle counter from 0, not a time value

10. Configuration affecting the interfaces

SettingDefaultInterface effect
DeviceIdnot setRestricts capture to the single device or XInput slot whose GUID matches. Read at initialisation only.
DeviceTypesGamepad, Joystick, Driving, FlightWhich DirectInput categories are enumerated. No effect on XInput. Read at initialisation only.
UseXInputtrueWhether IF-CMCTRL-02 exists at all. Read at initialisation only.
PollIntervalMs4Device poll cadence. Re-read every loop iteration. Does not affect event resolution.
SendAxisDatatrueSuppresses the DirectInput ControllerAxes event when cleared. Does not suppress the XInput one; see 7.4. Re-read every cycle.
TrackEdgeDetectiontrueDeclared but never read in this revision. No effect.
DebugLogsfalsePromotes acquisition and readiness lines to information level and turns first-failure-only poll warnings into every-failure warnings. Also the only way XInput disconnects are logged. Re-read at each use.

Generic SDK settings that change observable behaviour here:

SettingInterface effect
EnabledWhether the module runs at all
IntervalPublication cadence and button edge resolution; see 7.5
OverrideNodeId, NodeIdThe node id attached to the published packet

Timeout is inherited and unused by this module. See Manage capture modules for the generic settings and the Module SDK contract for the lifecycle they belong to.

11. Compatibility and versioning

There is no version negotiation on either interface.

ItemConstraint
PlatformWindows only. Both interfaces are Win32 APIs and IF-CMCTRL-03 calls user32.dll directly.
DirectInputDirectInput 8 via Vortice.DirectInput 3.8.3
XInputVortice.XInput 3.8.3, four player slots, fixed
Module SDKHEAT.Capture.ModuleSDK 2.0.5
Runtimenet10.0

The published contract that consumers depend on is the entity name, the entity id derivation, the button name set and the ControllerAxes field names. DirectInput button names and the DirectInput axis field set are driver-dependent and change with a device or driver change; the XInput button table and the six XInput axis fields are fixed in the module and change only with a module release.

12. Security considerations

Both device interfaces are workstation-local and unauthenticated. Any local process can enumerate and read the same controllers, and the module’s own non-exclusive acquisition deliberately preserves that: acquiring a device here does not deny it to anything else.

The module reads only game controller devices of the configured categories. It does not acquire keyboards or mice, and DirectInput keyboard and mouse types are not in the default DeviceTypes set. The captured data is button and axis actuation, which is behavioural rather than identifying, but it is a continuous record of an operator’s physical input and should be handled under the deployment’s own data-handling policy.

IF-CMCTRL-03 adds a registered window class and a top-level window to the host process. The window procedure handles only WM_DESTROY and forwards everything else to DefWindowProc, so it processes no message content and offers no scriptable surface. The window class is registered process-wide and its name is fixed, so another process can locate the window by class or title; it can then post messages to it, and a WM_CLOSE from outside destroys the window and ends the pump thread, which would leave the acquired DirectInput devices without the handle they were acquired against. Nothing in the module detects or recovers from that.

13. Conformance

  1. Connect one DirectInput-only device, such as a flight stick, and one Xbox-class pad. Start a capture session. Confirm the log lists each DirectInput device as DirectInput controller: {Name} ({Guid}) and each XInput slot as XInput controller: {Name} (User {Index}).
  2. Confirm one entity per device appears in every frame, with the driver-reported name for the DirectInput device and XInput Gamepad (Player 1) or XInput Controller (Player 1) for the pad.
  3. Press and hold a button on the DirectInput device for several capture cycles. Confirm one Pressed event, then one Hold event per cycle, then one Released event, with the driver-reported button name.
  4. Press A on the XInput pad and confirm a button event named A.
  5. Move each analogue control in turn and confirm the ControllerAxes payload for the DirectInput entity carries x, y, z, rx, ry, rz plus a slider or pov field per control the device has, and that the payload for the XInput entity carries exactly left_x, left_y, right_x, right_y, left_trigger, right_trigger.
  6. Centre a hat switch and confirm pov0 reads -1, then hold it forward and confirm it reads a degree value rather than a fraction.
  7. Clear SendAxisData. Confirm the DirectInput entity stops publishing ControllerAxes and the XInput entity does not. This is the expected outcome for this revision under CTRL-01, not a failure of the check.
  8. Clear TrackEdgeDetection and confirm button events continue on both paths, which is the expected outcome for this revision.
  9. Verify tap resolution against Interval. With Interval at its 1000 ms default, tap a button briefly and confirm no button event is produced for that tap. Lower Interval to 50 ms, repeat, and confirm the tap is captured.
  10. Select the DirectInput entry in DeviceId for the Xbox-class pad and start a capture. Within roughly two seconds of polling, confirm the one-shot warning about unchanging DirectInput samples appears and advises selecting the XInput entry. Confirm axis values do not change while the sticks are moved.
  11. Unplug the DirectInput device mid-session. Confirm a DirectInput poll error warning appears once, then confirm the entity is still present in subsequent frames with its last values and that no disconnection event is published.
  12. Unplug the XInput pad mid-session with DebugLogs clear. Confirm no log line appears and that the entity keeps publishing its last state and ControllerAxes event. Set DebugLogs and repeat to confirm XInput GetState failed is then logged.
  13. Connect an additional controller while a capture session is running. Confirm it is not captured, and that it is still not captured after stopping and restarting the capture within the same host process.
  14. Confirm the entity id for a given device is identical across two separate capture sessions and two separate host process runs.

14. Revision history

RevDateProduct versionChange
12026-09-020ea243cInitial issue.
22026-09-020ea243cFull issue against source. Added the DirectInput and XInput interfaces as separate specifications, the hidden input window interface, entity and event schemas with real names, the two-rate timing model, and the error and disconnect table.
Opens your browser's print dialog. Choose "Save as PDF" as the destination.