@forgeax/engine-render 0.1.34 → 0.1.36

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 (273) hide show
  1. package/README.md +69 -3
  2. package/dist/assembly/device-feature-admission.d.ts +2 -2
  3. package/dist/assembly/host-contract.d.ts +3 -2
  4. package/dist/assembly/host-contract.d.ts.map +1 -1
  5. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  6. package/dist/assembly/render-target-host.d.ts +5 -0
  7. package/dist/assembly/render-target-host.d.ts.map +1 -1
  8. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  9. package/dist/assembly/webgpu-renderer.d.ts +1 -0
  10. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  12. package/dist/authoring.mjs +2 -4
  13. package/dist/authoring.mjs.map +1 -1
  14. package/dist/chunk-2OZAS3WU.mjs +3489 -0
  15. package/dist/chunk-2OZAS3WU.mjs.map +1 -0
  16. package/dist/{chunk-UI5SFODX.mjs → chunk-BZQ4CDRF.mjs} +2338 -1991
  17. package/dist/chunk-BZQ4CDRF.mjs.map +1 -0
  18. package/dist/{chunk-MKDIT2EM.mjs → chunk-CJOV2XLO.mjs} +216 -261
  19. package/dist/chunk-CJOV2XLO.mjs.map +1 -0
  20. package/dist/chunk-D6WSBXJ2.mjs +1842 -0
  21. package/dist/chunk-D6WSBXJ2.mjs.map +1 -0
  22. package/dist/{chunk-HEB564WF.mjs → chunk-DRF6OULX.mjs} +4 -4
  23. package/dist/{chunk-HEB564WF.mjs.map → chunk-DRF6OULX.mjs.map} +1 -1
  24. package/dist/{chunk-OCDLEI6I.mjs → chunk-OB5WAFYH.mjs} +117 -5
  25. package/dist/chunk-OB5WAFYH.mjs.map +1 -0
  26. package/dist/{chunk-ODZWR6JQ.mjs → chunk-OEYKKMXG.mjs} +2042 -27
  27. package/dist/chunk-OEYKKMXG.mjs.map +1 -0
  28. package/dist/chunk-RIPAEGI6.mjs +48 -0
  29. package/dist/chunk-RIPAEGI6.mjs.map +1 -0
  30. package/dist/{chunk-ELJFJ2BU.mjs → chunk-X2KA6WHM.mjs} +29 -4
  31. package/dist/chunk-X2KA6WHM.mjs.map +1 -0
  32. package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
  33. package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
  34. package/dist/cloud/frame-context.d.ts +2 -2
  35. package/dist/cloud/frame-context.d.ts.map +1 -1
  36. package/dist/construct-renderer.mjs +936 -2140
  37. package/dist/construct-renderer.mjs.map +1 -1
  38. package/dist/device/device-scope.d.ts.map +1 -1
  39. package/dist/device/gpu-residency.d.ts +19 -19
  40. package/dist/device/gpu-residency.d.ts.map +1 -1
  41. package/dist/errors/render.d.ts +3 -2
  42. package/dist/errors/render.d.ts.map +1 -1
  43. package/dist/extract/material-context.d.ts +7 -0
  44. package/dist/extract/material-context.d.ts.map +1 -0
  45. package/dist/features/host.d.ts +5 -0
  46. package/dist/features/host.d.ts.map +1 -1
  47. package/dist/features/particle-mesh-layout.d.ts +19 -0
  48. package/dist/features/particle-mesh-layout.d.ts.map +1 -0
  49. package/dist/features/plan.d.ts +2 -0
  50. package/dist/features/plan.d.ts.map +1 -1
  51. package/dist/features/types.d.ts +8 -0
  52. package/dist/features/types.d.ts.map +1 -1
  53. package/dist/glyph-text-layout-system.d.ts +1 -1
  54. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  55. package/dist/gpu-driven/production-raster.d.ts +1 -1
  56. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  57. package/dist/gpu-driven/shadow-ownership.d.ts +3 -3
  58. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -1
  59. package/dist/gpu-driven/shadow-views.d.ts +1 -1
  60. package/dist/gpu-driven/shadow-views.d.ts.map +1 -1
  61. package/dist/gpu-driven/view-gpu.d.ts +1 -1
  62. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  63. package/dist/ibl/frame-resources.d.ts.map +1 -1
  64. package/dist/ibl/skylight-bind-group.d.ts +4 -4
  65. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.mjs +542 -9
  69. package/dist/index.mjs.map +1 -1
  70. package/dist/instances.d.ts +3 -4
  71. package/dist/instances.d.ts.map +1 -1
  72. package/dist/internal.mjs +7 -8
  73. package/dist/internal.mjs.map +1 -1
  74. package/dist/pbr-pipeline.d.ts +3 -3
  75. package/dist/pbr-pipeline.d.ts.map +1 -1
  76. package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
  77. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
  78. package/dist/pipeline/standard-output/resources.d.ts +2 -2
  79. package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
  80. package/dist/publication/contract.d.ts +77 -0
  81. package/dist/publication/contract.d.ts.map +1 -0
  82. package/dist/publication/dependencies.d.ts +7 -0
  83. package/dist/publication/dependencies.d.ts.map +1 -0
  84. package/dist/publication/prepare-geometry.d.ts +9 -0
  85. package/dist/publication/prepare-geometry.d.ts.map +1 -0
  86. package/dist/publication/programs.d.ts +5 -0
  87. package/dist/publication/programs.d.ts.map +1 -0
  88. package/dist/publication/publisher.d.ts +34 -0
  89. package/dist/publication/publisher.d.ts.map +1 -0
  90. package/dist/publication/receiver.d.ts +29 -0
  91. package/dist/publication/receiver.d.ts.map +1 -0
  92. package/dist/publication/resource-scope.d.ts +18 -0
  93. package/dist/publication/resource-scope.d.ts.map +1 -0
  94. package/dist/publication/targets.d.ts +33 -0
  95. package/dist/publication/targets.d.ts.map +1 -0
  96. package/dist/publication/video.d.ts +5 -0
  97. package/dist/publication/video.d.ts.map +1 -0
  98. package/dist/record/fold-buckets.d.ts +3 -3
  99. package/dist/record/fold-buckets.d.ts.map +1 -1
  100. package/dist/record/frame-snapshot.d.ts +6 -6
  101. package/dist/record/frame-snapshot.d.ts.map +1 -1
  102. package/dist/record/frame.d.ts +5 -4
  103. package/dist/record/frame.d.ts.map +1 -1
  104. package/dist/record/helpers.d.ts +2 -2
  105. package/dist/record/helpers.d.ts.map +1 -1
  106. package/dist/record/main-pass-geometry.d.ts +2 -2
  107. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  108. package/dist/record/main-pass-material.d.ts +7 -9
  109. package/dist/record/main-pass-material.d.ts.map +1 -1
  110. package/dist/record/main-pass-sprite-draws.d.ts +3 -16
  111. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  112. package/dist/record/main-pass.d.ts.map +1 -1
  113. package/dist/record/prepared-material-bindings.d.ts +2 -3
  114. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  115. package/dist/record/render-context.d.ts +4 -2
  116. package/dist/record/render-context.d.ts.map +1 -1
  117. package/dist/record/shadow-pass.d.ts +2 -2
  118. package/dist/record/shadow-pass.d.ts.map +1 -1
  119. package/dist/record/sprite-instance-buffer.d.ts +31 -0
  120. package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
  121. package/dist/record/target-capture-graph.d.ts +19 -0
  122. package/dist/record/target-capture-graph.d.ts.map +1 -0
  123. package/dist/record/typed-frame-graph.d.ts +2 -13
  124. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  125. package/dist/record/view-ubo.d.ts.map +1 -1
  126. package/dist/record/volume-params.d.ts +2 -2
  127. package/dist/record/volume-params.d.ts.map +1 -1
  128. package/dist/recovery/render-system-candidate.d.ts +5 -12
  129. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  130. package/dist/render-contract.d.ts +4 -1
  131. package/dist/render-contract.d.ts.map +1 -1
  132. package/dist/render-system-extract-tail.d.ts.map +1 -1
  133. package/dist/render-system-extract.d.ts +2 -0
  134. package/dist/render-system-extract.d.ts.map +1 -1
  135. package/dist/render-system-presentation.d.ts +2 -2
  136. package/dist/render-system-presentation.d.ts.map +1 -1
  137. package/dist/render-system-projections.d.ts +3 -2
  138. package/dist/render-system-projections.d.ts.map +1 -1
  139. package/dist/render-system-residency.d.ts +2 -2
  140. package/dist/render-system-residency.d.ts.map +1 -1
  141. package/dist/render-system.d.ts +3 -7
  142. package/dist/render-system.d.ts.map +1 -1
  143. package/dist/scene/render-scene.d.ts +8 -4
  144. package/dist/scene/render-scene.d.ts.map +1 -1
  145. package/dist/scene/render-source.d.ts +42 -0
  146. package/dist/scene/render-source.d.ts.map +1 -0
  147. package/dist/scene/source-systems.d.ts +6 -0
  148. package/dist/scene/source-systems.d.ts.map +1 -0
  149. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  150. package/dist/systems/skin-palette-types.d.ts +10 -0
  151. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  152. package/dist/systems/transparent-dispatch.d.ts +2 -2
  153. package/dist/systems/transparent-dispatch.d.ts.map +1 -1
  154. package/dist/systems/transparent-sort-config.d.ts +2 -1
  155. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  156. package/dist/temporal/index.mjs +6 -7
  157. package/dist/typed-shadow-passes.d.ts.map +1 -1
  158. package/package.json +21 -21
  159. package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
  160. package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
  161. package/src/__tests__/device-scope-lifecycle.unit.test.ts +26 -0
  162. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +3 -4
  163. package/src/__tests__/gpu-driven-production.integration.test.ts +18 -1
  164. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +23 -0
  165. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
  166. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +0 -1
  167. package/src/__tests__/material-vertex-texture.browser.test.ts +7 -0
  168. package/src/__tests__/material-vertex-texture.dawn.test.ts +7 -0
  169. package/src/__tests__/material-vertex-texture.fixture.ts +31 -0
  170. package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
  171. package/src/__tests__/physical-clearcoat.integration.test.ts +23 -23
  172. package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
  173. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +5 -0
  174. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  175. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +107 -5
  176. package/src/__tests__/runtime-material-program.integration.test.ts +79 -0
  177. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +23 -3
  178. package/src/assembly/device-feature-admission.ts +2 -2
  179. package/src/assembly/host-contract.ts +5 -2
  180. package/src/assembly/recovery/renderer-recover.ts +9 -0
  181. package/src/assembly/render-target-host.ts +57 -47
  182. package/src/assembly/renderer-facade.ts +3 -1
  183. package/src/assembly/webgpu-ready.ts +1 -1
  184. package/src/assembly/webgpu-renderer.ts +101 -70
  185. package/src/assembly/webgpu-vertex-layouts.ts +2 -9
  186. package/src/cloud/frame-context.ts +2 -2
  187. package/src/device/device-scope.ts +3 -0
  188. package/src/device/gpu-residency.ts +26 -25
  189. package/src/errors/render.ts +3 -1
  190. package/src/extract/material-context.ts +21 -0
  191. package/src/features/host.ts +13 -5
  192. package/src/features/particle-mesh-layout.ts +25 -0
  193. package/src/features/plan.ts +2 -0
  194. package/src/features/types.ts +8 -0
  195. package/src/glyph-text-layout-system.ts +2 -2
  196. package/src/gpu-driven/production-raster.ts +9 -3
  197. package/src/gpu-driven/shadow-ownership.ts +3 -3
  198. package/src/gpu-driven/shadow-views.ts +27 -2
  199. package/src/gpu-driven/view-gpu.ts +5 -0
  200. package/src/ibl/frame-resources.ts +0 -1
  201. package/src/ibl/skylight-bind-group.ts +13 -25
  202. package/src/index.ts +9 -0
  203. package/src/instances.ts +9 -6
  204. package/src/pbr-pipeline.ts +15 -18
  205. package/src/pipeline/standard-output/lut-gpu.ts +3 -3
  206. package/src/pipeline/standard-output/resources.ts +2 -2
  207. package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
  208. package/src/publication/__tests__/features.integration.test.ts +173 -0
  209. package/src/publication/__tests__/programs.unit.test.ts +25 -0
  210. package/src/publication/__tests__/publication.integration.test.ts +415 -0
  211. package/src/publication/__tests__/targets.integration.test.ts +120 -0
  212. package/src/publication/__tests__/video.browser.test.ts +50 -0
  213. package/src/publication/contract.ts +104 -0
  214. package/src/publication/dependencies.ts +54 -0
  215. package/src/publication/prepare-geometry.ts +94 -0
  216. package/src/publication/programs.ts +45 -0
  217. package/src/publication/publisher.ts +645 -0
  218. package/src/publication/receiver.ts +297 -0
  219. package/src/publication/resource-scope.ts +31 -0
  220. package/src/publication/targets.ts +182 -0
  221. package/src/publication/video.ts +33 -0
  222. package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
  223. package/src/record/__tests__/prepared-runtime-material.integration.test.ts +134 -0
  224. package/src/record/__tests__/sprite-material-abi.unit.test.ts +8 -10
  225. package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
  226. package/src/record/fold-buckets.ts +8 -5
  227. package/src/record/frame-snapshot.ts +6 -6
  228. package/src/record/frame.ts +87 -28
  229. package/src/record/helpers.ts +2 -2
  230. package/src/record/main-pass-geometry.ts +21 -2
  231. package/src/record/main-pass-material.ts +37 -33
  232. package/src/record/main-pass-sprite-draws.ts +6 -152
  233. package/src/record/main-pass.ts +8 -7
  234. package/src/record/prepared-material-bindings.ts +16 -37
  235. package/src/record/render-context.ts +4 -2
  236. package/src/record/shadow-pass.ts +65 -7
  237. package/src/record/sprite-instance-buffer.ts +155 -0
  238. package/src/record/target-capture-graph.ts +258 -0
  239. package/src/record/typed-frame-graph.ts +11 -207
  240. package/src/record/view-ubo.ts +1 -1
  241. package/src/record/volume-params.ts +6 -6
  242. package/src/recovery/render-system-candidate.ts +10 -18
  243. package/src/render-contract.ts +6 -1
  244. package/src/render-system-extract-tail.ts +48 -23
  245. package/src/render-system-extract.ts +11 -2
  246. package/src/render-system-presentation.ts +2 -2
  247. package/src/render-system-projections.ts +3 -2
  248. package/src/render-system-residency.ts +2 -2
  249. package/src/render-system.ts +82 -54
  250. package/src/scene/render-scene.ts +110 -100
  251. package/src/scene/render-source.ts +90 -0
  252. package/src/scene/source-systems.ts +33 -0
  253. package/src/systems/skin-palette-allocator.ts +1 -0
  254. package/src/systems/skin-palette-types.ts +11 -0
  255. package/src/systems/transparent-dispatch.ts +2 -2
  256. package/src/systems/transparent-sort-config.ts +3 -1
  257. package/src/typed-shadow-passes.ts +36 -5
  258. package/dist/chunk-BLXGONR2.mjs.map +0 -1
  259. package/dist/chunk-ELJFJ2BU.mjs.map +0 -1
  260. package/dist/chunk-ILQ5MEZF.mjs +0 -32
  261. package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
  262. package/dist/chunk-MKDIT2EM.mjs.map +0 -1
  263. package/dist/chunk-OCDLEI6I.mjs.map +0 -1
  264. package/dist/chunk-ODZWR6JQ.mjs.map +0 -1
  265. package/dist/chunk-RXZ3RGOU.mjs +0 -53
  266. package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
  267. package/dist/chunk-SHFUFJ5N.mjs +0 -1781
  268. package/dist/chunk-SHFUFJ5N.mjs.map +0 -1
  269. package/dist/chunk-UI5SFODX.mjs.map +0 -1
  270. package/dist/chunk-UINHLCZA.mjs +0 -2389
  271. package/dist/chunk-UINHLCZA.mjs.map +0 -1
  272. package/dist/chunk-WEX45HKH.mjs +0 -1524
  273. package/dist/chunk-WEX45HKH.mjs.map +0 -1
