@forgeax/engine-render 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +118 -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/assembly.d.ts +4 -17
  5. package/dist/assembly/material/assembly.d.ts.map +1 -1
  6. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -1
  7. package/dist/assembly/material/surface-projection.d.ts +24 -0
  8. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  9. package/dist/assembly/material-shader-policy.d.ts +20 -0
  10. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  12. package/dist/assembly/webgpu-renderer.d.ts +1 -2
  13. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  14. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  15. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  16. package/dist/assets/asset-decoders.d.ts.map +1 -1
  17. package/dist/authoring.mjs +1 -1
  18. package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
  19. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  20. package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
  21. package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
  22. package/dist/{chunk-YKB5DQV7.mjs → chunk-GF523LHF.mjs} +89 -21
  23. package/dist/chunk-GF523LHF.mjs.map +1 -0
  24. package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
  25. package/dist/chunk-HKXTW355.mjs.map +1 -0
  26. package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
  27. package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
  28. package/dist/{chunk-YRUYGA4A.mjs → chunk-TMIASV2N.mjs} +944 -577
  29. package/dist/chunk-TMIASV2N.mjs.map +1 -0
  30. package/dist/{chunk-Z3YYH2MB.mjs → chunk-YUQXSEHG.mjs} +160 -73
  31. package/dist/chunk-YUQXSEHG.mjs.map +1 -0
  32. package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  33. package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
  34. package/dist/components/instances.d.ts +13 -31
  35. package/dist/components/instances.d.ts.map +1 -1
  36. package/dist/construct-renderer.mjs +1012 -149
  37. package/dist/construct-renderer.mjs.map +1 -1
  38. package/dist/extract/contracts.d.ts +6 -6
  39. package/dist/extract/contracts.d.ts.map +1 -1
  40. package/dist/gpu-driven/production-raster.d.ts +6 -1
  41. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  42. package/dist/gpu-scene.d.ts +4 -0
  43. package/dist/gpu-scene.d.ts.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.mjs +27 -9
  47. package/dist/index.mjs.map +1 -1
  48. package/dist/instance-buffer-cache.d.ts +7 -1
  49. package/dist/instance-buffer-cache.d.ts.map +1 -1
  50. package/dist/instances.d.ts +101 -0
  51. package/dist/instances.d.ts.map +1 -0
  52. package/dist/internal.mjs +5 -5
  53. package/dist/materials.d.ts +63 -2
  54. package/dist/materials.d.ts.map +1 -1
  55. package/dist/pipeline-builder.d.ts.map +1 -1
  56. package/dist/pipeline-spec-types.d.ts +2 -0
  57. package/dist/pipeline-spec-types.d.ts.map +1 -1
  58. package/dist/pipeline-spec.d.ts.map +1 -1
  59. package/dist/record/frame-snapshot.d.ts +13 -2
  60. package/dist/record/frame-snapshot.d.ts.map +1 -1
  61. package/dist/record/frame.d.ts.map +1 -1
  62. package/dist/record/main-pass-geometry.d.ts +23 -4
  63. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  64. package/dist/record/main-pass-material.d.ts +20 -2
  65. package/dist/record/main-pass-material.d.ts.map +1 -1
  66. package/dist/record/main-pass-sprite-draws.d.ts +3 -3
  67. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  68. package/dist/record/main-pass.d.ts.map +1 -1
  69. package/dist/record/material-uniforms.d.ts +4 -0
  70. package/dist/record/material-uniforms.d.ts.map +1 -0
  71. package/dist/record/render-context.d.ts +4 -3
  72. package/dist/record/render-context.d.ts.map +1 -1
  73. package/dist/record/shadow-pass.d.ts +10 -1
  74. package/dist/record/shadow-pass.d.ts.map +1 -1
  75. package/dist/recovery/render-system-candidate.d.ts +1 -1
  76. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  77. package/dist/render-contract.d.ts +4 -0
  78. package/dist/render-contract.d.ts.map +1 -1
  79. package/dist/render-system-extract-tail.d.ts +6 -0
  80. package/dist/render-system-extract-tail.d.ts.map +1 -1
  81. package/dist/render-system-extract.d.ts +44 -9
  82. package/dist/render-system-extract.d.ts.map +1 -1
  83. package/dist/render-system.d.ts +4 -1
  84. package/dist/render-system.d.ts.map +1 -1
  85. package/dist/scene/render-scene-types.d.ts +2 -0
  86. package/dist/scene/render-scene-types.d.ts.map +1 -1
  87. package/dist/scene/render-scene.d.ts +12 -1
  88. package/dist/scene/render-scene.d.ts.map +1 -1
  89. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  90. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  91. package/dist/temporal/index.mjs +5 -5
  92. package/package.json +20 -20
  93. package/src/__tests__/fixtures/ai-weapon-spirit/README.md +20 -0
  94. package/src/__tests__/fixtures/ai-weapon-spirit/low-poly-toon.wgsl +178 -0
  95. package/src/__tests__/fixtures/ai-weapon-spirit/material.json +97 -0
  96. package/src/__tests__/fixtures/ai-weapon-spirit/painterly-surface.wgsl +55 -0
  97. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +3 -3
  98. package/src/__tests__/gpu-driven-view.browser.test.ts +11 -3
  99. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  100. package/src/__tests__/instances-store.unit.test.ts +46 -0
  101. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  102. package/src/__tests__/material-contract-inventory.unit.test.ts +48 -13
  103. package/src/__tests__/{material-static-dynamic-route.unit.test.ts → material-layer-projection.unit.test.ts} +30 -47
  104. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  105. package/src/__tests__/material-program-dispatch.unit.test.ts +48 -0
  106. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  107. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  108. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  109. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
  110. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +44 -2
  111. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  112. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  113. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  114. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  115. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  116. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  117. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  118. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  119. package/src/assembly/factory.ts +2 -3
  120. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  121. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  122. package/src/assembly/material/assembly.ts +8 -40
  123. package/src/assembly/material/standard-layer-projection.ts +3 -1
  124. package/src/assembly/material/surface-projection.ts +91 -0
  125. package/src/assembly/material-shader-policy.ts +88 -10
  126. package/src/assembly/recovery/renderer-recover.ts +1 -1
  127. package/src/assembly/webgpu-ready.ts +3 -16
  128. package/src/assembly/webgpu-renderer.ts +106 -166
  129. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  130. package/src/assets/asset-decoders.ts +27 -4
  131. package/src/components/instances.ts +14 -197
  132. package/src/extract/contracts.ts +6 -6
  133. package/src/gpu-driven/production-raster.ts +34 -1
  134. package/src/gpu-scene.ts +84 -29
  135. package/src/index.ts +1 -0
  136. package/src/instance-buffer-cache.ts +14 -1
  137. package/src/instances.ts +288 -0
  138. package/src/materials.ts +109 -14
  139. package/src/pipeline-builder.ts +14 -10
  140. package/src/pipeline-spec-types.ts +2 -0
  141. package/src/pipeline-spec.ts +13 -17
  142. package/src/record/frame-snapshot.ts +19 -2
  143. package/src/record/frame.ts +38 -3
  144. package/src/record/main-pass-geometry.ts +837 -337
  145. package/src/record/main-pass-material.ts +87 -2
  146. package/src/record/main-pass-sprite-draws.ts +66 -169
  147. package/src/record/main-pass.ts +24 -246
  148. package/src/record/material-uniforms.ts +186 -0
  149. package/src/record/render-context.ts +8 -0
  150. package/src/record/shadow-pass.ts +120 -115
  151. package/src/recovery/render-system-candidate.ts +46 -5
  152. package/src/render-contract.ts +4 -0
  153. package/src/render-system-extract-tail.ts +153 -57
  154. package/src/render-system-extract.ts +110 -73
  155. package/src/render-system.ts +59 -5
  156. package/src/scene/render-scene-types.ts +2 -0
  157. package/src/scene/render-scene.ts +579 -36
  158. package/src/systems/skin-palette-allocator.ts +129 -7
  159. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  160. package/dist/assembly/material/pipeline-projection.d.ts +0 -30
  161. package/dist/assembly/material/pipeline-projection.d.ts.map +0 -1
  162. package/dist/chunk-KZFTK6DO.mjs.map +0 -1
  163. package/dist/chunk-Q752SHI5.mjs.map +0 -1
  164. package/dist/chunk-YKB5DQV7.mjs.map +0 -1
  165. package/dist/chunk-YRUYGA4A.mjs.map +0 -1
  166. package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
  167. package/src/assembly/material/pipeline-projection.ts +0 -63
