Add Chromium-only Blender WebEngine parity work
This commit is contained in:
134
tools/web/check-main-roundtrip.mjs
Normal file
134
tools/web/check-main-roundtrip.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import factory from "../../web/app/src/vendor/blender/web_engine.js";
|
||||
|
||||
const root = new URL("../../", import.meta.url);
|
||||
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
|
||||
const cases = [
|
||||
{
|
||||
golden: JSON.parse(fs.readFileSync(new URL("tests/golden/W-080/animation-depsgraph.json", root), "utf8")),
|
||||
samplesKey: "frames",
|
||||
},
|
||||
{
|
||||
golden: JSON.parse(fs.readFileSync(new URL("tests/golden/W-080/pose-constraint-depsgraph.json", root), "utf8")),
|
||||
samplesKey: "samples",
|
||||
},
|
||||
];
|
||||
|
||||
function open(engine, handle, bytes) {
|
||||
const pointer = engine._malloc(bytes.byteLength);
|
||||
try {
|
||||
engine.HEAPU8.set(bytes, pointer);
|
||||
assert.equal(
|
||||
engine._web_engine_open_blend(handle, pointer, bytes.byteLength),
|
||||
0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()),
|
||||
);
|
||||
}
|
||||
finally {
|
||||
engine._free(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
function command(engine, handle, payload) {
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(payload));
|
||||
const pointer = engine._malloc(bytes.byteLength);
|
||||
try {
|
||||
engine.HEAPU8.set(bytes, pointer);
|
||||
assert.equal(
|
||||
engine._web_engine_apply_command(handle, pointer, bytes.byteLength),
|
||||
0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()),
|
||||
);
|
||||
}
|
||||
finally {
|
||||
engine._free(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
function readOutput(engine, handle, fn, owned) {
|
||||
const dataOut = engine._malloc(4);
|
||||
const lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(fn(handle, dataOut, lengthOut), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2];
|
||||
const length = engine.HEAPU32[lengthOut >>> 2];
|
||||
assert.ok(pointer && length, "native response is empty");
|
||||
const bytes = engine.HEAPU8.slice(pointer, pointer + length);
|
||||
if (owned) engine._web_engine_free_buffer(pointer);
|
||||
return bytes;
|
||||
}
|
||||
finally {
|
||||
engine._free(dataOut);
|
||||
engine._free(lengthOut);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(engine, handle) {
|
||||
const bytes = readOutput(engine, handle, engine._web_engine_evaluate_depsgraph, false);
|
||||
return JSON.parse(new TextDecoder().decode(bytes));
|
||||
}
|
||||
|
||||
function save(engine, handle) {
|
||||
return readOutput(engine, handle, engine._web_engine_save_blend, true);
|
||||
}
|
||||
|
||||
function maxError(actual, expected) {
|
||||
assert.equal(actual.length, expected.length);
|
||||
return Math.max(0, ...actual.map((value, index) => Math.abs(value - expected[index])));
|
||||
}
|
||||
|
||||
for (const testCase of cases) {
|
||||
const { golden } = testCase;
|
||||
const source = fs.readFileSync(new URL(`tests/files/web/${golden.fixture}`, root));
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const sourceHandle = engine._web_engine_create();
|
||||
open(engine, sourceHandle, source);
|
||||
command(engine, sourceHandle, { type: "setFrame", frame: 5 });
|
||||
const firstGeneration = save(engine, sourceHandle);
|
||||
engine._web_engine_destroy(sourceHandle);
|
||||
|
||||
const reopenedHandle = engine._web_engine_create();
|
||||
open(engine, reopenedHandle, firstGeneration);
|
||||
const samples = new Map(golden[testCase.samplesKey].map((sample) => [sample.frame, sample]));
|
||||
const frames = testCase.samplesKey === "frames" ? golden.frames.map((sample) => sample.frame) : golden.frames;
|
||||
for (const frame of frames) {
|
||||
command(engine, reopenedHandle, { type: "setFrame", frame });
|
||||
const report = evaluate(engine, reopenedHandle);
|
||||
const expected = samples.get(frame);
|
||||
assert.ok(expected, `missing golden sample at frame ${frame}`);
|
||||
const mesh = report.meshes.find((candidate) => candidate.sourceMeshId === `mesh:${golden.mesh}`);
|
||||
assert.ok(mesh, `round-trip mesh missing at frame ${frame}`);
|
||||
assert.ok(
|
||||
maxError(mesh.positions, expected.positions) <= golden.tolerance.maxPositionError,
|
||||
`${golden.fixture} frame ${frame} position parity failed`,
|
||||
);
|
||||
if (expected.worldMatrix) {
|
||||
assert.ok(
|
||||
maxError(mesh.worldMatrix, expected.worldMatrix) <= golden.tolerance.maxMatrixError,
|
||||
`${golden.fixture} frame ${frame} matrix parity failed`,
|
||||
);
|
||||
}
|
||||
if (expected.bones) {
|
||||
const armature = report.armatures.find((candidate) => candidate.id === `armature:${golden.armature}`);
|
||||
assert.ok(armature, `round-trip armature missing at frame ${frame}`);
|
||||
for (const [boneName, poseMatrix] of Object.entries(expected.bones)) {
|
||||
const bone = armature.bones.find((candidate) => candidate.name === boneName);
|
||||
assert.ok(bone, `${boneName} missing after round-trip at frame ${frame}`);
|
||||
assert.ok(
|
||||
maxError(bone.poseMatrix, poseMatrix) <= golden.tolerance.maxMatrixError,
|
||||
`${boneName} frame ${frame} pose parity failed`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const secondGeneration = save(engine, reopenedHandle);
|
||||
engine._web_engine_destroy(reopenedHandle);
|
||||
const secondHandle = engine._web_engine_create();
|
||||
open(engine, secondHandle, secondGeneration);
|
||||
engine._web_engine_destroy(secondHandle);
|
||||
assert.ok(firstGeneration.byteLength > 0 && secondGeneration.byteLength > 0);
|
||||
}
|
||||
|
||||
process.stdout.write("main-roundtrip-ok fixtures=animation_scene.blend,pose_constraint_scene.blend frames=1,5,10 generations=2\n");
|
||||
Reference in New Issue
Block a user