@flighthq/render-gl 0.3.0 → 0.3.1-next.1041.35b950c

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 (39) 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 +19 -12
  12. package/dist/glDraw.js.map +1 -1
  13. package/dist/glMaterialRegistry.d.ts.map +1 -1
  14. package/dist/glMaterialRegistry.js +13 -8
  15. package/dist/glMaterialRegistry.js.map +1 -1
  16. package/dist/glRenderState.d.ts +3 -1
  17. package/dist/glRenderState.d.ts.map +1 -1
  18. package/dist/glRenderState.js +53 -32
  19. package/dist/glRenderState.js.map +1 -1
  20. package/dist/glRenderStateBracket.d.ts.map +1 -1
  21. package/dist/glRenderStateBracket.js +54 -0
  22. package/dist/glRenderStateBracket.js.map +1 -1
  23. package/dist/glSkinPaletteTexture.d.ts +1 -1
  24. package/dist/glSkinPaletteTexture.d.ts.map +1 -1
  25. package/dist/glSkinPaletteTexture.js +6 -2
  26. package/dist/glSkinPaletteTexture.js.map +1 -1
  27. package/dist/glTextureResolver.d.ts.map +1 -1
  28. package/dist/glTextureResolver.js +13 -11
  29. package/dist/glTextureResolver.js.map +1 -1
  30. package/package.json +11 -10
  31. package/src/enableGlRenderStateGuards.test.ts +10 -0
  32. package/src/glCompressedTexture.test.ts +29 -12
  33. package/src/glDraw.test.ts +20 -1
  34. package/src/glMaterialRegistry.test.ts +16 -0
  35. package/src/glRenderState.test.ts +334 -25
  36. package/src/glRenderStateBracket.test.ts +170 -0
  37. package/src/glShader.test.ts +28 -0
  38. package/src/glTestHelper.test.ts +51 -0
  39. package/src/glTextureResolver.test.ts +11 -6
@@ -1,14 +1,31 @@
1
1
  import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
2
+ import {
3
+ createKeyedTable,
4
+ getRegistryTableEntry,
5
+ hasRegistryTableEntry,
6
+ withRegistryTableEntry,
7
+ } from '@flighthq/registry/contract';
2
8
  import {
3
9
  copyAllRenderersFromRenderState,
10
+ enableColorAdjustmentGuards,
11
+ enableColorAdjustments,
12
+ getColorAdjustmentUnsupportedGuard,
4
13
  getRenderStateRuntime,
5
14
  prepareScene2DRender,
6
15
  registerRenderer,
7
16
  } from '@flighthq/render/contract';
8
17
  import { createDisplayObject } from '@flighthq/scene2d/contract';
9
- import { BlendMode } from '@flighthq/types/contract';
10
-
11
- import { enableGlRenderStateGuards } from './enableGlRenderStateGuards';
18
+ import type {
19
+ GlColorAdjustmentMaterialFeature,
20
+ GlColorAdjustmentMaterialFeatureGuard,
21
+ RenderEffectPaddingResolver,
22
+ RenderState,
23
+ } from '@flighthq/types/contract';
24
+ import { BlendMode, RegistryEntryState } from '@flighthq/types/contract';
25
+
26
+ import { areGlRenderStateGuardsEnabled, enableGlRenderStateGuards } from './enableGlRenderStateGuards';
27
+ import { registerGlCompressedTextureDecoder, registerGlCompressedTextureUpload } from './glCompressedTexture';
28
+ import { isBlendModeSupported, registerGlBlendMode } from './glDraw';
12
29
  import { registerGlMaterialRenderer } from './glMaterialRegistry';
