@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,854 @@
|
|
|
1
|
+
// Material shader policy and async adapter.
|
|
2
|
+
// Variant selection, module caching, and manifest material preparation stay
|
|
3
|
+
// independent from renderer lifetime and generation-scoped ready state.
|
|
4
|
+
|
|
5
|
+
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
6
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
7
|
+
import type { PipelineLayout, Result, RhiDevice, ShaderModule } from '@forgeax/engine-rhi';
|
|
8
|
+
import { err, ok, RhiError } from '@forgeax/engine-rhi';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
11
|
+
findVariantByKey,
|
|
12
|
+
type MaterialShaderManifestEntry,
|
|
13
|
+
type ShaderCatalog,
|
|
14
|
+
type ShaderCatalogDevice,
|
|
15
|
+
standardPhysicalTextureFields,
|
|
16
|
+
} from '@forgeax/engine-shader';
|
|
17
|
+
import type { AssetGuid as AssetGuidType, ParamSchemaEntry } from '@forgeax/engine-types';
|
|
18
|
+
import { derive } from '@forgeax/engine-types';
|
|
19
|
+
import type { RenderFeatureMaterialShaderBindingContract } from '../features/plan';
|
|
20
|
+
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
|
|
21
|
+
import type { RhiErrorListenerRegistry } from '../lifecycle';
|
|
22
|
+
import { assertStorageBufferCap } from '../light-buffer-layout';
|
|
23
|
+
import { buildPbrMaterialUserRegionEntries } from '../pbr-pipeline';
|
|
24
|
+
import {
|
|
25
|
+
deriveExtendedLightingCapability,
|
|
26
|
+
extendedLightingSampledTextureCapacityAvailable,
|
|
27
|
+
} from '../prepare/extended-lighting/resources';
|
|
28
|
+
import { STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES } from './shader-prewarm-policy';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sync-shaped `ShaderCatalogDevice` adapter wrapping the backend pack's
|
|
32
|
+
* async `createShaderModule(rhiDevice, desc)`.
|
|
33
|
+
*
|
|
34
|
+
* Shape: fire-and-forget pre-bake — the first call for hash X returns
|
|
35
|
+
* `Result.err(RhiError)` (a pending signal) while kicking off the async
|
|
36
|
+
* createShaderModule + cache write; subsequent calls for the same hash hit
|
|
37
|
+
* the cache and return `Result.ok(ShaderModule)` (charter proposition 9
|
|
38
|
+
* graceful degradation).
|
|
39
|
+
*
|
|
40
|
+
* @placeholder M3 scope limit: the full async pre-bake wiring is delivered
|
|
41
|
+
* by the ECS-driven render pipeline (later closure). The current shape is
|
|
42
|
+
* already enough to support MVP-2.2 instance-per-engine + AI consumer
|
|
43
|
+
* onboarding (charter proposition 1 progressive disclosure / proposition 4
|
|
44
|
+
* explicit failure with retry guidance in the error hint).
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Extended ShaderCatalogDevice contract for engine internals: adds a
|
|
48
|
+
* `seedModule(label, module)` to populate the lazy adapter cache from an
|
|
49
|
+
* already-compiled module, so eager-built modules (PBR / unlit / shadow
|
|
50
|
+
* caster) and the lazy MaterialShader pipeline cache share one cache and
|
|
51
|
+
* the lazy path is hit-on-first-call (no 1-frame warmup) for engine-shipped
|
|
52
|
+
* shaders. Externally still typed as ShaderCatalogDevice (see callers in
|
|
53
|
+
* ShaderCatalog).
|
|
54
|
+
*/
|
|
55
|
+
export interface ShaderDeviceAdapterInternal extends ShaderCatalogDevice {
|
|
56
|
+
/**
|
|
57
|
+
* feat-20260609 R3-fixup: seed the adapter's moduleCache with an
|
|
58
|
+
* already-compiled ShaderModule under `label`. Used by the eager
|
|
59
|
+
* shadow_caster pre-bake so the lazy PSO build hit on
|
|
60
|
+
* `module-forgeax::default-shadow-caster` returns OK on frame 1.
|
|
61
|
+
*/
|
|
62
|
+
seedModule(label: string, module: ShaderModule): void;
|
|
63
|
+
invalidateModule(label: string): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type ShaderModuleAdapterMode = 'validated' | 'immediate';
|
|
67
|
+
|
|
68
|
+
export function makeShaderDeviceAdapter(
|
|
69
|
+
rhiDevice: RhiDevice,
|
|
70
|
+
_errorRegistry: RhiErrorListenerRegistry,
|
|
71
|
+
asyncCreateShaderModule:
|
|
72
|
+
| ((
|
|
73
|
+
device: RhiDevice,
|
|
74
|
+
desc: { code: string; label?: string | undefined },
|
|
75
|
+
) => Promise<Result<ShaderModule, RhiError>>)
|
|
76
|
+
| undefined,
|
|
77
|
+
immediateCreateShaderModule:
|
|
78
|
+
| ((
|
|
79
|
+
device: RhiDevice,
|
|
80
|
+
desc: { code: string; label?: string | undefined },
|
|
81
|
+
) => Result<ShaderModule, RhiError>)
|
|
82
|
+
| undefined,
|
|
83
|
+
mode: ShaderModuleAdapterMode = 'validated',
|
|
84
|
+
): ShaderDeviceAdapterInternal {
|
|
85
|
+
const moduleCache = new Map<string, ShaderModule>();
|
|
86
|
+
const errorCache = new Map<string, RhiError>();
|
|
87
|
+
const pending = new Map<string, number>();
|
|
88
|
+
const epochs = new Map<string, number>();
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
createShaderModule(desc): Result<ShaderModule, RhiError> {
|
|
92
|
+
const key = desc.label ?? desc.code;
|
|
93
|
+
const cachedModule = moduleCache.get(key);
|
|
94
|
+
if (cachedModule !== undefined) return ok(cachedModule);
|
|
95
|
+
const cachedError = errorCache.get(key);
|
|
96
|
+
if (cachedError !== undefined) return err(cachedError);
|
|
97
|
+
|
|
98
|
+
// WebGPU's raw createShaderModule is synchronous. Only the explicitly
|
|
99
|
+
// selected immediate adapter uses that entry: generated feature programs
|
|
100
|
+
// can avoid a slow getCompilationInfo() round trip, while the default
|
|
101
|
+
// validated adapter keeps the compiler-readiness barrier for ordinary
|
|
102
|
+
// render pipelines. The public async factory still owns diagnostics;
|
|
103
|
+
// pipeline creation remains the validation point for the fast path.
|
|
104
|
+
if (mode === 'immediate' && immediateCreateShaderModule !== undefined) {
|
|
105
|
+
const immediateResult = immediateCreateShaderModule(rhiDevice, {
|
|
106
|
+
code: desc.code,
|
|
107
|
+
...(desc.label === undefined ? {} : { label: desc.label }),
|
|
108
|
+
});
|
|
109
|
+
if (immediateResult.ok) {
|
|
110
|
+
moduleCache.set(key, immediateResult.value);
|
|
111
|
+
return immediateResult;
|
|
112
|
+
}
|
|
113
|
+
errorCache.set(key, immediateResult.error);
|
|
114
|
+
return immediateResult;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fire async creation; first sync call returns pending error (the AI consumer retry pattern).
|
|
118
|
+
// M3 D-P4: rhi-webgpu supplies the async factory; rhi-wgpu (no top-level
|
|
119
|
+
// async factory) falls back to the synchronous device.createShaderModule
|
|
120
|
+
// entry which already returns Result<ShaderModule, RhiError>.
|
|
121
|
+
if (!pending.has(key)) {
|
|
122
|
+
const epoch = epochs.get(key) ?? 0;
|
|
123
|
+
pending.set(key, epoch);
|
|
124
|
+
const desc2: { code: string; label?: string | undefined } = { code: desc.code };
|
|
125
|
+
if (desc.label !== undefined) desc2.label = desc.label;
|
|
126
|
+
const asyncResult: Promise<Result<ShaderModule, RhiError>> = asyncCreateShaderModule
|
|
127
|
+
? asyncCreateShaderModule(rhiDevice, desc2)
|
|
128
|
+
: invokeDeviceCreateShaderModule(rhiDevice, desc2);
|
|
129
|
+
void asyncResult.then((result) => {
|
|
130
|
+
if (pending.get(key) !== epoch || (epochs.get(key) ?? 0) !== epoch) return;
|
|
131
|
+
if (result.ok) {
|
|
132
|
+
moduleCache.set(key, result.value);
|
|
133
|
+
} else {
|
|
134
|
+
errorCache.set(key, result.error);
|
|
135
|
+
}
|
|
136
|
+
pending.delete(key);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return err(makeRhiNotAvailableError(key));
|
|
141
|
+
},
|
|
142
|
+
seedModule(label: string, module: ShaderModule): void {
|
|
143
|
+
moduleCache.set(label, module);
|
|
144
|
+
},
|
|
145
|
+
invalidateModule(label: string): void {
|
|
146
|
+
epochs.set(label, (epochs.get(label) ?? 0) + 1);
|
|
147
|
+
moduleCache.delete(label);
|
|
148
|
+
errorCache.delete(label);
|
|
149
|
+
pending.delete(label);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Fallback path for the M3 D-P4 auto-select facade when the backend pack
|
|
156
|
+
* does not supply a top-level async `createShaderModule` (e.g. the rhi-wgpu
|
|
157
|
+
* path which routes through the synchronous `device.createShaderModule`
|
|
158
|
+
* entry, or an explicit escape-hatch instance that omits the async
|
|
159
|
+
* factory). The forgeax `RhiDevice` interface intentionally does not expose
|
|
160
|
+
* a sync `createShaderModule` (fix-f3; see packages/rhi/src/index.ts line
|
|
161
|
+
* 1164), so this helper performs a structural probe + returns a structured
|
|
162
|
+
* error when neither the async factory nor a structural sync entry exists
|
|
163
|
+
* (charter proposition 4 explicit failure baseline).
|
|
164
|
+
*/
|
|
165
|
+
export function invokeDeviceCreateShaderModule(
|
|
166
|
+
rhiDevice: RhiDevice,
|
|
167
|
+
desc: { code: string; label?: string | undefined },
|
|
168
|
+
): Promise<Result<ShaderModule, RhiError>> {
|
|
169
|
+
const candidate = (
|
|
170
|
+
rhiDevice as RhiDevice & {
|
|
171
|
+
createShaderModule?: (d: {
|
|
172
|
+
code: string;
|
|
173
|
+
label?: string | undefined;
|
|
174
|
+
}) => Result<ShaderModule, RhiError>;
|
|
175
|
+
}
|
|
176
|
+
).createShaderModule;
|
|
177
|
+
if (typeof candidate === 'function') {
|
|
178
|
+
try {
|
|
179
|
+
return Promise.resolve(candidate.call(rhiDevice, desc));
|
|
180
|
+
} catch (caught) {
|
|
181
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
182
|
+
return Promise.resolve(
|
|
183
|
+
err(
|
|
184
|
+
new RhiError({
|
|
185
|
+
code: 'shader-compile-failed',
|
|
186
|
+
expected: 'device.createShaderModule returns Result<ShaderModule, RhiError>',
|
|
187
|
+
hint: `synchronous fallback threw: ${message}`,
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return Promise.resolve(
|
|
194
|
+
err(
|
|
195
|
+
new RhiError({
|
|
196
|
+
code: 'rhi-not-available',
|
|
197
|
+
expected:
|
|
198
|
+
'either RhiBackendPack.createShaderModule (async) or device.createShaderModule (sync) available',
|
|
199
|
+
hint: 'the explicit RhiInstance escape hatch must expose a top-level createShaderModule(device, desc) or RhiDevice.createShaderModule(desc) entry',
|
|
200
|
+
}),
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Synthesizes a `RhiError` with code 'rhi-not-available' for the
|
|
207
|
+
* sync-adapter pending path. AI-consumer onboarding: consume
|
|
208
|
+
* `.code === 'rhi-not-available'` → after `await loadManifest` retry
|
|
209
|
+
* `registry.get` (charter proposition 4 explicit failure + proposition 9).
|
|
210
|
+
*
|
|
211
|
+
* 'rhi-not-available' is the placeholder member of the 5-member closed
|
|
212
|
+
* union (plan-decisions OQ-P2).
|
|
213
|
+
*/
|
|
214
|
+
function makeRhiNotAvailableError(key: string): RhiError {
|
|
215
|
+
return new RhiError({
|
|
216
|
+
code: 'rhi-not-available',
|
|
217
|
+
expected: 'shader module pre-bake to complete asynchronously',
|
|
218
|
+
hint: `shader module '${key}' is still pending; await registry.loadManifest() finished and retry registry.get(hash) on next frame`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* bug-20260601-hello-tonemap-material-register M1: prepare engine-shipped
|
|
224
|
+
* material shaders (cap gate + manifest load + registration) before the
|
|
225
|
+
* renderer is returned so that `register<MaterialAsset>` referencing an
|
|
226
|
+
* engine shader (e.g. `forgeax::default-standard-pbr`) succeeds without
|
|
227
|
+
* waiting for `renderer.initialization` (plan-strategy D-1/D-2/D-5).
|
|
228
|
+
*
|
|
229
|
+
* Failures (cap-gate insufficient, manifest-malformed, shader-not-found)
|
|
230
|
+
* throw structured `RhiError` / `ShaderError` which propagate through
|
|
231
|
+
* `createRenderer`'s synchronous reject path (D-2 structured reject).
|
|
232
|
+
*/
|
|
233
|
+
export async function prepareMaterialShaders(
|
|
234
|
+
rhiDevice: RhiDevice,
|
|
235
|
+
getShader: () => ShaderCatalog,
|
|
236
|
+
assets: AssetRegistry,
|
|
237
|
+
// feat-20260629 M4: populated with uvSetCount from naga reflection JSON
|
|
238
|
+
// keyed by materialShaderId; consumed by getMaterialShaderPipeline for
|
|
239
|
+
// auto-fill of shaderUvSetCount clamp-to-last alias parameter.
|
|
240
|
+
materialShaderUvSetCounts: Map<string, number>,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const builtinGuid = (result: ReturnType<typeof AssetGuid.parse>): AssetGuidType => {
|
|
243
|
+
if (!result.ok) throw result.error;
|
|
244
|
+
return result.value;
|
|
245
|
+
};
|
|
246
|
+
// feat-20260528-material-shader-registration-unification M3 / w15:
|
|
247
|
+
// pre-computed UUIDv5 GUIDs for engine-shipped material shaders.
|
|
248
|
+
// Derived from FORGEAX_NAMESPACE (9a09805a-7623-482e-b322-9fc3591f2a38)
|
|
249
|
+
// with SHA-1 per RFC 4122 section 4.3.
|
|
250
|
+
const pbrGuidRes = AssetGuid.parse('94d85ce4-650c-54b1-a86a-eaf22696ecbc');
|
|
251
|
+
const unlitGuidRes = AssetGuid.parse('37f593ea-0c79-528c-b7f7-23d17045d776');
|
|
252
|
+
const spriteGuidRes = AssetGuid.parse('658234f6-a605-5fff-957d-7149b48fd0f4');
|
|
253
|
+
// feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7: stable
|
|
254
|
+
// UUID for sprite-lit material shader (matches sprite-lit.material.json
|
|
255
|
+
// subAssets[0].guid; shader-id catalog SSOT).
|
|
256
|
+
const spriteLitGuidRes = AssetGuid.parse('f0ec6a4b-cad1-5a3d-9b4e-6d2b0fa14d8e');
|
|
257
|
+
const pbrSkinGuidRes = AssetGuid.parse('5ad0833e-2f17-56e5-a3d2-dab543afae65');
|
|
258
|
+
// feat-20260531-world-space-msdf-text-rendering M5 / w21: stable UUIDv5 for
|
|
259
|
+
// the world-space MSDF text material shader (deriveBuiltin('forgeax::msdf-text')
|
|
260
|
+
// under FORGEAX_NAMESPACE). Registered via the manifest materialShaders[]
|
|
261
|
+
// loop below alongside sprite / unlit (D-7 -- materialShaderId path).
|
|
262
|
+
const msdfTextGuidRes = AssetGuid.parse('b8f92146-3b24-519b-97a2-271419b53563');
|
|
263
|
+
const shadowCasterGuidRes = AssetGuid.parse('2e167c2a-1747-5bd7-b56d-aea9bc3f436e');
|
|
264
|
+
const pbrGuid = builtinGuid(pbrGuidRes);
|
|
265
|
+
const unlitGuid = builtinGuid(unlitGuidRes);
|
|
266
|
+
const spriteGuid = builtinGuid(spriteGuidRes);
|
|
267
|
+
const spriteLitGuid = builtinGuid(spriteLitGuidRes);
|
|
268
|
+
const pbrSkinGuid = builtinGuid(pbrSkinGuidRes);
|
|
269
|
+
const msdfTextGuid = builtinGuid(msdfTextGuidRes);
|
|
270
|
+
const shadowCasterGuid = builtinGuid(shadowCasterGuidRes);
|
|
271
|
+
const ENGINE_SHADER_GUIDS = new Map<string, AssetGuidType>([
|
|
272
|
+
['forgeax::default-standard-pbr', pbrGuid],
|
|
273
|
+
['forgeax::default-unlit', unlitGuid],
|
|
274
|
+
['forgeax::sprite', spriteGuid],
|
|
275
|
+
['forgeax::sprite-lit', spriteLitGuid],
|
|
276
|
+
['forgeax::default-standard-pbr-skin', pbrSkinGuid],
|
|
277
|
+
['forgeax::msdf-text', msdfTextGuid],
|
|
278
|
+
['forgeax::default-shadow-caster', shadowCasterGuid],
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
// ── Step 0: storage-buffer cap gate ───────────────────────────────────────
|
|
282
|
+
const capValue = (rhiDevice.limits as Readonly<Record<string, number>>)
|
|
283
|
+
.maxStorageBuffersPerShaderStage;
|
|
284
|
+
const webgl2Downlevel = rhiDevice.caps.backendKind === 'wgpu-webgl2';
|
|
285
|
+
let storageBufferCapable = !webgl2Downlevel;
|
|
286
|
+
if (typeof capValue === 'number') {
|
|
287
|
+
const capCheck = assertStorageBufferCap(capValue);
|
|
288
|
+
if (!capCheck.ok) throw capCheck.error;
|
|
289
|
+
storageBufferCapable = !webgl2Downlevel && capCheck.value;
|
|
290
|
+
}
|
|
291
|
+
const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
|
|
292
|
+
const projectorAvailable =
|
|
293
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
294
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
295
|
+
|
|
296
|
+
// ── Step 1: manifest load ─────────────────────────────────────────────────
|
|
297
|
+
const registry = getShader();
|
|
298
|
+
const loaded = await registry.loadManifest();
|
|
299
|
+
if (!loaded.ok) {
|
|
300
|
+
throw loaded.error;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Step 1b: material shader variant resolution + registration ───────────
|
|
304
|
+
// feat-20260609-hdrp-cluster-fragment-ggx M1 / w7: dual-axis variant
|
|
305
|
+
// resolution. At boot time isHdrpActive is always false (URP default),
|
|
306
|
+
// so CLUSTER_FORWARD_AVAILABLE=false variant is selected. HDRP variant
|
|
307
|
+
// (CLUSTER_FORWARD_AVAILABLE=true) is resolved later when
|
|
308
|
+
// clustered Standard profile activation selects the clustered variant.
|
|
309
|
+
const isHdrpActive = false; // M4: wire to actual renderSystem.frameState.isHdrpActive
|
|
310
|
+
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
311
|
+
// bug-20260610: skip synthetic non-material engine entries piggy-backing
|
|
312
|
+
// on the materialShaders channel for variant surfacing (shadow_caster).
|
|
313
|
+
// They use the `forgeax::engine-` prefix and are consumed by Step 2's
|
|
314
|
+
// engine-entry compile path, NOT by installMaterialArtifact.
|
|
315
|
+
if (msEntry.identifier.startsWith('forgeax::engine-')) continue;
|
|
316
|
+
// buildVariantKey logic: sorted key=value pairs joined with +; all-true = ''.
|
|
317
|
+
const variantDefines: Record<string, boolean> = {
|
|
318
|
+
STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
|
|
319
|
+
};
|
|
320
|
+
// User material manifests may declare WEBGL2_COMPAT alongside the shared
|
|
321
|
+
// mesh-storage axis. The fallback backend must select that authored
|
|
322
|
+
// branch at boot; otherwise it asks for the impossible bare
|
|
323
|
+
// `STORAGE_BUFFER_AVAILABLE=false` key and fails before the first frame.
|
|
324
|
+
if (msEntry.variants.some((v) => 'WEBGL2_COMPAT' in v.defines)) {
|
|
325
|
+
variantDefines.WEBGL2_COMPAT = webgl2Downlevel;
|
|
326
|
+
}
|
|
327
|
+
// Only include CLUSTER_FORWARD_AVAILABLE when the material shader declares
|
|
328
|
+
// the axis (has a variant with this key), preserving backward compat for
|
|
329
|
+
// entries without the pragma.
|
|
330
|
+
if (msEntry.variants.some((v) => 'CLUSTER_FORWARD_AVAILABLE' in v.defines)) {
|
|
331
|
+
variantDefines.CLUSTER_FORWARD_AVAILABLE = isHdrpActive;
|
|
332
|
+
}
|
|
333
|
+
if (msEntry.variants.some((v) => 'DIRECTIONAL_PCSS_AVAILABLE' in v.defines)) {
|
|
334
|
+
variantDefines.DIRECTIONAL_PCSS_AVAILABLE = directionalPcssCapable(
|
|
335
|
+
rhiDevice.caps.backendKind,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
|
|
339
|
+
variantDefines.VERTEX_COLOR_AVAILABLE = false;
|
|
340
|
+
}
|
|
341
|
+
if (msEntry.variants.some((v) => 'PROBE_BLEND_AVAILABLE' in v.defines)) {
|
|
342
|
+
variantDefines.PROBE_BLEND_AVAILABLE = false;
|
|
343
|
+
}
|
|
344
|
+
if (msEntry.variants.some((v) => 'EXTENDED_LIGHTING_AVAILABLE' in v.defines)) {
|
|
345
|
+
variantDefines.EXTENDED_LIGHTING_AVAILABLE = extendedLightingShaderAvailable;
|
|
346
|
+
}
|
|
347
|
+
if (
|
|
348
|
+
msEntry.identifier === 'forgeax::default-standard-pbr' &&
|
|
349
|
+
msEntry.variants.some((v) => 'PROJECTOR_AVAILABLE' in v.defines)
|
|
350
|
+
) {
|
|
351
|
+
variantDefines.PROJECTOR_AVAILABLE = projectorAvailable;
|
|
352
|
+
}
|
|
353
|
+
if (msEntry.variants.some((v) => 'TRANSMISSION_AVAILABLE' in v.defines)) {
|
|
354
|
+
variantDefines.TRANSMISSION_AVAILABLE = false;
|
|
355
|
+
}
|
|
356
|
+
// Record-time fallback MRT is opt-in; boot the ordinary single-target
|
|
357
|
+
// Standard artifact even when the manifest declares this axis.
|
|
358
|
+
if (
|
|
359
|
+
msEntry.identifier === 'forgeax::default-standard-pbr' &&
|
|
360
|
+
msEntry.variants.some((v) => 'REFLECTION_FALLBACK_AVAILABLE' in v.defines)
|
|
361
|
+
) {
|
|
362
|
+
variantDefines.REFLECTION_FALLBACK_AVAILABLE = false;
|
|
363
|
+
}
|
|
364
|
+
// bug-20260708 M2 (a): PER_INSTANCE_REGION axis default = false at boot.
|
|
365
|
+
// Per-entity spritePH path (`main-pass-sprite-draws.ts:511-513`) expects
|
|
366
|
+
// `variantSet=undefined` to resolve to the PIR=false shader source (64B
|
|
367
|
+
// InstanceData) via `lookup.value.source` fallback in
|
|
368
|
+
// `getMaterialShaderPipeline`. The PIR=true canonical variant (80B) is
|
|
369
|
+
// reserved for SpriteInstances batches which explicitly request it via
|
|
370
|
+
// `SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET === ''` (canonical all-
|
|
371
|
+
// true) — resolved through `findVariantByKey(msEntry, '')` in the same
|
|
372
|
+
// `getMaterialShaderPipeline` substitution branch. Mirrors the
|
|
373
|
+
// CLUSTER_FORWARD_AVAILABLE pattern above: axis default false unless
|
|
374
|
+
// explicitly requested by a caller variantSet.
|
|
375
|
+
if (msEntry.variants.some((v) => 'PER_INSTANCE_REGION' in v.defines)) {
|
|
376
|
+
variantDefines.PER_INSTANCE_REGION = false;
|
|
377
|
+
}
|
|
378
|
+
if (msEntry.variants.some((v) => 'SKINNING_DISABLED' in v.defines)) {
|
|
379
|
+
variantDefines.SKINNING_DISABLED = true;
|
|
380
|
+
}
|
|
381
|
+
const sortedEntries = Object.entries(variantDefines).sort(([a], [b]) =>
|
|
382
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
383
|
+
);
|
|
384
|
+
const definesKey = sortedEntries.every(([, v]) => v === true)
|
|
385
|
+
? ''
|
|
386
|
+
: sortedEntries.map(([k, v]) => `${k}=${v}`).join('+');
|
|
387
|
+
const chosen = findVariantByKey(msEntry, definesKey);
|
|
388
|
+
if (msEntry.variants.length > 0 && chosen === undefined) {
|
|
389
|
+
throw new RhiError({
|
|
390
|
+
code: 'shader-compile-failed',
|
|
391
|
+
expected: `material shader '${msEntry.identifier}' declares the exact boot variant '${definesKey}'`,
|
|
392
|
+
hint: 'regenerate the shader manifest with the complete variant Cartesian product',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const wgsl = chosen?.composedWgsl ?? msEntry.composedWgsl;
|
|
396
|
+
if (wgsl.length > 0) {
|
|
397
|
+
const existing = registry.findMaterialArtifact(msEntry.identifier);
|
|
398
|
+
if (existing.ok) continue;
|
|
399
|
+
const paramSchema = JSON.parse(
|
|
400
|
+
msEntry.paramSchema,
|
|
401
|
+
) as readonly import('@forgeax/engine-types').ParamSchemaEntry[];
|
|
402
|
+
// feat-20260613-material-paramschema-driven-binding M3 / w12-w13:
|
|
403
|
+
// paramSchema is the SSOT; the BGL is derived on demand via
|
|
404
|
+
// `derive(paramSchema).bglEntries` and consumed by
|
|
405
|
+
// buildPbrPipelineLayouts at pipeline-build time. The historical
|
|
406
|
+
// separate bind-layout sidecar field has been deleted from
|
|
407
|
+
// MaterialShaderEntry / MaterialRuntimeInfo (D-1 / D-2).
|
|
408
|
+
registry.installMaterialArtifact(msEntry.identifier, {
|
|
409
|
+
source: wgsl,
|
|
410
|
+
paramSchema,
|
|
411
|
+
});
|
|
412
|
+
// feat-20260629 M4: store uvSetCount from naga reflection for
|
|
413
|
+
// auto-fill in getMaterialShaderPipeline clamp-to-last alias.
|
|
414
|
+
if (msEntry.uvSetCount !== undefined) {
|
|
415
|
+
materialShaderUvSetCounts.set(msEntry.identifier, msEntry.uvSetCount);
|
|
416
|
+
}
|
|
417
|
+
// Sanity check the schema parses through derive — a malformed sidecar
|
|
418
|
+
// schema (e.g. unknown type literal) should fail loud at register time
|
|
419
|
+
// (charter P3 explicit failure). The derived output is discarded; the
|
|
420
|
+
// SSOT consumer is buildPbrPipelineLayouts later.
|
|
421
|
+
derive(paramSchema);
|
|
422
|
+
|
|
423
|
+
const shaderGuid = ENGINE_SHADER_GUIDS.get(msEntry.identifier);
|
|
424
|
+
if (shaderGuid !== undefined) {
|
|
425
|
+
const shaderAsset = {
|
|
426
|
+
kind: 'shader' as const,
|
|
427
|
+
name: msEntry.identifier,
|
|
428
|
+
source: wgsl,
|
|
429
|
+
paramSchema,
|
|
430
|
+
};
|
|
431
|
+
// feat-20260614 M8 (D-17): catalogue the shader asset by GUID so it is
|
|
432
|
+
// GUID-addressable; no handle minted. A shader without an engine GUID
|
|
433
|
+
// lives only in the ShaderCatalog (the installMaterialArtifact SSOT
|
|
434
|
+
// above) -- there is no GUID to catalogue it under.
|
|
435
|
+
assets.catalog(shaderGuid, shaderAsset);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Step 1c: register forgeax::default-shadow-caster from its manifest identity ──
|
|
441
|
+
// feat-20260609-pipeline-driven-pass-selector-shadowcaster-via-mat M3 / T-007:
|
|
442
|
+
// The reserved material-shader identifier is the SSOT. Content heuristics are
|
|
443
|
+
// invalid here: a game-authored vertex-only shader can legitimately expose
|
|
444
|
+
// position without normal (billboards and procedural VFX commonly do), and
|
|
445
|
+
// must never be mistaken for the engine shadow caster merely because it is
|
|
446
|
+
// encountered first in the flat manifest.
|
|
447
|
+
const shadowCasterIdentifier = 'forgeax::default-shadow-caster';
|
|
448
|
+
if (!registry.findMaterialArtifact(shadowCasterIdentifier).ok) {
|
|
449
|
+
const manifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
|
|
450
|
+
(candidate) => candidate.identifier === shadowCasterIdentifier,
|
|
451
|
+
);
|
|
452
|
+
if (manifestEntry !== undefined) {
|
|
453
|
+
// shadow_caster has no authored material parameters. M3 / w12-w13
|
|
454
|
+
// (D-2 / D-12): derive([]) is the graceful empty-schema path.
|
|
455
|
+
registry.installMaterialArtifact(shadowCasterIdentifier, {
|
|
456
|
+
source: manifestEntry.composedWgsl,
|
|
457
|
+
paramSchema: [],
|
|
458
|
+
});
|
|
459
|
+
const shaderGuid = ENGINE_SHADER_GUIDS.get(shadowCasterIdentifier);
|
|
460
|
+
if (shaderGuid !== undefined) {
|
|
461
|
+
const shaderAsset = {
|
|
462
|
+
kind: 'shader' as const,
|
|
463
|
+
name: shadowCasterIdentifier,
|
|
464
|
+
source: manifestEntry.composedWgsl,
|
|
465
|
+
paramSchema: [] as readonly import('@forgeax/engine-types').ParamSchemaEntry[],
|
|
466
|
+
};
|
|
467
|
+
// feat-20260614 M8 (D-17): catalogue by GUID; no handle minted.
|
|
468
|
+
assets.catalog(shaderGuid, shaderAsset);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Per-variant PipelineLayout selector (M4.5 / D-10 option A).
|
|
476
|
+
*
|
|
477
|
+
* The HDRP variant (`CLUSTER_FORWARD_AVAILABLE=true` or canonical `''`
|
|
478
|
+
* all-true when the shader declares the cluster axis) needs the 7-slot
|
|
479
|
+
* group(2) BGL chain (`hdrpPbrPipelineLayout`); URP variants
|
|
480
|
+
* (`CLUSTER_FORWARD_AVAILABLE=false` / undefined / no axis) keep the
|
|
481
|
+
* 1-slot mesh-array BGL chain (`pbrPipelineLayout`).
|
|
482
|
+
*
|
|
483
|
+
* Returns `null` only when the URP fallback layout itself is null
|
|
484
|
+
* (Camera-only / empty-manifest path). When the HDRP layout is null but
|
|
485
|
+
* URP exists, the HDRP variant gracefully falls back to URP — the WGSL
|
|
486
|
+
* resolved upstream by `findVariantByKey` is the URP variant in that
|
|
487
|
+
* case (manifest entry with all-true definesKey === '' was registered
|
|
488
|
+
* with isHdrpActive=false at boot when storage-buffer caps are absent).
|
|
489
|
+
*
|
|
490
|
+
* Pure function — exported for unit-test access (M4.5 / w35).
|
|
491
|
+
*/
|
|
492
|
+
/**
|
|
493
|
+
* Closed enum of pipeline-layout kinds the selector can dispatch to.
|
|
494
|
+
*
|
|
495
|
+
* `pbr` — standard PBR (1-entry mesh-array BGL).
|
|
496
|
+
* `pbr-skin` — bug-20260611-skin-pipeline-layout: 2-entry mesh-array BGL
|
|
497
|
+
* (binding 0 meshes + binding 1 palette) for the `forgeax::pbr-skin`
|
|
498
|
+
* material shader.
|
|
499
|
+
* `hdrp-pbr` — feat-20260609 HDRP cluster-forward variant (7-slot group(2)
|
|
500
|
+
* BGL substituted at slot 2).
|
|
501
|
+
* `sprite-urp` — bug-20260708 M2 (c): sprite / sprite-lit shaders reuse
|
|
502
|
+
* the URP 1-slot mesh-array BGL (they don't declare
|
|
503
|
+
* CLUSTER_FORWARD_AVAILABLE) but their canonical all-true variant key
|
|
504
|
+
* `''` (`SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET`) MUST NOT trigger
|
|
505
|
+
* the `variantSet===''` → HDRP branch of the selector. Callers
|
|
506
|
+
* (`buildPipelineContext`) resolve `sprite-urp` from the sprite
|
|
507
|
+
* `materialShaderId` and pass it through; the selector short-circuits
|
|
508
|
+
* to `pbrPipelineLayout` before the HDRP check.
|
|
509
|
+
*
|
|
510
|
+
* AC-09 grep gate: the selector body (`selectPipelineLayoutForVariant`)
|
|
511
|
+
* **MUST NOT** contain literal `'forgeax::pbr-skin'` (or another
|
|
512
|
+
* shader-id literal). The caller resolves `LayoutKind` upstream and passes
|
|
513
|
+
* the structured value through (charter P4 consistent abstraction). New
|
|
514
|
+
* material-shader layouts add a `LayoutKind` member + a PipelineState slot
|
|
515
|
+
* + a switch arm here in one cut (charter P3 extensibility).
|
|
516
|
+
*/
|
|
517
|
+
export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'hdrp-skin' | 'sprite-urp' | 'unlit-urp';
|
|
518
|
+
|
|
519
|
+
function directionalPcssCapable(backendKind: RhiDevice['caps']['backendKind']): boolean {
|
|
520
|
+
return backendKind === 'webgpu' || backendKind === 'wgpu-native';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function isSharedMaterialUserRegionCompatible(
|
|
524
|
+
paramSchema: readonly ParamSchemaEntry[],
|
|
525
|
+
): boolean {
|
|
526
|
+
const withoutPhysicalMaps = (
|
|
527
|
+
schema: readonly ParamSchemaEntry[],
|
|
528
|
+
): readonly ParamSchemaEntry[] => {
|
|
529
|
+
const excluded = new Set<string>(standardPhysicalTextureFields(schema));
|
|
530
|
+
return schema.filter((entry) => !excluded.has(entry.name) || !entry.type.startsWith('texture'));
|
|
531
|
+
};
|
|
532
|
+
const sharedSchema = withoutPhysicalMaps(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
533
|
+
const candidateSchema = withoutPhysicalMaps(paramSchema);
|
|
534
|
+
const shared = buildPbrMaterialUserRegionEntries(sharedSchema);
|
|
535
|
+
const candidate = buildPbrMaterialUserRegionEntries(candidateSchema);
|
|
536
|
+
const sharedTextureNames = [...derive(sharedSchema).textureFieldNames];
|
|
537
|
+
const candidateTextureNames = [...derive(candidateSchema).textureFieldNames];
|
|
538
|
+
if (
|
|
539
|
+
candidateTextureNames.length !== sharedTextureNames.length ||
|
|
540
|
+
candidateTextureNames.some((name, index) => name !== sharedTextureNames[index])
|
|
541
|
+
) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
if (candidate.length > shared.length) return false;
|
|
545
|
+
return candidate.every((entry, index) => {
|
|
546
|
+
const canonical = shared[index];
|
|
547
|
+
if (canonical === undefined) return false;
|
|
548
|
+
return (
|
|
549
|
+
JSON.stringify({
|
|
550
|
+
binding: entry.binding,
|
|
551
|
+
visibility: entry.visibility,
|
|
552
|
+
buffer: entry.buffer ?? null,
|
|
553
|
+
sampler: entry.sampler ?? null,
|
|
554
|
+
texture: entry.texture ?? null,
|
|
555
|
+
storageTexture: entry.storageTexture ?? null,
|
|
556
|
+
}) ===
|
|
557
|
+
JSON.stringify({
|
|
558
|
+
binding: canonical.binding,
|
|
559
|
+
visibility: canonical.visibility,
|
|
560
|
+
buffer: canonical.buffer ?? null,
|
|
561
|
+
sampler: canonical.sampler ?? null,
|
|
562
|
+
texture: canonical.texture ?? null,
|
|
563
|
+
storageTexture: canonical.storageTexture ?? null,
|
|
564
|
+
})
|
|
565
|
+
);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Select the PBR variant that matches a fallback mesh with no COLOR_0 input.
|
|
571
|
+
* Unknown/custom material IDs may still use the legacy PBR fallback, so that
|
|
572
|
+
* path must obey the same geometry-owned vertex-color axis as registered PBR.
|
|
573
|
+
*/
|
|
574
|
+
export function selectNoColorPbrVariant(
|
|
575
|
+
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
576
|
+
storageBufferCapable: boolean,
|
|
577
|
+
variantSet: string | undefined,
|
|
578
|
+
extendedLightingShaderAvailable = true,
|
|
579
|
+
directionalPcssAvailable = true,
|
|
580
|
+
projectorAvailable: boolean = true,
|
|
581
|
+
): MaterialShaderManifestEntry['variants'][number] | undefined {
|
|
582
|
+
const hdrpRequested = variantSet === '' || requestsClusterVariant(variantSet);
|
|
583
|
+
const probeBlendRequested = requestsProbeBlendVariant(variantSet);
|
|
584
|
+
return manifestEntry?.variants.find(
|
|
585
|
+
(variant) =>
|
|
586
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
587
|
+
(!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
|
|
588
|
+
variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
|
|
589
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
590
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
591
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
592
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
593
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
594
|
+
(!('PROBE_BLEND_AVAILABLE' in variant.defines) ||
|
|
595
|
+
variant.defines.PROBE_BLEND_AVAILABLE === probeBlendRequested) &&
|
|
596
|
+
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
597
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === hdrpRequested),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function requestsClusterVariant(variantSet: string | undefined): boolean {
|
|
602
|
+
return variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') === true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function requestsProbeBlendVariant(variantSet: string | undefined): boolean {
|
|
606
|
+
return variantSet?.includes('PROBE_BLEND_AVAILABLE=true') === true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
|
|
610
|
+
export type MaterialShaderVertexInputContract = 'none' | 'render-material';
|
|
611
|
+
|
|
612
|
+
export function resolveMaterialShaderBindingContract(
|
|
613
|
+
source: string,
|
|
614
|
+
): MaterialShaderBindingContract {
|
|
615
|
+
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//gu, '').replace(/\/\/.*$/gmu, '');
|
|
616
|
+
const groups = [...withoutComments.matchAll(/@group\s*\(\s*(\d+)\s*\)/gu)].map((match) =>
|
|
617
|
+
Number(match[1]),
|
|
618
|
+
);
|
|
619
|
+
if (groups.length === 0) return 'group-0';
|
|
620
|
+
const declaresView =
|
|
621
|
+
/@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*0\s*\)\s*var\s*<\s*uniform\s*>\s*view(?:X_naga_oil_mod_[A-Z0-9]+)?\b/u.test(
|
|
622
|
+
withoutComments,
|
|
623
|
+
);
|
|
624
|
+
const declaresSceneDepth =
|
|
625
|
+
/@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*1\s*\)\s*var\s+scene_depth\s*:\s*texture_depth_2d\b/u.test(
|
|
626
|
+
withoutComments,
|
|
627
|
+
);
|
|
628
|
+
if (declaresView && declaresSceneDepth && groups.every((group) => group === 0)) {
|
|
629
|
+
return 'view-and-scene-depth';
|
|
630
|
+
}
|
|
631
|
+
if (declaresView && groups.every((group) => group === 0)) return 'view-only';
|
|
632
|
+
return groups.every((group) => group === 0) ? 'group-0-resource' : 'render-material';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function resolveMaterialShaderVertexInputContract(
|
|
636
|
+
source: string,
|
|
637
|
+
): MaterialShaderVertexInputContract {
|
|
638
|
+
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//gu, '').replace(/\/\/.*$/gmu, '');
|
|
639
|
+
const inputStructs = [...withoutComments.matchAll(/struct\s+([A-Za-z_]\w*)\s*\{([^}]*)\}/gu)]
|
|
640
|
+
.filter((match) => /@location\s*\(/u.test(match[2] ?? ''))
|
|
641
|
+
.map((match) => match[1]);
|
|
642
|
+
// WGSL vertex inputs are commonly declared in a named struct before the
|
|
643
|
+
// `@vertex` function (the engine uses `VsIn`, while package shaders often
|
|
644
|
+
// use `VertexInput`). Match the type actually passed as the first vertex
|
|
645
|
+
// parameter so fragment/output structs do not accidentally enable a
|
|
646
|
+
// vertex buffer layout for fullscreen shaders.
|
|
647
|
+
for (const inputStruct of inputStructs) {
|
|
648
|
+
if (inputStruct === undefined) continue;
|
|
649
|
+
const firstParameter = new RegExp(
|
|
650
|
+
`@vertex\\s*fn\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${inputStruct}\\b`,
|
|
651
|
+
'u',
|
|
652
|
+
);
|
|
653
|
+
if (firstParameter.test(withoutComments)) return 'render-material';
|
|
654
|
+
}
|
|
655
|
+
// A shader may declare a location directly on a vertex parameter rather
|
|
656
|
+
// than through a struct. Keep that contract explicit as well.
|
|
657
|
+
return /@vertex\s*fn[^{]*\([^)]*@location\s*\(/u.test(withoutComments)
|
|
658
|
+
? 'render-material'
|
|
659
|
+
: 'none';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function allowsUnlitPreparedFallback(
|
|
663
|
+
depthFormatOverride: GPUTextureFormat | null | undefined,
|
|
664
|
+
vertexLayout: string | undefined,
|
|
665
|
+
materialShaderId?: string,
|
|
666
|
+
): boolean {
|
|
667
|
+
return (
|
|
668
|
+
depthFormatOverride === null &&
|
|
669
|
+
(vertexLayout === undefined || !materialShaderId?.startsWith('forgeax::'))
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Prepared VFX draws own their vertex inputs and bind groups. A missing
|
|
675
|
+
* authored material shader is therefore retryable, but it is never safe to
|
|
676
|
+
* substitute the built-in PBR module: PBR declares mesh/instance groups that
|
|
677
|
+
* the VFX draw does not bind. Returning true keeps the feature on its
|
|
678
|
+
* structured next-frame recovery path instead of creating an invalid PSO.
|
|
679
|
+
*/
|
|
680
|
+
export function shouldDeferMissingPreparedMaterialShader(
|
|
681
|
+
vertexLayout: string | undefined,
|
|
682
|
+
): boolean {
|
|
683
|
+
return (
|
|
684
|
+
vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance ||
|
|
685
|
+
vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance ||
|
|
686
|
+
vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function selectPipelineLayoutForVariant(
|
|
691
|
+
state: {
|
|
692
|
+
readonly pbrPipelineLayout: PipelineLayout | null;
|
|
693
|
+
readonly pbrProbePipelineLayout?: PipelineLayout | null;
|
|
694
|
+
readonly hdrpPbrPipelineLayout: PipelineLayout | null;
|
|
695
|
+
readonly hdrpProbePbrPipelineLayout?: PipelineLayout | null;
|
|
696
|
+
readonly pbrSkinPipelineLayout: PipelineLayout | null;
|
|
697
|
+
readonly pbrSkinProbePipelineLayout?: PipelineLayout | null;
|
|
698
|
+
readonly hdrpSkinPipelineLayout?: PipelineLayout | null;
|
|
699
|
+
} | null,
|
|
700
|
+
variantSet: string | undefined,
|
|
701
|
+
layoutKind?: LayoutKind,
|
|
702
|
+
): PipelineLayout | null {
|
|
703
|
+
if (state === null) return null;
|
|
704
|
+
const probeBlendVariant = variantSet?.includes('PROBE_BLEND_AVAILABLE=true') === true;
|
|
705
|
+
// bug-20260611: skin layout selection takes precedence over HDRP variant
|
|
706
|
+
// resolution. HDRP × skin is OOS-1 (plan-strategy R-2 — left for a
|
|
707
|
+
// dedicated feat); when an HDRP-variant skin call ever lands, we fail
|
|
708
|
+
// fast with `null` rather than silently returning the URP layout
|
|
709
|
+
// (charter P3 explicit failure, mirrors memory anchor
|
|
710
|
+
// `hdrp-active-must-not-fallback-to-urp-pipeline`).
|
|
711
|
+
if (layoutKind === 'pbr-skin') {
|
|
712
|
+
return probeBlendVariant
|
|
713
|
+
? (state.pbrSkinProbePipelineLayout ?? null)
|
|
714
|
+
: state.pbrSkinPipelineLayout;
|
|
715
|
+
}
|
|
716
|
+
if (layoutKind === 'hdrp-skin') {
|
|
717
|
+
return state.hdrpSkinPipelineLayout ?? null;
|
|
718
|
+
}
|
|
719
|
+
// bug-20260708 M2 (c): sprite / sprite-lit shaders reuse the URP 1-slot
|
|
720
|
+
// mesh-array pipeline layout but their canonical all-true variant key
|
|
721
|
+
// `''` (SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET) must NOT trigger
|
|
722
|
+
// the `variantSet===''` → HDRP branch below. Caller-side
|
|
723
|
+
// (`buildPipelineContext`) resolves LayoutKind='sprite-urp' from the
|
|
724
|
+
// sprite `materialShaderId`; the selector short-circuits here before
|
|
725
|
+
// the HDRP variant check reads `variantSet`. Prevents R-1'
|
|
726
|
+
// device-lost when a SpriteInstances batch requests the PIR=true
|
|
727
|
+
// canonical variant.
|
|
728
|
+
if (layoutKind === 'sprite-urp') {
|
|
729
|
+
return state.pbrPipelineLayout;
|
|
730
|
+
}
|
|
731
|
+
if (layoutKind === 'unlit-urp') {
|
|
732
|
+
return state.pbrPipelineLayout;
|
|
733
|
+
}
|
|
734
|
+
// Static PBR callers can use the canonical all-true variant key (`''`)
|
|
735
|
+
// without becoming HDRP. Keep the shader variant identity independent from
|
|
736
|
+
// the pipeline-layout identity; shadow-caster static draws use this explicit
|
|
737
|
+
// kind so their ordinary mesh bind group remains compatible with the PSO.
|
|
738
|
+
if (layoutKind === 'pbr') {
|
|
739
|
+
return probeBlendVariant ? (state.pbrProbePipelineLayout ?? null) : state.pbrPipelineLayout;
|
|
740
|
+
}
|
|
741
|
+
// HDRP variant matches when:
|
|
742
|
+
// - layoutKind === 'hdrp-pbr' (caller-driven, future-proof), OR
|
|
743
|
+
// - variantSet is the canonical all-true key '' (boot-time HDRP
|
|
744
|
+
// registration produces '' when both axes resolve to true), OR
|
|
745
|
+
// - variantSet contains `CLUSTER_FORWARD_AVAILABLE=true` substring
|
|
746
|
+
// (record-stage caller emits the expanded form).
|
|
747
|
+
// URP variants have CLUSTER_FORWARD_AVAILABLE=false explicitly, or omit
|
|
748
|
+
// the axis altogether, or pass undefined (no variant routing).
|
|
749
|
+
// G-14 (D-11): empty-string is the canonical all-true HDRP key, so we
|
|
750
|
+
// explicitly distinguish '' vs undefined rather than using an optional-chain
|
|
751
|
+
// (charter P3: explicit failure / no implicit empty-vs-missing collapse).
|
|
752
|
+
const isHdrpVariant = layoutKind === 'hdrp-pbr';
|
|
753
|
+
if (isHdrpVariant) {
|
|
754
|
+
return probeBlendVariant
|
|
755
|
+
? (state.hdrpProbePbrPipelineLayout ?? null)
|
|
756
|
+
: state.hdrpPbrPipelineLayout;
|
|
757
|
+
}
|
|
758
|
+
return probeBlendVariant ? (state.pbrProbePipelineLayout ?? null) : state.pbrPipelineLayout;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Resolve a requested material variant against the actual backend capabilities.
|
|
763
|
+
* Record callers use the native-capability request; the backend-owned axes
|
|
764
|
+
* must be rewritten before the request reaches shader lookup and PSO layout
|
|
765
|
+
* selection.
|
|
766
|
+
*/
|
|
767
|
+
export function resolveMaterialShaderVariantSet(
|
|
768
|
+
requestedVariantSet: string | undefined,
|
|
769
|
+
variants: ReadonlyArray<{
|
|
770
|
+
readonly defines: Readonly<Record<string, boolean>>;
|
|
771
|
+
}>,
|
|
772
|
+
backendKind: RhiDevice['caps']['backendKind'],
|
|
773
|
+
storageBuffer: boolean,
|
|
774
|
+
maxSampledTexturesPerShaderStageOrProjectorAvailable?: number | boolean,
|
|
775
|
+
): string | undefined {
|
|
776
|
+
const declaredAxes = new Set(variants.flatMap((variant) => Object.keys(variant.defines)));
|
|
777
|
+
const declaresCapabilityVariants =
|
|
778
|
+
declaredAxes.has('WEBGL2_COMPAT') ||
|
|
779
|
+
declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
|
|
780
|
+
declaredAxes.has('EXTENDED_LIGHTING_AVAILABLE') ||
|
|
781
|
+
declaredAxes.has('DIRECTIONAL_PCSS_AVAILABLE') ||
|
|
782
|
+
declaredAxes.has('PROJECTOR_AVAILABLE');
|
|
783
|
+
if (!declaresCapabilityVariants) return requestedVariantSet;
|
|
784
|
+
|
|
785
|
+
const requested: Record<string, boolean> = {};
|
|
786
|
+
for (const part of requestedVariantSet?.split('+') ?? []) {
|
|
787
|
+
const separator = part.indexOf('=');
|
|
788
|
+
if (separator > 0) requested[part.slice(0, separator)] = part.slice(separator + 1) === 'true';
|
|
789
|
+
}
|
|
790
|
+
const sampledTextureLimit =
|
|
791
|
+
typeof maxSampledTexturesPerShaderStageOrProjectorAvailable === 'number'
|
|
792
|
+
? maxSampledTexturesPerShaderStageOrProjectorAvailable
|
|
793
|
+
: undefined;
|
|
794
|
+
const extendedLightingAvailable =
|
|
795
|
+
sampledTextureLimit === undefined
|
|
796
|
+
? true
|
|
797
|
+
: backendKind !== 'wgpu-webgl2' &&
|
|
798
|
+
storageBuffer &&
|
|
799
|
+
extendedLightingSampledTextureCapacityAvailable(sampledTextureLimit);
|
|
800
|
+
const projectorAvailable =
|
|
801
|
+
typeof maxSampledTexturesPerShaderStageOrProjectorAvailable === 'boolean'
|
|
802
|
+
? maxSampledTexturesPerShaderStageOrProjectorAvailable
|
|
803
|
+
: sampledTextureLimit === undefined
|
|
804
|
+
? true
|
|
805
|
+
: sampledTextureLimit >= STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
806
|
+
const filtered: Record<string, boolean> = {};
|
|
807
|
+
for (const axis of declaredAxes) {
|
|
808
|
+
filtered[axis] =
|
|
809
|
+
axis === 'WEBGL2_COMPAT'
|
|
810
|
+
? backendKind === 'wgpu-webgl2'
|
|
811
|
+
: axis === 'STORAGE_BUFFER_AVAILABLE'
|
|
812
|
+
? backendKind !== 'wgpu-webgl2' && storageBuffer
|
|
813
|
+
: axis === 'SKINNING_DISABLED'
|
|
814
|
+
? (requested[axis] ?? true)
|
|
815
|
+
: axis === 'CLUSTER_FORWARD_AVAILABLE'
|
|
816
|
+
? storageBuffer && backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
|
|
817
|
+
: axis === 'PER_INSTANCE_REGION'
|
|
818
|
+
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
819
|
+
: axis === 'VERTEX_COLOR_AVAILABLE'
|
|
820
|
+
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
821
|
+
: axis === 'EXTENDED_LIGHTING_AVAILABLE'
|
|
822
|
+
? extendedLightingAvailable
|
|
823
|
+
: axis === 'PROBE_BLEND_AVAILABLE'
|
|
824
|
+
? storageBuffer && (requested[axis] ?? false)
|
|
825
|
+
: axis === 'TRANSMISSION_AVAILABLE'
|
|
826
|
+
? (requested[axis] ?? false)
|
|
827
|
+
: axis === 'DIRECTIONAL_PCSS_AVAILABLE'
|
|
828
|
+
? directionalPcssCapable(backendKind)
|
|
829
|
+
: axis === 'PROJECTOR_AVAILABLE'
|
|
830
|
+
? projectorAvailable
|
|
831
|
+
: (requested[axis] ?? false);
|
|
832
|
+
}
|
|
833
|
+
const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
834
|
+
return sorted.every(([, value]) => value)
|
|
835
|
+
? requestedVariantSet === undefined
|
|
836
|
+
? undefined
|
|
837
|
+
: ''
|
|
838
|
+
: sorted.map(([name, value]) => `${name}=${value}`).join('+');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Keep a single-source material on its canonical module identity. A manifest
|
|
843
|
+
* entry with no variants cannot consume a synthesized capability or geometry
|
|
844
|
+
* axis, so carrying one into the pipeline cache only creates an uncached lazy
|
|
845
|
+
* compile beside the prewarmed source module.
|
|
846
|
+
*/
|
|
847
|
+
export function normalizeMaterialShaderVariantSet(
|
|
848
|
+
requestedVariantSet: string | undefined,
|
|
849
|
+
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
850
|
+
): string | undefined {
|
|
851
|
+
return manifestEntry !== undefined && manifestEntry.variants.length === 0
|
|
852
|
+
? undefined
|
|
853
|
+
: requestedVariantSet;
|
|
854
|
+
}
|