Skip to content

Runtime Settings

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

Runtime Settings

The port has a small set of its own presentation settings (stereo depth, panorama field of view, haptics strength, surround brightness, spatial menu, frame-rate target, own-weapon gain, head pointer, layer alignment, frame pacing) that the original game knows nothing about. They live in one lock-free C structure, halo_settings.c, shared between the engine thread that renders and the SwiftUI settings panel that adjusts them. Each value is seeded from an environment variable when first read, so scripted desktop runs behave as before, and each can then be changed live from the headset without restarting the engine. A few other startup choices (render resolution, the device's default environment) are made by the app before the engine starts. mods/runtime-settings.json records the shipped defaults for the release; it is documentation, not a loader.

Source files

File Role
native/EngineHost/halo_settings.h HaloSettings struct, accessor and head-pose API.
native/EngineHost/halo_settings.c Atomic storage, environment seeding (ensure), clamping, halo_settings_get/set, radial-fog switch.
native/EngineVision/Sources/EngineSettingsView.swift EngineSettingsModel (reads with halo_settings_get, writes every change with halo_settings_set) and the SwiftUI panel; the HaloRenderResolution picker.
native/EngineVision/Sources/EngineVisionRuntime.m Device environment defaults (engine_worker), render resolution and command line (enginevision_start), HALO_CORE_TELEMETRY.
native/EngineVision/Sources/EngineImmersive.swift Publishes head pose (halo_settings_set_head, halo_settings_set_head_roll); reads layer alignment, head pointer, backdrop brightness.
mods/runtime-settings.json Release record of defaults.
native/EngineHost/tests/test_halo_settings_layer_align.c Layer-alignment default and live toggle.

Data flow

flowchart LR
    env["process environment (HALO_*)"] -->|"first read: ensure()"| store["halo_settings.c atomics"]
    ui["EngineSettingsView (SwiftUI)"] -->|"init: halo_settings_get"| model["EngineSettingsModel"]
    store --> model
    model -->|"didSet: halo_settings_set (clamped)"| store
    store -->|"clamped accessors, read at frame boundaries"| eng["engine thread: panorama_hooks.inc, frame_pacing_hooks.inc, pointer.c"]
    store --> mix["audio thread: directsound_mixer.c, haptics.c"]
    store --> pres["presenter: EngineImmersive.swift"]
    pres -->|"halo_settings_set_head / _roll every frame"| store
    defaults["UserDefaults HaloRenderResolution (@AppStorage)"] -->|"enginevision_start"| cmd["HALO_CMDLINE_EXTRA -vidmode"]
    worker["engine_worker device defaults (setenv, no overwrite)"] --> env
Loading

Storage and thread safety

Every field is an independent _Atomic 32-bit word (floats are bit-cast with memcpy) read and written with relaxed ordering (halo_settings.c:9-15). There is no lock on the render path; "a torn pair is harmless because every value is read through its own clamped accessor". Initialisation is published with an acquire/release flag: the first call to any accessor, get or set runs ensure(), which seeds every field from the environment once per process (halo_settings.c:43-83). The header's contract is that values are read once per frame at well-defined points, never mid-pass, "so a change can not tear a frame between the two eyes" (halo_settings.h:3-8).

halo_settings_set (halo_settings.c:100-125) validates each field and replaces an out-of-range or NaN value with a fallback; the accessors repeat the same range check on read (halo_settings.c:127-136). Note that several set/accessor fallbacks differ from the environment defaults (the table lists both).

Persistence

Only the render resolution persists across launches (@AppStorage("HaloRenderResolution")). The live settings in HaloSettings are held in memory: EngineSettingsModel initialises from halo_settings_get and pushes every change with halo_settings_set, but nothing in EngineSettingsView.swift or halo_settings.c writes them to UserDefaults or a file. At the next launch they start again from the environment, which on the headset means the compiled defaults below.

Live settings

Setting (HaloSettings field) Environment seed Env default and valid range set/accessor range and fallback Settings panel control Consumers
stereo_separation: half the eye separation in Halo world units (one unit is ten feet; 0 = mono, which halves the world passes) HALO_STEREO_IPD (mm, 40–90, default 63) → mm / 1000 / 2 / 3.048; then HALO_STEREO_SEPARATION (world units, 0–1) overrides; HALO_STEREO starting with 0 forces 0 63 mm → about 0.01033 units 0–1, else 0 "Render both eyes" toggle and "Eye separation" 45–80 mm (step 1); off sends 0 panorama_hooks.inc stereo enable and eye offset (panorama_hooks.inc:33-34)
panorama_vfov: vertical field of view of each panorama view, radians HALO_PANORAMA_VFOV (degrees, 60–175) 105° 1.0472–3.0543 rad (60–175°), else 2.6179939 rad (150°) "Vertical coverage" 90–175° (step 5) panorama_view_vfov() (panorama_hooks.inc:50)
haptics_strength: scale for synthesized controller haptics (0 disables) HALO_HAPTICS starting with 0 → 0 1 0–4, else 1 "Strength" 0–2 (step 0.05) haptics.c (haptics.c:38, haptics.c:103)
backdrop_brightness: surround that continues the picture past its edges HALO_BACKDROP (0–2) 1 0–2, else 1 Surround "Brightness" 0–2 (step 0.05) EngineImmersive.swift (EngineImmersive.swift:981)
spatial_shell: render the front-end menu through the panorama path so its 3D backdrop surrounds the viewer HALO_SPATIAL_MENU starting with 0 → off on boolean "Spatial front-end menu" toggle host_panorama_dispatch: in the shell ([00718FC9] nonzero) with this off, the panorama path is skipped (panorama_hooks.inc:381-385)
panorama_target_fps: frame rate the bearing budget keeps (draws more of the sphere while busy time stays under it) HALO_PANORAMA_TARGET_FPS (10–60) 30 10–60, else 27 "Keep at least" 20–30 fps (step 1) panorama_target_fps() (panorama_hooks.inc:39-40); see Panorama Budget and LOD
self_gain_db: dB added to sounds Halo places on the listener (own weapon, reload, melee), which Halo hands to DirectSound 20 dB down HALO_SELF_GAIN_DB (0–24) 18 dB 0–24, else 18; halo_settings_self_gain() returns 10^(dB/20) "Your own weapon" 0–24 dB (step 1) directsound_mixer.c (directsound_mixer.c:118, directsound_mixer.c:275)
gaze_pointer: menu cursor also follows the head between pinches HALO_GAZE_POINTER starting with 1 → on off boolean Menus "Head pointer" toggle EngineImmersive.swift hover preview (EngineImmersive.swift:816), host_pointer_enabled()
layer_align: rotate panorama layers drawn in earlier engine frames to the newest centre camera (presentation only) HALO_LAYER_ALIGN starting with 1 → on off boolean Joins "Line up older views" toggle EngineImmersive.swift (EngineImmersive.swift:710); see Layer Alignment
frame_pacing: optional publication pacing on a 90 Hz grid (45/30/22.5/18/15 fps rungs) HALO_FRAME_PACING equal to 1, even or on → on (anything else, including malformed input, off) off on only when exactly 1 Frame rate "Even frame cadence" toggle frame_pacing_hooks.inc (frame_pacing_hooks.inc:84); see Frame Pacing

Rationale recorded in the source:

  • 105° vertical field (halo_settings.c:52-57): "Rectilinear projection spreads a wide vertical field thinly: at 150 degrees the centre of view gets under 3 pixels per degree, which reads as a soft, low-resolution image. 105 degrees nearly triples that for the same pixels, and the cap beyond it is faded rather than shown."
  • Head pointer off: "the eyes choose (the system's gaze ray at the pinch, its hover glow between)".
  • Layer alignment off (halo_settings.c:69-76): checked only on synthetic pictures; while the stick turns it bends the side bands at eye level and sharpens the trailing join's cross-fade, so it stays off "until a b30 stick-turn A/B on the headset shows the joins improve without those side effects".
  • Frame pacing off: "Aggregate headset traces cannot establish a clear per-frame win. Keep experimental pacing opt-in, including after malformed input." Timedemo mode ([007196D8] nonzero) forces pacing off regardless (frame_pacing_hooks.inc:82-84).
  • Own-weapon gain: "in a headset firing that cannot be heard over the music is a fault" (halo_settings.h:32-36).

Panel notes

EngineSettingsModel converts between the panel's units and the stored ones: eye separation mm / 1000 / 2 / 3.048 ("Halo's world unit is ten feet, so millimetres convert through 3.048"), degrees to radians for the field of view. When stereo is off the model stores 0 and remembers 63 mm for the slider. Any @Published change calls push(), which sends the whole structure; halo_settings_set stores every field, so the panel is the single writer of these fields once it has been opened. The panel header says "Changes apply to the next rendered frame."

The panel's slider ranges are narrower than what halo_settings accepts (for example 20–30 fps against 10–60, 90–175° against 60–175°, 45–80 mm against the environment's 40–90 mm), so an environment value outside a slider's range is shown clamped by SwiftUI's Slider; the code does not establish whether merely opening the panel re-sends such a value.

