xbintsc 0.3.69 → 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 (99) 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/llms.txt +15 -2
  52. package/package.json +1 -1
  53. package/runtime/ext_gui/canvas.cpp +580 -0
  54. package/runtime/ext_gui/canvas.h +170 -0
  55. package/runtime/ext_gui/canvas_internal.h +59 -0
  56. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  57. package/runtime/ext_gui/document.cpp +24 -1
  58. package/runtime/ext_gui/document.h +11 -0
  59. package/runtime/ext_gui/dom_api.cpp +3 -0
  60. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  61. package/runtime/ext_gui/dom_api_internal.h +73 -0
  62. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  63. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  64. package/runtime/ext_gui/gui.cpp +6 -0
  65. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  66. package/runtime/ext_gui/gui_engine.h +49 -0
  67. package/runtime/ext_gui/gui_events.cpp +21 -5
  68. package/runtime/ext_gui/gui_media.cpp +193 -0
  69. package/runtime/ext_gui/image.cpp +31 -12
  70. package/runtime/ext_gui/image.h +20 -7
  71. package/runtime/ext_gui/layout.cpp +52 -5
  72. package/runtime/ext_gui/layout.h +33 -1
  73. package/runtime/ext_gui/layout_flex.cpp +263 -99
  74. package/runtime/ext_gui/layout_flow.cpp +85 -9
  75. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  76. package/runtime/ext_gui/layout_inline.cpp +54 -37
  77. package/runtime/ext_gui/media.cpp +514 -0
  78. package/runtime/ext_gui/media.h +104 -0
  79. package/runtime/ext_gui/nanosvg.h +3278 -0
  80. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  81. package/runtime/ext_gui/paint.cpp +123 -38
  82. package/runtime/ext_gui/paint.h +36 -6
  83. package/runtime/ext_gui/renderer.cpp +113 -10
  84. package/runtime/ext_gui/renderer.h +49 -9
  85. package/runtime/ext_gui/renderer_image.cpp +203 -77
  86. package/runtime/ext_gui/renderer_internal.h +6 -8
  87. package/runtime/ext_gui/renderer_text.cpp +11 -26
  88. package/runtime/ext_gui/style.h +11 -0
  89. package/runtime/ext_gui/style_print.cpp +11 -0
  90. package/runtime/ext_gui/style_ua.cpp +7 -0
  91. package/runtime/ext_gui/style_values.cpp +40 -1
  92. package/runtime/ext_gui/svg.cpp +182 -0
  93. package/runtime/ext_gui/svg.h +46 -0
  94. package/runtime/ext_gui/text.cpp +156 -25
  95. package/runtime/ext_gui/text.h +11 -2
  96. package/runtime/ext_gui/window.cpp +4 -1
  97. package/scripts/build-gui.ts +148 -6
  98. package/scripts/check-file-length.ts +5 -1
  99. package/scripts/install-gui-deps.sh +28 -0
