---
title: "Scene-wide renderer settings"
description: "Part of the rendering guide (guides { path = \"topics/rendering\" }), which lists every part."
section: "Topics"
slug: "topics-rendering-settings"
canonical: "https://origozero.ai/docs/topics-rendering-settings"
updated: "2026-10-16T13:27:29.449974194+00:00"
tags: ["documentation", "guide", "rendering"]
---

# Scene-wide renderer settings

## Scene-wide renderer settings

The global pipeline switches — shadows, deferred shading, order-independent transparency, culling — live in the world's **`.world_settings`** file under `[render]`, not in a runtime API. The renderer reads the file once a frame, so an edit reaches the next frame. Edit them there (the engine/development guides cover world settings):

```toml
[render]
shadows = true
deferred = true
oit = true
culling_mode = "gpu"
anisotropy = 16        # texture filtering quality: 1, 2, 4, 8 or 16


shadow_caster_min_radius_px = 0  # smallest on-screen caster that still casts
shadow_caster_max_distance = 0   # how far from the camera casters keep casting

frame_rate_cap = 0        # most frames per second the engine runs at; 0 for no cap
max_frames_in_flight = 2  # frames of GPU work that may be outstanding
present_mode = "mailbox"  # how a presented frame reaches the display
```

The values above are the high tier's. `anisotropy` takes its default from the
device's quality tier wherever the file leaves it out; a value the file states
holds every device to it, the phone included. The **graphics-quality guide**
(`guides { path = "topics/graphics-quality" }`) lists what each tier states.


**Texture filtering** is the one of these with a runtime API as well, because it is the setting worth scaling with a quality slider. `anisotropy` is how many samples a material texture is allowed to take along the axis of greatest compression — which is what keeps detail on a surface seen at a grazing angle. At 1 (plain trilinear) a road, a floor, or a long wall receding toward the horizon blurs to a flat average well before it gets there; at 16 the detail that is in the texture stays resolvable.

```lua
renderer.setAnisotropy(16)   --> 16   the effective level, clamped to the device
renderer.anisotropy()        --> 16   what material textures sample at now
renderer.maxAnisotropy()     --> 16   what this device can do (16, or 1 without the feature)
```

`setAnisotropy` returns the level that was actually applied, so asking for more than the hardware offers reports the clamp rather than failing. It takes effect on the next frame for content already on screen — no reload, no re-upload. A level that is not a power of two in `[1, 16]` is an error rather than a silent round. The world setting seeds the level at boot and whenever the file changes; a runtime call overrides it until then.

**Frame pacing** is how far the CPU is allowed to run ahead of the GPU.
Submitting work returns before the GPU has done it, and everything a submission
holds — its staging allocations, its bind groups, its command buffer — stays
alive until it completes. A frame that asks for more work than the GPU finishes
in a frame's time therefore leaves that behind it, and unbounded that is memory
growth rather than a lower frame rate. `max_frames_in_flight` is the bound; the
runtime knob is the setting worth scaling with a latency slider.

```lua
renderer.framePacing()            --> { framesInFlight = 2, maxFramesInFlight = 2, mechanism = "submission-wait", … }
renderer.setMaxFramesInFlight(1)  --> 1    lowest latency, least CPU/GPU overlap
renderer.setPresentMode("mailbox")--> "mailbox"
```

`mechanism` names how the bound is enforced where the engine is running.
`submission-wait` waits for the frame that many frames back, so a paced frame
costs latency and still draws. `submitted-work-done` counts outstanding frames
off the queue's own completion signal and declines to start a frame while the
bound is met — the remedy on a platform that cannot block — counting those in
`pacedFrames` and leaving the last presented image up until the GPU catches up.
Either way `framesInFlight` is taken from the queue's completion signal and
stays at or under `maxFramesInFlight`, so it reads under the bound while the
device keeps up and at it while the device is behind — and the backlog a slow
frame can build is bounded by a number you set.

