Streaming Dynamic Vertex Data with WebGPU Storage Buffers
Architecting dynamic vertex streaming in WebGPU using storage buffers. Covers minimal design, memory boundaries, and strategies to avoid CPU‑GPU synchronization stalls.
24 Jul 2025, 21:18 UTC

The Problem: High-Frequency Vertex Updates
Updating large sets of vertex attributes—such as positions and normals for a real‑time 3D viewer—every frame can lead to significant CPU‑GPU synchronization stalls. Using standard vertex buffers for data that changes entirely every frame often forces the CPU to wait for the GPU to finish rendering before the buffer can be updated, killing the frame rate.
The Takeaway
To stream dynamic vertex data efficiently, use a storage buffer with MAP_WRITE | STORAGE usage. By mapping this buffer, writing interleaved data, and reading it directly in the vertex shader via a bind group, you bypass traditional vertex attribute pointers and gain more control over data layout, provided you stay within the device's maxStorageBufferBindingSize.
Requirements
- WebGPU Device: A device obtained via
navigator.gpu.requestAdapter()andadapter.requestDevice(). - Buffer Capabilities: Support for
GPUBufferUsage.MAP_WRITEandGPUBufferUsage.STORAGEon the same buffer. - Shader Support: A vertex shader capable of reading from a storage buffer (WGSL
var). - Limit Awareness: Access to
device.limits.maxStorageBufferBindingSizeto ensure the vertex set fits in a single binding.
Smallest Suitable Design
The minimal architecture for this streaming pattern consists of four components:
- The Storage Buffer: A single buffer allocated with a size of
vertexCount * stride. The stride must account for the interleaved data (e.g., 3 floats for position, 3 for normal = 24 bytes per vertex). - The Update Loop: Each frame, the application calls
buffer.mapAsync(GPUMapMode.WRITE). Once the promise resolves, the application writes the new vertex data into theArrayBufferViewand callsbuffer.unmap(). - The Bind Group: A bind group layout defining a storage buffer at
binding(0), which is then bound to the render pass. - The Vertex Shader: Instead of using
@location(n)attributes, the shader uses the@builtin(vertex_index)to index into the storage buffer:let pos = vertices[vertex_index].position;.
Trust and Data Boundaries
The storage buffer is owned by the web page, but its memory is managed by the GPU driver. The browser enforces a strict boundary: the page cannot access the buffer's memory unless it is explicitly mapped via mapAsync. On the GPU side, the driver validates memory access; if a shader attempts to read outside the buffer's allocated size, the driver prevents out-of-bounds memory corruption, typically resulting in a GPUValidationError or clamped indices depending on the implementation.
Operational Checks
To ensure the pipeline is functioning correctly, perform these checks during initialization and the render loop:
- Size Validation: Verify
vertexCount * stride <= device.limits.maxStorageBufferBindingSize. If this is false, the buffer creation will fail. - Mapping Verification: After
mapAsyncresolves, check thatbuffer.getMappedRange().byteLengthmatches the allocated size. - Usage Flags: Ensure
GPUBufferUsage.STORAGEis present, or the bind group creation will throw a validation error. - Pipeline Alignment: Confirm the WGSL shader's
@group(0) @binding(0)matches the JavaScript bind group layout.
Failure Modes and Design Changes
| Failure Mode | Condition | Design Change |
|---|---|---|
| Buffer Overflow | Data exceeds maxStorageBufferBindingSize |
Split data into multiple buffers or use a larger buffer with offset-based binding. |
| CPU‑GPU Stalls | mapAsync causes frame drops |
Implement double‑buffering (ping‑ponging between two buffers) to allow the GPU to read one while the CPU writes the other. |
| Hardware Incompatibility | Storage buffers not supported | Fall back to WebGL2 Vertex Buffer Objects (VBOs). |
Implementation Example
This example demonstrates the buffer setup and data write. Run this in a WebGPU‑enabled browser environment. Note: This code assumes a GPUDevice has already been initialized as device.
// Define vertex structure: Position (vec3), Normal (vec3)
const vertexCount = 1024;
const stride = 6 * 4; // 6 floats * 4 bytes
const size = vertexCount * stride;
if (size > device.limits.maxStorageBufferBindingSize) {
throw new Error('Vertex data exceeds max storage buffer size');
}
const vertexBuffer = device.createBuffer({
size: size,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.STORAGE,
});
async function updateVertices(data) {
// Request access to the buffer
await vertexBuffer.mapAsync(GPUMapMode.WRITE);
// Get the view and write data
const view = new Float32Array(vertexBuffer.getMappedRange());
view.set(data);
// Relinquish access so GPU can use it
vertexBuffer.unmap();
}
// Example data: [x,y,z, nx,ny,nz, ...]
const frameData = new Float32Array(vertexCount * 6).fill(0);
updateVertices(frameData);
Verification Steps
- Visual Check: Render a simple point cloud; if the points appear at the coordinates written in
frameData, the mapping is successful. - Validation Check: Open browser developer tools. If the buffer size or usage is incorrect, WebGPU will log a
GPUValidationError. - Performance Check: Use a frame‑time monitor. If
mapAsynccauses spikes, transition to the double‑buffering strategy mentioned in the failure modes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.