Repository navigation
WebGL2-only: drop WebGL1 path, adopt VAOs (per-flush attribute setup → bindVertexArray) #1509
Description
Summary
Drop the WebGL 1 rendering path and make the WebGL renderer WebGL 2-only (with the existing Canvas2D fallback unchanged), then adopt the WebGL2-only features that path unlocks — Vertex Array Objects (VAOs) first, as the concrete perf win.
Motivation
Two parts, kept distinct because they matter differently:
1. Dropping WebGL 1 by itself is ~0 runtime gain — it's an enabler + cleanup, not a speedup.
The ~9 WebGLVersion > 1 branches are per-flush / per-init, constant for a session, and perfectly branch-predicted — never in the per-vertex hot loop. Deleting them makes no existing WebGL 2 draw faster. The value of removing WebGL 1 is: smaller bundle, fewer code paths to test, removal of degraded fallbacks (Uint16 index coercion, NPOT-repeat guards, extension probing), and — most importantly — it lets us assume WebGL2-only features without guards.
2. The actual perf comes from what WebGL2-only unlocks — VAOs above all.
melonJS currently uses no VAOs (no createVertexArray / bindVertexArray). The batcher calls setVertexAttributes — i.e. N× enableVertexAttribArray + vertexAttribPointer — on every flush (src/video/webgl/batchers/batcher.js). A dense scene flushes ~10–20+ times/frame, so that attribute re-specification is paid repeatedly. With a VAO the layout is set up once and each flush becomes a single bindVertexArray — a real per-draw CPU saving. VAOs are core in WebGL 2 but only an unreliable extension on WebGL 1, so committing to WebGL2-only makes adopting them clean.
Scope
A. Make the WebGL renderer WebGL2-only
- Request a
webgl2context; if unavailable, fall back to Canvas2D (drop the WebGL1 middle tier). UpdateWebGLVersionhandling / the misconfiguration-throws path (Camera3d should fail loudly when used with the Canvas renderer #1479) accordingly. - Remove the
WebGLVersion > 1branches and their WebGL1 else-paths (renderer,quad_batcher,batcher,material_batcher): the offset/lengthbufferDataform, NPOT-repeat guards, Uint16 index coercion fallback,OES_element_index_uintassumptions, etc. - Keep Canvas2D fallback fully working (it already exists).
B. Adopt VAOs (the perf item)
- Create one VAO per batcher (or per shader/attribute-layout), set the vertex attribute layout once, and
bindVertexArrayper flush instead of re-callingsetVertexAttributes. - Handle context loss/restore (VAOs are GPU objects — recreate on
ONCONTEXT_RESTORED). - Verify the shared-vertex-buffer rebinding in
setBatcherstill holds with VAOs.
C. (Follow-on, optional in this ticket or separate) other WebGL2-only wins now unblocked
texStorage2Dimmutable textures + sized internal formats- MSAA renderbuffers /
blitFramebufferfor FBO antialiasing - core instancing (3D: mesh instancing (drawElementsInstanced for repeated geometry) #1508) without the
ANGLE_instanced_arraysdance - transform feedback (enables future GPU skinning)
Cost / risk
- Drops the WebGL-1-only device tier. WebGL 2 is ~96–98% globally; the Canvas2D fallback still covers the truly ancient, so the net is "WebGL 2 or Canvas2D." The 3D mesh path is already best-on-WebGL-2 (WebGL 1 3D is degraded today), so impact is mostly 2D on very old hardware that already falls back to Canvas2D.
- Breaking-ish: anything relying on a WebGL 1 context specifically. Call it out in the changelog; gate behind a major/minor as appropriate.
Acceptance criteria
- WebGL renderer creates only a
webgl2context; missing → Canvas2D (with the existing loud signal whenrenderer: video.WEBGLwas explicitly requested, per Camera3d should fail loudly when used with the Canvas renderer #1479). - All
WebGLVersion > 1branches removed; single code path remains. - Batchers use a VAO:
setVertexAttributesis called once per layout, not per flush (verifiable via a GL spy —vertexAttribPointercount per frame drops from O(flushes×attribs) to O(layouts)). - VAOs recreated correctly after context loss/restore.
- No visual regression across the full suite; FPS/draw-time on the night-city stress scene improves measurably (per-flush attribute setup eliminated).
References
src/video/webgl/webgl_renderer.js(context creation,WebGLVersion)src/video/webgl/batchers/batcher.js(flush,setVertexAttributesper flush — the VAO target)src/video/webgl/batchers/{quad,material,mesh,lit_mesh}_batcher.jssrc/video/webgl/glshader.js(setVertexAttributes)src/video/buffer/*,src/video/webgl/buffer/index.js- Related: 3D: retained-mode mesh rendering (upload geometry once, place with uniforms) #1507 (static VBO cache) and 3D: mesh instancing (drawElementsInstanced for repeated geometry) #1508 (instancing) are independent; this ticket's VAO work and WebGL2 baseline complement both.
Activity
obiot commentedon Jun 28, 2026
Folded in from #1391 (closed as duplicate)
#1391 ("Drop WebGL1 support") tracked the same WebGL2-only baseline. Merging its additional analysis here so nothing is lost.
Extra cleanup item (extends scope A)
preferWebGL1application setting (and any docs/types referencing it).IndexBufferto always useUint32Array(WebGL2 guaranteesOES_element_index_uint), and drop the WebGL1-specificbufferDataoverloads (WebGL2 accepts the offset+length form).Shader modernization — GLSL
#version 300 es(not yet in this ticket's scope)Going WebGL2-only also unlocks modern shaders:
attribute/varying→in/out;texture2D()→texture();gl_FragColor→ an explicitoutvariable.texture(uSamplers[idx], uv)) — eliminates the if/else sampler chain in the multi-texture batching fragment shader (a real per-fragment win + simpler codegen).precisionqualifiers instead of the single global one (src/video/webgl/utils/precision.js).Other WebGL2-only wins now unblocked (extends scope C)
Beyond the texStorage2D / MSAA / instancing / transform-feedback already listed:
isPowerOfTwo/nextPowerOfTwoguards (the repeat-guard removal is already in scope A; this also drops the POT checks insrc/video/texture/cache.js).Additional references (from #1391)
src/video/rendertarget/canvasrendertarget.js(besideswebgl_renderer.js).src/video/webgl/batchers/material_batcher.js,src/video/texture/cache.js.src/video/webgl/utils/precision.js.obiot commentedon Jul 29, 2026
Acceptance criteria status (PR #1553)
Four of five are met. Recording the fifth honestly rather than closing quietly over it.
webgl2context; missing → Canvas2D, with the Camera3d should fail loudly when used with the Canvas renderer #1479 loud signal preserved for an explicitrenderer: video.WEBGLWebGLVersion > 1branches removed; single code pathtests/webgl_vao_call_counts.spec.js: 19vertexAttribPointer+ 19enableVertexAttribArrayat startup (4+5+3+3+4 across the five batchers), 0 per frame thereafter, and 0 across 100 alternating batcher switchestests/webgl_vao_recreation.spec.js, driving a realWEBGL_lose_contextcycleMeasured on
night-cityFor comparison, a 2D scene (
platformer) saves ~42 calls/frame, so the stress scene does show the ~7× larger saving this ticket predicted — the mechanism is confirmed. But ~5.6 µs against a 16.6 ms budget is roughly 0.03%, which is not observable as FPS. Measured headless (SwiftShader), so absolute timings on real hardware will differ; the call counts are exact and hardware-independent.One correction to the ticket's premise
The motivation says the batcher re-specifies attributes "on every flush". That was true only for the mesh family: base
Batcher.flush()bound its ownglVertexBufferand re-applied the pointers, becausevertexAttribPointercaptures whicheverARRAY_BUFFERis bound when it is called — so switching from the shared buffer to its own invalidated the pointers set atuseShader()time.QuadBatcher.flush()overrode the base and did not re-specify, since it uploads into the already-bound sharedrenderer.vertexBuffer.That asymmetry was correct behaviour for a no-VAO design, not redundancy — and it is exactly what a VAO removes, since the VAO records buffer and pointer together, so restoring it needs no re-pointing after a buffer switch. It does mean the per-flush cost this ticket describes was a 3D-mesh cost, not a 2D-sprite one, which is why sprite scenes save far less than the framing implies.
Suggestion
Amend criterion 5 to "GL call count per frame drops from O(flushes × attribs) to O(layouts), verified by GL spy" — which is met and machine-checkable — and drop the FPS clause, or re-run
night-cityon real hardware before closing if a frame-time figure is wanted.WebGL 2 only renderer + Vertex Array Objects (#1509) — 20.0.0 (#1553)
test: scope the WebGL tripwire to CI so a GL-less machine still runs