@forgeax/engine-assets-runtime 0.1.3 → 0.1.6

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 (313) hide show
  1. package/README.md +442 -175
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  13. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  21. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  23. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  25. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  27. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  29. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  31. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  33. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  37. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  39. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  41. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  43. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  45. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  46. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  47. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  48. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  49. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  50. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  51. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  52. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  53. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  54. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  55. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  56. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  57. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  58. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  59. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  60. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  61. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  62. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  63. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  64. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  65. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  66. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  67. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  68. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  69. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  70. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  71. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  72. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  73. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  74. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  75. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  76. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  77. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  78. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  79. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  80. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  81. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  82. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  83. package/dist/__tests__/plugin.test.d.ts +2 -0
  84. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  85. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  86. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  87. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  88. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  89. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  90. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  91. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  92. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  93. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  94. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  95. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  96. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  97. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  98. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  99. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  100. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  101. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  102. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  103. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  104. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  105. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  106. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  107. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  108. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  109. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  110. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  111. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  112. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  113. package/dist/aabb.d.ts +3 -0
  114. package/dist/aabb.d.ts.map +1 -0
  115. package/dist/asset-registry.d.ts +552 -0
  116. package/dist/asset-registry.d.ts.map +1 -0
  117. package/dist/builtin-asset-registry.d.ts +22 -0
  118. package/dist/builtin-asset-registry.d.ts.map +1 -0
  119. package/dist/catalog-source.d.ts +2 -1
  120. package/dist/catalog-source.d.ts.map +1 -1
  121. package/dist/decode-image-bytes.d.ts +57 -0
  122. package/dist/decode-image-bytes.d.ts.map +1 -0
  123. package/dist/dynamic-texture-store.d.ts +89 -0
  124. package/dist/dynamic-texture-store.d.ts.map +1 -0
  125. package/dist/errors/asset.d.ts +201 -0
  126. package/dist/errors/asset.d.ts.map +1 -0
  127. package/dist/handles.d.ts +141 -0
  128. package/dist/handles.d.ts.map +1 -0
  129. package/dist/image-error.d.ts +6 -0
  130. package/dist/image-error.d.ts.map +1 -0
  131. package/dist/index.d.ts +32 -3
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.mjs +7218 -114
  134. package/dist/index.mjs.map +1 -1
  135. package/dist/internal.d.ts +2 -1
  136. package/dist/internal.d.ts.map +1 -1
  137. package/dist/internal.mjs +317 -3
  138. package/dist/internal.mjs.map +1 -1
  139. package/dist/loader-registry.d.ts +63 -0
  140. package/dist/loader-registry.d.ts.map +1 -0
  141. package/dist/loaders/inline-pack.d.ts +72 -0
  142. package/dist/loaders/inline-pack.d.ts.map +1 -0
  143. package/dist/loaders/mesh-bin.d.ts +21 -0
  144. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  145. package/dist/loaders/pack-artifact.d.ts +20 -0
  146. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  147. package/dist/material/dependency-graph.d.ts +9 -0
  148. package/dist/material/dependency-graph.d.ts.map +1 -0
  149. package/dist/material/generation-cache.d.ts +24 -0
  150. package/dist/material/generation-cache.d.ts.map +1 -0
  151. package/dist/material/inspection.d.ts +24 -0
  152. package/dist/material/inspection.d.ts.map +1 -0
  153. package/dist/material/loader.d.ts +59 -0
  154. package/dist/material/loader.d.ts.map +1 -0
  155. package/dist/material/runtime-shader.d.ts +14 -0
  156. package/dist/material/runtime-shader.d.ts.map +1 -0
  157. package/dist/mesh-bin.d.ts +3 -0
  158. package/dist/mesh-bin.d.ts.map +1 -0
  159. package/dist/mipmap-generator.d.ts +132 -0
  160. package/dist/mipmap-generator.d.ts.map +1 -0
  161. package/dist/payload-validate.d.ts +60 -0
  162. package/dist/payload-validate.d.ts.map +1 -0
  163. package/dist/plugin.d.ts +16 -0
  164. package/dist/plugin.d.ts.map +1 -0
  165. package/dist/registry/artifact-io.d.ts +16 -0
  166. package/dist/registry/artifact-io.d.ts.map +1 -0
  167. package/dist/registry/asset-evidence.d.ts +13 -0
  168. package/dist/registry/asset-evidence.d.ts.map +1 -0
  169. package/dist/registry/catalog-state.d.ts +47 -0
  170. package/dist/registry/catalog-state.d.ts.map +1 -0
  171. package/dist/registry/catalog.d.ts +48 -0
  172. package/dist/registry/catalog.d.ts.map +1 -0
  173. package/dist/registry/instantiate.d.ts +153 -0
  174. package/dist/registry/instantiate.d.ts.map +1 -0
  175. package/dist/registry/load-by-guid.d.ts +247 -0
  176. package/dist/registry/load-by-guid.d.ts.map +1 -0
  177. package/dist/registry/load-state.d.ts +26 -0
  178. package/dist/registry/load-state.d.ts.map +1 -0
  179. package/dist/registry/runtime-projection.d.ts +16 -0
  180. package/dist/registry/runtime-projection.d.ts.map +1 -0
  181. package/dist/registry/scene-publication-fence.d.ts +38 -0
  182. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  183. package/dist/registry/validate-material.d.ts +77 -0
  184. package/dist/registry/validate-material.d.ts.map +1 -0
  185. package/dist/resolve-asset-handle.d.ts +43 -0
  186. package/dist/resolve-asset-handle.d.ts.map +1 -0
  187. package/dist/resolve-tileset-runtime.d.ts +23 -0
  188. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  189. package/dist/scene-handle-fields.d.ts +88 -0
  190. package/dist/scene-handle-fields.d.ts.map +1 -0
  191. package/dist/scene-payload.d.ts +15 -0
  192. package/dist/scene-payload.d.ts.map +1 -0
  193. package/dist/wire-default-loaders.d.ts +29 -0
  194. package/dist/wire-default-loaders.d.ts.map +1 -0
  195. package/package.json +22 -9
  196. package/src/__tests__/aabb.unit.test.ts +97 -0
  197. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  198. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  199. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  200. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  201. package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
  202. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  203. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  204. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  205. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  206. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  207. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  208. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  209. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  210. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  211. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  212. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  213. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  214. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  215. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  216. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  217. package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
  218. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  219. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  220. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  221. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  222. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  223. package/src/__tests__/instantiate.unit.test.ts +684 -0
  224. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  225. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  226. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  227. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  228. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  229. package/src/__tests__/material-ready.integration.test.ts +492 -0
  230. package/src/__tests__/material-stale-generation.unit.test.ts +152 -0
  231. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  232. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  233. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  234. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  235. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  236. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  237. package/src/__tests__/plugin.test.ts +55 -0
  238. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  239. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  240. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  241. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  242. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  243. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  244. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  245. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  246. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  247. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  248. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +16 -0
  249. package/src/__tests__/static-rig.integration.test.ts +8 -0
  250. package/src/__tests__/validate-material.unit.test.ts +359 -0
  251. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  252. package/src/aabb.ts +107 -0
  253. package/src/asset-registry.ts +2040 -0
  254. package/src/builtin-asset-registry.ts +164 -0
  255. package/src/catalog-source.ts +14 -70
  256. package/src/decode-image-bytes.ts +126 -0
  257. package/src/dynamic-texture-store.ts +234 -0
  258. package/src/errors/asset.ts +347 -0
  259. package/src/handles.ts +180 -0
  260. package/src/image-error.ts +45 -0
  261. package/src/index.ts +177 -19
  262. package/src/internal.ts +2 -0
  263. package/src/loader-registry.ts +157 -0
  264. package/src/loaders/inline-pack.ts +837 -0
  265. package/src/loaders/mesh-bin.ts +304 -0
  266. package/src/loaders/pack-artifact.ts +665 -0
  267. package/src/material/dependency-graph.ts +46 -0
  268. package/src/material/generation-cache.ts +177 -0
  269. package/src/material/inspection.ts +46 -0
  270. package/src/material/loader.ts +326 -0
  271. package/src/material/runtime-shader.ts +133 -0
  272. package/src/mesh-bin.ts +5 -0
  273. package/src/mipmap-generator.ts +450 -0
  274. package/src/payload-validate.ts +573 -0
  275. package/src/plugin.ts +48 -0
  276. package/src/registry/artifact-io.ts +259 -0
  277. package/src/registry/asset-evidence.ts +51 -0
  278. package/src/registry/catalog-state.ts +409 -0
  279. package/src/registry/catalog.ts +369 -0
  280. package/src/registry/instantiate.ts +1056 -0
  281. package/src/registry/load-by-guid.ts +1657 -0
  282. package/src/registry/load-state.ts +115 -0
  283. package/src/registry/runtime-projection.ts +63 -0
  284. package/src/registry/scene-publication-fence.ts +256 -0
  285. package/src/registry/validate-material.ts +386 -0
  286. package/src/resolve-asset-handle.ts +147 -0
  287. package/src/resolve-tileset-runtime.ts +72 -0
  288. package/src/scene-handle-fields.ts +221 -0
  289. package/src/scene-payload.ts +207 -0
  290. package/src/wire-default-loaders.ts +79 -0
  291. package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
  292. package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
  293. package/dist/__tests__/asset-kind.test.d.ts +0 -2
  294. package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
  295. package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
  296. package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
  297. package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
  298. package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
  299. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
  300. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
  301. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
  302. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
  303. package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
  304. package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
  305. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
  306. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
  307. package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
  308. package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
  309. package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
  310. package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
  311. package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
  312. package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
  313. package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
