@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,73 @@
|
|
|
1
|
+
import { vec3 } from '@forgeax/engine-math';
|
|
2
|
+
import type { Buffer, RhiQueue } from '@forgeax/engine-rhi';
|
|
3
|
+
import { ok } from '@forgeax/engine-types';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { POINTS_LINES_VIEW_SLOT_STRIDE, writePointsLinesViewUbo } from '../../record/view-ubo';
|
|
6
|
+
import type { CameraSnapshot } from '../../render-contract';
|
|
7
|
+
|
|
8
|
+
function camera(): CameraSnapshot {
|
|
9
|
+
return {
|
|
10
|
+
position: vec3.create(0, 0, 3),
|
|
11
|
+
world: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 3, 1]),
|
|
12
|
+
fov: Math.PI / 4,
|
|
13
|
+
aspect: 16 / 9,
|
|
14
|
+
near: 0.1,
|
|
15
|
+
far: 100,
|
|
16
|
+
projection: 'perspective',
|
|
17
|
+
orthoLeft: -1,
|
|
18
|
+
orthoRight: 1,
|
|
19
|
+
orthoBottom: -1,
|
|
20
|
+
orthoTop: 1,
|
|
21
|
+
tonemap: 'none',
|
|
22
|
+
exposure: 1,
|
|
23
|
+
whitePoint: 4,
|
|
24
|
+
antialias: 'none',
|
|
25
|
+
bloom: 'off',
|
|
26
|
+
bloomThreshold: 1,
|
|
27
|
+
bloomIntensity: 1,
|
|
28
|
+
bloomBlurRadius: 4,
|
|
29
|
+
clearColor: [0, 0, 0, 1],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Points/Lines per-draw view slots', () => {
|
|
34
|
+
it('writes distinct transforms and styles at distinct dynamic offsets', () => {
|
|
35
|
+
const writes: Array<{ offset: number; payload: Float32Array }> = [];
|
|
36
|
+
const queue = {
|
|
37
|
+
writeBuffer: (_buffer: Buffer, offset: number, payload: Float32Array) => {
|
|
38
|
+
writes.push({ offset, payload: new Float32Array(payload) });
|
|
39
|
+
return ok(undefined);
|
|
40
|
+
},
|
|
41
|
+
} as unknown as RhiQueue;
|
|
42
|
+
const buffer = {} as Buffer;
|
|
43
|
+
const firstModel = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 2, 0, 0, 1]);
|
|
44
|
+
const secondModel = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -2, 0, 0, 1]);
|
|
45
|
+
|
|
46
|
+
writePointsLinesViewUbo(
|
|
47
|
+
queue,
|
|
48
|
+
buffer,
|
|
49
|
+
camera(),
|
|
50
|
+
640,
|
|
51
|
+
360,
|
|
52
|
+
firstModel,
|
|
53
|
+
{ kind: 'points', sizePx: 16, shape: 'circle' },
|
|
54
|
+
0,
|
|
55
|
+
);
|
|
56
|
+
writePointsLinesViewUbo(
|
|
57
|
+
queue,
|
|
58
|
+
buffer,
|
|
59
|
+
camera(),
|
|
60
|
+
640,
|
|
61
|
+
360,
|
|
62
|
+
secondModel,
|
|
63
|
+
{ kind: 'lines', widthPx: 4 },
|
|
64
|
+
POINTS_LINES_VIEW_SLOT_STRIDE,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(writes.map(({ offset }) => offset)).toEqual([0, POINTS_LINES_VIEW_SLOT_STRIDE]);
|
|
68
|
+
expect(writes[0]?.payload[28]).toBe(2);
|
|
69
|
+
expect(writes[1]?.payload[28]).toBe(-2);
|
|
70
|
+
expect(writes[0]?.payload.slice(36, 40)).toEqual(new Float32Array([16, 0, 1, 0]));
|
|
71
|
+
expect(writes[1]?.payload.slice(36, 40)).toEqual(new Float32Array([4, 1, 0, 0]));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { err, type MaterialAsset, type MeshAsset, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import { type PointShape, PointShapeValue, pointShapeFromU32 } from '../components/points';
|
|
3
|
+
import type {
|
|
4
|
+
PointsLinesBudgetExceededError,
|
|
5
|
+
PointsLinesInvalidStyleError,
|
|
6
|
+
PointsLinesMaterialUnsupportedError,
|
|
7
|
+
PointsLinesStyleUnsupportedError,
|
|
8
|
+
PointsLinesTopologyMismatchError,
|
|
9
|
+
} from '../errors/render';
|
|
10
|
+
import {
|
|
11
|
+
PointsLinesBudgetExceededError as BudgetError,
|
|
12
|
+
PointsLinesInvalidStyleError as InvalidStyleError,
|
|
13
|
+
PointsLinesMaterialUnsupportedError as MaterialError,
|
|
14
|
+
PointsLinesStyleUnsupportedError as StyleError,
|
|
15
|
+
PointsLinesTopologyMismatchError as TopologyError,
|
|
16
|
+
} from '../errors/render';
|
|
17
|
+
|
|
18
|
+
export interface PointsStyleInput {
|
|
19
|
+
readonly sizePx?: number;
|
|
20
|
+
readonly shape?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LinesStyleInput {
|
|
24
|
+
readonly widthPx?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PointsLinesAdmissionLimits {
|
|
28
|
+
readonly maxPoints?: number;
|
|
29
|
+
readonly maxSegments?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PointsLinesAdmissionInput {
|
|
33
|
+
readonly entity: number;
|
|
34
|
+
readonly points?: PointsStyleInput;
|
|
35
|
+
readonly lines?: LinesStyleInput;
|
|
36
|
+
readonly mesh: MeshAsset;
|
|
37
|
+
readonly material: MaterialAsset;
|
|
38
|
+
readonly limits?: PointsLinesAdmissionLimits;
|
|
39
|
+
readonly materialId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PointsLinesAdmission {
|
|
43
|
+
readonly component: 'Points' | 'Lines';
|
|
44
|
+
readonly shape?: PointShape;
|
|
45
|
+
readonly sizePx?: number;
|
|
46
|
+
readonly widthPx?: number;
|
|
47
|
+
readonly pointCount: number;
|
|
48
|
+
readonly segmentCount: number;
|
|
49
|
+
readonly submeshes: readonly number[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PointsLinesAdmissionError =
|
|
53
|
+
| PointsLinesInvalidStyleError
|
|
54
|
+
| PointsLinesTopologyMismatchError
|
|
55
|
+
| PointsLinesStyleUnsupportedError
|
|
56
|
+
| PointsLinesMaterialUnsupportedError
|
|
57
|
+
| PointsLinesBudgetExceededError;
|
|
58
|
+
|
|
59
|
+
function invalidStyle(
|
|
60
|
+
input: PointsLinesAdmissionInput,
|
|
61
|
+
component: 'Points' | 'Lines' | 'Points/Lines',
|
|
62
|
+
field: string,
|
|
63
|
+
value: number | string | undefined,
|
|
64
|
+
expected: string,
|
|
65
|
+
): Result<never, PointsLinesAdmissionError> {
|
|
66
|
+
return err(new InvalidStyleError({ entity: input.entity, component, field, value, expected }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function topologyMismatch(
|
|
70
|
+
input: PointsLinesAdmissionInput,
|
|
71
|
+
submesh: number,
|
|
72
|
+
expected: string,
|
|
73
|
+
actual: string,
|
|
74
|
+
): Result<never, PointsLinesAdmissionError> {
|
|
75
|
+
return err(new TopologyError({ entity: input.entity, submesh, expected, actual }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkStyle(
|
|
79
|
+
input: PointsLinesAdmissionInput,
|
|
80
|
+
): Result<
|
|
81
|
+
{ component: 'Points' | 'Lines'; shape?: PointShape; sizePx?: number; widthPx?: number },
|
|
82
|
+
PointsLinesAdmissionError
|
|
83
|
+
> {
|
|
84
|
+
const hasPoints = input.points !== undefined;
|
|
85
|
+
const hasLines = input.lines !== undefined;
|
|
86
|
+
if (hasPoints && hasLines) {
|
|
87
|
+
return invalidStyle(
|
|
88
|
+
input,
|
|
89
|
+
'Points/Lines',
|
|
90
|
+
'components',
|
|
91
|
+
'Points + Lines',
|
|
92
|
+
'exactly one style component',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!hasPoints && !hasLines) {
|
|
96
|
+
return invalidStyle(input, 'Points/Lines', 'components', undefined, 'one style component');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasPoints) {
|
|
100
|
+
const sizePx = input.points?.sizePx ?? 4;
|
|
101
|
+
const shapeValue = input.points?.shape ?? PointShapeValue.square;
|
|
102
|
+
const shape = pointShapeFromU32(shapeValue);
|
|
103
|
+
if (!Number.isFinite(sizePx) || sizePx <= 0) {
|
|
104
|
+
return invalidStyle(input, 'Points', 'sizePx', sizePx, 'finite sizePx > 0');
|
|
105
|
+
}
|
|
106
|
+
if (shape === undefined) {
|
|
107
|
+
return invalidStyle(input, 'Points', 'shape', shapeValue, "shape is 'square' or 'circle'");
|
|
108
|
+
}
|
|
109
|
+
return ok({ component: 'Points', shape, sizePx });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const widthPx = input.lines?.widthPx ?? 1;
|
|
113
|
+
if (!Number.isFinite(widthPx) || widthPx <= 0) {
|
|
114
|
+
return invalidStyle(input, 'Lines', 'widthPx', widthPx, 'finite widthPx > 0');
|
|
115
|
+
}
|
|
116
|
+
return ok({ component: 'Lines', widthPx });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function checkTopology(
|
|
120
|
+
input: PointsLinesAdmissionInput,
|
|
121
|
+
component: 'Points' | 'Lines',
|
|
122
|
+
): Result<
|
|
123
|
+
{ pointCount: number; segmentCount: number; submeshes: readonly number[] },
|
|
124
|
+
PointsLinesAdmissionError
|
|
125
|
+
> {
|
|
126
|
+
const expected = component === 'Points' ? 'point-list' : 'line-list';
|
|
127
|
+
let pointCount = 0;
|
|
128
|
+
let segmentCount = 0;
|
|
129
|
+
const acceptedSubmeshes: number[] = [];
|
|
130
|
+
let sawNonEmpty = false;
|
|
131
|
+
|
|
132
|
+
for (const [submeshIndex, submesh] of input.mesh.submeshes.entries()) {
|
|
133
|
+
const indexed = input.mesh.indices !== undefined && submesh.indexCount > 0;
|
|
134
|
+
const elementCount = indexed ? submesh.indexCount : submesh.vertexCount;
|
|
135
|
+
if (elementCount === 0) continue;
|
|
136
|
+
sawNonEmpty = true;
|
|
137
|
+
|
|
138
|
+
if (component === 'Lines' && submesh.topology === 'line-strip') {
|
|
139
|
+
return err(
|
|
140
|
+
new StyleError({
|
|
141
|
+
lane: 'admission',
|
|
142
|
+
field: 'topology',
|
|
143
|
+
member: 'line-strip',
|
|
144
|
+
supported: ['line-list'],
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (submesh.topology !== expected) {
|
|
149
|
+
return topologyMismatch(input, submeshIndex, expected, submesh.topology);
|
|
150
|
+
}
|
|
151
|
+
if (component === 'Lines' && elementCount % 2 !== 0) {
|
|
152
|
+
return topologyMismatch(input, submeshIndex, 'line-list pairs', 'line-list odd tail');
|
|
153
|
+
}
|
|
154
|
+
acceptedSubmeshes.push(submeshIndex);
|
|
155
|
+
if (component === 'Points') pointCount += elementCount;
|
|
156
|
+
else segmentCount += elementCount / 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!sawNonEmpty) return topologyMismatch(input, 0, expected, 'empty-range');
|
|
160
|
+
return ok({ pointCount, segmentCount, submeshes: acceptedSubmeshes });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function checkMaterial(input: PointsLinesAdmissionInput): Result<void, PointsLinesAdmissionError> {
|
|
164
|
+
const passes = input.material.passes ?? [];
|
|
165
|
+
const unlitModules = new Set(['forgeax_material::unlit', 'forgeax::default-unlit']);
|
|
166
|
+
if (passes.length !== 1 || passes[0] === undefined) {
|
|
167
|
+
const pass =
|
|
168
|
+
passes.find(
|
|
169
|
+
(candidate) => candidate.name === 'deferred' || candidate.name === 'shadow-caster',
|
|
170
|
+
) ?? passes[0];
|
|
171
|
+
return err(
|
|
172
|
+
new MaterialError({
|
|
173
|
+
entity: input.entity,
|
|
174
|
+
material: input.materialId ?? '<anonymous>',
|
|
175
|
+
pass: pass?.name ?? '<empty>',
|
|
176
|
+
module: pass?.program.module ?? '<empty>',
|
|
177
|
+
reason: 'points-lines admission requires one unlit forward pass',
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const pass = passes[0];
|
|
182
|
+
const lightMode = (pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode;
|
|
183
|
+
if (
|
|
184
|
+
!unlitModules.has(pass.program.module) ||
|
|
185
|
+
pass.name !== 'forward' ||
|
|
186
|
+
lightMode === 'ShadowCaster'
|
|
187
|
+
) {
|
|
188
|
+
return err(
|
|
189
|
+
new MaterialError({
|
|
190
|
+
entity: input.entity,
|
|
191
|
+
material: input.materialId ?? '<anonymous>',
|
|
192
|
+
pass: pass.name,
|
|
193
|
+
module: pass.program.module,
|
|
194
|
+
reason: 'points-lines admission requires an engine-owned unlit forward pass',
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return ok(undefined);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Validate one complete Points/Lines candidate before any renderer publication. */
|
|
202
|
+
export function admitPointsLines(
|
|
203
|
+
input: PointsLinesAdmissionInput,
|
|
204
|
+
): Result<PointsLinesAdmission, PointsLinesAdmissionError> {
|
|
205
|
+
const style = checkStyle(input);
|
|
206
|
+
if (!style.ok) return style;
|
|
207
|
+
const topology = checkTopology(input, style.value.component);
|
|
208
|
+
if (!topology.ok) return topology;
|
|
209
|
+
const material = checkMaterial(input);
|
|
210
|
+
if (!material.ok) return material;
|
|
211
|
+
|
|
212
|
+
if (style.value.component === 'Points' && input.limits?.maxPoints !== undefined) {
|
|
213
|
+
if (topology.value.pointCount > input.limits.maxPoints) {
|
|
214
|
+
return err(
|
|
215
|
+
new BudgetError({
|
|
216
|
+
lane: 'admission',
|
|
217
|
+
requested: topology.value.pointCount,
|
|
218
|
+
limit: input.limits.maxPoints,
|
|
219
|
+
unit: 'points',
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (style.value.component === 'Lines' && input.limits?.maxSegments !== undefined) {
|
|
225
|
+
if (topology.value.segmentCount > input.limits.maxSegments) {
|
|
226
|
+
return err(
|
|
227
|
+
new BudgetError({
|
|
228
|
+
lane: 'admission',
|
|
229
|
+
requested: topology.value.segmentCount,
|
|
230
|
+
limit: input.limits.maxSegments,
|
|
231
|
+
unit: 'segments',
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return ok({ ...style.value, ...topology.value });
|
|
238
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PointsLinesStyle } from './snapshot';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expands the local culling envelope by the maximum screen-space raster
|
|
5
|
+
* radius. The shader applies the exact physical-pixel offset; this envelope
|
|
6
|
+
* is deliberately conservative so transformed edge primitives are retained.
|
|
7
|
+
*/
|
|
8
|
+
export function expandPointsLinesBounds(
|
|
9
|
+
bounds: ArrayLike<number>,
|
|
10
|
+
style: PointsLinesStyle | undefined,
|
|
11
|
+
): Float32Array {
|
|
12
|
+
if (bounds.length < 6 || style === undefined) return new Float32Array(bounds);
|
|
13
|
+
const marginPx = style.kind === 'points' ? style.sizePx * 0.5 : style.widthPx * 0.5;
|
|
14
|
+
if (!Number.isFinite(marginPx) || marginPx <= 0) return new Float32Array(bounds);
|
|
15
|
+
return new Float32Array([
|
|
16
|
+
(bounds[0] ?? 0) - marginPx,
|
|
17
|
+
(bounds[1] ?? 0) - marginPx,
|
|
18
|
+
(bounds[2] ?? 0) - marginPx,
|
|
19
|
+
(bounds[3] ?? 0) + marginPx,
|
|
20
|
+
(bounds[4] ?? 0) + marginPx,
|
|
21
|
+
(bounds[5] ?? 0) + marginPx,
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { MeshAsset } from '@forgeax/engine-types';
|
|
2
|
+
import type { PointsLinesRetainedSnapshot } from './snapshot';
|
|
3
|
+
|
|
4
|
+
export interface PointsLinesExpandedGeometry {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly component: 'Points' | 'Lines';
|
|
7
|
+
/** Source endpoints retained for the M3 triangle-expansion consumer. */
|
|
8
|
+
readonly positions: Float32Array;
|
|
9
|
+
/** Interleaved position/otherPosition/corner stream for WGSL. */
|
|
10
|
+
readonly vertices: Float32Array;
|
|
11
|
+
/** Six indices per expanded quad. */
|
|
12
|
+
readonly indices: Uint32Array;
|
|
13
|
+
readonly sourceVertexCount: number;
|
|
14
|
+
readonly pointCount: number;
|
|
15
|
+
readonly segmentCount: number;
|
|
16
|
+
readonly expandedVertexCount: number;
|
|
17
|
+
readonly expandedIndexCount: number;
|
|
18
|
+
readonly sourceBytes: number;
|
|
19
|
+
readonly derivedBytes: number;
|
|
20
|
+
readonly bounds: Float32Array;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Shared CPU expansion cache. Material generations are not geometry key data. */
|
|
24
|
+
export class PointsLinesExpansionCache {
|
|
25
|
+
private readonly entries = new Map<string, PointsLinesExpandedGeometry>();
|
|
26
|
+
|
|
27
|
+
getOrCreate(snapshot: PointsLinesRetainedSnapshot, mesh: MeshAsset): PointsLinesExpandedGeometry {
|
|
28
|
+
if (snapshot.component === undefined || snapshot.style === undefined) {
|
|
29
|
+
return emptyGeometry(snapshot);
|
|
30
|
+
}
|
|
31
|
+
const key = expansionKey(snapshot);
|
|
32
|
+
const cached = this.entries.get(key);
|
|
33
|
+
if (cached !== undefined) return cached;
|
|
34
|
+
const geometry = deriveExpandedGeometry(snapshot, mesh, key);
|
|
35
|
+
this.entries.set(key, geometry);
|
|
36
|
+
return geometry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clear(): void {
|
|
40
|
+
this.entries.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get size(): number {
|
|
44
|
+
return this.entries.size;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function expansionKey(snapshot: PointsLinesRetainedSnapshot): string {
|
|
49
|
+
return `mesh:${snapshot.meshHandle}:${snapshot.meshGeneration}:component=${snapshot.component ?? 'none'}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function deriveExpandedGeometry(
|
|
53
|
+
snapshot: PointsLinesRetainedSnapshot,
|
|
54
|
+
mesh: MeshAsset,
|
|
55
|
+
key: string,
|
|
56
|
+
): PointsLinesExpandedGeometry {
|
|
57
|
+
const component = snapshot.component;
|
|
58
|
+
const style = snapshot.style;
|
|
59
|
+
if (component === undefined || style === undefined) return emptyGeometry(snapshot);
|
|
60
|
+
const sourcePositions = readPositions(mesh);
|
|
61
|
+
const positions: number[] = [];
|
|
62
|
+
const vertices: number[] = [];
|
|
63
|
+
const indices: number[] = [];
|
|
64
|
+
let sourceCursor = 0;
|
|
65
|
+
let pointCount = 0;
|
|
66
|
+
let segmentCount = 0;
|
|
67
|
+
const expectedTopology = component === 'Points' ? 'point-list' : 'line-list';
|
|
68
|
+
for (const submesh of mesh.submeshes) {
|
|
69
|
+
if (submesh.topology !== expectedTopology) {
|
|
70
|
+
sourceCursor += submesh.vertexCount;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const elements =
|
|
74
|
+
mesh.indices !== undefined && submesh.indexCount > 0
|
|
75
|
+
? Array.from(
|
|
76
|
+
mesh.indices.slice(submesh.indexOffset, submesh.indexOffset + submesh.indexCount),
|
|
77
|
+
)
|
|
78
|
+
: Array.from({ length: submesh.vertexCount }, (_, index) => sourceCursor + index);
|
|
79
|
+
for (const index of elements) {
|
|
80
|
+
const offset = index * 3;
|
|
81
|
+
positions.push(
|
|
82
|
+
sourcePositions[offset] ?? 0,
|
|
83
|
+
sourcePositions[offset + 1] ?? 0,
|
|
84
|
+
sourcePositions[offset + 2] ?? 0,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (component === 'Points') {
|
|
88
|
+
pointCount += elements.length;
|
|
89
|
+
for (const index of elements) {
|
|
90
|
+
const offset = index * 3;
|
|
91
|
+
const position = [
|
|
92
|
+
sourcePositions[offset] ?? 0,
|
|
93
|
+
sourcePositions[offset + 1] ?? 0,
|
|
94
|
+
sourcePositions[offset + 2] ?? 0,
|
|
95
|
+
];
|
|
96
|
+
if (style.kind !== 'points') continue;
|
|
97
|
+
emitQuad(vertices, indices, position, position);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
segmentCount += Math.floor(elements.length / 2);
|
|
101
|
+
for (let element = 0; element + 1 < elements.length; element += 2) {
|
|
102
|
+
const startOffset = (elements[element] ?? 0) * 3;
|
|
103
|
+
const endOffset = (elements[element + 1] ?? 0) * 3;
|
|
104
|
+
emitQuad(
|
|
105
|
+
vertices,
|
|
106
|
+
indices,
|
|
107
|
+
[
|
|
108
|
+
sourcePositions[startOffset] ?? 0,
|
|
109
|
+
sourcePositions[startOffset + 1] ?? 0,
|
|
110
|
+
sourcePositions[startOffset + 2] ?? 0,
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
sourcePositions[endOffset] ?? 0,
|
|
114
|
+
sourcePositions[endOffset + 1] ?? 0,
|
|
115
|
+
sourcePositions[endOffset + 2] ?? 0,
|
|
116
|
+
],
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
sourceCursor += submesh.vertexCount;
|
|
121
|
+
}
|
|
122
|
+
const expanded = new Float32Array(positions);
|
|
123
|
+
const expandedVertices = new Float32Array(vertices);
|
|
124
|
+
const expandedIndices = new Uint32Array(indices);
|
|
125
|
+
return {
|
|
126
|
+
key,
|
|
127
|
+
component,
|
|
128
|
+
positions: expanded,
|
|
129
|
+
vertices: expandedVertices,
|
|
130
|
+
indices: expandedIndices,
|
|
131
|
+
sourceVertexCount: sourcePositions.length / 3,
|
|
132
|
+
pointCount,
|
|
133
|
+
segmentCount,
|
|
134
|
+
expandedVertexCount: expandedVertices.length / 8,
|
|
135
|
+
expandedIndexCount: expandedIndices.length,
|
|
136
|
+
sourceBytes: mesh.vertices.byteLength + (mesh.indices?.byteLength ?? 0),
|
|
137
|
+
derivedBytes: expandedVertices.byteLength + expandedIndices.byteLength,
|
|
138
|
+
bounds: boundsOf(expanded),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function emitQuad(
|
|
143
|
+
vertices: number[],
|
|
144
|
+
indices: number[],
|
|
145
|
+
start: readonly number[],
|
|
146
|
+
end: readonly number[],
|
|
147
|
+
): void {
|
|
148
|
+
const base = vertices.length / 8;
|
|
149
|
+
const corners: readonly (readonly [number, number])[] = [
|
|
150
|
+
[-1, -1],
|
|
151
|
+
[-1, 1],
|
|
152
|
+
[1, -1],
|
|
153
|
+
[1, 1],
|
|
154
|
+
];
|
|
155
|
+
for (const [x, y] of corners) {
|
|
156
|
+
vertices.push(
|
|
157
|
+
start[0] ?? 0,
|
|
158
|
+
start[1] ?? 0,
|
|
159
|
+
start[2] ?? 0,
|
|
160
|
+
end[0] ?? 0,
|
|
161
|
+
end[1] ?? 0,
|
|
162
|
+
end[2] ?? 0,
|
|
163
|
+
x,
|
|
164
|
+
y,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
indices.push(base, base + 1, base + 2, base + 2, base + 1, base + 3);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readPositions(mesh: MeshAsset): Float32Array {
|
|
171
|
+
const attribute = mesh.attributes.position;
|
|
172
|
+
if (attribute instanceof Float32Array && attribute.length >= 3) {
|
|
173
|
+
return attribute;
|
|
174
|
+
}
|
|
175
|
+
const vertexCount = mesh.vertices.length / 3;
|
|
176
|
+
const stride = vertexCount > 0 ? mesh.vertices.length / vertexCount : 3;
|
|
177
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
178
|
+
for (let vertex = 0; vertex < vertexCount; vertex += 1) {
|
|
179
|
+
const source = vertex * stride;
|
|
180
|
+
positions.set(mesh.vertices.subarray(source, source + 3), vertex * 3);
|
|
181
|
+
}
|
|
182
|
+
return positions;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function boundsOf(positions: Float32Array): Float32Array {
|
|
186
|
+
if (positions.length === 0) return new Float32Array(0);
|
|
187
|
+
const bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
|
|
188
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
189
|
+
bounds[0] = Math.min(bounds[0] ?? Infinity, positions[index] ?? 0);
|
|
190
|
+
bounds[1] = Math.min(bounds[1] ?? Infinity, positions[index + 1] ?? 0);
|
|
191
|
+
bounds[2] = Math.min(bounds[2] ?? Infinity, positions[index + 2] ?? 0);
|
|
192
|
+
bounds[3] = Math.max(bounds[3] ?? -Infinity, positions[index] ?? 0);
|
|
193
|
+
bounds[4] = Math.max(bounds[4] ?? -Infinity, positions[index + 1] ?? 0);
|
|
194
|
+
bounds[5] = Math.max(bounds[5] ?? -Infinity, positions[index + 2] ?? 0);
|
|
195
|
+
}
|
|
196
|
+
return bounds;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function emptyGeometry(snapshot: PointsLinesRetainedSnapshot): PointsLinesExpandedGeometry {
|
|
200
|
+
return {
|
|
201
|
+
key: `empty:${snapshot.entityKey}`,
|
|
202
|
+
component: snapshot.component ?? 'Points',
|
|
203
|
+
positions: new Float32Array(0),
|
|
204
|
+
vertices: new Float32Array(0),
|
|
205
|
+
indices: new Uint32Array(0),
|
|
206
|
+
sourceVertexCount: 0,
|
|
207
|
+
pointCount: 0,
|
|
208
|
+
segmentCount: 0,
|
|
209
|
+
expandedVertexCount: 0,
|
|
210
|
+
expandedIndexCount: 0,
|
|
211
|
+
sourceBytes: 0,
|
|
212
|
+
derivedBytes: 0,
|
|
213
|
+
bounds: new Float32Array(0),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { RenderError } from '../errors/render';
|
|
2
|
+
import type { PointsLinesRetainedSnapshot, PointsLinesStyle } from './snapshot';
|
|
3
|
+
|
|
4
|
+
export type PointsLinesSourceError = Extract<
|
|
5
|
+
RenderError,
|
|
6
|
+
{ readonly code: `points-lines-${string}` }
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export type PointsLinesInspectionLane =
|
|
10
|
+
| 'pending'
|
|
11
|
+
| 'refused'
|
|
12
|
+
| 'direct'
|
|
13
|
+
| 'clustered'
|
|
14
|
+
| 'cpu-webgl2';
|
|
15
|
+
|
|
16
|
+
export interface PointsLinesInspectionCache {
|
|
17
|
+
readonly hit: boolean;
|
|
18
|
+
readonly rebuilds: number;
|
|
19
|
+
readonly evictions: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type PointsLinesInspectionRefusal = Pick<
|
|
23
|
+
PointsLinesSourceError,
|
|
24
|
+
'code' | 'expected' | 'hint' | 'detail'
|
|
25
|
+
> & {
|
|
26
|
+
readonly generation: number;
|
|
27
|
+
readonly lastKnownGood: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface PointsLinesInspection {
|
|
31
|
+
readonly entityKey: number;
|
|
32
|
+
readonly worldId: number;
|
|
33
|
+
readonly component: 'Points' | 'Lines';
|
|
34
|
+
readonly meshHandle: number;
|
|
35
|
+
readonly meshGeneration: number;
|
|
36
|
+
readonly materialHandle: number;
|
|
37
|
+
readonly materialGeneration: number;
|
|
38
|
+
readonly style: PointsLinesStyle;
|
|
39
|
+
readonly topology: 'point-list' | 'line-list';
|
|
40
|
+
readonly lane: PointsLinesInspectionLane;
|
|
41
|
+
readonly pointCount: number;
|
|
42
|
+
readonly segmentCount: number;
|
|
43
|
+
readonly sourceBytes: number;
|
|
44
|
+
readonly derivedBytes: number;
|
|
45
|
+
readonly cache: PointsLinesInspectionCache;
|
|
46
|
+
readonly drawCount: number;
|
|
47
|
+
readonly uploadBytes: number;
|
|
48
|
+
readonly lastKnownGood: boolean;
|
|
49
|
+
readonly refusal?: PointsLinesInspectionRefusal;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PointsLinesInspectionInput {
|
|
53
|
+
readonly snapshot: PointsLinesRetainedSnapshot;
|
|
54
|
+
readonly topology: 'point-list' | 'line-list';
|
|
55
|
+
readonly lane: PointsLinesInspectionLane;
|
|
56
|
+
readonly pointCount: number;
|
|
57
|
+
readonly segmentCount: number;
|
|
58
|
+
readonly sourceBytes: number;
|
|
59
|
+
readonly derivedBytes: number;
|
|
60
|
+
readonly cache: PointsLinesInspectionCache;
|
|
61
|
+
readonly drawCount: number;
|
|
62
|
+
readonly uploadBytes: number;
|
|
63
|
+
readonly lastKnownGood: boolean;
|
|
64
|
+
readonly refusal?: PointsLinesInspectionRefusal;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function inspectPointsLines(input: PointsLinesInspectionInput): PointsLinesInspection {
|
|
68
|
+
const { snapshot } = input;
|
|
69
|
+
if (snapshot.component === undefined) {
|
|
70
|
+
throw new Error('Points/Lines inspection requires a retained style component');
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
entityKey: snapshot.entityKey,
|
|
74
|
+
worldId: snapshot.worldId,
|
|
75
|
+
component: snapshot.component,
|
|
76
|
+
meshHandle: snapshot.meshHandle,
|
|
77
|
+
meshGeneration: snapshot.meshGeneration,
|
|
78
|
+
materialHandle: snapshot.materialHandle,
|
|
79
|
+
materialGeneration: snapshot.materialGeneration,
|
|
80
|
+
style: snapshot.style ?? { kind: 'lines', widthPx: 0 },
|
|
81
|
+
topology: input.topology,
|
|
82
|
+
lane: input.lane,
|
|
83
|
+
pointCount: input.pointCount,
|
|
84
|
+
segmentCount: input.segmentCount,
|
|
85
|
+
sourceBytes: input.sourceBytes,
|
|
86
|
+
derivedBytes: input.derivedBytes,
|
|
87
|
+
cache: { ...input.cache },
|
|
88
|
+
drawCount: input.drawCount,
|
|
89
|
+
uploadBytes: input.uploadBytes,
|
|
90
|
+
lastKnownGood: input.lastKnownGood,
|
|
91
|
+
...(input.refusal === undefined ? {} : { refusal: { ...input.refusal } }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function pointsLinesInspectionToJson(inspection: PointsLinesInspection): string {
|
|
96
|
+
return JSON.stringify(inspection);
|
|
97
|
+
}
|