@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
@@ -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'> = {
@@ -1,206 +1,23 @@
1
- // @forgeax/engine-render - Instances component (per-entity instanced-draw transforms).
2
- //
3
- // Two sources, one component (feat-20260622-chunk-gpu-instancing-sprite-tilemap
4
- // / AC-09; charter P4 consistent abstraction + charter P1 progressive disclosure).
5
- // The `Instances` ECS component has exactly TWO sources of `transforms` data, and
6
- // AI users should hold the boundary explicitly to avoid misreading the ECS view:
7
- //
8
- // Source 1 (EXPLICIT, the only source that lands in ECS) — AI user opt-in.
9
- // The AI user hand-attaches `Instances { transforms }` onto an entity at
10
- // spawn / set time for high-performance instanced draw. `world.get(e,
11
- // Instances)` returns the AI user's payload verbatim (a `Float32Array`
12
- // snapshot of column-major mat4 instances; stride = 16). This is the sole
13
- // code path through which `Instances` ever appears on an entity.
14
- //
15
- // Source 2 (TRANSPARENT, never written back into ECS) — engine record-stage
16
- // fold. When the AI user spawns N independent sprite / tilemap-cell
17
- // entities sharing the same (Layer.value, posZ, materialHandle) sort
18
- // equivalence class, the engine record stage transparently folds the
19
- // equivalent run into ONE `drawIndexed(indexCount, count)` using an
20
- // INTERNAL transient buffer (see `render-system-record.ts` fold operator
21
- // + `render.instancing.foldedDraws` metric, and the `RhiError
22
- // 'instancing-exceeds-uniform-cap'` fallback). The fold operator does
23
- // NOT auto-inject `Instances` onto those entities — `world.get(e,
24
- // Instances)` on a folded entity returns `Result.err` exactly as before.
25
- // The ECS view is preserved bit-for-bit (per-cell / per-sprite entity
26
- // retained, query / despawn / picking unchanged); the optimisation is
27
- // invisible at the component layer (charter P4: consistent abstraction —
28
- // the high-performance path is opt-in via Source 1, the default spawn
29
- // shape stays untouched).
30
- //
31
- // AI user reading this header: do NOT expect engine-side fold to materialise
32
- // as an `Instances` row on your entity. Read the metric (foldedDraws) or the
33
- // RhiError code to observe fold behaviour; never gate logic on a runtime
34
- // `world.get(e, Instances)` lookup after spawn.
35
- //
36
- // Schema: 1 array<f32> field `transforms` carrying packed column-major mat4
37
- // instance transforms (16 f32 per instance, stride = 16). Stride is enforced
38
- // through a TWO-LAYER contract:
39
- //
40
- // 1. AI user set-site: spawn / `world.set` / `world.push` callers pass a
41
- // `Float32Array` (or numeric array) whose `length` is a non-zero
42
- // multiple of 16. The set site is the AI user's responsibility -- the
43
- // engine cannot prove the column-major mat4 packing without the
44
- // caller's intent.
45
- // 2. RenderSystem extract entry: `render-system-extract.ts` performs a
46
- // defensive `transforms.length % 16 === 0` check on every
47
- // `world.get(e, Instances).transforms` snapshot at frame extract time;
48
- // violations route a structured `InstanceTransformsStrideMismatchError`
49
- // (`code: 'instance-transforms-stride-mismatch'`,
50
- // `detail: { actualLength, expectedStride: 16 }`) through the World
51
- // Layer-3 ErrorHandler and the renderable is skipped (fail-fast: the
52
- // malformed length never reaches the GPU upload path).
53
- //
54
- // feat-20260515-buffer-array-vocab-collapse M3 / w16 (decision §2.3 stride
55
- // responsibility migration): the legacy component-level
56
- // stride-declaration option (`{ transforms: 16 }` keyed on the retired
57
- // per-component stride defineComponent option key) was retired because:
58
- // - The ECS layer no longer carries a per-component stride schema (M2 /
59
- // w9 dropped the option from `DefineComponentOptions` -- the SSOT
60
- // moved to the RenderSystem entry + AI user set site).
61
- // - Centralising the check at the consumer (RenderSystem extract) rather
62
- // than the producer (ECS write paths) lets the GPU upload path stay
63
- // trust-the-snapshot; AI users get one structured error per offending
64
- // frame rather than one per write.
65
- //
66
- // feat-20260514-ecs-children-instances-managed-buffer-array M3 / w14:
67
- // migrated from the legacy `{ buffer: 'ref', count: 'u32' }` pair (which
68
- // cross-coupled with the now-deleted `AssetRegistry.createInstancedBuffer`
69
- // pipeline + the `InstancedBufferAsset` POD) to the ECS-managed array path.
70
- // Resize is a `world.push(e, Instances, 'transforms', value)` away (one f32
71
- // at a time) plus a future `world.grow` if a bulk-resize affordance is
72
- // added.
73
- //
74
- // Storage buffer cap-gate (D-5 OOS-08 follow-up):
75
- // - The runtime RenderSystem consumer (`render-system-record.ts`) checks
76
- // `device.caps.storageBuffer` before binding the per-entity transforms;
77
- // `caps.storageBuffer === false` (backend lacking storage buffer support,
78
- // e.g. rhi-wgpu webgl backend) routes a `RhiError` ('feature-not-enabled').
79
- // A future loop owns the per-draw fallback path.
80
- //
81
- // Group transform chained semantics (charter proposition 5 mental migration):
82
- // - When the same entity carries both `Transform` (entity_world) and
83
- // `Instances` (per-instance local transforms), the vertex shader
84
- // composes per instance:
85
- //
86
- // world_position[i] = entity_world * instances_local[i] * vertex_position
87
- //
88
- // - `Instances.transforms[i*16..i*16+15]` is interpreted as a local-space
89
- // transform under the entity, exactly like a child entity's `Transform`
90
- // under `ChildOf { parent }` in the hierarchy system. AI users reuse
91
- // the `parent x local` intuition without a new concept. Set the
92
- // entity's `Transform` to identity to make `instances_local[i]`
93
- // directly world-space.
94
- //
95
- // 4-segment minimum contract (read this header before reaching for
96
- // the source body):
97
- //
98
- // ===== (a) single-component import + spawn example =====
99
- //
100
- // import {
101
- // createRenderer, HANDLE_CUBE,
102
- // MeshFilter, MeshRenderer, Transform,
103
- // Instances, type InstancesData,
104
- // } from '@forgeax/engine-render';
105
- //
106
- // // 1 entity rendering N instanced cubes (16N packed f32, column-major mat4):
107
- // const transforms = new Float32Array(N * 16);
108
- // // ... fill column-major mat4 columns per instance ...
109
- // world.spawn(
110
- // { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
111
- // { component: MeshRenderer, data: {} },
112
- // { component: Instances, data: { transforms } },
113
- // );
114
- //
115
- // ===== (b) packed mat4 layout =====
116
- //
117
- // const transforms = new Float32Array(N * 16); // column-major mat4 per instance
118
- // // instance i occupies floats [i*16 .. i*16+15]:
119
- // // [m00 m10 m20 m30 <- column 0
120
- // // m01 m11 m21 m31 <- column 1
121
- // // m02 m12 m22 m32 <- column 2
122
- // // m03 m13 m23 m33] <- column 3 (translation in m03/m13/m23)
123
- //
124
- // `transforms.length` MUST be a non-zero multiple of 16. AI users gate at
125
- // the set / push site; the RenderSystem extract entry holds the second
126
- // defensive (AC-06: detail carries `{ expectedStride: 16, actualLength }`).
127
- // The instance count is the live snapshot length / 16.
128
- //
129
- // ===== (c) error code consumption (typed property access, no message regex) =====
130
- //
131
- // // Stride violation surfaces through the engine-ecs Layer-3 ErrorHandler
132
- // // when extract reads a malformed snapshot:
133
- // // on('error', (err) => {
134
- // // if (err.code === 'instance-transforms-stride-mismatch') {
135
- // // // err.detail.expectedStride === 16
136
- // // // err.detail.actualLength === <user-supplied length>
137
- // // }
138
- // // });
139
- // //
140
- // // RenderSystem cap-gate (D-5) -> RhiError 'feature-not-enabled' on the
141
- // // RhiErrorListenerRegistry channel:
142
- // // on('error', (err) => {
143
- // // if (err.code === 'feature-not-enabled') {
144
- // // console.warn(err.expected, err.hint);
145
- // // }
146
- // // });
147
- //
148
- // charter mapping: proposition 1 (single import surface
149
- // `import { Instances, type InstancesData } from '@forgeax/engine-render'`);
150
- // proposition 3 (machine-readable schema > prose:
151
- // `{ transforms: 'array<f32>' }` is the SSOT, stride 16 documented here +
152
- // enforced at the RenderSystem entry);
153
- // proposition 4 (explicit failure: stride violation routes a structured
154
- // EcsError, not a silent half-row); proposition 5 (consistent abstraction:
155
- // Instances mirrors Children -- both ride the array-vocab path).
156
- //
157
- // Anchors: requirements §AC-06 (stride 16 + RenderSystem upload path);
158
- // plan-strategy §2.3 (stride responsibility migration to RenderSystem entry
159
- // defensive + AI user set site) + §3.3 stride error path; plan-decisions
160
- // D-1 / D-5.
161
- // OOS-09 (no addChild Commands API) does not apply here; OOS-01 (no
162
- // dangling-entity sweep) does not apply (Instances stores f32 not entity).
163
-
164
1
  import { defineComponent } from '@forgeax/engine-ecs';
