Mystral binding evaluation

Pinned source: bafab09d599bd55aeb7433ba7131c3f320334c99, repository mystralengine/mystralnative. Static inspection only. No repositories edited, dependencies installed, or installers executed. No compile/runtime validation. Mystral’s configured third_party/ directory is absent and no isolated binding probe was built. Prerequisites for a meaningful isolated probe: C++ compiler, CMake, the runner’s pinned QuickJS-ng sources, matching Dawn or wgpu-native headers/library, and a usable GPU backend/driver. The unmodified bindings additionally need stb_image, Canvas2D and GLTF support symbols. Building the complete runtime brings further dependencies below.

Decision

Conditional go for a small extraction spike. No-go for dropping in the full runtime or presenting this as complete browser WebGPU support. Mystral offers useful native WebGPU wrapper code and a backend-neutral engine seam, but integration requires ownership, promise, cancellation, error and grant work. Keeping the existing runner engine is feasible in principle, not demonstrated by a build.

Findings

F1. A binding seam exists without runtime/window ownership. webgpu::initBindings(Engine*, instance, device, queue, surface, format, width, height, debug) accepts supplied handles and stores them. It does not construct the engine or enter the runtime loop. src/webgpu/bindings.cpp:351–370. Its current initialization nevertheless calls document.getElementById('canvas') before its fallback, so a missing document is not a supported clean entry path. src/webgpu/bindings.cpp:395–414. It also mixes WebGPU with image decoding, GLTF, Canvas2D and OffscreenCanvas. src/webgpu/bindings.cpp:11–63. Inference: split compute/device bindings from canvas/browser shims, rather than link this translation unit unchanged and assume a minimal dependency surface.

F2. Existing-context support needs an adapter, not the shipped QuickJS engine. Mystral’s abstract interface accepts opaque handles and native functions, exposes external ArrayBuffers, protection, private data and a release callback. include/mystral/js/engine.h:23–32, include/mystral/js/engine.h:123–146, include/mystral/js/engine.h:268–320. Its concrete QuickJS constructor always creates its own runtime and context and its destructor owns destruction. src/js/quickjs_engine.cpp:98–167. No supplied-context constructor exists there. A custom implementation of Mystral’s Engine can instead delegate into the runner’s Engine, or extracted wrappers can target the runner API directly. Raw context access alone is not an adoption API.

Runner evidence: native/graph-runner/src/js/engine.h:19–37 defines shared Value ownership and requires values die before the engine. Lines 49–153 expose evaluation, byte copying, object calls, promises, jobs and interrupts. src/js/quickjs_engine.cpp:27–50 owns a QuickJS runtime/context and uses both context/runtime opaque slots. Lines 256–280 implement promise creation/job draining. Its API lacks Mystral-style external buffer views, finalizer registration and raw context access. Inference: expose a narrow internal binding host or add required engine-neutral capabilities. Do not overwrite the existing opaque slots to attach Mystral state.

F3. QuickJS family matches, version/ABI compatibility is unproven. Mystral’s downloader selects QuickJS-ng 0.11.0, while its CMake compiles discovered sources rather than enforcing that version. scripts/download-deps.mjs:167–175, CMakeLists.txt:396–438. Runner native/graph-runner/CMakeLists.txt:21–24 pins QuickJS-ng commit d7215b0ad3fc2c562e8317fd9bca058fd4863804. Do not infer equivalence from the shared fork. Mystral uses heap-allocated JSValue* handles, while the runner uses its own RAII wrapper. Never reinterpret one as the other or link two QuickJS implementations into the same context. Compile extracted code against the runner’s single pin and test source/API compatibility. Bytecode remains engine-build-specific per runner engine.h:53–55.

F4. Promise-shaped methods do not establish asynchronous browser semantics. requestDevice returns the device directly and requestAdapter returns the adapter directly. src/webgpu/bindings.cpp:4133–4135, src/webgpu/bindings.cpp:4175–4180. mapAsync starts a native callback then synchronously polls, including blocking wgpu-device polling, before returning an already-settled expression. src/webgpu/bindings.cpp:1641–1694. onSubmittedWorkDone returns Promise.resolve() without a native completion callback. src/webgpu/bindings.cpp:1505–1513. Moreover QuickJS evalWithResult evaluates as a module, so expression-based promise returns using that method are suspect, not proven correct promises. src/js/quickjs_engine.cpp:211–229. Inference: host-owned completion queue must resolve/reject runner PromiseCapability values on the JS thread, then drain jobs. Native GPU callbacks must not invoke JS directly. Test actual Promise identity, GPU readback, pending-work tracking and cancellation, not merely successful await.

