Skip to content

Input and Controllers

M T edited this page Oct 4, 2026 · 1 revision

Input and Controllers

Halo PC 1.10 reads its input through DirectInput 8 (dinput8.dll: a system keyboard, a system mouse and optionally a joystick) and a little through USER32 messages. On Apple platforms there is no DirectInput, and on the headset there is no keyboard or mouse at all. The host therefore emulates DirectInput 8 in dinput8.c and feeds it from three native sources: an Apple GameController pad (gamecontroller.m), the visionOS look-and-pinch menu pointer (EngineMenuInput.swift + pointer.c), and scripted diagnostic input. By default the pad is translated into the keyboard and mouse input paths Halo already handles well (the "pad-to-key" bridge, HALO_PAD2KEY), rather than through Halo's PC joystick bindings.

This page also covers recentering, the per-frame head pose that the audio mixer and panorama consume, and the optional "a10" diagnostics. Controller haptics and the Bluetooth link guard are on Haptics.

Source files

File Role
native/EngineHost/dinput8.c DirectInput 8 (ANSI) emulation: IDirectInput8A, IDirectInputDevice8A for keyboard, mouse and a virtual gamepad; keyboard and mouse-button synthesis from the pad; menu detection.
native/EngineHost/gamecontroller.h API-neutral HostGCSnapshot, logical button indices, polling, haptics and link-statistics API, test injection hooks.
native/EngineHost/gamecontroller.m Apple GameController bridge: controller selection, capture snapshots with a reuse window, connect/disconnect observers, wireless discovery, Core Haptics playback.
native/EngineHost/gamecontroller_guard.h Pure pacing/backoff logic that keeps haptics from destabilising the Bluetooth link (documented on Haptics).
native/EngineHost/pointer.c / pointer.h / pointer_step.inc Menu pointer: tap queue, hover target and a servo that steers the engine's own UI cursor with synthetic mouse motion.
native/EngineHost/a10_gamepad.inc Optional (HALO_A10_GAMEPAD=1) session-only gamepad binding preset for Halo's native joystick path.
native/EngineHost/a10_control.inc Optional (HALO_A10_TRACE) diagnostics at original engine call boundaries.
native/EngineHost/d3d9.c IDirect3DDevice9::Present hosts the per-frame input work: scripted keys/look, pointer servo, the pad-to-key bridge, HALO_CTLLOG, HALO_A10_AUTOPLAY.
native/EngineHost/shims_misc.c USER32 message pump that turns Escape/Enter state into WM_KEYDOWN/WM_KEYUP for original text dialogs.
native/EngineVision/Sources/EngineMenuInput.swift Pure state machine turning visionOS spatial events (look + pinch) into one preview ray and completed clicks.
native/EngineVision/Sources/EngineImmersive.swift Receives spatial events, intersects rays with the menu panel, forwards taps/hover to the host, recenters and publishes head yaw/pitch/roll.
native/EngineVision/Sources/EngineVisionRuntime.m Bridge functions (enginevision_prepare_controller, enginevision_pointer, enginevision_menu_active) and the visionOS default environment.
native/EngineVision/Sources/HaloEngineVisionApp.swift App startup (enginevision_prepare_controller()), Recenter button, in-app control list.
docs/CONTROLS.md User-facing control summary.

Input pipeline

flowchart TD
    PAD["GCController extendedGamepad (DualSense, Xbox, MFi)"]
    POLL["hostgc_poll: capture() reused for 4 ms"]
    P2K["Present: pad-to-key bridge (sticks to WASD + mouse deltas)"]
    SEQ["Present: HALO_KEYSEQ / HALO_LOOKSEQ"]
    SPATIAL["visionOS spatial events (look + pinch)"]
    EMI["EngineMenuInput (preview + clicks)"]
    HIT["menuHit: ray to cylinder panel (u,v)"]
    PSET["host_pointer_set (hover / tap / cancel)"]
    SERVO["Present: host_pointer_servo"]
    GLOBALS["host_keyboard_state[256], host_mouse_dx/dy, host_mouse_buttons[8]"]
    KS["host_dinput_keyboard_state: globals + pad buttons as DIK keys"]
    MBS["host_dinput_mouse_buttons_state: globals + R2/L1"]
    KBD["DI keyboard: GetDeviceState / GetDeviceData"]
    MOUSE["DI mouse: GetDeviceState"]
    JOY["DI virtual gamepad (neutral unless HALO_PAD2KEY=0)"]
    U32["USER32 PeekMessageA: Esc / Enter messages"]
    ENG["Translated Halo input and UI"]

    PAD --> POLL
    POLL --> P2K
    POLL --> KS
    POLL --> MBS
    POLL --> JOY
    P2K --> GLOBALS
    SEQ --> GLOBALS
    SPATIAL --> EMI --> HIT --> PSET --> SERVO --> GLOBALS
    GLOBALS --> KS
    GLOBALS --> MBS
    GLOBALS --> MOUSE
    KS --> KBD
    KS --> U32
    MBS --> MOUSE
    KBD --> ENG
    MOUSE --> ENG
    JOY --> ENG
    U32 --> ENG
