@forgeax/engine-render 0.1.6 → 0.1.7
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 +139 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +7 -0
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/authoring.d.ts +1 -0
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-QC4VW3XJ.mjs → chunk-HCOGPHKS.mjs} +17 -3
- package/dist/chunk-HCOGPHKS.mjs.map +1 -0
- package/dist/chunk-YEJIUW7D.mjs +1152 -0
- package/dist/chunk-YEJIUW7D.mjs.map +1 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lines.d.ts +5 -0
- package/dist/components/lines.d.ts.map +1 -0
- package/dist/components/points.d.ts +15 -0
- package/dist/components/points.d.ts.map +1 -0
- package/dist/construct-renderer.mjs +952 -497
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/errors/render.d.ts +82 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +5 -1
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +3 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +2 -0
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
- package/dist/points-lines/admission.d.ts +36 -0
- package/dist/points-lines/admission.d.ts.map +1 -0
- package/dist/points-lines/bounds.d.ts +8 -0
- package/dist/points-lines/bounds.d.ts.map +1 -0
- package/dist/points-lines/expansion-cache.d.ts +28 -0
- package/dist/points-lines/expansion-cache.d.ts.map +1 -0
- package/dist/points-lines/inspection.d.ts +53 -0
- package/dist/points-lines/inspection.d.ts.map +1 -0
- package/dist/points-lines/prepare.d.ts +67 -0
- package/dist/points-lines/prepare.d.ts.map +1 -0
- package/dist/points-lines/record.d.ts +60 -0
- package/dist/points-lines/record.d.ts.map +1 -0
- package/dist/points-lines/snapshot.d.ts +49 -0
- package/dist/points-lines/snapshot.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame.d.ts +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +7 -0
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +17 -0
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +6 -0
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/render-contract.d.ts +10 -2
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +3 -0
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +3 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +7 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/package.json +19 -19
- package/src/__tests__/authoring-path.unit.test.ts +9 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +12 -2
- package/src/__tests__/material-cooked-projection.unit.test.ts +29 -7
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +28 -28
- package/src/__tests__/render-error-code-owner.test-d.ts +6 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +19 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +38 -0
- package/src/assembly/factory.ts +72 -3
- package/src/assembly/material/assembly.ts +2 -5
- package/src/authoring.ts +1 -0
- package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
- package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
- package/src/components/index.ts +7 -0
- package/src/components/lines.ts +18 -0
- package/src/components/points.ts +40 -0
- package/src/errors/render.ts +144 -1
- package/src/features/render-graph-raster.ts +22 -3
- package/src/gpu-driven/production-raster.ts +6 -1
- package/src/index.ts +17 -0
- package/src/inspection-types.ts +4 -0
- package/src/pbr-pipeline.ts +8 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
- package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
- package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
- package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
- package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
- package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
- package/src/points-lines/admission.ts +238 -0
- package/src/points-lines/bounds.ts +23 -0
- package/src/points-lines/expansion-cache.ts +215 -0
- package/src/points-lines/inspection.ts +97 -0
- package/src/points-lines/prepare.ts +177 -0
- package/src/points-lines/record.ts +125 -0
- package/src/points-lines/snapshot.ts +130 -0
- package/src/record/frame-lighting.ts +14 -2
- package/src/record/frame.ts +12 -1
- package/src/record/main-pass-geometry.ts +92 -0
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/main-pass.ts +6 -3
- package/src/record/render-context.ts +20 -0
- package/src/record/shadow-pass.ts +16 -5
- package/src/record/skybox-post-pass.ts +2 -1
- package/src/record/typed-frame-graph.ts +17 -16
- package/src/record/view-ubo.ts +47 -0
- package/src/render-contract.ts +13 -2
- package/src/render-system-extract.ts +71 -8
- package/src/render-system.ts +304 -2
- package/src/scene/render-scene-types.ts +3 -0
- package/src/scene/render-scene.ts +57 -2
- package/dist/chunk-QC4VW3XJ.mjs.map +0 -1
- package/dist/chunk-SCRSAK5B.mjs +0 -480
- package/dist/chunk-SCRSAK5B.mjs.map +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { MeshAsset } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { PointsLinesExpansionCache } from '../expansion-cache';
|
|
4
|
+
import {
|
|
5
|
+
createPointsLinesLaneAdapter,
|
|
6
|
+
createPointsLinesLaneContract,
|
|
7
|
+
createPointsLinesRecordPlan,
|
|
8
|
+
} from '../record';
|
|
9
|
+
import { createPointsLinesSnapshot } from '../snapshot';
|
|
10
|
+
|
|
11
|
+
function mesh(topology: 'point-list' | 'line-list', count: number): MeshAsset {
|
|
12
|
+
const vertices = new Float32Array(
|
|
13
|
+
Array.from({ length: count }, (_, index) => [index, index % 2, 0]).flat(),
|
|
14
|
+
);
|
|
15
|
+
return {
|
|
16
|
+
kind: 'mesh',
|
|
17
|
+
vertices,
|
|
18
|
+
indices: new Uint16Array(Array.from({ length: count }, (_, index) => index)),
|
|
19
|
+
attributes: { position: vertices },
|
|
20
|
+
aabb: new Float32Array([0, 0, 0, count, 1, 0]),
|
|
21
|
+
submeshes: [
|
|
22
|
+
{ indexOffset: 0, indexCount: count, vertexCount: count, topology, materialSlot: 0 },
|
|
23
|
+
],
|
|
24
|
+
materialSlots: [{ slotName: 'points-lines' }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function snapshot(component: 'Points' | 'Lines' | undefined) {
|
|
29
|
+
return createPointsLinesSnapshot({
|
|
30
|
+
worldId: 1,
|
|
31
|
+
entityKey: 7,
|
|
32
|
+
component,
|
|
33
|
+
meshHandle: 11,
|
|
34
|
+
meshGeneration: 3,
|
|
35
|
+
materialHandle: 13,
|
|
36
|
+
materialGeneration: 2,
|
|
37
|
+
style:
|
|
38
|
+
component === 'Points'
|
|
39
|
+
? { kind: 'points', sizePx: 16, shape: 'circle' }
|
|
40
|
+
: component === 'Lines'
|
|
41
|
+
? { kind: 'lines', widthPx: 4 }
|
|
42
|
+
: undefined,
|
|
43
|
+
layer: 0,
|
|
44
|
+
visible: true,
|
|
45
|
+
sourceBounds: new Float32Array([0, 0, 0, 4, 1, 0]),
|
|
46
|
+
viewport: { width: 640, height: 480, dpr: 2 },
|
|
47
|
+
projection: new Float32Array(16),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Points/Lines structural lane contract', () => {
|
|
52
|
+
it('marks RhiNull structural-only and preserves one shared lane contract', () => {
|
|
53
|
+
const adapter = createPointsLinesLaneAdapter('direct', 'null');
|
|
54
|
+
const geometry = new PointsLinesExpansionCache().getOrCreate(
|
|
55
|
+
snapshot('Points'),
|
|
56
|
+
mesh('point-list', 4),
|
|
57
|
+
);
|
|
58
|
+
const plan = adapter.createRecordPlan(snapshot('Points'), geometry);
|
|
59
|
+
|
|
60
|
+
expect(adapter.contract.evidence).toBe('structural-only');
|
|
61
|
+
expect(adapter.contract.authoring).toBe('shared');
|
|
62
|
+
expect(plan.drawCount).toBe(1);
|
|
63
|
+
expect(plan.shadowDrawCount).toBe(0);
|
|
64
|
+
expect(plan.graph).toEqual({ additionalAttachments: 0, additionalPasses: 0 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps the component-off path exact-zero', () => {
|
|
68
|
+
const cache = new PointsLinesExpansionCache();
|
|
69
|
+
const offPath = snapshot(undefined);
|
|
70
|
+
const geometry = cache.getOrCreate(offPath, mesh('point-list', 4));
|
|
71
|
+
const plan = createPointsLinesRecordPlan(
|
|
72
|
+
offPath,
|
|
73
|
+
geometry,
|
|
74
|
+
createPointsLinesLaneContract('direct', 'webgpu'),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(offPath.dedicatedResourceBytes).toBe(0);
|
|
78
|
+
expect(geometry.sourceBytes).toBe(0);
|
|
79
|
+
expect(geometry.derivedBytes).toBe(0);
|
|
80
|
+
expect(plan.vertexCount).toBe(0);
|
|
81
|
+
expect(plan.indexCount).toBe(0);
|
|
82
|
+
expect(plan.drawCount).toBe(0);
|
|
83
|
+
expect(plan.graph).toEqual({ additionalAttachments: 0, additionalPasses: 0 });
|
|
84
|
+
expect(plan.shadowDrawCount).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('reuses one expansion for 100 entities and stays within triangle bounds', () => {
|
|
88
|
+
const cache = new PointsLinesExpansionCache();
|
|
89
|
+
const authoring = snapshot('Points');
|
|
90
|
+
const source = mesh('point-list', 4);
|
|
91
|
+
const first = cache.getOrCreate(authoring, source);
|
|
92
|
+
|
|
93
|
+
for (let entity = 0; entity < 100; entity += 1) {
|
|
94
|
+
expect(cache.getOrCreate(authoring, source)).toBe(first);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const plan = createPointsLinesRecordPlan(
|
|
98
|
+
authoring,
|
|
99
|
+
first,
|
|
100
|
+
createPointsLinesLaneContract('clustered', 'webgpu'),
|
|
101
|
+
);
|
|
102
|
+
const primitiveCount = first.pointCount + first.segmentCount;
|
|
103
|
+
expect(cache.size).toBe(1);
|
|
104
|
+
expect(plan.vertexCount).toBeLessThanOrEqual(4 * primitiveCount);
|
|
105
|
+
expect(plan.indexCount).toBeLessThanOrEqual(6 * primitiveCount);
|
|
106
|
+
expect(plan.drawCount).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('records WebGL2 capability provenance without compute storage or indirect', () => {
|
|
110
|
+
const contract = createPointsLinesLaneContract('cpu-webgl2', 'wgpu-webgl2');
|
|
111
|
+
|
|
112
|
+
expect(contract.capabilities).toEqual({ compute: false, storage: false, indirect: false });
|
|
113
|
+
expect(contract.material).toMatchObject({
|
|
114
|
+
source: 'MaterialAsset',
|
|
115
|
+
shadingModel: 'unlit',
|
|
116
|
+
clusteredLighting: false,
|
|
117
|
+
lit: false,
|
|
118
|
+
});
|
|
119
|
+
expect(contract.graph.additionalAttachments).toBe(0);
|
|
120
|
+
expect(contract.graph.additionalPasses).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { buildMeshAttributeMapForUvSets } from '@forgeax/engine-geometry';
|
|
3
|
+
import { buildProfileModel, createProfiler } from '@forgeax/engine-profiler';
|
|
4
|
+
import {
|
|
5
|
+
Camera,
|
|
6
|
+
Lines,
|
|
7
|
+
Materials,
|
|
8
|
+
MeshFilter,
|
|
9
|
+
MeshRenderer,
|
|
10
|
+
PointShapeValue,
|
|
11
|
+
Points,
|
|
12
|
+
perspective,
|
|
13
|
+
} from '@forgeax/engine-render';
|
|
14
|
+
import type { RhiRenderPassEncoder } from '@forgeax/engine-rhi';
|
|
15
|
+
import { rhi } from '@forgeax/engine-rhi-null';
|
|
16
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
17
|
+
import { err, type MeshAsset, ok } from '@forgeax/engine-types';
|
|
18
|
+
import { bench, describe } from 'vitest';
|
|
19
|
+
import { createRenderer as constructRenderer } from '../../assembly/factory';
|
|
20
|
+
import { recordPointsLinesDraw } from '../../record/main-pass-geometry';
|
|
21
|
+
import { admitPointsLines } from '../admission';
|
|
22
|
+
import { PointsLinesExpansionCache } from '../expansion-cache';
|
|
23
|
+
import { type PointsLinesGpuResourceAdapter, PointsLinesPreparation } from '../prepare';
|
|
24
|
+
import { createPointsLinesLaneContract, createPointsLinesRecordPlan } from '../record';
|
|
25
|
+
import { createPointsLinesSnapshot } from '../snapshot';
|
|
26
|
+
|
|
27
|
+
const POINT_COUNT = 10_000;
|
|
28
|
+
const SEGMENT_COUNT = 10_000;
|
|
29
|
+
const SAMPLE_COUNT = 30;
|
|
30
|
+
const BENCH_OPTIONS = { iterations: SAMPLE_COUNT } as const;
|
|
31
|
+
|
|
32
|
+
function buildMesh(topology: 'point-list' | 'line-list'): MeshAsset {
|
|
33
|
+
const vertexCount = topology === 'point-list' ? POINT_COUNT : SEGMENT_COUNT * 2;
|
|
34
|
+
const vertices = new Float32Array(vertexCount * 3);
|
|
35
|
+
const attributes = new Float32Array(vertices);
|
|
36
|
+
for (let index = 0; index < vertexCount; index += 1) {
|
|
37
|
+
const offset = index * 3;
|
|
38
|
+
vertices[offset] = (index % 100) / 50 - 1;
|
|
39
|
+
vertices[offset + 1] = Math.floor(index / 100) / 100 - 1;
|
|
40
|
+
vertices[offset + 2] = 0;
|
|
41
|
+
}
|
|
42
|
+
const indices = topology === 'line-list' ? new Uint32Array(vertexCount) : undefined;
|
|
43
|
+
if (indices !== undefined) {
|
|
44
|
+
for (let index = 0; index < indices.length; index += 1) indices[index] = index;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
kind: 'mesh',
|
|
48
|
+
vertices,
|
|
49
|
+
...(indices === undefined ? {} : { indices }),
|
|
50
|
+
attributes: { ...buildMeshAttributeMapForUvSets(1), position: attributes },
|
|
51
|
+
aabb: new Float32Array([-1, -1, 0, 1, 1, 0]),
|
|
52
|
+
submeshes: [
|
|
53
|
+
{
|
|
54
|
+
indexOffset: 0,
|
|
55
|
+
indexCount: topology === 'line-list' ? vertexCount : 0,
|
|
56
|
+
vertexCount,
|
|
57
|
+
topology,
|
|
58
|
+
materialSlot: 0,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
materialSlots: [{ slotName: 'points-lines-bench' }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function snapshot(component: 'Points' | 'Lines') {
|
|
66
|
+
return createPointsLinesSnapshot({
|
|
67
|
+
worldId: 0,
|
|
68
|
+
entityKey: component === 'Points' ? 10 : 11,
|
|
69
|
+
component,
|
|
70
|
+
meshHandle: 99,
|
|
71
|
+
meshGeneration: 1,
|
|
72
|
+
materialHandle: 100,
|
|
73
|
+
materialGeneration: 1,
|
|
74
|
+
style:
|
|
75
|
+
component === 'Points'
|
|
76
|
+
? { kind: 'points', sizePx: 8, shape: 'circle' }
|
|
77
|
+
: { kind: 'lines', widthPx: 3 },
|
|
78
|
+
layer: 0,
|
|
79
|
+
visible: true,
|
|
80
|
+
sourceBounds: [-1, -1, 0, 1, 1, 0],
|
|
81
|
+
viewport: { width: 1280, height: 720, dpr: 1 },
|
|
82
|
+
projection: new Float32Array(16),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const MATERIAL = Materials.unlit([0.1, 0.9, 1, 1], { castShadow: false });
|
|
87
|
+
|
|
88
|
+
function manifestUrl(): string {
|
|
89
|
+
return `data:application/json,${encodeURIComponent(JSON.stringify({ schemaVersion: '1.0.0', entries: [] }))}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildDirectMesh(): MeshAsset {
|
|
93
|
+
const vertices = new Float32Array(POINT_COUNT * 3);
|
|
94
|
+
for (let index = 0; index < POINT_COUNT; index += 1) {
|
|
95
|
+
const offset = index * 3;
|
|
96
|
+
vertices[offset] = (index % 100) / 50 - 1;
|
|
97
|
+
vertices[offset + 1] = Math.floor(index / 100) / 100 - 1;
|
|
98
|
+
}
|
|
99
|
+
const indices = new Uint32Array(Math.floor(POINT_COUNT / 3) * 3);
|
|
100
|
+
for (let index = 0; index < indices.length; index += 1) indices[index] = index;
|
|
101
|
+
return {
|
|
102
|
+
kind: 'mesh',
|
|
103
|
+
vertices,
|
|
104
|
+
indices,
|
|
105
|
+
attributes: { ...buildMeshAttributeMapForUvSets(1), position: new Float32Array(vertices) },
|
|
106
|
+
aabb: new Float32Array([-1, -1, 0, 1, 1, 0]),
|
|
107
|
+
submeshes: [
|
|
108
|
+
{
|
|
109
|
+
indexOffset: 0,
|
|
110
|
+
indexCount: indices.length,
|
|
111
|
+
vertexCount: POINT_COUNT,
|
|
112
|
+
topology: 'triangle-list',
|
|
113
|
+
materialSlot: 0,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
materialSlots: [{ slotName: 'points-lines-direct-baseline' }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type RealRoute = {
|
|
121
|
+
readonly renderer: Awaited<ReturnType<typeof constructRenderer>>;
|
|
122
|
+
readonly profiler: ReturnType<typeof createProfiler>;
|
|
123
|
+
readonly pointsLines: {
|
|
124
|
+
readonly world: World;
|
|
125
|
+
readonly lease: import('../../render-contract').RenderWorldLease;
|
|
126
|
+
};
|
|
127
|
+
readonly direct: {
|
|
128
|
+
readonly world: World;
|
|
129
|
+
readonly lease: import('../../render-contract').RenderWorldLease;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function makeRealScene(
|
|
134
|
+
renderer: RealRoute['renderer'],
|
|
135
|
+
meshes: { readonly points: MeshAsset; readonly lines: MeshAsset },
|
|
136
|
+
component: 'points-lines' | 'direct',
|
|
137
|
+
): RealRoute['pointsLines'] {
|
|
138
|
+
const world = new World();
|
|
139
|
+
const materialHandle = world.allocSharedRef('MaterialAsset', MATERIAL);
|
|
140
|
+
if (component === 'points-lines') {
|
|
141
|
+
const pointMeshHandle = world.allocSharedRef('MeshAsset', meshes.points);
|
|
142
|
+
const lineMeshHandle = world.allocSharedRef('MeshAsset', meshes.lines);
|
|
143
|
+
world
|
|
144
|
+
.spawn(
|
|
145
|
+
{
|
|
146
|
+
component: Transform,
|
|
147
|
+
data: { pos: [-0.5, 0, -2], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
148
|
+
},
|
|
149
|
+
{ component: MeshFilter, data: { assetHandle: pointMeshHandle } },
|
|
150
|
+
{ component: MeshRenderer, data: { materials: [materialHandle] } },
|
|
151
|
+
{ component: Points, data: { sizePx: 8, shape: PointShapeValue.circle } },
|
|
152
|
+
)
|
|
153
|
+
.unwrap();
|
|
154
|
+
world
|
|
155
|
+
.spawn(
|
|
156
|
+
{ component: Transform, data: { pos: [0.5, 0, -2], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
|
|
157
|
+
{ component: MeshFilter, data: { assetHandle: lineMeshHandle } },
|
|
158
|
+
{ component: MeshRenderer, data: { materials: [materialHandle] } },
|
|
159
|
+
{ component: Lines, data: { widthPx: 3 } },
|
|
160
|
+
)
|
|
161
|
+
.unwrap();
|
|
162
|
+
} else {
|
|
163
|
+
const meshHandle = world.allocSharedRef('MeshAsset', meshes.points);
|
|
164
|
+
for (const x of [-0.5, 0.5]) {
|
|
165
|
+
world
|
|
166
|
+
.spawn(
|
|
167
|
+
{ component: Transform, data: { pos: [x, 0, -2], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
|
|
168
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
169
|
+
{ component: MeshRenderer, data: { materials: [materialHandle] } },
|
|
170
|
+
)
|
|
171
|
+
.unwrap();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
world
|
|
175
|
+
.spawn(
|
|
176
|
+
{ component: Transform, data: { pos: [0, 0, 3], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
|
|
177
|
+
{ component: Camera, data: { ...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }) } },
|
|
178
|
+
)
|
|
179
|
+
.unwrap();
|
|
180
|
+
const attached = renderer.attach(world);
|
|
181
|
+
if (!attached.ok) throw attached.error;
|
|
182
|
+
world.update().unwrap();
|
|
183
|
+
return { world, lease: attached.value };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function createRealRoute(): Promise<RealRoute> {
|
|
187
|
+
const profiler = createProfiler();
|
|
188
|
+
const renderer = await constructRenderer(
|
|
189
|
+
{ getContext: () => null },
|
|
190
|
+
{ rhi, profiler },
|
|
191
|
+
{ shaderManifestUrl: manifestUrl() },
|
|
192
|
+
);
|
|
193
|
+
return {
|
|
194
|
+
renderer,
|
|
195
|
+
profiler,
|
|
196
|
+
pointsLines: makeRealScene(renderer, { points: POINT_MESH, lines: LINE_MESH }, 'points-lines'),
|
|
197
|
+
direct: makeRealScene(
|
|
198
|
+
renderer,
|
|
199
|
+
{ points: buildDirectMesh(), lines: buildDirectMesh() },
|
|
200
|
+
'direct',
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function drawRealFrame(route: RealRoute, scene: RealRoute['pointsLines']): void {
|
|
206
|
+
const drawn = route.renderer.draw({
|
|
207
|
+
leases: [scene.lease],
|
|
208
|
+
camera: { lease: scene.lease },
|
|
209
|
+
environment: { lease: scene.lease },
|
|
210
|
+
});
|
|
211
|
+
if (!drawn.ok) throw drawn.error;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function captureProfile(route: RealRoute, scene: RealRoute['pointsLines']) {
|
|
215
|
+
const capture = route.profiler.startCapture({
|
|
216
|
+
frameLimit: 1,
|
|
217
|
+
eventLimit: 10_000,
|
|
218
|
+
detail: 'nested',
|
|
219
|
+
});
|
|
220
|
+
if (!capture.ok) throw capture.error;
|
|
221
|
+
drawRealFrame(route, scene);
|
|
222
|
+
const finished = capture.value.finish();
|
|
223
|
+
if (!finished.ok) throw finished.error;
|
|
224
|
+
const model = buildProfileModel(finished.value);
|
|
225
|
+
if (!model.ok) throw model.error;
|
|
226
|
+
return model.value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type BenchResource = { readonly geometryKey: string; readonly bytes: number };
|
|
230
|
+
|
|
231
|
+
const RESOURCE_ADAPTER: PointsLinesGpuResourceAdapter<BenchResource> = {
|
|
232
|
+
create: (geometry) => ok({ geometryKey: geometry.key, bytes: 0 }),
|
|
233
|
+
upload: (_resource, geometry) => ok(geometry.derivedBytes),
|
|
234
|
+
validate: (_resource, geometry) =>
|
|
235
|
+
geometry.expandedVertexCount > 0 && geometry.expandedIndexCount > 0
|
|
236
|
+
? ok(undefined)
|
|
237
|
+
: err(new Error('benchmark expansion is not drawable')),
|
|
238
|
+
destroy: () => undefined,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
function prepareWorkload(
|
|
242
|
+
component: 'Points' | 'Lines',
|
|
243
|
+
mesh: MeshAsset,
|
|
244
|
+
cache: PointsLinesExpansionCache,
|
|
245
|
+
preparation = new PointsLinesPreparation({ cache, adapter: RESOURCE_ADAPTER }),
|
|
246
|
+
) {
|
|
247
|
+
const authored =
|
|
248
|
+
component === 'Points' ? { points: { sizePx: 8, shape: 1 } } : { lines: { widthPx: 3 } };
|
|
249
|
+
const admission = admitPointsLines({
|
|
250
|
+
entity: component === 'Points' ? 10 : 11,
|
|
251
|
+
mesh,
|
|
252
|
+
material: MATERIAL,
|
|
253
|
+
...authored,
|
|
254
|
+
});
|
|
255
|
+
if (!admission.ok) throw admission.error;
|
|
256
|
+
const retained = snapshot(component);
|
|
257
|
+
const prepared = preparation.prepare(retained, mesh);
|
|
258
|
+
if (!prepared.ok) throw prepared.error;
|
|
259
|
+
const plan = createPointsLinesRecordPlan(
|
|
260
|
+
retained,
|
|
261
|
+
prepared.value.geometry,
|
|
262
|
+
createPointsLinesLaneContract('direct', 'webgpu'),
|
|
263
|
+
);
|
|
264
|
+
const draws: string[] = [];
|
|
265
|
+
const pass = {
|
|
266
|
+
draw: () => draws.push('draw'),
|
|
267
|
+
drawIndexed: () => draws.push('drawIndexed'),
|
|
268
|
+
} as unknown as RhiRenderPassEncoder;
|
|
269
|
+
recordPointsLinesDraw(pass, plan);
|
|
270
|
+
if (draws.length !== 1) throw new Error(`${component} record path emitted ${draws.length} draws`);
|
|
271
|
+
return { geometry: prepared.value.geometry, plan, preparation, draws };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function preparePair(
|
|
275
|
+
cache: PointsLinesExpansionCache,
|
|
276
|
+
pointMesh: MeshAsset,
|
|
277
|
+
lineMesh: MeshAsset,
|
|
278
|
+
preparations?: {
|
|
279
|
+
points?: PointsLinesPreparation<BenchResource>;
|
|
280
|
+
lines?: PointsLinesPreparation<BenchResource>;
|
|
281
|
+
},
|
|
282
|
+
) {
|
|
283
|
+
const points = prepareWorkload('Points', pointMesh, cache, preparations?.points);
|
|
284
|
+
const lines = prepareWorkload('Lines', lineMesh, cache, preparations?.lines);
|
|
285
|
+
return { points, lines };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateWorkload(pointMesh: MeshAsset, lineMesh: MeshAsset): void {
|
|
289
|
+
const cache = new PointsLinesExpansionCache();
|
|
290
|
+
const pair = preparePair(cache, pointMesh, lineMesh);
|
|
291
|
+
const primitiveCount = POINT_COUNT + SEGMENT_COUNT;
|
|
292
|
+
if (pair.points.geometry === pair.lines.geometry) {
|
|
293
|
+
throw new Error('Points and Lines must not share an expansion cache entry');
|
|
294
|
+
}
|
|
295
|
+
if (pair.points.geometry.expandedVertexCount !== POINT_COUNT * 4) {
|
|
296
|
+
throw new Error('Points expansion vertex count mismatch');
|
|
297
|
+
}
|
|
298
|
+
if (pair.lines.geometry.expandedIndexCount !== SEGMENT_COUNT * 6) {
|
|
299
|
+
throw new Error('Lines expansion index count mismatch');
|
|
300
|
+
}
|
|
301
|
+
if (pair.points.plan.drawCount !== 1 || pair.lines.plan.drawCount !== 1) {
|
|
302
|
+
throw new Error('Visible Points and Lines must each record one draw');
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
pair.points.geometry.expandedVertexCount + pair.lines.geometry.expandedVertexCount >
|
|
306
|
+
primitiveCount * 4
|
|
307
|
+
) {
|
|
308
|
+
throw new Error('Expanded vertex bound exceeded');
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
pair.points.geometry.expandedIndexCount + pair.lines.geometry.expandedIndexCount >
|
|
312
|
+
primitiveCount * 6
|
|
313
|
+
) {
|
|
314
|
+
throw new Error('Expanded index bound exceeded');
|
|
315
|
+
}
|
|
316
|
+
const offPath = cache.getOrCreate(
|
|
317
|
+
{ ...snapshot('Points'), component: undefined, style: undefined },
|
|
318
|
+
pointMesh,
|
|
319
|
+
);
|
|
320
|
+
if (offPath.derivedBytes !== 0 || offPath.expandedVertexCount !== 0) {
|
|
321
|
+
throw new Error('Off-path geometry must remain exact-zero');
|
|
322
|
+
}
|
|
323
|
+
if (offPath.derivedBytes !== 0) throw new Error('off-path derived bytes changed');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const POINT_MESH = buildMesh('point-list');
|
|
327
|
+
const LINE_MESH = buildMesh('line-list');
|
|
328
|
+
validateWorkload(POINT_MESH, LINE_MESH);
|
|
329
|
+
const REAL_ROUTE = await createRealRoute();
|
|
330
|
+
const REAL_PROFILE = await captureProfile(REAL_ROUTE, REAL_ROUTE.pointsLines);
|
|
331
|
+
const DIRECT_PROFILE = await captureProfile(REAL_ROUTE, REAL_ROUTE.direct);
|
|
332
|
+
|
|
333
|
+
function phaseDeltas(): Readonly<Record<string, number>> {
|
|
334
|
+
const direct = new Map(
|
|
335
|
+
DIRECT_PROFILE?.phases.map((phase) => [
|
|
336
|
+
`${phase.source}:${phase.phase}`,
|
|
337
|
+
phase.p95DurationMicros ?? 0,
|
|
338
|
+
]) ?? [],
|
|
339
|
+
);
|
|
340
|
+
return Object.fromEntries(
|
|
341
|
+
REAL_PROFILE?.phases.map((phase) => {
|
|
342
|
+
const key = `${phase.source}:${phase.phase}`;
|
|
343
|
+
return [key, (phase.p95DurationMicros ?? 0) - (direct.get(key) ?? 0)];
|
|
344
|
+
}) ?? [],
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// biome-ignore lint/suspicious/noConsole: benchmark emits machine-readable evidence
|
|
349
|
+
console.info(
|
|
350
|
+
JSON.stringify({
|
|
351
|
+
workload: { points: POINT_COUNT, segments: SEGMENT_COUNT, samples: SAMPLE_COUNT },
|
|
352
|
+
route: 'Renderer.extract-prepare-record',
|
|
353
|
+
baseline: 'same-Renderer.direct-mesh',
|
|
354
|
+
phases: phaseDeltas(),
|
|
355
|
+
gpuTiming: {
|
|
356
|
+
status: 'blocked',
|
|
357
|
+
reason: 'no adapter-qualified timestamp owner exists for this RhiNull runner',
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
let sink = 0;
|
|
362
|
+
|
|
363
|
+
describe('Points/Lines 10k + 10k benchmark', () => {
|
|
364
|
+
bench(
|
|
365
|
+
'same-Renderer direct baseline',
|
|
366
|
+
() => {
|
|
367
|
+
drawRealFrame(REAL_ROUTE, REAL_ROUTE.direct);
|
|
368
|
+
sink ^= REAL_ROUTE.renderer.inspect().frame.frameId;
|
|
369
|
+
},
|
|
370
|
+
BENCH_OPTIONS,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
bench(
|
|
374
|
+
'Renderer extract prepare record',
|
|
375
|
+
() => {
|
|
376
|
+
drawRealFrame(REAL_ROUTE, REAL_ROUTE.pointsLines);
|
|
377
|
+
sink ^= REAL_ROUTE.renderer.inspect().renderScene.pointsLines?.length ?? 0;
|
|
378
|
+
},
|
|
379
|
+
BENCH_OPTIONS,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
bench(
|
|
383
|
+
'Renderer warm extract prepare record',
|
|
384
|
+
() => {
|
|
385
|
+
drawRealFrame(REAL_ROUTE, REAL_ROUTE.pointsLines);
|
|
386
|
+
sink ^= REAL_ROUTE.renderer.inspect().renderScene.pointsLines?.length ?? 0;
|
|
387
|
+
},
|
|
388
|
+
BENCH_OPTIONS,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
bench(
|
|
392
|
+
'Renderer inspection after record',
|
|
393
|
+
() => {
|
|
394
|
+
sink ^= REAL_ROUTE.renderer.inspect().renderScene.pointsLines?.length ?? 0;
|
|
395
|
+
},
|
|
396
|
+
BENCH_OPTIONS,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
void sink;
|
|
400
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createPointsLinesLaneContract, type PointsLinesLane } from '../record';
|
|
3
|
+
|
|
4
|
+
const lanes: readonly PointsLinesLane[] = ['direct', 'clustered', 'cpu-webgl2'];
|
|
5
|
+
|
|
6
|
+
describe('Points/Lines raster lane integration contract', () => {
|
|
7
|
+
it.each(lanes)('%s consumes one unlit material and the existing graph', (lane) => {
|
|
8
|
+
const contract = createPointsLinesLaneContract(lane, 'webgpu');
|
|
9
|
+
|
|
10
|
+
expect(contract.lane).toBe(lane);
|
|
11
|
+
expect(contract.material.shadingModel).toBe('unlit');
|
|
12
|
+
expect(contract.material.source).toBe('MaterialAsset');
|
|
13
|
+
expect(contract.graph.additionalAttachments).toBe(0);
|
|
14
|
+
expect(contract.graph.additionalPasses).toBe(0);
|
|
15
|
+
expect(contract.shadowDrawCount).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('keeps clustered points and lines on the unlit route', () => {
|
|
19
|
+
const contract = createPointsLinesLaneContract('clustered', 'webgpu');
|
|
20
|
+
|
|
21
|
+
expect(contract.material.shadingModel).toBe('unlit');
|
|
22
|
+
expect(contract.material.clusteredLighting).toBe(false);
|
|
23
|
+
expect(contract.material.lit).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does not request compute, storage, or indirect features on CPU-WebGL2', () => {
|
|
27
|
+
const contract = createPointsLinesLaneContract('cpu-webgl2', 'wgpu-webgl2');
|
|
28
|
+
|
|
29
|
+
expect(contract.capabilities.compute).toBe(false);
|
|
30
|
+
expect(contract.capabilities.storage).toBe(false);
|
|
31
|
+
expect(contract.capabilities.indirect).toBe(false);
|
|
32
|
+
expect(contract.graph.additionalAttachments).toBe(0);
|
|
33
|
+
expect(contract.graph.additionalPasses).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('labels RhiNull as structural-only and keeps the same authoring contract', () => {
|
|
37
|
+
const contract = createPointsLinesLaneContract('direct', 'null');
|
|
38
|
+
|
|
39
|
+
expect(contract.evidence).toBe('structural-only');
|
|
40
|
+
expect(contract.authoring).toBe('shared');
|
|
41
|
+
expect(contract.graph.additionalAttachments).toBe(0);
|
|
42
|
+
expect(contract.graph.additionalPasses).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
comparePointsLinesSnapshots,
|
|
4
|
+
createPointsLinesSnapshot,
|
|
5
|
+
type PointsLinesRetainedSnapshot,
|
|
6
|
+
} from '../snapshot';
|
|
7
|
+
|
|
8
|
+
function snapshotInput(): Parameters<typeof createPointsLinesSnapshot>[0] {
|
|
9
|
+
return {
|
|
10
|
+
worldId: 3,
|
|
11
|
+
entityKey: 17,
|
|
12
|
+
component: 'Points',
|
|
13
|
+
meshHandle: 11,
|
|
14
|
+
meshGeneration: 4,
|
|
15
|
+
materialHandle: 23,
|
|
16
|
+
materialGeneration: 8,
|
|
17
|
+
style: { kind: 'points', sizePx: 6, shape: 'circle' },
|
|
18
|
+
layer: 12,
|
|
19
|
+
sortKey: -2,
|
|
20
|
+
visible: true,
|
|
21
|
+
sourceBounds: new Float32Array([-1, -2, -3, 1, 2, 3]),
|
|
22
|
+
viewport: { width: 800, height: 600, dpr: 2 },
|
|
23
|
+
projection: new Float32Array(16),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('Points/Lines retained snapshot', () => {
|
|
28
|
+
it('detaches ECS identity, owner generations, style, and view facts', () => {
|
|
29
|
+
const source = snapshotInput();
|
|
30
|
+
const snapshot = createPointsLinesSnapshot(source);
|
|
31
|
+
|
|
32
|
+
expect(snapshot).toEqual<PointsLinesRetainedSnapshot>({
|
|
33
|
+
worldId: 3,
|
|
34
|
+
entityKey: 17,
|
|
35
|
+
component: 'Points',
|
|
36
|
+
meshHandle: 11,
|
|
37
|
+
meshGeneration: 4,
|
|
38
|
+
materialHandle: 23,
|
|
39
|
+
materialGeneration: 8,
|
|
40
|
+
style: { kind: 'points', sizePx: 6, shape: 'circle' },
|
|
41
|
+
layer: 12,
|
|
42
|
+
sortKey: -2,
|
|
43
|
+
visible: true,
|
|
44
|
+
sourceBounds: new Float32Array([-1, -2, -3, 1, 2, 3]),
|
|
45
|
+
viewport: { width: 800, height: 600, dpr: 2 },
|
|
46
|
+
projection: new Float32Array(16),
|
|
47
|
+
});
|
|
48
|
+
expect(snapshot).not.toHaveProperty('world');
|
|
49
|
+
expect(snapshot.sourceBounds).not.toBe(source.sourceBounds);
|
|
50
|
+
expect(snapshot.projection).not.toBe(source.projection);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('classifies only owner generations and style as geometry invalidation', () => {
|
|
54
|
+
const base = createPointsLinesSnapshot(snapshotInput());
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
comparePointsLinesSnapshots(
|
|
58
|
+
base,
|
|
59
|
+
createPointsLinesSnapshot({ ...snapshotInput(), meshGeneration: 5 }),
|
|
60
|
+
),
|
|
61
|
+
).toBe('mesh');
|
|
62
|
+
expect(
|
|
63
|
+
comparePointsLinesSnapshots(
|
|
64
|
+
base,
|
|
65
|
+
createPointsLinesSnapshot({ ...snapshotInput(), materialGeneration: 9 }),
|
|
66
|
+
),
|
|
67
|
+
).toBe('material');
|
|
68
|
+
expect(
|
|
69
|
+
comparePointsLinesSnapshots(
|
|
70
|
+
base,
|
|
71
|
+
createPointsLinesSnapshot({
|
|
72
|
+
...snapshotInput(),
|
|
73
|
+
style: { kind: 'points', sizePx: 8, shape: 'square' },
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
).toBe('style');
|
|
77
|
+
expect(
|
|
78
|
+
comparePointsLinesSnapshots(
|
|
79
|
+
base,
|
|
80
|
+
createPointsLinesSnapshot({
|
|
81
|
+
...snapshotInput(),
|
|
82
|
+
viewport: { width: 1600, height: 1200, dpr: 2 },
|
|
83
|
+
}),
|
|
84
|
+
),
|
|
85
|
+
).toBe('view');
|
|
86
|
+
expect(
|
|
87
|
+
comparePointsLinesSnapshots(base, createPointsLinesSnapshot({ ...snapshotInput() })),
|
|
88
|
+
).toBe('none');
|
|
89
|
+
expect(base).not.toHaveProperty('derived');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('records a dedicated-resource zero when the style component is absent', () => {
|
|
93
|
+
const snapshot = createPointsLinesSnapshot({
|
|
94
|
+
...snapshotInput(),
|
|
95
|
+
component: undefined,
|
|
96
|
+
style: undefined,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(snapshot).toMatchObject({ component: undefined, dedicatedResourceBytes: 0 });
|
|
100
|
+
});
|
|
101
|
+
});
|