Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Agent scratch files
.working/

# Build directories
build/
build-*/
Expand Down
164 changes: 164 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(MYSTRAL_USE_DAWN "Use Dawn WebGPU implementation" ON) # Default - best compatibility
option(MYSTRAL_USE_WGPU "Use wgpu-native WebGPU implementation" OFF) # Alternative - has iOS support

# WebGL 2 compatibility (optional - ANGLE on desktop platforms)
option(MYSTRAL_USE_WEBGL "Enable ANGLE-backed WebGL 2 compatibility" OFF)
set(MYSTRAL_ANGLE_ROOT "" CACHE PATH "ANGLE package root containing include/ and platform runtime libraries")

# HTML5 parser and DOM core (optional - Lexbor)
option(MYSTRAL_USE_LEXBOR "Enable Lexbor HTML5 template parsing" ON)

# Ray Tracing (optional - hardware RT via DXR/Vulkan/Metal)
option(MYSTRAL_USE_RAYTRACING "Enable hardware ray tracing support" OFF)

Expand Down Expand Up @@ -72,13 +79,124 @@ endif()
message(STATUS "Mystral Platform: ${MYSTRAL_PLATFORM}")
message(STATUS "JS Engine: V8=${MYSTRAL_USE_V8} JSC=${MYSTRAL_USE_JSC} QuickJS=${MYSTRAL_USE_QUICKJS}")
message(STATUS "WebGPU: Dawn=${MYSTRAL_USE_DAWN} wgpu=${MYSTRAL_USE_WGPU}")
message(STATUS "WebGL 2: ANGLE=${MYSTRAL_USE_WEBGL}")
message(STATUS "HTML/DOM: Lexbor=${MYSTRAL_USE_LEXBOR}")

# ============================================================================
# Third Party Dependencies
# ============================================================================

set(THIRD_PARTY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party)

if(MYSTRAL_USE_WEBGL)
if(NOT MYSTRAL_PLATFORM MATCHES "^(windows|linux|macos)$")
message(FATAL_ERROR "MYSTRAL_USE_WEBGL supports Windows, Linux, and macOS desktop builds")
endif()

if(NOT MYSTRAL_ANGLE_ROOT)
set(MYSTRAL_ANGLE_ROOT "${THIRD_PARTY_DIR}/angle")
endif()

set(MYSTRAL_ANGLE_INCLUDE_DIR "${MYSTRAL_ANGLE_ROOT}/include")
if(EXISTS "${MYSTRAL_ANGLE_ROOT}/src/native/angle-includes/EGL/egl.h")
set(MYSTRAL_ANGLE_INCLUDE_DIR "${MYSTRAL_ANGLE_ROOT}/src/native/angle-includes")
endif()

if(WIN32)
set(MYSTRAL_ANGLE_RUNTIME_FILES
"libEGL.dll"
"libGLESv2.dll"
"d3dcompiler_47.dll"
)
set(MYSTRAL_ANGLE_RUNTIME_CANDIDATES
"${MYSTRAL_ANGLE_ROOT}/bin"
"${MYSTRAL_ANGLE_ROOT}/deps/windows/dll"
"${MYSTRAL_ANGLE_ROOT}/out/Release"
"${MYSTRAL_ANGLE_ROOT}"
)
elseif(APPLE)
set(MYSTRAL_ANGLE_RUNTIME_FILES
"libEGL.dylib"
"libGLESv2.dylib"
)
set(MYSTRAL_ANGLE_RUNTIME_CANDIDATES
"${MYSTRAL_ANGLE_ROOT}/lib"
"${MYSTRAL_ANGLE_ROOT}/out/Release"
"${MYSTRAL_ANGLE_ROOT}"
)
else()
set(MYSTRAL_ANGLE_RUNTIME_FILES
"libEGL.so"
"libGLESv2.so"
)
set(MYSTRAL_ANGLE_RUNTIME_CANDIDATES
"${MYSTRAL_ANGLE_ROOT}/lib"
"${MYSTRAL_ANGLE_ROOT}/out/Release"
"${MYSTRAL_ANGLE_ROOT}"
)
endif()

