Monorepo - #1798
Conversation
Build time: monorepo vs masterSince this PR changes how the build is assembled, here is a timing comparison against Baseline is run Total CI wall time (sum of all 32 jobs)
Per phase
ReadingComparable, and net faster. Configure time roughly halves, which is what you would expect: Checkout costs about 4s more on Windows and nothing measurable elsewhere. That is far less than the tree growth (~1.85M lines) would suggest, because One watch item. macOS Not included above: the new test steps this PR adds are extra cost on top of the baseline — the Android emulator run is ~321s per Ubuntu job (×4 engines), and iOS gains a UnitTests build plus a simulator run. That is new coverage rather than a regression, and it is why the Android and iOS jobs are longer in the most recent runs. |
Babylon Native currently assembles itself at configure time:
FetchContentclones ~10 pinned repositories, including all of JsRuntimeHost. This PR vendors those sources into the tree and rewires CMake toadd_subdirectorythem, so a clone is a complete, buildable, reviewable snapshot.What moved in-tree
JsRuntimeHostCore/{Node-API,Node-API-JSI,Node-API-Extensions,Foundation,JsRuntime,AppRuntime,ScriptLoader}andPolyfills/{Console,Scheduling,XMLHttpRequest,Fetch,URL,AbortController,WebSocket,Blob,File,Performance,TextDecoder,TextEncoder}bgfx.cmake,glslang,SPIRV-Cross,base-n,ios-cmake,AndroidExtensions,UrlLibDependencies/CMakeExtensionsScripts/extensions.cmakeStill fetched on demand, because they're large and only needed for non-default configurations:
arcana.cpp,arcore-android-sdk,googletest, plus the engine-specificquickjs-ng,hermes, andasio/llhttp(V8 inspector).Unit tests merged.
Apps/UnitTestsand JsRuntimeHost'sTests/UnitTestsare now one project. The JsRuntimeHost suites are the base and run on every platform — including the iOS simulator and the Android emulator, which is new for Babylon Native. The Babylon Native graphics suites are layered on top viaBABYLON_NATIVE_UNIT_TESTS_GRAPHICS, on by default for Win32/macOS/Linux and unavailable elsewhere.CI. The existing
Android_Ubuntu_*jobs now also run the tests on an emulator, and the iOS job now builds and runs them on a simulator. No new jobs.No public API, behaviour or dependency version changes. Everything is pinned at the same commits it was fetched from.
Implementation notes
Vendoring
Each dependency was copied at exactly the commit it was pinned to on
master, then itsFetchContent_Declaredeleted andFetchContent_MakeAvailable_With_Message(X)replaced withadd_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/X):8cd142951018de07c89c23f726fb7dc9c83745996c5515826c428337b7cbc0a5a7cc6d12325ce72b284e4301e5a6b44b279635276588db7cdd942624a512817ddbcd879a3929aef7d1d762871bdf86352e85a8d43b89246c460112c9e5546ad54b6e87b47573e77c0b9b0e8a5fb63d96dbde212c921993b4631780e42886e5f12bfd1a5568c7395f1d657f434.5.0metal-cppwas previously fetched as its own repository and turned into a single header at configure time with itsMakeSingleHeader.py. It is not vendored: bgfx already ships a pre-generated single-header copy atbgfx/3rdparty/metal-cpp/metal.hppand compiles its implementation into its Metal renderer, soDependencies/CMakeLists.txtjust copies that header into the build tree asMetal/Metal.hppand exposes it through the header-onlymetal-cppINTERFACE target. This keeps declarations and implementation at the same metal-cpp version and removes the configure-time Python dependency.ios-cmakeis special: it is consumed as a toolchain file beforeproject(), so instead of a subdirectory it setsios-cmake_SOURCE_DIRdirectly and keeps feedingCMAKE_TOOLCHAIN_FILE.Scripts/extensions.cmake(ex-CMakeExtensions) isinclude()d from the root before anything else — it defineswarnings_as_errors,disable_warnings,enable_objc_arc,download_nuget,set_cpu_platform_arch,npmandFetchContent_MakeAvailable_With_Message.Pitfalls hit while doing this
These cost the most time; they're the non-obvious part of the work.
Core/andPolyfills/leaves everyJSRUNTIMEHOST_*option undefined, soif(JSRUNTIMEHOST_POLYFILL_FETCH) add_subdirectory(Fetch)silently does nothing. This is invisible:target_link_libraries(X PRIVATE Fetch)with noFetchtarget is not a CMake error — it's treated as a plain-lFetch— so the failure only surfaces as a missing header hundreds of lines into the build. AllJSRUNTIMEHOST_*options, plusNAPI_BUILD_ABIand the engine-dependency block, were ported into the root.${CMAKE_SOURCE_DIR}is unusable here. Android adds Babylon Native as a subdirectory of a Gradle project, andInstall/Testis its own source root. Every reference to${CMAKE_SOURCE_DIR}/Scripts/extensions.cmakehad to become${CMAKE_CURRENT_LIST_DIR}-relative. LikewisePROJECT_IS_TOP_LEVELevaluated before a sub-project's ownproject()call reflects the enclosing project; useCMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR.NAPI_JAVASCRIPT_ENGINEmust be resolved in the root, beforeadd_subdirectory(Dependencies). The engine choice determines which dependencies are needed (QuickJS, Hermes, asio+llhttp). The per-platform defaults are duplicated inCore/Node-API/CMakeLists.txtso that directory still configures standalone;set(... CACHE ...)makes the second one a no-op.<name>_SOURCE_DIRis directory-scoped. Upstream JsRuntimeHost fetched Hermes from its root so the variable propagated down. HereDependencies/fetches it butCore/Node-APIandCore/AppRuntime/V8Inspectorconsume it, sohermes_SOURCE_DIRandllhttp_SOURCE_DIRare re-published asCACHE INTERNAL.3rdparty/{metal-cpp,khronos,directx-headers}— invisible on Windows/D3D11, fatal on macOS/iOS. Restored from bgfx@28a384fd. (3rdparty/metal-cppis also what themetal-cpptarget now consumes, so this restoration is load-bearing on Apple.)HERMES_UNICODE_LITE=ON(Hermes only skips its ICU lookup forAPPLE OR EMSCRIPTEN OR HERMES_IS_ANDROID OR HERMES_UNICODE_LITE, andHERMES_IS_ANDROIDbelongs to Hermes' React Native packaging), and a host-compiler bootstrap:hermesc/shermesare host tools run at build time, so when cross-compiling the in-tree targets are built for the wrong architecture.Dependencies/CMakeLists.txtconfigures and builds them intoBuild/HermesHostToolsat configure time and passesIMPORT_HOST_COMPILERS. AlsoHERMES_ENABLE_TOOLSmust stayON(Hermes unconditionally addsexternal/node-api-ctswhenHERMES_ENABLE_NAPIis on, and those reference$<TARGET_FILE:hermes>), andBOOST_CONTEXT_IMPLEMENTATION=winfibon Windows (the vendored assembler backend breaks under MSBuild).cmake -E envno longer resolves a relative program name againstWORKING_DIRECTORY— onlyPATHis searched.Apps/HeadlessScreenshotAppwas relying on this for DirectXTK'sfxc.exe. It now populates DirectXTK without configuring (SOURCE_SUBDIR DoNotConfigure), compiles the shaders at configure time with an explicitly locatedfxc.exe, then adds the subdirectory withUSE_PREBUILT_SHADERS.Unit test merge
Apps/UnitTestsbuilds on every platform except Android and visionOS. Layout:Source/Tests.JsRuntime.cpp(JsRuntimeHost'sTests/UnitTests/Shared/Shared.cpp),JavaScript/{src,dist}/tests.jsRuntime.*(itsScripts/tests.ts),Assets/{sample.json,Halo_Believe.ply},Scripts/symlink_target.js, and the polyfill/AppRuntime/gtest link set.BABYLON_NATIVE_UNIT_TESTS_GRAPHICS: the Babylon Native suites, the graphics libraries and the babylon.js assets. DefaultONfor(WIN32 AND NOT WINDOWS_STORE) OR (APPLE AND NOT IOS AND NOT VISIONOS) OR (UNIX AND NOT ANDROID AND NOT APPLE), elseOFF.Three edits were needed to the imported test source:
TEST(JavaScript, All)→TEST(JsRuntime, All)(Babylon Native already registersJavaScript.All), the script path →app:///Assets/tests.jsRuntime.js, andint RunTests()dropped.Hosts: graphics-on uses the existing
Source/App.cpp+App.{Win32.cpp,Apple.mm,X11.cpp}, which create a window and aGraphics::Configuration. Graphics-off uses a singleSource/App.JsRuntime.cppthat just callsInitGoogleTest/RUN_ALL_TESTS(with#ifdefs for the trace sink and iOS's exit-code-on-stderr). Android has nomain()—Android/app/src/main/cpp/JNI.cppdoes it.Two real bugs surfaced:
ARCANA_TEST_HOOKSmust be a global compile definition, not a target one.AppRuntime.DestroyDoesNotDeadlockdrives arcana'sblocking_concurrent_queueto reproduce a shutdown race. That queue is header-only and instantiated insideCore/AppRuntime's translation unit, so defining the hook only on the test target produces a queue that never calls it and the test hangs forever.add_compile_definitions(ARCANA_TEST_HOOKS)now sits in the root underBABYLON_NATIVE_BUILD_APPS OR JSRUNTIMEHOST_TESTS. The Android target needs its owntarget_compile_definitionsas well, becauseadd_compile_definitionsin Babylon Native's root scope doesn't propagate up to the Gradle project'scpp/directory. (Related artifact: when this test timed out it detached its thread, which then crashed the next test,NodeApi.CreateDataViewRejectsOverflowingRange, with0xC0000005. Both pass in isolation — don't chase the second failure.)AndroidExtensions/StdoutLogger::Stop()double-closed the pipe read ends already owned by the reader thread'sfdopen'dFILE*, so fdsan aborted the process after every test had passed. It now closes only the write ends;getlinethen returns −1 and the thread's ownfclosereleases the read end. NDK 28 enforces fdsan more strictly than the NDK 23 upstream uses, which is why JsRuntimeHost never saw it.Other details worth knowing:
googletestis fetched inDependencies/CMakeLists.txtfor all non-Android, non-visionOS platforms, and separately byApps/UnitTests/Android/app/src/main/cpp/CMakeLists.txtfor the Gradle build. Theadd_custom_command(OUTPUT … MAIN_DEPENDENCY <src>)asset-copy idiom only works if<src>is also listed inadd_executable. The symlink-resolution tests needsymlink_1.js→symlink_target.jsandsymlink_2.js→symlink_1.js, which can't be committed, so they're created as aPOST_BUILDstep; the JS side skips them unlesshostPlatformismacOS/Unix/Win32.CI
build-android.yml: after the existing Playground build, the fourAndroid_Ubuntu_*jobs now enable KVM and runconnectedAndroidTeston an api-33 / x86_64 /google_apisemulator with-no-window -gpu swiftshader_indirect. Guarded byrunner.os == 'Linux', so the threeAndroid_MacOS_*jobs are unaffected. Timeout 90 min on ubuntu. Hermes additionally frees disk space first — the host-compiler bootstrap plus NDK output plus an emulator image exhausts the default runner disk.build-ios.yml: newsimulatorinput; builds theUnitTestsscheme foriphonesimulatorand runs it viasimctl install+simctl launch --console, recovering the exit code from stderr (simctldoesn't propagate it).cmake --buildalready builds every target, soUnitTestsgets compiled (not run), matching JsRuntimeHost.Verified
Win32 desktop graphics-on: 23 tests / 11 suites pass. Win32 graphics-off and UWP x64: 6 tests / 4 suites pass. Android emulator (api-33 x86_64 swiftshader, V8): 6 suites pass. Playground validation tests pass on Windows/D3D11.
Not done
Babylon Native graphics tests on the Android emulator. Feasible — no Activity is required, since
WindowTis anANativeWindow*obtainable fromImageReader.getSurface(), and headless emulators fall back toswiftshader_indirectwhich provides GLES 3.0 — but slow, and MSAA/external-texture/device-loss cases would likely be flaky.