diff --git a/.gitignore b/.gitignore index 515a216..0d6bb10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Agent scratch files +.working/ + # Build directories build/ build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index c17b3f2..b1d718c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -72,6 +79,8 @@ 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 @@ -79,6 +88,115 @@ message(STATUS "WebGPU: Dawn=${MYSTRAL_USE_DAWN} wgpu=${MYSTRAL_USE_WGPU}") 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) @@ -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 @@ -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 @@ -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) @@ -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() @@ -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}" + "$/${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}) diff --git a/README.md b/README.md index 43b6cd7..a57e77f 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/examples/html-template.js b/examples/html-template.js new file mode 100644 index 0000000..e23d79c --- /dev/null +++ b/examples/html-template.js @@ -0,0 +1,27 @@ +console.log("=== Mystral Lexbor HTML Template Test ==="); + +const template = document.createElement("template"); +template.innerHTML = `

Mystral

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); diff --git a/examples/webgl2-triangle.js b/examples/webgl2-triangle.js new file mode 100644 index 0000000..93ee41f --- /dev/null +++ b/examples/webgl2-triangle.js @@ -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); diff --git a/include/mystral/dom/bindings.h b/include/mystral/dom/bindings.h new file mode 100644 index 0000000..128939c --- /dev/null +++ b/include/mystral/dom/bindings.h @@ -0,0 +1,11 @@ +#pragma once + +namespace mystral::js { +class Engine; +} + +namespace mystral::dom { + +bool initBindings(js::Engine *engine, bool debug = false); + +} // namespace mystral::dom diff --git a/include/mystral/dom/html_template.h b/include/mystral/dom/html_template.h new file mode 100644 index 0000000..0861600 --- /dev/null +++ b/include/mystral/dom/html_template.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +namespace mystral::dom { + +enum class NodeType { + Element = 1, + Text = 3, + Comment = 8, + DocumentFragment = 11, +}; + +struct ParsedNode { + NodeType type = NodeType::DocumentFragment; + std::string name; + std::string text; + std::vector> attributes; + std::vector> children; +}; + +std::unique_ptr parseHTMLTemplate(const std::string &html, + std::string &error); + +} // namespace mystral::dom diff --git a/include/mystral/platform/window.h b/include/mystral/platform/window.h index 1790636..53fd5d7 100644 --- a/include/mystral/platform/window.h +++ b/include/mystral/platform/window.h @@ -49,6 +49,21 @@ void* getMetalLayer(); */ void* getMetalLayerFromView(void* metalView); +/** + * Get the dedicated Core Animation layer used by ANGLE (macOS only) + */ +void* getWebGLMetalLayer(); + +/** + * Create a dedicated Core Animation layer for ANGLE presentation (macOS only) + */ +void* createWebGLMetalLayer(void* metalView); + +/** + * Remove the dedicated ANGLE presentation layer (macOS only) + */ +void destroyWebGLMetalLayer(void* metalLayer); + /** * Get drawable size of Metal layer (accounts for Retina) */ diff --git a/include/mystral/webgl/context.h b/include/mystral/webgl/context.h new file mode 100644 index 0000000..ac52f6a --- /dev/null +++ b/include/mystral/webgl/context.h @@ -0,0 +1,196 @@ +#pragma once + +#include "mystral/js/engine.h" + +#include +#include +#include +#include +#include + +namespace mystral::webgl { + +struct ContextAttributes { + bool alpha = true; + bool depth = true; + bool stencil = false; + bool antialias = true; + bool premultipliedAlpha = true; + bool preserveDrawingBuffer = false; + bool preferHighPerformance = true; + bool allowNativeTextureInterop = false; +}; + +enum class NativeWindowPlatform { + None, + Win32, + Metal, + X11, + Wayland, +}; + +struct NativeWindow { + NativeWindowPlatform platform = NativeWindowPlatform::None; + void *display = nullptr; + uintptr_t window = 0; + + explicit operator bool() const { + return platform != NativeWindowPlatform::None && window != 0; + } +}; + +struct ShaderPrecisionFormat { + int32_t rangeMin = 0; + int32_t rangeMax = 0; + int32_t precision = 0; +}; + +struct ActiveInfo { + std::string name; + int32_t size = 0; + uint32_t type = 0; +}; + +class Context { +public: + Context(); + ~Context(); + + Context(const Context &) = delete; + Context &operator=(const Context &) = delete; + + bool initialize(uint32_t width, uint32_t height, + const ContextAttributes &attributes, + const NativeWindow &nativeWindow = {}); + void shutdown(); + bool makeCurrent(); + bool present(); + + // Windows/ANGLE compositor integration. The returned device is owned by + // ANGLE. Imported D3D11 textures are exposed as regular GL textures. + void *nativeD3D11Device(); + uint32_t importD3D11Texture(void *nativeTexture); + + bool isInitialized() const; + bool isWindowSurface() const; + const std::string &errorMessage() const; + const std::string &renderer() const; + const std::string &version() const; + const std::string &shadingLanguageVersion() const; + + uint32_t createShader(uint32_t type); + void shaderSource(uint32_t shader, const std::string &source); + void compileShader(uint32_t shader); + int32_t getShaderParameter(uint32_t shader, uint32_t parameter); + std::string getShaderInfoLog(uint32_t shader); + ShaderPrecisionFormat getShaderPrecisionFormat(uint32_t shaderType, + uint32_t precisionType); + + uint32_t createProgram(); + void attachShader(uint32_t program, uint32_t shader); + void linkProgram(uint32_t program); + int32_t getProgramParameter(uint32_t program, uint32_t parameter); + std::string getProgramInfoLog(uint32_t program); + void useProgram(uint32_t program); + ActiveInfo getActiveAttrib(uint32_t program, uint32_t index); + ActiveInfo getActiveUniform(uint32_t program, uint32_t index); + int32_t getUniformLocation(uint32_t program, const std::string &name); + + uint32_t createBuffer(); + void bindBuffer(uint32_t target, uint32_t buffer); + void bufferData(uint32_t target, size_t size, const void *data, + uint32_t usage); + uint32_t createFramebuffer(); + void bindFramebuffer(uint32_t target, uint32_t framebuffer); + uint32_t createRenderbuffer(); + void bindRenderbuffer(uint32_t target, uint32_t renderbuffer); + uint32_t createTexture(); + void bindTexture(uint32_t target, uint32_t texture); + uint32_t createVertexArray(); + void bindVertexArray(uint32_t vertexArray); + + int32_t getAttribLocation(uint32_t program, const std::string &name); + void enableVertexAttribArray(uint32_t index); + void vertexAttribPointer(uint32_t index, int32_t size, uint32_t type, + bool normalized, int32_t stride, size_t offset); + void vertexAttribDivisor(uint32_t index, uint32_t divisor); + + void activeTexture(uint32_t texture); + void clearDepth(float depth); + void clearStencil(int32_t stencil); + void colorMask(bool red, bool green, bool blue, bool alpha); + void cullFace(uint32_t mode); + void deleteShader(uint32_t shader); + void depthFunc(uint32_t function); + void depthMask(bool enabled); + void disable(uint32_t capability); + void enable(uint32_t capability); + void frontFace(uint32_t mode); + void pixelStorei(uint32_t parameter, int32_t value); + void scissor(int32_t x, int32_t y, int32_t width, int32_t height); + void stencilMask(uint32_t mask); + + void framebufferRenderbuffer(uint32_t target, uint32_t attachment, + uint32_t renderbufferTarget, + uint32_t renderbuffer); + void framebufferTexture2D(uint32_t target, uint32_t attachment, + uint32_t textureTarget, uint32_t texture, + int32_t level); + void renderbufferStorage(uint32_t target, uint32_t internalFormat, + int32_t width, int32_t height); + void drawBuffers(const std::vector &buffers); + + void texImage2D(uint32_t target, int32_t level, int32_t internalFormat, + int32_t width, int32_t height, int32_t border, + uint32_t format, uint32_t type, const void *pixels); + void texImage3D(uint32_t target, int32_t level, int32_t internalFormat, + int32_t width, int32_t height, int32_t depth, int32_t border, + uint32_t format, uint32_t type, const void *pixels); + void texParameteri(uint32_t target, uint32_t parameter, int32_t value); + void texStorage2D(uint32_t target, int32_t levels, uint32_t internalFormat, + int32_t width, int32_t height); + void texSubImage2D(uint32_t target, int32_t level, int32_t xOffset, + int32_t yOffset, int32_t width, int32_t height, + uint32_t format, uint32_t type, const void *pixels); + + void uniform1f(int32_t location, float x); + void uniform1i(int32_t location, int32_t x); + void uniform1iv(int32_t location, int32_t count, const int32_t *values); + void uniform2f(int32_t location, float x, float y); + void uniform3f(int32_t location, float x, float y, float z); + void uniform3fv(int32_t location, int32_t count, const float *values); + void uniformMatrix3fv(int32_t location, int32_t count, bool transpose, + const float *values); + void uniformMatrix4fv(int32_t location, int32_t count, bool transpose, + const float *values); + + void viewport(int32_t x, int32_t y, int32_t width, int32_t height); + void clearColor(float red, float green, float blue, float alpha); + void clear(uint32_t mask); + void drawArrays(uint32_t mode, int32_t first, int32_t count); + void drawElements(uint32_t mode, int32_t count, uint32_t type, size_t offset); + void drawElementsInstanced(uint32_t mode, int32_t count, uint32_t type, + size_t offset, int32_t instanceCount); + void finish(); + void readPixels(int32_t x, int32_t y, int32_t width, int32_t height, + uint32_t format, uint32_t type, void *destination); + int32_t getInteger(uint32_t parameter); + std::vector getIntegers(uint32_t parameter, size_t count); + uint32_t getError(); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +bool initBindings(js::Engine *engine, bool debug = false); +ContextAttributes +contextAttributesFromJS(js::Engine *engine, + const std::vector &args); +void presentContexts(); +void shutdownBindings(); +js::JSValueHandle +createContextJSObject(js::Engine *engine, uint32_t width, uint32_t height, + const ContextAttributes &attributes = {}); + +} // namespace mystral::webgl diff --git a/include/mystral/websocket/client.h b/include/mystral/websocket/client.h new file mode 100644 index 0000000..97549f2 --- /dev/null +++ b/include/mystral/websocket/client.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +namespace mystral::websocket { + +enum class EventType { + Open, + Message, + Error, + Close, +}; + +struct Event { + EventType type = EventType::Error; + uint64_t connectionId = 0; + std::vector data; + std::string text; + std::string protocol; + uint16_t closeCode = 1006; + bool binary = false; + bool clean = false; +}; + +class ClientManager { +public: + static ClientManager& instance(); + + uint64_t connect(const std::string& url, const std::vector& protocols = {}); + bool send(uint64_t connectionId, std::vector data, bool binary); + void close(uint64_t connectionId, uint16_t code = 1000, const std::string& reason = {}); + std::vector pollEvents(); + void shutdown(); + + ClientManager(const ClientManager&) = delete; + ClientManager& operator=(const ClientManager&) = delete; + +private: + ClientManager(); + ~ClientManager(); + + struct Impl; + std::unique_ptr impl_; +}; + +inline ClientManager& getClientManager() { + return ClientManager::instance(); +} + +} // namespace mystral::websocket diff --git a/scripts/download-deps.mjs b/scripts/download-deps.mjs index 820f62e..9262abc 100644 --- a/scripts/download-deps.mjs +++ b/scripts/download-deps.mjs @@ -12,7 +12,7 @@ * node scripts/download-deps.mjs --only skia-ios # Download only iOS Skia * node scripts/download-deps.mjs --force # Re-download even if exists * - * Desktop deps: wgpu, sdl3, dawn, v8, quickjs, stb, cgltf, webp, skia, swc + * Desktop deps: wgpu, sdl3, dawn, v8, quickjs, stb, cgltf, lexbor, webp, skia, swc * iOS deps: wgpu-ios, skia-ios (for cross-compilation from macOS) * Android deps: wgpu-android, sdl3-android */ @@ -253,6 +253,13 @@ const DEPS = { ], rawUrl: 'https://raw.githubusercontent.com/jkuhlmann/cgltf/v1.14/cgltf.h', }, + lexbor: { + // Lexbor HTML5 parser and DOM core for template.innerHTML and CSS selectors + // https://github.com/lexbor/lexbor/releases + version: '3.0.0', + getUrl: () => `https://github.com/lexbor/lexbor/archive/refs/tags/v${DEPS.lexbor.version}.tar.gz`, + extractTo: 'lexbor', + }, webp: { // libwebp for WebP image decoding (used by GLTF EXT_texture_webp extension) // https://developers.google.com/speed/webp/download @@ -772,7 +779,7 @@ async function main() { const onlyIndex = args.indexOf('--only'); // Desktop deps (downloaded by default) - const desktopDeps = ['wgpu', 'sdl3', 'dawn', 'v8', 'quickjs', 'stb', 'cgltf', 'webp', 'skia', 'swc', 'libuv', 'draco', 'quiche']; + const desktopDeps = ['wgpu', 'sdl3', 'dawn', 'v8', 'quickjs', 'stb', 'cgltf', 'lexbor', 'webp', 'skia', 'swc', 'libuv', 'draco', 'quiche']; // iOS deps (only downloaded with --only or --ios) const iosDeps = ['wgpu-ios', 'skia-ios', 'quiche-ios']; diff --git a/src/audio/audio_bindings.cpp b/src/audio/audio_bindings.cpp index 0fbf8a6..4409425 100644 --- a/src/audio/audio_bindings.cpp +++ b/src/audio/audio_bindings.cpp @@ -177,6 +177,43 @@ js::JSValueHandle createGainNodeJS(js::Engine* engine, GainNode* nodePtr, js::JS return g_jsEngine->newUndefined(); }) ); + const auto setGainValue = [nodePtr](void* ctx, const std::vector& args) -> js::JSValueHandle { + if (!args.empty()) { + nodePtr->gain().setValue(static_cast(g_jsEngine->toNumber(args[0]))); + } + return g_jsEngine->newUndefined(); + }; + engine->setProperty(gainParam, "setValueAtTime", + engine->newFunction("setValueAtTime", setGainValue) + ); + engine->setProperty(gainParam, "linearRampToValueAtTime", + engine->newFunction("linearRampToValueAtTime", setGainValue) + ); + engine->setProperty(gainParam, "exponentialRampToValueAtTime", + engine->newFunction("exponentialRampToValueAtTime", setGainValue) + ); + engine->setProperty(gainParam, "setTargetAtTime", + engine->newFunction("setTargetAtTime", setGainValue) + ); + engine->setProperty(gainParam, "setValueCurveAtTime", + engine->newFunction("setValueCurveAtTime", [](void* ctx, const std::vector& args) -> js::JSValueHandle { + return g_jsEngine->newUndefined(); + }) + ); + engine->setProperty(gainParam, "cancelScheduledValues", + engine->newFunction("cancelScheduledValues", [](void* ctx, const std::vector& args) -> js::JSValueHandle { + return g_jsEngine->newUndefined(); + }) + ); + engine->setProperty(gainParam, "cancelAndHoldAtTime", + engine->newFunction("cancelAndHoldAtTime", [](void* ctx, const std::vector& args) -> js::JSValueHandle { + return g_jsEngine->newUndefined(); + }) + ); + auto setAudioParamPrototype = engine->getGlobalProperty("__mystralSetAudioParamPrototype"); + if (engine->isFunction(setAudioParamPrototype)) { + engine->call(setAudioParamPrototype, engine->newUndefined(), {gainParam}); + } engine->setProperty(jsNode, "gain", gainParam); // connect/disconnect @@ -325,6 +362,32 @@ js::JSValueHandle createAudioContextJS(js::Engine* engine, AudioContext* ctxPtr) void initializeAudioBindings(js::Engine* engine) { g_jsEngine = engine; + engine->evalScript(R"JS( +if (typeof globalThis.AudioParam === "undefined") { + globalThis.AudioParam = class AudioParam {}; +} +for (const method of [ + 'setValueAtTime', + 'linearRampToValueAtTime', + 'exponentialRampToValueAtTime', + 'setTargetAtTime', + 'setValueCurveAtTime', + 'cancelScheduledValues', + 'cancelAndHoldAtTime' +]) { + if (typeof AudioParam.prototype[method] !== 'function') { + AudioParam.prototype[method] = function(value) { + if (value !== undefined) this.value = value; + return this; + }; + } +} +globalThis.__mystralSetAudioParamPrototype = value => { + Object.setPrototypeOf(value, AudioParam.prototype); + return value; +}; +)JS", ""); + // Create AudioContext constructor auto audioContextCtor = engine->newFunction("AudioContext", [](void* ctx, const std::vector& args) -> js::JSValueHandle { diff --git a/src/dom/bindings.cpp b/src/dom/bindings.cpp new file mode 100644 index 0000000..6610eac --- /dev/null +++ b/src/dom/bindings.cpp @@ -0,0 +1,242 @@ +#include "mystral/dom/bindings.h" + +#include "mystral/dom/html_template.h" +#include "mystral/js/engine.h" + +#include +#include + +namespace mystral::dom { + +namespace { + +js::Engine *g_engine = nullptr; +bool g_debug = false; + +js::JSValueHandle createJSNode(const ParsedNode &node, + js::JSValueHandle parent) { + auto result = g_engine->newObject(); + g_engine->setProperty(result, "nodeType", + g_engine->newNumber(static_cast(node.type))); + g_engine->setProperty(result, "parentNode", parent); + + if (node.type == NodeType::Element) { + std::string localName = node.name; + for (char &character : localName) { + if (character >= 'A' && character <= 'Z') { + character = static_cast(character - 'A' + 'a'); + } + } + g_engine->setProperty(result, "nodeName", + g_engine->newString(node.name.c_str())); + g_engine->setProperty(result, "tagName", + g_engine->newString(node.name.c_str())); + g_engine->setProperty(result, "localName", + g_engine->newString(localName.c_str())); + + auto attributes = g_engine->newObject(); + for (const auto &[name, value] : node.attributes) { + g_engine->setProperty(attributes, name.c_str(), + g_engine->newString(value.c_str())); + g_engine->setProperty(result, name.c_str(), + g_engine->newString(value.c_str())); + if (name == "class") { + g_engine->setProperty(result, "className", + g_engine->newString(value.c_str())); + } + } + g_engine->setProperty(result, "attributes", attributes); + g_engine->setProperty(result, "style", g_engine->newObject()); + g_engine->setProperty(result, "dataset", g_engine->newObject()); + } else if (node.type == NodeType::Text || node.type == NodeType::Comment) { + const char *nodeName = node.type == NodeType::Text ? "#text" : "#comment"; + g_engine->setProperty(result, "nodeName", g_engine->newString(nodeName)); + g_engine->setProperty(result, "data", + g_engine->newString(node.text.c_str())); + g_engine->setProperty(result, "nodeValue", + g_engine->newString(node.text.c_str())); + g_engine->setProperty(result, "textContent", + g_engine->newString(node.text.c_str())); + } else { + g_engine->setProperty(result, "nodeName", + g_engine->newString("#document-fragment")); + } + + auto childNodes = g_engine->newArray(node.children.size()); + auto children = g_engine->newArray(); + uint32_t elementIndex = 0; + for (uint32_t index = 0; index < node.children.size(); ++index) { + auto child = createJSNode(*node.children[index], result); + g_engine->setPropertyIndex(childNodes, index, child); + if (node.children[index]->type == NodeType::Element) { + g_engine->setPropertyIndex(children, elementIndex++, child); + } + } + g_engine->setProperty(result, "childNodes", childNodes); + g_engine->setProperty(result, "children", children); + g_engine->setProperty(result, "childElementCount", + g_engine->newNumber(elementIndex)); + return result; +} + +} // namespace + +bool initBindings(js::Engine *engine, bool debug) { + if (!engine) { + return false; + } + g_engine = engine; + g_debug = debug; + + engine->setGlobalProperty( + "__parseHTMLTemplate", + engine->newFunction( + "__parseHTMLTemplate", + [](void *, const std::vector &args) { + const std::string html = + args.empty() ? "" : g_engine->toString(args[0]); + std::string error; + auto tree = parseHTMLTemplate(html, error); + if (!tree) { + g_engine->throwException(error.c_str()); + return g_engine->newNull(); + } + + auto root = createJSNode(*tree, g_engine->newNull()); + auto hydrate = + g_engine->getGlobalProperty("__mystralHydrateDOMTree"); + if (g_engine->isFunction(hydrate)) { + return g_engine->call(hydrate, g_engine->newUndefined(), {root}); + } + return root; + })); + + const bool initialized = engine->evalScript(R"JS( +if (typeof globalThis.Node === 'undefined') globalThis.Node = class Node {}; +if (typeof globalThis.Element === 'undefined') globalThis.Element = class Element extends Node {}; +if (typeof globalThis.HTMLElement === 'undefined') globalThis.HTMLElement = class HTMLElement extends Element {}; +if (typeof globalThis.Text === 'undefined') globalThis.Text = class Text extends Node {}; +if (typeof globalThis.Comment === 'undefined') globalThis.Comment = class Comment extends Node {}; +if (typeof globalThis.DocumentFragment === 'undefined') globalThis.DocumentFragment = class DocumentFragment extends Node {}; + +const nodePrototypeFor = node => { + if (node.nodeType === 1) return HTMLElement.prototype; + if (node.nodeType === 3) return Text.prototype; + if (node.nodeType === 8) return Comment.prototype; + return DocumentFragment.prototype; +}; + +const cloneDOMNode = (node, deep = false) => { + const clone = Object.create(nodePrototypeFor(node)); + for (const key of ['nodeType', 'nodeName', 'tagName', 'localName', 'data', 'nodeValue', 'textContent', 'id', 'className']) { + if (key in node) clone[key] = node[key]; + } + clone.attributes = { ...(node.attributes || {}) }; + clone.style = { ...(node.style || {}) }; + clone.dataset = { ...(node.dataset || {}) }; + clone.childNodes = []; + clone.children = []; + clone.parentNode = null; + if (deep) { + for (const child of node.childNodes || []) { + const childClone = cloneDOMNode(child, true); + childClone.parentNode = clone; + clone.childNodes.push(childClone); + if (childClone.nodeType === 1) clone.children.push(childClone); + } + } + return installDOMMethods(clone); +}; + +const installDOMMethods = node => { + Object.setPrototypeOf(node, nodePrototypeFor(node)); + node.childNodes ||= []; + node.children ||= []; + node.appendChild = function(child) { + if (!child) return child; + child.parentNode?.removeChild?.(child); + child.parentNode = this; + this.childNodes.push(child); + if (child.nodeType === 1) this.children.push(child); + return child; + }; + node.append = function(...children) { for (const child of children) this.appendChild(child); }; + node.insertBefore = function(child, reference) { + if (!reference) return this.appendChild(child); + const index = this.childNodes.indexOf(reference); + if (index < 0) return this.appendChild(child); + child.parentNode?.removeChild?.(child); + child.parentNode = this; + this.childNodes.splice(index, 0, child); + if (child.nodeType === 1) { + const elementIndex = this.childNodes.slice(0, index).filter(node => node.nodeType === 1).length; + this.children.splice(elementIndex, 0, child); + } + return child; + }; + node.removeChild = function(child) { + this.childNodes = this.childNodes.filter(value => value !== child); + this.children = this.children.filter(value => value !== child); + if (child) child.parentNode = null; + return child; + }; + node.remove = function() { this.parentNode?.removeChild?.(this); }; + node.before = function(...nodes) { + if (!this.parentNode) return; + for (const value of nodes) this.parentNode.insertBefore(value, this); + }; + node.after = function(...nodes) { + if (!this.parentNode) return; + const siblings = this.parentNode.childNodes; + let reference = siblings[siblings.indexOf(this) + 1] || null; + for (const value of nodes) { + this.parentNode.insertBefore(value, reference); + reference = siblings[siblings.indexOf(value) + 1] || null; + } + }; + node.replaceWith = function(...nodes) { + if (!this.parentNode) return; + const parent = this.parentNode; + for (const value of nodes) parent.insertBefore(value, this); + parent.removeChild(this); + }; + node.cloneNode = function(deep = false) { return cloneDOMNode(this, deep); }; + if (node.nodeType === 1) { + node.setAttribute = function(name, value) { this.attributes[name] = String(value); this[name] = String(value); }; + node.getAttribute = function(name) { return this.attributes[name] ?? null; }; + node.removeAttribute = function(name) { delete this.attributes[name]; delete this[name]; }; + } + for (const child of node.childNodes) { + installDOMMethods(child); + child.parentNode = node; + } + node.children = node.childNodes.filter(child => child.nodeType === 1); + return node; +}; + +globalThis.__mystralHydrateDOMTree = installDOMMethods; + +if (typeof globalThis.HTMLTemplateElement === 'undefined') { + globalThis.HTMLTemplateElement = class HTMLTemplateElement extends HTMLElement { + get innerHTML() { return this.__innerHTML || ''; } + set innerHTML(value) { + this.__innerHTML = String(value); + this.content = __parseHTMLTemplate(this.__innerHTML); + } + }; +} +globalThis.__mystralSetTemplatePrototype = value => { + Object.setPrototypeOf(value, HTMLTemplateElement.prototype); + value.content ||= __parseHTMLTemplate(''); + return value; +}; +)JS", + ""); + + if (g_debug && initialized) { + std::cout << "[DOM] Lexbor HTML template bindings initialized" << std::endl; + } + return initialized; +} + +} // namespace mystral::dom diff --git a/src/dom/html_template.cpp b/src/dom/html_template.cpp new file mode 100644 index 0000000..f6757e9 --- /dev/null +++ b/src/dom/html_template.cpp @@ -0,0 +1,131 @@ +#include "mystral/dom/html_template.h" + +#ifdef MYSTRAL_HAS_LEXBOR +#include +#endif + +namespace mystral::dom { + +#ifdef MYSTRAL_HAS_LEXBOR +namespace { + +std::string lexborString(const lxb_char_t *value, size_t length) { + return value ? std::string(reinterpret_cast(value), length) + : std::string(); +} + +std::unique_ptr copyNode(lxb_dom_node_t *node) { + auto result = std::make_unique(); + switch (node->type) { + case LXB_DOM_NODE_TYPE_ELEMENT: { + result->type = NodeType::Element; + size_t nameLength = 0; + const lxb_char_t *name = lxb_dom_node_name(node, &nameLength); + result->name = lexborString(name, nameLength); + + auto *element = lxb_dom_interface_element(node); + for (lxb_dom_attr_t *attribute = element->first_attr; attribute != nullptr; + attribute = attribute->next) { + size_t attributeNameLength = 0; + size_t attributeValueLength = 0; + const lxb_char_t *attributeName = + lxb_dom_attr_qualified_name(attribute, &attributeNameLength); + const lxb_char_t *attributeValue = + lxb_dom_attr_value(attribute, &attributeValueLength); + result->attributes.emplace_back( + lexborString(attributeName, attributeNameLength), + lexborString(attributeValue, attributeValueLength)); + } + break; + } + case LXB_DOM_NODE_TYPE_TEXT: + result->type = NodeType::Text; + break; + case LXB_DOM_NODE_TYPE_COMMENT: + result->type = NodeType::Comment; + break; + case LXB_DOM_NODE_TYPE_DOCUMENT_FRAGMENT: + result->type = NodeType::DocumentFragment; + break; + default: + return nullptr; + } + + if (node->type == LXB_DOM_NODE_TYPE_TEXT || + node->type == LXB_DOM_NODE_TYPE_COMMENT) { + size_t textLength = 0; + const lxb_char_t *text = lxb_dom_node_text_content(node, &textLength); + result->text = lexborString(text, textLength); + } + + for (lxb_dom_node_t *child = node->first_child; child != nullptr; + child = child->next) { + auto copiedChild = copyNode(child); + if (copiedChild) { + result->children.push_back(std::move(copiedChild)); + } + } + return result; +} + +} // namespace +#endif + +std::unique_ptr parseHTMLTemplate(const std::string &html, + std::string &error) { +#ifdef MYSTRAL_HAS_LEXBOR + lxb_html_document_t *document = lxb_html_document_create(); + if (!document) { + error = "Failed to create Lexbor HTML document"; + return nullptr; + } + + static constexpr char emptyDocument[] = + ""; + lxb_status_t status = lxb_html_document_parse( + document, reinterpret_cast(emptyDocument), + sizeof(emptyDocument) - 1); + if (status != LXB_STATUS_OK) { + error = "Failed to initialize Lexbor HTML document"; + lxb_html_document_destroy(document); + return nullptr; + } + + lxb_dom_element_t *context = lxb_dom_document_create_element( + &document->dom_document, reinterpret_cast("template"), + 8, nullptr); + if (!context) { + error = "Failed to create Lexbor template context"; + lxb_html_document_destroy(document); + return nullptr; + } + + lxb_dom_node_t *fragment = lxb_html_document_parse_fragment( + document, context, reinterpret_cast(html.data()), + html.size()); + if (!fragment) { + error = "Failed to parse HTML template fragment"; + lxb_html_document_destroy(document); + return nullptr; + } + + auto result = copyNode(fragment); + lxb_html_document_destroy(document); + if (!result) { + error = "Failed to copy parsed HTML template tree"; + } else { + // Lexbor's fragment parser returns its temporary context root. Expose that + // root as the DocumentFragment represented by template.content. + result->type = NodeType::DocumentFragment; + result->name.clear(); + result->attributes.clear(); + } + return result; +#else + (void)html; + error = "Lexbor HTML parsing is not enabled"; + return nullptr; +#endif +} + +} // namespace mystral::dom diff --git a/src/http/async_http_client.cpp b/src/http/async_http_client.cpp index 6841db8..2616383 100644 --- a/src/http/async_http_client.cpp +++ b/src/http/async_http_client.cpp @@ -475,8 +475,13 @@ void AsyncHttpClient::request(const std::string& method, curl_easy_setopt(easy, CURLOPT_POSTFIELDS, ctx->postData.data()); curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, (long)ctx->postData.size()); } - } else if (method == "DELETE") { - curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (method != "GET") { + curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method.c_str()); + if (!body.empty()) { + ctx->postData = body; + curl_easy_setopt(easy, CURLOPT_POSTFIELDS, ctx->postData.data()); + curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, (long)ctx->postData.size()); + } } // Set write callback @@ -558,6 +563,7 @@ namespace http { struct AsyncHttpClient::Impl { bool initialized = false; + std::queue> completedQueue; }; AsyncHttpClient& AsyncHttpClient::instance() { @@ -579,20 +585,14 @@ bool AsyncHttpClient::isReady() const { return false; } void AsyncHttpClient::get(const std::string& url, AsyncHttpCallback callback, const HttpOptions& options) { - HttpResponse response; - response.ok = false; - response.error = "Async HTTP not available"; - if (callback) callback(std::move(response)); + request("GET", url, {}, std::move(callback), options); } void AsyncHttpClient::post(const std::string& url, const std::vector& body, AsyncHttpCallback callback, const HttpOptions& options) { - HttpResponse response; - response.ok = false; - response.error = "Async HTTP not available"; - if (callback) callback(std::move(response)); + request("POST", url, body, std::move(callback), options); } void AsyncHttpClient::request(const std::string& method, @@ -602,13 +602,26 @@ void AsyncHttpClient::request(const std::string& method, const HttpOptions& options) { HttpResponse response; response.ok = false; + response.url = url; response.error = "Async HTTP not available"; - if (callback) callback(std::move(response)); + if (callback) { + impl_->completedQueue.emplace(std::move(callback), std::move(response)); + } } int AsyncHttpClient::activeRequestCount() const { return 0; } -bool AsyncHttpClient::processCompletedRequests() { return false; } +bool AsyncHttpClient::processCompletedRequests() { + const bool hadCallbacks = !impl_->completedQueue.empty(); + while (!impl_->completedQueue.empty()) { + auto completed = std::move(impl_->completedQueue.front()); + impl_->completedQueue.pop(); + if (completed.first) { + completed.first(std::move(completed.second)); + } + } + return hadCallbacks; +} } // namespace http } // namespace mystral diff --git a/src/platform/surface_metal.mm b/src/platform/surface_metal.mm index 5e922d8..b68b3b8 100644 --- a/src/platform/surface_metal.mm +++ b/src/platform/surface_metal.mm @@ -4,6 +4,8 @@ * Gets the CAMetalLayer from SDL's Metal view for WebGPU surface creation. */ +#include "mystral/platform/window.h" + #include #if defined(__APPLE__) @@ -40,6 +42,32 @@ return layer; } +#if defined(MYSTRAL_HAS_WEBGL) +void* createWebGLMetalLayer(void* metalView) { + CAMetalLayer* webgpuLayer = + (__bridge CAMetalLayer*)getMetalLayerFromView(metalView); + if (!webgpuLayer) { + return nullptr; + } + + CALayer* webglLayer = [CALayer layer]; + webglLayer.frame = webgpuLayer.bounds; + webglLayer.contentsScale = webgpuLayer.contentsScale; + webglLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; + [webgpuLayer addSublayer:webglLayer]; + std::cout << "[Surface] Created ANGLE presentation layer" << std::endl; + return (__bridge void*)webglLayer; +} + +void destroyWebGLMetalLayer(void* metalLayer) { + if (!metalLayer) { + return; + } + CALayer* layer = (__bridge CALayer*)metalLayer; + [layer removeFromSuperlayer]; +} +#endif + /** * Get the drawable size of the Metal layer (accounts for Retina scaling) */ diff --git a/src/platform/window.cpp b/src/platform/window.cpp index 0b9bb75..0a7e178 100644 --- a/src/platform/window.cpp +++ b/src/platform/window.cpp @@ -6,6 +6,7 @@ */ #include "mystral/platform/input.h" +#include "mystral/platform/window.h" #include #include #include @@ -29,6 +30,9 @@ struct Window { SDL_Window* sdlWindow = nullptr; #if defined(__APPLE__) SDL_MetalView metalView = nullptr; +#if defined(MYSTRAL_HAS_WEBGL) + void* webglMetalLayer = nullptr; +#endif #endif int width = 800; int height = 600; @@ -71,7 +75,25 @@ bool createWindow(const char* title, int width, int height, bool fullscreen, boo flags |= SDL_WINDOW_VULKAN; #endif +#if defined(__linux__) && defined(MYSTRAL_HAS_WEBGL) + SDL_PropertiesID createProperties = SDL_CreateProperties(); + if (!createProperties) { + std::cerr << "[Window] SDL_CreateProperties failed: " << SDL_GetError() << std::endl; + return false; + } + SDL_SetStringProperty(createProperties, SDL_PROP_WINDOW_CREATE_TITLE_STRING, title); + SDL_SetNumberProperty(createProperties, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width); + SDL_SetNumberProperty(createProperties, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height); + SDL_SetNumberProperty(createProperties, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, + static_cast(flags)); + SDL_SetBooleanProperty( + createProperties, + SDL_PROP_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN, true); + g_window.sdlWindow = SDL_CreateWindowWithProperties(createProperties); + SDL_DestroyProperties(createProperties); +#else g_window.sdlWindow = SDL_CreateWindow(title, width, height, flags); +#endif if (!g_window.sdlWindow) { std::cerr << "[Window] SDL_CreateWindow failed: " << SDL_GetError() << std::endl; @@ -97,6 +119,12 @@ bool createWindow(const char* title, int width, int height, bool fullscreen, boo std::cerr << "[Window] SDL_Metal_CreateView failed: " << SDL_GetError() << std::endl; } else { std::cout << "[Window] Metal view created" << std::endl; +#if defined(MYSTRAL_HAS_WEBGL) + g_window.webglMetalLayer = createWebGLMetalLayer(g_window.metalView); + if (!g_window.webglMetalLayer) { + std::cerr << "[Window] Failed to create ANGLE presentation layer" << std::endl; + } +#endif } #endif @@ -111,6 +139,12 @@ void destroyWindow() { std::cout << "[Window] Destroying window..." << std::endl; #if defined(__APPLE__) +#if defined(MYSTRAL_HAS_WEBGL) + if (g_window.webglMetalLayer) { + destroyWebGLMetalLayer(g_window.webglMetalLayer); + g_window.webglMetalLayer = nullptr; + } +#endif if (g_window.metalView) { SDL_Metal_DestroyView(g_window.metalView); g_window.metalView = nullptr; @@ -224,6 +258,14 @@ void* getMetalLayer() { return nullptr; } +void* getWebGLMetalLayer() { +#if defined(__APPLE__) && defined(MYSTRAL_HAS_WEBGL) + return g_window.webglMetalLayer; +#else + return nullptr; +#endif +} + /** * Get window dimensions */ diff --git a/src/runtime.cpp b/src/runtime.cpp index 9bf911c..762b355 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -2,10 +2,17 @@ #include "mystral/platform/window.h" #include "mystral/platform/input.h" #include "mystral/webgpu/context.h" +#ifdef MYSTRAL_HAS_WEBGL +#include "mystral/webgl/context.h" +#endif #include "mystral/js/engine.h" #include "mystral/js/module_system.h" +#ifdef MYSTRAL_HAS_LEXBOR +#include "mystral/dom/bindings.h" +#endif #include "mystral/http/http_client.h" #include "mystral/http/async_http_client.h" +#include "mystral/websocket/client.h" #include "mystral/webtransport/webtransport.h" #include "mystral/fs/async_file.h" #include "mystral/fs/file_watcher.h" @@ -43,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -411,8 +419,9 @@ class RuntimeImpl : public Runtime { // Set up Node.js-compatible process object (process.exit, etc.) setupProcess(); - // Set up fetch API + // Set up fetch and WebSocket APIs setupFetch(); + setupWebSocket(); // Set up WebTransport API (QUIC/HTTP3 via quiche; stubbed if not built) webtransport::initBindings(jsEngine_.get()); @@ -426,6 +435,13 @@ class RuntimeImpl : public Runtime { // Set up DOM event system (document, window, addEventListener, etc.) setupDOMEvents(); +#ifdef MYSTRAL_HAS_LEXBOR + if (!dom::initBindings(jsEngine_.get(), config_.debug)) { + std::cerr << "[Mystral] Failed to initialize Lexbor DOM bindings" << std::endl; + return false; + } +#endif + // Set up localStorage/sessionStorage (file-backed persistence) setupStorage(); @@ -496,6 +512,15 @@ class RuntimeImpl : public Runtime { rt::cleanupRTBindings(); #endif + // Stop WebSocket worker threads before libcurl is shut down by the HTTP client. + websocket::getClientManager().shutdown(); + if (jsEngine_) { + for (auto& [id, callback] : webSocketCallbacks_) { + jsEngine_->unprotect(callback); + } + } + webSocketCallbacks_.clear(); + // Shutdown async HTTP client (cancels pending requests) http::getAsyncHttpClient().shutdown(); @@ -556,6 +581,11 @@ class RuntimeImpl : public Runtime { jsEngine_->gc(); // Run twice for good measure } +#ifdef MYSTRAL_HAS_WEBGL + // Destroy ANGLE contexts while the SDL native window still exists. + webgl::shutdownBindings(); +#endif + jsEngine_.reset(); // Release JS engine webgpu_.reset(); // Release WebGPU resources if (!config_.noSdl) { @@ -771,7 +801,8 @@ class RuntimeImpl : public Runtime { // This must be called after runOnce() to invoke callbacks safely on the main thread http::getAsyncHttpClient().processCompletedRequests(); - // Drive WebTransport QUIC sessions and dispatch their JS events (main thread) + // Dispatch WebSocket and WebTransport events on the JavaScript main thread. + processWebSocketEvents(); webtransport::processEvents(); // Process completed async file reads (queues their callbacks) @@ -808,6 +839,11 @@ class RuntimeImpl : public Runtime { // Execute requestAnimationFrame callbacks (renders a frame) executeAnimationFrameCallbacks(); +#ifdef MYSTRAL_HAS_WEBGL + // WebGL drawing buffers are presented at the frame-compositing boundary. + webgl::presentContexts(); +#endif + // Free non-protected handles, per-frame native allocations, and Dawn resources jsEngine_->clearFrameHandles(); webgpu::endDawnFrame(); @@ -1645,6 +1681,11 @@ class RuntimeImpl : public Runtime { jsEngine_->setProperty(result, "ok", jsEngine_->newBoolean(response.ok)); jsEngine_->setProperty(result, "status", jsEngine_->newNumber(response.status)); jsEngine_->setProperty(result, "url", jsEngine_->newString(response.url.c_str())); + auto responseHeaders = jsEngine_->newObject(); + for (const auto& [name, value] : response.headers) { + jsEngine_->setProperty(responseHeaders, name.c_str(), jsEngine_->newString(value.c_str())); + } + jsEngine_->setProperty(result, "headers", responseHeaders); if (!response.error.empty()) { jsEngine_->setProperty(result, "error", jsEngine_->newString(response.error.c_str())); @@ -1700,6 +1741,19 @@ class RuntimeImpl : public Runtime { } } } + + auto headersVal = jsEngine_->getProperty(optObj, "headers"); + if (jsEngine_->isArray(headersVal)) { + auto lengthVal = jsEngine_->getProperty(headersVal, "length"); + auto length = static_cast(jsEngine_->toNumber(lengthVal)); + for (uint32_t index = 0; index < length; ++index) { + auto pair = jsEngine_->getPropertyIndex(headersVal, index); + if (!jsEngine_->isArray(pair)) continue; + auto name = jsEngine_->toString(jsEngine_->getPropertyIndex(pair, 0)); + auto value = jsEngine_->toString(jsEngine_->getPropertyIndex(pair, 1)); + if (!name.empty()) options.headers[name] = value; + } + } } // Get and protect the callback @@ -1718,6 +1772,11 @@ class RuntimeImpl : public Runtime { engine->setProperty(result, "ok", engine->newBoolean(response.ok)); engine->setProperty(result, "status", engine->newNumber(response.status)); engine->setProperty(result, "url", engine->newString(response.url.c_str())); + auto responseHeaders = engine->newObject(); + for (const auto& [name, value] : response.headers) { + engine->setProperty(responseHeaders, name.c_str(), engine->newString(value.c_str())); + } + engine->setProperty(result, "headers", responseHeaders); if (!response.error.empty()) { engine->setProperty(result, "error", engine->newString(response.error.c_str())); @@ -1950,7 +2009,13 @@ class Headers { } set(name, value) { - this._headers.set(name.toLowerCase(), value); + this._headers.set(name.toLowerCase(), String(value)); + } + + append(name, value) { + const key = name.toLowerCase(); + const previous = this._headers.get(key); + this._headers.set(key, previous ? `${previous}, ${value}` : String(value)); } has(name) { @@ -2083,7 +2148,11 @@ async function fetch(input, options = {}) { // HTTP/HTTPS request via async libcurl + libuv (non-blocking) return new Promise((resolve, reject) => { if (signal) signal.addEventListener('abort', () => reject(abortError())); - __httpRequestAsync(url, options, (result) => { + const nativeOptions = { + ...options, + headers: options.headers ? [...new Headers(options.headers).entries()] : [] + }; + __httpRequestAsync(url, nativeOptions, (result) => { if (result.error) { reject(new Error('Fetch error: ' + result.error)); } else { @@ -2091,7 +2160,8 @@ async function fetch(input, options = {}) { ok: result.ok, status: result.status, statusText: result.ok ? 'OK' : 'Error', - url: result.url || url + url: result.url || url, + headers: result.headers || {} })); } }); @@ -2142,6 +2212,173 @@ globalThis.Response = Response; jsEngine_->eval(fetchPolyfill, "fetch-polyfill.js"); std::cout << "[Mystral] Fetch API initialized (file://, http://, https://)" << std::endl; + const char* xhrPolyfill = R"XHR( +if (typeof globalThis.XMLHttpRequest === 'undefined') { + class XMLHttpRequest { + static UNSENT = 0; + static OPENED = 1; + static HEADERS_RECEIVED = 2; + static LOADING = 3; + static DONE = 4; + + constructor() { + this.readyState = XMLHttpRequest.UNSENT; + this.response = null; + this.responseText = ''; + this.responseType = ''; + this.responseURL = ''; + this.status = 0; + this.statusText = ''; + this.timeout = 0; + this.withCredentials = false; + this.onreadystatechange = null; + this.onload = null; + this.onerror = null; + this.onloadend = null; + this.onabort = null; + this.onloadstart = null; + this.onprogress = null; + this.ontimeout = null; + this.responseXML = null; + this._method = 'GET'; + this._url = ''; + this._headers = new Headers(); + this._responseHeaders = new Headers(); + this._listeners = new Map(); + this._aborted = false; + this.upload = { addEventListener() {}, removeEventListener() {} }; + } + + addEventListener(type, callback) { + const listeners = this._listeners.get(type) || []; + listeners.push(callback); + this._listeners.set(type, listeners); + } + + removeEventListener(type, callback) { + const listeners = this._listeners.get(type) || []; + this._listeners.set(type, listeners.filter(listener => listener !== callback)); + } + + _dispatch(type, properties = {}) { + const event = { type, target: this, currentTarget: this, ...properties }; + const handler = this['on' + type]; + if (typeof handler === 'function') handler.call(this, event); + for (const listener of this._listeners.get(type) || []) listener.call(this, event); + } + + _setReadyState(state) { + this.readyState = state; + this._dispatch('readystatechange'); + } + + open(method, url, async = true, username, password) { + if (!async) throw new Error('Synchronous XMLHttpRequest is not supported'); + this._method = String(method || 'GET').toUpperCase(); + this._url = String(url); + this._setReadyState(XMLHttpRequest.OPENED); + } + + setRequestHeader(name, value) { + if (this.readyState !== XMLHttpRequest.OPENED) { + throw new Error('InvalidStateError'); + } + this._headers.append(name, value); + } + + getResponseHeader(name) { + return this._responseHeaders.get(name); + } + + getAllResponseHeaders() { + return [...this._responseHeaders.entries()] + .map(([name, value]) => `${name}: ${value}\r\n`) + .join(''); + } + + overrideMimeType() {} + + abort() { + this._aborted = true; + this.status = 0; + this.response = null; + this.responseText = ''; + this._setReadyState(XMLHttpRequest.DONE); + this._dispatch('abort'); + this._dispatch('loadend'); + } + + async send(body = null) { + if (this.readyState !== XMLHttpRequest.OPENED) { + throw new Error('InvalidStateError'); + } + this._aborted = false; + let timeoutHandle = null; + if (this.timeout > 0) { + timeoutHandle = setTimeout(() => { + if (this.readyState === XMLHttpRequest.DONE) return; + this._aborted = true; + this.status = 0; + this._setReadyState(XMLHttpRequest.DONE); + this._dispatch('timeout'); + this._dispatch('loadend'); + }, this.timeout); + } + this._dispatch('loadstart'); + try { + const response = await fetch(this._url, { + method: this._method, + headers: this._headers, + body, + credentials: this.withCredentials ? 'include' : 'same-origin' + }); + if (this._aborted) return; + + this.status = response.status; + this.statusText = response.statusText; + this.responseURL = response.url; + this._responseHeaders = response.headers; + this._setReadyState(XMLHttpRequest.HEADERS_RECEIVED); + this._setReadyState(XMLHttpRequest.LOADING); + + const data = await response.arrayBuffer(); + if (this._aborted) return; + const text = new TextDecoder().decode(data); + this._dispatch('progress', { loaded: data.byteLength, total: data.byteLength, lengthComputable: true }); + switch (this.responseType) { + case 'arraybuffer': this.response = data; break; + case 'blob': this.response = new Blob([data]); break; + case 'json': this.response = text ? JSON.parse(text) : null; break; + case 'document': this.response = null; break; + default: + this.response = text; + this.responseText = text; + break; + } + if (timeoutHandle !== null) clearTimeout(timeoutHandle); + this._setReadyState(XMLHttpRequest.DONE); + this._dispatch('load'); + this._dispatch('loadend'); + } catch (error) { + if (this._aborted) return; + if (timeoutHandle !== null) clearTimeout(timeoutHandle); + this.status = 0; + this.statusText = ''; + this._setReadyState(XMLHttpRequest.DONE); + this._dispatch('error'); + this._dispatch('loadend'); + } + } + } + for (const key of ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE']) { + XMLHttpRequest.prototype[key] = XMLHttpRequest[key]; + } + globalThis.XMLHttpRequest = XMLHttpRequest; +} +)XHR"; + jsEngine_->eval(xhrPolyfill, "xhr-polyfill.js"); + std::cout << "[Mystral] XMLHttpRequest API initialized" << std::endl; + // --- WHATWG Streams ------------------------------------------------- // Real (spec-shaped) ReadableStream / WritableStream / TransformStream // plus TextEncoderStream / TextDecoderStream. These back the @@ -2439,6 +2676,221 @@ globalThis.Response = Response; std::cout << "[Mystral] Web Streams API initialized (ReadableStream/WritableStream/TransformStream)" << std::endl; } + void setupWebSocket() { + if (!jsEngine_) return; + + jsEngine_->setGlobalProperty("__webSocketConnect", + jsEngine_->newFunction("__webSocketConnect", [this](void*, const std::vector& args) { + if (args.size() < 3 || !jsEngine_->isFunction(args[2])) { + jsEngine_->throwException("WebSocket connect requires a URL, protocols, and callback"); + return jsEngine_->newUndefined(); + } + std::string url = jsEngine_->toString(args[0]); + std::vector protocols; + if (jsEngine_->isArray(args[1])) { + auto lengthValue = jsEngine_->getProperty(args[1], "length"); + auto length = static_cast(jsEngine_->toNumber(lengthValue)); + protocols.reserve(length); + for (uint32_t index = 0; index < length; ++index) { + protocols.push_back(jsEngine_->toString(jsEngine_->getPropertyIndex(args[1], index))); + } + } + auto callback = args[2]; + jsEngine_->protect(callback); + uint64_t id = websocket::getClientManager().connect(url, protocols); + webSocketCallbacks_[id] = callback; + return jsEngine_->newNumber(static_cast(id)); + }) + ); + + jsEngine_->setGlobalProperty("__webSocketSend", + jsEngine_->newFunction("__webSocketSend", [this](void*, const std::vector& args) { + if (args.size() < 3) return jsEngine_->newBoolean(false); + uint64_t id = static_cast(jsEngine_->toNumber(args[0])); + bool binary = jsEngine_->toBoolean(args[2]); + std::vector data; + if (jsEngine_->isString(args[1])) { + std::string text = jsEngine_->toString(args[1]); + data.assign(text.begin(), text.end()); + binary = false; + } else { + size_t size = 0; + void* source = jsEngine_->getArrayBufferData(args[1], &size); + if (!source && size > 0) return jsEngine_->newBoolean(false); + if (size > 0) { + auto* bytes = static_cast(source); + data.assign(bytes, bytes + size); + } + } + return jsEngine_->newBoolean(websocket::getClientManager().send(id, std::move(data), binary)); + }) + ); + + jsEngine_->setGlobalProperty("__webSocketClose", + jsEngine_->newFunction("__webSocketClose", [this](void*, const std::vector& args) { + if (args.empty()) return jsEngine_->newUndefined(); + uint64_t id = static_cast(jsEngine_->toNumber(args[0])); + uint16_t code = args.size() > 1 ? static_cast(jsEngine_->toNumber(args[1])) : 1000; + std::string reason = args.size() > 2 ? jsEngine_->toString(args[2]) : std::string{}; + websocket::getClientManager().close(id, code, reason); + return jsEngine_->newUndefined(); + }) + ); + + const char* webSocketPolyfill = R"WEBSOCKET( +(function () { + if (typeof globalThis.WebSocket !== 'undefined') return; + + class WebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + constructor(url, protocols = []) { + this.url = String(url); + this.readyState = WebSocket.CONNECTING; + this.bufferedAmount = 0; + this.extensions = ''; + this.protocol = ''; + this.binaryType = 'blob'; + this.onopen = null; + this.onmessage = null; + this.onerror = null; + this.onclose = null; + this._listeners = new Map(); + + const protocolList = typeof protocols === 'string' ? [protocols] : Array.from(protocols || [], String); + if (new Set(protocolList).size !== protocolList.length) { + throw new Error('SyntaxError: duplicate WebSocket protocol'); + } + this._id = __webSocketConnect(this.url, protocolList, event => this._handleNativeEvent(event)); + } + + addEventListener(type, callback) { + if (typeof callback !== 'function') return; + const listeners = this._listeners.get(type) || []; + listeners.push(callback); + this._listeners.set(type, listeners); + } + + removeEventListener(type, callback) { + const listeners = this._listeners.get(type) || []; + this._listeners.set(type, listeners.filter(listener => listener !== callback)); + } + + dispatchEvent(event) { + event.target = this; + event.currentTarget = this; + const handler = this['on' + event.type]; + if (typeof handler === 'function') handler.call(this, event); + for (const listener of this._listeners.get(event.type) || []) listener.call(this, event); + return true; + } + + _handleNativeEvent(event) { + if (event.type === 'open') { + this.readyState = WebSocket.OPEN; + this.protocol = event.protocol || ''; + this.dispatchEvent({ type: 'open' }); + return; + } + if (event.type === 'message') { + let data = event.data; + if (event.binary && this.binaryType === 'blob') data = new Blob([data]); + this.dispatchEvent({ type: 'message', data, origin: this.url, lastEventId: '' }); + return; + } + if (event.type === 'error') { + this.dispatchEvent({ type: 'error', message: event.message || 'WebSocket error' }); + return; + } + if (event.type === 'close') { + this.readyState = WebSocket.CLOSED; + this.dispatchEvent({ + type: 'close', + code: event.code, + reason: event.reason || '', + wasClean: !!event.wasClean + }); + } + } + + send(data) { + if (this.readyState === WebSocket.CONNECTING) throw new Error('InvalidStateError'); + if (this.readyState !== WebSocket.OPEN) return; + + let payload = data; + let binary = typeof data !== 'string'; + if (typeof Blob !== 'undefined' && data instanceof Blob) payload = data._data; + if (binary && !(payload instanceof ArrayBuffer) && !ArrayBuffer.isView(payload)) { + throw new TypeError('WebSocket.send supports strings, ArrayBuffers, typed arrays, and Blobs'); + } + this.bufferedAmount = typeof payload === 'string' ? payload.length : payload.byteLength; + if (!__webSocketSend(this._id, payload, binary)) throw new Error('WebSocket send failed'); + this.bufferedAmount = 0; + } + + close(code = 1000, reason = '') { + if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) return; + if (code !== 1000 && (code < 3000 || code > 4999)) throw new Error('InvalidAccessError'); + if (new TextEncoder().encode(String(reason)).byteLength > 123) throw new Error('SyntaxError'); + this.readyState = WebSocket.CLOSING; + __webSocketClose(this._id, code, String(reason)); + } + } + + for (const key of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) { + WebSocket.prototype[key] = WebSocket[key]; + } + globalThis.WebSocket = WebSocket; +})(); +)WEBSOCKET"; + jsEngine_->eval(webSocketPolyfill, "websocket-polyfill.js"); + std::cout << "[Mystral] WebSocket API initialized" << std::endl; + } + + void processWebSocketEvents() { + if (!jsEngine_) return; + for (auto& event : websocket::getClientManager().pollEvents()) { + auto callbackIt = webSocketCallbacks_.find(event.connectionId); + if (callbackIt == webSocketCallbacks_.end()) continue; + + auto value = jsEngine_->newObject(); + switch (event.type) { + case websocket::EventType::Open: + jsEngine_->setProperty(value, "type", jsEngine_->newString("open")); + jsEngine_->setProperty(value, "protocol", jsEngine_->newString(event.protocol.c_str())); + break; + case websocket::EventType::Message: + jsEngine_->setProperty(value, "type", jsEngine_->newString("message")); + jsEngine_->setProperty(value, "binary", jsEngine_->newBoolean(event.binary)); + if (event.binary) { + jsEngine_->setProperty(value, "data", jsEngine_->newArrayBuffer(event.data.data(), event.data.size())); + } else { + jsEngine_->setProperty(value, "data", jsEngine_->newString(event.text.c_str())); + } + break; + case websocket::EventType::Error: + jsEngine_->setProperty(value, "type", jsEngine_->newString("error")); + jsEngine_->setProperty(value, "message", jsEngine_->newString(event.text.c_str())); + break; + case websocket::EventType::Close: + jsEngine_->setProperty(value, "type", jsEngine_->newString("close")); + jsEngine_->setProperty(value, "code", jsEngine_->newNumber(event.closeCode)); + jsEngine_->setProperty(value, "reason", jsEngine_->newString(event.text.c_str())); + jsEngine_->setProperty(value, "wasClean", jsEngine_->newBoolean(event.clean)); + break; + } + + jsEngine_->call(callbackIt->second, jsEngine_->newUndefined(), {value}); + if (event.type == websocket::EventType::Close) { + jsEngine_->unprotect(callbackIt->second); + webSocketCallbacks_.erase(callbackIt); + } + } + } + void setupURL() { if (!jsEngine_) return; @@ -3385,6 +3837,7 @@ globalThis.__mystralNativeDecodeDracoAsync = function(buffer, attrs) { std::unique_ptr jsEngine_; std::unique_ptr moduleSystem_; storage::LocalStorage localStorage_; + std::unordered_map webSocketCallbacks_; // requestAnimationFrame state struct RAFCallback { @@ -3697,8 +4150,14 @@ globalThis.__mystralNativeDecodeDracoAsync = function(buffer, attrs) { auto el = args[0]; auto onload = jsEngine_->getProperty(el, "onload"); if (!jsEngine_->isUndefined(onload) && !jsEngine_->isNull(onload)) { - // Call onload via setTimeout to simulate async loading - jsEngine_->eval("setTimeout(() => { arguments[0] && arguments[0](); }, 0);", "onload-trigger"); + // Schedule the callback through the runtime timer queue so script + // loading remains asynchronous without relying on eval arguments. + auto setTimeout = jsEngine_->getGlobalProperty("setTimeout"); + std::vector timeoutArgs = { + onload, + jsEngine_->newNumber(0) + }; + jsEngine_->call(setTimeout, jsEngine_->newUndefined(), timeoutArgs); } } return jsEngine_->newUndefined(); diff --git a/src/webgl/bindings.cpp b/src/webgl/bindings.cpp new file mode 100644 index 0000000..fdcd036 --- /dev/null +++ b/src/webgl/bindings.cpp @@ -0,0 +1,1380 @@ +#include "mystral/webgl/context.h" + +#if defined(MYSTRAL_HAS_WEBGL) + +#define GL_GLES_PROTOTYPES 0 +#include +#include + +#include "mystral/platform/window.h" + +#include +#include +#include +#include + +namespace mystral::webgl { + +namespace { + +js::Engine *g_engine = nullptr; +bool g_debug = false; +NativeWindow g_nativeWindow; +bool g_windowContextClaimed = false; +bool g_presentFailureLogged = false; +std::vector> g_contexts; + +uint32_t toUint32(js::JSValueHandle value) { + return static_cast(g_engine->toNumber(value)); +} + +int32_t toInt32(js::JSValueHandle value) { + return static_cast(g_engine->toNumber(value)); +} + +float toFloat(js::JSValueHandle value) { + return static_cast(g_engine->toNumber(value)); +} + +js::JSValueHandle wrapGLObject(uint32_t id, const char *type) { + if (id == 0) { + return g_engine->newNull(); + } + auto object = g_engine->newObject(); + g_engine->setPrivateData( + object, reinterpret_cast(static_cast(id) + 1)); + g_engine->setProperty(object, "_id", g_engine->newNumber(id)); + g_engine->setProperty(object, "_type", g_engine->newString(type)); + return object; +} + +uint32_t unwrapGLObject(js::JSValueHandle value) { + if (g_engine->isNull(value) || g_engine->isUndefined(value)) { + return 0; + } + if (g_engine->isNumber(value)) { + return toUint32(value); + } + const uintptr_t encoded = + reinterpret_cast(g_engine->getPrivateData(value)); + return encoded > 0 ? static_cast(encoded - 1) : 0; +} + +js::JSValueHandle wrapUniformLocation(int32_t location) { + if (location < 0) { + return g_engine->newNull(); + } + auto object = g_engine->newObject(); + g_engine->setPrivateData( + object, reinterpret_cast(static_cast(location) + 1)); + g_engine->setProperty(object, "_id", g_engine->newNumber(location)); + g_engine->setProperty(object, "_type", + g_engine->newString("uniformLocation")); + return object; +} + +bool requireArguments(const std::vector &args, size_t count, + const char *method) { + if (args.size() >= count) { + return true; + } + const std::string message = + std::string(method) + " requires " + std::to_string(count) + " arguments"; + g_engine->throwException(message.c_str()); + return false; +} + +template +std::vector readNumericArray(js::JSValueHandle value) { + size_t byteLength = 0; + void *bytes = g_engine->getArrayBufferData(value, &byteLength); + if (bytes && byteLength >= sizeof(Value)) { + const auto *begin = static_cast(bytes); + return {begin, begin + byteLength / sizeof(Value)}; + } + + const uint32_t length = static_cast( + g_engine->toNumber(g_engine->getProperty(value, "length"))); + std::vector result(length); + for (uint32_t index = 0; index < length; ++index) { + result[index] = static_cast( + g_engine->toNumber(g_engine->getPropertyIndex(value, index))); + } + return result; +} + +const void *readPixelData(js::JSValueHandle value) { + if (g_engine->isNull(value) || g_engine->isUndefined(value)) { + return nullptr; + } + return g_engine->getArrayBufferData(value, nullptr); +} + +void setConstant(js::JSValueHandle object, const char *name, uint32_t value) { + g_engine->setProperty(object, name, g_engine->newNumber(value)); +} + +void installConstants(js::JSValueHandle object) { +#define WEBGL_CONSTANT(name) setConstant(object, #name, GL_##name) + WEBGL_CONSTANT(NO_ERROR); + WEBGL_CONSTANT(INVALID_ENUM); + WEBGL_CONSTANT(INVALID_VALUE); + WEBGL_CONSTANT(INVALID_OPERATION); + WEBGL_CONSTANT(OUT_OF_MEMORY); + WEBGL_CONSTANT(DEPTH_BUFFER_BIT); + WEBGL_CONSTANT(STENCIL_BUFFER_BIT); + WEBGL_CONSTANT(COLOR_BUFFER_BIT); + WEBGL_CONSTANT(POINTS); + WEBGL_CONSTANT(LINES); + WEBGL_CONSTANT(LINE_LOOP); + WEBGL_CONSTANT(LINE_STRIP); + WEBGL_CONSTANT(TRIANGLES); + WEBGL_CONSTANT(TRIANGLE_STRIP); + WEBGL_CONSTANT(TRIANGLE_FAN); + WEBGL_CONSTANT(ZERO); + WEBGL_CONSTANT(ONE); + WEBGL_CONSTANT(SRC_COLOR); + WEBGL_CONSTANT(ONE_MINUS_SRC_COLOR); + WEBGL_CONSTANT(SRC_ALPHA); + WEBGL_CONSTANT(ONE_MINUS_SRC_ALPHA); + WEBGL_CONSTANT(DST_ALPHA); + WEBGL_CONSTANT(ONE_MINUS_DST_ALPHA); + WEBGL_CONSTANT(DST_COLOR); + WEBGL_CONSTANT(ONE_MINUS_DST_COLOR); + WEBGL_CONSTANT(SRC_ALPHA_SATURATE); + WEBGL_CONSTANT(CONSTANT_COLOR); + WEBGL_CONSTANT(ONE_MINUS_CONSTANT_COLOR); + WEBGL_CONSTANT(CONSTANT_ALPHA); + WEBGL_CONSTANT(ONE_MINUS_CONSTANT_ALPHA); + WEBGL_CONSTANT(FUNC_ADD); + WEBGL_CONSTANT(FUNC_SUBTRACT); + WEBGL_CONSTANT(FUNC_REVERSE_SUBTRACT); + WEBGL_CONSTANT(MIN); + WEBGL_CONSTANT(MAX); + WEBGL_CONSTANT(ARRAY_BUFFER); + WEBGL_CONSTANT(ELEMENT_ARRAY_BUFFER); + WEBGL_CONSTANT(STATIC_DRAW); + WEBGL_CONSTANT(DYNAMIC_DRAW); + WEBGL_CONSTANT(STREAM_DRAW); + WEBGL_CONSTANT(FLOAT); + WEBGL_CONSTANT(HALF_FLOAT); + WEBGL_CONSTANT(INT); + WEBGL_CONSTANT(UNSIGNED_BYTE); + WEBGL_CONSTANT(UNSIGNED_SHORT); + WEBGL_CONSTANT(UNSIGNED_INT); + WEBGL_CONSTANT(UNSIGNED_SHORT_4_4_4_4); + WEBGL_CONSTANT(UNSIGNED_SHORT_5_5_5_1); + WEBGL_CONSTANT(RED); + WEBGL_CONSTANT(RG); + WEBGL_CONSTANT(RGB); + WEBGL_CONSTANT(RGBA); + WEBGL_CONSTANT(RED_INTEGER); + WEBGL_CONSTANT(RG_INTEGER); + WEBGL_CONSTANT(RGB_INTEGER); + WEBGL_CONSTANT(RGBA_INTEGER); + WEBGL_CONSTANT(R16F); + WEBGL_CONSTANT(RG16F); + WEBGL_CONSTANT(RGBA16F); + WEBGL_CONSTANT(R32F); + WEBGL_CONSTANT(RG32F); + WEBGL_CONSTANT(RGBA32F); + WEBGL_CONSTANT(RGBA8); + WEBGL_CONSTANT(DEPTH_COMPONENT24); + WEBGL_CONSTANT(VERTEX_SHADER); + WEBGL_CONSTANT(FRAGMENT_SHADER); + WEBGL_CONSTANT(COMPILE_STATUS); + WEBGL_CONSTANT(LINK_STATUS); + WEBGL_CONSTANT(VALIDATE_STATUS); + WEBGL_CONSTANT(DELETE_STATUS); + WEBGL_CONSTANT(INFO_LOG_LENGTH); + WEBGL_CONSTANT(ACTIVE_ATTRIBUTES); + WEBGL_CONSTANT(ACTIVE_UNIFORMS); + WEBGL_CONSTANT(FLOAT_MAT2); + WEBGL_CONSTANT(FLOAT_MAT3); + WEBGL_CONSTANT(FLOAT_MAT4); + WEBGL_CONSTANT(SAMPLER_2D_SHADOW); + WEBGL_CONSTANT(LOW_FLOAT); + WEBGL_CONSTANT(MEDIUM_FLOAT); + WEBGL_CONSTANT(HIGH_FLOAT); + WEBGL_CONSTANT(LOW_INT); + WEBGL_CONSTANT(MEDIUM_INT); + WEBGL_CONSTANT(HIGH_INT); + WEBGL_CONSTANT(RENDERER); + WEBGL_CONSTANT(VENDOR); + WEBGL_CONSTANT(VERSION); + WEBGL_CONSTANT(SHADING_LANGUAGE_VERSION); + WEBGL_CONSTANT(MAX_TEXTURE_SIZE); + WEBGL_CONSTANT(MAX_CUBE_MAP_TEXTURE_SIZE); + WEBGL_CONSTANT(MAX_VERTEX_ATTRIBS); + WEBGL_CONSTANT(MAX_TEXTURE_IMAGE_UNITS); + WEBGL_CONSTANT(MAX_VERTEX_TEXTURE_IMAGE_UNITS); + WEBGL_CONSTANT(MAX_COMBINED_TEXTURE_IMAGE_UNITS); + WEBGL_CONSTANT(MAX_VERTEX_UNIFORM_VECTORS); + WEBGL_CONSTANT(MAX_FRAGMENT_UNIFORM_VECTORS); + WEBGL_CONSTANT(MAX_VARYING_VECTORS); + WEBGL_CONSTANT(MAX_SAMPLES); + WEBGL_CONSTANT(MAX_UNIFORM_BUFFER_BINDINGS); + WEBGL_CONSTANT(NEVER); + WEBGL_CONSTANT(LESS); + WEBGL_CONSTANT(EQUAL); + WEBGL_CONSTANT(LEQUAL); + WEBGL_CONSTANT(GREATER); + WEBGL_CONSTANT(NOTEQUAL); + WEBGL_CONSTANT(GEQUAL); + WEBGL_CONSTANT(ALWAYS); + WEBGL_CONSTANT(CW); + WEBGL_CONSTANT(CCW); + WEBGL_CONSTANT(FRONT); + WEBGL_CONSTANT(BACK); + WEBGL_CONSTANT(CULL_FACE); + WEBGL_CONSTANT(DEPTH_TEST); + WEBGL_CONSTANT(STENCIL_TEST); + WEBGL_CONSTANT(SCISSOR_TEST); + WEBGL_CONSTANT(POLYGON_OFFSET_FILL); + WEBGL_CONSTANT(SAMPLE_ALPHA_TO_COVERAGE); + WEBGL_CONSTANT(SCISSOR_BOX); + WEBGL_CONSTANT(VIEWPORT); + WEBGL_CONSTANT(NONE); + WEBGL_CONSTANT(TEXTURE0); + WEBGL_CONSTANT(TEXTURE_2D); + WEBGL_CONSTANT(TEXTURE_3D); + WEBGL_CONSTANT(TEXTURE_2D_ARRAY); + WEBGL_CONSTANT(TEXTURE_CUBE_MAP); + WEBGL_CONSTANT(TEXTURE_CUBE_MAP_POSITIVE_X); + WEBGL_CONSTANT(TEXTURE_MAG_FILTER); + WEBGL_CONSTANT(TEXTURE_MIN_FILTER); + WEBGL_CONSTANT(TEXTURE_WRAP_S); + WEBGL_CONSTANT(TEXTURE_WRAP_T); + WEBGL_CONSTANT(NEAREST); + WEBGL_CONSTANT(LINEAR); + WEBGL_CONSTANT(NEAREST_MIPMAP_NEAREST); + WEBGL_CONSTANT(LINEAR_MIPMAP_NEAREST); + WEBGL_CONSTANT(NEAREST_MIPMAP_LINEAR); + WEBGL_CONSTANT(LINEAR_MIPMAP_LINEAR); + WEBGL_CONSTANT(REPEAT); + WEBGL_CONSTANT(CLAMP_TO_EDGE); + WEBGL_CONSTANT(MIRRORED_REPEAT); + WEBGL_CONSTANT(FRAMEBUFFER); + WEBGL_CONSTANT(DRAW_FRAMEBUFFER); + WEBGL_CONSTANT(RENDERBUFFER); + WEBGL_CONSTANT(COLOR_ATTACHMENT0); + WEBGL_CONSTANT(DEPTH_ATTACHMENT); + WEBGL_CONSTANT(UNPACK_ALIGNMENT); +#undef WEBGL_CONSTANT + setConstant(object, "UNPACK_FLIP_Y_WEBGL", 0x9240); + setConstant(object, "UNPACK_PREMULTIPLY_ALPHA_WEBGL", 0x9241); + setConstant(object, "UNPACK_COLORSPACE_CONVERSION_WEBGL", 0x9243); +} + +ContextAttributes +readContextAttributes(const std::vector &args) { + ContextAttributes attributes; + if (args.size() < 2 || !g_engine->isObject(args[1])) { + return attributes; + } + + const auto readBoolean = [&](const char *name, bool fallback) { + auto value = g_engine->getProperty(args[1], name); + return g_engine->isUndefined(value) ? fallback : g_engine->toBoolean(value); + }; + attributes.alpha = readBoolean("alpha", attributes.alpha); + attributes.depth = readBoolean("depth", attributes.depth); + attributes.stencil = readBoolean("stencil", attributes.stencil); + attributes.antialias = readBoolean("antialias", attributes.antialias); + attributes.premultipliedAlpha = + readBoolean("premultipliedAlpha", attributes.premultipliedAlpha); + attributes.preserveDrawingBuffer = + readBoolean("preserveDrawingBuffer", attributes.preserveDrawingBuffer); + + auto powerPreference = g_engine->getProperty(args[1], "powerPreference"); + if (!g_engine->isUndefined(powerPreference)) { + attributes.preferHighPerformance = + g_engine->toString(powerPreference) != "low-power"; + } + return attributes; +} + +} // namespace + +ContextAttributes +contextAttributesFromJS(js::Engine *engine, + const std::vector &args) { + g_engine = engine; + return readContextAttributes(args); +} + +bool initBindings(js::Engine *engine, bool debug) { + if (!engine) { + return false; + } + g_engine = engine; + g_debug = debug; + g_windowContextClaimed = false; + g_presentFailureLogged = false; + + g_nativeWindow = {}; + SDL_Window *sdlWindow = platform::getSDLWindow(); + if (sdlWindow) { + SDL_PropertiesID properties = SDL_GetWindowProperties(sdlWindow); +#if defined(_WIN32) + void *window = SDL_GetPointerProperty( + properties, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); + if (window) { + g_nativeWindow = {NativeWindowPlatform::Win32, nullptr, + reinterpret_cast(window)}; + } +#elif defined(__APPLE__) + void *layer = platform::getWebGLMetalLayer(); + if (layer) { + g_nativeWindow = {NativeWindowPlatform::Metal, nullptr, + reinterpret_cast(layer)}; + } +#elif defined(__linux__) + void *waylandDisplay = SDL_GetPointerProperty( + properties, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr); + void *waylandWindow = SDL_GetPointerProperty( + properties, SDL_PROP_WINDOW_WAYLAND_EGL_WINDOW_POINTER, nullptr); + if (waylandDisplay && waylandWindow) { + g_nativeWindow = {NativeWindowPlatform::Wayland, waylandDisplay, + reinterpret_cast(waylandWindow)}; + } else { + void *x11Display = SDL_GetPointerProperty( + properties, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); + const auto x11Window = static_cast(SDL_GetNumberProperty( + properties, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)); + if (x11Display && x11Window) { + g_nativeWindow = {NativeWindowPlatform::X11, x11Display, x11Window}; + } + } +#endif + } + if (g_debug) { + std::cout << "[WebGL] Native window: 0x" << std::hex + << g_nativeWindow.window << std::dec << std::endl; + } + + return engine->evalScript(R"JS( +if (typeof globalThis.WebGLRenderingContext === "undefined") { + globalThis.WebGLRenderingContext = class WebGLRenderingContext {}; +} +if (typeof globalThis.WebGL2RenderingContext === "undefined") { + globalThis.WebGL2RenderingContext = class WebGL2RenderingContext extends WebGLRenderingContext {}; +} +globalThis.__mystralSetWebGL2Prototype = value => { + Object.setPrototypeOf(value, WebGL2RenderingContext.prototype); + return value; +}; +)JS", + ""); +} + +void presentContexts() { + for (const auto &context : g_contexts) { + if (context->isWindowSurface() && !context->present() && + !g_presentFailureLogged) { + std::cerr << "[WebGL] ANGLE window presentation failed" << std::endl; + g_presentFailureLogged = true; + } + } +} + +void shutdownBindings() { + g_contexts.clear(); + g_engine = nullptr; + g_nativeWindow = {}; + g_windowContextClaimed = false; + g_presentFailureLogged = false; +} + +js::JSValueHandle createContextJSObject(js::Engine *engine, uint32_t width, + uint32_t height, + const ContextAttributes &attributes) { + if (!engine) { + return {}; + } + g_engine = engine; + + auto context = std::make_unique(); + const NativeWindow nativeWindow = + !g_windowContextClaimed ? g_nativeWindow : NativeWindow{}; + ContextAttributes contextAttributes = attributes; + contextAttributes.allowNativeTextureInterop = + nativeWindow.platform == NativeWindowPlatform::Win32; + if (!context->initialize(width, height, contextAttributes, nativeWindow)) { + const std::string windowError = context->errorMessage(); + contextAttributes.allowNativeTextureInterop = false; + if (!nativeWindow || !context->initialize(width, height, contextAttributes)) { + std::cerr << "[WebGL] Context creation failed: " + << context->errorMessage() << std::endl; + return engine->newNull(); + } + std::cerr << "[WebGL] Window surface unavailable, using an offscreen " + "drawing buffer: " + << windowError << std::endl; + } + if (context->isWindowSurface()) { + g_windowContextClaimed = true; + } + + Context *capturedContext = context.get(); + g_contexts.push_back(std::move(context)); + + if (g_debug) { + std::cout << "[WebGL] Renderer: " << capturedContext->renderer() + << std::endl; + std::cout << "[WebGL] Version: " << capturedContext->version() << std::endl; + std::cout << "[WebGL] Surface: " + << (capturedContext->isWindowSurface() ? "window" : "offscreen") + << std::endl; + } + + // Context methods live as long as the JS context object. Do not release their + // native callback closures at the end of the frame that created the context. + engine->suspendFrameTracking(); + auto gl = engine->newObject(); + engine->setPrivateData(gl, capturedContext); + engine->setProperty(gl, "_contextType", engine->newString("webgl2")); + engine->setProperty(gl, "drawingBufferWidth", engine->newNumber(width)); + engine->setProperty(gl, "drawingBufferHeight", engine->newNumber(height)); + installConstants(gl); + + engine->setProperty( + gl, "createShader", + engine->newFunction( + "createShader", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 1, "createShader")) + return g_engine->newNull(); + return wrapGLObject( + capturedContext->createShader(toUint32(args[0])), "shader"); + })); + engine->setProperty( + gl, "shaderSource", + engine->newFunction( + "shaderSource", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "shaderSource")) { + capturedContext->shaderSource(unwrapGLObject(args[0]), + g_engine->toString(args[1])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "compileShader", + engine->newFunction( + "compileShader", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "compileShader")) { + capturedContext->compileShader(unwrapGLObject(args[0])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "getShaderParameter", + engine->newFunction( + "getShaderParameter", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 2, "getShaderParameter")) + return g_engine->newNull(); + const int32_t value = capturedContext->getShaderParameter( + unwrapGLObject(args[0]), toUint32(args[1])); + const uint32_t parameter = toUint32(args[1]); + if (parameter == GL_COMPILE_STATUS || + parameter == GL_DELETE_STATUS) { + return g_engine->newBoolean(value != 0); + } + return g_engine->newNumber(value); + })); + engine->setProperty( + gl, "getShaderInfoLog", + engine->newFunction( + "getShaderInfoLog", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 1, "getShaderInfoLog")) + return g_engine->newString(""); + return g_engine->newString( + capturedContext->getShaderInfoLog(unwrapGLObject(args[0])) + .c_str()); + })); + engine->setProperty( + gl, "getShaderPrecisionFormat", + engine->newFunction( + "getShaderPrecisionFormat", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 2, "getShaderPrecisionFormat")) + return g_engine->newNull(); + const auto format = capturedContext->getShaderPrecisionFormat( + toUint32(args[0]), toUint32(args[1])); + auto result = g_engine->newObject(); + g_engine->setProperty(result, "rangeMin", + g_engine->newNumber(format.rangeMin)); + g_engine->setProperty(result, "rangeMax", + g_engine->newNumber(format.rangeMax)); + g_engine->setProperty(result, "precision", + g_engine->newNumber(format.precision)); + return result; + })); + + engine->setProperty( + gl, "createProgram", + engine->newFunction( + "createProgram", + [capturedContext](void *, const std::vector &) { + return wrapGLObject(capturedContext->createProgram(), "program"); + })); + engine->setProperty( + gl, "attachShader", + engine->newFunction( + "attachShader", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "attachShader")) { + capturedContext->attachShader(unwrapGLObject(args[0]), + unwrapGLObject(args[1])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "linkProgram", + engine->newFunction( + "linkProgram", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "linkProgram")) { + capturedContext->linkProgram(unwrapGLObject(args[0])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "getProgramParameter", + engine->newFunction( + "getProgramParameter", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 2, "getProgramParameter")) + return g_engine->newNull(); + const int32_t value = capturedContext->getProgramParameter( + unwrapGLObject(args[0]), toUint32(args[1])); + const uint32_t parameter = toUint32(args[1]); + if (parameter == GL_LINK_STATUS || + parameter == GL_VALIDATE_STATUS || + parameter == GL_DELETE_STATUS) { + return g_engine->newBoolean(value != 0); + } + return g_engine->newNumber(value); + })); + engine->setProperty( + gl, "getProgramInfoLog", + engine->newFunction( + "getProgramInfoLog", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 1, "getProgramInfoLog")) + return g_engine->newString(""); + return g_engine->newString( + capturedContext->getProgramInfoLog(unwrapGLObject(args[0])) + .c_str()); + })); + engine->setProperty( + gl, "useProgram", + engine->newFunction( + "useProgram", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "useProgram")) { + capturedContext->useProgram(unwrapGLObject(args[0])); + } + return g_engine->newUndefined(); + })); + const auto wrapActiveInfo = [](const ActiveInfo &info) { + auto result = g_engine->newObject(); + g_engine->setProperty(result, "name", + g_engine->newString(info.name.c_str())); + g_engine->setProperty(result, "size", g_engine->newNumber(info.size)); + g_engine->setProperty(result, "type", g_engine->newNumber(info.type)); + return result; + }; + engine->setProperty( + gl, "getActiveAttrib", + engine->newFunction( + "getActiveAttrib", + [capturedContext, + wrapActiveInfo](void *, const std::vector &args) { + if (!requireArguments(args, 2, "getActiveAttrib")) + return g_engine->newNull(); + return wrapActiveInfo(capturedContext->getActiveAttrib( + unwrapGLObject(args[0]), toUint32(args[1]))); + })); + engine->setProperty( + gl, "getActiveUniform", + engine->newFunction( + "getActiveUniform", + [capturedContext, + wrapActiveInfo](void *, const std::vector &args) { + if (!requireArguments(args, 2, "getActiveUniform")) + return g_engine->newNull(); + return wrapActiveInfo(capturedContext->getActiveUniform( + unwrapGLObject(args[0]), toUint32(args[1]))); + })); + engine->setProperty( + gl, "getUniformLocation", + engine->newFunction( + "getUniformLocation", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 2, "getUniformLocation")) + return g_engine->newNull(); + return wrapUniformLocation(capturedContext->getUniformLocation( + unwrapGLObject(args[0]), g_engine->toString(args[1]))); + })); + + engine->setProperty( + gl, "createBuffer", + engine->newFunction( + "createBuffer", + [capturedContext](void *, const std::vector &) { + return wrapGLObject(capturedContext->createBuffer(), "buffer"); + })); + engine->setProperty( + gl, "bindBuffer", + engine->newFunction( + "bindBuffer", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "bindBuffer")) { + capturedContext->bindBuffer(toUint32(args[0]), + unwrapGLObject(args[1])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "bufferData", + engine->newFunction( + "bufferData", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 3, "bufferData")) + return g_engine->newUndefined(); + size_t size = 0; + const void *data = nullptr; + if (g_engine->isNumber(args[1])) { + size = static_cast(g_engine->toNumber(args[1])); + } else { + data = g_engine->getArrayBufferData(args[1], &size); + if (!data) { + g_engine->throwException( + "bufferData requires a size, ArrayBuffer, or TypedArray"); + return g_engine->newUndefined(); + } + } + capturedContext->bufferData(toUint32(args[0]), size, data, + toUint32(args[2])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "createFramebuffer", + engine->newFunction( + "createFramebuffer", + [capturedContext](void *, const std::vector &) { + return wrapGLObject(capturedContext->createFramebuffer(), + "framebuffer"); + })); + engine->setProperty( + gl, "bindFramebuffer", + engine->newFunction( + "bindFramebuffer", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "bindFramebuffer")) + capturedContext->bindFramebuffer(toUint32(args[0]), + unwrapGLObject(args[1])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "createRenderbuffer", + engine->newFunction( + "createRenderbuffer", + [capturedContext](void *, const std::vector &) { + return wrapGLObject(capturedContext->createRenderbuffer(), + "renderbuffer"); + })); + engine->setProperty( + gl, "bindRenderbuffer", + engine->newFunction( + "bindRenderbuffer", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "bindRenderbuffer")) + capturedContext->bindRenderbuffer(toUint32(args[0]), + unwrapGLObject(args[1])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "createTexture", + engine->newFunction( + "createTexture", + [capturedContext](void *, const std::vector &) { + return wrapGLObject(capturedContext->createTexture(), "texture"); + })); + engine->setProperty( + gl, "bindTexture", + engine->newFunction( + "bindTexture", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "bindTexture")) + capturedContext->bindTexture(toUint32(args[0]), + unwrapGLObject(args[1])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "createVertexArray", + engine->newFunction( + "createVertexArray", + [capturedContext](void *, const std::vector &) { + return wrapGLObject(capturedContext->createVertexArray(), + "vertexArray"); + })); + engine->setProperty( + gl, "bindVertexArray", + engine->newFunction( + "bindVertexArray", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "bindVertexArray")) + capturedContext->bindVertexArray(unwrapGLObject(args[0])); + return g_engine->newUndefined(); + })); + + engine->setProperty( + gl, "getAttribLocation", + engine->newFunction( + "getAttribLocation", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 2, "getAttribLocation")) + return g_engine->newNumber(-1); + return g_engine->newNumber(capturedContext->getAttribLocation( + unwrapGLObject(args[0]), g_engine->toString(args[1]))); + })); + engine->setProperty( + gl, "enableVertexAttribArray", + engine->newFunction( + "enableVertexAttribArray", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "enableVertexAttribArray")) { + capturedContext->enableVertexAttribArray(toUint32(args[0])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "vertexAttribPointer", + engine->newFunction( + "vertexAttribPointer", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 6, "vertexAttribPointer")) { + capturedContext->vertexAttribPointer( + toUint32(args[0]), toInt32(args[1]), toUint32(args[2]), + g_engine->toBoolean(args[3]), toInt32(args[4]), + static_cast(g_engine->toNumber(args[5]))); + } + return g_engine->newUndefined(); + })); + + engine->setProperty( + gl, "vertexAttribDivisor", + engine->newFunction( + "vertexAttribDivisor", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "vertexAttribDivisor")) + capturedContext->vertexAttribDivisor(toUint32(args[0]), + toUint32(args[1])); + return g_engine->newUndefined(); + })); + + engine->setProperty( + gl, "activeTexture", + engine->newFunction( + "activeTexture", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "activeTexture")) + capturedContext->activeTexture(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "clearDepth", + engine->newFunction( + "clearDepth", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "clearDepth")) + capturedContext->clearDepth(toFloat(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "clearStencil", + engine->newFunction( + "clearStencil", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "clearStencil")) + capturedContext->clearStencil(toInt32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "colorMask", + engine->newFunction( + "colorMask", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 4, "colorMask")) + capturedContext->colorMask( + g_engine->toBoolean(args[0]), g_engine->toBoolean(args[1]), + g_engine->toBoolean(args[2]), g_engine->toBoolean(args[3])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "cullFace", + engine->newFunction( + "cullFace", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "cullFace")) + capturedContext->cullFace(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "deleteShader", + engine->newFunction( + "deleteShader", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "deleteShader")) + capturedContext->deleteShader(unwrapGLObject(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "depthFunc", + engine->newFunction( + "depthFunc", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "depthFunc")) + capturedContext->depthFunc(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "depthMask", + engine->newFunction( + "depthMask", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "depthMask")) + capturedContext->depthMask(g_engine->toBoolean(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "disable", + engine->newFunction( + "disable", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "disable")) + capturedContext->disable(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "enable", + engine->newFunction( + "enable", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "enable")) + capturedContext->enable(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "frontFace", + engine->newFunction( + "frontFace", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "frontFace")) + capturedContext->frontFace(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "pixelStorei", + engine->newFunction( + "pixelStorei", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 2, "pixelStorei")) + capturedContext->pixelStorei(toUint32(args[0]), toInt32(args[1])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "scissor", + engine->newFunction( + "scissor", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 4, "scissor")) + capturedContext->scissor(toInt32(args[0]), toInt32(args[1]), + toInt32(args[2]), toInt32(args[3])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "stencilMask", + engine->newFunction( + "stencilMask", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "stencilMask")) + capturedContext->stencilMask(toUint32(args[0])); + return g_engine->newUndefined(); + })); + + engine->setProperty( + gl, "framebufferRenderbuffer", + engine->newFunction( + "framebufferRenderbuffer", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 4, "framebufferRenderbuffer")) + capturedContext->framebufferRenderbuffer( + toUint32(args[0]), toUint32(args[1]), toUint32(args[2]), + unwrapGLObject(args[3])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "framebufferTexture2D", + engine->newFunction( + "framebufferTexture2D", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 5, "framebufferTexture2D")) + capturedContext->framebufferTexture2D( + toUint32(args[0]), toUint32(args[1]), toUint32(args[2]), + unwrapGLObject(args[3]), toInt32(args[4])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "renderbufferStorage", + engine->newFunction( + "renderbufferStorage", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 4, "renderbufferStorage")) + capturedContext->renderbufferStorage( + toUint32(args[0]), toUint32(args[1]), toInt32(args[2]), + toInt32(args[3])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "drawBuffers", + engine->newFunction( + "drawBuffers", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 1, "drawBuffers")) + capturedContext->drawBuffers(readNumericArray(args[0])); + return g_engine->newUndefined(); + })); + + engine->setProperty( + gl, "texImage2D", + engine->newFunction( + "texImage2D", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 9, "texImage2D")) + capturedContext->texImage2D( + toUint32(args[0]), toInt32(args[1]), toInt32(args[2]), + toInt32(args[3]), toInt32(args[4]), toInt32(args[5]), + toUint32(args[6]), toUint32(args[7]), readPixelData(args[8])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "texImage3D", + engine->newFunction( + "texImage3D", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 10, "texImage3D")) + capturedContext->texImage3D( + toUint32(args[0]), toInt32(args[1]), toInt32(args[2]), + toInt32(args[3]), toInt32(args[4]), toInt32(args[5]), + toInt32(args[6]), toUint32(args[7]), toUint32(args[8]), + readPixelData(args[9])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "texParameteri", + engine->newFunction( + "texParameteri", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 3, "texParameteri")) + capturedContext->texParameteri( + toUint32(args[0]), toUint32(args[1]), toInt32(args[2])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "texStorage2D", + engine->newFunction( + "texStorage2D", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 5, "texStorage2D")) + capturedContext->texStorage2D(toUint32(args[0]), toInt32(args[1]), + toUint32(args[2]), toInt32(args[3]), + toInt32(args[4])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "texSubImage2D", + engine->newFunction( + "texSubImage2D", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 9, "texSubImage2D")) + capturedContext->texSubImage2D( + toUint32(args[0]), toInt32(args[1]), toInt32(args[2]), + toInt32(args[3]), toInt32(args[4]), toInt32(args[5]), + toUint32(args[6]), toUint32(args[7]), readPixelData(args[8])); + return g_engine->newUndefined(); + })); + + const auto validUniform = [](const std::vector &args) { + return !args.empty() && !g_engine->isNull(args[0]) && + !g_engine->isUndefined(args[0]); + }; + engine->setProperty( + gl, "uniform1f", + engine->newFunction( + "uniform1f", [capturedContext, validUniform]( + void *, const std::vector &args) { + if (requireArguments(args, 2, "uniform1f") && validUniform(args)) + capturedContext->uniform1f( + static_cast(unwrapGLObject(args[0])), + toFloat(args[1])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniform1i", + engine->newFunction( + "uniform1i", [capturedContext, validUniform]( + void *, const std::vector &args) { + if (requireArguments(args, 2, "uniform1i") && validUniform(args)) + capturedContext->uniform1i( + static_cast(unwrapGLObject(args[0])), + toInt32(args[1])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniform1iv", + engine->newFunction( + "uniform1iv", + [capturedContext, + validUniform](void *, const std::vector &args) { + if (requireArguments(args, 2, "uniform1iv") && validUniform(args)) { + const auto values = readNumericArray(args[1]); + capturedContext->uniform1iv( + static_cast(unwrapGLObject(args[0])), + static_cast(values.size()), values.data()); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniform2f", + engine->newFunction( + "uniform2f", [capturedContext, validUniform]( + void *, const std::vector &args) { + if (requireArguments(args, 3, "uniform2f") && validUniform(args)) + capturedContext->uniform2f( + static_cast(unwrapGLObject(args[0])), + toFloat(args[1]), toFloat(args[2])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniform3f", + engine->newFunction( + "uniform3f", [capturedContext, validUniform]( + void *, const std::vector &args) { + if (requireArguments(args, 4, "uniform3f") && validUniform(args)) + capturedContext->uniform3f( + static_cast(unwrapGLObject(args[0])), + toFloat(args[1]), toFloat(args[2]), toFloat(args[3])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniform3fv", + engine->newFunction( + "uniform3fv", + [capturedContext, + validUniform](void *, const std::vector &args) { + if (requireArguments(args, 2, "uniform3fv") && validUniform(args)) { + const auto values = readNumericArray(args[1]); + capturedContext->uniform3fv( + static_cast(unwrapGLObject(args[0])), + static_cast(values.size() / 3), values.data()); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniformMatrix3fv", + engine->newFunction( + "uniformMatrix3fv", + [capturedContext, + validUniform](void *, const std::vector &args) { + if (requireArguments(args, 3, "uniformMatrix3fv") && + validUniform(args)) { + const auto values = readNumericArray(args[2]); + capturedContext->uniformMatrix3fv( + static_cast(unwrapGLObject(args[0])), + static_cast(values.size() / 9), + g_engine->toBoolean(args[1]), values.data()); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "uniformMatrix4fv", + engine->newFunction( + "uniformMatrix4fv", + [capturedContext, + validUniform](void *, const std::vector &args) { + if (requireArguments(args, 3, "uniformMatrix4fv") && + validUniform(args)) { + const auto values = readNumericArray(args[2]); + capturedContext->uniformMatrix4fv( + static_cast(unwrapGLObject(args[0])), + static_cast(values.size() / 16), + g_engine->toBoolean(args[1]), values.data()); + } + return g_engine->newUndefined(); + })); + + engine->setProperty( + gl, "viewport", + engine->newFunction( + "viewport", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 4, "viewport")) { + capturedContext->viewport(toInt32(args[0]), toInt32(args[1]), + toInt32(args[2]), toInt32(args[3])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "clearColor", + engine->newFunction( + "clearColor", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 4, "clearColor")) { + capturedContext->clearColor(toFloat(args[0]), toFloat(args[1]), + toFloat(args[2]), toFloat(args[3])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "clear", + engine->newFunction( + "clear", [capturedContext]( + void *, const std::vector &args) { + if (requireArguments(args, 1, "clear")) + capturedContext->clear(toUint32(args[0])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "drawArrays", + engine->newFunction( + "drawArrays", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 3, "drawArrays")) { + capturedContext->drawArrays(toUint32(args[0]), toInt32(args[1]), + toInt32(args[2])); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "drawElements", + engine->newFunction( + "drawElements", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 4, "drawElements")) + capturedContext->drawElements( + toUint32(args[0]), toInt32(args[1]), toUint32(args[2]), + static_cast(g_engine->toNumber(args[3]))); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "drawElementsInstanced", + engine->newFunction( + "drawElementsInstanced", + [capturedContext](void *, + const std::vector &args) { + if (requireArguments(args, 5, "drawElementsInstanced")) + capturedContext->drawElementsInstanced( + toUint32(args[0]), toInt32(args[1]), toUint32(args[2]), + static_cast(g_engine->toNumber(args[3])), + toInt32(args[4])); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "finish", + engine->newFunction( + "finish", + [capturedContext](void *, const std::vector &) { + capturedContext->finish(); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "commit", + engine->newFunction( + "commit", + [capturedContext](void *, const std::vector &) { + if (capturedContext->isWindowSurface()) { + capturedContext->present(); + } + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "readPixels", + engine->newFunction( + "readPixels", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 7, "readPixels")) + return g_engine->newUndefined(); + size_t destinationSize = 0; + void *destination = + g_engine->getArrayBufferData(args[6], &destinationSize); + if (!destination || destinationSize == 0) { + g_engine->throwException( + "readPixels requires a destination TypedArray"); + return g_engine->newUndefined(); + } + capturedContext->readPixels(toInt32(args[0]), toInt32(args[1]), + toInt32(args[2]), toInt32(args[3]), + toUint32(args[4]), toUint32(args[5]), + destination); + return g_engine->newUndefined(); + })); + engine->setProperty( + gl, "getError", + engine->newFunction( + "getError", + [capturedContext](void *, const std::vector &) { + return g_engine->newNumber(capturedContext->getError()); + })); + + engine->setProperty( + gl, "getParameter", + engine->newFunction( + "getParameter", + [capturedContext](void *, + const std::vector &args) { + if (!requireArguments(args, 1, "getParameter")) + return g_engine->newNull(); + switch (toUint32(args[0])) { + case GL_RENDERER: + return g_engine->newString(capturedContext->renderer().c_str()); + case GL_VENDOR: + return g_engine->newString("Mystral Native.js"); + case GL_VERSION: + return g_engine->newString("WebGL 2.0 Mystral ANGLE"); + case GL_SHADING_LANGUAGE_VERSION: + return g_engine->newString( + capturedContext->shadingLanguageVersion().c_str()); + case GL_SCISSOR_BOX: + case GL_VIEWPORT: { + const auto values = + capturedContext->getIntegers(toUint32(args[0]), 4); + auto result = g_engine->newArray(values.size()); + for (uint32_t index = 0; index < values.size(); ++index) { + g_engine->setPropertyIndex(result, index, + g_engine->newNumber(values[index])); + } + return result; + } + default: + return g_engine->newNumber( + capturedContext->getInteger(toUint32(args[0]))); + } + })); + engine->setProperty( + gl, "getContextAttributes", + engine->newFunction( + "getContextAttributes", + [attributes](void *, const std::vector &) { + auto result = g_engine->newObject(); + g_engine->setProperty(result, "alpha", + g_engine->newBoolean(attributes.alpha)); + g_engine->setProperty(result, "depth", + g_engine->newBoolean(attributes.depth)); + g_engine->setProperty(result, "stencil", + g_engine->newBoolean(attributes.stencil)); + g_engine->setProperty(result, "antialias", + g_engine->newBoolean(attributes.antialias)); + g_engine->setProperty( + result, "premultipliedAlpha", + g_engine->newBoolean(attributes.premultipliedAlpha)); + g_engine->setProperty( + result, "preserveDrawingBuffer", + g_engine->newBoolean(attributes.preserveDrawingBuffer)); + return result; + })); + engine->setProperty( + gl, "getSupportedExtensions", + engine->newFunction("getSupportedExtensions", + [](void *, const std::vector &) { + return g_engine->newArray(0); + })); + engine->setProperty( + gl, "getExtension", + engine->newFunction("getExtension", + [](void *, const std::vector &) { + return g_engine->newNull(); + })); + engine->setProperty( + gl, "isContextLost", + engine->newFunction("isContextLost", + [](void *, const std::vector &) { + return g_engine->newBoolean(false); + })); + + auto setPrototype = engine->getGlobalProperty("__mystralSetWebGL2Prototype"); + if (engine->isFunction(setPrototype)) { + engine->call(setPrototype, engine->newUndefined(), {gl}); + } + engine->resumeFrameTracking(); + return gl; +} + +} // namespace mystral::webgl + +#else + +namespace mystral::webgl { + +ContextAttributes +contextAttributesFromJS(js::Engine *, const std::vector &) { + return {}; +} +bool initBindings(js::Engine *, bool) { return false; } +void presentContexts() {} +void shutdownBindings() {} +js::JSValueHandle createContextJSObject(js::Engine *engine, uint32_t, uint32_t, + const ContextAttributes &) { + return engine ? engine->newNull() : js::JSValueHandle{}; +} + +} // namespace mystral::webgl + +#endif diff --git a/src/webgl/context.cpp b/src/webgl/context.cpp new file mode 100644 index 0000000..12e3d3f --- /dev/null +++ b/src/webgl/context.cpp @@ -0,0 +1,1166 @@ +#include "mystral/webgl/context.h" + +#if defined(MYSTRAL_HAS_WEBGL) + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#else +#include +#include +#if defined(__APPLE__) +#include +#else +#include +#endif +#endif + +#define EGL_EGL_PROTOTYPES 0 +#define GL_GLES_PROTOTYPES 0 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mystral::webgl { + +namespace { + +#if defined(_WIN32) +using RuntimeLibrary = HMODULE; +#else +using RuntimeLibrary = void *; +#endif + +std::string formatEGLError(EGLint error) { + std::ostringstream stream; + stream << "0x" << std::hex << std::uppercase << error; + return stream.str(); +} + +std::filesystem::path executableDirectory() { +#if defined(_WIN32) + std::wstring path(MAX_PATH, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, path.data(), MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return {}; + } + path.resize(length); + return std::filesystem::path(path).parent_path(); +#elif defined(__APPLE__) + uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::string path(size, '\0'); + if (_NSGetExecutablePath(path.data(), &size) != 0) { + return {}; + } + path.resize(std::char_traits::length(path.c_str())); + return std::filesystem::weakly_canonical(path).parent_path(); +#else + std::string path(PATH_MAX, '\0'); + const ssize_t length = readlink("/proc/self/exe", path.data(), path.size()); + if (length <= 0) { + return {}; + } + path.resize(static_cast(length)); + return std::filesystem::path(path).parent_path(); +#endif +} + +RuntimeLibrary openRuntimeLibrary(const std::filesystem::path &path) { +#if defined(_WIN32) + return LoadLibraryW(path.wstring().c_str()); +#else + return dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL); +#endif +} + +RuntimeLibrary loadRuntimeLibrary(const char *filename) { + if (const char *runtimeDirectory = + std::getenv("MYSTRAL_ANGLE_RUNTIME_DIR")) { + if (RuntimeLibrary module = openRuntimeLibrary( + std::filesystem::path(runtimeDirectory) / filename)) { + return module; + } + } + const auto directory = executableDirectory(); + if (!directory.empty()) { + if (RuntimeLibrary module = + openRuntimeLibrary(directory / std::filesystem::path(filename))) { + return module; + } + } + return openRuntimeLibrary(filename); +} + +void *findRuntimeSymbol(RuntimeLibrary module, const char *name) { +#if defined(_WIN32) + return reinterpret_cast(GetProcAddress(module, name)); +#else + return dlsym(module, name); +#endif +} + +void closeRuntimeLibrary(RuntimeLibrary module) { + if (!module) { + return; + } +#if defined(_WIN32) + FreeLibrary(module); +#else + dlclose(module); +#endif +} + +const char *eglLibraryName() { +#if defined(_WIN32) + return "libEGL.dll"; +#elif defined(__APPLE__) + return "libEGL.dylib"; +#else + return "libEGL.so"; +#endif +} + +const char *glesLibraryName() { +#if defined(_WIN32) + return "libGLESv2.dll"; +#elif defined(__APPLE__) + return "libGLESv2.dylib"; +#else + return "libGLESv2.so"; +#endif +} + +const char *backendName(NativeWindowPlatform platform) { + switch (platform) { + case NativeWindowPlatform::Win32: + return "D3D11"; + case NativeWindowPlatform::Metal: + return "Metal"; + case NativeWindowPlatform::X11: + return "Vulkan/X11"; + case NativeWindowPlatform::Wayland: + return "Vulkan/Wayland"; + case NativeWindowPlatform::None: +#if defined(_WIN32) + return "D3D11"; +#elif defined(__APPLE__) + return "Metal"; +#else + return "Vulkan/headless"; +#endif + } + return "unknown"; +} + +EGLNativeWindowType toEGLNativeWindow(uintptr_t window) { +#if defined(_WIN32) || defined(__APPLE__) + return reinterpret_cast(window); +#else + return static_cast(window); +#endif +} + +} // namespace + +struct Context::Impl { + RuntimeLibrary eglModule = nullptr; + RuntimeLibrary glesModule = nullptr; + + EGLDisplay display = EGL_NO_DISPLAY; + EGLConfig config = nullptr; + EGLContext context = EGL_NO_CONTEXT; + EGLSurface surface = EGL_NO_SURFACE; + + std::string error; + std::string rendererName; + std::string versionName; + std::string shadingLanguageVersionName; + bool initialized = false; + bool windowSurface = false; + + PFNEGLGETPROCADDRESSPROC eglGetProcAddress = nullptr; + PFNEGLGETDISPLAYPROC eglGetDisplay = nullptr; + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = nullptr; + PFNEGLINITIALIZEPROC eglInitialize = nullptr; + PFNEGLCHOOSECONFIGPROC eglChooseConfig = nullptr; + PFNEGLCREATECONTEXTPROC eglCreateContext = nullptr; + PFNEGLCREATEPBUFFERSURFACEPROC eglCreatePbufferSurface = nullptr; + PFNEGLCREATEWINDOWSURFACEPROC eglCreateWindowSurface = nullptr; + PFNEGLMAKECURRENTPROC eglMakeCurrent = nullptr; + PFNEGLSWAPBUFFERSPROC eglSwapBuffers = nullptr; + PFNEGLSWAPINTERVALPROC eglSwapInterval = nullptr; + PFNEGLDESTROYSURFACEPROC eglDestroySurface = nullptr; + PFNEGLDESTROYCONTEXTPROC eglDestroyContext = nullptr; + PFNEGLTERMINATEPROC eglTerminate = nullptr; + PFNEGLGETERRORPROC eglGetError = nullptr; + + PFNGLGETSTRINGPROC glGetString = nullptr; + PFNGLACTIVETEXTUREPROC glActiveTexture = nullptr; + PFNGLATTACHSHADERPROC glAttachShader = nullptr; + PFNGLBINDBUFFERPROC glBindBuffer = nullptr; + PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer = nullptr; + PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer = nullptr; + PFNGLBINDTEXTUREPROC glBindTexture = nullptr; + PFNGLBINDVERTEXARRAYPROC glBindVertexArray = nullptr; + PFNGLBUFFERDATAPROC glBufferData = nullptr; + PFNGLCLEARPROC glClear = nullptr; + PFNGLCLEARCOLORPROC glClearColor = nullptr; + PFNGLCLEARDEPTHFPROC glClearDepthf = nullptr; + PFNGLCLEARSTENCILPROC glClearStencil = nullptr; + PFNGLCOLORMASKPROC glColorMask = nullptr; + PFNGLCOMPILESHADERPROC glCompileShader = nullptr; + PFNGLCREATEPROGRAMPROC glCreateProgram = nullptr; + PFNGLCREATESHADERPROC glCreateShader = nullptr; + PFNGLCULLFACEPROC glCullFace = nullptr; + PFNGLDELETESHADERPROC glDeleteShader = nullptr; + PFNGLDEPTHFUNCPROC glDepthFunc = nullptr; + PFNGLDEPTHMASKPROC glDepthMask = nullptr; + PFNGLDISABLEPROC glDisable = nullptr; + PFNGLDRAWBUFFERSPROC glDrawBuffers = nullptr; + PFNGLDRAWARRAYSPROC glDrawArrays = nullptr; + PFNGLDRAWELEMENTSPROC glDrawElements = nullptr; + PFNGLDRAWELEMENTSINSTANCEDPROC glDrawElementsInstanced = nullptr; + PFNGLENABLEPROC glEnable = nullptr; + PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray = nullptr; + PFNGLFINISHPROC glFinish = nullptr; + PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer = nullptr; + PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D = nullptr; + PFNGLFRONTFACEPROC glFrontFace = nullptr; + PFNGLGENBUFFERSPROC glGenBuffers = nullptr; + PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers = nullptr; + PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers = nullptr; + PFNGLGENTEXTURESPROC glGenTextures = nullptr; + PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = nullptr; + PFNGLGETACTIVEATTRIBPROC glGetActiveAttrib = nullptr; + PFNGLGETACTIVEUNIFORMPROC glGetActiveUniform = nullptr; + PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation = nullptr; + PFNGLGETERRORPROC glGetError = nullptr; + PFNGLGETINTEGERVPROC glGetIntegerv = nullptr; + PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = nullptr; + PFNGLGETPROGRAMIVPROC glGetProgramiv = nullptr; + PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = nullptr; + PFNGLGETSHADERIVPROC glGetShaderiv = nullptr; + PFNGLGETSHADERPRECISIONFORMATPROC glGetShaderPrecisionFormat = nullptr; + PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = nullptr; + PFNGLLINKPROGRAMPROC glLinkProgram = nullptr; + PFNGLPIXELSTOREIPROC glPixelStorei = nullptr; + PFNGLREADPIXELSPROC glReadPixels = nullptr; + PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage = nullptr; + PFNGLSCISSORPROC glScissor = nullptr; + PFNGLSHADERSOURCEPROC glShaderSource = nullptr; + PFNGLSTENCILMASKPROC glStencilMask = nullptr; + PFNGLTEXIMAGE2DPROC glTexImage2D = nullptr; + PFNGLTEXIMAGE3DPROC glTexImage3D = nullptr; + PFNGLTEXPARAMETERIPROC glTexParameteri = nullptr; + PFNGLTEXSTORAGE2DPROC glTexStorage2D = nullptr; + PFNGLTEXSUBIMAGE2DPROC glTexSubImage2D = nullptr; + PFNGLUNIFORM1FPROC glUniform1f = nullptr; + PFNGLUNIFORM1IPROC glUniform1i = nullptr; + PFNGLUNIFORM1IVPROC glUniform1iv = nullptr; + PFNGLUNIFORM2FPROC glUniform2f = nullptr; + PFNGLUNIFORM3FPROC glUniform3f = nullptr; + PFNGLUNIFORM3FVPROC glUniform3fv = nullptr; + PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv = nullptr; + PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv = nullptr; + PFNGLUSEPROGRAMPROC glUseProgram = nullptr; + PFNGLVERTEXATTRIBDIVISORPROC glVertexAttribDivisor = nullptr; + PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer = nullptr; + PFNGLVIEWPORTPROC glViewport = nullptr; + + template Function loadEGL(const char *name) { + auto function = + reinterpret_cast(findRuntimeSymbol(eglModule, name)); + if (!function && eglGetProcAddress) { + function = reinterpret_cast(eglGetProcAddress(name)); + } + return function; + } + + template Function loadGLES(const char *name) { + auto function = + reinterpret_cast(findRuntimeSymbol(glesModule, name)); + if (!function && eglGetProcAddress) { + function = reinterpret_cast(eglGetProcAddress(name)); + } + return function; + } + + bool fail(const std::string &message) { + error = message; + if (eglGetError) { + error += " (EGL " + formatEGLError(eglGetError()) + ")"; + } + return false; + } + + bool loadLibraries() { + glesModule = loadRuntimeLibrary(glesLibraryName()); + eglModule = loadRuntimeLibrary(eglLibraryName()); + if (!eglModule || !glesModule) { + return fail(std::string("Could not load ANGLE ") + eglLibraryName() + + " and " + glesLibraryName() + + " beside the Mystral executable"); + } + + eglGetProcAddress = reinterpret_cast( + findRuntimeSymbol(eglModule, "eglGetProcAddress")); + if (!eglGetProcAddress) { + return fail("ANGLE did not export eglGetProcAddress"); + } + +#define LOAD_EGL(name) name = loadEGL(#name) + LOAD_EGL(eglGetDisplay); + LOAD_EGL(eglGetPlatformDisplayEXT); + LOAD_EGL(eglInitialize); + LOAD_EGL(eglChooseConfig); + LOAD_EGL(eglCreateContext); + LOAD_EGL(eglCreatePbufferSurface); + LOAD_EGL(eglCreateWindowSurface); + LOAD_EGL(eglMakeCurrent); + LOAD_EGL(eglSwapBuffers); + LOAD_EGL(eglSwapInterval); + LOAD_EGL(eglDestroySurface); + LOAD_EGL(eglDestroyContext); + LOAD_EGL(eglTerminate); + LOAD_EGL(eglGetError); +#undef LOAD_EGL + + if (!eglGetDisplay || !eglInitialize || !eglChooseConfig || + !eglCreateContext || !eglCreatePbufferSurface || + !eglCreateWindowSurface || !eglMakeCurrent || !eglSwapBuffers || + !eglSwapInterval || !eglDestroySurface || !eglDestroyContext || + !eglTerminate || !eglGetError) { + return fail("ANGLE is missing a required EGL entry point"); + } + return true; + } + + bool loadGLESFunctions() { +#define LOAD_GL(name) name = loadGLES(#name) + LOAD_GL(glGetString); + LOAD_GL(glActiveTexture); + LOAD_GL(glAttachShader); + LOAD_GL(glBindBuffer); + LOAD_GL(glBindFramebuffer); + LOAD_GL(glBindRenderbuffer); + LOAD_GL(glBindTexture); + LOAD_GL(glBindVertexArray); + LOAD_GL(glBufferData); + LOAD_GL(glClear); + LOAD_GL(glClearColor); + LOAD_GL(glClearDepthf); + LOAD_GL(glClearStencil); + LOAD_GL(glColorMask); + LOAD_GL(glCompileShader); + LOAD_GL(glCreateProgram); + LOAD_GL(glCreateShader); + LOAD_GL(glCullFace); + LOAD_GL(glDeleteShader); + LOAD_GL(glDepthFunc); + LOAD_GL(glDepthMask); + LOAD_GL(glDisable); + LOAD_GL(glDrawBuffers); + LOAD_GL(glDrawArrays); + LOAD_GL(glDrawElements); + LOAD_GL(glDrawElementsInstanced); + LOAD_GL(glEnable); + LOAD_GL(glEnableVertexAttribArray); + LOAD_GL(glFinish); + LOAD_GL(glFramebufferRenderbuffer); + LOAD_GL(glFramebufferTexture2D); + LOAD_GL(glFrontFace); + LOAD_GL(glGenBuffers); + LOAD_GL(glGenFramebuffers); + LOAD_GL(glGenRenderbuffers); + LOAD_GL(glGenTextures); + LOAD_GL(glGenVertexArrays); + LOAD_GL(glGetActiveAttrib); + LOAD_GL(glGetActiveUniform); + LOAD_GL(glGetAttribLocation); + LOAD_GL(glGetError); + LOAD_GL(glGetIntegerv); + LOAD_GL(glGetProgramInfoLog); + LOAD_GL(glGetProgramiv); + LOAD_GL(glGetShaderInfoLog); + LOAD_GL(glGetShaderiv); + LOAD_GL(glGetShaderPrecisionFormat); + LOAD_GL(glGetUniformLocation); + LOAD_GL(glLinkProgram); + LOAD_GL(glPixelStorei); + LOAD_GL(glReadPixels); + LOAD_GL(glRenderbufferStorage); + LOAD_GL(glScissor); + LOAD_GL(glShaderSource); + LOAD_GL(glStencilMask); + LOAD_GL(glTexImage2D); + LOAD_GL(glTexImage3D); + LOAD_GL(glTexParameteri); + LOAD_GL(glTexStorage2D); + LOAD_GL(glTexSubImage2D); + LOAD_GL(glUniform1f); + LOAD_GL(glUniform1i); + LOAD_GL(glUniform1iv); + LOAD_GL(glUniform2f); + LOAD_GL(glUniform3f); + LOAD_GL(glUniform3fv); + LOAD_GL(glUniformMatrix3fv); + LOAD_GL(glUniformMatrix4fv); + LOAD_GL(glUseProgram); + LOAD_GL(glVertexAttribDivisor); + LOAD_GL(glVertexAttribPointer); + LOAD_GL(glViewport); +#undef LOAD_GL + + return glGetString && glCreateShader && glShaderSource && glCompileShader && + glGetShaderiv && glGetShaderInfoLog && glGetShaderPrecisionFormat && + glCreateProgram && glAttachShader && glLinkProgram && + glGetProgramiv && glGetProgramInfoLog && glUseProgram && + glGenBuffers && glBindBuffer && glBufferData && + glGetAttribLocation && glEnableVertexAttribArray && + glVertexAttribPointer && glViewport && glClearColor && glClear && + glDrawArrays && glFinish && glReadPixels && glGetIntegerv && + glGetError; + } +}; + +Context::Context() : impl_(std::make_unique()) {} + +Context::~Context() { shutdown(); } + +bool Context::initialize(uint32_t width, uint32_t height, + const ContextAttributes &attributes, + const NativeWindow &nativeWindow) { + shutdown(); + impl_ = std::make_unique(); + + if (width == 0 || height == 0) { + return impl_->fail("WebGL drawing buffer dimensions must be non-zero"); + } + if (!impl_->loadLibraries()) { + return false; + } + + if (impl_->eglGetPlatformDisplayEXT) { + EGLint renderer = EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE; + EGLint nativePlatform = EGL_PLATFORM_SURFACELESS_MESA; + void *nativeDisplay = nullptr; +#if defined(_WIN32) + renderer = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + nativePlatform = 0; +#elif defined(__APPLE__) + renderer = EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE; + nativePlatform = 0; +#else + if (nativeWindow.platform == NativeWindowPlatform::X11) { + nativePlatform = EGL_PLATFORM_X11_EXT; + nativeDisplay = nativeWindow.display; + } else if (nativeWindow.platform == NativeWindowPlatform::Wayland) { + nativePlatform = EGL_PLATFORM_WAYLAND_EXT; + nativeDisplay = nativeWindow.display; + } +#endif + + std::vector displayAttributes = { + EGL_PLATFORM_ANGLE_TYPE_ANGLE, + renderer, + EGL_POWER_PREFERENCE_ANGLE, + attributes.preferHighPerformance ? EGL_HIGH_POWER_ANGLE + : EGL_LOW_POWER_ANGLE, + }; + if (nativePlatform != 0) { + displayAttributes.push_back( + EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE); + displayAttributes.push_back(nativePlatform); + } + displayAttributes.push_back(EGL_NONE); + impl_->display = impl_->eglGetPlatformDisplayEXT( + EGL_PLATFORM_ANGLE_ANGLE, nativeDisplay, displayAttributes.data()); + } + if (impl_->display == EGL_NO_DISPLAY) { + impl_->display = impl_->eglGetDisplay(EGL_DEFAULT_DISPLAY); + } + if (impl_->display == EGL_NO_DISPLAY) { + return impl_->fail(std::string("Could not acquire an ANGLE ") + + backendName(nativeWindow.platform) + " display"); + } + + if (!impl_->eglInitialize(impl_->display, nullptr, nullptr)) { + return impl_->fail("Could not initialize ANGLE EGL"); + } + + EGLint configAttributes[] = { + EGL_SURFACE_TYPE, + nativeWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES3_BIT_KHR, + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, + 8, + EGL_ALPHA_SIZE, + attributes.alpha ? 8 : 0, + EGL_DEPTH_SIZE, + attributes.depth ? 24 : 0, + EGL_STENCIL_SIZE, + attributes.stencil ? 8 : 0, + EGL_NONE, + }; + EGLint configCount = 0; + if (!impl_->eglChooseConfig(impl_->display, configAttributes, &impl_->config, + 1, &configCount) || + configCount == 0) { + // Some ANGLE builds expose ES3 contexts through an ES2-capable config. + configAttributes[3] = EGL_OPENGL_ES2_BIT; + if (!impl_->eglChooseConfig(impl_->display, configAttributes, + &impl_->config, 1, &configCount) || + configCount == 0) { + return impl_->fail( + "Could not choose an ANGLE WebGL2 framebuffer configuration"); + } + } + + const EGLint contextAttributes[] = { + EGL_CONTEXT_CLIENT_VERSION, + 3, + EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE, + attributes.allowNativeTextureInterop ? EGL_FALSE : EGL_TRUE, + EGL_CONTEXT_OPENGL_BACKWARDS_COMPATIBLE_ANGLE, + EGL_FALSE, + EGL_ROBUST_RESOURCE_INITIALIZATION_ANGLE, + EGL_TRUE, + EGL_NONE, + }; + impl_->context = impl_->eglCreateContext(impl_->display, impl_->config, + EGL_NO_CONTEXT, contextAttributes); + if (impl_->context == EGL_NO_CONTEXT) { + return impl_->fail("Could not create an ANGLE OpenGL ES 3 WebGL context"); + } + + if (nativeWindow) { + impl_->surface = impl_->eglCreateWindowSurface( + impl_->display, impl_->config, + toEGLNativeWindow(nativeWindow.window), nullptr); + impl_->windowSurface = impl_->surface != EGL_NO_SURFACE; + } else { + const EGLint surfaceAttributes[] = { + EGL_WIDTH, static_cast(width), + EGL_HEIGHT, static_cast(height), + EGL_NONE, + }; + impl_->surface = impl_->eglCreatePbufferSurface( + impl_->display, impl_->config, surfaceAttributes); + } + if (impl_->surface == EGL_NO_SURFACE) { + return impl_->fail(nativeWindow + ? "Could not create an ANGLE WebGL2 window surface" + : "Could not create an ANGLE WebGL2 drawing buffer"); + } + + if (!impl_->eglMakeCurrent(impl_->display, impl_->surface, impl_->surface, + impl_->context)) { + return impl_->fail("Could not make the ANGLE WebGL2 context current"); + } + if (!impl_->loadGLESFunctions()) { + return impl_->fail("ANGLE is missing a required OpenGL ES 3 entry point"); + } + + const auto readString = [this](GLenum name) { + const GLubyte *value = impl_->glGetString(name); + return value ? std::string(reinterpret_cast(value)) + : std::string(); + }; + impl_->rendererName = readString(GL_RENDERER); + impl_->versionName = readString(GL_VERSION); + impl_->shadingLanguageVersionName = readString(GL_SHADING_LANGUAGE_VERSION); + if (impl_->windowSurface) { + // WebGL presentation must not impose display-vsync pacing on uncapped + // games. + impl_->eglSwapInterval(impl_->display, 0); + } + impl_->initialized = true; + return true; +} + +void Context::shutdown() { + if (!impl_) { + return; + } + if (impl_->eglMakeCurrent && impl_->display != EGL_NO_DISPLAY) { + impl_->eglMakeCurrent(impl_->display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + } + if (impl_->eglDestroySurface && impl_->display != EGL_NO_DISPLAY && + impl_->surface != EGL_NO_SURFACE) { + impl_->eglDestroySurface(impl_->display, impl_->surface); + } + if (impl_->eglDestroyContext && impl_->display != EGL_NO_DISPLAY && + impl_->context != EGL_NO_CONTEXT) { + impl_->eglDestroyContext(impl_->display, impl_->context); + } + if (impl_->eglTerminate && impl_->display != EGL_NO_DISPLAY) { + impl_->eglTerminate(impl_->display); + } + impl_->surface = EGL_NO_SURFACE; + impl_->context = EGL_NO_CONTEXT; + impl_->display = EGL_NO_DISPLAY; + impl_->config = nullptr; + impl_->initialized = false; + impl_->windowSurface = false; + + if (impl_->eglModule) { + closeRuntimeLibrary(impl_->eglModule); + impl_->eglModule = nullptr; + } + if (impl_->glesModule) { + closeRuntimeLibrary(impl_->glesModule); + impl_->glesModule = nullptr; + } +} + +bool Context::makeCurrent() { + return impl_ && impl_->initialized && + impl_->eglMakeCurrent(impl_->display, impl_->surface, impl_->surface, + impl_->context); +} + +bool Context::present() { + if (!impl_ || !impl_->initialized || !impl_->windowSurface || + !makeCurrent()) { + return false; + } + return impl_->eglSwapBuffers(impl_->display, impl_->surface) == EGL_TRUE; +} + +void *Context::nativeD3D11Device() { +#if defined(_WIN32) + if (!impl_ || !impl_->initialized) { + return nullptr; + } + auto queryDisplay = + impl_->loadEGL("eglQueryDisplayAttribEXT"); + auto queryDevice = + impl_->loadEGL("eglQueryDeviceAttribEXT"); + if (!queryDisplay || !queryDevice) { + return nullptr; + } + + EGLAttrib deviceValue = 0; + if (queryDisplay(impl_->display, EGL_DEVICE_EXT, &deviceValue) != EGL_TRUE) { + return nullptr; + } + EGLAttrib d3d11Device = 0; + if (queryDevice(reinterpret_cast(deviceValue), + EGL_D3D11_DEVICE_ANGLE, &d3d11Device) != EGL_TRUE) { + return nullptr; + } + return reinterpret_cast(d3d11Device); +#else + return nullptr; +#endif +} + +uint32_t Context::importD3D11Texture(void *nativeTexture) { +#if defined(_WIN32) + if (!impl_ || !impl_->initialized || !nativeTexture || !makeCurrent()) { + return 0; + } + auto createImage = + impl_->loadEGL("eglCreateImageKHR"); + auto destroyImage = + impl_->loadEGL("eglDestroyImageKHR"); + auto imageTarget = impl_->loadGLES( + "glEGLImageTargetTexture2DOES"); + if (!createImage || !destroyImage || !imageTarget) { + impl_->error = "ANGLE is missing D3D11 EGL image entry points"; + return 0; + } + + const EGLint attributes[] = {EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RGBA, + EGL_NONE}; + EGLImageKHR image = createImage( + impl_->display, EGL_NO_CONTEXT, EGL_D3D11_TEXTURE_ANGLE, + reinterpret_cast(nativeTexture), attributes); + if (image == EGL_NO_IMAGE_KHR) { + impl_->error = "ANGLE could not create a D3D11 EGL image (EGL " + + formatEGLError(impl_->eglGetError()) + ")"; + return 0; + } + + while (impl_->glGetError() != GL_NO_ERROR) { + } + GLuint texture = 0; + impl_->glGenTextures(1, &texture); + impl_->glBindTexture(GL_TEXTURE_2D, texture); + impl_->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + impl_->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + impl_->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + impl_->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + imageTarget(GL_TEXTURE_2D, image); + const GLenum error = impl_->glGetError(); + destroyImage(impl_->display, image); + if (error != GL_NO_ERROR) { + std::ostringstream stream; + stream << "ANGLE could not bind the D3D11 EGL image (GL 0x" << std::hex + << std::uppercase << error << ")"; + impl_->error = stream.str(); + return 0; + } + return texture; +#else + (void)nativeTexture; + return 0; +#endif +} + +bool Context::isInitialized() const { return impl_ && impl_->initialized; } +bool Context::isWindowSurface() const { + return impl_ && impl_->initialized && impl_->windowSurface; +} +const std::string &Context::errorMessage() const { return impl_->error; } +const std::string &Context::renderer() const { return impl_->rendererName; } +const std::string &Context::version() const { return impl_->versionName; } +const std::string &Context::shadingLanguageVersion() const { + return impl_->shadingLanguageVersionName; +} + +uint32_t Context::createShader(uint32_t type) { + makeCurrent(); + return impl_->glCreateShader(type); +} +void Context::shaderSource(uint32_t shader, const std::string &source) { + makeCurrent(); + const char *data = source.data(); + const GLint length = static_cast(source.size()); + impl_->glShaderSource(shader, 1, &data, &length); +} +void Context::compileShader(uint32_t shader) { + makeCurrent(); + impl_->glCompileShader(shader); +} +int32_t Context::getShaderParameter(uint32_t shader, uint32_t parameter) { + makeCurrent(); + GLint value = 0; + impl_->glGetShaderiv(shader, parameter, &value); + return value; +} +std::string Context::getShaderInfoLog(uint32_t shader) { + makeCurrent(); + GLint length = 0; + impl_->glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); + if (length <= 1) + return {}; + std::string result(static_cast(length), '\0'); + GLsizei written = 0; + impl_->glGetShaderInfoLog(shader, length, &written, result.data()); + result.resize(static_cast(written)); + return result; +} +ShaderPrecisionFormat +Context::getShaderPrecisionFormat(uint32_t shaderType, uint32_t precisionType) { + makeCurrent(); + GLint range[2] = {}; + GLint precision = 0; + impl_->glGetShaderPrecisionFormat(shaderType, precisionType, range, + &precision); + return {range[0], range[1], precision}; +} + +uint32_t Context::createProgram() { + makeCurrent(); + return impl_->glCreateProgram(); +} +void Context::attachShader(uint32_t program, uint32_t shader) { + makeCurrent(); + impl_->glAttachShader(program, shader); +} +void Context::linkProgram(uint32_t program) { + makeCurrent(); + impl_->glLinkProgram(program); +} +int32_t Context::getProgramParameter(uint32_t program, uint32_t parameter) { + makeCurrent(); + GLint value = 0; + impl_->glGetProgramiv(program, parameter, &value); + return value; +} +std::string Context::getProgramInfoLog(uint32_t program) { + makeCurrent(); + GLint length = 0; + impl_->glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + if (length <= 1) + return {}; + std::string result(static_cast(length), '\0'); + GLsizei written = 0; + impl_->glGetProgramInfoLog(program, length, &written, result.data()); + result.resize(static_cast(written)); + return result; +} +void Context::useProgram(uint32_t program) { + makeCurrent(); + impl_->glUseProgram(program); +} +ActiveInfo Context::getActiveAttrib(uint32_t program, uint32_t index) { + makeCurrent(); + GLint maxLength = 0; + impl_->glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &maxLength); + std::vector name(static_cast(std::max(maxLength, 1))); + GLsizei length = 0; + GLint size = 0; + GLenum type = 0; + impl_->glGetActiveAttrib(program, index, maxLength, &length, &size, &type, + name.data()); + return {std::string(name.data(), static_cast(length)), size, type}; +} +ActiveInfo Context::getActiveUniform(uint32_t program, uint32_t index) { + makeCurrent(); + GLint maxLength = 0; + impl_->glGetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxLength); + std::vector name(static_cast(std::max(maxLength, 1))); + GLsizei length = 0; + GLint size = 0; + GLenum type = 0; + impl_->glGetActiveUniform(program, index, maxLength, &length, &size, &type, + name.data()); + return {std::string(name.data(), static_cast(length)), size, type}; +} +int32_t Context::getUniformLocation(uint32_t program, const std::string &name) { + makeCurrent(); + return impl_->glGetUniformLocation(program, name.c_str()); +} + +uint32_t Context::createBuffer() { + makeCurrent(); + GLuint buffer = 0; + impl_->glGenBuffers(1, &buffer); + return buffer; +} +void Context::bindBuffer(uint32_t target, uint32_t buffer) { + makeCurrent(); + impl_->glBindBuffer(target, buffer); +} +void Context::bufferData(uint32_t target, size_t size, const void *data, + uint32_t usage) { + makeCurrent(); + impl_->glBufferData(target, static_cast(size), data, usage); +} +uint32_t Context::createFramebuffer() { + makeCurrent(); + GLuint value = 0; + impl_->glGenFramebuffers(1, &value); + return value; +} +void Context::bindFramebuffer(uint32_t target, uint32_t framebuffer) { + makeCurrent(); + impl_->glBindFramebuffer(target, framebuffer); +} +uint32_t Context::createRenderbuffer() { + makeCurrent(); + GLuint value = 0; + impl_->glGenRenderbuffers(1, &value); + return value; +} +void Context::bindRenderbuffer(uint32_t target, uint32_t renderbuffer) { + makeCurrent(); + impl_->glBindRenderbuffer(target, renderbuffer); +} +uint32_t Context::createTexture() { + makeCurrent(); + GLuint value = 0; + impl_->glGenTextures(1, &value); + return value; +} +void Context::bindTexture(uint32_t target, uint32_t texture) { + makeCurrent(); + impl_->glBindTexture(target, texture); +} +uint32_t Context::createVertexArray() { + makeCurrent(); + GLuint value = 0; + impl_->glGenVertexArrays(1, &value); + return value; +} +void Context::bindVertexArray(uint32_t vertexArray) { + makeCurrent(); + impl_->glBindVertexArray(vertexArray); +} + +int32_t Context::getAttribLocation(uint32_t program, const std::string &name) { + makeCurrent(); + return impl_->glGetAttribLocation(program, name.c_str()); +} +void Context::enableVertexAttribArray(uint32_t index) { + makeCurrent(); + impl_->glEnableVertexAttribArray(index); +} +void Context::vertexAttribPointer(uint32_t index, int32_t size, uint32_t type, + bool normalized, int32_t stride, + size_t offset) { + makeCurrent(); + impl_->glVertexAttribPointer(index, size, type, + normalized ? GL_TRUE : GL_FALSE, stride, + reinterpret_cast(offset)); +} +void Context::vertexAttribDivisor(uint32_t index, uint32_t divisor) { + makeCurrent(); + impl_->glVertexAttribDivisor(index, divisor); +} + +void Context::activeTexture(uint32_t texture) { + makeCurrent(); + impl_->glActiveTexture(texture); +} +void Context::clearDepth(float depth) { + makeCurrent(); + impl_->glClearDepthf(depth); +} +void Context::clearStencil(int32_t stencil) { + makeCurrent(); + impl_->glClearStencil(stencil); +} +void Context::colorMask(bool red, bool green, bool blue, bool alpha) { + makeCurrent(); + impl_->glColorMask(red ? GL_TRUE : GL_FALSE, green ? GL_TRUE : GL_FALSE, + blue ? GL_TRUE : GL_FALSE, alpha ? GL_TRUE : GL_FALSE); +} +void Context::cullFace(uint32_t mode) { + makeCurrent(); + impl_->glCullFace(mode); +} +void Context::deleteShader(uint32_t shader) { + makeCurrent(); + impl_->glDeleteShader(shader); +} +void Context::depthFunc(uint32_t function) { + makeCurrent(); + impl_->glDepthFunc(function); +} +void Context::depthMask(bool enabled) { + makeCurrent(); + impl_->glDepthMask(enabled ? GL_TRUE : GL_FALSE); +} +void Context::disable(uint32_t capability) { + makeCurrent(); + impl_->glDisable(capability); +} +void Context::enable(uint32_t capability) { + makeCurrent(); + impl_->glEnable(capability); +} +void Context::frontFace(uint32_t mode) { + makeCurrent(); + impl_->glFrontFace(mode); +} +void Context::pixelStorei(uint32_t parameter, int32_t value) { + makeCurrent(); + // These three WebGL-only settings are handled during image unpacking. Typed + // array uploads need no conversion when they retain their default values. + if (parameter == 0x9240 || parameter == 0x9241 || parameter == 0x9243) { + return; + } + impl_->glPixelStorei(parameter, value); +} +void Context::scissor(int32_t x, int32_t y, int32_t width, int32_t height) { + makeCurrent(); + impl_->glScissor(x, y, width, height); +} +void Context::stencilMask(uint32_t mask) { + makeCurrent(); + impl_->glStencilMask(mask); +} + +void Context::framebufferRenderbuffer(uint32_t target, uint32_t attachment, + uint32_t renderbufferTarget, + uint32_t renderbuffer) { + makeCurrent(); + impl_->glFramebufferRenderbuffer(target, attachment, renderbufferTarget, + renderbuffer); +} +void Context::framebufferTexture2D(uint32_t target, uint32_t attachment, + uint32_t textureTarget, uint32_t texture, + int32_t level) { + makeCurrent(); + impl_->glFramebufferTexture2D(target, attachment, textureTarget, texture, + level); +} +void Context::renderbufferStorage(uint32_t target, uint32_t internalFormat, + int32_t width, int32_t height) { + makeCurrent(); + impl_->glRenderbufferStorage(target, internalFormat, width, height); +} +void Context::drawBuffers(const std::vector &buffers) { + makeCurrent(); + impl_->glDrawBuffers(static_cast(buffers.size()), buffers.data()); +} + +void Context::texImage2D(uint32_t target, int32_t level, int32_t internalFormat, + int32_t width, int32_t height, int32_t border, + uint32_t format, uint32_t type, const void *pixels) { + makeCurrent(); + impl_->glTexImage2D(target, level, internalFormat, width, height, border, + format, type, pixels); +} +void Context::texImage3D(uint32_t target, int32_t level, int32_t internalFormat, + int32_t width, int32_t height, int32_t depth, + int32_t border, uint32_t format, uint32_t type, + const void *pixels) { + makeCurrent(); + impl_->glTexImage3D(target, level, internalFormat, width, height, depth, + border, format, type, pixels); +} +void Context::texParameteri(uint32_t target, uint32_t parameter, + int32_t value) { + makeCurrent(); + impl_->glTexParameteri(target, parameter, value); +} +void Context::texStorage2D(uint32_t target, int32_t levels, + uint32_t internalFormat, int32_t width, + int32_t height) { + makeCurrent(); + impl_->glTexStorage2D(target, levels, internalFormat, width, height); +} +void Context::texSubImage2D(uint32_t target, int32_t level, int32_t xOffset, + int32_t yOffset, int32_t width, int32_t height, + uint32_t format, uint32_t type, + const void *pixels) { + makeCurrent(); + impl_->glTexSubImage2D(target, level, xOffset, yOffset, width, height, format, + type, pixels); +} + +void Context::uniform1f(int32_t location, float x) { + makeCurrent(); + impl_->glUniform1f(location, x); +} +void Context::uniform1i(int32_t location, int32_t x) { + makeCurrent(); + impl_->glUniform1i(location, x); +} +void Context::uniform1iv(int32_t location, int32_t count, + const int32_t *values) { + makeCurrent(); + impl_->glUniform1iv(location, count, values); +} +void Context::uniform2f(int32_t location, float x, float y) { + makeCurrent(); + impl_->glUniform2f(location, x, y); +} +void Context::uniform3f(int32_t location, float x, float y, float z) { + makeCurrent(); + impl_->glUniform3f(location, x, y, z); +} +void Context::uniform3fv(int32_t location, int32_t count, const float *values) { + makeCurrent(); + impl_->glUniform3fv(location, count, values); +} +void Context::uniformMatrix3fv(int32_t location, int32_t count, bool transpose, + const float *values) { + makeCurrent(); + impl_->glUniformMatrix3fv(location, count, transpose ? GL_TRUE : GL_FALSE, + values); +} +void Context::uniformMatrix4fv(int32_t location, int32_t count, bool transpose, + const float *values) { + makeCurrent(); + impl_->glUniformMatrix4fv(location, count, transpose ? GL_TRUE : GL_FALSE, + values); +} + +void Context::viewport(int32_t x, int32_t y, int32_t width, int32_t height) { + makeCurrent(); + impl_->glViewport(x, y, width, height); +} +void Context::clearColor(float red, float green, float blue, float alpha) { + makeCurrent(); + impl_->glClearColor(red, green, blue, alpha); +} +void Context::clear(uint32_t mask) { + makeCurrent(); + impl_->glClear(mask); +} +void Context::drawArrays(uint32_t mode, int32_t first, int32_t count) { + makeCurrent(); + impl_->glDrawArrays(mode, first, count); +} +void Context::drawElements(uint32_t mode, int32_t count, uint32_t type, + size_t offset) { + makeCurrent(); + impl_->glDrawElements(mode, count, type, + reinterpret_cast(offset)); +} +void Context::drawElementsInstanced(uint32_t mode, int32_t count, uint32_t type, + size_t offset, int32_t instanceCount) { + makeCurrent(); + impl_->glDrawElementsInstanced( + mode, count, type, reinterpret_cast(offset), instanceCount); +} +void Context::finish() { + makeCurrent(); + impl_->glFinish(); +} +void Context::readPixels(int32_t x, int32_t y, int32_t width, int32_t height, + uint32_t format, uint32_t type, void *destination) { + makeCurrent(); + impl_->glReadPixels(x, y, width, height, format, type, destination); +} +int32_t Context::getInteger(uint32_t parameter) { + makeCurrent(); + GLint value = 0; + impl_->glGetIntegerv(parameter, &value); + return value; +} +std::vector Context::getIntegers(uint32_t parameter, size_t count) { + makeCurrent(); + std::vector values(count); + impl_->glGetIntegerv(parameter, values.data()); + return values; +} +uint32_t Context::getError() { + makeCurrent(); + return impl_->glGetError(); +} + +} // namespace mystral::webgl + +#else + +namespace mystral::webgl { + +struct Context::Impl { + std::string error = "ANGLE WebGL2 is not enabled in this build"; +}; + +Context::Context() : impl_(std::make_unique()) {} +Context::~Context() = default; +bool Context::initialize(uint32_t, uint32_t, const ContextAttributes &, + const NativeWindow &) { + return false; +} +void Context::shutdown() {} +bool Context::makeCurrent() { return false; } +bool Context::present() { return false; } +void *Context::nativeD3D11Device() { return nullptr; } +uint32_t Context::importD3D11Texture(void *) { return 0; } +bool Context::isInitialized() const { return false; } +bool Context::isWindowSurface() const { return false; } +const std::string &Context::errorMessage() const { return impl_->error; } +const std::string &Context::renderer() const { return impl_->error; } +const std::string &Context::version() const { return impl_->error; } +const std::string &Context::shadingLanguageVersion() const { + return impl_->error; +} + +} // namespace mystral::webgl + +#endif diff --git a/src/webgpu/bindings.cpp b/src/webgpu/bindings.cpp index 1ea8ffa..b8f6db3 100644 --- a/src/webgpu/bindings.cpp +++ b/src/webgpu/bindings.cpp @@ -41,6 +41,10 @@ // Canvas 2D context (Skia-backed) #include "mystral/canvas/canvas2d.h" +#ifdef MYSTRAL_HAS_WEBGL +#include "mystral/webgl/context.h" +#endif + // Forward declaration for Canvas2D bindings namespace mystral { namespace canvas { @@ -56,6 +60,10 @@ struct OffscreenCanvas { int height = 150; mystral::js::JSValueHandle context2d; // Cached 2D context (created on first getContext call) bool hasContext2d = false; +#ifdef MYSTRAL_HAS_WEBGL + mystral::js::JSValueHandle contextWebGL2; + bool hasContextWebGL2 = false; +#endif }; // Global storage for offscreen canvases (prevents them from being destroyed) @@ -90,6 +98,26 @@ static WGPUSurface g_surface = nullptr; static WGPUInstance g_instance = nullptr; static js::Engine* g_engine = nullptr; +static js::JSValueHandle createStyleObject() { + auto style = g_engine->newObject(); + g_engine->setProperty(style, "setProperty", + g_engine->newFunction("setProperty", [](void* ctx, const std::vector& args) { + return g_engine->newUndefined(); + }) + ); + g_engine->setProperty(style, "removeProperty", + g_engine->newFunction("removeProperty", [](void* ctx, const std::vector& args) { + return g_engine->newUndefined(); + }) + ); + g_engine->setProperty(style, "getPropertyValue", + g_engine->newFunction("getPropertyValue", [](void* ctx, const std::vector& args) { + return g_engine->newString(""); + }) + ); + return style; +} + // Offscreen rendering support (for no-SDL mode) static WGPUTexture g_offscreenTexture = nullptr; static WGPUTextureView g_offscreenTextureView = nullptr; @@ -359,6 +387,12 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void g_verboseLogging = debug; g_engine = engine; +#ifdef MYSTRAL_HAS_WEBGL + if (!webgl::initBindings(engine, debug)) { + std::cerr << "[WebGL] Failed to initialize JavaScript bindings" << std::endl; + return false; + } +#endif g_instance = (WGPUInstance)wgpuInstance; g_device = (WGPUDevice)wgpuDevice; g_queue = (WGPUQueue)wgpuQueue; @@ -378,7 +412,7 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void // Create a mock parent element for the canvas (needed by Debugger) // ======================================================================== auto parentElement = engine->newObject(); - engine->setProperty(parentElement, "style", engine->newObject()); + engine->setProperty(parentElement, "style", createStyleObject()); engine->setProperty(parentElement, "appendChild", engine->newFunction("appendChild", [](void* ctx, const std::vector& args) { // No-op in native runtime @@ -418,8 +452,10 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void engine->setProperty(canvasObject, "height", engine->newNumber(g_canvasHeight)); engine->setProperty(canvasObject, "clientWidth", engine->newNumber(g_canvasWidth)); engine->setProperty(canvasObject, "clientHeight", engine->newNumber(g_canvasHeight)); + engine->setProperty(canvasObject, "dataset", engine->newObject()); // canvas.parentElement - mock parent element (for Debugger compatibility) + engine->setProperty(canvasObject, "style", createStyleObject()); engine->setProperty(canvasObject, "parentElement", parentElement); // canvas.getContext('webgpu') -> GPUCanvasContext @@ -448,6 +484,25 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void return ctx2d; } +#ifdef MYSTRAL_HAS_WEBGL + if (contextType == "webgl2") { + auto canvas = g_engine->getGlobalProperty("canvas"); + auto cached = g_engine->getProperty(canvas, "_contextWebGL2"); + if (!g_engine->isUndefined(cached) && !g_engine->isNull(cached)) { + return cached; + } + + auto context = webgl::createContextJSObject( + g_engine, g_canvasWidth, g_canvasHeight, + webgl::contextAttributesFromJS(g_engine, args)); + if (!g_engine->isNull(context)) { + g_engine->setProperty(context, "canvas", canvas); + g_engine->setProperty(canvas, "_contextWebGL2", context); + } + return context; + } +#endif + if (contextType != "webgpu") { std::cerr << "[Canvas] Unknown context type: " << contextType << std::endl; return g_engine->newNull(); @@ -609,6 +664,32 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void }) ); + engine->setProperty(existingDocument, "getElementsByClassName", + engine->newFunction("getElementsByClassName", [](void* ctx, const std::vector& args) { + auto result = g_engine->newArray(1); + auto element = g_engine->newObject(); + g_engine->setProperty(element, "style", createStyleObject()); + g_engine->setPropertyIndex(result, 0, element); + return result; + }) + ); + engine->setProperty(existingDocument, "querySelectorAll", + engine->newFunction("querySelectorAll", [](void* ctx, const std::vector& args) { + if (args.empty() || g_engine->toString(args[0]).empty()) { + return g_engine->newArray(0); + } + std::string selector = g_engine->toString(args[0]); + if (selector[0] != '.') { + return g_engine->newArray(0); + } + auto result = g_engine->newArray(1); + auto element = g_engine->newObject(); + g_engine->setProperty(element, "style", createStyleObject()); + g_engine->setPropertyIndex(result, 0, element); + return result; + }) + ); + // Add createElement to existing document // NOTE: runtime.cpp sets up a createElement with canvas support (toDataURL) for @loaders.gl WebP detection // We ALWAYS override it here to add proper Canvas 2D support for offscreen canvases @@ -623,9 +704,12 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void } // Add basic DOM element properties - g_engine->setProperty(element, "style", g_engine->newObject()); + g_engine->setProperty(element, "style", createStyleObject()); + g_engine->setProperty(element, "dataset", g_engine->newObject()); g_engine->setProperty(element, "className", g_engine->newString("")); - g_engine->setProperty(element, "innerHTML", g_engine->newString("")); + if (tagName != "template" && tagName != "TEMPLATE") { + g_engine->setProperty(element, "innerHTML", g_engine->newString("")); + } g_engine->setProperty(element, "textContent", g_engine->newString("")); g_engine->setProperty(element, "tagName", g_engine->newString(tagName.c_str())); g_engine->setProperty(element, "appendChild", @@ -633,6 +717,11 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void return a.empty() ? g_engine->newUndefined() : a[0]; }) ); + g_engine->setProperty(element, "append", + g_engine->newFunction("append", [](void* c, const std::vector& a) { + return g_engine->newUndefined(); + }) + ); g_engine->setProperty(element, "removeChild", g_engine->newFunction("removeChild", [](void* c, const std::vector& a) { return a.empty() ? g_engine->newUndefined() : a[0]; @@ -726,6 +815,38 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void return canvas->context2d; } +#ifdef MYSTRAL_HAS_WEBGL + if (contextType == "webgl2") { + if (canvas->hasContextWebGL2) { + return canvas->contextWebGL2; + } + + std::string globalName = "__offscreenCanvas_" + std::to_string(canvasId); + auto canvasElement = g_engine->getGlobalProperty(globalName.c_str()); + if (!g_engine->isNull(canvasElement) && !g_engine->isUndefined(canvasElement)) { + auto widthProp = g_engine->getProperty(canvasElement, "width"); + auto heightProp = g_engine->getProperty(canvasElement, "height"); + if (!g_engine->isUndefined(widthProp)) { + canvas->width = static_cast(g_engine->toNumber(widthProp)); + } + if (!g_engine->isUndefined(heightProp)) { + canvas->height = static_cast(g_engine->toNumber(heightProp)); + } + } + + canvas->contextWebGL2 = webgl::createContextJSObject( + g_engine, static_cast(canvas->width), + static_cast(canvas->height), + webgl::contextAttributesFromJS(g_engine, contextArgs)); + if (!g_engine->isNull(canvas->contextWebGL2)) { + g_engine->setProperty(canvas->contextWebGL2, "canvas", canvasElement); + canvas->hasContextWebGL2 = true; + g_engine->protect(canvas->contextWebGL2); + } + return canvas->contextWebGL2; + } +#endif + if (contextType == "webgpu") { // Create GPUCanvasContext for offscreen canvas // This shares the main surface/device for simplicity @@ -861,8 +982,8 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void return canvasContext; } - // Ignore webgl requests silently (PixiJS feature detection) - if (contextType == "webgl" || contextType == "webgl2" || contextType == "experimental-webgl") { + // WebGL 1 is not exposed until its compatibility profile is implemented. + if (contextType == "webgl" || contextType == "experimental-webgl") { return g_engine->newNull(); } @@ -906,10 +1027,75 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void ); } +#ifdef MYSTRAL_HAS_LEXBOR + if (tagName == "template" || tagName == "TEMPLATE") { + auto setTemplatePrototype = + g_engine->getGlobalProperty("__mystralSetTemplatePrototype"); + if (g_engine->isFunction(setTemplatePrototype)) { + g_engine->call(setTemplatePrototype, g_engine->newUndefined(), {element}); + } + } +#endif + return element; }) ); + // DOM libraries such as Three.js create canvases through the HTML namespace. + engine->setProperty(existingDocument, "createElementNS", + engine->newFunction("createElementNS", [](void* ctx, const std::vector& args) { + if (args.size() < 2) { + return g_engine->newNull(); + } + auto document = g_engine->getGlobalProperty("document"); + auto createElement = g_engine->getProperty(document, "createElement"); + return g_engine->call(createElement, document, {args[1]}); + }) + ); + engine->setProperty(existingDocument, "createComment", + engine->newFunction("createComment", [](void* ctx, const std::vector& args) { + auto comment = g_engine->newObject(); + std::string text = args.empty() ? "" : g_engine->toString(args[0]); + g_engine->setProperty(comment, "nodeType", g_engine->newNumber(8)); + g_engine->setProperty(comment, "data", g_engine->newString(text.c_str())); + g_engine->setProperty(comment, "textContent", g_engine->newString(text.c_str())); + g_engine->setProperty(comment, "remove", + g_engine->newFunction("remove", [](void* c, const std::vector& a) { + return g_engine->newUndefined(); + }) + ); + return comment; + }) + ); + engine->setProperty(existingDocument, "createDocumentFragment", + engine->newFunction("createDocumentFragment", [](void* ctx, const std::vector& args) { + auto fragment = g_engine->newObject(); + g_engine->setProperty(fragment, "nodeType", g_engine->newNumber(11)); + g_engine->setProperty(fragment, "childNodes", g_engine->newArray(0)); + g_engine->setProperty(fragment, "children", g_engine->newArray(0)); + g_engine->setProperty(fragment, "append", + g_engine->newFunction("append", [](void* c, const std::vector& a) { + return g_engine->newUndefined(); + }) + ); + return fragment; + }) + ); + engine->setProperty(existingDocument, "importNode", + engine->newFunction("importNode", [](void* ctx, const std::vector& args) { + if (args.empty()) { + return g_engine->newNull(); + } + auto cloneNode = g_engine->getProperty(args[0], "cloneNode"); + if (!g_engine->isFunction(cloneNode)) { + return args[0]; + } + std::vector cloneArgs; + cloneArgs.push_back(args.size() > 1 ? args[1] : g_engine->newBoolean(false)); + return g_engine->call(cloneNode, args[0], cloneArgs); + }) + ); + // Add document.body if not present, or enhance existing body with required methods auto existingBody = engine->getProperty(existingDocument, "body"); if (engine->isUndefined(existingBody) || engine->isNull(existingBody)) { @@ -917,7 +1103,11 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void engine->setProperty(existingDocument, "body", existingBody); } // Always add/update these methods on body - engine->setProperty(existingBody, "style", engine->newObject()); + engine->setProperty(existingBody, "style", createStyleObject()); + auto documentElement = engine->getProperty(existingDocument, "documentElement"); + if (!engine->isUndefined(documentElement) && !engine->isNull(documentElement)) { + engine->setProperty(documentElement, "style", createStyleObject()); + } engine->setProperty(existingBody, "appendChild", engine->newFunction("appendChild", [](void* ctx, const std::vector& args) { return args.empty() ? g_engine->newUndefined() : args[0]; @@ -942,6 +1132,8 @@ bool initBindings(js::Engine* engine, void* wgpuInstance, void* wgpuDevice, void // PixiJS and other libraries check these for feature detection engine->setProperty(navigatorHandle, "userAgent", engine->newString("Mozilla/5.0 (Macintosh; MystralNative/0.1) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36")); + engine->setProperty(navigatorHandle, "appVersion", + engine->newString("5.0 (MystralNative/0.1)")); engine->setProperty(navigatorHandle, "platform", engine->newString("MystralNative")); engine->setProperty(navigatorHandle, "vendor", engine->newString("Mystral Engine")); engine->setProperty(navigatorHandle, "language", engine->newString("en-US")); @@ -4342,6 +4534,117 @@ async function createImageBitmap(source, options) { globalThis.createImageBitmap = createImageBitmap; globalThis.ImageBitmap = ImageBitmap; +class FileReader { + constructor() { + this.result = null; + this.error = null; + this.readyState = FileReader.EMPTY; + this.onload = null; + this.onerror = null; + this.onloadend = null; + this._listeners = new Map(); + } + + addEventListener(type, callback) { + const listeners = this._listeners.get(type) || []; + listeners.push(callback); + this._listeners.set(type, listeners); + } + + removeEventListener(type, callback) { + const listeners = this._listeners.get(type) || []; + this._listeners.set(type, listeners.filter(listener => listener !== callback)); + } + + _dispatch(type) { + const event = { type, target: this }; + if (typeof this['on' + type] === 'function') this['on' + type](event); + for (const listener of this._listeners.get(type) || []) listener(event); + } + + async _read(source, transform) { + this.readyState = FileReader.LOADING; + try { + const value = source && typeof source.arrayBuffer === 'function' + ? await source.arrayBuffer() + : source; + this.result = await transform(value); + this.readyState = FileReader.DONE; + this._dispatch('load'); + } catch (error) { + this.error = error; + this.readyState = FileReader.DONE; + this._dispatch('error'); + } + this._dispatch('loadend'); + } + + readAsArrayBuffer(source) { + return this._read(source, value => value instanceof ArrayBuffer ? value : value?.buffer); + } + + readAsText(source) { + return this._read(source, value => new TextDecoder().decode(value)); + } + + readAsDataURL(source) { + return this._read(source, value => { + const bytes = new Uint8Array(value); + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return 'data:application/octet-stream;base64,' + btoa(binary); + }); + } + + abort() { + this.readyState = FileReader.DONE; + this._dispatch('abort'); + this._dispatch('loadend'); + } +} +FileReader.EMPTY = 0; +FileReader.LOADING = 1; +FileReader.DONE = 2; +globalThis.FileReader = FileReader; + +if (typeof globalThis.Node === 'undefined') { + globalThis.Node = class Node {}; +} +if (!Object.getOwnPropertyDescriptor(Node.prototype, 'firstChild')) { + Object.defineProperty(Node.prototype, 'firstChild', { + configurable: true, + get() { return this.childNodes?.[0] ?? this.children?.[0] ?? null; } + }); +} +if (!Object.getOwnPropertyDescriptor(Node.prototype, 'nextSibling')) { + Object.defineProperty(Node.prototype, 'nextSibling', { + configurable: true, + get() { + const siblings = this.parentNode?.childNodes || this.parentNode?.children || []; + const index = Array.prototype.indexOf.call(siblings, this); + return index >= 0 ? siblings[index + 1] ?? null : null; + } + }); +} +if (typeof Node.prototype.remove !== 'function') { + Node.prototype.remove = function() { this.parentNode?.removeChild?.(this); }; +} +if (typeof globalThis.Text === 'undefined') { + globalThis.Text = class Text extends Node {}; +} +if (typeof globalThis.Comment === 'undefined') { + globalThis.Comment = class Comment extends Node {}; +} +if (typeof globalThis.DocumentFragment === 'undefined') { + globalThis.DocumentFragment = class DocumentFragment extends Node {}; +} +if (typeof globalThis.Element === 'undefined') { + globalThis.Element = class Element extends Node {}; +} +if (typeof globalThis.HTMLElement === 'undefined') { + globalThis.HTMLElement = class HTMLElement extends Element {}; +} + // CanvasRenderingContext2D - Placeholder class for instanceof checks // The actual implementation is in Canvas2D bindings, this is just for type checking class CanvasRenderingContext2D { @@ -4352,8 +4655,8 @@ class CanvasRenderingContext2D { globalThis.CanvasRenderingContext2D = CanvasRenderingContext2D; // HTMLCanvasElement - Placeholder class for instanceof checks -class HTMLCanvasElement { - constructor() {} +class HTMLCanvasElement extends HTMLElement { + constructor() { super(); } } globalThis.HTMLCanvasElement = HTMLCanvasElement; diff --git a/src/websocket/client.cpp b/src/websocket/client.cpp new file mode 100644 index 0000000..4e87898 --- /dev/null +++ b/src/websocket/client.cpp @@ -0,0 +1,358 @@ +#include "mystral/websocket/client.h" + +#include +#include +#include +#include +#include +#include +#include + +#if !defined(MYSTRAL_HTTP_FOUNDATION) && !defined(MYSTRAL_HTTP_ANDROID) +#include +#endif + +namespace mystral::websocket { + +struct ClientManager::Impl { + struct OutgoingMessage { + std::vector data; + bool binary = false; + }; + + struct Connection { + uint64_t id = 0; + std::string url; + std::vector protocols; + std::string selectedProtocol; + std::mutex mutex; + std::deque outgoing; + std::atomic closeRequested{false}; + std::atomic finished{false}; + uint16_t closeCode = 1000; + std::string closeReason; + std::thread worker; + }; + + std::atomic nextId{1}; + std::mutex connectionsMutex; + std::unordered_map> connections; + std::mutex eventsMutex; + std::deque events; + + void queueEvent(Event event) { + std::lock_guard lock(eventsMutex); + events.push_back(std::move(event)); + } + +#if !defined(MYSTRAL_HTTP_FOUNDATION) && !defined(MYSTRAL_HTTP_ANDROID) + static size_t headerCallback(char* buffer, size_t size, size_t count, void* userData) { + const size_t length = size * count; + auto* connection = static_cast(userData); + std::string line(buffer, length); + constexpr const char* prefix = "sec-websocket-protocol:"; + std::string lower = line; + for (char& c : lower) { + if (c >= 'A' && c <= 'Z') c = static_cast(c - 'A' + 'a'); + } + if (lower.rfind(prefix, 0) == 0) { + std::string value = line.substr(std::char_traits::length(prefix)); + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(value.begin()); + while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ')) value.pop_back(); + connection->selectedProtocol = std::move(value); + } + return length; + } + + static int progressCallback(void* userData, curl_off_t, curl_off_t, curl_off_t, curl_off_t) { + auto* connection = static_cast(userData); + return connection->closeRequested.load() ? 1 : 0; + } + + static bool sendFrame(CURL* easy, const std::vector& data, unsigned int flags) { + size_t offset = 0; + do { + size_t sent = 0; + const void* source = data.empty() + ? static_cast("") + : static_cast(data.data() + offset); + CURLcode result = curl_ws_send(easy, source, data.size() - offset, &sent, 0, flags); + offset += sent; + if (result == CURLE_AGAIN) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + if (result != CURLE_OK) return false; + } while (offset < data.size()); + return true; + } + + void runConnection(const std::shared_ptr& connection) { + CURL* easy = curl_easy_init(); + if (!easy) { + queueEvent({EventType::Error, connection->id, {}, "Failed to create CURL WebSocket handle"}); + queueEvent({EventType::Close, connection->id, {}, {}, {}, 1006, false, false}); + connection->finished = true; + return; + } + + struct curl_slist* headers = nullptr; + if (!connection->protocols.empty()) { + std::string value = "Sec-WebSocket-Protocol: "; + for (size_t i = 0; i < connection->protocols.size(); ++i) { + if (i) value += ", "; + value += connection->protocols[i]; + } + headers = curl_slist_append(headers, value.c_str()); + } + + char errorBuffer[CURL_ERROR_SIZE] = {}; + curl_easy_setopt(easy, CURLOPT_URL, connection->url.c_str()); + curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 2L); + curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 10000L); + curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L); + curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, errorBuffer); + curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(easy, CURLOPT_XFERINFOFUNCTION, progressCallback); + curl_easy_setopt(easy, CURLOPT_XFERINFODATA, connection.get()); + curl_easy_setopt(easy, CURLOPT_USERAGENT, "MystralRuntime/0.1 (websocket)"); + curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, headerCallback); + curl_easy_setopt(easy, CURLOPT_HEADERDATA, connection.get()); + if (headers) curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); + + CURLcode result = curl_easy_perform(easy); + if (result != CURLE_OK) { + std::string message = errorBuffer[0] ? errorBuffer : curl_easy_strerror(result); + queueEvent({EventType::Error, connection->id, {}, std::move(message)}); + queueEvent({EventType::Close, connection->id, {}, {}, {}, 1006, false, false}); + if (headers) curl_slist_free_all(headers); + curl_easy_cleanup(easy); + connection->finished = true; + return; + } + + Event openEvent; + openEvent.type = EventType::Open; + openEvent.connectionId = connection->id; + openEvent.protocol = connection->selectedProtocol; + queueEvent(std::move(openEvent)); + + std::vector messageBuffer; + std::vector closeBuffer; + bool messageBinary = false; + bool remoteClose = false; + bool failed = false; + std::string failureMessage; + + while (!connection->closeRequested.load()) { + OutgoingMessage outgoing; + bool hasOutgoing = false; + { + std::lock_guard lock(connection->mutex); + if (!connection->outgoing.empty()) { + outgoing = std::move(connection->outgoing.front()); + connection->outgoing.pop_front(); + hasOutgoing = true; + } + } + if (hasOutgoing) { + unsigned int flags = outgoing.binary ? CURLWS_BINARY : CURLWS_TEXT; + if (!sendFrame(easy, outgoing.data, flags)) { + failed = true; + failureMessage = "WebSocket send failed"; + break; + } + } + + uint8_t buffer[65536]; + size_t received = 0; + const curl_ws_frame* meta = nullptr; + result = curl_ws_recv(easy, buffer, sizeof(buffer), &received, &meta); + if (result == CURLE_AGAIN) { + if (!hasOutgoing) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + if (result != CURLE_OK) { + failed = true; + failureMessage = curl_easy_strerror(result); + break; + } + if (!meta) continue; + + if (meta->flags & CURLWS_CLOSE) { + closeBuffer.insert(closeBuffer.end(), buffer, buffer + received); + if (meta->bytesleft == 0) { + remoteClose = true; + break; + } + continue; + } + if (meta->flags & (CURLWS_PING | CURLWS_PONG)) continue; + + if (meta->offset == 0 && messageBuffer.empty()) { + messageBinary = (meta->flags & CURLWS_BINARY) != 0; + } + messageBuffer.insert(messageBuffer.end(), buffer, buffer + received); + if (meta->bytesleft == 0 && !(meta->flags & CURLWS_CONT)) { + Event messageEvent; + messageEvent.type = EventType::Message; + messageEvent.connectionId = connection->id; + messageEvent.binary = messageBinary; + if (messageBinary) { + messageEvent.data = std::move(messageBuffer); + messageBuffer.clear(); + } else { + messageEvent.text.assign(messageBuffer.begin(), messageBuffer.end()); + messageBuffer.clear(); + } + queueEvent(std::move(messageEvent)); + } + } + + uint16_t closeCode = connection->closeCode; + std::string closeReason = connection->closeReason; + bool clean = !failed; + if (remoteClose) { + closeCode = 1005; + if (closeBuffer.size() >= 2) { + closeCode = static_cast((closeBuffer[0] << 8) | closeBuffer[1]); + closeReason.assign(closeBuffer.begin() + 2, closeBuffer.end()); + } + sendFrame(easy, closeBuffer, CURLWS_CLOSE); + } else if (!failed) { + std::vector payload; + payload.push_back(static_cast((closeCode >> 8) & 0xff)); + payload.push_back(static_cast(closeCode & 0xff)); + payload.insert(payload.end(), closeReason.begin(), closeReason.end()); + sendFrame(easy, payload, CURLWS_CLOSE); + } + + if (failed) { + queueEvent({EventType::Error, connection->id, {}, std::move(failureMessage)}); + closeCode = 1006; + closeReason.clear(); + clean = false; + } + + Event closeEvent; + closeEvent.type = EventType::Close; + closeEvent.connectionId = connection->id; + closeEvent.closeCode = closeCode; + closeEvent.text = std::move(closeReason); + closeEvent.clean = clean; + queueEvent(std::move(closeEvent)); + + if (headers) curl_slist_free_all(headers); + curl_easy_cleanup(easy); + connection->finished = true; + } +#endif +}; + +ClientManager& ClientManager::instance() { + static ClientManager manager; + return manager; +} + +ClientManager::ClientManager() : impl_(std::make_unique()) {} +ClientManager::~ClientManager() { shutdown(); } + +uint64_t ClientManager::connect(const std::string& url, const std::vector& protocols) { + auto connection = std::make_shared(); + connection->id = impl_->nextId.fetch_add(1); + connection->url = url; + connection->protocols = protocols; + { + std::lock_guard lock(impl_->connectionsMutex); + impl_->connections[connection->id] = connection; + } +#if !defined(MYSTRAL_HTTP_FOUNDATION) && !defined(MYSTRAL_HTTP_ANDROID) + connection->worker = std::thread([impl = impl_.get(), connection] { + impl->runConnection(connection); + }); +#else + impl_->queueEvent({EventType::Error, connection->id, {}, "WebSocket is unavailable on this platform"}); + impl_->queueEvent({EventType::Close, connection->id, {}, {}, {}, 1006, false, false}); + connection->finished = true; +#endif + return connection->id; +} + +bool ClientManager::send(uint64_t connectionId, std::vector data, bool binary) { + std::shared_ptr connection; + { + std::lock_guard lock(impl_->connectionsMutex); + auto it = impl_->connections.find(connectionId); + if (it == impl_->connections.end()) return false; + connection = it->second; + } + if (connection->closeRequested || connection->finished) return false; + std::lock_guard lock(connection->mutex); + connection->outgoing.push_back({std::move(data), binary}); + return true; +} + +void ClientManager::close(uint64_t connectionId, uint16_t code, const std::string& reason) { + std::shared_ptr connection; + { + std::lock_guard lock(impl_->connectionsMutex); + auto it = impl_->connections.find(connectionId); + if (it == impl_->connections.end()) return; + connection = it->second; + } + connection->closeCode = code; + connection->closeReason = reason; + connection->closeRequested = true; +} + +std::vector ClientManager::pollEvents() { + std::vector result; + { + std::lock_guard lock(impl_->eventsMutex); + result.reserve(impl_->events.size()); + while (!impl_->events.empty()) { + result.push_back(std::move(impl_->events.front())); + impl_->events.pop_front(); + } + } + + std::vector> finished; + { + std::lock_guard lock(impl_->connectionsMutex); + for (auto it = impl_->connections.begin(); it != impl_->connections.end();) { + if (it->second->finished) { + finished.push_back(it->second); + it = impl_->connections.erase(it); + } else { + ++it; + } + } + } + for (auto& connection : finished) { + if (connection->worker.joinable()) connection->worker.join(); + } + return result; +} + +void ClientManager::shutdown() { + if (!impl_) return; + std::vector> connections; + { + std::lock_guard lock(impl_->connectionsMutex); + for (auto& [id, connection] : impl_->connections) { + connection->closeRequested = true; + connections.push_back(connection); + } + impl_->connections.clear(); + } + for (auto& connection : connections) { + if (connection->worker.joinable()) connection->worker.join(); + } + { + std::lock_guard lock(impl_->eventsMutex); + impl_->events.clear(); + } +} + +} // namespace mystral::websocket diff --git a/tests/gpu/fetch.test.ts b/tests/gpu/fetch.test.ts index cdd12fc..e76baa7 100644 --- a/tests/gpu/fetch.test.ts +++ b/tests/gpu/fetch.test.ts @@ -225,4 +225,114 @@ describe("Fetch API", () => { expect(stdout).toContain("PASS: arrayBuffer works"); }); + + it("should support XMLHttpRequest text responses and events", async () => { + if (!existsSync(MYSTRAL_BIN)) { + console.log("Skipping: mystral binary not found"); + return; + } + + const testScript = ` + const states = []; + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => states.push(xhr.readyState); + xhr.open('GET', 'file://${join(TEST_DIR, "test.txt")}'); + xhr.setRequestHeader('X-Test', 'one'); + xhr.setRequestHeader('X-Test', 'two'); + xhr.onprogress = (event) => { + if (event.loaded !== 13 || event.total !== 13 || !event.lengthComputable) { + console.log('FAIL: invalid progress event'); + } + }; + xhr.onload = () => { + const passed = xhr.status === 200 && xhr.responseText === 'Hello, World!' && + states.includes(XMLHttpRequest.HEADERS_RECEIVED) && + states[states.length - 1] === XMLHttpRequest.DONE; + console.log(passed ? 'PASS: XMLHttpRequest works' : 'FAIL: invalid XMLHttpRequest result'); + }; + xhr.onerror = () => console.log('FAIL: XMLHttpRequest error'); + xhr.send(); + `; + + writeFileSync(join(TEST_DIR, "xhr-test.js"), testScript); + + const proc = spawn({ + cmd: [ + MYSTRAL_BIN, + "run", + join(TEST_DIR, "xhr-test.js"), + "--headless", + "--screenshot", + join(TEST_DIR, "xhr-test-screenshot.png"), + "--frames", + "10", + ], + stdout: "pipe", + stderr: "pipe", + }); + + const stdout = await new Response(proc.stdout).text(); + await proc.exited; + + expect(stdout).toContain("PASS: XMLHttpRequest works"); + }); + + it("should forward XMLHttpRequest methods, headers, bodies, and response headers", async () => { + if (!existsSync(MYSTRAL_BIN)) { + console.log("Skipping: mystral binary not found"); + return; + } + + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + const body = await request.text(); + return new Response( + `${request.method}|${request.headers.get("x-test")}|${body}`, + { headers: { "X-Reply": "seen" } } + ); + }, + }); + + try { + const testScript = ` + const xhr = new XMLHttpRequest(); + xhr.open('PATCH', '${server.url}resource'); + xhr.setRequestHeader('X-Test', 'one'); + xhr.setRequestHeader('X-Test', 'two'); + xhr.onload = () => { + const passed = xhr.status === 200 && + xhr.responseText === 'PATCH|one, two|payload' && + xhr.getResponseHeader('x-reply') === 'seen'; + console.log(passed ? 'PASS: XMLHttpRequest options work' : + 'FAIL: ' + xhr.status + '|' + xhr.responseText + '|' + xhr.getResponseHeader('x-reply')); + }; + xhr.onerror = () => console.log('FAIL: XMLHttpRequest options error'); + xhr.send('payload'); + `; + writeFileSync(join(TEST_DIR, "xhr-options-test.js"), testScript); + + const proc = spawn({ + cmd: [ + MYSTRAL_BIN, + "run", + join(TEST_DIR, "xhr-options-test.js"), + "--headless", + "--screenshot", + join(TEST_DIR, "xhr-options-test-screenshot.png"), + "--frames", + "120", + ], + stdout: "pipe", + stderr: "pipe", + }); + + const stdout = await new Response(proc.stdout).text(); + await proc.exited; + expect(stdout).toContain("PASS: XMLHttpRequest options work"); + } finally { + server.stop(true); + } + }); }); diff --git a/tests/websocket/client.test.ts b/tests/websocket/client.test.ts new file mode 100644 index 0000000..b376b48 --- /dev/null +++ b/tests/websocket/client.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "bun:test"; +import { spawn } from "bun"; +import { existsSync, mkdirSync, writeFileSync } from "fs"; +import { join } from "path"; + +const MYSTRAL_BIN = join(import.meta.dir, "../../build/mystral"); +const TEST_DIR = join(import.meta.dir, "../../.test-tmp"); + +describe("WebSocket API", () => { + it("should exchange text and binary messages and close cleanly", async () => { + if (!existsSync(MYSTRAL_BIN)) { + console.log("Skipping: mystral binary not found"); + return; + } + mkdirSync(TEST_DIR, { recursive: true }); + + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request, server) { + if ( + server.upgrade(request, { + headers: { "Sec-WebSocket-Protocol": "echo-protocol" }, + }) + ) { + return; + } + return new Response("WebSocket upgrade required", { status: 426 }); + }, + websocket: { + message(socket, message) { + socket.send(message); + }, + }, + }); + + try { + const url = server.url.href.replace(/^http/, "ws"); + const script = ` + const received = []; + const socket = new WebSocket('${url}', 'echo-protocol'); + socket.binaryType = 'arraybuffer'; + socket.onopen = () => { + socket.send('native-text'); + socket.send(new Uint8Array([7, 11, 13, 17])); + }; + socket.onmessage = event => { + received.push(typeof event.data === 'string' + ? 'text:' + event.data + : 'binary:' + Array.from(new Uint8Array(event.data)).join(',')); + if (received.length === 2) socket.close(4001, 'complete'); + }; + socket.onerror = event => { + console.log('FAIL: ' + (event.message || 'WebSocket error')); + process.exit(1); + }; + socket.onclose = event => { + const passed = socket.protocol === 'echo-protocol' && + received.includes('text:native-text') && + received.includes('binary:7,11,13,17') && + event.code === 4001 && event.reason === 'complete' && event.wasClean; + console.log(passed ? 'PASS: WebSocket works' : 'FAIL: invalid WebSocket result'); + process.exit(passed ? 0 : 1); + }; + setTimeout(() => { console.log('FAIL: timeout'); process.exit(1); }, 10000); + `; + const scriptPath = join(TEST_DIR, "websocket-test.js"); + writeFileSync(scriptPath, script); + + const proc = spawn({ + cmd: [MYSTRAL_BIN, "run", scriptPath, "--headless"], + stdout: "pipe", + stderr: "pipe", + }); + const stdout = await new Response(proc.stdout).text(); + const exitCode = await proc.exited; + + expect(exitCode).toBe(0); + expect(stdout).toContain("PASS: WebSocket works"); + } finally { + server.stop(true); + } + }, 30_000); +});