Create adaptive DPR manager
OptionalcustomConfig: Partial<AdaptiveDPRConfig>
Optional partial config to override defaults
PrivateconfigPrivaterendererPrivate ReadonlyFPS_PrivatefpsPrivatecurrentPrivatelastPrivatelastPrivateisPrivatelastPrivateisPrivatestallPrivatenonPrivateprobePrivateboundsPrivatehysteresisPrivaterefreshPrivateonPrivateloadPrivatelastPrivatescalePrivatescalePrivate ReadonlyprobePrivatelastPrivaterestingPrivatelastPrivatepinnedPrivatereadLive window.devicePixelRatio with the 0/undefined guard.
PrivateceilingThe highest DPR this session may currently render at: the display's
own DPR, capped by the pixel-ratio cap (see
rendering/pixel-ratio-cap — usually 1.0, because high DPR is off by
default).
Every place that used to treat lastSeenNativeDPR as an upper bound
reads this instead. The NATIVE snapshot is still what
syncNativeDPR tracks and rebases on, because a display change is a
real event regardless of the cap; the ceiling is what that snapshot
is allowed to mean for rendering.
Deliberately built from the SNAPSHOT rather than calling
getMaxPixelRatio() directly, so a tick stays internally consistent
with the native value syncNativeDPR resolved at its start.
PrivatesyncDetect a native-DPR change (monitor drag, browser zoom) and rebase.
The U-shape floor, a pending probe, the scale-up hysteresis timer, and the FPS window are all calibrated against absolute DPR values and frame timings of the OLD display, so a native change clears them wholesale. The operating DPR follows one of two rules:
Either way the new DPR is RE-APPLIED. There used to be a fast path here that skipped the apply while tracking, on the grounds that a null override in dpr-policy already follows the live ceiling for free. It was unsound: the manager cannot see the renderer's override, and "currentDPR equals the ceiling" does not imply the renderer is tracking. The property fuzzer produced the counterexample — a reduction to 1.0 applied while the ceiling was 2.0 (so the override is an explicit 1.0), then the ceiling falls to 1.0, then rises to 3.0. The last step reads as "tracking" because currentDPR happens to equal the OLD ceiling, so nothing was applied, and the manager reported 3.0 while the renderer kept drawing at 1.0 until something else forced a resize. The optimization only ever saved one redundant reallocation on a monitor drag — during which the browser fires a resize anyway.
Detection is lazy — evaluation ticks and public reads — which covers browser zoom (resize → interaction → evaluation) and monitor drags on the next activity without a matchMedia listener.
The CAP is watched here too, not just the native DPR, because it has
writers outside this class (a ?dpr= pin, a capture lifting it and
putting it back). Those all re-apply immediately today, but a cap
raised with no follow-up would otherwise leave currentDPR reporting
the old ceiling while a null override silently rendered at the new
one — a divergence the property fuzzer finds in one step.
Watching the cap matters for visibility too: a monitor drag or a zoom fires a window resize, which re-sizes the backbuffer on its own, but nothing fires for a cap change. Without this the new ceiling would have no visible effect until the user happened to resize the window.
the live native DPR
Set the renderer to use for DPR changes Called after SceneManager is initialized to avoid circular dependencies
Record frame timestamp for FPS calculation Should be called once per frame from the animation loop
Current timestamp from performance.now()
Get current FPS from frame timestamps Returns 0 if not enough data to calculate
PrivateevaluateEvaluate current performance and adjust DPR if needed.
true when the tick had a frame rate to judge. False means the FPS window held fewer than two samples — a just-cleared window after a gap reset or a display change — which is NOT an evaluation and must not consume the interval budget (see recordFrame).
PrivateapplyApply a settled probe verdict from the ProbeController.
Accepted: the scale-down helped — keep it, normal decisions resume
next tick. Rejected: revert to the pre-probe DPR and tighten the
ledger floor to the probed value so scale-down skips it until the
TTL expires. timestamp is the frame timestamp that triggered the
settle — used (not performance.now()) for the floor clock so TTL
decay stays consistent with FPS-window timing. A 'pending' verdict
means the probe window is still open: decide nothing this tick.
A CONTENT-CONFOUNDED probe (one kept across a content change because the loop was too slow to re-run the experiment) gets neither treatment: see applyConfoundedVerdict.
PrivateapplySettle a probe whose measurement window a scene-content change ran through (see notifyContentChanged, which keeps such a probe only when the loop is too slow to run a replacement experiment).
Its baseline was measured on the OLD content and its settle sample on the NEW one, so the ratio is confounded in BOTH directions and none of the usual consequences may be applied:
floorTtlMs, after which blocksScaleDownTo makes scale-down
impossible for 30s. Re-armed on every swap, that recreates the
"sits at native DPR forever" symptom in 30-second windows.So a confounded probe teaches NOTHING — exactly like an inconclusive one: KEEP the reduced DPR (fewer pixels never hurt a stuttering loop, which is the defensible half of the experiment), no revert, no floor, no backoff movement.
What this deliberately does NOT do is stop the walk. Under sustained
churn no clean experiment exists, so a reduction with no verdict is
all the loop can do, and walking down is the right distress response;
it is bounded by minDPR and lifts again through the normal scale-up
hysteresis once content settles. A previous revision held the walk
while a churn clock ran, and that mechanism was measured to invert
its own goal — its window was the same 5s constant
notifyContentChanged coalesces on, so churn arriving just slower
than the window walked FURTHER down (to minDPR) than not having it,
while per-frame churn pinned a 0.33fps scene at 1.62 indefinitely.
PrivatescaleScale DPR down for better performance, arming a probe so we
verify the move actually helped (see U-shape comment at the top
of this file). A scale-down taken on an UNREPRESENTATIVE window
applies WITHOUT a probe — the reduction still helps a janky load or
a stuttering loop, but such FPS samples must never become floor
evidence. suppressedBy names which cause it was (see
SuppressionCause) purely so the log line is honest about it.
PrivateapplyApply a just-decided ceiling demotion: clamp the operating DPR to 1.0 in one step (this replaces the tick's multiplicative scale-down — the clamp is usually the larger move).
Optionalreason: string
Optional log line override; default describes the punished-ascent path.
PrivatescaleScale DPR up for better quality
PrivateapplyApply current DPR to renderer
Enable or disable adaptive DPR
Whether to enable adaptive DPR
Check if adaptive DPR is enabled
Register callback for DPR changes Used by UI components to show/hide resolution indicator
Function to call when DPR changes
Get current state for debugging and UI
getState() plus the decision history: last suppression cause, ledger
backoff level, scale counts and the recent probe verdicts. Diagnostics
only — nothing in the controller reads these back.
PrivaterecordGet current DPR value.
Live-consistent: syncs against the current display first, so after a monitor/zoom change the returned value never reports a stale native snapshot (callers like the recording session persist this value and would otherwise install it as a supersampling override).
Get the native DPR (live window.devicePixelRatio, rebasing
internal state if the display changed since the last read).
Set manual DPR value (only works when adaptive is disabled)
Device pixel ratio to set (clamped to 0.25 - the session ceiling, i.e. the display's DPR capped by the allow-high-DPR setting)
Allow or forbid rendering above CSS resolution (DPR 1.0).
The runtime face of renderingControls.defaults.allowHighDPR /
viewer_config.allow_high_dpr. Writes the shared pixel-ratio cap
(see rendering/pixel-ratio-cap) and then re-settles the operating
DPR so the click has a visible effect immediately rather than after
a hysteresis window:
No-op while pinned, mirroring setEnabled — a ?dpr= session is
deliberately immune to persisted per-scene settings.
Whether the viewer may currently render above CSS resolution.
Pin a fixed manual DPR for the whole session (?dpr= URL param).
Disables adaptive mode, applies dpr as a manual DPR, and locks the
enabled state: subsequent setEnabled() / setHighDPRAllowed()
calls (persisted per-scene settings, the Performance toggles, scene
metadata) are ignored for the rest of the session. Intended for
deterministic E2E/visual-regression runs and repros.
An explicit pin RAISES the pixel-ratio cap to the pinned value if
needed, so ?dpr=2 renders at 2 even with high DPR disallowed —
otherwise the seam would silently clamp the pin to 1.0 and the
parameter would look broken. The pin is still bounded by the
display: ?dpr=4 on a 2x screen pins 2.
Device pixel ratio to pin
Check whether the DPR is pinned for this session (?dpr= URL param).
Inject a predicate polled at each evaluation to detect active data loading (decode/upload jank). While it returns true, FPS samples are treated as unrepresentative: scale-downs still apply (fewer pixels help a janky load too) but no probes are armed or settled and the refresh-cap estimator is not fed — load jank must never become learned floor/cap evidence. Pass null to disable.
Notify the manager that the animation loop stopped (idle pause, tab hide, dispose). Clears SESSION state only — the FPS window, the scale-up streak, any in-flight probe (voided unjudged: its before/after comparison would otherwise span the pause and compare workloads minutes apart), and the estimator's sample-stream transients (recent window, uniform-low plateau clock, unconsumed distress latch — see noteSessionInterrupted). LEARNED state (floor, backoff streak, refresh-cap mark, throttle verdict) survives: it is expensive evidence about this scene on this display, and wiping it here would replay a full rejected-probe episode on every interaction burst.
Idempotent and safe after dispose() (stopAnimation is also called from the controller's dispose path).
Restore full quality for the resting frame, just before the loop idle-pauses. The static image the user is about to study should be as sharp as this session allows — reduced DPR only ever traded quality for interaction smoothness, and there is no interaction anymore.
"Full quality" is the session CEILING, not the native DPR: with high DPR disallowed, resting at native would both contradict the setting and pay a 4x-pixel render-target reallocation (plus a visible sharpen/soften pop) on every idle/resume cycle.
Mutates OPERATING state only (never the floor/backoff — see notifyPaused). Remembers the operating DPR so notifyResumed() can snap straight back.
true when the DPR actually changed — the caller must then render one frame, because the resize clears the canvas.
The loop is starting again after an idle rest: snap straight back to the remembered operating DPR in ONE step (clamped to the live ceiling). Without this, every interaction burst after an idle restore would re-discover the reduction reactively — a cascade of scale-downs, probes, and render-target reallocations.
Notify the manager that scene content genuinely changed (dataset
loaded, layers added/removed, LOD level swapped in). Learned
bounds describe the OLD content, so their expiry is pulled forward
to at most contentChangeRecheckMs from now and the rejection
backoff streak resets — a re-probe against the new content is
cheap and justified. Calls are coalesced within
contentChangeRecheckMs so event bursts (per-frame LOD swaps
during a zoom) don't spam the ledger.
Unlike a pause or a display change this is NOT a frame-stream boundary — see the cadence-memory note in the body.
Caller-supplied clock for tests; defaults to
performance.now(), the same clock the frame loop feeds.
Check if currently in reduced resolution mode
Dispose resources
Manages adaptive pixel ratio for performance optimization.