Settings that are not live

Setting Storage Default Effect and where read
Radial fog HALO_RADIAL_FOG exactly 1, cached once per process with pthread_once off Experimental bearing-invariant fog. Kept out of the live structure so "simultaneous diagnostic and renderer reads cannot see different values during initialization" (halo_settings.c:17-31). Read by d3d9.c, d3d9_render.inc, metalrenderer.m and the diagnostics. See Radial Fog.
Render resolution UserDefaults key HaloRenderResolution via @AppStorage; picker offers 1280x960, 1600x1200, 1920x1440, 2048x1536 2048x1536 Read only in enginevision_start (visionOS) and passed as -vidmode W,H,60 through HALO_CMDLINE_EXTRA; accepted when 640 <= W <= 4096 and 480 <= H <= 3072. HALO_VIDMODE (a full W,H,R string) takes precedence; an existing HALO_CMDLINE_EXTRA is never overwritten. Takes effect at the next launch because "the engine fixes its back buffer when it creates the device" (EngineVisionRuntime.m:681-695).
Core telemetry HALO_CORE_TELEMETRY on in the app (off unless set to 0); always off in the desktop host Sets host_core_telemetry once before the engine runs (EngineVisionRuntime.m:656-659). See Threading and Synchronization.
Command line HALO_CMDLINE_EXTRA unset (desktop); -vidmode ... (visionOS) Appended to "C:\Halo\halo.exe" -window -novideo.

