Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/core/renderers/webgl/internal/RendererUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest';
import { createIndexBuffer } from './RendererUtils.js';
import type { WebGlContextWrapper } from '../../../lib/WebGlContextWrapper.js';

// Capture the Uint16Array handed to elementArrayBufferData so we can assert on
// the generated quad indices without a real GL context.
function mockGlw(): {
glw: WebGlContextWrapper;
getIndices: () => Uint16Array;
} {
let captured: Uint16Array | null = null;
const glw = {
STATIC_DRAW: 0,
createBuffer: () => ({}),
elementArrayBufferData: (_buffer: unknown, indices: Uint16Array) => {
captured = indices;
},
} as unknown as WebGlContextWrapper;
return {
glw,
getIndices: () => {
if (captured === null) {
throw new Error('elementArrayBufferData was not called');
}
return captured;
},
};
}

// The expected 6 element indices for quad `q`: two triangles over the quad's
// four vertices [4q, 4q+1, 4q+2, 4q+3] wound as [0,1,2, 2,1,3].
const expectedQuad = (q: number): number[] => {
const j = q * 4;
return [j, j + 1, j + 2, j + 2, j + 1, j + 3];
};

describe('createIndexBuffer', () => {
it('fills indices for EVERY quad, not just the first 1/6', () => {
// size / 80 = 800 quads, comfortably under the Uint16 cap.
const { glw, getIndices } = mockGlw();
createIndexBuffer(glw, 800 * 80);
const indices = getIndices();
const maxQuads = 800;

expect(indices.length).toBe(maxQuads * 6);

// First, middle and — critically — the LAST quad must be populated. The
// original bug stopped the loop at `i < maxQuads`, zeroing every quad past
// ~maxQuads/6 and collapsing it into a degenerate triangle.
for (const q of [0, 1, maxQuads >> 1, maxQuads - 2, maxQuads - 1]) {
const slice = Array.from(indices.subarray(q * 6, q * 6 + 6));
expect(slice).toEqual(expectedQuad(q));
}

// No trailing zeroed slots (every quad past index 0 references vertices > 0).
const lastQuad = Array.from(indices.subarray((maxQuads - 1) * 6));
expect(lastQuad.some((v) => v !== 0)).toBe(true);
});

it('caps at 16384 quads so Uint16 vertex ids never overflow', () => {
// Request far more than Uint16 can address (25000 quads worth of bytes).
const { glw, getIndices } = mockGlw();
createIndexBuffer(glw, 25000 * 80);
const indices = getIndices();

expect(indices.length).toBe(16384 * 6);
// The very last vertex id is 16384*4 - 1 = 65535, the Uint16 maximum.
expect(indices[indices.length - 1]).toBe(65535);
expect(Array.from(indices.subarray(16383 * 6, 16383 * 6 + 6))).toEqual(
expectedQuad(16383),
);
});
});
11 changes: 9 additions & 2 deletions src/core/renderers/webgl/internal/RendererUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ export function createIndexBuffer(
glw: WebGlContextWrapper,
size: number,
): WebGLBuffer | null {
const maxQuads = ~~(size / 80);
// 4 vertices per quad. Element indices are Uint16, so the largest vertex id
// we can address is 65535 — i.e. 16384 quads (16384 * 4 = 65536). Never claim
// more than that, regardless of the requested byte budget.
const maxQuads = Math.min(~~(size / 80), 16384);
const indices = new Uint16Array(maxQuads * 6);

for (let i = 0, j = 0; i < maxQuads; i += 6, j += 4) {
// i walks the index slot (6 per quad), j the vertex base (4 per quad). The
// bound must be maxQuads * 6 to fill every slot — stopping at maxQuads left
// ~5/6 of the buffer zeroed, collapsing every quad past ~maxQuads/6 into a
// degenerate triangle (the cause of tail geometry silently disappearing).
for (let i = 0, j = 0; i < maxQuads * 6; i += 6, j += 4) {
indices[i] = j;
indices[i + 1] = j + 1;
indices[i + 2] = j + 2;
Expand Down
Loading