Web: Change to a webworker architecture to stop freezing main thread

This commit is contained in:
2025-12-20 01:16:03 +01:00
parent 66feaab027
commit 92ce48997d
7 changed files with 277 additions and 178 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
codegen
chess.wasm
mbb_bishop.h
mbb_rook.h
tests
*.dSYM
between_lookup.h

26
chess-worker.js Normal file
View File

@@ -0,0 +1,26 @@
let exports;
async function init() {
const resp = await fetch("./chess.wasm");
if (!resp.ok) {
throw new Error("fetch wasm failed ${resp.status} ${resp.statusText}");
}
const { instance } =
await WebAssembly.instantiateStreaming(resp, {});
exports = instance.exports;
}
await init();
self.postMessage({ type: "ready" });
self.onmessage = (e) => {
const { id, method, args = [] } = e.data;
try {
const value = exports[method](...args);
self.postMessage({ id, ok: true, value });
} catch (err) {
self.postMessage({ id, ok: false, error: String(err?.message ?? err) });
}
};

View File

@@ -5,31 +5,37 @@
<title>Wasm Loader</title> <title>Wasm Loader</title>
<script type="module" src="./chess.js" defer></script> <script type="module" src="./chess.js" defer></script>
<style> <style>
#board { #board {
display: grid; display: grid;
grid-template-columns: repeat(8, 60px); grid-template-columns: repeat(8, 60px);
grid-template-rows: repeat(8, 60px); grid-template-rows: repeat(8, 60px);
width: 480px; width: 480px;
} }
.square { .square {
width: 60px; width: 60px;
height: 60px; height: 60px;
font-size: 57px; font-size: 55px;
} text-align: center;
}
.square.light { .square.light {
background: #f0d9b5; background: #f0d9b5;
} }
.square.dark { .square.dark {
background: #b58863; background: #b58863;
} }
.square.blue {
background: blue;
}
</style> </style>
</head> </head>
<body> <body>
<div id="board"></div> <div id="board"></div>
<p id="result"></div>
</body> </body>
</html> </html>

343
chess.js
View File

