@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
@@ -58,6 +58,7 @@ import {
58
58
  registerDepthOfFieldBuiltins,
59
59
  } from '../features/depth-of-field/depth-of-field-assembly';
60
60
  import { createRenderFeatureHost } from '../features/host';
61
+ import { motionBlurDrawOptions } from '../features/motion-blur/motion-blur-runtime';
61
62
  import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
62
63
  import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
63
64
  import {
@@ -66,7 +67,7 @@ import {
66
67
  } from '../fullscreen-post-process-pass';
67
68
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
68
69
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
69
- import type { BloomInspection } from '../inspection-types';
70
+ import { type BloomInspection, emptyBloomInspection } from '../inspection-types';
70
71
  import { DeviceScope } from '../lifecycle';
71
72
  import {
72
73
  createHdrpBindGroupLayoutDescriptor,
@@ -131,7 +132,7 @@ import type {
131
132
  SsrShaderSources,
132
133
  VolumetricFogShaderSources,
133
134
  } from '../render-contract';
134
- import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
135
+ import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
135
136
  import {
136
137
  attachGpuPassTimingSession,
137
138
  createRenderSystem,
@@ -146,10 +147,13 @@ import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-jo
146
147
  import type { RenderTarget } from '../targets/contracts';
147
148
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
148
149
  import type { TypedFrameObservationCapture } from '../typed-render-graph-primitives';
150
+ import { registerAnalyticFogPostProcess } from './analytic-fog-registration';
149
151
  import type { RhiBackendPack } from './backend-contract';
150
152
  import type { BundlerOptions } from './bundler-contract';
151
153
  import { createRendererDynamicGeometryController } from './dynamic-geometry-host';
154
+ import { registerFxaaPostProcess } from './fxaa-registration';
152
155
  import type { RendererAssemblyImplementation } from './host-contract';
156
+ import { observeLodOcclusionForReceipt } from './lod-observation';
153
157
  import {
154
158
  isEngineOwnedMaterialShader,
155
159
  resolveMaterialPipelineBindGroups,
@@ -177,6 +181,7 @@ import {
177
181
  shouldDeferMissingPreparedMaterialShader,
178
182
  } from './material-shader-policy';
179
183
  import type { MeshSsboGrowResult, MeshSsboState } from './mesh-ssbo-grow';
184
+ import { registerMotionBlurPostProcess } from './motion-blur-registration';
180
185
  import {
181
186
  type GenerationAggregate,
182
187
  type GenerationPublication,
@@ -1339,22 +1344,14 @@ async function makeWebGPURenderer(
1339
1344
  tonemapRegistered = true;
1340
1345
  },
1341
1346
  (source: string) => {
1342
- if (fxaaRegistered) return;
1343
- renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
1344
- source,
1345
- params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1346
- reads: ['ldrColor'],
1347
- });
1348
- fxaaRegistered = true;
1347
+ fxaaRegistered = registerFxaaPostProcess(renderSystem, source, fxaaRegistered);
1349
1348
  },
1349
+ (source: string) =>
1350
+ registerAnalyticFogPostProcess(renderSystem, source, candidateBuildState !== undefined),
1350
1351
  (entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
1351
1352
  if (candidateBuildState !== undefined || temporalPostProcessesRegistered) return;
1352
1353
  if (entries.motionBlur !== undefined) {
1353
- renderSystem.registerBuiltinPostProcess('forgeax.motion-blur', {
1354
- source: entries.motionBlur,
1355
- params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1356
- reads: ['scene-color', 'scene-temporal'],
1357
- });
1354
+ registerMotionBlurPostProcess(renderSystem, entries.motionBlur);
1358
1355
  }
1359
1356
  if (entries.taaResolve !== undefined) {
1360
1357
  renderSystem.registerBuiltinPostProcess('forgeax.taa-resolve', {
@@ -2718,22 +2715,7 @@ async function makeWebGPURenderer(
2718
2715
  : 'alive';
2719
2716
  const graph = deviceLost ? undefined : renderSystem.perFrameGraphInfo;
2720
2717
  const standardLighting = deviceLost ? undefined : renderSystem.standardLightingInspection;
2721
- const bloom: BloomInspection = deviceLost
2722
- ? {
2723
- graphStatus: 'empty',
2724
- enabled: false,
2725
- targetCount: 0,
2726
- targetBytes: 0,
2727
- resourceCount: 0,
2728
- passCount: 0,
2729
- encodeCount: 0,
2730
- bindGroupCount: 0,
2731
- uploadCount: 0,
2732
- residentChildBytes: 0,
2733
- generation: 0,
2734
- state: 'off',
2735
- }
2736
- : renderSystem.bloom;
2718
+ const bloom: BloomInspection = deviceLost ? emptyBloomInspection() : renderSystem.bloom;
2737
2719
  const surfaceStorage = pipelineState?.format ?? 'rgba8unorm';
2738
2720
  const surfaceDisplay = pipelineState?.colorAttachmentFormat ?? 'rgba8unorm';
2739
2721
  const observationId = `${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}:frame-${frameId}`;
@@ -3226,6 +3208,7 @@ async function makeWebGPURenderer(
3226
3208
  ...(frameRequest.camera.entityKey === undefined
3227
3209
  ? {}
3228
3210
  : { cameraEntityKey: frameRequest.camera.entityKey }),
3211
+ ...motionBlurDrawOptions(frameRequest),
3229
3212
  ...(frameRequest.profileFrame === undefined
3230
3213
  ? {}
3231
3214
  : { profileFrame: frameRequest.profileFrame }),
@@ -3377,6 +3360,8 @@ async function makeWebGPURenderer(
3377
3360
  const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3378
3361
  volumeTimingCapture?.discard();
3379
3362
  if (cleanupFailure !== undefined) return err(cleanupFailure);
3363
+ const invalidMotionBlur = renderSystem.motionBlurInvalidParams;
3364
+ if (invalidMotionBlur !== undefined) return err(invalidMotionBlur);
3380
3365
  return err(
3381
3366
  new RendererContractFailureError(
3382
3367
  'draw',
@@ -3715,7 +3700,7 @@ async function makeWebGPURenderer(
3715
3700
  receiptObservationBuffers.delete(receipt);
3716
3701
  }
3717
3702
  }
3718
- await renderSystem.observeLodOcclusion();
3703
+ await observeLodOcclusionForReceipt(renderSystem, receipt);
3719
3704
  const targetReadbacks =
3720
3705
  request.targetReadbacks === undefined || request.targetReadbacks.length === 0
3721
3706
  ? undefined
@@ -0,0 +1,21 @@
1
+ import type { BloomEnabled, Tonemap } from './components/camera';
2
+
3
+ /**
4
+ * The one effective Standard Bloom admission used by graph, resource, and
5
+ * record owners. Authoring remains Camera-owned; this is only the derived
6
+ * zero/non-zero topology decision.
7
+ */
8
+ export interface BloomAdmissionCamera {
9
+ readonly bloom: BloomEnabled;
10
+ readonly bloomIntensity: number;
11
+ readonly tonemap: Tonemap;
12
+ }
13
+
14
+ export function standardBloomAdmitted(camera: BloomAdmissionCamera | undefined): boolean {
15
+ return (
16
+ camera !== undefined &&
17
+ camera.bloom === 'on' &&
18
+ camera.bloomIntensity > 0 &&
19
+ camera.tonemap !== 'none'
20
+ );
21
+ }
@@ -8,6 +8,7 @@ describe('MotionBlur component schema', () => {
8
8
  shutterAngle: { type: 'f32', default: 180 },
9
9
  maxRadiusPixels: { type: 'f32', default: 32 },
10
10
  sampleCount: { type: 'f32', default: 8 },
11
+ targetFps: { type: 'f32', default: 60 },
11
12
  });
12
13
  expect(MotionBlur.fields).not.toHaveProperty('enabled');
13
14
  });
@@ -7,4 +7,8 @@ export const Atmosphere = defineComponent('Atmosphere', {
7
7
  mieCoefficient: { type: 'f32', default: 0.005 },
8
8
  mieDirectionalG: { type: 'f32', default: 0.8 },
9
9
  sunAngularRadius: { type: 'f32', default: 0.004675 },
10
+ /** Independent multiplier for the analytic circumsolar lobe. */
11
+ circumsolarStrength: { type: 'f32', default: 1 },
12
+ /** Relative angular width of the analytic circumsolar lobe. */
13
+ circumsolarWidth: { type: 'f32', default: 1 },
10
14
  });
@@ -1,9 +1,7 @@
1
1
  // @forgeax/engine-render - Camera (projection variant + ortho extension).
2
2
  //
3
- // Schema: 9 f32 columns perspective quartet (fov + aspect + near + far)
4
- // + projection discriminator (0 = perspective, 1 = orthographic) + ortho
5
- // quartet (left + right + bottom + top). The view matrix continues to be
6
- // derived from the entity's Transform (AC-06 case B fires
3
+ // Schema: projection, output, Bloom, and lifecycle columns. The view matrix
4
+ // continues to be derived from the entity's Transform (AC-06 case B fires
7
5
  // 'render-system-no-camera' when 0 such entities exist).
8
6
  //
9
7
  // Projection discriminator:
@@ -386,7 +384,7 @@ export function antialiasFromF32(value: number): Antialias {
386
384
  *
387
385
  * Two members:
388
386
  * `'off'` - default; no bloom post-processing (zero-overhead opt-out path)
389
- * `'on'` - bloom bright-pass + separable blur + composite pipeline
387
+ * `'on'` - five-level HDR extraction/downsample/tent-upsample pipeline
390
388
  *
391
389
  * Bloom is a discrete enum (0/1) — illegal values fail-fast with the closed
392
390
  * CameraError contract (charter P3), matching antialiasFromF32 precedent.
@@ -398,6 +396,19 @@ export const BLOOM_DISABLED = 0;
398
396
  /** Numeric encoding of bloom enabled (schema value for `bloom`). */
399
397
  export const BLOOM_ENABLED = 1;
400
398
 
399
+ /** Bloom intensity is a linear HDR add-back multiplier. */
400
+ export const CAMERA_BLOOM_INTENSITY_MIN = 0;
401
+ export const CAMERA_BLOOM_INTENSITY_MAX = 8;
402
+ /** Bloom threshold is expressed in scene-linear Rec.709 luminance. */
403
+ export const CAMERA_BLOOM_THRESHOLD_MIN = 0;
404
+ export const CAMERA_BLOOM_THRESHOLD_MAX = 65504;
405
+ /** Soft-knee transition width relative to the threshold. */
406
+ export const CAMERA_BLOOM_SOFT_KNEE_MIN = 0;
407
+ export const CAMERA_BLOOM_SOFT_KNEE_MAX = 1;
408
+ /** Coarse pyramid contribution. This is a blend, not a blur radius. */
409
+ export const CAMERA_BLOOM_SCATTER_MIN = 0;
410
+ export const CAMERA_BLOOM_SCATTER_MAX = 0.95;
411
+
401
412
  /**
402
413
  * Map a `camera.bloom` numeric value to the closed `BloomEnabled` string-literal
403
414
  * union. Invalid values fail-fast with structured error (charter P3).
@@ -414,6 +425,49 @@ export function bloomEnabledFromF32(value: number): BloomEnabled {
414
425
  );
415
426
  }
416
427
 
428
+ function validateBloomRange(field: string, value: number, min: number, max: number): void {
429
+ if (!Number.isFinite(value) || value < min || value > max) {
430
+ throw cameraError(
431
+ 'camera-bloom-invalid',
432
+ field,
433
+ value,
434
+ `a finite value in [${min}, ${max}]`,
435
+ `set Camera.${field} to a finite value in [${min}, ${max}]`,
436
+ );
437
+ }
438
+ }
439
+
440
+ /** Validate the complete Camera-owned Bloom contract at the extract boundary. */
441
+ export function validateCameraBloom(
442
+ bloom: number,
443
+ threshold: number,
444
+ intensity: number,
445
+ softKnee: number,
446
+ scatter: number,
447
+ ): BloomEnabled {
448
+ const enabled = bloomEnabledFromF32(bloom);
449
+ validateBloomRange(
450
+ 'bloomThreshold',
451
+ threshold,
452
+ CAMERA_BLOOM_THRESHOLD_MIN,
453
+ CAMERA_BLOOM_THRESHOLD_MAX,
454
+ );
455
+ validateBloomRange(
456
+ 'bloomIntensity',
457
+ intensity,
458
+ CAMERA_BLOOM_INTENSITY_MIN,
459
+ CAMERA_BLOOM_INTENSITY_MAX,
460
+ );
461
+ validateBloomRange(
462
+ 'bloomSoftKnee',
463
+ softKnee,
464
+ CAMERA_BLOOM_SOFT_KNEE_MIN,
465
+ CAMERA_BLOOM_SOFT_KNEE_MAX,
466
+ );
467
+ validateBloomRange('bloomScatter', scatter, CAMERA_BLOOM_SCATTER_MIN, CAMERA_BLOOM_SCATTER_MAX);
468
+ return enabled;
469
+ }
470
+
417
471
  /**
418
472
  * Camera projection parameters (perspective + orthographic variants) +
419
473
  * tonemap trio (mode + exposure + whitePoint).
@@ -447,8 +501,9 @@ export function bloomEnabledFromF32(value: number): BloomEnabled {
447
501
  * N>1 fires 'render-system-multi-camera' + uses first). Multi-viewport is
448
502
  * OOS (see feat-future-multi-viewport). The orthographic path reuses the
449
503
  * same near / far as the perspective path — both variants share the single
450
- * Camera archetype (17 scalar f32 columns + one historyVersion u32 column +
451
- * the `clearColor` array<f32,4> column + the `autoAspect` bool column).
504
+ * Camera archetype (30 schema fields, including the Bloom controls, one
505
+ * historyVersion u32 column, the `clearColor` array<f32,4> column, and the
506
+ * `autoAspect` bool column).
452
507
  *
453
508
  * @example Perspective camera at (0, 0, 3) looking down -Z (zero-config tonemap):
454
509
  * world.spawn(
@@ -514,7 +569,8 @@ export const Camera = defineComponent('Camera', {
514
569
  bloom: { type: 'f32', default: 0 },
515
570
  bloomThreshold: { type: 'f32', default: 1.0 },
516
571
  bloomIntensity: { type: 'f32', default: 1.0 },
517
- bloomBlurRadius: { type: 'f32', default: 4.0 },
572
+ bloomSoftKnee: { type: 'f32', default: 0.5 },
573
+ bloomScatter: { type: 'f32', default: 0.7 },
518
574
  // M1 target camera role: zero means this camera is eligible for display;
519
575
  // non-zero shared targets are auxiliary producers whose views and receipt
520
576
  // promotion remain owned by Renderer.
@@ -544,7 +600,7 @@ export const Camera = defineComponent('Camera', {
544
600
 
545
601
  // ─── Camera POD type (derived from Camera token — single source, AC-07) ─────
546
602
  //
547
- // ShapeOf<SchemaOf<typeof Camera>> resolves the 20-field POD from the Camera
603
+ // ShapeOf<SchemaOf<typeof Camera>> resolves the 30-field POD from the Camera
548
604
  // token's schema, which is itself derived from Camera.fields[k].type (D-A7).
549
605
  // This replaces the hand-maintained CameraDataPod interface — the field set
550
606
  // lives exclusively in the Camera component definition above.
@@ -626,7 +682,7 @@ function cameraColorGradingColumns(
626
682
 
627
683
  // ─── Camera factory functions (w13 SSOT refactoring) ─────────────────────
628
684
  //
629
- // Standalone factory functions that return 20-field CameraPod objects
685
+ // Standalone factory functions that return 29-field CameraPod objects
630
686
  // matching the Camera component column shape. Not static methods because
631
687
  // TypeScript const-namespace merge is not supported, and Object.assign
632
688
  // would break the Camera token's reference identity (archetype columns /
@@ -10,8 +10,8 @@
10
10
  //
11
11
  // AI users spawn with the assets-runtime constants `HANDLE_CUBE` /
12
12
  // `HANDLE_TRIANGLE` (now branded `Handle<'MeshAsset','shared'>` to
13
- // match the schema-derived shape); custom mesh registration is owned by
14
- // feat-future-asset-system (this MVP only exposes builtin handles).
13
+ // match the schema-derived shape). A loaded or custom MeshAsset payload is
14
+ // bound by allocating a World shared reference before publishing this field.
15
15
  //
16
16
  // Naming flavor: unity-style "MeshFilter / MeshRenderer" pair, but the forgeax
17
17
  // pair does NOT mirror Unity's filter-toggle semantics - MeshFilter only
@@ -22,12 +22,13 @@
22
22
  // query never matches -> entity is silently absent from the
23
23
  // RenderableSnapshot[] (NO default-material fallback, NO onError fire);
24
24
  // case B (MeshRenderer.material omitted at spawn) -> mid-grey
25
- // defaultMaterialSnapshot fallback (no onError); case C (material handle
25
+ // defaultMaterialSnapshot fallback (no onError); an unbound scalar shared<T>
26
+ // field (slot 0) is also not a renderable; case C (non-zero material handle
26
27
  // unresolved) -> RhiError 'asset-not-registered' (mirrors the
27
28
  // MeshFilter.assetHandle dangling-ref path).
28
29
  //
29
30
  // charter mapping: proposition 1 (single import) + proposition 4 (explicit
30
- // failure: missing or unregistered handle fires onError 'asset-not-registered'
31
+ // failure: a non-zero unregistered handle fires onError 'asset-not-registered'
31
32
  // with .detail = { assetHandle } + cross-asset brand mismatch is a TS
32
33
  // compile-time error) + proposition 5 (consistent abstraction: the schema
33
34
  // vocab `'shared<T>'` is the SSOT for AssetRegistry-owned handles across
@@ -39,12 +40,13 @@ import { defineComponent } from '@forgeax/engine-ecs';
39
40
  * Mesh filter (geometry asset reference).
40
41
  *
41
42
  * `assetHandle` carries a `Handle<'MeshAsset', 'shared'>` (u32-stored)
42
- * pointing into `engine.assets: AssetRegistry`. Use the predefined
43
- * constants `HANDLE_CUBE` / `HANDLE_TRIANGLE` exported from
44
- * `@forgeax/engine-render`; custom-mesh registration is OOS in MVP (see
45
- * feat-future-asset-system).
43
+ * pointing to a mesh payload in the current World shared-reference / builtin
44
+ * handle namespace. An omitted field defaults to slot `0`, the unbound
45
+ * sentinel; an entity with that value is not emitted as a renderable. Use the
46
+ * predefined constants `HANDLE_CUBE` / `HANDLE_TRIANGLE` for builtins and
47
+ * allocate a World shared reference for a loaded mesh payload.
46
48
  *
47
- * Error path: if the handle is not registered at draw time, RenderSystem
49
+ * Error path: if a non-zero handle is not registered at draw time, RenderSystem
48
50
  * fires `Renderer.onError` with
49
51
  * `RhiError({ code: 'asset-not-registered', detail: { assetHandle } })`
50
52
  * and skips this entity (other entities continue rendering; charter
@@ -5,4 +5,5 @@ export const MotionBlur = defineComponent('MotionBlur', {
5
5
  shutterAngle: { type: 'f32', default: 180 },
6
6
  maxRadiusPixels: { type: 'f32', default: 32 },
7
7
  sampleCount: { type: 'f32', default: 8 },
8
+ targetFps: { type: 'f32', default: 60 },
8
9
  });
@@ -75,7 +75,6 @@ import {
75
75
  type TextureAsset,
76
76
  type TextureShape,
77
77
  type MeshAsset as TypesMeshAsset,
78
- toShared,
79
78
  } from '@forgeax/engine-types';
80
79
  import { GpuBuffer, GpuTexture } from '../gpu-resource';
81
80
  import {
@@ -570,6 +569,12 @@ export class GpuResidencyCache {
570
569
  // equirect source always resolves to the same cubemap (idempotent, A2).
571
570
  private readonly cubemapIdempotentMap: Map<number, Handle<'EquirectAsset', 'shared'>> = new Map();
572
571
  private readonly meshGpuHandles: Map<string | number, MeshGpuHandles> = new Map();
572
+ /**
573
+ * Lower-detail POD identities packed into each resident mesh entry. The
574
+ * GPU entry only carries byte ranges; keeping the source identities beside
575
+ * it lets a later draw upgrade a root-only upload without hashing vertices.
576
+ */
577
+ private readonly meshLodAssets = new WeakMap<MeshGpuHandles, readonly TypesMeshAsset[]>();
573
578
  private readonly meshLifetimes = new WeakMap<MeshGpuHandles, MeshResidencyLifetime>();
574
579
  private readonly retiredMeshes = new Set<MeshGpuHandles>();
575
580
 
@@ -832,7 +837,7 @@ export class GpuResidencyCache {
832
837
  handle: Handle<'MeshAsset', 'shared'>,
833
838
  worldId: World | number = 0,
834
839
  ): { freed: number; errors: RhiError[] } {
835
- const id = this.worldKey(handleSlot(handle), worldId);
840
+ const id = this.worldKey(Number(handle), worldId);
836
841
  const entry = this.meshGpuHandles.get(id);
837
842
  if (entry === undefined) return { freed: 0, errors: [] };
838
843
 
@@ -1090,7 +1095,7 @@ export class GpuResidencyCache {
1090
1095
  handle: Handle<'MeshAsset', 'shared'>,
1091
1096
  worldId: World | number = 0,
1092
1097
  ): MeshGpuHandles | undefined {
1093
- return this.meshGpuHandles.get(this.worldKey(handleSlot(handle), worldId));
1098
+ return this.meshGpuHandles.get(this.worldKey(Number(handle), worldId));
1094
1099
  }
1095
1100
 
1096
1101
  /** Candidate leases retain the concrete allocation even after cache invalidation. */
@@ -1098,7 +1103,7 @@ export class GpuResidencyCache {
1098
1103
  handle: Handle<'MeshAsset', 'shared'>,
1099
1104
  worldId: World | number = 0,
1100
1105
  ): MeshResidencyLease | undefined {
1101
- const key = this.worldKey(handleSlot(handle), worldId);
1106
+ const key = this.worldKey(Number(handle), worldId);
1102
1107
  const entry = this.meshGpuHandles.get(key);
1103
1108
  return entry === undefined
1104
1109
  ? undefined
@@ -1110,7 +1115,7 @@ export class GpuResidencyCache {
1110
1115
 
1111
1116
  /** In-place author changes immediately miss the cache; old submissions keep their allocation. */
1112
1117
  invalidateMesh(handle: number, worldId: World | number = 0): void {
1113
- const key = this.worldKey(handleSlot(toShared<'MeshAsset'>(handle)), worldId);
1118
+ const key = this.worldKey(handle, worldId);
1114
1119
  const entry = this.meshGpuHandles.get(key);
1115
1120
  if (entry !== undefined) this.retireMeshEntry(key, entry);
1116
1121
  }
@@ -1156,9 +1161,32 @@ export class GpuResidencyCache {
1156
1161
  // path, not routed here).
1157
1162
  switch (pod.kind) {
1158
1163
  case 'mesh': {
1159
- const key = this.worldKey(id, worldId);
1164
+ // Recycled shared slots can coexist with an older GPU submission.
1165
+ // The generation is part of mesh residency identity, not only its slot.
1166
+ const key = this.worldKey(Number(handle), worldId);
1160
1167
  const existing = this.meshGpuHandles.get(key);
1161
- if (existing !== undefined) return ok(existing);
1168
+ if (existing !== undefined) {
1169
+ // A root mesh can become resident before its lower-detail assets are
1170
+ // available. Preserve the fast hit for the common case, but publish
1171
+ // a complete chain once the caller supplies one. Never downgrade a
1172
+ // resident chain when a later call has no lower assets yet.
1173
+ if (lodMeshes.length === 0) return ok(existing);
1174
+ const previousLodAssets = this.meshLodAssets.get(existing);
1175
+ const sameLodChain =
1176
+ previousLodAssets !== undefined &&
1177
+ previousLodAssets.length === lodMeshes.length &&
1178
+ previousLodAssets.every((asset, index) => asset === lodMeshes[index]);
1179
+ if (sameLodChain) return ok(existing);
1180
+
1181
+ const projected = deriveRenderDataMesh(pod);
1182
+ if (!projected.ok) return projected;
1183
+ const upgraded = this.uploadMeshById(key, pod, projected.value, lodMeshes);
1184
+ if (!upgraded.ok) return upgraded;
1185
+ // uploadMeshById publishes the new entry before this call so the
1186
+ // old allocation stays available to any already-recorded frame.
1187
+ this.retireMeshEntry(key, existing);
1188
+ return upgraded;
1189
+ }
1162
1190
  const projected = deriveRenderDataMesh(pod);
1163
1191
  if (!projected.ok) return projected;
1164
1192
  return this.uploadMeshById(key, pod, projected.value, lodMeshes);
@@ -1204,7 +1232,7 @@ export class GpuResidencyCache {
1204
1232
  ): Result<RecoveryResidencyPreparation, RhiError | AssetError | ImageError> {
1205
1233
  const id = handleSlot(handle);
1206
1234
  if (pod.kind === 'mesh') {
1207
- const key = this.worldKey(id, worldId);
1235
+ const key = this.worldKey(Number(handle), worldId);
1208
1236
  if (this.meshGpuHandles.has(key)) return ok({});
1209
1237
  const projected = deriveRenderDataMesh(pod);
1210
1238
  if (!projected.ok) return projected;
@@ -2035,6 +2063,10 @@ export class GpuResidencyCache {
2035
2063
  // Vertex-only mesh: skip the index buffer entirely. The indexed path below
2036
2064
  // is unchanged byte-for-byte when `indices` is present.
2037
2065
  let ibo: Buffer | null = null;
2066
+ const cleanupUpload = (): void => {
2067
+ if (ibo !== null) device.destroyBuffer(ibo);
2068
+ device.destroyBuffer(vbo);
2069
+ };
2038
2070
  if (indices !== undefined) {
2039
2071
  const iboResult = device.createBuffer({
2040
2072
  label: `mesh-${id}-ibo`,
@@ -2042,17 +2074,24 @@ export class GpuResidencyCache {
2042
2074
  usage: renderData.indexUsage,
2043
2075
  mappedAtCreation: false,
2044
2076
  });
2045
- if (!iboResult.ok) return iboResult;
2077
+ if (!iboResult.ok) {
2078
+ cleanupUpload();
2079
+ return iboResult;
2080
+ }
2046
2081
  ibo = iboResult.value;
2047
2082
  }
2048
2083
 
2049
2084
  const vboWriteResult = device.queue.writeBuffer(vbo, 0, vertices);
2050
- if (!vboWriteResult.ok) return err(vboWriteResult.error);
2085
+ if (!vboWriteResult.ok) {
2086
+ cleanupUpload();
2087
+ return err(vboWriteResult.error);
2088
+ }
2051
2089
 
2052
2090
  if (indices !== undefined) {
2053
2091
  const indexSrc = new Uint8Array(indexBytes);
2054
2092
  indexSrc.set(new Uint8Array(indices.buffer, indices.byteOffset, indexBytesUnpadded));
2055
- if (ibo === null)
2093
+ if (ibo === null) {
2094
+ cleanupUpload();
2056
2095
  return err(
2057
2096
  new RhiError({
2058
2097
  code: 'webgpu-runtime-error',
@@ -2060,8 +2099,12 @@ export class GpuResidencyCache {
2060
2099
  hint: 'the indexed mesh upload lost its RHI index-buffer owner',
2061
2100
  }),
2062
2101
  );
2102
+ }
2063
2103
  const iboWriteResult = device.queue.writeBuffer(ibo, 0, indexSrc);
2064
- if (!iboWriteResult.ok) return err(iboWriteResult.error);
2104
+ if (!iboWriteResult.ok) {
2105
+ cleanupUpload();
2106
+ return err(iboWriteResult.error);
2107
+ }
2065
2108
  }
2066
2109
 
2067
2110
  // The geometry projection already contains every packed attribute,
@@ -2085,6 +2128,7 @@ export class GpuResidencyCache {
2085
2128
  ...(lodRanges === undefined ? {} : { lodRanges }),
2086
2129
  };
2087
2130
  this.meshGpuHandles.set(id, entry);
2131
+ this.meshLodAssets.set(entry, lodMeshes);
2088
2132
  this.meshResidencyEpoch += 1;
2089
2133
  return ok(entry);
2090
2134
  }
@@ -0,0 +1,44 @@
1
+ import { mat4 } from '@forgeax/engine-math';
2
+ import type { FogFrame } from '../extract/environment';
3
+ import { computeProjectionMatrix, computeViewMatrix } from '../record/helpers';
4
+ import type { CameraSnapshot } from '../render-contract';
5
+
6
+ /** Numerical reference for the shader's closed-form height-density integral. */
7
+ export function analyticFogOpacity(
8
+ fog: Pick<FogFrame, 'density' | 'heightFalloff' | 'maxOpacity'>,
9
+ originY: number,
10
+ endY: number,
11
+ distance: number,
12
+ ): number {
13
+ const rho = fog.density * Math.exp(Math.max(-60, Math.min(60, -fog.heightFalloff * originY)));
14
+ const t = Math.max(-60, Math.min(60, fog.heightFalloff * (endY - originY)));
15
+ const integral = Math.abs(t) < 0.001 ? 1 - t / 2 + (t * t) / 6 : -Math.expm1(-t) / t;
16
+ return fog.maxOpacity * -Math.expm1(-Math.min(80, Math.max(0, rho * distance * integral)));
17
+ }
18
+
19
+ export function encodeAnalyticFogParams(fog: FogFrame, camera: CameraSnapshot): Uint8Array {
20
+ const projection = computeProjectionMatrix(camera);
21
+ // Match the scene depth's jittered projection before temporal resolve.
22
+ const jitter = camera.temporal?.currentJitterUv;
23
+ if (jitter !== undefined) {
24
+ for (let column = 0; column < 4; column++) {
25
+ projection[column * 4] =
26
+ (projection[column * 4] ?? 0) + jitter[0] * 2 * (projection[column * 4 + 3] ?? 0);
27
+ projection[column * 4 + 1] =
28
+ (projection[column * 4 + 1] ?? 0) - jitter[1] * 2 * (projection[column * 4 + 3] ?? 0);
29
+ }
30
+ }
31
+ const inverse = mat4.invert(
32
+ mat4.create(),
33
+ mat4.multiply(mat4.create(), projection, computeViewMatrix(camera)),
34
+ );
35
+ const data = new Float32Array(28);
36
+ data.set(inverse, 0);
37
+ data.set(camera.position, 16);
38
+ data.set([...fog.color, fog.density], 20);
39
+ data.set(
40
+ [fog.heightFalloff, fog.maxOpacity, camera.near, camera.projection === 'orthographic' ? 1 : 0],
41
+ 24,
42
+ );
43
+ return new Uint8Array(data.buffer);
44
+ }
@@ -88,8 +88,8 @@ export function addAtmosphereBackground(
88
88
  settings.mieDirectionalG,
89
89
  settings.sunAngularRadius,
90
90
  settings.sunAngularRadius > 0 ? 1 : 0,
91
- 0,
92
- 0,
91
+ settings.circumsolarStrength,
92
+ settings.circumsolarWidth,
93
93
  ]);
94
94
  frame.runtime.device.queue
95
95
  .writeBuffer(resources.buffer(params.value).unwrap(), 0, values)
@@ -19,6 +19,8 @@ import type {
19
19
  SurfaceFailureKind,
20
20
  } from '@forgeax/engine-render-graph';
21
21
  import type { DeviceResourceKind, DeviceScopeReceipt } from '../device/resource-types';
22
+ import type { SunCardinalityError } from '../environment/frame';
23
+ import type { MotionBlurValidationError } from '../features/motion-blur/motion-blur-params';
22
24
  import type {
23
25
  PreparedKind,
24
26
  RenderFeatureCapabilityKey,
@@ -783,6 +785,8 @@ export interface RenderFeatureStageFailedDetail {
783
785
  readonly stage: RenderFeatureStage;
784
786
  readonly recovery: RenderFeatureRecovery;
785
787
  readonly cleanupFailures?: readonly RenderFeatureCleanupFailure[];
788
+ /** Original stage exception when the feature did not return a RenderError. */
789
+ readonly cause?: unknown;
786
790
  }
787
791
 
788
792
  export interface RenderFeatureCapabilityMissingDetail {
@@ -947,6 +951,7 @@ export class RenderFeatureStageFailedError extends Error {
947
951
  order: number,
948
952
  stage: RenderFeatureStage,
949
953
  recovery: RenderFeatureRecovery,
954
+ cause?: unknown,
950
955
  ) {
951
956
  const expected = `feature '${featureIdentity}' completes its ${stage} stage without an error`;
952
957
  const hint = renderFeatureRecoveryHintByRecovery[recovery](featureIdentity, stage);
@@ -954,7 +959,13 @@ export class RenderFeatureStageFailedError extends Error {
954
959
  this.name = 'RenderFeatureStageFailedError';
955
960
  this.expected = expected;
956
961
  this.hint = hint;
957
- this.detail = { featureIdentity, order, stage, recovery };
962
+ this.detail = {
963
+ featureIdentity,
964
+ order,
965
+ stage,
966
+ recovery,
967
+ ...(cause === undefined ? {} : { cause }),
968
+ };
958
969
  }
959
970
  }
960
971
 
@@ -1966,12 +1977,14 @@ export type RenderError =
1966
1977
  | RenderFeatureDrawRecordingFailedError
1967
1978
  | EnvironmentSourceConflictError
1968
1979
  | FogCardinalityError
1980
+ | SunCardinalityError
1969
1981
  | TaaCapsInsufficientError
1970
1982
  | EnvironmentGenerationFailedError
1971
1983
  | AtmosphereInvalidParameterError
1972
1984
  | DynamicResolutionInvalidParameterError
1973
1985
  | DynamicResolutionRequiresTaaError
1974
1986
  | DynamicResolutionTimingUnavailableError
1987
+ | MotionBlurValidationError
1975
1988
  | OwnerStageFailedError
1976
1989
  | PointsLinesInvalidStyleError
1977
1990
  | PointsLinesTopologyMismatchError