@forgeax/engine-render 0.1.31 → 0.1.33

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 (293) hide show
  1. package/README.md +447 -34
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/fxaa-registration.d.ts +4 -0
  5. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  6. package/dist/assembly/lod-observation.d.ts +4 -0
  7. package/dist/assembly/lod-observation.d.ts.map +1 -0
  8. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  9. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  10. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  11. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  12. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  13. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  14. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  15. package/dist/assembly/webgpu-ready.d.ts +3 -2
  16. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  17. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  18. package/dist/bloom-admission.d.ts +13 -0
  19. package/dist/bloom-admission.d.ts.map +1 -0
  20. package/dist/{chunk-FUUO4UND.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-NO4WZCAH.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-NO4WZCAH.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-JRBDO2KB.mjs → chunk-NB2S7ZRX.mjs} +166 -10
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-7MQ3XBSP.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-C3DDSXYK.mjs → chunk-OO3DRYK7.mjs} +171 -230
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-S5UE2T5P.mjs → chunk-PLAXOOX5.mjs} +608 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-PSEQ6QPH.mjs → chunk-VOZWDM3L.mjs} +295 -198
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-MDQJPY3T.mjs → chunk-ZDQWGQW7.mjs} +10 -4
  35. package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
  36. package/dist/components/atmosphere.d.ts +2 -0
  37. package/dist/components/atmosphere.d.ts.map +1 -1
  38. package/dist/components/camera.d.ts +20 -4
  39. package/dist/components/camera.d.ts.map +1 -1
  40. package/dist/components/depth-of-field.d.ts +28 -0
  41. package/dist/components/depth-of-field.d.ts.map +1 -0
  42. package/dist/components/directional-light.d.ts +2 -1
  43. package/dist/components/directional-light.d.ts.map +1 -1
  44. package/dist/components/index.d.ts +1 -0
  45. package/dist/components/index.d.ts.map +1 -1
  46. package/dist/components/mesh-filter.d.ts +6 -5
  47. package/dist/components/mesh-filter.d.ts.map +1 -1
  48. package/dist/components/motion-blur.d.ts +1 -0
  49. package/dist/components/motion-blur.d.ts.map +1 -1
  50. package/dist/construct-renderer.mjs +3746 -837
  51. package/dist/construct-renderer.mjs.map +1 -1
  52. package/dist/device/gpu-residency.d.ts +6 -0
  53. package/dist/device/gpu-residency.d.ts.map +1 -1
  54. package/dist/environment/analytic-fog.d.ts +6 -0
  55. package/dist/environment/analytic-fog.d.ts.map +1 -0
  56. package/dist/errors/render.d.ts +6 -2
  57. package/dist/errors/render.d.ts.map +1 -1
  58. package/dist/extract/camera.d.ts.map +1 -1
  59. package/dist/extract/contracts.d.ts +7 -0
  60. package/dist/extract/contracts.d.ts.map +1 -1
  61. package/dist/extract/environment.d.ts +2 -0
  62. package/dist/extract/environment.d.ts.map +1 -1
  63. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
  64. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
  65. package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
  66. package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
  67. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
  68. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
  69. package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
  70. package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
  71. package/dist/features/error-reporting.d.ts +13 -0
  72. package/dist/features/error-reporting.d.ts.map +1 -0
  73. package/dist/features/host.d.ts +5 -0
  74. package/dist/features/host.d.ts.map +1 -1
  75. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  76. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  77. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  78. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  79. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  80. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  81. package/dist/features/plan.d.ts +7 -4
  82. package/dist/features/plan.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +9 -5
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/render-graph-compute.d.ts.map +1 -1
  86. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  87. package/dist/features/types.d.ts +19 -1
  88. package/dist/features/types.d.ts.map +1 -1
  89. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  90. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  91. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  92. package/dist/gpu-driven/production-raster.d.ts +3 -1
  93. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  94. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  95. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +6 -1
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/index.d.ts +8 -3
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.mjs +6 -5
  101. package/dist/index.mjs.map +1 -1
  102. package/dist/inspection-types.d.ts +78 -2
  103. package/dist/inspection-types.d.ts.map +1 -1
  104. package/dist/instances.d.ts +15 -1
  105. package/dist/instances.d.ts.map +1 -1
  106. package/dist/internal.mjs +6 -6
  107. package/dist/mesh-material-bindings.d.ts +3 -0
  108. package/dist/mesh-material-bindings.d.ts.map +1 -1
  109. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  110. package/dist/pipeline/standard-output/types.d.ts +4 -2
  111. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts +28 -19
  113. package/dist/pipeline/standard-post.d.ts.map +1 -1
  114. package/dist/pipeline-spec.d.ts +2 -2
  115. package/dist/plugin.d.ts.map +1 -1
  116. package/dist/record/frame-snapshot.d.ts +24 -1
  117. package/dist/record/frame-snapshot.d.ts.map +1 -1
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +5 -2
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +6 -5
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/render-context.d.ts +12 -28
  126. package/dist/record/render-context.d.ts.map +1 -1
  127. package/dist/record/shadow-pass.d.ts.map +1 -1
  128. package/dist/record/skybox-post-pass.d.ts +5 -3
  129. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  130. package/dist/record/typed-frame-graph.d.ts +8 -1
  131. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  133. package/dist/render-contract.d.ts +32 -5
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-graph-primitives.d.ts +2 -0
  136. package/dist/render-graph-primitives.d.ts.map +1 -1
  137. package/dist/render-pipeline.d.ts +15 -1
  138. package/dist/render-pipeline.d.ts.map +1 -1
  139. package/dist/render-system-extract-tail.d.ts.map +1 -1
  140. package/dist/render-system-extract.d.ts +26 -5
  141. package/dist/render-system-extract.d.ts.map +1 -1
  142. package/dist/render-system.d.ts +9 -2
  143. package/dist/render-system.d.ts.map +1 -1
  144. package/dist/scene/render-scene.d.ts +2 -0
  145. package/dist/scene/render-scene.d.ts.map +1 -1
  146. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  147. package/dist/temporal/frame.d.ts +7 -0
  148. package/dist/temporal/frame.d.ts.map +1 -1
  149. package/dist/temporal/index.mjs +5 -5
  150. package/dist/temporal/view.d.ts +6 -1
  151. package/dist/temporal/view.d.ts.map +1 -1
  152. package/dist/typed-render-graph-primitives.d.ts +10 -7
  153. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  154. package/dist/volume/component.d.ts +0 -1
  155. package/dist/volume/component.d.ts.map +1 -1
  156. package/dist/volume/passes.d.ts.map +1 -1
  157. package/dist/volume/temporal.d.ts +3 -1
  158. package/dist/volume/temporal.d.ts.map +1 -1
  159. package/package.json +21 -21
  160. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  161. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  162. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  163. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  164. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  165. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  166. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  167. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  168. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  169. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  170. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
  171. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  172. package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
  173. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  174. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  175. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  176. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  177. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  178. package/src/__tests__/instances-store.unit.test.ts +90 -0
  179. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  180. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  181. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +49 -0
  182. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  183. package/src/__tests__/public-surface.test-d.ts +8 -0
  184. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  185. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  186. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  187. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  188. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  189. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  190. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  191. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  192. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  193. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  194. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  195. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  196. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  197. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  198. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  199. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  200. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  201. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  202. package/src/assembly/analytic-fog-registration.ts +14 -0
  203. package/src/assembly/fxaa-registration.ts +17 -0
  204. package/src/assembly/lod-observation.ts +12 -0
  205. package/src/assembly/motion-blur-registration.ts +17 -0
  206. package/src/assembly/renderer-facade.ts +2 -0
  207. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  208. package/src/assembly/webgpu-ready-contract.ts +8 -3
  209. package/src/assembly/webgpu-ready.ts +186 -250
  210. package/src/assembly/webgpu-renderer.ts +30 -45
  211. package/src/bloom-admission.ts +21 -0
  212. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  213. package/src/components/atmosphere.ts +4 -0
  214. package/src/components/camera.ts +66 -10
  215. package/src/components/depth-of-field.ts +45 -0
  216. package/src/components/directional-light.ts +2 -1
  217. package/src/components/index.ts +1 -0
  218. package/src/components/mesh-filter.ts +11 -9
  219. package/src/components/motion-blur.ts +1 -0
  220. package/src/device/gpu-residency.ts +56 -12
  221. package/src/environment/analytic-fog.ts +44 -0
  222. package/src/environment/background.ts +2 -2
  223. package/src/errors/render.ts +14 -1
  224. package/src/extract/camera.ts +42 -9
  225. package/src/extract/contracts.ts +7 -0
  226. package/src/extract/environment.ts +4 -0
  227. package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
  228. package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
  229. package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
  230. package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
  231. package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
  232. package/src/features/error-reporting.ts +78 -0
  233. package/src/features/host.ts +26 -13
  234. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  235. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  236. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  237. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  238. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  239. package/src/features/plan.ts +25 -8
  240. package/src/features/prepared-gpu-work.ts +86 -35
  241. package/src/features/render-graph-compute.ts +13 -5
  242. package/src/features/render-graph-contribution.ts +2 -0
  243. package/src/features/types.ts +18 -1
  244. package/src/gpu-driven/material-artifacts.ts +2 -7
  245. package/src/gpu-driven/prepared-draw.ts +15 -10
  246. package/src/gpu-driven/production-raster.ts +83 -7
  247. package/src/gpu-driven/view-gpu.ts +17 -0
  248. package/src/gpu-scene.ts +188 -21
  249. package/src/index.ts +64 -0
  250. package/src/inspection-types.ts +98 -2
  251. package/src/instances.ts +93 -0
  252. package/src/mesh-material-bindings.ts +4 -0
  253. package/src/pipeline/standard-output/graph.ts +3 -0
  254. package/src/pipeline/standard-output/types.ts +4 -1
  255. package/src/pipeline/standard-post.ts +324 -76
  256. package/src/pipeline-spec.ts +5 -5
  257. package/src/plugin.ts +2 -0
  258. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  259. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  260. package/src/record/frame-snapshot.ts +31 -2
  261. package/src/record/frame-targets.ts +10 -6
  262. package/src/record/frame.ts +202 -28
  263. package/src/record/main-pass-geometry.ts +30 -1
  264. package/src/record/main-pass-sprite-draws.ts +6 -1
  265. package/src/record/main-pass.ts +1 -5
  266. package/src/record/mesh-ssbo.ts +43 -6
  267. package/src/record/render-context.ts +12 -28
  268. package/src/record/shadow-pass.ts +6 -1
  269. package/src/record/skybox-post-pass.ts +122 -196
  270. package/src/record/typed-frame-graph.ts +259 -15
  271. package/src/recovery/render-system-candidate.ts +28 -10
  272. package/src/render-contract.ts +34 -4
  273. package/src/render-graph-primitives.ts +8 -1
  274. package/src/render-pipeline.ts +22 -1
  275. package/src/render-system-extract-tail.ts +103 -13
  276. package/src/render-system-extract.ts +148 -38
  277. package/src/render-system.ts +332 -119
  278. package/src/scene/render-scene.ts +85 -4
  279. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  280. package/src/temporal/frame-coordinator.ts +8 -1
  281. package/src/temporal/frame.ts +20 -0
  282. package/src/temporal/view.ts +13 -2
  283. package/src/typed-render-graph-primitives.ts +124 -48
  284. package/src/volume/component.ts +0 -1
  285. package/src/volume/passes.ts +8 -4
  286. package/src/volume/resources.ts +1 -1
  287. package/src/volume/temporal.ts +11 -0
  288. package/dist/chunk-C3DDSXYK.mjs.map +0 -1
  289. package/dist/chunk-FUUO4UND.mjs.map +0 -1
  290. package/dist/chunk-JRBDO2KB.mjs.map +0 -1
  291. package/dist/chunk-MDQJPY3T.mjs.map +0 -1
  292. package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
  293. package/dist/chunk-S5UE2T5P.mjs.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import type { RhiCaps } from '@forgeax/engine-rhi';
