@flighthq/render-wgpu 0.3.0-next.906.07cea63 → 0.3.1-next.1041.35b950c
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 +7 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +7 -1
- package/dist/contract.js.map +1 -1
- package/dist/enableWgpuTextureResolverGuards.d.ts +4 -0
- package/dist/enableWgpuTextureResolverGuards.d.ts.map +1 -0
- package/dist/enableWgpuTextureResolverGuards.js +8 -0
- package/dist/enableWgpuTextureResolverGuards.js.map +1 -0
- package/dist/explainWgpuTextureResolution.d.ts +3 -0
- package/dist/explainWgpuTextureResolution.d.ts.map +1 -0
- package/dist/explainWgpuTextureResolution.js +15 -0
- package/dist/explainWgpuTextureResolution.js.map +1 -0
- 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/dist/wgpuBackground.d.ts +2 -0
- package/dist/wgpuBackground.d.ts.map +1 -1
- package/dist/wgpuBackground.js +30 -2
- package/dist/wgpuBackground.js.map +1 -1
- package/dist/wgpuCompressedTexture.d.ts +2 -2
- package/dist/wgpuCompressedTexture.d.ts.map +1 -1
- package/dist/wgpuCompressedTexture.js +52 -13
- package/dist/wgpuCompressedTexture.js.map +1 -1
- package/dist/wgpuDraw.d.ts +6 -4
- package/dist/wgpuDraw.d.ts.map +1 -1
- package/dist/wgpuDraw.js +150 -69
- package/dist/wgpuDraw.js.map +1 -1
- package/dist/wgpuExternalImageSource.d.ts +3 -0
- package/dist/wgpuExternalImageSource.d.ts.map +1 -0
- package/dist/wgpuExternalImageSource.js +51 -0
- package/dist/wgpuExternalImageSource.js.map +1 -0
- package/dist/wgpuExternalTexture.d.ts +4 -0
- package/dist/wgpuExternalTexture.d.ts.map +1 -0
- package/dist/wgpuExternalTexture.js +62 -0
- package/dist/wgpuExternalTexture.js.map +1 -0
- package/dist/wgpuMaterialRegistry.d.ts.map +1 -1
- package/dist/wgpuMaterialRegistry.js +17 -12
- package/dist/wgpuMaterialRegistry.js.map +1 -1
- package/dist/wgpuMipmap.d.ts.map +1 -1
- package/dist/wgpuMipmap.js +11 -9
- package/dist/wgpuMipmap.js.map +1 -1
- package/dist/wgpuRenderState.d.ts +15 -3
- package/dist/wgpuRenderState.d.ts.map +1 -1
- package/dist/wgpuRenderState.js +253 -33
- package/dist/wgpuRenderState.js.map +1 -1
- package/dist/wgpuRenderTarget.d.ts.map +1 -1
- package/dist/wgpuRenderTarget.js +2 -0
- package/dist/wgpuRenderTarget.js.map +1 -1
- package/dist/wgpuRenderTexture.d.ts +15 -0
- package/dist/wgpuRenderTexture.d.ts.map +1 -0
- package/dist/wgpuRenderTexture.js +130 -0
- package/dist/wgpuRenderTexture.js.map +1 -0
- package/dist/wgpuRenderTexturePool.d.ts +7 -0
- package/dist/wgpuRenderTexturePool.d.ts.map +1 -0
- package/dist/wgpuRenderTexturePool.js +86 -0
- package/dist/wgpuRenderTexturePool.js.map +1 -0
- package/dist/wgpuShader.d.ts.map +1 -1
- package/dist/wgpuShader.js +5 -1
- package/dist/wgpuShader.js.map +1 -1
- package/dist/wgpuSurface.d.ts +2 -2
- package/dist/wgpuSurface.d.ts.map +1 -1
- package/dist/wgpuSurface.js +61 -13
- package/dist/wgpuSurface.js.map +1 -1
- package/dist/wgpuTestHelper.d.ts +1 -0
- package/dist/wgpuTestHelper.d.ts.map +1 -1
- package/dist/wgpuTestHelper.js +125 -8
- package/dist/wgpuTestHelper.js.map +1 -1
- package/dist/wgpuTextureResolver.d.ts +9 -0
- package/dist/wgpuTextureResolver.d.ts.map +1 -0
- package/dist/wgpuTextureResolver.js +68 -0
- package/dist/wgpuTextureResolver.js.map +1 -0
- package/dist/wgpuTextureUpload.d.ts +2 -2
- package/dist/wgpuTextureUpload.d.ts.map +1 -1
- package/dist/wgpuTextureUpload.js +5 -10
- package/dist/wgpuTextureUpload.js.map +1 -1
- package/package.json +17 -7
- package/src/enableWgpuTextureResolverGuards.test.ts +65 -0
- package/src/explainWgpuTextureResolution.test.ts +36 -0
- package/src/wgpuBackground.test.ts +34 -1
- package/src/wgpuCompressedTexture.test.ts +35 -40
- package/src/wgpuDraw.test.ts +180 -62
- package/src/wgpuExternalImageSource.test.ts +89 -0
- package/src/wgpuExternalTexture.test.ts +35 -0
- package/src/wgpuMaterialRegistry.test.ts +44 -3
- package/src/wgpuMipmap.test.ts +10 -2
- package/src/wgpuRenderState.test.ts +462 -12
- package/src/wgpuRenderTarget.test.ts +14 -0
- package/src/wgpuRenderTexture.test.ts +165 -0
- package/src/wgpuRenderTexturePool.test.ts +146 -0
- package/src/wgpuShaderBinding.test.ts +6 -6
- package/src/wgpuSurface.test.ts +95 -3
- package/src/wgpuTestHelper.test.ts +64 -0
- package/src/wgpuTextureResolver.test.ts +212 -0
- package/src/wgpuTextureUpload.test.ts +28 -13
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { RenderTexture } from '@flighthq/types/contract';
|
|
2
|
+
import { RenderTargetTextureSourceKind } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
|
|
5
|
+
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
6
|
+
import {
|
|
7
|
+
bindWgpuRenderTexture,
|
|
8
|
+
destroyWgpuRenderTexture,
|
|
9
|
+
explainWgpuRenderTexture,
|
|
10
|
+
getWgpuRenderTextureTarget,
|
|
11
|
+
invalidateWgpuRenderTexture,
|
|
12
|
+
isWgpuRenderTextureReady,
|
|
13
|
+
renderIntoWgpuRenderTexture,
|
|
14
|
+
setWgpuRenderTextureGuard,
|
|
15
|
+
writeWgpuRenderTextureTarget,
|
|
16
|
+
} from './wgpuRenderTexture';
|
|
17
|
+
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
installWgpuMock();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function texture(): RenderTexture {
|
|
24
|
+
return {
|
|
25
|
+
colorSpace: 'linear',
|
|
26
|
+
flipX: false,
|
|
27
|
+
flipY: false,
|
|
28
|
+
sampler: {
|
|
29
|
+
anisotropy: 1,
|
|
30
|
+
magFilter: 'linear',
|
|
31
|
+
minFilter: 'linear',
|
|
32
|
+
mipmaps: false,
|
|
33
|
+
wrapU: 'clamp-to-edge',
|
|
34
|
+
wrapV: 'clamp-to-edge',
|
|
35
|
+
},
|
|
36
|
+
dimension: '2d',
|
|
37
|
+
source: { height: 8, kind: RenderTargetTextureSourceKind, version: 0, width: 8 },
|
|
38
|
+
uvOffset: { x: 0, y: 0 },
|
|
39
|
+
uvRotation: 0,
|
|
40
|
+
uvScale: { x: 1, y: 1 },
|
|
41
|
+
version: 0,
|
|
42
|
+
} as unknown as RenderTexture;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('bindWgpuRenderTexture', () => {
|
|
46
|
+
it('returns null before render and the state-owned target after render', async () => {
|
|
47
|
+
const state = await createWgpuRenderStateForTest();
|
|
48
|
+
const renderTexture = texture();
|
|
49
|
+
expect(bindWgpuRenderTexture(state, renderTexture)).toBeNull();
|
|
50
|
+
|
|
51
|
+
renderWgpuBackground(state);
|
|
52
|
+
renderIntoWgpuRenderTexture(state, renderTexture, () => {});
|
|
53
|
+
expect(bindWgpuRenderTexture(state, renderTexture)).not.toBeNull();
|
|
54
|
+
submitWgpuRenderPass(state);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('destroyWgpuRenderTexture', () => {
|
|
59
|
+
it('destroys and removes the hidden target', async () => {
|
|
60
|
+
const state = await createWgpuRenderStateForTest();
|
|
61
|
+
const renderTexture = texture();
|
|
62
|
+
renderWgpuBackground(state);
|
|
63
|
+
renderIntoWgpuRenderTexture(state, renderTexture, () => {});
|
|
64
|
+
const entry = getWgpuRenderStateRuntime(state).wgpuRenderTextureCache!.get(renderTexture)!;
|
|
65
|
+
const destroy = vi.spyOn(entry.target.texture, 'destroy');
|
|
66
|
+
|
|
67
|
+
destroyWgpuRenderTexture(state, renderTexture);
|
|
68
|
+
expect(destroy).toHaveBeenCalled();
|
|
69
|
+
expect(bindWgpuRenderTexture(state, renderTexture)).toBeNull();
|
|
70
|
+
submitWgpuRenderPass(state);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('explainWgpuRenderTexture', () => {
|
|
75
|
+
it('reports source dimensions and lifecycle status before allocation', async () => {
|
|
76
|
+
const state = await createWgpuRenderStateForTest();
|
|
77
|
+
expect(explainWgpuRenderTexture(state, texture())).toEqual({ height: 8, status: 'unrendered', width: 8 });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getWgpuRenderTextureTarget', () => {
|
|
82
|
+
it('returns the hidden target only after a completed write', async () => {
|
|
83
|
+
const state = await createWgpuRenderStateForTest();
|
|
84
|
+
const renderTexture = texture();
|
|
85
|
+
|
|
86
|
+
expect(getWgpuRenderTextureTarget(state, renderTexture)).toBeNull();
|
|
87
|
+
writeWgpuRenderTextureTarget(state, renderTexture, () => {});
|
|
88
|
+
expect(getWgpuRenderTextureTarget(state, renderTexture)).not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('invalidateWgpuRenderTexture', () => {
|
|
93
|
+
it('makes published content unavailable without destroying its target', async () => {
|
|
94
|
+
const state = await createWgpuRenderStateForTest();
|
|
95
|
+
const renderTexture = texture();
|
|
96
|
+
writeWgpuRenderTextureTarget(state, renderTexture, () => {});
|
|
97
|
+
const target = getWgpuRenderTextureTarget(state, renderTexture)!;
|
|
98
|
+
const destroy = vi.spyOn(target.texture, 'destroy');
|
|
99
|
+
|
|
100
|
+
invalidateWgpuRenderTexture(state, renderTexture);
|
|
101
|
+
|
|
102
|
+
expect(getWgpuRenderTextureTarget(state, renderTexture)).toBeNull();
|
|
103
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('isWgpuRenderTextureReady', () => {
|
|
108
|
+
it('tracks the successful rendered-content transition', async () => {
|
|
109
|
+
const state = await createWgpuRenderStateForTest();
|
|
110
|
+
const renderTexture = texture();
|
|
111
|
+
expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(false);
|
|
112
|
+
renderWgpuBackground(state);
|
|
113
|
+
renderIntoWgpuRenderTexture(state, renderTexture, () => {});
|
|
114
|
+
expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(true);
|
|
115
|
+
submitWgpuRenderPass(state);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('renderIntoWgpuRenderTexture', () => {
|
|
120
|
+
it('restores the enclosing pass and bumps version after success', async () => {
|
|
121
|
+
const state = await createWgpuRenderStateForTest();
|
|
122
|
+
const renderTexture = texture();
|
|
123
|
+
renderWgpuBackground(state);
|
|
124
|
+
const enclosingPass = getWgpuRenderStateRuntime(state).renderPass;
|
|
125
|
+
|
|
126
|
+
renderIntoWgpuRenderTexture(state, renderTexture, () => {});
|
|
127
|
+
|
|
128
|
+
expect(renderTexture.version).toBe(1);
|
|
129
|
+
expect(renderTexture.colorSpace).toBe('linear');
|
|
130
|
+
expect(getWgpuRenderStateRuntime(state).renderPass).not.toBeNull();
|
|
131
|
+
expect(getWgpuRenderStateRuntime(state).renderPass).not.toBe(enclosingPass);
|
|
132
|
+
submitWgpuRenderPass(state);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('setWgpuRenderTextureGuard', () => {
|
|
137
|
+
it('installs and removes the lifecycle diagnostic callback', async () => {
|
|
138
|
+
const state = await createWgpuRenderStateForTest();
|
|
139
|
+
const renderTexture = texture();
|
|
140
|
+
const guard = vi.fn();
|
|
141
|
+
|
|
142
|
+
setWgpuRenderTextureGuard(state, guard);
|
|
143
|
+
bindWgpuRenderTexture(state, renderTexture);
|
|
144
|
+
expect(guard).toHaveBeenCalledOnce();
|
|
145
|
+
|
|
146
|
+
setWgpuRenderTextureGuard(state, null);
|
|
147
|
+
bindWgpuRenderTexture(state, renderTexture);
|
|
148
|
+
expect(guard).toHaveBeenCalledOnce();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('writeWgpuRenderTextureTarget', () => {
|
|
153
|
+
it('passes the hidden target to the producer and publishes the completed write', async () => {
|
|
154
|
+
const state = await createWgpuRenderStateForTest();
|
|
155
|
+
const renderTexture = texture();
|
|
156
|
+
const producer = vi.fn();
|
|
157
|
+
|
|
158
|
+
writeWgpuRenderTextureTarget(state, renderTexture, producer);
|
|
159
|
+
|
|
160
|
+
expect(producer).toHaveBeenCalledOnce();
|
|
161
|
+
expect(producer.mock.calls[0][0]).toBe(getWgpuRenderTextureTarget(state, renderTexture));
|
|
162
|
+
expect(renderTexture.version).toBe(1);
|
|
163
|
+
expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
explainWgpuRenderTexture,
|
|
3
|
+
getWgpuRenderTextureTarget,
|
|
4
|
+
isWgpuRenderTextureReady,
|
|
5
|
+
writeWgpuRenderTextureTarget,
|
|
6
|
+
} from './wgpuRenderTexture';
|
|
7
|
+
import {
|
|
8
|
+
acquireWgpuRenderTexture,
|
|
9
|
+
createWgpuRenderTexturePool,
|
|
10
|
+
destroyWgpuRenderTexturePool,
|
|
11
|
+
releaseWgpuRenderTexture,
|
|
12
|
+
withWgpuRenderTextures,
|
|
13
|
+
} from './wgpuRenderTexturePool';
|
|
14
|
+
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
15
|
+
|
|
16
|
+
beforeAll(() => installWgpuMock());
|
|
17
|
+
|
|
18
|
+
describe('acquireWgpuRenderTexture', () => {
|
|
19
|
+
it('reuses a released handle and reapplies dimensions before the next write', async () => {
|
|
20
|
+
const state = await createWgpuRenderStateForTest();
|
|
21
|
+
const pool = createWgpuRenderTexturePool();
|
|
22
|
+
const first = acquireWgpuRenderTexture(state, pool, { width: 32, height: 16 });
|
|
23
|
+
writeWgpuRenderTextureTarget(state, first, () => {});
|
|
24
|
+
releaseWgpuRenderTexture(state, pool, first);
|
|
25
|
+
|
|
26
|
+
expect(explainWgpuRenderTexture(state, first).status).toBe('released');
|
|
27
|
+
expect(isWgpuRenderTextureReady(state, first)).toBe(false);
|
|
28
|
+
|
|
29
|
+
const second = acquireWgpuRenderTexture(state, pool, { width: 80, height: 48 });
|
|
30
|
+
expect(second).toBe(first);
|
|
31
|
+
expect(second.source.width).toBe(80);
|
|
32
|
+
expect(second.source.height).toBe(48);
|
|
33
|
+
expect(explainWgpuRenderTexture(state, second).status).toBe('unrendered');
|
|
34
|
+
|
|
35
|
+
writeWgpuRenderTextureTarget(state, second, () => {});
|
|
36
|
+
expect(explainWgpuRenderTexture(state, second)).toEqual({ width: 80, height: 48, status: 'ready' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('resets a released slab view before exposing the next logical target', async () => {
|
|
40
|
+
const state = await createWgpuRenderStateForTest();
|
|
41
|
+
const pool = createWgpuRenderTexturePool();
|
|
42
|
+
const slabView = acquireWgpuRenderTexture(state, pool, { width: 720, height: 480 });
|
|
43
|
+
slabView.flipX = true;
|
|
44
|
+
slabView.flipY = true;
|
|
45
|
+
slabView.uvOffset.x = 140 / 720;
|
|
46
|
+
slabView.uvOffset.y = 160 / 480;
|
|
47
|
+
slabView.uvRotation = Math.PI / 2;
|
|
48
|
+
slabView.uvScale.x = 100 / 720;
|
|
49
|
+
slabView.uvScale.y = 80 / 480;
|
|
50
|
+
releaseWgpuRenderTexture(state, pool, slabView);
|
|
51
|
+
|
|
52
|
+
const logicalTarget = acquireWgpuRenderTexture(state, pool, { width: 100, height: 80 });
|
|
53
|
+
|
|
54
|
+
expect(logicalTarget).toBe(slabView);
|
|
55
|
+
expect(logicalTarget).toMatchObject({
|
|
56
|
+
flipX: false,
|
|
57
|
+
flipY: false,
|
|
58
|
+
uvOffset: { x: 0, y: 0 },
|
|
59
|
+
uvRotation: 0,
|
|
60
|
+
uvScale: { x: 1, y: 1 },
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('does not allow a pool to cross GPU devices', async () => {
|
|
65
|
+
const a = await createWgpuRenderStateForTest();
|
|
66
|
+
const b = await createWgpuRenderStateForTest();
|
|
67
|
+
const pool = createWgpuRenderTexturePool();
|
|
68
|
+
acquireWgpuRenderTexture(a, pool, { width: 8, height: 8 });
|
|
69
|
+
|
|
70
|
+
expect(() => acquireWgpuRenderTexture(b, pool, { width: 8, height: 8 })).toThrow('cannot cross GPU devices');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('createWgpuRenderTexturePool', () => {
|
|
75
|
+
it('creates an empty device-unbound pool', () => {
|
|
76
|
+
const pool = createWgpuRenderTexturePool();
|
|
77
|
+
expect(pool.device).toBeNull();
|
|
78
|
+
expect(pool.destroyed).toBe(false);
|
|
79
|
+
expect(pool.free).toEqual([]);
|
|
80
|
+
expect(pool.leased.size).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('destroyWgpuRenderTexturePool', () => {
|
|
85
|
+
it('destroys hidden targets for free and outstanding leases', async () => {
|
|
86
|
+
const state = await createWgpuRenderStateForTest();
|
|
87
|
+
const pool = createWgpuRenderTexturePool();
|
|
88
|
+
const free = acquireWgpuRenderTexture(state, pool, { width: 8, height: 8 });
|
|
89
|
+
const leased = acquireWgpuRenderTexture(state, pool, { width: 16, height: 16 });
|
|
90
|
+
writeWgpuRenderTextureTarget(state, free, () => {});
|
|
91
|
+
writeWgpuRenderTextureTarget(state, leased, () => {});
|
|
92
|
+
const destroyFree = vi.spyOn(getWgpuRenderTextureTarget(state, free)!.texture, 'destroy');
|
|
93
|
+
const destroyLeased = vi.spyOn(getWgpuRenderTextureTarget(state, leased)!.texture, 'destroy');
|
|
94
|
+
releaseWgpuRenderTexture(state, pool, free);
|
|
95
|
+
|
|
96
|
+
destroyWgpuRenderTexturePool(state, pool);
|
|
97
|
+
|
|
98
|
+
expect(destroyFree).toHaveBeenCalledOnce();
|
|
99
|
+
expect(destroyLeased).toHaveBeenCalledOnce();
|
|
100
|
+
expect(pool.free).toHaveLength(0);
|
|
101
|
+
expect(pool.leased.size).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects acquisition after destruction', async () => {
|
|
105
|
+
const state = await createWgpuRenderStateForTest();
|
|
106
|
+
const pool = createWgpuRenderTexturePool();
|
|
107
|
+
destroyWgpuRenderTexturePool(state, pool);
|
|
108
|
+
expect(() => acquireWgpuRenderTexture(state, pool, { width: 8, height: 8 })).toThrow('has been destroyed');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('releaseWgpuRenderTexture', () => {
|
|
113
|
+
it('rejects a double release', async () => {
|
|
114
|
+
const state = await createWgpuRenderStateForTest();
|
|
115
|
+
const pool = createWgpuRenderTexturePool();
|
|
116
|
+
const texture = acquireWgpuRenderTexture(state, pool, { width: 8, height: 8 });
|
|
117
|
+
releaseWgpuRenderTexture(state, pool, texture);
|
|
118
|
+
|
|
119
|
+
expect(() => releaseWgpuRenderTexture(state, pool, texture)).toThrow('texture is not leased');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('withWgpuRenderTextures', () => {
|
|
124
|
+
it('releases every lease when the bracket callback throws', async () => {
|
|
125
|
+
const state = await createWgpuRenderStateForTest();
|
|
126
|
+
const pool = createWgpuRenderTexturePool();
|
|
127
|
+
|
|
128
|
+
expect(() =>
|
|
129
|
+
withWgpuRenderTextures(
|
|
130
|
+
state,
|
|
131
|
+
pool,
|
|
132
|
+
[
|
|
133
|
+
{ width: 8, height: 8 },
|
|
134
|
+
{ width: 8, height: 8 },
|
|
135
|
+
{ width: 8, height: 8 },
|
|
136
|
+
],
|
|
137
|
+
() => {
|
|
138
|
+
throw new Error('capture failed');
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
).toThrow('capture failed');
|
|
142
|
+
|
|
143
|
+
expect(pool.leased.size).toBe(0);
|
|
144
|
+
expect(pool.free).toHaveLength(3);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getOrCreateRenderProxy2D } from '@flighthq/render/contract';
|
|
2
|
-
import {
|
|
2
|
+
import { createSprite } from '@flighthq/scene2d/contract';
|
|
3
3
|
import type { WgpuBitmapShader } from '@flighthq/types/contract';
|
|
4
4
|
|
|
5
5
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
@@ -13,7 +13,7 @@ beforeAll(() => {
|
|
|
13
13
|
describe('getWgpuShader', () => {
|
|
14
14
|
it('returns undefined when no shader is bound', async () => {
|
|
15
15
|
const state = await createWgpuRenderStateForTest();
|
|
16
|
-
const bitmap =
|
|
16
|
+
const bitmap = createSprite();
|
|
17
17
|
const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
|
|
18
18
|
expect(getWgpuShader(renderProxy)).toBeUndefined();
|
|
19
19
|
});
|
|
@@ -22,7 +22,7 @@ describe('getWgpuShader', () => {
|
|
|
22
22
|
describe('resolveWgpuShader', () => {
|
|
23
23
|
it('returns null when no shader feature is enabled', async () => {
|
|
24
24
|
const state = await createWgpuRenderStateForTest();
|
|
25
|
-
const bitmap =
|
|
25
|
+
const bitmap = createSprite();
|
|
26
26
|
const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
|
|
27
27
|
expect(resolveWgpuShader(state, renderProxy)).toBeNull();
|
|
28
28
|
});
|
|
@@ -31,7 +31,7 @@ describe('resolveWgpuShader', () => {
|
|
|
31
31
|
describe('setWgpuShader', () => {
|
|
32
32
|
it('binds a shader to a display object', async () => {
|
|
33
33
|
const state = await createWgpuRenderStateForTest();
|
|
34
|
-
const bitmap =
|
|
34
|
+
const bitmap = createSprite();
|
|
35
35
|
const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
|
|
36
36
|
|
|
37
37
|
setWgpuShader(state, bitmap, fakeShader);
|
|
@@ -43,7 +43,7 @@ describe('setWgpuShader', () => {
|
|
|
43
43
|
it('installs the per-node shader binding resolver', async () => {
|
|
44
44
|
const state = await createWgpuRenderStateForTest();
|
|
45
45
|
const runtime = getWgpuRenderStateRuntime(state);
|
|
46
|
-
const bitmap =
|
|
46
|
+
const bitmap = createSprite();
|
|
47
47
|
const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
|
|
48
48
|
expect(runtime.webgpuShaderBindingResolver).toBeUndefined();
|
|
49
49
|
setWgpuShader(state, bitmap, fakeShader);
|
|
@@ -52,7 +52,7 @@ describe('setWgpuShader', () => {
|
|
|
52
52
|
|
|
53
53
|
it('removes the binding when shader is null', async () => {
|
|
54
54
|
const state = await createWgpuRenderStateForTest();
|
|
55
|
-
const bitmap =
|
|
55
|
+
const bitmap = createSprite();
|
|
56
56
|
const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
|
|
57
57
|
setWgpuShader(state, bitmap, fakeShader);
|
|
58
58
|
setWgpuShader(state, bitmap, null);
|
package/src/wgpuSurface.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { WgpuRenderState } from '@flighthq/types/contract';
|
|
1
2
|
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
3
|
|
|
3
4
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
4
5
|
import {
|
|
5
6
|
acquireWgpuFrameCaptureTexture,
|
|
6
|
-
|
|
7
|
+
createBitmapFromWgpuRenderState,
|
|
7
8
|
enableWgpuFrameCapture,
|
|
8
9
|
encodeWgpuFrameCapture,
|
|
9
10
|
} from './wgpuSurface';
|
|
@@ -22,13 +23,62 @@ describe('acquireWgpuFrameCaptureTexture', () => {
|
|
|
22
23
|
});
|
|
23
24
|
});
|
|
24
25
|
|
|
25
|
-
describe('
|
|
26
|
+
describe('createBitmapFromWgpuRenderState', () => {
|
|
26
27
|
it('throws when frame capture was never enabled (no buffer to read)', async () => {
|
|
27
28
|
const state = await createWgpuRenderStateForTest();
|
|
28
|
-
await expect(
|
|
29
|
+
await expect(createBitmapFromWgpuRenderState(state)).rejects.toThrow(/enableWgpuFrameCapture/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reads the mapped buffer within the bound', async () => {
|
|
33
|
+
const state = await createWgpuRenderStateForTest();
|
|
34
|
+
installCaptureBuffer(state, () => Promise.resolve());
|
|
35
|
+
|
|
36
|
+
const bitmap = await createBitmapFromWgpuRenderState(state, 1_000);
|
|
37
|
+
|
|
38
|
+
expect(bitmap.width).toBe(1);
|
|
39
|
+
expect(bitmap.height).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('fails by name when the buffer never maps, instead of awaiting forever', async () => {
|
|
43
|
+
// The driver failure this bounds: mapAsync neither resolves nor rejects. Before the bound, the
|
|
44
|
+
// caller sat here until something outside it gave up and reported a stall with no location.
|
|
45
|
+
const state = await createWgpuRenderStateForTest();
|
|
46
|
+
installCaptureBuffer(state, () => new Promise<void>(() => {}));
|
|
47
|
+
|
|
48
|
+
await expect(createBitmapFromWgpuRenderState(state, 25)).rejects.toThrow(/did not map within 25ms/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('leaves the wait unbounded when the caller passes 0', async () => {
|
|
52
|
+
const state = await createWgpuRenderStateForTest();
|
|
53
|
+
let settle = (): void => {};
|
|
54
|
+
installCaptureBuffer(state, () => new Promise<void>((resolve) => (settle = resolve)));
|
|
55
|
+
|
|
56
|
+
let done = false;
|
|
57
|
+
const pending = createBitmapFromWgpuRenderState(state, 0).then(() => (done = true));
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
59
|
+
expect(done).toBe(false);
|
|
60
|
+
|
|
61
|
+
settle();
|
|
62
|
+
await pending;
|
|
63
|
+
expect(done).toBe(true);
|
|
29
64
|
});
|
|
30
65
|
});
|
|
31
66
|
|
|
67
|
+
// Installs a 1×1 retained capture buffer whose map step the caller controls, so the map bound can be
|
|
68
|
+
// exercised without a GPU. The mock device's buffers carry no map surface of their own.
|
|
69
|
+
function installCaptureBuffer(state: WgpuRenderState, mapAsync: () => Promise<void>): void {
|
|
70
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
71
|
+
runtime.frameCaptureBuffer = {
|
|
72
|
+
mapAsync,
|
|
73
|
+
getMappedRange: () => new ArrayBuffer(256),
|
|
74
|
+
unmap: () => {},
|
|
75
|
+
destroy: () => {},
|
|
76
|
+
} as unknown as GPUBuffer;
|
|
77
|
+
runtime.frameCaptureWidth = 1;
|
|
78
|
+
runtime.frameCaptureHeight = 1;
|
|
79
|
+
runtime.frameCaptureBytesPerRow = 256;
|
|
80
|
+
}
|
|
81
|
+
|
|
32
82
|
describe('enableWgpuFrameCapture', () => {
|
|
33
83
|
it('sets the capture flag on the render state runtime', async () => {
|
|
34
84
|
const state = await createWgpuRenderStateForTest();
|
|
@@ -46,4 +96,46 @@ describe('encodeWgpuFrameCapture', () => {
|
|
|
46
96
|
encodeWgpuFrameCapture(state, encoder);
|
|
47
97
|
expect(copied).toBe(false);
|
|
48
98
|
});
|
|
99
|
+
|
|
100
|
+
it('skips the copy while a readback holds the buffer, and resumes once it lets go', async () => {
|
|
101
|
+
// The writer/reader handshake over the single retained buffer: an animating scene must not keep
|
|
102
|
+
// enqueueing copies into a buffer a readback has mapped or is waiting to map.
|
|
103
|
+
const state = await createWgpuRenderStateForTest();
|
|
104
|
+
enableWgpuFrameCapture(state);
|
|
105
|
+
acquireWgpuFrameCaptureTexture(state);
|
|
106
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
107
|
+
let mapState: GPUBufferMapState = 'unmapped';
|
|
108
|
+
installCaptureBuffer(state, () => Promise.resolve());
|
|
109
|
+
Object.defineProperty(runtime.frameCaptureBuffer, 'mapState', { get: () => mapState });
|
|
110
|
+
|
|
111
|
+
let copies = 0;
|
|
112
|
+
const encoder = { copyTextureToBuffer: () => copies++ } as unknown as GPUCommandEncoder;
|
|
113
|
+
|
|
114
|
+
mapState = 'pending';
|
|
115
|
+
encodeWgpuFrameCapture(state, encoder);
|
|
116
|
+
mapState = 'mapped';
|
|
117
|
+
encodeWgpuFrameCapture(state, encoder);
|
|
118
|
+
expect(copies).toBe(0);
|
|
119
|
+
|
|
120
|
+
mapState = 'unmapped';
|
|
121
|
+
encodeWgpuFrameCapture(state, encoder);
|
|
122
|
+
expect(copies).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('still copies when the implementation reports no map state at all', async () => {
|
|
126
|
+
// Fails safe in the direction that keeps capturing: an undefined mapState must not read as "busy"
|
|
127
|
+
// and silence every copy for the rest of the run.
|
|
128
|
+
const state = await createWgpuRenderStateForTest();
|
|
129
|
+
enableWgpuFrameCapture(state);
|
|
130
|
+
acquireWgpuFrameCaptureTexture(state);
|
|
131
|
+
installCaptureBuffer(state, () => Promise.resolve());
|
|
132
|
+
Object.defineProperty(getWgpuRenderStateRuntime(state).frameCaptureBuffer, 'mapState', {
|
|
133
|
+
get: () => undefined,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let copies = 0;
|
|
137
|
+
encodeWgpuFrameCapture(state, { copyTextureToBuffer: () => copies++ } as unknown as GPUCommandEncoder);
|
|
138
|
+
|
|
139
|
+
expect(copies).toBe(1);
|
|
140
|
+
});
|
|
49
141
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
2
|
+
|
|
3
|
+
beforeAll(() => {
|
|
4
|
+
installWgpuMock();
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
describe('installWgpuMock', () => {
|
|
8
|
+
it('accepts aligned writeBuffer data using typed-array element units', async () => {
|
|
9
|
+
const state = await createWgpuRenderStateForTest();
|
|
10
|
+
const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.COPY_DST });
|
|
11
|
+
|
|
12
|
+
expect(() => state.device.queue.writeBuffer(buffer, 0, new Uint16Array([0, 1, 2]), 0, 2)).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects writeBuffer content whose byte length is not a multiple of four', async () => {
|
|
16
|
+
const state = await createWgpuRenderStateForTest();
|
|
17
|
+
const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.COPY_DST });
|
|
18
|
+
|
|
19
|
+
expect(() => state.device.queue.writeBuffer(buffer, 0, new Uint16Array([0, 1, 2]))).toThrow(
|
|
20
|
+
expect.objectContaining({ name: 'OperationError' }),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('rejects writeBuffer source ranges and destination offsets outside the API contract', async () => {
|
|
25
|
+
const state = await createWgpuRenderStateForTest();
|
|
26
|
+
const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.COPY_DST });
|
|
27
|
+
|
|
28
|
+
expect(() => state.device.queue.writeBuffer(buffer, 0, new Uint32Array([0]), 2)).toThrow(
|
|
29
|
+
/data range is out of bounds/,
|
|
30
|
+
);
|
|
31
|
+
expect(() => state.device.queue.writeBuffer(buffer, 2, new Uint32Array([0]))).toThrow(
|
|
32
|
+
/bufferOffset must be a non-negative multiple of 4/,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects writeBuffer destinations without enough COPY_DST storage', async () => {
|
|
37
|
+
const state = await createWgpuRenderStateForTest();
|
|
38
|
+
const tooSmall = state.device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST });
|
|
39
|
+
const wrongUsage = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.VERTEX });
|
|
40
|
+
|
|
41
|
+
expect(() => state.device.queue.writeBuffer(tooSmall, 0, new Uint32Array([0, 1]))).toThrow(
|
|
42
|
+
/destination range exceeds the buffer size/,
|
|
43
|
+
);
|
|
44
|
+
expect(() => state.device.queue.writeBuffer(wrongUsage, 0, new Uint32Array([0]))).toThrow(
|
|
45
|
+
/must have COPY_DST usage/,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects writeTexture data smaller than an rgba8 copy requires', async () => {
|
|
50
|
+
const state = await createWgpuRenderStateForTest();
|
|
51
|
+
const texture = state.device.createTexture({
|
|
52
|
+
size: [2, 2, 1],
|
|
53
|
+
format: 'rgba8unorm',
|
|
54
|
+
usage: GPUTextureUsage.COPY_DST,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(() =>
|
|
58
|
+
state.device.queue.writeTexture({ texture }, new Uint8Array(15), { bytesPerRow: 8, rowsPerImage: 2 }, [2, 2, 1]),
|
|
59
|
+
).toThrow(expect.objectContaining({ name: 'OperationError' }));
|
|
60
|
+
expect(() =>
|
|
61
|
+
state.device.queue.writeTexture({ texture }, new Uint8Array(16), { bytesPerRow: 8, rowsPerImage: 2 }, [2, 2, 1]),
|
|
62
|
+
).not.toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|