Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fullstack): compile to WASM modules #296

Merged
merged 10 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions GomokuClient/packages/eslint-config/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default [
"messages.ts",
"src/api/**",
"tailwind.config.js",
"src/ai/**",
"public/build/**",
],
},

Expand Down
Binary file not shown.

Large diffs are not rendered by default.

Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.

Large diffs are not rendered by default.

Binary file not shown.
157 changes: 157 additions & 0 deletions GomokuClient/packages/gomoku-core/src/ai/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { checkSharedArrayBufferSupport } from "./util.js";

const STEngineURL = "/build/rapfi-single.js";
const MTEngineURL = "/build/rapfi-multi.js";
const supportSAB = checkSharedArrayBufferSupport();
var callback, worker;

function init(f) {
callback = f;

if (supportSAB) {
console.log("Attempting to initialize multi-threaded engine...");
import(/*@vite-ignore */ MTEngineURL)
.then(() => {
console.log("Multi-threaded engine imported.");
if (Bridge.ready) {
console.log("Bridge is ready.");
callback({ ok: true });
} else {
Bridge.setReady = () => {
console.log("Bridge is now ready.");
callback({ ok: true });
};
}

Bridge.readStdout = (d) => processOutput(d);
})
.catch((err) => {
console.error("Error importing multi-threaded engine:", err);
callback({ ok: false, error: err });
});
} else {
console.log(
"SharedArrayBuffer not supported. Falling back to single-threaded engine...",
);
worker = new Worker(STEngineURL, { type: "module" });

worker.onmessage = function (e) {
if (e.data.ready) {
console.log("Worker is ready.");
callback({ ok: true });
} else {
processOutput(e.data.output);
}
};

worker.onerror = function (ev) {
worker.terminate();
console.error("Worker spawn error: " + ev.message + ". Retrying...");
setTimeout(() => init(f), 200);
};
}
}

// Returns true if force stoped, otherwise returns false
function stopThinking() {
if (!supportSAB) {
console.warn("No support for SAB, failed to stop thinking.");

worker.terminate();
init(callback); // Use previous callback function

return true;
} else {
sendCommand("YXSTOP");
return false;
}
}

function sendCommand(cmd) {
if (typeof cmd !== "string" || cmd.length == 0) return;

if (supportSAB) {
// eslint-disable-next-line
Bridge.writeStdin(cmd);
} else {
worker.postMessage(cmd);
}
}

function processOutput(output) {
if (typeof callback !== "function") return;

let i = output.indexOf(" ");

if (i == -1) {
if (output == "OK") return;
else if (output == "SWAP") callback({ swap: true });
else {
let coord = output.split(",");
callback({ pos: [+coord[0], +coord[1]] });
}
return;
}

let head = output.substring(0, i);
let tail = output.substring(i + 1);

if (head == "MESSAGE") {
if (tail.startsWith("REALTIME")) {
let r = tail.split(" ");
if (r.length < 3) {
callback({
realtime: {
type: r[1],
},
});
} else {
let coord = r[2].split(",");
callback({
realtime: {
type: r[1],
pos: [+coord[0], +coord[1]],
},
});
}
} else {
callback({ msg: tail });
}
} else if (head == "INFO") {
i = tail.indexOf(" ");
head = tail.substring(0, i);
tail = tail.substring(i + 1);

if (head == "PV") callback({ multipv: tail });
else if (head == "NUMPV") callback({ numpv: +tail });
else if (head == "DEPTH") callback({ depth: +tail });
else if (head == "SELDEPTH") callback({ seldepth: +tail });
else if (head == "NODES") callback({ nodes: +tail });
else if (head == "TOTALNODES") callback({ totalnodes: +tail });
else if (head == "TOTALTIME") callback({ totaltime: +tail });
else if (head == "SPEED") callback({ speed: +tail });
else if (head == "EVAL") callback({ eval: tail });
else if (head == "WINRATE") callback({ winrate: parseFloat(tail) });
else if (head == "BESTLINE")
callback({
bestline: tail.match(/([A-Z]\d+)/g).map((s) => {
let coord = s.match(/([A-Z])(\d+)/);
let x = coord[1].charCodeAt(0) - "A".charCodeAt(0);
let y = +coord[2] - 1;
return [x, y];
}),
});
} else if (head == "ERROR") callback({ error: tail });
else if (head == "FORBID")
callback({
forbid: (tail.match(/.{4}/g) || []).map((s) => {
let coord = s.match(/([0-9][0-9])([0-9][0-9])/);
let x = +coord[1];
let y = +coord[2];
return [x, y];
}),
});
else callback({ unknown: tail });
}

