@forgeax/engine-render 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +100 -1
  2. package/dist/assembly/factory.d.ts +1 -1
  3. package/dist/assembly/factory.d.ts.map +1 -1
  4. package/dist/assembly/material/surface-projection.d.ts +24 -0
  5. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  6. package/dist/assembly/material-shader-policy.d.ts +20 -0
  7. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  8. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  9. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  10. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  12. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  13. package/dist/assets/asset-decoders.d.ts.map +1 -1
  14. package/dist/authoring.mjs +1 -1
  15. package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
  16. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  17. package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
  18. package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
  19. package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
  20. package/dist/chunk-HKXTW355.mjs.map +1 -0
  21. package/dist/{chunk-YKB5DQV7.mjs → chunk-JSEDTZYS.mjs} +87 -19
  22. package/dist/chunk-JSEDTZYS.mjs.map +1 -0
  23. package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
  24. package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
  25. package/dist/{chunk-Z3YYH2MB.mjs → chunk-N3RT2EUR.mjs} +94 -41
  26. package/dist/chunk-N3RT2EUR.mjs.map +1 -0
  27. package/dist/{chunk-YRUYGA4A.mjs → chunk-TCLDX7SZ.mjs} +559 -138
  28. package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
  29. package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  30. package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
  31. package/dist/components/instances.d.ts +13 -31
  32. package/dist/components/instances.d.ts.map +1 -1
  33. package/dist/construct-renderer.mjs +865 -142
  34. package/dist/construct-renderer.mjs.map +1 -1
  35. package/dist/gpu-driven/production-raster.d.ts +6 -1
  36. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  37. package/dist/gpu-scene.d.ts +4 -0
  38. package/dist/gpu-scene.d.ts.map +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.mjs +27 -9
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/instance-buffer-cache.d.ts +7 -1
  44. package/dist/instance-buffer-cache.d.ts.map +1 -1
  45. package/dist/instances.d.ts +101 -0
  46. package/dist/instances.d.ts.map +1 -0
  47. package/dist/internal.mjs +5 -5
  48. package/dist/materials.d.ts +63 -2
  49. package/dist/materials.d.ts.map +1 -1
  50. package/dist/pipeline-builder.d.ts.map +1 -1
  51. package/dist/pipeline-spec.d.ts.map +1 -1
  52. package/dist/record/frame-snapshot.d.ts +13 -2
  53. package/dist/record/frame-snapshot.d.ts.map +1 -1
  54. package/dist/record/frame.d.ts.map +1 -1
  55. package/dist/record/main-pass-geometry.d.ts +22 -3
  56. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  57. package/dist/record/render-context.d.ts +2 -1
  58. package/dist/record/render-context.d.ts.map +1 -1
  59. package/dist/record/shadow-pass.d.ts +11 -1
  60. package/dist/record/shadow-pass.d.ts.map +1 -1
  61. package/dist/recovery/render-system-candidate.d.ts +1 -1
  62. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  63. package/dist/render-contract.d.ts +4 -0
  64. package/dist/render-contract.d.ts.map +1 -1
  65. package/dist/render-system-extract-tail.d.ts +4 -0
  66. package/dist/render-system-extract-tail.d.ts.map +1 -1
  67. package/dist/render-system-extract.d.ts +25 -1
  68. package/dist/render-system-extract.d.ts.map +1 -1
  69. package/dist/render-system.d.ts +4 -1
  70. package/dist/render-system.d.ts.map +1 -1
  71. package/dist/scene/render-scene-types.d.ts +2 -0
  72. package/dist/scene/render-scene-types.d.ts.map +1 -1
  73. package/dist/scene/render-scene.d.ts +12 -1
  74. package/dist/scene/render-scene.d.ts.map +1 -1
  75. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  76. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  77. package/dist/temporal/index.mjs +5 -5
  78. package/package.json +20 -20
  79. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  80. package/src/__tests__/instances-store.unit.test.ts +46 -0
  81. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  82. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -13
  83. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  84. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  85. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  86. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  87. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
  88. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -0
  89. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  90. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  91. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  92. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  93. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  94. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  95. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  96. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  97. package/src/assembly/factory.ts +2 -0
  98. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  99. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  100. package/src/assembly/material/surface-projection.ts +90 -0
  101. package/src/assembly/material-shader-policy.ts +88 -10
  102. package/src/assembly/recovery/renderer-recover.ts +1 -1
  103. package/src/assembly/webgpu-ready.ts +3 -16
  104. package/src/assembly/webgpu-renderer.ts +90 -158
  105. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  106. package/src/assets/asset-decoders.ts +27 -4
  107. package/src/components/instances.ts +14 -197
  108. package/src/gpu-driven/production-raster.ts +34 -1
  109. package/src/gpu-scene.ts +84 -29
  110. package/src/index.ts +1 -0
  111. package/src/instance-buffer-cache.ts +14 -1
  112. package/src/instances.ts +288 -0
  113. package/src/materials.ts +107 -12
  114. package/src/pipeline-builder.ts +14 -10
  115. package/src/pipeline-spec.ts +6 -14
  116. package/src/record/frame-snapshot.ts +19 -2
  117. package/src/record/frame.ts +31 -2
  118. package/src/record/main-pass-geometry.ts +498 -59
  119. package/src/record/main-pass-sprite-draws.ts +1 -1
  120. package/src/record/render-context.ts +4 -0
  121. package/src/record/shadow-pass.ts +46 -99
  122. package/src/recovery/render-system-candidate.ts +41 -4
  123. package/src/render-contract.ts +4 -0
  124. package/src/render-system-extract-tail.ts +107 -51
  125. package/src/render-system-extract.ts +53 -26
  126. package/src/render-system.ts +40 -5
  127. package/src/scene/render-scene-types.ts +2 -0
  128. package/src/scene/render-scene.ts +579 -36
  129. package/src/systems/skin-palette-allocator.ts +129 -7
  130. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  131. package/dist/chunk-KZFTK6DO.mjs.map +0 -1
  132. package/dist/chunk-Q752SHI5.mjs.map +0 -1
  133. package/dist/chunk-YKB5DQV7.mjs.map +0 -1
  134. package/dist/chunk-YRUYGA4A.mjs.map +0 -1
  135. package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
