@raskell/wasm-ad 0.0.0-stage → 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 +21 -0
- package/README.md +122 -2
- package/asconfig.json +12 -0
- package/assembly/index.ts +769 -0
- package/bin/wasmad.mjs +461 -0
- package/cart.asconfig.json +12 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 raskell.io
|
|
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
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WASM-AD for AssemblyScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An AssemblyScript SDK for [WASM-AD](https://github.com/raskell-io/wasm-ad) carts: register
|
|
4
|
+
addresses, typed builders for backgrounds (tiled and affine), sprites, affine matrices and
|
|
5
|
+
sound, input, save RAM and `trace`, with no runtime allocation and no mutable globals.
|
|
6
|
+
|
|
7
|
+
## The starter cart
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install
|
|
11
|
+
npm run build # build/starter.wasm
|
|
12
|
+
node ../../tools/wasmad.mjs check build/starter.wasm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[`examples/starter.ts`](examples/starter.ts) is the same cart as every other SDK's starter: a
|
|
16
|
+
rotating affine checkerboard, a spinning star and a little tune on every sound channel, with
|
|
17
|
+
bit-identical frames and sound. Drop `build/starter.wasm` into the player.
|
|
18
|
+
|
|
19
|
+
## A new cart
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @raskell/wasm-ad assemblyscript@0.28
|
|
23
|
+
echo '{ "extends": "./node_modules/@raskell/wasm-ad/cart.asconfig.json" }' > asconfig.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { write16, reg, dispcnt, keys, key } from "@raskell/wasm-ad/assembly";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx asc cart.ts --config asconfig.json -o cart.wasm
|
|
32
|
+
npx wasmad meta cart.wasm title="My Cart" author=you
|
|
33
|
+
npx wasmad check cart.wasm
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`cart.asconfig.json` is the configuration below with `abort` pointing into the
|
|
37
|
+
installed package; extend it rather than copying it. `wasmad` is the cart
|
|
38
|
+
checker from `tools/wasmad.mjs`, shipped with this package.
|
|
39
|
+
|
|
40
|
+
`asconfig.json` sets what the spec requires:
|
|
41
|
+
|
|
42
|
+
| Option | Why |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `memoryBase: 163840` | Static data starts at 0x28000, above the hardware region (SPEC.md §4). |
|
|
45
|
+
| `initialMemory: 4` | At least 3 pages; 4 leaves room above the data. |
|
|
46
|
+
| `runtime: "stub"` | No garbage collector; carts do not allocate. |
|
|
47
|
+
| `use: abort=assembly/index/abort` | Traps instead of importing `env.abort`; a cart may only import `env.trace`. |
|
|
48
|
+
|
|
49
|
+
AssemblyScript cannot write custom sections, so `wasmad meta` adds the `wasm-ad` metadata
|
|
50
|
+
(`spec` is filled in). Run `wasmad check` before sharing a cart.
|
|
51
|
+
|
|
52
|
+
## Rules
|
|
53
|
+
|
|
54
|
+
A cart exports `update()`, and optionally `start()` and `scanline(y: i32)`. Do the setup in
|
|
55
|
+
`start`, not at the top level of a module, which would run before the host clears the hardware.
|
|
56
|
+
|
|
57
|
+
**Keep state in linear memory.** AssemblyScript compiles a module-level `let` or `var` to a
|
|
58
|
+
mutable WebAssembly global, which SPEC.md §12 forbids (hosts snapshot linear memory for save
|
|
59
|
+
states and netplay), and `wasmad check` rejects it. Put the cart's state in an
|
|
60
|
+
`@unmanaged` class placed in a static block of memory:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
@unmanaged
|
|
64
|
+
class State {
|
|
65
|
+
x: i32;
|
|
66
|
+
y: i32;
|
|
67
|
+
}
|
|
68
|
+
const state = changetype<State>(memory.data(offsetof<State>()));
|
|
69
|
+
|
|
70
|
+
export function start(): void {
|
|
71
|
+
state.x = 112; // memory starts zeroed; set initial values here
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`const` values are fine. Avoid `new`, string concatenation, arrays and anything else that
|
|
76
|
+
allocates; use `memory.data` for static buffers and tables, e.g.
|
|
77
|
+
`const NOTES = memory.data<u32>([262, 330, 392])` read with `load<u32>(NOTES + (i << 2))`.
|
|
78
|
+
|
|
79
|
+
## Sound
|
|
80
|
+
|
|
81
|
+
`sound.enable()` turns sound on with every channel on both sides. A channel plays when its
|
|
82
|
+
start bit is written; the host clears it after the frame (SPEC.md §14):
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
write16(reg.SOUND2CNT_L, sound.DUTY_50 | sound.envelope(12, 2, false));
|
|
86
|
+
write16(reg.SOUND2CNT_H, sound.START | sound.rate(440));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Sample channels play signed 8-bit samples you write to `sound.sampleRam()`:
|
|
90
|
+
`pcm.play(0, offset, length, 16000, 64)`.
|
|
91
|
+
|
|
92
|
+
## Affine backgrounds
|
|
93
|
+
|
|
94
|
+
In modes 1 and 2, BG2 (and BG3 in mode 2) is drawn through a matrix. Its tiles are 8bpp
|
|
95
|
+
(`bgTile8`) and its map is one byte per tile (`affineMap`). `setBgAffine(2, m, tx, ty, sx, sy)`
|
|
96
|
+
puts texture pixel (tx, ty) at screen pixel (sx, sy) and rotates and scales around it.
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
All values are plain integers; builders take a value and return a new one.
|
|
101
|
+
|
|
102
|
+
| | |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `read16`, `write16`, `read32`, `write32` | Raw register access, e.g. `write16(reg.DISPCNT, dispcnt.MODE0 \| dispcnt.BG0)` |
|
|
105
|
+
| `reg.*`, `reg.BGCNT(i)`, `reg.BGHOFS(i)`, `reg.BGVOFS(i)` | Register addresses by their SPEC.md names |
|
|
106
|
+
| `dispcnt`, `layer`, `blend`, `key` | Bit constants; `blend.cnt(effect, first, second)`, `blend.alpha(eva, evb)` |
|
|
107
|
+
| `bgcnt.priority/charBlock/screenBlock/size/bpp8/mosaic/affineSize/wrap` | `BGxCNT` builders |
|
|
108
|
+
| `rgb(r, g, b)` | 8-bit channels to RGB555 |
|
|
109
|
+
| `setBgColor(i, c)`, `setObjColor(i, c)` | Palette RAM |
|
|
110
|
+
| `bgTile(block, i)`, `objTile(i)`, `fillTile(addr, byte)` | Addresses of 32-byte 4bpp tiles |
|
|
111
|
+
| `screenBlock(b)`, `setScreenEntry(b, i, e)`, `screenEntry.make/hflip/vflip` | Background maps |
|
|
112
|
+
| `bgTile8(block, i)`, `affineMap(b)`, `setBgAffine(bg, m, tx, ty, sx, sy)` | Affine backgrounds (modes 1 and 2) |
|
|
113
|
+
| `sprite.make(x, y, SpriteSize.S16x16, tile)` then `.bank/.priority/.hflip/.vflip/.semiTransparent/.window/.mosaic/.bpp8/.affine` | Sprites, packed in a `u64` |
|
|
114
|
+
| `oam.set(i, s)`, `oam.hide(i)`, `oam.hideAll()`, `oam.setAffine(slot, m)` | OAM |
|
|
115
|
+
| `affine.rotateScaleFixed(angle, sx, sy)`, `affine.IDENTITY`, `affine.make` | Affine matrices; angle in 1/256 turns, scale in 8.8 |
|
|
116
|
+
| `fixed.sin(a)`, `fixed.cos(a)` | Q14 trigonometry, identical in every SDK |
|
|
117
|
+
| `buttons.update()`, `buttons.held(k)`, `buttons.pressed(k)`, `keys()` | Input |
|
|
118
|
+
| `frame()`, `seed()` | Time and the only entropy source |
|
|
119
|
+
| `saveRam()`, `saveRestored()`, `requestSave()` | 32 KB of save RAM at 0x19000 |
|
|
120
|
+
| `sound.enable/disable`, `sound.rate(hz)`, `sound.sweep/envelope/length/noise`, `sound.START/LENGTH/DUTY_*/WAVE_*` | Sound channels 1–4, through `reg.SOUND*` |
|
|
121
|
+
| `sound.waveRam()`, `sound.sampleRam()`, `sound.status()` | Wave RAM, sample RAM, which channels are playing |
|
|
122
|
+
| `pcm.play(ch, start, len, rate, vol)`, `pcm.playLooped`, `pcm.stop`, `pcm.position`, `pcm.playing` | Sample channels A (0) and B (1) |
|
|
123
|
+
| `trace(msg)` | UTF-8 to the host's debug log (at most 1 KB) |
|