Skip to content

Fix WebGPU draws using the destroyed gpu texture after InternalTexture.updateSize - #19123

Merged
deltakosh merged 3 commits into
BabylonJS:masterfrom
serdarucel:fix/webgpu-updatesize-uniqueid-19087
Nov 9, 2026
Merged

deltakosh merged 3 commits into
BabylonJS:masterfrom
serdarucel:fix/webgpu-updatesize-uniqueid-19087

Conversation

@serdarucel

Copy link
Copy Markdown
Contributor

Fixes #19087

After InternalTexture.updateSize() on a WebGPU texture, every later frame logged Destroyed texture [...] used in a submit and the mesh using the texture drew nothing.

WebGPUEngine.updateTextureDimensions released the GPU texture and created the new one on the same InternalTexture, keeping its uniqueId. Two caches then returned the old bind group: WebGPUMaterialContext.setTexture compared the ids through the stored texture object (the same object, so it always saw "no change"), and the global bind group cache is keyed by uniqueId. The old GPU texture was also destroyed immediately, although the current frame's commands may still use it.

Changes:

  • updateTextureDimensions gives the texture a new hardware texture wrapper for the new GPU texture, and releases the previous wrapper at the end of the frame (new WebGPUTextureManager.releaseHardwareTexture, which uses the existing deferred release list). The previous code could not defer the release because the wrapper was shared with the new texture.
  • It refreshes the texture's uniqueId, as _swapAndDie already does.
  • WebGPUMaterialContext.setTexture stores the id that was bound and compares against it, so binding the same object again with a new id marks the context dirty. This also covers _swapAndDie, which refreshes the id on an object that can be bound again.

Before: Chrome 156 (WebGPU, NVIDIA), UMD build of master (1fa12be). A 64x64 blue RawTexture on a StandardMaterial emissive slot, drawn for 10 frames, then internal.updateSize(128, 128) and texture.update(red 128x128), then 60 frames, with device uncapturederror counted and the center pixel read back:
Destroyed texture [... InternalUniqueId0_64x64x1 ...] used in a submit. 60 errors in 60 frames, center pixel 0,0,0,0. The same when the resize runs inside scene.onAfterRenderObservable (61 errors).

After: same page, UMD build of this branch: 0 errors in 60 frames, center pixel 255,0,0,255, uniqueId 0 -> 1. The same with the resize inside onAfterRenderObservable (0 errors).

Tests: npx vitest run --project=unit packages/dev/core/test/unit/Engines -> 44 files, 404 passed (1 expected fail already in the suite). prettier --check and eslint on the changed files report no errors.

New tests (webgpuUpdateTextureDimensions.test.ts, 4 tests): on master 3 fail on their own assertions (expected ... not to be ... for the new wrapper, expected 1 not to be 1 for the id, expected false to be true for the dirty flag), with the change all 4 pass. The unit tests run on stubs without a GPU device, so the real-GPU check above is the one that shows the GPU error is gone.

Not covered: #19086 (shadow uniform uploads), which is a separate change.

…e.updateSize

updateTextureDimensions released the gpu texture right away and created the new one on the same InternalTexture, with the same uniqueId. The material contexts compared the id through the stored object, and the bind group cache is keyed by that id, so both kept returning bind groups over the destroyed texture.

- Give the texture a new wrapper for the new gpu texture and release the previous one at the end of the frame, since the current frame's commands may still use it.
- Refresh the uniqueId of the texture after the gpu texture is replaced.
- Store in WebGPUMaterialContext the id that was bound and compare against it, so rebinding the same object with a new id marks the context dirty.
@deltakosh deltakosh self-assigned this Nov 6, 2026

@deltakosh deltakosh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hello @serdarucel,

This fixes both halves of the issue:

  • Deferred release: updateTextureDimensions now detaches the previous wrapper and releases it at the end of the frame, using the existing deferred list. createGPUTextureForInternalTexture builds a fresh WebGPUHardwareTexture because _hardwareTexture is null, and recomputes its format and usages from the InternalTexture, so nothing carried by the old wrapper is lost apart from textureAdditionalUsages, which is forwarded. The old comment's concern (releasing the new GPU texture at the end of the frame) no longer applies, since the two wrappers are now distinct.
  • Cache invalidation: refreshing uniqueId misses the global bind group cache, and storing the bound id in the material context cache catches the same object being bound again with a new id. That also covers _swapAndDie. setTexture is the only writer of that cache entry, so the stored id can't go stale.

