Import C/CPP files
This commit is contained in:
parent
adb8b4549e
commit
665892a798
10
README.md
10
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)})`),
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue