@forgeax/engine-render 0.1.23 → 0.1.24

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 (271) hide show
  1. package/README.md +131 -9
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts +168 -0
  15. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  17. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  18. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  19. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  20. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  21. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  22. package/dist/assembly/recovery/generation.d.ts +46 -0
  23. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  24. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  28. package/dist/assembly/renderer-facade.d.ts +10 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  30. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  31. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  32. package/dist/assembly/renderer-helpers.d.ts +19 -0
  33. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  34. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  35. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  36. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  37. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  38. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  39. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +55 -0
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  43. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  44. package/dist/authoring.mjs +1 -1
  45. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  46. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  47. package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
  48. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  49. package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
  50. package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
  51. package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
  52. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  53. package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
  54. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  55. package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
  56. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  57. package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
  58. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  59. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
  60. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  61. package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
  62. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  63. package/dist/construct-renderer.mjs +19841 -16190
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/device-scope.d.ts +2 -0
  66. package/dist/device/device-scope.d.ts.map +1 -1
  67. package/dist/device/gpu-residency.d.ts +24 -4
  68. package/dist/device/gpu-residency.d.ts.map +1 -1
  69. package/dist/environment/frame.d.ts +5 -0
  70. package/dist/environment/frame.d.ts.map +1 -1
  71. package/dist/environment/inspection.d.ts +7 -0
  72. package/dist/environment/inspection.d.ts.map +1 -1
  73. package/dist/environment/lifecycle.d.ts +20 -2
  74. package/dist/environment/lifecycle.d.ts.map +1 -1
  75. package/dist/errors/recover.d.ts +58 -2
  76. package/dist/errors/recover.d.ts.map +1 -1
  77. package/dist/errors/render.d.ts +13 -0
  78. package/dist/errors/render.d.ts.map +1 -1
  79. package/dist/extract/contracts.d.ts +899 -0
  80. package/dist/extract/contracts.d.ts.map +1 -0
  81. package/dist/features/host.d.ts +2 -0
  82. package/dist/features/host.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +3 -0
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/prepared-graphics-store.d.ts +2 -0
  86. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  87. package/dist/gpu-driven/production-raster.d.ts +28 -0
  88. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  89. package/dist/gpu-scene.d.ts +1 -0
  90. package/dist/gpu-scene.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +6 -6
  98. package/dist/internal.mjs +6 -6
  99. package/dist/light-buffer-layout.d.ts +3 -0
  100. package/dist/light-buffer-layout.d.ts.map +1 -1
  101. package/dist/materials.d.ts +34 -10
  102. package/dist/materials.d.ts.map +1 -1
  103. package/dist/mesh-material-bindings.d.ts +56 -0
  104. package/dist/mesh-material-bindings.d.ts.map +1 -1
  105. package/dist/occlusion-proxy.d.ts +4 -0
  106. package/dist/occlusion-proxy.d.ts.map +1 -0
  107. package/dist/pbr-pipeline.d.ts +32 -9
  108. package/dist/pbr-pipeline.d.ts.map +1 -1
  109. package/dist/points-lines/prepare.d.ts +3 -0
  110. package/dist/points-lines/prepare.d.ts.map +1 -1
  111. package/dist/points-lines/standard-owner.d.ts +33 -0
  112. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  113. package/dist/record/frame-snapshot.d.ts +5 -0
  114. package/dist/record/frame-snapshot.d.ts.map +1 -1
  115. package/dist/record/frame-targets.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +14 -2
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/main-pass-geometry.d.ts +14 -1
  119. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  120. package/dist/record/main-pass-material.d.ts +7 -17
  121. package/dist/record/main-pass-material.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +11 -3
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/recovery-pipeline.d.ts +30 -0
  126. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  127. package/dist/record/render-context.d.ts +26 -4
  128. package/dist/record/render-context.d.ts.map +1 -1
  129. package/dist/record/typed-frame-graph.d.ts +1 -1
  130. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  131. package/dist/recovery/render-system-candidate.d.ts +65 -0
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  133. package/dist/render-contract.d.ts +64 -7
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-system-extract-tail.d.ts +12 -0
  136. package/dist/render-system-extract-tail.d.ts.map +1 -0
  137. package/dist/render-system-extract.d.ts +156 -22
  138. package/dist/render-system-extract.d.ts.map +1 -1
  139. package/dist/render-system.d.ts +128 -4
  140. package/dist/render-system.d.ts.map +1 -1
  141. package/dist/scene/render-scene.d.ts +19 -0
  142. package/dist/scene/render-scene.d.ts.map +1 -1
  143. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  144. package/dist/temporal/index.mjs +6 -6
  145. package/dist/temporal/taa-history-store.d.ts +15 -1
  146. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  147. package/dist/temporal/transaction.d.ts +5 -0
  148. package/dist/temporal/transaction.d.ts.map +1 -1
  149. package/package.json +20 -20
  150. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  151. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  152. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  153. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  154. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  155. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  156. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  157. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  158. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  159. package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
  160. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  161. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  162. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  163. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  164. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  165. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  166. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  167. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  168. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  169. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  170. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
  171. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  172. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  173. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  174. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  175. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  176. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  177. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  178. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  179. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  180. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  181. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  182. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  183. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  184. package/src/assembly/bundler-contract.ts +9 -0
  185. package/src/assembly/device-feature-admission.ts +15 -5
  186. package/src/assembly/extended-lighting-fallback.ts +216 -0
  187. package/src/assembly/factory.ts +81 -10088
  188. package/src/assembly/material/assembly.ts +18 -2
  189. package/src/assembly/material/standard-layer-projection.ts +16 -0
  190. package/src/assembly/material-shader-policy.ts +854 -0
  191. package/src/assembly/material-shader-prewarm.ts +88 -0
  192. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  193. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  194. package/src/assembly/recovery/generation.ts +141 -0
  195. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  196. package/src/assembly/recovery/renderer-recover.ts +906 -0
  197. package/src/assembly/renderer-facade.ts +342 -0
  198. package/src/assembly/renderer-frame-transaction.ts +45 -0
  199. package/src/assembly/renderer-helpers.ts +99 -0
  200. package/src/assembly/renderer-lifecycle.ts +159 -0
  201. package/src/assembly/shader-prewarm-policy.ts +157 -0
  202. package/src/assembly/webgpu-ready-contract.ts +14 -0
  203. package/src/assembly/webgpu-ready.ts +4079 -0
  204. package/src/assembly/webgpu-renderer.ts +4095 -0
  205. package/src/components/camera.ts +1 -1
  206. package/src/debug-draw-glue.ts +1 -1
  207. package/src/device/device-scope.ts +35 -6
  208. package/src/device/gpu-residency.ts +194 -26
  209. package/src/environment/frame.ts +6 -0
  210. package/src/environment/inspection.ts +10 -0
  211. package/src/environment/lifecycle.ts +114 -8
  212. package/src/errors/recover.ts +76 -5
  213. package/src/errors/render.ts +17 -0
  214. package/src/extract/contracts.ts +974 -0
  215. package/src/features/host.ts +16 -0
  216. package/src/features/prepared-gpu-work.ts +20 -2
  217. package/src/features/prepared-graphics-store.ts +17 -0
  218. package/src/gpu-driven/production-raster.ts +97 -2
  219. package/src/gpu-scene.ts +3 -2
  220. package/src/ibl/IblPipelineCache.ts +134 -50
  221. package/src/ibl/skylight-bind-group.ts +26 -0
  222. package/src/index.ts +9 -1
  223. package/src/light-buffer-layout.ts +3 -0
  224. package/src/materials.ts +262 -45
  225. package/src/mesh-material-bindings.ts +59 -0
  226. package/src/occlusion-proxy.ts +64 -0
  227. package/src/pbr-pipeline.ts +139 -15
  228. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  229. package/src/points-lines/prepare.ts +9 -0
  230. package/src/points-lines/standard-owner.ts +403 -0
  231. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  232. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  233. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  234. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  235. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  236. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  237. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  238. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  239. package/src/record/frame-snapshot.ts +15 -0
  240. package/src/record/frame-targets.ts +11 -1
  241. package/src/record/frame.ts +6 -2
  242. package/src/record/helpers.ts +1 -1
  243. package/src/record/main-pass-geometry.ts +230 -5
  244. package/src/record/main-pass-material.ts +316 -209
  245. package/src/record/main-pass-sprite-draws.ts +6 -7
  246. package/src/record/main-pass.ts +94 -6
  247. package/src/record/mesh-ssbo.ts +32 -10
  248. package/src/record/recovery-pipeline.ts +695 -0
  249. package/src/record/render-context.ts +35 -4
  250. package/src/record/typed-frame-graph.ts +45 -1
  251. package/src/record/view-ubo.ts +3 -3
  252. package/src/recovery/render-system-candidate.ts +839 -0
  253. package/src/render-contract.ts +65 -7
  254. package/src/render-system-extract-tail.ts +2983 -0
  255. package/src/render-system-extract.ts +124 -2977
  256. package/src/render-system.ts +868 -659
  257. package/src/scene/render-scene.ts +124 -1
  258. package/src/temporal/frame-coordinator.ts +4 -1
  259. package/src/temporal/taa-history-store.ts +46 -2
  260. package/src/temporal/temporal-view.ts +1 -1
  261. package/src/temporal/transaction.ts +9 -0
  262. package/src/tilemap-chunk-extract-system.ts +1 -1
  263. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  264. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  265. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  266. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  267. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  268. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  269. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  270. package/dist/chunk-YALPS44A.mjs.map +0 -1
  271. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -0,0 +1,216 @@
