Skip to content

Add synchronous CreateForJavaScript and deprecate AddToContextAsync in ExternalTexture#1646

Merged
bghgary merged 45 commits into
BabylonJS:masterfrom
bghgary:external-texture-render-test
Jun 9, 2026
Merged

Add synchronous CreateForJavaScript and deprecate AddToContextAsync in ExternalTexture#1646
bghgary merged 45 commits into
BabylonJS:masterfrom
bghgary:external-texture-render-test

Conversation

@bghgary

@bghgary bghgary commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

[Updated by Copilot on behalf of @bghgary]

Changes

CreateForJavaScript(env, layerIndex = {}) replaces the async AddToContextAsync (kept as a [[deprecated]] shim that wraps it), returning a Napi::Value directly instead of a Promise. Existing ExternalTexture render/MSAA/DeviceLoss tests are migrated to the sync API.

Recursive-mutex deadlock fix. CreateForJavaScript previously held the impl mutex across a JS property lookup that could run a sibling Texture finalizer on the same thread, re-entering the same std::mutex and aborting under MSVC. Graphics::DeviceContext is now resolved before the lock is taken, and the impl mutex is encapsulated so it is never held across a JS callout — making the bug shape structurally impossible.

Single-slice layer selection. CreateForJavaScript/Update take an optional layerIndex to view one array layer of a multi-layer (e.g. NV12 decoder frame-pool) texture, selected at bind time via the bgfx setTexture view API (#3728). Graphics::Texture carries the view sub-range (ViewFirstLayer/ViewNumLayers) and NativeEngine::SetTexture binds the single-slice view. Requires the bgfx.cmake bump to 940054e1.

bghgary and others added 8 commits March 24, 2026 22:05
- 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>
@bghgary bghgary force-pushed the external-texture-render-test branch from 8d48bcb to bb4ec86 Compare March 25, 2026 16:13
bghgary and others added 17 commits March 25, 2026 09:13
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>
@bghgary bghgary marked this pull request as ready for review April 22, 2026 23:27
Copilot AI review requested due to automatic review settings April 22, 2026 23:27
- 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>
bghgary and others added 3 commits April 27, 2026 13:09
The mutex on ExternalTexture::Impl bridges graphics-thread state
(m_info, m_ptr, m_textures) to JS-thread reads in CreateForJavaScript.
Previously the outer ExternalTexture class reached through Impl::Mutex()
to lock around bodies that mixed cached-field reads, bgfx calls, and
napi object allocation. That shape is what produced BabylonJS#1646: a JS object
allocation triggered a sibling Texture finalizer on the same thread,
which re-entered the same mutex and aborted under MSVC's std::mutex.

Move the lock inside Impl so the lock scope matches the actual
cross-thread boundary, no more, no less:

- ImplBase exposes two JS-thread entry points, CreateTexture (returns a
  fully-constructed Graphics::Texture*) and DestroyTexture (called from
  the napi finalizer). Each takes the mutex internally and contains no
  JS callouts, so the lock can never be held across user-visible JS
  execution. This structurally prevents the recursive-mutex bug at this
  call site -- not just the one already fixed in 415f193.

- Impl::Update self-locks around the m_info / m_ptr / UpdateTextures
  publish, with explicit braces around the locked region.

- The Mutex() accessor, AddTexture, and RemoveTexture are removed from
  ImplBase's public surface. m_mutex is no longer reachable from outside
  the impl.

- ExternalTexture::Width/Height/Get/Update no longer lock. Per the
  public contract documented in ExternalTexture.h, every operation
  except CreateForJavaScript is graphics-thread-only and therefore
  serialized against Update() by single-threaded execution. The
  previous internal locks on these getters did not actually make them
  safe to call from other threads (no happens-before with prior
  Updates) and only obscured the real synchronization boundary.

- ExternalTexture::CreateForJavaScript becomes a thin wrapper:
  resolve DeviceContext (JS lookup, must run unlocked), call
  Impl::CreateTexture, then Napi::Pointer::Create with a finalizer
  that delegates to Impl::DestroyTexture via weak_ptr.

No platform-specific Impl files (D3D11/D3D12/Metal/OpenGL) need
changes -- none of them touched the mutex or texture set.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bghgary

bghgary commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

[Responded by Copilot on behalf of @bghgary]

Follow-up on the recursive-lock issue and audit, after the fix in 415f193.

Refactor: encapsulate the Impl mutex (04bfb6b)

The original fix resolved DeviceContext before locking, which closes the immediate hole. But the underlying shape — ExternalTexture reaching through m_impl->Mutex() to lock around bodies that mix cached-field reads, bgfx calls, and napi allocation — is structurally fragile. Any future addition to a locked region that allocates a JS object (or triggers a finalizer chain) reopens the same recursive-lock failure mode.

Moved the lock inside Impl so its scope matches the actual cross-thread boundary, no more, no less:

  • ImplBase exposes two JS-thread entry points: CreateTexture (constructs a Graphics::Texture*) and DestroyTexture (called from the napi finalizer). Each takes the mutex internally and contains no JS callouts, so the lock can never be held across user-visible JS execution at this site.
  • Impl::Update self-locks around the m_info / m_ptr / UpdateTextures publish, with explicit braces around the critical section.
  • Mutex(), AddTexture, and RemoveTexture are removed from ImplBase's public surface; m_mutex is no longer reachable from outside the impl.
  • ExternalTexture::Width/Height/Get/Update no longer lock. Per the contract in ExternalTexture.h, every operation except CreateForJavaScript is graphics-thread-only and therefore already serialized against Update() by single-threaded execution. The internal locks on these getters did not actually make them safe to call from other threads (no happens-before vs prior Update writes) and only obscured the real boundary.
  • ExternalTexture::CreateForJavaScript becomes a thin wrapper: resolve DeviceContext (must run unlocked, per the original fix), call Impl::CreateTexture, then Napi::Pointer::Create with a finalizer that delegates to Impl::DestroyTexture via weak_ptr.

Platform-specific Impl files (D3D11/D3D12/Metal/OpenGL) need no changes — none touch the mutex or texture set.

Test removal: lock-probe regression test (148df39)

The CreateForJavaScriptDoesNotHoldImplMutexAcrossJsCallout test installed a JS getter on _native._Graphics to deterministically reproduce the recursive-lock condition. It worked, but on review it bought too little:

  • It validated this specific shape of fix (lock not held across the GetFromJavaScript callout) rather than the absence of the bug. If anyone later switched the impl to std::recursive_mutex, the test would silently pass even with the buggy "lock-held-across-callout" pattern, since recursive_mutex does not throw on re-entrant lock.
  • It was tightly coupled to MSVC's std::mutex deadlock-detection throwing std::system_error on recursive lock; the probe is silent on libc++/libstdc++ where re-entrant lock is undefined behavior rather than an exception.
  • The wild-case crash involves a Chakra finalizer firing during a JS property lookup, which we cannot deterministically arrange. The test had to fabricate the recursive-lock shape via a JS getter, making it a fault-injection test rather than a true regression for the original failure path.

The encapsulation refactor above provides the structural guarantee the test was approximating. The fix itself is small and well-reviewed; the cost/value trade-off didn't justify keeping the test complexity.

Audit: any other affected sites?

Scanned plugin-side lock sites that bridge graphics-thread state to JS (grep -nE 'scoped_lock|unique_lock|lock_guard' Plugins/**/*.{h,cpp}). The pattern that produced #1646 — long-lived mutex held around Napi::* allocation or JS property lookup — is unique to the original ExternalTexture::CreateForJavaScript site. Other lock sites either (a) cover C++-only critical sections with no JS callouts, or (b) explicitly unlock before invoking JS (e.g., NativeXr::Impl::NotifySessionStateChanged releases its mutex before calling the session-state callback).

The encapsulation pattern from this PR — JS-thread entry points self-lock, never expose Mutex(), never call into JS while locked — is the right model for any future plugin that bridges graphics-thread state to JS.

bghgary added a commit to bghgary/BabylonNative that referenced this pull request Apr 29, 2026
…abylonJS#1646

In BabylonJS#1674's combination of rework-thread-model (BabylonJS#1652) and the
ExternalTexture sync API (BabylonJS#1646), three apps swapped the original
async AddToContextAsync flow for synchronous CreateForJavaScript but
kept the two-frame skeleton from the async version. The result is
that the loader.Dispatch which runs the JS startup() callback now
gets queued in frame 1 even though its observable wait
(startup.get_future().wait()) is in frame 2 — the dispatch can land
in either frame depending on JS-thread scheduling, breaking the
"each Start/Finish pair wraps a phase of work" pattern. On
rework-thread-model the JS thread will block on the closed gate
between frames 1 and 2 anyway, so it functionally works, but the
ordering is wrong.

Move the dispatch into the second frame so the texture creation,
startup() call, and the wait that observes them all run inside the
same frame:

  frame 1: load scripts            (no dispatch)
  frame 2: dispatch[startup] -> wait
  frame 3+: per-asset / render-loop

Three apps had this issue; UnitTests' ExternalTexture tests already
do it correctly. On bare rework-thread-model these apps still use
AddToContextAsync and don't have the bug.

Files:
- Apps/HeadlessScreenshotApp/Win32/App.cpp
- Apps/PrecompiledShaderTest/Source/App.cpp
- Apps/StyleTransferApp/Win32/App.cpp

Local build verification (Win32 RelWithDebInfo):
- HeadlessScreenshotApp: builds cleanly
- StyleTransferApp: builds cleanly
- PrecompiledShaderTest: not configured in this Build/Win32; change
  is structurally identical to HeadlessScreenshotApp which compiled.

[Created by Copilot on behalf of @bghgary]

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit that referenced this pull request May 21, 2026
## Context

Coverage for MSAA + ExternalTexture rendering. Both tests pass on master
on D3D11 today; this PR characterizes what works so future device-loss /
`updateWrappedNativeTexture` MSAA work has a regression baseline.

## Changes

Two new gtest cases in `Tests.ExternalTexture.Msaa.cpp` that drive a
JS-side `RenderTargetTexture` whose `colorAttachment` is a
`wrapNativeTexture` over an MSAA GPU texture, then read back the pixels
(resolving MSAA before staging):

- `RenderWithMsaaSamples1` -- discriminator, asserts zero blend pixels
along the rotated red-plane edges so the samples=4 case is diagnostic.
- `RenderWithMsaaSamples4` -- asserts red/white blend pixels along the
same edges.

Cross-platform per the convention #1646 establishes: file lives in
shared `SOURCES`, gated by `SKIP_EXTERNAL_TEXTURE_TESTS` /
`SKIP_RENDER_TESTS`. CMake defines `SKIP_RENDER_TESTS` for D3D12 (no
impl).

Renames `Utils.{h,cpp,mm}` -> `Helpers.{h,cpp,mm}` under `namespace
Helpers`. Collapses `CreateTestTexture` and the new MSAA-RT helper into
one `Helpers::CreateTexture(d, w, h, arraySize=1, renderTarget=false,
samples=1)`. Adds `Helpers::ReadPixels` taking `PlatformInfo` so the
Metal path can use bgfx's command queue (avoids cross-queue sync).
Implemented on D3D11 and Metal; D3D12 / OpenGL throw (test skipped on
those configurations).

Will conflict with #1646 on `Helpers.h` / `Helpers.D3D11.cpp` /
`CMakeLists.txt` -- whichever lands first picks a winner.

[Created by Copilot on behalf of @bghgary]

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit that referenced this pull request May 29, 2026
The MSAA tests deadlocked after merging master into rework-thread-model.

The render thread waits on startupDone while the JS thread runs the
AddToContextAsync .then() callback. That callback calls startup() ->
new NativeEngine() / new Scene() / new RenderTargetTexture(), which
eventually hits SubmitCommands. SubmitCommands now synchronously
acquires a FrameCompletionScope, which blocks while m_frameBlocked
is true. The gate only opens on the next StartRenderingCurrentFrame --
but the render thread is stuck on startupDone. Deadlock.

Fix: open frame 2 before waiting on startupDone, and reuse the same
frame for renderFrame(). Any JS work that touches the engine has to run
while a frame is in progress under the new model.

This workaround is temporary. Once #1646 lands, the test should migrate
from AddToContextAsync to the new synchronous CreateForJavaScript, at
which point startup() runs in the same JS task as the texture creation
and the cross-frame dance disappears entirely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit to bghgary/BabylonNative that referenced this pull request May 29, 2026
With BabylonJS#1646's synchronous ExternalTexture::CreateForJavaScript and BabylonJS#1652's
new threading model both in this branch, the original test pattern is
both unnecessary and broken:

- Unnecessary: AddToContextAsync's two-frame dance (queue in frame 1,
  wait for bgfx::frame() to resolve the promise, run startup in frame 2)
  exists only because the texture wrap was async. CreateForJavaScript is
  synchronous so the wrap, startup(), and renderFrame() all run in the
  same JS task inside a single frame.
- Broken: under the new model, SubmitCommands synchronously acquires a
  FrameCompletionScope which blocks when no frame is in progress.
  The old between-frames startup() pattern deadlocks.

Collapse to a single frame + single dispatch that wraps the texture,
runs startup(), and chains renderFrame()'s promise resolution. The host
app pattern host apps should follow with the new API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The MSAA test was added on master using AddToContextAsync. This PR
introduces synchronous CreateForJavaScript, so the test should use the
new API directly rather than the deprecated shim.

Collapse to a single frame + single dispatch that wraps the texture,
runs startup(), and chains renderFrame()'s promise resolution -- the
pattern host apps should follow with the new API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit to bghgary/BabylonNative that referenced this pull request May 29, 2026
BabylonJS#1689 added the rendering-enabled check to DeviceImpl::UpdateDevice on
master. Both BabylonJS#1646 and BabylonJS#1652 also have it. The check was dropped during
this branch's merge with the older partner-test base, causing
Device.UpdateDeviceThrowsWhenRenderingEnabled to fail in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary and others added 4 commits June 8, 2026 16:14
Updates bgfx.cmake from e5f3f31c to 940054e1, which brings in Branimir's
"setTexture view API" (bgfx #3728) plus the coordinated bgfx/bx update.

The bgfx bump requires the same source churn upstream master already absorbed:
- stb_image_resize.h was removed from bgfx's 3rdparty; switch to
  stb_image_resize2.h and the v2 entry point (stbir_resize_uint8_linear with
  STBIR_RGBA) in NativeEngine.
- bx renamed uint32_cnttz/uint32_min to countTrailingZeros<>/min<> and moved
  them to bx/math.h; update nanovg_babylon accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an optional layerIndex to ExternalTexture::CreateForJavaScript and
Update. When set, the wrapped JavaScript texture views only that one array
layer of a multi-layer (e.g. NV12 decoder frame-pool) texture, selected at
bind time via the bgfx setTexture view API.

Graphics::Texture gains a view sub-range (ViewFirstLayer/ViewNumLayers);
NativeEngine::SetTexture binds the single-slice view when ViewNumLayers != 0,
otherwise the whole texture. The constructor is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a D3D11 regression test for the "green remote participants" bug: an NV12
Texture2DArray is wrapped as per-plane (Y=R8, UV=R8G8) single-slice
ExternalTextures via layerIndex, and the selected slice's luma/chroma is passed
through to the output unchanged. The test asserts the output exactly matches the
target slice's YUV, catching the case where the per-plane UV view read the wrong
array slice (or the Y plane) for slice >= 1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit that referenced this pull request Jun 8, 2026
The test drives device loss/restore via the async AddToContextAsync + manual
frame-pump pattern, which deadlocks under the single-frame-encoder model in
this PR. It is re-enabled in #1646 after migration to the synchronous
CreateForJavaScript API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit that referenced this pull request Jun 9, 2026
Replaces the earlier skip. Under the single-frame-encoder model the async
AddToContextAsync .then() runs startup()/restoreTexture() on the JS thread,
whose SubmitCommands blocks until a frame is in progress. Open the next frame
before each blocking wait on the async result (and reuse it for the following
renderFrame), mirroring the workaround already applied to
Tests.ExternalTexture.Msaa.cpp. Obsoleted once #1646 migrates these tests to
the synchronous CreateForJavaScript API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit that referenced this pull request Jun 9, 2026
[Updated by Copilot on behalf of @bghgary]

## Context

Replaces the old per-thread encoder model
(`GetEncoderForThread`/`EndEncoders` + `UpdateToken` wrapping
`SafeTimespanGuarantor`) with a **single frame encoder** owned by
`DeviceImpl`, synchronized by a `FrameCompletionScope` RAII gate.
`SafeTimespanGuarantor`, `UpdateToken`, and the `Update` classes are
removed. `DeviceUpdate` remains as a no-op shim — removing it is an API
break and a separate PR (#1653).

## Model

`StartRenderingCurrentFrame` acquires the encoder (`bgfx::begin`) and
opens the gate; `FinishRenderingCurrentFrame` waits for all
`FrameCompletionScope`s to release, closes the gate, ends the encoder,
and submits the frame. All consumers (NativeEngine, Canvas, NativeXr)
read the shared encoder via `GetActiveEncoder()`. `SubmitCommands` and
`ReadTexture` each hold a stack-scoped `FrameCompletionScope`, so the
encoder can't be ended mid-work; when invoked outside a frame (e.g. an
XHR callback) the scope blocks until the next frame opens.

```
Main Thread                       JS Thread                 bgfx render thread
  StartRenderingCurrentFrame()
    m_frameEncoder = bgfx::begin(true)
    m_frameBlocked = false; CV notify
    tick frameStartDispatcher  -----> RAF callbacks
                                        submitCommands()
                                        AcquireFrameCompletionScope (immediate; frame open)
                                        GetEncoder() -> m_frameEncoder; draw...
                                        scope released; pendingScopes-- -> CV notify
  FinishRenderingCurrentFrame()
    wait scopes == 0; m_frameBlocked = true
    tick beforeRenderDispatcher
    bgfx::end(m_frameEncoder)
    Frame() -> bgfx::frame()  ------------------------------> GPU submit & render
    tick afterRenderDispatcher
```

## Synchronization rules

1. Single encoder, acquired before the gate opens (mutex gives memory
ordering) and ended only after all scopes release.
2. `SubmitCommands`/`ReadTexture` always hold a scope — the encoder
can't be ended mid-command, regardless of microtask draining or
reentrancy.
3. Apps must bracket any main-thread wait for JS work with
`Start`/`Finish`, or `SubmitCommands` blocks forever waiting for the
gate (HeadlessScreenshotApp, StyleTransferApp, UnitTests do this).
4. `Canvas::Flush` acquires a scope when called outside a frame, then
discards residual encoder state before nanovg rendering (the shared
encoder carries NativeEngine state).

## Notes

- `Tests.ExternalTexture.Msaa.cpp` opens the next frame before waiting
on `startup()` — a workaround for the legacy async `AddToContextAsync`
pattern, obsoleted once #1646's sync `CreateForJavaScript` lands.
- `Tests.ExternalTexture.RestoreAfterDeviceLoss` (added on master after
this branch) uses the same async pattern; the same frame-pump workaround
is applied here. Obsoleted once #1646's sync `CreateForJavaScript`
lands.
- bgfx: added `m_encoderBeginLock` in `bgfx::frame()` to prevent a
deadlock with `bgfx::destroy()`.
- Follow-up: removing the `DeviceUpdate` no-op shim is an API-breaking
change handled in #1653.

---------

Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com>
Co-authored-by: Gary Hsu <bghgary@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit to bghgary/BabylonNative that referenced this pull request Jun 9, 2026
Conflict resolutions (final state matches the BabylonJS#1674 integration reference plus
the layerIndex work from the Teams branch):
- Apps (HeadlessScreenshot/PrecompiledShaderTest/StyleTransfer): combine BabylonJS#1652's
  frame-pump (open a startup frame so CreateForJavaScript's blit has an active
  frame) with BabylonJS#1646's synchronous CreateForJavaScript migration.
- Tests.ExternalTexture.Msaa.cpp: BabylonJS#1646's synchronous CreateForJavaScript
  migration supersedes the frame-pump workaround BabylonJS#1652 added.
- NativeEngine::SetTexture: combine BabylonJS#1652's GetEncoder() with BabylonJS#1646's layerIndex
  single-slice view overload.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary added a commit to bghgary/BabylonNative that referenced this pull request Jun 9, 2026
The master merge brought in BabylonJS#1716's RestoreAfterDeviceLoss test using the
async AddToContextAsync + frame-pump pattern (added by BabylonJS#1652). With BabylonJS#1646's
synchronous CreateForJavaScript, that test runs startup() in the same JS task
as the texture wrap, so the frame-pump is unnecessary; migrate it to match the
other ExternalTexture tests.

Also correct a now-stale comment in AddToContextAsyncAndUpdate: the deprecated
AddToContextAsync shim resolves synchronously via CreateForJavaScript, so the
frame finish no longer drives its completion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary and others added 2 commits June 9, 2026 10:34
Conflict resolutions (final state matches the BabylonJS#1674 integration reference):
- Apps (HeadlessScreenshot/PrecompiledShaderTest/StyleTransfer): combine BabylonJS#1652's
  frame-pump (open a startup frame so CreateForJavaScript's blit has an active
  frame) with BabylonJS#1646's synchronous CreateForJavaScript migration.
- Tests.ExternalTexture.Msaa.cpp: BabylonJS#1646's synchronous CreateForJavaScript
  migration supersedes the frame-pump workaround BabylonJS#1652 added.
- NativeEngine::SetTexture: combine BabylonJS#1652's GetEncoder() with BabylonJS#1646's layerIndex
  single-slice view overload.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The master merge brought in BabylonJS#1716's RestoreAfterDeviceLoss test using the
async AddToContextAsync + frame-pump pattern (added by BabylonJS#1652). With BabylonJS#1646's
synchronous CreateForJavaScript, that test runs startup() in the same JS task
as the texture wrap, so the frame-pump is unnecessary; migrate it to match the
other ExternalTexture tests.

Also correct a now-stale comment in AddToContextAsyncAndUpdate: the deprecated
AddToContextAsync shim resolves synchronously via CreateForJavaScript, so the
frame finish no longer drives its completion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bghgary bghgary force-pushed the external-texture-render-test branch from 2cccc84 to c75319f Compare June 9, 2026 17:34
@bghgary bghgary changed the title Add synchronous ExternalTexture.CreateForJavaScript and deprecate AddToContextAsync Add synchronous CreateForJavaScript and deprecate AddToContextAsync in ExternalTexture Jun 9, 2026
@bghgary bghgary merged commit b6ecf80 into BabylonJS:master Jun 9, 2026
28 checks passed
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