package/src/materials.ts CHANGED
@@ -8,7 +8,11 @@ import type {
8
8
  MaterialRenderState,
9
9
  MaterialValue,
10
10
  } from '@forgeax/engine-types';
11
- import { deriveStandardLayerPlan } from '@forgeax/engine-types';
11
+ import { deriveStandardLayerPlan, standardSurfaceParameters } from '@forgeax/engine-types';
12
+ import {
13
+ DEFAULT_STANDARD_SURFACE_MODULE,
14
+ projectStandardSurfacePasses,
15
+ } from './assembly/material/surface-projection';
12
16
 
13
17
  export type MaterialTransmissionValidationReason =
14
18
  | 'non-finite'
@@ -58,7 +62,6 @@ export const SPRITE_PREMULTIPLIED_ALPHA_BLEND: GPUBlendState = {
58
62
  alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
59
63
  };
60
64
 
61
- const STANDARD_MODULE = 'forgeax_material::standard';
62
65
  const UNLIT_MODULE = 'forgeax_material::unlit';
63
66
  const SPRITE_MODULE = 'forgeax_material::sprite';
64
67
 
@@ -389,6 +392,70 @@ interface StandardOpts {
389
392
  readonly queue?: number;
390
393
  }
391
394
 
395
+ /** Built-in Standard authoring surface with engine-owned parameter defaults. */
396
+ export interface DefaultStandardOptions extends StandardOpts {
397
+ readonly surfaceModule?: never;
398
+ readonly parameters?: never;
399
+ readonly values?: never;
400
+ }
401
+
402
+ /** Import-first Standard Surface authoring; the root parameter contract stays caller-owned. */
403
+ export interface CustomStandardSurfaceOptions {
404
+ readonly surfaceModule: string;
405
+ readonly parameters: readonly MaterialParameter[];
406
+ readonly values: Readonly<Record<string, MaterialValue>>;
407
+ readonly colorSpace?: MaterialColorSpace;
408
+ readonly renderState?: MaterialRenderState;
409
+ readonly castShadow?: boolean;
410
+ readonly queue?: number;
411
+ readonly baseColor?: never;
412
+ readonly metallic?: never;
413
+ readonly roughness?: never;
414
+ readonly metallicChannel?: never;
415
+ readonly roughnessChannel?: never;
416
+ readonly clearcoat?: never;
417
+ readonly clearcoatRoughness?: never;
418
+ readonly clearcoatTexture?: never;
419
+ readonly clearcoatRoughnessTexture?: never;
420
+ readonly clearcoatNormalTexture?: never;
421
+ readonly clearcoatNormalScale?: never;
422
+ readonly anisotropyStrength?: never;
423
+ readonly anisotropyRotation?: never;
424
+ readonly anisotropyTexture?: never;
425
+ readonly sheenColor?: never;
426
+ readonly sheenRoughness?: never;
427
+ readonly sheenColorTexture?: never;
428
+ readonly sheenRoughnessTexture?: never;
429
+ readonly iridescence?: never;
430
+ readonly iridescenceIor?: never;
431
+ readonly iridescenceThicknessMinimum?: never;
432
+ readonly iridescenceThicknessMaximum?: never;
433
+ readonly iridescenceTexture?: never;
434
+ readonly iridescenceThicknessTexture?: never;
435
+ readonly specular?: never;
436
+ readonly specularColor?: never;
437
+ readonly specularTexture?: never;
438
+ readonly specularColorTexture?: never;
439
+ readonly emissive?: never;
440
+ readonly emissiveIntensity?: never;
441
+ readonly emissiveTexture?: never;
442
+ readonly baseColorTexture?: never;
443
+ readonly metallicRoughnessTexture?: never;
444
+ readonly normalTexture?: never;
445
+ readonly occlusionTexture?: never;
446
+ readonly occlusionStrength?: never;
447
+ readonly alphaCutoff?: never;
448
+ readonly transmission?: never;
449
+ readonly transmissionTexture?: never;
450
+ readonly ior?: never;
451
+ readonly thickness?: never;
452
+ readonly thicknessTexture?: never;
453
+ readonly attenuationColor?: never;
454
+ readonly attenuationDistance?: never;
455
+ }
456
+
457
+ export type StandardOptions = DefaultStandardOptions | CustomStandardSurfaceOptions;
458
+
392
459
  function validateChannel(
393
460
  name: 'metallicChannel' | 'roughnessChannel',
394
461
  value: number | undefined,
@@ -413,7 +480,7 @@ function validateFiniteRange(
413
480
  }
414
481
  }