13
30
  import {
14
31
  copyGlRenderStateRegistrations,
@@ -16,6 +33,8 @@ import {
16
33
  createGlRenderState,
17
34
  createGlRenderStateRuntime,
18
35
  destroyGlRenderState,
36
+ getGlColorAdjustmentMaterialFeature,
37
+ getGlColorAdjustmentMaterialFeatureGuard,
19
38
  getGlRenderStateRuntime,
20
39
  invalidateGlRenderStateCache,
21
40
  } from './glRenderState';
@@ -31,32 +50,135 @@ function makeCanvas() {
31
50
  return { canvas, gl };
32
51
  }
33
52
 
53
+ function getPaddingResolver(state: RenderState, kind: string): RenderEffectPaddingResolver | null {
54
+ const table = getRenderStateRuntime(state).registries.effectPaddingResolvers;
55
+ return table === undefined ? null : getRegistryTableEntry(table, kind);
56
+ }
57
+
58
+ function registerPaddingResolver(state: RenderState, kind: string, resolver: RenderEffectPaddingResolver): void {
59
+ const runtime = getRenderStateRuntime(state);
60
+ runtime.registries.effectPaddingResolvers = withRegistryTableEntry(
61
+ runtime.registries.effectPaddingResolvers ??
62
+ createKeyedTable<RenderEffectPaddingResolver>('RenderEffectPaddingResolver', 'Zero'),
63
+ kind,
64
+ resolver,
65
+ );
66
+ }
67
+
34
68
  describe('copyGlRenderStateRegistrations', () => {
35
69
  it('copies late GL registrations only when explicitly requested', () => {
36
70
  const { canvas } = makeCanvas();
37
71
  const screen = createGlRenderState(canvas);
38
72
  const offscreen = createGlOffscreenRenderState(screen);
73
+ const materialRenderer = { instanceFloatCount: 0, bind: vi.fn() } as never;
74
+ const offscreenMaterialRenderer = { instanceFloatCount: 0, bind: vi.fn() } as never;
75
+ const decoder = vi.fn(() => new Uint8ClampedArray(4));
39
76
  const resolver = vi.fn(() => null);
77
+ registerGlBlendMode(screen, 'acme.LateBlend', { src: 'ONE', dst: 'ZERO' });
78
+ registerGlCompressedTextureDecoder(screen, decoder);
79
+ registerGlCompressedTextureUpload(screen);
80
+ registerGlMaterialRenderer(screen, 'acme.LateMaterial', materialRenderer);
40
81
  registerGlTextureResolver(screen, 'acme.LateTexture', resolver);
41
82
 
42
- expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
83
+ expect(isBlendModeSupported(offscreen, 'acme.LateBlend')).toBe(false);
84
+ expect(
85
+ hasRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.materialRenderers, 'acme.LateMaterial'),
86
+ ).toBe(false);
87
+ expect(
88
+ hasRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
89
+ ).toBe(false);
90
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureDecoder.entry).toBeNull();
91
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureUpload.entry).toBeNull();
43
92
  copyGlRenderStateRegistrations(offscreen, screen);
44
- expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
93
+ expect(isBlendModeSupported(offscreen, 'acme.LateBlend')).toBe(true);
94
+ expect(
95
+ getRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.materialRenderers, 'acme.LateMaterial'),
96
+ ).toBe(materialRenderer);
97
+ expect(
98
+ getRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
99
+ ).toBe(resolver);
100
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureDecoder).toBe(
101
+ getGlRenderStateRuntime(screen).registries.compressedTextureDecoder,
102
+ );
103
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureDecoder.entry).toEqual({
104
+ state: RegistryEntryState.Bound,
105
+ value: decoder,
106
+ });
107
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureUpload).toBe(
108
+ getGlRenderStateRuntime(screen).registries.compressedTextureUpload,
109
+ );
110
+ registerGlMaterialRenderer(offscreen, 'acme.LateMaterial', offscreenMaterialRenderer);
111
+ registerGlBlendMode(offscreen, 'acme.LateBlend', { src: 'ZERO', dst: 'ONE' });
112
+ registerGlCompressedTextureDecoder(offscreen, null);
113
+ registerGlCompressedTextureUpload(offscreen, null);
114
+ registerGlTextureResolver(offscreen, 'acme.LateTexture', null);
115
+ expect(
116
+ getRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.materialRenderers, 'acme.LateMaterial'),
117
+ ).toBe(offscreenMaterialRenderer);
118
+ expect(
119
+ getRegistryTableEntry(getGlRenderStateRuntime(screen).registries.materialRenderers, 'acme.LateMaterial'),
120
+ ).toBe(materialRenderer);
121
+ expect(
122
+ getRegistryTableEntry(getGlRenderStateRuntime(screen).registries.blendRealizations, 'acme.LateBlend'),
123
+ ).toEqual({ src: 'ONE', dst: 'ZERO' });
124
+ expect(
125
+ hasRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
126
+ ).toBe(false);
127
+ expect(getRegistryTableEntry(getGlRenderStateRuntime(screen).registries.textureResolvers, 'acme.LateTexture')).toBe(
128
+ resolver,
129
+ );
130
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureDecoder.entry).toBeNull();
131
+ expect(getGlRenderStateRuntime(screen).registries.compressedTextureDecoder.entry?.state).toBe(
132
+ RegistryEntryState.Bound,
133
+ );
134
+ expect(getGlRenderStateRuntime(offscreen).registries.compressedTextureUpload.entry).toBeNull();
135
+ expect(getGlRenderStateRuntime(screen).registries.compressedTextureUpload.entry?.state).toBe(
136
+ RegistryEntryState.Bound,
137
+ );
45
138
  });
