"""GraphicsQuality: the authoritative quality-tier table (design D13, RM-D3).
One string dial, ``WorldEnvironment.quality_tier``, selects a tier; each named
tier resolves to a :class:`QualitySettings` record of dials through the single
``TIER_TABLE`` below. Both backends resolve through this table (the env-sync
spec applies it inside ``apply_spec``), so a tier means the same thing on
Vulkan desktop and WebGPU web.
Tier semantics:
* ``"auto"`` (the default): detect-default. No table lookup; each backend
keeps its own detection (the web runtime resolves a tier from the WebGPU
capability probe in ``quality_tier.js``, desktop runs at its built-in
defaults). The per-dial ``WorldEnvironment`` Properties still apply exactly
as before, so the default path is byte-identical to the pre-tier engine.
* ``"low"`` / ``"medium"`` / ``"high"`` / ``"ultra"``: the table drives every
wired dial. A per-dial Property explicitly moved OFF its default still wins
over the tier value (an escape hatch for one-dial tweaks); use ``"custom"``
to drive everything by hand. ``"high"`` is pinned to the engine defaults
(``QualitySettings()``), so selecting it renders identically to ``"auto"``
on desktop.
* ``"custom"``: pass-through. The table is never consulted; the per-dial
Properties are the whole story (same wiring as ``"auto"``, minus any future
auto-detection).
Wired dials today (each rides its existing env-sync row and transition path:
resize/drain on desktop, tier-invalidation on web): ``render_scale`` (design
D15, RM-D1/D2), ``probe_blend_count`` + ``probe_face_size`` (RM-B5). The
remaining dials are declared here so later work packages (D5 debug views,
E4 SSR, E7 FFT ocean, volumetric quality) consume the same table instead of
growing their own; they change nothing until a consumer lands.
"""
from dataclasses import dataclass
from typing import Any
__all__ = [
"QUALITY_TIERS",
"QualitySettings",
"TIER_TABLE",
"resolve_tier",
"tier_dial_overrides",
]
# The named tiers, weakest first. "auto" and "custom" are tier-SELECTOR values
# on WorldEnvironment.quality_tier, not tiers: neither resolves to a record.
QUALITY_TIERS: tuple[str, ...] = ("low", "medium", "high", "ultra")
[docs]
@dataclass(frozen=True, slots=True)
class QualitySettings:
"""One tier's dial record. Field defaults ARE the engine defaults (the
``"high"`` tier), so ``QualitySettings()`` always names today's look.
``ssr_mode`` / ``ssao_mode`` / ``volumetric_quality`` are CEILINGS on the
matching user-enabled feature (like ``WorldEnvironment.ambient_mode``): a
tier never switches a feature on, it caps how expensively it runs.
"""
# Main-view HDR-chain resolution scale (design D15). 1.0 = native.
render_scale: float = 1.0
# Directional CSM shadow-map resolution per cascade (future consumer;
# desktop currently fixes shadow_pass.SHADOW_MAP_SIZE = 2048).
shadow_map_size: int = 2048
# Reflection probes (RM-B5): top-K per-fragment blend width (0 disables
# probe capture + blending entirely) and per-face capture resolution.
probe_blend_count: int = 2
probe_face_size: int = 128
# Screen-space reflections ceiling (design D7, lands in E4):
# "off" | "half" (half-res trace) | "full".
ssr_mode: str = "half"
# SSAO ceiling: "off" | "on" | "hbao" (future consumer; today SSAO is the
# user's ssao_enabled toggle at one quality).
ssao_mode: str = "on"
# Screen-space global illumination ceiling (design D8, RM-E8): "off" | "on".
# SSGI is an expensive half-res hemisphere trace, so only "ultra" enables it
# by tier; "auto"/"custom" leave it to the per-env ``ssgi_enabled`` toggle.
ssgi_mode: str = "off"
# FFT ocean (design D11, lands in E7): cascade texture size + count.
ocean_size: int = 256
ocean_cascades: int = 3
# Volumetric-fog march quality ceiling (future consumer): "low" | "medium" | "high".
volumetric_quality: str = "high"
# Temporal upsampling (design D15, RM-D4): whether the TAA resolve may
# upgrade the bilinear upscale when render_scale < 1 (requires taa_enabled).
taau: bool = True
# THE tier table (design D13). "high" is the engine-default look by
# construction (QualitySettings() defaults); the golden suite pins it.
TIER_TABLE: dict[str, QualitySettings] = {
"low": QualitySettings(
render_scale=0.5,
shadow_map_size=1024,
probe_blend_count=0,
probe_face_size=64,
ssr_mode="off",
ssao_mode="off",
ocean_size=128,
ocean_cascades=2,
volumetric_quality="low",
taau=False,
),
"medium": QualitySettings(
render_scale=1.0,
shadow_map_size=1024,
probe_blend_count=2,
probe_face_size=64,
ssr_mode="off",
ssao_mode="off",
ocean_size=256,
ocean_cascades=2,
volumetric_quality="medium",
taau=True,
),
"high": QualitySettings(),
"ultra": QualitySettings(
render_scale=1.0,
shadow_map_size=4096,
probe_blend_count=8,
probe_face_size=128,
ssr_mode="full",
ssao_mode="hbao",
ssgi_mode="on",
ocean_size=256,
ocean_cascades=3,
volumetric_quality="high",
taau=True,
),
}
# WorldEnvironment Property defaults for the dials the tier table drives
# TODAY. A dial sitting at its default takes the tier value; a dial the user
# explicitly moved wins over the tier. test_graphics_quality.py asserts this
# map matches the live WorldEnvironment Property defaults, so a default change
# there cannot silently desync the tier resolution.
_WIRED_DIAL_DEFAULTS: dict[str, Any] = {
"render_scale": 1.0,
"probe_blend_count": None,
"probe_face_size": None,
}
_NO_OVERRIDES: dict[str, Any] = {}
[docs]
def resolve_tier(tier: str) -> QualitySettings | None:
"""The :class:`QualitySettings` for a named tier, else ``None``.
``"auto"`` and ``"custom"`` (and anything unknown: the Property enum on
``WorldEnvironment.quality_tier`` already rejects those) resolve to
``None`` = "no table, per-dial Properties / backend detection decide".
"""
return TIER_TABLE.get(tier)
[docs]
def tier_dial_overrides(env: Any) -> dict[str, Any]:
"""Env-attr -> effective value for the wired dials of *env*'s tier.
Empty for ``"auto"`` / ``"custom"`` (pass-through: the env-sync spec reads
the per-dial Properties directly, exactly the pre-tier behaviour, so the
default path costs one dict lookup). For a named tier, every wired dial
still at its Property default maps to the tier's value; explicitly-set
dials are omitted so the Property wins. ``apply_spec`` consults this on
both backends: the ONE resolution point of design D13.
"""
settings = TIER_TABLE.get(getattr(env, "quality_tier", "auto"))
if settings is None:
return _NO_OVERRIDES
overrides: dict[str, Any] = {}
for dial, default in _WIRED_DIAL_DEFAULTS.items():
if getattr(env, dial, default) == default:
overrides[dial] = getattr(settings, dial)
return overrides