Description
With WebGPURenderer, swapping a mesh's geometry can leave the render object drawing with a pipeline built for a previous geometry's vertex layout. The draw then fails WebGPU validation and nothing is drawn.
It happens after two swaps: a geometry whose attributes are StorageBufferAttributes (which the WebGPU backend pads from itemSize 3 to 4, a 16-byte stride), then another such geometry, then an ordinary BufferAttribute geometry (12-byte stride). The third draw binds the new 12-byte buffer to the 16-byte-stride pipeline:
Vertex range (first: 0, count: 6) requires a larger buffer (96) than the bound buffer size (72) of the vertex buffer at slot 0 with stride 16.
A single swap (storage → plain, or plain → storage) does not show it. Replacing the mesh object instead of swapping its geometry avoids it.
Reproduction steps
- Draw a mesh whose geometry uses
StorageBufferAttribute for position.
- Set
mesh.geometry to a second geometry that also uses StorageBufferAttribute, and draw.
- Set
mesh.geometry to an ordinary geometry (BufferAttribute), and draw.
Code
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer();
await renderer.init();
const device = renderer.backend.device;
device.addEventListener( 'uncapturederror', ( e ) => console.error( e.error.message ) );
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 50, 1, 0.1, 10 );
camera.position.z = 3;
const target = new THREE.RenderTarget( 64, 64 );
const tri = [ - 1, - 1, 0, 1, - 1, 0, 0, 1, 0 ];
function makeGeometry( Attribute, triangles ) {
const g = new THREE.BufferGeometry();
const p = [];
for ( let i = 0; i < triangles; i ++ ) p.push( ...tri );
g.setAttribute( 'position', new Attribute( new Float32Array( p ), 3 ) );
return g;
}
const mesh = new THREE.Mesh( makeGeometry( THREE.StorageBufferAttribute, 1 ), new THREE.MeshBasicMaterial() );
scene.add( mesh );
async function draw() {
renderer.setRenderTarget( target );
renderer.render( scene, camera );
renderer.setRenderTarget( null );
await device.queue.onSubmittedWorkDone();
}
await draw(); // fine
mesh.geometry = makeGeometry( THREE.StorageBufferAttribute, 3 );
await draw(); // fine
mesh.geometry = makeGeometry( THREE.BufferAttribute, 2 );
await draw(); // validation error: 12-byte buffer bound to the 16-byte-stride pipeline
The same happens with InstancedMesh and with MeshStandardMaterial.
Live example
The code above; no hosted example.
Screenshots
No response
Version
r186.1 (three@0.186.1)
Device
Desktop
Browser
Chrome
OS
Windows
Notes
Chrome 152 (and Edge), Windows 11, NVIDIA RTX 4070. From reading RenderObject / RenderObjects: setGeometry() runs when needsGeometryUpdate is true, but the pipeline is only rebuilt inside the renderObject.version !== material.version || renderObject.needsUpdate branch, and a geometry swap alone doesn't seem to reach it, even though getGeometryCacheKey() includes itemSize. That part is a guess.
Description
With
WebGPURenderer, swapping a mesh's geometry can leave the render object drawing with a pipeline built for a previous geometry's vertex layout. The draw then fails WebGPU validation and nothing is drawn.It happens after two swaps: a geometry whose attributes are
StorageBufferAttributes (which the WebGPU backend pads fromitemSize3 to 4, a 16-byte stride), then another such geometry, then an ordinaryBufferAttributegeometry (12-byte stride). The third draw binds the new 12-byte buffer to the 16-byte-stride pipeline:A single swap (storage → plain, or plain → storage) does not show it. Replacing the mesh object instead of swapping its geometry avoids it.
Reproduction steps
StorageBufferAttributeforposition.mesh.geometryto a second geometry that also usesStorageBufferAttribute, and draw.mesh.geometryto an ordinary geometry (BufferAttribute), and draw.Code
The same happens with
InstancedMeshand withMeshStandardMaterial.Live example
The code above; no hosted example.
Screenshots
No response
Version
r186.1 (
three@0.186.1)Device
Desktop
Browser
Chrome
OS
Windows
Notes
Chrome 152 (and Edge), Windows 11, NVIDIA RTX 4070. From reading
RenderObject/RenderObjects:setGeometry()runs whenneedsGeometryUpdateis true, but the pipeline is only rebuilt inside therenderObject.version !== material.version || renderObject.needsUpdatebranch, and a geometry swap alone doesn't seem to reach it, even thoughgetGeometryCacheKey()includesitemSize. That part is a guess.