@flighthq/render-gl 0.5.1-next.1069.554a2ac → 0.5.1-next.1201.9b49dee

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 (82) hide show
  1. package/README.md +2 -2
  2. package/dist/contract.d.ts +1 -3
  3. package/dist/contract.d.ts.map +1 -1
  4. package/dist/contract.js +1 -3
  5. package/dist/contract.js.map +1 -1
  6. package/dist/enableGlRenderStateGuards.js +2 -2
  7. package/dist/enableGlRenderStateGuards.js.map +1 -1
  8. package/dist/glCubeRenderTarget.d.ts +5 -5
  9. package/dist/glCubeRenderTarget.d.ts.map +1 -1
  10. package/dist/glCubeRenderTarget.js +16 -13
  11. package/dist/glCubeRenderTarget.js.map +1 -1
  12. package/dist/glFullscreenPass.d.ts +4 -3
  13. package/dist/glFullscreenPass.d.ts.map +1 -1
  14. package/dist/glFullscreenPass.js +152 -23
  15. package/dist/glFullscreenPass.js.map +1 -1
  16. package/dist/glLinearToSrgbPass.d.ts +2 -2
  17. package/dist/glLinearToSrgbPass.d.ts.map +1 -1
  18. package/dist/glLinearToSrgbPass.js.map +1 -1
  19. package/dist/glPresentRenderTarget.d.ts +2 -2
  20. package/dist/glPresentRenderTarget.d.ts.map +1 -1
  21. package/dist/glPresentRenderTarget.js.map +1 -1
  22. package/dist/glReadback.d.ts +2 -2
  23. package/dist/glReadback.d.ts.map +1 -1
  24. package/dist/glReadback.js +1 -1
  25. package/dist/glReadback.js.map +1 -1
  26. package/dist/glRenderPass.d.ts +6 -3
  27. package/dist/glRenderPass.d.ts.map +1 -1
  28. package/dist/glRenderPass.js +105 -86
  29. package/dist/glRenderPass.js.map +1 -1
  30. package/dist/glRenderState.d.ts +1 -2
  31. package/dist/glRenderState.d.ts.map +1 -1
  32. package/dist/glRenderState.js +53 -46
  33. package/dist/glRenderState.js.map +1 -1
  34. package/dist/glRenderTarget.d.ts +17 -16
  35. package/dist/glRenderTarget.d.ts.map +1 -1
  36. package/dist/glRenderTarget.js +38 -26
  37. package/dist/glRenderTarget.js.map +1 -1
  38. package/dist/glRenderTargetPool.d.ts +9 -9
  39. package/dist/glRenderTargetPool.d.ts.map +1 -1
  40. package/dist/glRenderTargetPool.js +10 -12
  41. package/dist/glRenderTargetPool.js.map +1 -1
  42. package/dist/glRenderTexture.d.ts +2 -2
  43. package/dist/glRenderTexture.d.ts.map +1 -1
  44. package/dist/glRenderTexture.js +11 -13
  45. package/dist/glRenderTexture.js.map +1 -1
  46. package/dist/glRenderTexturePool.js +3 -5
  47. package/dist/glRenderTexturePool.js.map +1 -1
  48. package/dist/glSkinPaletteTexture.js +8 -2
  49. package/dist/glSkinPaletteTexture.js.map +1 -1
  50. package/dist/glTestHelper.d.ts +0 -1
  51. package/dist/glTestHelper.d.ts.map +1 -1
  52. package/dist/glTestHelper.js +0 -1
  53. package/dist/glTestHelper.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/package.json +11 -11
  59. package/src/enableGlRenderStateGuards.test.ts +2 -4
  60. package/src/glContext.test.ts +3 -52
  61. package/src/glCubeRenderTarget.test.ts +10 -8
  62. package/src/glDraw.test.ts +4 -16
  63. package/src/glFullscreenPass.test.ts +145 -41
  64. package/src/glLinearToSrgbPass.test.ts +8 -5
  65. package/src/glPresentRenderTarget.test.ts +3 -5
  66. package/src/glReadback.test.ts +3 -3
  67. package/src/glRenderPass.test.ts +228 -55
  68. package/src/glRenderState.test.ts +106 -165
  69. package/src/glRenderTarget.test.ts +134 -99
  70. package/src/glRenderTargetPool.test.ts +69 -76
  71. package/src/glRenderTexture.test.ts +58 -31
  72. package/src/glSkinPaletteTexture.test.ts +9 -0
  73. package/src/glTextureResolver.test.ts +5 -1
  74. package/dist/glBackground.d.ts +0 -3
  75. package/dist/glBackground.d.ts.map +0 -1
  76. package/dist/glBackground.js +0 -19
  77. package/dist/glBackground.js.map +0 -1
  78. package/dist/glContext.d.ts +0 -3
  79. package/dist/glContext.d.ts.map +0 -1
  80. package/dist/glContext.js +0 -16
  81. package/dist/glContext.js.map +0 -1
  82. package/src/glBackground.test.ts +0 -68
