@forgeax/engine-render 0.1.24 → 0.1.26
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 +118 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +4 -17
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.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 -2
- 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-YKB5DQV7.mjs → chunk-GF523LHF.mjs} +89 -21
- package/dist/chunk-GF523LHF.mjs.map +1 -0
- package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
- package/dist/chunk-HKXTW355.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-YRUYGA4A.mjs → chunk-TMIASV2N.mjs} +944 -577
- package/dist/chunk-TMIASV2N.mjs.map +1 -0
- package/dist/{chunk-Z3YYH2MB.mjs → chunk-YUQXSEHG.mjs} +160 -73
- package/dist/chunk-YUQXSEHG.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 +1012 -149
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/extract/contracts.d.ts +6 -6
- package/dist/extract/contracts.d.ts.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-types.d.ts +2 -0
- package/dist/pipeline-spec-types.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 +23 -4
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +20 -2
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts +3 -3
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/material-uniforms.d.ts +4 -0
- package/dist/record/material-uniforms.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +4 -3
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +10 -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 +6 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +44 -9
- 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__/fixtures/ai-weapon-spirit/README.md +20 -0
- package/src/__tests__/fixtures/ai-weapon-spirit/low-poly-toon.wgsl +178 -0
- package/src/__tests__/fixtures/ai-weapon-spirit/material.json +97 -0
- package/src/__tests__/fixtures/ai-weapon-spirit/painterly-surface.wgsl +55 -0
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +3 -3
- package/src/__tests__/gpu-driven-view.browser.test.ts +11 -3
- 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 +48 -13
- package/src/__tests__/{material-static-dynamic-route.unit.test.ts → material-layer-projection.unit.test.ts} +30 -47
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-program-dispatch.unit.test.ts +48 -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 +44 -2
- 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 -3
- 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/assembly.ts +8 -40
- package/src/assembly/material/standard-layer-projection.ts +3 -1
- package/src/assembly/material/surface-projection.ts +91 -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 +106 -166
- 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/extract/contracts.ts +6 -6
- 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 +109 -14
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec-types.ts +2 -0
- package/src/pipeline-spec.ts +13 -17
- package/src/record/frame-snapshot.ts +19 -2
- package/src/record/frame.ts +38 -3
- package/src/record/main-pass-geometry.ts +837 -337
- package/src/record/main-pass-material.ts +87 -2
- package/src/record/main-pass-sprite-draws.ts +66 -169
- package/src/record/main-pass.ts +24 -246
- package/src/record/material-uniforms.ts +186 -0
- package/src/record/render-context.ts +8 -0
- package/src/record/shadow-pass.ts +120 -115
- package/src/recovery/render-system-candidate.ts +46 -5
- package/src/render-contract.ts +4 -0
- package/src/render-system-extract-tail.ts +153 -57
- package/src/render-system-extract.ts +110 -73
- package/src/render-system.ts +59 -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/assembly/material/pipeline-projection.d.ts +0 -30
- package/dist/assembly/material/pipeline-projection.d.ts.map +0 -1
- 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/assembly/material/pipeline-projection.ts +0 -63
package/src/gpu-scene.ts
CHANGED
|
@@ -225,6 +225,10 @@ export class GpuScene {
|
|
|
225
225
|
drawTemplate: [],
|
|
226
226
|
material: [],
|
|
227
227
|
};
|
|
228
|
+
/** Transform rows whose previous value advances only after submit. */
|
|
229
|
+
private readonly pendingTemporalTransforms = new Set<number>();
|
|
230
|
+
/** Previous rows whose post-submit upload must be retried before the next draw. */
|
|
231
|
+
private readonly pendingTemporalUploads = new Set<number>();
|
|
228
232
|
|
|
229
233
|
private constructor(
|
|
230
234
|
private readonly device: RhiDevice,
|
|
@@ -273,31 +277,47 @@ export class GpuScene {
|
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
sync(delta: RenderSceneApplyResult): Result<GpuSceneSyncResult, RhiError> {
|
|
280
|
+
for (const transformIndex of this.pendingTemporalUploads) {
|
|
281
|
+
this.writesByTable.transform.push(transformIndex);
|
|
282
|
+
}
|
|
283
|
+
const pendingWrites = TABLE_NAMES.some((name) => this.writesByTable[name].length > 0);
|
|
276
284
|
if (
|
|
277
285
|
delta.createdSlots.length === 0 &&
|
|
278
286
|
delta.updatedSlots.length === 0 &&
|
|
279
287
|
delta.recreatedSlots.length === 0 &&
|
|
280
|
-
delta.removedSlots.length === 0
|
|
288
|
+
delta.removedSlots.length === 0 &&
|
|
289
|
+
!pendingWrites
|
|
281
290
|
) {
|
|
282
291
|
this.noChangeFrames += 1;
|
|
283
292
|
return ok({ ranges: 0, bytes: 0, grew: false, cleared: 0 });
|
|
284
293
|
}
|
|
285
294
|
const writesByTable = this.writesByTable;
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
if (!this.identityUploaded && !writesByTable.transform.includes(0)) {
|
|
296
|
+
writesByTable.transform.push(0);
|
|
297
|
+
}
|
|
288
298
|
let highestChangedSlot = -1;
|
|
289
|
-
|
|
299
|
+
const contentUpdatedSlots = delta.contentUpdatedSlots ?? [];
|
|
300
|
+
const contentUpdatedSlotIds = new Set(contentUpdatedSlots.map((record) => record.slot));
|
|
301
|
+
const allocationResets = new Set<number>();
|
|
302
|
+
// RenderScene may recycle a released CPU slot for a different identity in
|
|
303
|
+
// the same delta. Clear the old GPU allocation before ensuring the new
|
|
304
|
+
// record; otherwise the later removal pass clears the newly allocated
|
|
305
|
+
// record and the next transform-only update sees no allocation.
|
|
306
|
+
for (const record of delta.removedSlots) {
|
|
307
|
+
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
308
|
+
this.clearSlot(record.slot, writesByTable);
|
|
309
|
+
}
|
|
310
|
+
for (const records of [delta.createdSlots, contentUpdatedSlots, delta.recreatedSlots]) {
|
|
290
311
|
for (const record of records) {
|
|
312
|
+
const previous = this.allocations[record.slot];
|
|
291
313
|
this.ensureAllocation(record, writesByTable);
|
|
314
|
+
if (previous !== this.allocations[record.slot]) allocationResets.add(record.slot);
|
|
292
315
|
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
for (const record of delta.updatedSlots) {
|
|
296
319
|
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
297
320
|
}
|
|
298
|
-
for (const record of delta.removedSlots) {
|
|
299
|
-
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
300
|
-
}
|
|
301
321
|
const requiredCapacity = Math.max(
|
|
302
322
|
highestChangedSlot + 1,
|
|
303
323
|
this.instances.requiredCapacity(),
|
|
@@ -312,11 +332,18 @@ export class GpuScene {
|
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
for (const record of delta.createdSlots) this.writeSlot(record, true, writesByTable);
|
|
315
|
-
for (const record of
|
|
335
|
+
for (const record of contentUpdatedSlots) {
|
|
336
|
+
this.writeSlot(record, allocationResets.has(record.slot), writesByTable);
|
|
337
|
+
}
|
|
338
|
+
for (const record of delta.updatedSlots) {
|
|
339
|
+
if (contentUpdatedSlotIds.has(record.slot)) continue;
|
|
340
|
+
this.writeRootTransform(record, false, writesByTable);
|
|
341
|
+
}
|
|
316
342
|
for (const record of delta.recreatedSlots) this.writeSlot(record, true, writesByTable);
|
|
317
|
-
for (const record of delta.removedSlots) this.clearSlot(record.slot, writesByTable);
|
|
318
343
|
const uploaded = this.uploadRows(writesByTable);
|
|
319
344
|
if (!uploaded.ok) return uploaded;
|
|
345
|
+
this.pendingTemporalUploads.clear();
|
|
346
|
+
for (const name of TABLE_NAMES) writesByTable[name].length = 0;
|
|
320
347
|
this.identityUploaded = true;
|
|
321
348
|
this.clearedSlots += delta.removedSlots.length;
|
|
322
349
|
return ok({
|
|
@@ -329,27 +356,47 @@ export class GpuScene {
|
|
|
329
356
|
|
|
330
357
|
/** Publish current transforms as previous only after a successful submit. */
|
|
331
358
|
commitTemporalFrame(enabled = true): Result<void, RhiError> {
|
|
332
|
-
if (!enabled)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
359
|
+
if (!enabled) {
|
|
360
|
+
this.pendingTemporalTransforms.clear();
|
|
361
|
+
this.pendingTemporalUploads.clear();
|
|
362
|
+
return ok(undefined);
|
|
363
|
+
}
|
|
364
|
+
if (this.pendingTemporalTransforms.size === 0) return ok(undefined);
|
|
336
365
|
const currentOffset = offset(TRANSFORM, 'currentWorld');
|
|
337
366
|
const previousOffset = offset(TRANSFORM, 'previousWorld');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
const ranges = coalesceSlots([...this.pendingTemporalTransforms]);
|
|
368
|
+
for (const range of ranges) {
|
|
369
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
370
|
+
this.pendingTemporalUploads.add(index);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const range of ranges) {
|
|
374
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
375
|
+
this.transformBytes.copyWithin(
|
|
376
|
+
index * TRANSFORM.stride + previousOffset,
|
|
341
377
|
index * TRANSFORM.stride + currentOffset,
|
|
342
378
|
index * TRANSFORM.stride + currentOffset + 64,
|
|
343
|
-
)
|
|
344
|
-
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const range of ranges) {
|
|
383
|
+
const firstOffset = range.start * TRANSFORM.stride;
|
|
384
|
+
const lastOffset = range.end * TRANSFORM.stride;
|
|
385
|
+
const uploaded = this.device.queue.writeBuffer(
|
|
386
|
+
this.buffers.transform,
|
|
387
|
+
firstOffset,
|
|
388
|
+
this.transformBytes.subarray(firstOffset, lastOffset),
|
|
345
389
|
);
|
|
390
|
+
if (!uploaded.ok) return uploaded;
|
|
346
391
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
392
|
+
this.pendingTemporalTransforms.clear();
|
|
393
|
+
this.pendingTemporalUploads.clear();
|
|
394
|
+
const bytes = ranges.reduce(
|
|
395
|
+
(total, range) => total + (range.end - range.start) * TRANSFORM.stride,
|
|
396
|
+
0,
|
|
397
|
+
);
|
|
398
|
+
this.uploadRanges += ranges.length;
|
|
399
|
+
this.uploadBytes += bytes;
|
|
353
400
|
return ok(undefined);
|
|
354
401
|
}
|
|
355
402
|
|
|
@@ -359,6 +406,9 @@ export class GpuScene {
|
|
|
359
406
|
this.transforms.reset(1);
|
|
360
407
|
this.draws.reset();
|
|
361
408
|
this.materials.reset();
|
|
409
|
+
this.pendingTemporalTransforms.clear();
|
|
410
|
+
this.pendingTemporalUploads.clear();
|
|
411
|
+
for (const name of TABLE_NAMES) this.writesByTable[name].length = 0;
|
|
362
412
|
const writesByTable: Record<GpuSceneTableName, number[]> = {
|
|
363
413
|
primitive: [],
|
|
364
414
|
instance: [],
|
|
@@ -520,7 +570,8 @@ export class GpuScene {
|
|
|
520
570
|
const localCurrentOffset = localOffset + offset(TRANSFORM, 'currentWorld');
|
|
521
571
|
const localPreviousOffset = localOffset + offset(TRANSFORM, 'previousWorld');
|
|
522
572
|
writeMat4(transform, localCurrentOffset, localWorld);
|
|
523
|
-
writeMat4(transform, localPreviousOffset, localWorld);
|
|
573
|
+
if (resetPrevious) writeMat4(transform, localPreviousOffset, localWorld);
|
|
574
|
+
else this.pendingTemporalTransforms.add(transformIndex);
|
|
524
575
|
writes.transform.push(transformIndex);
|
|
525
576
|
}
|
|
526
577
|
writes.instance.push(instanceIndex);
|
|
@@ -631,14 +682,16 @@ export class GpuScene {
|
|
|
631
682
|
const allocation = this.allocations[record.slot];
|
|
632
683
|
if (allocation === undefined) throw new RangeError('GPU Scene allocation unavailable');
|
|
633
684
|
const transformOffset = allocation.transformStart * TRANSFORM.stride;
|
|
634
|
-
const currentOffset = transformOffset + offset(TRANSFORM, 'currentWorld');
|
|
635
685
|
const previousOffset = transformOffset + offset(TRANSFORM, 'previousWorld');
|
|
636
686
|
if (resetPrevious) {
|
|
637
687
|
writeMat4(this.transformView, previousOffset, record.snapshot.transform.world);
|
|
638
|
-
} else {
|
|
639
|
-
this.transformBytes.copyWithin(previousOffset, currentOffset, currentOffset + 64);
|
|
640
688
|
}
|
|
641
|
-
writeMat4(
|
|
689
|
+
writeMat4(
|
|
690
|
+
this.transformView,
|
|
691
|
+
transformOffset + offset(TRANSFORM, 'currentWorld'),
|
|
692
|
+
record.snapshot.transform.world,
|
|
693
|
+
);
|
|
694
|
+
if (!resetPrevious) this.pendingTemporalTransforms.add(allocation.transformStart);
|
|
642
695
|
writes.transform.push(allocation.transformStart);
|
|
643
696
|
}
|
|
644
697
|
|
|
@@ -743,6 +796,8 @@ export class GpuScene {
|
|
|
743
796
|
TRANSFORM.stride,
|
|
744
797
|
).fill(0);
|
|
745
798
|
writes.transform.push(transformIndex);
|
|
799
|
+
this.pendingTemporalTransforms.delete(transformIndex);
|
|
800
|
+
this.pendingTemporalUploads.delete(transformIndex);
|
|
746
801
|
}
|
|
747
802
|
for (let ordinal = 0; ordinal < allocation.drawCount; ordinal += 1) {
|
|
748
803
|
const drawIndex = allocation.drawStart + ordinal;
|
package/src/index.ts
CHANGED
|
@@ -196,6 +196,7 @@ export type {
|
|
|
196
196
|
ReflectionProbeSelectionInspection,
|
|
197
197
|
TransmissionInspection,
|
|
198
198
|
} from './inspection-types';
|
|
199
|
+
export type { InstanceCollectionInspection } from './instances';
|
|
199
200
|
export {
|
|
200
201
|
type MaterialColorInput3,
|
|
201
202
|
type MaterialColorInput4,
|
|
@@ -23,12 +23,16 @@ import type { GpuBuffer } from './gpu-resource';
|
|
|
23
23
|
* and routes dispose through the RHI shim's destroy bookkeeping SSOT. The
|
|
24
24
|
* two `uploaded*` fields are the cache invalidation fingerprint -- when the
|
|
25
25
|
* archetype version bumps or the byte length changes, the record stage
|
|
26
|
-
* allocates a fresh GpuBuffer and replaces the entry.
|
|
26
|
+
* allocates a fresh GpuBuffer and replaces the entry. Explicit collection
|
|
27
|
+
* entries are keyed by renderer-owned collection id at the outer map; the
|
|
28
|
+
* legacy entity key remains only for sprite/fold snapshots.
|
|
27
29
|
*/
|
|
28
30
|
export interface InstanceBufferCacheEntry {
|
|
29
31
|
readonly buffer: GpuBuffer;
|
|
30
32
|
readonly uploadedByteLength: number;
|
|
31
33
|
readonly uploadedArchVersion: number;
|
|
34
|
+
/** Renderer-owned collection revision uploaded into this buffer. */
|
|
35
|
+
readonly uploadedRevision?: number;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function destroyInstanceBufferEntries(
|
|
@@ -85,6 +89,15 @@ export function disposeInstanceBuffers(
|
|
|
85
89
|
map.clear();
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** Destroy the renderer-owned buffers used by internal large-instance chunks. */
|
|
93
|
+
export function disposeInstanceBufferChunks(
|
|
94
|
+
map: Map<string, InstanceBufferCacheEntry>,
|
|
95
|
+
errorRegistry?: InstanceBufferCacheErrorSink,
|
|
96
|
+
): void {
|
|
97
|
+
destroyInstanceBufferEntries(map.values(), errorRegistry);
|
|
98
|
+
map.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
/** Destroy transient uniform-fallback chunks allocated for the previous frame. */
|
|
89
102
|
export function disposeTransientInstanceBuffers(
|
|
90
103
|
entries: InstanceBufferCacheEntry[],
|
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
|
+
}
|