Hi all,
We have had a lot of success building Manywhere on Babylon.js, and watching how fast Lite is moving we wanted to look at migrating early rather than late. We ran into one gap, and since it looked general rather than specific to us, we prototyped a fix before proposing anything.
The gap: there is no supported way to render geometry produced on the GPU. Every geometry entry point takes CPU-side typed arrays (createMeshFromData, updateMeshGeometry, resizeMeshGeometry, updateMeshPositions, and the rest), so “procedural” in Lite today means CPU-procedural. If a compute pass generates vertices, the only way to draw them is to read them back and re-upload.
There is also no user-facing compute API: 0 of 702 exported functions dispatch anything, and GPUComputePipeline and beginComputePass never appear in the public .d.ts. Compute exists internally (mipmaps, BRDF decode, thin-instance culling) but is not reachable. We checked the feature comparison, the lab scenes, and the typings before concluding this, so please correct us if we missed a route.
Core Babylon.js went through this already, in this thread: thin instances predated WebGPU and could not consume storage buffers, and support was added afterwards. We are asking for the Lite equivalent.
Why it matters
Manywhere generates terrain entirely on the GPU. A compute pass fills one bounded storage slab, chunks take slots in it, and they are drawn straight from it with no readback. A readback would stall the frame, and the design depends on terrain staying analytically queryable without waiting on the GPU. The same constraint applies to marching cubes, GPU particles with custom geometry, mesh deformation, and erosion simulation, so this is not really about our game.
What we prototyped
We implemented it on a fork rather than guessing: about 620 lines across 9 files, two commits, all 1,571 upstream unit tests still passing.
- Writable, vertex-capable storage buffers.
createStorageBuffer(engine, bytes, { writable: true, vertex: true }). These keep no CPU shadow, since there is nothing meaningful to mirror when the GPU owns the contents, so after device loss they reallocate empty and the owner refills. - Per-attribute vertex layout overrides.
positionandnormalare currently hardcoded tofloat32x3at stride 12, one buffer per attribute. An override lets one interleaved allocation back several attributes and letspositionbefloat32x4, so.wcan carry packed per-vertex data. Lite already renders strided buffers ingltf-interleave.ts; that machinery is just wired only to the glTF loader. createMeshFromStorageBuffer, withbaseVertexso several meshes share one slab. We used the draw call rather than a non-zerosetVertexBufferbind offset, becausegltf-interleave.tsnotes those corrupt vertex fetch on some AMD and Dawn paths.- A compute seam, shaped after
createShaderMaterial:
const filler = createComputeShader(engine, {
computeSource,
uniforms: [...],
storageBuffers: [{ name: "slab", type: "array<vec4<f32>>", writable: true }],
});
setComputeStorageBuffer(filler, "slab", slab);
await prepareComputeShader(filler); // createComputePipelineAsync
beginComputeBatch(engine);
for (const chunk of chunks) { setComputeUniform(...); dispatchCompute(engine, filler, groups); }
endComputeBatch(engine); // one encoder, one submit
Storage binds as an opaque StorageBuffer, so no raw WebGPU handle crosses the public API and pillar 4d holds. Nothing in the core render path imports the module, so it tree-shakes away when unused.
Result
Four terrain chunks generated by compute into one shared slab, drawn from distinct baseVertex slots, with the colour gradient driven purely by position.w. No readback, no copy, no engine._device. 30 frames, zero validation errors, clean disposal.
One thing worth flagging: our first batching attempt shared a single uniform buffer, and all four chunks silently rendered as one, because queue.writeBuffer is ordered against submission rather than recording. Each dispatch now takes its own slot in a dynamic-offset ring. Any batched compute API has to solve this.
What we would like to agree before opening a PR
- Is user-facing compute in scope for Lite at all? It is a new public surface, so that is your call rather than ours.
- Per-dispatch parameters, API or convention? Our dynamic-offset ring keeps
dispatchComputeergonomic. The alternative is a single-dispatch API where callers put per-item params in a storage buffer indexed by workgroup ID, which is what our production code does today. That is less API surface and scales better. - Where should the vertex layout live? We put it on the material. It could equally hang off the mesh via the existing
MeshGPU._vbLayoutthat the glTF interleave path already uses, which would reuse machinery instead of adding a parallel mechanism.
Happy to reshape any of it. If there is appetite we will open a PR with the lab scene, golden reference, and bundle-size ceiling that CONTRIBUTING.md requires, and we can split it so the storage-buffer change lands independently of the compute seam.
Yilmaz
