Repository navigation
Win32 Compatibility Layer
The translated halo.exe still calls the Windows API exactly as the original did: through its import address table (IAT), through COM vtables, and through GetProcAddress results. The Win32 compatibility layer is the set of native C functions ("shims") in native/EngineHost that answer those calls on macOS and visionOS. Each shim reads its arguments from the guest stack, does the work with POSIX/Darwin facilities against the flat guest address space, and returns exactly as a __stdcall (or __cdecl) Windows function would. This page covers the binding mechanism and every shimmed KERNEL32, USER32, GDI32, ADVAPI32, OLE32/OLEAUT32, WINMM, VERSION, SHELL32, WININET, WinSock, EULA, Keystone, NVCPL, Bink, Vorbis and DirectSound entry point. Direct3D 9, DirectInput 8 and DirectSound COM methods are covered on their own pages.
| File | Role |
|---|---|
host.c |
Import interning and binding (host_proc_address, setup_imports), magic-address dispatch (engine_dispatch_external), module handles, handle table, last-error, host_call_guest, host_exit. |
host.h |
ARG, RET_STDCALL, RET_CDECL, RET64_STDCALL, SHIM macros; handle kinds; guest memory accessors. |
host_path.inc |
Windows path → host path mapping. |
shims_kernel32.c |
All KERNEL32 shims (160 table entries). |
shims_misc.c |
USER32, GDI32, ADVAPI32, OLE32/OLEAUT32 and every other non-KERNEL32 DLL (155 table entries plus the DirectSound COM method entries). |
threading.c |
Back end for events, mutexes, threads and critical sections (Threading and Synchronization). |
resources.c |
PE resource reader for LoadStringA, FindResource*, and dialog templates. |
ddraw.c |
DirectDrawCreateEx, DirectDrawEnumerateExA and IDirectDraw7. |
tests/test_win32_*.c, tests/test_cache_read_wait.c, tests/test_desktop_video_dialog.c
|
Tests that #include the production shim sources. |
tools/export_engine_imports.py emits engine_imports[], one {IAT virtual address, DLL, name} record per static import (ordinal imports are named #n). At startup setup_imports (host.c:526-534) writes into every IAT slot the value returned by host_proc_address(dll, name) and logs N static imports bound, M without a shim yet.
host_proc_address (host.c:450-460) interns (dll, name) into procs[8192] (index 0 is reserved for the "return to host" token) and returns MAGIC_PROC | index, with MAGIC_PROC = 0xFF000000. When a new entry is created it looks up the native function with find_shim, which scans host_shims_kernel32, host_shims_misc, and the lazily built D3D9, DirectInput 8 and DirectDraw tables, comparing DLL names case-insensitively with .dll stripped (host.c:435-449). A missing shim is not an error at bind time: the slot still gets a magic address, so the game fails only if it actually calls it.
Module handles are a second magic range: host_module_handle returns MAGIC_MODULE | (i + 1) (MAGIC_MODULE = 0xFE000000) for the 27 names in known_modules (KERNEL32, USER32, ADVAPI32, GDI32, ole32, OLEAUT32, WSOCK32, WS2_32, BINKW32, VORBISFILE, VERSION, WINMM, WININET, SHELL32, DSOUND, d3d9, dinput8, keystone, strings, shfolder, eula, ddraw, psapi, winhttp, faultrep, NVCPL, mscoree) and 0 for anything else (host.c:432-470).
The generated engine_dispatch treats any target >= 0xFE000000 as external and calls engine_dispatch_external (host.c:495-510), which:
- decodes the index; index 0 longjmps out with "entry point returned";
- increments the per-import call counter and, if import tracing is on, logs the first 8 calls and every power-of-two call (
[import] DLL!Name (call n) args=...); - sets the thread-local
host_current_import(shown by the heartbeat and allocation-failure logs); - if there is no shim, logs
UNIMPLEMENTED import DLL!Name (return address ...)and callsengine_fail, which unwinds the thread; - otherwise calls
fn(cpu).
host.h defines the shim ABI (host.h:39-44):
| Macro | Meaning |
|---|---|
SHIM(name) |
static void shim_name(EngineCPU *cpu) |
ARG(i) |
the i-th 32-bit stack argument, G32(esp + 4 + 4*i) (the return address is at [esp]) |
RET_STDCALL(v, n) |
pop the return address, drop n argument dwords, eax = v, pc = return address
|
RET_CDECL(v) |
pop the return address only (caller cleans), eax = v
|
RET64_STDCALL(v, n) |
as RET_STDCALL with the 64-bit result in edx:eax
|
Every shim must return through one of these (or never return, for example ExitProcess). Getting the argument count wrong unbalances the guest stack, so the tests' fixture call() helpers assert both the restored pc and the exact final esp for every shim they exercise.
Window procedures, enumeration callbacks, APC completion routines and dialog procedures are guest functions. Shims call them with host_call_guest(cpu, fn, nargs, args, callee_pops) (host.c:484-494): it pushes the arguments right to left, pushes the return token MAGIC_PROC|0, dispatches, requires pc == MAGIC_PROC|0 afterward (otherwise engine_fail("callback return mismatch")), pops the arguments for cdecl callees, logs and repairs an unbalanced stack, restores the caller's pc and returns eax.
| API | Behaviour |
|---|---|
GetModuleHandleA |
NULL or any name containing halo (case-insensitive) → the image base; otherwise host_module_handle, error 126 if unknown. |
GetModuleFileNameA |
Always C:\Halo\halo.exe. |
LoadLibraryA |
host_module_handle(name); 0 and error 126 for unknown DLLs. |
FreeLibrary |
Returns TRUE. |
GetProcAddress |
Resolves the module handle (or halo.exe for the image base), formats ordinals as #n, calls host_proc_address, then returns 0 with ERROR_PROC_NOT_FOUND (127) unless host_proc_has_shim says a native function exists (shims_kernel32.c:48-62). The comment explains why: static imports must still bind (and fail loudly if called), but optional dynamic imports must see Windows' normal failure so the game takes its fallback path. |
host_handle_new(kind, object) allocates from a 4096-entry table guarded by handles_lock, starting at index 16, and returns index * 4 (host.c:391-402). Lookups check both the alignment and the expected kind (HANDLE_FILE, HANDLE_EVENT, HANDLE_MUTEX, HANDLE_THREAD, HANDLE_FIND, HANDLE_MAPPING, HANDLE_HEAP, HANDLE_MISC), so a handle of the wrong type behaves as invalid. A full table is fatal (host_exit(3)). Fixed pseudo-handles: standard input/output/error are 0x100/0x104/0x108, GetCurrentProcess is 0xFFFFFFFF, GetCurrentThread is 0xFFFFFFFE, the main window is 0x00010001, the desktop window is 0x10000.
host_set_last_error stores the code in a thread-local and mirrors it to the guest TEB at +0x34 (LastErrorValue) so translated code that reads the TEB directly sees it (host.c:79).
| API | Behaviour (source: shims_kernel32.c) |
|---|---|
GetCommandLineA |
guest_strdup(host_command_line) once ("C:\Halo\halo.exe" -window -novideo plus HALO_CMDLINE_EXTRA). |
GetStartupInfoA |
Zeroed 68-byte STARTUPINFOA with cb = 68, wShowWindow = 1, std handles 0x100/0x104/0x108. |
GetVersionExA |
Windows XP SP3: 5.1 build 2600, platform 2, "Service Pack 3"; for the EX structure, SP 3.0, suite mask 0x100, product type 1. |
GetSystemInfo |
Page size 4096, application address range 0x10000..0x7FFEFFFF, active processor mask 1, one processor, type 586, allocation granularity 65536, level 6, revision 0x0803. |
GetEnvironmentStrings / GetEnvironmentStringsW
|
A one-variable block SystemRoot=C:\WINDOWS. |
FreeEnvironmentStrings{A,W}, SetEnvironmentVariableA, SetStdHandle, SetHandleCount
|
Succeed without effect (SetHandleCount echoes its argument). |
GetStdHandle |
STD_INPUT/OUTPUT/ERROR_HANDLE → 0x100/0x104/0x108, else INVALID_HANDLE_VALUE. |
GetFileType |
2 (char) for std handles, 1 (disk) for file handles, else 0. |
GetCurrentProcess, GetCurrentThread, GetCurrentProcessId, GetCurrentThreadId
|
0xFFFFFFFF, 0xFFFFFFFE, 4660, and host_current_thread_id() (1 for the engine thread, 2+ for guest threads). |
ExitProcess |
Logs and host_exit(code). |
TerminateProcess |
Only the current-process pseudo-handle exits; any other handle fails with error 6 so an invalid target cannot stop Halo (shims_kernel32.c:84-88). |
SetUnhandledExceptionFilter, UnhandledExceptionFilter, SetErrorMode
|
Return 0, 1, 0. |
GetLastError / SetLastError
|
Thread-local, mirrored to TEB +0x34. |
IsProcessorFeaturePresent, IsBadReadPtr, IsBadWritePtr, IsBadCodePtr
|
Return 0 (feature absent / pointer good). |
RaiseException |
Logs the code and caller, then engine_fail. |
RtlUnwind |
engine_fail("RtlUnwind (SEH unwinding is not modeled)"). Structured exception handling is not emulated. |
FormatMessageA |
Writes "Unknown error". |
OutputDebugStringA |
host_log("[dbg] ..."). |
CreateProcessA |
Returns 0 (fails). |
lstrcmpiA |
strcasecmp. |
All guest allocations come from the allocators described on Guest Memory and Heap.
| API | Behaviour |
|---|---|
HeapCreate / HeapDestroy / GetProcessHeap
|
Heap handles are HANDLE_HEAP table entries with no object; all heaps share the single guest heap. GetProcessHeap creates its handle once. |
HeapAlloc / GlobalAlloc / LocalAlloc
|
guest_alloc(size); memory is always zeroed regardless of flags. |
HeapFree / GlobalFree / LocalFree
|
guest_free(ptr); returns TRUE (Heap) or NULL (Global/Local) even for invalid pointers, which guest_free ignores. |
HeapSize |
guest_alloc_size(ptr): the requested size from native metadata (0 for invalid/interior/freed). |
HeapReAlloc / GlobalReAlloc
|
Allocate the new block first, copy min(old size, new size) bytes using the native size, then free the old block. Growth is zero-filled because guest_alloc zeroes. |
GlobalLock / GlobalUnlock
|
Return the pointer itself / TRUE (handles are pointers). |
GlobalMemoryStatus |
Fixed values: 30% load; about 2 GiB total physical, page file and virtual; 1.5 GiB available physical and page file; 1.75 GiB available virtual. |
VirtualAlloc |
Addresses in [0x40000000, 0x7F000000) go to guest_page_reserve_explicit; everything else (including NULL) to guest_page_alloc. Failure sets error 487 (or 87 for size 0). Logged with [mem] trace. |
VirtualFree |
Returns TRUE; does not free (page reclamation is owner-only, see the memory page). |
VirtualProtect |
Returns TRUE and reports old protection PAGE_READWRITE (4). |
VirtualQuery |
Fabricates a committed 4 KiB PAGE_READWRITE private region at the page containing the address. |
InterlockedExchange |
Atomic __atomic_exchange_n on the guest dword. |
TlsAlloc, TlsFree, TlsGetValue and TlsSetValue (shims_kernel32.c:129-176) implement the 64 Windows TLS slots in each guest thread's TEB at +0xE10 + 4*index, the real TlsSlots offset, so translated code that reads fs:[0xE10+...] directly agrees with the API.
- Slot 0 is reserved for the host (
tls_allocatedstarts as1).TlsAllocreturns the lowest free index 1..63 orTLS_OUT_OF_INDEXESwith error 8. - Each slot has a global generation counter that
TlsAllocincrements. Each native thread keeps a thread-local copy of the generation it last saw for every slot; the firstTlsGetValue/TlsSetValueon a slot whose generation changed zeroes that thread's TEB slot. This gives Windows' guarantee that a newly allocated index reads 0 on every thread, without one thread writing another running thread's TEB. - Indices
>= 64fail with error 87;TlsFreeof an unallocated index or index 0 fails with 87.TlsGetValuesets last error 0 on success. - A slot whose generation reached
UINT64_MAXis never handed out again.
tests/test_win32_tls_lifetime.c exhausts all 63 slots, frees and reallocates one while three other threads hold values in it (they must read 0 afterwards and keep thread isolation), validates every error path, and runs four threads through 4000 allocate/use/free cycles each, asserting no slot is ever live twice.
The per-thread static TLS block from the PE TLS directory is separate: setup_thread_teb copies the TLS template into a fresh heap block and stores it at index 0 of a 64-entry array referenced from TEB +0x2C (host.c:535-557).
| API | Behaviour |
|---|---|
GetTickCount |
Milliseconds since the first call, plus 100000 (so it never starts near zero). |
QueryPerformanceCounter / QueryPerformanceFrequency
|
CLOCK_MONOTONIC_RAW nanoseconds / 1,000,000,000. |
GetSystemTimeAsFileTime |
CLOCK_REALTIME as 100 ns units since 1601. |
GetSystemTime / GetLocalTime
|
gettimeofday broken down with gmtime_r/localtime_r into SYSTEMTIME. |
GetTimeZoneInformation |
Zeroed structure, returns 0 (TIME_ZONE_ID_UNKNOWN). |
SystemTimeToFileTime / CompareFileTime
|
timegm conversion; 64-bit compare returning -1/0/1. |
GetDateFormatA / GetTimeFormatA
|
Fixed strings 9/13/2026 and 12:00:00. |
Sleep / SleepEx
|
See Sleep accounting and Cache-read waits. |
GetACP 1252, GetOEMCP 437, GetUserDefaultLCID/GetThreadLocale 0x409, IsValidCodePage/IsValidLocale/SetThreadLocale/EnumSystemLocalesA succeed, GetCPInfo reports single-byte with default char ?. GetLocaleInfoA answers LOCALE_IDEFAULTANSICODEPAGE ("1252"), LOCALE_IDEFAULTCODEPAGE ("437"), English/United States names, ISO en/US and "0409"; GetLocaleInfoW always answers "1252". GetStringTypeA/W classify with the C library (CT_CTYPE1 bits; any wide char above 255 is C1_ALPHA). LCMapStringA/W lower/upper-case Latin-1 (LCMAP_LOWERCASE 0x100, LCMAP_UPPERCASE 0x200). MultiByteToWideChar/WideCharToMultiByte are a one-to-one Latin-1 mapping (? for characters above 255), with the size-query and ERROR_INSUFFICIENT_BUFFER (122) behaviours. CompareStringA/W return CSTR_LESS_THAN/EQUAL/GREATER_THAN (1/2/3) honouring NORM_IGNORECASE; the A version copies at most 511 bytes of each string.
| API | Behaviour |
|---|---|
CreateFileA |
Maps the path with host_path, translates access (GENERIC_WRITE → O_WRONLY, plus GENERIC_READ → O_RDWR, else O_RDONLY) and disposition (CREATE_NEW → `O_CREAT |
CreateFileW |
Narrows the name to 8-bit, guest_strdups it, rewrites argument 0 and calls the A version. |
ReadFile |
Synchronous read, or pread at the OVERLAPPED offset (then sets Internal = 0, InternalHigh = bytes and moves the file position past the read). |
ReadFileEx |
Asynchronous read with completion routine; see Async I/O and APCs. |
WriteFile |
Std output/error handles go to stdout/stderr; files use write or pwrite at the OVERLAPPED offset. |
SetFilePointer |
lseek with optional high dword; error 131 on failure. |
GetFileSize |
fstat. |
SetEndOfFile |
ftruncate at the current position, retried on EINTR; error 131 if the position cannot be read, 112 for ENOSPC/EDQUOT, else 5. |
FlushFileBuffers |
fsync, retried on EINTR; 112 or 5 on failure. |
GetFileTime / SetFileTime
|
st_mtime for all three times / no-op success. |
CloseHandle |
Closes the fd and frees the FileObj for file handles; for every handle kind it releases the table slot. Event, mutex, thread and mapping objects are not freed. |
GetFileAttributesA / GetFileAttributesExA
|
stat: FILE_ATTRIBUTE_DIRECTORY (0x10) or FILE_ATTRIBUTE_ARCHIVE (0x20), size and times; error 2 when missing. HALO_FILELOG logs lookups of a10 paths. |
SetFileAttributesA |
No-op success. |
FindFirstFileA / FindNextFileA / FindClose
|
glob(3) over the mapped pattern; see FindFirstFile and profiles. |
CreateDirectoryA / RemoveDirectoryA / DeleteFileA
|
mkdir 0755 (error 183 if it exists) / rmdir / unlink. |
CopyFileA |
Buffered copy in 64 KiB blocks (the fail-if-exists flag is ignored). |
GetCurrentDirectoryA / GetFullPathNameA / GetTempPathA
|
C:\Halo; relative names are prefixed with C:\Halo\; C:\Temp\. |
GetDiskFreeSpaceExA |
100 GiB available, 500 GiB total, 100 GiB free. |
CreateFileMappingA / MapViewOfFile / UnmapViewOfFile
|
The mapping object shares the file's fd. MapViewOfFile allocates guest pages and preads the requested range into them, so a view is a private snapshot: later writes to the file are not visible and writes to the view are not written back. UnmapViewOfFile returns TRUE and does not free the pages. |
FindResourceA and FindResourceExA accept only integer type and name IDs; they read the resource from halo.exe (for the image base or NULL) or strings.dll (for the strings module handle) on disk through host_find_resource (resources.c:58-66), copy it into a heap block and return a HANDLE_MISC resource object; LoadResource returns that guest pointer, LockResource returns its argument, SizeofResource the clamped size. FindResourceW returns 0. The resource directory walk prefers language 0x409 and otherwise takes the first language. Console APIs (GetConsoleScreenBufferInfo, cursor functions, ReadConsoleInputA, FillConsoleOutput*, ...) fail with 0, except WriteConsoleA, which writes to stdout.
These are thin wrappers over threading.c; behaviour is on Threading and Synchronization.
| API | Maps to |
|---|---|
CreateEventA |
host_event_create(manual_reset, initial_state) (security attributes and name ignored) |
SetEvent |
host_event_set; error 6 for a bad handle |
CreateMutexA / ReleaseMutex
|
host_mutex_create(initial_owner) / host_mutex_release; error 288 (ERROR_NOT_OWNER) when not owned |
WaitForSingleObject |
host_wait_single(handle, ms); error 6 on WAIT_FAILED
|
WaitForSingleObjectEx |
If alertable and APCs are queued on this thread, runs them and returns WAIT_IO_COMPLETION (0xC0); otherwise as above |
InitializeCriticalSection, DeleteCriticalSection, EnterCriticalSection, LeaveCriticalSection
|
host_critical_section_* keyed by guest address |
CreateThread, ResumeThread, TerminateThread, ExitThread, GetExitCodeThread
|
host_thread_*; CreateThread writes the thread id if requested and sets error 8 on failure |
DuplicateHandle |
Copies the source handle value to the target (no new object) |
SetThreadPriority, GetThreadPriority, SetPriorityClass, GetPriorityClass
|
Succeed / 0 / succeed / NORMAL_PRIORITY_CLASS (0x20). Scheduling is controlled by QoS instead. |
There are no shims for CreateSemaphore*, ResetEvent, PulseEvent or WaitForMultipleObjects; the executable does not import them (an attempt to call an unbound import would stop with UNIMPLEMENTED import).
host_path(windows_path, buffer, size) in host_path.inc converts every guest path to a host path under host_game_root (the directory holding halo.exe):
- Strip a drive prefix (
X:) and any leading slashes or backslashes. - Start the output with
host_game_root. - For each component split on
\or/: unless it contains*or?, open the directory built so far and look for an entry whose name matches case-insensitively, preferring an exact-case match. Append the real spelling if found, otherwise the component as given (so new files and directories can be created). - Return an empty string if any component is 1024 bytes or longer or the result would not fit.
The reason is in the header comment: Windows names are case-insensitive but visionOS app storage is case-sensitive, and Halo is inconsistent about case (MAPS\BITMAPS.MAP versus maps\bitmaps.map). Wildcards are preserved for FindFirstFileA. The mapping does not special-case C:\Halo: an absolute C:\Halo\x would map to <root>/Halo/x. The folder APIs hand out other absolute paths (SHGetFolderPathA returns C:\Users\Player\Documents for CSIDL_PERSONAL), which therefore live inside the game root; the app's importer treats Users/Player/Documents/My Games/Halo under the game root as the player's mutable profile and save tree (EngineAssets.swift:116-119).
Each lookup performs one opendir/readdir scan per existing component, so path-heavy code pays directory scans; Halo's hot I/O is through already-open map files, so this cost is at open time only.
FindFirstFileA (shims_kernel32.c:538-548) maps the pattern with host_path and runs glob(3). One rewrite is applied: if the final component is exactly *.*, it becomes *. Win32's *.* matches names without an extension (MS-FSA 2.1.4.4), but POSIX glob requires a literal dot, which hid Halo's extensionless profile directories (New001 and similar) and "made each restart look like the first launch". The first match is written to WIN32_FIND_DATAA (attributes, the three times from st_mtime, the 64-bit size, and the leaf name at +44); the glob_t and the next index live in a HANDLE_FIND object. FindNextFileA returns subsequent matches and fails with ERROR_NO_MORE_FILES (18) at the end. No matches → INVALID_HANDLE_VALUE with error 2. HALO_FINDLOG logs every pattern and match count.
tests/test_win32_find_profiles.c creates New001/, Second.Profile/, blam.sav and readme in a temporary directory and checks *.* and * both find all four, *.sav finds one file, an exact name finds the directory, a non-matching pattern returns error 2, attributes and sizes are right, and FindClose releases the handle. (This test is not in the tools/run_source_checks.py case list.)
Halo's cache-file reader thread (00443940) reads map data with ReadFileEx and receives completions in alertable waits. The host implements this with a per-thread APC queue (shims_kernel32.c:446-492):
sequenceDiagram
participant R as Reader thread (guest 00443940)
participant K as ReadFileEx shim
participant Q as thread-local APC ring (64)
participant W as SleepEx / WaitForSingleObjectEx (alertable)
participant E as Engine thread Sleep(0) at cache wait site
R->>K: ReadFileEx(file, buf, n, overlapped, routine)
K->>K: reject if ring full (error 8), before any I/O
K->>K: pread at OVERLAPPED offset (retry EINTR)
K->>K: Internal=0, InternalHigh=bytes, count bytes by file kind
K->>Q: enqueue {routine, overlapped, bytes}
K-->>R: TRUE
R->>W: alertable wait
W->>Q: deliver_apcs: pop oldest, host_call_guest(routine, 0, bytes, overlapped)
W->>E: io_signal_completion() (condvar broadcast)
W-->>R: WAIT_IO_COMPLETION (0xC0)
Rules enforced by the code and tests/test_win32_apc_queue.c:
- The read itself is synchronous (
pread); only the completion is deferred, to the next alertable wait on the issuing thread, preserving Windows' delivery semantics. - The ring holds 64 completions. A 65th request fails with
ERROR_NOT_ENOUGH_MEMORY(8) beforepreadruns and before either guest output (buffer orOVERLAPPED) is written, so a rejected call has no side effects and is not counted. -
deliver_apcsremoves only the callback it is about to invoke. A completion routine may itself issue a read and enter another alertable wait; that nested dispatch sees older queued completions before the new one (FIFO across nesting). The test repeats a re-entrant scenario 200 times. - Queues are thread-local: completions queued by one thread are never delivered on another.
- Errors: missing
OVERLAPPEDor routine → 87; bad handle → 6;preadfailure → 87 forEINVAL, else 5; a nonzero read at end of file →ERROR_HANDLE_EOF(38) with nothing queued; a zero-byte request is accepted and completes with 0 bytes. Partial reads complete with the bytes actually read. - The completion arguments are
(dwErrorCode = 0, bytes, lpOverlapped)and the routine is called as__stdcall. - Reads are counted by file for the device report:
host_async_reads[]andhost_async_read_bytes[]are indexed byHOST_READ_LEVEL_MAP(any*.mapother than the two shared ones),HOST_READ_BITMAPS_MAP,HOST_READ_SOUNDS_MAPandHOST_READ_OTHER, classified case-insensitively by leaf name when the file is opened (file_read_kind, shims_kernel32.c:423-429).host_cache_readsandhost_cache_read_nstime everyReadFileExpread.
SleepEx(ms, TRUE) and WaitForSingleObjectEx(h, ms, TRUE) deliver queued APCs first and return WAIT_IO_COMPLETION if any ran, without sleeping or waiting.
Sleep is the engine's frame limiter, and its timing feeds the panorama bearing budget, so the shim both changes behaviour and measures (shims_kernel32.c:194-365).
Sleep(0) no longer yields. The comment explains: on the headset the engine thread's core is wanted by a 90 Hz compositor, and every sched_yield handed it over, "a fixed cost per frame that no reduction in the engine's own work can touch". So Sleep(0) returns immediately, except that a thread-local burst counter gives the core up with usleep(250) once every 256 calls, so that Halo's limiter spin ("fifty thousand times a frame on the desktop at 30 fps") does not pin a core. The burst counter carries across frames and gaps.
Counters (all cumulative, read by host_draw_profile_snapshot and the diagnostics bridge):
| Counter | Meaning |
|---|---|
host_sleep_calls, host_sleep_ns
|
Sleep(n>0) and SleepEx(n>0, not INFINITE) calls and their measured CLOCK_UPTIME_RAW duration (oversleep included). |
host_yield_calls |
Sleep(0) calls. |
host_yield_spin_ns |
The presenting thread's idle: gaps shorter than 2 ms between consecutive Sleep(0) calls, plus the presenting thread's Sleep(n) durations, the 250 µs give-ups, cache waits, and frame-pacer waits. |
host_present_sleep_ns |
The subset of that idle spent blocked (sleeps, give-ups, cache waits, pacer waits), so the report can separate on-core spin from off-core time. |
Only the presenting thread counts toward frame idle. d3d9.c records host_present_thread = pthread_self() at every Present (d3d9.c:525); yields on other guest threads are counted in host_yield_calls but never in host_yield_spin_ns, because "the bearing budget reads spare time that is not there and over-commits" otherwise. SleepEx is timed but was never presenter idle.
host_frame_idle_wait(start, end) is called only by the frame pacer around its own blocking wait (shims_kernel32.c:274-284): the wait counts once as presenter idle and resets last_yield_ns, so the next Sleep(0) does not also count the gap across it. With pacing off nobody calls it and accounting is unchanged.
tests/test_win32_sleep_accounting.c replaces clock_gettime_nsec_np and usleep with a deterministic fake clock and asserts the exact totals of the "Build78" shim, including its documented quirks: a short Sleep(n) between close Sleep(0) calls is counted by both, SleepEx is not presenter idle, the burst counter crosses gaps, a 300-frame play pattern totals exactly 3258900000 ns of spin, pacer waits count once, empty or reversed pacer intervals count nothing, and a non-presenting thread contributes nothing.
Halo's blocking cache reads (00443E10 and 00444550) wait for the reader thread by testing their request's done flag and calling Sleep(0) until it is set; those calls return to 00443EEA and 004446D8. The flag is set only by the completion routine 00443AE0, which the reader runs from its alertable waits (shims_kernel32.c:206-259).
When a Sleep(0)'s return address is one of those two sites and HALO_IO_WAIT is not 0, the shim blocks on a condition variable until any APC completion has been delivered since this thread last returned from such a wait, or until 1 ms (cache_wait_limit_ns) has passed. deliver_apcs calls io_signal_completion() after each routine. Properties called out in the comment and tested in tests/test_cache_read_wait.c:
- Halo's own loop is unchanged: it still tests its flag, reads the clock and runs its 132 ms housekeeping after every return.
- No completion is slept through: a completion delivered before the call sends the thread straight back (per-thread
io_completions_seen). - Progress never depends on the signal: the wait is bounded.
-
io_lockis held only around the counter, never while taking another lock; cancellation is disabled across the wait. - Other
Sleep(0)sites are untouched and not counted. - The time is counted as presenter idle exactly as the spin it replaces was, so the bearing budget sees the same frame.
- The test measures a simulated 30 ms read: the blocking mode makes at least ten times fewer
Sleep(0)calls than the spin (HALO_IO_WAIT=0).
Counters host_cache_wait_calls and host_cache_wait_ns (gaps between one wait's calls under 2 ms, plus any block) are measured identically in both modes so the two can be compared.
The host has no real windows. One guest window token, host_main_hwnd = 0x00010001, stands for whichever window Halo creates; the desktop is 0x10000. Window procedures are guest functions called through host_call_guest (shims_misc.c:59-64).
| API | Behaviour |
|---|---|
RegisterClassExA |
Records up to 16 classes {name, wndproc, atom}; atoms start at 0xC001. Rejects a null class, cbSize < 48, or a missing wndproc/name. |
UnregisterClassA |
Removes by atom or name. |
CreateWindowExA |
If a previous window was active, sends it WM_KILLFOCUS and WM_ACTIVATEAPP(0) first (Halo creates a temporary detection window before its game window, and the token is reused). Resets the message queue, selects the class wndproc, records the client size if it is in (0, 8192), then dispatches WM_CREATE synchronously and returns host_main_hwnd (shims_misc.c:100-118). |
ShowWindow |
Showing sends WM_SHOWWINDOW(1) and, on first activation, WM_ACTIVATEAPP(1), WM_ACTIVATE(WA_ACTIVE), WM_SETFOCUS. Hiding an active window sends WM_KILLFOCUS, WM_ACTIVATE(WA_INACTIVE), WM_ACTIVATEAPP(0), WM_SHOWWINDOW(0). Returns the previous visibility. |
DestroyWindow |
Deactivation messages if active, then WM_DESTROY. |
GetClientRect / GetWindowRect
|
The recorded client size for the game window; 4096 x 3072 for the desktop window. |
GetWindowPlacement |
Normal show state with the client size. |
SetWindowPos, MoveWindow, AdjustWindowRect, SetWindowTextA, InvalidateRect, ValidateRect, SetForegroundWindow
|
Succeed without effect. |
GetActiveWindow, GetForegroundWindow, SetActiveWindow, SetFocus
|
host_main_hwnd. |
GetParent, FindWindowA, EnableWindow, GetWindowLongA, SetWindowLongA
|
0. |
GetSystemMetrics |
SM_CXSCREEN 4096, SM_CYSCREEN 3072, SM_CMONITORS (80) 1, else 0. |
The virtual desktop is 4096 x 3072 on purpose (shims_misc.c:12-17): Halo only accepts a video mode strictly smaller than the desktop and falls back to 800x600 otherwise, and the largest mode in the D3D9 adapter table is 2560x1920. Window dimensions never replace desktop dimensions.
Halo's text editor and dialogs consume ordinary key messages separately from DirectInput. The host keeps a thread-local queue of up to 256 MSG records (shims_misc.c:25-56); when full, the oldest is dropped with a log line.
-
PeekMessageAfirst callspump_keyboard_messages(), which samples the merged keyboard state (host_dinput_keyboard_state, physical keys plus the menu-only controller keys) and queuesWM_KEYDOWN/WM_KEYUPfor Escape (DIK0x01, VK0x1B) and Return (DIK0x1C, VK0x0D) on every edge, with Windows-stylelParam(repeat 1, scan code, transition bits). It then returns the first message matching the window/range filter, removing it ifPM_REMOVEis set. Queuing on the engine's UI thread before its message pump means a press cannot arrive one frame late and accept a newly opened dialog. Arrow keys are deliberately not posted because DirectInput already navigates and duplicate arrow messages caused double steps in submenus. -
TranslateMessagequeuesWM_CHAR/WM_SYSCHARfor Return and Escape key-downs only. -
DispatchMessageAandSendMessageAcall the main wndproc;CallWindowProcAcalls the given procedure;DefWindowProcAreturns 0. -
PostQuitMessagesetshost_quit_requested. -
MsgWaitForMultipleObjectsreturns 0 immediately.
DialogBoxParamA (shims_misc.c:232-249) never shows UI. host_dialog_choose (resources.c:74-98) parses the DLGTEMPLATEEX from halo.exe or strings.dll, logs every text, and picks a push button: the button whose text starts with Continue, &Continue or OK, else the first push button, else 1. For template 102 (the adapter warning) it always picks "Continue Anyway". The shim then runs only the dialog procedure's WM_COMMAND/BN_CLICKED handler for that button; if the procedure calls EndDialog, its result is returned, otherwise the control id. The comment records the bug this fixes: returning the control id (1004) was read as nonzero, which "silently put the whole renderer into Safe Mode (fixed-function, no pixel shaders, lowest settings)". IsDlgButtonChecked returns 0, GetDlgItem 0, SetDlgItemTextA succeeds, DialogBoxIndirectParamA returns -1 and CreateDialogIndirectParamA 0.
LoadStringA reads RT_STRING block id/16 + 1 from the PE on disk and returns the id % 16-th counted UTF-16 string, narrowed to ASCII (? above 127) (resources.c:48-57). MessageBoxA logs the caption and text and returns IDOK. wsprintfA is a cdecl formatter that reads varargs from the guest stack and supports %s %c %d %i and other integer conversions with flags/width (output capped near 1000 characters).
GetCursorPos reports the client center; ShowCursor, SetCursor, SetCapture, GetCapture, GetAsyncKeyState, GetKeyState return 0; ReleaseCapture, ClientToScreen succeed; LoadCursorA/LoadIconA return fixed tokens; GetDoubleClickTime 500; PtInRect is implemented. Clipboard functions fail; SetPropA succeeds, GetPropA/RemovePropA return 0. Real input goes through DirectInput, see Input and Controllers.
GetDeviceCaps answers HORZRES 4096, VERTRES 3072, BITSPIXEL 32, PLANES 1, VREFRESH 60, LOGPIXELSX/Y 96. GetDeviceGammaRamp returns an identity ramp; SetDeviceGammaRamp, DeleteObject, StretchBlt succeed; GetDC, CreateCompatibleDC, CreateFontIndirectA return fixed tokens; SelectObject, SetTextColor, GetObjectA return 0.
Halo reads its installation and registration data from the registry. The host keeps an in-memory table of up to 256 values seeded from <game root>/halo-vision-registry.txt (shims_misc.c:273-336).
-
File format: one value per line, five
|-separated fields:root|key|name|type|data, where root isHKCU,HKLMorHKCR, type is the WindowsREG_*number, and data is a decimal integer forREG_DWORD(4), lowercase hex bytes forREG_BINARY(3), or text for strings (stored with a terminating NUL). Lines with fewer than five fields are skipped. Values are capped at 512 bytes, keys at 256 characters, names at 64. A template listing the expected Halo value names (without values) istools/halo-vision-registry.template.txt;tools/setup_halo.pygenerates the real file from the user's own registry export. It contains registration data, is private, and is excluded from the repository bytools/check_repository_hygiene.py. See Setup Wizard. -
Loading is lazy (first registry call) and logs
registry seed loaded: N valuesorno registry seed at .... -
Keys exist if any stored value's key equals the requested key or starts with it followed by
\(case-insensitive).RegOpenKeyExA/RegOpenKeyAreturnERROR_FILE_NOT_FOUND(2) for missing keys;RegCreateKeyExAalways succeeds and reportsREG_OPENED_EXISTING_KEYorREG_CREATED_NEW_KEY. Opened keys areHANDLE_MISCobjects holding{root, full key path}; subkeys of opened keys are joined with\. -
RegQueryValueExAmatches root, key and name case-insensitively, reports type and size, and returnsERROR_MORE_DATA(234) if the buffer is too small. -
RegSetValueExAupdates or appends (error 8 when 256 values exist) and immediately rewrites the whole file (reg_save), so the game's own registry writes persist across launches. -
RegCloseKeyfrees the key object.
CryptAcquireContextA returns a fixed provider token. CryptCreateHash hands out one of 15 hash slots; CryptHashData appends up to 64 KiB; CryptGetHashParam(HP_HASHVAL) computes SHA-1 (algorithm 0x8004) or otherwise MD5 with CommonCrypto, and also answers HP_HASHSIZE and HP_ALGID. Token, SID, ACL and security-descriptor functions (OpenProcessToken, OpenThreadToken, DuplicateToken, AllocateAndInitializeSid, FreeSid, GetLengthSid, InitializeAcl, AddAccessAllowedAce, InitializeSecurityDescriptor, SetSecurityDescriptorDacl/Group/Owner, IsValidSecurityDescriptor, AccessCheck) all return 0 (failure).
CoInitialize/CoUninitialize succeed; CoCreateInstance logs and returns REGDB_E_CLASSNOTREG (0x80040154); CLSIDFromString returns 0; StringFromGUID2 writes the all-zero GUID; VariantInit/VariantClear zero 16 bytes.
| DLL | Exports | Behaviour |
|---|---|---|
| WINMM |
timeBeginPeriod, timeEndPeriod
|
Return 0. |
| VERSION |
GetFileVersionInfoSizeA, GetFileVersionInfoA, VerQueryValueA
|
Return 0 (no version resource). |
| SHELL32 | ShellExecuteA |
Logs and returns 42 (success value > 32) without launching anything. |
| WININET | InternetQueryOptionA |
Returns 0. |
| WSOCK32 / WS2_32 | ordinals #115 WSAStartup, #116 WSACleanup, #111 WSAGetLastError, #23 socket, #57 gethostname, #52 gethostbyname, #3 closesocket |
WSAStartup fills a WinSock 2.2 WSADATA; socket returns INVALID_SOCKET; WSAGetLastError returns 10047 (WSAEAFNOSUPPORT); gethostname is halo; gethostbyname returns NULL. Networking is therefore unavailable. |
| shfolder | SHGetFolderPathA |
CSIDL_PERSONAL (5) → C:\Users\Player\Documents, CSIDL_APPDATA (0x1A) → C:\Users\Player\AppData\Roaming, CSIDL_PROGRAM_FILES (0x26) → C:\Program Files, else C:\Users\Player. |
| eula | EBUEula |
Logs and returns 1 (accepted). |
| keystone | KeystoneCreate |
cdecl, returns NULL. |
| d3d9 |
DisableD3DSpy, DebugSetMute
|
Return 0 (DebugSetMute is cdecl: the comment cites the caller cleaning at 005841BC). |
| NVCPL |
NvCplGetDataInt (cdecl, writes 0, returns 0), NvCplSetDataInt
|
Report the NVIDIA control panel as unavailable. |
| BINKW32 |
_BinkOpen@8, _BinkSetSoundSystem@8, _BinkOpenDirectSound@4
|
BinkOpen logs and returns NULL, so videos are skipped (the command line also passes -novideo). |
| VORBISFILE |
ov_open_callbacks, ov_clear, ov_read, ov_crosslap
|
Forwarded to vorbis_shim.c (stb_vorbis). See Audio System. |
| DSOUND |
DirectSoundCreate8 (#11), DirectSoundCreate (#1), DirectSoundEnumerateA (#2), DirectSoundCaptureEnumerateA (#9), GetDeviceID (#14), plus every IDirectSound8, IDirectSoundBuffer8, IDirectSound3DListener8 and IDirectSound3DBuffer8 method via HOST_DSOUND_SHIM_ENTRIES
|
Enumeration reports one "Primary Sound Driver" playback device and no capture devices; GetDeviceID returns 0x88780078. See Audio System. |
| ddraw |
DirectDrawCreateEx, DirectDrawEnumerateExA, 30 IDirectDraw7::* methods |
See below. |
ddraw.c exists only so the game's display probing succeeds. DirectDrawEnumerateExA reports one "Primary Display Driver". DirectDrawCreateEx builds a 30-entry guest vtable of IDirectDraw7::* magic addresses once and returns a single shared object. Implemented methods: AddRef/Release (reference count), EnumDisplayModes (640x480, 800x600, 1024x768, 1280x720, 1280x960, 1600x900 and 1920x1080, each at 16 and 32 bpp and 60 Hz; stops when the callback returns 0), GetCaps (256 MiB video memory), GetDisplayMode (1920x1080x32 at 60 Hz), GetMonitorFrequency (60), GetVerticalBlankStatus (TRUE), GetAvailableVidMem (256 MiB total, 200 MiB free), GetDeviceIdentifier (halo-vision-null / "Halo Vision Null Display", vendor 0x10DE), and CreateSurface, which fails with DDERR_UNSUPPORTED. Every other method returns DD_OK with the correct stack cleanup from the method table.
| Situation | Result |
|---|---|
| Guest calls an import with no shim |
UNIMPLEMENTED import DLL!Name logged, engine_fail, thread unwinds (engine thread: process ends with status 1). |
GetProcAddress for an unshimmed export |
Returns NULL with error 127; the game can take its fallback. |
| Handle table (4096) or proc table (8192) full |
host_exit(3). |
RaiseException, RtlUnwind
|
engine_fail; SEH is not modeled. |
| Guest callback returns elsewhere |
engine_fail("callback return mismatch"); an unbalanced callback stack is logged and repaired. |
| Test | Covers |
|---|---|
test_win32_apc_queue.c |
ReadFileEx + alertable WaitForSingleObjectEx: 64 accepted completions, saturation rejected before I/O or guest writes, FIFO nested dispatch, per-thread queues, 200 re-entrant cycles, partial/zero/EOF/error reads, read counting by file kind, TerminateProcess handle validation. |
test_cache_read_wait.c |
Cache-wait Sleep(0) only at the two return sites, bounded without a completion, woken by a completion, none slept through, presenter idle accounting, blocked vs HALO_IO_WAIT=0 spin call counts. |
test_win32_sleep_accounting.c |
Exact Sleep/SleepEx/Sleep(0) accounting with a fake clock, burst give-ups, pacer waits, presenter-only idle. |
test_win32_tls_lifetime.c |
TLS exhaustion, reuse, cross-thread zeroing, isolation, error paths, 16000 concurrent lifetimes. |
test_win32_heap_reuse.c |
HeapAlloc/Size/ReAlloc/Free, GlobalAlloc/Lock/ReAlloc/Free, LocalAlloc/Free against the production allocator; sizes from native metadata, growth zeroed, >2 GiB realloc turnover. |
test_win32_save_flush.c |
SetEndOfFile truncates at the current position, FlushFileBuffers uses fsync, bytes persist after reopen, read-only/failing/invalid descriptors report 5, 131 or 6. |
test_win32_find_profiles.c |
*.* finds extensionless profile directories; filters, metadata, end-of-enumeration error and handle cleanup. |
test_desktop_video_dialog.c |
Includes shims_misc.c and resources.c; checks the desktop window is distinct from the client window and that template 102 picks "Continue Anyway" (1004). It takes a game directory argument (for strings.dll) and is not in the run_source_checks.py list. Its desktop assertions expect 1920x1080, while the current shims_misc.c reports a 4096x3072 desktop, so as written the test does not match the current code. |
- EngineHost Overview
- Threading and Synchronization
- Guest Memory and Heap
- Engine Overrides and Hooks
- Direct3D9 Bridge, Audio System, Input and Controllers
- Panorama Budget and LOD, Frame Pacing (consumers of the Sleep accounting)
- Setup Wizard (registry seed), Diagnostics and Telemetry
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