Skip to content

Throw from Device::UpdateDevice when rendering is enabled#1689

Open
bghgary wants to merge 8 commits intoBabylonJS:masterfrom
bghgary:update-device-throw-when-initialized
Open

Throw from Device::UpdateDevice when rendering is enabled#1689
bghgary wants to merge 8 commits intoBabylonJS:masterfrom
bghgary:update-device-throw-when-initialized

Conversation

@bghgary
Copy link
Copy Markdown
Contributor

@bghgary bghgary commented May 6, 2026

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 first EnableRendering / StartRenderingCurrentFrame), the new device pointer gets stored in Bgfx.InitState.platformData and Dirty = true, but the swap does not actually take effect. bgfx::init only runs again on the next EnableRendering, which early-outs while Bgfx.Initialized == true.

Inside DeviceImpl::Frame(), UpdateBgfxState() runs each frame and, when Dirty, calls bgfx::setPlatformData + bgfx::frame(DISCARD) + bgfx::reset. So the new device pointer reaches g_platformData.context, but the bgfx renderer's m_device is captured at bgfx::init time and is never re-read by bgfx::reset on 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 (DisableRenderingUpdateDevice → frame); this PR makes the wrong flow fail loudly instead of silently.

Change

  • DeviceImpl::UpdateDevice throws std::runtime_error when called while m_state.Bgfx.Initialized == true.
  • Device::UpdateDevice doc-comment in Device.h states the contract: only valid when rendering is disabled; throws otherwise.
  • New test TEST(Device, UpdateDeviceThrowsWhenRenderingEnabled) in Tests.Device.D3D11.cpp. Verifies pre-init UpdateDevice is permitted, post-init throws, and the swap completes after DisableRendering.

Why only UpdateDevice and not the rest of the Update* family?

The Update* methods fall into two groups based on whether bgfx::reset (called from UpdateBgfxState) actually applies the change:

Method Applied via bgfx::reset? Notes
UpdateSize Yes bgfx::reset's primary purpose is resolution change
UpdateMSAA Yes BGFX_RESET_MSAA_* flags consumed by reset
UpdateAlphaPremultiplied Yes BGFX_RESET_TRANSPARENT_BACKBUFFER flag
UpdateBackBuffer (D3D11) Yes g_platformData.backBuffer re-read by reset
UpdateWindow Backend-dependent — being addressed separately in bgfx
UpdateDevice No on every backend renderer's m_device AddRef'd at init, never swapped by reset

UpdateDevice is uniquely "init-only on every backend" — making it throw is a clean cut. UpdateWindow has analogous inconsistency across bgfx backends and is being addressed in bgfx itself.

Local verification

[Created by Copilot on behalf of @bghgary]

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>
bghgary and others added 6 commits May 6, 2026 15:00
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>
@bghgary bghgary marked this pull request as ready for review May 8, 2026 15:37
Copilot AI review requested due to automatic review settings May 8, 2026 15:37
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

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_error from DeviceImpl::UpdateDevice when bgfx is already initialized.
  • Document the UpdateDevice contract in Device.h (only valid while rendering is disabled).
  • Add a D3D11 unit test asserting UpdateDevice throws 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.

Comment thread Core/Graphics/Source/DeviceImpl.cpp
Comment thread Core/Graphics/Include/Shared/Babylon/Graphics/Device.h Outdated
Comment thread Apps/UnitTests/Source/Tests.Device.D3D11.cpp
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>
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