wasmcart 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Luis Montes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,410 @@
1
+ # wasmcart
2
+
3
+ **A virtual cartridge format for safe, portable games.** A wasmcart cart is a
4
+ standalone WebAssembly module - a self-contained game that owns its own memory and
5
+ talks to the outside world only through a tiny, well-defined contract: the host
6
+ writes input + timing, calls `wc_render()` each frame, and reads back pixels and
7
+ audio. No filesystem, no syscalls, no ambient authority. Just pixels, sound, input,
8
+ and opt-in networking.
9
+
10
+ Because a cart is only WebAssembly + a fixed ABI, **the same cart runs anywhere a
11
+ conforming host exists** - Node.js, the browser, a libretro core in RetroArch, a
12
+ native player, a terminal - on any OS and any hardware with enough power. Write the
13
+ game once; it runs on all of them, sandboxed.
14
+
15
+ This repository is the **specification** and its **reference implementations**.
16
+
17
+ - 📄 **[SPEC.md](SPEC.md)** - the normative host↔cart contract (current ABI: v3)
18
+ - 🧩 **[`src/abi.js`](src/abi.js)** - the machine-readable contract (constants, layouts)
19
+ - 🖥️ **[`include/wc_cart.h`](include/wc_cart.h)** - the C side of the contract for cart authors
20
+ - 📚 **[`docs/`](docs/)** - per-subsystem guides (input, networking, GL, framebuffer, fetch, porting)
21
+
22
+ ## Reference implementations
23
+
24
+ Two reference hosts ship in this package - they define, by example, what a
25
+ conforming host does. Both are pure JavaScript (MIT).
26
+
27
+ | Import | Class | Runs on |
28
+ |--------|-------|---------|
29
+ | `wasmcart` | `CartHost` | Node.js (native GLES3 via a supplied WebGL2 context) |
30
+ | `wasmcart/web` | `CartHostWeb` | Browsers (WebGL2 from a `<canvas>`) |
31
+
32
+ ```js
33
+ import { CartHost } from 'wasmcart'; // Node
34
+ import { CartHostWeb } from 'wasmcart/web'; // browser
35
+ ```
36
+
37
+ Other hosts in the wasmcart org (own repos) run the *same* carts: a libretro core
38
+ (`wasmcart-libretro`), native players (`wasmcart-native-host`), and the terminal
39
+ emulator (`retroemu`). See **[The wasmcart org](#the-wasmcart-org)** below.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install wasmcart
45
+ ```
46
+
47
+ Requires Node.js >= 22.
48
+
49
+ ## Cart Formats
50
+
51
+ | Format | Description |
52
+ |--------|-------------|
53
+ | `.wasm` | Standalone WASM file, assets embedded as C arrays |
54
+ | `.wasc` | ZIP archive: `manifest.json` + `cart.wasm` + `assets/` (recommended for games with assets) |
55
+
56
+ ## The ABI
57
+
58
+ Every cart exports three functions:
59
+
60
+ - **`wc_get_info()`** - returns a pointer to a struct describing the cart's memory layout (framebuffer, audio ring, input pads, save data, timing)
61
+ - **`wc_init()`** - called once at startup
62
+ - **`wc_render()`** - called every frame (~60fps)
63
+
64
+ The cart declares all buffers as static globals. The host reads their locations from `wc_get_info()`, writes input/timing before each frame, and reads pixels/audio after `wc_render()` returns.
65
+
66
+ See [`examples/hello/wasmcart.h`](examples/hello/wasmcart.h) for the complete ABI header.
67
+
68
+ ### Rendering Mode
69
+
70
+ Every cart declares its rendering mode via `wc_info_t.gpu_api`:
71
+
72
+ | Value | Mode | Description |
73
+ |-------|------|-------------|
74
+ | 0 | **2D Framebuffer** | Cart writes ARGB8888 pixels to the framebuffer. Host reads and displays them. *(legacy - prefer gpu_api=1)* |
75
+ | 1 | **WebGL2 / GLES3** | Cart renders via GL function imports. The GPU output is the primary display. **Recommended for all carts.** |
76
+ | 2 | **WebGPU** | *(reserved for future use)* |
77
+ | 3 | **Vulkan** | *(reserved for future use)* |
78
+
79
+ **Rendering mode is declared once** in `wc_get_info()` and does not change during the cart's lifetime.
80
+
81
+ ### Recommended: All Carts Use GPU (gpu_api = 1)
82
+
83
+ **Every wasmcart host has OpenGL.** The recommended approach is for all carts to set `gpu_api = 1` and render all output through GL - even 2D pixel-buffer carts.
84
+
85
+ For carts that render pixels to a CPU buffer (software renderers, SDL2 2D games), use the `wc_gl_blit()` helper to upload the pixel buffer as a GL texture and draw a fullscreen quad:
86
+
87
+ ```c
88
+ #define WC_USE_GL
89
+ #include "wasmcart.h"
90
+ #include "wc_gl_blit.h" // single-header GL blit library
91
+
92
+ // In wc_get_info():
93
+ info.gpu_api = 1;
94
+
95
+ // In wc_render(), after drawing to your pixel buffer:
96
+ wc_gl_blit(my_pixels, width, height); // uploads as GL texture + draws quad
97
+ ```
98
+
99
+ This eliminates the host-side complexity of detecting 2D vs GL carts and managing two display paths. One rendering path for all carts, all hosts.
100
+
101
+ **Performance:** `glTexImage2D` is a DMA transfer - the GPU pulls pixel data without CPU waiting. At 1080p, this is significantly faster than the old CPU-side pixel copy + format conversion. 2D games that previously ran at 30fps at 1080p now run at 60fps with this approach.
102
+
103
+ **SDL2 carts** using the `sdl2_wc` backend can enable GL blit automatically:
104
+ ```c
105
+ info.gpu_api = 1; // in wc_get_info()
106
+ SDL_WASMCART_SetGLBlit(1); // in wc_init(), after SDL_Init
107
+ // Link with: sdl2_wc/sdl2_gl_blit.c
108
+ ```
109
+
110
+ SDL's software renderer draws pixels as usual. The `sdl2_wc` backend uploads them to GL on `SDL_RenderPresent`. No game code changes needed.
111
+
112
+ ### Legacy: 2D Framebuffer (gpu_api = 0)
113
+
114
+ Still supported for simplicity. The cart writes ARGB8888 pixels to a framebuffer, the host reads and displays them. No GL imports needed.
115
+
116
+ - Simplest possible cart - just write pixels to a buffer
117
+ - Host handles format conversion and display
118
+ - Performance limited by CPU pixel copy at high resolutions
119
+
120
+ ### GPU Carts (gpu_api = 1)
121
+
122
+ - Render via GL function imports (`"gl"` WASM module)
123
+ - The host displays GL output directly (swapBuffers)
124
+ - If the host needs pixels (terminal rendering, screenshots), the **host** performs readback (`glReadPixels`) at whatever frequency it chooses
125
+ - 2D and 3D content can coexist on the same GL context
126
+
127
+ **Compositing** (e.g., 2D HUD over 3D scene) is the cart's responsibility within its chosen GPU API. There is no hybrid mode - a cart that uses GL for 3D and wants a 2D overlay renders both through GL.
128
+
129
+ **Hosts should reject carts with unsupported gpu_api values** gracefully (e.g., "This host does not support WebGPU carts").
130
+
131
+ ### Resolution Negotiation
132
+
133
+ The host and cart negotiate resolution through a two-step process:
134
+
135
+ 1. **Host → Cart**: Before calling `wc_init()`, the host writes its preferred resolution to `wc_host_info_t.preferred_width` and `preferred_height`. This is a *suggestion* - the host's display capability, not a requirement. A value of 0 means "no preference."
136
+
137
+ 2. **Cart → Host**: During `wc_init()`, the cart reads the host's preference and decides its actual rendering resolution. It may use the preference directly, scale it, clamp it, or ignore it entirely. The cart writes its chosen resolution to `wc_info_t.width` and `wc_info_t.height`.
138
+
139
+ After `wc_init()` returns, the host reads the cart's actual width/height. These dimensions define:
140
+ - **2D carts**: the framebuffer size in pixels (ARGB8888, `width × height × 4` bytes)
141
+ - **GL carts**: the viewport/render target dimensions for GL calls
142
+
143
+ **Display scaling** is the host's responsibility:
144
+ - The host creates its display surface at whatever size it wants (its own preferred resolution, fullscreen, user-resizable window, etc.)
145
+ - The host scales the cart's output to fit the display, **preserving the cart's aspect ratio** with letterboxing/pillarboxing as needed
146
+ - The cart never knows or cares about the actual display size
147
+
148
+ **If no preferred resolution is specified** (both 0), the host should create its window at the cart's returned dimensions - a 1:1 pixel match with no scaling.
149
+
150
+ Example flow:
151
+ ```
152
+ Host sets preferred: 1920×1080
153
+ Cart reads preference, decides: 640×360 (16:9, manageable for this engine)
154
+ Host creates window: 1920×1080
155
+ Host scales 640×360 → 1920×1080 (exact 3x, no letterboxing needed)
156
+ ```
157
+
158
+ ```
159
+ Host sets preferred: 0×0 (no preference)
160
+ Cart uses its default: 320×240
161
+ Host creates window: 320×240 (1:1 match)
162
+ ```
163
+
164
+ ```
165
+ Host sets preferred: 1920×1080
166
+ Cart ignores it, uses fixed: 960×540
167
+ Host creates window: 1920×1080
168
+ Host scales 960×540 → 1920×1080 (exact 2x)
169
+ ```
170
+
171
+ This design means:
172
+ - The same `.wasc` cart works on any display size - phone, desktop, 4K TV, RetroArch
173
+ - The cart controls its rendering budget - a simple game can render at 320×240, a complex game at 1080p
174
+ - The host controls the display - letterboxing, fullscreen, window resize all work without cart cooperation
175
+
176
+ ### Manifest (`.wasc` carts)
177
+
178
+ The `manifest.json` inside a `.wasc` archive describes the cart:
179
+
180
+ ```json
181
+ {
182
+ "name": "My Game",
183
+ "version": "1.0.0",
184
+ "abi": 3,
185
+ "entry": "cart.wasm",
186
+ "players": 2,
187
+ "pointer": true,
188
+ "keyboard": true,
189
+ "net": {
190
+ "websocket": ["api.mygame.com"],
191
+ "data-channel": true
192
+ }
193
+ }
194
+ ```
195
+
196
+ All fields except `name`, `abi`, and `entry` are optional. `pointer`, `keyboard`, and `net` are ABI v3 features - gamepad input is always available regardless.
197
+
198
+ ### ABI v3: Networking & Extended Input
199
+
200
+ ABI v3 adds opt-in features beyond the core framebuffer/audio/gamepad loop:
201
+
202
+ - **Pointer input** (`"pointer": true`) - host writes `wc_pointer_t[10]` state (unified mouse + multitouch) and optionally calls `wc_ptr_on_down`, `wc_ptr_on_move`, `wc_ptr_on_up` exports
203
+ - **Keyboard input** (`"keyboard": true`) - host writes `uint8_t[32]` key state bitmask (USB HID scancodes) and optionally calls `wc_kb_on_down`, `wc_kb_on_up` exports
204
+ - **WebSocket** (`"net": {"websocket": [...]}`) - cart calls `wc_ws_open`/`send`/`close` imports, host delivers events via `wc_ws_on_open`/`on_message`/`on_close` exports
205
+ - **Data channels** (`"net": {"data-channel": true}`) - peer-to-peer via `wc_dc_send`/`broadcast` imports and `wc_dc_on_connect`/`on_message`/`on_disconnect` exports
206
+
207
+ All v3 exports are optional - the host silently skips events if the cart doesn't export the callbacks. Existing v2 carts work unchanged.
208
+
209
+ ## GPU ABI
210
+
211
+ There is **one GPU ABI: WebGL2 (OpenGL ES 3.0)**. All hosts present the same ES 3.0 GL surface. This is the ceiling - no host may expose ES 3.1+ or desktop GL features.
212
+
213
+ A cart that doesn't use the GPU at all can write pixels directly to a shared-memory framebuffer (ARGB8888). This is not a second GPU ABI - it's just pixels in a buffer, no GL involved.
214
+
215
+ ### Rules for GPU carts
216
+
217
+ 1. **ES 3.0 core only.** Do not use ES 3.1+ features (compute shaders, SSBO, image load/store). The browser host is WebGL2 which is ES 3.0. Native hosts cap `GL_VERSION` to ES 3.0.
218
+
219
+ 2. **Declare all GL functions as WASM imports at compile time.** There is no `eglGetProcAddress` or runtime function discovery in WASM. If a function isn't in the cart's import table, it cannot be called.
220
+
221
+ 3. **Extensions are informational, not guaranteed.** Hosts pass through real driver extensions via `GL_EXTENSIONS` (some carts like Godot need them for format detection). But extension *function pointers* are only available if the cart declares them as WASM imports. Calling an undeclared extension function traps.
222
+
223
+ 4. **GPU engines with getProcAddress callbacks** (Skia Ganesh, ANGLE, etc.) must override `glGetString(GL_EXTENSIONS)` in their callback to return empty - preventing the engine from probing for extension function pointers that don't exist as WASM imports. See the porting notes in the [wasmcart-sdl2](https://github.com/wasmcart/wasmcart-sdl2) repo for the full pattern.
224
+
225
+ 5. **Same `.wasc` runs everywhere.** If a cart works in the browser, it must work on Node.js, native, and RetroArch hosts. Staying within ES 3.0 core guarantees this.
226
+
227
+ ## Features
228
+
229
+ - **2D framebuffer** - ARGB8888 pixel buffer for software-rendered carts (no GL)
230
+ - **WebGL2 GPU** - one GL ABI everywhere. Cart imports WebGL2 functions, host provides them (native GLES3 on Node.js, WebGL2 in browser). Emscripten's GL output works directly.
231
+ - **Stereo audio** - Float32 or Int16 ring buffer, cart-declared sample rate
232
+ - **Gamepad input** - 4 pads with buttons, analog sticks, triggers (always available)
233
+ - **Pointer input** - unified mouse + touch via shared memory state + event callbacks (opt-in)
234
+ - **Keyboard input** - 256-bit key state bitmask (USB HID scancodes) + event callbacks (opt-in)
235
+ - **WebSocket networking** - event-driven WebSocket API with domain allowlist (opt-in)
236
+ - **Data channels** - peer-to-peer communication via host-managed connections (opt-in)
237
+ - **Save data** - persistent save blob (host manages storage)
238
+ - **Asset loading** - `.wasc` carts load files at runtime via `wc_asset_size()` / `wc_load_asset()`
239
+ - **WASI threads** - carts compiled with wasi-sdk `-pthread` can spawn background threads via pthreads
240
+
241
+ ## Node.js API
242
+
243
+ ```js
244
+ import { CartHost } from 'wasmcart';
245
+
246
+ const cart = new CartHost();
247
+ await cart.load('game.wasc');
248
+
249
+ // Main loop
250
+ const gamepads = []; // array of { buttons, axes, ... }
251
+ const frame = cart.runFrame(gamepads);
252
+
253
+ // frame.framebuffer - Uint8Array of ARGB pixels (for 2D carts)
254
+ // frame.audio - Int16Array of stereo PCM samples
255
+ // frame.saveData - Uint8Array (if cart uses save)
256
+
257
+ cart.destroy();
258
+ ```
259
+
260
+ ### Options
261
+
262
+ ```js
263
+ await cart.load('game.wasc', {
264
+ glBackend: gl, // required for GL carts (any WebGL2-compatible context)
265
+ preferredWidth: 800, // hint for resolution negotiation
266
+ preferredHeight: 600,
267
+ saveData: existingSaveBuffer, // restore previous save
268
+ });
269
+ ```
270
+
271
+ ### GL Carts
272
+
273
+ GL carts import functions from the `"gl"` WASM module. The host must provide a WebGL2-compatible context:
274
+
275
+ ```js
276
+ // Browser
277
+ const canvas = document.createElement('canvas');
278
+ const gl = canvas.getContext('webgl2');
279
+ await cart.load('gl_game.wasm', { glBackend: gl });
280
+
281
+ // Node.js - provide any WebGL2-compatible context
282
+ await cart.load('gl_game.wasm', { glBackend: glContext });
283
+ ```
284
+
285
+ ## CLI Tools
286
+
287
+ ### wasmcart-pack
288
+
289
+ Create `.wasc` archives from a `.wasm` file and an assets directory:
290
+
291
+ ```bash
292
+ npx wasmcart-pack --wasm cart.wasm --assets assets/ -o game.wasc
293
+ npx wasmcart-pack --wasm cart.wasm --assets assets/ -o game.wasc --name "My Game" --version "1.0"
294
+
295
+ # With ABI v3 features
296
+ npx wasmcart-pack --wasm cart.wasm -o game.wasc --pointer --keyboard
297
+ npx wasmcart-pack --wasm cart.wasm -o game.wasc --players 4 --ws api.mygame.com --data-channel
298
+ ```
299
+
300
+ ## Writing Carts
301
+
302
+ ### Minimal 2D cart (C + Emscripten)
303
+
304
+ ```c
305
+ #include "wasmcart.h"
306
+ #include <string.h>
307
+
308
+ #define WIDTH 320
309
+ #define HEIGHT 240
310
+
311
+ static uint32_t framebuffer[WIDTH * HEIGHT];
312
+ static wc_info_t info;
313
+
314
+ __attribute__((export_name("wc_get_info")))
315
+ wc_info_t* wc_get_info(void) {
316
+ info.version = 3;
317
+ info.width = WIDTH;
318
+ info.height = HEIGHT;
319
+ info.fb_ptr = (uint32_t)(uintptr_t)framebuffer;
320
+ return &info;
321
+ }
322
+
323
+ __attribute__((export_name("wc_init")))
324
+ void wc_init(void) {}
325
+
326
+ __attribute__((export_name("wc_render")))
327
+ void wc_render(void) {
328
+ // Fill screen red
329
+ for (int i = 0; i < WIDTH * HEIGHT; i++)
330
+ framebuffer[i] = 0xFFFF0000;
331
+ }
332
+ ```
333
+
334
+ ```bash
335
+ emcc -sSTANDALONE_WASM=1 -sALLOW_MEMORY_GROWTH=1 --no-entry -O2 -o cart.wasm cart.c
336
+ ```
337
+
338
+ ### Shared cart-author libraries
339
+
340
+ The [`include/`](include/) directory ships reusable C headers:
341
+
342
+ | Header | Purpose |
343
+ |--------|---------|
344
+ | `wc_cart.h` | **The C-side contract** - buffer declarations + `WC_FILL_INFO` macro |
345
+ | `wc_fb.h` | 2D drawing (fill_rect, blit, alpha blend) |
346
+ | `wc_gl.h` / `wc_gl_blit.h` | Shader compile/link, VAO/VBO helpers, CPU→GPU blit |
347
+ | `wc_math.h` | sin, cos, sqrt, atan2 (no libm) |
348
+ | `wc_mat4.h` / `wc_vec3.h` | 4x4 matrix + 3D vector ops |
349
+ | `wc_pcm_mixer.h` | Multi-channel PCM mixer + WAV parser |
350
+
351
+ For porting *existing* C/SDL games (the SDL2 backend + `stb_*` decoders), see the
352
+ **wasmcart-sdl2** repo.
353
+
354
+ ### Threading (wasi-sdk)
355
+
356
+ Carts can spawn background threads using standard pthreads. Requires wasi-sdk (not Emscripten):
357
+
358
+ ```bash
359
+ ${WASI_SDK}/bin/clang --target=wasm32-wasip1-threads -pthread \
360
+ -Wl,--import-memory,--shared-memory,--max-memory=67108864 \
361
+ -Wl,--no-entry -nostartfiles -O2 -o cart.wasm cart.c
362
+ ```
363
+
364
+ See [`examples/hello_threads/`](examples/hello_threads/) and the Threading section in the Porting Guide (in the [wasmcart-sdl2](https://github.com/wasmcart/wasmcart-sdl2) repo).
365
+
366
+ ## Examples
367
+
368
+ 34 example carts ranging from minimal (`hello`) to full game ports:
369
+
370
+ | Example | Type | Description |
371
+ |---------|------|-------------|
372
+ | `hello` | 2D | Minimal ABI demo |
373
+ | `hello_gl` | GL | Minimal GL triangle |
374
+ | `hello_threads` | 2D + threads | WASI threads demo |
375
+ | `snake`, `breakout`, `tetris` | 2D | Classic arcade games |
376
+ | `doom` | 2D | DOOM (doomgeneric) |
377
+ | `neverball`, `neverputt` | GL | GL1.x via gl4es |
378
+ | `chromium_bsu` | GL | GL1.x shoot-em-up |
379
+ | `etr` | GL | Extreme Tux Racer (SFML port) |
380
+ | `openarena2` | GL | Quake III Arena (ioquake3) |
381
+ | `flare`, `flare_es` | 2D | FLARE RPG (hand-port and SDL2 backend) |
382
+
383
+ ## Documentation
384
+
385
+ - **[SPEC.md](SPEC.md)** - the normative specification
386
+ - **[`docs/`](docs/)** - per-subsystem guides: [input](docs/input.md), [networking](docs/networking.md), [GL surface](docs/gl-surface.md), [framebuffer](docs/bind_framebuffer.md), [fetch](docs/fetch.md), [porting](docs/porting.md)
387
+ - **[`include/`](include/)** - C headers for cart authors (`wc_cart.h` is the contract; `wc_fb.h`/`wc_gl.h`/math/mixer are a lightweight SDK)
388
+
389
+ Porting existing C/SDL games (the SDL2 backend, `stb_*` helpers, and the full
390
+ porting guide) lives in the **wasmcart-sdl2** repo - see below.
391
+
392
+ ## The wasmcart org
393
+
394
+ wasmcart is a small ecosystem. This repo is the spec + JS reference hosts; the rest
395
+ are separate repos, all running the *same* carts:
396
+
397
+ | Repo | What it is |
398
+ |------|------------|
399
+ | **wasmcart** (this repo) | Spec, JS reference hosts (`CartHost`, `CartHostWeb`), `wasmcart-pack` |
400
+ | **wasmcart-sdl2** | SDL2 backend + `stb_*` helpers + porting guide - for porting existing C/SDL games |
401
+ | **wasmcart-native-host** | `libwasmcart` C host + `wasmcart-run` standalone SDL2 player (wasmtime / libnode) |
402
+ | **wasmcart-libretro** | libretro core - run carts in RetroArch / RetroDECK |
403
+ | **retroemu** | terminal + SDL host (libretro cores *and* wasmcart carts) |
404
+ | **wasmcart-website** | wasmcart.org - docs site |
405
+ | game port forks | each an upstream game fork on a `wasmcart` branch (`.wasc` shipped as Release artifacts) |
406
+
407
+ ## License
408
+
409
+ MIT - see [LICENSE](LICENSE). Compatible with all dependencies (fflate, yauzl,
410
+ yazl - all MIT).