unset(MYSTRAL_ANGLE_RUNTIME_DIR)
foreach(ANGLE_RUNTIME_CANDIDATE IN LISTS MYSTRAL_ANGLE_RUNTIME_CANDIDATES)
set(ANGLE_RUNTIME_CANDIDATE_COMPLETE TRUE)
foreach(ANGLE_RUNTIME_FILE IN LISTS MYSTRAL_ANGLE_RUNTIME_FILES)
if(NOT EXISTS "${ANGLE_RUNTIME_CANDIDATE}/${ANGLE_RUNTIME_FILE}")
set(ANGLE_RUNTIME_CANDIDATE_COMPLETE FALSE)
break()
endif()
endforeach()
if(ANGLE_RUNTIME_CANDIDATE_COMPLETE)
set(MYSTRAL_ANGLE_RUNTIME_DIR "${ANGLE_RUNTIME_CANDIDATE}")
break()
endif()
endforeach()
if(NOT MYSTRAL_ANGLE_RUNTIME_DIR)
message(FATAL_ERROR "Complete ANGLE runtime not found below ${MYSTRAL_ANGLE_ROOT}")
endif()

foreach(ANGLE_HEADER
"EGL/egl.h"
"EGL/eglext.h"
"EGL/eglext_angle.h"
"GLES2/gl2ext.h"
"GLES3/gl3.h"
"KHR/khrplatform.h")
if(NOT EXISTS "${MYSTRAL_ANGLE_INCLUDE_DIR}/${ANGLE_HEADER}")
message(FATAL_ERROR "Missing ANGLE header: ${MYSTRAL_ANGLE_INCLUDE_DIR}/${ANGLE_HEADER}")
endif()
endforeach()

message(STATUS "ANGLE headers: ${MYSTRAL_ANGLE_INCLUDE_DIR}")
message(STATUS "ANGLE runtime: ${MYSTRAL_ANGLE_RUNTIME_DIR}")
endif()

set(LEXBOR_FOUND OFF)
if(MYSTRAL_USE_LEXBOR)
set(LEXBOR_DIR ${THIRD_PARTY_DIR}/lexbor)
file(GLOB LEXBOR_SOURCE_DIRS ${LEXBOR_DIR}/lexbor-*)
if(LEXBOR_SOURCE_DIRS)
list(GET LEXBOR_SOURCE_DIRS 0 LEXBOR_SOURCE_DIR)
elseif(EXISTS ${LEXBOR_DIR}/CMakeLists.txt)
set(LEXBOR_SOURCE_DIR ${LEXBOR_DIR})
endif()

