#!/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));