Release Healer Man 0.1.7
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { analyzeGlbFile } from "./glb-performance.mjs";
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const baselineFile = path.join(
|
||||
projectRoot,
|
||||
"scripts/asset-pipeline/environment-performance-baseline.json",
|
||||
);
|
||||
const defaultRoots = [
|
||||
path.join(projectRoot, "public/assets/game/manastorm"),
|
||||
path.join(projectRoot, "src/assets/game/dungeons"),
|
||||
];
|
||||
const defaultThresholds = {
|
||||
maxEstimatedDrawCalls: 1_500,
|
||||
maxUnbatchedDrawCallSavings: 250,
|
||||
};
|
||||
|
||||
async function exists(file) {
|
||||
try {
|
||||
await access(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function collectVisualGlbs(root, files = []) {
|
||||
if (!await exists(root)) return files;
|
||||
const entry = await stat(root);
|
||||
if (entry.isFile()) {
|
||||
if (/(?:^|[-_])visual\.glb$/i.test(path.basename(root))) files.push(root);
|
||||
return files;
|
||||
}
|
||||
for (const child of await readdir(root, { withFileTypes: true })) {
|
||||
const target = path.join(root, child.name);
|
||||
if (child.isDirectory()) await collectVisualGlbs(target, files);
|
||||
else if (/(?:^|[-_])visual\.glb$/i.test(child.name)) files.push(target);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function projectPath(file) {
|
||||
return path.relative(projectRoot, file).split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function exceeds(report, limits) {
|
||||
return report.estimatedDrawCalls > limits.maxEstimatedDrawCalls
|
||||
|| report.unbatchedDrawCallSavings > limits.maxUnbatchedDrawCallSavings;
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const writeBaseline = args.includes("--write-baseline");
|
||||
const roots = args.filter((argument) => argument !== "--write-baseline")
|
||||
.map((argument) => path.resolve(projectRoot, argument));
|
||||
const files = [...new Set((await Promise.all(
|
||||
(roots.length ? roots : defaultRoots).map((root) => collectVisualGlbs(root)),
|
||||
)).flat())].sort();
|
||||
const reports = await Promise.all(files.map(async (file) => ({
|
||||
file: projectPath(file),
|
||||
byteLength: (await stat(file)).size,
|
||||
...(await analyzeGlbFile(file)),
|
||||
})));
|
||||
|
||||
if (writeBaseline) {
|
||||
const baseline = {
|
||||
schemaVersion: 1,
|
||||
thresholds: defaultThresholds,
|
||||
exceptions: Object.fromEntries(
|
||||
reports
|
||||
.filter((report) => exceeds(report, defaultThresholds))
|
||||
.map((report) => [report.file, {
|
||||
maxEstimatedDrawCalls: Math.max(defaultThresholds.maxEstimatedDrawCalls, report.estimatedDrawCalls),
|
||||
maxUnbatchedDrawCallSavings: Math.max(
|
||||
defaultThresholds.maxUnbatchedDrawCallSavings,
|
||||
report.unbatchedDrawCallSavings,
|
||||
),
|
||||
}]),
|
||||
),
|
||||
};
|
||||
await mkdir(path.dirname(baselineFile), { recursive: true });
|
||||
await writeFile(baselineFile, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
||||
console.log(`Wrote ${Object.keys(baseline.exceptions).length} performance exceptions to ${projectPath(baselineFile)}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!await exists(baselineFile)) {
|
||||
throw new Error(`Missing ${projectPath(baselineFile)}. Run this script with --write-baseline.`);
|
||||
}
|
||||
const baseline = JSON.parse(await readFile(baselineFile, "utf8"));
|
||||
const thresholds = { ...defaultThresholds, ...baseline.thresholds };
|
||||
const regressions = reports.filter((report) => {
|
||||
const limits = { ...thresholds, ...baseline.exceptions?.[report.file] };
|
||||
return exceeds(report, limits);
|
||||
});
|
||||
const debt = reports.filter((report) => exceeds(report, thresholds));
|
||||
const top = [...reports]
|
||||
.sort((left, right) => right.estimatedDrawCalls - left.estimatedDrawCalls)
|
||||
.slice(0, 12)
|
||||
.map((report) => ({
|
||||
asset: report.file,
|
||||
calls: report.estimatedDrawCalls,
|
||||
avoidable: report.unbatchedDrawCallSavings,
|
||||
gpuBatches: report.gpuInstanceBatches,
|
||||
nodes: report.nodes,
|
||||
}));
|
||||
|
||||
console.table(top);
|
||||
console.log(JSON.stringify({
|
||||
status: regressions.length ? "blocked" : "green",
|
||||
assets: reports.length,
|
||||
knownBudgetExceptions: debt.length,
|
||||
regressions: regressions.map((report) => ({
|
||||
file: report.file,
|
||||
estimatedDrawCalls: report.estimatedDrawCalls,
|
||||
unbatchedDrawCallSavings: report.unbatchedDrawCallSavings,
|
||||
})),
|
||||
}, null, 2));
|
||||
if (regressions.length) process.exitCode = 1;
|
||||
@@ -0,0 +1,249 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"thresholds": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 250
|
||||
},
|
||||
"exceptions": {
|
||||
"public/assets/game/manastorm/109-sunkentemple/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 320
|
||||
},
|
||||
"public/assets/game/manastorm/129-razorfendowns/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1507,
|
||||
"maxUnbatchedDrawCallSavings": 1406
|
||||
},
|
||||
"public/assets/game/manastorm/1771-custom-marsh/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2493,
|
||||
"maxUnbatchedDrawCallSavings": 2114
|
||||
},
|
||||
"public/assets/game/manastorm/1774-custom-tropical/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 910
|
||||
},
|
||||
"public/assets/game/manastorm/1775-custom-tuskarr/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 500
|
||||
},
|
||||
"public/assets/game/manastorm/1776-custom-vrykul1/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2223,
|
||||
"maxUnbatchedDrawCallSavings": 1818
|
||||
},
|
||||
"public/assets/game/manastorm/1781-forgottenmine/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 5756,
|
||||
"maxUnbatchedDrawCallSavings": 3818
|
||||
},
|
||||
"public/assets/game/manastorm/1785-valsharaharena/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 392
|
||||
},
|
||||
"public/assets/game/manastorm/1787-baradinholdarena/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 10462,
|
||||
"maxUnbatchedDrawCallSavings": 9362
|
||||
},
|
||||
"public/assets/game/manastorm/1794-twistingnether/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 3077,
|
||||
"maxUnbatchedDrawCallSavings": 2557
|
||||
},
|
||||
"public/assets/game/manastorm/209-tanarisinstance/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 4579,
|
||||
"maxUnbatchedDrawCallSavings": 3547
|
||||
},
|
||||
"public/assets/game/manastorm/229-blackrockspire/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 3972,
|
||||
"maxUnbatchedDrawCallSavings": 3598
|
||||
},
|
||||
"public/assets/game/manastorm/269-cavernsoftime/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 5446,
|
||||
"maxUnbatchedDrawCallSavings": 5289
|
||||
},
|
||||
"public/assets/game/manastorm/289-schoolofnecromancy/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 5140,
|
||||
"maxUnbatchedDrawCallSavings": 4790
|
||||
},
|
||||
"public/assets/game/manastorm/309-zul-gurub/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 17377,
|
||||
"maxUnbatchedDrawCallSavings": 15605
|
||||
},
|
||||
"public/assets/game/manastorm/329-stratholme/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2954,
|
||||
"maxUnbatchedDrawCallSavings": 2725
|
||||
},
|
||||
"public/assets/game/manastorm/34-stormwindjail/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 487
|
||||
},
|
||||
"public/assets/game/manastorm/349-mauradon/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 5624,
|
||||
"maxUnbatchedDrawCallSavings": 5352
|
||||
},
|
||||
"public/assets/game/manastorm/36-deadminesinstance/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 309
|
||||
},
|
||||
"public/assets/game/manastorm/389-orgrimmarinstance/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 881
|
||||
},
|
||||
"public/assets/game/manastorm/429-diremaul/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 5021,
|
||||
"maxUnbatchedDrawCallSavings": 4636
|
||||
},
|
||||
"public/assets/game/manastorm/469-blackwinglair/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 452
|
||||
},
|
||||
"public/assets/game/manastorm/47-razorfenkraulinstance/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 897
|
||||
},
|
||||
"public/assets/game/manastorm/48-blackfathom/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2164,
|
||||
"maxUnbatchedDrawCallSavings": 1950
|
||||
},
|
||||
"public/assets/game/manastorm/509-ahnqiraj/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 600
|
||||
},
|
||||
"public/assets/game/manastorm/531-ahnqirajtemple/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 953
|
||||
},
|
||||
"public/assets/game/manastorm/532-karazahn/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 8540,
|
||||
"maxUnbatchedDrawCallSavings": 7892
|
||||
},
|
||||
"public/assets/game/manastorm/533-stratholme-raid/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 4165,
|
||||
"maxUnbatchedDrawCallSavings": 4014
|
||||
},
|
||||
"public/assets/game/manastorm/540-hellfiremilitary/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 1150
|
||||
},
|
||||
"public/assets/game/manastorm/542-hellfiredemon/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 444
|
||||
},
|
||||
"public/assets/game/manastorm/543-hellfirerampart/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 296
|
||||
},
|
||||
"public/assets/game/manastorm/545-coilfangpumping/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 674
|
||||
},
|
||||
"public/assets/game/manastorm/546-coilfangmarsh/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 3104,
|
||||
"maxUnbatchedDrawCallSavings": 3033
|
||||
},
|
||||
"public/assets/game/manastorm/547-coilfangdraenei/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1535,
|
||||
"maxUnbatchedDrawCallSavings": 1455
|
||||
},
|
||||
"public/assets/game/manastorm/548-coilfangraid/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1916,
|
||||
"maxUnbatchedDrawCallSavings": 1683
|
||||
},
|
||||
"public/assets/game/manastorm/550-tempestkeepraid/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 420
|
||||
},
|
||||
"public/assets/game/manastorm/552-tempestkeeparcane/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 999
|
||||
},
|
||||
"public/assets/game/manastorm/553-tempestkeepatrium/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 1115
|
||||
},
|
||||
"public/assets/game/manastorm/554-tempestkeepfactory/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 872
|
||||
},
|
||||
"public/assets/game/manastorm/555-auchindounshadow/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2624,
|
||||
"maxUnbatchedDrawCallSavings": 2516
|
||||
},
|
||||
"public/assets/game/manastorm/556-auchindoundemon/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2095,
|
||||
"maxUnbatchedDrawCallSavings": 1987
|
||||
},
|
||||
"public/assets/game/manastorm/557-auchindounethereal/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 3164,
|
||||
"maxUnbatchedDrawCallSavings": 3026
|
||||
},
|
||||
"public/assets/game/manastorm/558-auchindoundraenei/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2613,
|
||||
"maxUnbatchedDrawCallSavings": 2523
|
||||
},
|
||||
"public/assets/game/manastorm/560-hillsbradpast/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 28259,
|
||||
"maxUnbatchedDrawCallSavings": 24733
|
||||
},
|
||||
"public/assets/game/manastorm/568-zulaman/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 4307,
|
||||
"maxUnbatchedDrawCallSavings": 3850
|
||||
},
|
||||
"public/assets/game/manastorm/574-valgarde70/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 8271,
|
||||
"maxUnbatchedDrawCallSavings": 7072
|
||||
},
|
||||
"public/assets/game/manastorm/575-utgardepinnacle/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 16277,
|
||||
"maxUnbatchedDrawCallSavings": 14018
|
||||
},
|
||||
"public/assets/game/manastorm/576-nexus70/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 396
|
||||
},
|
||||
"public/assets/game/manastorm/578-nexus80/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2283,
|
||||
"maxUnbatchedDrawCallSavings": 1896
|
||||
},
|
||||
"public/assets/game/manastorm/585-sunwell5manfix/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1810,
|
||||
"maxUnbatchedDrawCallSavings": 1618
|
||||
},
|
||||
"public/assets/game/manastorm/595-stratholmecot/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 4353,
|
||||
"maxUnbatchedDrawCallSavings": 3386
|
||||
},
|
||||
"public/assets/game/manastorm/600-draktheronkeep/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 3518,
|
||||
"maxUnbatchedDrawCallSavings": 2876
|
||||
},
|
||||
"public/assets/game/manastorm/602-ulduar80/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1973,
|
||||
"maxUnbatchedDrawCallSavings": 1295
|
||||
},
|
||||
"public/assets/game/manastorm/619-azjol-lowercity/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1596,
|
||||
"maxUnbatchedDrawCallSavings": 1243
|
||||
},
|
||||
"public/assets/game/manastorm/658-quarryoftears/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 618
|
||||
},
|
||||
"public/assets/game/manastorm/668-hallsofreflection/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 5464,
|
||||
"maxUnbatchedDrawCallSavings": 4606
|
||||
},
|
||||
"public/assets/game/manastorm/70-uldaman/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 3268,
|
||||
"maxUnbatchedDrawCallSavings": 3115
|
||||
},
|
||||
"public/assets/game/manastorm/821-tinketechshowdown/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 2585,
|
||||
"maxUnbatchedDrawCallSavings": 2379
|
||||
},
|
||||
"public/assets/game/manastorm/90-gnomeragoninstance/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 1051
|
||||
},
|
||||
"public/assets/game/manastorm/908-alvaencounter/visual.glb": {
|
||||
"maxEstimatedDrawCalls": 1500,
|
||||
"maxUnbatchedDrawCallSavings": 1096
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { open } from "node:fs/promises";
|
||||
|
||||
const GLB_MAGIC = 0x46546c67;
|
||||
const JSON_CHUNK_TYPE = 0x4e4f534a;
|
||||
const GPU_INSTANCING_EXTENSION = "EXT_mesh_gpu_instancing";
|
||||
|
||||
function primitiveCount(json, meshIndex) {
|
||||
return json.meshes?.[meshIndex]?.primitives?.length ?? 0;
|
||||
}
|
||||
|
||||
function gpuInstanceCount(json, node) {
|
||||
const attributes = node.extensions?.[GPU_INSTANCING_EXTENSION]?.attributes;
|
||||
if (!attributes || typeof attributes !== "object") return 0;
|
||||
return Math.max(
|
||||
0,
|
||||
...Object.values(attributes).map((accessorIndex) => (
|
||||
json.accessors?.[accessorIndex]?.count ?? 0
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
export function analyzeGlbJson(json) {
|
||||
const repeatedMeshes = new Map();
|
||||
let estimatedDrawCalls = 0;
|
||||
let expandedPrimitiveInstances = 0;
|
||||
let meshNodes = 0;
|
||||
let gpuInstanceBatches = 0;
|
||||
let gpuInstances = 0;
|
||||
|
||||
for (const node of json.nodes ?? []) {
|
||||
if (!Number.isInteger(node.mesh)) continue;
|
||||
meshNodes += 1;
|
||||
const primitives = primitiveCount(json, node.mesh);
|
||||
const instances = gpuInstanceCount(json, node);
|
||||
estimatedDrawCalls += primitives;
|
||||
expandedPrimitiveInstances += primitives * Math.max(1, instances);
|
||||
if (instances > 0) {
|
||||
gpuInstanceBatches += 1;
|
||||
gpuInstances += instances;
|
||||
continue;
|
||||
}
|
||||
repeatedMeshes.set(node.mesh, (repeatedMeshes.get(node.mesh) ?? 0) + 1);
|
||||
}
|
||||
|
||||
let eligibleRepeatedMeshes = 0;
|
||||
let eligibleRepeatedNodes = 0;
|
||||
let unbatchedDrawCallSavings = 0;
|
||||
for (const [meshIndex, count] of repeatedMeshes) {
|
||||
if (count < 3) continue;
|
||||
eligibleRepeatedMeshes += 1;
|
||||
eligibleRepeatedNodes += count;
|
||||
unbatchedDrawCallSavings += (count - 1) * primitiveCount(json, meshIndex);
|
||||
}
|
||||
|
||||
const materials = json.materials ?? [];
|
||||
return {
|
||||
nodes: json.nodes?.length ?? 0,
|
||||
meshNodes,
|
||||
meshes: json.meshes?.length ?? 0,
|
||||
materials: materials.length,
|
||||
textures: json.textures?.length ?? 0,
|
||||
doubleSidedMaterials: materials.filter((material) => material.doubleSided === true).length,
|
||||
blendedMaterials: materials.filter((material) => material.alphaMode === "BLEND").length,
|
||||
estimatedDrawCalls,
|
||||
expandedPrimitiveInstances,
|
||||
gpuInstanceBatches,
|
||||
gpuInstances,
|
||||
eligibleRepeatedMeshes,
|
||||
eligibleRepeatedNodes,
|
||||
unbatchedDrawCallSavings,
|
||||
usesGpuInstancing: (json.extensionsUsed ?? []).includes(GPU_INSTANCING_EXTENSION),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readGlbJson(file) {
|
||||
const handle = await open(file, "r");
|
||||
try {
|
||||
const header = Buffer.alloc(20);
|
||||
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
||||
if (bytesRead !== header.length
|
||||
|| header.readUInt32LE(0) !== GLB_MAGIC
|
||||
|| header.readUInt32LE(4) !== 2
|
||||
|| header.readUInt32LE(16) !== JSON_CHUNK_TYPE) {
|
||||
throw new Error(`${file} is not a supported GLB 2.0 file.`);
|
||||
}
|
||||
const jsonLength = header.readUInt32LE(12);
|
||||
const jsonBuffer = Buffer.alloc(jsonLength);
|
||||
const jsonRead = await handle.read(jsonBuffer, 0, jsonLength, 20);
|
||||
if (jsonRead.bytesRead !== jsonLength) throw new Error(`${file} has a truncated JSON chunk.`);
|
||||
return JSON.parse(jsonBuffer.toString("utf8"));
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function analyzeGlbFile(file) {
|
||||
return analyzeGlbJson(await readGlbJson(file));
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { analyzeGlbJson } from "./glb-performance.mjs";
|
||||
|
||||
test("counts repeated mesh placements as avoidable draw calls", () => {
|
||||
const report = analyzeGlbJson({
|
||||
meshes: [
|
||||
{ primitives: [{}, {}] },
|
||||
{ primitives: [{}] },
|
||||
],
|
||||
nodes: [
|
||||
{ mesh: 0 },
|
||||
{ mesh: 0 },
|
||||
{ mesh: 0 },
|
||||
{ mesh: 1 },
|
||||
],
|
||||
materials: [
|
||||
{ doubleSided: true, alphaMode: "BLEND" },
|
||||
{},
|
||||
],
|
||||
textures: [{}, {}],
|
||||
});
|
||||
|
||||
assert.equal(report.estimatedDrawCalls, 7);
|
||||
assert.equal(report.expandedPrimitiveInstances, 7);
|
||||
assert.equal(report.eligibleRepeatedMeshes, 1);
|
||||
assert.equal(report.eligibleRepeatedNodes, 3);
|
||||
assert.equal(report.unbatchedDrawCallSavings, 4);
|
||||
assert.equal(report.doubleSidedMaterials, 1);
|
||||
assert.equal(report.blendedMaterials, 1);
|
||||
});
|
||||
|
||||
test("counts one draw-call set for an EXT_mesh_gpu_instancing batch", () => {
|
||||
const report = analyzeGlbJson({
|
||||
extensionsUsed: ["EXT_mesh_gpu_instancing"],
|
||||
accessors: [{ count: 12 }],
|
||||
meshes: [{ primitives: [{}, {}, {}] }],
|
||||
nodes: [{
|
||||
mesh: 0,
|
||||
extensions: {
|
||||
EXT_mesh_gpu_instancing: {
|
||||
attributes: { TRANSLATION: 0 },
|
||||
},
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
assert.equal(report.estimatedDrawCalls, 3);
|
||||
assert.equal(report.expandedPrimitiveInstances, 36);
|
||||
assert.equal(report.gpuInstanceBatches, 1);
|
||||
assert.equal(report.gpuInstances, 12);
|
||||
assert.equal(report.unbatchedDrawCallSavings, 0);
|
||||
assert.equal(report.usesGpuInstancing, true);
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { basename, resolve } from "node:path";
|
||||
import {
|
||||
CLASSES,
|
||||
COA_CLASS_IDS,
|
||||
type ClassId,
|
||||
type CoaClassId,
|
||||
} from "../src/app/characterCatalog";
|
||||
import { TALENT_NODES, TALENT_TREES } from "../src/game/talentCatalog";
|
||||
|
||||
const projectRoot = resolve(import.meta.dirname, "..");
|
||||
const talentOutputDirectory = resolve(projectRoot, "src/game/generated/talentUi");
|
||||
const coaAbilityOutputDirectory = resolve(projectRoot, "src/game/generated/coaAbilities");
|
||||
|
||||
function writeJson(file: string, value: unknown): void {
|
||||
writeFileSync(file, `${JSON.stringify(value)}\n`, "utf8");
|
||||
console.log(`Wrote ${file}`);
|
||||
}
|
||||
|
||||
function removeStaleJson(directory: string, expectedNames: ReadonlySet<string>): void {
|
||||
for (const name of readdirSync(directory)) {
|
||||
if (!name.endsWith(".json") || expectedNames.has(name)) continue;
|
||||
unlinkSync(resolve(directory, name));
|
||||
}
|
||||
}
|
||||
|
||||
mkdirSync(talentOutputDirectory, { recursive: true });
|
||||
const talentClassIds = new Set<ClassId>(TALENT_TREES.map((tree) => tree.classId));
|
||||
const talentFileNames = new Set([...talentClassIds].map((classId) => `${classId}.json`));
|
||||
removeStaleJson(talentOutputDirectory, talentFileNames);
|
||||
|
||||
for (const classId of talentClassIds) {
|
||||
const trees = TALENT_TREES.filter((tree) => tree.classId === classId).map((tree) => ({
|
||||
id: tree.id,
|
||||
classId: tree.classId,
|
||||
name: tree.name,
|
||||
icon: tree.icon,
|
||||
background: tree.background,
|
||||
description: tree.description,
|
||||
system: tree.system,
|
||||
}));
|
||||
const treeIds = new Set(trees.map((tree) => tree.id));
|
||||
const nodes = TALENT_NODES.filter((node) => treeIds.has(node.treeId)).map((node) => ({
|
||||
id: node.id,
|
||||
treeId: node.treeId,
|
||||
index: node.index,
|
||||
prerequisiteId: node.prerequisiteId,
|
||||
prerequisiteRank: node.prerequisiteRank,
|
||||
prerequisites: node.prerequisites,
|
||||
column: node.column,
|
||||
row: node.row,
|
||||
icon: node.icon,
|
||||
maxRank: node.maxRank,
|
||||
name: node.name,
|
||||
description: node.description,
|
||||
rankDescriptions: node.rankDescriptions,
|
||||
system: node.system,
|
||||
entryType: node.entryType,
|
||||
abilityEssenceCost: node.abilityEssenceCost,
|
||||
talentEssenceCost: node.talentEssenceCost,
|
||||
requiredClassPoints: node.requiredClassPoints,
|
||||
requiredSpecPoints: node.requiredSpecPoints,
|
||||
requiredLevel: node.requiredLevel,
|
||||
isPassive: node.isPassive,
|
||||
}));
|
||||
writeJson(resolve(talentOutputDirectory, `${classId}.json`), {
|
||||
schemaVersion: 1,
|
||||
classId,
|
||||
trees,
|
||||
nodes,
|
||||
});
|
||||
}
|
||||
|
||||
interface CoaAbilitySnapshot {
|
||||
readonly schemaVersion: 1;
|
||||
readonly abilitiesByClass: Readonly<Record<CoaClassId, readonly unknown[]>>;
|
||||
readonly resourcesByClass: Readonly<Record<CoaClassId, unknown>>;
|
||||
readonly triggeredAbilitiesBySpellId: Readonly<Record<string, { readonly classId?: ClassId }>>;
|
||||
}
|
||||
|
||||
const coaAbilityInput = resolve(projectRoot, "src/game/generated/coaAbilityRuntimeCatalog.json");
|
||||
const coaSnapshot = JSON.parse(readFileSync(coaAbilityInput, "utf8")) as CoaAbilitySnapshot;
|
||||
mkdirSync(coaAbilityOutputDirectory, { recursive: true });
|
||||
const coaAbilityFileNames = new Set(COA_CLASS_IDS.map((classId) => `${classId}.json`));
|
||||
removeStaleJson(coaAbilityOutputDirectory, coaAbilityFileNames);
|
||||
|
||||
for (const classId of COA_CLASS_IDS) {
|
||||
const triggeredAbilitiesBySpellId = Object.fromEntries(Object.entries(
|
||||
coaSnapshot.triggeredAbilitiesBySpellId,
|
||||
).filter(([, ability]) => ability.classId === classId));
|
||||
writeJson(resolve(coaAbilityOutputDirectory, `${classId}.json`), {
|
||||
schemaVersion: 1,
|
||||
classId,
|
||||
abilities: coaSnapshot.abilitiesByClass[classId] ?? [],
|
||||
resource: coaSnapshot.resourcesByClass[classId],
|
||||
triggeredAbilitiesBySpellId,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Generated ${talentFileNames.size} talent UI shards and ${coaAbilityFileNames.size} Conquest ability shards from ${basename(coaAbilityInput)}.`);
|
||||
@@ -404,15 +404,23 @@ async function optimizeStage(slug) {
|
||||
const source = path.join(staging, entry.path);
|
||||
const outputName = optimizedChunkName(role, index, entries.length);
|
||||
const output = path.join(staging, outputName);
|
||||
await run(process.execPath, [
|
||||
cli,
|
||||
"meshopt",
|
||||
source,
|
||||
output,
|
||||
"--level",
|
||||
"high",
|
||||
]);
|
||||
if (role === "visual") {
|
||||
await run(process.execPath, [
|
||||
cli,
|
||||
"optimize",
|
||||
source,
|
||||
output,
|
||||
"--compress",
|
||||
"meshopt",
|
||||
"--meshopt-level",
|
||||
"high",
|
||||
"--instance",
|
||||
"true",
|
||||
"--instance-min",
|
||||
"3",
|
||||
"--texture-size",
|
||||
"2048",
|
||||
]);
|
||||
await run(process.execPath, [
|
||||
path.join(projectRoot, "scripts/asset-pipeline/compress-ktx2.mjs"),
|
||||
output,
|
||||
@@ -421,6 +429,15 @@ async function optimizeStage(slug) {
|
||||
"--jobs",
|
||||
"2",
|
||||
]);
|
||||
} else {
|
||||
await run(process.execPath, [
|
||||
cli,
|
||||
"meshopt",
|
||||
source,
|
||||
output,
|
||||
"--level",
|
||||
"high",
|
||||
]);
|
||||
}
|
||||
const geometry = await inspectGlb(output);
|
||||
const descriptor = {
|
||||
|
||||
Reference in New Issue
Block a user