export { init, sendCommand, stopThinking };
121 changes: 121 additions & 0 deletions GomokuClient/packages/gomoku-core/src/ai/rapfi-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
if (typeof Module === "undefined") {
// Bridge object for writing stdin & reading stdout
var Bridge =
typeof Bridge !== "undefined"
? Bridge
: {
ready: false,
writeStdin() {},
readStdout() {},
setReady() {
this.ready = true;
},
};

// Setup webassembly glue module (only for main thread)
var Module = {
preInit: [
function () {
FS.createLazyFile(
"/",
"mix6freestyle_bs15.bin.lz4",
"/network/mix6freestyle_bs15.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6freestyle_bsmix.bin.lz4",
"/network/mix6freestyle_bsmix.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6standard_bs15.bin.lz4",
"/network/mix6standard_bs15.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6renju_bs15_black.bin.lz4",
"/network/mix6renju_bs15_black.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6renju_bs15_white.bin.lz4",
"/network/mix6renju_bs15_white.bin.lz4",
true,
false,
);
},
],
preRun: [
function () {
let input = {
str: "",
index: 0,
set: function (str) {
this.str = str + "\n";
this.index = 0;
},
};

let output = {
str: "",
flush: function () {
Bridge.readStdout(this.str);
this.str = "";
},
};

function stdin() {
// Return ASCII code of character, or null if no input
let char = input.str.charCodeAt(input.index++);
return isNaN(char) ? null : char;
}

function stdout(char) {
if (!char || char == "\n".charCodeAt(0)) {
output.flush();
} else {
output.str += String.fromCharCode(char);
}
}

FS.init(stdin, stdout, stdout);
let pipeLoopOnce = Module.cwrap("gomocupLoopOnce", "number", []);
Bridge.writeStdin = function (data) {
input.set(data);
pipeLoopOnce();
};
},
],
onRuntimeInitialized() {
console.log("Rapfi bridge initialized");
Bridge.setReady();
},
};

// If we are running in a worker, setup onmessage & postMessage
if (typeof importScripts === "function") {
self.onmessage = function (e) {
Bridge.writeStdin(e.data);
};
Bridge.readStdout = function (data) {
postMessage({ output: data });
};
Bridge.setReady = function () {
postMessage({ ready: true });
};
} else {
// Otherwise we are running in the main window, adjust URL
Module.locateFile = function (url) {
return "/build/" + url;
};
Module.mainScriptUrlOrBlob = "/build/rapfi-multi.js";
}
}
17 changes: 17 additions & 0 deletions GomokuClient/packages/gomoku-core/src/ai/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Check whether SharedArrayBuffer is supported
export function checkSharedArrayBufferSupport() {
// Do not request cross origin isolated now
// as it is not widely support on all browsers
//if (!self.crossOriginIsolated) return false

let supportSAB = typeof self.SharedArrayBuffer !== "undefined";
if (supportSAB) {
let tempMemory = new WebAssembly.Memory({
initial: 1,
maximum: 1,
shared: true,
});
supportSAB = tempMemory.buffer instanceof self.SharedArrayBuffer;
}
return supportSAB;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createContext, useContext, useEffect, useState, useMemo } from "react";

import type { PropsWithChildren } from "react";

// @ts-ignore
import { init, sendCommand, stopThinking } from "@/ai/engine.js";

interface WasmEngineContextType {
isEngineReady: boolean;
sendCommand: (cmd: string) => void;
stopThinking: () => void;
}

const WasmEngineContext = createContext<WasmEngineContextType | undefined>(
undefined,
);

export const WasmEngineProvider = ({ children }: PropsWithChildren) => {
const [isEngineReady, setIsEngineReady] = useState(false);

useEffect(() => {
console.log("Initializing engine...");
init(({ ok }: { ok: boolean }) => {
if (ok) {
console.log("Engine initialized successfully.");
setIsEngineReady(true);
} else {
console.error("Engine failed to initialize.");
}
});
}, []);

const contextValue: WasmEngineContextType = useMemo(
() => ({
isEngineReady,
sendCommand,
stopThinking,
}),
[isEngineReady],
);

return (
<WasmEngineContext.Provider value={contextValue}>
{children}
</WasmEngineContext.Provider>
);
};

export const useWasmEngine = (): WasmEngineContextType => {
const context = useContext(WasmEngineContext);
if (!context) {
throw new Error("useWasmEngine must be used within a WasmEngineProvider");
}
return context;
};
Loading
Loading