Source code for simvx.core.graphics_quality

"""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