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.
- package/README.md +59 -4
- package/README.zh-CN.md +52 -2
- package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
- package/dist/tests/e2e/gui-advanced.test.js +261 -0
- package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
- package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
- package/dist/tests/e2e/gui-canvas.test.js +233 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-example.test.d.ts +1 -1
- package/dist/tests/e2e/gui-example.test.js +2 -2
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +5 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +73 -0
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.d.ts +8 -0
- package/dist/tests/e2e/gui-media.test.js +119 -0
- package/dist/tests/e2e/gui-media.test.js.map +1 -0
- package/dist/tests/e2e/gui-paint.test.js +40 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/doc/ai/extensions.md +16 -3
- package/doc/ai/zh-CN/extensions.md +10 -3
- package/doc/gui/animation.md +46 -0
- package/doc/gui/api.md +92 -0
- package/doc/gui/build.md +91 -0
- package/doc/gui/canvas.md +98 -0
- package/doc/gui/dom.md +123 -0
- package/doc/gui/input.md +22 -0
- package/doc/gui/known-issues.md +66 -0
- package/doc/gui/layout.md +72 -0
- package/doc/gui/media.md +87 -0
- package/doc/gui/rendering.md +98 -0
- package/doc/gui/runtime.md +88 -0
- package/doc/gui-scripts.md +7 -3
- package/doc/gui.md +80 -612
- package/doc/implemented.md +1 -0
- package/doc/zh-CN/gui/animation.md +46 -0
- package/doc/zh-CN/gui/api.md +88 -0
- package/doc/zh-CN/gui/build.md +84 -0
- package/doc/zh-CN/gui/canvas.md +80 -0
- package/doc/zh-CN/gui/dom.md +114 -0
- package/doc/zh-CN/gui/input.md +21 -0
- package/doc/zh-CN/gui/known-issues.md +50 -0
- package/doc/zh-CN/gui/layout.md +60 -0
- package/doc/zh-CN/gui/media.md +71 -0
- package/doc/zh-CN/gui/rendering.md +81 -0
- package/doc/zh-CN/gui/runtime.md +76 -0
- package/doc/zh-CN/gui-scripts.md +6 -2
- package/doc/zh-CN/gui.md +64 -532
- package/doc/zh-CN/implemented.md +1 -0
- package/doc/zh-CN/post/xbintsc-build-pelican.md +2 -2
- package/llms.txt +15 -2
- package/package.json +1 -1
- package/runtime/ext_gui/canvas.cpp +580 -0
- package/runtime/ext_gui/canvas.h +170 -0
- package/runtime/ext_gui/canvas_internal.h +59 -0
- package/runtime/ext_gui/canvas_raster.cpp +254 -0
- package/runtime/ext_gui/document.cpp +24 -1
- package/runtime/ext_gui/document.h +11 -0
- package/runtime/ext_gui/dom_api.cpp +3 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
- package/runtime/ext_gui/dom_api_internal.h +73 -0
- package/runtime/ext_gui/dom_api_media.cpp +274 -0
- package/runtime/ext_gui/dom_api_proto.cpp +80 -0
- package/runtime/ext_gui/gui.cpp +6 -0
- package/runtime/ext_gui/gui_canvas.cpp +95 -0
- package/runtime/ext_gui/gui_engine.h +49 -0
- package/runtime/ext_gui/gui_events.cpp +21 -5
- package/runtime/ext_gui/gui_media.cpp +193 -0
- package/runtime/ext_gui/image.cpp +31 -12
- package/runtime/ext_gui/image.h +20 -7
- package/runtime/ext_gui/layout.cpp +52 -5
- package/runtime/ext_gui/layout.h +33 -1
- package/runtime/ext_gui/layout_flex.cpp +263 -99
- package/runtime/ext_gui/layout_flow.cpp +85 -9
- package/runtime/ext_gui/layout_flow_internal.h +84 -5
- package/runtime/ext_gui/layout_inline.cpp +54 -37
- package/runtime/ext_gui/media.cpp +514 -0
- package/runtime/ext_gui/media.h +104 -0
- package/runtime/ext_gui/nanosvg.h +3278 -0
- package/runtime/ext_gui/nanosvgrast.h +1472 -0
- package/runtime/ext_gui/paint.cpp +123 -38
- package/runtime/ext_gui/paint.h +36 -6
- package/runtime/ext_gui/renderer.cpp +113 -10
- package/runtime/ext_gui/renderer.h +49 -9
- package/runtime/ext_gui/renderer_image.cpp +203 -77
- package/runtime/ext_gui/renderer_internal.h +6 -8
- package/runtime/ext_gui/renderer_text.cpp +11 -26
- package/runtime/ext_gui/style.h +11 -0
- package/runtime/ext_gui/style_print.cpp +11 -0
- package/runtime/ext_gui/style_ua.cpp +7 -0
- package/runtime/ext_gui/style_values.cpp +40 -1
- package/runtime/ext_gui/svg.cpp +182 -0
- package/runtime/ext_gui/svg.h +46 -0
- package/runtime/ext_gui/text.cpp +156 -25
- package/runtime/ext_gui/text.h +11 -2
- package/runtime/ext_gui/window.cpp +4 -1
- package/scripts/build-gui.ts +148 -6
- package/scripts/check-file-length.ts +5 -1
- package/scripts/install-gui-deps.sh +28 -0
package/doc/ai/extensions.md
CHANGED
|
@@ -67,10 +67,23 @@ import { createWindow, run } from "gui";
|
|
|
67
67
|
|
|
68
68
|
It is a self-contained GPU-accelerated renderer (own HTML parser, CSS cascade,
|
|
69
69
|
layout and compositor) — not a system WebView. It ships as a per-platform
|
|
70
|
-
prebuilt `gui.a`/`gui.lib` because it is C++.
|
|
70
|
+
prebuilt `gui.a`/`gui.lib` because it is C++. `<img>`, `<video>` and `<audio>`
|
|
71
|
+
really decode: a minimal static FFmpeg is bundled into the archive, and the DOM
|
|
72
|
+
media API (`play`/`pause`/`currentTime`/`volume`/…) plus the
|
|
73
|
+
`loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` events let a
|
|
74
|
+
program drive playback. There are no built-in controls — draw your own chrome
|
|
75
|
+
with elements and CSS (see `examples/gui/media.ts`). Video frames and audio PCM
|
|
76
|
+
are decoded on the main thread. `<canvas>` is supported too: `getContext("2d")`
|
|
77
|
+
returns a context whose fills, paths, text and `drawImage` are rasterised by a
|
|
78
|
+
self-written CPU rasteriser, and element handles expose `canvas` drawing plus
|
|
79
|
+
`width`/`height`. If that archive is missing for
|
|
71
80
|
your platform, the build fails with an actionable message; design and roadmap
|
|
72
|
-
live in [../gui.md](../gui.md)
|
|
73
|
-
[
|
|
81
|
+
live in [../gui.md](../gui.md) (overview; the engine is documented by function
|
|
82
|
+
under [`doc/gui/`](../gui/)), and the scripting surface in
|
|
83
|
+
[../gui-scripts.md](../gui-scripts.md). Media playback is the one part that
|
|
84
|
+
Windows does not get: the Windows `gui.a` is built without FFmpeg and media
|
|
85
|
+
slots are inert. See [../gui/canvas.md](../gui/canvas.md) for the canvas API and
|
|
86
|
+
its limits.
|
|
74
87
|
|
|
75
88
|
## Third-party npm packages
|
|
76
89
|
|
|
@@ -64,9 +64,16 @@ import { createWindow, run } from "gui";
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
它是自带渲染引擎的 GPU 加速实现(自己的 HTML 解析器、CSS 级联、布局与合成器),不是
|
|
67
|
-
系统 WebView
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
系统 WebView。`<img>`、`<video>`、`<audio>` 都是真正解码:归档内打入一个最小化的
|
|
68
|
+
静态 FFmpeg,DOM 媒体 API(`play`/`pause`/`currentTime`/`volume`/…)与
|
|
69
|
+
`loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` 事件可让程序驱动播放。
|
|
70
|
+
没有内置控件——用元素和 CSS 自绘控件(见 `examples/gui/media.ts`);视频帧与音频 PCM
|
|
71
|
+
都在主线程解码。也支持 `<canvas>`:`getContext("2d")` 返回的上下文,其填充、路径、
|
|
72
|
+
文本与 `drawImage` 由自研 CPU 光栅器处理,元素句柄还暴露 `width`/`height`。因为主体是 C++,它以各平台预编译的 `gui.a`/`gui.lib` 形式分发。如果该
|
|
73
|
+
平台的归档不存在,构建会以明确的提示失败;设计与路线图见 [../../zh-CN/gui.md](../../zh-CN/gui.md)(总览;引擎按功能拆分在 [`doc/zh-CN/gui/`](../../zh-CN/gui/) 下),
|
|
74
|
+
脚本接口见 [../../zh-CN/gui-scripts.md](../../zh-CN/gui-scripts.md)。媒体播放是
|
|
75
|
+
Windows 唯一不支持的:Windows 的 `gui.a` 不构建 FFmpeg,媒体槽位为空操作。Canvas API
|
|
76
|
+
及其限制见 [../../zh-CN/gui/canvas.md](../../zh-CN/gui/canvas.md)。
|
|
70
77
|
|
|
71
78
|
## 第三方 npm 包
|
|
72
79
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Animation: frames and transitions
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Animation frames (M10)
|
|
7
|
+
|
|
8
|
+
`requestAnimationFrame` runs a callback once on the next frame; the callback
|
|
9
|
+
receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
|
|
10
|
+
repainted in the same frame. Re-queue from inside the callback to animate:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const win = createWindow({ title: "anim", width: 320, height: 240 });
|
|
14
|
+
let n = 0;
|
|
15
|
+
const tick = (t: number) => {
|
|
16
|
+
win.document.getElementById("label").textContent = String(n++);
|
|
17
|
+
if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
|
|
18
|
+
};
|
|
19
|
+
win.on("ready", () => win.requestAnimationFrame(tick));
|
|
20
|
+
win.loadHTML("<div id='label'>0</div>");
|
|
21
|
+
run();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CSS transitions (M6b)
|
|
25
|
+
|
|
26
|
+
`XtDocument` runs CSS transitions between the *target* computed styles (the
|
|
27
|
+
cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
|
|
28
|
+
for layout and paint. When a state change alters a transitioned property, a
|
|
29
|
+
running transition is recorded and re-applied every frame until it finishes;
|
|
30
|
+
the engine advances the clock with real frame deltas (`XtDocument::advance`),
|
|
31
|
+
and `win.advance(ms)` lets tests step it deterministically.
|
|
32
|
+
|
|
33
|
+
- Supported properties: `background-color`, `color`, `border-color`,
|
|
34
|
+
`border-radius`; `transition: all` covers them. Structural/layout-affecting
|
|
35
|
+
properties are not animated yet (that would relayout every frame).
|
|
36
|
+
- Both the `transition` shorthand and the `transition-property` / `-duration` /
|
|
37
|
+
`-delay` / `-timing-function` longhands are parsed; time values accept `s` and
|
|
38
|
+
`ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
|
|
39
|
+
`ease-in-out` (`ease` is a smoothstep approximation).
|
|
40
|
+
- Retargeting mid-flight starts a new transition from the current interpolated
|
|
41
|
+
value, so reversing a hover animates smoothly from wherever it was.
|
|
42
|
+
- `computedStyle()` reports the displayed (interpolated) value, so transitions
|
|
43
|
+
are directly observable in tests.
|
|
44
|
+
|
|
45
|
+
Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
|
|
46
|
+
|
package/doc/gui/api.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# GUI API
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## TS-facing API
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createWindow, run, quit } from "gui";
|
|
10
|
+
|
|
11
|
+
const win = createWindow({ title: "Demo", width: 900, height: 600 });
|
|
12
|
+
win.setBackground("#14161c");
|
|
13
|
+
win.loadHTML(INDEX_HTML); // parses HTML/CSS and computes styles
|
|
14
|
+
win.on("ready", () => console.log("first frame presented"));
|
|
15
|
+
win.on("close", () => console.log("window closed"));
|
|
16
|
+
|
|
17
|
+
run(); // drives the main loop until all windows close
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Methods implemented on a window handle: `setTitle` / `setSize` / `loadHTML` /
|
|
21
|
+
`getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`, plus `driver()`,
|
|
22
|
+
which reports the SDL_GPU backend in use (`"vulkan"`, `"direct3d12"`, `"metal"`,
|
|
23
|
+
… — SDL picks the best one the device supports). The Web spellings are available
|
|
24
|
+
too: `addEventListener` / `removeEventListener` are aliases of `on` / `off` (their
|
|
25
|
+
`options` argument is accepted and ignored) and the `closed` getter mirrors
|
|
26
|
+
`isOpen()`, so a document written against the browser API runs unchanged.
|
|
27
|
+
Events emitted:
|
|
28
|
+
`ready` (after the first presented frame), `load`, `close`, `resize` and the
|
|
29
|
+
input events `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`,
|
|
30
|
+
`keyup`.
|
|
31
|
+
|
|
32
|
+
On Windows a GUI program is linked as a **Windows-subsystem** executable, so
|
|
33
|
+
launching it does not flash a console window. That also means stdout/stderr go
|
|
34
|
+
nowhere when it is started from Explorer; set `app.console: true` in
|
|
35
|
+
`xbintsc.config.json` to keep the console subsystem (useful while debugging).
|
|
36
|
+
|
|
37
|
+
Input handlers receive a single payload object (lifecycle handlers receive none):
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
|
|
41
|
+
win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
|
|
42
|
+
win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Field | Events | Meaning |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `x`, `y` / `clientX`, `clientY` | pointer, wheel | viewport-relative logical pixels |
|
|
48
|
+
| `button` | pointer | `0` left, `1` middle, `2` right (`-1` for move) |
|
|
49
|
+
| `clicks` | pointer | click count reported by the OS |
|
|
50
|
+
| `deltaX`, `deltaY` | wheel | scroll amount (`deltaY` positive = down) |
|
|
51
|
+
| `key`, `code` | keyboard | key name and physical scancode name |
|
|
52
|
+
| `repeat`, `ctrl`, `shift`, `alt`, `meta` | keyboard | modifiers |
|
|
53
|
+
| `target` | pointer, wheel | deepest element under the point as a CSS-like descriptor (`div#main.card`), or absent |
|
|
54
|
+
|
|
55
|
+
## Diagnostics
|
|
56
|
+
|
|
57
|
+
So the HTML/CSS/paint pipeline is testable without a GPU, a window handle
|
|
58
|
+
exposes read-only hooks:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
|
|
62
|
+
win.queryCount(selector) // number of matching elements
|
|
63
|
+
win.getBoundingClientRect(selector) // { x, y, width, height } (border box)
|
|
64
|
+
win.documentTree() // serialized DOM (debugging)
|
|
65
|
+
win.layoutTree() // serialized layout boxes (debugging)
|
|
66
|
+
win.paintCount() // number of shapes in the display list
|
|
67
|
+
win.paintList() // serialized display list (debugging)
|
|
68
|
+
win.measureText(text, fontSize?, family?) // shaped advance width in pixels
|
|
69
|
+
win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
|
|
70
|
+
win.hitTest(x, y) // deepest element descriptor, or ""
|
|
71
|
+
win.sendEvent(type, options?) // synthesise input (testing)
|
|
72
|
+
win.advance(ms) // step the CSS transition clock (testing)
|
|
73
|
+
win.stats() // engine/runtime counters (memory debugging)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`win.stats()` returns `{ heapBytes, heapAllocations, treeNodes, detachedRoots,`
|
|
77
|
+
`detachedNodes, handles, handleSlots, listeners }`: the live runtime heap, the
|
|
78
|
+
size of the DOM tree, the detached-node pool, and the element-handle tables. In a
|
|
79
|
+
steady state every one of them is constant — a climbing `detachedNodes` or
|
|
80
|
+
`handles` means the program removes nodes faster than it releases them (see
|
|
81
|
+
[Node lifetime](./dom.md#node-lifetime)).
|
|
82
|
+
|
|
83
|
+
They are used by `tests/e2e/gui-*.test.ts` to assert parsing, selector matching,
|
|
84
|
+
specificity, inheritance, `!important` and layout geometry. They will stay useful
|
|
85
|
+
afterwards for debugging.
|
|
86
|
+
|
|
87
|
+
## Multiple windows
|
|
88
|
+
|
|
89
|
+
Multiple windows fall out of the object model: `createWindow` returns a native
|
|
90
|
+
object handle; each handle owns its own `SDL_Window`/GPU surface and its own DOM
|
|
91
|
+
tree. `run()` drives one shared main loop that ticks every window and exits when
|
|
92
|
+
the last one closes.
|
package/doc/gui/build.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Packaging and build
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
- Sources live in `runtime/ext_gui/` (C/C++) plus vendored libs under `vendor/`
|
|
7
|
+
(gitignored; fetched on demand).
|
|
8
|
+
- `npm run gui` (`scripts/build-gui.ts`) fetches a pinned SDL3 (`SDL3_TAG`,
|
|
9
|
+
default `release-3.2.10`), builds a static SDL3, fetches and builds static
|
|
10
|
+
FreeType (`FREETYPE_VERSION`) and HarfBuzz (`HARFBUZZ_VERSION`), compiles the
|
|
11
|
+
engine and merges everything into `runtime/lib/<os>-<arch>/gui.a` (or
|
|
12
|
+
`gui.lib` with the MSVC ABI on Windows), same convention as `core.a`,
|
|
13
|
+
statically bundling all three. It also fetches, configures and builds a
|
|
14
|
+
minimal static **FFmpeg** (`FFMPEG_VERSION`, default `7.1`) with only the
|
|
15
|
+
demuxers/parsers/decoders the media elements need and merges those archives
|
|
16
|
+
into `gui.a`, defining `XT_GUI_WITH_FFMPEG` for the engine. The merge uses
|
|
17
|
+
`libtool` on macOS and `ar -M` (GNU/LLVM) elsewhere; CMake archives are
|
|
18
|
+
discovered under the build root or `Release/` for either generator style.
|
|
19
|
+
- `src/extensions/gui/index.ts` exposes the archive through `nativeObjects()`
|
|
20
|
+
(via `findRuntimeLibrary`, so `.a`/`.lib` both work) and the OS frameworks
|
|
21
|
+
through `linkerFlags()`.
|
|
22
|
+
- CI builds `gui.a` before assembling the release archive so it ships inside
|
|
23
|
+
`runtime/lib/<slug>/` (the release tarball copies the whole `runtime/` tree).
|
|
24
|
+
The archive is built and the example is run on Linux (under Xvfb, with the
|
|
25
|
+
lavapipe software Vulkan driver) and macOS; Windows is provisional.
|
|
26
|
+
- The existing cache fingerprint already hashes `nativeObjects()` contents, so
|
|
27
|
+
rebuilding `gui.a` invalidates cached binaries automatically.
|
|
28
|
+
- CMake build trees live *next to* their sources (`vendor/sdl-build`,
|
|
29
|
+
`vendor/freetype-build`, `vendor/harfbuzz-build`), never inside them: a
|
|
30
|
+
`CMakeCache.txt` records the machine it was configured on, and SDL's video
|
|
31
|
+
backends are `option()`s, so the first configure to run wins. A cached
|
|
32
|
+
`vendor/SDL` tree that also carried `build/` therefore restored one machine's
|
|
33
|
+
`SDL_X11`/`SDL_WAYLAND` choices into another's configure — SDL then aborts
|
|
34
|
+
with "SDL could not find X11 or Wayland development libraries" even though the
|
|
35
|
+
headers are installed. The CI cache is keyed per OS/arch and omits the build
|
|
36
|
+
trees; `npm run gui` also checks the X11/Wayland headers up front on
|
|
37
|
+
Debian/Ubuntu and reports the missing development packages by name (set
|
|
38
|
+
`xbintsc_SKIP_HEADER_CHECK=1` when they live under a prefix that check does
|
|
39
|
+
not know).
|
|
40
|
+
|
|
41
|
+
## Running the example
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm run runtime # rebuild core.a after a runtime change
|
|
45
|
+
npm run gui # build runtime/lib/<os>-<arch>/gui.a (fetches SDL3 once)
|
|
46
|
+
xbintsc run examples/gui/hello.ts --ext gui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Two examples live here: `hello.ts` (HTML/CSS + a reactive counter) and
|
|
50
|
+
[`pelican-bike/`](../../examples/gui/pelican-bike), a playable 2D game that uses
|
|
51
|
+
positioned layers, CSS sprites, `requestAnimationFrame` and keyboard input — see
|
|
52
|
+
its README for controls and the geometry report it prints.
|
|
53
|
+
|
|
54
|
+
`media.ts` (with `media.html`) demonstrates `<video>`/`<audio>`: it overlays a
|
|
55
|
+
CSS progress bar on the video and drives playback with `play()` / `pause()` /
|
|
56
|
+
`currentTime` / `volume`. Media `src` paths resolve relative to the process
|
|
57
|
+
working directory, so run it from the repository root:
|
|
58
|
+
`xbintsc run examples/gui/media.ts --ext gui`.
|
|
59
|
+
|
|
60
|
+
Set `XT_GUI_AUTOCLOSE_MS=<n>` to close all windows after `n` milliseconds,
|
|
61
|
+
which the e2e test (`tests/e2e/gui-*.test.ts`) uses to run headlessly.
|
|
62
|
+
|
|
63
|
+
Those suites are skipped on Windows by default (the archive is built there but
|
|
64
|
+
the run is not validated in CI). With a `gui.lib` and a working GPU, opt in with
|
|
65
|
+
`xbintsc_GUI_TESTS=1`:
|
|
66
|
+
|
|
67
|
+
```bat
|
|
68
|
+
set xbintsc_GUI_TESTS=1
|
|
69
|
+
npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
On a headless Linux box, install the SDL3 build headers and run under Xvfb with a
|
|
73
|
+
software Vulkan driver:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
sudo sh scripts/install-gui-deps.sh # clang, cmake, X11/Wayland headers, Vulkan, Xvfb
|
|
77
|
+
npm run runtime && npm run gui
|
|
78
|
+
xvfb-run -a --server-args="-screen 0 1280x720x24" \
|
|
79
|
+
npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`scripts/install-gui-deps.sh` holds the package list (`apt-get install -y clang
|
|
83
|
+
cmake libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev
|
|
84
|
+
libxinerama-dev libxfixes-dev libxkbcommon-dev libwayland-dev wayland-protocols
|
|
85
|
+
libdecor-0-dev libasound2-dev libpulse-dev libdbus-1-dev libudev-dev libdrm-dev
|
|
86
|
+
libgbm-dev libgl1-mesa-dev libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers
|
|
87
|
+
xvfb`) so that every Linux job which builds `gui.a` installs the same set: the
|
|
88
|
+
X11 backend is used by default, Wayland is enabled too but not yet exercised.
|
|
89
|
+
Install it before `npm run gui` anywhere the archive is built — SDL3 cannot
|
|
90
|
+
find a window backend without those headers.
|
|
91
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Canvas: `<canvas>` and the 2D context (M14)
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
`<canvas>` is a **replaced element** laid out like `<img>`: its intrinsic size
|
|
7
|
+
comes from the `width`/`height` attributes (default `300x150`, the CSS default
|
|
8
|
+
canvas size), an explicit CSS `width`/`height` wins, and the aspect ratio is
|
|
9
|
+
preserved when only one axis is constrained. Paint emits one `PaintImage` per
|
|
10
|
+
canvas, carrying a `canvas_id` instead of a `src`/`media_id`; the renderer
|
|
11
|
+
uploads the bitmap to an RGBA texture through the same image path as bitmaps and
|
|
12
|
+
video frames (`renderer_image.cpp`), so the GPU scales it into the CSS box.
|
|
13
|
+
|
|
14
|
+
The engine-side object is `xtgui::Canvas2D`
|
|
15
|
+
(`runtime/ext_gui/canvas.{h,cpp}`) — one RGBA8 bitmap plus the 2D drawing state.
|
|
16
|
+
The rasteriser lives in `canvas_raster.cpp`; the DOM bindings live in
|
|
17
|
+
`dom_api_canvas.cpp`, and the per-window lifetime in `gui_canvas.cpp`.
|
|
18
|
+
|
|
19
|
+
## Getting a context
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const canvas: any = document.getElementById("c");
|
|
23
|
+
const ctx = canvas.getContext("2d"); // or null for any other context kind
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`getContext("2d")` returns a context handle whose methods and accessors live on
|
|
27
|
+
a shared prototype. The handle is cached on the element, so every call returns
|
|
28
|
+
the **same** object and the drawing state (transform, styles, path) is per
|
|
29
|
+
canvas, not per call. `ctx.canvas` is the owning element handle.
|
|
30
|
+
|
|
31
|
+
## Drawing model
|
|
32
|
+
|
|
33
|
+
The rasteriser is deliberately small and self-written (no Skia/Cairo). It
|
|
34
|
+
anti-aliases with **4 vertical sub-scanlines** per pixel row and an **analytic
|
|
35
|
+
horizontal coverage** pass, and fills paths with the **non-zero winding rule**
|
|
36
|
+
(so `arc`-shaped holes work when wound the other way). An axis-aligned rectangle
|
|
37
|
+
(`b == 0 && c == 0` in the transform) takes a direct coverage fast path.
|
|
38
|
+
Blending is non-premultiplied source-over, scaled by `globalAlpha`.
|
|
39
|
+
|
|
40
|
+
Canvas pixels are **not** touched by the garbage collector: a canvas is owned by
|
|
41
|
+
the window, keyed by its DOM node (the same shape as the `<video>`/`<audio>`
|
|
42
|
+
decode table). It is released when the node is dropped (`loadHTML`, window
|
|
43
|
+
close/quit, or a mutation that discards the subtree), which also releases the
|
|
44
|
+
matching GPU texture.
|
|
45
|
+
|
|
46
|
+
## Supported API
|
|
47
|
+
|
|
48
|
+
State and transforms: `save` / `restore` (which restore the transform, line
|
|
49
|
+
width, alpha, font/text settings **and** the fill/stroke styles),
|
|
50
|
+
`translate`, `rotate`, `scale`, `transform(a,b,c,d,e,f)`, `setTransform`,
|
|
51
|
+
`resetTransform`.
|
|
52
|
+
|
|
53
|
+
Styles and settings: `fillStyle` / `strokeStyle` (CSS colors understood by the
|
|
54
|
+
cascade's color parser), `lineWidth`, `globalAlpha`, `font` (a CSS font
|
|
55
|
+
shorthand resolving to family, size and bold/italic — default
|
|
56
|
+
`10px sans-serif`), `textAlign` (`start`/`left`, `center`, `right`/`end`) and
|
|
57
|
+
`textBaseline` (`alphabetic`, `top`, `middle`, `bottom`, `hanging`,
|
|
58
|
+
`ideographic`). Reading a property returns the value last assigned; assigning an
|
|
59
|
+
unparseable color keeps the previous one.
|
|
60
|
+
|
|
61
|
+
Paths: `beginPath`, `closePath`, `moveTo`, `lineTo`, `rect`, `arc`,
|
|
62
|
+
`quadraticCurveTo`, `bezierCurveTo`, then `fill` (multi-loop, non-zero winding)
|
|
63
|
+
or `stroke`. Bezier and quadratic segments are flattened into line segments;
|
|
64
|
+
arcs are flattened too and each subpath is stroked as segment quads plus a disc
|
|
65
|
+
at each joint.
|
|
66
|
+
|
|
67
|
+
Rectangles: `clearRect`, `fillRect`, `strokeRect`.
|
|
68
|
+
|
|
69
|
+
Text: `measureText(text)` (returns `{ width }`), `fillText(text, x, y)` and
|
|
70
|
+
`strokeText(text, x, y)`. Text is shaped with HarfBuzz and rasterised with
|
|
71
|
+
FreeType (the same stack as HTML text) at the transform's approximate scale.
|
|
72
|
+
**Only translation and uniform scale are honoured** — a rotated or skewed
|
|
73
|
+
transform still places each glyph at its transformed baseline point but does not
|
|
74
|
+
rotate the glyph bitmaps. `strokeText` currently fills with the stroke style
|
|
75
|
+
rather than producing an outline.
|
|
76
|
+
|
|
77
|
+
Images: `drawImage(source, …)` in the 3-argument (`dx, dy`), 5-argument
|
|
78
|
+
(`dx, dy, dw, dh`) and 9-argument (`dx, dy, dw, dh, sx, sy, sw, sh`) forms.
|
|
79
|
+
`source` may be an `<img>` element (decoded through the image module) or another
|
|
80
|
+
`<canvas>` element (its current bitmap). Sampling is nearest-neighbour, so a
|
|
81
|
+
large upscale of a small source is blocky — fine for icons and pixel art, not a
|
|
82
|
+
photo pipeline.
|
|
83
|
+
|
|
84
|
+
Test hook: `getImageData(x, y, w, h)` copies a region to
|
|
85
|
+
`{ width, height, data }` where `data` is a row-major RGBA8 byte array.
|
|
86
|
+
Out-of-bounds pixels are transparent.
|
|
87
|
+
|
|
88
|
+
## Not implemented
|
|
89
|
+
|
|
90
|
+
Gradients and patterns, shadows, `clip()`, `globalCompositeOperation`, line
|
|
91
|
+
joins/caps/dashes/mitre, `setLineDash`, `createLinearGradient`/`createPattern`,
|
|
92
|
+
`putImageData`, `createImageData`, `isPointInPath`, `getTransform`,
|
|
93
|
+
`setTransform` with a `DOMMatrix`, `drawImage` with an image smoothing toggle,
|
|
94
|
+
`toDataURL`/`toBlob`, `<canvas>` as a CSS `background-image`, and offscreen
|
|
95
|
+
canvases (`OffscreenCanvas`, `transferControlToOffscreen`).
|
|
96
|
+
|
|
97
|
+
See also [Media](./media.md) for the image/video/audio decode stack that
|
|
98
|
+
`drawImage` and the texture upload share.
|
package/doc/gui/dom.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# DOM, HTML, CSS and AOT scripts
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there. The scripting surface has its own
|
|
5
|
+
> document, [gui-scripts.md](../gui-scripts.md).
|
|
6
|
+
|
|
7
|
+
## HTML/CSS subset (M3a)
|
|
8
|
+
|
|
9
|
+
**HTML parser** (`runtime/ext_gui/dom.{h,cpp}`): tags/attributes/text,
|
|
10
|
+
comments and doctype skipped, entity decoding (named + numeric/hex), void
|
|
11
|
+
elements, raw-text elements (`<style>`/`<script>`), and the common implicit
|
|
12
|
+
close rules (`li`, `dt`/`dd`, `option`, `p`, headings, table cells/rows).
|
|
13
|
+
|
|
14
|
+
**CSS parser** (`runtime/ext_gui/css.{h,cpp}`): comments and at-rules skipped
|
|
15
|
+
(for now), rules with multiple selectors, and declarations. Selectors: type,
|
|
16
|
+
`.class`, `#id`, attribute (`=`, `~=`, `|=`, `^=`, `$=`, `*=`), the four
|
|
17
|
+
combinators (descendant, child, adjacent and general sibling) and the
|
|
18
|
+
pseudo-classes `:first-child`, `:last-child`, `:only-child`, `:empty`,
|
|
19
|
+
`:root`, `:not(...)`, `:nth-child(an+b)`, `:disabled`, `:checked`, plus the
|
|
20
|
+
stateful `:hover` and `:focus` (see [Input](./input.md)). Values:
|
|
21
|
+
lengths (`px`, `%`, `em`, `rem`, `vw`, `vh`, `pt`, `pc`, `in`, `cm`, `mm`, `q`),
|
|
22
|
+
colors (hex, `rgb()`/`rgba()`, a named subset), numbers, keywords and shorthands
|
|
23
|
+
(`margin`/`padding`/`border`/`flex`).
|
|
24
|
+
|
|
25
|
+
**Cascade & computed style** (`runtime/ext_gui/style.{h,cpp}`): a small built-in
|
|
26
|
+
UA stylesheet, author rules sorted by `(!important, specificity, source order)`,
|
|
27
|
+
then inline `style=""` (highest specificity, but non-`!important` inline loses to
|
|
28
|
+
`!important`), plus CSS inheritance of the text properties. Relative lengths are
|
|
29
|
+
kept unresolved until layout, except `font-size` (resolved against the *parent*
|
|
30
|
+
font size) and `line-height`.
|
|
31
|
+
|
|
32
|
+
## Interactive DOM (M8)
|
|
33
|
+
|
|
34
|
+
`win.document` returns the document handle; element handles have stable identity
|
|
35
|
+
and read/write properties, attributes, traversal and geometry:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const doc = win.document;
|
|
39
|
+
const box = doc.querySelector("#box");
|
|
40
|
+
const inner = doc.querySelector("#inner");
|
|
41
|
+
inner.textContent = "hi"; // write
|
|
42
|
+
inner.classList.add("hot");
|
|
43
|
+
inner.style.setProperty("color", "#0f0");
|
|
44
|
+
inner.setAttribute("data-role", "lead");
|
|
45
|
+
console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
|
|
46
|
+
|
|
47
|
+
const created = doc.createElement("div");
|
|
48
|
+
created.textContent = "added";
|
|
49
|
+
box.appendChild(created);
|
|
50
|
+
box.removeChild(created);
|
|
51
|
+
|
|
52
|
+
inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
|
|
53
|
+
inner.click(); // synthesise a click at the element
|
|
54
|
+
console.log(box.offsetWidth, box.offsetHeight); // rounded border box
|
|
55
|
+
console.log(box.contains(inner)); // descendant test
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Mutations mark the document dirty; the engine restyles + relayouts lazily (before
|
|
59
|
+
the next read or frame). `inner = …`/`innerHTML = …` do **not** run scripts.
|
|
60
|
+
Element events support capture and bubble phases, `stopPropagation`, `once`, and
|
|
61
|
+
bubble up to `document`/`window`. The legacy `win.on(type, fn)` payload keeps its
|
|
62
|
+
**string** `e.target` (`div#id.class`); the element `Event.target` is a handle
|
|
63
|
+
whose descriptor matches the same string.
|
|
64
|
+
|
|
65
|
+
### Node lifetime
|
|
66
|
+
|
|
67
|
+
A mutation decides whether the nodes it removes can still be reached, and the
|
|
68
|
+
engine releases exactly those it cannot:
|
|
69
|
+
|
|
70
|
+
| Mutation | Removed nodes |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `innerHTML = …`, `inner = …`, `textContent = …`, `replaceChild` | **released** — the script has no way to refer to them again |
|
|
73
|
+
| `removeChild`, `remove` | **kept** in the document's detached pool — the DOM keeps a removed node alive and usable, so it can be re-attached |
|
|
74
|
+
|
|
75
|
+
Released nodes free their C++ subtree as well as their handle, so the counters
|
|
76
|
+
from `win.stats()` (`detachedNodes`, `handles`, `handleSlots`) return to their
|
|
77
|
+
previous values. That is what makes the usual game pattern — rewriting a sprite
|
|
78
|
+
with `innerHTML` every frame — run in constant memory; before, every replaced
|
|
79
|
+
subtree stayed pooled forever and a loop writing ten nodes per frame leaked
|
|
80
|
+
several megabytes per second.
|
|
81
|
+
|
|
82
|
+
The one case the engine cannot see is a node the script creates and then drops
|
|
83
|
+
without ever attaching it: `document.createElement("div")` parks the node in the
|
|
84
|
+
pool *and* keeps its handle reachable, because the mark-sweep collector has no
|
|
85
|
+
weak references, so the engine cannot tell that the script threw the handle
|
|
86
|
+
away. A loop that creates elements without attaching them therefore grows the
|
|
87
|
+
pool. Reuse the elements (or build the content with `innerHTML`, which releases
|
|
88
|
+
what it replaces) instead of creating fresh ones per frame.
|
|
89
|
+
|
|
90
|
+
## AOT scripts (M9)
|
|
91
|
+
|
|
92
|
+
Import an `.html` file that contains inline `<script lang="ts">` bodies; the
|
|
93
|
+
loader compiles each body into a native function and `win.loadHTML(page)` runs
|
|
94
|
+
them once the document is parsed (no JavaScript engine, no runtime `eval`):
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { createWindow, run } from "gui";
|
|
98
|
+
import page from "./page.html";
|
|
99
|
+
|
|
100
|
+
const win = createWindow({ title: "counter", width: 320, height: 240 });
|
|
101
|
+
win.loadHTML(page);
|
|
102
|
+
run();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<button id="b">0</button>
|
|
107
|
+
<script lang="ts">
|
|
108
|
+
const b = document.getElementById("b");
|
|
109
|
+
let n = 0;
|
|
110
|
+
b.addEventListener("click", () => { b.textContent = String(++n); });
|
|
111
|
+
</script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The two parameters (`window`, `document`) are ordinary function arguments, so
|
|
115
|
+
script locals need no global-object machinery. External scripts work too:
|
|
116
|
+
`<script src="./counter.ts">` is read at compile time, its imports are hoisted
|
|
117
|
+
(rewritten to resolve from the HTML file) and its body is wrapped the same way —
|
|
118
|
+
so a script module can `import` helpers and still see `document`. Scripts are
|
|
119
|
+
**compile-time assets**: HTML created at runtime (`innerHTML`, fetched over the
|
|
120
|
+
network) never executes, and `src` URLs (`https://…`, `data:…`) are ignored.
|
|
121
|
+
Everything runs in document order after parsing (effectively deferred). See
|
|
122
|
+
`doc/gui-scripts.md` for the full design.
|
|
123
|
+
|
package/doc/gui/input.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Input and events (M5)
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there. The public event API is in
|
|
5
|
+
> [api.md](./api.md).
|
|
6
|
+
|
|
7
|
+
`LayoutTree::hitTest` returns the deepest box containing a point (probing later
|
|
8
|
+
siblings first so the topmost element wins), and `xt_dom_describe` turns the
|
|
9
|
+
element into the `div#id.class` descriptor carried by event payloads.
|
|
10
|
+
|
|
11
|
+
- SDL pointer/wheel/key events are routed to the owning window, hit tested, and
|
|
12
|
+
delivered to the handlers registered with `on`. `mousedown` also moves focus.
|
|
13
|
+
- `:hover` matches the hovered element **and its ancestors** (so hovering a child
|
|
14
|
+
lights up its parents); `:focus` matches the focused element. When either
|
|
15
|
+
changes, `XtDocument::setHover`/`setFocus` recompute styles and layout and mark
|
|
16
|
+
the window's geometry dirty, so the change is painted on the next frame.
|
|
17
|
+
- `win.hitTest(x, y)` and `win.sendEvent(type, options)` expose hit testing and
|
|
18
|
+
synthetic input so the whole path is testable headlessly (the e2e suite drives
|
|
19
|
+
clicks, wheels and keys without a real mouse).
|
|
20
|
+
|
|
21
|
+
Still to do for full input: text selection, drag, IME and clipboard.
|
|
22
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Known issues and open questions
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Known issues
|
|
7
|
+
|
|
8
|
+
- **Intermittent crash at auto-close.** `XT_GUI_AUTOCLOSE_MS` can end a run
|
|
9
|
+
with a native access violation in roughly two runs out of five; a clean exit
|
|
10
|
+
looks identical otherwise. Windows reports it as the NTSTATUS exit code
|
|
11
|
+
`0xC0000005`; on Linux/macOS the process dies with a signal — an access
|
|
12
|
+
violation is `SIGSEGV`, so `spawnSync` reports `status: null` plus a
|
|
13
|
+
`signal`. It only reproduces with a *running animation-frame loop* (a static
|
|
14
|
+
document closes fine) and it is **not** the game's own code: no exception is
|
|
15
|
+
reported, the crash usually lands inside the first second regardless of the
|
|
16
|
+
deadline, and raising `XT_GC_THRESHOLD` hides it, so it looks like a
|
|
17
|
+
GC/teardown race. Interactive runs (no auto-close) are stable for minutes.
|
|
18
|
+
A leading cause was that the engine cached `xt_value`s in C++ containers
|
|
19
|
+
the mark-sweep collector cannot see — the AOT `<script>` registry, element
|
|
20
|
+
handles, element listeners and the queued/currently-running
|
|
21
|
+
`requestAnimationFrame` callbacks. They are now marked by GC root providers
|
|
22
|
+
(`xt_gui_script_gc_scan` and `xt_gui_gc_scan_roots`), which removes that
|
|
23
|
+
class of use-after-free. Since the M12 node-lifetime work it has not
|
|
24
|
+
reproduced at all: 44 auto-close runs of `examples/gui/pelican-bike` (deadlines
|
|
25
|
+
800 ms–4 s, with the default, a 1 MiB and a 64 MiB collector threshold, where
|
|
26
|
+
the old report said roughly two in five) all exited cleanly, which points at
|
|
27
|
+
the same root cause — a detached-node pool and handle table that grew with the
|
|
28
|
+
frame count while the collector's threshold ratcheted up. It stays listed
|
|
29
|
+
because the archive is built per platform and the failure is timing-sensitive;
|
|
30
|
+
`tests/e2e/gui-example.test.ts` still treats either crash spelling as an
|
|
31
|
+
acceptable outcome, and the behavioural assertions live in
|
|
32
|
+
`tests/e2e/gui-example-browser.test.ts` (no GPU, deterministic).
|
|
33
|
+
- **`display: none -> flex` restyle.** A hidden element being shown by a
|
|
34
|
+
*class change* on a flex container has also been observed to crash the same
|
|
35
|
+
way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
|
|
36
|
+
instead.
|
|
37
|
+
|
|
38
|
+
## Open questions
|
|
39
|
+
|
|
40
|
+
- Whether Linux ships X11, Wayland, or both in the first cut. (Decision:
|
|
41
|
+
X11 first, Wayland later.)
|
|
42
|
+
- **SVG:** SVG *files* are supported through the image loader
|
|
43
|
+
(`<img src="logo.svg">`): `runtime/ext_gui/svg.cpp` wraps the vendored
|
|
44
|
+
**NanoSVG** parser/rasteriser and `image.{h,cpp}` treats an SVG as a vector
|
|
45
|
+
image, rasterised at the drawn pixel size. Inline `<svg>` **trees** in a
|
|
46
|
+
document are still not supported: the HTML parser knows elements and
|
|
47
|
+
attributes, the layout engine knows boxes, and paint knows
|
|
48
|
+
rectangles/text/images, so `<svg>` subtrees would need a whole second
|
|
49
|
+
geometry+paint path. Vector art therefore has to arrive as an SVG/PNG file or
|
|
50
|
+
be drawn from boxes, as `examples/gui/pelican-bike` does.
|
|
51
|
+
- Windows: an MSVC-compatible `gui.lib` (COFF objects + `ar -M`/`llvm-ar`) is
|
|
52
|
+
produced by `scripts/build-gui.ts`, but it has not been validated in CI yet,
|
|
53
|
+
so the Windows step is provisional (the job stays green) and `package` skips
|
|
54
|
+
it. It also needs an SDL3 build with the D3D12/DXIL backend (DXIL requires
|
|
55
|
+
`dxc`).
|
|
56
|
+
- **Media on Windows:** the media decoders (FFmpeg) are only built and merged
|
|
57
|
+
into the POSIX `gui.a`; the Windows `gui.lib` is produced without them, so
|
|
58
|
+
`<video>`/`<audio>` are inert there (failures fire `error`). Enabling it needs
|
|
59
|
+
an MSVC-compatible FFmpeg build and is deferred until the Windows GUI path
|
|
60
|
+
itself is validated. `<video controls>` is likewise deferred — the intended
|
|
61
|
+
pattern is TS + a CSS overlay, as in `examples/gui/media.ts`.
|
|
62
|
+
- **D3D12 shaders:** the SPIR-V (Vulkan) and DXIL (Direct3D 12) blobs are built
|
|
63
|
+
and embedded, so the non-Metal path no longer skips geometry. DXIL remains
|
|
64
|
+
unusable on **SDL 3.2.10**: its D3D12 backend rejects any graphics pipeline
|
|
65
|
+
whose shaders declare a uniform buffer, and the engine passes the viewport that
|
|
66
|
+
way. Re-check after an SDL upgrade.
|