diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fe1cc65 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +IndentWidth: 4 +ColumnLimit: 240 +PointerAlignment: Left \ No newline at end of file diff --git a/README.md b/README.md index f723e96..3a23b87 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,7 @@ bun run build sudo apt install clang lld wabt ``` - Supports only function exports & `memory` - - exported all non-static functions - - no stdlib + - No stdlib ## Publishing diff --git a/build/assets/stdlib.c b/build/assets/stdlib.c new file mode 100644 index 0000000..ecdbf15 --- /dev/null +++ b/build/assets/stdlib.c @@ -0,0 +1,69 @@ +#include + +#define BLOCK_SIZE (64 * 1024) +#define BLOCK_1MB 16 + +static uintptr_t bump_pointer = (uintptr_t)&__heap_base; +static uintptr_t heap_end = BLOCK_1MB * BLOCK_SIZE; +IMPORT(grow) void grow(uint32_t blocks); + +static void* bump_alloc(uintptr_t n) { + uintptr_t r = bump_pointer; + bump_pointer += n; + + while (bump_pointer >= heap_end) { + grow(heap_end / BLOCK_SIZE); + heap_end *= 2; + } + + return (void*)r; +} + +void* malloc(uintptr_t n) { + return bump_alloc(n); +} + +void free(void* p) { (void)p; } + +void* memset(void* s, uint8_t c, uint32_t n) { + uint8_t* p = (uint8_t*)s; + while (n--) { + *p++ = c; + } + return s; +} + +void* memcpy(void* dest, const void* src, uint32_t n) { + uint8_t* d = (uint8_t*)dest; + const uint8_t* s = (const uint8_t*)src; + while (n--) { + *d++ = *s++; + } + return dest; +} + +int memcmp(const void* s1, const void* s2, uint32_t n) { + const uint8_t* p1 = (const uint8_t*)s1; + const uint8_t* p2 = (const uint8_t*)s2; + + while (n--) { + if (*p1 != *p2) { + return (*p1 - *p2); + } + p1++; + p2++; + } + + return 0; // Memory blocks are equal +} + +static uint64_t rand_state; + +void srand(uint64_t seed) { rand_state = seed; } + +uint64_t rand(void) { + uint64_t z = (rand_state += 0x9e3779b97f4a7c15); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; + z = (z ^ (z >> 27)) * 0x94d049bb133111eb; + return z ^ (z >> 31); +} diff --git a/build/assets/stdlib.h b/build/assets/stdlib.h new file mode 100644 index 0000000..3639e49 --- /dev/null +++ b/build/assets/stdlib.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +extern unsigned char __heap_base; + +#define IMPORT(name) __attribute__((import_module("env"), import_name(#name))) +#define EXPORT(name) __attribute__((export_name(#name))) + +EXPORT(malloc) void* malloc(uintptr_t); +void free(void*); + +void* memset(void* s, uint8_t c, uint32_t n); +void* memcpy(void* dest, const void* src, uint32_t n); +int memcmp(const void* s1, const void* s2, uint32_t n); + +IMPORT(log) void print_int(int64_t); + +EXPORT(__srand) void srand(uint64_t seed); +uint64_t rand(void); diff --git a/build/build.ts b/build/build.ts index 390f192..20ae4df 100644 --- a/build/build.ts +++ b/build/build.ts @@ -3,7 +3,7 @@ import { $ } from 'bun'; import path from 'path'; import fs from 'fs/promises'; import { buildHTML } from "./html"; -import inquirer from 'inquirer'; +import select from '@inquirer/select'; import { isGame, getGames } from './isGame'; const outDir = path.resolve(import.meta.dir, '..', '..', 'dist'); @@ -13,12 +13,10 @@ let game = process.argv[2]; const publish = process.env.PUBLISH_LOCATION; while (!await isGame(game)) { - const answer = await inquirer.prompt([{ - type: 'list', - name: 'game', - choices: await getGames(), - }]); - game = answer.game; + const game = await select({ + message: 'Game to build:', + choices: (await getGames()).map(value => ({ value })), + }); } const html = await buildHTML(game, true); diff --git a/build/wasmPlugin.ts b/build/wasmPlugin.ts index fc489e9..f209375 100644 --- a/build/wasmPlugin.ts +++ b/build/wasmPlugin.ts @@ -15,13 +15,34 @@ const wasmPlugin = ({ production, portable }: WasmLoaderConfig = {}): BunPlugin let wasmPath = path.resolve(import.meta.dir, '..', '..', 'dist', 'tmp.wasm'); let jsContent: string = ` async function instantiate(url) { - const { instance } = await WebAssembly.instantiateStreaming(fetch(url)); + const memory = new WebAssembly.Memory({ + initial: 16, + }); + let data = new DataView(memory.buffer); + const { instance } = await WebAssembly.instantiateStreaming(fetch(url), { + env: { + memory, + log(...args) { + console.log('[wasm]', ...args); + }, + grow(blocks) { + if (blocks > 0) { + memory.grow(blocks); + data = new DataView(memory.buffer); + } + } + } + }); + return { ...instance.exports, - data: new DataView(instance.exports.memory.buffer), + memory, + get data() { return data; }, }; } - const module = await instantiate(new URL("tmp.wasm", import.meta.url)); + const module = await instantiate(new URL($WASM$)); + if (typeof module.__srand === 'function') module.__srand(BigInt(Date.now())); + export default module; `; if (args.path.endsWith('.ts')) { @@ -49,7 +70,11 @@ const wasmPlugin = ({ production, portable }: WasmLoaderConfig = {}): BunPlugin } 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}`; + const buildAssets = path.resolve(import.meta.dir, 'assets'); + const stdlib = `${buildAssets}/stdlib.c`; + const opt = production ? '-O3': '-O0'; + const result = await $`clang --target=wasm32 ${opt} -flto -fno-builtin --no-standard-libraries -I ${buildAssets} -Wall -Wextra -Wpedantic -Werror -Wl,--lto-O3 -Wl,--no-entry -Wl,--import-memory -o ${wasmPath} ${args.path} ${stdlib}`; + if (result.exitCode !== 0) { throw new Error('Compile failed, check output'); } diff --git a/bun.lockb b/bun.lockb index a0c3a0c..1b68a43 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/compile_flags.txt b/compile_flags.txt new file mode 100644 index 0000000..71ef868 --- /dev/null +++ b/compile_flags.txt @@ -0,0 +1,9 @@ + --target=wasm32 +-fno-builtin +--no-standard-libraries +-I +build/assets/ +-Wall +-Wextra +-Wpedantic +-Werror \ No newline at end of file diff --git a/package.json b/package.json index d57b119..4843cd9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "bun build/build.ts" }, "dependencies": { + "@inquirer/select": "2.3.10", "classnames": "2.5.1", "preact": "10.22.0" }, @@ -18,7 +19,6 @@ "assemblyscript": "0.27.29", "bun-lightningcss": "0.2.0", "html-minifier": "4.0.0", - "inquirer": "9.3.4", "typescript": "5.5.2", "uglify-js": "3" } diff --git a/src/common/display/brick.tsx b/src/common/display/brick.tsx index f2298fa..c352a0c 100644 --- a/src/common/display/brick.tsx +++ b/src/common/display/brick.tsx @@ -1,5 +1,4 @@ import { render } from "preact"; -import type { Display } from "."; import { clamp, range } from "@common/utils"; import classNames from "classnames"; @@ -18,7 +17,7 @@ export interface BrickDisplayImage { height: number; } -export class BrickDisplay implements Display { +export class BrickDisplay { #field: boolean[] = new Array(FIELD_HEIGHT * FIELD_WIDTH); #miniField: boolean[] = new Array(MINI_FIELD_HEIGHT * MINI_FIELD_WIDTH); #score: number = 0; diff --git a/src/common/display/canvas.ts b/src/common/display/canvas.ts new file mode 100644 index 0000000..cdca469 --- /dev/null +++ b/src/common/display/canvas.ts @@ -0,0 +1,25 @@ +export function createCanvas(width: number, height: number) { + const canvas = document.createElement('canvas'); + + canvas.width = width; + canvas.height = height; + canvas.style.height = '100%'; + canvas.style.imageRendering = 'pixelated'; + + document.body.style.display = 'flex'; + document.body.style.justifyContent = 'center'; + document.body.append(canvas); + + return canvas; +} + +export function getImageData(image: HTMLImageElement): ImageData { + const canvas = document.createElement('canvas'); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const ctx = canvas.getContext('2d')!; + ctx.drawImage(image, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + return imageData; +} \ No newline at end of file diff --git a/src/common/display/index.ts b/src/common/display/index.ts deleted file mode 100644 index 714f5eb..0000000 --- a/src/common/display/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Display { - init(): void; - update(): void; -} - -export { BrickDisplay, type BrickDisplayImage } from './brick'; \ No newline at end of file diff --git a/src/games/brick-dungeon/index.ts b/src/games/brick-dungeon/index.ts index 51cdca3..18ee0b7 100644 --- a/src/games/brick-dungeon/index.ts +++ b/src/games/brick-dungeon/index.ts @@ -1,4 +1,4 @@ -import { BrickDisplay } from "@common/display"; +import { BrickDisplay } from "@common/display/brick"; import { isPressed, updateKeys } from "@common/input"; import spritesheetImage from './assets/spritesheet.png'; diff --git a/src/games/life/index.ts b/src/games/life/index.ts index 09fe4fa..b239491 100644 --- a/src/games/life/index.ts +++ b/src/games/life/index.ts @@ -1,9 +1,10 @@ +import { createCanvas } from "@common/display/canvas"; import life from "./life.c"; const width = life.getWidth(); const height = life.getHeight(); -const canvas = document.createElement('canvas'); +const canvas = createCanvas(width, height); const context = canvas.getContext('2d')!; const imageData = context.createImageData(width, height); @@ -12,15 +13,7 @@ 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); diff --git a/src/games/life/life.c b/src/games/life/life.c index cc2a0dc..4a09696 100644 --- a/src/games/life/life.c +++ b/src/games/life/life.c @@ -11,11 +11,11 @@ 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; } +int getWidth(void) { return width; } +int getHeight(void) { return height; } +uint8_t* getPixels(void) { return pixels; } -void step() +void step(void) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { @@ -76,7 +76,7 @@ static uint8_t rand_state[STATE_BYTES] = { 0x87, 0xdd, 0xdc, 0x10, 0x35, 0xbc, 0 static uint8_t rand8(void) { static uint16_t c = 0x42; - static int i = 0; + static uint32_t i = 0; uint16_t t; uint8_t x; diff --git a/src/games/wfc/assets/wfc.png b/src/games/wfc/assets/wfc.png new file mode 100644 index 0000000..d07b823 Binary files /dev/null and b/src/games/wfc/assets/wfc.png differ diff --git a/src/games/wfc/index.ts b/src/games/wfc/index.ts new file mode 100644 index 0000000..59886a9 --- /dev/null +++ b/src/games/wfc/index.ts @@ -0,0 +1,43 @@ +import wfc from './wfc.c'; +import wfcImage from './assets/wfc.png'; +import { createCanvas, getImageData } from "@common/display/canvas" + +const width = 256; +const height = 256; + +const canvas = createCanvas(width, height); +const context = canvas.getContext('2d')!; +const imageData = context.createImageData(width, height); +let pixels: Uint8ClampedArray; + +export default function main() { + console.log(wfc); + + const imageData = getImageData(wfcImage); + const dataPtr = wfc.malloc(imageData.data.length); + let buffer = new Uint8ClampedArray(wfc.memory.buffer, dataPtr, imageData.data.length); + buffer.set(imageData.data); + + wfc.init(dataPtr, imageData.width, imageData.height, canvas.width, canvas.height); + pixels = new Uint8ClampedArray(wfc.memory.buffer, wfc.getPixels(), width * height * 4); + requestAnimationFrame(loop); +} + +let sum = 0; +let count = 0; + +async function loop() { + const start = performance.now(); + wfc.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/wfc/wfc.c b/src/games/wfc/wfc.c new file mode 100644 index 0000000..b287176 --- /dev/null +++ b/src/games/wfc/wfc.c @@ -0,0 +1,212 @@ +#include + +#define PATTERN_SIZE 3 +#define PATTERN_BATCH 8 + +typedef struct { + uint32_t neighbours[9]; +} pattern_t; + +typedef struct { + /** pattern indices */ + uint32_t* possibilities; + uint32_t num_possibilities; +} superposition_t; + +typedef enum { NORTH, EAST, SOUTH, WEST, DIRECTION_MAX } direction_t; + +pattern_t* patterns; +uint32_t num_patterns; + +uint32_t* pixels; +superposition_t* superpositions; +uint32_t width; +uint32_t height; + +void patterns_init(uint32_t* pixel_data, uint16_t image_width, uint16_t image_height); +void superposition_init(void); + +EXPORT(init) void init(uint32_t* pixel_data, uint16_t image_width, uint16_t image_height, uint16_t canvas_width, uint16_t canvas_height) { + width = canvas_width; + height = canvas_height; + + patterns_init(pixel_data, image_width, image_height); + superposition_init(); +} + +pattern_t pattern_extract(uint32_t* pixel_data, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t batch) { + pattern_t pattern = {0}; + + for (uint8_t i = 0; i < PATTERN_SIZE * PATTERN_SIZE; i++) { + int8_t dx = (i % PATTERN_SIZE) - (PATTERN_SIZE / 2); + int8_t dy = (i / PATTERN_SIZE) - (PATTERN_SIZE / 2); + + int8_t tmp; + + if (batch == 1) { // horizontal flip + dx = PATTERN_SIZE - dx; + } else if (batch == 2) { // vertical flip + dy = PATTERN_SIZE - dy; + } else if (batch == 3) { // both flips + dx = PATTERN_SIZE - dx; + dy = PATTERN_SIZE - dy; + } else if (batch == 4) { // rotate 90 + tmp = dx; + dx = dy; + dy = PATTERN_SIZE - tmp; + } else if (batch == 5) { // rotate 270 + tmp = dy; + dy = dx; + dx = PATTERN_SIZE - tmp; + } else if (batch == 6) { // flip main axis + tmp = dy; + dy = dx; + dx = tmp; + } else if (batch == 7) { // flip secondary axis + tmp = dy; + dy = PATTERN_SIZE - dx; + dx = PATTERN_SIZE - tmp; + } + + uint16_t xx = (x + dx + width) % width; + uint16_t yy = (y + dy + height) % height; + + uint32_t idx = xx + yy * width; + pattern.neighbours[i] = pixel_data[idx]; + } + return pattern; +} + +bool pattern_check(pattern_t pattern) { + for (uint32_t i = 0; i < num_patterns; i++) { + if (memcmp(&pattern, &patterns[i], sizeof(pattern)) == 0) { + return false; // found duplicate + } + } + return true; +} + +void patterns_init(uint32_t* pixel_data, uint16_t image_width, uint16_t image_height) { + uint32_t max_patterns = image_width * image_height * PATTERN_BATCH; + patterns = malloc(max_patterns * sizeof(*patterns)); + + num_patterns = 0; + + for (uint16_t y = 0; y < image_height; y++) { + for (uint16_t x = 0; x < image_width; x++) { + for (uint8_t batch = 0; batch < PATTERN_BATCH; batch++) { + pattern_t pattern = pattern_extract(pixel_data, x, y, image_width, image_height, batch); + if (pattern_check(pattern)) { + patterns[num_patterns++] = pattern; + } + } + } + } + + print_int(num_patterns); + print_int(max_patterns); +} + +void superposition_init(void) { + pixels = malloc(width * height * sizeof(*pixels)); + superpositions = malloc(width * height * sizeof(*superpositions)); + + for (uint32_t i = 0; i < width * height; i++) { + superposition_t superposition = { + .num_possibilities = num_patterns, + .possibilities = malloc(num_patterns * sizeof(uint32_t)), + }; + + for (uint32_t p = 0; p < num_patterns; p++) { + superposition.possibilities[p] = p; + } + + superpositions[i] = superposition; + } +} + +bool pattern_match(pattern_t a, pattern_t b, direction_t direction) { + switch (direction) { + case NORTH: + for (uint8_t x = 0; x < PATTERN_SIZE; x++) { + if (a.neighbours[x] != b.neighbours[PATTERN_SIZE * (PATTERN_SIZE - 1) + x]) { + return false; + } + } + return true; + case EAST: + for (uint8_t y = 0; y < PATTERN_SIZE; y++) { + if (a.neighbours[y * PATTERN_SIZE + PATTERN_SIZE - 1] != b.neighbours[y * PATTERN_SIZE]) { + return false; + } + } + return true; + case SOUTH: + for (uint8_t x = 0; x < PATTERN_SIZE; x++) { + if (a.neighbours[PATTERN_SIZE * (PATTERN_SIZE - 1) + x] != b.neighbours[x]) { + return false; + } + } + return true; + case WEST: + for (uint8_t y = 0; y < PATTERN_SIZE; y++) { + if (a.neighbours[y * PATTERN_SIZE] != b.neighbours[y * PATTERN_SIZE + PATTERN_SIZE - 1]) { + return false; + } + } + return true; + default: + return false; + } +} + +void superposition_remove(superposition_t* superposition, uint32_t pattern) { + uint32_t i; + for (i = 0; i < superposition->num_possibilities; i++) { + if (superposition->possibilities[i] == pattern) + break; + } + + if (i < superposition->num_possibilities) { + for (; i < superposition->num_possibilities - 1; i++) { + superposition->possibilities[i] = superposition->possibilities[i + 1]; + } + superposition->num_possibilities--; + } +} + +void superpositions_draw(void) { + for (uint32_t i = 0; i < width * height; i++) { + uint64_t sum = 0; + superposition_t superposition = superpositions[i]; + for (uint32_t p = 0; p < superposition.num_possibilities; p++) { + sum += patterns[superposition.possibilities[p]].neighbours[(PATTERN_SIZE * PATTERN_SIZE) / 2]; + } + if (superposition.num_possibilities > 0) { + pixels[i] = sum / superposition.num_possibilities; + } else { + pixels[i] = 0; + } + } +} + +void superpositions_propagate(void) { + // TODO +} + +void superposition_collapse(superposition_t* superposition) { + if (superposition->num_possibilities > 1) { + uint32_t choice = rand() % superposition->num_possibilities; + superposition->possibilities[0] = superposition->possibilities[choice]; + superposition->num_possibilities = 1; + + superpositions_propagate(); + } +} + +EXPORT(step) void step(void) { + superposition_collapse(&superpositions[420]); + superpositions_draw(); +} + +EXPORT(getPixels) uint32_t* get_pixels(void) { return pixels; } diff --git a/src/types.d.ts b/src/types.d.ts index 8237dc1..cb926eb 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -48,6 +48,8 @@ declare module "*.c" { const instance: { memory: WebAssembly.Memory; data: DataView; + malloc: (size: number) => number; + free: (ptr: number) => void; [x: string]: (...args: any) => any; }; @@ -57,6 +59,8 @@ declare module "*.cpp" { const instance: { memory: WebAssembly.Memory; data: DataView; + malloc: (size: number) => void; + free: (ptr: number) => void; [x: string]: (...args: any) => any; };