import { clampResolution, motionResolution } from './renderResolution.js' import { frameIntervalP95, isContinuousGap, stepAdaptiveResolution } from './adaptiveResolution.js' /** One owner combines motion caps and adaptive decisions. Static views always use base quality. * applyScale checks the real renderer value; decisions commit even when no physical write is needed. * No viewer/Cesium dependency, and no wall-clock sleeps in tests. */ export function createRenderResolutionController({ applyScale, now, options = {} }) { let base, movingCap, floor, enabled, moving = false, adaptiveTarget let intervals = [], lastSampleAt = null, lastEvaluationAt = -Infinity let hotSinceMs = null, coldSinceMs = null, p95Ms = null, writes = 0 const clearSamples = () => { intervals = []; lastSampleAt = null; lastEvaluationAt = -Infinity hotSinceMs = coldSinceMs = p95Ms = null } const readOptions = values => { base = clampResolution(values.resolutionScale) ?? .75 movingCap = motionResolution({ base, moving: values.movingResolutionScale ?? .6, isMoving: true }) floor = Math.min(movingCap, clampResolution(values.adaptiveFloor) ?? .55) enabled = values.adaptiveResolution !== false adaptiveTarget = movingCap } const effectiveTarget = () => moving ? Math.min(movingCap, adaptiveTarget) : base const apply = () => { const changed = !!applyScale(effectiveTarget()) if (changed) writes++ return changed } readOptions(options) return { configure(values = {}) { readOptions(values); clearSamples(); return apply() }, setMoving(value) { if (moving !== (value === true)) { moving = value === true; clearSamples() } return apply() }, noteFrameInterval(value) { if (!moving || !enabled || !isContinuousGap(value)) return false intervals.push(Number(value)); if (intervals.length > 90) intervals.shift() lastSampleAt = now() return true }, tick() { if (!moving || !enabled) return false const at = now() if (lastSampleAt === null || at - lastSampleAt > 1500) { clearSamples(); return false } if (at - lastEvaluationAt < 500) return false p95Ms = frameIntervalP95(intervals) if (p95Ms === null) return false lastEvaluationAt = at const step = stepAdaptiveResolution({ p95Ms, currentScale: Math.min(movingCap, adaptiveTarget), baseScale: movingCap, floorScale: floor, nowMs: at, hotSinceMs, coldSinceMs }) hotSinceMs = step.hotSinceMs; coldSinceMs = step.coldSinceMs // State progresses even if motion already wrote the same target to the framebuffer. adaptiveTarget = step.scale return step.changed ? apply() : false }, status() { return { scale: effectiveTarget(), adaptiveTarget, moving, baseScale: base, movingCap, floorScale: floor, enabled, samples: intervals.length, p95Ms, writes } }, } }