`setPresentMode` answers the canonical spelling of what you asked for; read
`renderer.framePacing().presentMode` for the mode the surface took. Each of the
two settings is seeded on its own, so editing one of them in the file leaves a
runtime override of the other in force.

`present_mode` decides how a finished frame reaches the display: `fifo` queues
every frame and shows it on a vertical blank (never tears, never drops one),
`mailbox` replaces the queued frame with the newest, `immediate` presents as
soon as the frame is ready and can tear, `fifo_relaxed` tears rather than stall
when a frame misses its blank, and `auto_vsync` / `auto_no_vsync` leave the
choice to the backend. On a desktop the default is `mailbox`: a frame that runs
past the vblank budget then shows its real rate, where `fifo` would lock it to a
divisor of the refresh rate (a 7ms frame on a 144Hz display presents at a hard
72). On Android the default is `fifo`, so every frame lands on a vsync of the
panel and the engine makes no frame the panel never shows, which a phone would
pay for in battery and heat. A surface that does not offer the mode presents
`fifo`, so read `renderer.framePacing().presentMode` for what took effect and
`.presentModes` for what the surface offers.

**The frame-rate cap** holds the whole frame loop (scripts, physics, audio,
networking and drawing) to at most a rate content chooses. It is the setting a
game picks for battery and heat on a phone, or for a steady pace on any display.

```lua
renderer.setFrameRateCap(30)      --> 30
renderer.framePacing()            --> { displayRefreshHz = 120, frameRateCap = 30, refreshesPerFrame = 4, targetFps = 30, deliveredFps = 30.0, … }
renderer.setFrameRateCap(nil)     --> nil   every frame the display shows
```

Frames come due on an even grid. Where the display's refresh rate is known (a
phone's panel, a desktop window's monitor, a browser's animation frames) the
grid is a whole number of refreshes, the fewest whose rate stays inside the
cap, so each frame stays on screen for the same number of vsyncs: 30 on a
120 Hz panel is every 4th refresh, 60 every 2nd, and a cap the refresh rate
does not divide evenly delivers the next rate below it (50 on 120 Hz delivers
40). `targetFps` is what the cap delivers on this display and `deliveredFps`
what the loop actually ran at. On Android the system compositor is told the
rate too, so a panel with more than one refresh rate can run at one the cap
divides; with no cap it is told the panel's highest rate, so a 120 Hz phone
runs at 120. A headless engine presents to no display and holds the cap at its
own rate. `frame_rate_cap` under `[render]` in `.world_settings` seeds it, so a
world states its cap once and every session opens under it, and a runtime call
stays in force until that value changes. The `perf` reading an `execute` response carries judges
a capped frame against the cap's interval: under a cap of 30 a 33 ms frame is on
time, and one a refresh past its interval reads as slow.

**The pipeline cache** is the driver's compiled code for the pipelines this
renderer builds, kept across runs. A pipeline is machine code the GPU driver
compiles from the shader bound into it, and every launch pays that compile
before the first frame drawing with that pipeline can appear. The engine hands
each build the store, and writes the store back once a burst of builds settles —
and again as the engine closes, so a short run keeps what it compiled too. The
next launch of an unchanged shader reads back what the last one compiled.

```lua
renderer.pipelineCache()  --> { supported = true, pipelinesBuilt = 41, buildMs = 812.4, restoredBytes = 1_620_324, … }
```

`pipelinesBuilt` and `buildMs` are what this session built and what it cost —
the number the store lowers — and `restoredBytes` is what a previous run left
for this GPU. `path` names the file, which carries the GPU's identity, so a
machine with two adapters keeps two; the driver validates the blob it is handed
before trusting it, so one left by a driver that has since been updated is
rejected and that launch compiles from source. `saves`, `savedBytes` and
`dirty` describe writing it back — `dirty` stays true while the file is behind
what the driver holds, including after a write that failed, which `lastError`
then names — a failed write costs the stored compile and never the frame.
`supported` is false where the platform holds no store a program can carry — a
browser keeps its own and hands none out — and `reason` says which; the build
count and timing still read true there.

