Throw from Device::UpdateDevice when rendering is enabled#1689
Open
bghgary wants to merge 8 commits intoBabylonJS:masterfrom
Open
Throw from Device::UpdateDevice when rendering is enabled#1689bghgary wants to merge 8 commits intoBabylonJS:masterfrom
bghgary wants to merge 8 commits intoBabylonJS:masterfrom
Conversation
Before this change, UpdateDevice's effect on a Device whose bgfx is already
initialized was silently buffered: the new device pointer was stored in
Bgfx.InitState.platformData, but bgfx::init does not run again until the
caller does a DisableRendering / EnableRendering cycle, and EnableRendering
early-outs while already initialized. The result was a swap that appeared to
succeed but in which the device never actually changed -- a footgun that's
hard to diagnose.
This commit:
- Throws std::runtime_error from DeviceImpl::UpdateDevice when called with
Bgfx.Initialized == true. The throw message names the required call
sequence (DisableRendering first).
- Documents the contract on the public Device::UpdateDevice declaration in
Device.h, including the canonical D3D11/D3D12 device-removed recovery
sequence.
- Adds TEST(Device, UpdateDeviceThrowsWhenRenderingEnabled) that exercises
the contract: pre-init UpdateDevice is permitted, post-init throws,
and the swap completes successfully after DisableRendering.
UpdateWindow has the same surface-level contract but is left unchanged in
this PR -- the Android Playground's surfaceChanged JNI callback calls
UpdateWindow followed by UpdateSize while bgfx is initialized, and on the
OpenGL backend bgfx::reset (triggered by UpdateSize) re-creates the EGL
surface from the new ANativeWindow*. So the "init-only vs. reset-applicable"
classification is more nuanced for UpdateWindow than for UpdateDevice, and
narrowing the change to UpdateDevice avoids breaking the Android flow.
UpdateBackBuffer, UpdateSize, UpdateMSAA, and UpdateAlphaPremultiplied are
left unchanged because they are reset-applicable: bgfx::reset (called from
UpdateSize) reads g_platformData.backBuffer and init.resolution.reset, so
those updates take effect mid-frame as expected. The existing
TEST(Device, BackBuffer) exercises that flow and continues to pass.
Local verification: full UnitTests suite on Win32 D3D11 PASSED including
the new test; Win32 D3D12 builds and runs (UniformPadding sample passes).
[Created by Copilot on behalf of @bghgary]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary
added a commit
to bghgary/BabylonNative
that referenced
this pull request
May 6, 2026
Updates the Tests.Device.cpp file-header to reflect that the DisableRendering requirement is enforced (UpdateDevice throws std::runtime_error if called while rendering is enabled, per BabylonJS#1689) rather than just documented. Cross-references the contract test in Tests.Device.D3D11.cpp. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exception messages should describe what went wrong; how to recover belongs in the API doc-comment (which Device.h already has). Aligns with the simple- statement style of the other DeviceImpl exception messages. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bgfx is an implementation detail not part of the public Device API surface; the doc-comment should describe behavior in terms of the public API (EnableRendering / DisableRendering / etc). [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The post-Disable frame + GetPlatformInfo assertion was demonstrating the canonical UpdateDevice usage sequence, which is what TEST(Device, UpdateDevice) in BabylonJS#1687 does. The throw-test should focus on the throw contract only: - permitted pre-init - throws after EnableRendering - permitted again after DisableRendering [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drops: - second ID3D11Device (only the throw is being tested; same pointer suffices) - caller-owned BackBuffer setup (use g_deviceConfig HWND, let bgfx own swap chain) - between-frames EXPECT_THROW (redundant with the post-Start one) - over-explanatory comments Net ~30 lines removed. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The doc is for the API consumer; rationale for why the throw exists belongs in the commit message / PR body, not the public header. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Doc shouldn't presume the user app's call pattern. The throw contract is sufficient. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes Babylon::Graphics::Device::UpdateDevice fail loudly when called after bgfx has been initialized (i.e., while rendering is enabled), avoiding a silent no-op device “swap” that never actually takes effect on any backend.
Changes:
- Throw
std::runtime_errorfromDeviceImpl::UpdateDevicewhen bgfx is already initialized. - Document the
UpdateDevicecontract inDevice.h(only valid while rendering is disabled). - Add a D3D11 unit test asserting
UpdateDevicethrows when rendering is enabled.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Core/Graphics/Source/DeviceImpl.cpp | Adds a guard that throws if UpdateDevice is called after bgfx initialization. |
| Core/Graphics/Include/Shared/Babylon/Graphics/Device.h | Documents that UpdateDevice is only valid when rendering is disabled and will throw otherwise. |
| Apps/UnitTests/Source/Tests.Device.D3D11.cpp | Adds a new D3D11 unit test covering the “throws when rendering enabled” behavior. |
Reviewer flagged "only valid when rendering is disabled" as ambiguous (rendering can be "not in a frame" while still initialized). Make the contract explicit by naming the lifecycle states: before first EnableRendering / StartRenderingCurrentFrame, or after DisableRendering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bkaradzic-microsoft
approved these changes
May 8, 2026
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.
Context
Babylon::Graphics::Device::UpdateDevice(DeviceT)silently no-ops when called at the wrong time. If it's called while bgfx is initialized (after the firstEnableRendering/StartRenderingCurrentFrame), the new device pointer gets stored inBgfx.InitState.platformDataandDirty = true, but the swap does not actually take effect.bgfx::initonly runs again on the nextEnableRendering, which early-outs whileBgfx.Initialized == true.Inside
DeviceImpl::Frame(),UpdateBgfxState()runs each frame and, whenDirty, callsbgfx::setPlatformData + bgfx::frame(DISCARD) + bgfx::reset. So the new device pointer reachesg_platformData.context, but the bgfx renderer'sm_deviceis captured atbgfx::inittime and is never re-read bybgfx::reseton any backend (D3D11, D3D12, Metal, OpenGL). The caller's swap appears to succeed but the device never actually changes.#1687 adds a
TEST(Device, UpdateDevice)that exercises the canonical correct flow (DisableRendering→UpdateDevice→ frame); this PR makes the wrong flow fail loudly instead of silently.Change
DeviceImpl::UpdateDevicethrowsstd::runtime_errorwhen called whilem_state.Bgfx.Initialized == true.Device::UpdateDevicedoc-comment inDevice.hstates the contract: only valid when rendering is disabled; throws otherwise.TEST(Device, UpdateDeviceThrowsWhenRenderingEnabled)inTests.Device.D3D11.cpp. Verifies pre-initUpdateDeviceis permitted, post-init throws, and the swap completes afterDisableRendering.Why only
UpdateDeviceand not the rest of theUpdate*family?The
Update*methods fall into two groups based on whetherbgfx::reset(called fromUpdateBgfxState) actually applies the change:bgfx::reset?UpdateSizeUpdateMSAABGFX_RESET_MSAA_*flags consumed by resetUpdateAlphaPremultipliedBGFX_RESET_TRANSPARENT_BACKBUFFERflagUpdateBackBuffer(D3D11)g_platformData.backBufferre-read by resetUpdateWindowUpdateDevicem_deviceAddRef'd at init, never swapped by resetUpdateDeviceis uniquely "init-only on every backend" — making it throw is a clean cut.UpdateWindowhas analogous inconsistency across bgfx backends and is being addressed in bgfx itself.Local verification
Device.UpdateDeviceThrowsWhenRenderingEnabled.[Created by Copilot on behalf of @bghgary]