415
482
 
416
- function standard(opts: StandardOpts): MaterialAsset {
483
+ function standardDefault(opts: DefaultStandardOptions): MaterialAsset {
417
484
  const occlusionStrength = opts.occlusionStrength ?? 1;
418
485
  if (occlusionStrength < 0 || occlusionStrength > 1) {
419
486
  throw new Error(
@@ -511,21 +578,23 @@ function standard(opts: StandardOpts): MaterialAsset {
511
578
  if (opts.attenuationDistance !== undefined) values.attenuationDistance = opts.attenuationDistance;
512
579
  const forwardRenderState =
513
580
  transmission > 0 ? { ...(opts.renderState ?? {}), depthWriteEnabled: false } : opts.renderState;
514
- const parameters = standardParameters('linear', opts);
581
+ const parameters = standardSurfaceParameters(standardParameters('linear', opts));
515
582
  const declaredNames = new Set(parameters.map((parameter) => parameter.name));
516
583
  for (const name of Object.keys(values)) {
517
584
  if (!declaredNames.has(name)) delete values[name];
518
585
  }
519
- const physical = deriveStandardLayerPlan(parameters).mode === 'physical';
520
- const passes: [MaterialPass, ...MaterialPass[]] = [
521
- pass('forward', STANDARD_MODULE, forwardRenderState, 'fs_main', opts.queue),
522
- ];
523
- if (!physical && transmission === 0) {
524
- passes.push(pass('deferred', STANDARD_MODULE, opts.renderState, 'fs_gbuffer', opts.queue));
525
- }
526
- if (opts.castShadow !== false) {
527
- passes.push(pass('shadow-caster', STANDARD_MODULE, shadowCasterRenderState(opts.renderState)));
528
- }
586
+ const layerPlan = deriveStandardLayerPlan(parameters);
587
+ const projected = projectStandardSurfacePasses({
588
+ surfaceModule: DEFAULT_STANDARD_SURFACE_MODULE,
589
+ values,
590
+ ...(forwardRenderState === undefined ? {} : { renderState: forwardRenderState }),
591
+ ...(opts.castShadow === undefined ? {} : { castShadow: opts.castShadow }),
592
+ ...(opts.queue === undefined ? {} : { queue: opts.queue }),
593
+ layerPlan,
594
+ });
595
+ const passes = (
596
+ transmission > 0 ? projected.filter((entry) => entry.name !== 'deferred') : projected
597
+ ) as [MaterialPass, ...MaterialPass[]];
529
598
  return {
530
599
  kind: 'material',
531
600
  colorSpace: 'linear',
@@ -535,6 +604,32 @@ function standard(opts: StandardOpts): MaterialAsset {
535
604
  };
536
605
  }
537
606
 
607
+ function standardCustom(opts: CustomStandardSurfaceOptions): MaterialAsset {
608
+ const layerPlan = deriveStandardLayerPlan(
609
+ opts.parameters.filter((parameter) => parameter.optional !== true),
610
+ );
611
+ return {
612
+ kind: 'material',
613
+ ...(opts.colorSpace === undefined || opts.colorSpace === 'srgb'
614
+ ? {}
615
+ : { colorSpace: opts.colorSpace }),
616
+ passes: projectStandardSurfacePasses({
617
+ surfaceModule: opts.surfaceModule,
618
+ values: opts.values,
619
+ ...(opts.renderState === undefined ? {} : { renderState: opts.renderState }),
620
+ ...(opts.castShadow === undefined ? {} : { castShadow: opts.castShadow }),
621
+ ...(opts.queue === undefined ? {} : { queue: opts.queue }),
622
+ layerPlan,
623
+ }),
624
+ parameters: standardSurfaceParameters(opts.parameters),
625
+ values: opts.values,
626
+ };
627
+ }
628
+
629
+ export function standard(options: StandardOptions): MaterialAsset {
630
+ return options.surfaceModule === undefined ? standardDefault(options) : standardCustom(options);
631
+ }
632
+
538
633
  export const Materials = { unlit, standard, srgb } as const;
539
634
 
540
635
  export { SPRITE_MODULE };
@@ -277,7 +277,13 @@ export function buildPipelineForMaterialShader(
277
277
  const vsEntry = vertexEntry ?? (passKind === 'temporal' ? 'vs_temporal' : 'vs_main');
278
278
  const fsEntry =
279
279
  fragmentEntry ??
280
- (passKind === 'temporal' ? 'fs_temporal' : passKind === 'deferred' ? 'fs_gbuffer' : 'fs_main');
280
+ (passKind === 'temporal'
281
+ ? 'fs_temporal'
282
+ : passKind === 'deferred'
283
+ ? 'fs_gbuffer'
284
+ : passKind === 'shadow-caster'
285
+ ? 'fs_shadow'
286
+ : 'fs_main');
281
287
 
282
288
  let source = entry.source;
283
289
  if (defines !== undefined && Object.keys(defines).length > 0) {
@@ -345,15 +351,13 @@ export function buildPipelineForMaterialShader(
345
351
  vertex: {
346
352
  module: shaderModule,
347
353
  entryPoint: vsEntry,
348
- buffers:
349
- ctx.layoutKind === 'pbr-skin'
350
- ? [...ctx.vertexBuffers]
351
- : [
352
- {
353
- arrayStride: 12 * 4,
354
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' as const }],
355
- },
356
- ],
354
+ // Surface-backed Standard shadow entries evaluate the same material
355
+ // surface as Forward/Deferred, so their vertex stage consumes normal,
356
+ // UV, and tangent inputs in addition to position. Keep the complete
357
+ // derived mesh layout here; extra attributes remain legal for legacy
358
+ // position-only custom casters, while trimming to position would make
359
+ // the built-in Surface shadow shader fail WebGPU validation.
360
+ buffers: [...ctx.vertexBuffers],
357
361
  },
358
362
  // Vertex-only depth pass writes `fragment: undefined` (GPU derives
359
363
  // depth from gl_Position.z). A custom ShadowCaster shader with a
@@ -10,6 +10,8 @@ export interface PipelineSpec {
10
10
  readonly id: string;
11
11
  readonly passKind: string;
12
12
  readonly variantSet: string | undefined;
13
+ readonly vertexEntry?: string;
14
+ readonly fragmentEntry?: string;
13
15
  };
14
16
  readonly attachments: {
15
17
  readonly colorFormats: readonly GPUTextureFormat[];
@@ -19,6 +19,7 @@
19
19
 
20
20
  import {
21
21
  deriveVertexBufferLayout,
22
+ deriveVertexBufferLayoutFromProjection,
22
23
  deriveVertexLayoutProjection,
23
24
  type VertexLayoutProjection,
24
25
  } from '@forgeax/engine-geometry';
@@ -247,7 +248,11 @@ export function cacheKeyOf(spec: PipelineSpec): string {
247
248
  stripSegment,
248
249
  `vl:${vlDigest}`,
249
250
  renderStateHash(renderState),
250
- ].join(':') + uvSetCountSegment
251
+ ].join(':') +
252
+ uvSetCountSegment +
253
+ (shader.vertexEntry === undefined && shader.fragmentEntry === undefined
254
+ ? ''
255
+ : `:entries:${JSON.stringify([shader.vertexEntry ?? null, shader.fragmentEntry ?? null])}`)
251
256
  );
252
257
  }
253
258
 
@@ -407,25 +412,16 @@ export function buildPipelineDescriptor(
407
412
  ? { shaderUvSetCount: geometry.shaderUvSetCount }
408
413
  : {}),
409
414
  })
410
- : geometry.vertexLayoutProjection.attributes.length === 0
411
- ? []
412
- : [
413
- {
414
- arrayStride: geometry.vertexLayoutProjection.arrayStride,
415
- attributes: geometry.vertexLayoutProjection.attributes.map(
416
- ({ shaderLocation, offset, format }) => ({
417
- shaderLocation,
418
- offset,
419
- format,
420
- }),
421
- ),
422
- },
423
- ];
415
+ : deriveVertexBufferLayoutFromProjection(geometry.vertexLayoutProjection, {
416
+ ...(geometry.shaderUvSetCount !== undefined
417
+ ? { shaderUvSetCount: geometry.shaderUvSetCount }
418
+ : {}),
419
+ });
424
420
 
425
421
  const descriptor: Record<string, unknown> = {
426
422
  vertex: {
427
423
  module: modules.vertex,
428
- entryPoint: modules.vertexEntryPoint ?? 'vs_main',
424
+ entryPoint: spec.shader.vertexEntry ?? modules.vertexEntryPoint ?? 'vs_main',
429
425
  buffers: vertexBuffers,
430
426
  },
431
427
  };
@@ -434,7 +430,7 @@ export function buildPipelineDescriptor(
434
430
  if (attachments.colorFormats.length > 0) {
435
431
  descriptor.fragment = {
436
432
  module: modules.fragment,
437
- entryPoint: modules.fragmentEntryPoint ?? 'fs_main',
433
+ entryPoint: spec.shader.fragmentEntry ?? modules.fragmentEntryPoint ?? 'fs_main',
438
434
  targets: attachments.colorFormats.map((fmt) => {
439
435
  const target: Record<string, unknown> = { format: fmt };
440
436
  if (renderState?.blend !== undefined) {
@@ -18,6 +18,7 @@ import type { DirectionalShadowQuality } from '../components/directional-shadow-
18
18
  import type { EnvironmentGeneration } from '../environment/generation';
19
19
  import type { EnvironmentLifecycle } from '../environment/lifecycle';
20
20
  import type { GpuBuffer } from '../gpu-resource';
21
+ import type { InstanceCollectionId, InstanceResidency } from '../instances';
21
22
  import type { IblBindingInspection } from '../mesh-material-bindings';
22
23
  import type { StandardLightingInspection } from '../pipeline/standard-lighting/inspection';
23
24
  import type { PointShadowInspection } from '../point-shadow-inspection';
@@ -52,6 +53,17 @@ export function worldEntityKey(worldId: number, entityKey: number): number {
52
53
  return worldId * 4294967296 + entityKey; // 2^32 = 4294967296
53
54
  }
54
55
 
56
+ /** Per-renderer projection identity, with entity keys for sprite/fold snapshots. */
57
+ export function instanceCollectionCacheKey(
58
+ worldId: number,
59
+ instance: { readonly cacheKey: number; readonly collectionId?: number },
60
+ ): number {
61
+ // Keep collection entries in a disjoint numeric namespace from the shared
62
+ // legacy sprite/fold cache. Fold keys occupy -1..-(2^32), while ordinary
63
+ // world/entity keys are non-negative; this range starts below both.
64
+ return -(0x1_0000_0000 + worldEntityKey(worldId, instance.collectionId ?? instance.cacheKey));
65
+ }
66
+
55
67
  /**
56
68
  * Cross-frame cache entry for a material bind group whose source material and
57
69
  * all resolved GPU resources are stable. The material snapshot identity is
@@ -289,8 +301,9 @@ export function makeZeroCameraFallbackSnapshot(): CameraSnapshot {
289
301
  * Owned by the `createRenderSystem` closure; advanced once per
290
302
  * `recordFrame` invocation. The `instanceBuffers` map holds the per-entity
291
303
  * GPU storage buffers for Instances-bearing entities (cache key = the
292
- * packed Entity u32 surfaced via `InstancesSnapshot.cacheKey`); entries
293
- * are recreated when the `archVersion` bumps or `byteLength` changes.
304
+ * renderer-owned collection id, with the packed Entity u32 retained only for
305
+ * legacy snapshots); entries are recreated when the `archVersion` bumps or
306
+ * `byteLength` changes.
294
307
  *
295
308
  * feat-20260518-pbr-direct-lighting-mvp M3 / w14 (AC-17 a): the
296
309
  * `warnedZeroLightStandard` flag latches the first-frame warning that
@@ -390,6 +403,10 @@ export interface RenderFrameState {
390
403
  currentDirectionalShadowView: TextureView | null;
391
404
  currentSpotShadowView: TextureView | null;
392
405
  readonly instanceBuffers: Map<number, InstanceBufferCacheEntry>;
406
+ /** Renderer-owned collection owner used for record-stage residency facts. */
407
+ readonly instanceResidency?: Map<InstanceCollectionId, InstanceResidency>;
408
+ /** Stable buffers for renderer-owned collections split by device cap. */
409
+ readonly instanceBufferChunks?: Map<string, InstanceBufferCacheEntry>;
393
410
  /** Per-object ProbeBlendRecord buffers; upload is one atomic 160B write. */
394
411
  readonly probeBlendBuffers: Map<number, ProbeBlendBufferCacheEntry>;
395
412
  /** One renderer-owned record array; each object selects its 256B-aligned lane. */
@@ -1,3 +1,4 @@
1
+ import { uploadMaterialUniforms } from './material-uniforms';
1
2
  // @forgeax/engine-runtime - RenderSystem record stage: frame.
2
3
  // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
3
4
 
@@ -86,6 +87,7 @@ import {
86
87
  type DispatchCounts,
87
88
  type FrameObservationSource,
88
89
  getTextureIdentity,
90
+ instanceCollectionCacheKey,
89
91
  makeZeroCameraFallbackSnapshot,
90
92
  type ReflectionFallbackReadbackRequest,
91
93
  type RenderFrameState,
@@ -1924,7 +1926,11 @@ export function recordFrame(
1924
1926
  // the swap-chain fallback uses its raw storage view. The encoder and
1925
1927
  // sprite PSO must use the same resolved format because WebGPU requires
1926
1928
  // attachment and pipeline target formats to match.
1927
- const splitLdrSprite = computeSplitLdrSprite(validatedOrdered, tonemapActive);
1929
+ const splitLdrSprite = computeSplitLdrSprite(
1930
+ validatedOrdered,
1931
+ tonemapActive,
1932
+ transparentDispatch,
1933
+ );
1928
1934
  let ldrSpritePassView: TextureView | null = null;
1929
1935
  if (splitLdrSprite) {
1930
1936
  const unormViewRes = internals.device.createTextureView(currentTexture, {});
@@ -2154,6 +2160,7 @@ export function recordFrame(
2154
2160
  directionalShadowCacheReuse: directionalShadowCache.reuse,
2155
2161
  ...(profilePhase !== undefined ? { profilePhase } : {}),
2156
2162
  };
2163
+ uploadMaterialUniforms(passCtx);
2157
2164
  const fallbackReadbackRequest = createReflectionFallbackReadbackRequest(internals, frameState);
2158
2165
  if (fallbackReadbackRequest !== undefined) {
2159
2166
  frameState.reflectionFallbackReadback = fallbackReadbackRequest;
@@ -2993,8 +3000,14 @@ function cleanPerFrameCaches(
2993
3000
  // composites matching the write-side keys of #1-#3 — cross-world false eviction
2994
3001
  // is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
2995
3002
  const validatedEntityKeys = new Set<number>(retainedEntityKeys);
3003
+ const validatedInstanceKeys = new Set<number>(retainedEntityKeys);
2996
3004
  for (const v of validated) {
2997
- validatedEntityKeys.add(worldEntityKey(v.source.worldId, v.source.entityKey));
3005
+ const entityKey = worldEntityKey(v.source.worldId, v.source.entityKey);
3006
+ validatedEntityKeys.add(entityKey);
3007
+ validatedInstanceKeys.add(entityKey);
3008
+ if (v.source.instances !== undefined) {
3009
+ validatedInstanceKeys.add(instanceCollectionCacheKey(v.source.worldId, v.source.instances));
3010
+ }
2998
3011
  }
2999
3012
 
3000
3013
  // Clean per-entity material BG cache: drop outer-Map entries whose
@@ -3018,7 +3031,7 @@ function cleanPerFrameCaches(
3018
3031
  // material-handle-based and cross-world collision is semantically
3019
3032
  // correct (same material renders in same fold bucket).
3020
3033
  for (const [key, entry] of frameState.instanceBuffers.entries()) {
3021
- if (!validatedEntityKeys.has(key)) {
3034
+ if (!validatedInstanceKeys.has(key)) {
3022
3035
  if (!entry.buffer.isDestroyed) {
3023
3036
  const r = entry.buffer.destroy();
3024
3037
  if (!r.ok) internals.errorRegistry.fire(r.error);
@@ -3027,6 +3040,28 @@ function cleanPerFrameCaches(
3027
3040
  }
3028
3041
  }
3029
3042
 
3043
+ if (frameState.instanceResidency !== undefined) {
3044
+ const liveCollections = new Set(validated.map((entry) => entry.source.instances?.collectionId));
3045
+ for (const id of frameState.instanceResidency.keys()) {
3046
+ if (!liveCollections.has(id)) frameState.instanceResidency.delete(id);
3047
+ }
3048
+ }
3049
+
3050
+ // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
3051
+ // the authoring API, but retain the numeric owner prefix for eviction.
3052
+ if (frameState.instanceBufferChunks !== undefined) {
3053
+ for (const [key, entry] of frameState.instanceBufferChunks.entries()) {
3054
+ const separator = key.indexOf(':');
3055
+ const ownerKey = separator < 0 ? Number.NaN : Number(key.slice(0, separator));
3056
+ if (validatedInstanceKeys.has(ownerKey)) continue;
3057
+ if (!entry.buffer.isDestroyed) {
3058
+ const r = entry.buffer.destroy();
3059
+ if (!r.ok) internals.errorRegistry.fire(r.error);
3060
+ }
3061
+ frameState.instanceBufferChunks.delete(key);
3062
+ }
3063
+ }
3064
+
3030
3065
  if (frameState.morphBuffers !== undefined) {
3031
3066
  for (const [key, entry] of frameState.morphBuffers.entries()) {
3032
3067
  if (validatedEntityKeys.has(key)) continue;