@forgeax/engine-render 0.1.35 → 0.1.36
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 +17 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
- package/dist/{chunk-MXTWKZO4.mjs → chunk-2OZAS3WU.mjs} +225 -4
- package/dist/chunk-2OZAS3WU.mjs.map +1 -0
- package/dist/{chunk-G6HDNGY7.mjs → chunk-BZQ4CDRF.mjs} +24 -230
- package/dist/chunk-BZQ4CDRF.mjs.map +1 -0
- package/dist/{chunk-LNIUIKQ7.mjs → chunk-CJOV2XLO.mjs} +77 -19
- package/dist/chunk-CJOV2XLO.mjs.map +1 -0
- package/dist/{chunk-3XUBWRY2.mjs → chunk-D6WSBXJ2.mjs} +4 -4
- package/dist/{chunk-3XUBWRY2.mjs.map → chunk-D6WSBXJ2.mjs.map} +1 -1
- package/dist/{chunk-LM5IE7GF.mjs → chunk-DRF6OULX.mjs} +3 -3
- package/dist/{chunk-LM5IE7GF.mjs.map → chunk-DRF6OULX.mjs.map} +1 -1
- package/dist/{chunk-SGPUJW3F.mjs → chunk-OB5WAFYH.mjs} +3 -3
- package/dist/{chunk-SGPUJW3F.mjs.map → chunk-OB5WAFYH.mjs.map} +1 -1
- package/dist/{chunk-MZ4C7EHF.mjs → chunk-OEYKKMXG.mjs} +5 -5
- package/dist/{chunk-MZ4C7EHF.mjs.map → chunk-OEYKKMXG.mjs.map} +1 -1
- package/dist/construct-renderer.mjs +130 -71
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/features/particle-mesh-layout.d.ts +19 -0
- package/dist/features/particle-mesh-layout.d.ts.map +1 -0
- package/dist/features/types.d.ts +5 -1
- package/dist/features/types.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +1 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/shadow-views.d.ts +1 -1
- package/dist/gpu-driven/shadow-views.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +1 -1
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/index.mjs +48 -36
- package/dist/index.mjs.map +1 -1
- package/dist/internal.mjs +5 -5
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/publication/publisher.d.ts +1 -0
- package/dist/publication/publisher.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/prepared-material-bindings.d.ts +1 -2
- package/dist/record/prepared-material-bindings.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -0
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +2 -2
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +1 -8
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +4 -4
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +26 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +18 -1
- package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +23 -0
- package/src/__tests__/material-vertex-texture.browser.test.ts +7 -0
- package/src/__tests__/material-vertex-texture.dawn.test.ts +7 -0
- package/src/__tests__/material-vertex-texture.fixture.ts +31 -0
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +5 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +106 -4
- package/src/__tests__/runtime-material-program.integration.test.ts +79 -0
- package/src/__tests__/vertex-layouts-ssot.unit.test.ts +23 -3
- package/src/assembly/webgpu-renderer.ts +12 -9
- package/src/assembly/webgpu-vertex-layouts.ts +2 -9
- package/src/device/device-scope.ts +3 -0
- package/src/features/particle-mesh-layout.ts +25 -0
- package/src/features/types.ts +5 -1
- package/src/gpu-driven/production-raster.ts +9 -3
- package/src/gpu-driven/shadow-views.ts +27 -2
- package/src/gpu-driven/view-gpu.ts +5 -0
- package/src/pbr-pipeline.ts +4 -1
- package/src/publication/__tests__/features.integration.test.ts +51 -0
- package/src/publication/__tests__/publication.integration.test.ts +45 -0
- package/src/publication/publisher.ts +56 -29
- package/src/record/__tests__/prepared-runtime-material.integration.test.ts +134 -0
- package/src/record/frame.ts +45 -6
- package/src/record/prepared-material-bindings.ts +11 -37
- package/src/record/render-context.ts +2 -0
- package/src/record/shadow-pass.ts +65 -4
- package/src/record/typed-frame-graph.ts +5 -1
- package/src/recovery/render-system-candidate.ts +1 -9
- package/src/render-system-extract-tail.ts +15 -6
- package/src/render-system-extract.ts +9 -2
- package/src/render-system.ts +0 -1
- package/src/typed-shadow-passes.ts +36 -5
- package/dist/chunk-G6HDNGY7.mjs.map +0 -1
- package/dist/chunk-LNIUIKQ7.mjs.map +0 -1
- package/dist/chunk-MXTWKZO4.mjs.map +0 -1
|
@@ -982,6 +982,7 @@ export class GpuDrivenView {
|
|
|
982
982
|
labelPrefix = 'gpu-driven',
|
|
983
983
|
includeCompute = true,
|
|
984
984
|
surfaceSubmissionObservation?: () => SurfaceSubmissionCandidate | undefined,
|
|
985
|
+
executeIf?: (frame: FrameCtx) => boolean,
|
|
985
986
|
): Result<GpuDrivenViewGraphResources, RenderGraphError> {
|
|
986
987
|
const buffers = this.buffers;
|
|
987
988
|
const bindGroup = this.bindGroup;
|
|
@@ -1099,6 +1100,7 @@ export class GpuDrivenView {
|
|
|
1099
1100
|
{ resource: view.value, usage: 'uniform-read' },
|
|
1100
1101
|
{ resource: counters.value, usage: 'storage-read-write' },
|
|
1101
1102
|
],
|
|
1103
|
+
...(executeIf === undefined ? {} : { executeIf }),
|
|
1102
1104
|
encode: ({ pass }) => {
|
|
1103
1105
|
pass.setPipeline(this.resetPipeline);
|
|
1104
1106
|
pass.setBindGroup(0, bindGroup);
|
|
@@ -1116,6 +1118,7 @@ export class GpuDrivenView {
|
|
|
1116
1118
|
{ resource: counters.value, usage: 'storage-read-write' },
|
|
1117
1119
|
{ resource: visible.value, usage: 'storage-read-write' },
|
|
1118
1120
|
],
|
|
1121
|
+
...(executeIf === undefined ? {} : { executeIf }),
|
|
1119
1122
|
encode: ({ pass }) => {
|
|
1120
1123
|
pass.setPipeline(this.cullPipeline);
|
|
1121
1124
|
pass.setBindGroup(0, bindGroup);
|
|
@@ -1130,6 +1133,7 @@ export class GpuDrivenView {
|
|
|
1130
1133
|
{ resource: counters.value, usage: 'storage-read' },
|
|
1131
1134
|
{ resource: indirect.value, usage: 'storage-read-write' },
|
|
1132
1135
|
],
|
|
1136
|
+
...(executeIf === undefined ? {} : { executeIf }),
|
|
1133
1137
|
encode: ({ pass }) => {
|
|
1134
1138
|
pass.setPipeline(this.finalizePipeline);
|
|
1135
1139
|
pass.setBindGroup(0, bindGroup);
|
|
@@ -1148,6 +1152,7 @@ export class GpuDrivenView {
|
|
|
1148
1152
|
{ resource: indirect.value, usage: 'copy-src' },
|
|
1149
1153
|
{ resource: lodReadback.value, usage: 'copy-dst' },
|
|
1150
1154
|
],
|
|
1155
|
+
...(executeIf === undefined ? {} : { executeIf }),
|
|
1151
1156
|
encode: ({ encoder, resources }) => {
|
|
1152
1157
|
// A cached graph may run while an observer is awaiting mapAsync on the
|
|
1153
1158
|
// previous copy. WebGPU forbids submitting a copy into a
|
package/src/pbr-pipeline.ts
CHANGED
|
@@ -158,7 +158,10 @@ export function buildPbrMaterialUserRegionEntries(
|
|
|
158
158
|
let entries = derived.bglEntries.map(
|
|
159
159
|
(entry): GPUBindGroupLayoutEntry => ({
|
|
160
160
|
binding: entry.binding,
|
|
161
|
-
visibility:
|
|
161
|
+
visibility:
|
|
162
|
+
entry.texture !== undefined || entry.sampler !== undefined
|
|
163
|
+
? GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT
|
|
164
|
+
: entry.visibility,
|
|
162
165
|
...(entry.buffer === undefined ? {} : { buffer: entry.buffer }),
|
|
163
166
|
...(entry.sampler === undefined ? {} : { sampler: entry.sampler }),
|
|
164
167
|
...(entry.texture === undefined ? {} : { texture: entry.texture }),
|
|
@@ -120,3 +120,54 @@ it('publishes LOD GUID dependencies and rejects a missing lower-detail asset bef
|
|
|
120
120
|
candidate.discard();
|
|
121
121
|
publisher.dispose();
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
it('keeps two sealed publications and their feedback independent of later source mutation', async () => {
|
|
125
|
+
const world = new World();
|
|
126
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
127
|
+
let value = 1;
|
|
128
|
+
const consumed: unknown[] = [];
|
|
129
|
+
const feature: RenderFeature<unknown> = {
|
|
130
|
+
identity: 'sealed-events',
|
|
131
|
+
extract: () => ok({ value }),
|
|
132
|
+
plan: () => ok({ resources: [], passes: [] }),
|
|
133
|
+
onSourceFrameSubmitted: (data) => consumed.push(data),
|
|
134
|
+
};
|
|
135
|
+
const publisher = createRenderPublisher(
|
|
136
|
+
world,
|
|
137
|
+
assets,
|
|
138
|
+
{ source: 'sealed', epoch: 1 },
|
|
139
|
+
undefined,
|
|
140
|
+
[feature],
|
|
141
|
+
);
|
|
142
|
+
const seal = () => {
|
|
143
|
+
const candidate = publisher.prepare(value).unwrap();
|
|
144
|
+
const packet = structuredClone(candidate.packet, {
|
|
145
|
+
transfer: renderPublicationTransfers(candidate.packet),
|
|
146
|
+
});
|
|
147
|
+
candidate.accept();
|
|
148
|
+
return packet;
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
const first = seal();
|
|
152
|
+
value = 2;
|
|
153
|
+
const second = seal();
|
|
154
|
+
await Promise.resolve();
|
|
155
|
+
value = 99;
|
|
156
|
+
expect(second.features[0]?.data).toEqual({ value: 2 });
|
|
157
|
+
expect(publisher.prepare(3).ok).toBe(false);
|
|
158
|
+
expect(publisher.recycle(2, renderPublicationTransfers(second)).ok).toBe(false);
|
|
159
|
+
publisher.acknowledgeFeatures(1, [{ identity: feature.identity, feedback: null }]).unwrap();
|
|
160
|
+
publisher.recycle(1, renderPublicationTransfers(first)).unwrap();
|
|
161
|
+
expect(
|
|
162
|
+
publisher.acknowledgeFeatures(1, [{ identity: feature.identity, feedback: null }]).ok,
|
|
163
|
+
).toBe(false);
|
|
164
|
+
publisher.acknowledgeFeatures(2, [{ identity: feature.identity, feedback: null }]).unwrap();
|
|
165
|
+
expect(consumed).toEqual([{ value: 1 }, { value: 2 }]);
|
|
166
|
+
publisher.recycle(2, renderPublicationTransfers(second)).unwrap();
|
|
167
|
+
const third = seal();
|
|
168
|
+
expect(third.base).toBe(2);
|
|
169
|
+
expect(third.features[0]?.data).toEqual({ value: 99 });
|
|
170
|
+
} finally {
|
|
171
|
+
publisher.dispose();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
@@ -368,3 +368,48 @@ it('publishes detached skin poses and follows joint changes across skipped sourc
|
|
|
368
368
|
).toBeUndefined();
|
|
369
369
|
f.publisher.dispose();
|
|
370
370
|
});
|
|
371
|
+
|
|
372
|
+
it('renders a one-frame identity before its sealed removal and reuses two buffer slots', () => {
|
|
373
|
+
const f = fixture();
|
|
374
|
+
const seal = () => {
|
|
375
|
+
const candidate = f.publisher.prepare(0).unwrap();
|
|
376
|
+
const packet = structuredClone(candidate.packet, {
|
|
377
|
+
transfer: renderPublicationTransfers(candidate.packet),
|
|
378
|
+
});
|
|
379
|
+
candidate.accept();
|
|
380
|
+
return packet;
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
const first = seal();
|
|
384
|
+
f.world.despawn(f.first).unwrap();
|
|
385
|
+
const replacement = f.spawn();
|
|
386
|
+
f.world.update(0).unwrap();
|
|
387
|
+
const second = seal();
|
|
388
|
+
// More source mutations cannot edit either already-transferred publication.
|
|
389
|
+
f.world.despawn(replacement).unwrap();
|
|
390
|
+
f.world.update(0).unwrap();
|
|
391
|
+
const firstFrame = f.scene.consumePublication(f.receiver.accept(first).unwrap());
|
|
392
|
+
expect(firstFrame.renderables.map((row) => row.entityKey)).toContain(f.first);
|
|
393
|
+
f.publisher.recycle(first.revision, renderPublicationTransfers(first)).unwrap();
|
|
394
|
+
const secondFrame = f.scene.consumePublication(f.receiver.accept(second).unwrap());
|
|
395
|
+
expect(secondFrame.renderables.map((row) => row.entityKey)).toContain(replacement);
|
|
396
|
+
expect(secondFrame.renderables.map((row) => row.entityKey)).not.toContain(f.first);
|
|
397
|
+
f.publisher.recycle(second.revision, renderPublicationTransfers(second)).unwrap();
|
|
398
|
+
// Warm both slots for equivalent changed-transform frames, then drain in pairs.
|
|
399
|
+
let warmAllocations: number | undefined;
|
|
400
|
+
for (let index = 0; index < 4; index++) {
|
|
401
|
+
const packets = [1, 2].map((x) => {
|
|
402
|
+
f.world.set(f.second, Transform, { pos: [x, index, 0] }).unwrap();
|
|
403
|
+
f.world.update(0).unwrap();
|
|
404
|
+
return seal();
|
|
405
|
+
});
|
|
406
|
+
const allocations = f.publisher.inspect().allocations;
|
|
407
|
+
for (const packet of packets)
|
|
408
|
+
f.publisher.recycle(packet.revision, renderPublicationTransfers(packet)).unwrap();
|
|
409
|
+
warmAllocations ??= allocations;
|
|
410
|
+
expect(allocations).toBe(warmAllocations);
|
|
411
|
+
}
|
|
412
|
+
} finally {
|
|
413
|
+
f.publisher.dispose();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
@@ -68,9 +68,15 @@ export function createRenderPublisher(
|
|
|
68
68
|
const consumers = new Map<Handle<string, 'shared'>, Set<number>>();
|
|
69
69
|
const pending = new Set<number>();
|
|
70
70
|
let videoConsumers = new Map<number, readonly number[]>();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
// Two slots: the consuming frame and one sealed successor.
|
|
72
|
+
const flights = new Map<
|
|
73
|
+
number,
|
|
74
|
+
{
|
|
75
|
+
features: RenderPublication['features'];
|
|
76
|
+
owners: readonly RenderFeature<unknown>[];
|
|
77
|
+
submitted: boolean;
|
|
78
|
+
}
|
|
79
|
+
>();
|
|
74
80
|
const jointsByEntity = new Map<number, readonly number[]>();
|
|
75
81
|
const skinConsumers = new Map<number, Set<number>>();
|
|
76
82
|
let frameDependencies = new Set<Handle<string, 'shared'>>();
|
|
@@ -79,9 +85,9 @@ export function createRenderPublisher(
|
|
|
79
85
|
disposed = false,
|
|
80
86
|
active = false;
|
|
81
87
|
let lastScannedRows = 0,
|
|
82
|
-
inFlight = false,
|
|
83
88
|
allocations = 0;
|
|
84
89
|
let storage = [new ArrayBuffer(0), new ArrayBuffer(0), new ArrayBuffer(0), new ArrayBuffer(0)];
|
|
90
|
+
const spareStorage: ArrayBuffer[][] = [];
|
|
85
91
|
const buffer = (index: number, bytes: number): ArrayBuffer => {
|
|
86
92
|
let value = storage[index] as ArrayBuffer;
|
|
87
93
|
if (value.byteLength < bytes) {
|
|
@@ -104,7 +110,8 @@ export function createRenderPublisher(
|
|
|
104
110
|
members: members.size,
|
|
105
111
|
assetCount: consumers.size,
|
|
106
112
|
scannedRows: lastScannedRows,
|
|
107
|
-
inFlight,
|
|
113
|
+
inFlight: flights.size > 0,
|
|
114
|
+
inFlightCount: flights.size,
|
|
108
115
|
allocations,
|
|
109
116
|
}),
|
|
110
117
|
prepare(
|
|
@@ -112,7 +119,8 @@ export function createRenderPublisher(
|
|
|
112
119
|
temporalReset = false,
|
|
113
120
|
): Result<RenderPublicationCandidate, RenderPublicationError> {
|
|
114
121
|
if (disposed) return fail('disposed', 'publisher');
|
|
115
|
-
if (active ||
|
|
122
|
+
if (active || flights.size === 2)
|
|
123
|
+
return fail('in-flight', 'both publication slots are occupied');
|
|
116
124
|
const baseline = revision === 0;
|
|
117
125
|
const featureSources = [...features];
|
|
118
126
|
const batch = source.projection.read();
|
|
@@ -330,7 +338,18 @@ export function createRenderPublisher(
|
|
|
330
338
|
if (seenGuids.has(key)) continue;
|
|
331
339
|
seenGuids.add(key);
|
|
332
340
|
const asset = assets.lookup(key);
|
|
333
|
-
|
|
341
|
+
// Dynamic asset content is keyed by the canonical World handle. Feature
|
|
342
|
+
// dependencies must share the same identity as ordinary scene consumers.
|
|
343
|
+
const handle = internSharedRefFromGuid(
|
|
344
|
+
world,
|
|
345
|
+
assets,
|
|
346
|
+
key,
|
|
347
|
+
asset?.kind === 'material'
|
|
348
|
+
? 'MaterialAsset'
|
|
349
|
+
: asset?.kind === 'mesh'
|
|
350
|
+
? 'MeshAsset'
|
|
351
|
+
: 'RenderFeatureAsset',
|
|
352
|
+
);
|
|
334
353
|
if (asset === undefined || handle === undefined)
|
|
335
354
|
throw new Error(`Missing feature asset ${guid}`);
|
|
336
355
|
nextFrameDependencies.add(handle);
|
|
@@ -455,7 +474,10 @@ export function createRenderPublisher(
|
|
|
455
474
|
transformEntities,
|
|
456
475
|
transforms: matrices,
|
|
457
476
|
assets: [...assetRows].map(([handle, value]) => {
|
|
458
|
-
|
|
477
|
+
// Runtime content is a projection, so GUID identity stays with the
|
|
478
|
+
// underlying catalogued payload rather than the projected object.
|
|
479
|
+
const source = world.sharedRefs.resolve<string, Asset>(handle);
|
|
480
|
+
const guid = assets.guidOf(source.ok ? source.value : value);
|
|
459
481
|
return { handle, value, ...(guid === undefined ? {} : { guid }) };
|
|
460
482
|
}),
|
|
461
483
|
retiredAssets: [...retired],
|
|
@@ -474,13 +496,20 @@ export function createRenderPublisher(
|
|
|
474
496
|
for (const key of programs.keys()) publishedPrograms.add(key);
|
|
475
497
|
closed = true;
|
|
476
498
|
active = false;
|
|
477
|
-
inFlight = true;
|
|
478
499
|
revision++;
|
|
500
|
+
flights.set(revision, {
|
|
501
|
+
features: featureRows,
|
|
502
|
+
owners: featureSources,
|
|
503
|
+
submitted: false,
|
|
504
|
+
});
|
|
505
|
+
storage = spareStorage.pop() ?? [
|
|
506
|
+
new ArrayBuffer(0),
|
|
507
|
+
new ArrayBuffer(0),
|
|
508
|
+
new ArrayBuffer(0),
|
|
509
|
+
new ArrayBuffer(0),
|
|
510
|
+
];
|
|
479
511
|
for (const row of videoFrames) row.frame.close();
|
|
480
512
|
videoConsumers = nextVideoConsumers;
|
|
481
|
-
pendingFeatures = featureRows;
|
|
482
|
-
pendingFeatureOwners = featureSources;
|
|
483
|
-
acknowledgedFeatures.clear();
|
|
484
513
|
frameDependencies = nextFrameDependencies;
|
|
485
514
|
catalogEpoch = assets.catalogEpoch;
|
|
486
515
|
for (const entity of removedConsumers) {
|
|
@@ -560,26 +589,23 @@ export function createRenderPublisher(
|
|
|
560
589
|
receipts: readonly { readonly identity: string; readonly feedback: unknown }[],
|
|
561
590
|
): Result<void, RenderPublicationError> {
|
|
562
591
|
if (disposed) return fail('disposed', 'publisher');
|
|
563
|
-
|
|
592
|
+
const flight = flights.get(acceptedRevision);
|
|
593
|
+
if (flight === undefined || flight.submitted)
|
|
564
594
|
return fail('revision', 'feature acknowledgment');
|
|
565
595
|
const identities = new Set<string>();
|
|
566
596
|
for (const receipt of receipts) {
|
|
567
|
-
if (identities.has(receipt.identity)
|
|
597
|
+
if (identities.has(receipt.identity))
|
|
568
598
|
return fail('revision', 'duplicate feature acknowledgment');
|
|
569
|
-
const row =
|
|
570
|
-
const feature =
|
|
571
|
-
(feature) => feature.identity === receipt.identity,
|
|
572
|
-
);
|
|
599
|
+
const row = flight.features.find((row) => row.identity === receipt.identity);
|
|
600
|
+
const feature = flight.owners.find((feature) => feature.identity === receipt.identity);
|
|
573
601
|
if (row === undefined || feature === undefined)
|
|
574
602
|
return fail('shape', 'unknown feature acknowledgment');
|
|
575
603
|
identities.add(receipt.identity);
|
|
576
604
|
}
|
|
605
|
+
flight.submitted = true;
|
|
577
606
|
for (const receipt of receipts) {
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
const feature = pendingFeatureOwners.find(
|
|
581
|
-
(feature) => feature.identity === receipt.identity,
|
|
582
|
-
);
|
|
607
|
+
const row = flight.features.find((row) => row.identity === receipt.identity);
|
|
608
|
+
const feature = flight.owners.find((feature) => feature.identity === receipt.identity);
|
|
583
609
|
feature?.onSourceFrameSubmitted?.(row?.data, receipt.feedback);
|
|
584
610
|
}
|
|
585
611
|
return ok(undefined);
|
|
@@ -589,12 +615,14 @@ export function createRenderPublisher(
|
|
|
589
615
|
buffers: readonly ArrayBuffer[],
|
|
590
616
|
): Result<void, RenderPublicationError> {
|
|
591
617
|
if (disposed) return fail('disposed', 'publisher');
|
|
592
|
-
if (
|
|
618
|
+
if (flights.keys().next().value !== acceptedRevision)
|
|
593
619
|
return fail('revision', 'returned storage revision');
|
|
594
620
|
if (buffers.length !== 4 || buffers.some((value) => !(value instanceof ArrayBuffer)))
|
|
595
621
|
return fail('shape', 'returned storage');
|
|
596
|
-
storage
|
|
597
|
-
|
|
622
|
+
if (active || storage.some((buffer) => buffer.byteLength > 0))
|
|
623
|
+
spareStorage.push([...buffers]);
|
|
624
|
+
else storage = [...buffers];
|
|
625
|
+
flights.delete(acceptedRevision);
|
|
598
626
|
return ok(undefined);
|
|
599
627
|
},
|
|
600
628
|
dispose() {
|
|
@@ -607,9 +635,8 @@ export function createRenderPublisher(
|
|
|
607
635
|
pending.clear();
|
|
608
636
|
pendingMeshContent.clear();
|
|
609
637
|
videoConsumers.clear();
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
acknowledgedFeatures.clear();
|
|
638
|
+
flights.clear();
|
|
639
|
+
spareStorage.length = 0;
|
|
613
640
|
frameDependencies.clear();
|
|
614
641
|
jointsByEntity.clear();
|
|
615
642
|
skinConsumers.clear();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { AssetRegistry, RuntimeMaterialValue } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import { World } from '@forgeax/engine-ecs';
|
|
3
|
+
import type { BindGroupLayout } from '@forgeax/engine-rhi';
|
|
4
|
+
import { ShaderRegistry } from '@forgeax/engine-shader';
|
|
5
|
+
import { type MaterialAsset, ok } from '@forgeax/engine-types';
|
|
6
|
+
import { expect, it } from 'vitest';
|
|
7
|
+
import { renderPublicationTransfers } from '../../publication/contract';
|
|
8
|
+
import { createRenderPublisher } from '../../publication/publisher';
|
|
9
|
+
import { RenderPublicationReceiver } from '../../publication/receiver';
|
|
10
|
+
import { preparedMaterialBindings } from '../prepared-material-bindings';
|
|
11
|
+
import type { RenderSystemInternals } from '../render-context';
|
|
12
|
+
|
|
13
|
+
it('uploads accepted values on World and published-reader paths without mutating the base', () => {
|
|
14
|
+
const world = new World();
|
|
15
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
16
|
+
const guid = '12345678-1234-1234-1234-123456789abc';
|
|
17
|
+
const base: MaterialAsset = { kind: 'material', values: { roughness: 0.8 } };
|
|
18
|
+
assets.catalog(guid, base);
|
|
19
|
+
const handle = world.internSharedRef('MaterialAsset', base);
|
|
20
|
+
const content = world
|
|
21
|
+
.spawn({
|
|
22
|
+
component: RuntimeMaterialValue,
|
|
23
|
+
data: { asset: handle, parameter: 'roughness', kind: 0, value: [0.25] },
|
|
24
|
+
})
|
|
25
|
+
.unwrap();
|
|
26
|
+
const uploads: number[] = [];
|
|
27
|
+
let destroys = 0;
|
|
28
|
+
const runtime = {
|
|
29
|
+
assets,
|
|
30
|
+
getParamSchema: () => [{ name: 'roughness', type: 'f32' }],
|
|
31
|
+
getPipelineState: () => ({
|
|
32
|
+
defaultSampler: {},
|
|
33
|
+
skylightFallback: {
|
|
34
|
+
irradianceView: {},
|
|
35
|
+
prefilterView: {},
|
|
36
|
+
brdfLutView: {},
|
|
37
|
+
sampler: {},
|
|
38
|
+
intensityBuffer: {},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
device: {
|
|
42
|
+
caps: { storageBuffer: false },
|
|
43
|
+
limits: { maxSampledTexturesPerShaderStage: 16 },
|
|
44
|
+
createBuffer: () => ok({}),
|
|
45
|
+
destroyBuffer: () => {
|
|
46
|
+
destroys++;
|
|
47
|
+
return ok(undefined);
|
|
48
|
+
},
|
|
49
|
+
createBindGroup: () => ok({}),
|
|
50
|
+
queue: {
|
|
51
|
+
writeBuffer: (_buffer: unknown, _offset: number, bytes: Uint8Array) => {
|
|
52
|
+
uploads.push(new Float32Array(bytes.buffer, bytes.byteOffset, 1)[0] ?? NaN);
|
|
53
|
+
return ok(undefined);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
} as unknown as RenderSystemInternals;
|
|
58
|
+
const identity = { source: 'material-values', epoch: 1 };
|
|
59
|
+
const publisher = createRenderPublisher(world, assets, identity, undefined, [
|
|
60
|
+
{
|
|
61
|
+
identity: 'material-consumer',
|
|
62
|
+
extract: () => ok({}),
|
|
63
|
+
assetDependencies: () => [guid],
|
|
64
|
+
plan: () => ok({ resources: [], passes: [] }),
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
const receiver = new RenderPublicationReceiver(identity);
|
|
68
|
+
const publish = () => {
|
|
69
|
+
const candidate = publisher.prepare(0).unwrap();
|
|
70
|
+
const packet = structuredClone(candidate.packet);
|
|
71
|
+
candidate.accept();
|
|
72
|
+
const resources = receiver.accept(packet).unwrap().resources;
|
|
73
|
+
publisher.recycle(packet.revision, renderPublicationTransfers(packet)).unwrap();
|
|
74
|
+
return resources;
|
|
75
|
+
};
|
|
76
|
+
const reader = publish();
|
|
77
|
+
for (const source of [world, reader]) {
|
|
78
|
+
const bindings = preparedMaterialBindings(
|
|
79
|
+
runtime,
|
|
80
|
+
[source],
|
|
81
|
+
'shader',
|
|
82
|
+
0,
|
|
83
|
+
guid,
|
|
84
|
+
{} as BindGroupLayout,
|
|
85
|
+
).unwrap();
|
|
86
|
+
if ('release' in bindings) bindings.release?.();
|
|
87
|
+
}
|
|
88
|
+
expect(uploads).toEqual([0.25, 0.25]);
|
|
89
|
+
expect(base.values?.roughness).toBe(0.8);
|
|
90
|
+
const other = new World();
|
|
91
|
+
const otherHandle = other.internSharedRef('MaterialAsset', base);
|
|
92
|
+
const otherContent = other
|
|
93
|
+
.spawn({
|
|
94
|
+
component: RuntimeMaterialValue,
|
|
95
|
+
data: {
|
|
96
|
+
asset: otherHandle,
|
|
97
|
+
parameter: 'roughness',
|
|
98
|
+
kind: 0,
|
|
99
|
+
value: [0.6],
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
.unwrap();
|
|
103
|
+
for (const [owner, expected] of [
|
|
104
|
+
[1, 0.6],
|
|
105
|
+
[0, 0.25],
|
|
106
|
+
] as const) {
|
|
107
|
+
const bindings = preparedMaterialBindings(
|
|
108
|
+
runtime,
|
|
109
|
+
[world, other],
|
|
110
|
+
'shader',
|
|
111
|
+
owner,
|
|
112
|
+
guid,
|
|
113
|
+
{} as BindGroupLayout,
|
|
114
|
+
).unwrap();
|
|
115
|
+
expect(uploads.at(-1)).toBeCloseTo(expected);
|
|
116
|
+
if ('release' in bindings) bindings.release?.();
|
|
117
|
+
}
|
|
118
|
+
other.despawn(otherContent).unwrap();
|
|
119
|
+
world.despawn(content).unwrap();
|
|
120
|
+
for (const source of [world, publish()]) {
|
|
121
|
+
const restored = preparedMaterialBindings(
|
|
122
|
+
runtime,
|
|
123
|
+
[source],
|
|
124
|
+
'shader',
|
|
125
|
+
0,
|
|
126
|
+
guid,
|
|
127
|
+
{} as BindGroupLayout,
|
|
128
|
+
).unwrap();
|
|
129
|
+
expect(uploads.at(-1)).toBeCloseTo(0.8);
|
|
130
|
+
if ('release' in restored) restored.release?.();
|
|
131
|
+
}
|
|
132
|
+
expect(destroys).toBe(6);
|
|
133
|
+
publisher.dispose();
|
|
134
|
+
});
|
package/src/record/frame.ts
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
resolveDepthOfFieldFrameParams,
|
|
59
59
|
} from '../features/depth-of-field/depth-of-field-params';
|
|
60
60
|
import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
|
|
61
|
-
import type
|
|
61
|
+
import { type ShadowViewIdentity, shadowViewIdentityKey } from '../gpu-driven/shadow-views';
|
|
62
62
|
import type { GpuDrivenLodSubmitIdentity } from '../gpu-driven/view-gpu';
|
|
63
63
|
import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
|
|
64
64
|
import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_MAP_READ } from '../gpu-usage';
|
|
@@ -484,8 +484,42 @@ function shadowPlanes(matrix: Float32Array): Float32Array {
|
|
|
484
484
|
return frustum.fromViewProjection(frustum.create(), matrix as never);
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
function
|
|
488
|
-
|
|
487
|
+
function shadowViewPlanesByView(lights: ExtractedLights): ReadonlyMap<string, Float32Array> {
|
|
488
|
+
const views = new Map<string, Float32Array>();
|
|
489
|
+
for (const [index, matrix] of (lights.lightViewProj ?? []).entries()) {
|
|
490
|
+
views.set(shadowViewIdentityKey({ kind: 'directional', index }), shadowPlanes(matrix));
|
|
491
|
+
}
|
|
492
|
+
for (const [index, point] of lights.pointShadow.entries()) {
|
|
493
|
+
for (let face = 0; face < 6; face += 1) {
|
|
494
|
+
views.set(
|
|
495
|
+
shadowViewIdentityKey({ kind: 'point', index, face }),
|
|
496
|
+
shadowPlanes(point.shadowMatrices.subarray(face * 16, face * 16 + 16)),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
let spotIndex = 0;
|
|
501
|
+
for (const spot of lights.spot) {
|
|
502
|
+
if (spot.lightViewProj === undefined) continue;
|
|
503
|
+
views.set(
|
|
504
|
+
shadowViewIdentityKey({ kind: 'spot', index: spotIndex }),
|
|
505
|
+
shadowPlanes(spot.lightViewProj),
|
|
506
|
+
);
|
|
507
|
+
spotIndex += 1;
|
|
508
|
+
}
|
|
509
|
+
return views;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function shadowViewToken(
|
|
513
|
+
matrix: Float32Array,
|
|
514
|
+
targetSize: number | undefined,
|
|
515
|
+
graphGeneration: number,
|
|
516
|
+
): string {
|
|
517
|
+
// A newly accepted graph imports a new resource projection and must refresh
|
|
518
|
+
// the shadow compute inputs once. The graph-generation component is a
|
|
519
|
+
// one-shot resource fence; it is deliberately excluded from the GPU
|
|
520
|
+
// producer's topology signature so that this refresh cannot rebuild the
|
|
521
|
+
// graph indefinitely.
|
|
522
|
+
return `${Array.from(matrix).join(',')}|target:${targetSize}|graph:${graphGeneration}`;
|
|
489
523
|
}
|
|
490
524
|
|
|
491
525
|
function halfFloatToNumber(bits: number): number {
|
|
@@ -1948,7 +1982,7 @@ export function recordFrame(
|
|
|
1948
1982
|
shadowViews.push({
|
|
1949
1983
|
identity: { kind: 'directional', index },
|
|
1950
1984
|
planes: shadowPlanes(matrix),
|
|
1951
|
-
cacheToken:
|
|
1985
|
+
cacheToken: shadowViewToken(matrix, graphShadowMapSize, frameState.graphGeneration),
|
|
1952
1986
|
});
|
|
1953
1987
|
}
|
|
1954
1988
|
for (const [index, point] of lights.pointShadow.entries()) {
|
|
@@ -1957,7 +1991,7 @@ export function recordFrame(
|
|
|
1957
1991
|
shadowViews.push({
|
|
1958
1992
|
identity: { kind: 'point', index, face },
|
|
1959
1993
|
planes: shadowPlanes(matrix),
|
|
1960
|
-
cacheToken:
|
|
1994
|
+
cacheToken: shadowViewToken(matrix, graphShadowMapSize, frameState.graphGeneration),
|
|
1961
1995
|
});
|
|
1962
1996
|
}
|
|
1963
1997
|
}
|
|
@@ -1967,7 +2001,11 @@ export function recordFrame(
|
|
|
1967
2001
|
shadowViews.push({
|
|
1968
2002
|
identity: { kind: 'spot', index: shadowSpotIndex },
|
|
1969
2003
|
planes: shadowPlanes(spot.lightViewProj),
|
|
1970
|
-
cacheToken:
|
|
2004
|
+
cacheToken: shadowViewToken(
|
|
2005
|
+
spot.lightViewProj,
|
|
2006
|
+
graphShadowMapSize,
|
|
2007
|
+
frameState.graphGeneration,
|
|
2008
|
+
),
|
|
1971
2009
|
});
|
|
1972
2010
|
shadowSpotIndex += 1;
|
|
1973
2011
|
}
|
|
@@ -2727,6 +2765,7 @@ export function recordFrame(
|
|
|
2727
2765
|
gpuDrivenShadowDrawKeysByView: rasterGpuDriven.shadowDrawKeysByView,
|
|
2728
2766
|
}),
|
|
2729
2767
|
}),
|
|
2768
|
+
shadowViewPlanesByView: shadowViewPlanesByView(lights),
|
|
2730
2769
|
...(frameGpuDriven?.scene === undefined
|
|
2731
2770
|
? {}
|
|
2732
2771
|
: { gpuDrivenSceneMaterialBuffer: frameGpuDriven.scene.scene.materialBuffer }),
|
|
@@ -14,7 +14,7 @@ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_UNIFORM } from '../gpu-usag
|
|
|
14
14
|
import type { SkylightBindGroupResources } from '../ibl/skylight-bind-group';
|
|
15
15
|
import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
|
|
16
16
|
import type { RenderResourceScope } from '../publication/resource-scope';
|
|
17
|
-
import {
|
|
17
|
+
import { internSharedRefFromGuid } from '../render-system-extract';
|
|
18
18
|
import {
|
|
19
19
|
applyParamSchemaDefaultsToUbo,
|
|
20
20
|
applyParamSnapshotToUbo,
|
|
@@ -29,7 +29,6 @@ export type PreparedResolverCaches = {
|
|
|
29
29
|
readonly preparedGroup0Pipelines: WeakSet<object>;
|
|
30
30
|
readonly preparedViewOnlyPipelines: WeakSet<object>;
|
|
31
31
|
readonly preparedRenderMaterialPipelines: WeakSet<object>;
|
|
32
|
-
readonly preparedAssetHandles: WeakMap<RenderResourceScope, Map<string, number>>;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
export function createPreparedResolverCaches(): PreparedResolverCaches {
|
|
@@ -39,37 +38,22 @@ export function createPreparedResolverCaches(): PreparedResolverCaches {
|
|
|
39
38
|
preparedGroup0Pipelines: new WeakSet(),
|
|
40
39
|
preparedViewOnlyPipelines: new WeakSet(),
|
|
41
40
|
preparedRenderMaterialPipelines: new WeakSet(),
|
|
42
|
-
preparedAssetHandles: new WeakMap(),
|
|
43
41
|
};
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
function preparedHandle<Brand extends string>(
|
|
47
45
|
runtime: RenderSystemInternals,
|
|
48
|
-
caches: PreparedResolverCaches,
|
|
49
46
|
world: RenderResourceScope,
|
|
50
47
|
guid: string,
|
|
51
48
|
brand: Brand,
|
|
52
49
|
): Handle<Brand, 'shared'> | undefined {
|
|
53
|
-
let handles = caches.preparedAssetHandles.get(world);
|
|
54
|
-
if (handles === undefined) {
|
|
55
|
-
handles = new Map();
|
|
56
|
-
caches.preparedAssetHandles.set(world, handles);
|
|
57
|
-
}
|
|
58
|
-
const key = `${brand}:${guid.toLowerCase()}`;
|
|
59
|
-
const cached = handles.get(key);
|
|
60
|
-
if (cached !== undefined) return cached as Handle<Brand, 'shared'>;
|
|
61
50
|
if ('resolveAsset' in world)
|
|
62
51
|
return world.handleForGuid(guid) as Handle<Brand, 'shared'> | undefined;
|
|
63
|
-
|
|
64
|
-
if (asset === undefined) return undefined;
|
|
65
|
-
const handle = world.allocSharedRef(brand, asset);
|
|
66
|
-
handles.set(key, handle as number);
|
|
67
|
-
return handle;
|
|
52
|
+
return internSharedRefFromGuid(world, runtime.assets, guid, brand);
|
|
68
53
|
}
|
|
69
54
|
|
|
70
55
|
export function preparedMaterialBindings(
|
|
71
56
|
runtime: RenderSystemInternals,
|
|
72
|
-
caches: PreparedResolverCaches,
|
|
73
57
|
worlds: readonly RenderResourceScope[],
|
|
74
58
|
materialShaderId: string,
|
|
75
59
|
worldIndex: number,
|
|
@@ -78,14 +62,16 @@ export function preparedMaterialBindings(
|
|
|
78
62
|
skylightResources?: SkylightBindGroupResources,
|
|
79
63
|
) {
|
|
80
64
|
const world = worlds[worldIndex];
|
|
81
|
-
const
|
|
82
|
-
world === undefined
|
|
83
|
-
? undefined
|
|
84
|
-
: renderAssetByGuid<MaterialAsset>(world, runtime.assets, materialGuid);
|
|
65
|
+
const handle =
|
|
66
|
+
world === undefined ? undefined : preparedHandle(runtime, world, materialGuid, 'MaterialAsset');
|
|
85
67
|
const pipelineState = runtime.getPipelineState();
|
|
86
|
-
if (world === undefined ||
|
|
68
|
+
if (world === undefined || handle === undefined || pipelineState === null) {
|
|
87
69
|
return err(new Error('prepared material asset is unavailable'));
|
|
88
70
|
}
|
|
71
|
+
const resolved = resolveAssetHandle<MaterialAsset>(world, handle);
|
|
72
|
+
if (!resolved.ok) return resolved;
|
|
73
|
+
const material = resolved.value;
|
|
74
|
+
if (material.kind !== 'material') return err(new Error('prepared material asset is unavailable'));
|
|
89
75
|
const schema = runtime.getParamSchema?.(materialShaderId) ?? [];
|
|
90
76
|
// Feature materials share the ordinary linear parameter domain and semantic texture defaults.
|
|
91
77
|
const values = materialValuesToLinearRuntime(
|
|
@@ -132,13 +118,7 @@ export function preparedMaterialBindings(
|
|
|
132
118
|
: undefined;
|
|
133
119
|
let sampler = pipelineState.defaultSampler;
|
|
134
120
|
if (textureValue?.sampler !== undefined) {
|
|
135
|
-
const handle = preparedHandle(
|
|
136
|
-
runtime,
|
|
137
|
-
caches,
|
|
138
|
-
world,
|
|
139
|
-
String(textureValue.sampler),
|
|
140
|
-
'SamplerAsset',
|
|
141
|
-
);
|
|
121
|
+
const handle = preparedHandle(runtime, world, String(textureValue.sampler), 'SamplerAsset');
|
|
142
122
|
if (handle !== undefined) {
|
|
143
123
|
const asset = resolveAssetHandle<SamplerAsset>(world, handle);
|
|
144
124
|
if (asset.ok) {
|
|
@@ -149,13 +129,7 @@ export function preparedMaterialBindings(
|
|
|
149
129
|
}
|
|
150
130
|
let view = defaultViewForUserRegionField(field, pipelineState, schema);
|
|
151
131
|
if (textureValue !== undefined) {
|
|
152
|
-
const handle = preparedHandle(
|
|
153
|
-
runtime,
|
|
154
|
-
caches,
|
|
155
|
-
world,
|
|
156
|
-
String(textureValue.texture),
|
|
157
|
-
'TextureAsset',
|
|
158
|
-
);
|
|
132
|
+
const handle = preparedHandle(runtime, world, String(textureValue.texture), 'TextureAsset');
|
|
159
133
|
if (handle !== undefined) {
|
|
160
134
|
view = residentTextureView(world, runtime.gpuStore, runtime, handle) ?? view;
|
|
161
135
|
}
|
|
@@ -620,6 +620,8 @@ export interface _StandardForwardSceneView {
|
|
|
620
620
|
readonly gpuDrivenShadowDrawKeys?: ReadonlySet<string>;
|
|
621
621
|
/** View-scoped ShadowCaster claims for exact CPU residual routing. */
|
|
622
622
|
readonly gpuDrivenShadowDrawKeysByView?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
623
|
+
/** Per-shadow-view frusta shared by GPU compaction and CPU residual routing. */
|
|
624
|
+
readonly shadowViewPlanesByView?: ReadonlyMap<string, Float32Array>;
|
|
623
625
|
/** Extracted structured ShadowCaster ownership facts. */
|
|
624
626
|
readonly shadowCasterMembership?: readonly ShadowCasterMembership[];
|
|
625
627
|
/** Complete retained ShadowCaster rows, including camera-culled casters. */
|