Summary
Replace the raw GLenum values in the public batcher API with the WebGPU vocabulary — vertex formats ("float32x3", "unorm8x4") and topologies ("triangle-list", "line-list") — while continuing to accept the existing GLenum form indefinitely.
Groundwork for the future WebGPU backend, and a natural companion to #1492 (backend-neutral vertex layout descriptor).
Motivation
After #1509 each Batcher owns an immutable vertex state built from attributes + stride. That record is already shaped like a GPUVertexBufferLayout — except the type field holds a GLenum (gl.FLOAT, gl.UNSIGNED_BYTE), and draw modes hold gl.TRIANGLES and friends. Those are the last GL-specific values in an otherwise portable layout description, so a WebGPU backend would have to translate them on every path instead of consuming the record directly.
Scope — 6 public entry points, two vocabularies
Vertex formats (data type):
Batcher constructor, settings.attributes[].type — batchers/batcher.js
Batcher.addAttribute(name, size, type, normalized, offset) — batchers/batcher.js
Topologies (draw mode):
PrimitiveBatcher.drawVertices(mode, verts, vertexCount) — batchers/primitive_batcher.js
Batcher.flush(mode) and QuadBatcher.flush(mode)
- the
Batcher.mode property
Mapping
The WebGPU format grammar <baseType><bits>[x<count>] maps totally onto vertexAttribPointer: count → size, (baseType, bits) → the GLenum, unorm/snorm → normalized, and bits × count / 8 → the byte width (which replaces the switch (type) stride block in addAttribute).
| format |
size |
GL type |
normalized |
bytes |
float32 |
1 |
FLOAT |
false |
4 |
float32x2 |
2 |
FLOAT |
false |
8 |
float32x3 |
3 |
FLOAT |
false |
12 |
float32x4 |
4 |
FLOAT |
false |
16 |
unorm8x4 |
4 |
UNSIGNED_BYTE |
true |
4 |
Those five cover 100% of current engine usage (all five built-in batchers use only FLOAT sizes 1–4 and UNSIGNED_BYTE×4 normalized); the rest of the WebGPU set is a few extra table rows.
Backward compatibility
The GLenum form must keep working — custom batchers and custom shaders in the wild pass gl.FLOAT / gl.TRIANGLES today, and Batcher, QuadBatcher and PrimitiveBatcher are all public exports.
- Detect the new form with
typeof arg === "string"; otherwise take the legacy path.
- The legacy path should normalize into the new representation (reverse lookup:
(FLOAT, 3, false) → "float32x3"), so there is exactly one internal record regardless of which entry point was used. This is the point of the exercise — the WebGPU backend reads that record directly.
- Keep
size / type / normalized populated on the attribute record as derived fields, so existing code reading attributes[i].type is unaffected.
- This makes the change non-breaking, so it does not need a major version and the
GLenum form can stay supported indefinitely.
Key design decision
Store the format string on the attribute record and resolve it to a GLenum inside Batcher.createVertexState() — the GL-specific consumer — not eagerly at addAttribute() time. Converting eagerly buys only the signature change and leaves the record GL-flavoured; resolving late makes attributes + stride literally a GPUVertexBufferLayout + arrayStride, so #1492 becomes largely a rename.
Bonus: topology normalization falls out for free
WebGPU has no triangle-fan or line-loop. Once drawVertices speaks the WebGPU vocabulary those modes cannot be named, so normalization at the entry point (fan → triangle-list, loop → line-strip with the first vertex re-appended — the latter already exists in the chunked path) stops being a separate decision.
No engine code emits them today: every renderer call site passes gl.TRIANGLES or gl.LINES, and generateTriangleFan() already expands to a triangle list. They are reachable-but-unused support in the public method. Normalizing therefore also makes the per-topology chunk-overlap rules in #drawVerticesChunked (fan re-anchoring, loop closing) deletable.
Notes
Part of the #1184 (WebGPU renderer) groundwork. Follow-up to #1509. Related: #1492 (backend-neutral vertex layout), #1410 (renderer-agnostic TextureCache/Batcher).
Summary
Replace the raw
GLenumvalues in the public batcher API with the WebGPU vocabulary — vertex formats ("float32x3","unorm8x4") and topologies ("triangle-list","line-list") — while continuing to accept the existingGLenumform indefinitely.Groundwork for the future WebGPU backend, and a natural companion to #1492 (backend-neutral vertex layout descriptor).
Motivation
After #1509 each
Batcherowns an immutable vertex state built fromattributes+stride. That record is already shaped like aGPUVertexBufferLayout— except thetypefield holds aGLenum(gl.FLOAT,gl.UNSIGNED_BYTE), and draw modes holdgl.TRIANGLESand friends. Those are the last GL-specific values in an otherwise portable layout description, so a WebGPU backend would have to translate them on every path instead of consuming the record directly.Scope — 6 public entry points, two vocabularies
Vertex formats (data type):
Batcherconstructor,settings.attributes[].type—batchers/batcher.jsBatcher.addAttribute(name, size, type, normalized, offset)—batchers/batcher.jsTopologies (draw mode):
PrimitiveBatcher.drawVertices(mode, verts, vertexCount)—batchers/primitive_batcher.jsBatcher.flush(mode)andQuadBatcher.flush(mode)Batcher.modepropertyMapping
The WebGPU format grammar
<baseType><bits>[x<count>]maps totally ontovertexAttribPointer:count→size,(baseType, bits)→ theGLenum,unorm/snorm→normalized, andbits × count / 8→ the byte width (which replaces theswitch (type)stride block inaddAttribute).float32FLOATfloat32x2FLOATfloat32x3FLOATfloat32x4FLOATunorm8x4UNSIGNED_BYTEThose five cover 100% of current engine usage (all five built-in batchers use only
FLOATsizes 1–4 andUNSIGNED_BYTE×4 normalized); the rest of the WebGPU set is a few extra table rows.Backward compatibility
The
GLenumform must keep working — custom batchers and custom shaders in the wild passgl.FLOAT/gl.TRIANGLEStoday, andBatcher,QuadBatcherandPrimitiveBatcherare all public exports.typeof arg === "string"; otherwise take the legacy path.(FLOAT, 3, false)→"float32x3"), so there is exactly one internal record regardless of which entry point was used. This is the point of the exercise — the WebGPU backend reads that record directly.size/type/normalizedpopulated on the attribute record as derived fields, so existing code readingattributes[i].typeis unaffected.GLenumform can stay supported indefinitely.Key design decision
Store the format string on the attribute record and resolve it to a
GLenuminsideBatcher.createVertexState()— the GL-specific consumer — not eagerly ataddAttribute()time. Converting eagerly buys only the signature change and leaves the record GL-flavoured; resolving late makesattributes+strideliterally aGPUVertexBufferLayout+arrayStride, so #1492 becomes largely a rename.Bonus: topology normalization falls out for free
WebGPU has no
triangle-fanorline-loop. OncedrawVerticesspeaks the WebGPU vocabulary those modes cannot be named, so normalization at the entry point (fan → triangle-list, loop → line-strip with the first vertex re-appended — the latter already exists in the chunked path) stops being a separate decision.No engine code emits them today: every renderer call site passes
gl.TRIANGLESorgl.LINES, andgenerateTriangleFan()already expands to a triangle list. They are reachable-but-unused support in the public method. Normalizing therefore also makes the per-topology chunk-overlap rules in#drawVerticesChunked(fan re-anchoring, loop closing) deletable.Notes
arrayStrideto be a multiple of 4 and attribute offsets to be aligned. The built-in layouts (28/32/24/36/48) all pass, but GL tolerates what WebGPU rejects, so an assert would catch a non-conforming custom batcher early.stepMode: "instance"in WebGPU terms) — those call sites should be written in the neutral vocabulary rather than migrated afterwards. 3D: static-mesh VBO caching (retained-mode draw for unchanging geometry) #1507 imposes no constraint.Part of the #1184 (WebGPU renderer) groundwork. Follow-up to #1509. Related: #1492 (backend-neutral vertex layout), #1410 (renderer-agnostic TextureCache/Batcher).