@@ -0,0 +1,164 @@
1
+ // ─── BuiltinAssetRegistry (D-15 process-static tier) ────────────────────────
2
+ //
3
+ // D-15 two-tier asset resolution. Builtin payloads (the 5 procedural meshes
4
+ // the engine ships with) are PROCESS-STATIC: a frozen const keyed by a fixed
5
+ // slot u32 in [1, BUILTIN_BASE). They never enter a World's SharedRefStore,
6
+ // never participate in reference counting, and resolve without any World
7
+ // reference — so a builtin handle (HANDLE_CUBE etc.) is transparent across
8
+ // every World and every renderer.
9
+ //
10
+ // R-13 (module init-order): all five payload consts are constructed
11
+ // synchronously at module load, are module-level, and have no side effect.
12
+ // Importing this module then calling resolve() observes fully-constructed
13
+ // frozen payloads — there is no lazy getter and no circular-dependency
14
+ // init hazard.
15
+ //
16
+ // The companion AssetRegistry (asset-registry.ts) retreats to a thin
17
+ // guid<->handle index; user-tier payloads live in World.sharedRefs at
18
+ // slots >= BUILTIN_BASE. AssetRegistry.get(handle) dispatches by slot range:
19
+ // slot < BUILTIN_BASE -> BuiltinAssetRegistry.resolve, else world.sharedRefs.
20
+
21
+ import {
22
+ createBoxGeometry,
23
+ createCylinderGeometry,
24
+ createPlaneGeometry,
25
+ createSphereGeometry,
26
+ meshFromInterleaved,
27
+ } from '@forgeax/engine-geometry';
28
+ import type { Handle, MeshAsset as TypesMeshAsset } from '@forgeax/engine-types';
29
+ import { BUILTIN_BASE, handleSlot } from '@forgeax/engine-types';
30
+
31
+ // D-16: BUILTIN_BASE is the slot boundary [1, BUILTIN_BASE) builtin /
32
+ // [BUILTIN_BASE, +inf) user-tier. It lives in @forgeax/engine-types (the
33
+ // single shared dependency of ecs + runtime) to avoid a cross-package cycle;
34
+ // re-exported from the runtime barrel here for AI-user discoverability.
35
+ export { BUILTIN_BASE };
36
+
37
+ // ─── Builtin geometry data (12F: position + normal + uv + tangent) ──────────
38
+ // bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu: every
39
+ // builtin geometry payload keeps the local-space AABB produced by its geometry
40
+ // factory. Builtins are ordinary MeshAssets at the render boundary, so removing
41
+ // their bounds silently turns them into always-visible renderables and defeats
42
+ // the same frustum-culling path used by imported meshes.
43
+
44
+ // BUILTIN_CUBE is synthesized from `createBoxGeometry(1, 1, 1)` so the cube
45
+ // inherits Three.js-aligned per-face UV unwrap and per-vertex tangent vec4.
46
+ // The procedural factory always returns Result.ok for valid (>0) extents,
47
+ // hence the unwrap-with-throw is safe at module init.
48
+ const builtinCubeRes = createBoxGeometry(1, 1, 1);
49
+ if (!builtinCubeRes.ok) {
50
+ throw new Error(
51
+ `[builtin-asset-registry] createBoxGeometry(1,1,1) failed: ${builtinCubeRes.error.code}`,
52
+ );
53
+ }
54
+ export const BUILTIN_CUBE: TypesMeshAsset = Object.freeze(builtinCubeRes.value);
55
+
56
+ // BUILTIN_TRIANGLE: 3 vertices in the XY plane facing +Z, with a [0..1]² UV
57
+ // triangle so a textured triangle samples the texture (apex = top-centre,
58
+ // base = bottom-left / bottom-right). meshFromInterleaved expands the
59
+ // 8-floats interleaved input (pos + normal + uv) to the runtime 12-floats
60
+ // stride (adds tangent vec4 per `geometry/tangent.ts` path A).
61
+ const builtinTriangleRes = meshFromInterleaved(
62
+ new Float32Array([
63
+ // pos.xyz normal.xyz uv.xy
64
+ 0, 0.7, 0, 0, 0, 1, 0.5, 1, -0.7, -0.6, 0, 0, 0, 1, 0, 0, 0.7, -0.6, 0, 0, 0, 1, 1, 0,
65
+ ]),
66
+ new Uint16Array([0, 1, 2]),
67
+ );
68
+ if (!builtinTriangleRes.ok) {
69
+ throw new Error(
70
+ `[builtin-asset-registry] builtin triangle geometry failed: ${builtinTriangleRes.error.code}`,
71
+ );
72
+ }
73
+ export const BUILTIN_TRIANGLE: TypesMeshAsset = Object.freeze(builtinTriangleRes.value);
74
+
75
+ // BUILTIN_QUAD: unit-size plane on XY facing +Z (4 vertices, 2 triangles,
76
+ // 6 indices). Synthesised from `createPlaneGeometry(1, 1)` (which itself
77
+ // chains through `meshFromInterleaved`) so the resulting MeshAsset is
78
+ // byte-identical to procedural plane output — zero new layout
79
+ // discriminator, AI users reason about UV / pivot semantics by reading
80
+ // `packages/runtime/src/geometry/plane.ts` (charter P4 consistent
81
+ // abstraction; feat-20260520 M-1 / w06).
82
+ // bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu: this
83
+ // payload follows the same local-AABB contract as every other builtin.
84
+ const builtinQuadRes = createPlaneGeometry(1, 1);
85
+ if (!builtinQuadRes.ok) {
86
+ throw new Error(
87
+ `[builtin-asset-registry] createPlaneGeometry(1,1) failed: ${builtinQuadRes.error.code}`,
88
+ );
89
+ }
90
+ export const BUILTIN_QUAD: TypesMeshAsset = Object.freeze(builtinQuadRes.value);
91
+
92
+ // BUILTIN_SPHERE: UV-sphere synthesised from createSphereGeometry(1, 16, 12).
93
+ // Vertices are at exact radius-1 positions (sphere.ts:40-45) so the
94
+ // |hypot(pos)-1| < 1e-6 radius invariant holds by construction. Index
95
+ // buffer is Uint32Array — downstream consumers (step-3 upload loop,
96
+ // createRenderer.ts) auto-select 'uint32' indexFormat via
97
+ // `instanceof Uint32Array`.
98
+ const builtinSphereRes = createSphereGeometry(1, 16, 12);
99
+ if (!builtinSphereRes.ok) {
100
+ throw new Error(
101
+ `[builtin-asset-registry] createSphereGeometry(1,16,12) failed: ${builtinSphereRes.error.code}`,
102
+ );
103
+ }
104
+ export const BUILTIN_SPHERE: TypesMeshAsset = Object.freeze(builtinSphereRes.value);
105
+
106
+ // BUILTIN_CYLINDER: procedural cylinder synthesised from createCylinderGeometry(0.5, 0.5, 1, 16, 1).
107
+ // Radius-top=radius-bottom=0.5, height=1, 16 radial segments, 1 height segment — a
108
+ // unit-height open cylinder (no caps). meshFromInterleaved expands the 8-floats
109
+ // interleaved input (pos + normal + uv) to the runtime 12-floats stride (adds
110
+ // tangent vec4 per geometry/tangent.ts path A).
111
+ // feat-20260701-editor-world-container-doc-ecs-collapse M0 / AC-16:
112
+ // handle=6, GUID = deriveBuiltin('HANDLE_CYLINDER') UUIDv5
113
+ // (plan-strategy §2 D-6 + §5.6 builtin-guid-ssot gate)
114
+ const builtinCylinderRes = createCylinderGeometry(0.5, 0.5, 1, 16, 1);
115
+ if (!builtinCylinderRes.ok) {
116
+ throw new Error(
117
+ `[builtin-asset-registry] createCylinderGeometry(0.5,0.5,1,16,1) failed: ${builtinCylinderRes.error.code}`,
118
+ );
119
+ }
120
+ export const BUILTIN_CYLINDER: TypesMeshAsset = Object.freeze(builtinCylinderRes.value);
121
+
122
+ // BUILTIN_NINESLICE_QUAD: 4×4 grid plane synthesised from
123
+ // createPlaneGeometry(1, 1, 3, 3) — 16 vertices, 9 sub-quads × 6 indices = 54.
124
+ // Reuses the unit-quad vertex layout (12F: pos + normal + uv + tangent) so
125
+ // HANDLE_NINESLICE_QUAD funnels through the same sprite-pipeline binding
126
+ // table as HANDLE_QUAD; only the vertex_index → (i, j) decomposition in
127
+ // sprite.wgsl branches on slices presence (feat-20260527-sprite-nineslice
128
+ // M3, plan-strategy §D-2 + §D-4).
129
+ const builtinNineSliceQuadRes = createPlaneGeometry(1, 1, 3, 3);
130
+ if (!builtinNineSliceQuadRes.ok) {
131
+ throw new Error(
132
+ `[builtin-asset-registry] createPlaneGeometry(1,1,3,3) failed: ${builtinNineSliceQuadRes.error.code}`,
133
+ );
134
+ }
135
+ export const BUILTIN_NINESLICE_QUAD: TypesMeshAsset = Object.freeze(builtinNineSliceQuadRes.value);
136
+
137
+ // Slot u32 -> frozen payload. Slot ids 1..5 are the fixed builtin handle
138
+ // values (HANDLE_CUBE=1 .. HANDLE_NINESLICE_QUAD=5) defined in asset-registry.
139
+ const BUILTIN_BY_SLOT: ReadonlyMap<number, TypesMeshAsset> = new Map([
140
+ [1, BUILTIN_CUBE],
141
+ [2, BUILTIN_TRIANGLE],
142
+ [3, BUILTIN_QUAD],
143
+ [4, BUILTIN_SPHERE],
144
+ [5, BUILTIN_NINESLICE_QUAD],
145
+ [6, BUILTIN_CYLINDER],
146
+ ]);
147
+
148
+ /**
149
+ * Process-static builtin asset resolver (D-15). `resolve(handle)` returns the
150
+ * frozen builtin payload when `slot < BUILTIN_BASE`, else `null` (the slot
151
+ * belongs to the user tier — resolve it through `world.sharedRefs.resolve`).
152
+ *
153
+ * Read-only: there is no register / mint surface. The five builtin payloads
154
+ * are fixed at module load. AI users `import { BuiltinAssetRegistry } from
155
+ * '@forgeax/engine-runtime'` and resolve a builtin handle without a World.
156
+ */
157
+ export const BuiltinAssetRegistry = Object.freeze({
158
+ resolve<T extends TypesMeshAsset>(handle: Handle<string, 'shared'>): T | null {
159
+ const slot = handleSlot(handle);
160
+ if (slot >= BUILTIN_BASE) return null;
161
+ const payload = BUILTIN_BY_SLOT.get(slot);
162
+ return payload === undefined ? null : (payload as T);
163
+ },
164
+ });
@@ -1,14 +1,14 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-rhi';
1
2
  import {
2
3
  ASSET_ERROR_HINTS,
3
4
  AssetError,
4
5
  type CatalogDelta,
5
6
  type CatalogEntry,
6
- err,
7
- ok,
8
7
  type ResourceRevision,
9
- type Result,
10
8
  type RuntimeAssetBinding,
11
9
  } from '@forgeax/engine-types';