165
2
 
166
3
  /**
167
- * Per-entity instanced-draw transforms (ECS component).
168
- *
169
- * Carries a variable-length `array<f32>` of column-major mat4 instance
170
- * transforms (16 f32 per instance, stride = 16; documented above and
171
- * enforced at the RenderSystem extract entry, NOT at the ECS write paths).
172
- *
173
- * The runtime RenderSystem consumer materialises a fresh `Float32Array`
174
- * snapshot on every `world.get(e, Instances).transforms` access (D-4
175
- * no-cache), reads the live count from the snapshot's `length`, and
176
- * uploads the bytes to a per-entity GPU storage buffer in the record stage
177
- * (`render-system-record.ts`).
4
+ * World-owned local transforms for one explicit instanced renderable.
5
+ * Packed column-major mat4 values compose as entityWorld * instanceLocal.
6
+ * ECS owns copying, mutation and entity lifetime, including large arrays.
7
+ * Renderer extraction derives its own snapshot and GPU residency; neither
8
+ * authoring nor glTF SceneAsset lowering requires a Renderer identity.
178
9
  *
179
- * @example Spawn an entity rendering 10000 instanced cubes:
180
- * const transforms = new Float32Array(10000 * 16);
181
- * // ... fill column-major mat4 columns ...
182
- * world.spawn(
183
- * { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
184
- * { component: MeshRenderer, data: { ... } },
185
- * { component: Instances, data: { transforms } },
186
- * );
10
+ * @example
11
+ * const transforms = new Float32Array(20_000 * 16);
12
+ * for (let i = 0; i < 20_000; i++) {
13
+ * transforms[i * 16] = transforms[i * 16 + 5] =
14
+ * transforms[i * 16 + 10] = transforms[i * 16 + 15] = 1;
15
+ * }
16
+ * const entity = world.spawn({ component: Instances, data: { transforms } }).unwrap();
17
+ * world.set(entity, Instances, { transforms }).unwrap();
187
18
  */
