Skip to content

Commit efa0d70

Browse files
bghgaryCopilot
andcommitted
Add integration test for wrapNativeTexture layer-count population
Exercises the cross-repo path end to end: getTextureLayerCount feeds Babylon.js wrapNativeTexture (BabylonJS/Babylon.js#18535), which sets is2DArray / depth / baseDepth when a wrapped texture presents more than one layer. Wraps three ExternalTextures and asserts the resulting InternalTexture: - whole array (arraySize 2, no layerIndex) -> is2DArray, depth=baseDepth=2 - single slice (arraySize 2, layerIndex 0) -> plain 2D (as Teams wraps an NV12 plane) - single layer (arraySize 1) -> plain 2D The single-slice case guards against regressing a single array slice into a 2D array. Skips cleanly when the native binding or the consuming Babylon.js change is absent so it stays green across the cross-repo landing order. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6f26e4f commit efa0d70

5 files changed

Lines changed: 342 additions & 0 deletions

File tree

Apps/UnitTests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ set(BABYLONJS_MATERIALS_ASSETS
1212

1313
set(TEST_ASSETS
1414
"JavaScript/dist/tests.externalTexture.deviceLoss.js"
15+
"JavaScript/dist/tests.externalTexture.layerCount.js"
1516
"JavaScript/dist/tests.externalTexture.msaa.js"
1617
"JavaScript/dist/tests.externalTexture.render.js"
1718
"JavaScript/dist/tests.javaScript.all.js"
@@ -26,6 +27,7 @@ set(SOURCES
2627
"Source/App.cpp"
2728
"Source/Tests.ExternalTexture.cpp"
2829
"Source/Tests.ExternalTexture.DeviceLoss.cpp"
30+
"Source/Tests.ExternalTexture.LayerCount.cpp"
2931
"Source/Tests.ExternalTexture.Msaa.cpp"
3032
"Source/Tests.ExternalTexture.Render.cpp"
3133
"Source/Tests.JavaScript.cpp"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/******/ (() => { // webpackBootstrap
2+
/******/ "use strict";
3+
/******/ var __webpack_modules__ = ({
4+
5+
/***/ "@babylonjs/core"
6+
/*!**************************!*\
7+
!*** external "BABYLON" ***!
8+
\**************************/
9+
(module) {
10+
11+
module.exports = BABYLON;
12+
13+
/***/ }
14+
15+
/******/ });
16+
/************************************************************************/
17+
/******/ // The module cache
18+
/******/ var __webpack_module_cache__ = {};
19+
/******/
20+
/******/ // The require function
21+
/******/ function __webpack_require__(moduleId) {
22+
/******/ // Check if module is in cache
23+
/******/ var cachedModule = __webpack_module_cache__[moduleId];
24+
/******/ if (cachedModule !== undefined) {
25+
/******/ return cachedModule.exports;
26+
/******/ }
27+
/******/ // Check if module exists (development only)
28+
/******/ if (__webpack_modules__[moduleId] === undefined) {
29+
/******/ var e = new Error("Cannot find module '" + moduleId + "'");
30+
/******/ e.code = 'MODULE_NOT_FOUND';
31+
/******/ throw e;
32+
/******/ }
33+
/******/ // Create a new module (and put it into the cache)
34+
/******/ var module = __webpack_module_cache__[moduleId] = {
35+
/******/ // no module.id needed
36+
/******/ // no module.loaded needed
37+
/******/ exports: {}
38+
/******/ };
39+
/******/
40+
/******/ // Execute the module function
41+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
42+
/******/
43+
/******/ // Return the exports of the module
44+
/******/ return module.exports;
45+
/******/ }
46+
/******/
47+
/************************************************************************/
48+
/******/ /* webpack/runtime/compat get default export */
49+
/******/ (() => {
50+
/******/ // getDefaultExport function for compatibility with non-harmony modules
51+
/******/ __webpack_require__.n = (module) => {
52+
/******/ var getter = module && module.__esModule ?
53+
/******/ () => (module['default']) :
54+
/******/ () => (module);
55+
/******/ __webpack_require__.d(getter, { a: getter });
56+
/******/ return getter;
57+
/******/ };
58+
/******/ })();
59+
/******/
60+
/******/ /* webpack/runtime/define property getters */
61+
/******/ (() => {
62+
/******/ // define getter functions for harmony exports
63+
/******/ __webpack_require__.d = (exports, definition) => {
64+
/******/ for(var key in definition) {
65+
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
66+
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
67+
/******/ }
68+
/******/ }
69+
/******/ };
70+
/******/ })();
71+
/******/
72+
/******/ /* webpack/runtime/hasOwnProperty shorthand */
73+
/******/ (() => {
74+
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
75+
/******/ })();
76+
/******/
77+
/******/ /* webpack/runtime/make namespace object */
78+
/******/ (() => {
79+
/******/ // define __esModule on exports
80+
/******/ __webpack_require__.r = (exports) => {
81+
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
82+
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
83+
/******/ }
84+
/******/ Object.defineProperty(exports, '__esModule', { value: true });
85+
/******/ };
86+
/******/ })();
87+
/******/
88+
/************************************************************************/
89+
var __webpack_exports__ = {};
90+
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
91+
(() => {
92+
/*!*************************************************!*\
93+
!*** ./src/tests.externalTexture.layerCount.ts ***!
94+
\*************************************************/
95+
__webpack_require__.r(__webpack_exports__);
96+
/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babylonjs/core */ "@babylonjs/core");
97+
/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_0__);
98+
// JS-side scaffolding for Tests.ExternalTexture.LayerCount.cpp.
99+
//
100+
// Verifies that wrapNativeTexture auto-populates is2DArray/depth/baseDepth on the wrapped
101+
// InternalTexture from the native layer count:
102+
// - BN exposes getTextureLayerCount(handle) (BabylonJS/BabylonNative#1733), reporting the *bound*
103+
// layer count: 1 for a single-slice view (layerIndex set), else the underlying array size.
104+
// - BJS wrapNativeTexture consumes it (BabylonJS/Babylon.js#18535): layers > 1 -> is2DArray=true,
105+
// baseDepth=depth=layers; layers == 1 -> left as a plain 2D texture.
106+
// C++ wraps whole-array, single-slice, and single-layer native textures and asserts the values
107+
// reported back here.
108+
109+
110+
111+
var engine;
112+
113+
function getEngine() {
114+
if (!engine) {
115+
engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_0__.NativeEngine();
116+
// Avoid waiting on async shader compilation; this test never renders.
117+
delete engine.getCaps().parallelShaderCompile;
118+
}
119+
return engine;
120+
}
121+
122+
// Wraps the given native texture and returns the layer-related InternalTexture fields, plus
123+
// the raw native layer count so C++ can tell "binding missing" / "BJS too old" from a real failure.
124+
// Note: getTextureLayerCount is a binding on the raw native engine (engine._engine), not on the
125+
// BJS NativeEngine wrapper -- wrapNativeTexture reads it internally via this._engine.
126+
function inspectWrappedTexture(nativeTexture) {
127+
var e = getEngine();
128+
var nativeEngine = e._engine;
129+
var hasBinding = !!nativeEngine && typeof nativeEngine.getTextureLayerCount === "function";
130+
var rawLayerCount = hasBinding ? nativeEngine.getTextureLayerCount(nativeTexture) : -1;
131+
var internalTexture = e.wrapNativeTexture(nativeTexture);
132+
return {
133+
hasBinding: hasBinding,
134+
rawLayerCount: rawLayerCount,
135+
is2DArray: !!internalTexture.is2DArray,
136+
depth: internalTexture.depth,
137+
baseDepth: internalTexture.baseDepth
138+
};
139+
}
140+
141+
globalThis.inspectWrappedTexture = inspectWrappedTexture;
142+
})();
143+
144+
/******/ })()
145+
;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// JS-side scaffolding for Tests.ExternalTexture.LayerCount.cpp.
2+
//
3+
// Verifies that wrapNativeTexture auto-populates is2DArray/depth/baseDepth on the wrapped
4+
// InternalTexture from the native layer count:
5+
// - BN exposes getTextureLayerCount(handle) (BabylonJS/BabylonNative#1733), reporting the *bound*
6+
// layer count: 1 for a single-slice view (layerIndex set), else the underlying array size.
7+
// - BJS wrapNativeTexture consumes it (BabylonJS/Babylon.js#18535): layers > 1 -> is2DArray=true,
8+
// baseDepth=depth=layers; layers == 1 -> left as a plain 2D texture.
9+
// C++ wraps whole-array, single-slice, and single-layer native textures and asserts the values
10+
// reported back here.
11+
12+
import { NativeEngine } from "@babylonjs/core";
13+
14+
let engine: NativeEngine | undefined;
15+
16+
function getEngine(): any {
17+
if (!engine) {
18+
engine = new NativeEngine();
19+
// Avoid waiting on async shader compilation; this test never renders.
20+
delete engine.getCaps().parallelShaderCompile;
21+
}
22+
return engine;
23+
}
24+
25+
// Wraps the given native texture and returns the layer-related InternalTexture fields, plus
26+
// the raw native layer count so C++ can tell "binding missing" / "BJS too old" from a real failure.
27+
// Note: getTextureLayerCount is a binding on the raw native engine (engine._engine), not on the
28+
// BJS NativeEngine wrapper -- wrapNativeTexture reads it internally via this._engine.
29+
function inspectWrappedTexture(nativeTexture: any): any {
30+
const e = getEngine();
31+
const nativeEngine = e._engine;
32+
const hasBinding = !!nativeEngine && typeof nativeEngine.getTextureLayerCount === "function";
33+
const rawLayerCount = hasBinding ? nativeEngine.getTextureLayerCount(nativeTexture) : -1;
34+
const internalTexture: any = e.wrapNativeTexture(nativeTexture);
35+
return {
36+
hasBinding,
37+
rawLayerCount,
38+
is2DArray: !!internalTexture.is2DArray,
39+
depth: internalTexture.depth,
40+
baseDepth: internalTexture.baseDepth,
41+
};
42+
}
43+
44+
(globalThis as any).inspectWrappedTexture = inspectWrappedTexture;

