@flighthq/render-gl 0.1.0
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/glBackground.d.ts +3 -0
- package/dist/glBackground.d.ts.map +1 -0
- package/dist/glBackground.js +17 -0
- package/dist/glBackground.js.map +1 -0
- package/dist/glDraw.d.ts +11 -0
- package/dist/glDraw.d.ts.map +1 -0
- package/dist/glDraw.js +132 -0
- package/dist/glDraw.js.map +1 -0
- package/dist/glElement.d.ts +2 -0
- package/dist/glElement.d.ts.map +1 -0
- package/dist/glElement.js +9 -0
- package/dist/glElement.js.map +1 -0
- package/dist/glFullscreenPass.d.ts +11 -0
- package/dist/glFullscreenPass.d.ts.map +1 -0
- package/dist/glFullscreenPass.js +114 -0
- package/dist/glFullscreenPass.js.map +1 -0
- package/dist/glMaterialRegistry.d.ts +5 -0
- package/dist/glMaterialRegistry.d.ts.map +1 -0
- package/dist/glMaterialRegistry.js +25 -0
- package/dist/glMaterialRegistry.js.map +1 -0
- package/dist/glProgram.d.ts +4 -0
- package/dist/glProgram.d.ts.map +1 -0
- package/dist/glProgram.js +37 -0
- package/dist/glProgram.js.map +1 -0
- package/dist/glReadback.d.ts +3 -0
- package/dist/glReadback.d.ts.map +1 -0
- package/dist/glReadback.js +33 -0
- package/dist/glReadback.js.map +1 -0
- package/dist/glRenderState.d.ts +7 -0
- package/dist/glRenderState.d.ts.map +1 -0
- package/dist/glRenderState.js +153 -0
- package/dist/glRenderState.js.map +1 -0
- package/dist/glRenderTarget.d.ts +41 -0
- package/dist/glRenderTarget.d.ts.map +1 -0
- package/dist/glRenderTarget.js +284 -0
- package/dist/glRenderTarget.js.map +1 -0
- package/dist/glRenderTargetPool.d.ts +6 -0
- package/dist/glRenderTargetPool.d.ts.map +1 -0
- package/dist/glRenderTargetPool.js +33 -0
- package/dist/glRenderTargetPool.js.map +1 -0
- package/dist/glShader.d.ts +39 -0
- package/dist/glShader.d.ts.map +1 -0
- package/dist/glShader.js +116 -0
- package/dist/glShader.js.map +1 -0
- package/dist/glShaderBinding.d.ts +21 -0
- package/dist/glShaderBinding.d.ts.map +1 -0
- package/dist/glShaderBinding.js +60 -0
- package/dist/glShaderBinding.js.map +1 -0
- package/dist/glShaderRegistry.d.ts +5 -0
- package/dist/glShaderRegistry.d.ts.map +1 -0
- package/dist/glShaderRegistry.js +6 -0
- package/dist/glShaderRegistry.js.map +1 -0
- package/dist/glShaderTypes.d.ts +2 -0
- package/dist/glShaderTypes.d.ts.map +1 -0
- package/dist/glShaderTypes.js +2 -0
- package/dist/glShaderTypes.js.map +1 -0
- package/dist/glTestHelper.d.ts +14 -0
- package/dist/glTestHelper.d.ts.map +1 -0
- package/dist/glTestHelper.js +69 -0
- package/dist/glTestHelper.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +2 -0
- package/dist/internal.js.map +1 -0
- package/package.json +40 -0
- package/src/glBackground.test.ts +43 -0
- package/src/glDraw.test.ts +360 -0
- package/src/glElement.test.ts +38 -0
- package/src/glFullscreenPass.test.ts +185 -0
- package/src/glMaterialRegistry.test.ts +48 -0
- package/src/glProgram.test.ts +59 -0
- package/src/glReadback.test.ts +82 -0
- package/src/glRenderState.test.ts +186 -0
- package/src/glRenderTarget.test.ts +317 -0
- package/src/glRenderTargetPool.test.ts +97 -0
- package/src/glShader.test.ts +331 -0
- package/src/glShaderBinding.test.ts +149 -0
- package/src/glShaderRegistry.test.ts +19 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { GlRenderTarget } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { readGlRenderTargetPixels } from './glReadback';
|
|
4
|
+
import { createGlState } from './glTestHelper';
|
|
5
|
+
|
|
6
|
+
function makeTarget(overrides?: Partial<GlRenderTarget>): GlRenderTarget {
|
|
7
|
+
return {
|
|
8
|
+
width: 4,
|
|
9
|
+
height: 4,
|
|
10
|
+
format: 'rgba8',
|
|
11
|
+
sampleCount: 1,
|
|
12
|
+
framebuffer: {} as WebGLFramebuffer,
|
|
13
|
+
resolveFramebuffer: null,
|
|
14
|
+
textures: [{} as WebGLTexture],
|
|
15
|
+
texture: {} as WebGLTexture,
|
|
16
|
+
depthTexture: null,
|
|
17
|
+
colorRenderbuffers: [],
|
|
18
|
+
depthStencilRenderbuffer: null,
|
|
19
|
+
...overrides,
|
|
20
|
+
} as GlRenderTarget;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('readGlRenderTargetPixels', () => {
|
|
24
|
+
it('returns false for a zero-width target', () => {
|
|
25
|
+
const { state } = createGlState();
|
|
26
|
+
const target = makeTarget({ width: 0, height: 4 });
|
|
27
|
+
const out = new Uint8Array(16);
|
|
28
|
+
expect(readGlRenderTargetPixels(state, target, 0, 0, 1, 1, out)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns false for a zero-height target', () => {
|
|
32
|
+
const { state } = createGlState();
|
|
33
|
+
const target = makeTarget({ width: 4, height: 0 });
|
|
34
|
+
const out = new Uint8Array(16);
|
|
35
|
+
expect(readGlRenderTargetPixels(state, target, 0, 0, 1, 1, out)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false when the framebuffer is incomplete', () => {
|
|
39
|
+
const { state } = createGlState();
|
|
40
|
+
const target = makeTarget();
|
|
41
|
+
vi.spyOn(state.gl, 'checkFramebufferStatus').mockReturnValue(state.gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT ?? 36054);
|
|
42
|
+
const out = new Uint8Array(16);
|
|
43
|
+
expect(readGlRenderTargetPixels(state, target, 0, 0, 1, 1, out)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns true and calls readPixels when the framebuffer is complete', () => {
|
|
47
|
+
const { state } = createGlState();
|
|
48
|
+
const target = makeTarget();
|
|
49
|
+
vi.spyOn(state.gl, 'checkFramebufferStatus').mockReturnValue(state.gl.FRAMEBUFFER_COMPLETE ?? 36053);
|
|
50
|
+
const readPixelsSpy = vi.spyOn(state.gl, 'readPixels').mockImplementation(() => {});
|
|
51
|
+
const out = new Uint8Array(16);
|
|
52
|
+
const result = readGlRenderTargetPixels(state, target, 0, 0, 4, 4, out);
|
|
53
|
+
expect(result).toBe(true);
|
|
54
|
+
expect(readPixelsSpy).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('uses FLOAT type for Float32Array output', () => {
|
|
58
|
+
const { state } = createGlState();
|
|
59
|
+
const target = makeTarget({ format: 'rgba32f' });
|
|
60
|
+
vi.spyOn(state.gl, 'checkFramebufferStatus').mockReturnValue(state.gl.FRAMEBUFFER_COMPLETE ?? 36053);
|
|
61
|
+
const readPixelsSpy = vi.spyOn(state.gl, 'readPixels').mockImplementation(() => {});
|
|
62
|
+
const out = new Float32Array(16);
|
|
63
|
+
readGlRenderTargetPixels(state, target, 0, 0, 4, 4, out);
|
|
64
|
+
const glConst = state.gl;
|
|
65
|
+
expect(readPixelsSpy).toHaveBeenCalledWith(0, 0, 4, 4, expect.any(Number), glConst.FLOAT, out);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uses the resolveFramebuffer when present', () => {
|
|
69
|
+
const { state } = createGlState();
|
|
70
|
+
const resolveFbo = {} as WebGLFramebuffer;
|
|
71
|
+
const drawFbo = {} as WebGLFramebuffer;
|
|
72
|
+
const target = makeTarget({ framebuffer: drawFbo, resolveFramebuffer: resolveFbo });
|
|
73
|
+
const bindSpy = vi.spyOn(state.gl, 'bindFramebuffer');
|
|
74
|
+
vi.spyOn(state.gl, 'readPixels').mockImplementation(() => {});
|
|
75
|
+
const out = new Uint8Array(16);
|
|
76
|
+
readGlRenderTargetPixels(state, target, 0, 0, 1, 1, out);
|
|
77
|
+
// Verify the resolve FBO (not the draw FBO) was bound for reading.
|
|
78
|
+
const boundFbos = bindSpy.mock.calls.map((call) => call[1]);
|
|
79
|
+
expect(boundFbos).toContain(resolveFbo);
|
|
80
|
+
expect(boundFbos).not.toContain(drawFbo);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { BlendMode } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createGlRenderState,
|
|
5
|
+
createGlRenderStateRuntime,
|
|
6
|
+
destroyGlRenderState,
|
|
7
|
+
getGlRenderStateRuntime,
|
|
8
|
+
invalidateGlRenderStateCache,
|
|
9
|
+
} from './glRenderState';
|
|
10
|
+
import { makeGL } from './glTestHelper';
|
|
11
|
+
|
|
12
|
+
function makeCanvas() {
|
|
13
|
+
const canvas = document.createElement('canvas');
|
|
14
|
+
canvas.width = 200;
|
|
15
|
+
canvas.height = 100;
|
|
16
|
+
const gl = makeGL();
|
|
17
|
+
canvas.getContext = vi.fn().mockReturnValue(gl) as typeof canvas.getContext;
|
|
18
|
+
return { canvas, gl };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('createGlRenderState', () => {
|
|
22
|
+
it('throws when Gl2 context is unavailable', () => {
|
|
23
|
+
const canvas = document.createElement('canvas');
|
|
24
|
+
canvas.getContext = vi.fn().mockReturnValue(null) as typeof canvas.getContext;
|
|
25
|
+
expect(() => createGlRenderState(canvas)).toThrow('Failed to get WebGL2 context.');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('stores the canvas on the returned state', () => {
|
|
29
|
+
const { canvas } = makeCanvas();
|
|
30
|
+
const state = createGlRenderState(canvas);
|
|
31
|
+
expect(state.canvas).toBe(canvas);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('stores the GL context on the returned state', () => {
|
|
35
|
+
const { canvas, gl } = makeCanvas();
|
|
36
|
+
const state = createGlRenderState(canvas);
|
|
37
|
+
expect(state.gl).toBe(gl);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('initializes runtime currentBlendMode to null', () => {
|
|
41
|
+
const { canvas } = makeCanvas();
|
|
42
|
+
const state = createGlRenderState(canvas);
|
|
43
|
+
expect(getGlRenderStateRuntime(state).currentBlendMode).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('initializes runtime currentProgram to null', () => {
|
|
47
|
+
const { canvas } = makeCanvas();
|
|
48
|
+
const state = createGlRenderState(canvas);
|
|
49
|
+
expect(getGlRenderStateRuntime(state).currentProgram).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('initializes runtime currentTexture to null', () => {
|
|
53
|
+
const { canvas } = makeCanvas();
|
|
54
|
+
const state = createGlRenderState(canvas);
|
|
55
|
+
expect(getGlRenderStateRuntime(state).currentTexture).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('enables blending during initialization', () => {
|
|
59
|
+
const { canvas, gl } = makeCanvas();
|
|
60
|
+
createGlRenderState(canvas);
|
|
61
|
+
expect(gl.enable).toHaveBeenCalledWith((gl as unknown as { BLEND: number }).BLEND);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('disables depth testing during initialization', () => {
|
|
65
|
+
const { canvas, gl } = makeCanvas();
|
|
66
|
+
createGlRenderState(canvas);
|
|
67
|
+
expect(gl.disable).toHaveBeenCalledWith((gl as unknown as { DEPTH_TEST: number }).DEPTH_TEST);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('sets the default premultiplied-alpha blend function', () => {
|
|
71
|
+
const { canvas, gl } = makeCanvas();
|
|
72
|
+
createGlRenderState(canvas);
|
|
73
|
+
const g = gl as unknown as { ONE: number; ONE_MINUS_SRC_ALPHA: number };
|
|
74
|
+
expect(gl.blendFunc).toHaveBeenCalledWith(g.ONE, g.ONE_MINUS_SRC_ALPHA);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('requests an anti-aliased context by default', () => {
|
|
78
|
+
const { canvas } = makeCanvas();
|
|
79
|
+
createGlRenderState(canvas);
|
|
80
|
+
const attribs = (canvas.getContext as ReturnType<typeof vi.fn>).mock.calls[0][1] as WebGLContextAttributes;
|
|
81
|
+
expect(attribs.antialias).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('disables the anti-aliased context when antialias is false', () => {
|
|
85
|
+
const { canvas } = makeCanvas();
|
|
86
|
+
createGlRenderState(canvas, { antialias: false });
|
|
87
|
+
const attribs = (canvas.getContext as ReturnType<typeof vi.fn>).mock.calls[0][1] as WebGLContextAttributes;
|
|
88
|
+
expect(attribs.antialias).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('applies the backgroundColor option', () => {
|
|
92
|
+
const { canvas } = makeCanvas();
|
|
93
|
+
const state = createGlRenderState(canvas, { backgroundColor: 0xff0000ff });
|
|
94
|
+
expect(state.backgroundColor).toBe(0xff0000ff);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('uses the provided pixelRatio option', () => {
|
|
98
|
+
const { canvas } = makeCanvas();
|
|
99
|
+
const state = createGlRenderState(canvas, { pixelRatio: 2 });
|
|
100
|
+
expect(state.pixelRatio).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('defaults roundPixels to false', () => {
|
|
104
|
+
const { canvas } = makeCanvas();
|
|
105
|
+
const state = createGlRenderState(canvas);
|
|
106
|
+
expect(state.roundPixels).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('applies the roundPixels option', () => {
|
|
110
|
+
const { canvas } = makeCanvas();
|
|
111
|
+
const state = createGlRenderState(canvas, { roundPixels: true });
|
|
112
|
+
expect(state.roundPixels).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('createGlRenderStateRuntime', () => {
|
|
117
|
+
it('returns a runtime carrying the base entity-runtime binding slot', () => {
|
|
118
|
+
const runtime = createGlRenderStateRuntime();
|
|
119
|
+
expect(runtime.binding).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('destroyGlRenderState', () => {
|
|
124
|
+
it('deletes the state-owned shader programs and buffers', () => {
|
|
125
|
+
const { canvas, gl } = makeCanvas();
|
|
126
|
+
const state = createGlRenderState(canvas);
|
|
127
|
+
const deleteProgram = vi.spyOn(gl, 'deleteProgram');
|
|
128
|
+
const deleteBuffer = vi.spyOn(gl, 'deleteBuffer');
|
|
129
|
+
|
|
130
|
+
destroyGlRenderState(state);
|
|
131
|
+
|
|
132
|
+
expect(deleteProgram).toHaveBeenCalled();
|
|
133
|
+
expect(deleteBuffer).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('is safe to call twice (Gl deletes are no-ops on already-deleted resources)', () => {
|
|
137
|
+
const { canvas } = makeCanvas();
|
|
138
|
+
const state = createGlRenderState(canvas);
|
|
139
|
+
destroyGlRenderState(state);
|
|
140
|
+
expect(() => destroyGlRenderState(state)).not.toThrow();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('getGlRenderStateRuntime', () => {
|
|
145
|
+
it('returns the runtime attached by createGlRenderState', () => {
|
|
146
|
+
const { canvas } = makeCanvas();
|
|
147
|
+
const state = createGlRenderState(canvas);
|
|
148
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
149
|
+
expect(runtime).toBeDefined();
|
|
150
|
+
expect(runtime.defaultBitmapShader).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('resolves the same runtime object on repeated calls', () => {
|
|
154
|
+
const { canvas } = makeCanvas();
|
|
155
|
+
const state = createGlRenderState(canvas);
|
|
156
|
+
expect(getGlRenderStateRuntime(state)).toBe(getGlRenderStateRuntime(state));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('invalidateGlRenderStateCache', () => {
|
|
161
|
+
it('nulls the cached GL binding slots so the next draw re-binds from scratch', () => {
|
|
162
|
+
const { canvas } = makeCanvas();
|
|
163
|
+
const state = createGlRenderState(canvas);
|
|
164
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
165
|
+
|
|
166
|
+
// Simulate a full frame of render-gl activity plus a sibling renderer (scene-gl) binding raw GL
|
|
167
|
+
// state the cache never observed: the cache now points at bindings that are no longer current.
|
|
168
|
+
runtime.currentProgram = {} as WebGLProgram;
|
|
169
|
+
runtime.currentTexture = {} as WebGLTexture;
|
|
170
|
+
runtime.currentFramebuffer = {} as WebGLFramebuffer;
|
|
171
|
+
runtime.currentBlendMode = BlendMode.Add;
|
|
172
|
+
runtime.currentMaskDepth = 3;
|
|
173
|
+
runtime.currentScissorRect = { x: 0, y: 0, width: 1, height: 1 };
|
|
174
|
+
runtime.renderTargetViewport = { width: 4, height: 4 };
|
|
175
|
+
|
|
176
|
+
invalidateGlRenderStateCache(state);
|
|
177
|
+
|
|
178
|
+
expect(runtime.currentProgram).toBeNull();
|
|
179
|
+
expect(runtime.currentTexture).toBeNull();
|
|
180
|
+
expect(runtime.currentFramebuffer).toBeNull();
|
|
181
|
+
expect(runtime.currentBlendMode).toBeNull();
|
|
182
|
+
expect(runtime.currentMaskDepth).toBe(0);
|
|
183
|
+
expect(runtime.currentScissorRect).toBeNull();
|
|
184
|
+
expect(runtime.renderTargetViewport).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
3
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
4
|
+
|
|
5
|
+
import { getGlRenderStateRuntime } from './glRenderState';
|
|
6
|
+
import {
|
|
7
|
+
beginGlRenderTarget,
|
|
8
|
+
createGlRenderTarget,
|
|
9
|
+
destroyGlRenderTarget,
|
|
10
|
+
drawGlRenderTargetResult,
|
|
11
|
+
endGlRenderTarget,
|
|
12
|
+
resizeGlRenderTarget,
|
|
13
|
+
resolveGlRenderTarget,
|
|
14
|
+
} from './glRenderTarget';
|
|
15
|
+
import { createGlState } from './glTestHelper';
|
|
16
|
+
|
|
17
|
+
function makeState() {
|
|
18
|
+
const { state: _state, gl: _gl } = createGlState();
|
|
19
|
+
|
|
20
|
+
const mockFramebuffer = {} as WebGLFramebuffer;
|
|
21
|
+
const mockTexture = {} as WebGLTexture;
|
|
22
|
+
|
|
23
|
+
const gl = Object.assign(_gl as unknown as Record<string, unknown>, {
|
|
24
|
+
FRAMEBUFFER: 36160,
|
|
25
|
+
COLOR_ATTACHMENT0: 36064,
|
|
26
|
+
createFramebuffer: vi.fn(() => mockFramebuffer),
|
|
27
|
+
bindFramebuffer: vi.fn(),
|
|
28
|
+
framebufferTexture2D: vi.fn(),
|
|
29
|
+
deleteFramebuffer: vi.fn(),
|
|
30
|
+
deleteTexture: vi.fn(),
|
|
31
|
+
createTexture: vi.fn(() => mockTexture),
|
|
32
|
+
}) as unknown as WebGL2RenderingContext;
|
|
33
|
+
|
|
34
|
+
const state = _state;
|
|
35
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
36
|
+
runtime.currentFramebuffer = null;
|
|
37
|
+
runtime.renderTargetViewport = null;
|
|
38
|
+
|
|
39
|
+
return { state, gl };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('beginGlRenderTarget', () => {
|
|
43
|
+
it('binds the target framebuffer', () => {
|
|
44
|
+
const { state, gl } = makeState();
|
|
45
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
|
|
48
|
+
beginGlRenderTarget(state, target, createMatrix());
|
|
49
|
+
|
|
50
|
+
expect(vi.mocked(gl.bindFramebuffer)).toHaveBeenCalledWith(
|
|
51
|
+
(gl as unknown as { FRAMEBUFFER: number }).FRAMEBUFFER,
|
|
52
|
+
target.framebuffer,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('sets renderTargetViewport to the target dimensions', () => {
|
|
57
|
+
const { state } = makeState();
|
|
58
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
59
|
+
|
|
60
|
+
beginGlRenderTarget(state, target, createMatrix());
|
|
61
|
+
|
|
62
|
+
expect(getGlRenderStateRuntime(state).renderTargetViewport).toEqual({ width: 64, height: 48 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('sets renderTransform2D to the provided transform', () => {
|
|
66
|
+
const { state } = makeState();
|
|
67
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
68
|
+
const renderTransform = createMatrix();
|
|
69
|
+
renderTransform.tx = 10;
|
|
70
|
+
renderTransform.ty = 20;
|
|
71
|
+
|
|
72
|
+
beginGlRenderTarget(state, target, renderTransform);
|
|
73
|
+
|
|
74
|
+
expect(state.renderTransform2D!.tx).toBe(10);
|
|
75
|
+
expect(state.renderTransform2D!.ty).toBe(20);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('supports nested begin calls', () => {
|
|
79
|
+
const { state } = makeState();
|
|
80
|
+
const targetA = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
81
|
+
const targetB = createGlRenderTarget(state, { width: 32, height: 32 });
|
|
82
|
+
|
|
83
|
+
beginGlRenderTarget(state, targetA, createMatrix());
|
|
84
|
+
beginGlRenderTarget(state, targetB, createMatrix());
|
|
85
|
+
|
|
86
|
+
expect(getGlRenderStateRuntime(state).renderTargetViewport).toEqual({ width: 32, height: 32 });
|
|
87
|
+
|
|
88
|
+
endGlRenderTarget(state);
|
|
89
|
+
expect(getGlRenderStateRuntime(state).renderTargetViewport).toEqual({ width: 64, height: 48 });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('createGlRenderTarget', () => {
|
|
94
|
+
it('returns a render target with the requested dimensions', () => {
|
|
95
|
+
const { state } = makeState();
|
|
96
|
+
const target = createGlRenderTarget(state, { width: 128, height: 64 });
|
|
97
|
+
expect(target.width).toBe(128);
|
|
98
|
+
expect(target.height).toBe(64);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('enforces a minimum size of 1', () => {
|
|
102
|
+
const { state } = makeState();
|
|
103
|
+
const target = createGlRenderTarget(state, { width: 0, height: 0 });
|
|
104
|
+
expect(target.width).toBe(1);
|
|
105
|
+
expect(target.height).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('ceils fractional dimensions', () => {
|
|
109
|
+
const { state } = makeState();
|
|
110
|
+
const target = createGlRenderTarget(state, { width: 10.3, height: 20.9 });
|
|
111
|
+
expect(target.width).toBe(11);
|
|
112
|
+
expect(target.height).toBe(21);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('calls createFramebuffer and createTexture', () => {
|
|
116
|
+
const { state, gl } = makeState();
|
|
117
|
+
createGlRenderTarget(state, { width: 64, height: 64 });
|
|
118
|
+
expect(vi.mocked(gl.createFramebuffer)).toHaveBeenCalled();
|
|
119
|
+
expect(vi.mocked(gl.createTexture)).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('resets currentTexture to null after creation', () => {
|
|
123
|
+
const { state } = makeState();
|
|
124
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
125
|
+
runtime.currentTexture = {} as WebGLTexture;
|
|
126
|
+
createGlRenderTarget(state, { width: 32, height: 32 });
|
|
127
|
+
expect(runtime.currentTexture).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('destroyGlRenderTarget', () => {
|
|
132
|
+
it('deletes the framebuffer and texture', () => {
|
|
133
|
+
const { state, gl } = makeState();
|
|
134
|
+
const target = createGlRenderTarget(state, { width: 32, height: 32 });
|
|
135
|
+
const { framebuffer, texture } = target;
|
|
136
|
+
|
|
137
|
+
destroyGlRenderTarget(state, target);
|
|
138
|
+
|
|
139
|
+
expect(vi.mocked(gl.deleteFramebuffer)).toHaveBeenCalledWith(framebuffer);
|
|
140
|
+
expect(vi.mocked(gl.deleteTexture)).toHaveBeenCalledWith(texture);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('drawGlRenderTargetResult', () => {
|
|
145
|
+
it('is a no-op when target dimensions are zero', () => {
|
|
146
|
+
const { state, gl } = makeState();
|
|
147
|
+
const node = getOrCreateRenderProxy2D(state, createDisplayObject());
|
|
148
|
+
const target = createGlRenderTarget(state, { width: 1, height: 1 });
|
|
149
|
+
target.width = 0;
|
|
150
|
+
vi.clearAllMocks();
|
|
151
|
+
|
|
152
|
+
drawGlRenderTargetResult(state, node, target, createMatrix());
|
|
153
|
+
|
|
154
|
+
expect(vi.mocked(gl.bindTexture)).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('composites a valid target without throwing', () => {
|
|
158
|
+
const { state } = makeState();
|
|
159
|
+
const node = getOrCreateRenderProxy2D(state, createDisplayObject());
|
|
160
|
+
node.alpha = 1;
|
|
161
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
162
|
+
|
|
163
|
+
expect(() => drawGlRenderTargetResult(state, node, target, createMatrix())).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('binds the target texture', () => {
|
|
167
|
+
const { state, gl } = makeState();
|
|
168
|
+
const node = getOrCreateRenderProxy2D(state, createDisplayObject());
|
|
169
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
170
|
+
vi.clearAllMocks();
|
|
171
|
+
|
|
172
|
+
drawGlRenderTargetResult(state, node, target, createMatrix());
|
|
173
|
+
|
|
174
|
+
expect(vi.mocked(gl.bindTexture)).toHaveBeenCalledWith(
|
|
175
|
+
(gl as unknown as { TEXTURE_2D: number }).TEXTURE_2D,
|
|
176
|
+
target.texture,
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('endGlRenderTarget', () => {
|
|
182
|
+
it('restores the previous framebuffer', () => {
|
|
183
|
+
const { state, gl } = makeState();
|
|
184
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
185
|
+
|
|
186
|
+
beginGlRenderTarget(state, target, createMatrix());
|
|
187
|
+
vi.clearAllMocks();
|
|
188
|
+
endGlRenderTarget(state);
|
|
189
|
+
|
|
190
|
+
expect(vi.mocked(gl.bindFramebuffer)).toHaveBeenCalledWith(
|
|
191
|
+
(gl as unknown as { FRAMEBUFFER: number }).FRAMEBUFFER,
|
|
192
|
+
null,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('restores renderTargetViewport to null', () => {
|
|
197
|
+
const { state } = makeState();
|
|
198
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
199
|
+
|
|
200
|
+
beginGlRenderTarget(state, target, createMatrix());
|
|
201
|
+
endGlRenderTarget(state);
|
|
202
|
+
|
|
203
|
+
expect(getGlRenderStateRuntime(state).renderTargetViewport).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('restores the original renderTransform2D', () => {
|
|
207
|
+
const { state } = makeState();
|
|
208
|
+
const originalTransform = state.renderTransform2D;
|
|
209
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
210
|
+
const renderTransform = createMatrix();
|
|
211
|
+
renderTransform.tx = 99;
|
|
212
|
+
|
|
213
|
+
beginGlRenderTarget(state, target, renderTransform);
|
|
214
|
+
endGlRenderTarget(state);
|
|
215
|
+
|
|
216
|
+
expect(state.renderTransform2D).toBe(originalTransform);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('without a matching begin is a no-op', () => {
|
|
220
|
+
const { state, gl } = makeState();
|
|
221
|
+
expect(() => endGlRenderTarget(state)).not.toThrow();
|
|
222
|
+
expect(vi.mocked(gl.bindFramebuffer)).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('resizeGlRenderTarget', () => {
|
|
227
|
+
it('updates the target dimensions', () => {
|
|
228
|
+
const { state } = makeState();
|
|
229
|
+
const target = createGlRenderTarget(state, { width: 64, height: 64 });
|
|
230
|
+
|
|
231
|
+
resizeGlRenderTarget(state, target, 256, 128);
|
|
232
|
+
|
|
233
|
+
expect(target.width).toBe(256);
|
|
234
|
+
expect(target.height).toBe(128);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('enforces a minimum size of 1', () => {
|
|
238
|
+
const { state } = makeState();
|
|
239
|
+
const target = createGlRenderTarget(state, { width: 64, height: 64 });
|
|
240
|
+
|
|
241
|
+
resizeGlRenderTarget(state, target, 0, 0);
|
|
242
|
+
|
|
243
|
+
expect(target.width).toBe(1);
|
|
244
|
+
expect(target.height).toBe(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('reallocates the texture storage', () => {
|
|
248
|
+
const { state, gl } = makeState();
|
|
249
|
+
const target = createGlRenderTarget(state, { width: 64, height: 64 });
|
|
250
|
+
vi.clearAllMocks();
|
|
251
|
+
|
|
252
|
+
resizeGlRenderTarget(state, target, 128, 128);
|
|
253
|
+
|
|
254
|
+
expect(vi.mocked(gl.bindTexture)).toHaveBeenCalledWith(
|
|
255
|
+
(gl as unknown as { TEXTURE_2D: number }).TEXTURE_2D,
|
|
256
|
+
target.texture,
|
|
257
|
+
);
|
|
258
|
+
expect(vi.mocked(gl.texImage2D)).toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('resolveGlRenderTarget', () => {
|
|
263
|
+
it('is a no-op for a single-sample target', () => {
|
|
264
|
+
const { state, gl } = makeState();
|
|
265
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
266
|
+
expect(target.sampleCount).toBe(1);
|
|
267
|
+
vi.clearAllMocks();
|
|
268
|
+
|
|
269
|
+
resolveGlRenderTarget(state, target);
|
|
270
|
+
|
|
271
|
+
expect(vi.mocked(gl.bindFramebuffer)).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('is a no-op when the target has no resolve framebuffer', () => {
|
|
275
|
+
const { state, gl } = makeState();
|
|
276
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
277
|
+
// Force the MSAA sample-count gate open but leave resolveFramebuffer null.
|
|
278
|
+
target.sampleCount = 4;
|
|
279
|
+
target.resolveFramebuffer = null;
|
|
280
|
+
vi.clearAllMocks();
|
|
281
|
+
|
|
282
|
+
resolveGlRenderTarget(state, target);
|
|
283
|
+
|
|
284
|
+
expect(vi.mocked(gl.bindFramebuffer)).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('blits each color attachment and flushes for an MSAA target', () => {
|
|
288
|
+
const { state, gl } = makeState();
|
|
289
|
+
const target = createGlRenderTarget(state, { width: 64, height: 48 });
|
|
290
|
+
target.sampleCount = 4;
|
|
291
|
+
target.resolveFramebuffer = {} as WebGLFramebuffer;
|
|
292
|
+
// Two color attachments resolve as two separate blits.
|
|
293
|
+
target.textures = [{} as WebGLTexture, {} as WebGLTexture];
|
|
294
|
+
|
|
295
|
+
const blit = vi.fn();
|
|
296
|
+
const readBuffer = vi.fn();
|
|
297
|
+
const drawBuffers = vi.fn();
|
|
298
|
+
const flush = vi.fn();
|
|
299
|
+
Object.assign(gl as unknown as Record<string, unknown>, {
|
|
300
|
+
READ_FRAMEBUFFER: 36008,
|
|
301
|
+
DRAW_FRAMEBUFFER: 36009,
|
|
302
|
+
COLOR_BUFFER_BIT: 16384,
|
|
303
|
+
NEAREST: 9728,
|
|
304
|
+
NONE: 0,
|
|
305
|
+
blitFramebuffer: blit,
|
|
306
|
+
readBuffer,
|
|
307
|
+
drawBuffers,
|
|
308
|
+
flush,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
resolveGlRenderTarget(state, target);
|
|
312
|
+
|
|
313
|
+
expect(blit).toHaveBeenCalledTimes(2);
|
|
314
|
+
expect(readBuffer).toHaveBeenCalledTimes(2);
|
|
315
|
+
expect(flush).toHaveBeenCalledTimes(1);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
acquireGlRenderTarget,
|
|
3
|
+
createGlRenderTargetPool,
|
|
4
|
+
destroyGlRenderTargetPool,
|
|
5
|
+
releaseGlRenderTarget,
|
|
6
|
+
} from './glRenderTargetPool';
|
|
7
|
+
import { createGlState } from './glTestHelper';
|
|
8
|
+
|
|
9
|
+
describe('acquireGlRenderTarget', () => {
|
|
10
|
+
it('allocates a new target when the pool is empty', () => {
|
|
11
|
+
const { state } = createGlState();
|
|
12
|
+
const pool = createGlRenderTargetPool();
|
|
13
|
+
const target = acquireGlRenderTarget(state, pool, { width: 64, height: 48 });
|
|
14
|
+
expect(target.width).toBe(64);
|
|
15
|
+
expect(target.height).toBe(48);
|
|
16
|
+
expect(pool.free.length).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('reuses a matching released target instead of allocating', () => {
|
|
20
|
+
const { state } = createGlState();
|
|
21
|
+
const pool = createGlRenderTargetPool();
|
|
22
|
+
const first = acquireGlRenderTarget(state, pool, { width: 64, height: 48 });
|
|
23
|
+
releaseGlRenderTarget(pool, first);
|
|
24
|
+
|
|
25
|
+
const reused = acquireGlRenderTarget(state, pool, { width: 64, height: 48 });
|
|
26
|
+
expect(reused).toBe(first);
|
|
27
|
+
expect(pool.free.length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('allocates a new target when the free target has different dimensions', () => {
|
|
31
|
+
const { state } = createGlState();
|
|
32
|
+
const pool = createGlRenderTargetPool();
|
|
33
|
+
const first = acquireGlRenderTarget(state, pool, { width: 64, height: 48 });
|
|
34
|
+
releaseGlRenderTarget(pool, first);
|
|
35
|
+
|
|
36
|
+
const other = acquireGlRenderTarget(state, pool, { width: 128, height: 48 });
|
|
37
|
+
expect(other).not.toBe(first);
|
|
38
|
+
// The mismatched target stays parked in the free list.
|
|
39
|
+
expect(pool.free).toContain(first);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('matches the ceiled, clamped descriptor dimensions', () => {
|
|
43
|
+
const { state } = createGlState();
|
|
44
|
+
const pool = createGlRenderTargetPool();
|
|
45
|
+
const first = acquireGlRenderTarget(state, pool, { width: 10.2, height: 0 });
|
|
46
|
+
releaseGlRenderTarget(pool, first);
|
|
47
|
+
|
|
48
|
+
// 10.2 -> 11, 0 -> 1; an equivalent descriptor must hit the same parked target.
|
|
49
|
+
const reused = acquireGlRenderTarget(state, pool, { width: 10.9, height: 0.4 });
|
|
50
|
+
expect(reused).toBe(first);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('createGlRenderTargetPool', () => {
|
|
55
|
+
it('returns a pool with an empty free list', () => {
|
|
56
|
+
const pool = createGlRenderTargetPool();
|
|
57
|
+
expect(pool.free).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('destroyGlRenderTargetPool', () => {
|
|
62
|
+
it('destroys every parked target and empties the free list', () => {
|
|
63
|
+
const { state, gl } = createGlState();
|
|
64
|
+
const pool = createGlRenderTargetPool();
|
|
65
|
+
const a = acquireGlRenderTarget(state, pool, { width: 32, height: 32 });
|
|
66
|
+
const b = acquireGlRenderTarget(state, pool, { width: 16, height: 16 });
|
|
67
|
+
releaseGlRenderTarget(pool, a);
|
|
68
|
+
releaseGlRenderTarget(pool, b);
|
|
69
|
+
|
|
70
|
+
const deleteSpy = vi.spyOn(gl, 'deleteFramebuffer');
|
|
71
|
+
destroyGlRenderTargetPool(state, pool);
|
|
72
|
+
|
|
73
|
+
expect(deleteSpy).toHaveBeenCalledWith(a.framebuffer);
|
|
74
|
+
expect(deleteSpy).toHaveBeenCalledWith(b.framebuffer);
|
|
75
|
+
expect(pool.free.length).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('is a no-op on an empty pool', () => {
|
|
79
|
+
const { state } = createGlState();
|
|
80
|
+
const pool = createGlRenderTargetPool();
|
|
81
|
+
expect(() => destroyGlRenderTargetPool(state, pool)).not.toThrow();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('releaseGlRenderTarget', () => {
|
|
86
|
+
it('returns the target to the free list without destroying it', () => {
|
|
87
|
+
const { state, gl } = createGlState();
|
|
88
|
+
const pool = createGlRenderTargetPool();
|
|
89
|
+
const target = acquireGlRenderTarget(state, pool, { width: 64, height: 64 });
|
|
90
|
+
|
|
91
|
+
const deleteSpy = vi.spyOn(gl, 'deleteFramebuffer');
|
|
92
|
+
releaseGlRenderTarget(pool, target);
|
|
93
|
+
|
|
94
|
+
expect(pool.free).toContain(target);
|
|
95
|
+
expect(deleteSpy).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|