@flighthq/render-wgpu 0.5.1-next.1069.554a2ac → 0.5.1-next.1201.9b49dee
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/README.md +2 -2
- package/dist/contract.d.ts +9 -6
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +9 -6
- package/dist/contract.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/wgpuDraw.js +1 -1
- package/dist/wgpuDraw.js.map +1 -1
- package/dist/wgpuFrame.d.ts +23 -0
- package/dist/wgpuFrame.d.ts.map +1 -0
- package/dist/{wgpuBackground.js → wgpuFrame.js} +67 -121
- package/dist/wgpuFrame.js.map +1 -0
- package/dist/wgpuFullscreenPass.d.ts +2 -2
- package/dist/wgpuFullscreenPass.d.ts.map +1 -1
- package/dist/wgpuFullscreenPass.js +1 -2
- package/dist/wgpuFullscreenPass.js.map +1 -1
- package/dist/wgpuHost.d.ts +6 -6
- package/dist/wgpuHost.d.ts.map +1 -1
- package/dist/wgpuHost.js +17 -3
- package/dist/wgpuHost.js.map +1 -1
- package/dist/wgpuRenderPass.d.ts +9 -0
- package/dist/wgpuRenderPass.d.ts.map +1 -0
- package/dist/wgpuRenderPass.js +222 -0
- package/dist/wgpuRenderPass.js.map +1 -0
- package/dist/wgpuRenderState.d.ts +3 -4
- package/dist/wgpuRenderState.d.ts.map +1 -1
- package/dist/wgpuRenderState.js +17 -88
- package/dist/wgpuRenderState.js.map +1 -1
- package/dist/wgpuRenderTarget.d.ts +12 -29
- package/dist/wgpuRenderTarget.d.ts.map +1 -1
- package/dist/wgpuRenderTarget.js +19 -252
- package/dist/wgpuRenderTarget.js.map +1 -1
- package/dist/wgpuRenderTargetPool.d.ts +5 -5
- package/dist/wgpuRenderTargetPool.d.ts.map +1 -1
- package/dist/wgpuRenderTargetPool.js +7 -7
- package/dist/wgpuRenderTargetPool.js.map +1 -1
- package/dist/wgpuRenderTexture.d.ts +5 -4
- package/dist/wgpuRenderTexture.d.ts.map +1 -1
- package/dist/wgpuRenderTexture.js +14 -16
- package/dist/wgpuRenderTexture.js.map +1 -1
- package/dist/wgpuRenderTexturePool.js +1 -3
- package/dist/wgpuRenderTexturePool.js.map +1 -1
- package/dist/wgpuScissor.d.ts +1 -1
- package/dist/wgpuScissor.d.ts.map +1 -1
- package/dist/wgpuScissor.js +7 -4
- package/dist/wgpuScissor.js.map +1 -1
- package/dist/wgpuScreenAntialias.d.ts +3 -0
- package/dist/wgpuScreenAntialias.d.ts.map +1 -0
- package/dist/wgpuScreenAntialias.js +119 -0
- package/dist/wgpuScreenAntialias.js.map +1 -0
- package/dist/wgpuScreenCapture.d.ts +5 -0
- package/dist/wgpuScreenCapture.d.ts.map +1 -0
- package/dist/{wgpuSurface.js → wgpuScreenCapture.js} +55 -73
- package/dist/wgpuScreenCapture.js.map +1 -0
- package/dist/wgpuScreenRenderTarget.d.ts +9 -0
- package/dist/wgpuScreenRenderTarget.d.ts.map +1 -0
- package/dist/wgpuScreenRenderTarget.js +123 -0
- package/dist/wgpuScreenRenderTarget.js.map +1 -0
- package/dist/wgpuShader.js +2 -2
- package/dist/wgpuShader.js.map +1 -1
- package/dist/wgpuTestHelper.d.ts +8 -2
- package/dist/wgpuTestHelper.d.ts.map +1 -1
- package/dist/wgpuTestHelper.js +38 -3
- package/dist/wgpuTestHelper.js.map +1 -1
- package/dist/wgpuTextureRenderTarget.d.ts +18 -0
- package/dist/wgpuTextureRenderTarget.d.ts.map +1 -0
- package/dist/wgpuTextureRenderTarget.js +122 -0
- package/dist/wgpuTextureRenderTarget.js.map +1 -0
- package/package.json +11 -11
- package/src/wgpuDraw.test.ts +8 -8
- package/src/{wgpuBackground.test.ts → wgpuFrame.test.ts} +34 -20
- package/src/wgpuFullscreenPass.test.ts +7 -7
- package/src/wgpuHost.test.ts +28 -82
- package/src/wgpuRenderPass.test.ts +258 -0
- package/src/wgpuRenderState.test.ts +34 -313
- package/src/wgpuRenderTarget.test.ts +44 -219
- package/src/wgpuRenderTargetPool.test.ts +21 -21
- package/src/wgpuRenderTexture.test.ts +17 -15
- package/src/wgpuScissor.test.ts +43 -13
- package/src/wgpuScreenAntialias.test.ts +60 -0
- package/src/wgpuScreenCapture.test.ts +156 -0
- package/src/wgpuScreenRenderTarget.test.ts +152 -0
- package/src/wgpuShader.test.ts +5 -6
- package/src/wgpuTextureRenderTarget.test.ts +129 -0
- package/src/wgpuTextureResolver.test.ts +5 -5
- package/dist/wgpuAntialias.d.ts +0 -14
- package/dist/wgpuAntialias.d.ts.map +0 -1
- package/dist/wgpuAntialias.js +0 -155
- package/dist/wgpuAntialias.js.map +0 -1
- package/dist/wgpuBackground.d.ts +0 -20
- package/dist/wgpuBackground.d.ts.map +0 -1
- package/dist/wgpuBackground.js.map +0 -1
- package/dist/wgpuElement.d.ts +0 -7
- package/dist/wgpuElement.d.ts.map +0 -1
- package/dist/wgpuElement.js +0 -20
- package/dist/wgpuElement.js.map +0 -1
- package/dist/wgpuSurface.d.ts +0 -6
- package/dist/wgpuSurface.d.ts.map +0 -1
- package/dist/wgpuSurface.js.map +0 -1
- package/src/wgpuAntialias.test.ts +0 -239
- package/src/wgpuElement.test.ts +0 -145
- package/src/wgpuSurface.test.ts +0 -141
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
beginWgpuFrame,
|
|
3
|
-
renderWgpuBackground,
|
|
4
|
-
retireWgpuBuffer,
|
|
5
|
-
retireWgpuTexture,
|
|
6
|
-
submitWgpuRenderPass,
|
|
7
|
-
withWgpuFrameBorrow,
|
|
8
|
-
} from './wgpuBackground';
|
|
1
|
+
import { beginWgpuFrame, retireWgpuBuffer, retireWgpuTexture, submitWgpuFrame, withWgpuFrameBorrow } from './wgpuFrame';
|
|
9
2
|
import { createWgpuPipeline } from './wgpuPipeline';
|
|
10
3
|
import { createWgpuOffscreenRenderState, getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
11
|
-
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
4
|
+
import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
12
5
|
|
|
13
6
|
beforeAll(() => installWgpuMock());
|
|
14
7
|
|
|
@@ -22,15 +15,6 @@ describe('beginWgpuFrame', () => {
|
|
|
22
15
|
});
|
|
23
16
|
});
|
|
24
17
|
|
|
25
|
-
describe('renderWgpuBackground', () => {
|
|
26
|
-
it('opens the presentation pass on the current surface', async () => {
|
|
27
|
-
const state = await createWgpuRenderStateForTest();
|
|
28
|
-
renderWgpuBackground(state);
|
|
29
|
-
expect(getWgpuRenderStateRuntime(state).renderPass).not.toBeNull();
|
|
30
|
-
submitWgpuRenderPass(state);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
18
|
describe('retireWgpuBuffer', () => {
|
|
35
19
|
it('adds the buffer to the post-submit destruction ledger', async () => {
|
|
36
20
|
const state = await createWgpuRenderStateForTest();
|
|
@@ -49,13 +33,43 @@ describe('retireWgpuTexture', () => {
|
|
|
49
33
|
});
|
|
50
34
|
});
|
|
51
35
|
|
|
52
|
-
describe('
|
|
36
|
+
describe('submitWgpuFrame', () => {
|
|
53
37
|
it('submits and closes the active frame', async () => {
|
|
54
38
|
const state = await createWgpuRenderStateForTest();
|
|
55
39
|
beginWgpuFrame(state);
|
|
56
|
-
|
|
40
|
+
submitWgpuFrame(state);
|
|
57
41
|
expect(getWgpuRenderStateRuntime(state).commandEncoder).toBeNull();
|
|
58
42
|
});
|
|
43
|
+
|
|
44
|
+
it('closes a pass the caller left open rather than carrying its encoder into the next frame', async () => {
|
|
45
|
+
const state = await createWgpuRenderStateForTest();
|
|
46
|
+
beginWgpuFrame(state);
|
|
47
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
48
|
+
expect(getWgpuRenderStateRuntime(state).renderPass).not.toBeNull();
|
|
49
|
+
|
|
50
|
+
submitWgpuFrame(state);
|
|
51
|
+
|
|
52
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
53
|
+
expect(runtime.renderPass).toBeNull();
|
|
54
|
+
expect(runtime.passStack).toEqual([]);
|
|
55
|
+
expect(runtime.currentRenderTarget).toBeNull();
|
|
56
|
+
expect(runtime.renderTargetViewport).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
// A frame that only drew into texture targets has no screen to resolve or capture. Skipping that work
|
|
59
|
+
// is not an optimization: the capture texture still holds the LAST VISIBLE frame, so encoding a copy
|
|
60
|
+
// here would overwrite the readback buffer with a stale picture that a caller would read as current.
|
|
61
|
+
it('resolves and captures nothing when the frame never touched a screen target', async () => {
|
|
62
|
+
const state = await createWgpuRenderStateForTest();
|
|
63
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
64
|
+
beginWgpuFrame(state);
|
|
65
|
+
const encoder = runtime.commandEncoder!;
|
|
66
|
+
const copyToBuffer = vi.spyOn(encoder, 'copyTextureToBuffer');
|
|
67
|
+
|
|
68
|
+
expect(runtime.frameScreenTarget).toBeNull();
|
|
69
|
+
submitWgpuFrame(state);
|
|
70
|
+
|
|
71
|
+
expect(copyToBuffer).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
59
73
|
});
|
|
60
74
|
|
|
61
75
|
describe('withWgpuFrameBorrow', () => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { submitWgpuFrame } from './wgpuFrame';
|
|
2
2
|
import {
|
|
3
3
|
createWgpuFullscreenPipeline,
|
|
4
4
|
destroyWgpuFullscreenPipeline,
|
|
5
5
|
drawWgpuFullscreenPass,
|
|
6
6
|
initializeWgpuFullscreenPipeline,
|
|
7
7
|
} from './wgpuFullscreenPass';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
9
|
+
import { createWgpuTextureRenderTarget } from './wgpuTextureRenderTarget';
|
|
10
10
|
|
|
11
11
|
beforeAll(() => {
|
|
12
12
|
installWgpuMock();
|
|
@@ -49,17 +49,17 @@ describe('destroyWgpuFullscreenPipeline', () => {
|
|
|
49
49
|
describe('drawWgpuFullscreenPass', () => {
|
|
50
50
|
it('does not throw when render pass is open and no uniforms', async () => {
|
|
51
51
|
const state = await createWgpuRenderStateForTest();
|
|
52
|
-
|
|
52
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
53
53
|
const pipeline = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 1);
|
|
54
|
-
const target =
|
|
54
|
+
const target = createWgpuTextureRenderTarget(state, 64, 64);
|
|
55
55
|
expect(() => drawWgpuFullscreenPass(state, pipeline, [target], null)).not.toThrow();
|
|
56
|
-
|
|
56
|
+
submitWgpuFrame(state);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it('is a no-op when render pass is not open', async () => {
|
|
60
60
|
const state = await createWgpuRenderStateForTest();
|
|
61
61
|
const pipeline = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 1);
|
|
62
|
-
const target =
|
|
62
|
+
const target = createWgpuTextureRenderTarget(state, 64, 64);
|
|
63
63
|
expect(() => drawWgpuFullscreenPass(state, pipeline, [target], null)).not.toThrow();
|
|
64
64
|
});
|
|
65
65
|
});
|
package/src/wgpuHost.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
|
|
2
|
-
import type { Entity, WgpuHostAcquisition,
|
|
2
|
+
import type { Entity, WgpuHostAcquisition, HostWgpuProvider } from '@flighthq/types/contract';
|
|
3
3
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -12,23 +12,25 @@ import {
|
|
|
12
12
|
} from './wgpuHost';
|
|
13
13
|
import { createEmptyWgpuRegistries, createWgpuPipeline } from './wgpuPipeline';
|
|
14
14
|
import {
|
|
15
|
+
createWgpuAcquisition,
|
|
15
16
|
createWgpuRenderState,
|
|
16
|
-
createWgpuRenderStateFromCanvasElement,
|
|
17
17
|
destroyWgpuRenderState,
|
|
18
|
+
releaseWgpuAcquisition,
|
|
18
19
|
} from './wgpuRenderState';
|
|
19
20
|
import { installWgpuMock } from './wgpuTestHelper';
|
|
20
21
|
|
|
21
|
-
function fakeBackend():
|
|
22
|
+
function fakeBackend(): HostWgpuProvider {
|
|
22
23
|
return entityBackend({
|
|
23
24
|
acquire: vi.fn(),
|
|
25
|
+
attachSurface: vi.fn(() => null),
|
|
24
26
|
isSupported: vi.fn(() => true),
|
|
25
27
|
release: vi.fn(),
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
function entityBackend(fields: Omit<
|
|
31
|
+
function entityBackend(fields: Omit<HostWgpuProvider, keyof Entity>): HostWgpuProvider {
|
|
30
32
|
return (() => {
|
|
31
|
-
const out = allocateEntity<
|
|
33
|
+
const out = allocateEntity<HostWgpuProvider>();
|
|
32
34
|
Object.assign(out, fields);
|
|
33
35
|
return finishEntity(out);
|
|
34
36
|
})();
|
|
@@ -126,72 +128,43 @@ describe('setWgpuHostBackend', () => {
|
|
|
126
128
|
it('routes acquisition and release through the same selected backend', async () => {
|
|
127
129
|
const web = createWebWgpuHostBackend();
|
|
128
130
|
const acquired = await web.acquire(document.createElement('canvas'), {});
|
|
129
|
-
// Flight-owned: the state acquired these, so destroying it releases them. The caller-owned case is the
|
|
130
|
-
// opposite assertion and lives in its own test below.
|
|
131
131
|
const acquisition = allocateEntity<WgpuHostAcquisition>();
|
|
132
132
|
Object.assign(acquisition, acquired);
|
|
133
|
-
acquisition.ownership = '
|
|
133
|
+
acquisition.ownership = 'caller' as const;
|
|
134
134
|
const backend = entityBackend({
|
|
135
135
|
acquire: vi.fn(async () => acquisition),
|
|
136
|
+
attachSurface: vi.fn(() => null),
|
|
136
137
|
isSupported: vi.fn(() => true),
|
|
137
138
|
release: vi.fn(),
|
|
138
139
|
});
|
|
139
140
|
const canvas = document.createElement('canvas');
|
|
140
141
|
setWgpuHostBackend(backend);
|
|
141
142
|
|
|
142
|
-
const
|
|
143
|
-
expect(backend.acquire).toHaveBeenCalledWith(canvas, {
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
146
|
-
expect(state.format).toBe(acquisition.format);
|
|
147
|
-
destroyWgpuRenderState(state);
|
|
148
|
-
expect(backend.release).toHaveBeenCalledOnce();
|
|
149
|
-
expect(backend.release).toHaveBeenCalledWith(acquisition);
|
|
150
|
-
|
|
151
|
-
web.release(acquired);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('releases flight-owned handles through the selected backend when initialization fails', async () => {
|
|
155
|
-
const web = createWebWgpuHostBackend();
|
|
156
|
-
const acquired = await web.acquire(document.createElement('canvas'), {});
|
|
157
|
-
const acquisition = allocateEntity<WgpuHostAcquisition>();
|
|
158
|
-
Object.assign(acquisition, acquired);
|
|
159
|
-
acquisition.ownership = 'flight' as const;
|
|
160
|
-
const configure = vi.spyOn(acquisition.context, 'configure').mockImplementationOnce(() => {
|
|
161
|
-
throw new Error('configure failed');
|
|
162
|
-
});
|
|
163
|
-
const backend = entityBackend({
|
|
164
|
-
acquire: vi.fn(async () => acquisition),
|
|
165
|
-
isSupported: vi.fn(() => true),
|
|
166
|
-
release: vi.fn(),
|
|
167
|
-
});
|
|
168
|
-
setWgpuHostBackend(backend);
|
|
143
|
+
const routed = await createWgpuAcquisition(canvas);
|
|
144
|
+
expect(backend.acquire).toHaveBeenCalledWith(canvas, {});
|
|
145
|
+
expect(routed?.device).toBe(acquisition.device);
|
|
146
|
+
expect(routed?.format).toBe(acquisition.format);
|
|
169
147
|
|
|
170
|
-
|
|
148
|
+
releaseWgpuAcquisition(acquisition);
|
|
171
149
|
expect(backend.release).toHaveBeenCalledOnce();
|
|
172
150
|
expect(backend.release).toHaveBeenCalledWith(acquisition);
|
|
173
151
|
|
|
174
|
-
configure.mockRestore();
|
|
175
152
|
web.release(acquired);
|
|
176
153
|
});
|
|
177
154
|
|
|
178
|
-
// ★
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
155
|
+
// ★ A RENDER STATE OWNS NO HOST HANDLES AT ALL. It used to be built from an acquisition and had to
|
|
156
|
+
// decide, on every path including a failed construction, whether to release handles the caller might
|
|
157
|
+
// still be using — a decision no state is in a position to make correctly. It now takes the device
|
|
158
|
+
// alone, so there is nothing to get wrong: destroying a state cannot reach a context or a device, and
|
|
159
|
+
// the caller's handles outlive it by construction rather than by policy.
|
|
160
|
+
it('never releases host handles when a state built on the device is destroyed', async () => {
|
|
183
161
|
const web = createWebWgpuHostBackend();
|
|
184
162
|
const acquired = await web.acquire(document.createElement('canvas'), {});
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
acquisition.ownership = 'caller' as const;
|
|
188
|
-
const configure = vi.spyOn(acquisition.context, 'configure').mockImplementationOnce(() => {
|
|
189
|
-
throw new Error('configure failed');
|
|
190
|
-
});
|
|
191
|
-
const destroy = vi.spyOn(acquisition.device, 'destroy');
|
|
192
|
-
const unconfigure = vi.spyOn(acquisition.context, 'unconfigure');
|
|
163
|
+
const destroy = vi.spyOn(acquired.device, 'destroy');
|
|
164
|
+
const unconfigure = vi.spyOn(acquired.context, 'unconfigure');
|
|
193
165
|
const backend = entityBackend({
|
|
194
|
-
acquire: vi.fn(async () =>
|
|
166
|
+
acquire: vi.fn(async () => acquired),
|
|
167
|
+
attachSurface: vi.fn(() => null),
|
|
195
168
|
isSupported: vi.fn(() => true),
|
|
196
169
|
release: vi.fn((held) => {
|
|
197
170
|
held.context.unconfigure();
|
|
@@ -200,43 +173,16 @@ describe('setWgpuHostBackend', () => {
|
|
|
200
173
|
});
|
|
201
174
|
setWgpuHostBackend(backend);
|
|
202
175
|
|
|
203
|
-
|
|
204
|
-
expect(backend.release).not.toHaveBeenCalled();
|
|
205
|
-
expect(destroy).not.toHaveBeenCalled();
|
|
206
|
-
expect(unconfigure).not.toHaveBeenCalled();
|
|
207
|
-
|
|
208
|
-
configure.mockRestore();
|
|
209
|
-
destroy.mockRestore();
|
|
210
|
-
unconfigure.mockRestore();
|
|
211
|
-
web.release(acquired);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ★ O1. The destroy path, against a backend whose release really destroys.
|
|
215
|
-
it('O1: never releases caller-owned handles when the state is destroyed', async () => {
|
|
216
|
-
const web = createWebWgpuHostBackend();
|
|
217
|
-
const acquired = await web.acquire(document.createElement('canvas'), {});
|
|
218
|
-
const acquisition = allocateEntity<WgpuHostAcquisition>();
|
|
219
|
-
Object.assign(acquisition, acquired);
|
|
220
|
-
acquisition.ownership = 'caller' as const;
|
|
221
|
-
const destroy = vi.spyOn(acquisition.device, 'destroy');
|
|
222
|
-
const backend = entityBackend({
|
|
223
|
-
acquire: vi.fn(async () => acquisition),
|
|
224
|
-
isSupported: vi.fn(() => true),
|
|
225
|
-
release: vi.fn((held) => {
|
|
226
|
-
held.device.destroy();
|
|
227
|
-
}),
|
|
228
|
-
});
|
|
229
|
-
setWgpuHostBackend(backend);
|
|
230
|
-
|
|
231
|
-
const state = createWgpuRenderState(acquisition, _pipeline);
|
|
232
|
-
destroyWgpuRenderState(state);
|
|
176
|
+
destroyWgpuRenderState(createWgpuRenderState(acquired.device, _pipeline));
|
|
233
177
|
|
|
234
178
|
expect(backend.release).not.toHaveBeenCalled();
|
|
235
179
|
expect(destroy).not.toHaveBeenCalled();
|
|
180
|
+
expect(unconfigure).not.toHaveBeenCalled();
|
|
236
181
|
// The handles are still usable, which is the property the caller actually cares about.
|
|
237
|
-
expect(
|
|
182
|
+
expect(acquired.device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST }).size).toBe(4);
|
|
238
183
|
|
|
239
184
|
destroy.mockRestore();
|
|
185
|
+
unconfigure.mockRestore();
|
|
240
186
|
web.release(acquired);
|
|
241
187
|
});
|
|
242
188
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
|
+
|
|
3
|
+
import { beginWgpuFrame, submitWgpuFrame } from './wgpuFrame';
|
|
4
|
+
import {
|
|
5
|
+
beginWgpuRenderPass,
|
|
6
|
+
endWgpuRenderPass,
|
|
7
|
+
getWgpuActiveRenderPass,
|
|
8
|
+
getWgpuRenderPassViewport,
|
|
9
|
+
resumeWgpuRenderPass,
|
|
10
|
+
setWgpuRenderTransform2D,
|
|
11
|
+
suspendWgpuRenderPass,
|
|
12
|
+
} from './wgpuRenderPass';
|
|
13
|
+
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
14
|
+
import {
|
|
15
|
+
beginWgpuScreenRenderPassForTest,
|
|
16
|
+
createWgpuRenderStateForTest,
|
|
17
|
+
createWgpuScreenRenderTargetForTest,
|
|
18
|
+
installWgpuMock,
|
|
19
|
+
} from './wgpuTestHelper';
|
|
20
|
+
import { createWgpuTextureRenderTarget } from './wgpuTextureRenderTarget';
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
installWgpuMock();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('beginWgpuRenderPass', () => {
|
|
27
|
+
it('reports the target extent as the pass viewport', async () => {
|
|
28
|
+
const state = await createWgpuRenderStateForTest();
|
|
29
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
30
|
+
const pass = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 64, 64));
|
|
31
|
+
|
|
32
|
+
expect(pass.viewport.width).toBe(64);
|
|
33
|
+
expect(getWgpuRenderStateRuntime(state).renderTargetViewport?.width).toBe(64);
|
|
34
|
+
|
|
35
|
+
endWgpuRenderPass(pass);
|
|
36
|
+
endWgpuRenderPass(outer);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// The viewport is LOGICAL, not the storage extent. A supersampled target is allocated at 2x per axis,
|
|
40
|
+
// and a projection that divided by that physical width would map logical x = 800 to NDC 0 — the scene
|
|
41
|
+
// filling a quarter of its own target, which is exactly the defect the derived scale exists to prevent.
|
|
42
|
+
it('reports the logical extent for a supersampled target, not its physical storage', async () => {
|
|
43
|
+
const state = await createWgpuRenderStateForTest();
|
|
44
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
45
|
+
const target = createWgpuTextureRenderTarget(state, 400, 300, state.format, 'srgb', 4);
|
|
46
|
+
const pass = beginWgpuRenderPass(state, target);
|
|
47
|
+
|
|
48
|
+
expect(target.width).toBe(800);
|
|
49
|
+
expect(pass.viewport).toEqual({ height: 300, width: 400 });
|
|
50
|
+
|
|
51
|
+
endWgpuRenderPass(pass);
|
|
52
|
+
endWgpuRenderPass(outer);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('passes float RGBA clear values through to the GPU color attachment', async () => {
|
|
56
|
+
const state = await createWgpuRenderStateForTest();
|
|
57
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
58
|
+
const beginRenderPass = vi.spyOn(getWgpuRenderStateRuntime(state).commandEncoder!, 'beginRenderPass');
|
|
59
|
+
const target = createWgpuTextureRenderTarget(state, 32, 32, state.format, 'linear');
|
|
60
|
+
|
|
61
|
+
const pass = beginWgpuRenderPass(state, target, { color: [0.25, 0.5, 0.75, 1.0] });
|
|
62
|
+
|
|
63
|
+
const attachment = Array.from(beginRenderPass.mock.calls.at(-1)![0].colorAttachments)[0]!;
|
|
64
|
+
const clearValue = attachment.clearValue as GPUColorDict;
|
|
65
|
+
expect([clearValue.r, clearValue.g, clearValue.b, clearValue.a]).toEqual([0.25, 0.5, 0.75, 1.0]);
|
|
66
|
+
expect(attachment.loadOp).toBe('clear');
|
|
67
|
+
|
|
68
|
+
endWgpuRenderPass(pass);
|
|
69
|
+
endWgpuRenderPass(outer);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('preserves the target when no clear is given', async () => {
|
|
73
|
+
const state = await createWgpuRenderStateForTest();
|
|
74
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
75
|
+
const beginRenderPass = vi.spyOn(getWgpuRenderStateRuntime(state).commandEncoder!, 'beginRenderPass');
|
|
76
|
+
|
|
77
|
+
const pass = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 32, 32));
|
|
78
|
+
|
|
79
|
+
const attachment = Array.from(beginRenderPass.mock.calls.at(-1)![0].colorAttachments)[0]!;
|
|
80
|
+
expect(attachment.loadOp).toBe('load');
|
|
81
|
+
|
|
82
|
+
endWgpuRenderPass(pass);
|
|
83
|
+
endWgpuRenderPass(outer);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('opens the frame when none is open, and leaves an explicitly opened frame to its owner', async () => {
|
|
87
|
+
const state = await createWgpuRenderStateForTest();
|
|
88
|
+
const owning = beginWgpuScreenRenderPassForTest(state);
|
|
89
|
+
expect(owning.ownsFrame).toBe(true);
|
|
90
|
+
endWgpuRenderPass(owning);
|
|
91
|
+
expect(getWgpuRenderStateRuntime(state).commandEncoder).toBeNull();
|
|
92
|
+
|
|
93
|
+
beginWgpuFrame(state);
|
|
94
|
+
const borrowed = beginWgpuScreenRenderPassForTest(state);
|
|
95
|
+
expect(borrowed.ownsFrame).toBe(false);
|
|
96
|
+
endWgpuRenderPass(borrowed);
|
|
97
|
+
// The frame outlives the pass, which is what lets a shadow pass and a screen pass share one submit.
|
|
98
|
+
expect(getWgpuRenderStateRuntime(state).commandEncoder).not.toBeNull();
|
|
99
|
+
submitWgpuFrame(state);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('endWgpuRenderPass', () => {
|
|
104
|
+
it('restores the enclosing target, viewport, and clip state', async () => {
|
|
105
|
+
const state = await createWgpuRenderStateForTest();
|
|
106
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
107
|
+
const outer = beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] });
|
|
108
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
109
|
+
runtime.currentScissorRect = { height: 10, width: 10, x: 0, y: 0 };
|
|
110
|
+
|
|
111
|
+
const inner = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 32, 32));
|
|
112
|
+
expect(runtime.currentScissorRect).toBeNull();
|
|
113
|
+
endWgpuRenderPass(inner);
|
|
114
|
+
|
|
115
|
+
expect(runtime.currentRenderTarget).toBe(screen);
|
|
116
|
+
expect(runtime.renderTargetViewport).toEqual({ height: 600, width: 800 });
|
|
117
|
+
expect(runtime.currentScissorRect).toEqual({ height: 10, width: 10, x: 0, y: 0 });
|
|
118
|
+
|
|
119
|
+
endWgpuRenderPass(outer);
|
|
120
|
+
expect(runtime.currentRenderTarget).toBeNull();
|
|
121
|
+
expect(runtime.renderTargetViewport).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('resumes the enclosing pass with a fresh encoder that preserves its pixels', async () => {
|
|
125
|
+
// A WebGPU pass encoder cannot be reopened, so the enclosing pass is re-recorded — and it must load
|
|
126
|
+
// rather than clear, or every nested offscreen pass would wipe the frame that surrounds it.
|
|
127
|
+
const state = await createWgpuRenderStateForTest();
|
|
128
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
129
|
+
const runtime = getWgpuRenderStateRuntime(state);
|
|
130
|
+
const suspendedEncoder = outer.encoder;
|
|
131
|
+
|
|
132
|
+
const inner = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 32, 32));
|
|
133
|
+
expect(outer.encoder).toBeNull();
|
|
134
|
+
const beginRenderPass = vi.spyOn(runtime.commandEncoder!, 'beginRenderPass');
|
|
135
|
+
endWgpuRenderPass(inner);
|
|
136
|
+
|
|
137
|
+
expect(outer.encoder).not.toBeNull();
|
|
138
|
+
expect(outer.encoder).not.toBe(suspendedEncoder);
|
|
139
|
+
expect(runtime.renderPass).toBe(outer.encoder);
|
|
140
|
+
expect(Array.from(beginRenderPass.mock.calls.at(-1)![0].colorAttachments)[0]!.loadOp).toBe('load');
|
|
141
|
+
|
|
142
|
+
endWgpuRenderPass(outer);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('submits the frame it opened, and only that frame', async () => {
|
|
146
|
+
const state = await createWgpuRenderStateForTest();
|
|
147
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
148
|
+
const inner = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 16, 16));
|
|
149
|
+
|
|
150
|
+
endWgpuRenderPass(inner);
|
|
151
|
+
expect(getWgpuRenderStateRuntime(state).commandEncoder).not.toBeNull();
|
|
152
|
+
|
|
153
|
+
endWgpuRenderPass(outer);
|
|
154
|
+
expect(getWgpuRenderStateRuntime(state).commandEncoder).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('refuses a pass that is not the innermost open one', async () => {
|
|
158
|
+
const state = await createWgpuRenderStateForTest();
|
|
159
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
160
|
+
const inner = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 16, 16));
|
|
161
|
+
|
|
162
|
+
expect(() => endWgpuRenderPass(outer)).toThrow(/innermost open pass/);
|
|
163
|
+
|
|
164
|
+
endWgpuRenderPass(inner);
|
|
165
|
+
endWgpuRenderPass(outer);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getWgpuActiveRenderPass', () => {
|
|
170
|
+
it('names the innermost open pass and nothing outside a bracket', async () => {
|
|
171
|
+
const state = await createWgpuRenderStateForTest();
|
|
172
|
+
expect(getWgpuActiveRenderPass(state)).toBeNull();
|
|
173
|
+
|
|
174
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
175
|
+
expect(getWgpuActiveRenderPass(state)).toBe(outer);
|
|
176
|
+
const inner = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 16, 16));
|
|
177
|
+
expect(getWgpuActiveRenderPass(state)).toBe(inner);
|
|
178
|
+
|
|
179
|
+
endWgpuRenderPass(inner);
|
|
180
|
+
expect(getWgpuActiveRenderPass(state)).toBe(outer);
|
|
181
|
+
endWgpuRenderPass(outer);
|
|
182
|
+
expect(getWgpuActiveRenderPass(state)).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('getWgpuRenderPassViewport', () => {
|
|
187
|
+
it('throws outside a pass rather than inventing a canvas-sized default', async () => {
|
|
188
|
+
// A draw with no target bound is API misuse; a default extent would hide it until the picture came
|
|
189
|
+
// out the wrong size, with nothing in the failure naming the missing begin.
|
|
190
|
+
const state = await createWgpuRenderStateForTest();
|
|
191
|
+
expect(() => getWgpuRenderPassViewport(state)).toThrow(/beginWgpuRenderPass/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('reports the innermost pass extent', async () => {
|
|
195
|
+
const state = await createWgpuRenderStateForTest();
|
|
196
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
197
|
+
expect(getWgpuRenderPassViewport(state)).toEqual({ height: 600, width: 800 });
|
|
198
|
+
|
|
199
|
+
const inner = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 40, 20));
|
|
200
|
+
expect(getWgpuRenderPassViewport(state)).toEqual({ height: 20, width: 40 });
|
|
201
|
+
|
|
202
|
+
endWgpuRenderPass(inner);
|
|
203
|
+
endWgpuRenderPass(outer);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('resumeWgpuRenderPass', () => {
|
|
208
|
+
it('reopens a suspended pass over the same target', async () => {
|
|
209
|
+
const state = await createWgpuRenderStateForTest();
|
|
210
|
+
const pass = beginWgpuScreenRenderPassForTest(state);
|
|
211
|
+
const target = pass.target;
|
|
212
|
+
suspendWgpuRenderPass(pass);
|
|
213
|
+
|
|
214
|
+
resumeWgpuRenderPass(pass);
|
|
215
|
+
|
|
216
|
+
expect(pass.encoder).not.toBeNull();
|
|
217
|
+
expect(pass.target).toBe(target);
|
|
218
|
+
expect(getWgpuRenderStateRuntime(state).renderPass).toBe(pass.encoder);
|
|
219
|
+
endWgpuRenderPass(pass);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('setWgpuRenderTransform2D', () => {
|
|
224
|
+
it('installs a copy of the transform, restored by the enclosing pass', async () => {
|
|
225
|
+
const state = await createWgpuRenderStateForTest();
|
|
226
|
+
const outer = beginWgpuScreenRenderPassForTest(state);
|
|
227
|
+
const original = state.renderTransform2D;
|
|
228
|
+
const bake = createMatrix();
|
|
229
|
+
bake.tx = 42;
|
|
230
|
+
|
|
231
|
+
const pass = beginWgpuRenderPass(state, createWgpuTextureRenderTarget(state, 32, 32));
|
|
232
|
+
setWgpuRenderTransform2D(pass, bake);
|
|
233
|
+
expect(state.renderTransform2D?.tx).toBe(42);
|
|
234
|
+
expect(state.renderTransform2D).not.toBe(bake);
|
|
235
|
+
endWgpuRenderPass(pass);
|
|
236
|
+
|
|
237
|
+
expect(state.renderTransform2D).toBe(original);
|
|
238
|
+
endWgpuRenderPass(outer);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('suspendWgpuRenderPass', () => {
|
|
243
|
+
it('closes the encoder while leaving the pass open', async () => {
|
|
244
|
+
// The effect pipeline records its own fullscreen passes into the frame, which it can only do while
|
|
245
|
+
// no other pass encoder is recording.
|
|
246
|
+
const state = await createWgpuRenderStateForTest();
|
|
247
|
+
const pass = beginWgpuScreenRenderPassForTest(state);
|
|
248
|
+
|
|
249
|
+
suspendWgpuRenderPass(pass);
|
|
250
|
+
|
|
251
|
+
expect(pass.encoder).toBeNull();
|
|
252
|
+
expect(getWgpuRenderStateRuntime(state).renderPass).toBeNull();
|
|
253
|
+
expect(getWgpuActiveRenderPass(state)).toBe(pass);
|
|
254
|
+
|
|
255
|
+
resumeWgpuRenderPass(pass);
|
|
256
|
+
endWgpuRenderPass(pass);
|
|
257
|
+
});
|
|
258
|
+
});
|