@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
@@ -17,7 +17,7 @@ import type {
17
17
  PreparedGraphicsResolver,
18
18
  PreparedGraphicsResourceLease,
19
19
  } from '../prepare/prepared-graphics-resolver';
20
- import type { SceneDataTarget } from '../temporal/scene-data';
20
+ import { isSceneDataTarget, type SceneDataTarget } from '../temporal/scene-data';
21
21
  import { createSceneDataCatalog, type SceneDataCatalog } from '../temporal/scene-data-catalog';
22
22
  import {
23
23
  cloneRenderFeaturePlanSignatureSnapshot,
@@ -134,6 +134,8 @@ export interface RenderFeatureFrameInput {
134
134
  readonly worlds: readonly import('@forgeax/engine-ecs').World[];
135
135
  readonly owner: number;
136
136
  readonly frameNumber: number;
137
+ /** Physical render extent for bounded feature dispatch sizing. */
138
+ readonly frameSize?: { readonly width: number; readonly height: number };
137
139
  readonly visibilitySnapshots?: readonly RenderFeatureWorldVisibilitySnapshot[];
138
140
  readonly hiddenEntityReports?: readonly RenderFeatureHiddenEntityReport[];
139
141
  /** Active-pipeline logical targets available to producer-owned features. */
@@ -412,7 +414,7 @@ function asFeatureError(
412
414
  if (error instanceof Error && typeof (error as Partial<RenderError>).code === 'string') {
413
415
  return error as RenderError;
414
416
  }
415
- return new RenderFeatureStageFailedError(identity, order, stage, 'next-frame');
417
+ return new RenderFeatureStageFailedError(identity, order, stage, 'next-frame', error);
416
418
  }
417
419
 
418
420
  function featureErrorForSlot(slot: FeatureSlot, error: RenderError): RenderError {
@@ -431,6 +433,7 @@ function featureErrorForSlot(slot: FeatureSlot, error: RenderError): RenderError
431
433
  slot.order,
432
434
  'plan',
433
435
  'next-frame',
436
+ error,
434
437
  );
435
438
  default:
436
439
  return new RenderFeatureStageFailedError(
@@ -438,6 +441,7 @@ function featureErrorForSlot(slot: FeatureSlot, error: RenderError): RenderError
438
441
  slot.order,
439
442
  'plan',
440
443
  'next-frame',
444
+ error,
441
445
  );
442
446
  }
443
447
  }
@@ -467,6 +471,7 @@ function errorDescriptor(error: RenderError): RenderFeatureErrorDescriptor {
467
471
  order: -1,
468
472
  stage: 'plan',
469
473
  recovery: 'next-frame',
474
+ cause: error,
470
475
  },
471
476
  };
472
477
  }
