Luxar Viewer API Documentation - v2026.9.22
    Preparing search index...

    Manages adaptive pixel ratio for performance optimization.

    Index
    renderer: DPRRenderer | null = null
    FPS_SAMPLE_WINDOW_MS: 1000
    fpsTracker: FPSTracker = ...
    currentDPR: number
    lastSeenNativeDPR: number
    lastSeenCap: number
    isEnabled: boolean
    lastEvaluationTime: number = 0
    isReducedResolution: boolean = false
    stallDetector: StallDetector
    nonStallIntervals: number = 0
    probeController: ProbeController
    boundsLedger: BoundsLedger
    hysteresis: HysteresisTracker
    refreshRateEstimator: RefreshRateEstimator
    onDPRChange: DPRChangeCallback | null = null
    loadActivityPredicate: (() => boolean) | null = null
    lastSuppressedBy: SuppressionCause = null
    scaleDownCount: number = 0
    scaleUpCount: number = 0
    probeRecords: AdaptiveDPRProbeRecord[] = []
    lastContentChangeAt: number | null = null
    restingAtCeiling: boolean = false
    lastOperatingDPR: number | null = null
    pinned: boolean = false
    • The 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.

      Returns number

    • Detect 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:

      • Tracking the ceiling (no reduction/manual value engaged): follow the new ceiling.
      • Explicitly reduced/manual: keep the value, clamped to the new ceiling, so a move to a lower-DPI monitor never leaves a supersampling override behind (the pre-fix failure mode: a 2.0 override kept rendering 4x the pixels on a 1x monitor).

      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.

      Returns number

      the live native DPR

    • Record frame timestamp for FPS calculation Should be called once per frame from the animation loop

      Parameters

      • timestamp: number

        Current timestamp from performance.now()

      Returns void

    • Evaluate current performance and adjust DPR if needed.

      Parameters

      • timestamp: number

      Returns boolean

      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).

    • Apply 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.

      Parameters

      • verdict: NonNullable<ProbeVerdict | null>
      • timestamp: number
      • currentFPS: number

      Returns void

    • Settle 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:

      • A per-frame LOD swap to a FINER (heavier) level reads as 'rejected'. Reverting would push the DPR back UP — adding pixels to a scene that just got heavier — and pin a floor for the full 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.
      • A swap to a COARSER (faster) level reads as 'accepted', crediting the LOD coarsening to the DPR step and wiping the rejection-backoff streak that quiets scenes DPR reduction cannot help.

      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.

      Parameters

      • verdict:
            | { kind: "accepted"; probe: PendingProbe; fpsRatio: number }
            | { kind: "rejected"; probe: PendingProbe; fpsRatio: number }

      Returns void

    • Scale 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.

      Parameters

      Returns void

    • Apply 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).

      Parameters

      • fps: number
      • Optionalreason: string

        Optional log line override; default describes the punished-ascent path.

      Returns void

    • Parameters

      • verdict:
            | { kind: "accepted"; probe: PendingProbe; fpsRatio: number }
            | { kind: "rejected"; probe: PendingProbe; fpsRatio: number }
            | { kind: "inconclusive"; probe: PendingProbe }
      • currentFPS: number
      • timestamp: number

      Returns void

    • Get 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).

      Returns number

    • Get the native DPR (live window.devicePixelRatio, rebasing internal state if the display changed since the last read).

      Returns number

    • Set manual DPR value (only works when adaptive is disabled)

      Parameters

      • dpr: number

        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)

      Returns void

    • 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:

      • Turning it ON while ADAPTIVE: jump straight to the new ceiling. The user asked to see HiDPI; if the scene cannot sustain it the loop walks back down within a probe window, which is honest feedback rather than a silent no-op.
      • Turning it ON while MANUAL: widen the range only, leave the chosen DPR alone. In manual mode the user's number is authoritative and must not be moved out from under them.
      • Turning it OFF: clamp down at once in both modes. A cap that does not bind immediately is not a cap.

      No-op while pinned, mirroring setEnabled — a ?dpr= session is deliberately immune to persisted per-scene settings.

      Parameters

      • allowed: boolean

      Returns void

    • 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.

      Parameters

      • dpr: number

        Device pixel ratio to pin

      Returns void

    • 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.

      Parameters

      • predicate: (() => boolean) | null

      Returns void

    • 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).

      Returns void

    • 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.

      Returns boolean

      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.

      Returns void

    • 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.

      Parameters

      • timestamp: number = ...

        Caller-supplied clock for tests; defaults to performance.now(), the same clock the frame loop feeds.

      Returns void