@@ -1,39 +1,16 @@
const WasmHost = (() => {
async function load(url, imports = {}) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
}
const bytes = await res.arrayBuffer();
const result = await WebAssembly.instantiate(bytes, imports);
const instance = result.instance ?? result;
return { instance, exports: instance.exports };
}
function getFn(exportsObj, name) {
const fn = exportsObj[name];
if (typeof fn !== "function") {
throw new Error(`Export "${name}" is not a function`);
}
return fn;
}
return { load, getFn };
})();
const MoveResult = Object.freeze({ const MoveResult = Object.freeze({
Normal: 0, NORMAL: 0,
Check: 1, CHECK: 1,
Repeats: 2, REPEATS: 2,
Stalemate: 3, STALEMATE: 3,
Checkmate: 4, CHECKMATE: 4,
}); });
const _moveResultName = [ const _moveResultName = [
"Normal", "Normal",
"Check", "Check",
"Repeats", "Repeats",
"Stalemate", "Stalemate",
"Checkmate", "Checkmate",
]; ];
@@ -41,176 +18,248 @@ const _moveResultName = [
const Player = Object.freeze({ const Player = Object.freeze({
White: 0, White: 0,
Black: 1, Black: 1,
None: 2, None: 2,
}); });
const _playerName = [ const _playerName = [
"White", "White",
"Black", "Black",
"None", "None",
]; ];
const Piece = Object.freeze({ const Piece = Object.freeze({
Pawn: 0, Pawn: 0,
King: 1, King: 1,
Queen: 2, Queen: 2,
Bishop: 3, Bishop: 3,
Rook: 4, Rook: 4,
Knight: 5, Knight: 5,
Empty: 6, Empty: 6,
}); });
const _pieceName = [ const _pieceName = [
"Pawn", "Pawn",
"King", "King",
"Queen", "Queen",
"Bishop", "Bishop",
"Rook", "Rook",
"Knight", "Knight",
"Empty", "Empty",
]; ];
const _pieceChar = [ const _pieceChar = [
'P', 'P',
'K', 'K',
'Q', 'Q',
'B', 'B',
'R', 'R',
'N', 'N',
' ', ' ',
]; ];
const _pieceUnicode = [ const _pieceUnicode = [
['♙', '♔', '♕', '♗', '♖', '♘',], // white ['♙', '♔', '♕', '♗', '♖', '♘',], // white
['♟', '♚', '♛', '♝', '♜', '♞',], // black ['♟', '♚', '♛', '♝', '♜', '♞',], // black
['' , '', '', '', '', '', ], // none ['' , '', '', '', '', '', ], // none
]; ];
const indexDeserialize = (n) => ({ const indexDeserialize = (n) => ({
rank: Math.floor(n / 8), // every day we stray further from God rank: Math.floor(n / 8), // every day we stray further from God
file: (n % 8), file: (n % 8),
fileChar() { return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][this.file]; }, fileChar() { return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][this.file]; },
rankChar() { return ['1', '2', '3', '4', '5', '6', '7', '8'][this.rank]; }, rankChar() { return ['1', '2', '3', '4', '5', '6', '7', '8'][this.rank]; },
}); });
const indexFromCoord = (rank, file) => (8*rank + file); const serializedIndexFromCoord = (rank, file) => (8*rank + file);
const indexSerialize = (index) => (indexFromCoord(index.rank, index.file)); const indexSerialize = (index) => (serializedIndexFromCoord(index.rank, index.file));
const moveDeserialize = (n) => ({ const moveDeserialize = (n) => ({
appeal: Number((n >> 24n) & 0xFFn), appeal: Number((n >> 24n) & 0xFFn),
attr: Number((n >> 16n) & 0xFFn), attr: Number((n >> 16n) & 0xFFn),
from: indexDeserialize(Number((n >> 8n) & 0xFFn)), from: indexDeserialize(Number((n >> 8n) & 0xFFn)),
to: indexDeserialize(Number((n >> 0n) & 0xFFn)), to: indexDeserialize(Number((n >> 0n) & 0xFFn)),
}); });
const moveSerialize = (move) => ((move.from & 0xFF) << 8) | ((move.to & 0xFF)); const moveSerialize = (move) => (
(indexSerialize(move.from) & 0xFF) << 8) | ((indexSerialize(move.to) & 0xFF));
const squareDeserialize = (n) => ({ const squareDeserialize = (n) => ({
player: n == -1 ? Player.None : (n >> 8) & 0xFF, player: n == -1 ? Player.None : (n >> 8) & 0xFF,
piece: n == -1 ? Piece.Empty : (n & 0xFF), piece: n == -1 ? Piece.Empty : (n & 0xFF),
playerName() { playerName() {
if (this.player === Player.None) { if (this.player === Player.None) {
return "Empty"; return "Empty";
} else { } else {
return _playerName[this.player]; return _playerName[this.player];
} }
}, },
pieceName() { pieceName() {
if (this.player === Player.None) { if (this.player === Player.None) {
return ""; return "";
} else { } else {
return this.playerName() + _pieceName[this.piece]; return this.playerName() + _pieceName[this.piece];
} }
}, },
pieceUnicode() { pieceUnicode() {
return _pieceUnicode[this.player][this.piece]; return _pieceUnicode[this.player][this.piece];
} }
}); });
const WasmBoard = async (url) => {
/*
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
}
const bytes = await res.arrayBuffer();
const instantiated = await WebAssembly.instantiate(bytes, {});
const instance = instantiated.instance ?? instantiated;
const exports = instance.exports;
const wb_init = exports.wb_init;
const wb_move = exports.wb_move;
const wb_search = exports.wb_search;
const wb_board_at = exports.wb_board_at;
*/
const worker = new Worker("./chess-worker.js", { type: "module" });
await new Promise((resolve, reject) => {
worker.addEventListener("message", (e) => {
if (e.data?.type === "ready") {
resolve();
}
}, { once: true });
worker.addEventListener("error", reject, { once: true });
});
const wasmCall = (method, args = []) => new Promise((resolve, reject) => {
const id = crypto.randomUUID();
const onMessage = (e) => {
if (e.data?.id !== id) {
return;
}
cleanup();
if (e.data.ok) {
resolve(e.data.value);
} else {
reject(new Error(e.data.error));
}
};
const onError = (err) => {
cleanup();
reject(err);
};
const onMessageError = (e) => {
cleanup();
reject(new Error("Worker message deserialization failed (messageerror)."));
};
const cleanup = () => {
worker.removeEventListener("message", onMessage);
worker.removeEventListener("error", onError);
worker.removeEventListener("messageerror", onMessageError);
};
worker.addEventListener("message", onMessage);
worker.addEventListener("error", onError);
worker.addEventListener("messageerror", onMessageError);
worker.postMessage({ id, method, args });
});
await wasmCall("wb_init", []);
return {
state: MoveResult.Normal,
ongoing: function() /* nil -> bool */ {
return (this.state != MoveResult.STALEMATE
&& this.state != MoveResult.CHECKMATE);
},
search: async function(depth) /* Int -> Move */ {
return wasmCall("wb_search", [depth])
.then(moveDeserialize);
},
move: async function (move) /* (Move | SerializedMove) -> MoveResult */ {
const m = (typeof move === "number") ? move : moveSerialize(move);
return wasmCall("wb_move", [m]);
},
at: async function (index) /* (Index | SerializedIndex) -> Square */ {
const i = (typeof index === "number") ? index : indexSerialize(index);
return wasmCall("wb_board_at", [i])
.then(squareDeserialize);
},
};
};
const run = async () => { const run = async () => {
const { exports } = await WasmHost.load("./chess.wasm", {}); const board = await WasmBoard("./chess-worker.js");
const wb_init = WasmHost.getFn(exports, "wb_init"); const nextFrame = () => new Promise(requestAnimationFrame);
const wb_move = WasmHost.getFn(exports, "wb_move");
const wb_search = WasmHost.getFn(exports, "wb_search");
const wb_board_at = WasmHost.getFn(exports, "wb_board_at");
wb_init(); const createBoardUI = async (board) => {
const boardElem = document.getElementById("board");
boardElem.replaceChildren();
const filesChars = ["a","b","c","d","e","f","g","h"];
const ranksChars = ["8","7","6","5","4","3","2","1"];
const drawBoard = () => { const squares = new Array(64);
const board = document.getElementById("board");
board.replaceChildren();
const filesChars = ["a","b","c","d","e","f","g","h"];
const ranksChars = ["8","7","6","5","4","3","2","1"];
for (let rank = 7; rank >= 0; --rank) { for (let rank = 7; rank >= 0; --rank) {
for (let file = 0; file < 8; ++file) { for (let file = 0; file < 8; ++file) {
const square = document.createElement("div"); const el = document.createElement("div");
square.className = el.className =
"square " + ((file + rank) % 2 ? "dark" : "light"); "square " + ((file + rank) % 2 ? "dark" : "light");
square.dataset.file = filesChars[file]; el.dataset.file = filesChars[file];
square.dataset.rank = ranksChars[rank]; el.dataset.rank = ranksChars[rank];
const x = wb_board_at(indexFromCoord(rank, file)); const sq = await board.at(serializedIndexFromCoord(rank, file));
const sq = squareDeserialize(x); el.textContent = sq.pieceUnicode();
square.textContent = sq.pieceUnicode();
board.appendChild(square); boardElem.appendChild(el);
} squares[8*rank+file] = el;
} }
setTimeout(() => { }
// next task, browser had a chance to repaint
}, 0.01);
}
drawBoard();
const nextFrame = () => new Promise(requestAnimationFrame); return squares;
};
for (let i = 0; i < 200; ++i) { const draw = async (board, squareElems) => {
drawBoard(); for (let rank = 7; rank >= 0; --rank) {
for (let file = 0; file < 8; ++file) {
const sq = await board.at(serializedIndexFromCoord(rank, file));
squareElems[8*rank+file].textContent = sq.pieceUnicode();
}
}
};
await nextFrame(); const sqElems = await createBoardUI(board);
for (let i = 0; i < 200; ++i) {
await draw(board, sqElems);
await nextFrame();
const m = wb_search(7); const move = await board.search(7);
const move = moveDeserialize(m);
console.log(move.from); const fromSq = await board.at(move.from);
console.log(move.to); const toSq = await board.at(move.to);
const fromSqEnc = wb_board_at(indexSerialize(move.from)); const mr = board.move(move);
const toSqEnc = wb_board_at(indexSerialize(move.to)); if (mr == MoveResult.STALEMATE || mr == MoveResult.CHECKMATE) {
const resultEl = document.getElementById("result");
console.log(fromSqEnc); resultEl.textContent = _moveResultName[board.state];
console.log(toSqEnc); draw(board, sqElems);
await nextFrame();
const fromSq = squareDeserialize(fromSqEnc); break;
const toSq = squareDeserialize(toSqEnc); }
}
console.log(fromSq)
console.log(toSq)
console.log("from-player:", fromSq.playerName());
console.log("from-piece:", fromSq.pieceName());
console.log("to-player:", toSq.playerName());
console.log("to-piece:", toSq.pieceName());
console.log(m);
const f = move.from;
const t = move.to;
console.log("from:", move.from.fileChar(), move.from.rankChar(),
"to:", move.to.fileChar(), move.to.rankChar());
const mr = wb_move(Number(m));
if (mr == MoveResult.Stalemate || mr == MoveResult.Checkmate) {
console.log(_moveResultName[mr]);
break;
}
}
} }
run().catch((err) => { run().catch((err) => {

View File

@@ -7,12 +7,12 @@
static uint64_t g_rand_seed = 1; static uint64_t g_rand_seed = 1;
void my_srand(uint64_t n) static void my_srand(uint64_t n)
{ {
g_rand_seed = n == 0 ? 1 : n; g_rand_seed = n == 0 ? 1 : n;
} }
uint64_t my_rand64() static uint64_t my_rand64()
{ {
/* /*
* Xorshift RNG [1] + multiply * Xorshift RNG [1] + multiply
@@ -37,7 +37,7 @@ static int my_isdigit(int ch)
return ch >= '0' && ch <= '9'; return ch >= '0' && ch <= '9';
} }
void* my_memset(void *b, int c, size_t len) static void* my_memset(void *b, int c, size_t len)
{ {
unsigned char* x = b; unsigned char* x = b;
unsigned char ch = (unsigned char)c; unsigned char ch = (unsigned char)c;
@@ -47,7 +47,7 @@ void* my_memset(void *b, int c, size_t len)
return b; return b;
} }
void* my_memcpy(void* restrict dst, void const* restrict src, size_t n) static void* my_memcpy(void* restrict dst, void const* restrict src, size_t n)
{ {
uint8_t* d = dst; uint8_t* d = dst;
uint8_t const* s = src; uint8_t const* s = src;
@@ -73,7 +73,7 @@ int my_memcmp(void const* s1, void const* s2, size_t n)
return 0; return 0;
} }
size_t strlen(char const* s) static size_t strlen(char const* s)
{ {
size_t n = 0; size_t n = 0;
while (s[n++]) while (s[n++])

7
sys.h
View File

@@ -15,14 +15,13 @@ enum { /* TODO */
SYS_MADV_SEQUENTIAL = 0, SYS_MADV_SEQUENTIAL = 0,
}; };
static uint8_t g_buf[1024*1024*1024]; static uint8_t g_buf[1024*1024*1024 /* 1 GB */];
static size_t g_buf_len = 0; static size_t g_buf_len = 0;
static void* sys_mmap_anon_shared(size_t size, int, int) static void* sys_mmap_anon_shared(size_t size, int, int)
{ {
/* FIXME: this program relies on few memory allocations, a simple bump /* FIXME: this program relies on very few memory allocations, a simple bump
* allocator works for now, but will cause memory leaks in the future */ * allocator works for now, but will cause memory leaks in the future */
size = (size + 7ULL) & ~7ULL; size = (size + 7ULL) & ~7ULL;
if (g_buf_len + size > sizeof g_buf) { if (g_buf_len + size > sizeof g_buf) {

View File

@@ -40,9 +40,21 @@ uint64_t wb_search(int8_t max_depth)
int32_t wb_move(uint32_t move) int32_t wb_move(uint32_t move)
{ {
struct move m = move_deserialize(move); struct move const m = move_deserialize(move);
enum move_result const r = board_move_2(&g_board, m); enum move_result const mr = board_move_2(&g_board, m);
return (int32_t)r;
/* TODO: this checkmate/stalemate check needs to be abstracted better */
if (mr == MR_STALEMATE) {
return (int32_t)MR_STALEMATE;
}
struct move moves[MOVE_MAX];
size_t move_count = 0ULL;
all_moves(&g_board.pos, opposite_player(g_board.pos.player), &move_count, moves);
if (move_count == 0ULL) {
return MR_CHECKMATE;
}
return (int32_t)mr;
} }
void wb_init() void wb_init()