plum
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.
|