@@ -0,0 +1,72 @@
1
+ # Layout
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Layout engine (M3)
7
+
8
+ `runtime/ext_gui/layout.{h,cpp}` turns the styled DOM into a `LayoutBox` tree
9
+ with absolute (viewport-relative) geometry:
10
+
11
+ - **Block flow** — block-level children stack vertically; `display: none`
12
+ generates no box; `width: auto` fills the containing block, `height: auto`
13
+ wraps the content. The box model (margin/padding/border) is resolved,
14
+ including percentages against the containing block width. Adjoining vertical
15
+ margins between siblings collapse to `max(positive) + min(negative)`; a
16
+ parent's bottom margin does not collapse with its last child's (see the
17
+ limitations below).
18
+ - **Inline flow** — consecutive inline-level children form an anonymous inline
19
+ formatting context with greedy, word-based line breaking, `text-align` and
20
+ `line-height`. Inline elements get the union of their descendants' geometry;
21
+ `display: inline-block` is laid out atomically with a shrink-to-fit width.
22
+ Each line shrinks to the band left free by any floats (`float: left|right`)
23
+ that overlap it, so later lines wrap around them.
24
+ Each text fragment remembers the run it covers, so paint can shape it. Text is
25
+ measured with the HarfBuzz/FreeType stack (see [Text stack](./rendering.md#text-stack-m4b)).
26
+ - **Flexbox** — `row`/`column` (and the `-reverse` variants) with `gap`,
27
+ `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and `align-items`
28
+ (including `stretch` when the cross size is definite). `flex-wrap: wrap`
29
+ breaks items into several lines, `align-content`
30
+ (`stretch`/`flex-start`/`flex-end`/`center`/`space-between`/`space-around`)
31
+ distributes the lines on the cross axis and `wrap-reverse` flips that axis.
32
+ Item margins are part of the item's outer size on both axes, so a negative
33
+ `margin-top` pulls an item up instead of being ignored. Whitespace-only text
34
+ between items (source indentation) does not become a flex item.
35
+ - **Positioning** — `position: relative` offsets a box (and its subtree) by
36
+ `top`/`right`/`bottom`/`left` without changing the space it reserved;
37
+ `position: absolute` (and `fixed`, which resolves against the viewport) takes
38
+ the box out of flow and places it against the padding box of the nearest
39
+ positioned ancestor — the viewport when there is none. An `auto` inset keeps
40
+ the static position on that axis, an `auto` width is shrink-to-fit unless both
41
+ `left` and `right` are set (then it fills the space between them), and
42
+ percentages resolve against the containing block. Stacking is approximated by
43
+ sorting each parent's children by `z-index` (negative → `auto` → positive)
44
+ before painting; this is a single level, not a full stacking context, so a
45
+ child cannot escape its parent's level.
46
+ Note the consequence of that shrink-to-fit rule: an absolutely positioned box
47
+ with `width: auto` and no explicit size gets **no** available width for its own
48
+ inline content (its preferred width comes from its text or its non-absolute
49
+ children only), so inline runs inside it are laid out at zero available width
50
+ and each one wraps onto its own line. Give such a box an explicit `width`, or
51
+ place its children with `left`/`width` (as
52
+ `examples/gui/pelican-bike` does for its sprite runs), instead of relying on
53
+ line flow inside it.
54
+ - **Floats and clearance** — `float: left|right` takes a box out of flow,
55
+ shrink-to-fits its width, places it against the current line and lets later
56
+ inline lines wrap around it; `clear: left|right|both` pushes a following block
57
+ below the relevant floats, and a block that contains only floats grows to
58
+ enclose them.
59
+ - **`overflow` clipping** — a box with `overflow: hidden` (or `scroll`/`auto`)
60
+ clips its subtree to its padding box, intersected with any ancestor clip; the
61
+ renderer applies the result with a GPU scissor rectangle. There is no
62
+ scrolling yet, so `scroll`/`auto` clip like `hidden`.
63
+
64
+ Not yet implemented: parent/child and empty-block margin collapsing, anonymous
65
+ flex items from non-whitespace text, full `z-index` stacking contexts, and
66
+ `float` affecting block children (only inline content wraps; a block must
67
+ `clear`).
68
+
69
+ **Document** (`runtime/ext_gui/document.{h,cpp}`): owns the DOM tree, gathers
70
+ `<style>` text into one stylesheet, computes styles and layout for a viewport and
71
+ offers `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`.
72
+
@@ -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,98 @@
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
+ The window background (`setBackground`) is the render-pass clear colour.
58
+
59
+ Still to do in M4: gradients.
60
+
61
+ ## Text stack (M4b)
62
+
63
+ `runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
64
+ (metrics + eventual rasterisation). Both are built statically and linked into
65
+ `gui.a` by `scripts/build-gui.ts`.
66
+
67
+ - Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
68
+ DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
69
+ `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
70
+ Only regular upright faces are used for now; weight/italic selection is a
71
+ later refinement.
72
+ - The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
73
+ Windows, PingFang/STHeiti on macOS, Noto Sans CJK/WenQuanYi on Linux) before
74
+ the Latin-only ones. Setting `XT_GUI_FONT` to a Latin-only face gives its
75
+ Latin design instead (at the cost of CJK coverage).
76
+ - **Codepoints the primary face lacks fall back to another face.** The engine
77
+ keeps a small fallback chain — the platform's symbol face (Segoe UI Symbol,
78
+ Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the other family
79
+ class — and resolves each codepoint to the first face in it that has the
80
+ glyph. The run is shaped in segments, one per face, so a mixed run (Latin
81
+ plus an arrow, box-drawing or geometric shape, or Latin inside CJK) draws
82
+ every glyph instead of dropping the missing ones as `.notdef`. This is what
83
+ makes a character such as `▮`/`▯` (U+25AE/U+25AF, used by the pelican-bike
84
+ speed bar) visible even when the CJK primary has no geometric shapes. Each
85
+ shaped glyph carries its face (`ShapedGlyph::font`) so the renderer rasterises
86
+ it from the right atlas entry; `xt_text_scale` maps a face to the display's
87
+ scale factor.
88
+ - `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
89
+ ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
90
+ descent / normal line height. When no font file can be found the module falls
91
+ back to a deterministic per-byte approximation, so layout still works.
92
+ - `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
93
+ an 8-bit bitmap; the renderer packs those into the atlas. On HiDPI displays
94
+ glyphs are rasterised at `font_size * SDL_GetWindowPixelDensity` while quads
95
+ are positioned in logical pixels, so text stays crisp.
96
+ - Layout uses these real metrics for text widths, line breaking and
97
+ `line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
98
+
@@ -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.