Loading

The globals host_keyboard_state[256] (DIK scan code to 0x80 when down), host_mouse_dx, host_mouse_dy, host_mouse_dz and host_mouse_buttons[8] are defined in dinput8.c:85-86. In this source tree they are written only by the Present-time injectors in d3d9.c, by the pointer servo, and by test/probe code; no AppKit or UIKit keyboard/mouse handler writes them. Mouse deltas are consumed (reset to zero) by each mouse GetDeviceState.

The GameController layer

Snapshot

HostGCSnapshot (gamecontroller.h:78-91) is deliberately free of Apple types so dinput8.c stays plain C and testable:

Field Meaning
connected A live extended gamepad (or connected test snapshot).
lx, ly, rx, ry Thumbsticks in [-1, +1], +x right, +y up (GameController convention).
lt, rt Analog triggers in [0, 1].
dpad_up/down/left/right D-pad booleans.
buttons[HOSTGC_BUTTON_COUNT] Logical buttons (below).
sequence Increments with each hardware capture; a reused reading keeps its sequence.

Logical buttons (gamecontroller.h:57-76); the index is also the DirectInput button number of the virtual gamepad:

Index Constant GameController element DualSense label
0 HOSTGC_BTN_A buttonA Cross
1 HOSTGC_BTN_B buttonB Circle
2 HOSTGC_BTN_X buttonX Square
3 HOSTGC_BTN_Y buttonY Triangle
4 HOSTGC_BTN_LSHOULDER leftShoulder L1
5 HOSTGC_BTN_RSHOULDER rightShoulder R1
6 HOSTGC_BTN_LTRIGGER leftTrigger.pressed L2 (digital)
7 HOSTGC_BTN_RTRIGGER rightTrigger.pressed R2 (digital)
8 HOSTGC_BTN_LTHUMB leftThumbstickButton L3
9 HOSTGC_BTN_RTHUMB rightThumbstickButton R3
10 HOSTGC_BTN_MENU buttonMenu Options / Start
11 HOSTGC_BTN_OPTIONS buttonOptions Create / Share / Back
12 HOSTGC_BTN_HOME buttonHome PS / Guide
13-16 HOSTGC_BTN_DPAD_UP/DOWN/LEFT/RIGHT dpad.* D-pad

Controller selection and capture

  • current_extended_controller (gamecontroller.m:91-101) prefers GCController.current (the one the user last used) when it has an extended gamepad profile, else the first attached controller that does.
  • capture_reading (gamecontroller.m:410-454) calls [controller capture], which returns an immutable, internally consistent copy, so a reading can never mix two hardware reports. Input is polled on demand from the game thread; no value-changed handlers or run loop are needed for input to flow.
  • Reuse window (gamecontroller.m:63-89, hostgc_poll at 380-399): one frame reads the pad from about half a dozen places (keyboard and mouse synthesis, joystick Acquire/Poll/GetDeviceState, the USER32 pump, the pad-to-key bridge), and each capture copies every element into new objects. A reading younger than HALO_PAD_REUSE_US (default 4000 µs, max 20000, 0 disables reuse) is handed out again. The age is measured from before the capture, the capture itself runs unlocked (so the diagnostics thread never waits on GameController), and a capture that started earlier than the kept one never replaces it (remember_reading). A "no pad" reading is reused too.
  • An injected test snapshot (hostgc_inject_test_snapshot) is returned verbatim, ahead of the reuse window, until cleared.

Initialisation and discovery

hostgc_init (gamecontroller.m:103-166) is idempotent. It reads the haptics guard settings, registers GCControllerDidConnectNotification and GCControllerDidDisconnectNotification observers, and starts wireless discovery only if no extended gamepad is present; the connect handler stops discovery because "a scan takes radio time from the link to the pad already in hand". The connect/disconnect handlers feed the link guard and release the haptics engine on disconnect (see Haptics).