Apps/UnitTests/JavaScript/webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = {
1212
"tests.shaderCompilation.comprehensiveGLSL": './src/tests.shaderCompilation.comprehensiveGLSL.ts',
1313
"tests.externalTexture.msaa": './src/tests.externalTexture.msaa.ts',
1414
"tests.externalTexture.deviceLoss": './src/tests.externalTexture.deviceLoss.ts',
15+
"tests.externalTexture.layerCount": './src/tests.externalTexture.layerCount.ts',
1516
},
1617
externals: {
1718
"@babylonjs/core": "BABYLON",
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <Babylon/AppRuntime.h>
4+
#include <Babylon/Graphics/Device.h>
5+
#include <Babylon/Polyfills/Console.h>
6+
#include <Babylon/Polyfills/Window.h>
7+
#include <Babylon/Plugins/NativeEngine.h>
8+
#include <Babylon/Plugins/ExternalTexture.h>
9+
#include <Babylon/ScriptLoader.h>
10+
11+
#include "Helpers.h"
12+
13+
#include <cstdlib>
14+
#include <future>
15+
#include <iostream>
16+
#include <optional>
17+
18+
extern Babylon::Graphics::Configuration g_deviceConfig;
19+
20+
namespace
21+
{
22+
constexpr uint32_t TEX_SIZE = 64;
23+
24+
struct WrappedTextureInfo
25+
{
26+
bool hasBinding = false;
27+
int rawLayerCount = -1;
28+
bool is2DArray = false;
29+
uint32_t depth = 0;
30+
uint32_t baseDepth = 0;
31+
};
32+
}
33+
34+
// End-to-end test for wrapNativeTexture's array-layer population across the BN/BJS boundary:
35+
// * BN exposes getTextureLayerCount(handle) (BabylonJS/BabylonNative#1733). It reports the *bound*
36+
// layer count: the single-slice view count when an ExternalTexture is wrapped with a layerIndex,
37+
// otherwise the underlying array size.
38+
// * BJS wrapNativeTexture consumes it (BabylonJS/Babylon.js#18535): layers > 1 -> is2DArray=true,
39+
// baseDepth=depth=layers; layers == 1 -> a plain 2D texture.
40+
// Wraps three ExternalTextures and asserts the resulting InternalTexture layer flags:
41+
// (A) arraySize=2, no layerIndex -> whole array -> layerCount 2, is2DArray=true, depth=baseDepth=2.
42+
// (B) arraySize=2, layerIndex=0 -> single slice -> layerCount 1, is2DArray=false.
43+
// This mirrors how Teams wraps an NV12 plane and guards against regressing a slice to an array.
44+
// (C) arraySize=1, no layerIndex -> single layer -> layerCount 1, is2DArray=false.
45+
// Skips cleanly when the native binding or the consuming BJS change is absent so it stays green
46+
// across the cross-repo landing order.
47+
TEST(ExternalTexture, WrapNativeTextureLayerCount)
48+
{
49+
#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS)
50+
GTEST_SKIP();
51+
#else
52+
Babylon::Graphics::Device device{g_deviceConfig};
53+
54+
Babylon::AppRuntime::Options options{};
55+
options.UnhandledExceptionHandler = [](const Napi::Error& error) {
56+
std::cerr << "[Uncaught Error] " << Napi::GetErrorString(error) << std::endl;
57+
std::quick_exit(1);
58+
};
59+
Babylon::AppRuntime runtime{options};
60+
61+
runtime.Dispatch([&device](Napi::Env env) {
62+
env.Global().Set("globalThis", env.Global());
63+
device.AddToJavaScript(env);
64+
Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) {
65+
std::cout << message << std::endl;
66+
});
67+
Babylon::Polyfills::Window::Initialize(env);
68+
Babylon::Plugins::NativeEngine::Initialize(env);
69+
});
70+
71+
Babylon::ScriptLoader loader{runtime};
72+
loader.LoadScript("app:///Assets/babylon.max.js");
73+
loader.LoadScript("app:///Assets/tests.externalTexture.layerCount.js");
74+
75+
// Creates a native texture with the requested array size, wraps it via the synchronous
76+
// CreateForJavaScript (optionally selecting a single array layer), runs wrapNativeTexture on the
77+
// JS side, and returns the layer-related InternalTexture fields it reports. CreateForJavaScript
78+
// requires an active frame, so the whole wrap is bracketed by Start/FinishRenderingCurrentFrame.
79+
auto inspect = [&](uint32_t arraySize, std::optional<uint16_t> layerIndex) -> WrappedTextureInfo {
80+
device.StartRenderingCurrentFrame();
81+
82+
auto nativeTexture = Helpers::CreateTexture(
83+
device.GetPlatformInfo().Device, TEX_SIZE, TEX_SIZE, arraySize, false);
84+
Babylon::Plugins::ExternalTexture externalTexture{nativeTexture};
85+
86+
std::promise<WrappedTextureInfo> resultPromise;
87+
loader.Dispatch([&externalTexture, layerIndex, &resultPromise](Napi::Env env) {
88+
try
89+
{
90+
auto jsNativeTexture = externalTexture.CreateForJavaScript(env, layerIndex);
91+
auto result = env.Global()
92+
.Get("inspectWrappedTexture")
93+
.As<Napi::Function>()
94+
.Call({jsNativeTexture})
95+
.As<Napi::Object>();
96+
WrappedTextureInfo wti{};
97+
wti.hasBinding = result.Get("hasBinding").ToBoolean();
98+
wti.rawLayerCount = result.Get("rawLayerCount").ToNumber().Int32Value();
99+
wti.is2DArray = result.Get("is2DArray").ToBoolean();
100+
wti.depth = result.Get("depth").ToNumber().Uint32Value();
101+
wti.baseDepth = result.Get("baseDepth").ToNumber().Uint32Value();
102+
resultPromise.set_value(wti);
103+
}
104+
catch (...)
105+
{
106+
resultPromise.set_exception(std::current_exception());
107+
}
108+
});
109+
110+
auto info = resultPromise.get_future().get();
111+
device.FinishRenderingCurrentFrame();
112+
113+
Helpers::DestroyTexture(nativeTexture);
114+
return info;
115+
};
116+
117+
// --- (A) Multi-layer texture wrapped as the whole array ---
118+
const WrappedTextureInfo arrayInfo = inspect(2, std::nullopt);
119+
120+
if (!arrayInfo.hasBinding)
121+
{
122+
GTEST_SKIP() << "engine.getTextureLayerCount is unavailable -- requires Babylon Native with "
123+
"BabylonJS/BabylonNative#1733.";
124+
}
125+
126+
EXPECT_EQ(arrayInfo.rawLayerCount, 2) << "Whole-array wrap should report the native array size.";
127+
128+
if (arrayInfo.rawLayerCount == 2 && !arrayInfo.is2DArray)
129+
{
130+
GTEST_SKIP() << "wrapNativeTexture did not populate is2DArray from the layer count -- requires "
131+
"@babylonjs/core with BabylonJS/Babylon.js#18535.";
132+
}
133+
134+
EXPECT_TRUE(arrayInfo.is2DArray) << "Whole-array wrap should be is2DArray=true.";
135+
EXPECT_EQ(arrayInfo.depth, 2u) << "Whole-array depth should equal the native layer count.";
136+
EXPECT_EQ(arrayInfo.baseDepth, 2u) << "Whole-array baseDepth should equal the native layer count.";
137+
138+
// --- (B) Same array, wrapped as a single slice (layerIndex=0), as Teams wraps an NV12 plane ---
139+
const WrappedTextureInfo sliceInfo = inspect(2, static_cast<uint16_t>(0));
140+
141+
EXPECT_EQ(sliceInfo.rawLayerCount, 1) << "Single-slice view should report 1 bound layer, not the array size.";
142+
EXPECT_FALSE(sliceInfo.is2DArray) << "Single-slice view must not be flagged is2DArray.";
143+
144+
// --- (C) Single-layer texture ---
145+
const WrappedTextureInfo singleInfo = inspect(1, std::nullopt);
146+
147+
EXPECT_EQ(singleInfo.rawLayerCount, 1) << "Single-layer texture should report 1 layer.";
148+
EXPECT_FALSE(singleInfo.is2DArray) << "Single-layer texture should not be flagged is2DArray.";
149+
#endif
150+
}

0 commit comments

Comments
 (0)