@@ -30,7 +30,6 @@ import { createRenderReadLease, type RenderReadLease } from '@forgeax/engine-ecs
30
30
  import {
31
31
  deriveVertexBufferLayout,
32
32
  deriveVertexBufferLayoutFromProjection,
33
- type GpuVertexBufferLayoutEntry,
34
33
  type VertexLayoutProjection,
35
34
  } from '@forgeax/engine-geometry';
36
35
  import type {
@@ -176,7 +175,9 @@ import {
176
175
  makeShaderDeviceAdapter,
177
176
  normalizeMaterialShaderVariantSet,
178
177
  prepareMaterialShaders,
178
+ resolveMaterialShaderBackendArtifactKey,
179
179
  resolveMaterialShaderBindingContract,
180
+ resolveMaterialShaderUvSetCount,
180
181
  resolveMaterialShaderVariantSet,
181
182
  resolveMaterialShaderVertexInputContract,
182
183
  type ShaderDeviceAdapterInternal,
@@ -214,6 +215,17 @@ import {
214
215
  import { STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES } from './shader-prewarm-policy';
215
216
  import { buildReadyWebGPU } from './webgpu-ready';
216
217
  import { DEPTH_TEXTURE_FORMAT, HDR_COLOR_ATTACHMENT_FORMAT } from './webgpu-ready-contract';
218
+ import {
219
+ BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS,
220
+ DEFAULT_VERTEX_ATTRS,
221
+ MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS,
222
+ PBR_SKIN_SENTINEL_ATTR_BUFFER,
223
+ POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS,
224
+ PREPARED_INSTANCE_VERTEX_ATTRS,
225
+ PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS,
226
+ TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS,
227
+ toGpuVertexBufferLayouts,
228
+ } from './webgpu-vertex-layouts';
217
229
 
218
230
  export type { BundlerOptions } from './bundler-contract';
219
231
  export { assembleMaterialProjection } from './material/assembly';
@@ -231,7 +243,9 @@ export {
231
243
  allowsUnlitPreparedFallback,
232
244
  isSharedMaterialUserRegionCompatible,
233
245
  normalizeMaterialShaderVariantSet,
246
+ resolveMaterialShaderBackendArtifactKey,
234
247
  resolveMaterialShaderBindingContract,
248
+ resolveMaterialShaderUvSetCount,
235
249
  resolveMaterialShaderVariantSet,
236
250
  resolveMaterialShaderVertexInputContract,
237
251
  selectNoColorPbrVariant,
@@ -273,139 +287,6 @@ function adaptMipmapShaderModuleFactory(
273
287
  // Transform.world mat4 derivation (audio listener sync + picking read the
274
288
  // derived Transform.world from scripts).
275
289
 
276
- // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4): a single
277
- // shared empty ArrayBuffer used by `buildPipelineContext` to synthesize the
278
- // 6-key VertexAttributeMap when no caller-supplied attributes are available
279
- // for the pbr-skin path. `deriveVertexBufferLayout` is the SSOT for the layout
280
- // (vertex-attribute-layout.ts) and reads only key presence -- value identity
281
- // never matters -- so a zero-byte ArrayBuffer per key produces the exact
282
- // 6-attribute / 72-byte stride layout that the @forgeax::pbr-skin shader's
283
- // @location(0..5) declarations expect.
284
- const PBR_SKIN_SENTINEL_ATTR_BUFFER = new ArrayBuffer(0);
285
-
286
- /** Default 4-attribute vertex layout used as fallback when meshAttributes is undefined. */
287
- const DEFAULT_VERTEX_ATTRS: VertexAttributeMap = {
288
- position: new Float32Array(0),
289
- normal: new Float32Array(0),
290
- uv: new Float32Array(0),
291
- tangent: new Float32Array(0),
292
- };
293
-
294
- const PREPARED_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
295
- position: new Float32Array(0),
296
- normal: new Float32Array(0),
297
- uv: new Float32Array(0),
298
- tangent: new Float32Array(0),
299
- uv1: new Float32Array(0),
300
- };
301
-
302
- const PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
303
- position: new Float32Array(0),
304
- normal: new Float32Array(0),
305
- uv: new Float32Array(0),
306
- tangent: new Float32Array(0),
307
- skinIndex: new Uint16Array(0),
308
- skinWeight: new Float32Array(0),
309
- };
310
-
311
- const POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS = [
312
- {
313
- arrayStride: 9 * 4,
314
- stepMode: 'instance' as const,
315
- attributes: [
316
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
317
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
318
- { shaderLocation: 2, offset: 5 * 4, format: 'float32x4' as const },
319
- ],
320
- },
321
- ] satisfies readonly GPUVertexBufferLayout[];
322
-
323
- const BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
324
- {
325
- arrayStride: 31 * 4,
326
- stepMode: 'instance' as const,
327
- attributes: [
328
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
329
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
330
- { shaderLocation: 2, offset: 5 * 4, format: 'float32x2' as const },
331
- { shaderLocation: 3, offset: 7 * 4, format: 'float32x4' as const },
332
- { shaderLocation: 4, offset: 11 * 4, format: 'float32x4' as const },
333
- { shaderLocation: 5, offset: 15 * 4, format: 'float32x4' as const },
334
- { shaderLocation: 6, offset: 19 * 4, format: 'float32x4' as const },
335
- { shaderLocation: 7, offset: 23 * 4, format: 'float32x4' as const },
336
- { shaderLocation: 8, offset: 27 * 4, format: 'float32x4' as const },
337
- ],
338
- },
339
- ] satisfies readonly GPUVertexBufferLayout[];
340
-
341
- const TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS = [
342
- {
343
- arrayStride: 12 * 4,
344
- stepMode: 'instance' as const,
345
- attributes: [
346
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
347
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
348
- { shaderLocation: 2, offset: 6 * 4, format: 'float32x4' as const },
349
- { shaderLocation: 3, offset: 10 * 4, format: 'float32x2' as const },
350
- ],
351
- },
352
- ] satisfies readonly GPUVertexBufferLayout[];
353
-
354
- const MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
355
- {
356
- arrayStride: 12 * 4,
357
- stepMode: 'vertex' as const,
358
- attributes: [
359
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
360
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
361
- { shaderLocation: 2, offset: 6 * 4, format: 'float32x2' as const },
362
- { shaderLocation: 3, offset: 8 * 4, format: 'float32x4' as const },
363
- ],
364
- },
365
- {
366
- arrayStride: 28 * 4,
367
- stepMode: 'instance' as const,
368
- attributes: [
369
- { shaderLocation: 4, offset: 0, format: 'float32x3' as const },
370
- { shaderLocation: 5, offset: 3 * 4, format: 'float32x3' as const },
371
- { shaderLocation: 6, offset: 6 * 4, format: 'float32x3' as const },
372
- { shaderLocation: 7, offset: 9 * 4, format: 'float32x3' as const },
373
- { shaderLocation: 8, offset: 12 * 4, format: 'float32x4' as const },
374
- { shaderLocation: 9, offset: 16 * 4, format: 'float32x4' as const },
375
- { shaderLocation: 10, offset: 20 * 4, format: 'float32x4' as const },
376
- { shaderLocation: 11, offset: 24 * 4, format: 'float32x4' as const },
377
- ],
378
- },
379
- ] satisfies readonly GPUVertexBufferLayout[];
380
-
381
- function toGpuVertexFormat(
382
- format: GpuVertexBufferLayoutEntry['attributes'][number]['format'],
383
- ): GPUVertexFormat {
384
- switch (format) {
385
- case 'float32x2':
386
- case 'float32x3':
387
- case 'float32x4':
388
- case 'uint16x4':
389
- return format;
390
- default:
391
- throw new Error(`Unsupported geometry vertex format: ${format}`);
392
- }
393
- }
394
-
395
- function toGpuVertexBufferLayouts(
396
- entries: readonly GpuVertexBufferLayoutEntry[],
397
- ): readonly GPUVertexBufferLayout[] {
398
- return entries.map((entry) => ({
399
- arrayStride: entry.arrayStride,
400
- ...(entry.stepMode === undefined ? {} : { stepMode: entry.stepMode }),
401
- attributes: entry.attributes.map((attribute) => ({
402
- shaderLocation: attribute.shaderLocation,
403
- offset: attribute.offset,
404
- format: toGpuVertexFormat(attribute.format),
405
- })),
406
- }));
407
- }
408
-
409
290
  /**
410
291
  * Bundler-layer injection accepted by `createRenderer` (and proxied by
411
292
  * `createApp`) as the optional third argument.
@@ -1954,15 +1835,19 @@ async function makeWebGPURenderer(
1954
1835
  vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance)
1955
1836
  ? getOrBuildPreparedMaterialPipelineLayout(materialShaderId)
1956
1837
  : null;
1838
+ const materialShaderLookup =
1839
+ materialShaderId === undefined
1840
+ ? undefined
1841
+ : getShader().findMaterialArtifact(materialShaderId);
1842
+ const materialShaderSource = materialShaderLookup?.ok
1843
+ ? materialShaderLookup.value.source
1844
+ : undefined;
1957
1845
  const vertexInputContract =
1958
1846
  materialShaderId === undefined
1959
1847
  ? 'render-material'
1960
- : (() => {
1961
- const lookup = getShader().findMaterialArtifact(materialShaderId);
1962
- return lookup.ok
1963
- ? resolveMaterialShaderVertexInputContract(lookup.value.source)
1964
- : 'render-material';
1965
- })();
1848
+ : materialShaderSource !== undefined
1849
+ ? resolveMaterialShaderVertexInputContract(materialShaderSource)
1850
+ : 'render-material';
1966
1851
  const currentState = currentPipelineState();
1967
1852
  const pipelineLayout =
1968
1853
  group0Layout !== null
@@ -1979,9 +1864,16 @@ async function makeWebGPURenderer(
1979
1864
  ? perShaderLayout.pipelineLayout
1980
1865
  : selectPipelineLayoutForVariant(currentState, variantSet, layoutKind);
1981
1866
  if (pipelineLayout === null) return null;
1867
+ const currentMaterialUvSetCounts = currentMaterialShaderUvSetCounts();
1982
1868
  const resolvedUvSetCount =
1983
1869
  materialShaderId !== undefined
1984
- ? currentMaterialShaderUvSetCounts().get(materialShaderId)
1870
+ ? (currentMaterialUvSetCounts.get(materialShaderId) ??
1871
+ (materialShaderSource !== undefined
1872
+ ? resolveMaterialShaderUvSetCount(
1873
+ materialShaderSource,
1874
+ currentMaterialUvSetCounts.get(materialShaderId),
1875
+ )
1876
+ : undefined))
1985
1877
  : undefined;
1986
1878
  const vertexBuffers: readonly GPUVertexBufferLayout[] =
1987
1879
  vertexInputContract === 'none'
@@ -2252,15 +2144,31 @@ async function makeWebGPURenderer(
2252
2144
  (currentState !== null ? currentState.colorAttachmentFormat : 'bgra8unorm-srgb');
2253
2145
  // feat-20260629 M4: auto-fill shaderUvSetCount from naga reflection
2254
2146
  // (stored in materialShaderUvSetCounts during prepareMaterialShaders).
2255
- const resolvedUvSetCount =
2256
- shaderUvSetCount ?? currentMaterialShaderUvSetCounts().get(materialShaderId);
2257
- const manifestEntry = findMaterialShaderManifestEntry(materialShaderId);
2147
+ const pipelineShaderId = resolveMaterialShaderBackendArtifactKey(
2148
+ materialShaderId,
2149
+ internals.device.caps.backendKind,
2150
+ assets.getMaterialArtifact(materialShaderId),
2151
+ );
2152
+ const materialShaderLookup = getShader().findMaterialArtifact(pipelineShaderId);
2153
+ const materialShaderSource = materialShaderLookup.ok
2154
+ ? materialShaderLookup.value.source
2155
+ : undefined;
2156
+ const currentMaterialUvSetCounts = currentMaterialShaderUvSetCounts();
2157
+ const materialUvSetCount = resolveMaterialShaderUvSetCount(
2158
+ materialShaderSource,
2159
+ currentMaterialUvSetCounts.get(pipelineShaderId),
2160
+ );
2161
+ if (materialUvSetCount !== undefined) {
2162
+ currentMaterialUvSetCounts.set(pipelineShaderId, materialUvSetCount);
2163
+ }
2164
+ const resolvedUvSetCount = shaderUvSetCount ?? materialUvSetCount;
2165
+ const manifestEntry = findMaterialShaderManifestEntry(pipelineShaderId);
2258
2166
  // Sprite's omitted request is the boot-selected default artifact. The
2259
2167
  // explicit empty key remains the PER_INSTANCE_REGION=true request used by
2260
2168
  // SpriteInstances, so only that path is capability-rewritten here.
2261
2169
  const resolvedVariantSet =
2262
2170
  variantSet === undefined &&
2263
- (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2171
+ (pipelineShaderId === 'forgeax::sprite' || pipelineShaderId === 'forgeax::sprite-lit')
2264
2172
  ? undefined
2265
2173
  : resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2266
2174
  // The record stage also composes engine capability axes for user shaders.
@@ -2272,6 +2180,12 @@ async function makeWebGPURenderer(
2272
2180
  // A single-source manifest entry must stay on its canonical module key;
2273
2181
  // otherwise a synthesized capability axis races the prewarmed module.
2274
2182
  let effectiveVariantSet = normalizeMaterialShaderVariantSet(resolvedVariantSet, manifestEntry);
2183
+ // Runtime-published authored materials are installed from their immutable
2184
+ // cooked artifact, not from the build-time material-shader manifest. The
2185
+ // published artifact identity already selects the backend-specific bytes;
2186
+ // an absent manifest therefore means the source itself is the canonical
2187
+ // pipeline module and must not carry a synthesized capability key.
2188
+ if (manifestEntry === undefined) effectiveVariantSet = undefined;
2275
2189
  if (
2276
2190
  vertexLayoutProjection === undefined &&
2277
2191
  manifestEntry?.variants.some((variant) => 'VERTEX_COLOR_AVAILABLE' in variant.defines)
@@ -2294,7 +2208,7 @@ async function makeWebGPURenderer(
2294
2208
  }
2295
2209
  if (
2296
2210
  manifestEntry !== undefined &&
2297
- !materialShaderId.startsWith('forgeax::') &&
2211
+ !pipelineShaderId.startsWith('forgeax::') &&
2298
2212
  effectiveVariantSet !== undefined &&
2299
2213
  effectiveVariantSet !== ''
2300
2214
  ) {
@@ -2323,7 +2237,7 @@ async function makeWebGPURenderer(
2323
2237
  const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
2324
2238
  const isDepthOnlyPass = passKindPolicyTable[passKind]?.shape === 'depth-only';
2325
2239
  const spec: PipelineSpec = {
2326
- shader: { id: materialShaderId, passKind, variantSet: effectiveVariantSet },
2240
+ shader: { id: pipelineShaderId, passKind, variantSet: effectiveVariantSet },
2327
2241
  attachments: {
2328
2242
  colorFormats:
2329
2243
  additionalColorFormats === undefined
@@ -2362,7 +2276,7 @@ async function makeWebGPURenderer(
2362
2276
  const cached = currentPipelineCacheState().materialShaderPipelineCache.get(cacheKey);
2363
2277
  if (cached !== undefined) return cached;
2364
2278
  if (pipelineState === null) return null;
2365
- const lookup = getShader().findMaterialArtifact(materialShaderId);
2279
+ const lookup = getShader().findMaterialArtifact(pipelineShaderId);
2366
2280
  if (!lookup.ok) {
2367
2281
  if (shouldDeferMissingPreparedMaterialShader(vertexLayout)) {
2368
2282
  // Prepared VFX draws bind only their feature-owned groups. The
@@ -2377,7 +2291,7 @@ async function makeWebGPURenderer(
2377
2291
  // the standard pipeline from the engine-shipped PBR entry.
2378
2292
  if (
2379
2293
  renderState === undefined &&
2380
- !allowsUnlitPreparedFallback(depthFormatOverride, vertexLayout, materialShaderId)
2294
+ !allowsUnlitPreparedFallback(depthFormatOverride, vertexLayout, pipelineShaderId)
2381
2295
  ) {
2382
2296
  // A prepared feature declares the vertex layout it owns. Falling back
2383
2297
  // to default-unlit here would pair that data with the unlit shader's
@@ -2392,7 +2306,7 @@ async function makeWebGPURenderer(
2392
2306
  return buildAndCachePipeline(
2393
2307
  cacheKey,
2394
2308
  unlitLookup.value,
2395
- `pbr-pipeline-prepared-${materialShaderId}`,
2309
+ `pbr-pipeline-prepared-${pipelineShaderId}`,
2396
2310
  // Reuse the eagerly compiled engine unlit module. The adapter cache
2397
2311
  // is seeded under this label during renderer readiness; a new label
2398
2312
  // would keep color-only prepared passes in perpetual warm-up.
@@ -2420,7 +2334,7 @@ async function makeWebGPURenderer(
2420
2334
  return buildAndCachePipeline(
2421
2335
  cacheKey,
2422
2336
  { source: pbrEntry.wgsl, paramSchema: [] },
2423
- `pbr-pipeline-fallback-${materialShaderId}${isHdr ? '-hdr' : ''}`,
2337
+ `pbr-pipeline-fallback-${pipelineShaderId}${isHdr ? '-hdr' : ''}`,
2424
2338
  // w16-b: module identity is the fallback PBR source, stable across
2425
2339
  // topology / renderState / HDR (all baked into the PSO) so every
2426
2340
  // variant reuses one compiled module.
@@ -2454,8 +2368,8 @@ async function makeWebGPURenderer(
2454
2368
  // layout resolution so buildPipelineContext derives the layout that
2455
2369
  // matches the compiled module. Skin keeps its
2456
2370
  // own id so the pbr-skin fail-fast (null layout) is preserved.
2457
- materialShaderId === SKIN_MATERIAL_SHADER_ID
2458
- ? materialShaderId
2371
+ pipelineShaderId === SKIN_MATERIAL_SHADER_ID
2372
+ ? pipelineShaderId
2459
2373
  : 'forgeax::default-standard-pbr',
2460
2374
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2461
2375
  // forward meshAttributes through the fallback path so the pbr-skin
@@ -2489,7 +2403,7 @@ async function makeWebGPURenderer(
2489
2403
  if (effectiveVariantSet !== undefined) {
2490
2404
  const registry = getShader();
2491
2405
  for (const msEntry of registry.materialShaderManifestEntries()) {
2492
- if (msEntry.identifier === materialShaderId) {
2406
+ if (msEntry.identifier === pipelineShaderId) {
2493
2407
  const variant = findVariantByKey(msEntry, effectiveVariantSet);
2494
2408
  if (variant) {
2495
2409
  // M3 / w12-w13: variant substitution carries the same source +
@@ -2516,7 +2430,7 @@ async function makeWebGPURenderer(
2516
2430
  return buildAndCachePipeline(
2517
2431
  cacheKey,
2518
2432
  shaderEntry,
2519
- `pbr-pipeline-${materialShaderId}${isHdr ? '-hdr' : ''}${passKindLabelSegment}`,
2433
+ `pbr-pipeline-${pipelineShaderId}${isHdr ? '-hdr' : ''}${passKindLabelSegment}`,
2520
2434
  // feat-20260609 M4 / w31: when variantSet is non-empty, the module identity
2521
2435
  // includes the variant key so URP (STORAGE_BUFFER_AVAILABLE=true) and HDRP
2522
2436
  // (CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true) variants
@@ -2530,8 +2444,8 @@ async function makeWebGPURenderer(
2530
2444
  // empty-tail segment); module identity stays a function of the
2531
2445
  // exact variantSet string.
2532
2446
  effectiveVariantSet !== undefined
2533
- ? `module-${materialShaderId}#${effectiveVariantSet}`
2534
- : `module-${materialShaderId}`,
2447
+ ? `module-${pipelineShaderId}#${effectiveVariantSet}`
2448
+ : `module-${pipelineShaderId}`,
2535
2449
  isHdr,
2536
2450
  renderState,
2537
2451
  topology,
@@ -2547,7 +2461,7 @@ async function makeWebGPURenderer(
2547
2461
  // bug-20260611-skin-pipeline-layout: thread the registered materialShaderId
2548
2462
  // so buildPipelineContext can resolve LayoutKind='pbr-skin' for the skin
2549
2463
  // shader (2-entry mesh-array BGL).
2550
- materialShaderId,
2464
+ pipelineShaderId,
2551
2465
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2552
2466
  // forward meshAttributes so the pbr-skin path's vertex buffer layout
2553
2467
  // is derived via the SSOT (deriveVertexBufferLayout). For URP/HDRP
@@ -3009,6 +2923,24 @@ async function makeWebGPURenderer(
3009
2923
  featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
3010
2924
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
3011
2925
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2926
+ instanceCollections: Object.freeze(
2927
+ renderSystem.instanceCollectionsInspection.map((collection) =>
2928
+ Object.freeze({
2929
+ ...collection,
2930
+ uploadRanges: Object.freeze(
2931
+ collection.uploadRanges.map((range) => Object.freeze({ ...range })),
2932
+ ),
2933
+ ...(collection.error === undefined
2934
+ ? {}
2935
+ : {
2936
+ error: Object.freeze({
2937
+ ...collection.error,
2938
+ detail: Object.freeze({ ...collection.error.detail }),
2939
+ }),
2940
+ }),
2941
+ }),
2942
+ ),
2943
+ ),
3012
2944
  renderScene: renderSystem.renderScene,
3013
2945
  reflectionProbes: renderSystem.reflectionProbes,
3014
2946
  ssrDependencies: renderSystem.ssrDependencies,
@@ -0,0 +1,132 @@
1
+ import type { GpuVertexBufferLayoutEntry } from '@forgeax/engine-geometry';
2
+ import type { VertexAttributeMap } from '@forgeax/engine-types';
3
+
4
+ // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4): a single
5
+ // shared empty ArrayBuffer used by the pipeline builder to synthesize the
6
+ // pbr-skin attribute map when no caller-supplied attributes are available.
7
+ // `deriveVertexBufferLayout` is the SSOT for the layout and reads only key
8
+ // presence, so a zero-byte buffer per key preserves the six-attribute stride.
9
+ export const PBR_SKIN_SENTINEL_ATTR_BUFFER = new ArrayBuffer(0);
10
+
11
+ /** Default 4-attribute vertex layout used as fallback when meshAttributes is undefined. */
12
+ export const DEFAULT_VERTEX_ATTRS: VertexAttributeMap = {
13
+ position: new Float32Array(0),
14
+ normal: new Float32Array(0),
15
+ uv: new Float32Array(0),
16
+ tangent: new Float32Array(0),
17
+ };
18
+
19
+ export const PREPARED_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
20
+ position: new Float32Array(0),
21
+ normal: new Float32Array(0),
22
+ uv: new Float32Array(0),
23
+ tangent: new Float32Array(0),
24
+ uv1: new Float32Array(0),
25
+ };
26
+
27
+ export const PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
28
+ position: new Float32Array(0),
29
+ normal: new Float32Array(0),
30
+ uv: new Float32Array(0),
31
+ tangent: new Float32Array(0),
32
+ skinIndex: new Uint16Array(0),
33
+ skinWeight: new Float32Array(0),
34
+ };
35
+
36
+ export const POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS = [
37
+ {
38
+ arrayStride: 9 * 4,
39
+ stepMode: 'instance' as const,
40
+ attributes: [
41
+ { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
42
+ { shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
43
+ { shaderLocation: 2, offset: 5 * 4, format: 'float32x4' as const },
44
+ ],
45
+ },
46
+ ] satisfies readonly GPUVertexBufferLayout[];
47
+
48
+ export const BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
49
+ {
50
+ arrayStride: 31 * 4,
51
+ stepMode: 'instance' as const,
52
+ attributes: [
53
+ { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
54
+ { shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
55
+ { shaderLocation: 2, offset: 5 * 4, format: 'float32x2' as const },
56
+ { shaderLocation: 3, offset: 7 * 4, format: 'float32x4' as const },
57
+ { shaderLocation: 4, offset: 11 * 4, format: 'float32x4' as const },
58
+ { shaderLocation: 5, offset: 15 * 4, format: 'float32x4' as const },
59
+ { shaderLocation: 6, offset: 19 * 4, format: 'float32x4' as const },
60
+ { shaderLocation: 7, offset: 23 * 4, format: 'float32x4' as const },
61
+ { shaderLocation: 8, offset: 27 * 4, format: 'float32x4' as const },
62
+ ],
63
+ },
64
+ ] satisfies readonly GPUVertexBufferLayout[];
65
+
66
+ export const TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS = [
67
+ {
68
+ arrayStride: 12 * 4,
69
+ stepMode: 'instance' as const,
70
+ attributes: [
71
+ { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
72
+ { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
73
+ { shaderLocation: 2, offset: 6 * 4, format: 'float32x4' as const },
74
+ { shaderLocation: 3, offset: 10 * 4, format: 'float32x2' as const },
75
+ ],
76
+ },
77
+ ] satisfies readonly GPUVertexBufferLayout[];
78
+
79
+ export const MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
80
+ {
81
+ arrayStride: 12 * 4,
82
+ stepMode: 'vertex' as const,
83
+ attributes: [
84
+ { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
85
+ { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
86
+ { shaderLocation: 2, offset: 6 * 4, format: 'float32x2' as const },
87
+ { shaderLocation: 3, offset: 8 * 4, format: 'float32x4' as const },
88
+ ],
89
+ },
90
+ {
91
+ arrayStride: 28 * 4,
92
+ stepMode: 'instance' as const,
93
+ attributes: [
94
+ { shaderLocation: 4, offset: 0, format: 'float32x3' as const },
95
+ { shaderLocation: 5, offset: 3 * 4, format: 'float32x3' as const },
96
+ { shaderLocation: 6, offset: 6 * 4, format: 'float32x3' as const },
97
+ { shaderLocation: 7, offset: 9 * 4, format: 'float32x3' as const },
98
+ { shaderLocation: 8, offset: 12 * 4, format: 'float32x4' as const },
99
+ { shaderLocation: 9, offset: 16 * 4, format: 'float32x4' as const },
100
+ { shaderLocation: 10, offset: 20 * 4, format: 'float32x4' as const },
101
+ { shaderLocation: 11, offset: 24 * 4, format: 'float32x4' as const },
102
+ ],
103
+ },
104
+ ] satisfies readonly GPUVertexBufferLayout[];
105
+
106
+ function toGpuVertexFormat(
107
+ format: GpuVertexBufferLayoutEntry['attributes'][number]['format'],
108
+ ): GPUVertexFormat {
109
+ switch (format) {
110
+ case 'float32x2':
111
+ case 'float32x3':
112
+ case 'float32x4':
113
+ case 'uint16x4':
114
+ return format;
115
+ default:
116
+ throw new Error(`Unsupported geometry vertex format: ${format}`);
117
+ }
118
+ }
119
+
120
+ export function toGpuVertexBufferLayouts(
121
+ entries: readonly GpuVertexBufferLayoutEntry[],
122
+ ): readonly GPUVertexBufferLayout[] {
123
+ return entries.map((entry) => ({
124
+ arrayStride: entry.arrayStride,
125
+ ...(entry.stepMode === undefined ? {} : { stepMode: entry.stepMode }),
126
+ attributes: entry.attributes.map((attribute) => ({
127
+ shaderLocation: attribute.shaderLocation,
128
+ offset: attribute.offset,
129
+ format: toGpuVertexFormat(attribute.format),
130
+ })),
131
+ }));
132
+ }
@@ -6,6 +6,8 @@ import {
6
6
  err,
7
7
  MATERIAL_TEXTURE_SLOTS,
8
8
  type MaterialAsset,
9
+ type MaterialChildAsset,
10
+ type MaterialRootAsset,
9
11
  ok,
10
12
  type RenderPipelineAsset,
11
13
  type SamplerAsset,
@@ -31,6 +33,12 @@ function validMaterial(value: unknown): value is MaterialAsset {
31
33
  }
32
34
  if (value.passes !== undefined && !Array.isArray(value.passes)) return false;
33
35
  if (value.parameters !== undefined && !Array.isArray(value.parameters)) return false;
36
+ if (
37
+ value.parent !== undefined &&
38
+ ['colorSpace', 'passes', 'parameters'].some((field) => Object.hasOwn(value, field))
39
+ ) {
40
+ return false;
41
+ }
34
42
  return value.values === undefined || record(value.values);
35
43
  }
36
44
 
@@ -60,16 +68,22 @@ function resolveMaterialWireRefs(
60
68
  if (refs.length === 0) return ok(value);
61
69
 
62
70
  let changed = false;
63
- let parent = value.parent;
71
+ const wire = value as unknown as { readonly parent?: unknown };
72
+ let parent = wire.parent;
64
73
  if (typeof parent === 'number') {
65
74
  const resolved = resolveMaterialWireRef(parent, refs, 'parent', guid);
66
75
  if (!resolved.ok) return resolved;
67
- parent = resolved.value as unknown as MaterialAsset['parent'];
76
+ parent = resolved.value;
68
77
  changed = true;
69
78
  }
70
79
 
71
80
  if (value.values === undefined) {
72
- return changed ? ok({ ...value, ...(parent === undefined ? {} : { parent }) }) : ok(value);
81
+ if (!changed) return ok(value);
82
+ if (parent !== undefined) {
83
+ const child = value as MaterialChildAsset;
84
+ return ok({ ...child, parent: parent as unknown as MaterialChildAsset['parent'] });
85
+ }
86
+ return ok(value);
73
87
  }
74
88
 
75
89
  const textureFields = new Set(
@@ -105,7 +119,16 @@ function resolveMaterialWireRefs(
105
119
  values[field] = rewritten as unknown as NonNullable<MaterialAsset['values']>[string];
106
120
  }
107
121
  }
108
- return ok(changed ? { ...value, ...(parent === undefined ? {} : { parent }), values } : value);
122
+ if (!changed) return ok(value);
123
+ if (parent !== undefined) {
124
+ const child = value as MaterialChildAsset;
125
+ return ok({
126
+ ...child,
127
+ parent: parent as unknown as MaterialChildAsset['parent'],
128
+ values,
129
+ });
130
+ }
131
+ return ok({ ...value, values } as MaterialRootAsset);
109
132
  }
110
133
 
111
134
  export const materialContribution: AssetDecoderContribution<MaterialAsset, 'material'> = {