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