aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-09-22 20:48:09 +0200
committergbc dev <gbc@localhost>2026-09-22 20:48:09 +0200
commita2aa8fbb20cab296d44bcdd23c63c0f76ec52869 (patch)
tree700f7a394f8bceb27199590343b158e872e1404d
parentweb: selecting a Game Boy puts the caret in its console (diff)
downloadsl0pboy-a2aa8fbb20cab296d44bcdd23c63c0f76ec52869.tar.gz
sl0pboy-a2aa8fbb20cab296d44bcdd23c63c0f76ec52869.tar.xz
sl0pboy-a2aa8fbb20cab296d44bcdd23c63c0f76ec52869.zip
wasm: cap the stack at 256KB - 16.25MB -> 0.5MB per Game Boy
zig defaults the wasm stack to 16MB, and that lands in the module's *initial* memory, so every instance allocated 16.25MB before executing an instruction. With several Game Boys on one page that was the dominant cost: 123MB for six. The machine is a static and nothing here recurses deeply, so 256KB is plenty. Per instance: 20.5MB -> 4.8MB of linear memory (six: 123MB -> 29MB). Note the spelling - zig cc rejects -Wl,--stack-size and -zstack-size, and wants -Wl,-z,stack-size=N. With memory no longer the constraint, the page cap goes 6 -> 8 on measurement rather than guesswork. Emulation-only, one thread: 12.5x realtime at 1, 3.1x at 4, 1.5x at 8, and exactly 1.0x at 12 - i.e. ~12 is where a thread runs out of road with nothing left for rendering. 8 holds a verified 60fps each in a real browser with all eight leased. Golden frames are still pixel-exact on cgb-acid2, dmg-acid2, cpu_instrs@600 and pky@400 with its .sav, and two Game Boys still lease and ping 4/4.
-rw-r--r--Makefile7
-rw-r--r--README.md24
-rw-r--r--web/gbcore.js2
-rw-r--r--web/index.html2
4 files changed, 31 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 8b34825..e9d44be 100644
--- a/Makefile
+++ b/Makefile
@@ -16,9 +16,14 @@ BIN := $(BUILD)/sl0pboy
WASM_SRCS := $(addprefix $(SRCDIR)/,cpu.c ppu.c timer.c gb.c cart.c wasm.c)
WASM_OUT := web/sl0pboy.wasm
WASM_CC := zig cc
+# --stack-size: zig defaults the wasm stack to 16MB, which lands in the
+# module's *initial* memory - so every Game Boy on the page cost 16.25MB
+# before running an instruction. The machine is a static and the C code has no
+# deep recursion, so 256KB is plenty: 260 pages -> 8 pages per instance.
WASM_FLAGS := -target wasm32-wasi -mexec-model=reactor -O2 -std=c11 \
-Wall -Wextra -Wno-unused-parameter \
- -Wl,--no-entry -Wl,--export-dynamic -Wl,--strip-all
+ -Wl,--no-entry -Wl,--export-dynamic -Wl,--strip-all \
+ -Wl,-z,stack-size=262144
.PHONY: all clean run wasm
diff --git a/README.md b/README.md
index a9c2f8f..6dcc4ad 100644
--- a/README.md
+++ b/README.md
@@ -472,8 +472,28 @@ other and `10.0.0.1`, and nothing leaves the page.
Multiple machines are multiple *instantiations* of the same compiled module
(`WebAssembly.compile` once, `instantiate` N times). The core keeps its machine
in a static, so each Game Boy gets its own linear memory and total isolation
-for free — no handle-based API in C. One thread runs ~12 of them at full speed
-(measured: 12.3x realtime per instance), so the page caps at 6 with headroom.
+for free — no handle-based API in C.
+
+The page caps at 8, which is a judgement call on top of two real limits.
+Measured on one thread, emulation only:
+
+| Game Boys | per machine | headroom | wasm memory |
+| --- | --- | --- | --- |
+| 1 | 748 fps | 12.5x | 5MB |
+| 4 | 185 fps | 3.1x | 19MB |
+| 8 | 92 fps | 1.5x | 38MB |
+| 12 | 62 fps | **1.0x** | 57MB |
+| 16 | 46 fps | 0.8x | 76MB |
+
+So ~12 is where a single thread runs out of road with nothing left for
+rendering; 8 holds a measured 60fps each in a real browser. Above that you'd
+want a Worker per machine. The other ceiling is the DHCP pool (`10.0.0.2`–
+`.39`, inherited from gbhub) at 38.
+
+Each instance costs ~4.8MB of linear memory, which is mostly the 1MB ROM plus
+allocator slack — it *was* 20.5MB until the wasm stack was capped at 256KB
+(`-Wl,-z,stack-size`): zig defaults it to 16MB, and that lands in the module's
+initial memory, so every Game Boy paid 16.25MB before executing anything.
Bytes that arrive **outside** a SLIP frame are console I/O, not packets — the
same split gbhub makes. That is why each Game Boy on the page has a text box:
diff --git a/web/gbcore.js b/web/gbcore.js
index 637b939..8cecb82 100644
--- a/web/gbcore.js
+++ b/web/gbcore.js
@@ -110,6 +110,8 @@ export class GameBoy {
this.#ex.gbw_serial_push(bytes.length);
}
get serialDropped() { return this.#ex.gbw_serial_dropped(); }
+ // This machine's share of wasm linear memory - what N Game Boys really cost.
+ get memBytes() { return this.#ex.memory.buffer.byteLength; }
// ---- battery RAM ----
sram() {
diff --git a/web/index.html b/web/index.html
index d182b57..1d28c89 100644
--- a/web/index.html
+++ b/web/index.html
@@ -96,7 +96,7 @@ import { GameBoy, BTN } from "./gbcore.js";
import { Hub, ipStr, GW } from "./gbhub.js";
const $ = (id) => document.getElementById(id);
-const MAX_GB = 6; // one thread runs ~12 at full speed; leave headroom
+const MAX_GB = 8; // CPU-bound: ~12 fit in one thread with zero headroom
const ROM_URL = "roms/gbos.gb";
const logEl = $("log");