xbintsc 0.3.69 → 0.3.81

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 (115) hide show
  1. package/README.md +57 -4
  2. package/README.zh-CN.md +51 -2
  3. package/dist/src/driver/compiler.js +6 -1
  4. package/dist/src/driver/compiler.js.map +1 -1
  5. package/dist/src/extensions/gui/index.js +10 -2
  6. package/dist/src/extensions/gui/index.js.map +1 -1
  7. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  8. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  9. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  10. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  11. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  12. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  13. package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
  14. package/dist/tests/e2e/gui-example-svg.test.js +105 -0
  15. package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
  16. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  17. package/dist/tests/e2e/gui-example.test.js +2 -2
  18. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  19. package/dist/tests/e2e/gui-helpers.js +5 -0
  20. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  21. package/dist/tests/e2e/gui-layout.test.js +73 -0
  22. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  24. package/dist/tests/e2e/gui-media.test.js +119 -0
  25. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  26. package/dist/tests/e2e/gui-paint.test.js +40 -0
  27. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  28. package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
  29. package/dist/tests/e2e/gui-transform.test.js +198 -0
  30. package/dist/tests/e2e/gui-transform.test.js.map +1 -0
  31. package/doc/ai/cli.md +5 -0
  32. package/doc/ai/extensions.md +20 -3
  33. package/doc/ai/zh-CN/extensions.md +14 -3
  34. package/doc/gui/animation.md +97 -0
  35. package/doc/gui/api.md +92 -0
  36. package/doc/gui/build.md +91 -0
  37. package/doc/gui/canvas.md +98 -0
  38. package/doc/gui/dom.md +128 -0
  39. package/doc/gui/input.md +25 -0
  40. package/doc/gui/known-issues.md +78 -0
  41. package/doc/gui/layout.md +108 -0
  42. package/doc/gui/media.md +87 -0
  43. package/doc/gui/rendering.md +113 -0
  44. package/doc/gui/runtime.md +88 -0
  45. package/doc/gui-scripts.md +7 -3
  46. package/doc/gui.md +109 -612
  47. package/doc/implemented.md +1 -0
  48. package/doc/zh-CN/gui/animation.md +93 -0
  49. package/doc/zh-CN/gui/api.md +88 -0
  50. package/doc/zh-CN/gui/build.md +84 -0
  51. package/doc/zh-CN/gui/canvas.md +80 -0
  52. package/doc/zh-CN/gui/dom.md +119 -0
  53. package/doc/zh-CN/gui/input.md +23 -0
  54. package/doc/zh-CN/gui/known-issues.md +60 -0
  55. package/doc/zh-CN/gui/layout.md +91 -0
  56. package/doc/zh-CN/gui/media.md +71 -0
  57. package/doc/zh-CN/gui/rendering.md +92 -0
  58. package/doc/zh-CN/gui/runtime.md +76 -0
  59. package/doc/zh-CN/gui-scripts.md +6 -2
  60. package/doc/zh-CN/gui.md +89 -532
  61. package/doc/zh-CN/implemented.md +1 -0
  62. package/llms.txt +15 -2
  63. package/package.json +1 -1
  64. package/runtime/ext_gui/canvas.cpp +580 -0
  65. package/runtime/ext_gui/canvas.h +170 -0
  66. package/runtime/ext_gui/canvas_internal.h +59 -0
  67. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  68. package/runtime/ext_gui/document.cpp +45 -4
  69. package/runtime/ext_gui/document.h +15 -1
  70. package/runtime/ext_gui/dom_api.cpp +3 -0
  71. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  72. package/runtime/ext_gui/dom_api_internal.h +73 -0
  73. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  74. package/runtime/ext_gui/dom_api_node.cpp +6 -4
  75. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  76. package/runtime/ext_gui/gui.cpp +6 -0
  77. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  78. package/runtime/ext_gui/gui_engine.h +49 -0
  79. package/runtime/ext_gui/gui_events.cpp +21 -5
  80. package/runtime/ext_gui/gui_media.cpp +193 -0
  81. package/runtime/ext_gui/image.cpp +31 -12
  82. package/runtime/ext_gui/image.h +20 -7
  83. package/runtime/ext_gui/layout.cpp +130 -6
  84. package/runtime/ext_gui/layout.h +54 -1
  85. package/runtime/ext_gui/layout_flex.cpp +263 -99
  86. package/runtime/ext_gui/layout_flow.cpp +85 -9
  87. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  88. package/runtime/ext_gui/layout_inline.cpp +54 -37
  89. package/runtime/ext_gui/media.cpp +514 -0
  90. package/runtime/ext_gui/media.h +104 -0
  91. package/runtime/ext_gui/nanosvg.h +3278 -0
  92. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  93. package/runtime/ext_gui/paint.cpp +156 -38
  94. package/runtime/ext_gui/paint.h +42 -6
  95. package/runtime/ext_gui/renderer.cpp +117 -12
  96. package/runtime/ext_gui/renderer.h +49 -9
  97. package/runtime/ext_gui/renderer_image.cpp +205 -78
  98. package/runtime/ext_gui/renderer_internal.h +12 -9
  99. package/runtime/ext_gui/renderer_text.cpp +46 -39
  100. package/runtime/ext_gui/style.h +21 -0
  101. package/runtime/ext_gui/style_print.cpp +17 -0
  102. package/runtime/ext_gui/style_ua.cpp +7 -0
  103. package/runtime/ext_gui/style_values.cpp +83 -1
  104. package/runtime/ext_gui/svg.cpp +182 -0
  105. package/runtime/ext_gui/svg.h +46 -0
  106. package/runtime/ext_gui/text.cpp +156 -25
  107. package/runtime/ext_gui/text.h +11 -2
  108. package/runtime/ext_gui/transform.cpp +412 -0
  109. package/runtime/ext_gui/transform.h +112 -0
  110. package/runtime/ext_gui/window.cpp +10 -5
  111. package/scripts/build-gui.ts +148 -6
  112. package/scripts/check-file-length.ts +5 -1
  113. package/scripts/install-gui-deps.sh +28 -0
  114. package/src/driver/compiler.ts +6 -1
  115. package/src/extensions/gui/index.ts +10 -2
