@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
@@ -0,0 +1,583 @@
1
+ import { expect, it } from 'vitest';
2
+ import { MOTION_BLUR_COMPUTE_WGSL } from '../features/motion-blur/motion-blur-feature';
3
+
4
+ // The ordinary render project intentionally does not install browser WebGPU
5
+ // globals. Keep this maintained Dawn assertion portable across the `render`
6
+ // and `dawn` Vitest projects by using the WebGPU spec's stable bit values.
7
+ const COMPUTE_STAGE = 0x4;
8
+ const TEXTURE_COPY_SRC = 0x1;
9
+ const TEXTURE_COPY_DST = 0x2;
10
+ const TEXTURE_BINDING = 0x4;
11
+ const STORAGE_BINDING = 0x8;
12
+ const BUFFER_MAP_READ = 0x1;
13
+ const BUFFER_COPY_DST = 0x8;
14
+ const BUFFER_UNIFORM = 0x40;
15
+ const BUFFER_STORAGE = 0x80;
16
+
17
+ type MotionBlurProbe = {
18
+ readonly name: string;
19
+ readonly motionHalf: number;
20
+ readonly motionHalfY?: number;
21
+ readonly maxRadius: number;
22
+ readonly receiverX: number;
23
+ readonly receiverY?: number;
24
+ readonly width: number;
25
+ readonly height: number;
26
+ readonly moverStart: number;
27
+ readonly moverEnd: number;
28
+ readonly moverYStart?: number;
29
+ readonly moverYEnd?: number;
30
+ readonly expectNonZero?: boolean;
31
+ readonly sampleCount?: number;
32
+ readonly expectedRowEnergy?: number;
33
+ readonly expectedTotalEnergy?: number;
34
+ readonly energyTolerance?: number;
35
+ };
36
+
37
+ const PROBES: readonly MotionBlurProbe[] = [
38
+ {
39
+ name: 'short vector reaches a static receiver beside the mover',
40
+ motionHalf: 0x3400,
41
+ maxRadius: 32,
42
+ receiverX: 30,
43
+ width: 65,
44
+ height: 16,
45
+ moverStart: 32,
46
+ moverEnd: 35,
47
+ },
48
+ {
49
+ name: 'long vector reaches a static receiver through an adjacent tile',
50
+ motionHalf: 0x3c00,
51
+ maxRadius: 64,
52
+ receiverX: 30,
53
+ width: 64,
54
+ height: 16,
55
+ moverStart: 32,
56
+ moverEnd: 35,
57
+ },
58
+ {
59
+ name: 'long vector reaches a static receiver at the support boundary tile',
60
+ motionHalf: 0x4000,
61
+ maxRadius: 64,
62
+ receiverX: 30,
63
+ width: 128,
64
+ height: 16,
65
+ moverStart: 86,
66
+ moverEnd: 89,
67
+ },
68
+ {
69
+ name: 'cap32 derives the positive offset-plus-two tile',
70
+ motionHalf: 0x4400,
71
+ maxRadius: 32,
72
+ receiverX: 30,
73
+ width: 64,
74
+ height: 16,
75
+ // The seven support taps reserve one receiver slot. For radius32 the
76
+ // +18.3 pixel tap lands in this tile-3 strip, exercising the +2 tile
77
+ // summary walk without relying on a hand-picked unreachable pixel.
78
+ moverStart: 48,
79
+ moverEnd: 51,
80
+ },
81
+ {
82
+ name: 'cap64 derives the positive offset-plus-two tile',
83
+ motionHalf: 0x4400,
84
+ maxRadius: 64,
85
+ receiverX: 30,
86
+ width: 64,
87
+ height: 16,
88
+ // For radius64 the bounded +18.3 pixel tap also lands in tile 3.
89
+ moverStart: 48,
90
+ moverEnd: 51,
91
+ },
92
+ {
93
+ name: 'cap32 derives the positive diagonal offset-plus-two tile',
94
+ motionHalf: 0x4400,
95
+ motionHalfY: 0x4400,
96
+ maxRadius: 32,
97
+ receiverX: 14,
98
+ receiverY: 14,
99
+ width: 64,
100
+ height: 64,
101
+ // The capped +45 degree support tap lands at (32,32), in tile (2,2), which is
102
+ // the diagonal +2 candidate from receiver tile (0,0).
103
+ moverStart: 32,
104
+ moverEnd: 35,
105
+ moverYStart: 32,
106
+ moverYEnd: 35,
107
+ },
108
+ {
109
+ name: 'diagonal card conserves total energy at 4 taps',
110
+ motionHalf: 0x4000,
111
+ motionHalfY: 0x4000,
112
+ maxRadius: 32,
113
+ receiverX: 0,
114
+ receiverY: 0,
115
+ width: 64,
116
+ height: 64,
117
+ moverStart: 30,
118
+ moverEnd: 33,
119
+ moverYStart: 30,
120
+ moverYEnd: 33,
121
+ expectNonZero: false,
122
+ sampleCount: 4,
123
+ expectedTotalEnergy: 16,
124
+ },
125
+ {
126
+ name: 'diagonal card conserves total energy at 8 taps',
127
+ motionHalf: 0x4000,
128
+ motionHalfY: 0x4000,
129
+ maxRadius: 32,
130
+ receiverX: 0,
131
+ receiverY: 0,
132
+ width: 64,
133
+ height: 64,
134
+ moverStart: 30,
135
+ moverEnd: 33,
136
+ moverYStart: 30,
137
+ moverYEnd: 33,
138
+ expectNonZero: false,
139
+ sampleCount: 8,
140
+ expectedTotalEnergy: 16,
141
+ },
142
+ {
143
+ name: 'diagonal card conserves total energy at 16 taps',
144
+ motionHalf: 0x4000,
145
+ motionHalfY: 0x4000,
146
+ maxRadius: 32,
147
+ receiverX: 0,
148
+ receiverY: 0,
149
+ width: 64,
150
+ height: 64,
151
+ moverStart: 30,
152
+ moverEnd: 33,
153
+ moverYStart: 30,
154
+ moverYEnd: 33,
155
+ expectNonZero: false,
156
+ sampleCount: 16,
157
+ expectedTotalEnergy: 16,
158
+ },
159
+ {
160
+ name: 'horizontal card conserves total energy on a square canvas',
161
+ motionHalf: 0x4000,
162
+ maxRadius: 32,
163
+ receiverX: 0,
164
+ receiverY: 0,
165
+ width: 128,
166
+ height: 128,
167
+ moverStart: 62,
168
+ moverEnd: 65,
169
+ moverYStart: 62,
170
+ moverYEnd: 65,
171
+ expectNonZero: false,
172
+ sampleCount: 8,
173
+ expectedTotalEnergy: 16,
174
+ },
175
+ {
176
+ name: 'vertical card conserves total energy on a square canvas',
177
+ motionHalf: 0,
178
+ motionHalfY: 0x4000,
179
+ maxRadius: 32,
180
+ receiverX: 0,
181
+ receiverY: 0,
182
+ width: 128,
183
+ height: 128,
184
+ moverStart: 62,
185
+ moverEnd: 65,
186
+ moverYStart: 62,
187
+ moverYEnd: 65,
188
+ expectNonZero: false,
189
+ sampleCount: 8,
190
+ expectedTotalEnergy: 16,
191
+ },
192
+ {
193
+ name: 'cap64 derives the negative offset-plus-one tile',
194
+ motionHalf: 0xc000,
195
+ maxRadius: 64,
196
+ receiverX: 110,
197
+ width: 128,
198
+ height: 16,
199
+ moverStart: 91,
200
+ moverEnd: 94,
201
+ },
202
+ {
203
+ name: 'cap32 rejects a tile outside the support boundary',
204
+ motionHalf: 0x4000,
205
+ maxRadius: 32,
206
+ receiverX: 30,
207
+ width: 128,
208
+ height: 16,
209
+ moverStart: 102,
210
+ moverEnd: 105,
211
+ expectNonZero: false,
212
+ },
213
+ {
214
+ name: 'long vector preserves energy at half-cell translation 0',
215
+ motionHalf: 0x4000,
216
+ maxRadius: 32,
217
+ receiverX: 30,
218
+ width: 128,
219
+ height: 16,
220
+ moverStart: 62,
221
+ moverEnd: 65,
222
+ expectNonZero: false,
223
+ expectedRowEnergy: 4,
224
+ },
225
+ {
226
+ name: 'long vector preserves energy at half-cell translation 1',
227
+ motionHalf: 0x4000,
228
+ maxRadius: 32,
229
+ receiverX: 30,
230
+ width: 128,
231
+ height: 16,
232
+ moverStart: 63,
233
+ moverEnd: 66,
234
+ expectNonZero: false,
235
+ expectedRowEnergy: 4,
236
+ },
237
+ {
238
+ name: 'long vector preserves energy at half-cell translation 2',
239
+ motionHalf: 0x4000,
240
+ maxRadius: 32,
241
+ receiverX: 30,
242
+ width: 128,
243
+ height: 16,
244
+ moverStart: 64,
245
+ moverEnd: 67,
246
+ expectNonZero: false,
247
+ expectedRowEnergy: 4,
248
+ },
249
+ {
250
+ name: 'long vector preserves energy at half-cell translation 3',
251
+ motionHalf: 0x4000,
252
+ maxRadius: 32,
253
+ receiverX: 30,
254
+ width: 128,
255
+ height: 16,
256
+ moverStart: 65,
257
+ moverEnd: 68,
258
+ expectNonZero: false,
259
+ expectedRowEnergy: 4,
260
+ },
261
+ {
262
+ name: 'long vector preserves energy at half-cell translation 4',
263
+ motionHalf: 0x4000,
264
+ maxRadius: 32,
265
+ receiverX: 30,
266
+ width: 128,
267
+ height: 16,
268
+ moverStart: 66,
269
+ moverEnd: 69,
270
+ expectNonZero: false,
271
+ expectedRowEnergy: 4,
272
+ },
273
+ {
274
+ name: 'cap64 sparse source closes the bounded midpoint gap at 4 taps',
275
+ motionHalf: 0x4000,
276
+ maxRadius: 64,
277
+ receiverX: 30,
278
+ width: 128,
279
+ height: 16,
280
+ moverStart: 54,
281
+ moverEnd: 57,
282
+ sampleCount: 4,
283
+ },
284
+ {
285
+ name: 'cap64 sparse source preserves row energy at 8 taps',
286
+ motionHalf: 0x4000,
287
+ maxRadius: 64,
288
+ receiverX: 30,
289
+ width: 128,
290
+ height: 16,
291
+ moverStart: 54,
292
+ moverEnd: 57,
293
+ sampleCount: 8,
294
+ expectedRowEnergy: 4,
295
+ },
296
+ {
297
+ name: 'cap64 sparse source remains reachable at 16 taps',
298
+ motionHalf: 0x4000,
299
+ maxRadius: 64,
300
+ receiverX: 30,
301
+ width: 128,
302
+ height: 16,
303
+ moverStart: 54,
304
+ moverEnd: 57,
305
+ sampleCount: 16,
306
+ },
307
+ {
308
+ name: 'cap64 full support conserves energy at 4 taps',
309
+ motionHalf: 0x4000,
310
+ maxRadius: 64,
311
+ receiverX: 30,
312
+ width: 256,
313
+ height: 16,
314
+ moverStart: 126,
315
+ moverEnd: 129,
316
+ expectNonZero: false,
317
+ sampleCount: 4,
318
+ expectedRowEnergy: 4,
319
+ },
320
+ {
321
+ name: 'cap64 full support conserves energy at 8 taps',
322
+ motionHalf: 0x4000,
323
+ maxRadius: 64,
324
+ receiverX: 30,
325
+ width: 256,
326
+ height: 16,
327
+ moverStart: 126,
328
+ moverEnd: 129,
329
+ expectNonZero: false,
330
+ sampleCount: 8,
331
+ expectedRowEnergy: 4,
332
+ },
333
+ {
334
+ name: 'cap64 full support conserves energy at 16 taps',
335
+ motionHalf: 0x4000,
336
+ maxRadius: 64,
337
+ receiverX: 30,
338
+ width: 256,
339
+ height: 16,
340
+ moverStart: 126,
341
+ moverEnd: 129,
342
+ expectNonZero: false,
343
+ sampleCount: 16,
344
+ expectedRowEnergy: 4,
345
+ },
346
+ {
347
+ name: 'cap64 full support keeps translated phase energy',
348
+ motionHalf: 0x4000,
349
+ maxRadius: 64,
350
+ receiverX: 30,
351
+ width: 256,
352
+ height: 16,
353
+ moverStart: 127,
354
+ moverEnd: 130,
355
+ expectNonZero: false,
356
+ sampleCount: 8,
357
+ expectedRowEnergy: 4,
358
+ },
359
+ {
360
+ name: 'cap64 full support keeps reverse motion energy',
361
+ motionHalf: 0xc000,
362
+ maxRadius: 64,
363
+ receiverX: 94,
364
+ width: 256,
365
+ height: 16,
366
+ moverStart: 126,
367
+ moverEnd: 129,
368
+ sampleCount: 8,
369
+ expectedRowEnergy: 4,
370
+ },
371
+ {
372
+ name: 'cap64 translated sparse source stays reachable in view',
373
+ motionHalf: 0x4000,
374
+ maxRadius: 64,
375
+ receiverX: 94,
376
+ width: 256,
377
+ height: 16,
378
+ moverStart: 118,
379
+ moverEnd: 121,
380
+ sampleCount: 8,
381
+ expectedRowEnergy: 4,
382
+ },
383
+ ];
384
+
385
+ const COPY_BYTES_PER_ROW_ALIGNMENT = 256;
386
+
387
+ it.each(PROBES)('production compute lane: $name', async ({
388
+ name,
389
+ motionHalf,
390
+ maxRadius,
391
+ receiverX,
392
+ receiverY = 8,
393
+ width,
394
+ height,
395
+ moverStart,
396
+ moverEnd,
397
+ moverYStart = 0,
398
+ moverYEnd = height - 1,
399
+ motionHalfY = 0,
400
+ expectNonZero = true,
401
+ sampleCount = 8,
402
+ expectedRowEnergy,
403
+ expectedTotalEnergy,
404
+ energyTolerance = 0.03,
405
+ }) => {
406
+ const adapter = await globalThis.navigator.gpu.requestAdapter();
407
+ if (adapter === null)
408
+ throw new Error('Dawn adapter unavailable for Motion Blur production probe');
409
+ const device = await adapter.requestDevice();
410
+ device.pushErrorScope('validation');
411
+ const module = device.createShaderModule({ code: MOTION_BLUR_COMPUTE_WGSL });
412
+ const compilation = await module.getCompilationInfo();
413
+ const errors = compilation.messages.filter((message) => message.type === 'error');
414
+ expect(errors, errors.map((message) => message.message).join('\n')).toHaveLength(0);
415
+
416
+ const layout = device.createBindGroupLayout({
417
+ entries: [
418
+ { binding: 0, visibility: COMPUTE_STAGE, texture: { sampleType: 'float' } },
419
+ { binding: 1, visibility: COMPUTE_STAGE, texture: { sampleType: 'float' } },
420
+ { binding: 2, visibility: COMPUTE_STAGE, buffer: { type: 'storage' } },
421
+ {
422
+ binding: 3,
423
+ visibility: COMPUTE_STAGE,
424
+ storageTexture: { access: 'write-only', format: 'rgba16float' },
425
+ },
426
+ { binding: 4, visibility: COMPUTE_STAGE, buffer: { type: 'uniform' } },
427
+ ],
428
+ });
429
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
430
+ const summary = device.createComputePipeline({
431
+ layout: pipelineLayout,
432
+ compute: { module, entryPoint: 'tile_summary' },
433
+ });
434
+ const reconstruct = device.createComputePipeline({
435
+ layout: pipelineLayout,
436
+ compute: { module, entryPoint: 'reconstruct' },
437
+ });
438
+
439
+ const texture = (usage: number) =>
440
+ device.createTexture({
441
+ size: { width, height, depthOrArrayLayers: 1 },
442
+ format: 'rgba16float',
443
+ usage,
444
+ });
445
+ const color = texture(TEXTURE_BINDING | TEXTURE_COPY_DST);
446
+ const temporal = texture(TEXTURE_BINDING | TEXTURE_COPY_DST);
447
+ const output = texture(STORAGE_BINDING | TEXTURE_COPY_SRC);
448
+ const colorData = new Uint16Array(width * height * 4);
449
+ const temporalData = new Uint16Array(width * height * 4);
450
+ for (let y = 0; y < height; y += 1) {
451
+ for (let x = 0; x < width; x += 1) {
452
+ const index = (y * width + x) * 4;
453
+ const mover = x >= moverStart && x <= moverEnd && y >= moverYStart && y <= moverYEnd;
454
+ colorData[index] = mover ? 0x3c00 : 0;
455
+ colorData[index + 1] = mover ? 0x3c00 : 0;
456
+ colorData[index + 2] = mover ? 0x3c00 : 0;
457
+ colorData[index + 3] = 0x3c00;
458
+ temporalData[index] = mover ? motionHalf : 0;
459
+ temporalData[index + 1] = mover ? motionHalfY : 0;
460
+ temporalData[index + 2] = mover ? 0x4000 : 0x4400;
461
+ }
462
+ }
463
+ device.queue.writeTexture(
464
+ { texture: color },
465
+ colorData,
466
+ { bytesPerRow: width * 8, rowsPerImage: height },
467
+ { width, height, depthOrArrayLayers: 1 },
468
+ );
469
+ device.queue.writeTexture(
470
+ { texture: temporal },
471
+ temporalData,
472
+ { bytesPerRow: width * 8, rowsPerImage: height },
473
+ { width, height, depthOrArrayLayers: 1 },
474
+ );
475
+ const tileSummary = device.createBuffer({
476
+ size: Math.ceil(width / 16) * Math.ceil(height / 16) * 32,
477
+ usage: BUFFER_STORAGE,
478
+ });
479
+ const params = device.createBuffer({
480
+ size: 32,
481
+ usage: BUFFER_UNIFORM | BUFFER_COPY_DST,
482
+ });
483
+ const paramsData = new ArrayBuffer(32);
484
+ const paramsView = new DataView(paramsData);
485
+ paramsView.setFloat32(0, 180, true);
486
+ paramsView.setFloat32(4, maxRadius, true);
487
+ paramsView.setUint32(8, sampleCount, true);
488
+ paramsView.setFloat32(16, 60, true);
489
+ paramsView.setFloat32(20, 1, true);
490
+ paramsView.setFloat32(24, 1 / 60, true);
491
+ device.queue.writeBuffer(params, 0, paramsData);
492
+ const bindGroup = device.createBindGroup({
493
+ layout,
494
+ entries: [
495
+ { binding: 0, resource: color.createView() },
496
+ { binding: 1, resource: temporal.createView() },
497
+ { binding: 2, resource: { buffer: tileSummary } },
498
+ { binding: 3, resource: output.createView() },
499
+ { binding: 4, resource: { buffer: params } },
500
+ ],
501
+ });
502
+ const readback = device.createBuffer({
503
+ size:
504
+ Math.ceil((width * 8) / COPY_BYTES_PER_ROW_ALIGNMENT) * COPY_BYTES_PER_ROW_ALIGNMENT * height,
505
+ usage: BUFFER_COPY_DST | BUFFER_MAP_READ,
506
+ });
507
+ const readbackBytesPerRow =
508
+ Math.ceil((width * 8) / COPY_BYTES_PER_ROW_ALIGNMENT) * COPY_BYTES_PER_ROW_ALIGNMENT;
509
+ try {
510
+ const encoder = device.createCommandEncoder();
511
+ const summaryPass = encoder.beginComputePass();
512
+ summaryPass.setPipeline(summary);
513
+ summaryPass.setBindGroup(0, bindGroup);
514
+ summaryPass.dispatchWorkgroups(Math.ceil(width / 16), Math.ceil(height / 16));
515
+ summaryPass.end();
516
+ const reconstructPass = encoder.beginComputePass();
517
+ reconstructPass.setPipeline(reconstruct);
518
+ reconstructPass.setBindGroup(0, bindGroup);
519
+ reconstructPass.dispatchWorkgroups(Math.ceil(width / 8), Math.ceil(height / 8));
520
+ reconstructPass.end();
521
+ encoder.copyTextureToBuffer(
522
+ { texture: output },
523
+ { buffer: readback, bytesPerRow: readbackBytesPerRow, rowsPerImage: height },
524
+ { width, height, depthOrArrayLayers: 1 },
525
+ );
526
+ device.queue.submit([encoder.finish()]);
527
+ await device.queue.onSubmittedWorkDone();
528
+ const validationError = await device.popErrorScope();
529
+ expect(validationError, `${name} emitted a Dawn validation error`).toBeNull();
530
+ await readback.mapAsync(BUFFER_MAP_READ);
531
+ const result = new Uint16Array(readback.getMappedRange());
532
+ const receiver = result[receiverY * (readbackBytesPerRow / 2) + receiverX * 4];
533
+ if (expectNonZero) {
534
+ expect(
535
+ receiver,
536
+ `${name} output was black at receiver (${receiverX},${receiverY})`,
537
+ ).toBeGreaterThan(0);
538
+ } else {
539
+ expect(receiver, `${name} outside-support receiver unexpectedly received motion`).toBe(0);
540
+ }
541
+ const decodeFloat16 = (bits: number): number => {
542
+ const sign = (bits & 0x8000) === 0 ? 1 : -1;
543
+ const exponent = (bits >>> 10) & 0x1f;
544
+ const mantissa = bits & 0x03ff;
545
+ if (exponent === 0) return sign * 2 ** -14 * (mantissa / 1024);
546
+ if (exponent === 0x1f) return Number.POSITIVE_INFINITY;
547
+ return sign * 2 ** (exponent - 15) * (1 + mantissa / 1024);
548
+ };
549
+ if (expectedRowEnergy !== undefined) {
550
+ const rowEnergy = Array.from({ length: width }, (_, x) =>
551
+ decodeFloat16(result[receiverY * (readbackBytesPerRow / 2) + x * 4]),
552
+ ).reduce((sum, value) => sum + value, 0);
553
+ const relativeError =
554
+ Math.abs(rowEnergy - expectedRowEnergy) / Math.max(expectedRowEnergy, 1);
555
+ expect(
556
+ relativeError,
557
+ `${name} row energy ${rowEnergy} differs from ${expectedRowEnergy}`,
558
+ ).toBeLessThanOrEqual(energyTolerance);
559
+ }
560
+ if (expectedTotalEnergy !== undefined) {
561
+ const totalEnergy = Array.from({ length: height }, (_, y) =>
562
+ Array.from({ length: width }, (_, x) =>
563
+ decodeFloat16(result[y * (readbackBytesPerRow / 2) + x * 4]),
564
+ ).reduce((sum, value) => sum + value, 0),
565
+ ).reduce((sum, value) => sum + value, 0);
566
+ const relativeError =
567
+ Math.abs(totalEnergy - expectedTotalEnergy) / Math.max(expectedTotalEnergy, 1);
568
+ expect(
569
+ relativeError,
570
+ `${name} total energy ${totalEnergy} differs from ${expectedTotalEnergy}`,
571
+ ).toBeLessThanOrEqual(energyTolerance);
572
+ }
573
+ readback.unmap();
574
+ } finally {
575
+ readback.destroy();
576
+ tileSummary.destroy();
577
+ params.destroy();
578
+ color.destroy();
579
+ temporal.destroy();
580
+ output.destroy();
581
+ device.destroy();
582
+ }
583
+ });
@@ -181,6 +181,27 @@ describe('prepared Standard PBR producer-to-record integration', () => {
181
181
  });
182
182
  });
183
183
 
184
+ it('admits geometry attributes that the selected material does not consume', () => {
185
+ const source = artifact();
186
+ const result = resolvePreparedGpuDrivenDraw({
187
+ snapshot: snapshot(),
188
+ artifact: source,
189
+ geometry: {
190
+ ...geometry(source),
191
+ vertexInputs: [
192
+ ...source.receipt.vertexInputs,
193
+ { semantic: 'uv1', location: 6, format: 'float32x2' },
194
+ ],
195
+ },
196
+ generation: source.receipt.generation,
197
+ draw: snapshot().gpuDrivenDraws?.[0] as NonNullable<
198
+ RenderableSnapshot['gpuDrivenDraws']
199
+ >[number],
200
+ });
201
+
202
+ expect(result.ok).toBe(true);
203
+ });
204
+
184
205
  it('selects the colored receipt while extracting a real colored mesh draw', () => {
185
206
  const coloredSource = artifact(true);
186
207
  const coloredMesh: MeshAsset = {
@@ -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
+ }