From 5d020b985f4854e36b187af158583e0406f99842 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 15:17:59 -0700 Subject: [PATCH 01/71] Add integrations project --- CMakeLists.txt | 9 + Integrations/Android/CMakeLists.txt | 34 + .../main/cpp/BabylonNativeIntegrations.cpp | 283 ++++ Integrations/Apple/CMakeLists.txt | 44 + Integrations/Apple/Source/BNRuntime.mm | 62 + Integrations/Apple/Source/BNRuntimeInternal.h | 17 + Integrations/Apple/Source/BNView.mm | 116 ++ .../BabylonNativeIntegrations/BNRuntime.h | 47 + .../BabylonNativeIntegrations/BNView.h | 57 + .../BabylonNativeIntegrations.h | 7 + Integrations/CMakeLists.txt | 118 ++ .../Include/Babylon/Integrations/LogLevel.h | 15 + .../Include/Babylon/Integrations/Runtime.h | 98 ++ .../Babylon/Integrations/RuntimeOptions.h | 34 + .../Include/Babylon/Integrations/View.h | 102 ++ .../Babylon/Integrations/ViewDescriptor.h | 30 + Integrations/Source/Runtime.cpp | 210 +++ Integrations/Source/RuntimeImpl.h | 86 + Integrations/Source/View.cpp | 329 ++++ SimplifiedAPI.md | 1381 +++++++++++++++++ 20 files changed, 3079 insertions(+) create mode 100644 Integrations/Android/CMakeLists.txt create mode 100644 Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp create mode 100644 Integrations/Apple/CMakeLists.txt create mode 100644 Integrations/Apple/Source/BNRuntime.mm create mode 100644 Integrations/Apple/Source/BNRuntimeInternal.h create mode 100644 Integrations/Apple/Source/BNView.mm create mode 100644 Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h create mode 100644 Integrations/Apple/include/BabylonNativeIntegrations/BNView.h create mode 100644 Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h create mode 100644 Integrations/CMakeLists.txt create mode 100644 Integrations/Include/Babylon/Integrations/LogLevel.h create mode 100644 Integrations/Include/Babylon/Integrations/Runtime.h create mode 100644 Integrations/Include/Babylon/Integrations/RuntimeOptions.h create mode 100644 Integrations/Include/Babylon/Integrations/View.h create mode 100644 Integrations/Include/Babylon/Integrations/ViewDescriptor.h create mode 100644 Integrations/Source/Runtime.cpp create mode 100644 Integrations/Source/RuntimeImpl.h create mode 100644 Integrations/Source/View.cpp create mode 100644 SimplifiedAPI.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 526a3b93c..d194ca4f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,11 @@ option(BABYLON_NATIVE_PLUGIN_TESTUTILS "Include Babylon Native Plugin TestUtils. option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) option(BABYLON_NATIVE_POLYFILL_CANVAS "Include Babylon Native Polyfill Canvas." ON) +# Integrations +option(BABYLON_NATIVE_INTEGRATIONS "Build the cross-platform Babylon::Integrations facade (Runtime + View)." ON) +option(BABYLON_NATIVE_INTEGRATIONS_ANDROID "Build the Android JNI interop layer for Babylon::Integrations." OFF) +option(BABYLON_NATIVE_INTEGRATIONS_APPLE "Build the Apple (iOS / macOS / visionOS) Obj-C++ interop layer for Babylon::Integrations." OFF) + # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) @@ -300,6 +305,10 @@ add_subdirectory(Core) add_subdirectory(Plugins) add_subdirectory(Polyfills) +if(BABYLON_NATIVE_INTEGRATIONS) + add_subdirectory(Integrations) +endif() + if(BABYLON_NATIVE_BUILD_APPS) add_subdirectory(Apps) endif() diff --git a/Integrations/Android/CMakeLists.txt b/Integrations/Android/CMakeLists.txt new file mode 100644 index 000000000..d76f3a59c --- /dev/null +++ b/Integrations/Android/CMakeLists.txt @@ -0,0 +1,34 @@ +# Babylon::Integrations Android interop layer. +# +# Builds a shared library (`libBabylonNativeIntegrations.so`) containing +# the JNI entry points declared in `BabylonNative.kt`. The host's +# Android Studio / Gradle project consumes this CMakeLists via its +# `externalNativeBuild { cmake { path "..." } }` hookup, alongside +# adding `src/main/java/` to its `sourceSets`. +# +# Gated by BABYLON_NATIVE_INTEGRATIONS_ANDROID at the root. + +if(NOT ANDROID) + message(FATAL_ERROR + "Integrations/Android is Android-only. " + "Disable BABYLON_NATIVE_INTEGRATIONS_ANDROID for non-Android builds.") +endif() + +set(SOURCES + "src/main/cpp/BabylonNativeIntegrations.cpp") + +add_library(BabylonNativeIntegrations SHARED ${SOURCES}) + +warnings_as_errors(BabylonNativeIntegrations) + +target_link_libraries(BabylonNativeIntegrations + PRIVATE Integrations + PRIVATE AndroidExtensions + PRIVATE android # ANativeWindow_fromSurface + PRIVATE log + PRIVATE EGL # required by bgfx GL backend + PRIVATE GLESv3 + PRIVATE -lz) + +set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp new file mode 100644 index 000000000..5934bdb39 --- /dev/null +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -0,0 +1,283 @@ +// JNI interop for Babylon::Integrations on Android. +// +// This file is the C++ side of the Android interop layer. It exposes +// `extern "C" JNIEXPORT` entry points that any JVM-language host (Kotlin +// or Java) can call by declaring matching `external fun` / `native` +// methods. The shape of those declarations follows directly from the +// JNI signatures here. +// +// We deliberately do not ship a Kotlin or Java class library on top — +// per the plan's non-goals (SimplifiedAPI.md §2), the interop layer's +// job ends at "Kotlin/Java can call into native Babylon code"; designing +// idiomatic high-level wrappers in the host language is left to the +// consumer. +// +// Convention: opaque C++ pointers cross the JNI boundary as `jlong`. +// `unique_ptr::release()` transfers ownership to the JVM side; the +// matching `*Destroy` function calls `delete` on the raw pointer. +// +// Java class name on the other side is `com.babylonjs.integrations.BabylonNative` +// (matching the `Java_com_babylonjs_integrations_BabylonNative_*` symbol +// names below). Hosts can choose any class name they like — the Java +// signatures must match the C++ symbols byte-for-byte. + +#include +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +namespace +{ + using Babylon::Integrations::Runtime; + using Babylon::Integrations::View; + using Babylon::Integrations::ViewDescriptor; + + Runtime* AsRuntime(jlong handle) { return reinterpret_cast(handle); } + View* AsView(jlong handle) { return reinterpret_cast(handle); } + + // Convert a jstring to std::string. Returns empty string if `jstr` + // is null or UTF lookup fails. + std::string ToStdString(JNIEnv* env, jstring jstr) + { + if (jstr == nullptr) + { + return {}; + } + const char* utf = env->GetStringUTFChars(jstr, nullptr); + if (utf == nullptr) + { + return {}; + } + std::string result{utf}; + env->ReleaseStringUTFChars(jstr, utf); + return result; + } + + // Convert physical pixels (Android `View.onSizeChanged` units) to + // logical pixels (the C++ ViewDescriptor convention) using the screen + // density factor. See SimplifiedAPI.md §4.2 "Pixel units". + ViewDescriptor MakeViewDescriptor(ANativeWindow* window, jint physicalW, jint physicalH, jfloat density) + { + ViewDescriptor descriptor{}; + descriptor.nativeWindow = static_cast(window); + descriptor.width = static_cast(static_cast(physicalW) / density); + descriptor.height = static_cast(static_cast(physicalH) / density); + descriptor.devicePixelRatio = density; + return descriptor; + } +} + +extern "C" +{ + +// ===================================================================== +// Android-specific platform lifecycle (platform interop layer surface). +// +// These don't belong on the cross-platform Runtime/View API — they exist +// because Babylon Native plugins like NativeCamera require the host to +// register the JavaVM + current Activity via AndroidExtensions::Globals. +// See SimplifiedAPI.md §4.2 "Interop layer responsibilities". +// ===================================================================== + +// Call once at app startup, typically from `Application.onCreate`, +// before constructing any BabylonNativeRuntime. +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_androidGlobalInitialize( + JNIEnv* env, jclass, jobject context) +{ + JavaVM* javaVM{nullptr}; + if (env->GetJavaVM(&javaVM) != JNI_OK) + { + return; + } + android::global::Initialize(javaVM, context); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_androidGlobalSetCurrentActivity( + JNIEnv*, jclass, jobject activity) +{ + android::global::SetCurrentActivity(activity); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_androidGlobalPause(JNIEnv*, jclass) +{ + android::global::Pause(); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_androidGlobalResume(JNIEnv*, jclass) +{ + android::global::Resume(); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_androidGlobalRequestPermissionsResult( + JNIEnv* env, jclass, jint requestCode, jobjectArray permissions, jintArray grantResults) +{ + std::vector nativePermissions{}; + const jsize permissionCount = env->GetArrayLength(permissions); + nativePermissions.reserve(static_cast(permissionCount)); + for (jsize i = 0; i < permissionCount; ++i) + { + auto perm = static_cast(env->GetObjectArrayElement(permissions, i)); + nativePermissions.push_back(ToStdString(env, perm)); + env->DeleteLocalRef(perm); + } + + std::vector nativeGrantResults{}; + const jsize grantCount = env->GetArrayLength(grantResults); + if (grantCount > 0) + { + jint* grantElements = env->GetIntArrayElements(grantResults, nullptr); + nativeGrantResults.assign(grantElements, grantElements + grantCount); + env->ReleaseIntArrayElements(grantResults, grantElements, JNI_ABORT); + } + + android::global::RequestPermissionsResult( + static_cast(requestCode), + nativePermissions, + nativeGrantResults); +} + +// ===================================================================== +// Runtime +// ===================================================================== + +JNIEXPORT jlong JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeCreate(JNIEnv*, jclass) +{ + // unique_ptr::release() returns the raw pointer and gives up + // ownership *without* deleting; the JVM side now owns it via the + // returned jlong handle and must call runtimeDestroy() to free it. + return reinterpret_cast(Runtime::Create().release()); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong handle) +{ + delete AsRuntime(handle); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeLoadScript( + JNIEnv* env, jclass, jlong handle, jstring url) +{ + AsRuntime(handle)->LoadScript(ToStdString(env, url)); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeEval( + JNIEnv* env, jclass, jlong handle, jstring source, jstring sourceUrl) +{ + AsRuntime(handle)->Eval(ToStdString(env, source), ToStdString(env, sourceUrl)); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeSuspend(JNIEnv*, jclass, jlong handle) +{ + AsRuntime(handle)->Suspend(); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeResume(JNIEnv*, jclass, jlong handle) +{ + AsRuntime(handle)->Resume(); +} + +JNIEXPORT jboolean JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeIsSuspended(JNIEnv*, jclass, jlong handle) +{ + return AsRuntime(handle)->IsSuspended() ? JNI_TRUE : JNI_FALSE; +} + +// ===================================================================== +// View +// ===================================================================== + +JNIEXPORT jlong JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewAttach( + JNIEnv* env, jclass, jlong runtimeHandle, jobject surface, + jint physicalW, jint physicalH, jfloat density) +{ + if (surface == nullptr) + { + return 0; + } + ANativeWindow* window = ANativeWindow_fromSurface(env, surface); + if (window == nullptr) + { + return 0; + } + auto view = View::Attach(*AsRuntime(runtimeHandle), + MakeViewDescriptor(window, physicalW, physicalH, density)); + if (!view) + { + ANativeWindow_release(window); + return 0; + } + // The View's Device::UpdateWindow has acquired its own reference + // on the ANativeWindow internally (bgfx retains it for the surface + // binding lifetime). We can release our local acquire here. + ANativeWindow_release(window); + return reinterpret_cast(view.release()); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewDetach(JNIEnv*, jclass, jlong handle) +{ + delete AsView(handle); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewRenderFrame(JNIEnv*, jclass, jlong handle) +{ + AsView(handle)->RenderFrame(); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewResize( + JNIEnv*, jclass, jlong handle, jint physicalW, jint physicalH, jfloat density) +{ + AsView(handle)->Resize( + static_cast(static_cast(physicalW) / density), + static_cast(static_cast(physicalH) / density), + density); +} + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) +{ + AsView(handle)->OnPointerDown(static_cast(pointerId), x, y); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) +{ + AsView(handle)->OnPointerMove(static_cast(pointerId), x, y); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_viewPointerUp( + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) +{ + AsView(handle)->OnPointerUp(static_cast(pointerId), x, y); +} + +#endif + +} // extern "C" diff --git a/Integrations/Apple/CMakeLists.txt b/Integrations/Apple/CMakeLists.txt new file mode 100644 index 000000000..90c4cab80 --- /dev/null +++ b/Integrations/Apple/CMakeLists.txt @@ -0,0 +1,44 @@ +# Babylon::Integrations Apple interop layer. +# +# Builds a static library producing Obj-C `BNRuntime` and `BNView` +# classes, importable from Swift via the standard Obj-C bridge. +# The host's Xcode project (or downstream CMake project) consumes +# this directly. +# +# Gated by BABYLON_NATIVE_INTEGRATIONS_APPLE at the root. + +if(NOT APPLE) + message(FATAL_ERROR + "Integrations/Apple is for iOS / macOS / visionOS. " + "Disable BABYLON_NATIVE_INTEGRATIONS_APPLE for non-Apple builds.") +endif() + +set(SOURCES + "include/BabylonNativeIntegrations/BabylonNativeIntegrations.h" + "include/BabylonNativeIntegrations/BNRuntime.h" + "include/BabylonNativeIntegrations/BNView.h" + "Source/BNRuntime.mm" + "Source/BNRuntimeInternal.h" + "Source/BNView.mm") + +add_library(BabylonNativeIntegrations STATIC ${SOURCES}) + +warnings_as_errors(BabylonNativeIntegrations) + +target_include_directories(BabylonNativeIntegrations + PUBLIC "include" + PRIVATE "Source") + +target_link_libraries(BabylonNativeIntegrations + PRIVATE Integrations + PRIVATE "-framework Foundation" + PRIVATE "-framework QuartzCore") + +# Enable ARC for the Obj-C++ files. +set_source_files_properties( + "Source/BNRuntime.mm" + "Source/BNView.mm" + PROPERTIES COMPILE_FLAGS "-fobjc-arc") + +set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm new file mode 100644 index 000000000..9a4a68e14 --- /dev/null +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -0,0 +1,62 @@ +// BNRuntime.mm — Obj-C++ implementation bridging BNRuntime to +// Babylon::Integrations::Runtime. + +#import "BNRuntimeInternal.h" + +#include + +@implementation BNRuntime +{ + std::unique_ptr _runtime; +} + +- (instancetype)init +{ + if ((self = [super init])) + { + _runtime = Babylon::Integrations::Runtime::Create(); + } + return self; +} + +- (void)loadScript:(NSString*)url +{ + if (url == nil) + { + return; + } + _runtime->LoadScript(url.UTF8String); +} + +- (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL +{ + if (source == nil) + { + return; + } + const char* src = source.UTF8String; + const char* url = sourceURL ? sourceURL.UTF8String : ""; + _runtime->Eval(src, url); +} + +- (void)suspend +{ + _runtime->Suspend(); +} + +- (void)resume +{ + _runtime->Resume(); +} + +- (BOOL)isSuspended +{ + return _runtime->IsSuspended() ? YES : NO; +} + +- (Babylon::Integrations::Runtime*)nativeRuntime +{ + return _runtime.get(); +} + +@end diff --git a/Integrations/Apple/Source/BNRuntimeInternal.h b/Integrations/Apple/Source/BNRuntimeInternal.h new file mode 100644 index 000000000..576d8278f --- /dev/null +++ b/Integrations/Apple/Source/BNRuntimeInternal.h @@ -0,0 +1,17 @@ +// Internal Obj-C category exposing the underlying +// `Babylon::Integrations::Runtime*` to BNView.mm. Not part of the +// public Apple interop layer surface (Swift consumers don't see this). + +#pragma once + +#import + +#include + +NS_ASSUME_NONNULL_BEGIN + +@interface BNRuntime () +- (Babylon::Integrations::Runtime*)nativeRuntime; +@end + +NS_ASSUME_NONNULL_END diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm new file mode 100644 index 000000000..fdf02b452 --- /dev/null +++ b/Integrations/Apple/Source/BNView.mm @@ -0,0 +1,116 @@ +// BNView.mm — Obj-C++ implementation bridging BNView to +// Babylon::Integrations::View. + +#import "BNRuntimeInternal.h" +#import + +#import + +#include +#include + +#include +#include + +namespace +{ + // Read physical-pixel dimensions + DPR from a CAMetalLayer and + // convert to the C++ logical-pixel convention used by ViewDescriptor. + Babylon::Integrations::ViewDescriptor MakeViewDescriptor(CAMetalLayer* layer) + { + const CGFloat scale = layer.contentsScale > 0 ? layer.contentsScale : 1.0; + Babylon::Integrations::ViewDescriptor descriptor{}; + descriptor.nativeWindow = (__bridge void*)layer; + descriptor.width = static_cast(layer.drawableSize.width / scale); + descriptor.height = static_cast(layer.drawableSize.height / scale); + descriptor.devicePixelRatio = static_cast(scale); + return descriptor; + } +} + +@implementation BNView +{ + std::unique_ptr _view; +} + +- (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer +{ + if (runtime == nil || layer == nil) + { + return nil; + } + if ((self = [super init])) + { + // First attach on this runtime triggers GPU device construction + // + plugin initialization + queued-script flush. + _view = Babylon::Integrations::View::Attach(*runtime.nativeRuntime, MakeViewDescriptor(layer)); + if (!_view) + { + return nil; + } + } + return self; +} + +- (void)renderFrame +{ + if (_view) + { + _view->RenderFrame(); + } +} + +- (void)resizeForLayer:(CAMetalLayer*)layer +{ + if (!_view || layer == nil) + { + return; + } + const auto descriptor = MakeViewDescriptor(layer); + _view->Resize(descriptor.width, descriptor.height, descriptor.devicePixelRatio); +} + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + +- (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y +{ + if (_view) + { + _view->OnPointerDown(static_cast(pointerId), + static_cast(x), + static_cast(y)); + } +} + +- (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y +{ + if (_view) + { + _view->OnPointerMove(static_cast(pointerId), + static_cast(x), + static_cast(y)); + } +} + +- (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y +{ + if (_view) + { + _view->OnPointerUp(static_cast(pointerId), + static_cast(x), + static_cast(y)); + } +} + +#else + +// When NATIVEINPUT is disabled at native build time, the methods are +// still declared on the public BNView header for binary stability; +// they become no-ops here. +- (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { (void)pointerId; (void)x; (void)y; } +- (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { (void)pointerId; (void)x; (void)y; } +- (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { (void)pointerId; (void)x; (void)y; } + +#endif + +@end diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h new file mode 100644 index 000000000..a5976eb33 --- /dev/null +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h @@ -0,0 +1,47 @@ +// BNRuntime.h — public Obj-C interface for the Babylon::Integrations +// runtime on Apple platforms (iOS, macOS, visionOS). +// +// Swift consumers see this through the auto-generated Swift bridge +// (BNRuntime is exposed to Swift as `BNRuntime`). +// +// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. + +#pragma once + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BNRuntime : NSObject + +/// Constructs the runtime: starts the JS engine + thread, sets up +/// non-GPU polyfills and plugins. Cheap and synchronous; no GPU +/// device is created yet (that happens on the first `BNView` attach). +- (instancetype)init; + +/// Load a script from a URL onto the JS thread. Calls made before +/// the first `BNView` is created are queued internally and dispatched +/// after engine initialization completes during that first attach. +/// Calls after the first attach are dispatched immediately. +- (void)loadScript:(NSString*)url; + +/// Evaluate JavaScript source on the JS thread. Same queueing +/// semantics as `loadScript`. +- (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL; + +/// Reference-counted suspend. While suspended, JS timers pause and +/// any attached `BNView` becomes a no-op for `renderFrame` (the host +/// can keep calling it from its draw callback unconditionally; +/// nothing happens until `resume`). +- (void)suspend; + +/// Decrement the suspend count; resume the JS thread when the count +/// reaches zero. +- (void)resume; + +/// Whether the runtime is currently suspended. +@property (nonatomic, readonly, getter=isSuspended) BOOL suspended; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h new file mode 100644 index 000000000..3ae012da7 --- /dev/null +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h @@ -0,0 +1,57 @@ +// BNView.h — public Obj-C interface for the Babylon::Integrations +// view on Apple platforms. +// +// Construct against a host-provided `CAMetalLayer` (typically +// `MTKView.layer`). The first `BNView` constructed against a given +// `BNRuntime` triggers GPU device construction, plugin initialization, +// and queued-script flushing. Subsequent views attached to the same +// runtime are cheap surface rebinds. +// +// Width/height handling: this interop layer reads the layer's +// `drawableSize` (physical pixels) and `contentsScale` (DPR) directly, +// converting to the C++ logical-pixel convention internally. The +// Swift host does no unit math. +// +// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. + +#pragma once + +#import +#import + +@class CAMetalLayer; +@class BNRuntime; + +NS_ASSUME_NONNULL_BEGIN + +@interface BNView : NSObject + +/// Attach to `runtime` rendering against `layer` (the host's +/// user-visible Metal layer). On the first attach for a given +/// runtime, this triggers GPU device construction and engine +/// initialization. Subsequent attaches just rebind the surface. +- (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer; + +/// Render exactly one frame. Call from the host's draw callback +/// (e.g. `MTKViewDelegate.draw(in:)`). No-op if the runtime is +/// suspended. +- (void)renderFrame; + +/// Re-read drawableSize / contentsScale from the given layer and +/// apply as a resize. Call from +/// `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` or any other +/// resize hook. +- (void)resizeForLayer:(CAMetalLayer*)layer; + +/// Forward a pointer-down event. `x`, `y` are in logical pixels. +/// Only present when `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` is enabled +/// at native build time. +- (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; + +- (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; + +- (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h b/Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h new file mode 100644 index 000000000..6a8850d76 --- /dev/null +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h @@ -0,0 +1,7 @@ +// Umbrella header for the Babylon::Integrations Apple interop layer. +// Import this from Swift via the bridging header (or from Obj-C). + +#pragma once + +#import +#import diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt new file mode 100644 index 000000000..b926f0f6d --- /dev/null +++ b/Integrations/CMakeLists.txt @@ -0,0 +1,118 @@ +set(SOURCES + "Include/Babylon/Integrations/LogLevel.h" + "Include/Babylon/Integrations/Runtime.h" + "Include/Babylon/Integrations/RuntimeOptions.h" + "Include/Babylon/Integrations/View.h" + "Include/Babylon/Integrations/ViewDescriptor.h" + "Source/Runtime.cpp" + "Source/RuntimeImpl.h" + "Source/View.cpp") + +add_library(Integrations ${SOURCES}) + +warnings_as_errors(Integrations) + +target_include_directories(Integrations PUBLIC "Include") + +# Always-on dependencies. The Integrations layer formalizes the canonical +# Babylon Native setup, so the polyfills that AppContext.cpp historically +# initializes unconditionally (Blob / Console / Performance / TextDecoder / +# XMLHttpRequest) are always linked. +target_link_libraries(Integrations + PUBLIC napi + PRIVATE AppRuntime + PRIVATE ScriptLoader + PRIVATE GraphicsDevice + PRIVATE Blob + PRIVATE Console + PRIVATE Performance + PRIVATE TextDecoder + PRIVATE XMLHttpRequest) + +# ----- Conditionally-included polyfills ----- +# +# Each flag is exposed as a PUBLIC compile definition so the public +# headers (Runtime.h / View.h) and the impl source files can both gate +# on the same value via `#if BABYLON_NATIVE_*`. + +if(BABYLON_NATIVE_POLYFILL_WINDOW) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_WINDOW=1) + target_link_libraries(Integrations PRIVATE Window) +endif() + +if(BABYLON_NATIVE_POLYFILL_CANVAS) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_CANVAS=1) + target_link_libraries(Integrations PRIVATE Canvas) +endif() + +# ----- Conditionally-included plugins ----- + +if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEENGINE=1) + target_link_libraries(Integrations PRIVATE NativeEngine) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEINPUT=1) + target_link_libraries(Integrations PRIVATE NativeInput) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVECAMERA) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVECAMERA=1) + target_link_libraries(Integrations PRIVATE NativeCamera) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVECAPTURE=1) + target_link_libraries(Integrations PRIVATE NativeCapture) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEENCODING=1) + target_link_libraries(Integrations PRIVATE NativeEncoding) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS=1) + target_link_libraries(Integrations PRIVATE NativeOptimizations) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVETRACING) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVETRACING=1) + target_link_libraries(Integrations PRIVATE NativeTracing) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEXR) + # Currently exposes no public API surface in the Integrations layer + # (XR-specific helpers are a future addition — see SimplifiedAPI.md + # §7 Risks). The flag is still propagated as a compile definition so + # future XR-gated methods on Runtime/View are picked up consistently. + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEXR=1) +endif() + +if(BABYLON_NATIVE_PLUGIN_SHADERCACHE) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_SHADERCACHE=1) + target_link_libraries(Integrations PRIVATE ShaderCache) +endif() + +if(BABYLON_NATIVE_PLUGIN_TESTUTILS) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_TESTUTILS=1) + target_link_libraries(Integrations PRIVATE TestUtils) +endif() + +set_property(TARGET Integrations PROPERTY FOLDER Integrations) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) + +# ----- Per-platform interop layers ----- +# +# Each platform's interop layer lives in its own subdirectory and is +# opt-in via its own CMake flag. They depend on the cross-platform +# `Integrations` target above. + +if(BABYLON_NATIVE_INTEGRATIONS_ANDROID) + add_subdirectory(Android) +endif() + +if(BABYLON_NATIVE_INTEGRATIONS_APPLE) + add_subdirectory(Apple) +endif() diff --git a/Integrations/Include/Babylon/Integrations/LogLevel.h b/Integrations/Include/Babylon/Integrations/LogLevel.h new file mode 100644 index 000000000..a6f28cf39 --- /dev/null +++ b/Integrations/Include/Babylon/Integrations/LogLevel.h @@ -0,0 +1,15 @@ +#pragma once + +namespace Babylon::Integrations +{ + // Severity levels for the optional log callback on RuntimeOptions. + // Mirrors the levels used by `Babylon::Polyfills::Console::LogLevel` + // but is exposed as its own enum so consumers don't have to depend + // on the Console polyfill header just to read log output. + enum class LogLevel + { + Log, + Warn, + Error, + }; +} diff --git a/Integrations/Include/Babylon/Integrations/Runtime.h b/Integrations/Include/Babylon/Integrations/Runtime.h new file mode 100644 index 000000000..c259c8d98 --- /dev/null +++ b/Integrations/Include/Babylon/Integrations/Runtime.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace Babylon::Integrations +{ + class View; + struct RuntimeImpl; + + // Long-lived: typically created once per app/process. Sets up the + // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU + // polyfills/plugins. Construction is cheap and synchronous — + // no GPU device exists yet. Device construction and GPU plugin + // initialization (NativeEngine, etc.) are deferred to the first + // `View::Attach` call. + // + // See SimplifiedAPI.md §4.1 for the full design. + class Runtime + { + public: + static std::unique_ptr Create(RuntimeOptions options = {}); + + // // Future construction mode — adopt a host-owned Babylon::JsRuntime + // // instead of letting Runtime construct its own AppRuntime+JsRuntime. + // // Intended for hosts that already own a JS engine and want + // // Babylon Native plugins to live inside it (e.g. React Native: + // // Hermes/JSC + CallInvoker dispatcher). The Integrations layer + // // never sees JSI directly — only Babylon::JsRuntime, which the + // // host wires up against whatever JS engine they have. + // // + // // In Attach mode `~Runtime` does NOT tear down the JS engine + // // (the host owns it); Suspend/Resume only DisableRendering on + // // the Device since the JS thread isn't ours to pause. Same + // // instance API as Create-mode otherwise. See SimplifiedAPI.md + // // §4.1 "Construction modes". + // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, + // RuntimeOptions options = {}); + + ~Runtime(); + + // Non-copyable, non-movable (Views hold raw pointers back to this). + Runtime(const Runtime&) = delete; + Runtime& operator=(const Runtime&) = delete; + Runtime(Runtime&&) = delete; + Runtime& operator=(Runtime&&) = delete; + + // ----- JS interaction ----- + // + // Calls made before the first `View::Attach` are queued internally + // and dispatched onto the JS thread after engine initialization + // completes during that first Attach. Calls made after the first + // Attach are dispatched immediately. + // + // Safe to call from any thread. + void LoadScript(std::string_view url); + void Eval(std::string_view source, std::string_view sourceUrl = {}); + + // Escape hatch: post `callback` onto the JS thread. The callback + // runs after any pending init has completed. Useful for installing + // custom Napi globals, registering ObjectWrap classes, capturing + // `Napi::FunctionReference`s for native→JS calls, etc. + // + // Safe to call from any thread. + void RunOnJsThread(std::function callback); + + // ----- Suspend / Resume ----- + // + // Orthogonal to view attachment. Use when the host app is + // backgrounded, throttled, or otherwise should not be doing work + // (iOS applicationWillResignActive, Android onPause, modal + // dialogs, power-saving mode). While suspended: + // - JS timers (setTimeout/setInterval) pause. + // - In-flight microtasks complete; no new tasks are dispatched. + // - Any attached View becomes a no-op for RenderFrame() — the + // host can keep calling it from its draw callback; nothing + // happens until Resume(). + // Calls are reference-counted; nesting is safe. + // + // Safe to call from any thread. + void Suspend(); + void Resume(); + bool IsSuspended() const; + + private: + friend class View; + + Runtime(); + + std::unique_ptr m_impl; + }; +} diff --git a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Babylon/Integrations/RuntimeOptions.h new file mode 100644 index 000000000..7b951a9f2 --- /dev/null +++ b/Integrations/Include/Babylon/Integrations/RuntimeOptions.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace Babylon::Integrations +{ + struct RuntimeOptions + { + // MSAA sample count for the back buffer. Valid values: 0, 2, 4, 8, 16. + // Anything else disables MSAA. + uint8_t msaaSamples{4}; + + // Enable the JavaScript debugger. Only implemented for V8 and Chakra. + bool enableDebugger{false}; + + // Block engine startup until a debugger has attached. Only + // implemented for V8. + bool waitForDebugger{false}; + + // Optional log sink. If unset, log output is discarded. Wired into + // both `Babylon::DebugTrace` and the Console polyfill. + std::function log; + + // Optional handler for unhandled JavaScript exceptions. If unset, + // `Babylon::AppRuntime::DefaultUnhandledExceptionHandler` is used + // (which writes the error to the program output). + std::function onUnhandledError; + }; +} diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h new file mode 100644 index 000000000..b696c12f3 --- /dev/null +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -0,0 +1,102 @@ +#pragma once + +#include + +#include +#include + +namespace Babylon::Integrations +{ + class Runtime; + struct ViewImpl; + + // Transient: created when a host surface appears, destroyed when + // it goes away. Multiple sequential Views may be attached to the + // same Runtime over its lifetime. **At most one View may be attached + // at a time** — to switch surfaces, destroy the current View and + // construct a new one. + // + // See SimplifiedAPI.md §4.1 for the full design. + class View + { + public: + // Attaches a surface described by `descriptor` to `runtime`. + // + // The first Attach on a given Runtime is the heavy step: it + // constructs `Babylon::Graphics::Device` against `descriptor`, + // dispatches GPU plugin initialization (`Device::AddToJavaScript`, + // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, + // ...), and flushes any scripts queued via `Runtime::LoadScript` + // before this point. Opens the first frame. + // + // Subsequent Attach calls on the same Runtime are cheap: the + // Device is already constructed, plugins are initialized, the + // JS engine is running. They just call `Device::UpdateWindow` + + // `Device::EnableRendering` to bind the new surface, then open + // the first frame for the new attachment. + // + // Detach (`~View`) closes the in-flight frame and calls + // `Device::DisableRendering`. The Device persists on the + // Runtime, so the next Attach is fast. + // + // Must be called from the same thread that will call + // `RenderFrame` and `Resize` (the "frame thread"). + static std::unique_ptr Attach(Runtime& runtime, const ViewDescriptor& descriptor); + + ~View(); + + View(const View&) = delete; + View& operator=(const View&) = delete; + View(View&&) = delete; + View& operator=(View&&) = delete; + + // Render exactly one frame. Must be called from the same thread + // as `Attach` and `Resize` (the frame thread). No-op if the + // runtime is suspended. The host calls this from the platform + // view/control's existing draw callback (WM_PAINT on Win32, + // MTKViewDelegate::draw(in:) on Apple, View.onDraw on Android, + // etc. — see SimplifiedAPI.md §4.1 "How frames actually get + // rendered"). + void RenderFrame(); + + // Resize the bound surface. `width` and `height` are in + // **logical pixels**; `devicePixelRatio` is the physical-to-logical + // ratio (e.g. 2.0 for a Retina display). + // + // The platform interop layer is responsible for converting + // whatever its UI framework provides into this convention + // (Android `View.onSizeChanged` is in physical pixels; iOS + // `MTKViewDelegate.drawableSizeWillChange:` is in physical + // pixels; UWP `SwapChainPanel.SizeChanged` is in logical pixels; + // etc.). See SimplifiedAPI.md §4.2 "Pixel units". + // + // Must be called from the frame thread. + void Resize(uint32_t width, uint32_t height, float devicePixelRatio = 1.0f); + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + // ----- Pointer input forwarding ----- + // + // Host calls these from its event loop while the view exists. + // Routed to the JS thread via `NativeInput::Touch*`. Coordinates + // are in logical pixels (same convention as Resize). + // + // Babylon Native's `NativeInput` only exposes pointer (touch / + // mouse-as-pointer) input today; keyboard input is not part of + // the public Babylon Native input contract and is therefore not + // exposed here. Hosts that need keyboard handling can do it at + // the platform level and forward into JS via Runtime::RunOnJsThread. + // + // Safe to call from any thread. + void OnPointerDown(int32_t pointerId, float x, float y); + void OnPointerMove(int32_t pointerId, float x, float y); + void OnPointerUp(int32_t pointerId, float x, float y); +#endif + + private: + friend class Runtime; + + std::unique_ptr m_impl; + + explicit View(std::unique_ptr impl); + }; +} diff --git a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h new file mode 100644 index 000000000..c4c77c8ff --- /dev/null +++ b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace Babylon::Integrations +{ + // Description of a platform surface that a `View` will render + // into. Populated by the platform interop layer (or directly by a + // C++ host on Win32 / Linux) from whatever native object the + // host's UI framework provides: + // + // Win32 : HWND + // Linux (X11) : Window (X11 XID; reinterpret_cast through void*) + // Android : ANativeWindow* + // iOS / macOS / visionOS : CAMetalLayer* + // UWP : IInspectable* (e.g. SwapChainPanel) + // + // `width` and `height` are in **logical pixels**; `devicePixelRatio` + // is the physical-to-logical ratio (e.g. 2.0 for a Retina display). + // The platform interop layer (Integrations/Android, Integrations/Apple, + // Integrations/Uwp) is responsible for converting whatever its UI + // framework hands the host into this convention. + struct ViewDescriptor + { + void* nativeWindow{nullptr}; + uint32_t width{0}; + uint32_t height{0}; + float devicePixelRatio{1.0f}; + }; +} diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp new file mode 100644 index 000000000..f8ea21be3 --- /dev/null +++ b/Integrations/Source/Runtime.cpp @@ -0,0 +1,210 @@ +#include "RuntimeImpl.h" + +#include + +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE +#include +#endif + +#include +#include +#include + +namespace Babylon::Integrations +{ + RuntimeImpl::RuntimeImpl(RuntimeOptions options) + : m_options{std::move(options)} + { + // Wire DebugTrace through to the host's log callback (if any). + // DebugTrace is process-wide so this affects any concurrent + // Runtime instances; that matches AppContext's behavior today. + if (m_options.log) + { + Babylon::DebugTrace::EnableDebugTrace(true); + // DebugTrace doesn't carry a level; treat it as Log. + const auto& logCallback = m_options.log; + Babylon::DebugTrace::SetTraceOutput([logCallback](const char* message) { + logCallback(LogLevel::Log, message ? message : ""); + }); + } + + // Construct AppRuntime. This starts the JS thread and creates a + // Napi::Env. Plugin Initialize() calls will be dispatched onto + // this thread by the first View::Attach. + Babylon::AppRuntime::Options appRuntimeOptions{}; + appRuntimeOptions.EnableDebugger = m_options.enableDebugger; + appRuntimeOptions.WaitForDebugger = m_options.waitForDebugger; + if (m_options.onUnhandledError) + { + const auto& userHandler = m_options.onUnhandledError; + appRuntimeOptions.UnhandledExceptionHandler = [userHandler](const Napi::Error& error) { + std::ostringstream ss{}; + ss << "[Uncaught Error] " << Napi::GetErrorString(error); + userHandler(ss.str()); + }; + } + + m_appRuntime.emplace(std::move(appRuntimeOptions)); + + // ScriptLoader serializes LoadScript / Eval / Dispatch onto the + // AppRuntime's JS thread. Its dispatcher captures a reference to + // m_appRuntime, so ~ScriptLoader must complete before ~AppRuntime. + m_scriptLoader.emplace(*m_appRuntime); + } + + RuntimeImpl::~RuntimeImpl() + { + // Precondition: no View is currently attached. The host owns the + // ordering: destroy Views before destroying their Runtime. + assert(m_currentView == nullptr && "View must be destroyed before its Runtime."); + + // Discard any pending pre-init actions; we're tearing down. + { + std::lock_guard lock{m_pendingMutex}; + m_pending.clear(); + } + + // Order matters here: + // 1. ScriptLoader's dispatcher captures &m_appRuntime, so + // ~ScriptLoader must run before ~AppRuntime. + // 2. The Canvas polyfill and NativeInput pointer are referenced + // from JS-thread state; clear them before joining the JS + // thread, but only after ScriptLoader has drained. + // 3. ~AppRuntime joins the JS thread. After it returns, no + // JS-thread task is running. If the first-Attach init + // lambda was queued but not yet run when ~Impl began, it + // will run during the AppRuntime drain (if AppRuntime + // drains its queue before joining); m_canvas / m_input + // etc. may then be re-populated and discarded when ~Impl + // itself destroys the optionals. + // 4. ShaderCache::Disable() balances the Enable() that + // View::Attach calls on first attach. + // 5. Device + DeviceUpdate destroyed last because the JS + // thread referenced them via Device::AddToJavaScript. + m_scriptLoader.reset(); + +#if BABYLON_NATIVE_POLYFILL_CANVAS + m_canvas.reset(); +#endif + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + m_input = nullptr; +#endif + + m_appRuntime.reset(); + +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + if (m_device) + { + Babylon::Plugins::ShaderCache::Disable(); + } +#endif + + m_deviceUpdate.reset(); + m_device.reset(); + } + + std::unique_ptr Runtime::Create(RuntimeOptions options) + { + // Private ctor + manual unique_ptr because make_unique can't see it. + std::unique_ptr runtime{new Runtime()}; + runtime->m_impl = std::make_unique(std::move(options)); + return runtime; + } + + Runtime::Runtime() = default; + Runtime::~Runtime() = default; + + void Runtime::LoadScript(std::string_view url) + { + std::string urlCopy{url}; + + std::lock_guard lock{m_impl->m_pendingMutex}; + if (!m_impl->m_initialized) + { + // Capture by value into a callable that the first-Attach + // init lambda will invoke after plugin init completes. + m_impl->m_pending.emplace_back( + [scriptLoader = &*m_impl->m_scriptLoader, url = std::move(urlCopy)]() mutable { + scriptLoader->LoadScript(std::move(url)); + }); + return; + } + + // Past init: dispatch directly. The scriptLoader serializes on + // the JS thread. + m_impl->m_scriptLoader->LoadScript(std::move(urlCopy)); + } + + void Runtime::Eval(std::string_view source, std::string_view sourceUrl) + { + std::string sourceCopy{source}; + std::string urlCopy{sourceUrl}; + + std::lock_guard lock{m_impl->m_pendingMutex}; + if (!m_impl->m_initialized) + { + m_impl->m_pending.emplace_back( + [scriptLoader = &*m_impl->m_scriptLoader, + src = std::move(sourceCopy), + url = std::move(urlCopy)]() mutable { + scriptLoader->Eval(std::move(src), std::move(url)); + }); + return; + } + + m_impl->m_scriptLoader->Eval(std::move(sourceCopy), std::move(urlCopy)); + } + + void Runtime::RunOnJsThread(std::function callback) + { + if (!callback) + { + return; + } + + std::lock_guard lock{m_impl->m_pendingMutex}; + if (!m_impl->m_initialized) + { + // Defer through ScriptLoader so it stays serialized with + // any LoadScript / Eval calls the host queued before us. + m_impl->m_pending.emplace_back( + [scriptLoader = &*m_impl->m_scriptLoader, + cb = std::move(callback)]() mutable { + scriptLoader->Dispatch(std::move(cb)); + }); + return; + } + + m_impl->m_scriptLoader->Dispatch(std::move(callback)); + } + + void Runtime::Suspend() + { + std::lock_guard lock{m_impl->m_suspendMutex}; + if (m_impl->m_suspendCount++ == 0) + { + m_impl->m_appRuntime->Suspend(); + } + } + + void Runtime::Resume() + { + std::lock_guard lock{m_impl->m_suspendMutex}; + if (m_impl->m_suspendCount == 0) + { + // Mismatched Resume; ignore rather than underflow the count. + return; + } + if (--m_impl->m_suspendCount == 0) + { + m_impl->m_appRuntime->Resume(); + } + } + + bool Runtime::IsSuspended() const + { + std::lock_guard lock{m_impl->m_suspendMutex}; + return m_impl->m_suspendCount > 0; + } +} diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h new file mode 100644 index 000000000..8fd698abc --- /dev/null +++ b/Integrations/Source/RuntimeImpl.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#if BABYLON_NATIVE_POLYFILL_CANVAS +#include +#endif + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT +#include +#endif + +#include +#include +#include +#include +#include + +namespace Babylon::Integrations +{ + // Internal implementation of Runtime. Lives in Source/ so View.cpp + // can reach into it without exposing internals on the public header. + struct RuntimeImpl + { + explicit RuntimeImpl(RuntimeOptions options); + ~RuntimeImpl(); + + RuntimeImpl(const RuntimeImpl&) = delete; + RuntimeImpl& operator=(const RuntimeImpl&) = delete; + + // Configuration captured at construction. + RuntimeOptions m_options; + + // Always-alive: JS engine + thread, plus the script loader that + // serializes LoadScript / Eval / Dispatch onto that thread. + std::optional m_appRuntime; + std::optional m_scriptLoader; + + // Lazily constructed during the first View::Attach. + std::optional m_device; + std::optional m_deviceUpdate; + +#if BABYLON_NATIVE_POLYFILL_CANVAS + std::optional m_canvas; +#endif + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + // Owned by the JS world (returned by NativeInput::CreateForJavaScript). + // We just keep a pointer for forwarding View::OnPointer* calls. + Babylon::Plugins::NativeInput* m_input{nullptr}; +#endif + + // ----- Pre-init queueing ------ + // + // Before the first View::Attach completes engine initialization + // on the JS thread, LoadScript / Eval / RunOnJsThread calls are + // recorded here and flushed (in submission order) inside the + // first-Attach init lambda after all plugin Initialize() calls. + // After flush, m_initialized is true and subsequent calls + // dispatch directly through ScriptLoader / AppRuntime. + bool m_initialized{false}; + std::vector> m_pending; + std::mutex m_pendingMutex; + + // Reference-counted Suspend/Resume. + int m_suspendCount{0}; + mutable std::mutex m_suspendMutex; + + // 0..1; tracked so we can guard against multiple concurrent + // attachments (the API contract is "at most one View at a time"). + View* m_currentView{nullptr}; + }; + + // Internal implementation of View. Holds the back-reference to the + // Runtime that produced it. + struct ViewImpl + { + explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} + Runtime& m_runtime; + }; +} diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp new file mode 100644 index 000000000..e8a58e82b --- /dev/null +++ b/Integrations/Source/View.cpp @@ -0,0 +1,329 @@ +#include "RuntimeImpl.h" + +#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAMERA +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAPTURE +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEENCODING +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVETRACING +#include +#endif +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE +#include +#endif +#if BABYLON_NATIVE_PLUGIN_TESTUTILS +#include +#endif + +#include +#include +#include +#include +#include + +#if BABYLON_NATIVE_POLYFILL_WINDOW +#include +#endif + +#include +#include + +namespace Babylon::Integrations +{ + namespace + { + // Forward Babylon Console levels to the LogLevel exposed on + // RuntimeOptions::log so consumers don't have to depend on the + // Console polyfill header to read log output. + LogLevel ToIntegrationsLogLevel(Babylon::Polyfills::Console::LogLevel level) + { + switch (level) + { + case Babylon::Polyfills::Console::LogLevel::Log: return LogLevel::Log; + case Babylon::Polyfills::Console::LogLevel::Warn: return LogLevel::Warn; + case Babylon::Polyfills::Console::LogLevel::Error: return LogLevel::Error; + } + return LogLevel::Log; + } + + // Reinterpret the platform-erased `void*` from ViewDescriptor as the + // platform's Babylon::Graphics::WindowT. WindowT varies by + // platform: + // + // Win32 : HWND (pointer) + // Android : ANativeWindow* (pointer) + // Apple : CA::MetalLayer* (pointer; metal-cpp wrapper) + // X11 (Linux) : Window (XID — `unsigned long`) + // UWP / WinRT : winrt::Windows::Foundation::IInspectable + // (a value type wrapping a refcounted COM pointer) + // + // For UWP we reconstruct the wrapper from the ABI pointer the + // host stuffed in (typically via `winrt::get_abi(...)`); for + // every other platform a single `reinterpret_cast` covers + // pointer-to-pointer and void*-to-XID. + Babylon::Graphics::WindowT ToWindowT(void* nativeWindow) + { +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) + Babylon::Graphics::WindowT result{nullptr}; + if (nativeWindow != nullptr) + { + winrt::copy_from_abi(result, nativeWindow); + } + return result; +#else + return reinterpret_cast(nativeWindow); +#endif + } + } + + // --------------------------------------------------------------------- + // First-Attach engine initialization: dispatched onto the JS thread by + // the first View::Attach call. Runs all plugin/polyfill Initialize() + // calls in the same order as Apps/Playground/Shared/AppContext.cpp, + // then flushes any LoadScript / Eval / RunOnJsThread calls the host + // queued before the first Attach. + // + // After this lambda returns, m_initialized is true and subsequent + // Runtime::LoadScript / Eval / RunOnJsThread calls dispatch directly. + // --------------------------------------------------------------------- + static void RunFirstAttachInit(RuntimeImpl& impl, Babylon::Graphics::WindowT window) + { + impl.m_appRuntime->Dispatch([implPtr = &impl, window](Napi::Env env) { + // 1. Make the Device available to JS. + implPtr->m_device->AddToJavaScript(env); + + // 2. Polyfills (always-on). + Babylon::Polyfills::Blob::Initialize(env); + + { + const auto userLog = implPtr->m_options.log; + Babylon::Polyfills::Console::Initialize(env, + [userLog](const char* message, Babylon::Polyfills::Console::LogLevel level) { + if (userLog && message) + { + userLog(ToIntegrationsLogLevel(level), message); + } + }); + } + + Babylon::Polyfills::Performance::Initialize(env); + +#if BABYLON_NATIVE_POLYFILL_WINDOW + Babylon::Polyfills::Window::Initialize(env); +#endif + + Babylon::Polyfills::TextDecoder::Initialize(env); + Babylon::Polyfills::XMLHttpRequest::Initialize(env); + +#if BABYLON_NATIVE_POLYFILL_CANVAS + implPtr->m_canvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); +#endif + + // 3. Plugins. +#if BABYLON_NATIVE_PLUGIN_NATIVETRACING + Babylon::Plugins::NativeTracing::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEENCODING + Babylon::Plugins::NativeEncoding::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE + Babylon::Plugins::NativeEngine::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS + Babylon::Plugins::NativeOptimizations::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAPTURE + Babylon::Plugins::NativeCapture::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAMERA + Babylon::Plugins::NativeCamera::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + implPtr->m_input = &Babylon::Plugins::NativeInput::CreateForJavaScript(env); +#endif +#if BABYLON_NATIVE_PLUGIN_TESTUTILS + Babylon::Plugins::TestUtils::Initialize(env, window); +#else + (void)window; +#endif + + // 4. Flush pending LoadScript / Eval / RunOnJsThread calls + // in submission order. Holding m_pendingMutex across the + // iteration ensures any concurrent host-thread call lands + // *after* the queued ones in the JS thread queue (the + // host call blocks on the mutex until the iteration + // finishes appending each pending action to the + // ScriptLoader's task chain). + std::lock_guard lock{implPtr->m_pendingMutex}; + for (auto& action : implPtr->m_pending) + { + action(); + } + implPtr->m_pending.clear(); + implPtr->m_initialized = true; + }); + } + + // --------------------------------------------------------------------- + // View::Attach (first time and subsequent) + // --------------------------------------------------------------------- + std::unique_ptr View::Attach(Runtime& runtime, const ViewDescriptor& descriptor) + { + RuntimeImpl& impl = *runtime.m_impl; + + assert(impl.m_currentView == nullptr && "Only one View may be attached at a time."); + if (impl.m_currentView != nullptr) + { + return nullptr; + } + + const auto window = ToWindowT(descriptor.nativeWindow); + + if (!impl.m_device) + { + // First Attach on this Runtime: construct the Device and + // open the first frame, then dispatch the engine-init lambda + // onto the JS thread. + Babylon::Graphics::Configuration config{}; + config.Window = window; + config.Width = descriptor.width; + config.Height = descriptor.height; + config.MSAASamples = impl.m_options.msaaSamples; + + impl.m_device.emplace(config); + impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); + +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + Babylon::Plugins::ShaderCache::Enable(); +#endif + + // Open the first frame *before* dispatching the init lambda + // so the Device::AddToJavaScript inside the lambda sees an + // open frame to record into. + impl.m_device->StartRenderingCurrentFrame(); + impl.m_deviceUpdate->Start(); + + RunFirstAttachInit(impl, window); + } + else + { + // Subsequent Attach: reuse the existing Device, just rebind + // the surface and re-enable rendering. Plugins, polyfills, + // and any loaded scripts are preserved on the JS side. + impl.m_device->UpdateWindow(window); + impl.m_device->UpdateSize(descriptor.width, descriptor.height); + impl.m_device->EnableRendering(); + impl.m_device->StartRenderingCurrentFrame(); + impl.m_deviceUpdate->Start(); + } + + std::unique_ptr view{new View{std::make_unique(runtime)}}; + impl.m_currentView = view.get(); + return view; + } + + View::View(std::unique_ptr impl) + : m_impl{std::move(impl)} + { + } + + View::~View() + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + + // Symmetric counterpart to Attach: close the in-flight frame and + // disable rendering. The Device persists on the Runtime so the + // next Attach is cheap. + if (impl.m_device) + { + impl.m_deviceUpdate->Finish(); + impl.m_device->FinishRenderingCurrentFrame(); + impl.m_device->DisableRendering(); + } + + impl.m_currentView = nullptr; + } + + void View::RenderFrame() + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + + // Cheap suspended check (own mutex). Skip the GPU work entirely + // while the runtime is suspended; the host can keep calling + // RenderFrame() from its draw callback unconditionally. + if (m_impl->m_runtime.IsSuspended()) + { + return; + } + + // Babylon's JS render loop (requestAnimationFrame / scene.render) + // runs between Start and Finish, scheduled via DeviceUpdate onto + // the JS thread. This call doesn't enter JS directly — + // DeviceUpdate handles the cross-thread coordination via + // SafeTimespanGuarantor. + impl.m_deviceUpdate->Finish(); + impl.m_device->FinishRenderingCurrentFrame(); + impl.m_device->StartRenderingCurrentFrame(); + impl.m_deviceUpdate->Start(); + } + + void View::Resize(uint32_t width, uint32_t height, float devicePixelRatio) + { + // devicePixelRatio is informational at the C++ layer for now — + // Device computes its own DPR internally via GetDevicePixelRatio(). + // The parameter is part of the API for ViewDescriptor parity and so + // future Device-level DPR plumbing has a place to land. + (void)devicePixelRatio; + + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_device) + { + impl.m_device->UpdateSize(width, height); + } + } + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + void View::OnPointerDown(int32_t pointerId, float x, float y) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->TouchDown(static_cast(pointerId), + static_cast(x), + static_cast(y)); + } + } + + void View::OnPointerMove(int32_t pointerId, float x, float y) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->TouchMove(static_cast(pointerId), + static_cast(x), + static_cast(y)); + } + } + + void View::OnPointerUp(int32_t pointerId, float x, float y) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->TouchUp(static_cast(pointerId), + static_cast(x), + static_cast(y)); + } + } +#endif +} diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md new file mode 100644 index 000000000..4ea9a31d7 --- /dev/null +++ b/SimplifiedAPI.md @@ -0,0 +1,1381 @@ +# Babylon Native — Simplified Integration API Plan + +## 1. Problem + +Integrating Babylon Native into a host app today requires understanding and +hand-wiring a large number of internal components. The canonical setup +(see `Apps/Playground/Shared/AppContext.cpp`) requires the consumer to: + +- Create and configure `Babylon::Graphics::Device` + `DeviceUpdate`, drive + `StartRenderingCurrentFrame` / `FinishRenderingCurrentFrame` from the right + thread. +- Create a `Babylon::AppRuntime`, then `Dispatch` a lambda onto its JS thread + to call **~10+** `Initialize` functions (`Console`, `Window`, + `XMLHttpRequest`, `Canvas`, `Performance`, `Blob`, `TextDecoder`, + `NativeEngine`, `NativeInput`, `NativeXr`, `NativeCamera`, + `NativeCapture`, `NativeEncoding`, `NativeOptimizations`, + `NativeTracing`, `ShaderCache`, `TestUtils`, …) in the correct order. +- Use `ScriptLoader` to load `babylon.max.js`, loaders, materials, GUI, + serializers, ammo, recast, etc., in a specific order. +- Plumb window/surface handles, resize events, and input events from each + platform's native windowing system into `Graphics::Configuration` and + `NativeInput` respectively. +- Repeat all of this in per-platform glue: `Apps/Playground/Win32/App.cpp`, + `Apps/Playground/iOS/LibNativeBridge.mm`, + `Apps/Playground/Android/BabylonNative/...`, `macOS/`, `X11/`, `UWP/`, + `visionOS/`. + +There is no "single-call" path for the common case: *show a Babylon scene +in this view*. Even trivial integrations require ~150 lines of C++. + +## 2. Goal + +Provide a small, stable, opinionated API that lets a host app embed Babylon +Native in a handful of calls, while keeping the existing low-level API +intact for advanced users. + +The deliverable is two layers shipped in the repo: + +1. **A shared C++ Integrations layer** (`Babylon::Integrations`) — a thin facade + over the existing components that exposes a `Runtime` + `View` C++ + API. +2. **Per-platform interop layers** that bridge each platform's native + inter-language ABI (JNI on Android, Objective-C(++) on Apple, plain + C++ on Windows / Linux) to the shared Integrations layer. They are built + in C++ but expose entry points that are **directly callable from the + platform's UI language without a generic FFI generator**, exactly + the way `Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp` + exposes JNI entry points to Kotlin today. + +Non-goals: + +- Replacing or deprecating the existing component-level API. +- Rewriting any rendering, scripting, or platform code. The simplified API + is a *facade* over current components. +- Changing the JavaScript-facing contract (Babylon.js code keeps working). +- **Shipping precompiled "everything" binaries.** Babylon Native stays + source-distributed via CMake. Hosts must be able to disable any plugin + they don't use (`NativeXr`, `NativeCamera`, `NativeCapture`, …) at + configure time so unused features add zero binary size. +- **No language-specific high-level wrappers.** We do not ship a Kotlin + class library, a Swift package with idiomatic Swift wrapper types, a + managed .NET assembly, or a Rust safe-wrapper crate. The per-platform + interop layer's job ends at "Kotlin/Swift/etc. can call into native + Babylon code"; designing idiomatic high-level wrappers in each host + language is left to the consumer. +- **No generic C ABI / FFI surface.** With per-platform interop layers, + each platform talks to the Integrations layer via its own native mechanism + (JNI, Objective-C runtime, direct C++). A separate flat C ABI would + duplicate the surface for no consumer. + +## 3. Design Principles + +1. **One library, two layers.** Keep `Babylon::*` (low level) untouched. + Add `Babylon::Integrations::*` as a thin C++ facade. +2. **Sensible defaults, escape hatches.** Default config matches the + Playground's setup. Every default is overridable. +3. **No N-API in the public surface of the Integrations layer.** If a + consumer wants JS interop, they get an opaque escape hatch + (`RunOnJsThread`) but they don't have to learn Napi to embed a scene. +4. **Per-platform interop layers, not a generic FFI.** Each platform + ships a tiny C++ interop module that uses the platform's native + inter-language ABI (JNI, Objective-C, COM/WinRT where appropriate) + to expose `Runtime` + `View` operations to the host UI language. The + shape of the entry points is adapted to the platform — JNI methods + on Android, Objective-C classes on Apple, plain C++ on Windows — so + the host writes the minimum amount of glue code in their UI + language. +5. **Two distinct lifetimes: Runtime vs. View.** The Babylon Native + runtime (JS engine, loaded scripts, scene state, GPU device) is + long-lived and typically scoped to the host *application* or + *process*. Views/surfaces are transient — they come and go as the + host navigates between screens, backgrounds/foregrounds the app on + mobile, or detaches a rendering surface (e.g., Android + `surfaceDestroyed`/`surfaceCreated`, iOS background/foreground, + Win32 window recreation). The API must let a single `Runtime` + outlive any number of `View` attachments without tearing down JS + state, reloading scripts, or losing the scene. This is already + supported by the underlying `Graphics::Device` via `UpdateWindow`, + `EnableRendering`/`DisableRendering`, and + `StartRenderingCurrentFrame`/`FinishRenderingCurrentFrame` + (`Core/Graphics/Include/Shared/Babylon/Graphics/Device.h:114-134`); + the simplified facade exposes that capability cleanly. +6. **Lifecycle is explicit and symmetric.** Runtime: `create → … → + destroy`. View: `attach → resize / input → detach`. Either side may + be repeated: one runtime, many sequential view attachments. +7. **Host owns the frame source.** The library does not subscribe to + any vsync / display-link / choreographer source. The host calls + `View::RenderFrame()` from whatever draw callback its UI framework + already provides for the view/control that hosts the rendering + surface. See §4.1 *How frames actually get rendered*. +8. **C++ OO at the shared layer; per-platform handle-ification at the + interop layers.** the Integrations layer is C++-object-oriented (`Runtime` + and `View` classes with RAII, `std::function`, `std::string_view`). + We do **not** flatten it to a C ABI: + - Every interop layer's host language speaks C++ natively (JNI + files compile as C++, Obj-C uses `.mm`, C++/WinRT is C++), so the + interop modules call C++ methods directly. + - Win32 / Linux hosts consume the C++ API directly with no interop + layer in between — RAII is the natural shape for those. + - A flat C ABI would only buy something if we had a generic FFI + consumer or shipped a precompiled binary, both of which are + explicit non-goals. + Each interop layer handles its own conversion to opaque handles + where the host language requires them (JNI uses `jlong`; Obj-C + stores the C++ object in an Obj-C instance ivar). +9. **Conditional API surface mirrors plugin flags.** When a plugin or + polyfill is disabled at configure time, the corresponding methods + are removed from the public header — not silently no-opped. See + §4.4. + +## 4. Proposed Public Surface + +### 4.1 Shared C++ facade — `Babylon::Integrations` + +The facade splits along the lifetime boundary: a long-lived `Runtime` +that owns JS state and the GPU device, and a transient `View` that +binds a platform surface to the runtime for as long as that surface +exists. + +```cpp +namespace Babylon::Integrations +{ + // Platform-surface handle. Populated by the platform interop layer + // from whatever native object the host's UI framework provides + // (HWND, ANativeWindow*, CAMetalLayer*, …). The interop layer is + // also responsible for any unit conversion (see "Pixel units" below). + struct ViewDescriptor { + void* nativeWindow; // HWND / ANativeWindow* / CAMetalLayer* / ... + uint32_t width; // logical pixels (see "Pixel units") + uint32_t height; // logical pixels (see "Pixel units") + float devicePixelRatio = 1.0f; + }; + + struct RuntimeOptions { + uint32_t msaaSamples = 4; + bool enableDebugger = false; + bool enableShaderCache = true; + + // Which feature bundles to wire up. Defaults match Playground. + struct Features { + bool input = true; + bool xr = false; + bool camera = false; + bool capture = false; + } features; + + std::function log; + std::function onUnhandledError; + }; + + // Long-lived: typically created once per app/process. Sets up the + // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU + // polyfills/plugins. Construction is cheap and synchronous — + // no GPU device exists yet. Device construction and GPU plugin + // initialization (NativeEngine, etc.) are deferred to the first + // `View::Attach` call. + class Runtime { + public: + static std::unique_ptr Create(RuntimeOptions = {}); + + // // Future construction mode — adopt a host-owned Babylon::JsRuntime + // // instead of letting Runtime construct its own AppRuntime+JsRuntime. + // // Intended for hosts that already own a JS engine and want + // // Babylon Native plugins to live inside it: React Native (the + // // host already owns Hermes/JSC + a CallInvoker dispatcher), + // // future custom V8/QuickJS embedders, etc. The Integrations + // // layer never sees JSI directly — only Babylon::JsRuntime, + // // which the host wires up against whatever JS engine they have. + // // + // // In Attach mode `~Runtime` does NOT tear down the JS engine + // // (the host owns it); Suspend/Resume only DisableRendering on + // // the Device since the JS thread isn't ours to pause. Same + // // instance API as Create-mode otherwise — same LoadScript, + // // RunOnJsThread, View::Attach semantics. See "Construction + // // modes" below. + // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, + // RuntimeOptions = {}); + + // JS interaction — safe to call regardless of view/suspend state. + // LoadScript: calls made before the first `View::Attach` are + // queued internally and dispatched onto the JS thread after + // engine initialization completes during that first Attach. + // Calls made after the first Attach are dispatched immediately. + // (Most existing integrations — Playground's `AppContext`, both + // bridges — already gate their own LoadScript on engine init by + // hand; this just formalizes the same ordering.) + void LoadScript(std::string_view url); // file://, http(s)://, app:// + void Eval(std::string_view source, std::string_view sourceUrl = {}); + void RunOnJsThread(std::function); // escape hatch + + // Suspend/Resume — orthogonal to view attachment. Use when the host + // app is backgrounded, throttled, or otherwise should not be doing + // work (iOS applicationWillResignActive, Android onPause, power + // saving, modal dialogs). While suspended: + // - JS timers (setTimeout/setInterval) pause. + // - In-flight microtasks complete; no new tasks are dispatched. + // - Any attached View becomes a no-op for RenderFrame() — the + // host can keep calling it from its draw callback; nothing + // happens until Resume(). + // Calls are reference-counted: nesting is safe. + void Suspend(); + void Resume(); + bool IsSuspended() const; + + ~Runtime(); // detaches any current view implicitly + + private: + friend class View; + }; + + // Transient: created when a host surface appears, destroyed when it + // goes away. Multiple sequential Views may be attached to the same + // Runtime over its lifetime. At most one View may be attached at a + // time. + class View { + public: + // Attaches `handle` to `runtime`. + // + // First Attach on a given Runtime is the heavy step: it + // constructs `Graphics::Device` against `handle`, dispatches + // GPU plugin initialization (`Device::AddToJavaScript`, + // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, + // …), and flushes any scripts queued via `Runtime::LoadScript` + // before this point. Opens the first frame. + // + // Subsequent Attach calls on the same Runtime are cheap: the + // Device is already constructed, plugins are initialized, the + // JS engine is running. They just call `Device::UpdateWindow` + + // `Device::EnableRendering` to bind the new surface, then open + // the first frame for the new attachment. + // + // Detach (~View) closes the in-flight frame and calls + // `Device::DisableRendering`. The Device persists on the + // Runtime, so the next Attach is fast. + static std::unique_ptr Attach(Runtime& runtime, const ViewDescriptor& handle); + + // Render exactly one frame. Must be called from the same thread + // each time (the "frame thread"). No-op if the runtime is + // suspended. The host calls this from the platform view/control's + // existing draw callback — see "How frames actually get rendered". + void RenderFrame(); + + // Resize the bound surface. Width/height are in **logical + // pixels**; `dpr` is the physical-to-logical ratio (e.g. 2.0 + // for a Retina display). The platform interop layer is + // responsible for converting whatever its UI framework + // provides (Android `View.onSizeChanged` is in physical + // pixels; iOS `MTKViewDelegate.drawableSizeWillChange:` is in + // physical pixels; SwiftUI / AppKit hand you points; etc.) + // into this convention — see §4.2 "Pixel units". + void Resize(uint32_t width, uint32_t height, float dpr = 1.0f); + + // Input — host calls these from its event loop while the view + // exists. Safe to call from any thread; routed to the JS thread + // via NativeInput. + void OnPointerDown(int32_t pointerId, float x, float y); + void OnPointerMove(int32_t pointerId, float x, float y); + void OnPointerUp (int32_t pointerId, float x, float y); + void OnKey(int32_t keyCode, bool down); + + ~View(); + }; +} +``` + +#### Construction modes (forward-compatibility note) + +The `Runtime::Attach` factory above is reserved for a future addition; +it is shown commented-out in the header so the API shape leaves room +for it without breaking changes when added. + +The split is along a single axis — *who owns the JS runtime*: + +- **`Create` (standalone)** — the Integrations layer builds its own + `Babylon::AppRuntime` (JS thread + Napi env). Used by every host + that doesn't already have a JS engine: Win32, Android Activities + hosting Babylon directly, iOS apps with `BNRuntime`, etc. +- **`Attach` (embedded, future)** — the host has already wired up a + `Babylon::JsRuntime` (the existing public class at + `Core/JsRuntime/Include/Babylon/JsRuntime.h`) against their JS + engine and dispatcher and hands it in. Used by hosts whose + framework already owns the JS runtime: React Native (Hermes/JSC + + `CallInvoker` dispatcher) is the concrete consumer; this is + exactly what `BabylonReactNative`'s `BabylonNative.cpp:50` does + today via `Babylon::JsRuntime::CreateForJavaScript(env, dispatcher)`. + +Both factories produce a `Runtime` in the same "JS engine ready, +GPU not yet constructed" state. Past construction, the instance API +(`LoadScript`, `Eval`, `RunOnJsThread`, `View::Attach`, `RenderFrame`, +…) is identical — `LoadScript` and `Eval` work in Attach mode because +the Integrations layer's `ScriptLoader` accepts any object with a +`Dispatch(std::function)` method (see +`Core/ScriptLoader/Include/Babylon/ScriptLoader.h:19-23`), and both +`AppRuntime` (Create mode) and `JsRuntime` (Attach mode) qualify. + +A few semantic differences worth flagging at impl time: + +- **`LoadScript` / `Eval` are usually unused in Attach mode.** RN-style + hosts already own script loading via their framework's bundler + (Metro / Webpack imports `@babylonjs/core` and the experience code + as ES modules). The methods are still callable — they'd fetch via + XHR and execute via `env.RunScript` — but only make sense if the + host wants to load a UMD bundle outside its own module system. The + primary integration point in Attach mode is `RunOnJsThread`, which + is exactly the `JsRuntime::Dispatch` pattern BRN already uses + (`BabylonReactNative` `BabylonNative.cpp:50,107`). +- **`~Runtime`** — Create mode tears down the owned `AppRuntime`, + joining its JS thread. Attach mode does *not* tear down the host's + JS engine; it cancels any pending `LoadScript`/`Eval`/`RunOnJsThread` + lambdas, then `Napi::Detach`es. Hosts that re-create the runtime + across a JS-engine reload (e.g. RN dev-mode bridge invalidation — + `BabylonReactNative` listens for `RCTBridgeWillInvalidateModulesNotification` + and calls `Deinitialize` to release its `weak_ptr`) get a clean + destroy-then-recreate path. +- **`Suspend`/`Resume`** — Create mode pauses the JS thread via + `AppRuntime::Suspend`. Attach mode can only `DisableRendering` on + the `Device` since the JS thread isn't owned by us; the host's + framework controls JS-thread pause/resume. + +Adding `Attach` later is purely additive — one new static factory +taking an existing public Babylon Native type. No breaking change, +no surface duplication. + +#### Switching surfaces + +Whenever the host wants to render against a different platform +surface — including the swap from a hidden pre-warm window to the +user-visible one, the swap from one Activity's `SurfaceView` to +another after configuration changes, or any other surface change — +the pattern is the same: + +1. Destroy the current `View` (`view.reset()` / `~View()`). +2. Construct a new `View::Attach(runtime, newWindow)`. + +The Runtime's underlying `Graphics::Device` (constructed during the +*first* Attach and persisted on the Runtime thereafter) calls +`UpdateWindow` internally on subsequent Attaches. JS state, plugins, +loaded scripts, and scene state are all preserved across the swap. + +This is the only mechanism the Integrations layer provides for +changing surfaces. There is no `View::SwapTo`, no `Runtime::SetWindow`, +and no implicit "current surface" the host needs to track — there's +just construct a `View` to bind a surface and destroy it to unbind. + +#### Starting the engine before the user-visible UI exists (host pattern) + +Some hosts want the scene to be ready the moment the user navigates +to the rendering screen — they want script load, plugin init, scene +construction, texture/mesh upload, and shader compilation to have +already happened *before* the user-visible UI attaches. This is what +`babylon-native-bridge`'s `BabylonNativeBridge::start:` (iOS) and +`BabylonNativeBridge.preload(...)` (Android) do today. + +The Integrations layer does not bake this in as a feature — it falls +out naturally from the *Switching surfaces* model: + +1. At app start, host calls `Runtime::Create()`. This is cheap; no + GPU device exists yet. +2. Host calls `Runtime::LoadScript("app:///bundle.js")` etc. The + scripts are queued internally pending the first `View::Attach`. +3. When the host wants to start scene construction (typically still + before the user-visible UI is ready), host allocates a small + hidden window — iOS `[CAMetalLayer layer]` with `isHidden = YES`; + Android off-screen `SurfaceView`; Win32 `WS_EX_TOOLWINDOW` HWND — + and calls `View::Attach(runtime, hiddenWindow)`. This is the + first Attach, so it constructs the `Graphics::Device`, dispatches + GPU plugin init, flushes the queued scripts. The JS thread is now + running scene construction against the hidden surface. +4. Eventually the user-visible UI's surface becomes ready. Host + destroys the hidden-surface View, constructs a new + `View::Attach(runtime, realWindow)`. `Device::UpdateWindow` swaps + the surface; scene state is preserved. + +A host that doesn't care about pre-loading just calls `Runtime::Create()` +and then `View::Attach(runtime, realWindow)` whenever the real +surface is ready, with `LoadScript` calls anywhere in between. The +Integrations layer doesn't know which path the host took. + +#### Loading Babylon.js: two supported routes + +the Integrations layer is agnostic about how Babylon.js gets into the JS +runtime — `LoadScript` is the only mechanism. Two patterns are +first-class: + +1. **Pre-bundled (recommended for new integrations).** The host + bundles `@babylonjs/core` (and any of `@babylonjs/loaders`, + `materials`, `gui`, `serializers`, `havok`, etc.) into a single + ES-module-or-IIFE bundle using webpack / vite / esbuild / rollup, + then loads it with one call: + + ```cpp + runtime->LoadScript("app:///bundle.js"); + runtime->LoadScript("app:///experience.js"); // user's scene code + ``` + +2. **Multi-UMD (Playground-style).** The host loads each `babylon.*.js` + UMD bundle individually, in dependency order. This matches what + `Apps/Playground/Shared/AppContext.cpp` does today and is useful + for projects that drop in stock UMD builds without a bundler: + + ```cpp + runtime->LoadScript("app:///babylon.max.js"); + runtime->LoadScript("app:///babylonjs.loaders.js"); + runtime->LoadScript("app:///babylonjs.materials.js"); + runtime->LoadScript("app:///babylonjs.gui.js"); + runtime->LoadScript("app:///babylonjs.serializers.js"); + runtime->LoadScript("app:///experience.js"); + ``` + +`LoadScript` calls are queued onto the JS thread in submission order, +so this is sufficient — no separate `bootstrapScripts` option is +needed. The library does not preload anything by default. + +#### How frames actually get rendered + +Today the host is responsible for the per-frame +`FinishRenderingCurrentFrame()` / `StartRenderingCurrentFrame()` pair, +driven from a platform-specific source. The simplified API keeps that +responsibility on the host but collapses the per-frame work to a +single call: `view->RenderFrame()`. + +The library deliberately does **not** subscribe to a vsync source, +`CADisplayLink`, `Choreographer`, `CompositionTarget::Rendering`, or +anything similar. Every UI framework already gives the view/control +that hosts the rendering surface a natural draw callback — the host +wires `RenderFrame()` to that and is done. + +Examples of the "natural draw callback" per platform: + +| Platform | Where the host calls `RenderFrame()` | +|-----------------|----------------------------------------------------------------------------| +| Win32 | `WM_PAINT` handler on the rendering window | +| UWP | `SwapChainPanel`'s composition / rendering callback | +| macOS | `NSView::drawRect:` or an `MTKViewDelegate::drawInMTKView:` | +| iOS / visionOS | `UIView::drawRect:` or `MTKViewDelegate::drawInMTKView:` | +| Android | A custom `View`'s `onDraw(Canvas)` or `SurfaceView`'s render thread loop | +| X11 | `Expose` event handler | +| Wayland | `wl_surface::frame` callback (set up by the host's UI framework) | + +In all cases this is glue the host already writes to integrate any +custom rendering with its UI framework — Babylon Native does not add +new requirements. + +##### What `RenderFrame()` does + +```cpp +void View::RenderFrame() +{ + if (m_runtime.IsSuspended()) return; // pause cleanly, no GPU work + + m_device.FinishRenderingCurrentFrame(); // submit the in-flight frame + m_device.StartRenderingCurrentFrame(); // open the next one + // Babylon's JS render loop (requestAnimationFrame / scene.render) + // runs between Start and Finish, scheduled via DeviceUpdate onto + // the JS thread. RenderFrame does not call into JS directly — + // DeviceUpdate already coordinates Napi dispatch, bgfx command + // recording, and present. +} +``` + +##### Threading + +Two threads are in play: + +| Thread | Owner / lifetime | What runs there | +|------------------|---------------------------------------------------|-----------------------------------------------------------------| +| **JS thread** | `AppRuntime`'s `WorkQueue`, lives with `Runtime` | All Napi/JS execution (`scene.render()`, timers, XHR callbacks) | +| **Frame thread** | The thread the host calls `RenderFrame()` from | `Device::Start/FinishRenderingCurrentFrame` and bgfx submission | + +bgfx is configured to *not* create its own render thread +(`Core/Graphics/Source/DeviceImpl.cpp:205`), so the "render thread" +is simply whichever thread the host calls `RenderFrame()` from — +consistently. The contract is: + +- **Call `View::Attach`, `View::RenderFrame`, `View::Resize`, and + `View::~View` from the same thread** (the frame thread — usually + the UI thread, since draw callbacks fire on the UI thread). +- **Input methods (`OnPointerDown`, etc.) may be called from any + thread**; they post to the JS thread via `NativeInput`. +- The JS thread is invisible to the host; `RunOnJsThread` is the + only way to reach it. + +Cross-thread coordination between the frame thread and the JS thread +is handled entirely by the existing `DeviceUpdate` + +`SafeTimespanGuarantor` machinery +(`Core/Graphics/InternalInclude/Babylon/Graphics/SafeTimespanGuarantor.h`). +The simplified API does not reinvent it. + +#### Lifetime and state relationship + +``` +Runtime ────────────────────────────────────────────────► destroy + │ │ │ │ +View attachments ├── attach1 ─────┤ ├── attach2 ┤ + (window 1) │ (window 2) │ + │ │ +Suspend state running ─┬─ suspended ──┴── running ──┬─ suspended ─ running + │ │ + (app backgrounded) (modal dialog) +``` + +Three independent axes: +- **Runtime lifetime** — JS engine, scene, device. One per app/process. +- **View attachment** — *where* to render. Zero or one at a time, may + cycle freely. +- **Suspend state** — *whether* to do work. Independent of view; an + app can be suspended with or without a view attached. + +The `Runtime` keeps the JS engine, scene graph, loaded assets, and +`Graphics::Device` alive across detach/attach cycles. Re-attaching a +new `View` calls `Device::UpdateWindow` + `EnableRendering` and +resumes rendering without re-running any JS. + +Internally `Runtime` owns the `Graphics::Device`, `DeviceUpdate`, +`AppRuntime`, `ScriptLoader`, `Canvas`, and `NativeInput*`. +`View::Attach` performs the equivalent of `Device::UpdateWindow` + +`Device::EnableRendering` + `DeviceUpdate::Start` + +`Device::StartRenderingCurrentFrame`; `~View` performs the symmetric +`FinishRenderingCurrentFrame` + `DeviceUpdate::Finish` + +`DisableRendering` so rendering cleanly pauses while no surface +exists. + +### 4.2 Per-platform interop layers + +Each platform ships a tiny **C++ interop module** that uses the +platform's native inter-language ABI to expose `Runtime` + `View` to +the host UI language. These modules are the analog of today's +`Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp`, +generalized into reusable building blocks under `Integrations//`. + +The interop module **always includes** whatever small native-side +shim the platform's interop ABI fundamentally requires (e.g., a Java +class declaring `external fun` declarations on Android, an Objective-C +`@interface` header on Apple). It **does not** include idiomatic +high-level wrappers in the host language (no Kotlin "BabylonView" +class with Compose helpers, no Swift `BabylonScene` value type). +Designing those is the consumer's job. + +| Platform | Interop ABI | Interop module shape | Native-side host shim | +|------------------|----------------------------|-----------------------------------------------------------------------|------------------------------------------------------------------| +| Android | JNI | `extern "C" JNIEXPORT` functions on a single JNI class | Minimal Java/Kotlin class declaring `external fun` entry points | +| iOS / macOS / visionOS | Objective-C runtime | Obj-C++ (`.mm`) files exposing `@interface BNRuntime/BNView` | Obj-C `@interface` headers (auto-imported from Swift) | +| UWP | WinRT | C++/WinRT `runtimeclass`es | `.idl` for projection generation | +| Win32 | Plain C++ | Direct `Babylon::Integrations::*` (no interop layer needed) | None | +| Linux (X11/Wayland) | Plain C++ | Direct `Babylon::Integrations::*` (no interop layer needed) | None | + +The interop module's surface mirrors `Runtime` and `View` 1:1, with +one entry point per public method. It is intentionally as thin as the +JNI file we have today. + +#### Interop layer responsibilities + +Beyond the 1:1 mirroring of `Runtime` and `View`, each interop layer +owns two platform adaptations on the host's behalf so the host's +UI-language code stays as simple as possible: + +1. **Translate platform-natural units to the C++ contract.** The + shared C++ `View::Resize(width, height, dpr)` and `ViewDescriptor` + take **logical pixels + DPR**. Each interop layer converts + whatever its UI framework hands the host: + - **Android.** `View.onSizeChanged(int w, int h, ...)` provides + **physical pixels**. The interop layer divides by + `Resources.getSystem().getDisplayMetrics().density` and passes + the result + the density itself to the native side. The host's + Kotlin code passes the raw `w/h` it received. + - **Apple.** `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` + provides **physical pixels**; `view.contentsScale` is the DPR. + The interop layer divides and passes through. The host's Swift + code passes the `CGSize` it received (or hands over the layer + directly). + - **UWP.** `SwapChainPanel.SizeChanged` provides logical pixels + + `RasterizationScale` for DPR. The interop layer passes both + through unchanged. + - **Win32 / Linux.** No interop layer; the host C++ does the + conversion if its windowing system cares. +2. **Expose platform-specific lifecycle entries that don't belong on + the cross-platform API.** Examples from `babylon-native-bridge`: + - Android: `setCurrentActivity(Activity)` → + `android::global::SetCurrentActivity(...)`, + `activityOnRequestPermissionsResult(...)` → + `android::global::RequestPermissionsResult(...)`. These hook + into `AndroidExtensions/Globals.h` and are required by plugins + like `NativeCamera` that need to call back into the Java side. + - iOS: hooks for `applicationWillTerminate` and similar that map + to interop-layer cleanup. + + These live on the interop layer's own surface, *not* on + `Babylon::Integrations::Runtime` or `View`. The cross-platform layer + stays free of `#ifdef`s; per-platform concerns live where they + belong. + +The interop layer does **not** auto-allocate a hidden initial +surface, set rendering policy, or otherwise opt the host into a +particular lifecycle pattern. The host always provides the surface +it wants the runtime to use; the interop layer just forwards it. + +#### Distribution model: source + opt-in CMake subprojects + +**Babylon Native is not distributed as a precompiled binary.** Source-based +CMake remains the only distribution model — that's what allows hosts to +exclude unused plugins (`NativeXr`, `NativeCamera`, `NativeCapture`, +`NativeEncoding`, `ShaderCache`, etc.) and keep their final binary small. +A precompiled `babylon_native.dll/.so/.dylib` would force every host to +link every plugin. + +The Integrations layer is just **additional CMake targets that the host +opts into**, layered on top of the existing component targets: + +``` + Babylon::Graphics, Babylon::AppRuntime, Babylon::ScriptLoader, + Babylon::Polyfills::*, Babylon::Plugins::* (existing — unchanged) + │ + ▼ + Babylon::Integrations (new — shared C++ facade) + │ + ┌───────────────┼───────────────┬───────────────┐ + ▼ ▼ ▼ ▼ + Babylon::Integrations::Android ::Apple ::Uwp … + (new, optional) (new, optional) (new, optional) +``` + +Each new target is its own CMake subdirectory under `Integrations/`, +gated by a CMake option that defaults to OFF (except the cross-platform +facade itself, which defaults to ON because it's lightweight and useful +for any host). Platforms that don't need an interop layer (Win32, +Linux) consume `Babylon::Integrations` directly. + +| CMake option | Default | Builds | +|---------------------------------------|---------|-------------------------------------------------| +| `BABYLON_NATIVE_INTEGRATIONS` | ON | `Babylon::Integrations` (shared C++ facade) | +| `BABYLON_NATIVE_INTEGRATIONS_ANDROID` | OFF | JNI interop `.so` + companion Java sources | +| `BABYLON_NATIVE_INTEGRATIONS_APPLE` | OFF | Obj-C++ interop static lib + Obj-C headers | +| `BABYLON_NATIVE_INTEGRATIONS_UWP` | OFF | C++/WinRT runtimeclass DLL + `.winmd` | + +Plugin selection remains opt-in/out via CMake options that the +Integrations layer respects: + +| CMake option | Default | Effect | +|-------------------------------------------|---------|-------------------------------------------------| +| `BABYLON_NATIVE_PLUGIN_NATIVEENGINE` | ON | Required for rendering | +| `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` | ON | Required for `View::OnPointer*` / `OnKey` | +| `BABYLON_NATIVE_PLUGIN_NATIVEXR` | OFF | XR session support | +| `BABYLON_NATIVE_PLUGIN_NATIVECAMERA` | OFF | Webcam capture | +| `BABYLON_NATIVE_PLUGIN_NATIVECAPTURE` | OFF | Frame/region capture | +| `BABYLON_NATIVE_PLUGIN_NATIVEENCODING` | OFF | Image encoding | +| `BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS` | OFF | Babylon.js perf hooks | +| `BABYLON_NATIVE_PLUGIN_NATIVETRACING` | OFF | ETW / signpost tracing | +| `BABYLON_NATIVE_PLUGIN_SHADERCACHE` | ON | Disk shader cache | +| `BABYLON_NATIVE_POLYFILL_XHR` | ON | `XMLHttpRequest` | +| `BABYLON_NATIVE_POLYFILL_CANVAS` | ON | 2D canvas | + +`Babylon::Integrations` reads these flags (via `target_compile_definitions` +or `if(TARGET ...)` checks) and only wires up the corresponding plugin +in its setup function — so disabling `BABYLON_NATIVE_PLUGIN_NATIVEXR` +removes both the `NativeXr` library from the link line *and* the +`Babylon::Plugins::NativeXr::Initialize(env)` call from the runtime +boot, with no runtime overhead. + +The interop layers depend on `Babylon::Integrations` (which depends on +whichever plugins are enabled), so each interop artifact contains only +the code paths that were actually compiled in. + +##### Conditional API surface + +Plugin/polyfill flags don't just remove implementations — they remove +the **public API surface** that depends on them, so misuse is a +compile error in the host's language rather than a silent runtime +no-op. + +Each plugin-gated method in `Babylon::Integrations` is wrapped in +`#if BABYLON_NATIVE_PLUGIN_` (defined by the build via +`target_compile_definitions` when the corresponding CMake option is +ON). The interop layers mirror the same gating on their own entry +points: when a plugin is disabled the JNI export, the Obj-C method, +and the C++/WinRT method all disappear together. + +| CMake option | Public surface gated by it | +|-------------------------------------------|---------------------------------------------------------------------------------------------| +| `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` | `View::OnPointerDown` / `OnPointerMove` / `OnPointerUp` / `OnKey` and their interop entries | +| `BABYLON_NATIVE_PLUGIN_NATIVEXR` | XR-specific extension methods (e.g. `View::SetXrSessionStateChangedCallback`) | +| `BABYLON_NATIVE_PLUGIN_NATIVECAMERA` | Camera permission helpers, if any are added at the Integrations layer | +| `BABYLON_NATIVE_PLUGIN_NATIVECAPTURE` | `Runtime::CaptureFrame` (if exposed at the Integrations layer) | + +Method-level subset of the C++ header, illustrating the pattern: + +```cpp +class View { +public: + static std::unique_ptr Attach(Runtime&, const ViewDescriptor&); + void RenderFrame(); + void Resize(uint32_t w, uint32_t h, float dpr = 1.0f); + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + void OnPointerDown(int32_t pointerId, float x, float y); + void OnPointerMove(int32_t pointerId, float x, float y); + void OnPointerUp (int32_t pointerId, float x, float y); + void OnKey(int32_t keyCode, bool down); +#endif + + ~View(); +}; +``` + +And the matching Android JNI gating: + +```cpp +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT +extern "C" JNIEXPORT void JNICALL +Java_com_babylonjs_native_BabylonNative_viewPointerDown(JNIEnv*, jclass, jlong h, jint id, jfloat x, jfloat y) { + reinterpret_cast(h)->OnPointerDown(id, x, y); +} +// ...PointerMove, PointerUp, Key likewise. +#endif +``` + +The companion Kotlin shim under `Integrations/Android/src/main/java/` is +generated at configure time (or hand-maintained in matched halves) so +that the `external fun` declarations only appear when the +corresponding native entries do. Same approach for the Apple `.h` and +the UWP `.idl`. + +`RunOnJsThread` is gated by `BABYLON_NATIVE_EXPOSE_NAPI` (default ON, +since the escape hatch is harmless if unused) so a host that wants a +strictly N-API-free header surface can opt out. + +##### How the host consumes each interop layer + +- **Android.** `Integrations/Android/` contains a CMakeLists.txt that + builds the JNI interop `.so` and a `src/main/java/...` directory + with the companion Java/Kotlin shim class declaring `external fun` + entry points. The host's existing `app/build.gradle` references the + CMakeLists via `externalNativeBuild { cmake { path "..." } }` (the + standard Android NDK + gradle integration) and adds the Java sources + to its `sourceSets`. No AAR is produced by us; the host's existing + gradle build emits whatever artifact it already emits. +- **Apple.** `Integrations/Apple/` is consumed via CMake by the host's + Xcode project (or an `add_subdirectory` from the host's CMakeLists). + It produces a static library and a set of public Objective-C + headers that Swift code imports via the standard Swift–Obj-C bridge. +- **UWP.** `Integrations/Uwp/` produces a C++/WinRT runtime component DLL + and `.winmd`. The host's C# / C++/WinRT project references it + directly. +- **Win32 / Linux.** No interop layer; the host C++ uses + `Babylon::Integrations` directly. + +This keeps the source-build, opt-in-plugins model intact end-to-end: +no precompiled "everything" artifacts, and the host's build still +fully controls what code ends up in the final binary. + +## 5. Examples + +### Win32 (was ~250 LOC, becomes ~35) — direct C++ + +```cpp +#include +#include + +// Process-scoped: created once at startup, destroyed at shutdown. +static std::unique_ptr g_runtime; +// Window-scoped: created on WM_CREATE, destroyed on WM_DESTROY. +static std::unique_ptr g_view; + +LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) { + switch (msg) { + case WM_CREATE: { + RECT r; GetClientRect(hwnd, &r); + // First Attach: constructs Device, runs GPU plugin init, flushes + // queued LoadScript calls. Subsequent re-Attaches against this + // runtime are cheap. + g_view = Babylon::Integrations::View::Attach(*g_runtime, + { hwnd, (uint32_t)r.right, (uint32_t)r.bottom }); + return 0; + } + case WM_SIZE: if (g_view) g_view->Resize(LOWORD(l), HIWORD(l)); return 0; + case WM_PAINT: if (g_view) g_view->RenderFrame(); // natural draw callback + ValidateRect(hwnd, nullptr); return 0; + case WM_DESTROY: g_view.reset(); return 0; // runtime stays alive + } + return DefWindowProc(hwnd, msg, w, l); +} + +int WINAPI wWinMain(...) { + g_runtime = Babylon::Integrations::Runtime::Create(); + g_runtime->LoadScript("app:///experience.js"); // queued; flushed + // on first View::Attach + + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + if (g_view) InvalidateRect(msg.hwnd, nullptr, FALSE); // request next paint + } + g_runtime.reset(); +} +``` + +> The host wires `RenderFrame()` to `WM_PAINT` and triggers it via +> `InvalidateRect` from its message loop. Babylon Native does not +> subscribe to any DXGI / DWM source itself. + +**Pre-warm variant** — host wants the engine warm against a hidden +surface before the user-visible window opens. Allocate a hidden HWND +at app start, attach a View to it (this is what triggers Device +construction + GPU plugin init + script execution), and later destroy +it and re-attach against the real HWND when its `WM_CREATE` fires. + +```cpp +static std::unique_ptr g_prewarmView; +static HWND s_prewarmHwnd = nullptr; + +int WINAPI wWinMain(...) { + g_runtime = Babylon::Integrations::Runtime::Create(); + g_runtime->LoadScript("app:///experience.js"); + + s_prewarmHwnd = CreateWindowEx(WS_EX_TOOLWINDOW, ..., HWND_MESSAGE, ...); + g_prewarmView = Babylon::Integrations::View::Attach(*g_runtime, + { s_prewarmHwnd, 16, 16, 1.0f }); + // Engine is now initializing + scene is building against hidden surface. + + // ...real window's WM_CREATE will g_prewarmView.reset() then + // View::Attach to its HWND. Device::UpdateWindow swaps to the + // real HWND under the hood; scene state preserved. + + MSG msg; while (GetMessage(&msg, nullptr, 0, 0) > 0) { /* same as above */ } + g_runtime.reset(); + DestroyWindow(s_prewarmHwnd); +} +``` + +### Android — JNI interop layer + minimal Kotlin shim + +The library ships the JNI `.cpp` and a companion Kotlin class. The +host writes a custom `View` (or `SurfaceView`) subclass and calls +`renderFrame()` from its draw callback. + +The interop layer translates Android's physical-pixel + +`displayMetrics.density` pair into the C++ logical-pixel + DPR +convention (see §4.2). It does not allocate or own surfaces — the +host always supplies them. + +**Library-supplied JNI interop** (lives in `Integrations/Android/src/main/cpp/`): + +```cpp +extern "C" { + +JNIEXPORT jlong JNICALL +Java_com_babylonjs_native_BabylonNative_runtimeCreate(JNIEnv* env, jclass) { + // unique_ptr::release() returns the raw pointer and gives up + // ownership *without* deleting; the JVM side now owns it via the + // returned jlong handle and must call runtimeDestroy() to free it. + auto* rt = Babylon::Integrations::Runtime::Create().release(); + return reinterpret_cast(rt); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_native_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong h) { + delete reinterpret_cast(h); +} + +JNIEXPORT void JNICALL +Java_com_babylonjs_native_BabylonNative_runtimeLoadScript(JNIEnv* env, jclass, jlong h, jstring url) { + const char* s = env->GetStringUTFChars(url, nullptr); + reinterpret_cast(h)->LoadScript(s); + env->ReleaseStringUTFChars(url, s); +} + +JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_runtimeSuspend(JNIEnv*, jclass, jlong h) +{ reinterpret_cast(h)->Suspend(); } +JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_runtimeResume(JNIEnv*, jclass, jlong h) +{ reinterpret_cast(h)->Resume(); } + +// viewAttach: physical pixels in, logical pixels through to C++. +JNIEXPORT jlong JNICALL +Java_com_babylonjs_native_BabylonNative_viewAttach(JNIEnv* env, jclass, jlong rt, + jobject surface, + jint physicalW, jint physicalH, + jfloat density) { + ANativeWindow* win = ANativeWindow_fromSurface(env, surface); + Babylon::Integrations::ViewDescriptor descriptor{ + win, + (uint32_t)(physicalW / density), // logical pixels for C++ + (uint32_t)(physicalH / density), + density + }; + auto* v = Babylon::Integrations::View::Attach( + *reinterpret_cast(rt), descriptor).release(); + return reinterpret_cast(v); +} + +JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_viewDetach(JNIEnv*, jclass, jlong h) +{ delete reinterpret_cast(h); } +JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_viewRenderFrame(JNIEnv*, jclass, jlong h) +{ reinterpret_cast(h)->RenderFrame(); } + +// viewResize: same conversion as viewAttach. +JNIEXPORT void JNICALL +Java_com_babylonjs_native_BabylonNative_viewResize(JNIEnv*, jclass, jlong h, + jint physicalW, jint physicalH, + jfloat density) { + reinterpret_cast(h)->Resize( + (uint32_t)(physicalW / density), + (uint32_t)(physicalH / density), + density); +} + +} +``` + +**Library-supplied Kotlin shim** (lives in `Integrations/Android/src/main/java/com/babylonjs/native/`): + +```kotlin +package com.babylonjs.native + +class BabylonNativeRuntime { + private val handle: Long = nativeCreate() + + init { System.loadLibrary("babylon-native-interop") } + + fun loadScript(url: String) = nativeLoadScript(handle, url) + fun suspend() = nativeSuspend(handle) + fun resume() = nativeResume(handle) + fun close() = nativeDestroy(handle) + + internal fun nativeHandle(): Long = handle + + private external fun nativeCreate(): Long + private external fun nativeDestroy(handle: Long) + private external fun nativeLoadScript(handle: Long, url: String) + private external fun nativeSuspend(handle: Long) + private external fun nativeResume(handle: Long) +} + +// view descriptor — opaque from the host's perspective. The interop layer +// converts physical-pixel dimensions to logical pixels internally. +class BabylonNativeView(runtime: BabylonNativeRuntime, surface: Surface, + physicalW: Int, physicalH: Int, density: Float) { + private val handle = nativeAttach(runtime.nativeHandle(), surface, physicalW, physicalH, density) + + fun renderFrame() = nativeRenderFrame(handle) + fun resize(physicalW: Int, physicalH: Int, density: Float) = + nativeResize(handle, physicalW, physicalH, density) + fun detach() = nativeDetach(handle) + + private external fun nativeAttach(rt: Long, s: Surface, w: Int, h: Int, d: Float): Long + private external fun nativeDetach(handle: Long) + private external fun nativeRenderFrame(handle: Long) + private external fun nativeResize(handle: Long, w: Int, h: Int, d: Float) +} +``` + +**Host code — simple integration** (consumer's app — *not* shipped by the library): + +The simplest host creates the runtime at app start and attaches a +View when its real `SurfaceView`'s surface is ready. + +```kotlin +class MyApp : Application() { + val runtime by lazy { + BabylonNativeRuntime().apply { loadScript("app:///experience.js") } + // LoadScript queues; will execute after the first View::Attach. + } +} + +class BabylonView(context: Context) : SurfaceView(context), SurfaceHolder.Callback { + private val runtime get() = (context.applicationContext as MyApp).runtime + private var view: BabylonNativeView? = null + private val density = resources.displayMetrics.density + + init { holder.addCallback(this) } + + override fun surfaceCreated(h: SurfaceHolder) { + // First Attach: triggers Device construction + GPU plugin init + + // queued LoadScript flush. Subsequent Attaches are cheap. + view = BabylonNativeView(runtime, h.surface, width, height, density) + } + override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, hh: Int) { + view?.resize(w, hh, density) // physical pixels in + } + override fun surfaceDestroyed(h: SurfaceHolder) { + view?.detach(); view = null + } + + // Natural draw callback — host wires RenderFrame() here. + override fun onDraw(canvas: Canvas) { + view?.renderFrame() + invalidate() // request next draw + } +} +``` + +**Host code — pre-loading the engine before the user-visible UI exists:** + +A host that wants the engine warm before the user navigates to the +rendering screen creates the runtime + an off-screen `SurfaceView` +(the pattern from `babylon-native-bridge`'s +`BabylonNativeManagerView.java:51`) at app start, and attaches a +View to the off-screen surface so the first Attach kicks off Device +construction + plugin init + script execution. When the user-visible +surface arrives, the host destroys that View and attaches a new one +to the real surface (Device::UpdateWindow under the hood). + +```kotlin +class MyApp : Application() { + val runtime by lazy { + BabylonNativeRuntime().apply { loadScript("app:///experience.js") } + } + + private var hiddenSurface: Surface? = null + private var prewarmView: BabylonNativeView? = null + + override fun onCreate() { + super.onCreate() + // Off-screen SurfaceView attached to a window that is never shown. + // (Detail elided; same approach as babylon-native-bridge.) + hiddenSurface = createHiddenSurface() + prewarmView = BabylonNativeView(runtime, hiddenSurface!!, 1, 1, 1.0f) + // First Attach now: engine boots, scripts run, scene builds. + } + + fun releasePrewarm() { + prewarmView?.detach(); prewarmView = null + } +} +// ...later, when the user-visible BabylonView's surface is ready, host +// calls (myApp).releasePrewarm() and constructs a new BabylonNativeView +// against the real surface. + +class MainActivity : AppCompatActivity() { + private val runtime get() = (application as MyApp).runtime + override fun onPause() { super.onPause(); runtime.suspend() } + override fun onResume() { super.onResume(); runtime.resume() } +} +``` +### iOS / macOS — Obj-C++ interop layer + minimal Obj-C headers + +**Library-supplied Obj-C++ interop** (lives in `Integrations/Apple/`): + +The interop layer reads each `CAMetalLayer`'s `drawableSize` +(physical pixels) and `contentsScale` (DPR) directly so the Swift +host doesn't have to think about units — it just hands the layer +over (see §4.2). Surface allocation is the host's responsibility. + +```objc +// BNRuntime.h — public Obj-C header (Swift sees this via the bridge) +@interface BNRuntime : NSObject +- (instancetype)init; +- (void)loadScript:(NSString*)url; +- (void)suspend; +- (void)resume; +@end + +@interface BNView : NSObject +// `layer` is the user-visible CAMetalLayer (typically MTKView.layer). +// The interop layer reads drawableSize and contentsScale from it. +- (instancetype)initWithRuntime:(BNRuntime*)rt layer:(CAMetalLayer*)layer; +- (void)renderFrame; +- (void)resizeForLayer:(CAMetalLayer*)layer; // re-reads size/scale +@end +``` + +```objc++ +// BNRuntime.mm — implementation +@implementation BNRuntime { + std::unique_ptr _rt; +} +- (instancetype)init { + if ((self = [super init])) { + _rt = Babylon::Integrations::Runtime::Create(); + } + return self; +} +- (void)loadScript:(NSString*)url { _rt->LoadScript(url.UTF8String); } +- (void)suspend { _rt->Suspend(); } +- (void)resume { _rt->Resume(); } +- (Babylon::Integrations::Runtime*)native { return _rt.get(); } +@end + +@implementation BNView { + std::unique_ptr _v; +} +// Helper: read physical-pixel dims + DPR from the layer, convert to +// the C++ logical-pixel + DPR convention. +static Babylon::Integrations::ViewDescriptor MakeViewDescriptor(CAMetalLayer* layer) { + CGFloat scale = layer.contentsScale; + return { + (__bridge void*)layer, + (uint32_t)(layer.drawableSize.width / scale), // logical + (uint32_t)(layer.drawableSize.height / scale), + (float)scale + }; +} +- (instancetype)initWithRuntime:(BNRuntime*)rt layer:(CAMetalLayer*)layer { + if ((self = [super init])) { + // First Attach on this runtime triggers Device construction + + // GPU plugin init + queued LoadScript flush. + _v = Babylon::Integrations::View::Attach(*[rt native], MakeViewDescriptor(layer)); + } + return self; +} +- (void)renderFrame { _v->RenderFrame(); } +- (void)resizeForLayer:(CAMetalLayer*)layer { + auto h = MakeViewDescriptor(layer); + _v->Resize(h.width, h.height, h.devicePixelRatio); +} +@end +``` + +**Host code — simple integration** (consumer's app — *not* shipped by the library): + +The simplest host creates the runtime at app start, loads scripts +(queued), then constructs a `BNView` against the user-visible MTKView +layer in `viewDidLoad`. + +```swift +// AppDelegate — runtime created once at app start. +class AppDelegate: NSObject, UIApplicationDelegate { + let runtime: BNRuntime = { + let r = BNRuntime() + r.loadScript(Bundle.main.url(forResource: "experience", + withExtension: "js")!.absoluteString) + // LoadScript queues; flushes on first BNView attach. + return r + }() + + func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } + func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } +} + +class BabylonViewController: UIViewController, MTKViewDelegate { + var babylonView: BNView? + + override func viewDidLoad() { + let mtkView = view as! MTKView + mtkView.delegate = self + let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime + // First Attach: triggers Device + plugin init + queued script flush. + babylonView = BNView(runtime: runtime, layer: mtkView.layer as! CAMetalLayer) + } + + // Natural draw callback — no size arithmetic in Swift. + func draw(in view: MTKView) { babylonView?.renderFrame() } + func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { + babylonView?.resizeForLayer(v.layer as! CAMetalLayer) + } +} +``` + +**Host code — pre-loading the engine before the user-visible UI exists:** + +A host that wants the engine warm at app start creates a `BNView` +against an off-screen `CAMetalLayer` in the `AppDelegate` so the first +Attach fires immediately and starts initialization + scene +construction. Later, when a view controller appears, the host destroys +that View and constructs a new `BNView` against the user-visible +layer. + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + let runtime = BNRuntime() + var prewarmLayer: CAMetalLayer! + var prewarmView: BNView! + + func application(_ app: UIApplication, didFinishLaunchingWithOptions + options: [UIApplication.LaunchOptionsKey : Any]?) -> Bool { + runtime.loadScript(Bundle.main.url(forResource: "experience", + withExtension: "js")!.absoluteString) + + prewarmLayer = CAMetalLayer() + prewarmLayer.drawableSize = CGSize(width: 16, height: 16) + prewarmLayer.isHidden = true + prewarmView = BNView(runtime: runtime, layer: prewarmLayer) + // First Attach fires here; engine is now booting up + scripts running. + return true + } + + func releasePrewarm() { prewarmView = nil } // call before binding real layer + + func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } + func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } +} +// ... later, in the view controller: +// delegate.releasePrewarm() +// babylonView = BNView(runtime: delegate.runtime, +// layer: mtkView.layer as! CAMetalLayer) +// // Device::UpdateWindow swaps to the user-visible layer; scene state +// // and JS state are preserved. +``` + +### Suspend/Resume is reference-counted + +Multiple subsystems can request suspension independently — the runtime +only resumes when all requests have been released. This makes it safe +to combine app-lifecycle suspension with ad-hoc pauses (e.g., a modal +dialog, a power-saving mode, a long-running native operation): + +```cpp +runtime->Suspend(); // app backgrounded -> count = 1, suspended +runtime->Suspend(); // modal dialog also pauses -> count = 2, suspended +runtime->Resume(); // dialog closed -> count = 1, still suspended +runtime->Resume(); // app foregrounded -> count = 0, running +``` + +## 6. Implementation Phases + +### Phase 1 — Shared C++ facade (no new functionality) +- Extract the canonical setup from `Apps/Playground/Shared/AppContext.cpp` + into a new `Integrations` component, split along the lifetime boundary: + `Babylon::Integrations::Runtime` (long-lived; owns `AppRuntime`, + `ScriptLoader`, non-GPU polyfills/plugins; lazily constructs the + `Graphics::Device` on first View::Attach) and + `Babylon::Integrations::View` (transient; binds a platform surface + via `Device::UpdateWindow` + `EnableRendering` and drives the + per-frame `Start/FinishRenderingCurrentFrame` pair). +- Implement `Runtime::Suspend/Resume` on top of `AppRuntime::Suspend/Resume` + with reference-counted nesting. +- Implement `Runtime::LoadScript` queueing for calls made before the + first `View::Attach`; flush the queue inside that first Attach + after engine initialization completes. +- Add CMake option `BABYLON_NATIVE_INTEGRATIONS` (default ON). +- Plumb `BABYLON_NATIVE_PLUGIN_*` and `BABYLON_NATIVE_POLYFILL_*` flags + through `Babylon::Integrations`'s setup function so disabling a plugin + removes the link dep, the `Initialize` call, **and** the public + header surface that depends on it (see §4.4 *Conditional API + surface*). +- As a transitional step, refactor `AppContext` to be a thin wrapper + over `Babylon::Integrations` for parity validation; Playground keeps + working unchanged. `AppContext` is **deleted** at the end of Phase 5 + once every Playground host has been migrated to `Babylon::Integrations` + directly (see Phase 5.5 below). + +### Phase 2 — Win32 / Linux validation (no interop layer) +- Convert `Apps/Playground/Win32/App.cpp` and the Linux variants to use + `Babylon::Integrations` directly (no interop layer needed on these + platforms). +- Verify the `WM_PAINT` / Expose-event frame model works end-to-end + with the existing playground content. + +### Phase 3 — Android interop layer (`Integrations/Android/`) +- Create `Integrations/Android/CMakeLists.txt` that builds a JNI `.so` + exposing the entry points listed in §5. +- Add the companion `BabylonNative.kt` (or `.java`) shim under + `Integrations/Android/src/main/java/`. +- Convert `Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp` + to use the new interop layer (or simply delete it and have Playground + consume `Integrations/Android/` directly). +- Document the gradle integration: `externalNativeBuild` referencing + the `Integrations/Android` CMakeLists, plus adding the Java sources to + the host's `sourceSets`. +- Gate the build on `BABYLON_NATIVE_INTEGRATIONS_ANDROID` (default OFF). + +### Phase 4 — Apple interop layer (`Integrations/Apple/`) +- Create `Integrations/Apple/` with Obj-C++ implementations of `BNRuntime` + and `BNView` and their public Obj-C headers. +- Convert `Apps/Playground/iOS/LibNativeBridge.mm` and the macOS / + visionOS bridges to use the interop layer. +- Gate on `BABYLON_NATIVE_INTEGRATIONS_APPLE` (default OFF). + +### Phase 5 — UWP interop layer (`Integrations/Uwp/`) +- Create `Integrations/Uwp/` exposing C++/WinRT runtime classes (`.idl` + + generated projection). +- Convert `Apps/Playground/UWP/` to consume it. +- Gate on `BABYLON_NATIVE_INTEGRATIONS_UWP` (default OFF). + +### Phase 5.5 — Delete `AppContext` +- After all Playground hosts (Win32, Linux, Android, iOS, macOS, + visionOS, UWP) have been migrated to consume `Babylon::Integrations` + directly (Win32/Linux) or through their respective interop layers + (Android/Apple/UWP), delete `Apps/Playground/Shared/AppContext.{h,cpp}` + and any remaining `#include` references. +- The Playground apps then double as the simplest-possible canonical + examples of integrating `Babylon::Integrations` per platform — exactly + what we want users to copy from. + +### Phase 6 — Lifecycle and threading polish +- Audit Suspend/Resume across all platform interop layers; make sure + every platform's app-lifecycle hook is wired in the Playground app + but *not* assumed by the library. +- Document the frame-thread / JS-thread contract in + `Documentation/Components.md`. +- Add an integration test (`Apps/UnitTests/Source/Tests.Integrations.*`) + that exercises attach/detach/suspend/resume cycles without leaking + the device or the JS engine. + +### Phase 7 — Documentation +- Rewrite `Documentation/Components.md` "Getting started" to point at + `Babylon::Integrations`. +- Add per-platform integration guides under `Documentation/Integration/`: + Win32, Android, iOS, macOS, UWP, Linux. Each shows the smallest + possible host integration using the shared facade and (where + applicable) the platform's interop layer. + +## 7. Risks & Open Questions + +- **Surface swap discipline.** Constructing a `View` against a + different surface than the runtime is currently bound to triggers + `Device::UpdateWindow` (the same call `babylon-native-bridge` + makes from `surfaceChanged` — + `android/src/main/cpp/babylon.cpp:497-511`). Verify that this + cleanly handles: detach-while-no-frame-in-flight, + detach-mid-frame, and swap to a different surface mid-app. + Reference: existing start/finish discipline in + `Tests.ExternalTexture.D3D11.cpp:24-69`. +- **Shader cache directory.** `Babylon::Plugins::ShaderCache::Load/Save` + needs an OS-mandated cache path that varies per platform (Android + `Context.getCacheDir()`, iOS `NSCachesDirectory`, etc.). Add + `RuntimeOptions::shaderCachePath: std::optional` and + let the Integrations layer auto-load on `Create` and auto-save on + `Suspend` and `~Runtime`. Reference: bridge plumbing in + `BabylonNativeBridge.mm:88-106` and `babylon.cpp:242-260,378-398`. +- **JS ↔ native messaging.** Both bridges add a custom Napi + `ObjectWrap` (`LumiInterop`) exposing `callNative(jsonString)` and + `notifyReady()` to JS, plus a way to push results back to the host + (cached `jclass` + `JNIEnv` attach on Android; stored Swift block + on iOS). This is the second-most common integration need after + scene rendering. The plan currently only offers `RunOnJsThread` as + an N-API escape hatch. **Decide:** ship a typed message channel + (`Runtime::SetMessageHandler(std::function)` + + JS global `babylonNative.postMessage(string)`, JSON-string in / + out) so 90% of consumers don't have to write Napi. +- **Plugin granularity.** Some plugins have implicit JS dependencies + (e.g., `NativeXr` expects WebXR shims to exist). The setup function + needs to auto-skip dependent setup steps when their plugin is + disabled. +- **Headless / external-texture mode.** Hosts that render to an + external texture (no swap chain) currently use `BackBufferColor` / + `UpdateBackBuffer` directly on `Graphics::Device`. The simplified + facade may need a `View::AttachExternalTexture(...)` variant, or a + separate `Runtime::SetExternalTexture(...)` path with no `View` + attached. +- **Android-specific lifecycle entries on the interop layer.** The + Android JNI surface needs `setCurrentActivity(Activity)` and + `activityOnRequestPermissionsResult(...)` to feed + `AndroidExtensions/Globals.h` (consumed by `NativeCamera` etc.). + These don't belong on cross-platform `Runtime`/`View`; document + them as part of the Android interop layer's surface, alongside + the cross-platform mirror. +- **`RenderFrame` from a non-paint draw callback.** On Android, + `View.onDraw` is intended for `Canvas`-based 2D drawing; using it + as a generic frame tick may not be the idiomatic choice for + Vulkan/GLES rendering against a `SurfaceView`. Document + `SurfaceView` + a host-owned render thread as a supported pattern + alongside `onDraw`. The `babylon-native-bridge` Android demo uses + the `onDraw` + `invalidate()` pattern successfully + (`BabylonView.java:162-169`). +- **macOS without an interop layer.** A pure-C++ macOS host (rare, + but the unit-test app is one) should still be able to use + `Babylon::Integrations` directly with a `CAMetalLayer*`. Confirm this + works without pulling in the Obj-C interop layer. + +## 8. Out of Scope + +- Idiomatic high-level wrappers in any host language (no Kotlin + `BabylonView` `@Composable`, no Swift `BabylonScene` value type, no + managed .NET `BabylonControl`, no Rust `Scene` safe wrapper). +- A flat C ABI / FFI surface. Each platform talks to `Babylon::Integrations` + through its own native interop ABI. +- Precompiled "everything" artifacts published to package registries + (Maven Central, CocoaPods, NuGet, crates.io). Source-build via + CMake remains the only distribution channel. +- Replacing or deprecating the existing component-level API. +- Changes to the JavaScript-facing Babylon.js contract. From 286b40c8ed84970a20a3deaa7893706434375084 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 15:48:15 -0700 Subject: [PATCH 02/71] Handle mouse input --- .../Include/Babylon/Integrations/View.h | 39 ++++++++++++---- Integrations/Source/View.cpp | 46 +++++++++++++++++++ 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h index b696c12f3..7e51f47fb 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -74,22 +74,45 @@ namespace Babylon::Integrations void Resize(uint32_t width, uint32_t height, float devicePixelRatio = 1.0f); #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - // ----- Pointer input forwarding ----- + // ----- Pointer / mouse input forwarding ----- // // Host calls these from its event loop while the view exists. - // Routed to the JS thread via `NativeInput::Touch*`. Coordinates - // are in logical pixels (same convention as Resize). + // Routed to the JS thread via `NativeInput`. Coordinates are in + // logical pixels (same convention as Resize). // - // Babylon Native's `NativeInput` only exposes pointer (touch / - // mouse-as-pointer) input today; keyboard input is not part of - // the public Babylon Native input contract and is therefore not - // exposed here. Hosts that need keyboard handling can do it at - // the platform level and forward into JS via Runtime::RunOnJsThread. + // Babylon Native distinguishes pointer (touch) input from mouse + // input; both methods feed the same Babylon.js pointer-event + // pipeline but with different `pointerType` ('touch' vs. + // 'mouse'). Hosts driven by touch (Android, iOS) typically use + // OnPointer*; hosts driven by a cursor (Win32, macOS, UWP, X11) + // typically use OnMouse*. + // + // Babylon Native does not currently expose keyboard input; hosts + // that need keyboard handling do it at the platform level and + // forward into JS via `Runtime::RunOnJsThread`. // // Safe to call from any thread. + + // Touch / pointer events. void OnPointerDown(int32_t pointerId, float x, float y); void OnPointerMove(int32_t pointerId, float x, float y); void OnPointerUp(int32_t pointerId, float x, float y); + + // Mouse events. `buttonIndex` is one of LeftMouseButton(), + // MiddleMouseButton(), RightMouseButton(); `wheelAxis` is + // MouseWheelY(). The accessors return the matching + // `Babylon::Plugins::NativeInput::*_ID` value (single source of + // truth — no duplication, no risk of drift) without exposing the + // NativeInput header from this public View.h. + void OnMouseDown(uint32_t buttonIndex, float x, float y); + void OnMouseUp(uint32_t buttonIndex, float x, float y); + void OnMouseMove(float x, float y); + void OnMouseWheel(uint32_t wheelAxis, int32_t scrollValue); + + static uint32_t LeftMouseButton(); + static uint32_t MiddleMouseButton(); + static uint32_t RightMouseButton(); + static uint32_t MouseWheelY(); #endif private: diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index e8a58e82b..12cd74498 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -325,5 +325,51 @@ namespace Babylon::Integrations static_cast(y)); } } + + void View::OnMouseDown(uint32_t buttonIndex, float x, float y) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->MouseDown(buttonIndex, + static_cast(x), + static_cast(y)); + } + } + + void View::OnMouseUp(uint32_t buttonIndex, float x, float y) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->MouseUp(buttonIndex, + static_cast(x), + static_cast(y)); + } + } + + void View::OnMouseMove(float x, float y) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->MouseMove(static_cast(x), + static_cast(y)); + } + } + + void View::OnMouseWheel(uint32_t wheelAxis, int32_t scrollValue) + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + if (impl.m_input) + { + impl.m_input->MouseWheel(wheelAxis, scrollValue); + } + } + + uint32_t View::LeftMouseButton() { return Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID; } + uint32_t View::MiddleMouseButton() { return Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID; } + uint32_t View::RightMouseButton() { return Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID; } + uint32_t View::MouseWheelY() { return Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID; } #endif } From 3e4c4e25a07062420fdcd646416f9e5a1c057468 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 16:53:34 -0700 Subject: [PATCH 03/71] Unified log/error callback --- .../Include/Babylon/Integrations/LogLevel.h | 15 ++++++++++--- .../Babylon/Integrations/RuntimeOptions.h | 22 +++++++++++++------ Integrations/Source/Runtime.cpp | 13 ++++++----- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Integrations/Include/Babylon/Integrations/LogLevel.h b/Integrations/Include/Babylon/Integrations/LogLevel.h index a6f28cf39..447f3c475 100644 --- a/Integrations/Include/Babylon/Integrations/LogLevel.h +++ b/Integrations/Include/Babylon/Integrations/LogLevel.h @@ -3,13 +3,22 @@ namespace Babylon::Integrations { // Severity levels for the optional log callback on RuntimeOptions. - // Mirrors the levels used by `Babylon::Polyfills::Console::LogLevel` - // but is exposed as its own enum so consumers don't have to depend - // on the Console polyfill header just to read log output. + // + // The first three (Log / Warn / Error) mirror + // `Babylon::Polyfills::Console::LogLevel` and are used for + // `console.log` / `console.warn` / `console.error` calls and for + // `Babylon::DebugTrace` output. + // + // `Fatal` is used for **uncaught** JavaScript exceptions that + // propagated past every JS-side handler. The engine state may be + // inconsistent after a Fatal; a host that wants to terminate the + // process on uncaught errors can do so from inside its log + // callback (e.g. `if (level == LogLevel::Fatal) std::quick_exit(1);`). enum class LogLevel { Log, Warn, Error, + Fatal, }; } diff --git a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Babylon/Integrations/RuntimeOptions.h index 7b951a9f2..327fad99b 100644 --- a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Babylon/Integrations/RuntimeOptions.h @@ -22,13 +22,21 @@ namespace Babylon::Integrations // implemented for V8. bool waitForDebugger{false}; - // Optional log sink. If unset, log output is discarded. Wired into - // both `Babylon::DebugTrace` and the Console polyfill. + // Optional log sink. Receives: + // - `console.{log,warn,error}` output → LogLevel::{Log,Warn,Error} + // - `Babylon::DebugTrace` output → LogLevel::Log + // - Uncaught JS exceptions → LogLevel::Fatal + // + // If unset, ordinary log output is silently discarded and + // uncaught exceptions fall back to + // `Babylon::AppRuntime::DefaultUnhandledExceptionHandler` + // (which writes to the program output). + // + // Hosts that want process termination on uncaught exceptions + // (matching the historical AppContext behavior) can do so from + // inside this callback, e.g. + // + // if (level == LogLevel::Fatal) std::quick_exit(1); std::function log; - - // Optional handler for unhandled JavaScript exceptions. If unset, - // `Babylon::AppRuntime::DefaultUnhandledExceptionHandler` is used - // (which writes the error to the program output). - std::function onUnhandledError; }; } diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index f8ea21be3..e5ee0fa1f 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -21,7 +21,6 @@ namespace Babylon::Integrations if (m_options.log) { Babylon::DebugTrace::EnableDebugTrace(true); - // DebugTrace doesn't carry a level; treat it as Log. const auto& logCallback = m_options.log; Babylon::DebugTrace::SetTraceOutput([logCallback](const char* message) { logCallback(LogLevel::Log, message ? message : ""); @@ -34,13 +33,17 @@ namespace Babylon::Integrations Babylon::AppRuntime::Options appRuntimeOptions{}; appRuntimeOptions.EnableDebugger = m_options.enableDebugger; appRuntimeOptions.WaitForDebugger = m_options.waitForDebugger; - if (m_options.onUnhandledError) + + // Route uncaught JS exceptions through the host's log callback + // with LogLevel::Fatal. If no log callback is set, leave the + // AppRuntime default in place (writes to program output). + if (m_options.log) { - const auto& userHandler = m_options.onUnhandledError; - appRuntimeOptions.UnhandledExceptionHandler = [userHandler](const Napi::Error& error) { + const auto& logCallback = m_options.log; + appRuntimeOptions.UnhandledExceptionHandler = [logCallback](const Napi::Error& error) { std::ostringstream ss{}; ss << "[Uncaught Error] " << Napi::GetErrorString(error); - userHandler(ss.str()); + logCallback(LogLevel::Fatal, ss.str()); }; } From 722dadfa435dcfcb03c33523e5f48e9b985ce07f Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 16:53:57 -0700 Subject: [PATCH 04/71] Use Integration layer in Playground win32 --- Apps/Playground/CMakeLists.txt | 5 +- Apps/Playground/Shared/PlaygroundScripts.cpp | 27 +++ Apps/Playground/Shared/PlaygroundScripts.h | 35 +++ Apps/Playground/Win32/App.cpp | 228 ++++++++++--------- 4 files changed, 192 insertions(+), 103 deletions(-) create mode 100644 Apps/Playground/Shared/PlaygroundScripts.cpp create mode 100644 Apps/Playground/Shared/PlaygroundScripts.h diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 9c003c527..da2be9518 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -20,7 +20,9 @@ set(SCRIPTS set(SOURCES "Shared/AppContext.cpp" - "Shared/AppContext.h") + "Shared/AppContext.h" + "Shared/PlaygroundScripts.cpp" + "Shared/PlaygroundScripts.h") if(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) @@ -126,6 +128,7 @@ target_link_libraries(Playground PRIVATE Console PRIVATE ExternalTexture PRIVATE GraphicsDevice + PRIVATE Integrations PRIVATE NativeCamera PRIVATE NativeCapture PRIVATE NativeEncoding diff --git a/Apps/Playground/Shared/PlaygroundScripts.cpp b/Apps/Playground/Shared/PlaygroundScripts.cpp new file mode 100644 index 000000000..e40784af9 --- /dev/null +++ b/Apps/Playground/Shared/PlaygroundScripts.cpp @@ -0,0 +1,27 @@ +#include "PlaygroundScripts.h" + +#include +#include + +namespace Playground +{ + void Initialize() + { + // Process-wide perf-tracing configuration. Used to be done + // inside AppContext's constructor. + Babylon::PerfTrace::SetLevel(Babylon::PerfTrace::Level::Mark); + } + + void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime) + { + runtime.LoadScript("app:///Scripts/ammo.js"); + // Commenting out recast.js for now because v8jsi is incompatible with asm.js. + // runtime.LoadScript("app:///Scripts/recast.js"); + runtime.LoadScript("app:///Scripts/babylon.max.js"); + runtime.LoadScript("app:///Scripts/babylonjs.loaders.js"); + runtime.LoadScript("app:///Scripts/babylonjs.materials.js"); + runtime.LoadScript("app:///Scripts/babylon.gui.js"); + runtime.LoadScript("app:///Scripts/meshwriter.min.js"); + runtime.LoadScript("app:///Scripts/babylonjs.serializers.js"); + } +} diff --git a/Apps/Playground/Shared/PlaygroundScripts.h b/Apps/Playground/Shared/PlaygroundScripts.h new file mode 100644 index 000000000..beacc04ce --- /dev/null +++ b/Apps/Playground/Shared/PlaygroundScripts.h @@ -0,0 +1,35 @@ +#pragma once + +namespace Babylon::Integrations +{ + class Runtime; +} + +namespace Playground +{ + // Apply process-wide settings shared by every Playground host: + // currently `Babylon::PerfTrace::SetLevel(Mark)`. Call once per + // process, before constructing any `Babylon::Integrations::Runtime`. + // + // (DebugTrace setup is now handled by `RuntimeOptions::log` in the + // Integrations layer, so it doesn't need to live here.) + void Initialize(); + + // Queue the standard Babylon.js bootstrap scripts (Babylon core, + // loaders, materials, GUI, serializers, plus a few common extras) + // onto `runtime` in dependency order. + // + // These were historically loaded by `AppContext`'s constructor; the + // `Babylon::Integrations` layer no longer bundles script loading + // (each host decides between the multi-UMD route this helper + // implements and a single pre-bundled `bundle.js` route — see + // SimplifiedAPI.md §4.1 "Loading Babylon.js: two supported routes"). + // We keep the list here so every Playground host stays in sync as + // the bundle list evolves. + // + // Calls to `LoadScript` made before the first `View::Attach` are + // queued on the runtime and dispatched after engine initialization + // completes; this helper relies on that, so it's safe to call + // immediately after `Runtime::Create`. + void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime); +} diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index d160d6086..2accf883a 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -1,17 +1,26 @@ // App.cpp : Defines the entry point for the application. // +// Migrated to Babylon::Integrations: this host no longer constructs +// Babylon Native components directly. The cross-platform `Runtime` + +// `View` API handles plugin/polyfill setup, GPU device construction, +// frame rendering, and input forwarding. #include "App.h" -#include -#include + +#include +#include + +#include + #include #include #include + #include #include -#include +#include #include - +#include #define MAX_LOADSTRING 100 @@ -19,9 +28,16 @@ HINSTANCE hInst; // current instance WCHAR szTitle[MAX_LOADSTRING]; // The title bar text WCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name -std::optional appContext{}; + +// Process-scoped: created on app start, recreated on 'R' refresh, +// destroyed on app exit. +std::unique_ptr g_runtime; + +// Window-scoped: created on InitInstance after CreateWindowW returns, +// destroyed on WM_DESTROY (or torn down + recreated by RefreshBabylon). +std::unique_ptr g_view; + bool minimized{false}; -int buttonRefCount{0}; // Forward declarations of functions included in this code module: ATOM MyRegisterClass(HINSTANCE hInstance); @@ -65,57 +81,83 @@ namespace return arguments; } - void Uninitialize() + Babylon::Integrations::RuntimeOptions MakeRuntimeOptions() { - appContext.reset(); + Babylon::Integrations::RuntimeOptions options{}; + options.enableDebugger = true; // matches AppContext default + options.log = [](Babylon::Integrations::LogLevel level, std::string_view message) { + std::ostringstream ss{}; + ss << message << std::endl; + OutputDebugStringA(ss.str().data()); + std::cout << ss.str(); + + // Match AppContext's historical behavior: terminate on + // uncaught JS exceptions (Fatal). Routine console.error + // calls (Error) just print and continue. + if (level == Babylon::Integrations::LogLevel::Fatal) + { + std::quick_exit(1); + } + }; + return options; } - void RefreshBabylon(HWND hWnd) + void QueueScripts() { - Uninitialize(); + // Babylon.js bootstrap (core + loaders/materials/gui/serializers). + // Shared with the other Playground hosts via Shared/PlaygroundScripts. + Playground::LoadBootstrapScripts(*g_runtime); - RECT rect; - if (!GetClientRect(hWnd, &rect)) - { - throw std::exception{"Unable to get client rect"}; - } - - auto width = static_cast(rect.right - rect.left); - auto height = static_cast(rect.bottom - rect.top); - - appContext.emplace( - hWnd, - width, - height, - [](const char* message) { - std::ostringstream ss{}; - ss << message << std::endl; - OutputDebugStringA(ss.str().data()); - std::cout << ss.str(); - }); - - std::vector args = GetCommandLineArguments(); + const auto args = GetCommandLineArguments(); if (args.empty()) { - appContext->ScriptLoader().LoadScript("app:///Scripts/experience.js"); + g_runtime->LoadScript("app:///Scripts/experience.js"); } else { for (const auto& arg : args) { - appContext->ScriptLoader().LoadScript(GetUrlFromPath(arg)); + g_runtime->LoadScript(GetUrlFromPath(arg)); } - - appContext->ScriptLoader().LoadScript("app:///Scripts/playground_runner.js"); + g_runtime->LoadScript("app:///Scripts/playground_runner.js"); } } - void UpdateWindowSize(size_t width, size_t height) + Babylon::Integrations::ViewDescriptor DescribeWindow(HWND hWnd) { - if (appContext) + RECT rect; + if (!GetClientRect(hWnd, &rect)) { - appContext->Device().UpdateSize(width, height); + throw std::exception{"Unable to get client rect"}; } + Babylon::Integrations::ViewDescriptor descriptor{}; + descriptor.nativeWindow = static_cast(hWnd); + descriptor.width = static_cast(rect.right - rect.left); + descriptor.height = static_cast(rect.bottom - rect.top); + descriptor.devicePixelRatio = 1.0f; + return descriptor; + } + + void Uninitialize() + { + // Destroy in reverse-construction order: View first (so the + // surface is unbound and the in-flight frame is closed), then + // Runtime (which joins the JS thread). + g_view.reset(); + g_runtime.reset(); + } + + void RefreshBabylon(HWND hWnd) + { + Uninitialize(); + + g_runtime = Babylon::Integrations::Runtime::Create(MakeRuntimeOptions()); + QueueScripts(); + + // First View::Attach triggers GPU device construction, plugin + // initialization on the JS thread, and flushes the queued + // scripts. + g_view = Babylon::Integrations::View::Attach(*g_runtime, DescribeWindow(hWnd)); } } @@ -127,6 +169,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); + // Process-wide Playground setup (PerfTrace level, etc.). Shared + // with the other Playground hosts via Shared/PlaygroundScripts. + Playground::Initialize(); + // Initialize global strings LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadStringW(hInstance, IDC_PLAYGROUNDWIN32, szWindowClass, MAX_LOADSTRING); @@ -142,7 +188,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, MSG msg{}; - // Main message loop: + // Main message loop. When minimized, block on GetMessage to avoid + // spinning the CPU. Otherwise, peek + render one frame per loop + // iteration; View::RenderFrame is a no-op while suspended so we + // don't need to special-case that here. while (msg.message != WM_QUIT) { BOOL result; @@ -153,14 +202,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, } else { - if (appContext) + if (g_view) { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); + g_view->RenderFrame(); } - result = PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) && msg.message != WM_QUIT; } @@ -177,11 +222,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, return (int)msg.wParam; } -// -// FUNCTION: MyRegisterClass() -// -// PURPOSE: Registers the window class. -// ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEXW wcex; @@ -203,19 +243,9 @@ ATOM MyRegisterClass(HINSTANCE hInstance) return RegisterClassExW(&wcex); } -// -// FUNCTION: InitInstance(HINSTANCE, int) -// -// PURPOSE: Saves instance handle and creates main window -// -// COMMENTS: -// -// In this function, we save the instance handle in a global variable and -// create and display the main program window. -// BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { - hInst = hInstance; // Store instance handle in our global variable + hInst = hInstance; HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr); @@ -236,55 +266,46 @@ BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) void ProcessMouseButtons(tagPOINTER_BUTTON_CHANGE_TYPE changeType, int x, int y) { + using View = Babylon::Integrations::View; + if (!g_view) return; + switch (changeType) { case POINTER_CHANGE_FIRSTBUTTON_DOWN: - appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, x, y); + g_view->OnMouseDown(View::LeftMouseButton(), static_cast(x), static_cast(y)); break; case POINTER_CHANGE_FIRSTBUTTON_UP: - appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, x, y); + g_view->OnMouseUp(View::LeftMouseButton(), static_cast(x), static_cast(y)); break; case POINTER_CHANGE_SECONDBUTTON_DOWN: - appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, x, y); + g_view->OnMouseDown(View::RightMouseButton(), static_cast(x), static_cast(y)); break; case POINTER_CHANGE_SECONDBUTTON_UP: - appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, x, y); + g_view->OnMouseUp(View::RightMouseButton(), static_cast(x), static_cast(y)); break; case POINTER_CHANGE_THIRDBUTTON_DOWN: - appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, x, y); + g_view->OnMouseDown(View::MiddleMouseButton(), static_cast(x), static_cast(y)); break; case POINTER_CHANGE_THIRDBUTTON_UP: - appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, x, y); + g_view->OnMouseUp(View::MiddleMouseButton(), static_cast(x), static_cast(y)); break; } } -// -// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM) -// -// PURPOSE: Processes messages for the main window. -// -// WM_COMMAND - process the application menu -// WM_PAINT - Paint the main window -// WM_DESTROY - post a quit message and return -// -// LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + using View = Babylon::Integrations::View; + switch (message) { case WM_SYSCOMMAND: { if ((wParam & 0xFFF0) == SC_MINIMIZE) { - if (appContext) + if (g_runtime) { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - - appContext->Runtime().Suspend(); + g_runtime->Suspend(); } - minimized = true; } else if ((wParam & 0xFFF0) == SC_RESTORE) @@ -292,13 +313,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (minimized) { minimized = false; - - if (appContext) + if (g_runtime) { - appContext->Runtime().Resume(); - - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); + g_runtime->Resume(); } } } @@ -308,7 +325,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_COMMAND: { int wmId = LOWORD(wParam); - // Parse the menu selections: switch (wmId) { case IDM_ABOUT: @@ -324,9 +340,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } case WM_SIZE: { - auto width = static_cast(LOWORD(lParam)); - auto height = static_cast(HIWORD(lParam)); - UpdateWindowSize(width, height); + if (g_view) + { + g_view->Resize(static_cast(LOWORD(lParam)), + static_cast(HIWORD(lParam))); + } break; } case WM_DESTROY: @@ -345,15 +363,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } case WM_POINTERWHEEL: { - if (appContext && appContext->Input()) + if (g_view) { - appContext->Input()->MouseWheel(Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID, -GET_WHEEL_DELTA_WPARAM(wParam)); + g_view->OnMouseWheel(View::MouseWheelY(), -GET_WHEEL_DELTA_WPARAM(wParam)); } break; } case WM_POINTERDOWN: { - if (appContext && appContext->Input()) + if (g_view) { POINTER_INFO info; auto pointerId = GET_POINTERID_WPARAM(wParam); @@ -370,7 +388,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } else { - appContext->Input()->TouchDown(pointerId, x, y); + g_view->OnPointerDown(static_cast(pointerId), + static_cast(x), + static_cast(y)); } } } @@ -378,7 +398,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } case WM_POINTERUPDATE: { - if (appContext && appContext->Input()) + if (g_view) { auto pointerId = GET_POINTERID_WPARAM(wParam); POINTER_INFO info; @@ -392,11 +412,13 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (info.pointerType == PT_MOUSE) { ProcessMouseButtons(info.ButtonChangeType, x, y); - appContext->Input()->MouseMove(x, y); + g_view->OnMouseMove(static_cast(x), static_cast(y)); } else { - appContext->Input()->TouchMove(pointerId, x, y); + g_view->OnPointerMove(static_cast(pointerId), + static_cast(x), + static_cast(y)); } } } @@ -404,7 +426,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } case WM_POINTERUP: { - if (appContext && appContext->Input()) + if (g_view) { auto pointerId = GET_POINTERID_WPARAM(wParam); POINTER_INFO info; @@ -421,7 +443,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } else { - appContext->Input()->TouchUp(pointerId, x, y); + g_view->OnPointerUp(static_cast(pointerId), + static_cast(x), + static_cast(y)); } } } From 23c8ec3d6ed91e03e5645c589c88efa525c4f2ce Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 18:22:35 -0700 Subject: [PATCH 05/71] Add NativeXR support --- .../main/cpp/BabylonNativeIntegrations.cpp | 22 +++++++++++++++ Integrations/CMakeLists.txt | 13 +++++---- .../Include/Babylon/Integrations/Runtime.h | 23 ++++++++++++++++ Integrations/Source/Runtime.cpp | 27 +++++++++++++++++++ Integrations/Source/RuntimeImpl.h | 18 +++++++++++++ Integrations/Source/View.cpp | 17 ++++++++++++ 6 files changed, 115 insertions(+), 5 deletions(-) diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 5934bdb39..7989b0363 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -201,6 +201,28 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeIsSuspended(JNIEnv*, jclass return AsRuntime(handle)->IsSuspended() ? JNI_TRUE : JNI_FALSE; } +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + +JNIEXPORT void JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeSetXrSurface( + JNIEnv* env, jclass, jlong handle, jobject surface) +{ + ANativeWindow* window{nullptr}; + if (surface != nullptr) + { + window = ANativeWindow_fromSurface(env, surface); + } + AsRuntime(handle)->SetXrWindow(window); +} + +JNIEXPORT jboolean JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, jlong handle) +{ + return AsRuntime(handle)->IsXrActive() ? JNI_TRUE : JNI_FALSE; +} + +#endif // BABYLON_NATIVE_PLUGIN_NATIVEXR + // ===================================================================== // View // ===================================================================== diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index b926f0f6d..9ada2aab2 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -82,12 +82,15 @@ if(BABYLON_NATIVE_PLUGIN_NATIVETRACING) target_link_libraries(Integrations PRIVATE NativeTracing) endif() -if(BABYLON_NATIVE_PLUGIN_NATIVEXR) - # Currently exposes no public API surface in the Integrations layer - # (XR-specific helpers are a future addition — see SimplifiedAPI.md - # §7 Risks). The flag is still propagated as a compile definition so - # future XR-gated methods on Runtime/View are picked up consistently. +if(BABYLON_NATIVE_PLUGIN_NATIVEXR AND TARGET NativeXr) + # Public surface: Runtime::SetXrWindow / IsXrActive when this flag + # is set. RuntimeImpl.h includes , but + # that's an internal header so the dependency stays PRIVATE here. + # NativeXr is only compiled on Android/iOS (see Plugins/CMakeLists.txt), + # so we additionally check `TARGET NativeXr` to skip XR support on + # platforms where the plugin isn't built even if the flag is ON. target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEXR=1) + target_link_libraries(Integrations PRIVATE NativeXr) endif() if(BABYLON_NATIVE_PLUGIN_SHADERCACHE) diff --git a/Integrations/Include/Babylon/Integrations/Runtime.h b/Integrations/Include/Babylon/Integrations/Runtime.h index c259c8d98..06123dd24 100644 --- a/Integrations/Include/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Babylon/Integrations/Runtime.h @@ -88,6 +88,29 @@ namespace Babylon::Integrations void Resume(); bool IsSuspended() const; +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // ----- XR session control ----- + // + // Set the platform window XR will render into. The `void*` + // type carries: + // Android : ANativeWindow* (typically from a separate + // transparent SurfaceView overlay) + // Apple : CAMetalLayer* / MTKView* (a separate Metal layer + // distinct from the main View's layer) + // + // Pass nullptr to clear the XR surface. Safe to call before + // the first `View::Attach`; the supplied window is applied + // when NativeXr finishes initializing during that first Attach. + // Safe to call from any thread. + void SetXrWindow(void* nativeWindow); + + // True while an XR session is active. Updated from the JS + // thread by NativeXr's internal session-state callback; + // atomic so it can be polled from any thread (e.g. a host's + // draw callback choosing between rendering targets). + bool IsXrActive() const; +#endif + private: friend class View; diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index e5ee0fa1f..5a3618d0a 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -94,6 +94,13 @@ namespace Babylon::Integrations m_input = nullptr; #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // NativeXr holds JS-thread-bound resources and a strong ref to + // the Napi::Env it was initialized with. Destroy it before the + // AppRuntime joins the JS thread; same reason as ScriptLoader. + m_nativeXr.reset(); +#endif + m_appRuntime.reset(); #if BABYLON_NATIVE_PLUGIN_SHADERCACHE @@ -210,4 +217,24 @@ namespace Babylon::Integrations std::lock_guard lock{m_impl->m_suspendMutex}; return m_impl->m_suspendCount > 0; } + +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + void Runtime::SetXrWindow(void* nativeWindow) + { + std::lock_guard lock{m_impl->m_xrMutex}; + m_impl->m_xrWindow = nativeWindow; + if (m_impl->m_nativeXr) + { + m_impl->m_nativeXr->UpdateWindow(nativeWindow); + } + // If NativeXr isn't initialized yet (no View::Attach has + // happened), the value is stashed in m_xrWindow and applied + // by the first-Attach init lambda when it constructs NativeXr. + } + + bool Runtime::IsXrActive() const + { + return m_impl->m_isXrActive.load(std::memory_order_relaxed); + } +#endif } diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 8fd698abc..b8d6f9962 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -15,6 +15,10 @@ #include #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEXR +#include +#endif + #include #include #include @@ -55,6 +59,20 @@ namespace Babylon::Integrations Babylon::Plugins::NativeInput* m_input{nullptr}; #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // NativeXr is initialized during the first View::Attach (it + // needs a Napi::Env). Until then, m_xrWindow holds the host's + // most recent SetXrWindow value so we can apply it as soon as + // the plugin is alive. m_xrMutex serializes both fields plus + // the optional itself. m_isXrActive is updated from the JS + // thread by NativeXr's session-state callback and read from + // any thread by IsXrActive() polling. + std::optional m_nativeXr; + void* m_xrWindow{nullptr}; + mutable std::mutex m_xrMutex; + std::atomic m_isXrActive{false}; +#endif + // ----- Pre-init queueing ------ // // Before the first View::Attach completes engine initialization diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 12cd74498..3446833b7 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -151,6 +151,23 @@ namespace Babylon::Integrations #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT implPtr->m_input = &Babylon::Plugins::NativeInput::CreateForJavaScript(env); #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // Initialize NativeXr; apply any pending xr window the host + // may have already supplied via Runtime::SetXrWindow; wire + // the session-state callback to keep m_isXrActive in sync. + { + std::lock_guard xrLock{implPtr->m_xrMutex}; + implPtr->m_nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(env)); + if (implPtr->m_xrWindow) + { + implPtr->m_nativeXr->UpdateWindow(implPtr->m_xrWindow); + } + implPtr->m_nativeXr->SetSessionStateChangedCallback( + [implPtr](bool isActive) { + implPtr->m_isXrActive.store(isActive, std::memory_order_relaxed); + }); + } +#endif #if BABYLON_NATIVE_PLUGIN_TESTUTILS Babylon::Plugins::TestUtils::Initialize(env, window); #else From 8c3436390a3d650e9496e0379747147bf4291619 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 18:39:00 -0700 Subject: [PATCH 06/71] Use WindowT --- .../main/cpp/BabylonNativeIntegrations.cpp | 2 +- Integrations/Apple/Source/BNView.mm | 2 +- Integrations/CMakeLists.txt | 4 ++- .../Babylon/Integrations/ViewDescriptor.h | 27 ++++++++++------ Integrations/Source/View.cpp | 31 +------------------ 5 files changed, 23 insertions(+), 43 deletions(-) diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 7989b0363..70ce0349d 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -68,7 +68,7 @@ namespace ViewDescriptor MakeViewDescriptor(ANativeWindow* window, jint physicalW, jint physicalH, jfloat density) { ViewDescriptor descriptor{}; - descriptor.nativeWindow = static_cast(window); + descriptor.nativeWindow = window; descriptor.width = static_cast(static_cast(physicalW) / density); descriptor.height = static_cast(static_cast(physicalH) / density); descriptor.devicePixelRatio = density; diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index fdf02b452..fd708c958 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -20,7 +20,7 @@ { const CGFloat scale = layer.contentsScale > 0 ? layer.contentsScale : 1.0; Babylon::Integrations::ViewDescriptor descriptor{}; - descriptor.nativeWindow = (__bridge void*)layer; + descriptor.nativeWindow = (__bridge CA::MetalLayer*)layer; descriptor.width = static_cast(layer.drawableSize.width / scale); descriptor.height = static_cast(layer.drawableSize.height / scale); descriptor.devicePixelRatio = static_cast(scale); diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index 9ada2aab2..859aa3655 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -20,9 +20,11 @@ target_include_directories(Integrations PUBLIC "Include") # XMLHttpRequest) are always linked. target_link_libraries(Integrations PUBLIC napi + # GraphicsDevice is PUBLIC because ViewDescriptor.h (a public header) + # references `Babylon::Graphics::WindowT` from . + PUBLIC GraphicsDevice PRIVATE AppRuntime PRIVATE ScriptLoader - PRIVATE GraphicsDevice PRIVATE Blob PRIVATE Console PRIVATE Performance diff --git a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h index c4c77c8ff..eb07029b4 100644 --- a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h +++ b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace Babylon::Integrations @@ -7,22 +9,27 @@ namespace Babylon::Integrations // Description of a platform surface that a `View` will render // into. Populated by the platform interop layer (or directly by a // C++ host on Win32 / Linux) from whatever native object the - // host's UI framework provides: + // host's UI framework provides. + // + // `nativeWindow` is `Babylon::Graphics::WindowT`, which is the + // platform-specific native window handle that `Babylon::Graphics` + // already understands: // - // Win32 : HWND - // Linux (X11) : Window (X11 XID; reinterpret_cast through void*) - // Android : ANativeWindow* - // iOS / macOS / visionOS : CAMetalLayer* - // UWP : IInspectable* (e.g. SwapChainPanel) + // Win32 : HWND + // Android : ANativeWindow* + // iOS / macOS / visionOS : CA::MetalLayer* (metal-cpp wrapper) + // X11 (Linux) : Window (X11 XID — `unsigned long`) + // UWP / WinRT : winrt::Windows::Foundation::IInspectable + // (e.g. ICoreWindow, ISwapChainPanel) // // `width` and `height` are in **logical pixels**; `devicePixelRatio` // is the physical-to-logical ratio (e.g. 2.0 for a Retina display). - // The platform interop layer (Integrations/Android, Integrations/Apple, - // Integrations/Uwp) is responsible for converting whatever its UI - // framework hands the host into this convention. + // The platform interop layer (Integrations/Android, Integrations/Apple) + // is responsible for converting whatever its UI framework hands the + // host into this convention. struct ViewDescriptor { - void* nativeWindow{nullptr}; + Babylon::Graphics::WindowT nativeWindow{}; uint32_t width{0}; uint32_t height{0}; float devicePixelRatio{1.0f}; diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 3446833b7..33677ca20 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -55,35 +55,6 @@ namespace Babylon::Integrations } return LogLevel::Log; } - - // Reinterpret the platform-erased `void*` from ViewDescriptor as the - // platform's Babylon::Graphics::WindowT. WindowT varies by - // platform: - // - // Win32 : HWND (pointer) - // Android : ANativeWindow* (pointer) - // Apple : CA::MetalLayer* (pointer; metal-cpp wrapper) - // X11 (Linux) : Window (XID — `unsigned long`) - // UWP / WinRT : winrt::Windows::Foundation::IInspectable - // (a value type wrapping a refcounted COM pointer) - // - // For UWP we reconstruct the wrapper from the ABI pointer the - // host stuffed in (typically via `winrt::get_abi(...)`); for - // every other platform a single `reinterpret_cast` covers - // pointer-to-pointer and void*-to-XID. - Babylon::Graphics::WindowT ToWindowT(void* nativeWindow) - { -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) - Babylon::Graphics::WindowT result{nullptr}; - if (nativeWindow != nullptr) - { - winrt::copy_from_abi(result, nativeWindow); - } - return result; -#else - return reinterpret_cast(nativeWindow); -#endif - } } // --------------------------------------------------------------------- @@ -204,7 +175,7 @@ namespace Babylon::Integrations return nullptr; } - const auto window = ToWindowT(descriptor.nativeWindow); + const auto& window = descriptor.nativeWindow; if (!impl.m_device) { From 1b1ae7d078d38f4d42913f8d632eb92bd362a416 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 18:39:36 -0700 Subject: [PATCH 07/71] Use WindowT --- Apps/Playground/Win32/App.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 2accf883a..5470a3d0c 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -131,7 +131,7 @@ namespace throw std::exception{"Unable to get client rect"}; } Babylon::Integrations::ViewDescriptor descriptor{}; - descriptor.nativeWindow = static_cast(hWnd); + descriptor.nativeWindow = hWnd; descriptor.width = static_cast(rect.right - rect.left); descriptor.height = static_cast(rect.bottom - rect.top); descriptor.devicePixelRatio = 1.0f; From c24d053520699615aa6705d2622fbf59dabe4986 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 30 Apr 2026 18:56:01 -0700 Subject: [PATCH 08/71] Use arcana task_completion_source --- .../Include/Babylon/Integrations/Runtime.h | 7 +- Integrations/Source/Runtime.cpp | 72 +++++-------------- Integrations/Source/RuntimeImpl.h | 30 ++++---- Integrations/Source/View.cpp | 32 ++++----- 4 files changed, 56 insertions(+), 85 deletions(-) diff --git a/Integrations/Include/Babylon/Integrations/Runtime.h b/Integrations/Include/Babylon/Integrations/Runtime.h index 06123dd24..6f9eb1fb8 100644 --- a/Integrations/Include/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Babylon/Integrations/Runtime.h @@ -58,7 +58,10 @@ namespace Babylon::Integrations // completes during that first Attach. Calls made after the first // Attach are dispatched immediately. // - // Safe to call from any thread. + // Threading: these methods are NOT internally synchronized. + // Hosts should call them from a single thread (typically the + // host's UI/main thread), matching the existing contract of + // `Babylon::ScriptLoader` and `Babylon::AppRuntime::Dispatch`. void LoadScript(std::string_view url); void Eval(std::string_view source, std::string_view sourceUrl = {}); @@ -67,7 +70,7 @@ namespace Babylon::Integrations // custom Napi globals, registering ObjectWrap classes, capturing // `Napi::FunctionReference`s for native→JS calls, etc. // - // Safe to call from any thread. + // Threading: same single-thread contract as LoadScript / Eval. void RunOnJsThread(std::function callback); // ----- Suspend / Resume ----- diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 5a3618d0a..0b401f1f7 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -61,12 +61,6 @@ namespace Babylon::Integrations // ordering: destroy Views before destroying their Runtime. assert(m_currentView == nullptr && "View must be destroyed before its Runtime."); - // Discard any pending pre-init actions; we're tearing down. - { - std::lock_guard lock{m_pendingMutex}; - m_pending.clear(); - } - // Order matters here: // 1. ScriptLoader's dispatcher captures &m_appRuntime, so // ~ScriptLoader must run before ~AppRuntime. @@ -84,6 +78,11 @@ namespace Babylon::Integrations // View::Attach calls on first attach. // 5. Device + DeviceUpdate destroyed last because the JS // thread referenced them via Device::AddToJavaScript. + // + // m_initTcs is destroyed when this struct's members are + // destroyed. If complete() was never called (no View ever + // attached), the queued continuations are dropped without + // firing, which is the desired behavior on shutdown. m_scriptLoader.reset(); #if BABYLON_NATIVE_POLYFILL_CANVAS @@ -127,43 +126,20 @@ namespace Babylon::Integrations void Runtime::LoadScript(std::string_view url) { - std::string urlCopy{url}; - - std::lock_guard lock{m_impl->m_pendingMutex}; - if (!m_impl->m_initialized) - { - // Capture by value into a callable that the first-Attach - // init lambda will invoke after plugin init completes. - m_impl->m_pending.emplace_back( - [scriptLoader = &*m_impl->m_scriptLoader, url = std::move(urlCopy)]() mutable { - scriptLoader->LoadScript(std::move(url)); - }); - return; - } - - // Past init: dispatch directly. The scriptLoader serializes on - // the JS thread. - m_impl->m_scriptLoader->LoadScript(std::move(urlCopy)); + m_impl->m_initTcs.as_task().then(arcana::inline_scheduler, arcana::cancellation::none(), + [scriptLoader = &*m_impl->m_scriptLoader, url = std::string{url}]() mutable { + scriptLoader->LoadScript(std::move(url)); + }); } void Runtime::Eval(std::string_view source, std::string_view sourceUrl) { - std::string sourceCopy{source}; - std::string urlCopy{sourceUrl}; - - std::lock_guard lock{m_impl->m_pendingMutex}; - if (!m_impl->m_initialized) - { - m_impl->m_pending.emplace_back( - [scriptLoader = &*m_impl->m_scriptLoader, - src = std::move(sourceCopy), - url = std::move(urlCopy)]() mutable { - scriptLoader->Eval(std::move(src), std::move(url)); - }); - return; - } - - m_impl->m_scriptLoader->Eval(std::move(sourceCopy), std::move(urlCopy)); + m_impl->m_initTcs.as_task().then(arcana::inline_scheduler, arcana::cancellation::none(), + [scriptLoader = &*m_impl->m_scriptLoader, + source = std::string{source}, + url = std::string{sourceUrl}]() mutable { + scriptLoader->Eval(std::move(source), std::move(url)); + }); } void Runtime::RunOnJsThread(std::function callback) @@ -173,20 +149,10 @@ namespace Babylon::Integrations return; } - std::lock_guard lock{m_impl->m_pendingMutex}; - if (!m_impl->m_initialized) - { - // Defer through ScriptLoader so it stays serialized with - // any LoadScript / Eval calls the host queued before us. - m_impl->m_pending.emplace_back( - [scriptLoader = &*m_impl->m_scriptLoader, - cb = std::move(callback)]() mutable { - scriptLoader->Dispatch(std::move(cb)); - }); - return; - } - - m_impl->m_scriptLoader->Dispatch(std::move(callback)); + m_impl->m_initTcs.as_task().then(arcana::inline_scheduler, arcana::cancellation::none(), + [scriptLoader = &*m_impl->m_scriptLoader, cb = std::move(callback)]() mutable { + scriptLoader->Dispatch(std::move(cb)); + }); } void Runtime::Suspend() diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index b8d6f9962..f4919358b 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -19,11 +19,11 @@ #include #endif +#include + #include -#include #include #include -#include namespace Babylon::Integrations { @@ -73,17 +73,23 @@ namespace Babylon::Integrations std::atomic m_isXrActive{false}; #endif - // ----- Pre-init queueing ------ + // ----- Pre-init queueing ----- + // + // Host calls to LoadScript / Eval / RunOnJsThread are chained + // off `m_initTcs.as_task().then(inline_scheduler, ...)`. While + // the TCS is uncompleted (i.e. the first View::Attach hasn't + // finished plugin initialization on the JS thread), continuations + // sit on the task payload. The first-Attach init lambda calls + // `m_initTcs.complete()` after all plugins are initialized, + // which fires every queued continuation in registration order + // on the JS thread. After completion, subsequent + // `.then(inline_scheduler, ...)` calls run their callable + // synchronously on the calling thread, which then submits to + // ScriptLoader directly. // - // Before the first View::Attach completes engine initialization - // on the JS thread, LoadScript / Eval / RunOnJsThread calls are - // recorded here and flushed (in submission order) inside the - // first-Attach init lambda after all plugin Initialize() calls. - // After flush, m_initialized is true and subsequent calls - // dispatch directly through ScriptLoader / AppRuntime. - bool m_initialized{false}; - std::vector> m_pending; - std::mutex m_pendingMutex; + // Host-side serialization is the host's responsibility, matching + // ScriptLoader's existing contract; we do not add an outer mutex. + arcana::task_completion_source m_initTcs; // Reference-counted Suspend/Resume. int m_suspendCount{0}; diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 33677ca20..e0b79c649 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -61,11 +61,13 @@ namespace Babylon::Integrations // First-Attach engine initialization: dispatched onto the JS thread by // the first View::Attach call. Runs all plugin/polyfill Initialize() // calls in the same order as Apps/Playground/Shared/AppContext.cpp, - // then flushes any LoadScript / Eval / RunOnJsThread calls the host - // queued before the first Attach. + // then completes m_initTcs to unblock any LoadScript / Eval / + // RunOnJsThread calls the host queued before the first Attach. // - // After this lambda returns, m_initialized is true and subsequent - // Runtime::LoadScript / Eval / RunOnJsThread calls dispatch directly. + // After m_initTcs is complete, subsequent host calls to + // Runtime::LoadScript / Eval / RunOnJsThread fire their continuation + // synchronously on the calling thread (via inline_scheduler), which + // then submits to ScriptLoader directly. // --------------------------------------------------------------------- static void RunFirstAttachInit(RuntimeImpl& impl, Babylon::Graphics::WindowT window) { @@ -145,20 +147,14 @@ namespace Babylon::Integrations (void)window; #endif - // 4. Flush pending LoadScript / Eval / RunOnJsThread calls - // in submission order. Holding m_pendingMutex across the - // iteration ensures any concurrent host-thread call lands - // *after* the queued ones in the JS thread queue (the - // host call blocks on the mutex until the iteration - // finishes appending each pending action to the - // ScriptLoader's task chain). - std::lock_guard lock{implPtr->m_pendingMutex}; - for (auto& action : implPtr->m_pending) - { - action(); - } - implPtr->m_pending.clear(); - implPtr->m_initialized = true; + // 4. Unblock any LoadScript / Eval / RunOnJsThread calls + // the host registered before first Attach. Each was + // chained off m_initTcs.as_task().then(inline_scheduler, + // ..., [...] { scriptLoader->...; });, so completing the + // TCS here causes those continuations to fire (in + // registration order) on the JS thread, each submitting + // to ScriptLoader's task chain. + implPtr->m_initTcs.complete(); }); } From 0c32f8f95d5b8941a8b4a34572b03f960a34b0cc Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 13:38:15 -0700 Subject: [PATCH 09/71] Use Integration layer in Playground Android --- .../Android/BabylonNative/CMakeLists.txt | 53 ++--- .../src/main/cpp/BabylonNativeJNI.cpp | 196 ------------------ .../src/main/cpp/PlaygroundJNI.cpp | 46 ++++ .../babylonjs/integrations/BabylonNative.java | 100 +++++++++ .../library/babylonnative/BabylonView.java | 157 ++++++++++++-- .../com/library/babylonnative/Wrapper.java | 40 ---- .../main/cpp/BabylonNativeIntegrations.cpp | 31 ++- SimplifiedAPI.md | 15 +- 8 files changed, 342 insertions(+), 296 deletions(-) delete mode 100644 Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp create mode 100644 Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp create mode 100644 Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java delete mode 100644 Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java diff --git a/Apps/Playground/Android/BabylonNative/CMakeLists.txt b/Apps/Playground/Android/BabylonNative/CMakeLists.txt index bb9c348b5..79075895d 100644 --- a/Apps/Playground/Android/BabylonNative/CMakeLists.txt +++ b/Apps/Playground/Android/BabylonNative/CMakeLists.txt @@ -8,39 +8,30 @@ project(BabylonNative) get_filename_component(PLAYGROUND_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) get_filename_component(REPO_ROOT_DIR "${PLAYGROUND_DIR}/../.." ABSOLUTE) +# Build the cross-platform Integrations facade *and* the Android JNI +# interop layer (`libBabylonNativeIntegrations.so`). The Playground +# Android app loads that library directly via `BabylonNative.java` +# (in the `com.babylonjs.integrations` package). +set(BABYLON_NATIVE_INTEGRATIONS ON CACHE BOOL "" FORCE) +set(BABYLON_NATIVE_INTEGRATIONS_ANDROID ON CACHE BOOL "" FORCE) + add_subdirectory(${REPO_ROOT_DIR} "${CMAKE_CURRENT_BINARY_DIR}/BabylonNative") -add_library(BabylonNativeJNI SHARED - src/main/cpp/BabylonNativeJNI.cpp - ${PLAYGROUND_DIR}/Shared/AppContext.cpp) +# Append a Playground-specific JNI helper to the generic +# BabylonNativeIntegrations target so the bootstrap script list can stay +# in Apps/Playground/Shared/PlaygroundScripts.{h,cpp} (shared with the +# Win32 / iOS / macOS hosts) instead of being duplicated on the Java +# side. Compiling these into the same .so keeps the Integrations layer +# as a single in-process instance — handles produced by one entry point +# can be safely consumed by another. +target_sources(BabylonNativeIntegrations + PRIVATE src/main/cpp/PlaygroundJNI.cpp + ${PLAYGROUND_DIR}/Shared/PlaygroundScripts.cpp) -target_include_directories(BabylonNativeJNI +target_include_directories(BabylonNativeIntegrations PRIVATE ${PLAYGROUND_DIR}) -target_link_libraries(BabylonNativeJNI - PRIVATE GLESv3 - PRIVATE android - PRIVATE EGL - PRIVATE log - PRIVATE -lz - PRIVATE AndroidExtensions - PRIVATE AppRuntime - PRIVATE Blob - PRIVATE Canvas - PRIVATE Console - PRIVATE GraphicsDevice - PRIVATE NativeCamera - PRIVATE NativeCapture - PRIVATE NativeEncoding - PRIVATE NativeEngine - PRIVATE NativeInput - PRIVATE NativeOptimizations - PRIVATE NativeTracing - PRIVATE NativeXr - PRIVATE Performance - PRIVATE ScriptLoader - PRIVATE ShaderCache - PRIVATE TestUtils - PRIVATE TextDecoder - PRIVATE Window - PRIVATE XMLHttpRequest) +target_link_libraries(BabylonNativeIntegrations + PRIVATE Foundation) # for in PlaygroundScripts.cpp + + diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp deleted file mode 100644 index cebf24c0f..000000000 --- a/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include -#include // requires ndk r5 or newer -#include // requires ndk r5 or newer -#include - -#include -#include -#include - -#include -#include -#include - -namespace -{ - std::optional appContext{}; - std::optional nativeXr{}; - bool isXrActive{}; -} - -extern "C" -{ - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_initEngine(JNIEnv* env, jclass clazz) - { - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_finishEngine(JNIEnv* env, jclass clazz) - { - isXrActive = false; - - nativeXr.reset(); - appContext.reset(); - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_surfaceCreated(JNIEnv* env, jclass clazz, jobject surface, jobject jniContext) - { - if (!appContext) - { - JavaVM* javaVM{}; - if (env->GetJavaVM(&javaVM) != JNI_OK) - { - throw std::runtime_error("Failed to get Java VM"); - } - - android::global::Initialize(javaVM, jniContext); - - ANativeWindow* window = ANativeWindow_fromSurface(env, surface); - int32_t width = ANativeWindow_getWidth(window); - int32_t height = ANativeWindow_getHeight(window); - - appContext.emplace( - window, - static_cast(width), - static_cast(height), - [](const char* message) { - __android_log_write(ANDROID_LOG_INFO, "BabylonNative", message); - }, - [](Napi::Env env) { - nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(env)); - nativeXr->SetSessionStateChangedCallback([](bool isXrActive){ ::isXrActive = isXrActive; }); - }); - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_surfaceChanged(JNIEnv* env, jclass clazz, jint width, jint height, jobject surface) - { - if (appContext) - { - ANativeWindow* window = ANativeWindow_fromSurface(env, surface); - appContext->Runtime().Dispatch([window, width = static_cast(width), height = static_cast(height)](auto) { - appContext->Device().UpdateWindow(window); - appContext->Device().UpdateSize(width, height); - }); - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_setCurrentActivity(JNIEnv* env, jclass clazz, jobject currentActivity) - { - android::global::SetCurrentActivity(currentActivity); - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_activityOnPause(JNIEnv* env, jclass clazz) - { - android::global::Pause(); - if (appContext) - { - appContext->Runtime().Suspend(); - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_activityOnResume(JNIEnv* env, jclass clazz) - { - if (appContext) - { - appContext->Runtime().Resume(); - } - android::global::Resume(); - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_activityOnRequestPermissionsResult(JNIEnv* env, jclass clazz, jint requestCode, jobjectArray permissions, jintArray grantResults) - { - std::vector nativePermissions{}; - for (int i = 0; i < env->GetArrayLength(permissions); i++) - { - jstring permission = (jstring)env->GetObjectArrayElement(permissions, i); - const char* utfString{env->GetStringUTFChars(permission, nullptr)}; - nativePermissions.push_back(utfString); - env->ReleaseStringUTFChars(permission, utfString); - } - - auto grantResultElements{env->GetIntArrayElements(grantResults, nullptr)}; - auto grantResultElementCount = env->GetArrayLength(grantResults); - std::vector nativeGrantResults{grantResultElements, grantResultElements + grantResultElementCount}; - env->ReleaseIntArrayElements(grantResults, grantResultElements, 0); - - android::global::RequestPermissionsResult(requestCode, nativePermissions, nativeGrantResults); - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_loadScript(JNIEnv* env, jclass clazz, jstring path) - { - if (appContext) - { - appContext->ScriptLoader().LoadScript(env->GetStringUTFChars(path, nullptr)); - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_eval(JNIEnv* env, jclass clazz, jstring source, jstring sourceURL) - { - if (appContext) - { - std::string url = env->GetStringUTFChars(sourceURL, nullptr); - std::string src = env->GetStringUTFChars(source, nullptr); - appContext->ScriptLoader().Eval(std::move(src), std::move(url)); - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_setTouchInfo(JNIEnv* env, jclass clazz, jint pointerId, jfloat x, jfloat y, jboolean buttonAction, jint buttonValue) - { - if (appContext && appContext->Input()) - { - if (buttonAction) - { - if (buttonValue == 1) - appContext->Input()->TouchDown(pointerId, x, y); - else - appContext->Input()->TouchUp(pointerId, x, y); - } - else { - appContext->Input()->TouchMove(pointerId, x, y); - } - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_renderFrame(JNIEnv* env, jclass clazz) - { - if (appContext) - { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); - } - } - - JNIEXPORT void JNICALL - Java_com_library_babylonnative_Wrapper_xrSurfaceChanged(JNIEnv* env, jclass clazz, jobject surface) - { - if (nativeXr) - { - ANativeWindow* window{}; - if (surface) - { - window = ANativeWindow_fromSurface(env, surface); - } - nativeXr->UpdateWindow(window); - } - } - - JNIEXPORT jboolean JNICALL - Java_com_library_babylonnative_Wrapper_isXRActive(JNIEnv* env, jclass clazz) - { - return isXrActive; - } -} diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp new file mode 100644 index 000000000..3ed580adb --- /dev/null +++ b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp @@ -0,0 +1,46 @@ +// Playground-specific JNI helper. +// +// This file lives alongside the generic Integrations/Android JNI in the +// same shared library (`libBabylonNativeIntegrations.so`) — the +// Playground's CMakeLists adds it via `target_sources(...)`. Keeping +// everything in one .so means a single copy of Babylon::Integrations +// across the whole process; cross-library handle passing UB is avoided. +// +// The only purpose of this helper is to surface +// `Apps/Playground/Shared/PlaygroundScripts.{h,cpp}` to Java so that the +// Babylon.js bootstrap script list stays in one place (shared with the +// other Playground hosts: Win32, iOS, macOS, …) rather than being +// duplicated on the Java side. + +#include + +#include + +#include + +extern "C" +{ + +JNIEXPORT void JNICALL +Java_com_library_babylonnative_BabylonView_loadBootstrapScripts( + JNIEnv*, jclass, jlong runtimeHandle) +{ + if (runtimeHandle == 0) + { + return; + } + auto* runtime = reinterpret_cast(runtimeHandle); + + // Process-wide one-shot Playground setup (PerfTrace level, etc.). + // Re-calling is idempotent; safe even if multiple BabylonView + // instances queue bootstrap scripts. + Playground::Initialize(); + + // Queues each Babylon.js bootstrap script (ammo / babylon.max / + // loaders / materials / gui / meshwriter / serializers) onto the + // runtime; they run after the first View::Attach completes engine + // initialization on the JS thread. + Playground::LoadBootstrapScripts(*runtime); +} + +} // extern "C" diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java new file mode 100644 index 000000000..0c92c6500 --- /dev/null +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -0,0 +1,100 @@ +package com.babylonjs.integrations; + +import android.content.Context; +import android.view.Surface; + +/** + * JVM binding for the C++ Babylon::Integrations layer. The native methods + * here mirror, byte-for-byte, the {@code extern "C" JNIEXPORT} entry points + * declared in {@code Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp}. + * + *

This class is a thin facade — it owns no state and exposes the C++ + * API as static methods. Hosts typically wrap it in their own {@code View} + * subclass (see {@link com.library.babylonnative.BabylonView} for an example). + * + *

Lifecycle: + *

    + *
  1. Call {@link #androidGlobalInitialize(Context)} once at app startup + * (typically from {@code Application.onCreate}).
  2. + *
  3. Create a Runtime via {@link #runtimeCreate(boolean)} and remember + * the returned {@code long} handle.
  4. + *
  5. Optional: queue scripts via {@link #runtimeLoadScript(long, String)} + * — they run after the first {@link #viewAttach(long, Surface, int, int, float)}.
  6. + *
  7. Attach a View via {@link #viewAttach(long, Surface, int, int, float)}; + * call {@link #viewRenderFrame(long)} from your draw loop.
  8. + *
  9. Tear down with {@link #viewDetach(long)} then {@link #runtimeDestroy(long)}.
  10. + *
+ */ +public final class BabylonNative { + static { + System.loadLibrary("BabylonNativeIntegrations"); + } + + private BabylonNative() {} + + // ------------------------------------------------------------------- + // Process-wide platform lifecycle + // ------------------------------------------------------------------- + + /** Call once at app startup before any other method. */ + public static native void androidGlobalInitialize(Context context); + + public static native void androidGlobalSetCurrentActivity(Object activity); + + public static native void androidGlobalPause(); + + public static native void androidGlobalResume(); + + public static native void androidGlobalRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults); + + // ------------------------------------------------------------------- + // Runtime + // ------------------------------------------------------------------- + + /** Returns an opaque handle owned by the caller; release with {@link #runtimeDestroy(long)}. */ + public static native long runtimeCreate(boolean enableDebugger); + + public static native void runtimeDestroy(long handle); + + public static native void runtimeLoadScript(long handle, String url); + + public static native void runtimeEval(long handle, String source, String sourceUrl); + + public static native void runtimeSuspend(long handle); + + public static native void runtimeResume(long handle); + + public static native boolean runtimeIsSuspended(long handle); + + // Compiled into the native library only when BABYLON_NATIVE_PLUGIN_NATIVEXR + // is enabled. Calling these without that flag will produce an UnsatisfiedLinkError. + public static native void runtimeSetXrSurface(long handle, Surface surface); + + public static native boolean runtimeIsXrActive(long handle); + + // ------------------------------------------------------------------- + // View + // ------------------------------------------------------------------- + + /** + * Returns an opaque handle owned by the caller; release with {@link #viewDetach(long)}. + * {@code physicalWidth} and {@code physicalHeight} are the surface size in + * physical pixels (Android {@code View.onSizeChanged} units); {@code density} + * is {@code DisplayMetrics.density} (the physical-to-logical pixel ratio). + */ + public static native long viewAttach(long runtimeHandle, Surface surface, + int physicalWidth, int physicalHeight, float density); + + public static native void viewDetach(long handle); + + public static native void viewRenderFrame(long handle); + + public static native void viewResize(long handle, int physicalWidth, int physicalHeight, float density); + + public static native void viewPointerDown(long handle, int pointerId, float x, float y); + + public static native void viewPointerMove(long handle, int pointerId, float x, float y); + + public static native void viewPointerUp(long handle, int pointerId, float x, float y); +} diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 517f376dc..91477666d 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -3,22 +3,60 @@ import android.app.Activity; import android.content.Context; import android.graphics.Canvas; -import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.FrameLayout; +import com.babylonjs.integrations.BabylonNative; + +/** + * Playground View built on top of {@link BabylonNative}. Holds a single + * Runtime + View handle pair for the lifetime of this widget; reattaches + * the View on every {@code surfaceChanged} (Android may hand us a new + * surface after a configuration change or visibility transition). + */ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, View.OnTouchListener { - private static final FrameLayout.LayoutParams childViewLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - private static final String TAG = "BabylonView"; + private static final FrameLayout.LayoutParams childViewLayoutParams = + new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + /** + * Native helper implemented in + * {@code Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp}. + * Compiled into the same {@code libBabylonNativeIntegrations.so} + * loaded by {@link BabylonNative}; queues the Babylon.js bootstrap + * scripts (the list lives in {@code Apps/Playground/Shared/PlaygroundScripts.cpp} + * so all Playground hosts share it). + */ + private static native void loadBootstrapScripts(long runtimeHandle); + + /** {@link BabylonNative#androidGlobalInitialize(Context)} is process-wide; only call it once. */ + private static boolean sGlobalInitDone = false; + private boolean mViewReady = false; private final ViewDelegate mViewDelegate; private Activity mCurrentActivity; private final SurfaceView primarySurfaceView; private final SurfaceView xrSurfaceView; private final float pixelDensityScale = getResources().getDisplayMetrics().density; + + /** Native Runtime handle (0 if not created). Owned by this view. */ + private long mRuntimeHandle = 0; + + /** Native View handle (0 if not attached). Owned by this view. */ + private long mViewHandle = 0; + + /** + * The {@link android.view.Surface} that {@code mViewHandle} is currently + * bound to (null if not attached). Compared by reference in + * {@link #surfaceChanged} to decide between resize-in-place and a full + * detach + reattach: Android reuses the same {@code Surface} object for + * pure size changes (orientation, layout), and hands us a new + * {@code Surface} after a destroy/recreate cycle. + */ + private android.view.Surface mAttachedSurface = null; + public BabylonView(Context context, ViewDelegate viewDelegate) { this(context, viewDelegate, (Activity)viewDelegate); } @@ -26,6 +64,13 @@ public BabylonView(Context context, ViewDelegate viewDelegate) { public BabylonView(Context context, ViewDelegate viewDelegate, Activity currentActivity) { super(context); + // Process-wide one-shot init for AndroidExtensions::Globals + // (used by NativeCamera, NativeXr, etc.). + if (!sGlobalInitDone) { + BabylonNative.androidGlobalInitialize(context.getApplicationContext()); + sGlobalInitDone = true; + } + this.primarySurfaceView = new SurfaceView(context); this.primarySurfaceView.setLayoutParams(BabylonView.childViewLayoutParams); this.primarySurfaceView.getHolder().addCallback(this); @@ -47,12 +92,16 @@ public void surfaceCreated(SurfaceHolder holder) { @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Wrapper.xrSurfaceChanged(holder.getSurface()); + if (mRuntimeHandle != 0) { + BabylonNative.runtimeSetXrSurface(mRuntimeHandle, holder.getSurface()); + } } @Override public void surfaceDestroyed(SurfaceHolder holder) { - Wrapper.xrSurfaceChanged(null); + if (mRuntimeHandle != 0) { + BabylonNative.runtimeSetXrSurface(mRuntimeHandle, null); + } } }); this.xrSurfaceView.setVisibility(View.INVISIBLE); @@ -60,36 +109,49 @@ public void surfaceDestroyed(SurfaceHolder holder) { setWillNotDraw(false); - Wrapper.initEngine(); + // Create the Runtime up-front and queue the Babylon.js bootstrap + // scripts. They will run after the first viewAttach completes + // engine initialization on the JS thread. + mRuntimeHandle = BabylonNative.runtimeCreate(/*enableDebugger*/ true); + loadBootstrapScripts(mRuntimeHandle); } - public void setCurrentActivity(Activity currentActivity) - { + public void setCurrentActivity(Activity currentActivity) { if (currentActivity != this.mCurrentActivity) { this.mCurrentActivity = currentActivity; - Wrapper.setCurrentActivity(this.mCurrentActivity); + BabylonNative.androidGlobalSetCurrentActivity(this.mCurrentActivity); } } public void loadScript(String path) { - Wrapper.loadScript(path); + if (mRuntimeHandle != 0) { + BabylonNative.runtimeLoadScript(mRuntimeHandle, path); + } } public void eval(String source, String sourceURL) { - Wrapper.eval(source, sourceURL); + if (mRuntimeHandle != 0) { + BabylonNative.runtimeEval(mRuntimeHandle, source, sourceURL); + } } public void onPause() { setVisibility(View.GONE); - Wrapper.activityOnPause(); + BabylonNative.androidGlobalPause(); + if (mRuntimeHandle != 0) { + BabylonNative.runtimeSuspend(mRuntimeHandle); + } } public void onResume() { - Wrapper.activityOnResume(); + if (mRuntimeHandle != 0) { + BabylonNative.runtimeResume(mRuntimeHandle); + } + BabylonNative.androidGlobalResume(); } public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { - Wrapper.activityOnRequestPermissionsResult(requestCode, permissions, results); + BabylonNative.androidGlobalRequestPermissionsResult(requestCode, permissions, results); } /** @@ -97,8 +159,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in * not normally called or subclassed by clients of BabylonView. */ public void surfaceCreated(SurfaceHolder holder) { - Wrapper.surfaceCreated(holder.getSurface(), this.getContext()); - Wrapper.setCurrentActivity(this.mCurrentActivity); + BabylonNative.androidGlobalSetCurrentActivity(this.mCurrentActivity); if (!this.mViewReady) { this.mViewDelegate.onViewReady(); this.mViewReady = true; @@ -110,6 +171,15 @@ public void surfaceCreated(SurfaceHolder holder) { * not normally called or subclassed by clients of BabylonView. */ public void surfaceDestroyed(SurfaceHolder holder) { + // The Surface backing mViewHandle is being torn down. Drop the + // View so nothing tries to render into a dead surface; the next + // surfaceChanged will reattach. (Holding onto the Runtime is + // fine — it's surface-independent.) + if (mViewHandle != 0) { + BabylonNative.viewDetach(mViewHandle); + mViewHandle = 0; + mAttachedSurface = null; + } } /** @@ -117,7 +187,29 @@ public void surfaceDestroyed(SurfaceHolder holder) { * not normally called or subclassed by clients of BabylonView. */ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { - Wrapper.surfaceChanged((int)(w / this.pixelDensityScale), (int)(h / this.pixelDensityScale), holder.getSurface()); + if (mRuntimeHandle == 0) { + return; + } + + android.view.Surface surface = holder.getSurface(); + + // Same Surface as last attach: this is a size-only change. + // (Triggered by orientation rotation, parent layout, etc.) + if (mViewHandle != 0 && surface == mAttachedSurface) { + BabylonNative.viewResize(mViewHandle, w, h, this.pixelDensityScale); + return; + } + + // New Surface (or first attach). View::Attach contract requires + // at most one View per Runtime, so detach any stale one first. + if (mViewHandle != 0) { + BabylonNative.viewDetach(mViewHandle); + mViewHandle = 0; + } + + mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, surface, + w, h, this.pixelDensityScale); + mAttachedSurface = (mViewHandle != 0) ? surface : null; } public interface ViewDelegate { @@ -126,6 +218,10 @@ public interface ViewDelegate { @Override public boolean onTouch(View v, MotionEvent event) { + if (mViewHandle == 0) { + return false; + } + int pointerId = event.getPointerId(event.getActionIndex()); float mX = event.getX(event.getActionIndex()) / this.pixelDensityScale; float mY = event.getY(event.getActionIndex()) / this.pixelDensityScale; @@ -133,14 +229,14 @@ public boolean onTouch(View v, MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: - Wrapper.setTouchInfo(pointerId, mX, mY, true, 1); + BabylonNative.viewPointerDown(mViewHandle, pointerId, mX, mY); break; case MotionEvent.ACTION_MOVE: - Wrapper.setTouchInfo(pointerId, mX, mY, false, 0); + BabylonNative.viewPointerMove(mViewHandle, pointerId, mX, mY); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: - Wrapper.setTouchInfo(pointerId, mX, mY, true, 0); + BabylonNative.viewPointerUp(mViewHandle, pointerId, mX, mY); break; } return true; @@ -148,7 +244,19 @@ public boolean onTouch(View v, MotionEvent event) { @Override protected void finalize() throws Throwable { - Wrapper.finishEngine(); + try { + if (mViewHandle != 0) { + BabylonNative.viewDetach(mViewHandle); + mViewHandle = 0; + mAttachedSurface = null; + } + if (mRuntimeHandle != 0) { + BabylonNative.runtimeDestroy(mRuntimeHandle); + mRuntimeHandle = 0; + } + } finally { + super.finalize(); + } } /** @@ -163,13 +271,16 @@ public void surfaceRedrawNeeded(SurfaceHolder holder) { @Override protected void onDraw(Canvas canvas) { - if (Wrapper.isXRActive()) { + if (mRuntimeHandle != 0 && BabylonNative.runtimeIsXrActive(mRuntimeHandle)) { this.xrSurfaceView.setVisibility(View.VISIBLE); } else { this.xrSurfaceView.setVisibility(View.INVISIBLE); } - Wrapper.renderFrame(); + if (mViewHandle != 0) { + BabylonNative.viewRenderFrame(mViewHandle); + } invalidate(); } } + diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java deleted file mode 100644 index 8ab648690..000000000 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.library.babylonnative; - -import android.app.Activity; -import android.content.Context; -import android.view.Surface; - -public class Wrapper { - // JNI interface - static { - System.loadLibrary("BabylonNativeJNI"); - } - - public static native void initEngine(); - - public static native void finishEngine(); - - public static native void surfaceCreated(Surface surface, Context context); - - public static native void surfaceChanged(int width, int height, Surface surface); - - public static native void setCurrentActivity(Activity currentActivity); - - public static native void activityOnPause(); - - public static native void activityOnResume(); - - public static native void activityOnRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); - - public static native void setTouchInfo(int pointerId, float dx, float dy, boolean button, int buttonValue); - - public static native void loadScript(String path); - - public static native void eval(String source, String sourceURL); - - public static native void renderFrame(); - - public static native void xrSurfaceChanged(Surface surface); - - public static native boolean isXRActive(); -} \ No newline at end of file diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 70ce0349d..5b42a6024 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -26,6 +26,7 @@ #include +#include #include #include @@ -37,13 +38,27 @@ namespace { + using Babylon::Integrations::LogLevel; using Babylon::Integrations::Runtime; + using Babylon::Integrations::RuntimeOptions; using Babylon::Integrations::View; using Babylon::Integrations::ViewDescriptor; Runtime* AsRuntime(jlong handle) { return reinterpret_cast(handle); } View* AsView(jlong handle) { return reinterpret_cast(handle); } + int LogPriorityFor(LogLevel level) + { + switch (level) + { + case LogLevel::Log: return ANDROID_LOG_INFO; + case LogLevel::Warn: return ANDROID_LOG_WARN; + case LogLevel::Error: return ANDROID_LOG_ERROR; + case LogLevel::Fatal: return ANDROID_LOG_FATAL; + } + return ANDROID_LOG_INFO; + } + // Convert a jstring to std::string. Returns empty string if `jstr` // is null or UTF lookup fails. std::string ToStdString(JNIEnv* env, jstring jstr) @@ -155,12 +170,24 @@ Java_com_babylonjs_integrations_BabylonNative_androidGlobalRequestPermissionsRes // ===================================================================== JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeCreate(JNIEnv*, jclass) +Java_com_babylonjs_integrations_BabylonNative_runtimeCreate(JNIEnv*, jclass, jboolean enableDebugger) { + // Default Android consumers want logcat output; route Console + // polyfill / DebugTrace / uncaught JS exceptions there. Hosts + // that need different behavior can construct a Runtime in C++ + // directly with their own RuntimeOptions. + RuntimeOptions options{}; + options.enableDebugger = (enableDebugger == JNI_TRUE); + options.log = [](LogLevel level, std::string_view message) { + // logcat takes a NUL-terminated C string; copy the view. + std::string text{message}; + __android_log_write(LogPriorityFor(level), "BabylonNative", text.c_str()); + }; + // unique_ptr::release() returns the raw pointer and gives up // ownership *without* deleting; the JVM side now owns it via the // returned jlong handle and must call runtimeDestroy() to free it. - return reinterpret_cast(Runtime::Create().release()); + return reinterpret_cast(Runtime::Create(std::move(options)).release()); } JNIEXPORT void JNICALL diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index 4ea9a31d7..d66583f3d 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -141,11 +141,18 @@ exists. namespace Babylon::Integrations { // Platform-surface handle. Populated by the platform interop layer - // from whatever native object the host's UI framework provides - // (HWND, ANativeWindow*, CAMetalLayer*, …). The interop layer is - // also responsible for any unit conversion (see "Pixel units" below). + // from whatever native object the host's UI framework provides. + // The interop layer is also responsible for any unit conversion + // (see "Pixel units" below). + // + // `nativeWindow` is `Babylon::Graphics::WindowT`, the same per-platform + // typedef the Graphics layer already uses (HWND on Win32, + // ANativeWindow* on Android, CA::MetalLayer* on Apple, + // X11 `Window` on Linux, winrt::IInspectable on UWP). Using the + // typed handle avoids a round-trip through `void*` and gives + // hosts compile-time safety. struct ViewDescriptor { - void* nativeWindow; // HWND / ANativeWindow* / CAMetalLayer* / ... + Babylon::Graphics::WindowT nativeWindow{}; uint32_t width; // logical pixels (see "Pixel units") uint32_t height; // logical pixels (see "Pixel units") float devicePixelRatio = 1.0f; From bfb8dbca8d76701dec4c44acf0cc507ca13b0a56 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 14:47:10 -0700 Subject: [PATCH 10/71] Android simplification --- .../src/main/cpp/PlaygroundJNI.cpp | 6 +- .../babylonjs/integrations/BabylonNative.java | 10 +- .../library/babylonnative/BabylonView.java | 59 +++-------- .../playground/PlaygroundActivity.java | 30 +++++- Integrations/Android/CMakeLists.txt | 3 + .../Integrations/Android/RuntimeHandle.h | 21 ++++ .../main/cpp/BabylonNativeIntegrations.cpp | 99 ++++++++++++++----- Integrations/Source/Runtime.cpp | 8 +- 8 files changed, 148 insertions(+), 88 deletions(-) create mode 100644 Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp index 3ed580adb..02ec3259c 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp +++ b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp @@ -13,6 +13,7 @@ // duplicated on the Java side. #include +#include #include @@ -25,11 +26,11 @@ JNIEXPORT void JNICALL Java_com_library_babylonnative_BabylonView_loadBootstrapScripts( JNIEnv*, jclass, jlong runtimeHandle) { - if (runtimeHandle == 0) + auto* runtime = Babylon::Integrations::Android::RuntimeFromHandle(runtimeHandle); + if (runtime == nullptr) { return; } - auto* runtime = reinterpret_cast(runtimeHandle); // Process-wide one-shot Playground setup (PerfTrace level, etc.). // Re-calling is idempotent; safe even if multiple BabylonView @@ -44,3 +45,4 @@ Java_com_library_babylonnative_BabylonView_loadBootstrapScripts( } } // extern "C" + diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index 0c92c6500..fcb20d319 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -61,11 +61,11 @@ public static native void androidGlobalRequestPermissionsResult( public static native void runtimeEval(long handle, String source, String sourceUrl); - public static native void runtimeSuspend(long handle); - - public static native void runtimeResume(long handle); - - public static native boolean runtimeIsSuspended(long handle); + // Note: there is intentionally no per-Runtime Suspend/Resume on the + // Java surface. Each Runtime auto-subscribes to androidGlobalPause / + // androidGlobalResume in runtimeCreate, so the host Activity calls + // those once per state change and every Runtime in the process + // reacts. Hosts needing finer-grained control should use the C++ API. // Compiled into the native library only when BABYLON_NATIVE_PLUGIN_NATIVEXR // is enabled. Calling these without that flag will produce an UnsatisfiedLinkError. diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 91477666d..e04d57375 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -1,6 +1,5 @@ package com.library.babylonnative; -import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.view.MotionEvent; @@ -12,10 +11,17 @@ import com.babylonjs.integrations.BabylonNative; /** - * Playground View built on top of {@link BabylonNative}. Holds a single - * Runtime + View handle pair for the lifetime of this widget; reattaches - * the View on every {@code surfaceChanged} (Android may hand us a new - * surface after a configuration change or visibility transition). + * Playground View built on top of {@link BabylonNative}. Owns one + * Runtime + View handle pair for its lifetime. + * + *

Activity lifecycle: the host Activity is responsible for the + * process-wide {@code androidGlobalInitialize}, {@code SetCurrentActivity}, + * {@code Pause}/{@code Resume}, and {@code RequestPermissionsResult} + * notifications (see {@code PlaygroundActivity.java}). The Runtime + * automatically subscribes to {@code androidGlobalPause / Resume} when + * created, so the host Activity does not need to invoke any per-view + * pause/resume method — telling the JNI layer once is enough for every + * Runtime in the process. */ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, View.OnTouchListener { private static final FrameLayout.LayoutParams childViewLayoutParams = @@ -31,12 +37,8 @@ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, */ private static native void loadBootstrapScripts(long runtimeHandle); - /** {@link BabylonNative#androidGlobalInitialize(Context)} is process-wide; only call it once. */ - private static boolean sGlobalInitDone = false; - private boolean mViewReady = false; private final ViewDelegate mViewDelegate; - private Activity mCurrentActivity; private final SurfaceView primarySurfaceView; private final SurfaceView xrSurfaceView; private final float pixelDensityScale = getResources().getDisplayMetrics().density; @@ -58,25 +60,13 @@ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, private android.view.Surface mAttachedSurface = null; public BabylonView(Context context, ViewDelegate viewDelegate) { - this(context, viewDelegate, (Activity)viewDelegate); - } - - public BabylonView(Context context, ViewDelegate viewDelegate, Activity currentActivity) { super(context); - // Process-wide one-shot init for AndroidExtensions::Globals - // (used by NativeCamera, NativeXr, etc.). - if (!sGlobalInitDone) { - BabylonNative.androidGlobalInitialize(context.getApplicationContext()); - sGlobalInitDone = true; - } - this.primarySurfaceView = new SurfaceView(context); this.primarySurfaceView.setLayoutParams(BabylonView.childViewLayoutParams); this.primarySurfaceView.getHolder().addCallback(this); this.addView(this.primarySurfaceView); - this.mCurrentActivity = currentActivity; SurfaceHolder holder = this.primarySurfaceView.getHolder(); holder.addCallback(this); setOnTouchListener(this); @@ -116,13 +106,6 @@ public void surfaceDestroyed(SurfaceHolder holder) { loadBootstrapScripts(mRuntimeHandle); } - public void setCurrentActivity(Activity currentActivity) { - if (currentActivity != this.mCurrentActivity) { - this.mCurrentActivity = currentActivity; - BabylonNative.androidGlobalSetCurrentActivity(this.mCurrentActivity); - } - } - public void loadScript(String path) { if (mRuntimeHandle != 0) { BabylonNative.runtimeLoadScript(mRuntimeHandle, path); @@ -135,31 +118,11 @@ public void eval(String source, String sourceURL) { } } - public void onPause() { - setVisibility(View.GONE); - BabylonNative.androidGlobalPause(); - if (mRuntimeHandle != 0) { - BabylonNative.runtimeSuspend(mRuntimeHandle); - } - } - - public void onResume() { - if (mRuntimeHandle != 0) { - BabylonNative.runtimeResume(mRuntimeHandle); - } - BabylonNative.androidGlobalResume(); - } - - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { - BabylonNative.androidGlobalRequestPermissionsResult(requestCode, permissions, results); - } - /** * This method is part of the SurfaceHolder.Callback interface, and is * not normally called or subclassed by clients of BabylonView. */ public void surfaceCreated(SurfaceHolder holder) { - BabylonNative.androidGlobalSetCurrentActivity(this.mCurrentActivity); if (!this.mViewReady) { this.mViewDelegate.onViewReady(); this.mViewReady = true; diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 3064c14b7..9b2c77529 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -8,34 +8,58 @@ import android.support.v4.content.ContextCompat; import android.view.View; +import com.babylonjs.integrations.BabylonNative; import com.library.babylonnative.BabylonView; public class PlaygroundActivity extends Activity implements BabylonView.ViewDelegate { + /** {@link BabylonNative#androidGlobalInitialize} is process-wide; only call it once. */ + private static boolean sGlobalInitDone = false; + BabylonView mView; // Activity life @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); + + // Process-wide one-shot init for AndroidExtensions::Globals + // (used by NativeCamera, NativeXr, etc.). Belongs at the + // Activity/Application level — not on a per-view basis — because + // it broadcasts to process-wide handlers that aren't refcounted. + if (!sGlobalInitDone) { + BabylonNative.androidGlobalInitialize(getApplication()); + sGlobalInitDone = true; + } + BabylonNative.androidGlobalSetCurrentActivity(this); + mView = new BabylonView(getApplication(), this); setContentView(mView); } @Override protected void onPause() { - mView.onPause(); + // Hide the view to suppress its draw loop while paused. Visibility + // is restored by onWindowFocusChanged below when the Activity + // returns to the foreground. + mView.setVisibility(View.GONE); + + // Process-wide notification: every Runtime in this process + // auto-suspends because they each subscribed to this event in + // BabylonNative.runtimeCreate. Same for cross-cutting subsystems + // (NativeCamera, NativeXr) that hook AndroidExtensions::Globals. + BabylonNative.androidGlobalPause(); super.onPause(); } @Override protected void onResume() { super.onResume(); - mView.onResume(); + BabylonNative.androidGlobalResume(); } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { - mView.onRequestPermissionsResult(requestCode, permissions, results); + BabylonNative.androidGlobalRequestPermissionsResult(requestCode, permissions, results); } @Override diff --git a/Integrations/Android/CMakeLists.txt b/Integrations/Android/CMakeLists.txt index d76f3a59c..053d32164 100644 --- a/Integrations/Android/CMakeLists.txt +++ b/Integrations/Android/CMakeLists.txt @@ -15,12 +15,15 @@ if(NOT ANDROID) endif() set(SOURCES + "Include/Babylon/Integrations/Android/RuntimeHandle.h" "src/main/cpp/BabylonNativeIntegrations.cpp") add_library(BabylonNativeIntegrations SHARED ${SOURCES}) warnings_as_errors(BabylonNativeIntegrations) +target_include_directories(BabylonNativeIntegrations PUBLIC "Include") + target_link_libraries(BabylonNativeIntegrations PRIVATE Integrations PRIVATE AndroidExtensions diff --git a/Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h b/Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h new file mode 100644 index 000000000..3c5f409f9 --- /dev/null +++ b/Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +namespace Babylon::Integrations::Android +{ + // Convert an opaque jlong handle (as returned by `runtimeCreate` in + // `BabylonNativeIntegrations.cpp`) back to a Runtime pointer. + // + // The Android JNI layer wraps each Runtime in an internal struct + // that also holds Activity-lifecycle event tickets, so a direct + // `reinterpret_cast(handle)` is incorrect — hosts that + // ship their own JNI helpers alongside `libBabylonNativeIntegrations.so` + // (e.g. for app-specific bootstrap routines) must go through this + // function to resolve the handle correctly. + // + // Returns nullptr if `handle` is 0. + Runtime* RuntimeFromHandle(jlong handle); +} diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 5b42a6024..e5a7a5166 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -23,6 +23,7 @@ #include #include +#include #include @@ -33,6 +34,7 @@ #include #include +#include #include #include @@ -44,7 +46,25 @@ namespace using Babylon::Integrations::View; using Babylon::Integrations::ViewDescriptor; - Runtime* AsRuntime(jlong handle) { return reinterpret_cast(handle); } + // Wraps a Runtime with two `android::global` event tickets that + // auto-Suspend/Resume the Runtime in response to process-wide + // Activity lifecycle notifications. Member declaration order matters: + // tickets are declared *after* the Runtime so they're destroyed + // *before* the Runtime, which guarantees no callback can fire on a + // dead Runtime during teardown. + // + // Suspend/Resume on Babylon::Integrations::Runtime is reference-counted, + // so the auto-suspend composes safely with explicit host-side + // runtimeSuspend / runtimeResume calls. + struct AndroidRuntime + { + std::unique_ptr runtime; + android::global::AppStateChangedCallbackTicket pauseTicket; + android::global::AppStateChangedCallbackTicket resumeTicket; + }; + + AndroidRuntime* AsAndroidRuntime(jlong handle) { return reinterpret_cast(handle); } + Runtime* AsRuntime(jlong handle) { return AsAndroidRuntime(handle)->runtime.get(); } View* AsView(jlong handle) { return reinterpret_cast(handle); } int LogPriorityFor(LogLevel level) @@ -91,6 +111,21 @@ namespace } } +// Public handle-decoding entry point. Hosts that ship app-specific JNI +// helpers in the same `libBabylonNativeIntegrations.so` (e.g. the +// Playground's PlaygroundJNI.cpp for the Babylon.js bootstrap script +// list) call this to get back a Runtime* from the opaque jlong returned +// by `runtimeCreate`. Direct `reinterpret_cast(handle)` is +// wrong because each Runtime is wrapped in `AndroidRuntime` to hold +// Activity-lifecycle tickets. +namespace Babylon::Integrations::Android +{ + Runtime* RuntimeFromHandle(jlong handle) + { + return handle == 0 ? nullptr : AsRuntime(handle); + } +} + extern "C" { @@ -184,16 +219,44 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeCreate(JNIEnv*, jclass, jbo __android_log_write(LogPriorityFor(level), "BabylonNative", text.c_str()); }; - // unique_ptr::release() returns the raw pointer and gives up - // ownership *without* deleting; the JVM side now owns it via the - // returned jlong handle and must call runtimeDestroy() to free it. - return reinterpret_cast(Runtime::Create(std::move(options)).release()); + // Construct in two phases because the AppStateChangedCallbackTicket + // is neither default-constructible nor move-assignable: we need the + // Runtime pointer in hand before we can register the callbacks, and + // we register the callbacks before the wrapper itself exists. + auto runtime = Runtime::Create(std::move(options)); + Runtime* runtimePtr = runtime.get(); + + // Auto-Suspend/Resume on Activity lifecycle. Hosts call + // androidGlobalPause / androidGlobalResume from their Activity's + // onPause / onResume; every Runtime in the process gets suspended + // and resumed automatically. Since Runtime::Suspend/Resume are + // refcounted, this composes safely with any explicit + // runtimeSuspend / runtimeResume calls the host might make for + // finer-grained reasons (e.g. modal dialogs). + auto pauseTicket = android::global::AddPauseCallback([runtimePtr]() { + runtimePtr->Suspend(); + }); + auto resumeTicket = android::global::AddResumeCallback([runtimePtr]() { + runtimePtr->Resume(); + }); + + auto wrapper = std::unique_ptr{new AndroidRuntime{ + std::move(runtime), + std::move(pauseTicket), + std::move(resumeTicket), + }}; + + // Ownership transfers to the JVM side via the returned jlong. + return reinterpret_cast(wrapper.release()); } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong handle) { - delete AsRuntime(handle); + // Member dtor order (reverse declaration): resumeTicket → pauseTicket + // → runtime. The tickets unsubscribe before the Runtime is destroyed, + // so no callback can fire on a dead Runtime during teardown. + delete AsAndroidRuntime(handle); } JNIEXPORT void JNICALL @@ -210,23 +273,13 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeEval( AsRuntime(handle)->Eval(ToStdString(env, source), ToStdString(env, sourceUrl)); } -JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeSuspend(JNIEnv*, jclass, jlong handle) -{ - AsRuntime(handle)->Suspend(); -} - -JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeResume(JNIEnv*, jclass, jlong handle) -{ - AsRuntime(handle)->Resume(); -} - -JNIEXPORT jboolean JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeIsSuspended(JNIEnv*, jclass, jlong handle) -{ - return AsRuntime(handle)->IsSuspended() ? JNI_TRUE : JNI_FALSE; -} +// Note: there is intentionally no per-Runtime Suspend/Resume on the JNI +// surface. Activity-lifecycle Suspend/Resume is wired up automatically +// inside runtimeCreate above (each Runtime subscribes to +// android::global pause/resume callbacks). Hosts only call +// `androidGlobalPause` / `androidGlobalResume` once per Activity state +// change; every Runtime in the process reacts. Hosts that need +// finer-grained control should use the C++ API directly. #if BABYLON_NATIVE_PLUGIN_NATIVEXR diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 0b401f1f7..4ffde0bbf 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -67,13 +67,7 @@ namespace Babylon::Integrations // 2. The Canvas polyfill and NativeInput pointer are referenced // from JS-thread state; clear them before joining the JS // thread, but only after ScriptLoader has drained. - // 3. ~AppRuntime joins the JS thread. After it returns, no - // JS-thread task is running. If the first-Attach init - // lambda was queued but not yet run when ~Impl began, it - // will run during the AppRuntime drain (if AppRuntime - // drains its queue before joining); m_canvas / m_input - // etc. may then be re-populated and discarded when ~Impl - // itself destroys the optionals. + // 3. ~AppRuntime joins the JS thread. // 4. ShaderCache::Disable() balances the Enable() that // View::Attach calls on first attach. // 5. Device + DeviceUpdate destroyed last because the JS From 7655d6559d33b906a4f863c378c62bb8c116a6d1 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 14:55:36 -0700 Subject: [PATCH 11/71] Update plan for auto suspend/resume --- SimplifiedAPI.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index d66583f3d..409bbab97 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -1227,6 +1227,33 @@ runtime->Resume(); // dialog closed -> count = 1, still suspended runtime->Resume(); // app foregrounded -> count = 0, running ``` +### Per-platform Activity-lifecycle wiring + +Some platforms have a well-defined process-wide "the app is going to +the background" signal; some don't. Where one exists, the platform +interop layer auto-subscribes each Runtime to it on construction +(via `Suspend` / `Resume`); where it doesn't, the host wires it up +manually (or doesn't bother). This avoids forcing every host to +re-implement the same boilerplate while keeping the cross-platform +`Runtime::Suspend / Resume` available everywhere for hosts that want +to wire it themselves. + +| Platform | Process-wide signal | Where it's wired | +|---|---|---| +| Android | `android::global::AddPauseCallback` / `AddResumeCallback`, fired by the host's Activity via `androidGlobalPause` / `androidGlobalResume` | Auto-subscribed inside `runtimeCreate` in `Integrations/Android/.../BabylonNativeIntegrations.cpp`. Tickets are dropped when the Runtime is destroyed. | +| iOS / visionOS | `UIApplicationDidEnterBackgroundNotification` / `WillEnterForegroundNotification` (`NSNotificationCenter`) | Apple interop layer should subscribe in `BNRuntime`'s init; remove observers in `dealloc`. (Pending — wire up during the iOS migration.) | +| UWP | `CoreApplication::Suspending` / `Resuming` | Host typically owns a `CoreApplication`-scoped object directly; interop layer can either auto-subscribe or document the pattern. (Pending — wire up during the UWP migration.) | +| macOS | No clear "process backgrounded" notification (apps generally keep running). NSWorkspace sleep notifications exist but aren't usually what hosts want. | Don't auto-subscribe. Hosts call `Suspend / Resume` themselves if they need it. | +| Win32 | No process-wide signal — only per-HWND `WM_ACTIVATE`. | Don't auto-subscribe. Hosts call `Suspend / Resume` from `WM_ACTIVATE`/etc. | +| Linux / X11 | None standard. | Don't auto-subscribe. Host policy. | + +The auto-subscribe mechanism is **opaque** to hosts — they don't see the +ticket plumbing. Whether or not it's wired on a given platform, the +`Runtime::Suspend / Resume` C++ API is identical, so a host that wants +finer-grained control (e.g. suspending while a modal dialog is up) gets +the same code on every platform. Reference counting (above) makes the +manual and automatic paths compose cleanly. + ## 6. Implementation Phases ### Phase 1 — Shared C++ facade (no new functionality) From 3c5d1d28e25bee7c5e3b33db0718ecb3beadf66e Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 15:12:59 -0700 Subject: [PATCH 12/71] Android simplification --- .../library/babylonnative/BabylonView.java | 42 +++---------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index e04d57375..07c16e66e 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -49,16 +49,6 @@ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, /** Native View handle (0 if not attached). Owned by this view. */ private long mViewHandle = 0; - /** - * The {@link android.view.Surface} that {@code mViewHandle} is currently - * bound to (null if not attached). Compared by reference in - * {@link #surfaceChanged} to decide between resize-in-place and a full - * detach + reattach: Android reuses the same {@code Surface} object for - * pure size changes (orientation, layout), and hands us a new - * {@code Surface} after a destroy/recreate cycle. - */ - private android.view.Surface mAttachedSurface = null; - public BabylonView(Context context, ViewDelegate viewDelegate) { super(context); @@ -123,6 +113,10 @@ public void eval(String source, String sourceURL) { * not normally called or subclassed by clients of BabylonView. */ public void surfaceCreated(SurfaceHolder holder) { + android.graphics.Rect frame = holder.getSurfaceFrame(); + mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, holder.getSurface(), + frame.width(), frame.height(), this.pixelDensityScale); + if (!this.mViewReady) { this.mViewDelegate.onViewReady(); this.mViewReady = true; @@ -134,14 +128,9 @@ public void surfaceCreated(SurfaceHolder holder) { * not normally called or subclassed by clients of BabylonView. */ public void surfaceDestroyed(SurfaceHolder holder) { - // The Surface backing mViewHandle is being torn down. Drop the - // View so nothing tries to render into a dead surface; the next - // surfaceChanged will reattach. (Holding onto the Runtime is - // fine — it's surface-independent.) if (mViewHandle != 0) { BabylonNative.viewDetach(mViewHandle); mViewHandle = 0; - mAttachedSurface = null; } } @@ -150,29 +139,9 @@ public void surfaceDestroyed(SurfaceHolder holder) { * not normally called or subclassed by clients of BabylonView. */ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { - if (mRuntimeHandle == 0) { - return; - } - - android.view.Surface surface = holder.getSurface(); - - // Same Surface as last attach: this is a size-only change. - // (Triggered by orientation rotation, parent layout, etc.) - if (mViewHandle != 0 && surface == mAttachedSurface) { - BabylonNative.viewResize(mViewHandle, w, h, this.pixelDensityScale); - return; - } - - // New Surface (or first attach). View::Attach contract requires - // at most one View per Runtime, so detach any stale one first. if (mViewHandle != 0) { - BabylonNative.viewDetach(mViewHandle); - mViewHandle = 0; + BabylonNative.viewResize(mViewHandle, w, h, this.pixelDensityScale); } - - mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, surface, - w, h, this.pixelDensityScale); - mAttachedSurface = (mViewHandle != 0) ? surface : null; } public interface ViewDelegate { @@ -211,7 +180,6 @@ protected void finalize() throws Throwable { if (mViewHandle != 0) { BabylonNative.viewDetach(mViewHandle); mViewHandle = 0; - mAttachedSurface = null; } if (mRuntimeHandle != 0) { BabylonNative.runtimeDestroy(mRuntimeHandle); From e611bc3716f54eb41676e91f62399bdad0d06402 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 15:35:31 -0700 Subject: [PATCH 13/71] Android simplification --- .../src/main/cpp/PlaygroundJNI.cpp | 4 +- .../library/babylonnative/BabylonView.java | 85 +++---------------- .../playground/PlaygroundActivity.java | 49 ++++++++--- 3 files changed, 52 insertions(+), 86 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp index 02ec3259c..a8d4bb9a6 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp +++ b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp @@ -23,7 +23,7 @@ extern "C" { JNIEXPORT void JNICALL -Java_com_library_babylonnative_BabylonView_loadBootstrapScripts( +Java_com_android_babylonnative_playground_PlaygroundActivity_loadBootstrapScripts( JNIEnv*, jclass, jlong runtimeHandle) { auto* runtime = Babylon::Integrations::Android::RuntimeFromHandle(runtimeHandle); @@ -33,7 +33,7 @@ Java_com_library_babylonnative_BabylonView_loadBootstrapScripts( } // Process-wide one-shot Playground setup (PerfTrace level, etc.). - // Re-calling is idempotent; safe even if multiple BabylonView + // Re-calling is idempotent; safe even if multiple PlaygroundActivity // instances queue bootstrap scripts. Playground::Initialize(); diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 07c16e66e..092c19acf 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -11,8 +11,13 @@ import com.babylonjs.integrations.BabylonNative; /** - * Playground View built on top of {@link BabylonNative}. Owns one - * Runtime + View handle pair for its lifetime. + * Playground View built on top of {@link BabylonNative}. Borrows a + * Runtime handle from the host (the host is responsible for the + * Runtime's lifetime via {@link BabylonNative#runtimeCreate(boolean)} / + * {@link BabylonNative#runtimeDestroy(long)}); this class only owns the + * View handle, which mirrors the underlying Surface lifecycle: + * attach in {@code surfaceCreated}, resize in {@code surfaceChanged}, + * detach in {@code surfaceDestroyed}. * *

Activity lifecycle: the host Activity is responsible for the * process-wide {@code androidGlobalInitialize}, {@code SetCurrentActivity}, @@ -27,40 +32,26 @@ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, private static final FrameLayout.LayoutParams childViewLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - /** - * Native helper implemented in - * {@code Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp}. - * Compiled into the same {@code libBabylonNativeIntegrations.so} - * loaded by {@link BabylonNative}; queues the Babylon.js bootstrap - * scripts (the list lives in {@code Apps/Playground/Shared/PlaygroundScripts.cpp} - * so all Playground hosts share it). - */ - private static native void loadBootstrapScripts(long runtimeHandle); - - private boolean mViewReady = false; - private final ViewDelegate mViewDelegate; private final SurfaceView primarySurfaceView; private final SurfaceView xrSurfaceView; private final float pixelDensityScale = getResources().getDisplayMetrics().density; - /** Native Runtime handle (0 if not created). Owned by this view. */ - private long mRuntimeHandle = 0; + /** Runtime handle borrowed from the host. Not owned by this view. */ + private final long mRuntimeHandle; /** Native View handle (0 if not attached). Owned by this view. */ private long mViewHandle = 0; - public BabylonView(Context context, ViewDelegate viewDelegate) { + public BabylonView(Context context, long runtimeHandle) { super(context); + mRuntimeHandle = runtimeHandle; this.primarySurfaceView = new SurfaceView(context); this.primarySurfaceView.setLayoutParams(BabylonView.childViewLayoutParams); this.primarySurfaceView.getHolder().addCallback(this); this.addView(this.primarySurfaceView); - SurfaceHolder holder = this.primarySurfaceView.getHolder(); - holder.addCallback(this); setOnTouchListener(this); - this.mViewDelegate = viewDelegate; this.xrSurfaceView = new SurfaceView(context); this.xrSurfaceView.setLayoutParams(childViewLayoutParams); @@ -72,40 +63,18 @@ public void surfaceCreated(SurfaceHolder holder) { @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - if (mRuntimeHandle != 0) { - BabylonNative.runtimeSetXrSurface(mRuntimeHandle, holder.getSurface()); - } + BabylonNative.runtimeSetXrSurface(mRuntimeHandle, holder.getSurface()); } @Override public void surfaceDestroyed(SurfaceHolder holder) { - if (mRuntimeHandle != 0) { - BabylonNative.runtimeSetXrSurface(mRuntimeHandle, null); - } + BabylonNative.runtimeSetXrSurface(mRuntimeHandle, null); } }); this.xrSurfaceView.setVisibility(View.INVISIBLE); this.addView(this.xrSurfaceView); setWillNotDraw(false); - - // Create the Runtime up-front and queue the Babylon.js bootstrap - // scripts. They will run after the first viewAttach completes - // engine initialization on the JS thread. - mRuntimeHandle = BabylonNative.runtimeCreate(/*enableDebugger*/ true); - loadBootstrapScripts(mRuntimeHandle); - } - - public void loadScript(String path) { - if (mRuntimeHandle != 0) { - BabylonNative.runtimeLoadScript(mRuntimeHandle, path); - } - } - - public void eval(String source, String sourceURL) { - if (mRuntimeHandle != 0) { - BabylonNative.runtimeEval(mRuntimeHandle, source, sourceURL); - } } /** @@ -116,11 +85,6 @@ public void surfaceCreated(SurfaceHolder holder) { android.graphics.Rect frame = holder.getSurfaceFrame(); mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, holder.getSurface(), frame.width(), frame.height(), this.pixelDensityScale); - - if (!this.mViewReady) { - this.mViewDelegate.onViewReady(); - this.mViewReady = true; - } } /** @@ -144,10 +108,6 @@ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { } } - public interface ViewDelegate { - void onViewReady(); - } - @Override public boolean onTouch(View v, MotionEvent event) { if (mViewHandle == 0) { @@ -174,22 +134,6 @@ public boolean onTouch(View v, MotionEvent event) { return true; } - @Override - protected void finalize() throws Throwable { - try { - if (mViewHandle != 0) { - BabylonNative.viewDetach(mViewHandle); - mViewHandle = 0; - } - if (mRuntimeHandle != 0) { - BabylonNative.runtimeDestroy(mRuntimeHandle); - mRuntimeHandle = 0; - } - } finally { - super.finalize(); - } - } - /** * This method is part of the SurfaceHolder.Callback2 interface, and is * not normally called or subclassed by clients of BabylonView. @@ -202,7 +146,7 @@ public void surfaceRedrawNeeded(SurfaceHolder holder) { @Override protected void onDraw(Canvas canvas) { - if (mRuntimeHandle != 0 && BabylonNative.runtimeIsXrActive(mRuntimeHandle)) { + if (BabylonNative.runtimeIsXrActive(mRuntimeHandle)) { this.xrSurfaceView.setVisibility(View.VISIBLE); } else { this.xrSurfaceView.setVisibility(View.INVISIBLE); @@ -214,4 +158,3 @@ protected void onDraw(Canvas canvas) { invalidate(); } } - diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 9b2c77529..018920a5f 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -1,23 +1,27 @@ package com.android.babylonnative.playground; -import android.Manifest; import android.app.Activity; -import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; import android.view.View; import com.babylonjs.integrations.BabylonNative; import com.library.babylonnative.BabylonView; -public class PlaygroundActivity extends Activity implements BabylonView.ViewDelegate { +public class PlaygroundActivity extends Activity { /** {@link BabylonNative#androidGlobalInitialize} is process-wide; only call it once. */ private static boolean sGlobalInitDone = false; - BabylonView mView; + /** + * Native helper bridging to {@code Apps/Playground/Shared/PlaygroundScripts.cpp}, + * which holds the Babylon.js bootstrap script list shared with the + * other Playground hosts (Win32, iOS, macOS, …). Implemented in + * {@code Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp}. + */ + private static native void loadBootstrapScripts(long runtimeHandle); + + private long mRuntimeHandle = 0; + private BabylonView mView; - // Activity life @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -32,7 +36,20 @@ protected void onCreate(Bundle icicle) { } BabylonNative.androidGlobalSetCurrentActivity(this); - mView = new BabylonView(getApplication(), this); + // Owner of the Runtime lifetime: created here, destroyed in + // onDestroy. The View only borrows the handle for its surface + // bindings. + mRuntimeHandle = BabylonNative.runtimeCreate(/*enableDebugger*/ true); + + // Queue the Babylon.js bootstrap scripts, then the playground + // experience script. Both happen synchronously from this thread; + // the Runtime queues them internally and runs them after the + // first View::Attach completes engine initialization on the JS + // thread, in submission order. + loadBootstrapScripts(mRuntimeHandle); + BabylonNative.runtimeLoadScript(mRuntimeHandle, "app:///Scripts/experience.js"); + + mView = new BabylonView(getApplication(), mRuntimeHandle); setContentView(mView); } @@ -57,6 +74,17 @@ protected void onResume() { BabylonNative.androidGlobalResume(); } + @Override + protected void onDestroy() { + // Surface lifecycle (view detach) has already fired by the time + // we get here; just release the Runtime. + if (mRuntimeHandle != 0) { + BabylonNative.runtimeDestroy(mRuntimeHandle); + mRuntimeHandle = 0; + } + super.onDestroy(); + } + @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { BabylonNative.androidGlobalRequestPermissionsResult(requestCode, permissions, results); @@ -69,9 +97,4 @@ public void onWindowFocusChanged(boolean hasFocus) { mView.setVisibility(View.VISIBLE); } } - - @Override - public void onViewReady() { - mView.loadScript("app:///Scripts/experience.js"); - } } From 710d0ad21ab88728164c4b5aabaacc0bd345d1c8 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 17:10:24 -0700 Subject: [PATCH 14/71] DPR improvements --- .../babylonjs/integrations/BabylonNative.java | 28 ++++++---- .../library/babylonnative/BabylonView.java | 20 +++++--- Apps/Playground/Win32/App.cpp | 1 - .../main/cpp/BabylonNativeIntegrations.cpp | 51 +++++++++++-------- Integrations/Apple/Source/BNView.mm | 14 ++--- .../Include/Babylon/Integrations/View.h | 44 +++++++++++----- .../Babylon/Integrations/ViewDescriptor.h | 13 ++--- Integrations/Source/View.cpp | 14 ++--- SimplifiedAPI.md | 51 ++++++++++--------- 9 files changed, 144 insertions(+), 92 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index fcb20d319..a807aaef9 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -79,22 +79,32 @@ public static native void androidGlobalRequestPermissionsResult( /** * Returns an opaque handle owned by the caller; release with {@link #viewDetach(long)}. - * {@code physicalWidth} and {@code physicalHeight} are the surface size in - * physical pixels (Android {@code View.onSizeChanged} units); {@code density} - * is {@code DisplayMetrics.density} (the physical-to-logical pixel ratio). + * {@code width} and {@code height} are the surface size in physical + * pixels (Android {@code Surface.getSurfaceFrame()} or + * {@code View.onSizeChanged} units). The Device queries the screen + * device-pixel-ratio internally; the host does not need to compute + * or pass it. */ - public static native long viewAttach(long runtimeHandle, Surface surface, - int physicalWidth, int physicalHeight, float density); + public static native long viewAttach(long runtimeHandle, Surface surface, int width, int height); public static native void viewDetach(long handle); public static native void viewRenderFrame(long handle); - public static native void viewResize(long handle, int physicalWidth, int physicalHeight, float density); + public static native void viewResize(long handle, int width, int height); - public static native void viewPointerDown(long handle, int pointerId, float x, float y); + /** + * Pointer events. {@code physicalX}/{@code physicalY} are the raw + * coordinates from {@code MotionEvent.getX/getY} (physical pixels); + * the native layer converts them to logical (CSS) pixels using + * the underlying Device's queried device-pixel-ratio before + * forwarding to NativeInput, matching the browser's + * {@code PointerEvent.clientX/clientY} convention that Babylon.js + * consumes. + */ + public static native void viewPointerDown(long handle, int pointerId, float physicalX, float physicalY); - public static native void viewPointerMove(long handle, int pointerId, float x, float y); + public static native void viewPointerMove(long handle, int pointerId, float physicalX, float physicalY); - public static native void viewPointerUp(long handle, int pointerId, float x, float y); + public static native void viewPointerUp(long handle, int pointerId, float physicalX, float physicalY); } diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 092c19acf..4ad71465e 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -19,6 +19,11 @@ * attach in {@code surfaceCreated}, resize in {@code surfaceChanged}, * detach in {@code surfaceDestroyed}. * + *

All sizes and coordinates passed to the native layer are in + * physical pixels (Android's natural unit) — the Device queries the + * screen device-pixel-ratio internally and applies any conversions + * needed at the rendering layer. + * *

Activity lifecycle: the host Activity is responsible for the * process-wide {@code androidGlobalInitialize}, {@code SetCurrentActivity}, * {@code Pause}/{@code Resume}, and {@code RequestPermissionsResult} @@ -34,7 +39,6 @@ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, private final SurfaceView primarySurfaceView; private final SurfaceView xrSurfaceView; - private final float pixelDensityScale = getResources().getDisplayMetrics().density; /** Runtime handle borrowed from the host. Not owned by this view. */ private final long mRuntimeHandle; @@ -84,7 +88,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceCreated(SurfaceHolder holder) { android.graphics.Rect frame = holder.getSurfaceFrame(); mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, holder.getSurface(), - frame.width(), frame.height(), this.pixelDensityScale); + frame.width(), frame.height()); } /** @@ -104,7 +108,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { */ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { if (mViewHandle != 0) { - BabylonNative.viewResize(mViewHandle, w, h, this.pixelDensityScale); + BabylonNative.viewResize(mViewHandle, w, h); } } @@ -115,20 +119,20 @@ public boolean onTouch(View v, MotionEvent event) { } int pointerId = event.getPointerId(event.getActionIndex()); - float mX = event.getX(event.getActionIndex()) / this.pixelDensityScale; - float mY = event.getY(event.getActionIndex()) / this.pixelDensityScale; + float x = event.getX(event.getActionIndex()); + float y = event.getY(event.getActionIndex()); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: - BabylonNative.viewPointerDown(mViewHandle, pointerId, mX, mY); + BabylonNative.viewPointerDown(mViewHandle, pointerId, x, y); break; case MotionEvent.ACTION_MOVE: - BabylonNative.viewPointerMove(mViewHandle, pointerId, mX, mY); + BabylonNative.viewPointerMove(mViewHandle, pointerId, x, y); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: - BabylonNative.viewPointerUp(mViewHandle, pointerId, mX, mY); + BabylonNative.viewPointerUp(mViewHandle, pointerId, x, y); break; } return true; diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 5470a3d0c..620afa295 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -134,7 +134,6 @@ namespace descriptor.nativeWindow = hWnd; descriptor.width = static_cast(rect.right - rect.left); descriptor.height = static_cast(rect.bottom - rect.top); - descriptor.devicePixelRatio = 1.0f; return descriptor; } diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index e5a7a5166..8b7aa9e00 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -97,16 +97,17 @@ namespace return result; } - // Convert physical pixels (Android `View.onSizeChanged` units) to - // logical pixels (the C++ ViewDescriptor convention) using the screen - // density factor. See SimplifiedAPI.md §4.2 "Pixel units". - ViewDescriptor MakeViewDescriptor(ANativeWindow* window, jint physicalW, jint physicalH, jfloat density) + // Build a ViewDescriptor from an Android Surface and its size in + // physical pixels (as Android natively reports it). The Device + // queries the screen DPR internally (see DeviceImpl_Android.cpp's + // GetDevicePixelRatio), so the host doesn't need to compute or + // pass it. + ViewDescriptor MakeViewDescriptor(ANativeWindow* window, jint width, jint height) { ViewDescriptor descriptor{}; descriptor.nativeWindow = window; - descriptor.width = static_cast(static_cast(physicalW) / density); - descriptor.height = static_cast(static_cast(physicalH) / density); - descriptor.devicePixelRatio = density; + descriptor.width = static_cast(width); + descriptor.height = static_cast(height); return descriptor; } } @@ -310,7 +311,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, JNIEXPORT jlong JNICALL Java_com_babylonjs_integrations_BabylonNative_viewAttach( JNIEnv* env, jclass, jlong runtimeHandle, jobject surface, - jint physicalW, jint physicalH, jfloat density) + jint width, jint height) { if (surface == nullptr) { @@ -322,7 +323,7 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( return 0; } auto view = View::Attach(*AsRuntime(runtimeHandle), - MakeViewDescriptor(window, physicalW, physicalH, density)); + MakeViewDescriptor(window, width, height)); if (!view) { ANativeWindow_release(window); @@ -349,35 +350,45 @@ Java_com_babylonjs_integrations_BabylonNative_viewRenderFrame(JNIEnv*, jclass, j JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewResize( - JNIEnv*, jclass, jlong handle, jint physicalW, jint physicalH, jfloat density) + JNIEnv*, jclass, jlong handle, jint width, jint height) { - AsView(handle)->Resize( - static_cast(static_cast(physicalW) / density), - static_cast(static_cast(physicalH) / density), - density); + AsView(handle)->Resize(static_cast(width), + static_cast(height)); } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat physicalX, jfloat physicalY) { - AsView(handle)->OnPointerDown(static_cast(pointerId), x, y); + View* view = AsView(handle); + const float dpr = view->DevicePixelRatio(); + view->OnPointerDown(static_cast(pointerId), + physicalX / dpr, + physicalY / dpr); } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat physicalX, jfloat physicalY) { - AsView(handle)->OnPointerMove(static_cast(pointerId), x, y); + View* view = AsView(handle); + const float dpr = view->DevicePixelRatio(); + view->OnPointerMove(static_cast(pointerId), + physicalX / dpr, + physicalY / dpr); } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerUp( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat physicalX, jfloat physicalY) { - AsView(handle)->OnPointerUp(static_cast(pointerId), x, y); + View* view = AsView(handle); + const float dpr = view->DevicePixelRatio(); + view->OnPointerUp(static_cast(pointerId), + physicalX / dpr, + physicalY / dpr); } #endif diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index fd708c958..bb48b5be9 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -15,15 +15,17 @@ namespace { // Read physical-pixel dimensions + DPR from a CAMetalLayer and - // convert to the C++ logical-pixel convention used by ViewDescriptor. + // Build a ViewDescriptor from a CAMetalLayer. drawableSize is in + // physical pixels (= surface backing buffer size), which is what + // ViewDescriptor wants directly. The Device queries the screen + // contentsScale itself for any DPR-based math; the host does not + // need to compute or pass it. Babylon::Integrations::ViewDescriptor MakeViewDescriptor(CAMetalLayer* layer) { - const CGFloat scale = layer.contentsScale > 0 ? layer.contentsScale : 1.0; Babylon::Integrations::ViewDescriptor descriptor{}; descriptor.nativeWindow = (__bridge CA::MetalLayer*)layer; - descriptor.width = static_cast(layer.drawableSize.width / scale); - descriptor.height = static_cast(layer.drawableSize.height / scale); - descriptor.devicePixelRatio = static_cast(scale); + descriptor.width = static_cast(layer.drawableSize.width); + descriptor.height = static_cast(layer.drawableSize.height); return descriptor; } } @@ -67,7 +69,7 @@ - (void)resizeForLayer:(CAMetalLayer*)layer return; } const auto descriptor = MakeViewDescriptor(layer); - _view->Resize(descriptor.width, descriptor.height, descriptor.devicePixelRatio); + _view->Resize(descriptor.width, descriptor.height); } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h index 7e51f47fb..dda23052b 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -60,25 +60,34 @@ namespace Babylon::Integrations void RenderFrame(); // Resize the bound surface. `width` and `height` are in - // **logical pixels**; `devicePixelRatio` is the physical-to-logical - // ratio (e.g. 2.0 for a Retina display). - // - // The platform interop layer is responsible for converting - // whatever its UI framework provides into this convention - // (Android `View.onSizeChanged` is in physical pixels; iOS - // `MTKViewDelegate.drawableSizeWillChange:` is in physical - // pixels; UWP `SwapChainPanel.SizeChanged` is in logical pixels; - // etc.). See SimplifiedAPI.md §4.2 "Pixel units". + // **physical pixels** — the actual pixel-buffer dimensions of + // the surface the GPU will render into. Hosts pass through + // whatever their platform's view layer reports (e.g. + // Android's `View.onSizeChanged` w/h, iOS's + // `MTKViewDelegate.drawableSizeWillChange:` size). // // Must be called from the frame thread. - void Resize(uint32_t width, uint32_t height, float devicePixelRatio = 1.0f); + void Resize(uint32_t width, uint32_t height); #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT // ----- Pointer / mouse input forwarding ----- // // Host calls these from its event loop while the view exists. - // Routed to the JS thread via `NativeInput`. Coordinates are in - // logical pixels (same convention as Resize). + // Routed to the JS thread via `NativeInput`, where Babylon.js + // consumes them as `PointerEvent.clientX/clientY`. + // + // **Coordinates are in logical (CSS) pixels** — the same unit + // a browser would deliver. This matches Babylon.js's existing + // pointer pipeline: `clientX/clientY` are CSS pixels regardless + // of the canvas backing buffer's physical resolution. + // + // Some platforms' native pointer events are already in logical + // units (iOS `UITouch` points, macOS `NSEvent`, UWP + // `PointerPoint` at `RasterizationScale = 1`); others deliver + // physical pixels (Android `MotionEvent.getX/getY`, Win32 + // `WM_POINTER*`). The host or interop layer is responsible for + // dividing by `DevicePixelRatio()` (below) when its native + // event system delivers physical pixels. // // Babylon Native distinguishes pointer (touch) input from mouse // input; both methods feed the same Babylon.js pointer-event @@ -113,6 +122,17 @@ namespace Babylon::Integrations static uint32_t MiddleMouseButton(); static uint32_t RightMouseButton(); static uint32_t MouseWheelY(); + + // Current screen device-pixel-ratio (physical/logical pixel + // ratio), as queried from the platform by the underlying + // `Babylon::Graphics::Device`. Use this to convert physical + // pointer coordinates to the logical pixels the OnPointer* / + // OnMouse* APIs expect, on platforms where the native event + // system delivers physical pixels (Android, Win32). + // + // Only valid post-Attach (the Device is constructed there); + // returns 1.0 if called pre-Attach. + float DevicePixelRatio() const; #endif private: diff --git a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h index eb07029b4..1bb3789f3 100644 --- a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h +++ b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h @@ -22,16 +22,17 @@ namespace Babylon::Integrations // UWP / WinRT : winrt::Windows::Foundation::IInspectable // (e.g. ICoreWindow, ISwapChainPanel) // - // `width` and `height` are in **logical pixels**; `devicePixelRatio` - // is the physical-to-logical ratio (e.g. 2.0 for a Retina display). - // The platform interop layer (Integrations/Android, Integrations/Apple) - // is responsible for converting whatever its UI framework hands the - // host into this convention. + // `width` and `height` are in **physical pixels** — the actual + // pixel-buffer dimensions of the surface the GPU will render into. + // Hosts pass through whatever their platform's window/view delivers + // (e.g. Android `Surface.getSurfaceFrame()` or `View.onSizeChanged` + // both report physical pixels). The Device queries the screen's + // device-pixel-ratio internally; the host does not need to compute + // or pass it. struct ViewDescriptor { Babylon::Graphics::WindowT nativeWindow{}; uint32_t width{0}; uint32_t height{0}; - float devicePixelRatio{1.0f}; }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index e0b79c649..752b9f8e1 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -261,14 +261,8 @@ namespace Babylon::Integrations impl.m_deviceUpdate->Start(); } - void View::Resize(uint32_t width, uint32_t height, float devicePixelRatio) + void View::Resize(uint32_t width, uint32_t height) { - // devicePixelRatio is informational at the C++ layer for now — - // Device computes its own DPR internally via GetDevicePixelRatio(). - // The parameter is part of the API for ViewDescriptor parity and so - // future Device-level DPR plumbing has a place to land. - (void)devicePixelRatio; - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_device) { @@ -355,5 +349,11 @@ namespace Babylon::Integrations uint32_t View::MiddleMouseButton() { return Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID; } uint32_t View::RightMouseButton() { return Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID; } uint32_t View::MouseWheelY() { return Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID; } + + float View::DevicePixelRatio() const + { + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + return impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; + } #endif } diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index 409bbab97..f5835a232 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -142,8 +142,6 @@ namespace Babylon::Integrations { // Platform-surface handle. Populated by the platform interop layer // from whatever native object the host's UI framework provides. - // The interop layer is also responsible for any unit conversion - // (see "Pixel units" below). // // `nativeWindow` is `Babylon::Graphics::WindowT`, the same per-platform // typedef the Graphics layer already uses (HWND on Win32, @@ -151,11 +149,15 @@ namespace Babylon::Integrations // X11 `Window` on Linux, winrt::IInspectable on UWP). Using the // typed handle avoids a round-trip through `void*` and gives // hosts compile-time safety. + // + // `width` and `height` are in **physical pixels** — the actual + // pixel-buffer dimensions of the surface. The Device queries the + // screen device-pixel-ratio from the system itself (see §4.2 + // "Pixel units"); the host doesn't need to compute or pass it. struct ViewDescriptor { Babylon::Graphics::WindowT nativeWindow{}; - uint32_t width; // logical pixels (see "Pixel units") - uint32_t height; // logical pixels (see "Pixel units") - float devicePixelRatio = 1.0f; + uint32_t width; + uint32_t height; }; struct RuntimeOptions { @@ -581,25 +583,28 @@ Beyond the 1:1 mirroring of `Runtime` and `View`, each interop layer owns two platform adaptations on the host's behalf so the host's UI-language code stays as simple as possible: -1. **Translate platform-natural units to the C++ contract.** The - shared C++ `View::Resize(width, height, dpr)` and `ViewDescriptor` - take **logical pixels + DPR**. Each interop layer converts - whatever its UI framework hands the host: - - **Android.** `View.onSizeChanged(int w, int h, ...)` provides - **physical pixels**. The interop layer divides by - `Resources.getSystem().getDisplayMetrics().density` and passes - the result + the density itself to the native side. The host's - Kotlin code passes the raw `w/h` it received. +1. **Pass platform-natural pixel dimensions through unchanged.** The + shared C++ `View::Resize(width, height)` and `ViewDescriptor` + take **physical pixels** — the actual pixel-buffer size of the + surface. The host hands the interop layer whatever its UI + framework gives it; the interop layer does no conversion. + - **Android.** `Surface.getSurfaceFrame()` and + `View.onSizeChanged(int w, int h, ...)` already report physical + pixels. The Kotlin/Java host passes the raw `w/h` through. - **Apple.** `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` - provides **physical pixels**; `view.contentsScale` is the DPR. - The interop layer divides and passes through. The host's Swift - code passes the `CGSize` it received (or hands over the layer - directly). - - **UWP.** `SwapChainPanel.SizeChanged` provides logical pixels + - `RasterizationScale` for DPR. The interop layer passes both - through unchanged. - - **Win32 / Linux.** No interop layer; the host C++ does the - conversion if its windowing system cares. + and `CAMetalLayer.drawableSize` are already in physical pixels. + Pass through. + - **UWP.** `SwapChainPanel.SizeChanged` reports logical pixels; + the interop layer multiplies by `RasterizationScale` to recover + physical pixels before passing to C++. + - **Win32 / Linux.** `GetClientRect` and X11's per-window pixel + dimensions are already physical. Pass through. + + The Device queries device-pixel-ratio from the system itself per + platform (`getDensityDpi() / 160` on Android, `layer.contentsScale` + on Apple, `GetDpiForWindow` on Win32, etc. — see + `Core/Graphics/Source/DeviceImpl_*.cpp`). The host doesn't + compute, store, or pass it. 2. **Expose platform-specific lifecycle entries that don't belong on the cross-platform API.** Examples from `babylon-native-bridge`: - Android: `setCurrentActivity(Activity)` → From 4c3593afe0c3b877cdcf3e2fd32c1d719516b4be Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 17:21:38 -0700 Subject: [PATCH 15/71] Remove ViewDescriptor --- Apps/Playground/Win32/App.cpp | 26 ++++++------- .../main/cpp/BabylonNativeIntegrations.cpp | 19 ++-------- Integrations/Apple/Source/BNView.mm | 31 +++++---------- Integrations/CMakeLists.txt | 1 - .../Include/Babylon/Integrations/Runtime.h | 1 - .../Include/Babylon/Integrations/View.h | 24 +++++++++--- .../Babylon/Integrations/ViewDescriptor.h | 38 ------------------- Integrations/Source/View.cpp | 16 ++++---- SimplifiedAPI.md | 31 ++++----------- 9 files changed, 56 insertions(+), 131 deletions(-) delete mode 100644 Integrations/Include/Babylon/Integrations/ViewDescriptor.h diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 620afa295..4daae1ba1 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -123,20 +123,6 @@ namespace } } - Babylon::Integrations::ViewDescriptor DescribeWindow(HWND hWnd) - { - RECT rect; - if (!GetClientRect(hWnd, &rect)) - { - throw std::exception{"Unable to get client rect"}; - } - Babylon::Integrations::ViewDescriptor descriptor{}; - descriptor.nativeWindow = hWnd; - descriptor.width = static_cast(rect.right - rect.left); - descriptor.height = static_cast(rect.bottom - rect.top); - return descriptor; - } - void Uninitialize() { // Destroy in reverse-construction order: View first (so the @@ -153,10 +139,20 @@ namespace g_runtime = Babylon::Integrations::Runtime::Create(MakeRuntimeOptions()); QueueScripts(); + RECT rect; + if (!GetClientRect(hWnd, &rect)) + { + throw std::exception{"Unable to get client rect"}; + } + // First View::Attach triggers GPU device construction, plugin // initialization on the JS thread, and flushes the queued // scripts. - g_view = Babylon::Integrations::View::Attach(*g_runtime, DescribeWindow(hWnd)); + g_view = Babylon::Integrations::View::Attach( + *g_runtime, + hWnd, + static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)); } } diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 8b7aa9e00..645a9e52e 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -44,7 +44,6 @@ namespace using Babylon::Integrations::Runtime; using Babylon::Integrations::RuntimeOptions; using Babylon::Integrations::View; - using Babylon::Integrations::ViewDescriptor; // Wraps a Runtime with two `android::global` event tickets that // auto-Suspend/Resume the Runtime in response to process-wide @@ -96,20 +95,6 @@ namespace env->ReleaseStringUTFChars(jstr, utf); return result; } - - // Build a ViewDescriptor from an Android Surface and its size in - // physical pixels (as Android natively reports it). The Device - // queries the screen DPR internally (see DeviceImpl_Android.cpp's - // GetDevicePixelRatio), so the host doesn't need to compute or - // pass it. - ViewDescriptor MakeViewDescriptor(ANativeWindow* window, jint width, jint height) - { - ViewDescriptor descriptor{}; - descriptor.nativeWindow = window; - descriptor.width = static_cast(width); - descriptor.height = static_cast(height); - return descriptor; - } } // Public handle-decoding entry point. Hosts that ship app-specific JNI @@ -323,7 +308,9 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( return 0; } auto view = View::Attach(*AsRuntime(runtimeHandle), - MakeViewDescriptor(window, width, height)); + window, + static_cast(width), + static_cast(height)); if (!view) { ANativeWindow_release(window); diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index bb48b5be9..bd3543724 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -7,29 +7,10 @@ #import #include -#include #include #include -namespace -{ - // Read physical-pixel dimensions + DPR from a CAMetalLayer and - // Build a ViewDescriptor from a CAMetalLayer. drawableSize is in - // physical pixels (= surface backing buffer size), which is what - // ViewDescriptor wants directly. The Device queries the screen - // contentsScale itself for any DPR-based math; the host does not - // need to compute or pass it. - Babylon::Integrations::ViewDescriptor MakeViewDescriptor(CAMetalLayer* layer) - { - Babylon::Integrations::ViewDescriptor descriptor{}; - descriptor.nativeWindow = (__bridge CA::MetalLayer*)layer; - descriptor.width = static_cast(layer.drawableSize.width); - descriptor.height = static_cast(layer.drawableSize.height); - return descriptor; - } -} - @implementation BNView { std::unique_ptr _view; @@ -45,7 +26,13 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer { // First attach on this runtime triggers GPU device construction // + plugin initialization + queued-script flush. - _view = Babylon::Integrations::View::Attach(*runtime.nativeRuntime, MakeViewDescriptor(layer)); + // CAMetalLayer.drawableSize is already in physical pixels, which + // is what View::Attach expects. + _view = Babylon::Integrations::View::Attach( + *runtime.nativeRuntime, + (__bridge CA::MetalLayer*)layer, + static_cast(layer.drawableSize.width), + static_cast(layer.drawableSize.height)); if (!_view) { return nil; @@ -68,8 +55,8 @@ - (void)resizeForLayer:(CAMetalLayer*)layer { return; } - const auto descriptor = MakeViewDescriptor(layer); - _view->Resize(descriptor.width, descriptor.height); + _view->Resize(static_cast(layer.drawableSize.width), + static_cast(layer.drawableSize.height)); } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index 859aa3655..12fdc3795 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -3,7 +3,6 @@ set(SOURCES "Include/Babylon/Integrations/Runtime.h" "Include/Babylon/Integrations/RuntimeOptions.h" "Include/Babylon/Integrations/View.h" - "Include/Babylon/Integrations/ViewDescriptor.h" "Source/Runtime.cpp" "Source/RuntimeImpl.h" "Source/View.cpp") diff --git a/Integrations/Include/Babylon/Integrations/Runtime.h b/Integrations/Include/Babylon/Integrations/Runtime.h index 6f9eb1fb8..f4c9311d6 100644 --- a/Integrations/Include/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Babylon/Integrations/Runtime.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h index dda23052b..808ea81eb 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -20,11 +20,25 @@ namespace Babylon::Integrations class View { public: - // Attaches a surface described by `descriptor` to `runtime`. + // Attach `nativeWindow` (the platform-specific surface handle) + // to `runtime`. + // + // `nativeWindow` is `Babylon::Graphics::WindowT`, the same + // per-platform typedef the Graphics layer already uses + // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* + // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). + // + // `width` and `height` are in **physical pixels** — the actual + // pixel-buffer dimensions of the surface. Hosts pass through + // whatever their platform's window/view delivers (Android's + // `Surface.getSurfaceFrame()`, Apple's `CAMetalLayer.drawableSize`, + // Win32's `GetClientRect`, etc.). The Device queries the screen + // device-pixel-ratio internally; the host doesn't need to + // compute or pass it. // // The first Attach on a given Runtime is the heavy step: it - // constructs `Babylon::Graphics::Device` against `descriptor`, - // dispatches GPU plugin initialization (`Device::AddToJavaScript`, + // constructs `Babylon::Graphics::Device`, dispatches GPU plugin + // initialization (`Device::AddToJavaScript`, // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, // ...), and flushes any scripts queued via `Runtime::LoadScript` // before this point. Opens the first frame. @@ -41,7 +55,7 @@ namespace Babylon::Integrations // // Must be called from the same thread that will call // `RenderFrame` and `Resize` (the "frame thread"). - static std::unique_ptr Attach(Runtime& runtime, const ViewDescriptor& descriptor); + static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow, uint32_t width, uint32_t height); ~View(); diff --git a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h b/Integrations/Include/Babylon/Integrations/ViewDescriptor.h deleted file mode 100644 index 1bb3789f3..000000000 --- a/Integrations/Include/Babylon/Integrations/ViewDescriptor.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -#include - -namespace Babylon::Integrations -{ - // Description of a platform surface that a `View` will render - // into. Populated by the platform interop layer (or directly by a - // C++ host on Win32 / Linux) from whatever native object the - // host's UI framework provides. - // - // `nativeWindow` is `Babylon::Graphics::WindowT`, which is the - // platform-specific native window handle that `Babylon::Graphics` - // already understands: - // - // Win32 : HWND - // Android : ANativeWindow* - // iOS / macOS / visionOS : CA::MetalLayer* (metal-cpp wrapper) - // X11 (Linux) : Window (X11 XID — `unsigned long`) - // UWP / WinRT : winrt::Windows::Foundation::IInspectable - // (e.g. ICoreWindow, ISwapChainPanel) - // - // `width` and `height` are in **physical pixels** — the actual - // pixel-buffer dimensions of the surface the GPU will render into. - // Hosts pass through whatever their platform's window/view delivers - // (e.g. Android `Surface.getSurfaceFrame()` or `View.onSizeChanged` - // both report physical pixels). The Device queries the screen's - // device-pixel-ratio internally; the host does not need to compute - // or pass it. - struct ViewDescriptor - { - Babylon::Graphics::WindowT nativeWindow{}; - uint32_t width{0}; - uint32_t height{0}; - }; -} diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 752b9f8e1..d09e0c37b 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -161,7 +161,7 @@ namespace Babylon::Integrations // --------------------------------------------------------------------- // View::Attach (first time and subsequent) // --------------------------------------------------------------------- - std::unique_ptr View::Attach(Runtime& runtime, const ViewDescriptor& descriptor) + std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow, uint32_t width, uint32_t height) { RuntimeImpl& impl = *runtime.m_impl; @@ -171,17 +171,15 @@ namespace Babylon::Integrations return nullptr; } - const auto& window = descriptor.nativeWindow; - if (!impl.m_device) { // First Attach on this Runtime: construct the Device and // open the first frame, then dispatch the engine-init lambda // onto the JS thread. Babylon::Graphics::Configuration config{}; - config.Window = window; - config.Width = descriptor.width; - config.Height = descriptor.height; + config.Window = nativeWindow; + config.Width = width; + config.Height = height; config.MSAASamples = impl.m_options.msaaSamples; impl.m_device.emplace(config); @@ -197,15 +195,15 @@ namespace Babylon::Integrations impl.m_device->StartRenderingCurrentFrame(); impl.m_deviceUpdate->Start(); - RunFirstAttachInit(impl, window); + RunFirstAttachInit(impl, nativeWindow); } else { // Subsequent Attach: reuse the existing Device, just rebind // the surface and re-enable rendering. Plugins, polyfills, // and any loaded scripts are preserved on the JS side. - impl.m_device->UpdateWindow(window); - impl.m_device->UpdateSize(descriptor.width, descriptor.height); + impl.m_device->UpdateWindow(nativeWindow); + impl.m_device->UpdateSize(width, height); impl.m_device->EnableRendering(); impl.m_device->StartRenderingCurrentFrame(); impl.m_deviceUpdate->Start(); diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index f5835a232..a67f06146 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -140,26 +140,6 @@ exists. ```cpp namespace Babylon::Integrations { - // Platform-surface handle. Populated by the platform interop layer - // from whatever native object the host's UI framework provides. - // - // `nativeWindow` is `Babylon::Graphics::WindowT`, the same per-platform - // typedef the Graphics layer already uses (HWND on Win32, - // ANativeWindow* on Android, CA::MetalLayer* on Apple, - // X11 `Window` on Linux, winrt::IInspectable on UWP). Using the - // typed handle avoids a round-trip through `void*` and gives - // hosts compile-time safety. - // - // `width` and `height` are in **physical pixels** — the actual - // pixel-buffer dimensions of the surface. The Device queries the - // screen device-pixel-ratio from the system itself (see §4.2 - // "Pixel units"); the host doesn't need to compute or pass it. - struct ViewDescriptor { - Babylon::Graphics::WindowT nativeWindow{}; - uint32_t width; - uint32_t height; - }; - struct RuntimeOptions { uint32_t msaaSamples = 4; bool enableDebugger = false; @@ -261,7 +241,10 @@ namespace Babylon::Integrations // Detach (~View) closes the in-flight frame and calls // `Device::DisableRendering`. The Device persists on the // Runtime, so the next Attach is fast. - static std::unique_ptr Attach(Runtime& runtime, const ViewDescriptor& handle); + static std::unique_ptr Attach(Runtime& runtime, + Babylon::Graphics::WindowT nativeWindow, + uint32_t width, + uint32_t height); // Render exactly one frame. Must be called from the same thread // each time (the "frame thread"). No-op if the runtime is @@ -584,7 +567,7 @@ owns two platform adaptations on the host's behalf so the host's UI-language code stays as simple as possible: 1. **Pass platform-natural pixel dimensions through unchanged.** The - shared C++ `View::Resize(width, height)` and `ViewDescriptor` + shared C++ `View::Attach(... nativeWindow, w, h)` and `View::Resize(w, h)` take **physical pixels** — the actual pixel-buffer size of the surface. The host hands the interop layer whatever its UI framework gives it; the interop layer does no conversion. @@ -718,9 +701,9 @@ Method-level subset of the C++ header, illustrating the pattern: ```cpp class View { public: - static std::unique_ptr Attach(Runtime&, const ViewDescriptor&); + static std::unique_ptr Attach(Runtime&, Babylon::Graphics::WindowT, uint32_t w, uint32_t h); void RenderFrame(); - void Resize(uint32_t w, uint32_t h, float dpr = 1.0f); + void Resize(uint32_t w, uint32_t h); #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT void OnPointerDown(int32_t pointerId, float x, float y); From a6cbe1f165dc9720a540bbe93cb9995998001664 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 18:40:27 -0700 Subject: [PATCH 16/71] Platform specific query window dimensions and coerce input coordinates --- .../babylonjs/integrations/BabylonNative.java | 27 ++++------ .../library/babylonnative/BabylonView.java | 4 +- Apps/Playground/Win32/App.cpp | 14 +---- .../main/cpp/BabylonNativeIntegrations.cpp | 32 +++-------- Integrations/Apple/Source/BNView.mm | 10 ++-- Integrations/CMakeLists.txt | 12 ++++- .../Include/Babylon/Integrations/Runtime.h | 1 + .../Include/Babylon/Integrations/View.h | 48 ++++++----------- Integrations/Source/RuntimeImpl.h | 17 +++++- Integrations/Source/View.cpp | 44 ++++++++------- Integrations/Source/ViewImpl_Android.cpp | 27 ++++++++++ Integrations/Source/ViewImpl_Unix.cpp | 47 ++++++++++++++++ Integrations/Source/ViewImpl_Win32.cpp | 26 +++++++++ Integrations/Source/ViewImpl_WinRT.cpp | 36 +++++++++++++ Integrations/Source/ViewImpl_iOS.mm | 27 ++++++++++ Integrations/Source/ViewImpl_macOS.mm | 24 +++++++++ Integrations/Source/ViewImpl_visionOS.mm | 25 +++++++++ SimplifiedAPI.md | 54 ++++++++++--------- 18 files changed, 334 insertions(+), 141 deletions(-) create mode 100644 Integrations/Source/ViewImpl_Android.cpp create mode 100644 Integrations/Source/ViewImpl_Unix.cpp create mode 100644 Integrations/Source/ViewImpl_Win32.cpp create mode 100644 Integrations/Source/ViewImpl_WinRT.cpp create mode 100644 Integrations/Source/ViewImpl_iOS.mm create mode 100644 Integrations/Source/ViewImpl_macOS.mm create mode 100644 Integrations/Source/ViewImpl_visionOS.mm diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index a807aaef9..b5c5a9c9a 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -79,13 +79,11 @@ public static native void androidGlobalRequestPermissionsResult( /** * Returns an opaque handle owned by the caller; release with {@link #viewDetach(long)}. - * {@code width} and {@code height} are the surface size in physical - * pixels (Android {@code Surface.getSurfaceFrame()} or - * {@code View.onSizeChanged} units). The Device queries the screen - * device-pixel-ratio internally; the host does not need to compute - * or pass it. + * The View queries the surface's pixel-buffer size from the native + * window itself, and the Device queries the screen device-pixel-ratio + * from the system, so no dimensional info needs to be passed from Java. */ - public static native long viewAttach(long runtimeHandle, Surface surface, int width, int height); + public static native long viewAttach(long runtimeHandle, Surface surface); public static native void viewDetach(long handle); @@ -94,17 +92,14 @@ public static native void androidGlobalRequestPermissionsResult( public static native void viewResize(long handle, int width, int height); /** - * Pointer events. {@code physicalX}/{@code physicalY} are the raw - * coordinates from {@code MotionEvent.getX/getY} (physical pixels); - * the native layer converts them to logical (CSS) pixels using - * the underlying Device's queried device-pixel-ratio before - * forwarding to NativeInput, matching the browser's - * {@code PointerEvent.clientX/clientY} convention that Babylon.js - * consumes. + * Pointer events. Pass {@code MotionEvent.getX/getY} through unchanged + * (Android-native physical-pixel coordinates). The View internally + * normalizes to logical (CSS) pixels — the unit Babylon.js's + * {@code PointerEvent.clientX/clientY} pipeline expects. */ - public static native void viewPointerDown(long handle, int pointerId, float physicalX, float physicalY); + public static native void viewPointerDown(long handle, int pointerId, float x, float y); - public static native void viewPointerMove(long handle, int pointerId, float physicalX, float physicalY); + public static native void viewPointerMove(long handle, int pointerId, float x, float y); - public static native void viewPointerUp(long handle, int pointerId, float physicalX, float physicalY); + public static native void viewPointerUp(long handle, int pointerId, float x, float y); } diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 4ad71465e..a7b02de79 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -86,9 +86,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { * not normally called or subclassed by clients of BabylonView. */ public void surfaceCreated(SurfaceHolder holder) { - android.graphics.Rect frame = holder.getSurfaceFrame(); - mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, holder.getSurface(), - frame.width(), frame.height()); + mViewHandle = BabylonNative.viewAttach(mRuntimeHandle, holder.getSurface()); } /** diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 4daae1ba1..999f162d2 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -139,20 +139,10 @@ namespace g_runtime = Babylon::Integrations::Runtime::Create(MakeRuntimeOptions()); QueueScripts(); - RECT rect; - if (!GetClientRect(hWnd, &rect)) - { - throw std::exception{"Unable to get client rect"}; - } - // First View::Attach triggers GPU device construction, plugin // initialization on the JS thread, and flushes the queued - // scripts. - g_view = Babylon::Integrations::View::Attach( - *g_runtime, - hWnd, - static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top)); + // scripts. The View queries the HWND's client rect itself. + g_view = Babylon::Integrations::View::Attach(*g_runtime, hWnd); } } diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 645a9e52e..ee5d1741c 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -295,8 +295,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, JNIEXPORT jlong JNICALL Java_com_babylonjs_integrations_BabylonNative_viewAttach( - JNIEnv* env, jclass, jlong runtimeHandle, jobject surface, - jint width, jint height) + JNIEnv* env, jclass, jlong runtimeHandle, jobject surface) { if (surface == nullptr) { @@ -307,10 +306,7 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( { return 0; } - auto view = View::Attach(*AsRuntime(runtimeHandle), - window, - static_cast(width), - static_cast(height)); + auto view = View::Attach(*AsRuntime(runtimeHandle), window); if (!view) { ANativeWindow_release(window); @@ -347,35 +343,23 @@ Java_com_babylonjs_integrations_BabylonNative_viewResize( JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat physicalX, jfloat physicalY) + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { - View* view = AsView(handle); - const float dpr = view->DevicePixelRatio(); - view->OnPointerDown(static_cast(pointerId), - physicalX / dpr, - physicalY / dpr); + AsView(handle)->OnPointerDown(static_cast(pointerId), x, y); } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat physicalX, jfloat physicalY) + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { - View* view = AsView(handle); - const float dpr = view->DevicePixelRatio(); - view->OnPointerMove(static_cast(pointerId), - physicalX / dpr, - physicalY / dpr); + AsView(handle)->OnPointerMove(static_cast(pointerId), x, y); } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerUp( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat physicalX, jfloat physicalY) + JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { - View* view = AsView(handle); - const float dpr = view->DevicePixelRatio(); - view->OnPointerUp(static_cast(pointerId), - physicalX / dpr, - physicalY / dpr); + AsView(handle)->OnPointerUp(static_cast(pointerId), x, y); } #endif diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index bd3543724..31e40f280 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -25,14 +25,12 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer if ((self = [super init])) { // First attach on this runtime triggers GPU device construction - // + plugin initialization + queued-script flush. - // CAMetalLayer.drawableSize is already in physical pixels, which - // is what View::Attach expects. + // + plugin initialization + queued-script flush. The View + // queries the layer's drawableSize itself; the host doesn't + // need to pass dimensions. _view = Babylon::Integrations::View::Attach( *runtime.nativeRuntime, - (__bridge CA::MetalLayer*)layer, - static_cast(layer.drawableSize.width), - static_cast(layer.drawableSize.height)); + (__bridge CA::MetalLayer*)layer); if (!_view) { return nil; diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index 12fdc3795..dfdecefec 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -5,7 +5,8 @@ set(SOURCES "Include/Babylon/Integrations/View.h" "Source/Runtime.cpp" "Source/RuntimeImpl.h" - "Source/View.cpp") + "Source/View.cpp" + "Source/ViewImpl_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}") add_library(Integrations ${SOURCES}) @@ -19,7 +20,7 @@ target_include_directories(Integrations PUBLIC "Include") # XMLHttpRequest) are always linked. target_link_libraries(Integrations PUBLIC napi - # GraphicsDevice is PUBLIC because ViewDescriptor.h (a public header) + # GraphicsDevice is PUBLIC because View.h (a public header) # references `Babylon::Graphics::WindowT` from . PUBLIC GraphicsDevice PRIVATE AppRuntime @@ -30,6 +31,13 @@ target_link_libraries(Integrations PRIVATE TextDecoder PRIVATE XMLHttpRequest) +# ----- Per-platform link dependencies for ViewImpl_*.{cpp,mm} ----- +if(ANDROID) + target_link_libraries(Integrations PRIVATE android) # ANativeWindow_getWidth/Height +elseif(UNIX AND NOT APPLE) + target_link_libraries(Integrations PRIVATE X11) # XOpenDisplay / XGetGeometry +endif() + # ----- Conditionally-included polyfills ----- # # Each flag is exposed as a PUBLIC compile definition so the public diff --git a/Integrations/Include/Babylon/Integrations/Runtime.h b/Integrations/Include/Babylon/Integrations/Runtime.h index f4c9311d6..906a08038 100644 --- a/Integrations/Include/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Babylon/Integrations/Runtime.h @@ -115,6 +115,7 @@ namespace Babylon::Integrations private: friend class View; + friend struct ViewImpl; Runtime(); diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h index 808ea81eb..66e7ad076 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -27,14 +27,12 @@ namespace Babylon::Integrations // per-platform typedef the Graphics layer already uses // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). - // - // `width` and `height` are in **physical pixels** — the actual - // pixel-buffer dimensions of the surface. Hosts pass through - // whatever their platform's window/view delivers (Android's - // `Surface.getSurfaceFrame()`, Apple's `CAMetalLayer.drawableSize`, - // Win32's `GetClientRect`, etc.). The Device queries the screen - // device-pixel-ratio internally; the host doesn't need to - // compute or pass it. + // The View queries the surface's pixel-buffer size from the + // window itself (Android `ANativeWindow_getWidth/Height`, + // Apple `CAMetalLayer.drawableSize`, Win32 `GetClientRect`, + // X11 `XGetGeometry`, UWP bounds × scale) — the host doesn't + // pass dimensions. The Device queries the screen + // device-pixel-ratio internally as well. // // The first Attach on a given Runtime is the heavy step: it // constructs `Babylon::Graphics::Device`, dispatches GPU plugin @@ -55,7 +53,7 @@ namespace Babylon::Integrations // // Must be called from the same thread that will call // `RenderFrame` and `Resize` (the "frame thread"). - static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow, uint32_t width, uint32_t height); + static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow); ~View(); @@ -90,18 +88,15 @@ namespace Babylon::Integrations // Routed to the JS thread via `NativeInput`, where Babylon.js // consumes them as `PointerEvent.clientX/clientY`. // - // **Coordinates are in logical (CSS) pixels** — the same unit - // a browser would deliver. This matches Babylon.js's existing - // pointer pipeline: `clientX/clientY` are CSS pixels regardless - // of the canvas backing buffer's physical resolution. - // - // Some platforms' native pointer events are already in logical - // units (iOS `UITouch` points, macOS `NSEvent`, UWP - // `PointerPoint` at `RasterizationScale = 1`); others deliver - // physical pixels (Android `MotionEvent.getX/getY`, Win32 - // `WM_POINTER*`). The host or interop layer is responsible for - // dividing by `DevicePixelRatio()` (below) when its native - // event system delivers physical pixels. + // **Pass coordinates in whatever unit your platform's native + // event system delivers.** The View internally normalizes to + // logical (CSS) pixels — the unit Babylon.js expects — using + // a per-platform helper. On platforms whose native pointer + // events are already in logical units (iOS `UITouch`, macOS + // `NSEvent`, UWP `PointerPoint`), this is a passthrough; on + // platforms that deliver physical pixels (Android `MotionEvent`, + // Win32 `WM_POINTER*`, X11 button events), the View divides by + // the Device's queried device-pixel-ratio. // // Babylon Native distinguishes pointer (touch) input from mouse // input; both methods feed the same Babylon.js pointer-event @@ -136,17 +131,6 @@ namespace Babylon::Integrations static uint32_t MiddleMouseButton(); static uint32_t RightMouseButton(); static uint32_t MouseWheelY(); - - // Current screen device-pixel-ratio (physical/logical pixel - // ratio), as queried from the platform by the underlying - // `Babylon::Graphics::Device`. Use this to convert physical - // pointer coordinates to the logical pixels the OnPointer* / - // OnMouse* APIs expect, on platforms where the native event - // system delivers physical pixels (Android, Win32). - // - // Only valid post-Attach (the Device is constructed there); - // returns 1.0 if called pre-Attach. - float DevicePixelRatio() const; #endif private: diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index f4919358b..0147ea45c 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -24,6 +24,7 @@ #include #include #include +#include namespace Babylon::Integrations { @@ -101,10 +102,24 @@ namespace Babylon::Integrations }; // Internal implementation of View. Holds the back-reference to the - // Runtime that produced it. + // Runtime that produced it. Provides per-platform helpers + // (implemented in ViewImpl_*.cpp / .mm). struct ViewImpl { explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} Runtime& m_runtime; + + // Query the surface's pixel-buffer size from the native window + // handle. Implemented per-platform. + static std::pair QuerySize(Babylon::Graphics::WindowT window); + + // Convert native pointer-event coordinates to the logical (CSS) + // pixels the NativeInput / Babylon.js pointer pipeline expects. + // On platforms whose native event system already delivers + // logical units (iOS, macOS, UWP), this is a passthrough; on + // platforms that deliver physical pixels (Android, Win32, X11), + // this divides by the Device's queried device-pixel-ratio. + // Implemented per-platform. + std::pair ToLogicalCoords(float x, float y) const; }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index d09e0c37b..24887b931 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -161,7 +161,7 @@ namespace Babylon::Integrations // --------------------------------------------------------------------- // View::Attach (first time and subsequent) // --------------------------------------------------------------------- - std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow, uint32_t width, uint32_t height) + std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) { RuntimeImpl& impl = *runtime.m_impl; @@ -171,6 +171,12 @@ namespace Babylon::Integrations return nullptr; } + // Per-platform: query the surface's pixel-buffer size from the + // native window handle. ViewImpl_*.cpp implements this; e.g. + // ANativeWindow_getWidth on Android, GetClientRect on Win32, + // CAMetalLayer.drawableSize on Apple. + const auto [width, height] = ViewImpl::QuerySize(nativeWindow); + if (!impl.m_device) { // First Attach on this Runtime: construct the Device and @@ -274,9 +280,10 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_input) { + const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); impl.m_input->TouchDown(static_cast(pointerId), - static_cast(x), - static_cast(y)); + static_cast(lx), + static_cast(ly)); } } @@ -285,9 +292,10 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_input) { + const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); impl.m_input->TouchMove(static_cast(pointerId), - static_cast(x), - static_cast(y)); + static_cast(lx), + static_cast(ly)); } } @@ -296,9 +304,10 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_input) { + const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); impl.m_input->TouchUp(static_cast(pointerId), - static_cast(x), - static_cast(y)); + static_cast(lx), + static_cast(ly)); } } @@ -307,9 +316,10 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_input) { + const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); impl.m_input->MouseDown(buttonIndex, - static_cast(x), - static_cast(y)); + static_cast(lx), + static_cast(ly)); } } @@ -318,9 +328,10 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_input) { + const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); impl.m_input->MouseUp(buttonIndex, - static_cast(x), - static_cast(y)); + static_cast(lx), + static_cast(ly)); } } @@ -329,8 +340,9 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_input) { - impl.m_input->MouseMove(static_cast(x), - static_cast(y)); + const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + impl.m_input->MouseMove(static_cast(lx), + static_cast(ly)); } } @@ -347,11 +359,5 @@ namespace Babylon::Integrations uint32_t View::MiddleMouseButton() { return Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID; } uint32_t View::RightMouseButton() { return Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID; } uint32_t View::MouseWheelY() { return Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID; } - - float View::DevicePixelRatio() const - { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - return impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; - } #endif } diff --git a/Integrations/Source/ViewImpl_Android.cpp b/Integrations/Source/ViewImpl_Android.cpp new file mode 100644 index 000000000..7a5660066 --- /dev/null +++ b/Integrations/Source/ViewImpl_Android.cpp @@ -0,0 +1,27 @@ +#include "RuntimeImpl.h" + +#include + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + if (window == nullptr) + { + return {0, 0}; + } + return {static_cast(ANativeWindow_getWidth(window)), + static_cast(ANativeWindow_getHeight(window))}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // Android `MotionEvent.getX/getY` returns coordinates in physical + // pixels (the SurfaceView's pixel space). NativeInput / Babylon.js + // expect logical (CSS) pixels — divide by the device-pixel-ratio + // the underlying Device has cached. + const auto& impl = *m_runtime.m_impl; + const float dpr = impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; + return {x / dpr, y / dpr}; + } +} diff --git a/Integrations/Source/ViewImpl_Unix.cpp b/Integrations/Source/ViewImpl_Unix.cpp new file mode 100644 index 000000000..ac4c1de3f --- /dev/null +++ b/Integrations/Source/ViewImpl_Unix.cpp @@ -0,0 +1,47 @@ +#include "RuntimeImpl.h" + +#include + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + if (window == 0) + { + return {0, 0}; + } + + // X11 `Window` is just an XID; querying its geometry needs a + // Display connection. Open one transiently — same pattern as + // `Core/Graphics/Source/DeviceImpl_Unix.cpp::GetDevicePixelRatio`. + // (See https://github.com/BabylonJS/BabylonNative/issues/625.) + Display* display = XOpenDisplay(nullptr); + if (display == nullptr) + { + return {0, 0}; + } + + ::Window root{}; + int x{}, y{}; + unsigned int width{}, height{}, borderWidth{}, depth{}; + Status status = XGetGeometry(display, window, &root, &x, &y, + &width, &height, &borderWidth, &depth); + + XCloseDisplay(display); + + if (status == 0) + { + return {0, 0}; + } + return {width, height}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // X11 button-event coordinates are in physical pixels. Divide + // by the Device's queried DPR. + const auto& impl = *m_runtime.m_impl; + const float dpr = impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; + return {x / dpr, y / dpr}; + } +} diff --git a/Integrations/Source/ViewImpl_Win32.cpp b/Integrations/Source/ViewImpl_Win32.cpp new file mode 100644 index 000000000..46a49d016 --- /dev/null +++ b/Integrations/Source/ViewImpl_Win32.cpp @@ -0,0 +1,26 @@ +#include "RuntimeImpl.h" + +#include + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + RECT rect{}; + if (window == nullptr || !GetClientRect(window, &rect)) + { + return {0, 0}; + } + return {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // Win32 `WM_MOUSE*` / `WM_POINTER*` coordinates are in physical + // pixels for DPI-aware apps. Divide by the Device's queried DPR. + const auto& impl = *m_runtime.m_impl; + const float dpr = impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; + return {x / dpr, y / dpr}; + } +} diff --git a/Integrations/Source/ViewImpl_WinRT.cpp b/Integrations/Source/ViewImpl_WinRT.cpp new file mode 100644 index 000000000..8ff50ba19 --- /dev/null +++ b/Integrations/Source/ViewImpl_WinRT.cpp @@ -0,0 +1,36 @@ +#include "RuntimeImpl.h" + +#include +#include + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + // WindowT here is `winrt::Windows::Foundation::IInspectable` + // wrapping one of: ICoreWindow, ISwapChainPanel, + // Microsoft::UI::Xaml::Controls::ISwapChainPanel. + if (auto coreWindow = window.try_as()) + { + const auto bounds = coreWindow.Bounds(); + // CoreWindow.Bounds is in DIPs; multiply by DPI scale to get physical pixels. + // For now we return DIPs-as-pixels; hosts targeting hi-DPI may + // adjust by RasterizationScale / DisplayInformation.LogicalDpi. + return {static_cast(bounds.Width), + static_cast(bounds.Height)}; + } + if (auto panel = window.try_as()) + { + const auto fe = panel.as(); + return {static_cast(fe.ActualWidth() * panel.CompositionScaleX()), + static_cast(fe.ActualHeight() * panel.CompositionScaleY())}; + } + return {0, 0}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // UWP `PointerPoint.Position` is in DIPs (logical) — passthrough. + return {x, y}; + } +} diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm new file mode 100644 index 000000000..94de9a663 --- /dev/null +++ b/Integrations/Source/ViewImpl_iOS.mm @@ -0,0 +1,27 @@ +#include "RuntimeImpl.h" + +#import + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + if (window == nullptr) + { + return {0, 0}; + } + // metal-cpp's CA::MetalLayer* can be bridge-cast to the Obj-C + // CAMetalLayer*; drawableSize is in physical pixels. + CAMetalLayer* layer = (__bridge CAMetalLayer*)window; + const CGSize size = layer.drawableSize; + return {static_cast(size.width), + static_cast(size.height)}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // UIKit `UITouch.locationInView` and AppKit `NSEvent.locationInWindow` + // are already in logical points — passthrough. + return {x, y}; + } +} diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm new file mode 100644 index 000000000..6d8c49609 --- /dev/null +++ b/Integrations/Source/ViewImpl_macOS.mm @@ -0,0 +1,24 @@ +#include "RuntimeImpl.h" + +#import + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + if (window == nullptr) + { + return {0, 0}; + } + CAMetalLayer* layer = (__bridge CAMetalLayer*)window; + const CGSize size = layer.drawableSize; + return {static_cast(size.width), + static_cast(size.height)}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // AppKit `NSEvent.locationInWindow` is in logical points — passthrough. + return {x, y}; + } +} diff --git a/Integrations/Source/ViewImpl_visionOS.mm b/Integrations/Source/ViewImpl_visionOS.mm new file mode 100644 index 000000000..649ac7a74 --- /dev/null +++ b/Integrations/Source/ViewImpl_visionOS.mm @@ -0,0 +1,25 @@ +#include "RuntimeImpl.h" + +#import + +namespace Babylon::Integrations +{ + std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + { + if (window == nullptr) + { + return {0, 0}; + } + CAMetalLayer* layer = (__bridge CAMetalLayer*)window; + const CGSize size = layer.drawableSize; + return {static_cast(size.width), + static_cast(size.height)}; + } + + std::pair ViewImpl::ToLogicalCoords(float x, float y) const + { + // visionOS pointer interactions arrive through SwiftUI/RealityKit + // gesture recognizers in logical points — passthrough. + return {x, y}; + } +} diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index a67f06146..662e68bf4 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -242,9 +242,7 @@ namespace Babylon::Integrations // `Device::DisableRendering`. The Device persists on the // Runtime, so the next Attach is fast. static std::unique_ptr Attach(Runtime& runtime, - Babylon::Graphics::WindowT nativeWindow, - uint32_t width, - uint32_t height); + Babylon::Graphics::WindowT nativeWindow); // Render exactly one frame. Must be called from the same thread // each time (the "frame thread"). No-op if the runtime is @@ -566,28 +564,32 @@ Beyond the 1:1 mirroring of `Runtime` and `View`, each interop layer owns two platform adaptations on the host's behalf so the host's UI-language code stays as simple as possible: -1. **Pass platform-natural pixel dimensions through unchanged.** The - shared C++ `View::Attach(... nativeWindow, w, h)` and `View::Resize(w, h)` - take **physical pixels** — the actual pixel-buffer size of the - surface. The host hands the interop layer whatever its UI - framework gives it; the interop layer does no conversion. - - **Android.** `Surface.getSurfaceFrame()` and - `View.onSizeChanged(int w, int h, ...)` already report physical - pixels. The Kotlin/Java host passes the raw `w/h` through. - - **Apple.** `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` - and `CAMetalLayer.drawableSize` are already in physical pixels. - Pass through. - - **UWP.** `SwapChainPanel.SizeChanged` reports logical pixels; - the interop layer multiplies by `RasterizationScale` to recover - physical pixels before passing to C++. - - **Win32 / Linux.** `GetClientRect` and X11's per-window pixel - dimensions are already physical. Pass through. - - The Device queries device-pixel-ratio from the system itself per - platform (`getDensityDpi() / 160` on Android, `layer.contentsScale` - on Apple, `GetDpiForWindow` on Win32, etc. — see - `Core/Graphics/Source/DeviceImpl_*.cpp`). The host doesn't - compute, store, or pass it. +1. **Translate platform-native objects/units to the cross-platform C++ + contract — but no conversion math.** Per-platform helpers in + `Integrations/Source/ViewImpl_*.cpp` (mirroring the existing + `Core/Graphics/Source/DeviceImpl_*.cpp` pattern) handle the platform + facts so the interop layer can stay a pure ABI bridge: + - **Querying the surface's pixel-buffer size from the native window + handle** — `ANativeWindow_getWidth/Height` on Android, `GetClientRect` + on Win32, `CAMetalLayer.drawableSize` on Apple, `XGetGeometry` on + X11, `Bounds × scale` on UWP. `View::Attach(runtime, nativeWindow)` + does this internally — no host-supplied dimensions, no pixel-unit + bookkeeping crossing the JNI / Obj-C++ boundary. + - **Converting native pointer-event coordinates to logical (CSS) + pixels.** Babylon.js consumes pointer events as + `PointerEvent.clientX/clientY` (CSS pixels). On platforms whose + native event system is already in logical units (iOS `UITouch`, + macOS `NSEvent`, UWP `PointerPoint`), `View`'s per-platform + `ToLogicalCoords` is a passthrough; on platforms that deliver + physical pixels (Android `MotionEvent`, Win32 `WM_POINTER*`, X11 + button events), it divides by the Device's queried + device-pixel-ratio. Hosts pass coordinates from their native event + directly; the View handles the conversion. + + Resize is the one exception: hosts already have the new dimensions + from their resize event, so `View::Resize(w, h)` takes them + explicitly rather than re-querying the window — same convention as + `Babylon::Graphics::Device::UpdateSize`. 2. **Expose platform-specific lifecycle entries that don't belong on the cross-platform API.** Examples from `babylon-native-bridge`: - Android: `setCurrentActivity(Activity)` → @@ -701,7 +703,7 @@ Method-level subset of the C++ header, illustrating the pattern: ```cpp class View { public: - static std::unique_ptr Attach(Runtime&, Babylon::Graphics::WindowT, uint32_t w, uint32_t h); + static std::unique_ptr Attach(Runtime&, Babylon::Graphics::WindowT); void RenderFrame(); void Resize(uint32_t w, uint32_t h); From baa9738a2d60ff2d1f7552d6a9b80be01c3a6dd2 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 4 May 2026 21:40:38 -0700 Subject: [PATCH 17/71] QueueScripts -> LoadScripts --- Apps/Playground/Win32/App.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 999f162d2..030f927f2 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -102,7 +102,7 @@ namespace return options; } - void QueueScripts() + void LoadScripts() { // Babylon.js bootstrap (core + loaders/materials/gui/serializers). // Shared with the other Playground hosts via Shared/PlaygroundScripts. @@ -137,7 +137,7 @@ namespace Uninitialize(); g_runtime = Babylon::Integrations::Runtime::Create(MakeRuntimeOptions()); - QueueScripts(); + LoadScripts(); // First View::Attach triggers GPU device construction, plugin // initialization on the JS thread, and flushes the queued From 3add67472fabfedf2a4427dd963aa09b755e1b52 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 5 May 2026 09:46:09 -0700 Subject: [PATCH 18/71] Port iOS Playground app (untested) --- Apps/Playground/CMakeLists.txt | 11 ++- Apps/Playground/iOS/AppDelegate.swift | 37 +++---- Apps/Playground/iOS/LibNativeBridge.h | 21 ---- Apps/Playground/iOS/LibNativeBridge.mm | 99 ------------------- .../iOS/Playground-Bridging-Header.h | 11 +++ Apps/Playground/iOS/PlaygroundBootstrap.h | 24 +++++ Apps/Playground/iOS/PlaygroundBootstrap.mm | 33 +++++++ Apps/Playground/iOS/ViewController.swift | 60 +++++------ CMakeLists.txt | 5 +- Integrations/Apple/CMakeLists.txt | 3 +- Integrations/Apple/Source/BNRuntime.mm | 36 ++++++- Integrations/Apple/Source/BNView.mm | 8 +- .../BabylonNativeIntegrations/BNRuntime.h | 22 +++++ .../BabylonNativeIntegrations/BNView.h | 27 +++-- 14 files changed, 204 insertions(+), 193 deletions(-) delete mode 100644 Apps/Playground/iOS/LibNativeBridge.h delete mode 100644 Apps/Playground/iOS/LibNativeBridge.mm create mode 100644 Apps/Playground/iOS/Playground-Bridging-Header.h create mode 100644 Apps/Playground/iOS/PlaygroundBootstrap.h create mode 100644 Apps/Playground/iOS/PlaygroundBootstrap.mm diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index da2be9518..cf41657ea 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -33,12 +33,15 @@ if(APPLE) "${CMAKE_CURRENT_LIST_DIR}/iOS/Base.lproj/Main.storyboard" "${CMAKE_CURRENT_LIST_DIR}/iOS/Base.lproj/LaunchScreen.storyboard") set(RESOURCE_FILES ${STORYBOARD} ${SCRIPTS}) - set(ADDITIONAL_LIBRARIES PRIVATE z NativeXr) + # NativeXr is consumed transitively through `BabylonNativeIntegrations` + # (the Apple interop static lib); no need to link it directly. + set(ADDITIONAL_LIBRARIES PRIVATE z BabylonNativeIntegrations) set(SOURCES ${SOURCES} "iOS/AppDelegate.swift" "iOS/ViewController.swift" - "iOS/LibNativeBridge.h" - "iOS/LibNativeBridge.mm" + "iOS/PlaygroundBootstrap.h" + "iOS/PlaygroundBootstrap.mm" + "iOS/Playground-Bridging-Header.h" "AppleShared/GestureRecognizer.swift") set_source_files_properties(${SCRIPTS} ${BABYLON_SCRIPTS} ${DEPENDENCIES} PROPERTIES MACOSX_PACKAGE_LOCATION "Scripts") set_source_files_properties(${REFERENCE_IMAGES} PROPERTIES MACOSX_PACKAGE_LOCATION "ReferenceImages") @@ -178,7 +181,7 @@ if(APPLE) XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.BabylonNative.Playground.iOS" XCODE_ATTRIBUTE_SWIFT_VERSION "4.0" - XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_LIST_DIR}/iOS/LibNativeBridge.h" + XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_LIST_DIR}/iOS/Playground-Bridging-Header.h" XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks" XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) $(SDKROOT)$(SYSTEM_LIBRARY_DIR)/Frameworks" XCODE_ATTRIBUTE_ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES YES diff --git a/Apps/Playground/iOS/AppDelegate.swift b/Apps/Playground/iOS/AppDelegate.swift index ea4b901a2..d375df4b0 100644 --- a/Apps/Playground/iOS/AppDelegate.swift +++ b/Apps/Playground/iOS/AppDelegate.swift @@ -4,36 +4,37 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - var _bridge: LibNativeBridge? + + /// Owned by the app: created in `application(_:didFinishLaunchingWithOptions:)`, + /// torn down in `applicationWillTerminate`. The `ViewController` borrows + /// this handle to construct its `BNView`. + var runtime: BNRuntime? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - _bridge = LibNativeBridge() - return true - } + let runtime = BNRuntime(enableDebugger: true) - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - } + // Queue the Babylon.js bootstrap scripts (shared with the other + // Playground hosts via Apps/Playground/Shared/PlaygroundScripts.cpp), + // then the playground experience script. They will run after the + // first BNView attach completes engine initialization on the JS + // thread, in submission order. + PlaygroundBootstrap.loadScripts(runtime) + runtime.loadScript("app:///Scripts/experience.js") - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + self.runtime = runtime + return true } - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + func applicationWillResignActive(_ application: UIApplication) { + runtime?.suspend() } func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + runtime?.resume() } func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + runtime = nil } - - } diff --git a/Apps/Playground/iOS/LibNativeBridge.h b/Apps/Playground/iOS/LibNativeBridge.h deleted file mode 100644 index b3faf54ff..000000000 --- a/Apps/Playground/iOS/LibNativeBridge.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - - -@interface LibNativeBridge : NSObject - -- (instancetype)init; -- (void)dealloc; - -- (void)init:(MTKView*)inView screenScale:(float)inScreenScale width:(int)inWidth height:(int)inHeight xrView:(void*)xrView; -- (void)resize:(int)inWidth height:(int)inHeight; -- (void)render; -- (void)setTouchDown:(int)pointerId x:(int)inX y:(int)inY; -- (void)setTouchMove:(int)pointerId x:(int)inX y:(int)inY; -- (void)setTouchUp:(int)pointerId x:(int)inX y:(int)inY; -- (bool)isXRActive; - -@end - diff --git a/Apps/Playground/iOS/LibNativeBridge.mm b/Apps/Playground/iOS/LibNativeBridge.mm deleted file mode 100644 index aa1731215..000000000 --- a/Apps/Playground/iOS/LibNativeBridge.mm +++ /dev/null @@ -1,99 +0,0 @@ -#include "LibNativeBridge.h" - -#import -#import -#import - -std::optional appContext{}; -std::optional nativeXr{}; -bool isXrActive{}; -float screenScale{1.0f}; - -@implementation LibNativeBridge - -- (instancetype)init -{ - self = [super init]; - return self; -} - -- (void)dealloc -{ - isXrActive = false; - - nativeXr.reset(); - appContext.reset(); -} - -- (void)init:(MTKView*)view screenScale:(float)inScreenScale width:(int)inWidth height:(int)inHeight xrView:(void*)xrView -{ - screenScale = inScreenScale; - - appContext.emplace( - (__bridge CA::MetalLayer*)view.layer, - static_cast(inWidth), - static_cast(inHeight), - [](const char* message) { - NSLog(@"%s", message); - }, - [xrView](Napi::Env env) { - nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(env)); - nativeXr->UpdateWindow(xrView); - nativeXr->SetSessionStateChangedCallback([](bool isXrActive){ ::isXrActive = isXrActive; }); - }); - - appContext->ScriptLoader().LoadScript("app:///Scripts/experience.js"); -} - -- (void)resize:(int)inWidth height:(int)inHeight -{ - if (appContext) - { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - - appContext->Device().UpdateSize(static_cast(inWidth), static_cast(inHeight)); - - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); - } -} - -- (void)render -{ - if (appContext) - { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); - } -} - -- (void)setTouchDown:(int)pointerId x:(int)inX y:(int)inY -{ - if (appContext && appContext->Input()) { - appContext->Input()->TouchDown(pointerId, inX * screenScale, inY * screenScale); - } -} - -- (void)setTouchMove:(int)pointerId x:(int)inX y:(int)inY -{ - if (appContext && appContext->Input()) { - appContext->Input()->TouchMove(pointerId, inX * screenScale, inY * screenScale); - } -} - -- (void)setTouchUp:(int)pointerId x:(int)inX y:(int)inY -{ - if (appContext && appContext->Input()) { - appContext->Input()->TouchUp(pointerId, inX * screenScale, inY * screenScale); - } -} - -- (bool)isXRActive -{ - return ::isXrActive; -} - -@end diff --git a/Apps/Playground/iOS/Playground-Bridging-Header.h b/Apps/Playground/iOS/Playground-Bridging-Header.h new file mode 100644 index 000000000..c0f219e69 --- /dev/null +++ b/Apps/Playground/iOS/Playground-Bridging-Header.h @@ -0,0 +1,11 @@ +// Swift-Obj-C bridging header for the iOS Playground app. +// +// Exposes both the Babylon::Integrations Apple interop layer +// (`BNRuntime`, `BNView`) and the Playground-specific helper +// (`PlaygroundBootstrap`) to the Swift sources. + +#pragma once + +#import + +#import "PlaygroundBootstrap.h" diff --git a/Apps/Playground/iOS/PlaygroundBootstrap.h b/Apps/Playground/iOS/PlaygroundBootstrap.h new file mode 100644 index 000000000..627c7129e --- /dev/null +++ b/Apps/Playground/iOS/PlaygroundBootstrap.h @@ -0,0 +1,24 @@ +// PlaygroundBootstrap.h — Obj-C helper exposed to Swift via the +// bridging header. Single class method that hands a freshly-created +// `BNRuntime` to `Apps/Playground/Shared/PlaygroundScripts.cpp`, +// which loads the Babylon.js bootstrap script list shared with the +// other Playground hosts (Win32, Android, …). + +#pragma once + +#import + +@class BNRuntime; + +NS_ASSUME_NONNULL_BEGIN + +@interface PlaygroundBootstrap : NSObject + +/// Performs process-wide Playground setup (PerfTrace level, …) and +/// queues the Babylon.js bootstrap scripts onto `runtime`. Idempotent; +/// safe to call multiple times. ++ (void)loadScripts:(BNRuntime*)runtime; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apps/Playground/iOS/PlaygroundBootstrap.mm b/Apps/Playground/iOS/PlaygroundBootstrap.mm new file mode 100644 index 000000000..ea891c9f0 --- /dev/null +++ b/Apps/Playground/iOS/PlaygroundBootstrap.mm @@ -0,0 +1,33 @@ +// PlaygroundBootstrap.mm — implementation. Calls into the shared C++ +// `Apps/Playground/Shared/PlaygroundScripts.cpp` so the bootstrap +// script list lives in one place. + +#import "PlaygroundBootstrap.h" + +#import + +#include +#include + +// Re-declare the internal class extension that exposes the C++ +// `Runtime*` from `BNRuntime`. The actual implementation lives in +// `Integrations/Apple/Source/BNRuntime.mm`; declaring the same class +// extension here just makes the selector visible to the Obj-C++ +// compiler in this translation unit. +@interface BNRuntime () +- (Babylon::Integrations::Runtime*)nativeRuntime; +@end + +@implementation PlaygroundBootstrap + ++ (void)loadScripts:(BNRuntime*)runtime +{ + if (runtime == nil) + { + return; + } + Playground::Initialize(); + Playground::LoadBootstrapScripts(*[runtime nativeRuntime]); +} + +@end diff --git a/Apps/Playground/iOS/ViewController.swift b/Apps/Playground/iOS/ViewController.swift index 2d44facdc..63ab178e5 100644 --- a/Apps/Playground/iOS/ViewController.swift +++ b/Apps/Playground/iOS/ViewController.swift @@ -5,6 +5,7 @@ class ViewController: UIViewController { var mtkView: MTKView! var xrView: MTKView! + var bnView: BNView? override func viewDidLoad() { super.viewDidLoad() @@ -14,39 +15,39 @@ class ViewController: UIViewController { super.viewDidAppear(animated) guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, - let bridge = appDelegate._bridge + let runtime = appDelegate.runtime else { return } - + setupViews() - + let device = MTLCreateSystemDefaultDevice() mtkView.device = device - + mtkView.colorPixelFormat = .bgra8Unorm_srgb mtkView.depthStencilPixelFormat = .depth32Float - - // Simple gesture recognizer, just provides platform to handle input events - let gesture = UIBabylonGestureRecognizer( + + // Hand the runtime a reference to the XR overlay so NativeXr can + // render its content into a separate transparent layer when an + // XR session is active. + runtime.setXrView(xrView) + + // Construct the BNView against the main MetalLayer. First attach + // on this runtime triggers GPU device construction + plugin + // initialization on the JS thread + queued-script flush. + if let layer = mtkView.layer as? CAMetalLayer { + bnView = BNView(runtime: runtime, layer: layer) + } + + // Simple gesture recognizer: forwards touches to BNView. + let recognizer = UIBabylonGestureRecognizer( target: self, - onTouchDown: bridge.setTouchDown, - onTouchMove: bridge.setTouchMove, - onTouchUp: bridge.setTouchUp - ) - mtkView.addGestureRecognizer(gesture) - - let scale = view.contentScaleFactor - let width = view.bounds.size.width - let height = view.bounds.size.height - - bridge.init( - mtkView, - screenScale:Float(UIScreen.main.scale), - width:Int32(width * scale), - height:Int32(height * scale), - xrView:Unmanaged.passUnretained(xrView).toOpaque() + onTouchDown: { [weak self] (id, x, y) in self?.bnView?.pointerDown(Int(id), atX: CGFloat(x), y: CGFloat(y)) }, + onTouchMove: { [weak self] (id, x, y) in self?.bnView?.pointerMove(Int(id), atX: CGFloat(x), y: CGFloat(y)) }, + onTouchUp: { [weak self] (id, x, y) in self?.bnView?.pointerUp(Int(id), atX: CGFloat(x), y: CGFloat(y)) } ) + mtkView.addGestureRecognizer(recognizer) } - + func setupViews() { mtkView = MTKView() mtkView.delegate = self @@ -54,7 +55,7 @@ class ViewController: UIViewController { view.addSubview(mtkView) view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|[mtkView]|", options: [], metrics: nil, views: ["mtkView" : mtkView])) view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[mtkView]|", options: [], metrics: nil, views: ["mtkView" : mtkView])) - + xrView = MTKView() xrView.translatesAutoresizingMaskIntoConstraints = false xrView.isUserInteractionEnabled = false @@ -69,13 +70,12 @@ class ViewController: UIViewController { extension ViewController: MTKViewDelegate { func draw(in view: MTKView) { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } - xrView.isHidden = !(appDelegate._bridge?.isXRActive() ?? false) - appDelegate._bridge?.render() + xrView.isHidden = !(appDelegate.runtime?.isXRActive ?? false) + bnView?.renderFrame() } - + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } - appDelegate._bridge?.resize(Int32(size.width), height: Int32(size.height)) + bnView?.resize(withWidth: UInt(size.width), height: UInt(size.height)) } } diff --git a/CMakeLists.txt b/CMakeLists.txt index d194ca4f7..4830f49eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,7 +146,10 @@ option(BABYLON_NATIVE_POLYFILL_CANVAS "Include Babylon Native Polyfill Canvas." # Integrations option(BABYLON_NATIVE_INTEGRATIONS "Build the cross-platform Babylon::Integrations facade (Runtime + View)." ON) option(BABYLON_NATIVE_INTEGRATIONS_ANDROID "Build the Android JNI interop layer for Babylon::Integrations." OFF) -option(BABYLON_NATIVE_INTEGRATIONS_APPLE "Build the Apple (iOS / macOS / visionOS) Obj-C++ interop layer for Babylon::Integrations." OFF) +# Default ON for Apple platforms — the iOS Playground (and future +# macOS / visionOS Playground migrations) consume it directly. Apple +# hosts that don't want the interop layer can explicitly disable. +option(BABYLON_NATIVE_INTEGRATIONS_APPLE "Build the Apple (iOS / macOS / visionOS) Obj-C++ interop layer for Babylon::Integrations." ${APPLE}) # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) diff --git a/Integrations/Apple/CMakeLists.txt b/Integrations/Apple/CMakeLists.txt index 90c4cab80..badd63dc5 100644 --- a/Integrations/Apple/CMakeLists.txt +++ b/Integrations/Apple/CMakeLists.txt @@ -32,7 +32,8 @@ target_include_directories(BabylonNativeIntegrations target_link_libraries(BabylonNativeIntegrations PRIVATE Integrations PRIVATE "-framework Foundation" - PRIVATE "-framework QuartzCore") + PRIVATE "-framework QuartzCore" + PRIVATE "-framework MetalKit") # Enable ARC for the Obj-C++ files. set_source_files_properties( diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index 9a4a68e14..30087deaa 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -3,6 +3,9 @@ #import "BNRuntimeInternal.h" +#import +#import + #include @implementation BNRuntime @@ -11,10 +14,22 @@ @implementation BNRuntime } - (instancetype)init +{ + return [self initWithEnableDebugger:NO]; +} + +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger { if ((self = [super init])) { - _runtime = Babylon::Integrations::Runtime::Create(); + Babylon::Integrations::RuntimeOptions options{}; + options.enableDebugger = enableDebugger ? true : false; + // Default log sink: route every level to NSLog. Hosts that need + // their own routing should drop down to the C++ API. + options.log = [](Babylon::Integrations::LogLevel /*level*/, std::string_view message) { + NSLog(@"%.*s", static_cast(message.size()), message.data()); + }; + _runtime = Babylon::Integrations::Runtime::Create(std::move(options)); } return self; } @@ -54,9 +69,28 @@ - (BOOL)isSuspended return _runtime->IsSuspended() ? YES : NO; } +- (void)setXrView:(MTKView*)xrView +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + _runtime->SetXrWindow((__bridge void*)xrView); +#else + (void)xrView; +#endif +} + +- (BOOL)isXRActive +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + return _runtime->IsXrActive() ? YES : NO; +#else + return NO; +#endif +} + - (Babylon::Integrations::Runtime*)nativeRuntime { return _runtime.get(); } @end + diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index 31e40f280..b6f40ee2c 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -47,14 +47,14 @@ - (void)renderFrame } } -- (void)resizeForLayer:(CAMetalLayer*)layer +- (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height { - if (!_view || layer == nil) + if (!_view) { return; } - _view->Resize(static_cast(layer.drawableSize.width), - static_cast(layer.drawableSize.height)); + _view->Resize(static_cast(width), + static_cast(height)); } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h index a5976eb33..ef900d22b 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h @@ -10,6 +10,8 @@ #import +@class MTKView; + NS_ASSUME_NONNULL_BEGIN @interface BNRuntime : NSObject @@ -17,8 +19,12 @@ NS_ASSUME_NONNULL_BEGIN /// Constructs the runtime: starts the JS engine + thread, sets up /// non-GPU polyfills and plugins. Cheap and synchronous; no GPU /// device is created yet (that happens on the first `BNView` attach). +/// Default options: JS debugger off, log routes to `NSLog`. - (instancetype)init; +/// Same as `init` but lets the host opt into the JS debugger. +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger NS_DESIGNATED_INITIALIZER; + /// Load a script from a URL onto the JS thread. Calls made before /// the first `BNView` is created are queued internally and dispatched /// after engine initialization completes during that first attach. @@ -42,6 +48,22 @@ NS_ASSUME_NONNULL_BEGIN /// Whether the runtime is currently suspended. @property (nonatomic, readonly, getter=isSuspended) BOOL suspended; +/// Set the platform view that XR will render into (typically a +/// separate transparent `MTKView` overlay, distinct from the main +/// view's Metal layer). Pass `nil` to clear the XR surface. Safe to +/// call before the first `BNView` attach; the value is applied when +/// NativeXr finishes initializing during that first attach. +/// +/// Compiled-in only when `BABYLON_NATIVE_PLUGIN_NATIVEXR` is enabled +/// at native build time; otherwise this is a no-op. +- (void)setXrView:(nullable MTKView*)xrView; + +/// `YES` while an XR session is active. Updated from the JS thread +/// by NativeXr's internal session-state callback; safe to poll from +/// any thread. +@property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; + @end NS_ASSUME_NONNULL_END + diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h index 3ae012da7..9be98ea04 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h @@ -7,11 +7,6 @@ // and queued-script flushing. Subsequent views attached to the same // runtime are cheap surface rebinds. // -// Width/height handling: this interop layer reads the layer's -// `drawableSize` (physical pixels) and `contentsScale` (DPR) directly, -// converting to the C++ logical-pixel convention internally. The -// Swift host does no unit math. -// // See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. #pragma once @@ -37,15 +32,18 @@ NS_ASSUME_NONNULL_BEGIN /// suspended. - (void)renderFrame; -/// Re-read drawableSize / contentsScale from the given layer and -/// apply as a resize. Call from -/// `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` or any other -/// resize hook. -- (void)resizeForLayer:(CAMetalLayer*)layer; - -/// Forward a pointer-down event. `x`, `y` are in logical pixels. -/// Only present when `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` is enabled -/// at native build time. +/// Inform the view that the underlying surface's pixel-buffer size +/// has changed. Call from +/// `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` with the +/// `size.width` / `size.height` it provides. Sizes are in physical +/// pixels — the same convention as `CAMetalLayer.drawableSize`. +- (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height; + +/// Forward a pointer-down event. `x`, `y` are in logical (CSS) pixels +/// — pass `UITouch.location(in:)` (UIKit) or `NSEvent.locationInWindow` +/// (AppKit) coordinates through unchanged. The view internally +/// normalizes whatever you pass to the unit Babylon.js's pointer +/// pipeline expects. - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; - (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; @@ -55,3 +53,4 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END + From 5ce6913a8541b5d27efbdd9104b4fc7e0b9fb4cf Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 5 May 2026 13:09:39 -0700 Subject: [PATCH 19/71] Fix QuerySize for iOS --- Integrations/Source/ViewImpl_iOS.mm | 56 +++++++++++++++++++++--- Integrations/Source/ViewImpl_macOS.mm | 41 +++++++++++++++-- Integrations/Source/ViewImpl_visionOS.mm | 41 +++++++++++++++-- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm index 94de9a663..71584fa93 100644 --- a/Integrations/Source/ViewImpl_iOS.mm +++ b/Integrations/Source/ViewImpl_iOS.mm @@ -1,6 +1,9 @@ #include "RuntimeImpl.h" #import +#import + +#include namespace Babylon::Integrations { @@ -10,18 +13,59 @@ { return {0, 0}; } + // metal-cpp's CA::MetalLayer* can be bridge-cast to the Obj-C - // CAMetalLayer*; drawableSize is in physical pixels. + // CAMetalLayer*. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - const CGSize size = layer.drawableSize; - return {static_cast(size.width), - static_cast(size.height)}; + + // CAMetalLayer's hosting UIView (e.g. MTKView) is registered as + // the layer's delegate. If the host added it with Auto Layout + // and called us before the layout pass ran, force layout now so + // the layer has valid bounds. This keeps host code free of + // "remember to call layoutIfNeeded before constructing BNView" + // boilerplate. + UIView* hostView = nil; + if ([layer.delegate isKindOfClass:[UIView class]]) + { + hostView = (UIView*)layer.delegate; + if (hostView.bounds.size.width <= 0 || hostView.bounds.size.height <= 0) + { + [hostView layoutIfNeeded]; + } + } + + // Prefer the explicit drawableSize when the host has set it + // (e.g. the off-screen prewarm pattern in SimplifiedAPI §5). + CGSize size = layer.drawableSize; + + // MTKView's autoResizeDrawable updates drawableSize lazily — + // typically not until the first display tick — so it can still + // be (0, 0) here even when the layer is fully laid out. Fall + // back to the layer's own backing-pixel size, then seed + // drawableSize so bgfx and CAMetalLayer agree from now on. + if (size.width <= 0 || size.height <= 0) + { + size.width = layer.bounds.size.width * layer.contentsScale; + size.height = layer.bounds.size.height * layer.contentsScale; + if (size.width > 0 && size.height > 0) + { + layer.drawableSize = size; + } + } + + // Last-resort clamp: never hand bgfx (0, 0). If the host gave + // us a layer with no geometry yet, render at 1x1 until the + // host's resize callback (e.g. `MTKViewDelegate + // mtkView:drawableSizeWillChange:`) provides a real size. + const auto width = static_cast(std::max(size.width, 1)); + const auto height = static_cast(std::max(size.height, 1)); + return {width, height}; } std::pair ViewImpl::ToLogicalCoords(float x, float y) const { - // UIKit `UITouch.locationInView` and AppKit `NSEvent.locationInWindow` - // are already in logical points — passthrough. + // UIKit `UITouch.locationInView` is already in logical points — + // passthrough. return {x, y}; } } diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm index 6d8c49609..93fdeb5a9 100644 --- a/Integrations/Source/ViewImpl_macOS.mm +++ b/Integrations/Source/ViewImpl_macOS.mm @@ -1,6 +1,9 @@ #include "RuntimeImpl.h" #import +#import + +#include namespace Babylon::Integrations { @@ -10,10 +13,42 @@ { return {0, 0}; } + CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - const CGSize size = layer.drawableSize; - return {static_cast(size.width), - static_cast(size.height)}; + + // CAMetalLayer's hosting NSView (e.g. MTKView) is the layer's + // delegate. If the host added it with Auto Layout and called + // us before the layout pass ran, force layout now so the + // layer has valid bounds. + NSView* hostView = nil; + if ([layer.delegate isKindOfClass:[NSView class]]) + { + hostView = (NSView*)layer.delegate; + if (hostView.bounds.size.width <= 0 || hostView.bounds.size.height <= 0) + { + [hostView layoutSubtreeIfNeeded]; + } + } + + CGSize size = layer.drawableSize; + + // MTKView's autoResizeDrawable updates drawableSize lazily — + // typically not until the first display tick. Fall back to + // the layer's own backing-pixel size, then seed drawableSize + // so bgfx and CAMetalLayer agree from now on. + if (size.width <= 0 || size.height <= 0) + { + size.width = layer.bounds.size.width * layer.contentsScale; + size.height = layer.bounds.size.height * layer.contentsScale; + if (size.width > 0 && size.height > 0) + { + layer.drawableSize = size; + } + } + + const auto width = static_cast(std::max(size.width, 1)); + const auto height = static_cast(std::max(size.height, 1)); + return {width, height}; } std::pair ViewImpl::ToLogicalCoords(float x, float y) const diff --git a/Integrations/Source/ViewImpl_visionOS.mm b/Integrations/Source/ViewImpl_visionOS.mm index 649ac7a74..f8d34916f 100644 --- a/Integrations/Source/ViewImpl_visionOS.mm +++ b/Integrations/Source/ViewImpl_visionOS.mm @@ -1,6 +1,9 @@ #include "RuntimeImpl.h" #import +#import + +#include namespace Babylon::Integrations { @@ -10,10 +13,42 @@ { return {0, 0}; } + CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - const CGSize size = layer.drawableSize; - return {static_cast(size.width), - static_cast(size.height)}; + + // CAMetalLayer's hosting UIView is the layer's delegate. If + // the host added it with Auto Layout and called us before the + // layout pass ran, force layout now so the layer has valid + // bounds. + UIView* hostView = nil; + if ([layer.delegate isKindOfClass:[UIView class]]) + { + hostView = (UIView*)layer.delegate; + if (hostView.bounds.size.width <= 0 || hostView.bounds.size.height <= 0) + { + [hostView layoutIfNeeded]; + } + } + + CGSize size = layer.drawableSize; + + // MTKView's autoResizeDrawable updates drawableSize lazily — + // typically not until the first display tick. Fall back to + // the layer's own backing-pixel size, then seed drawableSize + // so bgfx and CAMetalLayer agree from now on. + if (size.width <= 0 || size.height <= 0) + { + size.width = layer.bounds.size.width * layer.contentsScale; + size.height = layer.bounds.size.height * layer.contentsScale; + if (size.width > 0 && size.height > 0) + { + layer.drawableSize = size; + } + } + + const auto width = static_cast(std::max(size.width, 1)); + const auto height = static_cast(std::max(size.height, 1)); + return {width, height}; } std::pair ViewImpl::ToLogicalCoords(float x, float y) const From 8696c0366564ca06683e5f950e9fce6ff82d8035 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 5 May 2026 13:50:57 -0700 Subject: [PATCH 20/71] Auto delegate on iOS --- Apps/Playground/iOS/ViewController.swift | 50 ++--- Integrations/Apple/Source/BNRuntime.mm | 16 ++ Integrations/Apple/Source/BNRuntimeInternal.h | 5 + Integrations/Apple/Source/BNView.mm | 88 ++++++++- .../BabylonNativeIntegrations/BNView.h | 79 +++++--- Integrations/Source/ViewImpl_iOS.mm | 52 +----- Integrations/Source/ViewImpl_macOS.mm | 41 +---- Integrations/Source/ViewImpl_visionOS.mm | 41 +---- SimplifiedAPI.md | 174 +++++++++++++----- 9 files changed, 307 insertions(+), 239 deletions(-) diff --git a/Apps/Playground/iOS/ViewController.swift b/Apps/Playground/iOS/ViewController.swift index 63ab178e5..08b791061 100644 --- a/Apps/Playground/iOS/ViewController.swift +++ b/Apps/Playground/iOS/ViewController.swift @@ -7,10 +7,6 @@ class ViewController: UIViewController { var xrView: MTKView! var bnView: BNView? - override func viewDidLoad() { - super.viewDidLoad() - } - override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard @@ -20,37 +16,32 @@ class ViewController: UIViewController { setupViews() - let device = MTLCreateSystemDefaultDevice() - mtkView.device = device - - mtkView.colorPixelFormat = .bgra8Unorm_srgb - mtkView.depthStencilPixelFormat = .depth32Float - - // Hand the runtime a reference to the XR overlay so NativeXr can - // render its content into a separate transparent layer when an - // XR session is active. + // Hand the runtime a reference to the XR overlay so NativeXr + // can render its content into a separate transparent layer + // when an XR session is active. The runtime keeps the + // overlay's visibility in sync with the XR session state on + // its own — the host doesn't need to toggle anything. runtime.setXrView(xrView) - // Construct the BNView against the main MetalLayer. First attach - // on this runtime triggers GPU device construction + plugin - // initialization on the JS thread + queued-script flush. - if let layer = mtkView.layer as? CAMetalLayer { - bnView = BNView(runtime: runtime, layer: layer) - } + // Attach BNView to the main MTKView. BNView installs itself as + // the view's MTKViewDelegate and drives the per-frame render + // and resize callbacks internally. First attach on this runtime + // triggers GPU device construction + plugin initialization on + // the JS thread + queued-script flush. + bnView = BNView(runtime: runtime, view: mtkView) // Simple gesture recognizer: forwards touches to BNView. let recognizer = UIBabylonGestureRecognizer( target: self, - onTouchDown: { [weak self] (id, x, y) in self?.bnView?.pointerDown(Int(id), atX: CGFloat(x), y: CGFloat(y)) }, - onTouchMove: { [weak self] (id, x, y) in self?.bnView?.pointerMove(Int(id), atX: CGFloat(x), y: CGFloat(y)) }, - onTouchUp: { [weak self] (id, x, y) in self?.bnView?.pointerUp(Int(id), atX: CGFloat(x), y: CGFloat(y)) } + onTouchDown: { [weak self] (id, x, y) in self?.bnView?.pointerDown(id: Int(id), x: CGFloat(x), y: CGFloat(y)) }, + onTouchMove: { [weak self] (id, x, y) in self?.bnView?.pointerMove(id: Int(id), x: CGFloat(x), y: CGFloat(y)) }, + onTouchUp: { [weak self] (id, x, y) in self?.bnView?.pointerUp (id: Int(id), x: CGFloat(x), y: CGFloat(y)) } ) mtkView.addGestureRecognizer(recognizer) } func setupViews() { mtkView = MTKView() - mtkView.delegate = self mtkView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(mtkView) view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|[mtkView]|", options: [], metrics: nil, views: ["mtkView" : mtkView])) @@ -66,16 +57,3 @@ class ViewController: UIViewController { } } -// MARK: MTKViewDelegate -extension ViewController: MTKViewDelegate { - func draw(in view: MTKView) { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } - xrView.isHidden = !(appDelegate.runtime?.isXRActive ?? false) - bnView?.renderFrame() - } - - func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { - bnView?.resize(withWidth: UInt(size.width), height: UInt(size.height)) - } -} - diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index 30087deaa..7f3be9719 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -11,6 +11,7 @@ @implementation BNRuntime { std::unique_ptr _runtime; + MTKView* _xrView; } - (instancetype)init @@ -72,6 +73,7 @@ - (BOOL)isSuspended - (void)setXrView:(MTKView*)xrView { #if BABYLON_NATIVE_PLUGIN_NATIVEXR + _xrView = xrView; _runtime->SetXrWindow((__bridge void*)xrView); #else (void)xrView; @@ -92,5 +94,19 @@ - (BOOL)isXRActive return _runtime.get(); } +- (void)updateXrViewIfNeeded +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + if (_xrView != nil) + { + const BOOL shouldBeHidden = !_runtime->IsXrActive(); + if (_xrView.hidden != shouldBeHidden) + { + _xrView.hidden = shouldBeHidden; + } + } +#endif +} + @end diff --git a/Integrations/Apple/Source/BNRuntimeInternal.h b/Integrations/Apple/Source/BNRuntimeInternal.h index 576d8278f..c1a502cca 100644 --- a/Integrations/Apple/Source/BNRuntimeInternal.h +++ b/Integrations/Apple/Source/BNRuntimeInternal.h @@ -12,6 +12,11 @@ NS_ASSUME_NONNULL_BEGIN @interface BNRuntime () - (Babylon::Integrations::Runtime*)nativeRuntime; + +/// Toggle the XR overlay view's visibility based on the runtime's +/// current XR-active state. Called by BNView once per frame so the +/// host doesn't have to manage XR overlay visibility itself. +- (void)updateXrViewIfNeeded; @end NS_ASSUME_NONNULL_END diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index b6f40ee2c..f16088828 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -4,6 +4,7 @@ #import "BNRuntimeInternal.h" #import +#import #import #include @@ -11,19 +12,57 @@ #include #include +@interface BNView () +@end + @implementation BNView { std::unique_ptr _view; + BNRuntime* _runtime; + MTKView* _mtkView; + BOOL _autoDelegate; } -- (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer +- (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view { - if (runtime == nil || layer == nil) + return [self initWithRuntime:runtime view:view autoDelegate:YES]; +} + +- (instancetype)initWithRuntime:(BNRuntime*)runtime + view:(MTKView*)view + autoDelegate:(BOOL)autoDelegate +{ + if (runtime == nil || view == nil) { return nil; } if ((self = [super init])) { + _runtime = runtime; + _mtkView = view; + _autoDelegate = autoDelegate; + + // MTKView's underlying layer is always a CAMetalLayer (its + // +layerClass override). + CAMetalLayer* layer = (CAMetalLayer*)view.layer; + + // Force a layout pass so the view's bounds are valid before we + // read them, and seed the layer's drawableSize from the view's + // logical bounds × backing scale. MTKView's autoResizeDrawable + // will keep drawableSize in sync from this point on, but at + // attach time it can still be (0, 0) — so we set it explicitly + // to avoid handing bgfx a zero-sized swap chain. + [view layoutIfNeeded]; +#if TARGET_OS_OSX + const CGFloat scale = view.window.backingScaleFactor > 0 + ? view.window.backingScaleFactor + : 1.0; +#else + const CGFloat scale = view.contentScaleFactor; +#endif + layer.drawableSize = CGSizeMake(view.bounds.size.width * scale, + view.bounds.size.height * scale); + // First attach on this runtime triggers GPU device construction // + plugin initialization + queued-script flush. The View // queries the layer's drawableSize itself; the host doesn't @@ -33,14 +72,37 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer (__bridge CA::MetalLayer*)layer); if (!_view) { + _mtkView = nil; return nil; } + + // Install ourselves as the delegate AFTER Attach so that any + // drawableSizeWillChange: dispatched as a side-effect of + // assignment doesn't reach us before _view is constructed. + if (_autoDelegate) + { + view.delegate = self; + } } return self; } +- (void)dealloc +{ + if (_autoDelegate && _mtkView.delegate == self) + { + _mtkView.delegate = nil; + } +} + - (void)renderFrame { + // The runtime owns the XR overlay view (handed to it via + // -setXrView:), so it's the natural place to keep its visibility + // in sync with the XR session state. Doing this here means hosts + // never have to manage the XR overlay themselves. + [_runtime updateXrViewIfNeeded]; + if (_view) { _view->RenderFrame(); @@ -49,14 +111,28 @@ - (void)renderFrame - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height { - if (!_view) + if (_view) { - return; + _view->Resize(static_cast(width), + static_cast(height)); } - _view->Resize(static_cast(width), - static_cast(height)); } +#pragma mark - MTKViewDelegate (auto-delegate mode only) + +- (void)mtkView:(MTKView* __unused)view drawableSizeWillChange:(CGSize)size +{ + [self resizeWithWidth:static_cast(size.width) + height:static_cast(size.height)]; +} + +- (void)drawInMTKView:(MTKView* __unused)view +{ + [self renderFrame]; +} + +#pragma mark - Pointer forwarding + #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h index 9be98ea04..1f625b9e6 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h @@ -1,11 +1,16 @@ // BNView.h — public Obj-C interface for the Babylon::Integrations // view on Apple platforms. // -// Construct against a host-provided `CAMetalLayer` (typically -// `MTKView.layer`). The first `BNView` constructed against a given -// `BNRuntime` triggers GPU device construction, plugin initialization, -// and queued-script flushing. Subsequent views attached to the same -// runtime are cheap surface rebinds. +// Construct against a host-provided `MTKView`. The first `BNView` +// constructed against a given `BNRuntime` triggers GPU device +// construction, plugin initialization, and queued-script flushing. +// Subsequent views attached to the same runtime are cheap surface +// rebinds. +// +// BNView installs itself as the MTKView's `delegate` and drives the +// per-frame render and resize callbacks internally — the host does +// not need to forward `MTKViewDelegate.draw(in:)` or +// `mtkView:drawableSizeWillChange:`. // // See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. @@ -14,41 +19,65 @@ #import #import -@class CAMetalLayer; +@class MTKView; @class BNRuntime; NS_ASSUME_NONNULL_BEGIN @interface BNView : NSObject -/// Attach to `runtime` rendering against `layer` (the host's -/// user-visible Metal layer). On the first attach for a given -/// runtime, this triggers GPU device construction and engine -/// initialization. Subsequent attaches just rebind the surface. -- (instancetype)initWithRuntime:(BNRuntime*)runtime layer:(CAMetalLayer*)layer; +/// Attach `runtime` to render into `view` ("super simple" mode). BNView +/// installs itself as the MTKView's `delegate` and drives the per-frame +/// render and resize callbacks internally — the host does not need to +/// forward anything. +/// +/// Equivalent to `-initWithRuntime:view:autoDelegate:` with +/// `autoDelegate = YES`. +- (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view; + +/// Attach `runtime` to render into `view` with explicit control over +/// the MTKView delegate. +/// +/// - When `autoDelegate` is `YES`, BNView installs itself as the +/// MTKView's delegate and drives `drawInMTKView:` and +/// `mtkView:drawableSizeWillChange:` internally. The host should +/// not assign its own delegate to the same MTKView. +/// - When `autoDelegate` is `NO`, the host keeps ownership of the +/// MTKView's delegate and is responsible for calling `-renderFrame` +/// and `-resizeWithWidth:height:` from its own MTKViewDelegate +/// methods. Use this mode when the host needs to interleave its +/// own per-frame work with the runtime's render. +- (instancetype)initWithRuntime:(BNRuntime*)runtime + view:(MTKView*)view + autoDelegate:(BOOL)autoDelegate NS_DESIGNATED_INITIALIZER; -/// Render exactly one frame. Call from the host's draw callback -/// (e.g. `MTKViewDelegate.draw(in:)`). No-op if the runtime is -/// suspended. +/// Render exactly one frame. Only used in manual-delegate mode +/// (`autoDelegate = NO`) — call from your own `MTKViewDelegate +/// drawInMTKView:` method. - (void)renderFrame; -/// Inform the view that the underlying surface's pixel-buffer size -/// has changed. Call from -/// `MTKViewDelegate.mtkView(_:drawableSizeWillChange:)` with the -/// `size.width` / `size.height` it provides. Sizes are in physical +/// Inform the runtime that the underlying surface's pixel-buffer size +/// has changed. Only used in manual-delegate mode — call from your +/// own `MTKViewDelegate mtkView:drawableSizeWillChange:` method with +/// the `size.width` / `size.height` it provides. Sizes are in physical /// pixels — the same convention as `CAMetalLayer.drawableSize`. -- (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height; +- (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height + NS_SWIFT_NAME(resize(width:height:)); /// Forward a pointer-down event. `x`, `y` are in logical (CSS) pixels /// — pass `UITouch.location(in:)` (UIKit) or `NSEvent.locationInWindow` -/// (AppKit) coordinates through unchanged. The view internally -/// normalizes whatever you pass to the unit Babylon.js's pointer -/// pipeline expects. -- (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; +/// (AppKit) coordinates through unchanged. +- (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y + NS_SWIFT_NAME(pointerDown(id:x:y:)); + +- (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y + NS_SWIFT_NAME(pointerMove(id:x:y:)); -- (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; +- (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y + NS_SWIFT_NAME(pointerUp(id:x:y:)); -- (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y; +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; @end diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm index 71584fa93..d028eebdd 100644 --- a/Integrations/Source/ViewImpl_iOS.mm +++ b/Integrations/Source/ViewImpl_iOS.mm @@ -1,9 +1,6 @@ #include "RuntimeImpl.h" #import -#import - -#include namespace Babylon::Integrations { @@ -13,53 +10,12 @@ { return {0, 0}; } - // metal-cpp's CA::MetalLayer* can be bridge-cast to the Obj-C - // CAMetalLayer*. + // CAMetalLayer*; drawableSize is in physical pixels. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - - // CAMetalLayer's hosting UIView (e.g. MTKView) is registered as - // the layer's delegate. If the host added it with Auto Layout - // and called us before the layout pass ran, force layout now so - // the layer has valid bounds. This keeps host code free of - // "remember to call layoutIfNeeded before constructing BNView" - // boilerplate. - UIView* hostView = nil; - if ([layer.delegate isKindOfClass:[UIView class]]) - { - hostView = (UIView*)layer.delegate; - if (hostView.bounds.size.width <= 0 || hostView.bounds.size.height <= 0) - { - [hostView layoutIfNeeded]; - } - } - - // Prefer the explicit drawableSize when the host has set it - // (e.g. the off-screen prewarm pattern in SimplifiedAPI §5). - CGSize size = layer.drawableSize; - - // MTKView's autoResizeDrawable updates drawableSize lazily — - // typically not until the first display tick — so it can still - // be (0, 0) here even when the layer is fully laid out. Fall - // back to the layer's own backing-pixel size, then seed - // drawableSize so bgfx and CAMetalLayer agree from now on. - if (size.width <= 0 || size.height <= 0) - { - size.width = layer.bounds.size.width * layer.contentsScale; - size.height = layer.bounds.size.height * layer.contentsScale; - if (size.width > 0 && size.height > 0) - { - layer.drawableSize = size; - } - } - - // Last-resort clamp: never hand bgfx (0, 0). If the host gave - // us a layer with no geometry yet, render at 1x1 until the - // host's resize callback (e.g. `MTKViewDelegate - // mtkView:drawableSizeWillChange:`) provides a real size. - const auto width = static_cast(std::max(size.width, 1)); - const auto height = static_cast(std::max(size.height, 1)); - return {width, height}; + const CGSize size = layer.drawableSize; + return {static_cast(size.width), + static_cast(size.height)}; } std::pair ViewImpl::ToLogicalCoords(float x, float y) const diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm index 93fdeb5a9..6d8c49609 100644 --- a/Integrations/Source/ViewImpl_macOS.mm +++ b/Integrations/Source/ViewImpl_macOS.mm @@ -1,9 +1,6 @@ #include "RuntimeImpl.h" #import -#import - -#include namespace Babylon::Integrations { @@ -13,42 +10,10 @@ { return {0, 0}; } - CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - - // CAMetalLayer's hosting NSView (e.g. MTKView) is the layer's - // delegate. If the host added it with Auto Layout and called - // us before the layout pass ran, force layout now so the - // layer has valid bounds. - NSView* hostView = nil; - if ([layer.delegate isKindOfClass:[NSView class]]) - { - hostView = (NSView*)layer.delegate; - if (hostView.bounds.size.width <= 0 || hostView.bounds.size.height <= 0) - { - [hostView layoutSubtreeIfNeeded]; - } - } - - CGSize size = layer.drawableSize; - - // MTKView's autoResizeDrawable updates drawableSize lazily — - // typically not until the first display tick. Fall back to - // the layer's own backing-pixel size, then seed drawableSize - // so bgfx and CAMetalLayer agree from now on. - if (size.width <= 0 || size.height <= 0) - { - size.width = layer.bounds.size.width * layer.contentsScale; - size.height = layer.bounds.size.height * layer.contentsScale; - if (size.width > 0 && size.height > 0) - { - layer.drawableSize = size; - } - } - - const auto width = static_cast(std::max(size.width, 1)); - const auto height = static_cast(std::max(size.height, 1)); - return {width, height}; + const CGSize size = layer.drawableSize; + return {static_cast(size.width), + static_cast(size.height)}; } std::pair ViewImpl::ToLogicalCoords(float x, float y) const diff --git a/Integrations/Source/ViewImpl_visionOS.mm b/Integrations/Source/ViewImpl_visionOS.mm index f8d34916f..649ac7a74 100644 --- a/Integrations/Source/ViewImpl_visionOS.mm +++ b/Integrations/Source/ViewImpl_visionOS.mm @@ -1,9 +1,6 @@ #include "RuntimeImpl.h" #import -#import - -#include namespace Babylon::Integrations { @@ -13,42 +10,10 @@ { return {0, 0}; } - CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - - // CAMetalLayer's hosting UIView is the layer's delegate. If - // the host added it with Auto Layout and called us before the - // layout pass ran, force layout now so the layer has valid - // bounds. - UIView* hostView = nil; - if ([layer.delegate isKindOfClass:[UIView class]]) - { - hostView = (UIView*)layer.delegate; - if (hostView.bounds.size.width <= 0 || hostView.bounds.size.height <= 0) - { - [hostView layoutIfNeeded]; - } - } - - CGSize size = layer.drawableSize; - - // MTKView's autoResizeDrawable updates drawableSize lazily — - // typically not until the first display tick. Fall back to - // the layer's own backing-pixel size, then seed drawableSize - // so bgfx and CAMetalLayer agree from now on. - if (size.width <= 0 || size.height <= 0) - { - size.width = layer.bounds.size.width * layer.contentsScale; - size.height = layer.bounds.size.height * layer.contentsScale; - if (size.width > 0 && size.height > 0) - { - layer.drawableSize = size; - } - } - - const auto width = static_cast(std::max(size.width, 1)); - const auto height = static_cast(std::max(size.height, 1)); - return {width, height}; + const CGSize size = layer.drawableSize; + return {static_cast(size.width), + static_cast(size.height)}; } std::pair ViewImpl::ToLogicalCoords(float x, float y) const diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index 662e68bf4..97ee48341 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -1052,10 +1052,10 @@ class MainActivity : AppCompatActivity() { **Library-supplied Obj-C++ interop** (lives in `Integrations/Apple/`): -The interop layer reads each `CAMetalLayer`'s `drawableSize` -(physical pixels) and `contentsScale` (DPR) directly so the Swift -host doesn't have to think about units — it just hands the layer -over (see §4.2). Surface allocation is the host's responsibility. +The interop layer takes a host-supplied `MTKView` directly. It installs +itself as the view's `MTKViewDelegate` and drives the per-frame render +and resize callbacks internally — the Swift host doesn't have to wire +anything up beyond constructing the `BNView`. ```objc // BNRuntime.h — public Obj-C header (Swift sees this via the bridge) @@ -1067,11 +1067,22 @@ over (see §4.2). Surface allocation is the host's responsibility. @end @interface BNView : NSObject -// `layer` is the user-visible CAMetalLayer (typically MTKView.layer). -// The interop layer reads drawableSize and contentsScale from it. -- (instancetype)initWithRuntime:(BNRuntime*)rt layer:(CAMetalLayer*)layer; -- (void)renderFrame; -- (void)resizeForLayer:(CAMetalLayer*)layer; // re-reads size/scale +// "Super simple" mode: BNView installs itself as the MTKView's delegate +// and drives draw/resize internally — no MTKViewDelegate boilerplate +// in host code. +- (instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view; + +// Explicit-mode init. With autoDelegate = NO, the host keeps ownership +// of the MTKView's delegate and calls -renderFrame and +// -resizeWithWidth:height: from its own MTKViewDelegate methods. Use +// this when you need to interleave host work with the runtime's +// per-frame render. +- (instancetype)initWithRuntime:(BNRuntime*)rt + view:(MTKView*)view + autoDelegate:(BOOL)autoDelegate; + +- (void)renderFrame; // manual mode only +- (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h; // manual mode only @end ``` @@ -1092,40 +1103,111 @@ over (see §4.2). Surface allocation is the host's responsibility. - (Babylon::Integrations::Runtime*)native { return _rt.get(); } @end +@interface BNView () +@end + @implementation BNView { std::unique_ptr _v; + MTKView* _mtkView; + BOOL _autoDelegate; } -// Helper: read physical-pixel dims + DPR from the layer, convert to -// the C++ logical-pixel + DPR convention. -static Babylon::Integrations::ViewDescriptor MakeViewDescriptor(CAMetalLayer* layer) { - CGFloat scale = layer.contentsScale; - return { - (__bridge void*)layer, - (uint32_t)(layer.drawableSize.width / scale), // logical - (uint32_t)(layer.drawableSize.height / scale), - (float)scale - }; +- (instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view { + return [self initWithRuntime:rt view:view autoDelegate:YES]; } -- (instancetype)initWithRuntime:(BNRuntime*)rt layer:(CAMetalLayer*)layer { +- (instancetype)initWithRuntime:(BNRuntime*)rt + view:(MTKView*)view + autoDelegate:(BOOL)autoDelegate { if ((self = [super init])) { + _mtkView = view; + _autoDelegate = autoDelegate; + CAMetalLayer* layer = (CAMetalLayer*)view.layer; + + // Force layout so bounds are valid, then seed drawableSize. + // MTKView's autoResizeDrawable keeps it in sync from here on, + // but it can still be (0, 0) at attach time. + [view layoutIfNeeded]; + const CGFloat scale = view.contentScaleFactor; // macOS: window.backingScaleFactor + layer.drawableSize = CGSizeMake(view.bounds.size.width * scale, + view.bounds.size.height * scale); + // First Attach on this runtime triggers Device construction + // GPU plugin init + queued LoadScript flush. - _v = Babylon::Integrations::View::Attach(*[rt native], MakeViewDescriptor(layer)); + _v = Babylon::Integrations::View::Attach(*[rt native], + (__bridge CA::MetalLayer*)layer); + if (autoDelegate) view.delegate = self; // install AFTER Attach } return self; } +- (void)dealloc { + if (_autoDelegate && _mtkView.delegate == self) { _mtkView.delegate = nil; } +} - (void)renderFrame { _v->RenderFrame(); } -- (void)resizeForLayer:(CAMetalLayer*)layer { - auto h = MakeViewDescriptor(layer); - _v->Resize(h.width, h.height, h.devicePixelRatio); +- (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h { + _v->Resize((uint32_t)w, (uint32_t)h); +} +- (void)mtkView:(MTKView*)v drawableSizeWillChange:(CGSize)size { + [self resizeWithWidth:(NSUInteger)size.width height:(NSUInteger)size.height]; } +- (void)drawInMTKView:(MTKView*)v { [self renderFrame]; } @end ``` -**Host code — simple integration** (consumer's app — *not* shipped by the library): +**Host code — "super simple" integration** (consumer's app — *not* shipped by the library): -The simplest host creates the runtime at app start, loads scripts +The minimal host creates the runtime at app start, loads scripts (queued), then constructs a `BNView` against the user-visible MTKView +and is done — BNView drives the per-frame render and resize itself. + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + let runtime: BNRuntime = { + let r = BNRuntime() + r.loadScript(Bundle.main.url(forResource: "experience", + withExtension: "js")!.absoluteString) + return r + }() + func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } + func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } +} + +class BabylonViewController: UIViewController { + var babylonView: BNView? + override func viewDidLoad() { + let mtkView = view as! MTKView + let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime + babylonView = BNView(runtime: runtime, view: mtkView) // that's it + } +} +``` + +**Host code — "simple" integration with manual delegate** (host needs to interleave per-frame work): + +When the host wants to do its own work each frame (e.g. toggling an +overlay's visibility based on runtime state), pass `autoDelegate: false` +and own the `MTKViewDelegate` yourself. Forward `draw(in:)` and +`drawableSizeWillChange:` to `BNView`'s `renderFrame` / `resize`. + +```swift +class BabylonViewController: UIViewController, MTKViewDelegate { + var babylonView: BNView? + var overlay: UIView! + let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime + + override func viewDidLoad() { + let mtkView = view as! MTKView + mtkView.delegate = self + babylonView = BNView(runtime: runtime, view: mtkView, autoDelegate: false) + } + + func draw(in view: MTKView) { + overlay.isHidden = !runtime.isOverlayActive // host's per-frame work + babylonView?.renderFrame() + } + func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { + babylonView?.resize(withWidth: UInt(size.width), height: UInt(size.height)) + } +} +``` layer in `viewDidLoad`. ```swift @@ -1143,21 +1225,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } } -class BabylonViewController: UIViewController, MTKViewDelegate { +class BabylonViewController: UIViewController { var babylonView: BNView? override func viewDidLoad() { let mtkView = view as! MTKView - mtkView.delegate = self let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime // First Attach: triggers Device + plugin init + queued script flush. - babylonView = BNView(runtime: runtime, layer: mtkView.layer as! CAMetalLayer) - } - - // Natural draw callback — no size arithmetic in Swift. - func draw(in view: MTKView) { babylonView?.renderFrame() } - func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { - babylonView?.resizeForLayer(v.layer as! CAMetalLayer) + // BNView installs itself as the MTKView's delegate and drives + // per-frame render/resize internally. + babylonView = BNView(runtime: runtime, view: mtkView) } } ``` @@ -1165,42 +1242,43 @@ class BabylonViewController: UIViewController, MTKViewDelegate { **Host code — pre-loading the engine before the user-visible UI exists:** A host that wants the engine warm at app start creates a `BNView` -against an off-screen `CAMetalLayer` in the `AppDelegate` so the first +against an off-screen `MTKView` in the `AppDelegate` so the first Attach fires immediately and starts initialization + scene construction. Later, when a view controller appears, the host destroys that View and constructs a new `BNView` against the user-visible -layer. +view. ```swift class AppDelegate: NSObject, UIApplicationDelegate { let runtime = BNRuntime() - var prewarmLayer: CAMetalLayer! - var prewarmView: BNView! + var prewarmView: MTKView! + var prewarmBN: BNView! func application(_ app: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey : Any]?) -> Bool { runtime.loadScript(Bundle.main.url(forResource: "experience", withExtension: "js")!.absoluteString) - prewarmLayer = CAMetalLayer() - prewarmLayer.drawableSize = CGSize(width: 16, height: 16) - prewarmLayer.isHidden = true - prewarmView = BNView(runtime: runtime, layer: prewarmLayer) + // Off-screen MTKView large enough to satisfy Metal validation; + // bgfx's first frame renders into this surface while the real + // UI is being assembled. + prewarmView = MTKView(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + prewarmView.isHidden = true + prewarmBN = BNView(runtime: runtime, view: prewarmView) // First Attach fires here; engine is now booting up + scripts running. return true } - func releasePrewarm() { prewarmView = nil } // call before binding real layer + func releasePrewarm() { prewarmBN = nil; prewarmView = nil } // call before binding real view func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } } // ... later, in the view controller: // delegate.releasePrewarm() -// babylonView = BNView(runtime: delegate.runtime, -// layer: mtkView.layer as! CAMetalLayer) -// // Device::UpdateWindow swaps to the user-visible layer; scene state -// // and JS state are preserved. +// babylonView = BNView(runtime: delegate.runtime, view: mtkView) +// // Device::UpdateWindow swaps to the user-visible surface; scene +// // state and JS state are preserved. ``` ### Suspend/Resume is reference-counted From 8d00baad22d49e88a2cf46c7511df52651b68a25 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 5 May 2026 16:22:22 -0700 Subject: [PATCH 21/71] Expose default delegate --- Apps/Playground/iOS/ViewController.swift | 11 +-- Integrations/Apple/CMakeLists.txt | 4 +- Integrations/Apple/Source/BNView.mm | 52 +++++------ Integrations/Apple/Source/BNViewDelegate.mm | 47 ++++++++++ .../BabylonNativeIntegrations/BNView.h | 88 +++++++++++-------- 5 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 Integrations/Apple/Source/BNViewDelegate.mm diff --git a/Apps/Playground/iOS/ViewController.swift b/Apps/Playground/iOS/ViewController.swift index 08b791061..405012440 100644 --- a/Apps/Playground/iOS/ViewController.swift +++ b/Apps/Playground/iOS/ViewController.swift @@ -23,11 +23,12 @@ class ViewController: UIViewController { // its own — the host doesn't need to toggle anything. runtime.setXrView(xrView) - // Attach BNView to the main MTKView. BNView installs itself as - // the view's MTKViewDelegate and drives the per-frame render - // and resize callbacks internally. First attach on this runtime - // triggers GPU device construction + plugin initialization on - // the JS thread + queued-script flush. + // Attach BNView to the main MTKView. Because mtkView's delegate + // is still nil at this point, BNView auto-installs a managed + // `BNViewDelegate` that drives the per-frame render and resize + // callbacks. First attach on this runtime triggers GPU device + // construction + plugin initialization on the JS thread + + // queued-script flush. bnView = BNView(runtime: runtime, view: mtkView) // Simple gesture recognizer: forwards touches to BNView. diff --git a/Integrations/Apple/CMakeLists.txt b/Integrations/Apple/CMakeLists.txt index badd63dc5..158499bcc 100644 --- a/Integrations/Apple/CMakeLists.txt +++ b/Integrations/Apple/CMakeLists.txt @@ -19,7 +19,8 @@ set(SOURCES "include/BabylonNativeIntegrations/BNView.h" "Source/BNRuntime.mm" "Source/BNRuntimeInternal.h" - "Source/BNView.mm") + "Source/BNView.mm" + "Source/BNViewDelegate.mm") add_library(BabylonNativeIntegrations STATIC ${SOURCES}) @@ -39,6 +40,7 @@ target_link_libraries(BabylonNativeIntegrations set_source_files_properties( "Source/BNRuntime.mm" "Source/BNView.mm" + "Source/BNViewDelegate.mm" PROPERTIES COMPILE_FLAGS "-fobjc-arc") set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index f16088828..b6e5f4466 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -12,25 +12,20 @@ #include #include -@interface BNView () -@end - @implementation BNView { std::unique_ptr _view; BNRuntime* _runtime; MTKView* _mtkView; - BOOL _autoDelegate; -} -- (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view -{ - return [self initWithRuntime:runtime view:view autoDelegate:YES]; + // When BNView auto-installs a default delegate (because the host + // didn't set one), it's held here so the strong reference outlives + // the MTKView's `weak` delegate slot. Stays nil if the host + // installed their own delegate before constructing BNView. + BNViewDelegate* _managedDelegate; } -- (instancetype)initWithRuntime:(BNRuntime*)runtime - view:(MTKView*)view - autoDelegate:(BOOL)autoDelegate +- (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view { if (runtime == nil || view == nil) { @@ -40,7 +35,6 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime { _runtime = runtime; _mtkView = view; - _autoDelegate = autoDelegate; // MTKView's underlying layer is always a CAMetalLayer (its // +layerClass override). @@ -76,12 +70,15 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime return nil; } - // Install ourselves as the delegate AFTER Attach so that any - // drawableSizeWillChange: dispatched as a side-effect of - // assignment doesn't reach us before _view is constructed. - if (_autoDelegate) + // If the host hasn't installed their own MTKViewDelegate by + // now, install a default `BNViewDelegate` so frames start + // flowing without any extra host wiring. Done AFTER Attach so + // any drawableSizeWillChange: dispatched as a side-effect of + // the assignment doesn't reach us before _view is constructed. + if (view.delegate == nil) { - view.delegate = self; + _managedDelegate = [[BNViewDelegate alloc] initWithView:self]; + view.delegate = _managedDelegate; } } return self; @@ -89,7 +86,9 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime - (void)dealloc { - if (_autoDelegate && _mtkView.delegate == self) + // Only clear the MTKView's delegate slot if it still points at the + // delegate we installed; never disturb a host-installed delegate. + if (_managedDelegate != nil && _mtkView.delegate == _managedDelegate) { _mtkView.delegate = nil; } @@ -100,7 +99,9 @@ - (void)renderFrame // The runtime owns the XR overlay view (handed to it via // -setXrView:), so it's the natural place to keep its visibility // in sync with the XR session state. Doing this here means hosts - // never have to manage the XR overlay themselves. + // never have to manage the XR overlay themselves, regardless of + // whether the runtime's frame is being driven by the auto-installed + // BNViewDelegate, a host subclass, or a fully-custom delegate. [_runtime updateXrViewIfNeeded]; if (_view) @@ -118,19 +119,6 @@ - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height } } -#pragma mark - MTKViewDelegate (auto-delegate mode only) - -- (void)mtkView:(MTKView* __unused)view drawableSizeWillChange:(CGSize)size -{ - [self resizeWithWidth:static_cast(size.width) - height:static_cast(size.height)]; -} - -- (void)drawInMTKView:(MTKView* __unused)view -{ - [self renderFrame]; -} - #pragma mark - Pointer forwarding #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT diff --git a/Integrations/Apple/Source/BNViewDelegate.mm b/Integrations/Apple/Source/BNViewDelegate.mm new file mode 100644 index 000000000..c4247c7c1 --- /dev/null +++ b/Integrations/Apple/Source/BNViewDelegate.mm @@ -0,0 +1,47 @@ +// BNViewDelegate.mm — default `MTKViewDelegate` that drives a BNView. +// Subclassable by hosts that want to insert per-frame work; just +// override the delegate methods and call `super` to keep the default +// forwarding behavior. + +#import + +@implementation BNViewDelegate +{ + // BNView holds the auto-installed delegate strongly, and + // host-installed subclass delegates are typically held strongly by + // the host's own controller — so a weak back-reference here is + // sufficient and avoids any chance of a retain cycle if a subclass + // captures the BNView in a closure or property. + __weak BNView* _view; +} + +- (instancetype)initWithView:(BNView*)view +{ + if (view == nil) + { + return nil; + } + if ((self = [super init])) + { + _view = view; + } + return self; +} + +#pragma mark - MTKViewDelegate + +- (void)mtkView:(MTKView* __unused)v drawableSizeWillChange:(CGSize)size +{ + [_view resizeWithWidth:static_cast(size.width) + height:static_cast(size.height)]; +} + +- (void)drawInMTKView:(MTKView* __unused)v +{ + // `[_view renderFrame]` already handles the XR overlay visibility + // toggle internally via the runtime, so subclasses that override + // `drawInMTKView:` and call `super` get the behavior for free. + [_view renderFrame]; +} + +@end diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h index 1f625b9e6..339a671c3 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h @@ -7,10 +7,13 @@ // Subsequent views attached to the same runtime are cheap surface // rebinds. // -// BNView installs itself as the MTKView's `delegate` and drives the -// per-frame render and resize callbacks internally — the host does -// not need to forward `MTKViewDelegate.draw(in:)` or -// `mtkView:drawableSizeWillChange:`. +// By default, if the MTKView has no delegate when `BNView` is +// constructed, BNView installs and strong-holds a `BNViewDelegate` +// for you and the host doesn't have to wire anything up. If the host +// wants to interleave per-frame work, they assign their own +// `MTKViewDelegate` (typically a `BNViewDelegate` subclass) to the +// MTKView before constructing the BNView; in that case BNView leaves +// the host's delegate alone. // // See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. @@ -18,49 +21,64 @@ #import #import +#import -@class MTKView; @class BNRuntime; +@class BNView; NS_ASSUME_NONNULL_BEGIN +/// Default `MTKViewDelegate` implementation that drives a `BNView`. +/// Forwards `drawInMTKView:` to `[bnView renderFrame]` and +/// `mtkView:drawableSizeWillChange:` to `[bnView resizeWithWidth:height:]`. +/// +/// `BNView` automatically installs and retains an instance of this +/// class when constructed against an `MTKView` that has no delegate +/// yet, so most hosts never need to construct one explicitly. +/// +/// To insert per-frame work, subclass `BNViewDelegate`, override the +/// delegate methods, and call `super` to keep the default forwarding +/// behavior. +@interface BNViewDelegate : NSObject + +/// Initialize a delegate that drives `view`. The reference is held +/// weakly; the host (or BNView, when this is the auto-installed +/// instance) is responsible for keeping the BNView alive. +- (nullable instancetype)initWithView:(BNView*)view NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + @interface BNView : NSObject -/// Attach `runtime` to render into `view` ("super simple" mode). BNView -/// installs itself as the MTKView's `delegate` and drives the per-frame -/// render and resize callbacks internally — the host does not need to -/// forward anything. +/// Attach `runtime` to render into `view`. On the first attach for a +/// given runtime, this triggers GPU device construction and engine +/// initialization. Subsequent attaches just rebind the surface. /// -/// Equivalent to `-initWithRuntime:view:autoDelegate:` with -/// `autoDelegate = YES`. -- (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view; - -/// Attach `runtime` to render into `view` with explicit control over -/// the MTKView delegate. +/// Returns `nil` if `runtime` or `view` is `nil`, or if the underlying +/// `Babylon::Integrations::View::Attach` fails. /// -/// - When `autoDelegate` is `YES`, BNView installs itself as the -/// MTKView's delegate and drives `drawInMTKView:` and -/// `mtkView:drawableSizeWillChange:` internally. The host should -/// not assign its own delegate to the same MTKView. -/// - When `autoDelegate` is `NO`, the host keeps ownership of the -/// MTKView's delegate and is responsible for calling `-renderFrame` -/// and `-resizeWithWidth:height:` from its own MTKViewDelegate -/// methods. Use this mode when the host needs to interleave its -/// own per-frame work with the runtime's render. -- (instancetype)initWithRuntime:(BNRuntime*)runtime - view:(MTKView*)view - autoDelegate:(BOOL)autoDelegate NS_DESIGNATED_INITIALIZER; - -/// Render exactly one frame. Only used in manual-delegate mode -/// (`autoDelegate = NO`) — call from your own `MTKViewDelegate -/// drawInMTKView:` method. +/// **Delegate management:** If `view.delegate` is `nil` at the time of +/// construction, BNView creates a `BNViewDelegate` and assigns it to +/// `view.delegate`, holding it strongly for the lifetime of the +/// BNView (so the host doesn't have to retain it themselves — +/// `MTKView.delegate` is a `weak` reference). If `view.delegate` is +/// already set, BNView does *not* touch it; the host is expected to +/// drive `-renderFrame` and `-resize(width:height:)` themselves +/// (typically via their own `MTKViewDelegate` or a `BNViewDelegate` +/// subclass). +- (nullable instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view; + +/// Render exactly one frame. Call from your `MTKViewDelegate +/// drawInMTKView:`, or rely on the auto-installed `BNViewDelegate` +/// to call it for you. - (void)renderFrame; /// Inform the runtime that the underlying surface's pixel-buffer size -/// has changed. Only used in manual-delegate mode — call from your -/// own `MTKViewDelegate mtkView:drawableSizeWillChange:` method with -/// the `size.width` / `size.height` it provides. Sizes are in physical -/// pixels — the same convention as `CAMetalLayer.drawableSize`. +/// has changed. Sizes are in physical pixels — the same convention as +/// `CAMetalLayer.drawableSize`. - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height NS_SWIFT_NAME(resize(width:height:)); From 799d4037603ff094a5d8f28364ad4d542a06cd1f Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 8 May 2026 15:09:25 -0700 Subject: [PATCH 22/71] Remove bad EnableRendering/DisableRendering --- Integrations/Source/View.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 24887b931..2db9bb70c 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -210,7 +210,6 @@ namespace Babylon::Integrations // and any loaded scripts are preserved on the JS side. impl.m_device->UpdateWindow(nativeWindow); impl.m_device->UpdateSize(width, height); - impl.m_device->EnableRendering(); impl.m_device->StartRenderingCurrentFrame(); impl.m_deviceUpdate->Start(); } @@ -236,7 +235,6 @@ namespace Babylon::Integrations { impl.m_deviceUpdate->Finish(); impl.m_device->FinishRenderingCurrentFrame(); - impl.m_device->DisableRendering(); } impl.m_currentView = nullptr; From f5c2bc8e7cd5faa03d088cb38683b1c11d06ab7b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 8 May 2026 16:47:51 -0700 Subject: [PATCH 23/71] Simplify JNI function names --- .../babylonjs/integrations/BabylonNative.java | 19 +++++++++++++------ .../library/babylonnative/BabylonView.java | 14 +++++++------- .../playground/PlaygroundActivity.java | 19 +++++++------------ .../main/cpp/BabylonNativeIntegrations.cpp | 17 ++++++++++------- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index b5c5a9c9a..2d7a29787 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -36,16 +36,23 @@ private BabylonNative() {} // Process-wide platform lifecycle // ------------------------------------------------------------------- - /** Call once at app startup before any other method. */ - public static native void androidGlobalInitialize(Context context); + /** + * Register the application Context. Hosts call this once at app + * startup (typically from {@code Application.onCreate} or the host + * Activity's {@code onCreate}), before constructing any Runtime. + * Calling more than once is harmless — the underlying + * {@code android::global::Initialize} replaces the existing + * Context global ref. + */ + public static native void setContext(Context context); - public static native void androidGlobalSetCurrentActivity(Object activity); + public static native void setCurrentActivity(Object activity); - public static native void androidGlobalPause(); + public static native void pause(); - public static native void androidGlobalResume(); + public static native void resume(); - public static native void androidGlobalRequestPermissionsResult( + public static native void requestPermissionsResult( int requestCode, String[] permissions, int[] grantResults); // ------------------------------------------------------------------- diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index a7b02de79..920ffff59 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -25,13 +25,13 @@ * needed at the rendering layer. * *

Activity lifecycle: the host Activity is responsible for the - * process-wide {@code androidGlobalInitialize}, {@code SetCurrentActivity}, - * {@code Pause}/{@code Resume}, and {@code RequestPermissionsResult} - * notifications (see {@code PlaygroundActivity.java}). The Runtime - * automatically subscribes to {@code androidGlobalPause / Resume} when - * created, so the host Activity does not need to invoke any per-view - * pause/resume method — telling the JNI layer once is enough for every - * Runtime in the process. + * process-wide {@code BabylonNative.setContext}, + * {@code setCurrentActivity}, {@code pause} / {@code resume}, and + * {@code requestPermissionsResult} notifications (see + * {@code PlaygroundActivity.java}). The Runtime automatically subscribes + * to {@code pause} / {@code resume} when created, so the host Activity + * does not need to invoke any per-view pause/resume method — telling + * the JNI layer once is enough for every Runtime in the process. */ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, View.OnTouchListener { private static final FrameLayout.LayoutParams childViewLayoutParams = diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 018920a5f..6ed35189e 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -8,9 +8,6 @@ import com.library.babylonnative.BabylonView; public class PlaygroundActivity extends Activity { - /** {@link BabylonNative#androidGlobalInitialize} is process-wide; only call it once. */ - private static boolean sGlobalInitDone = false; - /** * Native helper bridging to {@code Apps/Playground/Shared/PlaygroundScripts.cpp}, * which holds the Babylon.js bootstrap script list shared with the @@ -26,15 +23,13 @@ public class PlaygroundActivity extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); - // Process-wide one-shot init for AndroidExtensions::Globals + // Register the application Context with AndroidExtensions::Globals // (used by NativeCamera, NativeXr, etc.). Belongs at the // Activity/Application level — not on a per-view basis — because // it broadcasts to process-wide handlers that aren't refcounted. - if (!sGlobalInitDone) { - BabylonNative.androidGlobalInitialize(getApplication()); - sGlobalInitDone = true; - } - BabylonNative.androidGlobalSetCurrentActivity(this); + // The JNI layer guards against double-initialization internally. + BabylonNative.setContext(getApplication()); + BabylonNative.setCurrentActivity(this); // Owner of the Runtime lifetime: created here, destroyed in // onDestroy. The View only borrows the handle for its surface @@ -64,14 +59,14 @@ protected void onPause() { // auto-suspends because they each subscribed to this event in // BabylonNative.runtimeCreate. Same for cross-cutting subsystems // (NativeCamera, NativeXr) that hook AndroidExtensions::Globals. - BabylonNative.androidGlobalPause(); + BabylonNative.pause(); super.onPause(); } @Override protected void onResume() { super.onResume(); - BabylonNative.androidGlobalResume(); + BabylonNative.resume(); } @Override @@ -87,7 +82,7 @@ protected void onDestroy() { @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { - BabylonNative.androidGlobalRequestPermissionsResult(requestCode, permissions, results); + BabylonNative.requestPermissionsResult(requestCode, permissions, results); } @Override diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index ee5d1741c..890e4b8ab 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -124,10 +124,13 @@ extern "C" // See SimplifiedAPI.md §4.2 "Interop layer responsibilities". // ===================================================================== -// Call once at app startup, typically from `Application.onCreate`, -// before constructing any BabylonNativeRuntime. +// Set the application Context. Hosts call this once at app startup +// (typically from `Application.onCreate` or the host Activity's +// `onCreate`), before constructing any Runtime. Calling more than +// once is harmless — `android::global::Initialize` deletes any +// existing Context global ref and installs the new one. JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_androidGlobalInitialize( +Java_com_babylonjs_integrations_BabylonNative_setContext( JNIEnv* env, jclass, jobject context) { JavaVM* javaVM{nullptr}; @@ -139,26 +142,26 @@ Java_com_babylonjs_integrations_BabylonNative_androidGlobalInitialize( } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_androidGlobalSetCurrentActivity( +Java_com_babylonjs_integrations_BabylonNative_setCurrentActivity( JNIEnv*, jclass, jobject activity) { android::global::SetCurrentActivity(activity); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_androidGlobalPause(JNIEnv*, jclass) +Java_com_babylonjs_integrations_BabylonNative_pause(JNIEnv*, jclass) { android::global::Pause(); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_androidGlobalResume(JNIEnv*, jclass) +Java_com_babylonjs_integrations_BabylonNative_resume(JNIEnv*, jclass) { android::global::Resume(); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_androidGlobalRequestPermissionsResult( +Java_com_babylonjs_integrations_BabylonNative_requestPermissionsResult( JNIEnv* env, jclass, jint requestCode, jobjectArray permissions, jintArray grantResults) { std::vector nativePermissions{}; From 1b7a58e498569112766fdca9aa5b76fdd19c3b84 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 11 May 2026 11:15:41 -0700 Subject: [PATCH 24/71] Implicit View suspend/resume --- Integrations/Source/Runtime.cpp | 17 ++ Integrations/Source/RuntimeImpl.h | 27 +- Integrations/Source/View.cpp | 100 ++++-- copilot-mobile.md | 490 ++++++++++++++++++++++++++++++ 4 files changed, 608 insertions(+), 26 deletions(-) create mode 100644 copilot-mobile.md diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 4ffde0bbf..16cf36d07 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -154,6 +154,17 @@ namespace Babylon::Integrations std::lock_guard lock{m_impl->m_suspendMutex}; if (m_impl->m_suspendCount++ == 0) { + // Close the in-flight frame on the currently attached View + // (if any) BEFORE blocking the JS thread. This keeps the + // GPU side clean across the suspension: no held drawable, + // no open DeviceUpdate safe-timespan. The View tracks its + // own state, so this composes with the subsequent ~View + // (which Suspends again — no-op) and with hosts that + // suspend the Runtime while no View is attached. + if (m_impl->m_currentView) + { + m_impl->m_currentView->m_impl->Suspend(); + } m_impl->m_appRuntime->Suspend(); } } @@ -169,6 +180,12 @@ namespace Babylon::Integrations if (--m_impl->m_suspendCount == 0) { m_impl->m_appRuntime->Resume(); + // Re-open the frame on the attached View (if any) so the + // next RenderFrame call has something to Finish. + if (m_impl->m_currentView) + { + m_impl->m_currentView->m_impl->Resume(); + } } } diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 0147ea45c..2d6b5198c 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -103,11 +103,36 @@ namespace Babylon::Integrations // Internal implementation of View. Holds the back-reference to the // Runtime that produced it. Provides per-platform helpers - // (implemented in ViewImpl_*.cpp / .mm). + // (implemented in ViewImpl_*.cpp / .mm) plus Suspend / Resume that + // manage the in-flight frame across runtime suspension. + // + // `m_suspended` is the View's view of whether it's currently + // holding an open Device frame: + // - true → no frame is open (StartRenderingCurrentFrame + + // DeviceUpdate::Start have NOT been called, or have + // been matched by Finish / FinishRenderingCurrentFrame) + // - false → a frame is open and the JS thread's safe timespan + // is active + // + // Initially `true`; `View::Attach` flips it to `false` after + // opening the first frame. `~View` calls `Suspend` before + // `Device::DisableRendering`. `Runtime::Suspend / Resume` call + // through here on the suspendCount 0↔1 transitions. struct ViewImpl { explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} Runtime& m_runtime; + bool m_suspended{true}; + + // End the in-flight frame on the Device (Finish + + // FinishRenderingCurrentFrame). Idempotent — no-op if already + // suspended. + void Suspend(); + + // Open a new frame (StartRenderingCurrentFrame + + // DeviceUpdate::Start). Idempotent — no-op if not currently + // suspended. + void Resume(); // Query the surface's pixel-buffer size from the native window // handle. Implemented per-platform. diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 2db9bb70c..e29049487 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -171,17 +171,17 @@ namespace Babylon::Integrations return nullptr; } + const bool firstAttach = !impl.m_device; + // Per-platform: query the surface's pixel-buffer size from the // native window handle. ViewImpl_*.cpp implements this; e.g. // ANativeWindow_getWidth on Android, GetClientRect on Win32, // CAMetalLayer.drawableSize on Apple. const auto [width, height] = ViewImpl::QuerySize(nativeWindow); - if (!impl.m_device) + if (firstAttach) { - // First Attach on this Runtime: construct the Device and - // open the first frame, then dispatch the engine-init lambda - // onto the JS thread. + // First Attach on this Runtime: construct the Device. Babylon::Graphics::Configuration config{}; config.Window = nativeWindow; config.Width = width; @@ -194,28 +194,31 @@ namespace Babylon::Integrations #if BABYLON_NATIVE_PLUGIN_SHADERCACHE Babylon::Plugins::ShaderCache::Enable(); #endif - - // Open the first frame *before* dispatching the init lambda - // so the Device::AddToJavaScript inside the lambda sees an - // open frame to record into. - impl.m_device->StartRenderingCurrentFrame(); - impl.m_deviceUpdate->Start(); - - RunFirstAttachInit(impl, nativeWindow); } else { // Subsequent Attach: reuse the existing Device, just rebind - // the surface and re-enable rendering. Plugins, polyfills, - // and any loaded scripts are preserved on the JS side. + // the surface. Plugins, polyfills, and any loaded scripts + // are preserved on the JS side. impl.m_device->UpdateWindow(nativeWindow); impl.m_device->UpdateSize(width, height); - impl.m_device->StartRenderingCurrentFrame(); - impl.m_deviceUpdate->Start(); } std::unique_ptr view{new View{std::make_unique(runtime)}}; impl.m_currentView = view.get(); + + // Open the first frame via ViewImpl::Resume (which flips + // m_suspended → false). On first Attach, this must happen + // BEFORE dispatching the engine-init lambda so the + // Device::AddToJavaScript inside the lambda sees an open + // frame to record into. + view->m_impl->Resume(); + + if (firstAttach) + { + RunFirstAttachInit(impl, nativeWindow); + } + return view; } @@ -228,26 +231,73 @@ namespace Babylon::Integrations { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Symmetric counterpart to Attach: close the in-flight frame and - // disable rendering. The Device persists on the Runtime so the - // next Attach is cheap. - if (impl.m_device) + // End the in-flight frame if one is open. Idempotent: if the + // Runtime was already suspended (which closed the frame via + // ViewImpl::Suspend), this is a no-op. The Device persists on + // the Runtime so the next Attach is cheap. + m_impl->Suspend(); + + impl.m_currentView = nullptr; + } + + // --------------------------------------------------------------------- + // ViewImpl::Suspend / Resume + // + // Idempotent open/close of the in-flight Device frame. Called from: + // - View::Attach → Resume (open frame after Device setup) + // - View::~View → Suspend (close frame at teardown) + // - Runtime::Suspend / Resume → matching call on the currently + // attached view, if any, so the host's + // OS-level pause/resume signal cleanly + // brackets the GPU frame. + // + // The internal `m_suspended` flag means "no frame currently open." + // Initial state is `true`; the very first Resume opens the first + // frame. Multiple Suspend or Resume calls in a row are no-ops. + // --------------------------------------------------------------------- + void ViewImpl::Suspend() + { + if (m_suspended) + { + return; + } + RuntimeImpl& impl = *m_runtime.m_impl; + if (impl.m_device && impl.m_deviceUpdate) { impl.m_deviceUpdate->Finish(); impl.m_device->FinishRenderingCurrentFrame(); } + m_suspended = true; + } - impl.m_currentView = nullptr; + void ViewImpl::Resume() + { + if (!m_suspended) + { + return; + } + RuntimeImpl& impl = *m_runtime.m_impl; + if (impl.m_device && impl.m_deviceUpdate) + { + impl.m_device->StartRenderingCurrentFrame(); + impl.m_deviceUpdate->Start(); + } + m_suspended = false; } void View::RenderFrame() { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Cheap suspended check (own mutex). Skip the GPU work entirely - // while the runtime is suspended; the host can keep calling - // RenderFrame() from its draw callback unconditionally. - if (m_impl->m_runtime.IsSuspended()) + // Skip the GPU work entirely while this view is suspended; + // the host can keep calling RenderFrame() from its draw + // callback unconditionally. The view's `m_suspended` flag is + // flipped by Runtime::Suspend/Resume (and by ~View / Attach + // for the destruction / construction boundaries), so this + // check covers every "frame is not currently open" case + // including: pre-Attach, between Suspend and Resume, and + // during teardown. + if (m_impl->m_suspended) { return; } diff --git a/copilot-mobile.md b/copilot-mobile.md new file mode 100644 index 000000000..2235cdac1 --- /dev/null +++ b/copilot-mobile.md @@ -0,0 +1,490 @@ +# Copilot Mobile Migration to `Babylon::Integrations` + +> Plan for replacing the Copilot Mobile codebase's bespoke Babylon Native +> JNI bridge with the new `Babylon::Integrations` interop layer +> (`Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp` etc. +> in this repo). +> +> This document covers the **Android** migration. iOS will follow the +> same shape using `Integrations/Apple/`. + +--- + +## 1. What the Copilot Mobile stack looks like today + +Three repositories cooperate: + +``` +copilot-appearance ─► copilot-appearance.js (the JS bundle) + ▲ loaded via "file:///..." app:/// URL +babylon-native-bridge ─► libBabylonNativeJNI.so + Java glue (per-app JNI) + ▲ depends on + BabylonNative ─► AppRuntime, Device, plugins, polyfills (this repo) + ▲ consumed by +app-android ─► Compose / Kotlin host (the app) +``` + +### Native side (`babylon-native-bridge/android/src/main/cpp/babylon.cpp`) + +Single 850-line cpp file. Owns these process-globals (under one +`engineMutex`): + +```cpp +std::optional device; +std::optional deviceUpdate; +std::optional runtime; +Babylon::Plugins::NativeInput* nativeInput; +std::optional nativeCanvas; +std::optional scriptLoader; +std::atomic engineReady, waitingOnNewView, activityPaused, needsDisableRendering; +``` + +JNI symbols all on `com.microsoft.babylonnative.BabylonNativeBridge`: + +| JNI method | What it does | +|---|---| +| `preload(hiddenSurface, context, cacheDir, scriptPath)` | One-shot init: `android::global::Initialize`, construct Device against the hidden 16×16 surface, load persistent shader cache from `cacheDir`, construct `AppRuntime`, dispatch a JS-thread lambda that wires up Console / NativeEngine / NativeOptimizations / NativeInput / Polyfills / Canvas, registers `lumiInterop` (custom JS bridge), then `LoadScript(scriptPath)`. | +| `surfaceChanged(w, h, surface)` | Switch to a real on-screen surface. Tears down bgfx if `needsDisableRendering`, then `UpdateWindow / UpdateSize`, pumps a clear frame, sets `engineReady = true`. | +| `onSurfaceTeardown()` | Sets `engineReady = false; waitingOnNewView = true; needsDisableRendering = true`. Defers actual `DisableRendering` to the next `renderFrame` (lazy teardown). | +| `renderFrame()` | Tries the engineMutex (non-blocking); honors `needsDisableRendering` (tears bgfx down) and `activityPaused`; otherwise the standard `Finish/Start` pair. Tracks FPS over 100-frame windows. Skips entirely until `scriptNotifiedReady` is set by JS via `lumiInterop.notifyReady()`. | +| `readyToRender()` | Returns `engineReady`. | +| `setCurrentActivity / activityOnPause / activityOnResume / activityOnRequestPermissionsResult` | Forwards to `android::global::*`; `Pause` / `Resume` also `runtime->Suspend / Resume` and toggle the `activityPaused` flag. | +| `setTouchInfo(id, x, y, buttonAction, buttonValue)` | One method for down / move / up; `try_to_lock`. | +| `loadScript / eval / runScript` | Forwards to `ScriptLoader` / `AppRuntime::Dispatch`. | +| `getStats()` | Returns `"FPS "` from the renderFrame counter. | +| `finishEngine()` | Tears everything down in order. | + +Custom JS-visible global `lumiInterop` (implemented as a `Napi::ObjectWrap`): + +- `lumiInterop.notifyReady()` — flips `scriptNotifiedReady = true`. Until + this fires, `renderFrame` is a no-op (gate against rendering before + the JS side has finished its first `start`). +- `lumiInterop.callNative(jsonString)` — calls the Java static method + `BabylonNativeBridge.onNativeFunctionCallInternal(String)`, which + forwards to a single registered `NativeFunctionCallback`. This is + Copilot's JS→native message pipe. + +### Java side (`babylon-native-bridge/android/src/main/java`) + +- **`BabylonNativeBridge`** — `static native` declarations + the + `NativeFunctionCallback` registration glue. +- **`BabylonNativeManagerView`** — preload widget. The host puts an + invisible 1×1 dp `SurfaceView` on screen; when `surfaceCreated` fires, + the holder is stashed; `initializeWithScript(path)` (called once by + the host once the script is downloaded) calls `BabylonNativeBridge.preload(...)` + on the main thread. Pre-renders 5 frames (warming bgfx + shader compile) + then stops invalidating to avoid touching a dying EGL surface. +- **`BabylonView`** — visible widget. Constructed against an `Activity` + + an optional `useMediaOverlay` flag (Z-order). Calls + `setCurrentActivity` / `surfaceChanged` / `onSurfaceTeardown` / + `setTouchInfo`. Drives `renderFrame` from `onDraw` while `mViewReady`, + re-`invalidate()`s while `mRenderLoopActive`. `onPause / onResume` + forward to `BabylonNativeBridge.activityOnPause / Resume`. + +### Kotlin host (`app-android/.../BabylonManagerView.kt`) + +Compose component that mounts `BabylonNativeManagerView` (the preload +SurfaceView) inside an invisible 1×1 dp Box. Subscribes to a +`FilamentGlSignal` to wait until the parallel Filament engine has +initialized its GL context (avoids GL contention). Observes +`Lifecycle.Event.ON_PAUSE / ON_RESUME` to call +`BabylonNativeBridge.activityOnPause / Resume`. Calls +`BabylonNativeManagerView.initializeWithScript(scriptPath)` once the +script asset has been downloaded; retries after 2 seconds if it +silently fails. Adds an extra `SurfaceHolder.Callback` to the inner +SurfaceView to call `BabylonNativeBridge.onSurfaceTeardown()` on +`surfaceDestroyed`. + +### JS side (`copilot-appearance/.../surfaces/mobile/src/main.ts`) + +The downloaded bundle calls `lumiInterop.callNative(JSON.stringify({...}))` +to send messages to native (e.g. `"scriptLoaded"`, +`"initializationError"`), and `lumiInterop.notifyReady()` once +`window.start(...)` finishes synchronously. + +--- + +## 2. What `Babylon::Integrations` already does for this stack + +Comparing one to one: + +| Concern | Copilot Mobile today | `Babylon::Integrations` today | +|---|---|---| +| AppRuntime + JsRuntime + ScriptLoader lifecycle | `babylon.cpp::preload` / `finishEngine` | `Runtime::Create` / `~Runtime` | +| Device + DeviceUpdate construction at first surface | `preload` constructs against a 16×16 hidden surface | `View::Attach` on first call constructs against the real surface (or any surface — see "prewarm" below) | +| Initialization queueing (LoadScript before Device exists) | Not separated; `preload` does both at once | Pre-init queue with `arcana::task_completion_source` | +| Hidden / dummy surface to start engine before the visible UI exists | The "preload SurfaceView" pattern in `BabylonNativeManagerView` | Same pattern, documented in `SimplifiedAPI.md §4.1 "Starting the engine before the user-visible UI exists"` | +| Surface switch | `surfaceChanged` → `Device::UpdateWindow / UpdateSize` | `~View` then a fresh `View::Attach` (which calls the same Device methods internally) | +| Render frame | `renderFrame` (`Finish/Start` cycle) | `View::RenderFrame` (same cycle, with `m_runtime.IsSuspended()` short-circuit) | +| Suspend/Resume from Activity lifecycle | `activityOnPause` / `activityOnResume` JNI methods | Auto-wired in `runtimeCreate` via `android::global::AddPauseCallback / AddResumeCallback` (each Runtime auto-suspends when the host calls `BabylonNative.pause/resume`). | +| `android::global::*` plumbing (NativeCamera, NativeXr) | `setCurrentActivity` / `activityOnRequestPermissionsResult` JNI | `BabylonNative.setContext / setCurrentActivity / requestPermissionsResult` JNI | +| Pointer input | `setTouchInfo(id, x, y, button, value)` (one method) | `viewPointerDown / Move / Up` (three methods); the View internally divides physical→logical via `ViewImpl::ToLogicalCoords` | +| Console / DebugTrace / uncaught exceptions | Hard-coded `__android_log_write` switch on level | `RuntimeOptions::log(LogLevel, std::string_view)`; the JNI `runtimeCreate` already routes everything to logcat under tag `"BabylonNative"` | +| Persistent shader cache | Hard-coded in `babylon.cpp` (`PlaygroundShaderCache.bin` in `cacheDir`) | **Not yet** in Integrations layer — see Open Questions | +| `lumiInterop.notifyReady()` gate | `scriptNotifiedReady` atomic; `renderFrame` no-ops until it fires | **Not** in Integrations layer — Copilot-specific. See migration plan below. | +| `lumiInterop.callNative()` | Custom `Napi::ObjectWrap` registering global `lumiInterop`; calls back into Java via cached `jclass` | **Not** in Integrations layer — Copilot-specific. Stays in app-specific JNI helper. | +| FPS stats | `getStats()` returns "FPS " | **Not** in Integrations layer. Easy to keep app-side. | + +### Built-in features the migration brings for free + +- Threaded `Suspend / Resume` reference counting. +- TCS-based pre-init queueing — host can call `runtimeLoadScript` before + the Device exists. +- Lazy GPU init: `runtimeCreate` is cheap and allocation-free; first + `viewAttach` triggers Device + plugin init. Means we no longer need + the dummy 1×1 SurfaceView trick *for the engine init phase*. (We + still want it for Copilot's "render in the background while the + visible view isn't yet up" use case — but that's a different story; + see §3.2.) +- Consistent logical-pixel pointer coordinates (`ViewImpl_Android.cpp` + divides by `Device::GetDevicePixelRatio()`). +- Per-platform window-size query: `View::Attach(runtime, window)` calls + `ANativeWindow_getWidth/Height` itself; no need for the + `surfaceChanged(w, h, surface)` JNI signature. Resize is + `view.viewResize(w, h)`. + +--- + +## 3. Migration plan + +### 3.1 Copilot-specific app surface to keep + +Two things are app-specific and must remain in app-side glue (analogous +to `Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp` +in this repo, where the Playground app keeps a Playground-specific JNI +helper alongside the generic one): + +1. **`lumiInterop` global** — `notifyReady()` and `callNative(json)`, + plus the Java-side static method dispatch and the + `NativeFunctionCallback` registration. This is Copilot's product + contract with the JS bundle. +2. **Persistent shader cache load/save** — `cacheDir` plumbing, + `Babylon::Plugins::ShaderCache::Load / Save` to `PlaygroundShaderCache.bin`. + +Both live in a new file in babylon-native-bridge: +`android/src/main/cpp/copilot_jni.cpp` (or similar). Same pattern as +`PlaygroundJNI.cpp` — `target_sources` it onto the +`BabylonNativeIntegrations` shared library so there's exactly one copy +of `Babylon::Integrations` symbols in the process. + +### 3.2 Engine-prewarm pattern — keep using the hidden SurfaceView + +The Copilot product wants the engine warmed up before the user +navigates to the live view (so the avatar is ready as soon as the +visible surface appears). Two implementation options: + +**Option A — Same hidden-SurfaceView trick.** Mount a 1×1 invisible +`SurfaceView`, call `BNManager.viewAttach(runtime, surface)` against +its `ANativeWindow`. That triggers Device construction + plugin init ++ first frame. When the visible `BabylonView` later does its own +`surfaceCreated`, we **detach** the prewarm view and **attach** the +visible one. `Babylon::Integrations` permits exactly this: subsequent +`Attach` calls just rebind via `Device::UpdateWindow` (cheap), and the +JS state and ShaderCache survive. + +**Option B — Headless prewarm via `Runtime::Create` only.** No +SurfaceView at all; let `runtimeCreate` start the JS engine and queue +`runtimeLoadScript`; only when the visible `BabylonView` is created +does the first `viewAttach` happen. JS bundle initialization runs +serially after first attach (because pre-init queueing waits for the +Device). + +**Recommendation: Option A.** Matches today's behavior, matches the +Playground host pattern, and shaves the most latency from the visible +view's first frame. Compose composable structure barely changes. + +### 3.3 Concrete file-by-file changes + +#### `babylon-native-bridge/android/src/main/cpp/babylon.cpp` → mostly delete + +Replace with a new `copilot_jni.cpp` that is **very small**: + +- `LumiInterop` Napi::ObjectWrap class (lift from current babylon.cpp, + unchanged). +- One JNI method `Java_com_microsoft_babylonnative_CopilotBridge_initialize(runtimeHandle, cacheDir)` + that: + - Caches `g_javaVM` and the `BabylonNativeBridge` jclass global ref + (currently in `preload`). + - Loads shader cache from `cacheDir/PlaygroundShaderCache.bin` if + present. + - Calls `runtime->RunOnJsThread([](Napi::Env env){ LumiInterop::Initialize(env); })`. +- One JNI method `Java_com_microsoft_babylonnative_CopilotBridge_saveShaderCache()` + for the explicit save path (called from `Activity.onPause` and + `runtime` teardown). +- One JNI method `Java_com_microsoft_babylonnative_CopilotBridge_isReadyToRender(runtimeHandle)` + that returns `scriptNotifiedReady && runtimeHandle != 0`. (See §3.4 + about why this is on the app side.) +- Resolve the runtime handle via `Babylon::Integrations::Android::RuntimeFromHandle` + (already in `Integrations/Android/Include/...`). + +Net: ~150 lines of app-specific code, vs. today's 850. + +#### `BabylonNativeBridge.java` → delete (after step 5) + +Becomes redundant. Its two responsibilities split as follows: + +- **`static native` declarations for engine/view ops** → all callers + shift to `com.babylonjs.integrations.BabylonNative` (the generic + Integrations Java class, identical to what the Playground uses). +- **`NativeFunctionCallback` registration + `onNativeFunctionCallInternal(String)`** + → moves to a new `CopilotBridge` class (next). + +The intermediate `System.loadLibrary` static block stays alive on +`CopilotBridge` so the .so loads once. + +#### `CopilotBridge.java` (new) + +App-specific JNI surface. Wraps `copilot_jni.cpp`: + +```java +public final class CopilotBridge { + static { System.loadLibrary("BabylonNativeIntegrations"); } + private CopilotBridge() {} + + /** Performs Copilot-specific runtime setup: caches the JavaVM + + * CopilotBridge jclass for `lumiInterop.callNative`, registers + * the `lumiInterop` global on the JS side, loads the persistent + * shader cache from `cacheDir`. Call once per Runtime, after + * `BabylonNative.runtimeCreate`. */ + public static native void initialize(long runtimeHandle, String cacheDir); + + /** Save the persistent shader cache (typically called from + * Activity.onPause). No-op if `initialize` hasn't been called. */ + public static native void saveShaderCache(); + + /** Returns true once `lumiInterop.notifyReady()` has been called + * by the JS bundle. The Java widgets call this before each + * `viewRenderFrame` to avoid rendering empty frames. */ + public static native boolean isReadyToRender(); + + // ----- JS → native message pipe ----- + + public interface NativeFunctionCallback { + void onNativeFunctionCall(String jsonData); + } + private static NativeFunctionCallback callback; + public static void setNativeFunctionCallback(NativeFunctionCallback cb) { callback = cb; } + /** Called from native by `lumiInterop.callNative(json)`. */ + public static void onNativeFunctionCallInternal(String jsonData) { + if (callback != null) callback.onNativeFunctionCall(jsonData); + } +} +``` + +#### `BabylonNativeBridge.java` → mostly delete + +#### `BabylonNativeManagerView.java` → simplify + +Conceptually unchanged — still hosts an invisible 1×1 SurfaceView for +prewarm — but its native interactions become: + +- Constructor: `BabylonNative.setContext(context.getApplicationContext())` + (idempotent inside the JNI). Caller still creates the Runtime; this + view borrows the handle. +- `surfaceCreated`: stash holder + activity (today's behavior). +- `initializeWithScript(scriptPath)` (its public API, called from + Compose host once the asset is downloaded): + - Resolve to a real ANativeWindow on the main thread via the holder. + - `viewHandle = BabylonNative.viewAttach(runtimeHandle, surface)` — + triggers Device construction + plugin init + queued-script flush. + (Bootstrap = `runtimeLoadScript(scriptPath)` happened earlier on + the Runtime; the Integrations TCS queues it until first Attach.) + - `CopilotBridge.initialize(runtimeHandle, activity.cacheDir)` — + caches JavaVM + bridge jclass, loads shader cache, registers + `lumiInterop`. +- `onDraw` prewarm loop (5 frames): drives `BabylonNative.viewRenderFrame(viewHandle)`, + guarded by `CopilotBridge.isReadyToRender(runtimeHandle)`. Still + stops invalidating after 5 frames as today. +- `notifyTearDownCurrentSurface()`: `BabylonNative.viewDetach(viewHandle)` + (replaces `BabylonNativeBridge.onSurfaceTeardown()`). Caller is + `BabylonManagerView.kt` from `surfaceDestroyed`. + +**One ownership decision required:** today the Runtime is implicitly +created inside `preload`; nobody owns it explicitly. For the migration, +the Compose host (`BabylonManagerView.kt`) should own the +`runtimeHandle` (created on first composition, destroyed in +`onRelease`). `BabylonNativeManagerView` and `BabylonView` borrow it. + +#### `BabylonView.java` → simplify + +Becomes very close to the Playground's `BabylonView.java`. Borrows the +runtime handle from the host. `surfaceCreated → viewAttach`, +`surfaceDestroyed → viewDetach`, `surfaceChanged → viewResize`, +`onTouch → viewPointerDown / Move / Up`. Keeps the `useMediaOverlay` +parameter (product-specific Z-order behavior). + +Drops: +- Manual `pixelDensityScale` math (Integrations does it). +- `mViewReady` / `mRenderLoopActive` flags — replaced by the + attached/detached state of `viewHandle`. +- `setCurrentActivity` / `onPause / onResume / onRequestPermissionsResult` + forwarding — those move to the Activity / Compose host (matching + the design we settled on for the Playground). + +#### `BabylonManagerView.kt` (Compose host) → small changes + +- Owns `runtimeHandle: Long`; creates with + `BabylonNative.runtimeCreate(enableDebugger = false)` once Filament + signals ready, destroys in `onDispose`. +- Calls `BabylonNative.setContext(application)` and + `BabylonNative.setCurrentActivity(activity)` (instead of going + through `BabylonNativeBridge`). +- `Lifecycle.Event.ON_PAUSE / ON_RESUME` → `BabylonNative.pause / resume` + (no per-view forwarding). +- `LaunchedEffect` triggers `runtimeLoadScript(runtimeHandle, scriptPath)` + once the path is known — this can fire even before the SurfaceView + exists, because Integrations queues it. Then the `surfaceCreated` + path of `BabylonNativeManagerView` does its prewarm-attach and + `CopilotBridge.initialize`. +- Rest of Compose plumbing (`FilamentGlSignal` wait, ViewModel script + path resolution, retry-after-delay) is unchanged. + +#### `copilot-appearance/.../mobile/src/main.ts` → no change + +`lumiInterop.notifyReady` and `lumiInterop.callNative` are preserved +verbatim. + +#### `babylon-native-bridge/android/CMakeLists.txt` + +- Add `set(BABYLON_NATIVE_INTEGRATIONS ON CACHE BOOL "" FORCE)`. +- Add `set(BABYLON_NATIVE_INTEGRATIONS_ANDROID ON CACHE BOOL "" FORCE)`. +- Drop the `BabylonNativeJNI` `add_library` and its plugin link list. +- After `add_subdirectory(${BN_REPO_ROOT_DIR})`, add + `target_sources(BabylonNativeIntegrations PRIVATE + src/main/cpp/copilot_jni.cpp)` plus `target_link_libraries(... + PRIVATE NativeOptimizations URL Performance Window)` (the few + Copilot uses that aren't unconditionally linked by the + Integrations target — to verify against the current Integrations + `CMakeLists.txt`). + +The published `.so` becomes `libBabylonNativeIntegrations.so` instead +of `libBabylonNativeJNI.so`. Update gradle / Java +`System.loadLibrary("BabylonNativeIntegrations")` accordingly. + +### 3.4 Things that need extra design discussion + +1. **`scriptNotifiedReady` gating of `renderFrame`.** Today, `renderFrame` + is a no-op until JS calls `lumiInterop.notifyReady()`. The + Integrations `View::RenderFrame` doesn't have this gate. Two ways + to add it: + - **Cheap:** keep a static `std::atomic` in `copilot_jni.cpp` + that `lumiInterop.notifyReady` flips, and have + `BabylonNativeManagerView.onDraw` / `BabylonView.onDraw` consult + `CopilotBridge.isReadyToRender(runtimeHandle)` before calling + `viewRenderFrame`. Same behavior as today; isolated to app code. + - **Generic:** add an opt-in "render gate" callback to + `RuntimeOptions` so any host can express "don't render until JS + signals ready". Probably overkill for one app. + I recommend the cheap path. + +2. **Persistent shader cache.** `Babylon::Plugins::ShaderCache` is + already linked from Integrations (gated by + `BABYLON_NATIVE_PLUGIN_SHADERCACHE`); `View::Attach` enables it. + But Integrations doesn't expose `ShaderCache::Load / Save`. For + Copilot, `copilot_jni.cpp` calls `ShaderCache::Load(stream)` during + `CopilotBridge.initialize`, and `ShaderCache::Save(stream)` from a + `CopilotBridge.saveShaderCache()` JNI method (called from Compose + host's `ON_PAUSE`). + +3. **Existing crash-prevention bandaids in `babylon.cpp`** — + `engineMutex try_to_lock`, `needsDisableRendering` lazy bgfx + teardown, the "pump a clear frame" trick after `surfaceChanged`, + the `waitingOnNewView` / `activityPaused` interleavings. Some are + workarounds for races we've since fixed (e.g. the + `~ScriptLoader / ~AppRuntime` ordering is enforced in + `RuntimeImpl::~RuntimeImpl`); others may still be necessary on + certain devices (the lazy bgfx teardown comment cites SIGABRT in + `GlContext::resize`). Plan: move forward with the Integrations API + as-is; if the crashes reappear in QA, add corresponding hooks to + Integrations rather than retain the bandaids on the Copilot side. + +4. **`useMediaOverlay` Z-ordering.** Compose-side concern; preserved on + the Java widget. No native-API impact. + +### 3.5 Sequencing + +Suggested order (each step independently builds + runs): + +1. **Add `CopilotBridge.java` + `copilot_jni.cpp`** — `lumiInterop` + + shader-cache load/save + the `isReadyToRender` gate. Keep the + existing `babylon.cpp` working in parallel (no changes there yet). +2. **Wire up `Babylon::Integrations` build.** Update + `babylon-native-bridge/android/CMakeLists.txt` to enable the + Integrations Android target; produce `libBabylonNativeIntegrations.so` + *alongside* `libBabylonNativeJNI.so`. Add `BabylonNative.java` + (the generic JNI declarations) verbatim from the Playground, + adjusted for the Copilot package. +3. **Migrate `BabylonView.java`** to use the Integrations API (drop + `surfaceChanged / setTouchInfo` etc.). Keep + `BabylonNativeManagerView.java` and the Compose host calling + `BabylonNativeBridge.preload` for now — the visible view's + migration is independent. +4. **Migrate `BabylonNativeManagerView.java`** + Compose host: own the + runtime handle, prewarm via `viewAttach`. Delete + `BabylonNativeBridge.preload / surfaceChanged / activityOnPause / + activityOnResume / setCurrentActivity / setTouchInfo / loadScript + / eval / renderFrame / onSurfaceTeardown / readyToRender / + getStats / runScript / finishEngine` + their JNI implementations. +5. **Delete `babylon.cpp`** — at this point only `copilot_jni.cpp` + remains in `babylon-native-bridge/android/src/main/cpp/`. +6. **Delete `libBabylonNativeJNI.so` build target** from CMakeLists. + +After step 4 the app works end-to-end on the new API; steps 5–6 are +cleanup. + +### 3.6 Testing checklist + +- [ ] Cold-launch the app — `BabylonManagerView` mounts, prewarm + surface created, scene loads behind the scenes. +- [ ] Navigate to live view — `BabylonView` attaches to its visible + surface; first frame appears immediately (shader cache primed). +- [ ] Background / foreground the app multiple times — no SIGSEGV / + SIGABRT in `GlContext::resize` or `bgfx::reset`. +- [ ] Touch input on hi-DPI device — coordinates reach Babylon at the + correct logical-pixel scale (regression target: tap detection + is no different from today). +- [ ] `lumiInterop.callNative` round-trip — JS sends a payload, + `NativeFunctionCallback.onNativeFunctionCall(jsonData)` fires. +- [ ] Shader cache file is created in `cacheDir` after first session + and primes subsequent launches. +- [ ] Rotating the device → `surfaceChanged` → smooth resize, no + teardown/reattach unless Android genuinely recreates the + Surface (which the current `surfaceCreated → viewAttach` / + `surfaceChanged → viewResize` split handles). + +--- + +## 4. iOS migration (sketch — not in scope for this plan) + +`Apps/Playground/iOS` already uses the Apple interop layer +(`BNRuntime`, `BNView`). The `babylon-native-bridge/ios` folder will +follow the same shape: drop the bespoke Obj-C bridge, replace with +calls into `BNRuntime / BNView`, keep a small `CopilotBridge.mm` that +exposes `lumiInterop` + shader cache. + +--- + +## Open questions (resolved) + +1. ~~Is "render-gate via `lumiInterop.notifyReady`" still load-bearing, + or is the bug it was working around fixable / already fixed?~~ + **Confirmed: keep the gate. It's not a bug bandaid — it's a + legitimate "wait until JS has a scene" signal. The JS bundle's + `window.start(...)` does the engine/scene/asset setup synchronously, + then calls `lumiInterop.notifyReady()` at the end. Until that fires, + `viewRenderFrame` would just blit empty buffers. The Java widgets + will consult `CopilotBridge.isReadyToRender(runtimeHandle)` before + each render call.** +2. ~~Is the prewarm pattern (Option A) the desired ongoing behavior, or + is the team open to dropping it once the Integrations layer's lazy + GPU init is in place (Option B)?~~ **Confirmed: keep the prewarm + pattern (Option A). `BabylonNativeManagerView` retains its hidden + 1×1 SurfaceView; it just calls `viewAttach` on the Integrations + layer instead of `BabylonNativeBridge.preload`.** +3. ~~Naming — should the new app-specific class be `CopilotBridge` (as + in this plan) or stay `BabylonNativeBridge`?~~ **Confirmed: + `CopilotBridge` for the app-specific surface (`lumiInterop` glue, + shader-cache load/save, render-ready gate). The legacy + `BabylonNativeBridge` class disappears at end of step 5 (its only + surviving responsibility — the `NativeFunctionCallback` registration + — moves to `CopilotBridge`).** From 40912d2d18566c9263a80bc347dd383e69b350ab Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 11 May 2026 11:31:02 -0700 Subject: [PATCH 25/71] Move RunFirstAttachInit to runtime --- Integrations/Source/Runtime.cpp | 148 +++++++++++++++++++++++++++++ Integrations/Source/RuntimeImpl.h | 11 +++ Integrations/Source/View.cpp | 152 +----------------------------- 3 files changed, 160 insertions(+), 151 deletions(-) diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 16cf36d07..84f1dac49 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -2,9 +2,40 @@ #include +#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAMERA +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAPTURE +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEENCODING +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVETRACING +#include +#endif #if BABYLON_NATIVE_PLUGIN_SHADERCACHE #include #endif +#if BABYLON_NATIVE_PLUGIN_TESTUTILS +#include +#endif + +#include +#include +#include +#include +#include + +#if BABYLON_NATIVE_POLYFILL_WINDOW +#include +#endif #include #include @@ -12,6 +43,22 @@ namespace Babylon::Integrations { + namespace + { + // Forward Babylon Console levels to the LogLevel exposed on + // RuntimeOptions::log so consumers don't have to depend on the + // Console polyfill header to read log output. + LogLevel ToIntegrationsLogLevel(Babylon::Polyfills::Console::LogLevel level) + { + switch (level) + { + case Babylon::Polyfills::Console::LogLevel::Log: return LogLevel::Log; + case Babylon::Polyfills::Console::LogLevel::Warn: return LogLevel::Warn; + case Babylon::Polyfills::Console::LogLevel::Error: return LogLevel::Error; + } + return LogLevel::Log; + } + } RuntimeImpl::RuntimeImpl(RuntimeOptions options) : m_options{std::move(options)} { @@ -107,6 +154,107 @@ namespace Babylon::Integrations m_device.reset(); } + // --------------------------------------------------------------------- + // First-Attach engine initialization: dispatched onto the JS thread by + // the first View::Attach call. Runs all plugin/polyfill Initialize() + // calls in the same order as Apps/Playground/Shared/AppContext.cpp, + // then completes m_initTcs to unblock any LoadScript / Eval / + // RunOnJsThread calls the host queued before the first Attach. + // + // After m_initTcs is complete, subsequent host calls to + // Runtime::LoadScript / Eval / RunOnJsThread fire their continuation + // synchronously on the calling thread (via inline_scheduler), which + // then submits to ScriptLoader directly. + // --------------------------------------------------------------------- + void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window) + { + m_appRuntime->Dispatch([implPtr = this, window](Napi::Env env) { + // 1. Make the Device available to JS. + implPtr->m_device->AddToJavaScript(env); + + // 2. Polyfills (always-on). + Babylon::Polyfills::Blob::Initialize(env); + + { + const auto userLog = implPtr->m_options.log; + Babylon::Polyfills::Console::Initialize(env, + [userLog](const char* message, Babylon::Polyfills::Console::LogLevel level) { + if (userLog && message) + { + userLog(ToIntegrationsLogLevel(level), message); + } + }); + } + + Babylon::Polyfills::Performance::Initialize(env); + +#if BABYLON_NATIVE_POLYFILL_WINDOW + Babylon::Polyfills::Window::Initialize(env); +#endif + + Babylon::Polyfills::TextDecoder::Initialize(env); + Babylon::Polyfills::XMLHttpRequest::Initialize(env); + +#if BABYLON_NATIVE_POLYFILL_CANVAS + implPtr->m_canvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); +#endif + + // 3. Plugins. +#if BABYLON_NATIVE_PLUGIN_NATIVETRACING + Babylon::Plugins::NativeTracing::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEENCODING + Babylon::Plugins::NativeEncoding::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE + Babylon::Plugins::NativeEngine::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS + Babylon::Plugins::NativeOptimizations::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAPTURE + Babylon::Plugins::NativeCapture::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVECAMERA + Babylon::Plugins::NativeCamera::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + implPtr->m_input = &Babylon::Plugins::NativeInput::CreateForJavaScript(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // Initialize NativeXr; apply any pending xr window the host + // may have already supplied via Runtime::SetXrWindow; wire + // the session-state callback to keep m_isXrActive in sync. + { + std::lock_guard xrLock{implPtr->m_xrMutex}; + implPtr->m_nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(env)); + if (implPtr->m_xrWindow) + { + implPtr->m_nativeXr->UpdateWindow(implPtr->m_xrWindow); + } + implPtr->m_nativeXr->SetSessionStateChangedCallback( + [implPtr](bool isActive) { + implPtr->m_isXrActive.store(isActive, std::memory_order_relaxed); + }); + } +#endif +#if BABYLON_NATIVE_PLUGIN_TESTUTILS + Babylon::Plugins::TestUtils::Initialize(env, window); +#else + (void)window; +#endif + + // 4. Unblock any LoadScript / Eval / RunOnJsThread calls + // the host registered before first Attach. Each was + // chained off m_initTcs.as_task().then(inline_scheduler, + // ..., [...] { scriptLoader->...; });, so completing the + // TCS here causes those continuations to fire (in + // registration order) on the JS thread, each submitting + // to ScriptLoader's task chain. + implPtr->m_initTcs.complete(); + }); + } + std::unique_ptr Runtime::Create(RuntimeOptions options) { // Private ctor + manual unique_ptr because make_unique can't see it. diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 2d6b5198c..9d18e7dd0 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -99,6 +99,17 @@ namespace Babylon::Integrations // 0..1; tracked so we can guard against multiple concurrent // attachments (the API contract is "at most one View at a time"). View* m_currentView{nullptr}; + + // First-Attach engine initialization: dispatched onto the JS + // thread by View::Attach the very first time it constructs the + // Device. Runs all plugin/polyfill `Initialize` calls, wires + // NativeXr session-state callbacks, then completes `m_initTcs` + // to unblock any LoadScript / Eval / RunOnJsThread calls the + // host queued before the first Attach. + // + // The `window` parameter is forwarded to TestUtils::Initialize + // (the only plugin that wants it); ignored otherwise. + void RunFirstAttachInit(Babylon::Graphics::WindowT window); }; // Internal implementation of View. Holds the back-reference to the diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index e29049487..9bacc4373 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -1,163 +1,13 @@ #include "RuntimeImpl.h" -#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE -#include -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVECAMERA -#include -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVECAPTURE -#include -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEENCODING -#include -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS -#include -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVETRACING -#include -#endif #if BABYLON_NATIVE_PLUGIN_SHADERCACHE #include #endif -#if BABYLON_NATIVE_PLUGIN_TESTUTILS -#include -#endif - -#include -#include -#include -#include -#include - -#if BABYLON_NATIVE_POLYFILL_WINDOW -#include -#endif #include -#include namespace Babylon::Integrations { - namespace - { - // Forward Babylon Console levels to the LogLevel exposed on - // RuntimeOptions::log so consumers don't have to depend on the - // Console polyfill header to read log output. - LogLevel ToIntegrationsLogLevel(Babylon::Polyfills::Console::LogLevel level) - { - switch (level) - { - case Babylon::Polyfills::Console::LogLevel::Log: return LogLevel::Log; - case Babylon::Polyfills::Console::LogLevel::Warn: return LogLevel::Warn; - case Babylon::Polyfills::Console::LogLevel::Error: return LogLevel::Error; - } - return LogLevel::Log; - } - } - - // --------------------------------------------------------------------- - // First-Attach engine initialization: dispatched onto the JS thread by - // the first View::Attach call. Runs all plugin/polyfill Initialize() - // calls in the same order as Apps/Playground/Shared/AppContext.cpp, - // then completes m_initTcs to unblock any LoadScript / Eval / - // RunOnJsThread calls the host queued before the first Attach. - // - // After m_initTcs is complete, subsequent host calls to - // Runtime::LoadScript / Eval / RunOnJsThread fire their continuation - // synchronously on the calling thread (via inline_scheduler), which - // then submits to ScriptLoader directly. - // --------------------------------------------------------------------- - static void RunFirstAttachInit(RuntimeImpl& impl, Babylon::Graphics::WindowT window) - { - impl.m_appRuntime->Dispatch([implPtr = &impl, window](Napi::Env env) { - // 1. Make the Device available to JS. - implPtr->m_device->AddToJavaScript(env); - - // 2. Polyfills (always-on). - Babylon::Polyfills::Blob::Initialize(env); - - { - const auto userLog = implPtr->m_options.log; - Babylon::Polyfills::Console::Initialize(env, - [userLog](const char* message, Babylon::Polyfills::Console::LogLevel level) { - if (userLog && message) - { - userLog(ToIntegrationsLogLevel(level), message); - } - }); - } - - Babylon::Polyfills::Performance::Initialize(env); - -#if BABYLON_NATIVE_POLYFILL_WINDOW - Babylon::Polyfills::Window::Initialize(env); -#endif - - Babylon::Polyfills::TextDecoder::Initialize(env); - Babylon::Polyfills::XMLHttpRequest::Initialize(env); - -#if BABYLON_NATIVE_POLYFILL_CANVAS - implPtr->m_canvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); -#endif - - // 3. Plugins. -#if BABYLON_NATIVE_PLUGIN_NATIVETRACING - Babylon::Plugins::NativeTracing::Initialize(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEENCODING - Babylon::Plugins::NativeEncoding::Initialize(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE - Babylon::Plugins::NativeEngine::Initialize(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS - Babylon::Plugins::NativeOptimizations::Initialize(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVECAPTURE - Babylon::Plugins::NativeCapture::Initialize(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVECAMERA - Babylon::Plugins::NativeCamera::Initialize(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - implPtr->m_input = &Babylon::Plugins::NativeInput::CreateForJavaScript(env); -#endif -#if BABYLON_NATIVE_PLUGIN_NATIVEXR - // Initialize NativeXr; apply any pending xr window the host - // may have already supplied via Runtime::SetXrWindow; wire - // the session-state callback to keep m_isXrActive in sync. - { - std::lock_guard xrLock{implPtr->m_xrMutex}; - implPtr->m_nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(env)); - if (implPtr->m_xrWindow) - { - implPtr->m_nativeXr->UpdateWindow(implPtr->m_xrWindow); - } - implPtr->m_nativeXr->SetSessionStateChangedCallback( - [implPtr](bool isActive) { - implPtr->m_isXrActive.store(isActive, std::memory_order_relaxed); - }); - } -#endif -#if BABYLON_NATIVE_PLUGIN_TESTUTILS - Babylon::Plugins::TestUtils::Initialize(env, window); -#else - (void)window; -#endif - - // 4. Unblock any LoadScript / Eval / RunOnJsThread calls - // the host registered before first Attach. Each was - // chained off m_initTcs.as_task().then(inline_scheduler, - // ..., [...] { scriptLoader->...; });, so completing the - // TCS here causes those continuations to fire (in - // registration order) on the JS thread, each submitting - // to ScriptLoader's task chain. - implPtr->m_initTcs.complete(); - }); - } - // --------------------------------------------------------------------- // View::Attach (first time and subsequent) // --------------------------------------------------------------------- @@ -216,7 +66,7 @@ namespace Babylon::Integrations if (firstAttach) { - RunFirstAttachInit(impl, nativeWindow); + impl.RunFirstAttachInit(nativeWindow); } return view; From af7015cea733c80ba00d1b0ce3cef4c6d888ccd5 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 11 May 2026 14:12:23 -0700 Subject: [PATCH 26/71] Throw errors from interop layers for features that weren't compiled in Add ShaderCache support --- .../babylonjs/integrations/BabylonNative.java | 40 +++- .../main/cpp/BabylonNativeIntegrations.cpp | 187 +++++++++++++----- Integrations/Apple/Source/BNRuntime.mm | 29 ++- Integrations/Apple/Source/BNView.mm | 35 ++-- .../BabylonNativeIntegrations/BNRuntime.h | 32 ++- .../BabylonNativeIntegrations/BNView.h | 6 + .../Babylon/Integrations/RuntimeOptions.h | 13 ++ Integrations/Source/Runtime.cpp | 70 ++++++- Integrations/Source/RuntimeImpl.h | 26 +++ Integrations/Source/View.cpp | 8 - SimplifiedAPI.md | 24 ++- 11 files changed, 384 insertions(+), 86 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index 2d7a29787..d274886bd 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -62,6 +62,25 @@ public static native void requestPermissionsResult( /** Returns an opaque handle owned by the caller; release with {@link #runtimeDestroy(long)}. */ public static native long runtimeCreate(boolean enableDebugger); + /** + * Overload that wires up a persistent on-disk GPU shader cache. The + * cache is loaded on first {@link #viewAttach(long, Surface)} and + * saved on suspend and on {@link #runtimeDestroy(long)}. + * + *

Pass a writable path (typically + * {@code context.getCacheDir() + "/babylon.shadercache"}). Pass + * {@code null} to disable the on-disk cache (equivalent to the + * one-arg {@link #runtimeCreate(boolean)} overload). + * + *

If {@code shaderCachePath} is non-null but the native library + * was built without {@code BABYLON_NATIVE_PLUGIN_SHADERCACHE}, + * this method throws {@link IllegalStateException} so the + * misconfiguration surfaces at construction time rather than + * silently dropping the cache. Passing {@code null} is always + * safe regardless of native build config. + */ + public static native long runtimeCreate(boolean enableDebugger, String shaderCachePath); + public static native void runtimeDestroy(long handle); public static native void runtimeLoadScript(long handle, String url); @@ -74,10 +93,23 @@ public static native void requestPermissionsResult( // those once per state change and every Runtime in the process // reacts. Hosts needing finer-grained control should use the C++ API. - // Compiled into the native library only when BABYLON_NATIVE_PLUGIN_NATIVEXR - // is enabled. Calling these without that flag will produce an UnsatisfiedLinkError. + /** + * Set the platform Surface that XR will render into (typically a + * separate transparent SurfaceView overlay, distinct from the main + * View's surface). Pass {@code null} to clear the XR surface. + * + *

Throws {@link IllegalStateException} if invoked when the + * native library was built without + * {@code BABYLON_NATIVE_PLUGIN_NATIVEXR}. + */ public static native void runtimeSetXrSurface(long handle, Surface surface); + /** + * Returns whether an XR session is currently active. Returns + * {@code false} (never throws) when the native library was built + * without {@code BABYLON_NATIVE_PLUGIN_NATIVEXR} — no XR session + * can ever be active in that build. + */ public static native boolean runtimeIsXrActive(long handle); // ------------------------------------------------------------------- @@ -103,6 +135,10 @@ public static native void requestPermissionsResult( * (Android-native physical-pixel coordinates). The View internally * normalizes to logical (CSS) pixels — the unit Babylon.js's * {@code PointerEvent.clientX/clientY} pipeline expects. + * + *

Throws {@link IllegalStateException} if invoked when the + * native library was built without + * {@code BABYLON_NATIVE_PLUGIN_NATIVEINPUT}. */ public static native void viewPointerDown(long handle, int pointerId, float x, float y); diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 890e4b8ab..f9f9470bb 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -95,6 +95,68 @@ namespace env->ReleaseStringUTFChars(jstr, utf); return result; } + + // Throws a Java `IllegalStateException` with the given message. + // Used by interop entry points whose underlying plugin was not + // compiled into the native library, so the misconfiguration + // surfaces as a clean Java exception instead of an + // UnsatisfiedLinkError or silent no-op. + // + // `[[maybe_unused]]` is intentional: when every plugin flag is + // enabled, no `#else` branch in any JNI entry point invokes this + // helper, and -Werror=unused-function would otherwise fail the + // build. + [[maybe_unused]] void ThrowPluginNotEnabled(JNIEnv* env, const char* message) + { + jclass excClass = env->FindClass("java/lang/IllegalStateException"); + if (excClass != nullptr) + { + env->ThrowNew(excClass, message); + } + } + + // Shared body for the `runtimeCreate` JNI overloads. Wires up the + // Android-default logcat log sink, constructs the Runtime, attaches + // the Activity-lifecycle auto-Suspend/Resume tickets, and returns + // the opaque jlong handle the JVM side holds onto. + jlong MakeRuntimeHandle(RuntimeOptions options) + { + options.log = [](LogLevel level, std::string_view message) { + // logcat takes a NUL-terminated C string; copy the view. + std::string text{message}; + __android_log_write(LogPriorityFor(level), "BabylonNative", text.c_str()); + }; + + // Construct in two phases because the AppStateChangedCallbackTicket + // is neither default-constructible nor move-assignable: we need the + // Runtime pointer in hand before we can register the callbacks, and + // we register the callbacks before the wrapper itself exists. + auto runtime = Runtime::Create(std::move(options)); + Runtime* runtimePtr = runtime.get(); + + // Auto-Suspend/Resume on Activity lifecycle. Hosts call + // pause / resume from their Activity's onPause / onResume; + // every Runtime in the process gets suspended and resumed + // automatically. Since Runtime::Suspend/Resume are refcounted, + // this composes safely with any explicit runtimeSuspend / + // runtimeResume calls the host might make for finer-grained + // reasons (e.g. modal dialogs). + auto pauseTicket = android::global::AddPauseCallback([runtimePtr]() { + runtimePtr->Suspend(); + }); + auto resumeTicket = android::global::AddResumeCallback([runtimePtr]() { + runtimePtr->Resume(); + }); + + auto wrapper = std::unique_ptr{new AndroidRuntime{ + std::move(runtime), + std::move(pauseTicket), + std::move(resumeTicket), + }}; + + // Ownership transfers to the JVM side via the returned jlong. + return reinterpret_cast(wrapper.release()); + } } // Public handle-decoding entry point. Hosts that ship app-specific JNI @@ -194,49 +256,50 @@ Java_com_babylonjs_integrations_BabylonNative_requestPermissionsResult( // ===================================================================== JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeCreate(JNIEnv*, jclass, jboolean enableDebugger) +Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Z(JNIEnv*, jclass, jboolean enableDebugger) { // Default Android consumers want logcat output; route Console // polyfill / DebugTrace / uncaught JS exceptions there. Hosts // that need different behavior can construct a Runtime in C++ // directly with their own RuntimeOptions. + // + // Mangled name `__Z` (boolean) is the JNI long form, required + // because Java declares `runtimeCreate` as an overloaded native + // method (see the SHADERCACHE-gated overload below). RuntimeOptions options{}; options.enableDebugger = (enableDebugger == JNI_TRUE); - options.log = [](LogLevel level, std::string_view message) { - // logcat takes a NUL-terminated C string; copy the view. - std::string text{message}; - __android_log_write(LogPriorityFor(level), "BabylonNative", text.c_str()); - }; + return MakeRuntimeHandle(std::move(options)); +} - // Construct in two phases because the AppStateChangedCallbackTicket - // is neither default-constructible nor move-assignable: we need the - // Runtime pointer in hand before we can register the callbacks, and - // we register the callbacks before the wrapper itself exists. - auto runtime = Runtime::Create(std::move(options)); - Runtime* runtimePtr = runtime.get(); - - // Auto-Suspend/Resume on Activity lifecycle. Hosts call - // androidGlobalPause / androidGlobalResume from their Activity's - // onPause / onResume; every Runtime in the process gets suspended - // and resumed automatically. Since Runtime::Suspend/Resume are - // refcounted, this composes safely with any explicit - // runtimeSuspend / runtimeResume calls the host might make for - // finer-grained reasons (e.g. modal dialogs). - auto pauseTicket = android::global::AddPauseCallback([runtimePtr]() { - runtimePtr->Suspend(); - }); - auto resumeTicket = android::global::AddResumeCallback([runtimePtr]() { - runtimePtr->Resume(); - }); - - auto wrapper = std::unique_ptr{new AndroidRuntime{ - std::move(runtime), - std::move(pauseTicket), - std::move(resumeTicket), - }}; - - // Ownership transfers to the JVM side via the returned jlong. - return reinterpret_cast(wrapper.release()); +JNIEXPORT jlong JNICALL +Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__ZLjava_lang_String_2( + JNIEnv* env, jclass, jboolean enableDebugger, jstring shaderCachePath) +{ + // Overload that wires up `RuntimeOptions::shaderCachePath`. + // The JNI symbol is always exported so the Java surface is stable + // across native build configurations; if the caller passes a + // non-null path but `BABYLON_NATIVE_PLUGIN_SHADERCACHE` was not + // enabled at native build time, we throw `IllegalStateException` + // (fail loud, vs. silently dropping the path). + RuntimeOptions options{}; + options.enableDebugger = (enableDebugger == JNI_TRUE); + if (shaderCachePath != nullptr) + { +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + const char* utf = env->GetStringUTFChars(shaderCachePath, nullptr); + if (utf != nullptr) + { + options.shaderCachePath = utf; + env->ReleaseStringUTFChars(shaderCachePath, utf); + } +#else + ThrowPluginNotEnabled(env, + "shaderCachePath was provided but BABYLON_NATIVE_PLUGIN_SHADERCACHE " + "was not enabled at native build time."); + return 0; +#endif + } + return MakeRuntimeHandle(std::move(options)); } JNIEXPORT void JNICALL @@ -270,28 +333,40 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeEval( // change; every Runtime in the process reacts. Hosts that need // finer-grained control should use the C++ API directly. -#if BABYLON_NATIVE_PLUGIN_NATIVEXR - JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeSetXrSurface( JNIEnv* env, jclass, jlong handle, jobject surface) { +#if BABYLON_NATIVE_PLUGIN_NATIVEXR ANativeWindow* window{nullptr}; if (surface != nullptr) { window = ANativeWindow_fromSurface(env, surface); } AsRuntime(handle)->SetXrWindow(window); +#else + (void)handle; + (void)surface; + ThrowPluginNotEnabled(env, + "runtimeSetXrSurface was called but BABYLON_NATIVE_PLUGIN_NATIVEXR " + "was not enabled at native build time."); +#endif } JNIEXPORT jboolean JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, jlong handle) { +#if BABYLON_NATIVE_PLUGIN_NATIVEXR return AsRuntime(handle)->IsXrActive() ? JNI_TRUE : JNI_FALSE; +#else + // State query: "no XR session is active" is the correct answer + // when XR isn't compiled in, so this is intentionally a + // non-throwing path. + (void)handle; + return JNI_FALSE; +#endif } -#endif // BABYLON_NATIVE_PLUGIN_NATIVEXR - // ===================================================================== // View // ===================================================================== @@ -342,29 +417,49 @@ Java_com_babylonjs_integrations_BabylonNative_viewResize( static_cast(height)); } -#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) + JNIEnv* env, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + (void)env; AsView(handle)->OnPointerDown(static_cast(pointerId), x, y); +#else + (void)handle; (void)pointerId; (void)x; (void)y; + ThrowPluginNotEnabled(env, + "viewPointerDown was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT " + "was not enabled at native build time."); +#endif } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) + JNIEnv* env, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + (void)env; AsView(handle)->OnPointerMove(static_cast(pointerId), x, y); +#else + (void)handle; (void)pointerId; (void)x; (void)y; + ThrowPluginNotEnabled(env, + "viewPointerMove was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT " + "was not enabled at native build time."); +#endif } JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewPointerUp( - JNIEnv*, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) + JNIEnv* env, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + (void)env; AsView(handle)->OnPointerUp(static_cast(pointerId), x, y); -} - +#else + (void)handle; (void)pointerId; (void)x; (void)y; + ThrowPluginNotEnabled(env, + "viewPointerUp was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT " + "was not enabled at native build time."); #endif +} } // extern "C" diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index 7f3be9719..d880b1dd3 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -16,10 +16,16 @@ @implementation BNRuntime - (instancetype)init { - return [self initWithEnableDebugger:NO]; + return [self initWithEnableDebugger:NO shaderCachePath:nil]; } - (instancetype)initWithEnableDebugger:(BOOL)enableDebugger +{ + return [self initWithEnableDebugger:enableDebugger shaderCachePath:nil]; +} + +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger + shaderCachePath:(nullable NSString*)shaderCachePath { if ((self = [super init])) { @@ -30,6 +36,21 @@ - (instancetype)initWithEnableDebugger:(BOOL)enableDebugger options.log = [](Babylon::Integrations::LogLevel /*level*/, std::string_view message) { NSLog(@"%.*s", static_cast(message.size()), message.data()); }; + if (shaderCachePath != nil) + { +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + options.shaderCachePath = shaderCachePath.UTF8String; +#else + // Caller explicitly asked for shader caching but the + // plugin wasn't compiled in. Fail loudly rather than + // silently dropping the cache on the floor (which would + // be hard to diagnose at runtime). + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"shaderCachePath was provided but BABYLON_NATIVE_PLUGIN_SHADERCACHE was not enabled at native build time." + userInfo:nil]; +#endif + } _runtime = Babylon::Integrations::Runtime::Create(std::move(options)); } return self; @@ -77,6 +98,10 @@ - (void)setXrView:(MTKView*)xrView _runtime->SetXrWindow((__bridge void*)xrView); #else (void)xrView; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"setXrView: was called but BABYLON_NATIVE_PLUGIN_NATIVEXR was not enabled at native build time." + userInfo:nil]; #endif } @@ -85,6 +110,8 @@ - (BOOL)isXRActive #if BABYLON_NATIVE_PLUGIN_NATIVEXR return _runtime->IsXrActive() ? YES : NO; #else + // State query: "no XR session is active" is the correct answer when + // XR isn't compiled in, so this is intentionally a non-throwing path. return NO; #endif } diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index b6e5f4466..b22509c9e 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -121,47 +121,58 @@ - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height #pragma mark - Pointer forwarding -#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT if (_view) { _view->OnPointerDown(static_cast(pointerId), static_cast(x), static_cast(y)); } +#else + (void)pointerId; (void)x; (void)y; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"pointerDown:atX:y: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; +#endif } - (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT if (_view) { _view->OnPointerMove(static_cast(pointerId), static_cast(x), static_cast(y)); } +#else + (void)pointerId; (void)x; (void)y; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"pointerMove:atX:y: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; +#endif } - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT if (_view) { _view->OnPointerUp(static_cast(pointerId), static_cast(x), static_cast(y)); } -} - #else - -// When NATIVEINPUT is disabled at native build time, the methods are -// still declared on the public BNView header for binary stability; -// they become no-ops here. -- (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { (void)pointerId; (void)x; (void)y; } -- (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { (void)pointerId; (void)x; (void)y; } -- (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { (void)pointerId; (void)x; (void)y; } - + (void)pointerId; (void)x; (void)y; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"pointerUp:atX:y: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; #endif +} @end diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h index ef900d22b..7ce2dfb2a 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h @@ -23,7 +23,27 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init; /// Same as `init` but lets the host opt into the JS debugger. -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger; + +/// Same as `initWithEnableDebugger:` but also wires up a persistent +/// on-disk GPU shader cache. Pass a writable file path (typically +/// inside `NSCachesDirectory`, e.g. +/// `[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"babylon.shadercache"]`). +/// Pass `nil` to disable the on-disk cache (equivalent to the +/// `initWithEnableDebugger:` overload). +/// +/// The cache is loaded on first `BNView` attach and saved on `suspend` +/// and on deallocation. +/// +/// If `shaderCachePath` is non-`nil` but the native library was built +/// without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, this method raises an +/// `NSException` (name +/// `BabylonNativePluginNotEnabledException`) so the misconfiguration +/// surfaces at construction time rather than silently dropping the +/// cache. Passing `nil` is always safe regardless of build config. +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger + shaderCachePath:(nullable NSString*)shaderCachePath + NS_DESIGNATED_INITIALIZER; /// Load a script from a URL onto the JS thread. Calls made before /// the first `BNView` is created are queued internally and dispatched @@ -54,13 +74,17 @@ NS_ASSUME_NONNULL_BEGIN /// call before the first `BNView` attach; the value is applied when /// NativeXr finishes initializing during that first attach. /// -/// Compiled-in only when `BABYLON_NATIVE_PLUGIN_NATIVEXR` is enabled -/// at native build time; otherwise this is a no-op. +/// Raises an `NSException` (name +/// `BabylonNativePluginNotEnabledException`) if invoked when +/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` was not enabled at native build +/// time. - (void)setXrView:(nullable MTKView*)xrView; /// `YES` while an XR session is active. Updated from the JS thread /// by NativeXr's internal session-state callback; safe to poll from -/// any thread. +/// any thread. Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` was +/// not enabled at native build time (no XR session can ever be active +/// in that build). @property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; @end diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h index 339a671c3..b8b43de27 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h +++ b/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h @@ -85,6 +85,12 @@ NS_ASSUME_NONNULL_BEGIN /// Forward a pointer-down event. `x`, `y` are in logical (CSS) pixels /// — pass `UITouch.location(in:)` (UIKit) or `NSEvent.locationInWindow` /// (AppKit) coordinates through unchanged. +/// +/// Raises an `NSException` (name +/// `BabylonNativePluginNotEnabledException`) if invoked when +/// `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` was not enabled at native +/// build time. The same applies to `pointerMove:atX:y:` and +/// `pointerUp:atX:y:` below. - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(pointerDown(id:x:y:)); diff --git a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Babylon/Integrations/RuntimeOptions.h index 327fad99b..4dc5a6acb 100644 --- a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Babylon/Integrations/RuntimeOptions.h @@ -38,5 +38,18 @@ namespace Babylon::Integrations // // if (level == LogLevel::Fatal) std::quick_exit(1); std::function log; + +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + // Optional path for persisting the GPU shader cache across + // sessions. If non-empty: + // - Loaded synchronously during the first `View::Attach` (after + // `ShaderCache::Enable`). Missing or unreadable file: ignored. + // - Saved asynchronously during `Runtime::Suspend` (queued onto + // the JS thread before the suspension blocker) so the + // on-disk cache reflects any shaders compiled this session. + // - Saved synchronously during `~Runtime` so a final write + // happens before the JS thread is torn down. + std::string shaderCachePath; +#endif }; } diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 84f1dac49..6d740f989 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -38,6 +38,7 @@ #endif #include +#include #include #include @@ -109,21 +110,29 @@ namespace Babylon::Integrations assert(m_currentView == nullptr && "View must be destroyed before its Runtime."); // Order matters here: - // 1. ScriptLoader's dispatcher captures &m_appRuntime, so + // 1. Persist the shader cache. The View precondition above + // guarantees `ViewImpl::Suspend()` already ran via + // `~View`, so the engine is quiescent and a host-thread + // Save is race-free. + // 2. ScriptLoader's dispatcher captures &m_appRuntime, so // ~ScriptLoader must run before ~AppRuntime. - // 2. The Canvas polyfill and NativeInput pointer are referenced + // 3. The Canvas polyfill and NativeInput pointer are referenced // from JS-thread state; clear them before joining the JS // thread, but only after ScriptLoader has drained. - // 3. ~AppRuntime joins the JS thread. - // 4. ShaderCache::Disable() balances the Enable() that - // View::Attach calls on first attach. - // 5. Device + DeviceUpdate destroyed last because the JS + // 4. ~AppRuntime joins the JS thread. + // 5. ShaderCache::Disable() balances the Enable() that + // RunFirstAttachInit calls on first attach. + // 6. Device + DeviceUpdate destroyed last because the JS // thread referenced them via Device::AddToJavaScript. // // m_initTcs is destroyed when this struct's members are // destroyed. If complete() was never called (no View ever // attached), the queued continuations are dropped without // firing, which is the desired behavior on shutdown. +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + SaveShaderCache(); +#endif + m_scriptLoader.reset(); #if BABYLON_NATIVE_POLYFILL_CANVAS @@ -168,6 +177,16 @@ namespace Babylon::Integrations // --------------------------------------------------------------------- void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window) { +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + // Enable the process-wide shader cache singleton and hydrate + // it from disk before any JS-thread shader compilation can + // begin. Both calls are host-thread safe (the singleton is + // not yet referenced by NativeEngine, which gets initialized + // on the JS thread below). + Babylon::Plugins::ShaderCache::Enable(); + LoadShaderCache(); +#endif + m_appRuntime->Dispatch([implPtr = this, window](Napi::Env env) { // 1. Make the Device available to JS. implPtr->m_device->AddToJavaScript(env); @@ -255,6 +274,38 @@ namespace Babylon::Integrations }); } + // --------------------------------------------------------------------- + // Persistent shader cache (no-ops when `shaderCachePath` is empty). + // --------------------------------------------------------------------- +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + void RuntimeImpl::LoadShaderCache() + { + if (m_options.shaderCachePath.empty() || !Babylon::Plugins::ShaderCache::IsEnabled()) + { + return; + } + std::ifstream stream{m_options.shaderCachePath, std::ios::binary}; + if (stream.good()) + { + Babylon::Plugins::ShaderCache::Load(stream); + } + // Missing or unreadable file is fine — we just start with an empty cache. + } + + void RuntimeImpl::SaveShaderCache() + { + if (m_options.shaderCachePath.empty() || !Babylon::Plugins::ShaderCache::IsEnabled()) + { + return; + } + std::ofstream stream{m_options.shaderCachePath, std::ios::binary}; + if (stream.good()) + { + Babylon::Plugins::ShaderCache::Save(stream); + } + } +#endif + std::unique_ptr Runtime::Create(RuntimeOptions options) { // Private ctor + manual unique_ptr because make_unique can't see it. @@ -313,6 +364,13 @@ namespace Babylon::Integrations { m_impl->m_currentView->m_impl->Suspend(); } +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + // Persist the shader cache while the engine is known + // quiescent: the view's Suspend just closed the current + // frame and locked the update safe-timespan, so no engine + // shader compilation can be in flight. + m_impl->SaveShaderCache(); +#endif m_impl->m_appRuntime->Suspend(); } } diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 9d18e7dd0..b6ae01145 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -110,6 +110,32 @@ namespace Babylon::Integrations // The `window` parameter is forwarded to TestUtils::Initialize // (the only plugin that wants it); ignored otherwise. void RunFirstAttachInit(Babylon::Graphics::WindowT window); + +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + // ----- Persistent shader cache ----- + // + // Both methods are no-ops when `m_options.shaderCachePath` is + // empty. Both run synchronously on the host thread; they do + // not need to coordinate with the JS thread because callers + // (first-Attach, post-view-Suspend, destructor) are points at + // which the engine is known not to be compiling shaders. + + // Load the on-disk shader cache file into the in-memory cache. + // Called from `RunFirstAttachInit` right after + // `ShaderCache::Enable()`. Safe because no shaders have been + // compiled yet at this point — the cache map is quiescent. + void LoadShaderCache(); + + // Serialize the in-memory shader cache to disk. Called from + // `Runtime::Suspend` (after `ViewImpl::Suspend()` has closed + // the current frame and locked the update safe-timespan) and + // from `~RuntimeImpl` (after the View precondition has + // guaranteed `ViewImpl::Suspend()` already ran via `~View`). + // No async/JS-thread coordination is required: at these + // points there is no in-flight engine work writing to the + // cache. + void SaveShaderCache(); +#endif }; // Internal implementation of View. Holds the back-reference to the diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 9bacc4373..e39a3f4fd 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -1,9 +1,5 @@ #include "RuntimeImpl.h" -#if BABYLON_NATIVE_PLUGIN_SHADERCACHE -#include -#endif - #include namespace Babylon::Integrations @@ -40,10 +36,6 @@ namespace Babylon::Integrations impl.m_device.emplace(config); impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); - -#if BABYLON_NATIVE_PLUGIN_SHADERCACHE - Babylon::Plugins::ShaderCache::Enable(); -#endif } else { diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index 97ee48341..052fd21d1 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -155,6 +155,12 @@ namespace Babylon::Integrations std::function log; std::function onUnhandledError; + + // If non-empty, the GPU shader cache is loaded from this path + // on first `View::Attach` and saved back on `Suspend` and + // `~Runtime`. Pass a per-app writable directory file (e.g. + // Android `Context.getCacheDir()/babylon.shadercache`). + std::string shaderCachePath; }; // Long-lived: typically created once per app/process. Sets up the @@ -1422,13 +1428,17 @@ manual and automatic paths compose cleanly. detach-mid-frame, and swap to a different surface mid-app. Reference: existing start/finish discipline in `Tests.ExternalTexture.D3D11.cpp:24-69`. -- **Shader cache directory.** `Babylon::Plugins::ShaderCache::Load/Save` - needs an OS-mandated cache path that varies per platform (Android - `Context.getCacheDir()`, iOS `NSCachesDirectory`, etc.). Add - `RuntimeOptions::shaderCachePath: std::optional` and - let the Integrations layer auto-load on `Create` and auto-save on - `Suspend` and `~Runtime`. Reference: bridge plumbing in - `BabylonNativeBridge.mm:88-106` and `babylon.cpp:242-260,378-398`. +- **Shader cache directory.** *(Implemented.)* + `Babylon::Plugins::ShaderCache::Load/Save` is wired through + `RuntimeOptions::shaderCachePath` (`std::string`, empty = disabled). + The Integrations layer auto-loads on the first `View::Attach` (after + `ShaderCache::Enable`), auto-saves asynchronously on `Suspend` + (dispatched onto the JS thread before the suspension blocker takes + effect), and saves synchronously in `~Runtime`. Hosts only need to + pass the platform-appropriate cache path (Android + `Context.getCacheDir()`, iOS `NSCachesDirectory`, etc.). Reference: + bridge plumbing in `BabylonNativeBridge.mm:88-106` and + `babylon.cpp:242-260,378-398`. - **JS ↔ native messaging.** Both bridges add a custom Napi `ObjectWrap` (`LumiInterop`) exposing `callNative(jsonString)` and `notifyReady()` to JS, plus a way to push results back to the host From d22f40b4b8d37c10d439ee3baa379d23863c6fe3 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 11 May 2026 14:13:35 -0700 Subject: [PATCH 27/71] Add missed changes to md --- SimplifiedAPI.md | 218 ++++++++++++++++++++++++----------------------- 1 file changed, 112 insertions(+), 106 deletions(-) diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md index 97ee48341..6534a1b18 100644 --- a/SimplifiedAPI.md +++ b/SimplifiedAPI.md @@ -1052,111 +1052,117 @@ class MainActivity : AppCompatActivity() { **Library-supplied Obj-C++ interop** (lives in `Integrations/Apple/`): -The interop layer takes a host-supplied `MTKView` directly. It installs -itself as the view's `MTKViewDelegate` and drives the per-frame render -and resize callbacks internally — the Swift host doesn't have to wire -anything up beyond constructing the `BNView`. +The interop layer takes a host-supplied `MTKView` directly. `BNView` +forces a layout pass and seeds `CAMetalLayer.drawableSize` from +`view.bounds × scale` before handing the layer to the Graphics layer +— hosts don't have to think about MTKView's lazy `autoResizeDrawable` +semantics. + +`BNView` exposes `BNViewDelegate` as a public Obj-C class. The default +behavior is "smart": if the host hasn't set `MTKView.delegate` by the +time `BNView` is constructed, `BNView` creates and strongly retains a +`BNViewDelegate` for the host. If the host has already set their own +delegate (typically a `BNViewDelegate` subclass), `BNView` leaves it +alone. ```objc // BNRuntime.h — public Obj-C header (Swift sees this via the bridge) @interface BNRuntime : NSObject - (instancetype)init; - (void)loadScript:(NSString*)url; +- (void)setXrView:(nullable MTKView*)xrView; // runtime owns visibility toggling - (void)suspend; - (void)resume; @end @interface BNView : NSObject -// "Super simple" mode: BNView installs itself as the MTKView's delegate -// and drives draw/resize internally — no MTKViewDelegate boilerplate -// in host code. -- (instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view; - -// Explicit-mode init. With autoDelegate = NO, the host keeps ownership -// of the MTKView's delegate and calls -renderFrame and -// -resizeWithWidth:height: from its own MTKViewDelegate methods. Use -// this when you need to interleave host work with the runtime's -// per-frame render. -- (instancetype)initWithRuntime:(BNRuntime*)rt - view:(MTKView*)view - autoDelegate:(BOOL)autoDelegate; - -- (void)renderFrame; // manual mode only -- (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h; // manual mode only +// If `view.delegate` is nil at construction time, BNView lazily +// installs and retains a BNViewDelegate that drives the per-frame +// render. If the host pre-installed a delegate, BNView leaves it +// alone. +- (nullable instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view; + +// Used by `BNViewDelegate` (or by hosts using a fully manual +// `MTKViewDelegate`) — usually you don't call these yourself. +- (void)renderFrame; +- (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h NS_SWIFT_NAME(resize(width:height:)); @end -``` -```objc++ -// BNRuntime.mm — implementation -@implementation BNRuntime { - std::unique_ptr _rt; -} -- (instancetype)init { - if ((self = [super init])) { - _rt = Babylon::Integrations::Runtime::Create(); - } - return self; -} -- (void)loadScript:(NSString*)url { _rt->LoadScript(url.UTF8String); } -- (void)suspend { _rt->Suspend(); } -- (void)resume { _rt->Resume(); } -- (Babylon::Integrations::Runtime*)native { return _rt.get(); } -@end - -@interface BNView () +// Default MTKViewDelegate implementation. Subclass to insert per-frame +// work; call super to keep the default forwarding behavior. +@interface BNViewDelegate : NSObject +- (nullable instancetype)initWithView:(BNView*)view NS_DESIGNATED_INITIALIZER; @end +``` +```objc++ +// BNView.mm — implementation sketch @implementation BNView { std::unique_ptr _v; + BNRuntime* _runtime; MTKView* _mtkView; - BOOL _autoDelegate; + BNViewDelegate* _managedDelegate; // strong; nil if host set their own } - (instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view { - return [self initWithRuntime:rt view:view autoDelegate:YES]; -} -- (instancetype)initWithRuntime:(BNRuntime*)rt - view:(MTKView*)view - autoDelegate:(BOOL)autoDelegate { if ((self = [super init])) { + _runtime = rt; _mtkView = view; - _autoDelegate = autoDelegate; - CAMetalLayer* layer = (CAMetalLayer*)view.layer; // Force layout so bounds are valid, then seed drawableSize. - // MTKView's autoResizeDrawable keeps it in sync from here on, - // but it can still be (0, 0) at attach time. + // MTKView's autoResizeDrawable keeps it in sync from here on. [view layoutIfNeeded]; - const CGFloat scale = view.contentScaleFactor; // macOS: window.backingScaleFactor - layer.drawableSize = CGSizeMake(view.bounds.size.width * scale, - view.bounds.size.height * scale); + const CGFloat scale = view.contentScaleFactor; + ((CAMetalLayer*)view.layer).drawableSize = + CGSizeMake(view.bounds.size.width * scale, + view.bounds.size.height * scale); - // First Attach on this runtime triggers Device construction + - // GPU plugin init + queued LoadScript flush. _v = Babylon::Integrations::View::Attach(*[rt native], - (__bridge CA::MetalLayer*)layer); - if (autoDelegate) view.delegate = self; // install AFTER Attach + (__bridge CA::MetalLayer*)view.layer); + + // Auto-install a default delegate iff the host hasn't. + if (view.delegate == nil) { + _managedDelegate = [[BNViewDelegate alloc] initWithView:self]; + view.delegate = _managedDelegate; + } } return self; } - (void)dealloc { - if (_autoDelegate && _mtkView.delegate == self) { _mtkView.delegate = nil; } + if (_managedDelegate != nil && _mtkView.delegate == _managedDelegate) { + _mtkView.delegate = nil; + } +} +- (void)renderFrame { + [_runtime updateXrViewIfNeeded]; // default xrView visibility policy + _v->RenderFrame(); } -- (void)renderFrame { _v->RenderFrame(); } - (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h { _v->Resize((uint32_t)w, (uint32_t)h); } +@end + +// BNViewDelegate.mm — default MTKViewDelegate +@implementation BNViewDelegate { __weak BNView* _view; } +- (instancetype)initWithView:(BNView*)view { + if ((self = [super init])) { _view = view; } + return self; +} - (void)mtkView:(MTKView*)v drawableSizeWillChange:(CGSize)size { - [self resizeWithWidth:(NSUInteger)size.width height:(NSUInteger)size.height]; + [_view resizeWithWidth:(NSUInteger)size.width height:(NSUInteger)size.height]; +} +- (void)drawInMTKView:(MTKView*)v { + [_view renderFrame]; // BNView's renderFrame handles XR overlay } -- (void)drawInMTKView:(MTKView*)v { [self renderFrame]; } @end ``` -**Host code — "super simple" integration** (consumer's app — *not* shipped by the library): +**Host code — "super simple" integration** (the iOS Playground uses this): The minimal host creates the runtime at app start, loads scripts -(queued), then constructs a `BNView` against the user-visible MTKView -and is done — BNView drives the per-frame render and resize itself. +(queued), and constructs a `BNView` against the user-visible MTKView. +That's it — because `mtkView.delegate` is `nil`, BNView auto-installs +a `BNViewDelegate` that drives the per-frame render and resize, +including keeping the XR overlay's visibility in sync. ```swift class AppDelegate: NSObject, UIApplicationDelegate { @@ -1164,6 +1170,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { let r = BNRuntime() r.loadScript(Bundle.main.url(forResource: "experience", withExtension: "js")!.absoluteString) + // LoadScript queues; flushes on first BNView attach. return r }() func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } @@ -1175,67 +1182,65 @@ class BabylonViewController: UIViewController { override func viewDidLoad() { let mtkView = view as! MTKView let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime - babylonView = BNView(runtime: runtime, view: mtkView) // that's it + // First Attach: triggers Device + plugin init + queued script flush. + // BNView auto-installs a BNViewDelegate since mtkView.delegate is nil. + babylonView = BNView(runtime: runtime, view: mtkView) } } ``` -**Host code — "simple" integration with manual delegate** (host needs to interleave per-frame work): +**Host code — customize per-frame work via subclass:** -When the host wants to do its own work each frame (e.g. toggling an -overlay's visibility based on runtime state), pass `autoDelegate: false` -and own the `MTKViewDelegate` yourself. Forward `draw(in:)` and -`drawableSizeWillChange:` to `BNView`'s `renderFrame` / `resize`. +When the host wants to do per-frame work (e.g. updating an overlay +based on runtime state), subclass `BNViewDelegate`, override the +delegate methods, call `super` to keep the default behavior, and +install the subclass on the MTKView **before** constructing BNView so +the auto-install path doesn't fire. ```swift -class BabylonViewController: UIViewController, MTKViewDelegate { - var babylonView: BNView? - var overlay: UIView! - let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime - - override func viewDidLoad() { - let mtkView = view as! MTKView - mtkView.delegate = self - babylonView = BNView(runtime: runtime, view: mtkView, autoDelegate: false) +class MyDelegate: BNViewDelegate { + override func draw(in v: MTKView) { + beforeWork() + super.draw(in: v) // forwards to bnView.renderFrame (xrView toggle + render) + afterWork() } - - func draw(in view: MTKView) { - overlay.isHidden = !runtime.isOverlayActive // host's per-frame work - babylonView?.renderFrame() - } - func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { - babylonView?.resize(withWidth: UInt(size.width), height: UInt(size.height)) + override func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { + super.mtkView(v, drawableSizeWillChange: size) // forwards to bnView.resize + layoutOverlays(size) } } + +// In the view controller: +// 1. Construct BNView so it produces a BNView reference for the subclass. +// 2. Install the subclass; this overwrites any auto-installed default. +let bn = BNView(runtime: runtime, view: mtkView)! +let delegate = MyDelegate(view: bn)! // host retains; MTKView.delegate is weak +mtkView.delegate = delegate +self.viewDelegate = delegate +self.babylonView = bn ``` -layer in `viewDidLoad`. -```swift -// AppDelegate — runtime created once at app start. -class AppDelegate: NSObject, UIApplicationDelegate { - let runtime: BNRuntime = { - let r = BNRuntime() - r.loadScript(Bundle.main.url(forResource: "experience", - withExtension: "js")!.absoluteString) - // LoadScript queues; flushes on first BNView attach. - return r - }() +**Host code — full manual `MTKViewDelegate`:** - func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } - func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } -} +Hosts that need full control can skip `BNViewDelegate` entirely, write +their own delegate, and call `BNView`'s `renderFrame` / `resize` +directly. The XR overlay visibility toggle is still automatic because +`BNView.renderFrame` calls `[runtime updateXrViewIfNeeded]` itself. -class BabylonViewController: UIViewController { +```swift +class BabylonViewController: UIViewController, MTKViewDelegate { var babylonView: BNView? override func viewDidLoad() { let mtkView = view as! MTKView - let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime - // First Attach: triggers Device + plugin init + queued script flush. - // BNView installs itself as the MTKView's delegate and drives - // per-frame render/resize internally. + mtkView.delegate = self // BNView will see this is set and not touch it babylonView = BNView(runtime: runtime, view: mtkView) } + + func draw(in v: MTKView) { babylonView?.renderFrame() } + func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { + babylonView?.resize(width: UInt(size.width), height: UInt(size.height)) + } } ``` @@ -1277,8 +1282,9 @@ class AppDelegate: NSObject, UIApplicationDelegate { // ... later, in the view controller: // delegate.releasePrewarm() // babylonView = BNView(runtime: delegate.runtime, view: mtkView) -// // Device::UpdateWindow swaps to the user-visible surface; scene -// // state and JS state are preserved. +// // BNView auto-installs a BNViewDelegate on the user-visible +// // MTKView; Device::UpdateWindow swaps the underlying surface; +// // scene state and JS state are preserved. ``` ### Suspend/Resume is reference-counted From 659339d2be0e50f101acd9a00b6b9aab6da0bac0 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 11 May 2026 14:18:08 -0700 Subject: [PATCH 28/71] Remove accidentally added file --- copilot-mobile.md | 490 ---------------------------------------------- 1 file changed, 490 deletions(-) delete mode 100644 copilot-mobile.md diff --git a/copilot-mobile.md b/copilot-mobile.md deleted file mode 100644 index 2235cdac1..000000000 --- a/copilot-mobile.md +++ /dev/null @@ -1,490 +0,0 @@ -# Copilot Mobile Migration to `Babylon::Integrations` - -> Plan for replacing the Copilot Mobile codebase's bespoke Babylon Native -> JNI bridge with the new `Babylon::Integrations` interop layer -> (`Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp` etc. -> in this repo). -> -> This document covers the **Android** migration. iOS will follow the -> same shape using `Integrations/Apple/`. - ---- - -## 1. What the Copilot Mobile stack looks like today - -Three repositories cooperate: - -``` -copilot-appearance ─► copilot-appearance.js (the JS bundle) - ▲ loaded via "file:///..." app:/// URL -babylon-native-bridge ─► libBabylonNativeJNI.so + Java glue (per-app JNI) - ▲ depends on - BabylonNative ─► AppRuntime, Device, plugins, polyfills (this repo) - ▲ consumed by -app-android ─► Compose / Kotlin host (the app) -``` - -### Native side (`babylon-native-bridge/android/src/main/cpp/babylon.cpp`) - -Single 850-line cpp file. Owns these process-globals (under one -`engineMutex`): - -```cpp -std::optional device; -std::optional deviceUpdate; -std::optional runtime; -Babylon::Plugins::NativeInput* nativeInput; -std::optional nativeCanvas; -std::optional scriptLoader; -std::atomic engineReady, waitingOnNewView, activityPaused, needsDisableRendering; -``` - -JNI symbols all on `com.microsoft.babylonnative.BabylonNativeBridge`: - -| JNI method | What it does | -|---|---| -| `preload(hiddenSurface, context, cacheDir, scriptPath)` | One-shot init: `android::global::Initialize`, construct Device against the hidden 16×16 surface, load persistent shader cache from `cacheDir`, construct `AppRuntime`, dispatch a JS-thread lambda that wires up Console / NativeEngine / NativeOptimizations / NativeInput / Polyfills / Canvas, registers `lumiInterop` (custom JS bridge), then `LoadScript(scriptPath)`. | -| `surfaceChanged(w, h, surface)` | Switch to a real on-screen surface. Tears down bgfx if `needsDisableRendering`, then `UpdateWindow / UpdateSize`, pumps a clear frame, sets `engineReady = true`. | -| `onSurfaceTeardown()` | Sets `engineReady = false; waitingOnNewView = true; needsDisableRendering = true`. Defers actual `DisableRendering` to the next `renderFrame` (lazy teardown). | -| `renderFrame()` | Tries the engineMutex (non-blocking); honors `needsDisableRendering` (tears bgfx down) and `activityPaused`; otherwise the standard `Finish/Start` pair. Tracks FPS over 100-frame windows. Skips entirely until `scriptNotifiedReady` is set by JS via `lumiInterop.notifyReady()`. | -| `readyToRender()` | Returns `engineReady`. | -| `setCurrentActivity / activityOnPause / activityOnResume / activityOnRequestPermissionsResult` | Forwards to `android::global::*`; `Pause` / `Resume` also `runtime->Suspend / Resume` and toggle the `activityPaused` flag. | -| `setTouchInfo(id, x, y, buttonAction, buttonValue)` | One method for down / move / up; `try_to_lock`. | -| `loadScript / eval / runScript` | Forwards to `ScriptLoader` / `AppRuntime::Dispatch`. | -| `getStats()` | Returns `"FPS "` from the renderFrame counter. | -| `finishEngine()` | Tears everything down in order. | - -Custom JS-visible global `lumiInterop` (implemented as a `Napi::ObjectWrap`): - -- `lumiInterop.notifyReady()` — flips `scriptNotifiedReady = true`. Until - this fires, `renderFrame` is a no-op (gate against rendering before - the JS side has finished its first `start`). -- `lumiInterop.callNative(jsonString)` — calls the Java static method - `BabylonNativeBridge.onNativeFunctionCallInternal(String)`, which - forwards to a single registered `NativeFunctionCallback`. This is - Copilot's JS→native message pipe. - -### Java side (`babylon-native-bridge/android/src/main/java`) - -- **`BabylonNativeBridge`** — `static native` declarations + the - `NativeFunctionCallback` registration glue. -- **`BabylonNativeManagerView`** — preload widget. The host puts an - invisible 1×1 dp `SurfaceView` on screen; when `surfaceCreated` fires, - the holder is stashed; `initializeWithScript(path)` (called once by - the host once the script is downloaded) calls `BabylonNativeBridge.preload(...)` - on the main thread. Pre-renders 5 frames (warming bgfx + shader compile) - then stops invalidating to avoid touching a dying EGL surface. -- **`BabylonView`** — visible widget. Constructed against an `Activity` - + an optional `useMediaOverlay` flag (Z-order). Calls - `setCurrentActivity` / `surfaceChanged` / `onSurfaceTeardown` / - `setTouchInfo`. Drives `renderFrame` from `onDraw` while `mViewReady`, - re-`invalidate()`s while `mRenderLoopActive`. `onPause / onResume` - forward to `BabylonNativeBridge.activityOnPause / Resume`. - -### Kotlin host (`app-android/.../BabylonManagerView.kt`) - -Compose component that mounts `BabylonNativeManagerView` (the preload -SurfaceView) inside an invisible 1×1 dp Box. Subscribes to a -`FilamentGlSignal` to wait until the parallel Filament engine has -initialized its GL context (avoids GL contention). Observes -`Lifecycle.Event.ON_PAUSE / ON_RESUME` to call -`BabylonNativeBridge.activityOnPause / Resume`. Calls -`BabylonNativeManagerView.initializeWithScript(scriptPath)` once the -script asset has been downloaded; retries after 2 seconds if it -silently fails. Adds an extra `SurfaceHolder.Callback` to the inner -SurfaceView to call `BabylonNativeBridge.onSurfaceTeardown()` on -`surfaceDestroyed`. - -### JS side (`copilot-appearance/.../surfaces/mobile/src/main.ts`) - -The downloaded bundle calls `lumiInterop.callNative(JSON.stringify({...}))` -to send messages to native (e.g. `"scriptLoaded"`, -`"initializationError"`), and `lumiInterop.notifyReady()` once -`window.start(...)` finishes synchronously. - ---- - -## 2. What `Babylon::Integrations` already does for this stack - -Comparing one to one: - -| Concern | Copilot Mobile today | `Babylon::Integrations` today | -|---|---|---| -| AppRuntime + JsRuntime + ScriptLoader lifecycle | `babylon.cpp::preload` / `finishEngine` | `Runtime::Create` / `~Runtime` | -| Device + DeviceUpdate construction at first surface | `preload` constructs against a 16×16 hidden surface | `View::Attach` on first call constructs against the real surface (or any surface — see "prewarm" below) | -| Initialization queueing (LoadScript before Device exists) | Not separated; `preload` does both at once | Pre-init queue with `arcana::task_completion_source` | -| Hidden / dummy surface to start engine before the visible UI exists | The "preload SurfaceView" pattern in `BabylonNativeManagerView` | Same pattern, documented in `SimplifiedAPI.md §4.1 "Starting the engine before the user-visible UI exists"` | -| Surface switch | `surfaceChanged` → `Device::UpdateWindow / UpdateSize` | `~View` then a fresh `View::Attach` (which calls the same Device methods internally) | -| Render frame | `renderFrame` (`Finish/Start` cycle) | `View::RenderFrame` (same cycle, with `m_runtime.IsSuspended()` short-circuit) | -| Suspend/Resume from Activity lifecycle | `activityOnPause` / `activityOnResume` JNI methods | Auto-wired in `runtimeCreate` via `android::global::AddPauseCallback / AddResumeCallback` (each Runtime auto-suspends when the host calls `BabylonNative.pause/resume`). | -| `android::global::*` plumbing (NativeCamera, NativeXr) | `setCurrentActivity` / `activityOnRequestPermissionsResult` JNI | `BabylonNative.setContext / setCurrentActivity / requestPermissionsResult` JNI | -| Pointer input | `setTouchInfo(id, x, y, button, value)` (one method) | `viewPointerDown / Move / Up` (three methods); the View internally divides physical→logical via `ViewImpl::ToLogicalCoords` | -| Console / DebugTrace / uncaught exceptions | Hard-coded `__android_log_write` switch on level | `RuntimeOptions::log(LogLevel, std::string_view)`; the JNI `runtimeCreate` already routes everything to logcat under tag `"BabylonNative"` | -| Persistent shader cache | Hard-coded in `babylon.cpp` (`PlaygroundShaderCache.bin` in `cacheDir`) | **Not yet** in Integrations layer — see Open Questions | -| `lumiInterop.notifyReady()` gate | `scriptNotifiedReady` atomic; `renderFrame` no-ops until it fires | **Not** in Integrations layer — Copilot-specific. See migration plan below. | -| `lumiInterop.callNative()` | Custom `Napi::ObjectWrap` registering global `lumiInterop`; calls back into Java via cached `jclass` | **Not** in Integrations layer — Copilot-specific. Stays in app-specific JNI helper. | -| FPS stats | `getStats()` returns "FPS " | **Not** in Integrations layer. Easy to keep app-side. | - -### Built-in features the migration brings for free - -- Threaded `Suspend / Resume` reference counting. -- TCS-based pre-init queueing — host can call `runtimeLoadScript` before - the Device exists. -- Lazy GPU init: `runtimeCreate` is cheap and allocation-free; first - `viewAttach` triggers Device + plugin init. Means we no longer need - the dummy 1×1 SurfaceView trick *for the engine init phase*. (We - still want it for Copilot's "render in the background while the - visible view isn't yet up" use case — but that's a different story; - see §3.2.) -- Consistent logical-pixel pointer coordinates (`ViewImpl_Android.cpp` - divides by `Device::GetDevicePixelRatio()`). -- Per-platform window-size query: `View::Attach(runtime, window)` calls - `ANativeWindow_getWidth/Height` itself; no need for the - `surfaceChanged(w, h, surface)` JNI signature. Resize is - `view.viewResize(w, h)`. - ---- - -## 3. Migration plan - -### 3.1 Copilot-specific app surface to keep - -Two things are app-specific and must remain in app-side glue (analogous -to `Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp` -in this repo, where the Playground app keeps a Playground-specific JNI -helper alongside the generic one): - -1. **`lumiInterop` global** — `notifyReady()` and `callNative(json)`, - plus the Java-side static method dispatch and the - `NativeFunctionCallback` registration. This is Copilot's product - contract with the JS bundle. -2. **Persistent shader cache load/save** — `cacheDir` plumbing, - `Babylon::Plugins::ShaderCache::Load / Save` to `PlaygroundShaderCache.bin`. - -Both live in a new file in babylon-native-bridge: -`android/src/main/cpp/copilot_jni.cpp` (or similar). Same pattern as -`PlaygroundJNI.cpp` — `target_sources` it onto the -`BabylonNativeIntegrations` shared library so there's exactly one copy -of `Babylon::Integrations` symbols in the process. - -### 3.2 Engine-prewarm pattern — keep using the hidden SurfaceView - -The Copilot product wants the engine warmed up before the user -navigates to the live view (so the avatar is ready as soon as the -visible surface appears). Two implementation options: - -**Option A — Same hidden-SurfaceView trick.** Mount a 1×1 invisible -`SurfaceView`, call `BNManager.viewAttach(runtime, surface)` against -its `ANativeWindow`. That triggers Device construction + plugin init -+ first frame. When the visible `BabylonView` later does its own -`surfaceCreated`, we **detach** the prewarm view and **attach** the -visible one. `Babylon::Integrations` permits exactly this: subsequent -`Attach` calls just rebind via `Device::UpdateWindow` (cheap), and the -JS state and ShaderCache survive. - -**Option B — Headless prewarm via `Runtime::Create` only.** No -SurfaceView at all; let `runtimeCreate` start the JS engine and queue -`runtimeLoadScript`; only when the visible `BabylonView` is created -does the first `viewAttach` happen. JS bundle initialization runs -serially after first attach (because pre-init queueing waits for the -Device). - -**Recommendation: Option A.** Matches today's behavior, matches the -Playground host pattern, and shaves the most latency from the visible -view's first frame. Compose composable structure barely changes. - -### 3.3 Concrete file-by-file changes - -#### `babylon-native-bridge/android/src/main/cpp/babylon.cpp` → mostly delete - -Replace with a new `copilot_jni.cpp` that is **very small**: - -- `LumiInterop` Napi::ObjectWrap class (lift from current babylon.cpp, - unchanged). -- One JNI method `Java_com_microsoft_babylonnative_CopilotBridge_initialize(runtimeHandle, cacheDir)` - that: - - Caches `g_javaVM` and the `BabylonNativeBridge` jclass global ref - (currently in `preload`). - - Loads shader cache from `cacheDir/PlaygroundShaderCache.bin` if - present. - - Calls `runtime->RunOnJsThread([](Napi::Env env){ LumiInterop::Initialize(env); })`. -- One JNI method `Java_com_microsoft_babylonnative_CopilotBridge_saveShaderCache()` - for the explicit save path (called from `Activity.onPause` and - `runtime` teardown). -- One JNI method `Java_com_microsoft_babylonnative_CopilotBridge_isReadyToRender(runtimeHandle)` - that returns `scriptNotifiedReady && runtimeHandle != 0`. (See §3.4 - about why this is on the app side.) -- Resolve the runtime handle via `Babylon::Integrations::Android::RuntimeFromHandle` - (already in `Integrations/Android/Include/...`). - -Net: ~150 lines of app-specific code, vs. today's 850. - -#### `BabylonNativeBridge.java` → delete (after step 5) - -Becomes redundant. Its two responsibilities split as follows: - -- **`static native` declarations for engine/view ops** → all callers - shift to `com.babylonjs.integrations.BabylonNative` (the generic - Integrations Java class, identical to what the Playground uses). -- **`NativeFunctionCallback` registration + `onNativeFunctionCallInternal(String)`** - → moves to a new `CopilotBridge` class (next). - -The intermediate `System.loadLibrary` static block stays alive on -`CopilotBridge` so the .so loads once. - -#### `CopilotBridge.java` (new) - -App-specific JNI surface. Wraps `copilot_jni.cpp`: - -```java -public final class CopilotBridge { - static { System.loadLibrary("BabylonNativeIntegrations"); } - private CopilotBridge() {} - - /** Performs Copilot-specific runtime setup: caches the JavaVM + - * CopilotBridge jclass for `lumiInterop.callNative`, registers - * the `lumiInterop` global on the JS side, loads the persistent - * shader cache from `cacheDir`. Call once per Runtime, after - * `BabylonNative.runtimeCreate`. */ - public static native void initialize(long runtimeHandle, String cacheDir); - - /** Save the persistent shader cache (typically called from - * Activity.onPause). No-op if `initialize` hasn't been called. */ - public static native void saveShaderCache(); - - /** Returns true once `lumiInterop.notifyReady()` has been called - * by the JS bundle. The Java widgets call this before each - * `viewRenderFrame` to avoid rendering empty frames. */ - public static native boolean isReadyToRender(); - - // ----- JS → native message pipe ----- - - public interface NativeFunctionCallback { - void onNativeFunctionCall(String jsonData); - } - private static NativeFunctionCallback callback; - public static void setNativeFunctionCallback(NativeFunctionCallback cb) { callback = cb; } - /** Called from native by `lumiInterop.callNative(json)`. */ - public static void onNativeFunctionCallInternal(String jsonData) { - if (callback != null) callback.onNativeFunctionCall(jsonData); - } -} -``` - -#### `BabylonNativeBridge.java` → mostly delete - -#### `BabylonNativeManagerView.java` → simplify - -Conceptually unchanged — still hosts an invisible 1×1 SurfaceView for -prewarm — but its native interactions become: - -- Constructor: `BabylonNative.setContext(context.getApplicationContext())` - (idempotent inside the JNI). Caller still creates the Runtime; this - view borrows the handle. -- `surfaceCreated`: stash holder + activity (today's behavior). -- `initializeWithScript(scriptPath)` (its public API, called from - Compose host once the asset is downloaded): - - Resolve to a real ANativeWindow on the main thread via the holder. - - `viewHandle = BabylonNative.viewAttach(runtimeHandle, surface)` — - triggers Device construction + plugin init + queued-script flush. - (Bootstrap = `runtimeLoadScript(scriptPath)` happened earlier on - the Runtime; the Integrations TCS queues it until first Attach.) - - `CopilotBridge.initialize(runtimeHandle, activity.cacheDir)` — - caches JavaVM + bridge jclass, loads shader cache, registers - `lumiInterop`. -- `onDraw` prewarm loop (5 frames): drives `BabylonNative.viewRenderFrame(viewHandle)`, - guarded by `CopilotBridge.isReadyToRender(runtimeHandle)`. Still - stops invalidating after 5 frames as today. -- `notifyTearDownCurrentSurface()`: `BabylonNative.viewDetach(viewHandle)` - (replaces `BabylonNativeBridge.onSurfaceTeardown()`). Caller is - `BabylonManagerView.kt` from `surfaceDestroyed`. - -**One ownership decision required:** today the Runtime is implicitly -created inside `preload`; nobody owns it explicitly. For the migration, -the Compose host (`BabylonManagerView.kt`) should own the -`runtimeHandle` (created on first composition, destroyed in -`onRelease`). `BabylonNativeManagerView` and `BabylonView` borrow it. - -#### `BabylonView.java` → simplify - -Becomes very close to the Playground's `BabylonView.java`. Borrows the -runtime handle from the host. `surfaceCreated → viewAttach`, -`surfaceDestroyed → viewDetach`, `surfaceChanged → viewResize`, -`onTouch → viewPointerDown / Move / Up`. Keeps the `useMediaOverlay` -parameter (product-specific Z-order behavior). - -Drops: -- Manual `pixelDensityScale` math (Integrations does it). -- `mViewReady` / `mRenderLoopActive` flags — replaced by the - attached/detached state of `viewHandle`. -- `setCurrentActivity` / `onPause / onResume / onRequestPermissionsResult` - forwarding — those move to the Activity / Compose host (matching - the design we settled on for the Playground). - -#### `BabylonManagerView.kt` (Compose host) → small changes - -- Owns `runtimeHandle: Long`; creates with - `BabylonNative.runtimeCreate(enableDebugger = false)` once Filament - signals ready, destroys in `onDispose`. -- Calls `BabylonNative.setContext(application)` and - `BabylonNative.setCurrentActivity(activity)` (instead of going - through `BabylonNativeBridge`). -- `Lifecycle.Event.ON_PAUSE / ON_RESUME` → `BabylonNative.pause / resume` - (no per-view forwarding). -- `LaunchedEffect` triggers `runtimeLoadScript(runtimeHandle, scriptPath)` - once the path is known — this can fire even before the SurfaceView - exists, because Integrations queues it. Then the `surfaceCreated` - path of `BabylonNativeManagerView` does its prewarm-attach and - `CopilotBridge.initialize`. -- Rest of Compose plumbing (`FilamentGlSignal` wait, ViewModel script - path resolution, retry-after-delay) is unchanged. - -#### `copilot-appearance/.../mobile/src/main.ts` → no change - -`lumiInterop.notifyReady` and `lumiInterop.callNative` are preserved -verbatim. - -#### `babylon-native-bridge/android/CMakeLists.txt` - -- Add `set(BABYLON_NATIVE_INTEGRATIONS ON CACHE BOOL "" FORCE)`. -- Add `set(BABYLON_NATIVE_INTEGRATIONS_ANDROID ON CACHE BOOL "" FORCE)`. -- Drop the `BabylonNativeJNI` `add_library` and its plugin link list. -- After `add_subdirectory(${BN_REPO_ROOT_DIR})`, add - `target_sources(BabylonNativeIntegrations PRIVATE - src/main/cpp/copilot_jni.cpp)` plus `target_link_libraries(... - PRIVATE NativeOptimizations URL Performance Window)` (the few - Copilot uses that aren't unconditionally linked by the - Integrations target — to verify against the current Integrations - `CMakeLists.txt`). - -The published `.so` becomes `libBabylonNativeIntegrations.so` instead -of `libBabylonNativeJNI.so`. Update gradle / Java -`System.loadLibrary("BabylonNativeIntegrations")` accordingly. - -### 3.4 Things that need extra design discussion - -1. **`scriptNotifiedReady` gating of `renderFrame`.** Today, `renderFrame` - is a no-op until JS calls `lumiInterop.notifyReady()`. The - Integrations `View::RenderFrame` doesn't have this gate. Two ways - to add it: - - **Cheap:** keep a static `std::atomic` in `copilot_jni.cpp` - that `lumiInterop.notifyReady` flips, and have - `BabylonNativeManagerView.onDraw` / `BabylonView.onDraw` consult - `CopilotBridge.isReadyToRender(runtimeHandle)` before calling - `viewRenderFrame`. Same behavior as today; isolated to app code. - - **Generic:** add an opt-in "render gate" callback to - `RuntimeOptions` so any host can express "don't render until JS - signals ready". Probably overkill for one app. - I recommend the cheap path. - -2. **Persistent shader cache.** `Babylon::Plugins::ShaderCache` is - already linked from Integrations (gated by - `BABYLON_NATIVE_PLUGIN_SHADERCACHE`); `View::Attach` enables it. - But Integrations doesn't expose `ShaderCache::Load / Save`. For - Copilot, `copilot_jni.cpp` calls `ShaderCache::Load(stream)` during - `CopilotBridge.initialize`, and `ShaderCache::Save(stream)` from a - `CopilotBridge.saveShaderCache()` JNI method (called from Compose - host's `ON_PAUSE`). - -3. **Existing crash-prevention bandaids in `babylon.cpp`** — - `engineMutex try_to_lock`, `needsDisableRendering` lazy bgfx - teardown, the "pump a clear frame" trick after `surfaceChanged`, - the `waitingOnNewView` / `activityPaused` interleavings. Some are - workarounds for races we've since fixed (e.g. the - `~ScriptLoader / ~AppRuntime` ordering is enforced in - `RuntimeImpl::~RuntimeImpl`); others may still be necessary on - certain devices (the lazy bgfx teardown comment cites SIGABRT in - `GlContext::resize`). Plan: move forward with the Integrations API - as-is; if the crashes reappear in QA, add corresponding hooks to - Integrations rather than retain the bandaids on the Copilot side. - -4. **`useMediaOverlay` Z-ordering.** Compose-side concern; preserved on - the Java widget. No native-API impact. - -### 3.5 Sequencing - -Suggested order (each step independently builds + runs): - -1. **Add `CopilotBridge.java` + `copilot_jni.cpp`** — `lumiInterop` + - shader-cache load/save + the `isReadyToRender` gate. Keep the - existing `babylon.cpp` working in parallel (no changes there yet). -2. **Wire up `Babylon::Integrations` build.** Update - `babylon-native-bridge/android/CMakeLists.txt` to enable the - Integrations Android target; produce `libBabylonNativeIntegrations.so` - *alongside* `libBabylonNativeJNI.so`. Add `BabylonNative.java` - (the generic JNI declarations) verbatim from the Playground, - adjusted for the Copilot package. -3. **Migrate `BabylonView.java`** to use the Integrations API (drop - `surfaceChanged / setTouchInfo` etc.). Keep - `BabylonNativeManagerView.java` and the Compose host calling - `BabylonNativeBridge.preload` for now — the visible view's - migration is independent. -4. **Migrate `BabylonNativeManagerView.java`** + Compose host: own the - runtime handle, prewarm via `viewAttach`. Delete - `BabylonNativeBridge.preload / surfaceChanged / activityOnPause / - activityOnResume / setCurrentActivity / setTouchInfo / loadScript - / eval / renderFrame / onSurfaceTeardown / readyToRender / - getStats / runScript / finishEngine` + their JNI implementations. -5. **Delete `babylon.cpp`** — at this point only `copilot_jni.cpp` - remains in `babylon-native-bridge/android/src/main/cpp/`. -6. **Delete `libBabylonNativeJNI.so` build target** from CMakeLists. - -After step 4 the app works end-to-end on the new API; steps 5–6 are -cleanup. - -### 3.6 Testing checklist - -- [ ] Cold-launch the app — `BabylonManagerView` mounts, prewarm - surface created, scene loads behind the scenes. -- [ ] Navigate to live view — `BabylonView` attaches to its visible - surface; first frame appears immediately (shader cache primed). -- [ ] Background / foreground the app multiple times — no SIGSEGV / - SIGABRT in `GlContext::resize` or `bgfx::reset`. -- [ ] Touch input on hi-DPI device — coordinates reach Babylon at the - correct logical-pixel scale (regression target: tap detection - is no different from today). -- [ ] `lumiInterop.callNative` round-trip — JS sends a payload, - `NativeFunctionCallback.onNativeFunctionCall(jsonData)` fires. -- [ ] Shader cache file is created in `cacheDir` after first session - and primes subsequent launches. -- [ ] Rotating the device → `surfaceChanged` → smooth resize, no - teardown/reattach unless Android genuinely recreates the - Surface (which the current `surfaceCreated → viewAttach` / - `surfaceChanged → viewResize` split handles). - ---- - -## 4. iOS migration (sketch — not in scope for this plan) - -`Apps/Playground/iOS` already uses the Apple interop layer -(`BNRuntime`, `BNView`). The `babylon-native-bridge/ios` folder will -follow the same shape: drop the bespoke Obj-C bridge, replace with -calls into `BNRuntime / BNView`, keep a small `CopilotBridge.mm` that -exposes `lumiInterop` + shader cache. - ---- - -## Open questions (resolved) - -1. ~~Is "render-gate via `lumiInterop.notifyReady`" still load-bearing, - or is the bug it was working around fixable / already fixed?~~ - **Confirmed: keep the gate. It's not a bug bandaid — it's a - legitimate "wait until JS has a scene" signal. The JS bundle's - `window.start(...)` does the engine/scene/asset setup synchronously, - then calls `lumiInterop.notifyReady()` at the end. Until that fires, - `viewRenderFrame` would just blit empty buffers. The Java widgets - will consult `CopilotBridge.isReadyToRender(runtimeHandle)` before - each render call.** -2. ~~Is the prewarm pattern (Option A) the desired ongoing behavior, or - is the team open to dropping it once the Integrations layer's lazy - GPU init is in place (Option B)?~~ **Confirmed: keep the prewarm - pattern (Option A). `BabylonNativeManagerView` retains its hidden - 1×1 SurfaceView; it just calls `viewAttach` on the Integrations - layer instead of `BabylonNativeBridge.preload`.** -3. ~~Naming — should the new app-specific class be `CopilotBridge` (as - in this plan) or stay `BabylonNativeBridge`?~~ **Confirmed: - `CopilotBridge` for the app-specific surface (`lumiInterop` glue, - shader-cache load/save, render-ready gate). The legacy - `BabylonNativeBridge` class disappears at end of step 5 (its only - surviving responsibility — the `NativeFunctionCallback` registration - — moves to `CopilotBridge`).** From 4c9e7d1fd65434bd3f0911b0d8d3d929b25f7997 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 11 May 2026 17:22:26 -0700 Subject: [PATCH 29/71] Add missing polyfills --- CMakeLists.txt | 6 +++++- Integrations/CMakeLists.txt | 20 ++++++++++++++++++++ Integrations/Source/Runtime.cpp | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4830f49eb..6612caa10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,8 +140,12 @@ option(BABYLON_NATIVE_PLUGIN_SHADERTOOL "Include Babylon Native Plugin ShaderToo option(BABYLON_NATIVE_PLUGIN_TESTUTILS "Include Babylon Native Plugin TestUtils." ON) # Polyfills -option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) +option(BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER "Include Babylon Native Polyfill AbortController." ON) option(BABYLON_NATIVE_POLYFILL_CANVAS "Include Babylon Native Polyfill Canvas." ON) +option(BABYLON_NATIVE_POLYFILL_SCHEDULING "Include Babylon Native Polyfill Scheduling." ON) +option(BABYLON_NATIVE_POLYFILL_URL "Include Babylon Native Polyfill URL." ON) +option(BABYLON_NATIVE_POLYFILL_WEBSOCKET "Include Babylon Native Polyfill WebSocket." ON) +option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) # Integrations option(BABYLON_NATIVE_INTEGRATIONS "Build the cross-platform Babylon::Integrations facade (Runtime + View)." ON) diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index dfdecefec..30141127e 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -54,6 +54,26 @@ if(BABYLON_NATIVE_POLYFILL_CANVAS) target_link_libraries(Integrations PRIVATE Canvas) endif() +if(BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER=1) + target_link_libraries(Integrations PRIVATE AbortController) +endif() + +if(BABYLON_NATIVE_POLYFILL_SCHEDULING) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_SCHEDULING=1) + target_link_libraries(Integrations PRIVATE Scheduling) +endif() + +if(BABYLON_NATIVE_POLYFILL_URL) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_URL=1) + target_link_libraries(Integrations PRIVATE URL) +endif() + +if(BABYLON_NATIVE_POLYFILL_WEBSOCKET) + target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_WEBSOCKET=1) + target_link_libraries(Integrations PRIVATE WebSocket) +endif() + # ----- Conditionally-included plugins ----- if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 6d740f989..6d127e995 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -33,6 +33,22 @@ #include #include +#if BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER +#include +#endif + +#if BABYLON_NATIVE_POLYFILL_SCHEDULING +#include +#endif + +#if BABYLON_NATIVE_POLYFILL_URL +#include +#endif + +#if BABYLON_NATIVE_POLYFILL_WEBSOCKET +#include +#endif + #if BABYLON_NATIVE_POLYFILL_WINDOW #include #endif @@ -214,6 +230,19 @@ namespace Babylon::Integrations Babylon::Polyfills::TextDecoder::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); +#if BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER + Babylon::Polyfills::AbortController::Initialize(env); +#endif +#if BABYLON_NATIVE_POLYFILL_SCHEDULING + Babylon::Polyfills::Scheduling::Initialize(env); +#endif +#if BABYLON_NATIVE_POLYFILL_URL + Babylon::Polyfills::URL::Initialize(env); +#endif +#if BABYLON_NATIVE_POLYFILL_WEBSOCKET + Babylon::Polyfills::WebSocket::Initialize(env); +#endif + #if BABYLON_NATIVE_POLYFILL_CANVAS implPtr->m_canvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); #endif From 12b60a2c64fbe134e4427009ca8f0cb4d025d0ce Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 13 May 2026 12:06:02 -0700 Subject: [PATCH 30/71] Add missing cmake dep --- Integrations/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index 30141127e..9c9e6d75e 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -23,6 +23,7 @@ target_link_libraries(Integrations # GraphicsDevice is PUBLIC because View.h (a public header) # references `Babylon::Graphics::WindowT` from . PUBLIC GraphicsDevice + PRIVATE arcana PRIVATE AppRuntime PRIVATE ScriptLoader PRIVATE Blob From 37cdac598ddcf8a8f7e3d3fbdffc3cf8aed19e5c Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 13 May 2026 14:09:55 -0700 Subject: [PATCH 31/71] Fix dpr issues --- .../babylonjs/integrations/BabylonNative.java | 18 +++- Apps/Playground/Win32/App.cpp | 28 +++--- Core/Graphics/CMakeLists.txt | 3 +- .../Babylon/Graphics/DeviceQueries.h | 28 ++++++ Core/Graphics/Source/DeviceImpl.cpp | 3 +- Core/Graphics/Source/DeviceImpl.h | 1 - Core/Graphics/Source/DeviceImpl_Android.cpp | 3 +- Core/Graphics/Source/DeviceImpl_Unix.cpp | 3 +- Core/Graphics/Source/DeviceImpl_Win32.cpp | 3 +- Core/Graphics/Source/DeviceImpl_WinRT.cpp | 3 +- Core/Graphics/Source/DeviceImpl_iOS.mm | 3 +- Core/Graphics/Source/DeviceImpl_macOS.mm | 3 +- Core/Graphics/Source/DeviceImpl_visionOS.mm | 3 +- .../main/cpp/BabylonNativeIntegrations.cpp | 15 ++- Integrations/Apple/Source/BNView.mm | 12 ++- Integrations/CMakeLists.txt | 7 ++ .../Include/Babylon/Integrations/View.h | 92 +++++++++++------ Integrations/Source/RuntimeImpl.h | 15 +-- Integrations/Source/View.cpp | 99 ++++++++++++++----- Integrations/Source/ViewImpl_Android.cpp | 20 ++-- Integrations/Source/ViewImpl_Unix.cpp | 20 ++-- Integrations/Source/ViewImpl_Win32.cpp | 18 ++-- Integrations/Source/ViewImpl_WinRT.cpp | 29 +++--- Integrations/Source/ViewImpl_iOS.mm | 14 +-- Integrations/Source/ViewImpl_macOS.mm | 14 +-- Integrations/Source/ViewImpl_visionOS.mm | 15 +-- 26 files changed, 290 insertions(+), 182 deletions(-) create mode 100644 Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index d274886bd..cdda34fb2 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -128,13 +128,23 @@ public static native void requestPermissionsResult( public static native void viewRenderFrame(long handle); + /** + * Push the surface's new pixel-buffer dimensions. Both + * {@code width} and {@code height} are in **physical pixels** — + * pass the values you receive from + * {@code SurfaceHolder.Callback.surfaceChanged} unchanged. The + * native View divides by the device-pixel-ratio internally + * before configuring {@code Babylon::Graphics::Device}. + */ public static native void viewResize(long handle, int width, int height); /** - * Pointer events. Pass {@code MotionEvent.getX/getY} through unchanged - * (Android-native physical-pixel coordinates). The View internally - * normalizes to logical (CSS) pixels — the unit Babylon.js's - * {@code PointerEvent.clientX/clientY} pipeline expects. + * Pointer events. Pass {@code MotionEvent.getX/getY} through + * unchanged — Android-native physical-pixel coordinates. The + * native View divides by the device-pixel-ratio internally + * before forwarding to Babylon.js's + * {@code PointerEvent.clientX/clientY} pipeline (which expects + * logical / CSS pixels). * *

Throws {@link IllegalStateException} if invoked when the * native library was built without diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 030f927f2..ef6b9faf7 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -252,27 +252,28 @@ BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) void ProcessMouseButtons(tagPOINTER_BUTTON_CHANGE_TYPE changeType, int x, int y) { using View = Babylon::Integrations::View; + using CoordinateUnits = Babylon::Integrations::CoordinateUnits; if (!g_view) return; switch (changeType) { case POINTER_CHANGE_FIRSTBUTTON_DOWN: - g_view->OnMouseDown(View::LeftMouseButton(), static_cast(x), static_cast(y)); + g_view->OnMouseDown(View::LeftMouseButton(), static_cast(x), static_cast(y), CoordinateUnits::Physical); break; case POINTER_CHANGE_FIRSTBUTTON_UP: - g_view->OnMouseUp(View::LeftMouseButton(), static_cast(x), static_cast(y)); + g_view->OnMouseUp(View::LeftMouseButton(), static_cast(x), static_cast(y), CoordinateUnits::Physical); break; case POINTER_CHANGE_SECONDBUTTON_DOWN: - g_view->OnMouseDown(View::RightMouseButton(), static_cast(x), static_cast(y)); + g_view->OnMouseDown(View::RightMouseButton(), static_cast(x), static_cast(y), CoordinateUnits::Physical); break; case POINTER_CHANGE_SECONDBUTTON_UP: - g_view->OnMouseUp(View::RightMouseButton(), static_cast(x), static_cast(y)); + g_view->OnMouseUp(View::RightMouseButton(), static_cast(x), static_cast(y), CoordinateUnits::Physical); break; case POINTER_CHANGE_THIRDBUTTON_DOWN: - g_view->OnMouseDown(View::MiddleMouseButton(), static_cast(x), static_cast(y)); + g_view->OnMouseDown(View::MiddleMouseButton(), static_cast(x), static_cast(y), CoordinateUnits::Physical); break; case POINTER_CHANGE_THIRDBUTTON_UP: - g_view->OnMouseUp(View::MiddleMouseButton(), static_cast(x), static_cast(y)); + g_view->OnMouseUp(View::MiddleMouseButton(), static_cast(x), static_cast(y), CoordinateUnits::Physical); break; } } @@ -280,6 +281,7 @@ void ProcessMouseButtons(tagPOINTER_BUTTON_CHANGE_TYPE changeType, int x, int y) LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { using View = Babylon::Integrations::View; + using CoordinateUnits = Babylon::Integrations::CoordinateUnits; switch (message) { @@ -328,7 +330,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (g_view) { g_view->Resize(static_cast(LOWORD(lParam)), - static_cast(HIWORD(lParam))); + static_cast(HIWORD(lParam)), + CoordinateUnits::Physical); } break; } @@ -375,7 +378,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { g_view->OnPointerDown(static_cast(pointerId), static_cast(x), - static_cast(y)); + static_cast(y), + CoordinateUnits::Physical); } } } @@ -397,13 +401,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (info.pointerType == PT_MOUSE) { ProcessMouseButtons(info.ButtonChangeType, x, y); - g_view->OnMouseMove(static_cast(x), static_cast(y)); + g_view->OnMouseMove(static_cast(x), static_cast(y), CoordinateUnits::Physical); } else { g_view->OnPointerMove(static_cast(pointerId), static_cast(x), - static_cast(y)); + static_cast(y), + CoordinateUnits::Physical); } } } @@ -430,7 +435,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { g_view->OnPointerUp(static_cast(pointerId), static_cast(x), - static_cast(y)); + static_cast(y), + CoordinateUnits::Physical); } } } diff --git a/Core/Graphics/CMakeLists.txt b/Core/Graphics/CMakeLists.txt index 7f2059a35..24f910d40 100644 --- a/Core/Graphics/CMakeLists.txt +++ b/Core/Graphics/CMakeLists.txt @@ -4,8 +4,9 @@ set(SOURCES "Include/Shared/Babylon/Graphics/Device.h" "InternalInclude/Babylon/Graphics/BgfxCallback.h" "InternalInclude/Babylon/Graphics/continuation_scheduler.h" - "InternalInclude/Babylon/Graphics/FrameBuffer.h" "InternalInclude/Babylon/Graphics/DeviceContext.h" + "InternalInclude/Babylon/Graphics/DeviceQueries.h" + "InternalInclude/Babylon/Graphics/FrameBuffer.h" "InternalInclude/Babylon/Graphics/SafeTimespanGuarantor.h" "InternalInclude/Babylon/Graphics/Texture.h" "Source/BgfxCallback.cpp" diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h new file mode 100644 index 000000000..c971c7b2f --- /dev/null +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace Babylon::Graphics +{ + // Query the screen's device-pixel-ratio for the given native + // window. Free function so callers can obtain the ratio before a + // `Device` has been constructed (e.g. an interop layer converting + // physical → logical pixels for `Configuration::Width/Height` + // ahead of `Device::Device(config)`). + // + // For an existing `Device`, prefer `Device::GetDevicePixelRatio()` + // — that's the value this function returned when `UpdateWindow` + // was last called, cached on the `Device`. + // + // Visibility: + // - Declared in `InternalInclude/Babylon/Graphics/` rather than + // in the public `Device.h` because the implementation depends + // on platform-specific window types that are also internal + // (CAMetalLayer, ANativeWindow, HWND, X11 `Window`, etc.) and + // because most cross-platform host code should just use the + // `Device::GetDevicePixelRatio()` instance method. + // - Reachable to consumers of the `GraphicsDeviceContext` target + // (plugins, the Integrations facade). NOT reachable to plain + // `GraphicsDevice` consumers. + float GetDevicePixelRatio(WindowT window); +} diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 93a3fa75e..9ca5349c2 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -1,5 +1,6 @@ #include "DeviceImpl.h" +#include #include #include #include @@ -101,7 +102,7 @@ namespace Babylon::Graphics std::scoped_lock lock{m_state.Mutex}; ConfigureBgfxPlatformData(m_state.Bgfx.InitState.platformData, window); ConfigureBgfxRenderType(m_state.Bgfx.InitState.platformData, m_state.Bgfx.InitState.type); - m_state.Resolution.DevicePixelRatio = GetDevicePixelRatio(window); + m_state.Resolution.DevicePixelRatio = Babylon::Graphics::GetDevicePixelRatio(window); m_state.Bgfx.Dirty = true; } diff --git a/Core/Graphics/Source/DeviceImpl.h b/Core/Graphics/Source/DeviceImpl.h index a96f57c93..55df369b8 100644 --- a/Core/Graphics/Source/DeviceImpl.h +++ b/Core/Graphics/Source/DeviceImpl.h @@ -111,7 +111,6 @@ namespace Babylon::Graphics static const bgfx::RendererType::Enum s_bgfxRenderType; static void ConfigureBgfxPlatformData(bgfx::PlatformData& pd, WindowT window); static void ConfigureBgfxRenderType(bgfx::PlatformData& pd, bgfx::RendererType::Enum& renderType); - static float GetDevicePixelRatio(WindowT window); void UpdateBgfxState(); void UpdateBgfxResolution(); diff --git a/Core/Graphics/Source/DeviceImpl_Android.cpp b/Core/Graphics/Source/DeviceImpl_Android.cpp index 200eb39c1..9be4ac111 100644 --- a/Core/Graphics/Source/DeviceImpl_Android.cpp +++ b/Core/Graphics/Source/DeviceImpl_Android.cpp @@ -1,4 +1,5 @@ #include +#include #include "DeviceImpl.h" #include @@ -20,7 +21,7 @@ namespace Babylon::Graphics } } - float DeviceImpl::GetDevicePixelRatio(WindowT) + float GetDevicePixelRatio(WindowT) { // In Android, the baseline DPI is 160dpi. // See https://developer.android.com/training/multiscreen/screendensities#dips-pels diff --git a/Core/Graphics/Source/DeviceImpl_Unix.cpp b/Core/Graphics/Source/DeviceImpl_Unix.cpp index 4a39ddd99..08983c70a 100644 --- a/Core/Graphics/Source/DeviceImpl_Unix.cpp +++ b/Core/Graphics/Source/DeviceImpl_Unix.cpp @@ -1,4 +1,5 @@ #include +#include #include "DeviceImpl.h" namespace Babylon::Graphics @@ -12,7 +13,7 @@ namespace Babylon::Graphics { } - float DeviceImpl::GetDevicePixelRatio(WindowT) + float GetDevicePixelRatio(WindowT) { // TODO: We should persist a Display object instead of opening a new display. // See https://github.com/BabylonJS/BabylonNative/issues/625 diff --git a/Core/Graphics/Source/DeviceImpl_Win32.cpp b/Core/Graphics/Source/DeviceImpl_Win32.cpp index 13464993f..63e6f0c33 100644 --- a/Core/Graphics/Source/DeviceImpl_Win32.cpp +++ b/Core/Graphics/Source/DeviceImpl_Win32.cpp @@ -1,4 +1,5 @@ #include +#include #include "DeviceImpl.h" #include @@ -13,7 +14,7 @@ namespace Babylon::Graphics { } - float DeviceImpl::GetDevicePixelRatio(WindowT window) + float GetDevicePixelRatio(WindowT window) { UINT dpi = GetDpiForWindow(window); diff --git a/Core/Graphics/Source/DeviceImpl_WinRT.cpp b/Core/Graphics/Source/DeviceImpl_WinRT.cpp index c813a32ec..e91bb6f5c 100644 --- a/Core/Graphics/Source/DeviceImpl_WinRT.cpp +++ b/Core/Graphics/Source/DeviceImpl_WinRT.cpp @@ -1,4 +1,5 @@ #include +#include #include "DeviceImpl.h" #include #include @@ -24,7 +25,7 @@ namespace Babylon::Graphics { } - float DeviceImpl::GetDevicePixelRatio(WindowT window) + float GetDevicePixelRatio(WindowT window) { if (auto uiElement = window.try_as()) { diff --git a/Core/Graphics/Source/DeviceImpl_iOS.mm b/Core/Graphics/Source/DeviceImpl_iOS.mm index 011ce2692..8c1f900a5 100644 --- a/Core/Graphics/Source/DeviceImpl_iOS.mm +++ b/Core/Graphics/Source/DeviceImpl_iOS.mm @@ -1,5 +1,6 @@ #include #include +#include #include "DeviceImpl.h" #import @@ -25,7 +26,7 @@ bool IsValidScale(float scale) { } - float DeviceImpl::GetDevicePixelRatio(WindowT window) + float GetDevicePixelRatio(WindowT window) { // contentsScale can return 0 if it hasn't been set yet. float scale = static_cast(((CAMetalLayer*)window).contentsScale); diff --git a/Core/Graphics/Source/DeviceImpl_macOS.mm b/Core/Graphics/Source/DeviceImpl_macOS.mm index a589d9e14..4027c7cb9 100644 --- a/Core/Graphics/Source/DeviceImpl_macOS.mm +++ b/Core/Graphics/Source/DeviceImpl_macOS.mm @@ -1,5 +1,6 @@ #include #include +#include #include "DeviceImpl.h" #import @@ -16,7 +17,7 @@ { } - float DeviceImpl::GetDevicePixelRatio(WindowT window) + float GetDevicePixelRatio(WindowT window) { float scale = static_cast(((CAMetalLayer*)window).contentsScale); if (std::isinf(scale) || scale <= 0) diff --git a/Core/Graphics/Source/DeviceImpl_visionOS.mm b/Core/Graphics/Source/DeviceImpl_visionOS.mm index e156145b2..1fb5c3dd3 100644 --- a/Core/Graphics/Source/DeviceImpl_visionOS.mm +++ b/Core/Graphics/Source/DeviceImpl_visionOS.mm @@ -1,4 +1,5 @@ #include +#include #include "DeviceImpl.h" namespace Babylon::Graphics @@ -12,7 +13,7 @@ { } - float DeviceImpl::GetDevicePixelRatio(WindowT) + float GetDevicePixelRatio(WindowT) { return [UITraitCollection currentTraitCollection].displayScale; } diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index f9f9470bb..0b79bec19 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -413,8 +413,11 @@ JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_viewResize( JNIEnv*, jclass, jlong handle, jint width, jint height) { + // Java callers pass the SurfaceView's pixel-buffer dimensions + // (physical pixels on Android). AsView(handle)->Resize(static_cast(width), - static_cast(height)); + static_cast(height), + Babylon::Integrations::CoordinateUnits::Physical); } JNIEXPORT void JNICALL @@ -423,7 +426,9 @@ Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; - AsView(handle)->OnPointerDown(static_cast(pointerId), x, y); + // Java callers pass `MotionEvent.getX/getY`, which are in physical pixels. + AsView(handle)->OnPointerDown(static_cast(pointerId), x, y, + Babylon::Integrations::CoordinateUnits::Physical); #else (void)handle; (void)pointerId; (void)x; (void)y; ThrowPluginNotEnabled(env, @@ -438,7 +443,8 @@ Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; - AsView(handle)->OnPointerMove(static_cast(pointerId), x, y); + AsView(handle)->OnPointerMove(static_cast(pointerId), x, y, + Babylon::Integrations::CoordinateUnits::Physical); #else (void)handle; (void)pointerId; (void)x; (void)y; ThrowPluginNotEnabled(env, @@ -453,7 +459,8 @@ Java_com_babylonjs_integrations_BabylonNative_viewPointerUp( { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; - AsView(handle)->OnPointerUp(static_cast(pointerId), x, y); + AsView(handle)->OnPointerUp(static_cast(pointerId), x, y, + Babylon::Integrations::CoordinateUnits::Physical); #else (void)handle; (void)pointerId; (void)x; (void)y; ThrowPluginNotEnabled(env, diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index b22509c9e..d311abbc3 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -115,7 +115,8 @@ - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height if (_view) { _view->Resize(static_cast(width), - static_cast(height)); + static_cast(height), + Babylon::Integrations::CoordinateUnits::Physical); } } @@ -128,7 +129,8 @@ - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { _view->OnPointerDown(static_cast(pointerId), static_cast(x), - static_cast(y)); + static_cast(y), + Babylon::Integrations::CoordinateUnits::Logical); } #else (void)pointerId; (void)x; (void)y; @@ -146,7 +148,8 @@ - (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { _view->OnPointerMove(static_cast(pointerId), static_cast(x), - static_cast(y)); + static_cast(y), + Babylon::Integrations::CoordinateUnits::Logical); } #else (void)pointerId; (void)x; (void)y; @@ -164,7 +167,8 @@ - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y { _view->OnPointerUp(static_cast(pointerId), static_cast(x), - static_cast(y)); + static_cast(y), + Babylon::Integrations::CoordinateUnits::Logical); } #else (void)pointerId; (void)x; (void)y; diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index 9c9e6d75e..b821d7e02 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -23,6 +23,13 @@ target_link_libraries(Integrations # GraphicsDevice is PUBLIC because View.h (a public header) # references `Babylon::Graphics::WindowT` from . PUBLIC GraphicsDevice + # GraphicsDeviceContext is PRIVATE — needed so View.cpp can see + # for the free + # `Babylon::Graphics::GetDevicePixelRatio(window)` helper used to + # convert physical → logical pixels in `View::Attach`'s first-attach + # path (before the `Device` exists). Internal Graphics types are + # never exposed across the Integrations public surface. + PRIVATE GraphicsDeviceContext PRIVATE arcana PRIVATE AppRuntime PRIVATE ScriptLoader diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h index 66e7ad076..20d9bff2a 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -10,6 +10,40 @@ namespace Babylon::Integrations class Runtime; struct ViewImpl; + // Tag for the units of coordinates and dimensions handed to View + // methods. Babylon.js consumes pointer events and render-target + // sizes in **logical** (CSS / DIP) pixels; the View internally + // divides by the Device's queried device-pixel-ratio when given + // values in **physical** (surface pixel-buffer) units, so hosts + // pass through whatever their platform's native event system / + // surface-size API delivers. + // + // Examples: + // Physical: Android `MotionEvent.getX/getY` / + // `ANativeWindow_getWidth/Height`, + // Apple `CAMetalLayer.drawableSize`, + // Win32 `GetClientRect` / `WM_POINTER*` (DPI-aware), + // X11 button events / `XGetGeometry`. + // Logical: iOS `UITouch.location`, + // macOS `NSEvent.locationInWindow`, + // UWP `PointerPoint.Position` / `CoreWindow.Bounds`, + // host code that has already done its own DPR divide. + enum class CoordinateUnits + { + Physical, + Logical, + }; + + // Pixel dimensions returned by `ViewImpl::QuerySize`, tagged with + // the units they're in. `View::Attach` consumes this and converts + // to logical via the platform's queried DPR when needed. + struct ViewSize + { + uint32_t Width; + uint32_t Height; + CoordinateUnits Units; + }; + // Transient: created when a host surface appears, destroyed when // it goes away. Multiple sequential Views may be attached to the // same Runtime over its lifetime. **At most one View may be attached @@ -28,11 +62,9 @@ namespace Babylon::Integrations // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). // The View queries the surface's pixel-buffer size from the - // window itself (Android `ANativeWindow_getWidth/Height`, - // Apple `CAMetalLayer.drawableSize`, Win32 `GetClientRect`, - // X11 `XGetGeometry`, UWP bounds × scale) — the host doesn't - // pass dimensions. The Device queries the screen - // device-pixel-ratio internally as well. + // window itself via `ViewImpl::QuerySize`, which returns the + // platform-natural unit; the View internally converts to + // logical pixels before configuring the Device. // // The first Attach on a given Runtime is the heavy step: it // constructs `Babylon::Graphics::Device`, dispatches GPU plugin @@ -71,32 +103,36 @@ namespace Babylon::Integrations // rendered"). void RenderFrame(); - // Resize the bound surface. `width` and `height` are in - // **physical pixels** — the actual pixel-buffer dimensions of - // the surface the GPU will render into. Hosts pass through - // whatever their platform's view layer reports (e.g. - // Android's `View.onSizeChanged` w/h, iOS's - // `MTKViewDelegate.drawableSizeWillChange:` size). + // Resize the bound surface. The View converts physical → + // logical internally if `units == CoordinateUnits::Physical`, + // so the host can pass whatever its platform's view layer + // reports without doing the DPR divide. + // + // Examples: + // Android `View.onSizeChanged(w, h)` → Physical. + // iOS `MTKViewDelegate.drawableSizeWillChange:` → Physical. + // UWP `SizeChangedEventArgs.NewSize` (already in DIPs) → Logical. // // Must be called from the frame thread. - void Resize(uint32_t width, uint32_t height); + void Resize(uint32_t width, uint32_t height, CoordinateUnits units); #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT // ----- Pointer / mouse input forwarding ----- // // Host calls these from its event loop while the view exists. // Routed to the JS thread via `NativeInput`, where Babylon.js - // consumes them as `PointerEvent.clientX/clientY`. + // consumes them as `PointerEvent.clientX/clientY` (logical / + // CSS pixels). The View converts physical → logical internally + // if `units == CoordinateUnits::Physical`. // - // **Pass coordinates in whatever unit your platform's native - // event system delivers.** The View internally normalizes to - // logical (CSS) pixels — the unit Babylon.js expects — using - // a per-platform helper. On platforms whose native pointer - // events are already in logical units (iOS `UITouch`, macOS - // `NSEvent`, UWP `PointerPoint`), this is a passthrough; on - // platforms that deliver physical pixels (Android `MotionEvent`, - // Win32 `WM_POINTER*`, X11 button events), the View divides by - // the Device's queried device-pixel-ratio. + // Pass coordinates in whatever unit your platform's native + // event system delivers: + // Physical: Android `MotionEvent.getX/getY`, + // Win32 `WM_POINTER*` / `WM_MOUSE*` (DPI-aware), + // X11 button events. + // Logical: iOS `UITouch.location`, + // macOS `NSEvent.locationInWindow`, + // UWP `PointerPoint.Position`. // // Babylon Native distinguishes pointer (touch) input from mouse // input; both methods feed the same Babylon.js pointer-event @@ -112,9 +148,9 @@ namespace Babylon::Integrations // Safe to call from any thread. // Touch / pointer events. - void OnPointerDown(int32_t pointerId, float x, float y); - void OnPointerMove(int32_t pointerId, float x, float y); - void OnPointerUp(int32_t pointerId, float x, float y); + void OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units); + void OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units); + void OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units); // Mouse events. `buttonIndex` is one of LeftMouseButton(), // MiddleMouseButton(), RightMouseButton(); `wheelAxis` is @@ -122,9 +158,9 @@ namespace Babylon::Integrations // `Babylon::Plugins::NativeInput::*_ID` value (single source of // truth — no duplication, no risk of drift) without exposing the // NativeInput header from this public View.h. - void OnMouseDown(uint32_t buttonIndex, float x, float y); - void OnMouseUp(uint32_t buttonIndex, float x, float y); - void OnMouseMove(float x, float y); + void OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units); + void OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units); + void OnMouseMove(float x, float y, CoordinateUnits units); void OnMouseWheel(uint32_t wheelAxis, int32_t scrollValue); static uint32_t LeftMouseButton(); diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index b6ae01145..44f7b00c6 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -172,16 +172,9 @@ namespace Babylon::Integrations void Resume(); // Query the surface's pixel-buffer size from the native window - // handle. Implemented per-platform. - static std::pair QuerySize(Babylon::Graphics::WindowT window); - - // Convert native pointer-event coordinates to the logical (CSS) - // pixels the NativeInput / Babylon.js pointer pipeline expects. - // On platforms whose native event system already delivers - // logical units (iOS, macOS, UWP), this is a passthrough; on - // platforms that deliver physical pixels (Android, Win32, X11), - // this divides by the Device's queried device-pixel-ratio. - // Implemented per-platform. - std::pair ToLogicalCoords(float x, float y) const; + // handle, tagged with the platform-natural units it returned. + // Implemented per-platform. `View::Attach` converts to logical + // before configuring the Device. + static ViewSize QuerySize(Babylon::Graphics::WindowT window); }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index e39a3f4fd..624dfa201 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -1,9 +1,40 @@ #include "RuntimeImpl.h" +#include + #include namespace Babylon::Integrations { + namespace + { + // Convert a (width, height) measured in `units` to logical + // pixels. Caller always passes the real DPR; this helper + // decides whether to apply it based on `units`. + std::pair ToLogicalSize(uint32_t width, uint32_t height, + CoordinateUnits units, float dpr) + { + if (units == CoordinateUnits::Logical || dpr <= 0.0f) + { + return {width, height}; + } + return {static_cast(width / dpr), + static_cast(height / dpr)}; + } + + // Convert a (x, y) coordinate pair measured in `units` to + // logical pixels. + std::pair ToLogicalCoords(float x, float y, + CoordinateUnits units, float dpr) + { + if (units == CoordinateUnits::Logical || dpr <= 0.0f) + { + return {x, y}; + } + return {x / dpr, y / dpr}; + } + } + // --------------------------------------------------------------------- // View::Attach (first time and subsequent) // --------------------------------------------------------------------- @@ -20,18 +51,24 @@ namespace Babylon::Integrations const bool firstAttach = !impl.m_device; // Per-platform: query the surface's pixel-buffer size from the - // native window handle. ViewImpl_*.cpp implements this; e.g. - // ANativeWindow_getWidth on Android, GetClientRect on Win32, - // CAMetalLayer.drawableSize on Apple. - const auto [width, height] = ViewImpl::QuerySize(nativeWindow); + // native window handle in whatever unit is natural for the + // platform. `Babylon::Graphics::Device` expects logical pixels + // for `Configuration::Width/Height` and `UpdateSize`, so we + // convert if QuerySize returned physical. On first Attach the + // Device doesn't exist yet, so we go through the standalone + // `Babylon::Graphics::GetDevicePixelRatio(window)` free function. + const ViewSize querySize = ViewImpl::QuerySize(nativeWindow); + const float dpr = Babylon::Graphics::GetDevicePixelRatio(nativeWindow); + const auto [logicalW, logicalH] = ToLogicalSize( + querySize.Width, querySize.Height, querySize.Units, dpr); if (firstAttach) { // First Attach on this Runtime: construct the Device. Babylon::Graphics::Configuration config{}; config.Window = nativeWindow; - config.Width = width; - config.Height = height; + config.Width = logicalW; + config.Height = logicalH; config.MSAASamples = impl.m_options.msaaSamples; impl.m_device.emplace(config); @@ -43,7 +80,7 @@ namespace Babylon::Integrations // the surface. Plugins, polyfills, and any loaded scripts // are preserved on the JS side. impl.m_device->UpdateWindow(nativeWindow); - impl.m_device->UpdateSize(width, height); + impl.m_device->UpdateSize(logicalW, logicalH); } std::unique_ptr view{new View{std::make_unique(runtime)}}; @@ -155,82 +192,90 @@ namespace Babylon::Integrations impl.m_deviceUpdate->Start(); } - void View::Resize(uint32_t width, uint32_t height) + void View::Resize(uint32_t width, uint32_t height, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_device) { - impl.m_device->UpdateSize(width, height); + const auto [lw, lh] = ToLogicalSize(width, height, units, + impl.m_device->GetDevicePixelRatio()); + impl.m_device->UpdateSize(lw, lh); } } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - void View::OnPointerDown(int32_t pointerId, float x, float y) + void View::OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_input) + if (impl.m_input && impl.m_device) { - const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + const auto [lx, ly] = ToLogicalCoords(x, y, units, + impl.m_device->GetDevicePixelRatio()); impl.m_input->TouchDown(static_cast(pointerId), static_cast(lx), static_cast(ly)); } } - void View::OnPointerMove(int32_t pointerId, float x, float y) + void View::OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_input) + if (impl.m_input && impl.m_device) { - const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + const auto [lx, ly] = ToLogicalCoords(x, y, units, + impl.m_device->GetDevicePixelRatio()); impl.m_input->TouchMove(static_cast(pointerId), static_cast(lx), static_cast(ly)); } } - void View::OnPointerUp(int32_t pointerId, float x, float y) + void View::OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_input) + if (impl.m_input && impl.m_device) { - const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + const auto [lx, ly] = ToLogicalCoords(x, y, units, + impl.m_device->GetDevicePixelRatio()); impl.m_input->TouchUp(static_cast(pointerId), static_cast(lx), static_cast(ly)); } } - void View::OnMouseDown(uint32_t buttonIndex, float x, float y) + void View::OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_input) + if (impl.m_input && impl.m_device) { - const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + const auto [lx, ly] = ToLogicalCoords(x, y, units, + impl.m_device->GetDevicePixelRatio()); impl.m_input->MouseDown(buttonIndex, static_cast(lx), static_cast(ly)); } } - void View::OnMouseUp(uint32_t buttonIndex, float x, float y) + void View::OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_input) + if (impl.m_input && impl.m_device) { - const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + const auto [lx, ly] = ToLogicalCoords(x, y, units, + impl.m_device->GetDevicePixelRatio()); impl.m_input->MouseUp(buttonIndex, static_cast(lx), static_cast(ly)); } } - void View::OnMouseMove(float x, float y) + void View::OnMouseMove(float x, float y, CoordinateUnits units) { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_input) + if (impl.m_input && impl.m_device) { - const auto [lx, ly] = m_impl->ToLogicalCoords(x, y); + const auto [lx, ly] = ToLogicalCoords(x, y, units, + impl.m_device->GetDevicePixelRatio()); impl.m_input->MouseMove(static_cast(lx), static_cast(ly)); } diff --git a/Integrations/Source/ViewImpl_Android.cpp b/Integrations/Source/ViewImpl_Android.cpp index 7a5660066..e294916b2 100644 --- a/Integrations/Source/ViewImpl_Android.cpp +++ b/Integrations/Source/ViewImpl_Android.cpp @@ -4,24 +4,16 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } + // ANativeWindow_getWidth/Height return the surface's + // pixel-buffer size in physical (device) pixels. return {static_cast(ANativeWindow_getWidth(window)), - static_cast(ANativeWindow_getHeight(window))}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // Android `MotionEvent.getX/getY` returns coordinates in physical - // pixels (the SurfaceView's pixel space). NativeInput / Babylon.js - // expect logical (CSS) pixels — divide by the device-pixel-ratio - // the underlying Device has cached. - const auto& impl = *m_runtime.m_impl; - const float dpr = impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; - return {x / dpr, y / dpr}; + static_cast(ANativeWindow_getHeight(window)), + CoordinateUnits::Physical}; } } diff --git a/Integrations/Source/ViewImpl_Unix.cpp b/Integrations/Source/ViewImpl_Unix.cpp index ac4c1de3f..a491b903e 100644 --- a/Integrations/Source/ViewImpl_Unix.cpp +++ b/Integrations/Source/ViewImpl_Unix.cpp @@ -4,11 +4,11 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == 0) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } // X11 `Window` is just an XID; querying its geometry needs a @@ -18,7 +18,7 @@ namespace Babylon::Integrations Display* display = XOpenDisplay(nullptr); if (display == nullptr) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } ::Window root{}; @@ -31,17 +31,9 @@ namespace Babylon::Integrations if (status == 0) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } - return {width, height}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // X11 button-event coordinates are in physical pixels. Divide - // by the Device's queried DPR. - const auto& impl = *m_runtime.m_impl; - const float dpr = impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; - return {x / dpr, y / dpr}; + // XGetGeometry returns the window's size in physical pixels. + return {width, height, CoordinateUnits::Physical}; } } diff --git a/Integrations/Source/ViewImpl_Win32.cpp b/Integrations/Source/ViewImpl_Win32.cpp index 46a49d016..f6edca0a0 100644 --- a/Integrations/Source/ViewImpl_Win32.cpp +++ b/Integrations/Source/ViewImpl_Win32.cpp @@ -4,23 +4,17 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { RECT rect{}; if (window == nullptr || !GetClientRect(window, &rect)) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } + // For DPI-aware apps, `GetClientRect` returns the surface's + // pixel-buffer size in physical pixels. return {static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top)}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // Win32 `WM_MOUSE*` / `WM_POINTER*` coordinates are in physical - // pixels for DPI-aware apps. Divide by the Device's queried DPR. - const auto& impl = *m_runtime.m_impl; - const float dpr = impl.m_device ? impl.m_device->GetDevicePixelRatio() : 1.0f; - return {x / dpr, y / dpr}; + static_cast(rect.bottom - rect.top), + CoordinateUnits::Physical}; } } diff --git a/Integrations/Source/ViewImpl_WinRT.cpp b/Integrations/Source/ViewImpl_WinRT.cpp index 8ff50ba19..e0826f82d 100644 --- a/Integrations/Source/ViewImpl_WinRT.cpp +++ b/Integrations/Source/ViewImpl_WinRT.cpp @@ -5,32 +5,33 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { // WindowT here is `winrt::Windows::Foundation::IInspectable` // wrapping one of: ICoreWindow, ISwapChainPanel, // Microsoft::UI::Xaml::Controls::ISwapChainPanel. + // + // Both branches return logical (DIP) units; `View::Attach` + // converts to physical via the centralized + // `Babylon::Graphics::GetDevicePixelRatio(window)` (which on + // IUIElement-capable WinRT windows is `RasterizationScale`). + // This keeps a single DPR source of truth across the + // Integrations layer. if (auto coreWindow = window.try_as()) { const auto bounds = coreWindow.Bounds(); - // CoreWindow.Bounds is in DIPs; multiply by DPI scale to get physical pixels. - // For now we return DIPs-as-pixels; hosts targeting hi-DPI may - // adjust by RasterizationScale / DisplayInformation.LogicalDpi. return {static_cast(bounds.Width), - static_cast(bounds.Height)}; + static_cast(bounds.Height), + CoordinateUnits::Logical}; } if (auto panel = window.try_as()) { + // FrameworkElement.ActualWidth/Height are in DIPs. const auto fe = panel.as(); - return {static_cast(fe.ActualWidth() * panel.CompositionScaleX()), - static_cast(fe.ActualHeight() * panel.CompositionScaleY())}; + return {static_cast(fe.ActualWidth()), + static_cast(fe.ActualHeight()), + CoordinateUnits::Logical}; } - return {0, 0}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // UWP `PointerPoint.Position` is in DIPs (logical) — passthrough. - return {x, y}; + return {0, 0, CoordinateUnits::Logical}; } } diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm index d028eebdd..91e6d5a58 100644 --- a/Integrations/Source/ViewImpl_iOS.mm +++ b/Integrations/Source/ViewImpl_iOS.mm @@ -4,24 +4,18 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } // metal-cpp's CA::MetalLayer* can be bridge-cast to the Obj-C // CAMetalLayer*; drawableSize is in physical pixels. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; const CGSize size = layer.drawableSize; return {static_cast(size.width), - static_cast(size.height)}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // UIKit `UITouch.locationInView` is already in logical points — - // passthrough. - return {x, y}; + static_cast(size.height), + CoordinateUnits::Physical}; } } diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm index 6d8c49609..32d3f3379 100644 --- a/Integrations/Source/ViewImpl_macOS.mm +++ b/Integrations/Source/ViewImpl_macOS.mm @@ -4,21 +4,17 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } + // CAMetalLayer.drawableSize is in physical pixels. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; const CGSize size = layer.drawableSize; return {static_cast(size.width), - static_cast(size.height)}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // AppKit `NSEvent.locationInWindow` is in logical points — passthrough. - return {x, y}; + static_cast(size.height), + CoordinateUnits::Physical}; } } diff --git a/Integrations/Source/ViewImpl_visionOS.mm b/Integrations/Source/ViewImpl_visionOS.mm index 649ac7a74..32d3f3379 100644 --- a/Integrations/Source/ViewImpl_visionOS.mm +++ b/Integrations/Source/ViewImpl_visionOS.mm @@ -4,22 +4,17 @@ namespace Babylon::Integrations { - std::pair ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { - return {0, 0}; + return {0, 0, CoordinateUnits::Physical}; } + // CAMetalLayer.drawableSize is in physical pixels. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; const CGSize size = layer.drawableSize; return {static_cast(size.width), - static_cast(size.height)}; - } - - std::pair ViewImpl::ToLogicalCoords(float x, float y) const - { - // visionOS pointer interactions arrive through SwiftUI/RealityKit - // gesture recognizers in logical points — passthrough. - return {x, y}; + static_cast(size.height), + CoordinateUnits::Physical}; } } From 83a099e5c3035b9f4e09415d54fc5e8723984cfe Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 13 May 2026 14:13:30 -0700 Subject: [PATCH 32/71] Move ViewSize out of public API --- Integrations/Include/Babylon/Integrations/View.h | 10 ---------- Integrations/Source/RuntimeImpl.h | 8 +++++++- Integrations/Source/View.cpp | 2 +- Integrations/Source/ViewImpl_Android.cpp | 2 +- Integrations/Source/ViewImpl_Unix.cpp | 2 +- Integrations/Source/ViewImpl_Win32.cpp | 2 +- Integrations/Source/ViewImpl_WinRT.cpp | 2 +- Integrations/Source/ViewImpl_iOS.mm | 2 +- Integrations/Source/ViewImpl_macOS.mm | 2 +- Integrations/Source/ViewImpl_visionOS.mm | 2 +- 10 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Babylon/Integrations/View.h index 20d9bff2a..679c60b39 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Babylon/Integrations/View.h @@ -34,16 +34,6 @@ namespace Babylon::Integrations Logical, }; - // Pixel dimensions returned by `ViewImpl::QuerySize`, tagged with - // the units they're in. `View::Attach` consumes this and converts - // to logical via the platform's queried DPR when needed. - struct ViewSize - { - uint32_t Width; - uint32_t Height; - CoordinateUnits Units; - }; - // Transient: created when a host surface appears, destroyed when // it goes away. Multiple sequential Views may be attached to the // same Runtime over its lifetime. **At most one View may be attached diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 44f7b00c6..dd515ba4f 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -175,6 +175,12 @@ namespace Babylon::Integrations // handle, tagged with the platform-natural units it returned. // Implemented per-platform. `View::Attach` converts to logical // before configuring the Device. - static ViewSize QuerySize(Babylon::Graphics::WindowT window); + struct QuerySizeResult + { + uint32_t Width; + uint32_t Height; + CoordinateUnits Units; + }; + static QuerySizeResult QuerySize(Babylon::Graphics::WindowT window); }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 624dfa201..7d51f1d00 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -57,7 +57,7 @@ namespace Babylon::Integrations // convert if QuerySize returned physical. On first Attach the // Device doesn't exist yet, so we go through the standalone // `Babylon::Graphics::GetDevicePixelRatio(window)` free function. - const ViewSize querySize = ViewImpl::QuerySize(nativeWindow); + const auto querySize = ViewImpl::QuerySize(nativeWindow); const float dpr = Babylon::Graphics::GetDevicePixelRatio(nativeWindow); const auto [logicalW, logicalH] = ToLogicalSize( querySize.Width, querySize.Height, querySize.Units, dpr); diff --git a/Integrations/Source/ViewImpl_Android.cpp b/Integrations/Source/ViewImpl_Android.cpp index e294916b2..a56ba857b 100644 --- a/Integrations/Source/ViewImpl_Android.cpp +++ b/Integrations/Source/ViewImpl_Android.cpp @@ -4,7 +4,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { diff --git a/Integrations/Source/ViewImpl_Unix.cpp b/Integrations/Source/ViewImpl_Unix.cpp index a491b903e..1c364f763 100644 --- a/Integrations/Source/ViewImpl_Unix.cpp +++ b/Integrations/Source/ViewImpl_Unix.cpp @@ -4,7 +4,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == 0) { diff --git a/Integrations/Source/ViewImpl_Win32.cpp b/Integrations/Source/ViewImpl_Win32.cpp index f6edca0a0..b108f0917 100644 --- a/Integrations/Source/ViewImpl_Win32.cpp +++ b/Integrations/Source/ViewImpl_Win32.cpp @@ -4,7 +4,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { RECT rect{}; if (window == nullptr || !GetClientRect(window, &rect)) diff --git a/Integrations/Source/ViewImpl_WinRT.cpp b/Integrations/Source/ViewImpl_WinRT.cpp index e0826f82d..fa67c1419 100644 --- a/Integrations/Source/ViewImpl_WinRT.cpp +++ b/Integrations/Source/ViewImpl_WinRT.cpp @@ -5,7 +5,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { // WindowT here is `winrt::Windows::Foundation::IInspectable` // wrapping one of: ICoreWindow, ISwapChainPanel, diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm index 91e6d5a58..d598179b8 100644 --- a/Integrations/Source/ViewImpl_iOS.mm +++ b/Integrations/Source/ViewImpl_iOS.mm @@ -4,7 +4,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm index 32d3f3379..83461e653 100644 --- a/Integrations/Source/ViewImpl_macOS.mm +++ b/Integrations/Source/ViewImpl_macOS.mm @@ -4,7 +4,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { diff --git a/Integrations/Source/ViewImpl_visionOS.mm b/Integrations/Source/ViewImpl_visionOS.mm index 32d3f3379..83461e653 100644 --- a/Integrations/Source/ViewImpl_visionOS.mm +++ b/Integrations/Source/ViewImpl_visionOS.mm @@ -4,7 +4,7 @@ namespace Babylon::Integrations { - ViewSize ViewImpl::QuerySize(Babylon::Graphics::WindowT window) + ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) { if (window == nullptr) { From 50655a42ca3eb8f060d307d0e1286455c6f65f19 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 08:36:16 -0700 Subject: [PATCH 33/71] Re-org sources Add zero size check --- .../iOS/Playground-Bridging-Header.h | 2 +- Apps/Playground/iOS/PlaygroundBootstrap.mm | 2 +- Install/Install.cmake | 14 + Integrations/Android/CMakeLists.txt | 10 +- Integrations/Apple/CMakeLists.txt | 31 +- Integrations/Apple/Source/BNRuntime.mm | 9 + Integrations/Apple/Source/BNRuntimeInternal.h | 2 +- Integrations/Apple/Source/BNView.mm | 70 +++- Integrations/Apple/Source/BNViewDelegate.mm | 7 +- .../BabylonNativeIntegrations.h | 7 - Integrations/CMakeLists.txt | 12 +- .../Integrations/Android/RuntimeHandle.h | 42 +-- .../Babylon/Integrations/Apple}/BNRuntime.h | 185 +++++----- .../Integrations/Apple/BNRuntimeNative.h | 25 ++ .../Babylon/Integrations/Apple}/BNView.h | 9 +- .../Apple/BabylonNativeIntegrations.h | 8 + .../Babylon/Integrations/LogLevel.h | 48 +-- .../Babylon/Integrations/Runtime.h | 248 ++++++------- .../Babylon/Integrations/RuntimeOptions.h | 110 +++--- .../{ => Shared}/Babylon/Integrations/View.h | 338 +++++++++--------- Integrations/Source/View.cpp | 16 + 21 files changed, 656 insertions(+), 539 deletions(-) delete mode 100644 Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h rename Integrations/{Android/Include => Include/Platform/Android}/Babylon/Integrations/Android/RuntimeHandle.h (97%) rename Integrations/{Apple/include/BabylonNativeIntegrations => Include/Platform/Apple/Babylon/Integrations/Apple}/BNRuntime.h (97%) create mode 100644 Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h rename Integrations/{Apple/include/BabylonNativeIntegrations => Include/Platform/Apple/Babylon/Integrations/Apple}/BNView.h (92%) create mode 100644 Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h rename Integrations/Include/{ => Shared}/Babylon/Integrations/LogLevel.h (97%) rename Integrations/Include/{ => Shared}/Babylon/Integrations/Runtime.h (97%) rename Integrations/Include/{ => Shared}/Babylon/Integrations/RuntimeOptions.h (97%) rename Integrations/Include/{ => Shared}/Babylon/Integrations/View.h (97%) diff --git a/Apps/Playground/iOS/Playground-Bridging-Header.h b/Apps/Playground/iOS/Playground-Bridging-Header.h index c0f219e69..58bad9104 100644 --- a/Apps/Playground/iOS/Playground-Bridging-Header.h +++ b/Apps/Playground/iOS/Playground-Bridging-Header.h @@ -6,6 +6,6 @@ #pragma once -#import +#import #import "PlaygroundBootstrap.h" diff --git a/Apps/Playground/iOS/PlaygroundBootstrap.mm b/Apps/Playground/iOS/PlaygroundBootstrap.mm index ea891c9f0..ac2d10dc0 100644 --- a/Apps/Playground/iOS/PlaygroundBootstrap.mm +++ b/Apps/Playground/iOS/PlaygroundBootstrap.mm @@ -4,7 +4,7 @@ #import "PlaygroundBootstrap.h" -#import +#import #include #include diff --git a/Install/Install.cmake b/Install/Install.cmake index 72626d163..de94c0ede 100644 --- a/Install/Install.cmake +++ b/Install/Install.cmake @@ -249,3 +249,17 @@ if(TARGET XMLHttpRequest) install_lib(XMLHttpRequest) install_include(XMLHttpRequest) endif() + +# ---------------- +# Integrations +# ---------------- + +if(TARGET Integrations) + install_lib(Integrations) + install_include(Integrations) +endif() + +if(TARGET BabylonNativeIntegrations) + install_lib(BabylonNativeIntegrations) + install_include(BabylonNativeIntegrations) +endif() diff --git a/Integrations/Android/CMakeLists.txt b/Integrations/Android/CMakeLists.txt index 053d32164..aba1f6b4b 100644 --- a/Integrations/Android/CMakeLists.txt +++ b/Integrations/Android/CMakeLists.txt @@ -14,15 +14,17 @@ if(NOT ANDROID) "Disable BABYLON_NATIVE_INTEGRATIONS_ANDROID for non-Android builds.") endif() +set(ANDROID_INTEGRATIONS_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platform/Android") + set(SOURCES - "Include/Babylon/Integrations/Android/RuntimeHandle.h" - "src/main/cpp/BabylonNativeIntegrations.cpp") + "${ANDROID_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Android/RuntimeHandle.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/BabylonNativeIntegrations.cpp") add_library(BabylonNativeIntegrations SHARED ${SOURCES}) warnings_as_errors(BabylonNativeIntegrations) -target_include_directories(BabylonNativeIntegrations PUBLIC "Include") +target_include_directories(BabylonNativeIntegrations PUBLIC "${ANDROID_INTEGRATIONS_INCLUDE_ROOT}") target_link_libraries(BabylonNativeIntegrations PRIVATE Integrations @@ -34,4 +36,4 @@ target_link_libraries(BabylonNativeIntegrations PRIVATE -lz) set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/.." FILES ${SOURCES}) diff --git a/Integrations/Apple/CMakeLists.txt b/Integrations/Apple/CMakeLists.txt index 158499bcc..0cd41a196 100644 --- a/Integrations/Apple/CMakeLists.txt +++ b/Integrations/Apple/CMakeLists.txt @@ -13,35 +13,38 @@ if(NOT APPLE) "Disable BABYLON_NATIVE_INTEGRATIONS_APPLE for non-Apple builds.") endif() +set(APPLE_INTEGRATIONS_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platform/Apple") + set(SOURCES - "include/BabylonNativeIntegrations/BabylonNativeIntegrations.h" - "include/BabylonNativeIntegrations/BNRuntime.h" - "include/BabylonNativeIntegrations/BNView.h" - "Source/BNRuntime.mm" - "Source/BNRuntimeInternal.h" - "Source/BNView.mm" - "Source/BNViewDelegate.mm") + "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BabylonNativeIntegrations.h" + "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BNRuntime.h" + "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BNRuntimeNative.h" + "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BNView.h" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntimeInternal.h" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm") add_library(BabylonNativeIntegrations STATIC ${SOURCES}) warnings_as_errors(BabylonNativeIntegrations) target_include_directories(BabylonNativeIntegrations - PUBLIC "include" - PRIVATE "Source") + PUBLIC "${APPLE_INTEGRATIONS_INCLUDE_ROOT}" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Source") target_link_libraries(BabylonNativeIntegrations - PRIVATE Integrations + PUBLIC Integrations PRIVATE "-framework Foundation" PRIVATE "-framework QuartzCore" PRIVATE "-framework MetalKit") # Enable ARC for the Obj-C++ files. set_source_files_properties( - "Source/BNRuntime.mm" - "Source/BNView.mm" - "Source/BNViewDelegate.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm" PROPERTIES COMPILE_FLAGS "-fobjc-arc") set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/.." FILES ${SOURCES}) diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index d880b1dd3..f56a12164 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -2,6 +2,7 @@ // Babylon::Integrations::Runtime. #import "BNRuntimeInternal.h" +#import #import #import @@ -137,3 +138,11 @@ - (void)updateXrViewIfNeeded @end +namespace Babylon::Integrations::Apple +{ + Runtime* RuntimeFromBNRuntime(BNRuntime* runtime) + { + return runtime == nil ? nullptr : [runtime nativeRuntime]; + } +} + diff --git a/Integrations/Apple/Source/BNRuntimeInternal.h b/Integrations/Apple/Source/BNRuntimeInternal.h index c1a502cca..74190e2b7 100644 --- a/Integrations/Apple/Source/BNRuntimeInternal.h +++ b/Integrations/Apple/Source/BNRuntimeInternal.h @@ -4,7 +4,7 @@ #pragma once -#import +#import #include diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index d311abbc3..ca18c6aee 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -2,12 +2,13 @@ // Babylon::Integrations::View. #import "BNRuntimeInternal.h" -#import +#import #import #import #include +#include #include #include @@ -40,30 +41,65 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view // +layerClass override). CAMetalLayer* layer = (CAMetalLayer*)view.layer; - // Force a layout pass so the view's bounds are valid before we - // read them, and seed the layer's drawableSize from the view's - // logical bounds × backing scale. MTKView's autoResizeDrawable - // will keep drawableSize in sync from this point on, but at - // attach time it can still be (0, 0) — so we set it explicitly - // to avoid handing bgfx a zero-sized swap chain. - [view layoutIfNeeded]; + // If MTKView has not produced a drawable size yet, seed it + // from real laid-out bounds. If the host supplied a non-zero + // drawableSize explicitly (for example, for hidden preload), + // preserve it. + const CGSize drawableSize = layer.drawableSize; + if (drawableSize.width <= 0 || drawableSize.height <= 0) + { + [view layoutIfNeeded]; + + const CGSize boundsSize = view.bounds.size; + if (boundsSize.width > 0 && boundsSize.height > 0) + { #if TARGET_OS_OSX - const CGFloat scale = view.window.backingScaleFactor > 0 - ? view.window.backingScaleFactor - : 1.0; + CGFloat scale = view.window.backingScaleFactor > 0 + ? view.window.backingScaleFactor + : 1.0; #else - const CGFloat scale = view.contentScaleFactor; + CGFloat scale = view.contentScaleFactor; #endif - layer.drawableSize = CGSizeMake(view.bounds.size.width * scale, - view.bounds.size.height * scale); + if (scale <= 0) + { + scale = 1.0; + } + layer.drawableSize = CGSizeMake(boundsSize.width * scale, + boundsSize.height * scale); + } + } + + const CGSize finalDrawableSize = layer.drawableSize; + if (finalDrawableSize.width <= 0 || finalDrawableSize.height <= 0) + { + @throw [NSException + exceptionWithName:@"BabylonNativeInvalidViewException" + reason:@"BNView requires a non-zero drawableSize or non-zero bounds before attach." + userInfo:nil]; + } // First attach on this runtime triggers GPU device construction // + plugin initialization + queued-script flush. The View // queries the layer's drawableSize itself; the host doesn't // need to pass dimensions. - _view = Babylon::Integrations::View::Attach( - *runtime.nativeRuntime, - (__bridge CA::MetalLayer*)layer); + try + { + _view = Babylon::Integrations::View::Attach( + *runtime.nativeRuntime, + (__bridge CA::MetalLayer*)layer); + } + catch (const std::exception& exception) + { + NSLog(@"BNView: View::Attach failed: %s", exception.what()); + _mtkView = nil; + return nil; + } + catch (...) + { + NSLog(@"BNView: View::Attach failed with an unknown exception"); + _mtkView = nil; + return nil; + } if (!_view) { _mtkView = nil; diff --git a/Integrations/Apple/Source/BNViewDelegate.mm b/Integrations/Apple/Source/BNViewDelegate.mm index c4247c7c1..b5606da21 100644 --- a/Integrations/Apple/Source/BNViewDelegate.mm +++ b/Integrations/Apple/Source/BNViewDelegate.mm @@ -3,7 +3,7 @@ // override the delegate methods and call `super` to keep the default // forwarding behavior. -#import +#import @implementation BNViewDelegate { @@ -32,6 +32,11 @@ - (instancetype)initWithView:(BNView*)view - (void)mtkView:(MTKView* __unused)v drawableSizeWillChange:(CGSize)size { + if (size.width <= 0 || size.height <= 0) + { + return; + } + [_view resizeWithWidth:static_cast(size.width) height:static_cast(size.height)]; } diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h b/Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h deleted file mode 100644 index 6a8850d76..000000000 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BabylonNativeIntegrations.h +++ /dev/null @@ -1,7 +0,0 @@ -// Umbrella header for the Babylon::Integrations Apple interop layer. -// Import this from Swift via the bridging header (or from Obj-C). - -#pragma once - -#import -#import diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index b821d7e02..61ade49fe 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -1,8 +1,10 @@ +set(INTEGRATIONS_SHARED_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/Include/Shared") + set(SOURCES - "Include/Babylon/Integrations/LogLevel.h" - "Include/Babylon/Integrations/Runtime.h" - "Include/Babylon/Integrations/RuntimeOptions.h" - "Include/Babylon/Integrations/View.h" + "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/LogLevel.h" + "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/Runtime.h" + "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/RuntimeOptions.h" + "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/View.h" "Source/Runtime.cpp" "Source/RuntimeImpl.h" "Source/View.cpp" @@ -12,7 +14,7 @@ add_library(Integrations ${SOURCES}) warnings_as_errors(Integrations) -target_include_directories(Integrations PUBLIC "Include") +target_include_directories(Integrations PUBLIC "${INTEGRATIONS_SHARED_INCLUDE_ROOT}") # Always-on dependencies. The Integrations layer formalizes the canonical # Babylon Native setup, so the polyfills that AppContext.cpp historically diff --git a/Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h b/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h similarity index 97% rename from Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h rename to Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h index 3c5f409f9..604c7faa3 100644 --- a/Integrations/Android/Include/Babylon/Integrations/Android/RuntimeHandle.h +++ b/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h @@ -1,21 +1,21 @@ -#pragma once - -#include - -#include - -namespace Babylon::Integrations::Android -{ - // Convert an opaque jlong handle (as returned by `runtimeCreate` in - // `BabylonNativeIntegrations.cpp`) back to a Runtime pointer. - // - // The Android JNI layer wraps each Runtime in an internal struct - // that also holds Activity-lifecycle event tickets, so a direct - // `reinterpret_cast(handle)` is incorrect — hosts that - // ship their own JNI helpers alongside `libBabylonNativeIntegrations.so` - // (e.g. for app-specific bootstrap routines) must go through this - // function to resolve the handle correctly. - // - // Returns nullptr if `handle` is 0. - Runtime* RuntimeFromHandle(jlong handle); -} +#pragma once + +#include + +#include + +namespace Babylon::Integrations::Android +{ + // Convert an opaque jlong handle (as returned by `runtimeCreate` in + // `BabylonNativeIntegrations.cpp`) back to a Runtime pointer. + // + // The Android JNI layer wraps each Runtime in an internal struct + // that also holds Activity-lifecycle event tickets, so a direct + // `reinterpret_cast(handle)` is incorrect — hosts that + // ship their own JNI helpers alongside `libBabylonNativeIntegrations.so` + // (e.g. for app-specific bootstrap routines) must go through this + // function to resolve the handle correctly. + // + // Returns nullptr if `handle` is 0. + Runtime* RuntimeFromHandle(jlong handle); +} \ No newline at end of file diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h similarity index 97% rename from Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h rename to Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h index 7ce2dfb2a..9de9822c8 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNRuntime.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h @@ -1,93 +1,92 @@ -// BNRuntime.h — public Obj-C interface for the Babylon::Integrations -// runtime on Apple platforms (iOS, macOS, visionOS). -// -// Swift consumers see this through the auto-generated Swift bridge -// (BNRuntime is exposed to Swift as `BNRuntime`). -// -// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. - -#pragma once - -#import - -@class MTKView; - -NS_ASSUME_NONNULL_BEGIN - -@interface BNRuntime : NSObject - -/// Constructs the runtime: starts the JS engine + thread, sets up -/// non-GPU polyfills and plugins. Cheap and synchronous; no GPU -/// device is created yet (that happens on the first `BNView` attach). -/// Default options: JS debugger off, log routes to `NSLog`. -- (instancetype)init; - -/// Same as `init` but lets the host opt into the JS debugger. -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger; - -/// Same as `initWithEnableDebugger:` but also wires up a persistent -/// on-disk GPU shader cache. Pass a writable file path (typically -/// inside `NSCachesDirectory`, e.g. -/// `[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"babylon.shadercache"]`). -/// Pass `nil` to disable the on-disk cache (equivalent to the -/// `initWithEnableDebugger:` overload). -/// -/// The cache is loaded on first `BNView` attach and saved on `suspend` -/// and on deallocation. -/// -/// If `shaderCachePath` is non-`nil` but the native library was built -/// without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, this method raises an -/// `NSException` (name -/// `BabylonNativePluginNotEnabledException`) so the misconfiguration -/// surfaces at construction time rather than silently dropping the -/// cache. Passing `nil` is always safe regardless of build config. -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger - shaderCachePath:(nullable NSString*)shaderCachePath - NS_DESIGNATED_INITIALIZER; - -/// Load a script from a URL onto the JS thread. Calls made before -/// the first `BNView` is created are queued internally and dispatched -/// after engine initialization completes during that first attach. -/// Calls after the first attach are dispatched immediately. -- (void)loadScript:(NSString*)url; - -/// Evaluate JavaScript source on the JS thread. Same queueing -/// semantics as `loadScript`. -- (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL; - -/// Reference-counted suspend. While suspended, JS timers pause and -/// any attached `BNView` becomes a no-op for `renderFrame` (the host -/// can keep calling it from its draw callback unconditionally; -/// nothing happens until `resume`). -- (void)suspend; - -/// Decrement the suspend count; resume the JS thread when the count -/// reaches zero. -- (void)resume; - -/// Whether the runtime is currently suspended. -@property (nonatomic, readonly, getter=isSuspended) BOOL suspended; - -/// Set the platform view that XR will render into (typically a -/// separate transparent `MTKView` overlay, distinct from the main -/// view's Metal layer). Pass `nil` to clear the XR surface. Safe to -/// call before the first `BNView` attach; the value is applied when -/// NativeXr finishes initializing during that first attach. -/// -/// Raises an `NSException` (name -/// `BabylonNativePluginNotEnabledException`) if invoked when -/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` was not enabled at native build -/// time. -- (void)setXrView:(nullable MTKView*)xrView; - -/// `YES` while an XR session is active. Updated from the JS thread -/// by NativeXr's internal session-state callback; safe to poll from -/// any thread. Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` was -/// not enabled at native build time (no XR session can ever be active -/// in that build). -@property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; - -@end - -NS_ASSUME_NONNULL_END - +// BNRuntime.h — public Obj-C interface for the Babylon::Integrations +// runtime on Apple platforms (iOS, macOS, visionOS). +// +// Swift consumers see this through the auto-generated Swift bridge +// (BNRuntime is exposed to Swift as `BNRuntime`). +// +// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. + +#pragma once + +#import + +@class MTKView; + +NS_ASSUME_NONNULL_BEGIN + +@interface BNRuntime : NSObject + +/// Constructs the runtime: starts the JS engine + thread, sets up +/// non-GPU polyfills and plugins. Cheap and synchronous; no GPU +/// device is created yet (that happens on the first `BNView` attach). +/// Default options: JS debugger off, log routes to `NSLog`. +- (instancetype)init; + +/// Same as `init` but lets the host opt into the JS debugger. +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger; + +/// Same as `initWithEnableDebugger:` but also wires up a persistent +/// on-disk GPU shader cache. Pass a writable file path (typically +/// inside `NSCachesDirectory`, e.g. +/// `[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"babylon.shadercache"]`). +/// Pass `nil` to disable the on-disk cache (equivalent to the +/// `initWithEnableDebugger:` overload). +/// +/// The cache is loaded on first `BNView` attach and saved on `suspend` +/// and on deallocation. +/// +/// If `shaderCachePath` is non-`nil` but the native library was built +/// without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, this method raises an +/// `NSException` (name +/// `BabylonNativePluginNotEnabledException`) so the misconfiguration +/// surfaces at construction time rather than silently dropping the +/// cache. Passing `nil` is always safe regardless of build config. +- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger + shaderCachePath:(nullable NSString*)shaderCachePath + NS_DESIGNATED_INITIALIZER; + +/// Load a script from a URL onto the JS thread. Calls made before +/// the first `BNView` is created are queued internally and dispatched +/// after engine initialization completes during that first attach. +/// Calls after the first attach are dispatched immediately. +- (void)loadScript:(NSString*)url; + +/// Evaluate JavaScript source on the JS thread. Same queueing +/// semantics as `loadScript`. +- (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL; + +/// Reference-counted suspend. While suspended, JS timers pause and +/// any attached `BNView` becomes a no-op for `renderFrame` (the host +/// can keep calling it from its draw callback unconditionally; +/// nothing happens until `resume`). +- (void)suspend; + +/// Decrement the suspend count; resume the JS thread when the count +/// reaches zero. +- (void)resume; + +/// Whether the runtime is currently suspended. +@property (nonatomic, readonly, getter=isSuspended) BOOL suspended; + +/// Set the platform view that XR will render into (typically a +/// separate transparent `MTKView` overlay, distinct from the main +/// view's Metal layer). Pass `nil` to clear the XR surface. Safe to +/// call before the first `BNView` attach; the value is applied when +/// NativeXr finishes initializing during that first attach. +/// +/// Raises an `NSException` (name +/// `BabylonNativePluginNotEnabledException`) if invoked when +/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` was not enabled at native build +/// time. +- (void)setXrView:(nullable MTKView*)xrView; + +/// `YES` while an XR session is active. Updated from the JS thread +/// by NativeXr's internal session-state callback; safe to poll from +/// any thread. Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` was +/// not enabled at native build time (no XR session can ever be active +/// in that build). +@property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h new file mode 100644 index 000000000..51ac1404b --- /dev/null +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h @@ -0,0 +1,25 @@ +// Obj-C++ escape hatch for host-specific helpers layered on top of +// Babylon Native's Apple Integrations runtime. + +#pragma once + +#import + +#ifdef __cplusplus + +#include + +namespace Babylon::Integrations::Apple +{ + // Convert a BNRuntime wrapper back to its underlying C++ Runtime. + // + // Hosts that ship small app-specific Obj-C++ helpers alongside the + // Apple Integrations layer can use this to access C++ escape hatches + // such as Runtime::RunOnJsThread without making those concepts part + // of the Swift/Objective-C BNRuntime surface. + // + // Returns nullptr if runtime is nil. + Runtime* RuntimeFromBNRuntime(BNRuntime* runtime); +} + +#endif \ No newline at end of file diff --git a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h similarity index 92% rename from Integrations/Apple/include/BabylonNativeIntegrations/BNView.h rename to Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h index b8b43de27..4c9a16d00 100644 --- a/Integrations/Apple/include/BabylonNativeIntegrations/BNView.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h @@ -58,7 +58,13 @@ NS_ASSUME_NONNULL_BEGIN /// initialization. Subsequent attaches just rebind the surface. /// /// Returns `nil` if `runtime` or `view` is `nil`, or if the underlying -/// `Babylon::Integrations::View::Attach` fails. +/// `Babylon::Integrations::View::Attach` fails after view-size +/// preconditions are met. +/// +/// Raises an `NSException` (name +/// `BabylonNativeInvalidViewException`) if `view` has neither a +/// non-zero `drawableSize` nor non-zero bounds at attach time. Hidden +/// preload views should set a small non-zero `drawableSize` explicitly. /// /// **Delegate management:** If `view.delegate` is `nil` at the time of /// construction, BNView creates a `BNViewDelegate` and assigns it to @@ -106,4 +112,3 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END - diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h new file mode 100644 index 000000000..c7e5dbcef --- /dev/null +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h @@ -0,0 +1,8 @@ +// Umbrella header for the Babylon::Integrations Apple interop layer. +// Import this from Swift via the bridging header (or from Obj-C). + +#pragma once + +#import +#import +#import \ No newline at end of file diff --git a/Integrations/Include/Babylon/Integrations/LogLevel.h b/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h similarity index 97% rename from Integrations/Include/Babylon/Integrations/LogLevel.h rename to Integrations/Include/Shared/Babylon/Integrations/LogLevel.h index 447f3c475..29b49ffb9 100644 --- a/Integrations/Include/Babylon/Integrations/LogLevel.h +++ b/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h @@ -1,24 +1,24 @@ -#pragma once - -namespace Babylon::Integrations -{ - // Severity levels for the optional log callback on RuntimeOptions. - // - // The first three (Log / Warn / Error) mirror - // `Babylon::Polyfills::Console::LogLevel` and are used for - // `console.log` / `console.warn` / `console.error` calls and for - // `Babylon::DebugTrace` output. - // - // `Fatal` is used for **uncaught** JavaScript exceptions that - // propagated past every JS-side handler. The engine state may be - // inconsistent after a Fatal; a host that wants to terminate the - // process on uncaught errors can do so from inside its log - // callback (e.g. `if (level == LogLevel::Fatal) std::quick_exit(1);`). - enum class LogLevel - { - Log, - Warn, - Error, - Fatal, - }; -} +#pragma once + +namespace Babylon::Integrations +{ + // Severity levels for the optional log callback on RuntimeOptions. + // + // The first three (Log / Warn / Error) mirror + // `Babylon::Polyfills::Console::LogLevel` and are used for + // `console.log` / `console.warn` / `console.error` calls and for + // `Babylon::DebugTrace` output. + // + // `Fatal` is used for **uncaught** JavaScript exceptions that + // propagated past every JS-side handler. The engine state may be + // inconsistent after a Fatal; a host that wants to terminate the + // process on uncaught errors can do so from inside its log + // callback (e.g. `if (level == LogLevel::Fatal) std::quick_exit(1);`). + enum class LogLevel + { + Log, + Warn, + Error, + Fatal, + }; +} \ No newline at end of file diff --git a/Integrations/Include/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h similarity index 97% rename from Integrations/Include/Babylon/Integrations/Runtime.h rename to Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 906a08038..14429bb77 100644 --- a/Integrations/Include/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -1,124 +1,124 @@ -#pragma once - -#include - -#include - -#include -#include -#include - -namespace Babylon::Integrations -{ - class View; - struct RuntimeImpl; - - // Long-lived: typically created once per app/process. Sets up the - // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU - // polyfills/plugins. Construction is cheap and synchronous — - // no GPU device exists yet. Device construction and GPU plugin - // initialization (NativeEngine, etc.) are deferred to the first - // `View::Attach` call. - // - // See SimplifiedAPI.md §4.1 for the full design. - class Runtime - { - public: - static std::unique_ptr Create(RuntimeOptions options = {}); - - // // Future construction mode — adopt a host-owned Babylon::JsRuntime - // // instead of letting Runtime construct its own AppRuntime+JsRuntime. - // // Intended for hosts that already own a JS engine and want - // // Babylon Native plugins to live inside it (e.g. React Native: - // // Hermes/JSC + CallInvoker dispatcher). The Integrations layer - // // never sees JSI directly — only Babylon::JsRuntime, which the - // // host wires up against whatever JS engine they have. - // // - // // In Attach mode `~Runtime` does NOT tear down the JS engine - // // (the host owns it); Suspend/Resume only DisableRendering on - // // the Device since the JS thread isn't ours to pause. Same - // // instance API as Create-mode otherwise. See SimplifiedAPI.md - // // §4.1 "Construction modes". - // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, - // RuntimeOptions options = {}); - - ~Runtime(); - - // Non-copyable, non-movable (Views hold raw pointers back to this). - Runtime(const Runtime&) = delete; - Runtime& operator=(const Runtime&) = delete; - Runtime(Runtime&&) = delete; - Runtime& operator=(Runtime&&) = delete; - - // ----- JS interaction ----- - // - // Calls made before the first `View::Attach` are queued internally - // and dispatched onto the JS thread after engine initialization - // completes during that first Attach. Calls made after the first - // Attach are dispatched immediately. - // - // Threading: these methods are NOT internally synchronized. - // Hosts should call them from a single thread (typically the - // host's UI/main thread), matching the existing contract of - // `Babylon::ScriptLoader` and `Babylon::AppRuntime::Dispatch`. - void LoadScript(std::string_view url); - void Eval(std::string_view source, std::string_view sourceUrl = {}); - - // Escape hatch: post `callback` onto the JS thread. The callback - // runs after any pending init has completed. Useful for installing - // custom Napi globals, registering ObjectWrap classes, capturing - // `Napi::FunctionReference`s for native→JS calls, etc. - // - // Threading: same single-thread contract as LoadScript / Eval. - void RunOnJsThread(std::function callback); - - // ----- Suspend / Resume ----- - // - // Orthogonal to view attachment. Use when the host app is - // backgrounded, throttled, or otherwise should not be doing work - // (iOS applicationWillResignActive, Android onPause, modal - // dialogs, power-saving mode). While suspended: - // - JS timers (setTimeout/setInterval) pause. - // - In-flight microtasks complete; no new tasks are dispatched. - // - Any attached View becomes a no-op for RenderFrame() — the - // host can keep calling it from its draw callback; nothing - // happens until Resume(). - // Calls are reference-counted; nesting is safe. - // - // Safe to call from any thread. - void Suspend(); - void Resume(); - bool IsSuspended() const; - -#if BABYLON_NATIVE_PLUGIN_NATIVEXR - // ----- XR session control ----- - // - // Set the platform window XR will render into. The `void*` - // type carries: - // Android : ANativeWindow* (typically from a separate - // transparent SurfaceView overlay) - // Apple : CAMetalLayer* / MTKView* (a separate Metal layer - // distinct from the main View's layer) - // - // Pass nullptr to clear the XR surface. Safe to call before - // the first `View::Attach`; the supplied window is applied - // when NativeXr finishes initializing during that first Attach. - // Safe to call from any thread. - void SetXrWindow(void* nativeWindow); - - // True while an XR session is active. Updated from the JS - // thread by NativeXr's internal session-state callback; - // atomic so it can be polled from any thread (e.g. a host's - // draw callback choosing between rendering targets). - bool IsXrActive() const; -#endif - - private: - friend class View; - friend struct ViewImpl; - - Runtime(); - - std::unique_ptr m_impl; - }; -} +#pragma once + +#include + +#include + +#include +#include +#include + +namespace Babylon::Integrations +{ + class View; + struct RuntimeImpl; + + // Long-lived: typically created once per app/process. Sets up the + // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU + // polyfills/plugins. Construction is cheap and synchronous — + // no GPU device exists yet. Device construction and GPU plugin + // initialization (NativeEngine, etc.) are deferred to the first + // `View::Attach` call. + // + // See SimplifiedAPI.md §4.1 for the full design. + class Runtime + { + public: + static std::unique_ptr Create(RuntimeOptions options = {}); + + // // Future construction mode — adopt a host-owned Babylon::JsRuntime + // // instead of letting Runtime construct its own AppRuntime+JsRuntime. + // // Intended for hosts that already own a JS engine and want + // // Babylon Native plugins to live inside it (e.g. React Native: + // // Hermes/JSC + CallInvoker dispatcher). The Integrations layer + // // never sees JSI directly — only Babylon::JsRuntime, which the + // // host wires up against whatever JS engine they have. + // // + // // In Attach mode `~Runtime` does NOT tear down the JS engine + // // (the host owns it); Suspend/Resume only DisableRendering on + // // the Device since the JS thread isn't ours to pause. Same + // // instance API as Create-mode otherwise. See SimplifiedAPI.md + // // §4.1 "Construction modes". + // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, + // RuntimeOptions options = {}); + + ~Runtime(); + + // Non-copyable, non-movable (Views hold raw pointers back to this). + Runtime(const Runtime&) = delete; + Runtime& operator=(const Runtime&) = delete; + Runtime(Runtime&&) = delete; + Runtime& operator=(Runtime&&) = delete; + + // ----- JS interaction ----- + // + // Calls made before the first `View::Attach` are queued internally + // and dispatched onto the JS thread after engine initialization + // completes during that first Attach. Calls made after the first + // Attach are dispatched immediately. + // + // Threading: these methods are NOT internally synchronized. + // Hosts should call them from a single thread (typically the + // host's UI/main thread), matching the existing contract of + // `Babylon::ScriptLoader` and `Babylon::AppRuntime::Dispatch`. + void LoadScript(std::string_view url); + void Eval(std::string_view source, std::string_view sourceUrl = {}); + + // Escape hatch: post `callback` onto the JS thread. The callback + // runs after any pending init has completed. Useful for installing + // custom Napi globals, registering ObjectWrap classes, capturing + // `Napi::FunctionReference`s for native→JS calls, etc. + // + // Threading: same single-thread contract as LoadScript / Eval. + void RunOnJsThread(std::function callback); + + // ----- Suspend / Resume ----- + // + // Orthogonal to view attachment. Use when the host app is + // backgrounded, throttled, or otherwise should not be doing work + // (iOS applicationWillResignActive, Android onPause, modal + // dialogs, power-saving mode). While suspended: + // - JS timers (setTimeout/setInterval) pause. + // - In-flight microtasks complete; no new tasks are dispatched. + // - Any attached View becomes a no-op for RenderFrame() — the + // host can keep calling it from its draw callback; nothing + // happens until Resume(). + // Calls are reference-counted; nesting is safe. + // + // Safe to call from any thread. + void Suspend(); + void Resume(); + bool IsSuspended() const; + +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // ----- XR session control ----- + // + // Set the platform window XR will render into. The `void*` + // type carries: + // Android : ANativeWindow* (typically from a separate + // transparent SurfaceView overlay) + // Apple : CAMetalLayer* / MTKView* (a separate Metal layer + // distinct from the main View's layer) + // + // Pass nullptr to clear the XR surface. Safe to call before + // the first `View::Attach`; the supplied window is applied + // when NativeXr finishes initializing during that first Attach. + // Safe to call from any thread. + void SetXrWindow(void* nativeWindow); + + // True while an XR session is active. Updated from the JS + // thread by NativeXr's internal session-state callback; + // atomic so it can be polled from any thread (e.g. a host's + // draw callback choosing between rendering targets). + bool IsXrActive() const; +#endif + + private: + friend class View; + friend struct ViewImpl; + + Runtime(); + + std::unique_ptr m_impl; + }; +} \ No newline at end of file diff --git a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h similarity index 97% rename from Integrations/Include/Babylon/Integrations/RuntimeOptions.h rename to Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h index 4dc5a6acb..be8eb10a1 100644 --- a/Integrations/Include/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h @@ -1,55 +1,55 @@ -#pragma once - -#include - -#include -#include -#include -#include - -namespace Babylon::Integrations -{ - struct RuntimeOptions - { - // MSAA sample count for the back buffer. Valid values: 0, 2, 4, 8, 16. - // Anything else disables MSAA. - uint8_t msaaSamples{4}; - - // Enable the JavaScript debugger. Only implemented for V8 and Chakra. - bool enableDebugger{false}; - - // Block engine startup until a debugger has attached. Only - // implemented for V8. - bool waitForDebugger{false}; - - // Optional log sink. Receives: - // - `console.{log,warn,error}` output → LogLevel::{Log,Warn,Error} - // - `Babylon::DebugTrace` output → LogLevel::Log - // - Uncaught JS exceptions → LogLevel::Fatal - // - // If unset, ordinary log output is silently discarded and - // uncaught exceptions fall back to - // `Babylon::AppRuntime::DefaultUnhandledExceptionHandler` - // (which writes to the program output). - // - // Hosts that want process termination on uncaught exceptions - // (matching the historical AppContext behavior) can do so from - // inside this callback, e.g. - // - // if (level == LogLevel::Fatal) std::quick_exit(1); - std::function log; - -#if BABYLON_NATIVE_PLUGIN_SHADERCACHE - // Optional path for persisting the GPU shader cache across - // sessions. If non-empty: - // - Loaded synchronously during the first `View::Attach` (after - // `ShaderCache::Enable`). Missing or unreadable file: ignored. - // - Saved asynchronously during `Runtime::Suspend` (queued onto - // the JS thread before the suspension blocker) so the - // on-disk cache reflects any shaders compiled this session. - // - Saved synchronously during `~Runtime` so a final write - // happens before the JS thread is torn down. - std::string shaderCachePath; -#endif - }; -} +#pragma once + +#include + +#include +#include +#include +#include + +namespace Babylon::Integrations +{ + struct RuntimeOptions + { + // MSAA sample count for the back buffer. Valid values: 0, 2, 4, 8, 16. + // Anything else disables MSAA. + uint8_t msaaSamples{4}; + + // Enable the JavaScript debugger. Only implemented for V8 and Chakra. + bool enableDebugger{false}; + + // Block engine startup until a debugger has attached. Only + // implemented for V8. + bool waitForDebugger{false}; + + // Optional log sink. Receives: + // - `console.{log,warn,error}` output → LogLevel::{Log,Warn,Error} + // - `Babylon::DebugTrace` output → LogLevel::Log + // - Uncaught JS exceptions → LogLevel::Fatal + // + // If unset, ordinary log output is silently discarded and + // uncaught exceptions fall back to + // `Babylon::AppRuntime::DefaultUnhandledExceptionHandler` + // (which writes to the program output). + // + // Hosts that want process termination on uncaught exceptions + // (matching the historical AppContext behavior) can do so from + // inside this callback, e.g. + // + // if (level == LogLevel::Fatal) std::quick_exit(1); + std::function log; + +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + // Optional path for persisting the GPU shader cache across + // sessions. If non-empty: + // - Loaded synchronously during the first `View::Attach` (after + // `ShaderCache::Enable`). Missing or unreadable file: ignored. + // - Saved asynchronously during `Runtime::Suspend` (queued onto + // the JS thread before the suspension blocker) so the + // on-disk cache reflects any shaders compiled this session. + // - Saved synchronously during `~Runtime` so a final write + // happens before the JS thread is torn down. + std::string shaderCachePath; +#endif + }; +} \ No newline at end of file diff --git a/Integrations/Include/Babylon/Integrations/View.h b/Integrations/Include/Shared/Babylon/Integrations/View.h similarity index 97% rename from Integrations/Include/Babylon/Integrations/View.h rename to Integrations/Include/Shared/Babylon/Integrations/View.h index 679c60b39..27afa9141 100644 --- a/Integrations/Include/Babylon/Integrations/View.h +++ b/Integrations/Include/Shared/Babylon/Integrations/View.h @@ -1,169 +1,169 @@ -#pragma once - -#include - -#include -#include - -namespace Babylon::Integrations -{ - class Runtime; - struct ViewImpl; - - // Tag for the units of coordinates and dimensions handed to View - // methods. Babylon.js consumes pointer events and render-target - // sizes in **logical** (CSS / DIP) pixels; the View internally - // divides by the Device's queried device-pixel-ratio when given - // values in **physical** (surface pixel-buffer) units, so hosts - // pass through whatever their platform's native event system / - // surface-size API delivers. - // - // Examples: - // Physical: Android `MotionEvent.getX/getY` / - // `ANativeWindow_getWidth/Height`, - // Apple `CAMetalLayer.drawableSize`, - // Win32 `GetClientRect` / `WM_POINTER*` (DPI-aware), - // X11 button events / `XGetGeometry`. - // Logical: iOS `UITouch.location`, - // macOS `NSEvent.locationInWindow`, - // UWP `PointerPoint.Position` / `CoreWindow.Bounds`, - // host code that has already done its own DPR divide. - enum class CoordinateUnits - { - Physical, - Logical, - }; - - // Transient: created when a host surface appears, destroyed when - // it goes away. Multiple sequential Views may be attached to the - // same Runtime over its lifetime. **At most one View may be attached - // at a time** — to switch surfaces, destroy the current View and - // construct a new one. - // - // See SimplifiedAPI.md §4.1 for the full design. - class View - { - public: - // Attach `nativeWindow` (the platform-specific surface handle) - // to `runtime`. - // - // `nativeWindow` is `Babylon::Graphics::WindowT`, the same - // per-platform typedef the Graphics layer already uses - // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* - // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). - // The View queries the surface's pixel-buffer size from the - // window itself via `ViewImpl::QuerySize`, which returns the - // platform-natural unit; the View internally converts to - // logical pixels before configuring the Device. - // - // The first Attach on a given Runtime is the heavy step: it - // constructs `Babylon::Graphics::Device`, dispatches GPU plugin - // initialization (`Device::AddToJavaScript`, - // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, - // ...), and flushes any scripts queued via `Runtime::LoadScript` - // before this point. Opens the first frame. - // - // Subsequent Attach calls on the same Runtime are cheap: the - // Device is already constructed, plugins are initialized, the - // JS engine is running. They just call `Device::UpdateWindow` + - // `Device::EnableRendering` to bind the new surface, then open - // the first frame for the new attachment. - // - // Detach (`~View`) closes the in-flight frame and calls - // `Device::DisableRendering`. The Device persists on the - // Runtime, so the next Attach is fast. - // - // Must be called from the same thread that will call - // `RenderFrame` and `Resize` (the "frame thread"). - static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow); - - ~View(); - - View(const View&) = delete; - View& operator=(const View&) = delete; - View(View&&) = delete; - View& operator=(View&&) = delete; - - // Render exactly one frame. Must be called from the same thread - // as `Attach` and `Resize` (the frame thread). No-op if the - // runtime is suspended. The host calls this from the platform - // view/control's existing draw callback (WM_PAINT on Win32, - // MTKViewDelegate::draw(in:) on Apple, View.onDraw on Android, - // etc. — see SimplifiedAPI.md §4.1 "How frames actually get - // rendered"). - void RenderFrame(); - - // Resize the bound surface. The View converts physical → - // logical internally if `units == CoordinateUnits::Physical`, - // so the host can pass whatever its platform's view layer - // reports without doing the DPR divide. - // - // Examples: - // Android `View.onSizeChanged(w, h)` → Physical. - // iOS `MTKViewDelegate.drawableSizeWillChange:` → Physical. - // UWP `SizeChangedEventArgs.NewSize` (already in DIPs) → Logical. - // - // Must be called from the frame thread. - void Resize(uint32_t width, uint32_t height, CoordinateUnits units); - -#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - // ----- Pointer / mouse input forwarding ----- - // - // Host calls these from its event loop while the view exists. - // Routed to the JS thread via `NativeInput`, where Babylon.js - // consumes them as `PointerEvent.clientX/clientY` (logical / - // CSS pixels). The View converts physical → logical internally - // if `units == CoordinateUnits::Physical`. - // - // Pass coordinates in whatever unit your platform's native - // event system delivers: - // Physical: Android `MotionEvent.getX/getY`, - // Win32 `WM_POINTER*` / `WM_MOUSE*` (DPI-aware), - // X11 button events. - // Logical: iOS `UITouch.location`, - // macOS `NSEvent.locationInWindow`, - // UWP `PointerPoint.Position`. - // - // Babylon Native distinguishes pointer (touch) input from mouse - // input; both methods feed the same Babylon.js pointer-event - // pipeline but with different `pointerType` ('touch' vs. - // 'mouse'). Hosts driven by touch (Android, iOS) typically use - // OnPointer*; hosts driven by a cursor (Win32, macOS, UWP, X11) - // typically use OnMouse*. - // - // Babylon Native does not currently expose keyboard input; hosts - // that need keyboard handling do it at the platform level and - // forward into JS via `Runtime::RunOnJsThread`. - // - // Safe to call from any thread. - - // Touch / pointer events. - void OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units); - void OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units); - void OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units); - - // Mouse events. `buttonIndex` is one of LeftMouseButton(), - // MiddleMouseButton(), RightMouseButton(); `wheelAxis` is - // MouseWheelY(). The accessors return the matching - // `Babylon::Plugins::NativeInput::*_ID` value (single source of - // truth — no duplication, no risk of drift) without exposing the - // NativeInput header from this public View.h. - void OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units); - void OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units); - void OnMouseMove(float x, float y, CoordinateUnits units); - void OnMouseWheel(uint32_t wheelAxis, int32_t scrollValue); - - static uint32_t LeftMouseButton(); - static uint32_t MiddleMouseButton(); - static uint32_t RightMouseButton(); - static uint32_t MouseWheelY(); -#endif - - private: - friend class Runtime; - - std::unique_ptr m_impl; - - explicit View(std::unique_ptr impl); - }; -} +#pragma once + +#include + +#include +#include + +namespace Babylon::Integrations +{ + class Runtime; + struct ViewImpl; + + // Tag for the units of coordinates and dimensions handed to View + // methods. Babylon.js consumes pointer events and render-target + // sizes in **logical** (CSS / DIP) pixels; the View internally + // divides by the Device's queried device-pixel-ratio when given + // values in **physical** (surface pixel-buffer) units, so hosts + // pass through whatever their platform's native event system / + // surface-size API delivers. + // + // Examples: + // Physical: Android `MotionEvent.getX/getY` / + // `ANativeWindow_getWidth/Height`, + // Apple `CAMetalLayer.drawableSize`, + // Win32 `GetClientRect` / `WM_POINTER*` (DPI-aware), + // X11 button events / `XGetGeometry`. + // Logical: iOS `UITouch.location`, + // macOS `NSEvent.locationInWindow`, + // UWP `PointerPoint.Position` / `CoreWindow.Bounds`, + // host code that has already done its own DPR divide. + enum class CoordinateUnits + { + Physical, + Logical, + }; + + // Transient: created when a host surface appears, destroyed when + // it goes away. Multiple sequential Views may be attached to the + // same Runtime over its lifetime. **At most one View may be attached + // at a time** — to switch surfaces, destroy the current View and + // construct a new one. + // + // See SimplifiedAPI.md §4.1 for the full design. + class View + { + public: + // Attach `nativeWindow` (the platform-specific surface handle) + // to `runtime`. + // + // `nativeWindow` is `Babylon::Graphics::WindowT`, the same + // per-platform typedef the Graphics layer already uses + // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* + // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). + // The View queries the surface's pixel-buffer size from the + // window itself via `ViewImpl::QuerySize`, which returns the + // platform-natural unit; the View internally converts to + // logical pixels before configuring the Device. + // + // The first Attach on a given Runtime is the heavy step: it + // constructs `Babylon::Graphics::Device`, dispatches GPU plugin + // initialization (`Device::AddToJavaScript`, + // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, + // ...), and flushes any scripts queued via `Runtime::LoadScript` + // before this point. Opens the first frame. + // + // Subsequent Attach calls on the same Runtime are cheap: the + // Device is already constructed, plugins are initialized, the + // JS engine is running. They just call `Device::UpdateWindow` + + // `Device::EnableRendering` to bind the new surface, then open + // the first frame for the new attachment. + // + // Detach (`~View`) closes the in-flight frame and calls + // `Device::DisableRendering`. The Device persists on the + // Runtime, so the next Attach is fast. + // + // Must be called from the same thread that will call + // `RenderFrame` and `Resize` (the "frame thread"). + static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow); + + ~View(); + + View(const View&) = delete; + View& operator=(const View&) = delete; + View(View&&) = delete; + View& operator=(View&&) = delete; + + // Render exactly one frame. Must be called from the same thread + // as `Attach` and `Resize` (the frame thread). No-op if the + // runtime is suspended. The host calls this from the platform + // view/control's existing draw callback (WM_PAINT on Win32, + // MTKViewDelegate::draw(in:) on Apple, View.onDraw on Android, + // etc. — see SimplifiedAPI.md §4.1 "How frames actually get + // rendered"). + void RenderFrame(); + + // Resize the bound surface. The View converts physical → + // logical internally if `units == CoordinateUnits::Physical`, + // so the host can pass whatever its platform's view layer + // reports without doing the DPR divide. + // + // Examples: + // Android `View.onSizeChanged(w, h)` → Physical. + // iOS `MTKViewDelegate.drawableSizeWillChange:` → Physical. + // UWP `SizeChangedEventArgs.NewSize` (already in DIPs) → Logical. + // + // Must be called from the frame thread. + void Resize(uint32_t width, uint32_t height, CoordinateUnits units); + +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + // ----- Pointer / mouse input forwarding ----- + // + // Host calls these from its event loop while the view exists. + // Routed to the JS thread via `NativeInput`, where Babylon.js + // consumes them as `PointerEvent.clientX/clientY` (logical / + // CSS pixels). The View converts physical → logical internally + // if `units == CoordinateUnits::Physical`. + // + // Pass coordinates in whatever unit your platform's native + // event system delivers: + // Physical: Android `MotionEvent.getX/getY`, + // Win32 `WM_POINTER*` / `WM_MOUSE*` (DPI-aware), + // X11 button events. + // Logical: iOS `UITouch.location`, + // macOS `NSEvent.locationInWindow`, + // UWP `PointerPoint.Position`. + // + // Babylon Native distinguishes pointer (touch) input from mouse + // input; both methods feed the same Babylon.js pointer-event + // pipeline but with different `pointerType` ('touch' vs. + // 'mouse'). Hosts driven by touch (Android, iOS) typically use + // OnPointer*; hosts driven by a cursor (Win32, macOS, UWP, X11) + // typically use OnMouse*. + // + // Babylon Native does not currently expose keyboard input; hosts + // that need keyboard handling do it at the platform level and + // forward into JS via `Runtime::RunOnJsThread`. + // + // Safe to call from any thread. + + // Touch / pointer events. + void OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units); + void OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units); + void OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units); + + // Mouse events. `buttonIndex` is one of LeftMouseButton(), + // MiddleMouseButton(), RightMouseButton(); `wheelAxis` is + // MouseWheelY(). The accessors return the matching + // `Babylon::Plugins::NativeInput::*_ID` value (single source of + // truth — no duplication, no risk of drift) without exposing the + // NativeInput header from this public View.h. + void OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units); + void OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units); + void OnMouseMove(float x, float y, CoordinateUnits units); + void OnMouseWheel(uint32_t wheelAxis, int32_t scrollValue); + + static uint32_t LeftMouseButton(); + static uint32_t MiddleMouseButton(); + static uint32_t RightMouseButton(); + static uint32_t MouseWheelY(); +#endif + + private: + friend class Runtime; + + std::unique_ptr m_impl; + + explicit View(std::unique_ptr impl); + }; +} \ No newline at end of file diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 7d51f1d00..a1041ec64 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include namespace Babylon::Integrations { @@ -33,6 +35,14 @@ namespace Babylon::Integrations } return {x / dpr, y / dpr}; } + + void ValidateNonZeroSize(uint32_t width, uint32_t height, const char* operation) + { + if (width == 0 || height == 0) + { + throw std::runtime_error{std::string{operation} + " requires non-zero width and height."}; + } + } } // --------------------------------------------------------------------- @@ -58,9 +68,12 @@ namespace Babylon::Integrations // Device doesn't exist yet, so we go through the standalone // `Babylon::Graphics::GetDevicePixelRatio(window)` free function. const auto querySize = ViewImpl::QuerySize(nativeWindow); + ValidateNonZeroSize(querySize.Width, querySize.Height, "View::Attach native window size"); + const float dpr = Babylon::Graphics::GetDevicePixelRatio(nativeWindow); const auto [logicalW, logicalH] = ToLogicalSize( querySize.Width, querySize.Height, querySize.Units, dpr); + ValidateNonZeroSize(logicalW, logicalH, "View::Attach logical size"); if (firstAttach) { @@ -194,11 +207,14 @@ namespace Babylon::Integrations void View::Resize(uint32_t width, uint32_t height, CoordinateUnits units) { + ValidateNonZeroSize(width, height, "View::Resize size"); + RuntimeImpl& impl = *m_impl->m_runtime.m_impl; if (impl.m_device) { const auto [lw, lh] = ToLogicalSize(width, height, units, impl.m_device->GetDevicePixelRatio()); + ValidateNonZeroSize(lw, lh, "View::Resize logical size"); impl.m_device->UpdateSize(lw, lh); } } From 51f9f88c1dc1a7487baa7cd4bff047b6770da0be Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 08:37:05 -0700 Subject: [PATCH 34/71] Add infinity check --- Integrations/Apple/Source/BNView.mm | 32 +++++++++++++++---- Integrations/Apple/Source/BNViewDelegate.mm | 12 ++++++- .../Apple/Babylon/Integrations/Apple/BNView.h | 7 ++-- Integrations/Source/ViewImpl_iOS.mm | 14 ++++++++ Integrations/Source/ViewImpl_macOS.mm | 14 ++++++++ 5 files changed, 68 insertions(+), 11 deletions(-) diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index ca18c6aee..58a873413 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -10,9 +10,23 @@ #include #include +#include #include #include +namespace +{ + bool IsFinitePositive(CGFloat value) + { + return std::isfinite(static_cast(value)) && value > 0; + } + + bool IsFinitePositive(CGSize size) + { + return IsFinitePositive(size.width) && IsFinitePositive(size.height); + } +} + @implementation BNView { std::unique_ptr _view; @@ -46,12 +60,12 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view // drawableSize explicitly (for example, for hidden preload), // preserve it. const CGSize drawableSize = layer.drawableSize; - if (drawableSize.width <= 0 || drawableSize.height <= 0) + if (!IsFinitePositive(drawableSize)) { [view layoutIfNeeded]; const CGSize boundsSize = view.bounds.size; - if (boundsSize.width > 0 && boundsSize.height > 0) + if (IsFinitePositive(boundsSize)) { #if TARGET_OS_OSX CGFloat scale = view.window.backingScaleFactor > 0 @@ -60,21 +74,25 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view #else CGFloat scale = view.contentScaleFactor; #endif - if (scale <= 0) + if (!IsFinitePositive(scale)) { scale = 1.0; } - layer.drawableSize = CGSizeMake(boundsSize.width * scale, - boundsSize.height * scale); + const CGSize seededDrawableSize = CGSizeMake(boundsSize.width * scale, + boundsSize.height * scale); + if (IsFinitePositive(seededDrawableSize)) + { + layer.drawableSize = seededDrawableSize; + } } } const CGSize finalDrawableSize = layer.drawableSize; - if (finalDrawableSize.width <= 0 || finalDrawableSize.height <= 0) + if (!IsFinitePositive(finalDrawableSize)) { @throw [NSException exceptionWithName:@"BabylonNativeInvalidViewException" - reason:@"BNView requires a non-zero drawableSize or non-zero bounds before attach." + reason:@"BNView requires a finite, non-zero drawableSize or finite, non-zero bounds before attach." userInfo:nil]; } diff --git a/Integrations/Apple/Source/BNViewDelegate.mm b/Integrations/Apple/Source/BNViewDelegate.mm index b5606da21..6a07a357a 100644 --- a/Integrations/Apple/Source/BNViewDelegate.mm +++ b/Integrations/Apple/Source/BNViewDelegate.mm @@ -5,6 +5,16 @@ #import +#include + +namespace +{ + bool IsFinitePositive(CGFloat value) + { + return std::isfinite(static_cast(value)) && value > 0; + } +} + @implementation BNViewDelegate { // BNView holds the auto-installed delegate strongly, and @@ -32,7 +42,7 @@ - (instancetype)initWithView:(BNView*)view - (void)mtkView:(MTKView* __unused)v drawableSizeWillChange:(CGSize)size { - if (size.width <= 0 || size.height <= 0) + if (!IsFinitePositive(size.width) || !IsFinitePositive(size.height)) { return; } diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h index 4c9a16d00..61debc5ce 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h @@ -62,9 +62,10 @@ NS_ASSUME_NONNULL_BEGIN /// preconditions are met. /// /// Raises an `NSException` (name -/// `BabylonNativeInvalidViewException`) if `view` has neither a -/// non-zero `drawableSize` nor non-zero bounds at attach time. Hidden -/// preload views should set a small non-zero `drawableSize` explicitly. +/// `BabylonNativeInvalidViewException`) if `view` has neither a finite, +/// non-zero `drawableSize` nor finite, non-zero bounds at attach time. +/// Hidden preload views should set a small finite, non-zero `drawableSize` +/// explicitly. /// /// **Delegate management:** If `view.delegate` is `nil` at the time of /// construction, BNView creates a `BNViewDelegate` and assigns it to diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm index d598179b8..c5a55e0ec 100644 --- a/Integrations/Source/ViewImpl_iOS.mm +++ b/Integrations/Source/ViewImpl_iOS.mm @@ -1,7 +1,17 @@ #include "RuntimeImpl.h" +#include + #import +namespace +{ + bool IsFinitePositive(CGFloat value) + { + return std::isfinite(static_cast(value)) && value > 0; + } +} + namespace Babylon::Integrations { ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) @@ -14,6 +24,10 @@ // CAMetalLayer*; drawableSize is in physical pixels. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; const CGSize size = layer.drawableSize; + if (!IsFinitePositive(size.width) || !IsFinitePositive(size.height)) + { + return {0, 0, CoordinateUnits::Physical}; + } return {static_cast(size.width), static_cast(size.height), CoordinateUnits::Physical}; diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm index 83461e653..f547b34c9 100644 --- a/Integrations/Source/ViewImpl_macOS.mm +++ b/Integrations/Source/ViewImpl_macOS.mm @@ -1,7 +1,17 @@ #include "RuntimeImpl.h" +#include + #import +namespace +{ + bool IsFinitePositive(CGFloat value) + { + return std::isfinite(static_cast(value)) && value > 0; + } +} + namespace Babylon::Integrations { ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) @@ -13,6 +23,10 @@ // CAMetalLayer.drawableSize is in physical pixels. CAMetalLayer* layer = (__bridge CAMetalLayer*)window; const CGSize size = layer.drawableSize; + if (!IsFinitePositive(size.width) || !IsFinitePositive(size.height)) + { + return {0, 0, CoordinateUnits::Physical}; + } return {static_cast(size.width), static_cast(size.height), CoordinateUnits::Physical}; From 93f2d3c270bab72f76a0dda00081eb9c199b494a Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 14:36:50 -0700 Subject: [PATCH 35/71] Add options object at platform interop layers Add options to Playground::Initialize Update win32 playground to pass options from cmd options (parity with master AppContext) --- .../Android/BabylonNative/CMakeLists.txt | 2 + .../Android/BabylonNative/consumer-rules.pro | 2 + .../babylonjs/integrations/BabylonNative.java | 58 ++++-- .../library/babylonnative/BabylonView.java | 2 +- .../playground/PlaygroundActivity.java | 5 +- Apps/Playground/Shared/PlaygroundScripts.cpp | 17 +- Apps/Playground/Shared/PlaygroundScripts.h | 11 +- Apps/Playground/Win32/App.cpp | 29 +-- Apps/Playground/iOS/AppDelegate.swift | 5 +- .../main/cpp/BabylonNativeIntegrations.cpp | 136 +++++++++--- Integrations/Apple/Source/BNRuntime.mm | 39 +++- .../Babylon/Integrations/Apple/BNRuntime.h | 197 ++++++++++-------- .../Babylon/Integrations/RuntimeOptions.h | 7 +- Integrations/Source/Runtime.cpp | 5 +- 14 files changed, 323 insertions(+), 192 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/CMakeLists.txt b/Apps/Playground/Android/BabylonNative/CMakeLists.txt index 0fff99eae..bed3a71c5 100644 --- a/Apps/Playground/Android/BabylonNative/CMakeLists.txt +++ b/Apps/Playground/Android/BabylonNative/CMakeLists.txt @@ -26,10 +26,12 @@ add_subdirectory(${REPO_ROOT_DIR} "${CMAKE_CURRENT_BINARY_DIR}/BabylonNative") # can be safely consumed by another. target_sources(BabylonNativeIntegrations PRIVATE src/main/cpp/PlaygroundJNI.cpp + ${PLAYGROUND_DIR}/Shared/Diagnostics.cpp ${PLAYGROUND_DIR}/Shared/PlaygroundScripts.cpp) target_include_directories(BabylonNativeIntegrations PRIVATE ${PLAYGROUND_DIR}) target_link_libraries(BabylonNativeIntegrations + PRIVATE bx PRIVATE Foundation) # for in PlaygroundScripts.cpp diff --git a/Apps/Playground/Android/BabylonNative/consumer-rules.pro b/Apps/Playground/Android/BabylonNative/consumer-rules.pro index e69de29bb..7fe9a4ec7 100644 --- a/Apps/Playground/Android/BabylonNative/consumer-rules.pro +++ b/Apps/Playground/Android/BabylonNative/consumer-rules.pro @@ -0,0 +1,2 @@ +-keep class com.babylonjs.integrations.BabylonNative { *; } +-keep class com.babylonjs.integrations.BabylonNative$RuntimeOptions { *; } diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index cdda34fb2..5a0124b82 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -16,16 +16,40 @@ *

    *
  1. Call {@link #androidGlobalInitialize(Context)} once at app startup * (typically from {@code Application.onCreate}).
  2. - *
  3. Create a Runtime via {@link #runtimeCreate(boolean)} and remember + *
  4. Create a Runtime via {@link #runtimeCreate()} or {@link #runtimeCreate(RuntimeOptions)} and remember * the returned {@code long} handle.
  5. *
  6. Optional: queue scripts via {@link #runtimeLoadScript(long, String)} - * — they run after the first {@link #viewAttach(long, Surface, int, int, float)}.
  7. - *
  8. Attach a View via {@link #viewAttach(long, Surface, int, int, float)}; + * — they run after the first {@link #viewAttach(long, Surface)}.
  9. + *
  10. Attach a View via {@link #viewAttach(long, Surface)}; * call {@link #viewRenderFrame(long)} from your draw loop.
  11. *
  12. Tear down with {@link #viewDetach(long)} then {@link #runtimeDestroy(long)}.
  13. *
*/ public final class BabylonNative { + /** + * Construction options for {@link BabylonNative#runtimeCreate(RuntimeOptions)}. + * + *

The defaults match the C++ {@code Babylon::Integrations::RuntimeOptions} + * defaults. Fields are intentionally public so Java and Kotlin callers can use + * simple object-initializer patterns. + */ + public static final class RuntimeOptions { + /** Optional MSAA sample count for the back buffer. Valid values are 0, 2, 4, 8, and 16. */ + public Integer msaaSamples = null; + + /** Enable the JavaScript debugger when supported by the configured JS engine. Defaults to false. */ + public boolean enableDebugger = false; + + /** Enable Babylon::DebugTrace output through the default logcat sink. Defaults to false. */ + public boolean enableDebugTrace = false; + + /** Block engine startup until a debugger attaches when supported by the configured JS engine. Defaults to false. */ + public boolean waitForDebugger = false; + + /** Optional writable file path for a persistent on-disk GPU shader cache. */ + public String shaderCachePath = null; + } + static { System.loadLibrary("BabylonNativeIntegrations"); } @@ -60,26 +84,24 @@ public static native void requestPermissionsResult( // ------------------------------------------------------------------- /** Returns an opaque handle owned by the caller; release with {@link #runtimeDestroy(long)}. */ - public static native long runtimeCreate(boolean enableDebugger); + public static native long runtimeCreate(); /** - * Overload that wires up a persistent on-disk GPU shader cache. The - * cache is loaded on first {@link #viewAttach(long, Surface)} and - * saved on suspend and on {@link #runtimeDestroy(long)}. + * Returns an opaque handle owned by the caller; release with {@link #runtimeDestroy(long)}. * - *

Pass a writable path (typically - * {@code context.getCacheDir() + "/babylon.shadercache"}). Pass - * {@code null} to disable the on-disk cache (equivalent to the - * one-arg {@link #runtimeCreate(boolean)} overload). + *

Pass {@code null} to use the same defaults as {@link #runtimeCreate()}. + * If {@link RuntimeOptions#shaderCachePath} is non-null, the cache is loaded + * on first {@link #viewAttach(long, Surface)} and saved on suspend and on + * {@link #runtimeDestroy(long)}. * - *

If {@code shaderCachePath} is non-null but the native library - * was built without {@code BABYLON_NATIVE_PLUGIN_SHADERCACHE}, - * this method throws {@link IllegalStateException} so the - * misconfiguration surfaces at construction time rather than - * silently dropping the cache. Passing {@code null} is always - * safe regardless of native build config. + *

If {@link RuntimeOptions#shaderCachePath} is non-null but the native + * library was built without {@code BABYLON_NATIVE_PLUGIN_SHADERCACHE}, this + * method throws {@link IllegalStateException} so the misconfiguration surfaces + * at construction time rather than silently dropping the cache. Passing null + * options, or options with null {@code shaderCachePath}, is always safe + * regardless of native build config. */ - public static native long runtimeCreate(boolean enableDebugger, String shaderCachePath); + public static native long runtimeCreate(RuntimeOptions options); public static native void runtimeDestroy(long handle); diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 920ffff59..8ee06f765 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -13,7 +13,7 @@ /** * Playground View built on top of {@link BabylonNative}. Borrows a * Runtime handle from the host (the host is responsible for the - * Runtime's lifetime via {@link BabylonNative#runtimeCreate(boolean)} / + * Runtime's lifetime via {@link BabylonNative#runtimeCreate()} / * {@link BabylonNative#runtimeDestroy(long)}); this class only owns the * View handle, which mirrors the underlying Surface lifecycle: * attach in {@code surfaceCreated}, resize in {@code surfaceChanged}, diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 6ed35189e..3c18728bd 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -34,7 +34,10 @@ protected void onCreate(Bundle icicle) { // Owner of the Runtime lifetime: created here, destroyed in // onDestroy. The View only borrows the handle for its surface // bindings. - mRuntimeHandle = BabylonNative.runtimeCreate(/*enableDebugger*/ true); + BabylonNative.RuntimeOptions runtimeOptions = new BabylonNative.RuntimeOptions(); + runtimeOptions.enableDebugger = true; + runtimeOptions.enableDebugTrace = true; + mRuntimeHandle = BabylonNative.runtimeCreate(runtimeOptions); // Queue the Babylon.js bootstrap scripts, then the playground // experience script. Both happen synchronously from this thread; diff --git a/Apps/Playground/Shared/PlaygroundScripts.cpp b/Apps/Playground/Shared/PlaygroundScripts.cpp index e40784af9..bdb17f3e5 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.cpp +++ b/Apps/Playground/Shared/PlaygroundScripts.cpp @@ -5,11 +5,24 @@ namespace Playground { - void Initialize() + void Initialize(const PlaygroundOptions& options) { // Process-wide perf-tracing configuration. Used to be done // inside AppContext's constructor. - Babylon::PerfTrace::SetLevel(Babylon::PerfTrace::Level::Mark); + Babylon::PerfTrace::Level perfLevel{Babylon::PerfTrace::Level::Mark}; + if (options.PerfTrace.has_value()) + { + const auto& value = *options.PerfTrace; + if (value == "None" || value == "none") + { + perfLevel = Babylon::PerfTrace::Level::None; + } + else if (value == "Log" || value == "log" || value == "Detail" || value == "detail") + { + perfLevel = Babylon::PerfTrace::Level::Log; + } + } + Babylon::PerfTrace::SetLevel(perfLevel); } void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime) diff --git a/Apps/Playground/Shared/PlaygroundScripts.h b/Apps/Playground/Shared/PlaygroundScripts.h index beacc04ce..cc49624eb 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.h +++ b/Apps/Playground/Shared/PlaygroundScripts.h @@ -1,5 +1,7 @@ #pragma once +#include "CommandLine.h" + namespace Babylon::Integrations { class Runtime; @@ -8,12 +10,9 @@ namespace Babylon::Integrations namespace Playground { // Apply process-wide settings shared by every Playground host: - // currently `Babylon::PerfTrace::SetLevel(Mark)`. Call once per - // process, before constructing any `Babylon::Integrations::Runtime`. - // - // (DebugTrace setup is now handled by `RuntimeOptions::log` in the - // Integrations layer, so it doesn't need to live here.) - void Initialize(); + // currently PerfTrace. Call before queuing scripts and attaching + // the first view. + void Initialize(const PlaygroundOptions& options = {}); // Queue the standard Babylon.js bootstrap scripts (Babylon core, // loaders, materials, GUI, serializers, plus a few common extras) diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 5151dacc5..9a306c5af 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -7,10 +7,8 @@ #include "App.h" -#include #include #include -#include #include #include @@ -92,30 +90,11 @@ namespace return arguments; } - void ApplyTraceOptions() - { - Babylon::DebugTrace::EnableDebugTrace(options.DebugTrace.value_or(true)); - - Babylon::PerfTrace::Level perfLevel{Babylon::PerfTrace::Level::Mark}; - if (options.PerfTrace.has_value()) - { - const auto& value = *options.PerfTrace; - if (value == "None" || value == "none") - { - perfLevel = Babylon::PerfTrace::Level::None; - } - else if (value == "Log" || value == "log" || value == "Detail" || value == "detail") - { - perfLevel = Babylon::PerfTrace::Level::Log; - } - } - Babylon::PerfTrace::SetLevel(perfLevel); - } - Babylon::Integrations::RuntimeOptions MakeRuntimeOptions() { Babylon::Integrations::RuntimeOptions runtimeOptions{}; runtimeOptions.enableDebugger = true; // matches AppContext default + runtimeOptions.enableDebugTrace = options.DebugTrace.value_or(true); runtimeOptions.log = [](Babylon::Integrations::LogLevel level, std::string_view message) { std::string text{message}; while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) @@ -227,7 +206,7 @@ namespace Uninitialize(); g_runtime = Babylon::Integrations::Runtime::Create(MakeRuntimeOptions()); - ApplyTraceOptions(); + Playground::Initialize(options); QueuePlaygroundOptions(); LoadScripts(); @@ -291,10 +270,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, return 0; } - // Process-wide Playground setup (PerfTrace level, etc.). Shared - // with the other Playground hosts via Shared/PlaygroundScripts. - Playground::Initialize(); - // Initialize global strings LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadStringW(hInstance, IDC_PLAYGROUNDWIN32, szWindowClass, MAX_LOADSTRING); diff --git a/Apps/Playground/iOS/AppDelegate.swift b/Apps/Playground/iOS/AppDelegate.swift index d375df4b0..4694230db 100644 --- a/Apps/Playground/iOS/AppDelegate.swift +++ b/Apps/Playground/iOS/AppDelegate.swift @@ -11,7 +11,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var runtime: BNRuntime? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let runtime = BNRuntime(enableDebugger: true) + let runtimeOptions = BNRuntimeOptions() + runtimeOptions.enableDebugger = true + runtimeOptions.enableDebugTrace = true + let runtime = BNRuntime(options: runtimeOptions) // Queue the Babylon.js bootstrap scripts (shared with the other // Playground hosts via Apps/Playground/Shared/PlaygroundScripts.cpp), diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 0b79bec19..7a2d94b03 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -115,6 +115,102 @@ namespace } } + bool ApplyJavaRuntimeOptions(JNIEnv* env, jobject javaOptions, RuntimeOptions& options) + { + if (javaOptions == nullptr) + { + return true; + } + + jclass optionsClass = env->GetObjectClass(javaOptions); + if (optionsClass == nullptr) + { + return false; + } + + auto readBoolean = [&](const char* name, bool& target) { + jfieldID field = env->GetFieldID(optionsClass, name, "Z"); + if (field == nullptr) + { + return false; + } + target = env->GetBooleanField(javaOptions, field) == JNI_TRUE; + return true; + }; + + if (!readBoolean("enableDebugger", options.enableDebugger) || + !readBoolean("enableDebugTrace", options.enableDebugTrace) || + !readBoolean("waitForDebugger", options.waitForDebugger)) + { + env->DeleteLocalRef(optionsClass); + return false; + } + + jfieldID msaaSamplesField = env->GetFieldID(optionsClass, "msaaSamples", "Ljava/lang/Integer;"); + if (msaaSamplesField == nullptr) + { + env->DeleteLocalRef(optionsClass); + return false; + } + + jobject msaaSamples = env->GetObjectField(javaOptions, msaaSamplesField); + if (msaaSamples != nullptr) + { + jclass integerClass = env->GetObjectClass(msaaSamples); + if (integerClass == nullptr) + { + env->DeleteLocalRef(msaaSamples); + env->DeleteLocalRef(optionsClass); + return false; + } + + jmethodID intValue = env->GetMethodID(integerClass, "intValue", "()I"); + if (intValue == nullptr) + { + env->DeleteLocalRef(integerClass); + env->DeleteLocalRef(msaaSamples); + env->DeleteLocalRef(optionsClass); + return false; + } + + const jint value = env->CallIntMethod(msaaSamples, intValue); + options.msaaSamples = value >= 0 && value <= 255 ? static_cast(value) : 0; + env->DeleteLocalRef(integerClass); + env->DeleteLocalRef(msaaSamples); + } + + jfieldID shaderCachePathField = env->GetFieldID(optionsClass, "shaderCachePath", "Ljava/lang/String;"); + if (shaderCachePathField == nullptr) + { + env->DeleteLocalRef(optionsClass); + return false; + } + + auto shaderCachePath = static_cast(env->GetObjectField(javaOptions, shaderCachePathField)); + if (shaderCachePath != nullptr) + { +#if BABYLON_NATIVE_PLUGIN_SHADERCACHE + options.shaderCachePath = ToStdString(env, shaderCachePath); + env->DeleteLocalRef(shaderCachePath); + if (env->ExceptionCheck()) + { + env->DeleteLocalRef(optionsClass); + return false; + } +#else + env->DeleteLocalRef(shaderCachePath); + env->DeleteLocalRef(optionsClass); + ThrowPluginNotEnabled(env, + "shaderCachePath was provided but BABYLON_NATIVE_PLUGIN_SHADERCACHE " + "was not enabled at native build time."); + return false; +#endif + } + + env->DeleteLocalRef(optionsClass); + return true; + } + // Shared body for the `runtimeCreate` JNI overloads. Wires up the // Android-default logcat log sink, constructs the Runtime, attaches // the Activity-lifecycle auto-Suspend/Resume tickets, and returns @@ -256,48 +352,26 @@ Java_com_babylonjs_integrations_BabylonNative_requestPermissionsResult( // ===================================================================== JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Z(JNIEnv*, jclass, jboolean enableDebugger) +Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__(JNIEnv*, jclass) { // Default Android consumers want logcat output; route Console - // polyfill / DebugTrace / uncaught JS exceptions there. Hosts - // that need different behavior can construct a Runtime in C++ - // directly with their own RuntimeOptions. - // - // Mangled name `__Z` (boolean) is the JNI long form, required - // because Java declares `runtimeCreate` as an overloaded native - // method (see the SHADERCACHE-gated overload below). + // polyfill output and uncaught JS exceptions there. DebugTrace is routed + // there when enabled by RuntimeOptions. Hosts that need different behavior + // can construct a Runtime in C++ directly with their own RuntimeOptions. RuntimeOptions options{}; - options.enableDebugger = (enableDebugger == JNI_TRUE); return MakeRuntimeHandle(std::move(options)); } JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__ZLjava_lang_String_2( - JNIEnv* env, jclass, jboolean enableDebugger, jstring shaderCachePath) +Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Lcom_babylonjs_integrations_BabylonNative_00024RuntimeOptions_2( + JNIEnv* env, jclass, jobject javaOptions) { - // Overload that wires up `RuntimeOptions::shaderCachePath`. - // The JNI symbol is always exported so the Java surface is stable - // across native build configurations; if the caller passes a - // non-null path but `BABYLON_NATIVE_PLUGIN_SHADERCACHE` was not - // enabled at native build time, we throw `IllegalStateException` - // (fail loud, vs. silently dropping the path). + // RuntimeOptions is intentionally a Java object so Java and Kotlin callers + // can use a stable construction API as RuntimeOptions grows. RuntimeOptions options{}; - options.enableDebugger = (enableDebugger == JNI_TRUE); - if (shaderCachePath != nullptr) + if (!ApplyJavaRuntimeOptions(env, javaOptions, options)) { -#if BABYLON_NATIVE_PLUGIN_SHADERCACHE - const char* utf = env->GetStringUTFChars(shaderCachePath, nullptr); - if (utf != nullptr) - { - options.shaderCachePath = utf; - env->ReleaseStringUTFChars(shaderCachePath, utf); - } -#else - ThrowPluginNotEnabled(env, - "shaderCachePath was provided but BABYLON_NATIVE_PLUGIN_SHADERCACHE " - "was not enabled at native build time."); return 0; -#endif } return MakeRuntimeHandle(std::move(options)); } diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index f56a12164..ca6d53144 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -7,8 +7,22 @@ #import #import +#include #include +namespace +{ + uint8_t ToRuntimeMSAASamples(NSNumber* msaaSamples) + { + const long long value = msaaSamples.longLongValue; + return value >= 0 && value <= UINT8_MAX ? static_cast(value) : 0; + } +} + +@implementation BNRuntimeOptions + +@end + @implementation BNRuntime { std::unique_ptr _runtime; @@ -17,30 +31,33 @@ @implementation BNRuntime - (instancetype)init { - return [self initWithEnableDebugger:NO shaderCachePath:nil]; -} - -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger -{ - return [self initWithEnableDebugger:enableDebugger shaderCachePath:nil]; + return [self initWithOptions:nil]; } -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger - shaderCachePath:(nullable NSString*)shaderCachePath +- (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions { if ((self = [super init])) { Babylon::Integrations::RuntimeOptions options{}; - options.enableDebugger = enableDebugger ? true : false; + if (runtimeOptions != nil) + { + if (runtimeOptions.msaaSamples != nil) + { + options.msaaSamples = ToRuntimeMSAASamples(runtimeOptions.msaaSamples); + } + options.enableDebugger = runtimeOptions.enableDebugger ? true : false; + options.enableDebugTrace = runtimeOptions.enableDebugTrace ? true : false; + options.waitForDebugger = runtimeOptions.waitForDebugger ? true : false; + } // Default log sink: route every level to NSLog. Hosts that need // their own routing should drop down to the C++ API. options.log = [](Babylon::Integrations::LogLevel /*level*/, std::string_view message) { NSLog(@"%.*s", static_cast(message.size()), message.data()); }; - if (shaderCachePath != nil) + if (runtimeOptions.shaderCachePath != nil) { #if BABYLON_NATIVE_PLUGIN_SHADERCACHE - options.shaderCachePath = shaderCachePath.UTF8String; + options.shaderCachePath = runtimeOptions.shaderCachePath.UTF8String; #else // Caller explicitly asked for shader caching but the // plugin wasn't compiled in. Fail loudly rather than diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h index 9de9822c8..1ef9c3f5c 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h @@ -1,92 +1,107 @@ -// BNRuntime.h — public Obj-C interface for the Babylon::Integrations -// runtime on Apple platforms (iOS, macOS, visionOS). -// -// Swift consumers see this through the auto-generated Swift bridge -// (BNRuntime is exposed to Swift as `BNRuntime`). -// -// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. - -#pragma once - -#import - -@class MTKView; - -NS_ASSUME_NONNULL_BEGIN - -@interface BNRuntime : NSObject - -/// Constructs the runtime: starts the JS engine + thread, sets up -/// non-GPU polyfills and plugins. Cheap and synchronous; no GPU -/// device is created yet (that happens on the first `BNView` attach). -/// Default options: JS debugger off, log routes to `NSLog`. -- (instancetype)init; - -/// Same as `init` but lets the host opt into the JS debugger. -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger; - -/// Same as `initWithEnableDebugger:` but also wires up a persistent -/// on-disk GPU shader cache. Pass a writable file path (typically -/// inside `NSCachesDirectory`, e.g. -/// `[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"babylon.shadercache"]`). -/// Pass `nil` to disable the on-disk cache (equivalent to the -/// `initWithEnableDebugger:` overload). -/// -/// The cache is loaded on first `BNView` attach and saved on `suspend` -/// and on deallocation. -/// -/// If `shaderCachePath` is non-`nil` but the native library was built -/// without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, this method raises an -/// `NSException` (name -/// `BabylonNativePluginNotEnabledException`) so the misconfiguration -/// surfaces at construction time rather than silently dropping the -/// cache. Passing `nil` is always safe regardless of build config. -- (instancetype)initWithEnableDebugger:(BOOL)enableDebugger - shaderCachePath:(nullable NSString*)shaderCachePath - NS_DESIGNATED_INITIALIZER; - -/// Load a script from a URL onto the JS thread. Calls made before -/// the first `BNView` is created are queued internally and dispatched -/// after engine initialization completes during that first attach. -/// Calls after the first attach are dispatched immediately. -- (void)loadScript:(NSString*)url; - -/// Evaluate JavaScript source on the JS thread. Same queueing -/// semantics as `loadScript`. -- (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL; - -/// Reference-counted suspend. While suspended, JS timers pause and -/// any attached `BNView` becomes a no-op for `renderFrame` (the host -/// can keep calling it from its draw callback unconditionally; -/// nothing happens until `resume`). -- (void)suspend; - -/// Decrement the suspend count; resume the JS thread when the count -/// reaches zero. -- (void)resume; - -/// Whether the runtime is currently suspended. -@property (nonatomic, readonly, getter=isSuspended) BOOL suspended; - -/// Set the platform view that XR will render into (typically a -/// separate transparent `MTKView` overlay, distinct from the main -/// view's Metal layer). Pass `nil` to clear the XR surface. Safe to -/// call before the first `BNView` attach; the value is applied when -/// NativeXr finishes initializing during that first attach. -/// -/// Raises an `NSException` (name -/// `BabylonNativePluginNotEnabledException`) if invoked when -/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` was not enabled at native build -/// time. -- (void)setXrView:(nullable MTKView*)xrView; - -/// `YES` while an XR session is active. Updated from the JS thread -/// by NativeXr's internal session-state callback; safe to poll from -/// any thread. Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` was -/// not enabled at native build time (no XR session can ever be active -/// in that build). -@property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; - -@end - +// BNRuntime.h — public Obj-C interface for the Babylon::Integrations +// runtime on Apple platforms (iOS, macOS, visionOS). +// +// Swift consumers see this through the auto-generated Swift bridge +// (BNRuntime is exposed to Swift as `BNRuntime`). +// +// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. + +#pragma once + +#import + +@class MTKView; + +NS_ASSUME_NONNULL_BEGIN + +@interface BNRuntimeOptions : NSObject + +/// Optional MSAA sample count for the back buffer. Valid values are 0, 2, 4, 8, and 16. +/// Defaults to the shared C++ RuntimeOptions value when nil. +@property (nonatomic, strong, nullable) NSNumber* msaaSamples; + +/// Enable the JavaScript debugger when supported by the configured JS engine. +/// Defaults to NO, matching the shared C++ RuntimeOptions default. +@property (nonatomic) BOOL enableDebugger; + +/// Enable Babylon::DebugTrace output through the default log sink. +/// Defaults to NO, matching the shared C++ RuntimeOptions default. +@property (nonatomic) BOOL enableDebugTrace; + +/// Block engine startup until a debugger attaches when supported by the configured JS engine. +/// Defaults to NO, matching the shared C++ RuntimeOptions default. +@property (nonatomic) BOOL waitForDebugger; + +/// Optional writable file path for a persistent on-disk GPU shader cache. Defaults to nil. +@property (nonatomic, copy, nullable) NSString* shaderCachePath; + +@end + +@interface BNRuntime : NSObject + +/// Constructs the runtime: starts the JS engine + thread, sets up +/// non-GPU polyfills and plugins. Cheap and synchronous; no GPU +/// device is created yet (that happens on the first `BNView` attach). +/// Default options: JS debugger off, DebugTrace off, log routes to `NSLog`. +- (instancetype)init; + +/// Constructs the runtime with platform-friendly options. Pass nil to use +/// the same defaults as `init`. +/// +/// If `options.shaderCachePath` is non-`nil`, the cache is loaded on first +/// `BNView` attach and saved on `suspend` and on deallocation. +/// +/// If `options.shaderCachePath` is non-`nil` but the native library was built +/// without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, this initializer raises an +/// `NSException` (name +/// `BabylonNativePluginNotEnabledException`) so the misconfiguration +/// surfaces at construction time rather than silently dropping the +/// cache. Passing nil options, or options with nil `shaderCachePath`, is +/// always safe regardless of build config. +- (instancetype)initWithOptions:(nullable BNRuntimeOptions*)options NS_DESIGNATED_INITIALIZER; + +/// Load a script from a URL onto the JS thread. Calls made before +/// the first `BNView` is created are queued internally and dispatched +/// after engine initialization completes during that first attach. +/// Calls after the first attach are dispatched immediately. +- (void)loadScript:(NSString*)url; + +/// Evaluate JavaScript source on the JS thread. Same queueing +/// semantics as `loadScript`. +- (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL; + +/// Reference-counted suspend. While suspended, JS timers pause and +/// any attached `BNView` becomes a no-op for `renderFrame` (the host +/// can keep calling it from its draw callback unconditionally; +/// nothing happens until `resume`). +- (void)suspend; + +/// Decrement the suspend count; resume the JS thread when the count +/// reaches zero. +- (void)resume; + +/// Whether the runtime is currently suspended. +@property (nonatomic, readonly, getter=isSuspended) BOOL suspended; + +/// Set the platform view that XR will render into (typically a +/// separate transparent `MTKView` overlay, distinct from the main +/// view's Metal layer). Pass `nil` to clear the XR surface. Safe to +/// call before the first `BNView` attach; the value is applied when +/// NativeXr finishes initializing during that first attach. +/// +/// Raises an `NSException` (name +/// `BabylonNativePluginNotEnabledException`) if invoked when +/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` was not enabled at native build +/// time. +- (void)setXrView:(nullable MTKView*)xrView; + +/// `YES` while an XR session is active. Updated from the JS thread +/// by NativeXr's internal session-state callback; safe to poll from +/// any thread. Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` was +/// not enabled at native build time (no XR session can ever be active +/// in that build). +@property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; + +@end + NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h index be8eb10a1..742fde9c6 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h @@ -18,13 +18,18 @@ namespace Babylon::Integrations // Enable the JavaScript debugger. Only implemented for V8 and Chakra. bool enableDebugger{false}; + // Enable Babylon::DebugTrace. If a log sink is provided, DebugTrace + // output is forwarded to it as LogLevel::Log. + bool enableDebugTrace{false}; + // Block engine startup until a debugger has attached. Only // implemented for V8. bool waitForDebugger{false}; // Optional log sink. Receives: // - `console.{log,warn,error}` output → LogLevel::{Log,Warn,Error} - // - `Babylon::DebugTrace` output → LogLevel::Log + // - `Babylon::DebugTrace` output → LogLevel::Log, when + // enableDebugTrace is true // - Uncaught JS exceptions → LogLevel::Fatal // // If unset, ordinary log output is silently discarded and diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 6d127e995..cdc5ef0e9 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -79,17 +79,18 @@ namespace Babylon::Integrations RuntimeImpl::RuntimeImpl(RuntimeOptions options) : m_options{std::move(options)} { - // Wire DebugTrace through to the host's log callback (if any). + // Wire DebugTrace through to the host's log callback (if any), + // and enable it only when the host explicitly opts in. // DebugTrace is process-wide so this affects any concurrent // Runtime instances; that matches AppContext's behavior today. if (m_options.log) { - Babylon::DebugTrace::EnableDebugTrace(true); const auto& logCallback = m_options.log; Babylon::DebugTrace::SetTraceOutput([logCallback](const char* message) { logCallback(LogLevel::Log, message ? message : ""); }); } + Babylon::DebugTrace::EnableDebugTrace(m_options.enableDebugTrace); // Construct AppRuntime. This starts the JS thread and creates a // Napi::Env. Plugin Initialize() calls will be dispatched onto From 31f2afdffa1aa3a9d9ff0c1d5fe43151a3536d7a Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 14:37:08 -0700 Subject: [PATCH 36/71] Work around bgfx::init bug for Apple --- Core/Graphics/Source/DeviceImpl.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 9ca5349c2..86b79932e 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -214,7 +214,8 @@ namespace Babylon::Graphics } m_state.Bgfx.Initialized = true; - m_state.Bgfx.Dirty = false; + // TODO: Should be able to clear the Dirty flag, but bgfx::init is not doing everything that UpdateBgfxState is doing (at least on Apple platforms). + // m_state.Bgfx.Dirty = false; m_cancellationSource.emplace(); From 2f3f9d2f45743db35d9ac3baf62fdc94df951e25 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 15:01:34 -0700 Subject: [PATCH 37/71] Add bug link --- Core/Graphics/Source/DeviceImpl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 86b79932e..02fcd2a17 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -215,6 +215,7 @@ namespace Babylon::Graphics m_state.Bgfx.Initialized = true; // TODO: Should be able to clear the Dirty flag, but bgfx::init is not doing everything that UpdateBgfxState is doing (at least on Apple platforms). + // See https://github.com/BabylonJS/ThePirateCove/issues/1657 // m_state.Bgfx.Dirty = false; m_cancellationSource.emplace(); From b36c1db290a80c5071828662d15a8460fe648411 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 16:02:41 -0700 Subject: [PATCH 38/71] Defer View init until first resize --- Integrations/Apple/Source/BNView.mm | 92 ++------- Integrations/CMakeLists.txt | 15 +- .../Shared/Babylon/Integrations/View.h | 48 +++-- Integrations/Source/RuntimeImpl.h | 65 +++++-- Integrations/Source/View.cpp | 184 +++++++++++------- Integrations/Source/ViewImpl_Android.cpp | 19 -- Integrations/Source/ViewImpl_Unix.cpp | 39 ---- Integrations/Source/ViewImpl_Win32.cpp | 20 -- Integrations/Source/ViewImpl_WinRT.cpp | 37 ---- Integrations/Source/ViewImpl_iOS.mm | 35 ---- Integrations/Source/ViewImpl_macOS.mm | 34 ---- Integrations/Source/ViewImpl_visionOS.mm | 20 -- 12 files changed, 205 insertions(+), 403 deletions(-) delete mode 100644 Integrations/Source/ViewImpl_Android.cpp delete mode 100644 Integrations/Source/ViewImpl_Unix.cpp delete mode 100644 Integrations/Source/ViewImpl_Win32.cpp delete mode 100644 Integrations/Source/ViewImpl_WinRT.cpp delete mode 100644 Integrations/Source/ViewImpl_iOS.mm delete mode 100644 Integrations/Source/ViewImpl_macOS.mm delete mode 100644 Integrations/Source/ViewImpl_visionOS.mm diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index 58a873413..65e870ce3 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -8,25 +8,10 @@ #import #include -#include -#include #include #include -namespace -{ - bool IsFinitePositive(CGFloat value) - { - return std::isfinite(static_cast(value)) && value > 0; - } - - bool IsFinitePositive(CGSize size) - { - return IsFinitePositive(size.width) && IsFinitePositive(size.height); - } -} - @implementation BNView { std::unique_ptr _view; @@ -55,69 +40,20 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view // +layerClass override). CAMetalLayer* layer = (CAMetalLayer*)view.layer; - // If MTKView has not produced a drawable size yet, seed it - // from real laid-out bounds. If the host supplied a non-zero - // drawableSize explicitly (for example, for hidden preload), - // preserve it. - const CGSize drawableSize = layer.drawableSize; - if (!IsFinitePositive(drawableSize)) - { - [view layoutIfNeeded]; - - const CGSize boundsSize = view.bounds.size; - if (IsFinitePositive(boundsSize)) - { -#if TARGET_OS_OSX - CGFloat scale = view.window.backingScaleFactor > 0 - ? view.window.backingScaleFactor - : 1.0; -#else - CGFloat scale = view.contentScaleFactor; -#endif - if (!IsFinitePositive(scale)) - { - scale = 1.0; - } - const CGSize seededDrawableSize = CGSizeMake(boundsSize.width * scale, - boundsSize.height * scale); - if (IsFinitePositive(seededDrawableSize)) - { - layer.drawableSize = seededDrawableSize; - } - } - } - - const CGSize finalDrawableSize = layer.drawableSize; - if (!IsFinitePositive(finalDrawableSize)) - { - @throw [NSException - exceptionWithName:@"BabylonNativeInvalidViewException" - reason:@"BNView requires a finite, non-zero drawableSize or finite, non-zero bounds before attach." - userInfo:nil]; - } - - // First attach on this runtime triggers GPU device construction - // + plugin initialization + queued-script flush. The View - // queries the layer's drawableSize itself; the host doesn't - // need to pass dimensions. - try - { - _view = Babylon::Integrations::View::Attach( - *runtime.nativeRuntime, - (__bridge CA::MetalLayer*)layer); - } - catch (const std::exception& exception) - { - NSLog(@"BNView: View::Attach failed: %s", exception.what()); - _mtkView = nil; - return nil; - } - catch (...) - { - NSLog(@"BNView: View::Attach failed with an unknown exception"); - _mtkView = nil; - return nil; - } + // View::Attach is intentionally lightweight: it just stashes + // the layer pointer. No size query, no Device construction — + // and therefore nothing host-recoverable to throw. The MTKView + // delegate's `mtkView:drawableSizeWillChange:` (forwarded by + // BNViewDelegate to `-resizeWithWidth:height:`) is what + // actually drives Device construction + first-frame opening, + // on the first call. MTKView fires this callback before its + // first draw in normal Cocoa usage, so this bootstrap is + // automatic. Hosts that drive MTKView in unusual ways (e.g. + // hidden preload) can call `-resizeWithWidth:height:` directly + // with the surface's current pixel size. + _view = Babylon::Integrations::View::Attach( + *runtime.nativeRuntime, + (__bridge CA::MetalLayer*)layer); if (!_view) { _mtkView = nil; diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt index 61ade49fe..d51bb6ead 100644 --- a/Integrations/CMakeLists.txt +++ b/Integrations/CMakeLists.txt @@ -7,8 +7,7 @@ set(SOURCES "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/View.h" "Source/Runtime.cpp" "Source/RuntimeImpl.h" - "Source/View.cpp" - "Source/ViewImpl_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}") + "Source/View.cpp") add_library(Integrations ${SOURCES}) @@ -28,8 +27,9 @@ target_link_libraries(Integrations # GraphicsDeviceContext is PRIVATE — needed so View.cpp can see # for the free # `Babylon::Graphics::GetDevicePixelRatio(window)` helper used to - # convert physical → logical pixels in `View::Attach`'s first-attach - # path (before the `Device` exists). Internal Graphics types are + # convert physical → logical pixels on the first `View::Resize` + # call (before the `Device` exists or while it is still bound to + # the previous window on a re-attach). Internal Graphics types are # never exposed across the Integrations public surface. PRIVATE GraphicsDeviceContext PRIVATE arcana @@ -41,13 +41,6 @@ target_link_libraries(Integrations PRIVATE TextDecoder PRIVATE XMLHttpRequest) -# ----- Per-platform link dependencies for ViewImpl_*.{cpp,mm} ----- -if(ANDROID) - target_link_libraries(Integrations PRIVATE android) # ANativeWindow_getWidth/Height -elseif(UNIX AND NOT APPLE) - target_link_libraries(Integrations PRIVATE X11) # XOpenDisplay / XGetGeometry -endif() - # ----- Conditionally-included polyfills ----- # # Each flag is exposed as a PUBLIC compile definition so the public diff --git a/Integrations/Include/Shared/Babylon/Integrations/View.h b/Integrations/Include/Shared/Babylon/Integrations/View.h index 27afa9141..5f4582919 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/View.h +++ b/Integrations/Include/Shared/Babylon/Integrations/View.h @@ -51,27 +51,39 @@ namespace Babylon::Integrations // per-platform typedef the Graphics layer already uses // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). - // The View queries the surface's pixel-buffer size from the - // window itself via `ViewImpl::QuerySize`, which returns the - // platform-natural unit; the View internally converts to - // logical pixels before configuring the Device. + // The host owns the surface size: the View captures the + // window handle here and binds it to the Device on the first + // `Resize` call. Hosts MUST call `Resize` at least once + // (with the surface's current pixel dimensions) before the + // first frame will be rendered. // - // The first Attach on a given Runtime is the heavy step: it - // constructs `Babylon::Graphics::Device`, dispatches GPU plugin - // initialization (`Device::AddToJavaScript`, - // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, - // ...), and flushes any scripts queued via `Runtime::LoadScript` - // before this point. Opens the first frame. + // Attach itself is lightweight — it just registers as the + // current view and stashes the window handle. All Device + // work (first-time construction, or `UpdateWindow` + + // `UpdateSize` on a re-attach to an existing Runtime) is + // performed by the first `Resize` call, where the host- + // supplied dimensions become available. This folding is + // required: `Device::UpdateWindow` MUST be paired with a + // matching `Device::UpdateSize` or the next frame would be + // rendered to the new surface at the wrong size. // - // Subsequent Attach calls on the same Runtime are cheap: the - // Device is already constructed, plugins are initialized, the - // JS engine is running. They just call `Device::UpdateWindow` + - // `Device::EnableRendering` to bind the new surface, then open - // the first frame for the new attachment. + // The first Attach + Resize on a given Runtime is the heavy + // step: the Resize constructs `Babylon::Graphics::Device`, + // dispatches GPU plugin initialization + // (`Device::AddToJavaScript`, `NativeEngine::Initialize`, + // `NativeInput::CreateForJavaScript`, ...), and flushes any + // scripts queued via `Runtime::LoadScript` before this point. + // Opens the first frame. // - // Detach (`~View`) closes the in-flight frame and calls - // `Device::DisableRendering`. The Device persists on the - // Runtime, so the next Attach is fast. + // Subsequent Attach + Resize calls on the same Runtime are + // cheap: the Device is already constructed, plugins are + // initialized, the JS engine is running. They just call + // `Device::UpdateWindow` + `Device::UpdateSize` to bind the + // new surface, then open the first frame for the new + // attachment. + // + // Detach (`~View`) closes the in-flight frame. The Device + // persists on the Runtime, so the next Attach is fast. // // Must be called from the same thread that will call // `RenderFrame` and `Resize` (the "frame thread"). diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index dd515ba4f..4e9a116f6 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -139,9 +139,21 @@ namespace Babylon::Integrations }; // Internal implementation of View. Holds the back-reference to the - // Runtime that produced it. Provides per-platform helpers - // (implemented in ViewImpl_*.cpp / .mm) plus Suspend / Resume that - // manage the in-flight frame across runtime suspension. + // Runtime that produced it, the native window handle captured at + // `View::Attach`, plus Suspend / Resume that manage the in-flight + // frame across runtime suspension. + // + // `View::Attach` is intentionally cheap: it just stashes the + // window handle and registers as the current view. All Device + // interaction (first-time construction, or `UpdateWindow` + + // `UpdateSize` on a re-attach to an existing Runtime) is deferred + // to the first `View::Resize` call. This is a hard requirement: + // `Device::UpdateWindow` MUST be paired with a matching + // `Device::UpdateSize` or bgfx will render the next frame to the + // new window at the wrong size. Folding both into the first + // `Resize` makes the host-supplied dimensions the single source + // of truth — the Integrations layer never queries the window for + // its size. // // `m_suspended` is the View's view of whether it's currently // holding an open Device frame: @@ -151,36 +163,47 @@ namespace Babylon::Integrations // - false → a frame is open and the JS thread's safe timespan // is active // - // Initially `true`; `View::Attach` flips it to `false` after - // opening the first frame. `~View` calls `Suspend` before - // `Device::DisableRendering`. `Runtime::Suspend / Resume` call - // through here on the suspendCount 0↔1 transitions. + // Initially `true`; the first `View::Resize` flips it to `false` + // after binding the Device to the new window+size and opening the + // first frame. `~View` calls `Suspend` before relinquishing the + // view slot. `Runtime::Suspend / Resume` call through here on the + // suspendCount 0↔1 transitions; both are no-ops while + // `m_initialized` is still `false`, since there is no Device + // binding to suspend/resume yet. + // + // `m_initialized` is the View's "first Resize on this Attach has + // run" latch. Starts `false`; the first successful `Resize` sets + // it `true` after the Device is bound. It is the gate that lets + // `Resume()` open a frame: while `false`, `Resume()` is a no-op, + // so render-loop callbacks that fire between `Attach` and the + // first `Resize` (typical on Apple, where MTKView fires + // `drawInMTKView:` before `drawableSizeWillChange:`) silently + // do nothing rather than rendering to an unbound surface. struct ViewImpl { explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} Runtime& m_runtime; + + // Window handle captured at `View::Attach`. Used by the first + // `View::Resize` to construct (first-Attach-ever) or rebind + // (subsequent Attach) the Device. + Babylon::Graphics::WindowT m_window{}; + + // See class-level comment. Latches to `true` on first successful + // Resize; never flips back for the lifetime of this ViewImpl. + bool m_initialized{false}; + + // See class-level comment. "No frame is currently open." bool m_suspended{true}; // End the in-flight frame on the Device (Finish + // FinishRenderingCurrentFrame). Idempotent — no-op if already - // suspended. + // suspended or if the view has not yet been initialized. void Suspend(); // Open a new frame (StartRenderingCurrentFrame + // DeviceUpdate::Start). Idempotent — no-op if not currently - // suspended. + // suspended or if the view has not yet been initialized. void Resume(); - - // Query the surface's pixel-buffer size from the native window - // handle, tagged with the platform-natural units it returned. - // Implemented per-platform. `View::Attach` converts to logical - // before configuring the Device. - struct QuerySizeResult - { - uint32_t Width; - uint32_t Height; - CoordinateUnits Units; - }; - static QuerySizeResult QuerySize(Babylon::Graphics::WindowT window); }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index a1041ec64..c04fc629d 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -46,7 +46,28 @@ namespace Babylon::Integrations } // --------------------------------------------------------------------- - // View::Attach (first time and subsequent) + // View::Attach + // + // Lightweight: just register as the current view and stash the + // native window handle. All Device interaction (first-time + // construction, or `UpdateWindow` + `UpdateSize` on a re-attach to + // an existing Runtime) is deferred to the first `View::Resize`. + // + // Why deferred: `Device::UpdateWindow` MUST be paired with a + // matching `Device::UpdateSize` (otherwise bgfx renders the next + // frame to the new window at the old size). The previous design + // queried the window's size at Attach time via a per-platform + // helper, which created two sources of truth for size — the + // initial query, and the host's subsequent `Resize` calls — and + // forced the platform integration layer (BNView on Apple) to do + // gymnastics to ensure the surface had a non-zero size before + // Attach. Folding both into the first `Resize` makes the host the + // single source of truth. + // + // Render-loop callbacks that fire between `Attach` and the first + // `Resize` are safe: `m_initialized` is still `false`, so + // `RenderFrame` / `ViewImpl::Resume` early-out without touching + // the (potentially still-unbound) Device. // --------------------------------------------------------------------- std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) { @@ -58,59 +79,9 @@ namespace Babylon::Integrations return nullptr; } - const bool firstAttach = !impl.m_device; - - // Per-platform: query the surface's pixel-buffer size from the - // native window handle in whatever unit is natural for the - // platform. `Babylon::Graphics::Device` expects logical pixels - // for `Configuration::Width/Height` and `UpdateSize`, so we - // convert if QuerySize returned physical. On first Attach the - // Device doesn't exist yet, so we go through the standalone - // `Babylon::Graphics::GetDevicePixelRatio(window)` free function. - const auto querySize = ViewImpl::QuerySize(nativeWindow); - ValidateNonZeroSize(querySize.Width, querySize.Height, "View::Attach native window size"); - - const float dpr = Babylon::Graphics::GetDevicePixelRatio(nativeWindow); - const auto [logicalW, logicalH] = ToLogicalSize( - querySize.Width, querySize.Height, querySize.Units, dpr); - ValidateNonZeroSize(logicalW, logicalH, "View::Attach logical size"); - - if (firstAttach) - { - // First Attach on this Runtime: construct the Device. - Babylon::Graphics::Configuration config{}; - config.Window = nativeWindow; - config.Width = logicalW; - config.Height = logicalH; - config.MSAASamples = impl.m_options.msaaSamples; - - impl.m_device.emplace(config); - impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); - } - else - { - // Subsequent Attach: reuse the existing Device, just rebind - // the surface. Plugins, polyfills, and any loaded scripts - // are preserved on the JS side. - impl.m_device->UpdateWindow(nativeWindow); - impl.m_device->UpdateSize(logicalW, logicalH); - } - std::unique_ptr view{new View{std::make_unique(runtime)}}; + view->m_impl->m_window = nativeWindow; impl.m_currentView = view.get(); - - // Open the first frame via ViewImpl::Resume (which flips - // m_suspended → false). On first Attach, this must happen - // BEFORE dispatching the engine-init lambda so the - // Device::AddToJavaScript inside the lambda sees an open - // frame to record into. - view->m_impl->Resume(); - - if (firstAttach) - { - impl.RunFirstAttachInit(nativeWindow); - } - return view; } @@ -136,20 +107,23 @@ namespace Babylon::Integrations // ViewImpl::Suspend / Resume // // Idempotent open/close of the in-flight Device frame. Called from: - // - View::Attach → Resume (open frame after Device setup) - // - View::~View → Suspend (close frame at teardown) - // - Runtime::Suspend / Resume → matching call on the currently + // - View::Resize (first call) → Resume (open frame after Device + // binding completes) + // - View::~View → Suspend (close frame at teardown) + // - Runtime::Suspend / Resume → matching call on the currently // attached view, if any, so the host's // OS-level pause/resume signal cleanly // brackets the GPU frame. // // The internal `m_suspended` flag means "no frame currently open." - // Initial state is `true`; the very first Resume opens the first - // frame. Multiple Suspend or Resume calls in a row are no-ops. + // Initial state is `true`; the first `View::Resize` opens the first + // frame via Resume. Both methods are no-ops while `m_initialized` + // is still `false` (no Device binding exists yet for this view, so + // there is nothing to suspend/resume). // --------------------------------------------------------------------- void ViewImpl::Suspend() { - if (m_suspended) + if (m_suspended || !m_initialized) { return; } @@ -164,7 +138,7 @@ namespace Babylon::Integrations void ViewImpl::Resume() { - if (!m_suspended) + if (!m_suspended || !m_initialized) { return; } @@ -181,14 +155,14 @@ namespace Babylon::Integrations { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Skip the GPU work entirely while this view is suspended; - // the host can keep calling RenderFrame() from its draw - // callback unconditionally. The view's `m_suspended` flag is - // flipped by Runtime::Suspend/Resume (and by ~View / Attach - // for the destruction / construction boundaries), so this - // check covers every "frame is not currently open" case - // including: pre-Attach, between Suspend and Resume, and - // during teardown. + // Skip the GPU work entirely while this view has no open + // frame: the host can keep calling RenderFrame() from its + // draw callback unconditionally. `m_suspended` covers every + // "frame is not currently open" case, including: + // - Before the first Resize (m_initialized still false, so + // m_suspended has never been flipped). + // - Between Runtime::Suspend and Runtime::Resume. + // - During teardown (after ~View → Suspend). if (m_impl->m_suspended) { return; @@ -205,18 +179,86 @@ namespace Babylon::Integrations impl.m_deviceUpdate->Start(); } + // --------------------------------------------------------------------- + // View::Resize + // + // Owns deferred Device initialization. On the first call after + // `View::Attach`, this is where the Device is constructed (very + // first Attach on the Runtime) or re-bound to the new surface + // (subsequent Attach), and where the first frame is opened. Folding + // `UpdateWindow` + `UpdateSize` together here is required: the two + // calls MUST stay paired or bgfx will render to the new surface at + // the old size. + // + // On subsequent calls, this is a plain `UpdateSize` against the + // already-bound Device. + // --------------------------------------------------------------------- void View::Resize(uint32_t width, uint32_t height, CoordinateUnits units) { ValidateNonZeroSize(width, height, "View::Resize size"); RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (impl.m_device) + + if (!m_impl->m_initialized) { - const auto [lw, lh] = ToLogicalSize(width, height, units, - impl.m_device->GetDevicePixelRatio()); + // First Resize on this Attach: bind the Device to the + // window+size captured at Attach. We must compute DPR from + // the window directly via the standalone free function + // because, on a very first Attach, the Device doesn't + // exist yet; on a re-attach, the Device's stored DPR + // reflects the previous window and may not match the new + // one. + const auto window = m_impl->m_window; + const float dpr = Babylon::Graphics::GetDevicePixelRatio(window); + const auto [lw, lh] = ToLogicalSize(width, height, units, dpr); ValidateNonZeroSize(lw, lh, "View::Resize logical size"); - impl.m_device->UpdateSize(lw, lh); + + const bool firstAttachEver = !impl.m_device; + if (firstAttachEver) + { + Babylon::Graphics::Configuration config{}; + config.Window = window; + config.Width = lw; + config.Height = lh; + config.MSAASamples = impl.m_options.msaaSamples; + + impl.m_device.emplace(config); + impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); + } + else + { + // Re-attach to an existing Runtime: rebind the surface + // and update the size in lockstep. Plugins, polyfills, + // and any loaded scripts are preserved on the JS side. + impl.m_device->UpdateWindow(window); + impl.m_device->UpdateSize(lw, lh); + } + + // Flip the initialized latch BEFORE Resume() so the open- + // frame logic actually runs. + m_impl->m_initialized = true; + + // Open the first frame. On very first Attach this must + // happen BEFORE dispatching `RunFirstAttachInit` so the + // `Device::AddToJavaScript` inside that lambda sees an + // open frame to record into. + m_impl->Resume(); + + if (firstAttachEver) + { + impl.RunFirstAttachInit(window); + } + return; } + + // Subsequent Resize on an initialized view: plain UpdateSize + // against the already-bound Device, using the Device's stored + // DPR (which matches the window the Device is currently bound + // to). + const auto [lw, lh] = ToLogicalSize(width, height, units, + impl.m_device->GetDevicePixelRatio()); + ValidateNonZeroSize(lw, lh, "View::Resize logical size"); + impl.m_device->UpdateSize(lw, lh); } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT diff --git a/Integrations/Source/ViewImpl_Android.cpp b/Integrations/Source/ViewImpl_Android.cpp deleted file mode 100644 index a56ba857b..000000000 --- a/Integrations/Source/ViewImpl_Android.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "RuntimeImpl.h" - -#include - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - if (window == nullptr) - { - return {0, 0, CoordinateUnits::Physical}; - } - // ANativeWindow_getWidth/Height return the surface's - // pixel-buffer size in physical (device) pixels. - return {static_cast(ANativeWindow_getWidth(window)), - static_cast(ANativeWindow_getHeight(window)), - CoordinateUnits::Physical}; - } -} diff --git a/Integrations/Source/ViewImpl_Unix.cpp b/Integrations/Source/ViewImpl_Unix.cpp deleted file mode 100644 index 1c364f763..000000000 --- a/Integrations/Source/ViewImpl_Unix.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "RuntimeImpl.h" - -#include - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - if (window == 0) - { - return {0, 0, CoordinateUnits::Physical}; - } - - // X11 `Window` is just an XID; querying its geometry needs a - // Display connection. Open one transiently — same pattern as - // `Core/Graphics/Source/DeviceImpl_Unix.cpp::GetDevicePixelRatio`. - // (See https://github.com/BabylonJS/BabylonNative/issues/625.) - Display* display = XOpenDisplay(nullptr); - if (display == nullptr) - { - return {0, 0, CoordinateUnits::Physical}; - } - - ::Window root{}; - int x{}, y{}; - unsigned int width{}, height{}, borderWidth{}, depth{}; - Status status = XGetGeometry(display, window, &root, &x, &y, - &width, &height, &borderWidth, &depth); - - XCloseDisplay(display); - - if (status == 0) - { - return {0, 0, CoordinateUnits::Physical}; - } - // XGetGeometry returns the window's size in physical pixels. - return {width, height, CoordinateUnits::Physical}; - } -} diff --git a/Integrations/Source/ViewImpl_Win32.cpp b/Integrations/Source/ViewImpl_Win32.cpp deleted file mode 100644 index b108f0917..000000000 --- a/Integrations/Source/ViewImpl_Win32.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "RuntimeImpl.h" - -#include - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - RECT rect{}; - if (window == nullptr || !GetClientRect(window, &rect)) - { - return {0, 0, CoordinateUnits::Physical}; - } - // For DPI-aware apps, `GetClientRect` returns the surface's - // pixel-buffer size in physical pixels. - return {static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top), - CoordinateUnits::Physical}; - } -} diff --git a/Integrations/Source/ViewImpl_WinRT.cpp b/Integrations/Source/ViewImpl_WinRT.cpp deleted file mode 100644 index fa67c1419..000000000 --- a/Integrations/Source/ViewImpl_WinRT.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "RuntimeImpl.h" - -#include -#include - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - // WindowT here is `winrt::Windows::Foundation::IInspectable` - // wrapping one of: ICoreWindow, ISwapChainPanel, - // Microsoft::UI::Xaml::Controls::ISwapChainPanel. - // - // Both branches return logical (DIP) units; `View::Attach` - // converts to physical via the centralized - // `Babylon::Graphics::GetDevicePixelRatio(window)` (which on - // IUIElement-capable WinRT windows is `RasterizationScale`). - // This keeps a single DPR source of truth across the - // Integrations layer. - if (auto coreWindow = window.try_as()) - { - const auto bounds = coreWindow.Bounds(); - return {static_cast(bounds.Width), - static_cast(bounds.Height), - CoordinateUnits::Logical}; - } - if (auto panel = window.try_as()) - { - // FrameworkElement.ActualWidth/Height are in DIPs. - const auto fe = panel.as(); - return {static_cast(fe.ActualWidth()), - static_cast(fe.ActualHeight()), - CoordinateUnits::Logical}; - } - return {0, 0, CoordinateUnits::Logical}; - } -} diff --git a/Integrations/Source/ViewImpl_iOS.mm b/Integrations/Source/ViewImpl_iOS.mm deleted file mode 100644 index c5a55e0ec..000000000 --- a/Integrations/Source/ViewImpl_iOS.mm +++ /dev/null @@ -1,35 +0,0 @@ -#include "RuntimeImpl.h" - -#include - -#import - -namespace -{ - bool IsFinitePositive(CGFloat value) - { - return std::isfinite(static_cast(value)) && value > 0; - } -} - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - if (window == nullptr) - { - return {0, 0, CoordinateUnits::Physical}; - } - // metal-cpp's CA::MetalLayer* can be bridge-cast to the Obj-C - // CAMetalLayer*; drawableSize is in physical pixels. - CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - const CGSize size = layer.drawableSize; - if (!IsFinitePositive(size.width) || !IsFinitePositive(size.height)) - { - return {0, 0, CoordinateUnits::Physical}; - } - return {static_cast(size.width), - static_cast(size.height), - CoordinateUnits::Physical}; - } -} diff --git a/Integrations/Source/ViewImpl_macOS.mm b/Integrations/Source/ViewImpl_macOS.mm deleted file mode 100644 index f547b34c9..000000000 --- a/Integrations/Source/ViewImpl_macOS.mm +++ /dev/null @@ -1,34 +0,0 @@ -#include "RuntimeImpl.h" - -#include - -#import - -namespace -{ - bool IsFinitePositive(CGFloat value) - { - return std::isfinite(static_cast(value)) && value > 0; - } -} - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - if (window == nullptr) - { - return {0, 0, CoordinateUnits::Physical}; - } - // CAMetalLayer.drawableSize is in physical pixels. - CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - const CGSize size = layer.drawableSize; - if (!IsFinitePositive(size.width) || !IsFinitePositive(size.height)) - { - return {0, 0, CoordinateUnits::Physical}; - } - return {static_cast(size.width), - static_cast(size.height), - CoordinateUnits::Physical}; - } -} diff --git a/Integrations/Source/ViewImpl_visionOS.mm b/Integrations/Source/ViewImpl_visionOS.mm deleted file mode 100644 index 83461e653..000000000 --- a/Integrations/Source/ViewImpl_visionOS.mm +++ /dev/null @@ -1,20 +0,0 @@ -#include "RuntimeImpl.h" - -#import - -namespace Babylon::Integrations -{ - ViewImpl::QuerySizeResult ViewImpl::QuerySize(Babylon::Graphics::WindowT window) - { - if (window == nullptr) - { - return {0, 0, CoordinateUnits::Physical}; - } - // CAMetalLayer.drawableSize is in physical pixels. - CAMetalLayer* layer = (__bridge CAMetalLayer*)window; - const CGSize size = layer.drawableSize; - return {static_cast(size.width), - static_cast(size.height), - CoordinateUnits::Physical}; - } -} From d9ebdee0f7cebe29c3ac43156d68a7f3cb05155b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 16:09:06 -0700 Subject: [PATCH 39/71] Improve logging config and output for Apple --- .../main/cpp/BabylonNativeIntegrations.cpp | 9 ++-- Integrations/Apple/Source/BNRuntime.mm | 44 +++++++++++++++++-- .../Shared/Babylon/Integrations/LogLevel.h | 15 +++++-- .../Babylon/Integrations/RuntimeOptions.h | 4 +- Integrations/Source/Runtime.cpp | 5 ++- 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 7a2d94b03..d0cf192db 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -70,10 +70,11 @@ namespace { switch (level) { - case LogLevel::Log: return ANDROID_LOG_INFO; - case LogLevel::Warn: return ANDROID_LOG_WARN; - case LogLevel::Error: return ANDROID_LOG_ERROR; - case LogLevel::Fatal: return ANDROID_LOG_FATAL; + case LogLevel::Verbose: return ANDROID_LOG_VERBOSE; + case LogLevel::Log: return ANDROID_LOG_INFO; + case LogLevel::Warn: return ANDROID_LOG_WARN; + case LogLevel::Error: return ANDROID_LOG_ERROR; + case LogLevel::Fatal: return ANDROID_LOG_FATAL; } return ANDROID_LOG_INFO; } diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index ca6d53144..86e340d3d 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -7,6 +7,8 @@ #import #import +#include + #include #include @@ -17,6 +19,34 @@ uint8_t ToRuntimeMSAASamples(NSNumber* msaaSamples) const long long value = msaaSamples.longLongValue; return value >= 0 && value <= UINT8_MAX ? static_cast(value) : 0; } + + // Lazily-initialized process-wide logger used by the default log + // sink. Subsystem matches the Babylon Native CFBundleIdentifier + // convention so Console.app / `log stream` can filter it cleanly. + os_log_t BabylonNativeLogger() + { + static os_log_t logger = os_log_create("com.babylonjs.babylonnative", "Runtime"); + return logger; + } + + // Map LogLevel onto the closest os_log type. os_log has no "warn" + // distinct from "default", so Warn folds into DEFAULT (matching + // Apple's own console.warn → default mapping). DEBUG and INFO are + // filtered out of release builds and Console.app by default, so + // routing Verbose there keeps DebugTrace spam out of the way unless + // a developer explicitly opts in (`log stream --level debug ...`). + os_log_type_t ToOSLogType(Babylon::Integrations::LogLevel level) + { + switch (level) + { + case Babylon::Integrations::LogLevel::Verbose: return OS_LOG_TYPE_DEBUG; + case Babylon::Integrations::LogLevel::Log: return OS_LOG_TYPE_DEFAULT; + case Babylon::Integrations::LogLevel::Warn: return OS_LOG_TYPE_DEFAULT; + case Babylon::Integrations::LogLevel::Error: return OS_LOG_TYPE_ERROR; + case Babylon::Integrations::LogLevel::Fatal: return OS_LOG_TYPE_FAULT; + } + return OS_LOG_TYPE_DEFAULT; + } } @implementation BNRuntimeOptions @@ -49,10 +79,16 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions options.enableDebugTrace = runtimeOptions.enableDebugTrace ? true : false; options.waitForDebugger = runtimeOptions.waitForDebugger ? true : false; } - // Default log sink: route every level to NSLog. Hosts that need - // their own routing should drop down to the C++ API. - options.log = [](Babylon::Integrations::LogLevel /*level*/, std::string_view message) { - NSLog(@"%.*s", static_cast(message.size()), message.data()); + // Default log sink: route through `os_log_with_type` so the + // level survives all the way to Console.app / `log stream`. + // Hosts that need their own routing should drop down to the + // C++ API. `%{public}.*s` is required to print the message + // payload; without `{public}` os_log redacts non-scalar + // arguments as `` in release builds. + options.log = [](Babylon::Integrations::LogLevel level, std::string_view message) { + os_log_with_type(BabylonNativeLogger(), ToOSLogType(level), + "%{public}.*s", + static_cast(message.size()), message.data()); }; if (runtimeOptions.shaderCachePath != nil) { diff --git a/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h b/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h index 29b49ffb9..0df06051a 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h +++ b/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h @@ -4,18 +4,27 @@ namespace Babylon::Integrations { // Severity levels for the optional log callback on RuntimeOptions. // - // The first three (Log / Warn / Error) mirror + // `Verbose` is used for low-priority diagnostic output: currently + // `Babylon::DebugTrace` messages, which are produced only when the + // host opts in via `RuntimeOptions::enableDebugTrace`. Hosts that + // want a quieter log can filter on `level > LogLevel::Verbose`. + // + // The next three (Log / Warn / Error) mirror // `Babylon::Polyfills::Console::LogLevel` and are used for - // `console.log` / `console.warn` / `console.error` calls and for - // `Babylon::DebugTrace` output. + // `console.log` / `console.warn` / `console.error` calls. // // `Fatal` is used for **uncaught** JavaScript exceptions that // propagated past every JS-side handler. The engine state may be // inconsistent after a Fatal; a host that wants to terminate the // process on uncaught errors can do so from inside its log // callback (e.g. `if (level == LogLevel::Fatal) std::quick_exit(1);`). + // + // Values are ordered by increasing severity so hosts can compare + // them (`level >= LogLevel::Warn` etc.) for level-threshold + // filtering. enum class LogLevel { + Verbose, Log, Warn, Error, diff --git a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h index 742fde9c6..15eb43528 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h @@ -19,7 +19,7 @@ namespace Babylon::Integrations bool enableDebugger{false}; // Enable Babylon::DebugTrace. If a log sink is provided, DebugTrace - // output is forwarded to it as LogLevel::Log. + // output is forwarded to it as LogLevel::Verbose. bool enableDebugTrace{false}; // Block engine startup until a debugger has attached. Only @@ -28,7 +28,7 @@ namespace Babylon::Integrations // Optional log sink. Receives: // - `console.{log,warn,error}` output → LogLevel::{Log,Warn,Error} - // - `Babylon::DebugTrace` output → LogLevel::Log, when + // - `Babylon::DebugTrace` output → LogLevel::Verbose, when // enableDebugTrace is true // - Uncaught JS exceptions → LogLevel::Fatal // diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index cdc5ef0e9..e1e40e197 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -83,11 +83,14 @@ namespace Babylon::Integrations // and enable it only when the host explicitly opts in. // DebugTrace is process-wide so this affects any concurrent // Runtime instances; that matches AppContext's behavior today. + // DebugTrace output is low-priority diagnostic noise, so it's + // forwarded at LogLevel::Verbose to make it easy for hosts to + // filter out. if (m_options.log) { const auto& logCallback = m_options.log; Babylon::DebugTrace::SetTraceOutput([logCallback](const char* message) { - logCallback(LogLevel::Log, message ? message : ""); + logCallback(LogLevel::Verbose, message ? message : ""); }); } Babylon::DebugTrace::EnableDebugTrace(m_options.enableDebugTrace); From 3dfc1b0be9f1f38cef0cdf646bce59031b618c38 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 18:02:38 -0700 Subject: [PATCH 40/71] Defer View init until resume (if needed) --- .../Shared/Babylon/Integrations/Runtime.h | 11 +- Integrations/Source/Runtime.cpp | 19 +- Integrations/Source/RuntimeImpl.h | 107 +++---- Integrations/Source/View.cpp | 260 ++++++++++-------- 4 files changed, 222 insertions(+), 175 deletions(-) diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 14429bb77..0d45935b6 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -85,7 +85,16 @@ namespace Babylon::Integrations // happens until Resume(). // Calls are reference-counted; nesting is safe. // - // Safe to call from any thread. + // Threading: `Suspend` / `Resume` must be called from the frame + // thread (the same thread that drives `View::RenderFrame` / + // `View::Resize`). Hosts wiring these to platform lifecycle + // callbacks get this for free — iOS / macOS / Android / Win32 / + // UWP lifecycle callbacks all fire on the main / UI thread. A + // host that wants to suspend from a background thread should + // marshal the call to its main thread first, the same way it + // would for any other View / Runtime entry point. `IsSuspended` + // is the exception: it loads the suspend count atomically and + // is safe to read from any thread. void Suspend(); void Resume(); bool IsSuspended() const; diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index e1e40e197..1c53a2d38 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -383,8 +383,10 @@ namespace Babylon::Integrations void Runtime::Suspend() { - std::lock_guard lock{m_impl->m_suspendMutex}; - if (m_impl->m_suspendCount++ == 0) + // Frame-thread only (see Runtime.h). No locking needed for the + // count itself; the atomic exists so cross-thread IsSuspended() + // reads stay coherent. + if (m_impl->m_suspendCount.fetch_add(1, std::memory_order_relaxed) == 0) { // Close the in-flight frame on the currently attached View // (if any) BEFORE blocking the JS thread. This keeps the @@ -410,17 +412,19 @@ namespace Babylon::Integrations void Runtime::Resume() { - std::lock_guard lock{m_impl->m_suspendMutex}; - if (m_impl->m_suspendCount == 0) + // Frame-thread only (see Runtime.h). + if (m_impl->m_suspendCount.load(std::memory_order_relaxed) == 0) { // Mismatched Resume; ignore rather than underflow the count. return; } - if (--m_impl->m_suspendCount == 0) + if (m_impl->m_suspendCount.fetch_sub(1, std::memory_order_relaxed) == 1) { m_impl->m_appRuntime->Resume(); // Re-open the frame on the attached View (if any) so the - // next RenderFrame call has something to Finish. + // next RenderFrame call has something to Finish. On a view + // that has been attached but never sized, this also drives + // the deferred first-Resize init via InitializeIfReady. if (m_impl->m_currentView) { m_impl->m_currentView->m_impl->Resume(); @@ -430,8 +434,7 @@ namespace Babylon::Integrations bool Runtime::IsSuspended() const { - std::lock_guard lock{m_impl->m_suspendMutex}; - return m_impl->m_suspendCount > 0; + return m_impl->m_suspendCount.load(std::memory_order_relaxed) > 0; } #if BABYLON_NATIVE_PLUGIN_NATIVEXR diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 4e9a116f6..3cfb5f281 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -92,9 +92,13 @@ namespace Babylon::Integrations // ScriptLoader's existing contract; we do not add an outer mutex. arcana::task_completion_source m_initTcs; - // Reference-counted Suspend/Resume. - int m_suspendCount{0}; - mutable std::mutex m_suspendMutex; + // Reference-counted Suspend/Resume. Atomic so `IsSuspended()` + // can be polled cheaply from any thread (e.g. a worker checking + // whether to bother queuing more host-side work). Mutations to + // the count itself happen only on the frame thread (the + // documented contract of `Runtime::Suspend` / `Runtime::Resume`), + // so the increment/decrement do not need to be locked. + std::atomic m_suspendCount{0}; // 0..1; tracked so we can guard against multiple concurrent // attachments (the API contract is "at most one View at a time"). @@ -140,70 +144,79 @@ namespace Babylon::Integrations // Internal implementation of View. Holds the back-reference to the // Runtime that produced it, the native window handle captured at - // `View::Attach`, plus Suspend / Resume that manage the in-flight - // frame across runtime suspension. + // `View::Attach`, the most recent size handed in via `View::Resize`, + // plus Suspend / Resume that manage the in-flight frame across + // runtime suspension. // // `View::Attach` is intentionally cheap: it just stashes the // window handle and registers as the current view. All Device // interaction (first-time construction, or `UpdateWindow` + // `UpdateSize` on a re-attach to an existing Runtime) is deferred - // to the first `View::Resize` call. This is a hard requirement: - // `Device::UpdateWindow` MUST be paired with a matching - // `Device::UpdateSize` or bgfx will render the next frame to the - // new window at the wrong size. Folding both into the first - // `Resize` makes the host-supplied dimensions the single source - // of truth — the Integrations layer never queries the window for - // its size. + // to `InitializeIfReady`, which only runs once all three + // preconditions hold: // - // `m_suspended` is the View's view of whether it's currently - // holding an open Device frame: - // - true → no frame is open (StartRenderingCurrentFrame + - // DeviceUpdate::Start have NOT been called, or have - // been matched by Finish / FinishRenderingCurrentFrame) - // - false → a frame is open and the JS thread's safe timespan - // is active + // 1. `m_initialized` is still `false` (the view hasn't been + // initialized yet for this Attach), + // 2. `m_size` is set (the host has called `View::Resize` at + // least once with a non-zero size), and + // 3. `RuntimeImpl::m_suspendCount` is zero (the host hasn't + // called `Runtime::Suspend` without a matching `Resume`). // - // Initially `true`; the first `View::Resize` flips it to `false` - // after binding the Device to the new window+size and opening the - // first frame. `~View` calls `Suspend` before relinquishing the - // view slot. `Runtime::Suspend / Resume` call through here on the - // suspendCount 0↔1 transitions; both are no-ops while - // `m_initialized` is still `false`, since there is no Device - // binding to suspend/resume yet. + // `InitializeIfReady` is called from `View::Resize` (which sets + // condition 2) and from `ViewImpl::Resume` (which can clear + // condition 3). Whichever caller satisfies the last missing + // precondition is the one that actually runs the init recipe. + // This is also what makes the `UpdateWindow` + `UpdateSize` pair + // safe: both happen together inside `InitializeIfReady`, so bgfx + // never sees a window change without a matching size change. // - // `m_initialized` is the View's "first Resize on this Attach has - // run" latch. Starts `false`; the first successful `Resize` sets - // it `true` after the Device is bound. It is the gate that lets - // `Resume()` open a frame: while `false`, `Resume()` is a no-op, - // so render-loop callbacks that fire between `Attach` and the - // first `Resize` (typical on Apple, where MTKView fires - // `drawInMTKView:` before `drawableSizeWillChange:`) silently - // do nothing rather than rendering to an unbound surface. + // Once `m_initialized` flips to `true`, a Device frame is open + // iff `RuntimeImpl::m_suspendCount == 0`. `ViewImpl::Suspend` and + // `ViewImpl::Resume` are called by `Runtime::Suspend` / + // `Runtime::Resume` on the suspendCount 0↔1 transitions and + // toggle that frame open/closed. struct ViewImpl { explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} Runtime& m_runtime; - // Window handle captured at `View::Attach`. Used by the first - // `View::Resize` to construct (first-Attach-ever) or rebind - // (subsequent Attach) the Device. + // Window handle captured at `View::Attach`. Used by + // `InitializeIfReady` to construct (first-Attach-ever) or + // rebind (subsequent Attach) the Device. Babylon::Graphics::WindowT m_window{}; - // See class-level comment. Latches to `true` on first successful - // Resize; never flips back for the lifetime of this ViewImpl. - bool m_initialized{false}; + // Most recent size handed in via `View::Resize`, in logical + // pixels. Empty until the host has called `Resize` at least + // once. Used by `InitializeIfReady` to size the Device on the + // first init. + std::optional> m_size; - // See class-level comment. "No frame is currently open." - bool m_suspended{true}; + // Latches to `true` the first time `InitializeIfReady` runs + // all three preconditions to completion. Never flips back for + // the lifetime of this `ViewImpl`; a new `ViewImpl` is + // constructed on each `View::Attach`. + bool m_initialized{false}; - // End the in-flight frame on the Device (Finish + - // FinishRenderingCurrentFrame). Idempotent — no-op if already - // suspended or if the view has not yet been initialized. + // Close the in-flight frame on the Device (Finish + + // FinishRenderingCurrentFrame). Called by `Runtime::Suspend` + // on the suspendCount 0→1 transition, and by `~View`. No-op + // when the view is not yet initialized (no frame to close). void Suspend(); // Open a new frame (StartRenderingCurrentFrame + - // DeviceUpdate::Start). Idempotent — no-op if not currently - // suspended or if the view has not yet been initialized. + // DeviceUpdate::Start) on the Device. Called by + // `Runtime::Resume` on the suspendCount 1→0 transition. When + // the view is not yet initialized, this instead tries to run + // the deferred first-Resize init (which itself opens the + // first frame if it succeeds) — so a host that attached and + // resized while the Runtime was externally suspended sees its + // init kick off cleanly on the next `Runtime::Resume`. void Resume(); + + // Run the deferred Device-binding + first-frame-opening + // recipe iff all three preconditions are satisfied (see the + // class-level comment). No-op otherwise. Safe to call from + // any of the entry points that can satisfy a precondition. + void InitializeIfReady(); }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index c04fc629d..1c7358cf2 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -1,5 +1,6 @@ #include "RuntimeImpl.h" +#include #include #include @@ -51,23 +52,25 @@ namespace Babylon::Integrations // Lightweight: just register as the current view and stash the // native window handle. All Device interaction (first-time // construction, or `UpdateWindow` + `UpdateSize` on a re-attach to - // an existing Runtime) is deferred to the first `View::Resize`. + // an existing Runtime) is deferred to `ViewImpl::InitializeIfReady`, + // which is called from `View::Resize` and `ViewImpl::Resume` and + // only actually runs when all three init preconditions hold (see + // `RuntimeImpl.h` for details). // // Why deferred: `Device::UpdateWindow` MUST be paired with a // matching `Device::UpdateSize` (otherwise bgfx renders the next - // frame to the new window at the old size). The previous design - // queried the window's size at Attach time via a per-platform - // helper, which created two sources of truth for size — the - // initial query, and the host's subsequent `Resize` calls — and - // forced the platform integration layer (BNView on Apple) to do - // gymnastics to ensure the surface had a non-zero size before - // Attach. Folding both into the first `Resize` makes the host the - // single source of truth. + // frame to the new window at the old size), so we wait until the + // host has supplied a size via `Resize` and the Runtime is not + // currently externally suspended before binding the new surface. + // Folding the window-rebind and size-update together inside + // `InitializeIfReady` makes the host the single source of truth + // for surface size — the Integrations layer never queries the + // window for its size. // - // Render-loop callbacks that fire between `Attach` and the first - // `Resize` are safe: `m_initialized` is still `false`, so - // `RenderFrame` / `ViewImpl::Resume` early-out without touching - // the (potentially still-unbound) Device. + // Render-loop callbacks that fire between `Attach` and successful + // init are safe: `RenderFrame` early-outs while `m_initialized` + // is `false`, without touching the (potentially still-unbound) + // Device. // --------------------------------------------------------------------- std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) { @@ -94,11 +97,17 @@ namespace Babylon::Integrations { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // End the in-flight frame if one is open. Idempotent: if the - // Runtime was already suspended (which closed the frame via - // ViewImpl::Suspend), this is a no-op. The Device persists on - // the Runtime so the next Attach is cheap. - m_impl->Suspend(); + // Close the in-flight frame iff one is currently open. A frame + // is open exactly when this view is initialized and the + // Runtime is not externally suspended (Runtime::Suspend would + // already have closed the frame via ViewImpl::Suspend). The + // Device persists on the Runtime so the next Attach is cheap. + if (m_impl->m_initialized && + impl.m_suspendCount.load(std::memory_order_relaxed) == 0) + { + impl.m_deviceUpdate->Finish(); + impl.m_device->FinishRenderingCurrentFrame(); + } impl.m_currentView = nullptr; } @@ -106,64 +115,118 @@ namespace Babylon::Integrations // --------------------------------------------------------------------- // ViewImpl::Suspend / Resume // - // Idempotent open/close of the in-flight Device frame. Called from: - // - View::Resize (first call) → Resume (open frame after Device - // binding completes) - // - View::~View → Suspend (close frame at teardown) - // - Runtime::Suspend / Resume → matching call on the currently - // attached view, if any, so the host's - // OS-level pause/resume signal cleanly - // brackets the GPU frame. - // - // The internal `m_suspended` flag means "no frame currently open." - // Initial state is `true`; the first `View::Resize` opens the first - // frame via Resume. Both methods are no-ops while `m_initialized` - // is still `false` (no Device binding exists yet for this view, so - // there is nothing to suspend/resume). + // Called by `Runtime::Suspend` / `Runtime::Resume` on the + // suspendCount 0↔1 transitions (and by `~View` for the teardown + // close). When the view is already initialized, these are pure + // frame open/close operations. When the view is not yet + // initialized, Suspend has nothing to do (no frame exists) and + // Resume retries `InitializeIfReady` — because the suspendCount + // dropping to 0 may have been the last missing precondition. // --------------------------------------------------------------------- void ViewImpl::Suspend() { - if (m_suspended || !m_initialized) + if (!m_initialized) { return; } RuntimeImpl& impl = *m_runtime.m_impl; - if (impl.m_device && impl.m_deviceUpdate) + impl.m_deviceUpdate->Finish(); + impl.m_device->FinishRenderingCurrentFrame(); + } + + void ViewImpl::Resume() + { + if (!m_initialized) { - impl.m_deviceUpdate->Finish(); - impl.m_device->FinishRenderingCurrentFrame(); + // Runtime just resumed; the suspendCount precondition is + // now satisfied. If the host has also already called + // `View::Resize` (size precondition), this will succeed; + // otherwise it's a silent no-op until they do. + InitializeIfReady(); + return; } - m_suspended = true; + RuntimeImpl& impl = *m_runtime.m_impl; + impl.m_device->StartRenderingCurrentFrame(); + impl.m_deviceUpdate->Start(); } - void ViewImpl::Resume() + // --------------------------------------------------------------------- + // ViewImpl::InitializeIfReady + // + // The single recipe for binding the Device to this view's window + // and opening the first frame. Gated on all three preconditions + // (initialized? sized? not externally suspended?) so it can be + // called eagerly from anywhere that satisfies one of them, and + // does nothing until the last missing condition is fulfilled. + // --------------------------------------------------------------------- + void ViewImpl::InitializeIfReady() { - if (!m_suspended || !m_initialized) + if (m_initialized || !m_size) { return; } RuntimeImpl& impl = *m_runtime.m_impl; - if (impl.m_device && impl.m_deviceUpdate) + if (impl.m_suspendCount.load(std::memory_order_relaxed) > 0) + { + return; + } + + const auto [lw, lh] = *m_size; + const bool firstAttachEver = !impl.m_device; + if (firstAttachEver) + { + Babylon::Graphics::Configuration config{}; + config.Window = m_window; + config.Width = lw; + config.Height = lh; + config.MSAASamples = impl.m_options.msaaSamples; + + impl.m_device.emplace(config); + impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); + } + else { - impl.m_device->StartRenderingCurrentFrame(); - impl.m_deviceUpdate->Start(); + // Re-attach to an existing Runtime: rebind the surface and + // update the size in lockstep. Plugins, polyfills, and any + // loaded scripts are preserved on the JS side. + impl.m_device->UpdateWindow(m_window); + impl.m_device->UpdateSize(lw, lh); + } + + // Open the first frame inline. On very first Attach this MUST + // happen BEFORE dispatching `RunFirstAttachInit` so the + // `Device::AddToJavaScript` inside that lambda sees an open + // frame to record into. Both happen on the same host thread, + // so by the time the dispatched lambda runs on the JS thread, + // the frame is already open regardless of whether we entered + // here from `View::Resize` or from `ViewImpl::Resume`. + impl.m_device->StartRenderingCurrentFrame(); + impl.m_deviceUpdate->Start(); + m_initialized = true; + + if (firstAttachEver) + { + impl.RunFirstAttachInit(m_window); } - m_suspended = false; } void View::RenderFrame() { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Skip the GPU work entirely while this view has no open - // frame: the host can keep calling RenderFrame() from its - // draw callback unconditionally. `m_suspended` covers every - // "frame is not currently open" case, including: - // - Before the first Resize (m_initialized still false, so - // m_suspended has never been flipped). - // - Between Runtime::Suspend and Runtime::Resume. - // - During teardown (after ~View → Suspend). - if (m_impl->m_suspended) + // Only render when a frame is currently open. The host can keep + // calling RenderFrame() from its draw callback unconditionally; + // the two non-rendering cases early-out below. + if (!m_impl->m_initialized) + { + // Flag the pre-init case to help hosts diagnose "my draw + // callback is firing but nothing renders" mistakes. The + // externally-suspended case (Runtime::Suspend) is expected + // behavior and stays silent. + DEBUG_TRACE("Babylon::Integrations::View::RenderFrame skipped: View has not yet been initialized. Call View::Resize with the surface's pixel dimensions to begin rendering."); + return; + } + if (impl.m_suspendCount.load(std::memory_order_relaxed) > 0) { return; } @@ -182,16 +245,10 @@ namespace Babylon::Integrations // --------------------------------------------------------------------- // View::Resize // - // Owns deferred Device initialization. On the first call after - // `View::Attach`, this is where the Device is constructed (very - // first Attach on the Runtime) or re-bound to the new surface - // (subsequent Attach), and where the first frame is opened. Folding - // `UpdateWindow` + `UpdateSize` together here is required: the two - // calls MUST stay paired or bgfx will render to the new surface at - // the old size. - // - // On subsequent calls, this is a plain `UpdateSize` against the - // already-bound Device. + // Always stores the host-supplied size on the ViewImpl, then either + // pushes it through to `Device::UpdateSize` (already initialized) + // or kicks `InitializeIfReady` (still uninitialized). This is the + // single source of truth for surface size on the Integrations side. // --------------------------------------------------------------------- void View::Resize(uint32_t width, uint32_t height, CoordinateUnits units) { @@ -199,66 +256,31 @@ namespace Babylon::Integrations RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - if (!m_impl->m_initialized) - { - // First Resize on this Attach: bind the Device to the - // window+size captured at Attach. We must compute DPR from - // the window directly via the standalone free function - // because, on a very first Attach, the Device doesn't - // exist yet; on a re-attach, the Device's stored DPR - // reflects the previous window and may not match the new - // one. - const auto window = m_impl->m_window; - const float dpr = Babylon::Graphics::GetDevicePixelRatio(window); - const auto [lw, lh] = ToLogicalSize(width, height, units, dpr); - ValidateNonZeroSize(lw, lh, "View::Resize logical size"); - - const bool firstAttachEver = !impl.m_device; - if (firstAttachEver) - { - Babylon::Graphics::Configuration config{}; - config.Window = window; - config.Width = lw; - config.Height = lh; - config.MSAASamples = impl.m_options.msaaSamples; - - impl.m_device.emplace(config); - impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); - } - else - { - // Re-attach to an existing Runtime: rebind the surface - // and update the size in lockstep. Plugins, polyfills, - // and any loaded scripts are preserved on the JS side. - impl.m_device->UpdateWindow(window); - impl.m_device->UpdateSize(lw, lh); - } - - // Flip the initialized latch BEFORE Resume() so the open- - // frame logic actually runs. - m_impl->m_initialized = true; + // DPR source: before init, query the window directly because + // the Device either doesn't exist yet (first Attach ever) or is + // still bound to the previous attach's window (re-attach). + // After init, the Device's stored DPR matches the window we're + // bound to and is the authoritative source. + const float dpr = !m_impl->m_initialized + ? Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window) + : impl.m_device->GetDevicePixelRatio(); + const auto [lw, lh] = ToLogicalSize(width, height, units, dpr); + ValidateNonZeroSize(lw, lh, "View::Resize logical size"); - // Open the first frame. On very first Attach this must - // happen BEFORE dispatching `RunFirstAttachInit` so the - // `Device::AddToJavaScript` inside that lambda sees an - // open frame to record into. - m_impl->Resume(); + m_impl->m_size = {lw, lh}; - if (firstAttachEver) - { - impl.RunFirstAttachInit(window); - } - return; + if (m_impl->m_initialized) + { + impl.m_device->UpdateSize(lw, lh); + } + else + { + // Just satisfied the "size known" precondition; try to init. + // Silent no-op if the Runtime is currently externally + // suspended; the eventual `Runtime::Resume` will retry via + // `ViewImpl::Resume`. + m_impl->InitializeIfReady(); } - - // Subsequent Resize on an initialized view: plain UpdateSize - // against the already-bound Device, using the Device's stored - // DPR (which matches the window the Device is currently bound - // to). - const auto [lw, lh] = ToLogicalSize(width, height, units, - impl.m_device->GetDevicePixelRatio()); - ValidateNonZeroSize(lw, lh, "View::Resize logical size"); - impl.m_device->UpdateSize(lw, lh); } #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT From 31524abad875f54c590f495c107318c26250e20f Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 15 May 2026 18:10:56 -0700 Subject: [PATCH 41/71] Win32 initialization ordering --- Apps/Playground/Win32/App.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 9a306c5af..c25f40c41 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -354,12 +354,12 @@ BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) return FALSE; } + RefreshBabylon(hWnd); + ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); EnableMouseInPointer(true); - RefreshBabylon(hWnd); - return TRUE; } From 6defe893a90bc0576bae22c9dda49e2cf5a1e829 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 18 May 2026 14:28:27 -0700 Subject: [PATCH 42/71] Comments cleanup --- .../src/main/cpp/PlaygroundJNI.cpp | 29 ++- .../babylonjs/integrations/BabylonNative.java | 127 +++++-------- .../library/babylonnative/BabylonView.java | 31 ++-- .../playground/PlaygroundActivity.java | 43 ++--- Apps/Playground/Shared/PlaygroundScripts.cpp | 3 +- Apps/Playground/Shared/PlaygroundScripts.h | 23 +-- Apps/Playground/Win32/App.cpp | 21 +-- Apps/Playground/iOS/PlaygroundBootstrap.mm | 12 +- .../Babylon/Graphics/DeviceQueries.h | 26 +-- .../main/cpp/BabylonNativeIntegrations.cpp | 144 ++++++--------- Integrations/Apple/Source/BNRuntime.mm | 35 ++-- Integrations/Apple/Source/BNView.mm | 49 +++-- Integrations/Apple/Source/BNViewDelegate.mm | 14 +- .../Integrations/Android/RuntimeHandle.h | 16 +- .../Babylon/Integrations/Apple/BNRuntime.h | 68 +++---- .../Apple/Babylon/Integrations/Apple/BNView.h | 95 ++++------ .../Shared/Babylon/Integrations/LogLevel.h | 28 +-- .../Shared/Babylon/Integrations/Runtime.h | 99 ++++------ .../Babylon/Integrations/RuntimeOptions.h | 36 ++-- .../Shared/Babylon/Integrations/View.h | 160 +++++------------ Integrations/Source/Runtime.cpp | 149 ++++++--------- Integrations/Source/RuntimeImpl.h | 170 ++++++------------ Integrations/Source/View.cpp | 144 +++++---------- 23 files changed, 537 insertions(+), 985 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp index a8d4bb9a6..3b8d6bf01 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp +++ b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp @@ -1,16 +1,11 @@ -// Playground-specific JNI helper. +// Playground-specific JNI helper. Built into the same .so as the generic +// Integrations/Android JNI (added via target_sources in the Playground +// CMakeLists), so there's a single copy of Babylon::Integrations across +// the process — avoids cross-library handle-passing UB. // -// This file lives alongside the generic Integrations/Android JNI in the -// same shared library (`libBabylonNativeIntegrations.so`) — the -// Playground's CMakeLists adds it via `target_sources(...)`. Keeping -// everything in one .so means a single copy of Babylon::Integrations -// across the whole process; cross-library handle passing UB is avoided. -// -// The only purpose of this helper is to surface -// `Apps/Playground/Shared/PlaygroundScripts.{h,cpp}` to Java so that the -// Babylon.js bootstrap script list stays in one place (shared with the -// other Playground hosts: Win32, iOS, macOS, …) rather than being -// duplicated on the Java side. +// Sole purpose: surface Apps/Playground/Shared/PlaygroundScripts.{h,cpp} +// to Java so the bootstrap script list stays in one place, shared with +// the other Playground hosts. #include #include @@ -32,15 +27,11 @@ Java_com_android_babylonnative_playground_PlaygroundActivity_loadBootstrapScript return; } - // Process-wide one-shot Playground setup (PerfTrace level, etc.). - // Re-calling is idempotent; safe even if multiple PlaygroundActivity - // instances queue bootstrap scripts. + // Idempotent process-wide setup (PerfTrace level, etc.). Playground::Initialize(); - // Queues each Babylon.js bootstrap script (ammo / babylon.max / - // loaders / materials / gui / meshwriter / serializers) onto the - // runtime; they run after the first View::Attach completes engine - // initialization on the JS thread. + // Queue bootstrap scripts; they run after first View::Attach + // completes engine init on the JS thread. Playground::LoadBootstrapScripts(*runtime); } diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java index 5a0124b82..42ebdccea 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/integrations/BabylonNative.java @@ -4,49 +4,45 @@ import android.view.Surface; /** - * JVM binding for the C++ Babylon::Integrations layer. The native methods - * here mirror, byte-for-byte, the {@code extern "C" JNIEXPORT} entry points - * declared in {@code Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp}. + * JVM binding for the C++ Babylon::Integrations layer. Native methods + * mirror byte-for-byte the {@code extern "C" JNIEXPORT} entry points in + * {@code Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp}. * - *

This class is a thin facade — it owns no state and exposes the C++ - * API as static methods. Hosts typically wrap it in their own {@code View} - * subclass (see {@link com.library.babylonnative.BabylonView} for an example). + *

Thin facade — owns no state, exposes the C++ API as static methods. + * Hosts typically wrap it in their own {@code View} subclass (see + * {@link com.library.babylonnative.BabylonView}). * *

Lifecycle: *

    - *
  1. Call {@link #androidGlobalInitialize(Context)} once at app startup - * (typically from {@code Application.onCreate}).
  2. - *
  3. Create a Runtime via {@link #runtimeCreate()} or {@link #runtimeCreate(RuntimeOptions)} and remember - * the returned {@code long} handle.
  4. - *
  5. Optional: queue scripts via {@link #runtimeLoadScript(long, String)} - * — they run after the first {@link #viewAttach(long, Surface)}.
  6. - *
  7. Attach a View via {@link #viewAttach(long, Surface)}; - * call {@link #viewRenderFrame(long)} from your draw loop.
  8. + *
  9. {@link #setContext(Context)} once at app startup.
  10. + *
  11. Create a Runtime via {@link #runtimeCreate()} or + * {@link #runtimeCreate(RuntimeOptions)} and keep the handle.
  12. + *
  13. Optional: queue scripts via {@link #runtimeLoadScript(long, String)}; + * they run after the first {@link #viewAttach(long, Surface)}.
  14. + *
  15. Attach a View and drive {@link #viewRenderFrame(long)} from your draw loop.
  16. *
  17. Tear down with {@link #viewDetach(long)} then {@link #runtimeDestroy(long)}.
  18. *
*/ public final class BabylonNative { /** - * Construction options for {@link BabylonNative#runtimeCreate(RuntimeOptions)}. - * - *

The defaults match the C++ {@code Babylon::Integrations::RuntimeOptions} - * defaults. Fields are intentionally public so Java and Kotlin callers can use + * Construction options. Defaults match the C++ + * {@code Babylon::Integrations::RuntimeOptions}. Fields are public for * simple object-initializer patterns. */ public static final class RuntimeOptions { - /** Optional MSAA sample count for the back buffer. Valid values are 0, 2, 4, 8, and 16. */ + /** Optional MSAA samples (0, 2, 4, 8, 16). */ public Integer msaaSamples = null; - /** Enable the JavaScript debugger when supported by the configured JS engine. Defaults to false. */ + /** Enable the JavaScript debugger (engine-dependent). */ public boolean enableDebugger = false; - /** Enable Babylon::DebugTrace output through the default logcat sink. Defaults to false. */ + /** Enable Babylon::DebugTrace output through the logcat sink. */ public boolean enableDebugTrace = false; - /** Block engine startup until a debugger attaches when supported by the configured JS engine. Defaults to false. */ + /** Block engine startup until a debugger attaches (engine-dependent). */ public boolean waitForDebugger = false; - /** Optional writable file path for a persistent on-disk GPU shader cache. */ + /** Optional persistent on-disk shader cache path. */ public String shaderCachePath = null; } @@ -61,12 +57,9 @@ private BabylonNative() {} // ------------------------------------------------------------------- /** - * Register the application Context. Hosts call this once at app - * startup (typically from {@code Application.onCreate} or the host - * Activity's {@code onCreate}), before constructing any Runtime. - * Calling more than once is harmless — the underlying - * {@code android::global::Initialize} replaces the existing - * Context global ref. + * Register the application Context. Call once at app startup + * (typically from {@code Application.onCreate}) before constructing + * any Runtime. Repeated calls replace the existing Context global ref. */ public static native void setContext(Context context); @@ -83,23 +76,18 @@ public static native void requestPermissionsResult( // Runtime // ------------------------------------------------------------------- - /** Returns an opaque handle owned by the caller; release with {@link #runtimeDestroy(long)}. */ + /** Returns an opaque handle; release with {@link #runtimeDestroy(long)}. */ public static native long runtimeCreate(); /** - * Returns an opaque handle owned by the caller; release with {@link #runtimeDestroy(long)}. - * - *

Pass {@code null} to use the same defaults as {@link #runtimeCreate()}. - * If {@link RuntimeOptions#shaderCachePath} is non-null, the cache is loaded - * on first {@link #viewAttach(long, Surface)} and saved on suspend and on - * {@link #runtimeDestroy(long)}. + * Returns an opaque handle; release with {@link #runtimeDestroy(long)}. + * Pass {@code null} for the same defaults as {@link #runtimeCreate()}. * - *

If {@link RuntimeOptions#shaderCachePath} is non-null but the native - * library was built without {@code BABYLON_NATIVE_PLUGIN_SHADERCACHE}, this - * method throws {@link IllegalStateException} so the misconfiguration surfaces - * at construction time rather than silently dropping the cache. Passing null - * options, or options with null {@code shaderCachePath}, is always safe - * regardless of native build config. + *

If {@link RuntimeOptions#shaderCachePath} is non-null, the cache + * is loaded on first {@link #viewAttach(long, Surface)} and saved on + * suspend / destroy. Throws {@link IllegalStateException} when + * shaderCachePath is set but {@code BABYLON_NATIVE_PLUGIN_SHADERCACHE} + * is disabled in the native build. */ public static native long runtimeCreate(RuntimeOptions options); @@ -109,28 +97,22 @@ public static native void requestPermissionsResult( public static native void runtimeEval(long handle, String source, String sourceUrl); - // Note: there is intentionally no per-Runtime Suspend/Resume on the - // Java surface. Each Runtime auto-subscribes to androidGlobalPause / - // androidGlobalResume in runtimeCreate, so the host Activity calls - // those once per state change and every Runtime in the process - // reacts. Hosts needing finer-grained control should use the C++ API. + // No per-Runtime Suspend/Resume here: each Runtime auto-subscribes to + // pause/resume in runtimeCreate. Hosts call those once per Activity + // state change and every Runtime reacts. Use the C++ API for + // finer-grained control. /** - * Set the platform Surface that XR will render into (typically a - * separate transparent SurfaceView overlay, distinct from the main - * View's surface). Pass {@code null} to clear the XR surface. - * - *

Throws {@link IllegalStateException} if invoked when the - * native library was built without - * {@code BABYLON_NATIVE_PLUGIN_NATIVEXR}. + * Set the Surface that XR renders into (typically a transparent + * SurfaceView overlay). Pass {@code null} to clear. Throws + * {@link IllegalStateException} when {@code BABYLON_NATIVE_PLUGIN_NATIVEXR} + * is disabled in the native build. */ public static native void runtimeSetXrSurface(long handle, Surface surface); /** - * Returns whether an XR session is currently active. Returns - * {@code false} (never throws) when the native library was built - * without {@code BABYLON_NATIVE_PLUGIN_NATIVEXR} — no XR session - * can ever be active in that build. + * Whether an XR session is active. Returns {@code false} (never + * throws) when {@code BABYLON_NATIVE_PLUGIN_NATIVEXR} is disabled. */ public static native boolean runtimeIsXrActive(long handle); @@ -138,12 +120,7 @@ public static native void requestPermissionsResult( // View // ------------------------------------------------------------------- - /** - * Returns an opaque handle owned by the caller; release with {@link #viewDetach(long)}. - * The View queries the surface's pixel-buffer size from the native - * window itself, and the Device queries the screen device-pixel-ratio - * from the system, so no dimensional info needs to be passed from Java. - */ + /** Returns an opaque handle; release with {@link #viewDetach(long)}. */ public static native long viewAttach(long runtimeHandle, Surface surface); public static native void viewDetach(long handle); @@ -151,26 +128,20 @@ public static native void requestPermissionsResult( public static native void viewRenderFrame(long handle); /** - * Push the surface's new pixel-buffer dimensions. Both - * {@code width} and {@code height} are in **physical pixels** — - * pass the values you receive from - * {@code SurfaceHolder.Callback.surfaceChanged} unchanged. The - * native View divides by the device-pixel-ratio internally - * before configuring {@code Babylon::Graphics::Device}. + * Push the surface's new pixel-buffer dimensions. {@code width} and + * {@code height} are in physical pixels — pass the values from + * {@code SurfaceHolder.Callback.surfaceChanged} unchanged. The native + * View divides by DPR internally. */ public static native void viewResize(long handle, int width, int height); /** * Pointer events. Pass {@code MotionEvent.getX/getY} through - * unchanged — Android-native physical-pixel coordinates. The - * native View divides by the device-pixel-ratio internally - * before forwarding to Babylon.js's - * {@code PointerEvent.clientX/clientY} pipeline (which expects - * logical / CSS pixels). + * unchanged (physical pixels); native View converts internally + * before forwarding to Babylon.js's clientX/clientY pipeline. * - *

Throws {@link IllegalStateException} if invoked when the - * native library was built without - * {@code BABYLON_NATIVE_PLUGIN_NATIVEINPUT}. + *

Throws {@link IllegalStateException} when + * {@code BABYLON_NATIVE_PLUGIN_NATIVEINPUT} is disabled. */ public static native void viewPointerDown(long handle, int pointerId, float x, float y); diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index 8ee06f765..ff2207ee1 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -11,27 +11,20 @@ import com.babylonjs.integrations.BabylonNative; /** - * Playground View built on top of {@link BabylonNative}. Borrows a - * Runtime handle from the host (the host is responsible for the - * Runtime's lifetime via {@link BabylonNative#runtimeCreate()} / - * {@link BabylonNative#runtimeDestroy(long)}); this class only owns the - * View handle, which mirrors the underlying Surface lifecycle: - * attach in {@code surfaceCreated}, resize in {@code surfaceChanged}, - * detach in {@code surfaceDestroyed}. + * Playground View built on {@link BabylonNative}. Borrows a Runtime handle + * from the host (which owns the Runtime's lifetime); this class owns only + * the View handle, mirroring the Surface lifecycle: attach in + * {@code surfaceCreated}, resize in {@code surfaceChanged}, detach in + * {@code surfaceDestroyed}. * - *

All sizes and coordinates passed to the native layer are in - * physical pixels (Android's natural unit) — the Device queries the - * screen device-pixel-ratio internally and applies any conversions - * needed at the rendering layer. + *

All sizes and coordinates passed to native are physical pixels — + * the Device queries DPR internally. * - *

Activity lifecycle: the host Activity is responsible for the - * process-wide {@code BabylonNative.setContext}, - * {@code setCurrentActivity}, {@code pause} / {@code resume}, and - * {@code requestPermissionsResult} notifications (see - * {@code PlaygroundActivity.java}). The Runtime automatically subscribes - * to {@code pause} / {@code resume} when created, so the host Activity - * does not need to invoke any per-view pause/resume method — telling - * the JNI layer once is enough for every Runtime in the process. + *

The host Activity owns process-wide {@code setContext} / + * {@code setCurrentActivity} / {@code pause} / {@code resume} / + * {@code requestPermissionsResult} (see {@code PlaygroundActivity}). The + * Runtime auto-subscribes to pause/resume on creation, so there is no + * per-view pause/resume. */ public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, View.OnTouchListener { private static final FrameLayout.LayoutParams childViewLayoutParams = diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 3c18728bd..68f37bf35 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -9,10 +9,9 @@ public class PlaygroundActivity extends Activity { /** - * Native helper bridging to {@code Apps/Playground/Shared/PlaygroundScripts.cpp}, - * which holds the Babylon.js bootstrap script list shared with the - * other Playground hosts (Win32, iOS, macOS, …). Implemented in - * {@code Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp}. + * Bridges to {@code Apps/Playground/Shared/PlaygroundScripts.cpp}, + * which holds the bootstrap script list shared with the other + * Playground hosts. Implemented in PlaygroundJNI.cpp. */ private static native void loadBootstrapScripts(long runtimeHandle); @@ -23,27 +22,21 @@ public class PlaygroundActivity extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); - // Register the application Context with AndroidExtensions::Globals - // (used by NativeCamera, NativeXr, etc.). Belongs at the - // Activity/Application level — not on a per-view basis — because - // it broadcasts to process-wide handlers that aren't refcounted. - // The JNI layer guards against double-initialization internally. + // Register Context/Activity with AndroidExtensions::Globals (used + // by NativeCamera, NativeXr). Process-wide, not per-view. The JNI + // layer guards against double-initialization. BabylonNative.setContext(getApplication()); BabylonNative.setCurrentActivity(this); - // Owner of the Runtime lifetime: created here, destroyed in - // onDestroy. The View only borrows the handle for its surface - // bindings. + // Activity owns the Runtime lifetime; the View only borrows it. BabylonNative.RuntimeOptions runtimeOptions = new BabylonNative.RuntimeOptions(); runtimeOptions.enableDebugger = true; runtimeOptions.enableDebugTrace = true; mRuntimeHandle = BabylonNative.runtimeCreate(runtimeOptions); - // Queue the Babylon.js bootstrap scripts, then the playground - // experience script. Both happen synchronously from this thread; - // the Runtime queues them internally and runs them after the - // first View::Attach completes engine initialization on the JS - // thread, in submission order. + // Queue the bootstrap scripts + experience script. They run after + // first View::Attach completes engine init on the JS thread, in + // submission order. loadBootstrapScripts(mRuntimeHandle); BabylonNative.runtimeLoadScript(mRuntimeHandle, "app:///Scripts/experience.js"); @@ -53,15 +46,13 @@ protected void onCreate(Bundle icicle) { @Override protected void onPause() { - // Hide the view to suppress its draw loop while paused. Visibility - // is restored by onWindowFocusChanged below when the Activity - // returns to the foreground. + // Hide the view to stop its draw loop; onWindowFocusChanged + // restores visibility on return. mView.setVisibility(View.GONE); - // Process-wide notification: every Runtime in this process - // auto-suspends because they each subscribed to this event in - // BabylonNative.runtimeCreate. Same for cross-cutting subsystems - // (NativeCamera, NativeXr) that hook AndroidExtensions::Globals. + // Process-wide: every Runtime auto-suspends (each subscribed in + // runtimeCreate); cross-cutting subsystems (NativeCamera, + // NativeXr) also hook this via AndroidExtensions::Globals. BabylonNative.pause(); super.onPause(); } @@ -74,8 +65,8 @@ protected void onResume() { @Override protected void onDestroy() { - // Surface lifecycle (view detach) has already fired by the time - // we get here; just release the Runtime. + // Surface lifecycle (view detach) has already fired; just + // release the Runtime. if (mRuntimeHandle != 0) { BabylonNative.runtimeDestroy(mRuntimeHandle); mRuntimeHandle = 0; diff --git a/Apps/Playground/Shared/PlaygroundScripts.cpp b/Apps/Playground/Shared/PlaygroundScripts.cpp index bdb17f3e5..da9d00625 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.cpp +++ b/Apps/Playground/Shared/PlaygroundScripts.cpp @@ -7,8 +7,7 @@ namespace Playground { void Initialize(const PlaygroundOptions& options) { - // Process-wide perf-tracing configuration. Used to be done - // inside AppContext's constructor. + // Process-wide perf-tracing configuration. Babylon::PerfTrace::Level perfLevel{Babylon::PerfTrace::Level::Mark}; if (options.PerfTrace.has_value()) { diff --git a/Apps/Playground/Shared/PlaygroundScripts.h b/Apps/Playground/Shared/PlaygroundScripts.h index cc49624eb..123de12f2 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.h +++ b/Apps/Playground/Shared/PlaygroundScripts.h @@ -14,21 +14,16 @@ namespace Playground // the first view. void Initialize(const PlaygroundOptions& options = {}); - // Queue the standard Babylon.js bootstrap scripts (Babylon core, - // loaders, materials, GUI, serializers, plus a few common extras) - // onto `runtime` in dependency order. + // Queue the standard Babylon.js bootstrap scripts (core, loaders, + // materials, GUI, serializers, etc.) onto `runtime` in dependency order. // - // These were historically loaded by `AppContext`'s constructor; the - // `Babylon::Integrations` layer no longer bundles script loading - // (each host decides between the multi-UMD route this helper - // implements and a single pre-bundled `bundle.js` route — see - // SimplifiedAPI.md §4.1 "Loading Babylon.js: two supported routes"). - // We keep the list here so every Playground host stays in sync as - // the bundle list evolves. + // The `Babylon::Integrations` layer doesn't bundle script loading; + // each host picks between this multi-UMD route and a pre-bundled + // `bundle.js` route. Centralizing the list keeps every Playground + // host in sync as the bundle list evolves. // - // Calls to `LoadScript` made before the first `View::Attach` are - // queued on the runtime and dispatched after engine initialization - // completes; this helper relies on that, so it's safe to call - // immediately after `Runtime::Create`. + // LoadScript calls made before the first View::Attach are queued and + // dispatched after engine init, so this is safe to call immediately + // after `Runtime::Create`. void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime); } diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index c25f40c41..566fa89bd 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -1,9 +1,8 @@ // App.cpp : Defines the entry point for the application. // -// Migrated to Babylon::Integrations: this host no longer constructs -// Babylon Native components directly. The cross-platform `Runtime` + -// `View` API handles plugin/polyfill setup, GPU device construction, -// frame rendering, and input forwarding. +// Built on Babylon::Integrations: the cross-platform Runtime + View API +// handles plugin/polyfill setup, GPU device construction, frame rendering, +// and input forwarding. #include "App.h" @@ -93,7 +92,7 @@ namespace Babylon::Integrations::RuntimeOptions MakeRuntimeOptions() { Babylon::Integrations::RuntimeOptions runtimeOptions{}; - runtimeOptions.enableDebugger = true; // matches AppContext default + runtimeOptions.enableDebugger = true; runtimeOptions.enableDebugTrace = options.DebugTrace.value_or(true); runtimeOptions.log = [](Babylon::Integrations::LogLevel level, std::string_view message) { std::string text{message}; @@ -174,8 +173,6 @@ namespace void LoadScripts() { - // Babylon.js bootstrap (core + loaders/materials/gui/serializers). - // Shared with the other Playground hosts via Shared/PlaygroundScripts. Playground::LoadBootstrapScripts(*g_runtime); if (options.Scripts.empty()) @@ -194,9 +191,8 @@ namespace void Uninitialize() { - // Destroy in reverse-construction order: View first (so the - // surface is unbound and the in-flight frame is closed), then - // Runtime (which joins the JS thread). + // View first (unbinds surface, closes in-flight frame), then + // Runtime (joins JS thread). g_view.reset(); g_runtime.reset(); } @@ -210,9 +206,8 @@ namespace QueuePlaygroundOptions(); LoadScripts(); - // First View::Attach triggers GPU device construction, plugin - // initialization on the JS thread, and flushes the queued - // scripts. The View queries the HWND's client rect itself. + // First View::Attach triggers Device construction, plugin init, + // and flushes the queued scripts. g_view = Babylon::Integrations::View::Attach(*g_runtime, hWnd); } } diff --git a/Apps/Playground/iOS/PlaygroundBootstrap.mm b/Apps/Playground/iOS/PlaygroundBootstrap.mm index ac2d10dc0..022824416 100644 --- a/Apps/Playground/iOS/PlaygroundBootstrap.mm +++ b/Apps/Playground/iOS/PlaygroundBootstrap.mm @@ -1,6 +1,5 @@ -// PlaygroundBootstrap.mm — implementation. Calls into the shared C++ -// `Apps/Playground/Shared/PlaygroundScripts.cpp` so the bootstrap -// script list lives in one place. +// PlaygroundBootstrap.mm — calls into Apps/Playground/Shared/PlaygroundScripts.cpp +// so the bootstrap script list stays in one place. #import "PlaygroundBootstrap.h" @@ -9,11 +8,8 @@ #include #include -// Re-declare the internal class extension that exposes the C++ -// `Runtime*` from `BNRuntime`. The actual implementation lives in -// `Integrations/Apple/Source/BNRuntime.mm`; declaring the same class -// extension here just makes the selector visible to the Obj-C++ -// compiler in this translation unit. +// Re-declare the internal class extension that exposes the C++ Runtime* +// from BNRuntime (implementation lives in Integrations/Apple/Source/BNRuntime.mm). @interface BNRuntime () - (Babylon::Integrations::Runtime*)nativeRuntime; @end diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h index c971c7b2f..51192c628 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceQueries.h @@ -4,25 +4,15 @@ namespace Babylon::Graphics { - // Query the screen's device-pixel-ratio for the given native - // window. Free function so callers can obtain the ratio before a - // `Device` has been constructed (e.g. an interop layer converting - // physical → logical pixels for `Configuration::Width/Height` - // ahead of `Device::Device(config)`). + // Query the device-pixel-ratio for `window`. Free function so callers + // can obtain the ratio before a `Device` has been constructed (e.g. + // an interop layer converting physical → logical pixels for + // `Configuration::Width/Height` ahead of `Device::Device(config)`). // - // For an existing `Device`, prefer `Device::GetDevicePixelRatio()` - // — that's the value this function returned when `UpdateWindow` - // was last called, cached on the `Device`. + // For an existing Device, prefer `Device::GetDevicePixelRatio()` — + // that's this function's last-`UpdateWindow` return cached on the Device. // - // Visibility: - // - Declared in `InternalInclude/Babylon/Graphics/` rather than - // in the public `Device.h` because the implementation depends - // on platform-specific window types that are also internal - // (CAMetalLayer, ANativeWindow, HWND, X11 `Window`, etc.) and - // because most cross-platform host code should just use the - // `Device::GetDevicePixelRatio()` instance method. - // - Reachable to consumers of the `GraphicsDeviceContext` target - // (plugins, the Integrations facade). NOT reachable to plain - // `GraphicsDevice` consumers. + // Lives in InternalInclude/ to keep it off the public surface; + // reachable to in-tree consumers via the GraphicsDeviceContext target. float GetDevicePixelRatio(WindowT window); } diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index d0cf192db..552a43f6f 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -1,25 +1,18 @@ // JNI interop for Babylon::Integrations on Android. // // This file is the C++ side of the Android interop layer. It exposes -// `extern "C" JNIEXPORT` entry points that any JVM-language host (Kotlin -// or Java) can call by declaring matching `external fun` / `native` -// methods. The shape of those declarations follows directly from the -// JNI signatures here. +// `extern "C" JNIEXPORT` entry points that any JVM-language host can call +// via matching `external fun` / `native` declarations. // -// We deliberately do not ship a Kotlin or Java class library on top — -// per the plan's non-goals (SimplifiedAPI.md §2), the interop layer's -// job ends at "Kotlin/Java can call into native Babylon code"; designing -// idiomatic high-level wrappers in the host language is left to the -// consumer. +// We deliberately ship no Kotlin/Java class library on top — interop ends +// at "Kotlin/Java can call into Babylon". // -// Convention: opaque C++ pointers cross the JNI boundary as `jlong`. +// Convention: opaque C++ pointers cross JNI as `jlong`. // `unique_ptr::release()` transfers ownership to the JVM side; the -// matching `*Destroy` function calls `delete` on the raw pointer. +// matching `*Destroy` function `delete`s the raw pointer. // -// Java class name on the other side is `com.babylonjs.integrations.BabylonNative` -// (matching the `Java_com_babylonjs_integrations_BabylonNative_*` symbol -// names below). Hosts can choose any class name they like — the Java -// signatures must match the C++ symbols byte-for-byte. +// Symbols here follow `Java_com_babylonjs_integrations_BabylonNative_*`. +// Hosts can use any class name as long as Java signatures match. #include #include @@ -45,16 +38,13 @@ namespace using Babylon::Integrations::RuntimeOptions; using Babylon::Integrations::View; - // Wraps a Runtime with two `android::global` event tickets that - // auto-Suspend/Resume the Runtime in response to process-wide - // Activity lifecycle notifications. Member declaration order matters: - // tickets are declared *after* the Runtime so they're destroyed - // *before* the Runtime, which guarantees no callback can fire on a - // dead Runtime during teardown. + // Wraps a Runtime plus two `android::global` event tickets that + // auto-Suspend/Resume on Activity lifecycle. Tickets are declared + // AFTER the Runtime so they're destroyed BEFORE it — guaranteeing no + // callback fires on a dead Runtime during teardown. // - // Suspend/Resume on Babylon::Integrations::Runtime is reference-counted, - // so the auto-suspend composes safely with explicit host-side - // runtimeSuspend / runtimeResume calls. + // Runtime::Suspend/Resume are refcounted, so this composes safely + // with explicit host-side runtimeSuspend / runtimeResume calls. struct AndroidRuntime { std::unique_ptr runtime; @@ -97,16 +87,11 @@ namespace return result; } - // Throws a Java `IllegalStateException` with the given message. - // Used by interop entry points whose underlying plugin was not - // compiled into the native library, so the misconfiguration - // surfaces as a clean Java exception instead of an - // UnsatisfiedLinkError or silent no-op. - // - // `[[maybe_unused]]` is intentional: when every plugin flag is - // enabled, no `#else` branch in any JNI entry point invokes this - // helper, and -Werror=unused-function would otherwise fail the - // build. + // Throws a Java IllegalStateException. Used when a plugin wasn't + // compiled in, so the misconfiguration surfaces as a clean Java + // exception rather than UnsatisfiedLinkError or silent no-op. + // [[maybe_unused]]: when every plugin flag is enabled no `#else` + // branch references this and -Werror=unused-function would fail. [[maybe_unused]] void ThrowPluginNotEnabled(JNIEnv* env, const char* message) { jclass excClass = env->FindClass("java/lang/IllegalStateException"); @@ -212,32 +197,27 @@ namespace return true; } - // Shared body for the `runtimeCreate` JNI overloads. Wires up the - // Android-default logcat log sink, constructs the Runtime, attaches - // the Activity-lifecycle auto-Suspend/Resume tickets, and returns - // the opaque jlong handle the JVM side holds onto. + // Shared body for the runtimeCreate JNI overloads. Installs the + // default logcat sink, constructs the Runtime, and registers + // Activity-lifecycle auto-Suspend/Resume tickets. jlong MakeRuntimeHandle(RuntimeOptions options) { options.log = [](LogLevel level, std::string_view message) { - // logcat takes a NUL-terminated C string; copy the view. + // logcat needs a NUL-terminated C string. std::string text{message}; __android_log_write(LogPriorityFor(level), "BabylonNative", text.c_str()); }; - // Construct in two phases because the AppStateChangedCallbackTicket - // is neither default-constructible nor move-assignable: we need the - // Runtime pointer in hand before we can register the callbacks, and - // we register the callbacks before the wrapper itself exists. + // Two-phase construction: AppStateChangedCallbackTicket is neither + // default-constructible nor move-assignable, and we need the + // Runtime pointer before registering callbacks. auto runtime = Runtime::Create(std::move(options)); Runtime* runtimePtr = runtime.get(); - // Auto-Suspend/Resume on Activity lifecycle. Hosts call - // pause / resume from their Activity's onPause / onResume; - // every Runtime in the process gets suspended and resumed - // automatically. Since Runtime::Suspend/Resume are refcounted, - // this composes safely with any explicit runtimeSuspend / - // runtimeResume calls the host might make for finer-grained - // reasons (e.g. modal dialogs). + // Auto-Suspend/Resume on process-wide Activity lifecycle. Hosts + // call androidGlobalPause/Resume once per state change; every + // Runtime in the process reacts. Refcounted, so composes with + // any host-side explicit suspend/resume. auto pauseTicket = android::global::AddPauseCallback([runtimePtr]() { runtimePtr->Suspend(); }); @@ -257,12 +237,9 @@ namespace } // Public handle-decoding entry point. Hosts that ship app-specific JNI -// helpers in the same `libBabylonNativeIntegrations.so` (e.g. the -// Playground's PlaygroundJNI.cpp for the Babylon.js bootstrap script -// list) call this to get back a Runtime* from the opaque jlong returned -// by `runtimeCreate`. Direct `reinterpret_cast(handle)` is -// wrong because each Runtime is wrapped in `AndroidRuntime` to hold -// Activity-lifecycle tickets. +// helpers in `libBabylonNativeIntegrations.so` must use this rather than +// `reinterpret_cast(handle)`, because each Runtime is wrapped +// in `AndroidRuntime` to hold Activity-lifecycle tickets. namespace Babylon::Integrations::Android { Runtime* RuntimeFromHandle(jlong handle) @@ -275,19 +252,17 @@ extern "C" { // ===================================================================== -// Android-specific platform lifecycle (platform interop layer surface). +// Android platform lifecycle (interop layer surface). // -// These don't belong on the cross-platform Runtime/View API — they exist -// because Babylon Native plugins like NativeCamera require the host to -// register the JavaVM + current Activity via AndroidExtensions::Globals. -// See SimplifiedAPI.md §4.2 "Interop layer responsibilities". +// Not part of the cross-platform Runtime/View API — exists because +// plugins like NativeCamera require the host to register the JavaVM and +// current Activity via AndroidExtensions::Globals. // ===================================================================== // Set the application Context. Hosts call this once at app startup -// (typically from `Application.onCreate` or the host Activity's -// `onCreate`), before constructing any Runtime. Calling more than -// once is harmless — `android::global::Initialize` deletes any -// existing Context global ref and installs the new one. +// (typically from Application.onCreate) before constructing any Runtime. +// Calling more than once is harmless — Initialize replaces the existing +// Context global ref. JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_setContext( JNIEnv* env, jclass, jobject context) @@ -355,10 +330,9 @@ Java_com_babylonjs_integrations_BabylonNative_requestPermissionsResult( JNIEXPORT jlong JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__(JNIEnv*, jclass) { - // Default Android consumers want logcat output; route Console - // polyfill output and uncaught JS exceptions there. DebugTrace is routed - // there when enabled by RuntimeOptions. Hosts that need different behavior - // can construct a Runtime in C++ directly with their own RuntimeOptions. + // Default Android consumers want logcat output; Console polyfill, + // uncaught JS exceptions, and (when enabled) DebugTrace all route + // there. Hosts wanting different behavior use the C++ API directly. RuntimeOptions options{}; return MakeRuntimeHandle(std::move(options)); } @@ -367,8 +341,8 @@ JNIEXPORT jlong JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Lcom_babylonjs_integrations_BabylonNative_00024RuntimeOptions_2( JNIEnv* env, jclass, jobject javaOptions) { - // RuntimeOptions is intentionally a Java object so Java and Kotlin callers - // can use a stable construction API as RuntimeOptions grows. + // RuntimeOptions is a Java object so callers have a stable + // construction API as fields are added. RuntimeOptions options{}; if (!ApplyJavaRuntimeOptions(env, javaOptions, options)) { @@ -380,9 +354,8 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Lcom_babylonjs_inte JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong handle) { - // Member dtor order (reverse declaration): resumeTicket → pauseTicket - // → runtime. The tickets unsubscribe before the Runtime is destroyed, - // so no callback can fire on a dead Runtime during teardown. + // Reverse declaration order: tickets unsubscribe before the Runtime + // is destroyed, so no callback fires on a dead Runtime. delete AsAndroidRuntime(handle); } @@ -400,13 +373,11 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeEval( AsRuntime(handle)->Eval(ToStdString(env, source), ToStdString(env, sourceUrl)); } -// Note: there is intentionally no per-Runtime Suspend/Resume on the JNI -// surface. Activity-lifecycle Suspend/Resume is wired up automatically -// inside runtimeCreate above (each Runtime subscribes to -// android::global pause/resume callbacks). Hosts only call -// `androidGlobalPause` / `androidGlobalResume` once per Activity state -// change; every Runtime in the process reacts. Hosts that need -// finer-grained control should use the C++ API directly. +// No per-Runtime Suspend/Resume on the JNI surface: lifecycle wiring +// happens automatically in MakeRuntimeHandle via the android::global +// pause/resume tickets. Hosts call androidGlobalPause/Resume once per +// Activity state change; every Runtime in the process reacts. Use the +// C++ API for finer-grained control. JNIEXPORT void JNICALL Java_com_babylonjs_integrations_BabylonNative_runtimeSetXrSurface( @@ -434,9 +405,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, #if BABYLON_NATIVE_PLUGIN_NATIVEXR return AsRuntime(handle)->IsXrActive() ? JNI_TRUE : JNI_FALSE; #else - // State query: "no XR session is active" is the correct answer - // when XR isn't compiled in, so this is intentionally a - // non-throwing path. + // Non-throwing: "no XR active" is correct when XR is off. (void)handle; return JNI_FALSE; #endif @@ -465,9 +434,8 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( ANativeWindow_release(window); return 0; } - // The View's Device::UpdateWindow has acquired its own reference - // on the ANativeWindow internally (bgfx retains it for the surface - // binding lifetime). We can release our local acquire here. + // bgfx retains its own reference on the ANativeWindow for the + // surface-binding lifetime, so release our local acquire here. ANativeWindow_release(window); return reinterpret_cast(view.release()); } @@ -501,7 +469,7 @@ Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; - // Java callers pass `MotionEvent.getX/getY`, which are in physical pixels. + // MotionEvent.getX/getY are physical pixels. AsView(handle)->OnPointerDown(static_cast(pointerId), x, y, Babylon::Integrations::CoordinateUnits::Physical); #else diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index 86e340d3d..a3deefafe 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -20,21 +20,19 @@ uint8_t ToRuntimeMSAASamples(NSNumber* msaaSamples) return value >= 0 && value <= UINT8_MAX ? static_cast(value) : 0; } - // Lazily-initialized process-wide logger used by the default log - // sink. Subsystem matches the Babylon Native CFBundleIdentifier - // convention so Console.app / `log stream` can filter it cleanly. + // Process-wide os_log channel for the default log sink. Subsystem + // matches the Babylon Native CFBundleIdentifier convention so + // Console.app / `log stream` can filter it. os_log_t BabylonNativeLogger() { static os_log_t logger = os_log_create("com.babylonjs.babylonnative", "Runtime"); return logger; } - // Map LogLevel onto the closest os_log type. os_log has no "warn" - // distinct from "default", so Warn folds into DEFAULT (matching - // Apple's own console.warn → default mapping). DEBUG and INFO are - // filtered out of release builds and Console.app by default, so - // routing Verbose there keeps DebugTrace spam out of the way unless - // a developer explicitly opts in (`log stream --level debug ...`). + // Map LogLevel onto os_log types. os_log has no distinct "warn", so + // Warn folds into DEFAULT (matches Apple's console.warn → default). + // DEBUG/INFO are filtered out of release builds and Console.app by + // default, so Verbose lands there to suppress DebugTrace noise. os_log_type_t ToOSLogType(Babylon::Integrations::LogLevel level) { switch (level) @@ -79,12 +77,10 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions options.enableDebugTrace = runtimeOptions.enableDebugTrace ? true : false; options.waitForDebugger = runtimeOptions.waitForDebugger ? true : false; } - // Default log sink: route through `os_log_with_type` so the - // level survives all the way to Console.app / `log stream`. - // Hosts that need their own routing should drop down to the - // C++ API. `%{public}.*s` is required to print the message - // payload; without `{public}` os_log redacts non-scalar - // arguments as `` in release builds. + // Default log sink: route through os_log_with_type so the level + // reaches Console.app / `log stream`. Hosts wanting custom routing + // should use the C++ API. `%{public}.*s` is required — without + // `{public}` os_log redacts the payload as `` in release. options.log = [](Babylon::Integrations::LogLevel level, std::string_view message) { os_log_with_type(BabylonNativeLogger(), ToOSLogType(level), "%{public}.*s", @@ -95,10 +91,8 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions #if BABYLON_NATIVE_PLUGIN_SHADERCACHE options.shaderCachePath = runtimeOptions.shaderCachePath.UTF8String; #else - // Caller explicitly asked for shader caching but the - // plugin wasn't compiled in. Fail loudly rather than - // silently dropping the cache on the floor (which would - // be hard to diagnose at runtime). + // Fail loudly: silently dropping a caller-supplied cache path + // would be hard to diagnose later. @throw [NSException exceptionWithName:@"BabylonNativePluginNotEnabledException" reason:@"shaderCachePath was provided but BABYLON_NATIVE_PLUGIN_SHADERCACHE was not enabled at native build time." @@ -164,8 +158,7 @@ - (BOOL)isXRActive #if BABYLON_NATIVE_PLUGIN_NATIVEXR return _runtime->IsXrActive() ? YES : NO; #else - // State query: "no XR session is active" is the correct answer when - // XR isn't compiled in, so this is intentionally a non-throwing path. + // Non-throwing: "no XR active" is the correct answer when XR is off. return NO; #endif } diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index 65e870ce3..8634d8bb1 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -18,9 +18,8 @@ @implementation BNView BNRuntime* _runtime; MTKView* _mtkView; - // When BNView auto-installs a default delegate (because the host - // didn't set one), it's held here so the strong reference outlives - // the MTKView's `weak` delegate slot. Stays nil if the host + // When BNView auto-installs a default delegate, hold a strong ref + // here to outlive MTKView's `weak` delegate slot. Nil when the host // installed their own delegate before constructing BNView. BNViewDelegate* _managedDelegate; } @@ -36,21 +35,16 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view _runtime = runtime; _mtkView = view; - // MTKView's underlying layer is always a CAMetalLayer (its - // +layerClass override). + // MTKView's layer is always CAMetalLayer (via +layerClass). CAMetalLayer* layer = (CAMetalLayer*)view.layer; - // View::Attach is intentionally lightweight: it just stashes - // the layer pointer. No size query, no Device construction — - // and therefore nothing host-recoverable to throw. The MTKView - // delegate's `mtkView:drawableSizeWillChange:` (forwarded by - // BNViewDelegate to `-resizeWithWidth:height:`) is what - // actually drives Device construction + first-frame opening, - // on the first call. MTKView fires this callback before its - // first draw in normal Cocoa usage, so this bootstrap is - // automatic. Hosts that drive MTKView in unusual ways (e.g. - // hidden preload) can call `-resizeWithWidth:height:` directly - // with the surface's current pixel size. + // View::Attach is lightweight (just stashes the layer). Device + // construction is driven later by `-resizeWithWidth:height:`, + // which BNViewDelegate forwards from MTKView's + // `mtkView:drawableSizeWillChange:`. MTKView fires that before + // its first draw, so bootstrap is automatic. Hosts driving + // MTKView in unusual ways can call `-resizeWithWidth:height:` + // directly with the surface's pixel size. _view = Babylon::Integrations::View::Attach( *runtime.nativeRuntime, (__bridge CA::MetalLayer*)layer); @@ -60,11 +54,10 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view return nil; } - // If the host hasn't installed their own MTKViewDelegate by - // now, install a default `BNViewDelegate` so frames start - // flowing without any extra host wiring. Done AFTER Attach so - // any drawableSizeWillChange: dispatched as a side-effect of - // the assignment doesn't reach us before _view is constructed. + // If the host hasn't installed an MTKViewDelegate, install a + // default BNViewDelegate. Done AFTER Attach so any + // drawableSizeWillChange: triggered by the assignment doesn't + // reach us before _view is constructed. if (view.delegate == nil) { _managedDelegate = [[BNViewDelegate alloc] initWithView:self]; @@ -76,8 +69,7 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view - (void)dealloc { - // Only clear the MTKView's delegate slot if it still points at the - // delegate we installed; never disturb a host-installed delegate. + // Only clear if it's still ours; never disturb a host-installed delegate. if (_managedDelegate != nil && _mtkView.delegate == _managedDelegate) { _mtkView.delegate = nil; @@ -86,12 +78,11 @@ - (void)dealloc - (void)renderFrame { - // The runtime owns the XR overlay view (handed to it via - // -setXrView:), so it's the natural place to keep its visibility - // in sync with the XR session state. Doing this here means hosts - // never have to manage the XR overlay themselves, regardless of - // whether the runtime's frame is being driven by the auto-installed - // BNViewDelegate, a host subclass, or a fully-custom delegate. + // The runtime owns the XR overlay view (via -setXrView:), so it's + // the natural place to keep its visibility in sync. Doing this here + // means hosts never need to manage the overlay themselves regardless + // of whether frames are driven by the auto-installed delegate, a + // host subclass, or a fully-custom delegate. [_runtime updateXrViewIfNeeded]; if (_view) diff --git a/Integrations/Apple/Source/BNViewDelegate.mm b/Integrations/Apple/Source/BNViewDelegate.mm index 6a07a357a..66b1bf7d0 100644 --- a/Integrations/Apple/Source/BNViewDelegate.mm +++ b/Integrations/Apple/Source/BNViewDelegate.mm @@ -17,11 +17,10 @@ bool IsFinitePositive(CGFloat value) @implementation BNViewDelegate { - // BNView holds the auto-installed delegate strongly, and - // host-installed subclass delegates are typically held strongly by - // the host's own controller — so a weak back-reference here is - // sufficient and avoids any chance of a retain cycle if a subclass - // captures the BNView in a closure or property. + // BNView retains the auto-installed delegate strongly; host + // subclasses are typically held by the host's controller. Weak + // is sufficient and avoids retain cycles if a subclass captures + // the BNView. __weak BNView* _view; } @@ -53,9 +52,8 @@ - (void)mtkView:(MTKView* __unused)v drawableSizeWillChange:(CGSize)size - (void)drawInMTKView:(MTKView* __unused)v { - // `[_view renderFrame]` already handles the XR overlay visibility - // toggle internally via the runtime, so subclasses that override - // `drawInMTKView:` and call `super` get the behavior for free. + // [_view renderFrame] handles the XR overlay visibility toggle, so + // subclasses that override this and call super get it for free. [_view renderFrame]; } diff --git a/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h b/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h index 604c7faa3..102ca454c 100644 --- a/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h +++ b/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h @@ -6,16 +6,10 @@ namespace Babylon::Integrations::Android { - // Convert an opaque jlong handle (as returned by `runtimeCreate` in - // `BabylonNativeIntegrations.cpp`) back to a Runtime pointer. - // - // The Android JNI layer wraps each Runtime in an internal struct - // that also holds Activity-lifecycle event tickets, so a direct - // `reinterpret_cast(handle)` is incorrect — hosts that - // ship their own JNI helpers alongside `libBabylonNativeIntegrations.so` - // (e.g. for app-specific bootstrap routines) must go through this - // function to resolve the handle correctly. - // - // Returns nullptr if `handle` is 0. + // Convert a jlong handle from `runtimeCreate` back to a Runtime*. + // The JNI layer wraps each Runtime in an internal struct that also + // holds Activity-lifecycle tickets, so `reinterpret_cast` + // is wrong — hosts shipping their own JNI helpers must go through + // this. Returns nullptr if handle is 0. Runtime* RuntimeFromHandle(jlong handle); } \ No newline at end of file diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h index 1ef9c3f5c..5856757d1 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h @@ -3,8 +3,6 @@ // // Swift consumers see this through the auto-generated Swift bridge // (BNRuntime is exposed to Swift as `BNRuntime`). -// -// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. #pragma once @@ -39,67 +37,51 @@ NS_ASSUME_NONNULL_BEGIN @interface BNRuntime : NSObject -/// Constructs the runtime: starts the JS engine + thread, sets up -/// non-GPU polyfills and plugins. Cheap and synchronous; no GPU -/// device is created yet (that happens on the first `BNView` attach). -/// Default options: JS debugger off, DebugTrace off, log routes to `NSLog`. +/// Constructs the runtime: starts the JS engine/thread and sets up +/// non-GPU polyfills/plugins. The GPU Device is deferred to the first +/// `BNView` attach. Default options: debugger off, DebugTrace off, +/// log routes to `os_log`. - (instancetype)init; -/// Constructs the runtime with platform-friendly options. Pass nil to use -/// the same defaults as `init`. -/// -/// If `options.shaderCachePath` is non-`nil`, the cache is loaded on first -/// `BNView` attach and saved on `suspend` and on deallocation. +/// Constructs the runtime with platform-friendly options (nil = same +/// defaults as `init`). /// -/// If `options.shaderCachePath` is non-`nil` but the native library was built -/// without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, this initializer raises an -/// `NSException` (name -/// `BabylonNativePluginNotEnabledException`) so the misconfiguration -/// surfaces at construction time rather than silently dropping the -/// cache. Passing nil options, or options with nil `shaderCachePath`, is -/// always safe regardless of build config. +/// `options.shaderCachePath`: loaded on first BNView attach; saved on +/// `suspend` and on deallocation. Raises +/// `BabylonNativePluginNotEnabledException` when non-nil but the +/// native library was built without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`. - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)options NS_DESIGNATED_INITIALIZER; -/// Load a script from a URL onto the JS thread. Calls made before -/// the first `BNView` is created are queued internally and dispatched -/// after engine initialization completes during that first attach. -/// Calls after the first attach are dispatched immediately. +/// Load a script from a URL onto the JS thread. Calls made before the +/// first `BNView` is created are queued and dispatched after engine +/// init completes during that first attach; calls after first attach +/// are dispatched immediately. - (void)loadScript:(NSString*)url; -/// Evaluate JavaScript source on the JS thread. Same queueing -/// semantics as `loadScript`. +/// Evaluate JavaScript on the JS thread. Same queueing as `loadScript`. - (void)eval:(NSString*)source sourceURL:(NSString*)sourceURL; /// Reference-counted suspend. While suspended, JS timers pause and -/// any attached `BNView` becomes a no-op for `renderFrame` (the host -/// can keep calling it from its draw callback unconditionally; -/// nothing happens until `resume`). +/// any attached `BNView` becomes a no-op for `renderFrame`. - (void)suspend; -/// Decrement the suspend count; resume the JS thread when the count -/// reaches zero. +/// Decrement the suspend count; resume when it reaches zero. - (void)resume; /// Whether the runtime is currently suspended. @property (nonatomic, readonly, getter=isSuspended) BOOL suspended; -/// Set the platform view that XR will render into (typically a -/// separate transparent `MTKView` overlay, distinct from the main -/// view's Metal layer). Pass `nil` to clear the XR surface. Safe to -/// call before the first `BNView` attach; the value is applied when -/// NativeXr finishes initializing during that first attach. +/// Set the view that XR will render into (typically a transparent +/// MTKView overlay distinct from the main view's Metal layer). Pass +/// `nil` to clear. Safe to call before the first `BNView` attach; +/// applied when NativeXr finishes initializing. /// -/// Raises an `NSException` (name -/// `BabylonNativePluginNotEnabledException`) if invoked when -/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` was not enabled at native build -/// time. +/// Raises `BabylonNativePluginNotEnabledException` when +/// `BABYLON_NATIVE_PLUGIN_NATIVEXR` is not enabled at native build time. - (void)setXrView:(nullable MTKView*)xrView; -/// `YES` while an XR session is active. Updated from the JS thread -/// by NativeXr's internal session-state callback; safe to poll from -/// any thread. Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` was -/// not enabled at native build time (no XR session can ever be active -/// in that build). +/// `YES` while an XR session is active. Safe to poll from any thread. +/// Returns `NO` when `BABYLON_NATIVE_PLUGIN_NATIVEXR` is not enabled. @property (nonatomic, readonly, getter=isXRActive) BOOL xrActive; @end diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h index 61debc5ce..d1bff00aa 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h @@ -1,21 +1,14 @@ -// BNView.h — public Obj-C interface for the Babylon::Integrations -// view on Apple platforms. +// BNView.h — public Obj-C interface for the Babylon::Integrations view +// on Apple platforms. // -// Construct against a host-provided `MTKView`. The first `BNView` -// constructed against a given `BNRuntime` triggers GPU device -// construction, plugin initialization, and queued-script flushing. -// Subsequent views attached to the same runtime are cheap surface -// rebinds. +// Construct against a host-provided `MTKView`. The first BNView against +// a given BNRuntime triggers GPU device construction, plugin init, and +// queued-script flushing. Subsequent attaches are cheap surface rebinds. // -// By default, if the MTKView has no delegate when `BNView` is -// constructed, BNView installs and strong-holds a `BNViewDelegate` -// for you and the host doesn't have to wire anything up. If the host -// wants to interleave per-frame work, they assign their own -// `MTKViewDelegate` (typically a `BNViewDelegate` subclass) to the -// MTKView before constructing the BNView; in that case BNView leaves -// the host's delegate alone. -// -// See SimplifiedAPI.md §4.2 / §5 for the design and usage examples. +// If the MTKView has no delegate when BNView is constructed, BNView +// installs and retains a `BNViewDelegate` automatically. If the host +// installed a delegate first (typically a BNViewDelegate subclass for +// per-frame work), BNView leaves it alone. #pragma once @@ -28,22 +21,18 @@ NS_ASSUME_NONNULL_BEGIN -/// Default `MTKViewDelegate` implementation that drives a `BNView`. -/// Forwards `drawInMTKView:` to `[bnView renderFrame]` and -/// `mtkView:drawableSizeWillChange:` to `[bnView resizeWithWidth:height:]`. -/// -/// `BNView` automatically installs and retains an instance of this -/// class when constructed against an `MTKView` that has no delegate -/// yet, so most hosts never need to construct one explicitly. +/// Default `MTKViewDelegate` implementation that drives a BNView. +/// Forwards `drawInMTKView:` → `[bnView renderFrame]` and +/// `mtkView:drawableSizeWillChange:` → `[bnView resizeWithWidth:height:]`. /// -/// To insert per-frame work, subclass `BNViewDelegate`, override the -/// delegate methods, and call `super` to keep the default forwarding -/// behavior. +/// BNView installs and retains one automatically when constructed +/// against an MTKView that has no delegate. To insert per-frame work, +/// subclass this, override the delegate methods, and call `super`. @interface BNViewDelegate : NSObject -/// Initialize a delegate that drives `view`. The reference is held -/// weakly; the host (or BNView, when this is the auto-installed -/// instance) is responsible for keeping the BNView alive. +/// Initialize a delegate that drives `view`. The reference is weak; +/// the host (or BNView, for the auto-installed instance) keeps the +/// BNView alive. - (nullable instancetype)initWithView:(BNView*)view NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; @@ -54,50 +43,36 @@ NS_ASSUME_NONNULL_BEGIN @interface BNView : NSObject /// Attach `runtime` to render into `view`. On the first attach for a -/// given runtime, this triggers GPU device construction and engine -/// initialization. Subsequent attaches just rebind the surface. +/// given runtime this triggers GPU device construction and engine +/// initialization; subsequent attaches just rebind the surface. /// /// Returns `nil` if `runtime` or `view` is `nil`, or if the underlying -/// `Babylon::Integrations::View::Attach` fails after view-size -/// preconditions are met. -/// -/// Raises an `NSException` (name -/// `BabylonNativeInvalidViewException`) if `view` has neither a finite, -/// non-zero `drawableSize` nor finite, non-zero bounds at attach time. -/// Hidden preload views should set a small finite, non-zero `drawableSize` -/// explicitly. +/// `Babylon::Integrations::View::Attach` fails. /// -/// **Delegate management:** If `view.delegate` is `nil` at the time of -/// construction, BNView creates a `BNViewDelegate` and assigns it to -/// `view.delegate`, holding it strongly for the lifetime of the -/// BNView (so the host doesn't have to retain it themselves — -/// `MTKView.delegate` is a `weak` reference). If `view.delegate` is -/// already set, BNView does *not* touch it; the host is expected to -/// drive `-renderFrame` and `-resize(width:height:)` themselves -/// (typically via their own `MTKViewDelegate` or a `BNViewDelegate` -/// subclass). +/// **Delegate management:** If `view.delegate` is nil, BNView creates a +/// BNViewDelegate and assigns it (held strongly for the BNView's +/// lifetime, since MTKView.delegate is `weak`). If the host already +/// set a delegate, BNView leaves it alone and the host drives +/// `-renderFrame` and `-resizeWithWidth:height:`. - (nullable instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view; -/// Render exactly one frame. Call from your `MTKViewDelegate -/// drawInMTKView:`, or rely on the auto-installed `BNViewDelegate` -/// to call it for you. +/// Render exactly one frame. Call from your MTKViewDelegate's +/// `drawInMTKView:`, or rely on the auto-installed BNViewDelegate. - (void)renderFrame; /// Inform the runtime that the underlying surface's pixel-buffer size -/// has changed. Sizes are in physical pixels — the same convention as -/// `CAMetalLayer.drawableSize`. +/// has changed. Sizes are in physical pixels (same convention as +/// `CAMetalLayer.drawableSize`). - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height NS_SWIFT_NAME(resize(width:height:)); -/// Forward a pointer-down event. `x`, `y` are in logical (CSS) pixels -/// — pass `UITouch.location(in:)` (UIKit) or `NSEvent.locationInWindow` +/// Forward a pointer event. `x`, `y` are logical (CSS) pixels — pass +/// `UITouch.location(in:)` (UIKit) or `NSEvent.locationInWindow` /// (AppKit) coordinates through unchanged. /// -/// Raises an `NSException` (name -/// `BabylonNativePluginNotEnabledException`) if invoked when -/// `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` was not enabled at native -/// build time. The same applies to `pointerMove:atX:y:` and -/// `pointerUp:atX:y:` below. +/// Raises `BabylonNativePluginNotEnabledException` when +/// `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` is not enabled. Same applies to +/// `pointerMove:` and `pointerUp:`. - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(pointerDown(id:x:y:)); diff --git a/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h b/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h index 0df06051a..d748da87b 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h +++ b/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h @@ -2,26 +2,16 @@ namespace Babylon::Integrations { - // Severity levels for the optional log callback on RuntimeOptions. + // Severity levels for the RuntimeOptions log callback. Ordered by + // increasing severity so hosts can do `level >= LogLevel::Warn` filtering. // - // `Verbose` is used for low-priority diagnostic output: currently - // `Babylon::DebugTrace` messages, which are produced only when the - // host opts in via `RuntimeOptions::enableDebugTrace`. Hosts that - // want a quieter log can filter on `level > LogLevel::Verbose`. - // - // The next three (Log / Warn / Error) mirror - // `Babylon::Polyfills::Console::LogLevel` and are used for - // `console.log` / `console.warn` / `console.error` calls. - // - // `Fatal` is used for **uncaught** JavaScript exceptions that - // propagated past every JS-side handler. The engine state may be - // inconsistent after a Fatal; a host that wants to terminate the - // process on uncaught errors can do so from inside its log - // callback (e.g. `if (level == LogLevel::Fatal) std::quick_exit(1);`). - // - // Values are ordered by increasing severity so hosts can compare - // them (`level >= LogLevel::Warn` etc.) for level-threshold - // filtering. + // Verbose : `Babylon::DebugTrace` messages (only when + // `enableDebugTrace` is set). + // Log/Warn/Error : `console.log/warn/error`. Mirrors + // `Babylon::Polyfills::Console::LogLevel`. + // Fatal : uncaught JS exceptions that escaped every JS-side handler. + // Engine state may be inconsistent after a Fatal; hosts can + // terminate from the callback if desired. enum class LogLevel { Verbose, diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 0d45935b6..934acfcfb 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -15,30 +15,18 @@ namespace Babylon::Integrations // Long-lived: typically created once per app/process. Sets up the // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU - // polyfills/plugins. Construction is cheap and synchronous — - // no GPU device exists yet. Device construction and GPU plugin - // initialization (NativeEngine, etc.) are deferred to the first - // `View::Attach` call. - // - // See SimplifiedAPI.md §4.1 for the full design. + // polyfills/plugins. Construction is cheap and synchronous; the GPU + // Device and plugins (NativeEngine, etc.) are deferred to the first + // `View::Attach`. class Runtime { public: static std::unique_ptr Create(RuntimeOptions options = {}); - // // Future construction mode — adopt a host-owned Babylon::JsRuntime - // // instead of letting Runtime construct its own AppRuntime+JsRuntime. - // // Intended for hosts that already own a JS engine and want - // // Babylon Native plugins to live inside it (e.g. React Native: - // // Hermes/JSC + CallInvoker dispatcher). The Integrations layer - // // never sees JSI directly — only Babylon::JsRuntime, which the - // // host wires up against whatever JS engine they have. - // // - // // In Attach mode `~Runtime` does NOT tear down the JS engine - // // (the host owns it); Suspend/Resume only DisableRendering on - // // the Device since the JS thread isn't ours to pause. Same - // // instance API as Create-mode otherwise. See SimplifiedAPI.md - // // §4.1 "Construction modes". + // // Future construction mode: adopt a host-owned Babylon::JsRuntime + // // (e.g. React Native with Hermes/JSC + CallInvoker). In Attach mode + // // `~Runtime` does NOT tear down the JS engine, and Suspend/Resume + // // only toggles Device rendering. // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, // RuntimeOptions options = {}); @@ -52,49 +40,36 @@ namespace Babylon::Integrations // ----- JS interaction ----- // - // Calls made before the first `View::Attach` are queued internally - // and dispatched onto the JS thread after engine initialization - // completes during that first Attach. Calls made after the first - // Attach are dispatched immediately. + // Calls made before the first `View::Attach` are queued and dispatched + // onto the JS thread after engine initialization completes during that + // first Attach. Calls after first Attach are dispatched immediately. // - // Threading: these methods are NOT internally synchronized. - // Hosts should call them from a single thread (typically the - // host's UI/main thread), matching the existing contract of - // `Babylon::ScriptLoader` and `Babylon::AppRuntime::Dispatch`. + // Not internally synchronized — call from a single thread (typically + // the host's UI/main thread), matching `Babylon::ScriptLoader` / + // `AppRuntime::Dispatch`. void LoadScript(std::string_view url); void Eval(std::string_view source, std::string_view sourceUrl = {}); - // Escape hatch: post `callback` onto the JS thread. The callback - // runs after any pending init has completed. Useful for installing - // custom Napi globals, registering ObjectWrap classes, capturing - // `Napi::FunctionReference`s for native→JS calls, etc. - // - // Threading: same single-thread contract as LoadScript / Eval. + // Escape hatch: post `callback` onto the JS thread after any pending + // init completes. Useful for installing custom Napi globals, registering + // ObjectWrap classes, or capturing FunctionReferences for native→JS calls. + // Same single-thread contract as LoadScript / Eval. void RunOnJsThread(std::function callback); // ----- Suspend / Resume ----- // - // Orthogonal to view attachment. Use when the host app is - // backgrounded, throttled, or otherwise should not be doing work - // (iOS applicationWillResignActive, Android onPause, modal - // dialogs, power-saving mode). While suspended: + // Orthogonal to view attachment. Use for backgrounding, throttling, + // modal dialogs, etc. While suspended: // - JS timers (setTimeout/setInterval) pause. // - In-flight microtasks complete; no new tasks are dispatched. - // - Any attached View becomes a no-op for RenderFrame() — the - // host can keep calling it from its draw callback; nothing - // happens until Resume(). - // Calls are reference-counted; nesting is safe. + // - Any attached View's RenderFrame() becomes a no-op. + // Reference-counted; nesting is safe. // - // Threading: `Suspend` / `Resume` must be called from the frame - // thread (the same thread that drives `View::RenderFrame` / - // `View::Resize`). Hosts wiring these to platform lifecycle - // callbacks get this for free — iOS / macOS / Android / Win32 / - // UWP lifecycle callbacks all fire on the main / UI thread. A - // host that wants to suspend from a background thread should - // marshal the call to its main thread first, the same way it - // would for any other View / Runtime entry point. `IsSuspended` - // is the exception: it loads the suspend count atomically and - // is safe to read from any thread. + // `Suspend` / `Resume` must be called from the frame thread (same + // thread driving `View::RenderFrame` / `View::Resize`). Platform + // lifecycle callbacks fire on the main thread already, so wiring them + // up gets this for free. `IsSuspended` is atomic and safe from any + // thread. void Suspend(); void Resume(); bool IsSuspended() const; @@ -102,23 +77,17 @@ namespace Babylon::Integrations #if BABYLON_NATIVE_PLUGIN_NATIVEXR // ----- XR session control ----- // - // Set the platform window XR will render into. The `void*` - // type carries: - // Android : ANativeWindow* (typically from a separate - // transparent SurfaceView overlay) - // Apple : CAMetalLayer* / MTKView* (a separate Metal layer - // distinct from the main View's layer) + // Set the platform window XR will render into: + // Android : ANativeWindow* (typically a transparent SurfaceView overlay) + // Apple : CAMetalLayer* / MTKView* (separate Metal layer from the main View) // - // Pass nullptr to clear the XR surface. Safe to call before - // the first `View::Attach`; the supplied window is applied - // when NativeXr finishes initializing during that first Attach. - // Safe to call from any thread. + // Pass nullptr to clear. Safe to call before the first `View::Attach` + // (the window is applied when NativeXr finishes initializing) and + // from any thread. void SetXrWindow(void* nativeWindow); - // True while an XR session is active. Updated from the JS - // thread by NativeXr's internal session-state callback; - // atomic so it can be polled from any thread (e.g. a host's - // draw callback choosing between rendering targets). + // True while an XR session is active. Atomic; safe to poll from any + // thread (e.g. a draw callback selecting render targets). bool IsXrActive() const; #endif diff --git a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h index 15eb43528..379c399bb 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h @@ -27,33 +27,25 @@ namespace Babylon::Integrations bool waitForDebugger{false}; // Optional log sink. Receives: - // - `console.{log,warn,error}` output → LogLevel::{Log,Warn,Error} - // - `Babylon::DebugTrace` output → LogLevel::Verbose, when - // enableDebugTrace is true - // - Uncaught JS exceptions → LogLevel::Fatal + // - `console.{log,warn,error}` → LogLevel::{Log,Warn,Error} + // - `Babylon::DebugTrace` output → LogLevel::Verbose (when + // enableDebugTrace is true) + // - Uncaught JS exceptions → LogLevel::Fatal // - // If unset, ordinary log output is silently discarded and - // uncaught exceptions fall back to - // `Babylon::AppRuntime::DefaultUnhandledExceptionHandler` - // (which writes to the program output). - // - // Hosts that want process termination on uncaught exceptions - // (matching the historical AppContext behavior) can do so from - // inside this callback, e.g. - // - // if (level == LogLevel::Fatal) std::quick_exit(1); + // If unset, log output is discarded and uncaught exceptions fall + // back to `Babylon::AppRuntime::DefaultUnhandledExceptionHandler`. + // Hosts that want process termination on uncaught exceptions can do + // so from this callback, e.g. `if (level == Fatal) std::quick_exit(1);`. std::function log; #if BABYLON_NATIVE_PLUGIN_SHADERCACHE - // Optional path for persisting the GPU shader cache across - // sessions. If non-empty: - // - Loaded synchronously during the first `View::Attach` (after - // `ShaderCache::Enable`). Missing or unreadable file: ignored. + // Optional path for persisting the GPU shader cache across sessions. + // If non-empty: + // - Loaded synchronously during the first `View::Attach` (missing + // or unreadable file: ignored). // - Saved asynchronously during `Runtime::Suspend` (queued onto - // the JS thread before the suspension blocker) so the - // on-disk cache reflects any shaders compiled this session. - // - Saved synchronously during `~Runtime` so a final write - // happens before the JS thread is torn down. + // the JS thread before the suspension blocker). + // - Saved synchronously during `~Runtime`. std::string shaderCachePath; #endif }; diff --git a/Integrations/Include/Shared/Babylon/Integrations/View.h b/Integrations/Include/Shared/Babylon/Integrations/View.h index 5f4582919..dbba9b197 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/View.h +++ b/Integrations/Include/Shared/Babylon/Integrations/View.h @@ -10,83 +10,50 @@ namespace Babylon::Integrations class Runtime; struct ViewImpl; - // Tag for the units of coordinates and dimensions handed to View - // methods. Babylon.js consumes pointer events and render-target - // sizes in **logical** (CSS / DIP) pixels; the View internally - // divides by the Device's queried device-pixel-ratio when given - // values in **physical** (surface pixel-buffer) units, so hosts - // pass through whatever their platform's native event system / - // surface-size API delivers. + // Units for coordinates / dimensions passed to View methods. Babylon.js + // works in **logical** (CSS / DIP) pixels; the View divides by the + // Device's device-pixel-ratio when given Physical values so hosts can + // pass whatever their native event/surface API delivers. // - // Examples: - // Physical: Android `MotionEvent.getX/getY` / - // `ANativeWindow_getWidth/Height`, - // Apple `CAMetalLayer.drawableSize`, - // Win32 `GetClientRect` / `WM_POINTER*` (DPI-aware), - // X11 button events / `XGetGeometry`. - // Logical: iOS `UITouch.location`, - // macOS `NSEvent.locationInWindow`, - // UWP `PointerPoint.Position` / `CoreWindow.Bounds`, - // host code that has already done its own DPR divide. + // Physical: Android MotionEvent / ANativeWindow, Apple + // CAMetalLayer.drawableSize, Win32 WM_POINTER* / + // GetClientRect, X11 button events. + // Logical: iOS UITouch, macOS NSEvent, UWP PointerPoint / + // CoreWindow.Bounds, or any code that already divided by DPR. enum class CoordinateUnits { Physical, Logical, }; - // Transient: created when a host surface appears, destroyed when - // it goes away. Multiple sequential Views may be attached to the - // same Runtime over its lifetime. **At most one View may be attached - // at a time** — to switch surfaces, destroy the current View and - // construct a new one. - // - // See SimplifiedAPI.md §4.1 for the full design. + // Transient: created when a host surface appears, destroyed when it + // goes away. **At most one View may be attached at a time**; multiple + // sequential Views may share one Runtime over its lifetime. To switch + // surfaces, destroy the current View and construct a new one. class View { public: - // Attach `nativeWindow` (the platform-specific surface handle) - // to `runtime`. - // - // `nativeWindow` is `Babylon::Graphics::WindowT`, the same - // per-platform typedef the Graphics layer already uses - // (HWND on Win32, ANativeWindow* on Android, CA::MetalLayer* - // on Apple, X11 `Window` on Linux, winrt::IInspectable on UWP). - // The host owns the surface size: the View captures the - // window handle here and binds it to the Device on the first - // `Resize` call. Hosts MUST call `Resize` at least once - // (with the surface's current pixel dimensions) before the - // first frame will be rendered. + // Attach `nativeWindow` (platform-specific surface handle) to `runtime`. // - // Attach itself is lightweight — it just registers as the - // current view and stashes the window handle. All Device - // work (first-time construction, or `UpdateWindow` + - // `UpdateSize` on a re-attach to an existing Runtime) is - // performed by the first `Resize` call, where the host- - // supplied dimensions become available. This folding is - // required: `Device::UpdateWindow` MUST be paired with a - // matching `Device::UpdateSize` or the next frame would be - // rendered to the new surface at the wrong size. + // `nativeWindow` is `Babylon::Graphics::WindowT` — the same per-platform + // typedef the Graphics layer uses (HWND, ANativeWindow*, CA::MetalLayer*, + // X11 Window, winrt::IInspectable). The host owns the surface size; the + // View binds the window to the Device on the first `Resize`. Hosts MUST + // call `Resize` at least once before the first frame will be rendered. // - // The first Attach + Resize on a given Runtime is the heavy - // step: the Resize constructs `Babylon::Graphics::Device`, - // dispatches GPU plugin initialization - // (`Device::AddToJavaScript`, `NativeEngine::Initialize`, - // `NativeInput::CreateForJavaScript`, ...), and flushes any - // scripts queued via `Runtime::LoadScript` before this point. - // Opens the first frame. + // The first Attach+Resize on a given Runtime is the heavy step: it + // constructs the Device, initializes GPU plugins (`Device::AddToJavaScript`, + // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`), flushes + // queued `Runtime::LoadScript` calls, and opens the first frame. + // Subsequent Attach+Resize calls on the same Runtime just rebind the + // Device to the new surface. `~View` closes the in-flight frame; the + // Device persists on the Runtime so the next Attach is fast. // - // Subsequent Attach + Resize calls on the same Runtime are - // cheap: the Device is already constructed, plugins are - // initialized, the JS engine is running. They just call - // `Device::UpdateWindow` + `Device::UpdateSize` to bind the - // new surface, then open the first frame for the new - // attachment. + // Device-rebind work is deferred to the first `Resize` because + // `Device::UpdateWindow` MUST be paired with a matching `UpdateSize`. // - // Detach (`~View`) closes the in-flight frame. The Device - // persists on the Runtime, so the next Attach is fast. - // - // Must be called from the same thread that will call - // `RenderFrame` and `Resize` (the "frame thread"). + // Must be called from the "frame thread" — the same thread that will + // call `RenderFrame` and `Resize`. static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow); ~View(); @@ -96,70 +63,39 @@ namespace Babylon::Integrations View(View&&) = delete; View& operator=(View&&) = delete; - // Render exactly one frame. Must be called from the same thread - // as `Attach` and `Resize` (the frame thread). No-op if the - // runtime is suspended. The host calls this from the platform - // view/control's existing draw callback (WM_PAINT on Win32, - // MTKViewDelegate::draw(in:) on Apple, View.onDraw on Android, - // etc. — see SimplifiedAPI.md §4.1 "How frames actually get - // rendered"). + // Render exactly one frame on the frame thread. No-op if the runtime + // is suspended. Called from the platform view/control's draw callback + // (WM_PAINT, MTKViewDelegate::draw, View.onDraw, ...). void RenderFrame(); - // Resize the bound surface. The View converts physical → - // logical internally if `units == CoordinateUnits::Physical`, - // so the host can pass whatever its platform's view layer - // reports without doing the DPR divide. - // - // Examples: - // Android `View.onSizeChanged(w, h)` → Physical. - // iOS `MTKViewDelegate.drawableSizeWillChange:` → Physical. - // UWP `SizeChangedEventArgs.NewSize` (already in DIPs) → Logical. - // + // Resize the bound surface. Pass whatever units your view layer reports; + // the View converts to logical internally when `units == Physical`. // Must be called from the frame thread. void Resize(uint32_t width, uint32_t height, CoordinateUnits units); #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - // ----- Pointer / mouse input forwarding ----- - // - // Host calls these from its event loop while the view exists. - // Routed to the JS thread via `NativeInput`, where Babylon.js - // consumes them as `PointerEvent.clientX/clientY` (logical / - // CSS pixels). The View converts physical → logical internally - // if `units == CoordinateUnits::Physical`. - // - // Pass coordinates in whatever unit your platform's native - // event system delivers: - // Physical: Android `MotionEvent.getX/getY`, - // Win32 `WM_POINTER*` / `WM_MOUSE*` (DPI-aware), - // X11 button events. - // Logical: iOS `UITouch.location`, - // macOS `NSEvent.locationInWindow`, - // UWP `PointerPoint.Position`. + // Pointer / mouse input forwarding. Host calls these from its event + // loop while the view exists; routed to the JS thread via NativeInput + // and consumed by Babylon.js as PointerEvent.clientX/clientY (logical + // pixels). Pass whatever units your event system delivers — see + // CoordinateUnits above. // - // Babylon Native distinguishes pointer (touch) input from mouse - // input; both methods feed the same Babylon.js pointer-event - // pipeline but with different `pointerType` ('touch' vs. - // 'mouse'). Hosts driven by touch (Android, iOS) typically use - // OnPointer*; hosts driven by a cursor (Win32, macOS, UWP, X11) - // typically use OnMouse*. + // OnPointer* / OnMouse* differ in pointerType ('touch' vs 'mouse'). + // Touch-driven hosts (Android, iOS) use OnPointer*; cursor-driven hosts + // (Win32, macOS, UWP, X11) use OnMouse*. // - // Babylon Native does not currently expose keyboard input; hosts - // that need keyboard handling do it at the platform level and - // forward into JS via `Runtime::RunOnJsThread`. + // Keyboard input is not exposed; forward to JS via Runtime::RunOnJsThread. // // Safe to call from any thread. - // Touch / pointer events. void OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units); void OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units); void OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units); - // Mouse events. `buttonIndex` is one of LeftMouseButton(), - // MiddleMouseButton(), RightMouseButton(); `wheelAxis` is - // MouseWheelY(). The accessors return the matching - // `Babylon::Plugins::NativeInput::*_ID` value (single source of - // truth — no duplication, no risk of drift) without exposing the - // NativeInput header from this public View.h. + // Mouse events. `buttonIndex` is one of LeftMouseButton() / + // MiddleMouseButton() / RightMouseButton(); `wheelAxis` is MouseWheelY(). + // These accessors return the matching NativeInput button/axis IDs + // without leaking the NativeInput header into this public View.h. void OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units); void OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units); void OnMouseMove(float x, float y, CoordinateUnits units); diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 1c53a2d38..5e806690e 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -62,9 +62,8 @@ namespace Babylon::Integrations { namespace { - // Forward Babylon Console levels to the LogLevel exposed on - // RuntimeOptions::log so consumers don't have to depend on the - // Console polyfill header to read log output. + // Forward Babylon Console levels to the public LogLevel enum so + // consumers don't need to depend on the Console polyfill header. LogLevel ToIntegrationsLogLevel(Babylon::Polyfills::Console::LogLevel level) { switch (level) @@ -79,13 +78,9 @@ namespace Babylon::Integrations RuntimeImpl::RuntimeImpl(RuntimeOptions options) : m_options{std::move(options)} { - // Wire DebugTrace through to the host's log callback (if any), - // and enable it only when the host explicitly opts in. - // DebugTrace is process-wide so this affects any concurrent - // Runtime instances; that matches AppContext's behavior today. - // DebugTrace output is low-priority diagnostic noise, so it's - // forwarded at LogLevel::Verbose to make it easy for hosts to - // filter out. + // Forward DebugTrace through the host log callback (Verbose level + // for easy filtering). DebugTrace is process-wide; this matches + // AppContext's existing behavior. if (m_options.log) { const auto& logCallback = m_options.log; @@ -95,16 +90,15 @@ namespace Babylon::Integrations } Babylon::DebugTrace::EnableDebugTrace(m_options.enableDebugTrace); - // Construct AppRuntime. This starts the JS thread and creates a - // Napi::Env. Plugin Initialize() calls will be dispatched onto - // this thread by the first View::Attach. + // Construct AppRuntime: starts the JS thread and creates a Napi::Env. + // Plugin Initialize() calls are dispatched onto this thread by the + // first View::Attach. Babylon::AppRuntime::Options appRuntimeOptions{}; appRuntimeOptions.EnableDebugger = m_options.enableDebugger; appRuntimeOptions.WaitForDebugger = m_options.waitForDebugger; - // Route uncaught JS exceptions through the host's log callback - // with LogLevel::Fatal. If no log callback is set, leave the - // AppRuntime default in place (writes to program output). + // Route uncaught JS exceptions to the host log callback as Fatal. + // If no callback is set, the AppRuntime default writes to program output. if (m_options.log) { const auto& logCallback = m_options.log; @@ -117,38 +111,28 @@ namespace Babylon::Integrations m_appRuntime.emplace(std::move(appRuntimeOptions)); - // ScriptLoader serializes LoadScript / Eval / Dispatch onto the - // AppRuntime's JS thread. Its dispatcher captures a reference to - // m_appRuntime, so ~ScriptLoader must complete before ~AppRuntime. + // ScriptLoader's dispatcher captures &m_appRuntime, so ~ScriptLoader + // must run before ~AppRuntime. m_scriptLoader.emplace(*m_appRuntime); } RuntimeImpl::~RuntimeImpl() { - // Precondition: no View is currently attached. The host owns the - // ordering: destroy Views before destroying their Runtime. + // Host owns the ordering: destroy Views before their Runtime. assert(m_currentView == nullptr && "View must be destroyed before its Runtime."); - // Order matters here: - // 1. Persist the shader cache. The View precondition above - // guarantees `ViewImpl::Suspend()` already ran via - // `~View`, so the engine is quiescent and a host-thread - // Save is race-free. - // 2. ScriptLoader's dispatcher captures &m_appRuntime, so - // ~ScriptLoader must run before ~AppRuntime. - // 3. The Canvas polyfill and NativeInput pointer are referenced - // from JS-thread state; clear them before joining the JS - // thread, but only after ScriptLoader has drained. + // Teardown order: + // 1. SaveShaderCache: ~View already ran ViewImpl::Suspend, so the + // engine is quiescent and a host-thread Save is race-free. + // 2. ~ScriptLoader before ~AppRuntime (dispatcher captures it). + // 3. Canvas / NativeInput / NativeXr hold JS-thread-bound state; + // drop them before joining the JS thread. // 4. ~AppRuntime joins the JS thread. - // 5. ShaderCache::Disable() balances the Enable() that - // RunFirstAttachInit calls on first attach. - // 6. Device + DeviceUpdate destroyed last because the JS - // thread referenced them via Device::AddToJavaScript. + // 5. ShaderCache::Disable balances first-attach Enable. + // 6. Device + DeviceUpdate last (JS thread referenced them). // - // m_initTcs is destroyed when this struct's members are - // destroyed. If complete() was never called (no View ever - // attached), the queued continuations are dropped without - // firing, which is the desired behavior on shutdown. + // m_initTcs: if complete() was never called (no View ever attached), + // queued continuations are dropped on destruction, which is correct. #if BABYLON_NATIVE_PLUGIN_SHADERCACHE SaveShaderCache(); #endif @@ -164,9 +148,6 @@ namespace Babylon::Integrations #endif #if BABYLON_NATIVE_PLUGIN_NATIVEXR - // NativeXr holds JS-thread-bound resources and a strong ref to - // the Napi::Env it was initialized with. Destroy it before the - // AppRuntime joins the JS thread; same reason as ScriptLoader. m_nativeXr.reset(); #endif @@ -183,26 +164,17 @@ namespace Babylon::Integrations m_device.reset(); } - // --------------------------------------------------------------------- - // First-Attach engine initialization: dispatched onto the JS thread by - // the first View::Attach call. Runs all plugin/polyfill Initialize() - // calls in the same order as Apps/Playground/Shared/AppContext.cpp, - // then completes m_initTcs to unblock any LoadScript / Eval / - // RunOnJsThread calls the host queued before the first Attach. - // - // After m_initTcs is complete, subsequent host calls to - // Runtime::LoadScript / Eval / RunOnJsThread fire their continuation - // synchronously on the calling thread (via inline_scheduler), which - // then submits to ScriptLoader directly. - // --------------------------------------------------------------------- + // Dispatched onto the JS thread by the first View::Attach. Initializes + // all plugins/polyfills in the same order as AppContext.cpp, then + // completes m_initTcs to unblock host calls that were queued before + // first Attach. Post-init, those host calls fire their continuation + // synchronously via inline_scheduler and submit straight to ScriptLoader. void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window) { #if BABYLON_NATIVE_PLUGIN_SHADERCACHE - // Enable the process-wide shader cache singleton and hydrate - // it from disk before any JS-thread shader compilation can - // begin. Both calls are host-thread safe (the singleton is - // not yet referenced by NativeEngine, which gets initialized - // on the JS thread below). + // Enable + hydrate before any JS-thread shader compilation. Both + // calls are host-thread safe since NativeEngine hasn't been + // initialized yet. Babylon::Plugins::ShaderCache::Enable(); LoadShaderCache(); #endif @@ -274,9 +246,8 @@ namespace Babylon::Integrations implPtr->m_input = &Babylon::Plugins::NativeInput::CreateForJavaScript(env); #endif #if BABYLON_NATIVE_PLUGIN_NATIVEXR - // Initialize NativeXr; apply any pending xr window the host - // may have already supplied via Runtime::SetXrWindow; wire - // the session-state callback to keep m_isXrActive in sync. + // Initialize NativeXr, apply any pending xr window from a prior + // SetXrWindow, and wire the session-state callback. { std::lock_guard xrLock{implPtr->m_xrMutex}; implPtr->m_nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(env)); @@ -296,20 +267,12 @@ namespace Babylon::Integrations (void)window; #endif - // 4. Unblock any LoadScript / Eval / RunOnJsThread calls - // the host registered before first Attach. Each was - // chained off m_initTcs.as_task().then(inline_scheduler, - // ..., [...] { scriptLoader->...; });, so completing the - // TCS here causes those continuations to fire (in - // registration order) on the JS thread, each submitting - // to ScriptLoader's task chain. + // 4. Fire any host calls queued before the first Attach. implPtr->m_initTcs.complete(); }); } - // --------------------------------------------------------------------- - // Persistent shader cache (no-ops when `shaderCachePath` is empty). - // --------------------------------------------------------------------- + // Persistent shader cache (no-ops when shaderCachePath is empty). #if BABYLON_NATIVE_PLUGIN_SHADERCACHE void RuntimeImpl::LoadShaderCache() { @@ -322,7 +285,7 @@ namespace Babylon::Integrations { Babylon::Plugins::ShaderCache::Load(stream); } - // Missing or unreadable file is fine — we just start with an empty cache. + // Missing/unreadable file: start with an empty cache. } void RuntimeImpl::SaveShaderCache() @@ -341,7 +304,7 @@ namespace Babylon::Integrations std::unique_ptr Runtime::Create(RuntimeOptions options) { - // Private ctor + manual unique_ptr because make_unique can't see it. + // Private ctor; manual unique_ptr because make_unique can't see it. std::unique_ptr runtime{new Runtime()}; runtime->m_impl = std::make_unique(std::move(options)); return runtime; @@ -383,27 +346,23 @@ namespace Babylon::Integrations void Runtime::Suspend() { - // Frame-thread only (see Runtime.h). No locking needed for the - // count itself; the atomic exists so cross-thread IsSuspended() - // reads stay coherent. + // Frame-thread only (see Runtime.h). The atomic is for cross-thread + // IsSuspended() reads, not for protecting the count. if (m_impl->m_suspendCount.fetch_add(1, std::memory_order_relaxed) == 0) { - // Close the in-flight frame on the currently attached View - // (if any) BEFORE blocking the JS thread. This keeps the - // GPU side clean across the suspension: no held drawable, - // no open DeviceUpdate safe-timespan. The View tracks its - // own state, so this composes with the subsequent ~View - // (which Suspends again — no-op) and with hosts that - // suspend the Runtime while no View is attached. + // Close the in-flight frame on the attached View (if any) BEFORE + // blocking the JS thread: keeps the GPU side clean (no held + // drawable, no open DeviceUpdate safe-timespan). Composes + // correctly with ~View (re-Suspend is a no-op) and with + // suspending while no View is attached. if (m_impl->m_currentView) { m_impl->m_currentView->m_impl->Suspend(); } #if BABYLON_NATIVE_PLUGIN_SHADERCACHE - // Persist the shader cache while the engine is known - // quiescent: the view's Suspend just closed the current - // frame and locked the update safe-timespan, so no engine - // shader compilation can be in flight. + // Engine is quiescent here (ViewImpl::Suspend just closed the + // frame and locked the update safe-timespan), so the save is + // race-free with any pending shader compilation. m_impl->SaveShaderCache(); #endif m_impl->m_appRuntime->Suspend(); @@ -415,16 +374,15 @@ namespace Babylon::Integrations // Frame-thread only (see Runtime.h). if (m_impl->m_suspendCount.load(std::memory_order_relaxed) == 0) { - // Mismatched Resume; ignore rather than underflow the count. + // Mismatched Resume — ignore rather than underflow. return; } if (m_impl->m_suspendCount.fetch_sub(1, std::memory_order_relaxed) == 1) { m_impl->m_appRuntime->Resume(); - // Re-open the frame on the attached View (if any) so the - // next RenderFrame call has something to Finish. On a view - // that has been attached but never sized, this also drives - // the deferred first-Resize init via InitializeIfReady. + // Re-open the frame on the attached View. On a view that was + // attached but never sized, this also drives the deferred + // first-Resize init via InitializeIfReady. if (m_impl->m_currentView) { m_impl->m_currentView->m_impl->Resume(); @@ -446,9 +404,8 @@ namespace Babylon::Integrations { m_impl->m_nativeXr->UpdateWindow(nativeWindow); } - // If NativeXr isn't initialized yet (no View::Attach has - // happened), the value is stashed in m_xrWindow and applied - // by the first-Attach init lambda when it constructs NativeXr. + // If NativeXr isn't initialized yet (no View::Attach has happened), + // the value is applied by the first-Attach init lambda. } bool Runtime::IsXrActive() const diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index 3cfb5f281..d189651ae 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -61,162 +61,100 @@ namespace Babylon::Integrations #endif #if BABYLON_NATIVE_PLUGIN_NATIVEXR - // NativeXr is initialized during the first View::Attach (it - // needs a Napi::Env). Until then, m_xrWindow holds the host's - // most recent SetXrWindow value so we can apply it as soon as - // the plugin is alive. m_xrMutex serializes both fields plus - // the optional itself. m_isXrActive is updated from the JS - // thread by NativeXr's session-state callback and read from - // any thread by IsXrActive() polling. + // NativeXr is initialized during the first View::Attach (needs a + // Napi::Env). Until then, m_xrWindow holds the most recent + // SetXrWindow value for application post-init. m_xrMutex serializes + // these fields. m_isXrActive is written from the JS thread and + // polled from any thread. std::optional m_nativeXr; void* m_xrWindow{nullptr}; mutable std::mutex m_xrMutex; std::atomic m_isXrActive{false}; #endif - // ----- Pre-init queueing ----- + // Pre-init queue for host LoadScript / Eval / RunOnJsThread. + // Each call chains its work off `m_initTcs.as_task().then(...)`. + // The first-Attach init lambda calls `m_initTcs.complete()` after + // all plugins are initialized, firing every queued continuation + // (in registration order) on the JS thread. After completion, + // future `.then(inline_scheduler, ...)` calls run synchronously + // on the calling thread and submit to ScriptLoader directly. // - // Host calls to LoadScript / Eval / RunOnJsThread are chained - // off `m_initTcs.as_task().then(inline_scheduler, ...)`. While - // the TCS is uncompleted (i.e. the first View::Attach hasn't - // finished plugin initialization on the JS thread), continuations - // sit on the task payload. The first-Attach init lambda calls - // `m_initTcs.complete()` after all plugins are initialized, - // which fires every queued continuation in registration order - // on the JS thread. After completion, subsequent - // `.then(inline_scheduler, ...)` calls run their callable - // synchronously on the calling thread, which then submits to - // ScriptLoader directly. - // - // Host-side serialization is the host's responsibility, matching - // ScriptLoader's existing contract; we do not add an outer mutex. + // Host-side serialization is the host's responsibility (same + // contract as ScriptLoader); no outer mutex. arcana::task_completion_source m_initTcs; - // Reference-counted Suspend/Resume. Atomic so `IsSuspended()` - // can be polled cheaply from any thread (e.g. a worker checking - // whether to bother queuing more host-side work). Mutations to - // the count itself happen only on the frame thread (the - // documented contract of `Runtime::Suspend` / `Runtime::Resume`), - // so the increment/decrement do not need to be locked. + // Reference-counted suspend depth. Mutated only on the frame + // thread; atomic so `IsSuspended()` can be polled from any thread. std::atomic m_suspendCount{0}; - // 0..1; tracked so we can guard against multiple concurrent - // attachments (the API contract is "at most one View at a time"). + // 0..1 — enforces "at most one View attached at a time". View* m_currentView{nullptr}; - // First-Attach engine initialization: dispatched onto the JS - // thread by View::Attach the very first time it constructs the - // Device. Runs all plugin/polyfill `Initialize` calls, wires - // NativeXr session-state callbacks, then completes `m_initTcs` - // to unblock any LoadScript / Eval / RunOnJsThread calls the - // host queued before the first Attach. - // - // The `window` parameter is forwarded to TestUtils::Initialize - // (the only plugin that wants it); ignored otherwise. + // Dispatched onto the JS thread by the very first View::Attach. + // Runs all plugin/polyfill Initialize calls, wires NativeXr + // callbacks, and completes m_initTcs. `window` is forwarded to + // TestUtils::Initialize; ignored otherwise. void RunFirstAttachInit(Babylon::Graphics::WindowT window); #if BABYLON_NATIVE_PLUGIN_SHADERCACHE - // ----- Persistent shader cache ----- - // - // Both methods are no-ops when `m_options.shaderCachePath` is - // empty. Both run synchronously on the host thread; they do - // not need to coordinate with the JS thread because callers - // (first-Attach, post-view-Suspend, destructor) are points at - // which the engine is known not to be compiling shaders. - - // Load the on-disk shader cache file into the in-memory cache. - // Called from `RunFirstAttachInit` right after - // `ShaderCache::Enable()`. Safe because no shaders have been - // compiled yet at this point — the cache map is quiescent. + // Persistent shader cache. No-ops when `m_options.shaderCachePath` + // is empty. Both run synchronously on the host thread at points + // where the engine is known not to be compiling shaders + // (first-Attach, post-view-Suspend, ~RuntimeImpl) — no JS-thread + // coordination required. void LoadShaderCache(); - - // Serialize the in-memory shader cache to disk. Called from - // `Runtime::Suspend` (after `ViewImpl::Suspend()` has closed - // the current frame and locked the update safe-timespan) and - // from `~RuntimeImpl` (after the View precondition has - // guaranteed `ViewImpl::Suspend()` already ran via `~View`). - // No async/JS-thread coordination is required: at these - // points there is no in-flight engine work writing to the - // cache. void SaveShaderCache(); #endif }; - // Internal implementation of View. Holds the back-reference to the - // Runtime that produced it, the native window handle captured at - // `View::Attach`, the most recent size handed in via `View::Resize`, - // plus Suspend / Resume that manage the in-flight frame across - // runtime suspension. + // Internal implementation of View. Owns the back-reference to its + // Runtime, the window handle captured at Attach, the most recent + // Resize size, and the frame-lifecycle Suspend/Resume hooks. // - // `View::Attach` is intentionally cheap: it just stashes the - // window handle and registers as the current view. All Device - // interaction (first-time construction, or `UpdateWindow` + - // `UpdateSize` on a re-attach to an existing Runtime) is deferred - // to `InitializeIfReady`, which only runs once all three - // preconditions hold: + // Attach is cheap: it just stashes the window and registers as the + // current view. All Device work runs in `InitializeIfReady`, which + // is gated on three preconditions: // - // 1. `m_initialized` is still `false` (the view hasn't been - // initialized yet for this Attach), - // 2. `m_size` is set (the host has called `View::Resize` at - // least once with a non-zero size), and - // 3. `RuntimeImpl::m_suspendCount` is zero (the host hasn't - // called `Runtime::Suspend` without a matching `Resume`). + // 1. `m_initialized == false`, + // 2. `m_size` is set (host has called Resize at least once), and + // 3. `RuntimeImpl::m_suspendCount == 0`. // - // `InitializeIfReady` is called from `View::Resize` (which sets - // condition 2) and from `ViewImpl::Resume` (which can clear - // condition 3). Whichever caller satisfies the last missing - // precondition is the one that actually runs the init recipe. - // This is also what makes the `UpdateWindow` + `UpdateSize` pair - // safe: both happen together inside `InitializeIfReady`, so bgfx - // never sees a window change without a matching size change. + // Called from `View::Resize` (satisfies #2) and `ViewImpl::Resume` + // (can clear #3); whichever caller satisfies the last precondition + // runs the init. Folding `UpdateWindow` + `UpdateSize` together + // inside this function is what keeps the bgfx pair safe. // - // Once `m_initialized` flips to `true`, a Device frame is open - // iff `RuntimeImpl::m_suspendCount == 0`. `ViewImpl::Suspend` and - // `ViewImpl::Resume` are called by `Runtime::Suspend` / - // `Runtime::Resume` on the suspendCount 0↔1 transitions and - // toggle that frame open/closed. + // Once initialized, a Device frame is open iff `m_suspendCount == 0`. + // ViewImpl::Suspend/Resume toggle that frame on the 0↔1 transitions. struct ViewImpl { explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} Runtime& m_runtime; - // Window handle captured at `View::Attach`. Used by - // `InitializeIfReady` to construct (first-Attach-ever) or - // rebind (subsequent Attach) the Device. + // Captured at View::Attach; used by InitializeIfReady to + // construct (first-Attach-ever) or rebind the Device. Babylon::Graphics::WindowT m_window{}; - // Most recent size handed in via `View::Resize`, in logical - // pixels. Empty until the host has called `Resize` at least - // once. Used by `InitializeIfReady` to size the Device on the - // first init. + // Most recent Resize size, in logical pixels. Empty until the + // host calls Resize. std::optional> m_size; - // Latches to `true` the first time `InitializeIfReady` runs - // all three preconditions to completion. Never flips back for - // the lifetime of this `ViewImpl`; a new `ViewImpl` is - // constructed on each `View::Attach`. + // Latches true the first time InitializeIfReady succeeds; never + // flips back. A new ViewImpl is constructed per View::Attach. bool m_initialized{false}; - // Close the in-flight frame on the Device (Finish + - // FinishRenderingCurrentFrame). Called by `Runtime::Suspend` - // on the suspendCount 0→1 transition, and by `~View`. No-op - // when the view is not yet initialized (no frame to close). + // Close the in-flight frame (called by Runtime::Suspend on 0→1 + // and by ~View). No-op when uninitialized. void Suspend(); - // Open a new frame (StartRenderingCurrentFrame + - // DeviceUpdate::Start) on the Device. Called by - // `Runtime::Resume` on the suspendCount 1→0 transition. When - // the view is not yet initialized, this instead tries to run - // the deferred first-Resize init (which itself opens the - // first frame if it succeeds) — so a host that attached and - // resized while the Runtime was externally suspended sees its - // init kick off cleanly on the next `Runtime::Resume`. + // Open a new frame (called by Runtime::Resume on 1→0). When + // uninitialized, retries InitializeIfReady — handles the case + // where the host attached and resized while suspended. void Resume(); - // Run the deferred Device-binding + first-frame-opening - // recipe iff all three preconditions are satisfied (see the - // class-level comment). No-op otherwise. Safe to call from - // any of the entry points that can satisfy a precondition. + // Runs the deferred init iff all three preconditions hold. + // Safe to call from any caller that just satisfied one. void InitializeIfReady(); }; } diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index 1c7358cf2..e51ff9996 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -11,9 +11,7 @@ namespace Babylon::Integrations { namespace { - // Convert a (width, height) measured in `units` to logical - // pixels. Caller always passes the real DPR; this helper - // decides whether to apply it based on `units`. + // Convert (width, height) in `units` to logical pixels. std::pair ToLogicalSize(uint32_t width, uint32_t height, CoordinateUnits units, float dpr) { @@ -25,8 +23,7 @@ namespace Babylon::Integrations static_cast(height / dpr)}; } - // Convert a (x, y) coordinate pair measured in `units` to - // logical pixels. + // Convert (x, y) in `units` to logical pixels. std::pair ToLogicalCoords(float x, float y, CoordinateUnits units, float dpr) { @@ -46,32 +43,13 @@ namespace Babylon::Integrations } } - // --------------------------------------------------------------------- - // View::Attach - // - // Lightweight: just register as the current view and stash the - // native window handle. All Device interaction (first-time - // construction, or `UpdateWindow` + `UpdateSize` on a re-attach to - // an existing Runtime) is deferred to `ViewImpl::InitializeIfReady`, - // which is called from `View::Resize` and `ViewImpl::Resume` and - // only actually runs when all three init preconditions hold (see - // `RuntimeImpl.h` for details). - // - // Why deferred: `Device::UpdateWindow` MUST be paired with a - // matching `Device::UpdateSize` (otherwise bgfx renders the next - // frame to the new window at the old size), so we wait until the - // host has supplied a size via `Resize` and the Runtime is not - // currently externally suspended before binding the new surface. - // Folding the window-rebind and size-update together inside - // `InitializeIfReady` makes the host the single source of truth - // for surface size — the Integrations layer never queries the - // window for its size. - // - // Render-loop callbacks that fire between `Attach` and successful - // init are safe: `RenderFrame` early-outs while `m_initialized` - // is `false`, without touching the (potentially still-unbound) - // Device. - // --------------------------------------------------------------------- + // Lightweight: stash the window handle and register as current view. + // All Device work (first-time construction, or `UpdateWindow` + + // `UpdateSize` on a re-attach) is deferred to + // `ViewImpl::InitializeIfReady`, gated on all three preconditions + // (window stashed, size known, not externally suspended). Pre-init + // `RenderFrame` calls are safe — they early-out while `m_initialized` + // is false. See `RuntimeImpl.h` for the full state machine. std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) { RuntimeImpl& impl = *runtime.m_impl; @@ -97,10 +75,9 @@ namespace Babylon::Integrations { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Close the in-flight frame iff one is currently open. A frame - // is open exactly when this view is initialized and the - // Runtime is not externally suspended (Runtime::Suspend would - // already have closed the frame via ViewImpl::Suspend). The + // Close the in-flight frame iff one is currently open: i.e. the + // view is initialized and the Runtime is not externally suspended + // (Suspend already closed the frame via ViewImpl::Suspend). The // Device persists on the Runtime so the next Attach is cheap. if (m_impl->m_initialized && impl.m_suspendCount.load(std::memory_order_relaxed) == 0) @@ -112,17 +89,11 @@ namespace Babylon::Integrations impl.m_currentView = nullptr; } - // --------------------------------------------------------------------- - // ViewImpl::Suspend / Resume - // - // Called by `Runtime::Suspend` / `Runtime::Resume` on the - // suspendCount 0↔1 transitions (and by `~View` for the teardown - // close). When the view is already initialized, these are pure - // frame open/close operations. When the view is not yet - // initialized, Suspend has nothing to do (no frame exists) and - // Resume retries `InitializeIfReady` — because the suspendCount - // dropping to 0 may have been the last missing precondition. - // --------------------------------------------------------------------- + // Called by Runtime::Suspend/Resume on the 0↔1 transition (and ~View + // for teardown). When initialized, these are pure frame open/close. + // When uninitialized, Suspend is a no-op and Resume retries + // InitializeIfReady — the suspendCount drop may have been the last + // missing precondition. void ViewImpl::Suspend() { if (!m_initialized) @@ -138,10 +109,8 @@ namespace Babylon::Integrations { if (!m_initialized) { - // Runtime just resumed; the suspendCount precondition is - // now satisfied. If the host has also already called - // `View::Resize` (size precondition), this will succeed; - // otherwise it's a silent no-op until they do. + // Suspend precondition just cleared. Succeeds if `Resize` has + // also run; otherwise silent no-op until it does. InitializeIfReady(); return; } @@ -150,15 +119,10 @@ namespace Babylon::Integrations impl.m_deviceUpdate->Start(); } - // --------------------------------------------------------------------- - // ViewImpl::InitializeIfReady - // - // The single recipe for binding the Device to this view's window - // and opening the first frame. Gated on all three preconditions - // (initialized? sized? not externally suspended?) so it can be - // called eagerly from anywhere that satisfies one of them, and - // does nothing until the last missing condition is fulfilled. - // --------------------------------------------------------------------- + // Single recipe for binding the Device to this view's window and + // opening the first frame. Gated on all three preconditions + // (initialized? sized? not suspended?) so it can be called eagerly + // from anywhere that satisfies one of them. void ViewImpl::InitializeIfReady() { if (m_initialized || !m_size) @@ -186,20 +150,17 @@ namespace Babylon::Integrations } else { - // Re-attach to an existing Runtime: rebind the surface and - // update the size in lockstep. Plugins, polyfills, and any - // loaded scripts are preserved on the JS side. + // Re-attach to an existing Runtime: rebind surface and size in + // lockstep. JS state (plugins, polyfills, scripts) is preserved. impl.m_device->UpdateWindow(m_window); impl.m_device->UpdateSize(lw, lh); } - // Open the first frame inline. On very first Attach this MUST - // happen BEFORE dispatching `RunFirstAttachInit` so the - // `Device::AddToJavaScript` inside that lambda sees an open - // frame to record into. Both happen on the same host thread, - // so by the time the dispatched lambda runs on the JS thread, - // the frame is already open regardless of whether we entered - // here from `View::Resize` or from `ViewImpl::Resume`. + // Open the first frame inline. On the very first Attach this MUST + // happen BEFORE dispatching RunFirstAttachInit, so the + // Device::AddToJavaScript inside that lambda sees an open frame to + // record into. Both run on the host thread, so the dispatched + // lambda always sees an open frame on the JS thread. impl.m_device->StartRenderingCurrentFrame(); impl.m_deviceUpdate->Start(); m_initialized = true; @@ -214,15 +175,13 @@ namespace Babylon::Integrations { RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Only render when a frame is currently open. The host can keep - // calling RenderFrame() from its draw callback unconditionally; - // the two non-rendering cases early-out below. + // Host may call unconditionally from its draw callback; the + // not-rendering cases early-out. if (!m_impl->m_initialized) { - // Flag the pre-init case to help hosts diagnose "my draw - // callback is firing but nothing renders" mistakes. The - // externally-suspended case (Runtime::Suspend) is expected - // behavior and stays silent. + // Flag pre-init so hosts can diagnose "my draw callback fires + // but nothing renders". The externally-suspended case is + // expected and stays silent. DEBUG_TRACE("Babylon::Integrations::View::RenderFrame skipped: View has not yet been initialized. Call View::Resize with the surface's pixel dimensions to begin rendering."); return; } @@ -231,36 +190,27 @@ namespace Babylon::Integrations return; } - // Babylon's JS render loop (requestAnimationFrame / scene.render) - // runs between Start and Finish, scheduled via DeviceUpdate onto - // the JS thread. This call doesn't enter JS directly — - // DeviceUpdate handles the cross-thread coordination via - // SafeTimespanGuarantor. + // Babylon's JS render loop runs between Start and Finish, scheduled + // via DeviceUpdate's SafeTimespanGuarantor onto the JS thread. impl.m_deviceUpdate->Finish(); impl.m_device->FinishRenderingCurrentFrame(); impl.m_device->StartRenderingCurrentFrame(); impl.m_deviceUpdate->Start(); } - // --------------------------------------------------------------------- - // View::Resize - // - // Always stores the host-supplied size on the ViewImpl, then either - // pushes it through to `Device::UpdateSize` (already initialized) - // or kicks `InitializeIfReady` (still uninitialized). This is the - // single source of truth for surface size on the Integrations side. - // --------------------------------------------------------------------- + // Stores the host-supplied size on the ViewImpl, then either pushes + // it through Device::UpdateSize (initialized) or drives + // InitializeIfReady (uninitialized). Single source of truth for + // surface size in the Integrations layer. void View::Resize(uint32_t width, uint32_t height, CoordinateUnits units) { ValidateNonZeroSize(width, height, "View::Resize size"); RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // DPR source: before init, query the window directly because - // the Device either doesn't exist yet (first Attach ever) or is - // still bound to the previous attach's window (re-attach). - // After init, the Device's stored DPR matches the window we're - // bound to and is the authoritative source. + // DPR source: before init, query the window directly (Device + // doesn't exist or is still bound to the previous attach). After + // init, the Device's stored DPR is authoritative. const float dpr = !m_impl->m_initialized ? Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window) : impl.m_device->GetDevicePixelRatio(); @@ -275,10 +225,8 @@ namespace Babylon::Integrations } else { - // Just satisfied the "size known" precondition; try to init. - // Silent no-op if the Runtime is currently externally - // suspended; the eventual `Runtime::Resume` will retry via - // `ViewImpl::Resume`. + // "Size known" precondition just satisfied. Silent no-op if + // currently suspended; Runtime::Resume will retry. m_impl->InitializeIfReady(); } } From 6c29a18affaed97a8f0fa98be2db13ef80017ad6 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 19 May 2026 14:19:51 -0700 Subject: [PATCH 43/71] Switch to regular constructed objects instead of factory functions --- .../src/main/cpp/PlaygroundJNI.cpp | 2 +- .../playground/PlaygroundActivity.java | 2 +- Apps/Playground/Shared/PlaygroundScripts.h | 4 +- Apps/Playground/Win32/App.cpp | 13 ++- .../main/cpp/BabylonNativeIntegrations.cpp | 19 ++-- Integrations/Apple/Source/BNRuntime.mm | 6 +- Integrations/Apple/Source/BNView.mm | 22 ++-- .../Apple/Babylon/Integrations/Apple/BNView.h | 5 +- .../Shared/Babylon/Integrations/Runtime.h | 32 +++--- .../Babylon/Integrations/RuntimeOptions.h | 2 +- .../Shared/Babylon/Integrations/View.h | 56 +++++----- Integrations/Source/Runtime.cpp | 34 +++--- Integrations/Source/RuntimeImpl.h | 41 ++++--- Integrations/Source/View.cpp | 104 ++++++++---------- 14 files changed, 171 insertions(+), 171 deletions(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp index 3b8d6bf01..bc64b7232 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp +++ b/Apps/Playground/Android/BabylonNative/src/main/cpp/PlaygroundJNI.cpp @@ -30,7 +30,7 @@ Java_com_android_babylonnative_playground_PlaygroundActivity_loadBootstrapScript // Idempotent process-wide setup (PerfTrace level, etc.). Playground::Initialize(); - // Queue bootstrap scripts; they run after first View::Attach + // Queue bootstrap scripts; they run after the first View attach // completes engine init on the JS thread. Playground::LoadBootstrapScripts(*runtime); } diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 68f37bf35..1f2dcf2e1 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -35,7 +35,7 @@ protected void onCreate(Bundle icicle) { mRuntimeHandle = BabylonNative.runtimeCreate(runtimeOptions); // Queue the bootstrap scripts + experience script. They run after - // first View::Attach completes engine init on the JS thread, in + // the first View attach completes engine init on the JS thread, in // submission order. loadBootstrapScripts(mRuntimeHandle); BabylonNative.runtimeLoadScript(mRuntimeHandle, "app:///Scripts/experience.js"); diff --git a/Apps/Playground/Shared/PlaygroundScripts.h b/Apps/Playground/Shared/PlaygroundScripts.h index 123de12f2..b9fbb35d2 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.h +++ b/Apps/Playground/Shared/PlaygroundScripts.h @@ -22,8 +22,8 @@ namespace Playground // `bundle.js` route. Centralizing the list keeps every Playground // host in sync as the bundle list evolves. // - // LoadScript calls made before the first View::Attach are queued and + // LoadScript calls made before the first View attach are queued and // dispatched after engine init, so this is safe to call immediately - // after `Runtime::Create`. + // after constructing the Runtime. void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime); } diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 566fa89bd..564dd8ecc 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -37,11 +38,11 @@ WCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name // Process-scoped: created on app start, recreated on 'R' refresh, // destroyed on app exit. -std::unique_ptr g_runtime; +std::optional g_runtime; // Window-scoped: created on InitInstance after CreateWindowW returns, // destroyed on WM_DESTROY (or torn down + recreated by RefreshBabylon). -std::unique_ptr g_view; +std::optional g_view; bool minimized{false}; PlaygroundOptions options{}; @@ -201,14 +202,14 @@ namespace { Uninitialize(); - g_runtime = Babylon::Integrations::Runtime::Create(MakeRuntimeOptions()); + g_runtime.emplace(MakeRuntimeOptions()); Playground::Initialize(options); QueuePlaygroundOptions(); LoadScripts(); - // First View::Attach triggers Device construction, plugin init, - // and flushes the queued scripts. - g_view = Babylon::Integrations::View::Attach(*g_runtime, hWnd); + // First View attach triggers Device construction, plugin init, and + // flushes the queued scripts. + g_view.emplace(*g_runtime, hWnd); } } diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index 552a43f6f..ce302d40e 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -43,6 +43,10 @@ namespace // AFTER the Runtime so they're destroyed BEFORE it — guaranteeing no // callback fires on a dead Runtime during teardown. // + // Runtime is held by unique_ptr so its address is stable for the + // ticket lambdas (the tickets aren't move-assignable, so we have to + // know the address before constructing the wrapper). + // // Runtime::Suspend/Resume are refcounted, so this composes safely // with explicit host-side runtimeSuspend / runtimeResume calls. struct AndroidRuntime @@ -208,10 +212,7 @@ namespace __android_log_write(LogPriorityFor(level), "BabylonNative", text.c_str()); }; - // Two-phase construction: AppStateChangedCallbackTicket is neither - // default-constructible nor move-assignable, and we need the - // Runtime pointer before registering callbacks. - auto runtime = Runtime::Create(std::move(options)); + auto runtime = std::make_unique(std::move(options)); Runtime* runtimePtr = runtime.get(); // Auto-Suspend/Resume on process-wide Activity lifecycle. Hosts @@ -428,8 +429,12 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( { return 0; } - auto view = View::Attach(*AsRuntime(runtimeHandle), window); - if (!view) + View* view = nullptr; + try + { + view = new View{*AsRuntime(runtimeHandle), window}; + } + catch (const std::exception&) { ANativeWindow_release(window); return 0; @@ -437,7 +442,7 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( // bgfx retains its own reference on the ANativeWindow for the // surface-binding lifetime, so release our local acquire here. ANativeWindow_release(window); - return reinterpret_cast(view.release()); + return reinterpret_cast(view); } JNIEXPORT void JNICALL diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Integrations/Apple/Source/BNRuntime.mm index a3deefafe..ac0f1395e 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Integrations/Apple/Source/BNRuntime.mm @@ -53,7 +53,7 @@ @implementation BNRuntimeOptions @implementation BNRuntime { - std::unique_ptr _runtime; + std::optional _runtime; MTKView* _xrView; } @@ -99,7 +99,7 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions userInfo:nil]; #endif } - _runtime = Babylon::Integrations::Runtime::Create(std::move(options)); + _runtime.emplace(std::move(options)); } return self; } @@ -165,7 +165,7 @@ - (BOOL)isXRActive - (Babylon::Integrations::Runtime*)nativeRuntime { - return _runtime.get(); + return _runtime ? &*_runtime : nullptr; } - (void)updateXrViewIfNeeded diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index 8634d8bb1..7af324e9b 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -10,11 +10,11 @@ #include #include -#include +#include @implementation BNView { - std::unique_ptr _view; + std::optional _view; BNRuntime* _runtime; MTKView* _mtkView; @@ -30,6 +30,11 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view { return nil; } + Babylon::Integrations::Runtime* nativeRuntime = runtime.nativeRuntime; + if (nativeRuntime == nullptr) + { + return nil; + } if ((self = [super init])) { _runtime = runtime; @@ -38,24 +43,25 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view // MTKView's layer is always CAMetalLayer (via +layerClass). CAMetalLayer* layer = (CAMetalLayer*)view.layer; - // View::Attach is lightweight (just stashes the layer). Device + // View construction is lightweight (just stashes the layer). Device // construction is driven later by `-resizeWithWidth:height:`, // which BNViewDelegate forwards from MTKView's // `mtkView:drawableSizeWillChange:`. MTKView fires that before // its first draw, so bootstrap is automatic. Hosts driving // MTKView in unusual ways can call `-resizeWithWidth:height:` // directly with the surface's pixel size. - _view = Babylon::Integrations::View::Attach( - *runtime.nativeRuntime, - (__bridge CA::MetalLayer*)layer); - if (!_view) + @try + { + _view.emplace(*nativeRuntime, (__bridge CA::MetalLayer*)layer); + } + @catch (NSException*) { _mtkView = nil; return nil; } // If the host hasn't installed an MTKViewDelegate, install a - // default BNViewDelegate. Done AFTER Attach so any + // default BNViewDelegate. Done AFTER View construction so any // drawableSizeWillChange: triggered by the assignment doesn't // reach us before _view is constructed. if (view.delegate == nil) diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h index d1bff00aa..11ca1b322 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h @@ -46,8 +46,9 @@ NS_ASSUME_NONNULL_BEGIN /// given runtime this triggers GPU device construction and engine /// initialization; subsequent attaches just rebind the surface. /// -/// Returns `nil` if `runtime` or `view` is `nil`, or if the underlying -/// `Babylon::Integrations::View::Attach` fails. +/// Returns `nil` if `runtime` or `view` is `nil`, or if constructing the +/// underlying `Babylon::Integrations::View` throws (another View is +/// already attached to the runtime). /// /// **Delegate management:** If `view.delegate` is nil, BNView creates a /// BNViewDelegate and assigns it (held strongly for the BNView's diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 934acfcfb..57c421255 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -17,32 +17,35 @@ namespace Babylon::Integrations // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU // polyfills/plugins. Construction is cheap and synchronous; the GPU // Device and plugins (NativeEngine, etc.) are deferred to the first - // `View::Attach`. + // attached `View`. class Runtime { public: - static std::unique_ptr Create(RuntimeOptions options = {}); + explicit Runtime(RuntimeOptions options = {}); // // Future construction mode: adopt a host-owned Babylon::JsRuntime // // (e.g. React Native with Hermes/JSC + CallInvoker). In Attach mode // // `~Runtime` does NOT tear down the JS engine, and Suspend/Resume // // only toggles Device rendering. - // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, - // RuntimeOptions options = {}); + // static Runtime Adopt(Babylon::JsRuntime& jsRuntime, + // RuntimeOptions options = {}); ~Runtime(); - // Non-copyable, non-movable (Views hold raw pointers back to this). + // Non-copyable; movable. Cross-references between Runtime and View + // point at the heap-allocated pimpls, so moves of the outer wrappers + // are safe and don't invalidate any back-pointers. Runtime(const Runtime&) = delete; Runtime& operator=(const Runtime&) = delete; - Runtime(Runtime&&) = delete; - Runtime& operator=(Runtime&&) = delete; + Runtime(Runtime&&) noexcept; + Runtime& operator=(Runtime&&) noexcept; // ----- JS interaction ----- // - // Calls made before the first `View::Attach` are queued and dispatched - // onto the JS thread after engine initialization completes during that - // first Attach. Calls after first Attach are dispatched immediately. + // Calls made before the first `View` is attached are queued and + // dispatched onto the JS thread after engine initialization + // completes during that first attach. Calls after first attach are + // dispatched immediately. // // Not internally synchronized — call from a single thread (typically // the host's UI/main thread), matching `Babylon::ScriptLoader` / @@ -81,9 +84,9 @@ namespace Babylon::Integrations // Android : ANativeWindow* (typically a transparent SurfaceView overlay) // Apple : CAMetalLayer* / MTKView* (separate Metal layer from the main View) // - // Pass nullptr to clear. Safe to call before the first `View::Attach` - // (the window is applied when NativeXr finishes initializing) and - // from any thread. + // Pass nullptr to clear. Safe to call before the first `View` is + // attached (the window is applied when NativeXr finishes + // initializing) and from any thread. void SetXrWindow(void* nativeWindow); // True while an XR session is active. Atomic; safe to poll from any @@ -93,9 +96,6 @@ namespace Babylon::Integrations private: friend class View; - friend struct ViewImpl; - - Runtime(); std::unique_ptr m_impl; }; diff --git a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h index 379c399bb..151e77eb6 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h +++ b/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h @@ -41,7 +41,7 @@ namespace Babylon::Integrations #if BABYLON_NATIVE_PLUGIN_SHADERCACHE // Optional path for persisting the GPU shader cache across sessions. // If non-empty: - // - Loaded synchronously during the first `View::Attach` (missing + // - Loaded synchronously during the first View attach (missing // or unreadable file: ignored). // - Saved asynchronously during `Runtime::Suspend` (queued onto // the JS thread before the suspension blocker). diff --git a/Integrations/Include/Shared/Babylon/Integrations/View.h b/Integrations/Include/Shared/Babylon/Integrations/View.h index dbba9b197..73097782c 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/View.h +++ b/Integrations/Include/Shared/Babylon/Integrations/View.h @@ -26,42 +26,44 @@ namespace Babylon::Integrations Logical, }; - // Transient: created when a host surface appears, destroyed when it - // goes away. **At most one View may be attached at a time**; multiple - // sequential Views may share one Runtime over its lifetime. To switch - // surfaces, destroy the current View and construct a new one. + // Transient: constructed against a host surface, destroyed when the + // surface goes away. **At most one View may be attached to a Runtime + // at a time**; multiple sequential Views may share one Runtime over + // its lifetime. To switch surfaces, destroy the current View and + // construct a new one. class View { public: - // Attach `nativeWindow` (platform-specific surface handle) to `runtime`. + // Attach to `runtime`, binding `nativeWindow` (platform-specific + // surface handle: HWND on Win32, ANativeWindow* on Android, + // CA::MetalLayer* on Apple, X11 Window, winrt::IInspectable). The + // host owns the surface size; the View binds the window to the + // Device on the first `Resize`. Hosts MUST call `Resize` at least + // once with the surface's pixel dimensions before the first frame + // is rendered. // - // `nativeWindow` is `Babylon::Graphics::WindowT` — the same per-platform - // typedef the Graphics layer uses (HWND, ANativeWindow*, CA::MetalLayer*, - // X11 Window, winrt::IInspectable). The host owns the surface size; the - // View binds the window to the Device on the first `Resize`. Hosts MUST - // call `Resize` at least once before the first frame will be rendered. + // Throws `std::runtime_error` if another View is already attached + // to this Runtime. // - // The first Attach+Resize on a given Runtime is the heavy step: it - // constructs the Device, initializes GPU plugins (`Device::AddToJavaScript`, - // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`), flushes - // queued `Runtime::LoadScript` calls, and opens the first frame. - // Subsequent Attach+Resize calls on the same Runtime just rebind the - // Device to the new surface. `~View` closes the in-flight frame; the - // Device persists on the Runtime so the next Attach is fast. + // The first attach for a given Runtime is the heavy step: the + // first `Resize` after this constructs the Device, initializes + // GPU plugins (`Device::AddToJavaScript`, `NativeEngine::Initialize`, + // `NativeInput::CreateForJavaScript`), flushes queued `LoadScript` + // calls, and opens the first frame. Subsequent attaches on the + // same Runtime just rebind the Device to the new surface. + // Destroying the View closes the in-flight frame; the Device + // persists on the Runtime so the next View is fast. // - // Device-rebind work is deferred to the first `Resize` because - // `Device::UpdateWindow` MUST be paired with a matching `UpdateSize`. - // - // Must be called from the "frame thread" — the same thread that will - // call `RenderFrame` and `Resize`. - static std::unique_ptr Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow); + // Must be called from the "frame thread" — the same thread that + // will call `RenderFrame` and `Resize`. + View(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow); ~View(); View(const View&) = delete; View& operator=(const View&) = delete; - View(View&&) = delete; - View& operator=(View&&) = delete; + View(View&&) noexcept; + View& operator=(View&&) noexcept; // Render exactly one frame on the frame thread. No-op if the runtime // is suspended. Called from the platform view/control's draw callback @@ -108,10 +110,6 @@ namespace Babylon::Integrations #endif private: - friend class Runtime; - std::unique_ptr m_impl; - - explicit View(std::unique_ptr impl); }; } \ No newline at end of file diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 5e806690e..8f4128706 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -92,7 +92,7 @@ namespace Babylon::Integrations // Construct AppRuntime: starts the JS thread and creates a Napi::Env. // Plugin Initialize() calls are dispatched onto this thread by the - // first View::Attach. + // first View attach. Babylon::AppRuntime::Options appRuntimeOptions{}; appRuntimeOptions.EnableDebugger = m_options.enableDebugger; appRuntimeOptions.WaitForDebugger = m_options.waitForDebugger; @@ -122,8 +122,8 @@ namespace Babylon::Integrations assert(m_currentView == nullptr && "View must be destroyed before its Runtime."); // Teardown order: - // 1. SaveShaderCache: ~View already ran ViewImpl::Suspend, so the - // engine is quiescent and a host-thread Save is race-free. + // 1. SaveShaderCache: ~ViewImpl already ran ViewImpl::Suspend, so + // the engine is quiescent and a host-thread Save is race-free. // 2. ~ScriptLoader before ~AppRuntime (dispatcher captures it). // 3. Canvas / NativeInput / NativeXr hold JS-thread-bound state; // drop them before joining the JS thread. @@ -164,10 +164,10 @@ namespace Babylon::Integrations m_device.reset(); } - // Dispatched onto the JS thread by the first View::Attach. Initializes + // Dispatched onto the JS thread by the first View attach. Initializes // all plugins/polyfills in the same order as AppContext.cpp, then - // completes m_initTcs to unblock host calls that were queued before - // first Attach. Post-init, those host calls fire their continuation + // completes m_initTcs to unblock host calls that were queued before the + // first attach. Post-init, those host calls fire their continuation // synchronously via inline_scheduler and submit straight to ScriptLoader. void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window) { @@ -267,7 +267,7 @@ namespace Babylon::Integrations (void)window; #endif - // 4. Fire any host calls queued before the first Attach. + // 4. Fire any host calls queued before the first View attach. implPtr->m_initTcs.complete(); }); } @@ -302,16 +302,14 @@ namespace Babylon::Integrations } #endif - std::unique_ptr Runtime::Create(RuntimeOptions options) + Runtime::Runtime(RuntimeOptions options) + : m_impl{std::make_unique(std::move(options))} { - // Private ctor; manual unique_ptr because make_unique can't see it. - std::unique_ptr runtime{new Runtime()}; - runtime->m_impl = std::make_unique(std::move(options)); - return runtime; } - Runtime::Runtime() = default; Runtime::~Runtime() = default; + Runtime::Runtime(Runtime&&) noexcept = default; + Runtime& Runtime::operator=(Runtime&&) noexcept = default; void Runtime::LoadScript(std::string_view url) { @@ -353,11 +351,11 @@ namespace Babylon::Integrations // Close the in-flight frame on the attached View (if any) BEFORE // blocking the JS thread: keeps the GPU side clean (no held // drawable, no open DeviceUpdate safe-timespan). Composes - // correctly with ~View (re-Suspend is a no-op) and with + // correctly with ~ViewImpl (re-Suspend is a no-op) and with // suspending while no View is attached. if (m_impl->m_currentView) { - m_impl->m_currentView->m_impl->Suspend(); + m_impl->m_currentView->Suspend(); } #if BABYLON_NATIVE_PLUGIN_SHADERCACHE // Engine is quiescent here (ViewImpl::Suspend just closed the @@ -385,7 +383,7 @@ namespace Babylon::Integrations // first-Resize init via InitializeIfReady. if (m_impl->m_currentView) { - m_impl->m_currentView->m_impl->Resume(); + m_impl->m_currentView->Resume(); } } } @@ -404,8 +402,8 @@ namespace Babylon::Integrations { m_impl->m_nativeXr->UpdateWindow(nativeWindow); } - // If NativeXr isn't initialized yet (no View::Attach has happened), - // the value is applied by the first-Attach init lambda. + // If NativeXr isn't initialized yet (no View attach has happened), + // the value is applied by the first-attach init lambda. } bool Runtime::IsXrActive() const diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index d189651ae..f4023ffed 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -46,7 +46,7 @@ namespace Babylon::Integrations std::optional m_appRuntime; std::optional m_scriptLoader; - // Lazily constructed during the first View::Attach. + // Lazily constructed during the first attached View. std::optional m_device; std::optional m_deviceUpdate; @@ -61,7 +61,7 @@ namespace Babylon::Integrations #endif #if BABYLON_NATIVE_PLUGIN_NATIVEXR - // NativeXr is initialized during the first View::Attach (needs a + // NativeXr is initialized during the first View attach (needs a // Napi::Env). Until then, m_xrWindow holds the most recent // SetXrWindow value for application post-init. m_xrMutex serializes // these fields. m_isXrActive is written from the JS thread and @@ -74,7 +74,7 @@ namespace Babylon::Integrations // Pre-init queue for host LoadScript / Eval / RunOnJsThread. // Each call chains its work off `m_initTcs.as_task().then(...)`. - // The first-Attach init lambda calls `m_initTcs.complete()` after + // The first-attach init lambda calls `m_initTcs.complete()` after // all plugins are initialized, firing every queued continuation // (in registration order) on the JS thread. After completion, // future `.then(inline_scheduler, ...)` calls run synchronously @@ -88,10 +88,12 @@ namespace Babylon::Integrations // thread; atomic so `IsSuspended()` can be polled from any thread. std::atomic m_suspendCount{0}; - // 0..1 — enforces "at most one View attached at a time". - View* m_currentView{nullptr}; + // 0..1 — enforces "at most one View attached at a time". Points + // at the ViewImpl directly (not the outer View), so the back-ref + // stays valid across moves of the outer View. + ViewImpl* m_currentView{nullptr}; - // Dispatched onto the JS thread by the very first View::Attach. + // Dispatched onto the JS thread by the very first View attach. // Runs all plugin/polyfill Initialize calls, wires NativeXr // callbacks, and completes m_initTcs. `window` is forwarded to // TestUtils::Initialize; ignored otherwise. @@ -101,7 +103,7 @@ namespace Babylon::Integrations // Persistent shader cache. No-ops when `m_options.shaderCachePath` // is empty. Both run synchronously on the host thread at points // where the engine is known not to be compiling shaders - // (first-Attach, post-view-Suspend, ~RuntimeImpl) — no JS-thread + // (first-attach, post-view-Suspend, ~RuntimeImpl) — no JS-thread // coordination required. void LoadShaderCache(); void SaveShaderCache(); @@ -109,11 +111,11 @@ namespace Babylon::Integrations }; // Internal implementation of View. Owns the back-reference to its - // Runtime, the window handle captured at Attach, the most recent - // Resize size, and the frame-lifecycle Suspend/Resume hooks. + // RuntimeImpl, the window handle captured at construction, the most + // recent Resize size, and the frame-lifecycle Suspend/Resume hooks. // - // Attach is cheap: it just stashes the window and registers as the - // current view. All Device work runs in `InitializeIfReady`, which + // Construction is cheap: it just stashes the window and registers as + // the current view. All Device work runs in `InitializeIfReady`, which // is gated on three preconditions: // // 1. `m_initialized == false`, @@ -129,11 +131,16 @@ namespace Babylon::Integrations // ViewImpl::Suspend/Resume toggle that frame on the 0↔1 transitions. struct ViewImpl { - explicit ViewImpl(Runtime& runtime) : m_runtime{runtime} {} - Runtime& m_runtime; + explicit ViewImpl(RuntimeImpl& runtime) : m_runtime{runtime} {} + ~ViewImpl(); - // Captured at View::Attach; used by InitializeIfReady to - // construct (first-Attach-ever) or rebind the Device. + ViewImpl(const ViewImpl&) = delete; + ViewImpl& operator=(const ViewImpl&) = delete; + + RuntimeImpl& m_runtime; + + // Captured at construction; used by InitializeIfReady to + // construct (first-attach-ever) or rebind the Device. Babylon::Graphics::WindowT m_window{}; // Most recent Resize size, in logical pixels. Empty until the @@ -141,11 +148,11 @@ namespace Babylon::Integrations std::optional> m_size; // Latches true the first time InitializeIfReady succeeds; never - // flips back. A new ViewImpl is constructed per View::Attach. + // flips back. A new ViewImpl is constructed per View attach. bool m_initialized{false}; // Close the in-flight frame (called by Runtime::Suspend on 0→1 - // and by ~View). No-op when uninitialized. + // and by ~ViewImpl). No-op when uninitialized. void Suspend(); // Open a new frame (called by Runtime::Resume on 1→0). When diff --git a/Integrations/Source/View.cpp b/Integrations/Source/View.cpp index e51ff9996..38113ff57 100644 --- a/Integrations/Source/View.cpp +++ b/Integrations/Source/View.cpp @@ -3,9 +3,9 @@ #include #include -#include #include #include +#include namespace Babylon::Integrations { @@ -43,53 +43,40 @@ namespace Babylon::Integrations } } - // Lightweight: stash the window handle and register as current view. - // All Device work (first-time construction, or `UpdateWindow` + - // `UpdateSize` on a re-attach) is deferred to - // `ViewImpl::InitializeIfReady`, gated on all three preconditions - // (window stashed, size known, not externally suspended). Pre-init - // `RenderFrame` calls are safe — they early-out while `m_initialized` - // is false. See `RuntimeImpl.h` for the full state machine. - std::unique_ptr View::Attach(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) + View::View(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) { - RuntimeImpl& impl = *runtime.m_impl; - - assert(impl.m_currentView == nullptr && "Only one View may be attached at a time."); - if (impl.m_currentView != nullptr) + RuntimeImpl& runtimeImpl = *runtime.m_impl; + if (runtimeImpl.m_currentView != nullptr) { - return nullptr; + throw std::runtime_error{"Only one View may be attached to a Runtime at a time."}; } - std::unique_ptr view{new View{std::make_unique(runtime)}}; - view->m_impl->m_window = nativeWindow; - impl.m_currentView = view.get(); - return view; + m_impl = std::make_unique(runtimeImpl); + m_impl->m_window = nativeWindow; + runtimeImpl.m_currentView = m_impl.get(); } - View::View(std::unique_ptr impl) - : m_impl{std::move(impl)} - { - } + View::~View() = default; + View::View(View&&) noexcept = default; + View& View::operator=(View&&) noexcept = default; - View::~View() + ViewImpl::~ViewImpl() { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; - // Close the in-flight frame iff one is currently open: i.e. the // view is initialized and the Runtime is not externally suspended - // (Suspend already closed the frame via ViewImpl::Suspend). The - // Device persists on the Runtime so the next Attach is cheap. - if (m_impl->m_initialized && - impl.m_suspendCount.load(std::memory_order_relaxed) == 0) + // (Suspend already closed the frame). The Device persists on the + // Runtime so the next View is cheap. + if (m_initialized && + m_runtime.m_suspendCount.load(std::memory_order_relaxed) == 0) { - impl.m_deviceUpdate->Finish(); - impl.m_device->FinishRenderingCurrentFrame(); + m_runtime.m_deviceUpdate->Finish(); + m_runtime.m_device->FinishRenderingCurrentFrame(); } - impl.m_currentView = nullptr; + m_runtime.m_currentView = nullptr; } - // Called by Runtime::Suspend/Resume on the 0↔1 transition (and ~View + // Called by Runtime::Suspend/Resume on the 0↔1 transition (and ~ViewImpl // for teardown). When initialized, these are pure frame open/close. // When uninitialized, Suspend is a no-op and Resume retries // InitializeIfReady — the suspendCount drop may have been the last @@ -100,9 +87,8 @@ namespace Babylon::Integrations { return; } - RuntimeImpl& impl = *m_runtime.m_impl; - impl.m_deviceUpdate->Finish(); - impl.m_device->FinishRenderingCurrentFrame(); + m_runtime.m_deviceUpdate->Finish(); + m_runtime.m_device->FinishRenderingCurrentFrame(); } void ViewImpl::Resume() @@ -114,9 +100,8 @@ namespace Babylon::Integrations InitializeIfReady(); return; } - RuntimeImpl& impl = *m_runtime.m_impl; - impl.m_device->StartRenderingCurrentFrame(); - impl.m_deviceUpdate->Start(); + m_runtime.m_device->StartRenderingCurrentFrame(); + m_runtime.m_deviceUpdate->Start(); } // Single recipe for binding the Device to this view's window and @@ -129,51 +114,50 @@ namespace Babylon::Integrations { return; } - RuntimeImpl& impl = *m_runtime.m_impl; - if (impl.m_suspendCount.load(std::memory_order_relaxed) > 0) + if (m_runtime.m_suspendCount.load(std::memory_order_relaxed) > 0) { return; } const auto [lw, lh] = *m_size; - const bool firstAttachEver = !impl.m_device; + const bool firstAttachEver = !m_runtime.m_device; if (firstAttachEver) { Babylon::Graphics::Configuration config{}; config.Window = m_window; config.Width = lw; config.Height = lh; - config.MSAASamples = impl.m_options.msaaSamples; + config.MSAASamples = m_runtime.m_options.msaaSamples; - impl.m_device.emplace(config); - impl.m_deviceUpdate.emplace(impl.m_device->GetUpdate("update")); + m_runtime.m_device.emplace(config); + m_runtime.m_deviceUpdate.emplace(m_runtime.m_device->GetUpdate("update")); } else { // Re-attach to an existing Runtime: rebind surface and size in // lockstep. JS state (plugins, polyfills, scripts) is preserved. - impl.m_device->UpdateWindow(m_window); - impl.m_device->UpdateSize(lw, lh); + m_runtime.m_device->UpdateWindow(m_window); + m_runtime.m_device->UpdateSize(lw, lh); } - // Open the first frame inline. On the very first Attach this MUST + // Open the first frame inline. On the very first attach this MUST // happen BEFORE dispatching RunFirstAttachInit, so the // Device::AddToJavaScript inside that lambda sees an open frame to // record into. Both run on the host thread, so the dispatched // lambda always sees an open frame on the JS thread. - impl.m_device->StartRenderingCurrentFrame(); - impl.m_deviceUpdate->Start(); + m_runtime.m_device->StartRenderingCurrentFrame(); + m_runtime.m_deviceUpdate->Start(); m_initialized = true; if (firstAttachEver) { - impl.RunFirstAttachInit(m_window); + m_runtime.RunFirstAttachInit(m_window); } } void View::RenderFrame() { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; // Host may call unconditionally from its draw callback; the // not-rendering cases early-out. @@ -206,7 +190,7 @@ namespace Babylon::Integrations { ValidateNonZeroSize(width, height, "View::Resize size"); - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; // DPR source: before init, query the window directly (Device // doesn't exist or is still bound to the previous attach). After @@ -234,7 +218,7 @@ namespace Babylon::Integrations #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT void View::OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input && impl.m_device) { const auto [lx, ly] = ToLogicalCoords(x, y, units, @@ -247,7 +231,7 @@ namespace Babylon::Integrations void View::OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input && impl.m_device) { const auto [lx, ly] = ToLogicalCoords(x, y, units, @@ -260,7 +244,7 @@ namespace Babylon::Integrations void View::OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input && impl.m_device) { const auto [lx, ly] = ToLogicalCoords(x, y, units, @@ -273,7 +257,7 @@ namespace Babylon::Integrations void View::OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input && impl.m_device) { const auto [lx, ly] = ToLogicalCoords(x, y, units, @@ -286,7 +270,7 @@ namespace Babylon::Integrations void View::OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input && impl.m_device) { const auto [lx, ly] = ToLogicalCoords(x, y, units, @@ -299,7 +283,7 @@ namespace Babylon::Integrations void View::OnMouseMove(float x, float y, CoordinateUnits units) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input && impl.m_device) { const auto [lx, ly] = ToLogicalCoords(x, y, units, @@ -311,7 +295,7 @@ namespace Babylon::Integrations void View::OnMouseWheel(uint32_t wheelAxis, int32_t scrollValue) { - RuntimeImpl& impl = *m_impl->m_runtime.m_impl; + RuntimeImpl& impl = m_impl->m_runtime; if (impl.m_input) { impl.m_input->MouseWheel(wheelAxis, scrollValue); From 3a01fc427d7e7740eec9645546e2e461d7b59e45 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 20 May 2026 14:45:42 -0700 Subject: [PATCH 44/71] Add optional param for RunOnJsThread for ScriptLoader vs. AppRuntime Dispatch --- .../Shared/Babylon/Integrations/Runtime.h | 19 ++++++++++++++----- Integrations/Source/Runtime.cpp | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 57c421255..c9e1607ac 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -53,11 +53,20 @@ namespace Babylon::Integrations void LoadScript(std::string_view url); void Eval(std::string_view source, std::string_view sourceUrl = {}); - // Escape hatch: post `callback` onto the JS thread after any pending - // init completes. Useful for installing custom Napi globals, registering - // ObjectWrap classes, or capturing FunctionReferences for native→JS calls. - // Same single-thread contract as LoadScript / Eval. - void RunOnJsThread(std::function callback); + // Escape hatch: post `callback` onto the JS thread after engine + // initialization completes. Useful for installing custom Napi globals, + // registering ObjectWrap classes, or capturing FunctionReferences for + // native→JS calls. Same single-thread contract as LoadScript / Eval. + // + // `afterScriptLoad`: + // false (default) — dispatch via `AppRuntime::Dispatch`. The callback + // runs on the JS thread as soon as init is complete; it is NOT + // serialized behind queued `LoadScript` / `Eval` calls and may + // interleave with their execution. + // true — dispatch via `ScriptLoader::Dispatch`. The callback is + // serialized behind any earlier `LoadScript` / `Eval` / `RunOnJsThread(true)` + // calls, so it observes the JS state after those have finished. + void RunOnJsThread(std::function callback, bool afterScriptLoad = false); // ----- Suspend / Resume ----- // diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 8f4128706..52e9fa226 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -329,7 +329,7 @@ namespace Babylon::Integrations }); } - void Runtime::RunOnJsThread(std::function callback) + void Runtime::RunOnJsThread(std::function callback, bool afterScriptLoad) { if (!callback) { @@ -337,8 +337,18 @@ namespace Babylon::Integrations } m_impl->m_initTcs.as_task().then(arcana::inline_scheduler, arcana::cancellation::none(), - [scriptLoader = &*m_impl->m_scriptLoader, cb = std::move(callback)]() mutable { - scriptLoader->Dispatch(std::move(cb)); + [appRuntime = &*m_impl->m_appRuntime, + scriptLoader = &*m_impl->m_scriptLoader, + cb = std::move(callback), + afterScriptLoad]() mutable { + if (afterScriptLoad) + { + scriptLoader->Dispatch(std::move(cb)); + } + else + { + appRuntime->Dispatch(std::move(cb)); + } }); } From e71933b43097ad33297a60e7aba89078b3ba66e9 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 20 May 2026 16:10:08 -0700 Subject: [PATCH 45/71] Port UWP Playground --- Apps/Playground/UWP/App.cpp | 255 +++++++++++++++++++----------------- Apps/Playground/UWP/App.h | 13 +- 2 files changed, 147 insertions(+), 121 deletions(-) diff --git a/Apps/Playground/UWP/App.cpp b/Apps/Playground/UWP/App.cpp index f4b728879..10f485ac6 100644 --- a/Apps/Playground/UWP/App.cpp +++ b/Apps/Playground/UWP/App.cpp @@ -1,7 +1,16 @@ +// App.cpp : Defines the entry point for the application. +// +// Built on Babylon::Integrations: the cross-platform Runtime + View API +// handles plugin/polyfill setup, GPU device construction, frame rendering, +// and input forwarding. + #include "App.h" -#include +#include + #include +#include +#include using namespace winrt::Windows::ApplicationModel; using namespace winrt::Windows::ApplicationModel::Core; @@ -12,6 +21,9 @@ using namespace winrt::Windows::System; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Graphics::Display; +using Babylon::Integrations::CoordinateUnits; +using BNView = Babylon::Integrations::View; + // The main function is only used to initialize our IFrameworkView class. int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { @@ -20,29 +32,33 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) return 0; } -// Helper function to handle mouse button routing -void ProcessMouseButtons(Babylon::Plugins::NativeInput* input, PointerUpdateKind updateKind, int x, int y) +namespace { - switch (updateKind) + void ProcessMouseButtons(BNView& view, PointerUpdateKind updateKind, float x, float y) { - case PointerUpdateKind::LeftButtonPressed: - input->MouseDown(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, x, y); - break; - case PointerUpdateKind::MiddleButtonPressed: - input->MouseDown(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, x, y); - break; - case PointerUpdateKind::RightButtonPressed: - input->MouseDown(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, x, y); - break; - case PointerUpdateKind::LeftButtonReleased: - input->MouseUp(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, x, y); - break; - case PointerUpdateKind::MiddleButtonReleased: - input->MouseUp(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, x, y); - break; - case PointerUpdateKind::RightButtonReleased: - input->MouseUp(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, x, y); - break; + switch (updateKind) + { + case PointerUpdateKind::LeftButtonPressed: + view.OnMouseDown(BNView::LeftMouseButton(), x, y, CoordinateUnits::Logical); + break; + case PointerUpdateKind::MiddleButtonPressed: + view.OnMouseDown(BNView::MiddleMouseButton(), x, y, CoordinateUnits::Logical); + break; + case PointerUpdateKind::RightButtonPressed: + view.OnMouseDown(BNView::RightMouseButton(), x, y, CoordinateUnits::Logical); + break; + case PointerUpdateKind::LeftButtonReleased: + view.OnMouseUp(BNView::LeftMouseButton(), x, y, CoordinateUnits::Logical); + break; + case PointerUpdateKind::MiddleButtonReleased: + view.OnMouseUp(BNView::MiddleMouseButton(), x, y, CoordinateUnits::Logical); + break; + case PointerUpdateKind::RightButtonReleased: + view.OnMouseUp(BNView::RightMouseButton(), x, y, CoordinateUnits::Logical); + break; + default: + break; + } } } @@ -107,12 +123,9 @@ void App::Run() { while (!m_windowClosed) { - if (m_appContext) + if (m_view) { - m_appContext->DeviceUpdate().Finish(); - m_appContext->Device().FinishRenderingCurrentFrame(); - m_appContext->Device().StartRenderingCurrentFrame(); - m_appContext->DeviceUpdate().Start(); + m_view->RenderFrame(); } CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent); @@ -124,7 +137,10 @@ void App::Run() // class is torn down while the app is in the foreground. void App::Uninitialize() { - m_appContext.reset(); + // View first (unbinds surface, closes in-flight frame), then Runtime + // (joins JS thread). + m_view.reset(); + m_runtime.reset(); } // Application lifecycle event handlers. @@ -154,12 +170,10 @@ void App::OnSuspending(IInspectable const& /*sender*/, SuspendingEventArgs const // the app will be forced to exit. auto deferral = args.SuspendingOperation().GetDeferral(); - if (m_appContext) + if (m_runtime) { - m_appContext->DeviceUpdate().Finish(); - m_appContext->Device().FinishRenderingCurrentFrame(); - - m_appContext->Runtime().Suspend(); + // Closes any in-flight frame on the attached View internally. + m_runtime->Suspend(); } deferral.Complete(); @@ -167,15 +181,10 @@ void App::OnSuspending(IInspectable const& /*sender*/, SuspendingEventArgs const void App::OnResuming(IInspectable const& /*sender*/, IInspectable const& /*args*/) { - if (m_appContext) + if (m_runtime) { - // Restore any data or state that was unloaded on suspend. By default, data - // and state are persisted when resuming from suspend. Note that this event - // does not occur if the app was previously terminated. - m_appContext->Runtime().Resume(); - - m_appContext->Device().StartRenderingCurrentFrame(); - m_appContext->DeviceUpdate().Start(); + // Re-opens the frame on the attached View internally. + m_runtime->Resume(); } } @@ -183,11 +192,11 @@ void App::OnResuming(IInspectable const& /*sender*/, IInspectable const& /*args* void App::OnWindowSizeChanged(CoreWindow const& /*sender*/, WindowSizeChangedEventArgs const& args) { - if (m_appContext) + if (m_view) { - size_t width = static_cast(args.Size().Width * m_displayScale); - size_t height = static_cast(args.Size().Height * m_displayScale); - m_appContext->Device().UpdateSize(width, height); + m_view->Resize(static_cast(args.Size().Width), + static_cast(args.Size().Height), + CoordinateUnits::Logical); } } @@ -204,81 +213,78 @@ void App::OnWindowClosed(CoreWindow const& /*sender*/, CoreWindowEventArgs const void App::OnPointerMoved(CoreWindow const&, PointerEventArgs const& args) { - if (m_appContext && m_appContext->Input()) + if (!m_view) return; + + const auto position = args.CurrentPoint().RawPosition(); + const auto deviceType = args.CurrentPoint().PointerDevice().PointerDeviceType(); + const auto deviceSlot = args.CurrentPoint().PointerId(); + const auto updateKind = args.CurrentPoint().Properties().PointerUpdateKind(); + const auto x = position.X; + const auto y = position.Y; + + if (deviceType == winrt::Windows::Devices::Input::PointerDeviceType::Mouse) { - const auto position = args.CurrentPoint().RawPosition(); - const auto deviceType = args.CurrentPoint().PointerDevice().PointerDeviceType(); - const auto deviceSlot = args.CurrentPoint().PointerId(); - const auto updateKind = args.CurrentPoint().Properties().PointerUpdateKind(); - const auto x = static_cast(position.X * m_displayScale); - const auto y = static_cast(position.Y * m_displayScale); - - if (deviceType == winrt::Windows::Devices::Input::PointerDeviceType::Mouse) - { - m_appContext->Input()->MouseMove(x, y); + m_view->OnMouseMove(x, y, CoordinateUnits::Logical); - if (args.CurrentPoint().IsInContact()) - { - ProcessMouseButtons(m_appContext->Input(), updateKind, x, y); - } - } - else + if (args.CurrentPoint().IsInContact()) { - m_appContext->Input()->TouchMove(deviceSlot, x, y); + ProcessMouseButtons(*m_view, updateKind, x, y); } } + else + { + m_view->OnPointerMove(static_cast(deviceSlot), x, y, CoordinateUnits::Logical); + } } void App::OnPointerPressed(CoreWindow const&, PointerEventArgs const& args) { - if (m_appContext && m_appContext->Input()) + if (!m_view) return; + + const auto position = args.CurrentPoint().RawPosition(); + const auto deviceType = args.CurrentPoint().PointerDevice().PointerDeviceType(); + const auto deviceSlot = args.CurrentPoint().PointerId(); + const auto updateKind = args.CurrentPoint().Properties().PointerUpdateKind(); + const auto x = position.X; + const auto y = position.Y; + + if (deviceType == winrt::Windows::Devices::Input::PointerDeviceType::Mouse) { - const auto position = args.CurrentPoint().RawPosition(); - const auto deviceType = args.CurrentPoint().PointerDevice().PointerDeviceType(); - const auto deviceSlot = args.CurrentPoint().PointerId(); - const auto updateKind = args.CurrentPoint().Properties().PointerUpdateKind(); - const auto x = static_cast(position.X * m_displayScale); - const auto y = static_cast(position.Y * m_displayScale); - - if (deviceType == winrt::Windows::Devices::Input::PointerDeviceType::Mouse) - { - ProcessMouseButtons(m_appContext->Input(), updateKind, x, y); - } - else - { - m_appContext->Input()->TouchDown(deviceSlot, x, y); - } + ProcessMouseButtons(*m_view, updateKind, x, y); + } + else + { + m_view->OnPointerDown(static_cast(deviceSlot), x, y, CoordinateUnits::Logical); } } void App::OnPointerReleased(CoreWindow const&, PointerEventArgs const& args) { - if (m_appContext && m_appContext->Input()) + if (!m_view) return; + + const auto position = args.CurrentPoint().RawPosition(); + const auto deviceType = args.CurrentPoint().PointerDevice().PointerDeviceType(); + const auto deviceSlot = args.CurrentPoint().PointerId(); + const auto updateKind = args.CurrentPoint().Properties().PointerUpdateKind(); + const auto x = position.X; + const auto y = position.Y; + + if (deviceType == winrt::Windows::Devices::Input::PointerDeviceType::Mouse) { - const auto position = args.CurrentPoint().RawPosition(); - const auto deviceType = args.CurrentPoint().PointerDevice().PointerDeviceType(); - const auto deviceSlot = args.CurrentPoint().PointerId(); - const auto updateKind = args.CurrentPoint().Properties().PointerUpdateKind(); - const auto x = static_cast(position.X * m_displayScale); - const auto y = static_cast(position.Y * m_displayScale); - - if (deviceType == winrt::Windows::Devices::Input::PointerDeviceType::Mouse) - { - ProcessMouseButtons(m_appContext->Input(), updateKind, x, y); - } - else - { - m_appContext->Input()->TouchUp(deviceSlot, x, y); - } + ProcessMouseButtons(*m_view, updateKind, x, y); + } + else + { + m_view->OnPointerUp(static_cast(deviceSlot), x, y, CoordinateUnits::Logical); } } void App::OnPointerWheelChanged(CoreWindow const&, PointerEventArgs const& args) { - if (m_appContext && m_appContext->Input()) + if (m_view) { const auto delta = args.CurrentPoint().Properties().MouseWheelDelta(); - m_appContext->Input()->MouseWheel(Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID, -delta); + m_view->OnMouseWheel(BNView::MouseWheelY(), -delta); } } @@ -294,9 +300,9 @@ void App::OnKeyPressed(CoreWindow const& window, KeyEventArgs const& args) void App::OnDpiChanged(DisplayInformation const& /*sender*/, IInspectable const& /*args*/) { - DisplayInformation displayInformation = DisplayInformation::GetForCurrentView(); - m_displayScale = static_cast(displayInformation.RawPixelsPerViewPixel()); - // resize event happens after. No need to force resize here. + // DPR is queried by the View through Babylon::Graphics::GetDevicePixelRatio + // on each Resize, so we don't cache it here. SizeChanged fires after a DPI + // change, which re-drives Resize. } void App::OnOrientationChanged(DisplayInformation const& /*sender*/, IInspectable const& /*args*/) @@ -315,26 +321,27 @@ void App::RestartRuntime(Rect bounds) { Uninitialize(); - DisplayInformation displayInformation = DisplayInformation::GetForCurrentView(); - m_displayScale = static_cast(displayInformation.RawPixelsPerViewPixel()); - size_t width = static_cast(bounds.Width * m_displayScale); - size_t height = static_cast(bounds.Height * m_displayScale); - IInspectable window{CoreWindow::GetForCurrentThread()}; + Babylon::Integrations::RuntimeOptions runtimeOptions{}; + runtimeOptions.enableDebugger = true; + runtimeOptions.log = [](Babylon::Integrations::LogLevel /*level*/, std::string_view message) { + std::string line{message}; + if (line.empty() || line.back() != '\n') + { + line.push_back('\n'); + } + OutputDebugStringA(line.c_str()); + std::cout << line; + }; - m_appContext.emplace( - window, - width, - height, - [](const char* message) { - std::ostringstream ss{}; - ss << message << std::endl; - OutputDebugStringA(ss.str().data()); - std::cout << ss.str(); - }); + m_runtime.emplace(std::move(runtimeOptions)); + + PlaygroundOptions playgroundOptions{}; + Playground::Initialize(playgroundOptions); + Playground::LoadBootstrapScripts(*m_runtime); if (m_files == nullptr) { - m_appContext->ScriptLoader().LoadScript("app:///Scripts/experience.js"); + m_runtime->LoadScript("app:///Scripts/experience.js"); } else { @@ -344,9 +351,21 @@ void App::RestartRuntime(Rect bounds) // There is no built-in way to convert a local file path to a url in UWP, but // Foundation::Uri works with a url constructed using "file:///" with a local path. - m_appContext->ScriptLoader().LoadScript("file:///" + winrt::to_string(file.Path())); + m_runtime->LoadScript("file:///" + winrt::to_string(file.Path())); } - m_appContext->ScriptLoader().LoadScript("app:///Scripts/playground_runner.js"); + m_runtime->LoadScript("app:///Scripts/playground_runner.js"); } + + // First View attach triggers Device construction, plugin init, and + // flushes the queued scripts. The CoreWindow is passed as IInspectable + // (matching Babylon::Graphics::WindowT on WinRT). + IInspectable window{CoreWindow::GetForCurrentThread()}; + m_view.emplace(*m_runtime, window); + + // Drive the first Resize with the initial window bounds (logical pixels); + // the View handles physical conversion via GetDevicePixelRatio. + m_view->Resize(static_cast(bounds.Width), + static_cast(bounds.Height), + CoordinateUnits::Logical); } diff --git a/Apps/Playground/UWP/App.h b/Apps/Playground/UWP/App.h index bcff16385..a02b580e2 100644 --- a/Apps/Playground/UWP/App.h +++ b/Apps/Playground/UWP/App.h @@ -1,6 +1,8 @@ #pragma once -#include +#include +#include + #include #include @@ -50,12 +52,17 @@ struct App : winrt::implements m_appContext{}; + // Process-scoped: created on app start, recreated on 'R' refresh, + // destroyed in Uninitialize. + std::optional m_runtime{}; + + // Window-scoped: created during RestartRuntime, destroyed in Uninitialize + // (and before a fresh Runtime is constructed). + std::optional m_view{}; winrt::Windows::Foundation::Collections::IVectorView m_files{nullptr}; bool m_windowClosed; bool m_windowVisible; - float m_displayScale{1.f}; }; struct Direct3DApplicationSource : winrt::implements From 8a00f5fd48653f7b9d18e1c663792db37a5417a1 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 20 May 2026 16:22:44 -0700 Subject: [PATCH 46/71] Port MacOS Playground --- .../PlaygroundBootstrap.h | 9 +- .../PlaygroundBootstrap.mm | 0 Apps/Playground/CMakeLists.txt | 11 +- .../iOS/Playground-Bridging-Header.h | 2 +- .../macOS/{AppDelegate.m => AppDelegate.mm} | 0 Apps/Playground/macOS/ViewController.mm | 222 ++++++------------ Apps/Playground/macOS/main.mm | 4 - Integrations/Apple/Source/BNView.mm | 95 ++++++++ .../Apple/Babylon/Integrations/Apple/BNView.h | 31 +++ 9 files changed, 215 insertions(+), 159 deletions(-) rename Apps/Playground/{iOS => AppleShared}/PlaygroundBootstrap.h (59%) rename Apps/Playground/{iOS => AppleShared}/PlaygroundBootstrap.mm (100%) rename Apps/Playground/macOS/{AppDelegate.m => AppDelegate.mm} (100%) diff --git a/Apps/Playground/iOS/PlaygroundBootstrap.h b/Apps/Playground/AppleShared/PlaygroundBootstrap.h similarity index 59% rename from Apps/Playground/iOS/PlaygroundBootstrap.h rename to Apps/Playground/AppleShared/PlaygroundBootstrap.h index 627c7129e..b432b89ea 100644 --- a/Apps/Playground/iOS/PlaygroundBootstrap.h +++ b/Apps/Playground/AppleShared/PlaygroundBootstrap.h @@ -1,8 +1,9 @@ // PlaygroundBootstrap.h — Obj-C helper exposed to Swift via the -// bridging header. Single class method that hands a freshly-created -// `BNRuntime` to `Apps/Playground/Shared/PlaygroundScripts.cpp`, -// which loads the Babylon.js bootstrap script list shared with the -// other Playground hosts (Win32, Android, …). +// bridging header on iOS, and used directly from Obj-C++ on macOS. +// Single class method that hands a freshly-created `BNRuntime` to +// `Apps/Playground/Shared/PlaygroundScripts.cpp`, which loads the +// Babylon.js bootstrap script list shared with the other Playground +// hosts (Win32, Android, …). #pragma once diff --git a/Apps/Playground/iOS/PlaygroundBootstrap.mm b/Apps/Playground/AppleShared/PlaygroundBootstrap.mm similarity index 100% rename from Apps/Playground/iOS/PlaygroundBootstrap.mm rename to Apps/Playground/AppleShared/PlaygroundBootstrap.mm diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 94e07a48b..bc2d55183 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -43,9 +43,9 @@ if(APPLE) set(SOURCES ${SOURCES} "iOS/AppDelegate.swift" "iOS/ViewController.swift" - "iOS/PlaygroundBootstrap.h" - "iOS/PlaygroundBootstrap.mm" "iOS/Playground-Bridging-Header.h" + "AppleShared/PlaygroundBootstrap.h" + "AppleShared/PlaygroundBootstrap.mm" "AppleShared/GestureRecognizer.swift") set_source_files_properties(${SCRIPTS} ${BABYLON_SCRIPTS} ${DEPENDENCIES} PROPERTIES MACOSX_PACKAGE_LOCATION "Scripts") set_source_files_properties(${REFERENCE_IMAGES} PROPERTIES MACOSX_PACKAGE_LOCATION "ReferenceImages") @@ -64,12 +64,15 @@ if(APPLE) set(PLIST_FILE "${CMAKE_CURRENT_LIST_DIR}/macOS/Info.plist") set(STORYBOARD "${CMAKE_CURRENT_LIST_DIR}/macOS/Base.lproj/Main.storyboard") set(RESOURCE_FILES ${STORYBOARD}) + set(ADDITIONAL_LIBRARIES PRIVATE BabylonNativeIntegrations) set(SOURCES ${SOURCES} "macOS/main.mm" - "macOS/AppDelegate.m" "macOS/AppDelegate.h" + "macOS/AppDelegate.mm" + "macOS/ViewController.h" "macOS/ViewController.mm" - "macOS/ViewController.h") + "AppleShared/PlaygroundBootstrap.h" + "AppleShared/PlaygroundBootstrap.mm") set_source_files_properties(${SCRIPTS} ${BABYLON_SCRIPTS} ${DEPENDENCIES} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/Scripts") set_source_files_properties(${REFERENCE_IMAGES} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/ReferenceImages") endif() diff --git a/Apps/Playground/iOS/Playground-Bridging-Header.h b/Apps/Playground/iOS/Playground-Bridging-Header.h index 58bad9104..572407097 100644 --- a/Apps/Playground/iOS/Playground-Bridging-Header.h +++ b/Apps/Playground/iOS/Playground-Bridging-Header.h @@ -8,4 +8,4 @@ #import -#import "PlaygroundBootstrap.h" +#import "AppleShared/PlaygroundBootstrap.h" diff --git a/Apps/Playground/macOS/AppDelegate.m b/Apps/Playground/macOS/AppDelegate.mm similarity index 100% rename from Apps/Playground/macOS/AppDelegate.m rename to Apps/Playground/macOS/AppDelegate.mm diff --git a/Apps/Playground/macOS/ViewController.mm b/Apps/Playground/macOS/ViewController.mm index 722f6f4f3..75fcbcfd3 100644 --- a/Apps/Playground/macOS/ViewController.mm +++ b/Apps/Playground/macOS/ViewController.mm @@ -1,49 +1,21 @@ #import "ViewController.h" -#include -#include +#import +#import "AppleShared/PlaygroundBootstrap.h" #import -std::optional appContext{}; - -@interface EngineView : MTKView - -@end - -@implementation EngineView - -- (void)mtkView:(MTKView *)__unused view drawableSizeWillChange:(CGSize) size -{ - if (appContext) { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - - appContext->Device().UpdateSize(static_cast(size.width), static_cast(size.height)); - - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); - } -} - -- (void)drawInMTKView:(MTKView *)__unused view +@implementation ViewController { - if (appContext) { - appContext->DeviceUpdate().Finish(); - appContext->Device().FinishRenderingCurrentFrame(); - appContext->Device().StartRenderingCurrentFrame(); - appContext->DeviceUpdate().Start(); - } + BNRuntime* _runtime; + BNView* _bnView; + MTKView* _mtkView; } -@end - -@implementation ViewController - - (void)viewDidLoad { [super viewDidLoad]; - // Required for mouseMoved events. + // Required for mouseMoved events to be delivered to the view. NSTrackingArea* trackingArea = [ [NSTrackingArea alloc] initWithRect:NSZeroRect @@ -55,41 +27,45 @@ - (void)viewDidLoad { } - (void)uninitialize { - appContext.reset(); + // Tear down View first (closes in-flight frame, unbinds the surface), + // then Runtime (joins the JS thread). + _bnView = nil; + _runtime = nil; + [_mtkView removeFromSuperview]; + _mtkView = nil; } - (void)refreshBabylon { [self uninitialize]; - EngineView* engineView = [[EngineView alloc] initWithFrame:[self view].frame device:nil]; - engineView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; - [[self view] addSubview:engineView]; - engineView.delegate = engineView; - size_t width = static_cast(engineView.drawableSize.width); - size_t height = static_cast(engineView.drawableSize.height); - - appContext.emplace( - (__bridge CA::MetalLayer*)engineView.layer, - width, - height, - [](const char* message) - { - NSLog(@"%s", message); - }); + BNRuntimeOptions* options = [[BNRuntimeOptions alloc] init]; + options.enableDebugger = YES; + options.enableDebugTrace = YES; + _runtime = [[BNRuntime alloc] initWithOptions:options]; - NSArray *arguments = [[NSProcessInfo processInfo] arguments]; + [PlaygroundBootstrap loadScripts:_runtime]; + + NSArray* arguments = [[NSProcessInfo processInfo] arguments]; if (arguments.count == 1) { - appContext->ScriptLoader().LoadScript("app:///Scripts/experience.js"); + [_runtime loadScript:@"app:///Scripts/experience.js"]; } else { - for (NSUInteger i = 1; i < arguments.count; i++) { - appContext->ScriptLoader().LoadScript([arguments[i] UTF8String]); + for (NSUInteger i = 1; i < arguments.count; i++) + { + [_runtime loadScript:arguments[i]]; } - - appContext->ScriptLoader().LoadScript("app:///Scripts/playground_runner.js"); + [_runtime loadScript:@"app:///Scripts/playground_runner.js"]; } + + _mtkView = [[MTKView alloc] initWithFrame:[self view].frame device:nil]; + _mtkView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [[self view] addSubview:_mtkView]; + + // BNView attaches the runtime to the MTKView and installs a default + // MTKViewDelegate that drives per-frame render + resize. + _bnView = [[BNView alloc] initWithRuntime:_runtime view:_mtkView]; } - (void)viewDidAppear { @@ -104,119 +80,73 @@ - (void)viewDidDisappear { [self uninitialize]; } -- (CGFloat)getScreenHeight { - return [self view].frame.size.height; +#pragma mark - Input forwarding + +// AppKit reports event locations in window coordinates with a bottom-left +// origin; Babylon (CSS) expects top-left. Convert here and pass logical +// pixels through unchanged — BNView handles device-pixel-ratio scaling. +- (NSPoint)logicalPointFromEvent:(NSEvent*)event { + NSPoint location = [event locationInWindow]; + CGFloat height = [self view].frame.size.height; + return NSMakePoint(location.x, height - location.y); } -- (void)mouseMoved:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseMove(eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)mouseMoved:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseMoveAtX:p.x y:p.y]; } -- (void)mouseDown:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)mouseDown:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseDown:BNView.leftMouseButton atX:p.x y:p.y]; } -- (void)mouseDragged:(NSEvent *)theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseMove(eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)mouseDragged:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseMoveAtX:p.x y:p.y]; } -- (void)mouseUp:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)mouseUp:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseUp:BNView.leftMouseButton atX:p.x y:p.y]; } -- (void)otherMouseDown:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)otherMouseDown:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseDown:BNView.middleMouseButton atX:p.x y:p.y]; } -- (void)otherMouseDragged:(NSEvent *)theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseMove(eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)otherMouseDragged:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseMoveAtX:p.x y:p.y]; } -- (void)otherMouseUp:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)otherMouseUp:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseUp:BNView.middleMouseButton atX:p.x y:p.y]; } -- (void)rightMouseDown:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)rightMouseDown:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseDown:BNView.rightMouseButton atX:p.x y:p.y]; } -- (void)rightMouseDragged:(NSEvent *)theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseMove(eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)rightMouseDragged:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseMoveAtX:p.x y:p.y]; } -- (void)rightMouseUp:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - NSPoint eventLocation = [theEvent locationInWindow]; - auto invertedY = [self getScreenHeight] - eventLocation.y; - CGFloat screenScale = [[NSScreen mainScreen] backingScaleFactor]; - appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, eventLocation.x * screenScale, invertedY * screenScale); - } +- (void)rightMouseUp:(NSEvent*)theEvent { + NSPoint p = [self logicalPointFromEvent:theEvent]; + [_bnView mouseUp:BNView.rightMouseButton atX:p.x y:p.y]; } -- (void)scrollWheel:(NSEvent *) theEvent { - if (appContext && appContext->Input()) - { - appContext->Input()->MouseWheel(Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID, -theEvent.deltaY); - } +- (void)scrollWheel:(NSEvent*)theEvent { + // Negate so scroll-up matches Babylon's negative-delta convention. + [_bnView mouseWheel:BNView.mouseWheelY delta:static_cast(-theEvent.deltaY)]; } -- (IBAction)refresh:(id)__unused sender -{ +- (IBAction)refresh:(id)__unused sender { [self refreshBabylon]; } diff --git a/Apps/Playground/macOS/main.mm b/Apps/Playground/macOS/main.mm index d6965c2a2..2db32b7fb 100644 --- a/Apps/Playground/macOS/main.mm +++ b/Apps/Playground/macOS/main.mm @@ -1,12 +1,8 @@ #import -#import #import int main(int argc, const char * argv[]) { Diagnostics::Initialize(); - Babylon::DebugTrace::EnableDebugTrace(true); - Babylon::DebugTrace::SetTraceOutput([](const char* trace) { NSLog(@"%s", trace); }); - return NSApplicationMain(argc, argv); } diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index 7af324e9b..fa26bbd30 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -166,4 +166,99 @@ - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y #endif } +#pragma mark - Mouse forwarding + +- (void)mouseDown:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + if (_view) + { + _view->OnMouseDown(static_cast(button), + static_cast(x), + static_cast(y), + Babylon::Integrations::CoordinateUnits::Logical); + } +#else + (void)button; (void)x; (void)y; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"mouseDown:atX:y: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; +#endif +} + +- (void)mouseUp:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + if (_view) + { + _view->OnMouseUp(static_cast(button), + static_cast(x), + static_cast(y), + Babylon::Integrations::CoordinateUnits::Logical); + } +#else + (void)button; (void)x; (void)y; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"mouseUp:atX:y: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; +#endif +} + +- (void)mouseMoveAtX:(CGFloat)x y:(CGFloat)y +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + if (_view) + { + _view->OnMouseMove(static_cast(x), + static_cast(y), + Babylon::Integrations::CoordinateUnits::Logical); + } +#else + (void)x; (void)y; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"mouseMoveAtX:y: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; +#endif +} + +- (void)mouseWheel:(NSInteger)axis delta:(NSInteger)delta +{ +#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT + if (_view) + { + _view->OnMouseWheel(static_cast(axis), + static_cast(delta)); + } +#else + (void)axis; (void)delta; + @throw [NSException + exceptionWithName:@"BabylonNativePluginNotEnabledException" + reason:@"mouseWheel:delta: was called but BABYLON_NATIVE_PLUGIN_NATIVEINPUT was not enabled at native build time." + userInfo:nil]; +#endif +} + ++ (NSInteger)leftMouseButton +{ + return static_cast(Babylon::Integrations::View::LeftMouseButton()); +} + ++ (NSInteger)middleMouseButton +{ + return static_cast(Babylon::Integrations::View::MiddleMouseButton()); +} + ++ (NSInteger)rightMouseButton +{ + return static_cast(Babylon::Integrations::View::RightMouseButton()); +} + ++ (NSInteger)mouseWheelY +{ + return static_cast(Babylon::Integrations::View::MouseWheelY()); +} + @end diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h index 11ca1b322..ef74ee882 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h +++ b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h @@ -83,6 +83,37 @@ NS_ASSUME_NONNULL_BEGIN - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(pointerUp(id:x:y:)); +/// Forward a mouse-button event. `button` is one of `+leftMouseButton`, +/// `+middleMouseButton`, `+rightMouseButton`. `x`, `y` are logical +/// (CSS) pixels; AppKit hosts pass `NSEvent.locationInWindow` with the +/// Y axis flipped to a top-left origin. +/// +/// Raises `BabylonNativePluginNotEnabledException` when +/// `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` is not enabled. Same applies to +/// `mouseUp:`, `mouseMove:`, and `mouseWheel:`. +- (void)mouseDown:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y + NS_SWIFT_NAME(mouseDown(button:x:y:)); + +- (void)mouseUp:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y + NS_SWIFT_NAME(mouseUp(button:x:y:)); + +- (void)mouseMoveAtX:(CGFloat)x y:(CGFloat)y + NS_SWIFT_NAME(mouseMove(x:y:)); + +/// Forward a scroll-wheel event. `axis` is `+mouseWheelY`. `delta` is +/// the signed scroll amount; AppKit hosts pass `-NSEvent.deltaY` so +/// scroll-up matches Babylon's negative convention. +- (void)mouseWheel:(NSInteger)axis delta:(NSInteger)delta + NS_SWIFT_NAME(mouseWheel(axis:delta:)); + +/// Button identifiers accepted by `mouseDown:` and `mouseUp:`. +@property (class, nonatomic, readonly) NSInteger leftMouseButton; +@property (class, nonatomic, readonly) NSInteger middleMouseButton; +@property (class, nonatomic, readonly) NSInteger rightMouseButton; + +/// Wheel axis identifier accepted by `mouseWheel:delta:`. +@property (class, nonatomic, readonly) NSInteger mouseWheelY; + - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; From bbdcb29132c54ecdfd93040bb9c77980506e095b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 12:45:52 -0700 Subject: [PATCH 47/71] Initial resize in BNView --- Integrations/Apple/Source/BNView.mm | 43 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index fa26bbd30..b677c613f 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -44,12 +44,18 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view CAMetalLayer* layer = (CAMetalLayer*)view.layer; // View construction is lightweight (just stashes the layer). Device - // construction is driven later by `-resizeWithWidth:height:`, - // which BNViewDelegate forwards from MTKView's - // `mtkView:drawableSizeWillChange:`. MTKView fires that before - // its first draw, so bootstrap is automatic. Hosts driving - // MTKView in unusual ways can call `-resizeWithWidth:height:` - // directly with the surface's pixel size. + // construction is driven by the first `-resizeWithWidth:height:` + // call, which BNViewDelegate forwards from MTKView's + // `mtkView:drawableSizeWillChange:`. UIKit's view lifecycle + // reliably fires that callback during initial layout, but AppKit + // is more lazy: on macOS the callback typically does NOT fire + // for the initial drawable size if the delegate was installed + // after the size was already determined — only for subsequent + // changes. To keep both platforms working out of the box, we + // query the current drawable size below and kick off an explicit + // resize. If the drawable isn't sized yet (e.g. MTKView not yet + // in a window), the early-out below skips it and the delegate + // path will pick it up later. @try { _view.emplace(*nativeRuntime, (__bridge CA::MetalLayer*)layer); @@ -69,6 +75,31 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view _managedDelegate = [[BNViewDelegate alloc] initWithView:self]; view.delegate = _managedDelegate; } + + // Kick off the first resize using the MTKView's bounds in + // logical units (points/DIPs). `View::Resize` handles the DPR + // conversion internally; passing Logical here means we don't + // have to query the layer's drawableSize (which can be zero + // before MTKView's first layout pass) or recompute the same + // bounds × scale conversion ourselves. + // + // If bounds are also zero (e.g. MTKView not yet in a window), + // skip and rely on the delegate path + // (`mtkView:drawableSizeWillChange:`) to deliver the first + // size once layout happens. `View::Resize` past the first + // call is just an idempotent `Device::UpdateSize`, so the + // explicit kick here composes harmlessly with any subsequent + // delegate callback. + const CGSize bounds = view.bounds.size; + if (bounds.width > 0 && bounds.height > 0) + { + if (_view) + { + _view->Resize(static_cast(bounds.width), + static_cast(bounds.height), + Babylon::Integrations::CoordinateUnits::Logical); + } + } } return self; } From dcac7ae64ca0ad437afea6f3e049d22ad04b5647 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 12:49:30 -0700 Subject: [PATCH 48/71] Initial resize in Android interop layer --- .../src/main/cpp/BabylonNativeIntegrations.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp index ce302d40e..39e9e5667 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp @@ -439,6 +439,24 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( ANativeWindow_release(window); return 0; } + + // Kick off the first resize using the surface's current pixel + // dimensions. Hosts will typically also wire their SurfaceHolder + // callback's `surfaceChanged(holder, format, w, h)` to `viewResize` + // for subsequent size changes — this initial call composes cleanly + // with that because `View::Resize` past the first call is an + // idempotent `Device::UpdateSize`. If the surface reports zero + // (rare; surface not fully realized yet), skip and rely on the + // host's `surfaceChanged` to deliver the first size. + const int32_t surfaceWidth = ANativeWindow_getWidth(window); + const int32_t surfaceHeight = ANativeWindow_getHeight(window); + if (surfaceWidth > 0 && surfaceHeight > 0) + { + view->Resize(static_cast(surfaceWidth), + static_cast(surfaceHeight), + Babylon::Integrations::CoordinateUnits::Physical); + } + // bgfx retains its own reference on the ANativeWindow for the // surface-binding lifetime, so release our local acquire here. ANativeWindow_release(window); From dfd97619219091381ede7b1fd7e9011b8baf36f6 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 13:35:18 -0700 Subject: [PATCH 49/71] Port VisionOS Playground --- Apps/Playground/CMakeLists.txt | 10 +- Apps/Playground/visionOS/App.swift | 161 +++++++++++------- Apps/Playground/visionOS/LibNativeBridge.h | 24 --- Apps/Playground/visionOS/LibNativeBridge.mm | 93 ---------- .../visionOS/Playground-Bridging-Header.h | 11 ++ 5 files changed, 113 insertions(+), 186 deletions(-) delete mode 100644 Apps/Playground/visionOS/LibNativeBridge.h delete mode 100644 Apps/Playground/visionOS/LibNativeBridge.mm create mode 100644 Apps/Playground/visionOS/Playground-Bridging-Header.h diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index bc2d55183..58b706ebe 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -53,10 +53,14 @@ if(APPLE) set(PLIST_FILE "${CMAKE_CURRENT_LIST_DIR}/visionOS/Info.plist") set(RESOURCE_FILES ${SCRIPTS}) + # NativeXr is consumed transitively through `BabylonNativeIntegrations` + # (the Apple interop static lib); no need to link it directly. + set(ADDITIONAL_LIBRARIES PRIVATE z BabylonNativeIntegrations) set(SOURCES ${SOURCES} "visionOS/App.swift" - "visionOS/LibNativeBridge.mm" - "visionOS/LibNativeBridge.h" + "visionOS/Playground-Bridging-Header.h" + "AppleShared/PlaygroundBootstrap.h" + "AppleShared/PlaygroundBootstrap.mm" "AppleShared/GestureRecognizer.swift") set_source_files_properties(${SCRIPTS} ${BABYLON_SCRIPTS} ${DEPENDENCIES} PROPERTIES MACOSX_PACKAGE_LOCATION "Scripts") set_source_files_properties(${REFERENCE_IMAGES} PROPERTIES MACOSX_PACKAGE_LOCATION "ReferenceImages") @@ -222,7 +226,7 @@ if(APPLE) XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.BabylonNative.Playground.visionOS" XCODE_ATTRIBUTE_SWIFT_VERSION "5.0" - XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_LIST_DIR}/visionOS/LibNativeBridge.h" + XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_LIST_DIR}/visionOS/Playground-Bridging-Header.h" XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks" XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) $(SDKROOT)$(SYSTEM_LIBRARY_DIR)/Frameworks" XCODE_ATTRIBUTE_ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES YES diff --git a/Apps/Playground/visionOS/App.swift b/Apps/Playground/visionOS/App.swift index 53607edf1..940943234 100644 --- a/Apps/Playground/visionOS/App.swift +++ b/Apps/Playground/visionOS/App.swift @@ -1,77 +1,106 @@ +// App.swift — visionOS Playground entry point. +// +// Built on the Babylon::Integrations Apple interop layer (BNRuntime / +// BNView), the same one the iOS Playground uses. visionOS differs +// from iOS in two small ways: +// 1. SwiftUI app lifecycle (`@main App`) instead of +// `UIApplicationDelegate` — we own the BNRuntime as a top-level +// `@StateObject` and observe `scenePhase` for suspend/resume. +// 2. No storyboards. `BabylonView` is a `UIViewRepresentable` +// wrapping an `MTKView`; BNView auto-installs its render delegate. + import SwiftUI +import MetalKit + +/// Owns the `BNRuntime` for the app's lifetime. Constructed once on +/// app launch (so the JS engine + thread start immediately) and torn +/// down with the app. Bootstrap scripts and the playground experience +/// are queued here; they run after the first BNView attach inside +/// `BabylonView` completes engine initialization on the JS thread. +@MainActor +final class BabylonRuntime: ObservableObject { + let bnRuntime: BNRuntime -class MetalView: UIView { - override init(frame: CGRect) { - super.init(frame: frame) - self.backgroundColor = .clear - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupMetalLayer() { - guard let bridge = LibNativeBridge.sharedInstance() else { return } - - if bridge.metalLayer != nil { - return + init() { + let options = BNRuntimeOptions() + options.enableDebugger = true + options.enableDebugTrace = true + bnRuntime = BNRuntime(options: options) + + PlaygroundBootstrap.loadScripts(bnRuntime) + bnRuntime.loadScript("app:///Scripts/experience.js") } - - self.addGestureRecognizer( - UIBabylonGestureRecognizer( - target: self, - onTouchDown: bridge.setTouchDown, - onTouchMove: bridge.setTouchMove, - onTouchUp: bridge.setTouchUp - ) - ) - metalLayer.pixelFormat = .bgra8Unorm - metalLayer.framebufferOnly = true - - bridge.metalLayer = self.metalLayer - - let scale = UITraitCollection.current.displayScale - bridge.initialize(withWidth: Int(self.bounds.width * scale), height: Int(self.bounds.height * scale)) - } - - var metalLayer: CAMetalLayer { - return layer as! CAMetalLayer - } - - override class var layerClass: AnyClass { - return CAMetalLayer.self - } - - override func layoutSubviews() { - super.layoutSubviews() - setupMetalLayer() - updateDrawableSize() - } - - private func updateDrawableSize() { - let scale = UITraitCollection.current.displayScale - LibNativeBridge.sharedInstance().drawableWillChangeSize(withWidth: Int(bounds.width * scale), height: Int(bounds.height * scale)) - metalLayer.drawableSize = CGSize(width: bounds.width * scale, height: bounds.height * scale) - } } -struct MetalViewRepresentable: UIViewRepresentable { - typealias UIViewType = MetalView - - func makeUIView(context: Context) -> MetalView { - MetalView(frame: .zero) - } - - func updateUIView(_ uiView: MetalView, context: Context) {} -} +/// SwiftUI wrapper around the `MTKView` + `BNView` pair. BNView's +/// auto-installed `BNViewDelegate` drives per-frame render and resize +/// callbacks; we just have to keep BNView alive for the lifetime of +/// the MTKView (held in the Coordinator). +struct BabylonView: UIViewRepresentable { + let runtime: BNRuntime + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + func makeUIView(context: Context) -> MTKView { + let mtkView = MTKView() + + // First BNView attach on this runtime triggers GPU device + // construction + plugin initialization on the JS thread + + // queued-script flush. + context.coordinator.bnView = BNView(runtime: runtime, view: mtkView) + // Simple gesture recognizer: forwards touches to BNView. Same + // wiring as the iOS Playground. + let coordinator = context.coordinator + let recognizer = UIBabylonGestureRecognizer( + target: coordinator, + onTouchDown: { [weak coordinator] (id, x, y) in + coordinator?.bnView?.pointerDown(id: Int(id), x: CGFloat(x), y: CGFloat(y)) + }, + onTouchMove: { [weak coordinator] (id, x, y) in + coordinator?.bnView?.pointerMove(id: Int(id), x: CGFloat(x), y: CGFloat(y)) + }, + onTouchUp: { [weak coordinator] (id, x, y) in + coordinator?.bnView?.pointerUp(id: Int(id), x: CGFloat(x), y: CGFloat(y)) + } + ) + mtkView.addGestureRecognizer(recognizer) + + return mtkView + } + + func updateUIView(_ uiView: MTKView, context: Context) {} + + /// Holds the BNView strongly so it outlives `makeUIView`'s scope. + class Coordinator { + var bnView: BNView? + } +} @main struct ExampleApp: App { - var body: some Scene { - WindowGroup { - MetalViewRepresentable() - .frame(maxWidth: .infinity, maxHeight: .infinity) + @StateObject private var runtime = BabylonRuntime() + @Environment(\.scenePhase) private var scenePhase + + var body: some Scene { + WindowGroup { + BabylonView(runtime: runtime.bnRuntime) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onChange(of: scenePhase) { _, newPhase in + // BNRuntime suspend/resume is refcounted, so this + // composes safely with any other code that may + // suspend the runtime (e.g. modal dialogs). + switch newPhase { + case .active: + runtime.bnRuntime.resume() + case .inactive, .background: + runtime.bnRuntime.suspend() + @unknown default: + break + } + } + } } - } } diff --git a/Apps/Playground/visionOS/LibNativeBridge.h b/Apps/Playground/visionOS/LibNativeBridge.h deleted file mode 100644 index 0489a7068..000000000 --- a/Apps/Playground/visionOS/LibNativeBridge.h +++ /dev/null @@ -1,24 +0,0 @@ -#import -#import - -@class CAMetalLayer; - -@interface LibNativeBridge : NSObject - -@property (nonatomic, assign, getter=isInitialized) BOOL initialized; -@property (nonatomic, strong) CAMetalLayer *metalLayer; - -+ (instancetype)sharedInstance; - -- (void)setTouchDown:(int)pointerId x:(int)inX y:(int)inY; -- (void)setTouchMove:(int)pointerId x:(int)inX y:(int)inY; -- (void)setTouchUp:(int)pointerId x:(int)inX y:(int)inY; - -- (void)drawableWillChangeSizeWithWidth:(NSInteger)width height:(NSInteger)height; - -- (bool)initializeWithWidth:(NSInteger)width height:(NSInteger)height; -- (void)shutdown; -- (void)render; - -@end - diff --git a/Apps/Playground/visionOS/LibNativeBridge.mm b/Apps/Playground/visionOS/LibNativeBridge.mm deleted file mode 100644 index 78d4e7f87..000000000 --- a/Apps/Playground/visionOS/LibNativeBridge.mm +++ /dev/null @@ -1,93 +0,0 @@ -#import "LibNativeBridge.h" -#import - -@implementation LibNativeBridge { - std::optional _appContext; - CADisplayLink *_displayLink; -} - -+ (instancetype)sharedInstance { - static LibNativeBridge *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - -- (bool)initializeWithWidth:(NSInteger)width height:(NSInteger)height { - if (self.initialized) { - return YES; - } - - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)]; - [_displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode]; - - _appContext.emplace( - (__bridge CA::MetalLayer*)self.metalLayer, - static_cast(width), - static_cast(height), - [](const char* message) { NSLog(@"%s", message); }); - - _appContext->ScriptLoader().LoadScript("app:///Scripts/experience.js"); - - self.initialized = YES; - return true; -} - -- (void)drawableWillChangeSizeWithWidth:(NSInteger)width height:(NSInteger)height { - if (_appContext) { - _appContext->DeviceUpdate().Finish(); - _appContext->Device().FinishRenderingCurrentFrame(); - - _appContext->Device().UpdateSize(static_cast(width), static_cast(height)); - - _appContext->Device().StartRenderingCurrentFrame(); - _appContext->DeviceUpdate().Start(); - } -} - -- (void)setTouchDown:(int)pointerId x:(int)inX y:(int)inY { - if (_appContext && _appContext->Input()) { - _appContext->Input()->TouchDown(static_cast(pointerId), static_cast(inX), static_cast(inY)); - } -} - -- (void)setTouchMove:(int)pointerId x:(int)inX y:(int)inY { - if (_appContext && _appContext->Input()) { - _appContext->Input()->TouchMove(static_cast(pointerId), static_cast(inX), static_cast(inY)); - } -} - -- (void)setTouchUp:(int)pointerId x:(int)inX y:(int)inY { - if (_appContext && _appContext->Input()) { - _appContext->Input()->TouchUp(static_cast(pointerId), static_cast(inX), static_cast(inY)); - } -} - -- (void)render { - if (_appContext && self.initialized) { - _appContext->DeviceUpdate().Finish(); - _appContext->Device().FinishRenderingCurrentFrame(); - _appContext->Device().StartRenderingCurrentFrame(); - _appContext->DeviceUpdate().Start(); - } -} - -- (void)shutdown { - if (!self.initialized) { - return; - } - - _appContext.reset(); - - [_displayLink invalidate]; - _displayLink = NULL; - self.initialized = NO; -} - -- (void)dealloc { - [self shutdown]; -} - -@end diff --git a/Apps/Playground/visionOS/Playground-Bridging-Header.h b/Apps/Playground/visionOS/Playground-Bridging-Header.h new file mode 100644 index 000000000..7d18eb7ec --- /dev/null +++ b/Apps/Playground/visionOS/Playground-Bridging-Header.h @@ -0,0 +1,11 @@ +// Swift-Obj-C++ bridging header for the visionOS Playground app. +// +// Exposes both the Babylon::Integrations Apple interop layer +// (`BNRuntime`, `BNView`) and the Playground-specific helper +// (`PlaygroundBootstrap`) to the Swift sources. + +#pragma once + +#import + +#import "AppleShared/PlaygroundBootstrap.h" From b8772020358920e600da539153c0d65ff31f17dd Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 14:16:05 -0700 Subject: [PATCH 50/71] Port X11 Playground --- Apps/Playground/X11/App.cpp | 83 ++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/Apps/Playground/X11/App.cpp b/Apps/Playground/X11/App.cpp index 5205add33..b97a1b0b9 100644 --- a/Apps/Playground/X11/App.cpp +++ b/Apps/Playground/X11/App.cpp @@ -5,58 +5,70 @@ #include #include // syscall #undef None -#include +#include #include #include -#include -#include +#include +#include #include +#include +#include static const char* s_applicationName = "BabylonNative Playground"; static const char* s_applicationClass = "Playground"; namespace { - std::optional g_appContext{}; + std::optional g_runtime; + std::optional g_view; void Uninitialize() { - g_appContext.reset(); + // View first (unbinds surface, closes in-flight frame), then + // Runtime (joins JS thread). + g_view.reset(); + g_runtime.reset(); } - void InitBabylon(Window window, int width, int height, int argc, const char* const* argv) + void InitBabylon(Window window, int argc, const char* const* argv) { Uninitialize(); - g_appContext.emplace( - window, - static_cast(width), - static_cast(height), - [](const char* message) { - std::cout << message << std::endl; - }); + Babylon::Integrations::RuntimeOptions runtimeOptions{}; + runtimeOptions.log = [](Babylon::Integrations::LogLevel, std::string_view message) { + std::cout << message << std::endl; + }; + + g_runtime.emplace(std::move(runtimeOptions)); + Playground::Initialize(); + Playground::LoadBootstrapScripts(*g_runtime); if (argc == 1) { - g_appContext->ScriptLoader().LoadScript("app:///Scripts/experience.js"); + g_runtime->LoadScript("app:///Scripts/experience.js"); } else { for (int i = 1; i < argc; ++i) { - g_appContext->ScriptLoader().LoadScript(argv[i]); + g_runtime->LoadScript(argv[i]); } - g_appContext->ScriptLoader().LoadScript("app:///Scripts/playground_runner.js"); + g_runtime->LoadScript("app:///Scripts/playground_runner.js"); } + + // First View attach triggers Device construction, plugin init, and + // flushes the queued scripts. + g_view.emplace(*g_runtime, window); } - void UpdateWindowSize(float width, float height) + void UpdateWindowSize(uint32_t width, uint32_t height) { - if (g_appContext) + if (g_view) { - g_appContext->Device().UpdateSize(width, height); + // X11 reports surface dimensions in physical pixels. + g_view->Resize(width, height, Babylon::Integrations::CoordinateUnits::Physical); } } } @@ -131,18 +143,15 @@ int main(int _argc, const char* const* _argv) , NULL ); - InitBabylon(window, width, height, _argc, _argv); + InitBabylon(window, _argc, _argv); UpdateWindowSize(width, height); bool exit{}; while (!exit) { - if (!XPending(display) && g_appContext) + if (!XPending(display) && g_view) { - g_appContext->DeviceUpdate().Finish(); - g_appContext->Device().FinishRenderingCurrentFrame(); - g_appContext->Device().StartRenderingCurrentFrame(); - g_appContext->DeviceUpdate().Start(); + g_view->RenderFrame(); } else { @@ -171,22 +180,22 @@ int main(int _argc, const char* const* _argv) const XMotionEvent& xmotion = event.xmotion; const XButtonEvent& xbutton = event.xbutton; - if (g_appContext && g_appContext->Input()) { + if (g_view) { switch (xbutton.button) { case Button1: - g_appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, xmotion.x, xmotion.y); + g_view->OnMouseDown(Babylon::Integrations::View::LeftMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); break; case Button2: - g_appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, xmotion.x, xmotion.y); + g_view->OnMouseDown(Babylon::Integrations::View::MiddleMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); break; case Button3: - g_appContext->Input()->MouseDown(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, xmotion.x, xmotion.y); + g_view->OnMouseDown(Babylon::Integrations::View::RightMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); break; case Button4: - g_appContext->Input()->MouseWheel(Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID, -120); + g_view->OnMouseWheel(Babylon::Integrations::View::MouseWheelY(), -120); break; case Button5: - g_appContext->Input()->MouseWheel(Babylon::Plugins::NativeInput::MOUSEWHEEL_Y_ID, 120); + g_view->OnMouseWheel(Babylon::Integrations::View::MouseWheelY(), 120); break; } } @@ -197,18 +206,18 @@ int main(int _argc, const char* const* _argv) const XMotionEvent& xmotion = event.xmotion; const XButtonEvent& xbutton = event.xbutton; - if (g_appContext && g_appContext->Input()) + if (g_view) { switch (xbutton.button) { case Button1: - g_appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::LEFT_MOUSE_BUTTON_ID, xmotion.x, xmotion.y); + g_view->OnMouseUp(Babylon::Integrations::View::LeftMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); break; case Button2: - g_appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::MIDDLE_MOUSE_BUTTON_ID, xmotion.x, xmotion.y); + g_view->OnMouseUp(Babylon::Integrations::View::MiddleMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); break; case Button3: - g_appContext->Input()->MouseUp(Babylon::Plugins::NativeInput::RIGHT_MOUSE_BUTTON_ID, xmotion.x, xmotion.y); + g_view->OnMouseUp(Babylon::Integrations::View::RightMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); break; } } @@ -217,8 +226,8 @@ int main(int _argc, const char* const* _argv) case MotionNotify: { const XMotionEvent& xmotion = event.xmotion; - if (g_appContext && g_appContext->Input()) { - g_appContext->Input()->MouseMove(xmotion.x, xmotion.y); + if (g_view) { + g_view->OnMouseMove(xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); } } break; From 756d5f3686db6aa74118d565beff5ce252ded864 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 15:22:25 -0700 Subject: [PATCH 51/71] PR feedback --- Integrations/Apple/Source/BNView.mm | 4 ++-- .../Include/Shared/Babylon/Integrations/Runtime.h | 8 +++++--- Integrations/Source/Runtime.cpp | 10 +++++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Integrations/Apple/Source/BNView.mm b/Integrations/Apple/Source/BNView.mm index b677c613f..e16a7402b 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Integrations/Apple/Source/BNView.mm @@ -56,11 +56,11 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view // resize. If the drawable isn't sized yet (e.g. MTKView not yet // in a window), the early-out below skips it and the delegate // path will pick it up later. - @try + try { _view.emplace(*nativeRuntime, (__bridge CA::MetalLayer*)layer); } - @catch (NSException*) + catch (const std::exception&) { _mtkView = nil; return nil; diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index c9e1607ac..2ffd31e8d 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -32,9 +32,11 @@ namespace Babylon::Integrations ~Runtime(); - // Non-copyable; movable. Cross-references between Runtime and View - // point at the heap-allocated pimpls, so moves of the outer wrappers - // are safe and don't invalidate any back-pointers. + // Non-copyable; movable. Move-construction transfers the impl + // pointer, so any attached View keeps its back-reference valid. + // Move-assignment and destruction destroy the destination's + // existing impl; both share the same contract enforced by + // `~RuntimeImpl`: the destination must have no View attached. Runtime(const Runtime&) = delete; Runtime& operator=(const Runtime&) = delete; Runtime(Runtime&&) noexcept; diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index 52e9fa226..c92d1a383 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -410,7 +410,15 @@ namespace Babylon::Integrations m_impl->m_xrWindow = nativeWindow; if (m_impl->m_nativeXr) { - m_impl->m_nativeXr->UpdateWindow(nativeWindow); + // NativeXr's entry points are JS-thread-only; dispatch the + // actual UpdateWindow call instead of touching it from + // whatever thread invoked SetXrWindow. + m_impl->m_appRuntime->Dispatch([implPtr = m_impl.get(), nativeWindow](Napi::Env) { + if (implPtr->m_nativeXr) + { + implPtr->m_nativeXr->UpdateWindow(nativeWindow); + } + }); } // If NativeXr isn't initialized yet (no View attach has happened), // the value is applied by the first-attach init lambda. From 21863ae423221470d76cdaf039934544c0113cd3 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 15:33:14 -0700 Subject: [PATCH 52/71] PR feedback --- .../Include/Shared/Babylon/Integrations/Runtime.h | 8 +++++++- Integrations/Source/Runtime.cpp | 13 +++++++++++-- Integrations/Source/RuntimeImpl.h | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 2ffd31e8d..1667cb78f 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -23,6 +23,11 @@ namespace Babylon::Integrations public: explicit Runtime(RuntimeOptions options = {}); + // `noexcept(false)`: throws `std::runtime_error` if a View is + // still attached. Same contract for `~Runtime` and move-assign; + // see the move comment below. + ~Runtime() noexcept(false); + // // Future construction mode: adopt a host-owned Babylon::JsRuntime // // (e.g. React Native with Hermes/JSC + CallInvoker). In Attach mode // // `~Runtime` does NOT tear down the JS engine, and Suspend/Resume @@ -36,7 +41,8 @@ namespace Babylon::Integrations // pointer, so any attached View keeps its back-reference valid. // Move-assignment and destruction destroy the destination's // existing impl; both share the same contract enforced by - // `~RuntimeImpl`: the destination must have no View attached. + // `~RuntimeImpl`: the destination must have no View attached, + // or the process terminates with a diagnostic message. Runtime(const Runtime&) = delete; Runtime& operator=(const Runtime&) = delete; Runtime(Runtime&&) noexcept; diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index c92d1a383..c2aa56822 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -54,6 +54,7 @@ #endif #include +#include #include #include #include @@ -116,10 +117,18 @@ namespace Babylon::Integrations m_scriptLoader.emplace(*m_appRuntime); } - RuntimeImpl::~RuntimeImpl() + RuntimeImpl::~RuntimeImpl() noexcept(false) { // Host owns the ordering: destroy Views before their Runtime. - assert(m_currentView == nullptr && "View must be destroyed before its Runtime."); + // Throwing from a destructor is normally avoided, but `~Runtime` + // is implicitly `noexcept`, so this turns into a deterministic + // `std::terminate` with a clear message in both debug and + // release — strictly better than a debug-only assert that + // silently UBs in release. + if (m_currentView != nullptr) + { + throw std::runtime_error{"View must be destroyed before its Runtime."}; + } // Teardown order: // 1. SaveShaderCache: ~ViewImpl already ran ViewImpl::Suspend, so diff --git a/Integrations/Source/RuntimeImpl.h b/Integrations/Source/RuntimeImpl.h index f4023ffed..ad27ccf60 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Integrations/Source/RuntimeImpl.h @@ -33,7 +33,7 @@ namespace Babylon::Integrations struct RuntimeImpl { explicit RuntimeImpl(RuntimeOptions options); - ~RuntimeImpl(); + ~RuntimeImpl() noexcept(false); RuntimeImpl(const RuntimeImpl&) = delete; RuntimeImpl& operator=(const RuntimeImpl&) = delete; From 47e9684c01212f7368917a6dffa07c3a03cbf751 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 15:41:08 -0700 Subject: [PATCH 53/71] noexcept for destruction --- .../Shared/Babylon/Integrations/Runtime.h | 17 +++++++---------- Integrations/Source/Runtime.cpp | 12 +++++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h index 1667cb78f..860515e9f 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Integrations/Include/Shared/Babylon/Integrations/Runtime.h @@ -23,11 +23,6 @@ namespace Babylon::Integrations public: explicit Runtime(RuntimeOptions options = {}); - // `noexcept(false)`: throws `std::runtime_error` if a View is - // still attached. Same contract for `~Runtime` and move-assign; - // see the move comment below. - ~Runtime() noexcept(false); - // // Future construction mode: adopt a host-owned Babylon::JsRuntime // // (e.g. React Native with Hermes/JSC + CallInvoker). In Attach mode // // `~Runtime` does NOT tear down the JS engine, and Suspend/Resume @@ -35,18 +30,20 @@ namespace Babylon::Integrations // static Runtime Adopt(Babylon::JsRuntime& jsRuntime, // RuntimeOptions options = {}); - ~Runtime(); + // `noexcept(false)`: throws `std::runtime_error` if a View is still + // attached. Same contract for move-assignment (see below). + ~Runtime() noexcept(false); // Non-copyable; movable. Move-construction transfers the impl // pointer, so any attached View keeps its back-reference valid. // Move-assignment and destruction destroy the destination's - // existing impl; both share the same contract enforced by - // `~RuntimeImpl`: the destination must have no View attached, - // or the process terminates with a diagnostic message. + // existing impl; both share the same precondition enforced by + // `~RuntimeImpl`: the destination must have no View attached, or + // a `std::runtime_error` is thrown. Runtime(const Runtime&) = delete; Runtime& operator=(const Runtime&) = delete; Runtime(Runtime&&) noexcept; - Runtime& operator=(Runtime&&) noexcept; + Runtime& operator=(Runtime&&) noexcept(false); // ----- JS interaction ----- // diff --git a/Integrations/Source/Runtime.cpp b/Integrations/Source/Runtime.cpp index c2aa56822..05d7f947c 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Integrations/Source/Runtime.cpp @@ -120,11 +120,9 @@ namespace Babylon::Integrations RuntimeImpl::~RuntimeImpl() noexcept(false) { // Host owns the ordering: destroy Views before their Runtime. - // Throwing from a destructor is normally avoided, but `~Runtime` - // is implicitly `noexcept`, so this turns into a deterministic - // `std::terminate` with a clear message in both debug and - // release — strictly better than a debug-only assert that - // silently UBs in release. + // Both `~Runtime` and `Runtime::operator=(Runtime&&)` are + // declared `noexcept(false)` so this propagates to the caller + // instead of triggering `std::terminate`. if (m_currentView != nullptr) { throw std::runtime_error{"View must be destroyed before its Runtime."}; @@ -316,9 +314,9 @@ namespace Babylon::Integrations { } - Runtime::~Runtime() = default; + Runtime::~Runtime() noexcept(false) = default; Runtime::Runtime(Runtime&&) noexcept = default; - Runtime& Runtime::operator=(Runtime&&) noexcept = default; + Runtime& Runtime::operator=(Runtime&&) noexcept(false) = default; void Runtime::LoadScript(std::string_view url) { From 86f0b1875656e23f7741d54ba327e97b6135663f Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 15:56:15 -0700 Subject: [PATCH 54/71] Delete plan --- SimplifiedAPI.md | 1499 ---------------------------------------------- 1 file changed, 1499 deletions(-) delete mode 100644 SimplifiedAPI.md diff --git a/SimplifiedAPI.md b/SimplifiedAPI.md deleted file mode 100644 index f1d4e8729..000000000 --- a/SimplifiedAPI.md +++ /dev/null @@ -1,1499 +0,0 @@ -# Babylon Native — Simplified Integration API Plan - -## 1. Problem - -Integrating Babylon Native into a host app today requires understanding and -hand-wiring a large number of internal components. The canonical setup -(see `Apps/Playground/Shared/AppContext.cpp`) requires the consumer to: - -- Create and configure `Babylon::Graphics::Device` + `DeviceUpdate`, drive - `StartRenderingCurrentFrame` / `FinishRenderingCurrentFrame` from the right - thread. -- Create a `Babylon::AppRuntime`, then `Dispatch` a lambda onto its JS thread - to call **~10+** `Initialize` functions (`Console`, `Window`, - `XMLHttpRequest`, `Canvas`, `Performance`, `Blob`, `TextDecoder`, - `NativeEngine`, `NativeInput`, `NativeXr`, `NativeCamera`, - `NativeCapture`, `NativeEncoding`, `NativeOptimizations`, - `NativeTracing`, `ShaderCache`, `TestUtils`, …) in the correct order. -- Use `ScriptLoader` to load `babylon.max.js`, loaders, materials, GUI, - serializers, ammo, recast, etc., in a specific order. -- Plumb window/surface handles, resize events, and input events from each - platform's native windowing system into `Graphics::Configuration` and - `NativeInput` respectively. -- Repeat all of this in per-platform glue: `Apps/Playground/Win32/App.cpp`, - `Apps/Playground/iOS/LibNativeBridge.mm`, - `Apps/Playground/Android/BabylonNative/...`, `macOS/`, `X11/`, `UWP/`, - `visionOS/`. - -There is no "single-call" path for the common case: *show a Babylon scene -in this view*. Even trivial integrations require ~150 lines of C++. - -## 2. Goal - -Provide a small, stable, opinionated API that lets a host app embed Babylon -Native in a handful of calls, while keeping the existing low-level API -intact for advanced users. - -The deliverable is two layers shipped in the repo: - -1. **A shared C++ Integrations layer** (`Babylon::Integrations`) — a thin facade - over the existing components that exposes a `Runtime` + `View` C++ - API. -2. **Per-platform interop layers** that bridge each platform's native - inter-language ABI (JNI on Android, Objective-C(++) on Apple, plain - C++ on Windows / Linux) to the shared Integrations layer. They are built - in C++ but expose entry points that are **directly callable from the - platform's UI language without a generic FFI generator**, exactly - the way `Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp` - exposes JNI entry points to Kotlin today. - -Non-goals: - -- Replacing or deprecating the existing component-level API. -- Rewriting any rendering, scripting, or platform code. The simplified API - is a *facade* over current components. -- Changing the JavaScript-facing contract (Babylon.js code keeps working). -- **Shipping precompiled "everything" binaries.** Babylon Native stays - source-distributed via CMake. Hosts must be able to disable any plugin - they don't use (`NativeXr`, `NativeCamera`, `NativeCapture`, …) at - configure time so unused features add zero binary size. -- **No language-specific high-level wrappers.** We do not ship a Kotlin - class library, a Swift package with idiomatic Swift wrapper types, a - managed .NET assembly, or a Rust safe-wrapper crate. The per-platform - interop layer's job ends at "Kotlin/Swift/etc. can call into native - Babylon code"; designing idiomatic high-level wrappers in each host - language is left to the consumer. -- **No generic C ABI / FFI surface.** With per-platform interop layers, - each platform talks to the Integrations layer via its own native mechanism - (JNI, Objective-C runtime, direct C++). A separate flat C ABI would - duplicate the surface for no consumer. - -## 3. Design Principles - -1. **One library, two layers.** Keep `Babylon::*` (low level) untouched. - Add `Babylon::Integrations::*` as a thin C++ facade. -2. **Sensible defaults, escape hatches.** Default config matches the - Playground's setup. Every default is overridable. -3. **No N-API in the public surface of the Integrations layer.** If a - consumer wants JS interop, they get an opaque escape hatch - (`RunOnJsThread`) but they don't have to learn Napi to embed a scene. -4. **Per-platform interop layers, not a generic FFI.** Each platform - ships a tiny C++ interop module that uses the platform's native - inter-language ABI (JNI, Objective-C, COM/WinRT where appropriate) - to expose `Runtime` + `View` operations to the host UI language. The - shape of the entry points is adapted to the platform — JNI methods - on Android, Objective-C classes on Apple, plain C++ on Windows — so - the host writes the minimum amount of glue code in their UI - language. -5. **Two distinct lifetimes: Runtime vs. View.** The Babylon Native - runtime (JS engine, loaded scripts, scene state, GPU device) is - long-lived and typically scoped to the host *application* or - *process*. Views/surfaces are transient — they come and go as the - host navigates between screens, backgrounds/foregrounds the app on - mobile, or detaches a rendering surface (e.g., Android - `surfaceDestroyed`/`surfaceCreated`, iOS background/foreground, - Win32 window recreation). The API must let a single `Runtime` - outlive any number of `View` attachments without tearing down JS - state, reloading scripts, or losing the scene. This is already - supported by the underlying `Graphics::Device` via `UpdateWindow`, - `EnableRendering`/`DisableRendering`, and - `StartRenderingCurrentFrame`/`FinishRenderingCurrentFrame` - (`Core/Graphics/Include/Shared/Babylon/Graphics/Device.h:114-134`); - the simplified facade exposes that capability cleanly. -6. **Lifecycle is explicit and symmetric.** Runtime: `create → … → - destroy`. View: `attach → resize / input → detach`. Either side may - be repeated: one runtime, many sequential view attachments. -7. **Host owns the frame source.** The library does not subscribe to - any vsync / display-link / choreographer source. The host calls - `View::RenderFrame()` from whatever draw callback its UI framework - already provides for the view/control that hosts the rendering - surface. See §4.1 *How frames actually get rendered*. -8. **C++ OO at the shared layer; per-platform handle-ification at the - interop layers.** the Integrations layer is C++-object-oriented (`Runtime` - and `View` classes with RAII, `std::function`, `std::string_view`). - We do **not** flatten it to a C ABI: - - Every interop layer's host language speaks C++ natively (JNI - files compile as C++, Obj-C uses `.mm`, C++/WinRT is C++), so the - interop modules call C++ methods directly. - - Win32 / Linux hosts consume the C++ API directly with no interop - layer in between — RAII is the natural shape for those. - - A flat C ABI would only buy something if we had a generic FFI - consumer or shipped a precompiled binary, both of which are - explicit non-goals. - Each interop layer handles its own conversion to opaque handles - where the host language requires them (JNI uses `jlong`; Obj-C - stores the C++ object in an Obj-C instance ivar). -9. **Conditional API surface mirrors plugin flags.** When a plugin or - polyfill is disabled at configure time, the corresponding methods - are removed from the public header — not silently no-opped. See - §4.4. - -## 4. Proposed Public Surface - -### 4.1 Shared C++ facade — `Babylon::Integrations` - -The facade splits along the lifetime boundary: a long-lived `Runtime` -that owns JS state and the GPU device, and a transient `View` that -binds a platform surface to the runtime for as long as that surface -exists. - -```cpp -namespace Babylon::Integrations -{ - struct RuntimeOptions { - uint32_t msaaSamples = 4; - bool enableDebugger = false; - bool enableShaderCache = true; - - // Which feature bundles to wire up. Defaults match Playground. - struct Features { - bool input = true; - bool xr = false; - bool camera = false; - bool capture = false; - } features; - - std::function log; - std::function onUnhandledError; - - // If non-empty, the GPU shader cache is loaded from this path - // on first `View::Attach` and saved back on `Suspend` and - // `~Runtime`. Pass a per-app writable directory file (e.g. - // Android `Context.getCacheDir()/babylon.shadercache`). - std::string shaderCachePath; - }; - - // Long-lived: typically created once per app/process. Sets up the - // AppRuntime (JS thread + Napi env), JsRuntime, and non-GPU - // polyfills/plugins. Construction is cheap and synchronous — - // no GPU device exists yet. Device construction and GPU plugin - // initialization (NativeEngine, etc.) are deferred to the first - // `View::Attach` call. - class Runtime { - public: - static std::unique_ptr Create(RuntimeOptions = {}); - - // // Future construction mode — adopt a host-owned Babylon::JsRuntime - // // instead of letting Runtime construct its own AppRuntime+JsRuntime. - // // Intended for hosts that already own a JS engine and want - // // Babylon Native plugins to live inside it: React Native (the - // // host already owns Hermes/JSC + a CallInvoker dispatcher), - // // future custom V8/QuickJS embedders, etc. The Integrations - // // layer never sees JSI directly — only Babylon::JsRuntime, - // // which the host wires up against whatever JS engine they have. - // // - // // In Attach mode `~Runtime` does NOT tear down the JS engine - // // (the host owns it); Suspend/Resume only DisableRendering on - // // the Device since the JS thread isn't ours to pause. Same - // // instance API as Create-mode otherwise — same LoadScript, - // // RunOnJsThread, View::Attach semantics. See "Construction - // // modes" below. - // static std::unique_ptr Attach(Babylon::JsRuntime& jsRuntime, - // RuntimeOptions = {}); - - // JS interaction — safe to call regardless of view/suspend state. - // LoadScript: calls made before the first `View::Attach` are - // queued internally and dispatched onto the JS thread after - // engine initialization completes during that first Attach. - // Calls made after the first Attach are dispatched immediately. - // (Most existing integrations — Playground's `AppContext`, both - // bridges — already gate their own LoadScript on engine init by - // hand; this just formalizes the same ordering.) - void LoadScript(std::string_view url); // file://, http(s)://, app:// - void Eval(std::string_view source, std::string_view sourceUrl = {}); - void RunOnJsThread(std::function); // escape hatch - - // Suspend/Resume — orthogonal to view attachment. Use when the host - // app is backgrounded, throttled, or otherwise should not be doing - // work (iOS applicationWillResignActive, Android onPause, power - // saving, modal dialogs). While suspended: - // - JS timers (setTimeout/setInterval) pause. - // - In-flight microtasks complete; no new tasks are dispatched. - // - Any attached View becomes a no-op for RenderFrame() — the - // host can keep calling it from its draw callback; nothing - // happens until Resume(). - // Calls are reference-counted: nesting is safe. - void Suspend(); - void Resume(); - bool IsSuspended() const; - - ~Runtime(); // detaches any current view implicitly - - private: - friend class View; - }; - - // Transient: created when a host surface appears, destroyed when it - // goes away. Multiple sequential Views may be attached to the same - // Runtime over its lifetime. At most one View may be attached at a - // time. - class View { - public: - // Attaches `handle` to `runtime`. - // - // First Attach on a given Runtime is the heavy step: it - // constructs `Graphics::Device` against `handle`, dispatches - // GPU plugin initialization (`Device::AddToJavaScript`, - // `NativeEngine::Initialize`, `NativeInput::CreateForJavaScript`, - // …), and flushes any scripts queued via `Runtime::LoadScript` - // before this point. Opens the first frame. - // - // Subsequent Attach calls on the same Runtime are cheap: the - // Device is already constructed, plugins are initialized, the - // JS engine is running. They just call `Device::UpdateWindow` + - // `Device::EnableRendering` to bind the new surface, then open - // the first frame for the new attachment. - // - // Detach (~View) closes the in-flight frame and calls - // `Device::DisableRendering`. The Device persists on the - // Runtime, so the next Attach is fast. - static std::unique_ptr Attach(Runtime& runtime, - Babylon::Graphics::WindowT nativeWindow); - - // Render exactly one frame. Must be called from the same thread - // each time (the "frame thread"). No-op if the runtime is - // suspended. The host calls this from the platform view/control's - // existing draw callback — see "How frames actually get rendered". - void RenderFrame(); - - // Resize the bound surface. Width/height are in **logical - // pixels**; `dpr` is the physical-to-logical ratio (e.g. 2.0 - // for a Retina display). The platform interop layer is - // responsible for converting whatever its UI framework - // provides (Android `View.onSizeChanged` is in physical - // pixels; iOS `MTKViewDelegate.drawableSizeWillChange:` is in - // physical pixels; SwiftUI / AppKit hand you points; etc.) - // into this convention — see §4.2 "Pixel units". - void Resize(uint32_t width, uint32_t height, float dpr = 1.0f); - - // Input — host calls these from its event loop while the view - // exists. Safe to call from any thread; routed to the JS thread - // via NativeInput. - void OnPointerDown(int32_t pointerId, float x, float y); - void OnPointerMove(int32_t pointerId, float x, float y); - void OnPointerUp (int32_t pointerId, float x, float y); - void OnKey(int32_t keyCode, bool down); - - ~View(); - }; -} -``` - -#### Construction modes (forward-compatibility note) - -The `Runtime::Attach` factory above is reserved for a future addition; -it is shown commented-out in the header so the API shape leaves room -for it without breaking changes when added. - -The split is along a single axis — *who owns the JS runtime*: - -- **`Create` (standalone)** — the Integrations layer builds its own - `Babylon::AppRuntime` (JS thread + Napi env). Used by every host - that doesn't already have a JS engine: Win32, Android Activities - hosting Babylon directly, iOS apps with `BNRuntime`, etc. -- **`Attach` (embedded, future)** — the host has already wired up a - `Babylon::JsRuntime` (the existing public class at - `Core/JsRuntime/Include/Babylon/JsRuntime.h`) against their JS - engine and dispatcher and hands it in. Used by hosts whose - framework already owns the JS runtime: React Native (Hermes/JSC + - `CallInvoker` dispatcher) is the concrete consumer; this is - exactly what `BabylonReactNative`'s `BabylonNative.cpp:50` does - today via `Babylon::JsRuntime::CreateForJavaScript(env, dispatcher)`. - -Both factories produce a `Runtime` in the same "JS engine ready, -GPU not yet constructed" state. Past construction, the instance API -(`LoadScript`, `Eval`, `RunOnJsThread`, `View::Attach`, `RenderFrame`, -…) is identical — `LoadScript` and `Eval` work in Attach mode because -the Integrations layer's `ScriptLoader` accepts any object with a -`Dispatch(std::function)` method (see -`Core/ScriptLoader/Include/Babylon/ScriptLoader.h:19-23`), and both -`AppRuntime` (Create mode) and `JsRuntime` (Attach mode) qualify. - -A few semantic differences worth flagging at impl time: - -- **`LoadScript` / `Eval` are usually unused in Attach mode.** RN-style - hosts already own script loading via their framework's bundler - (Metro / Webpack imports `@babylonjs/core` and the experience code - as ES modules). The methods are still callable — they'd fetch via - XHR and execute via `env.RunScript` — but only make sense if the - host wants to load a UMD bundle outside its own module system. The - primary integration point in Attach mode is `RunOnJsThread`, which - is exactly the `JsRuntime::Dispatch` pattern BRN already uses - (`BabylonReactNative` `BabylonNative.cpp:50,107`). -- **`~Runtime`** — Create mode tears down the owned `AppRuntime`, - joining its JS thread. Attach mode does *not* tear down the host's - JS engine; it cancels any pending `LoadScript`/`Eval`/`RunOnJsThread` - lambdas, then `Napi::Detach`es. Hosts that re-create the runtime - across a JS-engine reload (e.g. RN dev-mode bridge invalidation — - `BabylonReactNative` listens for `RCTBridgeWillInvalidateModulesNotification` - and calls `Deinitialize` to release its `weak_ptr`) get a clean - destroy-then-recreate path. -- **`Suspend`/`Resume`** — Create mode pauses the JS thread via - `AppRuntime::Suspend`. Attach mode can only `DisableRendering` on - the `Device` since the JS thread isn't owned by us; the host's - framework controls JS-thread pause/resume. - -Adding `Attach` later is purely additive — one new static factory -taking an existing public Babylon Native type. No breaking change, -no surface duplication. - -#### Switching surfaces - -Whenever the host wants to render against a different platform -surface — including the swap from a hidden pre-warm window to the -user-visible one, the swap from one Activity's `SurfaceView` to -another after configuration changes, or any other surface change — -the pattern is the same: - -1. Destroy the current `View` (`view.reset()` / `~View()`). -2. Construct a new `View::Attach(runtime, newWindow)`. - -The Runtime's underlying `Graphics::Device` (constructed during the -*first* Attach and persisted on the Runtime thereafter) calls -`UpdateWindow` internally on subsequent Attaches. JS state, plugins, -loaded scripts, and scene state are all preserved across the swap. - -This is the only mechanism the Integrations layer provides for -changing surfaces. There is no `View::SwapTo`, no `Runtime::SetWindow`, -and no implicit "current surface" the host needs to track — there's -just construct a `View` to bind a surface and destroy it to unbind. - -#### Starting the engine before the user-visible UI exists (host pattern) - -Some hosts want the scene to be ready the moment the user navigates -to the rendering screen — they want script load, plugin init, scene -construction, texture/mesh upload, and shader compilation to have -already happened *before* the user-visible UI attaches. This is what -`babylon-native-bridge`'s `BabylonNativeBridge::start:` (iOS) and -`BabylonNativeBridge.preload(...)` (Android) do today. - -The Integrations layer does not bake this in as a feature — it falls -out naturally from the *Switching surfaces* model: - -1. At app start, host calls `Runtime::Create()`. This is cheap; no - GPU device exists yet. -2. Host calls `Runtime::LoadScript("app:///bundle.js")` etc. The - scripts are queued internally pending the first `View::Attach`. -3. When the host wants to start scene construction (typically still - before the user-visible UI is ready), host allocates a small - hidden window — iOS `[CAMetalLayer layer]` with `isHidden = YES`; - Android off-screen `SurfaceView`; Win32 `WS_EX_TOOLWINDOW` HWND — - and calls `View::Attach(runtime, hiddenWindow)`. This is the - first Attach, so it constructs the `Graphics::Device`, dispatches - GPU plugin init, flushes the queued scripts. The JS thread is now - running scene construction against the hidden surface. -4. Eventually the user-visible UI's surface becomes ready. Host - destroys the hidden-surface View, constructs a new - `View::Attach(runtime, realWindow)`. `Device::UpdateWindow` swaps - the surface; scene state is preserved. - -A host that doesn't care about pre-loading just calls `Runtime::Create()` -and then `View::Attach(runtime, realWindow)` whenever the real -surface is ready, with `LoadScript` calls anywhere in between. The -Integrations layer doesn't know which path the host took. - -#### Loading Babylon.js: two supported routes - -the Integrations layer is agnostic about how Babylon.js gets into the JS -runtime — `LoadScript` is the only mechanism. Two patterns are -first-class: - -1. **Pre-bundled (recommended for new integrations).** The host - bundles `@babylonjs/core` (and any of `@babylonjs/loaders`, - `materials`, `gui`, `serializers`, `havok`, etc.) into a single - ES-module-or-IIFE bundle using webpack / vite / esbuild / rollup, - then loads it with one call: - - ```cpp - runtime->LoadScript("app:///bundle.js"); - runtime->LoadScript("app:///experience.js"); // user's scene code - ``` - -2. **Multi-UMD (Playground-style).** The host loads each `babylon.*.js` - UMD bundle individually, in dependency order. This matches what - `Apps/Playground/Shared/AppContext.cpp` does today and is useful - for projects that drop in stock UMD builds without a bundler: - - ```cpp - runtime->LoadScript("app:///babylon.max.js"); - runtime->LoadScript("app:///babylonjs.loaders.js"); - runtime->LoadScript("app:///babylonjs.materials.js"); - runtime->LoadScript("app:///babylonjs.gui.js"); - runtime->LoadScript("app:///babylonjs.serializers.js"); - runtime->LoadScript("app:///experience.js"); - ``` - -`LoadScript` calls are queued onto the JS thread in submission order, -so this is sufficient — no separate `bootstrapScripts` option is -needed. The library does not preload anything by default. - -#### How frames actually get rendered - -Today the host is responsible for the per-frame -`FinishRenderingCurrentFrame()` / `StartRenderingCurrentFrame()` pair, -driven from a platform-specific source. The simplified API keeps that -responsibility on the host but collapses the per-frame work to a -single call: `view->RenderFrame()`. - -The library deliberately does **not** subscribe to a vsync source, -`CADisplayLink`, `Choreographer`, `CompositionTarget::Rendering`, or -anything similar. Every UI framework already gives the view/control -that hosts the rendering surface a natural draw callback — the host -wires `RenderFrame()` to that and is done. - -Examples of the "natural draw callback" per platform: - -| Platform | Where the host calls `RenderFrame()` | -|-----------------|----------------------------------------------------------------------------| -| Win32 | `WM_PAINT` handler on the rendering window | -| UWP | `SwapChainPanel`'s composition / rendering callback | -| macOS | `NSView::drawRect:` or an `MTKViewDelegate::drawInMTKView:` | -| iOS / visionOS | `UIView::drawRect:` or `MTKViewDelegate::drawInMTKView:` | -| Android | A custom `View`'s `onDraw(Canvas)` or `SurfaceView`'s render thread loop | -| X11 | `Expose` event handler | -| Wayland | `wl_surface::frame` callback (set up by the host's UI framework) | - -In all cases this is glue the host already writes to integrate any -custom rendering with its UI framework — Babylon Native does not add -new requirements. - -##### What `RenderFrame()` does - -```cpp -void View::RenderFrame() -{ - if (m_runtime.IsSuspended()) return; // pause cleanly, no GPU work - - m_device.FinishRenderingCurrentFrame(); // submit the in-flight frame - m_device.StartRenderingCurrentFrame(); // open the next one - // Babylon's JS render loop (requestAnimationFrame / scene.render) - // runs between Start and Finish, scheduled via DeviceUpdate onto - // the JS thread. RenderFrame does not call into JS directly — - // DeviceUpdate already coordinates Napi dispatch, bgfx command - // recording, and present. -} -``` - -##### Threading - -Two threads are in play: - -| Thread | Owner / lifetime | What runs there | -|------------------|---------------------------------------------------|-----------------------------------------------------------------| -| **JS thread** | `AppRuntime`'s `WorkQueue`, lives with `Runtime` | All Napi/JS execution (`scene.render()`, timers, XHR callbacks) | -| **Frame thread** | The thread the host calls `RenderFrame()` from | `Device::Start/FinishRenderingCurrentFrame` and bgfx submission | - -bgfx is configured to *not* create its own render thread -(`Core/Graphics/Source/DeviceImpl.cpp:205`), so the "render thread" -is simply whichever thread the host calls `RenderFrame()` from — -consistently. The contract is: - -- **Call `View::Attach`, `View::RenderFrame`, `View::Resize`, and - `View::~View` from the same thread** (the frame thread — usually - the UI thread, since draw callbacks fire on the UI thread). -- **Input methods (`OnPointerDown`, etc.) may be called from any - thread**; they post to the JS thread via `NativeInput`. -- The JS thread is invisible to the host; `RunOnJsThread` is the - only way to reach it. - -Cross-thread coordination between the frame thread and the JS thread -is handled entirely by the existing `DeviceUpdate` + -`SafeTimespanGuarantor` machinery -(`Core/Graphics/InternalInclude/Babylon/Graphics/SafeTimespanGuarantor.h`). -The simplified API does not reinvent it. - -#### Lifetime and state relationship - -``` -Runtime ────────────────────────────────────────────────► destroy - │ │ │ │ -View attachments ├── attach1 ─────┤ ├── attach2 ┤ - (window 1) │ (window 2) │ - │ │ -Suspend state running ─┬─ suspended ──┴── running ──┬─ suspended ─ running - │ │ - (app backgrounded) (modal dialog) -``` - -Three independent axes: -- **Runtime lifetime** — JS engine, scene, device. One per app/process. -- **View attachment** — *where* to render. Zero or one at a time, may - cycle freely. -- **Suspend state** — *whether* to do work. Independent of view; an - app can be suspended with or without a view attached. - -The `Runtime` keeps the JS engine, scene graph, loaded assets, and -`Graphics::Device` alive across detach/attach cycles. Re-attaching a -new `View` calls `Device::UpdateWindow` + `EnableRendering` and -resumes rendering without re-running any JS. - -Internally `Runtime` owns the `Graphics::Device`, `DeviceUpdate`, -`AppRuntime`, `ScriptLoader`, `Canvas`, and `NativeInput*`. -`View::Attach` performs the equivalent of `Device::UpdateWindow` + -`Device::EnableRendering` + `DeviceUpdate::Start` + -`Device::StartRenderingCurrentFrame`; `~View` performs the symmetric -`FinishRenderingCurrentFrame` + `DeviceUpdate::Finish` + -`DisableRendering` so rendering cleanly pauses while no surface -exists. - -### 4.2 Per-platform interop layers - -Each platform ships a tiny **C++ interop module** that uses the -platform's native inter-language ABI to expose `Runtime` + `View` to -the host UI language. These modules are the analog of today's -`Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp`, -generalized into reusable building blocks under `Integrations//`. - -The interop module **always includes** whatever small native-side -shim the platform's interop ABI fundamentally requires (e.g., a Java -class declaring `external fun` declarations on Android, an Objective-C -`@interface` header on Apple). It **does not** include idiomatic -high-level wrappers in the host language (no Kotlin "BabylonView" -class with Compose helpers, no Swift `BabylonScene` value type). -Designing those is the consumer's job. - -| Platform | Interop ABI | Interop module shape | Native-side host shim | -|------------------|----------------------------|-----------------------------------------------------------------------|------------------------------------------------------------------| -| Android | JNI | `extern "C" JNIEXPORT` functions on a single JNI class | Minimal Java/Kotlin class declaring `external fun` entry points | -| iOS / macOS / visionOS | Objective-C runtime | Obj-C++ (`.mm`) files exposing `@interface BNRuntime/BNView` | Obj-C `@interface` headers (auto-imported from Swift) | -| UWP | WinRT | C++/WinRT `runtimeclass`es | `.idl` for projection generation | -| Win32 | Plain C++ | Direct `Babylon::Integrations::*` (no interop layer needed) | None | -| Linux (X11/Wayland) | Plain C++ | Direct `Babylon::Integrations::*` (no interop layer needed) | None | - -The interop module's surface mirrors `Runtime` and `View` 1:1, with -one entry point per public method. It is intentionally as thin as the -JNI file we have today. - -#### Interop layer responsibilities - -Beyond the 1:1 mirroring of `Runtime` and `View`, each interop layer -owns two platform adaptations on the host's behalf so the host's -UI-language code stays as simple as possible: - -1. **Translate platform-native objects/units to the cross-platform C++ - contract — but no conversion math.** Per-platform helpers in - `Integrations/Source/ViewImpl_*.cpp` (mirroring the existing - `Core/Graphics/Source/DeviceImpl_*.cpp` pattern) handle the platform - facts so the interop layer can stay a pure ABI bridge: - - **Querying the surface's pixel-buffer size from the native window - handle** — `ANativeWindow_getWidth/Height` on Android, `GetClientRect` - on Win32, `CAMetalLayer.drawableSize` on Apple, `XGetGeometry` on - X11, `Bounds × scale` on UWP. `View::Attach(runtime, nativeWindow)` - does this internally — no host-supplied dimensions, no pixel-unit - bookkeeping crossing the JNI / Obj-C++ boundary. - - **Converting native pointer-event coordinates to logical (CSS) - pixels.** Babylon.js consumes pointer events as - `PointerEvent.clientX/clientY` (CSS pixels). On platforms whose - native event system is already in logical units (iOS `UITouch`, - macOS `NSEvent`, UWP `PointerPoint`), `View`'s per-platform - `ToLogicalCoords` is a passthrough; on platforms that deliver - physical pixels (Android `MotionEvent`, Win32 `WM_POINTER*`, X11 - button events), it divides by the Device's queried - device-pixel-ratio. Hosts pass coordinates from their native event - directly; the View handles the conversion. - - Resize is the one exception: hosts already have the new dimensions - from their resize event, so `View::Resize(w, h)` takes them - explicitly rather than re-querying the window — same convention as - `Babylon::Graphics::Device::UpdateSize`. -2. **Expose platform-specific lifecycle entries that don't belong on - the cross-platform API.** Examples from `babylon-native-bridge`: - - Android: `setCurrentActivity(Activity)` → - `android::global::SetCurrentActivity(...)`, - `activityOnRequestPermissionsResult(...)` → - `android::global::RequestPermissionsResult(...)`. These hook - into `AndroidExtensions/Globals.h` and are required by plugins - like `NativeCamera` that need to call back into the Java side. - - iOS: hooks for `applicationWillTerminate` and similar that map - to interop-layer cleanup. - - These live on the interop layer's own surface, *not* on - `Babylon::Integrations::Runtime` or `View`. The cross-platform layer - stays free of `#ifdef`s; per-platform concerns live where they - belong. - -The interop layer does **not** auto-allocate a hidden initial -surface, set rendering policy, or otherwise opt the host into a -particular lifecycle pattern. The host always provides the surface -it wants the runtime to use; the interop layer just forwards it. - -#### Distribution model: source + opt-in CMake subprojects - -**Babylon Native is not distributed as a precompiled binary.** Source-based -CMake remains the only distribution model — that's what allows hosts to -exclude unused plugins (`NativeXr`, `NativeCamera`, `NativeCapture`, -`NativeEncoding`, `ShaderCache`, etc.) and keep their final binary small. -A precompiled `babylon_native.dll/.so/.dylib` would force every host to -link every plugin. - -The Integrations layer is just **additional CMake targets that the host -opts into**, layered on top of the existing component targets: - -``` - Babylon::Graphics, Babylon::AppRuntime, Babylon::ScriptLoader, - Babylon::Polyfills::*, Babylon::Plugins::* (existing — unchanged) - │ - ▼ - Babylon::Integrations (new — shared C++ facade) - │ - ┌───────────────┼───────────────┬───────────────┐ - ▼ ▼ ▼ ▼ - Babylon::Integrations::Android ::Apple ::Uwp … - (new, optional) (new, optional) (new, optional) -``` - -Each new target is its own CMake subdirectory under `Integrations/`, -gated by a CMake option that defaults to OFF (except the cross-platform -facade itself, which defaults to ON because it's lightweight and useful -for any host). Platforms that don't need an interop layer (Win32, -Linux) consume `Babylon::Integrations` directly. - -| CMake option | Default | Builds | -|---------------------------------------|---------|-------------------------------------------------| -| `BABYLON_NATIVE_INTEGRATIONS` | ON | `Babylon::Integrations` (shared C++ facade) | -| `BABYLON_NATIVE_INTEGRATIONS_ANDROID` | OFF | JNI interop `.so` + companion Java sources | -| `BABYLON_NATIVE_INTEGRATIONS_APPLE` | OFF | Obj-C++ interop static lib + Obj-C headers | -| `BABYLON_NATIVE_INTEGRATIONS_UWP` | OFF | C++/WinRT runtimeclass DLL + `.winmd` | - -Plugin selection remains opt-in/out via CMake options that the -Integrations layer respects: - -| CMake option | Default | Effect | -|-------------------------------------------|---------|-------------------------------------------------| -| `BABYLON_NATIVE_PLUGIN_NATIVEENGINE` | ON | Required for rendering | -| `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` | ON | Required for `View::OnPointer*` / `OnKey` | -| `BABYLON_NATIVE_PLUGIN_NATIVEXR` | OFF | XR session support | -| `BABYLON_NATIVE_PLUGIN_NATIVECAMERA` | OFF | Webcam capture | -| `BABYLON_NATIVE_PLUGIN_NATIVECAPTURE` | OFF | Frame/region capture | -| `BABYLON_NATIVE_PLUGIN_NATIVEENCODING` | OFF | Image encoding | -| `BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS` | OFF | Babylon.js perf hooks | -| `BABYLON_NATIVE_PLUGIN_NATIVETRACING` | OFF | ETW / signpost tracing | -| `BABYLON_NATIVE_PLUGIN_SHADERCACHE` | ON | Disk shader cache | -| `BABYLON_NATIVE_POLYFILL_XHR` | ON | `XMLHttpRequest` | -| `BABYLON_NATIVE_POLYFILL_CANVAS` | ON | 2D canvas | - -`Babylon::Integrations` reads these flags (via `target_compile_definitions` -or `if(TARGET ...)` checks) and only wires up the corresponding plugin -in its setup function — so disabling `BABYLON_NATIVE_PLUGIN_NATIVEXR` -removes both the `NativeXr` library from the link line *and* the -`Babylon::Plugins::NativeXr::Initialize(env)` call from the runtime -boot, with no runtime overhead. - -The interop layers depend on `Babylon::Integrations` (which depends on -whichever plugins are enabled), so each interop artifact contains only -the code paths that were actually compiled in. - -##### Conditional API surface - -Plugin/polyfill flags don't just remove implementations — they remove -the **public API surface** that depends on them, so misuse is a -compile error in the host's language rather than a silent runtime -no-op. - -Each plugin-gated method in `Babylon::Integrations` is wrapped in -`#if BABYLON_NATIVE_PLUGIN_` (defined by the build via -`target_compile_definitions` when the corresponding CMake option is -ON). The interop layers mirror the same gating on their own entry -points: when a plugin is disabled the JNI export, the Obj-C method, -and the C++/WinRT method all disappear together. - -| CMake option | Public surface gated by it | -|-------------------------------------------|---------------------------------------------------------------------------------------------| -| `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` | `View::OnPointerDown` / `OnPointerMove` / `OnPointerUp` / `OnKey` and their interop entries | -| `BABYLON_NATIVE_PLUGIN_NATIVEXR` | XR-specific extension methods (e.g. `View::SetXrSessionStateChangedCallback`) | -| `BABYLON_NATIVE_PLUGIN_NATIVECAMERA` | Camera permission helpers, if any are added at the Integrations layer | -| `BABYLON_NATIVE_PLUGIN_NATIVECAPTURE` | `Runtime::CaptureFrame` (if exposed at the Integrations layer) | - -Method-level subset of the C++ header, illustrating the pattern: - -```cpp -class View { -public: - static std::unique_ptr Attach(Runtime&, Babylon::Graphics::WindowT); - void RenderFrame(); - void Resize(uint32_t w, uint32_t h); - -#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT - void OnPointerDown(int32_t pointerId, float x, float y); - void OnPointerMove(int32_t pointerId, float x, float y); - void OnPointerUp (int32_t pointerId, float x, float y); - void OnKey(int32_t keyCode, bool down); -#endif - - ~View(); -}; -``` - -And the matching Android JNI gating: - -```cpp -#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT -extern "C" JNIEXPORT void JNICALL -Java_com_babylonjs_native_BabylonNative_viewPointerDown(JNIEnv*, jclass, jlong h, jint id, jfloat x, jfloat y) { - reinterpret_cast(h)->OnPointerDown(id, x, y); -} -// ...PointerMove, PointerUp, Key likewise. -#endif -``` - -The companion Kotlin shim under `Integrations/Android/src/main/java/` is -generated at configure time (or hand-maintained in matched halves) so -that the `external fun` declarations only appear when the -corresponding native entries do. Same approach for the Apple `.h` and -the UWP `.idl`. - -`RunOnJsThread` is gated by `BABYLON_NATIVE_EXPOSE_NAPI` (default ON, -since the escape hatch is harmless if unused) so a host that wants a -strictly N-API-free header surface can opt out. - -##### How the host consumes each interop layer - -- **Android.** `Integrations/Android/` contains a CMakeLists.txt that - builds the JNI interop `.so` and a `src/main/java/...` directory - with the companion Java/Kotlin shim class declaring `external fun` - entry points. The host's existing `app/build.gradle` references the - CMakeLists via `externalNativeBuild { cmake { path "..." } }` (the - standard Android NDK + gradle integration) and adds the Java sources - to its `sourceSets`. No AAR is produced by us; the host's existing - gradle build emits whatever artifact it already emits. -- **Apple.** `Integrations/Apple/` is consumed via CMake by the host's - Xcode project (or an `add_subdirectory` from the host's CMakeLists). - It produces a static library and a set of public Objective-C - headers that Swift code imports via the standard Swift–Obj-C bridge. -- **UWP.** `Integrations/Uwp/` produces a C++/WinRT runtime component DLL - and `.winmd`. The host's C# / C++/WinRT project references it - directly. -- **Win32 / Linux.** No interop layer; the host C++ uses - `Babylon::Integrations` directly. - -This keeps the source-build, opt-in-plugins model intact end-to-end: -no precompiled "everything" artifacts, and the host's build still -fully controls what code ends up in the final binary. - -## 5. Examples - -### Win32 (was ~250 LOC, becomes ~35) — direct C++ - -```cpp -#include -#include - -// Process-scoped: created once at startup, destroyed at shutdown. -static std::unique_ptr g_runtime; -// Window-scoped: created on WM_CREATE, destroyed on WM_DESTROY. -static std::unique_ptr g_view; - -LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) { - switch (msg) { - case WM_CREATE: { - RECT r; GetClientRect(hwnd, &r); - // First Attach: constructs Device, runs GPU plugin init, flushes - // queued LoadScript calls. Subsequent re-Attaches against this - // runtime are cheap. - g_view = Babylon::Integrations::View::Attach(*g_runtime, - { hwnd, (uint32_t)r.right, (uint32_t)r.bottom }); - return 0; - } - case WM_SIZE: if (g_view) g_view->Resize(LOWORD(l), HIWORD(l)); return 0; - case WM_PAINT: if (g_view) g_view->RenderFrame(); // natural draw callback - ValidateRect(hwnd, nullptr); return 0; - case WM_DESTROY: g_view.reset(); return 0; // runtime stays alive - } - return DefWindowProc(hwnd, msg, w, l); -} - -int WINAPI wWinMain(...) { - g_runtime = Babylon::Integrations::Runtime::Create(); - g_runtime->LoadScript("app:///experience.js"); // queued; flushed - // on first View::Attach - - MSG msg; - while (GetMessage(&msg, nullptr, 0, 0) > 0) { - TranslateMessage(&msg); - DispatchMessage(&msg); - if (g_view) InvalidateRect(msg.hwnd, nullptr, FALSE); // request next paint - } - g_runtime.reset(); -} -``` - -> The host wires `RenderFrame()` to `WM_PAINT` and triggers it via -> `InvalidateRect` from its message loop. Babylon Native does not -> subscribe to any DXGI / DWM source itself. - -**Pre-warm variant** — host wants the engine warm against a hidden -surface before the user-visible window opens. Allocate a hidden HWND -at app start, attach a View to it (this is what triggers Device -construction + GPU plugin init + script execution), and later destroy -it and re-attach against the real HWND when its `WM_CREATE` fires. - -```cpp -static std::unique_ptr g_prewarmView; -static HWND s_prewarmHwnd = nullptr; - -int WINAPI wWinMain(...) { - g_runtime = Babylon::Integrations::Runtime::Create(); - g_runtime->LoadScript("app:///experience.js"); - - s_prewarmHwnd = CreateWindowEx(WS_EX_TOOLWINDOW, ..., HWND_MESSAGE, ...); - g_prewarmView = Babylon::Integrations::View::Attach(*g_runtime, - { s_prewarmHwnd, 16, 16, 1.0f }); - // Engine is now initializing + scene is building against hidden surface. - - // ...real window's WM_CREATE will g_prewarmView.reset() then - // View::Attach to its HWND. Device::UpdateWindow swaps to the - // real HWND under the hood; scene state preserved. - - MSG msg; while (GetMessage(&msg, nullptr, 0, 0) > 0) { /* same as above */ } - g_runtime.reset(); - DestroyWindow(s_prewarmHwnd); -} -``` - -### Android — JNI interop layer + minimal Kotlin shim - -The library ships the JNI `.cpp` and a companion Kotlin class. The -host writes a custom `View` (or `SurfaceView`) subclass and calls -`renderFrame()` from its draw callback. - -The interop layer translates Android's physical-pixel + -`displayMetrics.density` pair into the C++ logical-pixel + DPR -convention (see §4.2). It does not allocate or own surfaces — the -host always supplies them. - -**Library-supplied JNI interop** (lives in `Integrations/Android/src/main/cpp/`): - -```cpp -extern "C" { - -JNIEXPORT jlong JNICALL -Java_com_babylonjs_native_BabylonNative_runtimeCreate(JNIEnv* env, jclass) { - // unique_ptr::release() returns the raw pointer and gives up - // ownership *without* deleting; the JVM side now owns it via the - // returned jlong handle and must call runtimeDestroy() to free it. - auto* rt = Babylon::Integrations::Runtime::Create().release(); - return reinterpret_cast(rt); -} - -JNIEXPORT void JNICALL -Java_com_babylonjs_native_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong h) { - delete reinterpret_cast(h); -} - -JNIEXPORT void JNICALL -Java_com_babylonjs_native_BabylonNative_runtimeLoadScript(JNIEnv* env, jclass, jlong h, jstring url) { - const char* s = env->GetStringUTFChars(url, nullptr); - reinterpret_cast(h)->LoadScript(s); - env->ReleaseStringUTFChars(url, s); -} - -JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_runtimeSuspend(JNIEnv*, jclass, jlong h) -{ reinterpret_cast(h)->Suspend(); } -JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_runtimeResume(JNIEnv*, jclass, jlong h) -{ reinterpret_cast(h)->Resume(); } - -// viewAttach: physical pixels in, logical pixels through to C++. -JNIEXPORT jlong JNICALL -Java_com_babylonjs_native_BabylonNative_viewAttach(JNIEnv* env, jclass, jlong rt, - jobject surface, - jint physicalW, jint physicalH, - jfloat density) { - ANativeWindow* win = ANativeWindow_fromSurface(env, surface); - Babylon::Integrations::ViewDescriptor descriptor{ - win, - (uint32_t)(physicalW / density), // logical pixels for C++ - (uint32_t)(physicalH / density), - density - }; - auto* v = Babylon::Integrations::View::Attach( - *reinterpret_cast(rt), descriptor).release(); - return reinterpret_cast(v); -} - -JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_viewDetach(JNIEnv*, jclass, jlong h) -{ delete reinterpret_cast(h); } -JNIEXPORT void JNICALL Java_com_babylonjs_native_BabylonNative_viewRenderFrame(JNIEnv*, jclass, jlong h) -{ reinterpret_cast(h)->RenderFrame(); } - -// viewResize: same conversion as viewAttach. -JNIEXPORT void JNICALL -Java_com_babylonjs_native_BabylonNative_viewResize(JNIEnv*, jclass, jlong h, - jint physicalW, jint physicalH, - jfloat density) { - reinterpret_cast(h)->Resize( - (uint32_t)(physicalW / density), - (uint32_t)(physicalH / density), - density); -} - -} -``` - -**Library-supplied Kotlin shim** (lives in `Integrations/Android/src/main/java/com/babylonjs/native/`): - -```kotlin -package com.babylonjs.native - -class BabylonNativeRuntime { - private val handle: Long = nativeCreate() - - init { System.loadLibrary("babylon-native-interop") } - - fun loadScript(url: String) = nativeLoadScript(handle, url) - fun suspend() = nativeSuspend(handle) - fun resume() = nativeResume(handle) - fun close() = nativeDestroy(handle) - - internal fun nativeHandle(): Long = handle - - private external fun nativeCreate(): Long - private external fun nativeDestroy(handle: Long) - private external fun nativeLoadScript(handle: Long, url: String) - private external fun nativeSuspend(handle: Long) - private external fun nativeResume(handle: Long) -} - -// view descriptor — opaque from the host's perspective. The interop layer -// converts physical-pixel dimensions to logical pixels internally. -class BabylonNativeView(runtime: BabylonNativeRuntime, surface: Surface, - physicalW: Int, physicalH: Int, density: Float) { - private val handle = nativeAttach(runtime.nativeHandle(), surface, physicalW, physicalH, density) - - fun renderFrame() = nativeRenderFrame(handle) - fun resize(physicalW: Int, physicalH: Int, density: Float) = - nativeResize(handle, physicalW, physicalH, density) - fun detach() = nativeDetach(handle) - - private external fun nativeAttach(rt: Long, s: Surface, w: Int, h: Int, d: Float): Long - private external fun nativeDetach(handle: Long) - private external fun nativeRenderFrame(handle: Long) - private external fun nativeResize(handle: Long, w: Int, h: Int, d: Float) -} -``` - -**Host code — simple integration** (consumer's app — *not* shipped by the library): - -The simplest host creates the runtime at app start and attaches a -View when its real `SurfaceView`'s surface is ready. - -```kotlin -class MyApp : Application() { - val runtime by lazy { - BabylonNativeRuntime().apply { loadScript("app:///experience.js") } - // LoadScript queues; will execute after the first View::Attach. - } -} - -class BabylonView(context: Context) : SurfaceView(context), SurfaceHolder.Callback { - private val runtime get() = (context.applicationContext as MyApp).runtime - private var view: BabylonNativeView? = null - private val density = resources.displayMetrics.density - - init { holder.addCallback(this) } - - override fun surfaceCreated(h: SurfaceHolder) { - // First Attach: triggers Device construction + GPU plugin init + - // queued LoadScript flush. Subsequent Attaches are cheap. - view = BabylonNativeView(runtime, h.surface, width, height, density) - } - override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, hh: Int) { - view?.resize(w, hh, density) // physical pixels in - } - override fun surfaceDestroyed(h: SurfaceHolder) { - view?.detach(); view = null - } - - // Natural draw callback — host wires RenderFrame() here. - override fun onDraw(canvas: Canvas) { - view?.renderFrame() - invalidate() // request next draw - } -} -``` - -**Host code — pre-loading the engine before the user-visible UI exists:** - -A host that wants the engine warm before the user navigates to the -rendering screen creates the runtime + an off-screen `SurfaceView` -(the pattern from `babylon-native-bridge`'s -`BabylonNativeManagerView.java:51`) at app start, and attaches a -View to the off-screen surface so the first Attach kicks off Device -construction + plugin init + script execution. When the user-visible -surface arrives, the host destroys that View and attaches a new one -to the real surface (Device::UpdateWindow under the hood). - -```kotlin -class MyApp : Application() { - val runtime by lazy { - BabylonNativeRuntime().apply { loadScript("app:///experience.js") } - } - - private var hiddenSurface: Surface? = null - private var prewarmView: BabylonNativeView? = null - - override fun onCreate() { - super.onCreate() - // Off-screen SurfaceView attached to a window that is never shown. - // (Detail elided; same approach as babylon-native-bridge.) - hiddenSurface = createHiddenSurface() - prewarmView = BabylonNativeView(runtime, hiddenSurface!!, 1, 1, 1.0f) - // First Attach now: engine boots, scripts run, scene builds. - } - - fun releasePrewarm() { - prewarmView?.detach(); prewarmView = null - } -} -// ...later, when the user-visible BabylonView's surface is ready, host -// calls (myApp).releasePrewarm() and constructs a new BabylonNativeView -// against the real surface. - -class MainActivity : AppCompatActivity() { - private val runtime get() = (application as MyApp).runtime - override fun onPause() { super.onPause(); runtime.suspend() } - override fun onResume() { super.onResume(); runtime.resume() } -} -``` -### iOS / macOS — Obj-C++ interop layer + minimal Obj-C headers - -**Library-supplied Obj-C++ interop** (lives in `Integrations/Apple/`): - -The interop layer takes a host-supplied `MTKView` directly. `BNView` -forces a layout pass and seeds `CAMetalLayer.drawableSize` from -`view.bounds × scale` before handing the layer to the Graphics layer -— hosts don't have to think about MTKView's lazy `autoResizeDrawable` -semantics. - -`BNView` exposes `BNViewDelegate` as a public Obj-C class. The default -behavior is "smart": if the host hasn't set `MTKView.delegate` by the -time `BNView` is constructed, `BNView` creates and strongly retains a -`BNViewDelegate` for the host. If the host has already set their own -delegate (typically a `BNViewDelegate` subclass), `BNView` leaves it -alone. - -```objc -// BNRuntime.h — public Obj-C header (Swift sees this via the bridge) -@interface BNRuntime : NSObject -- (instancetype)init; -- (void)loadScript:(NSString*)url; -- (void)setXrView:(nullable MTKView*)xrView; // runtime owns visibility toggling -- (void)suspend; -- (void)resume; -@end - -@interface BNView : NSObject -// If `view.delegate` is nil at construction time, BNView lazily -// installs and retains a BNViewDelegate that drives the per-frame -// render. If the host pre-installed a delegate, BNView leaves it -// alone. -- (nullable instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view; - -// Used by `BNViewDelegate` (or by hosts using a fully manual -// `MTKViewDelegate`) — usually you don't call these yourself. -- (void)renderFrame; -- (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h NS_SWIFT_NAME(resize(width:height:)); -@end - -// Default MTKViewDelegate implementation. Subclass to insert per-frame -// work; call super to keep the default forwarding behavior. -@interface BNViewDelegate : NSObject -- (nullable instancetype)initWithView:(BNView*)view NS_DESIGNATED_INITIALIZER; -@end -``` - -```objc++ -// BNView.mm — implementation sketch -@implementation BNView { - std::unique_ptr _v; - BNRuntime* _runtime; - MTKView* _mtkView; - BNViewDelegate* _managedDelegate; // strong; nil if host set their own -} -- (instancetype)initWithRuntime:(BNRuntime*)rt view:(MTKView*)view { - if ((self = [super init])) { - _runtime = rt; - _mtkView = view; - - // Force layout so bounds are valid, then seed drawableSize. - // MTKView's autoResizeDrawable keeps it in sync from here on. - [view layoutIfNeeded]; - const CGFloat scale = view.contentScaleFactor; - ((CAMetalLayer*)view.layer).drawableSize = - CGSizeMake(view.bounds.size.width * scale, - view.bounds.size.height * scale); - - _v = Babylon::Integrations::View::Attach(*[rt native], - (__bridge CA::MetalLayer*)view.layer); - - // Auto-install a default delegate iff the host hasn't. - if (view.delegate == nil) { - _managedDelegate = [[BNViewDelegate alloc] initWithView:self]; - view.delegate = _managedDelegate; - } - } - return self; -} -- (void)dealloc { - if (_managedDelegate != nil && _mtkView.delegate == _managedDelegate) { - _mtkView.delegate = nil; - } -} -- (void)renderFrame { - [_runtime updateXrViewIfNeeded]; // default xrView visibility policy - _v->RenderFrame(); -} -- (void)resizeWithWidth:(NSUInteger)w height:(NSUInteger)h { - _v->Resize((uint32_t)w, (uint32_t)h); -} -@end - -// BNViewDelegate.mm — default MTKViewDelegate -@implementation BNViewDelegate { __weak BNView* _view; } -- (instancetype)initWithView:(BNView*)view { - if ((self = [super init])) { _view = view; } - return self; -} -- (void)mtkView:(MTKView*)v drawableSizeWillChange:(CGSize)size { - [_view resizeWithWidth:(NSUInteger)size.width height:(NSUInteger)size.height]; -} -- (void)drawInMTKView:(MTKView*)v { - [_view renderFrame]; // BNView's renderFrame handles XR overlay -} -@end -``` - -**Host code — "super simple" integration** (the iOS Playground uses this): - -The minimal host creates the runtime at app start, loads scripts -(queued), and constructs a `BNView` against the user-visible MTKView. -That's it — because `mtkView.delegate` is `nil`, BNView auto-installs -a `BNViewDelegate` that drives the per-frame render and resize, -including keeping the XR overlay's visibility in sync. - -```swift -class AppDelegate: NSObject, UIApplicationDelegate { - let runtime: BNRuntime = { - let r = BNRuntime() - r.loadScript(Bundle.main.url(forResource: "experience", - withExtension: "js")!.absoluteString) - // LoadScript queues; flushes on first BNView attach. - return r - }() - func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } - func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } -} - -class BabylonViewController: UIViewController { - var babylonView: BNView? - override func viewDidLoad() { - let mtkView = view as! MTKView - let runtime = (UIApplication.shared.delegate as! AppDelegate).runtime - // First Attach: triggers Device + plugin init + queued script flush. - // BNView auto-installs a BNViewDelegate since mtkView.delegate is nil. - babylonView = BNView(runtime: runtime, view: mtkView) - } -} -``` - -**Host code — customize per-frame work via subclass:** - -When the host wants to do per-frame work (e.g. updating an overlay -based on runtime state), subclass `BNViewDelegate`, override the -delegate methods, call `super` to keep the default behavior, and -install the subclass on the MTKView **before** constructing BNView so -the auto-install path doesn't fire. - -```swift -class MyDelegate: BNViewDelegate { - override func draw(in v: MTKView) { - beforeWork() - super.draw(in: v) // forwards to bnView.renderFrame (xrView toggle + render) - afterWork() - } - override func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { - super.mtkView(v, drawableSizeWillChange: size) // forwards to bnView.resize - layoutOverlays(size) - } -} - -// In the view controller: -// 1. Construct BNView so it produces a BNView reference for the subclass. -// 2. Install the subclass; this overwrites any auto-installed default. -let bn = BNView(runtime: runtime, view: mtkView)! -let delegate = MyDelegate(view: bn)! // host retains; MTKView.delegate is weak -mtkView.delegate = delegate -self.viewDelegate = delegate -self.babylonView = bn -``` - -**Host code — full manual `MTKViewDelegate`:** - -Hosts that need full control can skip `BNViewDelegate` entirely, write -their own delegate, and call `BNView`'s `renderFrame` / `resize` -directly. The XR overlay visibility toggle is still automatic because -`BNView.renderFrame` calls `[runtime updateXrViewIfNeeded]` itself. - -```swift -class BabylonViewController: UIViewController, MTKViewDelegate { - var babylonView: BNView? - - override func viewDidLoad() { - let mtkView = view as! MTKView - mtkView.delegate = self // BNView will see this is set and not touch it - babylonView = BNView(runtime: runtime, view: mtkView) - } - - func draw(in v: MTKView) { babylonView?.renderFrame() } - func mtkView(_ v: MTKView, drawableSizeWillChange size: CGSize) { - babylonView?.resize(width: UInt(size.width), height: UInt(size.height)) - } -} -``` - -**Host code — pre-loading the engine before the user-visible UI exists:** - -A host that wants the engine warm at app start creates a `BNView` -against an off-screen `MTKView` in the `AppDelegate` so the first -Attach fires immediately and starts initialization + scene -construction. Later, when a view controller appears, the host destroys -that View and constructs a new `BNView` against the user-visible -view. - -```swift -class AppDelegate: NSObject, UIApplicationDelegate { - let runtime = BNRuntime() - var prewarmView: MTKView! - var prewarmBN: BNView! - - func application(_ app: UIApplication, didFinishLaunchingWithOptions - options: [UIApplication.LaunchOptionsKey : Any]?) -> Bool { - runtime.loadScript(Bundle.main.url(forResource: "experience", - withExtension: "js")!.absoluteString) - - // Off-screen MTKView large enough to satisfy Metal validation; - // bgfx's first frame renders into this surface while the real - // UI is being assembled. - prewarmView = MTKView(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) - prewarmView.isHidden = true - prewarmBN = BNView(runtime: runtime, view: prewarmView) - // First Attach fires here; engine is now booting up + scripts running. - return true - } - - func releasePrewarm() { prewarmBN = nil; prewarmView = nil } // call before binding real view - - func applicationWillResignActive(_ app: UIApplication) { runtime.suspend() } - func applicationDidBecomeActive (_ app: UIApplication) { runtime.resume() } -} -// ... later, in the view controller: -// delegate.releasePrewarm() -// babylonView = BNView(runtime: delegate.runtime, view: mtkView) -// // BNView auto-installs a BNViewDelegate on the user-visible -// // MTKView; Device::UpdateWindow swaps the underlying surface; -// // scene state and JS state are preserved. -``` - -### Suspend/Resume is reference-counted - -Multiple subsystems can request suspension independently — the runtime -only resumes when all requests have been released. This makes it safe -to combine app-lifecycle suspension with ad-hoc pauses (e.g., a modal -dialog, a power-saving mode, a long-running native operation): - -```cpp -runtime->Suspend(); // app backgrounded -> count = 1, suspended -runtime->Suspend(); // modal dialog also pauses -> count = 2, suspended -runtime->Resume(); // dialog closed -> count = 1, still suspended -runtime->Resume(); // app foregrounded -> count = 0, running -``` - -### Per-platform Activity-lifecycle wiring - -Some platforms have a well-defined process-wide "the app is going to -the background" signal; some don't. Where one exists, the platform -interop layer auto-subscribes each Runtime to it on construction -(via `Suspend` / `Resume`); where it doesn't, the host wires it up -manually (or doesn't bother). This avoids forcing every host to -re-implement the same boilerplate while keeping the cross-platform -`Runtime::Suspend / Resume` available everywhere for hosts that want -to wire it themselves. - -| Platform | Process-wide signal | Where it's wired | -|---|---|---| -| Android | `android::global::AddPauseCallback` / `AddResumeCallback`, fired by the host's Activity via `androidGlobalPause` / `androidGlobalResume` | Auto-subscribed inside `runtimeCreate` in `Integrations/Android/.../BabylonNativeIntegrations.cpp`. Tickets are dropped when the Runtime is destroyed. | -| iOS / visionOS | `UIApplicationDidEnterBackgroundNotification` / `WillEnterForegroundNotification` (`NSNotificationCenter`) | Apple interop layer should subscribe in `BNRuntime`'s init; remove observers in `dealloc`. (Pending — wire up during the iOS migration.) | -| UWP | `CoreApplication::Suspending` / `Resuming` | Host typically owns a `CoreApplication`-scoped object directly; interop layer can either auto-subscribe or document the pattern. (Pending — wire up during the UWP migration.) | -| macOS | No clear "process backgrounded" notification (apps generally keep running). NSWorkspace sleep notifications exist but aren't usually what hosts want. | Don't auto-subscribe. Hosts call `Suspend / Resume` themselves if they need it. | -| Win32 | No process-wide signal — only per-HWND `WM_ACTIVATE`. | Don't auto-subscribe. Hosts call `Suspend / Resume` from `WM_ACTIVATE`/etc. | -| Linux / X11 | None standard. | Don't auto-subscribe. Host policy. | - -The auto-subscribe mechanism is **opaque** to hosts — they don't see the -ticket plumbing. Whether or not it's wired on a given platform, the -`Runtime::Suspend / Resume` C++ API is identical, so a host that wants -finer-grained control (e.g. suspending while a modal dialog is up) gets -the same code on every platform. Reference counting (above) makes the -manual and automatic paths compose cleanly. - -## 6. Implementation Phases - -### Phase 1 — Shared C++ facade (no new functionality) -- Extract the canonical setup from `Apps/Playground/Shared/AppContext.cpp` - into a new `Integrations` component, split along the lifetime boundary: - `Babylon::Integrations::Runtime` (long-lived; owns `AppRuntime`, - `ScriptLoader`, non-GPU polyfills/plugins; lazily constructs the - `Graphics::Device` on first View::Attach) and - `Babylon::Integrations::View` (transient; binds a platform surface - via `Device::UpdateWindow` + `EnableRendering` and drives the - per-frame `Start/FinishRenderingCurrentFrame` pair). -- Implement `Runtime::Suspend/Resume` on top of `AppRuntime::Suspend/Resume` - with reference-counted nesting. -- Implement `Runtime::LoadScript` queueing for calls made before the - first `View::Attach`; flush the queue inside that first Attach - after engine initialization completes. -- Add CMake option `BABYLON_NATIVE_INTEGRATIONS` (default ON). -- Plumb `BABYLON_NATIVE_PLUGIN_*` and `BABYLON_NATIVE_POLYFILL_*` flags - through `Babylon::Integrations`'s setup function so disabling a plugin - removes the link dep, the `Initialize` call, **and** the public - header surface that depends on it (see §4.4 *Conditional API - surface*). -- As a transitional step, refactor `AppContext` to be a thin wrapper - over `Babylon::Integrations` for parity validation; Playground keeps - working unchanged. `AppContext` is **deleted** at the end of Phase 5 - once every Playground host has been migrated to `Babylon::Integrations` - directly (see Phase 5.5 below). - -### Phase 2 — Win32 / Linux validation (no interop layer) -- Convert `Apps/Playground/Win32/App.cpp` and the Linux variants to use - `Babylon::Integrations` directly (no interop layer needed on these - platforms). -- Verify the `WM_PAINT` / Expose-event frame model works end-to-end - with the existing playground content. - -### Phase 3 — Android interop layer (`Integrations/Android/`) -- Create `Integrations/Android/CMakeLists.txt` that builds a JNI `.so` - exposing the entry points listed in §5. -- Add the companion `BabylonNative.kt` (or `.java`) shim under - `Integrations/Android/src/main/java/`. -- Convert `Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp` - to use the new interop layer (or simply delete it and have Playground - consume `Integrations/Android/` directly). -- Document the gradle integration: `externalNativeBuild` referencing - the `Integrations/Android` CMakeLists, plus adding the Java sources to - the host's `sourceSets`. -- Gate the build on `BABYLON_NATIVE_INTEGRATIONS_ANDROID` (default OFF). - -### Phase 4 — Apple interop layer (`Integrations/Apple/`) -- Create `Integrations/Apple/` with Obj-C++ implementations of `BNRuntime` - and `BNView` and their public Obj-C headers. -- Convert `Apps/Playground/iOS/LibNativeBridge.mm` and the macOS / - visionOS bridges to use the interop layer. -- Gate on `BABYLON_NATIVE_INTEGRATIONS_APPLE` (default OFF). - -### Phase 5 — UWP interop layer (`Integrations/Uwp/`) -- Create `Integrations/Uwp/` exposing C++/WinRT runtime classes (`.idl` + - generated projection). -- Convert `Apps/Playground/UWP/` to consume it. -- Gate on `BABYLON_NATIVE_INTEGRATIONS_UWP` (default OFF). - -### Phase 5.5 — Delete `AppContext` -- After all Playground hosts (Win32, Linux, Android, iOS, macOS, - visionOS, UWP) have been migrated to consume `Babylon::Integrations` - directly (Win32/Linux) or through their respective interop layers - (Android/Apple/UWP), delete `Apps/Playground/Shared/AppContext.{h,cpp}` - and any remaining `#include` references. -- The Playground apps then double as the simplest-possible canonical - examples of integrating `Babylon::Integrations` per platform — exactly - what we want users to copy from. - -### Phase 6 — Lifecycle and threading polish -- Audit Suspend/Resume across all platform interop layers; make sure - every platform's app-lifecycle hook is wired in the Playground app - but *not* assumed by the library. -- Document the frame-thread / JS-thread contract in - `Documentation/Components.md`. -- Add an integration test (`Apps/UnitTests/Source/Tests.Integrations.*`) - that exercises attach/detach/suspend/resume cycles without leaking - the device or the JS engine. - -### Phase 7 — Documentation -- Rewrite `Documentation/Components.md` "Getting started" to point at - `Babylon::Integrations`. -- Add per-platform integration guides under `Documentation/Integration/`: - Win32, Android, iOS, macOS, UWP, Linux. Each shows the smallest - possible host integration using the shared facade and (where - applicable) the platform's interop layer. - -## 7. Risks & Open Questions - -- **Surface swap discipline.** Constructing a `View` against a - different surface than the runtime is currently bound to triggers - `Device::UpdateWindow` (the same call `babylon-native-bridge` - makes from `surfaceChanged` — - `android/src/main/cpp/babylon.cpp:497-511`). Verify that this - cleanly handles: detach-while-no-frame-in-flight, - detach-mid-frame, and swap to a different surface mid-app. - Reference: existing start/finish discipline in - `Tests.ExternalTexture.D3D11.cpp:24-69`. -- **Shader cache directory.** *(Implemented.)* - `Babylon::Plugins::ShaderCache::Load/Save` is wired through - `RuntimeOptions::shaderCachePath` (`std::string`, empty = disabled). - The Integrations layer auto-loads on the first `View::Attach` (after - `ShaderCache::Enable`), auto-saves asynchronously on `Suspend` - (dispatched onto the JS thread before the suspension blocker takes - effect), and saves synchronously in `~Runtime`. Hosts only need to - pass the platform-appropriate cache path (Android - `Context.getCacheDir()`, iOS `NSCachesDirectory`, etc.). Reference: - bridge plumbing in `BabylonNativeBridge.mm:88-106` and - `babylon.cpp:242-260,378-398`. -- **JS ↔ native messaging.** Both bridges add a custom Napi - `ObjectWrap` (`LumiInterop`) exposing `callNative(jsonString)` and - `notifyReady()` to JS, plus a way to push results back to the host - (cached `jclass` + `JNIEnv` attach on Android; stored Swift block - on iOS). This is the second-most common integration need after - scene rendering. The plan currently only offers `RunOnJsThread` as - an N-API escape hatch. **Decide:** ship a typed message channel - (`Runtime::SetMessageHandler(std::function)` - + JS global `babylonNative.postMessage(string)`, JSON-string in / - out) so 90% of consumers don't have to write Napi. -- **Plugin granularity.** Some plugins have implicit JS dependencies - (e.g., `NativeXr` expects WebXR shims to exist). The setup function - needs to auto-skip dependent setup steps when their plugin is - disabled. -- **Headless / external-texture mode.** Hosts that render to an - external texture (no swap chain) currently use `BackBufferColor` / - `UpdateBackBuffer` directly on `Graphics::Device`. The simplified - facade may need a `View::AttachExternalTexture(...)` variant, or a - separate `Runtime::SetExternalTexture(...)` path with no `View` - attached. -- **Android-specific lifecycle entries on the interop layer.** The - Android JNI surface needs `setCurrentActivity(Activity)` and - `activityOnRequestPermissionsResult(...)` to feed - `AndroidExtensions/Globals.h` (consumed by `NativeCamera` etc.). - These don't belong on cross-platform `Runtime`/`View`; document - them as part of the Android interop layer's surface, alongside - the cross-platform mirror. -- **`RenderFrame` from a non-paint draw callback.** On Android, - `View.onDraw` is intended for `Canvas`-based 2D drawing; using it - as a generic frame tick may not be the idiomatic choice for - Vulkan/GLES rendering against a `SurfaceView`. Document - `SurfaceView` + a host-owned render thread as a supported pattern - alongside `onDraw`. The `babylon-native-bridge` Android demo uses - the `onDraw` + `invalidate()` pattern successfully - (`BabylonView.java:162-169`). -- **macOS without an interop layer.** A pure-C++ macOS host (rare, - but the unit-test app is one) should still be able to use - `Babylon::Integrations` directly with a `CAMetalLayer*`. Confirm this - works without pulling in the Obj-C interop layer. - -## 8. Out of Scope - -- Idiomatic high-level wrappers in any host language (no Kotlin - `BabylonView` `@Composable`, no Swift `BabylonScene` value type, no - managed .NET `BabylonControl`, no Rust `Scene` safe wrapper). -- A flat C ABI / FFI surface. Each platform talks to `Babylon::Integrations` - through its own native interop ABI. -- Precompiled "everything" artifacts published to package registries - (Maven Central, CocoaPods, NuGet, crates.io). Source-build via - CMake remains the only distribution channel. -- Replacing or deprecating the existing component-level API. -- Changes to the JavaScript-facing Babylon.js contract. From 27a535a4fba7295f7dfa647d3c183c9728baec07 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 16:12:39 -0700 Subject: [PATCH 55/71] Add missing initial Resize for Win32 Playground app --- Apps/Playground/Win32/App.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 564dd8ecc..4fe91409d 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -210,6 +210,16 @@ namespace // First View attach triggers Device construction, plugin init, and // flushes the queued scripts. g_view.emplace(*g_runtime, hWnd); + + // Drive the first Resize with the initial window bounds (physical pixels); + // the View handles physical conversion via GetDevicePixelRatio. + RECT rect{}; + if (GetClientRect(hWnd, &rect)) + { + g_view->Resize(static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top), + Babylon::Integrations::CoordinateUnits::Physical); + } } } From b33575ac47a825a773a7bbf8fcae033de1642398 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 21 May 2026 17:36:11 -0700 Subject: [PATCH 56/71] Remove AppContext --- .../babylon-native-debugging.instructions.md | 16 +- Apps/Playground/CMakeLists.txt | 2 - Apps/Playground/Shared/AppContext.cpp | 241 ------------------ Apps/Playground/Shared/AppContext.h | 55 ---- Apps/Playground/Shared/PlaygroundScripts.cpp | 53 ++++ Apps/Playground/Shared/PlaygroundScripts.h | 23 ++ Apps/Playground/UWP/App.cpp | 11 +- Apps/Playground/Win32/App.cpp | 35 +-- Apps/Playground/X11/App.cpp | 6 +- Integrations/CMakeLists.txt | 5 +- .../Babylon/Integrations/RuntimeOptions.h | 6 + Integrations/Source/Runtime.cpp | 32 ++- 12 files changed, 128 insertions(+), 357 deletions(-) delete mode 100644 Apps/Playground/Shared/AppContext.cpp delete mode 100644 Apps/Playground/Shared/AppContext.h diff --git a/.github/instructions/babylon-native-debugging.instructions.md b/.github/instructions/babylon-native-debugging.instructions.md index 9baf72432..44512bb57 100644 --- a/.github/instructions/babylon-native-debugging.instructions.md +++ b/.github/instructions/babylon-native-debugging.instructions.md @@ -24,7 +24,7 @@ For pure RenderDoc CLI usage (any app, not BN-specific), see |---|---| | `Apps/Playground/Shared/CommandLine.{h,cpp}` | Argument parser. Single source of truth for supported flags. | | `Apps/Playground/Shared/Diagnostics.{h,cpp}` | Crash handler, `DumpFailure`, finish-line, exit-code tracking. | -| `Apps/Playground/Shared/AppContext.cpp` | Wires `UnhandledExceptionHandler` + `console.error` into `DumpFailure`; injects `_playgroundOptions` into JS. | +| `Apps/Playground/Win32/App.cpp` (and the other per-host `App.*`) | Wires the `RuntimeOptions::log` callback into `DumpFailure` (`JS CONSOLE ERROR` for `LogLevel::Error`, `UNCAUGHT JS ERROR` for `LogLevel::Fatal`) and injects `_playgroundOptions` into JS via `Runtime::RunOnJsThread`. | | `Apps/Playground/Scripts/validation_native.js` | Test runner. Reads `_playgroundOptions`, picks tests, calls `TestUtils.captureNextFrame()`. Reference-image load failures arrive via `BABYLON.Tools.LoadFile`'s `onLoadFileError` and are tagged with `MISSING_REFERENCE_IMAGE:`. | | `Apps/Playground/Scripts/config.json` | Test catalog. Each entry has `title`, `playgroundId`/`scriptToRun`, `referenceImage`, optional `excludeFromAutomaticTesting`/`reason`/`onlyVisual`/`renderCount`/`capture`/`threshold`/`errorRatio`. | | `Plugins/TestUtils/Source/TestUtils.cpp` | Native side of `TestUtils.captureNextFrame()` -- calls `m_deviceContext.RequestCaptureNextFrame()`. | @@ -80,13 +80,15 @@ When you see `[Error] Error: Cannot load X` in stdout, **scroll up** -- short error line. The short line is kept so legacy log scrapers still match. ### 3. JS stack on every `console.error` -`AppContext.cpp`'s `Console::Initialize` callback calls +The Integrations layer's `Console::Initialize` callback calls `Babylon::Polyfills::Console::CaptureCurrentJsStack(env)` on every -`LogLevel::Error` message and appends the captured stack to the -`DumpFailure` banner body. The capture is best-effort -- if the JS engine -can't produce a stack (no JS context active, etc.) the helper returns an -empty string and the banner just shows the message. -Do not add per-callsite stack capture -- it's automatic. +`LogLevel::Error` message and appends the captured stack to the message +body that the host's `RuntimeOptions::log` callback receives (which the +Playground hosts then route into the `DumpFailure` banner). The capture is +best-effort — if the JS engine can't produce a stack (no JS context active, +unsupported engine, etc.) the helper returns an empty string and the host +just gets the bare message. +Do not add per-callsite stack capture — it's automatic. ### 4. Colored finish line `Playground: Finished in

Thin facade — owns no state, exposes the C++ API as static methods. * Hosts typically wrap it in their own {@code View} subclass (see @@ -26,7 +26,7 @@ public final class BabylonNative { /** * Construction options. Defaults match the C++ - * {@code Babylon::Integrations::RuntimeOptions}. Fields are public for + * {@code Babylon::Embedding::RuntimeOptions}. Fields are public for * simple object-initializer patterns. */ public static final class RuntimeOptions { @@ -47,7 +47,7 @@ public static final class RuntimeOptions { } static { - System.loadLibrary("BabylonNativeIntegrations"); + System.loadLibrary("BabylonNativeEmbedding"); } private BabylonNative() {} diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java index ff2207ee1..000ba726e 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java @@ -8,7 +8,7 @@ import android.view.View; import android.widget.FrameLayout; -import com.babylonjs.integrations.BabylonNative; +import com.babylonjs.embedding.BabylonNative; /** * Playground View built on {@link BabylonNative}. Borrows a Runtime handle diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java index 1f2dcf2e1..ca725d6ca 100644 --- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java +++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java @@ -4,7 +4,7 @@ import android.os.Bundle; import android.view.View; -import com.babylonjs.integrations.BabylonNative; +import com.babylonjs.embedding.BabylonNative; import com.library.babylonnative.BabylonView; public class PlaygroundActivity extends Activity { diff --git a/Apps/Playground/AppleShared/PlaygroundBootstrap.mm b/Apps/Playground/AppleShared/PlaygroundBootstrap.mm index 022824416..f8620e83a 100644 --- a/Apps/Playground/AppleShared/PlaygroundBootstrap.mm +++ b/Apps/Playground/AppleShared/PlaygroundBootstrap.mm @@ -3,15 +3,15 @@ #import "PlaygroundBootstrap.h" -#import +#import -#include +#include #include // Re-declare the internal class extension that exposes the C++ Runtime* -// from BNRuntime (implementation lives in Integrations/Apple/Source/BNRuntime.mm). +// from BNRuntime (implementation lives in Embedding/Apple/Source/BNRuntime.mm). @interface BNRuntime () -- (Babylon::Integrations::Runtime*)nativeRuntime; +- (Babylon::Embedding::Runtime*)nativeRuntime; @end @implementation PlaygroundBootstrap diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 08f767547..5fc6e3d2d 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -35,9 +35,9 @@ if(APPLE) "${CMAKE_CURRENT_LIST_DIR}/iOS/Base.lproj/Main.storyboard" "${CMAKE_CURRENT_LIST_DIR}/iOS/Base.lproj/LaunchScreen.storyboard") set(RESOURCE_FILES ${STORYBOARD} ${SCRIPTS}) - # NativeXr is consumed transitively through `BabylonNativeIntegrations` + # NativeXr is consumed transitively through `BabylonNativeEmbedding` # (the Apple interop static lib); no need to link it directly. - set(ADDITIONAL_LIBRARIES PRIVATE z BabylonNativeIntegrations) + set(ADDITIONAL_LIBRARIES PRIVATE z BabylonNativeEmbedding) set(SOURCES ${SOURCES} "iOS/AppDelegate.swift" "iOS/ViewController.swift" @@ -51,9 +51,9 @@ if(APPLE) set(PLIST_FILE "${CMAKE_CURRENT_LIST_DIR}/visionOS/Info.plist") set(RESOURCE_FILES ${SCRIPTS}) - # NativeXr is consumed transitively through `BabylonNativeIntegrations` + # NativeXr is consumed transitively through `BabylonNativeEmbedding` # (the Apple interop static lib); no need to link it directly. - set(ADDITIONAL_LIBRARIES PRIVATE z BabylonNativeIntegrations) + set(ADDITIONAL_LIBRARIES PRIVATE z BabylonNativeEmbedding) set(SOURCES ${SOURCES} "visionOS/App.swift" "visionOS/Playground-Bridging-Header.h" @@ -66,7 +66,7 @@ if(APPLE) set(PLIST_FILE "${CMAKE_CURRENT_LIST_DIR}/macOS/Info.plist") set(STORYBOARD "${CMAKE_CURRENT_LIST_DIR}/macOS/Base.lproj/Main.storyboard") set(RESOURCE_FILES ${STORYBOARD}) - set(ADDITIONAL_LIBRARIES PRIVATE BabylonNativeIntegrations) + set(ADDITIONAL_LIBRARIES PRIVATE BabylonNativeEmbedding) set(SOURCES ${SOURCES} "macOS/main.mm" "macOS/AppDelegate.h" @@ -152,7 +152,7 @@ target_link_libraries(Playground PRIVATE ExternalTexture PRIVATE File PRIVATE GraphicsDevice - PRIVATE Integrations + PRIVATE Embedding PRIVATE NativeCamera PRIVATE NativeCapture PRIVATE NativeEncoding diff --git a/Apps/Playground/Shared/PlaygroundScripts.cpp b/Apps/Playground/Shared/PlaygroundScripts.cpp index a1fc65395..8df310d62 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.cpp +++ b/Apps/Playground/Shared/PlaygroundScripts.cpp @@ -2,7 +2,7 @@ #include "Diagnostics.h" -#include +#include #include #include @@ -30,7 +30,7 @@ namespace Playground Babylon::PerfTrace::SetLevel(perfLevel); } - void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime) + void LoadBootstrapScripts(Babylon::Embedding::Runtime& runtime) { runtime.LoadScript("app:///Scripts/ammo.js"); // Commenting out recast.js for now because v8jsi is incompatible with asm.js. @@ -43,10 +43,10 @@ namespace Playground runtime.LoadScript("app:///Scripts/babylonjs.serializers.js"); } - std::function + std::function MakeLogCallback(std::function platformSink) { - return [sink = std::move(platformSink)](Babylon::Integrations::LogLevel level, std::string_view message) { + return [sink = std::move(platformSink)](Babylon::Embedding::LogLevel level, std::string_view message) { std::string text{message}; while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) { @@ -60,7 +60,7 @@ namespace Playground // Babylon.js routes recoverable errors through console.error; // surface them as a grep-able banner with a native callstack. - if (level == Babylon::Integrations::LogLevel::Error) + if (level == Babylon::Embedding::LogLevel::Error) { Diagnostics::DumpFailure( "JS CONSOLE ERROR", @@ -72,9 +72,9 @@ namespace Playground } // Uncaught JS exceptions: banner + finish-line + non-zero exit. - // The Integrations UnhandledExceptionHandler has already formatted + // The Embedding UnhandledExceptionHandler has already formatted // the message as "[Uncaught Error] ". - if (level == Babylon::Integrations::LogLevel::Fatal) + if (level == Babylon::Embedding::LogLevel::Fatal) { Diagnostics::DumpFailure( "UNCAUGHT JS ERROR", diff --git a/Apps/Playground/Shared/PlaygroundScripts.h b/Apps/Playground/Shared/PlaygroundScripts.h index b5b68ac4e..bbe91fad0 100644 --- a/Apps/Playground/Shared/PlaygroundScripts.h +++ b/Apps/Playground/Shared/PlaygroundScripts.h @@ -2,12 +2,12 @@ #include "CommandLine.h" -#include +#include #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { class Runtime; } @@ -22,7 +22,7 @@ namespace Playground // Queue the standard Babylon.js bootstrap scripts (core, loaders, // materials, GUI, serializers, etc.) onto `runtime` in dependency order. // - // The `Babylon::Integrations` layer doesn't bundle script loading; + // The `Babylon::Embedding` layer doesn't bundle script loading; // each host picks between this multi-UMD route and a pre-bundled // `bundle.js` route. Centralizing the list keeps every Playground // host in sync as the bundle list evolves. @@ -30,7 +30,7 @@ namespace Playground // LoadScript calls made before the first View attach are queued and // dispatched after engine init, so this is safe to call immediately // after constructing the Runtime. - void LoadBootstrapScripts(Babylon::Integrations::Runtime& runtime); + void LoadBootstrapScripts(Babylon::Embedding::Runtime& runtime); // Build a `RuntimeOptions::log` callback that: // 1) Trims trailing newlines from `message` and forwards it to @@ -40,13 +40,13 @@ namespace Playground // 2) On `LogLevel::Error`: emits a `JS CONSOLE ERROR` banner via // `Diagnostics::DumpFailure` (banner header + native callstack + // build info). The JS callstack is already appended by the - // Integrations layer, so it lands inside the banner body. + // Embedding layer, so it lands inside the banner body. // 3) On `LogLevel::Fatal`: emits an `UNCAUGHT JS ERROR` banner, // sets exit code 1, prints the finish line, and `std::quick_exit`s. // // Centralizing this matches the behavior every Playground host had - // pre-Integrations via `AppContext` (which routed `console.error` and + // pre-Embedding via `AppContext` (which routed `console.error` and // the unhandled exception handler through `DumpFailure`). - std::function + std::function MakeLogCallback(std::function platformSink); } diff --git a/Apps/Playground/UWP/App.cpp b/Apps/Playground/UWP/App.cpp index f315f4452..4a3dcc626 100644 --- a/Apps/Playground/UWP/App.cpp +++ b/Apps/Playground/UWP/App.cpp @@ -1,6 +1,6 @@ // App.cpp : Defines the entry point for the application. // -// Built on Babylon::Integrations: the cross-platform Runtime + View API +// Built on Babylon::Embedding: the cross-platform Runtime + View API // handles plugin/polyfill setup, GPU device construction, frame rendering, // and input forwarding. @@ -21,8 +21,8 @@ using namespace winrt::Windows::System; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Graphics::Display; -using Babylon::Integrations::CoordinateUnits; -using BNView = Babylon::Integrations::View; +using Babylon::Embedding::CoordinateUnits; +using BNView = Babylon::Embedding::View; // The main function is only used to initialize our IFrameworkView class. int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) @@ -321,7 +321,7 @@ void App::RestartRuntime(Rect bounds) { Uninitialize(); - Babylon::Integrations::RuntimeOptions runtimeOptions{}; + Babylon::Embedding::RuntimeOptions runtimeOptions{}; runtimeOptions.enableDebugger = true; runtimeOptions.log = Playground::MakeLogCallback([](std::string_view text) { std::string line{text}; diff --git a/Apps/Playground/UWP/App.h b/Apps/Playground/UWP/App.h index a02b580e2..a1098d78f 100644 --- a/Apps/Playground/UWP/App.h +++ b/Apps/Playground/UWP/App.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include @@ -54,11 +54,11 @@ struct App : winrt::implements m_runtime{}; + std::optional m_runtime{}; // Window-scoped: created during RestartRuntime, destroyed in Uninitialize // (and before a fresh Runtime is constructed). - std::optional m_view{}; + std::optional m_view{}; winrt::Windows::Foundation::Collections::IVectorView m_files{nullptr}; bool m_windowClosed; diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 46f4e5ade..1a3640d27 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -1,13 +1,13 @@ // App.cpp : Defines the entry point for the application. // -// Built on Babylon::Integrations: the cross-platform Runtime + View API +// Built on Babylon::Embedding: the cross-platform Runtime + View API // handles plugin/polyfill setup, GPU device construction, frame rendering, // and input forwarding. #include "App.h" -#include -#include +#include +#include #include #include @@ -38,11 +38,11 @@ WCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name // Process-scoped: created on app start, recreated on 'R' refresh, // destroyed on app exit. -std::optional g_runtime; +std::optional g_runtime; // Window-scoped: created on InitInstance after CreateWindowW returns, // destroyed on WM_DESTROY (or torn down + recreated by RefreshBabylon). -std::optional g_view; +std::optional g_view; bool minimized{false}; PlaygroundOptions options{}; @@ -90,9 +90,9 @@ namespace return arguments; } - Babylon::Integrations::RuntimeOptions MakeRuntimeOptions() + Babylon::Embedding::RuntimeOptions MakeRuntimeOptions() { - Babylon::Integrations::RuntimeOptions runtimeOptions{}; + Babylon::Embedding::RuntimeOptions runtimeOptions{}; runtimeOptions.enableDebugger = true; runtimeOptions.enableDebugTrace = options.DebugTrace.value_or(true); runtimeOptions.log = Playground::MakeLogCallback([](std::string_view text) { @@ -187,7 +187,7 @@ namespace { g_view->Resize(static_cast(rect.right - rect.left), static_cast(rect.bottom - rect.top), - Babylon::Integrations::CoordinateUnits::Physical); + Babylon::Embedding::CoordinateUnits::Physical); } } } @@ -340,8 +340,8 @@ BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) void ProcessMouseButtons(tagPOINTER_BUTTON_CHANGE_TYPE changeType, int x, int y) { - using View = Babylon::Integrations::View; - using CoordinateUnits = Babylon::Integrations::CoordinateUnits; + using View = Babylon::Embedding::View; + using CoordinateUnits = Babylon::Embedding::CoordinateUnits; if (!g_view) return; switch (changeType) @@ -369,8 +369,8 @@ void ProcessMouseButtons(tagPOINTER_BUTTON_CHANGE_TYPE changeType, int x, int y) LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - using View = Babylon::Integrations::View; - using CoordinateUnits = Babylon::Integrations::CoordinateUnits; + using View = Babylon::Embedding::View; + using CoordinateUnits = Babylon::Embedding::CoordinateUnits; switch (message) { diff --git a/Apps/Playground/X11/App.cpp b/Apps/Playground/X11/App.cpp index 1ac061fdc..7ef5c79e2 100644 --- a/Apps/Playground/X11/App.cpp +++ b/Apps/Playground/X11/App.cpp @@ -9,8 +9,8 @@ #include #include -#include -#include +#include +#include #include #include #include @@ -20,8 +20,8 @@ static const char* s_applicationClass = "Playground"; namespace { - std::optional g_runtime; - std::optional g_view; + std::optional g_runtime; + std::optional g_view; void Uninitialize() { @@ -35,7 +35,7 @@ namespace { Uninitialize(); - Babylon::Integrations::RuntimeOptions runtimeOptions{}; + Babylon::Embedding::RuntimeOptions runtimeOptions{}; runtimeOptions.log = Playground::MakeLogCallback([](std::string_view text) { std::cout << text << std::endl; }); @@ -68,7 +68,7 @@ namespace if (g_view) { // X11 reports surface dimensions in physical pixels. - g_view->Resize(width, height, Babylon::Integrations::CoordinateUnits::Physical); + g_view->Resize(width, height, Babylon::Embedding::CoordinateUnits::Physical); } } } @@ -183,19 +183,19 @@ int main(int _argc, const char* const* _argv) if (g_view) { switch (xbutton.button) { case Button1: - g_view->OnMouseDown(Babylon::Integrations::View::LeftMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseDown(Babylon::Embedding::View::LeftMouseButton(), xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); break; case Button2: - g_view->OnMouseDown(Babylon::Integrations::View::MiddleMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseDown(Babylon::Embedding::View::MiddleMouseButton(), xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); break; case Button3: - g_view->OnMouseDown(Babylon::Integrations::View::RightMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseDown(Babylon::Embedding::View::RightMouseButton(), xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); break; case Button4: - g_view->OnMouseWheel(Babylon::Integrations::View::MouseWheelY(), -120); + g_view->OnMouseWheel(Babylon::Embedding::View::MouseWheelY(), -120); break; case Button5: - g_view->OnMouseWheel(Babylon::Integrations::View::MouseWheelY(), 120); + g_view->OnMouseWheel(Babylon::Embedding::View::MouseWheelY(), 120); break; } } @@ -211,13 +211,13 @@ int main(int _argc, const char* const* _argv) switch (xbutton.button) { case Button1: - g_view->OnMouseUp(Babylon::Integrations::View::LeftMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseUp(Babylon::Embedding::View::LeftMouseButton(), xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); break; case Button2: - g_view->OnMouseUp(Babylon::Integrations::View::MiddleMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseUp(Babylon::Embedding::View::MiddleMouseButton(), xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); break; case Button3: - g_view->OnMouseUp(Babylon::Integrations::View::RightMouseButton(), xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseUp(Babylon::Embedding::View::RightMouseButton(), xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); break; } } @@ -227,7 +227,7 @@ int main(int _argc, const char* const* _argv) { const XMotionEvent& xmotion = event.xmotion; if (g_view) { - g_view->OnMouseMove(xmotion.x, xmotion.y, Babylon::Integrations::CoordinateUnits::Physical); + g_view->OnMouseMove(xmotion.x, xmotion.y, Babylon::Embedding::CoordinateUnits::Physical); } } break; diff --git a/Apps/Playground/iOS/Playground-Bridging-Header.h b/Apps/Playground/iOS/Playground-Bridging-Header.h index 572407097..6e8a19bec 100644 --- a/Apps/Playground/iOS/Playground-Bridging-Header.h +++ b/Apps/Playground/iOS/Playground-Bridging-Header.h @@ -1,11 +1,11 @@ // Swift-Obj-C bridging header for the iOS Playground app. // -// Exposes both the Babylon::Integrations Apple interop layer +// Exposes both the Babylon::Embedding Apple interop layer // (`BNRuntime`, `BNView`) and the Playground-specific helper // (`PlaygroundBootstrap`) to the Swift sources. #pragma once -#import +#import #import "AppleShared/PlaygroundBootstrap.h" diff --git a/Apps/Playground/macOS/ViewController.mm b/Apps/Playground/macOS/ViewController.mm index 27fdf1926..01e2c9fa2 100644 --- a/Apps/Playground/macOS/ViewController.mm +++ b/Apps/Playground/macOS/ViewController.mm @@ -1,6 +1,6 @@ #import "ViewController.h" -#import +#import #import "AppleShared/PlaygroundBootstrap.h" #import diff --git a/Apps/Playground/visionOS/App.swift b/Apps/Playground/visionOS/App.swift index bfabad0db..56f937678 100644 --- a/Apps/Playground/visionOS/App.swift +++ b/Apps/Playground/visionOS/App.swift @@ -1,6 +1,6 @@ // App.swift — visionOS Playground entry point. // -// Built on the Babylon::Integrations Apple interop layer (BNRuntime / +// Built on the Babylon::Embedding Apple interop layer (BNRuntime / // BNView), the same one the iOS Playground uses. visionOS differs // from iOS in two small ways: // 1. SwiftUI app lifecycle (`@main App`) instead of diff --git a/Apps/Playground/visionOS/Playground-Bridging-Header.h b/Apps/Playground/visionOS/Playground-Bridging-Header.h index 7d18eb7ec..5a9972399 100644 --- a/Apps/Playground/visionOS/Playground-Bridging-Header.h +++ b/Apps/Playground/visionOS/Playground-Bridging-Header.h @@ -1,11 +1,11 @@ // Swift-Obj-C++ bridging header for the visionOS Playground app. // -// Exposes both the Babylon::Integrations Apple interop layer +// Exposes both the Babylon::Embedding Apple interop layer // (`BNRuntime`, `BNView`) and the Playground-specific helper // (`PlaygroundBootstrap`) to the Swift sources. #pragma once -#import +#import #import "AppleShared/PlaygroundBootstrap.h" diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b6b4a2ef..23d51b1a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,13 +147,13 @@ option(BABYLON_NATIVE_POLYFILL_URL "Include Babylon Native Polyfill URL." ON) option(BABYLON_NATIVE_POLYFILL_WEBSOCKET "Include Babylon Native Polyfill WebSocket." ON) option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) -# Integrations -option(BABYLON_NATIVE_INTEGRATIONS "Build the cross-platform Babylon::Integrations facade (Runtime + View)." ON) -option(BABYLON_NATIVE_INTEGRATIONS_ANDROID "Build the Android JNI interop layer for Babylon::Integrations." OFF) +# Embedding +option(BABYLON_NATIVE_EMBEDDING "Build the cross-platform Babylon::Embedding facade (Runtime + View)." ON) +option(BABYLON_NATIVE_EMBEDDING_ANDROID "Build the Android JNI interop layer for Babylon::Embedding." OFF) # Default ON for Apple platforms — the iOS Playground (and future # macOS / visionOS Playground migrations) consume it directly. Apple # hosts that don't want the interop layer can explicitly disable. -option(BABYLON_NATIVE_INTEGRATIONS_APPLE "Build the Apple (iOS / macOS / visionOS) Obj-C++ interop layer for Babylon::Integrations." ${APPLE}) +option(BABYLON_NATIVE_EMBEDDING_APPLE "Build the Apple (iOS / macOS / visionOS) Obj-C++ interop layer for Babylon::Embedding." ${APPLE}) # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) @@ -312,8 +312,8 @@ add_subdirectory(Core) add_subdirectory(Plugins) add_subdirectory(Polyfills) -if(BABYLON_NATIVE_INTEGRATIONS) - add_subdirectory(Integrations) +if(BABYLON_NATIVE_EMBEDDING) + add_subdirectory(Embedding) endif() if(BABYLON_NATIVE_BUILD_APPS) diff --git a/Embedding/Android/CMakeLists.txt b/Embedding/Android/CMakeLists.txt new file mode 100644 index 000000000..fa820b076 --- /dev/null +++ b/Embedding/Android/CMakeLists.txt @@ -0,0 +1,39 @@ +# Babylon::Embedding Android interop layer. +# +# Builds a shared library (`libBabylonNativeEmbedding.so`) containing +# the JNI entry points declared in `BabylonNative.kt`. The host's +# Android Studio / Gradle project consumes this CMakeLists via its +# `externalNativeBuild { cmake { path "..." } }` hookup, alongside +# adding `src/main/java/` to its `sourceSets`. +# +# Gated by BABYLON_NATIVE_EMBEDDING_ANDROID at the root. + +if(NOT ANDROID) + message(FATAL_ERROR + "Embedding/Android is Android-only. " + "Disable BABYLON_NATIVE_EMBEDDING_ANDROID for non-Android builds.") +endif() + +set(ANDROID_EMBEDDING_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platform/Android") + +set(SOURCES + "${ANDROID_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Android/RuntimeHandle.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/BabylonNativeEmbedding.cpp") + +add_library(BabylonNativeEmbedding SHARED ${SOURCES}) + +warnings_as_errors(BabylonNativeEmbedding) + +target_include_directories(BabylonNativeEmbedding PUBLIC "${ANDROID_EMBEDDING_INCLUDE_ROOT}") + +target_link_libraries(BabylonNativeEmbedding + PRIVATE Embedding + PRIVATE AndroidExtensions + PRIVATE android # ANativeWindow_fromSurface + PRIVATE log + PRIVATE EGL # required by bgfx GL backend + PRIVATE GLESv3 + PRIVATE -lz) + +set_property(TARGET BabylonNativeEmbedding PROPERTY FOLDER Embedding) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/.." FILES ${SOURCES}) diff --git a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp b/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp similarity index 87% rename from Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp rename to Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp index 39e9e5667..3f5b9acc3 100644 --- a/Integrations/Android/src/main/cpp/BabylonNativeIntegrations.cpp +++ b/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp @@ -1,4 +1,4 @@ -// JNI interop for Babylon::Integrations on Android. +// JNI interop for Babylon::Embedding on Android. // // This file is the C++ side of the Android interop layer. It exposes // `extern "C" JNIEXPORT` entry points that any JVM-language host can call @@ -11,12 +11,12 @@ // `unique_ptr::release()` transfers ownership to the JVM side; the // matching `*Destroy` function `delete`s the raw pointer. // -// Symbols here follow `Java_com_babylonjs_integrations_BabylonNative_*`. +// Symbols here follow `Java_com_babylonjs_embedding_BabylonNative_*`. // Hosts can use any class name as long as Java signatures match. -#include -#include -#include +#include +#include +#include #include @@ -33,10 +33,10 @@ namespace { - using Babylon::Integrations::LogLevel; - using Babylon::Integrations::Runtime; - using Babylon::Integrations::RuntimeOptions; - using Babylon::Integrations::View; + using Babylon::Embedding::LogLevel; + using Babylon::Embedding::Runtime; + using Babylon::Embedding::RuntimeOptions; + using Babylon::Embedding::View; // Wraps a Runtime plus two `android::global` event tickets that // auto-Suspend/Resume on Activity lifecycle. Tickets are declared @@ -238,10 +238,10 @@ namespace } // Public handle-decoding entry point. Hosts that ship app-specific JNI -// helpers in `libBabylonNativeIntegrations.so` must use this rather than +// helpers in `libBabylonNativeEmbedding.so` must use this rather than // `reinterpret_cast(handle)`, because each Runtime is wrapped // in `AndroidRuntime` to hold Activity-lifecycle tickets. -namespace Babylon::Integrations::Android +namespace Babylon::Embedding::Android { Runtime* RuntimeFromHandle(jlong handle) { @@ -265,7 +265,7 @@ extern "C" // Calling more than once is harmless — Initialize replaces the existing // Context global ref. JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_setContext( +Java_com_babylonjs_embedding_BabylonNative_setContext( JNIEnv* env, jclass, jobject context) { JavaVM* javaVM{nullptr}; @@ -277,26 +277,26 @@ Java_com_babylonjs_integrations_BabylonNative_setContext( } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_setCurrentActivity( +Java_com_babylonjs_embedding_BabylonNative_setCurrentActivity( JNIEnv*, jclass, jobject activity) { android::global::SetCurrentActivity(activity); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_pause(JNIEnv*, jclass) +Java_com_babylonjs_embedding_BabylonNative_pause(JNIEnv*, jclass) { android::global::Pause(); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_resume(JNIEnv*, jclass) +Java_com_babylonjs_embedding_BabylonNative_resume(JNIEnv*, jclass) { android::global::Resume(); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_requestPermissionsResult( +Java_com_babylonjs_embedding_BabylonNative_requestPermissionsResult( JNIEnv* env, jclass, jint requestCode, jobjectArray permissions, jintArray grantResults) { std::vector nativePermissions{}; @@ -329,7 +329,7 @@ Java_com_babylonjs_integrations_BabylonNative_requestPermissionsResult( // ===================================================================== JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__(JNIEnv*, jclass) +Java_com_babylonjs_embedding_BabylonNative_runtimeCreate__(JNIEnv*, jclass) { // Default Android consumers want logcat output; Console polyfill, // uncaught JS exceptions, and (when enabled) DebugTrace all route @@ -339,7 +339,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__(JNIEnv*, jclass) } JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Lcom_babylonjs_integrations_BabylonNative_00024RuntimeOptions_2( +Java_com_babylonjs_embedding_BabylonNative_runtimeCreate__Lcom_babylonjs_embedding_BabylonNative_00024RuntimeOptions_2( JNIEnv* env, jclass, jobject javaOptions) { // RuntimeOptions is a Java object so callers have a stable @@ -353,7 +353,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeCreate__Lcom_babylonjs_inte } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong handle) +Java_com_babylonjs_embedding_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong handle) { // Reverse declaration order: tickets unsubscribe before the Runtime // is destroyed, so no callback fires on a dead Runtime. @@ -361,14 +361,14 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jl } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeLoadScript( +Java_com_babylonjs_embedding_BabylonNative_runtimeLoadScript( JNIEnv* env, jclass, jlong handle, jstring url) { AsRuntime(handle)->LoadScript(ToStdString(env, url)); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeEval( +Java_com_babylonjs_embedding_BabylonNative_runtimeEval( JNIEnv* env, jclass, jlong handle, jstring source, jstring sourceUrl) { AsRuntime(handle)->Eval(ToStdString(env, source), ToStdString(env, sourceUrl)); @@ -381,7 +381,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeEval( // C++ API for finer-grained control. JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeSetXrSurface( +Java_com_babylonjs_embedding_BabylonNative_runtimeSetXrSurface( JNIEnv* env, jclass, jlong handle, jobject surface) { #if BABYLON_NATIVE_PLUGIN_NATIVEXR @@ -401,7 +401,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeSetXrSurface( } JNIEXPORT jboolean JNICALL -Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, jlong handle) +Java_com_babylonjs_embedding_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, jlong handle) { #if BABYLON_NATIVE_PLUGIN_NATIVEXR return AsRuntime(handle)->IsXrActive() ? JNI_TRUE : JNI_FALSE; @@ -417,7 +417,7 @@ Java_com_babylonjs_integrations_BabylonNative_runtimeIsXrActive(JNIEnv*, jclass, // ===================================================================== JNIEXPORT jlong JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewAttach( +Java_com_babylonjs_embedding_BabylonNative_viewAttach( JNIEnv* env, jclass, jlong runtimeHandle, jobject surface) { if (surface == nullptr) @@ -454,7 +454,7 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( { view->Resize(static_cast(surfaceWidth), static_cast(surfaceHeight), - Babylon::Integrations::CoordinateUnits::Physical); + Babylon::Embedding::CoordinateUnits::Physical); } // bgfx retains its own reference on the ANativeWindow for the @@ -464,37 +464,37 @@ Java_com_babylonjs_integrations_BabylonNative_viewAttach( } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewDetach(JNIEnv*, jclass, jlong handle) +Java_com_babylonjs_embedding_BabylonNative_viewDetach(JNIEnv*, jclass, jlong handle) { delete AsView(handle); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewRenderFrame(JNIEnv*, jclass, jlong handle) +Java_com_babylonjs_embedding_BabylonNative_viewRenderFrame(JNIEnv*, jclass, jlong handle) { AsView(handle)->RenderFrame(); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewResize( +Java_com_babylonjs_embedding_BabylonNative_viewResize( JNIEnv*, jclass, jlong handle, jint width, jint height) { // Java callers pass the SurfaceView's pixel-buffer dimensions // (physical pixels on Android). AsView(handle)->Resize(static_cast(width), static_cast(height), - Babylon::Integrations::CoordinateUnits::Physical); + Babylon::Embedding::CoordinateUnits::Physical); } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( +Java_com_babylonjs_embedding_BabylonNative_viewPointerDown( JNIEnv* env, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; // MotionEvent.getX/getY are physical pixels. AsView(handle)->OnPointerDown(static_cast(pointerId), x, y, - Babylon::Integrations::CoordinateUnits::Physical); + Babylon::Embedding::CoordinateUnits::Physical); #else (void)handle; (void)pointerId; (void)x; (void)y; ThrowPluginNotEnabled(env, @@ -504,13 +504,13 @@ Java_com_babylonjs_integrations_BabylonNative_viewPointerDown( } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( +Java_com_babylonjs_embedding_BabylonNative_viewPointerMove( JNIEnv* env, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; AsView(handle)->OnPointerMove(static_cast(pointerId), x, y, - Babylon::Integrations::CoordinateUnits::Physical); + Babylon::Embedding::CoordinateUnits::Physical); #else (void)handle; (void)pointerId; (void)x; (void)y; ThrowPluginNotEnabled(env, @@ -520,13 +520,13 @@ Java_com_babylonjs_integrations_BabylonNative_viewPointerMove( } JNIEXPORT void JNICALL -Java_com_babylonjs_integrations_BabylonNative_viewPointerUp( +Java_com_babylonjs_embedding_BabylonNative_viewPointerUp( JNIEnv* env, jclass, jlong handle, jint pointerId, jfloat x, jfloat y) { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT (void)env; AsView(handle)->OnPointerUp(static_cast(pointerId), x, y, - Babylon::Integrations::CoordinateUnits::Physical); + Babylon::Embedding::CoordinateUnits::Physical); #else (void)handle; (void)pointerId; (void)x; (void)y; ThrowPluginNotEnabled(env, diff --git a/Embedding/Apple/CMakeLists.txt b/Embedding/Apple/CMakeLists.txt new file mode 100644 index 000000000..6bea2bf8e --- /dev/null +++ b/Embedding/Apple/CMakeLists.txt @@ -0,0 +1,50 @@ +# Babylon::Embedding Apple interop layer. +# +# Builds a static library producing Obj-C `BNRuntime` and `BNView` +# classes, importable from Swift via the standard Obj-C bridge. +# The host's Xcode project (or downstream CMake project) consumes +# this directly. +# +# Gated by BABYLON_NATIVE_EMBEDDING_APPLE at the root. + +if(NOT APPLE) + message(FATAL_ERROR + "Embedding/Apple is for iOS / macOS / visionOS. " + "Disable BABYLON_NATIVE_EMBEDDING_APPLE for non-Apple builds.") +endif() + +set(APPLE_EMBEDDING_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platform/Apple") + +set(SOURCES + "${APPLE_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Apple/BabylonNativeEmbedding.h" + "${APPLE_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Apple/BNRuntime.h" + "${APPLE_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Apple/BNRuntimeNative.h" + "${APPLE_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Apple/BNView.h" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntimeInternal.h" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm") + +add_library(BabylonNativeEmbedding STATIC ${SOURCES}) + +warnings_as_errors(BabylonNativeEmbedding) + +target_include_directories(BabylonNativeEmbedding + PUBLIC "${APPLE_EMBEDDING_INCLUDE_ROOT}" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Source") + +target_link_libraries(BabylonNativeEmbedding + PUBLIC Embedding + PRIVATE "-framework Foundation" + PRIVATE "-framework QuartzCore" + PRIVATE "-framework MetalKit") + +# Enable ARC for the Obj-C++ files. +set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm" + "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm" + PROPERTIES COMPILE_FLAGS "-fobjc-arc") + +set_property(TARGET BabylonNativeEmbedding PROPERTY FOLDER Embedding) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/.." FILES ${SOURCES}) diff --git a/Integrations/Apple/Source/BNRuntime.mm b/Embedding/Apple/Source/BNRuntime.mm similarity index 84% rename from Integrations/Apple/Source/BNRuntime.mm rename to Embedding/Apple/Source/BNRuntime.mm index ac0f1395e..b81a9140a 100644 --- a/Integrations/Apple/Source/BNRuntime.mm +++ b/Embedding/Apple/Source/BNRuntime.mm @@ -1,8 +1,8 @@ // BNRuntime.mm — Obj-C++ implementation bridging BNRuntime to -// Babylon::Integrations::Runtime. +// Babylon::Embedding::Runtime. #import "BNRuntimeInternal.h" -#import +#import #import #import @@ -33,15 +33,15 @@ os_log_t BabylonNativeLogger() // Warn folds into DEFAULT (matches Apple's console.warn → default). // DEBUG/INFO are filtered out of release builds and Console.app by // default, so Verbose lands there to suppress DebugTrace noise. - os_log_type_t ToOSLogType(Babylon::Integrations::LogLevel level) + os_log_type_t ToOSLogType(Babylon::Embedding::LogLevel level) { switch (level) { - case Babylon::Integrations::LogLevel::Verbose: return OS_LOG_TYPE_DEBUG; - case Babylon::Integrations::LogLevel::Log: return OS_LOG_TYPE_DEFAULT; - case Babylon::Integrations::LogLevel::Warn: return OS_LOG_TYPE_DEFAULT; - case Babylon::Integrations::LogLevel::Error: return OS_LOG_TYPE_ERROR; - case Babylon::Integrations::LogLevel::Fatal: return OS_LOG_TYPE_FAULT; + case Babylon::Embedding::LogLevel::Verbose: return OS_LOG_TYPE_DEBUG; + case Babylon::Embedding::LogLevel::Log: return OS_LOG_TYPE_DEFAULT; + case Babylon::Embedding::LogLevel::Warn: return OS_LOG_TYPE_DEFAULT; + case Babylon::Embedding::LogLevel::Error: return OS_LOG_TYPE_ERROR; + case Babylon::Embedding::LogLevel::Fatal: return OS_LOG_TYPE_FAULT; } return OS_LOG_TYPE_DEFAULT; } @@ -53,7 +53,7 @@ @implementation BNRuntimeOptions @implementation BNRuntime { - std::optional _runtime; + std::optional _runtime; MTKView* _xrView; } @@ -66,7 +66,7 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions { if ((self = [super init])) { - Babylon::Integrations::RuntimeOptions options{}; + Babylon::Embedding::RuntimeOptions options{}; if (runtimeOptions != nil) { if (runtimeOptions.msaaSamples != nil) @@ -81,7 +81,7 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions // reaches Console.app / `log stream`. Hosts wanting custom routing // should use the C++ API. `%{public}.*s` is required — without // `{public}` os_log redacts the payload as `` in release. - options.log = [](Babylon::Integrations::LogLevel level, std::string_view message) { + options.log = [](Babylon::Embedding::LogLevel level, std::string_view message) { os_log_with_type(BabylonNativeLogger(), ToOSLogType(level), "%{public}.*s", static_cast(message.size()), message.data()); @@ -163,7 +163,7 @@ - (BOOL)isXRActive #endif } -- (Babylon::Integrations::Runtime*)nativeRuntime +- (Babylon::Embedding::Runtime*)nativeRuntime { return _runtime ? &*_runtime : nullptr; } @@ -184,7 +184,7 @@ - (void)updateXrViewIfNeeded @end -namespace Babylon::Integrations::Apple +namespace Babylon::Embedding::Apple { Runtime* RuntimeFromBNRuntime(BNRuntime* runtime) { diff --git a/Integrations/Apple/Source/BNRuntimeInternal.h b/Embedding/Apple/Source/BNRuntimeInternal.h similarity index 68% rename from Integrations/Apple/Source/BNRuntimeInternal.h rename to Embedding/Apple/Source/BNRuntimeInternal.h index 74190e2b7..071c74a7e 100644 --- a/Integrations/Apple/Source/BNRuntimeInternal.h +++ b/Embedding/Apple/Source/BNRuntimeInternal.h @@ -1,17 +1,17 @@ // Internal Obj-C category exposing the underlying -// `Babylon::Integrations::Runtime*` to BNView.mm. Not part of the +// `Babylon::Embedding::Runtime*` to BNView.mm. Not part of the // public Apple interop layer surface (Swift consumers don't see this). #pragma once -#import +#import -#include +#include NS_ASSUME_NONNULL_BEGIN @interface BNRuntime () -- (Babylon::Integrations::Runtime*)nativeRuntime; +- (Babylon::Embedding::Runtime*)nativeRuntime; /// Toggle the XR overlay view's visibility based on the runtime's /// current XR-active state. Called by BNView once per frame so the diff --git a/Integrations/Apple/Source/BNView.mm b/Embedding/Apple/Source/BNView.mm similarity index 89% rename from Integrations/Apple/Source/BNView.mm rename to Embedding/Apple/Source/BNView.mm index b3675cb16..94400270d 100644 --- a/Integrations/Apple/Source/BNView.mm +++ b/Embedding/Apple/Source/BNView.mm @@ -1,20 +1,20 @@ // BNView.mm — Obj-C++ implementation bridging BNView to -// Babylon::Integrations::View. +// Babylon::Embedding::View. #import "BNRuntimeInternal.h" -#import +#import #import #import -#include +#include #include #include @implementation BNView { - std::optional _view; + std::optional _view; BNRuntime* _runtime; MTKView* _mtkView; @@ -31,7 +31,7 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view return nil; } - Babylon::Integrations::Runtime* nativeRuntime = runtime.nativeRuntime; + Babylon::Embedding::Runtime* nativeRuntime = runtime.nativeRuntime; if (nativeRuntime == nullptr) { return nil; @@ -98,7 +98,7 @@ - (instancetype)initWithRuntime:(BNRuntime*)runtime view:(MTKView*)view { _view->Resize(static_cast(bounds.width), static_cast(bounds.height), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } } } @@ -154,7 +154,7 @@ - (void)resizeWithWidth:(NSUInteger)width height:(NSUInteger)height // converts to physical internally using the layer's DPR. _view->Resize(static_cast(width), static_cast(height), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } } @@ -168,7 +168,7 @@ - (void)pointerDown:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y _view->OnPointerDown(static_cast(pointerId), static_cast(x), static_cast(y), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } #else (void)pointerId; (void)x; (void)y; @@ -187,7 +187,7 @@ - (void)pointerMove:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y _view->OnPointerMove(static_cast(pointerId), static_cast(x), static_cast(y), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } #else (void)pointerId; (void)x; (void)y; @@ -206,7 +206,7 @@ - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y _view->OnPointerUp(static_cast(pointerId), static_cast(x), static_cast(y), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } #else (void)pointerId; (void)x; (void)y; @@ -227,7 +227,7 @@ - (void)mouseDown:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y _view->OnMouseDown(static_cast(button), static_cast(x), static_cast(y), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } #else (void)button; (void)x; (void)y; @@ -246,7 +246,7 @@ - (void)mouseUp:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y _view->OnMouseUp(static_cast(button), static_cast(x), static_cast(y), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } #else (void)button; (void)x; (void)y; @@ -264,7 +264,7 @@ - (void)mouseMoveAtX:(CGFloat)x y:(CGFloat)y { _view->OnMouseMove(static_cast(x), static_cast(y), - Babylon::Integrations::CoordinateUnits::Logical); + Babylon::Embedding::CoordinateUnits::Logical); } #else (void)x; (void)y; @@ -294,22 +294,22 @@ - (void)mouseWheel:(NSInteger)axis delta:(NSInteger)delta + (NSInteger)leftMouseButton { - return static_cast(Babylon::Integrations::View::LeftMouseButton()); + return static_cast(Babylon::Embedding::View::LeftMouseButton()); } + (NSInteger)middleMouseButton { - return static_cast(Babylon::Integrations::View::MiddleMouseButton()); + return static_cast(Babylon::Embedding::View::MiddleMouseButton()); } + (NSInteger)rightMouseButton { - return static_cast(Babylon::Integrations::View::RightMouseButton()); + return static_cast(Babylon::Embedding::View::RightMouseButton()); } + (NSInteger)mouseWheelY { - return static_cast(Babylon::Integrations::View::MouseWheelY()); + return static_cast(Babylon::Embedding::View::MouseWheelY()); } @end diff --git a/Integrations/Apple/Source/BNViewDelegate.mm b/Embedding/Apple/Source/BNViewDelegate.mm similarity index 97% rename from Integrations/Apple/Source/BNViewDelegate.mm rename to Embedding/Apple/Source/BNViewDelegate.mm index 2c661ec1d..ee88baf05 100644 --- a/Integrations/Apple/Source/BNViewDelegate.mm +++ b/Embedding/Apple/Source/BNViewDelegate.mm @@ -11,7 +11,7 @@ // AppKit), the same way every other platform does — see // `-[BNView resizeWithWidth:height:]`. -#import +#import @implementation BNViewDelegate { diff --git a/Embedding/CMakeLists.txt b/Embedding/CMakeLists.txt new file mode 100644 index 000000000..c66e5be58 --- /dev/null +++ b/Embedding/CMakeLists.txt @@ -0,0 +1,153 @@ +set(EMBEDDING_SHARED_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/Include/Shared") + +set(SOURCES + "${EMBEDDING_SHARED_INCLUDE_ROOT}/Babylon/Embedding/LogLevel.h" + "${EMBEDDING_SHARED_INCLUDE_ROOT}/Babylon/Embedding/Runtime.h" + "${EMBEDDING_SHARED_INCLUDE_ROOT}/Babylon/Embedding/RuntimeOptions.h" + "${EMBEDDING_SHARED_INCLUDE_ROOT}/Babylon/Embedding/View.h" + "Source/Runtime.cpp" + "Source/RuntimeImpl.h" + "Source/View.cpp") + +add_library(Embedding ${SOURCES}) + +warnings_as_errors(Embedding) + +target_include_directories(Embedding PUBLIC "${EMBEDDING_SHARED_INCLUDE_ROOT}") + +# Always-on dependencies. The Embedding layer formalizes the canonical +# Babylon Native setup, so the standard polyfills (Blob / Console / File / +# Performance / TextDecoder / XMLHttpRequest) are always linked. +target_link_libraries(Embedding + PUBLIC napi + # GraphicsDevice is PUBLIC because View.h (a public header) + # references `Babylon::Graphics::WindowT` from . + PUBLIC GraphicsDevice + # GraphicsDeviceContext is PRIVATE — needed so View.cpp can see + # for the free + # `Babylon::Graphics::GetDevicePixelRatio(window)` helper used to + # convert physical → logical pixels on the first `View::Resize` + # call (before the `Device` exists or while it is still bound to + # the previous window on a re-attach). Internal Graphics types are + # never exposed across the Embedding public surface. + PRIVATE GraphicsDeviceContext + PRIVATE arcana + PRIVATE AppRuntime + PRIVATE ScriptLoader + PRIVATE Blob + PRIVATE Console + PRIVATE File + PRIVATE Performance + PRIVATE TextDecoder + PRIVATE XMLHttpRequest) + +# ----- Conditionally-included polyfills ----- +# +# Each flag is exposed as a PUBLIC compile definition so the public +# headers (Runtime.h / View.h) and the impl source files can both gate +# on the same value via `#if BABYLON_NATIVE_*`. + +if(BABYLON_NATIVE_POLYFILL_WINDOW) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_WINDOW=1) + target_link_libraries(Embedding PRIVATE Window) +endif() + +if(BABYLON_NATIVE_POLYFILL_CANVAS) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_CANVAS=1) + target_link_libraries(Embedding PRIVATE Canvas) +endif() + +if(BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER=1) + target_link_libraries(Embedding PRIVATE AbortController) +endif() + +if(BABYLON_NATIVE_POLYFILL_SCHEDULING) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_SCHEDULING=1) + target_link_libraries(Embedding PRIVATE Scheduling) +endif() + +if(BABYLON_NATIVE_POLYFILL_URL) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_URL=1) + target_link_libraries(Embedding PRIVATE URL) +endif() + +if(BABYLON_NATIVE_POLYFILL_WEBSOCKET) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_WEBSOCKET=1) + target_link_libraries(Embedding PRIVATE WebSocket) +endif() + +# ----- Conditionally-included plugins ----- + +if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEENGINE=1) + target_link_libraries(Embedding PRIVATE NativeEngine) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEINPUT=1) + target_link_libraries(Embedding PRIVATE NativeInput) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVECAMERA) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVECAMERA=1) + target_link_libraries(Embedding PRIVATE NativeCamera) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVECAPTURE=1) + target_link_libraries(Embedding PRIVATE NativeCapture) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEENCODING=1) + target_link_libraries(Embedding PRIVATE NativeEncoding) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS=1) + target_link_libraries(Embedding PRIVATE NativeOptimizations) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVETRACING) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVETRACING=1) + target_link_libraries(Embedding PRIVATE NativeTracing) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEXR AND TARGET NativeXr) + # Public surface: Runtime::SetXrWindow / IsXrActive when this flag + # is set. RuntimeImpl.h includes , but + # that's an internal header so the dependency stays PRIVATE here. + # NativeXr is only compiled on Android/iOS (see Plugins/CMakeLists.txt), + # so we additionally check `TARGET NativeXr` to skip XR support on + # platforms where the plugin isn't built even if the flag is ON. + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEXR=1) + target_link_libraries(Embedding PRIVATE NativeXr) +endif() + +if(BABYLON_NATIVE_PLUGIN_SHADERCACHE) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_SHADERCACHE=1) + target_link_libraries(Embedding PRIVATE ShaderCache) +endif() + +if(BABYLON_NATIVE_PLUGIN_TESTUTILS) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_TESTUTILS=1) + target_link_libraries(Embedding PRIVATE TestUtils) +endif() + +set_property(TARGET Embedding PROPERTY FOLDER Embedding) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) + +# ----- Per-platform interop layers ----- +# +# Each platform's interop layer lives in its own subdirectory and is +# opt-in via its own CMake flag. They depend on the cross-platform +# `Embedding` target above. + +if(BABYLON_NATIVE_EMBEDDING_ANDROID) + add_subdirectory(Android) +endif() + +if(BABYLON_NATIVE_EMBEDDING_APPLE) + add_subdirectory(Apple) +endif() diff --git a/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h b/Embedding/Include/Platform/Android/Babylon/Embedding/Android/RuntimeHandle.h similarity index 81% rename from Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h rename to Embedding/Include/Platform/Android/Babylon/Embedding/Android/RuntimeHandle.h index 102ca454c..e7008c3e5 100644 --- a/Integrations/Include/Platform/Android/Babylon/Integrations/Android/RuntimeHandle.h +++ b/Embedding/Include/Platform/Android/Babylon/Embedding/Android/RuntimeHandle.h @@ -1,10 +1,10 @@ #pragma once -#include +#include #include -namespace Babylon::Integrations::Android +namespace Babylon::Embedding::Android { // Convert a jlong handle from `runtimeCreate` back to a Runtime*. // The JNI layer wraps each Runtime in an internal struct that also diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h similarity index 97% rename from Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h rename to Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h index 5856757d1..b21496610 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntime.h +++ b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h @@ -1,4 +1,4 @@ -// BNRuntime.h — public Obj-C interface for the Babylon::Integrations +// BNRuntime.h — public Obj-C interface for the Babylon::Embedding // runtime on Apple platforms (iOS, macOS, visionOS). // // Swift consumers see this through the auto-generated Swift bridge diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntimeNative.h similarity index 64% rename from Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h rename to Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntimeNative.h index 51ac1404b..b8c0b4681 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNRuntimeNative.h +++ b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntimeNative.h @@ -1,20 +1,20 @@ // Obj-C++ escape hatch for host-specific helpers layered on top of -// Babylon Native's Apple Integrations runtime. +// Babylon Native's Apple Embedding runtime. #pragma once -#import +#import #ifdef __cplusplus -#include +#include -namespace Babylon::Integrations::Apple +namespace Babylon::Embedding::Apple { // Convert a BNRuntime wrapper back to its underlying C++ Runtime. // // Hosts that ship small app-specific Obj-C++ helpers alongside the - // Apple Integrations layer can use this to access C++ escape hatches + // Apple Embedding layer can use this to access C++ escape hatches // such as Runtime::RunOnJsThread without making those concepts part // of the Swift/Objective-C BNRuntime surface. // diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h similarity index 97% rename from Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h rename to Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h index 6b62a955b..609ce00bc 100644 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BNView.h +++ b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h @@ -1,4 +1,4 @@ -// BNView.h — public Obj-C interface for the Babylon::Integrations view +// BNView.h — public Obj-C interface for the Babylon::Embedding view // on Apple platforms. // // Construct against a host-provided `MTKView`. The first BNView against @@ -50,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN /// initialization; subsequent attaches just rebind the surface. /// /// Returns `nil` if `runtime` or `view` is `nil`, or if constructing the -/// underlying `Babylon::Integrations::View` throws (another View is +/// underlying `Babylon::Embedding::View` throws (another View is /// already attached to the runtime). /// /// **Delegate management:** If `view.delegate` is nil, BNView creates a diff --git a/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BabylonNativeEmbedding.h b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BabylonNativeEmbedding.h new file mode 100644 index 000000000..4350ddbe5 --- /dev/null +++ b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BabylonNativeEmbedding.h @@ -0,0 +1,8 @@ +// Umbrella header for the Babylon::Embedding Apple interop layer. +// Import this from Swift via the bridging header (or from Obj-C). + +#pragma once + +#import +#import +#import \ No newline at end of file diff --git a/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h b/Embedding/Include/Shared/Babylon/Embedding/LogLevel.h similarity index 93% rename from Integrations/Include/Shared/Babylon/Integrations/LogLevel.h rename to Embedding/Include/Shared/Babylon/Embedding/LogLevel.h index d748da87b..457aa7214 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/LogLevel.h +++ b/Embedding/Include/Shared/Babylon/Embedding/LogLevel.h @@ -1,6 +1,6 @@ #pragma once -namespace Babylon::Integrations +namespace Babylon::Embedding { // Severity levels for the RuntimeOptions log callback. Ordered by // increasing severity so hosts can do `level >= LogLevel::Warn` filtering. diff --git a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h b/Embedding/Include/Shared/Babylon/Embedding/Runtime.h similarity index 96% rename from Integrations/Include/Shared/Babylon/Integrations/Runtime.h rename to Embedding/Include/Shared/Babylon/Embedding/Runtime.h index c9e1607ac..eb0ee5c77 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/Runtime.h +++ b/Embedding/Include/Shared/Babylon/Embedding/Runtime.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include @@ -8,7 +8,7 @@ #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { class View; struct RuntimeImpl; diff --git a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h b/Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h similarity index 94% rename from Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h rename to Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h index ff7cf1945..e0f20ab79 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/RuntimeOptions.h +++ b/Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h @@ -1,13 +1,13 @@ #pragma once -#include +#include #include #include #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { struct RuntimeOptions { diff --git a/Integrations/Include/Shared/Babylon/Integrations/View.h b/Embedding/Include/Shared/Babylon/Embedding/View.h similarity index 97% rename from Integrations/Include/Shared/Babylon/Integrations/View.h rename to Embedding/Include/Shared/Babylon/Embedding/View.h index 73097782c..1b31276ac 100644 --- a/Integrations/Include/Shared/Babylon/Integrations/View.h +++ b/Embedding/Include/Shared/Babylon/Embedding/View.h @@ -5,7 +5,7 @@ #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { class Runtime; struct ViewImpl; diff --git a/Integrations/Source/Runtime.cpp b/Embedding/Source/Runtime.cpp similarity index 98% rename from Integrations/Source/Runtime.cpp rename to Embedding/Source/Runtime.cpp index a31ffd2e8..cd9b6b14e 100644 --- a/Integrations/Source/Runtime.cpp +++ b/Embedding/Source/Runtime.cpp @@ -59,13 +59,13 @@ #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { namespace { // Forward Babylon Console levels to the public LogLevel enum so // consumers don't need to depend on the Console polyfill header. - LogLevel ToIntegrationsLogLevel(Babylon::Polyfills::Console::LogLevel level) + LogLevel ToEmbeddingLogLevel(Babylon::Polyfills::Console::LogLevel level) { switch (level) { @@ -215,7 +215,7 @@ namespace Babylon::Integrations } } - userLog(ToIntegrationsLogLevel(level), message); + userLog(ToEmbeddingLogLevel(level), message); }); } diff --git a/Integrations/Source/RuntimeImpl.h b/Embedding/Source/RuntimeImpl.h similarity index 98% rename from Integrations/Source/RuntimeImpl.h rename to Embedding/Source/RuntimeImpl.h index f4023ffed..6f2260b3b 100644 --- a/Integrations/Source/RuntimeImpl.h +++ b/Embedding/Source/RuntimeImpl.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include @@ -26,7 +26,7 @@ #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { // Internal implementation of Runtime. Lives in Source/ so View.cpp // can reach into it without exposing internals on the public header. diff --git a/Integrations/Source/View.cpp b/Embedding/Source/View.cpp similarity index 97% rename from Integrations/Source/View.cpp rename to Embedding/Source/View.cpp index 47d72c9f6..916afc9d0 100644 --- a/Integrations/Source/View.cpp +++ b/Embedding/Source/View.cpp @@ -7,7 +7,7 @@ #include #include -namespace Babylon::Integrations +namespace Babylon::Embedding { namespace { @@ -161,7 +161,7 @@ namespace Babylon::Integrations // Flag pre-init so hosts can diagnose "my draw callback fires // but nothing renders". The externally-suspended case is // expected and stays silent. - DEBUG_TRACE("Babylon::Integrations::View::RenderFrame skipped: View has not yet been initialized. Call View::Resize with the surface's pixel dimensions to begin rendering."); + DEBUG_TRACE("Babylon::Embedding::View::RenderFrame skipped: View has not yet been initialized. Call View::Resize with the surface's pixel dimensions to begin rendering."); return; } if (impl.m_suspendCount.load(std::memory_order_relaxed) > 0) @@ -178,7 +178,7 @@ namespace Babylon::Integrations // Stores the host-supplied size on the ViewImpl, then either pushes // it through Device::UpdateSize (initialized) or drives // InitializeIfReady (uninitialized). Single source of truth for - // surface size in the Integrations layer. + // surface size in the Embedding layer. void View::Resize(uint32_t width, uint32_t height, CoordinateUnits units) { ValidateNonZeroSize(width, height, "View::Resize size"); diff --git a/Install/Install.cmake b/Install/Install.cmake index de94c0ede..3f66e0340 100644 --- a/Install/Install.cmake +++ b/Install/Install.cmake @@ -251,15 +251,15 @@ if(TARGET XMLHttpRequest) endif() # ---------------- -# Integrations +# Embedding # ---------------- -if(TARGET Integrations) - install_lib(Integrations) - install_include(Integrations) +if(TARGET Embedding) + install_lib(Embedding) + install_include(Embedding) endif() -if(TARGET BabylonNativeIntegrations) - install_lib(BabylonNativeIntegrations) - install_include(BabylonNativeIntegrations) +if(TARGET BabylonNativeEmbedding) + install_lib(BabylonNativeEmbedding) + install_include(BabylonNativeEmbedding) endif() diff --git a/Integrations/Android/CMakeLists.txt b/Integrations/Android/CMakeLists.txt deleted file mode 100644 index aba1f6b4b..000000000 --- a/Integrations/Android/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -# Babylon::Integrations Android interop layer. -# -# Builds a shared library (`libBabylonNativeIntegrations.so`) containing -# the JNI entry points declared in `BabylonNative.kt`. The host's -# Android Studio / Gradle project consumes this CMakeLists via its -# `externalNativeBuild { cmake { path "..." } }` hookup, alongside -# adding `src/main/java/` to its `sourceSets`. -# -# Gated by BABYLON_NATIVE_INTEGRATIONS_ANDROID at the root. - -if(NOT ANDROID) - message(FATAL_ERROR - "Integrations/Android is Android-only. " - "Disable BABYLON_NATIVE_INTEGRATIONS_ANDROID for non-Android builds.") -endif() - -set(ANDROID_INTEGRATIONS_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platform/Android") - -set(SOURCES - "${ANDROID_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Android/RuntimeHandle.h" - "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/BabylonNativeIntegrations.cpp") - -add_library(BabylonNativeIntegrations SHARED ${SOURCES}) - -warnings_as_errors(BabylonNativeIntegrations) - -target_include_directories(BabylonNativeIntegrations PUBLIC "${ANDROID_INTEGRATIONS_INCLUDE_ROOT}") - -target_link_libraries(BabylonNativeIntegrations - PRIVATE Integrations - PRIVATE AndroidExtensions - PRIVATE android # ANativeWindow_fromSurface - PRIVATE log - PRIVATE EGL # required by bgfx GL backend - PRIVATE GLESv3 - PRIVATE -lz) - -set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) -source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/.." FILES ${SOURCES}) diff --git a/Integrations/Apple/CMakeLists.txt b/Integrations/Apple/CMakeLists.txt deleted file mode 100644 index 0cd41a196..000000000 --- a/Integrations/Apple/CMakeLists.txt +++ /dev/null @@ -1,50 +0,0 @@ -# Babylon::Integrations Apple interop layer. -# -# Builds a static library producing Obj-C `BNRuntime` and `BNView` -# classes, importable from Swift via the standard Obj-C bridge. -# The host's Xcode project (or downstream CMake project) consumes -# this directly. -# -# Gated by BABYLON_NATIVE_INTEGRATIONS_APPLE at the root. - -if(NOT APPLE) - message(FATAL_ERROR - "Integrations/Apple is for iOS / macOS / visionOS. " - "Disable BABYLON_NATIVE_INTEGRATIONS_APPLE for non-Apple builds.") -endif() - -set(APPLE_INTEGRATIONS_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platform/Apple") - -set(SOURCES - "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BabylonNativeIntegrations.h" - "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BNRuntime.h" - "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BNRuntimeNative.h" - "${APPLE_INTEGRATIONS_INCLUDE_ROOT}/Babylon/Integrations/Apple/BNView.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm" - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntimeInternal.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm" - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm") - -add_library(BabylonNativeIntegrations STATIC ${SOURCES}) - -warnings_as_errors(BabylonNativeIntegrations) - -target_include_directories(BabylonNativeIntegrations - PUBLIC "${APPLE_INTEGRATIONS_INCLUDE_ROOT}" - PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Source") - -target_link_libraries(BabylonNativeIntegrations - PUBLIC Integrations - PRIVATE "-framework Foundation" - PRIVATE "-framework QuartzCore" - PRIVATE "-framework MetalKit") - -# Enable ARC for the Obj-C++ files. -set_source_files_properties( - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm" - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm" - "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm" - PROPERTIES COMPILE_FLAGS "-fobjc-arc") - -set_property(TARGET BabylonNativeIntegrations PROPERTY FOLDER Integrations) -source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/.." FILES ${SOURCES}) diff --git a/Integrations/CMakeLists.txt b/Integrations/CMakeLists.txt deleted file mode 100644 index 8168ea7f3..000000000 --- a/Integrations/CMakeLists.txt +++ /dev/null @@ -1,153 +0,0 @@ -set(INTEGRATIONS_SHARED_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/Include/Shared") - -set(SOURCES - "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/LogLevel.h" - "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/Runtime.h" - "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/RuntimeOptions.h" - "${INTEGRATIONS_SHARED_INCLUDE_ROOT}/Babylon/Integrations/View.h" - "Source/Runtime.cpp" - "Source/RuntimeImpl.h" - "Source/View.cpp") - -add_library(Integrations ${SOURCES}) - -warnings_as_errors(Integrations) - -target_include_directories(Integrations PUBLIC "${INTEGRATIONS_SHARED_INCLUDE_ROOT}") - -# Always-on dependencies. The Integrations layer formalizes the canonical -# Babylon Native setup, so the standard polyfills (Blob / Console / File / -# Performance / TextDecoder / XMLHttpRequest) are always linked. -target_link_libraries(Integrations - PUBLIC napi - # GraphicsDevice is PUBLIC because View.h (a public header) - # references `Babylon::Graphics::WindowT` from . - PUBLIC GraphicsDevice - # GraphicsDeviceContext is PRIVATE — needed so View.cpp can see - # for the free - # `Babylon::Graphics::GetDevicePixelRatio(window)` helper used to - # convert physical → logical pixels on the first `View::Resize` - # call (before the `Device` exists or while it is still bound to - # the previous window on a re-attach). Internal Graphics types are - # never exposed across the Integrations public surface. - PRIVATE GraphicsDeviceContext - PRIVATE arcana - PRIVATE AppRuntime - PRIVATE ScriptLoader - PRIVATE Blob - PRIVATE Console - PRIVATE File - PRIVATE Performance - PRIVATE TextDecoder - PRIVATE XMLHttpRequest) - -# ----- Conditionally-included polyfills ----- -# -# Each flag is exposed as a PUBLIC compile definition so the public -# headers (Runtime.h / View.h) and the impl source files can both gate -# on the same value via `#if BABYLON_NATIVE_*`. - -if(BABYLON_NATIVE_POLYFILL_WINDOW) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_WINDOW=1) - target_link_libraries(Integrations PRIVATE Window) -endif() - -if(BABYLON_NATIVE_POLYFILL_CANVAS) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_CANVAS=1) - target_link_libraries(Integrations PRIVATE Canvas) -endif() - -if(BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_ABORTCONTROLLER=1) - target_link_libraries(Integrations PRIVATE AbortController) -endif() - -if(BABYLON_NATIVE_POLYFILL_SCHEDULING) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_SCHEDULING=1) - target_link_libraries(Integrations PRIVATE Scheduling) -endif() - -if(BABYLON_NATIVE_POLYFILL_URL) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_URL=1) - target_link_libraries(Integrations PRIVATE URL) -endif() - -if(BABYLON_NATIVE_POLYFILL_WEBSOCKET) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_POLYFILL_WEBSOCKET=1) - target_link_libraries(Integrations PRIVATE WebSocket) -endif() - -# ----- Conditionally-included plugins ----- - -if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEENGINE=1) - target_link_libraries(Integrations PRIVATE NativeEngine) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEINPUT=1) - target_link_libraries(Integrations PRIVATE NativeInput) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVECAMERA) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVECAMERA=1) - target_link_libraries(Integrations PRIVATE NativeCamera) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVECAPTURE=1) - target_link_libraries(Integrations PRIVATE NativeCapture) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEENCODING=1) - target_link_libraries(Integrations PRIVATE NativeEncoding) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS=1) - target_link_libraries(Integrations PRIVATE NativeOptimizations) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVETRACING) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVETRACING=1) - target_link_libraries(Integrations PRIVATE NativeTracing) -endif() - -if(BABYLON_NATIVE_PLUGIN_NATIVEXR AND TARGET NativeXr) - # Public surface: Runtime::SetXrWindow / IsXrActive when this flag - # is set. RuntimeImpl.h includes , but - # that's an internal header so the dependency stays PRIVATE here. - # NativeXr is only compiled on Android/iOS (see Plugins/CMakeLists.txt), - # so we additionally check `TARGET NativeXr` to skip XR support on - # platforms where the plugin isn't built even if the flag is ON. - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEXR=1) - target_link_libraries(Integrations PRIVATE NativeXr) -endif() - -if(BABYLON_NATIVE_PLUGIN_SHADERCACHE) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_SHADERCACHE=1) - target_link_libraries(Integrations PRIVATE ShaderCache) -endif() - -if(BABYLON_NATIVE_PLUGIN_TESTUTILS) - target_compile_definitions(Integrations PUBLIC BABYLON_NATIVE_PLUGIN_TESTUTILS=1) - target_link_libraries(Integrations PRIVATE TestUtils) -endif() - -set_property(TARGET Integrations PROPERTY FOLDER Integrations) -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) - -# ----- Per-platform interop layers ----- -# -# Each platform's interop layer lives in its own subdirectory and is -# opt-in via its own CMake flag. They depend on the cross-platform -# `Integrations` target above. - -if(BABYLON_NATIVE_INTEGRATIONS_ANDROID) - add_subdirectory(Android) -endif() - -if(BABYLON_NATIVE_INTEGRATIONS_APPLE) - add_subdirectory(Apple) -endif() diff --git a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h b/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h deleted file mode 100644 index c7e5dbcef..000000000 --- a/Integrations/Include/Platform/Apple/Babylon/Integrations/Apple/BabylonNativeIntegrations.h +++ /dev/null @@ -1,8 +0,0 @@ -// Umbrella header for the Babylon::Integrations Apple interop layer. -// Import this from Swift via the bridging header (or from Obj-C). - -#pragma once - -#import -#import -#import \ No newline at end of file From f58f49b89034fa0ce0225684d9402c884995d141 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 11 Jun 2026 15:57:20 -0700 Subject: [PATCH 64/71] PR feedback --- .../src/main/cpp/BabylonNativeEmbedding.cpp | 46 ++++++++++++++++++- .../Shared/Babylon/Embedding/RuntimeOptions.h | 4 +- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp b/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp index 3f5b9acc3..4e506e025 100644 --- a/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp +++ b/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp @@ -54,6 +54,17 @@ namespace std::unique_ptr runtime; android::global::AppStateChangedCallbackTicket pauseTicket; android::global::AppStateChangedCallbackTicket resumeTicket; + +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // The ANativeWindow currently handed to Runtime::SetXrWindow. + // NativeXr stores the raw pointer without acquiring a reference (and + // only consumes it lazily at XR session creation on the JS thread), + // so the JNI layer owns this reference: it's acquired via + // ANativeWindow_fromSurface in runtimeSetXrSurface, released and + // replaced when the surface changes, and released in runtimeDestroy + // after the Runtime (and thus NativeXr) is torn down. + ANativeWindow* xrWindow{nullptr}; +#endif }; AndroidRuntime* AsAndroidRuntime(jlong handle) { return reinterpret_cast(handle); } @@ -355,9 +366,25 @@ Java_com_babylonjs_embedding_BabylonNative_runtimeCreate__Lcom_babylonjs_embeddi JNIEXPORT void JNICALL Java_com_babylonjs_embedding_BabylonNative_runtimeDestroy(JNIEnv*, jclass, jlong handle) { + AndroidRuntime* androidRuntime = AsAndroidRuntime(handle); + +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + // Grab the XR window before tearing down the Runtime; it can only be + // released once the Runtime (and NativeXr, which holds the raw pointer) + // is gone. + ANativeWindow* xrWindow = androidRuntime->xrWindow; +#endif + // Reverse declaration order: tickets unsubscribe before the Runtime // is destroyed, so no callback fires on a dead Runtime. - delete AsAndroidRuntime(handle); + delete androidRuntime; + +#if BABYLON_NATIVE_PLUGIN_NATIVEXR + if (xrWindow != nullptr) + { + ANativeWindow_release(xrWindow); + } +#endif } JNIEXPORT void JNICALL @@ -385,12 +412,27 @@ Java_com_babylonjs_embedding_BabylonNative_runtimeSetXrSurface( JNIEnv* env, jclass, jlong handle, jobject surface) { #if BABYLON_NATIVE_PLUGIN_NATIVEXR + AndroidRuntime* androidRuntime = AsAndroidRuntime(handle); + ANativeWindow* window{nullptr}; if (surface != nullptr) { + // +1 reference owned by us; released when replaced below or in + // runtimeDestroy. NativeXr only stores the raw pointer. window = ANativeWindow_fromSurface(env, surface); } - AsRuntime(handle)->SetXrWindow(window); + + androidRuntime->runtime->SetXrWindow(window); + + // Release the previously-held window now that NativeXr has been pointed + // at the new one. Hosts must not swap the XR surface out from under an + // active session; the usual flow is set(surface) … set(null) bracketing + // a session. + if (androidRuntime->xrWindow != nullptr) + { + ANativeWindow_release(androidRuntime->xrWindow); + } + androidRuntime->xrWindow = window; #else (void)handle; (void)surface; diff --git a/Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h b/Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h index e0f20ab79..323d674e2 100644 --- a/Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h +++ b/Embedding/Include/Shared/Babylon/Embedding/RuntimeOptions.h @@ -49,8 +49,8 @@ namespace Babylon::Embedding // If non-empty: // - Loaded synchronously during the first View attach (missing // or unreadable file: ignored). - // - Saved asynchronously during `Runtime::Suspend` (queued onto - // the JS thread before the suspension blocker). + // - Saved synchronously during `Runtime::Suspend` (after the + // in-flight frame is closed, when the engine is quiescent). // - Saved synchronously during `~Runtime`. std::string shaderCachePath; #endif From fa0f2e6295dcd075aa163a58ec454d12b0ec0a3f Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 12 Jun 2026 09:27:10 -0700 Subject: [PATCH 65/71] Remove unnecessary explicit lib references in Playground CMakeLists.txt, and instead rely on pulling them in transitively through Embedding --- Apps/Playground/CMakeLists.txt | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index f7bb08d55..2c70c9d9a 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -143,33 +143,18 @@ endif() target_include_directories(Playground PRIVATE ".") +# The Embedding layer links and initializes the full canonical set of +# polyfills/plugins (Blob, Canvas, Console, File, GraphicsDevice, the +# Native* plugins, ScriptLoader, ShaderCache, the polyfills, etc.) and +# forwards them transitively, so the Playground only needs to list the +# libraries it consumes directly: +# - Embedding : the Runtime + View API the host is built on. +# - bx : used by Shared/Diagnostics.cpp. +# - TestUtils : used by Win32/X11 --test mode. target_link_libraries(Playground - PRIVATE AbortController - PRIVATE AppRuntime - PRIVATE Blob - PRIVATE bx - PRIVATE Canvas - PRIVATE Console - PRIVATE ExternalTexture - PRIVATE File - PRIVATE GraphicsDevice PRIVATE Embedding - PRIVATE NativeCamera - PRIVATE NativeCapture - PRIVATE NativeEncoding - PRIVATE NativeEngine - PRIVATE NativeInput - PRIVATE NativeOptimizations - PRIVATE NativeTracing - PRIVATE Performance - PRIVATE ScriptLoader - PRIVATE ShaderCache + PRIVATE bx PRIVATE TestUtils - PRIVATE TextDecoder - PRIVATE TextEncoder - PRIVATE Window - PRIVATE XMLHttpRequest - PRIVATE Fetch ${ADDITIONAL_LIBRARIES} ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) From 1c7fcef6ac90f43c1a804bf47ecdc2d68da93d83 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 12 Jun 2026 12:06:29 -0700 Subject: [PATCH 66/71] Fix metal-cpp build issue --- Dependencies/CMakeLists.txt | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index 23040702a..58a82367f 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -208,27 +208,23 @@ if(APPLE) message(FATAL_ERROR "Failed to generate metal-cpp single header") endif() - # Generate source that includes the single header with the necessary implementation defines. - file(WRITE "${METAL_CPP_GENERATED_DIR}/Source/Metal.cpp" - "#define NS_PRIVATE_IMPLEMENTATION\n" - "#define MTL_PRIVATE_IMPLEMENTATION\n" - "#define CA_PRIVATE_IMPLEMENTATION\n" - "#include \n") - - set(SOURCES - "${METAL_CPP_GENERATED_DIR}/Include/Metal/Metal.hpp" - "${METAL_CPP_GENERATED_DIR}/Source/Metal.cpp") - - add_library(metal-cpp ${SOURCES}) - target_include_directories(metal-cpp SYSTEM PUBLIC "${METAL_CPP_GENERATED_DIR}/Include") + # metal-cpp is consumed as a header-only library. The metal-cpp + # implementation (the NS / MTL / CA `*_PRIVATE_IMPLEMENTATION` globals) is + # already compiled into bgfx's Metal renderer, which embeds and builds its + # own copy of metal-cpp (see bgfx's renderer_mtl.cpp). Compiling a second + # copy here would define those globals in both libmetal-cpp.a and libbgfx.a; + # whether that produces ~1900 "duplicate symbol" link errors then depends + # purely on the (fragile) order in which the two static archives appear on + # the link line. We instead expose metal-cpp as headers only and let the + # single bgfx-provided implementation satisfy the symbols at link time. + # Graphics — the only consumer — always links bgfx on Apple, so the + # implementation symbols always resolve. + add_library(metal-cpp INTERFACE) + target_include_directories(metal-cpp SYSTEM INTERFACE "${METAL_CPP_GENERATED_DIR}/Include") target_link_libraries(metal-cpp - PRIVATE "-framework Foundation" - PRIVATE "-framework Metal" - PRIVATE "-framework QuartzCore") - - source_group(TREE "${metal-cpp_SOURCE_DIR}" FILES ${METAL_CPP_HEADERS}) - source_group(TREE "${METAL_CPP_GENERATED_DIR}" FILES ${SOURCES}) - set_property(TARGET metal-cpp PROPERTY FOLDER Dependencies) + INTERFACE "-framework Foundation" + INTERFACE "-framework Metal" + INTERFACE "-framework QuartzCore") endif() # -------------------------------------------------- From 7fc240f530c667b7319ea7ce801c7a1057615138 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 15 Jun 2026 13:35:31 -0700 Subject: [PATCH 67/71] Apply suggestion from @bghgary Co-authored-by: Gary Hsu --- Embedding/Include/Shared/Babylon/Embedding/Runtime.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Embedding/Include/Shared/Babylon/Embedding/Runtime.h b/Embedding/Include/Shared/Babylon/Embedding/Runtime.h index eb0ee5c77..c8c8d8bff 100644 --- a/Embedding/Include/Shared/Babylon/Embedding/Runtime.h +++ b/Embedding/Include/Shared/Babylon/Embedding/Runtime.h @@ -32,9 +32,11 @@ namespace Babylon::Embedding ~Runtime(); - // Non-copyable; movable. Cross-references between Runtime and View - // point at the heap-allocated pimpls, so moves of the outer wrappers - // are safe and don't invalidate any back-pointers. + // Non-copyable; movable. Cross-references point at the heap-allocated + // pimpls, so move-construction is always safe and never invalidates a + // back-pointer. Move-assignment is safe only when the destination has + // no View attached -- it destroys the destination's pimpl, which + // ~RuntimeImpl guards with assert(m_currentView == nullptr). Runtime(const Runtime&) = delete; Runtime& operator=(const Runtime&) = delete; Runtime(Runtime&&) noexcept; From b15a7d6489f89e6ff34a7b4172f3ff35396ae564 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 15 Jun 2026 13:40:33 -0700 Subject: [PATCH 68/71] PR feedback --- Core/Graphics/Source/DeviceImpl.cpp | 4 ++-- Core/Graphics/Source/DeviceImpl.h | 4 ++-- Core/Graphics/Source/DeviceImpl_D3D11.cpp | 2 +- Core/Graphics/Source/DeviceImpl_D3D12.cpp | 2 +- Core/Graphics/Source/DeviceImpl_Metal.mm | 2 +- Core/Graphics/Source/DeviceImpl_OpenGL.cpp | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 0062b5252..3b449cd9d 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -55,7 +55,7 @@ namespace Babylon::Graphics // // Configure the window/device BEFORE the resolution below. The // first UpdateSize -> UpdateBgfxResolution syncs the render - // resolution onto the surface via UpdateWindowSize, which reads + // resolution onto the surface via ResizeRenderSurface, which reads // m_state.Window — so the window must already be set. This also // matches the re-attach ordering in ViewImpl::InitializeIfReady // (UpdateWindow then UpdateSize). @@ -506,7 +506,7 @@ namespace Babylon::Graphics // Keep the native rendering surface in sync with the render resolution // we just computed (platform/graphics-API specific; no-op where the // surface size is driven elsewhere). - UpdateWindowSize(m_state.Window, res.width, res.height); + ResizeRenderSurface(m_state.Window, res.width, res.height); } void DeviceImpl::RequestScreenShots() diff --git a/Core/Graphics/Source/DeviceImpl.h b/Core/Graphics/Source/DeviceImpl.h index 1734175f7..8f86412ba 100644 --- a/Core/Graphics/Source/DeviceImpl.h +++ b/Core/Graphics/Source/DeviceImpl.h @@ -123,7 +123,7 @@ namespace Babylon::Graphics // matches what bgfx renders into. Implemented per graphics API. The // window may be default-constructed (null) before UpdateWindow has run // (e.g. during construction), in which case there's nothing to size. - static void UpdateWindowSize(WindowT window, uint32_t width, uint32_t height); + static void ResizeRenderSurface(WindowT window, uint32_t width, uint32_t height); void UpdateBgfxState(); void UpdateBgfxResolution(); @@ -153,7 +153,7 @@ namespace Babylon::Graphics // The native window/surface we render into. Cached as WindowT (the // handle in Bgfx.InitState.platformData is type-erased to void* and - // can't be cast back to WindowT portably) so UpdateWindowSize can + // can't be cast back to WindowT portably) so ResizeRenderSurface can // push the render resolution onto the surface. Null until // UpdateWindow. WindowT Window{}; diff --git a/Core/Graphics/Source/DeviceImpl_D3D11.cpp b/Core/Graphics/Source/DeviceImpl_D3D11.cpp index 7c44e779f..ab94e5747 100644 --- a/Core/Graphics/Source/DeviceImpl_D3D11.cpp +++ b/Core/Graphics/Source/DeviceImpl_D3D11.cpp @@ -10,7 +10,7 @@ namespace Babylon::Graphics return {static_cast(bgfx::getInternalData()->context)}; } - void DeviceImpl::UpdateWindowSize(WindowT /*window*/, uint32_t /*width*/, uint32_t /*height*/) + void DeviceImpl::ResizeRenderSurface(WindowT /*window*/, uint32_t /*width*/, uint32_t /*height*/) { // No-op: the swap chain size is managed elsewhere on this platform. } diff --git a/Core/Graphics/Source/DeviceImpl_D3D12.cpp b/Core/Graphics/Source/DeviceImpl_D3D12.cpp index 415bd83a6..b34c50727 100644 --- a/Core/Graphics/Source/DeviceImpl_D3D12.cpp +++ b/Core/Graphics/Source/DeviceImpl_D3D12.cpp @@ -10,7 +10,7 @@ namespace Babylon::Graphics return {static_cast(bgfx::getInternalData()->context)}; } - void DeviceImpl::UpdateWindowSize(WindowT /*window*/, uint32_t /*width*/, uint32_t /*height*/) + void DeviceImpl::ResizeRenderSurface(WindowT /*window*/, uint32_t /*width*/, uint32_t /*height*/) { // No-op: the swap chain size is managed elsewhere on this platform. } diff --git a/Core/Graphics/Source/DeviceImpl_Metal.mm b/Core/Graphics/Source/DeviceImpl_Metal.mm index 432e871f1..f9f2ae110 100644 --- a/Core/Graphics/Source/DeviceImpl_Metal.mm +++ b/Core/Graphics/Source/DeviceImpl_Metal.mm @@ -14,7 +14,7 @@ }; } - void DeviceImpl::UpdateWindowSize(WindowT window, uint32_t width, uint32_t height) + void DeviceImpl::ResizeRenderSurface(WindowT window, uint32_t width, uint32_t height) { // The window is the CAMetalLayer we render into. Resize its drawable to // match the render resolution bgfx will use so the backbuffer and the diff --git a/Core/Graphics/Source/DeviceImpl_OpenGL.cpp b/Core/Graphics/Source/DeviceImpl_OpenGL.cpp index 434f6593d..8f2e39fc2 100644 --- a/Core/Graphics/Source/DeviceImpl_OpenGL.cpp +++ b/Core/Graphics/Source/DeviceImpl_OpenGL.cpp @@ -10,7 +10,7 @@ namespace Babylon::Graphics return {static_cast(bgfx::getInternalData()->context)}; } - void DeviceImpl::UpdateWindowSize(WindowT /*window*/, uint32_t /*width*/, uint32_t /*height*/) + void DeviceImpl::ResizeRenderSurface(WindowT /*window*/, uint32_t /*width*/, uint32_t /*height*/) { // No-op: the surface size is managed elsewhere on this platform. } From f487db5dca5d00fb29397258333e5ea8f7f2a30b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 15 Jun 2026 13:44:51 -0700 Subject: [PATCH 69/71] PR feedback --- Apps/Playground/iOS/AppDelegate.swift | 4 ++- Apps/Playground/visionOS/App.swift | 5 +++- Embedding/Apple/Source/BNRuntime.mm | 28 +++++++++++++------ .../Apple/Babylon/Embedding/Apple/BNRuntime.h | 18 ++++++++---- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/Apps/Playground/iOS/AppDelegate.swift b/Apps/Playground/iOS/AppDelegate.swift index 4694230db..3c1e7165e 100644 --- a/Apps/Playground/iOS/AppDelegate.swift +++ b/Apps/Playground/iOS/AppDelegate.swift @@ -14,7 +14,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let runtimeOptions = BNRuntimeOptions() runtimeOptions.enableDebugger = true runtimeOptions.enableDebugTrace = true - let runtime = BNRuntime(options: runtimeOptions) + guard let runtime = BNRuntime(options: runtimeOptions) else { + fatalError("Failed to construct BNRuntime") + } // Queue the Babylon.js bootstrap scripts (shared with the other // Playground hosts via Apps/Playground/Shared/PlaygroundScripts.cpp), diff --git a/Apps/Playground/visionOS/App.swift b/Apps/Playground/visionOS/App.swift index 56f937678..9179cba42 100644 --- a/Apps/Playground/visionOS/App.swift +++ b/Apps/Playground/visionOS/App.swift @@ -25,7 +25,10 @@ final class BabylonRuntime: ObservableObject { let options = BNRuntimeOptions() options.enableDebugger = true options.enableDebugTrace = true - bnRuntime = BNRuntime(options: options) + guard let bnRuntime = BNRuntime(options: options) else { + fatalError("Failed to construct BNRuntime") + } + self.bnRuntime = bnRuntime PlaygroundBootstrap.loadScripts(bnRuntime) bnRuntime.loadScript("app:///Scripts/experience.js") diff --git a/Embedding/Apple/Source/BNRuntime.mm b/Embedding/Apple/Source/BNRuntime.mm index b81a9140a..7c9e9817c 100644 --- a/Embedding/Apple/Source/BNRuntime.mm +++ b/Embedding/Apple/Source/BNRuntime.mm @@ -62,7 +62,7 @@ - (instancetype)init return [self initWithOptions:nil]; } -- (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions +- (nullable instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions { if ((self = [super init])) { @@ -91,15 +91,27 @@ - (instancetype)initWithOptions:(nullable BNRuntimeOptions*)runtimeOptions #if BABYLON_NATIVE_PLUGIN_SHADERCACHE options.shaderCachePath = runtimeOptions.shaderCachePath.UTF8String; #else - // Fail loudly: silently dropping a caller-supplied cache path - // would be hard to diagnose later. - @throw [NSException - exceptionWithName:@"BabylonNativePluginNotEnabledException" - reason:@"shaderCachePath was provided but BABYLON_NATIVE_PLUGIN_SHADERCACHE was not enabled at native build time." - userInfo:nil]; + // Fail the initializer: a caller-supplied cache path can't be + // honored without the ShaderCache plugin. Return nil (rather than + // throwing) so the failure is catchable from Swift and consistent + // with BNView's failable init. + os_log_error(BabylonNativeLogger(), + "BNRuntime init failed: shaderCachePath was provided but " + "BABYLON_NATIVE_PLUGIN_SHADERCACHE was not enabled at native build time."); + return nil; #endif } - _runtime.emplace(std::move(options)); + + try + { + _runtime.emplace(std::move(options)); + } + catch (const std::exception& e) + { + os_log_error(BabylonNativeLogger(), + "BNRuntime init failed: %{public}s", e.what()); + return nil; + } } return self; } diff --git a/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h index b21496610..cac73f4f7 100644 --- a/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h +++ b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNRuntime.h @@ -41,16 +41,24 @@ NS_ASSUME_NONNULL_BEGIN /// non-GPU polyfills/plugins. The GPU Device is deferred to the first /// `BNView` attach. Default options: debugger off, DebugTrace off, /// log routes to `os_log`. -- (instancetype)init; +/// +/// Returns `nil` if the runtime can't be constructed (see +/// `initWithOptions:`). +- (nullable instancetype)init; /// Constructs the runtime with platform-friendly options (nil = same /// defaults as `init`). /// /// `options.shaderCachePath`: loaded on first BNView attach; saved on -/// `suspend` and on deallocation. Raises -/// `BabylonNativePluginNotEnabledException` when non-nil but the -/// native library was built without `BABYLON_NATIVE_PLUGIN_SHADERCACHE`. -- (instancetype)initWithOptions:(nullable BNRuntimeOptions*)options NS_DESIGNATED_INITIALIZER; +/// `suspend` and on deallocation. +/// +/// Returns `nil` on failure — e.g. `options.shaderCachePath` is non-nil +/// but the native library was built without +/// `BABYLON_NATIVE_PLUGIN_SHADERCACHE`, or the underlying engine fails to +/// start. The reason is written to `os_log`. (Returning `nil` rather than +/// raising keeps the failure catchable from Swift and consistent with +/// `BNView`'s failable init.) +- (nullable instancetype)initWithOptions:(nullable BNRuntimeOptions*)options NS_DESIGNATED_INITIALIZER; /// Load a script from a URL onto the JS thread. Calls made before the /// first `BNView` is created are queued and dispatched after engine From f61cc920df575394bc90cce15c8a2a2c92d41fd9 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 15 Jun 2026 13:55:40 -0700 Subject: [PATCH 70/71] PR feedback --- Apps/Playground/macOS/ViewController.mm | 14 ++--- Embedding/Apple/Source/BNView.mm | 58 ++++++++++--------- .../Apple/Babylon/Embedding/Apple/BNView.h | 44 ++++++++------ 3 files changed, 65 insertions(+), 51 deletions(-) diff --git a/Apps/Playground/macOS/ViewController.mm b/Apps/Playground/macOS/ViewController.mm index 01e2c9fa2..0fd9d45d0 100644 --- a/Apps/Playground/macOS/ViewController.mm +++ b/Apps/Playground/macOS/ViewController.mm @@ -117,7 +117,7 @@ - (void)mouseMoved:(NSEvent*)theEvent { - (void)mouseDown:(NSEvent*)theEvent { NSPoint p = [self logicalPointFromEvent:theEvent]; - [_bnView mouseDown:BNView.leftMouseButton atX:p.x y:p.y]; + [_bnView mouseDown:BNViewMouseButtonLeft atX:p.x y:p.y]; } - (void)mouseDragged:(NSEvent*)theEvent { @@ -127,12 +127,12 @@ - (void)mouseDragged:(NSEvent*)theEvent { - (void)mouseUp:(NSEvent*)theEvent { NSPoint p = [self logicalPointFromEvent:theEvent]; - [_bnView mouseUp:BNView.leftMouseButton atX:p.x y:p.y]; + [_bnView mouseUp:BNViewMouseButtonLeft atX:p.x y:p.y]; } - (void)otherMouseDown:(NSEvent*)theEvent { NSPoint p = [self logicalPointFromEvent:theEvent]; - [_bnView mouseDown:BNView.middleMouseButton atX:p.x y:p.y]; + [_bnView mouseDown:BNViewMouseButtonMiddle atX:p.x y:p.y]; } - (void)otherMouseDragged:(NSEvent*)theEvent { @@ -142,12 +142,12 @@ - (void)otherMouseDragged:(NSEvent*)theEvent { - (void)otherMouseUp:(NSEvent*)theEvent { NSPoint p = [self logicalPointFromEvent:theEvent]; - [_bnView mouseUp:BNView.middleMouseButton atX:p.x y:p.y]; + [_bnView mouseUp:BNViewMouseButtonMiddle atX:p.x y:p.y]; } - (void)rightMouseDown:(NSEvent*)theEvent { NSPoint p = [self logicalPointFromEvent:theEvent]; - [_bnView mouseDown:BNView.rightMouseButton atX:p.x y:p.y]; + [_bnView mouseDown:BNViewMouseButtonRight atX:p.x y:p.y]; } - (void)rightMouseDragged:(NSEvent*)theEvent { @@ -157,12 +157,12 @@ - (void)rightMouseDragged:(NSEvent*)theEvent { - (void)rightMouseUp:(NSEvent*)theEvent { NSPoint p = [self logicalPointFromEvent:theEvent]; - [_bnView mouseUp:BNView.rightMouseButton atX:p.x y:p.y]; + [_bnView mouseUp:BNViewMouseButtonRight atX:p.x y:p.y]; } - (void)scrollWheel:(NSEvent*)theEvent { // Negate so scroll-up matches Babylon's negative-delta convention. - [_bnView mouseWheel:BNView.mouseWheelY delta:static_cast(-theEvent.deltaY)]; + [_bnView mouseWheel:BNViewMouseWheelAxisY delta:-theEvent.deltaY]; } - (IBAction)refresh:(id)__unused sender { diff --git a/Embedding/Apple/Source/BNView.mm b/Embedding/Apple/Source/BNView.mm index 94400270d..77774149e 100644 --- a/Embedding/Apple/Source/BNView.mm +++ b/Embedding/Apple/Source/BNView.mm @@ -12,6 +12,32 @@ #include #include +namespace +{ + // Map the public BNView mouse enums onto the NativeInput button/axis IDs + // the C++ View expects, keeping the NativeInput numbering out of the + // public Obj-C/Swift surface. + uint32_t NativeMouseButtonId(BNViewMouseButton button) + { + switch (button) + { + case BNViewMouseButtonLeft: return Babylon::Embedding::View::LeftMouseButton(); + case BNViewMouseButtonMiddle: return Babylon::Embedding::View::MiddleMouseButton(); + case BNViewMouseButtonRight: return Babylon::Embedding::View::RightMouseButton(); + } + return Babylon::Embedding::View::LeftMouseButton(); + } + + uint32_t NativeMouseWheelAxisId(BNViewMouseWheelAxis axis) + { + switch (axis) + { + case BNViewMouseWheelAxisY: return Babylon::Embedding::View::MouseWheelY(); + } + return Babylon::Embedding::View::MouseWheelY(); + } +} + @implementation BNView { std::optional _view; @@ -219,12 +245,12 @@ - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y #pragma mark - Mouse forwarding -- (void)mouseDown:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y +- (void)mouseDown:(BNViewMouseButton)button atX:(CGFloat)x y:(CGFloat)y { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT if (_view) { - _view->OnMouseDown(static_cast(button), + _view->OnMouseDown(NativeMouseButtonId(button), static_cast(x), static_cast(y), Babylon::Embedding::CoordinateUnits::Logical); @@ -238,12 +264,12 @@ - (void)mouseDown:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y #endif } -- (void)mouseUp:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y +- (void)mouseUp:(BNViewMouseButton)button atX:(CGFloat)x y:(CGFloat)y { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT if (_view) { - _view->OnMouseUp(static_cast(button), + _view->OnMouseUp(NativeMouseButtonId(button), static_cast(x), static_cast(y), Babylon::Embedding::CoordinateUnits::Logical); @@ -275,12 +301,12 @@ - (void)mouseMoveAtX:(CGFloat)x y:(CGFloat)y #endif } -- (void)mouseWheel:(NSInteger)axis delta:(NSInteger)delta +- (void)mouseWheel:(BNViewMouseWheelAxis)axis delta:(CGFloat)delta { #if BABYLON_NATIVE_PLUGIN_NATIVEINPUT if (_view) { - _view->OnMouseWheel(static_cast(axis), + _view->OnMouseWheel(NativeMouseWheelAxisId(axis), static_cast(delta)); } #else @@ -292,24 +318,4 @@ - (void)mouseWheel:(NSInteger)axis delta:(NSInteger)delta #endif } -+ (NSInteger)leftMouseButton -{ - return static_cast(Babylon::Embedding::View::LeftMouseButton()); -} - -+ (NSInteger)middleMouseButton -{ - return static_cast(Babylon::Embedding::View::MiddleMouseButton()); -} - -+ (NSInteger)rightMouseButton -{ - return static_cast(Babylon::Embedding::View::RightMouseButton()); -} - -+ (NSInteger)mouseWheelY -{ - return static_cast(Babylon::Embedding::View::MouseWheelY()); -} - @end diff --git a/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h index 609ce00bc..6824d5294 100644 --- a/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h +++ b/Embedding/Include/Platform/Apple/Babylon/Embedding/Apple/BNView.h @@ -21,6 +21,22 @@ NS_ASSUME_NONNULL_BEGIN +/// Mouse buttons accepted by `-mouseDown:atX:y:` / `-mouseUp:atX:y:`. +/// Obj-C can't nest a type in `@interface`, so the enum is prefixed and +/// surfaced to Swift nested as `BNView.MouseButton` (`.left` / `.middle` +/// / `.right`). +typedef NS_ENUM(NSInteger, BNViewMouseButton) { + BNViewMouseButtonLeft, + BNViewMouseButtonMiddle, + BNViewMouseButtonRight, +} NS_SWIFT_NAME(BNView.MouseButton); + +/// Scroll-wheel axes accepted by `-mouseWheel:delta:`. +/// Surfaced to Swift nested as `BNView.MouseWheelAxis` (`.y`). +typedef NS_ENUM(NSInteger, BNViewMouseWheelAxis) { + BNViewMouseWheelAxisY, +} NS_SWIFT_NAME(BNView.MouseWheelAxis); + /// Default `MTKViewDelegate` implementation that drives a BNView's /// per-frame rendering: forwards `drawInMTKView:` → `[bnView renderFrame]`. /// @@ -93,37 +109,29 @@ NS_ASSUME_NONNULL_BEGIN - (void)pointerUp:(NSInteger)pointerId atX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(pointerUp(id:x:y:)); -/// Forward a mouse-button event. `button` is one of `+leftMouseButton`, -/// `+middleMouseButton`, `+rightMouseButton`. `x`, `y` are logical -/// (CSS) pixels; AppKit hosts pass `NSEvent.locationInWindow` with the -/// Y axis flipped to a top-left origin. +/// Forward a mouse-button event. `button` selects which button. `x`, `y` +/// are logical (CSS) pixels; AppKit hosts pass `NSEvent.locationInWindow` +/// with the Y axis flipped to a top-left origin. /// /// Raises `BabylonNativePluginNotEnabledException` when /// `BABYLON_NATIVE_PLUGIN_NATIVEINPUT` is not enabled. Same applies to /// `mouseUp:`, `mouseMove:`, and `mouseWheel:`. -- (void)mouseDown:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y +- (void)mouseDown:(BNViewMouseButton)button atX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(mouseDown(button:x:y:)); -- (void)mouseUp:(NSInteger)button atX:(CGFloat)x y:(CGFloat)y +- (void)mouseUp:(BNViewMouseButton)button atX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(mouseUp(button:x:y:)); - (void)mouseMoveAtX:(CGFloat)x y:(CGFloat)y NS_SWIFT_NAME(mouseMove(x:y:)); -/// Forward a scroll-wheel event. `axis` is `+mouseWheelY`. `delta` is -/// the signed scroll amount; AppKit hosts pass `-NSEvent.deltaY` so -/// scroll-up matches Babylon's negative convention. -- (void)mouseWheel:(NSInteger)axis delta:(NSInteger)delta +/// Forward a scroll-wheel event. `axis` selects which axis. `delta` is the +/// signed scroll amount; AppKit hosts pass `-NSEvent.deltaY` so scroll-up +/// matches Babylon's negative convention. The fractional delta is truncated +/// to an integer for the underlying input pipeline. +- (void)mouseWheel:(BNViewMouseWheelAxis)axis delta:(CGFloat)delta NS_SWIFT_NAME(mouseWheel(axis:delta:)); -/// Button identifiers accepted by `mouseDown:` and `mouseUp:`. -@property (class, nonatomic, readonly) NSInteger leftMouseButton; -@property (class, nonatomic, readonly) NSInteger middleMouseButton; -@property (class, nonatomic, readonly) NSInteger rightMouseButton; - -/// Wheel axis identifier accepted by `mouseWheel:delta:`. -@property (class, nonatomic, readonly) NSInteger mouseWheelY; - - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; From f0492f804e577407b3269b62971c018f60c025b0 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 15 Jun 2026 15:43:48 -0700 Subject: [PATCH 71/71] PR feedback --- .../src/main/java/com/babylonjs/embedding/BabylonNative.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/BabylonNative.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/BabylonNative.java index cf260a742..6f0605e39 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/BabylonNative.java +++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/BabylonNative.java @@ -1,5 +1,6 @@ package com.babylonjs.embedding; +import android.app.Activity; import android.content.Context; import android.view.Surface; @@ -63,7 +64,7 @@ private BabylonNative() {} */ public static native void setContext(Context context); - public static native void setCurrentActivity(Object activity); + public static native void setCurrentActivity(Activity activity); public static native void pause();