Skip to content
Open

QuickJS #1764

Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ on:
required: false
type: string
default: macos-latest
js-engine:
required: false
type: string
default: ''

env:
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:symbolize=1
Expand All @@ -41,6 +45,7 @@ jobs:
- name: Generate macOS solution
run: |
cmake -G "${{ inputs.generator }}" -B build/macOS \
${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \
-D BABYLON_DEBUG_TRACE=ON \
-D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \
-D BABYLON_NATIVE_TESTS_USE_NOOP_METAL_DEVICE=ON
Comment thread
CedricGuillemet marked this conversation as resolved.
Expand Down Expand Up @@ -74,7 +79,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@v6
with:
name: macos-${{ inputs.generator }}${{ inputs.enable-sanitizers && '-sanitizer' || '' }}-core-dumps
name: macos-${{ inputs.generator }}-${{ inputs.js-engine != '' && inputs.js-engine || 'default' }}${{ inputs.enable-sanitizers && '-sanitizer' || '' }}-core-dumps
path: |
build/macOS/Apps/UnitTests/RelWithDebInfo/UnitTests
build/macOS/Apps/UnitTests/RelWithDebInfo/*.core.*
3 changes: 3 additions & 0 deletions .github/workflows/build-win32.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
elif [ "${{ inputs.napi-type }}" = "Hermes" ]; then
SUFFIX="_Hermes"
JS_DEFINE="-DNAPI_JAVASCRIPT_ENGINE=Hermes -DHERMES_ALLOW_BOOST_CONTEXT=0"
elif [ "${{ inputs.napi-type }}" = "QuickJS" ]; then
SUFFIX="_QuickJS"
JS_DEFINE="-DNAPI_JAVASCRIPT_ENGINE=QuickJS"
fi
echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT"
echo "js_define=$JS_DEFINE" >> "$GITHUB_OUTPUT"
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ jobs:
xcode-version: "26.4"
runs-on: macos-26

MacOS_QuickJS:
uses: ./.github/workflows/build-macos.yml
with:
xcode-version: "26.4"
runs-on: macos-26
js-engine: QuickJS

# ── Win32─────────────────────────────────────────────────────
Win32_x64_D3D11:
uses: ./.github/workflows/build-win32.yml
Expand All @@ -66,6 +73,13 @@ jobs:
platform: x64
napi-type: Hermes

Win32_x64_QuickJS_D3D11:
uses: ./.github/workflows/build-win32.yml
with:
platform: x64
napi-type: QuickJS
graphics-api: D3D11

Comment thread
CedricGuillemet marked this conversation as resolved.
Win32_x64_D3D11_Sanitizers:
uses: ./.github/workflows/build-win32.yml
with:
Expand Down Expand Up @@ -120,6 +134,13 @@ jobs:
cxx: g++
js-engine: JavaScriptCore

Ubuntu_Clang_QuickJS:
uses: ./.github/workflows/build-linux.yml
with:
cc: clang
cxx: clang++
js-engine: QuickJS

# ── Android ───────────────────────────────────────────────────
Android_Ubuntu_JSC:
uses: ./.github/workflows/build-android.yml
Expand All @@ -139,6 +160,12 @@ jobs:
runs-on: ubuntu-latest
js-engine: Hermes

Android_Ubuntu_QuickJS:
uses: ./.github/workflows/build-android.yml
with:
runs-on: ubuntu-latest
js-engine: QuickJS

Android_MacOS_JSC:
uses: ./.github/workflows/build-android.yml
with:
Expand All @@ -151,6 +178,12 @@ jobs:
runs-on: macos-26
js-engine: V8

Android_MacOS_QuickJS:
uses: ./.github/workflows/build-android.yml
with:
runs-on: macos-26
js-engine: QuickJS

# ── Installation Tests ────────────────────────────────────────
iOS_Installation:
uses: ./.github/workflows/test-install-ios.yml
Expand Down
2 changes: 2 additions & 0 deletions Apps/ModuleLoadTest/Source/App.Apple.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <Babylon/DebugTrace.h>
#include <Babylon/Graphics/Device.h>

#import <Foundation/Foundation.h>

#import <Metal/Metal.hpp>

#include <sys/sysctl.h>
Expand Down
62 changes: 46 additions & 16 deletions Apps/Playground/Scripts/validation_native.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,21 +371,36 @@
}
}

currentScene = eval(code + "\r\ncreateScene(engine)");

if (currentScene.then) {
// Handle if createScene returns a promise
currentScene.then(function (scene) {
currentScene = scene;
processCurrentScene(test, referenceImage, done, compareFunction);
}).catch(function (e) {
console.error(e);
const pgCode = code + "\r\ncreateScene(engine)";
// Defer scene construction to a fresh macrotask so
// eval()/createScene() run at a shallow native-stack
// depth instead of nested inside the native snippet
// load callback. Deep scenes otherwise pile onto the
// native XHR dispatch frames and can overflow engines
// with a small C stack (e.g. QuickJS).
Comment on lines +375 to +380

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't Babylon users hit the same problem? This isn't unique to validation tests, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it's a stack overflow issue. This should happen with other JS engines as well. debug/release build also have influence on the number of stack element count. QuickJS stack elements are bigger so the overflow happens sooner.

setTimeout(function () {
try {
currentScene = eval(pgCode);

if (currentScene.then) {
// Handle if createScene returns a promise
currentScene.then(function (scene) {
currentScene = scene;
processCurrentScene(test, referenceImage, done, compareFunction);
}).catch(function (e) {
console.error(e);
failTest(done);
});
} else {
// Handle if createScene returns a scene
processCurrentScene(test, referenceImage, done, compareFunction);
}
}
catch (e) {
console.error("Failed to evaluate playground snippet " + test.playgroundId + ": " + e);
failTest(done);
});
} else {
// Handle if createScene returns a scene
processCurrentScene(test, referenceImage, done, compareFunction);
}
}
}, 0);
}
catch (e) {
console.error("Failed to evaluate playground snippet " + test.playgroundId + ": " + e);
Expand Down Expand Up @@ -442,8 +457,23 @@
}
}

currentScene = eval(scriptToRun + test.functionToCall + "(engine)");
processCurrentScene(test, renderImage, done, compareFunction);
const scriptCode = scriptToRun + test.functionToCall + "(engine)";
// Defer scene construction to a fresh macrotask so
// eval()/<functionToCall>() run at a shallow native-stack
// depth instead of nested inside the native XHR
// completion callback. Deep scenes otherwise pile onto
// the native XHR dispatch frames and can overflow engines
// with a small C stack (e.g. QuickJS).
setTimeout(function () {
try {
currentScene = eval(scriptCode);
processCurrentScene(test, renderImage, done, compareFunction);
}
catch (e) {
console.error(e);
failTest(done);
}
}, 0);
}
catch (e) {
console.error(e);
Expand Down
2 changes: 2 additions & 0 deletions Apps/UnitTests/Source/App.Apple.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "App.h"
#include <Babylon/DebugTrace.h>

#import <Foundation/Foundation.h>

#include <mach-o/dyld.h>

std::filesystem::path GetExecutableDirectory()
Expand Down
6 changes: 3 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ FetchContent_Declare(AndroidExtensions
EXCLUDE_FROM_ALL)
FetchContent_Declare(arcana.cpp
GIT_REPOSITORY https://github.com/microsoft/arcana.cpp.git
GIT_TAG b9bf9d85fce37d5fc9dbfc4a4dc5e1531bee215a
GIT_TAG fe41b17dedea47b5dead98f0271ca284e94ac6a8
EXCLUDE_FROM_ALL)
FetchContent_Declare(arcore-android-sdk
GIT_REPOSITORY https://github.com/google-ar/arcore-android-sdk.git
Expand Down Expand Up @@ -53,8 +53,8 @@ FetchContent_Declare(ios-cmake
GIT_TAG 4.5.0
EXCLUDE_FROM_ALL)
FetchContent_Declare(JsRuntimeHost
GIT_REPOSITORY https://github.com/BabylonJS/JsRuntimeHost.git
GIT_TAG f07d99119887131a89a3580ada9c2a8dbc7782f7)
GIT_REPOSITORY https://github.com/CedricGuillemet/JsRuntimeHost.git
Comment thread
CedricGuillemet marked this conversation as resolved.
Outdated
GIT_TAG 0e34a58a2e0a986efbf7b70434b61739c451afb7) # quickjs branch (BabylonJS/JsRuntimeHost#132)
FetchContent_Declare(metal-cpp
GIT_REPOSITORY https://github.com/bkaradzic/metal-cpp.git
GIT_TAG metal-cpp_26
Expand Down
9 changes: 5 additions & 4 deletions Polyfills/Canvas/Source/Gradient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ namespace Babylon::Polyfills::Internal

Napi::Object CanvasGradient::CreateLinear(Napi::Env env, const std::shared_ptr<NVGcontext*>& context, float x0, float y0, float x1, float y1)
{
Napi::HandleScope scope{ env };

// NOTE: Do not open a Napi::HandleScope here. The gradient object created below is
// returned to the caller, and a plain (non-escapable) HandleScope would release the
// handle on close. Under the reference-counted QuickJS Node-API port that frees the
// object's only reference, yielding a dangling value (typeof "unknown", no prototype).
auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CANVAS_GRADIENT_CONSTRUCTOR_NAME).As<Napi::Function>();
auto gradientValue = func.New({ Napi::Value::From(env, x0), Napi::Value::From(env, y0), Napi::Value::From(env, x1), Napi::Value::From(env, y1) });
CanvasGradient::Unwrap(gradientValue)->context = context;
Expand All @@ -98,8 +100,7 @@ namespace Babylon::Polyfills::Internal

Napi::Object CanvasGradient::CreateRadial(Napi::Env env, const std::shared_ptr<NVGcontext*>& context, float x0, float y0, float r0, float x1, float y1, float r1)
{
Napi::HandleScope scope{ env };

// See CreateLinear: no HandleScope here so the returned gradient handle is not freed on scope close.
auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CANVAS_GRADIENT_CONSTRUCTOR_NAME).As<Napi::Function>();
auto gradientValue = func.New({ Napi::Value::From(env, x0), Napi::Value::From(env, y0), Napi::Value::From(env, x1), Napi::Value::From(env, y1), Napi::Value::From(env, r0), Napi::Value::From(env, r1) });
CanvasGradient::Unwrap(gradientValue)->context = context;
Expand Down
4 changes: 3 additions & 1 deletion Polyfills/Canvas/Source/ImageData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ namespace Babylon::Polyfills::Internal

Napi::Value ImageData::CreateInstance(Napi::Env env, Context* context, uint32_t width, uint32_t height)
{
Napi::HandleScope scope{env};
// No Napi::HandleScope here: the object created by func.New() is returned to the caller.
// A plain HandleScope would free the handle on close, which under the reference-counted
// QuickJS Node-API port leaves the caller with a dangling value.
Napi::Function func = DefineClass(
env,
JS_IMAGEDATA_CONSTRUCTOR_NAME,
Expand Down