diff --git a/makefile b/makefile index 3a902f7c..936a8a2c 100644 --- a/makefile +++ b/makefile @@ -20,12 +20,12 @@ mac: .dummy WINDOWS_RAYLIB_OBJS = $(RAYLIB_DIR)/rcore.o $(RAYLIB_DIR)/rglfw.o $(RAYLIB_DIR)/rshapes.o $(RAYLIB_DIR)/rtextures.o $(RAYLIB_DIR)/rtext.o $(RAYLIB_DIR)/rmodels.o $(RAYLIB_DIR)/raudio.o $(RAYLIB_DIR)/utils.o windows: .dummy - $(PRE) make -j$(J) -C vendor/raylib/src CC="$(RAYLIB_CC)" LDFLAGS="$(OPT)" CFLAGS="-w $(OPT) $(CLFAGS) -DPLATFORM_DESKTOP" PLATFORM=PLATFORM_DESKTOP OS=WINDOWS_NT - $(PRE) make -Bj$(J) -f tool/core.mak $(TARGET) OS=WINDOWS CC="$(CC)" EXE=.exe TEST_LUA="$(TEST_LUA)" CFLAGS="-DVM_USE_RAYLIB -DVM_NO_GC -DWIN32_LEAN_AND_MEAN $(CFLAGS)" LDFLAGS="$(RAYLIB_DIR)/libraylib.a -lopengl32 -lgdi32 -lwinmm" + $(PRE) make -Bj$(J) -C vendor/raylib/src CC="$(RAYLIB_CC)" LDFLAGS="$(OPT)" CFLAGS="-w -DPLATFORM_DESKTOP -DDWIN32_LEAN_AND_MEAN $(OPT) $(CLFAGS)" PLATFORM=PLATFORM_DESKTOP OS=WINDOWS_NT + $(PRE) make -Bj$(J) -f tool/core.mak $(TARGET) OS=WINDOWS CC="$(CC)" EXE=.exe TEST_LUA="$(TEST_LUA)" CFLAGS="-DVM_USE_RAYLIB -DVM_NO_GC -DWIN32_LEAN_AND_MEAN $(OPT) $(CFLAGS)" LDFLAGS="$(RAYLIB_DIR)/libraylib.a -lopengl32 -lgdi32 $(OPT) $(LDFLAGS)" freebsd: .dummy $(PRE) gmake -Bj$(J) -C vendor/raylib/src CC="$(RAYLIB_CC)" LDFLAGS="$(OPT)" CFLAGS="-w $(OPT) $(CLFAGS) -DPLATFORM_DESKTOP" PLATFORM=PLATFORM_DESKTOP - $(PRE) gmake -Bj$(J) -f tool/core.mak $(TARGET) OS=FREEBSD CC="$(CC)" EXE= TEST_LUA="$(TEST_LUA)" CFLAGS="-DVM_USE_RAYLIB $(CFLAGS)" LDFLAGS="-L/usr/local/lib vendor/raylib/src/libraylib.a -lOpenGL -lm -lpthread $(LDFLAGS)" + $(PRE) gmake -Bj$(J) -f tool/core.mak $(TARGET) OS=FREEBSD CC="$(CC)" EXE= TEST_LUA="$(TEST_LUA)" CFLAGS="-DVM_USE_RAYLIB $(OPT) $(CFLAGS)" LDFLAGS="-L/usr/local/lib vendor/raylib/src/libraylib.a -lOpenGL -lm -lpthread $(OPT) $(LDFLAGS)" WEB_RAYLIB_OBJS = $(RAYLIB_DIR)/rcore.o $(RAYLIB_DIR)/rshapes.o $(RAYLIB_DIR)/rtextures.o $(RAYLIB_DIR)/rtext.o $(RAYLIB_DIR)/rmodels.o $(RAYLIB_DIR)/raudio.o $(RAYLIB_DIR)/utils.o diff --git a/vendor/isocline b/vendor/isocline index 096e145d..8f6c5dc9 160000 --- a/vendor/isocline +++ b/vendor/isocline @@ -1 +1 @@ -Subproject commit 096e145d2eab09b80375c1c508a8fee2e5c5e5ef +Subproject commit 8f6c5dc979414c06b14851c7d433cac84055c8cd diff --git a/web-raw/index.html b/web-raw/index.html index a69e1b3c..32c4a044 100644 --- a/web-raw/index.html +++ b/web-raw/index.html @@ -13,130 +13,5 @@ - + \ No newline at end of file diff --git a/web-raw/index.js b/web-raw/index.js new file mode 100644 index 00000000..382ea6c3 --- /dev/null +++ b/web-raw/index.js @@ -0,0 +1,139 @@ + +Promise.all([ + import('./minivm.js'), + import('./inspect.js'), + fetch('./minivm.wasm'), +]).then(async ([{default: Module}, inspect, wasm]) => { + const saveInterval = 1; + + const wasmBinary = await wasm.arrayBuffer(); + const canvas = document.createElement('canvas'); + document.body.appendChild(canvas); + + const lastGame = localStorage.getItem('minivm.game'); + const app = new URLSearchParams(document.location.search).get('app') ?? lastGame ?? 'snake'; + localStorage.setItem('minivm.game', app); + const path = `minivm.save:${app}`; + + let lastKeys = new Set(); + let keys = new Set(); + let buttons = new Set(); + let lastButtons = new Set(); + let ptrx = 0; + let ptry = 0; + + document.addEventListener('keydown', ({ key }) => { + if (key.length !== 1) { + return; + } + keys.add(key.toUpperCase().charCodeAt(0)); + }); + + document.addEventListener('keyup', ({ key }) => { + if (key.length !== 1) { + return; + } + keys.delete(key.toUpperCase().charCodeAt(0)); + }); + + document.addEventListener('mousedown', ({ button }) => { + buttons.add(button); + }); + + document.addEventListener('mouseup', ({ button }) => { + buttons.delete(button); + }); + + canvas.addEventListener('mousemove', ({ offsetX, offsetY }) => { + ptrx = offsetX; + ptry = offsetY; + }); + + let ctx2d = canvas.getContext('2d'); + + ctx2d.fillStyle = '#FF00FF'; + ctx2d.fillRect(0, 0, 100, 100); + + const body = document.createElement('div'); + document.body.append(body); + body.style.userSelect = 'none'; + + const open = new Set(JSON.parse(localStorage.getItem('minivm.open') ?? '[]')); + + Module({ + arguments: [`test/app/${app}.lua`], + wasmBinary: wasmBinary, + preRun(mod) { + if (lastGame !== app) { + return; + } + const s = localStorage.getItem(path); + if (s == null) { + return; + } + const a = Uint8Array.from(JSON.parse(s)); + mod.FS.writeFile('/in.bin', a); + }, + _vm_canvas_draw_rect(x, y, w, h, r, g, b, a) { + ctx2d.fillStyle = `rgba(${r}, ${g}, ${b}, ${a / 255})`; + ctx2d.fillRect(x, y, Math.round(w + 1), Math.round(h + 1)); + }, + _vm_canvas_mouse_x() { + return ptrx | 0; + }, + _vm_canvas_mouse_y() { + return ptry | 0; + }, + _vm_canvas_mouse_pressed(button) { + return buttons.has(button); + }, + _vm_canvas_mouse_pressed_last(button) { + return lastButtons.has(button); + }, + _vm_canvas_key_pressed(key) { + return keys.has(key); + }, + _vm_canvas_key_pressed_last(key) { + return lastKeys.has(key); + }, + _vm_canvas_set_size(w, h) { + if (w !== canvas.width || h !== canvas.height) { + canvas.width = w; + canvas.height = h; + ctx2d = canvas.getContext('2d'); + ctx2d.fillStyle = '#FF00FF'; + ctx2d.fillRect(0, 0, w, h); + } + }, + _vm_std_app_frame_loop(vm) { + let n = 0; + const frame = () => { + requestAnimationFrame(frame); + const nKeys = new Set(keys); + const nButtons = new Set(buttons); + keys = new Set(keys); + buttons = new Set(buttons); + this._vm_std_app_frame(vm); + lastButtons = nButtons; + lastKeys = nKeys; + if (n++ % saveInterval === 0) { + this._vm_std_app_sync(vm); + const s = this.FS.readFile('/out.bin'); + if (s != null) { + localStorage.setItem(path, JSON.stringify(Array.from(s))); + } + body.onclick = () => { + const div = inspect.render(new Uint8Array(s), open); + body.innerHTML = ''; + body.appendChild(div); + localStorage.setItem('minivm.open', JSON.stringify(Array.from(open))); + }; + if (body.children.length === 0) { + body.onclick(); + } + } + }; + requestAnimationFrame(frame); + } + }); +}); \ No newline at end of file diff --git a/web-raw/inspect.js b/web-raw/inspect.js new file mode 100644 index 00000000..b4031d39 --- /dev/null +++ b/web-raw/inspect.js @@ -0,0 +1,326 @@ + +const VM_TAG_UNK = 0; +const VM_TAG_NIL = 1; +const VM_TAG_BOOL = 2; +const VM_TAG_I8 = 3; +const VM_TAG_I16 = 4; +const VM_TAG_I32 = 5; +const VM_TAG_I64 = 6; +const VM_TAG_F32 = 7; +const VM_TAG_F64 = 8; +const VM_TAG_STR = 9; +const VM_TAG_CLOSURE = 10; +const VM_TAG_FUN = 11; +const VM_TAG_TAB = 12; +const VM_TAG_FFI = 13; + +export class FFI { + constructor(n) { + this.n = n; + } +} + +export class Fun { + constructor(n) { + this.n = n; + } +} + +export class Closure extends Array {} + +export class InspectError extends Error {} + +export const inspect = (bytes) => { + const values = []; + let head = 0; + + const sleb = () => { + let shift = 0n; + let result = 0n; + while (true) { + let byte = BigInt(bytes[head++]); + result |= (byte & 0x7Fn) << shift; + shift += 7n; + if ((0x80n & byte) === 0n) { + if (shift < 64n && (byte & 0x40n) !== 0n) { + return result | -(1n << shift); + } + return result; + } + } + }; + + const ulebn = () => { + let x = 0n; + let shift = 0n; + while (true) { + let buf = BigInt(bytes[head++]); + x |= (buf & 0x7Fn) << shift; + if (buf < 0x80n) { + return x; + } + shift += 7n; + } + }; + + const uleb = () => { + return Number(ulebn()); + }; + + let n = 0; + stop: while (true) { + const tag = bytes[head++]; + let value; + switch (tag) { + default: { + throw new InspectError(); + } + case VM_TAG_UNK: { + break stop; + } + case VM_TAG_NIL: { + value = null; + break; + } + case VM_TAG_BOOL: { + value = bytes[head++] != 0; + break; + } + case VM_TAG_I8: + case VM_TAG_I16: + case VM_TAG_I32: + case VM_TAG_I64: { + value = sleb(); + break; + } + case VM_TAG_FUN: { + value = new Fun(sleb()); + break; + } + case VM_TAG_F32: { + const u32s = new Uint32Array(1); + u32s[0] = ulebn(); + const f32s = new Float32Array(u32s.buffer); + value = f32s[0]; + break; + } + case VM_TAG_F64: { + const u64s = new BigUint64Array(1); + u64s[0] = ulebn(); + const f64s = new Float64Array(u64s.buffer); + value = f64s[0]; + break; + } + case VM_TAG_STR: { + let len = uleb(); + let buf = '' + for (let i = 0; i < len; i++) { + buf += String.fromCharCode(bytes[head++]); + } + value = buf; + break; + } + case VM_TAG_FFI: { + value = new FFI(uleb()); + break; + } + case VM_TAG_CLOSURE: { + let len = uleb(); + let table = new Closure(); + for (let i = 0; i < len; i++) { + table.push(uleb()); + } + value = table; + break; + } + case VM_TAG_TAB: { + let len = uleb(); + let table = new Map(); + for (let i = 0; i < len; i++) { + table.set(uleb(), uleb()); + } + value = table; + break; + } + } + values.push(value); + n += 1; + } + + for (const value of Object.values(values)) { + if (value instanceof Closure) { + for (let i = 0; i < value.length; i++) { + value[i] = values[value[i]]; + } + } else if (value instanceof Map) { + const ents = [...value.entries()]; + for (const [k] of ents) { + value.delete(k); + } + for (const [k, v] of ents) { + if (values[k] != null && values[v] != null) { + value.set(values[k], values[v]); + } + } + } + }; + + return values; +}; + +export const render = (tree, open=new Set()) => { + if (tree instanceof Uint8Array) { + tree = inspect(tree)[0]; + } + + const body = document.createElement('div'); + + const depth = (elem) => { + let depth = 0; + for (let cur = elem; cur != body && cur != null; cur = cur.parentElement) { + depth += 1; + } + return depth; + }; + + const more = (path, name, tree, elem) => { + const pad = depth(elem) ? 2 : 0; + + if (tree instanceof Map) { + const details = document.createElement('details'); + const inner = document.createElement('div'); + const summary = document.createElement('summary'); + + details.appendChild(summary); + details.appendChild(inner); + elem.appendChild(details); + + summary.innerText = name; + inner.style.display = 'flex'; + inner.style.flexDirection = 'column'; + + const openDetails = () => { + details.open = true; + + const nums = []; + const strs = []; + for (const k of tree.keys()) { + if (typeof k === 'number') { + nums.push(Number(k)); + } + if (typeof k === 'string') { + strs.push(k); + } + } + nums.sort((x, y) => x - y); + for (const k of nums) { + more(`${path}[${k}]`, String(k), tree.get(k), inner); + } + strs.sort(); + for (const k of strs) { + more(`${path}.${k}`, k, tree.get(k), inner); + } + let n = 0; + for (const [k, v] of tree.entries()) { + if (typeof k !== 'number' && typeof k !== 'string') { + more(`${path}::k${n}`, 'key', k, inner); + more(`${path}::v${n}`, 'value', v, inner); + n += 1; + } + } + }; + + details.onclick = (e) => { + if (e.target !== summary) { + return; + } + + if (details.open) { + inner.innerHTML = ''; + open.delete(path); + return; + } + + open.add(path); + + openDetails(); + }; + + if (open.has(path)) { + openDetails(); + } + + details.style.paddingLeft = `${pad}em`; + } else if (tree instanceof Closure) { + const details = document.createElement('details'); + const inner = document.createElement('div'); + const summary = document.createElement('summary'); + + details.appendChild(summary); + details.appendChild(inner); + elem.appendChild(details); + + summary.innerText = name; + inner.style.display = 'flex'; + inner.style.flexDirection = 'column'; + + const openDetails = () => { + details.open = true; + + open.add(path); + + let n = 0; + for (const v of tree) { + more(`${path}[${n}]`, `[${n}]`, v, inner); + n++; + } + }; + + details.onclick = (e) => { + if (e.target !== summary) { + return; + } + + if (details.open) { + inner.innerHTML = ''; + open.delete(path); + return; + } + + openDetails(); + }; + + if (open.has(path)) { + openDetails(); + } + + details.style.paddingLeft = `${pad}em`; + } else if (typeof tree === 'string' || typeof tree === 'bigint' || typeof tree === 'number') { + const text = document.createElement('span'); + elem.appendChild(text); + + text.innerText = `${name} = ${tree}`; + text.style.paddingLeft = `${pad}em`; + } else if (tree instanceof Fun) { + const text = document.createElement('span'); + elem.appendChild(text); + + text.innerText = `${name} = `; + text.style.paddingLeft = `${pad}em`; + } else if (tree instanceof FFI) { + const text = document.createElement('span'); + elem.appendChild(text); + + text.innerText = `${name} = `; + text.style.paddingLeft = `${pad}em`; + } else { + console.log(tree); + } + }; + + body.innerHTML = ''; + body.style.fontFamily = 'monospace'; + more('_ENV', '_ENV', tree, body); + + return body; +}