@flighthq/render-gl 0.3.0-next.1032.e2b99fc → 0.3.0-next.1220.6982c21
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.
- package/dist/contract.d.ts +4 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +4 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableGlRenderStateGuards.d.ts +4 -0
- package/dist/enableGlRenderStateGuards.d.ts.map +1 -0
- package/dist/enableGlRenderStateGuards.js +30 -0
- package/dist/enableGlRenderStateGuards.js.map +1 -0
- package/dist/enableGlTextureResolverGuards.d.ts +4 -0
- package/dist/enableGlTextureResolverGuards.d.ts.map +1 -0
- package/dist/enableGlTextureResolverGuards.js +8 -0
- package/dist/enableGlTextureResolverGuards.js.map +1 -0
- package/dist/explainGlTextureResolution.d.ts +3 -0
- package/dist/explainGlTextureResolution.d.ts.map +1 -0
- package/dist/explainGlTextureResolution.js +12 -0
- package/dist/explainGlTextureResolution.js.map +1 -0
- package/dist/glCompressedTexture.js +1 -1
- package/dist/glCompressedTexture.js.map +1 -1
- package/dist/glDraw.d.ts +2 -2
- package/dist/glDraw.d.ts.map +1 -1
- package/dist/glDraw.js +17 -14
- package/dist/glDraw.js.map +1 -1
- package/dist/glExternalTexture.d.ts.map +1 -1
- package/dist/glExternalTexture.js +23 -18
- package/dist/glExternalTexture.js.map +1 -1
- package/dist/glFullscreenPass.js +3 -4
- package/dist/glFullscreenPass.js.map +1 -1
- package/dist/glLinearToSrgbPass.js +3 -3
- package/dist/glLinearToSrgbPass.js.map +1 -1
- package/dist/glPresentRenderTarget.js +3 -3
- package/dist/glPresentRenderTarget.js.map +1 -1
- package/dist/glRenderPass.d.ts.map +1 -1
- package/dist/glRenderPass.js +82 -36
- package/dist/glRenderPass.js.map +1 -1
- package/dist/glRenderState.d.ts +11 -1
- package/dist/glRenderState.d.ts.map +1 -1
- package/dist/glRenderState.js +180 -5
- package/dist/glRenderState.js.map +1 -1
- package/dist/glRenderStateBracket.d.ts.map +1 -1
- package/dist/glRenderStateBracket.js +16 -4
- package/dist/glRenderStateBracket.js.map +1 -1
- package/dist/glRenderTarget.d.ts +10 -1
- package/dist/glRenderTarget.d.ts.map +1 -1
- package/dist/glRenderTarget.js +129 -60
- package/dist/glRenderTarget.js.map +1 -1
- package/dist/glRenderTargetPool.d.ts +4 -1
- package/dist/glRenderTargetPool.d.ts.map +1 -1
- package/dist/glRenderTargetPool.js +32 -23
- package/dist/glRenderTargetPool.js.map +1 -1
- package/dist/glRenderTexture.d.ts +4 -1
- package/dist/glRenderTexture.d.ts.map +1 -1
- package/dist/glRenderTexture.js +75 -17
- package/dist/glRenderTexture.js.map +1 -1
- package/dist/glRenderTexturePool.d.ts +7 -0
- package/dist/glRenderTexturePool.d.ts.map +1 -0
- package/dist/glRenderTexturePool.js +87 -0
- package/dist/glRenderTexturePool.js.map +1 -0
- package/dist/glTestHelper.js +2 -2
- package/dist/glTestHelper.js.map +1 -1
- package/dist/glTextureResolver.d.ts +3 -3
- package/dist/glTextureResolver.d.ts.map +1 -1
- package/dist/glTextureResolver.js +32 -31
- package/dist/glTextureResolver.js.map +1 -1
- package/dist/glTextureUpload.d.ts +2 -2
- package/dist/glTextureUpload.d.ts.map +1 -1
- package/dist/glTextureUpload.js +1 -1
- package/dist/glTextureUpload.js.map +1 -1
- package/dist/glTextureVideoUpload.d.ts +2 -2
- package/dist/glTextureVideoUpload.d.ts.map +1 -1
- package/dist/glTextureVideoUpload.js +3 -3
- package/dist/glTextureVideoUpload.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/enableGlRenderStateGuards.test.ts +27 -0
- package/src/enableGlTextureResolverGuards.test.ts +58 -0
- package/src/explainGlTextureResolution.test.ts +32 -0
- package/src/glCompressedTexture.test.ts +2 -2
- package/src/glDraw.test.ts +17 -18
- package/src/glExternalTexture.test.ts +3 -3
- package/src/glFullscreenPass.test.ts +13 -0
- package/src/glLinearToSrgbPass.test.ts +13 -0
- package/src/glPresentRenderTarget.test.ts +15 -1
- package/src/glReadback.test.ts +13 -0
- package/src/glRenderPass.test.ts +63 -0
- package/src/glRenderState.test.ts +135 -0
- package/src/glRenderStateBracket.test.ts +31 -1
- package/src/glRenderTarget.test.ts +186 -3
- package/src/glRenderTargetPool.test.ts +110 -5
- package/src/glRenderTexture.test.ts +71 -11
- package/src/glRenderTexturePool.test.ts +112 -0
- package/src/glTextureResolver.test.ts +73 -51
- package/src/glTextureUpload.test.ts +2 -2
- package/src/glTextureVideoUpload.test.ts +5 -5
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BlendMode as BlendModeType,
|
|
3
|
+
GlRenderStateRuntime,
|
|
4
|
+
GlRenderTarget,
|
|
5
|
+
GlScissorRect,
|
|
6
|
+
GlViewportRect,
|
|
7
|
+
} from '@flighthq/types/contract';
|
|
2
8
|
import { BlendMode } from '@flighthq/types/contract';
|
|
3
9
|
|
|
4
10
|
import { getGlRenderStateRuntime } from './glRenderState';
|
|
@@ -17,10 +23,13 @@ type TestGlState = {
|
|
|
17
23
|
blendSrcRgb: number;
|
|
18
24
|
cullFace: boolean;
|
|
19
25
|
cullFaceMode: number;
|
|
26
|
+
currentRenderTarget: GlRenderTarget | null;
|
|
20
27
|
depthFunc: number;
|
|
21
28
|
depthMask: boolean;
|
|
22
29
|
depthTest: boolean;
|
|
30
|
+
framebuffer: WebGLFramebuffer | null;
|
|
23
31
|
program: WebGLProgram | null;
|
|
32
|
+
renderTargetViewport: GlViewportRect | null;
|
|
24
33
|
scissorBox: [number, number, number, number];
|
|
25
34
|
scissorRect: GlScissorRect | null;
|
|
26
35
|
scissorTest: boolean;
|
|
@@ -49,10 +58,13 @@ const OUTER_STATE: TestGlState = {
|
|
|
49
58
|
blendSrcRgb: 1,
|
|
50
59
|
cullFace: false,
|
|
51
60
|
cullFaceMode: 0x0405,
|
|
61
|
+
currentRenderTarget: { name: 'outer-target' } as unknown as GlRenderTarget,
|
|
52
62
|
depthFunc: 0x0203,
|
|
53
63
|
depthMask: false,
|
|
54
64
|
depthTest: true,
|
|
65
|
+
framebuffer: { name: 'outer-framebuffer' } as unknown as WebGLFramebuffer,
|
|
55
66
|
program: { name: 'outer-program' } as unknown as WebGLProgram,
|
|
67
|
+
renderTargetViewport: { height: 220, width: 300, x: 4, y: 5 },
|
|
56
68
|
scissorBox: [2, 3, 40, 50],
|
|
57
69
|
scissorRect: { x: 2, y: 3, width: 40, height: 50 },
|
|
58
70
|
scissorTest: true,
|
|
@@ -74,10 +86,13 @@ const MIDDLE_STATE: TestGlState = {
|
|
|
74
86
|
blendSrcRgb: 1,
|
|
75
87
|
cullFace: true,
|
|
76
88
|
cullFaceMode: 0x0404,
|
|
89
|
+
currentRenderTarget: { name: 'middle-target' } as unknown as GlRenderTarget,
|
|
77
90
|
depthFunc: 0x0201,
|
|
78
91
|
depthMask: true,
|
|
79
92
|
depthTest: false,
|
|
93
|
+
framebuffer: { name: 'middle-framebuffer' } as unknown as WebGLFramebuffer,
|
|
80
94
|
program: { name: 'middle-program' } as unknown as WebGLProgram,
|
|
95
|
+
renderTargetViewport: { height: 460, width: 620, x: 8, y: 9 },
|
|
81
96
|
scissorBox: [6, 7, 80, 90],
|
|
82
97
|
scissorRect: { x: 6, y: 7, width: 80, height: 90 },
|
|
83
98
|
scissorTest: false,
|
|
@@ -91,7 +106,10 @@ const INNER_STATE: TestGlState = {
|
|
|
91
106
|
...OUTER_STATE,
|
|
92
107
|
activeTexture: 0x84c3,
|
|
93
108
|
blendMode: BlendMode.Screen,
|
|
109
|
+
currentRenderTarget: { name: 'inner-target' } as unknown as GlRenderTarget,
|
|
110
|
+
framebuffer: { name: 'inner-framebuffer' } as unknown as WebGLFramebuffer,
|
|
94
111
|
program: { name: 'inner-program' } as unknown as WebGLProgram,
|
|
112
|
+
renderTargetViewport: { height: 200, width: 280, x: 10, y: 11 },
|
|
95
113
|
scissorRect: { x: 10, y: 11, width: 12, height: 13 },
|
|
96
114
|
texture: { name: 'inner-texture' } as unknown as WebGLTexture,
|
|
97
115
|
vertexArray: { name: 'inner-vao' } as unknown as WebGLVertexArrayObject,
|
|
@@ -194,16 +212,20 @@ function applyTestGlState(fixture: StatefulGl, values: Readonly<TestGlState>): v
|
|
|
194
212
|
gl.blendEquationSeparate(values.blendEquationRgb, values.blendEquationAlpha);
|
|
195
213
|
gl.bindVertexArray(values.vertexArray);
|
|
196
214
|
gl.useProgram(values.program);
|
|
215
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, values.framebuffer);
|
|
197
216
|
gl.activeTexture(values.activeTexture);
|
|
198
217
|
gl.bindTexture(gl.TEXTURE_2D, values.texture);
|
|
199
218
|
setCapability(gl, gl.SCISSOR_TEST, values.scissorTest);
|
|
200
219
|
gl.scissor(values.scissorBox[0], values.scissorBox[1], values.scissorBox[2], values.scissorBox[3]);
|
|
201
220
|
gl.viewport(values.viewport[0], values.viewport[1], values.viewport[2], values.viewport[3]);
|
|
202
221
|
runtime.currentBlendMode = values.blendMode;
|
|
222
|
+
runtime.currentFramebuffer = values.framebuffer;
|
|
223
|
+
runtime.currentRenderTarget = values.currentRenderTarget;
|
|
203
224
|
runtime.currentProgram = values.program;
|
|
204
225
|
runtime.currentScissorRect = values.scissorRect;
|
|
205
226
|
runtime.currentTexture = values.texture;
|
|
206
227
|
runtime.currentTextureStraightAlpha = values.straightAlpha;
|
|
228
|
+
runtime.renderTargetViewport = values.renderTargetViewport;
|
|
207
229
|
}
|
|
208
230
|
|
|
209
231
|
function createStatefulGl(): StatefulGl & ReturnType<typeof createGlState> {
|
|
@@ -221,6 +243,7 @@ function createStatefulGl(): StatefulGl & ReturnType<typeof createGlState> {
|
|
|
221
243
|
CURRENT_PROGRAM: 0x8b8d,
|
|
222
244
|
DEPTH_FUNC: 0x0b74,
|
|
223
245
|
DEPTH_WRITEMASK: 0x0b72,
|
|
246
|
+
FRAMEBUFFER_BINDING: 0x8ca6,
|
|
224
247
|
SCISSOR_BOX: 0x0c10,
|
|
225
248
|
TEXTURE_BINDING_2D: 0x8069,
|
|
226
249
|
VERTEX_ARRAY_BINDING: 0x85b5,
|
|
@@ -266,6 +289,9 @@ function createStatefulGl(): StatefulGl & ReturnType<typeof createGlState> {
|
|
|
266
289
|
(gl.useProgram as ReturnType<typeof vi.fn>).mockImplementation((value: WebGLProgram | null) =>
|
|
267
290
|
parameters.set(gl.CURRENT_PROGRAM, value),
|
|
268
291
|
);
|
|
292
|
+
(gl.bindFramebuffer as ReturnType<typeof vi.fn>).mockImplementation(
|
|
293
|
+
(_target: number, value: WebGLFramebuffer | null) => parameters.set(gl.FRAMEBUFFER_BINDING, value),
|
|
294
|
+
);
|
|
269
295
|
(gl.activeTexture as ReturnType<typeof vi.fn>).mockImplementation((value: number) =>
|
|
270
296
|
parameters.set(gl.ACTIVE_TEXTURE, value),
|
|
271
297
|
);
|
|
@@ -298,16 +324,20 @@ function expectTestGlState(fixture: StatefulGl, expected: Readonly<TestGlState>)
|
|
|
298
324
|
expect(parameters.get(gl.BLEND_EQUATION_ALPHA)).toBe(expected.blendEquationAlpha);
|
|
299
325
|
expect(parameters.get(gl.VERTEX_ARRAY_BINDING)).toBe(expected.vertexArray);
|
|
300
326
|
expect(parameters.get(gl.CURRENT_PROGRAM)).toBe(expected.program);
|
|
327
|
+
expect(parameters.get(gl.FRAMEBUFFER_BINDING)).toBe(expected.framebuffer);
|
|
301
328
|
expect(parameters.get(gl.ACTIVE_TEXTURE)).toBe(expected.activeTexture);
|
|
302
329
|
expect(fixture.textureBindings.get(expected.activeTexture)).toBe(expected.texture);
|
|
303
330
|
expect(gl.isEnabled(gl.SCISSOR_TEST)).toBe(expected.scissorTest);
|
|
304
331
|
expect(parameters.get(gl.SCISSOR_BOX)).toEqual(expected.scissorBox);
|
|
305
332
|
expect(parameters.get(gl.VIEWPORT)).toEqual(expected.viewport);
|
|
306
333
|
expect(runtime.currentBlendMode).toBe(expected.blendMode);
|
|
334
|
+
expect(runtime.currentFramebuffer).toBe(expected.framebuffer);
|
|
335
|
+
expect(runtime.currentRenderTarget).toBe(expected.currentRenderTarget);
|
|
307
336
|
expect(runtime.currentProgram).toBe(expected.program);
|
|
308
337
|
expect(runtime.currentScissorRect).toBe(expected.scissorRect);
|
|
309
338
|
expect(runtime.currentTexture).toBe(expected.texture);
|
|
310
339
|
expect(runtime.currentTextureStraightAlpha).toBe(expected.straightAlpha);
|
|
340
|
+
expect(runtime.renderTargetViewport).toBe(expected.renderTargetViewport);
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
function setCapability(gl: WebGL2RenderingContext, capability: number, enabled: boolean): void {
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
declareGlRenderTargetColorSpace,
|
|
10
10
|
destroyGlRenderTarget,
|
|
11
11
|
drawGlRenderTargetResult,
|
|
12
|
+
explainGlRenderTarget,
|
|
13
|
+
isGlRenderTargetFormatSupported,
|
|
12
14
|
resizeGlRenderTarget,
|
|
15
|
+
resolveGlRenderTargetAxes,
|
|
13
16
|
resolveGlRenderTarget,
|
|
14
17
|
} from './glRenderTarget';
|
|
15
18
|
import { createGlState } from './glTestHelper';
|
|
@@ -88,14 +91,44 @@ describe('createGlRenderTarget', () => {
|
|
|
88
91
|
expect(target.colorSpace).toBe('linear');
|
|
89
92
|
});
|
|
90
93
|
|
|
91
|
-
it('
|
|
94
|
+
it('retains canonical requested axes separately from effective storage', () => {
|
|
95
|
+
const { state, gl } = makeState();
|
|
96
|
+
vi.mocked(gl.getParameter).mockImplementation((parameter) => (parameter === gl.MAX_SAMPLES ? 4 : null));
|
|
97
|
+
const target = createGlRenderTarget(state, {
|
|
98
|
+
width: 32,
|
|
99
|
+
height: 16,
|
|
100
|
+
colorAttachments: 2,
|
|
101
|
+
colorFormats: ['rgba8', 'rgba16f'],
|
|
102
|
+
sampleCount: 8,
|
|
103
|
+
depth: 'depth-stencil-sampled',
|
|
104
|
+
colorSpace: 'linear',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(target.requestedAxes).toEqual({
|
|
108
|
+
width: 32,
|
|
109
|
+
height: 16,
|
|
110
|
+
format: 'rgba8',
|
|
111
|
+
colorAttachments: 2,
|
|
112
|
+
colorFormats: ['rgba8', 'rgba16f'],
|
|
113
|
+
sampleCount: 8,
|
|
114
|
+
depth: 'depth-stencil-sampled',
|
|
115
|
+
colorSpace: 'linear',
|
|
116
|
+
});
|
|
117
|
+
expect(target.sampleCount).toBe(4);
|
|
118
|
+
expect(target.depth).toBe('depth-stencil');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('falls back preferred float formats directly to rgba8 when EXT_color_buffer_float is unavailable', () => {
|
|
92
122
|
const { state, gl } = makeState();
|
|
93
123
|
// Simulate GL without float-render support (e.g. headless SwiftShader): a float target would be
|
|
94
124
|
// framebuffer-incomplete and render black, so the effective format degrades to the renderable rgba8.
|
|
95
125
|
(gl as unknown as { getExtension: (n: string) => unknown }).getExtension = (name: string) =>
|
|
96
126
|
name === 'EXT_color_buffer_float' ? null : {};
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
for (const format of ['rgba16f', 'rgba32f'] as const) {
|
|
128
|
+
const target = createGlRenderTarget(state, { width: 32, height: 32, format }, 'preferred');
|
|
129
|
+
expect(target.format).toBe('rgba8');
|
|
130
|
+
expect(target.colorFormats).toEqual(['rgba8']);
|
|
131
|
+
}
|
|
99
132
|
});
|
|
100
133
|
|
|
101
134
|
it('keeps a float format when EXT_color_buffer_float is available', () => {
|
|
@@ -104,6 +137,38 @@ describe('createGlRenderTarget', () => {
|
|
|
104
137
|
const target = createGlRenderTarget(state, { width: 32, height: 32, format: 'rgba16f' });
|
|
105
138
|
expect(target.format).toBe('rgba16f');
|
|
106
139
|
});
|
|
140
|
+
|
|
141
|
+
it('refuses a required unsupported float format before allocating storage', () => {
|
|
142
|
+
const { state, gl } = makeState();
|
|
143
|
+
(gl as unknown as { getExtension: (n: string) => unknown }).getExtension = (name: string) =>
|
|
144
|
+
name === 'EXT_color_buffer_float' ? null : {};
|
|
145
|
+
vi.clearAllMocks();
|
|
146
|
+
|
|
147
|
+
const target = createGlRenderTarget(state, { width: 32, height: 32, format: 'rgba32f' }, 'required');
|
|
148
|
+
|
|
149
|
+
expect(target).toBeNull();
|
|
150
|
+
expect(gl.createFramebuffer).not.toHaveBeenCalled();
|
|
151
|
+
expect(gl.createTexture).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('refuses required heterogeneous storage when any attachment format is unsupported', () => {
|
|
155
|
+
const { state, gl } = makeState();
|
|
156
|
+
(gl as unknown as { getExtension: (n: string) => unknown }).getExtension = (name: string) =>
|
|
157
|
+
name === 'EXT_color_buffer_float' ? null : {};
|
|
158
|
+
|
|
159
|
+
const target = createGlRenderTarget(
|
|
160
|
+
state,
|
|
161
|
+
{
|
|
162
|
+
width: 32,
|
|
163
|
+
height: 32,
|
|
164
|
+
colorAttachments: 2,
|
|
165
|
+
colorFormats: ['rgba8', 'rgba16f'],
|
|
166
|
+
},
|
|
167
|
+
'required',
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(target).toBeNull();
|
|
171
|
+
});
|
|
107
172
|
});
|
|
108
173
|
|
|
109
174
|
describe('declareGlRenderTargetColorSpace', () => {
|
|
@@ -114,6 +179,7 @@ describe('declareGlRenderTargetColorSpace', () => {
|
|
|
114
179
|
beginGlRenderPass(state, target);
|
|
115
180
|
expect(declareGlRenderTargetColorSpace(state, 'linear')).toBe(true);
|
|
116
181
|
expect(target.colorSpace).toBe('linear');
|
|
182
|
+
expect(target.requestedAxes.colorSpace).toBe('linear');
|
|
117
183
|
endGlRenderPass(state);
|
|
118
184
|
});
|
|
119
185
|
|
|
@@ -188,6 +254,57 @@ describe('drawGlRenderTargetResult', () => {
|
|
|
188
254
|
});
|
|
189
255
|
});
|
|
190
256
|
|
|
257
|
+
describe('explainGlRenderTarget', () => {
|
|
258
|
+
it('reports every GL capability substitution without losing the request', () => {
|
|
259
|
+
const { state, gl } = makeState();
|
|
260
|
+
vi.mocked(gl.getParameter).mockImplementation((parameter) => (parameter === gl.MAX_SAMPLES ? 4 : null));
|
|
261
|
+
(gl as unknown as { getExtension: (name: string) => unknown }).getExtension = (name: string) =>
|
|
262
|
+
name === 'EXT_color_buffer_float' ? null : {};
|
|
263
|
+
const target = createGlRenderTarget(state, {
|
|
264
|
+
width: 32,
|
|
265
|
+
height: 16,
|
|
266
|
+
format: 'rgba16f',
|
|
267
|
+
sampleCount: 8,
|
|
268
|
+
depth: 'depth-stencil-sampled',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(explainGlRenderTarget(target).differences).toEqual([
|
|
272
|
+
{ axis: 'format', effective: 'rgba8', requested: 'rgba16f' },
|
|
273
|
+
{ axis: 'colorFormats', effective: ['rgba8'], requested: ['rgba16f'] },
|
|
274
|
+
{ axis: 'sampleCount', effective: 4, requested: 8 },
|
|
275
|
+
{ axis: 'depth', effective: 'depth-stencil', requested: 'depth-stencil-sampled' },
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('reports no differences when GL realizes every requested axis', () => {
|
|
280
|
+
const { state } = makeState();
|
|
281
|
+
const target = createGlRenderTarget(state, { width: 32, height: 16 });
|
|
282
|
+
expect(explainGlRenderTarget(target).differences).toEqual([]);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('isGlRenderTargetFormatSupported', () => {
|
|
287
|
+
it('reports rgba8 independently of float extension support', () => {
|
|
288
|
+
const { state, gl } = makeState();
|
|
289
|
+
const getExtension = vi.fn(() => null);
|
|
290
|
+
(gl as unknown as { getExtension: (name: string) => unknown }).getExtension = getExtension;
|
|
291
|
+
|
|
292
|
+
expect(isGlRenderTargetFormatSupported(state, 'rgba8')).toBe(true);
|
|
293
|
+
expect(getExtension).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('reports both float formats from EXT_color_buffer_float', () => {
|
|
297
|
+
const { state, gl } = makeState();
|
|
298
|
+
(gl as unknown as { getExtension: (name: string) => unknown }).getExtension = () => null;
|
|
299
|
+
expect(isGlRenderTargetFormatSupported(state, 'rgba16f')).toBe(false);
|
|
300
|
+
expect(isGlRenderTargetFormatSupported(state, 'rgba32f')).toBe(false);
|
|
301
|
+
|
|
302
|
+
(gl as unknown as { getExtension: (name: string) => unknown }).getExtension = () => ({});
|
|
303
|
+
expect(isGlRenderTargetFormatSupported(state, 'rgba16f')).toBe(true);
|
|
304
|
+
expect(isGlRenderTargetFormatSupported(state, 'rgba32f')).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
191
308
|
describe('resizeGlRenderTarget', () => {
|
|
192
309
|
it('updates the target dimensions', () => {
|
|
193
310
|
const { state } = makeState();
|
|
@@ -222,6 +339,44 @@ describe('resizeGlRenderTarget', () => {
|
|
|
222
339
|
);
|
|
223
340
|
expect(vi.mocked(gl.texImage2D)).toHaveBeenCalled();
|
|
224
341
|
});
|
|
342
|
+
|
|
343
|
+
it('restores the tracked framebuffer after reallocating storage', () => {
|
|
344
|
+
const { state, gl } = makeState();
|
|
345
|
+
const target = createGlRenderTarget(state, { width: 64, height: 64 });
|
|
346
|
+
const previous = { name: 'previous-framebuffer' } as unknown as WebGLFramebuffer;
|
|
347
|
+
getGlRenderStateRuntime(state).currentFramebuffer = previous;
|
|
348
|
+
vi.clearAllMocks();
|
|
349
|
+
|
|
350
|
+
resizeGlRenderTarget(state, target, 128, 96);
|
|
351
|
+
|
|
352
|
+
expect(vi.mocked(gl.bindFramebuffer)).toHaveBeenLastCalledWith(gl.FRAMEBUFFER, previous);
|
|
353
|
+
expect(getGlRenderStateRuntime(state).currentFramebuffer).toBe(previous);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('preserves heterogeneous color formats and sampled depth across resize', () => {
|
|
357
|
+
const { state, gl } = makeState();
|
|
358
|
+
(gl as unknown as { getExtension: (name: string) => unknown }).getExtension = () => ({});
|
|
359
|
+
const target = createGlRenderTarget(state, {
|
|
360
|
+
width: 64,
|
|
361
|
+
height: 64,
|
|
362
|
+
colorAttachments: 2,
|
|
363
|
+
colorFormats: ['rgba8', 'rgba32f'],
|
|
364
|
+
depth: 'depth-stencil-sampled',
|
|
365
|
+
});
|
|
366
|
+
vi.clearAllMocks();
|
|
367
|
+
|
|
368
|
+
resizeGlRenderTarget(state, target, 128, 96);
|
|
369
|
+
|
|
370
|
+
expect(target.colorAttachments).toBe(2);
|
|
371
|
+
expect(target.colorFormats).toEqual(['rgba8', 'rgba32f']);
|
|
372
|
+
expect(target.depth).toBe('depth-stencil-sampled');
|
|
373
|
+
expect(target.depthTexture).not.toBeNull();
|
|
374
|
+
expect(vi.mocked(gl.texImage2D).mock.calls.map((call) => call[2])).toEqual([
|
|
375
|
+
gl.RGBA8,
|
|
376
|
+
gl.RGBA32F,
|
|
377
|
+
gl.DEPTH24_STENCIL8,
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
225
380
|
});
|
|
226
381
|
|
|
227
382
|
describe('resolveGlRenderTarget', () => {
|
|
@@ -338,3 +493,31 @@ describe('resolveGlRenderTarget', () => {
|
|
|
338
493
|
expect(gl.scissor).toHaveBeenLastCalledWith(3, 4, 16, 12);
|
|
339
494
|
});
|
|
340
495
|
});
|
|
496
|
+
|
|
497
|
+
describe('resolveGlRenderTargetAxes', () => {
|
|
498
|
+
it('queries effective axes without allocating target storage', () => {
|
|
499
|
+
const { state, gl } = makeState();
|
|
500
|
+
vi.mocked(gl.getParameter).mockImplementation((parameter) => (parameter === gl.MAX_SAMPLES ? 2 : null));
|
|
501
|
+
vi.clearAllMocks();
|
|
502
|
+
|
|
503
|
+
const axes = resolveGlRenderTargetAxes(state, { width: 32, height: 16, sampleCount: 4 });
|
|
504
|
+
|
|
505
|
+
expect(axes.sampleCount).toBe(2);
|
|
506
|
+
expect(gl.createFramebuffer).not.toHaveBeenCalled();
|
|
507
|
+
expect(gl.createTexture).not.toHaveBeenCalled();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('applies preferred fallback and required refusal without allocating storage', () => {
|
|
511
|
+
const { state, gl } = makeState();
|
|
512
|
+
(gl as unknown as { getExtension: (name: string) => unknown }).getExtension = () => null;
|
|
513
|
+
vi.clearAllMocks();
|
|
514
|
+
|
|
515
|
+
expect(resolveGlRenderTargetAxes(state, { width: 32, height: 16, format: 'rgba32f' }, 'preferred')).toMatchObject({
|
|
516
|
+
format: 'rgba8',
|
|
517
|
+
colorFormats: ['rgba8'],
|
|
518
|
+
});
|
|
519
|
+
expect(resolveGlRenderTargetAxes(state, { width: 32, height: 16, format: 'rgba32f' }, 'required')).toBeNull();
|
|
520
|
+
expect(gl.createFramebuffer).not.toHaveBeenCalled();
|
|
521
|
+
expect(gl.createTexture).not.toHaveBeenCalled();
|
|
522
|
+
});
|
|
523
|
+
});
|
|
@@ -27,18 +27,123 @@ describe('acquireGlRenderTarget', () => {
|
|
|
27
27
|
expect(pool.free.length).toBe(0);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it('
|
|
30
|
+
it('does not reuse a target with a different color space', () => {
|
|
31
31
|
const { state } = createGlState();
|
|
32
32
|
const pool = createGlRenderTargetPool();
|
|
33
33
|
const first = acquireGlRenderTarget(state, pool, { width: 64, height: 48, colorSpace: 'linear' });
|
|
34
34
|
expect(first.colorSpace).toBe('linear');
|
|
35
35
|
releaseGlRenderTarget(pool, first);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const other = acquireGlRenderTarget(state, pool, { width: 64, height: 48, colorSpace: 'srgb' });
|
|
38
|
+
expect(other).not.toBe(first);
|
|
39
|
+
expect(pool.free).toContain(first);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not reuse a target with different heterogeneous color formats', () => {
|
|
43
|
+
const { state } = createGlState();
|
|
44
|
+
const pool = createGlRenderTargetPool();
|
|
45
|
+
const first = acquireGlRenderTarget(state, pool, {
|
|
46
|
+
width: 64,
|
|
47
|
+
height: 48,
|
|
48
|
+
colorAttachments: 2,
|
|
49
|
+
colorFormats: ['rgba8', 'rgba8'],
|
|
50
|
+
});
|
|
51
|
+
releaseGlRenderTarget(pool, first);
|
|
52
|
+
|
|
53
|
+
const other = acquireGlRenderTarget(state, pool, {
|
|
54
|
+
width: 64,
|
|
55
|
+
height: 48,
|
|
56
|
+
colorAttachments: 2,
|
|
57
|
+
colorFormats: ['rgba8', 'rgba16f'],
|
|
58
|
+
});
|
|
59
|
+
expect(other).not.toBe(first);
|
|
60
|
+
expect(pool.free).toContain(first);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reuses the effective rgba8 target when a preferred float format is unsupported', () => {
|
|
64
|
+
const { state, gl } = createGlState();
|
|
65
|
+
vi.spyOn(gl, 'getExtension').mockReturnValue(null);
|
|
66
|
+
const pool = createGlRenderTargetPool();
|
|
67
|
+
const first = acquireGlRenderTarget(state, pool, { width: 64, height: 48, format: 'rgba8' });
|
|
68
|
+
releaseGlRenderTarget(pool, first);
|
|
69
|
+
|
|
70
|
+
const reused = acquireGlRenderTarget(state, pool, { width: 64, height: 48, format: 'rgba32f' }, 'preferred');
|
|
71
|
+
expect(reused).toBe(first);
|
|
72
|
+
expect(reused.format).toBe('rgba8');
|
|
73
|
+
expect(reused.requestedAxes.format).toBe('rgba32f');
|
|
74
|
+
expect(reused.requestedAxes.colorFormats).toEqual(['rgba32f']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('refuses an unsupported required float format without consuming a fallback target', () => {
|
|
78
|
+
const { state, gl } = createGlState();
|
|
79
|
+
vi.spyOn(gl, 'getExtension').mockReturnValue(null);
|
|
80
|
+
const pool = createGlRenderTargetPool();
|
|
81
|
+
const fallback = acquireGlRenderTarget(state, pool, { width: 64, height: 48, format: 'rgba8' });
|
|
82
|
+
releaseGlRenderTarget(pool, fallback);
|
|
83
|
+
const createFramebufferSpy = vi.spyOn(gl, 'createFramebuffer');
|
|
84
|
+
const clearSpy = vi.spyOn(gl, 'clear');
|
|
85
|
+
createFramebufferSpy.mockClear();
|
|
86
|
+
clearSpy.mockClear();
|
|
87
|
+
|
|
88
|
+
const refused = acquireGlRenderTarget(state, pool, { width: 64, height: 48, format: 'rgba32f' }, 'required');
|
|
89
|
+
expect(refused).toBeNull();
|
|
90
|
+
expect(pool.free).toEqual([fallback]);
|
|
91
|
+
expect(createFramebufferSpy).not.toHaveBeenCalled();
|
|
92
|
+
expect(clearSpy).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('allocates a supported required float format', () => {
|
|
96
|
+
const { state, gl } = createGlState();
|
|
97
|
+
vi.spyOn(gl, 'getExtension').mockReturnValue({} as never);
|
|
98
|
+
const pool = createGlRenderTargetPool();
|
|
99
|
+
|
|
100
|
+
const target = acquireGlRenderTarget(state, pool, { width: 64, height: 48, format: 'rgba16f' }, 'required');
|
|
101
|
+
expect(target).not.toBeNull();
|
|
102
|
+
expect(target?.format).toBe('rgba16f');
|
|
103
|
+
expect(target?.requestedAxes.format).toBe('rgba16f');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not reuse a target with a different depth mode', () => {
|
|
107
|
+
const { state } = createGlState();
|
|
108
|
+
const pool = createGlRenderTargetPool();
|
|
109
|
+
const first = acquireGlRenderTarget(state, pool, { width: 64, height: 48, depth: 'none' });
|
|
110
|
+
releaseGlRenderTarget(pool, first);
|
|
111
|
+
|
|
112
|
+
const other = acquireGlRenderTarget(state, pool, { width: 64, height: 48, depth: 'depth-stencil' });
|
|
113
|
+
expect(other).not.toBe(first);
|
|
114
|
+
expect(pool.free).toContain(first);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('re-stamps clear policy and requested axes on a compatible target', () => {
|
|
118
|
+
const { state } = createGlState();
|
|
119
|
+
const pool = createGlRenderTargetPool();
|
|
120
|
+
const first = acquireGlRenderTarget(state, pool, {
|
|
121
|
+
width: 64,
|
|
122
|
+
height: 48,
|
|
123
|
+
clearColors: [0xff0000ff],
|
|
124
|
+
clearDepth: 0.25,
|
|
125
|
+
});
|
|
126
|
+
releaseGlRenderTarget(pool, first);
|
|
127
|
+
|
|
128
|
+
const reused = acquireGlRenderTarget(state, pool, {
|
|
129
|
+
width: 64,
|
|
130
|
+
height: 48,
|
|
131
|
+
clearColors: [0x00ff00ff],
|
|
132
|
+
clearDepth: 0.75,
|
|
133
|
+
});
|
|
40
134
|
expect(reused).toBe(first);
|
|
41
|
-
expect(reused.
|
|
135
|
+
expect(reused.clearColors).toEqual([0x00ff00ff]);
|
|
136
|
+
expect(reused.clearDepth).toBe(0.75);
|
|
137
|
+
expect(reused.requestedAxes).toEqual({
|
|
138
|
+
width: 64,
|
|
139
|
+
height: 48,
|
|
140
|
+
format: 'rgba8',
|
|
141
|
+
colorAttachments: 1,
|
|
142
|
+
colorFormats: ['rgba8'],
|
|
143
|
+
sampleCount: 1,
|
|
144
|
+
depth: 'none',
|
|
145
|
+
colorSpace: 'srgb',
|
|
146
|
+
});
|
|
42
147
|
});
|
|
43
148
|
|
|
44
149
|
it('allocates a new target when the free target has different dimensions', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
2
|
import type { RenderTexture } from '@flighthq/types/contract';
|
|
3
|
+
import { RenderTargetTextureSourceKind } from '@flighthq/types/contract';
|
|
3
4
|
|
|
4
5
|
import { enableGlRenderTextureGuards } from './enableGlRenderTextureGuards';
|
|
5
6
|
import { getGlRenderStateRuntime } from './glRenderState';
|
|
@@ -8,9 +9,12 @@ import {
|
|
|
8
9
|
destroyGlRenderTexture,
|
|
9
10
|
explainGlRenderTexture,
|
|
10
11
|
getGlRenderTextureColorSpace,
|
|
12
|
+
getGlRenderTextureTarget,
|
|
13
|
+
invalidateGlRenderTexture,
|
|
11
14
|
isGlRenderTextureReady,
|
|
12
15
|
renderIntoGlRenderTexture,
|
|
13
16
|
setGlRenderTextureGuard,
|
|
17
|
+
writeGlRenderTextureTarget,
|
|
14
18
|
} from './glRenderTexture';
|
|
15
19
|
import { createGlState } from './glTestHelper';
|
|
16
20
|
|
|
@@ -95,6 +99,32 @@ describe('getGlRenderTextureColorSpace', () => {
|
|
|
95
99
|
});
|
|
96
100
|
});
|
|
97
101
|
|
|
102
|
+
describe('getGlRenderTextureTarget', () => {
|
|
103
|
+
it('returns the hidden target only after a completed write', () => {
|
|
104
|
+
const { state } = createRenderTextureState();
|
|
105
|
+
const renderTexture = createRenderTexture({ height: 8, width: 8 });
|
|
106
|
+
|
|
107
|
+
expect(getGlRenderTextureTarget(state, renderTexture)).toBeNull();
|
|
108
|
+
writeGlRenderTextureTarget(state, renderTexture, () => {});
|
|
109
|
+
expect(getGlRenderTextureTarget(state, renderTexture)).not.toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('invalidateGlRenderTexture', () => {
|
|
114
|
+
it('makes a previously published target unavailable without deleting it', () => {
|
|
115
|
+
const { state, gl } = createRenderTextureState();
|
|
116
|
+
const renderTexture = createRenderTexture({ height: 8, width: 8 });
|
|
117
|
+
writeGlRenderTextureTarget(state, renderTexture, () => {});
|
|
118
|
+
const deletes = vi.mocked(gl.deleteTexture).mock.calls.length;
|
|
119
|
+
|
|
120
|
+
invalidateGlRenderTexture(state, renderTexture);
|
|
121
|
+
|
|
122
|
+
expect(isGlRenderTextureReady(state, renderTexture)).toBe(false);
|
|
123
|
+
expect(getGlRenderTextureTarget(state, renderTexture)).toBeNull();
|
|
124
|
+
expect(gl.deleteTexture).toHaveBeenCalledTimes(deletes);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
98
128
|
describe('isGlRenderTextureReady', () => {
|
|
99
129
|
it('becomes true only after a completed render', () => {
|
|
100
130
|
const { state } = createRenderTextureState();
|
|
@@ -163,13 +193,29 @@ describe('renderIntoGlRenderTexture', () => {
|
|
|
163
193
|
expect(bindGlRenderTexture(state, renderTexture)).toBeNull();
|
|
164
194
|
});
|
|
165
195
|
|
|
196
|
+
it('keeps an outer writer active after a nested same-target pass is rejected', () => {
|
|
197
|
+
const { state } = createRenderTextureState();
|
|
198
|
+
const renderTexture = createRenderTexture({ height: 8, width: 8 });
|
|
199
|
+
|
|
200
|
+
renderIntoGlRenderTexture(state, renderTexture, () => {
|
|
201
|
+
getGlRenderStateRuntime(state).currentMaskDepth = 1;
|
|
202
|
+
|
|
203
|
+
expect(() => renderIntoGlRenderTexture(state, renderTexture, () => {})).toThrow(
|
|
204
|
+
'cannot nest the active framebuffer while a contour clip is live',
|
|
205
|
+
);
|
|
206
|
+
expect(explainGlRenderTexture(state, renderTexture).status).toBe('writing');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(explainGlRenderTexture(state, renderTexture).status).toBe('ready');
|
|
210
|
+
});
|
|
211
|
+
|
|
166
212
|
it('resizes the retained target when the source dimensions change', () => {
|
|
167
213
|
const { state } = createRenderTextureState();
|
|
168
214
|
const renderTexture = createRenderTexture({ height: 16, width: 32 });
|
|
169
215
|
renderIntoGlRenderTexture(state, renderTexture, () => {});
|
|
170
216
|
|
|
171
|
-
renderTexture.
|
|
172
|
-
renderTexture.
|
|
217
|
+
renderTexture.source.width = 12;
|
|
218
|
+
renderTexture.source.height = 10;
|
|
173
219
|
renderIntoGlRenderTexture(state, renderTexture, () => {});
|
|
174
220
|
|
|
175
221
|
expect(explainGlRenderTexture(state, renderTexture)).toEqual({
|
|
@@ -209,6 +255,21 @@ describe('setGlRenderTextureGuard', () => {
|
|
|
209
255
|
});
|
|
210
256
|
});
|
|
211
257
|
|
|
258
|
+
describe('writeGlRenderTextureTarget', () => {
|
|
259
|
+
it('passes the hidden target to the producer and publishes the completed write', () => {
|
|
260
|
+
const { state } = createRenderTextureState();
|
|
261
|
+
const renderTexture = createRenderTexture({ height: 10, width: 12 });
|
|
262
|
+
const producer = vi.fn();
|
|
263
|
+
|
|
264
|
+
writeGlRenderTextureTarget(state, renderTexture, producer);
|
|
265
|
+
|
|
266
|
+
expect(producer).toHaveBeenCalledOnce();
|
|
267
|
+
expect(producer.mock.calls[0][0]).toBe(getGlRenderTextureTarget(state, renderTexture));
|
|
268
|
+
expect(renderTexture.version).toBe(1);
|
|
269
|
+
expect(isGlRenderTextureReady(state, renderTexture)).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
212
273
|
function createRenderTextureState(): ReturnType<typeof createGlState> {
|
|
213
274
|
const fixture = createGlState();
|
|
214
275
|
const { gl } = fixture;
|
|
@@ -237,15 +298,14 @@ function createRenderTexture(options: { depth?: boolean; height: number; width:
|
|
|
237
298
|
uvOffset: { x: 0, y: 0 },
|
|
238
299
|
uvRotation: 0,
|
|
239
300
|
uvScale: { x: 1, y: 1 },
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
},
|
|
301
|
+
dimension: '2d',
|
|
302
|
+
source: {
|
|
303
|
+
colorSpace: 'linear',
|
|
304
|
+
depth: options.depth ? 'depth-stencil' : 'none',
|
|
305
|
+
height: options.height,
|
|
306
|
+
kind: RenderTargetTextureSourceKind,
|
|
307
|
+
version: 0,
|
|
308
|
+
width: options.width,
|
|
249
309
|
},
|
|
250
310
|
version: 0,
|
|
251
311
|
} as unknown as RenderTexture;
|