46
139
  });
47
140
 
48
141
  describe('createGlOffscreenRenderState', () => {
49
- it('shares context resources while snapshotting registration policy into independent maps', () => {
142
+ it('shares context resources and persistent registration snapshots through independent aggregates', () => {
50
143
  const { canvas } = makeCanvas();
51
144
  const screen = createGlRenderState(canvas);
52
145
  const renderer = { createData: () => null, submit: vi.fn() };
53
146
  const materialRenderer = { getBatchData: vi.fn(), getBatchFloats: vi.fn() } as never;
54
147
  const paddingResolver = vi.fn(() => ({ bottom: 1, left: 1, right: 1, top: 1 }));
55
148
  const textureResolver = vi.fn(() => null);
149
+ const effectRunner = vi.fn();
150
+ const colorAdjustmentFeature: GlColorAdjustmentMaterialFeature = {
151
+ drawShapeMeshes: vi.fn(),
152
+ flush: vi.fn(() => false),
153
+ fragmentShaderChunk: '',
154
+ matrixFragmentShaderChunk: '',
155
+ record: vi.fn(),
156
+ };
157
+ const colorAdjustmentFeatureGuard: GlColorAdjustmentMaterialFeatureGuard = vi.fn();
56
158
  registerRenderer(screen, 'acme.Node', renderer);
57
159
  registerGlMaterialRenderer(screen, 'acme.Material', materialRenderer);
58
160
  registerGlTextureResolver(screen, 'acme.Texture', textureResolver);
59
- getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.Effect', paddingResolver]]);
161
+ enableColorAdjustments(screen);
162
+ enableColorAdjustmentGuards(screen);
163
+ enableGlRenderStateGuards(screen);
164
+ getGlRenderStateRuntime(screen).registries.renderEffects = withRegistryTableEntry(
165
+ getGlRenderStateRuntime(screen).registries.renderEffects,
166
+ 'acme.Effect',
167
+ { runner: effectRunner as never },
168
+ );
169
+ registerPaddingResolver(screen, 'acme.Effect', paddingResolver);
170
+ getGlRenderStateRuntime(screen).registries.colorAdjustmentFeature = {
171
+ entry: { state: RegistryEntryState.Bound, value: colorAdjustmentFeature },
172
+ onMiss: 'Disabled',
173
+ registry: 'GlColorAdjustmentFeature',
174
+ shape: 'slot',
175
+ };
176
+ getGlRenderStateRuntime(screen).registries.colorAdjustmentFeatureGuard = {
177
+ entry: { state: RegistryEntryState.Bound, value: colorAdjustmentFeatureGuard },
178
+ onMiss: 'Disabled',
179
+ registry: 'GlColorAdjustmentFeatureGuard',
180
+ shape: 'slot',
181
+ };
60
182
  getGlRenderStateRuntime(screen).glRenderTextureCache = new WeakMap();
61
183
 
62
184
  const offscreen = createGlOffscreenRenderState(screen);
@@ -71,16 +193,67 @@ describe('createGlOffscreenRenderState', () => {
71
193
  );
72
194
  expect(offscreenRuntime.glRenderTextureCache).toBe(screenRuntime.glRenderTextureCache);
73
195
  expect(offscreenRuntime.quadIndexBuffer).toBe(screenRuntime.quadIndexBuffer);
74
- expect(offscreenRuntime.rendererMap).not.toBe(screenRuntime.rendererMap);
75
- expect(offscreenRuntime.materialRendererMap).not.toBe(screenRuntime.materialRendererMap);
76
- expect(offscreenRuntime.glTextureResolverRegistry).not.toBe(screenRuntime.glTextureResolverRegistry);
77
- expect(offscreenRuntime.renderEffectPaddingResolverRegistry).not.toBe(
78
- screenRuntime.renderEffectPaddingResolverRegistry,
196
+ expect(offscreenRuntime.registries.renderers).toBe(screenRuntime.registries.renderers);
197
+ expect(offscreenRuntime.registries).not.toBe(screenRuntime.registries);
198
+ expect(offscreenRuntime.registries.blendRealizations).toBe(screenRuntime.registries.blendRealizations);
199
+ expect(offscreenRuntime.registries.colorAdjustmentFeature).toBe(screenRuntime.registries.colorAdjustmentFeature);
200
+ expect(getGlColorAdjustmentMaterialFeature(offscreen)).toBe(colorAdjustmentFeature);
201
+ expect(offscreenRuntime.registries.colorAdjustmentFeatureGuard).toBe(
202
+ screenRuntime.registries.colorAdjustmentFeatureGuard,
203
+ );
204
+ expect(getGlColorAdjustmentMaterialFeatureGuard(offscreen)).toBe(colorAdjustmentFeatureGuard);
205
+ const sharedColorFeatureGuard = offscreenRuntime.registries.colorAdjustmentFeatureGuard;
206
+ screenRuntime.registries.colorAdjustmentFeatureGuard = undefined;
207
+ expect(getGlColorAdjustmentMaterialFeatureGuard(screen)).toBeNull();
208
+ expect(offscreenRuntime.registries.colorAdjustmentFeatureGuard).toBe(sharedColorFeatureGuard);
209
+ expect(getGlColorAdjustmentMaterialFeatureGuard(offscreen)).toBe(colorAdjustmentFeatureGuard);
210
+ const sharedColorFeature = offscreenRuntime.registries.colorAdjustmentFeature;
211
+ screenRuntime.registries.colorAdjustmentFeature = undefined;
212
+ expect(getGlColorAdjustmentMaterialFeature(screen)).toBeNull();
213
+ expect(offscreenRuntime.registries.colorAdjustmentFeature).toBe(sharedColorFeature);
214
+ expect(getGlColorAdjustmentMaterialFeature(offscreen)).toBe(colorAdjustmentFeature);
215
+ expect(offscreenRuntime.registries.colorAdjustments).toBe(screenRuntime.registries.colorAdjustments);
216
+ expect(offscreenRuntime.registries.colorAdjustmentUnsupportedGuard).toBe(
217
+ screenRuntime.registries.colorAdjustmentUnsupportedGuard,
79
218
  );
80
- expect(offscreenRuntime.rendererMap.get('acme.Node')).toBe(renderer);
81
- expect(offscreenRuntime.materialRendererMap?.get('acme.Material')).toBe(materialRenderer);
82
- expect(offscreenRuntime.glTextureResolverRegistry?.get('acme.Texture')).toBe(textureResolver);
83
- expect(offscreenRuntime.renderEffectPaddingResolverRegistry?.get('acme.Effect')).toBe(paddingResolver);
219
+ expect(getColorAdjustmentUnsupportedGuard(offscreen)).not.toBeNull();
220
+ const sharedUnsupportedGuard = offscreenRuntime.registries.colorAdjustmentUnsupportedGuard;
221
+ screenRuntime.registries.colorAdjustmentUnsupportedGuard = undefined;
222
+ expect(getColorAdjustmentUnsupportedGuard(screen)).toBeNull();
223
+ expect(offscreenRuntime.registries.colorAdjustmentUnsupportedGuard).toBe(sharedUnsupportedGuard);
224
+ expect(getColorAdjustmentUnsupportedGuard(offscreen)).not.toBeNull();
225
+ expect(offscreenRuntime.registries.renderRootGuard).toBe(screenRuntime.registries.renderRootGuard);
226
+ expect(areGlRenderStateGuardsEnabled(offscreen)).toBe(true);
227
+ const sharedRenderRootGuard = offscreenRuntime.registries.renderRootGuard;
228
+ screenRuntime.registries.renderRootGuard = undefined;
229
+ expect(screenRuntime.registries.renderRootGuard).toBeUndefined();
230
+ expect(offscreenRuntime.registries.renderRootGuard).toBe(sharedRenderRootGuard);
231
+ expect(areGlRenderStateGuardsEnabled(offscreen)).toBe(true);
232
+ expect(offscreenRuntime.registries.compressedTextureDecoder).toBe(
233
+ screenRuntime.registries.compressedTextureDecoder,
234
+ );
235
+ expect(offscreenRuntime.registries.compressedTextureUpload).toBe(screenRuntime.registries.compressedTextureUpload);
236
+ expect(offscreenRuntime.registries.customEffectShaders).toBe(screenRuntime.registries.customEffectShaders);
237
+ expect(offscreenRuntime.registries.customMaterialShaders).toBe(screenRuntime.registries.customMaterialShaders);
238
+ expect(offscreenRuntime.registries.materialRenderers).toBe(screenRuntime.registries.materialRenderers);
239
+ expect(offscreenRuntime.registries.meshMaterialRenderers).toBe(screenRuntime.registries.meshMaterialRenderers);
240
+ expect(offscreenRuntime.registries.modifierSnippets).toBe(screenRuntime.registries.modifierSnippets);
241
+ expect(offscreenRuntime.registries.modifierSnippetRevision).toBe(screenRuntime.registries.modifierSnippetRevision);
242
+ expect(offscreenRuntime.registries.pbrExtensions).toBe(screenRuntime.registries.pbrExtensions);
243
+ expect(offscreenRuntime.registries.pbrExtensionRevision).toBe(screenRuntime.registries.pbrExtensionRevision);
244
+ expect(offscreenRuntime.registries.renderEffects).toBe(screenRuntime.registries.renderEffects);
245
+ expect(offscreenRuntime.registries.shapeRasterizer).toBe(screenRuntime.registries.shapeRasterizer);
246
+ expect(offscreenRuntime.registries.strokeTessellator).toBe(screenRuntime.registries.strokeTessellator);
247
+ expect(offscreenRuntime.registries.textureResolvers).toBe(screenRuntime.registries.textureResolvers);
248
+ expect(offscreenRuntime.registries.velocityWriters).toBe(screenRuntime.registries.velocityWriters);
249
+ expect(offscreenRuntime.registries.effectPaddingResolvers).toBe(screenRuntime.registries.effectPaddingResolvers);
250
+ expect(getRegistryTableEntry(offscreenRuntime.registries.renderers, 'acme.Node')).toBe(renderer);
251
+ expect(getRegistryTableEntry(offscreenRuntime.registries.materialRenderers, 'acme.Material')).toBe(
252
+ materialRenderer,
253
+ );
254
+ expect(getRegistryTableEntry(offscreenRuntime.registries.textureResolvers, 'acme.Texture')).toBe(textureResolver);
255
+ expect(getRegistryTableEntry(offscreenRuntime.registries.renderEffects, 'acme.Effect')?.runner).toBe(effectRunner);
256
+ expect(getPaddingResolver(offscreen, 'acme.Effect')).toBe(paddingResolver);
84
257
 
85
258
  screenRuntime.currentProgram = {} as WebGLProgram;
86
259
  expect(offscreenRuntime.currentProgram).toBe(screenRuntime.currentProgram);
@@ -95,19 +268,23 @@ describe('createGlOffscreenRenderState', () => {
95
268
  const resolver = vi.fn(() => null);
96
269
  registerRenderer(screen, 'acme.LateNode', renderer);
97
270
  registerGlTextureResolver(screen, 'acme.LateTexture', resolver);
98
- getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.LateEffect', paddingResolver]]);
271
+ registerPaddingResolver(screen, 'acme.LateEffect', paddingResolver);
99
272
 
100
- expect(getRenderStateRuntime(offscreen).rendererMap.has('acme.LateNode')).toBe(false);
101
- expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
102
- expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.has('acme.LateEffect')).not.toBe(true);
273
+ expect(hasRegistryTableEntry(getRenderStateRuntime(offscreen).registries.renderers, 'acme.LateNode')).toBe(false);
274
+ expect(
275
+ hasRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
276
+ ).toBe(false);
277
+ expect(getPaddingResolver(offscreen, 'acme.LateEffect')).toBeNull();
103
278
 
104
279
  copyAllRenderersFromRenderState(offscreen, screen);
105
280
  copyGlRenderStateRegistrations(offscreen, screen);
106
- expect(getRenderStateRuntime(offscreen).rendererMap.get('acme.LateNode')).toBe(renderer);
107
- expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
108
- expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.get('acme.LateEffect')).toBe(
109
- paddingResolver,
281
+ expect(getRegistryTableEntry(getRenderStateRuntime(offscreen).registries.renderers, 'acme.LateNode')).toBe(
282
+ renderer,
110
283
  );
284
+ expect(
285
+ getRegistryTableEntry(getGlRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
286
+ ).toBe(resolver);
287
+ expect(getPaddingResolver(offscreen, 'acme.LateEffect')).toBe(paddingResolver);
111
288
  });
112
289
 
113
290
  it('keeps proxy trees independent and destroys derived renderer data without freeing shared context resources', () => {
@@ -229,9 +406,89 @@ describe('createGlRenderState', () => {
229
406
  });
230
407
 
231
408
  describe('createGlRenderStateRuntime', () => {
232
- it('returns a runtime carrying the base entity-runtime binding slot', () => {
409
+ it('returns a runtime with the base binding slot and empty named registration tables', () => {
233
410
  const runtime = createGlRenderStateRuntime();
234
411
  expect(runtime.binding).toBeNull();
412
+ expect(runtime.registries.blendRealizations).toMatchObject({
413
+ onMiss: 'Normal',
414
+ registry: 'GlBlendRealization',
415
+ shape: 'keyed',
416
+ });
417
+ expect(runtime.registries.blendRealizations.entries.size).toBe(0);
418
+ expect(runtime.registries.customEffectShaders).toMatchObject({
419
+ onMiss: 'Unregistered',
420
+ registry: 'GlCustomEffectShader',
421
+ shape: 'keyed',
422
+ });
423
+ expect(runtime.registries.customEffectShaders.entries.size).toBe(0);
424
+ expect(runtime.registries.customMaterialShaders).toMatchObject({
425
+ onMiss: 'Unregistered',
426
+ registry: 'GlCustomMaterialShader',
427
+ shape: 'keyed',
428
+ });
429
+ expect(runtime.registries.customMaterialShaders.entries.size).toBe(0);
430
+ expect(runtime.registries.materialRenderers).toMatchObject({
431
+ onMiss: 'StandardMaterial',
432
+ registry: 'GlMaterialRenderer',
433
+ shape: 'keyed',
434
+ });
435
+ expect(runtime.registries.materialRenderers.entries.size).toBe(0);
436
+ expect(runtime.registries.meshMaterialRenderers).toMatchObject({
437
+ onMiss: 'StandardMaterial',
438
+ registry: 'GlMeshMaterialRenderer',
439
+ shape: 'keyed',
440
+ });
441
+ expect(runtime.registries.meshMaterialRenderers.entries.size).toBe(0);
442
+ expect(runtime.registries.modifierSnippets).toMatchObject({
443
+ onMiss: 'Unregistered',
444
+ registry: 'GlModifierSnippet',
445
+ shape: 'keyed',
446
+ });
447
+ expect(runtime.registries.modifierSnippets.entries.size).toBe(0);
448
+ expect(runtime.registries.modifierSnippetRevision).toBe(0);
449
+ expect(runtime.registries.pbrExtensions).toMatchObject({
450
+ onMiss: 'Unregistered',
451
+ registry: 'GlPbrExtension',
452
+ shape: 'keyed',
453
+ });
454
+ expect(runtime.registries.pbrExtensions.entries.size).toBe(0);
455
+ expect(runtime.registries.pbrExtensionRevision).toBe(0);
456
+ expect(runtime.registries.renderEffects).toMatchObject({
457
+ onMiss: 'Unregistered',
458
+ registry: 'GlRenderEffect',
459
+ shape: 'keyed',
460
+ });
461
+ expect(runtime.registries.compressedTextureDecoder).toEqual({
462
+ entry: null,
463
+ onMiss: 'Unregistered',
464
+ registry: 'GlCompressedTextureDecoder',
465
+ shape: 'slot',
466
+ });
467
+ expect(runtime.registries.colorAdjustments).toBeUndefined();
468
+ expect(runtime.registries.compressedTextureUpload).toEqual({
469
+ entry: null,
470
+ onMiss: 'Unregistered',
471
+ registry: 'GlCompressedTextureUpload',
472
+ shape: 'slot',
473
+ });
474
+ expect(runtime.registries.shapeRasterizer).toEqual({
475
+ entry: null,
476
+ onMiss: 'Unregistered',
477
+ registry: 'GlShapeRasterizer',
478
+ shape: 'slot',
479
+ });
480
+ expect(runtime.registries.strokeTessellator).toEqual({
481
+ entry: null,
482
+ onMiss: 'Rasterize',
483
+ registry: 'StrokeTessellator',
484
+ shape: 'slot',
485
+ });
486
+ expect(runtime.registries.velocityWriters).toMatchObject({
487
+ onMiss: 'Unregistered',
488
+ registry: 'GlVelocityWriter',
489
+ shape: 'keyed',
490
+ });
491
+ expect(runtime.registries.velocityWriters.entries.size).toBe(0);
235
492
  });
236
493
  });
237
494
 
@@ -275,6 +532,58 @@ describe('enableGlRenderStateGuards', () => {
275
532
  });
276
533
  });
277
534
 
535
+ describe('getGlColorAdjustmentMaterialFeature', () => {
536
+ it('resolves only a bound feature entry', () => {
537
+ const { canvas } = makeCanvas();
538
+ const state = createGlRenderState(canvas);
539
+ const feature: GlColorAdjustmentMaterialFeature = {
540
+ drawShapeMeshes: vi.fn(),
541
+ flush: vi.fn(() => false),
542
+ fragmentShaderChunk: '',
543
+ matrixFragmentShaderChunk: '',
544
+ record: vi.fn(),
545
+ };
546
+ const runtime = getGlRenderStateRuntime(state);
547
+
548
+ expect(getGlColorAdjustmentMaterialFeature(state)).toBeNull();
549
+ runtime.registries.colorAdjustmentFeature = {
550
+ entry: { state: RegistryEntryState.Bound, value: feature },
551
+ onMiss: 'Disabled',
552
+ registry: 'GlColorAdjustmentFeature',
553
+ shape: 'slot',
554
+ };
555
+ expect(getGlColorAdjustmentMaterialFeature(state)).toBe(feature);
556
+ runtime.registries.colorAdjustmentFeature = {
557
+ ...runtime.registries.colorAdjustmentFeature,
558
+ entry: { state: RegistryEntryState.Tombstoned },
559
+ };
560
+ expect(getGlColorAdjustmentMaterialFeature(state)).toBeNull();
561
+ });
562
+ });
563
+
564
+ describe('getGlColorAdjustmentMaterialFeatureGuard', () => {
565
+ it('resolves only a bound guard entry', () => {
566
+ const { canvas } = makeCanvas();
567
+ const state = createGlRenderState(canvas);
568
+ const guard: GlColorAdjustmentMaterialFeatureGuard = vi.fn();
569
+ const runtime = getGlRenderStateRuntime(state);
570
+
571
+ expect(getGlColorAdjustmentMaterialFeatureGuard(state)).toBeNull();
572
+ runtime.registries.colorAdjustmentFeatureGuard = {
573
+ entry: { state: RegistryEntryState.Bound, value: guard },
574
+ onMiss: 'Disabled',
575
+ registry: 'GlColorAdjustmentFeatureGuard',
576
+ shape: 'slot',
577
+ };
578
+ expect(getGlColorAdjustmentMaterialFeatureGuard(state)).toBe(guard);
579
+ runtime.registries.colorAdjustmentFeatureGuard = {
580
+ ...runtime.registries.colorAdjustmentFeatureGuard,
581
+ entry: { state: RegistryEntryState.Tombstoned },
582
+ };
583
+ expect(getGlColorAdjustmentMaterialFeatureGuard(state)).toBeNull();
584
+ });
585
+ });
586
+
278
587
  describe('getGlRenderStateRuntime', () => {
279
588
  it('returns the runtime attached by createGlRenderState', () => {
280
589
  const { canvas } = makeCanvas();
@@ -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;