Head pose (not a setting)

halo_settings_set_head(yaw, pitch) and halo_settings_set_head_roll(roll) carry where the viewer is looking inside the panorama, published every frame by EngineImmersive.swift (EngineImmersive.swift:772, EngineImmersive.swift:778). "The pose is not a setting, so it is neither seeded from the environment nor carried in HaloSettings; it starts at zero and is replaced every frame" (halo_settings.c:138-154). Non-finite values become 0; roll is clamped to ±1.5 rad and a non-finite roll is ignored. Yaw is read by the mixer so a sound stays with its object when only the head turns (directsound_mixer.c:138); pitch and roll by the panorama hooks (roll places the two centre cameras along the tilted baseline, unless HALO_STEREO_ROLL=0).

Environment defaults installed by the app

On visionOS, engine_worker installs this list with setenv(name, value, 0) before calling host_run and logs each resolved value as [device-config] NAME=value (EngineVisionRuntime.m:612-645). Because the overwrite flag is 0, a value already present in the launch environment wins. The comment explains the purpose: "Match the verified campaign probe without relying on launch-time shell variables, which a normal headset launch does not supply."

Variable Value Meaning (owning page)
HALO_FF3D 1 Fixed-function 3D draw path for shaderless declaration draws (Direct3D9 Bridge)
HALO_PANORAMA 1 Multi-view panorama (Panorama System)
HALO_PANORAMA_DENSE 1 Dense panorama layout (Panorama System)
HALO_PANORAMA_WORLD_FP 1 World-space first-person weapon (Panorama System)
HALO_PV_SKIN_FAST 1 Exact-token skinning fast path (Geometry Fast Paths)
HALO_NATIVE_GATHER 1 Native BSP light/shadow gathers (Engine Overrides and Hooks)
HALO_DRAW_FASTPATH 1 Static geometry reuse, capped at 128 MiB (Metal Renderer)
HALO_PAD2KEY 1 Controller to keyboard/mouse bridge (Input and Controllers)
HALO_A10_AUTOPLAY 0 Probe autoplay off (Input and Controllers)
HALO_UNLOCK_CAMPAIGN 1 In-memory campaign unlock (Engine Overrides and Hooks)
HALO_AUDIO_VOICE_TRACE 1 Bounded five-second DirectSound/decoder summaries (Audio System)
HALO_CTLLOG 1 Controller logging (Input and Controllers)
HALO_HSC_TRACE 120 HSC script trace interval (Engine Overrides and Hooks)
HALO_HSC_TRACE_MAX 128 HSC trace events per snapshot
HALO_CORE_TELEMETRY 1 Core residency/scheduling/frame-split telemetry (Diagnostics and Telemetry)

