The one closed loop between measured GPU cost and rendered quality. Everything that trades image quality for frame time goes through it. A per-subsystem budget cannot answer "does this frame fit", because fitting is a property of the total, so there is exactly one controller steering on total frame GPU time.
Shape
One measured input (total frame GPU milliseconds over a rolling window), one state variable, and one ladder of levers derived from it.
frame GPU ms ──▶ rolling window ──▶ load = mean / target
│
▼
pressure (0 … 1)
│
┌────────────┬───────────┼──────────────┐
▼ ▼ ▼ ▼
resolution rays indirect grid refresh rung
0.00–0.45 0.40–0.70 0.68–0.88 (outer bound)pressure is 0 when the frame renders exactly what the project authored and 1 when every enabled lever sits at its floor. Each lever declares the pressure window over which it travels; the windows overlap so quality degrades along a curve rather than one cliff per lever, and cheap reversible levers are spent before those that cost a temporal reset. Ladder order is GovernorLevers field order; a comptime check refuses a field without a lever_table row.
A lever must be free to move. The temporal history is always output-sized and is not a lever; only the active render sub-rect moves with pressure, and persistent allocations stay fixed while the controller runs (test "no lever names a value that sizes an allocation"). The resolution lever exists only while an effective reconstruction provider is active, since without one nothing maps a smaller raster back onto the output grid; ray and indirect-resolution governance stay active regardless.
Two speeds, never nested. pressure is the fast inner loop (8/16/32-frame window by response profile). The presented refresh rung is the slow outer bound: it drops a step only once every lever is pinned and the frame still misses, and climbs back after a long stretch with nothing given up and real headroom. Quality is spent before frame rate, and neither loop reads the other's state in its normal range.
Rates. Climb takes roughly two windows to travel the full range, because frames already in flight were rendered at the old settings and the window still holds them. Release is far slower than climb, since restoring quality re-enters the cost that caused the reduction. Resolution restoration additionally uses a rung-aware Schmitt trigger: before restoring, the governor predicts the next rung's quadratic pixel cost from the measured lower rung, requires a latency/noise margin, and moves one rung per dwell.
Measurement. Every pass is timestamped every governed frame. The decision is published to the Profiler panel every frame (whether or not Recording is on) and serialised into the NDJSON trace as the gov object.
The resolution lever and the extent envelope
Render-relative targets are allocated once at the widest extent the policy permits (ResolvedReconstruction.render_alloc_dims), and each frame rasterizes the top-left sub-rect (render_dims). A scale change therefore moves a viewport, not an allocation, and discards no temporal history. HikariUniforms.renderExtent carries the active extent and renderViewport its ratio to the allocation; shaders read them through shaders/akari/modules/render_extent.akari.
Two viewports follow from that:
- Camera geometry (
gbuffer_geometry,forward_transparent) sets the viewport to the active sub-rect. The projection maps NDC across whatever the viewport covers, so a scissor here would crop rather than scale. - Every other render-domain pass keeps a full-attachment viewport, so
inn.uvstays allocation-relative and existinguv * dimensions(tex)math is correct without classifying passes by domain. ARenderScissorstops it shading the margin; a backend that no-ops it renders the same image.
The rule: anything that converts between a UV and NDC is picture-relative; anything that sizes or addresses a resource is allocation-relative. Concretely:
| Consumer | Rule |
|---|---|
World reconstruction (hikari_reconstruct_view_pos, hikari_sky_dir_from_uv) | Divide by the viewport scale to recover a screen coordinate |
| Temporal resolve | alloc_dims sizes texels, current_dims is the picture; the 3×3 neighbourhood clamps to the picture's last texel |
| Screen-space marches (SSGI, SSR, contact shadows, GTAO, RT GI reprojection) | The picture ends at the viewport scale, not at 1.0 |
Render-domain reprojection (temporal_effect, global_illumination_temporal, rt_gi) | Motion is an NDC delta; scale its half into allocation UV. The output-domain temporal resolve does not need this |
| Hierarchical reductions (depth pyramid, velocity tile-max) | Clamp each level's fetch to the valid region; dispatch only the active ceil-tile extent |
| Compute grids | Cover the picture's share of the target |
Resource sizing (sceneRenderAllocDims) | Depth target, transient pool, froxel grid, pyramid chain lengths size from the allocation |
| Spatial AO / GI / reflection resolves | Derive the ceil-rounded active extent from bound uniforms; every tap clamps to it and the raster pass scissors to the picture. A stale one-texel border is not a permitted residual |
Rules that were each learned from a visible failure and must hold:
- The governed fraction multiplies the allocation, not the output.
render_alloc_dimsalready has the authored preset baked in; applying a display-relative fraction to it would render a Performance-preset project at a quarter of the output with the governor at rest. - The froxel grid follows the allocation and always covers the full frustum. Cropping it to the active picture relattices the medium on every step (shafts flashing). The viewport scale rides
depthRange.w/noise.wand is used only to convert allocation-space render UVs back to picture UV;grid.wis the depth-cull mip arm, not spare packing. - Previous jitter uses the previous raster grid. The resolved frame view carries separate current and previous sampling grids, including phase counts.
- Hi-Z addresses its valid sub-rect: predict scales its fetch by the previous active extent, correct by the current.
The step transition
On the frame a step lands, "previous extent" and "current extent" differ, and a history read mapped with the current viewport scale is misregistered by the step ratio; guide rejection then discards history across the whole screen and every render-domain temporal signal restarts at once (a frame-wide lighting flash). HikariUniforms.renderViewportPrev carries the previous extent and hikari_render_history_uv (render_extent.akari) is the one correct mapping: current allocation UV → picture UV → reproject → previous sub-rect, bounds-checked against the previous picture. Every render-domain history consumer goes through it; a history read multiplied by renderViewport directly is a bug by construction. It degrades to identity while the governor holds still, so steady frames are bit-identical. A window resize still restarts these histories, because the allocation itself moved.
The jitter sequence follows the active reconstruction ratio
FrameView.jitterPhaseCount follows the FidelityFX rule floor(8 × ratio²) (minimum eight): 8 at native, 32 at 2×, 72 at 3×, 128 at 4×. A shorter repeated sequence permanently under-covers the larger footprint. The sequence length and the moving-history window answer different questions and must never be clamped to fit each other:
- A thin-feature lock never reduces an already accumulated sample count; stationary pixels keep the 1024-frame ceiling. The lock governs bounded history admission and rectification and may admit its reprojected history sample when the feature is absent from every current tap, with the HDR envelope extending only to that admitted value.
- The motion window stays in absolute frames. It is an error bound, and reprojection error per frame is roughly constant in render pixels whatever the phase count, so fast motion lands on a four-frame window. Scaling it with the sequence drags every locked thin feature across the screen as a ghost line.
coverage_phase_count is the CPU value passed through reconstructionParams.z; the shader does not derive or cap it (test "the resolve receives the exact jitter sequence used by the camera").
Composing with a vendor upscaler
Dynamic resolution and a temporal upscaler compose; the authored preset is the ceiling and the governor varies below it. Two rules:
- The floor is relative.
min_render_scaleis a fraction of the authored scale, not of the output. Measured against the output it would sit above every preset from Balanced down and collapse the travel to zero. - The mip bias does not ride the governor. The texture LOD bias lives in the geometry sampler catalog, and moving it recommits every pipeline that bakes the catalog (a root-signature rebuild on D3D12). It follows the authored ratio; a governed-down frame keeps a slightly conservative bias. Tracking it per frame would need a bias on the material sample path (
HikariUniforms.renderViewport.xalready carries the number).
Reconstruction plugins (FSR, MetalFX, …) are created against a maximum input size and accept a smaller input per frame. ReconstructionConfig is the allocate-once event (output_*, max_render_*, min_render_*, dynamic_render_extent); ReconstructionFramePayload is per frame, carrying both render_* (this frame's valid extent) and render_alloc_* (what the textures are). on_config fires only when the envelope changes, never because the governor moved the scale within it.
Not this
RtBudgetTier is demand sharing: how a fixed authored ray allowance is divided between the effects that are on. No feedback, same numbers every frame. It decides what "authored" means before the governor reduces anything.
Settings
Project Settings → Quality → Frame governor: mode, response, target, headroom, a toggle per lever, the resolution floor and step count. render.json path pipeline.quality.governor (Project file).
Where the refresh rate comes from
present_interval is the default target, and the rate reaches it as DisplayCapabilities.refresh_hz, a field of the same per-frame capability sample that carries EDR headroom.
| Backend | Source |
|---|---|
| Metal | NSScreen.maximumFramesPerSecond, sampled on the main thread in hikariPublishDisplayCaps. The panel's own cadence, not the display link's requested range |
| D3D12 | EnumDisplaySettingsW(ENUM_CURRENT_SETTINGS) on the output the window most overlaps. What the desktop runs at now |
A platform that reports nothing leaves it at zero, refreshIntervalMs returns zero, and targetMs degrades to target_ms with the rung pinned at 1.