@forgeax/engine-render 0.1.24 → 0.1.25
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 +100 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/surface-projection.d.ts +24 -0
- package/dist/assembly/material/surface-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +20 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
- package/dist/assets/asset-decoders.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
- package/dist/chunk-ADHHVYLW.mjs.map +1 -0
- package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
- package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
- package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
- package/dist/chunk-HKXTW355.mjs.map +1 -0
- package/dist/{chunk-YKB5DQV7.mjs → chunk-JSEDTZYS.mjs} +87 -19
- package/dist/chunk-JSEDTZYS.mjs.map +1 -0
- package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
- package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
- package/dist/{chunk-Z3YYH2MB.mjs → chunk-N3RT2EUR.mjs} +94 -41
- package/dist/chunk-N3RT2EUR.mjs.map +1 -0
- package/dist/{chunk-YRUYGA4A.mjs → chunk-TCLDX7SZ.mjs} +559 -138
- package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
- package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
- package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
- package/dist/components/instances.d.ts +13 -31
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +865 -142
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +6 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +4 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instance-buffer-cache.d.ts +7 -1
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +101 -0
- package/dist/instances.d.ts.map +1 -0
- package/dist/internal.mjs +5 -5
- package/dist/materials.d.ts +63 -2
- package/dist/materials.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +13 -2
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +22 -3
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +11 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +4 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +4 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +25 -1
- 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 +2 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +12 -1
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +6 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/package.json +20 -20
- package/src/__tests__/gpu-scene.unit.test.ts +38 -2
- package/src/__tests__/instances-store.unit.test.ts +46 -0
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +5 -13
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
- package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
- package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
- package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
- package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
- package/src/assembly/factory.ts +2 -0
- package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
- package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
- package/src/assembly/material/surface-projection.ts +90 -0
- package/src/assembly/material-shader-policy.ts +88 -10
- package/src/assembly/recovery/renderer-recover.ts +1 -1
- package/src/assembly/webgpu-ready.ts +3 -16
- package/src/assembly/webgpu-renderer.ts +90 -158
- package/src/assembly/webgpu-vertex-layouts.ts +132 -0
- package/src/assets/asset-decoders.ts +27 -4
- package/src/components/instances.ts +14 -197
- package/src/gpu-driven/production-raster.ts +34 -1
- package/src/gpu-scene.ts +84 -29
- package/src/index.ts +1 -0
- package/src/instance-buffer-cache.ts +14 -1
- package/src/instances.ts +288 -0
- package/src/materials.ts +107 -12
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec.ts +6 -14
- package/src/record/frame-snapshot.ts +19 -2
- package/src/record/frame.ts +31 -2
- package/src/record/main-pass-geometry.ts +498 -59
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/render-context.ts +4 -0
- package/src/record/shadow-pass.ts +46 -99
- package/src/recovery/render-system-candidate.ts +41 -4
- package/src/render-contract.ts +4 -0
- package/src/render-system-extract-tail.ts +107 -51
- package/src/render-system-extract.ts +53 -26
- package/src/render-system.ts +40 -5
- package/src/scene/render-scene-types.ts +2 -0
- package/src/scene/render-scene.ts +579 -36
- package/src/systems/skin-palette-allocator.ts +129 -7
- package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
- package/dist/chunk-KZFTK6DO.mjs.map +0 -1
- package/dist/chunk-Q752SHI5.mjs.map +0 -1
- package/dist/chunk-YKB5DQV7.mjs.map +0 -1
- package/dist/chunk-YRUYGA4A.mjs.map +0 -1
- package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
package/src/instances.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { RhiErrorCode } from '@forgeax/engine-rhi';
|
|
3
|
+
|
|
4
|
+
const MATRIX_STRIDE = 16;
|
|
5
|
+
|
|
6
|
+
export type InstanceCollectionId = number & {
|
|
7
|
+
readonly __forgeaxInstanceCollectionId: unique symbol;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface InstanceCollectionInfo {
|
|
11
|
+
readonly collectionId: InstanceCollectionId;
|
|
12
|
+
readonly count: number;
|
|
13
|
+
readonly revision: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InstanceUploadRange {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InstanceCollectionSnapshot extends InstanceCollectionInfo {
|
|
22
|
+
readonly transforms: Float32Array;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The renderer-owned submission lane selected for one collection. */
|
|
26
|
+
export type InstanceSubmissionLane =
|
|
27
|
+
| 'unresident'
|
|
28
|
+
| 'direct-storage'
|
|
29
|
+
| 'chunked-storage'
|
|
30
|
+
| 'direct-uniform'
|
|
31
|
+
| 'chunked-uniform'
|
|
32
|
+
| 'unavailable';
|
|
33
|
+
|
|
34
|
+
/** Backend identity carried by collection residency and failure facts. */
|
|
35
|
+
export type InstanceBackendKind = 'webgpu' | 'wgpu-native' | 'wgpu-webgl2' | 'null' | 'unknown';
|
|
36
|
+
|
|
37
|
+
/** Closed record-stage failure vocabulary for collection residency. */
|
|
38
|
+
export type InstanceCollectionFailureCode = RhiErrorCode | 'queue-write-buffer-failed';
|
|
39
|
+
|
|
40
|
+
/** Typed producer facts retained when a collection cannot be admitted. */
|
|
41
|
+
export interface InstanceCollectionFailureFacts {
|
|
42
|
+
readonly requestedBytes: number;
|
|
43
|
+
readonly supportedBytes: number | undefined;
|
|
44
|
+
readonly backend: InstanceBackendKind;
|
|
45
|
+
readonly owner: 'renderer.instances';
|
|
46
|
+
readonly cause: string;
|
|
47
|
+
readonly recovery: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Detached, bounded collection evidence exposed by Renderer.inspect(). */
|
|
51
|
+
export interface InstanceCollectionInspection extends InstanceCollectionInfo {
|
|
52
|
+
readonly residentGeneration: number | undefined;
|
|
53
|
+
readonly lane: InstanceSubmissionLane;
|
|
54
|
+
readonly uploadRanges: readonly InstanceUploadRange[];
|
|
55
|
+
readonly uploadedBytes: number;
|
|
56
|
+
readonly requestedBytes: number;
|
|
57
|
+
readonly supportedBytes: number | undefined;
|
|
58
|
+
readonly backend: InstanceBackendKind;
|
|
59
|
+
readonly owner: 'renderer.instances';
|
|
60
|
+
readonly error:
|
|
61
|
+
| {
|
|
62
|
+
readonly code: InstanceCollectionFailureCode;
|
|
63
|
+
readonly expected: string;
|
|
64
|
+
readonly hint: string;
|
|
65
|
+
readonly detail: InstanceCollectionFailureFacts;
|
|
66
|
+
}
|
|
67
|
+
| undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface InstanceResidency {
|
|
71
|
+
readonly frameNumber: number;
|
|
72
|
+
readonly residentGeneration: number | undefined;
|
|
73
|
+
readonly lane: InstanceSubmissionLane;
|
|
74
|
+
readonly uploadRanges: readonly InstanceUploadRange[];
|
|
75
|
+
readonly uploadedBytes: number;
|
|
76
|
+
readonly requestedBytes: number;
|
|
77
|
+
readonly supportedBytes: number | undefined;
|
|
78
|
+
readonly backend: InstanceBackendKind;
|
|
79
|
+
readonly error: InstanceCollectionInspection['error'];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function appendRange(ranges: InstanceUploadRange[], start: number, end: number): void {
|
|
83
|
+
if (start >= end) return;
|
|
84
|
+
// Updates are allowed to arrive in any order. Insert at the first range
|
|
85
|
+
// whose end reaches the new interval, then consume every range that starts
|
|
86
|
+
// before the merged end. This preserves the complete union for both
|
|
87
|
+
// adjacent and overlapping writes (for example [10, 12) then [8, 11)).
|
|
88
|
+
let index = 0;
|
|
89
|
+
while (index < ranges.length) {
|
|
90
|
+
const existing = ranges[index];
|
|
91
|
+
if (existing === undefined || existing.end >= start) break;
|
|
92
|
+
index += 1;
|
|
93
|
+
}
|
|
94
|
+
while (index < ranges.length) {
|
|
95
|
+
const existing = ranges[index];
|
|
96
|
+
if (existing === undefined || existing.start > end) break;
|
|
97
|
+
start = Math.min(start, existing.start);
|
|
98
|
+
end = Math.max(end, existing.end);
|
|
99
|
+
ranges.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
ranges.splice(index, 0, { start, end });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface CollectionRecord extends InstanceCollectionInfo {
|
|
105
|
+
readonly world: World;
|
|
106
|
+
readonly entity: number;
|
|
107
|
+
readonly transforms: Float32Array;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Rebuildable per-renderer projection. World remains the authoring authority. */
|
|
111
|
+
export class InstanceProjectionStore {
|
|
112
|
+
private nextId = 1;
|
|
113
|
+
private identities = new WeakMap<World, Map<number, InstanceCollectionId>>();
|
|
114
|
+
private readonly records = new Map<InstanceCollectionId, CollectionRecord>();
|
|
115
|
+
|
|
116
|
+
project(world: World, entity: number, transforms: ArrayLike<number>): InstanceCollectionSnapshot {
|
|
117
|
+
let entities = this.identities.get(world);
|
|
118
|
+
if (entities === undefined) {
|
|
119
|
+
entities = new Map();
|
|
120
|
+
this.identities.set(world, entities);
|
|
121
|
+
}
|
|
122
|
+
let collectionId = entities.get(entity);
|
|
123
|
+
if (collectionId === undefined) {
|
|
124
|
+
collectionId = this.nextId++ as InstanceCollectionId;
|
|
125
|
+
entities.set(entity, collectionId);
|
|
126
|
+
}
|
|
127
|
+
const previous = this.records.get(collectionId);
|
|
128
|
+
const unchanged =
|
|
129
|
+
previous !== undefined &&
|
|
130
|
+
previous.transforms.length === transforms.length &&
|
|
131
|
+
previous.transforms.every((value, index) => Object.is(value, transforms[index]));
|
|
132
|
+
const record: CollectionRecord = unchanged
|
|
133
|
+
? previous
|
|
134
|
+
: {
|
|
135
|
+
collectionId,
|
|
136
|
+
world,
|
|
137
|
+
entity,
|
|
138
|
+
transforms: new Float32Array(transforms),
|
|
139
|
+
count: transforms.length / MATRIX_STRIDE,
|
|
140
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
141
|
+
};
|
|
142
|
+
this.records.set(collectionId, record);
|
|
143
|
+
// Every consumer gets a complete snapshot. Reading never acknowledges an
|
|
144
|
+
// upload; each GPU resident records its own successfully uploaded revision.
|
|
145
|
+
return { ...this.info(record), transforms: record.transforms };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
retain(ids: ReadonlySet<InstanceCollectionId>): void {
|
|
149
|
+
for (const [id, record] of this.records) {
|
|
150
|
+
if (ids.has(id)) continue;
|
|
151
|
+
this.identities.get(record.world)?.delete(record.entity);
|
|
152
|
+
this.records.delete(id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Release one producer-owned collection when its render identity is removed. */
|
|
157
|
+
release(world: World, entity: number): void {
|
|
158
|
+
const entities = this.identities.get(world);
|
|
159
|
+
const collectionId = entities?.get(entity);
|
|
160
|
+
if (collectionId === undefined) return;
|
|
161
|
+
entities?.delete(entity);
|
|
162
|
+
this.records.delete(collectionId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
dispose(): void {
|
|
166
|
+
this.records.clear();
|
|
167
|
+
this.identities = new WeakMap();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Return detached residency facts for every live collection.
|
|
172
|
+
*
|
|
173
|
+
* @internal Joins CPU projection identity with the active device residency;
|
|
174
|
+
* callers only receive immutable PODs and never GPU handles.
|
|
175
|
+
*/
|
|
176
|
+
_inspections(
|
|
177
|
+
residency: ReadonlyMap<InstanceCollectionId, InstanceResidency> | undefined,
|
|
178
|
+
frameNumber: number,
|
|
179
|
+
): readonly InstanceCollectionInspection[] {
|
|
180
|
+
return Object.freeze(
|
|
181
|
+
[...this.records.values()].map((record) =>
|
|
182
|
+
this.projectInspection(record, residency?.get(record.collectionId), frameNumber),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private info(record: CollectionRecord): InstanceCollectionInfo {
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
collectionId: record.collectionId,
|
|
190
|
+
count: record.count,
|
|
191
|
+
revision: record.revision,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private projectInspection(
|
|
196
|
+
record: CollectionRecord,
|
|
197
|
+
residency: InstanceResidency | undefined,
|
|
198
|
+
frameNumber: number,
|
|
199
|
+
): InstanceCollectionInspection {
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
...this.info(record),
|
|
202
|
+
residentGeneration: residency?.residentGeneration,
|
|
203
|
+
lane: residency?.lane ?? 'unresident',
|
|
204
|
+
uploadRanges: Object.freeze(
|
|
205
|
+
residency?.frameNumber === frameNumber
|
|
206
|
+
? residency.uploadRanges.map((range) => ({ ...range }))
|
|
207
|
+
: [],
|
|
208
|
+
),
|
|
209
|
+
uploadedBytes: residency?.frameNumber === frameNumber ? residency.uploadedBytes : 0,
|
|
210
|
+
requestedBytes: residency?.requestedBytes ?? record.count * MATRIX_STRIDE * 4,
|
|
211
|
+
supportedBytes: residency?.supportedBytes,
|
|
212
|
+
backend: residency?.backend ?? 'unknown',
|
|
213
|
+
owner: 'renderer.instances',
|
|
214
|
+
error: residency?.error,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @internal Renderer record owner reports the accepted collection lane. */
|
|
220
|
+
export function recordInstanceResidency(
|
|
221
|
+
residency: Map<InstanceCollectionId, InstanceResidency>,
|
|
222
|
+
input: {
|
|
223
|
+
readonly collectionId: InstanceCollectionId;
|
|
224
|
+
readonly frameNumber: number;
|
|
225
|
+
readonly residentGeneration: number;
|
|
226
|
+
readonly lane: Exclude<InstanceSubmissionLane, 'unresident' | 'unavailable'>;
|
|
227
|
+
readonly uploadRanges: readonly InstanceUploadRange[];
|
|
228
|
+
readonly uploadedBytes: number;
|
|
229
|
+
readonly requestedBytes: number;
|
|
230
|
+
readonly supportedBytes: number | undefined;
|
|
231
|
+
readonly backend: InstanceBackendKind;
|
|
232
|
+
},
|
|
233
|
+
): void {
|
|
234
|
+
const previous = residency.get(input.collectionId);
|
|
235
|
+
const sameFrame =
|
|
236
|
+
previous !== undefined &&
|
|
237
|
+
previous.frameNumber === input.frameNumber &&
|
|
238
|
+
previous.error === undefined;
|
|
239
|
+
const uploadRanges = sameFrame
|
|
240
|
+
? [...previous.uploadRanges].reduce(
|
|
241
|
+
(ranges, range) => {
|
|
242
|
+
appendRange(ranges, range.start, range.end);
|
|
243
|
+
return ranges;
|
|
244
|
+
},
|
|
245
|
+
input.uploadRanges.map((range) => ({ ...range })),
|
|
246
|
+
)
|
|
247
|
+
: input.uploadRanges.map((range) => ({ ...range }));
|
|
248
|
+
residency.set(input.collectionId, {
|
|
249
|
+
frameNumber: input.frameNumber,
|
|
250
|
+
residentGeneration: input.residentGeneration,
|
|
251
|
+
lane: input.lane,
|
|
252
|
+
uploadRanges,
|
|
253
|
+
uploadedBytes: sameFrame ? previous.uploadedBytes + input.uploadedBytes : input.uploadedBytes,
|
|
254
|
+
requestedBytes: input.requestedBytes,
|
|
255
|
+
supportedBytes: input.supportedBytes,
|
|
256
|
+
backend: input.backend,
|
|
257
|
+
error: undefined,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** @internal Renderer record owner reports a typed admission failure. */
|
|
262
|
+
export function recordInstanceFailure(
|
|
263
|
+
residency: Map<InstanceCollectionId, InstanceResidency>,
|
|
264
|
+
input: {
|
|
265
|
+
readonly collectionId: InstanceCollectionId;
|
|
266
|
+
readonly code: InstanceCollectionFailureCode;
|
|
267
|
+
readonly expected: string;
|
|
268
|
+
readonly hint: string;
|
|
269
|
+
readonly facts: InstanceCollectionFailureFacts;
|
|
270
|
+
},
|
|
271
|
+
): void {
|
|
272
|
+
residency.set(input.collectionId, {
|
|
273
|
+
frameNumber: -1,
|
|
274
|
+
residentGeneration: undefined,
|
|
275
|
+
lane: 'unavailable',
|
|
276
|
+
uploadRanges: [],
|
|
277
|
+
uploadedBytes: 0,
|
|
278
|
+
requestedBytes: input.facts.requestedBytes,
|
|
279
|
+
supportedBytes: input.facts.supportedBytes,
|
|
280
|
+
backend: input.facts.backend,
|
|
281
|
+
error: {
|
|
282
|
+
code: input.code,
|
|
283
|
+
expected: input.expected,
|
|
284
|
+
hint: input.hint,
|
|
285
|
+
detail: { ...input.facts },
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
package/src/materials.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type {
|
|
|
9
9
|
MaterialValue,
|
|
10
10
|
} from '@forgeax/engine-types';
|
|
11
11
|
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_STANDARD_SURFACE_MODULE,
|
|
14
|
+
projectStandardSurfacePasses,
|
|
15
|
+
} from './assembly/material/surface-projection';
|
|
12
16
|
|
|
13
17
|
export type MaterialTransmissionValidationReason =
|
|
14
18
|
| 'non-finite'
|
|
@@ -58,7 +62,6 @@ export const SPRITE_PREMULTIPLIED_ALPHA_BLEND: GPUBlendState = {
|
|
|
58
62
|
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
59
63
|
};
|
|
60
64
|
|
|
61
|
-
const STANDARD_MODULE = 'forgeax_material::standard';
|
|
62
65
|
const UNLIT_MODULE = 'forgeax_material::unlit';
|
|
63
66
|
const SPRITE_MODULE = 'forgeax_material::sprite';
|
|
64
67
|
|
|
@@ -389,6 +392,70 @@ interface StandardOpts {
|
|
|
389
392
|
readonly queue?: number;
|
|
390
393
|
}
|
|
391
394
|
|
|
395
|
+
/** Built-in Standard authoring surface with engine-owned parameter defaults. */
|
|
396
|
+
export interface DefaultStandardOptions extends StandardOpts {
|
|
397
|
+
readonly surfaceModule?: never;
|
|
398
|
+
readonly parameters?: never;
|
|
399
|
+
readonly values?: never;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Import-first Standard Surface authoring; the root parameter contract stays caller-owned. */
|
|
403
|
+
export interface CustomStandardSurfaceOptions {
|
|
404
|
+
readonly surfaceModule: string;
|
|
405
|
+
readonly parameters: readonly MaterialParameter[];
|
|
406
|
+
readonly values: Readonly<Record<string, MaterialValue>>;
|
|
407
|
+
readonly colorSpace?: MaterialColorSpace;
|
|
408
|
+
readonly renderState?: MaterialRenderState;
|
|
409
|
+
readonly castShadow?: boolean;
|
|
410
|
+
readonly queue?: number;
|
|
411
|
+
readonly baseColor?: never;
|
|
412
|
+
readonly metallic?: never;
|
|
413
|
+
readonly roughness?: never;
|
|
414
|
+
readonly metallicChannel?: never;
|
|
415
|
+
readonly roughnessChannel?: never;
|
|
416
|
+
readonly clearcoat?: never;
|
|
417
|
+
readonly clearcoatRoughness?: never;
|
|
418
|
+
readonly clearcoatTexture?: never;
|
|
419
|
+
readonly clearcoatRoughnessTexture?: never;
|
|
420
|
+
readonly clearcoatNormalTexture?: never;
|
|
421
|
+
readonly clearcoatNormalScale?: never;
|
|
422
|
+
readonly anisotropyStrength?: never;
|
|
423
|
+
readonly anisotropyRotation?: never;
|
|
424
|
+
readonly anisotropyTexture?: never;
|
|
425
|
+
readonly sheenColor?: never;
|
|
426
|
+
readonly sheenRoughness?: never;
|
|
427
|
+
readonly sheenColorTexture?: never;
|
|
428
|
+
readonly sheenRoughnessTexture?: never;
|
|
429
|
+
readonly iridescence?: never;
|
|
430
|
+
readonly iridescenceIor?: never;
|
|
431
|
+
readonly iridescenceThicknessMinimum?: never;
|
|
432
|
+
readonly iridescenceThicknessMaximum?: never;
|
|
433
|
+
readonly iridescenceTexture?: never;
|
|
434
|
+
readonly iridescenceThicknessTexture?: never;
|
|
435
|
+
readonly specular?: never;
|
|
436
|
+
readonly specularColor?: never;
|
|
437
|
+
readonly specularTexture?: never;
|
|
438
|
+
readonly specularColorTexture?: never;
|
|
439
|
+
readonly emissive?: never;
|
|
440
|
+
readonly emissiveIntensity?: never;
|
|
441
|
+
readonly emissiveTexture?: never;
|
|
442
|
+
readonly baseColorTexture?: never;
|
|
443
|
+
readonly metallicRoughnessTexture?: never;
|
|
444
|
+
readonly normalTexture?: never;
|
|
445
|
+
readonly occlusionTexture?: never;
|
|
446
|
+
readonly occlusionStrength?: never;
|
|
447
|
+
readonly alphaCutoff?: never;
|
|
448
|
+
readonly transmission?: never;
|
|
449
|
+
readonly transmissionTexture?: never;
|
|
450
|
+
readonly ior?: never;
|
|
451
|
+
readonly thickness?: never;
|
|
452
|
+
readonly thicknessTexture?: never;
|
|
453
|
+
readonly attenuationColor?: never;
|
|
454
|
+
readonly attenuationDistance?: never;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export type StandardOptions = DefaultStandardOptions | CustomStandardSurfaceOptions;
|
|
458
|
+
|
|
392
459
|
function validateChannel(
|
|
393
460
|
name: 'metallicChannel' | 'roughnessChannel',
|
|
394
461
|
value: number | undefined,
|
|
@@ -413,7 +480,7 @@ function validateFiniteRange(
|
|
|
413
480
|
}
|
|
414
481
|
}
|
|
415
482
|
|
|
416
|
-
function
|
|
483
|
+
function standardDefault(opts: DefaultStandardOptions): MaterialAsset {
|
|
417
484
|
const occlusionStrength = opts.occlusionStrength ?? 1;
|
|
418
485
|
if (occlusionStrength < 0 || occlusionStrength > 1) {
|
|
419
486
|
throw new Error(
|
|
@@ -516,16 +583,18 @@ function standard(opts: StandardOpts): MaterialAsset {
|
|
|
516
583
|
for (const name of Object.keys(values)) {
|
|
517
584
|
if (!declaredNames.has(name)) delete values[name];
|
|
518
585
|
}
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
586
|
+
const layerPlan = deriveStandardLayerPlan(parameters);
|
|
587
|
+
const projected = projectStandardSurfacePasses({
|
|
588
|
+
surfaceModule: DEFAULT_STANDARD_SURFACE_MODULE,
|
|
589
|
+
values,
|
|
590
|
+
...(forwardRenderState === undefined ? {} : { renderState: forwardRenderState }),
|
|
591
|
+
...(opts.castShadow === undefined ? {} : { castShadow: opts.castShadow }),
|
|
592
|
+
...(opts.queue === undefined ? {} : { queue: opts.queue }),
|
|
593
|
+
layerPlan,
|
|
594
|
+
});
|
|
595
|
+
const passes = (
|
|
596
|
+
transmission > 0 ? projected.filter((entry) => entry.name !== 'deferred') : projected
|
|
597
|
+
) as [MaterialPass, ...MaterialPass[]];
|
|
529
598
|
return {
|
|
530
599
|
kind: 'material',
|
|
531
600
|
colorSpace: 'linear',
|
|
@@ -535,6 +604,32 @@ function standard(opts: StandardOpts): MaterialAsset {
|
|
|
535
604
|
};
|
|
536
605
|
}
|
|
537
606
|
|
|
607
|
+
function standardCustom(opts: CustomStandardSurfaceOptions): MaterialAsset {
|
|
608
|
+
const layerPlan = deriveStandardLayerPlan(
|
|
609
|
+
opts.parameters.filter((parameter) => parameter.optional !== true),
|
|
610
|
+
);
|
|
611
|
+
return {
|
|
612
|
+
kind: 'material',
|
|
613
|
+
...(opts.colorSpace === undefined || opts.colorSpace === 'srgb'
|
|
614
|
+
? {}
|
|
615
|
+
: { colorSpace: opts.colorSpace }),
|
|
616
|
+
passes: projectStandardSurfacePasses({
|
|
617
|
+
surfaceModule: opts.surfaceModule,
|
|
618
|
+
values: opts.values,
|
|
619
|
+
...(opts.renderState === undefined ? {} : { renderState: opts.renderState }),
|
|
620
|
+
...(opts.castShadow === undefined ? {} : { castShadow: opts.castShadow }),
|
|
621
|
+
...(opts.queue === undefined ? {} : { queue: opts.queue }),
|
|
622
|
+
layerPlan,
|
|
623
|
+
}),
|
|
624
|
+
parameters: opts.parameters,
|
|
625
|
+
values: opts.values,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function standard(options: StandardOptions): MaterialAsset {
|
|
630
|
+
return options.surfaceModule === undefined ? standardDefault(options) : standardCustom(options);
|
|
631
|
+
}
|
|
632
|
+
|
|
538
633
|
export const Materials = { unlit, standard, srgb } as const;
|
|
539
634
|
|
|
540
635
|
export { SPRITE_MODULE };
|
package/src/pipeline-builder.ts
CHANGED
|
@@ -277,7 +277,13 @@ export function buildPipelineForMaterialShader(
|
|
|
277
277
|
const vsEntry = vertexEntry ?? (passKind === 'temporal' ? 'vs_temporal' : 'vs_main');
|
|
278
278
|
const fsEntry =
|
|
279
279
|
fragmentEntry ??
|
|
280
|
-
(passKind === 'temporal'
|
|
280
|
+
(passKind === 'temporal'
|
|
281
|
+
? 'fs_temporal'
|
|
282
|
+
: passKind === 'deferred'
|
|
283
|
+
? 'fs_gbuffer'
|
|
284
|
+
: passKind === 'shadow-caster'
|
|
285
|
+
? 'fs_shadow'
|
|
286
|
+
: 'fs_main');
|
|
281
287
|
|
|
282
288
|
let source = entry.source;
|
|
283
289
|
if (defines !== undefined && Object.keys(defines).length > 0) {
|
|
@@ -345,15 +351,13 @@ export function buildPipelineForMaterialShader(
|
|
|
345
351
|
vertex: {
|
|
346
352
|
module: shaderModule,
|
|
347
353
|
entryPoint: vsEntry,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
],
|
|
354
|
+
// Surface-backed Standard shadow entries evaluate the same material
|
|
355
|
+
// surface as Forward/Deferred, so their vertex stage consumes normal,
|
|
356
|
+
// UV, and tangent inputs in addition to position. Keep the complete
|
|
357
|
+
// derived mesh layout here; extra attributes remain legal for legacy
|
|
358
|
+
// position-only custom casters, while trimming to position would make
|
|
359
|
+
// the built-in Surface shadow shader fail WebGPU validation.
|
|
360
|
+
buffers: [...ctx.vertexBuffers],
|
|
357
361
|
},
|
|
358
362
|
// Vertex-only depth pass writes `fragment: undefined` (GPU derives
|
|
359
363
|
// depth from gl_Position.z). A custom ShadowCaster shader with a
|
package/src/pipeline-spec.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
deriveVertexBufferLayout,
|
|
22
|
+
deriveVertexBufferLayoutFromProjection,
|
|
22
23
|
deriveVertexLayoutProjection,
|
|
23
24
|
type VertexLayoutProjection,
|
|
24
25
|
} from '@forgeax/engine-geometry';
|
|
@@ -407,20 +408,11 @@ export function buildPipelineDescriptor(
|
|
|
407
408
|
? { shaderUvSetCount: geometry.shaderUvSetCount }
|
|
408
409
|
: {}),
|
|
409
410
|
})
|
|
410
|
-
: geometry.vertexLayoutProjection
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
{
|
|
414
|
-
|
|
415
|
-
attributes: geometry.vertexLayoutProjection.attributes.map(
|
|
416
|
-
({ shaderLocation, offset, format }) => ({
|
|
417
|
-
shaderLocation,
|
|
418
|
-
offset,
|
|
419
|
-
format,
|
|
420
|
-
}),
|
|
421
|
-
),
|
|
422
|
-
},
|
|
423
|
-
];
|
|
411
|
+
: deriveVertexBufferLayoutFromProjection(geometry.vertexLayoutProjection, {
|
|
412
|
+
...(geometry.shaderUvSetCount !== undefined
|
|
413
|
+
? { shaderUvSetCount: geometry.shaderUvSetCount }
|
|
414
|
+
: {}),
|
|
415
|
+
});
|
|
424
416
|
|
|
425
417
|
const descriptor: Record<string, unknown> = {
|
|
426
418
|
vertex: {
|
|
@@ -18,6 +18,7 @@ import type { DirectionalShadowQuality } from '../components/directional-shadow-
|
|
|
18
18
|
import type { EnvironmentGeneration } from '../environment/generation';
|
|
19
19
|
import type { EnvironmentLifecycle } from '../environment/lifecycle';
|
|
20
20
|
import type { GpuBuffer } from '../gpu-resource';
|
|
21
|
+
import type { InstanceCollectionId, InstanceResidency } from '../instances';
|
|
21
22
|
import type { IblBindingInspection } from '../mesh-material-bindings';
|
|
22
23
|
import type { StandardLightingInspection } from '../pipeline/standard-lighting/inspection';
|
|
23
24
|
import type { PointShadowInspection } from '../point-shadow-inspection';
|
|
@@ -52,6 +53,17 @@ export function worldEntityKey(worldId: number, entityKey: number): number {
|
|
|
52
53
|
return worldId * 4294967296 + entityKey; // 2^32 = 4294967296
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/** Per-renderer projection identity, with entity keys for sprite/fold snapshots. */
|
|
57
|
+
export function instanceCollectionCacheKey(
|
|
58
|
+
worldId: number,
|
|
59
|
+
instance: { readonly cacheKey: number; readonly collectionId?: number },
|
|
60
|
+
): number {
|
|
61
|
+
// Keep collection entries in a disjoint numeric namespace from the shared
|
|
62
|
+
// legacy sprite/fold cache. Fold keys occupy -1..-(2^32), while ordinary
|
|
63
|
+
// world/entity keys are non-negative; this range starts below both.
|
|
64
|
+
return -(0x1_0000_0000 + worldEntityKey(worldId, instance.collectionId ?? instance.cacheKey));
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
/**
|
|
56
68
|
* Cross-frame cache entry for a material bind group whose source material and
|
|
57
69
|
* all resolved GPU resources are stable. The material snapshot identity is
|
|
@@ -289,8 +301,9 @@ export function makeZeroCameraFallbackSnapshot(): CameraSnapshot {
|
|
|
289
301
|
* Owned by the `createRenderSystem` closure; advanced once per
|
|
290
302
|
* `recordFrame` invocation. The `instanceBuffers` map holds the per-entity
|
|
291
303
|
* GPU storage buffers for Instances-bearing entities (cache key = the
|
|
292
|
-
* packed Entity u32
|
|
293
|
-
* are recreated when the `archVersion` bumps or
|
|
304
|
+
* renderer-owned collection id, with the packed Entity u32 retained only for
|
|
305
|
+
* legacy snapshots); entries are recreated when the `archVersion` bumps or
|
|
306
|
+
* `byteLength` changes.
|
|
294
307
|
*
|
|
295
308
|
* feat-20260518-pbr-direct-lighting-mvp M3 / w14 (AC-17 a): the
|
|
296
309
|
* `warnedZeroLightStandard` flag latches the first-frame warning that
|
|
@@ -390,6 +403,10 @@ export interface RenderFrameState {
|
|
|
390
403
|
currentDirectionalShadowView: TextureView | null;
|
|
391
404
|
currentSpotShadowView: TextureView | null;
|
|
392
405
|
readonly instanceBuffers: Map<number, InstanceBufferCacheEntry>;
|
|
406
|
+
/** Renderer-owned collection owner used for record-stage residency facts. */
|
|
407
|
+
readonly instanceResidency?: Map<InstanceCollectionId, InstanceResidency>;
|
|
408
|
+
/** Stable buffers for renderer-owned collections split by device cap. */
|
|
409
|
+
readonly instanceBufferChunks?: Map<string, InstanceBufferCacheEntry>;
|
|
393
410
|
/** Per-object ProbeBlendRecord buffers; upload is one atomic 160B write. */
|
|
394
411
|
readonly probeBlendBuffers: Map<number, ProbeBlendBufferCacheEntry>;
|
|
395
412
|
/** One renderer-owned record array; each object selects its 256B-aligned lane. */
|
package/src/record/frame.ts
CHANGED
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
type DispatchCounts,
|
|
87
87
|
type FrameObservationSource,
|
|
88
88
|
getTextureIdentity,
|
|
89
|
+
instanceCollectionCacheKey,
|
|
89
90
|
makeZeroCameraFallbackSnapshot,
|
|
90
91
|
type ReflectionFallbackReadbackRequest,
|
|
91
92
|
type RenderFrameState,
|
|
@@ -2993,8 +2994,14 @@ function cleanPerFrameCaches(
|
|
|
2993
2994
|
// composites matching the write-side keys of #1-#3 — cross-world false eviction
|
|
2994
2995
|
// is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
|
|
2995
2996
|
const validatedEntityKeys = new Set<number>(retainedEntityKeys);
|
|
2997
|
+
const validatedInstanceKeys = new Set<number>(retainedEntityKeys);
|
|
2996
2998
|
for (const v of validated) {
|
|
2997
|
-
|
|
2999
|
+
const entityKey = worldEntityKey(v.source.worldId, v.source.entityKey);
|
|
3000
|
+
validatedEntityKeys.add(entityKey);
|
|
3001
|
+
validatedInstanceKeys.add(entityKey);
|
|
3002
|
+
if (v.source.instances !== undefined) {
|
|
3003
|
+
validatedInstanceKeys.add(instanceCollectionCacheKey(v.source.worldId, v.source.instances));
|
|
3004
|
+
}
|
|
2998
3005
|
}
|
|
2999
3006
|
|
|
3000
3007
|
// Clean per-entity material BG cache: drop outer-Map entries whose
|
|
@@ -3018,7 +3025,7 @@ function cleanPerFrameCaches(
|
|
|
3018
3025
|
// material-handle-based and cross-world collision is semantically
|
|
3019
3026
|
// correct (same material renders in same fold bucket).
|
|
3020
3027
|
for (const [key, entry] of frameState.instanceBuffers.entries()) {
|
|
3021
|
-
if (!
|
|
3028
|
+
if (!validatedInstanceKeys.has(key)) {
|
|
3022
3029
|
if (!entry.buffer.isDestroyed) {
|
|
3023
3030
|
const r = entry.buffer.destroy();
|
|
3024
3031
|
if (!r.ok) internals.errorRegistry.fire(r.error);
|
|
@@ -3027,6 +3034,28 @@ function cleanPerFrameCaches(
|
|
|
3027
3034
|
}
|
|
3028
3035
|
}
|
|
3029
3036
|
|
|
3037
|
+
if (frameState.instanceResidency !== undefined) {
|
|
3038
|
+
const liveCollections = new Set(validated.map((entry) => entry.source.instances?.collectionId));
|
|
3039
|
+
for (const id of frameState.instanceResidency.keys()) {
|
|
3040
|
+
if (!liveCollections.has(id)) frameState.instanceResidency.delete(id);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
|
|
3045
|
+
// the authoring API, but retain the numeric owner prefix for eviction.
|
|
3046
|
+
if (frameState.instanceBufferChunks !== undefined) {
|
|
3047
|
+
for (const [key, entry] of frameState.instanceBufferChunks.entries()) {
|
|
3048
|
+
const separator = key.indexOf(':');
|
|
3049
|
+
const ownerKey = separator < 0 ? Number.NaN : Number(key.slice(0, separator));
|
|
3050
|
+
if (validatedInstanceKeys.has(ownerKey)) continue;
|
|
3051
|
+
if (!entry.buffer.isDestroyed) {
|
|
3052
|
+
const r = entry.buffer.destroy();
|
|
3053
|
+
if (!r.ok) internals.errorRegistry.fire(r.error);
|
|
3054
|
+
}
|
|
3055
|
+
frameState.instanceBufferChunks.delete(key);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3030
3059
|
if (frameState.morphBuffers !== undefined) {
|
|
3031
3060
|
for (const [key, entry] of frameState.morphBuffers.entries()) {
|
|
3032
3061
|
if (validatedEntityKeys.has(key)) continue;
|