@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import { createTexture } from '@flighthq/texture/contract';
|
|
3
|
+
import type { Image } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import { areGlTextureResolverGuardsEnabled, enableGlTextureResolverGuards } from './enableGlTextureResolverGuards';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
import { registerGlTextureResolver, resolveGlTexture } from './glTextureResolver';
|
|
8
|
+
|
|
9
|
+
describe('areGlTextureResolverGuardsEnabled', () => {
|
|
10
|
+
it('reports whether diagnostics were installed for the state', () => {
|
|
11
|
+
const { state } = createGlState();
|
|
12
|
+
expect(areGlTextureResolverGuardsEnabled(state)).toBe(false);
|
|
13
|
+
|
|
14
|
+
enableGlTextureResolverGuards(state);
|
|
15
|
+
expect(areGlTextureResolverGuardsEnabled(state)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('enableGlTextureResolverGuards', () => {
|
|
20
|
+
it('is idempotent', () => {
|
|
21
|
+
const { state } = createGlState();
|
|
22
|
+
enableGlTextureResolverGuards(state);
|
|
23
|
+
enableGlTextureResolverGuards(state);
|
|
24
|
+
expect(areGlTextureResolverGuardsEnabled(state)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('warns once for a missing resolver and stays silent for a registered resolver returning null', () => {
|
|
28
|
+
const { state } = createGlState();
|
|
29
|
+
const missing = textureWithKind('acme.missing.gl');
|
|
30
|
+
const registered = textureWithKind('acme.registered.gl');
|
|
31
|
+
const sink = createMemoryLogSink(4);
|
|
32
|
+
addLogSink(sink.sink);
|
|
33
|
+
enableGlTextureResolverGuards(state);
|
|
34
|
+
registerGlTextureResolver(state, 'acme.registered.gl', () => null);
|
|
35
|
+
try {
|
|
36
|
+
expect(resolveGlTexture(state, missing)).toBeNull();
|
|
37
|
+
expect(resolveGlTexture(state, missing)).toBeNull();
|
|
38
|
+
expect(resolveGlTexture(state, registered)).toBeNull();
|
|
39
|
+
|
|
40
|
+
const entries = getMemoryLogSinkEntries(sink);
|
|
41
|
+
expect(entries).toHaveLength(1);
|
|
42
|
+
expect(entries[0]?.data).toMatchObject({
|
|
43
|
+
kind: 'acme.missing.gl',
|
|
44
|
+
message:
|
|
45
|
+
'resolveGlTexture: texture source kind has no registered resolver — call registerGlTextureResolver(state, sourceKind, resolver), or copyGlRenderStateRegistrations(offscreenState, screenState) after a late screen registration',
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
removeLogSink(sink.sink);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function textureWithKind(kind: string) {
|
|
54
|
+
return createTexture({
|
|
55
|
+
dimension: '2d',
|
|
56
|
+
source: { kind } as Image,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import { createTexture } from '@flighthq/texture/contract';
|
|
3
|
+
import type { TextureSource } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import { explainGlTextureResolution } from './explainGlTextureResolution';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
import { registerGlTextureResolver } from './glTextureResolver';
|
|
8
|
+
|
|
9
|
+
describe('explainGlTextureResolution', () => {
|
|
10
|
+
it('distinguishes missing kinds, missing resolvers, and registered resolvers', () => {
|
|
11
|
+
const { state } = createGlState();
|
|
12
|
+
const texture = createTexture({
|
|
13
|
+
dimension: '2d',
|
|
14
|
+
source: createEntity({ height: 1, kind: 'acme.test', version: 0, width: 1 }) as TextureSource,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(explainGlTextureResolution(state, createTexture())).toEqual({
|
|
18
|
+
kind: null,
|
|
19
|
+
status: 'missing-kind',
|
|
20
|
+
});
|
|
21
|
+
expect(explainGlTextureResolution(state, texture)).toEqual({
|
|
22
|
+
kind: 'acme.test',
|
|
23
|
+
status: 'missing-resolver',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
registerGlTextureResolver(state, 'acme.test', () => null);
|
|
27
|
+
expect(explainGlTextureResolution(state, texture)).toEqual({
|
|
28
|
+
kind: 'acme.test',
|
|
29
|
+
status: 'registered',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompressedImage, GlCompressedTextureSupport, TextureContainer } from '@flighthq/types/contract';
|
|
2
|
-
import {
|
|
2
|
+
import { CompressedImageTextureSourceKind } from '@flighthq/types/contract';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
detectGlCompressedTextureSupport,
|
|
@@ -51,7 +51,7 @@ function uploadableCompressedImage(containerOverrides?: Partial<TextureContainer
|
|
|
51
51
|
return {
|
|
52
52
|
compressed: { container: { ...makeContainer(), ...containerOverrides }, payload: new Uint8Array(16) },
|
|
53
53
|
height: 4,
|
|
54
|
-
kind:
|
|
54
|
+
kind: CompressedImageTextureSourceKind,
|
|
55
55
|
version: 1,
|
|
56
56
|
width: 4,
|
|
57
57
|
} as unknown as CompressedImage;
|
package/src/glDraw.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getTextureSource } from '@flighthq/texture/contract';
|
|
2
|
+
import type { Bitmap, CompressedImage, Image, SamplerLike, Texture } from '@flighthq/types/contract';
|
|
2
3
|
import {
|
|
3
4
|
AdvancedBlendMode,
|
|
4
|
-
|
|
5
|
+
BitmapTextureSourceKind,
|
|
5
6
|
BlendMode,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
CompressedImageTextureSourceKind,
|
|
8
|
+
ImageTextureSourceKind,
|
|
8
9
|
} from '@flighthq/types/contract';
|
|
9
10
|
|
|
10
11
|
import { registerGlCompressedTextureDecoder, registerGlCompressedTextureUpload } from './glCompressedTexture';
|
|
@@ -50,7 +51,7 @@ function compressedBc3Image(): CompressedImage {
|
|
|
50
51
|
width: 4,
|
|
51
52
|
height: 4,
|
|
52
53
|
version: 1,
|
|
53
|
-
kind:
|
|
54
|
+
kind: CompressedImageTextureSourceKind,
|
|
54
55
|
} as unknown as CompressedImage;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -61,7 +62,7 @@ function bitmap(size: number, version: number): Bitmap {
|
|
|
61
62
|
data: new Uint8ClampedArray(size * size * 4),
|
|
62
63
|
format: 'rgba8unorm',
|
|
63
64
|
height: size,
|
|
64
|
-
kind:
|
|
65
|
+
kind: BitmapTextureSourceKind,
|
|
65
66
|
version,
|
|
66
67
|
width: size,
|
|
67
68
|
} as unknown as Bitmap;
|
|
@@ -291,9 +292,9 @@ describe('bindGlImageResourceTexture', () => {
|
|
|
291
292
|
source: document.createElement('img'),
|
|
292
293
|
width: 1,
|
|
293
294
|
height: 1,
|
|
294
|
-
kind:
|
|
295
|
+
kind: ImageTextureSourceKind,
|
|
295
296
|
version: 1,
|
|
296
|
-
} as unknown as
|
|
297
|
+
} as unknown as Image;
|
|
297
298
|
bindGlImageResourceTexture(state, image);
|
|
298
299
|
expect(gl.texImage2D).toHaveBeenCalled();
|
|
299
300
|
});
|
|
@@ -516,18 +517,16 @@ describe('bindGlVideoTexture', () => {
|
|
|
516
517
|
function videoTexture(version: number, readyState = 4, videoWidth = 320, videoHeight = 240): Texture {
|
|
517
518
|
return {
|
|
518
519
|
sampler: makeSampler(),
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
version,
|
|
524
|
-
},
|
|
520
|
+
dimension: '2d',
|
|
521
|
+
source: {
|
|
522
|
+
source: { readyState, videoWidth, videoHeight } as unknown as HTMLVideoElement,
|
|
523
|
+
version,
|
|
525
524
|
},
|
|
526
525
|
version,
|
|
527
526
|
} as unknown as Texture;
|
|
528
527
|
}
|
|
529
528
|
|
|
530
|
-
it('creates a texture, uploads the current frame, and caches by
|
|
529
|
+
it('creates a texture, uploads the current frame, and caches by source identity', () => {
|
|
531
530
|
const { state, gl } = createGlState();
|
|
532
531
|
const vt = videoTexture(3);
|
|
533
532
|
const t1 = bindGlVideoTexture(state, vt);
|
|
@@ -538,20 +537,20 @@ describe('bindGlVideoTexture', () => {
|
|
|
538
537
|
gl.RGBA,
|
|
539
538
|
gl.RGBA,
|
|
540
539
|
gl.UNSIGNED_BYTE,
|
|
541
|
-
(vt
|
|
540
|
+
(getTextureSource(vt) as Image).source,
|
|
542
541
|
);
|
|
543
542
|
const t2 = bindGlVideoTexture(state, vt);
|
|
544
543
|
expect(t2).toBe(t1);
|
|
545
544
|
});
|
|
546
545
|
|
|
547
|
-
it('re-uploads only when the
|
|
546
|
+
it('re-uploads only when the source version advances (the dirty-gate)', () => {
|
|
548
547
|
const { state, gl } = createGlState();
|
|
549
548
|
const vt = videoTexture(1);
|
|
550
549
|
bindGlVideoTexture(state, vt);
|
|
551
550
|
const uploads = (gl.texImage2D as ReturnType<typeof vi.fn>).mock.calls.length;
|
|
552
551
|
bindGlVideoTexture(state, vt);
|
|
553
552
|
expect((gl.texImage2D as ReturnType<typeof vi.fn>).mock.calls.length).toBe(uploads);
|
|
554
|
-
vt
|
|
553
|
+
getTextureSource(vt)!.version = 2;
|
|
555
554
|
bindGlVideoTexture(state, vt);
|
|
556
555
|
expect((gl.texImage2D as ReturnType<typeof vi.fn>).mock.calls.length).toBe(uploads + 1);
|
|
557
556
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getTextureSourceKind } from '@flighthq/texture/contract';
|
|
2
|
+
import { ExternalTextureSourceKind } from '@flighthq/types/contract';
|
|
3
3
|
|
|
4
4
|
import { createExternalGlTexture, disposeExternalGlTexture } from './glExternalTexture';
|
|
5
5
|
import { createGlState } from './glTestHelper';
|
|
@@ -12,7 +12,7 @@ describe('createExternalGlTexture', () => {
|
|
|
12
12
|
const deleteTexture = vi.spyOn(state.gl, 'deleteTexture');
|
|
13
13
|
const texture = createExternalGlTexture(state, handle, { height: 16, width: 32 });
|
|
14
14
|
|
|
15
|
-
expect(
|
|
15
|
+
expect(getTextureSourceKind(texture)).toBe(ExternalTextureSourceKind);
|
|
16
16
|
expect(resolveGlTexture(state, texture)).toBe(handle);
|
|
17
17
|
expect(disposeExternalGlTexture(state, texture)).toBe(true);
|
|
18
18
|
expect(resolveGlTexture(state, texture)).toBeNull();
|
|
@@ -16,9 +16,22 @@ void main() {
|
|
|
16
16
|
|
|
17
17
|
function makeTarget(framebuffer: WebGLFramebuffer, width = 32, height = 16): GlRenderTarget {
|
|
18
18
|
return {
|
|
19
|
+
requestedAxes: {
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
format: 'rgba8',
|
|
23
|
+
colorAttachments: 1,
|
|
24
|
+
colorFormats: ['rgba8'],
|
|
25
|
+
sampleCount: 1,
|
|
26
|
+
depth: 'none',
|
|
27
|
+
colorSpace: 'srgb',
|
|
28
|
+
},
|
|
19
29
|
width,
|
|
20
30
|
height,
|
|
21
31
|
format: 'rgba8',
|
|
32
|
+
colorAttachments: 1,
|
|
33
|
+
colorFormats: ['rgba8'],
|
|
34
|
+
depth: 'none',
|
|
22
35
|
colorSpace: 'srgb',
|
|
23
36
|
clearColors: [],
|
|
24
37
|
clearDepth: 1,
|
|
@@ -5,9 +5,22 @@ import { createGlState } from './glTestHelper';
|
|
|
5
5
|
|
|
6
6
|
function makeTarget(framebuffer: WebGLFramebuffer, texture: WebGLTexture, width = 32, height = 16): GlRenderTarget {
|
|
7
7
|
return {
|
|
8
|
+
requestedAxes: {
|
|
9
|
+
width,
|
|
10
|
+
height,
|
|
11
|
+
format: 'rgba16f',
|
|
12
|
+
colorAttachments: 1,
|
|
13
|
+
colorFormats: ['rgba16f'],
|
|
14
|
+
sampleCount: 1,
|
|
15
|
+
depth: 'none',
|
|
16
|
+
colorSpace: 'linear',
|
|
17
|
+
},
|
|
8
18
|
width,
|
|
9
19
|
height,
|
|
10
20
|
format: 'rgba16f',
|
|
21
|
+
colorAttachments: 1,
|
|
22
|
+
colorFormats: ['rgba16f'],
|
|
23
|
+
depth: 'none',
|
|
11
24
|
colorSpace: 'linear',
|
|
12
25
|
clearColors: [],
|
|
13
26
|
clearDepth: 1,
|
|
@@ -4,10 +4,24 @@ import { presentGlRenderTarget } from './glPresentRenderTarget';
|
|
|
4
4
|
import { createGlState } from './glTestHelper';
|
|
5
5
|
|
|
6
6
|
function makeTarget(colorSpace: 'linear' | 'srgb', texture: WebGLTexture): GlRenderTarget {
|
|
7
|
+
const format = colorSpace === 'linear' ? 'rgba16f' : 'rgba8';
|
|
7
8
|
return {
|
|
9
|
+
requestedAxes: {
|
|
10
|
+
width: 32,
|
|
11
|
+
height: 16,
|
|
12
|
+
format,
|
|
13
|
+
colorAttachments: 1,
|
|
14
|
+
colorFormats: [format],
|
|
15
|
+
sampleCount: 1,
|
|
16
|
+
depth: 'none',
|
|
17
|
+
colorSpace,
|
|
18
|
+
},
|
|
8
19
|
width: 32,
|
|
9
20
|
height: 16,
|
|
10
|
-
format
|
|
21
|
+
format,
|
|
22
|
+
colorAttachments: 1,
|
|
23
|
+
colorFormats: [format],
|
|
24
|
+
depth: 'none',
|
|
11
25
|
colorSpace,
|
|
12
26
|
clearColors: [],
|
|
13
27
|
clearDepth: 1,
|
package/src/glReadback.test.ts
CHANGED
|
@@ -5,9 +5,22 @@ import { createGlState } from './glTestHelper';
|
|
|
5
5
|
|
|
6
6
|
function makeTarget(overrides?: Partial<GlRenderTarget>): GlRenderTarget {
|
|
7
7
|
return {
|
|
8
|
+
requestedAxes: {
|
|
9
|
+
width: 4,
|
|
10
|
+
height: 4,
|
|
11
|
+
format: 'rgba8',
|
|
12
|
+
colorAttachments: 1,
|
|
13
|
+
colorFormats: ['rgba8'],
|
|
14
|
+
sampleCount: 1,
|
|
15
|
+
depth: 'none',
|
|
16
|
+
colorSpace: 'srgb',
|
|
17
|
+
},
|
|
8
18
|
width: 4,
|
|
9
19
|
height: 4,
|
|
10
20
|
format: 'rgba8',
|
|
21
|
+
colorAttachments: 1,
|
|
22
|
+
colorFormats: ['rgba8'],
|
|
23
|
+
depth: 'none',
|
|
11
24
|
colorSpace: 'srgb',
|
|
12
25
|
sampleCount: 1,
|
|
13
26
|
framebuffer: {} as WebGLFramebuffer,
|
package/src/glRenderPass.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render/contract';
|
|
3
|
+
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
2
4
|
import type { GlRenderTarget, Viewport } from '@flighthq/types/contract';
|
|
3
5
|
|
|
4
6
|
import { beginGlRenderPass, endGlRenderPass, setGlRenderTransform2D } from './glRenderPass';
|
|
@@ -8,9 +10,22 @@ import { createGlState } from './glTestHelper';
|
|
|
8
10
|
function makeTarget(overrides?: Partial<GlRenderTarget>): GlRenderTarget {
|
|
9
11
|
const texture = { id: 'c0' } as unknown as WebGLTexture;
|
|
10
12
|
return {
|
|
13
|
+
requestedAxes: {
|
|
14
|
+
width: 32,
|
|
15
|
+
height: 16,
|
|
16
|
+
format: 'rgba8',
|
|
17
|
+
colorAttachments: 1,
|
|
18
|
+
colorFormats: ['rgba8'],
|
|
19
|
+
sampleCount: 1,
|
|
20
|
+
depth: 'depth-stencil',
|
|
21
|
+
colorSpace: 'srgb',
|
|
22
|
+
},
|
|
11
23
|
width: 32,
|
|
12
24
|
height: 16,
|
|
13
25
|
format: 'rgba8',
|
|
26
|
+
colorAttachments: 1,
|
|
27
|
+
colorFormats: ['rgba8'],
|
|
28
|
+
depth: 'depth-stencil',
|
|
14
29
|
colorSpace: 'srgb',
|
|
15
30
|
clearColors: [],
|
|
16
31
|
clearDepth: 1,
|
|
@@ -161,6 +176,44 @@ describe('beginGlRenderPass', () => {
|
|
|
161
176
|
expect(runtime.currentRenderTarget).toBe(outer);
|
|
162
177
|
expect(runtime.renderTargetViewport).toEqual({ height: 48, width: 64, x: 0, y: 0 });
|
|
163
178
|
});
|
|
179
|
+
|
|
180
|
+
it('shares pass ownership between render states using the same context', () => {
|
|
181
|
+
const outerFixture = createGlState();
|
|
182
|
+
const innerFixture = createGlState();
|
|
183
|
+
const { gl, state: outerState } = outerFixture;
|
|
184
|
+
const innerState = innerFixture.state;
|
|
185
|
+
(innerState as { gl: WebGL2RenderingContext }).gl = gl;
|
|
186
|
+
const outer = makeTarget({ width: 64, height: 48 });
|
|
187
|
+
const inner = makeTarget({ width: 32, height: 24 });
|
|
188
|
+
|
|
189
|
+
beginGlRenderPass(outerState, outer);
|
|
190
|
+
beginGlRenderPass(innerState, inner);
|
|
191
|
+
endGlRenderPass(innerState);
|
|
192
|
+
|
|
193
|
+
expect(vi.mocked(gl.bindFramebuffer).mock.calls.at(-1)?.[1]).toBe(outer.framebuffer);
|
|
194
|
+
expect(getGlRenderStateRuntime(outerState).currentRenderTarget).toBe(outer);
|
|
195
|
+
expect(getGlRenderStateRuntime(innerState).currentRenderTarget).toBeNull();
|
|
196
|
+
endGlRenderPass(outerState);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('rejects a same-target contour pass across render states sharing one context', () => {
|
|
200
|
+
const outerFixture = createGlState();
|
|
201
|
+
const innerFixture = createGlState();
|
|
202
|
+
const { gl, state: outerState } = outerFixture;
|
|
203
|
+
const innerState = innerFixture.state;
|
|
204
|
+
(innerState as { gl: WebGL2RenderingContext }).gl = gl;
|
|
205
|
+
const target = makeTarget({ width: 64, height: 48 });
|
|
206
|
+
|
|
207
|
+
beginGlRenderPass(outerState, target);
|
|
208
|
+
getGlRenderStateRuntime(outerState).currentMaskDepth = 1;
|
|
209
|
+
|
|
210
|
+
expect(() => beginGlRenderPass(innerState, target)).toThrow(
|
|
211
|
+
'cannot nest the active framebuffer while a contour clip is live',
|
|
212
|
+
);
|
|
213
|
+
expect(getGlRenderStateRuntime(outerState).currentRenderTarget).toBe(target);
|
|
214
|
+
expect(getGlRenderStateRuntime(innerState).currentRenderTarget).toBeNull();
|
|
215
|
+
endGlRenderPass(outerState);
|
|
216
|
+
});
|
|
164
217
|
});
|
|
165
218
|
|
|
166
219
|
describe('endGlRenderPass', () => {
|
|
@@ -306,4 +359,14 @@ describe('setGlRenderTransform2D', () => {
|
|
|
306
359
|
|
|
307
360
|
expect(state.renderTransform2D).toBe(original);
|
|
308
361
|
});
|
|
362
|
+
|
|
363
|
+
it('dirties existing state-local proxy transforms for repeated offscreen captures', () => {
|
|
364
|
+
const { state } = createGlState();
|
|
365
|
+
const proxy = getOrCreateRenderProxy2D(state, createDisplayObject());
|
|
366
|
+
proxy.lastLocalTransformId = 7;
|
|
367
|
+
|
|
368
|
+
setGlRenderTransform2D(state, createMatrix());
|
|
369
|
+
|
|
370
|
+
expect(proxy.lastLocalTransformId).toBe(-1);
|
|
371
|
+
});
|
|
309
372
|
});
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import {
|
|
3
|
+
copyAllRenderersFromRenderState,
|
|
4
|
+
getRenderStateRuntime,
|
|
5
|
+
prepareScene2DRender,
|
|
6
|
+
registerRenderer,
|
|
7
|
+
} from '@flighthq/render/contract';
|
|
8
|
+
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
1
9
|
import { BlendMode } from '@flighthq/types/contract';
|
|
2
10
|
|
|
11
|
+
import { enableGlRenderStateGuards } from './enableGlRenderStateGuards';
|
|
12
|
+
import { registerGlMaterialRenderer } from './glMaterialRegistry';
|
|
3
13
|
import {
|
|
14
|
+
copyGlRenderStateRegistrations,
|
|
15
|
+
createGlOffscreenRenderState,
|
|
4
16
|
createGlRenderState,
|
|
5
17
|
createGlRenderStateRuntime,
|
|
6
18
|
destroyGlRenderState,
|
|
@@ -8,6 +20,7 @@ import {
|
|
|
8
20
|
invalidateGlRenderStateCache,
|
|
9
21
|
} from './glRenderState';
|
|
10
22
|
import { makeGL } from './glTestHelper';
|
|
23
|
+
import { registerGlTextureResolver } from './glTextureResolver';
|
|
11
24
|
|
|
12
25
|
function makeCanvas() {
|
|
13
26
|
const canvas = document.createElement('canvas');
|
|
@@ -18,6 +31,108 @@ function makeCanvas() {
|
|
|
18
31
|
return { canvas, gl };
|
|
19
32
|
}
|
|
20
33
|
|
|
34
|
+
describe('copyGlRenderStateRegistrations', () => {
|
|
35
|
+
it('copies late GL registrations only when explicitly requested', () => {
|
|
36
|
+
const { canvas } = makeCanvas();
|
|
37
|
+
const screen = createGlRenderState(canvas);
|
|
38
|
+
const offscreen = createGlOffscreenRenderState(screen);
|
|
39
|
+
const resolver = vi.fn(() => null);
|
|
40
|
+
registerGlTextureResolver(screen, 'acme.LateTexture', resolver);
|
|
41
|
+
|
|
42
|
+
expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
|
|
43
|
+
copyGlRenderStateRegistrations(offscreen, screen);
|
|
44
|
+
expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('createGlOffscreenRenderState', () => {
|
|
49
|
+
it('shares context resources while snapshotting registration policy into independent maps', () => {
|
|
50
|
+
const { canvas } = makeCanvas();
|
|
51
|
+
const screen = createGlRenderState(canvas);
|
|
52
|
+
const renderer = { createData: () => null, submit: vi.fn() };
|
|
53
|
+
const materialRenderer = { getBatchData: vi.fn(), getBatchFloats: vi.fn() } as never;
|
|
54
|
+
const paddingResolver = vi.fn(() => ({ bottom: 1, left: 1, right: 1, top: 1 }));
|
|
55
|
+
const textureResolver = vi.fn(() => null);
|
|
56
|
+
registerRenderer(screen, 'acme.Node', renderer);
|
|
57
|
+
registerGlMaterialRenderer(screen, 'acme.Material', materialRenderer);
|
|
58
|
+
registerGlTextureResolver(screen, 'acme.Texture', textureResolver);
|
|
59
|
+
getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.Effect', paddingResolver]]);
|
|
60
|
+
getGlRenderStateRuntime(screen).glRenderTextureCache = new WeakMap();
|
|
61
|
+
|
|
62
|
+
const offscreen = createGlOffscreenRenderState(screen);
|
|
63
|
+
const screenRuntime = getGlRenderStateRuntime(screen);
|
|
64
|
+
const offscreenRuntime = getGlRenderStateRuntime(offscreen);
|
|
65
|
+
|
|
66
|
+
expect(offscreen.gl).toBe(screen.gl);
|
|
67
|
+
expect(offscreen.canvas).toBe(screen.canvas);
|
|
68
|
+
expect(offscreenRuntime.textureCache).toBe(screenRuntime.textureCache);
|
|
69
|
+
expect(offscreenRuntime.textureSourcePremultipliedTextureCache).toBe(
|
|
70
|
+
screenRuntime.textureSourcePremultipliedTextureCache,
|
|
71
|
+
);
|
|
72
|
+
expect(offscreenRuntime.glRenderTextureCache).toBe(screenRuntime.glRenderTextureCache);
|
|
73
|
+
expect(offscreenRuntime.quadIndexBuffer).toBe(screenRuntime.quadIndexBuffer);
|
|
74
|
+
expect(offscreenRuntime.rendererMap).not.toBe(screenRuntime.rendererMap);
|
|
75
|
+
expect(offscreenRuntime.materialRendererMap).not.toBe(screenRuntime.materialRendererMap);
|
|
76
|
+
expect(offscreenRuntime.glTextureResolverRegistry).not.toBe(screenRuntime.glTextureResolverRegistry);
|
|
77
|
+
expect(offscreenRuntime.renderEffectPaddingResolverRegistry).not.toBe(
|
|
78
|
+
screenRuntime.renderEffectPaddingResolverRegistry,
|
|
79
|
+
);
|
|
80
|
+
expect(offscreenRuntime.rendererMap.get('acme.Node')).toBe(renderer);
|
|
81
|
+
expect(offscreenRuntime.materialRendererMap?.get('acme.Material')).toBe(materialRenderer);
|
|
82
|
+
expect(offscreenRuntime.glTextureResolverRegistry?.get('acme.Texture')).toBe(textureResolver);
|
|
83
|
+
expect(offscreenRuntime.renderEffectPaddingResolverRegistry?.get('acme.Effect')).toBe(paddingResolver);
|
|
84
|
+
|
|
85
|
+
screenRuntime.currentProgram = {} as WebGLProgram;
|
|
86
|
+
expect(offscreenRuntime.currentProgram).toBe(screenRuntime.currentProgram);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('requires explicit re-copy for registrations added after derivation', () => {
|
|
90
|
+
const { canvas } = makeCanvas();
|
|
91
|
+
const screen = createGlRenderState(canvas);
|
|
92
|
+
const offscreen = createGlOffscreenRenderState(screen);
|
|
93
|
+
const renderer = { createData: () => null, submit: vi.fn() };
|
|
94
|
+
const paddingResolver = vi.fn(() => ({ bottom: 2, left: 2, right: 2, top: 2 }));
|
|
95
|
+
const resolver = vi.fn(() => null);
|
|
96
|
+
registerRenderer(screen, 'acme.LateNode', renderer);
|
|
97
|
+
registerGlTextureResolver(screen, 'acme.LateTexture', resolver);
|
|
98
|
+
getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.LateEffect', paddingResolver]]);
|
|
99
|
+
|
|
100
|
+
expect(getRenderStateRuntime(offscreen).rendererMap.has('acme.LateNode')).toBe(false);
|
|
101
|
+
expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
|
|
102
|
+
expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.has('acme.LateEffect')).not.toBe(true);
|
|
103
|
+
|
|
104
|
+
copyAllRenderersFromRenderState(offscreen, screen);
|
|
105
|
+
copyGlRenderStateRegistrations(offscreen, screen);
|
|
106
|
+
expect(getRenderStateRuntime(offscreen).rendererMap.get('acme.LateNode')).toBe(renderer);
|
|
107
|
+
expect(getGlRenderStateRuntime(offscreen).glTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
|
|
108
|
+
expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.get('acme.LateEffect')).toBe(
|
|
109
|
+
paddingResolver,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('keeps proxy trees independent and destroys derived renderer data without freeing shared context resources', () => {
|
|
114
|
+
const { canvas, gl } = makeCanvas();
|
|
115
|
+
const screen = createGlRenderState(canvas);
|
|
116
|
+
const destroyData = vi.fn();
|
|
117
|
+
const root = createDisplayObject();
|
|
118
|
+
registerRenderer(screen, root.kind, {
|
|
119
|
+
createData: () => createEntity({}),
|
|
120
|
+
destroyData,
|
|
121
|
+
submit: vi.fn(),
|
|
122
|
+
});
|
|
123
|
+
const offscreen = createGlOffscreenRenderState(screen);
|
|
124
|
+
prepareScene2DRender(offscreen, root);
|
|
125
|
+
|
|
126
|
+
expect(getRenderStateRuntime(offscreen).renderProxyMap).not.toBe(getRenderStateRuntime(screen).renderProxyMap);
|
|
127
|
+
destroyGlRenderState(offscreen);
|
|
128
|
+
expect(destroyData).toHaveBeenCalledOnce();
|
|
129
|
+
expect(gl.deleteProgram).not.toHaveBeenCalled();
|
|
130
|
+
|
|
131
|
+
destroyGlRenderState(screen);
|
|
132
|
+
expect(gl.deleteProgram).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
21
136
|
describe('createGlRenderState', () => {
|
|
22
137
|
it('throws when Gl2 context is unavailable', () => {
|
|
23
138
|
const canvas = document.createElement('canvas');
|
|
@@ -141,6 +256,25 @@ describe('destroyGlRenderState', () => {
|
|
|
141
256
|
});
|
|
142
257
|
});
|
|
143
258
|
|
|
259
|
+
describe('enableGlRenderStateGuards', () => {
|
|
260
|
+
it('warns when one GL pipeline prepares two different roots', () => {
|
|
261
|
+
const { canvas } = makeCanvas();
|
|
262
|
+
const state = createGlRenderState(canvas);
|
|
263
|
+
const sink = createMemoryLogSink(4);
|
|
264
|
+
addLogSink(sink.sink);
|
|
265
|
+
enableGlRenderStateGuards(state);
|
|
266
|
+
try {
|
|
267
|
+
prepareScene2DRender(state, createDisplayObject());
|
|
268
|
+
prepareScene2DRender(state, createDisplayObject());
|
|
269
|
+
const entries = getMemoryLogSinkEntries(sink);
|
|
270
|
+
expect(entries).toHaveLength(1);
|
|
271
|
+
expect((entries[0].data as { message: string }).message).toContain('createGlOffscreenRenderState');
|
|
272
|
+
} finally {
|
|
273
|
+
removeLogSink(sink.sink);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
144
278
|
describe('getGlRenderStateRuntime', () => {
|
|
145
279
|
it('returns the runtime attached by createGlRenderState', () => {
|
|
146
280
|
const { canvas } = makeCanvas();
|
|
@@ -184,3 +318,4 @@ describe('invalidateGlRenderStateCache', () => {
|
|
|
184
318
|
expect(runtime.renderTargetViewport).toBeNull();
|
|
185
319
|
});
|
|
186
320
|
});
|
|
321
|
+
import { createEntity } from '@flighthq/entity/contract';
|