It is called:

The main window also declares .handlesGameControllerEvents(matching: .gamepad) (HaloEngineVisionApp.swift:234).

DirectInput 8 emulation

Objects and vtables

DirectInput8Create (dinput8.c:711-713) builds two guest vtables on first use (IDirectInput8A, 11 methods; IDirectInputDevice8A, 32 methods) whose slots resolve through host_proc_address("dinput8.dll", "Iface::Method"); host_shims_dinput8_build (dinput8.c:716-721) registers those names. Objects live in objs[64] (slot 0 unused); a guest object is 16 bytes (+0 vtable, +4 slot index). obj_from_guest checks index, liveness and identity; a method on a bad object calls engine_fail ("dinput8 bad this"). When the table is full obj_new logs and CreateDevice/DirectInput8Create return DIERR_OUTOFMEMORY (the host does not exit).

DIObj (dinput8.c:70-82) holds kind, refs, acquired, DIPROP_BUFFERSIZE, the interpreted joystick data format (FmtObj fmt[64]), axis range (default 0..65535), dead zone (0..10000), a 128-entry buffered-event ring, and the previous pad/keyboard snapshot for edge detection.

IDirectInput8A

Method Behaviour
CreateDevice GUID_SysKeyboard (Data1 0x6F1D2B61), GUID_SysMouse (0x6F1D2B60), or the virtual joystick GUID 0x6F1D2B70; anything else is DIERR_DEVICENOTREG.
EnumDevices For device type 0, class GAMECTRL (4), or gamepad/joystick types, and without DIEDFL_FORCEFEEDBACK, reports one "Halo Vision Gamepad" (dwDevType 0x00010115). It is enumerated even before a controller connects, because Halo enumerates only at startup; later status/acquire/poll calls stay truthful. It is excluded from force-feedback enumerations (it has none).
GetDeviceStatus For the virtual joystick: DI_OK when connected, else DIERR_INPUTLOST.
FindDevice DIERR_DEVICENOTREG. Others return DI_OK or E_NOTIMPL.

IDirectInputDevice8A

Method Keyboard Mouse Virtual gamepad
GetCapabilities type 0x113, 128 buttons type 0x112, 3 axes, 8 buttons type 0x00010115, 6 axes, 17 buttons, 1 POV; attached flag from hostgc_connected()
EnumObjects none none X, Y, "Right Trigger" (Z), Rx, Ry, "Left Trigger" (Rz) at offsets 0..20; POV "D-pad" at 0x80; 17 named buttons at 0xC0+n
Get/SetProperty buffer size buffer size DIPROP_BUFFERSIZE (≤128), DIPROP_RANGE, DIPROP_DEADZONE
SetDataFormat accepted accepted Interpreted (below)
Acquire Clears buffer and snapshots current keys sets acquired Fails with DIERR_INPUTLOST while no pad; logged once per absence, every retry counted
Unacquire clears state clears state clears state and buffer
GetDeviceState 256 DIK bytes from host_dinput_keyboard_state dx, dy, dz then merged buttons; deltas reset after the read Fills Halo's declared layout from a fresh snapshot
GetDeviceData Buffered key edges always 0 events Buffered axis/button/POV changes
Poll DI_OK if acquired same Re-polls; lost pad unacquires
CreateEffect E_NOTIMPL
GetForceFeedbackState 0

Acquire retries: the comment at dinput8.c:624-626 records 1,961 identical log lines in one Build74 session from Halo retrying every frame while the pad was away; now it logs once and counts host_dinput_acquire_lost().

Joystick data format interpretation

joy_parse_format (dinput8.c:215-275) reads Halo's DIDATAFORMAT and records, for each object, its offset and source. Objects are matched by GUID (X/Y/Z/Rx/Ry/Rz/Slider/POV/Button Data1 values 0xA36D02E0..F5) or, without a GUID, by DIDFT_* type class. Halo's runtime-built c_dfDIJoystick declares all 80 entries with DIDFT_ANYINSTANCE; as in real DirectInput these bind in occurrence order (buttons 0..16 beyond which entries are skipped; one POV; axes X, Y, Z, Rx, Ry, Rz, Slider, Slider).

joy_fill_state writes 0x80/0 for buttons, a POV in hundredths of a degree clockwise from north (0xFFFFFFFF centred; diagonals 4500/13500/22500/31500), and axes mapped through:

