@forgeax/engine-render 0.1.32 → 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 (250) hide show
  1. package/README.md +379 -33
  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 +1 -1
  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-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-JCIRYQ4V.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/mesh-filter.d.ts +6 -5
  41. package/dist/components/mesh-filter.d.ts.map +1 -1
  42. package/dist/components/motion-blur.d.ts +1 -0
  43. package/dist/components/motion-blur.d.ts.map +1 -1
  44. package/dist/construct-renderer.mjs +2865 -526
  45. package/dist/construct-renderer.mjs.map +1 -1
  46. package/dist/device/gpu-residency.d.ts +6 -0
  47. package/dist/device/gpu-residency.d.ts.map +1 -1
  48. package/dist/environment/analytic-fog.d.ts +6 -0
  49. package/dist/environment/analytic-fog.d.ts.map +1 -0
  50. package/dist/errors/render.d.ts +6 -2
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/camera.d.ts.map +1 -1
  53. package/dist/extract/contracts.d.ts +7 -0
  54. package/dist/extract/contracts.d.ts.map +1 -1
  55. package/dist/extract/environment.d.ts +2 -0
  56. package/dist/extract/environment.d.ts.map +1 -1
  57. package/dist/features/error-reporting.d.ts +13 -0
  58. package/dist/features/error-reporting.d.ts.map +1 -0
  59. package/dist/features/host.d.ts +5 -0
  60. package/dist/features/host.d.ts.map +1 -1
  61. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  62. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  63. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  64. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  65. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  66. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  67. package/dist/features/plan.d.ts +7 -4
  68. package/dist/features/plan.d.ts.map +1 -1
  69. package/dist/features/prepared-gpu-work.d.ts +9 -5
  70. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  71. package/dist/features/render-graph-compute.d.ts.map +1 -1
  72. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  73. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  74. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  75. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  76. package/dist/gpu-driven/production-raster.d.ts +3 -1
  77. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  78. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  79. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  80. package/dist/gpu-scene.d.ts +6 -1
  81. package/dist/gpu-scene.d.ts.map +1 -1
  82. package/dist/index.d.ts +5 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.mjs +5 -5
  85. package/dist/inspection-types.d.ts +43 -2
  86. package/dist/inspection-types.d.ts.map +1 -1
  87. package/dist/instances.d.ts +8 -1
  88. package/dist/instances.d.ts.map +1 -1
  89. package/dist/internal.mjs +6 -6
  90. package/dist/pipeline/standard-post.d.ts +28 -19
  91. package/dist/pipeline/standard-post.d.ts.map +1 -1
  92. package/dist/pipeline-spec.d.ts +2 -2
  93. package/dist/record/frame-snapshot.d.ts +3 -0
  94. package/dist/record/frame-snapshot.d.ts.map +1 -1
  95. package/dist/record/frame-targets.d.ts.map +1 -1
  96. package/dist/record/frame.d.ts +5 -2
  97. package/dist/record/frame.d.ts.map +1 -1
  98. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  99. package/dist/record/mesh-ssbo.d.ts +6 -5
  100. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  101. package/dist/record/render-context.d.ts +12 -28
  102. package/dist/record/render-context.d.ts.map +1 -1
  103. package/dist/record/shadow-pass.d.ts.map +1 -1
  104. package/dist/record/skybox-post-pass.d.ts +5 -3
  105. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  106. package/dist/record/typed-frame-graph.d.ts +1 -1
  107. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  108. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  109. package/dist/render-contract.d.ts +24 -4
  110. package/dist/render-contract.d.ts.map +1 -1
  111. package/dist/render-pipeline.d.ts +3 -1
  112. package/dist/render-pipeline.d.ts.map +1 -1
  113. package/dist/render-system-extract-tail.d.ts.map +1 -1
  114. package/dist/render-system-extract.d.ts +8 -1
  115. package/dist/render-system-extract.d.ts.map +1 -1
  116. package/dist/render-system.d.ts +7 -1
  117. package/dist/render-system.d.ts.map +1 -1
  118. package/dist/scene/render-scene.d.ts +2 -0
  119. package/dist/scene/render-scene.d.ts.map +1 -1
  120. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  121. package/dist/temporal/frame.d.ts +7 -0
  122. package/dist/temporal/frame.d.ts.map +1 -1
  123. package/dist/temporal/index.mjs +5 -5
  124. package/dist/temporal/view.d.ts +6 -1
  125. package/dist/temporal/view.d.ts.map +1 -1
  126. package/dist/typed-render-graph-primitives.d.ts +10 -7
  127. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  128. package/dist/volume/component.d.ts +0 -1
  129. package/dist/volume/component.d.ts.map +1 -1
  130. package/dist/volume/passes.d.ts.map +1 -1
  131. package/dist/volume/temporal.d.ts +3 -1
  132. package/dist/volume/temporal.d.ts.map +1 -1
  133. package/package.json +21 -21
  134. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  135. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  136. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  137. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  138. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  139. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  140. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  141. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  142. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  143. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  144. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  145. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  146. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  147. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  148. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  149. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  150. package/src/__tests__/instances-store.unit.test.ts +75 -0
  151. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  152. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  153. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  154. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  155. package/src/__tests__/public-surface.test-d.ts +8 -0
  156. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  157. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  158. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  159. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  160. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  161. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  162. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  163. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  164. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  165. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  166. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  167. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  168. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  169. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  170. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  171. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  172. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  173. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  174. package/src/assembly/analytic-fog-registration.ts +14 -0
  175. package/src/assembly/fxaa-registration.ts +17 -0
  176. package/src/assembly/lod-observation.ts +12 -0
  177. package/src/assembly/motion-blur-registration.ts +17 -0
  178. package/src/assembly/renderer-facade.ts +2 -0
  179. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  180. package/src/assembly/webgpu-ready-contract.ts +8 -3
  181. package/src/assembly/webgpu-ready.ts +181 -245
  182. package/src/assembly/webgpu-renderer.ts +16 -31
  183. package/src/bloom-admission.ts +21 -0
  184. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  185. package/src/components/atmosphere.ts +4 -0
  186. package/src/components/camera.ts +66 -10
  187. package/src/components/mesh-filter.ts +11 -9
  188. package/src/components/motion-blur.ts +1 -0
  189. package/src/device/gpu-residency.ts +56 -12
  190. package/src/environment/analytic-fog.ts +44 -0
  191. package/src/environment/background.ts +2 -2
  192. package/src/errors/render.ts +14 -1
  193. package/src/extract/camera.ts +20 -7
  194. package/src/extract/contracts.ts +7 -0
  195. package/src/extract/environment.ts +4 -0
  196. package/src/features/error-reporting.ts +78 -0
  197. package/src/features/host.ts +25 -12
  198. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  199. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  200. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  201. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  202. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  203. package/src/features/plan.ts +25 -8
  204. package/src/features/prepared-gpu-work.ts +86 -35
  205. package/src/features/render-graph-compute.ts +13 -5
  206. package/src/features/render-graph-contribution.ts +2 -0
  207. package/src/gpu-driven/material-artifacts.ts +2 -7
  208. package/src/gpu-driven/prepared-draw.ts +15 -10
  209. package/src/gpu-driven/production-raster.ts +83 -7
  210. package/src/gpu-driven/view-gpu.ts +17 -0
  211. package/src/gpu-scene.ts +188 -21
  212. package/src/index.ts +32 -0
  213. package/src/inspection-types.ts +60 -2
  214. package/src/instances.ts +80 -1
  215. package/src/pipeline/standard-post.ts +307 -78
  216. package/src/pipeline-spec.ts +5 -5
  217. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  218. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  219. package/src/record/frame-snapshot.ts +5 -1
  220. package/src/record/frame-targets.ts +10 -6
  221. package/src/record/frame.ts +163 -22
  222. package/src/record/main-pass-geometry.ts +30 -1
  223. package/src/record/main-pass-sprite-draws.ts +6 -1
  224. package/src/record/mesh-ssbo.ts +43 -6
  225. package/src/record/render-context.ts +12 -28
  226. package/src/record/shadow-pass.ts +6 -1
  227. package/src/record/skybox-post-pass.ts +122 -196
  228. package/src/record/typed-frame-graph.ts +153 -9
  229. package/src/recovery/render-system-candidate.ts +22 -10
  230. package/src/render-contract.ts +23 -4
  231. package/src/render-pipeline.ts +6 -1
  232. package/src/render-system-extract-tail.ts +16 -0
  233. package/src/render-system-extract.ts +45 -13
  234. package/src/render-system.ts +255 -120
  235. package/src/scene/render-scene.ts +85 -4
  236. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  237. package/src/temporal/frame-coordinator.ts +8 -1
  238. package/src/temporal/frame.ts +20 -0
  239. package/src/temporal/view.ts +13 -2
  240. package/src/typed-render-graph-primitives.ts +121 -48
  241. package/src/volume/component.ts +0 -1
  242. package/src/volume/passes.ts +8 -4
  243. package/src/volume/resources.ts +1 -1
  244. package/src/volume/temporal.ts +11 -0
  245. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  246. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  247. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  248. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  249. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  250. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