2
2
  import { err, ok } from '@forgeax/engine-types';
3
3
  import { describe, expect, it } from 'vitest';
4
+ import { RenderFeaturePreparationFailedError } from '../errors/render';
4
5
  import {
5
6
  createRenderFeatureHost,
6
7
  runRenderFeatureFrame,
@@ -136,3 +137,88 @@ describe('prepared graphics lifecycle ownership', () => {
136
137
  expect(releases).toBe(2);
137
138
  });
138
139
  });
140
+
141
+ for (const readyFirst of [true, false]) {
142
+ it(`retains partial resolver allocations through pending pipelines (ready pass: ${readyFirst})`, () => {
143
+ let releases = 0;
144
+ const lease = {
145
+ release: () => {
146
+ releases += 1;
147
+ return ok(undefined);
148
+ },
149
+ };
150
+ const feature: RenderFeature<undefined> = {
151
+ identity: 'pending.material',
152
+ extract: () => ok(undefined),
153
+ plan: () =>
154
+ ok({
155
+ resources: ['ready', 'pending'].flatMap((name) => [
156
+ {
157
+ kind: 'graphics-program' as const,
158
+ name,
159
+ program: {
160
+ shader: name,
161
+ vertexLayout: 'none',
162
+ colorFormats: ['rgba8unorm' as const],
163
+ },
164
+ },
165
+ {
166
+ kind: 'graphics-bindings' as const,
167
+ name: `${name}-bindings`,
168
+ program: name,
169
+ values: {},
170
+ },
171
+ ]),
172
+ passes: (readyFirst ? ['ready', 'pending'] : ['pending']).map((name) => ({
173
+ kind: 'raster' as const,
174
+ name,
175
+ colorAttachments: [
176
+ { target: 'swapchain', loadOp: 'load' as const, storeOp: 'store' as const },
177
+ ],
178
+ draws: [
179
+ {
180
+ vertexLayout: 'none' as const,
181
+ vertexData: [],
182
+ program: name,
183
+ bindings: [`${name}-bindings`],
184
+ draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
185
+ },
186
+ ],
187
+ })),
188
+ }),
189
+ };
190
+ const host = createRenderFeatureHost([feature]).unwrap();
191
+ const result = runRenderFeatureFrame(host, {
192
+ worlds: [],
193
+ owner: 0,
194
+ frameNumber: 1,
195
+ caps,
196
+ createPreparedGraphicsResolver: (input) => ({
197
+ leases: [lease],
198
+ release: lease.release,
199
+ resolve: (ref) => {
200
+ const item = input.lookup(ref);
201
+ if (item?.name === 'pending')
202
+ return err(
203
+ new RenderFeaturePreparationFailedError(
204
+ feature.identity,
205
+ 0,
206
+ 'prepare',
207
+ 'pipeline',
208
+ 'pending',
209
+ 'pipeline-pending',
210
+ 'next-frame',
211
+ ),
212
+ );
213
+ return ok({ kind: ref.kind, reference: ref, handle: {} } as never);
214
+ },
215
+ }),
216
+ });
217
+ expect(result.errors, JSON.stringify(result.errors)).toEqual([]);
218
+ expect(releases).toBe(0);
219
+ expect(result.preparedResourceBatches).toHaveLength(1);
220
+ host.markPreparedGraphicsSubmitted(result.preparedResourceBatches);
221
+ host.retirePreparedGraphics(result.preparedResourceBatches);
222
+ expect(releases).toBe(1);
223
+ });
224
+ }
@@ -2,6 +2,7 @@ import type {
2
2
  FrameObservationRequest,
3
3
  FrameReceipt,
4
4
  RenderError as PublicRenderError,
5
+ SunCardinalityError as PublicSunCardinalityError,
5
6
  ReflectionProbeInspection,
6
7
  ReflectionProbeSelectionInspection,
7
8
  RendererState,
@@ -65,6 +66,13 @@ const ssrInspection: SsrSpatialInspection = inspection.ssr;
65
66
  void ssrInspection;
66
67
  type IntentError = Extract<PublicRenderError, { readonly code: 'render-intent-invalid' }>;
67
68
  expectTypeOf<IntentError['detail']>().toEqualTypeOf<RenderIntentInvalidDetail>();
69
+ type PublicSunCardinalityCode = Extract<PublicRenderError['code'], 'sun-cardinality'>;
70
+ expectTypeOf<PublicSunCardinalityCode>().toEqualTypeOf<'sun-cardinality'>();
71
+ expectTypeOf<PublicSunCardinalityError['code']>().toEqualTypeOf<'sun-cardinality'>();
72
+ expectTypeOf<PublicSunCardinalityError['detail']>().toEqualTypeOf<{
73
+ readonly field: 'sun';
74
+ readonly value: number;
75
+ }>();
68
76
  const backendKind: RenderInspection['capabilities']['backendKind'] =
69
77
  inspection.capabilities.backendKind;
70
78
  void backendKind;
@@ -39,6 +39,7 @@ const expectedCodes = [
39
39
  'dynamic-resolution-invalid-parameter',
40
40
  'dynamic-resolution-requires-taa',
41
41
  'dynamic-resolution-timing-unavailable',
42
+ 'motion-blur-invalid-params',
42
43
  'shadow-invalid-config',
43
44
  'equirect-projection-failed',
44
45
  'standard-light-budget-exceeded',
@@ -63,6 +64,7 @@ const expectedCodes = [
63
64
  'render-feature-draw-recording-failed',
64
65
  'environment-source-conflict',
65
66
  'fog-cardinality',
67
+ 'sun-cardinality',
66
68
  'taa-caps-insufficient',
67
69
  'environment-generation-failed',
68
70
  'atmosphere-invalid-parameter',
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
4
+ shouldReportPendingRenderFeatureError,
5
+ } from '../features/error-reporting.js';
6
+
7
+ describe('pending render-feature error reporting', () => {
8
+ it('reports the first failure, suppresses the hot-loop, and reopens the cooldown', () => {
9
+ const lastReportedFrame = new Map<string, number>();
10
+ expect(shouldReportPendingRenderFeatureError(lastReportedFrame, 'feature\0plan', 10)).toBe(
11
+ true,
12
+ );
13
+ expect(shouldReportPendingRenderFeatureError(lastReportedFrame, 'feature\0plan', 11)).toBe(
14
+ false,
15
+ );
16
+ expect(
17
+ shouldReportPendingRenderFeatureError(
18
+ lastReportedFrame,
19
+ 'feature\0plan',
20
+ 10 + PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
21
+ ),
22
+ ).toBe(true);
23
+ });
24
+ });
@@ -89,6 +89,29 @@ function frame(host: RenderFeatureHost, frameNumber: number) {
89
89
  }
90
90
 
91
91
  describe('RenderFeature stage fault recovery lifecycle', () => {
92
+ it('retains the original exception when a feature stage throws', () => {
93
+ const cause = new TypeError('feature plan exploded');
94
+ const host = createRenderFeatureHost(
95
+ [
96
+ {
97
+ identity: 'synthetic.m10.throwing',
98
+ extract: () => ok({ ready: true }),
99
+ plan: () => {
100
+ throw cause;
101
+ },
102
+ },
103
+ ],
104
+ caps,
105
+ ).unwrap();
106
+
107
+ const result = frame(host, 1);
108
+ expect(result.errors[0]).toMatchObject({
109
+ code: 'render-feature-stage-failed',
110
+ detail: { featureIdentity: 'synthetic.m10.throwing', cause },
111
+ });
112
+ expect(host.diagnostics()[0]?.latestError).toMatchObject({ detail: { cause } });
113
+ });
114
+
92
115
  it.each<Fault>([
93
116
  'create',
94
117
  'prepared-resource',
@@ -130,4 +130,35 @@ describe('RenderScene temporal retry boundary', () => {
130
130
  instanceCollections.dispose();
131
131
  }
132
132
  });
133
+
134
+ it('keeps instance motion valid when stable generations reorder', () => {
135
+ const instanceCollections = new InstanceCollectionStore();
136
+ const collection = instanceCollections.create({ transforms: matrices(2, 1) }).unwrap();
137
+ const initialInstances = instanceCollections.snapshot(collection.collectionId).unwrap();
138
+ instanceCollections
139
+ .replace(
140
+ collection.collectionId,
141
+ matrices(2, 9),
142
+ new Uint32Array([
143
+ initialInstances.generations[1] ?? 0,
144
+ initialInstances.generations[0] ?? 0,
145
+ ]),
146
+ )
147
+ .unwrap();
148
+ const reorderedInstances = instanceCollections.snapshot(collection.collectionId).unwrap();
149
+ const scene = new RenderScene();
150
+ scene.setTemporalTracking(true);
151
+ try {
152
+ scene.apply([updateSnapshot(snapshot(7, 1, initialInstances))]);
153
+ submit(scene, 7);
154
+ scene.apply([updateInstances(snapshot(7, 1, reorderedInstances))]);
155
+
156
+ const temporal = scene.temporalSnapshotBySlot(0);
157
+ expect(temporal?.motionValid).toBe(true);
158
+ expect(temporal?.previousInstances?.generations).toEqual(initialInstances.generations);
159
+ expect(temporal?.previousInstances?.transforms).toEqual(initialInstances.transforms);
160
+ } finally {
161
+ instanceCollections.dispose();
162
+ }
163
+ });
133
164
  });
@@ -87,7 +87,8 @@ function temporalResolveFrame(
87
87
  bloom: 'off',
88
88
  bloomThreshold: 1,
89
89
  bloomIntensity: 1,
90
- bloomBlurRadius: 1,
90
+ bloomSoftKnee: 0.5,
91
+ bloomScatter: 0.7,
91
92
  clearColor: [0, 0, 0, 1],
92
93
  temporal: frameState.lastSuccessfulTemporalView,
93
94
  },
@@ -1147,6 +1148,7 @@ describe('renderer successful-submit transaction', () => {
1147
1148
  height: 4,
1148
1149
  frameIndex: 6,
1149
1150
  historyValid: true,
1151
+ sampleTimeSeconds: 1,
1150
1152
  });
1151
1153
  const frameState = {
1152
1154
  temporalGpuState: undefined,
@@ -1160,6 +1162,7 @@ describe('renderer successful-submit transaction', () => {
1160
1162
  height: 4,
1161
1163
  frameIndex: 6,
1162
1164
  historyValid: true,
1165
+ sampleTimeSeconds: 1.016,
1163
1166
  }),
1164
1167
  },
1165
1168
  environmentGeneration,
@@ -1244,6 +1247,7 @@ describe('renderer successful-submit transaction', () => {
1244
1247
  expect(active.valid).toBe(true);
1245
1248
  expect(active.readIndex).toBe(1);
1246
1249
  expect(frameState.lastSuccessfulTemporalView).toBe(lastSuccessfulTemporalView);
1250
+ expect(frameState.lastSuccessfulTemporalView.sampleTimeSeconds).toBe(1);
1247
1251
  expect(environmentLifecycle.inspect().activeSignature).toBe(activeEnvironment.signature);
1248
1252
  expect(environmentLifecycle.inspect().lkgSignature).toBe(activeEnvironment.signature);
1249
1253
  frameState.environmentGeneration = environmentLifecycle
@@ -1257,6 +1261,7 @@ describe('renderer successful-submit transaction', () => {
1257
1261
  height: 8,
1258
1262
  frameIndex: 7,
1259
1263
  historyValid: true,
1264
+ sampleTimeSeconds: 1.016,
1260
1265
  }),
1261
1266
  };
1262
1267
  const retryFrame = temporalResolveFrame(device, scope, frameState, {});
@@ -1273,6 +1278,7 @@ describe('renderer successful-submit transaction', () => {
1273
1278
  expect(gpuCommitAfterSubmit).toEqual([1]);
1274
1279
  expect(frameState.activeTemporalGpuState).not.toBe(active);
1275
1280
  expect(frameState.successfulTemporalFrameIndex).toBe(8);
1281
+ expect(frameState.lastSuccessfulTemporalView?.sampleTimeSeconds).toBe(1.016);
1276
1282
  expect(frameState.temporalGpuState).toBeUndefined();
1277
1283
  expect(frameState.retiringTemporalGpuStates.size).toBe(1);
1278
1284
  expect(publishEnvironment).toHaveBeenCalledTimes(1);
@@ -43,14 +43,14 @@ const entries: readonly FixtureEntry[] = [
43
43
  bindings: '[]',
44
44
  },
45
45
  {
46
- hash: 'fixture-bloom-bright',
47
- wgsl: '// fixture marker: BloomBrightParams\n',
46
+ hash: 'fixture-bloom-downsample',
47
+ wgsl: '// fixture marker: BloomDownsampleParams\n',
48
48
  glsl: '',
49
49
  bindings: '[]',
50
50
  },
51
51
  {
52
- hash: 'fixture-bloom-blur',
53
- wgsl: '// fixture marker: BloomBlurParams\n',
52
+ hash: 'fixture-bloom-upsample',
53
+ wgsl: '// fixture marker: BloomUpsampleParams\n',
54
54
  glsl: '',
55
55
  bindings: '[]',
56
56
  },
@@ -60,6 +60,8 @@ describe('skinned shadow caster', () => {
60
60
  expect(shader).toContain('discard;');
61
61
  expect(shader).toContain('@group(2) @binding(1)');
62
62
  expect(shader).toContain('let skinMatrix = palette[');
63
+ expect(shader).toContain('materialIndex = u32(meshes[meshIndex].temporal.w);');
64
+ expect(shader).not.toContain('bitcast<u32>(meshes[meshIndex].temporal.w)');
63
65
  expect(shader).toContain('@location(13) color : vec4<f32>');
64
66
  expect(shader).toContain('out.color = in.color;');
65
67
  expect(record).toContain('entry.source.skin.byteOffset');
@@ -41,7 +41,8 @@ function camera(): CameraSnapshot {
41
41
  bloom: 'off',
42
42
  bloomThreshold: 1,
43
43
  bloomIntensity: 1,
44
- bloomBlurRadius: 4,
44
+ bloomSoftKnee: 0.5,
45
+ bloomScatter: 0.7,
45
46
  clearColor: [0, 0, 0, 1],
46
47
  };
47
48
  }
@@ -31,7 +31,7 @@ function topology(): RenderPipelineTopology {
31
31
  storageFormat: 'bgra8unorm',
32
32
  viewFormat: 'bgra8unorm-srgb',
33
33
  },
34
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
34
+ camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on', bloomIntensity: 1 },
35
35
  shadow: {
36
36
  directional: { mapSize: 64, cascadeCount: 1 },
37
37
  spotMapSize: 64,
@@ -35,10 +35,15 @@ const dualViewProfile: SurfaceProfile = {
35
35
  hasDisplayEndpoint: true,
36
36
  };
37
37
 
38
+ type TopologyOverrides = Omit<Partial<RenderPipelineTopology>, 'camera'> & {
39
+ readonly camera?: Partial<RenderPipelineTopology['camera']>;
40
+ };
41
+
38
42
  function topology(
39
43
  profile: StandardProfile,
40
- overrides: Partial<RenderPipelineTopology> = {},
44
+ overrides: TopologyOverrides = {},
41
45
  ): RenderPipelineTopology {
46
+ const { camera: cameraOverride, ...rest } = overrides;
42
47
  return {
43
48
  pipelineId: STANDARD_PIPELINE_ID,
44
49
  standardProfile: profile,
@@ -49,7 +54,13 @@ function topology(
49
54
  storageFormat: 'bgra8unorm',
50
55
  viewFormat: 'bgra8unorm-srgb',
51
56
  },
52
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
57
+ camera: {
58
+ tonemap: 'aces-filmic',
59
+ antialias: 'fxaa',
60
+ bloom: 'on',
61
+ bloomIntensity: 1,
62
+ ...cameraOverride,
63
+ },
53
64
  shadow: {
54
65
  directional: { mapSize: 64, cascadeCount: 1 },
55
66
  spotMapSize: 64,
@@ -65,7 +76,7 @@ function topology(
65
76
  },
66
77
  featureTopologySignature: 'none',
67
78
  gpuDrivenTopologySignature: '',
68
- ...overrides,
79
+ ...rest,
69
80
  };
70
81
  }
71
82
 
@@ -109,7 +120,7 @@ function standardLightingFor(resolvedTopology: RenderPipelineTopology) {
109
120
  return standardLighting;
110
121
  }
111
122
 
112
- async function build(profile: StandardProfile, overrides?: Partial<RenderPipelineTopology>) {
123
+ async function build(profile: StandardProfile, overrides?: TopologyOverrides) {
113
124
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
114
125
  const resolvedTopology = topology(profile, overrides);
115
126
  const histogram = graph.createBuffer('standard-auto-exposure-histogram', { size: 1024 });
@@ -232,7 +243,7 @@ function importedTaaHistory(graph: RenderGraphBuilder<RenderPipelineFrame>) {
232
243
  };
233
244
  }
234
245
 
235
- async function buildTaa(profile: StandardProfile, overrides?: Partial<RenderPipelineTopology>) {
246
+ async function buildTaa(profile: StandardProfile, overrides?: TopologyOverrides) {
236
247
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
237
248
  const history = importedTaaHistory(graph);
238
249
  const resolvedTopology = topology(profile, overrides);
@@ -542,9 +553,8 @@ describe('forgeax::standard graph', () => {
542
553
  expect(result.ok).toBe(true);
543
554
  if (!result.ok) return;
544
555
  const names = result.value.inspect().passes.map((pass) => pass.name);
545
- expect(names).not.toContain('bloom-bright');
546
- expect(names).not.toContain('bloom-blur-h');
547
- expect(names).not.toContain('bloom-blur-v');
556
+ expect(names.some((name) => name.startsWith('bloom-downsample-'))).toBe(false);
557
+ expect(names.some((name) => name.startsWith('bloom-upsample-'))).toBe(false);
548
558
  expect(names).not.toContain('bloom-composite');
549
559
  expect(names).toContain('taa-resolve');
550
560
  expect(names).toContain('output-transform');
@@ -37,6 +37,16 @@ describe('TemporalView reset matrix', () => {
37
37
  expect(resolveTemporalReset(previous, next)).toBeUndefined();
38
38
  });
39
39
 
40
+ it('applies camera reset reasons to motion-blur-only off views', () => {
41
+ const previous = createTemporalView({ ...base().input, antialias: 'none' });
42
+ const next = createTemporalView({
43
+ ...previous.input,
44
+ antialias: 'none',
45
+ viewIdentity: 'secondary',
46
+ });
47
+ expect(resolveTemporalReset(previous, next)).toBe('view-switch');
48
+ });
49
+
40
50
  it('treats an explicit off commit as a first-frame boundary when TAA resumes', () => {
41
51
  const previous = base();
42
52
  const off = createTemporalView({
@@ -130,7 +130,8 @@ describe('TemporalView v1', () => {
130
130
  bloom: 'disabled',
131
131
  bloomThreshold: 1,
132
132
  bloomIntensity: 0,
133
- bloomBlurRadius: 0,
133
+ bloomSoftKnee: 0.5,
134
+ bloomScatter: 0.7,
134
135
  clearColor: [0, 0, 0, 1] as const,
135
136
  } as unknown as CameraSnapshot;
136
137
  const light = {
@@ -32,6 +32,10 @@ import { standardTemporalPostOrder } from '../temporal/standard-scene-data';
32
32
 
33
33
  let device: RhiDevice;
34
34
 
35
+ type TopologyOverrides = Omit<Partial<RenderPipelineTopology>, 'camera'> & {
36
+ readonly camera?: Partial<RenderPipelineTopology['camera']>;
37
+ };
38
+
35
39
  beforeAll(async () => {
36
40
  const adapter = await rhi.requestAdapter();
37
41
  if (!adapter.ok) throw adapter.error;
@@ -42,8 +46,9 @@ beforeAll(async () => {
42
46
 
43
47
  function topology(
44
48
  pipelineId: 'forgeax::standard',
45
- overrides: Partial<RenderPipelineTopology> = {},
49
+ overrides: TopologyOverrides = {},
46
50
  ): RenderPipelineTopology {
51
+ const { camera: cameraOverride, ...rest } = overrides;
47
52
  return {
48
53
  pipelineId,
49
54
  config: undefined,
@@ -53,7 +58,13 @@ function topology(
53
58
  storageFormat: 'bgra8unorm',
54
59
  viewFormat: 'bgra8unorm-srgb',
55
60
  },
56
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
61
+ camera: {
62
+ tonemap: 'aces-filmic',
63
+ antialias: 'fxaa',
64
+ bloom: 'on',
65
+ bloomIntensity: 1,
66
+ ...cameraOverride,
67
+ },
57
68
  shadow: {
58
69
  directional: { mapSize: 1024, cascadeCount: 4 },
59
70
  spotMapSize: 1024,
@@ -69,7 +80,7 @@ function topology(
69
80
  },
70
81
  featureTopologySignature: 'none',
71
82
  gpuDrivenTopologySignature: '',
72
- ...overrides,
83
+ ...rest,
73
84
  };
74
85
  }
75
86
 
@@ -877,7 +888,12 @@ describe('typed built-in pipeline topology', () => {
877
888
 
878
889
  it('keeps zero-shutter Motion Blur free of temporal target and passes', () => {
879
890
  expect(
880
- isMotionBlurTemporalDemand({ shutterAngle: 0, maxRadiusPixels: 32, sampleCount: 8 }),
891
+ isMotionBlurTemporalDemand({
892
+ shutterAngle: 0,
893
+ maxRadiusPixels: 32,
894
+ sampleCount: 8,
895
+ targetFps: 60,
896
+ }),
881
897
  ).toBe(false);
882
898
  const compiled = compile(
883
899
  standardPipeline,
@@ -42,7 +42,9 @@ describe('volumetric optics CPU oracle', () => {
42
42
 
43
43
  it('converts the shared integrated-light basis once before normalized HG', () => {
44
44
  expect(integrateShader).toContain('const VOLUME_LIGHT_PHASE_SCALE : f32 = FOUR_PI;');
45
- expect(integrateShader).toContain('phase *\n VOLUME_LIGHT_PHASE_SCALE *');
45
+ expect(integrateShader.match(/phase\s*\*\s*VOLUME_LIGHT_PHASE_SCALE\s*\*/g) ?? []).toHaveLength(
46
+ 1,
47
+ );
46
48
  });
47
49
 
48
50
  it('keeps empty density at identity transmittance', () => {