@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
@@ -2,6 +2,7 @@ import type {
2
2
  FrameObservationRequest,
3
3
  FrameReceipt,
4
4
  RenderError as PublicRenderError,
5
+ SunCardinalityError as PublicSunCardinalityError,
5
6
  ReflectionProbeInspection,
6
7
  ReflectionProbeSelectionInspection,
7
8
  RendererState,
@@ -65,6 +66,13 @@ const ssrInspection: SsrSpatialInspection = inspection.ssr;
65
66
  void ssrInspection;
66
67
  type IntentError = Extract<PublicRenderError, { readonly code: 'render-intent-invalid' }>;
67
68
  expectTypeOf<IntentError['detail']>().toEqualTypeOf<RenderIntentInvalidDetail>();
69
+ type PublicSunCardinalityCode = Extract<PublicRenderError['code'], 'sun-cardinality'>;
70
+ expectTypeOf<PublicSunCardinalityCode>().toEqualTypeOf<'sun-cardinality'>();
71
+ expectTypeOf<PublicSunCardinalityError['code']>().toEqualTypeOf<'sun-cardinality'>();
72
+ expectTypeOf<PublicSunCardinalityError['detail']>().toEqualTypeOf<{
73
+ readonly field: 'sun';
74
+ readonly value: number;
75
+ }>();
68
76
  const backendKind: RenderInspection['capabilities']['backendKind'] =
69
77
  inspection.capabilities.backendKind;
70
78
  void backendKind;
@@ -39,6 +39,7 @@ const expectedCodes = [
39
39
  'dynamic-resolution-invalid-parameter',
40
40
  'dynamic-resolution-requires-taa',
41
41
  'dynamic-resolution-timing-unavailable',
42
+ 'motion-blur-invalid-params',
42
43
  'shadow-invalid-config',
43
44
  'equirect-projection-failed',
44
45
  'standard-light-budget-exceeded',
@@ -63,6 +64,7 @@ const expectedCodes = [
63
64
  'render-feature-draw-recording-failed',
64
65
  'environment-source-conflict',
65
66
  'fog-cardinality',
67
+ 'sun-cardinality',
66
68
  'taa-caps-insufficient',
67
69
  'environment-generation-failed',
68
70
  'atmosphere-invalid-parameter',
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
4
+ shouldReportPendingRenderFeatureError,
5
+ } from '../features/error-reporting.js';
6
+
7
+ describe('pending render-feature error reporting', () => {
8
+ it('reports the first failure, suppresses the hot-loop, and reopens the cooldown', () => {
9
+ const lastReportedFrame = new Map<string, number>();
10
+ expect(shouldReportPendingRenderFeatureError(lastReportedFrame, 'feature\0plan', 10)).toBe(
11
+ true,
12
+ );
13
+ expect(shouldReportPendingRenderFeatureError(lastReportedFrame, 'feature\0plan', 11)).toBe(
14
+ false,
15
+ );
16
+ expect(
17
+ shouldReportPendingRenderFeatureError(
18
+ lastReportedFrame,
19
+ 'feature\0plan',
20
+ 10 + PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
21
+ ),
22
+ ).toBe(true);
23
+ });
24
+ });
@@ -89,6 +89,29 @@ function frame(host: RenderFeatureHost, frameNumber: number) {
89
89
  }
90
90
 
91
91
  describe('RenderFeature stage fault recovery lifecycle', () => {
92
+ it('retains the original exception when a feature stage throws', () => {
93
+ const cause = new TypeError('feature plan exploded');
94
+ const host = createRenderFeatureHost(
95
+ [
96
+ {
97
+ identity: 'synthetic.m10.throwing',
98
+ extract: () => ok({ ready: true }),
99
+ plan: () => {
100
+ throw cause;
101
+ },
102
+ },
103
+ ],
104
+ caps,
105
+ ).unwrap();
106
+
107
+ const result = frame(host, 1);
108
+ expect(result.errors[0]).toMatchObject({
109
+ code: 'render-feature-stage-failed',
110
+ detail: { featureIdentity: 'synthetic.m10.throwing', cause },
111
+ });
112
+ expect(host.diagnostics()[0]?.latestError).toMatchObject({ detail: { cause } });
113
+ });
114
+
92
115
  it.each<Fault>([
93
116
  'create',
94
117
  'prepared-resource',
@@ -130,4 +130,35 @@ describe('RenderScene temporal retry boundary', () => {
130
130
  instanceCollections.dispose();
131
131
  }
132
132
  });
133
+
134
+ it('keeps instance motion valid when stable generations reorder', () => {
135
+ const instanceCollections = new InstanceCollectionStore();
136
+ const collection = instanceCollections.create({ transforms: matrices(2, 1) }).unwrap();
137
+ const initialInstances = instanceCollections.snapshot(collection.collectionId).unwrap();
138
+ instanceCollections
139
+ .replace(
140
+ collection.collectionId,
141
+ matrices(2, 9),
142
+ new Uint32Array([
143
+ initialInstances.generations[1] ?? 0,
144
+ initialInstances.generations[0] ?? 0,
145
+ ]),
146
+ )
147
+ .unwrap();
148
+ const reorderedInstances = instanceCollections.snapshot(collection.collectionId).unwrap();
149
+ const scene = new RenderScene();
150
+ scene.setTemporalTracking(true);
151
+ try {
152
+ scene.apply([updateSnapshot(snapshot(7, 1, initialInstances))]);
153
+ submit(scene, 7);
154
+ scene.apply([updateInstances(snapshot(7, 1, reorderedInstances))]);
155
+
156
+ const temporal = scene.temporalSnapshotBySlot(0);
157
+ expect(temporal?.motionValid).toBe(true);
158
+ expect(temporal?.previousInstances?.generations).toEqual(initialInstances.generations);
159
+ expect(temporal?.previousInstances?.transforms).toEqual(initialInstances.transforms);
160
+ } finally {
161
+ instanceCollections.dispose();
162
+ }
163
+ });
133
164
  });
@@ -87,7 +87,8 @@ function temporalResolveFrame(
87
87
  bloom: 'off',
88
88
  bloomThreshold: 1,
89
89
  bloomIntensity: 1,
90
- bloomBlurRadius: 1,
90
+ bloomSoftKnee: 0.5,
91
+ bloomScatter: 0.7,
91
92
  clearColor: [0, 0, 0, 1],
92
93
  temporal: frameState.lastSuccessfulTemporalView,
93
94
  },
@@ -1147,6 +1148,7 @@ describe('renderer successful-submit transaction', () => {
1147
1148
  height: 4,
1148
1149
  frameIndex: 6,
1149
1150
  historyValid: true,
1151
+ sampleTimeSeconds: 1,
1150
1152
  });
1151
1153
  const frameState = {
1152
1154
  temporalGpuState: undefined,
@@ -1160,6 +1162,7 @@ describe('renderer successful-submit transaction', () => {
1160
1162
  height: 4,
1161
1163
  frameIndex: 6,
1162
1164
  historyValid: true,
1165
+ sampleTimeSeconds: 1.016,
1163
1166
  }),
1164
1167
  },
1165
1168
  environmentGeneration,
@@ -1244,6 +1247,7 @@ describe('renderer successful-submit transaction', () => {
1244
1247
  expect(active.valid).toBe(true);
1245
1248
  expect(active.readIndex).toBe(1);
1246
1249
  expect(frameState.lastSuccessfulTemporalView).toBe(lastSuccessfulTemporalView);
1250
+ expect(frameState.lastSuccessfulTemporalView.sampleTimeSeconds).toBe(1);
1247
1251
  expect(environmentLifecycle.inspect().activeSignature).toBe(activeEnvironment.signature);
1248
1252
  expect(environmentLifecycle.inspect().lkgSignature).toBe(activeEnvironment.signature);
1249
1253
  frameState.environmentGeneration = environmentLifecycle
@@ -1257,6 +1261,7 @@ describe('renderer successful-submit transaction', () => {
1257
1261
  height: 8,
1258
1262
  frameIndex: 7,
1259
1263
  historyValid: true,
1264
+ sampleTimeSeconds: 1.016,
1260
1265
  }),
1261
1266
  };
1262
1267
  const retryFrame = temporalResolveFrame(device, scope, frameState, {});
@@ -1273,6 +1278,7 @@ describe('renderer successful-submit transaction', () => {
1273
1278
  expect(gpuCommitAfterSubmit).toEqual([1]);
1274
1279
  expect(frameState.activeTemporalGpuState).not.toBe(active);
1275
1280
  expect(frameState.successfulTemporalFrameIndex).toBe(8);
1281
+ expect(frameState.lastSuccessfulTemporalView?.sampleTimeSeconds).toBe(1.016);
1276
1282
  expect(frameState.temporalGpuState).toBeUndefined();
1277
1283
  expect(frameState.retiringTemporalGpuStates.size).toBe(1);
1278
1284
  expect(publishEnvironment).toHaveBeenCalledTimes(1);
@@ -43,14 +43,14 @@ const entries: readonly FixtureEntry[] = [
43
43
  bindings: '[]',
44
44
  },
45
45
  {
46
- hash: 'fixture-bloom-bright',
47
- wgsl: '// fixture marker: BloomBrightParams\n',
46
+ hash: 'fixture-bloom-downsample',
47
+ wgsl: '// fixture marker: BloomDownsampleParams\n',
48
48
  glsl: '',
49
49
  bindings: '[]',
50
50
  },
51
51
  {
52
- hash: 'fixture-bloom-blur',
53
- wgsl: '// fixture marker: BloomBlurParams\n',
52
+ hash: 'fixture-bloom-upsample',
53
+ wgsl: '// fixture marker: BloomUpsampleParams\n',
54
54
  glsl: '',
55
55
  bindings: '[]',
56
56
  },
@@ -60,6 +60,8 @@ describe('skinned shadow caster', () => {
60
60
  expect(shader).toContain('discard;');
61
61
  expect(shader).toContain('@group(2) @binding(1)');
62
62
  expect(shader).toContain('let skinMatrix = palette[');
63
+ expect(shader).toContain('materialIndex = u32(meshes[meshIndex].temporal.w);');
64
+ expect(shader).not.toContain('bitcast<u32>(meshes[meshIndex].temporal.w)');
63
65
  expect(shader).toContain('@location(13) color : vec4<f32>');
64
66
  expect(shader).toContain('out.color = in.color;');
65
67
  expect(record).toContain('entry.source.skin.byteOffset');
@@ -41,7 +41,8 @@ function camera(): CameraSnapshot {
41
41
  bloom: 'off',
42
42
  bloomThreshold: 1,
43
43
  bloomIntensity: 1,
44
- bloomBlurRadius: 4,
44
+ bloomSoftKnee: 0.5,
45
+ bloomScatter: 0.7,
45
46
  clearColor: [0, 0, 0, 1],
46
47
  };
47
48
  }
@@ -31,7 +31,7 @@ function topology(): RenderPipelineTopology {
31
31
  storageFormat: 'bgra8unorm',
32
32
  viewFormat: 'bgra8unorm-srgb',
33
33
  },
34
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
34
+ camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on', bloomIntensity: 1 },
35
35
  shadow: {
36
36
  directional: { mapSize: 64, cascadeCount: 1 },
37
37
  spotMapSize: 64,
@@ -35,10 +35,15 @@ const dualViewProfile: SurfaceProfile = {
35
35
  hasDisplayEndpoint: true,
36
36
  };
37
37
 
38
+ type TopologyOverrides = Omit<Partial<RenderPipelineTopology>, 'camera'> & {
39
+ readonly camera?: Partial<RenderPipelineTopology['camera']>;
40
+ };
41
+
38
42
  function topology(
39
43
  profile: StandardProfile,
40
- overrides: Partial<RenderPipelineTopology> = {},
44
+ overrides: TopologyOverrides = {},
41
45
  ): RenderPipelineTopology {
46
+ const { camera: cameraOverride, ...rest } = overrides;
42
47
  return {
43
48
  pipelineId: STANDARD_PIPELINE_ID,
44
49
  standardProfile: profile,
@@ -49,7 +54,13 @@ function topology(
49
54
  storageFormat: 'bgra8unorm',
50
55
  viewFormat: 'bgra8unorm-srgb',
51
56
  },
52
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
57
+ camera: {
58
+ tonemap: 'aces-filmic',
59
+ antialias: 'fxaa',
60
+ bloom: 'on',
61
+ bloomIntensity: 1,
62
+ ...cameraOverride,
63
+ },
53
64
  shadow: {
54
65
  directional: { mapSize: 64, cascadeCount: 1 },
55
66
  spotMapSize: 64,
@@ -65,7 +76,7 @@ function topology(
65
76
  },
66
77
  featureTopologySignature: 'none',
67
78
  gpuDrivenTopologySignature: '',
68
- ...overrides,
79
+ ...rest,
69
80
  };
70
81
  }
71
82
 
@@ -109,7 +120,7 @@ function standardLightingFor(resolvedTopology: RenderPipelineTopology) {
109
120
  return standardLighting;
110
121
  }
111
122
 
112
- async function build(profile: StandardProfile, overrides?: Partial<RenderPipelineTopology>) {
123
+ async function build(profile: StandardProfile, overrides?: TopologyOverrides) {
113
124
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
114
125
  const resolvedTopology = topology(profile, overrides);
115
126
  const histogram = graph.createBuffer('standard-auto-exposure-histogram', { size: 1024 });
@@ -232,7 +243,7 @@ function importedTaaHistory(graph: RenderGraphBuilder<RenderPipelineFrame>) {
232
243
  };
233
244
  }
234
245
 
235
- async function buildTaa(profile: StandardProfile, overrides?: Partial<RenderPipelineTopology>) {
246
+ async function buildTaa(profile: StandardProfile, overrides?: TopologyOverrides) {
236
247
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
237
248
  const history = importedTaaHistory(graph);
238
249
  const resolvedTopology = topology(profile, overrides);
@@ -542,9 +553,8 @@ describe('forgeax::standard graph', () => {
542
553
  expect(result.ok).toBe(true);
543
554
  if (!result.ok) return;
544
555
  const names = result.value.inspect().passes.map((pass) => pass.name);
545
- expect(names).not.toContain('bloom-bright');
546
- expect(names).not.toContain('bloom-blur-h');
547
- expect(names).not.toContain('bloom-blur-v');
556
+ expect(names.some((name) => name.startsWith('bloom-downsample-'))).toBe(false);
557
+ expect(names.some((name) => name.startsWith('bloom-upsample-'))).toBe(false);
548
558
  expect(names).not.toContain('bloom-composite');
549
559
  expect(names).toContain('taa-resolve');
550
560
  expect(names).toContain('output-transform');
@@ -37,6 +37,16 @@ describe('TemporalView reset matrix', () => {
37
37
  expect(resolveTemporalReset(previous, next)).toBeUndefined();
38
38
  });
39
39
 
40
+ it('applies camera reset reasons to motion-blur-only off views', () => {
41
+ const previous = createTemporalView({ ...base().input, antialias: 'none' });
42
+ const next = createTemporalView({
43
+ ...previous.input,
44
+ antialias: 'none',
45
+ viewIdentity: 'secondary',
46
+ });
47
+ expect(resolveTemporalReset(previous, next)).toBe('view-switch');
48
+ });
49
+
40
50
  it('treats an explicit off commit as a first-frame boundary when TAA resumes', () => {
41
51
  const previous = base();
42
52
  const off = createTemporalView({
@@ -130,7 +130,8 @@ describe('TemporalView v1', () => {
130
130
  bloom: 'disabled',
131
131
  bloomThreshold: 1,
132
132
  bloomIntensity: 0,
133
- bloomBlurRadius: 0,
133
+ bloomSoftKnee: 0.5,
134
+ bloomScatter: 0.7,
134
135
  clearColor: [0, 0, 0, 1] as const,
135
136
  } as unknown as CameraSnapshot;
136
137
  const light = {
@@ -32,6 +32,10 @@ import { standardTemporalPostOrder } from '../temporal/standard-scene-data';
32
32
 
33
33
  let device: RhiDevice;
34
34
 
35
+ type TopologyOverrides = Omit<Partial<RenderPipelineTopology>, 'camera'> & {
36
+ readonly camera?: Partial<RenderPipelineTopology['camera']>;
37
+ };
38
+
35
39
  beforeAll(async () => {
36
40
  const adapter = await rhi.requestAdapter();
37
41
  if (!adapter.ok) throw adapter.error;
@@ -42,8 +46,9 @@ beforeAll(async () => {
42
46
 
43
47
  function topology(
44
48
  pipelineId: 'forgeax::standard',
45
- overrides: Partial<RenderPipelineTopology> = {},
49
+ overrides: TopologyOverrides = {},
46
50
  ): RenderPipelineTopology {
51
+ const { camera: cameraOverride, ...rest } = overrides;
47
52
  return {
48
53
  pipelineId,
49
54
  config: undefined,
@@ -53,7 +58,13 @@ function topology(
53
58
  storageFormat: 'bgra8unorm',
54
59
  viewFormat: 'bgra8unorm-srgb',
55
60
  },
56
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
61
+ camera: {
62
+ tonemap: 'aces-filmic',
63
+ antialias: 'fxaa',
64
+ bloom: 'on',
65
+ bloomIntensity: 1,
66
+ ...cameraOverride,
67
+ },
57
68
  shadow: {
58
69
  directional: { mapSize: 1024, cascadeCount: 4 },
59
70
  spotMapSize: 1024,
@@ -69,7 +80,7 @@ function topology(
69
80
  },
70
81
  featureTopologySignature: 'none',
71
82
  gpuDrivenTopologySignature: '',
72
- ...overrides,
83
+ ...rest,
73
84
  };
74
85
  }
75
86
 
@@ -877,7 +888,12 @@ describe('typed built-in pipeline topology', () => {
877
888
 
878
889
  it('keeps zero-shutter Motion Blur free of temporal target and passes', () => {
879
890
  expect(
880
- isMotionBlurTemporalDemand({ shutterAngle: 0, maxRadiusPixels: 32, sampleCount: 8 }),
891
+ isMotionBlurTemporalDemand({
892
+ shutterAngle: 0,
893
+ maxRadiusPixels: 32,
894
+ sampleCount: 8,
895
+ targetFps: 60,
896
+ }),
881
897
  ).toBe(false);
882
898
  const compiled = compile(
883
899
  standardPipeline,
@@ -42,7 +42,9 @@ describe('volumetric optics CPU oracle', () => {
42
42
 
43
43
  it('converts the shared integrated-light basis once before normalized HG', () => {
44
44
  expect(integrateShader).toContain('const VOLUME_LIGHT_PHASE_SCALE : f32 = FOUR_PI;');
45
- expect(integrateShader).toContain('phase *\n VOLUME_LIGHT_PHASE_SCALE *');
45
+ expect(integrateShader.match(/phase\s*\*\s*VOLUME_LIGHT_PHASE_SCALE\s*\*/g) ?? []).toHaveLength(
46
+ 1,
47
+ );
46
48
  });
47
49
 
48
50
  it('keeps empty density at identity transmittance', () => {
@@ -1,10 +1,11 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
- import { World } from '@forgeax/engine-ecs';
4
+ import { Time, World } from '@forgeax/engine-ecs';
5
5
  import { RenderGraphBuilder } from '@forgeax/engine-render-graph';
6
6
  import type {
7
7
  BindGroupLayoutDescriptor,
8
+ Buffer,
8
9
  RenderPipelineDescriptor,
9
10
  RhiAdapter,
10
11
  RhiDevice,
@@ -33,6 +34,7 @@ import {
33
34
  deriveVolumetricFogExtent,
34
35
  deriveVolumetricFogResolvedExtent,
35
36
  inspectVolumetricFogResources,
37
+ VOLUMETRIC_FOG_PARAMS_BYTES,
36
38
  } from '../volume/resources';
37
39
  import { resolveVolumeTemporalReset } from '../volume/temporal';
38
40
 
@@ -62,15 +64,12 @@ class RecoverableNullDevice extends RhiNullDevice {
62
64
  private readonly resolveLost: (info: LostInfo) => void;
63
65
  private readonly lostPromise: Promise<LostInfo>;
64
66
 
65
- constructor() {
67
+ constructor(queue: RhiNullQueue = new RhiNullQueue()) {
66
68
  let resolveLost!: (info: LostInfo) => void;
67
69
  const lostPromise = new Promise<LostInfo>((resolve) => {
68
70
  resolveLost = resolve;
69
71
  });
70
- super(
71
- new RhiNullQueue(),
72
- (bookkeeper, device) => new RhiNullCommandEncoder(bookkeeper, device),
73
- );
72
+ super(queue, (bookkeeper, device) => new RhiNullCommandEncoder(bookkeeper, device));
74
73
  this.resolveLost = resolveLost;
75
74
  this.lostPromise = lostPromise;
76
75
  }
@@ -104,13 +103,44 @@ class RecoverableNullAdapter implements RhiAdapter {
104
103
  readonly limits: Readonly<Record<string, number>> = {};
105
104
  readonly devices: RecoverableNullDevice[] = [];
106
105
 
106
+ constructor(private readonly queueFactory: () => RhiNullQueue = () => new RhiNullQueue()) {}
107
+
107
108
  requestDevice(): Promise<Result<RhiDevice, RhiError>> {
108
- const device = new RecoverableNullDevice();
109
+ const device = new RecoverableNullDevice(this.queueFactory());
109
110
  this.devices.push(device);
110
111
  return Promise.resolve(ok(device));
111
112
  }
112
113
  }
113
114
 
115
+ interface CapturedQueueWrite {
116
+ readonly buffer: Buffer;
117
+ readonly offset: number;
118
+ readonly bytes: Uint8Array;
119
+ }
120
+
121
+ /** Captures the real record-stage volume parameter writes without adding a
122
+ * second render implementation. */
123
+ class RecordingNullQueue extends RhiNullQueue {
124
+ readonly writes: CapturedQueueWrite[] = [];
125
+
126
+ override writeBuffer(
127
+ buffer: Buffer,
128
+ bufferOffset: number,
129
+ data: ArrayBufferView | ArrayBuffer,
130
+ dataOffset?: number,
131
+ size?: number,
132
+ ) {
133
+ const source =
134
+ data instanceof ArrayBuffer
135
+ ? new Uint8Array(data)
136
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
137
+ const start = dataOffset ?? 0;
138
+ const end = size === undefined ? source.byteLength : start + size;
139
+ this.writes.push({ buffer, offset: bufferOffset, bytes: source.slice(start, end) });
140
+ return super.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
141
+ }
142
+ }
143
+
114
144
  function surfaceCanvas(width = 1920, height = 1080): HTMLCanvasElement {
115
145
  return { width, height, getContext: () => null } as unknown as HTMLCanvasElement;
116
146
  }
@@ -336,11 +366,11 @@ describe('volumetric fog budget and recovery contract', () => {
336
366
  label: 'volume-params',
337
367
  kind: 'buffer',
338
368
  origin: 'imported',
339
- descriptor: { kind: 'buffer', size: 128 },
369
+ descriptor: { kind: 'buffer', size: VOLUMETRIC_FOG_PARAMS_BYTES },
340
370
  firstUse: 0,
341
371
  lastUse: 3,
342
372
  derivedUsage: 64,
343
- byteSize: 128,
373
+ byteSize: VOLUMETRIC_FOG_PARAMS_BYTES,
344
374
  },
345
375
  ],
346
376
  } as const;
@@ -349,21 +379,25 @@ describe('volumetric fog budget and recovery contract', () => {
349
379
  expect(facts.sampleCount).toBe(960 * 540 * 64);
350
380
  expect(facts.resolvedPixelCount).toBe(960 * 540);
351
381
  expect(facts.currentBytes).toBe(960 * 540 * 8);
352
- expect(facts.bufferBytes).toBe(128);
382
+ expect(facts.bufferBytes).toBe(VOLUMETRIC_FOG_PARAMS_BYTES);
353
383
  expect(facts.liveResourceCount).toBe(5);
354
384
  expect(facts.historyBytes).toBe(960 * 540 * 8 * 2);
355
385
  expect(facts.physicalResourceCount).toBe(5);
356
- expect(facts.totalBytes).toBe(960 * 540 * 16 * 4 + 960 * 540 * 8 * 3 + 128);
386
+ expect(facts.totalBytes).toBe(
387
+ 960 * 540 * 16 * 4 + 960 * 540 * 8 * 3 + VOLUMETRIC_FOG_PARAMS_BYTES,
388
+ );
357
389
  expect(facts.totalBytes).toBe(
358
390
  facts.currentBytes + facts.historyBytes + facts.scratchBytes + facts.bufferBytes,
359
391
  );
360
392
  expect(facts.totalBytes).toBeLessThanOrEqual(MAX_MEMORY_BYTES);
361
393
 
362
394
  const doubleBuffered = inspectVolumetricFogResources(graph, { parameterBufferCount: 2 });
363
- expect(doubleBuffered.bufferBytes).toBe(256);
395
+ expect(doubleBuffered.bufferBytes).toBe(VOLUMETRIC_FOG_PARAMS_BYTES * 2);
364
396
  expect(doubleBuffered.liveResourceCount).toBe(6);
365
397
  expect(doubleBuffered.physicalResourceCount).toBe(6);
366
- expect(doubleBuffered.totalBytes).toBe(960 * 540 * 16 * 4 + 960 * 540 * 8 * 3 + 256);
398
+ expect(doubleBuffered.totalBytes).toBe(
399
+ 960 * 540 * 16 * 4 + 960 * 540 * 8 * 3 + VOLUMETRIC_FOG_PARAMS_BYTES * 2,
400
+ );
367
401
  expect(doubleBuffered.totalBytes).toBe(
368
402
  doubleBuffered.currentBytes +
369
403
  doubleBuffered.historyBytes +
@@ -372,6 +406,136 @@ describe('volumetric fog budget and recovery contract', () => {
372
406
  );
373
407
  });
374
408
 
409
+ it('uses the canonical World clock to bound real volume history reuse', async () => {
410
+ const queue = new RecordingNullQueue();
411
+ const adapter = new RecoverableNullAdapter(() => queue);
412
+ const backend = {
413
+ ...nullRhi,
414
+ requestAdapter: async () => ok(adapter),
415
+ };
416
+ const created = await constructRendererHost(
417
+ surfaceCanvas(640, 360),
418
+ { rhi: backend },
419
+ { shaderManifestUrl: VOLUME_MANIFEST },
420
+ );
421
+ expect(created.ok).toBe(true);
422
+ if (!created.ok) return;
423
+ const { renderer } = created.value;
424
+ const world = new World();
425
+ const density = world.allocSharedRef('TextureAsset', densityAsset());
426
+ world
427
+ .spawn(
428
+ { component: Transform, data: { pos: [0, 0, 3] } },
429
+ {
430
+ component: Camera,
431
+ data: {
432
+ ...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }),
433
+ tonemap: TONEMAP_ACES_FILMIC,
434
+ },
435
+ },
436
+ )
437
+ .unwrap();
438
+ const sun = world
439
+ .spawn({
440
+ component: DirectionalLight,
441
+ data: { direction: [-0.4, -0.8, -0.3], castShadow: true },
442
+ })
443
+ .unwrap();
444
+ world
445
+ .spawn({
446
+ component: VolumetricFog,
447
+ data: {
448
+ light: sun,
449
+ density,
450
+ boundsMin: [-1, -1, -1],
451
+ boundsMax: [1, 1, 1],
452
+ extinction: [0.2, 0.2, 0.2],
453
+ albedo: [0.8, 0.8, 0.8],
454
+ emission: [0, 0, 0],
455
+ anisotropy: 0,
456
+ maxDistance: 50,
457
+ },
458
+ })
459
+ .unwrap();
460
+
461
+ const lease = renderer.attach(world);
462
+ expect(lease.ok).toBe(true);
463
+ if (!lease.ok) return;
464
+ const request = {
465
+ leases: [lease.value],
466
+ camera: { lease: lease.value },
467
+ environment: { lease: lease.value },
468
+ } as const;
469
+ const drawAndRead = async (): Promise<{
470
+ readonly history: number;
471
+ readonly worldTime: number;
472
+ readonly delta: number;
473
+ }> => {
474
+ queue.writes.length = 0;
475
+ const frame = renderer.draw(request);
476
+ expect(frame.ok).toBe(true);
477
+ if (!frame.ok) return { history: -1, worldTime: -1, delta: -1 };
478
+ expect((await frame.value.completed).ok).toBe(true);
479
+ const paramsWrite = queue.writes
480
+ .filter((write) => write.offset === 0 && write.bytes.byteLength === 144)
481
+ .at(-1);
482
+ expect(paramsWrite).toBeDefined();
483
+ if (paramsWrite === undefined) return { history: -1, worldTime: -1, delta: -1 };
484
+ const historyWrite = queue.writes
485
+ .filter(
486
+ (write) =>
487
+ write.buffer === paramsWrite.buffer &&
488
+ write.offset === 31 * Float32Array.BYTES_PER_ELEMENT &&
489
+ write.bytes.byteLength === 3 * Float32Array.BYTES_PER_ELEMENT,
490
+ )
491
+ .at(-1);
492
+ expect(historyWrite).toBeDefined();
493
+ if (historyWrite === undefined) return { history: -1, worldTime: -1, delta: -1 };
494
+ return {
495
+ history: new Float32Array(
496
+ historyWrite.bytes.buffer,
497
+ historyWrite.bytes.byteOffset,
498
+ 1,
499
+ )[0] as number,
500
+ worldTime: new Float32Array(
501
+ historyWrite.bytes.buffer,
502
+ historyWrite.bytes.byteOffset,
503
+ 3,
504
+ )[1] as number,
505
+ delta: new Float32Array(
506
+ historyWrite.bytes.buffer,
507
+ historyWrite.bytes.byteOffset,
508
+ 3,
509
+ )[2] as number,
510
+ };
511
+ };
512
+
513
+ world.update(1 / 60).unwrap();
514
+ const firstTime = world.getResource(Time).elapsed;
515
+ const first = await drawAndRead();
516
+ expect(first.worldTime).toBeCloseTo(firstTime, 5);
517
+ expect(first.history).toBe(0);
518
+
519
+ const stable = await drawAndRead();
520
+ expect(world.getResource(Time).elapsed).toBe(firstTime);
521
+ expect(stable.worldTime).toBeCloseTo(firstTime, 5);
522
+ expect(stable.history).toBe(1);
523
+
524
+ world.update(1 / 60).unwrap();
525
+ const movedTime = world.getResource(Time).elapsed;
526
+ const moved = await drawAndRead();
527
+ expect(moved.worldTime).toBeCloseTo(movedTime, 5);
528
+ expect(moved.worldTime).toBeGreaterThan(first.worldTime);
529
+ expect(moved.history).toBe(1);
530
+ expect(moved.delta).toBeCloseTo(1 / 60, 6);
531
+ expect(stable.delta).toBe(0);
532
+ for (let index = 0; index < 12; index += 1) world.update(1 / 60).unwrap();
533
+ const jumped = await drawAndRead();
534
+ expect(jumped.history).toBe(0);
535
+ expect(jumped.delta).toBeGreaterThan(0.1);
536
+ await renderer.dispose();
537
+ }, 30_000);
538
+
375
539
  it('derives a same-XY resolve descriptor when the packed raw extent is supplied alone', async () => {
376
540
  const adapter = await nullRhi.requestAdapter();
377
541
  expect(adapter.ok).toBe(true);