F5. Isolation, lifetime and errors need repair. Binding engine/device/queue/surface and resource maps are process globals. src/webgpu/bindings.cpp:84–180. Concurrent contexts or repeated runs are therefore not independently isolated as written. registerRelease defaults to no-op, and enumeration of the QuickJS implementation found no override. include/mystral/js/engine.h:268–274. Wrappers depend on it for native releases, for example texture views and bind groups. src/webgpu/bindings.cpp:3414–3416, src/webgpu/bindings.cpp:3768–3770. QuickJS protection is manual, callbacks allocate argument handles and the callback trampoline has no C++ exception boundary. src/js/quickjs_engine.cpp:588–600, src/js/quickjs_engine.cpp:713–751. pushErrorScope is no-op, popErrorScope resolves null and lost is constructed as an unresolved promise expression. src/webgpu/bindings.cpp:4095–4131. These are concrete scope limitations, not full-spec support. Use per-run state, explicit teardown, idempotent destroy/finalizers, native callback cancellation and runner-style exception translation.

F6. Headless GPU exists, grant control is a host integration responsibility. Context::initializeHeadless requests an adapter without a surface, processes adapter/device callbacks and creates offscreen rendering resources. src/webgpu/context.cpp:279–451. Offscreen textures can be supplied to bindings. src/webgpu/bindings.cpp:4811–4814. This establishes a no-window code path, not that this machine has a functioning GPU/driver. The public binding initializer accepts caller-selected handles, allowing the host to withhold registration or provide a restricted device. Its globals and browser shims do not provide a per-workflow authorization layer. Inference: host grants must govern GPU availability, feature/limit selection and resource budgets before registration. JS interrupts alone cannot reliably cancel a blocking native GPU call.

F7. Other APIs are separable in concept but bring ambient capabilities in the full runtime. Canvas2D uses Skia when compiled in and a stub fallback otherwise, with platform font managers. src/canvas/canvas2d.cpp:1–45. Audio uses SDL3, initializes the audio subsystem and opens default playback, without a browser user-activation gate in that constructor path. src/audio/audio_context.cpp:197–230. Runtime skips audio in no-SDL mode. src/runtime.cpp:439–442. Fetch exposes a global native file reader, strips file://, and desktop code opens the supplied path. src/runtime.cpp:1468–1488, src/runtime.cpp:1520–1537. HTTP fetch calls an async native request, with curl/libuv and runtime polling dependencies. src/runtime.cpp:2081–2104, src/runtime.cpp:757–802, CMakeLists.txt:1269–1284. The inspected fetch entry does not apply the runner’s workspace scope or network grants. Do not inherit full-runtime ambient file/network/audio access. Keep runner fetch/files/timers and explicitly granted rendering/audio adapters.

Minimal integration plan

A1. Extract only navigator.gpu/device/queue/buffer/compute operations into per-run state against the existing runner engine. Retain the runner’s timers, module evaluation, job queue and cancellation model. Add engine capabilities only where native finalizers or mapped buffers require them. Start with copied readback rather than external mapped views.

A2. Host creates one explicitly granted headless device using one pinned native backend/header version. Feed completions to the existing scheduler. Replace immediate success promises, blocking mapping, error-scope stubs and lost-device handling before claiming these methods work.

A3. Gate further adoption on an isolated test: real WGSL compute → submit → map → exact byte result, Promise identity/rejection, invalid shader, denied GPU grant, cancellation, repeated teardown and two independent contexts. Then add offscreen rendering, Canvas2D and audio only if needed. Full Web API coverage would require a separately enumerated compatibility suite.

Licensing

Mystral’s checked-in LICENSE is MIT and requires preserving its copyright/permission notice. LICENSE:1–21. This does not license all linked dependencies under MIT. CMake explicitly links QuickJS-ng, SDL3, Dawn/wgpu-native, optional Skia and desktop curl/zlib. CMakeLists.txt:1131–1133, CMakeLists.txt:1161–1208, CMakeLists.txt:1269–1308. Their downloaded artifacts/licenses were absent here, so transitive redistribution notices and build-specific terms remain unverified. Inventory the exact chosen backend and dependencies when packaging, rather than repeating an umbrella “MIT” claim.