@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
@@ -5,11 +5,12 @@ import type { RhiDevice } from '@forgeax/engine-rhi';
5
5
  import { rhi } from '@forgeax/engine-rhi-null';
6
6
  import { ok } from '@forgeax/engine-types';
7
7
  import { beforeAll, describe, expect, it } from 'vitest';
8
+ import { standardBloomAdmitted } from '../bloom-admission';
8
9
  import { prepareStandardLighting } from '../pipeline/standard-lighting/prepare';
9
10
  import { deriveStandardTopologyInput } from '../pipeline/standard-lighting/topology';
10
11
  import { selectStandardClusterTransport } from '../pipeline/standard-lighting/transport';
11
12
  import { standardPipeline } from '../pipeline/standard-pipeline';
12
- import { inspectStandardBloomGraph, STANDARD_BLOOM_TARGET_SPEC } from '../pipeline/standard-post';
13
+ import { inspectStandardBloomGraph } from '../pipeline/standard-post';
13
14
  import { DEFAULT_STANDARD_PROFILE, STANDARD_PIPELINE_ID } from '../pipeline/standard-profile';
14
15
  import type { RenderPipelineFrame, RenderPipelineTopology } from '../render-pipeline';
15
16
 
@@ -23,7 +24,7 @@ beforeAll(async () => {
23
24
  device = result.value;
24
25
  });
25
26
 
