Log inGet started

Scene-wide renderer settings

Updated October 16, 2026

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):

[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.

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.

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.

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.

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.

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.

  • documentation
  • guide
  • rendering