From 665892a79864e04aaab16b9854091a6bad25a43c Mon Sep 17 00:00:00 2001 From: Pabloader Date: Tue, 9 Jul 2024 21:01:37 +0000 Subject: [PATCH] Import C/CPP files --- README.md | 10 ++++ build/wasmPlugin.ts | 80 +++++++++++++++++++------------ src/games/life/index.ts | 47 ++++++++++++++++++ src/games/life/life.c | 103 ++++++++++++++++++++++++++++++++++++++++ src/types.d.ts | 27 +++++++++++ 5 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 src/games/life/index.ts create mode 100644 src/games/life/life.c diff --git a/README.md b/README.md index 894e3f0..f723e96 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,16 @@ bun run build - Example: `src/games/playground/awoo.wasm.ts` - Triggered by file name `*.wasm.ts` +- Import `*.c`/`*.cpp` files (compile to wasm on the fly) + - Example: `src/games/life/life.c` + - To use, `clang` and wasm toochain should be present in the system + ```bash + sudo apt install clang lld wabt + ``` + - Supports only function exports & `memory` + - exported all non-static functions + - no stdlib + ## Publishing diff --git a/build/wasmPlugin.ts b/build/wasmPlugin.ts index e46c2cf..fc489e9 100644 --- a/build/wasmPlugin.ts +++ b/build/wasmPlugin.ts @@ -1,4 +1,4 @@ -import { plugin, type BunPlugin } from "bun"; +import { plugin, $, type BunPlugin } from "bun"; import path from 'path'; import asc from 'assemblyscript/asc'; @@ -11,40 +11,58 @@ const wasmPlugin = ({ production, portable }: WasmLoaderConfig = {}): BunPlugin const p: BunPlugin = { name: "WASM loader", async setup(build) { - build.onLoad({ filter: /\.wasm\.ts$/ }, async (args) => { - if (portable) { - const contents = await Bun.file(args.path).text(); - return { - contents: `import "assemblyscript/std/portable/index.js";\n${contents}`, - loader: 'tsx', + build.onLoad({ filter: /\.(c(pp)?|wasm(\.ts)?)$/ }, async (args) => { + let wasmPath = path.resolve(import.meta.dir, '..', '..', 'dist', 'tmp.wasm'); + let jsContent: string = ` + async function instantiate(url) { + const { instance } = await WebAssembly.instantiateStreaming(fetch(url)); + return { + ...instance.exports, + data: new DataView(instance.exports.memory.buffer), + }; + } + const module = await instantiate(new URL("tmp.wasm", import.meta.url)); + export default module; + `; + if (args.path.endsWith('.ts')) { + if (portable) { + const contents = await Bun.file(args.path).text(); + return { + contents: `import "assemblyscript/std/portable/index.js";\n${contents}`, + loader: 'tsx', + } + } + const jsPath = wasmPath.replace(/\.wasm$/, '.js'); + const ascArgs = [ + args.path, + '--outFile', wasmPath, + '--bindings', 'esm', + '-Ospeed' + ]; + + const { error, stderr } = await asc.main(ascArgs); + if (error) { + console.error(stderr.toString(), error.message); + throw error; + } + jsContent = await Bun.file(jsPath).text(); + } else if (args.path.endsWith('.wasm')) { + wasmPath = args.path; + } else { + const result = await $`clang --target=wasm32 -O3 --no-standard-libraries -Wl,--export-all -Wl,--no-entry -fno-builtin -o ${wasmPath} ${args.path}`; + if (result.exitCode !== 0) { + throw new Error('Compile failed, check output'); } } - const wasmPath = path.resolve(import.meta.dir, '..', '..', 'dist', 'tmp.wasm'); - const jsPath = wasmPath.replace(/\.wasm$/, '.js'); - const ascArgs = [ - args.path, - '--outFile', wasmPath, - '--bindings', 'esm', - ]; - ascArgs.push(production ? '--optimize' : '--debug'); - const { error, stderr } = await asc.main(ascArgs); + const wasmContent = await Bun.file(wasmPath).arrayBuffer(); + const wasmBuffer = Buffer.from(wasmContent).toString('base64'); + const wasmURL = `data:application/wasm;base64,${wasmBuffer}`; - if (error) { - console.error(stderr.toString(), error.message); - throw error; - } else { - const jsContent = await Bun.file(jsPath).text(); - const wasmContent = await Bun.file(wasmPath).arrayBuffer(); - const wasmBuffer = Buffer.from(wasmContent).toString('base64'); - const wasmURL = `data:application/wasm;base64,${wasmBuffer}`; - - return { - loader: 'js', - contents: jsContent - .replace(/new URL\([^)]*\)/, `new URL(${JSON.stringify(wasmURL)})`), - }; - } + return { + loader: 'js', + contents: jsContent.replace(/new URL\([^)]*\)/, `new URL(${JSON.stringify(wasmURL)})`), + }; }); } }; diff --git a/src/games/life/index.ts b/src/games/life/index.ts new file mode 100644 index 0000000..09fe4fa --- /dev/null +++ b/src/games/life/index.ts @@ -0,0 +1,47 @@ +import life from "./life.c"; + +const width = life.getWidth(); +const height = life.getHeight(); + +const canvas = document.createElement('canvas'); +const context = canvas.getContext('2d')!; +const imageData = context.createImageData(width, height); + +const initField = life.initField as CallableFunction; +const step = life.step as CallableFunction; +const pixels = new Uint8Array(life.memory.buffer, life.getPixels(), width * height * 4); + +export default function main() { + document.body.append(canvas); + initField(Date.now()); + canvas.width = width; + canvas.height = height; + canvas.style.height = '100%'; + canvas.style.imageRendering = 'pixelated'; + + document.body.style.display = 'flex'; + document.body.style.justifyContent = 'center'; + + console.log(life, pixels.length); + + requestAnimationFrame(loop); +} + +let sum = 0; +let count = 0; + +async function loop() { + const start = performance.now(); + step(); + const end = performance.now(); + + sum += end - start; + count++; + + imageData.data.set(pixels); + + context.putImageData(imageData, 0, 0); + context.clearRect(0, 0, 35, 15); + context.fillText(`${(sum / count).toFixed(1)} ms`, 2, 10); + requestAnimationFrame(loop); +} \ No newline at end of file diff --git a/src/games/life/life.c b/src/games/life/life.c new file mode 100644 index 0000000..cc2a0dc --- /dev/null +++ b/src/games/life/life.c @@ -0,0 +1,103 @@ +#include + +#define width 512 +#define height 512 + +static uint8_t field[width * height]; +static uint8_t nextField[width * height]; +static uint8_t pixels[width * height * 4]; + +void* malloc(uint32_t n); +static uint8_t rand8(void); +static int countNeighbours(int x, int y); + +int getWidth() { return width; } +int getHeight() { return height; } +uint8_t* getPixels() { return pixels; } + +void step() +{ + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int count = countNeighbours(x, y); + uint8_t currentCell = field[x + y * width]; + int nextCell = currentCell; + if (currentCell && count < 2) nextCell = 0; + else if (currentCell && count > 3) nextCell = 0; + else if (!currentCell && count == 3) nextCell = 1; + + nextField[x + y * width] = nextCell; + } + } + + for (int i = 0; i < width * height; i++) { + field[i] = nextField[i]; + uint8_t px = field[i]; + + if (px) { + pixels[(i << 2) + 0] = 0; + pixels[(i << 2) + 1] = 0; + pixels[(i << 2) + 2] = 0; + } else { + pixels[(i << 2) + 0] = 255; + pixels[(i << 2) + 1] = 255; + pixels[(i << 2) + 2] = 255; + } + pixels[(i << 2) + 3] = 255; + } +} + +static int countNeighbours(int x, int y) +{ + int count = 0; + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + if (i == 0 && j == 0) continue; + + int xx = (x + i + width) % width; + int yy = (y + j + height) % height; + + if (field[xx + yy * width]) { + count++; + } + } + } + return count; +} + +/** Random generator implementation from https://stackoverflow.com/a/16761955 */ +#define STATE_BYTES 7 +#define MULT 0x13B /* for STATE_BYTES==6 only */ +#define MULT_LO (MULT & 255) +#define MULT_HI (MULT & 256) + +static uint8_t rand_state[STATE_BYTES] = { 0x87, 0xdd, 0xdc, 0x10, 0x35, 0xbc, 0x5c }; + +static uint8_t rand8(void) +{ + static uint16_t c = 0x42; + static int i = 0; + uint16_t t; + uint8_t x; + + x = rand_state[i]; + t = (uint16_t)x * MULT_LO + c; + c = t >> 8; +#if MULT_HI + c += x; +#endif + x = t & 255; + rand_state[i] = x; + if (++i >= sizeof(rand_state)) + i = 0; + return x; +} + +void initField(uint32_t randomSeed) +{ + *((uint32_t*)&rand_state[STATE_BYTES - sizeof(uint32_t)]) = randomSeed; // Voodoo + + for (int i = 0; i < width * height; i++) { + field[i] = rand8() & 1; + } +} diff --git a/src/types.d.ts b/src/types.d.ts index 94dcd03..8237dc1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -34,4 +34,31 @@ declare module "*.mp3" { declare module "*.ogg" { const audio: HTMLAudioElement; export default audio; +} +declare module "*.wasm" { + const instance: { + memory: WebAssembly.Memory; + data: DataView; + + [x: string]: (...args: any) => any; + }; + export default instance; +} +declare module "*.c" { + const instance: { + memory: WebAssembly.Memory; + data: DataView; + + [x: string]: (...args: any) => any; + }; + export default instance; +} +declare module "*.cpp" { + const instance: { + memory: WebAssembly.Memory; + data: DataView; + + [x: string]: (...args: any) => any; + }; + export default instance; } \ No newline at end of file