@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
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # `@forgeax/engine-render`
2
2
 
3
+ > [!NOTE]
4
+ > This package-owner README contains physical `@forgeax/engine-render` imports
5
+ > in internal ownership examples. Game-facing code should use the public
6
+ > facade `@forgeax/engine/render`; the Bloom consumer route is documented in
7
+ > [hello-bloom](../../apps/hello/bloom/README.md).
8
+
3
9
  > [!IMPORTANT]
4
10
  > The Standard pipeline keeps dark-gradient color in `rgba16float` until one
5
11
  > `outputTransform` reaches the `surface.storage.raw` endpoint; `inspect()` and
@@ -8,6 +14,16 @@
8
14
 
9
15
  ## Public frame contract
10
16
 
17
+ `Fog` provides distance fog with exponential height density through one
18
+ finite-depth fullscreen pass before temporal resolve, Bloom and tone mapping.
19
+ `density`, `heightFalloff`, linear `color` and `maxOpacity` are selected from
20
+ the frame's resource owner. Disabled or zero-density fog allocates no fog target;
21
+ there is no 3D texture, froxel grid or temporal history. The authored sky remains
22
+ unchanged. FXAA, TAA and no-AA use single-sample depth; MSAA is rejected until a
23
+ matching depth resolve is available. Translucent surfaces that do not write depth
24
+ use the opaque background depth. Use `VolumetricFog` for spatial density and
25
+ lighting/scattering effects; the meadow-style distance haze needs only `Fog`.
26
+
11
27
  ## 灯光最短入口
12
28
 
13
29
  三条最短入口:
@@ -101,6 +117,14 @@ plan. Register it at construction with
101
117
  `createRenderer(canvas, { features: [feature] })`. A feature never receives a
102
118
  device, queue, encoder, staging builder, or submit callback.
103
119
 
120
+ After a frame reaches queue submission, the optional `onFrameSubmitted` callback
121
+ receives a bounded `RenderFeatureSubmission` projection. Its `passes` contain
122
+ only work admitted after prepared-resource resolution, so producer diagnostics
123
+ can distinguish submitted compute dispatches and graphics draws from a plan
124
+ that was still pending. This is a receipt of admitted commands, not a GPU
125
+ readback or proof that an indirect draw produced non-zero instances; producers
126
+ that need those facts must use their existing readback or tape evidence.
127
+
104
128
  ## RenderFeature: the producer seam (first-read index)
105
129
 
106
130
  The public route is one `RenderFeature<FrameData>` through the active RenderGraph