if(LEXBOR_SOURCE_DIR AND EXISTS ${LEXBOR_SOURCE_DIR}/CMakeLists.txt)
set(LEXBOR_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(LEXBOR_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(LEXBOR_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(LEXBOR_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(LEXBOR_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE)
set(LEXBOR_BUILD_UTILS OFF CACHE BOOL "" FORCE)
add_subdirectory(${LEXBOR_SOURCE_DIR} ${CMAKE_BINARY_DIR}/lexbor-build EXCLUDE_FROM_ALL)
if(TARGET lexbor_static)
set(LEXBOR_FOUND ON)
message(STATUS "Found Lexbor: ${LEXBOR_SOURCE_DIR}")
endif()
else()
message(STATUS "Lexbor not found - native HTML template parsing disabled. Run 'node scripts/download-deps.mjs --only lexbor' to enable.")
endif()
endif()

# SDL3 - Build from source as static library for single-binary distribution
set(SDL3_DIR ${THIRD_PARTY_DIR}/sdl3)
set(SDL3_FOUND OFF)
Expand Down Expand Up @@ -992,6 +1110,7 @@ set(MYSTRAL_SOURCES
src/utils/cgltf_impl.cpp
src/http/http_client.cpp
src/http/async_http_client.cpp
src/websocket/client.cpp
src/webtransport/webtransport.cpp
src/webtransport/webtransport_polyfill.cpp
src/fs/async_file.cpp
Expand All @@ -1008,6 +1127,20 @@ set(MYSTRAL_SOURCES
src/video/gpu_readback_recorder.cpp
)

if(MYSTRAL_USE_WEBGL)
list(APPEND MYSTRAL_SOURCES
src/webgl/context.cpp
src/webgl/bindings.cpp
)
endif()

if(LEXBOR_FOUND)
list(APPEND MYSTRAL_SOURCES
src/dom/html_template.cpp
src/dom/bindings.cpp
)
endif()

# Ray tracing sources (conditional)
if(MYSTRAL_USE_RAYTRACING)
list(APPEND MYSTRAL_SOURCES
Expand Down Expand Up @@ -1054,6 +1187,19 @@ target_include_directories(mystral-runtime PRIVATE
${THIRD_PARTY_DIR}/cgltf
)

if(MYSTRAL_USE_WEBGL)
target_include_directories(mystral-runtime PRIVATE ${MYSTRAL_ANGLE_INCLUDE_DIR})
target_compile_definitions(mystral-runtime PUBLIC MYSTRAL_HAS_WEBGL)
if(CMAKE_DL_LIBS)
target_link_libraries(mystral-runtime PRIVATE ${CMAKE_DL_LIBS})
endif()
endif()

if(LEXBOR_FOUND)
target_link_libraries(mystral-runtime PRIVATE lexbor_static)
target_compile_definitions(mystral-runtime PUBLIC MYSTRAL_HAS_LEXBOR)
endif()

# Pass build configuration to source code
# Determine JS engine name
if(MYSTRAL_USE_V8)
Expand Down Expand Up @@ -1281,6 +1427,14 @@ else()
find_package(CURL REQUIRED)
find_package(ZLIB REQUIRED)
target_link_libraries(mystral-runtime PUBLIC CURL::libcurl ZLIB::ZLIB)
if(WIN32 AND (VCPKG_TARGET_TRIPLET MATCHES "-static$" OR CURL_USE_STATIC_LIBS))
set_source_files_properties(
src/http/http_client.cpp
src/http/async_http_client.cpp
src/websocket/client.cpp
PROPERTIES COMPILE_DEFINITIONS CURL_STATICLIB
)
endif()
message(STATUS "Found CURL: ${CURL_LIBRARIES}")
message(STATUS "Found ZLIB: ${ZLIB_LIBRARIES}")
endif()
Expand Down Expand Up @@ -1370,6 +1524,16 @@ if(NOT MYSTRAL_PLATFORM STREQUAL "ios" AND NOT MYSTRAL_PLATFORM STREQUAL "androi
add_executable(mystral src/cli/main.cpp)
target_link_libraries(mystral PRIVATE mystral-runtime)

if(MYSTRAL_USE_WEBGL)
foreach(ANGLE_RUNTIME_FILE IN LISTS MYSTRAL_ANGLE_RUNTIME_FILES)
add_custom_command(TARGET mystral POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MYSTRAL_ANGLE_RUNTIME_DIR}/${ANGLE_RUNTIME_FILE}"
"$<TARGET_FILE_DIR:mystral>/${ANGLE_RUNTIME_FILE}"
)
endforeach()
endif()

# Add webp include directories and definitions for video recording
if(TARGET webp::mux)
target_include_directories(mystral PRIVATE ${WEBP_INCLUDE_DIR})
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ cmake --build build --parallel
./build/mystral run examples/triangle.js
```

### Experimental Desktop WebGL 2

The optional ANGLE backend supports Windows (D3D11), Linux (Vulkan with X11 or Wayland), and macOS (Metal). Set `MYSTRAL_ANGLE_ROOT` to a package containing `include/EGL`, `include/GLES2`, `include/GLES3`, and `include/KHR`, plus the platform runtime libraries under `bin/` on Windows or `lib/` on Linux and macOS. Linux hosts also need a Vulkan loader, Vulkan driver, and XCB runtime; Wayland presentation additionally uses the system Wayland runtime libraries.

```bash
cmake -B build \
-DMYSTRAL_USE_WEBGL=ON \
-DMYSTRAL_ANGLE_ROOT=/path/to/angle-runtime
cmake --build build --parallel
./build/mystral run examples/webgl2-triangle.js
```

```powershell
cmake -B build `
-DMYSTRAL_USE_WEBGL=ON `
-DMYSTRAL_ANGLE_ROOT=C:\path\to\angle-runtime
cmake --build build --config Release
.\build\Release\mystral.exe run examples\webgl2-triangle.js
```

The current milestone supports WebGL 2 context creation, shaders, buffers, textures, framebuffers, uniforms, instancing, GPU completion, pixel readback, and automatic SDL window presentation. Linux supports X11 and Wayland window surfaces plus a headless pbuffer fallback. macOS presents ANGLE through a dedicated Core Animation layer so its Metal surface does not replace Dawn's `CAMetalLayer`. The observed API surface of an unchanged Three.js r181 texture, shadow, render-target, and instancing workload is covered. The first WebGL context owns the native window surface; full WebGL IDL coverage and multi-canvas compositing remain in progress.

Native HTML template parsing is available through [Lexbor v3](https://github.com/lexbor/lexbor). `MYSTRAL_USE_LEXBOR=ON` is the default when `third_party/lexbor` is present; run `node scripts/download-deps.mjs --only lexbor` to download the pinned source.

## What Can You Build?

Here's a complete "Hello Triangle" — the traditional first GPU program:
Expand Down Expand Up @@ -384,12 +408,12 @@ All dependencies are downloaded automatically as prebuilt binaries:
| SDL3 | Windowing, input, audio |
| V8 / QuickJS / JSC | JavaScript engine |
| Skia | Canvas 2D rendering |
| libcurl | HTTP requests |
| libcurl | HTTP and WebSocket requests |
| libuv | Async I/O, timers, file watching |
| Draco | Native Draco mesh decompression (optional) |
| SWC | TypeScript transpiling |

Prebuilt dependency binaries are managed via [mystralengine/library-builder](https://github.com/mystralengine/library-builder).
Prebuilt dependency binaries are managed via [mystralengine/library-builder](https://github.com/mystralengine/library-builder). Desktop WebSocket support requires libcurl built with its `ws` and `wss` protocols enabled.

## Documentation

Expand Down
27 changes: 27 additions & 0 deletions examples/html-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
console.log("=== Mystral Lexbor HTML Template Test ===");

const template = document.createElement("template");
template.innerHTML = `<section class="panel"><h1>Mystral</h1><!-- template marker --></section>tail`;

const content = template.content;
const section = content.firstChild;
const heading = section?.firstChild;
const comment = heading?.nextSibling;
const tail = section?.nextSibling;
const clone = content.cloneNode(true);

const passed =
content?.nodeType === 11 &&
section?.tagName === "SECTION" &&
section?.className === "panel" &&
heading?.tagName === "H1" &&
heading?.firstChild?.data === "Mystral" &&
comment?.nodeType === 8 &&
tail?.nodeType === 3 &&
clone !== content &&
clone.firstChild !== section &&
clone.firstChild?.parentNode === clone;

console.log(`HTML_TEMPLATE_NODES=${content.childNodes.length}`);
console.log(`HTML_TEMPLATE_RESULT=${passed ? "pass" : "fail"}`);
process.exit(passed ? 0 : 1);
97 changes: 97 additions & 0 deletions examples/webgl2-triangle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
console.log("=== Mystral ANGLE WebGL2 Test ===");

const gl = canvas.getContext("webgl2", {
alpha: false,
depth: true,
stencil: false,
antialias: false,
powerPreference: "high-performance"
});

if (!gl) {
throw new Error("ANGLE WebGL2 context creation failed");
}

function compileShader(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader;
}

const vertexShader = compileShader(gl.VERTEX_SHADER, `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}`);

const fragmentShader = compileShader(gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
out vec4 color;
void main() {
color = vec4(0.15, 0.65, 1.0, 1.0);
}`);

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}

const vertices = new Float32Array([
0.0, 0.75,
-0.75, -0.75,
0.75, -0.75
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

const position = gl.getAttribLocation(program, "position");
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);

gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0.04, 0.06, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.finish();

const center = new Uint8Array(4);
gl.readPixels(
Math.floor(gl.drawingBufferWidth / 2),
Math.floor(gl.drawingBufferHeight / 2),
1,
1,
gl.RGBA,
gl.UNSIGNED_BYTE,
center
);

const passed = center[2] > center[0] && center[2] > center[1] && gl.getError() === gl.NO_ERROR;
console.log(`ANGLE renderer: ${gl.getParameter(gl.RENDERER)}`);
console.log(`WebGL version: ${gl.getParameter(gl.VERSION)}`);
console.log(`Center pixel: ${Array.from(center).join(",")}`);
console.log(`WEBGL2_TRIANGLE_RESULT=${passed ? "pass" : "fail"}`);
if (!passed) {
throw new Error("ANGLE WebGL2 triangle validation failed");
}

let presentedFrames = 0;
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
presentedFrames++;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
setTimeout(() => {
console.log(`WEBGL2_PRESENTED_FRAMES=${presentedFrames}`);
process.exit(0);
}, 1500);
Loading