After the list, HALO_DRAW_FASTPATH is applied to the renderer immediately (mr_set_fast_paths), and HALO_TEXTURE_PACK / HALO_SHADER_PACK are set (again without overwriting) to the bundled TextureMods.hvt and ShaderMods.hvs when present (EngineVisionRuntime.m:646-654). The live-setting variables (HALO_STEREO*, HALO_PANORAMA_VFOV, ...) are not in the list, so on the headset they start at the compiled defaults.

The runtime-settings.json record

mods/runtime-settings.json is a release record of the defaults the app ships with. docs/FEATURES.md states: "The JSON records compiled defaults; it is not a separate settings loader." No code reads it at run time; changing it changes nothing. It is listed with its size and SHA-256 in SOURCE_MANIFEST.json and included in the complete release archive.

JSON key Value Corresponds to
formatVersion 1 record format
version 1.0.3 MARKETING_VERSION in project.yml
runtimeBaseline Build91 the runtime build these defaults were verified against
appliesAutomatically true defaults need no user action
renderResolution 2048x1536 HaloRenderResolution default / -vidmode 2048,1536,60
targetEngineFPS 30 panorama_target_fps (HALO_PANORAMA_TARGET_FPS)
panoramaVerticalFOVDegrees 105 panorama_vfov (HALO_PANORAMA_VFOV)
stereoIPDMillimetres 63 stereo_separation (HALO_STEREO_IPD)
hapticsStrength 1 haptics_strength
backdropBrightness 1 backdrop_brightness (HALO_BACKDROP)
selfGainDecibels 18 self_gain_db (HALO_SELF_GAIN_DB)
spatialMenu true spatial_shell (HALO_SPATIAL_MENU)
gazePointer false gaze_pointer (HALO_GAZE_POINTER)
experimentalLayerAlignment false layer_align (HALO_LAYER_ALIGN)
experimentalFramePacing false frame_pacing (HALO_FRAME_PACING)
radialFog false HALO_RADIAL_FOG
audio.sampleRate 48000 OUTPUT_RATE in directsound.c
audio.bufferFrames 1536 output_frames_per_buffer default (HALO_AUDIO_FRAMES, 64–2048, overrides) in directsound.c
audio.queuedBufferCount 3 OUTPUT_BUFFERS in directsound.c
environmentDefaults 15 variables exactly the engine_worker list above
privacy text states that no per-device tracking history, pose, identity, registration, save state or signing data is included, and that the sampled headset had no saved render-resolution override
limits text "Matching settings and assets do not establish equal performance or resolve known gameplay issues."

When a default changes in code, the record should be updated to match; nothing enforces that automatically in the files examined for this page.

Tests

Test Asserts
test_halo_settings_layer_align.c In fresh child processes (settings are seeded once per process): unset, empty, 0 and yes give 0, 1 gives 1, and halo_settings_get agrees with the accessor. Live: setting layer_align to 1 and back changes only that field (gaze_pointer and spatial_shell unchanged). Built by run_source_checks.py with halo_settings.c.
test_radial_fog_settings.c, test_frame_pacing_host.c, test_haptics_*.c, test_mixer_quality.c and several panorama tests Link halo_settings.c and exercise the radial-fog switch, pacing toggle, haptics strength and self gain from their subsystems' side; see Radial Fog, Frame Pacing, Haptics, Audio System.

Related pages

Clone this wiki locally