From ee3b14610cd98753a1d33fa844c9acae95885dfc Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Tue, 1 Sep 2026 14:34:00 +0200 Subject: [PATCH 1/6] Nodes: More consistent `dispose()` usage. (#34419) --- examples/jsm/lighting/vxgi/VXGINode.js | 2 ++ examples/jsm/tsl/display/AfterImageNode.js | 2 ++ examples/jsm/tsl/display/BilateralBlurNode.js | 2 ++ examples/jsm/tsl/display/BloomNode.js | 2 ++ examples/jsm/tsl/display/DenoiseNode.js | 4 ++-- examples/jsm/tsl/display/DepthOfFieldNode.js | 2 ++ examples/jsm/tsl/display/FSR1Node.js | 2 ++ examples/jsm/tsl/display/GTAONode.js | 2 ++ examples/jsm/tsl/display/GodraysNode.js | 2 ++ examples/jsm/tsl/display/LensflareNode.js | 2 ++ examples/jsm/tsl/display/OutlineNode.js | 2 ++ examples/jsm/tsl/display/RecurrentDenoiseNode.js | 2 ++ examples/jsm/tsl/display/SMAANode.js | 2 ++ examples/jsm/tsl/display/SSAONode.js | 2 ++ examples/jsm/tsl/display/SSGINode.js | 2 ++ examples/jsm/tsl/display/SSRNode.js | 2 ++ examples/jsm/tsl/display/SSSNode.js | 2 ++ examples/jsm/tsl/display/SharpenNode.js | 2 ++ examples/jsm/tsl/display/TAAUNode.js | 2 ++ examples/jsm/tsl/display/TRAANode.js | 2 ++ examples/jsm/tsl/display/TemporalReprojectNode.js | 2 ++ src/nodes/display/PassNode.js | 2 ++ src/nodes/gpgpu/ComputeNode.js | 9 --------- 23 files changed, 44 insertions(+), 11 deletions(-) diff --git a/examples/jsm/lighting/vxgi/VXGINode.js b/examples/jsm/lighting/vxgi/VXGINode.js index a28ea8aaf007cc..e47e85ed740680 100644 --- a/examples/jsm/lighting/vxgi/VXGINode.js +++ b/examples/jsm/lighting/vxgi/VXGINode.js @@ -633,6 +633,8 @@ class VXGINode extends TempNode { */ dispose() { + super.dispose(); + this._renderTarget.dispose(); this._material.dispose(); this.volume.dispose(); diff --git a/examples/jsm/tsl/display/AfterImageNode.js b/examples/jsm/tsl/display/AfterImageNode.js index 52751f3931eab8..304dac58c363c8 100644 --- a/examples/jsm/tsl/display/AfterImageNode.js +++ b/examples/jsm/tsl/display/AfterImageNode.js @@ -231,6 +231,8 @@ class AfterImageNode extends TempNode { */ dispose() { + super.dispose(); + this._compRT.dispose(); this._oldRT.dispose(); diff --git a/examples/jsm/tsl/display/BilateralBlurNode.js b/examples/jsm/tsl/display/BilateralBlurNode.js index 6aa06da6c229d1..e7931c0b2f201e 100644 --- a/examples/jsm/tsl/display/BilateralBlurNode.js +++ b/examples/jsm/tsl/display/BilateralBlurNode.js @@ -324,6 +324,8 @@ class BilateralBlurNode extends TempNode { */ dispose() { + super.dispose(); + this._horizontalRT.dispose(); this._verticalRT.dispose(); diff --git a/examples/jsm/tsl/display/BloomNode.js b/examples/jsm/tsl/display/BloomNode.js index e7c2bc01dec220..5fe20f10211d21 100644 --- a/examples/jsm/tsl/display/BloomNode.js +++ b/examples/jsm/tsl/display/BloomNode.js @@ -467,6 +467,8 @@ class BloomNode extends TempNode { */ dispose() { + super.dispose(); + for ( let i = 0; i < this._renderTargetsHorizontal.length; i ++ ) { this._renderTargetsHorizontal[ i ].dispose(); diff --git a/examples/jsm/tsl/display/DenoiseNode.js b/examples/jsm/tsl/display/DenoiseNode.js index a129eb9ee10131..3b8e7baf24fbdf 100644 --- a/examples/jsm/tsl/display/DenoiseNode.js +++ b/examples/jsm/tsl/display/DenoiseNode.js @@ -266,10 +266,10 @@ class DenoiseNode extends TempNode { */ dispose() { - this._noiseTexture.dispose(); - super.dispose(); + this._noiseTexture.dispose(); + } } diff --git a/examples/jsm/tsl/display/DepthOfFieldNode.js b/examples/jsm/tsl/display/DepthOfFieldNode.js index 396cbbca7b407e..f11c1ba9a61327 100644 --- a/examples/jsm/tsl/display/DepthOfFieldNode.js +++ b/examples/jsm/tsl/display/DepthOfFieldNode.js @@ -534,6 +534,8 @@ class DepthOfFieldNode extends TempNode { */ dispose() { + super.dispose(); + this._CoCRT.dispose(); this._CoCBlurredRT.dispose(); this._blur64RT.dispose(); diff --git a/examples/jsm/tsl/display/FSR1Node.js b/examples/jsm/tsl/display/FSR1Node.js index 8bbde596509a72..f3f189b701576c 100644 --- a/examples/jsm/tsl/display/FSR1Node.js +++ b/examples/jsm/tsl/display/FSR1Node.js @@ -454,6 +454,8 @@ class FSR1Node extends TempNode { */ dispose() { + super.dispose(); + this._easuRT.dispose(); this._rcasRT.dispose(); diff --git a/examples/jsm/tsl/display/GTAONode.js b/examples/jsm/tsl/display/GTAONode.js index cf0a58ac93bc7e..d28a8429532704 100644 --- a/examples/jsm/tsl/display/GTAONode.js +++ b/examples/jsm/tsl/display/GTAONode.js @@ -596,6 +596,8 @@ class GTAONode extends TempNode { */ dispose() { + super.dispose(); + this._aoRenderTarget.dispose(); this._noiseTexture.dispose(); diff --git a/examples/jsm/tsl/display/GodraysNode.js b/examples/jsm/tsl/display/GodraysNode.js index 5ae23b5a1e19a1..b76c4d7383497c 100644 --- a/examples/jsm/tsl/display/GodraysNode.js +++ b/examples/jsm/tsl/display/GodraysNode.js @@ -593,6 +593,8 @@ class GodraysNode extends TempNode { */ dispose() { + super.dispose(); + this._godraysRenderTarget.dispose(); this._material.dispose(); diff --git a/examples/jsm/tsl/display/LensflareNode.js b/examples/jsm/tsl/display/LensflareNode.js index cda8298b60fa7f..cf5ce51836c532 100644 --- a/examples/jsm/tsl/display/LensflareNode.js +++ b/examples/jsm/tsl/display/LensflareNode.js @@ -253,6 +253,8 @@ class LensflareNode extends TempNode { */ dispose() { + super.dispose(); + this._renderTarget.dispose(); this._material.dispose(); diff --git a/examples/jsm/tsl/display/OutlineNode.js b/examples/jsm/tsl/display/OutlineNode.js index 9fb1acc3b83051..517ad0a795ad74 100644 --- a/examples/jsm/tsl/display/OutlineNode.js +++ b/examples/jsm/tsl/display/OutlineNode.js @@ -748,6 +748,8 @@ class OutlineNode extends TempNode { */ dispose() { + super.dispose(); + this.selectedObjects.length = 0; this._renderTargetDepthBuffer.dispose(); diff --git a/examples/jsm/tsl/display/RecurrentDenoiseNode.js b/examples/jsm/tsl/display/RecurrentDenoiseNode.js index fd9c5eaa6a0757..751846d815f786 100644 --- a/examples/jsm/tsl/display/RecurrentDenoiseNode.js +++ b/examples/jsm/tsl/display/RecurrentDenoiseNode.js @@ -890,6 +890,8 @@ class RecurrentDenoiseNode extends TempNode { dispose() { + super.dispose(); + this._renderTarget.dispose(); this._material.dispose(); diff --git a/examples/jsm/tsl/display/SMAANode.js b/examples/jsm/tsl/display/SMAANode.js index dc464123acce94..e34129c8284cd3 100644 --- a/examples/jsm/tsl/display/SMAANode.js +++ b/examples/jsm/tsl/display/SMAANode.js @@ -675,6 +675,8 @@ class SMAANode extends TempNode { */ dispose() { + super.dispose(); + this._renderTargetEdges.dispose(); this._renderTargetWeights.dispose(); this._renderTargetBlend.dispose(); diff --git a/examples/jsm/tsl/display/SSAONode.js b/examples/jsm/tsl/display/SSAONode.js index 8f2107e5e48eba..2d930226a8287a 100644 --- a/examples/jsm/tsl/display/SSAONode.js +++ b/examples/jsm/tsl/display/SSAONode.js @@ -409,6 +409,8 @@ class SSAONode extends TempNode { */ dispose() { + super.dispose(); + this._aoRenderTarget.dispose(); this._blurRenderTarget.dispose(); diff --git a/examples/jsm/tsl/display/SSGINode.js b/examples/jsm/tsl/display/SSGINode.js index 930e7860dab125..c58a128f21f1e8 100644 --- a/examples/jsm/tsl/display/SSGINode.js +++ b/examples/jsm/tsl/display/SSGINode.js @@ -665,6 +665,8 @@ class SSGINode extends TempNode { */ dispose() { + super.dispose(); + this._ssgiRenderTarget.dispose(); this._material.dispose(); diff --git a/examples/jsm/tsl/display/SSRNode.js b/examples/jsm/tsl/display/SSRNode.js index 86d3a4901559e1..0f90b96566256a 100644 --- a/examples/jsm/tsl/display/SSRNode.js +++ b/examples/jsm/tsl/display/SSRNode.js @@ -1317,6 +1317,8 @@ class SSRNode extends TempNode { */ dispose() { + super.dispose(); + this._ssrRenderTarget.dispose(); this._blurRenderTarget.dispose(); diff --git a/examples/jsm/tsl/display/SSSNode.js b/examples/jsm/tsl/display/SSSNode.js index 290cec59b6b39c..52339a5ac898fb 100644 --- a/examples/jsm/tsl/display/SSSNode.js +++ b/examples/jsm/tsl/display/SSSNode.js @@ -468,6 +468,8 @@ class SSSNode extends TempNode { */ dispose() { + super.dispose(); + this._sssRenderTarget.dispose(); this._material.dispose(); diff --git a/examples/jsm/tsl/display/SharpenNode.js b/examples/jsm/tsl/display/SharpenNode.js index 98d3be6ba6e6a8..34e2b3930f9261 100644 --- a/examples/jsm/tsl/display/SharpenNode.js +++ b/examples/jsm/tsl/display/SharpenNode.js @@ -259,6 +259,8 @@ class SharpenNode extends TempNode { */ dispose() { + super.dispose(); + this._renderTarget.dispose(); if ( this._material !== null ) this._material.dispose(); diff --git a/examples/jsm/tsl/display/TAAUNode.js b/examples/jsm/tsl/display/TAAUNode.js index dab33d083278f2..2cfde943064390 100644 --- a/examples/jsm/tsl/display/TAAUNode.js +++ b/examples/jsm/tsl/display/TAAUNode.js @@ -791,6 +791,8 @@ class TAAUNode extends TempNode { */ dispose() { + super.dispose(); + this._historyRenderTarget.dispose(); this._resolveRenderTarget.dispose(); this._previousDepthRenderTarget.dispose(); diff --git a/examples/jsm/tsl/display/TRAANode.js b/examples/jsm/tsl/display/TRAANode.js index 4ff73005841f53..a94d4faeade74a 100644 --- a/examples/jsm/tsl/display/TRAANode.js +++ b/examples/jsm/tsl/display/TRAANode.js @@ -722,6 +722,8 @@ class TRAANode extends TempNode { */ dispose() { + super.dispose(); + this._historyRenderTarget.dispose(); this._resolveRenderTarget.dispose(); diff --git a/examples/jsm/tsl/display/TemporalReprojectNode.js b/examples/jsm/tsl/display/TemporalReprojectNode.js index 3284a1335b63fa..0e886024af2dd1 100644 --- a/examples/jsm/tsl/display/TemporalReprojectNode.js +++ b/examples/jsm/tsl/display/TemporalReprojectNode.js @@ -983,6 +983,8 @@ class TemporalReprojectNode extends TempNode { dispose() { + super.dispose(); + this._previousNormalTexture.dispose(); if ( this._previousDepthNode.value !== this._historyRenderTarget.depthTexture ) { diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js index 01fb6dc495869a..3ed1bae4127a07 100644 --- a/src/nodes/display/PassNode.js +++ b/src/nodes/display/PassNode.js @@ -1039,6 +1039,8 @@ class PassNode extends TempNode { */ dispose() { + super.dispose(); + this.renderTarget.dispose(); } diff --git a/src/nodes/gpgpu/ComputeNode.js b/src/nodes/gpgpu/ComputeNode.js index 949d4b71566411..2a36f8f8e3e92c 100644 --- a/src/nodes/gpgpu/ComputeNode.js +++ b/src/nodes/gpgpu/ComputeNode.js @@ -110,15 +110,6 @@ class ComputeNode extends Node { } - /** - * Executes the `dispose` event for this node. - */ - dispose() { - - this.dispatchEvent( { type: 'dispose' } ); - - } - /** * Sets the {@link ComputeNode#name} property. * From 97d803d329bef962b06ad4047ff441fd0ee25034 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Tue, 1 Sep 2026 15:06:45 +0200 Subject: [PATCH 2/6] WebGPURenderer: Fix viewport regression with array cameras. (#34424) --- src/renderers/webgl-fallback/WebGLBackend.js | 2 +- src/renderers/webgpu/WebGPUBackend.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index 3d2495df43ea33..3057c7e2bdeccb 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1266,7 +1266,7 @@ class WebGLBackend extends Backend { const renderTarget = this._currentContext.renderTarget; - const pixelRatio = renderTarget !== null ? 1 : this.renderer.getPixelRatio(); + const pixelRatio = ( renderTarget === null || renderTarget.isPostProcessingRenderTarget === true ) ? this.renderer.getPixelRatio() : 1; const isRenderCameraDepthArray = this._isRenderCameraDepthArray( this._currentContext ); const prevActiveCubeFace = this._currentContext.activeCubeFace; diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 34b5451a82ccea..e6345e42e2008f 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2257,7 +2257,8 @@ class WebGPUBackend extends Backend { } - const pixelRatio = context.renderTarget !== null ? 1 : this.renderer.getPixelRatio(); + const renderTarget = context.renderTarget; + const pixelRatio = ( renderTarget === null || renderTarget.isPostProcessingRenderTarget === true ) ? this.renderer.getPixelRatio() : 1; const indexPos = cameraIndex ? bindings.indexOf( cameraIndex ) : - 1; for ( let i = 0, len = cameras.length; i < len; i ++ ) { From e425807ac2d0645fa0ce497da971171fe66c91f8 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Tue, 1 Sep 2026 10:54:15 -0400 Subject: [PATCH 3/6] TSL Tests: Give each GPU unit test kernel its own random canary value (#34427) --- test/unit/addons/tsl/gpu-test-utils.js | 76 ++++++++++++++++++-------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/test/unit/addons/tsl/gpu-test-utils.js b/test/unit/addons/tsl/gpu-test-utils.js index 2309bbc1088a7f..04fecfa6ea019a 100644 --- a/test/unit/addons/tsl/gpu-test-utils.js +++ b/test/unit/addons/tsl/gpu-test-utils.js @@ -496,20 +496,44 @@ async function readBuffer( renderer, buffer ) { // The canary deliberately reuses one extra reserved row of a caller-supplied // buffer (rather than allocating a dedicated buffer, which tipped some call // sites over the WebGL2 fallback's tight simultaneously-bound-buffer budget) -// -- see `gpuTest`'s own comment for where that row lives and why. Currently -// only wired into `gpuTest`; `gpuFuzzTest` doesn't use this yet -- extending -// its `count`-many-instances/multi-site addressing to reserve a row safely -// turned out riskier (a native-WebGPU-only `getArrayBufferAsync()` failure, -// "Cannot read properties of undefined (reading 'size')", specifically when -// reusing an unused site/instance for the canary) and needs a more careful -// follow-up pass rather than shipping alongside this fix. -const CANARY_VALUE = 12345.6789; +// -- see `gpuTest`'s own comment for where that row lives and why. Wired +// into both `gpuTest` and `gpuFuzzTest`. +// +// The value itself is generated fresh per test x backend invocation (via +// `randomCanaryValue()`) rather than reused from one shared module-level +// constant -- confirmed necessary, not just defensive: a *shared* canary +// can't tell "this kernel ran" apart from "a different, still-bound kernel +// ran instead". Root cause, reproduced directly: when a kernel fails to +// link, the WebGL2 fallback logs the error but does not throw, and then +// calls `useProgram()` on the broken handle anyway -- which, per the WebGL +// spec, raises `INVALID_OPERATION` and leaves whatever program was +// *previously* bound still active, rather than unbinding it. If an earlier +// test in the same run already linked a valid compute kernel, the following +// transform-feedback draw silently reruns *that* kernel instead. With one +// constant canary shared by every kernel in the suite, that stale kernel's +// own canary write satisfies the check, and its self-consistent (but +// unrelated) `actual`/`expected` writes pass every assertion vacuously -- +// confirmed by deliberately reproducing a `tsl_bitcast_uint_to_int` return +// type bug this way: a shader that failed to link at 0 fail every time, +// until run as the very first webgl kernel of the process (no valid prior +// program to fall back to), which correctly failed via this exact canary +// check. A per-invocation random value closes that gap: a stale kernel's +// own (different) canary can never satisfy what the current test expects to +// read back. +function randomCanaryValue() { + + // An integer, not an arbitrary float: buffers are float32, and integers + // in this range round-trip through float32 exactly, so the readback + // comparison below needs no tolerance window to absorb rounding. + return 1000 + Math.floor( Math.random() * 9000 ); + +} -function writeCanary( buffer, row ) { +function writeCanary( buffer, row, canaryValue ) { If( instanceIndex.equal( row ), () => { - buffer.element( instanceIndex ).assign( vec4( CANARY_VALUE, 0, 0, 0 ) ); + buffer.element( instanceIndex ).assign( vec4( canaryValue, 0, 0, 0 ) ); } ); @@ -522,23 +546,18 @@ function writeCanary( buffer, row ) { // the same test fails outright (`getBufferSubData: no buffer`). Every caller // here already needs to read that same buffer's full data right afterwards // anyway, so read once and pass the array to both. -function assertKernelRan( assert, data, row, name, kind = 'gpuTest' ) { +function assertKernelRan( assert, data, row, canaryValue, name, kind = 'gpuTest' ) { const value = data[ row * 4 ]; - const ran = Math.abs( value - CANARY_VALUE ) < 1e-3; + const ran = value === canaryValue; if ( ! ran ) { assert.pushResult( { result: false, actual: value, - expected: CANARY_VALUE, - message: `${ kind } "${ name }": the compute kernel never ran (canary value missing -- ` + - `got ${ value }, expected ${ CANARY_VALUE }). This means the shader failed to build ` + - '(invalid WGSL/GLSL, most likely a NaN or otherwise malformed literal reaching ' + - 'generated shader source) -- check the console for the underlying WebGPU/WebGL compile ' + - 'error. Every assertion below would otherwise have silently compared a never-written 0 ' + - 'against a never-written 0 and passed regardless of what it claimed to check.' + expected: canaryValue, + message: `${ kind } "${ name }": compute kernel failed to build (canary mismatch -- got ${ value }, expected ${ canaryValue }).` } ); } @@ -596,6 +615,13 @@ export function gpuTest( name, buildFn, { maxAssertions = 64, backends = [ 'webg const canaryRow = totalRows - 1; const maxUsableAssertions = maxAssertions - 1; + // Generated once per test x backend invocation, outside the Fn() + // callback below (which can rebuild more than once -- see that + // callback's own comment) so every rebuild embeds the same literal + // -- see `randomCanaryValue()` for why this can't be a shared + // module-level constant. + const canaryValue = randomCanaryValue(); + const kernel = Fn( () => { // TSL callbacks passed to Fn()/If() are not guaranteed to run @@ -616,7 +642,7 @@ export function gpuTest( name, buildFn, { maxAssertions = 64, backends = [ 'webg // survive to match the actually-compiled kernel. nodes.length = 0; - writeCanary( actualBuffer, canaryRow ); + writeCanary( actualBuffer, canaryRow, canaryValue ); const makeNode = ( kind, tolerance, message ) => ( value1, value2 ) => { @@ -660,7 +686,7 @@ export function gpuTest( name, buildFn, { maxAssertions = 64, backends = [ 'webg const actualData = await readBuffer( renderer, actualBuffer ); const expectedData = await readBuffer( renderer, expectedBuffer ); - if ( ! assertKernelRan( assert, actualData, canaryRow, name ) ) return; + if ( ! assertKernelRan( assert, actualData, canaryRow, canaryValue, name ) ) return; nodes.forEach( ( node, id ) => { @@ -740,6 +766,10 @@ export function gpuFuzzTest( name, count, buildFn, { maxSitesPerInstance = 4, ma const canaryRow = count - 1; const maxUsableCount = count - 1; + // See `gpuTest`'s matching comment: generated once per test x + // backend invocation, not a shared module-level constant. + const canaryValue = randomCanaryValue(); + for ( let site = 0; site < maxSitesPerInstance; site ++ ) { const actualColumns = []; @@ -759,7 +789,7 @@ export function gpuFuzzTest( name, count, buildFn, { maxSitesPerInstance = 4, ma const kernel = Fn( () => { - writeCanary( actualBuffers[ 0 ][ 0 ], canaryRow ); + writeCanary( actualBuffers[ 0 ][ 0 ], canaryRow, canaryValue ); const makeNode = ( kind, tolerance, message ) => ( value1, value2 ) => { @@ -838,7 +868,7 @@ export function gpuFuzzTest( name, count, buildFn, { maxSitesPerInstance = 4, ma actualData[ 0 ] = await Promise.all( actualBuffers[ 0 ].map( ( buf ) => readBuffer( renderer, buf ) ) ); expectedData[ 0 ] = await Promise.all( expectedBuffers[ 0 ].map( ( buf ) => readBuffer( renderer, buf ) ) ); - if ( ! assertKernelRan( assert, actualData[ 0 ][ 0 ], canaryRow, name, 'gpuFuzzTest' ) ) return; + if ( ! assertKernelRan( assert, actualData[ 0 ][ 0 ], canaryRow, canaryValue, name, 'gpuFuzzTest' ) ) return; for ( const node of nodes ) { From 7fe7cde6db5fc39a40074ba3f49b4f9926553f27 Mon Sep 17 00:00:00 2001 From: William Liu Date: Wed, 2 Sep 2026 00:56:29 +1000 Subject: [PATCH 4/6] WebGPURenderer: Fix integer array texture bindings (#34422) Co-authored-by: Michael Herzog --- .../webgl-fallback/nodes/GLSLNodeBuilder.js | 17 ++--------------- src/renderers/webgpu/nodes/WGSLNodeBuilder.js | 6 ++++-- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js index 8cadcaaf910b41..3314d05d44be29 100644 --- a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +++ b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js @@ -873,21 +873,8 @@ ${ flowData.code } const textureNode = uniform.node; const texture = textureNode.value; - let typePrefix = ''; - - if ( texture.isDataTexture === true || texture.isData3DTexture === true ) { - - if ( texture.type === UnsignedIntType ) { - - typePrefix = 'u'; - - } else if ( texture.type === IntType ) { - - typePrefix = 'i'; - - } - - } + const componentType = this.getComponentTypeFromTexture( texture ); + const typePrefix = componentType === 'float' ? '' : componentType.charAt( 0 ); if ( uniform.type === 'texture3D' && texture.isArrayTexture === false ) { diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 6a7b1d8da99011..2741fb913266c7 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -2202,11 +2202,13 @@ ${ flowData.code } } else if ( texture.isArrayTexture === true || texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) { - textureType = 'texture_2d_array'; + const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 ); + textureType = `texture_2d_array<${ componentPrefix }32>`; } else if ( texture.is3DTexture === true || texture.isData3DTexture === true ) { - textureType = 'texture_3d'; + const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 ); + textureType = `texture_3d<${ componentPrefix }32>`; } else { From 1ee05ac416d312c38572ea0cc69bde32889255cd Mon Sep 17 00:00:00 2001 From: William Liu Date: Wed, 2 Sep 2026 01:27:02 +1000 Subject: [PATCH 5/6] StorageTextureNode: Preserve integer store values (#34423) --- src/nodes/accessors/StorageTextureNode.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nodes/accessors/StorageTextureNode.js b/src/nodes/accessors/StorageTextureNode.js index dddbbdec248539..6a6d3caf7bc105 100644 --- a/src/nodes/accessors/StorageTextureNode.js +++ b/src/nodes/accessors/StorageTextureNode.js @@ -260,7 +260,8 @@ class StorageTextureNode extends TextureNode { const textureProperty = super.generate( builder, 'property' ); const uvSnippet = uvNode.build( builder, this.value.is3DTexture === true ? 'uvec3' : 'uvec2' ); - const storeSnippet = storeNode.build( builder, 'vec4' ); + const storeType = builder.getTypeFromLength( 4, builder.getComponentTypeFromTexture( this.value ) ); + const storeSnippet = storeNode.build( builder, storeType ); const depthSnippet = depthNode ? depthNode.build( builder, 'int' ) : null; const snippet = builder.generateTextureStore( this.value, textureProperty, uvSnippet, depthSnippet, storeSnippet ); From 20bd26f2d1f2797602c084975b793591b3472558 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Tue, 1 Sep 2026 12:07:41 -0400 Subject: [PATCH 6/6] TSL Bug: Fix bitcast(uint, 'int')'s GLSL polyfill return type (#34429) --- .../webgl-fallback/nodes/GLSLNodeBuilder.js | 2 +- test/unit/addons/tsl/TSLBitOps.tests.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js index 3314d05d44be29..acd60a1b02b280 100644 --- a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +++ b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js @@ -11,7 +11,7 @@ import { error } from '../../../utils.js'; const glslPolyfills = { bitcast_int_uint: new CodeNode( /* glsl */'uint tsl_bitcast_int_to_uint ( int x ) { return floatBitsToUint( intBitsToFloat ( x ) ); }' ), - bitcast_uint_int: new CodeNode( /* glsl */'uint tsl_bitcast_uint_to_int ( uint x ) { return floatBitsToInt( uintBitsToFloat ( x ) ); }' ), + bitcast_uint_int: new CodeNode( /* glsl */'int tsl_bitcast_uint_to_int ( uint x ) { return floatBitsToInt( uintBitsToFloat ( x ) ); }' ), textureGather: new CodeNode( /* glsl */` vec4 tsl_textureGather( const int comp, sampler2D map, vec2 coord, ivec2 offset, bool flipY ) { if ( flipY ) offset.y = - offset.y; diff --git a/test/unit/addons/tsl/TSLBitOps.tests.js b/test/unit/addons/tsl/TSLBitOps.tests.js index 1ab862e53d9d5d..7ed65d921e350f 100644 --- a/test/unit/addons/tsl/TSLBitOps.tests.js +++ b/test/unit/addons/tsl/TSLBitOps.tests.js @@ -24,6 +24,19 @@ export default QUnit.module( 'TSL', () => { } ); + gpuTest( 'bitcast() reinterprets bits between int and uint', ( { assert } ) => { + + // -1's two's-complement bit pattern (0xFFFFFFFF) read as unsigned + // is the largest uint32 value. + assert.eq( bitcast( int( - 1 ), 'uint' ), uint( 4294967295 ), 'bitcast(-1, "uint") == 0xFFFFFFFF' ); + assert.eq( bitcast( uint( 4294967295 ), 'int' ), int( - 1 ), 'bitcast(0xFFFFFFFF, "int") == -1' ); + + // Round trip: bitcast is lossless reinterpretation, so converting + // out and back must recover the exact original value. + assert.eq( bitcast( bitcast( int( - 42 ), 'uint' ), 'int' ), int( - 42 ), 'bitcast round trip recovers the exact int' ); + + } ); + } ); QUnit.module( 'bit counting', () => {