Repository navigation
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.
| 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. |
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
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.
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 |
-
current_extended_controller(gamecontroller.m:91-101) prefersGCController.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_pollat 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 eachcapturecopies every element into new objects. A reading younger thanHALO_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.
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:
- by the visionOS app at startup on the main thread:
EngineAppSession.initcallsenginevision_prepare_controller()(HaloEngineVisionApp.swift:66-67,EngineVisionRuntime.m:480); - by DirectInput
EnumDevices,CreateDeviceof the joystick and joystickAcquire; - by
hostgc_play_haptic.
The main window also declares .handlesGameControllerEvents(matching: .gamepad) (HaloEngineVisionApp.swift:234).
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.
| 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. |
| 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().
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).
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.
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).
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.
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:
- Keys this bridge set last frame are released (tracked in a private
pk[]array). - If
rt > 0.5or the R2 button is down,halo_haptics_note_fire()opens the player-fire window for haptics. - Left stick past a 0.35 dead zone presses W/S/A/D (
0x11,0x1F,0x1E,0x20): movement is digital. - Right stick past 0.35 adds
rx * HALO_LOOKSCALEtohost_mouse_dxand-ry * HALO_LOOKSCALEtohost_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".
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.
| 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).
| 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.
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
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);.cancellednever 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.
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).
| 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. |
| 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.)
- The control panel shows Recenter view while immersive (
HaloEngineVisionApp.swift:183). It incrementsrecenterGenerationinEngineImmersiveControl(EngineImmersive.swift:134,266). - The renderer adopts the current device pose as
neutralOriginFromHeadwhen 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 viahalo_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 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.
| 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 |
| 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. |
| 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.
| 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.
| 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.
- Haptics: pulses on the same controller, and the link guard.
- Audio System: head-yaw panning; the trigger-held fire window.
- Immersive Presenter: spatial events, tracking bands, recentering, head pose.
- visionOS App: app session, control panel, settings window.
-
Runtime Settings:
gaze_pointerand other live settings. - Win32 Compatibility Layer: USER32 message pump and the import mechanism.
-
Engine Overrides and Hooks:
0x004C6E80and other boundaries. -
Direct3D9 Bridge:
Present, where the per-frame input work runs. - Diagnostics and Telemetry
- Environment Variables
- Testing and Source Checks
Documents master-chef at commit 9f915af (v1.0.3). Unofficial project, not affiliated with Microsoft, Bungie, Gearbox or Apple. Original code is MIT licensed; game content is not included.
Overview
- Architecture Overview
- Repository Layout
- Glossary
- Environment Variables
- Contributing Guide
- Open Questions
Translation
- Static Translation Pipeline
- XWA Decoder and Lifter
- Function Address Lists
- EngineReuse Runtime
- x87 Floating Point
Host runtime
- EngineHost Overview
- Win32 Compatibility Layer
- Threading and Synchronization
- Guest Memory and Heap
- Engine Overrides and Hooks
- Runtime Settings
Graphics
- Direct3D9 Bridge
- Metal Renderer
- Shader Translation
- Textures and Texture Packs
- Geometry Fast Paths
- Radial Fog
Panorama and presentation
- Panorama System
- Panorama Budget and LOD
- Frame Pacing
- visionOS App
- Immersive Presenter
- Layer Alignment
Audio and input
Tooling and process