112 lines
5.5 KiB
JavaScript
112 lines
5.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { readFileSync } from 'node:fs';
|
|
import { Worker } from 'node:worker_threads';
|
|
import { performance } from 'node:perf_hooks';
|
|
import { generateMap } from './mapPipeline.js';
|
|
import { createWorldMap } from './worldMap.js';
|
|
import { MAP_H } from './mapUtils.js';
|
|
import { collectTransferableBuffers } from './transferUtils.js';
|
|
|
|
function hashView(view) {
|
|
return createHash('sha256').update(Buffer.from(view.buffer, view.byteOffset, view.byteLength)).digest('hex');
|
|
}
|
|
function derivePatchSeed(baseSeed, rect, terrainType, variant = 0) {
|
|
let h = (baseSeed >>> 0) ^ 0x9e3779b9;
|
|
h = Math.imul(h ^ (rect.x0 | 0), 1664525) >>> 0;
|
|
h = Math.imul(h ^ (rect.y0 | 0), 1013904223) >>> 0;
|
|
h = Math.imul(h ^ (rect.x1 | 0), 2246822519) >>> 0;
|
|
h = Math.imul(h ^ (rect.y1 | 0), 3266489917) >>> 0;
|
|
h = Math.imul(h ^ (variant >>> 0), 668265263) >>> 0;
|
|
for (const ch of String(terrainType || 'auto')) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
|
|
return h >>> 0;
|
|
}
|
|
function browserWorkerAdapter(moduleUrl) {
|
|
const target = moduleUrl.href;
|
|
const code = `
|
|
import { parentPort } from 'node:worker_threads';
|
|
globalThis.self = { onmessage: null, postMessage(message, transfer) { parentPort.postMessage(message, transfer); } };
|
|
await import(${JSON.stringify(target)});
|
|
parentPort.on('message', (data) => self.onmessage?.({ data }));
|
|
`;
|
|
return new Worker(new URL(`data:text/javascript,${encodeURIComponent(code)}`), { type: 'module', execArgv: [] });
|
|
}
|
|
async function runPatchWorker(baseWorld, rect, baseSeed, variant) {
|
|
const worker = browserWorkerAdapter(new URL('./mapPatchWorker.js', import.meta.url));
|
|
const preview = structuredClone(baseWorld);
|
|
const transfer = Array.from(collectTransferableBuffers(preview));
|
|
const started = performance.now();
|
|
const message = await new Promise((resolve, reject) => {
|
|
worker.on('message', (m) => { if (m?.type !== 'progress') resolve(m); });
|
|
worker.on('error', reject);
|
|
worker.postMessage({
|
|
id: variant + 1,
|
|
world: preview,
|
|
rect,
|
|
options: {
|
|
patchMode: 'expansion', terrainType: 'auto',
|
|
seed: derivePatchSeed(baseSeed, rect, 'auto', variant),
|
|
variant, maxQualityRetries: 0, qualityTerrainAttempts: 1,
|
|
},
|
|
}, transfer);
|
|
});
|
|
await worker.terminate();
|
|
assert.equal(message.ok, true, `variant ${variant} worker failed: ${message.error || 'unknown'}`);
|
|
assert.equal(message.result?.ok, true, `variant ${variant} patch failed: ${message.result?.code || message.result?.reason || 'unknown'}`);
|
|
return { message, seconds: (performance.now() - started) / 1000 };
|
|
}
|
|
|
|
const appSource = readFileSync(new URL('./app.js', import.meta.url), 'utf8');
|
|
const viewportSource = readFileSync(new URL('./worldViewport.js', import.meta.url), 'utf8');
|
|
const rendererSource = readFileSync(new URL('./renderer.js', import.meta.url), 'utf8');
|
|
assert.match(appSource, /maxQualityRetries:\s*0/);
|
|
assert.match(appSource, /patchBusy/);
|
|
assert.match(appSource, /previewPatchDelta/);
|
|
assert.match(appSource, /renderRevision/);
|
|
assert.match(appSource, /worker\.terminate\?\.\(\)/);
|
|
assert.match(viewportSource, /viewport\.renderRevision/);
|
|
assert.match(rendererSource, /map\?\.renderRevision/);
|
|
|
|
const baseSeed = 1;
|
|
const baseWorld = createWorldMap(generateMap(baseSeed, { terrainType: 'auto', onProgress() {} }));
|
|
const ox = baseWorld.originX;
|
|
const oy = baseWorld.originY;
|
|
const rect = { kind: 'lasso', polygon: [
|
|
{ x: ox - 145, y: oy - 14 },
|
|
{ x: ox + 8, y: oy - 14 },
|
|
{ x: ox + 8, y: oy + MAP_H + 14 },
|
|
{ x: ox - 145, y: oy + MAP_H + 14 },
|
|
] };
|
|
rect.x0 = ox - 145; rect.y0 = oy - 14; rect.x1 = ox + 8; rect.y1 = oy + MAP_H + 14;
|
|
|
|
const a = await runPatchWorker(baseWorld, rect, baseSeed, 0);
|
|
const b = await runPatchWorker(baseWorld, rect, baseSeed, 1);
|
|
const aWorld = a.message.world;
|
|
const bWorld = b.message.world;
|
|
const hashesA = {
|
|
elevation: hashView(aWorld.fields.elevation), sea: hashView(aWorld.fields.sea), admin: hashView(aWorld.fields.adminId),
|
|
};
|
|
const hashesB = {
|
|
elevation: hashView(bWorld.fields.elevation), sea: hashView(bWorld.fields.sea), admin: hashView(bWorld.fields.adminId),
|
|
};
|
|
assert.notEqual(hashesA.elevation, hashesB.elevation, 'Alternative variants generated identical elevation');
|
|
assert.notEqual(hashesA.sea, hashesB.sea, 'Alternative variants generated identical sea mask');
|
|
assert.notEqual(hashesA.admin, hashesB.admin, 'Alternative variants generated identical administration');
|
|
assert.equal(a.message.result.qualityRetryCount || 0, 0);
|
|
assert.equal(b.message.result.qualityRetryCount || 0, 0);
|
|
assert.equal(a.message.result.candidateQuality?.selectedVariant, 0, 'variant 0 did not render its exact requested terrain variant');
|
|
assert.equal(b.message.result.candidateQuality?.selectedVariant, 1, 'variant 1 did not render its exact requested terrain variant');
|
|
assert.equal(a.message.result.seamDiagnostics?.status, 'clean');
|
|
assert.equal(b.message.result.seamDiagnostics?.status, 'clean');
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
interactiveRetryPolicy: 'one requested variant per click; no hidden whole-patch retry',
|
|
variants: [
|
|
{ variant: 0, seconds: Math.round(a.seconds * 100) / 100, selectedVariant: a.message.result.candidateQuality?.selectedVariant, hashes: hashesA },
|
|
{ variant: 1, seconds: Math.round(b.seconds * 100) / 100, selectedVariant: b.message.result.candidateQuality?.selectedVariant, hashes: hashesB },
|
|
],
|
|
alternativesDiffer: true,
|
|
explicitRenderRevision: true,
|
|
singlePatchWorkerLifetime: true,
|
|
}, null, 2));
|