You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/game/ThermalGovernor.ts

192 lines
7.0 KiB

3 weeks ago
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);
}
}
}