- Add 11:07 Chinese-narrated lesson video, VTT, poster and 13 chapter screenshots. - Regenerate 20-render WebGL2 matrix and 15 operation captures on RTX 4060. - Fix WASM staging for non-ASCII import filenames (猴头.glb); rebuild web_engine. - Harden lesson video recording pipeline; fix stale lesson-link E2E selector. - Add RE PBR derived libraries, lesson page, evidence and status report. Co-Authored-By: Claude Code <noreply@anthropic.com>
100 lines
5.1 KiB
JavaScript
100 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Validate the RE 5000PBR limitation fixes: displacement, diagnostics, HQ, GPU and camera. */
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const REPO = path.resolve(import.meta.dirname, "../..");
|
|
const LIMITS = path.join(REPO, "tests/golden/RE-PBR-LIMITS-001");
|
|
const RENDER = path.join(REPO, "tests/golden/RE-PBR-RENDER-001");
|
|
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
const exists = (file, label) => assert.ok(fs.existsSync(file), `missing ${label}: ${path.relative(REPO, file)}`);
|
|
|
|
const displacement = readJson(path.join(LIMITS, "displacement/report.json"));
|
|
assert.equal(displacement.status, "PASS");
|
|
assert.equal(displacement.nativeBinding.usage, "HEIGHT");
|
|
assert.ok(Math.abs(displacement.nativeBinding.scale - 0.001) < 1e-6);
|
|
assert.ok(Math.abs(displacement.nativeBinding.midlevel - 0.5) < 1e-6);
|
|
assert.equal(displacement.subdivision.sourceTriangles, 968);
|
|
assert.equal(displacement.subdivision.renderTriangles, 61952);
|
|
for (const key of ["1x", "100x"]) {
|
|
const render = displacement.renders[key];
|
|
const file = path.join(REPO, render.path);
|
|
exists(file, `displacement ${key}`);
|
|
assert.equal(sha256(file), render.sha256, `displacement ${key} SHA`);
|
|
assert.ok(render.mean > 1 && render.mean < 254);
|
|
}
|
|
assert.notEqual(displacement.renders["1x"].sha256, displacement.renders["100x"].sha256);
|
|
assert.ok(displacement.meanDelta > 0.01, "displacement 1x/100x mean delta too small");
|
|
|
|
const diagnostics = readJson(path.join(LIMITS, "diagnostics/report.json"));
|
|
assert.equal(diagnostics.status, "PASS");
|
|
assert.equal(diagnostics.views.length, 3);
|
|
for (const view of diagnostics.views) {
|
|
const file = path.join(REPO, view.path);
|
|
exists(file, `diagnostic ${view.mode}`);
|
|
assert.equal(sha256(file), view.sha256, `diagnostic ${view.mode} SHA`);
|
|
assert.ok(view.mean > 1 && view.mean < 254);
|
|
}
|
|
assert.equal(diagnostics.exposure.result, "PASS");
|
|
assert.equal(new Set(diagnostics.views.map((view) => view.sha256)).size, 3, "diagnostic views must differ");
|
|
|
|
const hq = readJson(path.join(LIMITS, "hq/report.json"));
|
|
assert.equal(hq.status, "PASS");
|
|
exists(path.join(REPO, hq.library.path), "HQ library");
|
|
assert.equal(sha256(path.join(REPO, hq.library.path)), hq.library.sha256);
|
|
assert.equal(hq.budget.loaded, 5);
|
|
assert.equal(hq.budget.rejected, 0);
|
|
assert.ok(hq.budget.gpuDecodedBytes < 512 * 1024 * 1024, "HQ exceeds WebGL2 decoded budget");
|
|
exists(path.join(REPO, hq.render.path), "HQ render");
|
|
assert.equal(sha256(path.join(REPO, hq.render.path)), hq.render.sha256);
|
|
|
|
const gpu = readJson(path.join(LIMITS, "gpu/batch0-gpu-enable.json"));
|
|
assert.equal(gpu.status, "PASS");
|
|
assert.match(gpu.webgl.renderer, /NVIDIA|AMD|Intel|ANGLE/);
|
|
assert.doesNotMatch(gpu.webgl.renderer, /SwiftShader|llvmpipe|software/i);
|
|
const hardwareRender = gpu.render.hardware;
|
|
const hardwareFile = path.join(REPO, hardwareRender.path);
|
|
exists(hardwareFile, "GPU hardware render");
|
|
assert.equal(sha256(hardwareFile), hardwareRender.sha256);
|
|
assert.match(gpu.render.swiftshader.sha256, /^[a-f0-9]{64}$/);
|
|
assert.ok(gpu.render.meanStd.swiftshader.mean > 1 && gpu.render.meanStd.swiftshader.mean < 254);
|
|
|
|
const renderReport = readJson(path.join(RENDER, "report.json"));
|
|
assert.equal(renderReport.status, "PASS");
|
|
assert.equal(renderReport.renders.length, 20);
|
|
assert.equal(new Set(renderReport.renders.map((render) => render.sha256)).size, 20, "20 renders must be unique");
|
|
assert.match(renderReport.webgl.renderer, /NVIDIA|AMD|Intel|ANGLE/);
|
|
const last = "material-black_worn_wall_brick";
|
|
const scene = renderReport.renders.find((render) => render.name === `${last}-1024-scene`);
|
|
const viewport = renderReport.renders.find((render) => render.name === `${last}-1024-viewport`);
|
|
assert.ok(scene && viewport);
|
|
assert.notEqual(scene.sha256, viewport.sha256, "SCENE and COMPARISON renders must differ");
|
|
const reopen = renderReport.renders.find((render) => render.name.endsWith("-reopen-1024-scene"));
|
|
assert.ok(reopen);
|
|
assert.match(reopen.textureStatus, /^ready:([1-9]\d*):0:/);
|
|
|
|
const lessonHtml = path.join(REPO, "web/app/public/tutorial/pbr/render-lesson.html");
|
|
exists(lessonHtml, "lesson HTML");
|
|
const html = fs.readFileSync(lessonHtml, "utf8");
|
|
for (const required of ["7. 进阶", "位移与细分", "诊断视图", "高清 2048", "RTX 4060"]) {
|
|
assert.ok(html.includes(required), `lesson misses ${required}`);
|
|
}
|
|
for (const source of [...html.matchAll(/src="([^"]+)"/g)].map((match) => match[1])) {
|
|
if (!source.startsWith("/")) continue;
|
|
exists(path.join(REPO, "web/app/public", source.slice(1)), `lesson asset ${source}`);
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
status: "PASS",
|
|
displacement: { scale: displacement.nativeBinding.scale, triangles: displacement.subdivision.renderTriangles, meanDelta: displacement.meanDelta },
|
|
diagnostics: diagnostics.views.length,
|
|
hq: { bytes: hq.library.bytes, gpuDecodedBytes: hq.budget.gpuDecodedBytes },
|
|
gpu: gpu.webgl.renderer,
|
|
renders: renderReport.renders.length,
|
|
uniqueRenders: 20,
|
|
reopen: reopen.textureStatus,
|
|
}, null, 2));
|