188
19
  export const Instances = defineComponent('Instances', {
189
20
  transforms: { type: 'array<f32>' },
190
21
  });
191
22
 
192
- /**
193
- * Type-level hint for `data` at the `Instances` spawn site.
194
- *
195
- * The runtime ECS column shape for `array<f32>` accepts a `Float32Array`
196
- * payload at spawn / set time (the bytes are copied into the BufferPool
197
- * slot). The `transforms` field is documented as `Float32Array` here so
198
- * AI-user IDE autocomplete picks up the typed-array shape.
199
- *
200
- * @example
201
- * import type { InstancesData } from '@forgeax/engine-render';
202
- * const data: InstancesData = { transforms: new Float32Array(N * 16) };
203
- */
204
- export type InstancesData = {
205
- readonly transforms: Float32Array;
206
- };
23
+ export type InstancesData = { readonly transforms: Float32Array };
@@ -1,3 +1,4 @@
1
+ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-cook';
1
2
  // Shared render-extract snapshot contracts.
2
3
  //
3
4
  // The extract orchestrator owns traversal and policy; this leaf owns the
@@ -658,6 +659,7 @@ export interface MaterialSnapshot {
658
659
  * discriminator (M4-T06).
659
660
  */
660
661
  readonly materialShaderId?: string | undefined;
662
+ readonly materialProgramKeys?: Readonly<Record<string, string>> | undefined;
661
663
  /**
662
664
  * Stable source handle for the material asset. Record uses this only for
663
665
  * same-frame assembly reuse; GPU bind-group validity still comes from the
@@ -763,12 +765,9 @@ export interface MaterialSnapshot {
763
765
  * from the legacy `shadingModel` discriminant (feat-20260625 M2 D-3,
764
766
  * finalised in w15; `shadingModel` field removed in tweak-20260701 M1).
765
767
  *
766
- * Extract derives this from the first pass's `renderState.blend !==
767
- * undefined` (post-feat-20260626-collapse: blend presence is the
768
- * single SSOT for "this material is transparent on the geometry
769
- * pipeline cache key"). Multi-pass materials whose mix of opaque +
770
- * transparent passes need finer routing should split into separate
771
- * MaterialAsset entries (the normal forgeax pattern).
768
+ * This describes the primary-pass snapshot for consumers that need a
769
+ * material summary. Actual draw routing and the LDR split read the selected
770
+ * DispatchEntry.renderState, so mixed opaque/transparent Passes remain valid.
772
771
  *
773
772
  * Type is `boolean | undefined` (derived): `undefined` means "no
774
773
  * passes / unknown"; consumers must read `=== true` / `!== true` to
@@ -961,6 +960,7 @@ export interface ExtractFramesOwner {
961
960
  }
962
961
 
963
962
  export interface PreparedExtractContext {
963
+ readonly materialContext?: MaterialCookProgramContext | undefined;
964
964
  readonly assets: AssetRegistry | null | undefined;
965
965
  readonly pipelineState: ExtractPipelineSurface | null | undefined;
966
966
  readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
@@ -49,7 +49,12 @@ import {
49
49
  projectedHeightForCandidate,
50
50
  type SubmissionPlan,
51
51
  } from './batch-topology';
52
- import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
52
+ import {
53
+ GPU_DRIVEN_VIEW_WGSL,
54
+ type GpuDrivenLodSelectionInspection,
55
+ GpuDrivenView,
56
+ selectGpuLodLane,
57
+ } from './view-gpu';
53
58
 
54
59
  const VERTEX_STAGE = 0x1;
55
60
  const FRAGMENT_STAGE = 0x2;
@@ -1038,6 +1043,34 @@ export class GpuDrivenProduction {
1038
1043
  return new GpuDrivenProduction(device, shaderModuleFactory);
1039
1044
  }
1040
1045
 
1046
+ /** Recovery has no frame retry: await generated programs before preparing the graph. */
1047
+ static async forRecoveryDevice(
1048
+ device: RhiDevice,
1049
+ factory: PipelineBuilderShaderModuleFactory,
1050
+ createShaderModule: (descriptor: {
1051
+ code: string;
1052
+ label?: string;
1053
+ }) => Promise<Result<ShaderModule, RhiError>>,
1054
+ ): Promise<Result<GpuDrivenProduction, RhiError>> {
1055
+ const modules = new Map<string, ShaderModule>();
1056
+ for (const descriptor of [
1057
+ { label: 'gpu-driven-view', code: GPU_DRIVEN_VIEW_WGSL },
1058
+ { label: 'gpu-driven-rigid-unlit', code: GPU_DRIVEN_RIGID_UNLIT_WGSL },
1059
+ ]) {
1060
+ const module = await createShaderModule(descriptor);
1061
+ if (!module.ok) return module;
1062
+ modules.set(descriptor.code, module.value);
1063
+ }
1064
+ return ok(
1065
+ new GpuDrivenProduction(device, {
1066
+ createShaderModule: (descriptor) => {
1067
+ const module = modules.get(descriptor.code);
1068
+ return module === undefined ? factory.createShaderModule(descriptor) : ok(module);
1069
+ },
1070
+ }),
1071
+ );
1072
+ }
1073
+
1041
1074
  createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
1042
1075
  return {
1043
1076
  kind: 'scene-table',