@forgeax/engine-render 0.1.23 → 0.1.24
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 +131 -9
- package/dist/assembly/bundler-contract.d.ts +9 -0
- package/dist/assembly/bundler-contract.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +8 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
- package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +7 -243
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +3 -2
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +168 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -0
- package/dist/assembly/material-shader-prewarm.d.ts +21 -0
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
- package/dist/assembly/recovery/generation.d.ts +46 -0
- package/dist/assembly/recovery/generation.d.ts.map +1 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts +10 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +19 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -0
- package/dist/assembly/renderer-lifecycle.d.ts +34 -0
- package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +55 -0
- package/dist/assembly/webgpu-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +152 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
- package/dist/chunk-KZFTK6DO.mjs.map +1 -0
- package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
- package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
- package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
- package/dist/chunk-RR5L4SRS.mjs.map +1 -0
- package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
- package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
- package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
- package/dist/chunk-UQYS7FRM.mjs.map +1 -0
- package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
- package/dist/chunk-YKB5DQV7.mjs.map +1 -0
- package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
- package/dist/chunk-YRUYGA4A.mjs.map +1 -0
- package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
- package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
- package/dist/construct-renderer.mjs +19841 -16190
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts +2 -0
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/device/gpu-residency.d.ts +24 -4
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/frame.d.ts +5 -0
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/inspection.d.ts +7 -0
- package/dist/environment/inspection.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +20 -2
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +58 -2
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +13 -0
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +899 -0
- package/dist/extract/contracts.d.ts.map +1 -0
- package/dist/features/host.d.ts +2 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +3 -0
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -0
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +28 -0
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +1 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +7 -6
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +7 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/internal.mjs +6 -6
- package/dist/light-buffer-layout.d.ts +3 -0
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts +34 -10
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +56 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/occlusion-proxy.d.ts +4 -0
- package/dist/occlusion-proxy.d.ts.map +1 -0
- package/dist/pbr-pipeline.d.ts +32 -9
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/points-lines/prepare.d.ts +3 -0
- package/dist/points-lines/prepare.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +33 -0
- package/dist/points-lines/standard-owner.d.ts.map +1 -0
- package/dist/record/frame-snapshot.d.ts +5 -0
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +14 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +14 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -17
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +11 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/recovery-pipeline.d.ts +30 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +26 -4
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +65 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -0
- package/dist/render-contract.d.ts +64 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +12 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +156 -22
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +128 -4
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +19 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -6
- package/dist/temporal/taa-history-store.d.ts +15 -1
- package/dist/temporal/taa-history-store.d.ts.map +1 -1
- package/dist/temporal/transaction.d.ts +5 -0
- package/dist/temporal/transaction.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
- package/src/__tests__/environment-generation.integration.test.ts +23 -0
- package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
- package/src/__tests__/factory-contract.integration.test.ts +122 -7
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
- package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
- package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
- package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
- package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
- package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
- package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
- package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
- package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
- package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
- package/src/__tests__/shader-manifest-fixture.ts +13 -16
- package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
- package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
- package/src/assembly/bundler-contract.ts +9 -0
- package/src/assembly/device-feature-admission.ts +15 -5
- package/src/assembly/extended-lighting-fallback.ts +216 -0
- package/src/assembly/factory.ts +81 -10088
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material-shader-policy.ts +854 -0
- package/src/assembly/material-shader-prewarm.ts +88 -0
- package/src/assembly/mesh-ssbo-grow.ts +270 -0
- package/src/assembly/recovery/device-loss-fanout.ts +89 -0
- package/src/assembly/recovery/generation.ts +141 -0
- package/src/assembly/recovery/recovery-attempt.ts +800 -0
- package/src/assembly/recovery/renderer-recover.ts +906 -0
- package/src/assembly/renderer-facade.ts +342 -0
- package/src/assembly/renderer-frame-transaction.ts +45 -0
- package/src/assembly/renderer-helpers.ts +99 -0
- package/src/assembly/renderer-lifecycle.ts +159 -0
- package/src/assembly/shader-prewarm-policy.ts +157 -0
- package/src/assembly/webgpu-ready-contract.ts +14 -0
- package/src/assembly/webgpu-ready.ts +4079 -0
- package/src/assembly/webgpu-renderer.ts +4095 -0
- package/src/components/camera.ts +1 -1
- package/src/debug-draw-glue.ts +1 -1
- package/src/device/device-scope.ts +35 -6
- package/src/device/gpu-residency.ts +194 -26
- package/src/environment/frame.ts +6 -0
- package/src/environment/inspection.ts +10 -0
- package/src/environment/lifecycle.ts +114 -8
- package/src/errors/recover.ts +76 -5
- package/src/errors/render.ts +17 -0
- package/src/extract/contracts.ts +974 -0
- package/src/features/host.ts +16 -0
- package/src/features/prepared-gpu-work.ts +20 -2
- package/src/features/prepared-graphics-store.ts +17 -0
- package/src/gpu-driven/production-raster.ts +97 -2
- package/src/gpu-scene.ts +3 -2
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +9 -1
- package/src/light-buffer-layout.ts +3 -0
- package/src/materials.ts +262 -45
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +139 -15
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
- package/src/points-lines/prepare.ts +9 -0
- package/src/points-lines/standard-owner.ts +403 -0
- package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
- package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
- package/src/record/frame-snapshot.ts +15 -0
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +6 -2
- package/src/record/helpers.ts +1 -1
- package/src/record/main-pass-geometry.ts +230 -5
- package/src/record/main-pass-material.ts +316 -209
- package/src/record/main-pass-sprite-draws.ts +6 -7
- package/src/record/main-pass.ts +94 -6
- package/src/record/mesh-ssbo.ts +32 -10
- package/src/record/recovery-pipeline.ts +695 -0
- package/src/record/render-context.ts +35 -4
- package/src/record/typed-frame-graph.ts +45 -1
- package/src/record/view-ubo.ts +3 -3
- package/src/recovery/render-system-candidate.ts +839 -0
- package/src/render-contract.ts +65 -7
- package/src/render-system-extract-tail.ts +2983 -0
- package/src/render-system-extract.ts +124 -2977
- package/src/render-system.ts +868 -659
- package/src/scene/render-scene.ts +124 -1
- package/src/temporal/frame-coordinator.ts +4 -1
- package/src/temporal/taa-history-store.ts +46 -2
- package/src/temporal/temporal-view.ts +1 -1
- package/src/temporal/transaction.ts +9 -0
- package/src/tilemap-chunk-extract-system.ts +1 -1
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
- package/dist/chunk-4L3P6DT5.mjs.map +0 -1
- package/dist/chunk-4NPUASZR.mjs.map +0 -1
- package/dist/chunk-DIYW3M5D.mjs.map +0 -1
- package/dist/chunk-FBC4R44J.mjs.map +0 -1
- package/dist/chunk-HZUPSYW7.mjs.map +0 -1
- package/dist/chunk-QODA5UKU.mjs.map +0 -1
- package/dist/chunk-YALPS44A.mjs.map +0 -1
- package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
|
@@ -0,0 +1,4079 @@
|
|
|
1
|
+
// WebGPU generation-scoped ready-state builder.
|
|
2
|
+
// This owner performs one complete device-generation promotion; it receives
|
|
3
|
+
// policy/adapters and returns a fully prepared PipelineState.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BuiltinAssetRegistry,
|
|
7
|
+
HANDLE_CUBE,
|
|
8
|
+
HANDLE_NINESLICE_QUAD,
|
|
9
|
+
HANDLE_QUAD,
|
|
10
|
+
HANDLE_SPHERE,
|
|
11
|
+
HANDLE_TRIANGLE,
|
|
12
|
+
} from '@forgeax/engine-assets-runtime';
|
|
13
|
+
import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
|
|
14
|
+
import type {
|
|
15
|
+
BindGroupLayout,
|
|
16
|
+
Buffer,
|
|
17
|
+
ComputePipeline,
|
|
18
|
+
PipelineLayout,
|
|
19
|
+
RenderPipeline,
|
|
20
|
+
Result,
|
|
21
|
+
RhiDevice,
|
|
22
|
+
Sampler,
|
|
23
|
+
ShaderModule,
|
|
24
|
+
} from '@forgeax/engine-rhi';
|
|
25
|
+
import { ok, RhiError } from '@forgeax/engine-rhi';
|
|
26
|
+
import { findVariantByKey, type ShaderCatalog } from '@forgeax/engine-shader';
|
|
27
|
+
import type { ManifestEntry } from '@forgeax/engine-types';
|
|
28
|
+
import { handleSlot } from '@forgeax/engine-types';
|
|
29
|
+
|
|
30
|
+
export type { MaterialShaderManifestEntry } from '@forgeax/engine-shader';
|
|
31
|
+
|
|
32
|
+
import type { GpuResidencyCache, MeshGpuHandles } from '../device/gpu-residency';
|
|
33
|
+
import { postProcessShaderModuleLabel } from '../fullscreen-post-process-pass';
|
|
34
|
+
import { GpuBuffer } from '../gpu-resource';
|
|
35
|
+
import { GPU_SHADER_STAGE_FRAGMENT } from '../gpu-stage';
|
|
36
|
+
import {
|
|
37
|
+
GPU_TEXTURE_USAGE_COPY_DST,
|
|
38
|
+
GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING,
|
|
39
|
+
GPU_TEXTURE_USAGE_TEXTURE_BINDING,
|
|
40
|
+
} from '../gpu-texture-usage';
|
|
41
|
+
import {
|
|
42
|
+
GPU_BUFFER_USAGE_COPY_DST,
|
|
43
|
+
GPU_BUFFER_USAGE_INDEX,
|
|
44
|
+
GPU_BUFFER_USAGE_STORAGE,
|
|
45
|
+
GPU_BUFFER_USAGE_UNIFORM,
|
|
46
|
+
GPU_BUFFER_USAGE_VERTEX,
|
|
47
|
+
} from '../gpu-usage';
|
|
48
|
+
import { createHdrpClusterMembershipBindGroupLayoutDescriptor } from '../hdrp-buffers';
|
|
49
|
+
import { setIblComposedShaders } from '../ibl/IblPipelineCache';
|
|
50
|
+
import {
|
|
51
|
+
createSkylightFallback,
|
|
52
|
+
FALLBACK_BYTES_PER_ROW,
|
|
53
|
+
type SkylightFallback,
|
|
54
|
+
} from '../ibl/skylight-bind-group';
|
|
55
|
+
import type { BloomInspection } from '../inspection-types';
|
|
56
|
+
import type { DeviceScope, RhiErrorListenerRegistry } from '../lifecycle';
|
|
57
|
+
import { assertStorageBufferCap } from '../light-buffer-layout';
|
|
58
|
+
import {
|
|
59
|
+
buildPbrPipelineLayouts,
|
|
60
|
+
buildPbrSkinLayouts,
|
|
61
|
+
createHdrpSkinBindGroupLayoutDescriptor,
|
|
62
|
+
type PipelineGroup2Contract,
|
|
63
|
+
resolvePipelineGroup2Contract,
|
|
64
|
+
} from '../pbr-pipeline';
|
|
65
|
+
import { STANDARD_CLUSTER_MEMBERSHIP_WGSL } from '../pipeline/standard-pipeline';
|
|
66
|
+
import { inspectStandardBloomGraph } from '../pipeline/standard-post';
|
|
67
|
+
import {
|
|
68
|
+
buildLinearLdrMaterialSpecTable,
|
|
69
|
+
buildSpecConstTable,
|
|
70
|
+
cacheKeyOf,
|
|
71
|
+
createHdrpBindGroupLayoutDescriptor,
|
|
72
|
+
getOrBuildPipeline,
|
|
73
|
+
type PipelineCache,
|
|
74
|
+
type PipelineDeviceProvider,
|
|
75
|
+
type PipelineSpec,
|
|
76
|
+
PipelineSpecError,
|
|
77
|
+
} from '../pipeline-spec';
|
|
78
|
+
import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
|
|
79
|
+
import { deriveExtendedLightingCapability } from '../prepare/extended-lighting/resources';
|
|
80
|
+
import type { BloomPersistentBundle } from '../record/render-context';
|
|
81
|
+
import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
|
|
82
|
+
import { POINTS_LINES_VIEW_BUFFER_SIZE } from '../record/view-ubo';
|
|
83
|
+
import {
|
|
84
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
85
|
+
type VolumetricFogShaderSources,
|
|
86
|
+
} from '../render-contract';
|
|
87
|
+
import {
|
|
88
|
+
MATERIAL_PER_ENTITY_STRIDE,
|
|
89
|
+
type PipelineState,
|
|
90
|
+
selectSwapChainFormat,
|
|
91
|
+
} from '../render-system';
|
|
92
|
+
import {
|
|
93
|
+
createSkinPaletteAllocator,
|
|
94
|
+
type SkinPaletteAllocator,
|
|
95
|
+
} from '../systems/skin-palette-allocator';
|
|
96
|
+
import { createExtendedLightingFallbackResources } from './extended-lighting-fallback';
|
|
97
|
+
import { invokeDeviceCreateShaderModule } from './material-shader-policy';
|
|
98
|
+
import { prewarmRequiredMaterialShaders } from './material-shader-prewarm';
|
|
99
|
+
import {
|
|
100
|
+
createMeshSsboGrowController,
|
|
101
|
+
INITIAL_MESH_SSBO_SLOT_COUNT,
|
|
102
|
+
type MeshSsboGrowDevice,
|
|
103
|
+
type MeshSsboGrowResult,
|
|
104
|
+
type MeshSsboState,
|
|
105
|
+
requireMeshSsboBuffer,
|
|
106
|
+
} from './mesh-ssbo-grow';
|
|
107
|
+
import { runShimStep, runShimSyncStep } from './renderer-helpers';
|
|
108
|
+
import {
|
|
109
|
+
prewarmMaterialShaderVariants,
|
|
110
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
111
|
+
selectHdrpPbrPrewarmVariants,
|
|
112
|
+
selectProbePrewarmVariants,
|
|
113
|
+
selectStandardPbrTransmissionPrewarmVariants,
|
|
114
|
+
} from './shader-prewarm-policy';
|
|
115
|
+
import {
|
|
116
|
+
BLUR_PARAMS_BYTES,
|
|
117
|
+
BRIGHT_PARAMS_BYTES,
|
|
118
|
+
COMPOSITE_PARAMS_BYTES,
|
|
119
|
+
HDR_COLOR_ATTACHMENT_FORMAT,
|
|
120
|
+
MIPMAP_PREWARM_FORMATS,
|
|
121
|
+
VIEW_UBO_BYTES,
|
|
122
|
+
} from './webgpu-ready-contract';
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build the `Renderer.initialization` Promise (D-S3 three-step strict-serial chain).
|
|
126
|
+
*
|
|
127
|
+
* Steps run in order; each rejection short-circuits the chain and the
|
|
128
|
+
* resulting Promise rejects with a structured `RhiError` / `ShaderError`.
|
|
129
|
+
* AI users `await renderer.initialization` once before the first `draw(world)` call;
|
|
130
|
+
* subsequent frames may skip the await (the Promise stays resolved).
|
|
131
|
+
*
|
|
132
|
+
* Step 1 (manifest load): `shader.loadManifest()` populates the runtime
|
|
133
|
+
* registry. Failure = `ShaderError 'manifest-malformed'` /
|
|
134
|
+
* `'shader-not-found'`.
|
|
135
|
+
*
|
|
136
|
+
* Step 2 (pipeline compile): synthesises the PBR pipeline (3 BindGroupLayout
|
|
137
|
+
* + 1 PipelineLayout + 1 ShaderModule + 1 RenderPipeline). Failure =
|
|
138
|
+
* `RhiError 'shader-compile-failed'` / `'feature-not-enabled'` /
|
|
139
|
+
* `'limit-exceeded'`.
|
|
140
|
+
*
|
|
141
|
+
* Step 3 (asset upload): allocates GPU buffers for the builtin cube and
|
|
142
|
+
* triangle meshes via `device.createBuffer` + `queue.writeBuffer`. Failure
|
|
143
|
+
* = `RhiError 'limit-exceeded'` / `'webgpu-runtime-error'` /
|
|
144
|
+
* `'queue-write-buffer-out-of-bounds'`.
|
|
145
|
+
*/
|
|
146
|
+
export async function buildReadyWebGPU(
|
|
147
|
+
rhiDevice: RhiDevice,
|
|
148
|
+
rendererScope: DeviceScope,
|
|
149
|
+
getShader: () => ShaderCatalog,
|
|
150
|
+
gpuStore: GpuResidencyCache,
|
|
151
|
+
asyncCreateShaderModule:
|
|
152
|
+
| ((
|
|
153
|
+
device: RhiDevice,
|
|
154
|
+
desc: { code: string; label?: string | undefined },
|
|
155
|
+
) => Promise<Result<ShaderModule, RhiError>>)
|
|
156
|
+
| undefined,
|
|
157
|
+
errorRegistry: RhiErrorListenerRegistry,
|
|
158
|
+
/**
|
|
159
|
+
* Material shader modules declared by producer features. These are compiled
|
|
160
|
+
* during Renderer.initialization so prepared graphics do not fail on their first
|
|
161
|
+
* frame while the shared async shader adapter is still warming up.
|
|
162
|
+
*/
|
|
163
|
+
requiredMaterialShaders: readonly string[],
|
|
164
|
+
/** Fullscreen feature modules compiled before the first synchronous frame. */
|
|
165
|
+
requiredFullscreenPostProcesses: readonly {
|
|
166
|
+
readonly identity: string;
|
|
167
|
+
readonly source: string;
|
|
168
|
+
}[],
|
|
169
|
+
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
170
|
+
// T-M2-05 + M3 / T-M3-04: surface for setting `internals.growMeshSsbo`
|
|
171
|
+
// + `internals.meshSsboState` so the record stage's
|
|
172
|
+
// `ensureMeshSsboCapacity` hook can reach both. The mesh-SSBO grow
|
|
173
|
+
// controller is owned in this function's scope, so the cleanest expose
|
|
174
|
+
// path is a callback that buildReadyWebGPU invokes once after
|
|
175
|
+
// `meshSsboController` is wired (the alternative — returning the
|
|
176
|
+
// function + state alongside the PipelineState — bloats every
|
|
177
|
+
// successful call site for two optional hooks).
|
|
178
|
+
setGrowMeshSsboHook: (
|
|
179
|
+
hook: (neededSlots: number) => MeshSsboGrowResult,
|
|
180
|
+
state: MeshSsboState,
|
|
181
|
+
) => void,
|
|
182
|
+
// feat-20260609 R3-fixup: seed-shader-module hook the lazy
|
|
183
|
+
// MaterialShader pipeline cache adapter exposes (see
|
|
184
|
+
// makeShaderDeviceAdapter / ShaderDeviceAdapterInternal). Used to seed
|
|
185
|
+
// the shadow_caster module under
|
|
186
|
+
// `module-forgeax::default-shadow-caster` so the lazy PSO build hits
|
|
187
|
+
// OK on frame 1 (no 1-frame warmup for the engine-shipped shadow
|
|
188
|
+
// caster).
|
|
189
|
+
seedShaderModule: (label: string, module: ShaderModule) => void,
|
|
190
|
+
// M6 fix-up (feat-20260615-pipeline-spec-ssot): seed-pipeline hook the
|
|
191
|
+
// outer `makeWebGPURenderer` exposes for `materialShaderPipelineCache`.
|
|
192
|
+
// Invoked once per URP-variant SPEC_CONST entry after the boot-time
|
|
193
|
+
// prewarm completes, so the URP record path's first-frame
|
|
194
|
+
// `getMaterialShaderPipeline` lookup hits a live PSO instead of falling
|
|
195
|
+
// into a 1-frame async-compile skip-draw window. Idempotent: caller
|
|
196
|
+
// guards against re-seeding when a key already exists.
|
|
197
|
+
seedMaterialShaderPipelineCache: (
|
|
198
|
+
key: string,
|
|
199
|
+
pso: RenderPipeline,
|
|
200
|
+
group2Contract: PipelineGroup2Contract,
|
|
201
|
+
) => void,
|
|
202
|
+
// feat-20260621 M-A3 (D-5): register the engine built-in tonemap onto the
|
|
203
|
+
// unified post-process channel once the tonemap manifest entry's composed
|
|
204
|
+
// WGSL is resolved. Invoked with the tonemap WGSL source string; the outer
|
|
205
|
+
// `makeWebGPURenderer` closure forwards it to
|
|
206
|
+
// the fullscreen feature host with `{ source, params }`.
|
|
207
|
+
// Scope bridge mirrors `setGrowMeshSsboHook` / `seedShaderModule`: the
|
|
208
|
+
// tonemap source resolves inside this async function (after the manifest-load
|
|
209
|
+
// await), by which point the synchronous `renderSystem` const is defined.
|
|
210
|
+
registerBuiltinTonemap: (source: string) => void,
|
|
211
|
+
/** Register the special FXAA pass' final-output policy params UBO. */
|
|
212
|
+
registerBuiltinFxaa: (source: string) => void,
|
|
213
|
+
registerBuiltinTemporalPostProcesses: (entries: {
|
|
214
|
+
readonly motionBlur?: string;
|
|
215
|
+
readonly taaResolve?: string;
|
|
216
|
+
}) => void,
|
|
217
|
+
/** Install manifest-owned volume utility sources only after all four modules compile. */
|
|
218
|
+
setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
|
|
219
|
+
): Promise<PipelineState> {
|
|
220
|
+
const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
|
|
221
|
+
// bug-20260610: WebGL2 fallback path — read storage-buffer capability up
|
|
222
|
+
// front so both Step 2 (shader-module compile, where we patch engine
|
|
223
|
+
// entries to the STORAGE_BUFFER_AVAILABLE=false variant) and Step 3
|
|
224
|
+
// (buildPbrPipelineLayouts, which switches between read-only-storage
|
|
225
|
+
// and uniform BGL entries) walk the same axis. Step 0 already ran in
|
|
226
|
+
// `prepareMaterialShaders`; this is an idempotent re-read driven by
|
|
227
|
+
// `device.limits.maxStorageBuffersPerShaderStage`, which is 0 on the
|
|
228
|
+
// wgpu-wasm WebGL2 backend (downlevel_webgl2_defaults).
|
|
229
|
+
const capValue = (rhiDevice.limits as Readonly<Record<string, number>>)
|
|
230
|
+
.maxStorageBuffersPerShaderStage;
|
|
231
|
+
const webgl2Downlevel = rhiDevice.caps.backendKind === 'wgpu-webgl2';
|
|
232
|
+
let storageBufferCapable = !webgl2Downlevel;
|
|
233
|
+
if (typeof capValue === 'number') {
|
|
234
|
+
const capCheck = assertStorageBufferCap(capValue);
|
|
235
|
+
storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
|
|
236
|
+
}
|
|
237
|
+
const directionalPcssAvailable =
|
|
238
|
+
rhiDevice.caps.backendKind === 'webgpu' || rhiDevice.caps.backendKind === 'wgpu-native';
|
|
239
|
+
const projectorAvailable =
|
|
240
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
241
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
242
|
+
// bug-20260612: choose swap-chain storage / view formats by backend.
|
|
243
|
+
// Channel 2 (native WebGPU) follows navigator.gpu.getPreferredCanvasFormat();
|
|
244
|
+
// Channel 3 (wgpu-wasm GLES, storageBufferCapable=false) hard-codes
|
|
245
|
+
// rgba8unorm. The pair flows into PipelineState.format /
|
|
246
|
+
// .colorAttachmentFormat below and through it into every downstream
|
|
247
|
+
// pipeline target, configure() call, and color-attachment format —
|
|
248
|
+
// single SSOT, no scattered branches. See selectSwapChainFormat
|
|
249
|
+
// (above the SWAP_CHAIN_*_FORMAT historical constants).
|
|
250
|
+
const surfaceViewFormats =
|
|
251
|
+
(rhiDevice as unknown as { readonly surfaceViewFormats?: boolean }).surfaceViewFormats ??
|
|
252
|
+
!webgl2Downlevel;
|
|
253
|
+
const swapChainFormats = selectSwapChainFormat(storageBufferCapable, surfaceViewFormats);
|
|
254
|
+
// The 'null' (headless RhiNull) backend has no UA preferred-canvas-format by
|
|
255
|
+
// design; the rgba8unorm fallback is its intended steady state, not a
|
|
256
|
+
// degraded one — firing 'rhi-not-available' there is territorially wrong
|
|
257
|
+
// (the backend IS available) and only pollutes Renderer error events in headless
|
|
258
|
+
// CI. Skip the diagnostic for it; Channel 2/3 still report a missing
|
|
259
|
+
// getPreferredCanvasFormat as before.
|
|
260
|
+
if (swapChainFormats.fallbackReason !== undefined && rhiDevice.caps.backendKind !== 'null') {
|
|
261
|
+
// Step ③ in selectSwapChainFormat fired — surface a structured
|
|
262
|
+
// diagnostic through the RhiError channel so AI users subscribed via
|
|
263
|
+
// Renderer error subscribers can detect "extremely-old UA / missing
|
|
264
|
+
// navigator.gpu.getPreferredCanvasFormat" and react. The renderer
|
|
265
|
+
// continues with the rgba8unorm fallback (charter §9 graceful
|
|
266
|
+
// degradation; charter P3 explicit failure — no silent fallback).
|
|
267
|
+
errorRegistry.fire(
|
|
268
|
+
new RhiError({
|
|
269
|
+
code: 'rhi-not-available',
|
|
270
|
+
expected: 'navigator.gpu.getPreferredCanvasFormat is callable on Channel 2',
|
|
271
|
+
hint: 'browser is too old or WebGPU implementation incomplete; falling back to rgba8unorm swap-chain format. Update the UA or use a Channel-3-compatible canvas configuration.',
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
// ── Step 2: PBR + unlit pipeline compile ───────────────────────────────────
|
|
276
|
+
// bug-20260519 D-1 + D-3: gate the entire PBR / unlit shader-compile block
|
|
277
|
+
// behind `manifestEntries.length > 0`. The Camera-only / clear-pass-only
|
|
278
|
+
// path (LO 1.1 hello-window equivalent) ships an empty manifest -- no
|
|
279
|
+
// PBR/unlit entry to find, no shader module to compile. When the gate is
|
|
280
|
+
// skipped both `unlitModule` and `pbrModule` stay `null` and the later
|
|
281
|
+
// unlit / standard `createRenderPipeline` calls are skipped in turn so
|
|
282
|
+
// the returned `PipelineState.{unlitPipeline,standardPipeline}` fields
|
|
283
|
+
// are written `null` (D-3 nullable). The render-time access point in
|
|
284
|
+
// `render-system-record.ts` narrows on `=== null` and fires a structured
|
|
285
|
+
// `RhiError shader-compile-failed` (charter P3 explicit failure;
|
|
286
|
+
// AC-03). Other PipelineState fields (BindGroupLayout chain / shared
|
|
287
|
+
// buffers / defaultSampler / fallbackTextureView / depthTexture* /
|
|
288
|
+
// identityInstanceBuffer / mesh handles) keep their existing
|
|
289
|
+
// construction so the clear-pass path remains fully wired (D-3
|
|
290
|
+
// explicit scope).
|
|
291
|
+
const registry = getShader();
|
|
292
|
+
//
|
|
293
|
+
// feat-20260518-pbr-direct-lighting-mvp M5 / w22.9 (AC-05 + plan-strategy
|
|
294
|
+
// D-3 + D-4 + dual-pipeline contract w12): the manifest now ships the
|
|
295
|
+
// pbr.wgsl + unlit.wgsl entries written by `@forgeax/engine-vite-plugin-shader`'s
|
|
296
|
+
// `buildStart` hook (engine-entries eager compile via naga_oil). Identify
|
|
297
|
+
// them by content marker (charter P3 explicit failure: silent fallback to
|
|
298
|
+
// a wrong entry would produce mis-shaded pixels indistinguishable from
|
|
299
|
+
// success). pbr.wgsl is the only entry whose composed body contains the
|
|
300
|
+
// `f_schlick(` BRDF helper call; the other engine entry is unlit.
|
|
301
|
+
// M3 D-P4: rhi-webgpu supplies the async factory; rhi-wgpu and the
|
|
302
|
+
// explicit escape hatch fall back to the synchronous device entry.
|
|
303
|
+
const manifestEntries: ManifestEntry[] = [];
|
|
304
|
+
for (const entry of registry.entries()) {
|
|
305
|
+
manifestEntries.push(entry);
|
|
306
|
+
}
|
|
307
|
+
let pbrModule: ShaderModule | null = null;
|
|
308
|
+
let unlitModule: ShaderModule | null = null;
|
|
309
|
+
let spriteModule: ShaderModule | null = null;
|
|
310
|
+
let spriteLitModule: ShaderModule | null = null;
|
|
311
|
+
let fxaaModule: ShaderModule | null = null;
|
|
312
|
+
let skyboxModule: ShaderModule | null = null;
|
|
313
|
+
let bloomBrightModule: ShaderModule | null = null;
|
|
314
|
+
let bloomBlurModule: ShaderModule | null = null;
|
|
315
|
+
let bloomCompositeModule: ShaderModule | null = null;
|
|
316
|
+
let ssaoModule: ShaderModule | null = null;
|
|
317
|
+
const findEngineManifestEntry = (identifier: string): ManifestEntry | undefined => {
|
|
318
|
+
const materialEntry = Array.from(registry.materialShaderManifestEntries()).find(
|
|
319
|
+
(candidate) => candidate.identifier === identifier,
|
|
320
|
+
);
|
|
321
|
+
if (materialEntry === undefined) return undefined;
|
|
322
|
+
return (
|
|
323
|
+
manifestEntries.find((entry) => entry.wgsl === materialEntry.composedWgsl) ?? {
|
|
324
|
+
hash: `engine:${identifier}`,
|
|
325
|
+
wgsl: materialEntry.composedWgsl,
|
|
326
|
+
glsl: undefined,
|
|
327
|
+
bindings: '',
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
if (manifestEntries.length > 0) {
|
|
332
|
+
// Merge of bug-20260519 D-1 + D-3 (manifest-zero gate, this branch's
|
|
333
|
+
// outer `if (manifestEntries.length > 0)`) + main feat-20260519-tonemap
|
|
334
|
+
// T-M2.5 (engine SSOT triple — pbr + unlit + tonemap) +
|
|
335
|
+
// feat-20260520-directional-light-shadow-mapping M1c / w9 (shadow_caster
|
|
336
|
+
// as additional engine entry) + feat-20260520-2d-sprite-layer-mvp M-3 / w24
|
|
337
|
+
// (sprite as additional engine entry) + feat-20260520-skylight-ibl-cubemap
|
|
338
|
+
// M5-amend Gap A (4 IBL precompute entries).
|
|
339
|
+
//
|
|
340
|
+
// Manifest non-empty: require pbr + unlit + tonemap; shadow_caster +
|
|
341
|
+
// sprite + IBL entries are optional — absent ones leave their module
|
|
342
|
+
// null and the dependent pipeline stays null (callers fail-fast at
|
|
343
|
+
// dispatch if they relied on a missing entry).
|
|
344
|
+
//
|
|
345
|
+
// Marker triage (charter P3 explicit failure):
|
|
346
|
+
// - tonemap.wgsl: declares `struct TonemapParams`
|
|
347
|
+
// - sprite.wgsl: declares `pivotAndSize` Material field
|
|
348
|
+
// - pbr.wgsl: composes `f_schlick`
|
|
349
|
+
// - shadow_caster.wgsl: only position input (no normal/uv/tangent)
|
|
350
|
+
// - IBL entries: identified by their fragment entry-point markers
|
|
351
|
+
// (equirectToCube_fs / irradianceConvolve_fs / prefilterEnv_fs /
|
|
352
|
+
// brdfLutBake_fs) which survive naga_oil composition unchanged.
|
|
353
|
+
// - unlit.wgsl: none of the above markers → falls into the unlit slot.
|
|
354
|
+
let pbrEntry = findEngineManifestEntry('forgeax::default-standard-pbr');
|
|
355
|
+
let unlitEntry = findEngineManifestEntry('forgeax::default-unlit');
|
|
356
|
+
let tonemapEntry: ManifestEntry | undefined;
|
|
357
|
+
let motionBlurEntry: ManifestEntry | undefined;
|
|
358
|
+
let spriteEntry: ManifestEntry | undefined;
|
|
359
|
+
// sprite-lit identification marker is the `spriteLitShadeAccum`
|
|
360
|
+
// helper (defined in sprite-lit.wgsl, absent from sprite.wgsl). The
|
|
361
|
+
// outer shading accumulator name is stable across shading-formula
|
|
362
|
+
// changes and unique to sprite-lit. Identified BEFORE sprite so the
|
|
363
|
+
// `pivotAndSize` marker (shared between sprite + sprite-lit since
|
|
364
|
+
// their paramSchema mirror) does not mis-classify sprite-lit as
|
|
365
|
+
// sprite.
|
|
366
|
+
let spriteLitEntry: ManifestEntry | undefined;
|
|
367
|
+
let taaResolveEntry: ManifestEntry | undefined;
|
|
368
|
+
let iblEquirectEntry: ManifestEntry | undefined;
|
|
369
|
+
let iblIrradianceEntry: ManifestEntry | undefined;
|
|
370
|
+
let iblPrefilterEntry: ManifestEntry | undefined;
|
|
371
|
+
let iblBrdfLutEntry: ManifestEntry | undefined;
|
|
372
|
+
let fxaaEntry: ManifestEntry | undefined;
|
|
373
|
+
let skyboxEntry: ManifestEntry | undefined;
|
|
374
|
+
let bloomBrightEntry: ManifestEntry | undefined;
|
|
375
|
+
let bloomBlurEntry: ManifestEntry | undefined;
|
|
376
|
+
let bloomCompositeEntry: ManifestEntry | undefined;
|
|
377
|
+
let ssaoEntry: ManifestEntry | undefined;
|
|
378
|
+
// feat-20260609 R3-fixup: shadow_caster module pre-bake. T-009 deleted
|
|
379
|
+
// the hardcoded shadowCasterPipeline; the lazy
|
|
380
|
+
// getMaterialShaderPipeline path (passKind='shadow-caster') uses
|
|
381
|
+
// the shared adapter cache, which is unwarmed for shadow_caster on
|
|
382
|
+
// frame 1. We eagerly compile + seed the adapter cache so the lazy
|
|
383
|
+
// build hit on `module-forgeax::default-shadow-caster` returns OK
|
|
384
|
+
// without a 1-frame warmup. Resolve it by its reserved manifest identity;
|
|
385
|
+
// game shaders are free to use each vertex-input subset.
|
|
386
|
+
let shadowCasterEntry = findEngineManifestEntry('forgeax::default-shadow-caster');
|
|
387
|
+
for (const entry of manifestEntries) {
|
|
388
|
+
if (entry.wgsl.includes('TonemapParams')) {
|
|
389
|
+
tonemapEntry ??= entry;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (entry.wgsl.includes('MotionBlurParams')) {
|
|
393
|
+
motionBlurEntry ??= entry;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (entry.wgsl.includes('fs_taa_resolve')) {
|
|
397
|
+
taaResolveEntry ??= entry;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (entry.wgsl.includes('spriteLitShadeAccum')) {
|
|
401
|
+
// sprite-lit identification — must precede the `pivotAndSize`
|
|
402
|
+
// marker since sprite-lit also carries that field (paramSchema
|
|
403
|
+
// mirror with sprite.wgsl).
|
|
404
|
+
spriteLitEntry ??= entry;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (entry.wgsl.includes('pivotAndSize')) {
|
|
408
|
+
spriteEntry ??= entry;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (entry.wgsl.includes('equirectToCube_fs')) {
|
|
412
|
+
iblEquirectEntry ??= entry;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (entry.wgsl.includes('irradianceConvolve_fs')) {
|
|
416
|
+
iblIrradianceEntry ??= entry;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (entry.wgsl.includes('prefilterEnv_fs')) {
|
|
420
|
+
iblPrefilterEntry ??= entry;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (entry.wgsl.includes('brdfLutBake_fs')) {
|
|
424
|
+
iblBrdfLutEntry ??= entry;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (entry.wgsl.includes('f_schlick')) {
|
|
428
|
+
pbrEntry ??= entry;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// feat-20260528-fxaa-post-processing: fxaa marker — the composed
|
|
432
|
+
// WGSL contains the rgb2luma helper unique to the FXAA algorithm
|
|
433
|
+
// (plan-strategy D-5). Identified before shadow_caster / unlit
|
|
434
|
+
// fallback so the marker takes priority over generic position-only
|
|
435
|
+
// heuristics.
|
|
436
|
+
if (entry.wgsl.includes('rgb2luma')) {
|
|
437
|
+
fxaaEntry ??= entry;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// feat-20260531-skybox-env-background M3 / w15: skybox marker --
|
|
441
|
+
// the composed WGSL contains the skybox_fs fragment entry point
|
|
442
|
+
// unique to skybox.wgsl (plan-strategy D-7).
|
|
443
|
+
if (entry.wgsl.includes('skybox_fs')) {
|
|
444
|
+
skyboxEntry ??= entry;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
// feat-20260531-bloom-first-declarative-render-graph-pass / w13:
|
|
448
|
+
// bloom marker triage (D-7). Identify the 3 bloom WGSL modules by their
|
|
449
|
+
// unique uniform-struct names ('BloomBrightParams' in bloom-bright.wgsl,
|
|
450
|
+
// 'BloomBlurParams' in bloom-blur.wgsl shared by H/V pipelines,
|
|
451
|
+
// 'BloomCompositeParams' in bloom-composite.wgsl). Struct names are naga
|
|
452
|
+
// IR and survive naga_oil composition unchanged -- unlike the original
|
|
453
|
+
// `// bloomBrightExtract` content-marker COMMENTS, which naga's WGSL
|
|
454
|
+
// writeback drops (comments are not part of the IR), leaving the markers
|
|
455
|
+
// absent from the composed `entry.wgsl` so the triage never matched and
|
|
456
|
+
// bloom silently never initialised (bug-20260625). Same survives-naga
|
|
457
|
+
// rationale as the SSAO `fs_ssao_calc` entry-point marker below.
|
|
458
|
+
if (entry.wgsl.includes('BloomBrightParams')) {
|
|
459
|
+
bloomBrightEntry ??= entry;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (entry.wgsl.includes('BloomBlurParams')) {
|
|
463
|
+
bloomBlurEntry ??= entry;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (entry.wgsl.includes('BloomCompositeParams')) {
|
|
467
|
+
bloomCompositeEntry ??= entry;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
// feat-20260612-hdrp-ssao M6 / w27: SSAO marker triage (D-E).
|
|
471
|
+
// The composed WGSL contains 'fs_ssao_calc' fragment entry point
|
|
472
|
+
// which survives naga_oil composition unchanged. Same pattern as
|
|
473
|
+
// the bloom struct-name markers above.
|
|
474
|
+
if (entry.wgsl.includes('fs_ssao_calc')) {
|
|
475
|
+
ssaoEntry ??= entry;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
unlitEntry ??= entry;
|
|
479
|
+
}
|
|
480
|
+
// Null is structural-only; real backends require the shipped manifest.
|
|
481
|
+
if (
|
|
482
|
+
rhiDevice.caps.backendKind === 'null' &&
|
|
483
|
+
(pbrEntry === undefined || unlitEntry === undefined || tonemapEntry === undefined)
|
|
484
|
+
) {
|
|
485
|
+
const fallback = (hash: string) =>
|
|
486
|
+
({ hash, wgsl: '', glsl: '', bindings: '' }) as ManifestEntry;
|
|
487
|
+
pbrEntry ??= fallback('null-pbr');
|
|
488
|
+
unlitEntry ??= fallback('null-unlit');
|
|
489
|
+
tonemapEntry ??= fallback('null-tonemap');
|
|
490
|
+
}
|
|
491
|
+
if (pbrEntry === undefined || unlitEntry === undefined || tonemapEntry === undefined) {
|
|
492
|
+
throw new RhiError({
|
|
493
|
+
code: 'shader-compile-failed',
|
|
494
|
+
expected:
|
|
495
|
+
'manifest entries include pbr.wgsl + unlit.wgsl + tonemap.wgsl (engine SSOT triple)',
|
|
496
|
+
hint: 'verify @forgeax/engine-vite-plugin-shader emits manifest.json with the 3 engine entries; check vite plugin engineEntries option',
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
// WebGL2 must compile the storage=false material variants against its
|
|
500
|
+
// uniform fallback layouts; patch the flat engine entries accordingly.
|
|
501
|
+
if (!storageBufferCapable) {
|
|
502
|
+
// Match by fields because multi-axis keys are compound and order-sensitive.
|
|
503
|
+
const idToVariantWgsl = new Map<string, string>();
|
|
504
|
+
for (const ms of registry.materialShaderManifestEntries()) {
|
|
505
|
+
const exact = ms.variants.find(
|
|
506
|
+
(v) =>
|
|
507
|
+
v.defines.STORAGE_BUFFER_AVAILABLE === false &&
|
|
508
|
+
(!('CLUSTER_FORWARD_AVAILABLE' in v.defines) ||
|
|
509
|
+
v.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
|
|
510
|
+
(!('VERTEX_COLOR_AVAILABLE' in v.defines) ||
|
|
511
|
+
v.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
512
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in v.defines) ||
|
|
513
|
+
v.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
514
|
+
(!('PROJECTOR_AVAILABLE' in v.defines) ||
|
|
515
|
+
v.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
516
|
+
(!('REFLECTION_FALLBACK_AVAILABLE' in v.defines) ||
|
|
517
|
+
v.defines.REFLECTION_FALLBACK_AVAILABLE === false) &&
|
|
518
|
+
v.defines.PROBE_BLEND_AVAILABLE !== true &&
|
|
519
|
+
(!('SKINNING_DISABLED' in v.defines) || v.defines.SKINNING_DISABLED === true),
|
|
520
|
+
);
|
|
521
|
+
const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
|
|
522
|
+
if (v !== undefined) {
|
|
523
|
+
idToVariantWgsl.set(ms.identifier, v.composedWgsl);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const patch = (entry: ManifestEntry, identifier: string): ManifestEntry => {
|
|
527
|
+
const wgsl = idToVariantWgsl.get(identifier);
|
|
528
|
+
return wgsl !== undefined ? { ...entry, wgsl } : entry;
|
|
529
|
+
};
|
|
530
|
+
pbrEntry = patch(pbrEntry, 'forgeax::default-standard-pbr');
|
|
531
|
+
unlitEntry = patch(unlitEntry, 'forgeax::default-unlit');
|
|
532
|
+
if (shadowCasterEntry !== undefined) {
|
|
533
|
+
// Shadow caster also needs the storage=false uniform fallback.
|
|
534
|
+
shadowCasterEntry = patch(shadowCasterEntry, 'forgeax::default-shadow-caster');
|
|
535
|
+
}
|
|
536
|
+
if (spriteEntry !== undefined) {
|
|
537
|
+
spriteEntry = patch(spriteEntry, 'forgeax::sprite');
|
|
538
|
+
}
|
|
539
|
+
if (spriteLitEntry !== undefined) {
|
|
540
|
+
spriteLitEntry = patch(spriteLitEntry, 'forgeax::sprite-lit');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Patch eager entries to the exact device/URP variant; canonical sources
|
|
544
|
+
// may declare optional vertex inputs absent from the boot layout.
|
|
545
|
+
const idToRuntimeVariantWgsl = new Map<string, string>();
|
|
546
|
+
for (const ms of registry.materialShaderManifestEntries()) {
|
|
547
|
+
const exact = ms.variants.find(
|
|
548
|
+
(variant) =>
|
|
549
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
550
|
+
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
551
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
|
|
552
|
+
(!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
|
|
553
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
554
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
555
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
556
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
557
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
558
|
+
(!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
|
|
559
|
+
variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
|
|
560
|
+
variant.defines.PROBE_BLEND_AVAILABLE !== true &&
|
|
561
|
+
(!('SKINNING_DISABLED' in variant.defines) ||
|
|
562
|
+
variant.defines.SKINNING_DISABLED === true) &&
|
|
563
|
+
(!('TRANSMISSION_AVAILABLE' in variant.defines) ||
|
|
564
|
+
variant.defines.TRANSMISSION_AVAILABLE === false) &&
|
|
565
|
+
(!('REFLECTION_FALLBACK_AVAILABLE' in variant.defines) ||
|
|
566
|
+
variant.defines.REFLECTION_FALLBACK_AVAILABLE === false),
|
|
567
|
+
);
|
|
568
|
+
const variant = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
|
|
569
|
+
if (variant !== undefined) {
|
|
570
|
+
idToRuntimeVariantWgsl.set(ms.identifier, variant.composedWgsl);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const patchRuntimeVariant = (entry: ManifestEntry, identifier: string): ManifestEntry => {
|
|
574
|
+
const wgsl = idToRuntimeVariantWgsl.get(identifier);
|
|
575
|
+
return wgsl !== undefined ? { ...entry, wgsl } : entry;
|
|
576
|
+
};
|
|
577
|
+
pbrEntry = patchRuntimeVariant(pbrEntry, 'forgeax::default-standard-pbr');
|
|
578
|
+
unlitEntry = patchRuntimeVariant(unlitEntry, 'forgeax::default-unlit');
|
|
579
|
+
if (shadowCasterEntry !== undefined) {
|
|
580
|
+
shadowCasterEntry = patchRuntimeVariant(shadowCasterEntry, 'forgeax::default-shadow-caster');
|
|
581
|
+
}
|
|
582
|
+
if (spriteEntry !== undefined) {
|
|
583
|
+
spriteEntry = patchRuntimeVariant(spriteEntry, 'forgeax::sprite');
|
|
584
|
+
}
|
|
585
|
+
if (spriteLitEntry !== undefined) {
|
|
586
|
+
spriteLitEntry = patchRuntimeVariant(spriteLitEntry, 'forgeax::sprite-lit');
|
|
587
|
+
}
|
|
588
|
+
// bug-20260708 M2 (a): sprite entry WGSL patch — always substitute the
|
|
589
|
+
// PIR=false variant so the eager compile at line ~4272 (which produces
|
|
590
|
+
// `spriteModule`, seeded under `module-forgeax::sprite` at line ~4297 and
|
|
591
|
+
// exposed via `shaderModuleMap.set('forgeax::sprite', ...)` at line
|
|
592
|
+
// ~5438) yields the 5575B PIR=false shader module. The character /
|
|
593
|
+
// sprite-atlas per-entity path (`main-pass-sprite-draws.ts:511-513`)
|
|
594
|
+
// routes `variantSet=undefined` through `getMaterialShaderPipeline`
|
|
595
|
+
// with `moduleLabel='module-forgeax::sprite'` — hitting this seeded
|
|
596
|
+
// module. SpriteInstances batches request `variantSet=''` →
|
|
597
|
+
// `moduleLabel='module-forgeax::sprite#'` (distinct label) → compile
|
|
598
|
+
// fresh from `findVariantByKey(msEntry, '')` returning the PIR=true
|
|
599
|
+
// 5653B variant. The upstream `manifestEntries` populated
|
|
600
|
+
// `spriteEntry.wgsl` with the plugin default (all-true canonical =
|
|
601
|
+
// PIR=true+SBA=true); we substitute the matching-SBA PIR=false variant
|
|
602
|
+
// here. Runs regardless of `storageBufferCapable` — the WebGL2 fallback
|
|
603
|
+
// patch above (bug-20260610) already substitutes SBA=false; we now
|
|
604
|
+
// substitute PIR=false on top so the boot-time seeded module for
|
|
605
|
+
// sprite is aligned with the boot-registered material-shader source
|
|
606
|
+
// (patched at line ~3710 via the PER_INSTANCE_REGION extension above).
|
|
607
|
+
if (spriteEntry !== undefined) {
|
|
608
|
+
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
609
|
+
if (msEntry.identifier === 'forgeax::sprite') {
|
|
610
|
+
const pirFalseVariant = msEntry.variants.find(
|
|
611
|
+
(v) =>
|
|
612
|
+
v.defines.PER_INSTANCE_REGION === false &&
|
|
613
|
+
v.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
|
|
614
|
+
);
|
|
615
|
+
if (pirFalseVariant !== undefined) {
|
|
616
|
+
spriteEntry = { ...spriteEntry, wgsl: pirFalseVariant.composedWgsl };
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Wire composed IBL shaders into IblPipelineCache before the cache's
|
|
623
|
+
// createIblPipelines runs (called downstream by the internal
|
|
624
|
+
// GpuResidencyCache equirect-to-cubemap projection during the first Skylight
|
|
625
|
+
// dispatch). When all 4 are present, register them; otherwise leave
|
|
626
|
+
// the cache untouched (charter F1: tests / non-IBL hosts that ship
|
|
627
|
+
// empty / 3-entry manifests still boot, and the IBL pipeline cache's
|
|
628
|
+
// own error surfacing covers the "missing" case downstream).
|
|
629
|
+
if (
|
|
630
|
+
iblEquirectEntry !== undefined &&
|
|
631
|
+
iblIrradianceEntry !== undefined &&
|
|
632
|
+
iblPrefilterEntry !== undefined &&
|
|
633
|
+
iblBrdfLutEntry !== undefined
|
|
634
|
+
) {
|
|
635
|
+
setIblComposedShaders({
|
|
636
|
+
equirectToCube: iblEquirectEntry.wgsl,
|
|
637
|
+
irradiance: iblIrradianceEntry.wgsl,
|
|
638
|
+
prefilter: iblPrefilterEntry.wgsl,
|
|
639
|
+
brdfLut: iblBrdfLutEntry.wgsl,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
// Points/Lines are an engine-owned record path rather than a producer
|
|
643
|
+
// RenderFeature, so they cannot declare `requiredMaterialShaders`. Their
|
|
644
|
+
// material pipeline still goes through the same lazy shader-module adapter
|
|
645
|
+
// as producer features. Prewarm the optional manifest artifact here so a
|
|
646
|
+
// first frame cannot race async module compilation and silently skip its
|
|
647
|
+
// draw while the adapter is still pending. Older/minimal manifests may not
|
|
648
|
+
// carry the optional entry; those hosts retain their existing behavior.
|
|
649
|
+
const pointsLinesLookup = registry.findMaterialArtifact(POINTS_LINES_MATERIAL_SHADER_ID);
|
|
650
|
+
if (pointsLinesLookup.ok) {
|
|
651
|
+
const pointsLinesLabel = `module-${POINTS_LINES_MATERIAL_SHADER_ID}`;
|
|
652
|
+
const pointsLinesShaderResult = await runShimStep(
|
|
653
|
+
() =>
|
|
654
|
+
asyncCreateShaderModule
|
|
655
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
656
|
+
code: pointsLinesLookup.value.source,
|
|
657
|
+
label: pointsLinesLabel,
|
|
658
|
+
})
|
|
659
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
660
|
+
code: pointsLinesLookup.value.source,
|
|
661
|
+
label: pointsLinesLabel,
|
|
662
|
+
}),
|
|
663
|
+
'shader-compile-failed',
|
|
664
|
+
`engine material shader '${POINTS_LINES_MATERIAL_SHADER_ID}' compiled`,
|
|
665
|
+
`inspect the composed WGSL for '${POINTS_LINES_MATERIAL_SHADER_ID}' and check device.features`,
|
|
666
|
+
);
|
|
667
|
+
if (!pointsLinesShaderResult.ok) throw pointsLinesShaderResult.error;
|
|
668
|
+
seedShaderModule(pointsLinesLabel, pointsLinesShaderResult.value);
|
|
669
|
+
}
|
|
670
|
+
await prewarmRequiredMaterialShaders({
|
|
671
|
+
rhiDevice,
|
|
672
|
+
registry,
|
|
673
|
+
asyncCreateShaderModule,
|
|
674
|
+
requiredMaterialShaders,
|
|
675
|
+
seedShaderModule,
|
|
676
|
+
});
|
|
677
|
+
for (const postProcess of requiredFullscreenPostProcesses) {
|
|
678
|
+
const label = postProcessShaderModuleLabel(postProcess.identity, postProcess.source);
|
|
679
|
+
const shaderResult = await runShimStep(
|
|
680
|
+
() =>
|
|
681
|
+
asyncCreateShaderModule
|
|
682
|
+
? asyncCreateShaderModule(rhiDevice, { code: postProcess.source, label })
|
|
683
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: postProcess.source, label }),
|
|
684
|
+
'shader-compile-failed',
|
|
685
|
+
`declared fullscreen render feature '${postProcess.identity}' compiled`,
|
|
686
|
+
`inspect the composed WGSL for '${postProcess.identity}' and check device.features`,
|
|
687
|
+
);
|
|
688
|
+
if (!shaderResult.ok) throw shaderResult.error;
|
|
689
|
+
seedShaderModule(label, shaderResult.value);
|
|
690
|
+
}
|
|
691
|
+
if (rhiDevice.caps.compute && rhiDevice.caps.storageTexture) {
|
|
692
|
+
const volumeSources = [
|
|
693
|
+
['inject', 'volume_inject', (source: string) => source.includes('fn volume_inject(')],
|
|
694
|
+
['temporal', 'volume_temporal', (source: string) => source.includes('fn volume_temporal(')],
|
|
695
|
+
[
|
|
696
|
+
'integrate',
|
|
697
|
+
'volume_integrate',
|
|
698
|
+
(source: string) => source.includes('fn volume_integrate('),
|
|
699
|
+
],
|
|
700
|
+
['composite', 'volume_composite', (source: string) => source.includes('fn volume_fs(')],
|
|
701
|
+
] as const;
|
|
702
|
+
const resolved = volumeSources.map(([stage, label, matches]) => ({
|
|
703
|
+
stage,
|
|
704
|
+
label,
|
|
705
|
+
entry: manifestEntries.find((candidate) => matches(candidate.wgsl)),
|
|
706
|
+
}));
|
|
707
|
+
const present = resolved.filter(({ entry }) => entry !== undefined).length;
|
|
708
|
+
if (present > 0) {
|
|
709
|
+
const missing = resolved.find(({ entry }) => entry === undefined);
|
|
710
|
+
if (missing !== undefined) {
|
|
711
|
+
throw new RhiError({
|
|
712
|
+
code: 'shader-compile-failed',
|
|
713
|
+
expected: 'shader manifest contains all four volumetric fog utility entries',
|
|
714
|
+
hint: `add the package-owned WGSL entry for '${missing.label}' to the engine shader manifest`,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const compiled: Array<{
|
|
718
|
+
readonly stage: (typeof volumeSources)[number][0];
|
|
719
|
+
readonly label: string;
|
|
720
|
+
readonly source: string;
|
|
721
|
+
readonly module: ShaderModule;
|
|
722
|
+
}> = [];
|
|
723
|
+
for (const { stage, label, entry } of resolved) {
|
|
724
|
+
if (entry === undefined) throw new Error(`missing volumetric shader ${label}`);
|
|
725
|
+
const shaderResult = await runShimStep(
|
|
726
|
+
() =>
|
|
727
|
+
asyncCreateShaderModule
|
|
728
|
+
? asyncCreateShaderModule(rhiDevice, { code: entry.wgsl, label })
|
|
729
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: entry.wgsl, label }),
|
|
730
|
+
'shader-compile-failed',
|
|
731
|
+
`volumetric fog shader module '${label}' compiled`,
|
|
732
|
+
`inspect manifest WGSL for '${label}' and check device.features`,
|
|
733
|
+
);
|
|
734
|
+
if (!shaderResult.ok) throw shaderResult.error;
|
|
735
|
+
compiled.push({ stage, label, source: entry.wgsl, module: shaderResult.value });
|
|
736
|
+
}
|
|
737
|
+
for (const { label, module } of compiled) seedShaderModule(label, module);
|
|
738
|
+
setVolumetricFogShaderSources({
|
|
739
|
+
inject: compiled.find(({ stage }) => stage === 'inject')?.source as string,
|
|
740
|
+
temporal: compiled.find(({ stage }) => stage === 'temporal')?.source as string,
|
|
741
|
+
integrate: compiled.find(({ stage }) => stage === 'integrate')?.source as string,
|
|
742
|
+
composite: compiled.find(({ stage }) => stage === 'composite')?.source as string,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const pbrShaderResult = await runShimStep(
|
|
747
|
+
() =>
|
|
748
|
+
asyncCreateShaderModule
|
|
749
|
+
? asyncCreateShaderModule(rhiDevice, { code: pbrEntry.wgsl, label: 'pbr' })
|
|
750
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: pbrEntry.wgsl, label: 'pbr' }),
|
|
751
|
+
'shader-compile-failed',
|
|
752
|
+
'PBR shader module compiled',
|
|
753
|
+
'inspect manifest pbr entry composed wgsl; check device.features',
|
|
754
|
+
);
|
|
755
|
+
if (!pbrShaderResult.ok) throw pbrShaderResult.error;
|
|
756
|
+
pbrModule = pbrShaderResult.value;
|
|
757
|
+
// feat-20260629-multi-uv-set-support: a real extra-UV mesh creates a
|
|
758
|
+
// layout-specific material PSO after the boot-time standard-layout PSO
|
|
759
|
+
// has been pre-warmed. Seed the shared shader-module adapter under the
|
|
760
|
+
// exact lazy-build labels so that this first-touch PSO can reuse the
|
|
761
|
+
// already compiled PBR variant instead of returning the transient
|
|
762
|
+
// `rhi-not-available` pending signal in a tight draw loop.
|
|
763
|
+
seedShaderModule('module-forgeax::default-standard-pbr', pbrModule);
|
|
764
|
+
const pbrManifestEntry = [...registry.materialShaderManifestEntries()].find(
|
|
765
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
766
|
+
);
|
|
767
|
+
const pbrVariant = pbrManifestEntry?.variants.find(
|
|
768
|
+
(variant) => variant.composedWgsl === pbrEntry.wgsl,
|
|
769
|
+
);
|
|
770
|
+
if (pbrVariant !== undefined) {
|
|
771
|
+
seedShaderModule(`module-forgeax::default-standard-pbr#${pbrVariant.definesKey}`, pbrModule);
|
|
772
|
+
}
|
|
773
|
+
const transmissionCapable =
|
|
774
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
775
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
776
|
+
const transmissionVariants = transmissionCapable
|
|
777
|
+
? selectStandardPbrTransmissionPrewarmVariants(
|
|
778
|
+
pbrManifestEntry,
|
|
779
|
+
storageBufferCapable,
|
|
780
|
+
directionalPcssAvailable,
|
|
781
|
+
projectorAvailable,
|
|
782
|
+
extendedLightingShaderAvailable,
|
|
783
|
+
)
|
|
784
|
+
: [];
|
|
785
|
+
|
|
786
|
+
// HDRP uses the canonical all-true variant key (`''`) and the lazy
|
|
787
|
+
// material pipeline adapter therefore requests a distinct module label.
|
|
788
|
+
// Compile every device-capability-matched HDRP variant and seed its exact
|
|
789
|
+
// label. Color availability is part of the geometry-owned variant axis, so
|
|
790
|
+
// both colored and no-color HDRP sources must be retained.
|
|
791
|
+
const prewarmedPbrModules = new Map<string, ShaderModule>([[pbrEntry.wgsl, pbrModule]]);
|
|
792
|
+
for (const transmissionVariant of transmissionVariants) {
|
|
793
|
+
const moduleLabel = `module-forgeax::default-standard-pbr#${transmissionVariant.definesKey}`;
|
|
794
|
+
let transmissionModule = prewarmedPbrModules.get(transmissionVariant.composedWgsl);
|
|
795
|
+
if (transmissionModule === undefined) {
|
|
796
|
+
const transmissionModuleResult = await runShimStep(
|
|
797
|
+
() =>
|
|
798
|
+
asyncCreateShaderModule
|
|
799
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
800
|
+
code: transmissionVariant.composedWgsl,
|
|
801
|
+
label: moduleLabel,
|
|
802
|
+
})
|
|
803
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
804
|
+
code: transmissionVariant.composedWgsl,
|
|
805
|
+
label: moduleLabel,
|
|
806
|
+
}),
|
|
807
|
+
'shader-compile-failed',
|
|
808
|
+
'Standard transmission shader module compiled',
|
|
809
|
+
'inspect the composed Standard transmission variant WGSL and check device.features',
|
|
810
|
+
);
|
|
811
|
+
if (!transmissionModuleResult.ok) throw transmissionModuleResult.error;
|
|
812
|
+
transmissionModule = transmissionModuleResult.value;
|
|
813
|
+
prewarmedPbrModules.set(transmissionVariant.composedWgsl, transmissionModule);
|
|
814
|
+
}
|
|
815
|
+
seedShaderModule(moduleLabel, transmissionModule);
|
|
816
|
+
}
|
|
817
|
+
for (const hdrpVariant of selectHdrpPbrPrewarmVariants(
|
|
818
|
+
pbrManifestEntry,
|
|
819
|
+
storageBufferCapable,
|
|
820
|
+
extendedLightingShaderAvailable,
|
|
821
|
+
transmissionCapable,
|
|
822
|
+
directionalPcssAvailable,
|
|
823
|
+
projectorAvailable,
|
|
824
|
+
)) {
|
|
825
|
+
const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
|
|
826
|
+
let hdrpModule = prewarmedPbrModules.get(hdrpVariant.composedWgsl);
|
|
827
|
+
if (hdrpModule === undefined) {
|
|
828
|
+
const hdrpModuleResult = await runShimStep(
|
|
829
|
+
() =>
|
|
830
|
+
asyncCreateShaderModule
|
|
831
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
832
|
+
code: hdrpVariant.composedWgsl,
|
|
833
|
+
label: moduleLabel,
|
|
834
|
+
})
|
|
835
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
836
|
+
code: hdrpVariant.composedWgsl,
|
|
837
|
+
label: moduleLabel,
|
|
838
|
+
}),
|
|
839
|
+
'shader-compile-failed',
|
|
840
|
+
'HDRP PBR shader module compiled',
|
|
841
|
+
'inspect the composed HDRP PBR variant WGSL and check device.features',
|
|
842
|
+
);
|
|
843
|
+
if (!hdrpModuleResult.ok) throw hdrpModuleResult.error;
|
|
844
|
+
hdrpModule = hdrpModuleResult.value;
|
|
845
|
+
prewarmedPbrModules.set(hdrpVariant.composedWgsl, hdrpModule);
|
|
846
|
+
}
|
|
847
|
+
seedShaderModule(moduleLabel, hdrpModule);
|
|
848
|
+
}
|
|
849
|
+
// ProbeBlend is an object-level opt-in. Keep its device-matched shader
|
|
850
|
+
// modules warm without adding probe variants to the boot PSO table.
|
|
851
|
+
await prewarmMaterialShaderVariants(
|
|
852
|
+
selectProbePrewarmVariants(
|
|
853
|
+
pbrManifestEntry,
|
|
854
|
+
storageBufferCapable,
|
|
855
|
+
extendedLightingShaderAvailable,
|
|
856
|
+
transmissionCapable,
|
|
857
|
+
directionalPcssAvailable,
|
|
858
|
+
projectorAvailable,
|
|
859
|
+
webgl2Downlevel,
|
|
860
|
+
),
|
|
861
|
+
prewarmedPbrModules,
|
|
862
|
+
(probeVariant, moduleLabel) =>
|
|
863
|
+
runShimStep(
|
|
864
|
+
() =>
|
|
865
|
+
asyncCreateShaderModule
|
|
866
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
867
|
+
code: probeVariant.composedWgsl,
|
|
868
|
+
label: moduleLabel,
|
|
869
|
+
})
|
|
870
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
871
|
+
code: probeVariant.composedWgsl,
|
|
872
|
+
label: moduleLabel,
|
|
873
|
+
}),
|
|
874
|
+
'shader-compile-failed',
|
|
875
|
+
`Standard PBR probe variant ${probeVariant.definesKey || '<default>'} compiled`,
|
|
876
|
+
'inspect the selected Standard PBR probe variant WGSL and device.features',
|
|
877
|
+
),
|
|
878
|
+
seedShaderModule,
|
|
879
|
+
);
|
|
880
|
+
// M2-07-c: Materials.standard with an explicit renderState reaches the
|
|
881
|
+
// lazy material pipeline path, whose module label includes the requested
|
|
882
|
+
// variant set. Reuse the boot-compiled PBR module only for manifest
|
|
883
|
+
// variants with byte-identical composed WGSL; HDRP and WebGL2 variants
|
|
884
|
+
// keep their own module identities and are never aliased here.
|
|
885
|
+
for (const materialEntry of registry.materialShaderManifestEntries()) {
|
|
886
|
+
for (const variant of materialEntry.variants) {
|
|
887
|
+
if (variant.composedWgsl === pbrEntry.wgsl) {
|
|
888
|
+
seedShaderModule(`module-${materialEntry.identifier}#${variant.definesKey}`, pbrModule);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const unlitShaderResult = await runShimStep(
|
|
893
|
+
() =>
|
|
894
|
+
asyncCreateShaderModule
|
|
895
|
+
? asyncCreateShaderModule(rhiDevice, { code: unlitEntry.wgsl, label: 'unlit' })
|
|
896
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: unlitEntry.wgsl, label: 'unlit' }),
|
|
897
|
+
'shader-compile-failed',
|
|
898
|
+
'unlit shader module compiled',
|
|
899
|
+
'inspect manifest unlit entry composed wgsl; check device.features',
|
|
900
|
+
);
|
|
901
|
+
if (!unlitShaderResult.ok) throw unlitShaderResult.error;
|
|
902
|
+
unlitModule = unlitShaderResult.value;
|
|
903
|
+
// Seed the shared lazy adapter with the eagerly compiled module so
|
|
904
|
+
// prepared color-only PSOs build synchronously on their first frame.
|
|
905
|
+
seedShaderModule('unlit', unlitModule);
|
|
906
|
+
// Material render-state variants use the canonical material id as their
|
|
907
|
+
// module-cache label. Seed that alias too so a first-frame stencil,
|
|
908
|
+
// blend, or cull variant does not skip its draw while the adapter warms.
|
|
909
|
+
seedShaderModule('module-forgeax::default-unlit', unlitModule);
|
|
910
|
+
// The canonical colored unlit variant uses a different composed WGSL from
|
|
911
|
+
// the boot-time no-color module. Seed its exact lazy label so the first
|
|
912
|
+
// COLOR_0 draw does not wait for an asynchronous module compile.
|
|
913
|
+
const unlitManifestEntry = [...registry.materialShaderManifestEntries()].find(
|
|
914
|
+
(entry) => entry.identifier === 'forgeax::default-unlit',
|
|
915
|
+
);
|
|
916
|
+
const coloredUnlitVariant = unlitManifestEntry?.variants.find(
|
|
917
|
+
(variant) =>
|
|
918
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === true &&
|
|
919
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
|
|
920
|
+
);
|
|
921
|
+
if (coloredUnlitVariant !== undefined && coloredUnlitVariant.composedWgsl !== unlitEntry.wgsl) {
|
|
922
|
+
const coloredUnlitShaderResult = await runShimStep(
|
|
923
|
+
() =>
|
|
924
|
+
asyncCreateShaderModule
|
|
925
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
926
|
+
code: coloredUnlitVariant.composedWgsl,
|
|
927
|
+
label: 'unlit-vertex-color',
|
|
928
|
+
})
|
|
929
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
930
|
+
code: coloredUnlitVariant.composedWgsl,
|
|
931
|
+
label: 'unlit-vertex-color',
|
|
932
|
+
}),
|
|
933
|
+
'shader-compile-failed',
|
|
934
|
+
'colored unlit shader module compiled',
|
|
935
|
+
'inspect the VERTEX_COLOR_AVAILABLE=true unlit variant and device.features',
|
|
936
|
+
);
|
|
937
|
+
if (!coloredUnlitShaderResult.ok) throw coloredUnlitShaderResult.error;
|
|
938
|
+
seedShaderModule(
|
|
939
|
+
`module-forgeax::default-unlit#${coloredUnlitVariant.definesKey}`,
|
|
940
|
+
coloredUnlitShaderResult.value,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const uncoloredUnlitVariant = unlitManifestEntry?.variants.find(
|
|
944
|
+
(variant) =>
|
|
945
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
946
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
|
|
947
|
+
);
|
|
948
|
+
if (uncoloredUnlitVariant !== undefined) {
|
|
949
|
+
seedShaderModule(
|
|
950
|
+
`module-forgeax::default-unlit#${uncoloredUnlitVariant.definesKey}`,
|
|
951
|
+
unlitModule,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
// feat-20260609 R3-fixup: eagerly compile shadow_caster module + seed
|
|
955
|
+
// the lazy MaterialShader pipeline cache adapter so the first frame's
|
|
956
|
+
// shadow PSO build (passKind='shadow-caster') hits OK without a
|
|
957
|
+
// 1-frame warmup. T-009 deleted the hardcoded shadowCasterPipeline;
|
|
958
|
+
// the lazy path replaces it but the adapter cache key
|
|
959
|
+
// ('module-forgeax::default-shadow-caster') was previously unwarmed,
|
|
960
|
+
// causing the first frame's createShaderModule to return
|
|
961
|
+
// 'rhi-not-available' and the shadow PSO build to fail (which left
|
|
962
|
+
// the shadow depth attachment unwritten). Optional: when the manifest
|
|
963
|
+
// omits shadow_caster (legacy hosts), the lazy path simply falls
|
|
964
|
+
// through to the existing 1-frame retry; no regression.
|
|
965
|
+
if (shadowCasterEntry !== undefined) {
|
|
966
|
+
const shadowCasterShaderResult = await runShimStep(
|
|
967
|
+
() =>
|
|
968
|
+
asyncCreateShaderModule
|
|
969
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
970
|
+
code: shadowCasterEntry.wgsl,
|
|
971
|
+
label: 'shadow_caster',
|
|
972
|
+
})
|
|
973
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
974
|
+
code: shadowCasterEntry.wgsl,
|
|
975
|
+
label: 'shadow_caster',
|
|
976
|
+
}),
|
|
977
|
+
'shader-compile-failed',
|
|
978
|
+
'shadow_caster shader module compiled',
|
|
979
|
+
'inspect manifest shadow_caster entry composed wgsl; check device.features',
|
|
980
|
+
);
|
|
981
|
+
if (!shadowCasterShaderResult.ok) throw shadowCasterShaderResult.error;
|
|
982
|
+
// Seed the adapter cache under the same label
|
|
983
|
+
// (`module-${materialShaderId}`) that getMaterialShaderPipeline
|
|
984
|
+
// uses so the lazy build's createShaderModule call hits OK on
|
|
985
|
+
// frame 1.
|
|
986
|
+
seedShaderModule('module-forgeax::default-shadow-caster', shadowCasterShaderResult.value);
|
|
987
|
+
// The shadow caster now has independent storage and skinning axes. The
|
|
988
|
+
// canonical all-true entry remains the ordinary static source through
|
|
989
|
+
// SKINNING_DISABLED=true; skinned meshes request the explicit false
|
|
990
|
+
// variant. Prewarm both for the active storage capability so neither
|
|
991
|
+
// path loses its first shadow frame to the lazy module retry contract.
|
|
992
|
+
const shadowCasterManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
|
|
993
|
+
(candidate) => candidate.identifier === 'forgeax::default-shadow-caster',
|
|
994
|
+
);
|
|
995
|
+
const activeShadowVariants = shadowCasterManifestEntry?.variants.filter(
|
|
996
|
+
(variant) =>
|
|
997
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
998
|
+
typeof variant.defines.SKINNING_DISABLED === 'boolean',
|
|
999
|
+
);
|
|
1000
|
+
for (const variant of activeShadowVariants ?? []) {
|
|
1001
|
+
if (variant.definesKey === '') {
|
|
1002
|
+
seedShaderModule(
|
|
1003
|
+
'module-forgeax::default-shadow-caster#',
|
|
1004
|
+
shadowCasterShaderResult.value,
|
|
1005
|
+
);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const variantShaderResult = await runShimStep(
|
|
1009
|
+
() =>
|
|
1010
|
+
asyncCreateShaderModule
|
|
1011
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1012
|
+
code: variant.composedWgsl,
|
|
1013
|
+
label: `shadow_caster#${variant.definesKey}`,
|
|
1014
|
+
})
|
|
1015
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1016
|
+
code: variant.composedWgsl,
|
|
1017
|
+
label: `shadow_caster#${variant.definesKey}`,
|
|
1018
|
+
}),
|
|
1019
|
+
'shader-compile-failed',
|
|
1020
|
+
`shadow_caster variant ${variant.definesKey || '<default>'} compiled`,
|
|
1021
|
+
'inspect the selected shadow_caster storage/skinning variant and device.features',
|
|
1022
|
+
);
|
|
1023
|
+
if (!variantShaderResult.ok) throw variantShaderResult.error;
|
|
1024
|
+
seedShaderModule(
|
|
1025
|
+
`module-forgeax::default-shadow-caster#${variant.definesKey}`,
|
|
1026
|
+
variantShaderResult.value,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// feat-20260621 M-A3 (D-5): register the built-in tonemap onto the unified
|
|
1032
|
+
// post-process channel instead of building a dedicated pipeline. The composed
|
|
1033
|
+
// tonemap WGSL (`tonemapEntry.wgsl`, @group(1) bindings after w16) becomes the
|
|
1034
|
+
// registered `entry.source`; fullscreen effect registration eager-creates the 16 B
|
|
1035
|
+
// params UBO (fail-fast). The empty-manifest path never reaches here (the
|
|
1036
|
+
// manifest triple guard above throws when tonemapEntry is undefined), so
|
|
1037
|
+
// registration is unconditional within this gate.
|
|
1038
|
+
//
|
|
1039
|
+
// Eager pre-warm (zero-regression): the dedicated tonemap pipeline used to be
|
|
1040
|
+
// built during `ready`, so tonemap rendered correctly on frame 1 with NO
|
|
1041
|
+
// event-loop yield. The unified `getPostProcessPipeline` lazy path otherwise
|
|
1042
|
+
// returns `rhi-not-available` until the async shader-compile promise resolves
|
|
1043
|
+
// -- a consumer driving `draw()` in a tight loop without an `await` between
|
|
1044
|
+
// frames (e.g. the hello-tonemap dawn smoke) would stall on a black frame
|
|
1045
|
+
// forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
|
|
1046
|
+
// here and `seedShaderModule` it under the exact label
|
|
1047
|
+
// `buildPostProcessPipeline` requests (`post-process-${id}-module`), so the
|
|
1048
|
+
// first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
|
|
1049
|
+
// synchronously and builds the pipeline on frame 1.
|
|
1050
|
+
const tonemapPrewarm = await runShimStep(
|
|
1051
|
+
() =>
|
|
1052
|
+
asyncCreateShaderModule
|
|
1053
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1054
|
+
code: tonemapEntry.wgsl,
|
|
1055
|
+
label: postProcessShaderModuleLabel(
|
|
1056
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
1057
|
+
tonemapEntry.wgsl,
|
|
1058
|
+
),
|
|
1059
|
+
})
|
|
1060
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1061
|
+
code: tonemapEntry.wgsl,
|
|
1062
|
+
label: postProcessShaderModuleLabel(
|
|
1063
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
1064
|
+
tonemapEntry.wgsl,
|
|
1065
|
+
),
|
|
1066
|
+
}),
|
|
1067
|
+
'shader-compile-failed',
|
|
1068
|
+
'tonemap shader module compiled (unified post-process prewarm)',
|
|
1069
|
+
'inspect manifest tonemap entry composed wgsl; check device.features',
|
|
1070
|
+
);
|
|
1071
|
+
if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
|
|
1072
|
+
seedShaderModule(
|
|
1073
|
+
postProcessShaderModuleLabel(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, tonemapEntry.wgsl),
|
|
1074
|
+
tonemapPrewarm.value,
|
|
1075
|
+
);
|
|
1076
|
+
registerBuiltinTonemap(tonemapEntry.wgsl);
|
|
1077
|
+
|
|
1078
|
+
// Temporal fullscreen passes execute synchronously from the compiled graph,
|
|
1079
|
+
// so their shader modules must be warmed before the initialization barrier
|
|
1080
|
+
// resolves. Otherwise the first tight draw loop sees an asynchronous
|
|
1081
|
+
// `rhi-not-available` result and leaves the temporal targets cleared even
|
|
1082
|
+
// though the graph topology is present.
|
|
1083
|
+
const prewarmTemporal = async (entry: ManifestEntry | undefined, id: string) => {
|
|
1084
|
+
if (entry === undefined) return;
|
|
1085
|
+
const prewarm = await runShimStep(
|
|
1086
|
+
() =>
|
|
1087
|
+
asyncCreateShaderModule
|
|
1088
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1089
|
+
code: entry.wgsl,
|
|
1090
|
+
label: postProcessShaderModuleLabel(id, entry.wgsl),
|
|
1091
|
+
})
|
|
1092
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1093
|
+
code: entry.wgsl,
|
|
1094
|
+
label: postProcessShaderModuleLabel(id, entry.wgsl),
|
|
1095
|
+
}),
|
|
1096
|
+
'shader-compile-failed',
|
|
1097
|
+
`${id} shader module compiled (unified post-process prewarm)`,
|
|
1098
|
+
`inspect manifest ${id} entry composed wgsl; check device.features`,
|
|
1099
|
+
);
|
|
1100
|
+
if (!prewarm.ok) throw prewarm.error;
|
|
1101
|
+
seedShaderModule(postProcessShaderModuleLabel(id, entry.wgsl), prewarm.value);
|
|
1102
|
+
};
|
|
1103
|
+
await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
|
|
1104
|
+
await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
|
|
1105
|
+
registerBuiltinTemporalPostProcesses({
|
|
1106
|
+
...(motionBlurEntry === undefined ? {} : { motionBlur: motionBlurEntry.wgsl }),
|
|
1107
|
+
...(taaResolveEntry === undefined ? {} : { taaResolve: taaResolveEntry.wgsl }),
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// feat-20260520-2d-sprite-layer-mvp M-3 / w24: sprite shader module
|
|
1111
|
+
// is optional in the 3-tuple legacy manifest (back-compat for apps
|
|
1112
|
+
// that locked their manifest URL before this feat). With the
|
|
1113
|
+
// vite-plugin-shader 4-entry surface (M-3 / w20), spriteEntry is
|
|
1114
|
+
// present and we build both LDR + HDR sprite pipeline variants.
|
|
1115
|
+
if (spriteEntry !== undefined) {
|
|
1116
|
+
const spriteShaderResult = await runShimStep(
|
|
1117
|
+
() =>
|
|
1118
|
+
asyncCreateShaderModule
|
|
1119
|
+
? asyncCreateShaderModule(rhiDevice, { code: spriteEntry.wgsl, label: 'sprite' })
|
|
1120
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1121
|
+
code: spriteEntry.wgsl,
|
|
1122
|
+
label: 'sprite',
|
|
1123
|
+
}),
|
|
1124
|
+
'shader-compile-failed',
|
|
1125
|
+
'sprite shader module compiled',
|
|
1126
|
+
'inspect manifest sprite entry composed wgsl; check device.features',
|
|
1127
|
+
);
|
|
1128
|
+
if (!spriteShaderResult.ok) throw spriteShaderResult.error;
|
|
1129
|
+
spriteModule = spriteShaderResult.value;
|
|
1130
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh CI-fix: seed the
|
|
1131
|
+
// lazy adapter cache under the same label (`module-forgeax::sprite`)
|
|
1132
|
+
// that `getMaterialShaderPipeline -> buildAndCachePipeline ->
|
|
1133
|
+
// buildPipelineForMaterialShader` uses, so the first lazy build of
|
|
1134
|
+
// the sprite PSO at LDR transparent-split time hits the module cache
|
|
1135
|
+
// on frame 1 and does NOT trip the 1-frame `rhi-not-available` skip
|
|
1136
|
+
// (which surfaces as `shader-compile-failed` "manifest entries
|
|
1137
|
+
// include sprite.wgsl" at the spritePH===null branch in
|
|
1138
|
+
// render-system-record.ts §spritePass). Mirrors the
|
|
1139
|
+
// `module-forgeax::default-shadow-caster` prewarm above; the pre-
|
|
1140
|
+
// feat-20260625 dedicated `spritePipeline` baked the module into a
|
|
1141
|
+
// boot-time PSO so this seed step was implicit. Post-w14 the sprite
|
|
1142
|
+
// shares the generic per-MaterialShader lazy build path, which now
|
|
1143
|
+
// needs the explicit seed to preserve frame-1 readiness.
|
|
1144
|
+
seedShaderModule('module-forgeax::sprite', spriteShaderResult.value);
|
|
1145
|
+
const spriteManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
|
|
1146
|
+
(entry) => entry.identifier === 'forgeax::sprite',
|
|
1147
|
+
);
|
|
1148
|
+
const regionVariant = spriteManifestEntry?.variants.find(
|
|
1149
|
+
(variant) =>
|
|
1150
|
+
variant.defines.PER_INSTANCE_REGION === true &&
|
|
1151
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
|
|
1152
|
+
);
|
|
1153
|
+
if (regionVariant !== undefined) {
|
|
1154
|
+
const regionShaderResult = await runShimStep(
|
|
1155
|
+
() =>
|
|
1156
|
+
asyncCreateShaderModule
|
|
1157
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1158
|
+
code: regionVariant.composedWgsl,
|
|
1159
|
+
label: `sprite#${regionVariant.definesKey}`,
|
|
1160
|
+
})
|
|
1161
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1162
|
+
code: regionVariant.composedWgsl,
|
|
1163
|
+
label: `sprite#${regionVariant.definesKey}`,
|
|
1164
|
+
}),
|
|
1165
|
+
'shader-compile-failed',
|
|
1166
|
+
`sprite variant ${regionVariant.definesKey || '<default>'} compiled`,
|
|
1167
|
+
'inspect the selected sprite PER_INSTANCE_REGION variant and device.features',
|
|
1168
|
+
);
|
|
1169
|
+
if (!regionShaderResult.ok) throw regionShaderResult.error;
|
|
1170
|
+
seedShaderModule(
|
|
1171
|
+
`module-forgeax::sprite#${regionVariant.definesKey}`,
|
|
1172
|
+
regionShaderResult.value,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
|
|
1178
|
+
// sprite-lit shader module — mirrors sprite registration shape (same
|
|
1179
|
+
// lazy-build seed under `module-forgeax::sprite-lit`). Optional in the
|
|
1180
|
+
// manifest (back-compat for apps locked to a pre-sprite-lit manifest URL).
|
|
1181
|
+
if (spriteLitEntry !== undefined) {
|
|
1182
|
+
const spriteLitEntryConst = spriteLitEntry;
|
|
1183
|
+
const spriteLitShaderResult = await runShimStep(
|
|
1184
|
+
() =>
|
|
1185
|
+
asyncCreateShaderModule
|
|
1186
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1187
|
+
code: spriteLitEntryConst.wgsl,
|
|
1188
|
+
label: 'sprite-lit',
|
|
1189
|
+
})
|
|
1190
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1191
|
+
code: spriteLitEntryConst.wgsl,
|
|
1192
|
+
label: 'sprite-lit',
|
|
1193
|
+
}),
|
|
1194
|
+
'shader-compile-failed',
|
|
1195
|
+
'sprite-lit shader module compiled',
|
|
1196
|
+
'inspect manifest sprite-lit entry composed wgsl; check device.features',
|
|
1197
|
+
);
|
|
1198
|
+
if (!spriteLitShaderResult.ok) throw spriteLitShaderResult.error;
|
|
1199
|
+
spriteLitModule = spriteLitShaderResult.value;
|
|
1200
|
+
seedShaderModule('module-forgeax::sprite-lit', spriteLitShaderResult.value);
|
|
1201
|
+
// Sprite-lit selects its Standard capability variant at record time.
|
|
1202
|
+
// Warm every device-matched variant before initialization resolves so a
|
|
1203
|
+
// tight smoke/browser frame loop can build the selected PSO synchronously.
|
|
1204
|
+
const spriteLitManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
|
|
1205
|
+
(entry) => entry.identifier === 'forgeax::sprite-lit',
|
|
1206
|
+
);
|
|
1207
|
+
const prewarmedSpriteLitModules = new Map<string, ShaderModule>([
|
|
1208
|
+
[spriteLitEntryConst.wgsl, spriteLitShaderResult.value],
|
|
1209
|
+
]);
|
|
1210
|
+
for (const variant of spriteLitManifestEntry?.variants ?? []) {
|
|
1211
|
+
if (variant.defines.STORAGE_BUFFER_AVAILABLE !== storageBufferCapable) continue;
|
|
1212
|
+
const moduleLabel = `module-forgeax::sprite-lit#${variant.definesKey}`;
|
|
1213
|
+
let variantModule = prewarmedSpriteLitModules.get(variant.composedWgsl);
|
|
1214
|
+
if (variantModule === undefined) {
|
|
1215
|
+
const variantResult = await runShimStep(
|
|
1216
|
+
() =>
|
|
1217
|
+
asyncCreateShaderModule
|
|
1218
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1219
|
+
code: variant.composedWgsl,
|
|
1220
|
+
label: moduleLabel,
|
|
1221
|
+
})
|
|
1222
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1223
|
+
code: variant.composedWgsl,
|
|
1224
|
+
label: moduleLabel,
|
|
1225
|
+
}),
|
|
1226
|
+
'shader-compile-failed',
|
|
1227
|
+
`sprite-lit variant ${variant.definesKey || '<default>'} compiled`,
|
|
1228
|
+
'inspect the selected sprite-lit Standard capability variant and device.features',
|
|
1229
|
+
);
|
|
1230
|
+
if (!variantResult.ok) throw variantResult.error;
|
|
1231
|
+
variantModule = variantResult.value;
|
|
1232
|
+
prewarmedSpriteLitModules.set(variant.composedWgsl, variantModule);
|
|
1233
|
+
}
|
|
1234
|
+
seedShaderModule(moduleLabel, variantModule);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// feat-20260528-fxaa-post-processing: fxaa shader module is optional
|
|
1239
|
+
// (apps with legacy manifests without fxaa.wgsl continue to boot).
|
|
1240
|
+
// Identified by rgb2luma content marker (plan-strategy D-5). When
|
|
1241
|
+
// present, the module is compiled here; pipeline construction happens
|
|
1242
|
+
// alongside the tonemap pipeline below (step 2 prebuild).
|
|
1243
|
+
if (fxaaEntry !== undefined) {
|
|
1244
|
+
const fxaaShaderResult = await runShimStep(
|
|
1245
|
+
() =>
|
|
1246
|
+
asyncCreateShaderModule
|
|
1247
|
+
? asyncCreateShaderModule(rhiDevice, { code: fxaaEntry.wgsl, label: 'fxaa' })
|
|
1248
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1249
|
+
code: fxaaEntry.wgsl,
|
|
1250
|
+
label: 'fxaa',
|
|
1251
|
+
}),
|
|
1252
|
+
'shader-compile-failed',
|
|
1253
|
+
'fxaa shader module compiled',
|
|
1254
|
+
'inspect manifest fxaa entry composed wgsl; check device.features',
|
|
1255
|
+
);
|
|
1256
|
+
if (!fxaaShaderResult.ok) throw fxaaShaderResult.error;
|
|
1257
|
+
fxaaModule = fxaaShaderResult.value;
|
|
1258
|
+
registerBuiltinFxaa(fxaaEntry.wgsl);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// feat-20260531-skybox-env-background M3 / w15: skybox shader module.
|
|
1262
|
+
// Optional (apps with legacy manifests without skybox.wgsl continue to
|
|
1263
|
+
// boot). Identified by skybox_fs content marker (plan-strategy D-7).
|
|
1264
|
+
// Compiled here; pipeline construction happens alongside tonemap/fxaa
|
|
1265
|
+
// in step 2 prebuild below.
|
|
1266
|
+
if (skyboxEntry !== undefined) {
|
|
1267
|
+
const skyboxShaderResult = await runShimStep(
|
|
1268
|
+
() =>
|
|
1269
|
+
asyncCreateShaderModule
|
|
1270
|
+
? asyncCreateShaderModule(rhiDevice, { code: skyboxEntry.wgsl, label: 'skybox' })
|
|
1271
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1272
|
+
code: skyboxEntry.wgsl,
|
|
1273
|
+
label: 'skybox',
|
|
1274
|
+
}),
|
|
1275
|
+
'shader-compile-failed',
|
|
1276
|
+
'skybox shader module compiled',
|
|
1277
|
+
'inspect manifest skybox entry composed wgsl; check device.features',
|
|
1278
|
+
);
|
|
1279
|
+
if (!skyboxShaderResult.ok) throw skyboxShaderResult.error;
|
|
1280
|
+
skyboxModule = skyboxShaderResult.value;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Bloom shader modules remain available to the lazy Bloom pipeline owner.
|
|
1284
|
+
// Their construction is still part of renderer readiness because the
|
|
1285
|
+
// backend may expose only an asynchronous module factory; the expensive
|
|
1286
|
+
// pipeline, binding, and parameter resources stay deferred until admission.
|
|
1287
|
+
if (bloomBrightEntry !== undefined) {
|
|
1288
|
+
const result = await runShimStep(
|
|
1289
|
+
() =>
|
|
1290
|
+
asyncCreateShaderModule
|
|
1291
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1292
|
+
code: bloomBrightEntry.wgsl,
|
|
1293
|
+
label: 'bloom-bright',
|
|
1294
|
+
})
|
|
1295
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1296
|
+
code: bloomBrightEntry.wgsl,
|
|
1297
|
+
label: 'bloom-bright',
|
|
1298
|
+
}),
|
|
1299
|
+
'shader-compile-failed',
|
|
1300
|
+
'bloom-bright shader module compiled',
|
|
1301
|
+
'inspect manifest bloom-bright entry composed wgsl; check device.features',
|
|
1302
|
+
);
|
|
1303
|
+
if (!result.ok) throw result.error;
|
|
1304
|
+
bloomBrightModule = result.value;
|
|
1305
|
+
}
|
|
1306
|
+
if (bloomBlurEntry !== undefined) {
|
|
1307
|
+
const result = await runShimStep(
|
|
1308
|
+
() =>
|
|
1309
|
+
asyncCreateShaderModule
|
|
1310
|
+
? asyncCreateShaderModule(rhiDevice, { code: bloomBlurEntry.wgsl, label: 'bloom-blur' })
|
|
1311
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1312
|
+
code: bloomBlurEntry.wgsl,
|
|
1313
|
+
label: 'bloom-blur',
|
|
1314
|
+
}),
|
|
1315
|
+
'shader-compile-failed',
|
|
1316
|
+
'bloom-blur shader module compiled',
|
|
1317
|
+
'inspect manifest bloom-blur entry composed wgsl; check device.features',
|
|
1318
|
+
);
|
|
1319
|
+
if (!result.ok) throw result.error;
|
|
1320
|
+
bloomBlurModule = result.value;
|
|
1321
|
+
}
|
|
1322
|
+
if (bloomCompositeEntry !== undefined) {
|
|
1323
|
+
const result = await runShimStep(
|
|
1324
|
+
() =>
|
|
1325
|
+
asyncCreateShaderModule
|
|
1326
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1327
|
+
code: bloomCompositeEntry.wgsl,
|
|
1328
|
+
label: 'bloom-composite',
|
|
1329
|
+
})
|
|
1330
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1331
|
+
code: bloomCompositeEntry.wgsl,
|
|
1332
|
+
label: 'bloom-composite',
|
|
1333
|
+
}),
|
|
1334
|
+
'shader-compile-failed',
|
|
1335
|
+
'bloom-composite shader module compiled',
|
|
1336
|
+
'inspect manifest bloom-composite entry composed wgsl; check device.features',
|
|
1337
|
+
);
|
|
1338
|
+
if (!result.ok) throw result.error;
|
|
1339
|
+
bloomCompositeModule = result.value;
|
|
1340
|
+
}
|
|
1341
|
+
// feat-20260612-hdrp-ssao M6 / w27: SSAO shader module compilation.
|
|
1342
|
+
// Optional manifest entry — absent on legacy manifests (zero-overhead
|
|
1343
|
+
// opt-out, same as bloom). Identified by 'fs_ssao_calc' content marker.
|
|
1344
|
+
if (ssaoEntry !== undefined) {
|
|
1345
|
+
const entry = ssaoEntry;
|
|
1346
|
+
const ssaoShaderResult = await runShimStep(
|
|
1347
|
+
() =>
|
|
1348
|
+
asyncCreateShaderModule
|
|
1349
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
1350
|
+
code: entry.wgsl,
|
|
1351
|
+
label: 'hdrp-ssao',
|
|
1352
|
+
})
|
|
1353
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1354
|
+
code: entry.wgsl,
|
|
1355
|
+
label: 'hdrp-ssao',
|
|
1356
|
+
}),
|
|
1357
|
+
'shader-compile-failed',
|
|
1358
|
+
'hdrp-ssao shader module compiled',
|
|
1359
|
+
'inspect manifest hdrp-ssao entry composed wgsl; check device.features',
|
|
1360
|
+
);
|
|
1361
|
+
if (!ssaoShaderResult.ok) throw ssaoShaderResult.error;
|
|
1362
|
+
ssaoModule = ssaoShaderResult.value;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// feat-20260520-skylight-ibl-cubemap M4 round-4 / t59: the PBR pipeline
|
|
1367
|
+
// layout construction migrated to `pbr-pipeline.ts buildPbrPipelineLayouts`.
|
|
1368
|
+
// The factory builds 4 BindGroupLayouts (view + material + mesh-array +
|
|
1369
|
+
// instances) and the 4-slot pipeline layout in one call, throwing on each
|
|
1370
|
+
// device.createBindGroupLayout / device.createPipelineLayout failure.
|
|
1371
|
+
// material BGL now carries 24 entries (canonical material 0..14 + Skylight
|
|
1372
|
+
// 15..21 + transmission 22..23); authored physical maps append after the
|
|
1373
|
+
// engine-owned injections. The pipeline layout itself stays at 4 slots, no
|
|
1374
|
+
// @group(4) allocated (D-5 round-4 fix for the round-2 maxBindGroups=4
|
|
1375
|
+
// BLOCKER). The standalone unlit BGL is the 15-entry user region exposed by
|
|
1376
|
+
// `buildUnlitMaterialBgl`; the shared boot path keeps its own fallback
|
|
1377
|
+
// resources at the material slot.
|
|
1378
|
+
//
|
|
1379
|
+
// feat-20260520-directional-light-shadow-mapping merge: view BGL entries
|
|
1380
|
+
// include binding(3) shadowMap + binding(4) comparison sampler (extended
|
|
1381
|
+
// in `buildPbrViewBglEntries`); the shadow caster pipeline + shadow RT
|
|
1382
|
+
// remain owned by createRenderer below, but the layout is shared.
|
|
1383
|
+
// bug-20260610: storageBufferCapable is hoisted to the top of
|
|
1384
|
+
// buildReadyWebGPU (above) so Step 2 (variant patch) and this layout
|
|
1385
|
+
// build read the same value.
|
|
1386
|
+
const pbrLayouts = runShimSyncStep(
|
|
1387
|
+
() =>
|
|
1388
|
+
ok(
|
|
1389
|
+
buildPbrPipelineLayouts(rhiDevice, {
|
|
1390
|
+
storageBuffer: storageBufferCapable,
|
|
1391
|
+
extendedLighting: extendedLightingShaderAvailable,
|
|
1392
|
+
projectorAvailable,
|
|
1393
|
+
}),
|
|
1394
|
+
),
|
|
1395
|
+
'webgpu-runtime-error',
|
|
1396
|
+
'buildPbrPipelineLayouts succeeded',
|
|
1397
|
+
'check device.limits.maxBindingsPerBindGroup (need >=14) and maxBindGroupsPerPipelineLayout',
|
|
1398
|
+
);
|
|
1399
|
+
if (!pbrLayouts.ok) throw pbrLayouts.error;
|
|
1400
|
+
const viewBglResult = ok(pbrLayouts.value.viewBgl);
|
|
1401
|
+
const materialBglResult = ok(pbrLayouts.value.materialBgl);
|
|
1402
|
+
const meshArrayBglResult = ok(pbrLayouts.value.meshArrayBgl);
|
|
1403
|
+
const instancesBglResult = ok(pbrLayouts.value.instancesBgl);
|
|
1404
|
+
const probeInstancesBglResult = ok(pbrLayouts.value.probeInstancesBgl);
|
|
1405
|
+
const pipelineLayoutResult = ok(pbrLayouts.value.pipelineLayout);
|
|
1406
|
+
|
|
1407
|
+
// feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w36 (D-10 option A):
|
|
1408
|
+
// boot-time build of the HDRP-variant PipelineLayout. The 4-BGL chain is
|
|
1409
|
+
// [view, material, hdrp-unified-7-slot, instances]; the HDRP unified BGL
|
|
1410
|
+
// (createHdrpBindGroupLayoutDescriptor) replaces the 1-slot pbr-mesh-array
|
|
1411
|
+
// BGL at group(2) so an HDRP-variant PSO validates against the 7-slot
|
|
1412
|
+
// group(2) bindGroup that the record stage sets via hdrpClusterBindGroup.
|
|
1413
|
+
// Stays null when createBindGroupLayout / createPipelineLayout fails;
|
|
1414
|
+
// selectPipelineLayoutForVariant gracefully falls back to pbrPipelineLayout
|
|
1415
|
+
// (URP layout) so the manifest entry's URP variant WGSL still builds a
|
|
1416
|
+
// valid PSO instead of hard-disabling the entire HDRP-variant build path.
|
|
1417
|
+
let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
|
|
1418
|
+
let hdrpProbePbrPipelineLayoutHandle: PipelineLayout | null = null;
|
|
1419
|
+
// The cluster layout is storage-buffer backed. On the WebGL2/fallback
|
|
1420
|
+
// capability tier its uniform-buffer projection exceeds the backend's
|
|
1421
|
+
// fragment-stage limit, and HDRP is not a valid route there anyway.
|
|
1422
|
+
if (storageBufferCapable) {
|
|
1423
|
+
const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor();
|
|
1424
|
+
const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
|
|
1425
|
+
if (hdrpUnifiedBglRes.ok) {
|
|
1426
|
+
const hdrpPlRes = rhiDevice.createPipelineLayout({
|
|
1427
|
+
label: 'hdrp-pbr-pl',
|
|
1428
|
+
bindGroupLayouts: [
|
|
1429
|
+
viewBglResult.value,
|
|
1430
|
+
materialBglResult.value,
|
|
1431
|
+
hdrpUnifiedBglRes.value,
|
|
1432
|
+
instancesBglResult.value,
|
|
1433
|
+
],
|
|
1434
|
+
});
|
|
1435
|
+
if (hdrpPlRes.ok) {
|
|
1436
|
+
hdrpPbrPipelineLayoutHandle = hdrpPlRes.value;
|
|
1437
|
+
}
|
|
1438
|
+
if (storageBufferCapable) {
|
|
1439
|
+
const hdrpProbePlRes = rhiDevice.createPipelineLayout({
|
|
1440
|
+
label: 'hdrp-probe-pbr-pl',
|
|
1441
|
+
bindGroupLayouts: [
|
|
1442
|
+
viewBglResult.value,
|
|
1443
|
+
materialBglResult.value,
|
|
1444
|
+
hdrpUnifiedBglRes.value,
|
|
1445
|
+
probeInstancesBglResult.value,
|
|
1446
|
+
],
|
|
1447
|
+
});
|
|
1448
|
+
if (hdrpProbePlRes.ok) hdrpProbePbrPipelineLayoutHandle = hdrpProbePlRes.value;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// The storage path can move the repeated light/cluster membership materializer
|
|
1454
|
+
// into the same command buffer as the deferred lighting pass. This is an
|
|
1455
|
+
// optional producer: module or pipeline creation failure leaves the CPU
|
|
1456
|
+
// binner active, and non-storage backends never attempt the path.
|
|
1457
|
+
let hdrpClusterMembershipPipeline: ComputePipeline | null = null;
|
|
1458
|
+
let hdrpClusterMembershipBindGroupLayout: BindGroupLayout | null = null;
|
|
1459
|
+
if (
|
|
1460
|
+
storageBufferCapable &&
|
|
1461
|
+
rhiDevice.caps.compute &&
|
|
1462
|
+
pbrModule !== null &&
|
|
1463
|
+
unlitModule !== null
|
|
1464
|
+
) {
|
|
1465
|
+
const producerBgl = rhiDevice.createBindGroupLayout(
|
|
1466
|
+
createHdrpClusterMembershipBindGroupLayoutDescriptor(),
|
|
1467
|
+
);
|
|
1468
|
+
if (producerBgl.ok) {
|
|
1469
|
+
hdrpClusterMembershipBindGroupLayout = producerBgl.value;
|
|
1470
|
+
}
|
|
1471
|
+
const membershipModule = asyncCreateShaderModule
|
|
1472
|
+
? await asyncCreateShaderModule(rhiDevice, {
|
|
1473
|
+
code: STANDARD_CLUSTER_MEMBERSHIP_WGSL,
|
|
1474
|
+
label: 'hdrp-cluster-membership',
|
|
1475
|
+
})
|
|
1476
|
+
: await invokeDeviceCreateShaderModule(rhiDevice, {
|
|
1477
|
+
code: STANDARD_CLUSTER_MEMBERSHIP_WGSL,
|
|
1478
|
+
label: 'hdrp-cluster-membership',
|
|
1479
|
+
});
|
|
1480
|
+
if (membershipModule.ok && hdrpClusterMembershipBindGroupLayout !== null) {
|
|
1481
|
+
const producerLayout = rhiDevice.createPipelineLayout({
|
|
1482
|
+
label: 'hdrp-cluster-membership-pl',
|
|
1483
|
+
bindGroupLayouts: [hdrpClusterMembershipBindGroupLayout],
|
|
1484
|
+
});
|
|
1485
|
+
if (producerLayout.ok) {
|
|
1486
|
+
const membershipPipeline = rhiDevice.createComputePipeline({
|
|
1487
|
+
label: 'hdrp-cluster-membership',
|
|
1488
|
+
layout: producerLayout.value,
|
|
1489
|
+
compute: {
|
|
1490
|
+
module: membershipModule.value,
|
|
1491
|
+
entryPoint: 'cs_cluster_membership',
|
|
1492
|
+
},
|
|
1493
|
+
});
|
|
1494
|
+
if (membershipPipeline.ok) {
|
|
1495
|
+
hdrpClusterMembershipPipeline = membershipPipeline.value;
|
|
1496
|
+
}
|
|
1497
|
+
} else {
|
|
1498
|
+
hdrpClusterMembershipBindGroupLayout = null;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// bug-20260611-skin-pipeline-layout-mesh-array-bgl-2bindings: boot-time
|
|
1504
|
+
// build of the skin-variant PipelineLayout. Mirrors the HDRP block above
|
|
1505
|
+
// (D-2 / D-3 in plan-strategy): reuse view / material / instances BGLs
|
|
1506
|
+
// from `pbrLayouts` and only create a 2-entry mesh-array BGL (binding 0
|
|
1507
|
+
// meshes + binding 1 palette) so the `forgeax::pbr-skin` shader's
|
|
1508
|
+
// `@group(2) @binding(1) palette` declaration validates. Stays null when
|
|
1509
|
+
// createBindGroupLayout / createPipelineLayout fails;
|
|
1510
|
+
// selectPipelineLayoutForVariant returns null in that case (charter P3
|
|
1511
|
+
// explicit failure -- no silent fallback to URP layout, mirroring memory
|
|
1512
|
+
// anchor `hdrp-active-must-not-fallback-to-urp-pipeline`).
|
|
1513
|
+
let pbrSkinPipelineLayoutHandle: PipelineLayout | null = null;
|
|
1514
|
+
let pbrSkinProbePipelineLayoutHandle: PipelineLayout | null = null;
|
|
1515
|
+
// feat-20260611 R2 / M8 / w28: capture the 2-binding skin mesh-array BGL
|
|
1516
|
+
// produced by `buildPbrSkinLayouts` so the record stage can build a BG
|
|
1517
|
+
// matching `pbr-skin-pl` (pipeline-layout BGL[2] is this 2-entry skin
|
|
1518
|
+
// BGL, NOT the 1-entry `pbr-mesh-array-bgl`). The BGL handle stays null
|
|
1519
|
+
// when the skin pipeline layout itself failed to build, keeping the skin
|
|
1520
|
+
// path explicitly disabled (charter P3 — record-stage falls back to URP
|
|
1521
|
+
// path which is correct for non-skin entries; skin entries hit the
|
|
1522
|
+
// explicit-failure branch).
|
|
1523
|
+
let pbrSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
|
|
1524
|
+
let hdrpSkinPipelineLayoutHandle: PipelineLayout | null = null;
|
|
1525
|
+
let hdrpSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
|
|
1526
|
+
{
|
|
1527
|
+
const skinLayoutsResult = runShimSyncStep(
|
|
1528
|
+
() =>
|
|
1529
|
+
ok(
|
|
1530
|
+
buildPbrSkinLayouts(rhiDevice, { storageBuffer: storageBufferCapable }, pbrLayouts.value),
|
|
1531
|
+
),
|
|
1532
|
+
'webgpu-runtime-error',
|
|
1533
|
+
'buildPbrSkinLayouts succeeded',
|
|
1534
|
+
'check device.limits.maxBindingsPerBindGroup (need >=14) and maxBindGroupsPerPipelineLayout',
|
|
1535
|
+
);
|
|
1536
|
+
if (skinLayoutsResult.ok) {
|
|
1537
|
+
pbrSkinPipelineLayoutHandle = skinLayoutsResult.value.pipelineLayout;
|
|
1538
|
+
pbrSkinProbePipelineLayoutHandle = skinLayoutsResult.value.probePipelineLayout;
|
|
1539
|
+
pbrSkinMeshBindGroupLayoutHandle = skinLayoutsResult.value.meshArrayBgl;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (storageBufferCapable) {
|
|
1543
|
+
const skinBglResult = rhiDevice.createBindGroupLayout(
|
|
1544
|
+
createHdrpSkinBindGroupLayoutDescriptor(),
|
|
1545
|
+
);
|
|
1546
|
+
if (skinBglResult.ok) {
|
|
1547
|
+
const skinPlResult = rhiDevice.createPipelineLayout({
|
|
1548
|
+
label: 'hdrp-skin-pl',
|
|
1549
|
+
bindGroupLayouts: [
|
|
1550
|
+
viewBglResult.value,
|
|
1551
|
+
materialBglResult.value,
|
|
1552
|
+
skinBglResult.value,
|
|
1553
|
+
instancesBglResult.value,
|
|
1554
|
+
],
|
|
1555
|
+
});
|
|
1556
|
+
if (skinPlResult.ok) {
|
|
1557
|
+
hdrpSkinPipelineLayoutHandle = skinPlResult.value;
|
|
1558
|
+
hdrpSkinMeshBindGroupLayoutHandle = skinBglResult.value;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// feat-20260612-skin-palette-per-frame-upload M1 / m1-2: skin palette
|
|
1564
|
+
// allocator. Replaces the prior 16320 B identity-seeded UBO stub (PR #353,
|
|
1565
|
+
// feat-20260611 R2 / M8 / w28 IS-14, retired identity-buffer field) with
|
|
1566
|
+
// the animator-ready `SkinPaletteAllocator` from
|
|
1567
|
+
// `./systems/skin-palette-allocator`. Per
|
|
1568
|
+
// plan-strategy D-1 candidate (b) the allocator is the single
|
|
1569
|
+
// authoritative carrier of the palette GPU resource -- no parallel
|
|
1570
|
+
// identity-fallback buffer; the boot code only constructs the allocator
|
|
1571
|
+
// and the per-frame extract / record stages drive `allocateSlice` +
|
|
1572
|
+
// `writeJointPalette` (M2 / M3) to land animated palette data. The
|
|
1573
|
+
// allocator's `buffer` is `null` until the first `allocateSlice` call
|
|
1574
|
+
// grows it, and the record stage gates skin entries on `pbrSkinPipelineLayout`
|
|
1575
|
+
// + `skinPaletteAllocator.buffer !== null` (charter P3 explicit failure).
|
|
1576
|
+
//
|
|
1577
|
+
// M6 fix: the cap is the device's max BUFFER binding size for the
|
|
1578
|
+
// selected usage path -- NOT 16320 B (that's the static BG @binding(1)
|
|
1579
|
+
// ENTRY size, a per-draw window slid by dynamic offset; the underlying
|
|
1580
|
+
// buffer must span every entity's window so `dynOffset + entry.size <=
|
|
1581
|
+
// buffer.size` holds for the last skinned draw). Pre-M6 conflated the
|
|
1582
|
+
// two and rejected the 2nd skin entity with SkinPaletteOverflowError.
|
|
1583
|
+
//
|
|
1584
|
+
// Storage path -> `maxStorageBufferBindingSize`; uniform fallback ->
|
|
1585
|
+
// `maxUniformBufferBindingSize` (WebGPU spec floor 64 KiB; 16320 still
|
|
1586
|
+
// fits 4 entities back-to-back even on the floor).
|
|
1587
|
+
const skinPaletteLimitKey = storageBufferCapable
|
|
1588
|
+
? 'maxStorageBufferBindingSize'
|
|
1589
|
+
: 'maxUniformBufferBindingSize';
|
|
1590
|
+
const skinPaletteDeviceLimit = (rhiDevice.limits as Readonly<Record<string, number>>)[
|
|
1591
|
+
skinPaletteLimitKey
|
|
1592
|
+
];
|
|
1593
|
+
const SKIN_PALETTE_MAX_BINDING_BYTES =
|
|
1594
|
+
typeof skinPaletteDeviceLimit === 'number' && skinPaletteDeviceLimit > 0
|
|
1595
|
+
? skinPaletteDeviceLimit
|
|
1596
|
+
: 65536; // WebGPU spec floor for maxUniformBufferBindingSize
|
|
1597
|
+
const skinPaletteAllocatorHandle: SkinPaletteAllocator = createSkinPaletteAllocator(
|
|
1598
|
+
rhiDevice,
|
|
1599
|
+
SKIN_PALETTE_MAX_BINDING_BYTES,
|
|
1600
|
+
storageBufferCapable,
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
// bug-20260519: the legacy `pbr-pipeline-unlit-builtin` (6F-stride + a
|
|
1604
|
+
// 24-byte zero-fill dummy VBO that hard-coded uv=(0,0)) is gone. BUILTIN
|
|
1605
|
+
// geometry now ships 12-floats per vertex (pos + normal + uv + tangent)
|
|
1606
|
+
// identical to procedural meshes, so a single (`unlit-procedural` /
|
|
1607
|
+
// `standard`) pipeline pair covers every renderable.
|
|
1608
|
+
|
|
1609
|
+
// ── Step 3: AssetRegistry builtin mesh GPU upload ─────────────────────────
|
|
1610
|
+
// M5 / w22.7 (feat-20260518-pbr-direct-lighting-mvp): the hard-coded
|
|
1611
|
+
// `[HANDLE_CUBE, HANDLE_TRIANGLE]` loop has moved to AssetRegistry —
|
|
1612
|
+
// `configureGpuDevice` (already invoked above the buildReadyWebGPU call)
|
|
1613
|
+
// replays every registered MeshAsset (incl. BUILTIN_CUBE / HANDLE_TRIANGLE
|
|
1614
|
+
// seeded by the constructor). Step 3 here only seeds the legacy
|
|
1615
|
+
// `pipelineState.meshes` Map for backward compat with existing fixtures
|
|
1616
|
+
// (render-system-record-instances.browser.test.ts etc.); render-system-record
|
|
1617
|
+
// queries `gpuStore.getMeshGpuHandles` first and falls back to this Map only
|
|
1618
|
+
// for the builtins — which keeps user-mesh registrations flowing through the
|
|
1619
|
+
// store's pull path (`ensureResident`) without a createRenderer rebuild
|
|
1620
|
+
// (AGENTS.md "Demo failures route to engine fixes").
|
|
1621
|
+
const queue = rhiDevice.queue;
|
|
1622
|
+
const meshHandles = new Map<number, MeshGpuHandles>();
|
|
1623
|
+
// feat-20260520-2d-sprite-layer-mvp post-merge fix: HANDLE_QUAD joins the
|
|
1624
|
+
// builtin upload loop so sprite materials referencing the unit-quad
|
|
1625
|
+
// through MeshFilter.assetHandle resolve to GPU vertex/index buffers
|
|
1626
|
+
// (record stage's `gpuStore.getMeshGpuHandles(handle)` returns the
|
|
1627
|
+
// GPU pair instead of firing `asset-not-registered`). Builtins are seeded
|
|
1628
|
+
// here (createRenderer step-3 direct upload), not via the store pull path
|
|
1629
|
+
// (D-1), so the upload chain ends here. Same explicit-upload intent as the
|
|
1630
|
+
// pre-existing HANDLE_CUBE / HANDLE_TRIANGLE pair (charter P5 consistent
|
|
1631
|
+
// abstraction).
|
|
1632
|
+
// feat-20260527-sprite-nineslice M2 / w12: HANDLE_NINESLICE_QUAD joins
|
|
1633
|
+
// the explicit-upload list so the 16-vertex / 54-index 9-slice quad has
|
|
1634
|
+
// GPU-resident vertex / index buffers when render-system-record routes
|
|
1635
|
+
// sprite + non-zero-slices entities to it (D-2). Closes feat-20260527
|
|
1636
|
+
// round-1 issue #2 dangling-slot root cause: the prior implement only
|
|
1637
|
+
// added the skip-list entry without the upload, leaving a registered
|
|
1638
|
+
// handle whose `pipelineState.meshes.get(id)` returned undefined.
|
|
1639
|
+
for (const handle of [
|
|
1640
|
+
HANDLE_CUBE,
|
|
1641
|
+
HANDLE_TRIANGLE,
|
|
1642
|
+
HANDLE_QUAD,
|
|
1643
|
+
HANDLE_SPHERE,
|
|
1644
|
+
HANDLE_NINESLICE_QUAD,
|
|
1645
|
+
]) {
|
|
1646
|
+
const id = handleSlot(handle);
|
|
1647
|
+
const gpu = gpuStore.getMeshGpuHandles(handle);
|
|
1648
|
+
if (gpu !== undefined) {
|
|
1649
|
+
meshHandles.set(id, gpu as MeshGpuHandles);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
// Fallback: AssetRegistry was constructed without a wired device
|
|
1653
|
+
// (e.g. test fixtures bypass `configureGpuDevice`); fall back to the
|
|
1654
|
+
// legacy direct-upload path so the BUILTIN seed retains GPU buffers
|
|
1655
|
+
// even on the unwired path.
|
|
1656
|
+
// M5 / w19: HANDLE_CUBE / HANDLE_TRIANGLE are now
|
|
1657
|
+
// `Handle<'MeshAsset','shared'>` (the unified SSOT brand from
|
|
1658
|
+
// `@forgeax/engine-types` per feat-20260517-handle-type-unify) which
|
|
1659
|
+
// matches AssetRegistry.get<MeshAsset>'s parameter type
|
|
1660
|
+
// `Handle<'MeshAsset', 'shared'>` directly — no cross-brand cast.
|
|
1661
|
+
// feat-20260614 M8 (D-15): the builtin seed handles are builtin-tier
|
|
1662
|
+
// (slot < BUILTIN_BASE); resolve their PODs directly from the process-
|
|
1663
|
+
// static BuiltinAssetRegistry (no World needed at boot).
|
|
1664
|
+
const asset = BuiltinAssetRegistry.resolve(handle);
|
|
1665
|
+
if (asset === null) continue;
|
|
1666
|
+
if (asset.kind !== 'mesh') continue;
|
|
1667
|
+
// Builtins always carry indices; the `?? 0` keeps typecheck happy now that
|
|
1668
|
+
// MeshAsset.indices is optional, and supports a vertex-only fallback mesh
|
|
1669
|
+
// if one is ever added here (indexBuffer: null path below).
|
|
1670
|
+
const meshIndices = asset.indices;
|
|
1671
|
+
const vertexBytes = asset.vertices.byteLength;
|
|
1672
|
+
const indexBytesUnpadded = meshIndices?.byteLength ?? 0;
|
|
1673
|
+
const indexBytes = ((indexBytesUnpadded + 3) >> 2) << 2; // round up to multiple of 4
|
|
1674
|
+
const vboLabel =
|
|
1675
|
+
handle === HANDLE_NINESLICE_QUAD
|
|
1676
|
+
? 'nineslice-quad-vbo'
|
|
1677
|
+
: handle === HANDLE_SPHERE
|
|
1678
|
+
? 'sphere-vbo'
|
|
1679
|
+
: handle === HANDLE_CUBE
|
|
1680
|
+
? 'cube-vbo'
|
|
1681
|
+
: handle === HANDLE_QUAD
|
|
1682
|
+
? 'quad-vbo'
|
|
1683
|
+
: 'triangle-vbo';
|
|
1684
|
+
const iboLabel =
|
|
1685
|
+
handle === HANDLE_NINESLICE_QUAD
|
|
1686
|
+
? 'nineslice-quad-ibo'
|
|
1687
|
+
: handle === HANDLE_SPHERE
|
|
1688
|
+
? 'sphere-ibo'
|
|
1689
|
+
: handle === HANDLE_CUBE
|
|
1690
|
+
? 'cube-ibo'
|
|
1691
|
+
: handle === HANDLE_QUAD
|
|
1692
|
+
? 'quad-ibo'
|
|
1693
|
+
: 'triangle-ibo';
|
|
1694
|
+
const vboResult = runShimSyncStep(
|
|
1695
|
+
() =>
|
|
1696
|
+
rhiDevice.createBuffer({
|
|
1697
|
+
label: vboLabel,
|
|
1698
|
+
size: vertexBytes,
|
|
1699
|
+
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1700
|
+
mappedAtCreation: false,
|
|
1701
|
+
}),
|
|
1702
|
+
'webgpu-runtime-error',
|
|
1703
|
+
'createBuffer (vbo) succeeded',
|
|
1704
|
+
'check device.limits.maxBufferSize and remaining VRAM',
|
|
1705
|
+
);
|
|
1706
|
+
if (!vboResult.ok) throw vboResult.error;
|
|
1707
|
+
const vboWrite = runShimSyncStep(
|
|
1708
|
+
() => queue.writeBuffer(vboResult.value, 0, asset.vertices),
|
|
1709
|
+
'queue-write-buffer-out-of-bounds',
|
|
1710
|
+
'queue.writeBuffer (vbo) succeeded',
|
|
1711
|
+
'verify offset alignment and bounds against buffer.size',
|
|
1712
|
+
);
|
|
1713
|
+
if (!vboWrite.ok) throw vboWrite.error;
|
|
1714
|
+
// Vertex-only mesh: skip the index buffer (indexBuffer: null below). The
|
|
1715
|
+
// indexed path is unchanged byte-for-byte when `meshIndices` is present.
|
|
1716
|
+
let ibo: Buffer | null = null;
|
|
1717
|
+
if (meshIndices !== undefined) {
|
|
1718
|
+
const iboResult = runShimSyncStep(
|
|
1719
|
+
() =>
|
|
1720
|
+
rhiDevice.createBuffer({
|
|
1721
|
+
label: iboLabel,
|
|
1722
|
+
size: indexBytes,
|
|
1723
|
+
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1724
|
+
mappedAtCreation: false,
|
|
1725
|
+
}),
|
|
1726
|
+
'webgpu-runtime-error',
|
|
1727
|
+
'createBuffer (ibo) succeeded',
|
|
1728
|
+
'check device.limits.maxBufferSize and remaining VRAM',
|
|
1729
|
+
);
|
|
1730
|
+
if (!iboResult.ok) throw iboResult.error;
|
|
1731
|
+
const indexBuffer = iboResult.value;
|
|
1732
|
+
ibo = indexBuffer;
|
|
1733
|
+
// Pad the source view up to indexBytes (multiple of 4) so writeBuffer's
|
|
1734
|
+
// 4-byte alignment requirement is satisfied even when the index byte
|
|
1735
|
+
// count itself is not a multiple of 4 (e.g. triangle = 6 bytes).
|
|
1736
|
+
const indexSrc = new Uint8Array(indexBytes);
|
|
1737
|
+
indexSrc.set(new Uint8Array(meshIndices.buffer, meshIndices.byteOffset, indexBytesUnpadded));
|
|
1738
|
+
const iboWrite = runShimSyncStep(
|
|
1739
|
+
() => queue.writeBuffer(indexBuffer, 0, indexSrc),
|
|
1740
|
+
'queue-write-buffer-out-of-bounds',
|
|
1741
|
+
'queue.writeBuffer (ibo) succeeded',
|
|
1742
|
+
'verify offset alignment and bounds against buffer.size',
|
|
1743
|
+
);
|
|
1744
|
+
if (!iboWrite.ok) throw iboWrite.error;
|
|
1745
|
+
}
|
|
1746
|
+
// M-3 / w12: builtin direct-upload fallback path mirrors the gpuStore
|
|
1747
|
+
// mesh entry shape -- raw RHI Buffer handles are wrapped in GpuBuffer so
|
|
1748
|
+
// the dispose chain (M-5) can walk them via `.destroy()`.
|
|
1749
|
+
meshHandles.set(id, {
|
|
1750
|
+
vertexBuffer: new GpuBuffer(rhiDevice, vboResult.value),
|
|
1751
|
+
indexBuffer: ibo === null ? null : new GpuBuffer(rhiDevice, ibo),
|
|
1752
|
+
vboBytes: vertexBytes,
|
|
1753
|
+
iboBytes: meshIndices === undefined ? 0 : indexBytes,
|
|
1754
|
+
indexCount: meshIndices?.length ?? 0,
|
|
1755
|
+
indexFormat: meshIndices instanceof Uint32Array ? 'uint32' : 'uint16',
|
|
1756
|
+
// bug-20260519: BUILTIN_CUBE / BUILTIN_TRIANGLE migrated to 12F
|
|
1757
|
+
// (position + normal + uv + tangent), so the fallback literal mirrors
|
|
1758
|
+
// every other mesh upload site.
|
|
1759
|
+
layout: '12F',
|
|
1760
|
+
layoutProjection: deriveVertexLayoutProjection(asset.attributes),
|
|
1761
|
+
// BUILTIN_CUBE / BUILTIN_TRIANGLE are single-UV (set 0 only).
|
|
1762
|
+
uvSetCount: 1,
|
|
1763
|
+
vertexCount:
|
|
1764
|
+
asset.vertices.length /
|
|
1765
|
+
(deriveVertexLayoutProjection(asset.attributes).arrayStride /
|
|
1766
|
+
Float32Array.BYTES_PER_ELEMENT),
|
|
1767
|
+
indexed: meshIndices !== undefined,
|
|
1768
|
+
topology: asset.submeshes[0]?.topology ?? 'triangle-list',
|
|
1769
|
+
submeshes: asset.submeshes,
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// ── Step 3.b: per-pipeline shared UBO / SSBO buffers ──────────────────────
|
|
1774
|
+
// The 3 BindGroups (view / material / mesh) the pbr.wgsl pipeline expects
|
|
1775
|
+
// are built per draw(world) frame in render-system.ts; the underlying
|
|
1776
|
+
// buffers are pipeline-scoped (allocated once, queue.writeBuffer-updated
|
|
1777
|
+
// per frame). The mesh storage path uses a runtime-sized
|
|
1778
|
+
// array<Mesh> bound up to instanceCount * 64 B per draw
|
|
1779
|
+
// (feat-20260511-tetris-retro-followups M4 D-P9); the buffer is initially
|
|
1780
|
+
// sized for INITIAL_MESH_SSBO_SLOT_COUNT = 1024 slots and grows on demand
|
|
1781
|
+
// via `meshSsboController.growMeshSsbo(neededSlots)` (M2 / T-M2-05;
|
|
1782
|
+
// pow2 doubling, ceiling = device.limits.maxStorageBufferBindingSize).
|
|
1783
|
+
const viewUboResult = runShimSyncStep(
|
|
1784
|
+
() =>
|
|
1785
|
+
rhiDevice.createBuffer({
|
|
1786
|
+
label: 'pbr-view-ubo',
|
|
1787
|
+
size: VIEW_UBO_BYTES,
|
|
1788
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1789
|
+
mappedAtCreation: false,
|
|
1790
|
+
}),
|
|
1791
|
+
'webgpu-runtime-error',
|
|
1792
|
+
'createBuffer (view ubo) succeeded',
|
|
1793
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
1794
|
+
);
|
|
1795
|
+
if (!viewUboResult.ok) throw viewUboResult.error;
|
|
1796
|
+
|
|
1797
|
+
const pointsLinesViewBufferResult = runShimSyncStep(
|
|
1798
|
+
() =>
|
|
1799
|
+
rhiDevice.createBuffer({
|
|
1800
|
+
label: 'points-lines-view-ubo',
|
|
1801
|
+
size: POINTS_LINES_VIEW_BUFFER_SIZE,
|
|
1802
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1803
|
+
mappedAtCreation: false,
|
|
1804
|
+
}),
|
|
1805
|
+
'webgpu-runtime-error',
|
|
1806
|
+
'createBuffer (points-lines view ubo) succeeded',
|
|
1807
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
1808
|
+
);
|
|
1809
|
+
if (!pointsLinesViewBufferResult.ok) throw pointsLinesViewBufferResult.error;
|
|
1810
|
+
|
|
1811
|
+
// feat-20260613-csm-cascaded-shadow-maps M5 / w28: per-pass cascade-index
|
|
1812
|
+
// UBO consumed by shadow_caster.wgsl. Stable singleton; the record stage
|
|
1813
|
+
// queue.writeBuffer-overwrites the fields immediately before each pass's
|
|
1814
|
+
// command-encoder submit, and the per-pass submits serialize host writes
|
|
1815
|
+
// against GPU reads.
|
|
1816
|
+
//
|
|
1817
|
+
// feat-20260625-spot-light-shadow-mapping M2 / w10 + w11 (D-1): grew 16 -> 80
|
|
1818
|
+
// B. First 16 B carry `index` u32 + `isSpot` u32 + 2 pad u32 (keeps the
|
|
1819
|
+
// mat4 16 B-aligned for WebGL2); the trailing 64 B carry `spotLightViewProj`
|
|
1820
|
+
// mat4x4<f32>, written per spot tile pass by recordSpotShadowPass and read by
|
|
1821
|
+
// the shadow_caster spot branch. Directional cascade passes leave isSpot=0
|
|
1822
|
+
// and ignore the matrix lanes.
|
|
1823
|
+
const shadowCasterCascadeUboResult = runShimSyncStep(
|
|
1824
|
+
() =>
|
|
1825
|
+
rhiDevice.createBuffer({
|
|
1826
|
+
label: 'shadow-caster-cascade-ubo',
|
|
1827
|
+
size: SHADOW_CASTER_BUFFER_SIZE,
|
|
1828
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1829
|
+
mappedAtCreation: false,
|
|
1830
|
+
}),
|
|
1831
|
+
'webgpu-runtime-error',
|
|
1832
|
+
'createBuffer (shadow-caster cascade ubo) succeeded',
|
|
1833
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
1834
|
+
);
|
|
1835
|
+
if (!shadowCasterCascadeUboResult.ok) throw shadowCasterCascadeUboResult.error;
|
|
1836
|
+
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 / T-M2-05:
|
|
1837
|
+
// mesh + material buffer pair owned by `meshSsboController`; the
|
|
1838
|
+
// controller closes over `rhiDevice` + `internals.errorRegistry` so
|
|
1839
|
+
// `growMeshSsbo(neededSlots)` (called by record stage in M3) can rebuild
|
|
1840
|
+
// both buffers in lock-step (AC-06). Initial allocation lands at
|
|
1841
|
+
// `INITIAL_MESH_SSBO_SLOT_COUNT = 1024` slots (parity with the pre-feat
|
|
1842
|
+
// legacy literal); subsequent grow events pow2-double in one shot
|
|
1843
|
+
// (AC-05). MATERIAL_PER_ENTITY_STRIDE stays at 512 B for the shared
|
|
1844
|
+
// allocation — only slot count grows (OOS-10). Wrapper-object identity (`meshSsboState.mesh` /
|
|
1845
|
+
// `meshSsboState.material`) is stable across grow so PipelineState
|
|
1846
|
+
// fields below reference these wrappers once and survive grow events
|
|
1847
|
+
// (research §F8 R1).
|
|
1848
|
+
// M5 / T-M5-02 (P0 fix surfaced by GRID_SIZE=46 stress smoke):
|
|
1849
|
+
// `MeshSsboGrowDevice.createBuffer` returns a raw `Buffer`, but
|
|
1850
|
+
// `rhiDevice.createBuffer` returns `Result<Buffer, RhiError>` (RHI
|
|
1851
|
+
// explicit-failure contract). The adapter below unwraps that Result
|
|
1852
|
+
// before storing the buffer, preventing a Result wrapper from reaching
|
|
1853
|
+
// queue.submit as
|
|
1854
|
+
// "no overload matched for writeBuffer: object is not of the correct
|
|
1855
|
+
// interface type" once the 1024-slot grow path actually fired (a
|
|
1856
|
+
// workload >= 1024 entities; M5 culling stress is the first to land).
|
|
1857
|
+
// Unit tests at M2-02/03 mock createBuffer to return `Buffer` directly,
|
|
1858
|
+
// so they never caught the mismatch. Adapter unwraps the Result here
|
|
1859
|
+
// (errors bubble to the surrounding `runShimSyncStep` / record-stage
|
|
1860
|
+
// outer try/catch as `webgpu-runtime-error`).
|
|
1861
|
+
const meshSsboGrowDeviceAdapter: MeshSsboGrowDevice = {
|
|
1862
|
+
limits: rhiDevice.limits,
|
|
1863
|
+
createBuffer: (descriptor) => {
|
|
1864
|
+
const result = rhiDevice.createBuffer(descriptor);
|
|
1865
|
+
if (!result.ok) throw result.error;
|
|
1866
|
+
return result.value;
|
|
1867
|
+
},
|
|
1868
|
+
};
|
|
1869
|
+
const meshSsboController = createMeshSsboGrowController({
|
|
1870
|
+
device: meshSsboGrowDeviceAdapter,
|
|
1871
|
+
errorRegistry: errorRegistry,
|
|
1872
|
+
initialSlotCount: INITIAL_MESH_SSBO_SLOT_COUNT,
|
|
1873
|
+
perEntityStride: MATERIAL_PER_ENTITY_STRIDE,
|
|
1874
|
+
// bug-20260610: WebGL2 fallback uses uniform-buffer for the mesh array
|
|
1875
|
+
// (matches the STORAGE_BUFFER_AVAILABLE=false shader variant which
|
|
1876
|
+
// declares `var<uniform> meshes : array<Mesh, 128>` instead of
|
|
1877
|
+
// `var<storage> meshes : array<Mesh>`).
|
|
1878
|
+
meshUsage: storageBufferCapable
|
|
1879
|
+
? GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST
|
|
1880
|
+
: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1881
|
+
materialUsage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1882
|
+
});
|
|
1883
|
+
const initialBuildResult = runShimSyncStep<true>(
|
|
1884
|
+
() => {
|
|
1885
|
+
meshSsboController.initialBuild();
|
|
1886
|
+
return ok(true as const);
|
|
1887
|
+
},
|
|
1888
|
+
'webgpu-runtime-error',
|
|
1889
|
+
'createBuffer (mesh ssbo + material ubo initial build) succeeded',
|
|
1890
|
+
'check device.limits.maxStorageBufferBindingSize / maxUniformBufferBindingSize',
|
|
1891
|
+
);
|
|
1892
|
+
if (!initialBuildResult.ok) throw initialBuildResult.error;
|
|
1893
|
+
const meshSsboState = meshSsboController.state;
|
|
1894
|
+
requireMeshSsboBuffer(meshSsboState.mesh);
|
|
1895
|
+
requireMeshSsboBuffer(meshSsboState.material);
|
|
1896
|
+
// Expose the grow hook + state on internals (via the setGrowMeshSsboHook
|
|
1897
|
+
// callback) so M3's record stage `ensureMeshSsboCapacity` can call it
|
|
1898
|
+
// (T-M2-05 acceptanceCheck #3 + T-M3-04 wiring: `growMeshSsbo` mention
|
|
1899
|
+
// count >= 2 in createRenderer.ts — definition + this exposure).
|
|
1900
|
+
setGrowMeshSsboHook(meshSsboController.growMeshSsbo, meshSsboController.state);
|
|
1901
|
+
|
|
1902
|
+
// feat-20260513-instanced-mesh M3 (T-M3-2): identity-mat4 fallback
|
|
1903
|
+
// storage buffer. Single 64-byte storage buffer carrying one identity
|
|
1904
|
+
// mat4 column-major. Renderables without an `Instances` component bind
|
|
1905
|
+
// this buffer at @group(3) so the shader's
|
|
1906
|
+
// `instances_local[instance_index]` lookup at idx=0 returns I (no
|
|
1907
|
+
// additional transform), giving the consistent-abstraction single
|
|
1908
|
+
// branch (charter prop 5; plan D-7 fallback semantics). The buffer is
|
|
1909
|
+
// seeded once at pipeline creation; the record-stage never rewrites
|
|
1910
|
+
// it.
|
|
1911
|
+
// bug-20260610: WebGL2 fallback uses uniform-buffer for instances
|
|
1912
|
+
// (the shader's `STORAGE_BUFFER_AVAILABLE=false` variant declares
|
|
1913
|
+
// `var<uniform> instances : array<InstanceData, 128>`).
|
|
1914
|
+
//
|
|
1915
|
+
// fix: the shared identity instance buffer is bound at @group(3) for EVERY
|
|
1916
|
+
// pipeline variant that lacks per-entity Instances, including the sprite
|
|
1917
|
+
// `PER_INSTANCE_REGION=true` variant whose InstanceData is 80 bytes
|
|
1918
|
+
// (mat4 64B + region vec4 16B) rather than the base PBR 64B (mat4 only).
|
|
1919
|
+
// The transparent split pass reaches that 80B-min-binding pipeline as soon
|
|
1920
|
+
// as transparent geometry exists (e.g. a glTF alphaMode=BLEND material),
|
|
1921
|
+
// and a 64B buffer fails WebGPU's min-binding-size validation
|
|
1922
|
+
// ("requires a buffer binding which is at least 80 bytes"), invalidating the
|
|
1923
|
+
// frame's command buffer. Size the shared identity buffer to the largest
|
|
1924
|
+
// InstanceData variant (80B) so it satisfies both the 64B PBR and 80B
|
|
1925
|
+
// sprite-region layouts; the extra 16 region bytes stay zeroed (a valid
|
|
1926
|
+
// empty UV region) and the PBR shader never reads past its mat4.
|
|
1927
|
+
const IDENTITY_INSTANCE_BYTES = storageBufferCapable ? 128 : 80 * 128;
|
|
1928
|
+
const identityInstanceResult = runShimSyncStep(
|
|
1929
|
+
() =>
|
|
1930
|
+
rhiDevice.createBuffer({
|
|
1931
|
+
label: 'pbr-identity-instance-ssbo',
|
|
1932
|
+
size: IDENTITY_INSTANCE_BYTES,
|
|
1933
|
+
usage: storageBufferCapable
|
|
1934
|
+
? GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST
|
|
1935
|
+
: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
1936
|
+
mappedAtCreation: false,
|
|
1937
|
+
}),
|
|
1938
|
+
'webgpu-runtime-error',
|
|
1939
|
+
'createBuffer (identity instance ssbo) succeeded',
|
|
1940
|
+
'check device.limits.maxStorageBufferBindingSize',
|
|
1941
|
+
);
|
|
1942
|
+
if (!identityInstanceResult.ok) throw identityInstanceResult.error;
|
|
1943
|
+
// Seed the identity mat4 (column-major; diagonal 1s). Storage-backed
|
|
1944
|
+
// InstanceData carries current + previous transforms, so mirror the mat4
|
|
1945
|
+
// into both slots; uniform-backed variants consume only the first slot.
|
|
1946
|
+
const identityPayload = new Float32Array(storageBufferCapable ? 32 : 16);
|
|
1947
|
+
for (const base of storageBufferCapable ? [0, 16] : [0]) {
|
|
1948
|
+
identityPayload[base] = 1;
|
|
1949
|
+
identityPayload[base + 5] = 1;
|
|
1950
|
+
identityPayload[base + 10] = 1;
|
|
1951
|
+
identityPayload[base + 15] = 1;
|
|
1952
|
+
}
|
|
1953
|
+
const identityWrite = runShimSyncStep(
|
|
1954
|
+
() => queue.writeBuffer(identityInstanceResult.value, 0, identityPayload),
|
|
1955
|
+
'queue-write-buffer-out-of-bounds',
|
|
1956
|
+
'queue.writeBuffer (identity instance ssbo) succeeded',
|
|
1957
|
+
'verify offset alignment and bounds against buffer.size',
|
|
1958
|
+
);
|
|
1959
|
+
if (!identityWrite.ok) throw identityWrite.error;
|
|
1960
|
+
|
|
1961
|
+
// feat-20260515 M3 / T-M3-05 (research F-6 fix): default sampler + fallback
|
|
1962
|
+
// 1x1 white texture seed the materialBindGroup sampler / textureView
|
|
1963
|
+
// entries when MaterialAsset.baseColorTexture is undefined. Default
|
|
1964
|
+
// sampler matches research F-5 SSOT three-source convergence (linear
|
|
1965
|
+
// min/mag/mipmap; repeat addressMode); the fallback texture is a 1x1
|
|
1966
|
+
// RGBA8 white pixel so unlit / standard materials with no texture
|
|
1967
|
+
// multiply by 1 in M5 once UV-driven sampling lands.
|
|
1968
|
+
const defaultSamplerResult = runShimSyncStep(
|
|
1969
|
+
() =>
|
|
1970
|
+
rhiDevice.createSampler({
|
|
1971
|
+
label: 'default-sampler',
|
|
1972
|
+
magFilter: 'linear',
|
|
1973
|
+
minFilter: 'linear',
|
|
1974
|
+
mipmapFilter: 'linear',
|
|
1975
|
+
addressModeU: 'repeat',
|
|
1976
|
+
addressModeV: 'repeat',
|
|
1977
|
+
}),
|
|
1978
|
+
'webgpu-runtime-error',
|
|
1979
|
+
'createSampler (default) succeeded',
|
|
1980
|
+
'check device.limits.maxSamplersPerShaderStage',
|
|
1981
|
+
);
|
|
1982
|
+
if (!defaultSamplerResult.ok) throw defaultSamplerResult.error;
|
|
1983
|
+
|
|
1984
|
+
const nearestSamplerResult = runShimSyncStep(
|
|
1985
|
+
() =>
|
|
1986
|
+
rhiDevice.createSampler({
|
|
1987
|
+
label: 'nearest-sampler',
|
|
1988
|
+
magFilter: 'nearest',
|
|
1989
|
+
minFilter: 'nearest',
|
|
1990
|
+
mipmapFilter: 'nearest',
|
|
1991
|
+
addressModeU: 'clamp-to-edge',
|
|
1992
|
+
addressModeV: 'clamp-to-edge',
|
|
1993
|
+
}),
|
|
1994
|
+
'webgpu-runtime-error',
|
|
1995
|
+
'createSampler (nearest) succeeded',
|
|
1996
|
+
'check device.limits.maxSamplersPerShaderStage',
|
|
1997
|
+
);
|
|
1998
|
+
if (!nearestSamplerResult.ok) throw nearestSamplerResult.error;
|
|
1999
|
+
|
|
2000
|
+
// feat-20260520-directional-light-shadow-mapping M1c / w8 + M2 / w14:
|
|
2001
|
+
// shadow comparison sampler — clamp-to-edge, linear filter, compare:'less'.
|
|
2002
|
+
// Used for shadow map sampling in M2/M3 and shadow depth pass. comparison
|
|
2003
|
+
// sampler enables textureSampleCompareLevel in pbr.wgsl's evalDirectional().
|
|
2004
|
+
// Created once at pipeline build time; reused across frames.
|
|
2005
|
+
const shadowSamplerResult = runShimSyncStep(
|
|
2006
|
+
() =>
|
|
2007
|
+
rhiDevice.createSampler({
|
|
2008
|
+
label: 'shadow-sampler',
|
|
2009
|
+
magFilter: 'linear',
|
|
2010
|
+
minFilter: 'linear',
|
|
2011
|
+
mipmapFilter: 'linear',
|
|
2012
|
+
addressModeU: 'clamp-to-edge',
|
|
2013
|
+
addressModeV: 'clamp-to-edge',
|
|
2014
|
+
compare: 'less',
|
|
2015
|
+
}),
|
|
2016
|
+
'webgpu-runtime-error',
|
|
2017
|
+
'createSampler (shadow) succeeded',
|
|
2018
|
+
'check device.limits.maxSamplersPerShaderStage',
|
|
2019
|
+
);
|
|
2020
|
+
if (!shadowSamplerResult.ok) throw shadowSamplerResult.error;
|
|
2021
|
+
|
|
2022
|
+
const fallbackTextureResult = runShimSyncStep(
|
|
2023
|
+
() =>
|
|
2024
|
+
rhiDevice.createTexture({
|
|
2025
|
+
label: 'fallback-white-1x1',
|
|
2026
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
2027
|
+
mipLevelCount: 1,
|
|
2028
|
+
sampleCount: 1,
|
|
2029
|
+
dimension: '2d',
|
|
2030
|
+
format: 'rgba8unorm',
|
|
2031
|
+
usage: GPU_TEXTURE_USAGE_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_DST,
|
|
2032
|
+
viewFormats: [],
|
|
2033
|
+
textureBindingViewDimension: undefined,
|
|
2034
|
+
}),
|
|
2035
|
+
'webgpu-runtime-error',
|
|
2036
|
+
'createTexture (fallback white) succeeded',
|
|
2037
|
+
'check device.limits.maxTextureDimension2D',
|
|
2038
|
+
);
|
|
2039
|
+
if (!fallbackTextureResult.ok) throw fallbackTextureResult.error;
|
|
2040
|
+
|
|
2041
|
+
// The fallback white pixel is a 1x1 RGBA8 sample. The forgeax rhi shim
|
|
2042
|
+
// enforces `bytesPerRow % 256 === 0` regardless of row count (spec
|
|
2043
|
+
// normative for multi-row copies, but the shim is uniformly strict).
|
|
2044
|
+
// Pad the source buffer to a 256-byte row stride; the upload still
|
|
2045
|
+
// writes only 1x1 because the destination size is 1x1.
|
|
2046
|
+
const fallbackPixel = new Uint8Array(FALLBACK_BYTES_PER_ROW);
|
|
2047
|
+
fallbackPixel[0] = 255;
|
|
2048
|
+
fallbackPixel[1] = 255;
|
|
2049
|
+
fallbackPixel[2] = 255;
|
|
2050
|
+
fallbackPixel[3] = 255;
|
|
2051
|
+
const fallbackWriteResult = runShimSyncStep(
|
|
2052
|
+
() =>
|
|
2053
|
+
queue.writeTexture(
|
|
2054
|
+
{
|
|
2055
|
+
texture: fallbackTextureResult.value,
|
|
2056
|
+
mipLevel: 0,
|
|
2057
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
2058
|
+
},
|
|
2059
|
+
fallbackPixel,
|
|
2060
|
+
{ offset: 0, bytesPerRow: FALLBACK_BYTES_PER_ROW, rowsPerImage: 1 },
|
|
2061
|
+
{ width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
2062
|
+
),
|
|
2063
|
+
'queue-write-buffer-out-of-bounds',
|
|
2064
|
+
'queue.writeTexture (fallback white pixel) succeeded',
|
|
2065
|
+
'verify bytesPerRow / rowsPerImage alignment',
|
|
2066
|
+
);
|
|
2067
|
+
if (!fallbackWriteResult.ok) throw fallbackWriteResult.error;
|
|
2068
|
+
|
|
2069
|
+
const fallbackTextureViewResult = runShimSyncStep(
|
|
2070
|
+
() =>
|
|
2071
|
+
rhiDevice.createTextureView(fallbackTextureResult.value, {
|
|
2072
|
+
label: 'fallback-white-view',
|
|
2073
|
+
dimension: '2d',
|
|
2074
|
+
}),
|
|
2075
|
+
'webgpu-runtime-error',
|
|
2076
|
+
'createTextureView (fallback white) succeeded',
|
|
2077
|
+
'check fallback texture format / usage',
|
|
2078
|
+
);
|
|
2079
|
+
if (!fallbackTextureViewResult.ok) throw fallbackTextureViewResult.error;
|
|
2080
|
+
|
|
2081
|
+
const extendedLightingFallback = createExtendedLightingFallbackResources(
|
|
2082
|
+
rhiDevice,
|
|
2083
|
+
extendedLightingShaderAvailable,
|
|
2084
|
+
);
|
|
2085
|
+
|
|
2086
|
+
// Normal-slot fallback: 1x1 RGBA8 (128, 128, 255, 255). pbr.wgsl decodes
|
|
2087
|
+
// sample.rg * 2 - 1 + z = sqrt(1 - x^2 - y^2), so RG=(128,128)=0.5 maps
|
|
2088
|
+
// to tangent (0, 0, 1) -- zero perturbation when normalTexture is absent.
|
|
2089
|
+
// Cannot share the white fallback (255,255,...) because RG=(255,255)=1.0
|
|
2090
|
+
// gives tangent.xy=(1,1) -> 1 - 2 = -1 under the sqrt -> NaN (saturate
|
|
2091
|
+
// clamps to 0 z=0, still wrong). White-on-missing semantics for baseColor
|
|
2092
|
+
// / metallicRoughness slots is preserved by keeping those bound to the
|
|
2093
|
+
// shared fallbackTextureView; only the normal slot uses this view.
|
|
2094
|
+
const fallbackNormalTextureResult = runShimSyncStep(
|
|
2095
|
+
() =>
|
|
2096
|
+
rhiDevice.createTexture({
|
|
2097
|
+
label: 'fallback-normal-1x1',
|
|
2098
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
2099
|
+
mipLevelCount: 1,
|
|
2100
|
+
sampleCount: 1,
|
|
2101
|
+
dimension: '2d',
|
|
2102
|
+
format: 'rgba8unorm',
|
|
2103
|
+
usage: GPU_TEXTURE_USAGE_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_DST,
|
|
2104
|
+
viewFormats: [],
|
|
2105
|
+
textureBindingViewDimension: undefined,
|
|
2106
|
+
}),
|
|
2107
|
+
'webgpu-runtime-error',
|
|
2108
|
+
'createTexture (fallback normal) succeeded',
|
|
2109
|
+
'check device.limits.maxTextureDimension2D',
|
|
2110
|
+
);
|
|
2111
|
+
if (!fallbackNormalTextureResult.ok) throw fallbackNormalTextureResult.error;
|
|
2112
|
+
|
|
2113
|
+
const fallbackNormalPixel = new Uint8Array(FALLBACK_BYTES_PER_ROW);
|
|
2114
|
+
fallbackNormalPixel[0] = 128;
|
|
2115
|
+
fallbackNormalPixel[1] = 128;
|
|
2116
|
+
fallbackNormalPixel[2] = 255;
|
|
2117
|
+
fallbackNormalPixel[3] = 255;
|
|
2118
|
+
const fallbackNormalWriteResult = runShimSyncStep(
|
|
2119
|
+
() =>
|
|
2120
|
+
queue.writeTexture(
|
|
2121
|
+
{
|
|
2122
|
+
texture: fallbackNormalTextureResult.value,
|
|
2123
|
+
mipLevel: 0,
|
|
2124
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
2125
|
+
},
|
|
2126
|
+
fallbackNormalPixel,
|
|
2127
|
+
{ offset: 0, bytesPerRow: FALLBACK_BYTES_PER_ROW, rowsPerImage: 1 },
|
|
2128
|
+
{ width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
2129
|
+
),
|
|
2130
|
+
'queue-write-buffer-out-of-bounds',
|
|
2131
|
+
'queue.writeTexture (fallback normal pixel) succeeded',
|
|
2132
|
+
'verify bytesPerRow / rowsPerImage alignment',
|
|
2133
|
+
);
|
|
2134
|
+
if (!fallbackNormalWriteResult.ok) throw fallbackNormalWriteResult.error;
|
|
2135
|
+
|
|
2136
|
+
const fallbackNormalTextureViewResult = runShimSyncStep(
|
|
2137
|
+
() =>
|
|
2138
|
+
rhiDevice.createTextureView(fallbackNormalTextureResult.value, {
|
|
2139
|
+
label: 'fallback-normal-view',
|
|
2140
|
+
dimension: '2d',
|
|
2141
|
+
}),
|
|
2142
|
+
'webgpu-runtime-error',
|
|
2143
|
+
'createTextureView (fallback normal) succeeded',
|
|
2144
|
+
'check fallback normal texture format / usage',
|
|
2145
|
+
);
|
|
2146
|
+
if (!fallbackNormalTextureViewResult.ok) throw fallbackNormalTextureViewResult.error;
|
|
2147
|
+
|
|
2148
|
+
// feat-20260520-directional-light-shadow-mapping M2 / w14 (D-1):
|
|
2149
|
+
// shadowFallbackTextureView is a 1x1 depth32float fallback bound at
|
|
2150
|
+
// viewBindGroup entry 3 when no shadow RT exists (castShadow:false
|
|
2151
|
+
// or allocation failed). Cleared to 1.0 (far plane) via a minimal
|
|
2152
|
+
// render pass so textureSampleCompareLevel always returns 1.0 (fully lit).
|
|
2153
|
+
// Uses RENDER_ATTACHMENT for the clear pass + TEXTURE_BINDING for sampling.
|
|
2154
|
+
const shadowFallbackTexResult = runShimSyncStep(
|
|
2155
|
+
() =>
|
|
2156
|
+
rhiDevice.createTexture({
|
|
2157
|
+
label: 'shadow-fallback-depth-1x1',
|
|
2158
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
2159
|
+
mipLevelCount: 1,
|
|
2160
|
+
sampleCount: 1,
|
|
2161
|
+
dimension: '2d',
|
|
2162
|
+
format: 'depth32float',
|
|
2163
|
+
usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING,
|
|
2164
|
+
viewFormats: [],
|
|
2165
|
+
textureBindingViewDimension: undefined,
|
|
2166
|
+
}),
|
|
2167
|
+
'webgpu-runtime-error',
|
|
2168
|
+
'createTexture (shadow fallback depth) succeeded',
|
|
2169
|
+
'check device.limits.maxTextureDimension2D',
|
|
2170
|
+
);
|
|
2171
|
+
if (!shadowFallbackTexResult.ok) throw shadowFallbackTexResult.error;
|
|
2172
|
+
|
|
2173
|
+
const shadowFallbackViewResult = runShimSyncStep(
|
|
2174
|
+
() =>
|
|
2175
|
+
rhiDevice.createTextureView(shadowFallbackTexResult.value, {
|
|
2176
|
+
label: 'shadow-fallback-depth-view',
|
|
2177
|
+
dimension: '2d',
|
|
2178
|
+
}),
|
|
2179
|
+
'webgpu-runtime-error',
|
|
2180
|
+
'createTextureView (shadow fallback depth) succeeded',
|
|
2181
|
+
'check shadow fallback texture format / usage',
|
|
2182
|
+
);
|
|
2183
|
+
if (!shadowFallbackViewResult.ok) throw shadowFallbackViewResult.error;
|
|
2184
|
+
|
|
2185
|
+
// Clear the 1x1 depth fallback to 1.0 (far plane) via a 1-pixel render pass.
|
|
2186
|
+
const shadowFallbackClearEncResult = rhiDevice.createCommandEncoder({
|
|
2187
|
+
label: 'shadow-fallback-clear-encoder',
|
|
2188
|
+
});
|
|
2189
|
+
if (!shadowFallbackClearEncResult.ok) throw shadowFallbackClearEncResult.error;
|
|
2190
|
+
const shadowFallbackPass = shadowFallbackClearEncResult.value.beginRenderPass({
|
|
2191
|
+
colorAttachments: [],
|
|
2192
|
+
depthStencilAttachment: {
|
|
2193
|
+
view: shadowFallbackViewResult.value,
|
|
2194
|
+
depthClearValue: 1,
|
|
2195
|
+
depthLoadOp: 'clear',
|
|
2196
|
+
depthStoreOp: 'store',
|
|
2197
|
+
},
|
|
2198
|
+
} as never);
|
|
2199
|
+
shadowFallbackPass.end();
|
|
2200
|
+
const shadowFallbackClearFinish = shadowFallbackClearEncResult.value.finish();
|
|
2201
|
+
if (!shadowFallbackClearFinish.ok) throw shadowFallbackClearFinish.error;
|
|
2202
|
+
const shadowFallbackClearSubmit = queue.submit([shadowFallbackClearFinish.value]);
|
|
2203
|
+
if (!shadowFallbackClearSubmit.ok) throw shadowFallbackClearSubmit.error;
|
|
2204
|
+
|
|
2205
|
+
// feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: 1x1x6
|
|
2206
|
+
// depth32float cube_array fallback bound at viewBindGroup entry 5 when no
|
|
2207
|
+
// PointLightShadow snapshots are active. dimension: '2d' with
|
|
2208
|
+
// depthOrArrayLayers: 6 produces a 6-layer 2D array that
|
|
2209
|
+
// `dimension: 'cube-array'` views can target (one cube, layers=1). The
|
|
2210
|
+
// texture is cleared to depth=1.0 (far plane) so
|
|
2211
|
+
// textureSampleCompareLevel always returns 1.0 (fully lit) regardless of
|
|
2212
|
+
// the depthRef the shader passes. AC-09 zero-allocation invariant
|
|
2213
|
+
// preserved: the real cube_array atlas in ShadowAtlas is still
|
|
2214
|
+
// lazy-allocated (the fallback is always created, but it only takes 24
|
|
2215
|
+
// bytes of GPU memory).
|
|
2216
|
+
const shadowAtlasFallbackTexResult = runShimSyncStep(
|
|
2217
|
+
() =>
|
|
2218
|
+
rhiDevice.createTexture({
|
|
2219
|
+
label: 'shadow-atlas-fallback-cube-1x1',
|
|
2220
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 6 },
|
|
2221
|
+
mipLevelCount: 1,
|
|
2222
|
+
sampleCount: 1,
|
|
2223
|
+
dimension: '2d',
|
|
2224
|
+
format: 'depth32float',
|
|
2225
|
+
usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING,
|
|
2226
|
+
viewFormats: [],
|
|
2227
|
+
textureBindingViewDimension: 'cube',
|
|
2228
|
+
}),
|
|
2229
|
+
'webgpu-runtime-error',
|
|
2230
|
+
'createTexture (shadow atlas fallback cube) succeeded',
|
|
2231
|
+
'check device.limits.maxTextureDimension2D and cube-array support',
|
|
2232
|
+
);
|
|
2233
|
+
if (!shadowAtlasFallbackTexResult.ok) throw shadowAtlasFallbackTexResult.error;
|
|
2234
|
+
|
|
2235
|
+
const shadowAtlasFallbackViewResult = runShimSyncStep(
|
|
2236
|
+
() =>
|
|
2237
|
+
rhiDevice.createTextureView(shadowAtlasFallbackTexResult.value, {
|
|
2238
|
+
label: 'shadow-atlas-fallback-cube-array-view',
|
|
2239
|
+
dimension: 'cube-array',
|
|
2240
|
+
aspect: 'depth-only',
|
|
2241
|
+
baseArrayLayer: 0,
|
|
2242
|
+
arrayLayerCount: 6,
|
|
2243
|
+
baseMipLevel: 0,
|
|
2244
|
+
mipLevelCount: 1,
|
|
2245
|
+
}),
|
|
2246
|
+
'webgpu-runtime-error',
|
|
2247
|
+
'createTextureView (shadow atlas fallback cube-array) succeeded',
|
|
2248
|
+
'check shadow atlas fallback texture format / usage / dimension',
|
|
2249
|
+
);
|
|
2250
|
+
if (!shadowAtlasFallbackViewResult.ok) throw shadowAtlasFallbackViewResult.error;
|
|
2251
|
+
|
|
2252
|
+
// Clear all 6 fallback faces to 1.0 (far plane). One pass per face
|
|
2253
|
+
// (WebGPU forbids cube views as render-pass attachments; per-face 2D view
|
|
2254
|
+
// is required).
|
|
2255
|
+
for (let face = 0; face < 6; face++) {
|
|
2256
|
+
const faceViewRes = runShimSyncStep(
|
|
2257
|
+
() =>
|
|
2258
|
+
rhiDevice.createTextureView(shadowAtlasFallbackTexResult.value, {
|
|
2259
|
+
label: `shadow-atlas-fallback-face-${face}`,
|
|
2260
|
+
dimension: '2d',
|
|
2261
|
+
aspect: 'depth-only',
|
|
2262
|
+
baseArrayLayer: face,
|
|
2263
|
+
arrayLayerCount: 1,
|
|
2264
|
+
baseMipLevel: 0,
|
|
2265
|
+
mipLevelCount: 1,
|
|
2266
|
+
}),
|
|
2267
|
+
'webgpu-runtime-error',
|
|
2268
|
+
`createTextureView (shadow atlas fallback face ${face}) succeeded`,
|
|
2269
|
+
'check shadow atlas fallback texture format / usage',
|
|
2270
|
+
);
|
|
2271
|
+
if (!faceViewRes.ok) throw faceViewRes.error;
|
|
2272
|
+
const encRes = rhiDevice.createCommandEncoder({
|
|
2273
|
+
label: `shadow-atlas-fallback-clear-encoder-face-${face}`,
|
|
2274
|
+
});
|
|
2275
|
+
if (!encRes.ok) throw encRes.error;
|
|
2276
|
+
const pass = encRes.value.beginRenderPass({
|
|
2277
|
+
colorAttachments: [],
|
|
2278
|
+
depthStencilAttachment: {
|
|
2279
|
+
view: faceViewRes.value,
|
|
2280
|
+
depthClearValue: 1,
|
|
2281
|
+
depthLoadOp: 'clear',
|
|
2282
|
+
depthStoreOp: 'store',
|
|
2283
|
+
},
|
|
2284
|
+
} as never);
|
|
2285
|
+
pass.end();
|
|
2286
|
+
const finRes = encRes.value.finish();
|
|
2287
|
+
if (!finRes.ok) throw finRes.error;
|
|
2288
|
+
const subRes = queue.submit([finRes.value]);
|
|
2289
|
+
if (!subRes.ok) throw subRes.error;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: shadowParams
|
|
2293
|
+
// uniform buffer = `array<vec4<f32>, 4>` (4 lanes x 16 B = 64 B). One
|
|
2294
|
+
// lane per PointLightShadow slot (cap = 4). Each lane stores
|
|
2295
|
+
// `(near, far, 1/(far-near), 0)` for depth-ref reconstruction in
|
|
2296
|
+
// lighting-punctual.wgsl evalPointShadowed. Written per frame in the
|
|
2297
|
+
// record stage from `frameState.pointShadowSnapshots`. Initial contents
|
|
2298
|
+
// are zero (writeBuffer at create time is implicit per spec); zero lanes
|
|
2299
|
+
// are safe because the WGSL sample path is gated on
|
|
2300
|
+
// `PointLight.shadowAtlasLayer >= 0` -- a non-shadow-casting light
|
|
2301
|
+
// cannot read its lane.
|
|
2302
|
+
const SHADOW_PARAMS_BYTES = 4 * 16;
|
|
2303
|
+
const shadowParamsBufferResult = runShimSyncStep(
|
|
2304
|
+
() =>
|
|
2305
|
+
rhiDevice.createBuffer({
|
|
2306
|
+
label: 'shadow-params-ubo',
|
|
2307
|
+
size: SHADOW_PARAMS_BYTES,
|
|
2308
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
2309
|
+
mappedAtCreation: false,
|
|
2310
|
+
}),
|
|
2311
|
+
'webgpu-runtime-error',
|
|
2312
|
+
'createBuffer (shadow params UBO) succeeded',
|
|
2313
|
+
'check device.limits.maxUniformBufferBindingSize (need >= 64)',
|
|
2314
|
+
);
|
|
2315
|
+
if (!shadowParamsBufferResult.ok) throw shadowParamsBufferResult.error;
|
|
2316
|
+
// Zero-initialize the buffer so the no-shadow path reads deterministic
|
|
2317
|
+
// zeros (writeBuffer with 64 B of zeros).
|
|
2318
|
+
const SHADOW_PARAMS_ZEROES = new Uint8Array(SHADOW_PARAMS_BYTES);
|
|
2319
|
+
const shadowParamsZeroWriteRes = queue.writeBuffer(
|
|
2320
|
+
shadowParamsBufferResult.value,
|
|
2321
|
+
0,
|
|
2322
|
+
SHADOW_PARAMS_ZEROES,
|
|
2323
|
+
);
|
|
2324
|
+
if (!shadowParamsZeroWriteRes.ok) throw shadowParamsZeroWriteRes.error;
|
|
2325
|
+
|
|
2326
|
+
// feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
|
|
2327
|
+
// the per-spot fragment-read perspective lightViewProj matrices no longer have
|
|
2328
|
+
// a standalone uniform buffer — they fold into the View UBO tail
|
|
2329
|
+
// (`view.spotLightViewProj`, bytes 528..784, allocated as part of
|
|
2330
|
+
// VIEW_UBO_BYTES = 784 above). The View UBO's createBuffer already zero-fills
|
|
2331
|
+
// on first writeBuffer; render-system-record writes the spot lanes inside the
|
|
2332
|
+
// per-frame viewPayload. Removing the dedicated buffer drops the WebGL2
|
|
2333
|
+
// fallback fragment uniform-buffer count from 12 back to 11.
|
|
2334
|
+
|
|
2335
|
+
// feat-20260520-skylight-ibl-cubemap M2 round-4 / t40 amend
|
|
2336
|
+
// (plan-strategy D-5 round-4 REVISED): allocate the fallback Skylight
|
|
2337
|
+
// identity resource bundle -- 1x1 all-zero rgba16float texture_cube * 2
|
|
2338
|
+
// (irradiance + prefilter) + 1x1 approximate rg16float brdfLut +
|
|
2339
|
+
// intensity=0 uniform buffer + a single linear/clamp sampler reused
|
|
2340
|
+
// across the three texture slots. No stand-alone BindGroupLayout /
|
|
2341
|
+
// BindGroup is allocated -- those roles moved into the PBR material
|
|
2342
|
+
// BGL factory (entries 7..13 inside @group(1)). The M4 record-stage
|
|
2343
|
+
// material BG assembly site feeds these resources through
|
|
2344
|
+
// `assembleMaterialWithSkylightEntries` when `skylightCount === 0` so
|
|
2345
|
+
// the active standard material path dispatches with ambient = 0 --
|
|
2346
|
+
// physical convergence with D-4 (charter F1: AI users writing demos do
|
|
2347
|
+
// not need a "is there a skylight?" branch).
|
|
2348
|
+
//
|
|
2349
|
+
// Keep the helper's narrow mockable contracts at the RHI boundary while
|
|
2350
|
+
// forwarding the concrete device and queue methods without a structural cast.
|
|
2351
|
+
let skylightFallback: SkylightFallback | null = null;
|
|
2352
|
+
try {
|
|
2353
|
+
const skylightDevice: Parameters<typeof createSkylightFallback>[0] = {
|
|
2354
|
+
createSampler: (descriptor) => rhiDevice.createSampler(descriptor),
|
|
2355
|
+
createTexture: (descriptor) => rhiDevice.createTexture(descriptor),
|
|
2356
|
+
createTextureView: (texture, descriptor) => rhiDevice.createTextureView(texture, descriptor),
|
|
2357
|
+
createBuffer: (descriptor) => rhiDevice.createBuffer(descriptor),
|
|
2358
|
+
};
|
|
2359
|
+
const skylightQueue: Parameters<typeof createSkylightFallback>[1] = {
|
|
2360
|
+
writeTexture: (destination, data, layout, size) =>
|
|
2361
|
+
queue.writeTexture(destination, data, layout, size),
|
|
2362
|
+
writeBuffer: (buffer, offset, data) => queue.writeBuffer(buffer, offset, data),
|
|
2363
|
+
};
|
|
2364
|
+
skylightFallback = createSkylightFallback(skylightDevice, skylightQueue);
|
|
2365
|
+
} catch (caught) {
|
|
2366
|
+
if (caught instanceof RhiError) throw caught;
|
|
2367
|
+
throw new RhiError({
|
|
2368
|
+
code: 'webgpu-runtime-error',
|
|
2369
|
+
expected: 'createSkylightFallback succeeded',
|
|
2370
|
+
hint: `verify texture_cube + uniform allocation (cause: ${
|
|
2371
|
+
caught instanceof Error ? caught.message : String(caught)
|
|
2372
|
+
})`,
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// feat-20260615-pipeline-spec-ssot M2-T4: PipelineCache is the single PSO container
|
|
2377
|
+
// (SSOT axiom — plan D-12). Created once at boot; shared across all call sites.
|
|
2378
|
+
// The provider is a thin wrapper over rhiDevice that fills in vertex buffers +
|
|
2379
|
+
// pipeline layout on the descriptor produced by buildPipelineDescriptor.
|
|
2380
|
+
const pipelineCache: PipelineCache = new Map();
|
|
2381
|
+
|
|
2382
|
+
// Shader module mapping — maps spec.shader.id to compiled shader modules +
|
|
2383
|
+
// optional per-pass metadata (layout, entry points). Material shaders fill
|
|
2384
|
+
// only vertex/fragment; fullscreen-post passes add layout + entry points
|
|
2385
|
+
// for the boot-time SPEC_CONST pre-warm and lazy-build getOrBuildPipeline.
|
|
2386
|
+
const shaderModuleMap = new Map<
|
|
2387
|
+
string,
|
|
2388
|
+
{
|
|
2389
|
+
vertex: unknown;
|
|
2390
|
+
fragment: unknown;
|
|
2391
|
+
vertexEntryPoint?: string;
|
|
2392
|
+
fragmentEntryPoint?: string;
|
|
2393
|
+
layout?: unknown;
|
|
2394
|
+
label?: string;
|
|
2395
|
+
}
|
|
2396
|
+
>();
|
|
2397
|
+
if (unlitModule !== null)
|
|
2398
|
+
shaderModuleMap.set('forgeax::default-unlit', {
|
|
2399
|
+
vertex: unlitModule,
|
|
2400
|
+
fragment: unlitModule,
|
|
2401
|
+
});
|
|
2402
|
+
if (pbrModule !== null)
|
|
2403
|
+
shaderModuleMap.set('forgeax::default-standard-pbr', {
|
|
2404
|
+
vertex: pbrModule,
|
|
2405
|
+
fragment: pbrModule,
|
|
2406
|
+
});
|
|
2407
|
+
if (spriteModule !== null)
|
|
2408
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w14 (D-7):
|
|
2409
|
+
// sprite shader registers under the single canonical id `forgeax::sprite`
|
|
2410
|
+
// (previously double-mapped via the engine-built default-sprite id for
|
|
2411
|
+
// the deleted boot-time pre-warm path). The generic per-MaterialShader
|
|
2412
|
+
// pipeline cache reads this entry when a sprite material lands a
|
|
2413
|
+
// transparent pass through the LDR split.
|
|
2414
|
+
shaderModuleMap.set('forgeax::sprite', {
|
|
2415
|
+
vertex: spriteModule,
|
|
2416
|
+
fragment: spriteModule,
|
|
2417
|
+
});
|
|
2418
|
+
// feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
|
|
2419
|
+
// sprite-lit shader registers under canonical `forgeax::sprite-lit` —
|
|
2420
|
+
// mirror sprite, parallel cache slot (D-11 string isolation).
|
|
2421
|
+
if (spriteLitModule !== null)
|
|
2422
|
+
shaderModuleMap.set('forgeax::sprite-lit', {
|
|
2423
|
+
vertex: spriteLitModule,
|
|
2424
|
+
fragment: spriteLitModule,
|
|
2425
|
+
});
|
|
2426
|
+
// M2-T4: fullscreen-post shader modules (tonemap + skybox) are registered
|
|
2427
|
+
// in their respective blocks after pipeline layout creation — layout is
|
|
2428
|
+
// required for the boot-time SPEC_CONST pre-warm to succeed. The entries
|
|
2429
|
+
// in SPEC_CONST_TABLE will be pre-warmed per-block.
|
|
2430
|
+
|
|
2431
|
+
// M2-T4: provider no longer overwrites vertex buffers — buildPipelineDescriptor
|
|
2432
|
+
// derives them from spec.geometry.vertexLayout (SSOT). Layout is only filled
|
|
2433
|
+
// when the descriptor doesn't already carry one (fullscreen-post passes set it
|
|
2434
|
+
// via modules.layout). Fullscreen-post module detection added for label generation.
|
|
2435
|
+
const pipelineDeviceProvider: PipelineDeviceProvider = {
|
|
2436
|
+
createRenderPipeline(descriptor: Record<string, unknown>):
|
|
2437
|
+
| {
|
|
2438
|
+
ok: true;
|
|
2439
|
+
value: unknown;
|
|
2440
|
+
}
|
|
2441
|
+
| { ok: false; error: unknown } {
|
|
2442
|
+
const d = { ...descriptor } as Record<string, unknown>;
|
|
2443
|
+
|
|
2444
|
+
// Layout: only fill if buildPipelineDescriptor did not already set it.
|
|
2445
|
+
if (d.layout === undefined) {
|
|
2446
|
+
d.layout = pipelineLayoutResult.value;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// Sprite HDR uses fragment entry point 'fs_main_hdr'; all other
|
|
2450
|
+
// standard material pipelines (unlit/standard LDR/HDR, sprite LDR)
|
|
2451
|
+
// use 'fs_main'. The spec is business-agnostic (plan D-7) so the
|
|
2452
|
+
// provider resolves this by checking the fragment module reference
|
|
2453
|
+
// against the compiled sprite module + the attachment format.
|
|
2454
|
+
//
|
|
2455
|
+
// Also generates a label for each PSO so integration tests and GPU
|
|
2456
|
+
// debug captures can identify the pipeline variant. The label shape
|
|
2457
|
+
// is derived from the fragment module + color format (backward-
|
|
2458
|
+
// compatible with pre-M2 descriptor literals).
|
|
2459
|
+
let label = 'pbr-pipeline';
|
|
2460
|
+
if (d.fragment) {
|
|
2461
|
+
const f = { ...(d.fragment as Record<string, unknown>) } as Record<string, unknown>;
|
|
2462
|
+
const fragModule = f.module;
|
|
2463
|
+
const targets = f.targets as Array<Record<string, unknown>> | undefined;
|
|
2464
|
+
const isHdr = targets?.[0]?.format === HDR_COLOR_ATTACHMENT_FORMAT;
|
|
2465
|
+
|
|
2466
|
+
if (fragModule === spriteModule) {
|
|
2467
|
+
label = isHdr ? 'sprite-pipeline-hdr' : 'sprite-pipeline';
|
|
2468
|
+
if (isHdr) {
|
|
2469
|
+
f.entryPoint = 'fs_main_hdr';
|
|
2470
|
+
}
|
|
2471
|
+
} else if (fragModule === spriteLitModule) {
|
|
2472
|
+
// feat-20260624 M1' / t7: sprite-lit mirrors sprite's HDR
|
|
2473
|
+
// entry swap — `fs_main` (LDR clamped) vs `fs_main_hdr`
|
|
2474
|
+
// (HDR pass-through). Labels keep the same shape.
|
|
2475
|
+
label = isHdr ? 'sprite-lit-pipeline-hdr' : 'sprite-lit-pipeline';
|
|
2476
|
+
if (isHdr) {
|
|
2477
|
+
f.entryPoint = 'fs_main_hdr';
|
|
2478
|
+
}
|
|
2479
|
+
} else if (fragModule === unlitModule || fragModule === pbrModule) {
|
|
2480
|
+
const prefix = fragModule === unlitModule ? 'unlit' : 'standard';
|
|
2481
|
+
label = isHdr ? `pbr-pipeline-${prefix}-hdr` : `pbr-pipeline-${prefix}`;
|
|
2482
|
+
} else if (fragModule === fxaaModule) {
|
|
2483
|
+
label = 'fxaa-pipeline';
|
|
2484
|
+
} else if (fragModule === skyboxModule) {
|
|
2485
|
+
const msaa = d.multisample as Record<string, unknown> | undefined;
|
|
2486
|
+
label = msaa !== undefined ? 'skybox-pipeline-msaa' : 'skybox-pipeline';
|
|
2487
|
+
} else if (fragModule === bloomBrightModule) {
|
|
2488
|
+
label = 'bloom-bright-pipeline';
|
|
2489
|
+
} else if (fragModule === bloomBlurModule) {
|
|
2490
|
+
label = (d.label as string) ?? 'bloom-blur-h-pipeline';
|
|
2491
|
+
} else if (fragModule === bloomCompositeModule) {
|
|
2492
|
+
label = 'bloom-composite-pipeline';
|
|
2493
|
+
} else if (fragModule === ssaoModule) {
|
|
2494
|
+
label = (d.label as string) ?? 'ssao-calc-pipeline';
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
d.fragment = f;
|
|
2498
|
+
}
|
|
2499
|
+
d.label = (d.label as string | undefined) ?? label;
|
|
2500
|
+
|
|
2501
|
+
return rhiDevice.createRenderPipeline(
|
|
2502
|
+
d as Parameters<typeof rhiDevice.createRenderPipeline>[0],
|
|
2503
|
+
);
|
|
2504
|
+
},
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// bug-20260615 fix-up: build the SPEC_CONST table with the runtime-resolved
|
|
2508
|
+
// LDR view format. Hard-coding `bgra8unorm-srgb` at module load made the
|
|
2509
|
+
// pre-warmed PSOs incompatible with the actual swap-chain format on
|
|
2510
|
+
// backends where `getPreferredCanvasFormat()` (or the wgpu-wasm GLES path)
|
|
2511
|
+
// returns `rgba8unorm` (Channel 3 + dawn-node) — every frame's whole
|
|
2512
|
+
// commandBuffer was being rejected. Calling `buildSpecConstTable` after
|
|
2513
|
+
// `selectSwapChainFormat` resolves keeps the pre-warmed key (`cacheKeyOf`)
|
|
2514
|
+
// and the runtime tonemap call site (uses the same `swapChainFormats.view`)
|
|
2515
|
+
// identical, so the cache lookup hits instead of double-building.
|
|
2516
|
+
// Pass both view (unlit / standard / tonemap LDR target) and storage
|
|
2517
|
+
// (sprite LDR target — pre-feat sprite PSO targeted swapChainFormats.storage
|
|
2518
|
+
// directly so the alpha-blend pass writes the raw, non-srgb view of the
|
|
2519
|
+
// swap-chain texture; see pipeline-spec.ts SPRITE_ATTACHMENTS jsdoc).
|
|
2520
|
+
const runtimeSpecConstTable = buildSpecConstTable(swapChainFormats.view);
|
|
2521
|
+
const runtimeLinearLdrMaterialSpecTable = buildLinearLdrMaterialSpecTable(swapChainFormats.view);
|
|
2522
|
+
const runtimeMaterialPrewarmTable = [
|
|
2523
|
+
...runtimeSpecConstTable,
|
|
2524
|
+
...runtimeLinearLdrMaterialSpecTable,
|
|
2525
|
+
];
|
|
2526
|
+
const group2ContractForSpec = (spec: PipelineSpec): PipelineGroup2Contract => {
|
|
2527
|
+
const material = [...registry.materialShaderManifestEntries()].find(
|
|
2528
|
+
(entry) => entry.identifier === spec.shader.id,
|
|
2529
|
+
);
|
|
2530
|
+
const source =
|
|
2531
|
+
material === undefined
|
|
2532
|
+
? undefined
|
|
2533
|
+
: spec.shader.variantSet === undefined
|
|
2534
|
+
? material?.variants[0]?.composedWgsl
|
|
2535
|
+
: findVariantByKey(material, spec.shader.variantSet)?.composedWgsl;
|
|
2536
|
+
return resolvePipelineGroup2Contract(source ?? '');
|
|
2537
|
+
};
|
|
2538
|
+
|
|
2539
|
+
// Boot-time pre-warm: build SPEC_CONST entries whose shader modules are
|
|
2540
|
+
// compiled. Entries referencing a missing module are silently skipped
|
|
2541
|
+
// (the empty-manifest path — D-3). Each build failure for an available
|
|
2542
|
+
// module throws PipelineSpecError (fail-fast, charter P3).
|
|
2543
|
+
if (
|
|
2544
|
+
unlitModule !== null ||
|
|
2545
|
+
pbrModule !== null ||
|
|
2546
|
+
spriteModule !== null ||
|
|
2547
|
+
spriteLitModule !== null
|
|
2548
|
+
) {
|
|
2549
|
+
for (const spec of runtimeMaterialPrewarmTable) {
|
|
2550
|
+
const modules = shaderModuleMap.get(spec.shader.id);
|
|
2551
|
+
if (modules === undefined) {
|
|
2552
|
+
// Module not compiled (empty-manifest for this shader): skip.
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
try {
|
|
2556
|
+
getOrBuildPipeline(spec, pipelineDeviceProvider, pipelineCache, modules);
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
2559
|
+
throw new PipelineSpecError({
|
|
2560
|
+
code: 'pipeline-build-failed',
|
|
2561
|
+
detail: { cause: err },
|
|
2562
|
+
hint: `Boot-time SPEC_CONST pre-warm failed for shader '${spec.shader.id}'; inspect gpuMessage on the cause`,
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// M6 fix-up: seed `materialShaderPipelineCache` (owned by the outer
|
|
2568
|
+
// `makeWebGPURenderer` scope) from the prewarmed `pipelineCache` for
|
|
2569
|
+
// SPEC_CONST entries whose `variantSet !== undefined`. The URP record
|
|
2570
|
+
// path queries `getMaterialShaderPipeline(...)` keyed off
|
|
2571
|
+
// `cacheKeyOf(spec)` with `variantSet=URP_PBR_VARIANT_SET`; both caches
|
|
2572
|
+
// generate keys via the same `cacheKeyOf` so the lookup hits the
|
|
2573
|
+
// boot-time prewarmed PSO instead of triggering a 1-frame async-compile
|
|
2574
|
+
// skip-draw. Seeding only variantSet-bearing entries keeps the
|
|
2575
|
+
// no-variant entries flowing through the original
|
|
2576
|
+
// `pipelineState.standardPipeline*` channel (consumed by sprite /
|
|
2577
|
+
// unlit-fallback paths), so URP-vs-no-variant cache identity stays
|
|
2578
|
+
// explicit instead of collapsing into one map (charter P3).
|
|
2579
|
+
for (const spec of runtimeSpecConstTable) {
|
|
2580
|
+
if (spec.shader.variantSet === undefined) continue;
|
|
2581
|
+
if (shaderModuleMap.get(spec.shader.id) === undefined) continue;
|
|
2582
|
+
const key = cacheKeyOf(spec);
|
|
2583
|
+
const built = pipelineCache.get(key);
|
|
2584
|
+
if (built !== undefined) {
|
|
2585
|
+
seedMaterialShaderPipelineCache(key, built as RenderPipeline, group2ContractForSpec(spec));
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// The linear-LDR geometry target is intentionally separate from the
|
|
2590
|
+
// swap-chain view target. Seed every material entry, including no-variant
|
|
2591
|
+
// unlit, because tonemap=none reaches this cache directly on frame one.
|
|
2592
|
+
for (const spec of runtimeLinearLdrMaterialSpecTable) {
|
|
2593
|
+
if (shaderModuleMap.get(spec.shader.id) === undefined) continue;
|
|
2594
|
+
const key = cacheKeyOf(spec);
|
|
2595
|
+
const built = pipelineCache.get(key);
|
|
2596
|
+
if (built !== undefined) {
|
|
2597
|
+
seedMaterialShaderPipelineCache(key, built as RenderPipeline, group2ContractForSpec(spec));
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// helper: look up a pre-warmed PSO from cache by (shaderId, isHdr, sampleCount).
|
|
2603
|
+
// Resolves the matching runtimeSpecConstTable entry, computes cacheKeyOf,
|
|
2604
|
+
// and returns the cached handle. Returns null when the spec entry's module
|
|
2605
|
+
// was not compiled (empty-manifest path) or the cache is cold.
|
|
2606
|
+
const getCachedPipelineOrNull = (
|
|
2607
|
+
shaderId: string,
|
|
2608
|
+
isHdr: boolean,
|
|
2609
|
+
sampleCount: 1 | 4,
|
|
2610
|
+
): RenderPipeline | null => {
|
|
2611
|
+
for (const entry of runtimeSpecConstTable) {
|
|
2612
|
+
if (entry.shader.id === shaderId && entry.attachments.sampleCount === sampleCount) {
|
|
2613
|
+
const color0 = entry.attachments.colorFormats[0];
|
|
2614
|
+
const entryIsHdr = color0 === HDR_COLOR_ATTACHMENT_FORMAT;
|
|
2615
|
+
if (entryIsHdr === isHdr) {
|
|
2616
|
+
const key = cacheKeyOf(entry);
|
|
2617
|
+
return (pipelineCache.get(key) ?? null) as RenderPipeline | null;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return null;
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
// ── feat-20260520-2d-sprite-layer-mvp / M-3 / w24 ────────────────────────
|
|
2625
|
+
//
|
|
2626
|
+
// Sprite alpha-blend pipeline pair — LDR (`bgra8unorm-srgb` swap-chain
|
|
2627
|
+
// view) + HDR (`rgba16float` offscreen view; routed when active camera
|
|
2628
|
+
// carries `tonemap !== 'none'`, same as unlit/standard HDR siblings).
|
|
2629
|
+
//
|
|
2630
|
+
// @new-surface sprite alpha-blend pipeline (4th + 5th GPU render-pipeline
|
|
2631
|
+
// handles on PipelineState; the engine grows 5 -> 9 distinct pipelines:
|
|
2632
|
+
// unlit + standard + tonemap each existed before; sprite adds LDR + HDR).
|
|
2633
|
+
// The blend op is premultiplied alpha (charter P5 consistent abstraction
|
|
2634
|
+
// with the OpenGL / WebGPU industry default; sprite.wgsl fragment outputs
|
|
2635
|
+
// premultiplied RGB so srcFactor='one' / dstFactor='one-minus-src-alpha'
|
|
2636
|
+
// composes correctly).
|
|
2637
|
+
//
|
|
2638
|
+
// @reuses pipelineLayoutResult (the 4-BindGroupLayout chain shared with
|
|
2639
|
+
// unlit / standard / pbr — view + material + meshArray + instances).
|
|
2640
|
+
// @reuses defaultSampler — sprite material BindGroup entries 3 + 5
|
|
2641
|
+
// (metallicRoughnessSampler / normalSampler placeholders bound to
|
|
2642
|
+
// `pipelineState.defaultSampler`; D-1 candidate b; zero new sampler
|
|
2643
|
+
// created).
|
|
2644
|
+
// @reuses defaultWhiteTextureView — sprite material BindGroup entries
|
|
2645
|
+
// 4 + 6 (metallicRoughnessTexture / normalTexture placeholders bound
|
|
2646
|
+
// to `pipelineState.defaultWhiteTextureView`; D-1 candidate b; the
|
|
2647
|
+
// 1x1 white view was already provisioned for unlit / standard fallback
|
|
2648
|
+
// so the sprite path adds 0 lines of new GPU resource code, only
|
|
2649
|
+
// binding references in render-system-record.ts w25).
|
|
2650
|
+
// Sprite material BindGroup populates entries 0..2 with sprite's own
|
|
2651
|
+
// uniform / sampler / texture, and entries 3..6 with
|
|
2652
|
+
// pipelineState.defaultSampler + pipelineState.defaultWhiteTextureView
|
|
2653
|
+
// (D-1 candidate b — zero new GPU resource; 4-line binding wiring lives
|
|
2654
|
+
// in render-system-record.ts w25). The unused entries are physically
|
|
2655
|
+
// bound to ensure WebGPU's BindGroupLayout congruence (declared in the
|
|
2656
|
+
// shader at @binding 3..6 even though the sprite fragment never reads
|
|
2657
|
+
// them; plan-strategy D-1 + sprite.wgsl JSDoc head).
|
|
2658
|
+
//
|
|
2659
|
+
// @derives unlit / standard LDR+HDR dual-pipeline structure (lines 2058-
|
|
2660
|
+
// 2147 above + 2174-2253 below). The sprite pair mirrors the unlit
|
|
2661
|
+
// pair byte-for-byte except for:
|
|
2662
|
+
// - module: spriteModule (vs unlitModule)
|
|
2663
|
+
// - fragment.targets[0].blend: premultiplied alpha (vs no blend)
|
|
2664
|
+
// - depthStencil.depthWriteEnabled: false (vs true)
|
|
2665
|
+
// - depthStencil.depthCompare: 'less-equal' (vs 'less')
|
|
2666
|
+
// The vertex stride stays 12F (HANDLE_QUAD passes through the same
|
|
2667
|
+
// 12-float interleaved layout as procedural meshes), so no new vertex
|
|
2668
|
+
// pipeline branch is needed in the record stage (plan-strategy §3 RT4).
|
|
2669
|
+
//
|
|
2670
|
+
// Premultiplied alpha blend op (`{ srcFactor: 'one', dstFactor:
|
|
2671
|
+
// 'one-minus-src-alpha', operation: 'add' }`) is the industry-default for
|
|
2672
|
+
// sprite atlases; sprite.wgsl emits premultiplied RGB so the over-
|
|
2673
|
+
// composite math (`dst' = src + dst * (1 - src.a)`) is direct.
|
|
2674
|
+
// feat-20260615-pipeline-spec-ssot M2-T4: sprite pipelines are pre-warmed in
|
|
2675
|
+
// SPEC_CONST_TABLE (4 entries: LDR/HDR x S1/S4). Cache lookup replaces the
|
|
2676
|
+
// prior local-handle variables + createMsaaVariant closure.
|
|
2677
|
+
// The pre-existing sprite-build-failure defer-to-null semantics are now
|
|
2678
|
+
// handled by the boot-time SPEC_CONST pre-warm block above: if the sprite
|
|
2679
|
+
// module exists but the SPEC_CONST build fails, the fail-fast throw blocks
|
|
2680
|
+
// the engine from entering the first frame (charter P3: no silent fallback).
|
|
2681
|
+
// AI users who need sprite tolerance for lavapipe / dawn-vulkan validation
|
|
2682
|
+
// can skip SPEC_CONST entries at their own peril via a future M7 opt-out
|
|
2683
|
+
// gate; the current M2 contract is fail-fast.
|
|
2684
|
+
// feat-20260608-tilemap-object-layer-rendering M2 / m2-t6 (D-8): SPEC_CONST
|
|
2685
|
+
// sprite entries set cullMode='none' so H/V flip via negative scale x/y
|
|
2686
|
+
// (tilemap per-cell entity TRS form, D-1) does not get culled when winding
|
|
2687
|
+
// inverts. See pipeline-spec.ts sprite LDR S1/S4 + HDR S1/S4 entries.
|
|
2688
|
+
|
|
2689
|
+
// ── feat-20260519-tonemap-reinhard-mvp / M2 / T-M2.5 ──────────────────────
|
|
2690
|
+
//
|
|
2691
|
+
// HDR variants of the unlit + standard pipelines (rgba16float colour
|
|
2692
|
+
// attachment instead of bgra8unorm-srgb) plus the post-process tonemap
|
|
2693
|
+
// pipeline + 3-entry BGL + sampler + 16 B params UBO. Routed by record-
|
|
2694
|
+
// stage when the active camera carries `tonemap !== 'none'` (AC-03(a) /
|
|
2695
|
+
// AC-11). Sharing the geometry shader modules across the sRGB + HDR
|
|
2696
|
+
// pipelines keeps the shader compile cost flat and the WGSL byte-for-byte
|
|
2697
|
+
// identical between the two routes — the only difference is the colour-
|
|
2698
|
+
// attachment format declaration in the fragment state target list (charter
|
|
2699
|
+
// P5 consistent abstraction; plan-strategy D-2 + D-3).
|
|
2700
|
+
//
|
|
2701
|
+
// bug-20260519 D-3 nullable extension: the HDR pipeline block is gated on
|
|
2702
|
+
// `pbrModule + unlitModule !== null` so the empty-manifest path skips every
|
|
2703
|
+
// device.create* call below and writes `null` into the corresponding
|
|
2704
|
+
// PipelineState fields. feat-20260621 M-A3 (D-5): the dedicated tonemap
|
|
2705
|
+
// pipeline / BGL / sampler / params-UBO handles are gone — the built-in
|
|
2706
|
+
// tonemap registers through the unified post-process channel (see the
|
|
2707
|
+
// `registerBuiltinTonemap` callback above; pipeline + BGL + sampler + UBO
|
|
2708
|
+
// are owned by dispatchFullscreenPass / the fullscreen feature host).
|
|
2709
|
+
let fxaaPipelineHandle: RenderPipeline | null = null;
|
|
2710
|
+
let fxaaBglHandle: BindGroupLayout | null = null;
|
|
2711
|
+
let fxaaSamplerHandle: Sampler | null = null;
|
|
2712
|
+
let skyboxPipelineHandle: RenderPipeline | null = null;
|
|
2713
|
+
let skyboxBglHandle: BindGroupLayout | null = null;
|
|
2714
|
+
let skyboxSamplerHandle: Sampler | null = null;
|
|
2715
|
+
let skyboxRotationBufferHandle: Buffer | null = null;
|
|
2716
|
+
let skyboxPipelineMsaaHandle: RenderPipeline | null = null;
|
|
2717
|
+
// feat-20260531-bloom-first-declarative-render-graph-pass / w13:
|
|
2718
|
+
// bloom pipeline handles (D-1, D-4, D-6). Bright + 2x blur (H/V per-axis)
|
|
2719
|
+
// + 1x composite = 4 pipelines. Blur H/V share the same WGSL module but
|
|
2720
|
+
// are separate pipelines with per-axis texelSize baked at creation (D-1).
|
|
2721
|
+
// All 4 use rgba16float target format (D-6).
|
|
2722
|
+
let bloomBrightPipelineHandle: RenderPipeline | null = null;
|
|
2723
|
+
let bloomBlurHPipelineHandle: RenderPipeline | null = null;
|
|
2724
|
+
let bloomBlurVPipelineHandle: RenderPipeline | null = null;
|
|
2725
|
+
let bloomCompositePipelineHandle: RenderPipeline | null = null;
|
|
2726
|
+
let bloomBrightBglHandle: BindGroupLayout | null = null;
|
|
2727
|
+
let bloomBlurBglHandle: BindGroupLayout | null = null;
|
|
2728
|
+
let bloomCompositeBglHandle: BindGroupLayout | null = null;
|
|
2729
|
+
let bloomSamplerHandle: Sampler | null = null;
|
|
2730
|
+
let bloomBrightParamsBufferHandle: Buffer | null = null;
|
|
2731
|
+
// bug-20260625: separate H/V blur params UBOs (see PerPassResources comment).
|
|
2732
|
+
let bloomBlurHParamsBufferHandle: Buffer | null = null;
|
|
2733
|
+
let bloomBlurVParamsBufferHandle: Buffer | null = null;
|
|
2734
|
+
let bloomCompositeParamsBufferHandle: Buffer | null = null;
|
|
2735
|
+
let bloomResourcesReady = false;
|
|
2736
|
+
let ensureBloomResources: (() => void) | undefined;
|
|
2737
|
+
let bloomActiveBundle: BloomPersistentBundle | undefined;
|
|
2738
|
+
let bloomCandidateBundle: BloomPersistentBundle | undefined;
|
|
2739
|
+
const retiringBloomBundles = new Set<BloomPersistentBundle>();
|
|
2740
|
+
let bloomGeneration = 0;
|
|
2741
|
+
let lastSuccessfulBloomReceipts:
|
|
2742
|
+
| import('../record/frame-snapshot').BloomFrameReceipts
|
|
2743
|
+
| undefined;
|
|
2744
|
+
const getBloomResourcesStable = (): BloomPersistentBundle | null =>
|
|
2745
|
+
bloomCandidateBundle ?? bloomActiveBundle ?? null;
|
|
2746
|
+
let commitBloomResources: (() => void) | undefined;
|
|
2747
|
+
let discardBloomResources: (() => void) | undefined;
|
|
2748
|
+
let retireBloomResources: ((completion: Promise<unknown>) => void) | undefined;
|
|
2749
|
+
let drainBloomResources: (() => void) | undefined;
|
|
2750
|
+
let inspectBloomResources:
|
|
2751
|
+
| ((graph?: import('@forgeax/engine-render-graph').CompiledRenderGraphInfo) => BloomInspection)
|
|
2752
|
+
| undefined;
|
|
2753
|
+
const ensureBloomResourcesStable = (): void => {
|
|
2754
|
+
ensureBloomResources?.();
|
|
2755
|
+
};
|
|
2756
|
+
const retireBloomResourcesStable = (completion: Promise<unknown>): void => {
|
|
2757
|
+
retireBloomResources?.(completion);
|
|
2758
|
+
};
|
|
2759
|
+
const commitBloomResourcesStable = (): void => {
|
|
2760
|
+
commitBloomResources?.();
|
|
2761
|
+
};
|
|
2762
|
+
const discardBloomResourcesStable = (): void => {
|
|
2763
|
+
discardBloomResources?.();
|
|
2764
|
+
};
|
|
2765
|
+
const drainBloomResourcesStable = (): void => {
|
|
2766
|
+
drainBloomResources?.();
|
|
2767
|
+
};
|
|
2768
|
+
const inspectBloomResourcesStable = (
|
|
2769
|
+
graph?: import('@forgeax/engine-render-graph').CompiledRenderGraphInfo,
|
|
2770
|
+
): BloomInspection => {
|
|
2771
|
+
return (
|
|
2772
|
+
inspectBloomResources?.(graph) ?? {
|
|
2773
|
+
graphStatus: 'empty',
|
|
2774
|
+
enabled: false,
|
|
2775
|
+
targetCount: 0,
|
|
2776
|
+
targetBytes: 0,
|
|
2777
|
+
resourceCount: 0,
|
|
2778
|
+
passCount: 0,
|
|
2779
|
+
encodeCount: 0,
|
|
2780
|
+
bindGroupCount: 0,
|
|
2781
|
+
uploadCount: 0,
|
|
2782
|
+
residentChildBytes: 0,
|
|
2783
|
+
generation: 0,
|
|
2784
|
+
state: 'off',
|
|
2785
|
+
}
|
|
2786
|
+
);
|
|
2787
|
+
};
|
|
2788
|
+
// feat-20260612-hdrp-ssao M6 / w26 + w43: SSAO pipeline handles (D-A).
|
|
2789
|
+
// calc + blur RenderPipeline pair sharing a dedicated 6-entry BGL.
|
|
2790
|
+
// Optional — null when manifest lacks hdrp-ssao entry.
|
|
2791
|
+
let ssaoCalcPipelineHandle: RenderPipeline | null = null;
|
|
2792
|
+
let ssaoBlurPipelineHandle: RenderPipeline | null = null;
|
|
2793
|
+
let ssaoBglHandle: BindGroupLayout | null = null;
|
|
2794
|
+
if (unlitModule !== null && pbrModule !== null) {
|
|
2795
|
+
// feat-20260615-pipeline-spec-ssot M2-T4: unlit/standard HDR pipeline
|
|
2796
|
+
// variants are pre-warmed in SPEC_CONST_TABLE (4 entries: unlit/standard
|
|
2797
|
+
// HDR x S1/S4). Cache lookup replaces prior local-handle variables.
|
|
2798
|
+
// The fxaa / skybox / bloom / SSAO fullscreen pipelines below
|
|
2799
|
+
// are NOT in SPEC_CONST_TABLE and remain boot-time lazy-built here.
|
|
2800
|
+
// feat-20260621 M-A3 (D-5): tonemap is no longer built here — it registers
|
|
2801
|
+
// through the unified post-process channel (registerBuiltinTonemap).
|
|
2802
|
+
|
|
2803
|
+
// feat-20260528-fxaa-post-processing M2 / w10: FXAA pipeline prebuilt.
|
|
2804
|
+
// When the manifest contains the fxaa entry (rgb2luma marker, D-5),
|
|
2805
|
+
// construct the 3-entry BGL (texture + sampler + output-policy UBO),
|
|
2806
|
+
// pipeline layout, fullscreen render pipeline (vertex = fullscreen
|
|
2807
|
+
// triangle from fxaa.wgsl, fragment = FXAA 3.11 algorithm), and
|
|
2808
|
+
// linear clamp-to-edge sampler. Mirrors the tonemap pipeline
|
|
2809
|
+
// construction pattern directly above.
|
|
2810
|
+
if (fxaaModule !== null) {
|
|
2811
|
+
// FXAA BindGroupLayout: 3 entries (texture + sampler + output-policy UBO).
|
|
2812
|
+
// D-2: the fxaa.wgsl fragment stage declares @binding(0) texture_2d<f32>
|
|
2813
|
+
// + @binding(1) sampler + @binding(2) FxaaParams.
|
|
2814
|
+
const fxaaBglResult = runShimSyncStep(
|
|
2815
|
+
() =>
|
|
2816
|
+
rhiDevice.createBindGroupLayout({
|
|
2817
|
+
label: 'fxaa-bgl',
|
|
2818
|
+
entries: [
|
|
2819
|
+
{
|
|
2820
|
+
binding: 0,
|
|
2821
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2822
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
2823
|
+
},
|
|
2824
|
+
{
|
|
2825
|
+
binding: 1,
|
|
2826
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2827
|
+
sampler: { type: 'filtering' },
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
binding: 2,
|
|
2831
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2832
|
+
buffer: { type: 'uniform' },
|
|
2833
|
+
},
|
|
2834
|
+
],
|
|
2835
|
+
}),
|
|
2836
|
+
'webgpu-runtime-error',
|
|
2837
|
+
'createBindGroupLayout(fxaa) succeeded',
|
|
2838
|
+
'check device.limits.maxBindGroupsPerPipelineLayout',
|
|
2839
|
+
);
|
|
2840
|
+
if (!fxaaBglResult.ok) throw fxaaBglResult.error;
|
|
2841
|
+
fxaaBglHandle = fxaaBglResult.value;
|
|
2842
|
+
|
|
2843
|
+
const fxaaPipelineLayoutResult = runShimSyncStep(
|
|
2844
|
+
() =>
|
|
2845
|
+
rhiDevice.createPipelineLayout({
|
|
2846
|
+
label: 'fxaa-pl',
|
|
2847
|
+
bindGroupLayouts: [fxaaBglResult.value],
|
|
2848
|
+
}),
|
|
2849
|
+
'webgpu-runtime-error',
|
|
2850
|
+
'createPipelineLayout(fxaa) succeeded',
|
|
2851
|
+
'verify fxaa BindGroupLayout matches shader @group(0) bindings',
|
|
2852
|
+
);
|
|
2853
|
+
if (!fxaaPipelineLayoutResult.ok) throw fxaaPipelineLayoutResult.error;
|
|
2854
|
+
|
|
2855
|
+
// M2-T4: FXAA pipeline via getOrBuildPipeline (lazy-build, not in SPEC_CONST_TABLE).
|
|
2856
|
+
// Color-space contract: FXAA writes bgra8unorm (NON-srgb) storage format.
|
|
2857
|
+
// Lazy-build via cache miss on first access.
|
|
2858
|
+
{
|
|
2859
|
+
const fxaaSpec: PipelineSpec = {
|
|
2860
|
+
shader: { id: 'forgeax::post::fxaa', passKind: 'post-process', variantSet: undefined },
|
|
2861
|
+
attachments: {
|
|
2862
|
+
colorFormats: [storageBufferCapable ? swapChainFormats.storage : swapChainFormats.view],
|
|
2863
|
+
depthFormat: undefined,
|
|
2864
|
+
sampleCount: 1,
|
|
2865
|
+
},
|
|
2866
|
+
geometry: {
|
|
2867
|
+
topology: 'triangle-list',
|
|
2868
|
+
stripIndexFormat: undefined,
|
|
2869
|
+
vertexLayout: {},
|
|
2870
|
+
},
|
|
2871
|
+
renderState: { cullMode: 'none' },
|
|
2872
|
+
};
|
|
2873
|
+
const modules = {
|
|
2874
|
+
vertex: fxaaModule,
|
|
2875
|
+
fragment: fxaaModule,
|
|
2876
|
+
layout: fxaaPipelineLayoutResult.value,
|
|
2877
|
+
};
|
|
2878
|
+
try {
|
|
2879
|
+
fxaaPipelineHandle = getOrBuildPipeline(
|
|
2880
|
+
fxaaSpec,
|
|
2881
|
+
pipelineDeviceProvider,
|
|
2882
|
+
pipelineCache,
|
|
2883
|
+
modules,
|
|
2884
|
+
) as RenderPipeline;
|
|
2885
|
+
} catch (err) {
|
|
2886
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
2887
|
+
throw new PipelineSpecError({
|
|
2888
|
+
code: 'pipeline-build-failed',
|
|
2889
|
+
detail: { cause: err },
|
|
2890
|
+
hint: 'createRenderPipeline (fxaa fullscreen) failed; inspect gpuMessage',
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// FXAA sampler: linear filter + clamp-to-edge. Clamp-to-edge
|
|
2896
|
+
// prevents edge bleed when sampling at the screen extents.
|
|
2897
|
+
const fxaaSamplerResult = runShimSyncStep(
|
|
2898
|
+
() =>
|
|
2899
|
+
rhiDevice.createSampler({
|
|
2900
|
+
label: 'fxaa-sampler',
|
|
2901
|
+
magFilter: 'linear',
|
|
2902
|
+
minFilter: 'linear',
|
|
2903
|
+
mipmapFilter: 'linear',
|
|
2904
|
+
addressModeU: 'clamp-to-edge',
|
|
2905
|
+
addressModeV: 'clamp-to-edge',
|
|
2906
|
+
}),
|
|
2907
|
+
'webgpu-runtime-error',
|
|
2908
|
+
'createSampler (fxaa) succeeded',
|
|
2909
|
+
'check device.limits.maxSamplersPerShaderStage',
|
|
2910
|
+
);
|
|
2911
|
+
if (!fxaaSamplerResult.ok) throw fxaaSamplerResult.error;
|
|
2912
|
+
fxaaSamplerHandle = fxaaSamplerResult.value;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// feat-20260531-skybox-env-background M3 / w15: skybox pipeline prebuilt.
|
|
2916
|
+
// When the manifest contains the skybox entry (skybox_fs marker, D-7),
|
|
2917
|
+
// construct the 4-entry BGL (texture_cube + sampler + View UBO + rotation UBO),
|
|
2918
|
+
// pipeline layout, fullscreen render pipeline (vertex = fullscreen
|
|
2919
|
+
// triangle from skybox.wgsl, fragment = cubemap sample + write HDR),
|
|
2920
|
+
// and linear clamp-to-edge sampler. Mirrors tonemap/fxaa construction
|
|
2921
|
+
// pattern. Skybox writes to hdrColor rgba16float, NOT to the swap-chain
|
|
2922
|
+
// (plan-strategy D-2: tonemap pass reads hdrColor and maps to LDR).
|
|
2923
|
+
if (skyboxModule !== null) {
|
|
2924
|
+
const skyboxBglResult = runShimSyncStep(
|
|
2925
|
+
() =>
|
|
2926
|
+
rhiDevice.createBindGroupLayout({
|
|
2927
|
+
label: 'skybox-bgl',
|
|
2928
|
+
entries: [
|
|
2929
|
+
{
|
|
2930
|
+
binding: 0,
|
|
2931
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2932
|
+
texture: { sampleType: 'float', viewDimension: 'cube' },
|
|
2933
|
+
},
|
|
2934
|
+
{
|
|
2935
|
+
binding: 1,
|
|
2936
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2937
|
+
sampler: { type: 'filtering' },
|
|
2938
|
+
},
|
|
2939
|
+
{
|
|
2940
|
+
binding: 2,
|
|
2941
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2942
|
+
buffer: { type: 'uniform' },
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
binding: 3,
|
|
2946
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
2947
|
+
buffer: { type: 'uniform' },
|
|
2948
|
+
},
|
|
2949
|
+
],
|
|
2950
|
+
}),
|
|
2951
|
+
'webgpu-runtime-error',
|
|
2952
|
+
'createBindGroupLayout(skybox) succeeded',
|
|
2953
|
+
'check device.limits.maxBindGroupsPerPipelineLayout',
|
|
2954
|
+
);
|
|
2955
|
+
if (!skyboxBglResult.ok) throw skyboxBglResult.error;
|
|
2956
|
+
skyboxBglHandle = skyboxBglResult.value;
|
|
2957
|
+
|
|
2958
|
+
const skyboxPipelineLayoutResult = runShimSyncStep(
|
|
2959
|
+
() =>
|
|
2960
|
+
rhiDevice.createPipelineLayout({
|
|
2961
|
+
label: 'skybox-pl',
|
|
2962
|
+
bindGroupLayouts: [skyboxBglResult.value],
|
|
2963
|
+
}),
|
|
2964
|
+
'webgpu-runtime-error',
|
|
2965
|
+
'createPipelineLayout(skybox) succeeded',
|
|
2966
|
+
'verify skybox BindGroupLayout matches shader @group(0) bindings',
|
|
2967
|
+
);
|
|
2968
|
+
if (!skyboxPipelineLayoutResult.ok) throw skyboxPipelineLayoutResult.error;
|
|
2969
|
+
|
|
2970
|
+
// Skybox pipeline writes to hdrColor rgba16float render target (NOT
|
|
2971
|
+
// M2-T4: skybox pipeline via getOrBuildPipeline + SPEC_CONST_TABLE pre-warm.
|
|
2972
|
+
// Skybox HDR S1 + S4 entries are in SPEC_CONST_TABLE. Register module with
|
|
2973
|
+
// layout + fragmentEntryPoint 'skybox_fs'. The S1 variant is built via
|
|
2974
|
+
// getOrBuildPipeline; MSAA S4 variant catches failure gracefully (same as
|
|
2975
|
+
// pre-M2 behavior: warn + fire error, set handle to null).
|
|
2976
|
+
shaderModuleMap.set('forgeax::skybox::cube', {
|
|
2977
|
+
vertex: skyboxModule,
|
|
2978
|
+
fragment: skyboxModule,
|
|
2979
|
+
fragmentEntryPoint: 'skybox_fs',
|
|
2980
|
+
layout: skyboxPipelineLayoutResult.value,
|
|
2981
|
+
});
|
|
2982
|
+
// S1 (non-MSAA)
|
|
2983
|
+
{
|
|
2984
|
+
const skyboxSpec: PipelineSpec = {
|
|
2985
|
+
shader: { id: 'forgeax::skybox::cube', passKind: 'skybox', variantSet: undefined },
|
|
2986
|
+
attachments: {
|
|
2987
|
+
colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
|
|
2988
|
+
depthFormat: undefined,
|
|
2989
|
+
sampleCount: 1,
|
|
2990
|
+
},
|
|
2991
|
+
geometry: {
|
|
2992
|
+
topology: 'triangle-list',
|
|
2993
|
+
stripIndexFormat: undefined,
|
|
2994
|
+
vertexLayout: {},
|
|
2995
|
+
},
|
|
2996
|
+
renderState: { cullMode: 'none' },
|
|
2997
|
+
};
|
|
2998
|
+
const modules = shaderModuleMap.get('forgeax::skybox::cube');
|
|
2999
|
+
if (modules === undefined) throw new Error('expected skybox module in shaderModuleMap');
|
|
3000
|
+
skyboxPipelineHandle = getOrBuildPipeline(
|
|
3001
|
+
skyboxSpec,
|
|
3002
|
+
pipelineDeviceProvider,
|
|
3003
|
+
pipelineCache,
|
|
3004
|
+
modules,
|
|
3005
|
+
) as RenderPipeline;
|
|
3006
|
+
}
|
|
3007
|
+
// S4 (MSAA variant — graceful failure, same as pre-M2)
|
|
3008
|
+
{
|
|
3009
|
+
const skyboxMsaaSpec: PipelineSpec = {
|
|
3010
|
+
shader: { id: 'forgeax::skybox::cube', passKind: 'skybox', variantSet: undefined },
|
|
3011
|
+
attachments: {
|
|
3012
|
+
colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
|
|
3013
|
+
depthFormat: undefined,
|
|
3014
|
+
sampleCount: 4,
|
|
3015
|
+
},
|
|
3016
|
+
geometry: {
|
|
3017
|
+
topology: 'triangle-list',
|
|
3018
|
+
stripIndexFormat: undefined,
|
|
3019
|
+
vertexLayout: {},
|
|
3020
|
+
},
|
|
3021
|
+
renderState: { cullMode: 'none' },
|
|
3022
|
+
};
|
|
3023
|
+
const modules = shaderModuleMap.get('forgeax::skybox::cube');
|
|
3024
|
+
if (modules === undefined) throw new Error('expected skybox module in shaderModuleMap');
|
|
3025
|
+
try {
|
|
3026
|
+
skyboxPipelineMsaaHandle = getOrBuildPipeline(
|
|
3027
|
+
skyboxMsaaSpec,
|
|
3028
|
+
pipelineDeviceProvider,
|
|
3029
|
+
pipelineCache,
|
|
3030
|
+
modules,
|
|
3031
|
+
) as RenderPipeline;
|
|
3032
|
+
} catch (msaaErr) {
|
|
3033
|
+
// The MSAA variant is a graceful-degradation path -- console.warn is the
|
|
3034
|
+
// canonical signal for "feature degrades, not fails" (noConsole allows warn).
|
|
3035
|
+
console.warn(
|
|
3036
|
+
`[forgeax] skybox MSAA pipeline variant build failed at renderer init; ` +
|
|
3037
|
+
`non-MSAA skybox unaffected. (cause: ${String(msaaErr)})`,
|
|
3038
|
+
);
|
|
3039
|
+
// PipelineSpecError carries the underlying cause in `.detail.cause`;
|
|
3040
|
+
// the warn above already surfaces the message.
|
|
3041
|
+
skyboxPipelineMsaaHandle = null;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// Skybox sampler: filterable (linear/linear/clamp). Clamp-to-edge
|
|
3046
|
+
// prevents seam artifacts at cubemap face boundaries.
|
|
3047
|
+
const skyboxSamplerResult = runShimSyncStep(
|
|
3048
|
+
() =>
|
|
3049
|
+
rhiDevice.createSampler({
|
|
3050
|
+
label: 'skybox-sampler',
|
|
3051
|
+
magFilter: 'linear',
|
|
3052
|
+
minFilter: 'linear',
|
|
3053
|
+
mipmapFilter: 'linear',
|
|
3054
|
+
addressModeU: 'clamp-to-edge',
|
|
3055
|
+
addressModeV: 'clamp-to-edge',
|
|
3056
|
+
}),
|
|
3057
|
+
'webgpu-runtime-error',
|
|
3058
|
+
'createSampler (skybox) succeeded',
|
|
3059
|
+
'check device.limits.maxSamplersPerShaderStage',
|
|
3060
|
+
);
|
|
3061
|
+
if (!skyboxSamplerResult.ok) throw skyboxSamplerResult.error;
|
|
3062
|
+
skyboxSamplerHandle = skyboxSamplerResult.value;
|
|
3063
|
+
|
|
3064
|
+
const skyboxRotationBufferResult = runShimSyncStep(
|
|
3065
|
+
() =>
|
|
3066
|
+
rhiDevice.createBuffer({
|
|
3067
|
+
label: 'skybox-rotation-ubo',
|
|
3068
|
+
size: 16,
|
|
3069
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
3070
|
+
mappedAtCreation: false,
|
|
3071
|
+
}),
|
|
3072
|
+
'webgpu-runtime-error',
|
|
3073
|
+
'createBuffer (skybox rotation) succeeded',
|
|
3074
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
3075
|
+
);
|
|
3076
|
+
if (!skyboxRotationBufferResult.ok) throw skyboxRotationBufferResult.error;
|
|
3077
|
+
skyboxRotationBufferHandle = skyboxRotationBufferResult.value;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// feat-20260531-bloom-first-declarative-render-graph-pass / w13:
|
|
3081
|
+
// bloom pipeline assembly (D-1, D-4, D-6). Assembled inside the
|
|
3082
|
+
// (unlit+pbr+tonemap) gate for rhiDevice access but bloom modules
|
|
3083
|
+
// are optional — each guard is independent. When bloom modules are
|
|
3084
|
+
// absent (legacy manifest), handles stay null and execute closures
|
|
3085
|
+
// skip the bloom passes entirely (zero-overhead opt-out).
|
|
3086
|
+
//
|
|
3087
|
+
// Pipeline roster:
|
|
3088
|
+
// bloom-bright : 1-tex + sampler + UBO@2 BGL, rgba16float target
|
|
3089
|
+
// bloom-blur-h : same BGL, same module as blur-v, H-axis texelSize
|
|
3090
|
+
// bloom-blur-v : same BGL, same module as blur-h, V-axis texelSize
|
|
3091
|
+
// bloom-composite: 2-tex + sampler + UBO@3 BGL, rgba16float target
|
|
3092
|
+
|
|
3093
|
+
ensureBloomResources = (): void => {
|
|
3094
|
+
if (bloomResourcesReady) return;
|
|
3095
|
+
const candidateScope = rendererScope.createChild(`${rendererScope.owner}:standard-bloom`);
|
|
3096
|
+
const adopt = <T>(
|
|
3097
|
+
kind: Parameters<DeviceScope['_adopt']>[0],
|
|
3098
|
+
value: T,
|
|
3099
|
+
cleanup: (value: T) => void,
|
|
3100
|
+
): T => {
|
|
3101
|
+
candidateScope._adopt(kind, value, cleanup);
|
|
3102
|
+
return value;
|
|
3103
|
+
};
|
|
3104
|
+
try {
|
|
3105
|
+
// Shared bloom sampler: linear filter + clamp-to-edge (all 4 passes
|
|
3106
|
+
// sample from textures using fullscreen triangle UVs).
|
|
3107
|
+
if (
|
|
3108
|
+
bloomBrightModule !== null ||
|
|
3109
|
+
bloomBlurModule !== null ||
|
|
3110
|
+
bloomCompositeModule !== null
|
|
3111
|
+
) {
|
|
3112
|
+
const bloomSamplerResult = runShimSyncStep(
|
|
3113
|
+
() =>
|
|
3114
|
+
rhiDevice.createSampler({
|
|
3115
|
+
label: 'bloom-sampler',
|
|
3116
|
+
magFilter: 'linear',
|
|
3117
|
+
minFilter: 'linear',
|
|
3118
|
+
mipmapFilter: 'linear',
|
|
3119
|
+
addressModeU: 'clamp-to-edge',
|
|
3120
|
+
addressModeV: 'clamp-to-edge',
|
|
3121
|
+
}),
|
|
3122
|
+
'webgpu-runtime-error',
|
|
3123
|
+
'createSampler (bloom) succeeded',
|
|
3124
|
+
'check device.limits.maxSamplersPerShaderStage',
|
|
3125
|
+
);
|
|
3126
|
+
if (!bloomSamplerResult.ok) throw bloomSamplerResult.error;
|
|
3127
|
+
bloomSamplerHandle = adopt('binding', bloomSamplerResult.value, () => undefined);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// Bloom bright: 1-tex + sampler + UBO@2 BGL (D-4).
|
|
3131
|
+
if (bloomBrightModule !== null && bloomSamplerHandle !== null) {
|
|
3132
|
+
const brightBglResult = runShimSyncStep(
|
|
3133
|
+
() =>
|
|
3134
|
+
rhiDevice.createBindGroupLayout({
|
|
3135
|
+
label: 'bloom-bright-bgl',
|
|
3136
|
+
entries: [
|
|
3137
|
+
{
|
|
3138
|
+
binding: 0,
|
|
3139
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3140
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
3141
|
+
},
|
|
3142
|
+
{
|
|
3143
|
+
binding: 1,
|
|
3144
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3145
|
+
sampler: { type: 'filtering' },
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
binding: 2,
|
|
3149
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3150
|
+
buffer: { type: 'uniform' },
|
|
3151
|
+
},
|
|
3152
|
+
],
|
|
3153
|
+
}),
|
|
3154
|
+
'webgpu-runtime-error',
|
|
3155
|
+
'createBindGroupLayout(bloom-bright) succeeded',
|
|
3156
|
+
'check device.limits.maxBindGroupsPerPipelineLayout',
|
|
3157
|
+
);
|
|
3158
|
+
if (!brightBglResult.ok) throw brightBglResult.error;
|
|
3159
|
+
bloomBrightBglHandle = adopt('binding', brightBglResult.value, () => undefined);
|
|
3160
|
+
|
|
3161
|
+
const brightPlResult = runShimSyncStep(
|
|
3162
|
+
() =>
|
|
3163
|
+
rhiDevice.createPipelineLayout({
|
|
3164
|
+
label: 'bloom-bright-pl',
|
|
3165
|
+
bindGroupLayouts: [brightBglResult.value],
|
|
3166
|
+
}),
|
|
3167
|
+
'webgpu-runtime-error',
|
|
3168
|
+
'createPipelineLayout(bloom-bright) succeeded',
|
|
3169
|
+
'verify bloom-bright BindGroupLayout matches shader @group(0) bindings',
|
|
3170
|
+
);
|
|
3171
|
+
if (!brightPlResult.ok) throw brightPlResult.error;
|
|
3172
|
+
adopt('binding', brightPlResult.value, () => undefined);
|
|
3173
|
+
|
|
3174
|
+
// M2-T4: bloom-bright pipeline via getOrBuildPipeline (lazy-build).
|
|
3175
|
+
{
|
|
3176
|
+
const brightSpec: PipelineSpec = {
|
|
3177
|
+
shader: {
|
|
3178
|
+
id: 'forgeax::post::bloom-bright',
|
|
3179
|
+
passKind: 'post-process',
|
|
3180
|
+
variantSet: undefined,
|
|
3181
|
+
},
|
|
3182
|
+
attachments: {
|
|
3183
|
+
colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
|
|
3184
|
+
depthFormat: undefined,
|
|
3185
|
+
sampleCount: 1,
|
|
3186
|
+
},
|
|
3187
|
+
geometry: {
|
|
3188
|
+
topology: 'triangle-list',
|
|
3189
|
+
stripIndexFormat: undefined,
|
|
3190
|
+
vertexLayout: {},
|
|
3191
|
+
},
|
|
3192
|
+
renderState: { cullMode: 'none' },
|
|
3193
|
+
};
|
|
3194
|
+
const modules = {
|
|
3195
|
+
vertex: bloomBrightModule,
|
|
3196
|
+
fragment: bloomBrightModule,
|
|
3197
|
+
layout: brightPlResult.value,
|
|
3198
|
+
};
|
|
3199
|
+
try {
|
|
3200
|
+
bloomBrightPipelineHandle = getOrBuildPipeline(
|
|
3201
|
+
brightSpec,
|
|
3202
|
+
pipelineDeviceProvider,
|
|
3203
|
+
pipelineCache,
|
|
3204
|
+
modules,
|
|
3205
|
+
) as RenderPipeline;
|
|
3206
|
+
bloomBrightPipelineHandle = adopt(
|
|
3207
|
+
'pipeline',
|
|
3208
|
+
bloomBrightPipelineHandle,
|
|
3209
|
+
() => undefined,
|
|
3210
|
+
);
|
|
3211
|
+
} catch (err) {
|
|
3212
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
3213
|
+
throw new PipelineSpecError({
|
|
3214
|
+
code: 'pipeline-build-failed',
|
|
3215
|
+
detail: { cause: err },
|
|
3216
|
+
hint: 'createRenderPipeline (bloom-bright fullscreen) failed; inspect gpuMessage',
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
// Bright params UBO: 16 B std140 (threshold f32 + 12 B pad).
|
|
3222
|
+
const brightParamsResult = runShimSyncStep(
|
|
3223
|
+
() =>
|
|
3224
|
+
rhiDevice.createBuffer({
|
|
3225
|
+
label: 'bloom-bright-params-ubo',
|
|
3226
|
+
size: BRIGHT_PARAMS_BYTES,
|
|
3227
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
3228
|
+
mappedAtCreation: false,
|
|
3229
|
+
}),
|
|
3230
|
+
'webgpu-runtime-error',
|
|
3231
|
+
'createBuffer (bloom-bright params UBO) succeeded',
|
|
3232
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
3233
|
+
);
|
|
3234
|
+
if (!brightParamsResult.ok) throw brightParamsResult.error;
|
|
3235
|
+
bloomBrightParamsBufferHandle = adopt('buffer', brightParamsResult.value, (value) => {
|
|
3236
|
+
rhiDevice.destroyBuffer(value);
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// Bloom blur H/V: same BGL (1-tex + sampler + UBO@2), same module,
|
|
3241
|
+
// two separate pipelines with per-axis texelSize (D-1, D-4).
|
|
3242
|
+
if (bloomBlurModule !== null && bloomSamplerHandle !== null) {
|
|
3243
|
+
const blurBglResult = runShimSyncStep(
|
|
3244
|
+
() =>
|
|
3245
|
+
rhiDevice.createBindGroupLayout({
|
|
3246
|
+
label: 'bloom-blur-bgl',
|
|
3247
|
+
entries: [
|
|
3248
|
+
{
|
|
3249
|
+
binding: 0,
|
|
3250
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3251
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
3252
|
+
},
|
|
3253
|
+
{
|
|
3254
|
+
binding: 1,
|
|
3255
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3256
|
+
sampler: { type: 'filtering' },
|
|
3257
|
+
},
|
|
3258
|
+
{
|
|
3259
|
+
binding: 2,
|
|
3260
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3261
|
+
buffer: { type: 'uniform' },
|
|
3262
|
+
},
|
|
3263
|
+
],
|
|
3264
|
+
}),
|
|
3265
|
+
'webgpu-runtime-error',
|
|
3266
|
+
'createBindGroupLayout(bloom-blur) succeeded',
|
|
3267
|
+
'check device.limits.maxBindGroupsPerPipelineLayout',
|
|
3268
|
+
);
|
|
3269
|
+
if (!blurBglResult.ok) throw blurBglResult.error;
|
|
3270
|
+
bloomBlurBglHandle = adopt('binding', blurBglResult.value, () => undefined);
|
|
3271
|
+
|
|
3272
|
+
const blurPlResult = runShimSyncStep(
|
|
3273
|
+
() =>
|
|
3274
|
+
rhiDevice.createPipelineLayout({
|
|
3275
|
+
label: 'bloom-blur-pl',
|
|
3276
|
+
bindGroupLayouts: [blurBglResult.value],
|
|
3277
|
+
}),
|
|
3278
|
+
'webgpu-runtime-error',
|
|
3279
|
+
'createPipelineLayout(bloom-blur) succeeded',
|
|
3280
|
+
'verify bloom-blur BindGroupLayout matches shader @group(0) bindings',
|
|
3281
|
+
);
|
|
3282
|
+
if (!blurPlResult.ok) throw blurPlResult.error;
|
|
3283
|
+
adopt('binding', blurPlResult.value, () => undefined);
|
|
3284
|
+
|
|
3285
|
+
// M2-T4: bloom-blur H/V pipelines via getOrBuildPipeline (lazy-build).
|
|
3286
|
+
// H and V share the same PSO descriptor — only per-axis texelSize UBO
|
|
3287
|
+
// distinguishes them at record time. getOrBuildPipeline cache-hit on the
|
|
3288
|
+
// second call returns the same handle (identical spec, identical PSO).
|
|
3289
|
+
{
|
|
3290
|
+
const blurSpec: PipelineSpec = {
|
|
3291
|
+
shader: {
|
|
3292
|
+
id: 'forgeax::post::bloom-blur',
|
|
3293
|
+
passKind: 'post-process',
|
|
3294
|
+
variantSet: undefined,
|
|
3295
|
+
},
|
|
3296
|
+
attachments: {
|
|
3297
|
+
colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
|
|
3298
|
+
depthFormat: undefined,
|
|
3299
|
+
sampleCount: 1,
|
|
3300
|
+
},
|
|
3301
|
+
geometry: {
|
|
3302
|
+
topology: 'triangle-list',
|
|
3303
|
+
stripIndexFormat: undefined,
|
|
3304
|
+
vertexLayout: {},
|
|
3305
|
+
},
|
|
3306
|
+
renderState: { cullMode: 'none' },
|
|
3307
|
+
};
|
|
3308
|
+
const modules = {
|
|
3309
|
+
vertex: bloomBlurModule,
|
|
3310
|
+
fragment: bloomBlurModule,
|
|
3311
|
+
layout: blurPlResult.value,
|
|
3312
|
+
label: 'bloom-blur-h-pipeline',
|
|
3313
|
+
};
|
|
3314
|
+
try {
|
|
3315
|
+
bloomBlurHPipelineHandle = getOrBuildPipeline(
|
|
3316
|
+
blurSpec,
|
|
3317
|
+
pipelineDeviceProvider,
|
|
3318
|
+
pipelineCache,
|
|
3319
|
+
modules,
|
|
3320
|
+
) as RenderPipeline;
|
|
3321
|
+
bloomBlurHPipelineHandle = adopt(
|
|
3322
|
+
'pipeline',
|
|
3323
|
+
bloomBlurHPipelineHandle,
|
|
3324
|
+
() => undefined,
|
|
3325
|
+
);
|
|
3326
|
+
} catch (err) {
|
|
3327
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
3328
|
+
throw new PipelineSpecError({
|
|
3329
|
+
code: 'pipeline-build-failed',
|
|
3330
|
+
detail: { cause: err },
|
|
3331
|
+
hint: 'createRenderPipeline (bloom-blur-h fullscreen) failed; inspect gpuMessage',
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
// V is cache-hit on the same spec (identical PSO; per-axis UBO differentiates at record time).
|
|
3335
|
+
bloomBlurVPipelineHandle = bloomBlurHPipelineHandle;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
// Blur params UBOs: 16 B std140 (texelSize.xy + radius + pad) each.
|
|
3339
|
+
// bug-20260625: one per axis -- H and V must not share a buffer (the
|
|
3340
|
+
// shared-buffer writeBuffer race made both passes blur vertically).
|
|
3341
|
+
const blurHParamsResult = runShimSyncStep(
|
|
3342
|
+
() =>
|
|
3343
|
+
rhiDevice.createBuffer({
|
|
3344
|
+
label: 'bloom-blur-h-params-ubo',
|
|
3345
|
+
size: BLUR_PARAMS_BYTES,
|
|
3346
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
3347
|
+
mappedAtCreation: false,
|
|
3348
|
+
}),
|
|
3349
|
+
'webgpu-runtime-error',
|
|
3350
|
+
'createBuffer (bloom-blur-h params UBO) succeeded',
|
|
3351
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
3352
|
+
);
|
|
3353
|
+
if (!blurHParamsResult.ok) throw blurHParamsResult.error;
|
|
3354
|
+
bloomBlurHParamsBufferHandle = adopt('buffer', blurHParamsResult.value, (value) => {
|
|
3355
|
+
rhiDevice.destroyBuffer(value);
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
const blurVParamsResult = runShimSyncStep(
|
|
3359
|
+
() =>
|
|
3360
|
+
rhiDevice.createBuffer({
|
|
3361
|
+
label: 'bloom-blur-v-params-ubo',
|
|
3362
|
+
size: BLUR_PARAMS_BYTES,
|
|
3363
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
3364
|
+
mappedAtCreation: false,
|
|
3365
|
+
}),
|
|
3366
|
+
'webgpu-runtime-error',
|
|
3367
|
+
'createBuffer (bloom-blur-v params UBO) succeeded',
|
|
3368
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
3369
|
+
);
|
|
3370
|
+
if (!blurVParamsResult.ok) throw blurVParamsResult.error;
|
|
3371
|
+
bloomBlurVParamsBufferHandle = adopt('buffer', blurVParamsResult.value, (value) => {
|
|
3372
|
+
rhiDevice.destroyBuffer(value);
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// Bloom composite: 2-tex + sampler + UBO@3 BGL (D-4, D-5).
|
|
3377
|
+
if (bloomCompositeModule !== null && bloomSamplerHandle !== null) {
|
|
3378
|
+
const compositeBglResult = runShimSyncStep(
|
|
3379
|
+
() =>
|
|
3380
|
+
rhiDevice.createBindGroupLayout({
|
|
3381
|
+
label: 'bloom-composite-bgl',
|
|
3382
|
+
entries: [
|
|
3383
|
+
{
|
|
3384
|
+
binding: 0,
|
|
3385
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3386
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
binding: 1,
|
|
3390
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3391
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
binding: 2,
|
|
3395
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3396
|
+
sampler: { type: 'filtering' },
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
binding: 3,
|
|
3400
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3401
|
+
buffer: { type: 'uniform' },
|
|
3402
|
+
},
|
|
3403
|
+
],
|
|
3404
|
+
}),
|
|
3405
|
+
'webgpu-runtime-error',
|
|
3406
|
+
'createBindGroupLayout(bloom-composite) succeeded',
|
|
3407
|
+
'check device.limits.maxBindGroupsPerPipelineLayout',
|
|
3408
|
+
);
|
|
3409
|
+
if (!compositeBglResult.ok) throw compositeBglResult.error;
|
|
3410
|
+
bloomCompositeBglHandle = adopt('binding', compositeBglResult.value, () => undefined);
|
|
3411
|
+
|
|
3412
|
+
const compositePlResult = runShimSyncStep(
|
|
3413
|
+
() =>
|
|
3414
|
+
rhiDevice.createPipelineLayout({
|
|
3415
|
+
label: 'bloom-composite-pl',
|
|
3416
|
+
bindGroupLayouts: [compositeBglResult.value],
|
|
3417
|
+
}),
|
|
3418
|
+
'webgpu-runtime-error',
|
|
3419
|
+
'createPipelineLayout(bloom-composite) succeeded',
|
|
3420
|
+
'verify bloom-composite BindGroupLayout matches shader @group(0) bindings',
|
|
3421
|
+
);
|
|
3422
|
+
if (!compositePlResult.ok) throw compositePlResult.error;
|
|
3423
|
+
adopt('binding', compositePlResult.value, () => undefined);
|
|
3424
|
+
|
|
3425
|
+
// M2-T4: bloom-composite pipeline via getOrBuildPipeline (lazy-build).
|
|
3426
|
+
{
|
|
3427
|
+
const compositeSpec: PipelineSpec = {
|
|
3428
|
+
shader: {
|
|
3429
|
+
id: 'forgeax::post::bloom-composite',
|
|
3430
|
+
passKind: 'post-process',
|
|
3431
|
+
variantSet: undefined,
|
|
3432
|
+
},
|
|
3433
|
+
attachments: {
|
|
3434
|
+
colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
|
|
3435
|
+
depthFormat: undefined,
|
|
3436
|
+
sampleCount: 1,
|
|
3437
|
+
},
|
|
3438
|
+
geometry: {
|
|
3439
|
+
topology: 'triangle-list',
|
|
3440
|
+
stripIndexFormat: undefined,
|
|
3441
|
+
vertexLayout: {},
|
|
3442
|
+
},
|
|
3443
|
+
renderState: { cullMode: 'none' },
|
|
3444
|
+
};
|
|
3445
|
+
const modules = {
|
|
3446
|
+
vertex: bloomCompositeModule,
|
|
3447
|
+
fragment: bloomCompositeModule,
|
|
3448
|
+
layout: compositePlResult.value,
|
|
3449
|
+
};
|
|
3450
|
+
try {
|
|
3451
|
+
bloomCompositePipelineHandle = getOrBuildPipeline(
|
|
3452
|
+
compositeSpec,
|
|
3453
|
+
pipelineDeviceProvider,
|
|
3454
|
+
pipelineCache,
|
|
3455
|
+
modules,
|
|
3456
|
+
) as RenderPipeline;
|
|
3457
|
+
bloomCompositePipelineHandle = adopt(
|
|
3458
|
+
'pipeline',
|
|
3459
|
+
bloomCompositePipelineHandle,
|
|
3460
|
+
() => undefined,
|
|
3461
|
+
);
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
3464
|
+
throw new PipelineSpecError({
|
|
3465
|
+
code: 'pipeline-build-failed',
|
|
3466
|
+
detail: { cause: err },
|
|
3467
|
+
hint: 'createRenderPipeline (bloom-composite fullscreen) failed; inspect gpuMessage',
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// Composite params UBO: 16 B std140 (intensity f32 + 12 B pad).
|
|
3473
|
+
const compositeParamsResult = runShimSyncStep(
|
|
3474
|
+
() =>
|
|
3475
|
+
rhiDevice.createBuffer({
|
|
3476
|
+
label: 'bloom-composite-params-ubo',
|
|
3477
|
+
size: COMPOSITE_PARAMS_BYTES,
|
|
3478
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
3479
|
+
mappedAtCreation: false,
|
|
3480
|
+
}),
|
|
3481
|
+
'webgpu-runtime-error',
|
|
3482
|
+
'createBuffer (bloom-composite params UBO) succeeded',
|
|
3483
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
3484
|
+
);
|
|
3485
|
+
if (!compositeParamsResult.ok) throw compositeParamsResult.error;
|
|
3486
|
+
bloomCompositeParamsBufferHandle = adopt(
|
|
3487
|
+
'buffer',
|
|
3488
|
+
compositeParamsResult.value,
|
|
3489
|
+
(value) => {
|
|
3490
|
+
rhiDevice.destroyBuffer(value);
|
|
3491
|
+
},
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
bloomCandidateBundle = {
|
|
3496
|
+
scope: candidateScope,
|
|
3497
|
+
generation: bloomGeneration + 1,
|
|
3498
|
+
bloomBrightPipeline: bloomBrightPipelineHandle,
|
|
3499
|
+
bloomBlurHPipeline: bloomBlurHPipelineHandle,
|
|
3500
|
+
bloomBlurVPipeline: bloomBlurVPipelineHandle,
|
|
3501
|
+
bloomCompositePipeline: bloomCompositePipelineHandle,
|
|
3502
|
+
bloomBrightBindGroupLayout: bloomBrightBglHandle,
|
|
3503
|
+
bloomBlurBindGroupLayout: bloomBlurBglHandle,
|
|
3504
|
+
bloomCompositeBindGroupLayout: bloomCompositeBglHandle,
|
|
3505
|
+
bloomSampler: bloomSamplerHandle,
|
|
3506
|
+
bloomBrightParamsBuffer: bloomBrightParamsBufferHandle,
|
|
3507
|
+
bloomBlurHParamsBuffer: bloomBlurHParamsBufferHandle,
|
|
3508
|
+
bloomBlurVParamsBuffer: bloomBlurVParamsBufferHandle,
|
|
3509
|
+
bloomCompositeParamsBuffer: bloomCompositeParamsBufferHandle,
|
|
3510
|
+
};
|
|
3511
|
+
inspectBloomResources = (graph): BloomInspection => {
|
|
3512
|
+
const retiring = [...retiringBloomBundles].sort(
|
|
3513
|
+
(left, right) => left.generation - right.generation,
|
|
3514
|
+
);
|
|
3515
|
+
const active = bloomActiveBundle?.scope.isAlive() ? bloomActiveBundle : undefined;
|
|
3516
|
+
const liveRetiring = retiring.filter((bundle) => bundle.scope.state === 'retiring');
|
|
3517
|
+
const selected = active ?? liveRetiring[liveRetiring.length - 1];
|
|
3518
|
+
if (selected === undefined) {
|
|
3519
|
+
return {
|
|
3520
|
+
graphStatus: 'empty',
|
|
3521
|
+
enabled: false,
|
|
3522
|
+
targetCount: 0,
|
|
3523
|
+
targetBytes: 0,
|
|
3524
|
+
resourceCount: 0,
|
|
3525
|
+
passCount: 0,
|
|
3526
|
+
encodeCount: 0,
|
|
3527
|
+
bindGroupCount: 0,
|
|
3528
|
+
uploadCount: 0,
|
|
3529
|
+
residentChildBytes: 0,
|
|
3530
|
+
generation: 0,
|
|
3531
|
+
state: 'off',
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
const state = active === undefined ? 'retiring' : 'active';
|
|
3535
|
+
const ownedBundles = [...(active === undefined ? [] : [active]), ...liveRetiring];
|
|
3536
|
+
const graphInspection = inspectStandardBloomGraph(graph);
|
|
3537
|
+
const receipts = lastSuccessfulBloomReceipts ?? {
|
|
3538
|
+
uploadCount: 0,
|
|
3539
|
+
bindGroupCount: 0,
|
|
3540
|
+
encodeCount: 0,
|
|
3541
|
+
};
|
|
3542
|
+
return {
|
|
3543
|
+
graphStatus: active === undefined ? 'empty' : graphInspection.status,
|
|
3544
|
+
enabled: active !== undefined,
|
|
3545
|
+
targetCount: active === undefined ? 0 : graphInspection.targetCount,
|
|
3546
|
+
targetBytes: active === undefined ? 0 : graphInspection.targetBytes,
|
|
3547
|
+
resourceCount: ownedBundles.reduce(
|
|
3548
|
+
(count, bundle) => count + bundle.scope.resourceDelta(),
|
|
3549
|
+
0,
|
|
3550
|
+
),
|
|
3551
|
+
passCount: active === undefined ? 0 : graphInspection.passCount,
|
|
3552
|
+
encodeCount: active === undefined ? 0 : receipts.encodeCount,
|
|
3553
|
+
bindGroupCount: active === undefined ? 0 : receipts.bindGroupCount,
|
|
3554
|
+
uploadCount: active === undefined ? 0 : receipts.uploadCount,
|
|
3555
|
+
residentChildBytes: 0,
|
|
3556
|
+
generation: selected?.generation ?? 0,
|
|
3557
|
+
state,
|
|
3558
|
+
};
|
|
3559
|
+
};
|
|
3560
|
+
commitBloomResources = () => {
|
|
3561
|
+
if (bloomCandidateBundle?.scope !== candidateScope) return;
|
|
3562
|
+
bloomActiveBundle = bloomCandidateBundle;
|
|
3563
|
+
bloomCandidateBundle = undefined;
|
|
3564
|
+
bloomGeneration += 1;
|
|
3565
|
+
bloomResourcesReady = true;
|
|
3566
|
+
};
|
|
3567
|
+
discardBloomResources = () => {
|
|
3568
|
+
if (bloomCandidateBundle?.scope !== candidateScope) return;
|
|
3569
|
+
candidateScope.abandon();
|
|
3570
|
+
bloomCandidateBundle = undefined;
|
|
3571
|
+
bloomResourcesReady = false;
|
|
3572
|
+
};
|
|
3573
|
+
const reportBloomFenceFailure = (cause: unknown): void => {
|
|
3574
|
+
if (cause instanceof RhiError) {
|
|
3575
|
+
errorRegistry.fire(cause);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
const detailError = {
|
|
3579
|
+
code: 'bloom-fence-rejected',
|
|
3580
|
+
message: String(cause),
|
|
3581
|
+
...(cause instanceof Error ? { name: cause.name } : {}),
|
|
3582
|
+
};
|
|
3583
|
+
errorRegistry.fire(
|
|
3584
|
+
new RhiError({
|
|
3585
|
+
code: 'webgpu-runtime-error',
|
|
3586
|
+
expected: 'Bloom queue completion resolves after the submitted frame retires',
|
|
3587
|
+
hint: 'inspect renderer errors for the queue completion cause',
|
|
3588
|
+
detail: { error: detailError },
|
|
3589
|
+
}),
|
|
3590
|
+
);
|
|
3591
|
+
};
|
|
3592
|
+
retireBloomResources = (completion) => {
|
|
3593
|
+
const retiring = bloomActiveBundle;
|
|
3594
|
+
if (retiring === undefined) return;
|
|
3595
|
+
bloomActiveBundle = undefined;
|
|
3596
|
+
bloomResourcesReady = false;
|
|
3597
|
+
retiringBloomBundles.add(retiring);
|
|
3598
|
+
retiring.scope.beginRetire();
|
|
3599
|
+
void completion.then(
|
|
3600
|
+
() => {
|
|
3601
|
+
if (!retiringBloomBundles.has(retiring)) return;
|
|
3602
|
+
retiring.scope.retire();
|
|
3603
|
+
retiringBloomBundles.delete(retiring);
|
|
3604
|
+
},
|
|
3605
|
+
(cause) => {
|
|
3606
|
+
if (!retiringBloomBundles.has(retiring)) return;
|
|
3607
|
+
reportBloomFenceFailure(cause);
|
|
3608
|
+
retiring.scope.retire();
|
|
3609
|
+
retiringBloomBundles.delete(retiring);
|
|
3610
|
+
},
|
|
3611
|
+
);
|
|
3612
|
+
};
|
|
3613
|
+
drainBloomResources = () => {
|
|
3614
|
+
if (bloomCandidateBundle !== undefined) {
|
|
3615
|
+
bloomCandidateBundle.scope.abandon();
|
|
3616
|
+
bloomCandidateBundle = undefined;
|
|
3617
|
+
}
|
|
3618
|
+
if (bloomActiveBundle !== undefined) {
|
|
3619
|
+
bloomActiveBundle.scope.retire();
|
|
3620
|
+
bloomActiveBundle = undefined;
|
|
3621
|
+
}
|
|
3622
|
+
for (const retiring of retiringBloomBundles) retiring.scope.retire();
|
|
3623
|
+
retiringBloomBundles.clear();
|
|
3624
|
+
bloomResourcesReady = false;
|
|
3625
|
+
};
|
|
3626
|
+
} catch (cause) {
|
|
3627
|
+
candidateScope.abandon();
|
|
3628
|
+
if (bloomCandidateBundle?.scope === candidateScope) bloomCandidateBundle = undefined;
|
|
3629
|
+
commitBloomResources = undefined;
|
|
3630
|
+
discardBloomResources = undefined;
|
|
3631
|
+
drainBloomResources = undefined;
|
|
3632
|
+
bloomBrightPipelineHandle = null;
|
|
3633
|
+
bloomBlurHPipelineHandle = null;
|
|
3634
|
+
bloomBlurVPipelineHandle = null;
|
|
3635
|
+
bloomCompositePipelineHandle = null;
|
|
3636
|
+
bloomBrightBglHandle = null;
|
|
3637
|
+
bloomBlurBglHandle = null;
|
|
3638
|
+
bloomCompositeBglHandle = null;
|
|
3639
|
+
bloomSamplerHandle = null;
|
|
3640
|
+
bloomBrightParamsBufferHandle = null;
|
|
3641
|
+
bloomBlurHParamsBufferHandle = null;
|
|
3642
|
+
bloomBlurVParamsBufferHandle = null;
|
|
3643
|
+
bloomCompositeParamsBufferHandle = null;
|
|
3644
|
+
throw cause;
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
|
|
3648
|
+
// ── feat-20260612-hdrp-ssao M6 / w26 + w43 + M8 / w37 ───────────────────
|
|
3649
|
+
//
|
|
3650
|
+
// SSAO post-processing chain: 2 passes (calc + blur) with a dedicated
|
|
3651
|
+
// 9-entry BGL matching hdrp-ssao.wgsl @group(0) bindings 0-8 (D-A + D-D).
|
|
3652
|
+
// Both pipelines share the same BGL (calc binds 0-6, blur binds 7-8 +
|
|
3653
|
+
// reuses the 256B uniform write); the WGSL declares all entries even when
|
|
3654
|
+
// a given pass leaves some unused, so wgpu/dawn pipeline-layout matching
|
|
3655
|
+
// is one-shot.
|
|
3656
|
+
//
|
|
3657
|
+
// w37 dawn-blocker fix (carry from w27-a): pre-M8 the BGL had a single
|
|
3658
|
+
// sampler at binding 3 typed 'filtering' that paired with the depth
|
|
3659
|
+
// texture at binding 5. WebGPU requires depth textures to be sampled with
|
|
3660
|
+
// a non-filtering / comparison sampler — the mismatch crashed every HDRP
|
|
3661
|
+
// PSO build on dawn (7 dawn tests red unrelated to SSAO itself).
|
|
3662
|
+
// ssao_depth_sampler at binding 6 (non-filtering) is dedicated to
|
|
3663
|
+
// hdr_depth; the existing filtering sampler at binding 3 stays for the
|
|
3664
|
+
// float noise / gbuffer_normal textures.
|
|
3665
|
+
//
|
|
3666
|
+
// Fullscreen triangle vertex (vs_ssao), R8 scalar fragment output. Cull
|
|
3667
|
+
// none, no depth/stencil (fullscreen post-process pass).
|
|
3668
|
+
if (ssaoModule !== null) {
|
|
3669
|
+
// Dedicated SSAO BGL: 9 entries (bindings 0-8 per current WGSL).
|
|
3670
|
+
// 0 = uniform (SsaoUniform 256B)
|
|
3671
|
+
// 1 = uniform (kernel UBO)
|
|
3672
|
+
// 2 = texture_2d (noise)
|
|
3673
|
+
// 3 = sampler (filtering, for noise / normal float textures)
|
|
3674
|
+
// 4 = texture_2d (gbuffer_normal)
|
|
3675
|
+
// 5 = texture_depth_2d (hdrDepth)
|
|
3676
|
+
// 6 = sampler (non-filtering, dedicated to depth) -- w37
|
|
3677
|
+
// 7 = texture_2d (ssaoRaw, blur input) -- w37
|
|
3678
|
+
// 8 = sampler (filtering, for ssaoRaw) -- w37
|
|
3679
|
+
const ssaoBglResult = runShimSyncStep(
|
|
3680
|
+
() =>
|
|
3681
|
+
rhiDevice.createBindGroupLayout({
|
|
3682
|
+
label: 'ssao-bgl',
|
|
3683
|
+
entries: [
|
|
3684
|
+
{
|
|
3685
|
+
binding: 0,
|
|
3686
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3687
|
+
buffer: { type: 'uniform' },
|
|
3688
|
+
},
|
|
3689
|
+
{
|
|
3690
|
+
binding: 1,
|
|
3691
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3692
|
+
buffer: { type: 'uniform' },
|
|
3693
|
+
},
|
|
3694
|
+
{
|
|
3695
|
+
binding: 2,
|
|
3696
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3697
|
+
// hdrp-ssao-noise is rgba32float; without the
|
|
3698
|
+
// float32-filterable extension this format is unfilterable.
|
|
3699
|
+
// The SSAO noise generator uses NEAREST/REPEAT sampling
|
|
3700
|
+
// so unfilterable-float is sufficient and works on dawn.
|
|
3701
|
+
texture: { sampleType: 'unfilterable-float', viewDimension: '2d' },
|
|
3702
|
+
},
|
|
3703
|
+
{
|
|
3704
|
+
binding: 3,
|
|
3705
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3706
|
+
// The noise sampler (binding 2 + 4) must be non-filtering
|
|
3707
|
+
// because binding 2 is unfilterable-float. WebGPU validation
|
|
3708
|
+
// pairs sampler 'filtering' kind with filterable textures
|
|
3709
|
+
// only; using non-filtering for the noise + gbuffer_normal
|
|
3710
|
+
// path keeps both samples valid.
|
|
3711
|
+
sampler: { type: 'non-filtering' },
|
|
3712
|
+
},
|
|
3713
|
+
{
|
|
3714
|
+
binding: 4,
|
|
3715
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3716
|
+
texture: { sampleType: 'unfilterable-float', viewDimension: '2d' },
|
|
3717
|
+
},
|
|
3718
|
+
{
|
|
3719
|
+
binding: 5,
|
|
3720
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3721
|
+
texture: { sampleType: 'depth', viewDimension: '2d' },
|
|
3722
|
+
},
|
|
3723
|
+
{
|
|
3724
|
+
binding: 6,
|
|
3725
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3726
|
+
sampler: { type: 'non-filtering' },
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
binding: 7,
|
|
3730
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3731
|
+
texture: { sampleType: 'unfilterable-float', viewDimension: '2d' },
|
|
3732
|
+
},
|
|
3733
|
+
{
|
|
3734
|
+
binding: 8,
|
|
3735
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
3736
|
+
// Shared with binding 3 (single sampler resource); both BGL
|
|
3737
|
+
// entries must declare the same sampler-type kind.
|
|
3738
|
+
sampler: { type: 'non-filtering' },
|
|
3739
|
+
},
|
|
3740
|
+
],
|
|
3741
|
+
}),
|
|
3742
|
+
'webgpu-runtime-error',
|
|
3743
|
+
'createBindGroupLayout(ssao) succeeded',
|
|
3744
|
+
'check device.limits.maxBindGroupsPerPipelineLayout',
|
|
3745
|
+
);
|
|
3746
|
+
if (!ssaoBglResult.ok) throw ssaoBglResult.error;
|
|
3747
|
+
ssaoBglHandle = ssaoBglResult.value;
|
|
3748
|
+
|
|
3749
|
+
const ssaoPipelineLayoutResult = runShimSyncStep(
|
|
3750
|
+
() =>
|
|
3751
|
+
rhiDevice.createPipelineLayout({
|
|
3752
|
+
label: 'ssao-pl',
|
|
3753
|
+
bindGroupLayouts: [ssaoBglResult.value],
|
|
3754
|
+
}),
|
|
3755
|
+
'webgpu-runtime-error',
|
|
3756
|
+
'createPipelineLayout(ssao) succeeded',
|
|
3757
|
+
'verify SSAO BindGroupLayout matches shader @group(2) bindings',
|
|
3758
|
+
);
|
|
3759
|
+
if (!ssaoPipelineLayoutResult.ok) throw ssaoPipelineLayoutResult.error;
|
|
3760
|
+
|
|
3761
|
+
// M2-T4: SSAO calc + blur pipelines via getOrBuildPipeline (lazy-build).
|
|
3762
|
+
// Different fragment entry points distinguish calc (fs_ssao_calc) from
|
|
3763
|
+
// blur (fs_ssao_blur); distinct synthetic shader IDs ensure correct cache
|
|
3764
|
+
// key separation (same module, different entry points → different PSOs).
|
|
3765
|
+
{
|
|
3766
|
+
const layout = ssaoPipelineLayoutResult.value;
|
|
3767
|
+
const baseModules = {
|
|
3768
|
+
vertex: ssaoModule,
|
|
3769
|
+
fragment: ssaoModule,
|
|
3770
|
+
vertexEntryPoint: 'vs_ssao',
|
|
3771
|
+
layout,
|
|
3772
|
+
};
|
|
3773
|
+
// SSAO calc
|
|
3774
|
+
{
|
|
3775
|
+
const calcSpec: PipelineSpec = {
|
|
3776
|
+
shader: {
|
|
3777
|
+
id: 'forgeax::post::ssao-calc',
|
|
3778
|
+
passKind: 'post-process',
|
|
3779
|
+
variantSet: undefined,
|
|
3780
|
+
},
|
|
3781
|
+
attachments: {
|
|
3782
|
+
colorFormats: ['r8unorm'],
|
|
3783
|
+
depthFormat: undefined,
|
|
3784
|
+
sampleCount: 1,
|
|
3785
|
+
},
|
|
3786
|
+
geometry: {
|
|
3787
|
+
topology: 'triangle-list',
|
|
3788
|
+
stripIndexFormat: undefined,
|
|
3789
|
+
vertexLayout: {},
|
|
3790
|
+
},
|
|
3791
|
+
renderState: { cullMode: 'none' },
|
|
3792
|
+
};
|
|
3793
|
+
try {
|
|
3794
|
+
ssaoCalcPipelineHandle = getOrBuildPipeline(
|
|
3795
|
+
calcSpec,
|
|
3796
|
+
pipelineDeviceProvider,
|
|
3797
|
+
pipelineCache,
|
|
3798
|
+
{ ...baseModules, fragmentEntryPoint: 'fs_ssao_calc' },
|
|
3799
|
+
) as RenderPipeline;
|
|
3800
|
+
} catch (err) {
|
|
3801
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
3802
|
+
throw new PipelineSpecError({
|
|
3803
|
+
code: 'pipeline-build-failed',
|
|
3804
|
+
detail: { cause: err },
|
|
3805
|
+
hint: 'createRenderPipeline (ssao-calc fullscreen) failed; inspect gpuMessage',
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
// SSAO blur
|
|
3810
|
+
{
|
|
3811
|
+
const blurSpec: PipelineSpec = {
|
|
3812
|
+
shader: {
|
|
3813
|
+
id: 'forgeax::post::ssao-blur',
|
|
3814
|
+
passKind: 'post-process',
|
|
3815
|
+
variantSet: undefined,
|
|
3816
|
+
},
|
|
3817
|
+
attachments: {
|
|
3818
|
+
colorFormats: ['r8unorm'],
|
|
3819
|
+
depthFormat: undefined,
|
|
3820
|
+
sampleCount: 1,
|
|
3821
|
+
},
|
|
3822
|
+
geometry: {
|
|
3823
|
+
topology: 'triangle-list',
|
|
3824
|
+
stripIndexFormat: undefined,
|
|
3825
|
+
vertexLayout: {},
|
|
3826
|
+
},
|
|
3827
|
+
renderState: { cullMode: 'none' },
|
|
3828
|
+
};
|
|
3829
|
+
try {
|
|
3830
|
+
ssaoBlurPipelineHandle = getOrBuildPipeline(
|
|
3831
|
+
blurSpec,
|
|
3832
|
+
pipelineDeviceProvider,
|
|
3833
|
+
pipelineCache,
|
|
3834
|
+
{ ...baseModules, fragmentEntryPoint: 'fs_ssao_blur' },
|
|
3835
|
+
) as RenderPipeline;
|
|
3836
|
+
} catch (err) {
|
|
3837
|
+
if (err instanceof PipelineSpecError) throw err;
|
|
3838
|
+
throw new PipelineSpecError({
|
|
3839
|
+
code: 'pipeline-build-failed',
|
|
3840
|
+
detail: { cause: err },
|
|
3841
|
+
hint: 'createRenderPipeline (ssao-blur fullscreen) failed; inspect gpuMessage',
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
// feat-20260601-device/gpu-residency-extraction M1 (D-9 sub-contract 1): prewarm
|
|
3850
|
+
// the mipmap pipeline cache for the smoke texture formats while still on the
|
|
3851
|
+
// async `renderer.initialization` path. This builds the one-time mipmap shader module +
|
|
3852
|
+
// per-format pipeline into the deviceCache so the record-stage texture
|
|
3853
|
+
// ensureResident (sync) reproduces the pre-extraction async uploadTexture
|
|
3854
|
+
// byte-for-byte without an async stall in the synchronous draw frame. A build
|
|
3855
|
+
// failure here is surfaced through ready's reject channel (structured RhiError).
|
|
3856
|
+
//
|
|
3857
|
+
// Gated on `manifestEntries.length > 0` (the same Camera-only / clear-pass
|
|
3858
|
+
// skip as the Step-2 pipeline compile, bug-20260519 D-3): a zero-manifest
|
|
3859
|
+
// world renders no material geometry, so no texture is ever made resident and
|
|
3860
|
+
// the mipmap shader-module build (a `createShaderModule` call) must not fire
|
|
3861
|
+
// -- preserving the zero-manifest "0 createShaderModule" invariant
|
|
3862
|
+
// (renderer-ready.test.ts AC-02).
|
|
3863
|
+
if (manifestEntries.length > 0) {
|
|
3864
|
+
const prewarmRes = await gpuStore.prewarmMipmapPipeline(rhiDevice, MIPMAP_PREWARM_FORMATS);
|
|
3865
|
+
if (!prewarmRes.ok) throw prewarmRes.error;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
const pipelineState: PipelineState = {
|
|
3869
|
+
device: rhiDevice,
|
|
3870
|
+
// feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
|
|
3871
|
+
// pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
|
|
3872
|
+
// backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
|
|
3873
|
+
// configurations (6F builtin + 12F procedural; the 6F + pbr combination
|
|
3874
|
+
// does not exist per D-2). All three share the identical 4-BindGroupLayout
|
|
3875
|
+
// chain so material BG entries built once compose for each of the three
|
|
3876
|
+
// pipelines (charter P5 consistent abstraction). The legacy `pipeline`
|
|
3877
|
+
// alias field has been retired; consumers select per
|
|
3878
|
+
// (mat.materialShaderId, mesh.layout) tuple via the record-stage three-way
|
|
3879
|
+
// setPipeline branch (w22.11).
|
|
3880
|
+
// bug-20260519: BUILTIN cube migrated to 12F so the legacy
|
|
3881
|
+
// `unlitBuiltinPipeline` (+ its zero-stride `unlitBuiltinDummyAttrBuffer`)
|
|
3882
|
+
// is gone; consumers pick per `mat.materialShaderId` only via the record-stage
|
|
3883
|
+
// 2-way `setPipeline` branch.
|
|
3884
|
+
// feat-20260615-pipeline-spec-ssot M2-T4: standard material PSOs are
|
|
3885
|
+
// pre-warmed in SPEC_CONST_TABLE and cached in pipelineCache. Cache
|
|
3886
|
+
// lookup replaces the prior local-handle variables (SSOT axiom D-12).
|
|
3887
|
+
// lookupSpecInTable resolves entries by (shaderId, isHdr, sampleCount);
|
|
3888
|
+
// null when the spec entry's module was not compiled (empty-manifest path).
|
|
3889
|
+
// (see definition near SPEC_CONST boot-time pre-warm block above)
|
|
3890
|
+
unlitPipeline: getCachedPipelineOrNull('forgeax::default-unlit', false, 1),
|
|
3891
|
+
standardPipeline: getCachedPipelineOrNull('forgeax::default-standard-pbr', false, 1),
|
|
3892
|
+
unlitPipelineMsaa: getCachedPipelineOrNull('forgeax::default-unlit', false, 4),
|
|
3893
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w14 (D-7):
|
|
3894
|
+
// four sprite-dedicated boot-time pre-warms are gone. Sprite PSO now
|
|
3895
|
+
// lands lazily through the generic per-MaterialShader pipeline cache
|
|
3896
|
+
// (`getMaterialShaderPipeline('forgeax::sprite', ...)`) keyed on
|
|
3897
|
+
// premultiplied-alpha renderState at draw time. -4 entries off
|
|
3898
|
+
// SPEC_CONST_TABLE (15 from 19); -4 fields off PipelineState.
|
|
3899
|
+
unlitPipelineHdrMsaa: getCachedPipelineOrNull('forgeax::default-unlit', true, 4),
|
|
3900
|
+
// feat-20260523-shader-template-instance-split M9-T03 (D-PipelineBuilder):
|
|
3901
|
+
// expose the shared pbr/unlit/sprite pipeline layout so the per-
|
|
3902
|
+
// MaterialShader pipeline cache callback (createRenderer.ts
|
|
3903
|
+
// getMaterialShaderPipeline) can reuse it at lazy build time without
|
|
3904
|
+
// re-running pbrLayouts construction. `null` when the manifest is empty
|
|
3905
|
+
// (Camera-only path; bug-20260519 D-3 nullable parallel to the unlit /
|
|
3906
|
+
// standard pipeline fields above).
|
|
3907
|
+
pbrPipelineLayout:
|
|
3908
|
+
unlitModule !== null && pbrModule !== null ? pipelineLayoutResult.value : null,
|
|
3909
|
+
// HDRP variant uses the unified group(2) layout built above.
|
|
3910
|
+
hdrpPbrPipelineLayout:
|
|
3911
|
+
unlitModule !== null && pbrModule !== null ? hdrpPbrPipelineLayoutHandle : null,
|
|
3912
|
+
hdrpProbePbrPipelineLayout:
|
|
3913
|
+
unlitModule !== null && pbrModule !== null ? hdrpProbePbrPipelineLayoutHandle : null,
|
|
3914
|
+
hdrpClusterMembershipPipeline,
|
|
3915
|
+
hdrpClusterMembershipBindGroupLayout,
|
|
3916
|
+
// Skin variant uses its dedicated mesh-array layout; both handles are
|
|
3917
|
+
// gated on the same boot modules and may be null for camera-only startup.
|
|
3918
|
+
pbrSkinPipelineLayout:
|
|
3919
|
+
unlitModule !== null && pbrModule !== null ? pbrSkinPipelineLayoutHandle : null,
|
|
3920
|
+
pbrSkinProbePipelineLayout:
|
|
3921
|
+
unlitModule !== null && pbrModule !== null ? pbrSkinProbePipelineLayoutHandle : null,
|
|
3922
|
+
pbrSkinMeshBindGroupLayout:
|
|
3923
|
+
unlitModule !== null && pbrModule !== null ? pbrSkinMeshBindGroupLayoutHandle : null,
|
|
3924
|
+
hdrpSkinPipelineLayout:
|
|
3925
|
+
unlitModule !== null && pbrModule !== null ? hdrpSkinPipelineLayoutHandle : null,
|
|
3926
|
+
hdrpSkinMeshBindGroupLayout:
|
|
3927
|
+
unlitModule !== null && pbrModule !== null ? hdrpSkinMeshBindGroupLayoutHandle : null,
|
|
3928
|
+
// feat-20260612-skin-palette-per-frame-upload M1 / m1-2: animator-ready
|
|
3929
|
+
// skin-palette allocator (replaces the prior identity-buffer stub).
|
|
3930
|
+
// Same gating as `pbrSkinPipelineLayout` -- `null` when the skin
|
|
3931
|
+
// pipeline-layout build itself failed.
|
|
3932
|
+
skinPaletteAllocator:
|
|
3933
|
+
unlitModule !== null && pbrModule !== null ? skinPaletteAllocatorHandle : null,
|
|
3934
|
+
meshes: meshHandles,
|
|
3935
|
+
format: swapChainFormats.storage,
|
|
3936
|
+
colorAttachmentFormat: swapChainFormats.view,
|
|
3937
|
+
surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
|
|
3938
|
+
viewBindGroupLayout: viewBglResult.value,
|
|
3939
|
+
extendedLightingAvailable: extendedLightingShaderAvailable,
|
|
3940
|
+
projectorAvailable,
|
|
3941
|
+
materialBindGroupLayout: materialBglResult.value,
|
|
3942
|
+
meshBindGroupLayout: meshArrayBglResult.value,
|
|
3943
|
+
viewUniformBuffer: viewUboResult.value,
|
|
3944
|
+
pointsLinesViewBuffer: pointsLinesViewBufferResult.value,
|
|
3945
|
+
shadowCasterCascadeBuffer: shadowCasterCascadeUboResult.value,
|
|
3946
|
+
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
3947
|
+
materialUniformBuffer: meshSsboState.material,
|
|
3948
|
+
meshStorageBuffer: meshSsboState.mesh,
|
|
3949
|
+
instancesBindGroupLayout: instancesBglResult.value,
|
|
3950
|
+
probeInstancesBindGroupLayout: probeInstancesBglResult.value,
|
|
3951
|
+
identityInstanceBuffer: identityInstanceResult.value,
|
|
3952
|
+
defaultSampler: defaultSamplerResult.value,
|
|
3953
|
+
nearestSampler: nearestSamplerResult.value,
|
|
3954
|
+
fallbackTextureView: fallbackTextureViewResult.value,
|
|
3955
|
+
defaultWhiteTextureView: fallbackTextureViewResult.value,
|
|
3956
|
+
// Normal-slot fallback view (1x1 RGBA8 (128,128,255,255)). RG=(128,128)
|
|
3957
|
+
// decodes to tangent (0,0,1) under pbr.wgsl's RG-only normal decoder.
|
|
3958
|
+
// Distinct from defaultWhiteTextureView because RG=(255,255)=1.0 gives
|
|
3959
|
+
// sqrt(1 - 1 - 1) = NaN, breaking the no-normal-map case.
|
|
3960
|
+
defaultNormalTextureView: fallbackNormalTextureViewResult.value,
|
|
3961
|
+
// feat-20260519-tonemap-reinhard-mvp M2 / T-M2.5: HDR fallback for the
|
|
3962
|
+
// built-in unlit pipeline (rgba16float colour attachment). Standard
|
|
3963
|
+
// material variants are resolved by the material-shader cache.
|
|
3964
|
+
unlitPipelineHdr: getCachedPipelineOrNull('forgeax::default-unlit', true, 1),
|
|
3965
|
+
// feat-20260520-directional-light-shadow-mapping M2 / w14 (D-1):
|
|
3966
|
+
// 1x1 depth32float fallback bound at viewBindGroup binding(3).
|
|
3967
|
+
shadowFallbackTextureView: shadowFallbackViewResult.value,
|
|
3968
|
+
...extendedLightingFallback,
|
|
3969
|
+
// feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: 1x1x6
|
|
3970
|
+
// depth32float cube_array fallback bound at viewBindGroup binding(5)
|
|
3971
|
+
// when no PointLightShadow snapshots are active. Always-present
|
|
3972
|
+
// (24 B GPU footprint); ShadowAtlas takes over when a real frame has
|
|
3973
|
+
// pointShadowSnapshots.length > 0.
|
|
3974
|
+
shadowAtlasFallbackTextureView: shadowAtlasFallbackViewResult.value,
|
|
3975
|
+
// feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: 64 B point
|
|
3976
|
+
// shadow params UBO bound at viewBindGroup binding(6). Written per
|
|
3977
|
+
// frame from `frameState.pointShadowSnapshots`.
|
|
3978
|
+
shadowParamsBuffer: shadowParamsBufferResult.value,
|
|
3979
|
+
// feat-20260625-spot-light-shadow-mapping w25: the spot lightViewProj
|
|
3980
|
+
// matrices fold into the View UBO tail (`view.spotLightViewProj`); no
|
|
3981
|
+
// dedicated buffer / view-BG binding 9 (WebGL2 uniform-buffer budget).
|
|
3982
|
+
skylightFallback,
|
|
3983
|
+
// feat-20260529-rendergraph-pass-abstraction M3 / w11 (D-2 + Finding 3):
|
|
3984
|
+
// per-pass mutable resource slots moved to PerPassResources.
|
|
3985
|
+
perPassResources: {
|
|
3986
|
+
depthTexture: null,
|
|
3987
|
+
depthTextureView: null,
|
|
3988
|
+
depthTextureWidth: 0,
|
|
3989
|
+
depthTextureHeight: 0,
|
|
3990
|
+
configured: false,
|
|
3991
|
+
hdrColorTexture: null,
|
|
3992
|
+
hdrColorView: null,
|
|
3993
|
+
hdrDepthTexture: null,
|
|
3994
|
+
hdrDepthView: null,
|
|
3995
|
+
hdrTextureWidth: 0,
|
|
3996
|
+
hdrTextureHeight: 0,
|
|
3997
|
+
hdrDepthSampleCount: 1,
|
|
3998
|
+
fxaaPipeline: fxaaPipelineHandle,
|
|
3999
|
+
fxaaBindGroupLayout: fxaaBglHandle,
|
|
4000
|
+
fxaaSampler: fxaaSamplerHandle,
|
|
4001
|
+
// feat-20260604-learn-render-4.10-anti-aliasing-msaa M2 / w7: MSAA
|
|
4002
|
+
// attachment slots. All null/0 until the first antialias='msaa' frame.
|
|
4003
|
+
msaaColorTexture: null,
|
|
4004
|
+
msaaColorView: null,
|
|
4005
|
+
msaaSpriteColorTexture: null,
|
|
4006
|
+
msaaSpriteColorView: null,
|
|
4007
|
+
msaaDepthTexture: null,
|
|
4008
|
+
msaaDepthView: null,
|
|
4009
|
+
msaaTextureWidth: 0,
|
|
4010
|
+
msaaTextureHeight: 0,
|
|
4011
|
+
hdrColorMsaaTexture: null,
|
|
4012
|
+
hdrColorMsaaView: null,
|
|
4013
|
+
skyboxPipeline: skyboxPipelineHandle,
|
|
4014
|
+
skyboxPipelineMsaa: skyboxPipelineMsaaHandle,
|
|
4015
|
+
skyboxBindGroupLayout: skyboxBglHandle,
|
|
4016
|
+
skyboxSampler: skyboxSamplerHandle,
|
|
4017
|
+
skyboxRotationBuffer: skyboxRotationBufferHandle,
|
|
4018
|
+
shadowTexture: null,
|
|
4019
|
+
shadowMapSize: 0,
|
|
4020
|
+
shadowCascadeCount: 0,
|
|
4021
|
+
shadowSampler: shadowSamplerResult.value,
|
|
4022
|
+
shadowLightSpaceMatrix: null,
|
|
4023
|
+
shadowCsmLightViewProj: null,
|
|
4024
|
+
shadowCsmSelection: null,
|
|
4025
|
+
// feat-20260531-bloom-first-declarative-render-graph-pass / w13 + w16:
|
|
4026
|
+
// bloom per-pass resource slots. Pipeline handles assembled during
|
|
4027
|
+
// buildReadyWebGPU (marker-triage + compile + createRenderPipeline).
|
|
4028
|
+
// Intermediate textures are allocate in the execute closures at 1/2-res
|
|
4029
|
+
// (ensureLazyTexture, slot width/height tracking for size-drift rebuild).
|
|
4030
|
+
// BindGroup caches survive until the intermediate view is invalidated
|
|
4031
|
+
// by a resize (width/height drift forces null).
|
|
4032
|
+
bloomBrightPipeline: null,
|
|
4033
|
+
bloomBlurHPipeline: null,
|
|
4034
|
+
bloomBlurVPipeline: null,
|
|
4035
|
+
bloomCompositePipeline: null,
|
|
4036
|
+
bloomBrightBindGroupLayout: null,
|
|
4037
|
+
bloomBlurBindGroupLayout: null,
|
|
4038
|
+
bloomCompositeBindGroupLayout: null,
|
|
4039
|
+
bloomSampler: null,
|
|
4040
|
+
bloomBrightParamsBuffer: null,
|
|
4041
|
+
bloomBlurHParamsBuffer: null,
|
|
4042
|
+
bloomBlurVParamsBuffer: null,
|
|
4043
|
+
bloomCompositeParamsBuffer: null,
|
|
4044
|
+
ensureBloomResources: ensureBloomResourcesStable,
|
|
4045
|
+
getBloomResources: getBloomResourcesStable,
|
|
4046
|
+
commitBloomResources: commitBloomResourcesStable,
|
|
4047
|
+
commitBloomFrameReceipts: (receipts) => {
|
|
4048
|
+
lastSuccessfulBloomReceipts = receipts;
|
|
4049
|
+
},
|
|
4050
|
+
discardBloomResources: discardBloomResourcesStable,
|
|
4051
|
+
retireBloomResources: retireBloomResourcesStable,
|
|
4052
|
+
drainBloomResources: drainBloomResourcesStable,
|
|
4053
|
+
inspectBloomResources: inspectBloomResourcesStable,
|
|
4054
|
+
bloomBrightTexture: null,
|
|
4055
|
+
bloomBrightView: null,
|
|
4056
|
+
bloomBrightWidth: 0,
|
|
4057
|
+
bloomBrightHeight: 0,
|
|
4058
|
+
bloomBlurHTexture: null,
|
|
4059
|
+
bloomBlurHView: null,
|
|
4060
|
+
bloomBlurHWidth: 0,
|
|
4061
|
+
bloomBlurHHeight: 0,
|
|
4062
|
+
bloomBlurVTexture: null,
|
|
4063
|
+
bloomBlurVView: null,
|
|
4064
|
+
bloomBlurVWidth: 0,
|
|
4065
|
+
bloomBlurVHeight: 0,
|
|
4066
|
+
// feat-20260612-hdrp-ssao M6 / w26 + M8 / w38: SSAO pipeline slots.
|
|
4067
|
+
ssaoCalcPipeline: ssaoCalcPipelineHandle,
|
|
4068
|
+
ssaoBlurPipeline: ssaoBlurPipelineHandle,
|
|
4069
|
+
ssaoBgl: ssaoBglHandle,
|
|
4070
|
+
// M8 / w38: lazy-allocated on first SSAO record frame. Sampler kinds
|
|
4071
|
+
// and the 1x1 ssaoRaw fallback view are constant across frames and
|
|
4072
|
+
// cached after first construction.
|
|
4073
|
+
ssaoFilteringSampler: null,
|
|
4074
|
+
ssaoDepthSampler: null,
|
|
4075
|
+
ssaoFallbackRawView: null,
|
|
4076
|
+
},
|
|
4077
|
+
};
|
|
4078
|
+
return pipelineState;
|
|
4079
|
+
}
|