Repository navigation
Direct3D9 Bridge
The Direct3D 9 bridge is the host-side replacement for d3d9.dll (and a tiny ddraw.dll). The statically translated Halo PC executable still calls Direct3DCreate9 and then drives a COM device through vtables exactly as it did on Windows; the bridge allocates those COM objects in guest memory, points every vtable slot at a native shim, tracks all device state on the host, and turns each draw into a call on the Metal Renderer. It sits between the translated engine (see EngineReuse Runtime and Win32 Compatibility Layer) and Metal; the Panorama System hooks into the same draw path to redirect world and HUD draws into per-bearing targets.
| File | Role |
|---|---|
d3d9.c |
COM object table, vtable/shim generation, IDirect3D9, IDirect3DDevice9 and resource interfaces, lighting, Reset, Present, shim registration. |
d3d9_render.inc |
Device draw state, state-block recording, render-target surfaces, texture upload, the three draw backends (programmable, fixed-function 3D, pretransformed RHW), index normalisation, vertex-buffer residency, diagnostics. Included by d3d9.c. |
d3d9_capture.inc |
Opt-in render trace and draw-payload capture (diagnostics only). |
d3d9_process_vertices.inc |
IDirect3DDevice9::ProcessVertices front end (see Geometry Fast Paths). |
fp_vertex_capture.inc |
One-frame first-person (viewmodel) draw capture, diagnostics only. |
model_vertex_capture.inc, model_capture_boundary.h, model_capture_scope.h, model_capture_hooks.inc
|
One-frame actor (biped) draw capture keyed on the original model-draw functions, diagnostics only. |
stateblock.h |
Generic masked state-block payload (HostStateBlock). |
rhw_declaration.h |
Recognises Halo's two POSITIONT vertex declarations as RHW FVF layouts. |
ddraw.c |
Minimal IDirectDraw7 for the game's display-mode probing. |
metalwin.h / metalwin.m
|
macOS window presenter (CPU framebuffer upload to a CAMetalLayer). On visionOS the same C API is provided by the app bridge. |
resources.c |
PE RT_STRING/dialog resource reader for halo.exe/strings.dll. It has no texture-related code; it is listed here only because texture resources are not read from PE files at all. |
The header comment at the top of d3d9.c (lines 1-3) still says "programmable shaders remain pending"; that comment predates the programmable path and is stale. Every method is accounted for, so stdcall stacks stay balanced; methods without an implementation log once and return D3D_OK.
Each D3D object is a 16-byte guest allocation created by obj_new:
| Guest offset | Contents |
|---|---|
| +0 | Pointer to the class vtable (guest memory) |
| +4 | Host slot index into objs[65536]
|
| +8 | Generation number (non-zero, incremented per allocation) |
| +12 | Unused |
vtable_for builds one guest vtable per class lazily; each slot holds host_proc_address("d3d9.dll", "IFace::Method"), i.e. the magic address the translated code calls to reach a host shim. The shim table is generated by the METHOD_SHIM(class, index, handler) macro (line 969): one native function per (class, vtable slot). A shim recovers this from ARG(0), validates it with obj_from_guest (slot in range, objs[slot].guest == this, guest generation word matches), and calls the class handler with the method index. A bad this is fatal (engine_fail("d3d9 bad this")). Every handler returns through RET_STDCALL(hr, argc) where argc (including this) comes from the per-class method tables at lines 73-87, so the callee pops exactly the stdcall argument bytes.
host_shims_d3d9_build registers Direct3DCreate9 plus every IFace::Method name with the shim layer (up to 512 entries). Classes that share a method layout share shim arrays: textures/cube/volume textures share tex_fns, VB/IB share buf_fns, and vertex declarations, shaders, state blocks, swap chains and queries share simple_fns.
Binding validation uses obj_find, a 16384-bucket hash from guest address to slot, so a caller-supplied pointer is never dereferenced to discover its class.
D3DObj is one record per object kind (K_D3D, K_DEVICE, K_TEXTURE, K_CUBETEX, K_VOLTEX, K_VB, K_IB, K_SURFACE, K_VOLUME, K_VDECL, K_VSHADER, K_PSHADER, K_STATEBLOCK, K_SWAPCHAIN, K_QUERY). The fields that matter beyond the obvious description fields:
| Field | Meaning |
|---|---|
data, data_capacity
|
Guest buffer for VB/IB/surface bytes; implicit back/depth buffers keep a reusable capacity. |
level_data[16], level_surface[16], face_data[6][16], face_surface[6][16]
|
Per-mip (and per-cube-face) guest pixel storage and the cached IDirect3DSurface9/IDirect3DVolume9 alias for each level. |
owner |
For a level alias, the parent texture's guest address. |
renderer, gpu_dirty
|
For render-target surfaces, the mr_context and whether the GPU copy is newer than the guest bytes. |
content_generation, hashed_generation, content_key
|
Texture/buffer change tracking and the cached upload key (see Textures and Texture Packs). For shaders content_key is the token hash. |
mod_generation, mod_entry, mod_checked
|
Cached texture-pack match for the current generation. |
stateblock, stateblock_device
|
Custom state-block payload and the device it retains. |
resident, resident_generation, resident_copies, resident_refused
|
Vertex-buffer residency (see Geometry Fast Paths). |
radial_fog_terms, radial_fog_view_plane
|
Vertex shaders: fog terms the Radial Fog rewrite would change, computed once at creation. |
The table holds at most 65536 objects; exhausting it logs d3d object table full and exits with code 3. Freed slots go on a free list; the generation word makes a stale guest pointer to a reused slot fail validation.
obj_release and obj_retain implement the rules:
- Only buffers, declarations, shaders, state blocks, textures, surfaces and volumes are reclaimable (
obj_reclaimable). Device,IDirect3D9, swap chain and query objects never reach zero;Releasefloors at 1. - A texture level alias is created with zero references and is owned by its parent. While the game holds a reference to a level, the level holds one reference on the parent; when that last external reference is dropped the parent reference is released instead of destroying the level, so GPU-authoritative contents survive until the parent dies.
-
obj_destroydestroys child surfaces (and their Metal render targets) before freeing shared pages, drops any resident vertex copy, frees guest storage, zeroes the guest object, and releases the creating device (resource_device) and a state block's device. - Every resource created through the device calls
obj_attach_device, soGetDevicereturns a retained device and the device outlives its resources. - All binding slots (
SetTexture,SetStreamSource,SetIndices,SetVertexDeclaration, shaders, render target, depth surface) hold strong references throughbinding_replace, which retains the new owner before releasing the old one.
Handled by method_d3d.
| Method | Behaviour |
|---|---|
GetAdapterCount |
1 |
GetAdapterIdentifier |
Driver halo-vision-metal, description Halo Vision Metal Renderer, device \\.\DISPLAY1, driver version 0x0009000F00000000, vendor 0x10DE, device id 1. |
GetAdapterModeCount / EnumAdapterModes
|
Ten modes from 640x480 to 2560x1920 at 60 Hz, X8R8G8B8. |
GetAdapterDisplayMode |
1920x1080. |
CheckDeviceType, CheckDeviceFormat, CheckDeviceMultiSampleType, CheckDepthStencilMatch, CheckDeviceFormatConversion
|
Always D3D_OK (CheckDeviceFormat logs its arguments). |
GetDeviceCaps |
fill_caps: a fixed 304-byte D3DCAPS9 (8 texture stages, 8 simultaneous textures, 4096 max texture size, VS/PS version fields 0xFFFE0200/0xFFFF0200, 256 VS constants, etc.). |
CreateDevice |
Creates the device, the implicit back buffer (X8R8G8B8, usage 1) stored in device->data and the implicit depth surface (format from the present parameters or 75 = D3DFMT_D24S8) in device->level_surface[0]; clamps the back buffer to at least 64x64 (default 640x480); resets render state and lighting; calls metalwin_init once. |
method_device first offers the call to the state-block control handler, then to render_method, which itself tries state-block recording, resource bindings and target bindings before its own switch. Whatever is left falls through to the method_device switch.
| Group | Methods (vtable index) | Behaviour |
|---|---|---|
| Lifetime |
AddRef (1), Release (2), QueryInterface (0) |
QueryInterface returns this without checking the IID. |
| Lost device |
TestCooperativeLevel (3) |
Always D3D_OK; the device is never lost. |
| Info |
GetAvailableTextureMem (4) = 256 MiB; EvictManagedResources (5) no-op; GetDeviceCaps (7); GetDisplayMode (8) = back-buffer size; GetCreationParameters (9) = adapter 0, HAL, main HWND, flags 0x40; ShowCursor (12) returns 0; GetNumberOfSwapChains (15) = 1; GetRasterStatus (19) = in vblank; GetGammaRamp (22) identity ramp. |
|
| Swap chain |
GetSwapChain (14) |
Allocates a new swap-chain object each call. |
| Reset / Present | 16, 17 | See Reset and Present. |
| Resource creation |
CreateTexture (23), CreateVolumeTexture (24), CreateCubeTexture (25), CreateVertexBuffer (26), CreateIndexBuffer (27), CreateRenderTarget (28), CreateDepthStencilSurface (29), CreateOffscreenPlainSurface (36), CreateVertexDeclaration (86), CreateVertexShader (91), CreatePixelShader (106), CreateQuery (118), CreateStateBlock (59) |
See Resources. |
| Surface transfer |
GetRenderTargetData (32), StretchRect (34), ColorFill (35) |
surface_copy: CPU nearest-neighbour copy between A8R8G8B8/X8R8G8B8 surfaces after reading back GPU contents; X8 forces alpha to 0xFF; filters other than NONE/POINT return D3DERR_NOTAVAILABLE. ColorFill writes guest bytes directly. |
| Targets |
GetBackBuffer (18), SetRenderTarget (37), GetRenderTarget (38), SetDepthStencilSurface (39), GetDepthStencilSurface (40) |
target_binding_method. Only render-target index 0 is accepted; SetRenderTarget also resets the viewport to the surface size, as D3D9 does. |
| Scene |
BeginScene (41), EndScene (42) |
No-ops. |
| Clear | 43 | Rectangle lists are rejected (D3DERR_NOTAVAILABLE). A colour clear marks the target GPU-authoritative and calls mr_clear; depth and stencil clears call mr_clear_depth/mr_clear_stencil. |
| Transforms |
SetTransform (44), GetTransform (45) |
VIEW (2), PROJECTION (3), WORLD (256) and TEXTURE0..7 (16..23); others ignored. MultiplyTransform (46) is a no-op. |
| Viewport |
SetViewport (47), GetViewport (48) |
Stored verbatim. |
| Material / lights | 49-54 | Validated and stored for CPU lighting (see Fixed-function lighting). |
| Render, stage and sampler state | 57/58, 66/67, 68/69 | Stored in draw_state.rs[256], ts[8][33], sampler[16][14]; out-of-range indices return D3DERR_INVALIDCALL. |
| State blocks | 59, 60, 61 | See State blocks. |
| Bindings |
GetTexture/SetTexture (64/65), SetVertexDeclaration/GetVertexDeclaration (87/88), SetFVF/GetFVF (89/90), shaders (92/93, 107/108), SetStreamSource/GetStreamSource (100/101), SetIndices/GetIndices (104/105) |
resource_binding_method. SetFVF clears the declaration binding and vice versa (d3d9_set_vertex_input). |
| Shader constants | 94-99, 109-114 | 256 VS float4, 224 PS float4, 16 int4 and 16 bool per stage, range checked. |
| Draws | 81-84 | See Draw path. |
ProcessVertices |
85 | See Geometry Fast Paths. |
| Misc getters |
ValidateDevice (70), GetSoftwareVertexProcessing (78), GetNPatchMode (80) |
ValidateDevice reports one pass; GetSoftwareVertexProcessing returns 0; GetNPatchMode pushes 0.0 on the emulated x87 stack (its float return). |
Explicit no-ops (D3D_OK) |
MultiplyTransform (46), SetClipPlane (55), SetPaletteEntries (71), SetCurrentTexturePalette (73), SetScissorRect (75), SetSoftwareVertexProcessing (77), SetNPatchMode (79), SetStreamSourceFreq (102) |
Accepted and ignored. The scissor rectangle is ignored; SCISSORTESTENABLE itself causes draws to be rejected. |
Logged as unimplemented (return D3D_OK) |
Everything else, e.g. GetDirect3D (6), cursor methods (10, 11), CreateAdditionalSwapChain (13), SetDialogBoxMode (20), SetGammaRamp (21), UpdateSurface (30), UpdateTexture (31), GetFrontBufferData (33), GetClipPlane (56), clip status (62, 63), palette getters, GetScissorRect (76), patches (115-117). |
unimplemented remembers up to 64 distinct (class, method) pairs and logs d3d9 X::Y not implemented (returning D3D_OK) once each.
create_texture validates size (up to 8192x8192, depth up to 512), computes the full mip count when levels == 0, caps levels at 16, and records the description; it does not allocate pixel storage. Storage for a level is allocated on first LockRect/GetSurfaceLevel from guest pages (method_texture). level_size handles DXT1 (8-byte blocks) and DXT2-5 (16-byte blocks); other formats use bytes_per_pixel_x8 (unknown formats are assumed 32 bpp).
-
LockRectflushes any GPU-newer alias of the level (readback), allocates storage, incrementscontent_generation, and returns pitch and pointer.UnlockRectandAddDirtyRectalso increment the generation. Lock flags are not inspected for textures. -
GetSurfaceLevel/GetCubeMapSurface/GetVolumeLevelreturn the cached alias described above; each cube face/mip pair has a distinct alias. -
GetLevelDesc,GetLevelCountandGetTypework;GetPriorityandGetLODreturn 0 andGetAutoGenFilterTypereturns 2 (LINEAR);SetPrivateData,FreePrivateData,SetPriority,PreLoad,SetLOD,SetAutoGenFilterTypeandGenerateMipSubLevelsare no-ops;GetPrivateDatais logged as unimplemented.
Only level 0 is ever uploaded to Metal; lower levels the game writes are ignored and the renderer regenerates mips on the CPU (see Textures and Texture Packs).
create_buffer allocates the guest storage up front (index buffers must be D3DFMT_INDEX16 (101) or INDEX32 (102)). method_buffer:
-
Lock(offset, size, ppData, flags)range-checks, incrementslockedandcontent_generation, and returns a pointer into the guest buffer. If the flags containD3DLOCK_NOOVERWRITE(0x1000) orD3DLOCK_DISCARD(0x2000) the buffer is permanently retired from residency, because those flags identify streaming use even whenD3DUSAGE_DYNAMICwas omitted. -
Unlockdecrementslockedand increments the generation. -
GetDescreports format, type, usage, pool, size and FVF.
Draws read buffer bytes straight from guest memory; there is no separate "managed" copy.
make_surface allocates guest pages for every standalone surface. LockRect reads back GPU contents first (surface_flush) and marks the content changed; GetDC returns a fake HDC 0x20003. GetContainer checks the requested IID against the standard COM IIDs (IUnknown, IDirect3DResource9, IDirect3DBaseTexture9, the specific texture interface, or IDirect3DDevice9 for standalone surfaces) and returns E_NOINTERFACE otherwise (resource_get_container).
A surface becomes a Metal render target the first time a draw or clear targets it (surface_prepare); only A8R8G8B8 (21) and X8R8G8B8 (22) surfaces qualify. Ownership of the pixels alternates between the guest bytes and the GPU:
stateDiagram-v2
[*] --> CPU_authoritative
CPU_authoritative --> GPU_authoritative: draw or colour Clear, after surface_prepare uploads the guest bytes
GPU_authoritative --> GPU_authoritative: further draws
GPU_authoritative --> CPU_authoritative: surface_flush readback for LockRect, StretchRect, Present fallback or texture alias
CPU_authoritative --> CPU_authoritative: ColorFill or surface_copy writes guest bytes
gpu_dirty records which side is newer. A render-target texture whose level 0 is GPU-dirty is sampled directly through mr_target_texture without any readback. All surfaces that own renderers are listed in render_surfaces[] so flush_data can read back any surface whose guest storage a texture upload is about to hash.
CreateVertexDeclaration copies up to 64 D3DVERTEXELEMENT9 entries plus the end marker. CreateVertexShader/CreatePixelShader (line 785) copy the token stream up to the 0x0000FFFF end token (at most 65536 words), substitute an exact-byte pixel-shader replacement from the shader pack if one matches, compute a 64-bit FNV-1a content_key, hand the tokens to mr_shader_created so they are translated off the engine thread, and (vertex shaders) count radial-fog terms. Details are in Shader Translation. GetFunction/GetDeclaration return the stored bytes.
Queries record their type; Issue is a no-op and GetData always writes 1 (if the buffer has at least 4 bytes) and returns S_OK, so occlusion and event queries never stall. Swap-chain Present only increments the frame counter; GetBackBuffer returns the device back buffer; GetPresentParameters reports the back-buffer size and X8R8G8B8.
HostD3DDrawState is the single live copy of device state: render states, texture-stage states, sampler states, 16 texture bindings, FVF, shaders, declaration, 16 streams (pointer/offset/stride), index buffer, viewport, the full VS/PS constant files, and the world/view/projection/texture matrices. render_defaults applies the D3D9 defaults the bridge relies on (Z enable, Z write, SRCBLEND=ONE, DESTBLEND=ZERO, CULLMODE=CCW, ZFUNC=LESSEQUAL, ALPHAFUNC=ALWAYS, stencil defaults, COLORWRITEENABLE=0xF, BLENDOP=ADD, stage 0 MODULATE/SELECTARG1 defaults, wrap addressing, point filtering, identity matrices, full-target viewport).
Getters and draws always read the live state, even while a state block is being recorded.
BeginStateBlock/EndStateBlock produce a custom block backed by HostStateBlock: a byte image of HostD3DStateSnapshot (draw state, material, 256 light slots) with a parallel byte mask. While recording, stateblock_record_method diverts setters into host_sb_write (marking the bytes they cover) instead of changing live state; invalid arguments return D3DERR_INVALIDCALL without touching the mask.
-
Capturecopies live bytes into the masked positions only;Applycopies masked bytes back into a snapshot of live state and restores it throughdraw_state_restore, which retains every new binding before releasing old ones. - The 36 resource offsets (16 textures, 16 streams, VS, PS, declaration, index buffer) are reference-counted inside the block (
host_sb_replace_ref); a partial write over a binding field is rejected before any mutation. A custom block also retains its device. -
MultiplyTransform,SetClipPlane, palette, scissor rectangle, software vertex processing, N-patch mode andSetStreamSourceFreqare not stored; the first time each is seen during recording a[stateblock] legacy setter ... is not stored in custom masksline is logged. -
CreateStateBlock(predefinedD3DSBT_*blocks) creates an object whoseCapture/Applyare no-ops. The observational trace (HALO_STATEBLOCK_TRACE=1) exists to show whether the engine depends on them.
BeginStateBlock while already recording, and Reset while recording, return D3DERR_INVALIDCALL.
SetMaterial, SetLight and LightEnable are validated (finite values, light type 1-3, non-negative range/attenuation, spot angles) and stored in current_material and light_slots[256]; at most 8 lights can be enabled at once (lighting_enable). Enabling an undefined slot installs the D3D default white directional light.
Lighting runs on the CPU per vertex in the fixed-function 3D path only. d3d9_prepare_lighting is computed once per draw: the inverse-transpose cofactors of the world matrix (normals are passed through untransformed if the determinant is below 1e-12), and per enabled light the negated normalised direction (directional) or normalised spot axis plus cos(phi/2)/cos(theta/2). d3d9_light_vertex_prepared then evaluates emissive + ambient·D3DRS_AMBIENT + Σ attenuation·(ambient + diffuse·N·L·spot), with material sources selected by D3DRS_COLORVERTEX (141) and the *MATERIALSOURCE states (145, 147, 148), optional NORMALIZENORMALS (143), range and 1/(a0+a1·d+a2·d²) attenuation, and the D3D spot falloff. Specular lighting is not computed; the vertex specular colour is passed through.
All four draw methods (DrawPrimitive, DrawIndexedPrimitive, DrawPrimitiveUP, DrawIndexedPrimitiveUP) end in gpu_draw, which wraps the backend call in mr_texture_bindings_begin/end so textures resolved for this draw cannot be evicted by a later stage's upload.
flowchart TD
A["Translated engine calls IDirect3DDevice9::Draw*"] --> B["render_method (d3d9_render.inc)"]
B --> C{"Which draw?"}
C -->|"DrawPrimitive / DrawIndexedPrimitive"| D["Validate VB/IB ranges, compute first vertex and base"]
C -->|"DrawPrimitiveUP / DrawIndexedPrimitiveUP"| E["Use caller memory, unbind stream 0 and indices afterwards"]
D --> F["gpu_draw: texture binding scope"]
E --> F
F --> G["gpu_draw_impl: panorama UI gate, capture hooks"]
G --> H{"Vertex shader bound?"}
H -->|"yes"| P["gpu_program_draw"]
H -->|"no"| I{"HALO_FF3D and declaration, no PS, no FVF?"}
I -->|"yes"| J["ff3d_draw: CPU transform + lighting"]
J -->|"D3DERR_NOTAVAILABLE without reason"| K
I -->|"no"| K["RHW path: FVF 0x144 / 0x1C4 / 0x244 or POSITIONT declaration"]
K --> L{"Pixel shader bound?"}
L -->|"yes"| M["mr_draw_program_rhw"]
L -->|"no"| N["mr_draw_rhw"]
P --> Q["Upload textures, normalise indices, resident VB lookup"]
Q --> R["mr_draw_program"]
J --> S["mr_draw_fixed_clip"]
R --> T["Metal command buffer (shared)"]
S --> T
M --> T
N --> T
render_method computes the vertex count from the primitive count with primitive_vertices (triangle list 3n, strip/fan n+2, other types 0, primitive counts above 65534 rejected) and checks that stream 0 and the index buffer cover the requested range. For DrawIndexedPrimitive the vertex pointer passed on is stream0 + offset + (BaseVertexIndex + MinIndex)·stride and the index base becomes -MinIndex, so every index the renderer sees is relative to the first vertex actually copied. For the UP variants the stream-0 binding (and the index binding) is cleared afterwards, as D3D9 specifies.
Note: primitive_vertices returns 0 for point and line primitives, so those topologies are rejected by the device entry points even though the renderer itself supports line and point lists (see Metal Renderer).
draw_indices then converts 16- or 32-bit indices (rebased by base) to a 16-bit scratch array, expands triangle fans into lists, and rejects any index outside [0, nv) or above 65535, more than 196608 indices, or an index range past the 4 GiB guest space. It has a vectorised fast path for 16-bit lists/strips (see Geometry Fast Paths). Vertex and index scratch buffers grow to the largest draw and are reused (draw_scratch_grow); only the engine thread submits draws.
gpu_program_draw requires a bound vertex shader and declaration and rejects (with a logged reason) anything outside what the renderer implements:
| Rejection reason | Condition |
|---|---|
programmable object binding |
No VS or no declaration |
programmable topology |
Primitive not list/strip/fan |
sRGB/separatealpha/scissor |
SRGBWRITEENABLE (194), SEPARATEALPHABLENDENABLE (206) or SCISSORTESTENABLE (174) |
two-sided stencil |
TWOSIDEDSTENCILMODE (185) |
blend factors |
Illegal blend factors/operation |
secondary stream binding / secondary stream range
|
A declaration stream other than 0 not bound, stride 0 or above 256, or range outside the buffer |
texture kind / texture upload
|
Bound texture could not be uploaded |
It fills an mr_program_state with the token streams and their precomputed keys, the declaration, the full constant files, TEXTUREFACTOR, colour write mask (forced to include alpha when drawing into the panorama HUD target), fog enable/colour and radial-fog mode, depth/stencil/cull/alpha-test state and the blend mapping. Secondary streams are addressed at the same first vertex as stream 0. Samplers are resolved for every stage when a pixel shader is bound; without a pixel shader only stages up to the first D3DTOP_DISABLE whose operands actually read D3DTA_TEXTURE are resolved. When the fast paths are on, stream 0 and secondary streams may be bound from a resident GPU copy.
When HALO_FF3D is set (the visionOS app sets it to 1; desktop leaves it unset), draws with no VS, no PS, no FVF, a declaration, and no scissor or two-sided stencil go to ff3d_draw:
- The declaration is scanned for stream-0
POSITION(float3/float4),TEXCOORD0..7on any stream, stream-0COLOR0/COLOR1(D3DCOLOR) and a float3NORMAL. Without an untransformed position it returnsD3DERR_NOTAVAILABLEso the RHW path can try. - During a panorama pass it records the projection scale and viewport of the first depth-tested draw into the back buffer (
panorama_px/py,panorama_draw_viewport). - Each vertex is transformed by world·view·projection to homogeneous clip space;
wis kept rather than divided, because dividing bywon the CPU produced inf/NaN atw = 0and could turn a clipped primitive into a screen-sized triangle. Metal does the clipping. - Vertex colour comes from
COLOR0(white if absent), then CPU lighting ifD3DRS_LIGHTINGis on. With radial fog enabled on a panorama pass andFOGENABLEset withFOGTABLEMODE = NONE, a per-vertex range-fog factor is computed (see Radial Fog). - For each texture stage up to the first
DISABLEthat samples a texture, the source coordinate set is read from the right stream (secondary streams are indexed by the same vertex), optionally transformed byD3DTTFF_COUNT2(only flags 0 and 2 are accepted; generated coordinates are rejected), and stored in the stage's own slot. The stage's texture must be a 2D texture. - The vertices go to
mr_draw_fixed_clip, whose generated fragment shader evaluates the texture-stage combiner (see Metal Renderer).
Diagnostic switches HALO_FF3D_FLAT, HALO_FF3D_SHADE and HALO_FF3D_WHITE replace the vertex colour (flat green, a normal-based two-tone shade, or white) and use a single-stage SELECTARG1(DIFFUSE) material.
The rest of gpu_draw_impl handles screen-space vertices. It accepts FVF 0x144 (XYZRHW|DIFFUSE|TEX1, 28-byte minimum stride), 0x1C4 (+SPECULAR, 32 bytes) and 0x244 (TEX2, 36 bytes). When no FVF is set, halo_rhw_declaration_fvf recognises the two POSITIONT declarations the original executable builds (tables 0065E380/0065E3A0, created by 005301B0) byte-for-byte as 0x144/0x1C4. This is a draw-local interpretation; GetFVF still reports 0.
The path rejects FOGENABLE, two-sided stencil, separate alpha, sRGB write and scissor. Two sub-cases:
-
Pixel shader bound (HUD/screen effects): vertices are repacked into
mr_vertex_fixed_rhw(colour, optional specular, one or two UV sets), every stage's texture is resolved, and the draw goes tomr_draw_program_rhwwith the translated pixel shader. Cull mode 0 is promoted to 1 (none). -
No pixel shader: alpha test must be
GREATEREQUALif enabled. If colour or alpha is written, stage 1 must be disabled or unbound (multiple texture stages). Stage 0 must be one of the combinations the simple shader implements: colourSELECTARG1(TEXTURE)orMODULATE(TEXTURE, DIFFUSE|CURRENT), alphaSELECTARG1(DIFFUSE|CURRENT),SELECTARG1(TEXTURE)orMODULATE(TEXTURE, DIFFUSE|CURRENT). Selecting diffuse alpha setstexture_color_only(Halo's additive particles modulate texture RGB but keep diffuse alpha; requiring texture alpha used to discard those draws). Sampler LOD bias and sRGB sampling are rejected.SELECTARG1(TEXTURE)is emulated by forcing the vertex colour (or alpha) to white so the shader's multiply leaves the texel unchanged.
Every RHW vertex gets +0.5 added to x and y: D3D9 integer pixel centres correspond to Metal half-integer centres.
A depth-only draw (colour write mask 0 and no alpha test) skips texture resolution entirely.
map_blend_state maps ALPHABLENDENABLE/SRCBLEND/DESTBLEND/BLENDOP to the renderer:
D3D SRCBLEND / DESTBLEND (op ADD) |
Renderer mode |
|---|---|
| blending disabled | MR_BLEND_NONE |
| ONE / ZERO | MR_BLEND_NONE |
| SRCALPHA / INVSRCALPHA | MR_BLEND_SRC_ALPHA |
| ONE / ONE | MR_BLEND_ADD |
| DESTCOLOR / ZERO, ZERO / SRCCOLOR | MR_BLEND_MODULATE |
| DESTCOLOR / SRCCOLOR | MR_BLEND_MODULATE2 |
| SRCALPHA / ZERO | MR_BLEND_SRC_ALPHA_ZERO |
| SRCALPHA / ONE | MR_BLEND_SRC_ALPHA_ADD |
| ONE / INVSRCALPHA | MR_BLEND_PREMULTIPLIED |
| DESTALPHA / ONE | MR_BLEND_DEST_ALPHA_ADD |
anything else legal, or a non-ADD BLENDOP
|
MR_BLEND_CUSTOM with raw factors/op |
MR_BLEND_CUSTOM is rejected (draw returns blend factors) for ops outside 1-5, factors outside 1-15, and the two BOTH* factors (12, 13) that Metal cannot express. The Metal factor equations are on the Metal Renderer page; DEST_ALPHA_ADD applies src·dstA + dst to RGB and alpha.
A draw the bridge cannot represent returns D3DERR_NOTAVAILABLE through unsupported_draw, which logs the first 24 reasons with the FVF/VS/PS/declaration; it is never counted as a successful draw. Range errors return D3DERR_INVALIDCALL, allocation failures E_OUTOFMEMORY. Renderer errors are reported through mr_last_error() (first 30 logged).
- Rejects a null device/parameters or a state block being recorded.
- Uses the requested size (0 keeps the current size); the size must be 64-8192.
- Calls
mr_resizeon the back buffer's renderer first; on failure returnsD3DERR_NOTAVAILABLEand leaves every surface, size and binding unchanged. The renderer keeps its pipeline and texture caches; only the size-dependent colour/depth targets are replaced. - Re-describes the implicit back buffer and depth surface (
reset_implicit_surface), reallocating guest storage only when it must grow, and clearing it. - Rebinds the implicit targets, reapplies render-state and lighting defaults (all bindings released), and invalidates panorama history (
host_panorama_invalidate).
The device is never lost (TestCooperativeLevel always succeeds), so there is no D3DPOOL_DEFAULT recreation cycle.
Present (case 17) is also the host's per-frame heartbeat: it records the presenting thread, checks the FP environment, runs the input injection and controller bridges, the gaze pointer servo and the audio watchdog, and then publishes the frame through one of three paths:
| Condition | Action |
|---|---|
| A panorama GPU pool slot is ready and valid |
metalwin_present_gpu(slot, w, h): zero-copy hand-off; views and HUD were already blitted into the platform pool. |
| A GPU sink is active but the world frame is incomplete |
metalwin_present_dropped: nothing is copied; the presenter keeps its last complete panorama. |
| Otherwise |
surface_flush (readback) then metalwin_present(guest bytes); a failed readback returns D3DERR_NOTAVAILABLE. |
Afterwards it finishes the first-person and model captures, writes optional frame captures, logs every 60 frames, prints the draw-traffic report every 600 frames, and exits when host_frame_limit is reached. The panorama-specific parts are described in Panorama System and Immersive Presenter.
metalwin.m creates one fixed-size NSWindow with a layer-hosting CAMetalLayer (BGRA8, opaque). Window creation is marshalled to the main thread with a 3-second timeout so a host whose main thread is blocked fails cleanly instead of deadlocking; metalwin_present uploads the BGRA framebuffer with replaceRegion, draws a full-screen triangle with a linear clamp sampler, presents, and pumps Cocoa events (asynchronously on the main queue when called from another thread). metalwin_present_gpu and metalwin_present_dropped are declared in the header but implemented by the visionOS bridge (EngineVisionBridge.h).
ddraw.c exists only so the game's display probing succeeds: DirectDrawEnumerateExA reports one "Primary Display Driver", DirectDrawCreateEx returns a single shared object, EnumDisplayModes calls back with seven modes at 16 and 32 bpp, GetCaps/GetAvailableVidMem report 256 MiB, GetDeviceIdentifier reports halo-vision-null, and CreateSurface fails with DDERR_UNSUPPORTED. Every other method returns DD_OK.
All diagnostics are opt-in and observational: none of them changes draw acceptance or guest state.
| Facility | Switch | What it does |
|---|---|---|
| Render trace |
HALO_RENDER_TRACE_FROM, _TO, _EVERY
|
Per selected frame, groups draws by a key (primitive, stride, FVF, shaders, declaration, colour mask, target, viewport, depth/fog/stencil/scissor, blend, HRESULT, reason, pass) into at most 32 rows and logs [render-trace]/[render-draw] at Present (d3d9_capture.inc). |
| Draw reject trace |
HALO_DRAW_REJECT_TRACE=1, HALO_DRAW_REJECT_FROM, _TO
|
Logs up to 32 distinct rejected-draw keys with blend/alpha/depth state and up to 8 declarations element by element. |
| Drop tally | HALO_DROPTALLY |
Counts unsupported-draw reasons and logs them every 60 frames. |
| Render capture |
HALO_RENDER_CAPTURE=<dir>, _FROM, _TO, _PASS (0-2), _LIMIT (1-256, default 40), _VS, _FVF
|
Writes JSON/binary payloads per draw (state, transforms, material, lights, constants, shaders, level-0 textures, declaration, vertices, secondary streams, indices, HRESULT sidecars) within a 128 MiB budget, plus capture-summary.json. |
| First-person capture |
HALO_FP_CAPTURE=<dir>, HALO_FP_CAPTURE_FROM
|
One frame of the centre pass inside the viewmodel scope (004924B0): up to 16 draws, 8 ProcessVertices calls and 8 bone uploads (SetVertexShaderConstantF starting at c29), with clip-space vertices and normalised indices. |
| Model capture |
HALO_MODEL_CAPTURE=<dir>, _FROM, _OBJECT
|
Hooks the original model-draw functions 004D6FC0 (normal) and 00533850/00533730 (deferred) via host_model_capture_dispatch; selects one world biped (caller 0050F049, object type 0) and captures up to 64 draws, 16 PV calls, 32 bone uploads in a 64 MiB budget, plus a 64-entry candidate inventory. |
| Draw profile | HALO_DRAW_PROFILE |
Times CPU vertex work and submission per draw; host_draw_profile_report logs [draw-profile]. Per-pass timing is always on. |
| Sampler trace | HALO_SAMPLER_TRACE |
Logs up to 32 distinct mag/min/mip/anisotropy tuples. |
| State-block trace | HALO_STATEBLOCK_TRACE=1 |
Logs create/begin/end/capture/apply (32 details, then counts) with hashes of bindings, matrices and constants, and a summary each second. |
| Frame capture |
HALO_FRAME_CAPTURE=<dir>, HALO_FRAME_CAPTURE_EVERY (default 250), HALO_CAPTURE_DENSE
|
Writes frame-NNNN.bgra + JSON of the back buffer at frame 1, the frame limit, every N frames, or every 20 frames. |
Defaults are "unset" unless noted. The visionOS app sets several of these before the engine starts (EngineVisionRuntime.m) without overriding explicit values; see Environment Variables and Runtime Settings.
| Variable | Default | Effect | Read at |
|---|---|---|---|
HALO_FF3D |
unset (visionOS: 1) |
Enables the CPU fixed-function 3D backend. | d3d9_render.inc:831 |
HALO_FF3D_FLAT, HALO_FF3D_SHADE, HALO_FF3D_WHITE
|
unset | Diagnostic vertex colouring for the FF3D path. | d3d9_render.inc:714-716 |
HALO_FF3D_NOALPHA |
unset | Ignores alpha test in the FF3D path. | d3d9_render.inc:709 |
HALO_FF3D_LOG, HALO_FF3D_PASSES, HALO_FF3D_RAW
|
unset | FF3D logging: draw/texture/vertex samples and material/light setters; one log per distinct pass signature; raw vertex bytes for large textures. | d3d9_render.inc:672, d3d9.c:771, d3d9_render.inc:739, d3d9_render.inc:761 |
HALO_FF3D_DUMPTEX |
unset | Directory to dump the first 40 decoded textures as .bgra. |
d3d9_render.inc:343 |
HALO_DROPTALLY |
unset | Per-reason drop counts every 60 frames. | d3d9_render.inc:264 |
HALO_DRAW_REJECT_TRACE, _FROM, _TO
|
off | Rejected-draw trace window. | d3d9_render.inc:212 |
HALO_DRAW_PROFILE |
unset | Per-draw CPU timing and [draw-profile] log. |
d3d9_render.inc:404 |
HALO_SAMPLER_TRACE |
unset | Distinct sampler tuples. | d3d9_render.inc:473 |
HALO_RENDER_TRACE_FROM, _TO, _EVERY
|
off | Render-trace frame window and interval. | d3d9_capture.inc:28-29 |
HALO_RENDER_CAPTURE and _FROM, _TO, _PASS, _LIMIT, _VS, _FVF
|
off | Draw payload capture. | d3d9_capture.inc:170-172, :210-212 |
HALO_PROCESS_VERTICES_TRACE, HALO_PROCESS_VERTICES_CAPTURE
|
unset | Log ProcessVertices calls; capture their source vertices (and restrict render capture to them). |
d3d9_render.inc:1060-1065 |
HALO_FP_CAPTURE, HALO_FP_CAPTURE_FROM
|
off | First-person capture. | fp_vertex_capture.inc:21-22 |
HALO_MODEL_CAPTURE, _FROM, _OBJECT
|
off | Actor capture. | model_capture_hooks.inc:9, model_vertex_capture.inc:28-29 |
HALO_STATEBLOCK_TRACE |
off (1 enables) |
State-block trace. | d3d9.c:321 |
HALO_FRAME_CAPTURE, HALO_FRAME_CAPTURE_EVERY (250), HALO_CAPTURE_DENSE
|
off | Back-buffer dumps at Present. | d3d9.c:733-737 |
HALO_RADIAL_FOG |
off | See Radial Fog. | halo_settings.c |
HALO_PV_SKIN_FAST, HALO_PV_DIFFERENTIAL, HALO_PV_DIFFERENTIAL_FROM_FRAME, HALO_DRAW_FASTPATH
|
see Geometry Fast Paths | ||
HALO_TEXTURE_PACK, HALO_TEXTURE_MODS, HALO_SHADER_PACK, HALO_SHADER_MODS
|
see Textures and Texture Packs and Shader Translation |
Present also evaluates a set of debugging and automation switches. They are not graphics features, but they live in d3d9.c because Present is the per-frame boundary:
| Variable | Default | Effect | Line |
|---|---|---|---|
HALO_KEYSEQ |
unset |
start:end:hexScan,... holds DirectInput scan codes during frame ranges. |
528 |
HALO_LOOKSEQ |
unset |
start:end:dx:dy,... injects relative mouse motion. |
536 |
HALO_PAD2KEY |
on (0 disables) |
Maps the controller sticks to WASD and mouse look when no menu is active; right trigger notifies haptics. | 551 |
HALO_LOOKSCALE |
25 | Stick-to-mouse scale for HALO_PAD2KEY. |
564 |
HALO_CTLLOG |
unset (visionOS 1) |
Logs the pad-to-player input chain every 60 frames. | 570 |
HALO_SETMODE1_AT |
unset | Sets game mode global 00719720 to 1 at a frame. |
583 |
HALO_MKSESSION_AT, HALO_MKSESSION_FN
|
unset, 0049d210
|
Calls a guest function at a frame. | 584-586 |
HALO_SAVEWATCH, HALO_SAVEFORCE
|
unset | Log / clear the save-busy flags. | 590-592 |
HALO_POKE8, HALO_POKE32, HALO_PEEK
|
unset | Write/read guest globals in 0x400000-0xC00000 each frame (peek every 60). |
595-601 |
HALO_FORCEDIFF |
unset | Forces campaign difficulty 0-3. | 610 |
HALO_SETGLOBAL |
unset | Sets HSC boolean globals by index. | 616 |
HALO_A10_AUTOPLAY |
unset (visionOS 0) |
1 forces Heroic, clears save-busy and input gates for diagnostics; must not establish playability evidence. |
625 |
HALO_WAKE_SCRIPT, HALO_SCAN_THREADS
|
unset | Wake / list HSC script threads. | 638-643 |
HALO_SCAN_DEVICES, HALO_OPEN_DEVICES
|
unset | Log nearby device machines; force them open. | 653-672 |
HALO_TRACE |
unset | Logs game-mode/shell state changes. | 679 |
HALO_CONSOLE_FRAME, HALO_CONSOLE_CMD
|
unset | Executes a console command at a frame via 004C6A80. |
686 |
HALO_LOADMAP, HALO_LOADMAP_FRAME, HALO_CAMPAIGN, HALO_DIFF, HALO_GAMEMODE, HALO_GAMEMODE_FRAME
|
unset | Requests a map load at a frame, optionally via the campaign-start function 0049CFD0 with a difficulty, and sets a game-mode flag. |
691-709 |
Most of these use getenv directly each frame; the hot draw-path switches use the HOST_ENV macro (host.h:121), which caches the value per call site.
The bridge is engine-thread only. Draw scratch buffers, the object table, draw_state, capture state and the render-surface list have no locks; the comment on draw_scratch_grow states the invariant ("only the engine thread submits draws, and a draw is finished before the next begins"). Shader translation is pushed to the renderer's worker threads (see Shader Translation), and GPU completion happens on Metal threads, but neither touches bridge state. See Threading and Synchronization.
| Test | Built by tools/run_source_checks.py
|
What it asserts |
|---|---|---|
test_d3d9_resource_lifetime.c |
yes (macOS) | Buffer COM/Lock/binding/state-block/Reset/shader ownership against the real guest heap and page allocators; slot recycling over 140000 buffers; 9000-descriptor hash collision/removal stress. |
test_d3d9_texture_lifetime.c |
yes (macOS) | Texture/cube/volume parent aliases (36 distinct cube face-mips), GetContainer/GetDevice references, GPU-target teardown, sampler/custom-block/RT/depth/Reset ownership over 96000 descriptor turnovers. |
test_d3d9_reset.c |
yes (macOS) | Reset keeps the renderer, frees replaced pages, reuses smaller storage, resets state, invalidates same-size panorama history, and changes nothing when mr_resize fails. |
test_d3d9_stateblock.c |
no (standalone) | Custom Begin/End control, invalid/nested recording, live state untouched while recording, sparse F/I/B constant masks, masked Capture/Apply over 1000 cycles without reallocation, binding retains, final Release, predefined blocks unchanged. |
test_stateblock.c |
no |
stateblock.h alone: mask recording, replacement/null/duplicate resource ownership, allocation counts. |
test_d3d9_stateblock_trace.c |
no | The trace preserves CPU/draw state, caps details, counts omissions and rate-limits summaries. |
test_d3d9_lighting.c |
no |
D3DMATERIAL9/D3DLIGHT9 layouts (68/104 bytes), 8-light limit, directional/point/spot results, and that prepared lighting equals the per-vertex reference across light kinds, material sources and singular transforms. |
test_d3d9_destalpha_blend.c |
no | FF3D accepts DESTALPHA/ONE with the observed POSITION/NORMAL/TEXCOORD declaration, forwards the RGB-only mask and depth state, and leaves live state unchanged. It also asserts that DESTALPHA/ZERO is rejected; with the current MR_BLEND_CUSTOM mapping that pair is legal, so this standalone fixture appears to predate the custom-blend change. |
test_rhw_declaration.c |
no | Both POSITIONT tables map to 0x144/0x1C4; offsets, stream and type must match exactly; ordinary BSP POSITION declarations are excluded. |
test_draw_reject_trace.c |
no | Window gating, deduplication, lifetime caps, invalid-handle safety, unchanged draw state, HRESULT and fallback reason. |
test_render_capture_selectors.c |
no | Frame/pass/limit selectors, default limit 40, 128 MiB budget, metadata/result files, read-only state. |
test_fp_vertex_capture.c |
no | FP capture gates, payloads, caps, manifest, one-shot closure; live and guest bytes unchanged. |
test_model_vertex_capture.c |
no | Actor ABI/biped filter, reentry, nested deferred identity, guest escape cleanup, PV before/after, stream slices, omissions. |
test_draw_indices.c, test_vb_resident.c, test_d3d9_radial_fog.c
|
yes (macOS) | See Geometry Fast Paths and Radial Fog. |
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