Files
Ustagammon/web/engine-wasm.mjs
Pierre MARTINandClaude Opus 5.5 ae1ea54782 Moteur WebAssembly : remettre la pile en place après chaque commande
callMain (Emscripten) copie les arguments sur la pile du WebAssembly
(64 Ko) sans les retirer. Pendant une partie dans le navigateur, le même
moteur reçoit des centaines de commandes : vers la 540e, la pile débordait
et corrompait la mémoire du moteur (moteur muet dans Firefox, onglet planté
dans Chromium, « memory access out of bounds » sous Node). Repéré par
make test-browser. stackSave et stackRestore sont exportés et encadrent
chaque commande ; un test en envoie 3000 à la suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:27:24 +01:00

88 lines
3.5 KiB
JavaScript

// engine-wasm.mjs -- le moteur WebAssembly vu depuis JavaScript.
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
//
// Le WebAssembly (build/wasm/, voir engine-wasm/wasm.mk) contient le moteur
// et le programme engine-cli. On charge le module une fois, on initialise le
// moteur, puis chaque commande est un appel à main avec les mêmes arguments
// qu'engine-cli en ligne de commande : la sortie est donc le même JSON.
//
// Utilisé par le Web Worker de la page (engine-worker.mjs) et, sous Node,
// par engine-wasm/engine-cli.mjs et engine-wasm/bench.mjs. Le moteur calcule
// sur le fil qui l'appelle : dans le navigateur, toujours dans un Worker.
// Dossier des données dans le système de fichiers virtuel (--preload-file).
const DATA_DIR = "/data";
// createEngineModule : la fonction exportée par build/wasm/engine.mjs.
// wasmDir : où trouver engine.wasm et engine.data, terminé par « / » (une
// adresse dans le navigateur, un chemin de fichier sous Node).
// Renvoie le moteur prêt, avec :
// - runText(args) : { text, exitCode }, la sortie brute d'engine-cli ;
// - run(args) : { result, ms }, le JSON décodé et le temps de calcul
// (lève une erreur si le moteur en signale une) ;
// - memoryBytes() : taille actuelle de la mémoire WebAssembly.
export async function loadEngine(createEngineModule, wasmDir) {
let output = [];
const module = await createEngineModule({
locateFile: (name) => wasmDir + name,
print: (line) => output.push(line),
printErr: (line) => output.push(line),
});
const status = module.ccall("engine_init", "number", ["string"], [DATA_DIR]);
if (status !== 0) {
throw new Error("le moteur n'a pas pu charger ses données (code " + status + ")");
}
// callMain copie les arguments sur la pile du WebAssembly sans jamais
// les retirer : on remet la pile en place après chaque commande, sinon
// elle déborde au bout de quelques centaines de commandes (environ 540
// pendant une partie) et le moteur plante ou ne répond plus.
function runText(args) {
output = [];
const stack = module.stackSave();
try {
const exitCode = module.callMain(args.map(String));
return { text: output.join("\n"), exitCode };
} finally {
module.stackRestore(stack);
}
}
function run(args) {
const start = performance.now();
const { text } = runText(args);
const ms = performance.now() - start;
const result = JSON.parse(text);
if (result.error) {
throw new Error(result.error.message);
}
return { result, ms };
}
return {
runText,
run,
memoryBytes: () => module.HEAPU8.length,
};
}
// Arguments d'engine-cli pour chaque fonction de l'interface. Les dés sont
// un tableau [3, 1] ou une chaîne « 31 ».
export const commands = {
bestMove: (positionId, matchId, dice, ply) =>
["--ply", ply, "bestmove", positionId, matchId, diceText(dice)],
cubeDecision: (positionId, matchId, ply) =>
["--ply", ply, "cube", positionId, matchId],
evaluate: (positionId, matchId, ply) =>
["--ply", ply, "eval", positionId, matchId],
reference: (positionId, matchId, dice, ply) =>
["--ply", ply, "reference", positionId, matchId, ...(dice ? [diceText(dice)] : [])],
};
function diceText(dice) {
return Array.isArray(dice) ? dice.join("") : String(dice);
}