@flighthq/render-wgpu 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc
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 +14 -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.js +2 -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 +40 -12
- 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 +139 -67
- 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 +10 -9
- 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 +11 -2
- package/dist/wgpuRenderState.d.ts.map +1 -1
- package/dist/wgpuRenderState.js +217 -30
- 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 +15 -1
- 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 +67 -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 +13 -7
- package/src/enableWgpuTextureResolverGuards.test.ts +65 -0
- package/src/explainWgpuTextureResolution.test.ts +36 -0
- package/src/wgpuCompressedTexture.test.ts +18 -35
- package/src/wgpuDraw.test.ts +164 -62
- package/src/wgpuExternalImageSource.test.ts +89 -0
- package/src/wgpuExternalTexture.test.ts +35 -0
- package/src/wgpuMaterialRegistry.test.ts +28 -2
- package/src/wgpuMipmap.test.ts +10 -2
- package/src/wgpuRenderState.test.ts +143 -11
- 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/wgpuTextureResolver.test.ts +209 -0
- package/src/wgpuTextureUpload.test.ts +28 -13
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
1
2
|
import {
|
|
3
|
+
copyAllRenderersFromRenderState,
|
|
4
|
+
getRenderStateRuntime,
|
|
5
|
+
prepareScene2DRender,
|
|
6
|
+
registerRenderer,
|
|
7
|
+
} from '@flighthq/render/contract';
|
|
8
|
+
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
9
|
+
|
|
10
|
+
import { beginWgpuFrame } from './wgpuBackground';
|
|
11
|
+
import { registerWgpuMaterialRenderer } from './wgpuMaterialRegistry';
|
|
12
|
+
import {
|
|
13
|
+
copyWgpuRenderStateRegistrations,
|
|
14
|
+
createWgpuOffscreenRenderState,
|
|
2
15
|
createWgpuRenderStateRuntime,
|
|
3
16
|
destroyWgpuRenderState,
|
|
4
17
|
getWgpuRenderStateRuntime,
|
|
@@ -6,11 +19,121 @@ import {
|
|
|
6
19
|
isWgpuSupported,
|
|
7
20
|
} from './wgpuRenderState';
|
|
8
21
|
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
22
|
+
import { registerWgpuTextureResolver } from './wgpuTextureResolver';
|
|
9
23
|
|
|
10
24
|
beforeAll(() => {
|
|
11
25
|
installWgpuMock();
|
|
12
26
|
});
|
|
13
27
|
|
|
28
|
+
describe('copyWgpuRenderStateRegistrations', () => {
|
|
29
|
+
it('copies late Wgpu registrations only when explicitly requested', async () => {
|
|
30
|
+
const screen = await createWgpuRenderStateForTest();
|
|
31
|
+
const offscreen = createWgpuOffscreenRenderState(screen);
|
|
32
|
+
const resolver = vi.fn(() => null);
|
|
33
|
+
registerWgpuTextureResolver(screen, 'acme.LateTexture', resolver);
|
|
34
|
+
|
|
35
|
+
expect(getWgpuRenderStateRuntime(offscreen).wgpuTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
|
|
36
|
+
copyWgpuRenderStateRegistrations(offscreen, screen);
|
|
37
|
+
expect(getWgpuRenderStateRuntime(offscreen).wgpuTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createWgpuOffscreenRenderState', () => {
|
|
42
|
+
it('shares device resources while snapshotting independent registration policy', async () => {
|
|
43
|
+
const screen = await createWgpuRenderStateForTest();
|
|
44
|
+
const renderer = { createData: () => null, submit: vi.fn() };
|
|
45
|
+
const materialRenderer = { instanceFloatCount: 0, getShaderModule: vi.fn() } as never;
|
|
46
|
+
const paddingResolver = vi.fn(() => ({ bottom: 1, left: 1, right: 1, top: 1 }));
|
|
47
|
+
const textureResolver = vi.fn(() => null);
|
|
48
|
+
const effectRunner = vi.fn();
|
|
49
|
+
registerRenderer(screen, 'acme.Node', renderer);
|
|
50
|
+
registerWgpuMaterialRenderer(screen, 'acme.Material', materialRenderer);
|
|
51
|
+
registerWgpuTextureResolver(screen, 'acme.Texture', textureResolver);
|
|
52
|
+
getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.Effect', paddingResolver]]);
|
|
53
|
+
getWgpuRenderStateRuntime(screen).wgpuRenderEffectRegistry = new Map([['acme.Effect', effectRunner]]);
|
|
54
|
+
getWgpuRenderStateRuntime(screen).wgpuRenderTextureCache = new WeakMap();
|
|
55
|
+
|
|
56
|
+
const offscreen = createWgpuOffscreenRenderState(screen);
|
|
57
|
+
const screenRuntime = getWgpuRenderStateRuntime(screen);
|
|
58
|
+
const offscreenRuntime = getWgpuRenderStateRuntime(offscreen);
|
|
59
|
+
|
|
60
|
+
expect(offscreen.device).toBe(screen.device);
|
|
61
|
+
expect(offscreen.canvas).toBe(screen.canvas);
|
|
62
|
+
expect(offscreenRuntime.pipelineCache).toBe(screenRuntime.pipelineCache);
|
|
63
|
+
expect(offscreenRuntime.textureCache).toBe(screenRuntime.textureCache);
|
|
64
|
+
expect(offscreenRuntime.wgpuRenderTextureCache).toBe(screenRuntime.wgpuRenderTextureCache);
|
|
65
|
+
expect(offscreenRuntime.uniformBindGroupLayout).toBe(screenRuntime.uniformBindGroupLayout);
|
|
66
|
+
expect(offscreenRuntime.uniformBuffer).not.toBe(screenRuntime.uniformBuffer);
|
|
67
|
+
expect(offscreenRuntime.rendererMap).not.toBe(screenRuntime.rendererMap);
|
|
68
|
+
expect(offscreenRuntime.materialRendererMap).not.toBe(screenRuntime.materialRendererMap);
|
|
69
|
+
expect(offscreenRuntime.wgpuTextureResolverRegistry).not.toBe(screenRuntime.wgpuTextureResolverRegistry);
|
|
70
|
+
expect(offscreenRuntime.wgpuRenderEffectRegistry).not.toBe(screenRuntime.wgpuRenderEffectRegistry);
|
|
71
|
+
expect(offscreenRuntime.renderEffectPaddingResolverRegistry).not.toBe(
|
|
72
|
+
screenRuntime.renderEffectPaddingResolverRegistry,
|
|
73
|
+
);
|
|
74
|
+
expect(offscreenRuntime.rendererMap.get('acme.Node')).toBe(renderer);
|
|
75
|
+
expect(offscreenRuntime.materialRendererMap?.get('acme.Material')).toBe(materialRenderer);
|
|
76
|
+
expect(offscreenRuntime.wgpuTextureResolverRegistry?.get('acme.Texture')).toBe(textureResolver);
|
|
77
|
+
expect(offscreenRuntime.wgpuRenderEffectRegistry?.get('acme.Effect')).toBe(effectRunner);
|
|
78
|
+
expect(offscreenRuntime.renderEffectPaddingResolverRegistry?.get('acme.Effect')).toBe(paddingResolver);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('owns an independent encoder and proxy tree', async () => {
|
|
82
|
+
const screen = await createWgpuRenderStateForTest();
|
|
83
|
+
const offscreen = createWgpuOffscreenRenderState(screen);
|
|
84
|
+
const root = createDisplayObject();
|
|
85
|
+
registerRenderer(offscreen, root.kind, { createData: () => createEntity({}), submit: vi.fn() });
|
|
86
|
+
prepareScene2DRender(offscreen, root);
|
|
87
|
+
beginWgpuFrame(screen);
|
|
88
|
+
beginWgpuFrame(offscreen);
|
|
89
|
+
|
|
90
|
+
expect(getRenderStateRuntime(offscreen).renderProxyMap).not.toBe(getRenderStateRuntime(screen).renderProxyMap);
|
|
91
|
+
expect(getWgpuRenderStateRuntime(offscreen).commandEncoder).not.toBe(
|
|
92
|
+
getWgpuRenderStateRuntime(screen).commandEncoder,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('destroys derived renderer data and its own uniform ring without freeing the screen ring', async () => {
|
|
97
|
+
const screen = await createWgpuRenderStateForTest();
|
|
98
|
+
const root = createDisplayObject();
|
|
99
|
+
const destroyData = vi.fn();
|
|
100
|
+
registerRenderer(screen, root.kind, {
|
|
101
|
+
createData: () => createEntity({}),
|
|
102
|
+
destroyData,
|
|
103
|
+
submit: vi.fn(),
|
|
104
|
+
});
|
|
105
|
+
const offscreen = createWgpuOffscreenRenderState(screen);
|
|
106
|
+
prepareScene2DRender(offscreen, root);
|
|
107
|
+
const screenDestroy = vi.spyOn(getWgpuRenderStateRuntime(screen).uniformBuffer, 'destroy');
|
|
108
|
+
const offscreenDestroy = vi.spyOn(getWgpuRenderStateRuntime(offscreen).uniformBuffer, 'destroy');
|
|
109
|
+
|
|
110
|
+
destroyWgpuRenderState(offscreen);
|
|
111
|
+
|
|
112
|
+
expect(destroyData).toHaveBeenCalledOnce();
|
|
113
|
+
expect(offscreenDestroy).toHaveBeenCalledOnce();
|
|
114
|
+
expect(screenDestroy).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('requires explicit re-copy for renderers and padding registered after derivation', async () => {
|
|
118
|
+
const screen = await createWgpuRenderStateForTest();
|
|
119
|
+
const offscreen = createWgpuOffscreenRenderState(screen);
|
|
120
|
+
const renderer = { createData: () => null, submit: vi.fn() };
|
|
121
|
+
const paddingResolver = vi.fn(() => ({ bottom: 2, left: 2, right: 2, top: 2 }));
|
|
122
|
+
registerRenderer(screen, 'acme.LateNode', renderer);
|
|
123
|
+
getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.LateEffect', paddingResolver]]);
|
|
124
|
+
|
|
125
|
+
expect(getRenderStateRuntime(offscreen).rendererMap.has('acme.LateNode')).toBe(false);
|
|
126
|
+
expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.has('acme.LateEffect')).not.toBe(true);
|
|
127
|
+
|
|
128
|
+
copyAllRenderersFromRenderState(offscreen, screen);
|
|
129
|
+
copyWgpuRenderStateRegistrations(offscreen, screen);
|
|
130
|
+
expect(getRenderStateRuntime(offscreen).rendererMap.get('acme.LateNode')).toBe(renderer);
|
|
131
|
+
expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.get('acme.LateEffect')).toBe(
|
|
132
|
+
paddingResolver,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
14
137
|
describe('createWgpuRenderState', () => {
|
|
15
138
|
it('returns a render state with device and context', async () => {
|
|
16
139
|
const state = await createWgpuRenderStateForTest();
|
|
@@ -85,8 +208,8 @@ describe('getWgpuRenderStateRuntime', () => {
|
|
|
85
208
|
describe('getWgpuSampler', () => {
|
|
86
209
|
it('caches a sampler per filter+wrap+mip+anisotropy config and reuses it (one createSampler)', async () => {
|
|
87
210
|
const state = await createWgpuRenderStateForTest();
|
|
88
|
-
const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
|
|
89
|
-
const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
|
|
211
|
+
const a = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
|
|
212
|
+
const b = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
|
|
90
213
|
expect(a).toBe(b);
|
|
91
214
|
// The cache is keyed by a packed NUMBER (no per-call string allocation), and one config caches once.
|
|
92
215
|
expect([...getWgpuRenderStateRuntime(state).samplerCache.keys()].every((k) => typeof k === 'number')).toBe(true);
|
|
@@ -95,17 +218,17 @@ describe('getWgpuSampler', () => {
|
|
|
95
218
|
|
|
96
219
|
it('returns a distinct sampler for a different wrap or filter', async () => {
|
|
97
220
|
const state = await createWgpuRenderStateForTest();
|
|
98
|
-
const repeat = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
|
|
99
|
-
const clamp = getWgpuSampler(state, 'linear', 'clamp-to-edge', 'clamp-to-edge');
|
|
100
|
-
const nearest = getWgpuSampler(state, 'nearest', 'repeat', 'repeat');
|
|
221
|
+
const repeat = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
|
|
222
|
+
const clamp = getWgpuSampler(state, 'linear', 'linear', 'clamp-to-edge', 'clamp-to-edge');
|
|
223
|
+
const nearest = getWgpuSampler(state, 'nearest', 'nearest', 'repeat', 'repeat');
|
|
101
224
|
expect(repeat).not.toBe(clamp);
|
|
102
225
|
expect(repeat).not.toBe(nearest);
|
|
103
226
|
});
|
|
104
227
|
|
|
105
228
|
it('keys the mip filter separately so a trilinear sampler differs from a non-mip one', async () => {
|
|
106
229
|
const state = await createWgpuRenderStateForTest();
|
|
107
|
-
const noMip = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
|
|
108
|
-
const trilinear = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear');
|
|
230
|
+
const noMip = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
|
|
231
|
+
const trilinear = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear');
|
|
109
232
|
expect(noMip).not.toBe(trilinear);
|
|
110
233
|
expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(2);
|
|
111
234
|
});
|
|
@@ -114,18 +237,27 @@ describe('getWgpuSampler', () => {
|
|
|
114
237
|
// WebGPU rejects maxAnisotropy > 1 unless min/mag/mip are all linear, so a nearest+aniso request
|
|
115
238
|
// collapses to the SAME sampler as the explicit linear/trilinear anisotropic request.
|
|
116
239
|
const state = await createWgpuRenderStateForTest();
|
|
117
|
-
const collapsed = getWgpuSampler(state, 'nearest', 'clamp-to-edge', 'clamp-to-edge', undefined, 8);
|
|
118
|
-
const explicit = getWgpuSampler(state, 'linear', 'clamp-to-edge', 'clamp-to-edge', 'linear', 8);
|
|
240
|
+
const collapsed = getWgpuSampler(state, 'nearest', 'nearest', 'clamp-to-edge', 'clamp-to-edge', undefined, 8);
|
|
241
|
+
const explicit = getWgpuSampler(state, 'linear', 'linear', 'clamp-to-edge', 'clamp-to-edge', 'linear', 8);
|
|
119
242
|
expect(collapsed).toBe(explicit);
|
|
120
243
|
});
|
|
121
244
|
|
|
122
245
|
it('floors and clamps the anisotropy level into the cache key', async () => {
|
|
123
246
|
const state = await createWgpuRenderStateForTest();
|
|
124
|
-
const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear', 4.9);
|
|
125
|
-
const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear', 4);
|
|
247
|
+
const a = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear', 4.9);
|
|
248
|
+
const b = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear', 4);
|
|
126
249
|
expect(a).toBe(b);
|
|
127
250
|
expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(1);
|
|
128
251
|
});
|
|
252
|
+
|
|
253
|
+
it('keeps independent minification and magnification filters', async () => {
|
|
254
|
+
const state = await createWgpuRenderStateForTest();
|
|
255
|
+
const createSampler = vi.spyOn(state.device, 'createSampler');
|
|
256
|
+
getWgpuSampler(state, 'nearest', 'linear', 'repeat', 'repeat');
|
|
257
|
+
expect(createSampler).toHaveBeenLastCalledWith(
|
|
258
|
+
expect.objectContaining({ minFilter: 'nearest', magFilter: 'linear' }),
|
|
259
|
+
);
|
|
260
|
+
});
|
|
129
261
|
});
|
|
130
262
|
|
|
131
263
|
describe('isWgpuSupported', () => {
|
|
@@ -155,6 +155,20 @@ describe('endWgpuRenderPass', () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
describe('resizeWgpuRenderTarget', () => {
|
|
158
|
+
it('preserves the allocation when dimensions are unchanged', async () => {
|
|
159
|
+
const state = await createWgpuRenderStateForTest();
|
|
160
|
+
const target = createWgpuRenderTarget(state, 64, 64);
|
|
161
|
+
const texture = target.texture;
|
|
162
|
+
const depthStencilTexture = target.depthStencilTexture;
|
|
163
|
+
const bindGroup = target.bindGroup;
|
|
164
|
+
|
|
165
|
+
resizeWgpuRenderTarget(state, target, 64, 64);
|
|
166
|
+
|
|
167
|
+
expect(target.texture).toBe(texture);
|
|
168
|
+
expect(target.depthStencilTexture).toBe(depthStencilTexture);
|
|
169
|
+
expect(target.bindGroup).toBe(bindGroup);
|
|
170
|
+
});
|
|
171
|
+
|
|
158
172
|
it('updates width, height, and bind group', async () => {
|
|
159
173
|
const state = await createWgpuRenderStateForTest();
|
|
160
174
|
const target = createWgpuRenderTarget(state, 64, 64);
|
|
@@ -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);
|