@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
@@ -53,7 +53,12 @@ import {
53
53
  RenderFeatureCapabilityMissingError,
54
54
  RenderFeatureStageFailedError,
55
55
  } from '../errors/render';
56
+ import {
57
+ dofInspection,
58
+ registerDepthOfFieldBuiltins,
59
+ } from '../features/depth-of-field/depth-of-field-assembly';
56
60
  import { createRenderFeatureHost } from '../features/host';
61
+ import { motionBlurDrawOptions } from '../features/motion-blur/motion-blur-runtime';
57
62
  import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
58
63
  import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
59
64
  import {
@@ -62,7 +67,7 @@ import {
62
67
  } from '../fullscreen-post-process-pass';
63
68
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
64
69
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
65
- import type { BloomInspection } from '../inspection-types';
70
+ import { type BloomInspection, emptyBloomInspection } from '../inspection-types';
66
71
  import { DeviceScope } from '../lifecycle';
67
72
  import {
68
73
  createHdrpBindGroupLayoutDescriptor,
@@ -127,7 +132,7 @@ import type {
127
132
  SsrShaderSources,
128
133
  VolumetricFogShaderSources,
129
134
  } from '../render-contract';
130
- import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
135
+ import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
131
136
  import {
132
137
  attachGpuPassTimingSession,
133
138
  createRenderSystem,
@@ -142,10 +147,13 @@ import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-jo
142
147
  import type { RenderTarget } from '../targets/contracts';
143
148
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
144
149
  import type { TypedFrameObservationCapture } from '../typed-render-graph-primitives';
150
+ import { registerAnalyticFogPostProcess } from './analytic-fog-registration';
145
151
  import type { RhiBackendPack } from './backend-contract';
146
152
  import type { BundlerOptions } from './bundler-contract';
147
153
  import { createRendererDynamicGeometryController } from './dynamic-geometry-host';
154
+ import { registerFxaaPostProcess } from './fxaa-registration';
148
155
  import type { RendererAssemblyImplementation } from './host-contract';
156
+ import { observeLodOcclusionForReceipt } from './lod-observation';
149
157
  import {
150
158
  isEngineOwnedMaterialShader,
151
159
  resolveMaterialPipelineBindGroups,
@@ -173,6 +181,7 @@ import {
173
181
  shouldDeferMissingPreparedMaterialShader,
174
182
  } from './material-shader-policy';
175
183
  import type { MeshSsboGrowResult, MeshSsboState } from './mesh-ssbo-grow';
184
+ import { registerMotionBlurPostProcess } from './motion-blur-registration';
176
185
  import {
177
186
  type GenerationAggregate,
178
187
  type GenerationPublication,
@@ -1248,13 +1257,6 @@ async function makeWebGPURenderer(
1248
1257
  }
1249
1258
  return undefined;
1250
1259
  };
1251
- // feat-20260622-s5 M3 / w17: the pipeline build is factored into a closure so
1252
- // the recover() rebuild can re-run the SAME three-step assembly against the
1253
- // freshly-acquired device (SSOT — one build path, one set of seed callbacks).
1254
- // All captured references (getShader / gpuStore / getShaderModuleAdapter /
1255
- // materialShaderPipelineCache / renderSystem) are stable across recover; only
1256
- // `internals.device` / `internals.pack` are read live, so a rebuild after a
1257
- // device swap compiles against the new device.
1258
1260
  const promotePipelineExtendedLighting = (state: PipelineState, scope: DeviceScope): void => {
1259
1261
  if (
1260
1262
  state.extendedLightingAvailable !== true ||
@@ -1287,9 +1289,10 @@ async function makeWebGPURenderer(
1287
1289
  };
1288
1290
  extendedLightingState = promoteExtendedLightingCandidate(extendedLightingState, candidate);
1289
1291
  };
1290
- let tonemapRegistered = false;
1291
- let fxaaRegistered = false;
1292
- let temporalPostProcessesRegistered = false;
1292
+ let tonemapRegistered = false,
1293
+ fxaaRegistered = false;
1294
+ let temporalPostProcessesRegistered = false,
1295
+ depthOfFieldRegistered = false;
1293
1296
  const buildPipeline = (
1294
1297
  scope: DeviceScope = activeDeviceScope,
1295
1298
  device: RhiDevice = currentBuildDevice(),
@@ -1311,7 +1314,6 @@ async function makeWebGPURenderer(
1311
1314
  ),
1312
1315
  materialShaderUvSetCounts.get('forgeax::default-standard-pbr') ?? 0,
1313
1316
  requiredFullscreenPostProcesses,
1314
- // Publish growth and capacity together for the active or candidate device.
1315
1317
  (hook, state) => {
1316
1318
  if (candidateBuildState !== undefined) {
1317
1319
  candidateBuildState.growMeshSsbo = hook;
@@ -1321,12 +1323,10 @@ async function makeWebGPURenderer(
1321
1323
  internals.meshSsboState = state;
1322
1324
  }
1323
1325
  },
1324
- // Both module adapters consume the generation's eager shader cache.
1325
1326
  (label, module) => {
1326
1327
  getShaderModuleAdapter().seedModule(label, module);
1327
1328
  getImmediateShaderModuleAdapter().seedModule(label, module);
1328
1329
  },
1329
- // Prewarm only the active build generation's material pipeline cache.
1330
1330
  (key, pso, group2Contract) => {
1331
1331
  const pipelineCache = currentPipelineCacheState().materialShaderPipelineCache;
1332
1332
  if (!pipelineCache.has(key)) {
@@ -1334,7 +1334,6 @@ async function makeWebGPURenderer(
1334
1334
  }
1335
1335
  materialShaderPipelineGroup2Contracts.set(pso as object, group2Contract);
1336
1336
  },
1337
- // Register the output transform after manifest loading, once RenderSystem exists.
1338
1337
  (source: string) => {
1339
1338
  if (candidateBuildState !== undefined || tonemapRegistered) return;
1340
1339
  renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
@@ -1345,22 +1344,14 @@ async function makeWebGPURenderer(
1345
1344
  tonemapRegistered = true;
1346
1345
  },
1347
1346
  (source: string) => {
1348
- if (fxaaRegistered) return;
1349
- renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
1350
- source,
1351
- params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1352
- reads: ['ldrColor'],
1353
- });
1354
- fxaaRegistered = true;
1347
+ fxaaRegistered = registerFxaaPostProcess(renderSystem, source, fxaaRegistered);
1355
1348
  },
1349
+ (source: string) =>
1350
+ registerAnalyticFogPostProcess(renderSystem, source, candidateBuildState !== undefined),
1356
1351
  (entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
1357
1352
  if (candidateBuildState !== undefined || temporalPostProcessesRegistered) return;
1358
1353
  if (entries.motionBlur !== undefined) {
1359
- renderSystem.registerBuiltinPostProcess('forgeax.motion-blur', {
1360
- source: entries.motionBlur,
1361
- params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1362
- reads: ['scene-color', 'scene-temporal'],
1363
- });
1354
+ registerMotionBlurPostProcess(renderSystem, entries.motionBlur);
1364
1355
  }
1365
1356
  if (entries.taaResolve !== undefined) {
1366
1357
  renderSystem.registerBuiltinPostProcess('forgeax.taa-resolve', {
@@ -1371,6 +1362,11 @@ async function makeWebGPURenderer(
1371
1362
  }
1372
1363
  temporalPostProcessesRegistered = true;
1373
1364
  },
1365
+ (entries: { readonly singleSample?: string; readonly multisampled?: string }) => {
1366
+ if (candidateBuildState !== undefined || depthOfFieldRegistered) return;
1367
+ registerDepthOfFieldBuiltins(renderSystem, entries);
1368
+ depthOfFieldRegistered = true;
1369
+ },
1374
1370
  (sources: VolumetricFogShaderSources) => {
1375
1371
  internals.volumetricFogShaders = sources;
1376
1372
  },
@@ -2719,22 +2715,7 @@ async function makeWebGPURenderer(
2719
2715
  : 'alive';
2720
2716
  const graph = deviceLost ? undefined : renderSystem.perFrameGraphInfo;
2721
2717
  const standardLighting = deviceLost ? undefined : renderSystem.standardLightingInspection;
2722
- const bloom: BloomInspection = deviceLost
2723
- ? {
2724
- graphStatus: 'empty',
2725
- enabled: false,
2726
- targetCount: 0,
2727
- targetBytes: 0,
2728
- resourceCount: 0,
2729
- passCount: 0,
2730
- encodeCount: 0,
2731
- bindGroupCount: 0,
2732
- uploadCount: 0,
2733
- residentChildBytes: 0,
2734
- generation: 0,
2735
- state: 'off',
2736
- }
2737
- : renderSystem.bloom;
2718
+ const bloom: BloomInspection = deviceLost ? emptyBloomInspection() : renderSystem.bloom;
2738
2719
  const surfaceStorage = pipelineState?.format ?? 'rgba8unorm';
2739
2720
  const surfaceDisplay = pipelineState?.colorAttachmentFormat ?? 'rgba8unorm';
2740
2721
  const observationId = `${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}:frame-${frameId}`;
@@ -2810,6 +2791,7 @@ async function makeWebGPURenderer(
2810
2791
  ...(renderSystem.motionBlurInspection === undefined
2811
2792
  ? {}
2812
2793
  : { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
2794
+ ...dofInspection(renderSystem.depthOfFieldInspection),
2813
2795
  ...(renderSystem.lodOcclusionInspection === undefined
2814
2796
  ? {}
2815
2797
  : { lodOcclusion: Object.freeze(renderSystem.lodOcclusionInspection) }),
@@ -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 /
@@ -0,0 +1,45 @@
1
+ import { defineComponent, type SchemaOf, type ShapeOf } from '@forgeax/engine-ecs';
2
+
3
+ /** Closed quality budget used by the built-in camera depth-of-field pass. */
4
+ export type DepthOfFieldQuality = 'low' | 'medium' | 'high';
5
+
6
+ export const DepthOfFieldQualityValue = Object.freeze({
7
+ low: 0,
8
+ medium: 1,
9
+ high: 2,
10
+ } as const);
11
+
12
+ /** Selects which signed circle-of-confusion side contributes to the image. */
13
+ export type DepthOfFieldSide = 'both' | 'near' | 'far';
14
+
15
+ export const DepthOfFieldSideValue = Object.freeze({
16
+ both: 0,
17
+ near: 1,
18
+ far: 2,
19
+ } as const);
20
+
21
+ /** Active-camera physical depth-of-field authoring data; presence enables it. */
22
+ export const DepthOfField = defineComponent('DepthOfField', {
23
+ focusDistance: { type: 'f32', default: 8 },
24
+ fStop: { type: 'f32', default: 2.8 },
25
+ sensorHeight: { type: 'f32', default: 0.024 },
26
+ maxRadiusPixels: { type: 'f32', default: 16 },
27
+ quality: { type: 'f32', default: DepthOfFieldQualityValue.medium },
28
+ blurSide: { type: 'f32', default: DepthOfFieldSideValue.both },
29
+ });
30
+
31
+ export type DepthOfFieldData = ShapeOf<SchemaOf<typeof DepthOfField>>;
32
+
33
+ export function depthOfFieldQualityFromF32(value: number): DepthOfFieldQuality | undefined {
34
+ if (value === DepthOfFieldQualityValue.low) return 'low';
35
+ if (value === DepthOfFieldQualityValue.medium) return 'medium';
36
+ if (value === DepthOfFieldQualityValue.high) return 'high';
37
+ return undefined;
38
+ }
39
+
40
+ export function depthOfFieldSideFromF32(value: number): DepthOfFieldSide | undefined {
41
+ if (value === DepthOfFieldSideValue.both) return 'both';
42
+ if (value === DepthOfFieldSideValue.near) return 'near';
43
+ if (value === DepthOfFieldSideValue.far) return 'far';
44
+ return undefined;
45
+ }
@@ -60,7 +60,8 @@ export {
60
60
  * splitLambda ∈ [0,1] — PSSM split weight (default 0.75)
61
61
  * cascadeBlend ∈ [0,0.5] — blend width between cascades (default 0.2)
62
62
  * mapSize >= 1 — shadow map resolution (default 2048)
63
- * depthBias — normalized receiver depth subtraction (default 0.00001)
63
+ * depthBias — normalized receiver depth floor (default 0.00001),
64
+ * plus automatic texel/filter/slope coverage
64
65
  * normalBias — world-space receiver normal offset (default 0.05)
65
66
  * shadowDistance > 0 — how far shadows reach in front of the camera,
66
67
  * in world units (default 200). This is the sole
@@ -1,6 +1,7 @@
1
1
  export * from './atmosphere';
2
2
  export * from './camera';
3
3
  export * from './cube-camera';
4
+ export * from './depth-of-field';
4
5
  export * from './directional-light';
5
6
  export * from './dynamic-resolution';
6
7
  export * from './fog';
@@ -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
  }