@forgeax/engine-runtime 0.1.27 → 0.1.29
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 +98 -0
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +480 -15
- package/dist/index.mjs.map +1 -1
- package/dist/scene-utils/mount-override-fold.d.ts +3 -1
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/package.json +37 -30
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
- package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
- package/src/__tests__/asset-registry-mounts.test.ts +174 -734
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
- package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
- package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
- package/src/__tests__/collect-scene-asset.test.ts +96 -113
- package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
- package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
- package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
- package/src/__tests__/collect-transient-skip.test.ts +20 -27
- package/src/__tests__/components.test-d.ts +17 -1
- package/src/__tests__/components.unit.test.ts +75 -27
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
- package/src/__tests__/feature-depth-material.fixture.ts +473 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
- package/src/__tests__/geometry.unit.test.ts +42 -43
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
- package/src/__tests__/handle-migration.test.ts +0 -1
- package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
- package/src/__tests__/instances.test-d.ts +13 -52
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
- package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
- package/src/__tests__/material-publication.browser.test.ts +29 -0
- package/src/__tests__/material-publication.commands.ts +23 -0
- package/src/__tests__/material-publication.dawn.test.ts +24 -0
- package/src/__tests__/material-publication.fixture.ts +349 -0
- package/src/__tests__/material-publication.server.ts +109 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
- package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
- package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
- package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
- package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
- package/src/__tests__/record-worldid-isolation.test.ts +47 -28
- package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
- package/src/__tests__/render-system-extract.test.ts +61 -0
- package/src/__tests__/render-system-extract.unit.test.ts +21 -7
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
- package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
- package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
- package/src/__tests__/render-system-mega.test.ts +50 -62
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
- package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
- package/src/__tests__/roundtrip-primitive.test.ts +24 -23
- package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
- package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
- package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
- package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
- package/src/__tests__/serialize-scene-asset.test.ts +36 -50
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
- package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
- package/src/__tests__/transient-fields-collect.test.ts +4 -4
- package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
- package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
- package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
- package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
- package/src/__tests__/wave1-dynamic-geometry.browser.test.ts +326 -0
- package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
- package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
- package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
- package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
- package/src/__tests__/world-scene-despawn.test.ts +8 -8
- package/src/__tests__/world-scene-instantiate.test.ts +23 -29
- package/src/__tests__/world-scene-nested.test.ts +90 -163
- package/src/__tests__/world-scene-override.test.ts +17 -16
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
- package/src/collect-scene-asset.ts +637 -19
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/index.ts +1 -0
- package/src/scene-utils/mount-override-fold.ts +5 -14
- package/src/__tests__/node-shims.d.ts +0 -31
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { stdout } from 'node:process';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { World } from '@forgeax/engine-ecs';
|
|
5
|
+
import { createBoxGeometry } from '@forgeax/engine-geometry';
|
|
6
|
+
import { AssetGuid } from '@forgeax/engine-pack';
|
|
7
|
+
import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
|
|
8
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
9
|
+
import type { MaterialAsset } from '@forgeax/engine-types';
|
|
10
|
+
import { buildEngineShaderManifest } from '@forgeax/engine-vite-plugin-shader';
|
|
3
11
|
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { constructRuntimeRendererHost } from '../../renderer-host';
|
|
4
13
|
|
|
5
14
|
type CookedRecord = {
|
|
6
15
|
readonly specializationKey: string;
|
|
@@ -44,6 +53,13 @@ const fixture = JSON.parse(
|
|
|
44
53
|
),
|
|
45
54
|
) as { readonly assets: readonly MaterialRow[] };
|
|
46
55
|
|
|
56
|
+
const authoredShaderPackage = fileURLToPath(
|
|
57
|
+
new URL(
|
|
58
|
+
'../../../../../apps/hello/custom-shader/src/pulse-material.shader.pack.json',
|
|
59
|
+
import.meta.url,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
47
63
|
const materialRows = fixture.assets.filter((asset) => asset.kind === 'material');
|
|
48
64
|
const root = materialRows.find((asset) => asset.payload.parent === undefined);
|
|
49
65
|
const derived = materialRows.find((asset) => asset.payload.parent === root?.guid);
|
|
@@ -98,8 +114,188 @@ describe('custom-shader cooked MaterialAsset fixture', () => {
|
|
|
98
114
|
});
|
|
99
115
|
});
|
|
100
116
|
|
|
101
|
-
it('
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
it('loads the published material and draws 300 real Engine frames', async () => {
|
|
118
|
+
const manifest = await buildEngineShaderManifest({
|
|
119
|
+
materialPackages: [authoredShaderPackage],
|
|
120
|
+
});
|
|
121
|
+
const dataUrl = (value: unknown) =>
|
|
122
|
+
`data:application/json,${encodeURIComponent(JSON.stringify(value))}`;
|
|
123
|
+
const gpuErrors: string[] = [];
|
|
124
|
+
let device: GPUDevice | undefined;
|
|
125
|
+
let target: GPUTexture | undefined;
|
|
126
|
+
const canvas = {
|
|
127
|
+
width: 64,
|
|
128
|
+
height: 64,
|
|
129
|
+
getContext: () => ({
|
|
130
|
+
configure(descriptor: GPUCanvasConfiguration) {
|
|
131
|
+
device = descriptor.device;
|
|
132
|
+
device.addEventListener('uncapturederror', (event) =>
|
|
133
|
+
gpuErrors.push(event.error.message),
|
|
134
|
+
);
|
|
135
|
+
target = device.createTexture({
|
|
136
|
+
size: [64, 64],
|
|
137
|
+
format: descriptor.format,
|
|
138
|
+
viewFormats: [
|
|
139
|
+
descriptor.format === 'bgra8unorm' ? 'bgra8unorm-srgb' : 'rgba8unorm-srgb',
|
|
140
|
+
],
|
|
141
|
+
usage: 0x10 | 0x01,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
unconfigure() {},
|
|
145
|
+
getCurrentTexture: () => target,
|
|
146
|
+
}),
|
|
147
|
+
addEventListener() {},
|
|
148
|
+
removeEventListener() {},
|
|
149
|
+
};
|
|
150
|
+
const constructed = await constructRuntimeRendererHost(
|
|
151
|
+
canvas as never,
|
|
152
|
+
{},
|
|
153
|
+
{ shaderManifestUrl: dataUrl(manifest) },
|
|
154
|
+
);
|
|
155
|
+
if (!constructed.ok) throw constructed.error;
|
|
156
|
+
const { renderer, assets } = constructed.value;
|
|
157
|
+
const errors: unknown[] = [];
|
|
158
|
+
renderer.subscribe((event) => {
|
|
159
|
+
if (event.kind === 'error') errors.push(event.error);
|
|
160
|
+
});
|
|
161
|
+
const world = new World();
|
|
162
|
+
try {
|
|
163
|
+
// The fixture's parent-less material is the publication root. Keep the
|
|
164
|
+
// runtime smoke on the same authority used by the contract assertions
|
|
165
|
+
// above; the pack payload does not carry a second `role` discriminator.
|
|
166
|
+
if (root === undefined) throw new Error('missing root publication');
|
|
167
|
+
const packUrl = dataUrl(fixture);
|
|
168
|
+
assets.configurePackIndex(
|
|
169
|
+
dataUrl(
|
|
170
|
+
fixture.assets.map((asset) => ({
|
|
171
|
+
guid: asset.guid,
|
|
172
|
+
kind: asset.kind,
|
|
173
|
+
packageUrl: packUrl,
|
|
174
|
+
})),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
for (const [guid, color] of [
|
|
178
|
+
['01935b00-7d8c-7c4e-9f12-345678abcd11', [220, 100, 40, 255]],
|
|
179
|
+
['01935b00-7d8c-7c4e-9f12-345678abcd12', [128, 128, 255, 255]],
|
|
180
|
+
] as const) {
|
|
181
|
+
const parsed = AssetGuid.parse(guid);
|
|
182
|
+
if (!parsed.ok) throw parsed.error;
|
|
183
|
+
const cataloged = assets.catalog(parsed.value, {
|
|
184
|
+
kind: 'texture',
|
|
185
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
186
|
+
format: 'rgba8unorm',
|
|
187
|
+
data: new Uint8Array(color),
|
|
188
|
+
colorSpace: 'linear',
|
|
189
|
+
mips: { kind: 'none' },
|
|
190
|
+
});
|
|
191
|
+
if (!cataloged.ok) throw cataloged.error;
|
|
192
|
+
}
|
|
193
|
+
const rootGuid = AssetGuid.parse(root.guid);
|
|
194
|
+
if (!rootGuid.ok) throw rootGuid.error;
|
|
195
|
+
const loaded = await assets.loadByGuid<MaterialAsset>(rootGuid.value);
|
|
196
|
+
if (!loaded.ok) throw loaded.error;
|
|
197
|
+
const readiness = assets.getMaterialReadiness(root.guid);
|
|
198
|
+
expect(readiness?.status).toBe('Ready');
|
|
199
|
+
if (readiness?.status !== 'Ready') throw new Error('material never became ready');
|
|
200
|
+
expect(readiness.artifactDigest).toBe(root.payload.cooked?.artifactDigest);
|
|
201
|
+
const module = loaded.value.passes?.[0]?.program.module;
|
|
202
|
+
if (module === undefined) throw new Error('material has no program');
|
|
203
|
+
const installed = assets.shaderRegistry.findMaterialArtifact(module);
|
|
204
|
+
if (!installed.ok) throw installed.error;
|
|
205
|
+
const program = readiness.programs.find((candidate) =>
|
|
206
|
+
candidate.selections.some((selection) => selection.pass === 'Forward'),
|
|
207
|
+
);
|
|
208
|
+
if (program === undefined) throw new Error('material has no forward program');
|
|
209
|
+
expect(installed.value.source).toBe(new TextDecoder().decode(program.artifact.bytes));
|
|
210
|
+
world
|
|
211
|
+
.spawn(
|
|
212
|
+
{ component: Transform, data: { pos: [0, 0, 3], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
|
|
213
|
+
{ component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } },
|
|
214
|
+
)
|
|
215
|
+
.unwrap();
|
|
216
|
+
const attached = renderer.attach(world);
|
|
217
|
+
if (!attached.ok) throw attached.error;
|
|
218
|
+
const lease = attached.value;
|
|
219
|
+
const frame = { leases: [lease], camera: { lease }, environment: { lease } };
|
|
220
|
+
const draw = () => {
|
|
221
|
+
world.update().unwrap();
|
|
222
|
+
const rendered = renderer.draw(frame);
|
|
223
|
+
if (!rendered.ok) throw rendered.error;
|
|
224
|
+
};
|
|
225
|
+
const readPixel = async () => {
|
|
226
|
+
if (device === undefined || target === undefined) throw new Error('missing GPU surface');
|
|
227
|
+
const readback = device.createBuffer({
|
|
228
|
+
size: 256,
|
|
229
|
+
usage: 0x08 | 0x01,
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
const encoder = device.createCommandEncoder();
|
|
233
|
+
encoder.copyTextureToBuffer(
|
|
234
|
+
{ texture: target, origin: [32, 32] },
|
|
235
|
+
{ buffer: readback, bytesPerRow: 256 },
|
|
236
|
+
[1, 1],
|
|
237
|
+
);
|
|
238
|
+
device.queue.submit([encoder.finish()]);
|
|
239
|
+
await readback.mapAsync(0x01);
|
|
240
|
+
const pixel = [...new Uint8Array(readback.getMappedRange()).slice(0, 4)];
|
|
241
|
+
readback.unmap();
|
|
242
|
+
return pixel;
|
|
243
|
+
} finally {
|
|
244
|
+
readback.destroy();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
draw();
|
|
248
|
+
const background = await readPixel();
|
|
249
|
+
const mesh = createBoxGeometry(1, 1, 1).unwrap();
|
|
250
|
+
const entity = world
|
|
251
|
+
.spawn(
|
|
252
|
+
{ component: Transform, data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
|
|
253
|
+
{ component: MeshFilter, data: { assetHandle: world.allocSharedRef('MeshAsset', mesh) } },
|
|
254
|
+
{
|
|
255
|
+
component: MeshRenderer,
|
|
256
|
+
data: { materials: [world.internSharedRef('MaterialAsset', loaded.value)] },
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
.unwrap();
|
|
260
|
+
for (let index = 0; index < 300; index++) {
|
|
261
|
+
draw();
|
|
262
|
+
// Match the host's frame boundary so queued shader/texture preparation runs.
|
|
263
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
264
|
+
}
|
|
265
|
+
const pixel = await readPixel();
|
|
266
|
+
expect(errors).toEqual([]);
|
|
267
|
+
expect(gpuErrors).toEqual([]);
|
|
268
|
+
expect(
|
|
269
|
+
pixel
|
|
270
|
+
.slice(0, 3)
|
|
271
|
+
.reduce((delta, value, index) => delta + Math.abs(value - (background[index] ?? 0)), 0),
|
|
272
|
+
).toBeGreaterThan(32);
|
|
273
|
+
world.despawn(entity).unwrap();
|
|
274
|
+
draw();
|
|
275
|
+
const removed = await readPixel();
|
|
276
|
+
expect(removed).toEqual(background);
|
|
277
|
+
expect(errors).toEqual([]);
|
|
278
|
+
expect(gpuErrors).toEqual([]);
|
|
279
|
+
if (process.env.FORGEAX_MATERIAL_SMOKE_EVIDENCE === '1') {
|
|
280
|
+
stdout.write(
|
|
281
|
+
`MATERIAL_DRAW_EVIDENCE=${JSON.stringify({
|
|
282
|
+
status: 'pass',
|
|
283
|
+
renderer: 'forgeax-runtime',
|
|
284
|
+
backend: 'dawn-webgpu',
|
|
285
|
+
frames: 300,
|
|
286
|
+
rootGuid: root.guid,
|
|
287
|
+
rootArtifactDigest: readiness.artifactDigest,
|
|
288
|
+
materialIdentity: { ...readiness.record.receipt.identity, materialGuid: root.guid },
|
|
289
|
+
pixel,
|
|
290
|
+
background,
|
|
291
|
+
removed,
|
|
292
|
+
observed: { enumerate: true, load: true, bind: true, draw: true, readback: true },
|
|
293
|
+
})}\n`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
renderer.dispose();
|
|
298
|
+
target?.destroy();
|
|
299
|
+
}
|
|
300
|
+
}, 60000);
|
|
105
301
|
});
|
|
@@ -401,6 +401,14 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
401
401
|
it('exhaustive switch covers all members without a default branch', () => {
|
|
402
402
|
function exhaustive(code: RuntimeLayerErrorCode): string {
|
|
403
403
|
switch (code) {
|
|
404
|
+
case 'auto-exposure-invalid-parameter':
|
|
405
|
+
return 'auto exposure invalid parameter';
|
|
406
|
+
case 'auto-exposure-capability-unavailable':
|
|
407
|
+
return 'auto exposure capability unavailable';
|
|
408
|
+
case 'auto-exposure-stale-generation':
|
|
409
|
+
return 'auto exposure stale generation';
|
|
410
|
+
case 'auto-exposure-stage-failed':
|
|
411
|
+
return 'auto exposure stage failed';
|
|
404
412
|
case 'lifecycle-construction-failed':
|
|
405
413
|
return 'lifecycle construction failed';
|
|
406
414
|
case 'world-lease-invalid':
|
|
@@ -586,6 +594,11 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
586
594
|
it('exhaustive switch covers all 3 new members alongside existing members', () => {
|
|
587
595
|
function exhaustive(code: RuntimeLayerErrorCode): string {
|
|
588
596
|
switch (code) {
|
|
597
|
+
case 'auto-exposure-invalid-parameter':
|
|
598
|
+
case 'auto-exposure-capability-unavailable':
|
|
599
|
+
case 'auto-exposure-stale-generation':
|
|
600
|
+
case 'auto-exposure-stage-failed':
|
|
601
|
+
return 'ok';
|
|
589
602
|
case 'lifecycle-construction-failed':
|
|
590
603
|
return 'ok';
|
|
591
604
|
case 'world-lease-invalid':
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { it } from 'vitest';
|
|
2
|
+
import { particleDepthLayouts, verifyFeatureDepthMaterial } from './feature-depth-material.fixture';
|
|
3
|
+
|
|
4
|
+
it('samples depth written by a vertex-only prepared graphics pass', async () => {
|
|
5
|
+
await verifyFeatureDepthMaterial({
|
|
6
|
+
...particleDepthLayouts[0],
|
|
7
|
+
view: false,
|
|
8
|
+
missing: 'none',
|
|
9
|
+
writeDepth: true,
|
|
10
|
+
});
|
|
11
|
+
}, 120_000);
|
|
12
|
+
|
|
13
|
+
it.each(
|
|
14
|
+
particleDepthLayouts.flatMap((layout) =>
|
|
15
|
+
[false, true].flatMap((view) =>
|
|
16
|
+
(['none', 'material', 'depth'] as const).map((missing) => ({ ...layout, view, missing })),
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
)(
|
|
20
|
+
'combines depth/material/particle input: $name view=$view missing=$missing',
|
|
21
|
+
verifyFeatureDepthMaterial,
|
|
22
|
+
120_000,
|
|
23
|
+
);
|