10
+ import { fetchCatalog } from './registry/catalog';
11
+
12
12
  export type CatalogListener = (delta: CatalogDelta) => void;
13
13
 
14
14
  /** Read-only catalog source backed by static entries or a canonical URL. */
@@ -69,33 +69,17 @@ export function createCatalogSource(options: {
69
69
  }),
70
70
  );
71
71
  }
72
- const response = await (options.fetch ?? globalThis.fetch)(options.url);
73
- if (!response.ok) {
74
- return err(
75
- new AssetError({
76
- code: 'asset-fetch-failed',
77
- expected: 'the configured Catalog URL to return HTTP 200',
78
- hint: 'verify the producer Catalog URL and republish the current snapshot',
79
- detail: { sourcePath: options.url },
80
- }),
81
- );
82
- }
83
- let raw: unknown;
84
- try {
85
- raw = await response.json();
86
- } catch {
87
- return err(
88
- new AssetError({
89
- code: 'asset-parse-failed',
90
- expected: 'the configured Catalog URL to return JSON',
91
- hint: 'repair the producer Catalog before loading the current publication',
92
- }),
93
- );
94
- }
95
- const rows = scopedRows(raw, options.expectedScope);
96
- if (!rows.ok) return rows;
97
- const result = rows.value.map((row) => ({ ...row }));
98
- return ok(result);
72
+ const result = await fetchCatalog(
73
+ options.url,
74
+ options.fetch ?? globalThis.fetch,
75
+ undefined,
76
+ options.expectedRevision,
77
+ options.expectedScope,
78
+ );
79
+ if (!result.ok) return result;
80
+ return ok(
81
+ [...result.value].map(([guid, entry]) => ({ guid, ...entry })) as readonly CatalogEntry[],
82
+ );
99
83
  },
