@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
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
} from '../gpu-usage';
|
|
20
20
|
import { createHdrpSkinUnifiedBindGroup, getOrCreateHdrpBuffers } from '../hdrp-buffers';
|
|
21
21
|
import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
|
|
22
|
+
import {
|
|
23
|
+
type InstanceCollectionFailureCode,
|
|
24
|
+
recordInstanceFailure,
|
|
25
|
+
recordInstanceResidency,
|
|
26
|
+
} from '../instances';
|
|
22
27
|
import { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from '../materials';
|
|
23
28
|
import {
|
|
24
29
|
createPbrSkinMeshBindGroupEntries,
|
|
@@ -39,13 +44,17 @@ import {
|
|
|
39
44
|
import type { PointsLinesRecordPlan } from '../points-lines/record';
|
|
40
45
|
import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
|
|
41
46
|
import type { RenderRecordPhase } from '../render-contract';
|
|
42
|
-
import type { MaterialSnapshot } from '../render-system-extract';
|
|
47
|
+
import type { MaterialSnapshot, RenderableSnapshot } from '../render-system-extract';
|
|
43
48
|
import {
|
|
44
49
|
PROBE_BLEND_RECORD_BYTE_SIZE,
|
|
45
50
|
PROBE_BLEND_RECORD_STRIDE,
|
|
46
51
|
probeBlendRecordOffset,
|
|
47
52
|
} from '../scene/probe-blend-record';
|
|
48
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
getOpaqueResourceIdentity,
|
|
55
|
+
instanceCollectionCacheKey,
|
|
56
|
+
worldEntityKey,
|
|
57
|
+
} from './frame-snapshot';
|
|
49
58
|
import {
|
|
50
59
|
geometryRenderStateForTopology,
|
|
51
60
|
isEntityFullyTransparent,
|
|
@@ -54,6 +63,7 @@ import {
|
|
|
54
63
|
} from './main-pass-material';
|
|
55
64
|
import {
|
|
56
65
|
getOrCreateFromChain,
|
|
66
|
+
INSTANCE_STORAGE_STRIDE_FLOATS,
|
|
57
67
|
INSTANCE_UBO_FULL_ARRAY_BYTES,
|
|
58
68
|
MAX_UNIFORM_INSTANCES,
|
|
59
69
|
MESH_PER_ENTITY_STRIDE,
|
|
@@ -73,6 +83,18 @@ type GeometryInstanceDraw = {
|
|
|
73
83
|
readonly instanceCount: number;
|
|
74
84
|
};
|
|
75
85
|
|
|
86
|
+
type InstanceUploadResult =
|
|
87
|
+
| {
|
|
88
|
+
readonly ok: true;
|
|
89
|
+
readonly ranges: readonly { readonly start: number; readonly end: number }[];
|
|
90
|
+
readonly bytes: number;
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
readonly ok: false;
|
|
94
|
+
readonly ranges: readonly { readonly start: number; readonly end: number }[];
|
|
95
|
+
readonly bytes: number;
|
|
96
|
+
};
|
|
97
|
+
|
|
76
98
|
type GeometryBindingState = {
|
|
77
99
|
pipeline: RenderPipeline | null;
|
|
78
100
|
instancesBuffer: Buffer | null;
|
|
@@ -1268,8 +1290,285 @@ export function recordGeometryDraws(
|
|
|
1268
1290
|
*
|
|
1269
1291
|
* @internal
|
|
1270
1292
|
*/
|
|
1271
|
-
function
|
|
1272
|
-
c:
|
|
1293
|
+
function uploadInstanceRanges(
|
|
1294
|
+
c: Pick<
|
|
1295
|
+
_InternalRenderPipelineContext,
|
|
1296
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1297
|
+
>,
|
|
1298
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1299
|
+
buffer: Buffer,
|
|
1300
|
+
ranges: readonly { readonly start: number; readonly end: number }[],
|
|
1301
|
+
storageBacked: boolean,
|
|
1302
|
+
sourceOffsetInstances = 0,
|
|
1303
|
+
): InstanceUploadResult {
|
|
1304
|
+
const strideBytes =
|
|
1305
|
+
(storageBacked ? INSTANCE_STORAGE_STRIDE_FLOATS : 16) * Float32Array.BYTES_PER_ELEMENT;
|
|
1306
|
+
const uploadedRanges: { start: number; end: number }[] = [];
|
|
1307
|
+
let uploadedBytes = 0;
|
|
1308
|
+
for (const range of ranges) {
|
|
1309
|
+
const start = Math.max(0, Math.min(inst.instanceCount, range.start));
|
|
1310
|
+
const end = Math.max(start, Math.min(inst.instanceCount, range.end));
|
|
1311
|
+
if (end <= start) continue;
|
|
1312
|
+
const source = inst.transforms.subarray(
|
|
1313
|
+
(sourceOffsetInstances + start) * 16,
|
|
1314
|
+
(sourceOffsetInstances + end) * 16,
|
|
1315
|
+
);
|
|
1316
|
+
const payload = storageBacked ? packInstanceStorageBuffer(source) : source;
|
|
1317
|
+
const written = c.runtime.device.queue.writeBuffer(buffer, start * strideBytes, payload);
|
|
1318
|
+
if (!written.ok) {
|
|
1319
|
+
c.runtime.errorRegistry.fire(written.error);
|
|
1320
|
+
return { ok: false, ranges: uploadedRanges, bytes: uploadedBytes };
|
|
1321
|
+
}
|
|
1322
|
+
uploadedRanges.push({ start, end });
|
|
1323
|
+
uploadedBytes += payload.byteLength;
|
|
1324
|
+
}
|
|
1325
|
+
return { ok: true, ranges: uploadedRanges, bytes: uploadedBytes };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
export function reportInstanceResidency(
|
|
1329
|
+
c: Pick<
|
|
1330
|
+
_InternalRenderPipelineContext,
|
|
1331
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1332
|
+
>,
|
|
1333
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1334
|
+
input: {
|
|
1335
|
+
readonly lane: 'direct-storage' | 'chunked-storage' | 'direct-uniform' | 'chunked-uniform';
|
|
1336
|
+
readonly requestedBytes: number;
|
|
1337
|
+
readonly supportedBytes: number | undefined;
|
|
1338
|
+
readonly uploadRanges: readonly { readonly start: number; readonly end: number }[];
|
|
1339
|
+
readonly uploadedBytes: number;
|
|
1340
|
+
},
|
|
1341
|
+
): void {
|
|
1342
|
+
if (inst.collectionId === undefined || c.frameState.instanceResidency === undefined) return;
|
|
1343
|
+
recordInstanceResidency(c.frameState.instanceResidency, {
|
|
1344
|
+
collectionId: inst.collectionId,
|
|
1345
|
+
frameNumber: c.frameState.frameNumber,
|
|
1346
|
+
residentGeneration: c.runtime.deviceScope.generation,
|
|
1347
|
+
lane: input.lane,
|
|
1348
|
+
requestedBytes: input.requestedBytes,
|
|
1349
|
+
supportedBytes: input.supportedBytes,
|
|
1350
|
+
uploadRanges: input.uploadRanges,
|
|
1351
|
+
uploadedBytes: input.uploadedBytes,
|
|
1352
|
+
backend: c.runtime.device.caps.backendKind,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function reportInstanceFailure(
|
|
1357
|
+
c: Pick<
|
|
1358
|
+
_InternalRenderPipelineContext,
|
|
1359
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1360
|
+
>,
|
|
1361
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1362
|
+
error: {
|
|
1363
|
+
readonly code: InstanceCollectionFailureCode;
|
|
1364
|
+
readonly expected: string;
|
|
1365
|
+
readonly hint: string;
|
|
1366
|
+
},
|
|
1367
|
+
requestedBytes: number,
|
|
1368
|
+
supportedBytes: number | undefined,
|
|
1369
|
+
): void {
|
|
1370
|
+
if (inst.collectionId === undefined || c.frameState.instanceResidency === undefined) return;
|
|
1371
|
+
recordInstanceFailure(c.frameState.instanceResidency, {
|
|
1372
|
+
collectionId: inst.collectionId,
|
|
1373
|
+
code: error.code,
|
|
1374
|
+
expected: error.expected,
|
|
1375
|
+
hint: error.hint,
|
|
1376
|
+
facts: {
|
|
1377
|
+
requestedBytes,
|
|
1378
|
+
supportedBytes,
|
|
1379
|
+
backend: c.runtime.device.caps.backendKind,
|
|
1380
|
+
owner: 'renderer.instances',
|
|
1381
|
+
cause: error.code,
|
|
1382
|
+
recovery: error.hint,
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function clearInstanceChunksForOwner(
|
|
1388
|
+
c: Pick<
|
|
1389
|
+
_InternalRenderPipelineContext,
|
|
1390
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1391
|
+
>,
|
|
1392
|
+
ownerKey: number,
|
|
1393
|
+
): void {
|
|
1394
|
+
const chunks = c.frameState.instanceBufferChunks;
|
|
1395
|
+
if (chunks === undefined) return;
|
|
1396
|
+
const prefix = `${ownerKey}:`;
|
|
1397
|
+
for (const [key, entry] of chunks.entries()) {
|
|
1398
|
+
if (!key.startsWith(prefix)) continue;
|
|
1399
|
+
if (!entry.buffer.isDestroyed) {
|
|
1400
|
+
const destroyed = entry.buffer.destroy();
|
|
1401
|
+
if (!destroyed.ok) c.runtime.errorRegistry.fire(destroyed.error);
|
|
1402
|
+
}
|
|
1403
|
+
chunks.delete(key);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
export function resolveStorageInstanceChunks(
|
|
1408
|
+
c: Pick<
|
|
1409
|
+
_InternalRenderPipelineContext,
|
|
1410
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1411
|
+
>,
|
|
1412
|
+
entry: _InternalRenderPipelineContext['validatedOrdered'][number],
|
|
1413
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1414
|
+
cap: number,
|
|
1415
|
+
probeBuffer: Buffer | undefined,
|
|
1416
|
+
probeOffset: number,
|
|
1417
|
+
): readonly GeometryInstanceDraw[] | null {
|
|
1418
|
+
const { runtime, frameState } = c;
|
|
1419
|
+
const strideBytes = INSTANCE_STORAGE_STRIDE_FLOATS * Float32Array.BYTES_PER_ELEMENT;
|
|
1420
|
+
const chunkCapacity = Math.floor(cap / strideBytes);
|
|
1421
|
+
if (chunkCapacity < 1) {
|
|
1422
|
+
reportInstanceFailure(
|
|
1423
|
+
c,
|
|
1424
|
+
inst,
|
|
1425
|
+
{
|
|
1426
|
+
code: 'limit-exceeded',
|
|
1427
|
+
expected: `maxStorageBufferBindingSize (${cap}) >= ${strideBytes}`,
|
|
1428
|
+
hint: 'use a backend with storage-buffer bindings large enough for one InstanceData record',
|
|
1429
|
+
},
|
|
1430
|
+
inst.instanceCount * strideBytes,
|
|
1431
|
+
cap,
|
|
1432
|
+
);
|
|
1433
|
+
runtime.errorRegistry.fire(
|
|
1434
|
+
new RhiError({
|
|
1435
|
+
code: 'limit-exceeded',
|
|
1436
|
+
expected: `maxStorageBufferBindingSize (${cap}) >= ${strideBytes}`,
|
|
1437
|
+
hint: 'use a backend with storage-buffer bindings large enough for one InstanceData record',
|
|
1438
|
+
detail: { maxStorageBufferBindingSize: cap, requestedBytes: strideBytes },
|
|
1439
|
+
}),
|
|
1440
|
+
);
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
const ownerKey = instanceCollectionCacheKey(entry.source.worldId, inst);
|
|
1444
|
+
const chunks = frameState.instanceBufferChunks;
|
|
1445
|
+
const activeKeys = new Set<string>();
|
|
1446
|
+
const draws: GeometryInstanceDraw[] = [];
|
|
1447
|
+
const uploadedRanges: { start: number; end: number }[] = [];
|
|
1448
|
+
let uploadedBytes = 0;
|
|
1449
|
+
for (let start = 0; start < inst.instanceCount; start += chunkCapacity) {
|
|
1450
|
+
const end = Math.min(inst.instanceCount, start + chunkCapacity);
|
|
1451
|
+
const count = end - start;
|
|
1452
|
+
const chunkKey = `${ownerKey}:${start}`;
|
|
1453
|
+
activeKeys.add(chunkKey);
|
|
1454
|
+
// Keep stable-resident checks arithmetic-only; packing is deferred to the
|
|
1455
|
+
// upload path below, where a new or dirty range actually needs payload.
|
|
1456
|
+
const payloadBytes = count * strideBytes;
|
|
1457
|
+
const previous = chunks?.get(chunkKey);
|
|
1458
|
+
let active =
|
|
1459
|
+
previous !== undefined &&
|
|
1460
|
+
previous.uploadedArchVersion === inst.archVersion &&
|
|
1461
|
+
previous.uploadedByteLength === payloadBytes
|
|
1462
|
+
? previous
|
|
1463
|
+
: undefined;
|
|
1464
|
+
let activeIsNew = false;
|
|
1465
|
+
if (active === undefined) {
|
|
1466
|
+
const created = runtime.device.createBuffer({
|
|
1467
|
+
size: payloadBytes,
|
|
1468
|
+
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
1469
|
+
mappedAtCreation: false,
|
|
1470
|
+
});
|
|
1471
|
+
if (!created.ok) {
|
|
1472
|
+
runtime.errorRegistry.fire(created.error);
|
|
1473
|
+
reportInstanceFailure(c, inst, created.error, inst.instanceCount * strideBytes, cap);
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
if (previous !== undefined && !previous.buffer.isDestroyed) {
|
|
1477
|
+
const destroyed = previous.buffer.destroy();
|
|
1478
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1479
|
+
}
|
|
1480
|
+
active = {
|
|
1481
|
+
buffer: new GpuBuffer(runtime.device, created.value),
|
|
1482
|
+
uploadedArchVersion: inst.archVersion,
|
|
1483
|
+
uploadedByteLength: payloadBytes,
|
|
1484
|
+
};
|
|
1485
|
+
activeIsNew = true;
|
|
1486
|
+
chunks?.set(chunkKey, active);
|
|
1487
|
+
if (chunks === undefined) frameState.transientInstanceBuffers.push(active);
|
|
1488
|
+
}
|
|
1489
|
+
if (active === undefined) return null;
|
|
1490
|
+
const needsUpload =
|
|
1491
|
+
activeIsNew || active.uploadedRevision !== (inst.revision ?? inst.archVersion);
|
|
1492
|
+
const rangesToUpload = [
|
|
1493
|
+
{
|
|
1494
|
+
start: 0,
|
|
1495
|
+
end: count,
|
|
1496
|
+
},
|
|
1497
|
+
];
|
|
1498
|
+
if (needsUpload && rangesToUpload.length > 0) {
|
|
1499
|
+
const upload = uploadInstanceRanges(
|
|
1500
|
+
c,
|
|
1501
|
+
inst,
|
|
1502
|
+
active.buffer.handle,
|
|
1503
|
+
rangesToUpload,
|
|
1504
|
+
true,
|
|
1505
|
+
start,
|
|
1506
|
+
);
|
|
1507
|
+
for (const range of upload.ranges) {
|
|
1508
|
+
uploadedRanges.push({ start: range.start + start, end: range.end + start });
|
|
1509
|
+
}
|
|
1510
|
+
uploadedBytes += upload.bytes;
|
|
1511
|
+
if (!upload.ok) {
|
|
1512
|
+
reportInstanceFailure(
|
|
1513
|
+
c,
|
|
1514
|
+
inst,
|
|
1515
|
+
{
|
|
1516
|
+
code: 'queue-write-buffer-failed',
|
|
1517
|
+
expected:
|
|
1518
|
+
'the renderer-owned instance storage buffer accepts the complete instance payload',
|
|
1519
|
+
hint: 'retry after the active device is healthy or recover the renderer',
|
|
1520
|
+
},
|
|
1521
|
+
inst.instanceCount * strideBytes,
|
|
1522
|
+
cap,
|
|
1523
|
+
);
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (needsUpload) {
|
|
1528
|
+
{
|
|
1529
|
+
const published = { ...active, uploadedRevision: inst.revision ?? inst.archVersion };
|
|
1530
|
+
active = published;
|
|
1531
|
+
chunks?.set(chunkKey, published);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const activeBuffer = active;
|
|
1535
|
+
draws.push({
|
|
1536
|
+
instanceBuffer: activeBuffer.buffer.handle,
|
|
1537
|
+
instanceBindGroup: resolveGeometryInstancesBindGroup(
|
|
1538
|
+
c,
|
|
1539
|
+
activeBuffer.buffer.handle,
|
|
1540
|
+
probeBuffer,
|
|
1541
|
+
),
|
|
1542
|
+
probeOffset,
|
|
1543
|
+
instanceCount: count,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
if (chunks !== undefined) {
|
|
1547
|
+
const prefix = `${ownerKey}:`;
|
|
1548
|
+
for (const [key, stale] of chunks.entries()) {
|
|
1549
|
+
if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
|
|
1550
|
+
if (!stale.buffer.isDestroyed) {
|
|
1551
|
+
const destroyed = stale.buffer.destroy();
|
|
1552
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1553
|
+
}
|
|
1554
|
+
chunks.delete(key);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
reportInstanceResidency(c, inst, {
|
|
1558
|
+
lane: 'chunked-storage',
|
|
1559
|
+
requestedBytes: inst.instanceCount * strideBytes,
|
|
1560
|
+
supportedBytes: cap,
|
|
1561
|
+
uploadRanges: uploadedRanges,
|
|
1562
|
+
uploadedBytes,
|
|
1563
|
+
});
|
|
1564
|
+
return draws;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
export function resolveGeometryInstanceBuffer(
|
|
1568
|
+
c: Pick<
|
|
1569
|
+
_InternalRenderPipelineContext,
|
|
1570
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1571
|
+
>,
|
|
1273
1572
|
entry: _InternalRenderPipelineContext['validatedOrdered'][number],
|
|
1274
1573
|
identityInstanceDraws: readonly GeometryInstanceDraw[],
|
|
1275
1574
|
useProbeBlend: boolean,
|
|
@@ -1304,6 +1603,11 @@ function resolveGeometryInstanceBuffer(
|
|
|
1304
1603
|
// A present, empty Instances array means zero draws. Do not fall back to
|
|
1305
1604
|
// the shared identity buffer, which would silently render one instance.
|
|
1306
1605
|
if (inst.instanceCount === 0) return [];
|
|
1606
|
+
const entityCacheKey = instanceCollectionCacheKey(entry.source.worldId, inst);
|
|
1607
|
+
const revision = inst.revision ?? inst.archVersion;
|
|
1608
|
+
const sameRevision = (cached: InstanceBufferCacheEntry): boolean =>
|
|
1609
|
+
cached.uploadedRevision === revision;
|
|
1610
|
+
const fullRange = [{ start: 0, end: inst.instanceCount }] as const;
|
|
1307
1611
|
let instanceBuffer: Buffer = pipelineState.identityInstanceBuffer;
|
|
1308
1612
|
let instanceCount = 1;
|
|
1309
1613
|
{
|
|
@@ -1315,41 +1619,126 @@ function resolveGeometryInstanceBuffer(
|
|
|
1315
1619
|
let instanceBufferUsage = GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST;
|
|
1316
1620
|
|
|
1317
1621
|
if (uniformFallback && inst.instanceCount > MAX_UNIFORM_INSTANCES) {
|
|
1622
|
+
const ownerKey = instanceCollectionCacheKey(entry.source.worldId, inst);
|
|
1623
|
+
const chunks = frameState.instanceBufferChunks;
|
|
1624
|
+
const activeKeys = new Set<string>();
|
|
1318
1625
|
const draws: GeometryInstanceDraw[] = [];
|
|
1626
|
+
const uniformStrideBytes = 16 * Float32Array.BYTES_PER_ELEMENT;
|
|
1627
|
+
const uploadedRanges: { start: number; end: number }[] = [];
|
|
1628
|
+
let uploadedBytes = 0;
|
|
1319
1629
|
for (let start = 0; start < inst.instanceCount; start += MAX_UNIFORM_INSTANCES) {
|
|
1320
1630
|
const count = Math.min(MAX_UNIFORM_INSTANCES, inst.instanceCount - start);
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1631
|
+
const chunkKey = `${ownerKey}:${start}`;
|
|
1632
|
+
activeKeys.add(chunkKey);
|
|
1633
|
+
const payloadBytes = count * uniformStrideBytes;
|
|
1634
|
+
const previous = chunks?.get(chunkKey);
|
|
1635
|
+
let active =
|
|
1636
|
+
previous !== undefined &&
|
|
1637
|
+
previous.uploadedArchVersion === inst.archVersion &&
|
|
1638
|
+
previous.uploadedByteLength === payloadBytes
|
|
1639
|
+
? previous
|
|
1640
|
+
: undefined;
|
|
1641
|
+
let activeIsNew = false;
|
|
1642
|
+
if (active === undefined) {
|
|
1643
|
+
const created = runtime.device.createBuffer({
|
|
1644
|
+
size: INSTANCE_UBO_FULL_ARRAY_BYTES,
|
|
1645
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1646
|
+
mappedAtCreation: false,
|
|
1647
|
+
});
|
|
1648
|
+
if (!created.ok) {
|
|
1649
|
+
runtime.errorRegistry.fire(created.error);
|
|
1650
|
+
reportInstanceFailure(
|
|
1651
|
+
c,
|
|
1652
|
+
inst,
|
|
1653
|
+
created.error,
|
|
1654
|
+
inst.instanceCount * uniformStrideBytes,
|
|
1655
|
+
MAX_UNIFORM_INSTANCES * uniformStrideBytes,
|
|
1656
|
+
);
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
if (previous !== undefined && !previous.buffer.isDestroyed) {
|
|
1660
|
+
const destroyed = previous.buffer.destroy();
|
|
1661
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1662
|
+
}
|
|
1663
|
+
active = {
|
|
1664
|
+
buffer: new GpuBuffer(runtime.device, created.value),
|
|
1665
|
+
uploadedArchVersion: inst.archVersion,
|
|
1666
|
+
uploadedByteLength: payloadBytes,
|
|
1667
|
+
};
|
|
1668
|
+
activeIsNew = true;
|
|
1669
|
+
chunks?.set(chunkKey, active);
|
|
1670
|
+
if (chunks === undefined) frameState.transientInstanceBuffers.push(active);
|
|
1330
1671
|
}
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1340
|
-
|
|
1672
|
+
const needsUpload = activeIsNew || !sameRevision(active);
|
|
1673
|
+
const rangesToUpload = [
|
|
1674
|
+
{
|
|
1675
|
+
start: 0,
|
|
1676
|
+
end: count,
|
|
1677
|
+
},
|
|
1678
|
+
];
|
|
1679
|
+
if (needsUpload && rangesToUpload.length > 0) {
|
|
1680
|
+
const upload = uploadInstanceRanges(
|
|
1681
|
+
c,
|
|
1682
|
+
inst,
|
|
1683
|
+
active.buffer.handle,
|
|
1684
|
+
rangesToUpload,
|
|
1685
|
+
false,
|
|
1686
|
+
start,
|
|
1687
|
+
);
|
|
1688
|
+
for (const range of upload.ranges) {
|
|
1689
|
+
uploadedRanges.push({ start: range.start + start, end: range.end + start });
|
|
1690
|
+
}
|
|
1691
|
+
uploadedBytes += upload.bytes;
|
|
1692
|
+
if (!upload.ok) {
|
|
1693
|
+
reportInstanceFailure(
|
|
1694
|
+
c,
|
|
1695
|
+
inst,
|
|
1696
|
+
{
|
|
1697
|
+
code: 'queue-write-buffer-failed',
|
|
1698
|
+
expected:
|
|
1699
|
+
'the renderer-owned instance uniform buffer accepts the complete instance payload',
|
|
1700
|
+
hint: 'retry after the active device is healthy or recover the renderer',
|
|
1701
|
+
},
|
|
1702
|
+
inst.instanceCount * uniformStrideBytes,
|
|
1703
|
+
MAX_UNIFORM_INSTANCES * uniformStrideBytes,
|
|
1704
|
+
);
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (needsUpload) {
|
|
1709
|
+
const published = { ...active, uploadedRevision: revision };
|
|
1710
|
+
active = published;
|
|
1711
|
+
chunks?.set(chunkKey, published);
|
|
1341
1712
|
}
|
|
1342
1713
|
draws.push({
|
|
1343
|
-
instanceBuffer: buffer.handle,
|
|
1714
|
+
instanceBuffer: active.buffer.handle,
|
|
1344
1715
|
instanceBindGroup: resolveGeometryInstancesBindGroup(
|
|
1345
1716
|
c,
|
|
1346
|
-
buffer.handle,
|
|
1717
|
+
active.buffer.handle,
|
|
1347
1718
|
probeBinding?.buffer,
|
|
1348
1719
|
),
|
|
1349
1720
|
probeOffset: probeBinding?.offset ?? -1,
|
|
1350
1721
|
instanceCount: count,
|
|
1351
1722
|
});
|
|
1352
1723
|
}
|
|
1724
|
+
if (chunks !== undefined) {
|
|
1725
|
+
const prefix = `${ownerKey}:`;
|
|
1726
|
+
for (const [key, stale] of chunks.entries()) {
|
|
1727
|
+
if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
|
|
1728
|
+
if (!stale.buffer.isDestroyed) {
|
|
1729
|
+
const destroyed = stale.buffer.destroy();
|
|
1730
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1731
|
+
}
|
|
1732
|
+
chunks.delete(key);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
reportInstanceResidency(c, inst, {
|
|
1736
|
+
lane: 'chunked-uniform',
|
|
1737
|
+
requestedBytes: inst.instanceCount * uniformStrideBytes,
|
|
1738
|
+
supportedBytes: MAX_UNIFORM_INSTANCES * uniformStrideBytes,
|
|
1739
|
+
uploadRanges: uploadedRanges,
|
|
1740
|
+
uploadedBytes,
|
|
1741
|
+
});
|
|
1353
1742
|
return draws;
|
|
1354
1743
|
}
|
|
1355
1744
|
if (uniformFallback) {
|
|
@@ -1359,31 +1748,30 @@ function resolveGeometryInstanceBuffer(
|
|
|
1359
1748
|
{
|
|
1360
1749
|
// Cap-gate (LimitExceededDetail single emit point — feat-20260514
|
|
1361
1750
|
// M3 / w15 anchor): `requestedBytes <= maxStorageBufferBindingSize`.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1751
|
+
// Derive the size from the authoring count; storage packing is deferred
|
|
1752
|
+
// until uploadInstanceRanges confirms a new or dirty resident.
|
|
1753
|
+
const requestedBytes =
|
|
1754
|
+
inst.instanceCount *
|
|
1755
|
+
(uniformFallback ? 16 : INSTANCE_STORAGE_STRIDE_FLOATS) *
|
|
1756
|
+
Float32Array.BYTES_PER_ELEMENT;
|
|
1366
1757
|
const cap = runtime.device.limits.maxStorageBufferBindingSize;
|
|
1367
1758
|
if (!uniformFallback && typeof cap === 'number' && cap > 0 && requestedBytes > cap) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
requestedBytes,
|
|
1376
|
-
},
|
|
1377
|
-
}),
|
|
1759
|
+
const chunked = resolveStorageInstanceChunks(
|
|
1760
|
+
c,
|
|
1761
|
+
entry,
|
|
1762
|
+
inst,
|
|
1763
|
+
cap,
|
|
1764
|
+
probeBinding?.buffer,
|
|
1765
|
+
probeBinding?.offset ?? -1,
|
|
1378
1766
|
);
|
|
1379
|
-
return
|
|
1767
|
+
return chunked;
|
|
1380
1768
|
} else {
|
|
1381
1769
|
// Look up the cached GPU buffer or create a fresh one when the
|
|
1382
1770
|
// archetype version bumped or the byte length changed.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
);
|
|
1771
|
+
// entityCacheKey is the worldEntityKey + collectionId namespace.
|
|
1772
|
+
const cached = frameState.instanceBuffers.get(entityCacheKey);
|
|
1386
1773
|
let active: InstanceBufferCacheEntry | null = null;
|
|
1774
|
+
let activeIsNew = false;
|
|
1387
1775
|
if (
|
|
1388
1776
|
cached !== undefined &&
|
|
1389
1777
|
cached.uploadedArchVersion === inst.archVersion &&
|
|
@@ -1398,6 +1786,8 @@ function resolveGeometryInstanceBuffer(
|
|
|
1398
1786
|
});
|
|
1399
1787
|
if (!bufRes.ok) {
|
|
1400
1788
|
runtime.errorRegistry.fire(bufRes.error);
|
|
1789
|
+
reportInstanceFailure(c, inst, bufRes.error, requestedBytes, cap);
|
|
1790
|
+
return null;
|
|
1401
1791
|
} else {
|
|
1402
1792
|
// feat-20260619 M4 / F12: destroy the old cached buffer
|
|
1403
1793
|
// before replacing it with the new one (D-6).
|
|
@@ -1411,24 +1801,67 @@ function resolveGeometryInstanceBuffer(
|
|
|
1411
1801
|
uploadedArchVersion: inst.archVersion,
|
|
1412
1802
|
uploadedByteLength: requestedBytes,
|
|
1413
1803
|
};
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1804
|
+
activeIsNew = true;
|
|
1805
|
+
if (active !== null) {
|
|
1806
|
+
// Keep the collection identity in the worldEntityKey namespace.
|
|
1807
|
+
frameState.instanceBuffers.set(entityCacheKey, active);
|
|
1808
|
+
}
|
|
1418
1809
|
}
|
|
1419
1810
|
}
|
|
1420
1811
|
if (active !== null) {
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
)
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1812
|
+
const needsUpload = activeIsNew || !sameRevision(active);
|
|
1813
|
+
const ranges = [fullRange[0]];
|
|
1814
|
+
let uploadRanges: readonly { readonly start: number; readonly end: number }[] = [];
|
|
1815
|
+
let uploadedBytes = 0;
|
|
1816
|
+
if (needsUpload && ranges.length > 0) {
|
|
1817
|
+
const upload = uploadInstanceRanges(
|
|
1818
|
+
c,
|
|
1819
|
+
inst,
|
|
1820
|
+
active.buffer.handle,
|
|
1821
|
+
ranges,
|
|
1822
|
+
!uniformFallback,
|
|
1823
|
+
);
|
|
1824
|
+
uploadRanges = upload.ranges;
|
|
1825
|
+
uploadedBytes = upload.bytes;
|
|
1826
|
+
if (!upload.ok) {
|
|
1827
|
+
reportInstanceFailure(
|
|
1828
|
+
c,
|
|
1829
|
+
inst,
|
|
1830
|
+
{
|
|
1831
|
+
code: 'queue-write-buffer-failed',
|
|
1832
|
+
expected:
|
|
1833
|
+
'the renderer-owned instance buffer accepts the complete instance payloads',
|
|
1834
|
+
hint: 'retry after the active device is healthy or recover the renderer',
|
|
1835
|
+
},
|
|
1836
|
+
requestedBytes,
|
|
1837
|
+
uniformFallback
|
|
1838
|
+
? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
|
|
1839
|
+
: typeof cap === 'number'
|
|
1840
|
+
? cap
|
|
1841
|
+
: undefined,
|
|
1842
|
+
);
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (needsUpload) {
|
|
1847
|
+
const published = { ...active, uploadedRevision: revision };
|
|
1848
|
+
frameState.instanceBuffers.set(entityCacheKey, published);
|
|
1849
|
+
active = published;
|
|
1431
1850
|
}
|
|
1851
|
+
instanceBuffer = active.buffer.handle;
|
|
1852
|
+
instanceCount = Math.max(1, inst.instanceCount);
|
|
1853
|
+
reportInstanceResidency(c, inst, {
|
|
1854
|
+
lane: uniformFallback ? 'direct-uniform' : 'direct-storage',
|
|
1855
|
+
requestedBytes,
|
|
1856
|
+
supportedBytes: uniformFallback
|
|
1857
|
+
? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
|
|
1858
|
+
: typeof cap === 'number'
|
|
1859
|
+
? cap
|
|
1860
|
+
: undefined,
|
|
1861
|
+
uploadRanges,
|
|
1862
|
+
uploadedBytes,
|
|
1863
|
+
});
|
|
1864
|
+
clearInstanceChunksForOwner(c, entityCacheKey);
|
|
1432
1865
|
}
|
|
1433
1866
|
}
|
|
1434
1867
|
// The cache write above is keyed by worldEntityKey(worldId, cacheKey)
|
|
@@ -1446,7 +1879,10 @@ function resolveGeometryInstanceBuffer(
|
|
|
1446
1879
|
}
|
|
1447
1880
|
|
|
1448
1881
|
export function resolveProbeBlendBuffer(
|
|
1449
|
-
c:
|
|
1882
|
+
c: Pick<
|
|
1883
|
+
_InternalRenderPipelineContext,
|
|
1884
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1885
|
+
>,
|
|
1450
1886
|
record: import('../scene/probe-blend-record').ProbeBlendRecord | undefined,
|
|
1451
1887
|
entityKey?: number,
|
|
1452
1888
|
): { readonly buffer: Buffer; readonly offset: number } {
|
|
@@ -1494,7 +1930,10 @@ export function resolveProbeBlendBuffer(
|
|
|
1494
1930
|
}
|
|
1495
1931
|
|
|
1496
1932
|
export function resolveGeometryInstancesBindGroup(
|
|
1497
|
-
c:
|
|
1933
|
+
c: Pick<
|
|
1934
|
+
_InternalRenderPipelineContext,
|
|
1935
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1936
|
+
>,
|
|
1498
1937
|
instanceBuffer: Buffer,
|
|
1499
1938
|
probeBuffer?: Buffer,
|
|
1500
1939
|
): BindGroup {
|
|
@@ -856,7 +856,7 @@ function recordSpriteEntityDraws(
|
|
|
856
856
|
new RhiError({
|
|
857
857
|
code: 'limit-exceeded',
|
|
858
858
|
expected: `requestedBytes (${requestedBytes}) <= maxStorageBufferBindingSize (${cap})`,
|
|
859
|
-
hint: 'reduce
|
|
859
|
+
hint: 'reduce the sprite batch to fit within device.limits.maxStorageBufferBindingSize or use a storage-capable backend',
|
|
860
860
|
detail: {
|
|
861
861
|
maxStorageBufferBindingSize: cap,
|
|
862
862
|
requestedBytes,
|