Files
workinf_Blender_Wasm/web/protocol/device-budget.ts
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

70 lines
3.4 KiB
TypeScript

export const DEVICE_BUDGET_SCHEMA_VERSION = 1 as const;
export type DeviceBudgetTier = "CONSERVATIVE" | "BALANCED" | "HIGH";
export interface DeviceBudgetObservation {
schemaVersion: typeof DEVICE_BUDGET_SCHEMA_VERSION;
identitySha256: string;
webgl2: { status: "PASS" | "BLOCKED"; renderer?: string; vendor?: string };
webgpu: { status: "PASS" | "BLOCKED"; device?: string; description?: string; isFallbackAdapter?: boolean };
hardwareConcurrency: number | null;
deviceMemory: number | null;
}
export interface DeviceBudgetLimits {
maxTextureGPUBytes: number;
maxTexturePayloadBytes: number;
maxTextureDimension: number;
maxLights: number;
maxShadowMaps: number;
}
export interface DeviceBudgetSelection {
schemaVersion: typeof DEVICE_BUDGET_SCHEMA_VERSION;
identitySha256: string;
tier: DeviceBudgetTier;
reason: "MISSING_GPU" | "UNTRUSTED_ADAPTER" | "WEBGPU_UNAVAILABLE" | "BALANCED_CAPABILITY" | "HIGH_CAPABILITY";
limits: DeviceBudgetLimits;
}
const mib = 1024 * 1024;
const LIMITS: Readonly<Record<DeviceBudgetTier, DeviceBudgetLimits>> = Object.freeze({
CONSERVATIVE: Object.freeze({ maxTextureGPUBytes: 256 * mib, maxTexturePayloadBytes: 256 * mib, maxTextureDimension: 8192, maxLights: 8, maxShadowMaps: 2 }),
BALANCED: Object.freeze({ maxTextureGPUBytes: 512 * mib, maxTexturePayloadBytes: 512 * mib, maxTextureDimension: 16384, maxLights: 16, maxShadowMaps: 4 }),
HIGH: Object.freeze({ maxTextureGPUBytes: 1024 * mib, maxTexturePayloadBytes: 512 * mib, maxTextureDimension: 16384, maxLights: 64, maxShadowMaps: 8 }),
});
function validHash(value: unknown): value is string {
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
}
function validPositive(value: number | null): value is number {
return value !== null && Number.isSafeInteger(value) && value > 0;
}
export function selectDeviceBudget(observation: DeviceBudgetObservation): DeviceBudgetSelection {
if (!observation || observation.schemaVersion !== DEVICE_BUDGET_SCHEMA_VERSION || !validHash(observation.identitySha256)) {
throw new Error("DEVICE_BUDGET_IDENTITY_INVALID");
}
const conservative = (reason: DeviceBudgetSelection["reason"]): DeviceBudgetSelection => ({
schemaVersion: DEVICE_BUDGET_SCHEMA_VERSION,
identitySha256: observation.identitySha256,
tier: "CONSERVATIVE",
reason,
limits: LIMITS.CONSERVATIVE,
});
if (observation.webgl2.status !== "PASS") return conservative("MISSING_GPU");
const renderer = `${observation.webgl2.renderer ?? ""} ${observation.webgpu.description ?? ""}`.toLowerCase();
if (!renderer || renderer.includes("swiftshader") || renderer.includes("unknown") || observation.webgpu.isFallbackAdapter) return conservative("UNTRUSTED_ADAPTER");
if (observation.webgpu.status !== "PASS") return conservative("WEBGPU_UNAVAILABLE");
if (!validPositive(observation.hardwareConcurrency) || !validPositive(observation.deviceMemory)) return conservative("UNTRUSTED_ADAPTER");
const tier: DeviceBudgetTier = observation.hardwareConcurrency >= 8 && observation.deviceMemory >= 8 ? "HIGH" : "BALANCED";
return { schemaVersion: DEVICE_BUDGET_SCHEMA_VERSION, identitySha256: observation.identitySha256, tier, reason: tier === "HIGH" ? "HIGH_CAPABILITY" : "BALANCED_CAPABILITY", limits: LIMITS[tier] };
}
export function deviceBudgetLimits(tier: DeviceBudgetTier): DeviceBudgetLimits {
const limits = LIMITS[tier];
if (!limits) throw new Error("DEVICE_BUDGET_TIER_INVALID");
return limits;
}