@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
@@ -32,7 +32,11 @@ import type {
32
32
  TextureFormat,
33
33
  TextureView,
34
34
  } from '@forgeax/engine-rhi';
35
- import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA, type ShaderCatalog } from '@forgeax/engine-shader';
35
+ import {
36
+ type ShaderCatalog,
37
+ STANDARD_PIPELINE_PARAM_SCHEMA,
38
+ standardPhysicalTextureFields,
39
+ } from '@forgeax/engine-shader';
36
40
  import { derive, type ParamSchemaEntry } from '@forgeax/engine-types';
37
41
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from './gpu-stage';
38
42
  import type { PipelineSpec } from './pipeline-spec-types';
@@ -101,11 +105,13 @@ export function standardPbrColorFormats(
101
105
  /**
102
106
  * The built-in standard-PBR material paramSchema's texture/user-region shape,
103
107
  * used as the fallback when the material BGL is built without a registry
104
- * (the caps-driven `buildPbrPipelineLayouts` seam). Declares the 8 Standard
105
- * user-region textures + a numeric UBO run; `derive()` collapses the numerics
106
- * into binding 0 and emits sampler/texture pairs at 1..6 — byte-equivalent to
107
- * the legacy fixed base-7. The shared PBR parameter contract comes from the
108
- * shader package; this module only derives the GPU layout.
108
+ * (the caps-driven `buildPbrPipelineLayouts` seam). Declares the canonical
109
+ * Standard user-region textures + a numeric UBO run; `derive()` collapses the
110
+ * numerics into binding 0 and emits one sampler/texture pair per declared
111
+ * texture. Physical map pairs stay outside this boot region and are appended
112
+ * only for an authored root that declares them. The shared PBR parameter
113
+ * contract comes from the shader package; this module only derives the GPU
114
+ * layout.
109
115
  */
110
116
  /**
111
117
  * Build the PBR material BGL **user-region** from a paramSchema via the
@@ -116,9 +122,10 @@ export function standardPbrColorFormats(
116
122
  * custom schema (e.g. parallax + heightTexture) shifts the injection region by
117
123
  * one sampler/texture pair automatically.
118
124
  *
119
- * The built-in `default-standard-pbr` paramSchema declares exactly 8 textures,
120
- * so this derives to binding 0 UBO + 8 pairs = 17 entries. Injection is appended
121
- * after that derived region.
125
+ * The canonical `STANDARD_PIPELINE_PARAM_SCHEMA` currently declares seven
126
+ * non-physical textures, so this derives to binding 0 UBO + seven pairs = 15
127
+ * entries. IBL and transmission injection are appended after that region;
128
+ * authored physical maps are appended after the engine-owned injections.
122
129
  *
123
130
  * One material-UBO convention is layered on top of the pure `derive()` output:
124
131
  * binding 0 is patched to `{ type: 'uniform', hasDynamicOffset: true }` with
@@ -132,9 +139,18 @@ export function standardPbrColorFormats(
132
139
  * built-in standard-PBR shape when omitted, for the caps-driven seam).
133
140
  */
134
141
  export function buildPbrMaterialUserRegionEntries(
135
- paramSchema: readonly ParamSchemaEntry[] = DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
142
+ paramSchema: readonly ParamSchemaEntry[] = STANDARD_PIPELINE_PARAM_SCHEMA,
143
+ excludedTextureFields: readonly string[] = [],
136
144
  ): GPUBindGroupLayoutEntry[] {
137
- const derived = derive(paramSchema);
145
+ const excluded = new Set(excludedTextureFields);
146
+ for (const field of standardPhysicalTextureFields(paramSchema)) excluded.add(field);
147
+ const derived = derive(
148
+ excluded.size === 0
149
+ ? paramSchema
150
+ : paramSchema.filter(
151
+ (entry) => !excluded.has(entry.name) || !entry.type.startsWith('texture'),
152
+ ),
153
+ );
138
154
  // Project the engine-owned entry shape into the DOM WebGPU descriptor. Omit
139
155
  // absent optional members so exactOptionalPropertyTypes remains true at the
140
156
  // boundary instead of leaking an `undefined` property into the descriptor.
@@ -330,6 +346,68 @@ export const INJECTION_KIND_LENGTHS = {
330
346
  transmission: TRANSMISSION_INJECTION_LENGTH,
331
347
  };
332
348
 
349
+ export function physicalTextureFields(paramSchema: readonly ParamSchemaEntry[]): readonly string[] {
350
+ return standardPhysicalTextureFields(paramSchema);
351
+ }
352
+
353
+ export function appendTextureInjection(
354
+ bgl: readonly GPUBindGroupLayoutEntry[],
355
+ fields: readonly string[],
356
+ ): GPUBindGroupLayoutEntry[] {
357
+ const start = bgl.length;
358
+ const fieldSet = new Set(fields);
359
+ return standardPhysicalTextureFields(
360
+ fields.map((name) => ({ name, type: 'texture2d' as const })),
361
+ ).flatMap((field, index) => {
362
+ if (!fieldSet.has(field)) return [];
363
+ const binding = start + index * 2;
364
+ return [
365
+ {
366
+ binding,
367
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
368
+ sampler: { type: 'filtering' as const },
369
+ },
370
+ {
371
+ binding: binding + 1,
372
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
373
+ texture: { sampleType: 'float' as const, viewDimension: '2d' as const },
374
+ },
375
+ ];
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Return the stable identity of the merged material bind-group layout.
381
+ *
382
+ * This is deliberately derived from the same descriptor builder consumed by
383
+ * the device path. Callers use it to keep pipeline, bind-group, and
384
+ * frame-local lookup caches on one layout identity even when equivalent
385
+ * paramSchema arrays were allocated independently.
386
+ */
387
+ export function materialBindGroupLayoutIdentity(
388
+ shaderId: string,
389
+ paramSchema: readonly ParamSchemaEntry[],
390
+ ): string {
391
+ const descriptor = buildBindGroupLayoutDescriptor(
392
+ {
393
+ shader: { id: shaderId, passKind: 'forward', variantSet: undefined },
394
+ attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
395
+ geometry: { topology: 'triangle-list', vertexLayout: {} },
396
+ renderState: undefined,
397
+ },
398
+ { kind: 'pbr-material-merged', materialParamSchema: paramSchema },
399
+ );
400
+ // Descriptor shape alone cannot distinguish two Standard roots that expose
401
+ // the same number of physical texture pairs under different semantic names.
402
+ // The resource order is part of the admitted root contract, so include the
403
+ // canonical physical projection in the cache identity while keeping the
404
+ // actual WebGPU descriptor owned by the builder above.
405
+ return JSON.stringify({
406
+ entries: descriptor.entries,
407
+ physicalTextureFields: physicalTextureFields(paramSchema),
408
+ });
409
+ }
410
+
333
411
  /**
334
412
  * Caps shape consumed by the BGL factory functions for storage-buffer vs
335
413
  * uniform-buffer branching. Mirrors the two fields the engine reads from
@@ -568,7 +646,7 @@ export function buildPbrPipelineLayouts(
568
646
 
569
647
  // Material BGL: derived user region + Skylight + transmission injection.
570
648
  // The transmission start binding is computed from the post-Skylight BGL
571
- // length (= 24 for the built-in Standard schema) by appendInjection.
649
+ // length (= 22 for the canonical Standard schema) by appendInjection.
572
650
  const materialBglRes = device.createBindGroupLayout(
573
651
  buildBindGroupLayoutDescriptor(BGL_ONLY_SPEC_STUB, { kind: 'pbr-material-merged' }),
574
652
  );
@@ -658,6 +736,21 @@ export function buildPbrPipelineLayouts(
658
736
  export const SKIN_MATERIAL_SHADER_ID = 'forgeax::pbr-skin' as const;
659
737
  export const SHADOW_CASTER_SHADER_ID = 'forgeax::default-shadow-caster' as const;
660
738
 
739
+ // Authored Standard templates are published under a unique module id for each
740
+ // root contract. Keep the family marker in the id so every downstream render
741
+ // owner (extract, layout, and record) can make the same Standard-vs-custom and
742
+ // rigid-vs-skinned decision without a second registry or a runtime shader
743
+ // inspection pass.
744
+ const AUTHORED_STANDARD_ID_RE = /::(?:standard|pbr-skin)(?:-|$)/;
745
+
746
+ export function isStandardPbrSkinMaterialShader(shaderId: string | undefined): boolean {
747
+ return (
748
+ shaderId === SKIN_MATERIAL_SHADER_ID ||
749
+ shaderId === 'forgeax::default-standard-pbr-skin' ||
750
+ (shaderId !== undefined && /::pbr-skin(?:-|$)/.test(shaderId))
751
+ );
752
+ }
753
+
661
754
  export function shadowCasterVariantSet(storageBuffer: boolean, skinned: boolean): string {
662
755
  const skinningDisabled = !skinned;
663
756
  if (storageBuffer && skinningDisabled) return '';
@@ -692,6 +785,22 @@ export function resolvePipelineGroup2Contract(source: string): PipelineGroup2Con
692
785
 
693
786
  /** Returns true for the engine-shipped standard-PBR material shader family. */
694
787
  export function isStandardPbrMaterialShader(shaderId: string | undefined): boolean {
788
+ return (
789
+ shaderId === 'forgeax::default-standard-pbr' ||
790
+ shaderId === SKIN_MATERIAL_SHADER_ID ||
791
+ shaderId === 'forgeax::default-standard-pbr-skin' ||
792
+ (shaderId !== undefined && AUTHORED_STANDARD_ID_RE.test(shaderId))
793
+ );
794
+ }
795
+
796
+ /**
797
+ * Return true only for the two engine-owned Standard roots. Authored Standard
798
+ * aliases intentionally remain outside this set: their cooked root schema may
799
+ * omit transmission or other optional resources, so their compact user-region
800
+ * layout must be derived from that artifact instead of borrowing the boot
801
+ * layout's reserved slots.
802
+ */
803
+ export function isCanonicalStandardPbrMaterialShader(shaderId: string | undefined): boolean {
695
804
  return (
696
805
  shaderId === 'forgeax::default-standard-pbr' ||
697
806
  shaderId === SKIN_MATERIAL_SHADER_ID ||
@@ -1240,14 +1349,29 @@ export function buildBindGroupLayoutDescriptor(
1240
1349
  // + IBL injection (7) + transmission injection (2). The user-region
1241
1350
  // size is the only variable; injection start = userRegion.length so a
1242
1351
  // custom schema shifts IBL/transmission by its sampler/texture pairs.
1243
- // The built-in 8-texture standard-PBR schema is 17 + 7 + 2 = 26 entries.
1352
+ // The built-in standard-PBR schema is 17 + 7 + 2 = 26 entries;
1353
+ // clearcoat texture pairs are derived from the effective root schema.
1244
1354
  //
1245
1355
  // Schema source priority (D-1): explicit materialParamSchema option >
1246
1356
  // registry lookup of spec.shader.id > built-in standard-PBR fallback.
1247
1357
  const resolvedSchema = resolveMaterialParamSchema(spec, options);
1248
- const userRegion = buildPbrMaterialUserRegionEntries(resolvedSchema);
1358
+ const effectiveSchema = resolvedSchema ?? STANDARD_PIPELINE_PARAM_SCHEMA;
1359
+ const physicalFields = physicalTextureFields(effectiveSchema);
1360
+ // Standard's built-in shader keeps the transmission user-region slots
1361
+ // reserved even when a particular root omits those values. Physical
1362
+ // maps extend that canonical region; they must not compact the IBL
1363
+ // bindings to the smaller authored subset. A compact user region is
1364
+ // still correct for authored (non-engine) material shaders.
1365
+ const userRegionSchema = isCanonicalStandardPbrMaterialShader(spec.shader.id)
1366
+ ? STANDARD_PIPELINE_PARAM_SCHEMA
1367
+ : effectiveSchema;
1368
+ const userRegion = buildPbrMaterialUserRegionEntries(userRegionSchema);
1249
1369
  const afterIbl = [...userRegion, ...appendInjection(userRegion, 'ibl')];
1250
- const merged = [...afterIbl, ...appendInjection(afterIbl, 'transmission')];
1370
+ const afterTransmission = [...afterIbl, ...appendInjection(afterIbl, 'transmission')];
1371
+ const merged = [
1372
+ ...afterTransmission,
1373
+ ...appendTextureInjection(afterTransmission, physicalFields),
1374
+ ];
1251
1375
  return {
1252
1376
  label: 'pbr-material-skylight-bgl',
1253
1377
  entries: merged,
@@ -154,4 +154,26 @@ describe('Points/Lines expansion cache and LKG recovery', () => {
154
154
  expect(preparation.prepare(snapshot(), mesh()).ok).toBe(true);
155
155
  expect(preparation.inspect()).toMatchObject({ status: 'resident', lastKnownGood: true });
156
156
  });
157
+
158
+ it('abandons lost-device handles without invoking the dead-device destroy path', () => {
159
+ let destroys = 0;
160
+ const preparation = new PointsLinesPreparation({
161
+ cache: new PointsLinesExpansionCache(),
162
+ adapter: {
163
+ create: () => ok('lost-device-resource'),
164
+ upload: () => ok(64),
165
+ validate: () => ok(undefined),
166
+ destroy: () => {
167
+ destroys += 1;
168
+ },
169
+ },
170
+ });
171
+
172
+ expect(preparation.prepare(snapshot(), mesh()).ok).toBe(true);
173
+ preparation.abandonForDeviceLoss();
174
+ preparation.abandonForDeviceLoss();
175
+
176
+ expect(destroys).toBe(0);
177
+ expect(preparation.inspect()).toMatchObject({ status: 'rebuild-pending', liveBytes: 0 });
178
+ });
157
179
  });
@@ -37,6 +37,8 @@ export interface PointsLinesLanePreparationAdapter<Resource> {
37
37
  mesh: MeshAsset,
38
38
  ): Result<PointsLinesPreparedResource<Resource>, PointsLinesPrepareFailedError>;
39
39
  resetForDeviceLoss(): void;
40
+ /** Drop lost-device handles without calling destroy on the dead device. */
41
+ abandonForDeviceLoss(): void;
40
42
  lastKnownGood(): PointsLinesPreparedResource<Resource> | undefined;
41
43
  inspect(): PointsLinesPreparationInspection<Resource>;
42
44
  }
@@ -121,6 +123,12 @@ export class PointsLinesPreparation<Resource> {
121
123
  this.status = 'rebuild-pending';
122
124
  }
123
125
 
126
+ abandonForDeviceLoss(): void {
127
+ this.live = undefined;
128
+ this.pendingCandidateBytes = 0;
129
+ this.status = 'rebuild-pending';
130
+ }
131
+
124
132
  lastKnownGood(): PointsLinesPreparedResource<Resource> | undefined {
125
133
  return this.live;
126
134
  }
@@ -171,6 +179,7 @@ export function createPointsLinesLanePreparationAdapter<Resource>(
171
179
  contract: createPointsLinesLaneContract(lane, backend),
172
180
  prepare: (snapshot, mesh) => preparation.prepare(snapshot, mesh),
173
181
  resetForDeviceLoss: () => preparation.resetForDeviceLoss(),
182
+ abandonForDeviceLoss: () => preparation.abandonForDeviceLoss(),
174
183
  lastKnownGood: () => preparation.lastKnownGood(),
175
184
  inspect: () => preparation.inspect(),
176
185
  };
@@ -0,0 +1,403 @@
1
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
2
+ import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
3
+ import { err, ok } from '@forgeax/engine-rhi';
4
+ import type { MaterialAsset, MeshAsset } from '@forgeax/engine-types';
5
+ import { toShared } from '@forgeax/engine-types';
6
+ import { PointsLinesMaterialUnsupportedError } from '../errors/render';
7
+ import {
8
+ GPU_BUFFER_USAGE_COPY_DST,
9
+ GPU_BUFFER_USAGE_INDEX,
10
+ GPU_BUFFER_USAGE_VERTEX,
11
+ } from '../gpu-usage';
12
+ import type { ValidatedRenderable } from '../record/frame-snapshot';
13
+ import type {
14
+ PointsLinesRecordOwner,
15
+ PointsLinesRecordSubmission,
16
+ RenderSystemInternals,
17
+ } from '../record/render-context';
18
+ import type { RecoveryPointsLinesCandidate } from '../render-system';
19
+ import { admitPointsLines } from './admission';
20
+ import { PointsLinesExpansionCache } from './expansion-cache';
21
+ import type { PointsLinesInspection, PointsLinesSourceError } from './inspection';
22
+ import { inspectPointsLines } from './inspection';
23
+ import {
24
+ createPointsLinesLanePreparationAdapter,
25
+ type PointsLinesLanePreparationAdapter,
26
+ } from './prepare';
27
+ import {
28
+ createPointsLinesLaneAdapter,
29
+ type PointsLinesBackend,
30
+ type PointsLinesLane,
31
+ } from './record';
32
+ import type { PointsLinesRetainedSnapshot } from './snapshot';
33
+
34
+ type PointsLinesGpuResources = {
35
+ readonly vertexBuffer: import('@forgeax/engine-rhi').Buffer;
36
+ readonly indexBuffer: import('@forgeax/engine-rhi').Buffer;
37
+ };
38
+ type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
39
+
40
+ /**
41
+ * The Standard renderer's single Points/Lines prepare owner.
42
+ *
43
+ * The retained snapshot is the identity boundary; this owner only resolves
44
+ * the source assets, runs admission, and publishes the existing preparation
45
+ * and record contracts to the main geometry loop. The expansion cache is
46
+ * shared across lanes and this owner keeps the one prepared vertex/index
47
+ * resource pair for each retained identity.
48
+ */
49
+ export class StandardPointsLinesOwner implements PointsLinesRecordOwner {
50
+ private readonly cache = new PointsLinesExpansionCache();
51
+ private readonly preparations = new Map<string, PointsLinesPreparationAdapter>();
52
+ private readonly active = new Map<string, PointsLinesInspection>();
53
+ private readonly layoutProjection = deriveVertexLayoutProjection({
54
+ position: new Float32Array(0),
55
+ normal: new Float32Array(0),
56
+ uv: new Float32Array(0),
57
+ tangent: new Float32Array(0),
58
+ });
59
+
60
+ constructor(private readonly internals: RenderSystemInternals) {}
61
+
62
+ beginFrame(): void {
63
+ this.active.clear();
64
+ }
65
+
66
+ prepare(entry: ValidatedRenderable, clustered: boolean): PointsLinesRecordSubmission | undefined {
67
+ return this.prepareWithMaps(entry, clustered, this.internals, this.preparations, this.active);
68
+ }
69
+
70
+ prepareRecoveryCandidate(
71
+ entries: readonly ValidatedRenderable[],
72
+ clustered: boolean,
73
+ runtime: RenderSystemInternals,
74
+ ): RecoveryPointsLinesCandidate | undefined {
75
+ const stagedPreparations = new Map<string, PointsLinesPreparationAdapter>();
76
+ const stagedInspections = new Map<string, PointsLinesInspection>();
77
+ let pointEntryCount = 0;
78
+ try {
79
+ for (const entry of entries) {
80
+ const snapshot = entry.source.pointsLines;
81
+ if (snapshot?.component === undefined) continue;
82
+ pointEntryCount += 1;
83
+ const prepared = this.prepareWithMaps(
84
+ entry,
85
+ clustered,
86
+ runtime,
87
+ stagedPreparations,
88
+ stagedInspections,
89
+ );
90
+ if (prepared === undefined) {
91
+ throw new Error(
92
+ `recovery Points/Lines preparation failed for ${snapshot.worldId}:${snapshot.entityKey}`,
93
+ );
94
+ }
95
+ }
96
+ } catch (cause) {
97
+ for (const preparation of stagedPreparations.values()) {
98
+ preparation.resetForDeviceLoss();
99
+ }
100
+ throw cause;
101
+ }
102
+ if (pointEntryCount === 0) return undefined;
103
+
104
+ let released = false;
105
+ let published = false;
106
+ const candidate: RecoveryPointsLinesCandidate = {
107
+ createRecoveryRoot: (scope) => ({
108
+ kind: 'buffer',
109
+ create: () => {
110
+ if (!scope.isAlive() || released) {
111
+ throw new Error('Points/Lines recovery candidate scope is not active.');
112
+ }
113
+ return candidate;
114
+ },
115
+ cleanup: () => candidate.release(),
116
+ }),
117
+ publish: () => {
118
+ if (released || published) return;
119
+ this.preparations.clear();
120
+ for (const [key, preparation] of stagedPreparations) {
121
+ this.preparations.set(key, preparation);
122
+ }
123
+ published = true;
124
+ },
125
+ release: () => {
126
+ if (released || published) return;
127
+ released = true;
128
+ for (const preparation of stagedPreparations.values()) {
129
+ preparation.resetForDeviceLoss();
130
+ }
131
+ stagedPreparations.clear();
132
+ stagedInspections.clear();
133
+ },
134
+ };
135
+ return candidate;
136
+ }
137
+
138
+ private prepareWithMaps(
139
+ entry: ValidatedRenderable,
140
+ clustered: boolean,
141
+ runtime: RenderSystemInternals,
142
+ preparations: Map<string, PointsLinesPreparationAdapter>,
143
+ inspections: Map<string, PointsLinesInspection>,
144
+ ): PointsLinesRecordSubmission | undefined {
145
+ const snapshot = entry.source.pointsLines;
146
+ if (snapshot === undefined || snapshot.component === undefined || entry.world === undefined) {
147
+ return undefined;
148
+ }
149
+ const key = `${snapshot.worldId}:${snapshot.entityKey}`;
150
+ const topology = snapshot.component === 'Points' ? 'point-list' : 'line-list';
151
+ const backend = this.backend(runtime);
152
+ const lane = backend === 'wgpu-webgl2' ? 'cpu-webgl2' : clustered ? 'clustered' : 'direct';
153
+ const meshResult = resolveAssetHandle<MeshAsset>(
154
+ entry.world,
155
+ toShared<'MeshAsset'>(entry.source.assetHandle),
156
+ );
157
+ const materialResult = resolveAssetHandle<MaterialAsset>(
158
+ entry.world,
159
+ toShared<'MaterialAsset'>(snapshot.materialHandle),
160
+ );
161
+ if (!meshResult.ok || !materialResult.ok) {
162
+ this.publishRefusal(
163
+ inspections,
164
+ snapshot,
165
+ new PointsLinesMaterialUnsupportedError({
166
+ entity: snapshot.entityKey,
167
+ material: 'unresolved',
168
+ pass: 'forward',
169
+ module: 'asset-resolution',
170
+ reason: 'source MeshAsset or MaterialAsset could not be resolved',
171
+ }),
172
+ );
173
+ return undefined;
174
+ }
175
+ const admission = admitPointsLines({
176
+ entity: snapshot.entityKey,
177
+ mesh: meshResult.value,
178
+ material: materialResult.value,
179
+ ...(snapshot.style?.kind === 'points'
180
+ ? {
181
+ points: {
182
+ sizePx: snapshot.style.sizePx,
183
+ shape: snapshot.style.shape === 'circle' ? 1 : 0,
184
+ },
185
+ }
186
+ : { lines: { widthPx: snapshot.style?.widthPx ?? 1 } }),
187
+ });
188
+ if (!admission.ok) {
189
+ this.publishRefusal(inspections, snapshot, admission.error);
190
+ return undefined;
191
+ }
192
+ const geometryKey = this.cache.getOrCreate(snapshot, meshResult.value).key;
193
+ const prepKey = `${geometryKey}:${lane}:${backend}`;
194
+ let preparation = preparations.get(prepKey);
195
+ if (preparation === undefined) {
196
+ // A new expansion is visible cold upload work. Candidate preparation
197
+ // uses an unarmed runtime and stages the result; the published runtime
198
+ // is armed, so a missing staged resource fails before any RHI create.
199
+ runtime.recoveryColdWorkGuard?.noteUploadColdWork();
200
+ preparation = this.createPreparation(runtime, geometryKey, lane, backend);
201
+ preparations.set(prepKey, preparation);
202
+ } else if (preparation.lastKnownGood()?.geometry.key !== geometryKey) {
203
+ runtime.recoveryColdWorkGuard?.noteUploadColdWork();
204
+ }
205
+ const prepared = preparation.prepare(snapshot, meshResult.value);
206
+ if (!prepared.ok) {
207
+ const lkg = preparation.lastKnownGood();
208
+ if (lkg !== undefined) {
209
+ const lkgPlan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
210
+ lkg.snapshot,
211
+ lkg.geometry,
212
+ );
213
+ const state = preparation.inspect();
214
+ inspections.set(
215
+ key,
216
+ inspectPointsLines({
217
+ snapshot,
218
+ topology,
219
+ lane,
220
+ pointCount: lkg.geometry.pointCount,
221
+ segmentCount: lkg.geometry.segmentCount,
222
+ sourceBytes: lkg.geometry.sourceBytes,
223
+ derivedBytes: lkg.geometry.derivedBytes,
224
+ cache: { hit: true, rebuilds: state.rebuilds, evictions: 0 },
225
+ drawCount: lkgPlan.drawCount,
226
+ uploadBytes: 0,
227
+ lastKnownGood: true,
228
+ refusal: {
229
+ code: prepared.error.code,
230
+ expected: prepared.error.expected,
231
+ hint: prepared.error.hint,
232
+ detail: prepared.error.detail,
233
+ generation: prepared.error.detail.generation,
234
+ lastKnownGood: true,
235
+ },
236
+ }),
237
+ );
238
+ return {
239
+ plan: lkgPlan,
240
+ vertexBuffer: lkg.resource.vertexBuffer,
241
+ indexBuffer: lkg.resource.indexBuffer,
242
+ layoutProjection: this.layoutProjection,
243
+ };
244
+ }
245
+ this.publishRefusal(inspections, snapshot, prepared.error);
246
+ return undefined;
247
+ }
248
+ const plan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
249
+ snapshot,
250
+ prepared.value.geometry,
251
+ );
252
+ const state = preparation.inspect();
253
+ inspections.set(
254
+ key,
255
+ inspectPointsLines({
256
+ snapshot,
257
+ topology,
258
+ lane,
259
+ pointCount: prepared.value.geometry.pointCount,
260
+ segmentCount: prepared.value.geometry.segmentCount,
261
+ sourceBytes: prepared.value.geometry.sourceBytes,
262
+ derivedBytes: prepared.value.geometry.derivedBytes,
263
+ cache: {
264
+ hit: prepared.value.uploadedBytes === 0,
265
+ rebuilds: state.rebuilds,
266
+ evictions: 0,
267
+ },
268
+ drawCount: plan.drawCount,
269
+ uploadBytes: prepared.value.uploadedBytes,
270
+ lastKnownGood: prepared.value.lastKnownGood,
271
+ }),
272
+ );
273
+ return {
274
+ plan,
275
+ vertexBuffer: prepared.value.resource.vertexBuffer,
276
+ indexBuffer: prepared.value.resource.indexBuffer,
277
+ layoutProjection: this.layoutProjection,
278
+ };
279
+ }
280
+
281
+ resetForDeviceLoss(): void {
282
+ for (const preparation of this.preparations.values()) preparation.resetForDeviceLoss();
283
+ this.active.clear();
284
+ }
285
+
286
+ abandonForDeviceLoss(): void {
287
+ for (const preparation of this.preparations.values()) {
288
+ preparation.abandonForDeviceLoss();
289
+ }
290
+ this.active.clear();
291
+ }
292
+
293
+ dispose(): void {
294
+ this.resetForDeviceLoss();
295
+ this.preparations.clear();
296
+ }
297
+
298
+ inspections(): readonly PointsLinesInspection[] {
299
+ return [...this.active.values()];
300
+ }
301
+
302
+ private backend(runtime: RenderSystemInternals): PointsLinesBackend {
303
+ switch (runtime.device.caps.backendKind) {
304
+ case 'wgpu-webgl2':
305
+ return 'wgpu-webgl2';
306
+ case 'null':
307
+ return 'null';
308
+ default:
309
+ return 'webgpu';
310
+ }
311
+ }
312
+
313
+ private publishRefusal(
314
+ inspections: Map<string, PointsLinesInspection>,
315
+ snapshot: PointsLinesRetainedSnapshot,
316
+ error: PointsLinesSourceError,
317
+ ): void {
318
+ inspections.set(
319
+ `${snapshot.worldId}:${snapshot.entityKey}`,
320
+ inspectPointsLines({
321
+ snapshot,
322
+ topology: snapshot.component === 'Points' ? 'point-list' : 'line-list',
323
+ lane: 'refused',
324
+ pointCount: 0,
325
+ segmentCount: 0,
326
+ sourceBytes: 0,
327
+ derivedBytes: 0,
328
+ cache: { hit: false, rebuilds: 0, evictions: 0 },
329
+ drawCount: 0,
330
+ uploadBytes: 0,
331
+ lastKnownGood: false,
332
+ refusal: {
333
+ code: error.code,
334
+ expected: error.expected,
335
+ hint: error.hint,
336
+ detail: error.detail,
337
+ generation: snapshot.meshGeneration,
338
+ lastKnownGood: false,
339
+ },
340
+ }),
341
+ );
342
+ }
343
+
344
+ private createPreparation(
345
+ runtime: RenderSystemInternals,
346
+ geometryKey: string,
347
+ lane: PointsLinesLane,
348
+ backend: PointsLinesBackend,
349
+ ): PointsLinesPreparationAdapter {
350
+ return createPointsLinesLanePreparationAdapter(lane, backend, {
351
+ cache: this.cache,
352
+ adapter: {
353
+ create: (geometry) => {
354
+ const vertex = runtime.device.createBuffer({
355
+ label: `points-lines-vertices:${geometryKey}`,
356
+ size: Math.max(4, geometry.vertices.byteLength),
357
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
358
+ mappedAtCreation: false,
359
+ });
360
+ if (!vertex.ok) return err(vertex.error);
361
+ const index = runtime.device.createBuffer({
362
+ label: `points-lines-indices:${geometryKey}`,
363
+ size: Math.max(4, geometry.indices.byteLength),
364
+ usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
365
+ mappedAtCreation: false,
366
+ });
367
+ if (!index.ok) {
368
+ runtime.device.destroyBuffer(vertex.value);
369
+ return err(index.error);
370
+ }
371
+ return ok({ vertexBuffer: vertex.value, indexBuffer: index.value });
372
+ },
373
+ upload: (resource, geometry) => {
374
+ if (geometry.vertices.byteLength > 0) {
375
+ const vertexWrite = runtime.device.queue.writeBuffer(
376
+ resource.vertexBuffer,
377
+ 0,
378
+ geometry.vertices,
379
+ );
380
+ if (!vertexWrite.ok) return err(vertexWrite.error);
381
+ }
382
+ if (geometry.indices.byteLength > 0) {
383
+ const indexWrite = runtime.device.queue.writeBuffer(
384
+ resource.indexBuffer,
385
+ 0,
386
+ geometry.indices,
387
+ );
388
+ if (!indexWrite.ok) return err(indexWrite.error);
389
+ }
390
+ return ok(geometry.derivedBytes);
391
+ },
392
+ validate: (_resource, geometry) =>
393
+ geometry.expandedVertexCount > 0 && geometry.expandedIndexCount > 0
394
+ ? ok(undefined)
395
+ : err(new Error('Points/Lines expansion contains no drawable triangles')),
396
+ destroy: (resource) => {
397
+ runtime.device.destroyBuffer(resource.vertexBuffer);
398
+ runtime.device.destroyBuffer(resource.indexBuffer);
399
+ },
400
+ },
401
+ });
402
+ }
403
+ }