Graphics quality on phones and desktops: the device's quality tier
A world is authored on one machine and played on every other. The engine
assigns each device a quality tier at boot, "low", "medium" or
"high", from what the device states about itself: its GPU family and limits,
its memory, and its display (a phone or a touch browser states a density; a
desktop window states none). Every renderer default a world's settings leave
unstated comes from that tier, so a world that never mentions phones still
plays on one at the phone's defaults.
local q = renderer.qualityTier()
print(q.tier, q.reason)
-- medium Mali-G78 is a mali GPU of the medium class; 7.0 GiB of memory
print(q.defaults.resolutionScale, q.defaults.anisotropy)
| Default | high | medium | low |
|---|---|---|---|
| Texture budget | the device's own | at most 768 MiB | at most 384 MiB |
Anisotropy (render.anisotropy) | 16 | 8 | 4 |
| Scene on a handheld display | 2.25 px per point, 2.4 Mpx at most | 2.0 px per point, 1.3 Mpx | 1.75 px per point, 0.8 Mpx |
| Scene on a desktop display | every pixel | 1.3 Mpx at most | 0.8 Mpx at most |
A desktop, and any device whose GPU is outside the handheld families, is
"high": the renderer exactly as a desktop has always run it, one scene pixel
per display pixel. A handheld starts at the tier its GPU's generation affords
(Adreno 7xx, Immortalis and Mali-G7xx are high; Adreno 6xx and Mali-G57 to G78
medium; PowerVR, Mali-G52 and older low) and is held down by its memory (under
about 4.5 GiB is low, under 6.5 GiB medium) and by a GPU that cannot hold an
8192-texel texture.
The scene on a dense phone screen. A phone panel packs far more pixels than
a 3D scene shows at arm's length, and shading cost is pixel count. On a
handheld the tier renders the scene at its own density within its pixel budget
(renderer.resolutionScale() reads below 1 on a 1080x2400 phone) under
renderer.setResolutionScaleMode("reconstruct"), so the UI and text compose at
the display's full resolution on top of the upscaled scene. A desktop renders
every pixel at the high tier it is assigned; a lower tier chosen on one (a
player's "low graphics") renders the scene within that tier's pixel budget the
same way, so renderer.setQualityTier alone is a whole graphics setting on
every platform.
Overriding. Three levels, each standing over the one before:
- The world's settings: a
render.*key the world's settings state is used on every device, whatever its tier. renderer.setQualityTier("low" | "medium" | "high")chooses another tier for the process (a settings screen offering the player a quality choice is this call);renderer.setQualityTier()returns to the device's own. The choice holds across scene loads, and a change of tier applies the new tier's defaults at the call, replacing what an earlier renderer call set for them; choosing the tier already in force changes no setting.- A single renderer call (
setResolutionScale,setTextureBudget,setAnisotropy) overrides that one default until the tier changes again, including a call made in the same script right aftersetQualityTier. AsetTextureBudgetbudget stands over the tier's texture cap untilsetTextureBudget()returns it to the device's own.
The engine log states the tier and every default it chose at boot, and again
whenever the tier changes ([quality] lines).
A graphics setting players choose
A settings panel offering "Low / Medium / High" needs one call per choice; the tier carries every default with it, the render scale included, on a phone and on a desktop alike:
-- A button's handler. The choice holds for the process, across scene loads
-- and reloads, so nothing has to re-apply it when a scene loads.
local function choose(tier: string?)
local q = renderer.setQualityTier(tier) -- nil: back to the device's own
print(q.tier, q.defaults.resolutionScale, q.defaults.anisotropy)
end
renderer.qualityTier().tier is the choice in force (the one to highlight in
the panel) and deviceTier the device's own. A choice that should outlast the
process (the next launch of the app) is the world's to store, in the player's
runtime_data, and to hand back to renderer.setQualityTier at startup.
Seeing what it costs
renderer.renderTargets() and renderer.shadowMemory() name the memory the
tier's defaults buy, and renderer.viewportSize() / renderer.compositeSize()
the scene raster and the display it is carried up to. Read them before and
after renderer.setQualityTier to see what a tier spends on this device.