@@ -2,16 +2,30 @@ import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
2
2
  import { createMatrix } from '@flighthq/geometry/contract';
3
3
  import { getOrCreateRenderProxy2D } from '@flighthq/render/contract';
4
4
  import { createDisplayObject } from '@flighthq/scene2d/contract';
5
- import type { GlContext, GlRenderTarget, Viewport } from '@flighthq/types/contract';
6
-
7
- import { beginGlRenderPass, endGlRenderPass, setGlRenderTransform2D } from './glRenderPass';
8
- import { createGlOffscreenRenderState } from './glRenderState';
9
- import { getGlRenderStateRuntime } from './glRenderState';
5
+ import type {
6
+ GlContext,
7
+ GlRenderPass,
8
+ GlRenderTarget,
9
+ GlTextureRenderTarget,
10
+ Viewport,
11
+ } from '@flighthq/types/contract';
12
+
13
+ import { clearGlRenderTarget, clearGlRenderTargetAttachments, fillGlRect } from './glFullscreenPass';
14
+ import {
15
+ acquireGlRenderPassHandle,
16
+ beginGlRenderPass,
17
+ endGlRenderPass,
18
+ getGlCurrentRenderPass,
19
+ releaseGlRenderPassHandle,
20
+ setGlRenderTransform2D,
21
+ } from './glRenderPass';
22
+ import { createGlRenderState, getGlRenderStateRuntime } from './glRenderState';
23
+ import { drawGlTextureRenderTargetResult } from './glRenderTarget';
10
24
  import { createGlState } from './glTestHelper';
11
25
 
