@flighthq/render-gl 0.2.0 → 0.2.1-next.410.a23f189
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/glCompressedTexture.d.ts +8 -0
- package/dist/glCompressedTexture.d.ts.map +1 -0
- package/dist/glCompressedTexture.js +270 -0
- package/dist/glCompressedTexture.js.map +1 -0
- package/dist/glDraw.d.ts +4 -2
- package/dist/glDraw.d.ts.map +1 -1
- package/dist/glDraw.js +215 -20
- package/dist/glDraw.js.map +1 -1
- package/dist/glFullscreenPass.d.ts.map +1 -1
- package/dist/glFullscreenPass.js +32 -0
- package/dist/glFullscreenPass.js.map +1 -1
- package/dist/glLinearToSrgbPass.d.ts +4 -0
- package/dist/glLinearToSrgbPass.d.ts.map +1 -0
- package/dist/glLinearToSrgbPass.js +42 -0
- package/dist/glLinearToSrgbPass.js.map +1 -0
- package/dist/glPresentRenderTarget.d.ts +3 -0
- package/dist/glPresentRenderTarget.d.ts.map +1 -0
- package/dist/glPresentRenderTarget.js +35 -0
- package/dist/glPresentRenderTarget.js.map +1 -0
- package/dist/glRenderPass.d.ts +5 -0
- package/dist/glRenderPass.d.ts.map +1 -0
- package/dist/glRenderPass.js +130 -0
- package/dist/glRenderPass.js.map +1 -0
- package/dist/glRenderState.d.ts +1 -1
- package/dist/glRenderState.d.ts.map +1 -1
- package/dist/glRenderState.js +1 -0
- package/dist/glRenderState.js.map +1 -1
- package/dist/glRenderTarget.d.ts +2 -15
- package/dist/glRenderTarget.d.ts.map +1 -1
- package/dist/glRenderTarget.js +48 -57
- package/dist/glRenderTarget.js.map +1 -1
- package/dist/glRenderTargetPool.d.ts.map +1 -1
- package/dist/glRenderTargetPool.js +4 -0
- package/dist/glRenderTargetPool.js.map +1 -1
- package/dist/glSkinPaletteTexture.d.ts +5 -0
- package/dist/glSkinPaletteTexture.d.ts.map +1 -0
- package/dist/glSkinPaletteTexture.js +42 -0
- package/dist/glSkinPaletteTexture.js.map +1 -0
- package/dist/glTestHelper.d.ts.map +1 -1
- package/dist/glTestHelper.js +1 -0
- package/dist/glTestHelper.js.map +1 -1
- package/dist/glTextureUpload.d.ts +5 -0
- package/dist/glTextureUpload.d.ts.map +1 -0
- package/dist/glTextureUpload.js +27 -0
- package/dist/glTextureUpload.js.map +1 -0
- package/dist/glTextureVideoUpload.d.ts +3 -0
- package/dist/glTextureVideoUpload.d.ts.map +1 -0
- package/dist/glTextureVideoUpload.js +24 -0
- package/dist/glTextureVideoUpload.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/glCompressedTexture.test.ts +276 -0
- package/src/glDraw.test.ts +319 -36
- package/src/glFullscreenPass.test.ts +22 -1
- package/src/glLinearToSrgbPass.test.ts +75 -0
- package/src/glPresentRenderTarget.test.ts +62 -0
- package/src/glReadback.test.ts +1 -0
- package/src/glRenderPass.test.ts +170 -0
- package/src/glRenderTarget.test.ts +63 -98
- package/src/glRenderTargetPool.test.ts +14 -0
- package/src/glSkinPaletteTexture.test.ts +129 -0
- package/src/glTextureUpload.test.ts +49 -0
- package/src/glTextureVideoUpload.test.ts +53 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { GlCompressedTextureSupport, ImageResource, TextureContainer } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
detectGlCompressedTextureSupport,
|
|
5
|
+
getGlCompressedTextureFormat,
|
|
6
|
+
hasGlCompressedTextureFormat,
|
|
7
|
+
registerGlCompressedTextureDecoder,
|
|
8
|
+
registerGlCompressedTextureUpload,
|
|
9
|
+
uploadGlCompressedTextureContainer,
|
|
10
|
+
} from './glCompressedTexture';
|
|
11
|
+
import { getGlRenderStateRuntime } from './glRenderState';
|
|
12
|
+
import { createGlState } from './glTestHelper';
|
|
13
|
+
|
|
14
|
+
// The subset of extension enum constants the tests exercise (BC3 via s3tc, ASTC 4x4).
|
|
15
|
+
const S3TC_EXT = { COMPRESSED_RGBA_S3TC_DXT5_EXT: 0x83f3 };
|
|
16
|
+
const ASTC_EXT = { COMPRESSED_RGBA_ASTC_4x4_KHR: 0x93b0 };
|
|
17
|
+
|
|
18
|
+
// A WebGL2 mock whose getExtension returns only the named extension objects; every other extension
|
|
19
|
+
// resolves to null so a format outside the supported set maps to -1.
|
|
20
|
+
function makeGl(available: Readonly<Record<string, Record<string, number>>>): WebGL2RenderingContext {
|
|
21
|
+
return {
|
|
22
|
+
RGBA: 0x1908,
|
|
23
|
+
UNSIGNED_BYTE: 0x1401,
|
|
24
|
+
TEXTURE_2D: 0x0de1,
|
|
25
|
+
TEXTURE_2D_ARRAY: 0x8c1a,
|
|
26
|
+
TEXTURE_CUBE_MAP_POSITIVE_X: 0x8515,
|
|
27
|
+
getExtension: vi.fn((name: string) => available[name] ?? null),
|
|
28
|
+
compressedTexImage2D: vi.fn(),
|
|
29
|
+
compressedTexSubImage3D: vi.fn(),
|
|
30
|
+
texStorage3D: vi.fn(),
|
|
31
|
+
texImage2D: vi.fn(),
|
|
32
|
+
} as unknown as WebGL2RenderingContext;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeContainer(): TextureContainer {
|
|
36
|
+
return {
|
|
37
|
+
format: 'bc3',
|
|
38
|
+
width: 4,
|
|
39
|
+
height: 4,
|
|
40
|
+
depth: 1,
|
|
41
|
+
mipLevels: 1,
|
|
42
|
+
layers: 1,
|
|
43
|
+
faces: 1,
|
|
44
|
+
supercompression: 'None',
|
|
45
|
+
levels: [{ byteOffset: 0, byteLength: 16, width: 4, height: 4 }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A compressed-only ImageResource wrapping makeContainer, for exercising the installed upload seam.
|
|
50
|
+
function uploadableCompressedImage(): ImageResource {
|
|
51
|
+
return {
|
|
52
|
+
source: null,
|
|
53
|
+
data: null,
|
|
54
|
+
compressed: { container: makeContainer(), payload: new Uint8Array(16) },
|
|
55
|
+
width: 4,
|
|
56
|
+
height: 4,
|
|
57
|
+
version: 1,
|
|
58
|
+
alphaType: 'straight',
|
|
59
|
+
} as unknown as ImageResource;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('detectGlCompressedTextureSupport', () => {
|
|
63
|
+
it('reports a family true only when its extension resolves', () => {
|
|
64
|
+
const gl = makeGl({ WEBGL_compressed_texture_astc: ASTC_EXT });
|
|
65
|
+
const support = detectGlCompressedTextureSupport(gl);
|
|
66
|
+
expect(support.astc).toBe(true);
|
|
67
|
+
expect(support.s3tc).toBe(false);
|
|
68
|
+
expect(support.bptc).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('requires both s3tc and s3tc_srgb for the s3tc flag', () => {
|
|
72
|
+
const onlyBase = detectGlCompressedTextureSupport(makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT }));
|
|
73
|
+
expect(onlyBase.s3tc).toBe(false);
|
|
74
|
+
const both = detectGlCompressedTextureSupport(
|
|
75
|
+
makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT, WEBGL_compressed_texture_s3tc_srgb: {} }),
|
|
76
|
+
);
|
|
77
|
+
expect(both.s3tc).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getGlCompressedTextureFormat', () => {
|
|
82
|
+
it('resolves the GL enum from the live extension object', () => {
|
|
83
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
84
|
+
expect(getGlCompressedTextureFormat(gl, 'bc3')).toBe(0x83f3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns -1 when the extension is absent', () => {
|
|
88
|
+
const gl = makeGl({});
|
|
89
|
+
expect(getGlCompressedTextureFormat(gl, 'bc3')).toBe(-1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns -1 for uncompressed and Basis-intermediate formats', () => {
|
|
93
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
94
|
+
expect(getGlCompressedTextureFormat(gl, 'rgba8unorm')).toBe(-1);
|
|
95
|
+
expect(getGlCompressedTextureFormat(gl, 'etc1s')).toBe(-1);
|
|
96
|
+
expect(getGlCompressedTextureFormat(gl, 'uastc')).toBe(-1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('hasGlCompressedTextureFormat', () => {
|
|
101
|
+
const support: GlCompressedTextureSupport = {
|
|
102
|
+
astc: true,
|
|
103
|
+
bptc: false,
|
|
104
|
+
etc: false,
|
|
105
|
+
pvrtc: false,
|
|
106
|
+
rgtc: false,
|
|
107
|
+
s3tc: true,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
it('maps a format to its family flag, including the srgb twin', () => {
|
|
111
|
+
expect(hasGlCompressedTextureFormat(support, 'bc3')).toBe(true);
|
|
112
|
+
expect(hasGlCompressedTextureFormat(support, 'bc3Srgb')).toBe(true);
|
|
113
|
+
expect(hasGlCompressedTextureFormat(support, 'astc8x8')).toBe(true);
|
|
114
|
+
expect(hasGlCompressedTextureFormat(support, 'bc7')).toBe(false);
|
|
115
|
+
expect(hasGlCompressedTextureFormat(support, 'etc2Rgb')).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('is false for a non-compressed format', () => {
|
|
119
|
+
expect(hasGlCompressedTextureFormat(support, 'rgba8unorm')).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('registerGlCompressedTextureDecoder', () => {
|
|
124
|
+
it('installs the decoder on the render-state runtime and clears it with null', () => {
|
|
125
|
+
const { state } = createGlState();
|
|
126
|
+
const decode = vi.fn(() => new Uint8ClampedArray(4));
|
|
127
|
+
registerGlCompressedTextureDecoder(state, decode);
|
|
128
|
+
expect(getGlRenderStateRuntime(state).compressedTextureDecoder).toBe(decode);
|
|
129
|
+
registerGlCompressedTextureDecoder(state, null);
|
|
130
|
+
expect(getGlRenderStateRuntime(state).compressedTextureDecoder).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('registerGlCompressedTextureUpload', () => {
|
|
135
|
+
it('installs the compressed upload seam on the render-state runtime and clears it with null', () => {
|
|
136
|
+
const { state } = createGlState();
|
|
137
|
+
registerGlCompressedTextureUpload(state);
|
|
138
|
+
expect(getGlRenderStateRuntime(state).compressedTextureUpload).toBeTypeOf('function');
|
|
139
|
+
registerGlCompressedTextureUpload(state, null);
|
|
140
|
+
expect(getGlRenderStateRuntime(state).compressedTextureUpload).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('uploads a compressed-only ImageResource once installed, threading the registered decoder', () => {
|
|
144
|
+
const { state, gl } = createGlState();
|
|
145
|
+
const rgba = new Uint8ClampedArray(4 * 4 * 4);
|
|
146
|
+
const decode = vi.fn(() => rgba);
|
|
147
|
+
registerGlCompressedTextureUpload(state);
|
|
148
|
+
registerGlCompressedTextureDecoder(state, decode);
|
|
149
|
+
const uploader = getGlRenderStateRuntime(state).compressedTextureUpload!;
|
|
150
|
+
const ok = uploader(gl, uploadableCompressedImage(), decode);
|
|
151
|
+
expect(ok).toBe(true);
|
|
152
|
+
expect(decode).toHaveBeenCalledWith('bc3', 4, 4, expect.any(Uint8Array));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('reports false for a resource with no compressed payload', () => {
|
|
156
|
+
const { state, gl } = createGlState();
|
|
157
|
+
registerGlCompressedTextureUpload(state);
|
|
158
|
+
const uploader = getGlRenderStateRuntime(state).compressedTextureUpload!;
|
|
159
|
+
const plain = { source: null, data: null, compressed: null } as unknown as ImageResource;
|
|
160
|
+
expect(uploader(gl, plain, null)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('uploadGlCompressedTextureContainer', () => {
|
|
165
|
+
it('drives compressedTexImage2D per level on the native path', () => {
|
|
166
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
167
|
+
const payload = new Uint8Array(16);
|
|
168
|
+
const ok = uploadGlCompressedTextureContainer(gl, makeContainer(), payload);
|
|
169
|
+
expect(ok).toBe(true);
|
|
170
|
+
expect(gl.compressedTexImage2D).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(gl.compressedTexImage2D).toHaveBeenCalledWith(gl.TEXTURE_2D, 0, 0x83f3, 4, 4, 0, expect.any(Uint8Array));
|
|
172
|
+
expect(gl.texImage2D).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('decodes to RGBA and uploads via texImage2D when native support is missing', () => {
|
|
176
|
+
const gl = makeGl({});
|
|
177
|
+
const payload = new Uint8Array(16);
|
|
178
|
+
const rgba = new Uint8ClampedArray(4 * 4 * 4);
|
|
179
|
+
const decode = vi.fn(() => rgba);
|
|
180
|
+
const ok = uploadGlCompressedTextureContainer(gl, makeContainer(), payload, decode);
|
|
181
|
+
expect(ok).toBe(true);
|
|
182
|
+
expect(decode).toHaveBeenCalledWith('bc3', 4, 4, expect.any(Uint8Array));
|
|
183
|
+
expect(gl.texImage2D).toHaveBeenCalledWith(gl.TEXTURE_2D, 0, gl.RGBA, 4, 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
|
|
184
|
+
expect(gl.compressedTexImage2D).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns false when neither native support nor a decoder is available', () => {
|
|
188
|
+
const gl = makeGl({});
|
|
189
|
+
expect(uploadGlCompressedTextureContainer(gl, makeContainer(), new Uint8Array(16))).toBe(false);
|
|
190
|
+
expect(gl.compressedTexImage2D).not.toHaveBeenCalled();
|
|
191
|
+
expect(gl.texImage2D).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns false when the decoder cannot handle the format', () => {
|
|
195
|
+
const gl = makeGl({});
|
|
196
|
+
const decode = vi.fn(() => null);
|
|
197
|
+
expect(uploadGlCompressedTextureContainer(gl, makeContainer(), new Uint8Array(16), decode)).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('slices each level from the payload at its byte range', () => {
|
|
201
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
202
|
+
const container: TextureContainer = {
|
|
203
|
+
...makeContainer(),
|
|
204
|
+
mipLevels: 2,
|
|
205
|
+
levels: [
|
|
206
|
+
{ byteOffset: 0, byteLength: 16, width: 4, height: 4 },
|
|
207
|
+
{ byteOffset: 16, byteLength: 8, width: 2, height: 2 },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
const payload = new Uint8Array(24);
|
|
211
|
+
uploadGlCompressedTextureContainer(gl, container, payload);
|
|
212
|
+
expect(gl.compressedTexImage2D).toHaveBeenCalledTimes(2);
|
|
213
|
+
const secondCall = (gl.compressedTexImage2D as ReturnType<typeof vi.fn>).mock.calls[1];
|
|
214
|
+
const view = secondCall[6] as Uint8Array;
|
|
215
|
+
expect(view.byteOffset).toBe(16);
|
|
216
|
+
expect(view.byteLength).toBe(8);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns false without uploading when the container is still supercompressed', () => {
|
|
220
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
221
|
+
const container: TextureContainer = { ...makeContainer(), supercompression: 'Zstd' };
|
|
222
|
+
expect(uploadGlCompressedTextureContainer(gl, container, new Uint8Array(16))).toBe(false);
|
|
223
|
+
expect(gl.compressedTexImage2D).not.toHaveBeenCalled();
|
|
224
|
+
expect(gl.texImage2D).not.toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('targets each cube face from the flat mip+face index', () => {
|
|
228
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
229
|
+
// A single-mip cubemap: six faces, flat index === face since mipLevels === 1.
|
|
230
|
+
const levels = Array.from({ length: 6 }, (_unused, face) => ({
|
|
231
|
+
byteOffset: face * 16,
|
|
232
|
+
byteLength: 16,
|
|
233
|
+
width: 4,
|
|
234
|
+
height: 4,
|
|
235
|
+
}));
|
|
236
|
+
const container: TextureContainer = { ...makeContainer(), faces: 6, levels };
|
|
237
|
+
uploadGlCompressedTextureContainer(gl, container, new Uint8Array(6 * 16));
|
|
238
|
+
expect(gl.compressedTexImage2D).toHaveBeenCalledTimes(6);
|
|
239
|
+
const calls = (gl.compressedTexImage2D as ReturnType<typeof vi.fn>).mock.calls;
|
|
240
|
+
for (let face = 0; face < 6; face += 1) {
|
|
241
|
+
expect(calls[face][0]).toBe(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face);
|
|
242
|
+
expect(calls[face][1]).toBe(0);
|
|
243
|
+
}
|
|
244
|
+
expect(gl.texStorage3D).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('allocates immutable storage once and sub-uploads each layer of an array into TEXTURE_2D_ARRAY', () => {
|
|
248
|
+
const gl = makeGl({ WEBGL_compressed_texture_s3tc: S3TC_EXT });
|
|
249
|
+
// A single-mip, two-layer array (layer-major): flat index 0 = layer 0, index 1 = layer 1.
|
|
250
|
+
const container: TextureContainer = {
|
|
251
|
+
...makeContainer(),
|
|
252
|
+
layers: 2,
|
|
253
|
+
levels: [
|
|
254
|
+
{ byteOffset: 0, byteLength: 16, width: 4, height: 4 },
|
|
255
|
+
{ byteOffset: 16, byteLength: 16, width: 4, height: 4 },
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
uploadGlCompressedTextureContainer(gl, container, new Uint8Array(32));
|
|
259
|
+
expect(gl.texStorage3D).toHaveBeenCalledTimes(1);
|
|
260
|
+
expect(gl.texStorage3D).toHaveBeenCalledWith(gl.TEXTURE_2D_ARRAY, 1, 0x83f3, 4, 4, 2);
|
|
261
|
+
expect(gl.compressedTexSubImage3D).toHaveBeenCalledTimes(2);
|
|
262
|
+
const subCalls = (gl.compressedTexSubImage3D as ReturnType<typeof vi.fn>).mock.calls;
|
|
263
|
+
expect(subCalls[0][4]).toBe(0);
|
|
264
|
+
expect(subCalls[1][4]).toBe(1);
|
|
265
|
+
expect(gl.compressedTexImage2D).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns false for a cubemap the device cannot upload natively (no face-by-face decode)', () => {
|
|
269
|
+
const gl = makeGl({});
|
|
270
|
+
const container: TextureContainer = { ...makeContainer(), faces: 6 };
|
|
271
|
+
const decode = vi.fn(() => new Uint8ClampedArray(4 * 4 * 4));
|
|
272
|
+
expect(uploadGlCompressedTextureContainer(gl, container, new Uint8Array(6 * 16), decode)).toBe(false);
|
|
273
|
+
expect(decode).not.toHaveBeenCalled();
|
|
274
|
+
expect(gl.texImage2D).not.toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
});
|