import { Director, director, game } from 'cc'; import { EDITOR } from 'cc/env'; import { LoadScheduler } from '../../mx/module/res/LoadScheduler'; import { ResourcePhase } from '../../mx/module/res/ResourcePhase'; import { ThermalProfile } from './ThermalProfile'; import { PerfReport } from './PerfReport'; /** * 发热治理:后台 IO 预算 + 分相位帧率。 * 大厅保持 60;战斗按 DeviceCapability 档固定 50/40/30(tier3/1/0)。 */ export class ThermalGovernor { private static _installed = false; private static _phase: ResourcePhase = ResourcePhase.Boot; private static _stressLevel = 0; /** 本局战斗已改用稳定档 */ private static _usingSustain = false; /** 本局已两次冲上限失败,不再回升 */ private static _lockedThisBattle = false; /** 本局从上限掉到稳定档的次数 */ private static _failCount = 0; /** 实测低于阈值的累计秒 */ private static _belowSec = 0; /** 稳定档上累计秒(用于尝试回升) */ private static _okSec = 0; /** 帧间隔指数滑动平均 */ private static _emaDt = 1 / 60; /** 切帧率后跳过若干帧,避免 dt 尖峰误判 */ private static _ignoreFrames = 0; /** 上限档滚动 FPS 低于此值则准备切稳定档 */ private static readonly ADAPT_DOWN_FPS = 50; /** 低于阈值需持续的秒数 */ private static readonly ADAPT_DOWN_SEC = 1.5; /** 稳定档上保持多久才尝试回到上限 */ private static readonly ADAPT_UP_SEC = 8; /** 切帧率后忽略的帧数 */ private static readonly ADAPT_IGNORE_FRAMES = 3; /** EMA 新样本权重 */ private static readonly EMA_ALPHA = 0.15; static install(): void { if (this._installed) return; this._installed = true; ThermalProfile.resolve(); director.on(Director.EVENT_AFTER_UPDATE, ThermalGovernor._onAfterUpdate); } static onResourcePhase(phase: ResourcePhase): void { if (!this._installed) this.install(); const wasBattle = this._phase === ResourcePhase.Battle; const nowBattle = phase === ResourcePhase.Battle; if (wasBattle !== nowBattle) { this._resetAdaptive(); } this._phase = phase; this._apply(); } /** MemoryDowngrade D1/D2:收紧后台预载;D2 可把战斗钉在稳定档 */ static applyStressLevel(level: number): void { if (level <= this._stressLevel) return; this._stressLevel = level; if (level >= 2) { this._usingSustain = true; } this._apply(); } static clearStress(): void { if (this._stressLevel === 0) return; this._stressLevel = 0; this._apply(); } /** 进出战斗时复位自适应状态,下一局重新冲击上限 */ private static _resetAdaptive(): void { this._usingSustain = false; this._lockedThisBattle = false; this._failCount = 0; this._belowSec = 0; this._okSec = 0; this._emaDt = 1 / 60; this._ignoreFrames = ThermalGovernor.ADAPT_IGNORE_FRAMES; } /** 战斗中按滚动帧时在上限档与稳定档之间切换 */ private static _onAfterUpdate(): void { if (EDITOR) return; if (ThermalGovernor._ignoreFrames > 0) { ThermalGovernor._ignoreFrames--; return; } if (ThermalGovernor._phase !== ResourcePhase.Battle) return; const cfg = ThermalProfile.current; if (!cfg.enableAdaptiveBattleFrameRate) return; if (cfg.battleFrameRate <= cfg.battleSustainFrameRate) return; if (ThermalGovernor._stressLevel >= 2) return; if (ThermalGovernor._lockedThisBattle) return; const dt = game.deltaTime; if (!(dt > 0) || dt > 0.1) return; const a = ThermalGovernor.EMA_ALPHA; ThermalGovernor._emaDt = ThermalGovernor._emaDt * (1 - a) + dt * a; const fps = 1 / ThermalGovernor._emaDt; if (ThermalGovernor._usingSustain) { ThermalGovernor._okSec += dt; if (ThermalGovernor._okSec >= ThermalGovernor.ADAPT_UP_SEC) { ThermalGovernor._usingSustain = false; ThermalGovernor._okSec = 0; ThermalGovernor._belowSec = 0; ThermalGovernor._ignoreFrames = ThermalGovernor.ADAPT_IGNORE_FRAMES; ThermalGovernor._apply(); } return; } if (fps < ThermalGovernor.ADAPT_DOWN_FPS) { ThermalGovernor._belowSec += dt; if (ThermalGovernor._belowSec >= ThermalGovernor.ADAPT_DOWN_SEC) { ThermalGovernor._usingSustain = true; ThermalGovernor._belowSec = 0; ThermalGovernor._okSec = 0; ThermalGovernor._failCount++; if (ThermalGovernor._failCount >= 2) { ThermalGovernor._lockedThisBattle = true; } ThermalGovernor._ignoreFrames = ThermalGovernor.ADAPT_IGNORE_FRAMES; ThermalGovernor._apply(); } return; } ThermalGovernor._belowSec = 0; } /** 当前相位应使用的目标帧率 */ private static _resolveTargetFps(): number { const cfg = ThermalProfile.current; if (!cfg.enablePhaseFrameRate) { return cfg.battleFrameRate; } switch (this._phase) { case ResourcePhase.Hall: case ResourcePhase.HallPreload: case ResourcePhase.BattleExit: return cfg.hallFrameRate; case ResourcePhase.Boot: return cfg.bootFrameRate; case ResourcePhase.Battle: if (this._usingSustain || (this._stressLevel >= 2 && cfg.enableStressFrameRate)) { return cfg.battleSustainFrameRate; } return cfg.battleFrameRate; default: return cfg.hallFrameRate; } } private static _apply(): void { ThermalProfile.resolve(); const cfg = ThermalProfile.current; const scheduler = LoadScheduler.getInstance(); let loadScale = cfg.hallLoadBudgetScale; if (this._stressLevel >= 1) { loadScale = Math.min(loadScale, cfg.stressLoadBudgetScale); } if (this._phase === ResourcePhase.Battle) { scheduler.setSilentBudgetScale(1); scheduler.setBackgroundBudgetScale(1); } else { scheduler.setSilentBudgetScale(loadScale); scheduler.setBackgroundBudgetScale(loadScale); } if (EDITOR) return; let target = this._resolveTargetFps(); if (this._stressLevel >= 2 && cfg.enableStressFrameRate && this._phase !== ResourcePhase.Battle) { target = Math.min(target, cfg.stressHallFrameRate); } if (game.frameRate !== target) { game.frameRate = target; PerfReport.reportThermalFps(this._phase, target, this._stressLevel); } } }