Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Systems8 min read

Frame governor

On this page
On this pageShapeThe resolution lever and the extent envelopeThe step transitionThe jitter sequence follows the active reconstruction ratioComposing with a vendor upscalerNot thisSettingsWhere the refresh rate comes from Back to top

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.

text
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.uv stays allocation-relative and existing uv * dimensions(tex) math is correct without classifying passes by domain. A RenderScissor stops 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:

ConsumerRule
World reconstruction (hikari_reconstruct_view_pos, hikari_sky_dir_from_uv)Divide by the viewport scale to recover a screen coordinate
Temporal resolvealloc_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 gridsCover 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 resolvesDerive 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_dims already 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.w and is used only to convert allocation-space render UVs back to picture UV; grid.w is 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_scale is 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.x already 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.

BackendSource
MetalNSScreen.maximumFramesPerSecond, sampled on the main thread in hikariPublishDisplayCaps. The panel's own cadence, not the display link's requested range
D3D12EnumDisplaySettingsW(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.

PreviousRenderer architecture mapNext GPU particles

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/frame-governor.md
On this pageShapeThe resolution lever and the extent envelopeThe step transitionThe jitter sequence follows the active reconstruction ratioComposing with a vendor upscalerNot thisSettingsWhere the refresh rate comes from Back to top