@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
@@ -39,6 +39,7 @@ import {
39
39
  projectDirectionalShadowInspectionSource,
40
40
  } from './assembly/directional-shadow-inspection';
41
41
  import { createGpuDrivenOwner } from './assembly/gpu-driven-owner';
42
+ import { standardBloomAdmitted } from './bloom-admission';
42
43
  import { createClusterBinScratch } from './cluster-binner';
43
44
  import { EnvironmentLifecycle } from './environment/lifecycle';
44
45
  import {
@@ -48,11 +49,24 @@ import {
48
49
  } from './errors/render';
49
50
  import { projectDepthOfFieldInspection } from './features/depth-of-field/depth-of-field-inspection';
50
51
  import { resolveDepthOfFieldFrameParams } from './features/depth-of-field/depth-of-field-params';
52
+ import { reportPendingRenderFeatureErrors } from './features/error-reporting';
51
53
  import {
52
54
  type RenderFeaturePreparedGraphicsResolverInput,
53
55
  runRenderFeatureFrame,
54
56
  settlePreparedGraphicsCompletion,
55
57
  } from './features/host';
58
+ import {
59
+ createMotionBlurFeature,
60
+ type MotionBlurFeatureInput,
61
+ } from './features/motion-blur/motion-blur-feature';
62
+ import {
63
+ MotionBlurValidationError,
64
+ motionBlurTemporalDemand,
65
+ } from './features/motion-blur/motion-blur-params';
66
+ import {
67
+ planMotionBlurFrame,
68
+ projectMotionBlurInspection,
69
+ } from './features/motion-blur/motion-blur-runtime';
56
70
  import { createFeatureNoiseResolver } from './features/noise-texture';
57
71
  import {
58
72
  createRenderFeatureGpuWorkOwner,
@@ -83,6 +97,7 @@ import type {
83
97
  TemporalTargetInspection,
84
98
  TransmissionInspection,
85
99
  } from './inspection-types';
100
+ import { emptyBloomInspection } from './inspection-types';
86
101
  import { buildOcclusionProxyVertices } from './occlusion-proxy';
87
102
  import {
88
103
  AUTO_EXPOSURE_HISTOGRAM_BYTES,
@@ -542,7 +557,10 @@ export interface RenderSystem {
542
557
  options: FrameObservationOptions,
543
558
  ): Promise<Result<FrameObservation, ObservationUnavailableError>>;
544
559
  /** Resolve the renderer-owned GPU LOD counters for the last accepted submit. */
545
- observeLodOcclusion(): Promise<void>;
560
+ observeLodOcclusion(receipt?: {
561
+ readonly frameId: number;
562
+ readonly deviceGeneration: number;
563
+ }): Promise<void>;
546
564
  /** Internal same-frame graph target access for backend diagnostics. */
547
565
  getCurrentGraphTarget(name: string): CurrentGraphTarget | undefined;
548
566
  /** Internal test capture copied into the draw's command submission. */
@@ -580,6 +598,8 @@ export interface RenderSystem {
580
598
  readonly temporal: TemporalInspection;
581
599
  readonly bloom: BloomInspection;
582
600
  readonly motionBlurInspection: MotionBlurInspection | undefined;
601
+ /** The last extraction validation failure, preserved for public draw(). */
602
+ readonly motionBlurInvalidParams: MotionBlurValidationError | undefined;
583
603
  readonly depthOfFieldInspection: DepthOfFieldInspection | undefined;
584
604
  readonly temporalTargetInspection: TemporalTargetInspection | undefined;
585
605
  readonly lodOcclusionInspection: LodOcclusionInspection | undefined;
@@ -723,13 +743,6 @@ function reportPreparedGraphicsCompletionError(
723
743
  );
724
744
  }
725
745
 
726
- function isPendingRenderFeaturePreparation(error: RenderError): boolean {
727
- return (
728
- error.code === 'render-feature-preparation-failed' &&
729
- error.detail.reason.startsWith('rhi-not-available:')
730
- );
731
- }
732
-
733
746
  function makePreparedPipelinePendingError(): RhiError {
734
747
  return new RhiError({
735
748
  code: 'rhi-not-available',
@@ -897,6 +910,24 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
897
910
  let preparedFeatureSkylightResources: SkylightBindGroupResources | undefined;
898
911
  const dynamicGeometryFrames = createDynamicGeometryFrameBindings();
899
912
  let latestCamera: CameraSnapshot | undefined;
913
+ // Last public parameter failure; cleared only after a valid extraction.
914
+ let lastMotionBlurInvalidParams: MotionBlurValidationError | undefined;
915
+ // Lazily install the compute lane when a capable frame demands it.
916
+ let motionBlurFeatureInput: MotionBlurFeatureInput | undefined;
917
+ const motionBlurFeature = createMotionBlurFeature(() => motionBlurFeatureInput);
918
+ const ensureMotionBlurFeature = (): void => {
919
+ const host = internals.featureHost;
920
+ if (
921
+ host === undefined ||
922
+ host.features.some((feature) => feature.identity === 'forgeax.motion-blur')
923
+ ) {
924
+ return;
925
+ }
926
+ const installed = host.install(
927
+ motionBlurFeature as import('./features/types').RenderFeature<unknown>,
928
+ );
929
+ if (!installed.ok) internals.errorRegistry.fire(installed.error);
930
+ };
900
931
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
901
932
  const instanceCollections = new InstanceCollectionStore();
902
933
  const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
@@ -1291,6 +1322,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1291
1322
  // tiles until the first frame with a castShadow spot (AC-03).
1292
1323
  spotShadowSnapshots: [],
1293
1324
  };
1325
+ // A feature that is explicitly recoverable on the next frame must not
1326
+ // publish the same transient failure on every render tick. Keep one
1327
+ // renderer-owned cooldown per structured feature failure while retaining
1328
+ // the original error for the first and periodic reports.
1329
+ const pendingFeatureErrorLastReportedFrame = new Map<string, number>();
1294
1330
  const resolveSsrDependencies = (requested: boolean): SsrDependenciesInspection => {
1295
1331
  // Explicit SSR capability preparation belongs to initialization. Inspection
1296
1332
  // only projects receipts and never starts work or creates image history.
@@ -1752,6 +1788,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1752
1788
  readonly visible: number;
1753
1789
  readonly occluded: number;
1754
1790
  readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
1791
+ readonly submit?: { readonly frameId: number; readonly deviceGeneration: number };
1755
1792
  readonly worldSelections?: readonly {
1756
1793
  readonly worldKey: number;
1757
1794
  readonly primitiveSlot: number;
@@ -1763,9 +1800,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1763
1800
  }[];
1764
1801
  }
1765
1802
  | undefined,
1803
+ targetInspection: LodOcclusionInspection | undefined = lastLodOcclusionInspection,
1804
+ targetWorldAttachments: ReadonlyMap<number, string> = lastLodWorldAttachments,
1766
1805
  ): void => {
1767
- if (selection === undefined || lastLodOcclusionInspection === undefined) return;
1768
- const inspection = lastLodOcclusionInspection;
1806
+ if (selection === undefined || targetInspection === undefined) return;
1807
+ const inspection = targetInspection;
1769
1808
  // The GPU readback histogram is the selector fact, while final submitted
1770
1809
  // counts are owned by the persistent visibility projection. Keeping the
1771
1810
  // projection count here excludes ordinary non-LOD geometry (for example a
@@ -1777,7 +1816,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1777
1816
  const selectionsByAttachment = new Map<string, (typeof worldSelections)[number][]>();
1778
1817
  let attributionValid = true;
1779
1818
  for (const selected of worldSelections) {
1780
- const attachmentId = lastLodWorldAttachments.get(selected.worldKey);
1819
+ const attachmentId = targetWorldAttachments.get(selected.worldKey);
1781
1820
  if (attachmentId === undefined) {
1782
1821
  attributionValid = false;
1783
1822
  break;
@@ -1790,15 +1829,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1790
1829
  const nextWorlds = worlds.map((world) => {
1791
1830
  const selected = selectionsByAttachment.get(world.attachmentId);
1792
1831
  const baseRow = world.rows[0];
1793
- if (selected === undefined || baseRow === undefined) {
1794
- if (world.rows.length > 0) attributionValid = false;
1795
- return world;
1796
- }
1797
- const candidateCount = selected.reduce((sum, row) => sum + row.candidateCount, 0);
1798
- if (candidateCount !== baseRow.count.candidates) {
1799
- attributionValid = false;
1800
- return world;
1801
- }
1832
+ // A World with only plain meshes has no LOD selection row. Keep its
1833
+ // projection-only receipt and allow Worlds with valid LOD rows to
1834
+ // retain same-submit attribution. Unknown selected world keys still
1835
+ // fail closed above.
1836
+ if (selected === undefined || baseRow === undefined) return world;
1837
+ // The projection row is intentionally CPU-scoped and may omit
1838
+ // selector-only candidates that remain in the full GPU plan. The
1839
+ // same-submit GPU row owns the denominator; comparing it to the
1840
+ // projection count would reject a valid receipt (for example 11 GPU
1841
+ // candidates versus 10 CPU-admitted renderables).
1802
1842
  return Object.freeze({
1803
1843
  ...world,
1804
1844
  attribution: Object.freeze({
@@ -1828,7 +1868,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1828
1868
  lastLodOcclusionInspection = inspectLodOcclusion({
1829
1869
  ...inspection,
1830
1870
  count: {
1831
- ...lastLodOcclusionInspection.count,
1871
+ ...inspection.count,
1832
1872
  candidates: selection.candidateCount,
1833
1873
  visible: selection.visible,
1834
1874
  occluded: selection.occluded,
@@ -2274,24 +2314,20 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2274
2314
  });
2275
2315
  },
2276
2316
  get motionBlurInspection(): MotionBlurInspection | undefined {
2277
- const camera = latestCamera;
2278
- if (camera?.motionBlur === undefined) return undefined;
2279
- const demanded = camera.motionBlur.shutterAngle > 0;
2280
- const unavailable = demanded && !internals.device.caps.rgba16floatRenderable;
2281
- const admitted =
2282
- frameState.activeTemporalGpuState !== undefined ||
2283
- frameState.temporalGpuState !== undefined;
2284
- return {
2285
- enabled: demanded,
2286
- status: !demanded ? 'off' : unavailable || !admitted ? 'reset' : 'active',
2287
- shutterAngle: camera.motionBlur.shutterAngle,
2288
- maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
2289
- sampleCount: camera.motionBlur.sampleCount,
2290
- temporalDemand: demanded ? 'scene-data-temporal-v1' : 'none',
2291
- passName: 'motion-blur',
2292
- historyWrites: 0,
2293
- ...(unavailable ? { lastFailure: 'scene-data-unavailable' as const } : {}),
2294
- };
2317
+ return projectMotionBlurInspection({
2318
+ camera: latestCamera,
2319
+ invalidParams: lastMotionBlurInvalidParams,
2320
+ capabilities: internals.device.caps,
2321
+ acceptedTemporal: frameState.lastSuccessfulTemporalView,
2322
+ transaction: frameState.temporalFrameTransaction.inspect(),
2323
+ execution: frameState.motionBlurExecution,
2324
+ temporalFrameId: frameState.temporalFrame?.frameId,
2325
+ deviceGeneration: internals.deviceScope.generation,
2326
+ graphGeneration: frameState.graphGeneration,
2327
+ });
2328
+ },
2329
+ get motionBlurInvalidParams(): MotionBlurValidationError | undefined {
2330
+ return lastMotionBlurInvalidParams;
2295
2331
  },
2296
2332
  get depthOfFieldInspection(): DepthOfFieldInspection | undefined {
2297
2333
  const camera = latestCamera;
@@ -2354,22 +2390,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2354
2390
  const lifecycle = resources?.inspectBloomResources?.(
2355
2391
  frameState.compiledFrameGraph?.inspect(),
2356
2392
  );
2357
- return Object.freeze(
2358
- lifecycle ?? {
2359
- graphStatus: 'empty',
2360
- enabled: false,
2361
- targetCount: 0,
2362
- targetBytes: 0,
2363
- resourceCount: 0,
2364
- passCount: 0,
2365
- encodeCount: 0,
2366
- bindGroupCount: 0,
2367
- uploadCount: 0,
2368
- residentChildBytes: 0,
2369
- generation: 0,
2370
- state: 'off',
2371
- },
2372
- );
2393
+ return Object.freeze(lifecycle ?? emptyBloomInspection());
2373
2394
  },
2374
2395
  get transmission(): TransmissionInspection | undefined {
2375
2396
  return lastTransmissionInspection;
@@ -2444,6 +2465,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2444
2465
  const profileSession = internals.profiler?.activeSession();
2445
2466
  let ownsProfileFrame = false;
2446
2467
  let submitted = false;
2468
+ // A prior failed record/submit may have staged a temporal capture. A
2469
+ // capture belongs only to the draw call that created it; discard it
2470
+ // before this call can prepare or commit a new candidate.
2471
+ persistentRenderScene.discardTemporalFrame();
2447
2472
  // A failed draw must never leave a previous frame eligible to publish a
2448
2473
  // newly accepted dynamic-geometry candidate.
2449
2474
  dynamicGeometryFrames.begin();
@@ -2475,55 +2500,67 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2475
2500
  dispatchCounts.unlit = 0;
2476
2501
 
2477
2502
  const resourceWorld = compositionWorlds[resourceOwner] as World;
2478
- const frame = runProfiledRenderPhase(profileSession, 'extract', () => {
2479
- return persistentRenderScene.extractComposition(
2480
- compositionWorlds,
2481
- { cameraOwner, resourceOwner },
2482
- internals.assets.catalogEpoch,
2483
- (renderables) =>
2484
- extractFrames(
2485
- compositionWorlds,
2486
- {
2487
- cameraOwner,
2488
- resourceOwner,
2489
- ...(cameraEntityKey === undefined ? {} : { cameraEntityKey }),
2490
- },
2491
- internals.assets,
2492
- internals.getPipelineState(),
2493
- persistentRenderScene.materialSnapshotCacheStore(),
2494
- {
2495
- ...(internals.device.caps.backendKind === 'null'
2496
- ? {}
2497
- : {
2498
- materialContext: {
2499
- backend:
2500
- internals.device.caps.backendKind === 'wgpu-webgl2'
2501
- ? ('webgl2' as const)
2502
- : internals.device.caps.backendKind,
2503
- capability: internals.device.caps.storageBuffer
2504
- ? ('storage-buffer' as const)
2505
- : ('uniform-fallback' as const),
2506
- pipeline: 'forward' as const,
2507
- geometry: 'mesh' as const,
2508
- pass: 'forward' as const,
2509
- profile: 'forgeax-material-wgsl-v1' as const,
2510
- toolchain: 'naga-oil' as const,
2511
- instrumentation: 'none' as const,
2512
- },
2513
- }),
2514
- cull: 'none',
2515
- retainHidden: true,
2516
- renderables,
2517
- instanceCollections,
2518
- ...(!internals.device.caps.storageBuffer ||
2519
- internals.getMaterialShaderArtifact === undefined
2520
- ? {}
2521
- : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2522
- },
2523
- ),
2524
- compositionLeases,
2525
- );
2526
- });
2503
+ let frame: ReturnType<PersistentRenderScene['extractComposition']>;
2504
+ try {
2505
+ frame = runProfiledRenderPhase(profileSession, 'extract', () => {
2506
+ return persistentRenderScene.extractComposition(
2507
+ compositionWorlds,
2508
+ { cameraOwner, resourceOwner },
2509
+ internals.assets.catalogEpoch,
2510
+ (renderables) =>
2511
+ extractFrames(
2512
+ compositionWorlds,
2513
+ {
2514
+ cameraOwner,
2515
+ resourceOwner,
2516
+ ...(cameraEntityKey === undefined ? {} : { cameraEntityKey }),
2517
+ },
2518
+ internals.assets,
2519
+ internals.getPipelineState(),
2520
+ persistentRenderScene.materialSnapshotCacheStore(),
2521
+ {
2522
+ ...(internals.device.caps.backendKind === 'null'
2523
+ ? {}
2524
+ : {
2525
+ materialContext: {
2526
+ backend:
2527
+ internals.device.caps.backendKind === 'wgpu-webgl2'
2528
+ ? ('webgl2' as const)
2529
+ : internals.device.caps.backendKind,
2530
+ capability: internals.device.caps.storageBuffer
2531
+ ? ('storage-buffer' as const)
2532
+ : ('uniform-fallback' as const),
2533
+ pipeline: 'forward' as const,
2534
+ geometry: 'mesh' as const,
2535
+ pass: 'forward' as const,
2536
+ profile: 'forgeax-material-wgsl-v1' as const,
2537
+ toolchain: 'naga-oil' as const,
2538
+ instrumentation: 'none' as const,
2539
+ },
2540
+ }),
2541
+ cull: 'none',
2542
+ retainHidden: true,
2543
+ renderables,
2544
+ instanceCollections,
2545
+ ...(!internals.device.caps.storageBuffer ||
2546
+ internals.getMaterialShaderArtifact === undefined
2547
+ ? {}
2548
+ : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2549
+ },
2550
+ ),
2551
+ compositionLeases,
2552
+ );
2553
+ });
2554
+ } catch (cause) {
2555
+ if (cause instanceof MotionBlurValidationError) {
2556
+ lastMotionBlurInvalidParams = cause;
2557
+ latestCamera = undefined;
2558
+ internals.errorRegistry.fire(cause);
2559
+ return false;
2560
+ }
2561
+ throw cause;
2562
+ }
2563
+ lastMotionBlurInvalidParams = undefined;
2527
2564
  const {
2528
2565
  cameras: extractedCameras,
2529
2566
  cubeCameras,
@@ -2760,6 +2797,15 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2760
2797
  const submissionRenderables = visibilityProjection.renderables;
2761
2798
  const submissionDispatch = visibilityProjection.dispatch;
2762
2799
  const inspectionCamera = latestCamera;
2800
+ // The public frame receipt is allocated by WebGPURenderer immediately
2801
+ // before this draw (`observationFrameId`). RenderSystem's private
2802
+ // frameNumber is an internal generation and also advances on failed
2803
+ // record attempts, so it cannot be used as receipt identity. Keep the
2804
+ // internal generation for recovery/visibility state, but bind the LOD
2805
+ // inspection to the frame id that the caller will receive.
2806
+ const lodSubmitFrameId =
2807
+ (internals as RenderSystemInternals & { readonly observationFrameId?: number })
2808
+ .observationFrameId ?? frameState.frameNumber;
2763
2809
  let lodCandidateCountForFrame = 0;
2764
2810
  if (inspectionCamera !== undefined) {
2765
2811
  const firstRenderable = renderables[0];
@@ -2802,7 +2848,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2802
2848
  renderables,
2803
2849
  submissionRenderables,
2804
2850
  inspectionCamera,
2805
- frameState.frameNumber,
2851
+ lodSubmitFrameId,
2806
2852
  worldKeys,
2807
2853
  slotByEntity,
2808
2854
  {
@@ -2834,7 +2880,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2834
2880
  firstRenderable === undefined
2835
2881
  ? 'none'
2836
2882
  : `asset-handle:${firstRenderable.assetHandle}`,
2837
- sourceKey: `render-frame:${frameState.frameNumber}`,
2883
+ sourceKey: `render-frame:${lodSubmitFrameId}`,
2838
2884
  },
2839
2885
  view: {
2840
2886
  attachmentId:
@@ -2847,7 +2893,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2847
2893
  primitiveSlot: 0,
2848
2894
  slotGeneration: persistentRenderScene.inspect().topology.revision,
2849
2895
  },
2850
- generation: frameState.frameNumber,
2896
+ generation: lodSubmitFrameId,
2851
2897
  count: {
2852
2898
  candidates: candidateCount,
2853
2899
  visible: inspectionRenderables.length,
@@ -2866,7 +2912,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2866
2912
  visible: true,
2867
2913
  })),
2868
2914
  submit: {
2869
- frameId: frameState.frameNumber,
2915
+ frameId: lodSubmitFrameId,
2870
2916
  build: resolveInspectionBuild(internals),
2871
2917
  deviceGeneration: internals.deviceScope.generation,
2872
2918
  },
@@ -2878,7 +2924,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2878
2924
  lastLodWorldAttachments = new Map();
2879
2925
  }
2880
2926
  const temporalDemanded =
2881
- cameras[0]?.antialias === 'taa' || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
2927
+ cameras[0]?.antialias === 'taa' ||
2928
+ (cameras[0]?.motionBlur !== undefined &&
2929
+ motionBlurTemporalDemand({
2930
+ shutterAngle: cameras[0].motionBlur.shutterAngle,
2931
+ maxRadiusPixels: cameras[0].motionBlur.maxRadiusPixels,
2932
+ sampleCount: cameras[0].motionBlur.sampleCount,
2933
+ targetFps: cameras[0].motionBlur.targetFps ?? 60,
2934
+ }));
2882
2935
  if (temporalDemanded) {
2883
2936
  persistentRenderScene.prepareTemporalFrame(submissionRenderables);
2884
2937
  }
@@ -2989,6 +3042,44 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2989
3042
  storageBuffer: internals.device.caps.storageBuffer,
2990
3043
  multisample: internals.device.caps.backendKind !== 'wgpu-webgl2',
2991
3044
  });
3045
+ const motionBlurParams = cameras[0]?.motionBlur;
3046
+ let motionBlurDelta = 0;
3047
+ try {
3048
+ const delta = (compositionWorlds[cameraOwner] as World).getResource(Time).delta;
3049
+ motionBlurDelta = Number.isFinite(delta) ? Math.max(0, delta) : 0;
3050
+ } catch {
3051
+ motionBlurDelta = 0;
3052
+ }
3053
+ const acceptedSampleTime = frameState.lastSuccessfulTemporalView?.sampleTimeSeconds;
3054
+ const cameraViewIdentity = `camera:${cameras[0]?.entityKey ?? 0}`;
3055
+ const canvasWidth = Math.max(1, internals.canvas.width);
3056
+ const canvasHeight = Math.max(1, internals.canvas.height);
3057
+ const motionBlurPlan = planMotionBlurFrame({
3058
+ params: motionBlurParams,
3059
+ sampleTimeSeconds: opts.sampleTimeSeconds,
3060
+ acceptedSampleTimeSeconds: acceptedSampleTime,
3061
+ fallbackDeltaSeconds: motionBlurDelta,
3062
+ acceptedTemporal: frameState.lastSuccessfulTemporalView,
3063
+ temporalReset: opts.temporalReset === true,
3064
+ width: canvasWidth,
3065
+ height: canvasHeight,
3066
+ cameraViewIdentity,
3067
+ historyVersion: cameras[0]?.historyVersion ?? 0,
3068
+ environmentSignature: environment?.environmentSignature ?? '',
3069
+ fogSignature: environment?.fogSignature ?? '',
3070
+ deviceGeneration: internals.deviceScope?.generation ?? 0,
3071
+ });
3072
+ const demandedMotionBlur = motionBlurPlan.demanded;
3073
+ motionBlurFeatureInput = motionBlurPlan.featureInput;
3074
+ if (
3075
+ demandedMotionBlur &&
3076
+ internals.device.caps.compute === true &&
3077
+ internals.device.caps.storageBuffer === true &&
3078
+ internals.device.caps.storageTexture === true &&
3079
+ internals.device.caps.rgba16floatRenderable === true
3080
+ ) {
3081
+ ensureMotionBlurFeature();
3082
+ }
2992
3083
  preparedFeatureSkylightResources =
2993
3084
  preparedPipelineState === null
2994
3085
  ? undefined
@@ -3001,6 +3092,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3001
3092
  worlds: compositionWorlds,
3002
3093
  owner: resourceOwner,
3003
3094
  frameNumber: frameState.frameNumber,
3095
+ frameSize: { width: internals.canvas.width, height: internals.canvas.height },
3004
3096
  visibilitySnapshots: frame.featureVisibilitySnapshots,
3005
3097
  hiddenEntityReports: frame.hiddenEntityReports,
3006
3098
  targets: featureTargets,
@@ -3035,11 +3127,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3035
3127
  gpuWork: featureGpuWork,
3036
3128
  });
3037
3129
  lastVisibilityStats.explicitlyHidden = featureFrame.hiddenEntityReports.length;
3038
- for (const featureError of featureFrame.errors) {
3039
- if (!isPendingRenderFeaturePreparation(featureError)) {
3040
- internals.errorRegistry.fire(featureError);
3041
- }
3042
- }
3130
+ reportPendingRenderFeatureErrors(
3131
+ featureFrame.errors,
3132
+ frameState.frameNumber,
3133
+ pendingFeatureErrorLastReportedFrame,
3134
+ internals.errorRegistry,
3135
+ );
3043
3136
  const postProcessParamsCandidate = preparePostProcessParamsResources(
3044
3137
  featureFrame.fullscreenEffects,
3045
3138
  );
@@ -3172,7 +3265,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3172
3265
  }
3173
3266
  return runProfiledRenderPhase(profileSession, phase, action);
3174
3267
  };
3175
- if (cameras[0]?.bloom === 'on') {
3268
+ const bloomAdmitted = standardBloomAdmitted(cameras[0]);
3269
+ if (bloomAdmitted) {
3176
3270
  internals.getPipelineState()?.perPassResources.ensureBloomResources?.();
3177
3271
  }
3178
3272
  const liveOutput = cameras[0]?.output;
@@ -3226,6 +3320,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3226
3320
  activeEntityKeys: visibilityProjection.activeEntityKeys,
3227
3321
  activeEntityRevision: visibilityProjection.activeEntityRevision,
3228
3322
  telemetryCandidateCount: lodCandidateCountForFrame,
3323
+ telemetrySubmit: {
3324
+ frameId: lodSubmitFrameId,
3325
+ deviceGeneration: internals.deviceScope.generation,
3326
+ },
3229
3327
  ...(occlusionProjection === undefined ? {} : { occlusion: occlusionProjection }),
3230
3328
  },
3231
3329
  compositionLeases,
@@ -3254,6 +3352,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3254
3352
  undefined,
3255
3353
  ssrDependencies,
3256
3354
  dynamicGeometryFrames.onRenderableDraw(compositionWorlds),
3355
+ environment?.fog,
3356
+ opts.sampleTimeSeconds,
3357
+ opts.temporalReset === true,
3257
3358
  ),
3258
3359
  );
3259
3360
  dynamicGeometryFrames.commit(submitted);
@@ -3341,7 +3442,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3341
3442
  frameState.depthOfFieldLastSubmitFailed = true;
3342
3443
  }
3343
3444
  internals.getPipelineState()?.perPassResources.commitBloomResources?.();
3344
- const nextBloom = cameras[0]?.bloom ?? 'off';
3445
+ const nextBloom = bloomAdmitted ? 'on' : 'off';
3345
3446
  if (frameState.lastSuccessfulBloom === 'on' && nextBloom !== 'on') {
3346
3447
  internals
3347
3448
  .getPipelineState()
@@ -3406,6 +3507,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3406
3507
  }),
3407
3508
  );
3408
3509
  } finally {
3510
+ if (!submitted) persistentRenderScene.discardTemporalFrame();
3409
3511
  internals.recoveryColdWorkGuard?.endFrame();
3410
3512
  if (ownsProfileFrame) {
3411
3513
  try {
@@ -3424,13 +3526,46 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3424
3526
  const currentFrameId = frameState.frameNumber - 1;
3425
3527
  return observeCurrentFrame(options, frameState.currentFrameObservationSource, currentFrameId);
3426
3528
  },
3427
- async observeLodOcclusion(): Promise<void> {
3529
+ async observeLodOcclusion(receipt?: {
3530
+ readonly frameId: number;
3531
+ readonly deviceGeneration: number;
3532
+ }): Promise<void> {
3533
+ const capturedInspection = lastLodOcclusionInspection;
3534
+ const capturedWorldAttachments = lastLodWorldAttachments;
3535
+ if (capturedInspection === undefined) return;
3536
+ if (
3537
+ receipt !== undefined &&
3538
+ (capturedInspection.submit.frameId !== receipt.frameId ||
3539
+ capturedInspection.submit.deviceGeneration !== receipt.deviceGeneration)
3540
+ )
3541
+ return;
3428
3542
  await ensureOcclusionRuntime().waitForCompletions();
3429
- applyLodSelectionTelemetry(await gpuDrivenProduction.readLodSelection());
3430
- if (lastLodOcclusionInspection !== undefined) {
3543
+ if (lastLodOcclusionInspection !== capturedInspection) return;
3544
+ const selection = await gpuDrivenProduction.readLodSelection();
3545
+ if (lastLodOcclusionInspection !== capturedInspection || selection === undefined) return;
3546
+ if (
3547
+ selection.submit !== undefined &&
3548
+ (selection.submit.frameId !== capturedInspection.submit.frameId ||
3549
+ selection.submit.deviceGeneration !== capturedInspection.submit.deviceGeneration)
3550
+ )
3551
+ return;
3552
+ if (
3553
+ receipt !== undefined &&
3554
+ (selection.submit === undefined ||
3555
+ selection.submit.frameId !== receipt.frameId ||
3556
+ selection.submit.deviceGeneration !== receipt.deviceGeneration)
3557
+ )
3558
+ return;
3559
+ applyLodSelectionTelemetry(selection, capturedInspection, capturedWorldAttachments);
3560
+ const published = lastLodOcclusionInspection;
3561
+ if (
3562
+ published !== undefined &&
3563
+ published.submit.frameId === capturedInspection.submit.frameId &&
3564
+ published.submit.deviceGeneration === capturedInspection.submit.deviceGeneration
3565
+ ) {
3431
3566
  const queryLatency = ensureOcclusionRuntime().inspectQueryLatency();
3432
3567
  lastLodOcclusionInspection = inspectLodOcclusion({
3433
- ...lastLodOcclusionInspection,
3568
+ ...published,
3434
3569
  queryLatencyUs: {
3435
3570
  median: queryLatency.median,
3436
3571
  p95: queryLatency.p95,