One particle array, one projection, two backends. In points mode the right canvas is nearly empty, because WGSL has nowhere to write a point size. Switch to quads and it comes back. Everything on this page is rendered rather than described, and the point-size numbers are read back out of the framebuffer.
What both backends were asked for, in CSS pixels.
Lit bounding box of one particle, read back with readPixels.
Same particle, read back with copyTextureToBuffer.
Drag drawn past this and the unwritten slots still render.
gl_PointSize in WGSL, and that is the whole storyIn WebGL you write one builtin in the vertex shader and a point becomes a quad the driver rasterises for you. WGSL has no equivalent output. Not renamed, not gated behind a device limit, not a Chrome gap that will close: a point-list primitive is one fragment, and the spec says so deliberately, because point sprites were a source of driver divergence for twenty years.
Nothing warns you. The port compiles, the pipeline validates, the draw call succeeds, and your particle hero renders as a dusting of single pixels that reads on a retina display as "the shader is subtly broken" rather than "the API dropped a feature". The fix is two triangles per particle with the size applied in the vertex shader, which is what the quads switch above does, and which you were probably going to need anyway the first time you wanted a soft edge.
// WebGL2: the driver sizes the point for you gl_Position = project(pos); gl_PointSize = u_size; // 12.0 -> a 12px square of fragments // WGSL: there is no line to write here. A point is one fragment. // So expand a quad yourself, in NDC, from the pixel size you wanted: const C = array<vec2f,6>(vec2f(-1,-1),vec2f(1,-1),vec2f(-1,1), vec2f(-1,1),vec2f(1,-1),vec2f(1,1)); var clip = project(pos); clip.x += C[vi].x * u.size / u.res.x; // half-size, px -> ndc clip.y += C[vi].y * u.size / u.res.y;
The source I took this from lists ignored line width as a WebGPU limitation. It is, and there is no API for it at all. The part usually left out: gl.lineWidth() has been a no-op on nearly every desktop driver for a decade. WebGL reports its real range in ALIASED_LINE_WIDTH_RANGE, which is printed live in the receipt below from your machine.
Drag the lines slider up and watch both panels. If your driver is like most, the two hairlines stay identical at every setting and the only honest conclusion is that WebGPU removed an API that never worked. That reframes the porting note: this is not a regression to route around, it is a bit of folklore finally being deleted. Thick lines were always ribbons of triangles, in both APIs, whether or not you knew it.
Push the drawn slider past the written-slots figure. The extra instances appear, all of them stacked in the centre of the scene, and the frame rate does not move because they cost almost nothing. This is the failure people describe as random geometry appearing at the origin when they raise an instance count mid-interaction.
The usual explanation is that the buffer contains garbage. It does not, and that matters when you are debugging at 1am. Both WebGL and WebGPU zero-fill new GPU allocations, because handing your page whatever another process left in video memory would be a serious information leak. What you are seeing is vec3(0,0,0) for every unwritten particle, projected to exactly the same screen position. It looks like corruption. It is arithmetic.
Which gives you a real fix rather than a superstition. Zeroing the buffer again will not help, it is already zero. Either write every slot you intend to draw before you raise the count, or seed the unused ones with a scale of zero so the degenerate geometry is culled at no cost.
All three failures produce a successful draw call. There is no console warning to grep for, which is why they cost an afternoon each.
Both figures above are a framebuffer readback, not a claim. The technique costs a pipeline stall, so it sits behind a button.
Soft edges, per-particle rotation, texture atlases: everything past a hard dot needs a quad. The WebGPU limitation only forces the thing you would have done.