12
- function makeTarget(overrides?: Partial<GlRenderTarget>): GlRenderTarget {
26
+ function makeTarget(overrides?: Partial<GlTextureRenderTarget>): GlTextureRenderTarget {
13
27
  const texture = { id: 'c0' } as unknown as WebGLTexture;
14
- const out = allocateEntity<GlRenderTarget>();
28
+ const out = allocateEntity<GlTextureRenderTarget>();
15
29
  out.requestedAxes = {
16
30
  width: 32,
17
31
  height: 16,
@@ -29,8 +43,6 @@ function makeTarget(overrides?: Partial<GlRenderTarget>): GlRenderTarget {
29
43
  out.colorFormats = ['rgba8'];
30
44
  out.depth = 'depth-stencil';
31
45
  out.colorSpace = 'srgb';
32
- out.clearColors = [];
33
- out.clearDepth = 1;
34
46
  out.sampleCount = 1;
35
47
  out.framebuffer = {} as WebGLFramebuffer;
36
48
  out.resolveFramebuffer = null;
@@ -43,57 +55,93 @@ function makeTarget(overrides?: Partial<GlRenderTarget>): GlRenderTarget {
43
55
  return finishEntity(out);
44
56
  }
45
57
 
58
+ describe('acquireGlRenderPassHandle', () => {
59
+ it('returns a pass with the given gl, state, and target', () => {
60
+ const { state, gl } = createGlState();
61
+ const target = makeTarget();
62
+
63
+ const handle = acquireGlRenderPassHandle(gl, state, target);
64
+
65
+ expect(handle.gl).toBe(gl);
66
+ expect(handle.state).toBe(state);
67
+ expect(handle.target).toBe(target);
68
+ });
69
+
70
+ it('reuses a previously released handle from the same context', () => {
71
+ const { state, gl } = createGlState();
72
+ const target = makeTarget();
73
+
74
+ const first = acquireGlRenderPassHandle(gl, state, target);
75
+ releaseGlRenderPassHandle(gl, first);
76
+ const second = acquireGlRenderPassHandle(gl, state, target);
77
+
78
+ expect(second).toBe(first);
79
+ });
80
+ });
81
+
46
82
  describe('beginGlRenderPass', () => {
47
- it('clears every color attachment and depth by default', () => {
83
+ it('clears color and depth when clear descriptor specifies both', () => {
48
84
  const { state, gl } = createGlState();
49
85
  const clearColor = vi.spyOn(gl, 'clearBufferfv');
50
86
  const clearDepth = vi.spyOn(gl, 'clearBufferfi');
51
87
 
52
- beginGlRenderPass(state, makeTarget());
88
+ beginGlRenderPass(state, makeTarget(), { color: [0, 0, 0, 0], depth: 1.0, stencil: 0 });
53
89
 
54
90
  expect(clearColor).toHaveBeenCalledWith(gl.COLOR, 0, expect.anything());
55
91
  expect(clearDepth).toHaveBeenCalledWith(gl.DEPTH_STENCIL, 0, 1, 0);
56
92
  });
57
93
 
58
- it('spares color when preserveColor is true, still clears depth', () => {
94
+ it('preserves everything when no clear descriptor is given', () => {
59
95
  const { state, gl } = createGlState();
60
96
  const clearColor = vi.spyOn(gl, 'clearBufferfv');
61
97
  const clearDepth = vi.spyOn(gl, 'clearBufferfi');
62
98
 
63
- beginGlRenderPass(state, makeTarget(), { preserveColor: true });
99
+ beginGlRenderPass(state, makeTarget());
100
+
101
+ expect(clearColor).not.toHaveBeenCalled();
102
+ expect(clearDepth).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it('preserves color when color is omitted from clear descriptor', () => {
106
+ const { state, gl } = createGlState();
107
+ const clearColor = vi.spyOn(gl, 'clearBufferfv');
108
+ const clearDepth = vi.spyOn(gl, 'clearBufferfi');
109
+
110
+ beginGlRenderPass(state, makeTarget(), { depth: 1.0, stencil: 0 });
64
111
 
65
112
  expect(clearColor).not.toHaveBeenCalled();
66
113
  expect(clearDepth).toHaveBeenCalled();
67
114
  });
68
115
 
69
- it('spares depth when preserveDepth is true, still clears color', () => {
116
+ it('preserves depth when depth is omitted from clear descriptor', () => {
70
117
  const { state, gl } = createGlState();
71
118
  const clearColor = vi.spyOn(gl, 'clearBufferfv');
72
119
  const clearDepth = vi.spyOn(gl, 'clearBufferfi');
73
120
 
74
- beginGlRenderPass(state, makeTarget(), { preserveDepth: true });
121
+ beginGlRenderPass(state, makeTarget(), { color: [0, 0, 0, 0] });
75
122
 
76
123
  expect(clearColor).toHaveBeenCalled();
77
124
  expect(clearDepth).not.toHaveBeenCalled();
78
125
  });
79
126
 
80
- it('preserves per attachment location when preserveColor is an array', () => {
127
+ it('clears per-attachment with colors array, skipping undefined entries', () => {
81
128
  const { state, gl } = createGlState();
82
129
  const c0 = { id: 'c0' } as unknown as WebGLTexture;
83
130
  const c1 = { id: 'c1' } as unknown as WebGLTexture;
84
131
  const clearColor = vi.spyOn(gl, 'clearBufferfv');
85
132
 
86
- // Keep location 0, clear location 1 — the MRT / G-buffer path.
87
- beginGlRenderPass(state, makeTarget({ textures: [c0, c1], texture: c0 }), { preserveColor: [true, false] });
133
+ beginGlRenderPass(state, makeTarget({ textures: [c0, c1], texture: c0 }), {
134
+ colors: [undefined, [0, 0, 0, 0]],
135
+ });
88
136
 
89
137
  expect(clearColor.mock.calls.map((c) => c[1])).toEqual([1]);
90
138
  });
91
139
 
92
- it("uses the target's packed clearColor over the background color when present", () => {
93
- const { state, gl } = createGlState({ backgroundColorRgba: [0, 0, 0, 1] });
140
+ it('writes float RGBA values directly to clearBufferfv', () => {
141
+ const { state, gl } = createGlState();
94
142
  const clearColor = vi.spyOn(gl, 'clearBufferfv');
95
143
 
96
- beginGlRenderPass(state, makeTarget({ clearColors: [0xff0000ff] })); // opaque red
144
+ beginGlRenderPass(state, makeTarget(), { color: [1, 0, 0, 1] });
97
145
 
98
146
  const rgba = clearColor.mock.calls[0][2] as Float32Array;
99
147
  expect(rgba[0]).toBeCloseTo(1);
@@ -117,7 +165,7 @@ describe('beginGlRenderPass', () => {
117
165
  const target = makeTarget({ width: 100, height: 80 });
118
166
  const viewport = makeViewport(10, 20, 30, 40);
119
167
 
120
- beginGlRenderPass(state, target, undefined, viewport);
168
+ beginGlRenderPass(state, target, { color: [0, 0, 0, 0] }, viewport);
121
169
 
122
170
  expect(gl.viewport).toHaveBeenLastCalledWith(10, 20, 30, 40);
123
171
  expect(gl.enable).toHaveBeenCalledWith(gl.SCISSOR_TEST);
@@ -171,10 +219,10 @@ describe('beginGlRenderPass', () => {
171
219
  const runtime = getGlRenderStateRuntime(state);
172
220
 
173
221
  beginGlRenderPass(state, outer);
174
- beginGlRenderPass(state, inner);
222
+ const innerPass = beginGlRenderPass(state, inner);
175
223
  expect(runtime.currentRenderTarget).toBe(inner);
176
224
 
177
- endGlRenderPass(state);
225
+ endGlRenderPass(innerPass);
178
226
  expect(runtime.currentRenderTarget).toBe(outer);
179
227
  expect(runtime.renderTargetViewport).toEqual({ height: 48, width: 64, x: 0, y: 0 });
180
228
  });
@@ -188,14 +236,14 @@ describe('beginGlRenderPass', () => {
188
236
  const outer = makeTarget({ width: 64, height: 48 });
189
237
  const inner = makeTarget({ width: 32, height: 24 });
190
238
 
191
- beginGlRenderPass(outerState, outer);
192
- beginGlRenderPass(innerState, inner);
193
- endGlRenderPass(innerState);
239
+ const outerPass = beginGlRenderPass(outerState, outer);
240
+ const innerPass = beginGlRenderPass(innerState, inner);
241
+ endGlRenderPass(innerPass);
194
242
 
195
243
  expect(vi.mocked(gl.bindFramebuffer).mock.calls.at(-1)?.[1]).toBe(outer.framebuffer);
196
244
  expect(getGlRenderStateRuntime(outerState).currentRenderTarget).toBe(outer);
197
245
  expect(getGlRenderStateRuntime(innerState).currentRenderTarget).toBeNull();
198
- endGlRenderPass(outerState);
246
+ endGlRenderPass(outerPass);
199
247
  });
200
248
 
201
249
  it('rejects a same-target contour pass across render states sharing one context', () => {
@@ -206,7 +254,7 @@ describe('beginGlRenderPass', () => {
206
254
  (innerState as { gl: GlContext }).gl = gl;
207
255
  const target = makeTarget({ width: 64, height: 48 });
208
256
 
209
- beginGlRenderPass(outerState, target);
257
+ const outerPass = beginGlRenderPass(outerState, target);
210
258
  getGlRenderStateRuntime(outerState).currentMaskDepth = 1;
211
259
 
212
260
  expect(() => beginGlRenderPass(innerState, target)).toThrow(
@@ -214,7 +262,7 @@ describe('beginGlRenderPass', () => {
214
262
  );
215
263
  expect(getGlRenderStateRuntime(outerState).currentRenderTarget).toBe(target);
216
264
  expect(getGlRenderStateRuntime(innerState).currentRenderTarget).toBeNull();
217
- endGlRenderPass(outerState);
265
+ endGlRenderPass(outerPass);
218
266
  });
219
267
  });
220
268
 
@@ -224,8 +272,8 @@ describe('endGlRenderPass', () => {
224
272
  const target = makeTarget();
225
273
  const bindFramebuffer = vi.spyOn(gl, 'bindFramebuffer');
226
274
 
227
- beginGlRenderPass(state, target);
228
- endGlRenderPass(state);
275
+ const pass = beginGlRenderPass(state, target);
276
+ endGlRenderPass(pass);
229
277
 
230
278
  // The pass began with the canvas default bound (null); ending restores it.
231
279
  expect(bindFramebuffer.mock.calls.at(-1)?.[1]).toBe(null);
@@ -235,8 +283,8 @@ describe('endGlRenderPass', () => {
235
283
  const { state } = createGlState();
236
284
  const target = makeTarget();
237
285
 
238
- beginGlRenderPass(state, target);
239
- endGlRenderPass(state);
286
+ const pass = beginGlRenderPass(state, target);
287
+ endGlRenderPass(pass);
240
288
 
241
289
  expect(getGlRenderStateRuntime(state).renderTargetViewport).toBeNull();
242
290
  });
@@ -246,8 +294,8 @@ describe('endGlRenderPass', () => {
246
294
  const target = makeTarget({ width: 100, height: 80 });
247
295
 
248
296
  beginGlRenderPass(state, target, undefined, makeViewport(10, 20, 30, 40));
249
- beginGlRenderPass(state, target, undefined, makeViewport(15, 25, 10, 10));
250
- endGlRenderPass(state);
297
+ const innerPass = beginGlRenderPass(state, target, undefined, makeViewport(15, 25, 10, 10));
298
+ endGlRenderPass(innerPass);
251
299
 
252
300
  expect(gl.viewport).toHaveBeenLastCalledWith(10, 20, 30, 40);
253
301
  expect(gl.scissor).toHaveBeenLastCalledWith(10, 20, 30, 40);
@@ -265,12 +313,12 @@ describe('endGlRenderPass', () => {
265
313
  runtime.scissorStack!.push(outerClip);
266
314
  runtime.clipForms.push('rect');
267
315
 
268
- beginGlRenderPass(state, makeTarget({ width: 100, height: 80 }));
316
+ const innerPass = beginGlRenderPass(state, makeTarget({ width: 100, height: 80 }));
269
317
  expect(runtime.clipForms).toEqual([]);
270
318
  expect(runtime.currentMaskDepth).toBe(0);
271
319
 
272
320
  // An inner renderGlScene2D.finalize now has no enclosing entry to drain.
273
- endGlRenderPass(state);
321
+ endGlRenderPass(innerPass);
274
322
  expect(runtime.currentScissorRect).toBe(outerClip);
275
323
  expect(runtime.scissorStack).toEqual([{ height: 40, width: 30, x: 10, y: 20 }, outerClip]);
276
324
  expect(runtime.clipForms).toEqual(['rect']);
@@ -284,11 +332,11 @@ describe('endGlRenderPass', () => {
284
332
  });
285
333
  vi.mocked(gl.getParameter).mockImplementation((parameter) => (parameter === gl.DEPTH_WRITEMASK ? depthMask : null));
286
334
 
287
- beginGlRenderPass(state, makeTarget(), { preserveDepth: true });
335
+ beginGlRenderPass(state, makeTarget(), { color: [0, 0, 0, 0] });
288
336
  gl.depthMask(false);
289
- beginGlRenderPass(state, makeTarget());
337
+ const innerPass = beginGlRenderPass(state, makeTarget(), { depth: 1.0, stencil: 0 });
290
338
  expect(depthMask).toBe(true);
291
- endGlRenderPass(state);
339
+ endGlRenderPass(innerPass);
292
340
 
293
341
  expect(depthMask).toBe(false);
294
342
  });
@@ -299,12 +347,12 @@ describe('endGlRenderPass', () => {
299
347
  runtime.clipForms = ['contour'];
300
348
  runtime.currentMaskDepth = 1;
301
349
 
302
- beginGlRenderPass(state, makeTarget());
350
+ const pass = beginGlRenderPass(state, makeTarget());
303
351
  expect(runtime.clipForms).toEqual([]);
304
352
  expect(runtime.currentMaskDepth).toBe(0);
305
353
  expect(gl.disable).toHaveBeenCalledWith(gl.STENCIL_TEST);
306
354
 
307
- endGlRenderPass(state);
355
+ endGlRenderPass(pass);
308
356
  expect(runtime.clipForms).toEqual(['contour']);
309
357
  expect(runtime.currentMaskDepth).toBe(1);
310
358
  expect(gl.enable).toHaveBeenCalledWith(gl.STENCIL_TEST);
@@ -316,8 +364,8 @@ describe('endGlRenderPass', () => {
316
364
  it('disables scissor when the outer partial pass returns to the canvas', () => {
317
365
  const { state, gl } = createGlState();
318
366
 
319
- beginGlRenderPass(state, makeTarget(), undefined, makeViewport(1, 2, 10, 8));
320
- endGlRenderPass(state);
367
+ const pass = beginGlRenderPass(state, makeTarget(), undefined, makeViewport(1, 2, 10, 8));
368
+ endGlRenderPass(pass);
321
369
 
322
370
  expect(gl.disable).toHaveBeenCalledWith(gl.SCISSOR_TEST);
323
371
  expect(getGlRenderStateRuntime(state).currentScissorRect).toBeNull();
@@ -326,7 +374,8 @@ describe('endGlRenderPass', () => {
326
374
 
327
375
  it('throws when there is no matching begin', () => {
328
376
  const { state } = createGlState();
329
- expect(() => endGlRenderPass(state)).toThrow('without a matching beginGlRenderPass');
377
+ const mockPass = { gl: state.gl, state, target: {} as GlRenderTarget } as GlRenderPass;
378
+ expect(() => endGlRenderPass(mockPass)).toThrow('without a matching beginGlRenderPass');
330
379
  });
331
380
  });
332
381
 
@@ -334,6 +383,34 @@ function makeViewport(x: number, y: number, width: number, height: number): View
334
383
  return { devicePixelRatio: 1, height, width, x, y } as Viewport;
335
384
  }
336
385
 
386
+ describe('getGlCurrentRenderPass', () => {
387
+ it('returns null when no pass is active', () => {
388
+ const { state } = createGlState();
389
+
390
+ expect(getGlCurrentRenderPass(state)).toBeNull();
391
+ });
392
+
393
+ it('returns the active pass during a render pass', () => {
394
+ const { state } = createGlState();
395
+ const target = makeTarget();
396
+
397
+ const pass = beginGlRenderPass(state, target);
398
+
399
+ expect(getGlCurrentRenderPass(state)).toBe(pass);
400
+ endGlRenderPass(pass);
401
+ });
402
+
403
+ it('returns null after the pass ends', () => {
404
+ const { state } = createGlState();
405
+ const target = makeTarget();
406
+
407
+ const pass = beginGlRenderPass(state, target);
408
+ endGlRenderPass(pass);
409
+
410
+ expect(getGlCurrentRenderPass(state)).toBeNull();
411
+ });
412
+ });
413
+
337
414
  describe('offscreen 2D projection basis', () => {
338
415
  // Pins the property a downstream report claimed was broken: that an offscreen state's 2D pass projects
339
416
  // into the BOUND TARGET's dimensions rather than the shared context's drawing buffer. Reading the
@@ -341,26 +418,26 @@ describe('offscreen 2D projection basis', () => {
341
418
  // looking plausible, so this asserts the pass actually populates its explicit viewport.
342
419
  it('derives the viewport from the target, not from the shared drawing buffer', () => {
343
420
  const { state } = createGlState();
344
- const offscreen = createGlOffscreenRenderState(state.contextState, state.pipeline);
421
+ const offscreen = createGlRenderState(state.gl, state.pipeline);
345
422
  const runtime = getGlRenderStateRuntime(offscreen);
346
423
  expect(offscreen.gl).toBe(state.gl);
347
424
  expect([state.gl.drawingBufferWidth, state.gl.drawingBufferHeight]).toEqual([200, 100]);
348
425
 
349
- beginGlRenderPass(offscreen, makeTarget({ width: 64, height: 32 }));
426
+ const pass = beginGlRenderPass(offscreen, makeTarget({ width: 64, height: 32 }));
350
427
 
351
428
  expect(runtime.renderTargetViewport).toEqual({ height: 32, width: 64, x: 0, y: 0 });
352
- endGlRenderPass(offscreen);
429
+ endGlRenderPass(pass);
353
430
  });
354
431
 
355
432
  it('restores the previous basis when the pass ends', () => {
356
433
  // Guards the guard: a pass that set the viewport and never restored it would satisfy the test above
357
434
  // while leaving every later screen draw projecting into the offscreen target's dimensions.
358
435
  const { state } = createGlState();
359
- const offscreen = createGlOffscreenRenderState(state.contextState, state.pipeline);
436
+ const offscreen = createGlRenderState(state.gl, state.pipeline);
360
437
  const runtime = getGlRenderStateRuntime(offscreen);
361
438
 
362
- beginGlRenderPass(offscreen, makeTarget({ width: 64, height: 32 }));
363
- endGlRenderPass(offscreen);
439
+ const pass = beginGlRenderPass(offscreen, makeTarget({ width: 64, height: 32 }));
440
+ endGlRenderPass(pass);
364
441
 
365
442
  expect(runtime.renderTargetViewport).toBe(null);
366
443
  });
@@ -370,7 +447,7 @@ describe('offscreen 2D projection basis', () => {
370
447
  // compensation. Identity is the correct neutral for both, and the two constructors agreeing is what
371
448
  // makes an offscreen pass behave like a screen pass until a caller deliberately changes it.
372
449
  const { state } = createGlState();
373
- const offscreen = createGlOffscreenRenderState(state.contextState, state.pipeline);
450
+ const offscreen = createGlRenderState(state.gl, state.pipeline);
374
451
 
375
452
  // Field-by-field, not toEqual: a Matrix is entity-backed and carries runtime identity beyond its
376
453
  // public fields, so two structurally identical matrices are not deeply equal.
@@ -381,6 +458,30 @@ describe('offscreen 2D projection basis', () => {
381
458
  });
382
459
  });
383
460
 
461
+ describe('releaseGlRenderPassHandle', () => {
462
+ it('returns a handle to the pool for reuse', () => {
463
+ const { state, gl } = createGlState();
464
+ const target = makeTarget();
465
+
466
+ const handle = acquireGlRenderPassHandle(gl, state, target);
467
+ releaseGlRenderPassHandle(gl, handle);
468
+ const reused = acquireGlRenderPassHandle(gl, state, target);
469
+
470
+ expect(reused).toBe(handle);
471
+ });
472
+
473
+ it('creates the pool lazily on first release', () => {
474
+ const { state, gl } = createGlState();
475
+ const target = makeTarget();
476
+ const handle = acquireGlRenderPassHandle(gl, state, target);
477
+
478
+ releaseGlRenderPassHandle(gl, handle);
479
+
480
+ const second = acquireGlRenderPassHandle(gl, state, target);
481
+ expect(second).toBe(handle);
482
+ });
483
+ });
484
+
384
485
  describe('setGlRenderTransform2D', () => {
385
486
  it('installs a copy of the transform as the 2D root device transform', () => {
386
487
  const { state } = createGlState();
@@ -401,10 +502,10 @@ describe('setGlRenderTransform2D', () => {
401
502
  const cacheTransform = createMatrix();
402
503
  cacheTransform.tx = 99;
403
504
 
404
- beginGlRenderPass(state, target, { preserveColor: true });
505
+ const pass = beginGlRenderPass(state, target, { depth: 1.0, stencil: 0 });
405
506
  setGlRenderTransform2D(state, cacheTransform);
406
507
  expect(state.renderTransform2D?.tx).toBe(99);
407
- endGlRenderPass(state);
508
+ endGlRenderPass(pass);
408
509
 
409
510
  expect(state.renderTransform2D).toBe(original);
410
511
  });
@@ -419,3 +520,75 @@ describe('setGlRenderTransform2D', () => {
419
520
  expect(proxy.lastLocalTransformId).toBe(-1);
420
521
  });
421
522
  });
523
+
524
+ describe('zero-compat ratchet', () => {
525
+ it('clearGlRenderTarget takes (target, clear) — no state parameter', () => {
526
+ expect(clearGlRenderTarget).toHaveLength(2);
527
+ });
528
+
529
+ it('clearGlRenderTargetAttachments takes (target, clear) — no state parameter', () => {
530
+ expect(clearGlRenderTargetAttachments).toHaveLength(2);
531
+ });
532
+
533
+ it('fillGlRect first parameter is a pass, not state', () => {
534
+ const { state } = createGlState();
535
+ const target = makeTarget();
536
+ const pass = beginGlRenderPass(state, target);
537
+ expect(pass.gl).toBe(state.gl);
538
+ expect(pass.state).toBe(state);
539
+ expect(pass.target).toBe(target);
540
+ endGlRenderPass(pass);
541
+ });
542
+
543
+ it('drawGlTextureRenderTargetResult first parameter is a pass', () => {
544
+ expect(drawGlTextureRenderTargetResult).toHaveLength(4);
545
+ });
546
+
547
+ it('beginGlRenderPass returns a GlRenderPass with gl, state, and target', () => {
548
+ const { state } = createGlState();
549
+ const target = makeTarget();
550
+ const pass = beginGlRenderPass(state, target);
551
+ expect(pass).toHaveProperty('gl');
552
+ expect(pass).toHaveProperty('state');
553
+ expect(pass).toHaveProperty('target');
554
+ expect(pass.gl).toBe(state.gl);
555
+ expect(pass.state).toBe(state);
556
+ expect(pass.target).toBe(target);
557
+ endGlRenderPass(pass);
558
+ });
559
+
560
+ it('endGlRenderPass accepts GlRenderPass only', () => {
561
+ expect(endGlRenderPass).toHaveLength(1);
562
+ });
563
+
564
+ it('getGlCurrentRenderPass tracks the active pass on the state', () => {
565
+ const { state } = createGlState();
566
+ const target = makeTarget();
567
+ expect(getGlCurrentRenderPass(state)).toBeNull();
568
+ const pass = beginGlRenderPass(state, target);
569
+ expect(getGlCurrentRenderPass(state)).toBe(pass);
570
+ endGlRenderPass(pass);
571
+ expect(getGlCurrentRenderPass(state)).toBeNull();
572
+ });
573
+
574
+ it('pooled handles are reused across begin/end cycles', () => {
575
+ const { state, gl } = createGlState();
576
+ const target = makeTarget();
577
+ const first = beginGlRenderPass(state, target);
578
+ endGlRenderPass(first);
579
+ const second = beginGlRenderPass(state, target);
580
+ expect(second).toBe(first);
581
+ endGlRenderPass(second);
582
+ });
583
+
584
+ it('screen and offscreen states share the context pass pool', () => {
585
+ const { state, gl } = createGlState();
586
+ const offscreen = createGlRenderState(state.gl, state.pipeline);
587
+ const target = makeTarget();
588
+ const screenPass = beginGlRenderPass(state, target);
589
+ endGlRenderPass(screenPass);
590
+ const offscreenPass = beginGlRenderPass(offscreen, target);
591
+ expect(offscreenPass).toBe(screenPass);
592
+ endGlRenderPass(offscreenPass);
593
+ });
594
+ });