100
84
  subscribe(listener) {
101
85
  return options.subscribe?.(listener) ?? (() => {});
@@ -103,43 +87,3 @@ export function createCatalogSource(options: {
103
87
  ...(options.expectedScope === undefined ? {} : { expectedScope: options.expectedScope }),
104
88
  };
105
89
  }
106
-
107
- function scopedRows(
108
- raw: unknown,
109
- expectedScope: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'> | undefined,
110
- ): Result<readonly CatalogEntry[], AssetError> {
111
- if (Array.isArray(raw)) return ok(raw as readonly CatalogEntry[]);
112
- if (raw === null || typeof raw !== 'object') return parseRowsError('Catalog JSON rows');
113
- const snapshot = raw as Record<string, unknown>;
114
- if (!Array.isArray(snapshot.entries)) return parseRowsError('Catalog JSON entries');
115
- if (expectedScope !== undefined) {
116
- if (
117
- snapshot.scopeId !== expectedScope.scopeId ||
118
- snapshot.generation !== expectedScope.generation ||
119
- snapshot.authority !== 'authoritative'
120
- ) {
121
- return err(
122
- new AssetError({
123
- code: 'asset-parse-failed',
124
- expected: 'the Catalog snapshot to match the active scope and generation',
125
- hint: 'restore the authoritative producer Catalog for the active runtime scope',
126
- detail: {
127
- expectedScope,
128
- actualScope: { scopeId: snapshot.scopeId, generation: snapshot.generation },
129
- } as never,
130
- }),
131
- );
132
- }
133
- }
134
- return ok(snapshot.entries as readonly CatalogEntry[]);
135
- }
136
-
137
- function parseRowsError(expected: string): Result<never, AssetError> {
138
- return err(
139
- new AssetError({
140
- code: 'asset-parse-failed',
141
- expected,
142
- hint: 'repair the producer Catalog before loading the current publication',
143
- }),
144
- );
145
- }
@@ -0,0 +1,126 @@
1
+ // @forgeax/engine-assets-runtime -- runtime image byte decoder
2
+ // (tweak-20260714-runtime-image-bytes-decoder-add-decodeimagebytes M2 / m2-1).
3
+ //
4
+ // M1 established the signature + TSDoc contract + red test skeleton. M2
5
+ // fills the body:
6
+ // 1. mime whitelist pre-check (PNG / JPEG); non-match -> structured
7
+ // `image-format-unsupported` err (charter P3 explicit failure).
8
+ // 2. Delegate to `@forgeax/engine-image::decodeImageInBrowser` (SSOT
9
+ // browser-safe decoder; charter architecture principle 1 SSOT).
10
+ // 3. Map `DecodedImage` -> `TextureAsset` POD; `format` derives from
11
+ // `opts.colorSpace` (srgb -> 'rgba8unorm-srgb' / linear ->
12
+ // 'rgba8unorm', mirrors `image-importer.ts` colorSpaceToFormat); when
13
+ // `opts.mipmap !== false`, `mipLevelCount` derives from
14
+ // `numMipLevels({width,height})` (charter Derive-Don't-Duplicate).
15
+ //
16
+ // decode-image-bytes.ts is the SINGLE assets-runtime file allowed to
17
+ // statically import @forgeax/engine-image (charter D-2; the
18
+ // scripts/check-image-pipeline-isolation.mjs Path (a.2-anti) whitelist
19
+ // row anchors this exact path). The gate's (a.1) forbidden-symbol regex
20
+ // `\basync\s+function\s+decodeImage\b` does NOT match `decodeImageBytes`
21
+ // because `\b` between `e` and `B` fails (both are word chars) --
22
+ // verified at M2 by running the gate locally.
23
+ import { decodeImageInBrowser } from '@forgeax/engine-image';
24
+ import type { ImageError, Result, TextureAsset } from '@forgeax/engine-types';
25
+ import { err, ok } from '@forgeax/engine-types';
26
+ import { makeImageError } from './image-error';
27
+ import { numMipLevels } from './mipmap-generator';
28
+
29
+ /** v1 mime whitelist -- PNG / JPEG only (charter P1 progressive disclosure). */
30
+ const SUPPORTED_MIMES = ['image/png', 'image/jpeg'] as const;
31
+ type SupportedMime = (typeof SUPPORTED_MIMES)[number];
32
+
33
+ function isSupportedMime(mime: string): mime is SupportedMime {
34
+ return SUPPORTED_MIMES.some((supportedMime) => supportedMime === mime);
35
+ }
36
+
37
+ /**
38
+ * Decode a PNG / JPEG byte stream into a `TextureAsset` POD.
39
+ *
40
+ * Runtime SDK entry for AI users who hold image bytes (fetched over the
41
+ * network, embedded as base64, produced by an out-of-tree decoder, etc.) and
42
+ * want to feed them into `world.allocSharedRef('TextureAsset', pod)` +
43
+ * `GpuResourceStore.ensureResident` without the disk-side importer / build
44
+ * pipeline in the loop (charter P1 progressive disclosure: one-step "bytes
45
+ * in, POD out").
46
+ *
47
+ * v1 supports the two universally-decodable mime types: `image/png` and
48
+ * `image/jpeg`. Failure surfaces are all structured via the closed
49
+ * `ImageError` union (`.code` / `.expected` / `.hint` / `.detail`); the
50
+ * function never throws for expected failures (AGENTS.md Error model +
51
+ * charter P3 explicit failure).
52
+ *
53
+ * Error codes possible (subset of the closed `ImageErrorCode` union):
54
+ * - `image-format-unsupported` -- mime not in the v1 whitelist (`detail.actualMime`)
55
+ * - `image-decode-failed` -- decoder rejected the bytes, or the environment
56
+ * lacks `createImageBitmap` (`detail.reason`)
57
+ *
58
+ * @param bytes - Encoded image byte stream (`Uint8Array` or `ArrayBuffer`).
59
+ * @param mime - Byte-stream mime type. v1 whitelist: `'image/png' | 'image/jpeg'`.
60
+ * @param opts - Optional decode overrides:
61
+ * - `colorSpace`: `'srgb'` (default) or `'linear'`. Derives POD `format`:
62
+ * `srgb -> 'rgba8unorm-srgb'`, `linear -> 'rgba8unorm'`.
63
+ * - `mipmap`: `true` (default) or `false`. When true, `mipLevelCount` is
64
+ * computed via `numMipLevels({ width, height })`; when false, forced 1.
65
+ * @returns `ok(TextureAsset)` on success; `err(ImageError)` on failure.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { decodeImageBytes } from '@forgeax/engine-assets-runtime';
70
+ *
71
+ * const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
72
+ * const result = await decodeImageBytes(bytes, 'image/png');
73
+ * if (!result.ok) {
74
+ * // The ImageError code and detail are one correlated discriminated union.
75
+ * switch (result.error.code) {
76
+ * case 'image-format-unsupported':
77
+ * console.error('bad mime:', result.error.detail.actualMime);
78
+ * break;
79
+ * case 'image-decode-failed':
80
+ * console.error('decode reason:', result.error.detail.reason);
81
+ * break;
82
+ * }
83
+ * return;
84
+ * }
85
+ * const handle = world.allocSharedRef('TextureAsset', result.value);
86
+ * ```
87
+ */
88
+ export async function decodeImageBytes(
89
+ bytes: Uint8Array | ArrayBuffer,
90
+ mime: string,
91
+ opts: { colorSpace?: 'srgb' | 'linear'; mipmap?: boolean } = {},
92
+ ): Promise<Result<TextureAsset, ImageError>> {
93
+ if (!isSupportedMime(mime)) {
94
+ return err(
95
+ makeImageError({
96
+ code: 'image-format-unsupported',
97
+ actualMime: mime,
98
+ }),
99
+ );
100
+ }
101
+
102
+ const colorSpace = opts.colorSpace ?? 'srgb';
103
+ const mipmap = opts.mipmap ?? true;
104
+
105
+ const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
106
+ const decoded = await decodeImageInBrowser(u8, mime, { colorSpace, mipmap });
107
+ if (!decoded.ok) {
108
+ return err(decoded.error);
109
+ }
110
+
111
+ const dec = decoded.value;
112
+ const format: GPUTextureFormat = colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm';
113
+ const mipLevelCount = mipmap ? numMipLevels({ width: dec.width, height: dec.height }) : 1;
114
+
115
+ const pod: TextureAsset = {
116
+ kind: 'texture',
117
+ width: dec.width,
118
+ height: dec.height,
119
+ format,
120
+ data: dec.bytes,
121
+ colorSpace,
122
+ mipmap,
123
+ mipLevelCount,
124
+ };
125
+ return ok(pod);
126
+ }
@@ -0,0 +1,234 @@
1
+ // @forgeax/engine-assets-runtime — DynamicTextureStore: transient per-frame GPU
2
+ // textures for video sources (feat-20260623-world-space-video-asset M4 / w15).
3
+ //
4
+ // D-3: video frames are re-uploaded every frame — the opposite of the static
5
+ // "upload once / cache forever" semantics GpuResourceStore.ensureResident
6
+ // implements. Routing video through ensureResident would either poison its
7
+ // permanent cache (AC-08) or crash it (its `switch (pod.kind)` has no `video`
8
+ // arm). So video gets its OWN store with a transient lifecycle, exactly the way
9
+ // cube-texture upload is an eager independent path that never enters the
10
+ // ensureResident switch (research Finding 7 precedent).
11
+ //
12
+ // This store does NOT import GpuResourceStore and accepts NO static texture
13
+ // handle — it is keyed solely by the VideoAsset clip handle. Each key owns a
14
+ // single GPUTexture sized to the source video; the texture is recreated only
15
+ // when the source dimensions change (a steady-size clip allocates once and is
16
+ // re-written in place every frame via copyExternalImageToTexture). The view is
17
+ // re-fetched after each upload so the bind group always samples the latest
18
+ // frame.
19
+ //
20
+ // Charter P3: every failure returns a structured Result (never throws for an
21
+ // expected GPU failure); the per-frame caller (record stage videoTextureView)
22
+ // fires the RhiError on the engine error channel and falls back to the default
23
+ // view for that frame.
24
+
25
+ import type { Result, RhiDevice, RhiError, Texture, TextureView } from '@forgeax/engine-rhi';
26
+ import { ok } from '@forgeax/engine-rhi';
27
+ import type { Handle } from '@forgeax/engine-types';
28
+ import { handleSlot } from '@forgeax/engine-types';
29
+
30
+ /**
31
+ * The host-owned external image the engine copies a video frame from
32
+ * (HTMLVideoElement / VideoFrame / ImageBitmap / canvas). Mirrors the `source`
33
+ * member of the WebGPU `GPUCopyExternalImageSourceInfo` the RHI
34
+ * `copyExternalImageToTexture` consumes; aliased here so the store does not pull
35
+ * the whole webgpu descriptor type into its surface.
36
+ */
37
+ export type CopyExternalImageSource = GPUCopyExternalImageSourceInfo['source'];
38
+
39
+ /**
40
+ * GPUTextureUsage bits a video destination texture needs: COPY_DST (the
41
+ * copyExternalImageToTexture write target), TEXTURE_BINDING (sampled in the
42
+ * material bind group), and RENDER_ATTACHMENT (required by the WebGPU spec for
43
+ * a copyExternalImageToTexture destination). Mirrors the literals
44
+ * gpu-resource-store.ts uses for its upload textures (no shared const exists to
45
+ * import without coupling the two stores — D-3 keeps them independent).
46
+ */
47
+ const VIDEO_TEXTURE_USAGE = 0x2 | 0x4 | 0x10;
48
+
49
+ /** rgba8unorm-srgb: a video frame decodes to sRGB; sampling returns linear. */
50
+ const VIDEO_TEXTURE_FORMAT = 'rgba8unorm-srgb' as const;
51
+
52
+ /**
53
+ * The minimal RHI device surface DynamicTextureStore needs. Declared structurally
54
+ * (not the full RhiDevice) so unit tests drive it with a small mock and the store
55
+ * stays decoupled from the rest of the device surface (Pipeline Isolation).
56
+ */
57
+ export interface DynamicTextureDevice {
58
+ createTexture(desc: {
59
+ readonly size: {
60
+ readonly width: number;
61
+ readonly height: number;
62
+ readonly depthOrArrayLayers: number;
63
+ };
64
+ readonly format: typeof VIDEO_TEXTURE_FORMAT;
65
+ readonly usage: number;
66
+ readonly label?: string;
67
+ }): Result<Texture, RhiError>;
68
+ createTextureView(texture: Texture, desc: Record<string, never>): Result<TextureView, RhiError>;
69
+ destroyTexture(texture: Texture): Result<void, RhiError>;
70
+ readonly queue: {
71
+ copyExternalImageToTexture(
72
+ source: { readonly source: CopyExternalImageSource; readonly flipY?: boolean },
73
+ destination: { readonly texture: Texture },
74
+ copySize: {
75
+ readonly width: number;
76
+ readonly height: number;
77
+ readonly depthOrArrayLayers: number;
78
+ },
79
+ ): Result<void, RhiError>;
80
+ };
81
+ }
82
+
83
+ /** Adapt the opaque RHI device to the store's intentionally small upload seam. */
84
+ export function adaptDynamicTextureDevice(device: RhiDevice): DynamicTextureDevice {
85
+ return {
86
+ createTexture: (descriptor) =>
87
+ device.createTexture({
88
+ ...descriptor,
89
+ mipLevelCount: undefined,
90
+ sampleCount: undefined,
91
+ dimension: undefined,
92
+ viewFormats: undefined,
93
+ textureBindingViewDimension: undefined,
94
+ }),
95
+ createTextureView: (texture) =>
96
+ device.createTextureView(texture, {
97
+ label: undefined,
98
+ format: undefined,
99
+ dimension: undefined,
100
+ usage: undefined,
101
+ aspect: undefined,
102
+ baseMipLevel: undefined,
103
+ mipLevelCount: undefined,
104
+ baseArrayLayer: undefined,
105
+ arrayLayerCount: undefined,
106
+ }),
107
+ destroyTexture: (texture) => device.destroyTexture(texture),
108
+ queue: {
109
+ copyExternalImageToTexture: (source, destination, copySize) =>
110
+ device.queue.copyExternalImageToTexture(
111
+ { source: source.source, origin: [0, 0], flipY: source.flipY ?? false },
112
+ { texture: destination.texture, origin: [0, 0, 0] },
113
+ copySize,
114
+ ),
115
+ },
116
+ };
117
+ }
118
+
119
+ interface TransientEntry {
120
+ texture: Texture;
121
+ view: TextureView;
122
+ width: number;
123
+ height: number;
124
+ }
125
+
126
+ /**
127
+ * Transient per-frame texture store for video sources. Independent of
128
+ * GpuResourceStore: it neither imports nor reaches into the static residency
129
+ * cache (AC-08 / D-3).
130
+ */
131
+ export class DynamicTextureStore {
132
+ private device: DynamicTextureDevice | undefined = undefined;
133
+ private readonly entries = new Map<number, TransientEntry>();
134
+
135
+ /**
136
+ * Wire the GPU device the store uploads through. Called once after the
137
+ * renderer captures its device (mirrors GpuResourceStore.configureGpuDevice).
138
+ * A replacement device invalidates every cached texture: the handles in the
139
+ * map belong to the old device and cannot be reused after renderer recovery.
140
+ */
141
+ configureGpuDevice(device: DynamicTextureDevice): void {
142
+ if (this.device !== undefined && this.device !== device) {
143
+ this.destroyAll();
144
+ }
145
+ this.device = device;
146
+ }
147
+
148
+ /**
149
+ * Upload one video frame for `clip` from the host-owned source image
150
+ * (HTMLVideoElement / VideoFrame / ImageBitmap), (re)allocating the transient
151
+ * texture when its size changes, and return the current-frame view to bind.
152
+ *
153
+ * Returns `undefined` (not an error) when the device is not yet wired or the
154
+ * source has no decodable dimensions yet (metadata pending) — the caller binds
155
+ * the default view that frame. A structured RhiError surfaces only when a wired
156
+ * device rejects the allocation or the copy (charter P3).
157
+ */
158
+ uploadFrame(
159
+ clip: Handle<'VideoAsset', 'shared'>,
160
+ source: CopyExternalImageSource,
161
+ width: number,
162
+ height: number,
163
+ ): Result<TextureView, RhiError> | undefined {
164
+ const device = this.device;
165
+ if (device === undefined) return undefined;
166
+ if (width <= 0 || height <= 0) return undefined;
167
+
168
+ const id = handleSlot(clip);
169
+ const ensured = this.ensureEntry(device, id, width, height);
170
+ if (!ensured.ok) return ensured;
171
+ const entry = ensured.value;
172
+
173
+ const copyRes = device.queue.copyExternalImageToTexture(
174
+ { source, flipY: true },
175
+ { texture: entry.texture },
176
+ { width, height, depthOrArrayLayers: 1 },
177
+ );
178
+ if (!copyRes.ok) return copyRes;
179
+ return ok(entry.view);
180
+ }
181
+
182
+ /**
183
+ * The current-frame view for a clip, if one has been uploaded this session,
184
+ * else undefined. The record stage reads this when assembling the bind group
185
+ * (a frame that has not uploaded yet falls back to the default view).
186
+ */
187
+ getView(clip: Handle<'VideoAsset', 'shared'>): TextureView | undefined {
188
+ return this.entries.get(handleSlot(clip))?.view;
189
+ }
190
+
191
+ /** Destroy every transient texture + drop the map (renderer teardown). */
192
+ destroyAll(): void {
193
+ const device = this.device;
194
+ for (const entry of this.entries.values()) {
195
+ device?.destroyTexture(entry.texture);
196
+ }
197
+ this.entries.clear();
198
+ }
199
+
200
+ /**
201
+ * Get the entry for `id`, (re)allocating its texture + view when absent or
202
+ * when the source dimensions changed. A same-size re-upload reuses the
203
+ * existing texture (allocate-once for a steady clip; the per-frame cost is the
204
+ * copyExternalImageToTexture write, not a texture create).
205
+ */
206
+ private ensureEntry(
207
+ device: DynamicTextureDevice,
208
+ id: number,
209
+ width: number,
210
+ height: number,
211
+ ): Result<TransientEntry, RhiError> {
212
+ const existing = this.entries.get(id);
213
+ if (existing !== undefined && existing.width === width && existing.height === height) {
214
+ return ok(existing);
215
+ }
216
+ if (existing !== undefined) device.destroyTexture(existing.texture);
217
+
218
+ const texRes = device.createTexture({
219
+ size: { width, height, depthOrArrayLayers: 1 },
220
+ format: VIDEO_TEXTURE_FORMAT,
221
+ usage: VIDEO_TEXTURE_USAGE,
222
+ label: `video-transient-${id}`,
223
+ });
224
+ if (!texRes.ok) return texRes;
225
+ const viewRes = device.createTextureView(texRes.value, {});
226
+ if (!viewRes.ok) {
227
+ device.destroyTexture(texRes.value);
228
+ return viewRes;
229
+ }
230
+ const entry: TransientEntry = { texture: texRes.value, view: viewRes.value, width, height };
231
+ this.entries.set(id, entry);
232
+ return ok(entry);
233
+ }
234
+ }