From 92ce48997def8ac26d973c8c8001a726907567fa Mon Sep 17 00:00:00 2001 From: Ole Morud Date: Sat, 20 Dec 2025 01:16:03 +0100 Subject: [PATCH] Web: Change to a webworker architecture to stop freezing main thread --- .gitignore | 7 + chess-worker.js | 26 ++++ chess.html | 44 ++++--- chess.js | 343 +++++++++++++++++++++++++++--------------------- libc-lite.h | 10 +- sys.h | 7 +- wasm-compat.c | 18 ++- 7 files changed, 277 insertions(+), 178 deletions(-) create mode 100644 .gitignore create mode 100644 chess-worker.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6887c61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +codegen +chess.wasm +mbb_bishop.h +mbb_rook.h +tests +*.dSYM +between_lookup.h diff --git a/chess-worker.js b/chess-worker.js new file mode 100644 index 0000000..685749d --- /dev/null +++ b/chess-worker.js @@ -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) }); + } +}; + diff --git a/chess.html b/chess.html index c31b45a..d4e4887 100644 --- a/chess.html +++ b/chess.html @@ -5,31 +5,37 @@ Wasm Loader - -
+
+

diff --git a/chess.js b/chess.js index c316ba9..4c96158 100644 --- a/chess.js +++ b/chess.js @@ -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({ - Normal: 0, - Check: 1, - Repeats: 2, - Stalemate: 3, - Checkmate: 4, + NORMAL: 0, + CHECK: 1, + REPEATS: 2, + STALEMATE: 3, + CHECKMATE: 4, }); const _moveResultName = [ - "Normal", + "Normal", "Check", - "Repeats", + "Repeats", "Stalemate", "Checkmate", ]; @@ -41,176 +18,248 @@ const _moveResultName = [ const Player = Object.freeze({ White: 0, Black: 1, - None: 2, + None: 2, }); const _playerName = [ "White", "Black", - "None", + "None", ]; const Piece = Object.freeze({ Pawn: 0, King: 1, - Queen: 2, - Bishop: 3, - Rook: 4, - Knight: 5, - Empty: 6, + Queen: 2, + Bishop: 3, + Rook: 4, + Knight: 5, + Empty: 6, }); const _pieceName = [ "Pawn", "King", - "Queen", - "Bishop", - "Rook", - "Knight", - "Empty", + "Queen", + "Bishop", + "Rook", + "Knight", + "Empty", ]; const _pieceChar = [ 'P', 'K', - 'Q', - 'B', - 'R', - 'N', - ' ', + 'Q', + 'B', + 'R', + 'N', + ' ', ]; const _pieceUnicode = [ - ['♙', '♔', '♕', '♗', '♖', '♘',], // white - ['♟', '♚', '♛', '♝', '♜', '♞',], // black - ['' , '', '', '', '', '', ], // none + ['♙', '♔', '♕', '♗', '♖', '♘',], // white + ['♟', '♚', '♛', '♝', '♜', '♞',], // black + ['' , '', '', '', '', '', ], // none ]; const indexDeserialize = (n) => ({ - rank: Math.floor(n / 8), // every day we stray further from God - file: (n % 8), - fileChar() { return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][this.file]; }, - rankChar() { return ['1', '2', '3', '4', '5', '6', '7', '8'][this.rank]; }, + rank: Math.floor(n / 8), // every day we stray further from God + file: (n % 8), + fileChar() { return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][this.file]; }, + 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) => ({ - appeal: Number((n >> 24n) & 0xFFn), - attr: Number((n >> 16n) & 0xFFn), - from: indexDeserialize(Number((n >> 8n) & 0xFFn)), - to: indexDeserialize(Number((n >> 0n) & 0xFFn)), + appeal: Number((n >> 24n) & 0xFFn), + attr: Number((n >> 16n) & 0xFFn), + from: indexDeserialize(Number((n >> 8n) & 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) => ({ - player: n == -1 ? Player.None : (n >> 8) & 0xFF, - piece: n == -1 ? Piece.Empty : (n & 0xFF), - playerName() { - if (this.player === Player.None) { - return "Empty"; - } else { - return _playerName[this.player]; - } - }, - pieceName() { - if (this.player === Player.None) { - return ""; - } else { - return this.playerName() + _pieceName[this.piece]; - } - }, - pieceUnicode() { - return _pieceUnicode[this.player][this.piece]; - } + player: n == -1 ? Player.None : (n >> 8) & 0xFF, + piece: n == -1 ? Piece.Empty : (n & 0xFF), + playerName() { + if (this.player === Player.None) { + return "Empty"; + } else { + return _playerName[this.player]; + } + }, + pieceName() { + if (this.player === Player.None) { + return ""; + } else { + return this.playerName() + _pieceName[this.piece]; + } + }, + pieceUnicode() { + 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 { exports } = await WasmHost.load("./chess.wasm", {}); + const board = await WasmBoard("./chess-worker.js"); - const wb_init = WasmHost.getFn(exports, "wb_init"); - 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"); + const nextFrame = () => new Promise(requestAnimationFrame); - 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 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"]; + const squares = new Array(64); - for (let rank = 7; rank >= 0; --rank) { - for (let file = 0; file < 8; ++file) { - const square = document.createElement("div"); - square.className = - "square " + ((file + rank) % 2 ? "dark" : "light"); - square.dataset.file = filesChars[file]; - square.dataset.rank = ranksChars[rank]; + for (let rank = 7; rank >= 0; --rank) { + for (let file = 0; file < 8; ++file) { + const el = document.createElement("div"); + el.className = + "square " + ((file + rank) % 2 ? "dark" : "light"); + el.dataset.file = filesChars[file]; + el.dataset.rank = ranksChars[rank]; - const x = wb_board_at(indexFromCoord(rank, file)); - const sq = squareDeserialize(x); - square.textContent = sq.pieceUnicode(); + const sq = await board.at(serializedIndexFromCoord(rank, file)); + el.textContent = sq.pieceUnicode(); - board.appendChild(square); - } - } - setTimeout(() => { - // next task, browser had a chance to repaint - }, 0.01); - } - drawBoard(); + boardElem.appendChild(el); + squares[8*rank+file] = el; + } + } - const nextFrame = () => new Promise(requestAnimationFrame); + return squares; + }; - for (let i = 0; i < 200; ++i) { - drawBoard(); + const draw = async (board, squareElems) => { + 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 = moveDeserialize(m); + const move = await board.search(7); - console.log(move.from); - console.log(move.to); + const fromSq = await board.at(move.from); + const toSq = await board.at(move.to); - const fromSqEnc = wb_board_at(indexSerialize(move.from)); - const toSqEnc = wb_board_at(indexSerialize(move.to)); - - console.log(fromSqEnc); - console.log(toSqEnc); - - const fromSq = squareDeserialize(fromSqEnc); - 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; - } - } + const mr = board.move(move); + if (mr == MoveResult.STALEMATE || mr == MoveResult.CHECKMATE) { + const resultEl = document.getElementById("result"); + resultEl.textContent = _moveResultName[board.state]; + draw(board, sqElems); + await nextFrame(); + break; + } + } } run().catch((err) => { diff --git a/libc-lite.h b/libc-lite.h index c2f2fd6..3add4a1 100644 --- a/libc-lite.h +++ b/libc-lite.h @@ -7,12 +7,12 @@ 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; } -uint64_t my_rand64() +static uint64_t my_rand64() { /* * Xorshift RNG [1] + multiply @@ -37,7 +37,7 @@ static int my_isdigit(int ch) 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 ch = (unsigned char)c; @@ -47,7 +47,7 @@ void* my_memset(void *b, int c, size_t len) 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 const* s = src; @@ -73,7 +73,7 @@ int my_memcmp(void const* s1, void const* s2, size_t n) return 0; } -size_t strlen(char const* s) +static size_t strlen(char const* s) { size_t n = 0; while (s[n++]) diff --git a/sys.h b/sys.h index 8b543aa..2c72c39 100644 --- a/sys.h +++ b/sys.h @@ -15,14 +15,13 @@ enum { /* TODO */ 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 void* sys_mmap_anon_shared(size_t size, int, int) { - /* FIXME: this program relies on few memory allocations, a simple bump - * allocator works for now, but will cause memory leaks in the future */ - + /* FIXME: this program relies on very few memory allocations, a simple bump + * allocator works for now, but will cause memory leaks in the future */ size = (size + 7ULL) & ~7ULL; if (g_buf_len + size > sizeof g_buf) { diff --git a/wasm-compat.c b/wasm-compat.c index 8b36033..08749ee 100644 --- a/wasm-compat.c +++ b/wasm-compat.c @@ -40,9 +40,21 @@ uint64_t wb_search(int8_t max_depth) int32_t wb_move(uint32_t move) { - struct move m = move_deserialize(move); - enum move_result const r = board_move_2(&g_board, m); - return (int32_t)r; + struct move const m = move_deserialize(move); + enum move_result const mr = board_move_2(&g_board, m); + + /* 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()