plum

#treesitter#compiler#wasm

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

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


e3bef5b — Peter John 2026-11-02T17:11:59+05:30
docs(plans): renderer features review plan
docs/plans/2026-11-02-renderer-features.review.md ADDED
@@ -0,0 +1,76 @@
1
+ # Renderer features: multi-texture batching, HiDPI, device loss, SDF text, render targets, blend modes
2
+
3
+ ## Summary
4
+ Our browser renderer (`plum-game/web/plum_bridge.js`, WebGPU only, one extern call per draw) closes the gaps with Pixi that matter for real games:
5
+ - **Multi-texture batching.** A draw call holds up to 16 textures, so y-sorted scenes that mix atlases stay at a handful of draws.
6
+ - **High-DPI canvas sizing and GPU device-loss recovery.**
7
+ - **Crisp scalable text** from a signed-distance-field (SDF) glyph atlas.
8
+ - **Render-to-texture and blend modes,** for minimaps, lighting and selection highlights.
9
+
10
+ The Plum API grows by four small additions. Existing games run unchanged.
11
+
12
+ ## What changes for Plum code
13
+ ```plum
14
+ canvas := textureCreate(256, 256) # a texture you can draw into (render target)
15
+ renderTarget(canvas) # following draws go into it ...
16
+ clear(Color.Transparent)
17
+ drawRect(0.0, 0.0, 10.0, 10.0, Color.Red)
18
+ renderTarget(-1) # ... back to the screen
19
+ drawTexture(canvas, 20.0, 20.0, Color.White)
20
+
21
+ blendMode(Blend.Add) # Blend.Normal | Blend.Add | Blend.Multiply | Blend.Screen
22
+ drawTexture(glow, x, y, Color.White)
23
+ blendMode(Blend.Normal)
24
+ ```
25
+ - `textureCreate(w: Int, h: Int) -> Int`: a blank, transparent texture that you can draw into.
26
+ - `renderTarget(t: Int)`: sends the following draws into texture `t`, or back to the screen with `-1`. The camera is per target, and drawing into a target resets it to that target's pixels.
27
+ - `blendMode(m: Int)`: sets the blend mode for the following draws. The modes are constants in `primitive Blend`: `Normal 0`, `Add 1`, `Multiply 2`, `Screen 3`.
28
+ - `drawText` keeps its signature, but text is now rendered from an SDF atlas, so it stays crisp at any size or zoom.
29
+
30
+ ## How it works
31
+ - **Batching:**
32
+ - the batcher keeps 16 texture slots per draw; each vertex carries its slot index and the shader picks that texture;
33
+ - a draw call ends only when all 16 slots are in use and a 17th texture is needed, or when the blend mode, render target or camera changes;
34
+ - a texture keeps its slot if it is reused within the same draw.
35
+ - **HiDPI:**
36
+ - the canvas backing store is `css size × devicePixelRatio`;
37
+ - every game coordinate stays in CSS pixels: `screenWidth`/`screenHeight` return CSS pixels and `mousePos` is in CSS pixels;
38
+ - the renderer scales by the device pixel ratio when it projects;
39
+ - resizing and zooming are handled.
40
+ - **Device loss:**
41
+ - on `device.lost` the renderer requests a new device;
42
+ - it re-creates the pipelines and re-uploads every texture from its kept source (image bitmaps, text atlas, render-target contents are lost and cleared);
43
+ - then rendering continues;
44
+ - it logs `GPU device lost (<reason>); restored`, and if no device can be obtained it shows an on-page message.
45
+ - **SDF text:**
46
+ - one glyph atlas is built at startup from the page font with a small SDF generator (8-bit distance, ~48px base) covering ASCII + Latin-1; other characters are added on demand;
47
+ - text is drawn as glyph quads through the same batch, so it costs no separate texture upload per string;
48
+ - a smoothstep in the shader keeps edges crisp from 8px to large sizes.
49
+ - **Render targets and blending:**
50
+ - each `textureCreate` texture is also a render attachment;
51
+ - a draw into a target flushes the current batch first;
52
+ - the blend modes map to standard WebGPU blend states.
53
+
54
+ ## Work, in order
55
+ 1. Multi-texture batching (`plum_bridge.js` shader + batcher). This one comes first and is benchmarked: the stress scene with 4 interleaved textures must not regress, and a new fixture with 8 textures interleaved per sprite must show far fewer draw calls (logged via `--json` `draws`).
56
+ 2. HiDPI sizing + device-loss recovery. The device loss is tested by calling `device.destroy()` from the browser check and expecting rendering to resume.
57
+ 3. SDF text atlas + glyph quads. This also removes the per-string canvas texture cache.
58
+ 4. `textureCreate`, `renderTarget` and `blendMode` (`primitive Blend` in `std/Game`), in the headless runner's `Backend` and `Recorder`, `plum_bridge.js`, README. Orb Run gains a glow (additive) on orbs; the scene demo gets a minimap rendered into a texture.
59
+ 5. Browser checks for game.plum, scene.plum and the new fixtures (screenshots read), benchmark update in `BENCHMARK.md`, README Games section updated.
60
+
61
+ ## How we will know it works
62
+ - Under `--json`, `draws` per frame drops from ~1 per texture switch to ≤ ceil(textures/16) on the interleaved fixture, and the stress-scene frame time does not regress.
63
+ - The browser check passes:
64
+ - at `devicePixelRatio` 2 (Playwright `deviceScaleFactor: 2`), with sharp text and sprites in the screenshot;
65
+ - after a forced `device.destroy()`.
66
+ - The text screenshot stays sharp at sizes 8, 16, 48 and 128, both scaled and zoomed.
67
+ - A render-target fixture draws into a texture and then draws that texture, and the pixel at a known spot has the expected colour. Additive blend brightens, verified the same way.
68
+ - All existing suites pass: `cargo test --workspace`, `plum test`, plum-game and web-test.
69
+
70
+ ## Decisions (settled)
71
+ - WebGPU only (user ruling); no WebGL2 path is touched.
72
+ - Game coordinates stay in CSS pixels; HiDPI is invisible to Plum code.
73
+ - Text keeps the `drawText` signature; font choice stays the page's sans-serif.
74
+
75
+ ## Not in scope
76
+ - Filters (blur, glow shader passes), masks, meshes, 9-slice and tiling sprites, compressed textures, and font files or font selection.