@flighthq/effects-wgpu 0.3.1-next.1055.12501ae → 0.3.1-next.1076.4f48950
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/effects-wgpu",
|
|
3
|
-
"version": "0.3.1-next.
|
|
3
|
+
"version": "0.3.1-next.1076.4f48950",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/adjustments": "0.3.1-next.
|
|
42
|
-
"@flighthq/color": "0.3.1-next.
|
|
43
|
-
"@flighthq/effects": "0.3.1-next.
|
|
44
|
-
"@flighthq/geometry": "0.3.1-next.
|
|
45
|
-
"@flighthq/log": "0.3.1-next.
|
|
46
|
-
"@flighthq/render-wgpu": "0.3.1-next.
|
|
47
|
-
"@flighthq/registry": "0.3.1-next.
|
|
48
|
-
"@flighthq/types": "0.3.1-next.
|
|
41
|
+
"@flighthq/adjustments": "0.3.1-next.1076.4f48950",
|
|
42
|
+
"@flighthq/color": "0.3.1-next.1076.4f48950",
|
|
43
|
+
"@flighthq/effects": "0.3.1-next.1076.4f48950",
|
|
44
|
+
"@flighthq/geometry": "0.3.1-next.1076.4f48950",
|
|
45
|
+
"@flighthq/log": "0.3.1-next.1076.4f48950",
|
|
46
|
+
"@flighthq/render-wgpu": "0.3.1-next.1076.4f48950",
|
|
47
|
+
"@flighthq/registry": "0.3.1-next.1076.4f48950",
|
|
48
|
+
"@flighthq/types": "0.3.1-next.1076.4f48950"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@webgpu/types": "^0.1.70"
|
|
@@ -13,26 +13,177 @@ import {
|
|
|
13
13
|
beforeAll(() => installWgpuMock());
|
|
14
14
|
|
|
15
15
|
describe('beginWgpuRenderEffectPipeline', () => {
|
|
16
|
-
it('
|
|
17
|
-
|
|
16
|
+
it('allocates the scene target on first begin and reuses the same one afterwards', async () => {
|
|
17
|
+
const state = await createWgpuRenderStateForTest();
|
|
18
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
19
|
+
|
|
20
|
+
beginWgpuFrame(state);
|
|
21
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
22
|
+
const first = pipeline.sceneTarget;
|
|
23
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
24
|
+
|
|
25
|
+
beginWgpuFrame(state);
|
|
26
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
27
|
+
|
|
28
|
+
expect(first).not.toBeNull();
|
|
29
|
+
// The retained-target contract: begin resizes rather than reallocating, so a per-frame pipeline
|
|
30
|
+
// does not churn GPU memory. A new object here would be a leak the pool cannot see.
|
|
31
|
+
expect(pipeline.sceneTarget).toBe(first);
|
|
32
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('stamps the requested color space onto the scene target before the pass opens', async () => {
|
|
36
|
+
const state = await createWgpuRenderStateForTest();
|
|
37
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
38
|
+
|
|
39
|
+
beginWgpuFrame(state);
|
|
40
|
+
beginWgpuRenderEffectPipeline(state, pipeline, 'linear');
|
|
41
|
+
|
|
42
|
+
expect(pipeline.sceneTarget?.colorSpace).toBe('linear');
|
|
43
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('packs the background quadruple into one 0xRRGGBBAA clear color', async () => {
|
|
47
|
+
const state = await createWgpuRenderStateForTest();
|
|
48
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
49
|
+
// Quarter/half/three-quarter channels, chosen so a wrong byte order cannot coincide with the
|
|
50
|
+
// right one — the ground truth is arithmetic here, not a value read back out of the source.
|
|
51
|
+
state.backgroundColorRgba.splice(0, state.backgroundColorRgba.length, 0x40 / 255, 0x80 / 255, 0xc0 / 255, 1);
|
|
52
|
+
|
|
53
|
+
beginWgpuFrame(state);
|
|
54
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
55
|
+
|
|
56
|
+
expect(pipeline.sceneTarget?.clearColors).toEqual([0x4080c0ff]);
|
|
57
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('leaves the clear list empty when the background is not a full quadruple', async () => {
|
|
61
|
+
const state = await createWgpuRenderStateForTest();
|
|
62
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
63
|
+
state.backgroundColorRgba.splice(0, state.backgroundColorRgba.length, 0.5, 0.5, 0.5);
|
|
64
|
+
|
|
65
|
+
beginWgpuFrame(state);
|
|
66
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
67
|
+
|
|
68
|
+
// Empty means "no background to composite" — a different frame than a transparent clear, and the
|
|
69
|
+
// reason the target falls back to the render state's own background instead.
|
|
70
|
+
expect(pipeline.sceneTarget?.clearColors).toEqual([]);
|
|
71
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
18
72
|
});
|
|
19
73
|
});
|
|
20
74
|
|
|
21
75
|
describe('createWgpuRenderEffectPipeline', () => {
|
|
22
|
-
it('
|
|
23
|
-
|
|
76
|
+
it('returns a pipeline with nothing allocated yet', async () => {
|
|
77
|
+
const state = await createWgpuRenderStateForTest();
|
|
78
|
+
|
|
79
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
80
|
+
|
|
81
|
+
expect(pipeline.sceneTarget).toBeNull();
|
|
82
|
+
expect(pipeline.velocityTexture).toBeNull();
|
|
83
|
+
expect(pipeline.lutTexture).toEqual({ texture: null, size: 0, lut: null });
|
|
84
|
+
expect(pipeline.lutCache.signature).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('copies the options rather than aliasing the caller object', async () => {
|
|
88
|
+
const state = await createWgpuRenderStateForTest();
|
|
89
|
+
const options = { format: 'rgba16f' as const };
|
|
90
|
+
|
|
91
|
+
const pipeline = createWgpuRenderEffectPipeline(state, options);
|
|
92
|
+
options.format = 'rgba8' as unknown as 'rgba16f';
|
|
93
|
+
|
|
94
|
+
// The pipeline outlives the call; an alias would let a caller retune the format of a pipeline
|
|
95
|
+
// whose scene target was already allocated against the old one.
|
|
96
|
+
expect(pipeline.options.format).toBe('rgba16f');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('gives each pipeline its own pool and caches', async () => {
|
|
100
|
+
const state = await createWgpuRenderStateForTest();
|
|
101
|
+
|
|
102
|
+
const a = createWgpuRenderEffectPipeline(state);
|
|
103
|
+
const b = createWgpuRenderEffectPipeline(state);
|
|
104
|
+
|
|
105
|
+
expect(a.pool).not.toBe(b.pool);
|
|
106
|
+
expect(a.lutCache).not.toBe(b.lutCache);
|
|
107
|
+
expect(a.lutTexture).not.toBe(b.lutTexture);
|
|
24
108
|
});
|
|
25
109
|
});
|
|
26
110
|
|
|
27
111
|
describe('destroyWgpuRenderEffectPipeline', () => {
|
|
28
|
-
it('
|
|
29
|
-
|
|
112
|
+
it('releases the scene target and clears the LUT caches', async () => {
|
|
113
|
+
const state = await createWgpuRenderStateForTest();
|
|
114
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
115
|
+
beginWgpuFrame(state);
|
|
116
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
117
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
118
|
+
expect(pipeline.sceneTarget).not.toBeNull();
|
|
119
|
+
|
|
120
|
+
destroyWgpuRenderEffectPipeline(state, pipeline);
|
|
121
|
+
|
|
122
|
+
expect(pipeline.sceneTarget).toBeNull();
|
|
123
|
+
expect(pipeline.lutTexture).toEqual({ texture: null, size: 0, lut: null });
|
|
124
|
+
expect(pipeline.lutCache.signature).toBeNull();
|
|
125
|
+
expect(pipeline.lutCache.lut).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('is safe on a pipeline that never began a frame', async () => {
|
|
129
|
+
const state = await createWgpuRenderStateForTest();
|
|
130
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
131
|
+
|
|
132
|
+
expect(() => destroyWgpuRenderEffectPipeline(state, pipeline)).not.toThrow();
|
|
133
|
+
expect(pipeline.sceneTarget).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('is idempotent — a second destroy neither throws nor resurrects state', async () => {
|
|
137
|
+
const state = await createWgpuRenderStateForTest();
|
|
138
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
139
|
+
beginWgpuFrame(state);
|
|
140
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
141
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
142
|
+
|
|
143
|
+
destroyWgpuRenderEffectPipeline(state, pipeline);
|
|
144
|
+
expect(() => destroyWgpuRenderEffectPipeline(state, pipeline)).not.toThrow();
|
|
145
|
+
expect(pipeline.sceneTarget).toBeNull();
|
|
30
146
|
});
|
|
31
147
|
});
|
|
32
148
|
|
|
33
149
|
describe('endWgpuRenderEffectPipeline', () => {
|
|
34
|
-
it('
|
|
35
|
-
|
|
150
|
+
it('returns without touching the pipeline when no scene target was begun', async () => {
|
|
151
|
+
const state = await createWgpuRenderStateForTest();
|
|
152
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
153
|
+
const pool = pipeline.pool;
|
|
154
|
+
|
|
155
|
+
// The early return is the only thing standing between an unbegun pipeline and endWgpuRenderPass
|
|
156
|
+
// popping a pass that was never pushed, so it is asserted rather than assumed.
|
|
157
|
+
expect(() => endWgpuRenderEffectPipeline(state, pipeline, [])).not.toThrow();
|
|
158
|
+
expect(pipeline.sceneTarget).toBeNull();
|
|
159
|
+
expect(pipeline.pool).toBe(pool);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('runs an empty operation list without allocating scratch targets', async () => {
|
|
163
|
+
const state = await createWgpuRenderStateForTest();
|
|
164
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
165
|
+
|
|
166
|
+
beginWgpuFrame(state);
|
|
167
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
168
|
+
endWgpuRenderEffectPipeline(state, pipeline, []);
|
|
169
|
+
|
|
170
|
+
// Every acquire must be matched by a release before end returns; a pool holding entries after an
|
|
171
|
+
// empty chain would mean the ping-pong leaked a target on the no-op path.
|
|
172
|
+
expect(pipeline.pool.free.length).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns pooled scratch targets after running a chain', async () => {
|
|
176
|
+
const state = await createWgpuRenderStateForTest();
|
|
177
|
+
const pipeline = createWgpuRenderEffectPipeline(state);
|
|
178
|
+
const chain = [{ kind: 'test.wgpu-pipeline-unregistered' } as RenderEffect];
|
|
179
|
+
|
|
180
|
+
beginWgpuFrame(state);
|
|
181
|
+
beginWgpuRenderEffectPipeline(state, pipeline);
|
|
182
|
+
endWgpuRenderEffectPipeline(state, pipeline, chain);
|
|
183
|
+
|
|
184
|
+
// An unregistered kind is skipped, so the chain does no work — but the bracketing still has to
|
|
185
|
+
// balance. This is the acquire/release invariant measured at its cheapest observable point.
|
|
186
|
+
expect(pipeline.pool.free.every((target) => target !== pipeline.sceneTarget)).toBe(true);
|
|
36
187
|
});
|
|
37
188
|
});
|
|
38
189
|
|