@flighthq/render-gl 0.3.0 → 0.3.1-next.1050.9114695

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/enableGlRenderStateGuards.d.ts.map +1 -1
  2. package/dist/enableGlRenderStateGuards.js +12 -3
  3. package/dist/enableGlRenderStateGuards.js.map +1 -1
  4. package/dist/explainGlTextureResolution.d.ts.map +1 -1
  5. package/dist/explainGlTextureResolution.js +4 -1
  6. package/dist/explainGlTextureResolution.js.map +1 -1
  7. package/dist/glCompressedTexture.d.ts.map +1 -1
  8. package/dist/glCompressedTexture.js +13 -2
  9. package/dist/glCompressedTexture.js.map +1 -1
  10. package/dist/glDraw.d.ts.map +1 -1
  11. package/dist/glDraw.js +21 -14
  12. package/dist/glDraw.js.map +1 -1
  13. package/dist/glFullscreenPass.d.ts.map +1 -1
  14. package/dist/glFullscreenPass.js +2 -1
  15. package/dist/glFullscreenPass.js.map +1 -1
  16. package/dist/glMaterialRegistry.d.ts.map +1 -1
  17. package/dist/glMaterialRegistry.js +13 -8
  18. package/dist/glMaterialRegistry.js.map +1 -1
  19. package/dist/glRenderState.d.ts +3 -1
  20. package/dist/glRenderState.d.ts.map +1 -1
  21. package/dist/glRenderState.js +55 -37
  22. package/dist/glRenderState.js.map +1 -1
  23. package/dist/glRenderStateBracket.d.ts.map +1 -1
  24. package/dist/glRenderStateBracket.js +54 -0
  25. package/dist/glRenderStateBracket.js.map +1 -1
  26. package/dist/glRenderTarget.js.map +1 -1
  27. package/dist/glShader.d.ts +2 -1
  28. package/dist/glShader.d.ts.map +1 -1
  29. package/dist/glShader.js +10 -0
  30. package/dist/glShader.js.map +1 -1
  31. package/dist/glShaderBinding.d.ts.map +1 -1
  32. package/dist/glShaderBinding.js +2 -1
  33. package/dist/glShaderBinding.js.map +1 -1
  34. package/dist/glSkinPaletteTexture.d.ts +1 -1
  35. package/dist/glSkinPaletteTexture.d.ts.map +1 -1
  36. package/dist/glSkinPaletteTexture.js +6 -2
  37. package/dist/glSkinPaletteTexture.js.map +1 -1
  38. package/dist/glTextureResolver.d.ts.map +1 -1
  39. package/dist/glTextureResolver.js +13 -11
  40. package/dist/glTextureResolver.js.map +1 -1
  41. package/package.json +11 -10
  42. package/src/enableGlRenderStateGuards.test.ts +10 -0
  43. package/src/glCompressedTexture.test.ts +29 -12
  44. package/src/glDraw.test.ts +25 -6
  45. package/src/glMaterialRegistry.test.ts +16 -0
  46. package/src/glRenderState.test.ts +338 -29
  47. package/src/glRenderStateBracket.test.ts +170 -0
  48. package/src/glShader.test.ts +55 -1
  49. package/src/glTestHelper.test.ts +51 -0
  50. package/src/glTextureResolver.test.ts +11 -6
