plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


bf83574 — Peter John 2026-11-02T16:07:39+05:30
docs(plans): render experiments spec (batcher, command buffer, Plum scene graph)
docs/plans/2026-11-02-render-experiments.md ADDED
@@ -0,0 +1,67 @@
1
+ # Render experiments: own batcher, command buffer, Plum scene graph
2
+
3
+ **Branch:** `superlite/render-experiments` (based on `superlite/pixi-spike`, which has `--renderer`, `plum-game/web-test --measure/--headed/--uncapped` and `fixtures/stress.plum`).
4
+ **Question:** what is the fastest way to get Plum draw data to the GPU, and what does a scene graph written in Plum cost? User preference: a libGDX-like scene graph; an own host/renderer if it is faster.
5
+
6
+ ## Variants (all measured on the same stress scene)
7
+
8
+ | id | renderer | draw data path |
9
+ |---|---|---|
10
+ | A | macroquad (web wasm32, and native) | one extern call per draw (current) |
11
+ | B | Pixi (spike) | one extern call per draw |
12
+ | C | `gpu`: own JS batcher, WebGPU with WebGL2 fallback | one extern call per draw |
13
+ | D | `gpu` (web) and macroquad (native) | command buffer in wasm linear memory, read once per frame |
14
+ | E | C and D | stress scene built with the Plum scene graph `std/Scene` |
15
+
16
+ ## Linear memory primitive (compiler)
17
+
18
+ `plum-std/Mem.plum`: `primitive Mem` with compiler intrinsics. Addresses are byte offsets (`Int`, wrapped to i32). A module that uses `Mem` gets one exported wasm memory named `memory` (initial 16 pages, growable); modules that don't use it are unchanged.
19
+
20
+ - `Mem.size() -> Int` (bytes), `Mem.grow(pages: Int) -> Int` (old page count, -1 on failure)
21
+ - `Mem.storeF32(addr: Int, v: Float)`, `Mem.storeI32(addr: Int, v: Int)`, `Mem.storeU8(addr: Int, v: Int)`
22
+ - `Mem.loadF32(addr: Int) -> Float`, `Mem.loadI32(addr: Int) -> Int`, `Mem.loadU8(addr: Int) -> Int`
23
+
24
+ ## Command buffer (`plum-std/GameCmd.plum`)
25
+
26
+ Same API as `std/Game`, same names and signatures, so a game switches by changing one import. Input, assets, sound and screen queries stay `extern`. The draw functions (`clear`, `drawRect`, `drawLine`, `drawCircle`, `drawText`, `drawTexture`, `drawTextureEx`, `setCamera`, `resetCamera`) instead append commands to a buffer in linear memory, growing it as needed. GameCmd also exports:
27
+
28
+ - `export fun cmdReset()`: the host calls it before `draw`;
29
+ - `export fun cmdPtr() -> Int` and `export fun cmdLen() -> Int`: byte offset and length of this frame's commands, which the host reads after `draw` from the `memory` export.
30
+
31
+ Encoding: little-endian. Every field is 4 bytes: f32 for Float, i32 for Int and Bool. Each command is a u32 opcode followed by its fields:
32
+
33
+ | op | command | fields |
34
+ |---|---|---|
35
+ | 1 | clear | r g b a |
36
+ | 2 | rect | x y w h r g b a |
37
+ | 3 | line | x1 y1 x2 y2 thickness r g b a |
38
+ | 4 | circle | x y radius r g b a |
39
+ | 5 | text | x y size r g b a byteLen(i32), then UTF-8 bytes zero-padded to a multiple of 4 |
40
+ | 6 | texture | tex(i32) x y r g b a |
41
+ | 7 | textureEx | tex(i32) sx sy sw sh x y w h rotation flipX(i32) r g b a |
42
+ | 8 | setCamera | x y scale |
43
+ | 9 | resetCamera | (no fields) |
44
+
45
+ Draw order is command order. Semantics are exactly those of `std/Game` (see the README Games section).
46
+
47
+ ## Scene graph (`plum-std/Scene.plum`)
48
+
49
+ libGDX Scene2D-like, written in Plum, drawing through whichever Game module the program imports. Since a std module can't be generic over the import, it targets `std/Game`; the fixtures provide a GameCmd copy, `Scene` over `GameCmd`, as a fixture file for variant E-D.
50
+
51
+ - A node has: x, y, width, height, originX, originY, scaleX, scaleY, rotation (radians), color, visible, children, and an optional `act: fn(Node, Float)`.
52
+ - Kinds: group, sprite (texture plus source rect), rect, circle, text.
53
+ - `Stage`: the root with a camera, plus `act(dt)` and `draw()`.
54
+ - World transforms compose parent→child (translate, rotate about origin, scale). Drawing uses `drawTextureEx` with the composed position, rotation and size; skew from non-uniform scale under rotation isn't supported, as with libGDX actors without group transforms.
55
+ - Hit testing: `stage.hit(x, y) -> Node?`.
56
+ - Pure-logic `test` blocks.
57
+
58
+ ## Ownership
59
+
60
+ | agent | owns |
61
+ |---|---|
62
+ | compiler | plum-checker, plum-wasm-codegen, plum-core (if needed), `plum-std/Mem.plum`, `plum-std/GameCmd.plum`, their tests (in its own worktree) |
63
+ | gpu-web | `plum-game/web/plum_bridge_gpu.js`, `plum-game/web/index_gpu.html`, `plum-cli/src/game.rs` (renderer flag), plum-cli game tests, the plum-game/web-test harness (src/, tests/) |
64
+ | native-cmd | `plum-game/src/*` (macroquad native reading the command buffer; per-frame timing for native via `PLUM_GAME_MEASURE=secs`), plum-game/tests |
65
+ | scene | `plum-std/Scene.plum`, `plum-game/web-test/fixtures/*` (stress variants), `plum-examples/scene.plum` (a small scene-graph demo) |
66
+
67
+ The results go in `plum-game/web-test/BENCHMARK.md`, written by the coordinator's benchmark run.