Repository navigation
Panorama Budget and LOD
Every panorama bearing repeats Halo's whole scene traversal, so the engine thread's cost grows with the number of bearings drawn. Engine time, not GPU time, limits this port. The bearing budget (panorama_budget.h) decides each frame which of the nine bearing passes to run. It draws what the viewer is looking at every frame and spends measured spare time on the rest of the sphere. Under load it steps through four "tiers" that refresh the periphery more slowly and eventually share one forward picture between both eyes.
The panorama LOD (panorama_lod.h) is a native replacement for Halo's per-object pixel-size routine 0050F740. It makes an object's level of detail, shadow and lighting cadence identical in every bearing that draws it, so the joins do not pop.
Both run inside the 0050BEA0 panorama hook described on Panorama System. The budget's busy-time target is supplied by the frame pacer when pacing is enabled (Frame Pacing).
| File | Role |
|---|---|
native/EngineHost/panorama_budget.h |
Header-only budget controller: planning, measurement, tiers, motion fill, scene entry, pacer coupling. |
native/EngineHost/panorama_lod.h |
Native 0050F740 with a frame-wide depth and scale; three modes. |
native/EngineHost/panorama_hooks.inc |
Calls the budget once per frame, applies its mask in the pass loop, feeds back timings, and dispatches the LOD routine. |
native/EngineHost/panorama.h |
host_panorama_budget_extra/tier, host_panorama_busy_seconds for reports. |
native/EngineHost/halo_settings.c |
HALO_PANORAMA_TARGET_FPS seeding and the live target. |
native/EngineHost/shims_kernel32.c |
Presenting-thread idle accounting (host_yield_spin_ns) that busy time is derived from. |
Busy time. At each panorama entry the hook measures the frame period since the previous entry and subtracts the presenting thread's idle accumulated in host_yield_spin_ns over that period (panorama_hooks.inc:462-470). Idle includes:
- Halo's limiter spinning in
Sleep(0); -
Sleep(n)on the presenting thread; - the frame pacer's blocking wait (
host_frame_idle_wait).
The Sleep comment explains why sleep must count as idle. Left out, "the bearing budget read 24-30 ms of busy time at Halo's 30 fps cap whatever the engine did ... and could never step back down to stereo" (shims_kernel32.c:295-299).
Pass timing. Every pass is timed with CLOCK_UPTIME_RAW around the original 0050BEA0 (panorama_hooks.inc:651-656). After a non-aborted frame, halo_panorama_budget_note_passes(total, count) updates:
-
pass_ema, the cost of one bearing pass; -
ppf_ema, passes per frame.
Both are EMAs with weight 0.1. They are only updated once every layer is ready, because the full sphere after a load "would price every pass at its texture-upload cost". Samples of 0 s or above 1 s are ignored.
Gaze. halo_settings_head_yaw() and head_pitch() are radians from the recentred forward direction. The compositor writes them every frame (EngineImmersive.swift:772).
Target. host_frame_pacer_budget_target(1 / panorama_target_fps()) gives the target. Without pacing, it is the target FPS period. While pacing, it is the rung's period less jitter headroom (see Frame Pacing).
HaloPanoramaView (panorama_budget.h:8) is { int layer; float yaw, pitch; int eye; }. The hook's static schedule[] holds nine entries in draw order: 0, 2, 5, 6, 7, 8, 9, 4, 1.
HaloPanoramaBudget (panorama_budget.h:25-63):
| Field | Meaning |
|---|---|
last_drawn[10] |
Frame number each layer was last actually drawn. Used for staleness. |
previous_yaw, yaw_rate, previous_pitch, pitch_rate, have_previous
|
Gaze history. Rates are per frame, clamped to +/-0.35 rad. |
extra_half |
Optional extra passes per two frames. 1 is the floor (one distant bearing every other frame). |
busy_ema |
Smoothed busy time (EMA weight 0.2). |
since_adjust, tier_dwell
|
Seconds of measured frame period since the last adjustment / tier change. Seconds rather than frames, because frame counts "made every wait three times longer at 11 fps than at 30". |
tier, tier_pressure, tier_relief
|
Heavy tier 0..3 and the consecutive-adjustment counters that move it. |
pass_ema, ppf_ema
|
Cost of one pass, passes per frame. |
level_tier, have_level_tier
|
Tier the last level settled at, used as the next level's start. |
scene_epoch |
Advances on scene entry. The frame pacer resets when it changes. |
distant_phase |
Tier-3 slot (1 or 3 of 8) for the distant refresh, latched once per eight-frame cycle. |
Constants (panorama_budget.h:65-79):
| Constant | Value | Use |
|---|---|---|
HALO_PANORAMA_BUDGET_LOOKAHEAD |
3 frames | Gaze prediction lead. |
HALO_PANORAMA_TIER_MAX |
3 | Lightest tier. |
HALO_PANORAMA_TIER_UP_PERIODS |
3 | Consecutive over-target adjustments before a heavier tier. |
HALO_PANORAMA_TIER_DOWN_PERIODS |
6 | Consecutive relief adjustments before a lighter tier. |
HALO_PANORAMA_BUDGET_ADJUST_SECONDS |
0.25 | Adjustment interval. |
HALO_PANORAMA_TIER_UP_SECONDS |
1.0 | Minimum dwell before stepping heavier. |
HALO_PANORAMA_TIER_DOWN_SECONDS |
3.0 | Minimum dwell before stepping lighter. |
HALO_PANORAMA_STEREO_DWELL_SECONDS |
6.0 | Minimum dwell for the visible step from tier 2 (mono) back to tier 1 (stereo). |
halo_panorama_tier_floor_ppf[] |
{4.5, 3.5, 2.6, 1.625} |
Passes per frame each tier's floor draws looking ahead (tier 3: 13 passes in 8 frames). |
HALO_PANORAMA_GAZE_SEAM_RADIANS |
0.21 (about 12 deg) | Gaze this far off the bearing's centre puts the neighbouring join in view. |
| Tier | Centre | Ring neighbours within 90 deg of gaze | Caps (when head pitched toward them, more than 0.5 rad) | Seam neighbour (join in view) | Distant extras |
|---|---|---|---|---|---|
| 0 | stereo pair, every frame | every frame | every frame | (covered by the neighbours) | budgeted, extra_half can grow |
| 1 | stereo pair, every frame | alternate: right side on even frames, left on odd | odd frames | every frame | floor: 1 every other frame |
| 2 | mono (one centre for both eyes) | as tier 1 | odd frames | every frame | floor |
| 3 | mono | right neighbour at frame%4==0, left at ==2; none if a join is in view |
up at frame%4==1, down at ==3
|
even frames | exactly one in 8 frames, in the latched odd slot |
halo_panorama_budget_mono() is tier >= 2. The hook draws stereo only when stereo is enabled and the budget is not mono (with the tight budget active) (panorama_hooks.inc:497).
The header comment gives the motivation for the heavy tiers. On the headset one pass of Silent Cartographer is 15 ms. The floor at tier 0 (centre pair + two neighbours + half an extra) is 4.7 passes, about 9 fps. "The forward view keeps its frame rate; the sides refresh slower."
Source: panorama_budget.h:201-320.
flowchart TD
A["Gaze yaw/pitch from the presenter"] --> B["yaw_rate = wrapped delta, clamped to 0.35 rad/frame, ahead = yaw + 3 * rate"]
B --> C["score each active view = cos(angle to (ahead, pitch)), layer 4 inactive in mono"]
C --> D["Mandatory: layer 1, layer 4 if stereo, best-scoring view(s)"]
D --> E["Ring views within 90 deg of ahead, caps if pitch beyond 0.5 rad"]
E --> F{"tier"}
F -- "0" --> G["draw all of them"]
F -- "1-2" --> H["alternate sides by frame parity, caps on odd frames"]
F -- "3" --> I["one per 4-frame phase, ring neighbours dropped if a join is in view"]
G --> J
H --> J
I --> J["Seam: if tier at least 1 and gaze more than 0.21 rad off the gaze bearing, add the neighbour on that side (tier 3: even frames only)"]
J --> K["Rest: sort by min(age,64) * (0.7 + 0.3 * nearness)"]
K --> L["Clamp extra_half to 2 * rest (anti-windup)"]
L --> M{"tier 3?"}
M -- yes --> N["take 1 only when frame%8 equals distant_phase (latched at frame%8==0: 3 if pitch above 0.5 else 1)"]
M -- no --> O["take = extra_half/2, plus 1 on odd frames if extra_half is odd"]
N --> P["add to mask the first take views"]
O --> P
P --> Q{"camera moving or head rate above 0.003?"}
Q -- yes --> R["motion_fill: up to 2 more visible stale views if they fit under 0.9 * target"]
Q -- no --> S["final mask"]
R --> S
Details:
-
Prediction. Only yaw is predicted.
ahead = yaw + rate * 3. Pitch scoring uses the current pitch.pitch_rateis stored only so motion fill can trigger. -
Score. The score is
cos(pitch) cos(vpitch) cos(dyaw) + sin(pitch) sin(vpitch), the cosine of the angle between the view centre and the gaze (panorama_budget.h:91-94). In mono, layer 4 is excluded fromactiveentirely. -
Gaze bearing. The highest-scoring ring view (pitch 0) gives
gaze_off, the wrapped yaw offset ofaheadfrom that view's centre.seam_in_viewis true when|gaze_off| > 0.21. -
Mandatory set. This is layer 1, plus layer 4 if stereo, plus every view within 1e-5 of the best score. It also includes the "beside" views, subject to the tier rules in the table:
- ring views within 90 deg of
ahead; -
UPonly ifpitch > 0.5; -
DOWNonly ifpitch < -0.5.
- ring views within 90 deg of
-
Seam rule. At tier at least 1 with a join in view, the ring view at
gaze yaw +/- 60 deg(on the side ofgaze_off) is drawn every frame, or on even frames at tier 3. The comment explains that taking the seam neighbour in turns was up to three frames behind at tier 3, "which at a brisk stick turn is several degrees of misalignment and a flash of stale lighting across the join" (panorama_budget.h:267-280). -
Distant extras. The remaining active views are insertion-sorted by
min(frames since drawn, 64) * (0.7 + 0.3 * (score+1)/2). Age dominates, with a bounded nearness weight, so "the bearing behind the head still comes round within about seventeen frames at the tier-zero floor budget". -
Anti-windup.
extra_halfis clamped to2 * rest(minimum 1) before it is spent. Credits above the number of optional views "made overload adjustments do no real work for up to seconds" (panorama_budget.h:300-306). -
Tier-3 distant slot. The odd slot (1 or 3 of every 8 frames) is chosen at
frame % 8 == 0and kept for the cycle. The slot without the cap toward the gaze is preferred, so the distant pass lands on a lighter frame. Latching prevents "a moving head from postponing every distant refresh by switching slots".
The caller records the actually drawn mask with halo_panorama_budget_drawn after the frame, so staleness counts real pictures.
halo_panorama_budget_motion_fill (panorama_budget.h:322-361) runs when host_panorama_camera_moving() is set, or when the yaw or pitch rate exceeds 0.003 rad/frame (panorama_hooks.inc:509-512).
- It needs valid, finite
pass_ema,busy_emaandppf_ema. Without timings it returns the mask unchanged. - It estimates non-pass time as
other = busy - pass * ppf. - It admits at most two more views. Each is admitted only while
other + pass * (passes + 1) <= 0.9 * target. - The candidate is the highest-scoring undrawn view against the current gaze with score above 0.35. Ties go to the older
last_drawn. - Caps are a special case. Looking roughly ahead, a cap's centre is about 90 deg away, but its edges overlap the visible ring. So when
|pitch| > 1.5and the score is between -0.15 and 0.36, the cap's score is lifted to 0.36. That puts it just above the threshold and below the front neighbours (cos 60 deg = 0.5). The opposite cap is not drawn when looking steeply up or down.
Motion fill "does not enable geometric warping or lower the frame-rate target". It spends measured spare time on visible stale neighbours rather than leaving it idle during a heavy tier's relief dwell.
The pass loop applies the mask only after host_panorama_all_layers_ready() (panorama_hooks.inc:540-558). Before that, the full sphere is drawn.
-
HALO_PANORAMA_ALL_VIEWS=1draws every active layer every frame. -
HALO_PANORAMA_TIGHT_BUDGET=0restores the older schedule. It draws every bearing within 90 deg of the current gaze (caps when pitch is beyond +/-0.5), plus one rotating layer per frame from{7, 8, 9, 5, 6}(panorama_rotation % 5). In this mode the budget never forces mono.
Called once per frame with busy time, measured period, target, and an extra ceiling of 2 * view_count = 18 (panorama_hooks.inc:483-485). Source: panorama_budget.h:126-193.
-
Guard. Busy time of 0 or over 1 s is ignored ("a load, not a frame"). An invalid period is replaced by
max(busy, target). -
Smoothing.
busy_ema += 0.2 * (busy - busy_ema). Both dwell clocks advance by the period. - Interval. Adjustments happen every 0.25 s of measured period.
-
Extras. The target is a ceiling, not a centre.
-
Grow. At tier 0 only, when
busy_ema < 0.8 * target,extra_half++(up to the ceiling). -
Shrink. When
busy_ema > targetandextra_half > 1, the excess is priced in measured passes:drop = ceil(2 * (busy - 0.9 * target) / pass_ema)half-passes, clamped to[1, extra_half - 1]. This keeps 10% headroom. Without pass timing, it halves whenbusy > 1.25 * targetand steps by 1 otherwise. Only optional views are shed this way.
-
Grow. At tier 0 only, when
-
Pressure and relief.
-
Pressure is "at the floor (
extra_half == 1) and still over target". -
Relief means one more pass fits under nine tenths of the target:
busy + pass_ema < 0.9 * target. Without timing it meansbusy < 0.6 * target. The comment explains why the pass-priced test exists: at Halo's 30 fps cap, busy time read 24-30 ms whatever the pass count, so "once a heavy scene took the budget to mono it stayed mono for the rest of the session". - Each counter resets the other.
-
Pressure is "at the floor (
-
Overload jump. At the floor with
busy > 1.5 * target, after a dwell of at least 0.25 s, the tier jumps to the lightest one predicted to fit:busy - pass_ema * (ppf_ema - floor_ppf[t]) <= target. Without timing it jumps to tier 3. - Step up. 3 pressure adjustments and 1 s dwell at the current tier move one tier heavier.
- Step down. 6 relief adjustments and a dwell of 3 s (6 s for 2 to 1, the return to stereo) move one tier lighter.
The struct comment still says "Each step waits ninety frames". The constants and code use seconds of measured period.
When the shell closes or a frame gap exceeds 1 s in play, the hook calls halo_panorama_budget_scene_entry (panorama_budget.h:104-117), followed by host_panorama_invalidate(). Scene entry:
- increments
scene_epoch; - starts at the tier the last level settled at, or 3 if none, but never lighter than 2;
- sets
extra_half = 1; - zeroes
busy_ema,pass_emaandppf_ema, and the pressure, relief and dwell counters.
The comment records the bug this prevents: Build74 "carried the menu's tier 0 and full extras into Silent Cartographer and drew nine passes a frame at 3-11 fps".
halo_panorama_budget_note_level runs on every non-shell frame and remembers the current tier as level_tier.
| Function | Purpose |
|---|---|
halo_panorama_budget_floor_busy(b, busy) |
busy - pass_ema * (ppf_ema - 1.625), floored at 0. This is what the frame would cost at the lightest tier's floor. The pacer chooses its cadence from this, "so it never settles on a slower cadence only because the budget filled a faster one with extra bearings" (panorama_budget.h:369-379). |
halo_panorama_budget_retarget(b, target) |
When the pacer moves to a faster cadence or stops pacing, jump to the lightest tier predicted to fit the new target and reset extras to 1. Only ever heavier (panorama_budget.h:381-396). |
host_panorama_budget_extra(), host_panorama_budget_tier() and host_panorama_busy_seconds() feed EngineVisionDrawProfile. They appear in diagnostics as panoramaBudgetTier, panoramaBudgetExtraHalf and panoramaBusyMilliseconds (EngineVisionRuntime.m:259-261).
0050F740 computes an object's on-screen pixel size as 2 * r * S / depth (panorama_lod.h:11-23):
-
ris the bounding radius (obj+0xAC), halved or quartered by the LOD switch at0x689450; -
Sis the view's pixels per unit at unit depth (0x7C32F0); -
depthis the object's distance along the view's forward axis, clamped to at least 0.1.
That single number drives a lot:
- the model LOD and the draw cutoff (
004D6FC0); - shadow admission above 30 px and the shadow fade-in up to 45 px (
0050EBA0); - the shadow silhouette LOD at 0.3 of the value;
- the lighting record and how often it is refreshed (
0050EA00,0050F150,0050F270).
Each bearing has its own forward axis, so one object gets a different depth, and therefore a different pixel size, in every bearing and in each eye. Where bearings overlap (the band/cap belt between 37.5 and 52.5 deg of elevation, the ring's guard columns and cross-fades), the same object could be drawn with two LODs, a shadow in one and none in the other, and lighting refreshed on two cadences: "a pop at the seam".
panorama_lod_mode() reads HALO_PANORAMA_LOD on each frame entry through a per-site cached HOST_ENV (panorama_hooks.inc:63-72).
| Mode | Value | Depth used | Behaviour |
|---|---|---|---|
HALO_PANORAMA_LOD_DISTANCE |
default | Straight-line distance from the head centre | Pixel size is the object's true angular size times the centre view's S. On the centre axis it equals the original. Off-axis it is cos(angle) of the old value: 0.87 at a band's side edge, 0.79 at 37.5 deg up. It is identical in every bearing and both eyes, and does not change as the view turns. |
HALO_PANORAMA_LOD_NEAREST |
nearest |
Largest of the depths along all bearing axes | Equals the original in the bearing whose axis is nearest the object, and never exceeds it elsewhere. Changes as the view turns, like the original. |
HALO_PANORAMA_LOD_BEARING |
bearing or 0
|
The original per-bearing planar depth | For comparison. Bit-identical to the translated routine. |
At frame entry, halo_panorama_lod_begin is called for each renderer view with that view's saved central pose (panorama_hooks.inc:500-506, panorama_lod.h:98-111). It does three things:
- copies the head position from the pose;
- for
NEAREST, computes up to 12 bearing axes (one per schedule entry) with the same float operations the pass loop uses (halo_panorama_lod_bearing_axis); - clears
scale.
scale is latched on the first hooked call of the frame from 0x7C32F0. Every bearing has the same S: 0050CC40 stores S = h / (2 tan(vfov/2)) from the raster height and vertical field, which the loop gives every bearing alike (DENSE narrows only the width). So it is "the centre view's S; holding it for the frame only makes that true by construction".
panorama_lod_reach is set to the eye separation for the frame.
panorama_lod_dispatch (panorama_hooks.inc:73-90) replaces 0050F740 only when all of these hold:
- the call is inside a panorama frame (
panorama_nested); - the current view index (
0x007C3108) is below the record count and below 4; - the mode is not
BEARING; - the x87 stack has room for the original's three temporaries (
fp_valid & 0xE0clear). Otherwise the original runs "and is left to fail as it would"; - the current camera (
0x007C3114) is within|separation| * 1.01 + 1e-4of the head centre. This excludes the mirror pass (0050BA80renders it through0050BFB0as view -1 from a reflected camera) and any foreign camera, while admitting both eyes.
halo_panorama_lod_object_pixels (panorama_lod.h:150-231) reproduces the original in order:
- the
0x400000special-case flag (returnsFLT_MAX); - the radius switch;
- the 12-byte stack copy of the position;
- the sign test and the 0.1 clamp (a NaN takes the clamp);
- the same x87 operations and stack depth, the same EAX/ECX/EDX, EFLAGS and status word.
Only the depth term and S differ. Depth arithmetic goes through engine_fp_arithmetic in the guest's rounding mode, and the square root honours the guest rounding mode as FSQRT would (halo_panorama_lod_sqrt). With the frame's depth, only ST0 and the condition bits that compared it (C0/C2/C3, and AX, which holds the status word) can differ from the original.
| Variable | Default | Effect | Read at |
|---|---|---|---|
HALO_PANORAMA_TARGET_FPS |
30 (accepted 10..60) | Seeds the live "Keep at least" target. The budget keeps busy time under 1/target. The settings slider offers 20..30. An invalid value set at runtime falls back to 27. |
halo_settings.c:63, halo_settings.c:111-112 |
HALO_PANORAMA_TIGHT_BUDGET |
on |
"0": legacy near-gaze plus one rotating layer schedule; no budget-forced mono. |
panorama_hooks.inc:459-460 |
HALO_PANORAMA_ALL_VIEWS |
unset |
"1": draw every active layer every frame (ignore the mask). |
panorama_hooks.inc:451-452 |
HALO_PANORAMA_FORCE_TIER |
unset |
0..3: outside the shell, hold that tier and extra_half = 1 every frame. "Mac measurement only", so two builds draw the same passes. |
panorama_hooks.inc:489-494 |
HALO_PANORAMA_LOD |
distance |
nearest or bearing/0 (see modes). |
panorama_hooks.inc:68 |
HALO_FRAME_PACING |
off | When on, the pacer supplies the budget target (see Frame Pacing). | halo_settings.c:79-81 |
All budget and LOD state is static to the engine thread. The gaze, roll and target are read from halo_settings atomics once per frame at entry. The three report getters are plain reads of engine-thread fields, called from the diagnostics thread.
| Test | What it asserts |
|---|---|
test_panorama_budget.c |
No phantom extras: 18 is clamped to 10. A cheap corridor followed by a costly room fits again within 0.8 s with fewer than 20 misses. For every yaw/pitch: the mask stays inside the validity mask, the centre pair is always drawn, and the best view is always drawn. Tier-0 stereo floor is 540 passes per 120 frames, and every layer refreshes within 12 frames. Extras grow and shrink. A head turn reaches the right bearing early. With one extra per frame, the five distant layers rotate fairly. Tiers climb to 3, with fewer passes per tier and mono from tier 2, then descend one dwell at a time with extras held until tier 0. Tier 3 averages 104 passes per 64 frames with at most 2 per frame. Caps and pitch changes do not stack passes. No layer goes unrefreshed for more than 64 frames at any tier. Relief comes from pass-priced room. Extras halve when well over. Seam neighbours are covered. Scene entry starts heavy and remembers the level tier. Waits are in seconds. The overload jump works. |
test_panorama_motion_budget.c |
No timings means no fill. Both front neighbours are filled when room exists. A costly scene keeps the floor. Exactly one view is admitted when only one fits. NaN timing is a no-op. Caps are filled when looking ahead; only the oldest cap when room is short; never the opposite cap. Yaw and pitch rates are measured. |
test_panorama_lod.c |
(1) Against the translated 0050F740 over random and adversarial inputs (NaN, infinities, signed zeros, subnormals, clamp, FLT_MAX case, every radius switch, every rounding mode), BEARING is bit-identical across all guest state. DISTANCE/NEAREST differ only in ST0 and the compare bits, and ST0 matches `2 r S / max( |
test_frame_pacer.c budget_checks
|
floor_busy arithmetic. The pacer's target earns more extras than the plain 1/30 target for the same busy time. |
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