Source Value
X / Y Left stick; Y inverted (GameController y-up to DirectInput y-down)
Rx / Ry Right stick; Ry inverted
Z Right trigger
Rz Left trigger
Slider Range midpoint

Sticks use a per-axis (not radial) dead-zone rescale ((|v|-dz)/(1-dz)), mapped from [-1,1] to [min,max]; triggers map [0,1] to [min,max] (dinput8.c:91-144).

GetDeviceData returns DIDEVICEOBJECTDATA records (offset, data, millisecond timestamp, sequence) from a ring capped by DIPROP_BUFFERSIZE; overflow drops the oldest and returns DI_BUFFEROVERFLOW once; DIGDD_PEEK reads without consuming (dinput8.c:541-599).

When the joystick reports neutral

joy_read_gameplay_snapshot (dinput8.c:418-430) zeroes every axis and button (keeping connected and sequence) whenever a menu is active or the pad-to-key bridge is on. The comment explains: menus are driven through one keyboard route even after the profile assigns a joystick, and reporting the same button through the joystick too "causes skipped choices"; in gameplay, neutralising the joystick prevents profile bindings from double-firing. So with default settings Halo sees a connected but idle gamepad, and all real input arrives as keyboard and mouse.

Menu detection

host_dinput_menu_active (dinput8.c:317-326) is true when either original engine flag is set:

Guest address Meaning (from comments)
0x00718FC9 (byte) UI shell active: set by 004C8930 after selecting levels\ui\ui, cleared by 004C8B40 on shell teardown.
0x00718FA6 (int16) Count of open pausing widgets: incremented by 00499924, decremented by 00497CF1. Covers in-game pause dialogs.

It returns 0 before the guest address space exists (engine_flat_base == NULL), because SwiftUI diagnostics can ask before the engine starts. The host only reads these flags; it never writes them. The visionOS presenter queries the same function as enginevision_menu_active() (EngineVisionRuntime.m:777-778).

Pad to keyboard and mouse

Buttons (host_dinput_keyboard_state, host_dinput_mouse_buttons_state)

dinput8.c:337-380. The function starts from host_keyboard_state (so an independently held key is preserved) and ORs in keys derived from the current pad reading.

Always (menus and gameplay):

Pad Key
Options/Start (MENU) Escape (0x01): pause menu and cinematic skip

Gameplay (no menu active), only when HALO_PAD2KEY is not 0:

Pad DIK key Halo PC action (per code comments)
Square (X) E (0x12) and R (0x13) Use/action and reload; E also acknowledges the cryotube prompt on Normal
Cross (A) Space (0x39) Jump
Circle (B) or L3 Left Ctrl (0x1D) Crouch
Triangle (Y) Tab (0x0F) Switch weapon
R1 F (0x21) Melee
L2 (button, or lt > 0.25) or R3 Z (0x2C) Zoom
D-pad up Q (0x10) Flashlight
D-pad down G (0x22) Switch grenade
D-pad left R (0x13) Reload only
D-pad right X (0x2D) Exchange weapon
Create/Back (OPTIONS) F1 (0x3B) Score
R2 (button, or rt > 0.25) Mouse button 0 Fire
L1 Mouse button 1 Grenade (the in-app control list says "L1: grenade"; UseButtonProbe.c asserts L2 never sets a mouse button: "Aim must never throw a grenade")

Menus (UI shell or pause widget): left stick beyond ±0.55 or the D-pad gives arrow keys (Up 0xC8, Down 0xD0, Left 0xCB, Right 0xCD; opposite directions cancel), Cross gives Enter (0x1C), Circle gives Escape. The pad's mouse buttons are suppressed in menus. Menu navigation works regardless of HALO_PAD2KEY.

The comment at dinput8.c:331-336 records the rule: never emit Cross=Enter during campaign gameplay.

The keyboard device's buffered data (keyboard_buffer_events) diffs successive host_dinput_keyboard_state results into DIK edge events, so Halo's buffered keyboard sees the same press/release edges a physical key would make.

Sticks (Present bridge)

Inside IDirect3DDevice9::Present (d3d9.c:547-568), once per engine frame, when HALO_PAD2KEY is not 0, a pad is connected and no menu is active:

  1. Keys this bridge set last frame are released (tracked in a private pk[] array).
  2. If rt > 0.5 or the R2 button is down, halo_haptics_note_fire() opens the player-fire window for haptics.
  3. Left stick past a 0.35 dead zone presses W/S/A/D (0x11, 0x1F, 0x1E, 0x20): movement is digital.
  4. Right stick past 0.35 adds rx * HALO_LOOKSCALE to host_mouse_dx and -ry * HALO_LOOKSCALE to host_mouse_dy (default scale 25 mouse counts per frame at full deflection). Because the delta is added per presented frame, turn speed scales with the engine's frame rate.

