@flighthq/render-wgpu 0.3.1-next.975.01aea94 → 0.4.0-edge.1908.751021c
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 +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableWgpuTextureResolverGuards.d.ts.map +1 -1
- package/dist/enableWgpuTextureResolverGuards.js +11 -0
- package/dist/enableWgpuTextureResolverGuards.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/wgpuAntialias.d.ts +10 -0
- package/dist/wgpuAntialias.d.ts.map +1 -0
- package/dist/wgpuAntialias.js +136 -0
- package/dist/wgpuAntialias.js.map +1 -0
- package/dist/wgpuBackground.d.ts.map +1 -1
- package/dist/wgpuBackground.js +12 -4
- package/dist/wgpuBackground.js.map +1 -1
- package/dist/wgpuDraw.d.ts.map +1 -1
- package/dist/wgpuDraw.js +21 -11
- package/dist/wgpuDraw.js.map +1 -1
- package/dist/wgpuMipmap.d.ts +1 -0
- package/dist/wgpuMipmap.d.ts.map +1 -1
- package/dist/wgpuMipmap.js +3 -0
- package/dist/wgpuMipmap.js.map +1 -1
- package/dist/wgpuRenderState.d.ts.map +1 -1
- package/dist/wgpuRenderState.js +15 -0
- package/dist/wgpuRenderState.js.map +1 -1
- package/dist/wgpuRenderTarget.d.ts +16 -2
- package/dist/wgpuRenderTarget.d.ts.map +1 -1
- package/dist/wgpuRenderTarget.js +52 -9
- package/dist/wgpuRenderTarget.js.map +1 -1
- package/dist/wgpuRenderTargetPool.d.ts +1 -0
- package/dist/wgpuRenderTargetPool.d.ts.map +1 -1
- package/dist/wgpuRenderTargetPool.js +9 -6
- package/dist/wgpuRenderTargetPool.js.map +1 -1
- package/dist/wgpuRenderTexture.js +3 -3
- package/dist/wgpuRenderTexture.js.map +1 -1
- package/dist/wgpuScissor.d.ts +1 -0
- package/dist/wgpuScissor.d.ts.map +1 -1
- package/dist/wgpuScissor.js +8 -1
- package/dist/wgpuScissor.js.map +1 -1
- package/dist/wgpuTestHelper.d.ts +2 -2
- package/dist/wgpuTestHelper.d.ts.map +1 -1
- package/dist/wgpuTestHelper.js +2 -2
- package/dist/wgpuTestHelper.js.map +1 -1
- package/package.json +12 -12
- package/src/enableWgpuTextureResolverGuards.test.ts +30 -0
- package/src/wgpuAntialias.test.ts +217 -0
- package/src/wgpuDraw.test.ts +35 -0
- package/src/wgpuMipmap.test.ts +11 -1
- package/src/wgpuRenderTarget.test.ts +53 -0
- package/src/wgpuRenderTargetPool.test.ts +16 -0
- package/src/wgpuRenderTexture.test.ts +13 -0
- package/src/wgpuScissor.test.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/render-wgpu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-edge.1908.751021c",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,17 +38,17 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/color": "0.
|
|
42
|
-
"@flighthq/entity": "0.
|
|
43
|
-
"@flighthq/scene2d": "0.
|
|
44
|
-
"@flighthq/geometry": "0.
|
|
45
|
-
"@flighthq/log": "0.
|
|
46
|
-
"@flighthq/render": "0.
|
|
47
|
-
"@flighthq/registry": "0.
|
|
48
|
-
"@flighthq/signals": "0.
|
|
49
|
-
"@flighthq/bitmap": "0.
|
|
50
|
-
"@flighthq/texture": "0.
|
|
51
|
-
"@flighthq/types": "0.
|
|
41
|
+
"@flighthq/color": "0.4.0-edge.1908.751021c",
|
|
42
|
+
"@flighthq/entity": "0.4.0-edge.1908.751021c",
|
|
43
|
+
"@flighthq/scene2d": "0.4.0-edge.1908.751021c",
|
|
44
|
+
"@flighthq/geometry": "0.4.0-edge.1908.751021c",
|
|
45
|
+
"@flighthq/log": "0.4.0-edge.1908.751021c",
|
|
46
|
+
"@flighthq/render": "0.4.0-edge.1908.751021c",
|
|
47
|
+
"@flighthq/registry": "0.4.0-edge.1908.751021c",
|
|
48
|
+
"@flighthq/signals": "0.4.0-edge.1908.751021c",
|
|
49
|
+
"@flighthq/bitmap": "0.4.0-edge.1908.751021c",
|
|
50
|
+
"@flighthq/texture": "0.4.0-edge.1908.751021c",
|
|
51
|
+
"@flighthq/types": "0.4.0-edge.1908.751021c"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@webgpu/types": "^0.1.70"
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
areWgpuTextureResolverGuardsEnabled,
|
|
7
7
|
enableWgpuTextureResolverGuards,
|
|
8
8
|
} from './enableWgpuTextureResolverGuards';
|
|
9
|
+
import { bindWgpuTexture } from './wgpuDraw';
|
|
10
|
+
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
9
11
|
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
10
12
|
import { registerWgpuTextureResolver, resolveWgpuTexture } from './wgpuTextureResolver';
|
|
11
13
|
|
|
@@ -55,6 +57,34 @@ describe('enableWgpuTextureResolverGuards', () => {
|
|
|
55
57
|
removeLogSink(sink.sink);
|
|
56
58
|
}
|
|
57
59
|
});
|
|
60
|
+
|
|
61
|
+
it('installs the mipmap-degraded guard on the runtime', async () => {
|
|
62
|
+
const state = await createWgpuRenderStateForTest();
|
|
63
|
+
expect(getWgpuRenderStateRuntime(state).mipmapDegradedGuard).toBeNull();
|
|
64
|
+
enableWgpuTextureResolverGuards(state);
|
|
65
|
+
expect(getWgpuRenderStateRuntime(state).mipmapDegradedGuard).not.toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('logs a warning when mipmaps are requested without a registered generator', async () => {
|
|
69
|
+
const state = await createWgpuRenderStateForTest();
|
|
70
|
+
const sink = createMemoryLogSink(4);
|
|
71
|
+
addLogSink(sink.sink);
|
|
72
|
+
enableWgpuTextureResolverGuards(state);
|
|
73
|
+
try {
|
|
74
|
+
const canvas = document.createElement('canvas');
|
|
75
|
+
canvas.width = 8;
|
|
76
|
+
canvas.height = 8;
|
|
77
|
+
bindWgpuTexture(state, canvas, true);
|
|
78
|
+
|
|
79
|
+
const entries = getMemoryLogSinkEntries(sink);
|
|
80
|
+
expect(entries).toHaveLength(1);
|
|
81
|
+
expect(entries[0]?.data).toMatchObject({
|
|
82
|
+
message: expect.stringContaining('no WGPU mipmap generator registered'),
|
|
83
|
+
});
|
|
84
|
+
} finally {
|
|
85
|
+
removeLogSink(sink.sink);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
58
88
|
});
|
|
59
89
|
|
|
60
90
|
function textureWithKind(kind: string) {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { WgpuRenderTarget } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
acquireWgpuSurfaceAntialiasView,
|
|
5
|
+
clearWgpuSurfacePresentation,
|
|
6
|
+
encodeWgpuSurfaceAntialiasResolve,
|
|
7
|
+
getWgpuSurfaceRenderExtent,
|
|
8
|
+
getWgpuSurfaceRenderScale,
|
|
9
|
+
} from './wgpuAntialias';
|
|
10
|
+
import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
|
|
11
|
+
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
12
|
+
import { setWgpuRenderPassScissorRect } from './wgpuScissor';
|
|
13
|
+
import { enableWgpuFrameCapture } from './wgpuSurface';
|
|
14
|
+
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
installWgpuMock();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('acquireWgpuSurfaceAntialiasView', () => {
|
|
21
|
+
it('returns and retains a 2x render view when the option is enabled', async () => {
|
|
22
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
23
|
+
const presentationView = state.context.getCurrentTexture().createView();
|
|
24
|
+
|
|
25
|
+
const view = acquireWgpuSurfaceAntialiasView(state, presentationView);
|
|
26
|
+
|
|
27
|
+
expect(view).toBe(getWgpuRenderStateRuntime(state).surfaceAntialiasView);
|
|
28
|
+
expect(getWgpuRenderStateRuntime(state).surfacePresentationView).toBe(presentationView);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('clearWgpuSurfacePresentation', () => {
|
|
33
|
+
it('drops the per-frame presentation view without destroying reusable resolve resources', async () => {
|
|
34
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
35
|
+
acquireWgpuSurfaceAntialiasView(state, state.context.getCurrentTexture().createView());
|
|
36
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
37
|
+
const texture = runtime.surfaceAntialiasTexture;
|
|
38
|
+
|
|
39
|
+
clearWgpuSurfacePresentation(state);
|
|
40
|
+
|
|
41
|
+
expect(runtime.surfacePresentationView).toBeNull();
|
|
42
|
+
expect(runtime.surfaceAntialiasTexture).toBe(texture);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('encodeWgpuSurfaceAntialiasResolve', () => {
|
|
47
|
+
it('draws one fullscreen triangle into the retained presentation view', async () => {
|
|
48
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
49
|
+
acquireWgpuSurfaceAntialiasView(state, state.context.getCurrentTexture().createView());
|
|
50
|
+
const draw = vi.fn();
|
|
51
|
+
const beginRenderPass = vi.fn(() => ({
|
|
52
|
+
draw,
|
|
53
|
+
end: () => {},
|
|
54
|
+
setBindGroup: () => {},
|
|
55
|
+
setPipeline: () => {},
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
encodeWgpuSurfaceAntialiasResolve(state, { beginRenderPass } as unknown as GPUCommandEncoder);
|
|
59
|
+
|
|
60
|
+
expect(beginRenderPass).toHaveBeenCalledOnce();
|
|
61
|
+
expect(draw).toHaveBeenCalledWith(3);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('getWgpuSurfaceRenderExtent', () => {
|
|
66
|
+
it('switches from the logical canvas extent to the acquired supersample extent', async () => {
|
|
67
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
68
|
+
expect(getWgpuSurfaceRenderExtent(state)).toEqual({ width: 800, height: 600 });
|
|
69
|
+
|
|
70
|
+
acquireWgpuSurfaceAntialiasView(state, state.context.getCurrentTexture().createView());
|
|
71
|
+
|
|
72
|
+
expect(getWgpuSurfaceRenderExtent(state)).toEqual({ width: 1600, height: 1200 });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('getWgpuSurfaceRenderScale', () => {
|
|
77
|
+
it('scales only the acquired main surface', async () => {
|
|
78
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
79
|
+
acquireWgpuSurfaceAntialiasView(state, state.context.getCurrentTexture().createView());
|
|
80
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
81
|
+
|
|
82
|
+
expect(getWgpuSurfaceRenderScale(state)).toBe(2);
|
|
83
|
+
runtime.currentRenderTarget = {} as WgpuRenderTarget;
|
|
84
|
+
expect(getWgpuSurfaceRenderScale(state)).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('WgpuRenderOptions.antialias', () => {
|
|
89
|
+
it('leaves the direct presentation path and all resolve resources off by default', async () => {
|
|
90
|
+
const state = await createWgpuRenderStateForTest();
|
|
91
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
92
|
+
const { encoder, setViewport } = installRecordingEncoder(state);
|
|
93
|
+
|
|
94
|
+
renderWgpuBackground(state);
|
|
95
|
+
|
|
96
|
+
expect(runtime.surfaceAntialiasEnabled).toBe(false);
|
|
97
|
+
expect(runtime.surfaceAntialiasTexture).toBeNull();
|
|
98
|
+
expect(runtime.surfaceAntialiasResolvePipeline).toBeNull();
|
|
99
|
+
expect(runtime.surfacePresentationView).toBeNull();
|
|
100
|
+
expect(setViewport).toHaveBeenCalledWith(0, 0, 800, 600, 0, 1);
|
|
101
|
+
expect(encoder.beginRenderPass).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('renders the main pass into a 2x surface while preserving logical canvas coordinates', async () => {
|
|
105
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
106
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
107
|
+
const { setViewport } = installRecordingEncoder(state);
|
|
108
|
+
|
|
109
|
+
renderWgpuBackground(state);
|
|
110
|
+
|
|
111
|
+
expect(runtime.surfaceAntialiasTexture?.width).toBe(1600);
|
|
112
|
+
expect(runtime.surfaceAntialiasTexture?.height).toBe(1200);
|
|
113
|
+
expect(runtime.depthStencilWidth).toBe(1600);
|
|
114
|
+
expect(runtime.depthStencilHeight).toBe(1200);
|
|
115
|
+
expect(runtime.renderTargetViewport).toBeNull();
|
|
116
|
+
expect(runtime.surfacePresentationView).not.toBeNull();
|
|
117
|
+
expect(setViewport).toHaveBeenCalledWith(0, 0, 1600, 1200, 0, 1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('encodes one fullscreen resolve before frame-capture readback', async () => {
|
|
121
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
122
|
+
const events: string[] = [];
|
|
123
|
+
let passIndex = 0;
|
|
124
|
+
const encoder = {
|
|
125
|
+
beginRenderPass: vi.fn(() => {
|
|
126
|
+
const name = passIndex++ === 0 ? 'main' : 'resolve';
|
|
127
|
+
events.push(`${name}:begin`);
|
|
128
|
+
return {
|
|
129
|
+
draw: (vertices: number) => events.push(`${name}:draw:${vertices}`),
|
|
130
|
+
end: () => events.push(`${name}:end`),
|
|
131
|
+
setBindGroup: () => {},
|
|
132
|
+
setPipeline: () => {},
|
|
133
|
+
setScissorRect: () => {},
|
|
134
|
+
setStencilReference: () => {},
|
|
135
|
+
setViewport: () => {},
|
|
136
|
+
} as unknown as GPURenderPassEncoder;
|
|
137
|
+
}),
|
|
138
|
+
copyTextureToBuffer: () => events.push('capture:copy'),
|
|
139
|
+
finish: () => {
|
|
140
|
+
events.push('encoder:finish');
|
|
141
|
+
return {} as GPUCommandBuffer;
|
|
142
|
+
},
|
|
143
|
+
} as unknown as GPUCommandEncoder;
|
|
144
|
+
vi.spyOn(state.device, 'createCommandEncoder').mockReturnValue(encoder);
|
|
145
|
+
enableWgpuFrameCapture(state);
|
|
146
|
+
|
|
147
|
+
renderWgpuBackground(state);
|
|
148
|
+
submitWgpuRenderPass(state);
|
|
149
|
+
|
|
150
|
+
expect(events).toEqual([
|
|
151
|
+
'main:begin',
|
|
152
|
+
'main:end',
|
|
153
|
+
'resolve:begin',
|
|
154
|
+
'resolve:draw:3',
|
|
155
|
+
'resolve:end',
|
|
156
|
+
'capture:copy',
|
|
157
|
+
'encoder:finish',
|
|
158
|
+
]);
|
|
159
|
+
expect(getWgpuRenderStateRuntime(state).surfacePresentationView).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('reallocates the supersample surface when the canvas size changes', async () => {
|
|
163
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
164
|
+
renderWgpuBackground(state);
|
|
165
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
166
|
+
const first = runtime.surfaceAntialiasTexture!;
|
|
167
|
+
const destroy = vi.spyOn(first, 'destroy');
|
|
168
|
+
submitWgpuRenderPass(state);
|
|
169
|
+
|
|
170
|
+
state.canvas.width = 400;
|
|
171
|
+
state.canvas.height = 300;
|
|
172
|
+
renderWgpuBackground(state);
|
|
173
|
+
|
|
174
|
+
expect(destroy).toHaveBeenCalledOnce();
|
|
175
|
+
expect(runtime.surfaceAntialiasTexture).not.toBe(first);
|
|
176
|
+
expect(runtime.surfaceAntialiasTexture?.width).toBe(800);
|
|
177
|
+
expect(runtime.surfaceAntialiasTexture?.height).toBe(600);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('scales main-surface scissors but leaves offscreen-target scissors unchanged', async () => {
|
|
181
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
182
|
+
renderWgpuBackground(state);
|
|
183
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
184
|
+
const setScissorRect = vi.fn();
|
|
185
|
+
const pass = { setScissorRect } as unknown as GPURenderPassEncoder;
|
|
186
|
+
|
|
187
|
+
setWgpuRenderPassScissorRect(state, pass, 3, 5, 7, 11);
|
|
188
|
+
expect(setScissorRect).toHaveBeenLastCalledWith(6, 10, 14, 22);
|
|
189
|
+
|
|
190
|
+
runtime.currentRenderTarget = {} as WgpuRenderTarget;
|
|
191
|
+
setWgpuRenderPassScissorRect(state, pass, 3, 5, 7, 11);
|
|
192
|
+
expect(setScissorRect).toHaveBeenLastCalledWith(3, 5, 7, 11);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function installRecordingEncoder(state: { device: GPUDevice }): {
|
|
197
|
+
encoder: GPUCommandEncoder & { beginRenderPass: ReturnType<typeof vi.fn> };
|
|
198
|
+
setViewport: ReturnType<typeof vi.fn>;
|
|
199
|
+
} {
|
|
200
|
+
const setViewport = vi.fn();
|
|
201
|
+
const pass = {
|
|
202
|
+
draw: () => {},
|
|
203
|
+
end: () => {},
|
|
204
|
+
setBindGroup: () => {},
|
|
205
|
+
setPipeline: () => {},
|
|
206
|
+
setScissorRect: () => {},
|
|
207
|
+
setStencilReference: () => {},
|
|
208
|
+
setViewport,
|
|
209
|
+
} as unknown as GPURenderPassEncoder;
|
|
210
|
+
const beginRenderPass = vi.fn(() => pass);
|
|
211
|
+
const encoder = {
|
|
212
|
+
beginRenderPass,
|
|
213
|
+
finish: () => ({}) as GPUCommandBuffer,
|
|
214
|
+
} as unknown as GPUCommandEncoder & { beginRenderPass: ReturnType<typeof vi.fn> };
|
|
215
|
+
vi.spyOn(state.device, 'createCommandEncoder').mockReturnValue(encoder);
|
|
216
|
+
return { encoder, setViewport };
|
|
217
|
+
}
|
package/src/wgpuDraw.test.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
updateWgpuTextureEntry,
|
|
31
31
|
warmWgpuPipelines,
|
|
32
32
|
} from './wgpuDraw';
|
|
33
|
+
import { registerWgpuMipmapGeneration } from './wgpuMipmap';
|
|
33
34
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
34
35
|
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
35
36
|
|
|
@@ -228,6 +229,7 @@ describe('bindWgpuTexture', () => {
|
|
|
228
229
|
|
|
229
230
|
it('allocates a full mip chain and generates it when generateMips is true', async () => {
|
|
230
231
|
const state = await createWgpuRenderStateForTest();
|
|
232
|
+
registerWgpuMipmapGeneration(state);
|
|
231
233
|
const createTexture = vi.spyOn(state.device, 'createTexture');
|
|
232
234
|
const submit = vi.spyOn(state.device.queue, 'submit');
|
|
233
235
|
const canvas = document.createElement('canvas');
|
|
@@ -238,6 +240,39 @@ describe('bindWgpuTexture', () => {
|
|
|
238
240
|
expect(createTexture).toHaveBeenCalledWith(expect.objectContaining({ mipLevelCount: 4 }));
|
|
239
241
|
expect(submit).toHaveBeenCalled();
|
|
240
242
|
});
|
|
243
|
+
|
|
244
|
+
it('degrades to a single mip level when generateMips is true but no generator is registered', async () => {
|
|
245
|
+
const state = await createWgpuRenderStateForTest();
|
|
246
|
+
const createTexture = vi.spyOn(state.device, 'createTexture');
|
|
247
|
+
const canvas = document.createElement('canvas');
|
|
248
|
+
canvas.width = 8;
|
|
249
|
+
canvas.height = 8;
|
|
250
|
+
bindWgpuTexture(state, canvas, true);
|
|
251
|
+
expect(createTexture).toHaveBeenCalledWith(expect.objectContaining({ mipLevelCount: 1 }));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('invokes the mipmapDegradedGuard when degrading', async () => {
|
|
255
|
+
const state = await createWgpuRenderStateForTest();
|
|
256
|
+
const guard = vi.fn();
|
|
257
|
+
getWgpuRenderStateRuntime(state).mipmapDegradedGuard = guard;
|
|
258
|
+
const canvas = document.createElement('canvas');
|
|
259
|
+
canvas.width = 8;
|
|
260
|
+
canvas.height = 8;
|
|
261
|
+
bindWgpuTexture(state, canvas, true);
|
|
262
|
+
expect(guard).toHaveBeenCalledWith(state);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('does not invoke the mipmapDegradedGuard when the generator is registered', async () => {
|
|
266
|
+
const state = await createWgpuRenderStateForTest();
|
|
267
|
+
registerWgpuMipmapGeneration(state);
|
|
268
|
+
const guard = vi.fn();
|
|
269
|
+
getWgpuRenderStateRuntime(state).mipmapDegradedGuard = guard;
|
|
270
|
+
const canvas = document.createElement('canvas');
|
|
271
|
+
canvas.width = 8;
|
|
272
|
+
canvas.height = 8;
|
|
273
|
+
bindWgpuTexture(state, canvas, true);
|
|
274
|
+
expect(guard).not.toHaveBeenCalled();
|
|
275
|
+
});
|
|
241
276
|
});
|
|
242
277
|
|
|
243
278
|
describe('bindWgpuVideoTexture', () => {
|
package/src/wgpuMipmap.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WgpuRenderState } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
|
-
import { generateWgpuMipmaps, getWgpuMipLevelCount } from './wgpuMipmap';
|
|
3
|
+
import { generateWgpuMipmaps, getWgpuMipLevelCount, registerWgpuMipmapGeneration } from './wgpuMipmap';
|
|
4
4
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
5
5
|
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
6
6
|
|
|
@@ -79,3 +79,13 @@ describe('getWgpuMipLevelCount', () => {
|
|
|
79
79
|
expect(getWgpuMipLevelCount(5, 3)).toBe(3);
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
describe('registerWgpuMipmapGeneration', () => {
|
|
84
|
+
it('installs generateWgpuMipmaps on the runtime slot', async () => {
|
|
85
|
+
const state = await createWgpuRenderStateForTest();
|
|
86
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
87
|
+
expect(runtime.mipmapGenerator).toBeNull();
|
|
88
|
+
registerWgpuMipmapGeneration(state);
|
|
89
|
+
expect(runtime.mipmapGenerator).toBe(generateWgpuMipmaps);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
|
+
import type { WgpuRenderTarget } from '@flighthq/types/contract';
|
|
2
3
|
|
|
3
4
|
import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
|
|
4
5
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
5
6
|
import {
|
|
7
|
+
getWgpuRenderTargetSupersampleScale,
|
|
6
8
|
beginWgpuRenderPass,
|
|
7
9
|
createWgpuRenderTarget,
|
|
8
10
|
declareWgpuRenderTargetColorSpace,
|
|
@@ -70,6 +72,16 @@ describe('createWgpuRenderTarget', () => {
|
|
|
70
72
|
expect(createWgpuRenderTarget(state, 16, 16).colorSpace).toBe('srgb');
|
|
71
73
|
expect(createWgpuRenderTarget(state, 16, 16, state.format, 'linear').colorSpace).toBe('linear');
|
|
72
74
|
});
|
|
75
|
+
|
|
76
|
+
it('realizes four coverage samples as a 2x extent in each axis', async () => {
|
|
77
|
+
const state = await createWgpuRenderStateForTest();
|
|
78
|
+
const target = createWgpuRenderTarget(state, 64, 48, state.format, 'srgb', 4);
|
|
79
|
+
expect(target.width).toBe(128);
|
|
80
|
+
expect(target.height).toBe(96);
|
|
81
|
+
expect(target.sampleCount).toBe(4);
|
|
82
|
+
expect(target.texture.width).toBe(128);
|
|
83
|
+
expect(target.texture.height).toBe(96);
|
|
84
|
+
});
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
describe('declareWgpuRenderTargetColorSpace', () => {
|
|
@@ -154,6 +166,34 @@ describe('endWgpuRenderPass', () => {
|
|
|
154
166
|
});
|
|
155
167
|
});
|
|
156
168
|
|
|
169
|
+
describe('getWgpuRenderTargetSupersampleScale', () => {
|
|
170
|
+
// ★ THE ALLOCATOR AND THE PROJECTION HAVE TO AGREE. A sampleCount-4 target is allocated at 2x per axis;
|
|
171
|
+
// if the 2D projection divides by that physical width it maps logical x = 800 to NDC 0, and the scene
|
|
172
|
+
// fills a quarter of its own target. Measured on effect-sepia before the repair: the WebGPU frame
|
|
173
|
+
// equalled the WebGL frame sampled at (2x, 2y) on 12 of 12 grid points; after, 48 of 48 matched 1:1.
|
|
174
|
+
it('reports 2 for a supersampled target and 1 for a single-sample one', () => {
|
|
175
|
+
expect(getWgpuRenderTargetSupersampleScale({ sampleCount: 4 } as WgpuRenderTarget)).toBe(2);
|
|
176
|
+
expect(getWgpuRenderTargetSupersampleScale({ sampleCount: 1 } as WgpuRenderTarget)).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Dividing the physical extent by the scale has to land back on the logical extent exactly, or the
|
|
180
|
+
// projection drifts by a pixel at odd sizes. The allocator ceils BEFORE scaling, which is what makes
|
|
181
|
+
// this exact rather than approximately right.
|
|
182
|
+
it('inverts the allocator exactly, including at an odd logical size', () => {
|
|
183
|
+
for (const [logical, sampleCount] of [
|
|
184
|
+
[801, 4],
|
|
185
|
+
[599, 4],
|
|
186
|
+
[800, 1],
|
|
187
|
+
[1, 4],
|
|
188
|
+
] as const) {
|
|
189
|
+
const physical = Math.max(1, Math.ceil(logical)) * (sampleCount === 4 ? 2 : 1);
|
|
190
|
+
const scale = getWgpuRenderTargetSupersampleScale({ sampleCount } as WgpuRenderTarget);
|
|
191
|
+
|
|
192
|
+
expect(physical / scale).toBe(logical);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
157
197
|
describe('resizeWgpuRenderTarget', () => {
|
|
158
198
|
it('preserves the allocation when dimensions are unchanged', async () => {
|
|
159
199
|
const state = await createWgpuRenderStateForTest();
|
|
@@ -179,6 +219,19 @@ describe('resizeWgpuRenderTarget', () => {
|
|
|
179
219
|
expect(target.bindGroup).toBeDefined();
|
|
180
220
|
expect(target.bindGroup).not.toBe(previousBindGroup);
|
|
181
221
|
});
|
|
222
|
+
|
|
223
|
+
it('reallocates when the effective sample count changes', async () => {
|
|
224
|
+
const state = await createWgpuRenderStateForTest();
|
|
225
|
+
const target = createWgpuRenderTarget(state, 64, 48);
|
|
226
|
+
const previousTexture = target.texture;
|
|
227
|
+
|
|
228
|
+
resizeWgpuRenderTarget(state, target, 64, 48, 4);
|
|
229
|
+
|
|
230
|
+
expect(target.width).toBe(128);
|
|
231
|
+
expect(target.height).toBe(96);
|
|
232
|
+
expect(target.sampleCount).toBe(4);
|
|
233
|
+
expect(target.texture).not.toBe(previousTexture);
|
|
234
|
+
});
|
|
182
235
|
});
|
|
183
236
|
|
|
184
237
|
describe('setWgpuRenderTransform2D', () => {
|
|
@@ -30,6 +30,22 @@ describe('acquireWgpuRenderTarget', () => {
|
|
|
30
30
|
expect(hdr.format).toBe('rgba16float');
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it('realizes and pools a four-sample request at twice the extent in each axis', async () => {
|
|
34
|
+
const state = await createWgpuRenderStateForTest();
|
|
35
|
+
const pool = createWgpuRenderTargetPool();
|
|
36
|
+
const supersampled = acquireWgpuRenderTarget(state, pool, { width: 64, height: 48, sampleCount: 4 });
|
|
37
|
+
expect(supersampled.width).toBe(128);
|
|
38
|
+
expect(supersampled.height).toBe(96);
|
|
39
|
+
expect(supersampled.sampleCount).toBe(4);
|
|
40
|
+
|
|
41
|
+
releaseWgpuRenderTarget(pool, supersampled);
|
|
42
|
+
const singleSampled = acquireWgpuRenderTarget(state, pool, { width: 128, height: 96 });
|
|
43
|
+
expect(singleSampled).not.toBe(supersampled);
|
|
44
|
+
|
|
45
|
+
const reused = acquireWgpuRenderTarget(state, pool, { width: 64, height: 48, sampleCount: 4 });
|
|
46
|
+
expect(reused).toBe(supersampled);
|
|
47
|
+
});
|
|
48
|
+
|
|
33
49
|
it('re-stamps the logical color space when reusing storage', async () => {
|
|
34
50
|
const state = await createWgpuRenderStateForTest();
|
|
35
51
|
const pool = createWgpuRenderTargetPool();
|
|
@@ -162,4 +162,17 @@ describe('writeWgpuRenderTextureTarget', () => {
|
|
|
162
162
|
expect(renderTexture.version).toBe(1);
|
|
163
163
|
expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(true);
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
it('realizes a four-sample render texture at twice its logical extent', async () => {
|
|
167
|
+
const state = await createWgpuRenderStateForTest();
|
|
168
|
+
const renderTexture = texture();
|
|
169
|
+
renderTexture.source.sampleCount = 4;
|
|
170
|
+
|
|
171
|
+
writeWgpuRenderTextureTarget(state, renderTexture, () => {});
|
|
172
|
+
|
|
173
|
+
const target = getWgpuRenderTextureTarget(state, renderTexture)!;
|
|
174
|
+
expect(target.width).toBe(16);
|
|
175
|
+
expect(target.height).toBe(16);
|
|
176
|
+
expect(target.sampleCount).toBe(4);
|
|
177
|
+
});
|
|
165
178
|
});
|
package/src/wgpuScissor.test.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { renderWgpuBackground } from './wgpuBackground';
|
|
2
2
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyWgpuScissorRect,
|
|
5
|
+
popWgpuScissorRect,
|
|
6
|
+
pushWgpuScissorRect,
|
|
7
|
+
setWgpuRenderPassScissorRect,
|
|
8
|
+
} from './wgpuScissor';
|
|
4
9
|
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
5
10
|
|
|
6
11
|
beforeAll(() => {
|
|
@@ -95,3 +100,15 @@ describe('pushWgpuScissorRect', () => {
|
|
|
95
100
|
expect(getWgpuRenderStateRuntime(state).currentScissorRect?.width).toBe(50);
|
|
96
101
|
});
|
|
97
102
|
});
|
|
103
|
+
|
|
104
|
+
describe('setWgpuRenderPassScissorRect', () => {
|
|
105
|
+
it('scales logical coordinates on the acquired supersample surface', async () => {
|
|
106
|
+
const state = await createWgpuRenderStateForTest({ antialias: true });
|
|
107
|
+
renderWgpuBackground(state);
|
|
108
|
+
const setScissorRect = vi.fn();
|
|
109
|
+
|
|
110
|
+
setWgpuRenderPassScissorRect(state, { setScissorRect } as unknown as GPURenderPassEncoder, 3, 5, 7, 11);
|
|
111
|
+
|
|
112
|
+
expect(setScissorRect).toHaveBeenCalledWith(6, 10, 14, 22);
|
|
113
|
+
});
|
|
114
|
+
});
|