Add synchronous ExternalTexture.CreateForJavaScript and deprecate AddToContextAsync#1646
Add synchronous ExternalTexture.CreateForJavaScript and deprecate AddToContextAsync#1646bghgary wants to merge 32 commits intoBabylonJS:masterfrom
Conversation
- Add Tests.ExternalTexture.Render.cpp: end-to-end test that renders a texture array through a ShaderMaterial to an external render target, verifying each slice (red, green, blue) via pixel readback. - Add tests.externalTexture.render.ts: JS test with sampler2DArray shader. - Add RenderDoc.h/cpp to UnitTests for optional GPU capture support. - Add Utils helpers: CreateTestTextureArrayWithData, CreateRenderTargetTexture, ReadBackRenderTarget, DestroyRenderTargetTexture (D3D11, Metal, stubs for D3D12/OpenGL). - Fix ExternalTexture_Shared.h: pass m_impl->NumLayers() instead of hardcoded 1 in Attach(), preserving texture array metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove layerIndex parameter from Impl::Update declaration to match the updated signature in ExternalTexture_Shared.h. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate HeadlessScreenshotApp, StyleTransferApp, and PrecompiledShaderTest from AddToContextAsync (promise-based) to CreateForJavaScript (synchronous). This removes the extra frame pump and promise callbacks that were previously required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…llers - Move RenderDoc.h/cpp to D3D11-only CMake block with HAS_RENDERDOC define - Guard RenderDoc calls with HAS_RENDERDOC instead of WIN32 - Update StyleTransferApp and PrecompiledShaderTest to use CreateForJavaScript Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On OpenGL, TextureT is unsigned int (not a pointer), so reinterpret_cast fails. Add NativeHandleToUintPtr helper using if constexpr to handle both pointer types (D3D11/Metal/D3D12) and integer types (OpenGL). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RenderDoc.h/cpp now accept void* device instead of ID3D11Device* - Move RenderDoc to WIN32 block (not D3D11-only) since it works with any API - Fix OpenGL build: use NativeHandleToUintPtr helper for TextureT cast - Add Linux support (dlopen librenderdoc.so) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move DestroyTestTexture after FinishRenderingCurrentFrame so bgfx::frame() processes the texture creation command before the native resource is released. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
8d48bcb to
bb4ec86
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ensure the JS startup dispatch completes before calling deviceUpdate.Finish() and FinishRenderingCurrentFrame(). This guarantees that bgfx::frame() processes the CreateForJavaScript texture creation command, making the texture available for subsequent render frames. The old async API had an implicit sync point (addToContext.wait) that the new sync API lost. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eation - Rename CreateForJavaScriptWithTextureArray to CreateForJavaScript and use arraySize=1 since texture array rendering is covered by RenderTextureArray. The old test crashed on CI (STATUS_BREAKPOINT in bgfx when creating texture arrays via encoder on WARP). - Revert two-step create+override approach back to single createTexture2D call with _external parameter (overrideInternal from JS thread doesn't work since the D3D11 resource isn't created until bgfx::frame). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CreateForJavaScript already exists in Tests.ExternalTexture.cpp, causing a linker duplicate symbol error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… test) The D3D11-specific CreateForJavaScript test crashed on CI due to bgfx assertions when calling createTexture2D with _external on the encoder thread. The cross-platform CreateForJavaScript test in Tests.ExternalTexture.cpp already covers this functionality. The texture array rendering is covered by RenderTextureArray. Also revert app startup ordering to Finish->Wait (matching the pattern used by HeadlessScreenshotApp on master). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bgfx callback's fatal() handler was silently calling debugBreak() on DebugCheck assertions with no output, making CI crashes impossible to diagnose. Now logs the file, line, error code and message to stderr before breaking, so the assertion details appear in CI logs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add DISM/d3dconfig step to CI to enable D3D debug layer, which will provide detailed D3D11 validation messages for the CreateShaderResourceView E_INVALIDARG failure. Kept the _external createTexture2D path (reverted the AfterRenderScheduler approach) so we can see the actual D3D debug output that explains the SRV mismatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bgfx _external texture path triggers E_INVALIDARG in CreateShaderResourceView on CI's WARP D3D11 driver. The overrideInternal alternative doesn't support full array textures (hardcodes ArraySize=1). Since the _external path works on real GPUs, skip the render test on CI via BABYLON_NATIVE_SKIP_RENDER_TESTS and keep the direct _external path. Also adds D3D debug layer enablement to CI for future diagnostics, and logs bgfx fatal errors to stderr before crashing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from _external parameter (crashes on WARP) to create+overrideInternal two-step approach. The overrideInternal path is compatible with WARP but sets ArraySize=1 in the SRV, so the RenderTextureArray test (which needs full array access) is skipped on CI. The render test works on real GPUs where the _external path succeeds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The overrideInternal call fires on AfterRenderScheduler after the first bgfx::frame(). An additional frame pump ensures the native texture backing is applied before the scene render. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove deleted Tests.ExternalTexture.D3D11.cpp from Install/Test/CMakeLists.txt - Add extra frame pump after CreateForJavaScript in HeadlessScreenshotApp and StyleTransferApp so overrideInternal has time to apply the native texture backing before the first render. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nder-test # Conflicts: # .github/jobs/win32.yml
…erResourceView The createTexture2D _external path now works on WARP after the bgfx update in master. Drop the placeholder + AfterRenderScheduler + overrideInternal two-step dance and the extra frame pump that existed to apply it. This also restores full array-slice SRV so RenderTextureArray no longer needs a sanitizer-only skip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove 'pump an extra frame' block from StyleTransferApp and HeadlessScreenshotApp (no longer needed after the _external revert). - Update ExternalTexture Readme to match the one-call createTexture2D path and direct-construction consumer pattern. - Drop unused DispatchSync declaration from UnitTests/Utils.h. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The SKIP_RENDER_TESTS flag was originally added because WARP could not handle the _external path in bgfx. bgfx PR BabylonJS#1669 fixed CreateShaderResourceView for the _external parameter, so the render test should pass on WARP now. Let CI verify. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reword BABYLON_NATIVE_SKIP_RENDER_TESTS option description. WARP is available on Win32, so 'no real GPU' is inaccurate; the real reason to skip is a missing per-backend test harness. - Add braces around if constexpr / else branches in NativeHandleToUintPtr per coding style. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR modernizes the ExternalTexture plugin by switching its JavaScript exposure from an async/promise-based flow to a synchronous API, fixing array-layer metadata propagation, and adding an end-to-end texture-array render validation test in UnitTests.
Changes:
- Replace
AddToContextAsync(promise-based) with synchronousCreateForJavaScript, and update consumers/tests/docs accordingly. - Fix the
numLayerspropagation bug (texture arrays now preserve layer count through attach/update) and add basic thread-safety via a shared mutex. - Add a new external texture array render test (C++ + JS harness), plus platform helper utilities and optional RenderDoc capture hooks.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Plugins/ExternalTexture/Source/ExternalTexture_Shared.h | Implements the synchronous CreateForJavaScript, adds locking in getters/update, and switches tracking to texture objects. |
| Plugins/ExternalTexture/Source/ExternalTexture_Base.h | Replaces handle tracking with Graphics::Texture* tracking and updates all attached textures on Update(). |
| Plugins/ExternalTexture/Source/ExternalTexture_D3D11.cpp | Updates platform impl signature to match new Update API. |
| Plugins/ExternalTexture/Source/ExternalTexture_D3D12.cpp | Updates platform impl signature to match new Update API. |
| Plugins/ExternalTexture/Source/ExternalTexture_Metal.cpp | Updates platform impl signature to match new Update API. |
| Plugins/ExternalTexture/Source/ExternalTexture_OpenGL.cpp | Updates platform impl signature to match new Update API. |
| Plugins/ExternalTexture/Include/Babylon/Plugins/ExternalTexture.h | Public API change: exposes CreateForJavaScript and updates threading contract. |
| Plugins/ExternalTexture/CMakeLists.txt | Removes dependency on JsRuntimeInternal now that texture creation is synchronous. |
| Plugins/ExternalTexture/Readme.md | Updates documentation sample code from async/promise flow to synchronous creation. |
| Install/Test/CMakeLists.txt | Removes the old D3D11-specific external texture test source from install tests. |
| Core/Graphics/Source/BgfxCallback.cpp | Ensures bgfx fatal errors are always printed to stderr (better CI visibility). |
| Apps/UnitTests/CMakeLists.txt | Adds render test sources/assets; introduces BABYLON_NATIVE_SKIP_RENDER_TESTS option. |
| Apps/UnitTests/Source/Tests.ExternalTexture.cpp | Updates unit test to validate synchronous CreateForJavaScript. |
| Apps/UnitTests/Source/Tests.ExternalTexture.Render.cpp | Adds new end-to-end rendering validation for texture arrays rendered into an external RT. |
| Apps/UnitTests/Source/Tests.ExternalTexture.D3D11.cpp | Removes legacy D3D11-only async/layer-index external texture test. |
| Apps/UnitTests/Source/Utils.h | Adds new cross-backend helpers for texture arrays, RT creation, and readback. |
| Apps/UnitTests/Source/Utils.D3D11.cpp | Implements texture-array creation, RT creation, and readback for D3D11. |
| Apps/UnitTests/Source/Utils.D3D12.cpp | Adds stubs for new helpers (currently not implemented for D3D12). |
| Apps/UnitTests/Source/Utils.Metal.mm | Implements texture-array creation, RT creation, and readback for Metal. |
| Apps/UnitTests/Source/Utils.OpenGL.cpp | Adds stubs for new helpers (currently not implemented for OpenGL). |
| Apps/UnitTests/Source/RenderDoc.h | Declares optional RenderDoc capture helpers for UnitTests. |
| Apps/UnitTests/Source/RenderDoc.cpp | Implements optional RenderDoc integration (guarded by RENDERDOC). |
| Apps/UnitTests/JavaScript/webpack.config.js | Adds the new external texture render-test bundle entry. |
| Apps/UnitTests/JavaScript/src/tests.externalTexture.render.ts | Adds JS-side shader/material setup and slice rendering for the render test. |
| Apps/UnitTests/JavaScript/dist/tests.externalTexture.render.js | Adds the built JS bundle consumed by UnitTests. |
| Apps/StyleTransferApp/Win32/App.cpp | Migrates consumer from async external texture creation to synchronous API. |
| Apps/PrecompiledShaderTest/Source/App.cpp | Migrates consumer from async external texture creation to synchronous API. |
| Apps/HeadlessScreenshotApp/Win32/App.cpp | Migrates consumer from async external texture creation to synchronous API. |
- RenderDoc.cpp: drop absolute Windows path; use plain #include "renderdoc_app.h" - Tests.ExternalTexture.Render.cpp: handle renderSlice rejection and add wait timeout to prevent test hangs - Tests.ExternalTexture.cpp: add ExternalTexture.Update test (Update() path was dropped in the refactor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Restore AddToContextAsync as a deprecated shim around CreateForJavaScript for source compat with existing consumers - Tighten render test: color tolerance 25 -> 2; require exact 100%% match (was 90%%) - Drop BABYLON_NATIVE_SKIP_RENDER_TESTS option; gate SKIP_RENDER_TESTS directly on GRAPHICS_API STREQUAL "D3D12" Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Matches the established pattern from ExternalTexture.AddToContextAsyncAndUpdate on master: wait for JS work to complete while the frame is still open, then Finish. The new CreateForJavaScript and RenderTextureArray tests in this PR had accidentally dropped this pattern, which works on master but deadlocks under the reworked threading model in BabylonJS#1652 (Finish closes the frame gate while JS is still acquiring FrameCompletionScopes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
[Responded by Copilot on behalf of @bghgary] Hit a deadlock in Symptom
Stack (key frames, top → bottom)Root cause
If the GC sweeps a previously-created This is a timing-dependent issue: it requires GC to fire inside the Suggested fixMove Napi::Value ExternalTexture::CreateForJavaScript(Napi::Env env) const
{
- std::scoped_lock lock{m_impl->Mutex()};
-
Graphics::DeviceContext& context = Graphics::DeviceContext::GetFromJavaScript(env);
+ std::scoped_lock lock{m_impl->Mutex()};
+
bgfx::TextureHandle handle = bgfx::createTexture2D(...);
...
}This was the minimal fix that unblocked the deadlocking scenario in our integration. The remaining lock scope still covers all A more general guideline that may be worth applying across the file: any path that calls into the JS engine while holding |
…e-mutex deadlock Reproduce the deadlock reported in BabylonJS#1646 (comment) without needing any Babylon scene, GC pressure, or external scheduling. The bug: CreateForJavaScript holds m_impl->Mutex() across the JS-side property lookup `Graphics::DeviceContext::GetFromJavaScript(env)`. That lookup can run user JS or engine GC/finalizers (e.g. a sibling Texture finalizer registered by Napi::Pointer::Create) which themselves re-enter m_impl->Mutex() on the same thread. On MSVC, recursively locking std::mutex throws std::system_error("resource_deadlock_would_occur"), which then escapes the AppRuntime dispatch lambda and triggers std::abort. The test redefines `_native._Graphics` as an accessor whose getter, when invoked, asks the ExternalTexture for its Width() (which also takes m_impl->Mutex()) and observes whether the lock is currently held on the caller's thread. With the bug present, Width() throws system_error and the test reports `recursiveLockObserved=true`. With the fix, the lookup runs before the mutex is acquired and Width() succeeds. Test is Win32-only because it relies on MSVC's std::mutex deadlock detection to surface the recursive lock as a recoverable exception. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The JS-side property lookup `Graphics::DeviceContext::GetFromJavaScript` can run engine GC and finalizers, which on Chakra can finalize a previously-collected Texture wrapper for the same ExternalTexture. That finalizer also takes m_impl->Mutex(). Holding the mutex across the lookup therefore re-enters the same std::mutex on the same thread and throws std::system_error on MSVC, which then escapes the AppRuntime dispatch lambda and triggers std::abort. DeviceContext is process-scoped and does not require the impl mutex, so move its resolution above the scoped_lock. The lock still covers all m_impl reads and the AddTexture call. Verified by the regression test added in the prior commit: ExternalTexture.CreateForJavaScriptDoesNotHoldImplMutexAcrossJsCallout fails before this change and passes after. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
[Responded by Copilot on behalf of @bghgary]
Thanks — confirmed and fixed in 415f193. The diagnosis above is correct: Fix (415f193): resolve Regression test (dd75619): Compare: https://github.com/BabylonJS/BabylonNative/compare/e453e9de..415f193f |
The regression test added in dd75619 uses the Node-API C api_define_properties to install a test accessor for `_native._Graphics`. The JSI Node-API shim does not expose that symbol, so the JSI build (e.g. Win32_x64_JSI_D3D11) failed to compile. Gate the test on `defined(NAPI_VERSION)`, which is defined by the shared Node-API header (Chakra) but not by the JSI Node-API shim. The bug being tested is also Chakra-specific (synchronous GC during a JS-side property lookup), so skipping on JSI matches the test's actual scope. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Overview
This PR adds a synchronous
CreateForJavaScriptAPI to the ExternalTexture plugin (deprecating the asyncAddToContextAsync), adds thread safety, fixes anumLayersbug, and adds an end-to-end rendering test.ExternalTexture: Synchronous API
The main change introduces
CreateForJavaScript(which synchronously returns aNapi::Value) as the replacement forAddToContextAsync(which returned aNapi::Promise).AddToContextAsyncis retained as a[[deprecated]]shim that wrapsCreateForJavaScriptin an already-resolvedNapi::Promise::Deferred, so existingawaitconsumers keep compiling.Old (async — required promises and a separate JS callback to signal completion):
C++:
JS:
New (sync — no promises, no callbacks):
C++:
Babylon::Plugins::ExternalTexture externalTexture{d3d12Resource}; jsRuntime.Dispatch([externalTexture = std::move(externalTexture)](Napi::Env env) { auto jsTexture = externalTexture.CreateForJavaScript(env); env.Global().Get("setup").As<Napi::Function>().Call({ jsTexture, Napi::Value::From(env, width), Napi::Value::From(env, height), }); });JS:
Internally,
CreateForJavaScriptnow callsbgfx::createTexture2D(..., _external)directly in a single call — the previous two-stepcreateTexture2D+bgfx::overrideInternaldance (and its companion "pump an extra frame" in consumers) is gone. The direct path was originally blocked by a WARPE_INVALIDARGonCreateShaderResourceView; a recent bgfx update (#1669) fixes it, so the workaround is no longer needed.Other ExternalTexture Changes
numLayersbug:CreateForJavaScriptpassed a hardcoded1fornumLayersinAttach(). Changed tom_impl->NumLayers()so texture array metadata (and SRVArraySize) is preserved.std::scoped_locktoWidth(),Height(),Get(),CreateForJavaScript(), andUpdate().UpdateAPI: RemovedlayerIndexparameter — the full texture (all layers) is always updated.AddTexture/RemoveTexture/UpdateTextures).New: External Texture Array Render Test
Tests.ExternalTexture.Render.cpp: Creates a 3-slice texture array (R/G/B), renders each slice via asampler2DArrayshader to an external render target, verifies pixels.tests.externalTexture.render.ts: JS test with GLSL shaders sampling from a texture array.RenderDoc.h/cpp: Optional GPU capture support (disabled by default; enable by definingRENDERDOC).CreateTestTextureArrayWithData,CreateRenderTargetTexture,ReadBackRenderTarget(D3D11, Metal; stubs for D3D12/OpenGL).GRAPHICS_API STREQUAL "D3D12"compile-time gate); D3D12 support is being added separately in Enable Unit Tests on Win32 D3D12 CI #1671.Consumer Updates
Apps/PrecompiledShaderTest,Apps/HeadlessScreenshotApp,Apps/StyleTransferApp: migrated to the sync API; removed the "pump an extra frame sooverrideInternalapplies" blocks.Testing
ExternalTexture.RenderTextureArraytest (4096/4096 pixels match on all 3 slices).PrecompiledShaderTest: 0/1,048,576 pixels differ vs reference.