The comment calls this route a way to "sidestep Halo's flaky PC analog-joystick binding".

USER32 messages

pump_keyboard_messages (shims_misc.c:43-58) runs from PeekMessageA and, when the main window is active, queues WM_KEYDOWN/WM_KEYUP for Escape (VK 0x1B) and Return (VK 0x0D) on transitions of host_dinput_keyboard_state. This serves "original text dialogs that do not consume the DirectInput key state". GetAsyncKeyState/GetKeyState always return 0 and GetCursorPos returns the screen centre.

Routing modes

Mode Gameplay buttons Gameplay sticks Menus Joystick device
HALO_PAD2KEY unset or 1 (default; explicitly 1 on visionOS) Keyboard keys + mouse buttons WASD + mouse deltas Arrow/Enter/Escape keys Connected, always neutral
HALO_PAD2KEY=0 Native joystick buttons (Halo's profile bindings) Native joystick axes Arrow/Enter/Escape keys Live outside menus

With HALO_PAD2KEY=0, a fresh PC profile has no bindings for "Halo Vision Gamepad"; HALO_A10_GAMEPAD=1 can install a preset (below).

Effective control map (defaults)

Control Action
Left stick Move (W/A/S/D)
Right stick Look/aim (mouse motion)
R2 Fire
L2 or R3 Zoom
L1 Grenade (mouse button 1)
R1 Melee
Cross Jump; Enter in menus
Circle or L3 Crouch; Escape (back) in menus
Square Use and reload
Triangle Switch weapon
D-pad up / down / left / right Flashlight / switch grenade / reload / exchange weapon
Options Escape: pause, skip cinematic
Create F1: score

The in-app "PS5 controller controls" list (HaloEngineVisionApp.swift:191-200) matches this table. Discrepancy: docs/CONTROLS.md lists "L2 / left trigger: Throw grenade"; the code maps L2 to Z (zoom) and L1 to the grenade mouse button.

Menu pointer: look and pinch

On visionOS the front-end and pause menus are driven by the system's look-and-pinch interaction. Halo's menus already follow a DirectInput mouse cursor; the port steers that cursor instead of synthesising menu choices.

sequenceDiagram
    participant OS as visionOS compositor
    participant R as EngineImmersiveRenderer
    participant MI as EngineMenuInput
    participant P as pointer.c
    participant E as Engine thread (Present)
    OS->>R: onSpatialEvent (indirectPinch / pointer, phase, selectionRay)
    R->>MI: update(id, phase, target)
    loop each rendered frame while a menu is up
        R->>MI: drain() gives preview + completed clicks
        R->>R: menuHit(ray) to panel (u,v), refined by tracking band
        R->>P: host_pointer_set(u,v,onPanel,TAP) per click
        R->>P: host_pointer_set(u,v,onPanel,HOVER) for preview
    end
    loop each engine frame
        E->>P: host_pointer_servo()
        P->>P: read cursor at 0x00718F84/88
        alt tap queued and cursor within 6 px
            P->>E: host_mouse_buttons[0]=0x80 (one frame), then release
        else
            P->>E: host_mouse_dx/dy += step toward target
        end
    end
Loading

Spatial events (EngineMenuInput)

EngineMenuInput<ID> (EngineMenuInput.swift:1-60) treats spatial events as interactions, not as a continuous eye pose:

  • The first valid selection ray of an interaction ID is kept for its whole life; later updates (or another hand) cannot change or release it. A ray is valid if finite with non-zero direction.
  • A click is committed only when that interaction ends (.ended); .cancelled never clicks; an event that arrives already ended with no recorded ray cannot borrow an old ray.
  • drain() returns the newest active interaction's ray as the hover preview and every completed click since the last drain (two pinches between frames both survive).
  • setEnabled(false) (menu hidden, renderer stopped, tracking lost) clears everything and suppresses still-held IDs so a pinch that began before a reset cannot click afterwards; terminal events retire suppressed IDs so reused IDs work.
  • At most 16 active interactions, 16 pending clicks, 16 suppressed IDs.

EngineImmersiveRenderer.handle(spatial:) (EngineImmersive.swift:469-497) accepts .indirectPinch (look and pinch) and .pointer (physical mouse or simulator) events and records trackingAreaIdentifier on visionOS 26.

Ray to panel

Each frame with a menu up and no loading card (EngineImmersive.swift:780-821), the presenter transforms each ray into the recentred frame and intersects it with the menu cylinder (EngineImmersiveScreenGeometry.menuHit, EngineImmersiveScreenGeometry.swift:98-116) to get panel coordinates (0,0 top left) and an on-panel flag. On visionOS 26 the presenter also registers up to 32 hover tracking areas, one per lit text row of the interface found on the GPU, and when a pinch names one of those rows its vertical coordinate is snapped to that row's centre (and its horizontal coordinate kept inside the row's extent). The band detection itself belongs to the Immersive Presenter.

