@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
package/src/wgpuDraw.test.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { getOrCreateRenderProxy2D, prepareScene2DRender } from '@flighthq/render/contract';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { createSprite } from '@flighthq/scene2d/contract';
|
|
3
|
+
import { getTextureSource } from '@flighthq/texture/contract';
|
|
4
|
+
import type { Bitmap, CompressedImage, Image, Texture, WgpuTextureEntry } from '@flighthq/types/contract';
|
|
5
|
+
import {
|
|
6
|
+
BitmapTextureSourceKind,
|
|
7
|
+
BlendMode,
|
|
8
|
+
CompressedImageTextureSourceKind,
|
|
9
|
+
ImageTextureSourceKind,
|
|
10
|
+
} from '@flighthq/types/contract';
|
|
5
11
|
|
|
6
12
|
import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
|
|
13
|
+
import { registerWgpuCompressedTextureDecoder, registerWgpuCompressedTextureUpload } from './wgpuCompressedTexture';
|
|
7
14
|
import {
|
|
8
15
|
applyWgpuBlendMode,
|
|
16
|
+
bindWgpuBitmapTexture,
|
|
17
|
+
bindWgpuCompressedImageTexture,
|
|
9
18
|
bindWgpuImageResourceTexture,
|
|
10
19
|
bindWgpuTexture,
|
|
11
20
|
bindWgpuVideoTexture,
|
|
@@ -28,6 +37,47 @@ beforeAll(() => {
|
|
|
28
37
|
installWgpuMock();
|
|
29
38
|
});
|
|
30
39
|
|
|
40
|
+
function requireTextureEntry(entry: WgpuTextureEntry | null): WgpuTextureEntry {
|
|
41
|
+
if (entry === null) throw new Error('Expected a ready WebGPU texture entry');
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bitmap(size: number, version: number): Bitmap {
|
|
46
|
+
return {
|
|
47
|
+
alphaType: 'straight',
|
|
48
|
+
colorSpace: 'srgb',
|
|
49
|
+
data: new Uint8ClampedArray(size * size * 4),
|
|
50
|
+
format: 'rgba8unorm',
|
|
51
|
+
height: size,
|
|
52
|
+
kind: BitmapTextureSourceKind,
|
|
53
|
+
version,
|
|
54
|
+
width: size,
|
|
55
|
+
} as unknown as Bitmap;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compressedBc3Image(): CompressedImage {
|
|
59
|
+
return {
|
|
60
|
+
compressed: {
|
|
61
|
+
container: {
|
|
62
|
+
depth: 1,
|
|
63
|
+
faces: 1,
|
|
64
|
+
format: 'bc3',
|
|
65
|
+
height: 4,
|
|
66
|
+
layers: 1,
|
|
67
|
+
levels: [{ byteLength: 16, byteOffset: 0, height: 4, width: 4 }],
|
|
68
|
+
mipLevels: 1,
|
|
69
|
+
supercompression: 'None',
|
|
70
|
+
width: 4,
|
|
71
|
+
},
|
|
72
|
+
payload: new Uint8Array(16),
|
|
73
|
+
},
|
|
74
|
+
height: 4,
|
|
75
|
+
kind: CompressedImageTextureSourceKind,
|
|
76
|
+
version: 1,
|
|
77
|
+
width: 4,
|
|
78
|
+
} as unknown as CompressedImage;
|
|
79
|
+
}
|
|
80
|
+
|
|
31
81
|
describe('applyWgpuBlendMode', () => {
|
|
32
82
|
it('updates currentBlendMode on the state', async () => {
|
|
33
83
|
const state = await createWgpuRenderStateForTest();
|
|
@@ -42,27 +92,46 @@ describe('applyWgpuBlendMode', () => {
|
|
|
42
92
|
});
|
|
43
93
|
});
|
|
44
94
|
|
|
45
|
-
describe('
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
source: null,
|
|
49
|
-
data: new Uint8ClampedArray(size * size * 4),
|
|
50
|
-
width: size,
|
|
51
|
-
height: size,
|
|
52
|
-
version,
|
|
53
|
-
alphaType: 'straight',
|
|
54
|
-
} as unknown as ImageResource;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
it('uploads a data-only ImageResource (a generated Surface) via writeTexture', async () => {
|
|
95
|
+
describe('bindWgpuBitmapTexture', () => {
|
|
96
|
+
it('uploads CPU-readable pixels via writeTexture', async () => {
|
|
58
97
|
const state = await createWgpuRenderStateForTest();
|
|
59
98
|
const writeTexture = vi.spyOn(state.device.queue, 'writeTexture');
|
|
60
|
-
const entry =
|
|
99
|
+
const entry = bindWgpuBitmapTexture(state, bitmap(4, 1));
|
|
61
100
|
expect(entry.texture).toBeDefined();
|
|
62
101
|
expect(writeTexture).toHaveBeenCalled();
|
|
63
102
|
});
|
|
64
103
|
|
|
65
|
-
it('
|
|
104
|
+
it('caches by resource identity and re-uploads only when the version changes', async () => {
|
|
105
|
+
const state = await createWgpuRenderStateForTest();
|
|
106
|
+
const image = bitmap(2, 1);
|
|
107
|
+
const first = bindWgpuBitmapTexture(state, image);
|
|
108
|
+
expect(bindWgpuBitmapTexture(state, image)).toBe(first);
|
|
109
|
+
const createTexture = vi.spyOn(state.device, 'createTexture');
|
|
110
|
+
image.version = 2;
|
|
111
|
+
bindWgpuBitmapTexture(state, image);
|
|
112
|
+
expect(createTexture).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('bindWgpuCompressedImageTexture', () => {
|
|
117
|
+
it('routes through the registered compressed upload seam', async () => {
|
|
118
|
+
const state = await createWgpuRenderStateForTest();
|
|
119
|
+
const decode = vi.fn(() => new Uint8ClampedArray(4 * 4 * 4));
|
|
120
|
+
registerWgpuCompressedTextureUpload(state);
|
|
121
|
+
registerWgpuCompressedTextureDecoder(state, decode);
|
|
122
|
+
const entry = bindWgpuCompressedImageTexture(state, compressedBc3Image());
|
|
123
|
+
expect(entry?.texture).toBeDefined();
|
|
124
|
+
expect(decode).toHaveBeenCalledWith('bc3', 4, 4, expect.any(Uint8Array));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns null when no compressed uploader is registered', async () => {
|
|
128
|
+
const state = await createWgpuRenderStateForTest();
|
|
129
|
+
expect(bindWgpuCompressedImageTexture(state, compressedBc3Image())).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('bindWgpuImageResourceTexture', () => {
|
|
134
|
+
it('uploads a host-backed image via copyExternalImageToTexture', async () => {
|
|
66
135
|
const state = await createWgpuRenderStateForTest();
|
|
67
136
|
const copy = vi.spyOn(state.device.queue, 'copyExternalImageToTexture');
|
|
68
137
|
const canvas = document.createElement('canvas');
|
|
@@ -70,25 +139,52 @@ describe('bindWgpuImageResourceTexture', () => {
|
|
|
70
139
|
canvas.height = 4;
|
|
71
140
|
const image = {
|
|
72
141
|
source: canvas,
|
|
73
|
-
data: null,
|
|
74
142
|
width: 4,
|
|
75
143
|
height: 4,
|
|
144
|
+
kind: ImageTextureSourceKind,
|
|
76
145
|
version: 1,
|
|
77
|
-
|
|
78
|
-
} as unknown as ImageResource;
|
|
146
|
+
} as unknown as Image;
|
|
79
147
|
bindWgpuImageResourceTexture(state, image);
|
|
80
148
|
expect(copy).toHaveBeenCalled();
|
|
81
149
|
});
|
|
82
150
|
|
|
83
|
-
it('
|
|
151
|
+
it('skips an initial upload when the canvas has no pixels', async () => {
|
|
84
152
|
const state = await createWgpuRenderStateForTest();
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
const copy = vi.spyOn(state.device.queue, 'copyExternalImageToTexture');
|
|
154
|
+
const canvas = document.createElement('canvas');
|
|
155
|
+
canvas.width = 0;
|
|
156
|
+
canvas.height = 4;
|
|
157
|
+
const image = {
|
|
158
|
+
source: canvas,
|
|
159
|
+
width: 0,
|
|
160
|
+
height: 4,
|
|
161
|
+
kind: ImageTextureSourceKind,
|
|
162
|
+
version: 1,
|
|
163
|
+
} as unknown as Image;
|
|
164
|
+
expect(bindWgpuImageResourceTexture(state, image)).toBeNull();
|
|
165
|
+
expect(copy).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('keeps the previous upload when the browser cannot snapshot the current source', async () => {
|
|
169
|
+
const state = await createWgpuRenderStateForTest();
|
|
170
|
+
const copy = vi.spyOn(state.device.queue, 'copyExternalImageToTexture');
|
|
171
|
+
const canvas = document.createElement('canvas');
|
|
172
|
+
canvas.width = 4;
|
|
173
|
+
canvas.height = 4;
|
|
174
|
+
const image = {
|
|
175
|
+
source: canvas,
|
|
176
|
+
width: 4,
|
|
177
|
+
height: 4,
|
|
178
|
+
kind: ImageTextureSourceKind,
|
|
179
|
+
version: 1,
|
|
180
|
+
} as unknown as Image;
|
|
181
|
+
const entry = requireTextureEntry(bindWgpuImageResourceTexture(state, image));
|
|
182
|
+
copy.mockImplementation(() => {
|
|
183
|
+
throw new TypeError('Failed to copy content from external image.');
|
|
184
|
+
});
|
|
89
185
|
image.version = 2;
|
|
90
|
-
bindWgpuImageResourceTexture(state, image);
|
|
91
|
-
expect(
|
|
186
|
+
expect(bindWgpuImageResourceTexture(state, image)).toBe(entry);
|
|
187
|
+
expect(copy).toHaveBeenCalledTimes(2);
|
|
92
188
|
});
|
|
93
189
|
});
|
|
94
190
|
|
|
@@ -98,7 +194,7 @@ describe('bindWgpuTexture', () => {
|
|
|
98
194
|
const canvas = document.createElement('canvas');
|
|
99
195
|
canvas.width = 8;
|
|
100
196
|
canvas.height = 8;
|
|
101
|
-
const entry = bindWgpuTexture(state, canvas);
|
|
197
|
+
const entry = requireTextureEntry(bindWgpuTexture(state, canvas));
|
|
102
198
|
expect(entry).toBeDefined();
|
|
103
199
|
expect(entry.texture).toBeDefined();
|
|
104
200
|
expect(bindWgpuTexture(state, canvas)).toBe(entry);
|
|
@@ -130,7 +226,14 @@ describe('bindWgpuTexture', () => {
|
|
|
130
226
|
|
|
131
227
|
describe('bindWgpuVideoTexture', () => {
|
|
132
228
|
function videoTexture(version: number, readyState = 4, width = 320, height = 240): Texture {
|
|
229
|
+
const source = document.createElement('video');
|
|
230
|
+
Object.defineProperties(source, {
|
|
231
|
+
readyState: { configurable: true, value: readyState, writable: true },
|
|
232
|
+
videoHeight: { configurable: true, value: height, writable: true },
|
|
233
|
+
videoWidth: { configurable: true, value: width, writable: true },
|
|
234
|
+
});
|
|
133
235
|
return {
|
|
236
|
+
colorSpace: 'srgb',
|
|
134
237
|
sampler: {
|
|
135
238
|
anisotropy: 1,
|
|
136
239
|
magFilter: 'linear',
|
|
@@ -139,18 +242,16 @@ describe('bindWgpuVideoTexture', () => {
|
|
|
139
242
|
wrapU: 'clamp-to-edge',
|
|
140
243
|
wrapV: 'clamp-to-edge',
|
|
141
244
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
version,
|
|
147
|
-
},
|
|
245
|
+
dimension: '2d',
|
|
246
|
+
source: {
|
|
247
|
+
source,
|
|
248
|
+
version,
|
|
148
249
|
},
|
|
149
250
|
version,
|
|
150
251
|
} as unknown as Texture;
|
|
151
252
|
}
|
|
152
253
|
|
|
153
|
-
it('uploads once per
|
|
254
|
+
it('uploads once per source version and caches the texture by source identity', async () => {
|
|
154
255
|
const state = await createWgpuRenderStateForTest();
|
|
155
256
|
const copy = vi.spyOn(state.device.queue, 'copyExternalImageToTexture');
|
|
156
257
|
const video = videoTexture(1);
|
|
@@ -159,7 +260,7 @@ describe('bindWgpuVideoTexture', () => {
|
|
|
159
260
|
expect(copy).toHaveBeenCalledTimes(1);
|
|
160
261
|
expect(bindWgpuVideoTexture(state, video)).toBe(first);
|
|
161
262
|
expect(copy).toHaveBeenCalledTimes(1);
|
|
162
|
-
video
|
|
263
|
+
getTextureSource(video)!.version = 2;
|
|
163
264
|
expect(bindWgpuVideoTexture(state, video)).toBe(first);
|
|
164
265
|
expect(copy).toHaveBeenCalledTimes(2);
|
|
165
266
|
});
|
|
@@ -168,7 +269,7 @@ describe('bindWgpuVideoTexture', () => {
|
|
|
168
269
|
const state = await createWgpuRenderStateForTest();
|
|
169
270
|
const video = videoTexture(1, 1, 0, 0);
|
|
170
271
|
expect(bindWgpuVideoTexture(state, video)).toBeNull();
|
|
171
|
-
const element = video.
|
|
272
|
+
const element = (getTextureSource(video) as Image).source as HTMLVideoElement;
|
|
172
273
|
Object.assign(element, { readyState: 4, videoHeight: 120, videoWidth: 160 });
|
|
173
274
|
const first = bindWgpuVideoTexture(state, video)!;
|
|
174
275
|
const destroy = vi.spyOn(first.texture, 'destroy');
|
|
@@ -178,6 +279,18 @@ describe('bindWgpuVideoTexture', () => {
|
|
|
178
279
|
expect(destroy).toHaveBeenCalled();
|
|
179
280
|
});
|
|
180
281
|
|
|
282
|
+
it('realizes distinct linear and sRGB textures for one video source', async () => {
|
|
283
|
+
const state = await createWgpuRenderStateForTest();
|
|
284
|
+
const createTexture = vi.spyOn(state.device, 'createTexture');
|
|
285
|
+
const video = videoTexture(1);
|
|
286
|
+
const srgb = bindWgpuVideoTexture(state, video);
|
|
287
|
+
expect(createTexture).toHaveBeenLastCalledWith(expect.objectContaining({ format: 'rgba8unorm-srgb' }));
|
|
288
|
+
video.colorSpace = 'linear';
|
|
289
|
+
const linear = bindWgpuVideoTexture(state, video);
|
|
290
|
+
expect(linear).not.toBe(srgb);
|
|
291
|
+
expect(createTexture).toHaveBeenLastCalledWith(expect.objectContaining({ format: 'rgba8unorm' }));
|
|
292
|
+
});
|
|
293
|
+
|
|
181
294
|
it('destroys and removes an uploaded video texture', async () => {
|
|
182
295
|
const state = await createWgpuRenderStateForTest();
|
|
183
296
|
const video = videoTexture(1);
|
|
@@ -204,7 +317,7 @@ describe('createWgpuTextureEntry', () => {
|
|
|
204
317
|
const canvas = document.createElement('canvas');
|
|
205
318
|
canvas.width = 4;
|
|
206
319
|
canvas.height = 4;
|
|
207
|
-
const entry = createWgpuTextureEntry(state, 4, 4, canvas);
|
|
320
|
+
const entry = requireTextureEntry(createWgpuTextureEntry(state, 4, 4, canvas));
|
|
208
321
|
expect(entry.texture).toBeDefined();
|
|
209
322
|
expect(entry.bindGroup).toBeDefined();
|
|
210
323
|
});
|
|
@@ -220,13 +333,13 @@ describe('drawWgpuQuad', () => {
|
|
|
220
333
|
it('does not throw when render pass is open', async () => {
|
|
221
334
|
const state = await createWgpuRenderStateForTest();
|
|
222
335
|
renderWgpuBackground(state);
|
|
223
|
-
const bitmap =
|
|
336
|
+
const bitmap = createSprite();
|
|
224
337
|
prepareScene2DRender(state, bitmap);
|
|
225
338
|
const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
|
|
226
339
|
const canvas = document.createElement('canvas');
|
|
227
340
|
canvas.width = 4;
|
|
228
341
|
canvas.height = 4;
|
|
229
|
-
const entry = bindWgpuTexture(state, canvas);
|
|
342
|
+
const entry = requireTextureEntry(bindWgpuTexture(state, canvas));
|
|
230
343
|
expect(() => drawWgpuQuad(state, renderProxy, entry, 0, 0, 4, 4, 0, 0, 1, 1)).not.toThrow();
|
|
231
344
|
submitWgpuRenderPass(state);
|
|
232
345
|
});
|
|
@@ -236,13 +349,13 @@ describe('drawWgpuQuadWithTransform', () => {
|
|
|
236
349
|
it('does not throw when render pass is open', async () => {
|
|
237
350
|
const state = await createWgpuRenderStateForTest();
|
|
238
351
|
renderWgpuBackground(state);
|
|
239
|
-
const bitmap =
|
|
352
|
+
const bitmap = createSprite();
|
|
240
353
|
prepareScene2DRender(state, bitmap);
|
|
241
354
|
const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
|
|
242
355
|
const canvas = document.createElement('canvas');
|
|
243
356
|
canvas.width = 4;
|
|
244
357
|
canvas.height = 4;
|
|
245
|
-
const entry = bindWgpuTexture(state, canvas);
|
|
358
|
+
const entry = requireTextureEntry(bindWgpuTexture(state, canvas));
|
|
246
359
|
const t = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
247
360
|
expect(() => drawWgpuQuadWithTransform(state, renderProxy, t, entry, 0, 0, 4, 4, 0, 0, 1, 1)).not.toThrow();
|
|
248
361
|
submitWgpuRenderPass(state);
|
|
@@ -270,20 +383,9 @@ describe('getWgpuRenderProxyColorScaleBias', () => {
|
|
|
270
383
|
});
|
|
271
384
|
|
|
272
385
|
describe('resolveWgpuSmoothingBindGroup', () => {
|
|
273
|
-
function dataResource(size: number, version: number): ImageResource {
|
|
274
|
-
return {
|
|
275
|
-
source: null,
|
|
276
|
-
data: new Uint8ClampedArray(size * size * 4),
|
|
277
|
-
width: size,
|
|
278
|
-
height: size,
|
|
279
|
-
version,
|
|
280
|
-
alphaType: 'straight',
|
|
281
|
-
} as unknown as ImageResource;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
386
|
it('returns the default bind group for a null smoothing (no variant built)', async () => {
|
|
285
387
|
const state = await createWgpuRenderStateForTest();
|
|
286
|
-
const entry =
|
|
388
|
+
const entry = bindWgpuBitmapTexture(state, bitmap(4, 1));
|
|
287
389
|
expect(resolveWgpuSmoothingBindGroup(state, entry, null)).toBe(entry.bindGroup);
|
|
288
390
|
expect(entry.bindGroupLinear).toBeUndefined();
|
|
289
391
|
expect(entry.bindGroupNearest).toBeUndefined();
|
|
@@ -291,7 +393,7 @@ describe('resolveWgpuSmoothingBindGroup', () => {
|
|
|
291
393
|
|
|
292
394
|
it('builds and caches distinct LINEAR and NEAREST variants for true/false', async () => {
|
|
293
395
|
const state = await createWgpuRenderStateForTest();
|
|
294
|
-
const entry =
|
|
396
|
+
const entry = bindWgpuBitmapTexture(state, bitmap(4, 1));
|
|
295
397
|
const createBindGroup = vi.spyOn(state.device, 'createBindGroup');
|
|
296
398
|
|
|
297
399
|
resolveWgpuSmoothingBindGroup(state, entry, true);
|
|
@@ -310,13 +412,13 @@ describe('resolveWgpuSmoothingBindGroup', () => {
|
|
|
310
412
|
|
|
311
413
|
it('drops the cached variants when the texture re-uploads (version bump)', async () => {
|
|
312
414
|
const state = await createWgpuRenderStateForTest();
|
|
313
|
-
const image =
|
|
314
|
-
const entry =
|
|
415
|
+
const image = bitmap(4, 1);
|
|
416
|
+
const entry = bindWgpuBitmapTexture(state, image);
|
|
315
417
|
resolveWgpuSmoothingBindGroup(state, entry, true);
|
|
316
418
|
resolveWgpuSmoothingBindGroup(state, entry, false);
|
|
317
419
|
expect(entry.bindGroupLinear).toBeDefined();
|
|
318
420
|
image.version = 2;
|
|
319
|
-
|
|
421
|
+
bindWgpuBitmapTexture(state, image);
|
|
320
422
|
expect(entry.bindGroupLinear).toBeUndefined();
|
|
321
423
|
expect(entry.bindGroupNearest).toBeUndefined();
|
|
322
424
|
});
|
|
@@ -329,7 +431,7 @@ describe('submitWgpuQuadDraw', () => {
|
|
|
329
431
|
const canvas = document.createElement('canvas');
|
|
330
432
|
canvas.width = 4;
|
|
331
433
|
canvas.height = 4;
|
|
332
|
-
const entry = bindWgpuTexture(state, canvas);
|
|
434
|
+
const entry = requireTextureEntry(bindWgpuTexture(state, canvas));
|
|
333
435
|
expect(() => submitWgpuQuadDraw(state, 0, entry.bindGroup)).not.toThrow();
|
|
334
436
|
submitWgpuRenderPass(state);
|
|
335
437
|
});
|
|
@@ -339,7 +441,7 @@ describe('submitWgpuQuadDraw', () => {
|
|
|
339
441
|
const canvas = document.createElement('canvas');
|
|
340
442
|
canvas.width = 4;
|
|
341
443
|
canvas.height = 4;
|
|
342
|
-
const entry = bindWgpuTexture(state, canvas);
|
|
444
|
+
const entry = requireTextureEntry(bindWgpuTexture(state, canvas));
|
|
343
445
|
expect(() => submitWgpuQuadDraw(state, 0, entry.bindGroup)).not.toThrow();
|
|
344
446
|
});
|
|
345
447
|
});
|
|
@@ -350,7 +452,7 @@ describe('updateWgpuTextureEntry', () => {
|
|
|
350
452
|
const canvas = document.createElement('canvas');
|
|
351
453
|
canvas.width = 4;
|
|
352
454
|
canvas.height = 4;
|
|
353
|
-
const entry = createWgpuTextureEntry(state, 4, 4, canvas);
|
|
455
|
+
const entry = requireTextureEntry(createWgpuTextureEntry(state, 4, 4, canvas));
|
|
354
456
|
expect(() => updateWgpuTextureEntry(state, entry, canvas)).not.toThrow();
|
|
355
457
|
});
|
|
356
458
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { isWgpuExternalImageSourceReady, tryCopyWgpuExternalImageToTexture } from './wgpuExternalImageSource';
|
|
2
|
+
import { createReadyImageElementForTest } from './wgpuTestHelper';
|
|
3
|
+
|
|
4
|
+
describe('isWgpuExternalImageSourceReady', () => {
|
|
5
|
+
it('accepts a dimension-valid canvas without probing or binding its context', () => {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.width = 4;
|
|
8
|
+
canvas.height = 4;
|
|
9
|
+
const getContext = vi.spyOn(canvas, 'getContext');
|
|
10
|
+
expect(isWgpuExternalImageSourceReady(canvas, 4, 4)).toBe(true);
|
|
11
|
+
expect(getContext).not.toHaveBeenCalled();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('accepts a decoded image with enough pixels', () => {
|
|
15
|
+
const image = createReadyImageElementForTest(4, 4);
|
|
16
|
+
expect(isWgpuExternalImageSourceReady(image, 4, 4)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('rejects zero extents and undersized sources', () => {
|
|
20
|
+
const canvas = document.createElement('canvas');
|
|
21
|
+
canvas.width = 4;
|
|
22
|
+
canvas.height = 4;
|
|
23
|
+
expect(isWgpuExternalImageSourceReady(canvas, 0, 4)).toBe(false);
|
|
24
|
+
expect(isWgpuExternalImageSourceReady(canvas, 5, 4)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects an undecoded image element', () => {
|
|
28
|
+
const image = document.createElement('img');
|
|
29
|
+
expect(isWgpuExternalImageSourceReady(image, 1, 1)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('rejects an unrecognized source instead of failing open', () => {
|
|
33
|
+
expect(isWgpuExternalImageSourceReady({} as GPUCopyExternalImageSource, 1, 1)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('tryCopyWgpuExternalImageToTexture', () => {
|
|
38
|
+
it('copies a ready source', () => {
|
|
39
|
+
const copyExternalImageToTexture = vi.fn();
|
|
40
|
+
const queue = { copyExternalImageToTexture } as unknown as GPUQueue;
|
|
41
|
+
const texture = {} as GPUTexture;
|
|
42
|
+
const source = createReadyImageElementForTest(4, 4);
|
|
43
|
+
expect(tryCopyWgpuExternalImageToTexture(queue, { source }, { texture }, 4, 4)).toBe(true);
|
|
44
|
+
expect(copyExternalImageToTexture).toHaveBeenCalledWith({ source }, { texture }, [4, 4, 1]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('fails closed when a canvas has no usable context', () => {
|
|
48
|
+
const canvas = document.createElement('canvas');
|
|
49
|
+
canvas.width = 4;
|
|
50
|
+
canvas.height = 4;
|
|
51
|
+
const copyExternalImageToTexture = vi.fn(() => {
|
|
52
|
+
throw new DOMException('canvas has no rendering context', 'OperationError');
|
|
53
|
+
});
|
|
54
|
+
const queue = { copyExternalImageToTexture } as unknown as GPUQueue;
|
|
55
|
+
expect(tryCopyWgpuExternalImageToTexture(queue, { source: canvas }, { texture: {} as GPUTexture }, 4, 4)).toBe(
|
|
56
|
+
false,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('fails closed when the browser cannot snapshot current canvas content', () => {
|
|
61
|
+
const canvas = document.createElement('canvas');
|
|
62
|
+
canvas.width = 4;
|
|
63
|
+
canvas.height = 4;
|
|
64
|
+
const copyExternalImageToTexture = vi.fn(() => {
|
|
65
|
+
throw new TypeError(
|
|
66
|
+
"Failed to execute 'copyExternalImageToTexture' on 'GPUQueue': Failed to copy content from external image.",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
const queue = { copyExternalImageToTexture } as unknown as GPUQueue;
|
|
70
|
+
expect(tryCopyWgpuExternalImageToTexture(queue, { source: canvas }, { texture: {} as GPUTexture }, 4, 4)).toBe(
|
|
71
|
+
false,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rethrows security failures', () => {
|
|
76
|
+
const canvas = document.createElement('canvas');
|
|
77
|
+
canvas.width = 4;
|
|
78
|
+
canvas.height = 4;
|
|
79
|
+
const error = new DOMException('tainted canvas', 'SecurityError');
|
|
80
|
+
const queue = {
|
|
81
|
+
copyExternalImageToTexture: vi.fn(() => {
|
|
82
|
+
throw error;
|
|
83
|
+
}),
|
|
84
|
+
} as unknown as GPUQueue;
|
|
85
|
+
expect(() =>
|
|
86
|
+
tryCopyWgpuExternalImageToTexture(queue, { source: canvas }, { texture: {} as GPUTexture }, 4, 4),
|
|
87
|
+
).toThrow(error);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getTextureSourceKind } from '@flighthq/texture/contract';
|
|
2
|
+
import { ExternalTextureSourceKind } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import { createExternalWgpuTexture, disposeExternalWgpuTexture } from './wgpuExternalTexture';
|
|
5
|
+
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
6
|
+
import { resolveWgpuTexture } from './wgpuTextureResolver';
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
installWgpuMock();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('createExternalWgpuTexture', () => {
|
|
13
|
+
it('resolves a borrowed handle and forgets it without destroying the allocation', async () => {
|
|
14
|
+
const state = await createWgpuRenderStateForTest();
|
|
15
|
+
const destroy = vi.fn();
|
|
16
|
+
const handle = { createView: vi.fn(() => ({})), destroy } as unknown as GPUTexture;
|
|
17
|
+
const texture = createExternalWgpuTexture(state, handle, { height: 16, width: 32 });
|
|
18
|
+
|
|
19
|
+
expect(getTextureSourceKind(texture)).toBe(ExternalTextureSourceKind);
|
|
20
|
+
expect(resolveWgpuTexture(state, texture)?.texture).toBe(handle);
|
|
21
|
+
expect(disposeExternalWgpuTexture(state, texture)).toBe(true);
|
|
22
|
+
expect(resolveWgpuTexture(state, texture)).toBeNull();
|
|
23
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('disposeExternalWgpuTexture', () => {
|
|
28
|
+
it('returns false after the borrowed handle has already been forgotten', async () => {
|
|
29
|
+
const state = await createWgpuRenderStateForTest();
|
|
30
|
+
const handle = { createView: () => ({}) } as unknown as GPUTexture;
|
|
31
|
+
const texture = createExternalWgpuTexture(state, handle, { height: 1, width: 1 });
|
|
32
|
+
expect(disposeExternalWgpuTexture(state, texture)).toBe(true);
|
|
33
|
+
expect(disposeExternalWgpuTexture(state, texture)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import { enableRenderRegistryGuards, explainRenderRegistryMisses } from '@flighthq/render/contract';
|
|
1
3
|
import type { Material, WgpuMaterialRenderer, WgpuRenderState } from '@flighthq/types/contract';
|
|
2
|
-
import { StandardMaterialKind, EntityRuntimeKey } from '@flighthq/types/contract';
|
|
4
|
+
import { StandardMaterialKind, EntityRuntimeKey, RenderRegistry } from '@flighthq/types/contract';
|
|
3
5
|
|
|
4
6
|
import {
|
|
5
7
|
getWgpuMaterialRenderer,
|
|
@@ -12,7 +14,8 @@ const TestKind = 'TestMaterial';
|
|
|
12
14
|
const testRenderer: WgpuMaterialRenderer = { instanceFloatCount: 0, getShaderModule: () => ({}) as GPUShaderModule };
|
|
13
15
|
|
|
14
16
|
function makeState(): WgpuRenderState {
|
|
15
|
-
|
|
17
|
+
// `device` is what the guard's message table dispatches on to name the wgpu registrar.
|
|
18
|
+
const state = { device: {} } as WgpuRenderState;
|
|
16
19
|
state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
|
|
17
20
|
return state;
|
|
18
21
|
}
|
|
@@ -53,4 +56,27 @@ describe('resolveWgpuMaterialRenderer', () => {
|
|
|
53
56
|
expect(resolveWgpuMaterialRenderer(state, makeMaterial('Other'))).toBe(testRenderer);
|
|
54
57
|
expect(resolveWgpuMaterialRenderer(state, null)).toBe(testRenderer);
|
|
55
58
|
});
|
|
59
|
+
|
|
60
|
+
it('reports the missing kind against the wgpu registrar, not the gl one', () => {
|
|
61
|
+
const state = makeState();
|
|
62
|
+
enableRenderRegistryGuards(state);
|
|
63
|
+
const sink = createMemoryLogSink(4);
|
|
64
|
+
addLogSink(sink.sink);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
resolveWgpuMaterialRenderer(state, makeMaterial(TestKind));
|
|
68
|
+
|
|
69
|
+
expect(explainRenderRegistryMisses(state).misses).toEqual([
|
|
70
|
+
{ kind: TestKind, registry: RenderRegistry.MaterialRenderer },
|
|
71
|
+
]);
|
|
72
|
+
expect(getMemoryLogSinkEntries(sink)[0]?.data).toMatchObject({
|
|
73
|
+
kind: TestKind,
|
|
74
|
+
message:
|
|
75
|
+
'resolveWgpuMaterialRenderer: material kind has no registered renderer, so nodes using it do not draw — call registerWgpuMaterialRenderer(state, kind, renderer)',
|
|
76
|
+
registry: RenderRegistry.MaterialRenderer,
|
|
77
|
+
});
|
|
78
|
+
} finally {
|
|
79
|
+
removeLogSink(sink.sink);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
56
82
|
});
|
package/src/wgpuMipmap.test.ts
CHANGED
|
@@ -53,10 +53,18 @@ describe('generateWgpuMipmaps', () => {
|
|
|
53
53
|
const state = await createWgpuRenderStateForTest();
|
|
54
54
|
spyEncoderPasses(state);
|
|
55
55
|
generateWgpuMipmaps(state, makeTextureSpy(), 4, 4, 'rgba8unorm');
|
|
56
|
-
const pipeline = getWgpuRenderStateRuntime(state).
|
|
56
|
+
const pipeline = getWgpuRenderStateRuntime(state).mipmapPipelineCache.get('rgba8unorm')?.pipeline;
|
|
57
57
|
expect(pipeline).toBeDefined();
|
|
58
58
|
generateWgpuMipmaps(state, makeTextureSpy(), 8, 8, 'rgba8unorm');
|
|
59
|
-
expect(getWgpuRenderStateRuntime(state).
|
|
59
|
+
expect(getWgpuRenderStateRuntime(state).mipmapPipelineCache.get('rgba8unorm')?.pipeline).toBe(pipeline);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('keeps linear and sRGB downsample pipelines distinct', async () => {
|
|
63
|
+
const state = await createWgpuRenderStateForTest();
|
|
64
|
+
spyEncoderPasses(state);
|
|
65
|
+
generateWgpuMipmaps(state, makeTextureSpy(), 4, 4, 'rgba8unorm');
|
|
66
|
+
generateWgpuMipmaps(state, makeTextureSpy(), 4, 4, 'rgba8unorm-srgb');
|
|
67
|
+
expect(getWgpuRenderStateRuntime(state).mipmapPipelineCache.size).toBe(2);
|
|
60
68
|
});
|
|
61
69
|
});
|
|
62
70
|
|