1
+ import { RhiNullCommandEncoder, RhiNullDevice, RhiNullQueue, rhi } from '@forgeax/engine-rhi-null';
2
+ import { ok } from '@forgeax/engine-types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import {
5
+ acquireDeviceGeneration,
6
+ probeCandidateGraphExecution,
7
+ runRecoveryStep,
8
+ } from '../assembly/recovery/recovery-attempt';
9
+ import {
10
+ createRecoveryContinuation,
11
+ createRecoveryDeadline,
12
+ createRendererLifecycle,
13
+ createSingleFlight,
14
+ RECOVERY_ADAPTER_DEADLINE_MS,
15
+ RECOVERY_DEVICE_DEADLINE_MS,
16
+ RECOVERY_PHASES,
17
+ RECOVERY_TOTAL_DEADLINE_MS,
18
+ } from '../assembly/renderer-lifecycle';
19
+
20
+ describe('renderer recovery lifecycle', () => {
21
+ it('exhausts the public lifecycle states and rejects illegal transitions', () => {
22
+ const lifecycle = createRendererLifecycle();
23
+
24
+ expect(lifecycle.state()).toBe('alive');
25
+ expect(lifecycle.transition('device-lost')).toBe(true);
26
+ expect(lifecycle.transition('recovering')).toBe(true);
27
+ expect(lifecycle.transition('alive')).toBe(true);
28
+ expect(lifecycle.transition('faulted')).toBe(false);
29
+ expect(lifecycle.transition('disposed')).toBe(true);
30
+ expect(lifecycle.transition('alive')).toBe(false);
31
+ expect(lifecycle.states()).toEqual([
32
+ 'alive',
33
+ 'device-lost',
34
+ 'recovering',
35
+ 'faulted',
36
+ 'disposed',
37
+ ]);
38
+ });
39
+
40
+ it('shares one recovery promise and one candidate for concurrent callers', async () => {
41
+ let attempts = 0;
42
+ let release: (() => void) | undefined;
43
+ const flight = createSingleFlight(async () => {
44
+ attempts += 1;
45
+ await new Promise<void>((resolve) => {
46
+ release = resolve;
47
+ });
48
+ return { candidate: attempts };
49
+ });
50
+
51
+ const first = flight.run();
52
+ const second = flight.run();
53
+ expect(first).toBe(second);
54
+ expect(flight.inFlight()).toBe(first);
55
+ expect(attempts).toBe(1);
56
+
57
+ release?.();
58
+ await expect(first).resolves.toEqual({ candidate: 1 });
59
+ expect(flight.inFlight()).toBeUndefined();
60
+ });
61
+
62
+ it('enforces the 20 second attempt and 10 second acquisition budgets', () => {
63
+ const deadline = createRecoveryDeadline(1000);
64
+
65
+ expect(deadline.totalDeadlineMs).toBe(RECOVERY_TOTAL_DEADLINE_MS);
66
+ expect(deadline.adapterDeadlineMs).toBe(RECOVERY_ADAPTER_DEADLINE_MS);
67
+ expect(deadline.deviceDeadlineMs).toBe(RECOVERY_DEVICE_DEADLINE_MS);
68
+ expect(deadline.isValid('acquire-adapter', 10_999)).toBe(true);
69
+ expect(deadline.isValid('acquire-adapter', 11_000)).toBe(false);
70
+ expect(deadline.isValid('acquire-device', 11_000)).toBe(false);
71
+ expect(deadline.isValid('compile-graph', 20_999)).toBe(true);
72
+ expect(deadline.isValid('compile-graph', 21_000)).toBe(false);
73
+ expect(RECOVERY_PHASES).toEqual([
74
+ 'quiesce',
75
+ 'acquire-adapter',
76
+ 'acquire-device',
77
+ 'rehydrate',
78
+ 'compile-graph',
79
+ 'publish',
80
+ 'cleanup',
81
+ ]);
82
+ });
83
+
84
+ it('reports acquisition phases in their execution order', async () => {
85
+ const phases: string[] = [];
86
+ const result = await acquireDeviceGeneration(
87
+ { rhi },
88
+ {} as HTMLCanvasElement,
89
+ undefined,
90
+ undefined,
91
+ (phase) => phases.push(phase),
92
+ );
93
+
94
+ expect(result.ok).toBe(true);
95
+ expect(phases).toEqual(['acquire-adapter', 'acquire-device']);
96
+ });
97
+
98
+ it('treats the null graph probe as structural-only and rejects a non-null pixel mismatch', async () => {
99
+ const adapter = (await rhi.requestAdapter()).unwrap();
100
+ const nullDevice = (await adapter.requestDevice()).unwrap();
101
+ const texture = nullDevice
102
+ .createTexture({
103
+ label: 'recovery-probe-test.texture',
104
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
105
+ mipLevelCount: 1,
106
+ sampleCount: 1,
107
+ dimension: '2d',
108
+ format: 'rgba8unorm',
109
+ usage: 0x11,
110
+ viewFormats: undefined,
111
+ textureBindingViewDimension: undefined,
112
+ })
113
+ .unwrap();
114
+ const view = nullDevice.createTextureView(texture, { dimension: '2d' }).unwrap();
115
+ const nullProbe = await probeCandidateGraphExecution(nullDevice, {
116
+ texture,
117
+ view,
118
+ format: 'rgba8unorm',
119
+ });
120
+ expect(nullProbe.ok).toBe(true);
121
+
122
+ class PixellessNonNullDevice extends RhiNullDevice {
123
+ override get caps() {
124
+ return { ...super.caps, backendKind: 'webgpu' as const };
125
+ }
126
+ }
127
+ const nonNullDevice = new PixellessNonNullDevice(
128
+ new RhiNullQueue(),
129
+ (bookkeeper, device) => new RhiNullCommandEncoder(bookkeeper, device),
130
+ );
131
+ const nonNullTexture = nonNullDevice
132
+ .createTexture({
133
+ label: 'recovery-probe-test.non-null-texture',
134
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
135
+ mipLevelCount: 1,
136
+ sampleCount: 1,
137
+ dimension: '2d',
138
+ format: 'rgba8unorm',
139
+ usage: 0x11,
140
+ viewFormats: undefined,
141
+ textureBindingViewDimension: undefined,
142
+ })
143
+ .unwrap();
144
+ const nonNullView = nonNullDevice
145
+ .createTextureView(nonNullTexture, { dimension: '2d' })
146
+ .unwrap();
147
+ const nonNullProbe = await probeCandidateGraphExecution(nonNullDevice, {
148
+ texture: nonNullTexture,
149
+ view: nonNullView,
150
+ format: 'rgba8unorm',
151
+ });
152
+ expect(nonNullProbe.ok).toBe(false);
153
+ if (!nonNullProbe.ok) expect(nonNullProbe.error.code).toBe('webgpu-runtime-error');
154
+ });
155
+
156
+ it('invalidates late continuation and cleans the candidate exactly once', () => {
157
+ let cleanupCount = 0;
158
+ const continuation = createRecoveryContinuation(createRecoveryDeadline(0), () => {
159
+ cleanupCount += 1;
160
+ });
161
+
162
+ expect(continuation.isValid('compile-graph', 19_999)).toBe(true);
163
+ continuation.abandon(20_000);
164
+ expect(continuation.isValid('compile-graph', 20_000)).toBe(false);
165
+ continuation.cleanupOnce();
166
+ continuation.cleanupOnce();
167
+ expect(cleanupCount).toBe(1);
168
+ });
169
+
170
+ it('invalidates an async step on timeout before a late completion can write', async () => {
171
+ let cleanupCount = 0;
172
+ let lateWrites = 0;
173
+ const startedAt = Date.now();
174
+ const deadline = {
175
+ startedAt,
176
+ totalDeadlineMs: 1,
177
+ adapterDeadlineMs: 1,
178
+ deviceDeadlineMs: 1,
179
+ deadlineAt: startedAt + 1,
180
+ beginDeviceAcquisition: (_now: number) => undefined,
181
+ isValid: (
182
+ _phase:
183
+ | 'quiesce'
184
+ | 'acquire-adapter'
185
+ | 'acquire-device'
186
+ | 'rehydrate'
187
+ | 'compile-graph'
188
+ | 'publish'
189
+ | 'cleanup',
190
+ now: number,
191
+ ) => now < startedAt + 1,
192
+ elapsed: (now: number) => Math.max(0, now - startedAt),
193
+ };
194
+ const continuation = createRecoveryContinuation(deadline, () => {
195
+ cleanupCount += 1;
196
+ });
197
+
198
+ const outcome = await runRecoveryStep(
199
+ async (attemptToken) => {
200
+ await new Promise((resolve) => setTimeout(resolve, 10));
201
+ if (attemptToken?.isValid('compile-graph', Date.now())) lateWrites += 1;
202
+ return ok(undefined);
203
+ },
204
+ 'compile-graph',
205
+ deadline,
206
+ Date.now,
207
+ continuation,
208
+ );
209
+
210
+ expect(outcome.kind).toBe('timeout');
211
+ if (outcome.kind === 'timeout') expect(outcome.timeout.cause.code).toBe('webgpu-runtime-error');
212
+ await new Promise((resolve) => setTimeout(resolve, 20));
213
+ expect(lateWrites).toBe(0);
214
+ expect(cleanupCount).toBe(1);
215
+ });
216
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { TaaHistoryStore } from '../temporal/taa-history-store';
3
+
4
+ type RecoveryReceipt = {
5
+ readonly deviceGeneration: number;
6
+ readonly completed: boolean;
7
+ };
8
+
9
+ type RecoverableHistory = TaaHistoryStore & {
10
+ recoverForGeneration(deviceGeneration: number): void;
11
+ };
12
+
13
+ function recover(store: TaaHistoryStore, deviceGeneration: number): void {
14
+ (store as RecoverableHistory).recoverForGeneration(deviceGeneration);
15
+ }
16
+
17
+ function commit(store: TaaHistoryStore, receipt: RecoveryReceipt): void {
18
+ const attempt = store.begin('device-generation');
19
+ attempt.commit(receipt as never);
20
+ }
21
+
22
+ describe('M4 / m4_t2 — target history recovery keeps logical token', () => {
23
+ it('publishes uninitialized replacement storage with neutral fallback', () => {
24
+ const store = new TaaHistoryStore();
25
+ const token = (
26
+ store.inspect() as typeof store.inspect extends () => infer T ? T : never & { token: string }
27
+ ).token;
28
+
29
+ recover(store, 8);
30
+
31
+ expect(store.inspect()).toMatchObject({
32
+ token,
33
+ deviceGeneration: 8,
34
+ content: 'uninitialized',
35
+ fallback: 'neutral-texture',
36
+ valid: false,
37
+ requestVersion: 0,
38
+ });
39
+ });
40
+
41
+ it('raises history only for a matching completed receipt', () => {
42
+ const store = new TaaHistoryStore();
43
+ recover(store, 8);
44
+ const before = store.inspect();
45
+
46
+ commit(store, { deviceGeneration: 7, completed: true });
47
+ expect(store.inspect()).toMatchObject({
48
+ token: before.token,
49
+ deviceGeneration: 8,
50
+ content: 'uninitialized',
51
+ valid: false,
52
+ requestVersion: 0,
53
+ });
54
+
55
+ commit(store, { deviceGeneration: 8, completed: false });
56
+ expect(store.inspect().valid).toBe(false);
57
+
58
+ commit(store, { deviceGeneration: 8, completed: true });
59
+ expect(store.inspect()).toMatchObject({
60
+ token: before.token,
61
+ deviceGeneration: 8,
62
+ content: 'active',
63
+ valid: true,
64
+ requestVersion: 1,
65
+ });
66
+ });
67
+
68
+ it('does not invent once or on-demand requests during replacement', () => {
69
+ const store = new TaaHistoryStore();
70
+ recover(store, 9);
71
+ const inspection = store.inspect() as typeof store.inspect extends () => infer T
72
+ ? T
73
+ : never & {
74
+ requestVersion: number;
75
+ };
76
+
77
+ expect(inspection.requestVersion).toBe(0);
78
+ expect(inspection.content).toBe('uninitialized');
79
+ });
80
+ });
@@ -58,22 +58,19 @@ const standardPbrVariants = (() => {
58
58
  const variants = [];
59
59
  for (const cluster of [false, true]) {
60
60
  for (const storage of [false, true]) {
61
- for (const transmission of [false, true]) {
62
- for (const vertexColor of [false, true]) {
63
- const defines = {
64
- CLUSTER_FORWARD_AVAILABLE: cluster,
65
- STORAGE_BUFFER_AVAILABLE: storage,
66
- TRANSMISSION_AVAILABLE: transmission,
67
- VERTEX_COLOR_AVAILABLE: vertexColor,
68
- };
69
- const sortedDefines = Object.entries(defines).sort(([left], [right]) =>
70
- left < right ? -1 : left > right ? 1 : 0,
71
- );
72
- const definesKey = sortedDefines.every(([, value]) => value)
73
- ? ''
74
- : sortedDefines.map(([key, value]) => `${key}=${value}`).join('+');
75
- variants.push({ definesKey, defines, composedWgsl: pbrWgsl });
76
- }
61
+ for (const vertexColor of [false, true]) {
62
+ const defines = {
63
+ CLUSTER_FORWARD_AVAILABLE: cluster,
64
+ STORAGE_BUFFER_AVAILABLE: storage,
65
+ VERTEX_COLOR_AVAILABLE: vertexColor,
66
+ };
67
+ const sortedDefines = Object.entries(defines).sort(([left], [right]) =>
68
+ left < right ? -1 : left > right ? 1 : 0,
69
+ );
70
+ const definesKey = sortedDefines.every(([, value]) => value)
71
+ ? ''
72
+ : sortedDefines.map(([key, value]) => `${key}=${value}`).join('+');
73
+ variants.push({ definesKey, defines, composedWgsl: pbrWgsl });
77
74
  }
78
75
  }
79
76
  }
@@ -20,7 +20,7 @@ const fragmentConsumerSources = [
20
20
  pbrPipelineSource,
21
21
  iblPipelineCacheSource,
22
22
  readFileSync(new URL('../ibl/skylight-bind-group.ts', import.meta.url), 'utf8'),
23
- readFileSync(new URL('../assembly/factory.ts', import.meta.url), 'utf8'),
23
+ readFileSync(new URL('../assembly/webgpu-renderer.ts', import.meta.url), 'utf8'),
24
24
  ];
25
25
 
26
26
  describe('shader stage owner', () => {
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Materials } from '../materials.js';
3
+
4
+ describe('Standard pass projection', () => {
5
+ it('keeps default and custom base surfaces on the base pass family', () => {
6
+ const material = Materials.standard({ baseColor: [1, 1, 1, 1] });
7
+ expect(material.passes?.map((pass) => pass.name)).toEqual([
8
+ 'forward',
9
+ 'deferred',
10
+ 'shadow-caster',
11
+ ]);
12
+ });
13
+
14
+ it('uses the same physical forward-only projection for a zero-factor declaration', () => {
15
+ const material = Materials.standard({
16
+ baseColor: [1, 1, 1, 1],
17
+ clearcoat: 0,
18
+ clearcoatRoughness: 0,
19
+ });
20
+ expect(material.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
21
+ expect(material.parameters?.some((parameter) => parameter.name === 'clearcoat')).toBe(true);
22
+ });
23
+ });
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
  import {
3
3
  type MaterialShaderManifestEntry,
4
4
  selectHdrpPbrPrewarmVariants,
5
+ selectProbePrewarmVariants,
5
6
  selectStandardPbrTransmissionPrewarmVariants,
6
7
  } from '../assembly/factory.js';
7
8
 
@@ -9,10 +10,6 @@ const falseKey =
9
10
  'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false';
10
11
  const trueKey =
11
12
  'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
12
- const hdrpFalseKey =
13
- 'CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false';
14
- const hdrpTrueKey =
15
- 'CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
16
13
  const probeFalseKey =
17
14
  'CLUSTER_FORWARD_AVAILABLE=false+PROBE_BLEND_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false';
18
15
  const probeTrueKey =
@@ -30,29 +27,33 @@ function standardEntry(
30
27
  };
31
28
  }
32
29
 
33
- function transmissionVariant(definesKey: string, transmission: boolean) {
30
+ function variant(cluster: boolean, storage: boolean, projector?: boolean) {
31
+ const defines = {
32
+ CLUSTER_FORWARD_AVAILABLE: cluster,
33
+ STORAGE_BUFFER_AVAILABLE: storage,
34
+ VERTEX_COLOR_AVAILABLE: false,
35
+ ...(projector === undefined ? {} : { PROJECTOR_AVAILABLE: projector }),
36
+ };
34
37
  return {
35
- definesKey,
36
- defines: {
37
- CLUSTER_FORWARD_AVAILABLE: false,
38
- STORAGE_BUFFER_AVAILABLE: true,
39
- TRANSMISSION_AVAILABLE: transmission,
40
- VERTEX_COLOR_AVAILABLE: false,
41
- },
42
- composedWgsl: `standard-${transmission}`,
38
+ definesKey: Object.entries(defines)
39
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
40
+ .map(([name, value]) => `${name}=${value}`)
41
+ .join('+'),
42
+ defines,
43
+ composedWgsl: JSON.stringify(defines),
43
44
  };
44
45
  }
45
46
 
46
- function hdrpVariant(definesKey: string, transmission: boolean) {
47
+ function transmissionVariant(definesKey: string, transmission: boolean) {
47
48
  return {
48
49
  definesKey,
49
50
  defines: {
50
- CLUSTER_FORWARD_AVAILABLE: true,
51
+ CLUSTER_FORWARD_AVAILABLE: false,
51
52
  STORAGE_BUFFER_AVAILABLE: true,
52
53
  TRANSMISSION_AVAILABLE: transmission,
53
54
  VERTEX_COLOR_AVAILABLE: false,
54
55
  },
55
- composedWgsl: `hdrp-${transmission}`,
56
+ composedWgsl: `standard-${transmission}`,
56
57
  };
57
58
  }
58
59
 
@@ -60,103 +61,112 @@ function probeTransmissionVariant(definesKey: string, transmission: boolean) {
60
61
  const base = transmissionVariant(definesKey, transmission);
61
62
  return {
62
63
  ...base,
63
- definesKey,
64
64
  defines: { ...base.defines, PROBE_BLEND_AVAILABLE: true },
65
65
  };
66
66
  }
67
67
 
68
- function capabilityTransmissionVariant(
69
- definesKey: string,
70
- transmission: boolean,
71
- extendedLighting: boolean,
72
- ) {
73
- const base = transmissionVariant(definesKey, transmission);
74
- return {
75
- ...base,
76
- definesKey,
77
- defines: {
78
- ...base.defines,
79
- EXTENDED_LIGHTING_AVAILABLE: extendedLighting,
80
- PROJECTOR_AVAILABLE: true,
81
- },
82
- composedWgsl: `standard-${extendedLighting ? 'extended' : 'fallback'}-${transmission}`,
83
- };
84
- }
85
-
86
68
  describe('Standard transmission exact-key prewarm selection', () => {
87
- it('returns both declared variants through exact manifest lookup', () => {
69
+ it('returns both declared transmission variants', () => {
88
70
  const entry = standardEntry([
89
71
  transmissionVariant(falseKey, false),
90
72
  transmissionVariant(trueKey, true),
91
73
  ]);
92
- const selected = selectStandardPbrTransmissionPrewarmVariants(entry, true);
93
- expect(selected.map((variant) => variant.definesKey)).toEqual([falseKey, trueKey]);
74
+ expect(
75
+ selectStandardPbrTransmissionPrewarmVariants(entry, true).map((item) => item.definesKey),
76
+ ).toEqual([falseKey, trueKey]);
94
77
  });
95
78
 
96
- it('fails when the manifest does not declare an exact transmission key', () => {
79
+ it('rejects a missing exact transmission variant', () => {
97
80
  const entry = standardEntry([transmissionVariant(falseKey, false)]);
98
81
  expect(() => selectStandardPbrTransmissionPrewarmVariants(entry, true)).toThrow(
99
82
  'TRANSMISSION_AVAILABLE=true',
100
83
  );
101
84
  });
85
+ });
102
86
 
103
- it('fails when the Standard material shader manifest row is missing', () => {
104
- expect(() => selectStandardPbrTransmissionPrewarmVariants(undefined, true)).toThrow(
105
- 'manifest row is missing',
106
- );
107
- });
108
-
109
- it('keeps object-level probe variants out of the boot prewarm selection', () => {
87
+ describe('HDRP PBR capability prewarm selection', () => {
88
+ it('selects only the matching static capability/geometry state', () => {
110
89
  const entry = standardEntry([
111
- probeTransmissionVariant(probeFalseKey, false),
112
- transmissionVariant(falseKey, false),
113
- probeTransmissionVariant(probeTrueKey, true),
114
- transmissionVariant(trueKey, true),
90
+ variant(true, true),
91
+ variant(true, false),
92
+ variant(false, true),
93
+ variant(false, false),
115
94
  ]);
116
- expect(
117
- selectStandardPbrTransmissionPrewarmVariants(entry, true).map(
118
- (variant) => variant.definesKey,
119
- ),
120
- ).toEqual([falseKey, trueKey]);
95
+ expect(selectHdrpPbrPrewarmVariants(entry, true).map((item) => item.definesKey)).toEqual([
96
+ variant(true, true).definesKey,
97
+ ]);
98
+ // Cluster-forward requires the storage-backed group(2) ABI. A device
99
+ // without storage support must stay on the URP route and prewarm nothing.
100
+ expect(selectHdrpPbrPrewarmVariants(entry, false)).toEqual([]);
121
101
  });
122
102
 
123
- it('selects the prewarm pair for the admitted extended-lighting capability', () => {
124
- const fallbackFalseKey = `${falseKey}+EXTENDED_LIGHTING_AVAILABLE=false+PROJECTOR_AVAILABLE=true`;
125
- const fallbackTrueKey = `${trueKey}+EXTENDED_LIGHTING_AVAILABLE=false+PROJECTOR_AVAILABLE=true`;
126
- const extendedFalseKey = `${falseKey}+EXTENDED_LIGHTING_AVAILABLE=true+PROJECTOR_AVAILABLE=true`;
127
- const extendedTrueKey = `${trueKey}+EXTENDED_LIGHTING_AVAILABLE=true+PROJECTOR_AVAILABLE=true`;
103
+ it('respects an explicitly declared projector capability', () => {
128
104
  const entry = standardEntry([
129
- capabilityTransmissionVariant(fallbackFalseKey, false, false),
130
- capabilityTransmissionVariant(fallbackTrueKey, true, false),
131
- capabilityTransmissionVariant(extendedFalseKey, false, true),
132
- capabilityTransmissionVariant(extendedTrueKey, true, true),
105
+ variant(true, true, true),
106
+ variant(true, true, false),
107
+ variant(true, true),
133
108
  ]);
134
-
135
109
  expect(
136
- selectStandardPbrTransmissionPrewarmVariants(entry, true, true, true, false).map(
137
- (variant) => variant.definesKey,
138
- ),
139
- ).toEqual([fallbackFalseKey, fallbackTrueKey]);
140
- expect(
141
- selectStandardPbrTransmissionPrewarmVariants(entry, true, true, true, true).map(
142
- (variant) => variant.definesKey,
143
- ),
144
- ).toEqual([extendedFalseKey, extendedTrueKey]);
110
+ selectHdrpPbrPrewarmVariants(entry, true, undefined, true).map((item) => item.definesKey),
111
+ ).toEqual([variant(true, true, true).definesKey, variant(true, true).definesKey]);
145
112
  });
146
113
  });
147
114
 
148
- describe('HDRP PBR prewarm capability selection', () => {
149
- const entry = standardEntry([hdrpVariant(hdrpFalseKey, false), hdrpVariant(hdrpTrueKey, true)]);
150
-
151
- it('does not compile the transmission variant below the sampled-texture limit', () => {
152
- expect(
153
- selectHdrpPbrPrewarmVariants(entry, true, false).map((variant) => variant.definesKey),
154
- ).toEqual([hdrpFalseKey]);
115
+ describe('probe material module prewarm capability selection', () => {
116
+ it('keeps probe variants out of the fallback tier and matches device-owned axes', () => {
117
+ const entry = standardEntry([
118
+ probeTransmissionVariant(probeFalseKey, false),
119
+ probeTransmissionVariant(probeTrueKey, true),
120
+ ]);
121
+ expect(selectProbePrewarmVariants(entry, false)).toEqual([]);
122
+ expect(selectProbePrewarmVariants(entry, true).map((variant) => variant.definesKey)).toEqual([
123
+ probeFalseKey,
124
+ probeTrueKey,
125
+ ]);
155
126
  });
156
127
 
157
- it('retains both HDRP variants when transmission is supported', () => {
158
- expect(
159
- selectHdrpPbrPrewarmVariants(entry, true, true).map((variant) => variant.definesKey),
160
- ).toEqual([hdrpFalseKey, hdrpTrueKey]);
128
+ it('filters backend and sampled-texture axes but retains geometry/topology choices', () => {
129
+ const goodKey =
130
+ 'CLUSTER_FORWARD_AVAILABLE=true+EXTENDED_LIGHTING_AVAILABLE=false+PROBE_BLEND_AVAILABLE=true+PROJECTOR_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=true';
131
+ const wrongExtendedKey = goodKey.replace(
132
+ 'EXTENDED_LIGHTING_AVAILABLE=false',
133
+ 'EXTENDED_LIGHTING_AVAILABLE=true',
134
+ );
135
+ const wrongProjectorKey = goodKey.replace(
136
+ 'PROJECTOR_AVAILABLE=false',
137
+ 'PROJECTOR_AVAILABLE=true',
138
+ );
139
+ const wrongTransmissionKey = goodKey.replace(
140
+ 'TRANSMISSION_AVAILABLE=false',
141
+ 'TRANSMISSION_AVAILABLE=true',
142
+ );
143
+ const variant = (definesKey: string, defines: Record<string, boolean>) => ({
144
+ definesKey,
145
+ defines,
146
+ composedWgsl: definesKey,
147
+ });
148
+ const baseDefines = {
149
+ CLUSTER_FORWARD_AVAILABLE: true,
150
+ EXTENDED_LIGHTING_AVAILABLE: false,
151
+ PROBE_BLEND_AVAILABLE: true,
152
+ PROJECTOR_AVAILABLE: false,
153
+ STORAGE_BUFFER_AVAILABLE: true,
154
+ TRANSMISSION_AVAILABLE: false,
155
+ VERTEX_COLOR_AVAILABLE: true,
156
+ };
157
+ const selected = selectProbePrewarmVariants(
158
+ standardEntry([
159
+ variant(goodKey, baseDefines),
160
+ variant(wrongExtendedKey, { ...baseDefines, EXTENDED_LIGHTING_AVAILABLE: true }),
161
+ variant(wrongProjectorKey, { ...baseDefines, PROJECTOR_AVAILABLE: true }),
162
+ variant(wrongTransmissionKey, { ...baseDefines, TRANSMISSION_AVAILABLE: true }),
163
+ ]),
164
+ true,
165
+ false,
166
+ false,
167
+ true,
168
+ false,
169
+ );
170
+ expect(selected.map((candidate) => candidate.definesKey)).toEqual([goodKey]);
161
171
  });
162
172
  });
@@ -219,7 +219,9 @@ describe('volumetric fog RenderGraph topology', () => {
219
219
  fileURLToPath(new URL('../render-system.ts', import.meta.url)),
220
220
  'utf8',
221
221
  );
222
- const recoverStart = renderSystemSource.indexOf('resetForRecover(): void {');
222
+ const recoverStart = renderSystemSource.indexOf(
223
+ 'resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void {',
224
+ );
223
225
  expect(recoverStart).toBeGreaterThanOrEqual(0);
224
226
  const recoverEnd = renderSystemSource.indexOf(
225
227
  'restorePostProcessResources(): void',
@@ -0,0 +1,9 @@
1
+ import type { ImportTransport } from '@forgeax/engine-types';
2
+
3
+ /** Build-tool inputs forwarded from the host into renderer assembly. */
4
+ export interface BundlerOptions {
5
+ readonly importTransport?: ImportTransport | undefined;
6
+ readonly shaderManifestUrl?: string | undefined;
7
+ /** Exact checkout revision emitted by the build-tool adapter. */
8
+ readonly build?: string | undefined;
9
+ }
@@ -14,6 +14,14 @@ const COMPRESSION_FEATURES: GPUFeatureName[] = [
14
14
  // request that larger limit when the adapter can provide it; adapter limits are
15
15
  // not automatically inherited by requestDevice().
16
16
  export const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
17
+ /**
18
+ * The complete Standard physical root can declare all optional texture slots.
19
+ * Its material group therefore needs one more sampled-texture entry than the
20
+ * extended-lighting topology alone. Request this ceiling when the adapter can
21
+ * provide it; lower-capability devices remain fail-closed at material
22
+ * pipeline admission instead of receiving an invalid WebGPU layout.
23
+ */
24
+ export const STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES = 21;
17
25
 
18
26
  export interface DeviceFeatureAdmissionOptions {
19
27
  readonly gpuPassTiming?: object | undefined;
@@ -41,11 +49,13 @@ export function deriveDeviceFeatureAdmission(
41
49
  }
42
50
  const adapterSampledTextureLimit = adapter.limits?.maxSampledTexturesPerShaderStage ?? 0;
43
51
  const requiredSampledTextureLimit =
44
- adapterSampledTextureLimit >= EXTENDED_LIGHTING_REQUIRED_SAMPLED_TEXTURES
45
- ? EXTENDED_LIGHTING_REQUIRED_SAMPLED_TEXTURES
46
- : adapterSampledTextureLimit >= STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES
47
- ? STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES
48
- : undefined;
52
+ adapterSampledTextureLimit >= STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES
53
+ ? STANDARD_PHYSICAL_REQUIRED_SAMPLED_TEXTURES
54
+ : adapterSampledTextureLimit >= EXTENDED_LIGHTING_REQUIRED_SAMPLED_TEXTURES
55
+ ? EXTENDED_LIGHTING_REQUIRED_SAMPLED_TEXTURES
56
+ : adapterSampledTextureLimit >= STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES
57
+ ? STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES
58
+ : undefined;
49
59
  return {
50
60
  requiredFeatures,
51
61
  ...(requiredSampledTextureLimit === undefined