diff --git a/.github/jobs/win32.yml b/.github/jobs/win32.yml index 7a699387d..fc118f91b 100644 --- a/.github/jobs/win32.yml +++ b/.github/jobs/win32.yml @@ -44,7 +44,7 @@ jobs: # BGFX_CONFIG_MAX_FRAME_BUFFERS is set so enough Framebuffers are available before V8 starts disposing unused ones - script: | - cmake -G "Visual Studio 17 2022" -B build/${{variables.solutionName}} -A ${{parameters.platform}} ${{variables.jsEngineDefine}} -D BX_CONFIG_DEBUG=ON -D GRAPHICS_API=${{parameters.graphics_api}} -D CMAKE_UNITY_BUILD=$(UNITY_BUILD) -D BGFX_CONFIG_MAX_FRAME_BUFFERS=256 -D BABYLON_DEBUG_TRACE=ON -D ENABLE_SANITIZERS=$(SANITIZER_FLAG) + cmake -G "Visual Studio 17 2022" -B build/${{variables.solutionName}} -A ${{parameters.platform}} ${{variables.jsEngineDefine}} -D BX_CONFIG_DEBUG=ON -D GRAPHICS_API=${{parameters.graphics_api}} -D CMAKE_UNITY_BUILD=$(UNITY_BUILD) -D BGFX_CONFIG_MAX_FRAME_BUFFERS=256 -D BABYLON_DEBUG_TRACE=ON -D ENABLE_SANITIZERS=$(SANITIZER_FLAG) -D BABYLON_NATIVE_SKIP_RENDER_TESTS=ON displayName: "Generate ${{variables.solutionName}} solution" - task: MSBuild@1 @@ -65,6 +65,12 @@ jobs: reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\UnitTests.exe" /v DumpFolder /t REG_SZ /d "$(Build.ArtifactStagingDirectory)/Dumps" /f displayName: "Enable Crash Dumps" + - script: | + DISM /online /add-capability /capabilityname:tools.graphics.directx~~~~0.0.1.0 /norestart + d3dconfig debug-layer debug-layer-mode=force-on + displayName: "Enable D3D Debug Layer" + continueOnError: true + - powershell: | $vs = vswhere -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath $msvc = Get-ChildItem "$vs\VC\Tools\MSVC" | Sort-Object Name -Descending | Select-Object -First 1 diff --git a/.gitignore b/.gitignore index cc2428209..54aa5c536 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /build .DS_Store .vscode + +shaderCache.bin diff --git a/Apps/HeadlessScreenshotApp/Win32/App.cpp b/Apps/HeadlessScreenshotApp/Win32/App.cpp index c89d59b74..e249d051c 100644 --- a/Apps/HeadlessScreenshotApp/Win32/App.cpp +++ b/Apps/HeadlessScreenshotApp/Win32/App.cpp @@ -127,41 +127,32 @@ int main() // Create a render target texture for the output. winrt::com_ptr outputTexture = CreateD3DRenderTargetTexture(d3dDevice.get()); - std::promise addToContext{}; std::promise startup{}; // Create an external texture for the render target texture and pass it to // the `startup` JavaScript function. - loader.Dispatch([externalTexture = Babylon::Plugins::ExternalTexture{outputTexture.get()}, &addToContext, &startup](Napi::Env env) { - auto jsPromise = externalTexture.AddToContextAsync(env); - addToContext.set_value(); - - auto jsOnFulfilled = Napi::Function::New(env, [&startup](const Napi::CallbackInfo& info) { - auto nativeTexture = info[0]; - info.Env().Global().Get("startup").As().Call( - { - nativeTexture, - Napi::Value::From(info.Env(), WIDTH), - Napi::Value::From(info.Env(), HEIGHT), - }); - startup.set_value(); - }); - - jsPromise = jsPromise.Get("then").As().Call(jsPromise, {jsOnFulfilled}).As(); - - CatchAndLogError(jsPromise); + loader.Dispatch([externalTexture = Babylon::Plugins::ExternalTexture{outputTexture.get()}, &startup](Napi::Env env) { + auto nativeTexture = externalTexture.CreateForJavaScript(env); + env.Global().Get("startup").As().Call( + { + nativeTexture, + Napi::Value::From(env, WIDTH), + Napi::Value::From(env, HEIGHT), + }); + startup.set_value(); }); - // Wait for `AddToContextAsync` to be called. - addToContext.get_future().wait(); - - // Render a frame so that `AddToContextAsync` will complete. deviceUpdate.Finish(); device.FinishRenderingCurrentFrame(); - // Wait for `startup` to finish. startup.get_future().wait(); + // Pump an extra frame so overrideInternal applies the native texture. + device.StartRenderingCurrentFrame(); + deviceUpdate.Start(); + deviceUpdate.Finish(); + device.FinishRenderingCurrentFrame(); + struct Asset { const char* Name; diff --git a/Apps/PrecompiledShaderTest/Source/App.cpp b/Apps/PrecompiledShaderTest/Source/App.cpp index 1ae0dee10..ca77787e4 100644 --- a/Apps/PrecompiledShaderTest/Source/App.cpp +++ b/Apps/PrecompiledShaderTest/Source/App.cpp @@ -135,40 +135,32 @@ int RunApp( Babylon::ScriptLoader loader{runtime}; loader.LoadScript("app:///index.js"); - std::promise addToContext{}; std::promise startup{}; // Create an external texture for the render target texture and pass it to // the `startup` JavaScript function. - loader.Dispatch([externalTexture = std::move(externalTexture), &addToContext, &startup](Napi::Env env) { - auto jsPromise = externalTexture.AddToContextAsync(env); - addToContext.set_value(); - - auto jsOnFulfilled = Napi::Function::New(env, [&startup](const Napi::CallbackInfo& info) { - auto nativeTexture = info[0]; - info.Env().Global().Get("startup").As().Call( - { - nativeTexture, - Napi::Value::From(info.Env(), WIDTH), - Napi::Value::From(info.Env(), HEIGHT), - }); - startup.set_value(); - }); - - jsPromise = jsPromise.Get("then").As().Call(jsPromise, {jsOnFulfilled}).As(); - CatchAndLogError(jsPromise); + loader.Dispatch([externalTexture = std::move(externalTexture), &startup](Napi::Env env) { + auto nativeTexture = externalTexture.CreateForJavaScript(env); + env.Global().Get("startup").As().Call( + { + nativeTexture, + Napi::Value::From(env, WIDTH), + Napi::Value::From(env, HEIGHT), + }); + startup.set_value(); }); - // Wait for `AddToContextAsync` to be called. - addToContext.get_future().wait(); - - // Render a frame so that `AddToContextAsync` will complete. deviceUpdate.Finish(); device.FinishRenderingCurrentFrame(); - // Wait for `startup` to finish. startup.get_future().wait(); + // Pump an extra frame so overrideInternal applies the native texture. + device.StartRenderingCurrentFrame(); + deviceUpdate.Start(); + deviceUpdate.Finish(); + device.FinishRenderingCurrentFrame(); + // Start a new frame for rendering the scene. device.StartRenderingCurrentFrame(); deviceUpdate.Start(); diff --git a/Apps/StyleTransferApp/Win32/App.cpp b/Apps/StyleTransferApp/Win32/App.cpp index 3688a9752..1f48c7ced 100644 --- a/Apps/StyleTransferApp/Win32/App.cpp +++ b/Apps/StyleTransferApp/Win32/App.cpp @@ -334,37 +334,32 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, loader.LoadScript("app:///Scripts/babylonjs.loaders.js"); loader.LoadScript("app:///Scripts/index.js"); - std::promise addToContext{}; std::promise startup{}; // Create an external texture for the render target texture and pass it to // the `startup` JavaScript function. - loader.Dispatch([externalTexture = Babylon::Plugins::ExternalTexture{g_BabylonRenderTexture.get()}, &addToContext, &startup](Napi::Env env) { - auto jsPromise = externalTexture.AddToContextAsync(env); - addToContext.set_value(); - - jsPromise.Get("then").As().Call(jsPromise, {Napi::Function::New(env, [&startup](const Napi::CallbackInfo& info) { - auto nativeTexture = info[0]; - info.Env().Global().Get("startup").As().Call( - { - nativeTexture, - Napi::Value::From(info.Env(), WIDTH), - Napi::Value::From(info.Env(), HEIGHT), - }); - startup.set_value(); - })}); + loader.Dispatch([externalTexture = Babylon::Plugins::ExternalTexture{g_BabylonRenderTexture.get()}, &startup](Napi::Env env) { + auto nativeTexture = externalTexture.CreateForJavaScript(env); + env.Global().Get("startup").As().Call( + { + nativeTexture, + Napi::Value::From(env, WIDTH), + Napi::Value::From(env, HEIGHT), + }); + startup.set_value(); }); - // Wait for `AddToContextAsync` to be called. - addToContext.get_future().wait(); - - // Render a frame so that `AddToContextAsync` will complete. g_update->Finish(); g_device->FinishRenderingCurrentFrame(); - // Wait for `startup` to finish. startup.get_future().wait(); + // Pump an extra frame so overrideInternal applies the native texture. + g_device->StartRenderingCurrentFrame(); + g_update->Start(); + g_update->Finish(); + g_device->FinishRenderingCurrentFrame(); + // --------------------------- Rendering loop ------------------------- HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PLAYGROUNDWIN32)); diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index b748e8b78..d2f777936 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -3,6 +3,7 @@ if(NOT((WIN32 AND NOT WINDOWS_STORE) OR (APPLE AND NOT IOS) OR (UNIX AND NOT AND endif() option(BABYLON_NATIVE_TESTS_USE_NOOP_METAL_DEVICE "Use a noop Metal device for Apple platforms." OFF) +option(BABYLON_NATIVE_SKIP_RENDER_TESTS "Skip GPU render tests (e.g. on CI without real GPU)." OFF) set(BABYLONJS_ASSETS "../node_modules/babylonjs/babylon.max.js") @@ -11,6 +12,7 @@ set(BABYLONJS_MATERIALS_ASSETS "../node_modules/babylonjs-materials/babylonjs.materials.js") set(TEST_ASSETS + "JavaScript/dist/tests.externalTexture.render.js" "JavaScript/dist/tests.javaScript.all.js" "JavaScript/dist/tests.shaderCache.basicScene.js") @@ -18,6 +20,7 @@ set(SOURCES "Source/App.h" "Source/App.cpp" "Source/Tests.ExternalTexture.cpp" + "Source/Tests.ExternalTexture.Render.cpp" "Source/Tests.JavaScript.cpp" "Source/Tests.ShaderCache.cpp" "Source/Utils.h" @@ -25,8 +28,7 @@ set(SOURCES if(GRAPHICS_API STREQUAL "D3D11") set(SOURCES ${SOURCES} - "Source/Tests.Device.${GRAPHICS_API}.cpp" - "Source/Tests.ExternalTexture.${GRAPHICS_API}.cpp") + "Source/Tests.Device.${GRAPHICS_API}.cpp") endif() if(APPLE) @@ -42,7 +44,11 @@ elseif(UNIX AND NOT ANDROID) set(SOURCES ${SOURCES} "Source/App.X11.cpp") set(ADDITIONAL_COMPILE_DEFINITIONS PRIVATE SKIP_EXTERNAL_TEXTURE_TESTS) elseif(WIN32) - set(SOURCES ${SOURCES} "Source/App.Win32.cpp") + set(SOURCES ${SOURCES} + "Source/App.Win32.cpp" + "Source/RenderDoc.h" + "Source/RenderDoc.cpp") + set(ADDITIONAL_COMPILE_DEFINITIONS ${ADDITIONAL_COMPILE_DEFINITIONS} PRIVATE HAS_RENDERDOC) endif() add_executable(UnitTests ${BABYLONJS_ASSETS} ${BABYLONJS_MATERIALS_ASSETS} ${TEST_ASSETS} ${SOURCES}) @@ -66,6 +72,10 @@ target_link_libraries(UnitTests target_compile_definitions(UnitTests PRIVATE ${ADDITIONAL_COMPILE_DEFINITIONS}) +if(BABYLON_NATIVE_SKIP_RENDER_TESTS) + target_compile_definitions(UnitTests PRIVATE SKIP_RENDER_TESTS) +endif() + add_test(NAME UnitTests COMMAND UnitTests) # See https://gitlab.kitware.com/cmake/cmake/-/issues/23543 diff --git a/Apps/UnitTests/JavaScript/dist/tests.externalTexture.render.js b/Apps/UnitTests/JavaScript/dist/tests.externalTexture.render.js new file mode 100644 index 000000000..f499c3f79 --- /dev/null +++ b/Apps/UnitTests/JavaScript/dist/tests.externalTexture.render.js @@ -0,0 +1,639 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "@babylonjs/core" +/*!**************************!*\ + !*** external "BABYLON" ***! + \**************************/ +(module) { + +"use strict"; +module.exports = BABYLON; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/OverloadYield.js" +/*!******************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/OverloadYield.js ***! + \******************************************************************/ +(module) { + +function _OverloadYield(e, d) { + this.v = e, this.k = d; +} +module.exports = _OverloadYield, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regenerator.js" +/*!****************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regenerator.js ***! + \****************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +var regeneratorDefine = __webpack_require__(/*! ./regeneratorDefine.js */ "../../node_modules/@babel/runtime/helpers/regeneratorDefine.js"); +function _regenerator() { + /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ + var e, + t, + r = "function" == typeof Symbol ? Symbol : {}, + n = r.iterator || "@@iterator", + o = r.toStringTag || "@@toStringTag"; + function i(r, n, o, i) { + var c = n && n.prototype instanceof Generator ? n : Generator, + u = Object.create(c.prototype); + return regeneratorDefine(u, "_invoke", function (r, n, o) { + var i, + c, + u, + f = 0, + p = o || [], + y = !1, + G = { + p: 0, + n: 0, + v: e, + a: d, + f: d.bind(e, 4), + d: function d(t, r) { + return i = t, c = 0, u = e, G.n = r, a; + } + }; + function d(r, n) { + for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { + var o, + i = p[t], + d = G.p, + l = i[2]; + r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); + } + if (o || r > 1) return a; + throw y = !0, n; + } + return function (o, p, l) { + if (f > 1) throw TypeError("Generator is already running"); + for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { + i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); + try { + if (f = 2, i) { + if (c || (o = "next"), t = i[o]) { + if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); + if (!t.done) return t; + u = t.value, c < 2 && (c = 0); + } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); + i = e; + } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; + } catch (t) { + i = e, c = 1, u = t; + } finally { + f = 1; + } + } + return { + value: t, + done: y + }; + }; + }(r, o, i), !0), u; + } + var a = {}; + function Generator() {} + function GeneratorFunction() {} + function GeneratorFunctionPrototype() {} + t = Object.getPrototypeOf; + var c = [][n] ? t(t([][n]())) : (regeneratorDefine(t = {}, n, function () { + return this; + }), t), + u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); + function f(e) { + return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, regeneratorDefine(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; + } + return GeneratorFunction.prototype = GeneratorFunctionPrototype, regeneratorDefine(u, "constructor", GeneratorFunctionPrototype), regeneratorDefine(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", regeneratorDefine(GeneratorFunctionPrototype, o, "GeneratorFunction"), regeneratorDefine(u), regeneratorDefine(u, o, "Generator"), regeneratorDefine(u, n, function () { + return this; + }), regeneratorDefine(u, "toString", function () { + return "[object Generator]"; + }), (module.exports = _regenerator = function _regenerator() { + return { + w: i, + m: f + }; + }, module.exports.__esModule = true, module.exports["default"] = module.exports)(); +} +module.exports = _regenerator, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorAsync.js" +/*!*********************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorAsync.js ***! + \*********************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +var regeneratorAsyncGen = __webpack_require__(/*! ./regeneratorAsyncGen.js */ "../../node_modules/@babel/runtime/helpers/regeneratorAsyncGen.js"); +function _regeneratorAsync(n, e, r, t, o) { + var a = regeneratorAsyncGen(n, e, r, t, o); + return a.next().then(function (n) { + return n.done ? n.value : a.next(); + }); +} +module.exports = _regeneratorAsync, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorAsyncGen.js" +/*!************************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorAsyncGen.js ***! + \************************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +var regenerator = __webpack_require__(/*! ./regenerator.js */ "../../node_modules/@babel/runtime/helpers/regenerator.js"); +var regeneratorAsyncIterator = __webpack_require__(/*! ./regeneratorAsyncIterator.js */ "../../node_modules/@babel/runtime/helpers/regeneratorAsyncIterator.js"); +function _regeneratorAsyncGen(r, e, t, o, n) { + return new regeneratorAsyncIterator(regenerator().w(r, e, t, o), n || Promise); +} +module.exports = _regeneratorAsyncGen, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorAsyncIterator.js" +/*!*****************************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorAsyncIterator.js ***! + \*****************************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +var OverloadYield = __webpack_require__(/*! ./OverloadYield.js */ "../../node_modules/@babel/runtime/helpers/OverloadYield.js"); +var regeneratorDefine = __webpack_require__(/*! ./regeneratorDefine.js */ "../../node_modules/@babel/runtime/helpers/regeneratorDefine.js"); +function AsyncIterator(t, e) { + function n(r, o, i, f) { + try { + var c = t[r](o), + u = c.value; + return u instanceof OverloadYield ? e.resolve(u.v).then(function (t) { + n("next", t, i, f); + }, function (t) { + n("throw", t, i, f); + }) : e.resolve(u).then(function (t) { + c.value = t, i(c); + }, function (t) { + return n("throw", t, i, f); + }); + } catch (t) { + f(t); + } + } + var r; + this.next || (regeneratorDefine(AsyncIterator.prototype), regeneratorDefine(AsyncIterator.prototype, "function" == typeof Symbol && Symbol.asyncIterator || "@asyncIterator", function () { + return this; + })), regeneratorDefine(this, "_invoke", function (t, o, i) { + function f() { + return new e(function (e, r) { + n(t, i, e, r); + }); + } + return r = r ? r.then(f, f) : f(); + }, !0); +} +module.exports = AsyncIterator, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorDefine.js" +/*!**********************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorDefine.js ***! + \**********************************************************************/ +(module) { + +function _regeneratorDefine(e, r, n, t) { + var i = Object.defineProperty; + try { + i({}, "", {}); + } catch (e) { + i = 0; + } + module.exports = _regeneratorDefine = function regeneratorDefine(e, r, n, t) { + function o(r, n) { + _regeneratorDefine(e, r, function (e) { + return this._invoke(r, n, e); + }); + } + r ? i ? i(e, r, { + value: n, + enumerable: !t, + configurable: !t, + writable: !t + }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); + }, module.exports.__esModule = true, module.exports["default"] = module.exports, _regeneratorDefine(e, r, n, t); +} +module.exports = _regeneratorDefine, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorKeys.js" +/*!********************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorKeys.js ***! + \********************************************************************/ +(module) { + +function _regeneratorKeys(e) { + var n = Object(e), + r = []; + for (var t in n) r.unshift(t); + return function e() { + for (; r.length;) if ((t = r.pop()) in n) return e.value = t, e.done = !1, e; + return e.done = !0, e; + }; +} +module.exports = _regeneratorKeys, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorRuntime.js" +/*!***********************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorRuntime.js ***! + \***********************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +var OverloadYield = __webpack_require__(/*! ./OverloadYield.js */ "../../node_modules/@babel/runtime/helpers/OverloadYield.js"); +var regenerator = __webpack_require__(/*! ./regenerator.js */ "../../node_modules/@babel/runtime/helpers/regenerator.js"); +var regeneratorAsync = __webpack_require__(/*! ./regeneratorAsync.js */ "../../node_modules/@babel/runtime/helpers/regeneratorAsync.js"); +var regeneratorAsyncGen = __webpack_require__(/*! ./regeneratorAsyncGen.js */ "../../node_modules/@babel/runtime/helpers/regeneratorAsyncGen.js"); +var regeneratorAsyncIterator = __webpack_require__(/*! ./regeneratorAsyncIterator.js */ "../../node_modules/@babel/runtime/helpers/regeneratorAsyncIterator.js"); +var regeneratorKeys = __webpack_require__(/*! ./regeneratorKeys.js */ "../../node_modules/@babel/runtime/helpers/regeneratorKeys.js"); +var regeneratorValues = __webpack_require__(/*! ./regeneratorValues.js */ "../../node_modules/@babel/runtime/helpers/regeneratorValues.js"); +function _regeneratorRuntime() { + "use strict"; + + var r = regenerator(), + e = r.m(_regeneratorRuntime), + t = (Object.getPrototypeOf ? Object.getPrototypeOf(e) : e.__proto__).constructor; + function n(r) { + var e = "function" == typeof r && r.constructor; + return !!e && (e === t || "GeneratorFunction" === (e.displayName || e.name)); + } + var o = { + "throw": 1, + "return": 2, + "break": 3, + "continue": 3 + }; + function a(r) { + var e, t; + return function (n) { + e || (e = { + stop: function stop() { + return t(n.a, 2); + }, + "catch": function _catch() { + return n.v; + }, + abrupt: function abrupt(r, e) { + return t(n.a, o[r], e); + }, + delegateYield: function delegateYield(r, o, a) { + return e.resultName = o, t(n.d, regeneratorValues(r), a); + }, + finish: function finish(r) { + return t(n.f, r); + } + }, t = function t(r, _t, o) { + n.p = e.prev, n.n = e.next; + try { + return r(_t, o); + } finally { + e.next = n.n; + } + }), e.resultName && (e[e.resultName] = n.v, e.resultName = void 0), e.sent = n.v, e.next = n.n; + try { + return r.call(this, e); + } finally { + n.p = e.prev, n.n = e.next; + } + }; + } + return (module.exports = _regeneratorRuntime = function _regeneratorRuntime() { + return { + wrap: function wrap(e, t, n, o) { + return r.w(a(e), t, n, o && o.reverse()); + }, + isGeneratorFunction: n, + mark: r.m, + awrap: function awrap(r, e) { + return new OverloadYield(r, e); + }, + AsyncIterator: regeneratorAsyncIterator, + async: function async(r, e, t, o, u) { + return (n(e) ? regeneratorAsyncGen : regeneratorAsync)(a(r), e, t, o, u); + }, + keys: regeneratorKeys, + values: regeneratorValues + }; + }, module.exports.__esModule = true, module.exports["default"] = module.exports)(); +} +module.exports = _regeneratorRuntime, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/regeneratorValues.js" +/*!**********************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/regeneratorValues.js ***! + \**********************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +var _typeof = (__webpack_require__(/*! ./typeof.js */ "../../node_modules/@babel/runtime/helpers/typeof.js")["default"]); +function _regeneratorValues(e) { + if (null != e) { + var t = e["function" == typeof Symbol && Symbol.iterator || "@@iterator"], + r = 0; + if (t) return t.call(e); + if ("function" == typeof e.next) return e; + if (!isNaN(e.length)) return { + next: function next() { + return e && r >= e.length && (e = void 0), { + value: e && e[r++], + done: !e + }; + } + }; + } + throw new TypeError(_typeof(e) + " is not iterable"); +} +module.exports = _regeneratorValues, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/typeof.js" +/*!***********************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/typeof.js ***! + \***********************************************************/ +(module) { + +function _typeof(o) { + "@babel/helpers - typeof"; + + return module.exports = _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { + return typeof o; + } : function (o) { + return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; + }, module.exports.__esModule = true, module.exports["default"] = module.exports, _typeof(o); +} +module.exports = _typeof, module.exports.__esModule = true, module.exports["default"] = module.exports; + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/regenerator/index.js" +/*!**************************************************************!*\ + !*** ../../node_modules/@babel/runtime/regenerator/index.js ***! + \**************************************************************/ +(module, __unused_webpack_exports, __webpack_require__) { + +// TODO(Babel 8): Remove this file. + +var runtime = __webpack_require__(/*! ../helpers/regeneratorRuntime */ "../../node_modules/@babel/runtime/helpers/regeneratorRuntime.js")(); +module.exports = runtime; + +// Copied from https://github.com/facebook/regenerator/blob/main/packages/runtime/runtime.js#L736= +try { + regeneratorRuntime = runtime; +} catch (accidentalStrictMode) { + if (typeof globalThis === "object") { + globalThis.regeneratorRuntime = runtime; + } else { + Function("r", "regeneratorRuntime = r")(runtime); + } +} + + +/***/ }, + +/***/ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js" +/*!*************************************************************************!*\ + !*** ../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js ***! + \*************************************************************************/ +(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ _asyncToGenerator) +/* harmony export */ }); +function asyncGeneratorStep(n, t, e, r, o, a, c) { + try { + var i = n[a](c), + u = i.value; + } catch (n) { + return void e(n); + } + i.done ? t(u) : Promise.resolve(u).then(r, o); +} +function _asyncToGenerator(n) { + return function () { + var t = this, + e = arguments; + return new Promise(function (r, o) { + var a = n.apply(t, e); + function _next(n) { + asyncGeneratorStep(a, r, o, _next, _throw, "next", n); + } + function _throw(n) { + asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); + } + _next(void 0); + }); + }; +} + + +/***/ } + +/******/ }); +/************************************************************************/ +/******/ // 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 in strict mode. +(() => { +"use strict"; +/*!*********************************************!*\ + !*** ./src/tests.externalTexture.render.ts ***! + \*********************************************/ +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/asyncToGenerator */ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime/regenerator */ "../../node_modules/@babel/runtime/regenerator/index.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babylonjs/core */ "@babylonjs/core"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_2__); + + +var vertexShader = "\n precision highp float;\n attribute vec3 position;\n attribute vec2 uv;\n uniform mat4 worldViewProjection;\n varying vec2 vUV;\n void main(void) {\n gl_Position = worldViewProjection * vec4(position, 1.0);\n vUV = uv;\n }\n"; + + + + + + + + + + + +var fragmentShader = "\n precision highp float;\n precision highp sampler2DArray;\n uniform sampler2DArray textureArraySampler;\n uniform float sliceIndex;\n varying vec2 vUV;\n void main(void) {\n gl_FragColor = texture(textureArraySampler, vec3(vUV, sliceIndex));\n }\n"; + + + + + + + + + + +var engine; +var scene; +var material; + +function startup( +inputNativeTexture, +outputNativeTexture, +width, +height) +{ + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.NativeEngine(); + delete engine.getCaps().parallelShaderCompile; + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.Scene(engine); + + // Wrap the output texture as a render target. + var outputTexture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.RenderTargetTexture( + "output", + { width: width, height: height }, + scene, + { + colorAttachment: engine.wrapNativeTexture(outputNativeTexture), + generateDepthBuffer: true, + generateStencilBuffer: false + } + ); + + // Orthographic camera filling the viewport exactly. + var camera = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.FreeCamera("camera", new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.Vector3(0, 0, -1), scene); + camera.setTarget(_babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.Vector3.Zero()); + camera.mode = _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.Camera.ORTHOGRAPHIC_CAMERA; + camera.orthoTop = 1; + camera.orthoBottom = -1; + camera.orthoLeft = -1; + camera.orthoRight = 1; + camera.outputRenderTarget = outputTexture; + + // Fullscreen quad. + var quad = _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.MeshBuilder.CreatePlane("quad", { size: 2 }, scene); + + // Shader material that samples from a texture array. + material = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.ShaderMaterial( + "textureArrayShader", + scene, + { vertexSource: vertexShader, fragmentSource: fragmentShader }, + { + attributes: ["position", "uv"], + uniforms: ["worldViewProjection", "sliceIndex"], + samplers: ["textureArraySampler"] + } + ); + + material.onError = function (_effect, errors) { + console.error("ShaderMaterial compilation error: " + errors); + }; + + material.backFaceCulling = false; + material.depthFunction = _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.Constants.ALWAYS; + + // Wrap the input texture array and bind it to the shader. + var internalTex = engine.wrapNativeTexture(inputNativeTexture); + var inputTexWrapper = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_2__.Texture(null, scene); + inputTexWrapper._texture = internalTex; + material.setTexture("textureArraySampler", inputTexWrapper); + material.setFloat("sliceIndex", 0); + + quad.material = material; +}function + +renderSlice(_x) {return _renderSlice.apply(this, arguments);}function _renderSlice() {_renderSlice = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee(sliceIndex) {return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: + material.setFloat("sliceIndex", sliceIndex);_context.next = 1;return ( + scene.whenReadyAsync());case 1: + scene.render();case 2:case "end":return _context.stop();}}, _callee);}));return _renderSlice.apply(this, arguments);} + + +globalThis.startup = startup; +globalThis.renderSlice = renderSlice; +})(); + +/******/ })() +; \ No newline at end of file diff --git a/Apps/UnitTests/JavaScript/src/tests.externalTexture.render.ts b/Apps/UnitTests/JavaScript/src/tests.externalTexture.render.ts new file mode 100644 index 000000000..044381d9c --- /dev/null +++ b/Apps/UnitTests/JavaScript/src/tests.externalTexture.render.ts @@ -0,0 +1,112 @@ +import { + Camera, + Constants, + FreeCamera, + MeshBuilder, + NativeEngine, + RenderTargetTexture, + Scene, + ShaderMaterial, + Texture, + Vector3, +} from "@babylonjs/core"; + +const vertexShader = ` + precision highp float; + attribute vec3 position; + attribute vec2 uv; + uniform mat4 worldViewProjection; + varying vec2 vUV; + void main(void) { + gl_Position = worldViewProjection * vec4(position, 1.0); + vUV = uv; + } +`; + +const fragmentShader = ` + precision highp float; + precision highp sampler2DArray; + uniform sampler2DArray textureArraySampler; + uniform float sliceIndex; + varying vec2 vUV; + void main(void) { + gl_FragColor = texture(textureArraySampler, vec3(vUV, sliceIndex)); + } +`; + +let engine: NativeEngine; +let scene: Scene; +let material: ShaderMaterial; + +function startup( + inputNativeTexture: any, + outputNativeTexture: any, + width: number, + height: number +): void { + engine = new NativeEngine(); + delete engine.getCaps().parallelShaderCompile; + scene = new Scene(engine); + + // Wrap the output texture as a render target. + const outputTexture = new RenderTargetTexture( + "output", + { width, height }, + scene, + { + colorAttachment: engine.wrapNativeTexture(outputNativeTexture), + generateDepthBuffer: true, + generateStencilBuffer: false, + } + ); + + // Orthographic camera filling the viewport exactly. + const camera = new FreeCamera("camera", new Vector3(0, 0, -1), scene); + camera.setTarget(Vector3.Zero()); + camera.mode = Camera.ORTHOGRAPHIC_CAMERA; + camera.orthoTop = 1; + camera.orthoBottom = -1; + camera.orthoLeft = -1; + camera.orthoRight = 1; + camera.outputRenderTarget = outputTexture; + + // Fullscreen quad. + const quad = MeshBuilder.CreatePlane("quad", { size: 2 }, scene); + + // Shader material that samples from a texture array. + material = new ShaderMaterial( + "textureArrayShader", + scene, + { vertexSource: vertexShader, fragmentSource: fragmentShader }, + { + attributes: ["position", "uv"], + uniforms: ["worldViewProjection", "sliceIndex"], + samplers: ["textureArraySampler"], + } + ); + + material.onError = (_effect, errors) => { + console.error("ShaderMaterial compilation error: " + errors); + }; + + material.backFaceCulling = false; + material.depthFunction = Constants.ALWAYS; + + // Wrap the input texture array and bind it to the shader. + const internalTex = engine.wrapNativeTexture(inputNativeTexture); + const inputTexWrapper = new Texture(null, scene); + inputTexWrapper._texture = internalTex; + material.setTexture("textureArraySampler", inputTexWrapper); + material.setFloat("sliceIndex", 0); + + quad.material = material; +} + +async function renderSlice(sliceIndex: number): Promise { + material.setFloat("sliceIndex", sliceIndex); + await scene.whenReadyAsync(); + scene.render(); +} + +(globalThis as any).startup = startup; +(globalThis as any).renderSlice = renderSlice; diff --git a/Apps/UnitTests/JavaScript/webpack.config.js b/Apps/UnitTests/JavaScript/webpack.config.js index 41667f4b0..c138e5dff 100644 --- a/Apps/UnitTests/JavaScript/webpack.config.js +++ b/Apps/UnitTests/JavaScript/webpack.config.js @@ -7,6 +7,7 @@ module.exports = { devtool: false, entry: { "tests.javaScript.all": './src/tests.javaScript.all.ts', + "tests.externalTexture.render": './src/tests.externalTexture.render.ts', "tests.shaderCache.basicScene": './src/tests.shaderCache.basicScene.ts', }, externals: { diff --git a/Apps/UnitTests/Source/RenderDoc.cpp b/Apps/UnitTests/Source/RenderDoc.cpp new file mode 100644 index 000000000..e53d64c3a --- /dev/null +++ b/Apps/UnitTests/Source/RenderDoc.cpp @@ -0,0 +1,63 @@ +#include "RenderDoc.h" + +#ifdef RENDERDOC + +#ifdef _WIN32 +#include +#include "C:\Program Files\RenderDoc\renderdoc_app.h" +#elif defined(__linux__) +#include +#include "renderdoc_app.h" +#endif + +namespace +{ + RENDERDOC_API_1_1_2* rdoc_api = nullptr; +} + +#endif + +void RenderDoc::Init() +{ +#ifdef RENDERDOC +#ifdef _WIN32 + if (HMODULE mod = GetModuleHandleA("renderdoc.dll")) + { + pRENDERDOC_GetAPI RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)GetProcAddress(mod, "RENDERDOC_GetAPI"); + int ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2, (void**)&rdoc_api); + (void)ret; + } +#elif defined(__linux__) + if (void* mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD)) + { + pRENDERDOC_GetAPI RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)dlsym(mod, "RENDERDOC_GetAPI"); + int ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2, (void**)&rdoc_api); + (void)ret; + } +#endif +#endif +} + +void RenderDoc::StartFrameCapture(void* device) +{ +#ifdef RENDERDOC + if (rdoc_api) + { + rdoc_api->StartFrameCapture(device, nullptr); + } +#else + (void)device; +#endif +} + +void RenderDoc::StopFrameCapture(void* device) +{ +#ifdef RENDERDOC + if (rdoc_api) + { + rdoc_api->EndFrameCapture(device, nullptr); + } +#else + (void)device; +#endif +} diff --git a/Apps/UnitTests/Source/RenderDoc.h b/Apps/UnitTests/Source/RenderDoc.h new file mode 100644 index 000000000..8d1a56f3e --- /dev/null +++ b/Apps/UnitTests/Source/RenderDoc.h @@ -0,0 +1,11 @@ +#pragma once + +// Uncomment this to enable renderdoc captures +// #define RENDERDOC + +namespace RenderDoc +{ + void Init(); + void StartFrameCapture(void* device = nullptr); + void StopFrameCapture(void* device = nullptr); +} diff --git a/Apps/UnitTests/Source/Tests.ExternalTexture.D3D11.cpp b/Apps/UnitTests/Source/Tests.ExternalTexture.D3D11.cpp deleted file mode 100644 index 24147e05e..000000000 --- a/Apps/UnitTests/Source/Tests.ExternalTexture.D3D11.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "Utils.h" - -#include - -extern Babylon::Graphics::Configuration g_deviceConfig; - -TEST(ExternalTexture, AddToContextAsyncAndUpdateWithLayerIndex) -{ -#ifdef SKIP_EXTERNAL_TEXTURE_TESTS - GTEST_SKIP(); -#else - Babylon::Graphics::Device device{g_deviceConfig}; - Babylon::Graphics::DeviceUpdate update{device.GetUpdate("update")}; - - device.StartRenderingCurrentFrame(); - update.Start(); - - auto nativeTexture = CreateTestTexture(device.GetPlatformInfo().Device, 256, 256, 3); - - Babylon::Plugins::ExternalTexture externalTexture{nativeTexture}; - - std::promise addToContext{}; - std::promise promiseResolved{}; - - Babylon::AppRuntime runtime{}; - runtime.Dispatch([&device, &addToContext, &promiseResolved, externalTexture](Napi::Env env) { - 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); - - // Test with explicit layer index 1 - auto jsPromise = externalTexture.AddToContextAsync(env, 1); - addToContext.set_value(); - - auto jsOnFulfilled = Napi::Function::New(env, [&promiseResolved](const Napi::CallbackInfo& info) { - promiseResolved.set_value(); - }); - - auto jsOnRejected = Napi::Function::New(env, [&promiseResolved](const Napi::CallbackInfo& info) { - promiseResolved.set_exception(std::make_exception_ptr(info[0].As())); - }); - - jsPromise.Get("then").As().Call(jsPromise, {jsOnFulfilled, jsOnRejected}); - }); - - // Wait for AddToContextAsync to be called. - addToContext.get_future().wait(); - - // Render a frame so that AddToContextAsync will complete. - update.Finish(); - device.FinishRenderingCurrentFrame(); - - // Wait for promise to resolve. - promiseResolved.get_future().wait(); - - // Start a new frame. - device.StartRenderingCurrentFrame(); - update.Start(); - - // Update the external texture to a new texture with explicit layer index 2. - externalTexture.Update(nativeTexture, std::nullopt, 2); - - DestroyTestTexture(nativeTexture); - - update.Finish(); - device.FinishRenderingCurrentFrame(); -#endif -} diff --git a/Apps/UnitTests/Source/Tests.ExternalTexture.Render.cpp b/Apps/UnitTests/Source/Tests.ExternalTexture.Render.cpp new file mode 100644 index 000000000..51ea3a80d --- /dev/null +++ b/Apps/UnitTests/Source/Tests.ExternalTexture.Render.cpp @@ -0,0 +1,184 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "Utils.h" +#ifdef HAS_RENDERDOC +#include "RenderDoc.h" +#endif + +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; + +TEST(ExternalTexture, RenderTextureArray) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + constexpr uint32_t TEX_SIZE = 64; + constexpr uint32_t SLICE_COUNT = 3; + + const Color sliceColors[SLICE_COUNT] = { + {255, 0, 0, 255}, + {0, 255, 0, 255}, + {0, 0, 255, 255}, + }; + +#ifdef HAS_RENDERDOC + RenderDoc::Init(); +#endif + + Babylon::Graphics::Device device{g_deviceConfig}; + Babylon::Graphics::DeviceUpdate update{device.GetUpdate("update")}; + + device.StartRenderingCurrentFrame(); + update.Start(); + + auto inputTexture = CreateTestTextureArrayWithData( + device.GetPlatformInfo().Device, TEX_SIZE, TEX_SIZE, sliceColors, SLICE_COUNT); + Babylon::Plugins::ExternalTexture inputExternalTexture{inputTexture}; + + auto outputTexture = CreateRenderTargetTexture( + device.GetPlatformInfo().Device, TEX_SIZE, TEX_SIZE); + Babylon::Plugins::ExternalTexture outputExternalTexture{outputTexture}; + + std::promise startupDone; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [](const Napi::Error& error) { + std::cerr << "[Uncaught Error] " << Napi::GetErrorString(error) << std::endl; + std::cerr.flush(); + }; + + 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.render.js"); + + loader.Dispatch([&inputExternalTexture, &outputExternalTexture, &startupDone](Napi::Env env) { + auto jsInput = inputExternalTexture.CreateForJavaScript(env); + auto jsOutput = outputExternalTexture.CreateForJavaScript(env); + env.Global().Get("startup").As().Call({ + jsInput, + jsOutput, + Napi::Number::New(env, TEX_SIZE), + Napi::Number::New(env, TEX_SIZE), + }); + startupDone.set_value(); + }); + + update.Finish(); + device.FinishRenderingCurrentFrame(); + + startupDone.get_future().wait(); + + // Pump an extra frame so that bgfx::frame() processes the placeholder + // texture creation and AfterRenderScheduler fires overrideInternal + // to apply the native texture backing. + device.StartRenderingCurrentFrame(); + update.Start(); + update.Finish(); + device.FinishRenderingCurrentFrame(); + + for (uint32_t sliceIndex = 0; sliceIndex < SLICE_COUNT; ++sliceIndex) + { +#ifdef HAS_RENDERDOC + RenderDoc::StartFrameCapture(device.GetPlatformInfo().Device); +#endif + + device.StartRenderingCurrentFrame(); + update.Start(); + + std::promise renderDone; + + loader.Dispatch([sliceIndex, &renderDone](Napi::Env env) { + auto jsPromise = env.Global().Get("renderSlice").As().Call({ + Napi::Number::New(env, sliceIndex), + }).As(); + + auto jsOnFulfilled = Napi::Function::New(env, [&renderDone](const Napi::CallbackInfo&) { + renderDone.set_value(); + }); + + jsPromise.Get("then").As().Call(jsPromise, {jsOnFulfilled}); + }); + + renderDone.get_future().wait(); + + update.Finish(); + device.FinishRenderingCurrentFrame(); + +#ifdef HAS_RENDERDOC + RenderDoc::StopFrameCapture(device.GetPlatformInfo().Device); +#endif + + auto pixels = ReadBackRenderTarget( + device.GetPlatformInfo().Device, outputTexture, TEX_SIZE, TEX_SIZE); + + const auto& expected = sliceColors[sliceIndex]; + uint32_t matchCount = 0; + const uint32_t totalPixels = TEX_SIZE * TEX_SIZE; + + for (uint32_t i = 0; i < 5 && i < totalPixels; ++i) + { + std::cout << " pixel[" << i << "] = (" + << static_cast(pixels[i * 4 + 0]) << ", " + << static_cast(pixels[i * 4 + 1]) << ", " + << static_cast(pixels[i * 4 + 2]) << ", " + << static_cast(pixels[i * 4 + 3]) << ")" << std::endl; + } + + for (uint32_t i = 0; i < totalPixels; ++i) + { + const uint8_t r = pixels[i * 4 + 0]; + const uint8_t g = pixels[i * 4 + 1]; + const uint8_t b = pixels[i * 4 + 2]; + + if (std::abs(static_cast(r) - expected.R) < 25 && + std::abs(static_cast(g) - expected.G) < 25 && + std::abs(static_cast(b) - expected.B) < 25) + { + ++matchCount; + } + } + + const double matchPercent = + static_cast(matchCount) / totalPixels * 100.0; + + std::cout << "Slice " << sliceIndex << ": " << matchCount << "/" + << totalPixels << " pixels match (" << matchPercent << "%)" + << std::endl; + + EXPECT_GE(matchPercent, 90.0) + << "Slice " << sliceIndex << ": expected (" + << static_cast(expected.R) << ", " + << static_cast(expected.G) << ", " + << static_cast(expected.B) << ") but only " + << matchPercent << "% of pixels matched"; + } + + DestroyRenderTargetTexture(outputTexture); + DestroyTestTexture(inputTexture); +#endif +} diff --git a/Apps/UnitTests/Source/Tests.ExternalTexture.cpp b/Apps/UnitTests/Source/Tests.ExternalTexture.cpp index b842e81f4..8665a9df3 100644 --- a/Apps/UnitTests/Source/Tests.ExternalTexture.cpp +++ b/Apps/UnitTests/Source/Tests.ExternalTexture.cpp @@ -36,7 +36,7 @@ TEST(ExternalTexture, Construction) #endif } -TEST(ExternalTexture, AddToContextAsyncAndUpdate) +TEST(ExternalTexture, CreateForJavaScript) { #ifdef SKIP_EXTERNAL_TEXTURE_TESTS GTEST_SKIP(); @@ -51,11 +51,10 @@ TEST(ExternalTexture, AddToContextAsyncAndUpdate) Babylon::Plugins::ExternalTexture externalTexture{nativeTexture}; DestroyTestTexture(nativeTexture); - std::promise addToContext{}; - std::promise promiseResolved{}; + std::promise done{}; Babylon::AppRuntime runtime{}; - runtime.Dispatch([&device, &addToContext, &promiseResolved, externalTexture](Napi::Env env) { + runtime.Dispatch([&device, &done, externalTexture](Napi::Env env) { device.AddToJavaScript(env); Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) { @@ -66,38 +65,14 @@ TEST(ExternalTexture, AddToContextAsyncAndUpdate) Babylon::Plugins::NativeEngine::Initialize(env); - auto jsPromise = externalTexture.AddToContextAsync(env); - addToContext.set_value(); + auto jsTexture = externalTexture.CreateForJavaScript(env); + EXPECT_TRUE(jsTexture.IsObject()); - auto jsOnFulfilled = Napi::Function::New(env, [&promiseResolved](const Napi::CallbackInfo& info) { - promiseResolved.set_value(); - }); - - auto jsOnRejected = Napi::Function::New(env, [&promiseResolved](const Napi::CallbackInfo& info) { - promiseResolved.set_exception(std::make_exception_ptr(info[0].As())); - }); - - jsPromise.Get("then").As().Call(jsPromise, {jsOnFulfilled, jsOnRejected}); + done.set_value(); }); - // Wait for AddToContextAsync to be called. - addToContext.get_future().wait(); - - // Render a frame so that AddToContextAsync will complete. - update.Finish(); - device.FinishRenderingCurrentFrame(); - - // Wait for promise to resolve. - promiseResolved.get_future().wait(); - - // Start a new frame. - device.StartRenderingCurrentFrame(); - update.Start(); - - // Update the external texture to a new texture. - auto nativeTexture2 = CreateTestTexture(device.GetPlatformInfo().Device, 256, 256); - externalTexture.Update(nativeTexture2); - DestroyTestTexture(nativeTexture2); + // Wait for CreateForJavaScript to complete. + done.get_future().wait(); update.Finish(); device.FinishRenderingCurrentFrame(); diff --git a/Apps/UnitTests/Source/Utils.D3D11.cpp b/Apps/UnitTests/Source/Utils.D3D11.cpp index e5bfd2c26..050fe8d89 100644 --- a/Apps/UnitTests/Source/Utils.D3D11.cpp +++ b/Apps/UnitTests/Source/Utils.D3D11.cpp @@ -26,3 +26,106 @@ void DestroyTestTexture(Babylon::Graphics::TextureT texture) { texture->Release(); } + +Babylon::Graphics::TextureT CreateTestTextureArrayWithData(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height, const Color* sliceColors, uint32_t sliceCount) +{ + const uint32_t rowPitch = width * 4; + const uint32_t sliceSize = rowPitch * height; + + std::vector pixels(sliceSize * sliceCount); + for (uint32_t s = 0; s < sliceCount; ++s) + { + for (uint32_t i = 0; i < width * height; ++i) + { + uint8_t* p = pixels.data() + s * sliceSize + i * 4; + p[0] = sliceColors[s].R; + p[1] = sliceColors[s].G; + p[2] = sliceColors[s].B; + p[3] = sliceColors[s].A; + } + } + + std::vector initData(sliceCount); + for (uint32_t s = 0; s < sliceCount; ++s) + { + initData[s].pSysMem = pixels.data() + s * sliceSize; + initData[s].SysMemPitch = rowPitch; + initData[s].SysMemSlicePitch = 0; + } + + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = sliceCount; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + ID3D11Texture2D* texture = nullptr; + EXPECT_HRESULT_SUCCEEDED(device->CreateTexture2D(&desc, initData.data(), &texture)); + + return texture; +} + +Babylon::Graphics::TextureT CreateRenderTargetTexture(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height) +{ + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + ID3D11Texture2D* texture = nullptr; + EXPECT_HRESULT_SUCCEEDED(device->CreateTexture2D(&desc, nullptr, &texture)); + + return texture; +} + +std::vector ReadBackRenderTarget(Babylon::Graphics::DeviceT device, Babylon::Graphics::TextureT texture, uint32_t width, uint32_t height) +{ + ID3D11DeviceContext* context = nullptr; + device->GetImmediateContext(&context); + + D3D11_TEXTURE2D_DESC stagingDesc{}; + stagingDesc.Width = width; + stagingDesc.Height = height; + stagingDesc.MipLevels = 1; + stagingDesc.ArraySize = 1; + stagingDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + stagingDesc.SampleDesc.Count = 1; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + ID3D11Texture2D* staging = nullptr; + EXPECT_HRESULT_SUCCEEDED(device->CreateTexture2D(&stagingDesc, nullptr, &staging)); + + context->CopyResource(staging, static_cast(texture)); + + D3D11_MAPPED_SUBRESOURCE mapped{}; + EXPECT_HRESULT_SUCCEEDED(context->Map(staging, 0, D3D11_MAP_READ, 0, &mapped)); + + const uint32_t rowPitch = width * 4; + std::vector pixels(rowPitch * height); + const auto* src = static_cast(mapped.pData); + for (uint32_t row = 0; row < height; ++row) + { + memcpy(pixels.data() + row * rowPitch, src + row * mapped.RowPitch, rowPitch); + } + + context->Unmap(staging, 0); + staging->Release(); + context->Release(); + + return pixels; +} + +void DestroyRenderTargetTexture(Babylon::Graphics::TextureT texture) +{ + texture->Release(); +} diff --git a/Apps/UnitTests/Source/Utils.D3D12.cpp b/Apps/UnitTests/Source/Utils.D3D12.cpp index 65b4f6d23..bfb940af9 100644 --- a/Apps/UnitTests/Source/Utils.D3D12.cpp +++ b/Apps/UnitTests/Source/Utils.D3D12.cpp @@ -39,3 +39,23 @@ void DestroyTestTexture(Babylon::Graphics::TextureT texture) { texture->Release(); } + +Babylon::Graphics::TextureT CreateTestTextureArrayWithData(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height, const Color* sliceColors, uint32_t sliceCount) +{ + throw std::runtime_error{"not implemented for D3D12"}; +} + +Babylon::Graphics::TextureT CreateRenderTargetTexture(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height) +{ + throw std::runtime_error{"not implemented for D3D12"}; +} + +std::vector ReadBackRenderTarget(Babylon::Graphics::DeviceT device, Babylon::Graphics::TextureT texture, uint32_t width, uint32_t height) +{ + throw std::runtime_error{"not implemented for D3D12"}; +} + +void DestroyRenderTargetTexture(Babylon::Graphics::TextureT texture) +{ + texture->Release(); +} diff --git a/Apps/UnitTests/Source/Utils.Metal.mm b/Apps/UnitTests/Source/Utils.Metal.mm index b2ec3603e..a8fa18ce7 100644 --- a/Apps/UnitTests/Source/Utils.Metal.mm +++ b/Apps/UnitTests/Source/Utils.Metal.mm @@ -22,3 +22,66 @@ void DestroyTestTexture(Babylon::Graphics::TextureT texture) { texture->release(); } + +Babylon::Graphics::TextureT CreateTestTextureArrayWithData(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height, const Color* sliceColors, uint32_t sliceCount) +{ + MTL::TextureDescriptor* descriptor = MTL::TextureDescriptor::texture2DDescriptor( + MTL::PixelFormatRGBA8Unorm, width, height, false); + descriptor->setTextureType(MTL::TextureType2DArray); + descriptor->setArrayLength(sliceCount); + descriptor->setUsage(MTL::TextureUsageShaderRead); + descriptor->setStorageMode(MTL::StorageModeManaged); + + MTL::Texture* texture = device->newTexture(descriptor); + EXPECT_NE(texture, nullptr); + + const uint32_t bytesPerRow = width * 4; + const uint32_t sliceSize = bytesPerRow * height; + std::vector pixels(sliceSize); + + for (uint32_t s = 0; s < sliceCount; ++s) + { + for (uint32_t i = 0; i < width * height; ++i) + { + uint8_t* p = pixels.data() + i * 4; + p[0] = sliceColors[s].R; + p[1] = sliceColors[s].G; + p[2] = sliceColors[s].B; + p[3] = sliceColors[s].A; + } + + MTL::Region region = MTL::Region::Make2D(0, 0, width, height); + texture->replaceRegion(region, 0, s, pixels.data(), bytesPerRow, 0); + } + + return texture; +} + +Babylon::Graphics::TextureT CreateRenderTargetTexture(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height) +{ + MTL::TextureDescriptor* descriptor = MTL::TextureDescriptor::texture2DDescriptor( + MTL::PixelFormatRGBA8Unorm, width, height, false); + descriptor->setUsage(MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead); + descriptor->setStorageMode(MTL::StorageModeManaged); + + MTL::Texture* texture = device->newTexture(descriptor); + EXPECT_NE(texture, nullptr); + + return texture; +} + +std::vector ReadBackRenderTarget(Babylon::Graphics::DeviceT device, Babylon::Graphics::TextureT texture, uint32_t width, uint32_t height) +{ + const uint32_t bytesPerRow = width * 4; + std::vector pixels(bytesPerRow * height); + + MTL::Region region = MTL::Region::Make2D(0, 0, width, height); + texture->getBytes(pixels.data(), bytesPerRow, region, 0); + + return pixels; +} + +void DestroyRenderTargetTexture(Babylon::Graphics::TextureT texture) +{ + texture->release(); +} diff --git a/Apps/UnitTests/Source/Utils.OpenGL.cpp b/Apps/UnitTests/Source/Utils.OpenGL.cpp index 7f1ab668a..b9c81eec2 100644 --- a/Apps/UnitTests/Source/Utils.OpenGL.cpp +++ b/Apps/UnitTests/Source/Utils.OpenGL.cpp @@ -10,3 +10,23 @@ void DestroyTestTexture(Babylon::Graphics::TextureT texture) { throw std::runtime_error{"not implemented"}; } + +Babylon::Graphics::TextureT CreateTestTextureArrayWithData(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height, const Color* sliceColors, uint32_t sliceCount) +{ + throw std::runtime_error{"not implemented"}; +} + +Babylon::Graphics::TextureT CreateRenderTargetTexture(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height) +{ + throw std::runtime_error{"not implemented"}; +} + +std::vector ReadBackRenderTarget(Babylon::Graphics::DeviceT device, Babylon::Graphics::TextureT texture, uint32_t width, uint32_t height) +{ + throw std::runtime_error{"not implemented"}; +} + +void DestroyRenderTargetTexture(Babylon::Graphics::TextureT texture) +{ + throw std::runtime_error{"not implemented"}; +} diff --git a/Apps/UnitTests/Source/Utils.h b/Apps/UnitTests/Source/Utils.h index a3ee6a982..f17c35a78 100644 --- a/Apps/UnitTests/Source/Utils.h +++ b/Apps/UnitTests/Source/Utils.h @@ -1,6 +1,22 @@ #pragma once #include +#include + +#include +#include + +struct Color +{ + uint8_t R, G, B, A; +}; Babylon::Graphics::TextureT CreateTestTexture(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height, uint32_t arraySize = 1); void DestroyTestTexture(Babylon::Graphics::TextureT texture); + +Babylon::Graphics::TextureT CreateTestTextureArrayWithData(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height, const Color* sliceColors, uint32_t sliceCount); +Babylon::Graphics::TextureT CreateRenderTargetTexture(Babylon::Graphics::DeviceT device, uint32_t width, uint32_t height); +std::vector ReadBackRenderTarget(Babylon::Graphics::DeviceT device, Babylon::Graphics::TextureT texture, uint32_t width, uint32_t height); +void DestroyRenderTargetTexture(Babylon::Graphics::TextureT texture); + +void DispatchSync(Babylon::AppRuntime& runtime, std::function func); \ No newline at end of file diff --git a/Core/Graphics/Source/BgfxCallback.cpp b/Core/Graphics/Source/BgfxCallback.cpp index f0d394ab2..5c2a86b8b 100644 --- a/Core/Graphics/Source/BgfxCallback.cpp +++ b/Core/Graphics/Source/BgfxCallback.cpp @@ -25,14 +25,17 @@ namespace Babylon::Graphics void BgfxCallback::fatal(const char* filePath, uint16_t line, bgfx::Fatal::Enum code, const char* str) { + // Always log fatal errors to stderr so they appear in CI logs. + trace(filePath, line, "BGFX FATAL 0x%08x: %s\n", code, str); + fprintf(stderr, "BGFX FATAL %s (%d): 0x%08x: %s\n", filePath, line, code, str); + fflush(stderr); + if (bgfx::Fatal::DebugCheck == code) { bx::debugBreak(); } else { - trace(filePath, line, "BGFX 0x%08x: %s\n", code, str); - BX_UNUSED(code, str); abort(); } } diff --git a/Install/Test/CMakeLists.txt b/Install/Test/CMakeLists.txt index dbf87e910..31a2c900e 100644 --- a/Install/Test/CMakeLists.txt +++ b/Install/Test/CMakeLists.txt @@ -45,7 +45,6 @@ if(WIN32 AND NOT WINDOWS_STORE) "${SOURCE_DIR}/App.Win32.cpp" "${SOURCE_DIR}/Tests.Device.D3D11.cpp" "${SOURCE_DIR}/Tests.ExternalTexture.cpp" - "${SOURCE_DIR}/Tests.ExternalTexture.D3D11.cpp" "${SOURCE_DIR}/Tests.JavaScript.cpp" "${SOURCE_DIR}/Tests.ShaderCache.cpp" "${SOURCE_DIR}/Utils.D3D11.cpp" diff --git a/Plugins/ExternalTexture/CMakeLists.txt b/Plugins/ExternalTexture/CMakeLists.txt index e3fd2786f..56fda4405 100644 --- a/Plugins/ExternalTexture/CMakeLists.txt +++ b/Plugins/ExternalTexture/CMakeLists.txt @@ -13,7 +13,6 @@ target_include_directories(ExternalTexture target_link_libraries(ExternalTexture PUBLIC napi PUBLIC GraphicsDevice - PRIVATE JsRuntimeInternal PRIVATE GraphicsDeviceContext) target_compile_definitions(ExternalTexture diff --git a/Plugins/ExternalTexture/Include/Babylon/Plugins/ExternalTexture.h b/Plugins/ExternalTexture/Include/Babylon/Plugins/ExternalTexture.h index 1834ff9b8..a84d0d765 100644 --- a/Plugins/ExternalTexture/Include/Babylon/Plugins/ExternalTexture.h +++ b/Plugins/ExternalTexture/Include/Babylon/Plugins/ExternalTexture.h @@ -1,16 +1,16 @@ #pragma once -#include +#include #include #include #include namespace Babylon::Plugins { + // All operations of this class must be called from the graphics thread unless otherwise noted. class ExternalTexture final { public: - // NOTE: Must call from the Graphics thread. ExternalTexture(Graphics::TextureT, std::optional = {}); ~ExternalTexture(); @@ -31,13 +31,14 @@ namespace Babylon::Plugins // Returns the underlying texture. Graphics::TextureT Get() const; - // Adds this texture to the graphics context of the given N-API environment. - // NOTE: Must call from the JavaScript thread. - Napi::Promise AddToContextAsync(Napi::Env, std::optional layerIndex = {}) const; + // Creates a JavaScript value wrapping this external texture. + // Wrap the returned value with `engine.wrapNativeTexture` on the JS side to get a Babylon.js `InternalTexture`. + // This method must be called from the JavaScript thread. The caller must ensure no other thread + // is concurrently calling any other operations on this object, including move operations. + Napi::Value CreateForJavaScript(Napi::Env) const; // Updates to a new texture. - // NOTE: Must call from the Graphics thread. - void Update(Graphics::TextureT, std::optional = {}, std::optional layerIndex = {}); + void Update(Graphics::TextureT, std::optional = {}); private: class Impl; diff --git a/Plugins/ExternalTexture/Readme.md b/Plugins/ExternalTexture/Readme.md index 6ddf457c1..56ec57c0b 100644 --- a/Plugins/ExternalTexture/Readme.md +++ b/Plugins/ExternalTexture/Readme.md @@ -25,80 +25,52 @@ To set the ExternalTexture to be used as a render target by Babylon.js one must int width = 1024; // Your render target width. int height = 768; // Your render target height. -std::promise textureCreationSubmitted {}; -std::promise textureCreationDone {}; - // Create an ExternalTexture from an ID3D12Resource. auto externalTexture = std::make_shared(d3d12Resource); -jsRuntime.Dispatch([&externalTexture, &textureCreationSubmitted, width, height, &textureCreationDone](Napi::Env env) +jsRuntime.Dispatch([&externalTexture, width, height](Napi::Env env) { // Creates a JS object that can be used by the Babylon Engine to create a render texture. - auto jsPromisse = externalTexture->AddToContextAsync(env); + auto jsTexture = externalTexture->CreateForJavaScript(env); auto result = env.Global().Get("YOUR_JS_FUNCTION").As().Call( { - jsPromisse, + jsTexture, Napi::Value::From(env, width), Napi::Value::From(env, height), - Napi::Function::New(env, [&textureCreationDone](const Napi::CallbackInfo& info) - { - textureCreationDone.set_value(); - }) }); - textureCreationSubmitted.set_value(); }); - -// Wait for texture creation to be submitted. -textureCreationSubmitted.get_future().get(); - -// Run 1 render loop so the texture can get created. -m_update->Finish(); -m_device->FinishRenderingCurrentFrame(); -m_device->StartRenderingCurrentFrame(); -m_update->Start(); - -// Wait for callback to confirm the texture is created on the JS side. -textureCreated.get_future().get(); ``` The usual JS function to assign the external texture object as a render target for the Babylon scene camera looks like the following: ```js -function YOUR_JS_FUNCTION(externalTexturePromisse, width, height, textureCreatedCallback) { - externalTexturePromisse.then((externalTexture) => { - const outputTexture = engine.wrapNativeTexture(externalTexture); - scene.activeCamera.outputRenderTarget = new BABYLON.RenderTargetTexture( - "ExternalTexture", - { - width: width, - height: height, - }, - scene, - { - colorAttachment: outputTexture, - generateDepthBuffer: true, - generateStencilBuffer: true, - } - ); - textureCreatedCallback(); - }); +function YOUR_JS_FUNCTION(externalTexture, width, height) { + const outputTexture = engine.wrapNativeTexture(externalTexture); + scene.activeCamera.outputRenderTarget = new BABYLON.RenderTargetTexture( + "ExternalTexture", + { + width: width, + height: height, + }, + scene, + { + colorAttachment: outputTexture, + generateDepthBuffer: true, + generateStencilBuffer: true, + } + ); } ``` ## ExternalTexture class -`Babylon::Plugins::ExternalTexture` is a class that can be constructed with a native texture and then used to send to the JavaScript environment using `ExternalTexture::AddToContextAsync`. It is important to note that the constructor will only store the necessary information to convert the native texture to the bgfx texture, but it will _not_ create bgfx texture. This class will hold a strong reference to the native texture when possible. The native texture ownership will be shared with JS when `ExternalTexture::AddToContextAsync` is called. It is safe to destroy this class before `ExternalTexture::AddToContextAsync` async operation completes. +`Babylon::Plugins::ExternalTexture` is a class that can be constructed with a native texture and then used to create a JavaScript texture object via `ExternalTexture::CreateForJavaScript`. The constructor stores the necessary information about the native texture (dimensions, format, flags) and holds a strong reference to the native texture when possible. This class assumes that the native texture was created using the same graphics device used to create the Babylon::Device. See [Properly Initialize `Babylon::Graphics::Device`](#properly-initialize-babylongraphicsdevice). -The following will happen inside a call to `ExternalTexture::AddToContextAsync`: - -- A `Napi::Promise` will be created to encapsulate the async operation over a frame. -- During `Babylon::Graphics::DeviceContext::BeforeRenderScheduler()`, a new dummy bgfx texture will be created. -- During `Babylon::Graphics::DeviceContext::AfterRenderScheduler()`, this bgfx texture will be overridden with the native texture. -- On the JS thread, a `Napi::Pointer` will be created to hold the texture and the JS promise will resolved with this object. +`ExternalTexture::CreateForJavaScript` synchronously returns a `Napi::Value` wrapping a bgfx texture handle. The native texture backing is applied via `bgfx::overrideInternal` on the next render frame. The returned value can be passed to `engine.wrapNativeTexture` on the JS side. -It is safe to create multiple JS objects from the same `Babylon::Plugins::ExternalTexture` via `ExternalTexture::AddToContextAsync`. +It is safe to create multiple JS objects from the same `Babylon::Plugins::ExternalTexture` via `ExternalTexture::CreateForJavaScript`. Once the JS texture is available on the JS side, use `engine.wrapNativeTexture` to create an Babylon.js `InternalTexture`. diff --git a/Plugins/ExternalTexture/Source/ExternalTexture_Base.h b/Plugins/ExternalTexture/Source/ExternalTexture_Base.h index f3da6a7ff..d3eb3d6f4 100644 --- a/Plugins/ExternalTexture/Source/ExternalTexture_Base.h +++ b/Plugins/ExternalTexture/Source/ExternalTexture_Base.h @@ -2,13 +2,27 @@ #include #include +#include #include #include #include #include +#include namespace Babylon::Plugins { + namespace + { + template + uintptr_t NativeHandleToUintPtr(T value) + { + if constexpr (std::is_pointer_v) + return reinterpret_cast(value); + else + return static_cast(value); + } + } + class ExternalTexture::ImplBase { public: @@ -19,27 +33,25 @@ namespace Babylon::Plugins uint16_t NumLayers() const { return m_info.NumLayers; } uint64_t Flags() const { return m_info.Flags; } - void AddHandle(bgfx::TextureHandle handle) + void AddTexture(Graphics::Texture* texture) { - std::scoped_lock lock{m_mutex}; - - if (!m_handles.insert(handle).second) + if (!m_textures.insert(texture).second) { - assert(!"Failed to insert handle"); + assert(!"Failed to insert texture"); } } - void RemoveHandle(bgfx::TextureHandle handle) + void RemoveTexture(Graphics::Texture* texture) { - std::scoped_lock lock{m_mutex}; - - auto it = m_handles.find(handle); - if (it != m_handles.end()) + auto it = m_textures.find(texture); + if (it != m_textures.end()) { - m_handles.erase(it); + m_textures.erase(it); } } + std::mutex& Mutex() const { return m_mutex; } + protected: static bool IsFullMipChain(uint16_t mipLevel, uint16_t width, uint16_t height) { @@ -63,16 +75,27 @@ namespace Babylon::Plugins return BGFX_TEXTURE_NONE; } - void UpdateHandles(Graphics::TextureT ptr, std::optional layerIndex) + void UpdateTextures(Graphics::TextureT ptr) { - std::scoped_lock lock{m_mutex}; - - for (auto handle : m_handles) + for (auto* texture : m_textures) { - if (bgfx::overrideInternal(handle, uintptr_t(ptr), layerIndex.value_or(0)) == 0) + bgfx::TextureHandle handle = bgfx::createTexture2D( + Width(), + Height(), + HasMips(), + NumLayers(), + Format(), + Flags(), + 0, + NativeHandleToUintPtr(ptr) + ); + + if (!bgfx::isValid(handle)) { - assert(!"Failed to override texture"); + throw std::runtime_error{"Failed to create external texture"}; } + + texture->Attach(handle, true, m_info.Width, m_info.Height, HasMips(), m_info.NumLayers, m_info.Format, m_info.Flags); } } @@ -89,15 +112,7 @@ namespace Babylon::Plugins Info m_info{}; private: - struct TextureHandleLess - { - bool operator()(const bgfx::TextureHandle& a, const bgfx::TextureHandle& b) const - { - return a.idx < b.idx; - } - }; - mutable std::mutex m_mutex{}; - std::set m_handles{}; + std::set m_textures{}; }; } diff --git a/Plugins/ExternalTexture/Source/ExternalTexture_D3D11.cpp b/Plugins/ExternalTexture/Source/ExternalTexture_D3D11.cpp index 333c012e4..bb49a0c3e 100644 --- a/Plugins/ExternalTexture/Source/ExternalTexture_D3D11.cpp +++ b/Plugins/ExternalTexture/Source/ExternalTexture_D3D11.cpp @@ -163,7 +163,7 @@ namespace Babylon::Plugins public: // Implemented in ExternalTexture_Shared.h Impl(Graphics::TextureT, std::optional); - void Update(Graphics::TextureT, std::optional, std::optional); + void Update(Graphics::TextureT, std::optional); Graphics::TextureT Get() const { diff --git a/Plugins/ExternalTexture/Source/ExternalTexture_D3D12.cpp b/Plugins/ExternalTexture/Source/ExternalTexture_D3D12.cpp index 0936118a6..8d9b29915 100644 --- a/Plugins/ExternalTexture/Source/ExternalTexture_D3D12.cpp +++ b/Plugins/ExternalTexture/Source/ExternalTexture_D3D12.cpp @@ -163,7 +163,7 @@ namespace Babylon::Plugins public: // Implemented in ExternalTexture_Shared.h Impl(Graphics::TextureT, std::optional); - void Update(Graphics::TextureT, std::optional, std::optional); + void Update(Graphics::TextureT, std::optional); Graphics::TextureT Get() const { diff --git a/Plugins/ExternalTexture/Source/ExternalTexture_Metal.cpp b/Plugins/ExternalTexture/Source/ExternalTexture_Metal.cpp index e8a01e359..f96526b0b 100644 --- a/Plugins/ExternalTexture/Source/ExternalTexture_Metal.cpp +++ b/Plugins/ExternalTexture/Source/ExternalTexture_Metal.cpp @@ -251,7 +251,7 @@ namespace Babylon::Plugins public: // Implemented in ExternalTexture_Shared.h Impl(Graphics::TextureT, std::optional); - void Update(Graphics::TextureT, std::optional, std::optional); + void Update(Graphics::TextureT, std::optional); Graphics::TextureT Get() const { diff --git a/Plugins/ExternalTexture/Source/ExternalTexture_OpenGL.cpp b/Plugins/ExternalTexture/Source/ExternalTexture_OpenGL.cpp index 14b4e6343..95dcbcbd0 100644 --- a/Plugins/ExternalTexture/Source/ExternalTexture_OpenGL.cpp +++ b/Plugins/ExternalTexture/Source/ExternalTexture_OpenGL.cpp @@ -16,7 +16,7 @@ namespace Babylon::Plugins public: // Implemented in ExternalTexture_Shared.h Impl(Graphics::TextureT, std::optional); - void Update(Graphics::TextureT, std::optional, std::optional); + void Update(Graphics::TextureT, std::optional); Graphics::TextureT Get() const { diff --git a/Plugins/ExternalTexture/Source/ExternalTexture_Shared.h b/Plugins/ExternalTexture/Source/ExternalTexture_Shared.h index f0362487a..6f72a6ecf 100644 --- a/Plugins/ExternalTexture/Source/ExternalTexture_Shared.h +++ b/Plugins/ExternalTexture/Source/ExternalTexture_Shared.h @@ -14,7 +14,7 @@ namespace Babylon::Plugins Set(ptr); } - void ExternalTexture::Impl::Update(Graphics::TextureT ptr, std::optional overrideFormat, std::optional layerIndex) + void ExternalTexture::Impl::Update(Graphics::TextureT ptr, std::optional overrideFormat) { Info info; GetInfo(ptr, overrideFormat, info); @@ -25,7 +25,7 @@ namespace Babylon::Plugins m_info = info; Set(ptr); - UpdateHandles(ptr, layerIndex); + UpdateTextures(ptr); } ExternalTexture::ExternalTexture(Graphics::TextureT ptr, std::optional overrideFormat) @@ -45,83 +45,84 @@ namespace Babylon::Plugins uint32_t ExternalTexture::Width() const { + std::scoped_lock lock{m_impl->Mutex()}; + return m_impl->Width(); } uint32_t ExternalTexture::Height() const { + std::scoped_lock lock{m_impl->Mutex()}; + return m_impl->Height(); } Graphics::TextureT ExternalTexture::Get() const { + std::scoped_lock lock{m_impl->Mutex()}; + return m_impl->Get(); } - Napi::Promise ExternalTexture::AddToContextAsync(Napi::Env env, std::optional layerIndex) const + Napi::Value ExternalTexture::CreateForJavaScript(Napi::Env env) const { - Graphics::DeviceContext& context = Graphics::DeviceContext::GetFromJavaScript(env); - JsRuntime& runtime = JsRuntime::GetFromJavaScript(env); - - auto deferred{Napi::Promise::Deferred::New(env)}; - auto promise{deferred.Promise()}; + std::scoped_lock lock{m_impl->Mutex()}; - DEBUG_TRACE("ExternalTexture [0x%p] AddToContextAsync", m_impl.get()); - - arcana::make_task(context.BeforeRenderScheduler(), arcana::cancellation_source::none(), [&context, &runtime, deferred = std::move(deferred), impl = m_impl, layerIndex = std::move(layerIndex)]() mutable { - // REVIEW: The bgfx texture handle probably needs to be an RAII object to make sure it gets clean up during the asynchrony. - // For example, if any of the schedulers/dispatches below don't fire, then the texture handle will leak. - bgfx::TextureHandle handle = bgfx::createTexture2D(impl->Width(), impl->Height(), impl->HasMips(), impl->NumLayers(), impl->Format(), impl->Flags()); - DEBUG_TRACE("ExternalTexture [0x%p] create %d x %d %d mips %d layers. Format : %d Flags : %d. (bgfx handle id %d)", - impl.get(), int(impl->Width()), int(impl->Height()), int(impl->HasMips()), int(impl->NumLayers()), int(impl->Format()), int(impl->Flags()), int(handle.idx)); - if (!bgfx::isValid(handle)) - { - DEBUG_TRACE("ExternalTexture [0x%p] is not valid", impl.get()); - runtime.Dispatch([deferred{std::move(deferred)}](Napi::Env env) { - deferred.Reject(Napi::Error::New(env, "Failed to create native texture").Value()); - }); - - return; - } + Graphics::DeviceContext& context = Graphics::DeviceContext::GetFromJavaScript(env); - arcana::make_task(context.AfterRenderScheduler(), arcana::cancellation_source::none(), [&runtime, &context, deferred = std::move(deferred), handle, impl = std::move(impl), layerIndex = std::move(layerIndex)]() mutable { - if (bgfx::overrideInternal(handle, uintptr_t(impl->Get()), layerIndex.value_or(0)) == 0) - { - runtime.Dispatch([deferred = std::move(deferred), handle](Napi::Env env) { - bgfx::destroy(handle); - deferred.Reject(Napi::Error::New(env, "Failed to override native texture").Value()); - }); + // Create a placeholder bgfx texture. The native resource backing is + // applied via overrideInternal on the AfterRenderScheduler, which runs + // during bgfx::frame(). This two-step approach is required because + // bgfx's _external parameter to createTexture2D causes + // CreateShaderResourceView failures on WARP (E_INVALIDARG). + // The caller must pump one frame (FinishRenderingCurrentFrame) before + // the texture is usable for rendering. + bgfx::TextureHandle handle = bgfx::createTexture2D( + m_impl->Width(), + m_impl->Height(), + m_impl->HasMips(), + m_impl->NumLayers(), + m_impl->Format(), + m_impl->Flags() + ); + + DEBUG_TRACE("ExternalTexture [0x%p] CreateForJavaScript %d x %d %d mips %d layers. Format : %d Flags : %d. (bgfx handle id %d)", + m_impl.get(), int(m_impl->Width()), int(m_impl->Height()), int(m_impl->HasMips()), int(m_impl->NumLayers()), int(m_impl->Format()), int(m_impl->Flags()), int(handle.idx)); + + if (!bgfx::isValid(handle)) + { + throw Napi::Error::New(env, "Failed to create external texture"); + } - return; - } + // Schedule the native resource override for the render thread. + arcana::make_task(context.AfterRenderScheduler(), arcana::cancellation_source::none(), + [handle, impl = m_impl]() { + bgfx::overrideInternal(handle, NativeHandleToUintPtr(impl->Get())); + }); - runtime.Dispatch([deferred = std::move(deferred), handle, &context, impl = std::move(impl)](Napi::Env env) mutable { - auto* texture = new Graphics::Texture{context}; - DEBUG_TRACE("ExternalTexture [0x%p] attach %d x %d %d mips. Format : %d Flags : %d. (bgfx handle id %d)", - impl.get(), int(impl->Width()), int(impl->Height()), int(impl->HasMips()), int(impl->Format()), int(impl->Flags()), int(handle.idx)); - texture->Attach(handle, true, impl->Width(), impl->Height(), impl->HasMips(), 1, impl->Format(), impl->Flags()); + auto* texture = new Graphics::Texture{context}; + texture->Attach(handle, true, m_impl->Width(), m_impl->Height(), m_impl->HasMips(), m_impl->NumLayers(), m_impl->Format(), m_impl->Flags()); - impl->AddHandle(texture->Handle()); + m_impl->AddTexture(texture); - auto jsObject = Napi::Pointer::Create(env, texture, [texture, weakImpl = std::weak_ptr{impl}] { - if (auto impl = weakImpl.lock()) - { - impl->RemoveHandle(texture->Handle()); - } + auto jsObject = Napi::Pointer::Create(env, texture, [texture, weakImpl = std::weak_ptr{m_impl}] { + if (auto impl = weakImpl.lock()) + { + std::scoped_lock lock{impl->Mutex()}; - delete texture; - }); + impl->RemoveTexture(texture); + } - deferred.Resolve(jsObject); - }); - }); + delete texture; }); - return promise; + return jsObject; } - void ExternalTexture::Update(Graphics::TextureT ptr, std::optional overrideFormat, std::optional layerIndex) + void ExternalTexture::Update(Graphics::TextureT ptr, std::optional overrideFormat) { - m_impl->Update(ptr, overrideFormat, layerIndex); + std::scoped_lock lock{m_impl->Mutex()}; + + m_impl->Update(ptr, overrideFormat); } }