LGTM, thanks!

@deltakosh
deltakosh requested a review from Popov72 November 6, 2026 06:54
@Popov72

Popov72 commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

Snapshot stored with reference name:
refs/pull/19123/merge

Test environment:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/19123/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/19123/merge/index.html#WGZLGJ#4600

Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves):

https://playground.babylonjs.com/?snapshot=refs/pull/19123/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/19123/merge
https://gui.babylonjs.com/?snapshot=refs/pull/19123/merge
https://nme.babylonjs.com/?snapshot=refs/pull/19123/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/19123/merge#BCU1XR#0

If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools.

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

🟢 Memory Leak Test Results

4 passed, 0 leaked out of 4 scenarios

🟢 All memory leak tests passed — no leaks detected.

Passed Scenarios (4)
Scenario Package
Core Playground #2FDQT5#1508 @babylonjs/core
Core Playground #T90MQ4#14 @babylonjs/core
Core Playground #8EDB5N#2 @babylonjs/core
Core Playground #LL5BIQ#636 @babylonjs/core

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@deltakosh deltakosh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed the delta e6605b9...f2933a7.

  • updateTextureDimensions now reads _originalFormatIsRGB from the previous wrapper before releasing it, and sets it on the replacement that createGPUTextureForInternalTexture creates. A resized RGB raw cube therefore still goes through the RGB->RGBA expansion in updateRawCubeTexture.
  • I checked the rest of WebGPUHardwareTexture. The other fields are either recomputed by createGPUTextureForInternalTexture (format, originalFormat, textureUsages), carried over explicitly (textureAdditionalUsages), or lazily rebuilt GPU resources (mipmap generation and invertY copy state). Nothing else is lost when the wrapper is replaced.
  • The new regression test runs the real updateRawCubeTexture after the resize and checks 65,536-byte faces with the expected RGBA layout.

Thanks @Popov72 for catching this one, I missed it in my previous pass.

@deltakosh

Copy link
Copy Markdown
Contributor

Up @serdarucel

@deltakosh deltakosh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed the delta f2933a7...243f829.

  • updateTextureDimensions now keeps the WebGPUHardwareTexture returned by createGPUTextureForInternalTexture, which is declared with that return type and returns the wrapper it attached to texture._hardwareTexture, and sets _originalFormatIsRGB on that local. Because the cast on the null-narrowed texture._hardwareTexture is gone, the TS2352 that @Popov72 reported no longer applies. My previous approval missed that error, so thanks for catching it.
  • The runtime behaviour matches f2933a7: the same wrapper receives the same flag.
  • Both test stubs of createGPUTextureForInternalTexture now return the wrapper, as the real method does, so the tests exercise the new call shape.

LGTM.

@Popov72 Popov72 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified the requested fixes and the complete follow-up delta. No new actionable findings.

@deltakosh
deltakosh enabled auto-merge (squash) November 8, 2026 09:53
@RaananW

RaananW commented Nov 9, 2026

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
5 pipeline(s) were filtered out due to trigger conditions.

@bjsplat

bjsplat commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

🟢 Memory Leak Test Results

4 passed, 0 leaked out of 4 scenarios

🟢 All memory leak tests passed — no leaks detected.

Passed Scenarios (4)
Scenario Package
Core Playground #2FDQT5#1508 @babylonjs/core
Core Playground #T90MQ4#14 @babylonjs/core
Core Playground #8EDB5N#2 @babylonjs/core
Core Playground #LL5BIQ#636 @babylonjs/core

@bjsplat

bjsplat commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@bjsplat

bjsplat commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

@deltakosh
deltakosh merged commit 7bce70c into BabylonJS:master Nov 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.

[WebGPU] InternalTexture.updateSize leaves draws using the destroyed texture

5 participants