xbintsc 0.3.68 → 0.3.79

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.
Files changed (100) hide show
  1. package/README.md +59 -4
  2. package/README.zh-CN.md +52 -2
  3. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  4. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  5. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  6. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  7. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  8. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  9. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  10. package/dist/tests/e2e/gui-example.test.js +2 -2
  11. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  12. package/dist/tests/e2e/gui-helpers.js +5 -0
  13. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  14. package/dist/tests/e2e/gui-layout.test.js +73 -0
  15. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  16. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  17. package/dist/tests/e2e/gui-media.test.js +119 -0
  18. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  19. package/dist/tests/e2e/gui-paint.test.js +40 -0
  20. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  21. package/doc/ai/extensions.md +16 -3
  22. package/doc/ai/zh-CN/extensions.md +10 -3
  23. package/doc/gui/animation.md +46 -0
  24. package/doc/gui/api.md +92 -0
  25. package/doc/gui/build.md +91 -0
  26. package/doc/gui/canvas.md +98 -0
  27. package/doc/gui/dom.md +123 -0
  28. package/doc/gui/input.md +22 -0
  29. package/doc/gui/known-issues.md +66 -0
  30. package/doc/gui/layout.md +72 -0
  31. package/doc/gui/media.md +87 -0
  32. package/doc/gui/rendering.md +98 -0
  33. package/doc/gui/runtime.md +88 -0
  34. package/doc/gui-scripts.md +7 -3
  35. package/doc/gui.md +80 -612
  36. package/doc/implemented.md +1 -0
  37. package/doc/zh-CN/gui/animation.md +46 -0
  38. package/doc/zh-CN/gui/api.md +88 -0
  39. package/doc/zh-CN/gui/build.md +84 -0
  40. package/doc/zh-CN/gui/canvas.md +80 -0
  41. package/doc/zh-CN/gui/dom.md +114 -0
  42. package/doc/zh-CN/gui/input.md +21 -0
  43. package/doc/zh-CN/gui/known-issues.md +50 -0
  44. package/doc/zh-CN/gui/layout.md +60 -0
  45. package/doc/zh-CN/gui/media.md +71 -0
  46. package/doc/zh-CN/gui/rendering.md +81 -0
  47. package/doc/zh-CN/gui/runtime.md +76 -0
  48. package/doc/zh-CN/gui-scripts.md +6 -2
  49. package/doc/zh-CN/gui.md +64 -532
  50. package/doc/zh-CN/implemented.md +1 -0
  51. package/doc/zh-CN/post/xbintsc-build-pelican.md +2 -2
  52. package/llms.txt +15 -2
  53. package/package.json +1 -1
  54. package/runtime/ext_gui/canvas.cpp +580 -0
  55. package/runtime/ext_gui/canvas.h +170 -0
  56. package/runtime/ext_gui/canvas_internal.h +59 -0
  57. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  58. package/runtime/ext_gui/document.cpp +24 -1
  59. package/runtime/ext_gui/document.h +11 -0
  60. package/runtime/ext_gui/dom_api.cpp +3 -0
  61. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  62. package/runtime/ext_gui/dom_api_internal.h +73 -0
  63. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  64. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  65. package/runtime/ext_gui/gui.cpp +6 -0
  66. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  67. package/runtime/ext_gui/gui_engine.h +49 -0
  68. package/runtime/ext_gui/gui_events.cpp +21 -5
  69. package/runtime/ext_gui/gui_media.cpp +193 -0
  70. package/runtime/ext_gui/image.cpp +31 -12
  71. package/runtime/ext_gui/image.h +20 -7
  72. package/runtime/ext_gui/layout.cpp +52 -5
  73. package/runtime/ext_gui/layout.h +33 -1
  74. package/runtime/ext_gui/layout_flex.cpp +263 -99
  75. package/runtime/ext_gui/layout_flow.cpp +85 -9
  76. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  77. package/runtime/ext_gui/layout_inline.cpp +54 -37
  78. package/runtime/ext_gui/media.cpp +514 -0
  79. package/runtime/ext_gui/media.h +104 -0
  80. package/runtime/ext_gui/nanosvg.h +3278 -0
  81. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  82. package/runtime/ext_gui/paint.cpp +123 -38
  83. package/runtime/ext_gui/paint.h +36 -6
  84. package/runtime/ext_gui/renderer.cpp +113 -10
  85. package/runtime/ext_gui/renderer.h +49 -9
  86. package/runtime/ext_gui/renderer_image.cpp +203 -77
  87. package/runtime/ext_gui/renderer_internal.h +6 -8
  88. package/runtime/ext_gui/renderer_text.cpp +11 -26
  89. package/runtime/ext_gui/style.h +11 -0
  90. package/runtime/ext_gui/style_print.cpp +11 -0
  91. package/runtime/ext_gui/style_ua.cpp +7 -0
  92. package/runtime/ext_gui/style_values.cpp +40 -1
  93. package/runtime/ext_gui/svg.cpp +182 -0
  94. package/runtime/ext_gui/svg.h +46 -0
  95. package/runtime/ext_gui/text.cpp +156 -25
  96. package/runtime/ext_gui/text.h +11 -2
  97. package/runtime/ext_gui/window.cpp +4 -1
  98. package/scripts/build-gui.ts +148 -6
  99. package/scripts/check-file-length.ts +5 -1
  100. package/scripts/install-gui-deps.sh +28 -0
package/doc/gui.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # xbintsc GUI extension (self-hosted HTML/CSS renderer)
2
2
 
3
- Status: **M12** — features (M1–M11) are complete: HTML parsing, CSS selector
3
+ Status: **M14** — features (M1–M13) are complete: HTML parsing, CSS selector
4
4
  matching, the cascade, computed styles and layout (block, inline, Flexbox and
5
5
  CSS positioning) are
6
6
  in place, and the engine *paints*: it builds a display list of rectangles, images
7
7
  and shaped text runs and renders them through SDL_GPU. Input events are hit
8
8
  tested and delivered to native TS handlers, `:hover`/`:focus` are matched
9
9
  dynamically, `<img>` is sized from its intrinsic dimensions and drawn from a
10
- texture, and CSS transitions animate paint properties. **M8** adds an interactive
10
+ texture — PNG/JPEG/… bitmaps and SVG files alike, the latter rasterised at the
11
+ drawn size so vector art stays crisp — and CSS transitions animate paint
12
+ properties. **M8** adds an interactive
11
13
  DOM: element handles with stable identity, mutation (`appendChild`, `textContent`,
12
14
  `classList`, `style`, …) and element-level events with capture/bubbling.
