Add ExternalTexture.RestoreAfterDeviceLoss test#1716
Merged
bghgary merged 12 commits intoMay 29, 2026
Conversation
End-to-end test for BJS #18469 `updateWrappedNativeTexture` on Native: 1. Create an external GPU texture, wrap it via `wrapNativeTexture`, render a red plane into it, readback → verify red pixels. 2. Create a SECOND GPU texture (simulating device restore), call `updateWrappedNativeTexture` to repoint the wrapper at the new handle without losing the InternalTexture identity. 3. Render a blue plane into the restored RTT, readback → verify blue pixels (confirms the new handle is wired up correctly and the framebuffer was rebuilt). Verified locally on Win32 D3D11 Debug: before=13456 red/0 blue, after=0 red/13456 blue. Requires a `babylon.max.js` build that includes BJS #18469 (merged 2026-05-21). The test will fail on the published npm 9.9.0 UMD because `updateWrappedNativeTexture` doesn't exist there yet — it will pass once the next patch version ships. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new end-to-end UnitTests scenario validating Babylon.js Native wrapped-native-texture “device loss / restore” behavior by swapping the underlying native texture handle and verifying rendering output changes accordingly.
Changes:
- Introduces a new C++ GTest that wraps an external GPU texture, renders/readbacks pre- and post-handle swap, and validates pixel colors.
- Adds a paired TypeScript test harness that creates the RTT over a wrapped native texture and calls
updateWrappedNativeTextureto repoint it. - Wires the new JS bundle into the UnitTests webpack build and UnitTests CMake asset/source lists.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| Apps/UnitTests/Source/Tests.WrappedNativeTexture.Restore.cpp | New GTest driving the two-phase render/readback workflow around a wrapped native texture handle swap. |
| Apps/UnitTests/JavaScript/webpack.config.js | Adds a new webpack entry for the wrapped-native-texture restore test bundle. |
| Apps/UnitTests/JavaScript/src/tests.wrappedNativeTexture.restore.ts | JS-side setup/render/restore functions that exercise wrapNativeTexture + updateWrappedNativeTexture. |
| Apps/UnitTests/JavaScript/dist/tests.wrappedNativeTexture.restore.js | Generated bundle for the new TypeScript test entry. |
| Apps/UnitTests/CMakeLists.txt | Registers the new C++ test source and JS asset so it’s built and packaged into UnitTests. |
…-> 200) The test's playground uses fire-and-forget parseFromSnippetAsync to fetch a GUI layout over the network. With renderCount=50, the async fetch may not complete before the screenshot is captured on slow CI runners (Ubuntu_Clang_JSC). The rendered image shows the dark 3D background instead of the GUI text panel, producing ~10K pixel diff. This is the flake that has hit Ubuntu_Clang_JSC on multiple PRs (ran=107 passed=106 failed=1, always this test). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The nightly BN pipeline artifact may lag behind npm. When the bundled UMD doesn't include updateWrappedNativeTexture (requires @babylonjs/core >= 9.9.1), throw immediately so the C++ promise rejects and the test fails fast instead of hanging until CI timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GUI snippet tests (Load GUI snippet with unicode, Parse GUI json with
unicode) called parseFromSnippetAsync / parseFromURLAsync fire-and-forget.
On slow CI runners the async fetch hadn't completed by the capture frame,
causing intermittent failures.
Fix: use config.json `replace` to transform the fire-and-forget pattern
into `return adv.parseFromSnippetAsync(...).then(() => { return scene; })`,
making createScene return a promise. The BN runner already handles async
createScene (`currentScene.then`), so no renderCount hack needed.
Also:
- Port BJS's `guiIsReady()` gate: defer frame counting until all
AdvancedDynamicTextures report ready (handles image loading after
GUI parse completes).
- Fix `const` -> `let` for scriptToRun reassignment in the
scriptToRun code path (latent strict-mode bug).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update playground snippet versions to match BJS config where the code is unchanged (no visual diff, no new reference images needed): - Solid particle system: #WCDZS#92 -> BabylonJS#531 (v2 snippet format, same code) - Volumetric Light Scattering + Morph Targets: #5E318S#5 -> BabylonJS#7 (same code) - wgsl-in-shadermaterial: #8RU8Q3#157 -> BabylonJS#193 (same code) The latter two are excludeFromAutomaticTesting so no CI impact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update package-lock.json to resolve babylonjs@9.9.1 (was pinned to 9.3.4). This version includes updateWrappedNativeTexture needed by the restore test. Rebuilds webpack bundles against the new @babylonjs/core. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
BJS PR #18506 created new snippet revisions that return `Promise<Scene>` so the test framework can properly await async setup: - `#YS93KY#1` Load GUI snippet with unicode - `#ERVGT5#1` Parse GUI json with unicode Switching BN to those revisions makes the `replace` workaround unnecessary, and the `guiIsReady` gate I had ported from the old BJS runner is no longer needed either (BJS dropped it in the same PR). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5c4b933 to
ada86c1
Compare
The #1 revisions of these snippets return Promise<Scene> for proper async await, but BN hits a bgfx dynamic-texture blit race on Win32 D3D11 and Ubuntu OpenGL that causes the GUI to render with stale content on the first capture frame. The race is separate from the snippet contract and is being addressed in a separate PR (either via the threading model rework or an immediate-blit fix in NativeEngine). Reverting to the #0 snippets with the historical renderCount:50 buys an extra ~50 frames of rendering before capture, which masks the race. Once the proper fix lands, this commit should be reverted to put us back on the #1 snippets without renderCount. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`engine.wrapNativeTexture` / `engine.updateWrappedNativeTexture` (BJS) are the JS-side counterparts to `ExternalTexture::AddToContextAsync` / `ExternalTexture::Update` (BN) -- same feature, two abstraction layers. Bucket the test with the existing `Tests.ExternalTexture.*` family so the naming matches the rest of that suite. Also drop the stale-prone BJS PR reference from the file header comment; the API name and test body already explain what's being exercised. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match the suffix-as-category convention used by sibling files (Tests.ExternalTexture.Msaa.cpp, Tests.ExternalTexture.D3D11.cpp). The "DeviceLoss" suffix names the category so future device-loss-related tests (e.g. depth/stencil restore, multi-RT) can live in the same file. The test name itself is unchanged: ExternalTexture.RestoreAfterDeviceLoss. Pure rename; CMakeLists, webpack entry, ScriptLoader path, and the JS file's header comment are updated to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eLoss
The original test created a second texture on the same device and just
swapped wrappers via updateWrappedNativeTexture -- it exercised the
JS-side wrapper-repoint API but never actually triggered a device-loss /
restore. A regression in BJS that broke restore for External-source RT
wrappers (e.g. removing the InternalTextureSource.External skip in
_rebuildRenderTargetWrappers) would not be caught.
Rewrite to drive the real flow:
1. Create explicit deviceA via Helpers::CreateDevice(), wrap a texture
on deviceA, render red, readback -- expect red.
2. device.DisableRendering() tears bgfx down on deviceA.
3. device.UpdateDevice(deviceB) re-points the Graphics::Device at a
fresh graphics device.
4. The next StartRenderingCurrentFrame triggers EnableRendering ->
bgfx::init on deviceB -> render-reset callback -> BJS
_restoreEngineAfterContextLost (Native override) ->
_rebuildGraphicsResources -> onContextRestoredObservable.
External-source RT wrappers and InternalTextures are skipped by
the rebuild walks, leaving the wrapped RT intact and waiting for
updateWrappedNativeTexture.
5. Create the new texture on deviceB (using deviceB directly --
device.GetPlatformInfo().Device returns stale data in the window
between DisableRendering and EnableRendering because it goes
through bgfx::getInternalData), wrap it, AddToContextAsync, call
restoreTexture -> updateWrappedNativeTexture.
6. Render blue, readback -- expect blue.
Switch the UnhandledExceptionHandler to std::quick_exit(1) to match the
existing majority pattern in the suite (Tests.JavaScript.cpp,
Tests.ShaderCache.cpp, Tests.ShaderCompilation.cpp,
Tests.UniformPadding.cpp) so a JS-side regression during the restore
flow (e.g. _rebuildRenderTargetWrappers trying to rebuild the External
wrapper) terminates the test loudly instead of being swallowed.
Verified locally on Win32 D3D11 RelWithDebInfo:
* Positive: passes with BJS 9.9.1 (#18469 skip present);
"WebGL context successfully restored." logged.
* Negative: removed the InternalTextureSource.External skip in
babylon.max.js _rebuildRenderTargetWrappers; test correctly
exits 1 with "[Uncaught Error] RuntimeError: Unsupported texture
format or type: format -1, type -1." inside
_rebuildRenderTargetWrappers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ryantrem
approved these changes
May 29, 2026
bghgary
added a commit
to bghgary/BabylonNative
that referenced
this pull request
May 29, 2026
Same fix as the MSAA migration: AddToContextAsync's between-frames .then() pattern deadlocks under BabylonJS#1652's threading model because SubmitCommands synchronously acquires a FrameCompletionScope which blocks when no frame is in progress. CreateForJavaScript runs synchronously inside the same dispatch lambda, so startup() / restoreTexture() execute in the same JS task as the wrap. No cross-frame .then() dance, no deadlock. Without this, all four Win32 D3D11 jobs hit the 60-minute timeout on the 'ExternalTexture.RestoreAfterDeviceLoss' test (added in BabylonJS#1716, merged into master after this branch's last sync). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
End-to-end test for BJS #18469 — External-source RT wrappers survive a Native device-loss/restore.
Flow
wrapNativeTextureondeviceA→ render red → readback →DisableRendering→UpdateDevice(deviceB)→ nextStartRenderingCurrentFramefires BJS_restoreEngineAfterContextLost(#18469's skip keeps the wrapped RT intact) →updateWrappedNativeTexture→ render blue → readback.D3D11-only via
Helpers::CreateDevice().UnhandledExceptionHandlercallsstd::quick_exit(1)(matchingTests.JavaScript.cppet al.) so a BJS regression that rebuilds the External wrapper terminates loudly.Requires
@babylonjs/core≥ 9.9.1.Local verification
"WebGL context successfully restored."babylon.max.js→ test exits 1 on uncaughtUnsupported texture formatinside_rebuildRenderTargetWrappers.[Created by Copilot on behalf of @bghgary]