@@ -0,0 +1,87 @@
1
+ # Media: images, video and audio
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Images (M6a)
7
+
8
+ `runtime/ext_gui/image.{h,cpp}` wraps the vendored **stb_image** header
9
+ decoding PNG/JPEG/BMP/GIF/TGA to RGBA8, with a decode cache and a header-only
10
+ size cache (`stbi_info`). Paths accept a `file://` prefix and percent-encoding.
11
+ `runtime/ext_gui/svg.{h,cpp}` adds SVG on top: it wraps the vendored **NanoSVG**
12
+ library (parse + anti-aliased scanline rasteriser), detects an SVG by extension
13
+ or by its contents, and produces the same RGBA8 `Image` the pipeline already
14
+ consumes. `stb_image` cannot decode SVG, so this is what makes
15
+ `<img src="logo.svg">` work.
16
+
17
+ - `<img>` is a replaced element (`display: inline-block`): layout gives it the
18
+ CSS size when set, otherwise the intrinsic pixel size, and preserves the
19
+ aspect ratio when only one axis is constrained. The intrinsic size is read
20
+ from the file header (no full decode) while building the box tree, and for an
21
+ SVG it comes from the parsed `width`/`height` or the `viewBox` (falling back
22
+ to 300x150 when neither is present).
23
+ - Paint emits one `PaintImage` per `<img>`; the renderer uploads each unique
24
+ `src` to an RGBA texture and draws textured quads, batching consecutive quads
25
+ that share a texture. A **bitmap** decodes at its intrinsic resolution and is
26
+ cached by path; a **vector** (SVG) is rasterised for the drawn size in device
27
+ pixels (logical size × the window's pixel density), preserving the viewBox
28
+ aspect ratio, and cached by that size, so scaling or a HiDPI display produces
29
+ a crisp render instead of a magnified bitmap.
30
+ - The parse is cached per path; the parse and rasteriser state are released in
31
+ `xt_svg_shutdown` (`xt_image_shutdown`).
32
+
33
+ Still to do for images: CSS `background-image: url(...)`, `data:` URIs,
34
+ `object-fit`, 9-slice borders and inline `<svg>` elements (only SVG *files* are
35
+ supported; see [Open questions](./known-issues.md#open-questions)).
36
+
37
+ ## Video & audio (M13)
38
+
39
+ `runtime/ext_gui/media.{h,cpp}` wraps a statically vendored **FFmpeg** build and
40
+ `runtime/ext_gui/gui_media.cpp` connects the DOM tree to it. `<video>` and
41
+ `<audio>` really decode and play; there is no WebView and no platform media
42
+ framework in the path.
43
+
44
+ - **Decoding.** `avformat_open_input` opens the `src` (a path — `file://` is
45
+ accepted, but media paths are *not* percent-decoded, unlike images), the
46
+ best stream is selected for each kind, and a software decoder yields
47
+ `yuv/rgb` frames which `sws_scale` converts to RGBA8 at the coded size. The
48
+ supported containers/codecs are the minimal FFmpeg configuration from
49
+ `scripts/build-gui.ts`: MP4/MOV/MKV/AVI/FLV/MPEG-TS/M4V/ASF, MP3/OGG/WAV/FLAC/
50
+ AAC; H.264, HEVC, VP8, VP9, AV1, MPEG-4, MJPEG video; AAC, MP3, Opus, Vorbis,
51
+ FLAC, ALAC, AC3/E-AC3, WMAv2 and the raw PCM formats audio.
52
+ - **Video.** `<video>` is a replaced element like `<img>` (UA default
53
+ `300x150`, overridden by the decoded size via `setMediaSize`). Paint emits the
54
+ same `PaintImage` as an image; the renderer uploads each new decoded frame to
55
+ an RGBA texture (`ensureMediaTexture`) and draws a textured quad, so the GPU
56
+ scales it to the CSS box instead of the CPU resizing. Playback timing is the
57
+ wall clock (`SDL_GetTicks`), not the frame delta.
58
+ - **Audio.** There is no audio callback: the decoder is pushed on the main
59
+ thread as it is ticked and the PCM is queued with
60
+ `SDL_OpenAudioDeviceStream` in push mode (`NULL` callback), buffering about
61
+ 0.25 s ahead at 48 kHz stereo `SDL_AUDIO_F32`. `volume` maps to
62
+ `SDL_SetAudioStreamGain`; audio is best-effort and is skipped when no device
63
+ is available (headless CI).
64
+ - **Threading.** The engine is single-threaded and not thread-aware, so all
65
+ decoding happens on the main thread inside `xt_gui_media_tick` — no decode
66
+ thread, no cross-thread GC exposure.
67
+ - **DOM API.** Element handles expose the media surface directly (see
68
+ `runtime/ext_gui/dom_api_media.cpp` and the prototype in
69
+ `dom_api_proto.cpp`): `play()` / `pause()` / `load()`, and the
70
+ `paused` / `ended` / `duration` / `currentTime` / `volume` / `muted` / `loop`
71
+ / `autoplay` / `videoWidth` / `videoHeight` / `readyState` / `src`
72
+ properties. Media elements also emit `loadedmetadata`, `play`, `pause`,
73
+ `ended`, `timeupdate` (every 250 ms) and `error`, so the usual
74
+ `addEventListener` pattern works.
75
+ - **No built-in controls.** `<video controls>` is deliberately *not*
76
+ implemented: the element always renders the raw frame, and the program draws
77
+ its own chrome (a progress bar, buttons) with ordinary elements and CSS. See
78
+ `examples/gui/media.html` (`examples/gui/media.ts`) for the pattern.
79
+ - **Lifecycle.** Players are owned per window (`XtGuiMediaEntry`, keyed by
80
+ node). `xt_gui_media_sync` drops entries whose node is gone, reopens on a
81
+ `src` change, and opens on first use; `xt_gui_media_shutdown` runs on window
82
+ close/quit/`loadHTML`.
83
+
84
+ Still to do for media: `<video controls>`, separate audio/video clock drift
85
+ correction, hardware decode, streaming protocols (only `file` is enabled),
86
+ subtitles, `playbackRate` and per-frame `requestVideoFrameCallback`.
87
+
@@ -0,0 +1,113 @@
1
+ # Paint, GPU and text
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Paint and display list (M4a/M4b)
7
+
8
+ `runtime/ext_gui/paint.{h,cpp}` walks the layout tree in painter's order and
9
+ emits a backend-agnostic `DisplayList`: a painter-order `ops` list of
10
+ **rectangles** (backgrounds and four solid border edges, with `border-radius`),
11
+ **text runs** (each carrying its text, colour and resolved `FontSpec`) and
12
+ **images**, backed by parallel payload arrays. Each op also carries the active
13
+ clip rectangle (the padding box of the nearest `overflow: hidden` ancestor), so
14
+ `overflow` clipping and `z-index` ordering survive into the renderer.
15
+ `DisplayList::dump()` feeds `paintList()`/`paintCount()`.
16
+
17
+ `opacity` is honoured only at its extremes: a box whose computed `opacity` is
18
+ `0` (and its whole subtree) paints nothing, and anything above `0` paints fully
19
+ opaque. A shape carries its colour's alpha only, so there is no compositing of a
20
+ partly transparent box — the `opacity` transitions the CSS parser accepts are
21
+ therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
22
+ the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
23
+
24
+ `runtime/ext_gui/renderer.{h,cpp}` turns that list into batched vertex buffers
25
+ per window (shapes, glyph quads and images) and merges consecutive ops that
26
+ share a pipeline, clip and texture into **draw runs**. Each run is drawn with
27
+ its own GPU scissor rectangle (scaled to device pixels), so painter order and
28
+ `overflow` clipping are applied per op through the SDL_GPU graphics pipelines:
29
+
30
+ - The shared pipelines are created lazily from the device's supported shader
31
+ format. Each of SDL_GPU's three backends consumes a different binary and none
32
+ of them compiles GLSL/HLSL at runtime, so the engine ships all three and
33
+ `selectShader` (`renderer_shaders.h`) picks the one the device accepts:
34
+ **MSL** for Metal (compiled by SDL from the embedded source),
35
+ **SPIR-V** for Vulkan (`runtime/ext_gui/spirv/*.{vert,frag}`, compiled by
36
+ `glslc`), and **DXIL** for Direct3D 12 (`shaders.hlsl`, compiled by `dxc`).
37
+ `scripts/build-gui-shaders.mjs` regenerates the embedded blobs and the result
38
+ is committed, so nothing extra is needed to build the engine.
39
+ The descriptor bindings differ per format — SPIR-V follows what
40
+ `SDL_gpu_vulkan.c` builds (uniforms `set=1, binding=0`, samplers
41
+ `set=2, binding=0`, textures `set=2, binding=1`) — and glslc names every entry
42
+ point `main` where MSL/DXIL keep the descriptive names.
43
+ Note that SDL 3.2.10's Direct3D 12 backend cannot create a graphics pipeline
44
+ whose shaders declare a uniform buffer (it fails with `E_INVALIDARG`); the
45
+ engine passes the viewport that way, so the DXIL path is built but not usable
46
+ until that is resolved upstream.
47
+ - A **rounded-rectangle distance field** in the shape fragment shader gives
48
+ antialiased fills; the vertex carries `position`, `local`, `half extents`,
49
+ `radius` and colour, and a viewport-size push constant does the projection.
50
+ Alpha blending is enabled.
51
+ - The text pipeline samples a **single shared grayscale glyph atlas**
52
+ (`R8_UNORM`, 2048², shelf-packed, LINEAR filtering) and draws each glyph as a
53
+ textured quad (`position`, `uv`, colour), modulating alpha by the coverage.
54
+ - Geometry is uploaded only when the document or viewport changes
55
+ (`geometry.dirty`), so steady-state frames are bind-and-draw.
56
+
57
+ Each op also carries the `Matrix2D` of the box it came from
58
+ (`runtime/ext_gui/transform.h`, `PaintRect::transform` and friends). It is
59
+ applied while the vertex buffer is built, not in the shader: `pushRect` maps its
60
+ four corners through the matrix (the distance-field `local`/half-extent/radius
61
+ fields stay in the *box* frame, so a rounded corner stays round after a
62
+ rotation), and `pushQuad`/`pushGlyph` map the glyph or image quad the same way.
63
+ That kept the MSL/SPIR-V/DXIL shaders and the pipelines untouched — a transform
64
+ costs a few multiplies per vertex and nothing per pixel. A clip rectangle of a
65
+ transformed box is the bounding box of the transformed padding box, because the
66
+ GPU scissor is axis aligned. Text is the one case that needs more than
67
+ multiplying corners: a scaling transform would magnify an atlas raster, so the
68
+ atlas is asked for glyphs at the device scale **times the matrix's
69
+ magnification** (clamped to 0.5–4x, since the atlas is finite) and the quad is
70
+ divided back into logical units, exactly the way HiDPI scaling already worked.
71
+
72
+ The window background (`setBackground`) is the render-pass clear colour.
73
+
74
+ Still to do in M4: gradients.
75
+
76
+ ## Text stack (M4b)
77
+
78
+ `runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
79
+ (metrics + eventual rasterisation). Both are built statically and linked into
80
+ `gui.a` by `scripts/build-gui.ts`.
81
+
82
+ - Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
83
+ DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
84
+ `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
85
+ Only regular upright faces are used for now; weight/italic selection is a
86
+ later refinement.
87
+ - The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
88
+ Windows, PingFang/STHeiti on macOS, Noto Sans CJK/WenQuanYi on Linux) before
89
+ the Latin-only ones. Setting `XT_GUI_FONT` to a Latin-only face gives its
90
+ Latin design instead (at the cost of CJK coverage).
91
+ - **Codepoints the primary face lacks fall back to another face.** The engine
92
+ keeps a small fallback chain — the platform's symbol face (Segoe UI Symbol,
93
+ Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the other family
94
+ class — and resolves each codepoint to the first face in it that has the
95
+ glyph. The run is shaped in segments, one per face, so a mixed run (Latin
96
+ plus an arrow, box-drawing or geometric shape, or Latin inside CJK) draws
97
+ every glyph instead of dropping the missing ones as `.notdef`. This is what
98
+ makes a character such as `▮`/`▯` (U+25AE/U+25AF, used by the pelican-bike
99
+ speed bar) visible even when the CJK primary has no geometric shapes. Each
100
+ shaped glyph carries its face (`ShapedGlyph::font`) so the renderer rasterises
101
+ it from the right atlas entry; `xt_text_scale` maps a face to the display's
102
+ scale factor.
103
+ - `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
104
+ ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
105
+ descent / normal line height. When no font file can be found the module falls
106
+ back to a deterministic per-byte approximation, so layout still works.
107
+ - `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
108
+ an 8-bit bitmap; the renderer packs those into the atlas. On HiDPI displays
109
+ glyphs are rasterised at `font_size * SDL_GetWindowPixelDensity` while quads
110
+ are positioned in logical pixels, so text stays crisp.
111
+ - Layout uses these real metrics for text widths, line breaking and
112
+ `line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
113
+
@@ -0,0 +1,88 @@
1
+ # Runtime integration and memory
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Event loop integration
7
+
8
+ The runtime change (already landed):
9
+
10
+ - `int xt_loop_poll(int timeout_ms)` — one reactor iteration; `0` polls without
11
+ blocking, `< 0` blocks.
12
+ - `void xt_loop_set_main(xt_main_loop_fn fn)` — a host may take over the main
13
+ loop. `xt_run_event_loop()` delegates to it; the generated `main` is unchanged.
14
+ - `xt_loop_set_main(NULL)` restores the default `select(2)` loop.
15
+
16
+ The GUI engine can either register its own loop through `xt_loop_set_main`, or
17
+ expose an explicit `run()`; **M2 uses the explicit `run()`** so the window is a
18
+ plain native call the TypeScript program controls. Each tick it:
19
+
20
+ 1. pumps SDL window/input events for every window,
21
+ 2. builds/uploads the display list when it changed and renders every open
22
+ window (clear pass + shape geometry + text geometry),
23
+ 3. calls `xt_loop_poll(0)` and `xt_drain_microtasks()` so sockets/timers and
24
+ `await` continuations keep making progress,
25
+ 4. repeats until all windows close or `quit()` is called.
26
+
27
+ The `xt_loop_set_main` hook remains available for hosts that want to own the
28
+ loop themselves.
29
+
30
+ This keeps network I/O, timers and `await` working inside a GUI program.
31
+
32
+ ## Native ↔ TS bridge
33
+
34
+ - **TS → engine**: direct `xt_gui_*` calls / window methods.
35
+ - **Engine → TS**: `xt_call_with_this(fn, thisValue, argc, argv)` with function
36
+ values captured from TS (e.g. event handlers registered with `win.on(...)`).
37
+ - **Future page→native RPC**: not needed while there is no page JS; native TS is
38
+ the controller. If a declarative layer is added later, it will use the same
39
+ `xt_call_*` entry points.
40
+
41
+ ## Memory & GC
42
+
43
+ The engine owns most of a GUI program's memory outside the runtime heap: the DOM
44
+ tree, the detached-node pool, the element-handle tables, the layout box tree and
45
+ the display list are C++, so the mark-sweep collector cannot see them. `stats()`
46
+ (see [Diagnostics](./api.md#diagnostics)) reports the parts that can grow:
47
+
48
+ | Counter | Expected in a steady state |
49
+ | --- | --- |
50
+ | `treeNodes` | constant — the authored document |
51
+ | `detachedRoots`, `detachedNodes` | 0, or constant for nodes the script removed itself and still holds |
52
+ | `handles`, `handleSlots` | constant — one per element the script still refers to |
53
+ | `listeners` | constant — one entry per element with listeners |
54
+ | `heapBytes`, `heapAllocations` | sawtooth around the collector's threshold |
55
+
56
+ Everything else is per-frame scratch (boxes, display list, vertex staging) that is
57
+ freed before the next frame. A counter that climbs with the frame count is the
58
+ leak signature to look for first; `tests/e2e/gui-retention.test.ts` asserts that
59
+ the usual mutation patterns leave all of them unchanged.
60
+
61
+ Two engine details matter when reading those numbers:
62
+
63
+ - **What releases nodes.** `innerHTML`/`inner`/`textContent`/`replaceChild`
64
+ release the subtrees they drop; `removeChild`/`remove` keep them (see [Node
65
+ lifetime](./dom.md#node-lifetime)). A game that rebuilds a sprite with
66
+ `innerHTML` therefore runs in constant memory.
67
+ - **What can still accumulate.** A node created with `createElement` and never
68
+ attached stays in the pool, because the collector has no weak references and
69
+ the engine cannot tell that the script dropped its handle. The same applies to
70
+ a node removed and then forgotten by the script. Both are bounded by the number
71
+ of *live* handles, but a program that creates elements per frame without
72
+ attaching them grows the pool with the frame count.
73
+ - **Vector images are cached by pixel size.** A bitmap `<img>` decodes once and
74
+ is cached by path, but an SVG is rasterised at the drawn device size, so the
75
+ decoded-pixel and GPU-texture caches hold one entry per distinct size. That is
76
+ constant for a static layout (the usual case) and small for a handful of
77
+ sizes, but a program that reflows one SVG to a new integer size every frame
78
+ accumulates entries; prefer animating a bitmap, or keep the vector to a fixed
79
+ size.
80
+
81
+ The collector behind `heapBytes` is a non-moving mark-sweep (`runtime/xt_alloc.c`).
82
+ Its trigger follows the live set: after every collection the threshold becomes
83
+ twice the surviving heap (floor 1 MiB), so a small live set means small, frequent
84
+ collections and a bounded resident set. `XT_GC_THRESHOLD=<bytes>` pins the
85
+ threshold instead, and `XT_GC_TRACE=1` prints one line per collection (heap and
86
+ live-allocation counts before and after, the next trigger, and the live set by
87
+ object kind) to stderr — that is how the DOM leak was traced to the node pool.
88
+
@@ -241,6 +241,9 @@ frees, so cycles are harmless).
241
241
  - mutation: `appendChild` / `insertBefore` / `removeChild` / `replaceChild` /
242
242
  `remove` / `cloneNode(deep?)`.
243
243
  - geometry/focus: `getBoundingClientRect`, `focus`, `blur`, `click`.
244
+ - media (`<video>` / `<audio>`): `play()` / `pause()` / `load()` and the
245
+ `src`, `paused`, `ended`, `duration`, `currentTime`, `volume`, `muted`,
246
+ `loop`, `autoplay`, `videoWidth`, `videoHeight`, `readyState` properties.
244
247
  - events: `addEventListener(type, fn, optionsOrCapture?)`,
245
248
  `removeEventListener`, `dispatchEvent`.
246
249
 
@@ -342,9 +345,10 @@ legacy `target` field over the handle.
342
345
 
343
346
  ## Docs to update
344
347
 
345
- - `doc/gui.md` — Non-goals ("Executing page `<script>`" → "Executing *runtime*
346
- page scripts; compile-time scripts are AOT-compiled"), locked decision #1,
347
- TS-facing API, milestones, progress log.
348
+ - `doc/gui.md` (overview) and `doc/gui/dom.md` — Non-goals ("Executing page
349
+ `<script>`" → "Executing *runtime* page scripts; compile-time scripts are
350
+ AOT-compiled"), locked decision #1, the AOT script section, milestones and
351
+ the progress log. The TS-facing API is in `doc/gui/api.md`.
348
352
  - `doc/implemented.md` / `doc/unimplemented.md` — move the new DOM/script
349
353
  capabilities across.
350
354
  - `README.md` — mention the DOM API + AOT scripts.