@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { beginWgpuRenderPass, endWgpuRenderPass } from './wgpuRenderPass';
|
|
2
|
+
import { enableWgpuScreenRenderTargetAntialias } from './wgpuScreenAntialias';
|
|
3
|
+
import { bindWgpuScreenRenderTarget } from './wgpuScreenRenderTarget';
|
|
4
|
+
import { createWgpuRenderStateForTest, createWgpuScreenRenderTargetForTest, installWgpuMock } from './wgpuTestHelper';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
installWgpuMock();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('enableWgpuScreenRenderTargetAntialias', () => {
|
|
11
|
+
// ★ THE COST IS OPT-IN, AND THAT IS THE POINT OF THE SLOTS. A screen target that was never asked for
|
|
12
|
+
// antialiasing carries no resolve hook, so the module holding the resolve pipeline and its shader is
|
|
13
|
+
// unreachable from an ordinary frame loop and tree-shakes out of the bundle entirely.
|
|
14
|
+
it('installs the hooks a plain screen target does not carry', async () => {
|
|
15
|
+
const state = await createWgpuRenderStateForTest();
|
|
16
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
17
|
+
|
|
18
|
+
expect(screen.antialias).toBe(false);
|
|
19
|
+
expect(screen.acquireAntialiasView).toBeNull();
|
|
20
|
+
expect(screen.encodeAntialiasResolve).toBeNull();
|
|
21
|
+
|
|
22
|
+
enableWgpuScreenRenderTargetAntialias(screen);
|
|
23
|
+
|
|
24
|
+
expect(screen.antialias).toBe(true);
|
|
25
|
+
expect(screen.acquireAntialiasView).not.toBeNull();
|
|
26
|
+
expect(screen.encodeAntialiasResolve).not.toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('draws into supersample storage twice the surface extent', async () => {
|
|
30
|
+
const state = await createWgpuRenderStateForTest();
|
|
31
|
+
const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true }, 400, 300);
|
|
32
|
+
|
|
33
|
+
const view = bindWgpuScreenRenderTarget(state, screen);
|
|
34
|
+
|
|
35
|
+
expect(screen.width).toBe(800);
|
|
36
|
+
expect(view).toBe(screen.antialiasView);
|
|
37
|
+
expect(view).not.toBe(screen.presentationView);
|
|
38
|
+
expect(screen.antialiasTexture?.width).toBe(800);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('resolves the supersample texture into the presentation view', async () => {
|
|
42
|
+
const state = await createWgpuRenderStateForTest();
|
|
43
|
+
const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true });
|
|
44
|
+
const pass = beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] });
|
|
45
|
+
const presentationView = screen.presentationView;
|
|
46
|
+
const encoded: GPURenderPassDescriptor[] = [];
|
|
47
|
+
const encoder = {
|
|
48
|
+
beginRenderPass: (descriptor: GPURenderPassDescriptor) => {
|
|
49
|
+
encoded.push(descriptor);
|
|
50
|
+
return { draw: () => {}, end: () => {}, setBindGroup: () => {}, setPipeline: () => {} };
|
|
51
|
+
},
|
|
52
|
+
} as unknown as GPUCommandEncoder;
|
|
53
|
+
|
|
54
|
+
screen.encodeAntialiasResolve!(state, screen, encoder);
|
|
55
|
+
|
|
56
|
+
expect(encoded.length).toBe(1);
|
|
57
|
+
expect(Array.from(encoded[0]!.colorAttachments)[0]!.view).toBe(presentationView);
|
|
58
|
+
endWgpuRenderPass(pass);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { WgpuScreenRenderTarget } from '@flighthq/types/contract';
|
|
2
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { beginWgpuRenderPass, endWgpuRenderPass } from './wgpuRenderPass';
|
|
5
|
+
import {
|
|
6
|
+
createBitmapFromWgpuScreenRenderTarget,
|
|
7
|
+
enableWgpuScreenRenderTargetCapture,
|
|
8
|
+
encodeWgpuScreenRenderTargetCapture,
|
|
9
|
+
} from './wgpuScreenCapture';
|
|
10
|
+
import { createWgpuRenderStateForTest, createWgpuScreenRenderTargetForTest, installWgpuMock } from './wgpuTestHelper';
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
installWgpuMock();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('createBitmapFromWgpuScreenRenderTarget', () => {
|
|
17
|
+
it('throws when capture was never enabled (no buffer to read)', async () => {
|
|
18
|
+
const state = await createWgpuRenderStateForTest();
|
|
19
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
20
|
+
await expect(createBitmapFromWgpuScreenRenderTarget(screen)).rejects.toThrow(/enableWgpuScreenRenderTargetCapture/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('reads the mapped buffer within the bound', async () => {
|
|
24
|
+
const state = await createWgpuRenderStateForTest();
|
|
25
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
26
|
+
installCaptureBuffer(screen, () => Promise.resolve());
|
|
27
|
+
|
|
28
|
+
const bitmap = await createBitmapFromWgpuScreenRenderTarget(screen, 1_000);
|
|
29
|
+
|
|
30
|
+
expect(bitmap.width).toBe(1);
|
|
31
|
+
expect(bitmap.height).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('fails by name when the buffer never maps, instead of awaiting forever', async () => {
|
|
35
|
+
// The driver failure this bounds: mapAsync neither resolves nor rejects. Before the bound, the
|
|
36
|
+
// caller sat here until something outside it gave up and reported a stall with no location.
|
|
37
|
+
const state = await createWgpuRenderStateForTest();
|
|
38
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
39
|
+
installCaptureBuffer(screen, () => new Promise<void>(() => {}));
|
|
40
|
+
|
|
41
|
+
await expect(createBitmapFromWgpuScreenRenderTarget(screen, 25)).rejects.toThrow(/did not map within 25ms/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('leaves the wait unbounded when the caller passes 0', async () => {
|
|
45
|
+
const state = await createWgpuRenderStateForTest();
|
|
46
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
47
|
+
let settle = (): void => {};
|
|
48
|
+
installCaptureBuffer(screen, () => new Promise<void>((resolve) => (settle = resolve)));
|
|
49
|
+
|
|
50
|
+
let done = false;
|
|
51
|
+
const pending = createBitmapFromWgpuScreenRenderTarget(screen, 0).then(() => (done = true));
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
53
|
+
expect(done).toBe(false);
|
|
54
|
+
|
|
55
|
+
settle();
|
|
56
|
+
await pending;
|
|
57
|
+
expect(done).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('enableWgpuScreenRenderTargetCapture', () => {
|
|
62
|
+
it('sets the capture flag on the screen target', async () => {
|
|
63
|
+
const state = await createWgpuRenderStateForTest();
|
|
64
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
65
|
+
expect(screen.captureEnabled).toBe(false);
|
|
66
|
+
enableWgpuScreenRenderTargetCapture(screen);
|
|
67
|
+
expect(screen.captureEnabled).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('redirects the frame into an offscreen texture rather than the swap chain', async () => {
|
|
71
|
+
// The whole reason capture exists: software and headless adapters never present the swap chain, and
|
|
72
|
+
// its texture reads back as zeros, so a captured frame must be drawn somewhere readable instead.
|
|
73
|
+
const state = await createWgpuRenderStateForTest();
|
|
74
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
75
|
+
expect(screen.captureTexture).toBeNull();
|
|
76
|
+
|
|
77
|
+
enableWgpuScreenRenderTargetCapture(screen);
|
|
78
|
+
endWgpuRenderPass(beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] }));
|
|
79
|
+
|
|
80
|
+
expect(screen.captureTexture).not.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('encodeWgpuScreenRenderTargetCapture', () => {
|
|
85
|
+
it('is a no-op when capture is disabled', async () => {
|
|
86
|
+
const state = await createWgpuRenderStateForTest();
|
|
87
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
88
|
+
let copied = false;
|
|
89
|
+
const encoder = { copyTextureToBuffer: () => (copied = true) } as unknown as GPUCommandEncoder;
|
|
90
|
+
encodeWgpuScreenRenderTargetCapture(screen, encoder);
|
|
91
|
+
expect(copied).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('skips the copy while a readback holds the buffer, and resumes once it lets go', async () => {
|
|
95
|
+
// The writer/reader handshake over the single retained buffer: an animating scene must not keep
|
|
96
|
+
// enqueueing copies into a buffer a readback has mapped or is waiting to map.
|
|
97
|
+
const state = await createWgpuRenderStateForTest();
|
|
98
|
+
const screen = captureReadyScreenRenderTarget(state);
|
|
99
|
+
let mapState: GPUBufferMapState = 'unmapped';
|
|
100
|
+
installCaptureBuffer(screen, () => Promise.resolve());
|
|
101
|
+
Object.defineProperty(screen.captureBuffer, 'mapState', { get: () => mapState });
|
|
102
|
+
|
|
103
|
+
let copies = 0;
|
|
104
|
+
const encoder = { copyTextureToBuffer: () => copies++ } as unknown as GPUCommandEncoder;
|
|
105
|
+
|
|
106
|
+
mapState = 'pending';
|
|
107
|
+
encodeWgpuScreenRenderTargetCapture(screen, encoder);
|
|
108
|
+
mapState = 'mapped';
|
|
109
|
+
encodeWgpuScreenRenderTargetCapture(screen, encoder);
|
|
110
|
+
expect(copies).toBe(0);
|
|
111
|
+
|
|
112
|
+
mapState = 'unmapped';
|
|
113
|
+
encodeWgpuScreenRenderTargetCapture(screen, encoder);
|
|
114
|
+
expect(copies).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('still copies when the implementation reports no map state at all', async () => {
|
|
118
|
+
// Fails safe in the direction that keeps capturing: an undefined mapState must not read as "busy"
|
|
119
|
+
// and silence every copy for the rest of the run.
|
|
120
|
+
const state = await createWgpuRenderStateForTest();
|
|
121
|
+
const screen = captureReadyScreenRenderTarget(state);
|
|
122
|
+
installCaptureBuffer(screen, () => Promise.resolve());
|
|
123
|
+
Object.defineProperty(screen.captureBuffer, 'mapState', { get: () => undefined });
|
|
124
|
+
|
|
125
|
+
let copies = 0;
|
|
126
|
+
encodeWgpuScreenRenderTargetCapture(screen, {
|
|
127
|
+
copyTextureToBuffer: () => copies++,
|
|
128
|
+
} as unknown as GPUCommandEncoder);
|
|
129
|
+
|
|
130
|
+
expect(copies).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// A screen target that has drawn one captured frame, so its capture texture exists.
|
|
135
|
+
function captureReadyScreenRenderTarget(
|
|
136
|
+
state: Awaited<ReturnType<typeof createWgpuRenderStateForTest>>,
|
|
137
|
+
): WgpuScreenRenderTarget {
|
|
138
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
139
|
+
enableWgpuScreenRenderTargetCapture(screen);
|
|
140
|
+
endWgpuRenderPass(beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] }));
|
|
141
|
+
return screen;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Installs a 1x1 retained capture buffer whose map step the caller controls, so the map bound can be
|
|
145
|
+
// exercised without a GPU. The mock device's buffers carry no map surface of their own.
|
|
146
|
+
function installCaptureBuffer(target: WgpuScreenRenderTarget, mapAsync: () => Promise<void>): void {
|
|
147
|
+
target.captureBuffer = {
|
|
148
|
+
mapAsync,
|
|
149
|
+
getMappedRange: () => new ArrayBuffer(256),
|
|
150
|
+
unmap: () => {},
|
|
151
|
+
destroy: () => {},
|
|
152
|
+
} as unknown as GPUBuffer;
|
|
153
|
+
target.captureWidth = 1;
|
|
154
|
+
target.captureHeight = 1;
|
|
155
|
+
target.captureBytesPerRow = 256;
|
|
156
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { beginWgpuRenderPass, endWgpuRenderPass } from './wgpuRenderPass';
|
|
2
|
+
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
3
|
+
import {
|
|
4
|
+
bindWgpuScreenRenderTarget,
|
|
5
|
+
createWgpuScreenRenderTarget,
|
|
6
|
+
destroyWgpuScreenRenderTarget,
|
|
7
|
+
endWgpuScreenRenderTargetFrame,
|
|
8
|
+
initializeWgpuScreenRenderTarget,
|
|
9
|
+
isWgpuScreenRenderTarget,
|
|
10
|
+
syncWgpuScreenRenderTargetExtent,
|
|
11
|
+
} from './wgpuScreenRenderTarget';
|
|
12
|
+
import { createWgpuRenderStateForTest, createWgpuScreenRenderTargetForTest, installWgpuMock } from './wgpuTestHelper';
|
|
13
|
+
import { createWgpuTextureRenderTarget } from './wgpuTextureRenderTarget';
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
installWgpuMock();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('bindWgpuScreenRenderTarget', () => {
|
|
20
|
+
it('acquires one swap-chain view per frame and releases it at the end of the frame', async () => {
|
|
21
|
+
// Nothing may hold a swap-chain view across frames — that is the property that makes a screen target
|
|
22
|
+
// unsampleable — so the view is acquired inside the frame and dropped when it is submitted.
|
|
23
|
+
const state = await createWgpuRenderStateForTest();
|
|
24
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
25
|
+
|
|
26
|
+
const first = bindWgpuScreenRenderTarget(state, screen);
|
|
27
|
+
expect(bindWgpuScreenRenderTarget(state, screen)).toBe(first);
|
|
28
|
+
expect(screen.presentationView).toBe(first);
|
|
29
|
+
|
|
30
|
+
endWgpuScreenRenderTargetFrame(screen);
|
|
31
|
+
expect(screen.presentationView).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('follows a surface that resized between frames', async () => {
|
|
35
|
+
const state = await createWgpuRenderStateForTest();
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
canvas.width = 320;
|
|
38
|
+
canvas.height = 240;
|
|
39
|
+
const screen = createWgpuScreenRenderTarget(state.device, canvas, { format: state.format });
|
|
40
|
+
|
|
41
|
+
canvas.width = 640;
|
|
42
|
+
canvas.height = 480;
|
|
43
|
+
bindWgpuScreenRenderTarget(state, screen);
|
|
44
|
+
|
|
45
|
+
expect(screen.width).toBe(640);
|
|
46
|
+
expect(screen.height).toBe(480);
|
|
47
|
+
expect(screen.depthStencilTexture.width).toBe(640);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('draws into supersample storage when the target is antialiased', async () => {
|
|
51
|
+
const state = await createWgpuRenderStateForTest();
|
|
52
|
+
const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true }, 400, 300);
|
|
53
|
+
|
|
54
|
+
const view = bindWgpuScreenRenderTarget(state, screen);
|
|
55
|
+
|
|
56
|
+
expect(screen.width).toBe(800);
|
|
57
|
+
expect(view).toBe(screen.antialiasView);
|
|
58
|
+
expect(view).not.toBe(screen.presentationView);
|
|
59
|
+
expect(screen.antialiasTexture?.width).toBe(800);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('createWgpuScreenRenderTarget', () => {
|
|
64
|
+
it('configures the surface context and declares one color attachment', async () => {
|
|
65
|
+
const state = await createWgpuRenderStateForTest();
|
|
66
|
+
const canvas = document.createElement('canvas');
|
|
67
|
+
canvas.width = 800;
|
|
68
|
+
canvas.height = 600;
|
|
69
|
+
const context = canvas.getContext('webgpu')!;
|
|
70
|
+
const configure = vi.spyOn(context, 'configure');
|
|
71
|
+
|
|
72
|
+
const screen = createWgpuScreenRenderTarget(state.device, canvas, { format: 'rgba8unorm' });
|
|
73
|
+
|
|
74
|
+
expect(configure).toHaveBeenCalledOnce();
|
|
75
|
+
expect(configure.mock.calls[0]![0].format).toBe('rgba8unorm');
|
|
76
|
+
expect(screen.context).toBe(context);
|
|
77
|
+
expect(screen.colorAttachments).toBe(1);
|
|
78
|
+
expect(screen.format).toBe('rgba8unorm');
|
|
79
|
+
expect(screen.width).toBe(800);
|
|
80
|
+
expect(screen.height).toBe(600);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('reports a surface the host cannot present rather than returning a target that cannot bind', async () => {
|
|
84
|
+
const state = await createWgpuRenderStateForTest();
|
|
85
|
+
const surface = { getContext: () => null, height: 100, width: 100 };
|
|
86
|
+
|
|
87
|
+
expect(() => createWgpuScreenRenderTarget(state.device, surface)).toThrow(/cannot present WebGPU/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('destroyWgpuScreenRenderTarget', () => {
|
|
92
|
+
it('frees the storage it owns and unconfigures the context', async () => {
|
|
93
|
+
const state = await createWgpuRenderStateForTest();
|
|
94
|
+
const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true });
|
|
95
|
+
bindWgpuScreenRenderTarget(state, screen);
|
|
96
|
+
const depth = vi.spyOn(screen.depthStencilTexture, 'destroy');
|
|
97
|
+
const antialias = vi.spyOn(screen.antialiasTexture!, 'destroy');
|
|
98
|
+
const unconfigure = vi.spyOn(screen.context, 'unconfigure');
|
|
99
|
+
|
|
100
|
+
destroyWgpuScreenRenderTarget(screen);
|
|
101
|
+
|
|
102
|
+
expect(depth).toHaveBeenCalledOnce();
|
|
103
|
+
expect(antialias).toHaveBeenCalledOnce();
|
|
104
|
+
expect(unconfigure).toHaveBeenCalledOnce();
|
|
105
|
+
expect(screen.antialiasView).toBeNull();
|
|
106
|
+
expect(screen.presentationView).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('endWgpuScreenRenderTargetFrame', () => {
|
|
111
|
+
it('is what a submitted frame calls, leaving no view behind', async () => {
|
|
112
|
+
const state = await createWgpuRenderStateForTest();
|
|
113
|
+
const screen = createWgpuScreenRenderTargetForTest(state);
|
|
114
|
+
|
|
115
|
+
endWgpuRenderPass(beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] }));
|
|
116
|
+
|
|
117
|
+
expect(screen.presentationView).toBeNull();
|
|
118
|
+
expect(getWgpuRenderStateRuntime(state).frameScreenTarget).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('initializeWgpuScreenRenderTarget', () => {
|
|
123
|
+
it('is the construction initializer of createWgpuScreenRenderTarget', () => {
|
|
124
|
+
expect(typeof initializeWgpuScreenRenderTarget).toBe('function');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('isWgpuScreenRenderTarget', () => {
|
|
129
|
+
it('separates the two realizations by the field that says where the pixels live', async () => {
|
|
130
|
+
const state = await createWgpuRenderStateForTest();
|
|
131
|
+
|
|
132
|
+
expect(isWgpuScreenRenderTarget(createWgpuScreenRenderTargetForTest(state))).toBe(true);
|
|
133
|
+
expect(isWgpuScreenRenderTarget(createWgpuTextureRenderTarget(state, 16, 16))).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('syncWgpuScreenRenderTargetExtent', () => {
|
|
138
|
+
it('follows the surface without a resize call', async () => {
|
|
139
|
+
const state = await createWgpuRenderStateForTest();
|
|
140
|
+
const canvas = document.createElement('canvas');
|
|
141
|
+
canvas.width = 320;
|
|
142
|
+
canvas.height = 240;
|
|
143
|
+
const screen = createWgpuScreenRenderTarget(state.device, canvas, { format: state.format });
|
|
144
|
+
|
|
145
|
+
canvas.width = 500;
|
|
146
|
+
canvas.height = 400;
|
|
147
|
+
syncWgpuScreenRenderTargetExtent(screen);
|
|
148
|
+
|
|
149
|
+
expect(screen.width).toBe(500);
|
|
150
|
+
expect(screen.height).toBe(400);
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/wgpuShader.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AdvancedBlendMode, BlendMode } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
|
-
import { renderWgpuBackground } from './wgpuBackground';
|
|
4
3
|
import { getWgpuRenderStateDeviceResources, getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
5
4
|
import {
|
|
6
5
|
UNIFORM_BYTE_SIZE,
|
|
@@ -14,7 +13,7 @@ import {
|
|
|
14
13
|
writeWgpuMatrixOnlyUniforms,
|
|
15
14
|
writeWgpuQuadUniforms,
|
|
16
15
|
} from './wgpuShader';
|
|
17
|
-
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
16
|
+
import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
18
17
|
|
|
19
18
|
beforeAll(() => {
|
|
20
19
|
installWgpuMock();
|
|
@@ -164,7 +163,7 @@ describe('writeWgpuMatrixOnlyUniforms', () => {
|
|
|
164
163
|
it('advances uniformOffset', async () => {
|
|
165
164
|
const state = await createWgpuRenderStateForTest();
|
|
166
165
|
const runtime = getWgpuRenderStateRuntime(state);
|
|
167
|
-
|
|
166
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
168
167
|
const before = runtime.uniformOffset;
|
|
169
168
|
const fakeNode = { alpha: 1, useColorScaleBias: false, colorScaleBias: null };
|
|
170
169
|
const t = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
@@ -176,7 +175,7 @@ describe('writeWgpuQuadUniforms', () => {
|
|
|
176
175
|
it('advances uniformOffset by uniformStride', async () => {
|
|
177
176
|
const state = await createWgpuRenderStateForTest();
|
|
178
177
|
const runtime = getWgpuRenderStateRuntime(state);
|
|
179
|
-
|
|
178
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
180
179
|
const before = runtime.uniformOffset;
|
|
181
180
|
const fakeNode = {
|
|
182
181
|
alpha: 1,
|
|
@@ -191,7 +190,7 @@ describe('writeWgpuQuadUniforms', () => {
|
|
|
191
190
|
it('writes quad coordinates into uniform data at float offset 24', async () => {
|
|
192
191
|
const state = await createWgpuRenderStateForTest();
|
|
193
192
|
const runtime = getWgpuRenderStateRuntime(state);
|
|
194
|
-
|
|
193
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
195
194
|
const fakeNode = {
|
|
196
195
|
alpha: 0.5,
|
|
197
196
|
useColorScaleBias: false,
|
|
@@ -211,7 +210,7 @@ describe('writeWgpuQuadUniforms', () => {
|
|
|
211
210
|
it('writes the native straight-alpha texture flag at u32 offset 14', async () => {
|
|
212
211
|
const state = await createWgpuRenderStateForTest();
|
|
213
212
|
const runtime = getWgpuRenderStateRuntime(state);
|
|
214
|
-
|
|
213
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
215
214
|
const fakeNode = {
|
|
216
215
|
alpha: 1,
|
|
217
216
|
useColorScaleBias: false,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
|
+
|
|
3
|
+
import { resolveWgpuSmoothingBindGroup } from './wgpuDraw';
|
|
4
|
+
import { submitWgpuFrame } from './wgpuFrame';
|
|
5
|
+
import { getWgpuRenderStateDeviceResources } from './wgpuRenderState';
|
|
6
|
+
import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
7
|
+
import {
|
|
8
|
+
createWgpuTextureRenderTarget,
|
|
9
|
+
destroyWgpuTextureRenderTarget,
|
|
10
|
+
drawWgpuTextureRenderTargetResult,
|
|
11
|
+
initializeWgpuTextureRenderTarget,
|
|
12
|
+
resizeWgpuTextureRenderTarget,
|
|
13
|
+
} from './wgpuTextureRenderTarget';
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
installWgpuMock();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('createWgpuTextureRenderTarget', () => {
|
|
20
|
+
it('returns a target with texture, view, binding cache, and depth-stencil', async () => {
|
|
21
|
+
const state = await createWgpuRenderStateForTest();
|
|
22
|
+
const target = createWgpuTextureRenderTarget(state, 256, 256);
|
|
23
|
+
expect(target.texture).toBeDefined();
|
|
24
|
+
expect(target.view).toBeDefined();
|
|
25
|
+
expect(target.bindings).toBeDefined();
|
|
26
|
+
expect(target.depthStencilTexture).toBeDefined();
|
|
27
|
+
expect(target.width).toBe(256);
|
|
28
|
+
expect(target.height).toBe(256);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('clamps to minimum 1×1', async () => {
|
|
32
|
+
const state = await createWgpuRenderStateForTest();
|
|
33
|
+
const target = createWgpuTextureRenderTarget(state, 0, 0);
|
|
34
|
+
expect(target.width).toBe(1);
|
|
35
|
+
expect(target.height).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('defaults to sRGB and accepts a linear content declaration', async () => {
|
|
39
|
+
const state = await createWgpuRenderStateForTest();
|
|
40
|
+
expect(createWgpuTextureRenderTarget(state, 16, 16).colorSpace).toBe('srgb');
|
|
41
|
+
expect(createWgpuTextureRenderTarget(state, 16, 16, state.format, 'linear').colorSpace).toBe('linear');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('realizes four coverage samples as a 2x extent in each axis', async () => {
|
|
45
|
+
const state = await createWgpuRenderStateForTest();
|
|
46
|
+
const target = createWgpuTextureRenderTarget(state, 64, 48, state.format, 'srgb', 4);
|
|
47
|
+
expect(target.width).toBe(128);
|
|
48
|
+
expect(target.height).toBe(96);
|
|
49
|
+
expect(target.sampleCount).toBe(4);
|
|
50
|
+
expect(target.texture.width).toBe(128);
|
|
51
|
+
expect(target.texture.height).toBe(96);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('destroyWgpuTextureRenderTarget', () => {
|
|
56
|
+
it('calls destroy on texture and depth-stencil', async () => {
|
|
57
|
+
const state = await createWgpuRenderStateForTest();
|
|
58
|
+
const target = createWgpuTextureRenderTarget(state, 64, 64);
|
|
59
|
+
const destroyTexture = vi.spyOn(target.texture, 'destroy');
|
|
60
|
+
const destroyDepth = vi.spyOn(target.depthStencilTexture, 'destroy');
|
|
61
|
+
destroyWgpuTextureRenderTarget(target);
|
|
62
|
+
expect(destroyTexture).toHaveBeenCalled();
|
|
63
|
+
expect(destroyDepth).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('drawWgpuTextureRenderTargetResult', () => {
|
|
68
|
+
it('does not throw with an open render pass', async () => {
|
|
69
|
+
const state = await createWgpuRenderStateForTest();
|
|
70
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
71
|
+
const target = createWgpuTextureRenderTarget(state, 64, 64);
|
|
72
|
+
const fakeNode = {
|
|
73
|
+
alpha: 1,
|
|
74
|
+
material: null,
|
|
75
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
76
|
+
};
|
|
77
|
+
expect(() => drawWgpuTextureRenderTargetResult(state, fakeNode, target, createMatrix())).not.toThrow();
|
|
78
|
+
submitWgpuFrame(state);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('initializeWgpuTextureRenderTarget', () => {
|
|
83
|
+
it('is the construction initializer of createWgpuTextureRenderTarget', () => {
|
|
84
|
+
expect(typeof initializeWgpuTextureRenderTarget).toBe('function');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('resizeWgpuTextureRenderTarget', () => {
|
|
89
|
+
it('preserves the allocation when dimensions are unchanged', async () => {
|
|
90
|
+
const state = await createWgpuRenderStateForTest();
|
|
91
|
+
const target = createWgpuTextureRenderTarget(state, 64, 64);
|
|
92
|
+
const texture = target.texture;
|
|
93
|
+
const depthStencilTexture = target.depthStencilTexture;
|
|
94
|
+
const sampler = getWgpuRenderStateDeviceResources(state).linearSampler;
|
|
95
|
+
const bindGroup = resolveWgpuSmoothingBindGroup(state, target, true);
|
|
96
|
+
|
|
97
|
+
resizeWgpuTextureRenderTarget(state, target, 64, 64);
|
|
98
|
+
|
|
99
|
+
expect(target.texture).toBe(texture);
|
|
100
|
+
expect(target.depthStencilTexture).toBe(depthStencilTexture);
|
|
101
|
+
// The view survived, so its cached bindings must survive with it.
|
|
102
|
+
expect(target.bindings.get(sampler)).toBe(bindGroup);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('updates width, height, and drops bindings that referenced the old view', async () => {
|
|
106
|
+
const state = await createWgpuRenderStateForTest();
|
|
107
|
+
const target = createWgpuTextureRenderTarget(state, 64, 64);
|
|
108
|
+
const previousBindGroup = resolveWgpuSmoothingBindGroup(state, target, true);
|
|
109
|
+
resizeWgpuTextureRenderTarget(state, target, 200, 150);
|
|
110
|
+
expect(target.width).toBe(200);
|
|
111
|
+
expect(target.height).toBe(150);
|
|
112
|
+
// Reallocation replaces the view, so every binding over the old one is dropped and rebuilt.
|
|
113
|
+
expect(target.bindings.size).toBe(0);
|
|
114
|
+
expect(resolveWgpuSmoothingBindGroup(state, target, true)).not.toBe(previousBindGroup);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('reallocates when the effective sample count changes', async () => {
|
|
118
|
+
const state = await createWgpuRenderStateForTest();
|
|
119
|
+
const target = createWgpuTextureRenderTarget(state, 64, 48);
|
|
120
|
+
const previousTexture = target.texture;
|
|
121
|
+
|
|
122
|
+
resizeWgpuTextureRenderTarget(state, target, 64, 48, 4);
|
|
123
|
+
|
|
124
|
+
expect(target.width).toBe(128);
|
|
125
|
+
expect(target.height).toBe(96);
|
|
126
|
+
expect(target.sampleCount).toBe(4);
|
|
127
|
+
expect(target.texture).not.toBe(previousTexture);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
RenderTargetTextureSourceKind,
|
|
9
9
|
} from '@flighthq/types/contract';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { submitWgpuFrame } from './wgpuFrame';
|
|
12
12
|
import { getWgpuRenderStateRuntime } from './wgpuRenderState';
|
|
13
13
|
import { renderIntoWgpuRenderTexture } from './wgpuRenderTexture';
|
|
14
|
-
import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
14
|
+
import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
|
|
15
15
|
import {
|
|
16
16
|
registerWgpuImageTextureResolver,
|
|
17
17
|
registerWgpuBitmapTextureResolver,
|
|
@@ -177,10 +177,10 @@ describe('registerWgpuRenderTextureResolver', () => {
|
|
|
177
177
|
registerWgpuRenderTextureResolver(state);
|
|
178
178
|
expect(resolveWgpuTexture(state, texture)).toBeNull();
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
renderIntoWgpuRenderTexture(state, texture, () => {});
|
|
180
|
+
beginWgpuScreenRenderPassForTest(state);
|
|
181
|
+
renderIntoWgpuRenderTexture(state, texture, () => {}, { color: [0, 0, 0, 0], depth: 1.0, stencil: 0 });
|
|
182
182
|
expect(resolveWgpuTexture(state, texture)).not.toBeNull();
|
|
183
|
-
|
|
183
|
+
submitWgpuFrame(state);
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
186
|
|
package/dist/wgpuAntialias.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { WgpuPresentationRenderState, WgpuRenderState } from '@flighthq/types/contract';
|
|
2
|
-
export declare function acquireWgpuSurfaceAntialiasView(state: Readonly<WgpuPresentationRenderState>, presentationView: GPUTextureView): GPUTextureView | null;
|
|
3
|
-
export declare function clearWgpuSurfacePresentation(state: Readonly<WgpuPresentationRenderState>): void;
|
|
4
|
-
export declare function encodeWgpuSurfaceAntialiasResolve(state: Readonly<WgpuPresentationRenderState>, encoder: GPUCommandEncoder): void;
|
|
5
|
-
export declare function getWgpuSurfaceLogicalExtent(state: Readonly<WgpuRenderState>): {
|
|
6
|
-
width: number;
|
|
7
|
-
height: number;
|
|
8
|
-
};
|
|
9
|
-
export declare function getWgpuSurfaceRenderExtent(state: Readonly<WgpuRenderState>): {
|
|
10
|
-
width: number;
|
|
11
|
-
height: number;
|
|
12
|
-
};
|
|
13
|
-
export declare function getWgpuSurfaceRenderScale(state: Readonly<WgpuRenderState>): number;
|
|
14
|
-
//# sourceMappingURL=wgpuAntialias.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"wgpuAntialias.d.ts","sourceRoot":"","sources":["../src/wgpuAntialias.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAS7F,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,EAC5C,gBAAgB,EAAE,cAAc,GAC/B,cAAc,GAAG,IAAI,CA8CvB;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,GAAG,IAAI,CAE/F;AAKD,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,EAC5C,OAAO,EAAE,iBAAiB,GACzB,IAAI,CAqBN;AAKD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAQ/G;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAW9G;AAID,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,MAAM,CAGlF"}
|