Completed clicks on the panel are sent as HOST_POINTER_TAP (2); the preview as HOST_POINTER_HOVER (0). If no pinch is in progress and the Head pointer setting is on, the preview is the head's forward ray instead. Leaving the menu, losing tracking, or stopping/replacing the session sends HOST_POINTER_CANCEL (-1).

Pointer servo (pointer.c)

Item Detail
Cursor location Guest globals 0x00718F84 (x) and 0x00718F88 (y), located by a desktop probe; the engine clamps them to a 640x480 interface extent (pointer.c:10-16).
host_pointer_set pointer.c:38-57. Maps (u,v) to pixels; CANCEL clears hover and taps; HOVER replaces the hover target; TAP also queues the target (ring of 16). Invalid or off-panel targets cannot click.
host_pointer_servo pointer.c:59-92. Called from Present before DirectInput is read. Outside menus it clears everything and releases a button it pressed, without touching a button the controller holds. In a menu: after a press frame it always spends one frame released; the target is the oldest queued tap or else the hover; if a tap's target is within 6 px (Manhattan) it presses mouse button 0 for one engine frame, otherwise it adds a step to the mouse deltas.
Step pointer_step.inc. 0.7 of the remaining distance in pixels per frame, capped at 300 px, converted to mouse counts through the inverse of the engine's measured acceleration (d counts move d px up to 5, then d + 0.047 d²), so the loop does not ring.

Head pointer setting

Setting Default Effect
HaloSettings.gaze_pointer ("Head pointer" toggle, EngineSettingsView.swift:105-107) Off unless HALO_GAZE_POINTER=1 (halo_settings.c:65-68) Between pinches the cursor follows the head's forward ray. Pinch taps work either way.

(The comment in pointer.c:35 says HALO_GAZE_POINTER=0 seeds it off; the actual seeding treats anything other than a leading 1 as off.)

Recentering and head pose

  • The control panel shows Recenter view while immersive (HaloEngineVisionApp.swift:183). It increments recenterGeneration in EngineImmersiveControl (EngineImmersive.swift:134, 266).
  • The renderer adopts the current device pose as neutralOriginFromHead when the generation changes, and once automatically on the first valid panorama frame, so loading/menu frames do not fix the world to a transient pose (EngineImmersive.swift:747-752).
  • Every frame, the head's forward direction relative to that neutral pose is published as yaw/pitch via halo_settings_set_head, and the roll via halo_settings_set_head_roll (EngineImmersive.swift:756-779). The Audio System mixer pans against the yaw; the panorama and stereo rendering consume yaw, pitch and roll (Panorama System).
  • Head pose does not drive aiming or movement; the app says so ("Your head position does not control aiming or movement").

"A10" diagnostics

