Files
Crystal/tests/reference-parity.spec.ts
T
2026-09-23 09:23:29 +02:00

107 lines
3.4 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
const romPath = resolve(
process.env.CRYSTAL_ROM_PATH ??
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc",
);
test("native decompression matches original GB assembly executing in the reference emulator", async ({
page,
}) => {
test.skip(!existsSync(romPath), "Requires the local user ROM.");
await page.goto("/reference.html");
// File chooser supplies bytes to an isolated test input, without an HTTP ROM endpoint.
await page.evaluate(() => {
const input = document.createElement("input");
input.type = "file";
input.id = "reference-fixture";
document.body.append(input);
});
await page.locator("#reference-fixture").setInputFiles(romPath);
const result = await page.evaluate(async () => {
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
const { decompress } = await import("/src/game/rom/lz.ts");
const { SYMBOLS, NATIVE_MAPS } = await import("/src/game/rom/profile.ts");
const file = (
document.querySelector("#reference-fixture") as HTMLInputElement
).files![0];
const bytes = new Uint8Array(await file.arrayBuffer());
const canvas = document.createElement("canvas");
const reference = new BinjgbEmulator();
const assets = new CrystalAssets(bytes);
await reference.load(bytes, canvas);
const comparisons: {
map: string;
bytes: number;
equal: boolean;
returned: boolean;
}[] = [];
for (const id of Object.keys(NATIVE_MAPS)) {
const map = assets.map(id);
const pointer = assets.reader.far(SYMBOLS.tilesets + map.tileset * 15);
const bank = Math.floor(pointer / 0x4000),
address = 0x4000 + (pointer % 0x4000);
const native = decompress(assets.reader, pointer);
// Original test trampoline in WRAM: DI; SP=$c7fe; A=bank;
// HL=compressed ROM address; DE=$d000; CALL FarDecompress ($0b40);
// write a sentinel and loop. The referenced routine is the user's ROM.
const program = [
0xf3,
0x31,
0xfe,
0xc7,
0x3e,
bank,
0x21,
address & 255,
address >> 8,
0x11,
0,
0xd0,
0xcd,
0x40,
0x0b,
0x3e,
0x42,
0xea,
0xff,
0xc6,
0x18,
0xfe,
];
reference.writeMemory(0xff70, 6);
reference.writeMemory(0xff9d, 1);
reference.writeMemory(0xc6ff, 0);
program.forEach((value, index) =>
reference.writeMemory(0xc000 + index, value),
);
reference.setProgramCounter(0xc000);
reference.advanceFrames(30);
comparisons.push({
map: id,
bytes: native.length,
returned: reference.readMemory(0xc6ff) === 0x42,
equal: native.every(
(value, index) => reference.readMemory(0xd000 + index) === value,
),
});
}
reference.dispose();
return comparisons;
});
expect(result.length).toBe(6);
for (const comparison of result) {
expect(
comparison.returned,
`${comparison.map}: original routine returned`,
).toBe(true);
expect(
comparison.equal,
`${comparison.map}: ${comparison.bytes} decompressed bytes match`,
).toBe(true);
}
});