| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- /**
- * 自适应渲染分辨率(票 61 第十九轮:以最优帧率为目标、画质只在需要时让位)。
- *
- * ## 思路
- *
- * 维护者要「最优帧率,同时保持当前质量」。固定降档(0.75 / 移动 0.6)是**一刀切**:
- * 快机器白让了画质,慢机器又可能不够。自适应的做法是**按实测帧间隔动态微调**:
- *
- * · 帧间隔 p95 持续偏慢(掉帧)→ 分辨率降一档(像素量立刻少 ~10%);
- * · 持续偏快(有富余)→ 分辨率升一档,回到画质上限;
- * · 中间地带不动 —— 避免在阈值附近来回抖。
- *
- * 画质上限 = 部署配置的基准档(默认 0.75),下限 0.55:只有机器真的撑不住时才让位,
- * 且让得有限 —— 这就是「保持当前质量」的量化表达。
- *
- * 纯函数:输入窗口统计与当前档位,输出下一步档位与计时器,脱浏览器可断言。
- */
- /** 帧间隔窗口的分位数(p95)—— 与 DEV 面板的 movingFrameIntervals 同口径。 */
- export function percentile(sorted, p) {
- if (!Array.isArray(sorted) || !sorted.length) return null
- const list = [...sorted].sort((a, b) => a - b)
- const index = Math.min(list.length - 1, Math.floor(list.length * p))
- return list[index]
- }
- /**
- * 推进一步自适应决策。
- *
- * @param {object} state
- * @param {number} state.p95Ms 最近窗口的帧间隔 p95(毫秒)
- * @param {number} state.currentScale 当前内部分辨率
- * @param {number} state.baseScale 画质上限(部署基准档)
- * @param {number} [state.floorScale] 画质下限(默认 0.55)
- * @param {number} state.nowMs 当前时刻
- * @param {number|null} state.hotSinceMs 「持续偏慢」计时起点(null = 未在累计)
- * @param {number|null} state.coldSinceMs 「持续偏快」计时起点
- * @param {number} [state.hotHoldMs] 偏慢要持续多久才降档(默认 1500)
- * @param {number} [state.coldHoldMs] 偏快要持续多久才升档(默认 4000,慢于降档防抖动)
- * @param {number} [state.step] 每档步长(默认 0.05)
- * @param {number} [state.hotMs] 「偏慢」阈值(默认 21ms ≈ 48fps)
- * @param {number} [state.coldMs] 「偏快」阈值(默认 16.7ms = 60fps)
- * @returns {{scale:number, hotSinceMs:(number|null), coldSinceMs:(number|null), changed:boolean}}
- */
- export function stepAdaptiveResolution(state = {}) {
- const base = Number(state.baseScale)
- const current = Number(state.currentScale)
- const floor = Number.isFinite(Number(state.floorScale)) ? Number(state.floorScale) : 0.55
- const step = Number.isFinite(Number(state.step)) ? Number(state.step) : 0.05
- const hotMs = Number.isFinite(Number(state.hotMs)) ? Number(state.hotMs) : 21
- const coldMs = Number.isFinite(Number(state.coldMs)) ? Number(state.coldMs) : 16.7
- const hotHoldMs = Number.isFinite(Number(state.hotHoldMs)) ? Number(state.hotHoldMs) : 1500
- const coldHoldMs = Number.isFinite(Number(state.coldHoldMs)) ? Number(state.coldHoldMs) : 4000
- const nowMs = Number(state.nowMs)
- const p95 = Number(state.p95Ms)
- // 基础校验:任何关键量非法都「什么都不做」(宁可不动,不猜一个档位)。
- if (!Number.isFinite(base) || !Number.isFinite(current) || !Number.isFinite(nowMs) || !Number.isFinite(p95)) {
- return { scale: current, hotSinceMs: state.hotSinceMs ?? null, coldSinceMs: state.coldSinceMs ?? null, changed: false }
- }
- const clampUp = (v) => Math.min(base, Math.max(floor, v))
- let { hotSinceMs = null, coldSinceMs = null } = state
- if (p95 > hotMs) {
- // 偏慢:累计「降档观察期」,偏快计时清零(方向相反的两个计时不能同时走)。
- coldSinceMs = null
- if (hotSinceMs === null) hotSinceMs = nowMs
- if (nowMs - hotSinceMs >= hotHoldMs) {
- const next = clampUp(current - step)
- const changed = Math.abs(next - current) > 1e-6
- return { scale: next, hotSinceMs: null, coldSinceMs: null, changed }
- }
- } else if (p95 < coldMs) {
- hotSinceMs = null
- if (coldSinceMs === null) coldSinceMs = nowMs
- if (nowMs - coldSinceMs >= coldHoldMs) {
- const next = clampUp(current + step)
- const changed = Math.abs(next - current) > 1e-6
- return { scale: next, hotSinceMs: null, coldSinceMs: null, changed }
- }
- } else {
- // 中间地带(coldMs..hotMs):两个方向都不累计 —— 这是防抖带,不许在这个区间里来回切。
- hotSinceMs = null
- coldSinceMs = null
- }
- return { scale: current, hotSinceMs, coldSinceMs, changed: false }
- }
- /**
- * 从一组帧间隔(毫秒)算出 p95;样本不足返回 null(调用方据此跳过本轮决策)。
- */
- export function frameIntervalP95(intervalsMs, minSamples = 30) {
- if (!Array.isArray(intervalsMs) || intervalsMs.length < minSamples) return null
- return percentile(intervalsMs.filter((n) => Number.isFinite(n) && n > 0), 0.95)
- }
|