26
- function topology(bloom: 'off' | 'on'): RenderPipelineTopology {
27
+ function topology(bloom: 'off' | 'on', bloomIntensity = 1): RenderPipelineTopology {
27
28
  return {
28
29
  pipelineId: STANDARD_PIPELINE_ID,
29
30
  standardProfile: DEFAULT_STANDARD_PROFILE,
@@ -34,7 +35,12 @@ function topology(bloom: 'off' | 'on'): RenderPipelineTopology {
34
35
  storageFormat: 'bgra8unorm',
35
36
  viewFormat: 'bgra8unorm-srgb',
36
37
  },
37
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom },
38
+ camera: {
39
+ tonemap: 'aces-filmic',
40
+ antialias: 'fxaa',
41
+ bloom,
42
+ bloomIntensity,
43
+ },
38
44
  shadow: {
39
45
  directional: { mapSize: 64, cascadeCount: 1 },
40
46
  spotMapSize: 64,
@@ -80,7 +86,7 @@ function clusteredStandardLighting() {
80
86
  return lighting.value;
81
87
  }
82
88
 
83
- function inspect(bloom: 'off' | 'on') {
89
+ function inspect(bloom: 'off' | 'on', bloomIntensity = 1) {
84
90
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
85
91
  const built = standardPipeline.build(
86
92
  {
@@ -89,7 +95,7 @@ function inspect(bloom: 'off' | 'on') {
89
95
  projectGpuDriven: () => ok(undefined),
90
96
  contributeFeatures: () => ok(undefined),
91
97
  },
92
- topology(bloom),
98
+ topology(bloom, bloomIntensity),
93
99
  );
94
100
  if (!built.ok) throw built.error;
95
101
  const compiled = graph.compile({ device, surfaceSize: { width: 640, height: 360 } });
@@ -98,6 +104,15 @@ function inspect(bloom: 'off' | 'on') {
98
104
  }
99
105
 
100
106
  describe('Standard Bloom exact-zero topology', () => {
107
+ it.each([
108
+ ['off', 'aces-filmic', 1, false],
109
+ ['on', 'aces-filmic', 0, false],
110
+ ['on', 'none', 1, false],
111
+ ['on', 'aces-filmic', 1, true],
112
+ ] as const)('uses one effective admission for %s/%s/%s', (bloom, tonemap, intensity, expected) => {
113
+ expect(standardBloomAdmitted({ bloom, tonemap, bloomIntensity: intensity })).toBe(expected);
114
+ });
115
+
101
116
  it('Bloom off declares no Bloom targets or passes', () => {
102
117
  const info = inspect('off');
103
118
  const bloomPasses = info.passes.filter((pass) => pass.name.startsWith('bloom-'));
@@ -109,6 +124,10 @@ describe('Standard Bloom exact-zero topology', () => {
109
124
  targetCount: 0,
110
125
  targetBytes: 0,
111
126
  passCount: 0,
127
+ levelCount: 0,
128
+ levelDimensions: [],
129
+ downsamplePassCount: 0,
130
+ upsamplePassCount: 0,
112
131
  });
113
132
  expect({
114
133
  targetCount: bloomResources.length,
@@ -127,6 +146,15 @@ describe('Standard Bloom exact-zero topology', () => {
127
146
  });
128
147
  });
129
148
 
149
+ it('treats intensity=0 as exact-zero even when the Camera enum is on', () => {
150
+ const info = inspect('on', 0);
151
+ expect(info.passes.filter((pass) => pass.name.startsWith('bloom-'))).toHaveLength(0);
152
+ expect(info.resources.filter((resource) => resource.label.startsWith('bloom-'))).toHaveLength(
153
+ 0,
154
+ );
155
+ expect(inspectStandardBloomGraph(info).status).toBe('empty');
156
+ });
157
+
130
158
  it('keeps Bloom declaration behind the camera gate in both lane owners', () => {
131
159
  const forwardSource = readFileSync(
132
160
  new URL('../pipeline/standard-forward-lane.ts', import.meta.url),
@@ -142,8 +170,8 @@ describe('Standard Bloom exact-zero topology', () => {
142
170
  );
143
171
  expect(forwardSource).toContain('addStandardPost');
144
172
  expect(clusteredSource).toContain('addStandardPost');
145
- const gate = postSource.indexOf("topology.camera.bloom === 'on'");
146
- const declaration = postSource.indexOf('STANDARD_BLOOM_TARGET_SPEC.composited');
173
+ const gate = postSource.indexOf('bloom: !clearOnly && standardBloomAdmitted(topology.camera)');
174
+ const declaration = postSource.indexOf('const bloomSpecs = standardBloomTargetSpecs(');
147
175
  expect(gate).toBeGreaterThanOrEqual(0);
148
176
  expect(declaration).toBeGreaterThan(gate);
149
177
  });
@@ -153,15 +181,27 @@ describe('Standard Bloom exact-zero topology', () => {
153
181
  const info = inspect('on');
154
182
  expect(inspectStandardBloomGraph(info)).toEqual({
155
183
  status: 'valid',
156
- targetCount: 4,
157
- targetBytes: (640 * 360 + 3 * 320 * 180) * 8,
158
- passCount: 4,
184
+ targetCount: 10,
185
+ targetBytes: 3_069_440,
186
+ passCount: 10,
187
+ levelCount: 5,
188
+ levelDimensions: [
189
+ { width: 320, height: 180 },
190
+ { width: 160, height: 90 },
191
+ { width: 80, height: 45 },
192
+ { width: 40, height: 23 },
193
+ { width: 20, height: 12 },
194
+ ],
195
+ downsamplePassCount: 5,
196
+ upsamplePassCount: 4,
159
197
  });
160
- expect(info.resources.find((resource) => resource.label === 'bloom-bright')).toMatchObject({
198
+ expect(
199
+ info.resources.find((resource) => resource.label === 'bloom-downsample-0'),
200
+ ).toMatchObject({
161
201
  descriptor: {
162
202
  kind: 'texture',
163
203
  format: 'rgba16float',
164
- size: STANDARD_BLOOM_TARGET_SPEC.bright.size,
204
+ size: { width: 320, height: 180 },
165
205
  width: 320,
166
206
  height: 180,
167
207
  depthOrArrayLayers: 1,
@@ -171,7 +211,18 @@ describe('Standard Bloom exact-zero topology', () => {
171
211
  });
172
212
  expect(
173
213
  info.passes.map((pass) => pass.name).filter((name) => name.startsWith('bloom-')),
174
- ).toEqual(['bloom-bright', 'bloom-blur-h', 'bloom-blur-v', 'bloom-composite']);
214
+ ).toEqual([
215
+ 'bloom-downsample-0',
216
+ 'bloom-downsample-1',
217
+ 'bloom-downsample-2',
218
+ 'bloom-downsample-3',
219
+ 'bloom-downsample-4',
220
+ 'bloom-upsample-3',
221
+ 'bloom-upsample-2',
222
+ 'bloom-upsample-1',
223
+ 'bloom-upsample-0',
224
+ 'bloom-composite',
225
+ ]);
175
226
  }
176
227
  });
177
228
 
@@ -180,7 +231,7 @@ describe('Standard Bloom exact-zero topology', () => {
180
231
  const drifted = {
181
232
  ...info,
182
233
  resources: info.resources.map((resource) =>
183
- resource.label === STANDARD_BLOOM_TARGET_SPEC.bright.label
234
+ resource.label === 'bloom-downsample-0'
184
235
  ? {
185
236
  ...resource,
186
237
  descriptor:
@@ -196,6 +247,10 @@ describe('Standard Bloom exact-zero topology', () => {
196
247
  targetCount: 0,
197
248
  targetBytes: 0,
198
249
  passCount: 0,
250
+ levelCount: 0,
251
+ levelDimensions: [],
252
+ downsamplePassCount: 0,
253
+ upsamplePassCount: 0,
199
254
  });
200
255
  });
201
256
 
@@ -203,15 +258,17 @@ describe('Standard Bloom exact-zero topology', () => {
203
258
  const info = inspect('on');
204
259
  const drifted = {
205
260
  ...info,
206
- resources: info.resources.filter(
207
- (resource) => resource.label !== STANDARD_BLOOM_TARGET_SPEC.bright.label,
208
- ),
261
+ resources: info.resources.filter((resource) => resource.label !== 'bloom-downsample-0'),
209
262
  };
210
263
  expect(inspectStandardBloomGraph(drifted)).toEqual({
211
264
  status: 'invalid',
212
265
  targetCount: 0,
213
266
  targetBytes: 0,
214
267
  passCount: 0,
268
+ levelCount: 0,
269
+ levelDimensions: [],
270
+ downsamplePassCount: 0,
271
+ upsamplePassCount: 0,
215
272
  });
216
273
  });
217
274
 
@@ -219,13 +276,50 @@ describe('Standard Bloom exact-zero topology', () => {
219
276
  const info = inspect('on');
220
277
  const drifted = {
221
278
  ...info,
222
- passes: info.passes.filter((pass) => pass.name !== 'bloom-blur-h'),
279
+ passes: info.passes.filter((pass) => pass.name !== 'bloom-downsample-1'),
223
280
  };
224
281
  expect(inspectStandardBloomGraph(drifted)).toEqual({
225
282
  status: 'invalid',
226
283
  targetCount: 0,
227
284
  targetBytes: 0,
228
285
  passCount: 0,
286
+ levelCount: 0,
287
+ levelDimensions: [],
288
+ downsamplePassCount: 0,
289
+ upsamplePassCount: 0,
290
+ });
291
+ });
292
+
293
+ it('rejects forged odd-scale extents instead of reporting inferred sizes or cost', () => {
294
+ const info = inspect('on');
295
+ const forgedLabels = new Set(['bloom-downsample-0', 'bloom-downsample-1', 'bloom-upsample-0']);
296
+ const forged = {
297
+ ...info,
298
+ resources: info.resources.map((resource) =>
299
+ forgedLabels.has(resource.label) && resource.descriptor.kind === 'texture'
300
+ ? {
301
+ ...resource,
302
+ descriptor: {
303
+ ...resource.descriptor,
304
+ size: { width: 4, height: 4 },
305
+ width: 4,
306
+ height: 4,
307
+ },
308
+ extent: { width: 4, height: 4, depthOrArrayLayers: 1 },
309
+ byteSize: 128,
310
+ }
311
+ : resource,
312
+ ),
313
+ };
314
+ expect(inspectStandardBloomGraph(forged)).toEqual({
315
+ status: 'invalid',
316
+ targetCount: 0,
317
+ targetBytes: 0,
318
+ passCount: 0,
319
+ levelCount: 0,
320
+ levelDimensions: [],
321
+ downsamplePassCount: 0,
322
+ upsamplePassCount: 0,
229
323
  });
230
324
  });
231
325
  });
@@ -33,7 +33,12 @@ function build(bloom: 'off' | 'on') {
33
33
  storageFormat: 'bgra8unorm',
34
34
  viewFormat: 'bgra8unorm-srgb',
35
35
  },
36
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom },
36
+ camera: {
37
+ tonemap: 'aces-filmic',
38
+ antialias: 'fxaa',
39
+ bloom,
40
+ bloomIntensity: 1,
41
+ },
37
42
  shadow: {
38
43
  directional: { mapSize: 64, cascadeCount: 1 },
39
44
  spotMapSize: 64,
@@ -103,7 +108,7 @@ describe('Standard Bloom lifecycle contract', () => {
103
108
  const on = bloomSummary(build('on'));
104
109
  const off = bloomSummary(build('off'));
105
110
  const recovered = bloomSummary(build('on'));
106
- expect(on.passes).toHaveLength(4);
111
+ expect(on.passes).toHaveLength(10);
107
112
  expect(on.resources.length).toBeGreaterThan(0);
108
113
  expect(off).toEqual({ passes: [], resources: [] });
109
114
  expect(recovered).toEqual(on);
@@ -113,7 +118,15 @@ describe('Standard Bloom lifecycle contract', () => {
113
118
  const names = build('on').passes.map((pass) => pass.name);
114
119
  const order = [
115
120
  'forward',
116
- 'bloom-bright',
121
+ 'bloom-downsample-0',
122
+ 'bloom-downsample-1',
123
+ 'bloom-downsample-2',
124
+ 'bloom-downsample-3',
125
+ 'bloom-downsample-4',
126
+ 'bloom-upsample-3',
127
+ 'bloom-upsample-2',
128
+ 'bloom-upsample-1',
129
+ 'bloom-upsample-0',
117
130
  'bloom-composite',
118
131
  'output-transform',
119
132
  'fxaa',
@@ -132,7 +145,7 @@ describe('Standard Bloom lifecycle contract', () => {
132
145
  const transaction = new LifecycleTransaction(bloom);
133
146
  transaction.add({
134
147
  kind: 'texture',
135
- create: () => ({ label: 'bloom-bright' }),
148
+ create: () => ({ label: 'bloom-downsample-0' }),
136
149
  cleanup: () => undefined,
137
150
  });
138
151
  const committed = await transaction.commit();
@@ -69,13 +69,33 @@ describe('DynamicResolution render component registration', () => {
69
69
  shutterAngle: 120,
70
70
  maxRadiusPixels: 12,
71
71
  sampleCount: 6,
72
+ targetFps: 60,
72
73
  });
73
74
  expect(extractFrames([world], 0).cameras[0]?.motionBlur).toEqual({
74
75
  shutterAngle: 120,
75
76
  maxRadiusPixels: 12,
76
77
  sampleCount: 6,
78
+ targetFps: 60,
77
79
  });
78
80
 
79
81
  await context.fiber.dispose();
80
82
  });
83
+
84
+ it('routes invalid MotionBlur data through the structured extraction error', async () => {
85
+ const world = new World();
86
+ const context = await createWorldContext(world, [renderComponentsPlugin(), scenePlugin()]);
87
+ world
88
+ .spawn(
89
+ { component: Transform, data: {} },
90
+ { component: Camera, data: { antialias: ANTIALIAS_TAA } },
91
+ { component: MotionBlur, data: { targetFps: 59.5 } },
92
+ )
93
+ .unwrap();
94
+ world.update(1 / 60).unwrap();
95
+
96
+ expect(() => extractCameraSnapshots(world)).toThrowError(
97
+ expect.objectContaining({ code: 'motion-blur-invalid-params' }),
98
+ );
99
+ await context.fiber.dispose();
100
+ });
81
101
  });
@@ -20,6 +20,8 @@ const frame = selectEnvironment({
20
20
  mieCoefficient: 0.005,
21
21
  mieDirectionalG: 0.8,
22
22
  sunAngularRadius: 0.004675,
23
+ circumsolarStrength: 1,
24
+ circumsolarWidth: 1,
23
25
  },
24
26
  },
25
27
  ],
@@ -48,6 +50,8 @@ describe('Environment and Fog lane graph contract', () => {
48
50
  mieCoefficient: 0.006,
49
51
  mieDirectionalG: 0.7,
50
52
  sunAngularRadius: 0.01,
53
+ circumsolarStrength: 1.4,
54
+ circumsolarWidth: 1.6,
51
55
  },
52
56
  })
53
57
  .unwrap();
@@ -70,6 +74,8 @@ describe('Environment and Fog lane graph contract', () => {
70
74
  expect(extracted.environment.source.atmosphere?.mieCoefficient).toBeCloseTo(0.006);
71
75
  expect(extracted.environment.source.atmosphere?.mieDirectionalG).toBeCloseTo(0.7);
72
76
  expect(extracted.environment.source.atmosphere?.sunAngularRadius).toBeCloseTo(0.01);
77
+ expect(extracted.environment.source.atmosphere?.circumsolarStrength).toBeCloseTo(1.4);
78
+ expect(extracted.environment.source.atmosphere?.circumsolarWidth).toBeCloseTo(1.6);
73
79
  const signature = JSON.parse(extracted.environment.signature) as {
74
80
  environments: [{ atmosphere: Record<string, number> }];
75
81
  suns: [{ direction: [number, number, number] }];
@@ -79,6 +85,8 @@ describe('Environment and Fog lane graph contract', () => {
79
85
  expect(signature.environments[0].atmosphere.mieCoefficient).toBeCloseTo(0.006);
80
86
  expect(signature.environments[0].atmosphere.mieDirectionalG).toBeCloseTo(0.7);
81
87
  expect(signature.environments[0].atmosphere.sunAngularRadius).toBeCloseTo(0.01);
88
+ expect(signature.environments[0].atmosphere.circumsolarStrength).toBeCloseTo(1.4);
89
+ expect(signature.environments[0].atmosphere.circumsolarWidth).toBeCloseTo(1.6);
82
90
  expect(signature.suns[0].direction).toEqual([0, 1, 0]);
83
91
  });
84
92
 
@@ -162,6 +170,8 @@ describe('Environment and Fog lane graph contract', () => {
162
170
  mieCoefficient: 0.005,
163
171
  mieDirectionalG: 0.8,
164
172
  sunAngularRadius: 0.004675,
173
+ circumsolarStrength: 1,
174
+ circumsolarWidth: 1,
165
175
  },
166
176
  })
167
177
  .unwrap();
@@ -190,6 +200,8 @@ describe('Environment and Fog lane graph contract', () => {
190
200
  mieCoefficient: 0.005,
191
201
  mieDirectionalG: 0.8,
192
202
  sunAngularRadius: 0.004675,
203
+ circumsolarStrength: 1,
204
+ circumsolarWidth: 1,
193
205
  })
194
206
  .unwrap();
195
207
  const invalid = extractFrames([world], 0);
@@ -203,6 +215,8 @@ describe('Environment and Fog lane graph contract', () => {
203
215
  mieCoefficient: 0.006,
204
216
  mieDirectionalG: 0.7,
205
217
  sunAngularRadius: 0.01,
218
+ circumsolarStrength: 1,
219
+ circumsolarWidth: 1,
206
220
  })
207
221
  .unwrap();
208
222
  const corrected = extractFrames([world], 0);
@@ -7,6 +7,7 @@ import type {
7
7
  EnvironmentCandidate,
8
8
  FogCandidate,
9
9
  } from '../extract/environment';
10
+ import { SunCardinalityError } from '../index';
10
11
 
11
12
  const atmosphereParameters: AtmosphereParameters = {
12
13
  turbidity: 2,
@@ -14,6 +15,8 @@ const atmosphereParameters: AtmosphereParameters = {
14
15
  mieCoefficient: 0.005,
15
16
  mieDirectionalG: 0.8,
16
17
  sunAngularRadius: 0.004675,
18
+ circumsolarStrength: 1,
19
+ circumsolarWidth: 1,
17
20
  };
18
21
 
19
22
  const image = (entityKey: number, sourceKey: string): EnvironmentCandidate => ({
@@ -97,6 +100,7 @@ describe('Environment and Fog selection (M2)', () => {
97
100
  expect(result.error.expected).toContain('sun');
98
101
  expect(result.error.hint).toContain('sun');
99
102
  expect(result.error.detail).toMatchObject({ field, value });
103
+ expect(result.error).toBeInstanceOf(SunCardinalityError);
100
104
  });
101
105
 
102
106
  it('rejects invalid Fog parameters at the field owner', () => {
@@ -114,6 +118,8 @@ describe('Environment and Fog selection (M2)', () => {
114
118
  ['mieCoefficient', { mieCoefficient: -0.001 }],
115
119
  ['mieDirectionalG', { mieDirectionalG: 1.1 }],
116
120
  ['sunAngularRadius', { sunAngularRadius: Number.POSITIVE_INFINITY }],
121
+ ['circumsolarStrength', { circumsolarStrength: -0.001 }],
122
+ ['circumsolarWidth', { circumsolarWidth: 0.1 }],
117
123
  ] as const)('rejects invalid Atmosphere.%s with field-owned detail', (field, override) => {
118
124
  const result = selection(
119
125
  [
@@ -141,6 +147,8 @@ describe('Environment and Fog selection (M2)', () => {
141
147
  ['mieCoefficient', 0, 100],
142
148
  ['mieDirectionalG', 0, 0.999],
143
149
  ['sunAngularRadius', 0, 1],
150
+ ['circumsolarStrength', 0, 4],
151
+ ['circumsolarWidth', 0.25, 4],
144
152
  ] as const)('accepts the documented %s boundaries', (field, minimum, maximum) => {
145
153
  for (const value of [minimum, maximum]) {
146
154
  const result = selection(
@@ -165,6 +173,8 @@ describe('Environment and Fog selection (M2)', () => {
165
173
  ['mieCoefficient', [-Number.MIN_VALUE, Number.POSITIVE_INFINITY]],
166
174
  ['mieDirectionalG', [-Number.MIN_VALUE, 1]],
167
175
  ['sunAngularRadius', [-Number.MIN_VALUE, Number.POSITIVE_INFINITY]],
176
+ ['circumsolarStrength', [-Number.MIN_VALUE, 4.001]],
177
+ ['circumsolarWidth', [0.249, 4.001]],
168
178
  ] as const)('rejects %s values outside the shared shader domain', (field, values) => {
169
179
  for (const value of values) {
170
180
  const result = selection(
@@ -207,6 +217,8 @@ describe('Environment and Fog selection (M2)', () => {
207
217
  expect(baseline.value.source).toMatchObject({ atmosphere: atmosphereParameters });
208
218
  expect(baseline.value.signature).not.toBe(changed.value.signature);
209
219
  expect(baseline.value.signature).toContain('sunAngularRadius');
220
+ expect(baseline.value.signature).toContain('circumsolarStrength');
221
+ expect(baseline.value.signature).toContain('circumsolarWidth');
210
222
  });
211
223
 
212
224
  it('keeps Fog orthogonal and produces identical direct/clustered frame facts', () => {