A pipeline a material, the sky, a depth view, a post-process pass or a compute
program a script registers needs after boot is built **off the frame thread** —
the shader module behind that compute program with it — on a pool of worker
threads on native (`buildWorkers` says how many), behind the browser's
asynchronous pipeline entry point on the web. So a burst of new materials or
post-process effects costs a frame nothing beyond drawing the placeholder until
each pipeline lands, and a burst of effects costs it nothing beyond each effect
starting on the frame its pipeline does; a dispatch issued before its program
arrives is held over and runs then, and `compute.programState` reads `pending`
until it does.
`offFrameBuilt` and `offFrameBuildMs` are the pipelines and the milliseconds
that never held a frame. `buildsSubmitted`, `buildsFinished`, `buildsFailed`
and `buildsInstalled` count every module and pipeline request made,
completed, refused and taken back by a frame, so a queue that is draining
reads differently from one that is stuck. `buildsAbandoned` counts the
requests a renderer still held when a device rebuild let it go, which no frame
takes back, and `buildsPending` is what the renderer drawing now has yet to
take back, a finished result included until a frame installs it. Every
request made is installed, abandoned or pending, so a rebuilt device leaves
nothing behind in `buildsPending`, and it settles to zero with
`renderer.pendingWork().pipelineBuilds`. A material whose pipelines are still
building stays as it was drawing until the last of them lands, and a pipeline
the driver refuses is reported once against the shader, the way a compile
error is.

**What the renderer still owes the picture** is its own reading. A frame draws
what the renderer holds at the moment it runs, and several of the things it
holds arrive over more than one frame: a pipeline is bound the frame its build
is taken in, a texture is uploaded under a per-frame time budget, a released
texture's GPU object is freed on the frame the release queue is drained, a
streamable texture climbs toward the level its screen size asks for one level at
a time.

The texture upload budget is 3 ms of uploading per frame, read after each
upload: a frame always uploads the texture at the front of the queue, and stops
taking more once the uploads it made have spent the budget. So a screen that
shows several large images for the first time, or a batch of
`renderer.texture.create` calls in one step, lands its textures over the next
few frames rather than in one long frame, in the order they were made. Until its
texture lands, a UI image naming it draws its loading state and a material slot
naming it samples the slot's fallback; both pick the texture up on the frame it
arrives.
The queue holds one upload per texture: a `renderer.texture.update` of a
texture still waiting replaces the pixels it will land with, and keeps its
place, and a `renderer.destroy` of one takes its upload out of the queue.
`textureUploads` counts the textures still queued.

```lua
renderer.pendingWork()  --> { settled = true, outstanding = 0, pipelineBuilds = 0, textureUploads = 0, textureReleases = 0, textureResidency = 0, epoch = 41, frame = 9120 }
```

`settled` is true when nothing is outstanding, and means the frames after this
one draw the same picture unless something changes the scene — the word a
measurement of a rendered frame holds for before it takes one. `epoch` counts
the times the renderer has gone from owing nothing to owing something, which is
what a PAIR of readings needs: a run of work that begins and ends between two
samples leaves both of them reading `settled` and leaves the epoch higher, so
`settled` at both ends under one epoch says nothing arrived across the whole
span. `Test.settleReading` and `Test.readWhileRendererIsStill` are built on
exactly that pair of conditions, so a suite measuring a picture reads one the
renderer has finished putting up.

Per-camera overrides live on the **Camera component** — `debugChannel`, `postProcessing` (whether that camera runs the post-process chain, and the render-feature passes that declared themselves part of the film with it), and the `renderLayers` spec (a space-separated list of layer names, e.g. `"all !ui"`, where `ui` / `sky` / `debug` / `EditorUI` are built-in layers) decide what that camera draws (the render-textures guide uses this for secondary views).

That spec is one half of the render-layer system; the other half is which
entities are on each layer, written with `renderLayer.set` — `{ tree = true }`
puts an entity and every descendant on a layer in one call, and the reply counts
what it moved. The **render-layers guide** covers both sides.
