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/SPEC.md ADDED
@@ -0,0 +1,477 @@
1
+ # wasmcart Specification
2
+
3
+ > **ABI version: 3.** This is the normative specification for the wasmcart virtual
4
+ > cartridge format - the host↔cart contract that any conforming host (see the
5
+ > reference implementations in [`src/`](src/)) and any cart must follow. The
6
+ > machine-readable form of these constants lives in [`src/abi.js`](src/abi.js); the
7
+ > C-side contract in [`include/wc_cart.h`](include/wc_cart.h).
8
+
9
+
10
+ ## Overview
11
+
12
+ A cart declares its capabilities in a manifest and exports three functions
13
+ (`wc_get_info`, `wc_init`, `wc_render`). Gamepad input is always available;
14
+ networking (WebSocket + data channels) and extended input (pointer + keyboard) are
15
+ opt-in per cart. The host provides everything through a small set of imports and
16
+ shared-memory regions; the cart owns its own memory and reaches nothing else.
17
+
18
+ ---
19
+
20
+ ## Manifest
21
+
22
+ ```json
23
+ {
24
+ "name": "Game Name",
25
+ "version": "1.0.0",
26
+ "abi": 3,
27
+ "entry": "cart.wasm",
28
+ "players": 4,
29
+ "pointer": true,
30
+ "keyboard": true,
31
+ "net": {
32
+ "websocket": ["api.mygame.com", "leaderboard.example.com"],
33
+ "data-channel": true
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### Fields
39
+
40
+ **`players`** (integer, optional, default: 1)
41
+ - How many gamepad inputs the game uses (1-4)
42
+
43
+ **`pointer`** (boolean, optional, default: false)
44
+ - If true, host writes pointer state (unified mouse/touch) and delivers pointer event callbacks
45
+ - If false, pointer state is not updated
46
+
47
+ **`keyboard`** (boolean, optional, default: false)
48
+ - If true, host writes raw key state and delivers key event callbacks
49
+ - If false, host does not deliver raw key input to the cart
50
+
51
+ **`net`** (object, optional)
52
+ - Omitted = no networking. Cart receives no network imports.
53
+ - If present, host provides the corresponding network imports to the cart
54
+ - Host MAY refuse to provide networking (e.g., offline device) - cart must handle gracefully
55
+
56
+ **`net.websocket`** (array of strings, optional)
57
+ - Domain allowlist for WebSocket connections
58
+ - Host enforces - connection attempts to unlisted domains fail
59
+ - No wildcards, no raw IPs, no localhost
60
+
61
+ **`net.data-channel`** (boolean, optional)
62
+ - If true, cart gets binary data channel imports
63
+ - Host manages peer connections and signaling (opaque to cart)
64
+
65
+ ---
66
+
67
+ ## Exports (cart provides)
68
+
69
+ ```c
70
+ wc_info_t* wc_get_info(void); // returns cart info struct
71
+ void wc_init(void); // called once at startup
72
+ void wc_render(void); // called every frame
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Imports (host provides)
78
+
79
+ ```c
80
+ // Logging
81
+ void wc_log(const char* ptr, uint32_t len);
82
+
83
+ // Assets (v2+)
84
+ int32_t wc_asset_size(const char* path, uint32_t path_len);
85
+ int32_t wc_load_asset(const char* path, uint32_t path_len, void* dest, uint32_t max_size);
86
+
87
+ // GL (~100 functions, optional, imported from "gl" module)
88
+ void glClear(uint32_t mask);
89
+ // ... etc
90
+ ```
91
+
92
+ ---
93
+
94
+ ## wc_info_t
95
+
96
+ ```c
97
+ typedef struct {
98
+ uint32_t version; // 3
99
+ uint32_t width;
100
+ uint32_t height;
101
+ uint32_t fb_ptr;
102
+ uint32_t audio_ptr;
103
+ uint32_t audio_cap;
104
+ uint32_t audio_write_ptr;
105
+ uint32_t input_ptr; // → wc_pad_t[4]
106
+ uint32_t save_ptr;
107
+ uint32_t save_size;
108
+ uint32_t time_ptr;
109
+ uint32_t host_info_ptr;
110
+ uint32_t flags;
111
+ uint32_t audio_sample_rate;
112
+ // v3 additions
113
+ uint32_t pointer_ptr; // → wc_pointer_t[10] (80 bytes), 0 = not used
114
+ uint32_t keys_ptr; // → uint8_t[32] key state bitmask, 0 = not used
115
+ } wc_info_t;
116
+ ```
117
+
118
+ ### Flags
119
+
120
+ ```c
121
+ #define WC_FLAG_AUDIO_F32 0x01 // audio ring buffer uses float32
122
+ #define WC_FLAG_NET_WS 0x02 // cart wants WebSocket imports
123
+ #define WC_FLAG_NET_DC 0x04 // cart wants data channel imports
124
+ #define WC_FLAG_POINTER 0x08 // cart wants pointer input
125
+ #define WC_FLAG_KEYBOARD 0x10 // cart wants raw keyboard input
126
+ ```
127
+
128
+ ---
129
+
130
+ ## WebSocket
131
+
132
+ ### Cart imports (calls into host)
133
+
134
+ ```c
135
+ // Open a WebSocket connection.
136
+ // url must be in the manifest's websocket allowlist.
137
+ // Returns a connection ID (>= 0) or -1 on failure.
138
+ int32_t wc_ws_open(const char* url, uint32_t url_len);
139
+
140
+ // Close a WebSocket connection.
141
+ void wc_ws_close(int32_t conn_id, uint32_t code);
142
+
143
+ // Send binary data. Returns bytes sent, or -1 on error.
144
+ int32_t wc_ws_send(int32_t conn_id, const void* data, uint32_t len);
145
+
146
+ // Send text data. Returns bytes sent, or -1 on error.
147
+ int32_t wc_ws_send_text(int32_t conn_id, const char* str, uint32_t len);
148
+
149
+ // Get readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
150
+ int32_t wc_ws_state(int32_t conn_id);
151
+ ```
152
+
153
+ ### Cart exports (host calls into cart - all optional)
154
+
155
+ ```c
156
+ void wc_ws_on_open(int32_t conn_id);
157
+ void wc_ws_on_message(int32_t conn_id, const void* data, uint32_t len);
158
+ void wc_ws_on_message_text(int32_t conn_id, const char* str, uint32_t len);
159
+ void wc_ws_on_close(int32_t conn_id, uint32_t code);
160
+ void wc_ws_on_error(int32_t conn_id);
161
+ ```
162
+
163
+ ### Notes
164
+ - Event-driven - mirrors the real WebSocket API
165
+ - Host buffers events and delivers them before each `wc_render()` call
166
+ - Both binary and text frame support
167
+ - Cart exports are optional - if missing, host silently drops events
168
+ - Host validates URL against manifest allowlist before connecting
169
+ - Connection IDs are small integers managed by host (0, 1, 2, ...)
170
+ - `data`/`str` pointers in callbacks are temporary - cart must copy what it needs
171
+
172
+ ---
173
+
174
+ ## Data Channel
175
+
176
+ For peer-to-peer gameplay. The host manages signaling and peer connections - the cart just sees binary data channels. The underlying transport (WebRTC, relayed, LAN UDP, etc.) is opaque to the cart.
177
+
178
+ ### Cart imports (calls into host)
179
+
180
+ ```c
181
+ // Get number of currently connected peers.
182
+ int32_t wc_dc_peer_count(void);
183
+
184
+ // Get info about a peer by index (0 to peer_count-1).
185
+ // Writes a null-terminated username/label into dest.
186
+ // Returns the peer's connection ID, or -1 if index out of range.
187
+ int32_t wc_dc_peer_info(uint32_t index, char* dest, uint32_t max_len);
188
+
189
+ // Send binary data to a specific peer. Returns bytes sent, or -1 on error.
190
+ int32_t wc_dc_send(int32_t peer_id, const void* data, uint32_t len);
191
+
192
+ // Send binary data to all connected peers. Returns peer count, or -1 on error.
193
+ int32_t wc_dc_broadcast(const void* data, uint32_t len);
194
+ ```
195
+
196
+ ### Cart exports (host calls into cart - all optional)
197
+
198
+ ```c
199
+ // peer_id is stable for this session.
200
+ void wc_dc_on_connect(int32_t peer_id, const char* label, uint32_t label_len);
201
+ void wc_dc_on_disconnect(int32_t peer_id);
202
+ void wc_dc_on_message(int32_t peer_id, const void* data, uint32_t len);
203
+ ```
204
+
205
+ ### Notes
206
+ - Cart does NOT manage connections - host handles all signaling
207
+ - peer_id works like a socket descriptor - games can use it to track players
208
+ - Cart exports are optional - if missing, cart can still poll via `wc_dc_peer_count()`/`wc_dc_peer_info()`
209
+ - Binary only - games serialize their own protocols
210
+ - Delivery semantics are host-defined (may be unreliable or reliable depending on transport)
211
+ - Host delivers events before `wc_render()`
212
+
213
+ ---
214
+
215
+ ## Pointer Input
216
+
217
+ Unified mouse + touch. Opt-in via `"pointer": true` in manifest. Both shared-memory state and event callbacks.
218
+
219
+ ### Shared memory (host writes every frame)
220
+
221
+ ```c
222
+ typedef struct {
223
+ int16_t x; // cart-space coordinates (0 to width-1)
224
+ int16_t y; // cart-space coordinates (0 to height-1)
225
+ uint8_t buttons; // bitmask: bit0=primary, bit1=secondary, bit2=middle
226
+ uint8_t active; // 1 if this pointer exists
227
+ uint8_t _pad[2];
228
+ } wc_pointer_t; // 8 bytes
229
+
230
+ // 10 pointer slots, 80 bytes total
231
+ // Cart sets wc_info_t.pointer_ptr to a wc_pointer_t[10] buffer
232
+ ```
233
+
234
+ ### Cart exports (host calls into cart - all optional)
235
+
236
+ ```c
237
+ void wc_ptr_on_down(uint32_t id, int16_t x, int16_t y, uint8_t button);
238
+ void wc_ptr_on_move(uint32_t id, int16_t x, int16_t y);
239
+ void wc_ptr_on_up(uint32_t id, uint8_t button);
240
+ ```
241
+
242
+ ### Notes
243
+ - Host normalizes screen coordinates to cart resolution
244
+ - Mouse = pointer 0 (always active when cursor is over window)
245
+ - Touch = each finger gets the next available slot, active only while touching, buttons=0x01
246
+ - If device has both mouse and touch, they coexist - mouse is 0, fingers fill 1+
247
+ - `button` param: 0=primary (left click / touch), 1=secondary (right click), 2=middle
248
+ - State array is always up to date regardless of whether cart exports callbacks
249
+ - Host delivers events before `wc_render()`
250
+
251
+ ---
252
+
253
+ ## Keyboard Input
254
+
255
+ Opt-in via `"keyboard": true` in manifest. Both shared-memory state and event callbacks.
256
+
257
+ ### Shared memory (host writes every frame)
258
+
259
+ ```c
260
+ // 256-bit bitmask - one bit per keycode
261
+ // Cart sets wc_info_t.keys_ptr to a uint8_t[32] buffer
262
+ uint8_t wc_keys[32]; // 32 bytes
263
+
264
+ // Test if key is down:
265
+ // wc_keys[keycode >> 3] & (1 << (keycode & 7))
266
+ ```
267
+
268
+ ### Cart exports (host calls into cart - all optional)
269
+
270
+ ```c
271
+ void wc_kb_on_down(uint8_t keycode, uint8_t modifiers);
272
+ void wc_kb_on_up(uint8_t keycode, uint8_t modifiers);
273
+ ```
274
+
275
+ ### Modifier bitmask
276
+
277
+ ```c
278
+ #define WC_MOD_SHIFT 0x01
279
+ #define WC_MOD_CTRL 0x02
280
+ #define WC_MOD_ALT 0x04
281
+ #define WC_MOD_META 0x08
282
+ ```
283
+
284
+ ### Keycodes (USB HID scancodes)
285
+
286
+ ```c
287
+ // Letters (0x04–0x1D)
288
+ #define WC_KEY_A 0x04
289
+ #define WC_KEY_B 0x05
290
+ #define WC_KEY_C 0x06
291
+ #define WC_KEY_D 0x07
292
+ #define WC_KEY_E 0x08
293
+ #define WC_KEY_F 0x09
294
+ #define WC_KEY_G 0x0A
295
+ #define WC_KEY_H 0x0B
296
+ #define WC_KEY_I 0x0C
297
+ #define WC_KEY_J 0x0D
298
+ #define WC_KEY_K 0x0E
299
+ #define WC_KEY_L 0x0F
300
+ #define WC_KEY_M 0x10
301
+ #define WC_KEY_N 0x11
302
+ #define WC_KEY_O 0x12
303
+ #define WC_KEY_P 0x13
304
+ #define WC_KEY_Q 0x14
305
+ #define WC_KEY_R 0x15
306
+ #define WC_KEY_S 0x16
307
+ #define WC_KEY_T 0x17
308
+ #define WC_KEY_U 0x18
309
+ #define WC_KEY_V 0x19
310
+ #define WC_KEY_W 0x1A
311
+ #define WC_KEY_X 0x1B
312
+ #define WC_KEY_Y 0x1C
313
+ #define WC_KEY_Z 0x1D
314
+
315
+ // Numbers (0x1E–0x27)
316
+ #define WC_KEY_1 0x1E
317
+ #define WC_KEY_2 0x1F
318
+ #define WC_KEY_3 0x20
319
+ #define WC_KEY_4 0x21
320
+ #define WC_KEY_5 0x22
321
+ #define WC_KEY_6 0x23
322
+ #define WC_KEY_7 0x24
323
+ #define WC_KEY_8 0x25
324
+ #define WC_KEY_9 0x26
325
+ #define WC_KEY_0 0x27
326
+
327
+ // Common keys
328
+ #define WC_KEY_ENTER 0x28
329
+ #define WC_KEY_ESCAPE 0x29
330
+ #define WC_KEY_BACKSPACE 0x2A
331
+ #define WC_KEY_TAB 0x2B
332
+ #define WC_KEY_SPACE 0x2C
333
+
334
+ // Punctuation
335
+ #define WC_KEY_MINUS 0x2D
336
+ #define WC_KEY_EQUAL 0x2E
337
+ #define WC_KEY_LBRACKET 0x2F
338
+ #define WC_KEY_RBRACKET 0x30
339
+ #define WC_KEY_BACKSLASH 0x31
340
+ #define WC_KEY_SEMICOLON 0x33
341
+ #define WC_KEY_QUOTE 0x34
342
+ #define WC_KEY_GRAVE 0x35
343
+ #define WC_KEY_COMMA 0x36
344
+ #define WC_KEY_PERIOD 0x37
345
+ #define WC_KEY_SLASH 0x38
346
+
347
+ // Function keys (0x3A–0x45)
348
+ #define WC_KEY_F1 0x3A
349
+ #define WC_KEY_F2 0x3B
350
+ #define WC_KEY_F3 0x3C
351
+ #define WC_KEY_F4 0x3D
352
+ #define WC_KEY_F5 0x3E
353
+ #define WC_KEY_F6 0x3F
354
+ #define WC_KEY_F7 0x40
355
+ #define WC_KEY_F8 0x41
356
+ #define WC_KEY_F9 0x42
357
+ #define WC_KEY_F10 0x43
358
+ #define WC_KEY_F11 0x44
359
+ #define WC_KEY_F12 0x45
360
+
361
+ // Navigation
362
+ #define WC_KEY_INSERT 0x49
363
+ #define WC_KEY_HOME 0x4A
364
+ #define WC_KEY_PAGEUP 0x4B
365
+ #define WC_KEY_DELETE 0x4C
366
+ #define WC_KEY_END 0x4D
367
+ #define WC_KEY_PAGEDOWN 0x4E
368
+
369
+ // Arrows
370
+ #define WC_KEY_RIGHT 0x4F
371
+ #define WC_KEY_LEFT 0x50
372
+ #define WC_KEY_DOWN 0x51
373
+ #define WC_KEY_UP 0x52
374
+
375
+ // Numpad
376
+ #define WC_KEY_NUMLOCK 0x53
377
+ #define WC_KEY_KP_DIVIDE 0x54
378
+ #define WC_KEY_KP_MULTIPLY 0x55
379
+ #define WC_KEY_KP_MINUS 0x56
380
+ #define WC_KEY_KP_PLUS 0x57
381
+ #define WC_KEY_KP_ENTER 0x58
382
+ #define WC_KEY_KP_1 0x59
383
+ #define WC_KEY_KP_2 0x5A
384
+ #define WC_KEY_KP_3 0x5B
385
+ #define WC_KEY_KP_4 0x5C
386
+ #define WC_KEY_KP_5 0x5D
387
+ #define WC_KEY_KP_6 0x5E
388
+ #define WC_KEY_KP_7 0x5F
389
+ #define WC_KEY_KP_8 0x60
390
+ #define WC_KEY_KP_9 0x61
391
+ #define WC_KEY_KP_0 0x62
392
+ #define WC_KEY_KP_PERIOD 0x63
393
+
394
+ // Modifiers (0xE0–0xE7)
395
+ #define WC_KEY_LCTRL 0xE0
396
+ #define WC_KEY_LSHIFT 0xE1
397
+ #define WC_KEY_LALT 0xE2
398
+ #define WC_KEY_LMETA 0xE3
399
+ #define WC_KEY_RCTRL 0xE4
400
+ #define WC_KEY_RSHIFT 0xE5
401
+ #define WC_KEY_RALT 0xE6
402
+ #define WC_KEY_RMETA 0xE7
403
+ ```
404
+
405
+ All keycodes follow USB HID Usage Tables. SDL provides these natively. Browser `KeyboardEvent.code` requires a static lookup table to convert (well-documented 1:1 mapping).
406
+
407
+ ### Notes
408
+ - Host delivers events before `wc_render()`
409
+ - State bitmask is always up to date regardless of whether cart exports callbacks
410
+ - When `"keyboard": true` is in manifest, host does not map keyboard keys to gamepad
411
+
412
+ ---
413
+
414
+ ## Security Model
415
+
416
+ A cart is a plain WebAssembly module with **no ambient authority**. It has no
417
+ syscalls, no filesystem, no network, no clock beyond what the host hands it, and no
418
+ way to reach anything outside its own module memory except through the imports the
419
+ host provides. Everything a cart can touch is mediated by the host and validated
420
+ before it acts. This is what makes running an untrusted cart safe.
421
+
422
+ ### Filesystem and assets
423
+
424
+ Carts have **no filesystem access.** There is no `open`, `read`, `write`, `stat`,
425
+ directory listing, or any path-based I/O available to a cart - those imports simply
426
+ do not exist.
427
+
428
+ The only files a cart can read are **its own bundled assets**, and only through the
429
+ asset API:
430
+
431
+ - `wc_asset_size(path, len)` and `wc_load_asset(path, len, dest, maxSize)` resolve
432
+ paths **against the cart's own asset bundle only** (the `assets/` entries inside
433
+ its `.wasc`, or the dev-mode directory). A cart cannot name a file outside that
434
+ scope.
435
+ - The host **validates every requested path** and rejects: absolute paths (`/...`,
436
+ `\...`), Windows drive letters (`C:`), parent-directory traversal (`..`), null
437
+ bytes, and backslashes. A rejected or unknown path returns `-1`; there is no way
438
+ for it to resolve to a host file.
439
+ - A cart therefore cannot read the user's files, other carts' assets, or anything
440
+ on the host - its entire readable world is the assets it shipped with.
441
+
442
+ ### Saving is host-managed
443
+
444
+ Carts do **not** write files to save progress. Persistence is entirely the host's
445
+ responsibility, through a fixed shared-memory region:
446
+
447
+ - The cart declares a save region via `wc_info_t.save_ptr` / `save_size` (a plain
448
+ byte blob in the cart's own memory).
449
+ - **The host owns storage.** Before `wc_init()`, the host loads any existing save
450
+ bytes into that region so the cart can read them at startup. After a frame (or on
451
+ demand), the host reads the region back and persists it however it sees fit
452
+ (a file, browser storage, a libretro SRAM/save-state, a database - the cart never
453
+ knows or cares).
454
+ - The cart never chooses *where* or *whether* data is stored; it only reads and
455
+ writes its own in-memory save blob. This keeps saving safe (no filesystem write
456
+ authority) and portable (the same cart saves correctly on every host).
457
+
458
+ ### Networking
459
+
460
+ 1. **No networking by default** - omit `net` from manifest = zero network access
461
+ 2. **Domain allowlist** - WebSocket connections only to declared domains
462
+ 3. **No raw sockets** - no TCP, no UDP, no localhost, no IP addresses
463
+ 4. **Host enforces** - cart can't bypass; imports validate before acting
464
+ 5. **Graceful degradation** - offline hosts provide stub imports returning -1
465
+ 6. **Data channels are host-managed** - cart can't initiate peer connections
466
+ 7. **No DNS resolution** - cart can't enumerate network
467
+
468
+ ### Summary
469
+
470
+ | Capability | Cart access |
471
+ |------------|-------------|
472
+ | Host filesystem | none |
473
+ | Own bundled assets | read-only, path-validated, via `wc_load_asset` |
474
+ | Save data | in-memory blob only; host owns persistence |
475
+ | Network | none unless declared in the manifest (allowlisted WebSocket / data channels) |
476
+ | Syscalls / clock / RNG | none except what the host explicitly imports |
477
+