Skip to content

Add ExternalTexture.RestoreAfterDeviceLoss test#1716

Merged
bghgary merged 12 commits into
BabylonJS:masterfrom
bghgary:wrapped-native-texture-restore
May 29, 2026
Merged

Add ExternalTexture.RestoreAfterDeviceLoss test#1716
bghgary merged 12 commits into
BabylonJS:masterfrom
bghgary:wrapped-native-texture-restore

Conversation

@bghgary
Copy link
Copy Markdown
Contributor

@bghgary bghgary commented May 22, 2026

End-to-end test for BJS #18469 — External-source RT wrappers survive a Native device-loss/restore.

Flow

wrapNativeTexture on deviceA → render red → readback → DisableRenderingUpdateDevice(deviceB) → next StartRenderingCurrentFrame fires BJS _restoreEngineAfterContextLost (#18469's skip keeps the wrapped RT intact) → updateWrappedNativeTexture → render blue → readback.

D3D11-only via Helpers::CreateDevice(). UnhandledExceptionHandler calls std::quick_exit(1) (matching Tests.JavaScript.cpp et al.) so a BJS regression that rebuilds the External wrapper terminates loudly.

Requires @babylonjs/core ≥ 9.9.1.

Local verification

  • Positive: PASSES; BJS logs "WebGL context successfully restored."
  • Negative: stubbing #18469's External-source skip out of local babylon.max.js → test exits 1 on uncaught Unsupported texture format inside _rebuildRenderTargetWrappers.

[Created by Copilot on behalf of @bghgary]

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>
Copilot AI review requested due to automatic review settings May 22, 2026 00:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 updateWrappedNativeTexture to 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.

Comment thread Apps/UnitTests/JavaScript/src/tests.externalTexture.deviceLoss.ts
bghgary and others added 6 commits May 21, 2026 17:18
…-> 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>
@bghgary bghgary force-pushed the wrapped-native-texture-restore branch from 5c4b933 to ada86c1 Compare May 26, 2026 22:05
bghgary and others added 4 commits May 28, 2026 10:20
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>
@bghgary bghgary changed the title Add wrapped-native-texture device-loss restore unit test Add ExternalTexture.RestoreAfterDeviceLoss test May 29, 2026
Comment thread Apps/UnitTests/JavaScript/src/tests.externalTexture.deviceLoss.ts
@bghgary bghgary merged commit 8df2d91 into BabylonJS:master May 29, 2026
28 checks passed
@bghgary bghgary deleted the wrapped-native-texture-restore branch May 29, 2026 19:57
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants