Builtin Library
readme.md
temporalUpscale system
Render fewer pixels than the display has, and get the display's pixels anyway.
Shading cost scales with pixel count, so rasterizing the scene smaller is the one lever that buys frame time without taking anything out of the frame. What it spends is sharpness: an upscale invents nothing, and one frame carries only as many distinct samples as the scene had pixels.
This system spends the frames instead. The projection samples a different point inside every pixel each frame, so consecutive frames are different estimates of the same continuous image; the reconstruction carries them forward through the scene's own motion vectors and mixes each new one in. After a handful of frames the standing result holds samples from several different grids — detail no single frame ever rasterized.
Using it
Put TemporalUpscale on any entity in the scene:
entity(id).component.add("TemporalUpscale", { renderScale = 0.5 })
Half the scale is a quarter of the pixels. Every field is live: edit
renderScale in the inspector and the scene rasterizes at the new size from the
next frame. Removing the component puts the render scale back where it was
before the component moved it, so the frame returns to the resolution it had.
The visibility raster
The visibility raster rasterizes through the same sub-pixel offset this pass moves the projection by, so the depth and motion it lays describe the image the resolve shades.
For code that wants it without a component — a settings screen, a benchmark — reach for the module:
local Upscale = require("@builtin::systems.temporalUpscale.temporalUpscale")
Upscale.enable({ renderScale = 0.5, sharpness = 0.35 })
Upscale.get()
Upscale.disable()
Leaving renderScale out of enable leaves the scale where it is, which is what
lets @builtin::systems.renderScale.renderScale keep steering it against a frame
budget while the reconstruction runs.
Where it runs
The reconstruction is a compute pass at afterScenePost — the seam where the
frame has already reached the display's resolution and the UI has not been
composited onto it yet. So the scene is reconstructed and the text over it stays
untouched, drawn once at the display's own size.
Its two accumulation targets are created with screenSpace = "composite", which
is what keeps them the size of that composited image while the scene renders
smaller, and what keeps an offscreen render — an RT camera, a capture — from
clearing the accumulation the way it clears a scene-space scratch target. The
scene's depth and velocity stay at the scene's resolution; the pass maps each
composite pixel back into that grid to read them.
Where the composite lands depends on who presents the viewport. In a runtime
session the renderer presents it, and the upscale to the display's resolution
happens before afterScenePost — so the composite is the display's own size and
the reconstruction resolves onto the display's grid. Under a UI viewport panel
the panel draws the scene target at its own rect, nothing upscales before the
composite, and the two spaces carry the same size: the pass then accumulates at
the render resolution and the panel scales the result. It says so in the log on
the second frame when that is what it finds.
Its relationship to the other temporal pass
@builtin::systems.antiAliasing.temporalAntiAliasing accumulates the same way at
the scene's own resolution. Both own the projection's sample point, so they are
alternatives: run temporal antialiasing when the scene renders at the display's
size, and temporal upsampling when it renders below it. Enabling one while the
other runs logs a warning — the frame carries a single sub-pixel offset, so each
accumulation would be reprojecting against a jitter the other set.
Files
temporalUpscale.package/
TemporalUpscale.component/ -- the settings, authored and persisted
temporalUpscale.module/ -- enable / get / active / disable
taau.renderFeature/ -- the jitter, the two targets, the pass
taau_resolve.computeShader/ -- the reconstruction itself