@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
|
@@ -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 { DispatchEntry, 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;
|
|
@@ -98,6 +120,8 @@ type MaterialPipelineLookup = {
|
|
|
98
120
|
readonly topology: PrimitiveTopology;
|
|
99
121
|
readonly indexFormat: 'uint16' | 'uint32';
|
|
100
122
|
readonly variantSet: string | undefined;
|
|
123
|
+
readonly vertexEntry: string | undefined;
|
|
124
|
+
readonly fragmentEntry: string | undefined;
|
|
101
125
|
readonly passKind: PassKind;
|
|
102
126
|
readonly layoutProjection: import('@forgeax/engine-geometry').VertexLayoutProjection;
|
|
103
127
|
readonly sampleCount: number;
|
|
@@ -343,8 +367,10 @@ export function recordGeometryDraws(
|
|
|
343
367
|
submeshMaterial: MaterialSnapshot,
|
|
344
368
|
entityKey: number,
|
|
345
369
|
materialWorld: World,
|
|
370
|
+
materialShaderId?: string,
|
|
346
371
|
) => BindGroup,
|
|
347
372
|
passKind: PassKind = 'forward',
|
|
373
|
+
selectedDispatch?: readonly DispatchEntry[],
|
|
348
374
|
): void {
|
|
349
375
|
const {
|
|
350
376
|
runtime,
|
|
@@ -359,6 +385,17 @@ export function recordGeometryDraws(
|
|
|
359
385
|
viewBindGroup,
|
|
360
386
|
viewBindGroupDynamicOffset = 0,
|
|
361
387
|
} = c;
|
|
388
|
+
const materialPasses = new Map<number, Map<number, DispatchEntry[]>>();
|
|
389
|
+
for (const dispatch of selectedDispatch ?? []) {
|
|
390
|
+
let byMaterial = materialPasses.get(dispatch.renderableIndex);
|
|
391
|
+
if (byMaterial === undefined) {
|
|
392
|
+
byMaterial = new Map();
|
|
393
|
+
materialPasses.set(dispatch.renderableIndex, byMaterial);
|
|
394
|
+
}
|
|
395
|
+
const passes = byMaterial.get(dispatch.materialHandle);
|
|
396
|
+
if (passes === undefined) byMaterial.set(dispatch.materialHandle, [dispatch]);
|
|
397
|
+
else passes.push(dispatch);
|
|
398
|
+
}
|
|
362
399
|
const diagnosticsEnabled = materialDiagnosticsEnabled();
|
|
363
400
|
const colorFormatOverride = c.transparentColorFormat as GPUTextureFormat | undefined;
|
|
364
401
|
// The prepared Standard topology is the frame authority. A bind-group
|
|
@@ -416,6 +453,8 @@ export function recordGeometryDraws(
|
|
|
416
453
|
variantSet: string | undefined,
|
|
417
454
|
colorFormat: GPUTextureFormat | undefined,
|
|
418
455
|
layoutProjection: import('@forgeax/engine-geometry').VertexLayoutProjection,
|
|
456
|
+
vertexEntry?: string,
|
|
457
|
+
fragmentEntry?: string,
|
|
419
458
|
): MaterialShaderPipelineEntry | null => {
|
|
420
459
|
// The fallback MRT is an extension of the built-in Standard PBR output
|
|
421
460
|
// contract only. The render pass may carry a second attachment whenever
|
|
@@ -441,6 +480,8 @@ export function recordGeometryDraws(
|
|
|
441
480
|
cached.topology === topology &&
|
|
442
481
|
cached.indexFormat === indexFormat &&
|
|
443
482
|
cached.variantSet === variantSet &&
|
|
483
|
+
cached.vertexEntry === vertexEntry &&
|
|
484
|
+
cached.fragmentEntry === fragmentEntry &&
|
|
444
485
|
cached.passKind === passKind &&
|
|
445
486
|
cached.layoutProjection.digest === layoutProjection.digest &&
|
|
446
487
|
cached.sampleCount === sampleCount &&
|
|
@@ -452,10 +493,12 @@ export function recordGeometryDraws(
|
|
|
452
493
|
}
|
|
453
494
|
}
|
|
454
495
|
const resolvedVariantSetResult =
|
|
455
|
-
|
|
456
|
-
(materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
|
|
496
|
+
runtime.getMaterialArtifact?.(materialShaderId) !== undefined
|
|
457
497
|
? ({ ok: true, value: undefined } as const)
|
|
458
|
-
:
|
|
498
|
+
: variantSet === undefined &&
|
|
499
|
+
(materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
|
|
500
|
+
? ({ ok: true, value: undefined } as const)
|
|
501
|
+
: variantSetFromVertexLayoutProjection(layoutProjection, variantSet);
|
|
459
502
|
if (!resolvedVariantSetResult.ok) {
|
|
460
503
|
runtime.errorRegistry.fire(resolvedVariantSetResult.error);
|
|
461
504
|
return null;
|
|
@@ -486,6 +529,8 @@ export function recordGeometryDraws(
|
|
|
486
529
|
undefined,
|
|
487
530
|
undefined,
|
|
488
531
|
reflectionFallbackFormats,
|
|
532
|
+
vertexEntry,
|
|
533
|
+
fragmentEntry,
|
|
489
534
|
) ?? null;
|
|
490
535
|
const handle =
|
|
491
536
|
profilePhase === undefined
|
|
@@ -506,6 +551,8 @@ export function recordGeometryDraws(
|
|
|
506
551
|
topology,
|
|
507
552
|
indexFormat,
|
|
508
553
|
variantSet,
|
|
554
|
+
vertexEntry,
|
|
555
|
+
fragmentEntry,
|
|
509
556
|
passKind,
|
|
510
557
|
layoutProjection,
|
|
511
558
|
sampleCount,
|
|
@@ -569,7 +616,8 @@ export function recordGeometryDraws(
|
|
|
569
616
|
// decal submesh) is NOT skipped at the entity level; its opaque submeshes
|
|
570
617
|
// draw here and the per-submesh loop below skips the transparent ones
|
|
571
618
|
// (they are drawn in the blend sub-pass instead).
|
|
572
|
-
if (splitLdrSprite && isEntityFullyTransparent(entry.source))
|
|
619
|
+
if (selectedDispatch === undefined && splitLdrSprite && isEntityFullyTransparent(entry.source))
|
|
620
|
+
continue;
|
|
573
621
|
|
|
574
622
|
const pipelineTag: 'unlit' = 'unlit';
|
|
575
623
|
|
|
@@ -953,295 +1001,315 @@ export function recordGeometryDraws(
|
|
|
953
1001
|
const materialHandle = submeshMaterial.materialHandle ?? 0;
|
|
954
1002
|
if (materialHandles === undefined || !materialHandles.has(materialHandle)) continue;
|
|
955
1003
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
// objects form the WeakMap chain and fully discriminate the
|
|
975
|
-
// binding state since sampler/textureView/buffer handle identities
|
|
976
|
-
// are stable across frames.
|
|
977
|
-
//
|
|
978
|
-
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13:
|
|
979
|
-
// sprite materials now use the same per-submesh BG construction
|
|
980
|
-
// (the sprite-specific BG branch above is deleted; sprite per-
|
|
981
|
-
// submesh single-slot is still enforced via materialSlotIndices
|
|
982
|
-
// and the sprite-shaped paramSnapshot fills the PBR-shaped BGL
|
|
983
|
-
// bindings via fallback textures for the 4 unused slots).
|
|
984
|
-
// feat-city-glb Bug 5: per-submesh material BG assembly extracted to
|
|
985
|
-
// the shared `buildPerSubmeshMaterialBg` closure (also called by the
|
|
986
|
-
// LDR blend sub-pass). Resolves the shader's user-region textures
|
|
987
|
-
// (baseColor/MR/normal + a custom Nth texture), emissive/occlusion
|
|
988
|
-
// injection, and Skylight merge; deduped cross-entity via the
|
|
989
|
-
// shaderId-outer `materialBgShared` cache.
|
|
990
|
-
const materialSlot = materialSlotIndices[i]?.[matSlotIdx] ?? materialSlotIndices[i]?.[0] ?? 0;
|
|
991
|
-
const processValue = (
|
|
992
|
-
globalThis as {
|
|
993
|
-
process?: { env?: Record<string, string | undefined> };
|
|
1004
|
+
const draws: readonly (DispatchEntry | undefined)[] =
|
|
1005
|
+
selectedDispatch === undefined
|
|
1006
|
+
? [undefined]
|
|
1007
|
+
: (materialPasses.get(entry.renderableIndex)?.get(submeshMaterial.materialHandle ?? 0) ??
|
|
1008
|
+
[]);
|
|
1009
|
+
for (const selectedPass of draws) {
|
|
1010
|
+
const selectedShaderId =
|
|
1011
|
+
selectedPass === undefined
|
|
1012
|
+
? submeshMaterial.materialShaderId
|
|
1013
|
+
: selectedPass.materialShaderId;
|
|
1014
|
+
const selectedRenderState =
|
|
1015
|
+
selectedPass === undefined ? submeshMaterial.renderState : selectedPass.renderState;
|
|
1016
|
+
const selectedStencil =
|
|
1017
|
+
(selectedPass === undefined ? entry.stencilReference : selectedPass.stencilReference) ??
|
|
1018
|
+
0;
|
|
1019
|
+
if (selectedStencil !== lastStencilReference) {
|
|
1020
|
+
pass.setStencilReference(selectedStencil);
|
|
1021
|
+
lastStencilReference = selectedStencil;
|
|
994
1022
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1023
|
+
// feat-city-glb Bug 5 (per-submesh transparency): in the LDR split, a
|
|
1024
|
+
// transparent submesh is drawn in the blend sub-pass (non-sRGB view),
|
|
1025
|
+
// NOT here in the sRGB geometry pass. Skip it. Opaque submeshes of the
|
|
1026
|
+
// same (mixed) mesh still draw here. Single-material / fully-opaque
|
|
1027
|
+
// meshes are unaffected (their submesh materials are not transparent).
|
|
1028
|
+
if (
|
|
1029
|
+
splitLdrSprite &&
|
|
1030
|
+
(selectedPass === undefined
|
|
1031
|
+
? submeshMaterial.transparent === true
|
|
1032
|
+
: selectedRenderState?.blend !== undefined)
|
|
1033
|
+
) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
// bug-20260610 layer 7d: per-submesh BG construction. Texture
|
|
1037
|
+
// views resolve from `matsForRebind[smIdx]` so the j-th submesh
|
|
1038
|
+
// sees its own materials[j] textures (baseColor / MR / normal /
|
|
1039
|
+
// emissive / occlusion). Pick slot j when materials.length covers
|
|
1040
|
+
// smIdx; otherwise fall back to slot 0 (count-mismatch already
|
|
1041
|
+
// filtered by extract; this guard handles the materials.length=1
|
|
1042
|
+
// single-material path mapped over multi-submesh meshes safely).
|
|
1043
|
+
// This BG drops entityKey: identical-texture-set submeshes
|
|
1044
|
+
// (whether on the same entity or different ones) share one BG via
|
|
1045
|
+
// the shaderId-outer `materialBgShared` cache. The 14 handle
|
|
1046
|
+
// objects form the WeakMap chain and fully discriminate the
|
|
1047
|
+
// binding state since sampler/textureView/buffer handle identities
|
|
1048
|
+
// are stable across frames.
|
|
1049
|
+
//
|
|
1050
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13:
|
|
1051
|
+
// sprite materials now use the same per-submesh BG construction
|
|
1052
|
+
// (the sprite-specific BG branch above is deleted; sprite per-
|
|
1053
|
+
// submesh single-slot is still enforced via materialSlotIndices
|
|
1054
|
+
// and the sprite-shaped paramSnapshot fills the PBR-shaped BGL
|
|
1055
|
+
// bindings via fallback textures for the 4 unused slots).
|
|
1056
|
+
// feat-city-glb Bug 5: per-submesh material BG assembly extracted to
|
|
1057
|
+
// the shared `buildPerSubmeshMaterialBg` closure (also called by the
|
|
1058
|
+
// LDR blend sub-pass). Resolves the shader's user-region textures
|
|
1059
|
+
// (baseColor/MR/normal + a custom Nth texture), emissive/occlusion
|
|
1060
|
+
// injection, and Skylight merge; deduped cross-entity via the
|
|
1061
|
+
// shaderId-outer `materialBgShared` cache.
|
|
1062
|
+
const materialSlot =
|
|
1063
|
+
materialSlotIndices[i]?.[matSlotIdx] ?? materialSlotIndices[i]?.[0] ?? 0;
|
|
1064
|
+
const processValue = (
|
|
1065
|
+
globalThis as {
|
|
1066
|
+
process?: { env?: Record<string, string | undefined> };
|
|
1067
|
+
}
|
|
1068
|
+
).process;
|
|
1069
|
+
if (
|
|
1070
|
+
processValue?.env?.FORGEAX_MATERIAL_PIPELINE_DIAGNOSTICS === '1' &&
|
|
1071
|
+
(submeshMaterial.materialShaderId === 'forgeax::default-standard-pbr' ||
|
|
1072
|
+
submeshMaterial.materialShaderId === 'forgeax::pbr-skin')
|
|
1073
|
+
) {
|
|
1074
|
+
console.error(
|
|
1075
|
+
`[render-material] draw receipt: ${JSON.stringify({
|
|
1076
|
+
entityIndex: entry.renderableIndex,
|
|
1014
1077
|
materialSlot,
|
|
1015
|
-
submeshMaterial,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
)
|
|
1019
|
-
|
|
1020
|
-
|
|
1078
|
+
materialHandle: submeshMaterial.materialHandle,
|
|
1079
|
+
clearcoat: submeshMaterial.paramSnapshot?.clearcoat,
|
|
1080
|
+
shader: submeshMaterial.materialShaderId,
|
|
1081
|
+
})}`,
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
perSubmeshBg =
|
|
1085
|
+
profilePhase === undefined
|
|
1086
|
+
? resolveMaterialBindGroup(
|
|
1021
1087
|
materialSlot,
|
|
1022
1088
|
submeshMaterial,
|
|
1023
1089
|
entry.source.entityKey,
|
|
1024
1090
|
entry.world ?? c.world,
|
|
1025
|
-
|
|
1091
|
+
selectedShaderId,
|
|
1092
|
+
)
|
|
1093
|
+
: profileGeometrySegment(c, passKind, 'material-bind-groups', () =>
|
|
1094
|
+
resolveMaterialBindGroup(
|
|
1095
|
+
materialSlot,
|
|
1096
|
+
submeshMaterial,
|
|
1097
|
+
entry.source.entityKey,
|
|
1098
|
+
entry.world ?? c.world,
|
|
1099
|
+
selectedShaderId,
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
materialGroup1DynamicOffsets[0] = materialSlot * MATERIAL_PER_ENTITY_STRIDE;
|
|
1103
|
+
pass.setBindGroup(1, perSubmeshBg, materialGroup1DynamicOffsets, 0, 1);
|
|
1104
|
+
const smTopology = sm.topology;
|
|
1105
|
+
const smMaterialShaderId =
|
|
1106
|
+
selectedPass !== undefined
|
|
1107
|
+
? selectedShaderId
|
|
1108
|
+
: entry.source.skin !== undefined
|
|
1109
|
+
? isStandardPbrSkinMaterialShader(submeshMaterial.materialShaderId)
|
|
1110
|
+
? submeshMaterial.materialShaderId
|
|
1111
|
+
: SKIN_MATERIAL_SHADER_ID
|
|
1112
|
+
: submeshMaterial.materialShaderId;
|
|
1113
|
+
const probeBlendAvailable =
|
|
1114
|
+
probeBlendRecordAvailable && isStandardPbrMaterialShader(smMaterialShaderId);
|
|
1115
|
+
const isSpriteShader =
|
|
1116
|
+
smMaterialShaderId === 'forgeax::sprite' || smMaterialShaderId === 'forgeax::sprite-lit';
|
|
1117
|
+
// Preserve the dedicated sprite-pass state when the linear-LDR graph
|
|
1118
|
+
// routes a sprite through this generic geometry path. Negative scale
|
|
1119
|
+
// flips winding, so back-face culling would erase the quad.
|
|
1120
|
+
const basePipelineRenderState = isSpriteShader
|
|
1121
|
+
? {
|
|
1122
|
+
...selectedRenderState,
|
|
1123
|
+
depthWriteEnabled: false,
|
|
1124
|
+
depthCompare: 'less-equal' as const,
|
|
1125
|
+
cullMode: 'none' as const,
|
|
1126
|
+
blend: selectedRenderState?.blend ?? SPRITE_PREMULTIPLIED_ALPHA_BLEND,
|
|
1127
|
+
}
|
|
1128
|
+
: geometryRenderStateForTopology(smTopology, selectedRenderState);
|
|
1129
|
+
const pipelineRenderState = geometryRenderStateForPass(basePipelineRenderState, passKind);
|
|
1130
|
+
let smPipelineHandle: typeof pipelineState.unlitPipeline;
|
|
1131
|
+
let materialGroup2Contract: 'mesh' | 'skin' | 'cluster' | 'skin-cluster' | undefined;
|
|
1132
|
+
let materialPipelineEntry: MaterialShaderPipelineEntry | null = null;
|
|
1133
|
+
if (smMaterialShaderId === undefined || smMaterialShaderId === 'forgeax::default-unlit') {
|
|
1134
|
+
const unlitShaderId = smMaterialShaderId ?? 'forgeax::default-unlit';
|
|
1135
|
+
const unlitProjectionVariantResult = variantSetFromVertexLayoutProjection(
|
|
1136
|
+
entry.mesh.layoutProjection,
|
|
1137
|
+
undefined,
|
|
1138
|
+
);
|
|
1139
|
+
if (!unlitProjectionVariantResult.ok) {
|
|
1140
|
+
runtime.errorRegistry.fire(unlitProjectionVariantResult.error);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const unlitHasColor =
|
|
1144
|
+
unlitProjectionVariantResult.value === 'VERTEX_COLOR_AVAILABLE=true';
|
|
1145
|
+
const unlitVariantSet = standardStorageVariantSet(
|
|
1146
|
+
runtime.device.caps.storageBuffer,
|
|
1147
|
+
unlitHasColor,
|
|
1148
|
+
);
|
|
1149
|
+
const unlitRsp = resolveMaterialPipeline(
|
|
1150
|
+
unlitShaderId,
|
|
1151
|
+
pipelineRenderState,
|
|
1152
|
+
smTopology,
|
|
1153
|
+
entry.mesh.indexFormat,
|
|
1154
|
+
unlitVariantSet,
|
|
1155
|
+
colorFormatOverride,
|
|
1156
|
+
entry.mesh.layoutProjection,
|
|
1157
|
+
selectedPass?.vertexEntry,
|
|
1158
|
+
selectedPass?.fragmentEntry,
|
|
1159
|
+
);
|
|
1160
|
+
materialPipelineEntry = unlitRsp;
|
|
1161
|
+
smPipelineHandle =
|
|
1162
|
+
unlitRsp?.pipeline ??
|
|
1163
|
+
(colorFormatOverride === undefined
|
|
1164
|
+
? selectGeometryPipeline(pipelineState, isHdrTarget, msaaActive)
|
|
1165
|
+
: null);
|
|
1166
|
+
} else if (smMaterialShaderId !== undefined) {
|
|
1167
|
+
// feat-20260609 M4.5 / w38 (D-11): the variantSet handed to
|
|
1168
|
+
// getMaterialShaderPipeline MUST mirror the boot-time
|
|
1169
|
+
// `definesKey` rule at createRenderer.ts:2483-2485 (sortedEntries
|
|
1170
|
+
// .every(v=>v===true) ? '' : 'A=v+...'). manifest variant.definesKey
|
|
1171
|
+
// is `''` for the all-true variant, so HDRP (both axes true) must
|
|
1172
|
+
// pass the canonical empty key to hit that variant via
|
|
1173
|
+
// findVariantByKey. Passing the expanded form would produce a
|
|
1174
|
+
// miss and silently fall back to the registered default WGSL,
|
|
1175
|
+
// creating a layout/binding mismatch under HDRP.
|
|
1176
|
+
//
|
|
1177
|
+
// URP path passes the explicit expanded form because the URP
|
|
1178
|
+
// variant's manifest definesKey IS that exact non-empty string
|
|
1179
|
+
// (CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true)
|
|
1180
|
+
// -- the canonical-empty rule only applies to the all-true case.
|
|
1181
|
+
const hasVertexColor = entry.mesh.layoutProjection.attributes.some(
|
|
1182
|
+
(attribute) => attribute.key === 'color',
|
|
1183
|
+
);
|
|
1184
|
+
// The all-true HDRP variant uses the canonical empty key only when
|
|
1185
|
+
// every variant axis is true. A plain mesh adds the geometry-owned
|
|
1186
|
+
// VERTEX_COLOR_AVAILABLE=false axis, so retain the two capability
|
|
1187
|
+
// axes explicitly or the selector would silently choose the URP
|
|
1188
|
+
// group(2) layout for an HDRP cluster bind group.
|
|
1189
|
+
const capabilityVariantSet = standardTopologyVariantSet(
|
|
1190
|
+
c.standardLighting,
|
|
1191
|
+
runtime.device.caps.storageBuffer,
|
|
1192
|
+
hasVertexColor,
|
|
1193
|
+
probeBlendAvailable,
|
|
1194
|
+
);
|
|
1195
|
+
const transmissionAvailable = requestsStandardTransmissionVariant(submeshMaterial);
|
|
1196
|
+
const materialCapabilityVariantSet =
|
|
1197
|
+
transmissionAvailable && !capabilityVariantSet.includes('TRANSMISSION_AVAILABLE=')
|
|
1198
|
+
? `${capabilityVariantSet === '' ? '' : `${capabilityVariantSet}+`}TRANSMISSION_AVAILABLE=true`
|
|
1199
|
+
: capabilityVariantSet;
|
|
1200
|
+
// Sprite's base request must retain the boot-selected artifact. Only
|
|
1201
|
+
// SpriteInstances uses the explicit empty key for the
|
|
1202
|
+
// PER_INSTANCE_REGION=true variant; synthesizing the generic PBR
|
|
1203
|
+
// capability axes for a base sprite would invent a lazy module label
|
|
1204
|
+
// that has no boot seed and skip the first HDR draw.
|
|
1205
|
+
const variantSet = isSpriteShader
|
|
1206
|
+
? smMaterialShaderId === 'forgeax::sprite' && entry.source.spriteInstances !== undefined
|
|
1207
|
+
? ''
|
|
1208
|
+
: undefined
|
|
1209
|
+
: entry.variantSet === undefined
|
|
1210
|
+
? materialCapabilityVariantSet
|
|
1211
|
+
: capabilityVariantSet === ''
|
|
1212
|
+
? entry.variantSet
|
|
1213
|
+
: `${capabilityVariantSet}+${entry.variantSet}`;
|
|
1214
|
+
const cachedPipeline = resolveMaterialPipeline(
|
|
1215
|
+
smMaterialShaderId,
|
|
1216
|
+
pipelineRenderState,
|
|
1217
|
+
smTopology,
|
|
1218
|
+
entry.mesh.indexFormat,
|
|
1219
|
+
variantSet,
|
|
1220
|
+
colorFormatOverride,
|
|
1221
|
+
entry.mesh.layoutProjection,
|
|
1222
|
+
selectedPass?.vertexEntry,
|
|
1223
|
+
selectedPass?.fragmentEntry,
|
|
1224
|
+
);
|
|
1225
|
+
// feat-20260615-pipeline-spec-ssot M6-T1: cache miss resolves to
|
|
1226
|
+
// null uniformly across URP / HDRP / skin shaders. Charter P3
|
|
1227
|
+
// explicit failure: the pre-M6 URP-path silent fallback to the
|
|
1228
|
+
// boot-time `pipelineState.standardPipeline*` (M4.5-followup w43)
|
|
1229
|
+
// masked real PipelineSpecError build failures behind a
|
|
1230
|
+
// layout-compatible-but-wrong PSO. The per-submesh
|
|
1231
|
+
// `if (smPipelineHandle === null) continue` skip-draw (which
|
|
1232
|
+
// already covered HDRP-active and skin miss paths) is now the
|
|
1233
|
+
// single uniform recovery shape -- one frame of skip-draw on
|
|
1234
|
+
// first-touch, then the cached PSO flows in once the async
|
|
1235
|
+
// build resolves. The pre-loop skin-PSO probe still skips the
|
|
1236
|
+
// entire entry on first-submesh probe miss; this site only
|
|
1237
|
+
// fires on per-submesh topology variance miss.
|
|
1238
|
+
materialPipelineEntry = cachedPipeline;
|
|
1239
|
+
smPipelineHandle = cachedPipeline?.pipeline ?? null;
|
|
1240
|
+
} else {
|
|
1241
|
+
smPipelineHandle = selectGeometryPipeline(pipelineState, isHdrTarget, msaaActive);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (smPipelineHandle === null) {
|
|
1245
|
+
if (diagnosticsEnabled) {
|
|
1246
|
+
console.error(
|
|
1247
|
+
`[render-material] draw skipped: ${JSON.stringify({
|
|
1248
|
+
entityKey: entry.source.entityKey,
|
|
1249
|
+
materialHandle: submeshMaterial.materialHandle,
|
|
1250
|
+
shader: smMaterialShaderId,
|
|
1251
|
+
reason: 'pipeline-unavailable',
|
|
1252
|
+
})}`,
|
|
1026
1253
|
);
|
|
1027
|
-
materialGroup1DynamicOffsets[0] = materialSlot * MATERIAL_PER_ENTITY_STRIDE;
|
|
1028
|
-
pass.setBindGroup(1, perSubmeshBg, materialGroup1DynamicOffsets, 0, 1);
|
|
1029
|
-
const smTopology = sm.topology;
|
|
1030
|
-
const smMaterialShaderId =
|
|
1031
|
-
entry.source.skin !== undefined
|
|
1032
|
-
? isStandardPbrSkinMaterialShader(submeshMaterial.materialShaderId)
|
|
1033
|
-
? submeshMaterial.materialShaderId
|
|
1034
|
-
: SKIN_MATERIAL_SHADER_ID
|
|
1035
|
-
: submeshMaterial.materialShaderId;
|
|
1036
|
-
const probeBlendAvailable =
|
|
1037
|
-
probeBlendRecordAvailable && isStandardPbrMaterialShader(smMaterialShaderId);
|
|
1038
|
-
const isSpriteShader =
|
|
1039
|
-
smMaterialShaderId === 'forgeax::sprite' || smMaterialShaderId === 'forgeax::sprite-lit';
|
|
1040
|
-
// Preserve the dedicated sprite-pass state when the linear-LDR graph
|
|
1041
|
-
// routes a sprite through this generic geometry path. Negative scale
|
|
1042
|
-
// flips winding, so back-face culling would erase the quad.
|
|
1043
|
-
const basePipelineRenderState = isSpriteShader
|
|
1044
|
-
? {
|
|
1045
|
-
...submeshMaterial.renderState,
|
|
1046
|
-
depthWriteEnabled: false,
|
|
1047
|
-
depthCompare: 'less-equal' as const,
|
|
1048
|
-
cullMode: 'none' as const,
|
|
1049
|
-
blend: submeshMaterial.renderState?.blend ?? SPRITE_PREMULTIPLIED_ALPHA_BLEND,
|
|
1050
1254
|
}
|
|
1051
|
-
: geometryRenderStateForTopology(smTopology, submeshMaterial.renderState);
|
|
1052
|
-
const pipelineRenderState = geometryRenderStateForPass(basePipelineRenderState, passKind);
|
|
1053
|
-
let smPipelineHandle: typeof pipelineState.unlitPipeline;
|
|
1054
|
-
let materialGroup2Contract: 'mesh' | 'skin' | 'cluster' | 'skin-cluster' | undefined;
|
|
1055
|
-
let materialPipelineEntry: MaterialShaderPipelineEntry | null = null;
|
|
1056
|
-
if (smMaterialShaderId === undefined || smMaterialShaderId === 'forgeax::default-unlit') {
|
|
1057
|
-
const unlitShaderId = smMaterialShaderId ?? 'forgeax::default-unlit';
|
|
1058
|
-
const unlitProjectionVariantResult = variantSetFromVertexLayoutProjection(
|
|
1059
|
-
entry.mesh.layoutProjection,
|
|
1060
|
-
undefined,
|
|
1061
|
-
);
|
|
1062
|
-
if (!unlitProjectionVariantResult.ok) {
|
|
1063
|
-
runtime.errorRegistry.fire(unlitProjectionVariantResult.error);
|
|
1064
1255
|
continue;
|
|
1065
1256
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
entry.mesh.indexFormat,
|
|
1076
|
-
unlitVariantSet,
|
|
1077
|
-
colorFormatOverride,
|
|
1078
|
-
entry.mesh.layoutProjection,
|
|
1079
|
-
);
|
|
1080
|
-
materialPipelineEntry = unlitRsp;
|
|
1081
|
-
smPipelineHandle =
|
|
1082
|
-
unlitRsp?.pipeline ??
|
|
1083
|
-
(colorFormatOverride === undefined
|
|
1084
|
-
? selectGeometryPipeline(pipelineState, isHdrTarget, msaaActive)
|
|
1085
|
-
: null);
|
|
1086
|
-
} else if (smMaterialShaderId !== undefined) {
|
|
1087
|
-
// feat-20260609 M4.5 / w38 (D-11): the variantSet handed to
|
|
1088
|
-
// getMaterialShaderPipeline MUST mirror the boot-time
|
|
1089
|
-
// `definesKey` rule at createRenderer.ts:2483-2485 (sortedEntries
|
|
1090
|
-
// .every(v=>v===true) ? '' : 'A=v+...'). manifest variant.definesKey
|
|
1091
|
-
// is `''` for the all-true variant, so HDRP (both axes true) must
|
|
1092
|
-
// pass the canonical empty key to hit that variant via
|
|
1093
|
-
// findVariantByKey. Passing the expanded form would produce a
|
|
1094
|
-
// miss and silently fall back to the registered default WGSL,
|
|
1095
|
-
// creating a layout/binding mismatch under HDRP.
|
|
1096
|
-
//
|
|
1097
|
-
// URP path passes the explicit expanded form because the URP
|
|
1098
|
-
// variant's manifest definesKey IS that exact non-empty string
|
|
1099
|
-
// (CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true)
|
|
1100
|
-
// -- the canonical-empty rule only applies to the all-true case.
|
|
1101
|
-
const hasVertexColor = entry.mesh.layoutProjection.attributes.some(
|
|
1102
|
-
(attribute) => attribute.key === 'color',
|
|
1103
|
-
);
|
|
1104
|
-
// The all-true HDRP variant uses the canonical empty key only when
|
|
1105
|
-
// every variant axis is true. A plain mesh adds the geometry-owned
|
|
1106
|
-
// VERTEX_COLOR_AVAILABLE=false axis, so retain the two capability
|
|
1107
|
-
// axes explicitly or the selector would silently choose the URP
|
|
1108
|
-
// group(2) layout for an HDRP cluster bind group.
|
|
1109
|
-
const capabilityVariantSet = standardTopologyVariantSet(
|
|
1110
|
-
c.standardLighting,
|
|
1111
|
-
runtime.device.caps.storageBuffer,
|
|
1112
|
-
hasVertexColor,
|
|
1257
|
+
// The probe record is an ABI extension of the built-in Standard PBR
|
|
1258
|
+
// shaders only. RenderScene may retain a record on every projected
|
|
1259
|
+
// renderable, but unlit/custom shaders still use the one-binding
|
|
1260
|
+
// instances BGL and must never receive the probe bind group. Resolve
|
|
1261
|
+
// this per submesh so mixed-material meshes cannot cross the layouts.
|
|
1262
|
+
const instanceDraws = resolveGeometryInstanceBuffer(
|
|
1263
|
+
c,
|
|
1264
|
+
entry,
|
|
1265
|
+
identityInstanceDraws,
|
|
1113
1266
|
probeBlendAvailable,
|
|
1114
1267
|
);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
: `${capabilityVariantSet}+${entry.variantSet}`;
|
|
1134
|
-
const cachedPipeline = resolveMaterialPipeline(
|
|
1135
|
-
smMaterialShaderId,
|
|
1136
|
-
pipelineRenderState,
|
|
1137
|
-
smTopology,
|
|
1138
|
-
entry.mesh.indexFormat,
|
|
1139
|
-
variantSet,
|
|
1140
|
-
colorFormatOverride,
|
|
1141
|
-
entry.mesh.layoutProjection,
|
|
1142
|
-
);
|
|
1143
|
-
// feat-20260615-pipeline-spec-ssot M6-T1: cache miss resolves to
|
|
1144
|
-
// null uniformly across URP / HDRP / skin shaders. Charter P3
|
|
1145
|
-
// explicit failure: the pre-M6 URP-path silent fallback to the
|
|
1146
|
-
// boot-time `pipelineState.standardPipeline*` (M4.5-followup w43)
|
|
1147
|
-
// masked real PipelineSpecError build failures behind a
|
|
1148
|
-
// layout-compatible-but-wrong PSO. The per-submesh
|
|
1149
|
-
// `if (smPipelineHandle === null) continue` skip-draw (which
|
|
1150
|
-
// already covered HDRP-active and skin miss paths) is now the
|
|
1151
|
-
// single uniform recovery shape -- one frame of skip-draw on
|
|
1152
|
-
// first-touch, then the cached PSO flows in once the async
|
|
1153
|
-
// build resolves. The pre-loop skin-PSO probe still skips the
|
|
1154
|
-
// entire entry on first-submesh probe miss; this site only
|
|
1155
|
-
// fires on per-submesh topology variance miss.
|
|
1156
|
-
materialPipelineEntry = cachedPipeline;
|
|
1157
|
-
smPipelineHandle = cachedPipeline?.pipeline ?? null;
|
|
1158
|
-
} else {
|
|
1159
|
-
smPipelineHandle = selectGeometryPipeline(pipelineState, isHdrTarget, msaaActive);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
if (smPipelineHandle === null) {
|
|
1163
|
-
if (diagnosticsEnabled) {
|
|
1268
|
+
if (instanceDraws === null) continue;
|
|
1269
|
+
// Standard clustered PBR variants declare the unified cluster/SSAO
|
|
1270
|
+
// group(2) layout. Unlit and other URP-layout materials must bind the
|
|
1271
|
+
// ordinary mesh group instead; choosing one group for the whole pass
|
|
1272
|
+
// makes Dawn reject an otherwise valid unlit pipeline and invalidates
|
|
1273
|
+
// the command buffer before it reaches the surface.
|
|
1274
|
+
if (!isSkinEntry) {
|
|
1275
|
+
materialGroup2Contract = materialPipelineEntry?.group2Contract;
|
|
1276
|
+
if (materialGroup2Contract === undefined) continue;
|
|
1277
|
+
const selectedGroup = selectMaterialGroup2(
|
|
1278
|
+
meshGroup2,
|
|
1279
|
+
meshBindGroup,
|
|
1280
|
+
materialGroup2Contract,
|
|
1281
|
+
);
|
|
1282
|
+
if (selectedGroup === null) continue;
|
|
1283
|
+
group2BindGroup = selectedGroup;
|
|
1284
|
+
}
|
|
1285
|
+
if (diagnosticsEnabled && submeshMaterial.textureHandles !== undefined) {
|
|
1164
1286
|
console.error(
|
|
1165
|
-
`[render-material] draw
|
|
1287
|
+
`[render-material] draw submitted: ${JSON.stringify({
|
|
1166
1288
|
entityKey: entry.source.entityKey,
|
|
1167
1289
|
materialHandle: submeshMaterial.materialHandle,
|
|
1168
1290
|
shader: smMaterialShaderId,
|
|
1169
|
-
|
|
1291
|
+
textureHandles: [...submeshMaterial.textureHandles.entries()].map(
|
|
1292
|
+
([field, handle]) => ({
|
|
1293
|
+
field,
|
|
1294
|
+
handle,
|
|
1295
|
+
}),
|
|
1296
|
+
),
|
|
1297
|
+
skin: isSkinEntry
|
|
1298
|
+
? {
|
|
1299
|
+
resourcesReady: skinResources !== null,
|
|
1300
|
+
psoReady: skinPsoProbe !== null,
|
|
1301
|
+
paletteBufferReady: skinSlice?.buffer !== undefined,
|
|
1302
|
+
paletteByteOffset: skinSlice?.byteOffset,
|
|
1303
|
+
dynamicOffsets: [...group2DynamicOffsets],
|
|
1304
|
+
}
|
|
1305
|
+
: undefined,
|
|
1306
|
+
pipelineReady: true,
|
|
1170
1307
|
})}`,
|
|
1171
1308
|
);
|
|
1172
1309
|
}
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1175
|
-
// The probe record is an ABI extension of the built-in Standard PBR
|
|
1176
|
-
// shaders only. RenderScene may retain a record on every projected
|
|
1177
|
-
// renderable, but unlit/custom shaders still use the one-binding
|
|
1178
|
-
// instances BGL and must never receive the probe bind group. Resolve
|
|
1179
|
-
// this per submesh so mixed-material meshes cannot cross the layouts.
|
|
1180
|
-
const instanceDraws = resolveGeometryInstanceBuffer(
|
|
1181
|
-
c,
|
|
1182
|
-
entry,
|
|
1183
|
-
identityInstanceDraws,
|
|
1184
|
-
probeBlendAvailable,
|
|
1185
|
-
);
|
|
1186
|
-
if (instanceDraws === null) continue;
|
|
1187
|
-
// Standard clustered PBR variants declare the unified cluster/SSAO
|
|
1188
|
-
// group(2) layout. Unlit and other URP-layout materials must bind the
|
|
1189
|
-
// ordinary mesh group instead; choosing one group for the whole pass
|
|
1190
|
-
// makes Dawn reject an otherwise valid unlit pipeline and invalidates
|
|
1191
|
-
// the command buffer before it reaches the surface.
|
|
1192
|
-
if (!isSkinEntry) {
|
|
1193
|
-
materialGroup2Contract = materialPipelineEntry?.group2Contract;
|
|
1194
|
-
if (materialGroup2Contract === undefined) continue;
|
|
1195
|
-
const selectedGroup = selectMaterialGroup2(
|
|
1196
|
-
meshGroup2,
|
|
1197
|
-
meshBindGroup,
|
|
1198
|
-
materialGroup2Contract,
|
|
1199
|
-
);
|
|
1200
|
-
if (selectedGroup === null) continue;
|
|
1201
|
-
group2BindGroup = selectedGroup;
|
|
1202
|
-
}
|
|
1203
|
-
if (diagnosticsEnabled && submeshMaterial.textureHandles !== undefined) {
|
|
1204
|
-
console.error(
|
|
1205
|
-
`[render-material] draw submitted: ${JSON.stringify({
|
|
1206
|
-
entityKey: entry.source.entityKey,
|
|
1207
|
-
materialHandle: submeshMaterial.materialHandle,
|
|
1208
|
-
shader: smMaterialShaderId,
|
|
1209
|
-
textureHandles: [...submeshMaterial.textureHandles.entries()].map(
|
|
1210
|
-
([field, handle]) => ({
|
|
1211
|
-
field,
|
|
1212
|
-
handle,
|
|
1213
|
-
}),
|
|
1214
|
-
),
|
|
1215
|
-
skin: isSkinEntry
|
|
1216
|
-
? {
|
|
1217
|
-
resourcesReady: skinResources !== null,
|
|
1218
|
-
psoReady: skinPsoProbe !== null,
|
|
1219
|
-
paletteBufferReady: skinSlice?.buffer !== undefined,
|
|
1220
|
-
paletteByteOffset: skinSlice?.byteOffset,
|
|
1221
|
-
dynamicOffsets: [...group2DynamicOffsets],
|
|
1222
|
-
}
|
|
1223
|
-
: undefined,
|
|
1224
|
-
pipelineReady: true,
|
|
1225
|
-
})}`,
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
pass.setBindGroup(2, group2BindGroup, group2DynamicOffsets);
|
|
1310
|
+
pass.setBindGroup(2, group2BindGroup, group2DynamicOffsets);
|
|
1229
1311
|
|
|
1230
|
-
|
|
1231
|
-
recordOcclusionCandidate(() =>
|
|
1232
|
-
submitSubmeshDraws(
|
|
1233
|
-
pass,
|
|
1234
|
-
bindingState,
|
|
1235
|
-
smPipelineHandle,
|
|
1236
|
-
instanceDraws,
|
|
1237
|
-
entry.mesh.indexed,
|
|
1238
|
-
sm.indexCount,
|
|
1239
|
-
sm.vertexCount,
|
|
1240
|
-
sm.indexOffset,
|
|
1241
|
-
),
|
|
1242
|
-
);
|
|
1243
|
-
} else {
|
|
1244
|
-
profileGeometrySegment(c, passKind, 'draw-submit', () =>
|
|
1312
|
+
if (profilePhase === undefined) {
|
|
1245
1313
|
recordOcclusionCandidate(() =>
|
|
1246
1314
|
submitSubmeshDraws(
|
|
1247
1315
|
pass,
|
|
@@ -1253,11 +1321,26 @@ export function recordGeometryDraws(
|
|
|
1253
1321
|
sm.vertexCount,
|
|
1254
1322
|
sm.indexOffset,
|
|
1255
1323
|
),
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1324
|
+
);
|
|
1325
|
+
} else {
|
|
1326
|
+
profileGeometrySegment(c, passKind, 'draw-submit', () =>
|
|
1327
|
+
recordOcclusionCandidate(() =>
|
|
1328
|
+
submitSubmeshDraws(
|
|
1329
|
+
pass,
|
|
1330
|
+
bindingState,
|
|
1331
|
+
smPipelineHandle,
|
|
1332
|
+
instanceDraws,
|
|
1333
|
+
entry.mesh.indexed,
|
|
1334
|
+
sm.indexCount,
|
|
1335
|
+
sm.vertexCount,
|
|
1336
|
+
sm.indexOffset,
|
|
1337
|
+
),
|
|
1338
|
+
),
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (diagnosticsEnabled && perSubmeshBg !== null) {
|
|
1342
|
+
recordIblPipelineBinding(c, submeshMaterial, smPipelineHandle, perSubmeshBg);
|
|
1343
|
+
}
|
|
1261
1344
|
}
|
|
1262
1345
|
}
|
|
1263
1346
|
}
|
|
@@ -1268,8 +1351,285 @@ export function recordGeometryDraws(
|
|
|
1268
1351
|
*
|
|
1269
1352
|
* @internal
|
|
1270
1353
|
*/
|
|
1271
|
-
function
|
|
1272
|
-
c:
|
|
1354
|
+
function uploadInstanceRanges(
|
|
1355
|
+
c: Pick<
|
|
1356
|
+
_InternalRenderPipelineContext,
|
|
1357
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1358
|
+
>,
|
|
1359
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1360
|
+
buffer: Buffer,
|
|
1361
|
+
ranges: readonly { readonly start: number; readonly end: number }[],
|
|
1362
|
+
storageBacked: boolean,
|
|
1363
|
+
sourceOffsetInstances = 0,
|
|
1364
|
+
): InstanceUploadResult {
|
|
1365
|
+
const strideBytes =
|
|
1366
|
+
(storageBacked ? INSTANCE_STORAGE_STRIDE_FLOATS : 16) * Float32Array.BYTES_PER_ELEMENT;
|
|
1367
|
+
const uploadedRanges: { start: number; end: number }[] = [];
|
|
1368
|
+
let uploadedBytes = 0;
|
|
1369
|
+
for (const range of ranges) {
|
|
1370
|
+
const start = Math.max(0, Math.min(inst.instanceCount, range.start));
|
|
1371
|
+
const end = Math.max(start, Math.min(inst.instanceCount, range.end));
|
|
1372
|
+
if (end <= start) continue;
|
|
1373
|
+
const source = inst.transforms.subarray(
|
|
1374
|
+
(sourceOffsetInstances + start) * 16,
|
|
1375
|
+
(sourceOffsetInstances + end) * 16,
|
|
1376
|
+
);
|
|
1377
|
+
const payload = storageBacked ? packInstanceStorageBuffer(source) : source;
|
|
1378
|
+
const written = c.runtime.device.queue.writeBuffer(buffer, start * strideBytes, payload);
|
|
1379
|
+
if (!written.ok) {
|
|
1380
|
+
c.runtime.errorRegistry.fire(written.error);
|
|
1381
|
+
return { ok: false, ranges: uploadedRanges, bytes: uploadedBytes };
|
|
1382
|
+
}
|
|
1383
|
+
uploadedRanges.push({ start, end });
|
|
1384
|
+
uploadedBytes += payload.byteLength;
|
|
1385
|
+
}
|
|
1386
|
+
return { ok: true, ranges: uploadedRanges, bytes: uploadedBytes };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
export function reportInstanceResidency(
|
|
1390
|
+
c: Pick<
|
|
1391
|
+
_InternalRenderPipelineContext,
|
|
1392
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1393
|
+
>,
|
|
1394
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1395
|
+
input: {
|
|
1396
|
+
readonly lane: 'direct-storage' | 'chunked-storage' | 'direct-uniform' | 'chunked-uniform';
|
|
1397
|
+
readonly requestedBytes: number;
|
|
1398
|
+
readonly supportedBytes: number | undefined;
|
|
1399
|
+
readonly uploadRanges: readonly { readonly start: number; readonly end: number }[];
|
|
1400
|
+
readonly uploadedBytes: number;
|
|
1401
|
+
},
|
|
1402
|
+
): void {
|
|
1403
|
+
if (inst.collectionId === undefined || c.frameState.instanceResidency === undefined) return;
|
|
1404
|
+
recordInstanceResidency(c.frameState.instanceResidency, {
|
|
1405
|
+
collectionId: inst.collectionId,
|
|
1406
|
+
frameNumber: c.frameState.frameNumber,
|
|
1407
|
+
residentGeneration: c.runtime.deviceScope.generation,
|
|
1408
|
+
lane: input.lane,
|
|
1409
|
+
requestedBytes: input.requestedBytes,
|
|
1410
|
+
supportedBytes: input.supportedBytes,
|
|
1411
|
+
uploadRanges: input.uploadRanges,
|
|
1412
|
+
uploadedBytes: input.uploadedBytes,
|
|
1413
|
+
backend: c.runtime.device.caps.backendKind,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function reportInstanceFailure(
|
|
1418
|
+
c: Pick<
|
|
1419
|
+
_InternalRenderPipelineContext,
|
|
1420
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1421
|
+
>,
|
|
1422
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1423
|
+
error: {
|
|
1424
|
+
readonly code: InstanceCollectionFailureCode;
|
|
1425
|
+
readonly expected: string;
|
|
1426
|
+
readonly hint: string;
|
|
1427
|
+
},
|
|
1428
|
+
requestedBytes: number,
|
|
1429
|
+
supportedBytes: number | undefined,
|
|
1430
|
+
): void {
|
|
1431
|
+
if (inst.collectionId === undefined || c.frameState.instanceResidency === undefined) return;
|
|
1432
|
+
recordInstanceFailure(c.frameState.instanceResidency, {
|
|
1433
|
+
collectionId: inst.collectionId,
|
|
1434
|
+
code: error.code,
|
|
1435
|
+
expected: error.expected,
|
|
1436
|
+
hint: error.hint,
|
|
1437
|
+
facts: {
|
|
1438
|
+
requestedBytes,
|
|
1439
|
+
supportedBytes,
|
|
1440
|
+
backend: c.runtime.device.caps.backendKind,
|
|
1441
|
+
owner: 'renderer.instances',
|
|
1442
|
+
cause: error.code,
|
|
1443
|
+
recovery: error.hint,
|
|
1444
|
+
},
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function clearInstanceChunksForOwner(
|
|
1449
|
+
c: Pick<
|
|
1450
|
+
_InternalRenderPipelineContext,
|
|
1451
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1452
|
+
>,
|
|
1453
|
+
ownerKey: number,
|
|
1454
|
+
): void {
|
|
1455
|
+
const chunks = c.frameState.instanceBufferChunks;
|
|
1456
|
+
if (chunks === undefined) return;
|
|
1457
|
+
const prefix = `${ownerKey}:`;
|
|
1458
|
+
for (const [key, entry] of chunks.entries()) {
|
|
1459
|
+
if (!key.startsWith(prefix)) continue;
|
|
1460
|
+
if (!entry.buffer.isDestroyed) {
|
|
1461
|
+
const destroyed = entry.buffer.destroy();
|
|
1462
|
+
if (!destroyed.ok) c.runtime.errorRegistry.fire(destroyed.error);
|
|
1463
|
+
}
|
|
1464
|
+
chunks.delete(key);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
export function resolveStorageInstanceChunks(
|
|
1469
|
+
c: Pick<
|
|
1470
|
+
_InternalRenderPipelineContext,
|
|
1471
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1472
|
+
>,
|
|
1473
|
+
entry: _InternalRenderPipelineContext['validatedOrdered'][number],
|
|
1474
|
+
inst: NonNullable<RenderableSnapshot['instances']>,
|
|
1475
|
+
cap: number,
|
|
1476
|
+
probeBuffer: Buffer | undefined,
|
|
1477
|
+
probeOffset: number,
|
|
1478
|
+
): readonly GeometryInstanceDraw[] | null {
|
|
1479
|
+
const { runtime, frameState } = c;
|
|
1480
|
+
const strideBytes = INSTANCE_STORAGE_STRIDE_FLOATS * Float32Array.BYTES_PER_ELEMENT;
|
|
1481
|
+
const chunkCapacity = Math.floor(cap / strideBytes);
|
|
1482
|
+
if (chunkCapacity < 1) {
|
|
1483
|
+
reportInstanceFailure(
|
|
1484
|
+
c,
|
|
1485
|
+
inst,
|
|
1486
|
+
{
|
|
1487
|
+
code: 'limit-exceeded',
|
|
1488
|
+
expected: `maxStorageBufferBindingSize (${cap}) >= ${strideBytes}`,
|
|
1489
|
+
hint: 'use a backend with storage-buffer bindings large enough for one InstanceData record',
|
|
1490
|
+
},
|
|
1491
|
+
inst.instanceCount * strideBytes,
|
|
1492
|
+
cap,
|
|
1493
|
+
);
|
|
1494
|
+
runtime.errorRegistry.fire(
|
|
1495
|
+
new RhiError({
|
|
1496
|
+
code: 'limit-exceeded',
|
|
1497
|
+
expected: `maxStorageBufferBindingSize (${cap}) >= ${strideBytes}`,
|
|
1498
|
+
hint: 'use a backend with storage-buffer bindings large enough for one InstanceData record',
|
|
1499
|
+
detail: { maxStorageBufferBindingSize: cap, requestedBytes: strideBytes },
|
|
1500
|
+
}),
|
|
1501
|
+
);
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
const ownerKey = instanceCollectionCacheKey(entry.source.worldId, inst);
|
|
1505
|
+
const chunks = frameState.instanceBufferChunks;
|
|
1506
|
+
const activeKeys = new Set<string>();
|
|
1507
|
+
const draws: GeometryInstanceDraw[] = [];
|
|
1508
|
+
const uploadedRanges: { start: number; end: number }[] = [];
|
|
1509
|
+
let uploadedBytes = 0;
|
|
1510
|
+
for (let start = 0; start < inst.instanceCount; start += chunkCapacity) {
|
|
1511
|
+
const end = Math.min(inst.instanceCount, start + chunkCapacity);
|
|
1512
|
+
const count = end - start;
|
|
1513
|
+
const chunkKey = `${ownerKey}:${start}`;
|
|
1514
|
+
activeKeys.add(chunkKey);
|
|
1515
|
+
// Keep stable-resident checks arithmetic-only; packing is deferred to the
|
|
1516
|
+
// upload path below, where a new or dirty range actually needs payload.
|
|
1517
|
+
const payloadBytes = count * strideBytes;
|
|
1518
|
+
const previous = chunks?.get(chunkKey);
|
|
1519
|
+
let active =
|
|
1520
|
+
previous !== undefined &&
|
|
1521
|
+
previous.uploadedArchVersion === inst.archVersion &&
|
|
1522
|
+
previous.uploadedByteLength === payloadBytes
|
|
1523
|
+
? previous
|
|
1524
|
+
: undefined;
|
|
1525
|
+
let activeIsNew = false;
|
|
1526
|
+
if (active === undefined) {
|
|
1527
|
+
const created = runtime.device.createBuffer({
|
|
1528
|
+
size: payloadBytes,
|
|
1529
|
+
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
1530
|
+
mappedAtCreation: false,
|
|
1531
|
+
});
|
|
1532
|
+
if (!created.ok) {
|
|
1533
|
+
runtime.errorRegistry.fire(created.error);
|
|
1534
|
+
reportInstanceFailure(c, inst, created.error, inst.instanceCount * strideBytes, cap);
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
if (previous !== undefined && !previous.buffer.isDestroyed) {
|
|
1538
|
+
const destroyed = previous.buffer.destroy();
|
|
1539
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1540
|
+
}
|
|
1541
|
+
active = {
|
|
1542
|
+
buffer: new GpuBuffer(runtime.device, created.value),
|
|
1543
|
+
uploadedArchVersion: inst.archVersion,
|
|
1544
|
+
uploadedByteLength: payloadBytes,
|
|
1545
|
+
};
|
|
1546
|
+
activeIsNew = true;
|
|
1547
|
+
chunks?.set(chunkKey, active);
|
|
1548
|
+
if (chunks === undefined) frameState.transientInstanceBuffers.push(active);
|
|
1549
|
+
}
|
|
1550
|
+
if (active === undefined) return null;
|
|
1551
|
+
const needsUpload =
|
|
1552
|
+
activeIsNew || active.uploadedRevision !== (inst.revision ?? inst.archVersion);
|
|
1553
|
+
const rangesToUpload = [
|
|
1554
|
+
{
|
|
1555
|
+
start: 0,
|
|
1556
|
+
end: count,
|
|
1557
|
+
},
|
|
1558
|
+
];
|
|
1559
|
+
if (needsUpload && rangesToUpload.length > 0) {
|
|
1560
|
+
const upload = uploadInstanceRanges(
|
|
1561
|
+
c,
|
|
1562
|
+
inst,
|
|
1563
|
+
active.buffer.handle,
|
|
1564
|
+
rangesToUpload,
|
|
1565
|
+
true,
|
|
1566
|
+
start,
|
|
1567
|
+
);
|
|
1568
|
+
for (const range of upload.ranges) {
|
|
1569
|
+
uploadedRanges.push({ start: range.start + start, end: range.end + start });
|
|
1570
|
+
}
|
|
1571
|
+
uploadedBytes += upload.bytes;
|
|
1572
|
+
if (!upload.ok) {
|
|
1573
|
+
reportInstanceFailure(
|
|
1574
|
+
c,
|
|
1575
|
+
inst,
|
|
1576
|
+
{
|
|
1577
|
+
code: 'queue-write-buffer-failed',
|
|
1578
|
+
expected:
|
|
1579
|
+
'the renderer-owned instance storage buffer accepts the complete instance payload',
|
|
1580
|
+
hint: 'retry after the active device is healthy or recover the renderer',
|
|
1581
|
+
},
|
|
1582
|
+
inst.instanceCount * strideBytes,
|
|
1583
|
+
cap,
|
|
1584
|
+
);
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (needsUpload) {
|
|
1589
|
+
{
|
|
1590
|
+
const published = { ...active, uploadedRevision: inst.revision ?? inst.archVersion };
|
|
1591
|
+
active = published;
|
|
1592
|
+
chunks?.set(chunkKey, published);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
const activeBuffer = active;
|
|
1596
|
+
draws.push({
|
|
1597
|
+
instanceBuffer: activeBuffer.buffer.handle,
|
|
1598
|
+
instanceBindGroup: resolveGeometryInstancesBindGroup(
|
|
1599
|
+
c,
|
|
1600
|
+
activeBuffer.buffer.handle,
|
|
1601
|
+
probeBuffer,
|
|
1602
|
+
),
|
|
1603
|
+
probeOffset,
|
|
1604
|
+
instanceCount: count,
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (chunks !== undefined) {
|
|
1608
|
+
const prefix = `${ownerKey}:`;
|
|
1609
|
+
for (const [key, stale] of chunks.entries()) {
|
|
1610
|
+
if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
|
|
1611
|
+
if (!stale.buffer.isDestroyed) {
|
|
1612
|
+
const destroyed = stale.buffer.destroy();
|
|
1613
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1614
|
+
}
|
|
1615
|
+
chunks.delete(key);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
reportInstanceResidency(c, inst, {
|
|
1619
|
+
lane: 'chunked-storage',
|
|
1620
|
+
requestedBytes: inst.instanceCount * strideBytes,
|
|
1621
|
+
supportedBytes: cap,
|
|
1622
|
+
uploadRanges: uploadedRanges,
|
|
1623
|
+
uploadedBytes,
|
|
1624
|
+
});
|
|
1625
|
+
return draws;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
export function resolveGeometryInstanceBuffer(
|
|
1629
|
+
c: Pick<
|
|
1630
|
+
_InternalRenderPipelineContext,
|
|
1631
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1632
|
+
>,
|
|
1273
1633
|
entry: _InternalRenderPipelineContext['validatedOrdered'][number],
|
|
1274
1634
|
identityInstanceDraws: readonly GeometryInstanceDraw[],
|
|
1275
1635
|
useProbeBlend: boolean,
|
|
@@ -1304,6 +1664,11 @@ function resolveGeometryInstanceBuffer(
|
|
|
1304
1664
|
// A present, empty Instances array means zero draws. Do not fall back to
|
|
1305
1665
|
// the shared identity buffer, which would silently render one instance.
|
|
1306
1666
|
if (inst.instanceCount === 0) return [];
|
|
1667
|
+
const entityCacheKey = instanceCollectionCacheKey(entry.source.worldId, inst);
|
|
1668
|
+
const revision = inst.revision ?? inst.archVersion;
|
|
1669
|
+
const sameRevision = (cached: InstanceBufferCacheEntry): boolean =>
|
|
1670
|
+
cached.uploadedRevision === revision;
|
|
1671
|
+
const fullRange = [{ start: 0, end: inst.instanceCount }] as const;
|
|
1307
1672
|
let instanceBuffer: Buffer = pipelineState.identityInstanceBuffer;
|
|
1308
1673
|
let instanceCount = 1;
|
|
1309
1674
|
{
|
|
@@ -1315,41 +1680,126 @@ function resolveGeometryInstanceBuffer(
|
|
|
1315
1680
|
let instanceBufferUsage = GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST;
|
|
1316
1681
|
|
|
1317
1682
|
if (uniformFallback && inst.instanceCount > MAX_UNIFORM_INSTANCES) {
|
|
1683
|
+
const ownerKey = instanceCollectionCacheKey(entry.source.worldId, inst);
|
|
1684
|
+
const chunks = frameState.instanceBufferChunks;
|
|
1685
|
+
const activeKeys = new Set<string>();
|
|
1318
1686
|
const draws: GeometryInstanceDraw[] = [];
|
|
1687
|
+
const uniformStrideBytes = 16 * Float32Array.BYTES_PER_ELEMENT;
|
|
1688
|
+
const uploadedRanges: { start: number; end: number }[] = [];
|
|
1689
|
+
let uploadedBytes = 0;
|
|
1319
1690
|
for (let start = 0; start < inst.instanceCount; start += MAX_UNIFORM_INSTANCES) {
|
|
1320
1691
|
const count = Math.min(MAX_UNIFORM_INSTANCES, inst.instanceCount - start);
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1692
|
+
const chunkKey = `${ownerKey}:${start}`;
|
|
1693
|
+
activeKeys.add(chunkKey);
|
|
1694
|
+
const payloadBytes = count * uniformStrideBytes;
|
|
1695
|
+
const previous = chunks?.get(chunkKey);
|
|
1696
|
+
let active =
|
|
1697
|
+
previous !== undefined &&
|
|
1698
|
+
previous.uploadedArchVersion === inst.archVersion &&
|
|
1699
|
+
previous.uploadedByteLength === payloadBytes
|
|
1700
|
+
? previous
|
|
1701
|
+
: undefined;
|
|
1702
|
+
let activeIsNew = false;
|
|
1703
|
+
if (active === undefined) {
|
|
1704
|
+
const created = runtime.device.createBuffer({
|
|
1705
|
+
size: INSTANCE_UBO_FULL_ARRAY_BYTES,
|
|
1706
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1707
|
+
mappedAtCreation: false,
|
|
1708
|
+
});
|
|
1709
|
+
if (!created.ok) {
|
|
1710
|
+
runtime.errorRegistry.fire(created.error);
|
|
1711
|
+
reportInstanceFailure(
|
|
1712
|
+
c,
|
|
1713
|
+
inst,
|
|
1714
|
+
created.error,
|
|
1715
|
+
inst.instanceCount * uniformStrideBytes,
|
|
1716
|
+
MAX_UNIFORM_INSTANCES * uniformStrideBytes,
|
|
1717
|
+
);
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
if (previous !== undefined && !previous.buffer.isDestroyed) {
|
|
1721
|
+
const destroyed = previous.buffer.destroy();
|
|
1722
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1723
|
+
}
|
|
1724
|
+
active = {
|
|
1725
|
+
buffer: new GpuBuffer(runtime.device, created.value),
|
|
1726
|
+
uploadedArchVersion: inst.archVersion,
|
|
1727
|
+
uploadedByteLength: payloadBytes,
|
|
1728
|
+
};
|
|
1729
|
+
activeIsNew = true;
|
|
1730
|
+
chunks?.set(chunkKey, active);
|
|
1731
|
+
if (chunks === undefined) frameState.transientInstanceBuffers.push(active);
|
|
1330
1732
|
}
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1340
|
-
|
|
1733
|
+
const needsUpload = activeIsNew || !sameRevision(active);
|
|
1734
|
+
const rangesToUpload = [
|
|
1735
|
+
{
|
|
1736
|
+
start: 0,
|
|
1737
|
+
end: count,
|
|
1738
|
+
},
|
|
1739
|
+
];
|
|
1740
|
+
if (needsUpload && rangesToUpload.length > 0) {
|
|
1741
|
+
const upload = uploadInstanceRanges(
|
|
1742
|
+
c,
|
|
1743
|
+
inst,
|
|
1744
|
+
active.buffer.handle,
|
|
1745
|
+
rangesToUpload,
|
|
1746
|
+
false,
|
|
1747
|
+
start,
|
|
1748
|
+
);
|
|
1749
|
+
for (const range of upload.ranges) {
|
|
1750
|
+
uploadedRanges.push({ start: range.start + start, end: range.end + start });
|
|
1751
|
+
}
|
|
1752
|
+
uploadedBytes += upload.bytes;
|
|
1753
|
+
if (!upload.ok) {
|
|
1754
|
+
reportInstanceFailure(
|
|
1755
|
+
c,
|
|
1756
|
+
inst,
|
|
1757
|
+
{
|
|
1758
|
+
code: 'queue-write-buffer-failed',
|
|
1759
|
+
expected:
|
|
1760
|
+
'the renderer-owned instance uniform buffer accepts the complete instance payload',
|
|
1761
|
+
hint: 'retry after the active device is healthy or recover the renderer',
|
|
1762
|
+
},
|
|
1763
|
+
inst.instanceCount * uniformStrideBytes,
|
|
1764
|
+
MAX_UNIFORM_INSTANCES * uniformStrideBytes,
|
|
1765
|
+
);
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (needsUpload) {
|
|
1770
|
+
const published = { ...active, uploadedRevision: revision };
|
|
1771
|
+
active = published;
|
|
1772
|
+
chunks?.set(chunkKey, published);
|
|
1341
1773
|
}
|
|
1342
1774
|
draws.push({
|
|
1343
|
-
instanceBuffer: buffer.handle,
|
|
1775
|
+
instanceBuffer: active.buffer.handle,
|
|
1344
1776
|
instanceBindGroup: resolveGeometryInstancesBindGroup(
|
|
1345
1777
|
c,
|
|
1346
|
-
buffer.handle,
|
|
1778
|
+
active.buffer.handle,
|
|
1347
1779
|
probeBinding?.buffer,
|
|
1348
1780
|
),
|
|
1349
1781
|
probeOffset: probeBinding?.offset ?? -1,
|
|
1350
1782
|
instanceCount: count,
|
|
1351
1783
|
});
|
|
1352
1784
|
}
|
|
1785
|
+
if (chunks !== undefined) {
|
|
1786
|
+
const prefix = `${ownerKey}:`;
|
|
1787
|
+
for (const [key, stale] of chunks.entries()) {
|
|
1788
|
+
if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
|
|
1789
|
+
if (!stale.buffer.isDestroyed) {
|
|
1790
|
+
const destroyed = stale.buffer.destroy();
|
|
1791
|
+
if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
|
|
1792
|
+
}
|
|
1793
|
+
chunks.delete(key);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
reportInstanceResidency(c, inst, {
|
|
1797
|
+
lane: 'chunked-uniform',
|
|
1798
|
+
requestedBytes: inst.instanceCount * uniformStrideBytes,
|
|
1799
|
+
supportedBytes: MAX_UNIFORM_INSTANCES * uniformStrideBytes,
|
|
1800
|
+
uploadRanges: uploadedRanges,
|
|
1801
|
+
uploadedBytes,
|
|
1802
|
+
});
|
|
1353
1803
|
return draws;
|
|
1354
1804
|
}
|
|
1355
1805
|
if (uniformFallback) {
|
|
@@ -1359,31 +1809,30 @@ function resolveGeometryInstanceBuffer(
|
|
|
1359
1809
|
{
|
|
1360
1810
|
// Cap-gate (LimitExceededDetail single emit point — feat-20260514
|
|
1361
1811
|
// M3 / w15 anchor): `requestedBytes <= maxStorageBufferBindingSize`.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1812
|
+
// Derive the size from the authoring count; storage packing is deferred
|
|
1813
|
+
// until uploadInstanceRanges confirms a new or dirty resident.
|
|
1814
|
+
const requestedBytes =
|
|
1815
|
+
inst.instanceCount *
|
|
1816
|
+
(uniformFallback ? 16 : INSTANCE_STORAGE_STRIDE_FLOATS) *
|
|
1817
|
+
Float32Array.BYTES_PER_ELEMENT;
|
|
1366
1818
|
const cap = runtime.device.limits.maxStorageBufferBindingSize;
|
|
1367
1819
|
if (!uniformFallback && typeof cap === 'number' && cap > 0 && requestedBytes > cap) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
requestedBytes,
|
|
1376
|
-
},
|
|
1377
|
-
}),
|
|
1820
|
+
const chunked = resolveStorageInstanceChunks(
|
|
1821
|
+
c,
|
|
1822
|
+
entry,
|
|
1823
|
+
inst,
|
|
1824
|
+
cap,
|
|
1825
|
+
probeBinding?.buffer,
|
|
1826
|
+
probeBinding?.offset ?? -1,
|
|
1378
1827
|
);
|
|
1379
|
-
return
|
|
1828
|
+
return chunked;
|
|
1380
1829
|
} else {
|
|
1381
1830
|
// Look up the cached GPU buffer or create a fresh one when the
|
|
1382
1831
|
// archetype version bumped or the byte length changed.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
);
|
|
1832
|
+
// entityCacheKey is the worldEntityKey + collectionId namespace.
|
|
1833
|
+
const cached = frameState.instanceBuffers.get(entityCacheKey);
|
|
1386
1834
|
let active: InstanceBufferCacheEntry | null = null;
|
|
1835
|
+
let activeIsNew = false;
|
|
1387
1836
|
if (
|
|
1388
1837
|
cached !== undefined &&
|
|
1389
1838
|
cached.uploadedArchVersion === inst.archVersion &&
|
|
@@ -1398,6 +1847,8 @@ function resolveGeometryInstanceBuffer(
|
|
|
1398
1847
|
});
|
|
1399
1848
|
if (!bufRes.ok) {
|
|
1400
1849
|
runtime.errorRegistry.fire(bufRes.error);
|
|
1850
|
+
reportInstanceFailure(c, inst, bufRes.error, requestedBytes, cap);
|
|
1851
|
+
return null;
|
|
1401
1852
|
} else {
|
|
1402
1853
|
// feat-20260619 M4 / F12: destroy the old cached buffer
|
|
1403
1854
|
// before replacing it with the new one (D-6).
|
|
@@ -1411,24 +1862,67 @@ function resolveGeometryInstanceBuffer(
|
|
|
1411
1862
|
uploadedArchVersion: inst.archVersion,
|
|
1412
1863
|
uploadedByteLength: requestedBytes,
|
|
1413
1864
|
};
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1865
|
+
activeIsNew = true;
|
|
1866
|
+
if (active !== null) {
|
|
1867
|
+
// Keep the collection identity in the worldEntityKey namespace.
|
|
1868
|
+
frameState.instanceBuffers.set(entityCacheKey, active);
|
|
1869
|
+
}
|
|
1418
1870
|
}
|
|
1419
1871
|
}
|
|
1420
1872
|
if (active !== null) {
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
)
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1873
|
+
const needsUpload = activeIsNew || !sameRevision(active);
|
|
1874
|
+
const ranges = [fullRange[0]];
|
|
1875
|
+
let uploadRanges: readonly { readonly start: number; readonly end: number }[] = [];
|
|
1876
|
+
let uploadedBytes = 0;
|
|
1877
|
+
if (needsUpload && ranges.length > 0) {
|
|
1878
|
+
const upload = uploadInstanceRanges(
|
|
1879
|
+
c,
|
|
1880
|
+
inst,
|
|
1881
|
+
active.buffer.handle,
|
|
1882
|
+
ranges,
|
|
1883
|
+
!uniformFallback,
|
|
1884
|
+
);
|
|
1885
|
+
uploadRanges = upload.ranges;
|
|
1886
|
+
uploadedBytes = upload.bytes;
|
|
1887
|
+
if (!upload.ok) {
|
|
1888
|
+
reportInstanceFailure(
|
|
1889
|
+
c,
|
|
1890
|
+
inst,
|
|
1891
|
+
{
|
|
1892
|
+
code: 'queue-write-buffer-failed',
|
|
1893
|
+
expected:
|
|
1894
|
+
'the renderer-owned instance buffer accepts the complete instance payloads',
|
|
1895
|
+
hint: 'retry after the active device is healthy or recover the renderer',
|
|
1896
|
+
},
|
|
1897
|
+
requestedBytes,
|
|
1898
|
+
uniformFallback
|
|
1899
|
+
? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
|
|
1900
|
+
: typeof cap === 'number'
|
|
1901
|
+
? cap
|
|
1902
|
+
: undefined,
|
|
1903
|
+
);
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (needsUpload) {
|
|
1908
|
+
const published = { ...active, uploadedRevision: revision };
|
|
1909
|
+
frameState.instanceBuffers.set(entityCacheKey, published);
|
|
1910
|
+
active = published;
|
|
1431
1911
|
}
|
|
1912
|
+
instanceBuffer = active.buffer.handle;
|
|
1913
|
+
instanceCount = Math.max(1, inst.instanceCount);
|
|
1914
|
+
reportInstanceResidency(c, inst, {
|
|
1915
|
+
lane: uniformFallback ? 'direct-uniform' : 'direct-storage',
|
|
1916
|
+
requestedBytes,
|
|
1917
|
+
supportedBytes: uniformFallback
|
|
1918
|
+
? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
|
|
1919
|
+
: typeof cap === 'number'
|
|
1920
|
+
? cap
|
|
1921
|
+
: undefined,
|
|
1922
|
+
uploadRanges,
|
|
1923
|
+
uploadedBytes,
|
|
1924
|
+
});
|
|
1925
|
+
clearInstanceChunksForOwner(c, entityCacheKey);
|
|
1432
1926
|
}
|
|
1433
1927
|
}
|
|
1434
1928
|
// The cache write above is keyed by worldEntityKey(worldId, cacheKey)
|
|
@@ -1446,7 +1940,10 @@ function resolveGeometryInstanceBuffer(
|
|
|
1446
1940
|
}
|
|
1447
1941
|
|
|
1448
1942
|
export function resolveProbeBlendBuffer(
|
|
1449
|
-
c:
|
|
1943
|
+
c: Pick<
|
|
1944
|
+
_InternalRenderPipelineContext,
|
|
1945
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1946
|
+
>,
|
|
1450
1947
|
record: import('../scene/probe-blend-record').ProbeBlendRecord | undefined,
|
|
1451
1948
|
entityKey?: number,
|
|
1452
1949
|
): { readonly buffer: Buffer; readonly offset: number } {
|
|
@@ -1494,7 +1991,10 @@ export function resolveProbeBlendBuffer(
|
|
|
1494
1991
|
}
|
|
1495
1992
|
|
|
1496
1993
|
export function resolveGeometryInstancesBindGroup(
|
|
1497
|
-
c:
|
|
1994
|
+
c: Pick<
|
|
1995
|
+
_InternalRenderPipelineContext,
|
|
1996
|
+
'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
|
|
1997
|
+
>,
|
|
1498
1998
|
instanceBuffer: Buffer,
|
|
1499
1999
|
probeBuffer?: Buffer,
|
|
1500
2000
|
): BindGroup {
|