diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index 4ffe04381..69b8a5b0f 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -12,6 +12,7 @@ set(BABYLONJS_MATERIALS_ASSETS set(TEST_ASSETS "JavaScript/dist/tests.externalTexture.deviceLoss.js" + "JavaScript/dist/tests.externalTexture.layerCount.js" "JavaScript/dist/tests.externalTexture.msaa.js" "JavaScript/dist/tests.externalTexture.render.js" "JavaScript/dist/tests.javaScript.all.js" @@ -26,6 +27,7 @@ set(SOURCES "Source/App.cpp" "Source/Tests.ExternalTexture.cpp" "Source/Tests.ExternalTexture.DeviceLoss.cpp" + "Source/Tests.ExternalTexture.LayerCount.cpp" "Source/Tests.ExternalTexture.Msaa.cpp" "Source/Tests.ExternalTexture.Render.cpp" "Source/Tests.JavaScript.cpp" diff --git a/Apps/UnitTests/JavaScript/dist/tests.externalTexture.layerCount.js b/Apps/UnitTests/JavaScript/dist/tests.externalTexture.layerCount.js new file mode 100644 index 000000000..d3729bb69 --- /dev/null +++ b/Apps/UnitTests/JavaScript/dist/tests.externalTexture.layerCount.js @@ -0,0 +1,154 @@ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ "@babylonjs/core" +/*!**************************!*\ + !*** external "BABYLON" ***! + \**************************/ +(module) { + +module.exports = BABYLON; + +/***/ } + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Check if module exists (development only) +/******/ if (__webpack_modules__[moduleId] === undefined) { +/******/ var e = new Error("Cannot find module '" + moduleId + "'"); +/******/ e.code = 'MODULE_NOT_FOUND'; +/******/ throw e; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. +(() => { +/*!*************************************************!*\ + !*** ./src/tests.externalTexture.layerCount.ts ***! + \*************************************************/ +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babylonjs/core */ "@babylonjs/core"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_0__); +// JS-side scaffolding for Tests.ExternalTexture.LayerCount.cpp. +// +// Verifies that wrapNativeTexture auto-populates is2DArray/depth/baseDepth on the wrapped +// InternalTexture from the native layer count: +// - BN exposes getTextureLayerCount(handle) (BabylonJS/BabylonNative#1733), reporting the *bound* +// layer count: 1 for a single-slice view (layerIndex set), else the underlying array size. +// - BJS wrapNativeTexture consumes it (BabylonJS/Babylon.js#18535): layers > 1 -> is2DArray=true, +// baseDepth=depth=layers; layers == 1 -> left as a plain 2D texture. +// C++ wraps whole-array, single-slice, and single-layer native textures and asserts the values +// reported back here. + + + + +var engine; + +function getEngine() { + if (!engine) { + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_0__.NativeEngine(); + // This test never renders; don't wait on async shader compilation. + delete engine.getCaps().parallelShaderCompile; + } + return engine; +} + + + + + + + + + +// Wraps the given native texture and returns the layer-related InternalTexture fields, plus the raw +// native layer count so C++ can tell "binding missing" / "BJS too old" from a real failure. +function inspectWrappedTexture(nativeTexture) { + var e = getEngine(); + // getTextureLayerCount is a binding on the raw native engine (_engine), which is private and not + // yet in the shipped @babylonjs/core types; wrapNativeTexture reads it internally via this._engine. + var nativeEngine = e._engine; + var hasBinding = !!nativeEngine && typeof nativeEngine.getTextureLayerCount === "function"; + var rawLayerCount = hasBinding ? nativeEngine.getTextureLayerCount(nativeTexture) : -1; + var internalTexture = e.wrapNativeTexture(nativeTexture); + return { + hasBinding: hasBinding, + rawLayerCount: rawLayerCount, + is2DArray: !!internalTexture.is2DArray, + depth: internalTexture.depth, + baseDepth: internalTexture.baseDepth + }; +} + +globalThis.inspectWrappedTexture = inspectWrappedTexture; +})(); + +/******/ })() +; \ No newline at end of file diff --git a/Apps/UnitTests/JavaScript/src/tests.externalTexture.layerCount.ts b/Apps/UnitTests/JavaScript/src/tests.externalTexture.layerCount.ts new file mode 100644 index 000000000..dc6e13cb7 --- /dev/null +++ b/Apps/UnitTests/JavaScript/src/tests.externalTexture.layerCount.ts @@ -0,0 +1,53 @@ +// JS-side scaffolding for Tests.ExternalTexture.LayerCount.cpp. +// +// Verifies that wrapNativeTexture auto-populates is2DArray/depth/baseDepth on the wrapped +// InternalTexture from the native layer count: +// - BN exposes getTextureLayerCount(handle) (BabylonJS/BabylonNative#1733), reporting the *bound* +// layer count: 1 for a single-slice view (layerIndex set), else the underlying array size. +// - BJS wrapNativeTexture consumes it (BabylonJS/Babylon.js#18535): layers > 1 -> is2DArray=true, +// baseDepth=depth=layers; layers == 1 -> left as a plain 2D texture. +// C++ wraps whole-array, single-slice, and single-layer native textures and asserts the values +// reported back here. + +import { NativeEngine } from "@babylonjs/core"; +import type { InternalTexture } from "@babylonjs/core"; + +let engine: NativeEngine | undefined; + +function getEngine(): NativeEngine { + if (!engine) { + engine = new NativeEngine(); + // This test never renders; don't wait on async shader compilation. + delete engine.getCaps().parallelShaderCompile; + } + return engine; +} + +interface WrappedTextureInfo { + hasBinding: boolean; + rawLayerCount: number; + is2DArray: boolean; + depth: number; + baseDepth: number; +} + +// Wraps the given native texture and returns the layer-related InternalTexture fields, plus the raw +// native layer count so C++ can tell "binding missing" / "BJS too old" from a real failure. +function inspectWrappedTexture(nativeTexture: unknown): WrappedTextureInfo { + const e = getEngine(); + // getTextureLayerCount is a binding on the raw native engine (_engine), which is private and not + // yet in the shipped @babylonjs/core types; wrapNativeTexture reads it internally via this._engine. + const nativeEngine = (e as any)._engine; + const hasBinding = !!nativeEngine && typeof nativeEngine.getTextureLayerCount === "function"; + const rawLayerCount = hasBinding ? nativeEngine.getTextureLayerCount(nativeTexture) : -1; + const internalTexture: InternalTexture = e.wrapNativeTexture(nativeTexture); + return { + hasBinding, + rawLayerCount, + is2DArray: !!internalTexture.is2DArray, + depth: internalTexture.depth, + baseDepth: internalTexture.baseDepth, + }; +} + +(globalThis as any).inspectWrappedTexture = inspectWrappedTexture; diff --git a/Apps/UnitTests/JavaScript/webpack.config.js b/Apps/UnitTests/JavaScript/webpack.config.js index 1bbc2e26f..65615719e 100644 --- a/Apps/UnitTests/JavaScript/webpack.config.js +++ b/Apps/UnitTests/JavaScript/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { "tests.shaderCompilation.comprehensiveGLSL": './src/tests.shaderCompilation.comprehensiveGLSL.ts', "tests.externalTexture.msaa": './src/tests.externalTexture.msaa.ts', "tests.externalTexture.deviceLoss": './src/tests.externalTexture.deviceLoss.ts', + "tests.externalTexture.layerCount": './src/tests.externalTexture.layerCount.ts', }, externals: { "@babylonjs/core": "BABYLON", diff --git a/Apps/UnitTests/Source/Tests.ExternalTexture.LayerCount.cpp b/Apps/UnitTests/Source/Tests.ExternalTexture.LayerCount.cpp new file mode 100644 index 000000000..218bcdf41 --- /dev/null +++ b/Apps/UnitTests/Source/Tests.ExternalTexture.LayerCount.cpp @@ -0,0 +1,151 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "Helpers.h" + +#include +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; + +namespace +{ + constexpr uint32_t TEX_SIZE = 64; + + struct WrappedTextureInfo + { + bool hasBinding = false; + int rawLayerCount = -1; + bool is2DArray = false; + uint32_t depth = 0; + uint32_t baseDepth = 0; + }; +} + +// End-to-end test for wrapNativeTexture's array-layer population across the BN/BJS boundary: +// * BN exposes getTextureLayerCount(handle) (BabylonJS/BabylonNative#1733). It reports the *bound* +// layer count: the single-slice view count when an ExternalTexture is wrapped with a layerIndex, +// otherwise the underlying array size. +// * BJS wrapNativeTexture consumes it (BabylonJS/Babylon.js#18535): layers > 1 -> is2DArray=true, +// baseDepth=depth=layers; layers == 1 -> a plain 2D texture. +// Wraps three ExternalTextures and asserts the resulting InternalTexture layer flags: +// (A) arraySize=2, no layerIndex -> whole array -> layerCount 2, is2DArray=true, depth=baseDepth=2. +// (B) arraySize=2, layerIndex=0 -> single slice -> layerCount 1, is2DArray=false. +// This mirrors a host wrapping one slice of an NV12 array per video plane, and guards +// against regressing a single array slice into a 2D array. +// (C) arraySize=1, no layerIndex -> single layer -> layerCount 1, is2DArray=false. +// Skips cleanly when the native binding or the consuming BJS change is absent so it stays green +// across the cross-repo landing order. +TEST(ExternalTexture, WrapNativeTextureLayerCount) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + Babylon::Graphics::Device device{g_deviceConfig}; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [](const Napi::Error& error) { + std::cerr << "[Uncaught Error] " << Napi::GetErrorString(error) << std::endl; + std::quick_exit(1); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&device](Napi::Env env) { + env.Global().Set("globalThis", env.Global()); + device.AddToJavaScript(env); + Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) { + std::cout << message << std::endl; + }); + Babylon::Polyfills::Window::Initialize(env); + Babylon::Plugins::NativeEngine::Initialize(env); + }); + + Babylon::ScriptLoader loader{runtime}; + loader.LoadScript("app:///Assets/babylon.max.js"); + loader.LoadScript("app:///Assets/tests.externalTexture.layerCount.js"); + + // Creates a native texture with the requested array size, wraps it via the synchronous + // CreateForJavaScript (optionally selecting a single array layer), runs wrapNativeTexture on the + // JS side, and returns the layer-related InternalTexture fields it reports. CreateForJavaScript + // requires an active frame, so the whole wrap is bracketed by Start/FinishRenderingCurrentFrame. + auto inspect = [&](uint32_t arraySize, std::optional layerIndex) -> WrappedTextureInfo { + device.StartRenderingCurrentFrame(); + + auto nativeTexture = Helpers::CreateTexture( + device.GetPlatformInfo().Device, TEX_SIZE, TEX_SIZE, arraySize, false); + Babylon::Plugins::ExternalTexture externalTexture{nativeTexture}; + + std::promise resultPromise; + loader.Dispatch([&externalTexture, layerIndex, &resultPromise](Napi::Env env) { + try + { + auto jsNativeTexture = externalTexture.CreateForJavaScript(env, layerIndex); + auto result = env.Global() + .Get("inspectWrappedTexture") + .As() + .Call({jsNativeTexture}) + .As(); + WrappedTextureInfo wti{}; + wti.hasBinding = result.Get("hasBinding").ToBoolean(); + wti.rawLayerCount = result.Get("rawLayerCount").ToNumber().Int32Value(); + wti.is2DArray = result.Get("is2DArray").ToBoolean(); + wti.depth = result.Get("depth").ToNumber().Uint32Value(); + wti.baseDepth = result.Get("baseDepth").ToNumber().Uint32Value(); + resultPromise.set_value(wti); + } + catch (...) + { + resultPromise.set_exception(std::current_exception()); + } + }); + + auto info = resultPromise.get_future().get(); + device.FinishRenderingCurrentFrame(); + + Helpers::DestroyTexture(nativeTexture); + return info; + }; + + // --- (A) Multi-layer texture wrapped as the whole array --- + const WrappedTextureInfo arrayInfo = inspect(2, std::nullopt); + + if (!arrayInfo.hasBinding) + { + GTEST_SKIP() << "engine.getTextureLayerCount is unavailable -- requires Babylon Native with " + "BabylonJS/BabylonNative#1733."; + } + + EXPECT_EQ(arrayInfo.rawLayerCount, 2) << "Whole-array wrap should report the native array size."; + + if (arrayInfo.rawLayerCount == 2 && !arrayInfo.is2DArray) + { + GTEST_SKIP() << "wrapNativeTexture did not populate is2DArray from the layer count -- requires " + "@babylonjs/core with BabylonJS/Babylon.js#18535."; + } + + EXPECT_TRUE(arrayInfo.is2DArray) << "Whole-array wrap should be is2DArray=true."; + EXPECT_EQ(arrayInfo.depth, 2u) << "Whole-array depth should equal the native layer count."; + EXPECT_EQ(arrayInfo.baseDepth, 2u) << "Whole-array baseDepth should equal the native layer count."; + + // --- (B) Same array, wrapped as a single slice (layerIndex=0): one slice of an NV12-style array --- + const WrappedTextureInfo sliceInfo = inspect(2, static_cast(0)); + + EXPECT_EQ(sliceInfo.rawLayerCount, 1) << "Single-slice view should report 1 bound layer, not the array size."; + EXPECT_FALSE(sliceInfo.is2DArray) << "Single-slice view must not be flagged is2DArray."; + + // --- (C) Single-layer texture --- + const WrappedTextureInfo singleInfo = inspect(1, std::nullopt); + + EXPECT_EQ(singleInfo.rawLayerCount, 1) << "Single-layer texture should report 1 layer."; + EXPECT_FALSE(singleInfo.is2DArray) << "Single-layer texture should not be flagged is2DArray."; +#endif +} diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 864031710..0e1cb7a83 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -719,6 +719,7 @@ namespace Babylon InstanceMethod("loadCubeTextureWithMips", &NativeEngine::LoadCubeTextureWithMips), InstanceMethod("getTextureWidth", &NativeEngine::GetTextureWidth), InstanceMethod("getTextureHeight", &NativeEngine::GetTextureHeight), + InstanceMethod("getTextureLayerCount", &NativeEngine::GetTextureLayerCount), InstanceMethod("deleteTexture", &NativeEngine::DeleteTexture), InstanceMethod("readTexture", &NativeEngine::ReadTexture), @@ -1624,6 +1625,17 @@ namespace Babylon return Napi::Value::From(info.Env(), texture->Height()); } + Napi::Value NativeEngine::GetTextureLayerCount(const Napi::CallbackInfo& info) + { + const Graphics::Texture* texture = info[0].As>().Get(); + // When the texture is bound as a single-slice view (ViewNumLayers > 0, e.g. an + // ExternalTexture wrapped with a layerIndex), report the bound layer count rather + // than the underlying array size so consumers see a 2D texture, not an array. + const uint16_t viewNumLayers = texture->ViewNumLayers(); + const uint16_t layerCount = viewNumLayers > 0 ? viewNumLayers : texture->NumLayers(); + return Napi::Value::From(info.Env(), layerCount); + } + void NativeEngine::SetTextureSampling(NativeDataStream::Reader& data) { auto& texture = *data.ReadPointer(); diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index 229ef0e3a..82dded65e 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -106,6 +106,7 @@ namespace Babylon void LoadCubeTextureWithMips(const Napi::CallbackInfo& info); Napi::Value GetTextureWidth(const Napi::CallbackInfo& info); Napi::Value GetTextureHeight(const Napi::CallbackInfo& info); + Napi::Value GetTextureLayerCount(const Napi::CallbackInfo& info); void SetTextureSampling(NativeDataStream::Reader& data); void SetTextureWrapMode(NativeDataStream::Reader& data); void SetTextureAnisotropicLevel(NativeDataStream::Reader& data);