@@ -157,6 +181,13 @@ surface receiver position along its world-space normal before projection (defaul
157
181
  Normal offset and depth bias are independent: increasing the CSM depth span must
158
182
  not magnify the world-space normal offset. Both PCF and PCSS project the offset
159
183
  receiver; volumetric samples have no surface normal and use only depth bias.
184
+ Surface receivers also get a per-cascade depth correction derived from the
185
+ world size of a shadow texel, the filter footprint, and the receiver's depth
186
+ slope along the light's two image axes. The existing normal offset counts
187
+ toward this coverage; only the missing amount is added to `depthBias`, after
188
+ conversion through that cascade's light-space depth span. PCSS derives its
189
+ blocker-search and comparison coverage separately from their actual radii.
190
+ This does not change either author field's units or add shadow texture taps.
160
191
  Cascade projections include their incoming blend band, and PCSS depth spans
161
192
  match the actual projection, including toward-light caster reach. The normal
162
193
  position offset follows the approach in
@@ -192,6 +223,8 @@ index path, not a second schema.
192
223
  import {
193
224
  ANTIALIAS_TAA,
194
225
  Atmosphere,
226
+ BLOOM_DISABLED,
227
+ CAMERA_EXPOSURE_MODE_MANUAL,
195
228
  Camera,
196
229
  Fog,
197
230
  type FramePlan,
@@ -214,6 +247,17 @@ code-specific `detail`; they do not create a second registry or silently pick
214
247
  the first entity. Frame facts contain IDs, revisions, and POD values only, not
215
248
  textures, buffers, devices, or other live GPU objects.
216
249
 
250
+ An Atmosphere also requires exactly one `DirectionalLight` sun. A direct render
251
+ extraction with no sun routes `SunCardinalityError` through the World-owned
252
+ error boundary (`console.error` with the structured error) before extraction
253
+ throws; that internal route is deliberately separate from `app.onError`.
254
+ `app.onError` receives errors returned by the App frame loop and renderer event
255
+ stream. Inspect `error.code` and `error.detail.value`, repair the owning World,
256
+ then retry the same extraction or the next frame. For zero suns, spawn one
257
+ `DirectionalLight`; for multiple suns, remove the extras so the Atmosphere has
258
+ one owner. The renderer does not manufacture a fallback sun or retain a
259
+ partially selected environment.
260
+
217
261
  The Standard graph renders an Atmosphere source as a 128-by-128, six-face
218
262
  `rgba16float` sky cube and a background pass before scene geometry. The selected
219
263
  DirectionalLight supplies the sun direction, color, and illuminance; the sun
@@ -225,6 +269,42 @@ Its color and intensity remain the lighting controls; no Skylight means no
225
269
  global ambient contribution. An explicit equirect keeps its image source.
226
270
  Use a neutral Skylight tint when comparing local captures against the sky.
227
271
 
272
+ ### Atmosphere controls and fixed-exposure comparisons
273
+
274
+ `Atmosphere` keeps the visible disc and the analytic sky response as separate
275
+ controls. `circumsolarStrength` and `circumsolarWidth` affect only the Mie
276
+ forward lobe used while producing the cached sky cube. The defaults preserve the
277
+ baseline response; `sunAngularRadius` still controls the separate disc in the
278
+ background pass and does not grow a halo.
279
+
280
+ | Field | Default | Valid range | Meaning |
281
+ |:--|:--:|:--|:--|
282
+ | `circumsolarStrength` | `1` | `[0, 4]` | Multiplier for the circumsolar Mie lobe |
283
+ | `circumsolarWidth` | `1` | `[0.25, 4]` | Relative lobe width; larger values broaden it, smaller values narrow it |
284
+ | `sunAngularRadius` | `0.004675` rad | `[0, +∞)` | Radius of the separate visible sun disc; `0` disables the disc |
285
+
286
+ For an angular or sunset sweep, hold the camera exposure in manual mode and
287
+ disable Bloom so the measurement belongs to the Atmosphere response. Vary the
288
+ two circumsolar fields while leaving `sunAngularRadius` unchanged:
289
+
290
+ ```ts
291
+ world.set(atmosphere, Atmosphere, {
292
+ circumsolarStrength: 1.5,
293
+ circumsolarWidth: 2,
294
+ sunAngularRadius: 0.004675,
295
+ }).unwrap();
296
+ world.set(camera, Camera, {
297
+ exposureMode: CAMERA_EXPOSURE_MODE_MANUAL,
298
+ exposure: 1,
299
+ bloom: BLOOM_DISABLED,
300
+ }).unwrap();
301
+ ```
302
+
303
+ The fixed exposure and Bloom setting are comparison setup, not additional
304
+ Atmosphere state. Use the renderer's submitted frame or a real Browser/Dawn
305
+ readback for visual claims; source values and a sky-cube upload do not prove a
306
+ pixel response.
307
+
228
308
  The cube has 786,432 texture payload bytes. Its 16-square irradiance cube and
229
309
  64-square, five-mip prefilter add 274,176 bytes; parameters and vertices total
230
310
  1,560 bytes, before backend allocation alignment. DeviceScope owns one cached
@@ -259,6 +339,8 @@ import { createRenderer } from '@forgeax/engine-runtime';
259
339
  const created = await createRenderer(canvas, { gpuPassTiming: {} });
260
340
  if (!created.ok) throw created.error;
261
341
  const renderer = created.value;
342
+ const ready = await renderer.initialization;
343
+ if (!ready.ok) throw ready.error;
262
344
  const attached = renderer.attach(world);
263
345
  if (!attached.ok) throw attached.error;
264
346
  const drawn = renderer.draw({
@@ -332,6 +414,168 @@ before publication; an invalid update keeps the renderer's last-known-good
332
414
  fog frame and exposes the structured failure through the existing inspection
333
415
  path. There is no app-local fog state.
334
416
 
417
+ ### VolumetricFog authoring, World time, and recovery
418
+
419
+ `VolumetricFog` is an authored, one-owner component backed by a linear 3D
420
+ `TextureAsset`. The selected light must be a live same-World
421
+ `DirectionalLight`, `PointLight`, or `SpotLight`; Point/Spot selection also
422
+ requires the corresponding `Transform`. Validate the authoring POD before
423
+ spawning the component, then let the existing RenderSystem extract it. The
424
+ World `Time` resource is the only simulation clock: `world.update(delta)`
425
+ advances it, and the volume parameter block receives that elapsed value.
426
+
427
+ ```ts
428
+ import { Time, World } from '@forgeax/engine-ecs';
429
+ import {
430
+ Atmosphere,
431
+ BLOOM_DISABLED,
432
+ CAMERA_EXPOSURE_MODE_MANUAL,
433
+ Camera,
434
+ DirectionalLight,
435
+ VolumetricFog,
436
+ extractVolumetricFog,
437
+ perspective,
438
+ resolveSelectedVolumetricLight,
439
+ validateVolumetricFog,
440
+ type VolumetricFogAuthoring,
441
+ type Renderer,
442
+ } from '@forgeax/engine-render';
443
+ import { Transform } from '@forgeax/engine-scene';
444
+ import type { TextureAsset } from '@forgeax/engine-types';
445
+
446
+ const world = new World();
447
+ const densityAsset: TextureAsset = {
448
+ kind: 'texture',
449
+ shape: { viewDimension: '3d', extent: { width: 4, height: 4, depth: 4 } },
450
+ format: 'r8unorm',
451
+ colorSpace: 'linear',
452
+ mips: { kind: 'none' },
453
+ data: new Uint8Array(4 * 4 * 4).fill(32),
454
+ };
455
+ const density = world.allocSharedRef('TextureAsset', densityAsset);
456
+ // A production project normally obtains this TextureAsset from its existing
457
+ // Pack/Catalog load path, then allocates the same shared handle in this World.
458
+ // The inline payload keeps this authoring example executable and deterministic.
459
+ const camera = world.spawn(
460
+ { component: Transform, data: { pos: [0, 0, 3] } },
461
+ { component: Camera, data: {
462
+ ...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }),
463
+ exposureMode: CAMERA_EXPOSURE_MODE_MANUAL,
464
+ exposure: 1,
465
+ bloom: BLOOM_DISABLED,
466
+ } },
467
+ ).unwrap();
468
+ const sun = world.spawn({
469
+ component: DirectionalLight,
470
+ data: { direction: [-0.4, -0.8, -0.3], castShadow: true },
471
+ }).unwrap();
472
+ const atmosphere = world.spawn({
473
+ component: Atmosphere,
474
+ data: { circumsolarStrength: 1, circumsolarWidth: 1, sunAngularRadius: 0.004675 },
475
+ }).unwrap();
476
+
477
+ const authored: VolumetricFogAuthoring = {
478
+ light: sun,
479
+ density: {
480
+ guid: 'density-demo',
481
+ generation: 1,
482
+ shape: { viewDimension: '3d', extent: { width: 4, height: 4, depth: 4 } },
483
+ format: 'r8unorm',
484
+ colorSpace: 'linear',
485
+ },
486
+ bounds: { min: [-1, -1, -1], max: [1, 1, 1] },
487
+ extinction: [0.2, 0.2, 0.2],
488
+ albedo: [0.8, 0.8, 0.8],
489
+ emission: [0, 0, 0],
490
+ anisotropy: 0,
491
+ maxDistance: 50,
492
+ };
493
+ const checked = validateVolumetricFog(authored);
494
+ if (!checked.ok) throw checked.error;
495
+ const extracted = extractVolumetricFog([checked.value]);
496
+ if (!extracted.ok) throw extracted.error;
497
+ const selected = resolveSelectedVolumetricLight(world, sun);
498
+ if (selected.status === 'unresolved') {
499
+ throw new Error(`${selected.reason}: ${selected.hint}`);
500
+ }
501
+ world.spawn({
502
+ component: VolumetricFog,
503
+ data: {
504
+ light: sun,
505
+ density,
506
+ boundsMin: authored.bounds.min,
507
+ boundsMax: authored.bounds.max,
508
+ extinction: authored.extinction,
509
+ albedo: authored.albedo,
510
+ emission: authored.emission,
511
+ anisotropy: authored.anisotropy,
512
+ maxDistance: authored.maxDistance,
513
+ },
514
+ }).unwrap();
515
+
516
+ world.update(1 / 60).unwrap();
517
+ const elapsed = world.getResource(Time).elapsed; // the projected volume time
518
+ void camera;
519
+ void atmosphere;
520
+ void elapsed;
521
+ ```
522
+
523
+ If validation fails, read its structured `code`, `expected`, `hint`, and typed
524
+ `detail`. If light resolution is unresolved, read its `reason`, `expected`,
525
+ `actual`, and `hint`; repair the owning authoring field, and extract again.
526
+ Do not manufacture a light or a density fallback. After a submitted frame,
527
+ `renderer.inspect().volumetricFog` reports `status`, `resourceStage`, the
528
+ selected-light facts, and candidate/LKG generations. For a renderer device
529
+ loss, wait for the host recovery boundary, call `await renderer.recover()`,
530
+ then submit the same World again and inspect the replacement generation:
531
+
532
+ ```ts
533
+ import type { World } from '@forgeax/engine-ecs';
534
+ import type { Renderer } from '@forgeax/engine-render';
535
+
536
+ // After the existing createRenderer(canvas) happy path returns a Renderer,
537
+ // call `await recoverAndDraw(renderer, world)` with the World above.
538
+ async function recoverAndDraw(renderer: Renderer, world: World): Promise<void> {
539
+ const ready = await renderer.initialization;
540
+ if (!ready.ok) throw ready.error;
541
+ const attached = renderer.attach(world);
542
+ if (!attached.ok) throw attached.error;
543
+ const lease = attached.value;
544
+ if (renderer.state() === 'device-lost') {
545
+ const recovered = await renderer.recover();
546
+ if (!recovered.ok) throw recovered.error;
547
+ }
548
+ const retry = renderer.draw({
549
+ leases: [lease],
550
+ camera: { lease },
551
+ environment: { lease },
552
+ });
553
+ if (!retry.ok) throw retry.error;
554
+ const completed = await retry.value.completed;
555
+ if (!completed.ok) throw completed.error;
556
+ const volume = renderer.inspect().volumetricFog;
557
+ console.log(volume?.status, volume?.resourceStage, volume?.generation);
558
+ }
559
+ ```
560
+
561
+ The recovery call rebuilds renderer-owned resources; it is not a second volume
562
+ clock or registry. Pausing the simulation means withholding `world.update`.
563
+ Equal time and normal monotonic World ticks up to 100 ms retain eligible
564
+ volume history. Clock rollback, a larger jump, or changed time availability
565
+ invalidates it. Camera, light, medium, size and resource changes still invalidate
566
+ history. Integration and shadow visibility use stable cell centers, so a reset
567
+ does not expose render-frame random noise. The ray uses 96 base intervals;
568
+ segments within four interval lengths of a selected punctual source use four
569
+ midpoints to resolve its inverse-square peak and spot cone. This local refinement
570
+ keeps the density expression and packed shadow resolution unchanged. A real GPU
571
+ punctual-scattering oracle protects spatial accuracy alongside the continuous-frame
572
+ browser check; homogeneous transmittance alone cannot detect light-band artifacts.
573
+ History is clipped to the current
574
+ neighborhood and its weight decays with elapsed World time (at most 0.875 per
575
+ frame, about 87 ms half-life at 60 Hz or slower), limiting trails from animated
576
+ density. This route covers authored density and time projection only; it does
577
+ not add a procedural cloud API or a Voxel integration.
578
+
335
579
  Point shadows use one renderer-owned cube-array `ShadowAtlas`. The public
336
580
  `SHADOW_ATLAS_DEFAULT_FACE_SIZE` and `SHADOW_ATLAS_DEFAULT_LAYERS` constants
337
581
  are the only capacity owner; extract assigns `shadowAtlasLayer: -1` to
@@ -368,6 +612,8 @@ rebuild or cold-cook its source, then retry the same request.
368
612
  const created = await createRenderer(canvas);
369
613
  if (!created.ok) throw created.error;
370
614
  const renderer = created.value;
615
+ const ready = await renderer.initialization;
616
+ if (!ready.ok) throw ready.error;
371
617
  const attached = renderer.attach(world);
372
618
  if (!attached.ok) throw attached.error;
373
619
  const frame = renderer.draw({
@@ -896,20 +1142,131 @@ not from inspecting shader source text for a particular output structure.
896
1142
 
897
1143
  ## Motion Blur temporal consumer
898
1144
 
899
- The Standard camera may carry the presence-enabled `MotionBlur` component. Its
900
- validated parameters are `shutterAngle` in `[0, 360]`, `maxRadiusPixels` in
901
- `[0, 64]`, and integer `sampleCount` in `[4, 16]`. Zero shutter is an explicit
902
- zero-work case. The feature reads the shared `scene-data-temporal` sampled token
903
- and contributes one raster pass after optional TAA and before Bloom. It does
904
- not own a previous frame, write TAA history, allocate storage/compute state, or
905
- cache an RHI handle.
906
-
907
- `renderer.inspect()` exposes only detached Motion Blur POD facts when the
908
- component is active: status, bounded parameters, demand, pass identity, and
909
- the invariant `historyWrites: 0`. Invalid parameters and unavailable temporal
910
- data are structured failures with owner-specific recovery; callers should fix
911
- the component or capability and retry the same frame. The hello-taa carrier
912
- contains the four-lane smoke and falsifier evidence.
1145
+ The Standard camera enables Motion Blur by carrying the presence-enabled
1146
+ component. Authoring stays bounded and validated at the component boundary:
1147
+
1148
+ ```ts
1149
+ import { MotionBlur } from '@forgeax/engine-render';
1150
+
1151
+ const result = world.addComponent(cameraEntity, {
1152
+ component: MotionBlur,
1153
+ data: { shutterAngle: 180, maxRadiusPixels: 32, sampleCount: 8, targetFps: 60 },
1154
+ });
1155
+ if (!result.ok) throw result.error;
1156
+ ```
1157
+
1158
+ | Input / state | Contract |
1159
+ |:--|:--|
1160
+ | Parameters | `shutterAngle ∈ [0, 360]`, `maxRadiusPixels ∈ [0, 64]`, integer `sampleCount ∈ [4, 16]`, integer `targetFps ∈ [0, 240]`; zero shutter/radius is zero work. |
1161
+ | Temporal source | One renderer-owned `scene-data-temporal` sampled target. Motion validity and color reactivity are separate fields. |
1162
+ | Post order | TAA resolve → Motion Blur → auto exposure/Bloom/tone output. Motion Blur never writes TAA history. |
1163
+ | WebGPU/native lane | One 16×16 tile summary dispatch plus one fused reconstruction dispatch. Long vectors use an even-pixel half-resolution source lattice with depth guidance. |
1164
+ | Bounded work | Effective color tiers are exactly `0|4|8|16`; metadata probes are fixed and color work is linear in the tier. There is no radius-squared neighborhood, per-pixel scatter, or atomic accumulation. |
1165
+ | WebGL2 fallback | One directional raster gather, explicitly reported as `raster-limited`; it does not claim compute quality or timing parity. |
1166
+
1167
+ The authored integer sample count maps to one of the four execution tiers:
1168
+ `4..7 → 4`, `8..15 → 8`, and `16 → 16`; a zero-work bypass is `0`. The
1169
+ selected tier is the total color budget for every direction, including a
1170
+ bounded fallback when all candidates are rejected. There is no implicit
1171
+ center tap added to the budget.
1172
+
1173
+ For an uncovered moving support, accepted foreground coverage reconstructs a
1174
+ bounded portion of an opaque silhouette edge rather than averaging rejected
1175
+ support as black. The matching empty-background trail is attenuated by the
1176
+ same fixed factor, so the correction redistributes energy and does not add
1177
+ another sample. If no source is accepted, the receiver's center color remains
1178
+ the conservative fallback. A recovered midpoint anchor is still only partial
1179
+ evidence. Compute and raster-limited lanes share the rule without an extra
1180
+ pass or radius-sized neighborhood.
1181
+
1182
+ Reset and recovery remain observable through the existing temporal owner:
1183
+
1184
+ ```ts
1185
+ const inspection = renderer.inspect();
1186
+ if (
1187
+ inspection.motionBlur?.status === 'reset' &&
1188
+ inspection.motionBlur.resetReason === 'time-discontinuity' &&
1189
+ inspection.temporal.resetReason === 'time-discontinuity'
1190
+ ) {
1191
+ // The next successful draw commits the new baseline; retry the same renderer.
1192
+ renderer.draw(frameInput());
1193
+ }
1194
+ ```
1195
+
1196
+ After a rejected graph or queue submit, `motionBlur.lastFailure` is
1197
+ `'submit-failure'` while the previous successful temporal view remains the
1198
+ baseline. A `'scene-data-unavailable'` failure means the shared
1199
+ `standard-scene-data` producer or its `rgba16float` capability was not
1200
+ admitted; repair that producer/capability and retry the same frame. It never
1201
+ creates a private Motion Blur history target.
1202
+
1203
+ The renderer carries the raw host render sample timestamp through App/Worker
1204
+ and accepts it only after a successful submit. A gap greater than `100 ms`
1205
+ creates a `time-discontinuity` reset; a normal `30 Hz` interval remains valid,
1206
+ and a failed submit cannot advance the accepted temporal clock. Pause/resume
1207
+ also marks the next frame as a new baseline. Instance pose updates preserve
1208
+ the last submitted transform. Collection replacement mints new identity
1209
+ generations by default; a producer that keeps an instance through compaction
1210
+ or reorder can pass its prior generation tokens in the new order, allowing the
1211
+ record stage to pair each current transform with the real prior transform.
1212
+
1213
+ `renderer.inspect().motionBlur` exposes detached POD facts: status (`off`,
1214
+ `active`, `reset`, `limited`, or `invalid`), selected lane, effective tier,
1215
+ pass count, reset reason, last failure, and `historyWrites: 0`. Invalid public
1216
+ parameters return `MotionBlurValidationError` with code
1217
+ `motion-blur-invalid-params`; repair the named component field or capability
1218
+ and retry the same frame. The typed cause is preserved by the public draw
1219
+ result, so a caller can repair the named field without parsing `Error.message`:
1220
+
1221
+ ```ts
1222
+ const result = renderer.draw(frameInput);
1223
+ if (!result.ok && result.error.code === 'motion-blur-invalid-params') {
1224
+ console.error(result.error.detail.field, result.error.detail.value);
1225
+ // Repair the component field, then retry the same frame.
1226
+ }
1227
+ ```
1228
+
1229
+ The hello-taa carrier contains the Dawn, Browser, WebGL2, RhiNull, performance,
1230
+ and source-level falsifier paths.
1231
+
1232
+ ## Multiscale HDR Bloom
1233
+
1234
+ Bloom is a Camera-owned Standard post feature. `bloom` selects the enabled
1235
+ path, while `bloomIntensity` is bounded to `[0, 8]`, `bloomThreshold` to
1236
+ `[0, 65504]`, `bloomSoftKnee` to `[0, 1]`, and `bloomScatter` to `[0, 0.95]`.
1237
+ The defaults are intensity `1`, threshold `1`, soft knee `0.5`, and scatter
1238
+ `0.7`; the disabled value and intensity `0` are exact zero-work cases.
1239
+
1240
+ Game authors import this contract through the public umbrella facade:
1241
+
1242
+ ```ts
1243
+ import {
1244
+ BLOOM_ENABLED,
1245
+ Camera,
1246
+ TONEMAP_REINHARD_EXTENDED,
1247
+ } from '@forgeax/engine/render';
1248
+ ```
1249
+
1250
+ The physical `@forgeax/engine-render` package remains the repository ownership
1251
+ unit; it is not the recommended consumer import path.
1252
+
1253
+ An enabled frame stays in the linear HDR domain and uses one typed graph:
1254
+ five ceil-halved downsample levels `D0..D4`, four tent upsample levels
1255
+ `U3..U0`, then one HDR composite before tone mapping. `D0` applies the soft
1256
+ threshold per source texel and weights each texel by its exact source/target
1257
+ rectangle overlap, including odd extents. `D1..D4` use the shared 13-tap
1258
+ reduction; each upsample blends the current level with the next coarser level
1259
+ using the authored scatter. Composite adds only RGB bloom and preserves the
1260
+ scene alpha.
1261
+
1262
+ `renderer.inspect().bloom` is detached POD evidence from the compiled graph.
1263
+ Its level dimensions, `targetBytes`, pass topology, and
1264
+ `residentChildBytes` come from validated descriptors and active resource
1265
+ ownership, not from an expected-size calculation. A missing, forged, or
1266
+ invalid descriptor graph reports `invalid`; an off camera reports `empty` and
1267
+ zero target/resident bytes. Candidate generations remain behind the existing
1268
+ LKG fence and are released only after the submit fence; device recovery
1269
+ rehydrates the same Bloom bundle before a new receipt is accepted.
913
1270
 
914
1271
  > [!IMPORTANT]
915
1272
  > Render consumes the effective MaterialAsset snapshot produced by extract. Each texture slot carries its own coordinate set and transform into the built-in PBR binding layout; render records do not reinterpret authoring fields or manufacture shader artifacts. The effective `passes` are already validated.
@@ -947,7 +1304,7 @@ base-only root keeps `forward + deferred + shadow`, while declaring any
947
1304
  second-stage layer or physical texture selects `forward + shadow`.
948
1305
 
949
1306
  ```ts
950
- import { Materials } from '@forgeax/engine-render';
1307
+ import { Materials } from '@forgeax/engine/render';
951
1308
 
952
1309
  const material = Materials.standard({
953
1310
  baseColor: [0.72, 0.48, 0.22, 1],
@@ -1089,10 +1446,14 @@ renderer.instances.update(collection.collectionId, {
1089
1446
  transforms: patch,
1090
1447
  });
1091
1448
 
1092
- // Replacement keeps the logical identity and publishes a new revision.
1449
+ // Replacement publishes a new revision. Pass prior generation tokens in their
1450
+ // new order when compaction/reorder preserves logical instance identity;
1451
+ // omitted tokens intentionally seed replacement rows.
1093
1452
  renderer.instances.replace(collection.collectionId, identityMatrices(instanceCount + 1));
1094
1453
  const current = renderer.instances.inspect(collection.collectionId).unwrap();
1095
1454
  const detached = renderer.instances.snapshot(collection.collectionId).unwrap();
1455
+ // A read-only observation does not advance the renderer upload cursor.
1456
+ const observed = renderer.instances.peek(collection.collectionId).unwrap();
1096
1457
  renderer.instances.release(collection.collectionId);
1097
1458
  ```
1098
1459
 
@@ -1101,7 +1462,9 @@ storage, `replace` publishes a complete new revision (and grows capacity when
1101
1462
  needed), `update` validates a bounded matrix interval and records its dirty
1102
1463
  range, `inspect` returns identity/count/capacity/revision, `snapshot` returns a
1103
1464
  detached matrix copy plus dirty ranges for extraction, and `release` makes the
1104
- collection id terminal. A caller never provides a GPU buffer or maintains
1465
+ collection id terminal. `peek` returns the same detached matrices without
1466
+ advancing the renderer upload cursor, so read-only inspection cannot hide a
1467
+ later dirty range. A caller never provides a GPU buffer or maintains
1105
1468
  backend-specific chunks. `detached.transforms` is an observation, not mutable
1106
1469
  renderer state.
1107
1470
 
@@ -1393,8 +1756,8 @@ separate cache/recovery authority.
1393
1756
  > Owner: render vocabulary and the extract → prepare → record frame boundary. Runtime selects concrete services and calls this package; it does not re-own these tokens.
1394
1757
 
1395
1758
  ```ts
1396
- import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
1397
- import { createRenderer } from '@forgeax/engine-runtime';
1759
+ import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine/render';
1760
+ import { createRenderer } from '@forgeax/engine/runtime';
1398
1761
 
1399
1762
  const renderer = await createRenderer(canvas);
1400
1763
  const attached = renderer.attach(world);
@@ -1908,10 +2271,10 @@ ownership is [`@forgeax/engine-render-graph`](../render-graph/README.md).
1908
2271
  Dynamic consumers use the same boundary explicitly:
1909
2272
 
1910
2273
  ```ts
1911
- const { Camera, MeshFilter, MeshRenderer } = await import('@forgeax/engine-render');
2274
+ const { Camera, MeshFilter, MeshRenderer } = await import('@forgeax/engine/render');
1912
2275
  ```
1913
2276
 
1914
- `@forgeax/engine-runtime` remains the host assembly entry for `createRenderer` and backend policy. Import `Materials` from this package. Runtime is not a compatibility barrel for render components.
2277
+ `@forgeax/engine/runtime` remains the host assembly entry for `createRenderer` and backend policy. Import `Materials` from `@forgeax/engine/render`. Runtime is not a compatibility barrel for render components.
1915
2278
 
1916
2279
  Optional text, tilemap, and sprite authoring is intentionally isolated from the
1917
2280
  base vocabulary:
@@ -2290,16 +2653,66 @@ existing observable state; resource disposal is distinct from JavaScript GC.
2290
2653
 
2291
2654
  ### Volumetric density coordinates
2292
2655
 
2293
- `VolumetricFog.density` is a linear scalar field over `boundsMin` to `boundsMax`.
2294
- World positions map to `(position - boundsMin) / (boundsMax - boundsMin)` and
2295
- sample with clamp-to-edge addressing. The renderer clamps negative density at
2296
- the optical-depth boundary, but adds no bias, octave synthesis, repetition or
2297
- animation. White density means one; zero density contributes no extinction or
2298
- scattering. Noise synthesis belongs to the asset producer. The existing volume
2299
- passes integrate this field against scene depth before output tonemapping.
2300
-
2301
- Density integration uses fixed segment midpoints and the profile's logical
2302
- froxel depth, including the clipped final segment. It does not rotate density
2303
- samples with frame number: static authored air must not create temporal noise.
2304
- Shadow visibility uses matching froxel centers without per-frame XY jitter or
2305
- stochastic byte dithering. Shared PCF shadow filtering remains in effect.
2656
+ `VolumetricFog.density` is a producer-owned linear 3D source tile. The volume
2657
+ integrator samples that tile in repeated world-space coordinates, combines the
2658
+ same three scales used by the approved fog fixture, and uses World time for the
2659
+ continuous advection term. The renderer keeps the source texture resident; it
2660
+ does not rebuild noise data for each frame. Its source expression is mapped to a
2661
+ signed density and clamped only at the optical-depth boundary so Beer-Lambert
2662
+ extinction remains non-negative.
2663
+
2664
+ Density integration uses fixed midpoints over 96 base ray intervals, including
2665
+ the clipped final segment. Segments near a selected PointLight or SpotLight
2666
+ receive four local midpoints to resolve the inverse-square peak without making
2667
+ the whole ray uniformly expensive. Frame identity does not rotate density
2668
+ samples. Shadow visibility uses matching froxel centers without per-frame XY
2669
+ jitter or stochastic byte dithering; shared PCF shadow filtering remains in
2670
+ effect.
2671
+
2672
+ ## Camera Depth of Field
2673
+
2674
+ `DepthOfField` is the built-in Standard camera effect for ordinary meshes. Add it
2675
+ only to the active perspective camera; component removal or
2676
+ `maxRadiusPixels: 0` declares the exact zero-work path. The side and quality
2677
+ fields use the closed numeric values exported by this package:
2678
+
2679
+ ```ts
2680
+ import {
2681
+ DepthOfField,
2682
+ DepthOfFieldQualityValue,
2683
+ DepthOfFieldSideValue,
2684
+ } from '@forgeax/engine-render';
2685
+
2686
+ world.addComponent(camera, {
2687
+ component: DepthOfField,
2688
+ data: {
2689
+ focusDistance: 8,
2690
+ fStop: 1.4,
2691
+ sensorHeight: 0.024,
2692
+ maxRadiusPixels: 16,
2693
+ quality: DepthOfFieldQualityValue.medium,
2694
+ blurSide: DepthOfFieldSideValue.both,
2695
+ },
2696
+ });
2697
+ ```
2698
+
2699
+ The near, far and both presets share one signed thin-lens CoC. `quality` changes
2700
+ sampling density only, so it does not change the optical radius. The graph uses
2701
+ the Standard `RenderExtent`: full-resolution CoC/composite targets follow the
2702
+ current internal or output domain, and half-resolution targets use
2703
+ `max(1, ceil(axis / 2))`. Current temporal-v1 depth validity and the resolved
2704
+ multisampled depth binding remain the source of truth.
2705
+
2706
+ After a real `draw()` submission, read `renderer.inspect().depthOfField` for the
2707
+ requested component values, effective submitted values, graph/device
2708
+ generations, extents, pass count and descriptor-derived bytes. `lastKnownGood`
2709
+ and `effective` stay bound to the accepted submission; a failed candidate keeps
2710
+ the prior accepted projection and reports its failure reason for recovery.
2711
+ When the active camera has no `DepthOfField` component, the inspection remains
2712
+ available with `status: 'off'` and zero DoF graph facts. Invalid fields and an
2713
+ orthographic camera stay on the normal camera snapshot path with structured
2714
+ `error.code`, `error.expected`, `error.hint`, and `error.detail`; they report
2715
+ `status: 'invalid'` or `status: 'unsupported'` without admitting a DoF graph.
2716
+ The Engine Preview/game capability lab uses this same component on its ordinary
2717
+ mesh camera, and its settings control toggles component presence through the
2718
+ normal World path.
@@ -0,0 +1,3 @@
1
+ import type { RenderSystem } from '../render-system';
2
+ export declare function registerAnalyticFogPostProcess(renderSystem: Pick<RenderSystem, 'registerBuiltinPostProcess'>, source: string, candidate: boolean): void;
3
+ //# sourceMappingURL=analytic-fog-registration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytic-fog-registration.d.ts","sourceRoot":"","sources":["../../src/assembly/analytic-fog-registration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,4BAA4B,CAAC,EAC9D,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,OAAO,GACjB,IAAI,CAON"}
@@ -0,0 +1,4 @@
1
+ import type { RenderSystem } from '../render-system';
2
+ /** Register the built-in FXAA post-process once for the active generation. */
3
+ export declare function registerFxaaPostProcess(renderSystem: Pick<RenderSystem, 'registerBuiltinPostProcess'>, source: string, registered: boolean): boolean;
4
+ //# sourceMappingURL=fxaa-registration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fxaa-registration.d.ts","sourceRoot":"","sources":["../../src/assembly/fxaa-registration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,8EAA8E;AAC9E,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,4BAA4B,CAAC,EAC9D,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,OAAO,GAClB,OAAO,CAQT"}
@@ -0,0 +1,4 @@
1
+ import type { FrameReceipt } from '../render-contract';
2
+ import type { RenderSystem } from '../render-system';
3
+ export declare function observeLodOcclusionForReceipt(renderSystem: Pick<RenderSystem, 'observeLodOcclusion'>, receipt: Pick<FrameReceipt, 'frameId' | 'deviceGeneration'>): Promise<void>;
4
+ //# sourceMappingURL=lod-observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lod-observation.d.ts","sourceRoot":"","sources":["../../src/assembly/lod-observation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,qBAAqB,CAAC,EACvD,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,kBAAkB,CAAC,GAC1D,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -0,0 +1,3 @@
1
+ import type { RenderSystem } from '../render-system';
2
+ export declare function registerMotionBlurPostProcess(renderSystem: Pick<RenderSystem, 'registerBuiltinPostProcess'>, source: string): void;
3
+ //# sourceMappingURL=motion-blur-registration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion-blur-registration.d.ts","sourceRoot":"","sources":["../../src/assembly/motion-blur-registration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,4BAA4B,CAAC,EAC9D,MAAM,EAAE,MAAM,GACb,IAAI,CASN"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderer-facade.d.ts","sourceRoot":"","sources":["../../src/assembly/renderer-facade.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,sBAAsB,EAE5B,MAAM,kBAAkB,CAAC;AAM1B,OAAO,KAAK,EAEV,QAAQ,EAKR,aAAa,EAEd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGlE,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,0BAA0B,GAAG,QAAQ,CA6M7E;AAMD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAG7C;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,sBAAsB,CA0BjG;AA2DD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CA6ChF;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAMzE"}
1
+ {"version":3,"file":"renderer-facade.d.ts","sourceRoot":"","sources":["../../src/assembly/renderer-facade.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,sBAAsB,EAE5B,MAAM,kBAAkB,CAAC;AAO1B,OAAO,KAAK,EAEV,QAAQ,EAKR,aAAa,EAEd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGlE,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,0BAA0B,GAAG,QAAQ,CA8M7E;AAMD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAG7C;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,sBAAsB,CA0BjG;AA2DD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CA6ChF;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAMzE"}
@@ -0,0 +1,13 @@
1
+ import type { Result, RhiDevice, RhiError, ShaderModule } from '@forgeax/engine-rhi';
2
+ import type { ManifestEntry } from '@forgeax/engine-types';
3
+ export type AsyncShaderModuleFactory = ((device: RhiDevice, desc: {
4
+ readonly code: string;
5
+ readonly label?: string;
6
+ }) => Promise<Result<ShaderModule, RhiError>>) | undefined;
7
+ /** Create the shared readiness prewarmer for temporal/fullscreen shader entries. */
8
+ export declare function createTemporalShaderPrewarmer(input: {
9
+ readonly device: RhiDevice;
10
+ readonly asyncCreateShaderModule: AsyncShaderModuleFactory;
11
+ readonly seedShaderModule: (label: string, module: ShaderModule) => void;
12
+ }): (entry: ManifestEntry | undefined, id: string) => Promise<void>;
13
+ //# sourceMappingURL=temporal-post-process-prewarm.d.ts.map