@@ -23,6 +23,9 @@ type TestGlState = {
23
23
  blendSrcRgb: number;
24
24
  cullFace: boolean;
25
25
  cullFaceMode: number;
26
+ frontFace: number;
27
+ stencilTest: boolean;
28
+ stencilWriteMask: number;
26
29
  currentRenderTarget: GlRenderTarget | null;
27
30
  depthFunc: number;
28
31
  depthMask: boolean;
@@ -58,6 +61,9 @@ const OUTER_STATE: TestGlState = {
58
61
  blendSrcRgb: 1,
59
62
  cullFace: false,
60
63
  cullFaceMode: 0x0405,
64
+ frontFace: 0x0900, // GL_CW — a host that does not use Flight's default
65
+ stencilTest: true, // enabled on purpose: a fixture that starts disabled cannot tell 'restored' from 'left off'
66
+ stencilWriteMask: 0x0f,
61
67
  currentRenderTarget: { name: 'outer-target' } as unknown as GlRenderTarget,
62
68
  depthFunc: 0x0203,
63
69
  depthMask: false,
@@ -86,6 +92,9 @@ const MIDDLE_STATE: TestGlState = {
86
92
  blendSrcRgb: 1,
87
93
  cullFace: true,
88
94
  cullFaceMode: 0x0404,
95
+ frontFace: 0x0901, // GL_CCW
96
+ stencilTest: false,
97
+ stencilWriteMask: 0xff,
89
98
  currentRenderTarget: { name: 'middle-target' } as unknown as GlRenderTarget,
90
99
  depthFunc: 0x0201,
91
100
  depthMask: true,
@@ -136,6 +145,57 @@ describe('popGlRenderState', () => {
136
145
  expectTestGlState(fixture, OUTER_STATE);
137
146
  });
138
147
 
148
+ it('restores a two-sided stencil configuration without collapsing the back face onto the front', () => {
149
+ const fixture = createStatefulGl();
150
+ const gl = fixture.gl;
151
+ applyTestGlState(fixture, OUTER_STATE);
152
+ // A host driving two-sided stencil — the case stencilOpSeparate exists for. Every back-face value
153
+ // differs from its front counterpart, so a restore that writes both faces from the front's saved
154
+ // values cannot pass by coincidence: it would report the front correct and the back overwritten.
155
+ gl.stencilOpSeparate(gl.FRONT, 0x1e00, 0x1e00, 0x1e01); // KEEP, KEEP, REPLACE
156
+ gl.stencilOpSeparate(gl.BACK, 0x1e02, 0x1e03, 0x150a); // INCR, DECR, INVERT
157
+ gl.stencilFuncSeparate(gl.FRONT, 0x0201, 1, 0xf0); // LESS
158
+ gl.stencilFuncSeparate(gl.BACK, 0x0204, 2, 0x0f); // GREATER
159
+ gl.stencilMaskSeparate(gl.FRONT, 0xaa);
160
+ gl.stencilMaskSeparate(gl.BACK, 0x55);
161
+ const host = new Map(fixture.parameters);
162
+ pushGlRenderState(fixture.state);
163
+
164
+ // What Flight's own 2D clip pass writes: separate wrap ops per face, then a symmetric gate.
165
+ gl.stencilOpSeparate(gl.FRONT, 0x1e00, 0x1e00, 0x8507); // INCR_WRAP
166
+ gl.stencilOpSeparate(gl.BACK, 0x1e00, 0x1e00, 0x8508); // DECR_WRAP
167
+ gl.stencilFunc(0x0207, 0, 0xff); // ALWAYS, both faces
168
+ gl.stencilMask(0xff);
169
+ popGlRenderState(fixture.state);
170
+
171
+ for (const parameter of TWO_SIDED_STENCIL_PARAMETERS) {
172
+ expect([parameter, fixture.parameters.get(gl[parameter])]).toEqual([parameter, host.get(gl[parameter])]);
173
+ }
174
+ });
175
+
176
+ it('restores colour-write, clear-colour, and upload-premultiply state Flight leaves behind', () => {
177
+ const fixture = createStatefulGl();
178
+ const gl = fixture.gl;
179
+ applyTestGlState(fixture, OUTER_STATE);
180
+ // A host whose own settings all differ from both the GL defaults and from what Flight will set,
181
+ // so neither 'never restored' nor 'reset to the default' can pass as restoration.
182
+ gl.colorMask(true, false, true, false);
183
+ gl.clearColor(0.25, 0.5, 0.75, 1);
184
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
185
+ pushGlRenderState(fixture.state);
186
+
187
+ // What Flight's own passes do: mask colour off while rasterizing clip coverage, clear a target,
188
+ // and premultiply a texture upload. None of these puts the previous value back.
189
+ gl.colorMask(false, false, false, false);
190
+ gl.clearColor(0, 0, 0, 0);
191
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
192
+ popGlRenderState(fixture.state);
193
+
194
+ expect(fixture.parameters.get(gl.COLOR_WRITEMASK)).toEqual([true, false, true, false]);
195
+ expect(fixture.parameters.get(gl.COLOR_CLEAR_VALUE)).toEqual([0.25, 0.5, 0.75, 1]);
196
+ expect(fixture.parameters.get(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL)).toBe(false);
197
+ });
198
+
139
199
  it('restores a non-active texture unit binding', () => {
140
200
  const fixture = createStatefulGl();
141
201
  applyTestGlState(fixture, OUTER_STATE);
@@ -207,6 +267,9 @@ function applyTestGlState(fixture: StatefulGl, values: Readonly<TestGlState>): v
207
267
  gl.depthFunc(values.depthFunc);
208
268
  setCapability(gl, gl.CULL_FACE, values.cullFace);
209
269
  gl.cullFace(values.cullFaceMode);
270
+ gl.frontFace(values.frontFace);
271
+ setCapability(gl, gl.STENCIL_TEST, values.stencilTest);
272
+ gl.stencilMask(values.stencilWriteMask);
210
273
  setCapability(gl, gl.BLEND, values.blend);
211
274
  gl.blendFuncSeparate(values.blendSrcRgb, values.blendDstRgb, values.blendSrcAlpha, values.blendDstAlpha);
212
275
  gl.blendEquationSeparate(values.blendEquationRgb, values.blendEquationAlpha);
@@ -244,6 +307,27 @@ function createStatefulGl(): StatefulGl & ReturnType<typeof createGlState> {
244
307
  DEPTH_FUNC: 0x0b74,
245
308
  DEPTH_WRITEMASK: 0x0b72,
246
309
  FRAMEBUFFER_BINDING: 0x8ca6,
310
+ FRONT_FACE: 0x0b46,
311
+ BACK: 0x0405,
312
+ COLOR_CLEAR_VALUE: 0x0c22,
313
+ COLOR_WRITEMASK: 0x0c23,
314
+ UNPACK_PREMULTIPLY_ALPHA_WEBGL: 0x9241,
315
+ FRONT: 0x0404,
316
+ STENCIL_BACK_FAIL: 0x8801,
317
+ STENCIL_BACK_FUNC: 0x8800,
318
+ STENCIL_BACK_PASS_DEPTH_FAIL: 0x8802,
319
+ STENCIL_BACK_PASS_DEPTH_PASS: 0x8803,
320
+ STENCIL_BACK_REF: 0x8ca3,
321
+ STENCIL_BACK_VALUE_MASK: 0x8ca4,
322
+ STENCIL_BACK_WRITEMASK: 0x8ca5,
323
+ STENCIL_FAIL: 0x0b94,
324
+ STENCIL_FUNC: 0x0b92,
325
+ STENCIL_PASS_DEPTH_FAIL: 0x0b95,
326
+ STENCIL_PASS_DEPTH_PASS: 0x0b96,
327
+ STENCIL_REF: 0x0b97,
328
+ STENCIL_TEST: 0x0b90,
329
+ STENCIL_VALUE_MASK: 0x0b93,
330
+ STENCIL_WRITEMASK: 0x0b98,
247
331
  SCISSOR_BOX: 0x0c10,
248
332
  TEXTURE_BINDING_2D: 0x8069,
249
333
  VERTEX_ARRAY_BINDING: 0x85b5,
@@ -271,6 +355,65 @@ function createStatefulGl(): StatefulGl & ReturnType<typeof createGlState> {
271
355
  (gl.cullFace as ReturnType<typeof vi.fn>).mockImplementation((value: number) =>
272
356
  parameters.set(gl.CULL_FACE_MODE, value),
273
357
  );
358
+ (gl.frontFace as ReturnType<typeof vi.fn>).mockImplementation((value: number) =>
359
+ parameters.set(gl.FRONT_FACE, value),
360
+ );
361
+ (gl.colorMask as ReturnType<typeof vi.fn>).mockImplementation((r: boolean, g: boolean, b: boolean, a: boolean) =>
362
+ parameters.set(gl.COLOR_WRITEMASK, [r, g, b, a]),
363
+ );
364
+ (gl.clearColor as ReturnType<typeof vi.fn>).mockImplementation((r: number, g: number, b: number, a: number) =>
365
+ parameters.set(gl.COLOR_CLEAR_VALUE, [r, g, b, a]),
366
+ );
367
+ (gl.pixelStorei as ReturnType<typeof vi.fn>).mockImplementation((parameter: number, value: unknown) =>
368
+ parameters.set(parameter, value),
369
+ );
370
+ // Stencil is two-faced state: the unsuffixed setters write FRONT_AND_BACK while the Separate ones
371
+ // write only the named face. That rule was measured against a real WebGL2 context rather than assumed
372
+ // here — a fake cannot be the oracle for the API it fakes. `null` is the unsuffixed, both-faces form.
373
+ const writeStencilMask = (face: number | null, value: number): void => {
374
+ if (face !== gl.BACK) parameters.set(gl.STENCIL_WRITEMASK, value);
375
+ if (face !== gl.FRONT) parameters.set(gl.STENCIL_BACK_WRITEMASK, value);
376
+ };
377
+ const writeStencilFunc = (face: number | null, func: number, ref: number, mask: number): void => {
378
+ if (face !== gl.BACK) {
379
+ parameters.set(gl.STENCIL_FUNC, func);
380
+ parameters.set(gl.STENCIL_REF, ref);
381
+ parameters.set(gl.STENCIL_VALUE_MASK, mask);
382
+ }
383
+ if (face !== gl.FRONT) {
384
+ parameters.set(gl.STENCIL_BACK_FUNC, func);
385
+ parameters.set(gl.STENCIL_BACK_REF, ref);
386
+ parameters.set(gl.STENCIL_BACK_VALUE_MASK, mask);
387
+ }
388
+ };
389
+ const writeStencilOp = (face: number | null, fail: number, depthFail: number, depthPass: number): void => {
390
+ if (face !== gl.BACK) {
391
+ parameters.set(gl.STENCIL_FAIL, fail);
392
+ parameters.set(gl.STENCIL_PASS_DEPTH_FAIL, depthFail);
393
+ parameters.set(gl.STENCIL_PASS_DEPTH_PASS, depthPass);
394
+ }
395
+ if (face !== gl.FRONT) {
396
+ parameters.set(gl.STENCIL_BACK_FAIL, fail);
397
+ parameters.set(gl.STENCIL_BACK_PASS_DEPTH_FAIL, depthFail);
398
+ parameters.set(gl.STENCIL_BACK_PASS_DEPTH_PASS, depthPass);
399
+ }
400
+ };
401
+ (gl.stencilMask as ReturnType<typeof vi.fn>).mockImplementation((value: number) => writeStencilMask(null, value));
402
+ (gl.stencilMaskSeparate as ReturnType<typeof vi.fn>).mockImplementation((face: number, value: number) =>
403
+ writeStencilMask(face, value),
404
+ );
405
+ (gl.stencilFunc as ReturnType<typeof vi.fn>).mockImplementation((func: number, ref: number, mask: number) =>
406
+ writeStencilFunc(null, func, ref, mask),
407
+ );
408
+ (gl.stencilFuncSeparate as ReturnType<typeof vi.fn>).mockImplementation(
409
+ (face: number, func: number, ref: number, mask: number) => writeStencilFunc(face, func, ref, mask),
410
+ );
411
+ (gl.stencilOp as ReturnType<typeof vi.fn>).mockImplementation((fail: number, dFail: number, dPass: number) =>
412
+ writeStencilOp(null, fail, dFail, dPass),
413
+ );
414
+ (gl.stencilOpSeparate as ReturnType<typeof vi.fn>).mockImplementation(
415
+ (face: number, fail: number, dFail: number, dPass: number) => writeStencilOp(face, fail, dFail, dPass),
416
+ );
274
417
  (gl.blendFuncSeparate as ReturnType<typeof vi.fn>).mockImplementation(
275
418
  (srcRgb: number, dstRgb: number, srcAlpha: number, dstAlpha: number) => {
276
419
  parameters.set(gl.BLEND_SRC_RGB, srcRgb);
@@ -315,6 +458,14 @@ function expectTestGlState(fixture: StatefulGl, expected: Readonly<TestGlState>)
315
458
  expect(parameters.get(gl.DEPTH_FUNC)).toBe(expected.depthFunc);
316
459
  expect(gl.isEnabled(gl.CULL_FACE)).toBe(expected.cullFace);
317
460
  expect(parameters.get(gl.CULL_FACE_MODE)).toBe(expected.cullFaceMode);
461
+ // The 3D mesh path picks a front face per draw from the model determinant, so this is state Flight
462
+ // mutates and must hand back untouched — a host set to CW must not get CCW returned.
463
+ expect(parameters.get(gl.FRONT_FACE)).toBe(expected.frontFace);
464
+ // Flight's 2D clip system rasterizes stencil masks; its internal restore is scoped to Flight's own
465
+ // nesting and disables the test outright when Flight had no clip, so the host-facing preservation
466
+ // has to happen here. The outer fixture starts ENABLED so 'restored' is distinguishable from 'left off'.
467
+ expect(gl.isEnabled(gl.STENCIL_TEST)).toBe(expected.stencilTest);
468
+ expect(parameters.get(gl.STENCIL_WRITEMASK)).toBe(expected.stencilWriteMask);
318
469
  expect(gl.isEnabled(gl.BLEND)).toBe(expected.blend);
319
470
  expect(parameters.get(gl.BLEND_SRC_RGB)).toBe(expected.blendSrcRgb);
320
471
  expect(parameters.get(gl.BLEND_DST_RGB)).toBe(expected.blendDstRgb);
@@ -347,3 +498,22 @@ function setCapability(gl: WebGL2RenderingContext, capability: number, enabled:
347
498
  gl.disable(capability);
348
499
  }
349
500
  }
501
+
502
+ // Both faces of every stencil parameter the bracket saves. Naming them here rather than asserting a
503
+ // handful keeps the list checkable against the saved-state fields by eye.
504
+ const TWO_SIDED_STENCIL_PARAMETERS = [
505
+ 'STENCIL_FAIL',
506
+ 'STENCIL_BACK_FAIL',
507
+ 'STENCIL_PASS_DEPTH_FAIL',
508
+ 'STENCIL_BACK_PASS_DEPTH_FAIL',
509
+ 'STENCIL_PASS_DEPTH_PASS',
510
+ 'STENCIL_BACK_PASS_DEPTH_PASS',
511
+ 'STENCIL_FUNC',
512
+ 'STENCIL_BACK_FUNC',
513
+ 'STENCIL_REF',
514
+ 'STENCIL_BACK_REF',
515
+ 'STENCIL_VALUE_MASK',
516
+ 'STENCIL_BACK_VALUE_MASK',
517
+ 'STENCIL_WRITEMASK',
518
+ 'STENCIL_BACK_WRITEMASK',
519
+ ] as const;
@@ -1,16 +1,18 @@
1
1
  import { EntityRuntimeKey } from '@flighthq/types/contract';
2
2
 
3
+ import { getGlRenderStateRuntime } from './glRenderState';
3
4
  import {
4
5
  compileDefaultGlProgram,
5
6
  compileGlBitmapProgram,
6
7
  createDefaultGlBitmapShader,
7
8
  createGlBitmapShader,
9
+ ensureDefaultGlBitmapShader,
8
10
  setGlAttributes,
9
11
  setGlBaseUniforms,
10
12
  setGlMatrixFromTransform,
11
13
  setGlMatrixFromValues,
12
14
  } from './glShader';
13
- import { makeGL, makeShaderLoc } from './glTestHelper';
15
+ import { createGlState, makeGL, makeShaderLoc } from './glTestHelper';
14
16
 
15
17
  describe('compileDefaultGlProgram', () => {
16
18
  it('returns shader locations with all required fields', () => {
@@ -150,6 +152,30 @@ describe('createGlBitmapShader', () => {
150
152
  });
151
153
  });
152
154
 
155
+ describe('ensureDefaultGlBitmapShader', () => {
156
+ it('compiles and caches the default shader on first call', () => {
157
+ const { state } = createGlState();
158
+ const runtime = getGlRenderStateRuntime(state);
159
+ runtime.defaultBitmapShader = null;
160
+ runtime.shaderLoc = null;
161
+ const shader = ensureDefaultGlBitmapShader(state);
162
+ expect(shader).toBeDefined();
163
+ expect(shader.program).toBeDefined();
164
+ expect(runtime.defaultBitmapShader).toBe(shader);
165
+ expect(runtime.shaderLoc).not.toBeNull();
166
+ });
167
+
168
+ it('returns the cached shader on subsequent calls', () => {
169
+ const { state } = createGlState();
170
+ const runtime = getGlRenderStateRuntime(state);
171
+ runtime.defaultBitmapShader = null;
172
+ runtime.shaderLoc = null;
173
+ const first = ensureDefaultGlBitmapShader(state);
174
+ const second = ensureDefaultGlBitmapShader(state);
175
+ expect(second).toBe(first);
176
+ });
177
+ });
178
+
153
179
  describe('setGlAttributes', () => {
154
180
  it('enables the position vertex attrib array', () => {
155
181
  const gl = makeGL();
@@ -233,6 +259,34 @@ describe('setGlBaseUniforms', () => {
233
259
  });
234
260
 
235
261
  describe('setGlMatrixFromTransform', () => {
262
+ it('projects the quad corner order to a clockwise triangle, which is why 2D draws need culling off', () => {
263
+ // Measured in a real WebGL2 context, not argued: with CULL_FACE enabled and the CCW default, this
264
+ // quad renders 0 lit pixels where it otherwise renders 2304 — a single-sided 3D draw earlier in the
265
+ // frame therefore erases all 2D content. `renderGlScene2D` disables CULL_FACE because of THIS, and
266
+ // without an assertion the reason lives only in a comment: someone who later made the winding CCW
267
+ // would find the disable looking superfluous. The corner order below mirrors the quadVertexData
268
+ // fill in glDraw.ts and must be changed with it.
269
+ const gl = makeGL();
270
+ const m = new Float32Array(9);
271
+ const canvas = document.createElement('canvas');
272
+ canvas.width = 200;
273
+ canvas.height = 100;
274
+ setGlMatrixFromTransform(gl, makeShaderLoc(), m, { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, canvas);
275
+
276
+ const project = (x: number, y: number): [number, number] => [
277
+ m[0] * x + m[3] * y + m[6],
278
+ m[1] * x + m[4] * y + m[7],
279
+ ];
280
+ const [x0, y0, x1, y1] = [20, 30, 120, 80];
281
+ const [ax, ay] = project(x0, y0);
282
+ const [bx, by] = project(x1, y0);
283
+ const [cx, cy] = project(x1, y1);
284
+
285
+ // Signed area of the first indexed triangle (indices [0, 1, 2]); negative is clockwise.
286
+ const signedArea = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
287
+ expect(signedArea).toBeLessThan(0);
288
+ });
289
+
236
290
  it('computes matrix scaled by canvas dimensions for identity transform', () => {
237
291
  const gl = makeGL();
238
292
  const loc = makeShaderLoc();
@@ -0,0 +1,51 @@
1
+ import { makeGL } from './glTestHelper';
2
+
3
+ describe('makeGL', () => {
4
+ it('keeps draw validation outside a test-installed mock implementation', () => {
5
+ const gl = makeGL();
6
+ const draw = vi.fn();
7
+ (gl.drawElements as ReturnType<typeof vi.fn>).mockImplementation(draw);
8
+
9
+ gl.drawElements(gl.TRIANGLES, 1, gl.UNSIGNED_SHORT, 1);
10
+
11
+ expect(draw).not.toHaveBeenCalled();
12
+ expect(gl.getError()).toBe(gl.INVALID_OPERATION);
13
+
14
+ gl.drawElements(gl.TRIANGLES, 1, gl.UNSIGNED_SHORT, 0);
15
+ expect(draw).toHaveBeenCalledOnce();
16
+ });
17
+
18
+ it('sets and consumes the first GL draw error without throwing', () => {
19
+ const gl = makeGL();
20
+
21
+ expect(() => gl.drawElements(gl.TRIANGLES, -1, gl.UNSIGNED_SHORT, 0)).not.toThrow();
22
+ gl.drawElements(0xffff, 1, gl.UNSIGNED_SHORT, 0);
23
+
24
+ expect(gl.drawElements).not.toHaveBeenCalled();
25
+ expect(gl.getError()).toBe(gl.INVALID_VALUE);
26
+ expect(gl.getError()).toBe(gl.NO_ERROR);
27
+ });
28
+
29
+ it('reports a type-misaligned index offset as INVALID_OPERATION', () => {
30
+ const gl = makeGL();
31
+
32
+ gl.drawElements(gl.TRIANGLES, 1, gl.UNSIGNED_SHORT, 1);
33
+
34
+ expect(gl.getError()).toBe(gl.INVALID_OPERATION);
35
+ expect(gl.getError()).toBe(gl.NO_ERROR);
36
+ });
37
+
38
+ it('validates non-indexed and instanced draw entry points', () => {
39
+ const gl = makeGL();
40
+
41
+ gl.drawArrays(0xffff, 0, 1);
42
+ expect(gl.getError()).toBe(gl.INVALID_ENUM);
43
+
44
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 1, -1);
45
+ expect(gl.getError()).toBe(gl.INVALID_VALUE);
46
+
47
+ gl.drawElementsInstanced(gl.TRIANGLES, 1, gl.FLOAT, 0, 1);
48
+ expect(gl.getError()).toBe(gl.INVALID_ENUM);
49
+ expect(gl.getError()).toBe(gl.NO_ERROR);
50
+ });
51
+ });
@@ -1,4 +1,5 @@
1
1
  import { createEntity } from '@flighthq/entity/contract';
2
+ import { getRegistryTableKeys } from '@flighthq/registry/contract';
2
3
  import type { Image, RenderTexture, TextureLike, TextureSource } from '@flighthq/types/contract';
3
4
  import {
4
5
  BitmapTextureSourceKind,
@@ -80,11 +81,17 @@ function textureWithTarget(): TextureLike {
80
81
  return texture;
81
82
  }
82
83
 
84
+ function registeredTextureSourceKinds(state: Parameters<typeof getGlRenderStateRuntime>[0]): string[] {
85
+ const kinds: string[] = [];
86
+ getRegistryTableKeys(kinds, getGlRenderStateRuntime(state).registries.textureResolvers);
87
+ return kinds;
88
+ }
89
+
83
90
  describe('registerGlBitmapTextureResolver', () => {
84
91
  it('registers only the Bitmap source key', () => {
85
92
  const { state } = createGlState();
86
93
  registerGlBitmapTextureResolver(state);
87
- expect([...getGlRenderStateRuntime(state).glTextureResolverRegistry!.keys()]).toEqual([BitmapTextureSourceKind]);
94
+ expect(registeredTextureSourceKinds(state)).toEqual([BitmapTextureSourceKind]);
88
95
  });
89
96
  });
90
97
 
@@ -92,9 +99,7 @@ describe('registerGlCompressedImageTextureResolver', () => {
92
99
  it('registers only the CompressedImage source key', () => {
93
100
  const { state } = createGlState();
94
101
  registerGlCompressedImageTextureResolver(state);
95
- expect([...getGlRenderStateRuntime(state).glTextureResolverRegistry!.keys()]).toEqual([
96
- CompressedImageTextureSourceKind,
97
- ]);
102
+ expect(registeredTextureSourceKinds(state)).toEqual([CompressedImageTextureSourceKind]);
98
103
  });
99
104
  });
100
105
 
@@ -225,7 +230,7 @@ describe('registerGlTextureResolver', () => {
225
230
 
226
231
  registerGlTextureResolver(a, sourceKind, second);
227
232
  expect(resolveGlTexture(a, texture)).toEqual({ second: true });
228
- expect(getGlRenderStateRuntime(a).glTextureResolverRegistry?.size).toBe(1);
233
+ expect(getGlRenderStateRuntime(a).registries.textureResolvers.entries.size).toBe(1);
229
234
 
230
235
  registerGlTextureResolver(a, sourceKind, null);
231
236
  expect(resolveGlTexture(a, texture)).toBeNull();
@@ -244,7 +249,7 @@ describe('registerStandardGlTextureResolvers', () => {
244
249
  it('registers bitmap, image, and render texture sources without compressed images', () => {
245
250
  const { state } = createGlState();
246
251
  registerStandardGlTextureResolvers(state);
247
- expect([...getGlRenderStateRuntime(state).glTextureResolverRegistry!.keys()]).toEqual([
252
+ expect(registeredTextureSourceKinds(state)).toEqual([
248
253
  BitmapTextureSourceKind,
249
254
  ImageTextureSourceKind,
250
255
  RenderTargetTextureSourceKind,