From 801a6be31a7908070aa290f9366740956de46415 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Wed, 14 Oct 2026 22:13:16 -0700 Subject: [PATCH 01/14] docs: PRD-614 MystralNative v0.1.6 upstream catch-up plan --- ...-mystralnative-upstream-catch-up-v0.1.6.md | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md new file mode 100644 index 0000000000..6f0897bcfa --- /dev/null +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -0,0 +1,107 @@ +# PRD-614 — Catch the runtime-native fork up with MystralNative v0.1.6 + +**Status:** NOT STARTED +**Priority:** P2 — the open Wayland and zero-length `writeBuffer` boxes are planned parity work; no release depends on them. +**Complexity:** 2 (LOW) — about 7 implementation files, no new module; risk override: none. +**Owner:** João +**Depends on:** None + +## Context + +`packages/runtime-native/` is a maintained fork of +[MystralNative](https://github.com/mystralengine/mystralnative). `packages/runtime-native/NOTICE` +records the upstream baseline `841fe379` (v0.1.5). Upstream `main` is now `bafab09` (v0.1.6 plus one +merged PR). That range has 15 commits and changes 17 files (+325/−98). + +Triage of each upstream change against the fork (2026-10-14): + +| Upstream commit(s) | Change | Fork state | Disposition | +| --- | --- | --- | --- | +| `6eeb33b`, `4e01a6e`, `a5b93f1` | Wayland WebGPU surface: `createSurfaceWithWLDisplay`, Wayland `_Compat` descriptor typedefs, `runtime.cpp` tries Wayland before X11 | `src/runtime.cpp:681` fails with "Wayland not yet supported"; `src/platform/window.cpp:233` and `src/cli/main.cpp:1130` force SDL to X11 | **Port** (Phase 1). Keep X11 as the default. | +| `9bec9d7` (part) | `queue.writeBuffer` with zero-length data returns instead of throwing | `src/webgpu/bindings.cpp:1687` throws `writeBuffer: invalid data` on `dataSize == 0` | **Port** (Phase 2). The WebGPU spec makes a zero-size write a valid no-op. | +| `9bec9d7` (part) | MSAA `resolveTarget` on color attachments, and surface tracking through the resolve target | Already in the fork: `src/webgpu/bindings_commands.cpp:1150`, `src/webgpu/bindings_frame_stream.cpp:382` | Skip — present. | +| `9bec9d7` (part) | `externalTexture` bind group layout entry | Absent. three 0.185 sets it only for XR textures (`WebGPUBackend.setXRRenderTargetTextures`), and the fork has no `importExternalTexture` | Skip — see Decisions. | +| `9bec9d7` (part) | Debug `std::cout` per buffer binding in `createBindGroupLayout` | — | Skip — log noise in a hot path. | +| `0d87767` | wgpu-native: replace `wgpuInstanceProcessEvents` (an `unimplemented!()` abort in older wgpu-native) with `wgpuDevicePoll`; libuv-less `DebugServer` stub | Pinned wgpu-native v25.0.2.2 implements `wgpuInstanceProcessEvents` (`src/lib.rs:2690`, `poll_all_devices`). The fork gates `debug_server.cpp` behind `TN_ENABLE_DEBUG_SERVER` (`CMakeLists.txt:1672`) | Skip — not applicable. | +| `4e01a6e` (part) | `#include ` in `http_client.h` | Already present | Skip — present. | +| `aa2c18e`, `de75cf7`, `0b2b225`, `53e96a3`, `8f748eb`, `fcd2e95` | linux-arm64 build (QuickJS + wgpu), Skia `arm64` lib subdir, quiche `0.24.6-4`, Canvas 2D on arm64 | Fork has no linux-arm64 row in `build-matrix.json` | Not in this PRD — see Blocked on. | +| `02c86dc`, `21611f0`, `fd2d126`, CI/release workflows, `install.sh` | `.gitignore`, README, version bump, upstream CI | Fork has its own `.gitignore` (`third_party/` ignored), CI and release | Skip — fork owns these. | + +## Solution + +Port the two behaviour changes that the fork lacks, then move the provenance baseline so the next +catch-up starts from `bafab09`. + +```mermaid +flowchart LR + A[SDL window] -->|SDL_VIDEODRIVER=wayland| B[wl_display + wl_surface] + A -->|default x11| C[X11 Display + Window] + B --> D[Context::createSurfaceWithWLDisplay] + C --> E[Context::createSurfaceWithX11Display] + D --> F[createSurfaceWithDescriptor] + E --> F +``` + +- **Wayland.** Take upstream's split of `createSurfaceWithDisplay` into + `createSurfaceWithX11Display` and `createSurfaceWithWLDisplay` over one + `createSurfaceWithDescriptor`. `runtime.cpp` reads SDL's Wayland handles when SDL runs the Wayland + driver, else X11. Keep the X11 hint in `window.cpp` and the X11 override for the web overlay in + `main.cpp`: the overlay and the key forwarding use X11 only. Result: an explicit + `SDL_VIDEODRIVER=wayland` now presents frames instead of failing. Dawn's prebuilt + `libwebgpu_dawn.a` already carries the Wayland surface symbols. +- **Zero-length `writeBuffer`.** Return `undefined` when `dataSize == 0` before the null-pointer + check, in the direct binding and in the frame-op stream decoder, to match the browser. +- **Provenance.** Update `NOTICE` to the new upstream baseline. This PRD keeps the per-commit + disposition table above; no separate record. + +Risks: the Wayland path changes the shared surface function that the X11 path also uses, so the +default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS +(`src/runtime-scripts/frame-op-stream.js`), so the zero-length case must not trip the +`truncated writeBuffer record` check. + +## Acceptance Criteria + +- [ ] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — Evidence: pending. +- [ ] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — Evidence: pending. +- [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), red before Phase 2 — Evidence: pending. + +## Blocked on + +- A headless Wayland compositor (`weston`) on this machine or the Debian laptop for AC-1. Neither `weston`, `cage` nor `sway` is installed here — unblocked by João installing one, or by approving an install. +- Whether linux-arm64 becomes a supported native target. If yes, a separate PRD ports `aa2c18e`, `de75cf7`, `0b2b225`, `53e96a3` and the Skia `arm64` lib subdir, with its own arm64 lane — unblocked by João's decision. +- Whether Wayland becomes the default SDL driver when no web overlay runs. This PRD keeps X11 as the default — unblocked by João's decision. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +| --- | --- | --- | --- | +| Wayland WebGPU surface | `mystral` CLI → `Runtime` surface setup (`src/runtime.cpp:655`) → `Context::createSurfaceWithWLDisplay` | `createSurfaceWithDisplay(…, PLATFORM_XLIB)` is deleted; the X11 path delegates to `createSurfaceWithX11Display` | AC-1, AC-2 | +| Zero-length `writeBuffer` | Game JS → `GPUQueue.writeBuffer` binding (`src/webgpu/bindings.cpp:1687`) and frame-op stream decoder (`src/webgpu/bindings_frame_stream.cpp:334`) | Throw becomes no-op | AC-3 | + +## Decisions + +- 2026-10-14 (agent, triage): skip upstream's `externalTexture` layout entry. three 0.185 sets `externalTexture` only for WebXR textures, and the fork has no `importExternalTexture`, so the entry has no consumer. Revisit when a game needs `GPUExternalTexture`. +- 2026-10-14 (agent, triage): skip the wgpu-native `wgpuInstanceProcessEvents` replacement. The pinned wgpu-native v25.0.2.2 implements that function, so the abort that upstream fixed cannot occur. + +## Execution Phases + +#### Phase 1: Wayland surface +**Status:** NOT STARTED +**Files:** `include/mystral/webgpu/context.h`, `src/webgpu/context.cpp`, `include/mystral/webgpu_compat.h`, `src/runtime.cpp` (all under `packages/runtime-native/`) +**Implementation:** Port `6eeb33b` and `4e01a6e` onto the fork's versions of these files. Delete `createSurfaceWithDisplay`. Keep the X11 default hints. +- [ ] Native desktop build compiles with the Wayland descriptor. proof: `pnpm native:build` +- [ ] AC-2 default X11 run. proof: `pnpm native:verify:desktop` +- [ ] AC-1 Wayland run. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` + +#### Phase 2: Zero-length `writeBuffer` +**Status:** NOT STARTED +**Files:** `src/webgpu/bindings.cpp`, `src/webgpu/bindings_frame_stream.cpp`, `conformance/registry.json` and its new case +**Implementation:** Add the conformance case first and see it fail on native. Then return `undefined` for `dataSize == 0` in both paths. +- [ ] AC-3 direct binding path, red then green. proof: `pnpm parity` new case +- [ ] AC-3 frame-op stream path, red then green. proof: `pnpm parity` new case with the frame-op stream on + +#### Phase 3: Provenance +**Status:** NOT STARTED +**Files:** `packages/runtime-native/NOTICE` +**Implementation:** Set `Upstream provenance baseline:` to `bafab09d599bd55aeb7433ba7131c3f320334c99` and name v0.1.6. +- [ ] NOTICE names the new baseline and the doc checks pass. proof: `pnpm check:docs` From 9eb486631de0642211ff5f6fe4407625a2b25132 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Wed, 14 Oct 2026 22:46:30 -0700 Subject: [PATCH 02/14] docs(PRD-614): preserve explicit arm64 acceptance scope --- ...-mystralnative-upstream-catch-up-v0.1.6.md | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 6f0897bcfa..c634736536 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -1,8 +1,8 @@ # PRD-614 — Catch the runtime-native fork up with MystralNative v0.1.6 -**Status:** NOT STARTED -**Priority:** P2 — the open Wayland and zero-length `writeBuffer` boxes are planned parity work; no release depends on them. -**Complexity:** 2 (LOW) — about 7 implementation files, no new module; risk override: none. +**Status:** IN PROGRESS +**Priority:** P1 — unfinished Wayland, zero-length `writeBuffer` and linux-arm64 Canvas 2D proofs gate the requested native-platform catch-up. +**Complexity:** 4 → MEDIUM — estimated 6–10 implementation files (+2), independent linux-arm64 build/artifact boundary (+2), no new module; risk override: none. Compatibility risk: shared surface changes must preserve default X11; arm64 dependency versions and library layouts must remain compatible with the QuickJS/wgpu host and Canvas 2D. **Owner:** João **Depends on:** None @@ -24,12 +24,13 @@ Triage of each upstream change against the fork (2026-10-14): | `9bec9d7` (part) | Debug `std::cout` per buffer binding in `createBindGroupLayout` | — | Skip — log noise in a hot path. | | `0d87767` | wgpu-native: replace `wgpuInstanceProcessEvents` (an `unimplemented!()` abort in older wgpu-native) with `wgpuDevicePoll`; libuv-less `DebugServer` stub | Pinned wgpu-native v25.0.2.2 implements `wgpuInstanceProcessEvents` (`src/lib.rs:2690`, `poll_all_devices`). The fork gates `debug_server.cpp` behind `TN_ENABLE_DEBUG_SERVER` (`CMakeLists.txt:1672`) | Skip — not applicable. | | `4e01a6e` (part) | `#include ` in `http_client.h` | Already present | Skip — present. | -| `aa2c18e`, `de75cf7`, `0b2b225`, `53e96a3`, `8f748eb`, `fcd2e95` | linux-arm64 build (QuickJS + wgpu), Skia `arm64` lib subdir, quiche `0.24.6-4`, Canvas 2D on arm64 | Fork has no linux-arm64 row in `build-matrix.json` | Not in this PRD — see Blocked on. | +| `aa2c18e`, `de75cf7`, `0b2b225`, `53e96a3`, `8f748eb`, `fcd2e95` | linux-arm64 build (QuickJS + wgpu), Skia `arm64` lib subdir, quiche `0.24.6-4`, Canvas 2D on arm64 | Existing `native-platforms.yml` has a linux-arm64 starter row; upstream dependency and Canvas 2D compatibility remain unproved | **Port/adapt and prove** (Phase 3): arm64 Skia, quiche `0.24.6-4`, SWC `swc-41`, libwebp aarch64, Canvas 2D on. | | `02c86dc`, `21611f0`, `fd2d126`, CI/release workflows, `install.sh` | `.gitignore`, README, version bump, upstream CI | Fork has its own `.gitignore` (`third_party/` ignored), CI and release | Skip — fork owns these. | ## Solution -Port the two behaviour changes that the fork lacks, then move the provenance baseline so the next +Port the two behaviour changes and complete the linux-arm64 dependency/Canvas 2D catch-up. +Only after AC-1 through AC-4 have green proofs, move the provenance baseline so the next catch-up starts from `bafab09`. ```mermaid @@ -51,7 +52,8 @@ flowchart LR `libwebgpu_dawn.a` already carries the Wayland surface symbols. - **Zero-length `writeBuffer`.** Return `undefined` when `dataSize == 0` before the null-pointer check, in the direct binding and in the frame-op stream decoder, to match the browser. -- **Provenance.** Update `NOTICE` to the new upstream baseline. This PRD keeps the per-commit +- **linux-arm64.** Reuse the existing `native-platforms.yml` linux-arm64 leg, adapting the native dependency/build paths for arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. Keep Canvas 2D enabled and prove the candidate through a manually dispatched run; a skipped leg does not count. +- **Provenance.** After every required runtime/platform proof is green, update `NOTICE` to the full upstream baseline `bafab09d599bd55aeb7433ba7131c3f320334c99`. This PRD keeps the per-commit disposition table above; no separate record. Risks: the Wayland path changes the shared surface function that the X11 path also uses, so the @@ -63,13 +65,12 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS - [ ] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — Evidence: pending. - [ ] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — Evidence: pending. -- [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), red before Phase 2 — Evidence: pending. +- [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), RED then GREEN on both the direct binding and frame-op stream paths — Evidence: pending. +- [ ] AC-4 [shared]: The dispatched `native-platforms.yml` linux-arm64 leg is GREEN with Canvas 2D enabled, arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. proof: `gh workflow run native-platforms.yml --ref ` followed by `gh run view ` confirming the linux-arm64 job and dependency/build logs on the candidate SHA — Evidence: pending. ## Blocked on -- A headless Wayland compositor (`weston`) on this machine or the Debian laptop for AC-1. Neither `weston`, `cage` nor `sway` is installed here — unblocked by João installing one, or by approving an install. -- Whether linux-arm64 becomes a supported native target. If yes, a separate PRD ports `aa2c18e`, `de75cf7`, `0b2b225`, `53e96a3` and the Skia `arm64` lib subdir, with its own arm64 lane — unblocked by João's decision. -- Whether Wayland becomes the default SDL driver when no web overlay runs. This PRD keeps X11 as the default — unblocked by João's decision. +- AC-1 currently lacks `weston` on this machine; no `weston`, `cage` or `sway` is installed. Unblock with a user-space compositor extraction (no sudo or host-wide install), or an available Debian laptop compositor. This is a technical prerequisite, not a human approval or platform-scope decision. ## Integration Ledger @@ -77,9 +78,12 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS | --- | --- | --- | --- | | Wayland WebGPU surface | `mystral` CLI → `Runtime` surface setup (`src/runtime.cpp:655`) → `Context::createSurfaceWithWLDisplay` | `createSurfaceWithDisplay(…, PLATFORM_XLIB)` is deleted; the X11 path delegates to `createSurfaceWithX11Display` | AC-1, AC-2 | | Zero-length `writeBuffer` | Game JS → `GPUQueue.writeBuffer` binding (`src/webgpu/bindings.cpp:1687`) and frame-op stream decoder (`src/webgpu/bindings_frame_stream.cpp:334`) | Throw becomes no-op | AC-3 | +| linux-arm64 Canvas 2D | `gh workflow run native-platforms.yml` → existing linux-arm64 starter leg → native dependency/build paths → Canvas 2D-enabled host | Adapt upstream dependency versions and arm64 library layouts; retain the existing workflow | AC-4 | ## Decisions +- 2026-10-14 (user pasted task, agent reconciliation): the explicit AC-4 supersedes the earlier linux-arm64 exclusion. Keep X11 as the default, include linux-arm64 in this PRD, and advance NOTICE only after all four acceptance criteria pass; no owner decision is pending. + - 2026-10-14 (agent, triage): skip upstream's `externalTexture` layout entry. three 0.185 sets `externalTexture` only for WebXR textures, and the fork has no `importExternalTexture`, so the entry has no consumer. Revisit when a game needs `GPUExternalTexture`. - 2026-10-14 (agent, triage): skip the wgpu-native `wgpuInstanceProcessEvents` replacement. The pinned wgpu-native v25.0.2.2 implements that function, so the abort that upstream fixed cannot occur. @@ -100,8 +104,10 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS - [ ] AC-3 direct binding path, red then green. proof: `pnpm parity` new case - [ ] AC-3 frame-op stream path, red then green. proof: `pnpm parity` new case with the frame-op stream on -#### Phase 3: Provenance +#### Phase 3: linux-arm64 proof, then provenance **Status:** NOT STARTED -**Files:** `packages/runtime-native/NOTICE` -**Implementation:** Set `Upstream provenance baseline:` to `bafab09d599bd55aeb7433ba7131c3f320334c99` and name v0.1.6. -- [ ] NOTICE names the new baseline and the doc checks pass. proof: `pnpm check:docs` +**Files:** Native dependency/build scripts, arm64 Skia/Canvas 2D configuration, existing `.github/workflows/native-platforms.yml` only if required, and `packages/runtime-native/NOTICE`. +**Implementation:** Adapt the upstream linux-arm64 changes to the existing leg; include arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64 with Canvas 2D enabled. Dispatch the workflow against the candidate branch and require its linux-arm64 leg to be GREEN. Only after AC-1 through AC-4 pass, set `Upstream provenance baseline:` to the full hash and name v0.1.6. +- [ ] linux-arm64 dependency/build configuration includes arm64 Skia, quiche `0.24.6-4`, SWC `swc-41`, libwebp aarch64 and Canvas 2D enabled. proof: dispatched `native-platforms.yml` linux-arm64 dependency/build logs on the candidate SHA +- [ ] AC-4 dispatched linux-arm64 leg is GREEN. proof: `gh workflow run native-platforms.yml --ref ` + `gh run view ` (successful linux-arm64 job on the candidate SHA) +- [ ] After AC-1 through AC-4 are green, NOTICE names `bafab09d599bd55aeb7433ba7131c3f320334c99` and v0.1.6. proof: `git diff -- packages/runtime-native/NOTICE` + `pnpm check:docs` From e4d010d44f997f9fe767c4d0de55ee4a443b51a1 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Wed, 14 Oct 2026 23:32:28 -0700 Subject: [PATCH 03/14] feat(PRD-614): port native catch-up paths and arm64 dependencies --- ...-mystralnative-upstream-catch-up-v0.1.6.md | 43 +++++--- packages/runtime-native/CMakeLists.txt | 26 ++++- .../linux-video-driver-routing.spec.ts | 38 ++++++++ .../runtime-native/conformance/registry.json | 13 +++ .../scenes/shared/queue-write-buffer-proof.js | 71 ++++++++++++++ .../scenes/shared/queue-write-buffer-zero.js | 11 +++ .../include/mystral/webgpu_compat.h | 8 ++ packages/runtime-native/native-deps.lock.json | 40 ++++++++ .../runtime-native/scripts/download-deps.mjs | 58 +++++++---- .../scripts/verify-desktop-core.mjs | 26 ++++- packages/runtime-native/scripts/xvfb.sh | 11 +++ packages/runtime-native/src/cli/main.cpp | 13 ++- .../runtime-native/src/platform/window.cpp | 12 ++- packages/runtime-native/src/runtime.cpp | 55 +++++++---- .../runtime-native/src/webgpu/bindings.cpp | 26 ++++- .../runtime-native/src/webgpu/context.cpp | 9 +- .../tests/dependency-provenance.test.mjs | 97 ++++++++++++++++++- .../tests/frame_op_stream_replay_test.cpp | 92 ++++++++++++++++++ 18 files changed, 588 insertions(+), 61 deletions(-) create mode 100644 packages/runtime-native/__tests__/linux-video-driver-routing.spec.ts create mode 100644 packages/runtime-native/conformance/scenes/shared/queue-write-buffer-proof.js create mode 100644 packages/runtime-native/conformance/scenes/shared/queue-write-buffer-zero.js diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index c634736536..7faf252743 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -43,13 +43,13 @@ flowchart LR E --> F ``` -- **Wayland.** Take upstream's split of `createSurfaceWithDisplay` into - `createSurfaceWithX11Display` and `createSurfaceWithWLDisplay` over one - `createSurfaceWithDescriptor`. `runtime.cpp` reads SDL's Wayland handles when SDL runs the Wayland - driver, else X11. Keep the X11 hint in `window.cpp` and the X11 override for the web overlay in - `main.cpp`: the overlay and the key forwarding use X11 only. Result: an explicit - `SDL_VIDEODRIVER=wayland` now presents frames instead of failing. Dawn's prebuilt - `libwebgpu_dawn.a` already carries the Wayland surface symbols. +- **Wayland.** Reuse `createSurfaceWithDisplay` with its existing `PLATFORM_WAYLAND` discriminator + instead of renaming the X11 API. Read SDL's Wayland handles when that driver is requested; keep + X11 as the default. Honor the task's legacy `SDL_VIDEODRIVER` spelling and SDL 3's + `SDL_VIDEO_DRIVER`, and log the observed SDL driver. The X11-only web overlay rejects an explicit + Wayland request rather than silently overriding it. Actual presentation remains unproved: + the current Dawn prebuilt rejects the Wayland surface descriptor; its supported variant and + the existing wgpu build profile are being investigated. - **Zero-length `writeBuffer`.** Return `undefined` when `dataSize == 0` before the null-pointer check, in the direct binding and in the frame-op stream decoder, to match the browser. - **linux-arm64.** Reuse the existing `native-platforms.yml` linux-arm64 leg, adapting the native dependency/build paths for arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. Keep Canvas 2D enabled and prove the candidate through a manually dispatched run; a skipped leg does not count. @@ -76,12 +76,17 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS | Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | | --- | --- | --- | --- | -| Wayland WebGPU surface | `mystral` CLI → `Runtime` surface setup (`src/runtime.cpp:655`) → `Context::createSurfaceWithWLDisplay` | `createSurfaceWithDisplay(…, PLATFORM_XLIB)` is deleted; the X11 path delegates to `createSurfaceWithX11Display` | AC-1, AC-2 | +| Wayland WebGPU surface | `mystral` CLI → `Runtime` surface setup (`src/runtime.cpp:655`) → `Context::createSurfaceWithDisplay` with `PLATFORM_WAYLAND` | Replace the unsupported Wayland branch; retain the existing X11 path and default | AC-1, AC-2 | | Zero-length `writeBuffer` | Game JS → `GPUQueue.writeBuffer` binding (`src/webgpu/bindings.cpp:1687`) and frame-op stream decoder (`src/webgpu/bindings_frame_stream.cpp:334`) | Throw becomes no-op | AC-3 | | linux-arm64 Canvas 2D | `gh workflow run native-platforms.yml` → existing linux-arm64 starter leg → native dependency/build paths → Canvas 2D-enabled host | Adapt upstream dependency versions and arm64 library layouts; retain the existing workflow | AC-4 | ## Decisions +- 2026-10-14 (AFK decision, Phase 2 detached sources): the detached-source check leaves the shared portable helper and lives in `runWriteBufferContract`'s async JS proof, because Android V8 has no `ArrayBuffer.prototype.transfer` and a `transfer`-requiring shared helper makes the parity row throw there. Desktop-focused proof keeps the fail-closed `transfer` requirement, so the case cannot pass silently on an engine that cannot detach a buffer. No public flag, new API or optional skip; no binding change. +- 2026-10-14 (AFK lane, Phase 2 detached sources): a detached `ArrayBuffer` or TypedArray reports `byteLength` 0 (ECMA-262 25.1.5.3), so the empty-source guard accepted it as a no-op. The guard now reads the intrinsic brand getter first and then probes: `new Uint8Array(data)` for ArrayBuffer, `%TypedArray%.prototype.values.call(data)` for TypedArray, both of which throw `TypeError` on a detached receiver (`quickjs.c:54120-54122`, V8 13.1 `v8-array-buffer.cc`). `ArrayBuffer.prototype.slice` is not used because it runs a species lookup on live receivers. The DataView entry keeps its getter, which already throws on detach. No `WasDetached()` virtual method and no constructor-name check, which user code can spoof. +- 2026-10-14 (AFK lane, Phase 1 source only): the user's exact command is `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop`, and SDL 3.2.30 reads `SDL_VIDEO_DRIVER`, not the legacy spelling (`SDL_hints.h:3326`). Five harness sites set `SDL_VIDEODRIVER=x11` and `verify-desktop-core.mjs` wraps its run in `scripts/xvfb.sh`, so the literal command would run X11 and pass falsely. Decision: `window.cpp` resolves the requested driver from `SDL_VIDEO_DRIVER` first, then the legacy `SDL_VIDEODRIVER`, and defaults to `x11`; `verify-desktop-core.mjs` runs without the Xvfb wrapper when a Wayland driver is explicitly requested, and logs the request; `scripts/xvfb.sh` names the X11 it provides instead of silently standing in for Wayland. `runtime.cpp` logs `SDL_GetCurrentVideoDriver()` as the receipt, so a Wayland claim comes from SDL itself. +- 2026-10-14 (AFK lane, Phase 1 source only): keep `createSurfaceWithDisplay(void*, void*, int)` and add the `PLATFORM_WAYLAND` case rather than taking upstream's split into `createSurfaceWithX11Display`/`createSurfaceWithWLDisplay`. The fork's header already documents the second parameter as "X11 Window or Wayland surface", the behaviour is identical, and a public rename would touch two more call sites for no gain. +- 2026-10-14 (AFK lane, Phase 1 source only): the web overlay attaches to an X11 surface only, and `main.cpp` forces it with `SDL_HINT_OVERRIDE`. An explicit Wayland request with the overlay is refused with a message naming `ui.renderer: "native-css"` or unsetting the variable, instead of being silently overridden to X11. The X11-scoped desktop gates keep running on the X display `scripts/xvfb.sh` provides, and say so. - 2026-10-14 (user pasted task, agent reconciliation): the explicit AC-4 supersedes the earlier linux-arm64 exclusion. Keep X11 as the default, include linux-arm64 in this PRD, and advance NOTICE only after all four acceptance criteria pass; no owner decision is pending. - 2026-10-14 (agent, triage): skip upstream's `externalTexture` layout entry. three 0.185 sets `externalTexture` only for WebXR textures, and the fork has no `importExternalTexture`, so the entry has no consumer. Revisit when a game needs `GPUExternalTexture`. @@ -90,20 +95,36 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Execution Phases #### Phase 1: Wayland surface -**Status:** NOT STARTED -**Files:** `include/mystral/webgpu/context.h`, `src/webgpu/context.cpp`, `include/mystral/webgpu_compat.h`, `src/runtime.cpp` (all under `packages/runtime-native/`) +**Status:** PARTIAL — candidate CLI compiles and two default-X11 core captures pass; actual Wayland selects the correct driver but the current Dawn library rejects its descriptor. Full candidate commands and Wayland presentation remain unproved. +**Files:** `include/mystral/webgpu/context.h`, `src/webgpu/context.cpp`, `include/mystral/webgpu_compat.h`, `src/runtime.cpp`, `src/platform/window.cpp`, `src/cli/main.cpp`, `scripts/verify-desktop-core.mjs`, `scripts/xvfb.sh`, `__tests__/linux-video-driver-routing.spec.ts` (all under `packages/runtime-native/`) **Implementation:** Port `6eeb33b` and `4e01a6e` onto the fork's versions of these files. Delete `createSurfaceWithDisplay`. Keep the X11 default hints. - [ ] Native desktop build compiles with the Wayland descriptor. proof: `pnpm native:build` - [ ] AC-2 default X11 run. proof: `pnpm native:verify:desktop` - [ ] AC-1 Wayland run. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` +Candidate evidence (2026-10-14): `cmake --build packages/runtime-native/build/tn-linux --target mystral -j2` exits 0. Two `verify-desktop-core.mjs` runs report observed driver `x11`, 300 frames and non-blank 1280×720 captures (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run logs `SDL video driver: wayland` and `Creating Wayland surface`, then fails before frames: Dawn reports `Unsupported sType (SurfaceSourceWaylandSurface)` and zero surface formats (`artifacts/prd614-wayland-first-failure-runtime.log`). No unchanged retry or passing Wayland claim. The signed user-space Weston compositor passed socket/shm/EGL smoke; its owned process was stopped after this run. The existing wgpu profile and compatible Dawn artifacts are being inspected before another presentation attempt. + #### Phase 2: Zero-length `writeBuffer` -**Status:** NOT STARTED +**Status:** PARTIAL — direct binding fixed and focused native RED→GREEN proved; decoder already correct; required parity RED→GREEN remains incomplete. **Files:** `src/webgpu/bindings.cpp`, `src/webgpu/bindings_frame_stream.cpp`, `conformance/registry.json` and its new case **Implementation:** Add the conformance case first and see it fail on native. Then return `undefined` for `dataSize == 0` in both paths. - [ ] AC-3 direct binding path, red then green. proof: `pnpm parity` new case - [ ] AC-3 frame-op stream path, red then green. proof: `pnpm parity` new case with the frame-op stream on +Test-first evidence (2026-10-14): focused `threenative-frame-op-stream-replay-test` built in this checkout with Dawn+V8, native Vulkan NVIDIA RTX 2080. Direct mode exits 1 solely on empty Uint8Array, ArrayBuffer and DataView (`writeBuffer: invalid data`); stream mode exits 0 with a native replay crossing. Both execute the same shared proof: five zero-size writes, three invalid-source rejections, and GPU readback `[11,22,55,66]` after nonzero writes. No runtime fix yet; the stream already accepts these inputs. Logs: `packages/runtime-native/artifacts/conformance/prd614-zero-{direct,stream}.log`. Registry dry-run validates the selected row (exit 2 because unselected rows remain blocked); JS syntax, focused Biome check and diff whitespace checks pass. Conformance runner/report specs pass: 2 files, 88 tests, 49.06 s (`pnpm exec vitest run tests/conformance-runner.test.mjs tests/conformance-report.test.mjs --reporter=dot` from `packages/runtime-native`). + +Focused continuation (2026-10-14): direct binding now accepts genuine empty ArrayBuffer, TypedArray and DataView sources, validates source ranges before returning, and avoids null-pointer arithmetic on no-ops. Built-in byteLength getters reject spoofed `{ byteLength: 0 }`; the shared proof now checks seven invalid sources, five valid zero-size writes, and nonzero GPU readback `[11,22,55,66]`. Focused own-checkout Dawn+V8/Vulkan test: direct **RED exit 1 → GREEN exit 0**; stream **PASS exit 0 before and after**. Production encoder emits zero-byte records (no elision); the new backend observer counts exactly seven decoded writeBuffer records, proving all five zero-byte records reached the actual decoder. Decoder already accepts zero payloads with valid bounds; no decoder runtime edit and no invented RED. Both AC-3 phase boxes remain unchecked because the required parity RED→GREEN on both paths has not been met. Logs: `packages/runtime-native/artifacts/conformance/prd614-zero-{direct,stream}-{before,after}.log`; focused build: `prd614-zero-green-build.log` in the same directory. `pnpm parity --target web --only-tests 91-queue-write-buffer-zero --out artifacts/conformance/prd614-zero-web` passes the selected row on headed Chromium WebGPU (NVIDIA/turing adapter; zero GPU validation errors), **exit 2** because the other 107 rows remain blocked. Report/capture: `packages/runtime-native/artifacts/conformance/prd614-zero-web/`. Focused Biome passes with one existing complexity warning; `git diff --check` passes. No full workspace build, commit, push, or AC tick. + +Detached-source continuation (2026-10-14): a detached `ArrayBuffer` or TypedArray reports `byteLength` 0 (ECMA-262 25.1.5.3), so the empty-source guard accepted it as a no-op — a security regression, not the AC-3 behaviour. The check is desktop-focused and now lives in `runWriteBufferContract`'s async JS proof right after `verifyQueueWriteBuffer(device)`, on its own 16-byte `COPY_DST` buffer destroyed in a `finally`; it detaches a real `ArrayBuffer`, `Uint8Array` and `DataView` with `ArrayBuffer.prototype.transfer`, throws when `transfer` is absent, and fails if any detached source is accepted. The shared portable helper is transfer-free, because Android V8 cannot detach a buffer and the parity row would throw there; its empty, nonempty, range and readback checks are unchanged, and the seven-record decoder count is unchanged because rejected detached writes emit no record. Focused own-checkout Dawn+V8/Vulkan test: direct **RED exit 1** (`detached ArrayBuffer source was accepted; detached Uint8Array source was accepted`) before the guard change, **GREEN exit 0** after; stream exit 0 before and after with `Decoded writeBuffer records: 7 (five zero-byte, two nonzero)` on the valid path only. The stream transport already rejected detached sources before the decoder, so no stream RED exists. The default no-argument replay-test suite still exits 0. Rerun after the split, against a freshly rebundled shared proof: direct **exit 0**, stream **exit 0** with `Decoded writeBuffer records: 7`, default suite **exit 0**; logs `packages/runtime-native/artifacts/conformance/prd614-portable-{direct,stream,replay-default}.log`. Guard change: the intrinsic brand getter runs first, then a probe that throws on a detached receiver — `new Uint8Array(data)` for ArrayBuffer and `%TypedArray%.prototype.values.call(data)` for TypedArray; `ArrayBuffer.prototype.slice` is not used because it runs a species lookup on live receivers. QuickJS `JS_GetArrayBuffer` sets a pending `TypeError` for detached input (`quickjs.c:54120-54122`), but only the detached path reaches it and that path now always throws a fresh `writeBuffer: invalid data`, so the leftover never escapes. Logs: `packages/runtime-native/artifacts/conformance/prd614-detached-{direct,stream}-{before,after}.log`, build `prd614-detached-green-build.log`, suite `prd614-detached-replay-default.log`. No AC tick, commit or push. `THREENATIVE_RUNTIME_BINARY="$PWD/packages/runtime-native/build/tn-linux/mystral" pnpm parity --target desktop --only-tests 91-queue-write-buffer-zero --reference artifacts/conformance/prd614-zero-web --out artifacts/conformance/prd614-zero-desktop` passes the selected row, **exit 2** with 107 unselected rows blocked. Report/capture: `packages/runtime-native/artifacts/conformance/prd614-zero-desktop/`; raw runner log: `packages/runtime-native/artifacts/conformance/prd614-zero-desktop-run.log`. This intentionally uses the pre-existing own-checkout runtime executable (not relinked by the focused test-target build); it validates the existing stream path, not the new direct-binding fix. The newly linked focused replay-test binary supplies the direct-fix GREEN. Other native verification remained untouched. Full desktop executable qualification of the direct fix and other platform rows remain unverified. + +Reproduce from repository root: +```sh +packages/runtime-native/node_modules/.bin/esbuild packages/runtime-native/conformance/scenes/shared/queue-write-buffer-proof.js --bundle --format=iife --global-name=writeBufferProof --outfile=packages/runtime-native/artifacts/conformance/prd614-write-buffer-proof.js +cmake --build packages/runtime-native/build/tn-linux --target threenative-frame-op-stream-replay-test -j2 +packages/runtime-native/build/tn-linux/threenative-frame-op-stream-replay-test write-buffer-direct packages/runtime-native/artifacts/conformance/prd614-write-buffer-proof.js +packages/runtime-native/build/tn-linux/threenative-frame-op-stream-replay-test write-buffer-stream packages/runtime-native/artifacts/conformance/prd614-write-buffer-proof.js +``` + #### Phase 3: linux-arm64 proof, then provenance **Status:** NOT STARTED **Files:** Native dependency/build scripts, arm64 Skia/Canvas 2D configuration, existing `.github/workflows/native-platforms.yml` only if required, and `packages/runtime-native/NOTICE`. diff --git a/packages/runtime-native/CMakeLists.txt b/packages/runtime-native/CMakeLists.txt index d390a68570..1d69840cec 100644 --- a/packages/runtime-native/CMakeLists.txt +++ b/packages/runtime-native/CMakeLists.txt @@ -1096,10 +1096,22 @@ else() endif() else() set(SKIA_PLATFORM_DIR "linux-gpu") - set(SKIA_LIB_SUBDIR "x64") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(SKIA_LIB_SUBDIR "arm64") + else() + set(SKIA_LIB_SUBDIR "x64") + endif() endif() endif() +# Linux arm64 ships an arm64 Skia prebuilt (library-builder chrome/m145), so a build that asked +# for Canvas2D and cannot find it is a missing dependency, not a host that never had Canvas2D. +# Disabling it there made the arm64 desktop artifact quietly differ from every other platform. +if(TN_ENABLE_CANVAS2D AND MYSTRAL_PLATFORM STREQUAL "linux" + AND CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(TN_SKIA_ARM64_REQUIRED ON) +endif() + if(TN_ENABLE_CANVAS2D AND EXISTS ${SKIA_TARGET_DIR}) message(STATUS "Found Skia directory: ${SKIA_TARGET_DIR}") @@ -1131,6 +1143,13 @@ if(TN_ENABLE_CANVAS2D AND EXISTS ${SKIA_TARGET_DIR}) message(STATUS "Found Skia: ${SKIA_LIBRARY}") message(STATUS "Skia includes: ${SKIA_INCLUDE_DIR}") else() + if(TN_SKIA_ARM64_REQUIRED) + message(FATAL_ERROR + "TN_SKIA_ARM64_MISSING: Canvas2D was requested on arm64 but Skia is incomplete.\n" + " Library: ${SKIA_LIB_PATH}\n" + " Include: ${SKIA_INCLUDE_DIR}\n" + " Run 'node scripts/download-deps.mjs --only skia' on an arm64 host, or configure with -DTN_ENABLE_CANVAS2D=OFF to drop the surface deliberately.") + endif() message(WARNING "Skia library or headers not found:") message(WARNING " Library: ${SKIA_LIB_PATH}") message(WARNING " Include: ${SKIA_INCLUDE_DIR}") @@ -1139,6 +1158,11 @@ else() if(NOT TN_ENABLE_CANVAS2D) message(STATUS "Skia disabled because TN_ENABLE_CANVAS2D=OFF") endif() + if(TN_SKIA_ARM64_REQUIRED) + message(FATAL_ERROR + "TN_SKIA_ARM64_MISSING: Canvas2D was requested on arm64 but no Skia was found at ${SKIA_TARGET_DIR}.\n" + " Run 'node scripts/download-deps.mjs --only skia' on an arm64 host, or configure with -DTN_ENABLE_CANVAS2D=OFF to drop the surface deliberately.") + endif() if(MYSTRAL_PLATFORM STREQUAL "ios") message(STATUS "Skia iOS not found - Canvas 2D support disabled. Run 'node scripts/download-deps.mjs --only skia-ios' to enable.") else() diff --git a/packages/runtime-native/__tests__/linux-video-driver-routing.spec.ts b/packages/runtime-native/__tests__/linux-video-driver-routing.spec.ts new file mode 100644 index 0000000000..97f3c01c01 --- /dev/null +++ b/packages/runtime-native/__tests__/linux-video-driver-routing.spec.ts @@ -0,0 +1,38 @@ +/** + * The desktop core gate must run on the video driver a caller actually asked for. + * + * `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` is the documented Wayland command and SDL 3 + * reads `SDL_VIDEO_DRIVER`, so the gate that used to pin `SDL_VIDEODRIVER=x11` and wrap every run in + * Xvfb proved X11 and called it Wayland. These cases pin the requested-driver routing. + */ + +import { describe, expect, it } from "vitest"; + +// @ts-expect-error -- the desktop verifier is plain ESM with no type declarations. +import { requestedLinuxVideoDriver } from "../scripts/verify-desktop-core.mjs"; + +describe("requestedLinuxVideoDriver", () => { + it("honors the SDL 2 spelling, the wayland request included", () => { + expect(requestedLinuxVideoDriver({ SDL_VIDEODRIVER: "wayland" })).toEqual({ + driver: "wayland", + wayland: true, + }); + }); + + it("prefers the SDL 3 spelling when both are set", () => { + expect( + requestedLinuxVideoDriver({ SDL_VIDEO_DRIVER: "x11", SDL_VIDEODRIVER: "wayland" }), + ).toEqual({ + driver: "x11", + wayland: false, + }); + }); + + it("keeps x11 as the default, and does not read an empty variable as a request", () => { + expect(requestedLinuxVideoDriver({})).toEqual({ driver: "x11", wayland: false }); + expect(requestedLinuxVideoDriver({ SDL_VIDEO_DRIVER: "", SDL_VIDEODRIVER: "" })).toEqual({ + driver: "x11", + wayland: false, + }); + }); +}); diff --git a/packages/runtime-native/conformance/registry.json b/packages/runtime-native/conformance/registry.json index 18ecd85b7e..a7bb7740e5 100644 --- a/packages/runtime-native/conformance/registry.json +++ b/packages/runtime-native/conformance/registry.json @@ -1310,6 +1310,19 @@ "desktopGate": false, "required": true }, + { + "id": "91-queue-write-buffer-zero", + "title": "zero-length queue writeBuffer preserves nonzero writes and rejects invalid sources", + "category": "runtime", + "status": "implemented", + "scene": "conformance/scenes/shared/queue-write-buffer-zero.js", + "tolerance": { + "pixelMismatchRatio": 0.01, + "perceptualDeltaE": 3.0 + }, + "desktopGate": true, + "required": true + }, { "id": "91-frame-upload-stream", "title": "steady-state frame upload stream", diff --git a/packages/runtime-native/conformance/scenes/shared/queue-write-buffer-proof.js b/packages/runtime-native/conformance/scenes/shared/queue-write-buffer-proof.js new file mode 100644 index 0000000000..6189455d8d --- /dev/null +++ b/packages/runtime-native/conformance/scenes/shared/queue-write-buffer-proof.js @@ -0,0 +1,71 @@ +export async function verifyQueueWriteBuffer(device) { + if (!device?.queue || typeof device.createBuffer !== "function") { + throw new Error("writeBuffer proof requires a real GPU device"); + } + const buffer = device.createBuffer({ + size: 16, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + }); + const readback = device.createBuffer({ + size: 16, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const failures = []; + try { + device.queue.writeBuffer(buffer, 0, new Uint32Array([11, 22, 33, 44])); + const source = new Uint32Array([99]); + const emptyWrites = [ + [new Uint8Array(0)], + [new ArrayBuffer(0)], + [new DataView(new ArrayBuffer(0))], + [source, 0, 0], + [source, 1, 0], + ]; + for (const args of emptyWrites) { + try { + if (device.queue.writeBuffer(buffer, 0, ...args) !== undefined) { + failures.push("zero-length write did not return undefined"); + } + } catch (error) { + failures.push(`zero-length ${args[0].constructor.name}: ${String(error.message || error)}`); + } + } + const invalidSources = [ + [source, -1], + [source, 0, 2], + [{}], + [{ byteLength: 0 }], + [new Uint8Array(0), 1], + [new ArrayBuffer(0), 0, 1], + [new DataView(new ArrayBuffer(0)), 1], + ]; + for (const args of invalidSources) { + let rejected = false; + try { + device.queue.writeBuffer(buffer, 0, ...args); + } catch { + rejected = true; + } + if (!rejected) failures.push("invalid nonempty source was accepted"); + } + device.queue.writeBuffer(buffer, 8, new Uint32Array([55, 66])); + const encoder = device.createCommandEncoder(); + encoder.copyBufferToBuffer(buffer, 0, readback, 0, 16); + device.queue.submit([encoder.finish()]); + await readback.mapAsync(GPUMapMode.READ); + const observed = Array.from(new Uint32Array(readback.getMappedRange())); + readback.unmap(); + if (observed.join(",") !== "11,22,55,66") { + failures.push(`nonzero writes or no-op preservation failed: ${observed}`); + } + if (failures.length) throw new Error(failures.join("; ")); + return { + emptyWrites: emptyWrites.length, + invalidSourcesRejected: invalidSources.length, + observed, + }; + } finally { + buffer.destroy(); + readback.destroy(); + } +} diff --git a/packages/runtime-native/conformance/scenes/shared/queue-write-buffer-zero.js b/packages/runtime-native/conformance/scenes/shared/queue-write-buffer-zero.js new file mode 100644 index 0000000000..6a96cae19f --- /dev/null +++ b/packages/runtime-native/conformance/scenes/shared/queue-write-buffer-zero.js @@ -0,0 +1,11 @@ +import { startBehaviorScene } from "./scene-support.js"; +import { verifyQueueWriteBuffer } from "./queue-write-buffer-proof.js"; + +export function startScene(canvas, dimensions) { + return startBehaviorScene(canvas, dimensions, "queue-write-buffer-zero", async () => { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) throw new Error("writeBuffer proof requires a GPU adapter"); + const device = await adapter.requestDevice(); + return verifyQueueWriteBuffer(device); + }); +} diff --git a/packages/runtime-native/include/mystral/webgpu_compat.h b/packages/runtime-native/include/mystral/webgpu_compat.h index b9624feec0..0f3f2d3561 100644 --- a/packages/runtime-native/include/mystral/webgpu_compat.h +++ b/packages/runtime-native/include/mystral/webgpu_compat.h @@ -39,6 +39,10 @@ typedef WGPUSurfaceDescriptorFromWindowsHWND WGPUSurfaceDescriptorFromWindowsHWN typedef WGPUSurfaceDescriptorFromXlibWindow WGPUSurfaceDescriptorFromXlibWindow_Compat; #define WGPUSType_SurfaceDescriptorFromXlibWindow_Compat WGPUSType_SurfaceDescriptorFromXlibWindow +// Surface descriptors - Linux Wayland +typedef WGPUSurfaceDescriptorFromWaylandSurface WGPUSurfaceDescriptorFromWaylandSurface_Compat; +#define WGPUSType_SurfaceDescriptorFromWaylandSurface_Compat WGPUSType_SurfaceDescriptorFromWaylandSurface + // Surface descriptors - Android ANativeWindow typedef WGPUSurfaceDescriptorFromAndroidNativeWindow WGPUSurfaceDescriptorFromAndroidNativeWindow_Compat; #define WGPUSType_SurfaceDescriptorFromAndroidNativeWindow_Compat WGPUSType_SurfaceDescriptorFromAndroidNativeWindow @@ -133,6 +137,10 @@ typedef WGPUSurfaceSourceWindowsHWND WGPUSurfaceDescriptorFromWindowsHWND_Compat typedef WGPUSurfaceSourceXlibWindow WGPUSurfaceDescriptorFromXlibWindow_Compat; #define WGPUSType_SurfaceDescriptorFromXlibWindow_Compat WGPUSType_SurfaceSourceXlibWindow +// Surface descriptors - Linux Wayland +typedef WGPUSurfaceSourceWaylandSurface WGPUSurfaceDescriptorFromWaylandSurface_Compat; +#define WGPUSType_SurfaceDescriptorFromWaylandSurface_Compat WGPUSType_SurfaceSourceWaylandSurface + // Surface descriptors - Android ANativeWindow typedef WGPUSurfaceSourceAndroidNativeWindow WGPUSurfaceDescriptorFromAndroidNativeWindow_Compat; #define WGPUSType_SurfaceDescriptorFromAndroidNativeWindow_Compat WGPUSType_SurfaceSourceAndroidNativeWindow diff --git a/packages/runtime-native/native-deps.lock.json b/packages/runtime-native/native-deps.lock.json index 58ac1d92df..2349d1c7ed 100644 --- a/packages/runtime-native/native-deps.lock.json +++ b/packages/runtime-native/native-deps.lock.json @@ -488,6 +488,16 @@ ], "bootstrap": "two-run-fetch-2026-09-11" }, + { + "id": "quiche-linux-aarch64", + "url": "https://github.com/mystralengine/library-builder/releases/download/quiche-0.24.6-4/quiche-linux-arm64.zip", + "sha256": "5e316678246fdf7390845bdd81ea815a8018f3ad5a22d0fe7d42361835f3e843", + "bytes": 19682417, + "targets": [ + "desktop" + ], + "bootstrap": "github-release-digest-2026-10-14" + }, { "id": "quiche-mac-arm64", "url": "https://github.com/mystralengine/library-builder/releases/download/quiche-0.24.6-3/quiche-mac-arm64.zip", @@ -676,6 +686,16 @@ ], "bootstrap": "two-run-fetch-2026-09-11" }, + { + "id": "webp-linux-aarch64", + "url": "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.5.0-linux-aarch64.tar.gz", + "sha256": "ec874dd2e52097f11dd4999176ae6b6fb0ecb165bc1e05d3ecc9b29014fb003e", + "bytes": 10138687, + "targets": [ + "desktop" + ], + "bootstrap": "download-sha256-2026-10-14" + }, { "id": "webp-mac-arm64", "url": "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.5.0-mac-arm64.tar.gz", @@ -761,6 +781,16 @@ ], "bootstrap": "two-run-fetch-2026-09-11" }, + { + "id": "skia-linux-aarch64", + "url": "https://github.com/mystralengine/library-builder/releases/download/chrome%2Fm145/skia-build-linux-arm64-gpu-release.zip", + "sha256": "1cd646e55a6abd4a97935939bb419c11ea7a1bb6f8ccd6cf324da305cf2d982a", + "bytes": 41600223, + "targets": [ + "desktop" + ], + "bootstrap": "github-release-digest-2026-10-14" + }, { "id": "skia-mac-arm64", "url": "https://github.com/olilarkin/skia-builder/releases/download/chrome%2Fm145/skia-build-mac-arm64-gpu-release.zip", @@ -879,6 +909,16 @@ ], "bootstrap": "two-run-fetch-2026-09-11" }, + { + "id": "swc-linux-aarch64", + "url": "https://github.com/mystralengine/library-builder/releases/download/swc-41/swc-linux-arm64.zip", + "sha256": "63ce88b0d943709c3c80cc07afe9a37455d4724fd2e50bba605bd74219f8b69d", + "bytes": 12933800, + "targets": [ + "desktop" + ], + "bootstrap": "github-release-digest-2026-10-14" + }, { "id": "swc-mac-arm64", "url": "https://github.com/mystralengine/library-builder/releases/download/swc-11/swc-mac-arm64.zip", diff --git a/packages/runtime-native/scripts/download-deps.mjs b/packages/runtime-native/scripts/download-deps.mjs index a6cc308a83..2f88a3c207 100644 --- a/packages/runtime-native/scripts/download-deps.mjs +++ b/packages/runtime-native/scripts/download-deps.mjs @@ -84,6 +84,11 @@ const WGPU_DEPS = new Set(['wgpu', 'wgpu-ios', 'wgpu-android']); let wgpuVersionOverride = null; // Dependency versions and URLs +// Linux arm64 pins. x64 stays on the tags above; these are the first library-builder +// releases that publish a linux arm64 archive for the dependency named in the comment. +const LINUX_ARM64_QUICHE = 'quiche-0.24.6-4'; +const LINUX_ARM64_SWC = 'swc-41'; + const DEPS = { wgpu: { version: DEFAULT_WGPU_VERSION, @@ -214,11 +219,16 @@ const DEPS = { // https://github.com/mystralengine/library-builder/releases version: 'quiche-0.24.6-3', getUrl: () => { - const baseUrl = 'https://github.com/mystralengine/library-builder/releases/download/quiche-0.24.6-3'; + // Linux arm64 first reached a published prebuilt at -4; x64 stays on -3. + const tag = platformName === 'linux' && ARCH === 'arm64' ? LINUX_ARM64_QUICHE : DEPS.quiche.version; + const baseUrl = `https://github.com/mystralengine/library-builder/releases/download/${tag}`; if (platformName === 'macos') { const arch = ARCH === 'arm64' ? 'arm64' : 'x86_64'; return `${baseUrl}/quiche-mac-${arch}.zip`; }if (platformName === 'linux') { + if (ARCH === 'arm64') { + return `${baseUrl}/quiche-linux-arm64.zip`; + } if (ARCH !== 'x64') { console.warn(`quiche prebuilts not available for ${platformName}-${archName}`); return null; @@ -293,7 +303,8 @@ const DEPS = { const arch = ARCH === 'arm64' ? 'arm64' : 'x86-64'; return `https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${version}-mac-${arch}.tar.gz`; }if (platformName === 'linux') { - return `https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${version}-linux-x86-64.tar.gz`; + const arch = ARCH === 'arm64' ? 'aarch64' : 'x86-64'; + return `https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${version}-linux-${arch}.tar.gz`; }if (platformName === 'windows') { return `https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${version}-windows-x64.zip`; } @@ -333,12 +344,17 @@ const DEPS = { // version: 'chrome/m145', getUrl: () => { - const baseUrl = 'https://github.com/olilarkin/skia-builder/releases/download/chrome%2Fm145'; + // olilarkin/skia-builder publishes Linux x64 only; library-builder mirrors the same + // chrome/m145 build for linux arm64, which is the arm64 archive this project pins. + const baseUrl = ARCH === 'arm64' && platformName === 'linux' + ? 'https://github.com/mystralengine/library-builder/releases/download/chrome%2Fm145' + : 'https://github.com/olilarkin/skia-builder/releases/download/chrome%2Fm145'; if (platformName === 'macos') { const arch = ARCH === 'arm64' ? 'arm64' : 'x86_64'; return `${baseUrl}/skia-build-mac-${arch}-gpu-release.zip`; }if (platformName === 'linux') { - return `${baseUrl}/skia-build-linux-x64-gpu-release.zip`; + const arch = ARCH === 'arm64' ? 'arm64' : 'x64'; + return `${baseUrl}/skia-build-linux-${arch}-gpu-release.zip`; }if (platformName === 'windows') { return `${baseUrl}/skia-build-win-x64-gpu-release.zip`; } @@ -350,11 +366,16 @@ const DEPS = { swc: { version: 'swc-11', getUrl: () => { - const baseUrl = 'https://github.com/mystralengine/library-builder/releases/download/swc-11'; + // swc-41 is the first library-builder tag with swc-linux-arm64.zip; x64 stays on swc-11. + const tag = platformName === 'linux' && ARCH === 'arm64' ? LINUX_ARM64_SWC : DEPS.swc.version; + const baseUrl = `https://github.com/mystralengine/library-builder/releases/download/${tag}`; if (platformName === 'macos') { const arch = ARCH === 'arm64' ? 'arm64' : 'x86_64'; return `${baseUrl}/swc-mac-${arch}.zip`; }if (platformName === 'linux') { + if (ARCH === 'arm64') { + return `${baseUrl}/swc-linux-arm64.zip`; + } if (ARCH !== 'x64') { console.warn(`SWC prebuilts not available for ${platformName}-${archName}`); return null; @@ -1302,7 +1323,9 @@ export async function downloadDep(name, options = {}) { console.warn(`Skipping ${name} - no prebuilt available for this platform`); return false; } - if (!lockPayloadForUrl(lock.payloads, url)) { + // Same-lock receipts can name another architecture; require the selected payload too. + const selectedPayload = lockPayloadForUrl(lock.payloads, url); + if (!selectedPayload) { console.error( `TN_NATIVE_DEP_LOCK_MISSING: no lock payload covers ${url} (requested as '${name}'). Refusing an unreviewed download.`, ); @@ -1315,8 +1338,8 @@ export async function downloadDep(name, options = {}) { if (!process.argv.includes('--force')) { if (!WGPU_DEPS.has(name)) { const receipt = readReceipt(destDir, lock.lockHash); - if (!receipt) { - console.warn(`${name} has no receipt for the current lock; replacing the stale dependency cache`); + if (!receipt || !Array.isArray(receipt.payloads) || !receipt.payloads.includes(selectedPayload.id)) { + console.warn(`${name} has no receipt for ${selectedPayload.id} under the current lock; replacing the stale dependency cache`); } else { console.log('Skipping (use --force to re-download)'); return true; @@ -1545,18 +1568,17 @@ async function main() { if (requestedBackend === 'dawn') assertDawnAndroidArchive(); if (requestedWgpuVersion) configureWgpuOverride(requestedWgpuVersion); - // Linux arm64 has no prebuilt for the x64 desktop stack: kuoruan/libv8 ships Linux x64 only, - // Dawn's `ubuntu-latest` release asset is x64, and skia/swc/quiche/webp publish no arm64 Linux - // archive. A source V8 build is not allowed in CI. The arm64 desktop host is therefore built with - // the QuickJS engine over wgpu-native, whose `wgpu-linux-aarch64` prebuilt is already in the - // dependency lock; the CMake build disables the optional features whose libraries are absent - // (Canvas2D/Skia, WebTransport/quiche, TS/SWC) instead of linking a foreign architecture. libuv - // has no arm64 prebuilt either, so arm64 also fetches the architecture-independent `libuv-source` - // and CMake builds it from that pinned source (see CMakeLists.txt). See scripts/native-build.mjs - // for the matching configure overrides. + // Linux arm64 keeps the QuickJS engine over wgpu-native, whose `wgpu-linux-aarch64` prebuilt + // is already in the dependency lock: kuoruan/libv8 ships Linux x64 only and Dawn's + // `ubuntu-latest` release asset is x64, and a source V8 build is not allowed in CI. Everything + // else the x64 desktop set links now has an arm64 archive, so the arm64 host builds the same + // feature set instead of dropping Canvas2D, WebP textures, TypeScript and WebTransport. + // libuv is the exception: library-builder ships libuv-linux-x64 only, so arm64 fetches the + // architecture-independent `libuv-source` and CMake builds it from that pinned source. See + // scripts/native-build.mjs for the matching configure overrides. const linuxArm64 = platformName === 'linux' && ARCH === 'arm64'; const desktopDeps = linuxArm64 - ? ['wgpu', 'sdl3', 'quickjs', 'stb', 'libuv-source'] + ? ['wgpu', 'sdl3', 'quickjs', 'stb', 'webp', 'skia', 'swc', 'quiche', 'libuv-source'] : ['wgpu', 'sdl3', 'dawn', 'v8', 'quickjs', 'stb', 'webp', platformName === 'windows' ? 'skia-win-static' : 'skia', 'swc', 'libuv', 'libuv-source', 'quiche']; // iOS deps (only downloaded with --only or --ios) diff --git a/packages/runtime-native/scripts/verify-desktop-core.mjs b/packages/runtime-native/scripts/verify-desktop-core.mjs index ac545c70a5..d391375d97 100644 --- a/packages/runtime-native/scripts/verify-desktop-core.mjs +++ b/packages/runtime-native/scripts/verify-desktop-core.mjs @@ -281,6 +281,19 @@ export function analyzePresentTicks(log, { minTicks = 1 } = {}) { return { failures: [...new Set(failures)], ticks }; } +/** + * The Linux video driver this run must use, from the environment a caller set. + * + * SDL 3 reads `SDL_VIDEO_DRIVER`; `SDL_VIDEODRIVER` is the SDL 2 spelling the documented + * `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` still uses, and `src/platform/window.cpp` + * honours it for the same reason. SDL_VIDEO_DRIVER wins when both are set. Absent either, this gate + * keeps its X11 default. + */ +export function requestedLinuxVideoDriver(env = process.env) { + const driver = env.SDL_VIDEO_DRIVER || env.SDL_VIDEODRIVER || 'x11'; + return { driver, wayland: driver === 'wayland' }; +} + export function verifyDesktopCore({ frames = 300 } = {}) { const preset = process.platform === 'darwin' ? 'tn-macos' @@ -308,13 +321,20 @@ export function verifyDesktopCore({ frames = 300 } = {}) { // Not `xvfb-run`: on xorg-server-xvfb 21.1.24 its cleanup `kill` fails after Xvfb has already // exited and that failing kill's status replaces the command's, so this gate reported a red // 300-frame run that had in fact rendered every frame and written a good screenshot. - const command = process.platform === 'linux' ? 'sh' : binary; - const args = process.platform === 'linux' + const requested = requestedLinuxVideoDriver(); + // Xvfb is an X11 server, so wrapping a Wayland request in it would run the proof on X11 and report + // it as Wayland. A Wayland run needs the compositor the caller started, and it reads the window + // handles SDL's Wayland driver hands back. + const command = process.platform === 'linux' && !requested.wayland ? 'sh' : binary; + const args = process.platform === 'linux' && !requested.wayland ? [join(workspace, 'scripts', 'xvfb.sh'), binary, ...runtimeArgs] : runtimeArgs; const runtimeEnv = { ...process.env }; if (process.platform === 'linux') { - runtimeEnv.SDL_VIDEODRIVER = 'x11'; + console.info(`desktop core gate: SDL video driver ${requested.driver}${requested.wayland ? ' (no Xvfb wrapper; the compositor comes from the environment)' : ''}`); + // Both spellings, so the child resolves the same driver this gate read. + runtimeEnv.SDL_VIDEO_DRIVER = requested.driver; + runtimeEnv.SDL_VIDEODRIVER = requested.driver; // Same reason as the video driver: this gate runs headless on a runner with no sound card, and // SDL aborts before the first frame with // [Audio] Failed to open audio device: ALSA: Couldn't open audio device: No such file or diff --git a/packages/runtime-native/scripts/xvfb.sh b/packages/runtime-native/scripts/xvfb.sh index 17d3bc99dd..b7c3f8cf23 100644 --- a/packages/runtime-native/scripts/xvfb.sh +++ b/packages/runtime-native/scripts/xvfb.sh @@ -33,6 +33,17 @@ display_file="$runtime/display" Xvfb -displayfd 3 +extension COMPOSITE +extension SHAPE -screen 0 "$screen" -nolisten tcp 3>"$display_file" & xvfb_pid=$! +# This script hands the command an X11 server, so it is also the honest place to answer a Wayland +# request: say which display the run really uses instead of letting an X11 result pass as Wayland. +# Both spellings are set because SDL 3 reads SDL_VIDEO_DRIVER and src/platform/window.cpp also +# honours the SDL 2 spelling SDL_VIDEODRIVER. +if [ "${SDL_VIDEO_DRIVER:-${SDL_VIDEODRIVER:-}}" = "wayland" ]; then + echo "scripts/xvfb.sh: SDL_VIDEO_DRIVER/SDL_VIDEODRIVER asks for Wayland; this gate runs on the X11 display this script provides." >&2 +fi +SDL_VIDEO_DRIVER=x11 +SDL_VIDEODRIVER=x11 +export SDL_VIDEO_DRIVER SDL_VIDEODRIVER + compositor_pid="" cleanup() { diff --git a/packages/runtime-native/src/cli/main.cpp b/packages/runtime-native/src/cli/main.cpp index 2370739a00..909a114d0c 100644 --- a/packages/runtime-native/src/cli/main.cpp +++ b/packages/runtime-native/src/cli/main.cpp @@ -1124,7 +1124,18 @@ static std::unique_ptr createConfiguredRuntime(const CLIOption // Only the web overlay needs this. The CSS UI backend attaches to no window system at all — it // hands the compositor pixels — so it must not cost a Wayland session its native video driver. if (!opts.uiRoot.empty() && !opts.cssUiRenderer) { - // The web overlay attaches to an X11 surface, also on Wayland via XWayland. + // The web overlay attaches to an X11 surface, also on Wayland via XWayland. SDL_HINT_OVERRIDE + // below replaces any explicit driver, so a Wayland request with this overlay would run on X11 + // without saying so. Refuse it instead and name the two ways forward. + const char* sdl3Driver = std::getenv("SDL_VIDEO_DRIVER"); + const char* legacyDriver = std::getenv("SDL_VIDEODRIVER"); + const char* requestedDriver = (sdl3Driver != nullptr && *sdl3Driver != '\0') ? sdl3Driver : legacyDriver; + if (requestedDriver != nullptr && std::string(requestedDriver) == "wayland") { + std::cerr << "[Mystral] The web UI overlay needs X11, but SDL_VIDEO_DRIVER/SDL_VIDEODRIVER " + "asks for Wayland. Set ui.renderer: \"native-css\", or unset the variable." + << std::endl; + std::exit(1); + } // Select both backends before SDL/GTK initialization and worker creation; a desktop // session's GDK_BACKEND=wayland must not leave the game's requested HUD unattached. SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "x11", SDL_HINT_OVERRIDE); diff --git a/packages/runtime-native/src/platform/window.cpp b/packages/runtime-native/src/platform/window.cpp index 61e26347ce..ca4be7d7d0 100644 --- a/packages/runtime-native/src/platform/window.cpp +++ b/packages/runtime-native/src/platform/window.cpp @@ -228,9 +228,15 @@ bool createWindow( std::cout << "[Window] Creating window: " << title << " (" << width << "x" << height << ")" << std::endl; #if defined(__linux__) && !defined(__ANDROID__) - // The Linux WebGPU surface currently consumes SDL's X11 handles. Prefer that supported - // backend even in a Wayland session; an explicit SDL_VIDEODRIVER remains an override. - SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11"); + // X11 stays the default because it is the backend the UI overlay and the verified desktop lane + // use. SDL 3 reads SDL_VIDEO_DRIVER only, so an explicit request written with the SDL 2 + // spelling (SDL_VIDEODRIVER) is honoured here as well; SDL_VIDEO_DRIVER wins when both are set. + const char* sdl3Driver = std::getenv("SDL_VIDEO_DRIVER"); + const char* legacyDriver = std::getenv("SDL_VIDEODRIVER"); + const char* requestedDriver = (sdl3Driver != nullptr && *sdl3Driver != '\0') ? sdl3Driver + : ((legacyDriver != nullptr && *legacyDriver != '\0') ? legacyDriver : "x11"); + std::cout << "[Window] SDL video driver request: " << requestedDriver << std::endl; + SDL_SetHint(SDL_HINT_VIDEO_DRIVER, requestedDriver); #endif // Initialize SDL if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMEPAD)) { diff --git a/packages/runtime-native/src/runtime.cpp b/packages/runtime-native/src/runtime.cpp index fa53aa38cb..cb0efe1c64 100644 --- a/packages/runtime-native/src/runtime.cpp +++ b/packages/runtime-native/src/runtime.cpp @@ -652,34 +652,53 @@ class RuntimeImpl : public Runtime { return false; } #elif defined(__linux__) - // Linux: Use X11 from SDL (Wayland support would need additional work) + // Linux: use whichever handles SDL actually gave us. The driver is named here because the + // receipt for "this ran on Wayland" has to come from SDL, not from an environment variable + // a caller set. SDL_Window* sdlWindow = platform::getSDLWindow(); if (!sdlWindow) { std::cerr << "[Mystral] Failed to get SDL window" << std::endl; return false; } - // Try X11 first - void* xdisplay = SDL_GetPointerProperty(SDL_GetWindowProperties(sdlWindow), - SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); - if (xdisplay) { - auto xwindow = static_cast(SDL_GetNumberProperty(SDL_GetWindowProperties(sdlWindow), + SDL_PropertiesID windowProps = SDL_GetWindowProperties(sdlWindow); + std::cout << "[Mystral] SDL video driver: " + << (SDL_GetCurrentVideoDriver() ? SDL_GetCurrentVideoDriver() : "none") << std::endl; + + void* wlDisplay = SDL_GetPointerProperty(windowProps, + SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr); + if (wlDisplay) { + void* wlSurface = SDL_GetPointerProperty(windowProps, + SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr); + if (!wlSurface) { + std::cerr << "[Mystral] Wayland display without a surface" << std::endl; + return false; + } + std::cout << "[Mystral] Using Wayland display: " << wlDisplay << " surface: " << wlSurface << std::endl; + if (!webgpu_->createSurfaceWithDisplay(wlDisplay, wlSurface, webgpu::Context::PLATFORM_WAYLAND)) { + std::cerr << "[Mystral] Failed to create WebGPU surface" << std::endl; + return false; + } + } else { + void* xdisplay = SDL_GetPointerProperty(windowProps, + SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); + if (!xdisplay) { + std::cerr << "[Mystral] Wayland or X11 display not available." << std::endl; + return false; + } + auto xwindow = static_cast(SDL_GetNumberProperty(windowProps, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)); - if (xwindow) { - std::cout << "[Mystral] Using X11 display: " << xdisplay << " window: " << xwindow << std::endl; - // Pass both display and window pointer for proper X11 surface creation - // Dawn requires the display pointer, wgpu-native can work with just window - if (!webgpu_->createSurfaceWithDisplay(xdisplay, reinterpret_cast(xwindow), webgpu::Context::PLATFORM_XLIB)) { - std::cerr << "[Mystral] Failed to create WebGPU surface" << std::endl; - return false; - } - } else { + if (!xwindow) { std::cerr << "[Mystral] Failed to get X11 window" << std::endl; return false; } - } else { - std::cerr << "[Mystral] X11 display not available. Wayland not yet supported." << std::endl; - return false; + std::cout << "[Mystral] Using X11 display: " << xdisplay << " window: " << xwindow << std::endl; + // Pass both display and window pointer for proper X11 surface creation + // Dawn requires the display pointer, wgpu-native can work with just window + if (!webgpu_->createSurfaceWithDisplay(xdisplay, reinterpret_cast(xwindow), webgpu::Context::PLATFORM_XLIB)) { + std::cerr << "[Mystral] Failed to create WebGPU surface" << std::endl; + return false; + } } #else std::cerr << "[Mystral] WebGPU surface creation not implemented for this platform" << std::endl; diff --git a/packages/runtime-native/src/webgpu/bindings.cpp b/packages/runtime-native/src/webgpu/bindings.cpp index dc3efa4a8b..f292254304 100644 --- a/packages/runtime-native/src/webgpu/bindings.cpp +++ b/packages/runtime-native/src/webgpu/bindings.cpp @@ -1688,8 +1688,29 @@ static js::JSValueHandle handleGpuQueueWriteBuffer(BindingsState* state, Binding size_t dataSize = 0; void* dataPtr = state->engine->getArrayBufferData(args[2], &dataSize); if (!dataPtr || dataSize == 0) { - state->engine->throwException("writeBuffer: invalid data"); - return state->engine->newUndefined(); + // Empty backing stores may have no pointer; brand-check before accepting a no-op. + js::JSValueGuard isEmptySource(*state->engine, state->engine->evalScriptWithResult(R"JS((data) => { + const typedArrays = Object.getPrototypeOf(Uint8Array.prototype); + const emptySource = (prototype, probe) => { + let empty; + try { empty = Object.getOwnPropertyDescriptor(prototype, "byteLength").get.call(data) === 0; } + catch { return false; } + if (!empty) return false; + try { probe(data); } catch { return false; } + return true; + }; + // A detached ArrayBuffer or TypedArray still reports byteLength 0, so probe it: + // both probes throw on a detached receiver and run no user code. + if (emptySource(ArrayBuffer.prototype, (value) => new Uint8Array(value))) return true; + if (emptySource(typedArrays, (value) => typedArrays.values.call(value))) return true; + return emptySource(DataView.prototype, () => {}); + })JS", "write-buffer-empty-source.js")); + js::JSValueGuard validEmpty(*state->engine, state->engine->call( + isEmptySource.get(), state->engine->newUndefined(), {args[2]})); + if (!state->engine->toBoolean(validEmpty.get())) { + state->engine->throwException("writeBuffer: invalid data"); + return state->engine->newUndefined(); + } } // WebGPU expresses dataOffset/size in elements for a TypedArray, // but in bytes for ArrayBuffer and DataView inputs. @@ -1716,6 +1737,7 @@ static js::JSValueHandle handleGpuQueueWriteBuffer(BindingsState* state, Binding state->engine->throwException("writeBuffer: source range exceeds the supplied buffer view"); return state->engine->newUndefined(); } + if (writeSize == 0) return state->engine->newUndefined(); const uint8_t* source = static_cast(dataPtr) + dataOffset; if (buffer && state->queue) { const size_t alignedWriteSize = (writeSize + 3) & ~size_t(3); diff --git a/packages/runtime-native/src/webgpu/context.cpp b/packages/runtime-native/src/webgpu/context.cpp index 23224077bd..8a6ca3b98a 100644 --- a/packages/runtime-native/src/webgpu/context.cpp +++ b/packages/runtime-native/src/webgpu/context.cpp @@ -1042,6 +1042,7 @@ bool Context::createSurfaceWithDisplay(void* display, void* window, int platform #if defined(__linux__) && !defined(__ANDROID__) WGPUSurfaceDescriptorFromXlibWindow_Compat xlibDesc = {}; + WGPUSurfaceDescriptorFromWaylandSurface_Compat waylandDesc = {}; if (platformType == PLATFORM_XLIB) { xlibDesc.chain.sType = WGPUSType_SurfaceDescriptorFromXlibWindow_Compat; @@ -1049,8 +1050,14 @@ bool Context::createSurfaceWithDisplay(void* display, void* window, int platform xlibDesc.window = reinterpret_cast(window); surfaceDesc.nextInChain = (WGPUChainedStruct*)&xlibDesc; std::cout << "[WebGPU] Creating X11 surface with display: " << display << " window: " << window << std::endl; + } else if (platformType == PLATFORM_WAYLAND) { + waylandDesc.chain.sType = WGPUSType_SurfaceDescriptorFromWaylandSurface_Compat; + waylandDesc.display = display; // wl_display* + waylandDesc.surface = window; // wl_surface* + surfaceDesc.nextInChain = (WGPUChainedStruct*)&waylandDesc; + std::cout << "[WebGPU] Creating Wayland surface with display: " << display << " surface: " << window << std::endl; } else { - std::cerr << "[WebGPU] createSurfaceWithDisplay only supports PLATFORM_XLIB on Linux" << std::endl; + std::cerr << "[WebGPU] createSurfaceWithDisplay only supports PLATFORM_XLIB or PLATFORM_WAYLAND on Linux" << std::endl; return false; } #else diff --git a/packages/runtime-native/tests/dependency-provenance.test.mjs b/packages/runtime-native/tests/dependency-provenance.test.mjs index ec102a2ee4..11812d8938 100644 --- a/packages/runtime-native/tests/dependency-provenance.test.mjs +++ b/packages/runtime-native/tests/dependency-provenance.test.mjs @@ -1,8 +1,9 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { test } from 'vitest'; const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); @@ -65,6 +66,35 @@ test('Linux arm64 fetches libuv source where no prebuilt exists, and CMake build ); }); +test('Linux arm64 payloads name arm64 assets and leave the x64 pins alone', () => { + // The arm64 desktop set fetches skia/webp/swc/quiche, so the lock is what an arm64 download + // resolves and hashes. A payload id that says aarch64 while its URL names x64 bytes is the + // failure this catches: the download succeeds and links a foreign architecture. Which platform + // selects which payload is proven by the arm64 download run and its ELF check, not by reading + // the resolver's source text. + const lock = readLock(); + const urlOf = (component, id) => + lock.components.find((c) => c.name === component)?.payloads.find((p) => p.id === id)?.url; + + for (const [component, id, asset] of [ + ['quiche', 'quiche-linux-aarch64', 'quiche-linux-arm64.zip'], + ['webp', 'webp-linux-aarch64', 'libwebp-1.5.0-linux-aarch64.tar.gz'], + ['skia', 'skia-linux-aarch64', 'skia-build-linux-arm64-gpu-release.zip'], + ['swc', 'swc-linux-aarch64', 'swc-linux-arm64.zip'], + ]) { + const url = urlOf(component, id); + assert.ok(url, `lock component '${component}' must carry payload '${id}'`); + assert.ok(url.endsWith(asset), `payload '${id}' must point at ${asset}, got ${url}`); + assert.ok(!/x64|x86_64/u.test(url), `payload '${id}' must not link a foreign architecture`); + } + + // x64 keeps the assets it always had: arm64 is an added payload, not a re-pin of the old one. + assert.ok(urlOf('quiche', 'quiche-linux-x64')?.endsWith('quiche-linux-x64.zip')); + assert.ok(urlOf('swc', 'swc-linux-x64')?.endsWith('swc-linux-x64.zip')); + assert.ok(urlOf('skia', 'skia-linux')?.endsWith('skia-build-linux-x64-gpu-release.zip')); + assert.ok(urlOf('webp', 'webp-linux')?.endsWith('libwebp-1.5.0-linux-x86-64.tar.gz')); +}); + test('should reject a tampered archive before extraction when downloaded bytes differ', () => { // PRD-059 Phase 1 archive-before-extract gate, run against the real transaction // helper path: bytes that do not match the lock digest never reach an extractor. @@ -118,11 +148,72 @@ test('should report the locked matrix without network access', () => { // PRD-059 Phase 1 user verification: deps:verify lists every selectable // payload and performs no download, extraction, or toolchain invocation. const output = execFileSync(process.execPath, [DOWNLOADER, '--check-lock'], { encoding: 'utf8' }); - assert.match(output, /payloads: 63/); + assert.match(output, /payloads: 67/); assert.match(output, /wgpu:/); assert.match(output, /gradle-wrapper:/); }); +test('a same-lock receipt naming another payload must not count as a cache hit', async () => { + // A receipt states which locked payload bytes are installed, not only which lock they came from. + // An x64 checkout that selects the arm64 payload shares the lock hash, so a lock-hash-only check + // reports a hit and the arm64 build keeps linking foreign-architecture quiche/swc/webp. The + // selected payload id belongs to the hit; a receipt without it is the stale-cache path. + const root = mkdtempSync(join(tmpdir(), 'tn-dep-cache-')); + const realFetch = globalThis.fetch; + try { + // The real downloader in an isolated package root: destDir is derived from the module's own + // location, so copying the scripts keeps this checkout's third_party/ tree untouched. + cpSync(join(PACKAGE_ROOT, 'scripts'), join(root, 'scripts'), { recursive: true }); + cpSync(LOCK_PATH, join(root, 'native-deps.lock.json')); + cpSync(join(PACKAGE_ROOT, 'package.json'), join(root, 'package.json')); + const downloader = await import(pathToFileURL(join(root, 'scripts', 'download-deps.mjs')).href); + const component = readLock().components.find((c) => c.name === 'webp'); + const lockContext = { + lockHash: 'fixture-lock-hash', + payloads: new Map(component.payloads.map((p) => [p.id, { ...p, component: 'webp', version: component.version }])), + }; + const destDir = join(root, 'third_party', 'webp'); + const fetched = []; + globalThis.fetch = async (url) => { fetched.push(url); return { ok: false, status: 599 }; }; + + // The resolver's own choice, learned from the real download path with no cache present, so the + // fixture never guesses which architecture this machine selects. + assert.equal(await downloader.downloadDep('webp', { lockContext }), false); + const selectedUrl = fetched.at(-1); + const selected = component.payloads.find((p) => p.url === selectedUrl); + assert.ok(selected, `no locked webp payload covers the selected url ${selectedUrl}`); + const foreign = component.payloads.find((p) => p.url !== selectedUrl); + assert.ok(foreign, 'webp must lock a second architecture for a wrong-payload receipt to exist'); + + const seed = (payloadIds) => { + rmSync(destDir, { recursive: true, force: true }); + mkdirSync(join(destDir, '.threenative-receipts'), { recursive: true }); + writeFileSync(join(destDir, 'libwebp.a'), 'installed'); + writeFileSync(join(destDir, '.threenative-receipts', 'receipt.json'), JSON.stringify({ + schemaVersion: 1, dependency: 'webp', lockHash: lockContext.lockHash, + payloads: payloadIds, files: ['libwebp.a'], + })); + }; + + seed([foreign.id]); + fetched.length = 0; + assert.equal(await downloader.downloadDep('webp', { lockContext }), false, + 'a receipt naming another payload under the same lock must miss'); + assert.deepEqual(fetched, [selectedUrl], 'the stale tree must be replaced by the selected payload'); + assert.equal(existsSync(join(destDir, 'libwebp.a')), false, 'replacement must not keep the foreign tree'); + + seed([selected.id]); + fetched.length = 0; + assert.equal(await downloader.downloadDep('webp', { lockContext }), true, + 'a receipt naming the selected payload must hit'); + assert.deepEqual(fetched, [], 'a cache hit must not download'); + assert.equal(existsSync(join(destDir, 'libwebp.a')), true, 'a cache hit must keep the installed tree'); + } finally { + globalThis.fetch = realFetch; + rmSync(root, { recursive: true, force: true }); + } +}); + test('should print usage and exit without touching caches for --help', () => { // Block acquisition before importing the downloader, even if the help guard regresses. const denyNetwork = ` diff --git a/packages/runtime-native/tests/frame_op_stream_replay_test.cpp b/packages/runtime-native/tests/frame_op_stream_replay_test.cpp index 9e98654695..d6b0f309e8 100644 --- a/packages/runtime-native/tests/frame_op_stream_replay_test.cpp +++ b/packages/runtime-native/tests/frame_op_stream_replay_test.cpp @@ -4,6 +4,7 @@ #include "../src/webgpu/bindings_state.h" #include +#include #include #include #include @@ -1000,7 +1001,98 @@ void runArrayLayerUploadContract() { "offset writeTexture lands layer 2's bytes in layer 2 and leaves layer 1 alone: " + observed); } +void runWriteBufferContract(bool direct, const char* proofBundle) { + mystral::RuntimeConfig config; + config.width = 1; + config.height = 1; + config.noSdl = true; + const auto runtime = mystral::Runtime::create(config); + if (!runtime || !runtime->getWebGPUBindingsState()) { + expect(false, "writeBuffer proof requires native WebGPU bindings"); + return; + } + auto* state = static_cast(runtime->getWebGPUBindingsState()); + auto* engine = state->engine; + state->profiling.disableFrameOpStreamForTesting = direct; + frameReplayBackendEntries = 0; + state->profiling.frameOpStreamNativeCallObserver = [](const char* operation) { + if (std::string(operation) == "writeBuffer") frameReplayBackendEntries += 1; + }; + std::ifstream input(proofBundle); + std::ostringstream source; + source << input.rdbuf(); + if (!input || source.str().empty()) { + expect(false, "writeBuffer proof bundle must exist and be nonempty"); + return; + } + expect(engine->evalScript(source.str().c_str(), proofBundle), "shared writeBuffer proof loaded"); + expect(engine->evalScript(R"JS((async () => { + try { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) throw new Error("missing native GPU adapter"); + const device = await adapter.requestDevice(); + await writeBufferProof.verifyQueueWriteBuffer(device); + if (typeof ArrayBuffer.prototype.transfer !== "function") { + throw new Error("ArrayBuffer.prototype.transfer is required to prove detached source rejection"); + } + // Desktop-focused arm only: the portable shared proof stays transfer-free because Android V8 + // cannot detach a buffer, so the detached-source contract is proved here on Dawn + V8. + const detachedBuffer = device.createBuffer({size: 16, usage: GPUBufferUsage.COPY_DST}); + try { + const detached = [ + ["ArrayBuffer", new ArrayBuffer(4)], + ["Uint8Array", new Uint8Array(4)], + ["DataView", new DataView(new ArrayBuffer(4))], + ]; + for (const [, detachedSource] of detached) { + if (detachedSource.buffer) detachedSource.buffer.transfer(); + else detachedSource.transfer(); + } + const accepted = []; + for (const [name, detachedSource] of detached) { + try { + device.queue.writeBuffer(detachedBuffer, 0, detachedSource); + accepted.push(name); + } catch {} + } + if (accepted.length) throw new Error(`detached sources were accepted: ${accepted.join(", ")}`); + globalThis.__tnDetachedRejected = detached.length; + } finally { + detachedBuffer.destroy(); + } + globalThis.__tnWriteBufferPassed = true; + } catch (error) { + globalThis.__tnWriteBufferError = String(error.stack || error); + } finally { + globalThis.__tnWriteBufferDone = true; + } + })())JS", "tn-write-buffer-proof.js"), "writeBuffer proof started"); + awaitFlag(runtime.get(), engine, "__tnWriteBufferDone"); + expect(engine->toBoolean(engine->getGlobalProperty("__tnWriteBufferPassed")), + "native writeBuffer proof: " + engine->toString(engine->getGlobalProperty("__tnWriteBufferError"))); + expect(direct ? !state->profiling.frameOpStreamDrain.ptr : !!state->profiling.frameOpStreamDrain.ptr, + "requested direct/stream transport was installed"); + if (!direct) { + expect(state->profiling.frameOpStreamReplayCrossings > 0, + "writeBuffer proof crossed the native frame replay decoder"); + expect(frameReplayBackendEntries == 7, + "decoder reached all five zero-byte and two nonzero production writeBuffer records: " + + std::to_string(frameReplayBackendEntries)); + std::cout << "Decoded writeBuffer records: " << frameReplayBackendEntries + << " (five zero-byte, two nonzero); GPU readback checked" << std::endl; + } +} + int main(int argc, char** argv) { + if (argc > 1 && (std::string(argv[1]) == "write-buffer-direct" || + std::string(argv[1]) == "write-buffer-stream")) { + if (argc != 3) { + std::cerr << "write-buffer-direct|write-buffer-stream requires the shared IIFE proof bundle" << std::endl; + return 1; + } + runWriteBufferContract(std::string(argv[1]) == "write-buffer-direct", argv[2]); + return failures == 0 ? 0 : 1; + } const bool disableStreamControl = argc > 1 && std::string(argv[1]) == "disabled-stream-control"; runContract(disableStreamControl); From 581d53f6b1cbbd35773a8e740682ade31faf955a Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Wed, 14 Oct 2026 23:48:30 -0700 Subject: [PATCH 04/14] test(native): align desktop driver and CLI split contracts --- packages/runtime-native/tests/desktop-core-gate.test.mjs | 8 +++++++- .../runtime-native/tests/runtime-next-contract.test.mjs | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/runtime-native/tests/desktop-core-gate.test.mjs b/packages/runtime-native/tests/desktop-core-gate.test.mjs index 94a8b46642..1b5654cb43 100644 --- a/packages/runtime-native/tests/desktop-core-gate.test.mjs +++ b/packages/runtime-native/tests/desktop-core-gate.test.mjs @@ -240,7 +240,13 @@ test('desktop verifier preserves evidence and scopes its SDL drivers to Linux', /if \(process\.platform === 'linux'\) \{\n([\s\S]*?)\n\x20{2}\}/u, )?.[1]; expect(linuxBranch).toBeDefined(); - expect(linuxBranch).toMatch(/runtimeEnv\.SDL_VIDEODRIVER = 'x11'/); + // The driver is the caller's, not a literal: a Wayland request has to reach the child, which is + // what src/cli/main.cpp reads before it refuses the web UI over Wayland. Both spellings are set, + // so the child resolves the driver this gate read; x11 stays the default inside + // requestedLinuxVideoDriver, which linux-video-driver-routing.spec.ts proves. + expect(linuxBranch).toMatch(/runtimeEnv\.SDL_VIDEO_DRIVER = requested\.driver/); + expect(linuxBranch).toMatch(/runtimeEnv\.SDL_VIDEODRIVER = requested\.driver/); + expect(linuxBranch).not.toMatch(/SDL_VIDEODRIVER = 'x11'/); // The audio driver is a *default*, not an override: this gate makes no audio assertion, and a // machine with a real device must keep using it. `verify-desktop-audio.mjs` owns the audio // contract and runs before this gate in the same command. diff --git a/packages/runtime-native/tests/runtime-next-contract.test.mjs b/packages/runtime-native/tests/runtime-next-contract.test.mjs index 6d72121113..4497954414 100644 --- a/packages/runtime-native/tests/runtime-next-contract.test.mjs +++ b/packages/runtime-native/tests/runtime-next-contract.test.mjs @@ -791,7 +791,9 @@ test('CLI build tools are separate units behind an unchanged dispatch surface', // tool body, so the cap moves with it rather than the split being loosened. PRD-400 adds runtime // web-UI readiness/deadline wiring here; the direct split assertions below remain the contract. // The native-css renderer adds 17 lines of the same kind: choosing which overlay backend attaches. - assert.ok(main.split('\n').length <= 1902, 'main.cpp still contains a build-time tool body'); + // PRD-614 adds 11 net lines: refusing the web UI when the environment asks SDL for Wayland, read from + // the same variables the desktop gate routes. + assert.ok(main.split('\n').length <= 1913, 'main.cpp still contains a build-time tool body'); assert.doesNotMatch(main, /static int (compileBundle|bakeLightmaps)\(/u); assert.match(main, /dispatchBuildTool\(argc, argv\)/u); assert.match(dispatcher, /mystral::vfs::getExecutablePath\(\)[\s\S]*mystral-tools/u); From a8b355190995c09adecb2278e408f3b2129f72e3 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Wed, 14 Oct 2026 23:57:39 -0700 Subject: [PATCH 05/14] build(native): produce pinned Dawn with Wayland and X11 --- .github/workflows/build-quiche-owned.yml | 59 ++++++++++++++++++ .../scripts/build-dawn-linux.mjs | 62 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 packages/runtime-native/scripts/build-dawn-linux.mjs diff --git a/.github/workflows/build-quiche-owned.yml b/.github/workflows/build-quiche-owned.yml index d63c38eb8a..f4537d9fc1 100644 --- a/.github/workflows/build-quiche-owned.yml +++ b/.github/workflows/build-quiche-owned.yml @@ -2,6 +2,12 @@ name: Build owned quiche dependency on: workflow_dispatch: + inputs: + dependency: + description: Owned dependency to build + type: choice + options: [quiche, dawn] + default: quiche pull_request: branches: - main @@ -38,6 +44,7 @@ concurrency: jobs: build: + if: github.event_name != 'workflow_dispatch' || inputs.dependency != 'dawn' strategy: fail-fast: false matrix: @@ -224,3 +231,55 @@ jobs: --repo "${{ github.repository }}" \ --verify-tag --title "Owned quiche ${{ github.ref_name }}" \ --generate-notes + + dawn: + if: github.event_name == 'workflow_dispatch' && inputs.dependency == 'dawn' + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v7 + - name: Build pinned Linux x64 Dawn in isolated manylinux container + run: node packages/runtime-native/scripts/build-dawn-linux.mjs + - uses: actions/upload-artifact@v4 + with: + name: dawn-linux-x64 + path: | + packages/runtime-native/artifacts/dawn-linux-x64/Dawn-*.tar.gz + packages/runtime-native/artifacts/dawn-linux-x64/manifest-linux-x64.json + if-no-files-found: error + + publish-dawn: + if: github.event_name == 'workflow_dispatch' && inputs.dependency == 'dawn' + needs: dawn + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dawn-linux-x64 + path: dist-dawn + - name: Publish immutable dependency cache + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + SOURCE_REF: ${{ github.sha }} + run: | + set -euo pipefail + ARCHIVE=dist-dawn/Dawn-d14ae3d97ad74100e9f382efef5e9c0872ddbeb2-ubuntu-latest-Release.tar.gz + test -s "$ARCHIVE" + test -s dist-dawn/manifest-linux-x64.json + python3 - <<'PY' + import hashlib, json, pathlib + root = pathlib.Path('dist-dawn') + manifest = json.loads((root / 'manifest-linux-x64.json').read_text()) + sha = 'd14ae3d97ad74100e9f382efef5e9c0872ddbeb2' + archive = f'Dawn-{sha}-ubuntu-latest-Release.tar.gz' + assert manifest['source_commit'] == manifest['upstream_commit'] == sha + assert manifest['expected_tag'] == 'dawn-linux-x64-v1' + assert manifest['target'] == 'linux-x64' and manifest['archive'] == archive + assert manifest['archive_sha256'] == hashlib.sha256((root / archive).read_bytes()).hexdigest() + PY + gh release create dawn-linux-x64-v1 "$ARCHIVE" dist-dawn/manifest-linux-x64.json \ + --target "$SOURCE_REF" --title "Dawn Linux x64 dependency cache v1" --generate-notes diff --git a/packages/runtime-native/scripts/build-dawn-linux.mjs b/packages/runtime-native/scripts/build-dawn-linux.mjs new file mode 100644 index 0000000000..00d339adda --- /dev/null +++ b/packages/runtime-native/scripts/build-dawn-linux.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +// Same Linux recipe as the pinned upstream ci.yml; no host packages or cleanup. +const sourceCommit = 'd14ae3d97ad74100e9f382efef5e9c0872ddbeb2'; +const container = 'dockcross/manylinux_2_28-x64@sha256:5c844b0636fc4837dfa06f7ea1790c7b6cf22790b1082c3b3410320fce145c6e'; +const topdir = `Dawn-${sourceCommit}-ubuntu-latest-Release`; +const flags = [ + '-C', '.github/workflows/dawn-ci.cmake', + '-DCMAKE_CXX_COMPILER=g++', '-DCMAKE_C_COMPILER=gcc', + '-DCMAKE_BUILD_TYPE=Release', '-DDAWN_USE_WAYLAND=ON', '-DDAWN_USE_X11=ON', +]; + +if (process.argv.length !== 2) throw new Error('Usage: node build-dawn-linux.mjs (no arguments)'); +const output = fileURLToPath(new URL('../artifacts/dawn-linux-x64/', import.meta.url)); +mkdirSync(fileURLToPath(new URL('../artifacts/', import.meta.url)), { recursive: true }); +// Refuse previous build trees rather than reusing stale CMake or dependency state. +mkdirSync(output); +const result = spawnSync('docker', [ + 'run', '--rm', '--platform', 'linux/amd64', + '--volume', `${output}:/work`, '--workdir', '/work', + '--entrypoint', '/bin/bash', container, '-euo', 'pipefail', '-c', ` +test "$(uname -m)" = x86_64 +git init source +cd source +git remote add origin https://github.com/google/dawn.git +git fetch --depth=1 origin ${sourceCommit} +git checkout --detach FETCH_HEAD +test "$(git rev-parse HEAD)" = ${sourceCommit} +dnf install -y mesa-libGL-devel libxcb libxcb-devel libX11-xcb libXcursor-devel libXrandr-devel libXinerama-devel libXi-devel libXext-devel libxkbcommon libxkbcommon-devel libxkbcommon-x11-devel mesa-vulkan-drivers wayland-protocols-devel wayland-devel +cmake -S . -B out/Release ${flags.join(' ')} +cmake --build out/Release --config Release --parallel 4 +cmake --install out/Release --config Release --prefix /work/${topdir} +test -s /work/${topdir}/include/webgpu/webgpu.h +test -s /work/${topdir}/include/webgpu/webgpu_cpp.h +test -s /work/${topdir}/lib64/libwebgpu_dawn.a || test -s /work/${topdir}/lib/libwebgpu_dawn.a +for library in /work/${topdir}/lib64/libwebgpu_dawn.a /work/${topdir}/lib/libwebgpu_dawn.a; do + if test -s "$library"; then strings "$library" > /work/dawn-symbols.txt; break; fi +done +for symbol in vkCreateWaylandSurfaceKHR vkCreateXcbSurfaceKHR vkCreateXlibSurfaceKHR; do + grep -Fx "$symbol" /work/dawn-symbols.txt +done +cd /work +cmake -E tar czf ${topdir}.tar.gz ${topdir} +`, +], { stdio: 'inherit' }); +if (result.error) throw result.error; +if (result.status !== 0) throw new Error(`Dawn container failed: ${result.status ?? result.signal}`); +const archive = `${topdir}.tar.gz`; +writeFileSync(`${output}/manifest-linux-x64.json`, `${JSON.stringify({ + artifact_revision: 1, + upstream_commit: sourceCommit, + source_commit: sourceCommit, + target: 'linux-x64', + expected_tag: 'dawn-linux-x64-v1', + toolchain: { container, flags, DAWN_FETCH_DEPENDENCIES: 'ON', DAWN_ENABLE_INSTALL: 'ON' }, + archive, + archive_sha256: createHash('sha256').update(readFileSync(`${output}/${archive}`)).digest('hex'), +}, null, 2)}\n`); From 12ff3d8871cd8562d06c187e06c2becc95fabc2e Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 00:01:15 -0700 Subject: [PATCH 06/14] test(native): verify Dawn producer failure and archive receipts --- .../tests/build-dawn-linux.test.mjs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/runtime-native/tests/build-dawn-linux.test.mjs diff --git a/packages/runtime-native/tests/build-dawn-linux.test.mjs b/packages/runtime-native/tests/build-dawn-linux.test.mjs new file mode 100644 index 0000000000..8fa7cabe21 --- /dev/null +++ b/packages/runtime-native/tests/build-dawn-linux.test.mjs @@ -0,0 +1,106 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { gzipSync } from "node:zlib"; +import { afterEach, test as vitestTest } from "vitest"; +import { makeTempDirSync } from "../../../test-support/temp-dir.js"; + +const test = vitestTest.skipIf(process.platform === "win32"); + +const commit = "d14ae3d97ad74100e9f382efef5e9c0872ddbeb2"; +const archive = `Dawn-${commit}-ubuntu-latest-Release.tar.gz`; +const bytes = gzipSync(Buffer.alloc(1024)); +const roots = []; +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function fixture() { + const root = makeTempDirSync("tn-dawn-producer-"); + roots.push(root); + mkdirSync(join(root, "scripts")); + mkdirSync(join(root, "bin")); + const script = join(root, "scripts/build-dawn-linux.mjs"); + cpSync(new URL("../scripts/build-dawn-linux.mjs", import.meta.url), script); + const marker = join(root, "docker-called"); + writeFileSync(join(root, "bin/docker"), `#!${process.execPath} +const fs = require('node:fs'); +fs.writeFileSync(${JSON.stringify(marker)}, 'called'); +if (process.env.TN_TEST_DOCKER_FAIL) process.exit(23); +const args = process.argv.slice(2); +const volume = args[args.indexOf('--volume') + 1]; +if (!volume.endsWith(':/work')) process.exit(24); +if (!process.env.TN_TEST_DOCKER_NO_ARCHIVE) + fs.writeFileSync(require('node:path').join(volume.slice(0, -6), ${JSON.stringify(archive)}), Buffer.from(${JSON.stringify(bytes.toString("base64"))}, 'base64')); +`, { mode: 0o755 }); + const output = join(root, "artifacts/dawn-linux-x64"); + const manifest = join(output, "manifest-linux-x64.json"); + const run = (args = [], env = {}) => spawnSync(process.execPath, [script, ...args], { + cwd: root, encoding: "utf8", timeout: 10_000, + env: { ...process.env, PATH: join(root, "bin"), ...env }, + }); + return { run, output, manifest, marker }; +} + +test("rejects arguments before invoking Docker or creating artifacts", () => { + const f = fixture(); + const result = f.run(["--unexpected"]); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /Usage:/u); + assert.equal(existsSync(f.output), false); + assert.equal(existsSync(f.marker), false); +}); + +test("propagates Docker failure without publishing a success manifest", () => { + const f = fixture(); + const result = f.run([], { TN_TEST_DOCKER_FAIL: "1" }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /Dawn container failed: 23/u); + assert.equal(existsSync(f.marker), true); + assert.equal(existsSync(f.manifest), false); +}); + +test("refuses success when Docker returns without an archive", () => { + const f = fixture(); + const result = f.run([], { TN_TEST_DOCKER_NO_ARCHIVE: "1" }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /ENOENT/u); + assert.equal(existsSync(f.marker), true); + assert.equal(existsSync(f.manifest), false); +}); + +test("hashes the actual synthetic archive and records pinned source and target identity", () => { + const f = fixture(); + const result = f.run(); + assert.equal(result.status, 0, result.stderr); + const actualBytes = readFileSync(join(f.output, archive)); + assert.deepEqual(actualBytes, bytes); + assert.deepEqual(JSON.parse(readFileSync(f.manifest, "utf8")), { + artifact_revision: 1, + upstream_commit: commit, + source_commit: commit, + target: "linux-x64", + expected_tag: "dawn-linux-x64-v1", + toolchain: { + container: "dockcross/manylinux_2_28-x64@sha256:5c844b0636fc4837dfa06f7ea1790c7b6cf22790b1082c3b3410320fce145c6e", + flags: ["-C", ".github/workflows/dawn-ci.cmake", "-DCMAKE_CXX_COMPILER=g++", "-DCMAKE_C_COMPILER=gcc", "-DCMAKE_BUILD_TYPE=Release", "-DDAWN_USE_WAYLAND=ON", "-DDAWN_USE_X11=ON"], + DAWN_FETCH_DEPENDENCIES: "ON", + DAWN_ENABLE_INSTALL: "ON", + }, + archive, + archive_sha256: createHash("sha256").update(actualBytes).digest("hex"), + }); +}); + +test("refuses an existing output directory without overwriting prior artifacts", () => { + const f = fixture(); + mkdirSync(f.output, { recursive: true }); + writeFileSync(f.manifest, "previous artifact"); + const result = f.run(); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /EEXIST/u); + assert.equal(readFileSync(f.manifest, "utf8"), "previous artifact"); + assert.equal(existsSync(f.marker), false); +}); From 538b8f01367b8e4c56f38633d134abf7c62c3780 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 00:05:27 -0700 Subject: [PATCH 07/14] docs(prd614): record rebuilt X11 qualification --- ...PRD-614-mystralnative-upstream-catch-up-v0.1.6.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 7faf252743..6a6ba225e3 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -64,7 +64,7 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Acceptance Criteria - [ ] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — Evidence: pending. -- [ ] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — Evidence: pending. +- [x] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — PASS 2026-10-15 on the rebuilt candidate, observed SDL driver `x11`, 300 nonblank frames; complete audio/stability/physics/native-contract/loading/UI/bundle command exits 0. - [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), RED then GREEN on both the direct binding and frame-op stream paths — Evidence: pending. - [ ] AC-4 [shared]: The dispatched `native-platforms.yml` linux-arm64 leg is GREEN with Canvas 2D enabled, arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. proof: `gh workflow run native-platforms.yml --ref ` followed by `gh run view ` confirming the linux-arm64 job and dependency/build logs on the candidate SHA — Evidence: pending. @@ -95,14 +95,14 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Execution Phases #### Phase 1: Wayland surface -**Status:** PARTIAL — candidate CLI compiles and two default-X11 core captures pass; actual Wayland selects the correct driver but the current Dawn library rejects its descriptor. Full candidate commands and Wayland presentation remain unproved. +**Status:** PARTIAL — the normal candidate build and complete default-X11 verification pass; actual Wayland selects the correct driver but the current Dawn library rejects its descriptor. Wayland presentation and the visual judge remain pending. **Files:** `include/mystral/webgpu/context.h`, `src/webgpu/context.cpp`, `include/mystral/webgpu_compat.h`, `src/runtime.cpp`, `src/platform/window.cpp`, `src/cli/main.cpp`, `scripts/verify-desktop-core.mjs`, `scripts/xvfb.sh`, `__tests__/linux-video-driver-routing.spec.ts` (all under `packages/runtime-native/`) -**Implementation:** Port `6eeb33b` and `4e01a6e` onto the fork's versions of these files. Delete `createSurfaceWithDisplay`. Keep the X11 default hints. -- [ ] Native desktop build compiles with the Wayland descriptor. proof: `pnpm native:build` -- [ ] AC-2 default X11 run. proof: `pnpm native:verify:desktop` +**Implementation:** Port the surface behavior from `6eeb33b` and `4e01a6e` through the existing `createSurfaceWithDisplay` platform discriminator. Keep X11 as the default and preserve Dawn as the Linux x64 backend. +- [x] Native desktop build compiles with the Wayland descriptor. proof: `pnpm native:build` — PASS 2026-10-15, 384 targets, actual CLI and tools linked. +- [x] AC-2 default X11 run. proof: `pnpm native:verify:desktop` — PASS 2026-10-15, complete command exits 0; two preceding same-pose core captures also pass 300/nonblank. - [ ] AC-1 Wayland run. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` -Candidate evidence (2026-10-14): `cmake --build packages/runtime-native/build/tn-linux --target mystral -j2` exits 0. Two `verify-desktop-core.mjs` runs report observed driver `x11`, 300 frames and non-blank 1280×720 captures (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run logs `SDL video driver: wayland` and `Creating Wayland surface`, then fails before frames: Dawn reports `Unsupported sType (SurfaceSourceWaylandSurface)` and zero surface formats (`artifacts/prd614-wayland-first-failure-runtime.log`). No unchanged retry or passing Wayland claim. The signed user-space Weston compositor passed socket/shm/EGL smoke; its owned process was stopped after this run. The existing wgpu profile and compatible Dawn artifacts are being inspected before another presentation attempt. +Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two same-pose core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. Actual replacement-library presentation remains unverified. #### Phase 2: Zero-length `writeBuffer` **Status:** PARTIAL — direct binding fixed and focused native RED→GREEN proved; decoder already correct; required parity RED→GREEN remains incomplete. From 2d0f39f7e4372126c9a0c41e383554a323886807 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 00:48:17 -0700 Subject: [PATCH 08/14] fix(native): keep Dawn cache prerelease and record arm64 proof --- .github/workflows/build-quiche-owned.yml | 3 ++- .../PRD-614-mystralnative-upstream-catch-up-v0.1.6.md | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-quiche-owned.yml b/.github/workflows/build-quiche-owned.yml index f4537d9fc1..7007f187ca 100644 --- a/.github/workflows/build-quiche-owned.yml +++ b/.github/workflows/build-quiche-owned.yml @@ -282,4 +282,5 @@ jobs: assert manifest['archive_sha256'] == hashlib.sha256((root / archive).read_bytes()).hexdigest() PY gh release create dawn-linux-x64-v1 "$ARCHIVE" dist-dawn/manifest-linux-x64.json \ - --target "$SOURCE_REF" --title "Dawn Linux x64 dependency cache v1" --generate-notes + --target "$SOURCE_REF" --title "Dawn Linux x64 dependency cache v1" \ + --prerelease --latest=false --generate-notes diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 6a6ba225e3..6e8bcb9142 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -66,7 +66,7 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS - [ ] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — Evidence: pending. - [x] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — PASS 2026-10-15 on the rebuilt candidate, observed SDL driver `x11`, 300 nonblank frames; complete audio/stability/physics/native-contract/loading/UI/bundle command exits 0. - [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), RED then GREEN on both the direct binding and frame-op stream paths — Evidence: pending. -- [ ] AC-4 [shared]: The dispatched `native-platforms.yml` linux-arm64 leg is GREEN with Canvas 2D enabled, arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. proof: `gh workflow run native-platforms.yml --ref ` followed by `gh run view ` confirming the linux-arm64 job and dependency/build logs on the candidate SHA — Evidence: pending. +- [x] AC-4 [shared]: The dispatched `native-platforms.yml` linux-arm64 leg is GREEN with Canvas 2D enabled, arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. proof: [run 38695846954](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695846954), successful job `116292229770` on `a8b355190995c09adecb2278e408f3b2129f72e3`; actual configure, verified locked payloads and 300-frame starter artifacts pass. ## Blocked on @@ -126,9 +126,11 @@ packages/runtime-native/build/tn-linux/threenative-frame-op-stream-replay-test w ``` #### Phase 3: linux-arm64 proof, then provenance -**Status:** NOT STARTED +**Status:** PARTIAL — required linux-arm64 job passes with the specified dependencies and Canvas 2D; NOTICE waits for AC-1 and AC-3. **Files:** Native dependency/build scripts, arm64 Skia/Canvas 2D configuration, existing `.github/workflows/native-platforms.yml` only if required, and `packages/runtime-native/NOTICE`. **Implementation:** Adapt the upstream linux-arm64 changes to the existing leg; include arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64 with Canvas 2D enabled. Dispatch the workflow against the candidate branch and require its linux-arm64 leg to be GREEN. Only after AC-1 through AC-4 pass, set `Upstream provenance baseline:` to the full hash and name v0.1.6. -- [ ] linux-arm64 dependency/build configuration includes arm64 Skia, quiche `0.24.6-4`, SWC `swc-41`, libwebp aarch64 and Canvas 2D enabled. proof: dispatched `native-platforms.yml` linux-arm64 dependency/build logs on the candidate SHA -- [ ] AC-4 dispatched linux-arm64 leg is GREEN. proof: `gh workflow run native-platforms.yml --ref ` + `gh run view ` (successful linux-arm64 job on the candidate SHA) +- [x] linux-arm64 dependency/build configuration includes arm64 Skia, quiche `0.24.6-4`, SWC `swc-41`, libwebp aarch64 and Canvas 2D enabled. proof: actual job `116292229770` configures `TN_ENABLE_CANVAS2D="ON"`, resolves `Release/arm64/libskia.a` and aarch64 libwebp, and verifies `quiche-linux-aarch64` / `swc-linux-aarch64` against the candidate lock. +- [x] AC-4 dispatched linux-arm64 leg is GREEN. proof: [run 38695846954](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695846954), candidate `a8b355190995c09adecb2278e408f3b2129f72e3`, attempt 1, job `116292229770` concludes success; starter and container reports each pass 300 frames and nonblank captures. - [ ] After AC-1 through AC-4 are green, NOTICE names `bafab09d599bd55aeb7433ba7131c3f320334c99` and v0.1.6. proof: `git diff -- packages/runtime-native/NOTICE` + `pnpm check:docs` + +Verified CI evidence (2026-10-15): the exact-candidate receipt binds the successful job, build configuration and reports. The starter capture has 13,117 colors and the container capture 13,145; movement, collection/restart, visibility and diagnostics assertions pass. Log lines 2861/2870 verify the locked SWC/quiche arm64 payload IDs; their candidate lock URLs are `swc-41/swc-linux-arm64.zip` and `quiche-0.24.6-4/quiche-linux-arm64.zip`. The preceding titles print unchanged x64 default version strings, which are cosmetic and do not identify the verified archives. This proves the required arm64 leg, not the still-running whole native matrix. From e0b35d8fed58a95edbf60dda7eee4a232bce3f3a Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 00:56:09 -0700 Subject: [PATCH 09/14] fix(native): pin verified Wayland-enabled Dawn for Linux --- ...-mystralnative-upstream-catch-up-v0.1.6.md | 22 +++++++++---------- packages/runtime-native/native-deps.lock.json | 8 +++---- .../runtime-native/scripts/download-deps.mjs | 8 ++++--- .../tests/dependency-provenance.test.mjs | 6 ++--- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 6e8bcb9142..6e6512fff4 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -95,27 +95,25 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Execution Phases #### Phase 1: Wayland surface -**Status:** PARTIAL — the normal candidate build and complete default-X11 verification pass; actual Wayland selects the correct driver but the current Dawn library rejects its descriptor. Wayland presentation and the visual judge remain pending. +**Status:** PARTIAL — the normal candidate build and complete default-X11 verification pass; the original Dawn library rejected the correctly selected Wayland descriptor. A verified same-revision replacement is now pinned; rebuilt Wayland presentation and the visual judge remain pending. **Files:** `include/mystral/webgpu/context.h`, `src/webgpu/context.cpp`, `include/mystral/webgpu_compat.h`, `src/runtime.cpp`, `src/platform/window.cpp`, `src/cli/main.cpp`, `scripts/verify-desktop-core.mjs`, `scripts/xvfb.sh`, `__tests__/linux-video-driver-routing.spec.ts` (all under `packages/runtime-native/`) **Implementation:** Port the surface behavior from `6eeb33b` and `4e01a6e` through the existing `createSurfaceWithDisplay` platform discriminator. Keep X11 as the default and preserve Dawn as the Linux x64 backend. - [x] Native desktop build compiles with the Wayland descriptor. proof: `pnpm native:build` — PASS 2026-10-15, 384 targets, actual CLI and tools linked. -- [x] AC-2 default X11 run. proof: `pnpm native:verify:desktop` — PASS 2026-10-15, complete command exits 0; two preceding same-pose core captures also pass 300/nonblank. +- [x] AC-2 default X11 run. proof: `pnpm native:verify:desktop` — PASS 2026-10-15, complete command exits 0; two preceding core captures also pass 300/nonblank. - [ ] AC-1 Wayland run. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` -Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two same-pose core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. Actual replacement-library presentation remains unverified. +Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library presentation remains unverified. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; recapture will use identical fixed scene bytes on actual baseline and final candidate binaries, two runs each, before another judgment. #### Phase 2: Zero-length `writeBuffer` -**Status:** PARTIAL — direct binding fixed and focused native RED→GREEN proved; decoder already correct; required parity RED→GREEN remains incomplete. -**Files:** `src/webgpu/bindings.cpp`, `src/webgpu/bindings_frame_stream.cpp`, `conformance/registry.json` and its new case -**Implementation:** Add the conformance case first and see it fail on native. Then return `undefined` for `dataSize == 0` in both paths. -- [ ] AC-3 direct binding path, red then green. proof: `pnpm parity` new case -- [ ] AC-3 frame-op stream path, red then green. proof: `pnpm parity` new case with the frame-op stream on +**Status:** PARTIAL — direct binding RED→GREEN and rebuilt desktop qualification pass; the decoder already accepts valid zero-byte records, so its requested RED is unmet. +**Files:** `src/webgpu/bindings.cpp`, `conformance/registry.json`, the shared conformance proof and existing native replay harness. +**Implementation:** Validate genuine sources and ranges before the empty-write no-op; preserve malformed and detached-source rejection. Exercise the real frame-op decoder without manufacturing a failure. +- [x] AC-3 direct binding path, red then green. proof: focused native Dawn+V8/Vulkan harness exits 1 on genuine empty sources before the fix and 0 after; nonzero readback and invalid-source controls pass. +- [ ] AC-3 frame-op stream path, red then green. proof: actual production encoder supplies five zero-byte and two nonzero records; all seven reach the decoder and pass both before and after. No genuine RED exists. -Test-first evidence (2026-10-14): focused `threenative-frame-op-stream-replay-test` built in this checkout with Dawn+V8, native Vulkan NVIDIA RTX 2080. Direct mode exits 1 solely on empty Uint8Array, ArrayBuffer and DataView (`writeBuffer: invalid data`); stream mode exits 0 with a native replay crossing. Both execute the same shared proof: five zero-size writes, three invalid-source rejections, and GPU readback `[11,22,55,66]` after nonzero writes. No runtime fix yet; the stream already accepts these inputs. Logs: `packages/runtime-native/artifacts/conformance/prd614-zero-{direct,stream}.log`. Registry dry-run validates the selected row (exit 2 because unselected rows remain blocked); JS syntax, focused Biome check and diff whitespace checks pass. Conformance runner/report specs pass: 2 files, 88 tests, 49.06 s (`pnpm exec vitest run tests/conformance-runner.test.mjs tests/conformance-report.test.mjs --reporter=dot` from `packages/runtime-native`). +Verified evidence (2026-10-15): the shared proof covers five zero-size writes, seven invalid-source rejections and GPU readback `[11,22,55,66]`. Direct RED→GREEN logs are `packages/runtime-native/artifacts/conformance/prd614-zero-direct-{before,after}.log`; stream before/after logs use `prd614-zero-stream-{before,after}.log` in the same directory. Genuine detached ArrayBuffer/TypedArray/DataView assertions separately exposed and fixed an empty-source guard regression, with direct RED→GREEN and stream PASS/PASS (`prd614-detached-{direct,stream}-{before,after}.log`). These transfer-based checks stay in the desktop harness so the shared conformance case remains portable. The final focused direct, stream and default replay suite all exit 0 (`prd614-portable-{direct,stream,replay-default}.log`). -Focused continuation (2026-10-14): direct binding now accepts genuine empty ArrayBuffer, TypedArray and DataView sources, validates source ranges before returning, and avoids null-pointer arithmetic on no-ops. Built-in byteLength getters reject spoofed `{ byteLength: 0 }`; the shared proof now checks seven invalid sources, five valid zero-size writes, and nonzero GPU readback `[11,22,55,66]`. Focused own-checkout Dawn+V8/Vulkan test: direct **RED exit 1 → GREEN exit 0**; stream **PASS exit 0 before and after**. Production encoder emits zero-byte records (no elision); the new backend observer counts exactly seven decoded writeBuffer records, proving all five zero-byte records reached the actual decoder. Decoder already accepts zero payloads with valid bounds; no decoder runtime edit and no invented RED. Both AC-3 phase boxes remain unchecked because the required parity RED→GREEN on both paths has not been met. Logs: `packages/runtime-native/artifacts/conformance/prd614-zero-{direct,stream}-{before,after}.log`; focused build: `prd614-zero-green-build.log` in the same directory. `pnpm parity --target web --only-tests 91-queue-write-buffer-zero --out artifacts/conformance/prd614-zero-web` passes the selected row on headed Chromium WebGPU (NVIDIA/turing adapter; zero GPU validation errors), **exit 2** because the other 107 rows remain blocked. Report/capture: `packages/runtime-native/artifacts/conformance/prd614-zero-web/`. Focused Biome passes with one existing complexity warning; `git diff --check` passes. No full workspace build, commit, push, or AC tick. - -Detached-source continuation (2026-10-14): a detached `ArrayBuffer` or TypedArray reports `byteLength` 0 (ECMA-262 25.1.5.3), so the empty-source guard accepted it as a no-op — a security regression, not the AC-3 behaviour. The check is desktop-focused and now lives in `runWriteBufferContract`'s async JS proof right after `verifyQueueWriteBuffer(device)`, on its own 16-byte `COPY_DST` buffer destroyed in a `finally`; it detaches a real `ArrayBuffer`, `Uint8Array` and `DataView` with `ArrayBuffer.prototype.transfer`, throws when `transfer` is absent, and fails if any detached source is accepted. The shared portable helper is transfer-free, because Android V8 cannot detach a buffer and the parity row would throw there; its empty, nonempty, range and readback checks are unchanged, and the seven-record decoder count is unchanged because rejected detached writes emit no record. Focused own-checkout Dawn+V8/Vulkan test: direct **RED exit 1** (`detached ArrayBuffer source was accepted; detached Uint8Array source was accepted`) before the guard change, **GREEN exit 0** after; stream exit 0 before and after with `Decoded writeBuffer records: 7 (five zero-byte, two nonzero)` on the valid path only. The stream transport already rejected detached sources before the decoder, so no stream RED exists. The default no-argument replay-test suite still exits 0. Rerun after the split, against a freshly rebundled shared proof: direct **exit 0**, stream **exit 0** with `Decoded writeBuffer records: 7`, default suite **exit 0**; logs `packages/runtime-native/artifacts/conformance/prd614-portable-{direct,stream,replay-default}.log`. Guard change: the intrinsic brand getter runs first, then a probe that throws on a detached receiver — `new Uint8Array(data)` for ArrayBuffer and `%TypedArray%.prototype.values.call(data)` for TypedArray; `ArrayBuffer.prototype.slice` is not used because it runs a species lookup on live receivers. QuickJS `JS_GetArrayBuffer` sets a pending `TypeError` for detached input (`quickjs.c:54120-54122`), but only the detached path reaches it and that path now always throws a fresh `writeBuffer: invalid data`, so the leftover never escapes. Logs: `packages/runtime-native/artifacts/conformance/prd614-detached-{direct,stream}-{before,after}.log`, build `prd614-detached-green-build.log`, suite `prd614-detached-replay-default.log`. No AC tick, commit or push. `THREENATIVE_RUNTIME_BINARY="$PWD/packages/runtime-native/build/tn-linux/mystral" pnpm parity --target desktop --only-tests 91-queue-write-buffer-zero --reference artifacts/conformance/prd614-zero-web --out artifacts/conformance/prd614-zero-desktop` passes the selected row, **exit 2** with 107 unselected rows blocked. Report/capture: `packages/runtime-native/artifacts/conformance/prd614-zero-desktop/`; raw runner log: `packages/runtime-native/artifacts/conformance/prd614-zero-desktop-run.log`. This intentionally uses the pre-existing own-checkout runtime executable (not relinked by the focused test-target build); it validates the existing stream path, not the new direct-binding fix. The newly linked focused replay-test binary supplies the direct-fix GREEN. Other native verification remained untouched. Full desktop executable qualification of the direct fix and other platform rows remain unverified. +The rebuilt actual CLI passes full `pnpm native:verify:desktop` (exit 0; `/tmp/prd614-x11-candidate-full.log`). The selected new parity row passes both headed Chromium WebGPU (NVIDIA/turing) and the rebuilt desktop host; each runner exits 2 because 107 unselected rows are blocked, not because the selected row fails. Final reports are `packages/runtime-native/artifacts/conformance/prd614-zero-web-final/report.json` and `prd614-zero-desktop-candidate/report.json`; desktop raw log is `/tmp/prd614-zero-desktop-candidate.log`. AC-3 overall remains open: a passing baseline decoder cannot supply the requested stream RED. Reproduce from repository root: ```sh diff --git a/packages/runtime-native/native-deps.lock.json b/packages/runtime-native/native-deps.lock.json index 2349d1c7ed..aa355322fa 100644 --- a/packages/runtime-native/native-deps.lock.json +++ b/packages/runtime-native/native-deps.lock.json @@ -225,13 +225,13 @@ "payloads": [ { "id": "dawn-ubuntu", - "url": "https://github.com/google/dawn/releases/download/v20260117.152313/Dawn-d14ae3d97ad74100e9f382efef5e9c0872ddbeb2-ubuntu-latest-Release.tar.gz", - "sha256": "34f042447836b2d36204b05cbc76ec8186eedf9c58fbcdca8c1f0607d8b9826d", - "bytes": 28197558, + "url": "https://github.com/ThreeNativeHQ/threenative-engine/releases/download/dawn-linux-x64-v1/Dawn-d14ae3d97ad74100e9f382efef5e9c0872ddbeb2-ubuntu-latest-Release.tar.gz", + "sha256": "bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530", + "bytes": 28193047, "targets": [ "desktop-linux" ], - "bootstrap": "two-run-fetch-2026-09-11" + "bootstrap": "github-release-digest-2026-10-15" }, { "id": "dawn-macos-arm64", diff --git a/packages/runtime-native/scripts/download-deps.mjs b/packages/runtime-native/scripts/download-deps.mjs index 2f88a3c207..cf458d1879 100644 --- a/packages/runtime-native/scripts/download-deps.mjs +++ b/packages/runtime-native/scripts/download-deps.mjs @@ -146,7 +146,7 @@ const DEPS = { extractTo: 'sdl3-ios', }, dawn: { - // Dawn prebuilts from official releases: https://github.com/google/dawn/releases + // Official Dawn prebuilts; Linux uses our same-revision Wayland+X11 dependency cache. // Naming: Dawn-{commit}-{platform}-Release.tar.gz // Headers: dawn-headers-{commit}.tar.gz // @@ -163,7 +163,7 @@ const DEPS = { const variant = ARCH === 'arm64' ? 'macos-latest' : 'macos-15-intel'; return `https://github.com/google/dawn/releases/download/${DEPS.dawn.version}/Dawn-${commit}-${variant}-Release.tar.gz`; }if (platformName === 'linux') { - return `https://github.com/google/dawn/releases/download/${DEPS.dawn.version}/Dawn-${commit}-ubuntu-latest-Release.tar.gz`; + return `https://github.com/ThreeNativeHQ/threenative-engine/releases/download/dawn-linux-x64-v1/Dawn-${commit}-ubuntu-latest-Release.tar.gz`; }if (platformName === 'windows') { return `https://github.com/google/dawn/releases/download/${DEPS.dawn.version}/Dawn-${commit}-windows-latest-Release.tar.gz`; } @@ -1249,7 +1249,7 @@ export async function downloadDep(name, options = {}) { return false; } - console.log(`\n=== Downloading ${name} ${dep.version} ===`); + console.log(`\n=== Downloading ${name} ===`); const lock = options.lockContext ?? readNativeDepsLockContext(); if (name === 'v8-android') { @@ -1332,6 +1332,8 @@ export async function downloadDep(name, options = {}) { return false; } + console.log(`Selected locked payload ${selectedPayload.id}: ${url}`); + // Check if already downloaded if (existsSync(destDir)) { console.log(`${name} already exists at ${destDir}`); diff --git a/packages/runtime-native/tests/dependency-provenance.test.mjs b/packages/runtime-native/tests/dependency-provenance.test.mjs index 11812d8938..de9a95b3da 100644 --- a/packages/runtime-native/tests/dependency-provenance.test.mjs +++ b/packages/runtime-native/tests/dependency-provenance.test.mjs @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; +import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { test } from 'vitest'; +import { makeTempDirSync } from '../../../test-support/temp-dir.js'; const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const LOCK_PATH = join(PACKAGE_ROOT, 'native-deps.lock.json'); @@ -158,7 +158,7 @@ test('a same-lock receipt naming another payload must not count as a cache hit', // An x64 checkout that selects the arm64 payload shares the lock hash, so a lock-hash-only check // reports a hit and the arm64 build keeps linking foreign-architecture quiche/swc/webp. The // selected payload id belongs to the hit; a receipt without it is the stale-cache path. - const root = mkdtempSync(join(tmpdir(), 'tn-dep-cache-')); + const root = makeTempDirSync('tn-dep-cache-'); const realFetch = globalThis.fetch; try { // The real downloader in an isolated package root: destDir is derived from the module's own From bccd65166b7272ffacc1daf23d624159116206ad Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 02:27:43 -0700 Subject: [PATCH 10/14] test(native): record final Wayland and visual qualification --- .../PRD-614-mystralnative-upstream-catch-up-v0.1.6.md | 10 +++++----- docs/verification/native-coverage-2026-08-28.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 6e6512fff4..4c18639cca 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -63,7 +63,7 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Acceptance Criteria -- [ ] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — Evidence: pending. +- [x] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — PASS 2026-10-15: complete command exits 0, observed Wayland, 300 nonblank frames; independent fixed-pose judge NEUTRAL on both runs. - [x] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — PASS 2026-10-15 on the rebuilt candidate, observed SDL driver `x11`, 300 nonblank frames; complete audio/stability/physics/native-contract/loading/UI/bundle command exits 0. - [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), RED then GREEN on both the direct binding and frame-op stream paths — Evidence: pending. - [x] AC-4 [shared]: The dispatched `native-platforms.yml` linux-arm64 leg is GREEN with Canvas 2D enabled, arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. proof: [run 38695846954](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695846954), successful job `116292229770` on `a8b355190995c09adecb2278e408f3b2129f72e3`; actual configure, verified locked payloads and 300-frame starter artifacts pass. @@ -95,14 +95,14 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Execution Phases #### Phase 1: Wayland surface -**Status:** PARTIAL — the normal candidate build and complete default-X11 verification pass; the original Dawn library rejected the correctly selected Wayland descriptor. A verified same-revision replacement is now pinned; rebuilt Wayland presentation and the visual judge remain pending. +**Status:** COMPLETE — the normal build and complete default-X11 and explicit-Wayland commands pass on the same candidate. The pinned same-revision Dawn replacement preserves both surfaces; independent fixed-pose visual judgment is NEUTRAL. **Files:** `include/mystral/webgpu/context.h`, `src/webgpu/context.cpp`, `include/mystral/webgpu_compat.h`, `src/runtime.cpp`, `src/platform/window.cpp`, `src/cli/main.cpp`, `scripts/verify-desktop-core.mjs`, `scripts/xvfb.sh`, `__tests__/linux-video-driver-routing.spec.ts` (all under `packages/runtime-native/`) **Implementation:** Port the surface behavior from `6eeb33b` and `4e01a6e` through the existing `createSurfaceWithDisplay` platform discriminator. Keep X11 as the default and preserve Dawn as the Linux x64 backend. - [x] Native desktop build compiles with the Wayland descriptor. proof: `pnpm native:build` — PASS 2026-10-15, 384 targets, actual CLI and tools linked. - [x] AC-2 default X11 run. proof: `pnpm native:verify:desktop` — PASS 2026-10-15, complete command exits 0; two preceding core captures also pass 300/nonblank. -- [ ] AC-1 Wayland run. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` +- [x] AC-1 Wayland run and visual preservation. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` exits 0; fresh independent judge returns NEUTRAL on every fixed-pose pair and repeatability set. -Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library presentation remains unverified. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; recapture will use identical fixed scene bytes on actual baseline and final candidate binaries, two runs each, before another judgment. +Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library Wayland presentation now passes 300 frames and a nonblank 1280×720 capture with 4,096 overlay pixels (`artifacts/prd614-wayland-gl-smoke.png`, `/tmp/prd614-wayland-gl-smoke.log`): observed SDL driver `wayland`, NVIDIA RTX 2080/Vulkan, 12 surface formats and 301 presents. Weston remains headless with its supported `--renderer=gl`, using NVIDIA EGL and DMA-BUF modifiers; its default no-op renderer exposed zero formats. The exact full `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` command exits 0 (`/tmp/prd614-wayland-gl-final-full.log`), including all 50 native contracts, audio/stability/physics/loading/UI and nine external-bundle tests. Its core reports actual driver `wayland`, 300 frames, nonblank 1280×720 and 4,096 overlay pixels. Default full X11 also exits 0 on the same candidate (`/tmp/prd614-x11-dawn-final-full.log`). Final-source `pnpm typecheck && pnpm lint` exits 0; lint reports existing warnings without fixes. Canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0: 47 targets executed, three configured blockers reported, 78.40% total coverage and every existing floor met. The generated record binds source digest `6fd6a85360ae1c1cd2016f6daf241e8a892e616ee6041d5d9f0c091d14c2a0a3`. The final full unit suite remains pending. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; that failure is resolved by deterministic recapture on actual baseline and final candidate binaries. Identical scene bytes (`b72c7292bab250349fc7bc0ce234c2507182d2a6ecfc285c27e30770c7cd8b73`) freeze only the two cube rotation updates. Two runs each of baseline X11, final X11 and final Wayland pass 300 frames/nonblank/overlay checks. The fresh independent judge returns NEUTRAL for all four before/after pairs, all three repeatability sets and overall: pose, silhouette, position, color/exposure, overlap and overlay are preserved. Labelled images are published on the existing orphan `prd614-screenshots` branch under `fixed-pose/`; original failed-pose images remain available. Fresh generated-coverage and dependency-SBOM contracts pass 13/13. #### Phase 2: Zero-length `writeBuffer` **Status:** PARTIAL — direct binding RED→GREEN and rebuilt desktop qualification pass; the decoder already accepts valid zero-byte records, so its requested RED is unmet. @@ -124,7 +124,7 @@ packages/runtime-native/build/tn-linux/threenative-frame-op-stream-replay-test w ``` #### Phase 3: linux-arm64 proof, then provenance -**Status:** PARTIAL — required linux-arm64 job passes with the specified dependencies and Canvas 2D; NOTICE waits for AC-1 and AC-3. +**Status:** PARTIAL — required linux-arm64 job passes with the specified dependencies and Canvas 2D; NOTICE waits for the unmet AC-3 stream RED. **Files:** Native dependency/build scripts, arm64 Skia/Canvas 2D configuration, existing `.github/workflows/native-platforms.yml` only if required, and `packages/runtime-native/NOTICE`. **Implementation:** Adapt the upstream linux-arm64 changes to the existing leg; include arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64 with Canvas 2D enabled. Dispatch the workflow against the candidate branch and require its linux-arm64 leg to be GREEN. Only after AC-1 through AC-4 pass, set `Upstream provenance baseline:` to the full hash and name v0.1.6. - [x] linux-arm64 dependency/build configuration includes arm64 Skia, quiche `0.24.6-4`, SWC `swc-41`, libwebp aarch64 and Canvas 2D enabled. proof: actual job `116292229770` configures `TN_ENABLE_CANVAS2D="ON"`, resolves `Release/arm64/libskia.a` and aarch64 libwebp, and verifies `quiche-linux-aarch64` / `swc-linux-aarch64` against the candidate lock. diff --git a/docs/verification/native-coverage-2026-08-28.md b/docs/verification/native-coverage-2026-08-28.md index c39a4e022d..622802aeb2 100644 --- a/docs/verification/native-coverage-2026-08-28.md +++ b/docs/verification/native-coverage-2026-08-28.md @@ -14,19 +14,19 @@ targets could not be built and are named below. | `src/fs/` | 235 | 189 | 80.43% | | `src/http/` | 410 | 377 | 91.95% | | `src/js/` | 2785 | 2212 | 79.43% | -| `src/platform/` | 1460 | 1003 | 68.70% | +| `src/platform/` | 1465 | 1008 | 68.81% | | `src/raytracing/` | 461 | 399 | 86.55% | -| `src/runtime.cpp` | 2425 | 1890 | 77.94% | +| `src/runtime.cpp` | 2442 | 1896 | 77.64% | | `src/screenshot_gate.cpp` | 27 | 24 | 88.89% | | `src/storage/` | 327 | 286 | 87.46% | | `src/utils/` | 0 | 0 | 0.00% | | `src/vfs/` | 239 | 195 | 81.59% | -| `src/webgpu/` | 9842 | 7566 | 76.87% | +| `src/webgpu/` | 9870 | 7566 | 76.66% | | `src/webtransport/` | 1391 | 1078 | 77.50% | | `src/workers/` | 615 | 527 | 85.69% | -| **TOTAL** | **24785** | **19459** | **78.51%** | +| **TOTAL** | **24835** | **19470** | **78.40%** | -Source digest: `sha256:2828ae43f9f7f863426676c8b76acf9a02576361e410f0fafac09a8df706b1b9` +Source digest: `sha256:6fd6a85360ae1c1cd2016f6daf241e8a892e616ee6041d5d9f0c091d14c2a0a3` The default `pnpm budgets` gate reads this committed measurement without configuring or compiling the native host. Any native source, native C++ test, CTest registration, or coverage aggregation From 1c908738e2f4834fa9fca767e0253311442f0b3d Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 02:40:14 -0700 Subject: [PATCH 11/14] fix(native): ratchet regenerated coverage floors upward --- ...PRD-614-mystralnative-upstream-catch-up-v0.1.6.md | 2 +- docs/verification/native-coverage-2026-08-28.md | 8 ++++---- .../scripts/measure-native-coverage.mjs | 4 +++- scripts/__tests__/check-native-coverage.spec.ts | 12 +++++++++++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 4c18639cca..0c4c201106 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -102,7 +102,7 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS - [x] AC-2 default X11 run. proof: `pnpm native:verify:desktop` — PASS 2026-10-15, complete command exits 0; two preceding core captures also pass 300/nonblank. - [x] AC-1 Wayland run and visual preservation. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` exits 0; fresh independent judge returns NEUTRAL on every fixed-pose pair and repeatability set. -Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library Wayland presentation now passes 300 frames and a nonblank 1280×720 capture with 4,096 overlay pixels (`artifacts/prd614-wayland-gl-smoke.png`, `/tmp/prd614-wayland-gl-smoke.log`): observed SDL driver `wayland`, NVIDIA RTX 2080/Vulkan, 12 surface formats and 301 presents. Weston remains headless with its supported `--renderer=gl`, using NVIDIA EGL and DMA-BUF modifiers; its default no-op renderer exposed zero formats. The exact full `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` command exits 0 (`/tmp/prd614-wayland-gl-final-full.log`), including all 50 native contracts, audio/stability/physics/loading/UI and nine external-bundle tests. Its core reports actual driver `wayland`, 300 frames, nonblank 1280×720 and 4,096 overlay pixels. Default full X11 also exits 0 on the same candidate (`/tmp/prd614-x11-dawn-final-full.log`). Final-source `pnpm typecheck && pnpm lint` exits 0; lint reports existing warnings without fixes. Canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0: 47 targets executed, three configured blockers reported, 78.40% total coverage and every existing floor met. The generated record binds source digest `6fd6a85360ae1c1cd2016f6daf241e8a892e616ee6041d5d9f0c091d14c2a0a3`. The final full unit suite remains pending. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; that failure is resolved by deterministic recapture on actual baseline and final candidate binaries. Identical scene bytes (`b72c7292bab250349fc7bc0ce234c2507182d2a6ecfc285c27e30770c7cd8b73`) freeze only the two cube rotation updates. Two runs each of baseline X11, final X11 and final Wayland pass 300 frames/nonblank/overlay checks. The fresh independent judge returns NEUTRAL for all four before/after pairs, all three repeatability sets and overall: pose, silhouette, position, color/exposure, overlap and overlay are preserved. Labelled images are published on the existing orphan `prd614-screenshots` branch under `fixed-pose/`; original failed-pose images remain available. Fresh generated-coverage and dependency-SBOM contracts pass 13/13. +Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library Wayland presentation now passes 300 frames and a nonblank 1280×720 capture with 4,096 overlay pixels (`artifacts/prd614-wayland-gl-smoke.png`, `/tmp/prd614-wayland-gl-smoke.log`): observed SDL driver `wayland`, NVIDIA RTX 2080/Vulkan, 12 surface formats and 301 presents. Weston remains headless with its supported `--renderer=gl`, using NVIDIA EGL and DMA-BUF modifiers; its default no-op renderer exposed zero formats. The exact full `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` command exits 0 (`/tmp/prd614-wayland-gl-final-full.log`), including all 50 native contracts, audio/stability/physics/loading/UI and nine external-bundle tests. Its core reports actual driver `wayland`, 300 frames, nonblank 1280×720 and 4,096 overlay pixels. Default full X11 also exits 0 on the same candidate (`/tmp/prd614-x11-dawn-final-full.log`). Final-source `pnpm typecheck && pnpm lint` exits 0; lint reports existing warnings without fixes. Canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0: 47 targets executed, three configured blockers reported, 78.39% total coverage and every existing floor met. The generated record binds source digest `4694f6c260feec4929114d04f323617c5d8b260073cdb3394f70e751c2122dce`. The final full unit suite remains pending. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; that failure is resolved by deterministic recapture on actual baseline and final candidate binaries. Identical scene bytes (`b72c7292bab250349fc7bc0ce234c2507182d2a6ecfc285c27e30770c7cd8b73`) freeze only the two cube rotation updates. Two runs each of baseline X11, final X11 and final Wayland pass 300 frames/nonblank/overlay checks. The fresh independent judge returns NEUTRAL for all four before/after pairs, all three repeatability sets and overall: pose, silhouette, position, color/exposure, overlap and overlay are preserved. Labelled images are published on the existing orphan `prd614-screenshots` branch under `fixed-pose/`; original failed-pose images remain available. Fresh generated-coverage and dependency-SBOM contracts pass 13/13. The separate root freshness fixture exposed a generator bug: an improved platform measurement retained a floor more than five points below it. The normal generator now ratchets floors upward while retaining tighter floors and regression rejection; actual regeneration raises platform 63.70%→63.81%. Its semantic fixture has genuine RED→GREEN. The first regeneration stopped at a transient timestamp-query failure; the isolated contract and one bounded complete retry pass unchanged. The interrupted full suite has no passing verdict; a fresh full run remains pending. #### Phase 2: Zero-length `writeBuffer` **Status:** PARTIAL — direct binding RED→GREEN and rebuilt desktop qualification pass; the decoder already accepts valid zero-byte records, so its requested RED is unmet. diff --git a/docs/verification/native-coverage-2026-08-28.md b/docs/verification/native-coverage-2026-08-28.md index 622802aeb2..fc53ae0da0 100644 --- a/docs/verification/native-coverage-2026-08-28.md +++ b/docs/verification/native-coverage-2026-08-28.md @@ -23,10 +23,10 @@ targets could not be built and are named below. | `src/vfs/` | 239 | 195 | 81.59% | | `src/webgpu/` | 9870 | 7566 | 76.66% | | `src/webtransport/` | 1391 | 1078 | 77.50% | -| `src/workers/` | 615 | 527 | 85.69% | -| **TOTAL** | **24835** | **19470** | **78.40%** | +| `src/workers/` | 615 | 524 | 85.20% | +| **TOTAL** | **24835** | **19467** | **78.39%** | -Source digest: `sha256:6fd6a85360ae1c1cd2016f6daf241e8a892e616ee6041d5d9f0c091d14c2a0a3` +Source digest: `sha256:4694f6c260feec4929114d04f323617c5d8b260073cdb3394f70e751c2122dce` The default `pnpm budgets` gate reads this committed measurement without configuring or compiling the native host. Any native source, native C++ test, CTest registration, or coverage aggregation @@ -41,7 +41,7 @@ change requires this opt-in command to refresh the record. | `src/fs/` | 75.43% | | `src/http/` | 86.95% | | `src/js/` | 74.50% | -| `src/platform/` | 63.70% | +| `src/platform/` | 63.81% | | `src/raytracing/` | 81.55% | | `src/runtime.cpp` | 73.28% | | `src/screenshot_gate.cpp` | 88.89% | diff --git a/packages/runtime-native/scripts/measure-native-coverage.mjs b/packages/runtime-native/scripts/measure-native-coverage.mjs index 1316788600..82f0774700 100644 --- a/packages/runtime-native/scripts/measure-native-coverage.mjs +++ b/packages/runtime-native/scripts/measure-native-coverage.mjs @@ -318,7 +318,9 @@ export function retainedCoverageFloors(previousRecord, subsystems) { } return subsystems.map(({ name, percent }) => { const path = `src/${name}${name.includes(".") ? "" : "/"}`; - return { name: path, percent: recorded.get(path) ?? percent }; + // Match the gate's five-point margin without ever loosening an established floor. + const ratcheted = Number((percent - 5).toFixed(2)); + return { name: path, percent: Math.max(recorded.get(path) ?? percent, ratcheted) }; }); } diff --git a/scripts/__tests__/check-native-coverage.spec.ts b/scripts/__tests__/check-native-coverage.spec.ts index 4520617589..ddf6399b96 100644 --- a/scripts/__tests__/check-native-coverage.spec.ts +++ b/scripts/__tests__/check-native-coverage.spec.ts @@ -65,7 +65,7 @@ describe("native coverage gate", () => { ).toThrow(/duplicate row: src\/js\//u); }); - it("should retain the established floor when regeneration measures a regression", async () => { + it("should ratchet improved coverage while retaining established regression floors", async () => { const coverageModule = (await import( new URL("../../packages/runtime-native/scripts/measure-native-coverage.mjs", import.meta.url) .href @@ -75,6 +75,16 @@ describe("native coverage gate", () => { subsystems: Array<{ name: string; percent: number }>, ): Array<{ name: string; percent: number }>; }; + const improved = record.replace("34.00%", "20.00%"); + expect( + coverageModule.retainedCoverageFloors(improved, [ + { name: "webgpu", percent: 35 }, + { name: "js", percent: 40 }, + ]), + ).toEqual([ + { name: "src/webgpu/", percent: 30 }, + { name: "src/js/", percent: 40 }, + ]); const established = record.replace("34.00%", "33.82%"); expect( coverageModule.retainedCoverageFloors(established, [ From 15d39c6828e74bd08f5bbc409d5e2043ddc11e17 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 02:52:56 -0700 Subject: [PATCH 12/14] docs(native): record final PRD614 suite and review results --- .../native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 0c4c201106..861db86318 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -102,7 +102,7 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS - [x] AC-2 default X11 run. proof: `pnpm native:verify:desktop` — PASS 2026-10-15, complete command exits 0; two preceding core captures also pass 300/nonblank. - [x] AC-1 Wayland run and visual preservation. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` exits 0; fresh independent judge returns NEUTRAL on every fixed-pose pair and repeatability set. -Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library Wayland presentation now passes 300 frames and a nonblank 1280×720 capture with 4,096 overlay pixels (`artifacts/prd614-wayland-gl-smoke.png`, `/tmp/prd614-wayland-gl-smoke.log`): observed SDL driver `wayland`, NVIDIA RTX 2080/Vulkan, 12 surface formats and 301 presents. Weston remains headless with its supported `--renderer=gl`, using NVIDIA EGL and DMA-BUF modifiers; its default no-op renderer exposed zero formats. The exact full `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` command exits 0 (`/tmp/prd614-wayland-gl-final-full.log`), including all 50 native contracts, audio/stability/physics/loading/UI and nine external-bundle tests. Its core reports actual driver `wayland`, 300 frames, nonblank 1280×720 and 4,096 overlay pixels. Default full X11 also exits 0 on the same candidate (`/tmp/prd614-x11-dawn-final-full.log`). Final-source `pnpm typecheck && pnpm lint` exits 0; lint reports existing warnings without fixes. Canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0: 47 targets executed, three configured blockers reported, 78.39% total coverage and every existing floor met. The generated record binds source digest `4694f6c260feec4929114d04f323617c5d8b260073cdb3394f70e751c2122dce`. The final full unit suite remains pending. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; that failure is resolved by deterministic recapture on actual baseline and final candidate binaries. Identical scene bytes (`b72c7292bab250349fc7bc0ce234c2507182d2a6ecfc285c27e30770c7cd8b73`) freeze only the two cube rotation updates. Two runs each of baseline X11, final X11 and final Wayland pass 300 frames/nonblank/overlay checks. The fresh independent judge returns NEUTRAL for all four before/after pairs, all three repeatability sets and overall: pose, silhouette, position, color/exposure, overlap and overlay are preserved. Labelled images are published on the existing orphan `prd614-screenshots` branch under `fixed-pose/`; original failed-pose images remain available. Fresh generated-coverage and dependency-SBOM contracts pass 13/13. The separate root freshness fixture exposed a generator bug: an improved platform measurement retained a floor more than five points below it. The normal generator now ratchets floors upward while retaining tighter floors and regression rejection; actual regeneration raises platform 63.70%→63.81%. Its semantic fixture has genuine RED→GREEN. The first regeneration stopped at a transient timestamp-query failure; the isolated contract and one bounded complete retry pass unchanged. The interrupted full suite has no passing verdict; a fresh full run remains pending. +Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library Wayland presentation now passes 300 frames and a nonblank 1280×720 capture with 4,096 overlay pixels (`artifacts/prd614-wayland-gl-smoke.png`, `/tmp/prd614-wayland-gl-smoke.log`): observed SDL driver `wayland`, NVIDIA RTX 2080/Vulkan, 12 surface formats and 301 presents. Weston remains headless with its supported `--renderer=gl`, using NVIDIA EGL and DMA-BUF modifiers; its default no-op renderer exposed zero formats. The exact full `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` command exits 0 (`/tmp/prd614-wayland-gl-final-full.log`), including all 50 native contracts, audio/stability/physics/loading/UI and nine external-bundle tests. Its core reports actual driver `wayland`, 300 frames, nonblank 1280×720 and 4,096 overlay pixels. Default full X11 also exits 0 on the same candidate (`/tmp/prd614-x11-dawn-final-full.log`). Final-source `pnpm typecheck && pnpm lint` exits 0; lint reports existing warnings without fixes. Canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0: 47 targets executed, three configured blockers reported, 78.39% total coverage and every existing floor met. The generated record binds source digest `4694f6c260feec4929114d04f323617c5d8b260073cdb3394f70e751c2122dce`. The final authoritative `taskset -c 0-4 pnpm test` exits 0 on `1c908738e2f4834fa9fca767e0253311442f0b3d`: root 777 files pass / three skip, 10,590 tests pass / 20 skip; native 138 files pass, 1,594 tests pass / 62 skip. A fresh independent read-only review exits 0 with no actionable defects. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; that failure is resolved by deterministic recapture on actual baseline and final candidate binaries. Identical scene bytes (`b72c7292bab250349fc7bc0ce234c2507182d2a6ecfc285c27e30770c7cd8b73`) freeze only the two cube rotation updates. Two runs each of baseline X11, final X11 and final Wayland pass 300 frames/nonblank/overlay checks. The fresh independent judge returns NEUTRAL for all four before/after pairs, all three repeatability sets and overall: pose, silhouette, position, color/exposure, overlap and overlay are preserved. Labelled images are published on the existing orphan `prd614-screenshots` branch under `fixed-pose/`; original failed-pose images remain available. Fresh generated-coverage and dependency-SBOM contracts pass 13/13. The separate root freshness fixture exposed a generator bug: an improved platform measurement retained a floor more than five points below it. The normal generator now ratchets floors upward while retaining tighter floors and regression rejection; actual regeneration raises platform 63.70%→63.81%. Its semantic fixture has genuine RED→GREEN. The first regeneration stopped at a transient timestamp-query failure; the isolated contract and one bounded complete retry pass unchanged. The interrupted full suite has no passing verdict; the subsequent frozen-candidate full run passes as recorded above. #### Phase 2: Zero-length `writeBuffer` **Status:** PARTIAL — direct binding RED→GREEN and rebuilt desktop qualification pass; the decoder already accepts valid zero-byte records, so its requested RED is unmet. @@ -131,4 +131,4 @@ packages/runtime-native/build/tn-linux/threenative-frame-op-stream-replay-test w - [x] AC-4 dispatched linux-arm64 leg is GREEN. proof: [run 38695846954](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695846954), candidate `a8b355190995c09adecb2278e408f3b2129f72e3`, attempt 1, job `116292229770` concludes success; starter and container reports each pass 300 frames and nonblank captures. - [ ] After AC-1 through AC-4 are green, NOTICE names `bafab09d599bd55aeb7433ba7131c3f320334c99` and v0.1.6. proof: `git diff -- packages/runtime-native/NOTICE` + `pnpm check:docs` -Verified CI evidence (2026-10-15): the exact-candidate receipt binds the successful job, build configuration and reports. The starter capture has 13,117 colors and the container capture 13,145; movement, collection/restart, visibility and diagnostics assertions pass. Log lines 2861/2870 verify the locked SWC/quiche arm64 payload IDs; their candidate lock URLs are `swc-41/swc-linux-arm64.zip` and `quiche-0.24.6-4/quiche-linux-arm64.zip`. The preceding titles print unchanged x64 default version strings, which are cosmetic and do not identify the verified archives. This proves the required arm64 leg, not the still-running whole native matrix. +Verified CI evidence (2026-10-15): the exact-candidate receipt binds the successful job, build configuration and reports. The starter capture has 13,117 colors and the container capture 13,145; movement, collection/restart, visibility and diagnostics assertions pass. Log lines 2861/2870 verify the locked SWC/quiche arm64 payload IDs; their candidate lock URLs are `swc-41/swc-linux-arm64.zip` and `quiche-0.24.6-4/quiche-linux-arm64.zip`. The preceding titles print unchanged x64 default version strings, which are cosmetic and do not identify the verified archives. The entire dispatched native matrix subsequently concludes SUCCESS on `a8b355190995c09adecb2278e408f3b2129f72e3`; this is the exact earlier dispatch, not a claim about future CI on the final candidate. From 3f476eb61871cdedb3d8928c86fa31f5e3c98ac5 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 19:35:12 -0700 Subject: [PATCH 13/14] fix(native): clean child scratch in display verification wrapper --- ...D-614-mystralnative-upstream-catch-up-v0.1.6.md | 4 +++- packages/runtime-native/scripts/xvfb.sh | 4 ++++ .../runtime-native/tests/distribution.test.mjs | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 861db86318..16bb556dc5 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -70,7 +70,7 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Blocked on -- AC-1 currently lacks `weston` on this machine; no `weston`, `cage` or `sway` is installed. Unblock with a user-space compositor extraction (no sudo or host-wide install), or an available Debian laptop compositor. This is a technical prerequisite, not a human approval or platform-scope decision. +- AC-3 stream RED requirement remains OPEN pending the user’s answer: the actual decoder passes both before and after, so no genuine RED exists. AC-1 has passed with the user-space Weston compositor. ## Integration Ledger @@ -104,6 +104,8 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS Candidate evidence (2026-10-15): the normal build and complete X11 command pass on the rebuilt CLI. Two core captures report observed driver `x11`, 300 frames and nonblank 1280×720 images (`artifacts/prd614-x11-after-{1,2}.png`). First explicit legacy-env Wayland run reaches driver `wayland`, then Dawn rejects `SurfaceSourceWaylandSurface` with zero formats (`artifacts/prd614-wayland-first-failure-runtime.log`); no unchanged retry. The signed user-space Weston passed socket/shm/EGL smoke and its owned process was stopped. The pinned Dawn archive lacks the Wayland Vulkan loader entry. Its [exact-commit replacement build](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695725341) enables Wayland and X11 in an isolated, digest-pinned upstream-compatible container, preserving the backend. The producer and publication pass. The downloaded archive SHA-256 is `bafce657936c161dc14642ddb61f71dcf6528b7ae90c40698831797e9c43d530` (28,193,047 bytes); independent extraction verifies installed headers, x86_64 ELF and actual Wayland/Xcb/Xlib loader entries. The dependency cache is a prerelease and excluded from Latest; publication now sets both flags explicitly. Actual replacement-library Wayland presentation now passes 300 frames and a nonblank 1280×720 capture with 4,096 overlay pixels (`artifacts/prd614-wayland-gl-smoke.png`, `/tmp/prd614-wayland-gl-smoke.log`): observed SDL driver `wayland`, NVIDIA RTX 2080/Vulkan, 12 surface formats and 301 presents. Weston remains headless with its supported `--renderer=gl`, using NVIDIA EGL and DMA-BUF modifiers; its default no-op renderer exposed zero formats. The exact full `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` command exits 0 (`/tmp/prd614-wayland-gl-final-full.log`), including all 50 native contracts, audio/stability/physics/loading/UI and nine external-bundle tests. Its core reports actual driver `wayland`, 300 frames, nonblank 1280×720 and 4,096 overlay pixels. Default full X11 also exits 0 on the same candidate (`/tmp/prd614-x11-dawn-final-full.log`). Final-source `pnpm typecheck && pnpm lint` exits 0; lint reports existing warnings without fixes. Canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0: 47 targets executed, three configured blockers reported, 78.39% total coverage and every existing floor met. The generated record binds source digest `4694f6c260feec4929114d04f323617c5d8b260073cdb3394f70e751c2122dce`. The final authoritative `taskset -c 0-4 pnpm test` exits 0 on `1c908738e2f4834fa9fca767e0253311442f0b3d`: root 777 files pass / three skip, 10,590 tests pass / 20 skip; native 138 files pass, 1,594 tests pass / 62 skip. A fresh independent read-only review exits 0 with no actionable defects. The first independent X11 visual judgment returned REGRESSION because the rotating cube poses differed; that failure is resolved by deterministic recapture on actual baseline and final candidate binaries. Identical scene bytes (`b72c7292bab250349fc7bc0ce234c2507182d2a6ecfc285c27e30770c7cd8b73`) freeze only the two cube rotation updates. Two runs each of baseline X11, final X11 and final Wayland pass 300 frames/nonblank/overlay checks. The fresh independent judge returns NEUTRAL for all four before/after pairs, all three repeatability sets and overall: pose, silhouette, position, color/exposure, overlap and overlay are preserved. Labelled images are published on the existing orphan `prd614-screenshots` branch under `fixed-pose/`; original failed-pose images remain available. Fresh generated-coverage and dependency-SBOM contracts pass 13/13. The separate root freshness fixture exposed a generator bug: an improved platform measurement retained a floor more than five points below it. The normal generator now ratchets floors upward while retaining tighter floors and regression rejection; actual regeneration raises platform 63.70%→63.81%. Its semantic fixture has genuine RED→GREEN. The first regeneration stopped at a transient timestamp-query failure; the isolated contract and one bounded complete retry pass unchanged. The interrupted full suite has no passing verdict; the subsequent frozen-candidate full run passes as recorded above. +Verification refresh (PR507): the engine native verification wrapper now scopes child `TMPDIR` to its existing owned runtime directory, fixing the real PulseAudio `pulse-*` scratch leak. Separately verified phases: native 1,595 pass / 62 skip, physics 29 JS + 23 Rust pass, strict publint pass, namespace 1→1 (`artifacts/pr507-validation/native-final.log`); root 10,590 pass / 20 skip across 780 files, namespace 1→1, exit 0 on `0c5118fbd` plus the local 18-line fix (`artifacts/pr507-validation/root-unit-final.log`). Final typecheck, lint and documentation checks also pass. + #### Phase 2: Zero-length `writeBuffer` **Status:** PARTIAL — direct binding RED→GREEN and rebuilt desktop qualification pass; the decoder already accepts valid zero-byte records, so its requested RED is unmet. **Files:** `src/webgpu/bindings.cpp`, `conformance/registry.json`, the shared conformance proof and existing native replay harness. diff --git a/packages/runtime-native/scripts/xvfb.sh b/packages/runtime-native/scripts/xvfb.sh index b7c3f8cf23..e2e7daaca1 100644 --- a/packages/runtime-native/scripts/xvfb.sh +++ b/packages/runtime-native/scripts/xvfb.sh @@ -59,6 +59,10 @@ cleanup() { } trap cleanup EXIT INT TERM +# Child libraries (including PulseAudio) leave scratch directories for their caller to own. +TMPDIR="$runtime" +export TMPDIR + # 10s was enough until a loaded two-core CI runner missed it: run 33789430714's installed-verifier # case reported "Xvfb did not report a display within 10 seconds" while Xvfb was still alive and # starting. The loop already exits the moment Xvfb dies, so a higher ceiling costs a healthy run diff --git a/packages/runtime-native/tests/distribution.test.mjs b/packages/runtime-native/tests/distribution.test.mjs index fa430f9558..b34d007f7b 100644 --- a/packages/runtime-native/tests/distribution.test.mjs +++ b/packages/runtime-native/tests/distribution.test.mjs @@ -92,6 +92,20 @@ async function serveFixtureRelease(root, contents) { const roots = []; const run = promisify(execFile); +test.skipIf(process.platform !== 'linux')('display wrapper cleans child scratch without removing sibling data', async () => { + const parent = makeTempDirSync('tn-display-scratch-'); + const retained = join(parent, 'retained'); + writeFileSync(retained, 'preserve'); + const result = await run('sh', [ + join(import.meta.dirname, '../scripts/xvfb.sh'), process.execPath, '-e', + 'const dir = require("node:path").join(require("node:os").tmpdir(), "pulse-child"); require("node:fs").mkdirSync(dir); console.log(dir)', + ], { env: { ...process.env, TMPDIR: parent } }); + const scratch = result.stdout.trim(); + assert.ok(scratch.startsWith(`${parent}/`)); + assert.equal(existsSync(scratch), false, `child scratch leaked: ${scratch}`); + assert.equal(readFileSync(retained, 'utf8'), 'preserve'); +}); + async function packRuntime(root) { const archives = join(root, 'archives'); mkdirSync(archives); From 315a796b8cb55781169c4f046baa92f425c35fc5 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 15 Oct 2026 21:19:06 -0700 Subject: [PATCH 14/14] docs(native): complete v0.1.6 catch-up qualification --- ...-mystralnative-upstream-catch-up-v0.1.6.md | 34 ++++++++++++------- .../native-coverage-2026-08-28.md | 4 +-- packages/runtime-native/NOTICE | 2 +- 3 files changed, 25 insertions(+), 15 deletions(-) rename docs/PRDs/{native => done}/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md (76%) diff --git a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md b/docs/PRDs/done/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md similarity index 76% rename from docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md rename to docs/PRDs/done/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md index 16bb556dc5..256b72a511 100644 --- a/docs/PRDs/native/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md +++ b/docs/PRDs/done/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md @@ -1,7 +1,7 @@ # PRD-614 — Catch the runtime-native fork up with MystralNative v0.1.6 -**Status:** IN PROGRESS -**Priority:** P1 — unfinished Wayland, zero-length `writeBuffer` and linux-arm64 Canvas 2D proofs gate the requested native-platform catch-up. +**Status:** COMPLETE +**Priority:** P1 — native-platform catch-up; all four acceptance criteria are verified under the approved stream regression correction. **Complexity:** 4 → MEDIUM — estimated 6–10 implementation files (+2), independent linux-arm64 build/artifact boundary (+2), no new module; risk override: none. Compatibility risk: shared surface changes must preserve default X11; arm64 dependency versions and library layouts must remain compatible with the QuickJS/wgpu host and Canvas 2D. **Owner:** João **Depends on:** None @@ -47,9 +47,7 @@ flowchart LR instead of renaming the X11 API. Read SDL's Wayland handles when that driver is requested; keep X11 as the default. Honor the task's legacy `SDL_VIDEODRIVER` spelling and SDL 3's `SDL_VIDEO_DRIVER`, and log the observed SDL driver. The X11-only web overlay rejects an explicit - Wayland request rather than silently overriding it. Actual presentation remains unproved: - the current Dawn prebuilt rejects the Wayland surface descriptor; its supported variant and - the existing wgpu build profile are being investigated. + Wayland request rather than silently overriding it. Actual presentation is proved with the pinned same-revision Dawn replacement described in Phase 1. - **Zero-length `writeBuffer`.** Return `undefined` when `dataSize == 0` before the null-pointer check, in the direct binding and in the frame-op stream decoder, to match the browser. - **linux-arm64.** Reuse the existing `native-platforms.yml` linux-arm64 leg, adapting the native dependency/build paths for arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. Keep Canvas 2D enabled and prove the candidate through a manually dispatched run; a skipped leg does not count. @@ -65,12 +63,12 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS - [x] AC-1 [local]: A native desktop game run with `SDL_VIDEODRIVER=wayland` under a headless Wayland compositor presents 300 non-blank frames. proof: `weston --backend=headless` + `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop` — PASS 2026-10-15: complete command exits 0, observed Wayland, 300 nonblank frames; independent fixed-pose judge NEUTRAL on both runs. - [x] AC-2 [local]: The default X11 run still presents 300 non-blank frames. proof: `pnpm native:verify:desktop` — PASS 2026-10-15 on the rebuilt candidate, observed SDL driver `x11`, 300 nonblank frames; complete audio/stability/physics/native-contract/loading/UI/bundle command exits 0. -- [ ] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), RED then GREEN on both the direct binding and frame-op stream paths — Evidence: pending. +- [x] AC-3 [local]: `device.queue.writeBuffer(buffer, 0, new Uint8Array(0))` returns without an exception on native, as on web. proof: new case in `pnpm parity` (`packages/runtime-native/conformance/registry.json`), genuine RED→GREEN on the direct binding; production encoder/decoder baseline-pass regression coverage on the frame-op stream, retaining invalid-source, detached-source and nonzero readback controls. - [x] AC-4 [shared]: The dispatched `native-platforms.yml` linux-arm64 leg is GREEN with Canvas 2D enabled, arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64. proof: [run 38695846954](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695846954), successful job `116292229770` on `a8b355190995c09adecb2278e408f3b2129f72e3`; actual configure, verified locked payloads and 300-frame starter artifacts pass. ## Blocked on -- AC-3 stream RED requirement remains OPEN pending the user’s answer: the actual decoder passes both before and after, so no genuine RED exists. AC-1 has passed with the user-space Weston compositor. +None. The user approved baseline-pass production stream regression coverage on 2026-10-16; no stream RED is claimed. ## Integration Ledger @@ -82,6 +80,8 @@ default X11 run is proved again. The frame-op stream encodes `writeBuffer` in JS ## Decisions +- 2026-10-16 (João, approved correction): replace the impossible frame-op stream RED requirement with honest baseline-pass production encoder/decoder regression coverage. The decoder already accepts valid zero-byte records; retain the production stream regression guard, all invalid/detached-source controls and nonzero GPU readback. Direct binding keeps its genuine RED→GREEN. No failure is manufactured. + - 2026-10-14 (AFK decision, Phase 2 detached sources): the detached-source check leaves the shared portable helper and lives in `runWriteBufferContract`'s async JS proof, because Android V8 has no `ArrayBuffer.prototype.transfer` and a `transfer`-requiring shared helper makes the parity row throw there. Desktop-focused proof keeps the fail-closed `transfer` requirement, so the case cannot pass silently on an engine that cannot detach a buffer. No public flag, new API or optional skip; no binding change. - 2026-10-14 (AFK lane, Phase 2 detached sources): a detached `ArrayBuffer` or TypedArray reports `byteLength` 0 (ECMA-262 25.1.5.3), so the empty-source guard accepted it as a no-op. The guard now reads the intrinsic brand getter first and then probes: `new Uint8Array(data)` for ArrayBuffer, `%TypedArray%.prototype.values.call(data)` for TypedArray, both of which throw `TypeError` on a detached receiver (`quickjs.c:54120-54122`, V8 13.1 `v8-array-buffer.cc`). `ArrayBuffer.prototype.slice` is not used because it runs a species lookup on live receivers. The DataView entry keeps its getter, which already throws on detach. No `WasDetached()` virtual method and no constructor-name check, which user code can spoof. - 2026-10-14 (AFK lane, Phase 1 source only): the user's exact command is `SDL_VIDEODRIVER=wayland pnpm native:verify:desktop`, and SDL 3.2.30 reads `SDL_VIDEO_DRIVER`, not the legacy spelling (`SDL_hints.h:3326`). Five harness sites set `SDL_VIDEODRIVER=x11` and `verify-desktop-core.mjs` wraps its run in `scripts/xvfb.sh`, so the literal command would run X11 and pass falsely. Decision: `window.cpp` resolves the requested driver from `SDL_VIDEO_DRIVER` first, then the legacy `SDL_VIDEODRIVER`, and defaults to `x11`; `verify-desktop-core.mjs` runs without the Xvfb wrapper when a Wayland driver is explicitly requested, and logs the request; `scripts/xvfb.sh` names the X11 it provides instead of silently standing in for Wayland. `runtime.cpp` logs `SDL_GetCurrentVideoDriver()` as the receipt, so a Wayland claim comes from SDL itself. @@ -107,15 +107,15 @@ Candidate evidence (2026-10-15): the normal build and complete X11 command pass Verification refresh (PR507): the engine native verification wrapper now scopes child `TMPDIR` to its existing owned runtime directory, fixing the real PulseAudio `pulse-*` scratch leak. Separately verified phases: native 1,595 pass / 62 skip, physics 29 JS + 23 Rust pass, strict publint pass, namespace 1→1 (`artifacts/pr507-validation/native-final.log`); root 10,590 pass / 20 skip across 780 files, namespace 1→1, exit 0 on `0c5118fbd` plus the local 18-line fix (`artifacts/pr507-validation/root-unit-final.log`). Final typecheck, lint and documentation checks also pass. #### Phase 2: Zero-length `writeBuffer` -**Status:** PARTIAL — direct binding RED→GREEN and rebuilt desktop qualification pass; the decoder already accepts valid zero-byte records, so its requested RED is unmet. +**Status:** COMPLETE — genuine direct RED→GREEN; approved production stream GREEN→GREEN regression coverage; merged-candidate direct, stream and default replay commands exit 0. **Files:** `src/webgpu/bindings.cpp`, `conformance/registry.json`, the shared conformance proof and existing native replay harness. **Implementation:** Validate genuine sources and ranges before the empty-write no-op; preserve malformed and detached-source rejection. Exercise the real frame-op decoder without manufacturing a failure. - [x] AC-3 direct binding path, red then green. proof: focused native Dawn+V8/Vulkan harness exits 1 on genuine empty sources before the fix and 0 after; nonzero readback and invalid-source controls pass. -- [ ] AC-3 frame-op stream path, red then green. proof: actual production encoder supplies five zero-byte and two nonzero records; all seven reach the decoder and pass both before and after. No genuine RED exists. +- [x] AC-3 production frame-op stream regression guard. proof: actual production encoder supplies five zero-byte and two nonzero records; all seven reach the decoder and pass before and after, with invalid/detached-source rejection and GPU readback preserved. Rebuilt merged-candidate direct, stream and default replay commands exit 0; `artifacts/pr507-validation/{zero-direct-merged,zero-stream-merged,replay-default-merged}.log`. Verified evidence (2026-10-15): the shared proof covers five zero-size writes, seven invalid-source rejections and GPU readback `[11,22,55,66]`. Direct RED→GREEN logs are `packages/runtime-native/artifacts/conformance/prd614-zero-direct-{before,after}.log`; stream before/after logs use `prd614-zero-stream-{before,after}.log` in the same directory. Genuine detached ArrayBuffer/TypedArray/DataView assertions separately exposed and fixed an empty-source guard regression, with direct RED→GREEN and stream PASS/PASS (`prd614-detached-{direct,stream}-{before,after}.log`). These transfer-based checks stay in the desktop harness so the shared conformance case remains portable. The final focused direct, stream and default replay suite all exit 0 (`prd614-portable-{direct,stream,replay-default}.log`). -The rebuilt actual CLI passes full `pnpm native:verify:desktop` (exit 0; `/tmp/prd614-x11-candidate-full.log`). The selected new parity row passes both headed Chromium WebGPU (NVIDIA/turing) and the rebuilt desktop host; each runner exits 2 because 107 unselected rows are blocked, not because the selected row fails. Final reports are `packages/runtime-native/artifacts/conformance/prd614-zero-web-final/report.json` and `prd614-zero-desktop-candidate/report.json`; desktop raw log is `/tmp/prd614-zero-desktop-candidate.log`. AC-3 overall remains open: a passing baseline decoder cannot supply the requested stream RED. +The rebuilt actual CLI passes full `pnpm native:verify:desktop` (exit 0; `/tmp/prd614-x11-candidate-full.log`). The selected new parity row passes both headed Chromium WebGPU (NVIDIA/turing) and the rebuilt desktop host; each runner exits 2 because 107 unselected rows are blocked, not because the selected row fails. Final reports are `packages/runtime-native/artifacts/conformance/prd614-zero-web-final/report.json` and `prd614-zero-desktop-candidate/report.json`; desktop raw log is `/tmp/prd614-zero-desktop-candidate.log`. The stream result is GREEN→GREEN regression coverage; the approved requirement does not claim a stream RED. Reproduce from repository root: ```sh @@ -126,11 +126,21 @@ packages/runtime-native/build/tn-linux/threenative-frame-op-stream-replay-test w ``` #### Phase 3: linux-arm64 proof, then provenance -**Status:** PARTIAL — required linux-arm64 job passes with the specified dependencies and Canvas 2D; NOTICE waits for the unmet AC-3 stream RED. +**Status:** COMPLETE — earlier dispatched linux-arm64 proof retained at its actual SHA; AC-1 through AC-4 supported and NOTICE updated to v0.1.6. **Files:** Native dependency/build scripts, arm64 Skia/Canvas 2D configuration, existing `.github/workflows/native-platforms.yml` only if required, and `packages/runtime-native/NOTICE`. **Implementation:** Adapt the upstream linux-arm64 changes to the existing leg; include arm64 Skia, quiche `0.24.6-4`, SWC `swc-41` and libwebp aarch64 with Canvas 2D enabled. Dispatch the workflow against the candidate branch and require its linux-arm64 leg to be GREEN. Only after AC-1 through AC-4 pass, set `Upstream provenance baseline:` to the full hash and name v0.1.6. - [x] linux-arm64 dependency/build configuration includes arm64 Skia, quiche `0.24.6-4`, SWC `swc-41`, libwebp aarch64 and Canvas 2D enabled. proof: actual job `116292229770` configures `TN_ENABLE_CANVAS2D="ON"`, resolves `Release/arm64/libskia.a` and aarch64 libwebp, and verifies `quiche-linux-aarch64` / `swc-linux-aarch64` against the candidate lock. - [x] AC-4 dispatched linux-arm64 leg is GREEN. proof: [run 38695846954](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38695846954), candidate `a8b355190995c09adecb2278e408f3b2129f72e3`, attempt 1, job `116292229770` concludes success; starter and container reports each pass 300 frames and nonblank captures. -- [ ] After AC-1 through AC-4 are green, NOTICE names `bafab09d599bd55aeb7433ba7131c3f320334c99` and v0.1.6. proof: `git diff -- packages/runtime-native/NOTICE` + `pnpm check:docs` +- [x] After AC-1 through AC-4 are green, NOTICE names `bafab09d599bd55aeb7433ba7131c3f320334c99` and v0.1.6. proof: `git diff -- packages/runtime-native/NOTICE` + `pnpm check:docs` (final document check recorded below) Verified CI evidence (2026-10-15): the exact-candidate receipt binds the successful job, build configuration and reports. The starter capture has 13,117 colors and the container capture 13,145; movement, collection/restart, visibility and diagnostics assertions pass. Log lines 2861/2870 verify the locked SWC/quiche arm64 payload IDs; their candidate lock URLs are `swc-41/swc-linux-arm64.zip` and `quiche-0.24.6-4/quiche-linux-arm64.zip`. The preceding titles print unchanged x64 default version strings, which are cosmetic and do not identify the verified archives. The entire dispatched native matrix subsequently concludes SUCCESS on `a8b355190995c09adecb2278e408f3b2129f72e3`; this is the exact earlier dispatch, not a claim about future CI on the final candidate. + +Merged-source reconciliation (2026-10-16, `746e1fff0`): `git diff --exit-code 1c908738e2f4834fa9fca767e0253311442f0b3d HEAD --` over the surface context/header, runtime, window/CLI routing, desktop-core harness, direct binding, frame decoder, production JS encoder, shared proof and replay harness returns 0. Existing dependency payload descriptors are identical; only `v8-linux-arm64` is added. The earlier X11 and headless-Wayland reports both bind binary SHA-256 `9e92191ce792a0603789a32450b80dd8a9ef287631bd53671151f75be94f35af`; their `completedAt` values are timestamps, not source SHAs. Their full-command and fixed-pose visual proofs are reused for the unchanged surface behavior. Console/pipeline output changed since that revision, so the actual merged host and replay executable were rebuilt with `cmake --build packages/runtime-native/build/tn-linux --target threenative-frame-op-stream-replay-test mystral -j2` (exit 0). Fresh `node packages/runtime-native/scripts/verify-desktop-core.mjs` exits 0 on X11: 300 frames, nonblank 1280×720. Fresh current-source Wayland core verification also passes 300 nonblank 1280×720 frames (`artifacts/pr507-validation/wayland-fresh-core-report.json`), using actual host SHA-256 `396fed8f450aa74a24ab447ce2e95caababa6ef9cf9e859edd1947664290cfc2`; rebuilt replay SHA-256 is `b67d7b9c231a47ac1bd96eb6d9c65c3e90333eb6948536435c0e58be8056ade8`. This refresh proves the Wayland core only; full qualification and the NEUTRAL fixed-pose visual verdict remain historical. The current full Wayland qualification is in progress with no result claimed; no fresh visual run is claimed. Final-candidate CI is still required: manual run `38841301134` belongs to the previous `3f476eb61` head. + +The four original zero-write logs were inspected: direct-before reports the genuine empty Uint8Array/ArrayBuffer/DataView rejection; direct-after has no proof failure; both stream logs report seven decoded records and checked GPU readback. Those logs contain no source SHA; runtime millisecond counters are not provenance. The existing harness fails closed on missing proof input, failed async assertions, wrong transport, absent decoder crossings or a record count other than seven. A fresh esbuild bundle of the current shared proof and rebuilt merged executable pass `write-buffer-direct`, `write-buffer-stream` and the default replay contract (all exit 0). Assertions retain five empty writes, seven invalid sources, three desktop detached sources and nonzero readback `[11,22,55,66]`. The stream observer counts all seven actual production encoder/decoder writes. This refresh binds the GREEN result to current source; the historical RED is not relabelled as a merged-source run. + +The actual linux-arm64 dispatch remains run `38695846954` / job `116292229770` at `a8b355190995c09adecb2278e408f3b2129f72e3`. It proves the catch-up dependencies and Canvas 2D on that earlier candidate; the merged cached-V8 default is a later develop change, not rerun AC-4 evidence. Focused merged dependency-provenance, native coverage/generator/digest contracts pass 36/36; `linux-arm64-v8-source.spec.ts` passes 28/28. No future CI result is claimed. + +Final local validation (2026-10-16): canonical `CMAKE_BUILD_PARALLEL_LEVEL=2 pnpm --filter @threenative/runtime-native native:coverage` exits 0 after real compilation/execution: 47 native contract targets, three explicitly configured blockers, 19,492/24,858 lines (78.41%). Generated source digest is `1a8ed34cf063a9530e8fa11b2722ae0b4c475402ee1baef998b5dea0acd3a4d7`; every existing tighter floor and the ratchet policy are retained. No output was hand-picked. `pnpm exec vitest run scripts/__tests__/check-native-coverage.spec.ts` passes 7/7, including exact-input freshness, upward ratchet and regression rejection. `pnpm check:docs` and all six required prose-lane spec files pass (248 tests); `git diff --check` passes. Logs: `artifacts/pr507-validation/{coverage-merged,coverage-freshness-ratchet-merged,focused-native-contracts,arm64-source-contract,docs-merged-final,docs-contracts-merged-final}.log`. `pnpm prd:progress docs/PRDs/done/PRD-614-mystralnative-upstream-catch-up-v0.1.6.md` reports 3/3 phases, 8/8 phase boxes, 4/4 acceptance and `prd:100% — ready`. No whole-root suite, typecheck or lint was rerun for these documentation/generated-output edits. + +Fresh compiled provenance: merged replay executable SHA-256 `b67d7b9c231a47ac1bd96eb6d9c65c3e90333eb6948536435c0e58be8056ade8`, actual host `396fed8f450aa74a24ab447ce2e95caababa6ef9cf9e859edd1947664290cfc2`, freshly bundled shared proof `4668a9ad5c57392814ed981551112554036d356630ae0845712941ce5ae94b06`. These identify this local rerun, not the historical before arms or earlier dispatched arm64 job. diff --git a/docs/verification/native-coverage-2026-08-28.md b/docs/verification/native-coverage-2026-08-28.md index dc90f94cb5..70539e9856 100644 --- a/docs/verification/native-coverage-2026-08-28.md +++ b/docs/verification/native-coverage-2026-08-28.md @@ -21,10 +21,10 @@ targets could not be built and are named below. | `src/storage/` | 327 | 286 | 87.46% | | `src/utils/` | 0 | 0 | 0.00% | | `src/vfs/` | 239 | 195 | 81.59% | -| `src/webgpu/` | 9885 | 7581 | 76.69% | +| `src/webgpu/` | 9885 | 7584 | 76.72% | | `src/webtransport/` | 1391 | 1078 | 77.50% | | `src/workers/` | 615 | 527 | 85.69% | -| **TOTAL** | **24858** | **19489** | **78.40%** | +| **TOTAL** | **24858** | **19492** | **78.41%** | Source digest: `sha256:1a8ed34cf063a9530e8fa11b2722ae0b4c475402ee1baef998b5dea0acd3a4d7` diff --git a/packages/runtime-native/NOTICE b/packages/runtime-native/NOTICE index b5215620b9..abdaa89fb3 100644 --- a/packages/runtime-native/NOTICE +++ b/packages/runtime-native/NOTICE @@ -3,7 +3,7 @@ ThreeNative Native Runtime This package is a maintained fork of MystralNative, originally published by Mystral Engine under the MIT License: https://github.com/mystralengine/mystralnative -Upstream provenance baseline: 841fe379ca1ab23c87c99fac3b901e37487ce8f2 (v0.1.5) +Upstream provenance baseline: bafab09d599bd55aeb7433ba7131c3f320334c99 (v0.1.6) Imported maintained-fork state: c705b16eef02bc49e68fe6c9b4114698aa6b3d7f The upstream license is preserved in LICENSE. Third-party dependencies are not vendored;