@@ -76,26 +76,26 @@ describe('Standard clearcoat root contract', () => {
76
76
  );
77
77
 
78
78
  // The canonical seven texture pairs occupy bindings 1..14. IBL remains
79
- // at 15..21 and transmission at 22..23; authored physical maps append at
79
+ // at 15..20 and transmission at 21..22; authored physical maps append at
80
80
  // their canonical fixed slots (26, 27, 30, 31), followed by the
81
81
  // scene-index row at binding 46. Compacting the authored root here shifts WGSL IBL bindings and
82
82
  // causes Dawn to reject the pipeline before any pixels are submitted.
83
- expect(descriptor.entries).toHaveLength(30);
83
+ expect(descriptor.entries).toHaveLength(29);
84
84
  expect(descriptor.entries.slice(0, 15).map((entry) => entry.binding)).toEqual(
85
85
  Array.from({ length: 15 }, (_, binding) => binding),
86
86
  );
87
87
  expect(descriptor.entries[15]?.texture).toMatchObject({ viewDimension: 'cube' });
88
88
  expect(descriptor.entries[17]?.texture).toMatchObject({ viewDimension: 'cube' });
89
- expect(descriptor.entries[22]?.sampler).toMatchObject({ type: 'filtering' });
90
- expect(descriptor.entries[23]?.texture).toMatchObject({ viewDimension: '2d' });
91
- expect(descriptor.entries.slice(24, 28).map((entry) => entry.binding)).toEqual([
89
+ expect(descriptor.entries[21]?.sampler).toMatchObject({ type: 'filtering' });
90
+ expect(descriptor.entries[22]?.texture).toMatchObject({ viewDimension: '2d' });
91
+ expect(descriptor.entries.slice(23, 27).map((entry) => entry.binding)).toEqual([
92
92
  26, 27, 30, 31,
93
93
  ]);
94
- expect(descriptor.entries[29]).toMatchObject({
94
+ expect(descriptor.entries[28]).toMatchObject({
95
95
  binding: 47,
96
96
  texture: { sampleType: 'float', viewDimension: 'cube' },
97
97
  });
98
- expect(descriptor.entries[28]).toMatchObject({
98
+ expect(descriptor.entries[27]).toMatchObject({
99
99
  binding: 46,
100
100
  visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
101
101
  buffer: { type: 'read-only-storage', hasDynamicOffset: false },
@@ -122,7 +122,7 @@ describe('Standard clearcoat root contract', () => {
122
122
  // PBR layouts keep the producer-owned scene-material row at binding 46,
123
123
  // even when an authored schema has no numeric parameters.
124
124
  expect(descriptor.entries.map((entry) => entry.binding)).toEqual([
125
- ...Array.from({ length: 10 }, (_, binding) => binding),
125
+ ...Array.from({ length: 9 }, (_, binding) => binding),
126
126
  46,
127
127
  47,
128
128
  ]);
@@ -144,43 +144,43 @@ describe('Standard clearcoat root contract', () => {
144
144
  );
145
145
 
146
146
  expect(descriptor.entries.map((entry) => entry.binding)).toEqual([
147
- ...Array.from({ length: 16 }, (_, binding) => binding),
147
+ ...Array.from({ length: 15 }, (_, binding) => binding),
148
148
  46,
149
149
  ]);
150
- expect(descriptor.entries[10]).toMatchObject({
150
+ expect(descriptor.entries[9]).toMatchObject({
151
151
  sampler: { type: 'non-filtering' },
152
152
  });
153
- expect(descriptor.entries[11]).toMatchObject({
153
+ expect(descriptor.entries[10]).toMatchObject({
154
154
  texture: { sampleType: 'unfilterable-float', viewDimension: '2d' },
155
155
  });
156
- expect(descriptor.entries[12]).toMatchObject({
156
+ expect(descriptor.entries[11]).toMatchObject({
157
157
  sampler: { type: 'filtering' },
158
158
  });
159
- expect(descriptor.entries[13]).toMatchObject({
159
+ expect(descriptor.entries[12]).toMatchObject({
160
160
  texture: { sampleType: 'float', viewDimension: '2d' },
161
161
  });
162
- expect(descriptor.entries[14]).toMatchObject({
162
+ expect(descriptor.entries[13]).toMatchObject({
163
163
  sampler: { type: 'non-filtering' },
164
164
  });
165
- expect(descriptor.entries[15]).toMatchObject({
165
+ expect(descriptor.entries[14]).toMatchObject({
166
166
  texture: { sampleType: 'unfilterable-float', viewDimension: '2d' },
167
167
  });
168
168
 
169
169
  // The medium is a project material artifact with an explicit empty
170
170
  // paramSchema. Its per-shader user region is therefore the reserved UBO
171
- // at binding 0, followed by IBL 1..7, transmission 8..9, and Surface
172
- // resources 10..15. Keep this source assertion next to the descriptor
171
+ // at binding 0, followed by IBL 1..6, transmission 7..8, and Surface
172
+ // resources 9..14. Keep this source assertion next to the descriptor
173
173
  // assertion so a future injection-order change cannot leave a compiled
174
174
  // WGSL binding map out of sync with the runtime BGL.
175
175
  const source = readFileSync(
176
176
  resolve(import.meta.dirname, '../../../shader/src/single-layer-medium.wgsl'),
177
177
  'utf8',
178
178
  );
179
- expect(source).toContain('@group(1) @binding(10) var surfaceRawDepthSampler');
180
- expect(source).toContain('@group(1) @binding(11) var surfaceRawDepthTexture');
181
- expect(source).toContain('@group(1) @binding(12) var surfaceNearestLayerSampler');
182
- expect(source).toContain('@group(1) @binding(13) var surfaceNearestLayerTexture');
183
- expect(source).toContain('@group(1) @binding(14) var surfaceNearestDepthSampler');
184
- expect(source).toContain('@group(1) @binding(15) var surfaceNearestDepthTexture');
179
+ expect(source).toContain('@group(1) @binding(9) var surfaceRawDepthSampler');
180
+ expect(source).toContain('@group(1) @binding(10) var surfaceRawDepthTexture');
181
+ expect(source).toContain('@group(1) @binding(11) var surfaceNearestLayerSampler');
182
+ expect(source).toContain('@group(1) @binding(12) var surfaceNearestLayerTexture');
183
+ expect(source).toContain('@group(1) @binding(13) var surfaceNearestDepthSampler');
184
+ expect(source).toContain('@group(1) @binding(14) var surfaceNearestDepthTexture');
185
185
  });
186
186
  });
@@ -0,0 +1,40 @@
1
+ import { STANDARD_MATERIAL_PARAM_SCHEMA } from '@forgeax/engine-types';
2
+ import { expect, it } from 'vitest';
3
+ import { STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES } from '../assembly/device-feature-admission';
4
+ import { buildBindGroupLayoutDescriptor, buildPbrViewBglEntries } from '../pbr-pipeline';
5
+ import type { PipelineSpec } from '../pipeline-spec';
6
+
7
+ it('fits the physical carrier and extended-lighting view within the admitted device budget', () => {
8
+ const textureFields = new Set([
9
+ 'baseColorTexture',
10
+ 'metallicRoughnessTexture',
11
+ 'normalTexture',
12
+ 'emissiveTexture',
13
+ 'occlusionTexture',
14
+ 'clearcoatTexture',
15
+ 'clearcoatRoughnessTexture',
16
+ 'clearcoatNormalTexture',
17
+ 'transmissionTexture',
18
+ 'thicknessTexture',
19
+ ]);
20
+ const spec: PipelineSpec = {
21
+ shader: { id: 'budget::physical', passKind: 'forward', variantSet: undefined },
22
+ attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
23
+ geometry: { topology: 'triangle-list', vertexLayout: {} },
24
+ renderState: undefined,
25
+ };
26
+ const entries = [
27
+ ...buildPbrViewBglEntries({ storageBuffer: true, extendedLighting: true }),
28
+ ...buildBindGroupLayoutDescriptor(spec, {
29
+ kind: 'pbr-material-merged',
30
+ materialParamSchema: STANDARD_MATERIAL_PARAM_SCHEMA.filter(
31
+ (field) => field.type !== 'texture2d' || textureFields.has(field.name),
32
+ ),
33
+ caps: { storageBuffer: true, transmissionBackdrop: true },
34
+ }).entries,
35
+ ].filter((entry) => (entry.visibility & 2) !== 0);
36
+ expect(entries.filter((entry) => entry.sampler !== undefined).length).toBeLessThanOrEqual(16);
37
+ expect(entries.filter((entry) => entry.texture !== undefined).length).toBeLessThanOrEqual(
38
+ STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES,
39
+ );
40
+ });
@@ -139,6 +139,11 @@ describe('recovery candidate preparation contract', () => {
139
139
  expect(rendererSource).not.toMatch(/\n\s*materialShaderPipelineCache\.(get|set|has)\(/);
140
140
  });
141
141
 
142
+ it('forks the validated shader catalog onto a recovery device', () => {
143
+ expect(rendererSource).toContain('activeCatalog.forkForDevice(getShaderModuleAdapter())');
144
+ expect(rendererSource).toContain('state === candidateShaderState && activeCatalog !== null');
145
+ });
146
+
142
147
  it('resolves recovery variants from the candidate device capabilities', () => {
143
148
  const start = rendererSource.indexOf('const resolveCachedMaterialShaderVariantSet');
144
149
  const end = rendererSource.indexOf('const findMaterialShaderManifestEntry', start);
@@ -15,6 +15,7 @@ import type {
15
15
  } from '../index';
16
16
 
17
17
  const expectedCodes = [
18
+ 'render-publication-invalid',
18
19
  'auto-exposure-invalid-parameter',
19
20
  'auto-exposure-capability-unavailable',
20
21
  'auto-exposure-stale-generation',
@@ -1,10 +1,14 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
3
4
  import { type RhiDevice, RhiError } from '@forgeax/engine-rhi';
4
- import type {
5
- MaterialShaderManifestEntry,
6
- MaterialShaderManifestVariant,
7
- ShaderCatalog,
5
+ import {
6
+ createMaterialProgramArtifactReceipt,
7
+ type MaterialShaderManifestEntry,
8
+ type MaterialShaderManifestVariant,
9
+ type ShaderCatalog,
10
+ ShaderRegistry,
11
+ type ShaderRegistryDevice,
8
12
  } from '@forgeax/engine-shader';
9
13
  import { describe, expect, it, vi } from 'vitest';
10
14
  import { deviceOptionsForAdapter } from '../assembly/device-feature-admission';
@@ -128,6 +132,104 @@ fn cloud_direct_solar_factorX() -> f32 {
128
132
  });
129
133
  });
130
134
 
135
+ it('selects the replacement device capability variant after a metadata-only fork', async () => {
136
+ const uniformReceipt = createMaterialProgramArtifactReceipt({
137
+ schema: [],
138
+ directEntry: 'vs_uniform',
139
+ sceneIndexEntry: 'vs_scene_uniform',
140
+ vertexInputs: [],
141
+ layoutIdentity: 'uniform',
142
+ });
143
+ const storageReceipt = createMaterialProgramArtifactReceipt({
144
+ schema: [],
145
+ directEntry: 'vs_storage',
146
+ sceneIndexEntry: 'vs_scene_storage',
147
+ vertexInputs: [],
148
+ layoutIdentity: 'storage',
149
+ });
150
+ const entry: MaterialShaderManifestEntry = {
151
+ identifier: 'game::variant',
152
+ sourcePath: 'variant.wgsl',
153
+ composedWgsl: 'fallback',
154
+ paramSchema: '[]',
155
+ uvSetCount: 3,
156
+ variants: [
157
+ {
158
+ definesKey: 'STORAGE_BUFFER_AVAILABLE=false',
159
+ defines: { STORAGE_BUFFER_AVAILABLE: false },
160
+ composedWgsl: 'uniform-variant',
161
+ receipt: uniformReceipt,
162
+ },
163
+ {
164
+ definesKey: '',
165
+ defines: { STORAGE_BUFFER_AVAILABLE: true },
166
+ composedWgsl: 'storage-variant',
167
+ receipt: storageReceipt,
168
+ },
169
+ ],
170
+ };
171
+ const manifestUrl = URL.createObjectURL(
172
+ new Blob([JSON.stringify({ entries: [], materialShaders: [entry] })]),
173
+ );
174
+ const registryDevice = (): ShaderRegistryDevice => ({
175
+ createShaderModule: () => ({ ok: true, value: {} as never }),
176
+ });
177
+ const device = (storageBuffer: boolean): RhiDevice =>
178
+ ({
179
+ caps: {
180
+ backendKind: 'webgpu',
181
+ storageBuffer,
182
+ rgba16floatRenderable: true,
183
+ samplerAliasing: true,
184
+ },
185
+ limits: {
186
+ maxStorageBuffersPerShaderStage: storageBuffer ? 8 : 0,
187
+ maxSampledTexturesPerShaderStage: 32,
188
+ maxTextureArrayLayers: 64,
189
+ maxUniformBuffersPerShaderStage: 4,
190
+ },
191
+ }) as unknown as RhiDevice;
192
+
193
+ try {
194
+ const active = new ShaderRegistry({ device: registryDevice(), manifestUrl });
195
+ const activeAssets = new AssetRegistry(active);
196
+ await prepareMaterialShaders(device(false), () => active, activeAssets, new Map());
197
+ active.installMaterialArtifact('game::custom', {
198
+ source: 'custom-variant',
199
+ paramSchema: [],
200
+ receipt: uniformReceipt,
201
+ });
202
+ const activeArtifact = active.findMaterialArtifact('game::variant');
203
+ expect(activeArtifact.ok).toBe(true);
204
+ if (!activeArtifact.ok) return;
205
+ expect(activeArtifact.value.source).toBe('uniform-variant');
206
+ expect(activeArtifact.value.receipt).toEqual(uniformReceipt);
207
+
208
+ URL.revokeObjectURL(manifestUrl);
209
+ const candidate = active.forkForDevice(registryDevice());
210
+ const candidateAssets = new AssetRegistry(candidate);
211
+ const uvSetCounts = new Map<string, number>();
212
+ await prepareMaterialShaders(device(true), () => candidate, candidateAssets, uvSetCounts);
213
+ const candidateArtifact = candidate.findMaterialArtifact('game::variant');
214
+ expect(candidateArtifact.ok).toBe(true);
215
+ if (!candidateArtifact.ok) return;
216
+ expect(candidateArtifact.value.source).toBe('storage-variant');
217
+ expect(candidateArtifact.value.receipt).toEqual(storageReceipt);
218
+ expect(uvSetCounts.get('game::variant')).toBe(3);
219
+ expect(candidate.findMaterialArtifact('game::custom').ok).toBe(false);
220
+
221
+ const customArtifact = active.findMaterialArtifact('game::custom');
222
+ expect(customArtifact.ok).toBe(true);
223
+ if (!customArtifact.ok) return;
224
+ candidate.installMaterialArtifact('game::custom', customArtifact.value);
225
+ const candidateCustom = candidate.findMaterialArtifact('game::custom');
226
+ expect(candidateCustom.ok && candidateCustom.value.source).toBe('custom-variant');
227
+ expect(activeArtifact.value.source).toBe('uniform-variant');
228
+ } finally {
229
+ URL.revokeObjectURL(manifestUrl);
230
+ }
231
+ });
232
+
131
233
  it('requests the extended-lighting sampled-texture limit when the adapter supports it', () => {
132
234
  const features = new Set<GPUFeatureName>();
133
235
  expect(
@@ -137,7 +239,7 @@ fn cloud_direct_solar_factorX() -> f32 {
137
239
  } as never),
138
240
  ).toEqual({
139
241
  requiredFeatures: [],
140
- requiredLimits: { maxSampledTexturesPerShaderStage: 22 },
242
+ requiredLimits: { maxSampledTexturesPerShaderStage: 23 },
141
243
  });
142
244
  expect(
143
245
  deviceOptionsForAdapter({
@@ -0,0 +1,79 @@
1
+ import { AssetRegistry, HANDLE_CUBE, RuntimeMaterialValue } from '@forgeax/engine-assets-runtime';
2
+ import { World } from '@forgeax/engine-ecs';
3
+ import { Transform } from '@forgeax/engine-scene';
4
+ import { ShaderRegistry } from '@forgeax/engine-shader';
5
+ import type { MaterialAsset } from '@forgeax/engine-types';
6
+ import { expect, it } from 'vitest';
7
+ import {
8
+ MATERIAL_CONTEXT,
9
+ materialRecordFixture,
10
+ } from '../../../assets-runtime/src/__tests__/fixtures/material-publication';
11
+ import { MeshFilter, MeshRenderer } from '../components';
12
+ import { extractFrames, resolveMaterialSnapshot } from '../render-system-extract';
13
+
14
+ it('keeps cooked Forward and ShadowCaster programs when World values change or a catalogue entry is replaced', () => {
15
+ const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
16
+ const world = new World();
17
+ const record = materialRecordFixture({
18
+ parameters: [{ name: 'roughness', type: 'f32' }],
19
+ values: { roughness: 0.8 },
20
+ passes: [
21
+ { name: 'Forward', program: { module: 'game::material' } },
22
+ { name: 'ShadowCaster', program: { module: 'game::shadow' } },
23
+ ],
24
+ });
25
+ const base: MaterialAsset = { kind: 'material', ...record.resolved };
26
+ assets.catalog(record.guid, base).unwrap();
27
+ if (!record.sourceClosure || !record.parameterContract)
28
+ throw new Error('Missing cooked metadata');
29
+ assets.recordMaterialReadiness(record.guid, {
30
+ status: 'Ready',
31
+ guid: record.guid,
32
+ materialGuid: record.guid,
33
+ publicationGeneration: record.publicationGeneration,
34
+ specializationKey: record.specializationKey,
35
+ artifactDigest: record.artifactDigest,
36
+ sourceClosure: record.sourceClosure,
37
+ parameterContract: record.parameterContract,
38
+ record,
39
+ programs: record.programs,
40
+ });
41
+ const handle = world.internSharedRef('MaterialAsset', base);
42
+ world
43
+ .spawn(
44
+ { component: Transform, data: {} },
45
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
46
+ { component: MeshRenderer, data: { materials: [handle] } },
47
+ )
48
+ .unwrap();
49
+ world.update(0).unwrap();
50
+ const frameRead = () =>
51
+ extractFrames([world], 0, assets, undefined, undefined, {
52
+ materialContext: MATERIAL_CONTEXT,
53
+ cull: 'none',
54
+ }).renderables[0]?.materials[0];
55
+ const read = () =>
56
+ resolveMaterialSnapshot(handle, world, assets, undefined, undefined, MATERIAL_CONTEXT);
57
+ const before = read();
58
+ expect(frameRead()?.materialProgramKeys).toEqual(before.materialProgramKeys);
59
+ expect(Object.keys(before.materialProgramKeys ?? {})).toEqual(['Forward', 'ShadowCaster']);
60
+ const content = world
61
+ .spawn({
62
+ component: RuntimeMaterialValue,
63
+ data: {
64
+ asset: handle,
65
+ parameter: 'roughness',
66
+ kind: 0,
67
+ value: [0.25],
68
+ },
69
+ })
70
+ .unwrap();
71
+ expect(read().roughness).toBe(0.25);
72
+ expect(frameRead()?.materialProgramKeys).toEqual(before.materialProgramKeys);
73
+ expect(read().materialProgramKeys).toEqual(before.materialProgramKeys);
74
+ assets.catalog(record.guid, { ...base, values: { roughness: 0.6 } }).unwrap();
75
+ expect(read().materialProgramKeys).toEqual(before.materialProgramKeys);
76
+ world.despawn(content).unwrap();
77
+ expect(read().roughness).toBeCloseTo(0.8);
78
+ expect(read().materialProgramKeys).toEqual(before.materialProgramKeys);
79
+ });
@@ -1,4 +1,4 @@
1
- import { DEFAULT_VERTEX_ATTRIBUTE_MAP, deriveVertexBufferLayout } from '@forgeax/engine-geometry';
1
+ import { deriveVertexBufferLayout } from '@forgeax/engine-geometry';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import {
4
4
  BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS,
@@ -17,11 +17,31 @@ const FLOAT32_COMPONENT_COUNT = {
17
17
  describe('prepared feature vertex layout SSOT', () => {
18
18
  it('derives the mesh vertex stream from geometry layout ownership', () => {
19
19
  const [expected] = toGpuVertexBufferLayouts(
20
- deriveVertexBufferLayout(DEFAULT_VERTEX_ATTRIBUTE_MAP),
20
+ deriveVertexBufferLayout({
21
+ position: new Float32Array(),
22
+ normal: new Float32Array(),
23
+ uv: new Float32Array(),
24
+ tangent: new Float32Array(),
25
+ uv1: new Float32Array(),
26
+ color: new Float32Array(),
27
+ }),
21
28
  );
22
29
  const [actual] = MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS;
23
30
 
24
- expect(actual).toEqual({ ...expected, stepMode: 'vertex' });
31
+ if (expected === undefined) throw new Error('Missing geometry descriptor');
32
+ expect(actual).toEqual({
33
+ ...expected,
34
+ stepMode: 'vertex',
35
+ attributes: expected.attributes.map((attribute) => ({
36
+ ...attribute,
37
+ shaderLocation:
38
+ attribute.shaderLocation === 6
39
+ ? 15
40
+ : attribute.shaderLocation === 13
41
+ ? 14
42
+ : attribute.shaderLocation,
43
+ })),
44
+ });
25
45
  });
26
46
 
27
47
  it.each([
@@ -21,12 +21,12 @@ export function transmissionBackdropAvailable(limit: number | undefined): boolea
21
21
  }
22
22
  /**
23
23
  * The complete Standard physical root can declare all optional texture slots.
24
- * Its material group therefore needs one more sampled-texture entry than the
24
+ * Its material group therefore needs two more sampled-texture entries than the
25
25
  * extended-lighting topology alone. Request this ceiling when the adapter can
26
26
  * provide it; lower-capability devices remain fail-closed at material
27
27
  * pipeline admission instead of receiving an invalid WebGPU layout.
28
28
  */
29
- export const STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES = 22;
29
+ export const STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES = 23;
30
30
 
31
31
  export interface DeviceFeatureAdmissionOptions {
32
32
  readonly gpuPassTiming?: object | undefined;
@@ -14,6 +14,7 @@ import type { ObservationUnavailableError, RenderError } from '../errors/render'
14
14
  import type { RenderFeature, RenderFeatureDiagnostics } from '../features/types';
15
15
  import type { GpuDrivenProductionInspection, LodOcclusionInspection } from '../inspection-types';
16
16
  import type { MeshMaterialBindingObservation } from '../mesh-material-bindings';
17
+ import type { PublishedRenderFrameInput } from '../publication/contract';
17
18
  import type { FrameObservation, FrameObservationOptions } from '../record/frame';
18
19
  import type { CurrentGraphTarget, GraphTargetCaptureRequest } from '../record/frame-snapshot';
19
20
  import type {
@@ -120,7 +121,9 @@ export interface RendererHostImplementation {
120
121
  readonly lodOcclusion: LodOcclusionInspection | undefined;
121
122
  readonly gpuDriven: GpuDrivenProductionInspection;
122
123
  };
123
- drawFrame(request: RenderFrameInput): RenderResult<FrameReceipt, RhiError | RenderError>;
124
+ drawFrame(
125
+ request: RenderFrameInput | PublishedRenderFrameInput,
126
+ ): RenderResult<FrameReceipt, RhiError | RenderError>;
124
127
  /** Arm one upcoming frame for receipt-bound color observations. */
125
128
  requestObservation?: (
126
129
  domains: readonly import('../render-contract').FrameObservationDomain[],
@@ -154,7 +157,7 @@ export interface RendererLegacyHostAdapter {
154
157
  attachScene(world: World): RenderResult<void, RhiError>;
155
158
  detachScene(world: World): void;
156
159
  draw(
157
- worldsOrRequest: readonly World[] | RenderFrameInput,
160
+ worldsOrRequest: readonly World[] | RenderFrameInput | PublishedRenderFrameInput,
158
161
  options?: DrawOwnerOptions,
159
162
  ): RenderResult<void | FrameReceipt, RhiError | RenderError>;
160
163
  observeCurrentFrame(
@@ -10,6 +10,7 @@ import type {
10
10
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
11
11
  import type { MaterialShaderManifestEntry, ShaderCatalog } from '@forgeax/engine-shader';
12
12
  import type { Handle, ParamSchemaEntry } from '@forgeax/engine-types';
13
+ import { AssetError } from '@forgeax/engine-types';
13
14
  import { DeviceScope } from '../../device/device-scope';
14
15
  import { GpuResidencyCache } from '../../device/gpu-residency';
15
16
  import { RecoverError, type RecoverFailure } from '../../errors/recover';
@@ -399,6 +400,14 @@ export function createRendererRecovery(deps: RendererRecoveryDependencies): Rend
399
400
  device,
400
401
  adaptMipmapShaderModuleFactory(internals.pack.createShaderModule),
401
402
  (world, pod) => {
403
+ if ('resolveAsset' in world)
404
+ return err(
405
+ new AssetError({
406
+ code: 'asset-not-found',
407
+ expected: 'source-owned environment asset allocation',
408
+ hint: 'publish the environment asset from its source owner before GPU preparation',
409
+ }),
410
+ );
402
411
  const handle: Handle<'EquirectAsset', 'shared'> = world.allocSharedRef(
403
412
  'EquirectAsset',
404
413
  pod,
@@ -79,6 +79,11 @@ interface ReadbackRecord {
79
79
  export interface RenderTargetHost {
80
80
  /** The logical owner for every target lifecycle operation. */
81
81
  readonly owner: 'renderer';
82
+ /** Logical descriptions for the source publication boundary; no physical state escapes. */
83
+ descriptions(): readonly {
84
+ readonly target: RenderTarget;
85
+ readonly descriptor: RenderTargetDescriptor;
86
+ }[];
82
87
  createRenderTarget(descriptor: RenderTargetDescriptor): RenderResult<RenderTarget, RenderError>;
83
88
  resizeRenderTarget(
84
89
  target: RenderTarget,
@@ -231,53 +236,51 @@ function makePhysical(
231
236
  const faceViews: TextureView[] = [];
232
237
  const depthTextures: Texture[] = [];
233
238
  const depthViews: TextureView[] = [];
234
- if (descriptor.shape === 'cube') {
235
- for (let face = 0; face < layers; face += 1) {
236
- const depth = device.createTexture({
237
- label: `render-target.${generation}.depth.${face}`,
238
- size: {
239
- width: descriptor.width,
240
- height: descriptor.height,
241
- depthOrArrayLayers: descriptor.sampleCount === 4 ? 1 : layers,
242
- },
243
- mipLevelCount: 1,
244
- sampleCount: descriptor.sampleCount,
245
- dimension: '2d',
246
- format: 'depth24plus-stencil8',
247
- usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
248
- viewFormats: undefined,
249
- textureBindingViewDimension: undefined,
250
- });
251
- if (!depth.ok)
252
- return {
253
- ok: false,
254
- error: new RenderTargetOperationFailedError({
255
- operation: 'create',
256
- stage: 'allocation',
257
- generation,
258
- cause: depth.error,
259
- recovery: 'retry',
260
- }),
261
- };
262
- const depthView = device.createTextureView(depth.value, {
263
- dimension: '2d',
264
- baseArrayLayer: descriptor.sampleCount === 4 ? 0 : face,
265
- arrayLayerCount: 1,
266
- });
267
- if (!depthView.ok)
268
- return {
269
- ok: false,
270
- error: new RenderTargetOperationFailedError({
271
- operation: 'create',
272
- stage: 'allocation',
273
- generation,
274
- cause: depthView.error,
275
- recovery: 'retry',
276
- }),
277
- };
278
- depthTextures.push(depth.value);
279
- depthViews.push(depthView.value);
280
- }
239
+ for (let face = 0; face < layers; face += 1) {
240
+ const depth = device.createTexture({
241
+ label: `render-target.${generation}.depth.${face}`,
242
+ size: {
243
+ width: descriptor.width,
244
+ height: descriptor.height,
245
+ depthOrArrayLayers: descriptor.sampleCount === 4 ? 1 : layers,
246
+ },
247
+ mipLevelCount: 1,
248
+ sampleCount: descriptor.sampleCount,
249
+ dimension: '2d',
250
+ format: 'depth24plus-stencil8',
251
+ usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
252
+ viewFormats: undefined,
253
+ textureBindingViewDimension: undefined,
254
+ });
255
+ if (!depth.ok)
256
+ return {
257
+ ok: false,
258
+ error: new RenderTargetOperationFailedError({
259
+ operation: 'create',
260
+ stage: 'allocation',
261
+ generation,
262
+ cause: depth.error,
263
+ recovery: 'retry',
264
+ }),
265
+ };
266
+ const depthView = device.createTextureView(depth.value, {
267
+ dimension: '2d',
268
+ baseArrayLayer: descriptor.sampleCount === 4 ? 0 : face,
269
+ arrayLayerCount: 1,
270
+ });
271
+ if (!depthView.ok)
272
+ return {
273
+ ok: false,
274
+ error: new RenderTargetOperationFailedError({
275
+ operation: 'create',
276
+ stage: 'allocation',
277
+ generation,
278
+ cause: depthView.error,
279
+ recovery: 'retry',
280
+ }),
281
+ };
282
+ depthTextures.push(depth.value);
283
+ depthViews.push(depthView.value);
281
284
  }
282
285
  if (descriptor.sampleCount === 4) {
283
286
  for (let face = 0; face < layers; face += 1) {
@@ -405,6 +408,13 @@ export function createRenderTargetHost(options: RenderTargetHostOptions = {}): R
405
408
 
406
409
  return Object.freeze({
407
410
  owner: 'renderer' as const,
411
+ descriptions() {
412
+ return [...targets].map((target) => {
413
+ const state = owner.inspect(target);
414
+ if (!state.ok) throw state.error;
415
+ return { target, descriptor: state.value.candidate?.descriptor ?? state.value.descriptor };
416
+ });
417
+ },
408
418
  createRenderTarget(
409
419
  descriptor: RenderTargetDescriptor,
410
420
  ): RenderResult<RenderTarget, RenderError> {
@@ -1,3 +1,4 @@
1
+ import { RenderPublicationError } from '../publication/contract';
1
2
  // Public renderer facade and profile validation.
2
3
  // This module narrows the lifetime-heavy host implementation to the stable
3
4
  // lease/receipt contract and owns the profile/error projection helpers.
@@ -64,7 +65,8 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
64
65
  cause: structuredRendererCause(cause, 'renderer-event'),
65
66
  });
66
67
  const drawError = (cause: RhiError | RenderError): RenderError => {
67
- if (cause instanceof MotionBlurValidationError) return cause;
68
+ if (cause instanceof MotionBlurValidationError || cause instanceof RenderPublicationError)
69
+ return cause;
68
70
  if (cause instanceof RendererOperationError) return cause;
69
71
  if (cause instanceof RendererContractFailureError) {
70
72
  return new RendererOperationError('device-operation-failed', {
@@ -1439,7 +1439,7 @@ export async function buildReadyWebGPU(
1439
1439
  // instances) and the 4-slot pipeline layout in one call, throwing on each
1440
1440
  // device.createBindGroupLayout / device.createPipelineLayout failure.
1441
1441
  // material BGL now carries 24 entries (canonical material 0..14 + Skylight
1442
- // 15..21 + transmission 22..23); authored physical maps append after the
1442
+ // 15..20 + transmission 21..22); authored physical maps append after the
1443
1443
  // engine-owned injections. The pipeline layout itself stays at 4 slots, no
1444
1444
  // @group(4) allocated (D-5 round-4 fix for the round-2 maxBindGroups=4
1445
1445
  // BLOCKER). The standalone unlit BGL is the 15-entry user region exposed by