@@ -606,14 +611,17 @@ function preparePlanResources(
606
611
  : targetHandle(resource.logicalTarget, targets);
607
612
  if (
608
613
  resource.logicalTarget !== undefined &&
609
- (logical === undefined || !isRenderFeatureTargetHandle(logical))
614
+ (logical === undefined ||
615
+ (typeof logical !== 'string' &&
616
+ !isRenderFeatureTargetHandle(logical) &&
617
+ !isSceneDataTarget(logical)))
610
618
  ) {
611
619
  return err(planFailure(slot));
612
620
  }
613
621
  const prepared = gpu.prepareTextureView(
614
622
  resource.name,
615
- resource.resource.value as TextureView,
616
- logical === undefined || !isRenderFeatureTargetHandle(logical) ? undefined : logical,
623
+ resource.resource.value as TextureView | undefined,
624
+ logical === undefined ? undefined : logical,
617
625
  );
618
626
  if (!prepared.ok) return prepared;
619
627
  preparedGpuResources.set(resource.name, {
@@ -1429,7 +1437,10 @@ export function runRenderFeatureFrame(
1429
1437
  () =>
1430
1438
  feature.plan(extracted.value, {
1431
1439
  caps: input.caps,
1432
- frame: { frameNumber: input.frameNumber },
1440
+ frame: {
1441
+ frameNumber: input.frameNumber,
1442
+ ...(input.frameSize === undefined ? {} : input.frameSize),
1443
+ },
1433
1444
  generation: transaction.generation,
1434
1445
  targets,
1435
1446
  sceneData,
@@ -1499,9 +1510,9 @@ export function runRenderFeatureFrame(
1499
1510
  resolver === undefined
1500
1511
  ? undefined
1501
1512
  : (descriptor: import('./prepared-graphics').RenderFeatureGraphicsPassDescriptor) => {
1502
- const resolved = resolveGraphicsSnapshot(resolver, descriptor, transaction.generation);
1503
- if (!resolved.ok) resolver.release();
1504
- return resolved;
1513
+ // A pending pass can be skipped while earlier passes remain usable.
1514
+ // Keep their shared resolver leases alive until submission retires them.
1515
+ return resolveGraphicsSnapshot(resolver, descriptor, transaction.generation);
1505
1516
  };
1506
1517
  const releaseResolver = (): void => {
1507
1518
  if (resolver !== undefined) resolver.release();
@@ -1529,9 +1540,11 @@ export function runRenderFeatureFrame(
1529
1540
  host.setStatus(identity, 'failed', slot.latestError);
1530
1541
  continue;
1531
1542
  }
1532
- const graphicsLeases = projected.value.passes.flatMap(
1533
- (pass) => pass.resolvedGraphics?.leases ?? [],
1534
- );
1543
+ // Include partial allocations from skipped pending passes, even when no pass
1544
+ // completed. Fatal projection failures release the resolver above instead.
1545
+ const graphicsLeases =
1546
+ resolver?.leases ??
1547
+ projected.value.passes.flatMap((pass) => pass.resolvedGraphics?.leases ?? []);
1535
1548
  if (graphicsLeases.length > 0) requiresPreparedResourceKey = true;
1536
1549
  const leases = [...new Set(graphicsLeases), ...(gpu?.retireUntouched() ?? [])];
1537
1550
  const retained = host.retainPreparedGraphics(identity, leases);
@@ -1545,7 +1558,7 @@ export function runRenderFeatureFrame(
1545
1558
  if (feature.onFrameSubmitted !== undefined) {
1546
1559
  submissionSensitivePlans.add(plannedFrame);
1547
1560
  submissionAcknowledgments.set(plannedFrame, () =>
1548
- feature.onFrameSubmitted?.(extracted.value),
1561
+ feature.onFrameSubmitted?.(extracted.value, projected.value),
1549
1562
  );
1550
1563
  }
1551
1564
  plans.push(plannedFrame);
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
  import { SCENE_DATA_TEMPORAL_V1_SCHEMA } from '../../../temporal/scene-data';
3
3
  import { createSceneDataCatalog } from '../../../temporal/scene-data-catalog';
4
4
  import { freezeRenderFeaturePlan } from '../../plan';
5
- import { planMotionBlur } from '../motion-blur-feature';
5
+ import { MOTION_BLUR_COMPUTE_WGSL, planMotionBlur } from '../motion-blur-feature';
6
6
  import { DEFAULT_MOTION_BLUR_PARAMS } from '../motion-blur-params';
7
7
 
8
8
  const caps = {
@@ -64,4 +64,114 @@ describe('Motion Blur feature plan', () => {
64
64
  expect(pass.sampledTargets).not.toContain('motion-depth');
65
65
  expect(catalog.schema).toBe(SCENE_DATA_TEMPORAL_V1_SCHEMA);
66
66
  });
67
+
68
+ it('selects one tile summary and one fused reconstruction on compute-capable targets', () => {
69
+ const computeCaps = {
70
+ rgba16floatRenderable: true,
71
+ compute: true,
72
+ storageBuffer: true,
73
+ storageTexture: true,
74
+ } as never;
75
+ const catalog = createSceneDataCatalog({
76
+ featureIdentity: 'forgeax.motion-blur',
77
+ generation: 4,
78
+ planIdentity: 'forgeax.motion-blur:4',
79
+ rgba16floatRenderable: true,
80
+ });
81
+ const targets = [
82
+ {
83
+ name: 'motion-input',
84
+ kind: 'color' as const,
85
+ format: 'rgba16float' as const,
86
+ sampleCount: 1 as const,
87
+ },
88
+ {
89
+ name: 'motion-output',
90
+ kind: 'color' as const,
91
+ format: 'rgba16float' as const,
92
+ sampleCount: 1 as const,
93
+ },
94
+ ];
95
+ const planned = planMotionBlur(
96
+ DEFAULT_MOTION_BLUR_PARAMS,
97
+ {
98
+ caps: computeCaps,
99
+ frame: { frameNumber: 7, width: 1920, height: 1080 },
100
+ generation: 4,
101
+ targets,
102
+ sceneData: catalog,
103
+ },
104
+ { frameDeltaSeconds: 1 / 60, reset: false },
105
+ );
106
+ expect(planned.ok).toBe(true);
107
+ if (!planned.ok) return;
108
+ const frozen = freezeRenderFeaturePlan('forgeax.motion-blur', planned.value, targets);
109
+ expect(frozen.ok).toBe(true);
110
+ if (!frozen.ok) return;
111
+
112
+ const computeProgram = frozen.value.resources.find(
113
+ (resource) => resource.kind === 'compute-program',
114
+ );
115
+ const tileSummary = frozen.value.resources.find(
116
+ (resource) => resource.kind === 'buffer' && resource.name === 'motion-blur-tile-summary',
117
+ );
118
+ expect(computeProgram?.kind).toBe('compute-program');
119
+ expect(tileSummary).toMatchObject({
120
+ kind: 'buffer',
121
+ size: Math.ceil(1920 / 16) * Math.ceil(1080 / 16) * 32,
122
+ usage: ['storage'],
123
+ });
124
+ expect(frozen.value.passes.map((pass) => pass.name)).toEqual([
125
+ 'motion-blur-tile-summary',
126
+ 'motion-blur',
127
+ ]);
128
+ expect(frozen.value.passes).toHaveLength(2);
129
+ expect(frozen.value.passes[0]).toMatchObject({
130
+ kind: 'compute',
131
+ dispatches: [{ entryPoint: 'tile_summary', workgroups: [120, 68, 1] }],
132
+ });
133
+ expect(frozen.value.passes[1]).toMatchObject({
134
+ kind: 'compute',
135
+ dispatches: [{ entryPoint: 'reconstruct', workgroups: [240, 135, 1] }],
136
+ });
137
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('2.0 * (f32(index) + 0.5) / denominator - 1.0');
138
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let sourcePixel = workgroupBase');
139
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('halfFilteredColor');
140
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('workgroupBarrier()');
141
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('if (!inBounds) {');
142
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('depthReject');
143
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('sourceVelocitySegmentCovers');
144
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('TILE_NEIGHBORHOOD_COUNT');
145
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('tileIntersectsWorkgroupSupport');
146
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('tileNeighborhoodOffset');
147
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('const EDGE_FILL_FACTOR : f32 = 0.99;');
148
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let acceptedColor');
149
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let acceptedAlpha');
150
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let emptyReceiver');
151
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let sourceScale');
152
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let sourceContribution');
153
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let trailScale');
154
+ expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let outputAlpha');
155
+ expect(MOTION_BLUR_COMPUTE_WGSL).not.toContain('TILE_CANDIDATE_OFFSETS');
156
+ expect(MOTION_BLUR_COMPUTE_WGSL).not.toContain('atomic');
157
+ expect(MOTION_BLUR_COMPUTE_WGSL).not.toContain('textureSampleLevel');
158
+ if (computeProgram?.kind === 'compute-program') {
159
+ const entries = computeProgram.program.bindings?.[0]?.entries ?? [];
160
+ expect(entries.find((entry) => entry.binding === 2)?.buffer?.type).toBe('storage');
161
+ expect(entries.find((entry) => entry.binding === 3)?.storageTexture?.access).toBe(
162
+ 'write-only',
163
+ );
164
+ }
165
+ });
166
+
167
+ it('compiles the compute lane WGSL before it reaches a backend', async () => {
168
+ const { compileShader } = await import(
169
+ /* @vite-ignore */
170
+ new URL('../../../../../shader-compiler/dist/index.mjs', import.meta.url).href
171
+ );
172
+ const compiled = await compileShader(MOTION_BLUR_COMPUTE_WGSL, {
173
+ id: 'forgeax::motion-blur-compute',
174
+ });
175
+ expect(compiled.ok, compiled.ok ? undefined : String(compiled.error)).toBe(true);
176
+ });
67
177
  });
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import {
3
3
  DEFAULT_MOTION_BLUR_PARAMS,
4
+ effectiveMotionBlurSampleCount,
5
+ isMotionBlurIntervalValid,
6
+ motionBlurExposureScale,
7
+ motionBlurSampleDelta,
4
8
  motionBlurTemporalDemand,
5
9
  validateMotionBlurParams,
6
10
  } from '../motion-blur-params';
@@ -12,6 +16,7 @@ describe('Motion Blur parameter validation', () => {
12
16
  shutterAngle: 180,
13
17
  maxRadiusPixels: 32,
14
18
  sampleCount: 8,
19
+ targetFps: 60,
15
20
  });
16
21
  });
17
22
 
@@ -32,6 +37,9 @@ describe('Motion Blur parameter validation', () => {
32
37
  ['sampleCount', 3],
33
38
  ['sampleCount', 17],
34
39
  ['sampleCount', 8.5],
40
+ ['targetFps', -1],
41
+ ['targetFps', 240.5],
42
+ ['targetFps', 59.5],
35
43
  ] as const)('returns structured failure for %s=%s', (field, value) => {
36
44
  const result = validateMotionBlurParams({ [field]: value });
37
45
  expect(result.ok).toBe(false);
@@ -45,9 +53,56 @@ describe('Motion Blur parameter validation', () => {
45
53
 
46
54
  it('does no temporal work when omitted or shutter is zero', () => {
47
55
  expect(motionBlurTemporalDemand(undefined)).toBe(false);
48
- expect(motionBlurTemporalDemand({ shutterAngle: 0, maxRadiusPixels: 32, sampleCount: 8 })).toBe(
49
- false,
50
- );
56
+ expect(
57
+ motionBlurTemporalDemand({
58
+ shutterAngle: 0,
59
+ maxRadiusPixels: 32,
60
+ sampleCount: 8,
61
+ targetFps: 60,
62
+ }),
63
+ ).toBe(false);
64
+ expect(
65
+ motionBlurTemporalDemand({
66
+ shutterAngle: 180,
67
+ maxRadiusPixels: 0,
68
+ sampleCount: 8,
69
+ targetFps: 60,
70
+ }),
71
+ ).toBe(false);
51
72
  expect(motionBlurTemporalDemand(DEFAULT_MOTION_BLUR_PARAMS)).toBe(true);
52
73
  });
74
+
75
+ it('rounds authored sample budgets down to the fixed shader tiers', () => {
76
+ expect(effectiveMotionBlurSampleCount(3)).toBe(0);
77
+ expect(effectiveMotionBlurSampleCount(4)).toBe(4);
78
+ expect(effectiveMotionBlurSampleCount(7)).toBe(4);
79
+ expect(effectiveMotionBlurSampleCount(8)).toBe(8);
80
+ expect(effectiveMotionBlurSampleCount(15)).toBe(8);
81
+ expect(effectiveMotionBlurSampleCount(16)).toBe(16);
82
+ });
83
+
84
+ it('rejects unstable intervals and scales displacement to the target rate', () => {
85
+ expect(isMotionBlurIntervalValid(1 / 60)).toBe(true);
86
+ expect(isMotionBlurIntervalValid(0)).toBe(false);
87
+ expect(isMotionBlurIntervalValid(0.101)).toBe(false);
88
+ expect(motionBlurExposureScale(1 / 60, 60)).toBeCloseTo(1);
89
+ expect(motionBlurExposureScale(1 / 120, 60)).toBeCloseTo(2);
90
+ expect(motionBlurExposureScale(1 / 60, 0)).toBe(1);
91
+ expect(motionBlurExposureScale(0, 60)).toBe(0);
92
+ expect(motionBlurExposureScale(1 / 60, 60.5)).toBe(0);
93
+ expect(motionBlurExposureScale(1 / 60, 241)).toBe(0);
94
+ });
95
+
96
+ it('keeps raw render hitches visible before ECS clamp policy', () => {
97
+ expect(motionBlurSampleDelta(1.2, 1, 0.1)).toBeCloseTo(0.2);
98
+ expect(motionBlurSampleDelta(1 / 30, 0, 0.1)).toBeCloseTo(1 / 30);
99
+ expect(isMotionBlurIntervalValid(motionBlurSampleDelta(1.2, 1, 0.1))).toBe(false);
100
+ expect(isMotionBlurIntervalValid(motionBlurSampleDelta(1 / 30, 0, 0.1))).toBe(true);
101
+ expect(motionBlurSampleDelta(undefined, undefined, 0.1)).toBe(0.1);
102
+ });
103
+
104
+ it('fails closed for an explicitly invalid host/replay sample', () => {
105
+ expect(Number.isNaN(motionBlurSampleDelta(Number.NaN, 1, 1 / 60))).toBe(true);
106
+ expect(Number.isNaN(motionBlurSampleDelta(1, Number.POSITIVE_INFINITY, 1 / 60))).toBe(true);
107
+ });
53
108
  });