-
Notifications
You must be signed in to change notification settings - Fork 5
visionOS App
native/EngineVision is the visionOS application target, shipped as HaloVision with the display name "Halo Vision". It is the process that hosts everything else. A SwiftUI App declares a full immersive space, which is the launch scene, plus two auxiliary windows. An Objective-C runtime file (EngineVisionRuntime.m) starts the statically translated Halo engine (host_run) on one dedicated 1.5 GB-stack worker thread. That same file implements the platform side of the host's presentation interface (metalwin_*), which receives frames and panorama layers from the Direct3D9 Bridge. Swift code then reads engine state, frames and panorama leases through a small C bridge (EngineVisionBridge.h). It also finds and imports the owned game files, keeps saves under Application Support, and exposes live presentation settings.
This page covers the app shell, startup, the bridge API, asset storage, settings, the runtime state machine, and the project and plist configuration. Two other pages cover the rendering side: Immersive Presenter explains how frames reach the headset, and Diagnostics and Telemetry covers the report files.
| File | Role |
|---|---|
project.yml |
XcodeGen spec: one application target, sources, resources, frameworks, compiler flags, Info.plist keys |
Info.plist |
Extra plist keys merged with the generated ones: game-controller profile, file sharing |
Resources/PrivacyInfo.xcprivacy |
Privacy manifest |
Resources/ThirdPartyNotices.txt |
Bundled third-party license text (MojoShader and others) |
Sources/HaloEngineVisionApp.swift |
@main SwiftUI app, scenes, EngineAppSession startup owner, setup window UI |
Sources/EngineRuntimeModel.swift |
Main-actor observable mirror of the C runtime state, polled every 0.2 s |
Sources/EngineVisionBridge.h |
C API between Swift and the runtime (states, frames, panorama, telemetry, pointer, metalwin_*) |
Sources/EngineVisionRuntime.m |
Implements the bridge: engine worker thread, device defaults, frame/panorama publication, zero-copy GPU pool, profile and core telemetry collection |
Sources/audio_session.inc |
AVAudioSession coordinator, included into the runtime file |
Sources/EngineVision-Bridging-Header.h |
Exposes haptics.h, gamecontroller.h, halo_settings.h, panorama.h, frame_pacer.h, the bridge and the diagnostics bridge to Swift |
Sources/EngineAssets.swift |
Game-file catalog, bundled-payload manifest, import, verification, save preservation, EngineAssetStore
|
Sources/EngineSettingsView.swift |
"Presentation" settings window bound to HaloSettings
|
Sources/EngineFrameTexture.swift |
Immutable Metal snapshot of the latest flat BGRA frame |
Sources/EngineFrameView.swift |
MTKView preview of the flat frame inside the setup window |
Sources/EngineHaptics.swift |
Forwards audio-derived haptic events to the host's controller (see Haptics) |
Sources/EngineMenuInput.swift |
Look-and-pinch menu input state machine (used by the presenter) |
Sources/EngineImmersive*.swift, EnginePanorama*.swift, EngineWorldCadence.swift, EngineLayerAlignment.swift
|
See Immersive Presenter and Layer Alignment |
Sources/EngineDiagnostic*.swift/.h/.m, EngineCoreTelemetry.swift, EngineDeepTelemetry.swift, EngineGatherDiagnostics.swift
|
See Diagnostics and Telemetry |
Tests/AssetValidationMain.swift, BundledAssetValidationMain.swift, MutableAssetPreservationMain.swift, RuntimeFrameHarness.m, AudioSessionRecoveryValidation.m, MenuInputValidation.swift, FrameTextureValidation.swift
|
Tests for the code on this page (see Testing) |
flowchart TB
subgraph App["HaloEngineVisionApp (SwiftUI App)"]
IS["ImmersiveSpace id HaloEngineImmersive<br/>CompositorLayer, .full immersion<br/>defaultLaunchBehavior .presented"]
WS["WindowGroup HaloSetup<br/>EngineContentView<br/>defaultLaunchBehavior .suppressed"]
WP["WindowGroup HaloSettings<br/>EngineSettingsView<br/>defaultLaunchBehavior .suppressed"]
end
SES["EngineAppSession (@MainActor)<br/>0.2 s refresh timer"]
ASSETS["EngineAssetStore"]
MODEL["EngineRuntimeModel<br/>0.2 s refresh timer"]
IMM["EngineImmersiveSession"]
DIAG["EngineDiagnostics"]
BRIDGE["EngineVisionBridge.h (C)"]
RT["EngineVisionRuntime.m"]
WORKER["engine_worker pthread<br/>1.5 GB stack, QOS_CLASS_USER_INTERACTIVE<br/>host_run(halo.exe, root)"]
HOST["EngineHost + translated engine<br/>(d3d9.c calls metalwin_*)"]
IS -- "startImmersive(layer)" --> SES
WS --> ASSETS
WS --> MODEL
WS --> IMM
SES --> ASSETS
SES --> MODEL
SES --> IMM
MODEL --> DIAG
MODEL --> BRIDGE
IMM --> BRIDGE
BRIDGE --> RT
RT -- "pthread_create" --> WORKER
WORKER --> HOST
HOST -- "metalwin_present / _gpu / _dropped" --> RT
HaloEngineVisionApp (HaloEngineVisionApp.swift:9-54) owns one @StateObject EngineAppSession and declares three scenes:
| Scene | ID | Content | Launch behaviour | Notes |
|---|---|---|---|---|
ImmersiveSpace |
EngineImmersiveSession.spaceID = "HaloEngineImmersive"
|
CompositorLayer(configuration: EngineImmersiveLayerConfiguration()), whose closure calls session.startImmersive(layer)
|
.presented |
.immersionStyle(selection: .constant(.full), in: .full) and .upperLimbVisibility(.hidden). Watches scenePhase to flush diagnostics. |
WindowGroup |
"HaloSetup" |
EngineContentView |
.suppressed |
Default size 1180 x 760. Dismisses itself when immersiveActive becomes true. |
WindowGroup |
"HaloSettings" |
EngineSettingsView |
.suppressed |
Default size 620 x 820. Opened from the setup window's "Presentation settings" button. |
There is no RealityKit content. The immersive scene is a CompositorServices CompositorLayer, and the app draws into it with Metal (see Immersive Presenter). The app's preferred default scene role is CPSceneSessionRoleImmersiveSpaceApplication (project.yml), so the game itself is the launch scene.
The comment at HaloEngineVisionApp.swift:35-40 gives the reason. visionOS keeps an app's windows visible inside a full immersive space, and the setup window has no place in the game. The app therefore never opens it. visionOS shows it on its own when the immersive space could not present at launch. In that case the window opens the space itself as soon as the engine has a frame: .onChange(of: engine.frameSequence) at L253-L261 runs at most once per showing of the window (autoOpened).
On every scenePhase change (with initial: true) the app records app.scenePhase: <phase> in the immersive trace and calls engine.diagnostics.flush(reason:wait:). When the phase is .background, the flush waits until the report is on disk, because a report written as the app leaves the foreground may be the run's last (L23-L28).
EngineAppSession (L56-L116) is a @MainActor ObservableObject that holds the three long-lived models:
| Property | Type | Purpose |
|---|---|---|
assets |
EngineAssetStore |
Finds, imports and verifies game files |
engine |
EngineRuntimeModel |
Mirrors C runtime state; owns EngineDiagnostics
|
immersive |
EngineImmersiveSession |
Owns the compositor renderer lifetime |
immersiveActive |
@Published Bool |
Deduplicated copy of immersive.$active, used to dismiss the setup window |
init() calls enginevision_prepare_controller(), which runs hostgc_init() on the main thread so controller discovery is registered before game startup. It then subscribes to immersive.$active, runs refresh() once, and schedules refresh() every 0.2 s.
refresh() (L81-L115) does two jobs:
- It copies preparation and immersive counters (submitted, cancelled, tracking-loss, GPU completed/failed frames, GPU milliseconds, configuration string, last submission) into
engine.diagnostics. - It drives startup from
assets.phaseand sets the immersive loading card:
assets.phase |
Action | Loading card title | Shown |
|---|---|---|---|
.checking |
none | "Preparing Halo · Build N" | yes, no progress |
.importing |
none | "Preparing Halo · Build N" | yes, progress = copied/total |
.missing, .failed
|
none | "Halo setup needs attention" | yes |
.ready |
engine.start(root: assets.rootURL) if engine.canStart
|
"Starting Halo · Build N", or "Halo stopped · Build N" when the state is FAILED/STOPPED
|
while frameSequence == 0 or failed |
So the engine starts automatically, inside the 0.2 s timer, as soon as the assets are ready. No button press is needed. Build N comes from CFBundleVersion (L7).
EngineContentView (L118-L290) has two parts: a 340-pt sidebar and an EngineFrameView preview of the flat engine frame. The sidebar contains:
- The state label: Ready, Starting, Running, Stopped, Failed, or Unknown, mapped from
ENGINEVISION_*. On failure the status text is shown. - "Import owned Halo folder" (
.fileImporter,[.folder]). Only shown when the app bundle carries no game payload, and only enabled while the engine is idle. - Import progress, the current file, percent, the receipt's file count and byte total.
- "Start Halo" / "Stop Halo" (
engine.start/engine.stop). - "Enter immersive Halo" / "Leave immersive view". Disabled while opening or before the first engine frame.
- "Recenter view", which calls
immersive.recenter(). - "Presentation settings", which calls
openWindow(id: "HaloSettings"). - "Retry local setup" for a failed bundled payload.
- A controller status line and a "PS5 controller controls" help disclosure.
- A "Diagnostics" disclosure (status, frame size, frame number, save status) and a
ShareLinktoreport.json.
The view also declares .handlesGameControllerEvents(matching: .gamepad).
Opening and closing the immersive space is guarded by generation tokens (EngineImmersiveOwnership, described in Immersive Presenter):
-
ensureImmersiveOpen()(L277-L289) takes a token fromimmersive.prepare()and awaitsopenImmersiveSpace(id:). It only acts on the result if it still owns the token..userCancelledand.errorcallfinish()and set a status line. -
toggleImmersive()dismisses when active, then callsfinish()only if the token is still current. -
.onChange(of: immersive.closeRequest)dismisses the space after the renderer loop ends, but only ifcanDismissClosedSession(generation)still holds.
EngineRuntimeModel is a main-actor mirror of the C runtime, polled every 0.2 s:
| Published | Source |
|---|---|
state |
enginevision_runtime_state() |
status |
enginevision_copy_status into a 512-byte buffer |
frameSequence, frameSize
|
enginevision_frame_info |
controllerStatus, diagnosticsStatus
|
diagnostics.controllerStatus, diagnostics.saveStatus after diagnostics.sample(...)
|
canStart is state == IDLE. isActive is STARTING || RUNNING. start(root:) calls enginevision_start(root.path). If that returns nonzero and the runtime did not itself record FAILED, the status becomes "Engine start failed with POSIX status N." stop() calls enginevision_request_stop(). Each refresh() also feeds EngineDiagnostics.sample(...), the entry point for all report and timeline writing (see Diagnostics and Telemetry).
The C states are declared in EngineVisionBridge.h:13-19:
stateDiagram-v2
[*] --> IDLE
IDLE --> STARTING: enginevision_start accepted the root
STARTING --> FAILED: path too long, stack or QoS rejected, pthread_create failed
STARTING --> RUNNING: metalwin_init ok, or first metalwin_present*
STARTING --> FAILED: metalwin_init invalid size
RUNNING --> FAILED: frame buffer realloc failed
RUNNING --> STOPPED: host_run returned 0
RUNNING --> FAILED: host_run returned nonzero
STOPPED --> [*]
FAILED --> [*]
There is no transition back to IDLE. enginevision_start returns EALREADY once the state has left IDLE, and the status strings say "Relaunch the app to run it again." The engine is a one-shot, process-wide singleton.
sequenceDiagram
participant Main as Main thread (EngineAppSession timer)
participant Model as EngineRuntimeModel
participant RT as enginevision_start
participant Audio as audio_session_prepare
participant W as engine_worker (pthread)
participant Host as host_run
participant D3D as d3d9.c Present
participant Pub as metalwin_* (runtime)
Main->>Model: assets.phase == .ready and canStart
Model->>RT: enginevision_start(rootPath)
RT->>RT: HALO_VIDMODE / HaloRenderResolution -> HALO_CMDLINE_EXTRA "-vidmode W,H,60"
RT->>RT: HALO_PANORAMA_GPU -> gpu_enabled, host_panorama_set_gpu_sink()
RT->>RT: append HALO_CMDLINE_EXTRA to host_command_line
RT->>RT: lock, state IDLE? -> STARTING, runtime_root, runtime_exe = root/halo.exe
RT->>Audio: audio_session_prepare() (visionOS)
RT->>RT: pthread_attr: 1.5 GB stack, QOS_CLASS_USER_INTERACTIVE
RT->>W: pthread_create + detach
RT-->>Model: 0
W->>W: record engine_worker_port
W->>W: setenv device defaults (no overwrite), HALO_TEXTURE_PACK, HALO_SHADER_PACK
W->>W: mr_set_fast_paths(HALO_DRAW_FASTPATH == "1")
W->>W: host_core_telemetry = HALO_CORE_TELEMETRY != "0"
W->>W: set_status(STARTING, "Loading Halo and preparing the renderer.")
W->>Host: host_run(runtime_exe, runtime_root)
Host->>D3D: game creates device, presents frames
D3D->>Pub: metalwin_init(w, h) -> RUNNING
D3D->>Pub: metalwin_present_gpu / _dropped / metalwin_present
Host-->>W: result
W->>W: STOPPED (0) or FAILED (nonzero)
EngineVisionRuntime.m:669-759:
- A
NULLor empty root returnsEINVAL. -
Video mode (visionOS only) (L681-L695). The mode comes from
HALO_VIDMODE. If that is empty, it comes from theUserDefaultsstringHaloRenderResolution("WxH", accepted only when 640 ≤ w ≤ 4096 and 480 ≤ h ≤ 3072). If neither is set, it is2048,1536,60. The result is written toHALO_CMDLINE_EXTRAas-vidmode <mode>without overwriting an existing value. The comment records how the default was reached. 4:3 keeps the HUD layout. 2560x1920 was too much for the M2 with three views. 1920x1440 was used after stereo doubled the world passes. The default moved back up to 2048x1536 once the duplicate view was removed and the engine, not pixels, became the limit. -
Zero-copy panorama switch (L704-L710).
HALO_PANORAMA_GPUdefaults to on for visionOS unless set to0, and to off on the Mac unless set to a non-0value. When enabled, the runtime registersHaloPanoramaGPUSink { gpu_acquire, gpu_release, gpu_carry }with the host. The comment explains why the Mac can also run it: a lease that dropped three of ten layers once shipped as a black headset screen while every desktop composite looked fine. - If
HALO_CMDLINE_EXTRAis set and not already present, it is appended tohost_cmdline_buf, which starts as"C:\Halo\halo.exe" -window -novideo(L29-L30). - Under
runtime_lock: if the state is notIDLE, returnEALREADY. Otherwise copy the root androot/halo.exeintoPATH_MAXbuffers (overflow setsFAILED, "Imported game path is too long.", and returnsENAMETOOLONG), then setSTARTING. - visionOS:
audio_session_prepare()(see Audio session). - Thread attributes (L730-L749):
- Stack size of exactly 1536 MiB, read back and checked. A mismatch is treated as
EINVAL. -
QOS_CLASS_USER_INTERACTIVE. The comment explains that without a QoS class the engine thread ran at default priority, eligible for efficiency cores, beside a 90 Hz user-interactive compositor. The headset ran the engine at about 4 fps in play while the Mac ran it at 16. - Any failure sets
FAILEDwith "No unsafe fallback was started." There is no smaller-stack retry.
- Stack size of exactly 1536 MiB, read back and checked. A mismatch is treated as
-
pthread_create(engine_worker)andpthread_detach. Returns 0.
- Stores its Mach port in
engine_worker_port. The draw profile and core telemetry use it to read the thread's CPU time. - On visionOS, applies device defaults with
setenv(name, value, 0), so a value already present in the launch environment wins. Each value is logged as[device-config] NAME=value. A normal headset launch has no shell environment, so these defaults reproduce the configuration of the verified campaign probe:
| Variable | Device default | Comment in source |
|---|---|---|
HALO_FF3D |
1 |
|
HALO_PANORAMA |
1 |
see Panorama System |
HALO_PANORAMA_DENSE |
1 |
|
HALO_PANORAMA_WORLD_FP |
1 |
world-space first-person arms in the side bands |
HALO_PV_SKIN_FAST |
1 |
exact-token skinning |
HALO_NATIVE_GATHER |
1 |
native light/shadow gathers; 0 keeps the translated path |
HALO_DRAW_FASTPATH |
1 |
static geometry reuse, capped at 128 MiB (see Geometry Fast Paths) |
HALO_PAD2KEY |
1 |
|
HALO_A10_AUTOPLAY |
0 |
|
HALO_UNLOCK_CAMPAIGN |
1 |
|
HALO_AUDIO_VOICE_TRACE |
1 |
bounded five-second DirectSound summaries |
HALO_CTLLOG |
1 |
|
HALO_HSC_TRACE |
120 |
|
HALO_HSC_TRACE_MAX |
128 |
|
HALO_CORE_TELEMETRY |
1 |
measurement only; 0 restores the pre-telemetry paths |
- Calls
mr_set_fast_paths(getenv("HALO_DRAW_FASTPATH") == "1")on the engine thread before any draws. An early diagnostics snapshot may already have cached the renderer switch, so it is applied explicitly here (L646-L650). - Points
HALO_TEXTURE_PACKandHALO_SHADER_PACKat the bundledTextureMods.hvtandShaderMods.hvs, again without overwriting (see Visual Mods Pipeline). - Decides
host_core_telemetryonce: on unlessHALO_CORE_TELEMETRYstarts with0. - Sets status "Loading Halo and preparing the renderer.", runs
host_run(runtime_exe, runtime_root)(host.c:625), stores the exit code, and moves toSTOPPEDorFAILEDwith one of three messages.
d3d9.c calls an interface named after the macOS window (metalwin). On the Mac it is implemented by EngineHost/metalwin.m. On visionOS, EngineVisionRuntime.m implements it and creates no UIKit or AppKit objects:
| Function | visionOS behaviour |
|---|---|
metalwin_init(w, h, title) |
Rejects sizes outside 1..8192 (FAILED). Otherwise sets RUNNING, "Renderer ready ... waiting for the first engine frame." |
metalwin_present(bgra, w, h) |
Byte path (L531-L595). Copies the flat frame into latest_frame (realloc on growth; failure sets FAILED and requests quit) and increments latest_sequence. When host_panorama_frame reports a complete frame of matching size, it copies all HALO_PANORAMA_LAYERS layers into latest_panorama and fills panorama_info. A FLAT status also advances gpu_flat_sequence, so a menu supersedes any pending world frame. |
metalwin_present_gpu(slot, w, h) |
Zero-copy path (L423-L457). Builds the frame's EngineVisionPanoramaInfo, calls the frame pacer (host_frame_pacer_present) with no lock held, then mr_commit_async(gpu_published, p). The slot is published from the command buffer's completion handler. |
metalwin_present_dropped(w, h) |
A world frame the pool could not take (L459-L478). Increments the sequence and records an incomplete panorama state without touching the last published zero-copy frame. |
metalwin_should_close() |
host_quit_requested != 0 |
metalwin_poll() |
No-op |
All three present functions call engine_thread_sample() first (L67-L88). It records which CPU the engine thread is on and, if core telemetry is on, adds one frame-split sample (see Diagnostics and Telemetry).
The panorama pool internals (slot states, carry, publication ordering) are summarised in Immersive Presenter. The host-side producer is documented in Panorama System.
| Thread | What it does here |
|---|---|
| Main |
EngineAppSession / EngineRuntimeModel timers, SwiftUI, enginevision_start, all enginevision_* reads |
engine_worker |
host_run, every metalwin_*, engine_thread_sample
|
| Metal completion thread |
gpu_published (publishes a pool slot) |
| Compositor render thread |
enginevision_panorama_gpu_latest/release, enginevision_frame_info/copy_latest_frame, enginevision_pointer, enginevision_menu_active
|
HaloVision.audio-session serial queue |
Audio session transitions and a 250 ms watchdog |
All runtime state (runtime_state, runtime_status, frames, panorama info, pool slots) is guarded by one os_unfair_lock runtime_lock. The pacer wait in metalwin_present_gpu happens with the lock released ("nothing is locked while it waits"). The display-latch and publish-lag samples are lock-free _Atomic ring buffers (L97-L115).
Every function below is implemented in EngineVisionRuntime.m unless noted.
| Function | Line | Behaviour |
|---|---|---|
int enginevision_start(const char *game_root) |
h:33 | Starts the one engine instance. The root must contain halo.exe. Returns 0 only after the worker thread exists. Error codes: EINVAL, EALREADY, ENAMETOOLONG, or the pthread error. |
void enginevision_prepare_controller(void) |
h:35 |
hostgc_init() on the main thread, before game startup |
void enginevision_request_stop(void) |
h:36 | Sets host_quit_requested = 1. The engine stops at its next host boundary. |
int enginevision_runtime_state(void) |
h:37 | One of ENGINEVISION_IDLE..FAILED (0..4) |
int enginevision_exit_code(void) |
h:38 |
host_run return value once it has returned |
void enginevision_copy_status(char *, size_t) |
h:39 | Copies the 512-byte human-readable status |
int enginevision_menu_active(void) |
h:147 |
host_dinput_menu_active(): 1 while the engine has a menu up |
void enginevision_pointer(float u, float v, int on_panel, int action) |
h:189 | Forwards to host_pointer_set. (u, v) is normalised with (0,0) at top left. Action -1 = HOST_POINTER_CANCEL, 0 = hover/preview, 2 = queue one left-click. Taps stay queued until the engine cursor reaches the target. This is not a held-button API. |
| Item | Line | Notes |
|---|---|---|
EngineVisionFrameInfo { sequence, width, height, byte_count } |
h:21-26 | |
bool enginevision_frame_info(out) |
h:43 | True when a byte frame exists. sequence advances on every present, including zero-copy ones. |
bool enginevision_copy_latest_frame(dst, capacity, out) |
h:44-45 | Race-safe info/copy split: returns false if capacity is smaller than the newest frame, and the caller retries after fresh info. out is always filled. |
EngineFrameDigest / enginevision_frame_digest(out)
|
h:28-29 | At most 1024 strided pixel samples of the byte frame (alpha masked): FNV-1a style hash, non-black count, count differing from the first pixel. Used by diagnostics to show the engine is drawing. |
| Item | Line | Notes |
|---|---|---|
EngineVisionPanoramaInfo |
h:47-69 |
sequence, width/height, byte_count (0 for zero-copy frames), per-layer projection_x/y, source_epoch, flat_sequence, scene_epoch, per-layer layer_epoch, status (0 flat/menu, 1 complete panorama, 2 incomplete world), failure_reason, stereo, per-layer viewport u_min/v_min/u_max/v_max, layer_pose[layer][9] (position, forward, up), cut_epoch. Arrays are indexed by layer; the zenith and nadir sit at layers 5 and 6. |
bool enginevision_panorama_info(out) |
h:72 | Copies one coherent publication. True only when byte_count > 0. Fields are returned even on false. |
bool enginevision_copy_panorama(dst, capacity, out) |
h:73 | Copies all layers' bytes atomically with the info |
| Item | Line | Notes |
|---|---|---|
EngineVisionPanoramaGPUSnapshot { slot, info, textures[HALO_PANORAMA_LAYERS] } |
h:80-84 | Textures are unretained id<MTLTexture> pointers, valid while leased |
bool enginevision_panorama_gpu_enabled(void) |
h:85 | The HALO_PANORAMA_GPU decision |
bool enginevision_panorama_gpu_latest(out) |
h:86 | Leases the newest ready slot (one lease). Also stamps the display latch for the frame pacer. |
void enginevision_panorama_gpu_release(int32_t slot) |
h:87 | Call from the completion handler of the command buffer that sampled the slot |
EngineVisionPanoramaGPUStats / enginevision_panorama_gpu_stats(out)
|
h:93-99 |
published, dropped, publish_failed, carry_copies, carry_failed, publish_superseded, latest_slot, slot_state[3], slot_leases[3], last_error[128]. These counters tell apart "the compositor could not lease a frame" and "the engine never made one". |
| Item | Line | Notes |
|---|---|---|
EngineVisionDrawProfile / enginevision_draw_profile(out)
|
h:104-145 | Cumulative engine-thread profile: passes and their time, readback, draws, vertex/submit/upload time, the game's Sleep calls and yields, haptic onsets, panorama budget (extra passes per two frames, tier 0-3, busy seconds), gaze pointer frames and cursor, engine-thread CPU ns, frames per CPU number (16 buckets), P/E core counts, Metal pipeline compile/wait/background/prewarm/archive counters, audio watchdog rebuilds. Field meanings are in Diagnostics and Telemetry. |
EngineVisionCoreTelemetry / enginevision_core_telemetry(out)
|
h:151-184 | Frame split, ticks, thread scheduling, process rusage and power counters. Returns false with a zeroed output while HALO_CORE_TELEMETRY=0 or before the engine starts. See Diagnostics and Telemetry. |
enginevision_diagnostic_sample(EngineDiagnosticSample *) and enginevision_capture_host_log(path) are declared in a separate header and implemented in EngineDiagnosticsBridge.m. Both are documented in Diagnostics and Telemetry.
The file is included into EngineVisionRuntime.m and compiled only for visionOS. audio_session_prepare() (audio_session.inc:62-140) runs once (dispatch_once) and does the following:
- Creates the serial queue
HaloVision.audio-session. - Moves the DirectSound stall watchdog onto a 250 ms timer on that queue (
host_dsound_watchdog_set_external). Previously the watchdog ran once per engine frame, so it slept through the very stalls it exists for (level loads, engine stops). - Observes interruptions, media-services resets (which force a queue rebuild), foreground and background transitions, and route changes.
- Activates synchronously with reason
startup.
audio_session_resume sets the Playback category and activates the session. If needed it rebuilds the output and resumes the queue. On failure it pauses the output and retries the whole transaction no sooner than 2 s later from the tick. Coming back to the foreground clears an interruption's shouldResume = NO, because returning to the game is the player's own action. Going to the background pauses cleanly, because the app has no audio background mode. Route logging is limited to 64 lines and records port types only, never device names or UIDs (L19-L28). The mixer side is covered in Audio System.
EngineAssetCatalog (EngineAssets.swift:11-24) requires these files at minimum sizes, and requires halo.exe to have a specific SHA-256 (the PC 1.10 executable):
| Path | Minimum bytes |
|---|---|
halo.exe |
2,000,000 (and SHA-256 must equal EngineAssetCatalog.haloSHA256) |
maps/ui.map |
2,000,000 |
maps/a10.map |
90,000,000 |
maps/bitmaps.map |
300,000,000 |
maps/sounds.map |
200,000,000 |
shaders/vsh.bin |
30,000 |
shaders/fx.bin |
900,000 |
strings.dll |
1,000,000 |
halo-vision-registry.txt |
100 |
halo-vision-registry.txt is a small text file that setup creates locally to stand in for the Windows registry values the game reads. It is private to the user and never published (see Setup Wizard and Win32 Compatibility Layer).
| Location | Contents |
|---|---|
<bundle>/GamePayload/ and <bundle>/GamePayloadManifest.json
|
Optional. A locally built app can carry the owned game files (project.yml adds them as optional resources from .setup/; public source releases never include them). |
Application Support/HaloVision/Game/ |
Root for a user-imported folder (no bundled payload) |
Application Support/HaloVision/PackagedGames/<payloadID>/ |
Root for a bundled payload, one directory per manifest payloadID
|
<root>/engine-vision-import.json |
EngineImportReceipt (importedAt, fileCount, totalBytes, executableSHA256, payloadID) |
<root>/Users/Player/Documents/My Games/Halo/ |
EngineAssetImporter.bundledMutableRoot: profiles, checkpoints and playlists the game writes through its emulated Windows user directory |
Application Support/HaloVision/.Game-import-<UUID>/ |
Staging during import, removed on failure |
Application Support/HaloVision/.Game-backup-<UUID>/ |
Previous install while the new one is promoted |
Documents/Diagnostics/<runID>/ |
Diagnostics (see Diagnostics and Telemetry) |
Because the payload directory is keyed by payloadID, a new game payload never overwrites an older version's files or saves (L411-L413).
EngineAssetStore (L378-L470) publishes phase ∈ {checking, missing, importing, ready, failed(String)}, plus detail, copied, total, currentFile, receipt, and rootURL. hasBundledPayload is true if either GamePayload or GamePayloadManifest.json exists in the bundle.
flowchart TD
A["EngineAssetStore.init"] --> B{"hasBundledPayload?"}
B -- yes --> C["prepareBundledGame()"]
B -- no --> D["restore(): validate(HaloVision/Game)"]
D -- ok --> R["ready"]
D -- error --> M["missing"]
M -- "user picks folder" --> I1["importFolder(source)"]
I1 --> P1["importing (detached task)"]
P1 -- ok --> R
P1 -- error --> F["failed"]
C --> L["load + check manifest"]
L --> V{"validateInstalledBundle(PackagedGames/payloadID)"}
V -- ok --> R
V -- not ready --> P2["importing (detached, verify SHA-256 of every file)"]
P2 -- ok --> R
P2 -- error --> F
F -- "Retry local setup" --> C
EngineBundledManifest.check() (L80-L112) rejects the manifest unless all of the following hold:
-
formatVersion == 1. -
payloadIDis 64 lowercase hex characters. -
fileCount > 0and equalsfiles.count. -
totalBytes > 0. -
executableSHA256is the catalog hash. - Every path is relative, with no empty,
.or..components, no backslash or NUL, is notengine-vision-import.json, and is unique case-insensitively. - Every hash is valid.
- Sizes sum without overflow to
totalBytes. - Every required file is present at its minimum size.
- The manifest's
halo.exehash matches.
EngineAssetImporter.importFolder(_:destination:bundleManifest:progress:) (L149-L228):
-
Build the entry list.
- With a manifest, entries come straight from manifest paths. FileManager URLs are not used for this, because
/varand/private/varaliases once cost file names. Each entry must be a regular, non-symlink file of the exact size. - Without a manifest,
collect()enumerates the chosen folder, skipping hidden files and symlinks. It requires every file to resolve inside the folder and canonicalises case: top-levelmaps/shadersdirectories become lowercase, and top-level files are lowercased. Case-insensitive duplicates are rejected. Required files and thehalo.exehash are checked before copying begins.
- With a manifest, entries come straight from manifest paths. FileManager URLs are not used for this, because
-
Copy to a staging directory
.Game-import-<UUID>beside the destination, in 1 MiB chunks.Task.checkCancellation()runs between chunks, and progress is reported per chunk. With a manifest, every file's SHA-256 is computed during the copy and must match. -
Bundled repair only:
preserveMutableGameData(from: destination, into: staging)copies the player's entireUsers/Player/Documents/My Games/Halotree from the live install over the staged defaults. The tree is rejected if it contains symlinks or non-file, non-directory items. - Write the receipt into staging atomically, then
validate(staging). -
promoteStaging(L291-L320):- If no install exists, move staging into place.
- Otherwise rename the old install to
.Game-backup-<UUID>, then move staging into place. - If that move fails, rename the backup back. If the rollback also fails, the error names the backup directory so the saves can be found.
- Remove the backup on a best-effort basis. A cleanup failure does not fail the import.
- Any error removes the staging directory and rethrows.
validateInstalledBundle checks the receipt's payloadID, fileCount and totalBytes against the manifest. It then calls validatePreparedFiles, which checks the size of every manifest file except those under bundledMutableRoot (isMutableInstalledPath, case-insensitive prefix match). This means a save that has changed size does not make an otherwise complete install look broken and trigger a repair (L241-L257). The design rationale is in the comment at L116-L119: the bundled copies are first-launch defaults, and after installation the whole subtree belongs to the player.
Info.plist sets UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace, so the app's Documents folder is visible in Files. That folder holds Diagnostics/ and, in simulator capture mode, SimCaptures/. The importer reaches folders the user picks through security-scoped URLs (startAccessingSecurityScopedResource).
The window is titled "Presentation". EngineSettingsModel (L10-L60) initialises every control from halo_settings_get. The host's defaults are themselves read from environment variables, so scripted runs behave the same. Every didSet calls push(), which writes the whole HaloSettings struct back with halo_settings_set. Changes take effect on the next rendered frame without an engine restart. The underlying store and its environment variables are documented in Runtime Settings.
| Section | Control | Range / step |
HaloSettings field |
Host env default |
|---|---|---|---|---|
| Depth | "Render both eyes" toggle | stereo_separation > 0 |
HALO_STEREO (on unless 0) |
|
| Depth | "Eye separation" slider | 45-80 mm, step 1 |
stereo_separation = mm / 1000 / 2 / 3.048 (Halo world unit is 10 ft = 3.048 m) |
HALO_STEREO_IPD 63 mm |
| Field of view | "Vertical coverage" | 90-175°, step 5 |
panorama_vfov (radians) |
HALO_PANORAMA_VFOV 105° |
| Joins | "Line up older views" | layer_align |
HALO_LAYER_ALIGN off (see Layer Alignment) |
|
| Frame rate | "Keep at least" | 20-30 fps, step 1 | panorama_target_fps |
HALO_PANORAMA_TARGET_FPS 30 |
| Frame rate | "Even frame cadence" | frame_pacing |
HALO_FRAME_PACING off (see Frame Pacing) |
|
| Surround | "Brightness" | 0-2x, step 0.05 | backdrop_brightness |
HALO_BACKDROP 1.0 |
| Surround | "Spatial front-end menu" | spatial_shell |
HALO_SPATIAL_MENU on |
|
| Menus | "Head pointer" | gaze_pointer |
HALO_GAZE_POINTER off |
|
| Sound | "Your own weapon" | 0-24 dB, step 1 | self_gain_db |
HALO_SELF_GAIN_DB 18 |
| Haptics | "Strength" | 0-2x, step 0.05 | haptics_strength |
HALO_HAPTICS (1 unless 0) |
| Haptics | "Test pulse" button, "N detected · M played" | hostgc_play_haptic(1, 0.5) |
||
| (bottom) | "Render resolution (next launch)" picker | 1280x960, 1600x1200, 1920x1440, 2048x1536 |
@AppStorage("HaloRenderResolution"), default "2048x1536"
|
read in enginevision_start; HALO_VIDMODE wins |
The haptics status line and its counters refresh every 0.5 s from EngineHaptics.state, eventsTaken and eventsPlayed.
EngineFrameTexture.refresh(expectedSequence:) (EngineFrameTexture.swift:13-45) reads enginevision_frame_info, copies the bytes with enginevision_copy_latest_frame, and validates the size. It then allocates a new bgra8Unorm texture for every new sequence and exposes a bgra8Unorm_srgb view of it. Calling replace() on the existing texture would race a command buffer from the previous compositor frame that is still sampling it. EngineFrameView (EngineFrameView.swift) is a 60 fps MTKView that letterboxes the frame with a single full-screen triangle. It also attaches a GCEventInteraction with handledEventTypes = .gamepad and receivesEventsInView = false.
| Setting | Value |
|---|---|
| Project name / target |
EngineVision, type: application, platform: visionOS
|
| Deployment target | visionOS 26.0 |
PRODUCT_NAME / module |
HaloVision / EngineVision
|
PRODUCT_BUNDLE_IDENTIFIER |
org.example.halovision (placeholder; local setup supplies a real identifier and signing, see Device Preparation and Signing) |
MARKETING_VERSION / CURRENT_PROJECT_VERSION
|
1.0.3 / 103
|
TARGETED_DEVICE_FAMILY |
7 (Vision) |
| Swift | 5.0, SWIFT_STRICT_CONCURRENCY: minimal
|
| Optimisation |
GCC_OPTIMIZATION_LEVEL 0 (base), 2 (Release) |
| Bridging header | Sources/EngineVision-Bridging-Header.h |
| Header search paths |
Sources, third_party/mojoshader, native/EngineHost, native/EngineReuse, native/build/engine-reuse/whole-exe
|
OTHER_CFLAGS |
-frounding-math -ffp-contract=off -DENGINE_FLAT_MEMORY=1 -DHALO_ARM64_FENV_FAST=1, MojoShader profile switches (Metal only) (see x87 Floating Point, Shader Translation) |
OTHER_LDFLAGS |
-lm |
| Frameworks | Foundation, SwiftUI, UIKit, Metal, MetalKit, QuartzCore, CompositorServices, ARKit, CoreHaptics, GameController, AudioToolbox, AVFAudio |
| Native sources compiled in |
EngineHost/host.c, shims_kernel32.c, shims_misc.c, directsound.c, directsound_mixer.c, haptics.c, halo_settings.c, pointer.c, vorbis_shim.c, d3d9.c, texture_decode.c, metalshader.c, dinput8.c, ddraw.c, resources.c, overrides.c, gamecontroller.m, threading.c, metalrenderer.m, MojoShader core and Metal profile, and the generated engine chunk_*.c, engine_bundle.c, engine_imports.c
|
| Resources |
ThirdPartyNotices.txt, PrivacyInfo.xcprivacy, .setup/VisualMods/{TextureMods.hvt, ShaderMods.hvs, CEnshineSources.zip, VisualModsManifest.json}, and optionally .setup/GamePayload (folder) and .setup/GamePayloadManifest.json
|
tools/build_engine_vision.py runs xcodegen generate --spec native/EngineVision/project.yml and xcodebuild. When Xcode's visionOS platform runtime is missing it falls back to a "direct xros toolchain" build, which compiles the same sources with xcrun --sdk xros clang and writes its own binary Info.plist. Only the direct path writes a HaloBuildID key (a UTC timestamp), which diagnostics report as buildID (build_engine_vision.py:187). See Build System.
Generated from project.yml (GENERATE_INFOPLIST_FILE: YES) plus the checked-in Info.plist:
| Key | Value | Source |
|---|---|---|
CFBundleDisplayName |
Halo Vision | project.yml |
GCSupportsControllerUserInteraction |
YES | project.yml |
GCRequiresControllerUserInteraction |
{ visionOS: true } |
Info.plist |
GCSupportedGameControllers |
[{ ProfileName: ExtendedGamepad }] |
Info.plist |
NSHandsTrackingUsageDescription |
"A pinch chooses the menu item you are looking at; hands are never recorded." | project.yml |
NSWorldSensingUsageDescription |
"Head tracking keeps the curved original-engine screen aligned with your chosen reclined pose; aiming remains on the controller." | project.yml |
UILaunchScreen_Generation, UIApplicationSceneManifest_Generation
|
YES | project.yml |
UIApplicationPreferredDefaultSceneSessionRole |
CPSceneSessionRoleImmersiveSpaceApplication |
project.yml |
UIFileSharingEnabled, LSSupportsOpeningDocumentsInPlace
|
true | Info.plist |
The direct build path writes an equivalent dictionary with a slightly different hands-tracking string ("A pinch selects the menu item you are looking at."). It also sets UISceneInitialImmersionStyle = UIImmersionStyleFull explicitly.
PrivacyInfo.xcprivacy declares:
-
NSPrivacyTracking = false, no tracking domains, no collected data types. - One accessed-API category:
NSPrivacyAccessedAPICategoryFileTimestamp, with reasonsC617.1and3B52.1.
The importer and history files read file metadata (size, type), and the watcher relies on file modification dates.
The runtime keeps no network code in this target. Diagnostics stay in the app container until the user shares them (see Diagnostics and Telemetry).
| Failure | Where | What the user sees |
|---|---|---|
Missing or undersized game file, wrong halo.exe hash |
EngineAssetImporter.validate/checkedEntries |
missing / failed phase; loading card "Halo setup needs attention" with the error text |
| Bundled payload incomplete or tampered | manifest check, per-file SHA-256 |
failed; "Retry local setup" reruns prepareBundledGame()
|
| Promotion failure | promoteStaging |
Old install restored; if the rollback also fails, the error names .Game-backup-<UUID>
|
| 1.5 GB stack or QoS rejected | enginevision_start |
FAILED, message says no unsafe fallback was started |
| Invalid back buffer size | metalwin_init |
FAILED |
| Frame buffer allocation failure | metalwin_present |
FAILED, quit requested |
| Engine exits | engine_worker |
STOPPED (status 0) or FAILED with host status; relaunch required |
| Immersive space fails to open | ensureImmersiveOpen |
Setup window status line; window remains |
| Test | What it asserts | How it is run |
|---|---|---|
Tests/AssetValidationMain.swift |
preflight(folder) passes on a real game folder; prints file count, bytes and hash |
Manual CLI (asset-validation GAME_FOLDER) |
Tests/BundledAssetValidationMain.swift |
Preflight across /var vs /private/var aliases and a symlinked .app; install and reuse; rejects ../escape, duplicate paths, wrong total, stale payloadID, a corrupted hash, and a deleted leaf file. Progress is monotonic and ends at totalBytes. |
Manual, against a local .setup payload |
Tests/MutableAssetPreservationMain.swift |
A resized save does not invalidate an install; a missing immutable file does; a repair carries changed and new profile files into staging; a failed promotion leaves the original save intact | Manual |
Tests/RuntimeFrameHarness.m |
start(NULL) gives EINVAL; start reaches STOPPED with exit 0, or FAILED with the "No unsafe fallback" message; a second start gives EALREADY; frame info/copy bounds; digest counts; oversized present ignored |
Manual (links the runtime with a stub host_run) |
Tests/AudioSessionRecoveryValidation.m |
Includes the real audio_session.inc with mock AVAudioSession, clock and DirectSound hooks. Activation retry is bounded to 2 s; rebuild retry after reset; background and interruption gates; a stopped runtime blocks resume. |
tools/run_source_checks.py (macOS) |
Tests/FrameTextureValidation.swift |
A refresh between encoding and executing a GPU read does not mutate the in-flight texture; a duplicate sequence is not reallocated | Manual (Metal) |
Tests/MenuInputValidation.swift |
See Immersive Presenter | run_source_checks.py |
The full list of what tools/run_source_checks.py runs is on Testing and Source Checks.
- Immersive Presenter: how frames reach the headset
- Layer Alignment
- Diagnostics and Telemetry
- Panorama System, Panorama Budget and LOD, Frame Pacing
- EngineHost Overview, Runtime Settings, Threading and Synchronization
- Direct3D9 Bridge, Metal Renderer
- Audio System, Input and Controllers, Haptics
- Build System, Device Preparation and Signing, Setup Wizard
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