fix(webgl): fill entire quad index buffer; cap at Uint16 limit#79
Merged
Conversation
createIndexBuffer's loop used `i < maxQuads` as its bound, but `i` is the write cursor into the index array and advances by 6 (six indices per quad). The array is `maxQuads * 6` long, so the loop exited ~6x too early, leaving roughly 5/6 of the buffer at its initialized value of 0. Every quad past ~maxQuads/6 (~4167 with the default 2e6 buffer budget) therefore drew with all-zero indices — two degenerate triangles over vertex 0 — and rendered nothing. This silently dropped any geometry beyond ~4167 quads in a frame. SDF glyphs index into this shared buffer, so text added last (e.g. a high-zIndex debug overlay on top of a large card grid) fell into the dead zone and vanished, while everything before it rendered fine. Fixes: - Loop bound is now `i < maxQuads * 6`, so every slot gets valid indices. - maxQuads is capped at 16384. Uint16 element indices can only address 65536 vertices (16384 quads * 4); the previous 25000 would have overflowed the vertex id past 65535 and wrapped. The capped buffer is also smaller on the GPU (192 KB vs ~293 KB) while raising usable capacity ~4x. Adds a unit test covering full population of the buffer (including the last quad) and the 16384 cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chiefcll
added a commit
that referenced
this pull request
Jun 9, 2026
…t) (#80) Adds `?autosweep=true` to the stress-tv example. Instead of driving the grid by hand with the remote, it sweeps every tier from low to high card count, measures median FPS per step (rAF timing, with a short warm-up to skip the rebuild spike), and bisects between the last good rung and the first bad one to report the highest count that holds the target frame rate. ?test=stress-tv&autosweep=true # target 60 fps (default) ?test=stress-tv&autosweep=true&targetfps=30 Results print to the console (console.table) and an on-screen panel. The sweep reports two distinct ceilings so they are never conflated: - performance wall: the measured FPS knee (CPU- or GPU-bound) - correctness wall: the Uint16 index-buffer cap (16384 quads / glyphs) The reported sweet spot is min(perf wall, correctness wall), and counts are clamped to the cap so the sweep never builds a scene whose own text would drop. VAO is fixed at renderer construction, so A/B it with two runs and compare: ?test=stress-tv&autosweep=true vs ?test=stress-tv&autosweep=true&novao=true Also refactors buildGrid to take an explicit count so the sweep can drive arbitrary values; interactive remote controls are unchanged. Note: the correctness-wall math assumes the index-buffer fix (#79). Best reviewed/merged alongside it; without it the real cap is ~4167 and high-count tiers would drop text mid-sweep. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
createIndexBufferonly filled valid indices for ~1/6 of the quad index buffer, silently capping renderable geometry at ~4167 quads per frame. Anything beyond that drew with all-zero indices (degenerate triangles) and rendered nothing.Root cause
iis the write cursor into the index array and advances by 6 per quad (six indices per quad), so the bound must bemaxQuads * 6. Stopping ati < maxQuadsexited ~6× too early, leaving ~5/6 of the buffer at its initialized value of0. A quad whose six indices are all0is two zero-area triangles over vertex 0 → invisible.Because SDF glyphs index into this same shared buffer, text added last in a frame (e.g. a high-
zIndexdebug overlay drawn over a large card grid) fell into the dead zone and disappeared, while everything before it rendered correctly.Fix
i < maxQuads * 6, so every slot receives valid indices.maxQuadsis capped at 16384. Uint16 element indices can only address 65536 vertices (16384 quads × 4); the previous value of 25000 would have overflowed the vertex id past 65535 and wrapped to the wrong vertices. The capped index buffer is also smaller on the GPU (192 KB vs ~293 KB) while raising usable capacity ~4× (4167 → 16384 quads).Impact
This affected any scene exceeding ~4167 quads in a frame — not just text. Large grids (e.g.
viewport-memory) were hitting it. Reproducible with thestress-tvexample at tier 4 / 200 cards +?debug=true: the overlay truncated mid-line; with this fix it renders fully.Tests
RendererUtils.test.ts: asserts the buffer is fully populated (including the last quad, which the old loop left zeroed) and thatmaxQuadscaps at 16384 with the final vertex id landing exactly at 65535.Notes for reviewer
bufferMemorypast16384 * 80no longer grows this buffer; going beyond would require 32-bit indices (OES_element_index_uint/ WebGL2).🤖 Generated with Claude Code