@@ -4,11 +4,11 @@ import { mat4, vec3 } from '@forgeax/engine-math';
4
4
  import { GlobalTransform } from '@forgeax/engine-scene';
5
5
  import {
6
6
  antialiasFromF32,
7
- bloomEnabledFromF32,
8
7
  Camera,
9
8
  cameraExposureFromColumns,
10
9
  cameraProjectionFromF32,
11
10
  tonemapFromF32,
11
+ validateCameraBloom,
12
12
  validateCameraColorGrading,
13
13
  } from '../components/camera';
14
14
  import { DepthOfField } from '../components/depth-of-field';
@@ -22,6 +22,7 @@ import {
22
22
  depthOfFieldRequestFailure,
23
23
  resolveDepthOfFieldParams,
24
24
  } from '../features/depth-of-field/depth-of-field-params';
25
+ import { resolveMotionBlurParams } from '../features/motion-blur/motion-blur-params';
25
26
  import type { CameraSnapshot } from '../render-contract';
26
27
  import { getActiveCamera, selectActiveCameraIndex } from '../systems/active-camera';
27
28
 
@@ -44,6 +45,9 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
44
45
  for (const row of cameraQuery) {
45
46
  const cam = row.get(Camera);
46
47
  const motionBlur = row.get(MotionBlur);
48
+ const motionBlurResult = resolveMotionBlurParams(motionBlur);
49
+ if (!motionBlurResult.ok) throw motionBlurResult.error;
50
+ const motionBlurParams = motionBlurResult.value;
47
51
  const depthOfField = row.get(DepthOfField);
48
52
  validateCameraColorGrading(cam.temperature, cam.tint, cam.colorLutStrength);
49
53
  const dynamicResolution = row.get(DynamicResolution);
@@ -58,6 +62,13 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
58
62
  ? undefined
59
63
  : depthOfFieldRequestFailure(depthOfFieldResult.error);
60
64
  const screenSpaceReflection = row.get(ScreenSpaceReflection);
65
+ const bloom = validateCameraBloom(
66
+ cam.bloom,
67
+ cam.bloomThreshold,
68
+ cam.bloomIntensity,
69
+ cam.bloomSoftKnee,
70
+ cam.bloomScatter,
71
+ );
61
72
  const entity = row.entity as EntityHandle;
62
73
  const worldMat = readWorldMatrix(world, entity);
63
74
  if (worldMat === undefined) continue;
@@ -92,10 +103,11 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
92
103
  ...(dynamicResolutionResult.value === undefined
93
104
  ? {}
94
105
  : { dynamicResolution: dynamicResolutionResult.value }),
95
- bloom: bloomEnabledFromF32(cam.bloom),
106
+ bloom,
96
107
  bloomThreshold: cam.bloomThreshold,
97
108
  bloomIntensity: cam.bloomIntensity,
98
- bloomBlurRadius: cam.bloomBlurRadius,
109
+ bloomSoftKnee: cam.bloomSoftKnee,
110
+ bloomScatter: cam.bloomScatter,
99
111
  clearColor: [
100
112
  cam.clearColor[0] ?? 0,
101
113
  cam.clearColor[1] ?? 0,
@@ -106,13 +118,14 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
106
118
  ? { depthOfField: depthOfFieldResult.value }
107
119
  : {}),
108
120
  ...(depthOfFieldError === undefined ? {} : { depthOfFieldError }),
109
- ...(motionBlur === undefined
121
+ ...(motionBlurParams === undefined
110
122
  ? {}
111
123
  : {
112
124
  motionBlur: {
113
- shutterAngle: motionBlur.shutterAngle,
114
- maxRadiusPixels: motionBlur.maxRadiusPixels,
115
- sampleCount: motionBlur.sampleCount,
125
+ shutterAngle: motionBlurParams.shutterAngle,
126
+ maxRadiusPixels: motionBlurParams.maxRadiusPixels,
127
+ sampleCount: motionBlurParams.sampleCount,
128
+ targetFps: motionBlurParams.targetFps,
116
129
  },
117
130
  }),
118
131
  ...(screenSpaceReflection === undefined
@@ -394,6 +394,8 @@ export interface ExtractedVolumetricFog {
394
394
  readonly generation?: number;
395
395
  readonly digest?: string;
396
396
  readonly worldId?: number;
397
+ /** Canonical simulation time projected from the owning World. */
398
+ readonly worldTimeSeconds?: number;
397
399
  readonly lightEntity?: EntityHandle;
398
400
  readonly lightKind?: 'directional' | 'point' | 'spot';
399
401
  readonly pointLightEntity?: EntityHandle;
@@ -530,6 +532,9 @@ export type RenderableReactiveReason =
530
532
  export interface RenderableTemporalSnapshot {
531
533
  readonly previousEpoch: number | undefined;
532
534
  readonly previousSource: RenderablePreviousSource;
535
+ /** True only when current and previous geometry belong to one stable source identity. */
536
+ readonly motionValid: boolean;
537
+ /** Color-history invalidation is independent from motion validity. */
533
538
  readonly reactive: boolean;
534
539
  readonly reactiveReasons: readonly RenderableReactiveReason[];
535
540
  readonly previousTransform: TransformSnapshot;
@@ -563,6 +568,8 @@ export interface GpuDrivenDrawSnapshot {
563
568
  export interface InstancesSnapshot {
564
569
  /** Packed column-major mat4 transforms (16 f32 per instance). */
565
570
  readonly transforms: Float32Array;
571
+ /** Optional unique identity generations; tokens travel with instances through reorder. */
572
+ readonly generations?: Uint32Array;
566
573
  /** Number of instances (transforms.length / 16). */
567
574
  readonly instanceCount: number;
568
575
  /** Stable per-entity GPU buffer cache key (the packed Entity u32). */
@@ -39,6 +39,8 @@ export interface AtmosphereParameters {
39
39
  readonly mieCoefficient: number;
40
40
  readonly mieDirectionalG: number;
41
41
  readonly sunAngularRadius: number;
42
+ readonly circumsolarStrength: number;
43
+ readonly circumsolarWidth: number;
42
44
  }
43
45
 
44
46
  /** Shared CPU contract for values accepted by the atmosphere shaders. */
@@ -50,6 +52,8 @@ export const ATMOSPHERE_PARAMETER_RANGES: Readonly<
50
52
  mieCoefficient: Object.freeze({ min: 0, max: Number.POSITIVE_INFINITY }),
51
53
  mieDirectionalG: Object.freeze({ min: 0, max: 0.999 }),
52
54
  sunAngularRadius: Object.freeze({ min: 0, max: Number.POSITIVE_INFINITY }),
55
+ circumsolarStrength: Object.freeze({ min: 0, max: 4 }),
56
+ circumsolarWidth: Object.freeze({ min: 0.25, max: 4 }),
53
57
  });
54
58
 
55
59
  export interface FogCandidate {
@@ -0,0 +1,78 @@
1
+ import type { RenderError } from '../errors/render';
2
+
3
+ /**
4
+ * Recoverable feature preparation failures are still useful diagnostics, but
5
+ * they are not new failures on every render tick. Keep the reporting policy
6
+ * independent from the frame orchestration so it can be exercised without a
7
+ * renderer or device.
8
+ */
9
+ export const PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES = 60;
10
+
11
+ export function shouldReportPendingRenderFeatureError(
12
+ lastReportedFrame: Map<string, number>,
13
+ key: string,
14
+ frameNumber: number,
15
+ intervalFrames = PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
16
+ ): boolean {
17
+ const lastReported = lastReportedFrame.get(key);
18
+ if (lastReported !== undefined && frameNumber - lastReported < intervalFrames) return false;
19
+ lastReportedFrame.set(key, frameNumber);
20
+ return true;
21
+ }
22
+
23
+ export function reportPendingRenderFeatureErrors(
24
+ errors: readonly RenderError[],
25
+ frameNumber: number,
26
+ lastReportedFrame: Map<string, number>,
27
+ errorRegistry: { fire(error: RenderError): void },
28
+ ): void {
29
+ for (const error of errors) {
30
+ if (!isPendingRenderFeaturePreparation(error)) {
31
+ errorRegistry.fire(error);
32
+ continue;
33
+ }
34
+ // A missing prepared resource during its normal asynchronous warm-up is
35
+ // an expected candidate state. Keep that path silent; other next-frame
36
+ // failures still receive first/periodic reporting.
37
+ if (isSilentRenderFeatureWarmup(error)) continue;
38
+ const key = pendingRenderFeatureErrorKey(error);
39
+ if (shouldReportPendingRenderFeatureError(lastReportedFrame, key, frameNumber)) {
40
+ errorRegistry.fire(error);
41
+ }
42
+ }
43
+ }
44
+
45
+ function isPendingRenderFeaturePreparation(error: RenderError): boolean {
46
+ if (!('detail' in error)) return false;
47
+ switch (error.code) {
48
+ case 'render-feature-stage-failed':
49
+ case 'render-feature-preparation-failed':
50
+ case 'render-feature-prepared-state-mismatch':
51
+ case 'render-feature-draw-recording-failed':
52
+ return error.detail.recovery === 'next-frame';
53
+ default:
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function isSilentRenderFeatureWarmup(error: RenderError): boolean {
59
+ return (
60
+ error.code === 'render-feature-preparation-failed' &&
61
+ error.detail.reason.startsWith('rhi-not-available:')
62
+ );
63
+ }
64
+
65
+ function pendingRenderFeatureErrorKey(error: RenderError): string {
66
+ const detail = 'detail' in error ? error.detail : undefined;
67
+ const featureIdentity =
68
+ detail !== undefined &&
69
+ 'featureIdentity' in detail &&
70
+ typeof detail.featureIdentity === 'string'
71
+ ? detail.featureIdentity
72
+ : '';
73
+ const order =
74
+ detail !== undefined && 'order' in detail && typeof detail.order === 'number'
75
+ ? detail.order
76
+ : -1;
77
+ return `${error.code}\0${featureIdentity}\0${order}\0${error.expected}\0${error.hint}`;
78
+ }
@@ -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);
@@ -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
  });