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.
352 lines
12 KiB
352 lines
12 KiB
|
3 weeks ago
|
import { sys } from 'cc';
|
||
|
|
import { BYTEDANCE, WECHAT } from 'cc/env';
|
||
|
|
import type { MemoryTierId } from './memory-profile.json';
|
||
|
|
import { MiniGamePf } from '../../mx/tools/MiniGameHostInfo';
|
||
|
|
import {
|
||
|
|
douyinDeviceScoreToQuality,
|
||
|
|
memoryMbToQuality,
|
||
|
|
type MiniGameOs,
|
||
|
|
wechatBenchmarkToQuality,
|
||
|
|
wechatModelLevelToQuality,
|
||
|
|
webDeviceMemoryToQuality,
|
||
|
|
} from './device-capability-standards';
|
||
|
|
|
||
|
|
/** 设备综合性能档(画质 / 内存策略共用) */
|
||
|
|
export type DeviceQuality = 'low' | 'medium' | 'high';
|
||
|
|
|
||
|
|
export type CapabilityProbeSource =
|
||
|
|
| 'wx.getDeviceBenchmarkInfo'
|
||
|
|
| 'wx.getDeviceInfo'
|
||
|
|
| 'tt.deviceScore'
|
||
|
|
| 'legacy.getSystemInfoSync'
|
||
|
|
| 'web.deviceMemory'
|
||
|
|
| 'web.hardwareConcurrency'
|
||
|
|
| 'fallback';
|
||
|
|
|
||
|
|
/** 参与分档的运行时信号(不含机型原文,可打日志) */
|
||
|
|
export interface DeviceCapabilitySignals {
|
||
|
|
platform: string;
|
||
|
|
os: string;
|
||
|
|
miniGameOs: MiniGameOs;
|
||
|
|
probeSource: CapabilityProbeSource;
|
||
|
|
/** Chrome / 部分 WebView,单位 GB */
|
||
|
|
deviceMemoryGb?: number;
|
||
|
|
/** 逻辑核数 */
|
||
|
|
hardwareConcurrency?: number;
|
||
|
|
/** 微信 benchmarkLevel(getDeviceBenchmarkInfo 优先) */
|
||
|
|
benchmarkLevel?: number;
|
||
|
|
/** 微信 modelLevel:0 未知,1 高,2 中,3 低 */
|
||
|
|
modelLevel?: number;
|
||
|
|
/** 抖音 deviceScore.overall,0–10 */
|
||
|
|
deviceScoreOverall?: number;
|
||
|
|
/** 物理内存 MB(微信 memorySize 等) */
|
||
|
|
memorySizeMb?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
type PlatformApi = {
|
||
|
|
getDeviceBenchmarkInfo?: (opts: {
|
||
|
|
success?: (res: { benchmarkLevel?: number; modelLevel?: number }) => void;
|
||
|
|
fail?: () => void;
|
||
|
|
}) => void;
|
||
|
|
getDeviceInfo?: () => { benchmarkLevel?: number; memorySize?: string | number; platform?: string };
|
||
|
|
getSystemInfoSync?: () => Record<string, unknown>;
|
||
|
|
};
|
||
|
|
|
||
|
|
const PROBE_TIMEOUT_MS = 800;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 运行时设备能力探测(唯一分档入口)。
|
||
|
|
* 小游戏:优先官方 API(微信 getDeviceBenchmarkInfo / 抖音 deviceScore)及文档阈值;
|
||
|
|
* Web/桌面:deviceMemory + 核数;无信号时再 OS 兜底。
|
||
|
|
*/
|
||
|
|
export class DeviceCapability {
|
||
|
|
private static _resolved = false;
|
||
|
|
private static _platformProbed = false;
|
||
|
|
private static _probePromise: Promise<void> | null = null;
|
||
|
|
private static _quality: DeviceQuality = 'medium';
|
||
|
|
private static _memoryTier: MemoryTierId = 'tier0';
|
||
|
|
private static _signals: DeviceCapabilitySignals = {
|
||
|
|
platform: '',
|
||
|
|
os: '',
|
||
|
|
miniGameOs: 'unknown',
|
||
|
|
probeSource: 'fallback',
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Boot 前完成:拉取微信/抖音官方性能数据后再定档 */
|
||
|
|
static ensurePlatformProbe(): Promise<void> {
|
||
|
|
if (this._platformProbed) return Promise.resolve();
|
||
|
|
if (!this._probePromise) {
|
||
|
|
this._probePromise = this._runPlatformProbe();
|
||
|
|
}
|
||
|
|
return this._probePromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
static resolve(): void {
|
||
|
|
if (this._resolved) return;
|
||
|
|
this._resolved = true;
|
||
|
|
if (!this._platformProbed) {
|
||
|
|
this._signals = { ...this._signals, ...this._collectWebSignals() };
|
||
|
|
}
|
||
|
|
this._quality = this._scoreQuality(this._signals);
|
||
|
|
this._memoryTier = this._qualityToMemoryTier(this._quality);
|
||
|
|
}
|
||
|
|
|
||
|
|
static get quality(): DeviceQuality {
|
||
|
|
this.resolve();
|
||
|
|
return this._quality;
|
||
|
|
}
|
||
|
|
|
||
|
|
static get memoryTier(): MemoryTierId {
|
||
|
|
this.resolve();
|
||
|
|
return this._memoryTier;
|
||
|
|
}
|
||
|
|
|
||
|
|
static get signals(): Readonly<DeviceCapabilitySignals> {
|
||
|
|
this.resolve();
|
||
|
|
return this._signals;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 调试 / 设置面板强制档位 */
|
||
|
|
static overrideQuality(quality: DeviceQuality): void {
|
||
|
|
this._resolved = true;
|
||
|
|
this._platformProbed = true;
|
||
|
|
this._quality = quality;
|
||
|
|
this._memoryTier = this._qualityToMemoryTier(quality);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _runPlatformProbe(): Promise<void> {
|
||
|
|
const base = this._collectWebSignals();
|
||
|
|
this._signals = { ...this._signals, ...base };
|
||
|
|
|
||
|
|
const pf = gg?.sdk?.pf as PlatformApi | null | undefined;
|
||
|
|
if (pf) {
|
||
|
|
return Promise.race([
|
||
|
|
this._probeMiniGamePlatform(pf),
|
||
|
|
new Promise<void>((r) => setTimeout(r, PROBE_TIMEOUT_MS)),
|
||
|
|
]).then(() => {
|
||
|
|
this._platformProbed = true;
|
||
|
|
this._recomputeAfterProbe();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this._platformProbed = true;
|
||
|
|
this._recomputeAfterProbe();
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _probeMiniGamePlatform(pf: PlatformApi): Promise<void> {
|
||
|
|
if (MiniGamePf.isKs()) {
|
||
|
|
this._probeLegacySystemInfo(pf);
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
if (WECHAT) {
|
||
|
|
return this._probeWeChat(pf);
|
||
|
|
}
|
||
|
|
if (BYTEDANCE) {
|
||
|
|
this._probeDouyin(pf);
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
this._probeLegacySystemInfo(pf);
|
||
|
|
return Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _probeWeChat(pf: PlatformApi): Promise<void> {
|
||
|
|
let chain = Promise.resolve();
|
||
|
|
if (pf.getDeviceBenchmarkInfo) {
|
||
|
|
chain = new Promise<void>((resolve) => {
|
||
|
|
pf.getDeviceBenchmarkInfo!({
|
||
|
|
success: (res) => {
|
||
|
|
if (typeof res.benchmarkLevel === 'number') {
|
||
|
|
this._signals.benchmarkLevel = res.benchmarkLevel;
|
||
|
|
}
|
||
|
|
if (typeof res.modelLevel === 'number') {
|
||
|
|
this._signals.modelLevel = res.modelLevel;
|
||
|
|
}
|
||
|
|
this._signals.probeSource = 'wx.getDeviceBenchmarkInfo';
|
||
|
|
resolve();
|
||
|
|
},
|
||
|
|
fail: () => resolve(),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return chain.then(() => {
|
||
|
|
const deviceInfo = pf.getDeviceInfo?.();
|
||
|
|
if (deviceInfo) {
|
||
|
|
this._signals.miniGameOs = this._readMiniGameOs(deviceInfo.platform, deviceInfo);
|
||
|
|
if (this._signals.benchmarkLevel == null && typeof deviceInfo.benchmarkLevel === 'number') {
|
||
|
|
this._signals.benchmarkLevel = deviceInfo.benchmarkLevel;
|
||
|
|
if (this._signals.probeSource === 'fallback') {
|
||
|
|
this._signals.probeSource = 'wx.getDeviceInfo';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const memRaw = deviceInfo.memorySize;
|
||
|
|
const memMb = typeof memRaw === 'string' ? Number(memRaw) : memRaw;
|
||
|
|
if (typeof memMb === 'number' && memMb > 0) {
|
||
|
|
this._signals.memorySizeMb = memMb;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _probeDouyin(pf: PlatformApi): void {
|
||
|
|
const info = pf.getSystemInfoSync?.();
|
||
|
|
if (!info) return;
|
||
|
|
|
||
|
|
this._signals.miniGameOs = this._readMiniGameOs(info.platform as string | undefined, info);
|
||
|
|
|
||
|
|
const score = info.deviceScore as { overall?: number } | undefined;
|
||
|
|
if (score && typeof score.overall === 'number') {
|
||
|
|
this._signals.deviceScoreOverall = score.overall;
|
||
|
|
this._signals.probeSource = 'tt.deviceScore';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _probeLegacySystemInfo(pf: PlatformApi): void {
|
||
|
|
const info = pf.getSystemInfoSync?.();
|
||
|
|
if (!info) return;
|
||
|
|
|
||
|
|
this._signals.miniGameOs = this._readMiniGameOs(info.platform as string | undefined, info);
|
||
|
|
this._signals.probeSource = 'legacy.getSystemInfoSync';
|
||
|
|
|
||
|
|
const bench = info.benchmarkLevel;
|
||
|
|
if (typeof bench === 'number') {
|
||
|
|
this._signals.benchmarkLevel = bench;
|
||
|
|
}
|
||
|
|
const memRaw = info.memorySize;
|
||
|
|
const memMb = typeof memRaw === 'string' ? Number(memRaw) : memRaw;
|
||
|
|
if (typeof memMb === 'number' && memMb > 0) {
|
||
|
|
this._signals.memorySizeMb = memMb as number;
|
||
|
|
}
|
||
|
|
|
||
|
|
const score = info.deviceScore as { overall?: number } | undefined;
|
||
|
|
if (score && typeof score.overall === 'number') {
|
||
|
|
this._signals.deviceScoreOverall = score.overall;
|
||
|
|
this._signals.probeSource = 'tt.deviceScore';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _recomputeAfterProbe(): void {
|
||
|
|
this._resolved = false;
|
||
|
|
this.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _collectWebSignals(): Partial<DeviceCapabilitySignals> {
|
||
|
|
const partial: Partial<DeviceCapabilitySignals> = {
|
||
|
|
platform: String(sys.platform),
|
||
|
|
os: String(sys.os),
|
||
|
|
miniGameOs: 'unknown',
|
||
|
|
probeSource: 'fallback',
|
||
|
|
};
|
||
|
|
|
||
|
|
const mem = (sys as unknown as { deviceMemory?: number }).deviceMemory;
|
||
|
|
if (typeof mem === 'number' && mem > 0) {
|
||
|
|
partial.deviceMemoryGb = mem;
|
||
|
|
partial.probeSource = 'web.deviceMemory';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof navigator !== 'undefined') {
|
||
|
|
const cores = navigator.hardwareConcurrency;
|
||
|
|
if (typeof cores === 'number' && cores > 0) {
|
||
|
|
partial.hardwareConcurrency = cores;
|
||
|
|
if (partial.probeSource === 'fallback') {
|
||
|
|
partial.probeSource = 'web.hardwareConcurrency';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return partial;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _readMiniGameOs(platform: string | undefined, info: Record<string, unknown>): MiniGameOs {
|
||
|
|
const fromPlatform = this._parseOsToken(platform);
|
||
|
|
if (fromPlatform !== 'unknown') return fromPlatform;
|
||
|
|
const system = info.system;
|
||
|
|
if (typeof system === 'string') {
|
||
|
|
return this._parseOsToken(system);
|
||
|
|
}
|
||
|
|
return 'unknown';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _parseOsToken(raw: string | undefined): MiniGameOs {
|
||
|
|
if (!raw) return 'unknown';
|
||
|
|
const lower = raw.toLowerCase();
|
||
|
|
if (lower.includes('ios') || lower.includes('iphone') || lower.includes('ipad')) return 'ios';
|
||
|
|
if (lower.includes('android') || lower.includes('harmony')) return 'android';
|
||
|
|
return 'unknown';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _isMobileRuntime(s: DeviceCapabilitySignals): boolean {
|
||
|
|
if (s.miniGameOs === 'ios' || s.miniGameOs === 'android') return true;
|
||
|
|
return s.os === String(sys.OS.IOS)
|
||
|
|
|| s.os === String(sys.OS.ANDROID)
|
||
|
|
|| s.platform === String(sys.Platform.MOBILE_BROWSER);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _isDesktopRuntime(s: DeviceCapabilitySignals): boolean {
|
||
|
|
return s.os === String(sys.OS.WINDOWS)
|
||
|
|
|| s.os === String(sys.OS.OSX)
|
||
|
|
|| s.os === String(sys.OS.LINUX);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 分档优先级(与官方文档一致):
|
||
|
|
* 1. 抖音 deviceScore.overall
|
||
|
|
* 2. 微信 benchmarkLevel(分 iOS / Android 阈值)
|
||
|
|
* 3. 微信 modelLevel(benchmark 未知)
|
||
|
|
* 4. deviceMemory / memorySize
|
||
|
|
* 5. CPU 核数
|
||
|
|
* 6. 桌面 / 移动 OS 兜底
|
||
|
|
*/
|
||
|
|
private static _scoreQuality(s: DeviceCapabilitySignals): DeviceQuality {
|
||
|
|
const isMobile = this._isMobileRuntime(s);
|
||
|
|
|
||
|
|
if (s.deviceScoreOverall != null) {
|
||
|
|
const q = douyinDeviceScoreToQuality(s.deviceScoreOverall);
|
||
|
|
if (q) return q;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (s.benchmarkLevel != null) {
|
||
|
|
const q = wechatBenchmarkToQuality(s.benchmarkLevel, s.miniGameOs);
|
||
|
|
if (q) return q;
|
||
|
|
if (s.benchmarkLevel === -1 && s.modelLevel != null) {
|
||
|
|
const mq = wechatModelLevelToQuality(s.modelLevel);
|
||
|
|
if (mq) return mq;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (s.modelLevel != null && s.benchmarkLevel == null) {
|
||
|
|
const mq = wechatModelLevelToQuality(s.modelLevel);
|
||
|
|
if (mq) return mq;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (s.deviceMemoryGb != null) {
|
||
|
|
return webDeviceMemoryToQuality(s.deviceMemoryGb, isMobile);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (s.memorySizeMb != null) {
|
||
|
|
return memoryMbToQuality(s.memorySizeMb, isMobile);
|
||
|
|
}
|
||
|
|
|
||
|
|
const cores = s.hardwareConcurrency;
|
||
|
|
if (cores != null) {
|
||
|
|
if (cores >= 8) return 'high';
|
||
|
|
if (cores >= 4) return 'medium';
|
||
|
|
return 'low';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._isDesktopRuntime(s)) return 'high';
|
||
|
|
if (isMobile) return 'medium';
|
||
|
|
return 'medium';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _qualityToMemoryTier(q: DeviceQuality): MemoryTierId {
|
||
|
|
switch (q) {
|
||
|
|
case 'low': return 'tier0';
|
||
|
|
case 'medium': return 'tier1';
|
||
|
|
case 'high': return 'tier3';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|