renderResolutionController.js 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. import { clampResolution, motionResolution } from './renderResolution.js'
  2. import { frameIntervalP95, isContinuousGap, stepAdaptiveResolution } from './adaptiveResolution.js'
  3. /** One owner combines motion caps and adaptive decisions. Static views always use base quality.
  4. * applyScale checks the real renderer value; decisions commit even when no physical write is needed.
  5. * No viewer/Cesium dependency, and no wall-clock sleeps in tests.
  6. */
  7. export function createRenderResolutionController({ applyScale, now, options = {} }) {
  8. let base, movingCap, floor, enabled, moving = false, adaptiveTarget
  9. let intervals = [], lastSampleAt = null, lastEvaluationAt = -Infinity
  10. let hotSinceMs = null, coldSinceMs = null, p95Ms = null, writes = 0
  11. const clearSamples = () => {
  12. intervals = []; lastSampleAt = null; lastEvaluationAt = -Infinity
  13. hotSinceMs = coldSinceMs = p95Ms = null
  14. }
  15. const readOptions = values => {
  16. base = clampResolution(values.resolutionScale) ?? .75
  17. movingCap = motionResolution({ base, moving: values.movingResolutionScale ?? .6, isMoving: true })
  18. floor = Math.min(movingCap, clampResolution(values.adaptiveFloor) ?? .55)
  19. enabled = values.adaptiveResolution !== false
  20. adaptiveTarget = movingCap
  21. }
  22. const effectiveTarget = () => moving ? Math.min(movingCap, adaptiveTarget) : base
  23. const apply = () => {
  24. const changed = !!applyScale(effectiveTarget())
  25. if (changed) writes++
  26. return changed
  27. }
  28. readOptions(options)
  29. return {
  30. configure(values = {}) { readOptions(values); clearSamples(); return apply() },
  31. setMoving(value) {
  32. if (moving !== (value === true)) { moving = value === true; clearSamples() }
  33. return apply()
  34. },
  35. noteFrameInterval(value) {
  36. if (!moving || !enabled || !isContinuousGap(value)) return false
  37. intervals.push(Number(value)); if (intervals.length > 90) intervals.shift()
  38. lastSampleAt = now()
  39. return true
  40. },
  41. tick() {
  42. if (!moving || !enabled) return false
  43. const at = now()
  44. if (lastSampleAt === null || at - lastSampleAt > 1500) { clearSamples(); return false }
  45. if (at - lastEvaluationAt < 500) return false
  46. p95Ms = frameIntervalP95(intervals)
  47. if (p95Ms === null) return false
  48. lastEvaluationAt = at
  49. const step = stepAdaptiveResolution({ p95Ms, currentScale: Math.min(movingCap, adaptiveTarget), baseScale: movingCap, floorScale: floor, nowMs: at, hotSinceMs, coldSinceMs })
  50. hotSinceMs = step.hotSinceMs; coldSinceMs = step.coldSinceMs
  51. // State progresses even if motion already wrote the same target to the framebuffer.
  52. adaptiveTarget = step.scale
  53. return step.changed ? apply() : false
  54. },
  55. status() { return { scale: effectiveTarget(), adaptiveTarget, moving, baseScale: base, movingCap, floorScale: floor, enabled, samples: intervals.length, p95Ms, writes } },
  56. }
  57. }