13
15
  **M9** compiles `<script>` bodies ahead of time (inline and `<script src>`) — see
@@ -15,10 +17,31 @@ DOM: element handles with stable identity, mutation (`appendChild`, `textContent
15
17
  **M11** adds `position: relative`/`absolute`/`fixed` with their offsets, which is
16
18
  what `examples/gui/pelican-bike` — a playable 2D game — is built on. **M12** makes
17
19
  DOM node lifetime explicit (what a mutation releases, what it keeps) and gives the
18
- engine bounded memory: see "Node lifetime" and "Memory & GC".
19
- This document records the locked decisions, the architecture, the milestone plan
20
- and the current progress of a cross-platform GUI extension that renders an
21
- HTML/CSS UI with its own GPU-accelerated engine.
20
+ engine bounded memory: see [Node lifetime](./gui/dom.md#node-lifetime) and
21
+ [Memory & GC](./gui/runtime.md#memory--gc). **M13** plays real
22
+ `<video>`/`<audio>` — a vendored minimal FFmpeg decodes them on the main thread,
23
+ video frames go through the image texture path and audio to an SDL stream, with
24
+ the DOM media API exposing `play`/`pause`/`currentTime`/… and media events.
25
+ **M14** adds `<canvas>` with a self-written 2D software rasteriser (anti-aliased
26
+ fills, paths, text and `drawImage`) uploaded through the image texture path —
27
+ see [Canvas](./gui/canvas.md).
28
+ This overview records the locked decisions, the architecture, the milestone plan
29
+ and the current progress; the engine itself is documented by function in the
30
+ pages above.
31
+
32
+ ## Contents
33
+
34
+ - [API](./gui/api.md) — window handles, events and diagnostics
35
+ - [DOM, HTML and CSS](./gui/dom.md) — parsing, cascade, interactive DOM and AOT scripts
36
+ - [Layout](./gui/layout.md) — block, inline, Flexbox and positioning
37
+ - [Rendering](./gui/rendering.md) — display list, GPU pipelines and text
38
+ - [Media](./gui/media.md) — images, SVG, video and audio
39
+ - [Canvas](./gui/canvas.md) — `<canvas>` and the 2D context
40
+ - [Input](./gui/input.md) — hit testing, events and `:hover`/`:focus`
41
+ - [Animation](./gui/animation.md) — `requestAnimationFrame` and CSS transitions
42
+ - [Runtime integration](./gui/runtime.md) — event loop, native bridge and memory
43
+ - [Packaging and build](./gui/build.md) — building `gui.a` and running examples
44
+ - [Known issues and open questions](./gui/known-issues.md)
22
45
 
23
46
  ## Goals
24
47
 
@@ -74,7 +97,7 @@ HTML/CSS UI with its own GPU-accelerated engine.
74
97
  ▼
75
98
  windowing + input (SDL3) ── multiple windows
76
99
  text (HarfBuzz + FreeType) ── glyph atlas / SDF
77
- images (stb_image)
100
+ images (stb_image; SVG via NanoSVG)
78
101
  ```
79
102
 
80
103
  The engine never talks to the compiler. The compiler only sees an `Extension`
@@ -88,10 +111,12 @@ with `nativeObjects()`, `linkerFlags()` and `modules()`.
88
111
  | GPU | **SDL_GPU** (SDL3) | one render path over Metal / Vulkan / D3D12; avoids three backends |
89
112
  | Text shaping | **HarfBuzz** | correct complex-script shaping |
90
113
  | Glyph raster | **FreeType** | glyph outlines → GPU atlas / SDF |
91
- | Images | **stb_image** (vendored header) | single header to start |
114
+ | Images | **stb_image** (vendored header) for bitmaps, **NanoSVG** (vendored headers) for SVG | single headers to start; SVG files rasterise on demand |
115
+ | Video + audio | **FFmpeg** (statically vendored, minimal build) | decodes the common container/codec set; no WebView or platform media framework |
92
116
 
93
- Pinned versions: SDL3 `release-3.2.10`, FreeType `2.13.3`, HarfBuzz `10.1.0`
94
- (overridable with `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION`).
117
+ Pinned versions: SDL3 `release-3.2.10`, FreeType `2.13.3`, HarfBuzz `10.1.0`,
118
+ FFmpeg `7.1` (overridable with `SDL3_TAG` / `FREETYPE_VERSION` /
119
+ `HARFBUZZ_VERSION` / `FFMPEG_VERSION`).
95
120
 
96
121
  Alternative if SDL3 is rejected: **GLFW + OpenGL 3.3** (simpler, but OpenGL is
97
122
  deprecated on macOS and gives no modern GPU abstraction). **wgpu-native** if the
@@ -101,508 +126,6 @@ engine were Rust.
101
126
  > xbintsc stays "download and run"; only OS frameworks are added by
102
127
  > `linkerFlags()`.
103
128
 
104
- ## TS-facing API
105
-
106
- ```ts
107
- import { createWindow, run, quit } from "gui";
108
-
109
- const win = createWindow({ title: "Demo", width: 900, height: 600 });
110
- win.setBackground("#14161c");
111
- win.loadHTML(INDEX_HTML); // parses HTML/CSS and computes styles
112
- win.on("ready", () => console.log("first frame presented"));
113
- win.on("close", () => console.log("window closed"));
114
-
115
- run(); // drives the main loop until all windows close
116
- ```
117
-
118
- Methods implemented on a window handle: `setTitle` / `setSize` / `loadHTML` /
119
- `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`, plus `driver()`,
120
- which reports the SDL_GPU backend in use (`"vulkan"`, `"direct3d12"`, `"metal"`,
121
- … — SDL picks the best one the device supports). The Web spellings are available
122
- too: `addEventListener` / `removeEventListener` are aliases of `on` / `off` (their
123
- `options` argument is accepted and ignored) and the `closed` getter mirrors
124
- `isOpen()`, so a document written against the browser API runs unchanged.
125
- Events emitted:
126
- `ready` (after the first presented frame), `load`, `close`, `resize` and the
127
- input events `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`,
128
- `keyup`.
129
-
130
- On Windows a GUI program is linked as a **Windows-subsystem** executable, so
131
- launching it does not flash a console window. That also means stdout/stderr go
132
- nowhere when it is started from Explorer; set `app.console: true` in
133
- `xbintsc.config.json` to keep the console subsystem (useful while debugging).
134
-
135
- Input handlers receive a single payload object (lifecycle handlers receive none):
136
-
137
- ```ts
138
- win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
139
- win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
140
- win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
141
- ```
142
-
143
- | Field | Events | Meaning |
144
- | --- | --- | --- |
145
- | `x`, `y` / `clientX`, `clientY` | pointer, wheel | viewport-relative logical pixels |
146
- | `button` | pointer | `0` left, `1` middle, `2` right (`-1` for move) |
147
- | `clicks` | pointer | click count reported by the OS |
148
- | `deltaX`, `deltaY` | wheel | scroll amount (`deltaY` positive = down) |
149
- | `key`, `code` | keyboard | key name and physical scancode name |
150
- | `repeat`, `ctrl`, `shift`, `alt`, `meta` | keyboard | modifiers |
151
- | `target` | pointer, wheel | deepest element under the point as a CSS-like descriptor (`div#main.card`), or absent |
152
-
153
- ### Implemented HTML/CSS subset (M3a)
154
-
155
- **HTML parser** (`runtime/ext_gui/dom.{h,cpp}`): tags/attributes/text,
156
- comments and doctype skipped, entity decoding (named + numeric/hex), void
157
- elements, raw-text elements (`<style>`/`<script>`), and the common implicit
158
- close rules (`li`, `dt`/`dd`, `option`, `p`, headings, table cells/rows).
159
-
160
- **CSS parser** (`runtime/ext_gui/css.{h,cpp}`): comments and at-rules skipped
161
- (for now), rules with multiple selectors, and declarations. Selectors: type,
162
- `.class`, `#id`, attribute (`=`, `~=`, `|=`, `^=`, `$=`, `*=`), the four
163
- combinators (descendant, child, adjacent and general sibling) and the
164
- pseudo-classes `:first-child`, `:last-child`, `:only-child`, `:empty`,
165
- `:root`, `:not(...)`, `:nth-child(an+b)`, `:disabled`, `:checked`, plus the
166
- stateful `:hover` and `:focus` (see *Implemented input*). Values:
167
- lengths (`px`, `%`, `em`, `rem`, `vw`, `vh`, `pt`, `pc`, `in`, `cm`, `mm`, `q`),
168
- colors (hex, `rgb()`/`rgba()`, a named subset), numbers, keywords and shorthands
169
- (`margin`/`padding`/`border`/`flex`).
170
-
171
- **Cascade & computed style** (`runtime/ext_gui/style.{h,cpp}`): a small built-in
172
- UA stylesheet, author rules sorted by `(!important, specificity, source order)`,
173
- then inline `style=""` (highest specificity, but non-`!important` inline loses to
174
- `!important`), plus CSS inheritance of the text properties. Relative lengths are
175
- kept unresolved until layout, except `font-size` (resolved against the *parent*
176
- font size) and `line-height`.
177
-
178
- ### Diagnostics
179
-
180
- So the HTML/CSS/paint pipeline is testable without a GPU, a window handle
181
- exposes read-only hooks:
182
-
183
- ```ts
184
- win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
185
- win.queryCount(selector) // number of matching elements
186
- win.getBoundingClientRect(selector) // { x, y, width, height } (border box)
187
- win.documentTree() // serialized DOM (debugging)
188
- win.layoutTree() // serialized layout boxes (debugging)
189
- win.paintCount() // number of shapes in the display list
190
- win.paintList() // serialized display list (debugging)
191
- win.measureText(text, fontSize?, family?) // shaped advance width in pixels
192
- win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
193
- win.hitTest(x, y) // deepest element descriptor, or ""
194
- win.sendEvent(type, options?) // synthesise input (testing)
195
- win.advance(ms) // step the CSS transition clock (testing)
196
- win.stats() // engine/runtime counters (memory debugging)
197
- ```
198
-
199
- `win.stats()` returns `{ heapBytes, heapAllocations, treeNodes, detachedRoots,`
200
- `detachedNodes, handles, handleSlots, listeners }`: the live runtime heap, the
201
- size of the DOM tree, the detached-node pool, and the element-handle tables. In a
202
- steady state every one of them is constant — a climbing `detachedNodes` or
203
- `handles` means the program removes nodes faster than it releases them (see
204
- "Node lifetime" below).
205
-
206
- They are used by `tests/e2e/gui-*.test.ts` to assert parsing, selector matching,
207
- specificity, inheritance, `!important` and layout geometry. They will stay useful
208
- afterwards for debugging.
209
-
210
- ### Interactive DOM (M8)
211
-
212
- `win.document` returns the document handle; element handles have stable identity
213
- and read/write properties, attributes, traversal and geometry:
214
-
215
- ```ts
216
- const doc = win.document;
217
- const box = doc.querySelector("#box");
218
- const inner = doc.querySelector("#inner");
219
- inner.textContent = "hi"; // write
220
- inner.classList.add("hot");
221
- inner.style.setProperty("color", "#0f0");
222
- inner.setAttribute("data-role", "lead");
223
- console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
224
-
225
- const created = doc.createElement("div");
226
- created.textContent = "added";
227
- box.appendChild(created);
228
- box.removeChild(created);
229
-
230
- inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
231
- inner.click(); // synthesise a click at the element
232
- console.log(box.offsetWidth, box.offsetHeight); // rounded border box
233
- console.log(box.contains(inner)); // descendant test
234
- ```
235
-
236
- Mutations mark the document dirty; the engine restyles + relayouts lazily (before
237
- the next read or frame). `inner = …`/`innerHTML = …` do **not** run scripts.
238
- Element events support capture and bubble phases, `stopPropagation`, `once`, and
239
- bubble up to `document`/`window`. The legacy `win.on(type, fn)` payload keeps its
240
- **string** `e.target` (`div#id.class`); the element `Event.target` is a handle
241
- whose descriptor matches the same string.
242
-
243
- #### Node lifetime
244
-
245
- A mutation decides whether the nodes it removes can still be reached, and the
246
- engine releases exactly those it cannot:
247
-
248
- | Mutation | Removed nodes |
249
- | --- | --- |
250
- | `innerHTML = …`, `inner = …`, `textContent = …`, `replaceChild` | **released** — the script has no way to refer to them again |
251
- | `removeChild`, `remove` | **kept** in the document's detached pool — the DOM keeps a removed node alive and usable, so it can be re-attached |
252
-
253
- Released nodes free their C++ subtree as well as their handle, so the counters
254
- from `win.stats()` (`detachedNodes`, `handles`, `handleSlots`) return to their
255
- previous values. That is what makes the usual game pattern — rewriting a sprite
256
- with `innerHTML` every frame — run in constant memory; before, every replaced
257
- subtree stayed pooled forever and a loop writing ten nodes per frame leaked
258
- several megabytes per second.
259
-
260
- The one case the engine cannot see is a node the script creates and then drops
261
- without ever attaching it: `document.createElement("div")` parks the node in the
262
- pool *and* keeps its handle reachable, because the mark-sweep collector has no
263
- weak references, so the engine cannot tell that the script threw the handle
264
- away. A loop that creates elements without attaching them therefore grows the
265
- pool. Reuse the elements (or build the content with `innerHTML`, which releases
266
- what it replaces) instead of creating fresh ones per frame.
267
-
268
- ### AOT scripts (M9)
269
-
270
- Import an `.html` file that contains inline `<script lang="ts">` bodies; the
271
- loader compiles each body into a native function and `win.loadHTML(page)` runs
272
- them once the document is parsed (no JavaScript engine, no runtime `eval`):
273
-
274
- ```ts
275
- import { createWindow, run } from "gui";
276
- import page from "./page.html";
277
-
278
- const win = createWindow({ title: "counter", width: 320, height: 240 });
279
- win.loadHTML(page);
280
- run();
281
- ```
282
-
283
- ```html
284
- <button id="b">0</button>
285
- <script lang="ts">
286
- const b = document.getElementById("b");
287
- let n = 0;
288
- b.addEventListener("click", () => { b.textContent = String(++n); });
289
- </script>
290
- ```
291
-
292
- The two parameters (`window`, `document`) are ordinary function arguments, so
293
- script locals need no global-object machinery. External scripts work too:
294
- `<script src="./counter.ts">` is read at compile time, its imports are hoisted
295
- (rewritten to resolve from the HTML file) and its body is wrapped the same way —
296
- so a script module can `import` helpers and still see `document`. Scripts are
297
- **compile-time assets**: HTML created at runtime (`innerHTML`, fetched over the
298
- network) never executes, and `src` URLs (`https://…`, `data:…`) are ignored.
299
- Everything runs in document order after parsing (effectively deferred). See
300
- `doc/gui-scripts.md` for the full design.
301
-
302
- ### Animation frames (M10)
303
-
304
- `requestAnimationFrame` runs a callback once on the next frame; the callback
305
- receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
306
- repainted in the same frame. Re-queue from inside the callback to animate:
307
-
308
- ```ts
309
- const win = createWindow({ title: "anim", width: 320, height: 240 });
310
- let n = 0;
311
- const tick = (t: number) => {
312
- win.document.getElementById("label").textContent = String(n++);
313
- if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
314
- };
315
- win.on("ready", () => win.requestAnimationFrame(tick));
316
- win.loadHTML("<div id='label'>0</div>");
317
- run();
318
- ```
319
-
320
- ### Implemented layout (M3)
321
-
322
- `runtime/ext_gui/layout.{h,cpp}` turns the styled DOM into a `LayoutBox` tree
323
- with absolute (viewport-relative) geometry:
324
-
325
- - **Block flow** — block-level children stack vertically (no margin collapsing
326
- yet); `display: none` generates no box; `width: auto` fills the containing
327
- block, `height: auto` wraps the content. The box model (margin/padding/border)
328
- is resolved, including percentages against the containing block width.
329
- - **Inline flow** — consecutive inline-level children form an anonymous inline
330
- formatting context with greedy, word-based line breaking, `text-align` and
331
- `line-height`. Inline elements get the union of their descendants' geometry;
332
- `display: inline-block` is laid out atomically with a shrink-to-fit width.
333
- Each text fragment remembers the run it covers, so paint can shape it. Text is
334
- measured with the HarfBuzz/FreeType stack (see *Implemented text*).
335
- - **Flexbox** — single-line `row`/`column` (and the `-reverse` variants) with
336
- `gap`, `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and
337
- `align-items` (including `stretch` when the cross size is definite). Item
338
- margins are part of the item's outer size on both axes, so a negative
339
- `margin-top` pulls an item up instead of being ignored.
340
- - **Positioning** — `position: relative` offsets a box (and its subtree) by
341
- `top`/`right`/`bottom`/`left` without changing the space it reserved;
342
- `position: absolute` (and `fixed`, which resolves against the viewport) takes
343
- the box out of flow and places it against the padding box of the nearest
344
- positioned ancestor — the viewport when there is none. An `auto` inset keeps
345
- the static position on that axis, an `auto` width is shrink-to-fit unless both
346
- `left` and `right` are set (then it fills the space between them), and
347
- percentages resolve against the containing block. There is no `z-index`
348
- stacking yet: painting stays in document order.
349
- Note the consequence of that shrink-to-fit rule: an absolutely positioned box
350
- with `width: auto` and no explicit size gets **no** available width for its own
351
- inline content (its preferred width comes from its text or its non-absolute
352
- children only), so inline runs inside it are laid out at zero available width
353
- and each one wraps onto its own line. Give such a box an explicit `width`, or
354
- place its children with `left`/`width` (as
355
- `examples/gui/pelican-bike` does for its sprite runs), instead of relying on
356
- line flow inside it.
357
-
358
- Not yet implemented: margin collapsing, multi-line flex wrapping, `z-index`
359
- stacking, `overflow` clipping and floats.
360
-
361
- **Document** (`runtime/ext_gui/document.{h,cpp}`): owns the DOM tree, gathers
362
- `<style>` text into one stylesheet, computes styles and layout for a viewport and
363
- offers `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`.
364
-
365
- ### Implemented paint (M4a/M4b)
366
-
367
- `runtime/ext_gui/paint.{h,cpp}` walks the layout tree in painter's order and
368
- emits a backend-agnostic `DisplayList` with two parallel lists: **rectangles**
369
- (backgrounds and four solid border edges, with `border-radius`) and **text runs**
370
- (each carrying its text, colour and resolved `FontSpec`). Keeping them separate
371
- lets the renderer draw all rectangles, then all text on top, with one draw call
372
- per list. `DisplayList::dump()` feeds `paintList()`/`paintCount()`.
373
-
374
- `opacity` is honoured only at its extremes: a box whose computed `opacity` is
375
- `0` (and its whole subtree) paints nothing, and anything above `0` paints fully
376
- opaque. A shape carries its colour's alpha only, so there is no compositing of a
377
- partly transparent box — the `opacity` transitions the CSS parser accepts are
378
- therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
379
- the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
380
-
381
- `runtime/ext_gui/renderer.{h,cpp}` turns that list into two batched vertex
382
- buffers per window (one for shapes, one for glyph quads) drawn through two
383
- SDL_GPU graphics pipelines:
384
-
385
- - The shared pipelines are created lazily from the device's supported shader
386
- format. Each of SDL_GPU's three backends consumes a different binary and none
387
- of them compiles GLSL/HLSL at runtime, so the engine ships all three and
388
- `selectShader` (`renderer_shaders.h`) picks the one the device accepts:
389
- **MSL** for Metal (compiled by SDL from the embedded source),
390
- **SPIR-V** for Vulkan (`runtime/ext_gui/spirv/*.{vert,frag}`, compiled by
391
- `glslc`), and **DXIL** for Direct3D 12 (`shaders.hlsl`, compiled by `dxc`).
392
- `scripts/build-gui-shaders.mjs` regenerates the embedded blobs and the result
393
- is committed, so nothing extra is needed to build the engine.
394
- The descriptor bindings differ per format — SPIR-V follows what
395
- `SDL_gpu_vulkan.c` builds (uniforms `set=1, binding=0`, samplers
396
- `set=2, binding=0`, textures `set=2, binding=1`) — and glslc names every entry
397
- point `main` where MSL/DXIL keep the descriptive names.
398
- Note that SDL 3.2.10's Direct3D 12 backend cannot create a graphics pipeline
399
- whose shaders declare a uniform buffer (it fails with `E_INVALIDARG`); the
400
- engine passes the viewport that way, so the DXIL path is built but not usable
401
- until that is resolved upstream.
402
- - A **rounded-rectangle distance field** in the shape fragment shader gives
403
- antialiased fills; the vertex carries `position`, `local`, `half extents`,
404
- `radius` and colour, and a viewport-size push constant does the projection.
405
- Alpha blending is enabled.
406
- - The text pipeline samples a **single shared grayscale glyph atlas**
407
- (`R8_UNORM`, 2048², shelf-packed, LINEAR filtering) and draws each glyph as a
408
- textured quad (`position`, `uv`, colour), modulating alpha by the coverage.
409
- - Geometry is uploaded only when the document or viewport changes
410
- (`geometry.dirty`), so steady-state frames are bind-and-draw.
411
-
412
- The window background (`setBackground`) is the render-pass clear colour.
413
-
414
- Still to do in M4: gradients.
415
-
416
- ### Implemented input (M5)
417
-
418
- `LayoutTree::hitTest` returns the deepest box containing a point (probing later
419
- siblings first so the topmost element wins), and `xt_dom_describe` turns the
420
- element into the `div#id.class` descriptor carried by event payloads.
421
-
422
- - SDL pointer/wheel/key events are routed to the owning window, hit tested, and
423
- delivered to the handlers registered with `on`. `mousedown` also moves focus.
424
- - `:hover` matches the hovered element **and its ancestors** (so hovering a child
425
- lights up its parents); `:focus` matches the focused element. When either
426
- changes, `XtDocument::setHover`/`setFocus` recompute styles and layout and mark
427
- the window's geometry dirty, so the change is painted on the next frame.
428
- - `win.hitTest(x, y)` and `win.sendEvent(type, options)` expose hit testing and
429
- synthetic input so the whole path is testable headlessly (the e2e suite drives
430
- clicks, wheels and keys without a real mouse).
431
-
432
- Still to do for full input: text selection, drag, IME and clipboard.
433
-
434
- ### Implemented images (M6a)
435
-
436
- `runtime/ext_gui/image.{h,cpp}` wraps the vendored **stb_image** header
437
- decoding PNG/JPEG/BMP/GIF/TGA to RGBA8, with a decode cache and a header-only
438
- size cache (`stbi_info`). Paths accept a `file://` prefix and percent-encoding.
439
-
440
- - `<img>` is a replaced element (`display: inline-block`): layout gives it the
441
- CSS size when set, otherwise the intrinsic pixel size, and preserves the
442
- aspect ratio when only one axis is constrained. The intrinsic size is read
443
- from the file header (no full decode) while building the box tree.
444
- - Paint emits one `PaintImage` per `<img>`; the renderer decodes/uploads each
445
- unique `src` to an RGBA texture (cached by path) and draws textured quads,
446
- batching consecutive quads that share a texture.
447
-
448
- Still to do for images: CSS `background-image: url(...)`, `data:` URIs,
449
- `object-fit` and 9-slice borders.
450
-
451
- ### Implemented transitions (M6b)
452
-
453
- `XtDocument` runs CSS transitions between the *target* computed styles (the
454
- cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
455
- for layout and paint. When a state change alters a transitioned property, a
456
- running transition is recorded and re-applied every frame until it finishes;
457
- the engine advances the clock with real frame deltas (`XtDocument::advance`),
458
- and `win.advance(ms)` lets tests step it deterministically.
459
-
460
- - Supported properties: `background-color`, `color`, `border-color`,
461
- `border-radius`; `transition: all` covers them. Structural/layout-affecting
462
- properties are not animated yet (that would relayout every frame).
463
- - Both the `transition` shorthand and the `transition-property` / `-duration` /
464
- `-delay` / `-timing-function` longhands are parsed; time values accept `s` and
465
- `ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
466
- `ease-in-out` (`ease` is a smoothstep approximation).
467
- - Retargeting mid-flight starts a new transition from the current interpolated
468
- value, so reversing a hover animates smoothly from wherever it was.
469
- - `computedStyle()` reports the displayed (interpolated) value, so transitions
470
- are directly observable in tests.
471
-
472
- Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
473
-
474
- ### Implemented text stack (M4b)
475
-
476
- `runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
477
- (metrics + eventual rasterisation). Both are built statically and linked into
478
- `gui.a` by `scripts/build-gui.ts`.
479
-
480
- - Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
481
- DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
482
- `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
483
- Only regular upright faces are used for now; weight/italic selection is a
484
- later refinement.
485
- - The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
486
- Windows, PingFang on macOS, Noto Sans CJK/WenQuanYi on Linux) before the
487
- Latin-only ones, because a run is shaped with a *single* face: without this,
488
- Chinese/Japanese/Korean text renders as blank or `.notdef` boxes. Set
489
- `XT_GUI_FONT` to a Latin-only face to get its Latin design instead (at the
490
- cost of CJK coverage).
491
- - `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
492
- ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
493
- descent / normal line height. When no font file can be found the module falls
494
- back to a deterministic per-byte approximation, so layout still works.
495
- - `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
496
- an 8-bit bitmap; the renderer packs those into the atlas. On HiDPI displays
497
- glyphs are rasterised at `font_size * SDL_GetWindowPixelDensity` while quads
498
- are positioned in logical pixels, so text stays crisp.
499
- - Layout uses these real metrics for text widths, line breaking and
500
- `line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
501
-
502
- Multiple windows fall out of the object model: `createWindow` returns a native
503
- object handle; each handle owns its own `SDL_Window`/GPU surface and its own DOM
504
- tree. `run()` drives one shared main loop that ticks every window and exits when
505
- the last one closes.
506
-
507
- ## Event loop integration
508
-
509
- The runtime change (already landed):
510
-
511
- - `int xt_loop_poll(int timeout_ms)` — one reactor iteration; `0` polls without
512
- blocking, `< 0` blocks.
513
- - `void xt_loop_set_main(xt_main_loop_fn fn)` — a host may take over the main
514
- loop. `xt_run_event_loop()` delegates to it; the generated `main` is unchanged.
515
- - `xt_loop_set_main(NULL)` restores the default `select(2)` loop.
516
-
517
- The GUI engine can either register its own loop through `xt_loop_set_main`, or
518
- expose an explicit `run()`; **M2 uses the explicit `run()`** so the window is a
519
- plain native call the TypeScript program controls. Each tick it:
520
-
521
- 1. pumps SDL window/input events for every window,
522
- 2. builds/uploads the display list when it changed and renders every open
523
- window (clear pass + shape geometry + text geometry),
524
- 3. calls `xt_loop_poll(0)` and `xt_drain_microtasks()` so sockets/timers and
525
- `await` continuations keep making progress,
526
- 4. repeats until all windows close or `quit()` is called.
527
-
528
- The `xt_loop_set_main` hook remains available for hosts that want to own the
529
- loop themselves.
530
-
531
- This keeps network I/O, timers and `await` working inside a GUI program.
532
-
533
- ## Native ↔ TS bridge
534
-
535
- - **TS → engine**: direct `xt_gui_*` calls / window methods.
536
- - **Engine → TS**: `xt_call_with_this(fn, thisValue, argc, argv)` with function
537
- values captured from TS (e.g. event handlers registered with `win.on(...)`).
538
- - **Future page→native RPC**: not needed while there is no page JS; native TS is
539
- the controller. If a declarative layer is added later, it will use the same
540
- `xt_call_*` entry points.
541
-
542
- ## Packaging & build
543
-
544
- - Sources live in `runtime/ext_gui/` (C/C++) plus vendored libs under `vendor/`
545
- (gitignored; fetched on demand).
546
- - `npm run gui` (`scripts/build-gui.ts`) fetches a pinned SDL3 (`SDL3_TAG`,
547
- default `release-3.2.10`), builds a static SDL3, fetches and builds static
548
- FreeType (`FREETYPE_VERSION`) and HarfBuzz (`HARFBUZZ_VERSION`), compiles the
549
- engine and merges everything into `runtime/lib/<os>-<arch>/gui.a` (or
550
- `gui.lib` with the MSVC ABI on Windows), same convention as `core.a`,
551
- statically bundling all three. The merge uses `libtool` on macOS and
552
- `ar -M` (GNU/LLVM) elsewhere; CMake archives are discovered under the build
553
- root or `Release/` for either generator style.
554
- - `src/extensions/gui/index.ts` exposes the archive through `nativeObjects()`
555
- (via `findRuntimeLibrary`, so `.a`/`.lib` both work) and the OS frameworks
556
- through `linkerFlags()`.
557
- - CI builds `gui.a` before assembling the release archive so it ships inside
558
- `runtime/lib/<slug>/` (the release tarball copies the whole `runtime/` tree).
559
- The archive is built and the example is run on Linux (under Xvfb, with the
560
- lavapipe software Vulkan driver) and macOS; Windows is provisional.
561
- - The existing cache fingerprint already hashes `nativeObjects()` contents, so
562
- rebuilding `gui.a` invalidates cached binaries automatically.
563
-
564
- ### Running the example
565
-
566
- ```sh
567
- npm run runtime # rebuild core.a after a runtime change
568
- npm run gui # build runtime/lib/<os>-<arch>/gui.a (fetches SDL3 once)
569
- xbintsc run examples/gui/hello.ts --ext gui
570
- ```
571
-
572
- Two examples live here: `hello.ts` (HTML/CSS + a reactive counter) and
573
- [`pelican-bike/`](../examples/gui/pelican-bike), a playable 2D game that uses
574
- positioned layers, CSS sprites, `requestAnimationFrame` and keyboard input — see
575
- its README for controls and the geometry report it prints.
576
-
577
- Set `XT_GUI_AUTOCLOSE_MS=<n>` to close all windows after `n` milliseconds,
578
- which the e2e test (`tests/e2e/gui-*.test.ts`) uses to run headlessly.
579
-
580
- Those suites are skipped on Windows by default (the archive is built there but
581
- the run is not validated in CI). With a `gui.lib` and a working GPU, opt in with
582
- `xbintsc_GUI_TESTS=1`:
583
-
584
- ```bat
585
- set xbintsc_GUI_TESTS=1
586
- npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
587
- ```
588
-
589
- On a headless Linux box, install the SDL3 build headers and run under Xvfb with a
590
- software Vulkan driver:
591
-
592
- ```sh
593
- sudo apt-get install -y clang cmake libx11-dev libxext-dev libxrandr-dev \
594
- libxcursor-dev libxi-dev libxinerama-dev libxfixes-dev libxkbcommon-dev \
595
- libwayland-dev wayland-protocols libdecor-0-dev libasound2-dev libpulse-dev \
596
- libdbus-1-dev libudev-dev libdrm-dev libgbm-dev libgl1-mesa-dev \
597
- libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers xvfb
598
- npm run runtime && npm run gui
599
- xvfb-run -a --server-args="-screen 0 1280x720x24" \
600
- npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
601
- ```
602
-
603
- Those are the packages the CI `compile-examples` job installs. The X11 backend is
604
- used by default; Wayland is enabled too but not yet exercised.
605
-
606
129
  ## Milestones
607
130
 
608
131
  1. **M1 — foundation** ✅
@@ -633,8 +156,10 @@ used by default; Wayland is enabled too but not yet exercised.
633
156
  restyle (`css.*`, `style.*`, `document.*`, `gui.cpp`), plus the
634
157
  `hitTest`/`sendEvent` test hooks.
635
158
  6. **M6 — images, then CSS transitions/animations**
636
- - **M6a — images** ✅ stb_image decode, `<img>` replaced-element layout,
637
- per-file GPU textures and textured quads (`image.*`, `paint.*`, `renderer.*`).
159
+ - **M6a — images** ✅ stb_image decode (bitmaps) and NanoSVG rasterisation
160
+ (SVG files, at the drawn pixel size), `<img>` replaced-element layout,
161
+ per-file GPU textures and textured quads (`image.*`, `svg.*`, `paint.*`,
162
+ `renderer.*`).
638
163
  - **M6b — transitions/animations** ✅ `transition` shorthand + longhands,
639
164
  animated `background-color`/`color`/`border-color`/`border-radius`, retargeting
640
165
  and `win.advance(ms)`. `@keyframes` remain.
@@ -650,7 +175,7 @@ used by default; Wayland is enabled too but not yet exercised.
650
175
  keyed by `scripts/build-gui.ts`.
651
176
  - Windows (build + run) stays provisional until the MSVC-compatible
652
177
  `gui.lib` and D3D12/DXIL shader path are validated; a failure is logged
653
- without annotating the run (see *Open questions*).
178
+ without annotating the run (see [Open questions](./gui/known-issues.md#open-questions)).
654
179
  8. **M8 — interactive DOM** ✅
655
180
  - Element/document handles with stable identity, read/write properties via
656
181
  runtime accessors, traversal/attributes/queries, mutation with lazy
@@ -697,7 +222,24 @@ used by default; Wayland is enabled too but not yet exercised.
697
222
  so a leaking or spiking program cannot spiral (`runtime/xt_alloc.c`).
698
223
  `XT_GC_TRACE=1` reports every collection.
699
224
  - `win.stats()` exposes the engine counters; e2e coverage in
700
- `tests/e2e/gui-retention.test.ts` (see "Memory & GC").
225
+ `tests/e2e/gui-retention.test.ts` (see [Memory & GC](./gui/runtime.md#memory--gc)).
226
+ 13. **M13 — video & audio** ✅
227
+ - `<video>` / `<audio>` decode through a statically vendored minimal FFmpeg
228
+ (`media.*`, `gui_media.cpp`), frames upload to GPU textures through the
229
+ existing image pipeline, audio is pushed to an SDL stream on the main
230
+ thread, and the DOM media API plus `loadedmetadata`/`play`/`pause`/
231
+ `ended`/`timeupdate`/`error` events are exposed on element handles
232
+ (`dom_api_media.cpp`, `dom_api_proto.cpp`). Controlled from TS + CSS;
233
+ there are no built-in controls. Example in `examples/gui/media.*`, e2e in
234
+ `tests/e2e/gui-media.test.ts`.
235
+ 14. **M14 — `<canvas>` 2D** ✅
236
+ - `<canvas>` is a replaced element (`xtgui::Canvas2D`, `layout.*`), the
237
+ `getContext("2d")` handle and drawing API live in `dom_api_canvas.cpp`,
238
+ the self-written anti-aliased software rasteriser in `canvas_raster.cpp`,
239
+ and the per-window lifetime in `gui_canvas.cpp`. The bitmap uploads to a
240
+ GPU texture through the image path (`renderer_image.cpp`) and the display
241
+ list carries a `canvas_id` (`paint.*`). See [Canvas](./gui/canvas.md);
242
+ e2e in `tests/e2e/gui-canvas.test.ts`.
701
243
 
702
244
  ## Progress log
703
245
 
@@ -722,7 +264,9 @@ used by default; Wayland is enabled too but not yet exercised.
722
264
  `hitTest`/`sendEvent` test hooks, e2e coverage.
723
265
  - **M6a** ✅ image decoding (`image.*`, vendored stb_image), `<img>` intrinsic
724
266
  sizing in layout, `PaintImage` in the display list, per-file RGBA textures and
725
- an image pipeline in `renderer.*`, e2e coverage.
267
+ an image pipeline in `renderer.*`, e2e coverage. SVG files are decoded too
268
+ (`svg.*`, vendored NanoSVG): a vector source is rasterised at the drawn device
269
+ pixel size and cached by that size, so scaling and HiDPI stay crisp.
726
270
  - **M6b** ✅ CSS transitions (parsing in `style.*`, an animation clock and
727
271
  transition state in `document.*`, `advance`/`advance(ms)` hooks, e2e coverage).
728
272
  - **M7** ✅ builds `gui.a` in CI on Linux and macOS (required) and runs the
@@ -762,99 +306,23 @@ used by default; Wayland is enabled too but not yet exercised.
762
306
  inert, and the collector's trigger follows the live set instead of only
763
307
  ratcheting upwards. `win.stats()` exposes the engine counters and
764
308
  `tests/e2e/gui-retention.test.ts` pins them; a game loop rewriting its sprite
765
- every frame now runs in constant memory (see "Memory & GC").
766
-
767
- ## Memory & GC
768
-
769
- The engine owns most of a GUI program's memory outside the runtime heap: the DOM
770
- tree, the detached-node pool, the element-handle tables, the layout box tree and
771
- the display list are C++, so the mark-sweep collector cannot see them. `stats()`
772
- (see "Diagnostics") reports the parts that can grow:
773
-
774
- | Counter | Expected in a steady state |
775
- | --- | --- |
776
- | `treeNodes` | constant — the authored document |
777
- | `detachedRoots`, `detachedNodes` | 0, or constant for nodes the script removed itself and still holds |
778
- | `handles`, `handleSlots` | constant — one per element the script still refers to |
779
- | `listeners` | constant — one entry per element with listeners |
780
- | `heapBytes`, `heapAllocations` | sawtooth around the collector's threshold |
781
-
782
- Everything else is per-frame scratch (boxes, display list, vertex staging) that is
783
- freed before the next frame. A counter that climbs with the frame count is the
784
- leak signature to look for first; `tests/e2e/gui-retention.test.ts` asserts that
785
- the usual mutation patterns leave all of them unchanged.
786
-
787
- Two engine details matter when reading those numbers:
788
-
789
- - **What releases nodes.** `innerHTML`/`inner`/`textContent`/`replaceChild`
790
- release the subtrees they drop; `removeChild`/`remove` keep them (see "Node
791
- lifetime"). A game that rebuilds a sprite with `innerHTML` therefore runs in
792
- constant memory.
793
- - **What can still accumulate.** A node created with `createElement` and never
794
- attached stays in the pool, because the collector has no weak references and
795
- the engine cannot tell that the script dropped its handle. The same applies to
796
- a node removed and then forgotten by the script. Both are bounded by the number
797
- of *live* handles, but a program that creates elements per frame without
798
- attaching them grows the pool with the frame count.
799
-
800
- The collector behind `heapBytes` is a non-moving mark-sweep (`runtime/xt_alloc.c`).
801
- Its trigger follows the live set: after every collection the threshold becomes
802
- twice the surviving heap (floor 1 MiB), so a small live set means small, frequent
803
- collections and a bounded resident set. `XT_GC_THRESHOLD=<bytes>` pins the
804
- threshold instead, and `XT_GC_TRACE=1` prints one line per collection (heap and
805
- live-allocation counts before and after, the next trigger, and the live set by
806
- object kind) to stderr — that is how the DOM leak was traced to the node pool.
807
-
808
- ## Known issues
809
-
810
- - **Intermittent crash at auto-close.** `XT_GUI_AUTOCLOSE_MS` can end a run
811
- with a native access violation in roughly two runs out of five; a clean exit
812
- looks identical otherwise. Windows reports it as the NTSTATUS exit code
813
- `0xC0000005`; on Linux/macOS the process dies with a signal — an access
814
- violation is `SIGSEGV`, so `spawnSync` reports `status: null` plus a
815
- `signal`. It only reproduces with a *running animation-frame loop* (a static
816
- document closes fine) and it is **not** the game's own code: no exception is
817
- reported, the crash usually lands inside the first second regardless of the
818
- deadline, and raising `XT_GC_THRESHOLD` hides it, so it looks like a
819
- GC/teardown race. Interactive runs (no auto-close) are stable for minutes.
820
- A leading cause was that the engine cached `xt_value`s in C++ containers
821
- the mark-sweep collector cannot see — the AOT `<script>` registry, element
822
- handles, element listeners and the queued/currently-running
823
- `requestAnimationFrame` callbacks. They are now marked by GC root providers
824
- (`xt_gui_script_gc_scan` and `xt_gui_gc_scan_roots`), which removes that
825
- class of use-after-free. Since the M12 node-lifetime work it has not
826
- reproduced at all: 44 auto-close runs of `examples/gui/pelican-bike` (deadlines
827
- 800 ms–4 s, with the default, a 1 MiB and a 64 MiB collector threshold, where
828
- the old report said roughly two in five) all exited cleanly, which points at
829
- the same root cause — a detached-node pool and handle table that grew with the
830
- frame count while the collector's threshold ratcheted up. It stays listed
831
- because the archive is built per platform and the failure is timing-sensitive;
832
- `tests/e2e/gui-example.test.ts` still treats either crash spelling as an
833
- acceptable outcome, and the behavioural assertions live in
834
- `tests/e2e/gui-example-browser.test.ts` (no GPU, deterministic).
835
- - **`display: none -> flex` restyle.** A hidden element being shown by a
836
- *class change* on a flex container has also been observed to crash the same
837
- way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
838
- instead.
839
-
840
- ## Open questions
309
+ every frame now runs in constant memory (see [Memory & GC](./gui/runtime.md#memory--gc)).
310
+ - **M13** ✅ `<video>` / `<audio>` support. A minimal static FFmpeg is vendored
311
+ and merged into `gui.a` (`scripts/build-gui.ts`), `media.*` decodes video to
312
+ RGBA frames and audio to 48 kHz stereo PCM on the main thread, `gui_media.cpp`
313
+ owns the players and pumps the media clock and events each frame,
314
+ `renderer_image.cpp` uploads each new video frame through the image texture
315
+ path, and `dom_api_media.cpp` exposes `play`/`pause`/`load` plus the media
316
+ properties on element handles. `examples/gui/media.*` demonstrates the
317
+ TS-plus-CSS-controls pattern and `tests/e2e/gui-media.test.ts` pins it.
318
+ - **M14** ✅ `<canvas>` support. A new replaced element (`layout.*`) carries an
319
+ intrinsic size from its `width`/`height` attributes (default `300x150`),
320
+ `xtgui::Canvas2D` (`canvas.{h,cpp}`) owns the RGBA8 bitmap and drawing state,
321
+ `canvas_raster.cpp` is a self-written anti-aliased software rasteriser
322
+ (4 sub-scanlines + analytic horizontal coverage, non-zero winding), the
323
+ `getContext("2d")` handle and its API are bound in `dom_api_canvas.cpp`, and
324
+ `gui_canvas.cpp` owns the per-window lifetime. The bitmap uploads through the
325
+ image texture path (`renderer_image.cpp`) and the display list carries a
326
+ `canvas_id` (`paint.*`). See [Canvas](./gui/canvas.md); e2e in
327
+ `tests/e2e/gui-canvas.test.ts`.
841
328
 
842
- - Whether Linux ships X11, Wayland, or both in the first cut. (Decision:
843
- X11 first, Wayland later.)
844
- - **SVG:** there is no SVG support and none is planned. The HTML parser knows
845
- elements and attributes, the layout engine knows boxes, and paint knows
846
- rectangles/text/images, so `<svg>` trees would need a whole second
847
- geometry+paint path. An SVG *file* works only through the image loader
848
- (`<img src="logo.svg">`), which needs an SVG rasteriser (`stb_image` does not
849
- decode SVG); vector art therefore has to arrive as a raster (PNG) or be drawn
850
- from boxes, as `examples/gui/pelican-bike` does.
851
- - Windows: an MSVC-compatible `gui.lib` (COFF objects + `ar -M`/`llvm-ar`) is
852
- produced by `scripts/build-gui.ts`, but it has not been validated in CI yet,
853
- so the Windows step is provisional (the job stays green) and `package` skips
854
- it. It also needs an SDL3 build with the D3D12/DXIL backend (DXIL requires
855
- `dxc`).
856
- - **D3D12 shaders:** the SPIR-V (Vulkan) and DXIL (Direct3D 12) blobs are built
857
- and embedded, so the non-Metal path no longer skips geometry. DXIL remains
858
- unusable on **SDL 3.2.10**: its D3D12 backend rejects any graphics pipeline
859
- whose shaders declare a uniform buffer, and the engine passes the viewport that
860
- way. Re-check after an SDL upgrade.