a10 is the first campaign level, levels\a10\a10 (the Pillar of Autumn mission; the code's comments refer to its cryotube tutorial). a10_gamepad.inc checks for that exact map name (GSTR(0x00719779)), and the remaining "a10" switches were built while bringing up input in that level. None run by default.

Switch Code What it does
HALO_A10_GAMEPAD=1 a10_gamepad.inc:34-110 At the main-loop boundary 0x004C6E80, in campaign mode on levels\a10\a10, with a player, the virtual gamepad enumerated (0x006B1A74 == 0x6F1D2B70) and a controller connected: assigns device 0 to slot 0 if both are unassigned, and fills only empty (0x7FFF) binding fields: left stick move, right stick look, A jump, B back, X action, Y switch weapon, L1 grenade, R1 melee, L2/R3 zoom, R2 fire, L3 crouch, Create scoreboard, D-pad flashlight/grenade type/reload/exchange; accept and back button indices default to A and B. Session-only: no profile files, poses, commands or script gates change. Useful only with HALO_PAD2KEY=0, since otherwise the joystick reports neutral.
HALO_A10_TRACE (any value) a10_control.inc:6-46, a10_gamepad.inc:100-109 Logs [a10], [a10-cinematic], [a10-view], [a10-bsp], [a10-material], [a10-effects], [a10-call] and [a10-pad] lines at selected engine call boundaries (main loop every 60 iterations, BSP/material/effects functions). Read only.
HALO_A10_AUTOPLAY=1 d3d9.c:620-635 Diagnostic shortcut only. Every Present: forces Heroic difficulty, clears the save-busy flag, sets the mark_fast_setup script global, clears the player-input-disabled byte and the control record's input-block bit. The comment says it "must not run by default or establish campaign-playability evidence". visionOS sets it to 0 explicitly.

Both hooks run from engine_dispatch_override (overrides.c:252-253); see Engine Overrides and Hooks.

Scripted and diagnostic input

Variable Format Effect Read at
HALO_KEYSEQ startF:endF:hexScan,... Holds DIK scan codes during presented-frame ranges. d3d9.c:527-533
HALO_LOOKSEQ startF:endF:dx:dy,... Adds mouse deltas each frame in the range. d3d9.c:534-540
HALO_CTLLOG any value; 1 on visionOS Every 60 frames logs [ctl]: player-input-disabled byte, control record header, W key, device packet move/aim floats at 0x007124AC..B8, control record forward/strafe/yaw, pad state. "Shows exactly where input dies." d3d9.c:570-582

Environment variables

Variable Default Effect Read at
HALO_PAD2KEY on (unset); visionOS sets 1 0 disables the pad-to-keyboard/mouse bridge, the gameplay button keys and the trigger fire window; the native joystick goes live outside menus. dinput8.c:327-330, d3d9.c:551, EngineVisionRuntime.m:630
HALO_LOOKSCALE 25 Mouse counts per frame at full right-stick deflection. Read every frame with atof. d3d9.c:564
HALO_PAD_REUSE_US 4000 Controller reading reuse window in µs, clamped 0..20000; 0 captures every call. gamecontroller.m:78-89
HALO_GAZE_POINTER off 1 seeds the Head pointer setting on. halo_settings.c:65-68
HALO_A10_GAMEPAD off Exactly 1 enables the a10 joystick preset. a10_gamepad.inc:39-40
HALO_A10_TRACE off Presence enables a10 diagnostics logging. a10_control.inc:9, a10_gamepad.inc:41
HALO_A10_AUTOPLAY off; visionOS sets 0 Exactly 1 enables the state-forcing diagnostic shortcut. d3d9.c:625
HALO_KEYSEQ, HALO_LOOKSEQ, HALO_CTLLOG unset (HALO_CTLLOG=1 on visionOS) See above. d3d9.c
HALO_SIM_HEAD unset Simulator: yaw,pitch degrees replace the head pose used for menus, panning and panorama. EngineImmersive.swift:355-360
HALO_HAPTICS_GUARD, HALO_HAPTIC_MIN_INTERVAL_MS, HALO_HAPTIC_CONNECT_GRACE_MS on, 250, 3000 Controller link guard; see Haptics. gamecontroller.m:110-113

The visionOS defaults are applied with setenv(..., 0) in engine_worker (EngineVisionRuntime.m:612-645), so an explicit value in the launch environment wins.

Probe-only variables (in MenuInputProbe.m):

Variable Effect
HALO_PADSEQ start:end:button,... pad script; buttons A B X Y M 1 2 3 4 5 6 C, directions D U L R.
HALO_STICKSEQ start:end:lx:ly:rx:ry,... stick script.
HALO_NO_PROBE_INPUT Disables the mode's built-in pad edges.
HALO_PROBE_INPUT_FILE Live JSON mailbox (below); incompatible with HALO_KEYSEQ.
HALO_PROBE_INPUT_FROM_FRAME, HALO_PROBE_LIVE_SECONDS, HALO_PROBE_FRAMES Live-control start frame, session length (1..3600 s), frame limit.
HALO_PROBE_POINTER u,v: from frame 300 hover the pointer there and log servo progress.
HALO_PROBE_MOUSE_TRACE, HALO_PROBE_MOUSE_STAIR, HALO_PROBE_FIND_CURSOR Cursor-response measurements used to find the cursor globals and the acceleration curve.

Diagnostics

Counter Source
gamepad_reads Joystick GetDeviceState calls (host_dinput_gamepad_reads).
keyboard_events Keyboard buffered events delivered (host_dinput_keyboard_events).
dinput_acquire_lost Refused joystick Acquires.
controller_captures, controller_reuses, controller_reuse_us hostgc_poll_stats.
controller_connected, controller_sequence, button_mask, axes[6] One hostgc_poll per sample.
pointer_frames, pointer_x, pointer_y host_pointer_stats: servo frames and last cursor read.
controllerLink hostgc_link_stats plus battery (EngineDiagnostics.swift:88-105).

Sampled by EngineDiagnosticsBridge.m:54-61. The control panel shows "Controller connected · N menu events · M pad reads" (EngineDiagnostics.swift:142). See Diagnostics and Telemetry.

Threading

Thread Input work
Engine thread All DirectInput calls, the Present-time bridge, pointer servo, USER32 pump. Calls hostgc_poll.
Main thread hostgc_init at app start; GameController notifications (queue nil); diagnostics polls.
Immersive render thread Spatial-event handling (under pinchLock), menu hit-testing, host_pointer_set, head pose publication.

hostgc_poll and the snapshot cache are protected by an os_unfair_lock; pointer.c by pointer_lock. The DirectInput object table and the host_* globals are not locked; they are touched from the engine thread, with the pointer servo also running there.

Tests and probes

Test Runner What it asserts
test_dinput_lifecycle.c portable source check 256 COM lifetimes with stale-pointer rejection; enumeration frees its descriptor; full object table recovers with DIERR_OUTOFMEMORY; buffered overflow/peek; disconnect unacquires; reacquire; gameplay-to-pause transitions release Space and press Enter; Acquire retries counted with one log per absence; host_dinput_menu_active is 0 before the guest exists.
test_pointer_input.c portable Against the measured mouse curve: a tap survives later hovers; rapid taps keep separate targets and press/release pairs; cancel drops pending taps and releases; leaving menus clears input without overwriting a controller-held button; invalid targets never click; the tap queue is bounded at 16.
test_pointer_step.c portable Convergence within 6 frames without ringing, small corrections within 3, still converges if the curve is ±25 % off, full jump ≤ 75 counts.
test_controller_snapshot.m Mac Mocked GameController: one capture per window, default window 4000 µs, disconnect seen after the window, test injection immediate, HALO_PAD_REUSE_US=0 captures every call, clamping, older capture never replaces newer, 4000 concurrent polls consistent.
test_controller_link_guard.c portable See Haptics.
MenuInputValidation.swift Mac (swiftc) EngineMenuInput: no input while disabled; two taps between frames survive; delivered once; another hand cannot release; original ray kept; missing/invalid rays and cancels never click; reset suppresses a held pinch; terminal events retire reused IDs; backlog capped at 16.
UseButtonProbe.c Manual; not registered in run_source_checks.py (links against host objects) Every gameplay button-to-key mapping with press and release; Square gives E; disconnect clears keys; a physically held E is preserved; shell flag gives Enter not E; Options gives Escape outside menus; L2 never sets a mouse button, R2/L1 do; pause widget isolates gameplay keys; HALO_PAD2KEY=0 disables them.
GuestWeaponTelemetryProbeTests.m Manual Includes MenuInputProbe.m: the probe's read-only guest pose/weapon telemetry reports trigger/action state and merged mouse buttons, validates salts, capacities and types, never writes guest memory, and emits valid JSON.
MenuInputProbe.m via tools/probe_engine_menu_input.py Manual (needs generated engine and game data) Runs the original engine on the Mac with injected pad snapshots. Modes neutral, dpad, cross, down, enter, back, profile, difficulty, difficulty-key, launch press fixed edges at fixed frames; launch also taps Cross+Options periodically to skip cinematics. The Python wrapper relinks the probe (--build), refuses stale binaries, clones the game directory (APFS clonefile) so saves are untouched, sets HALO_FRAME_CAPTURE, enforces a timeout and writes run.json. Its scope string: "Original engine with diagnostic input injection; no hardware-input or playable-campaign proof."

Live mailbox format (HALO_PROBE_INPUT_FILE, parsed by live_parse at MenuInputProbe.m:72): a JSON object with sequence (strictly increasing when the command changes), issued_at_ms, optional timeout_ms (20..2000, default 500), buttons (names A..HOME), sticks (lx ly rx ry), triggers (lt rt), dpad (up down left right, opposite pairs rejected) and snapshot. Malformed, stale or expired commands return the pad to neutral; a command's lease is never renewed by rereading.

Related pages

Clone this wiki locally