@carbonenginejs/runtime-resource 0.1.0 → 0.8.0

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 (329) hide show
  1. package/FORMAT-PROVENANCE.md +155 -135
  2. package/LICENSE +21 -21
  3. package/NOTICE +29 -29
  4. package/README.md +548 -241
  5. package/dist/CjsMotherLode.js +1271 -56
  6. package/dist/CjsMotherLode.js.map +1 -1
  7. package/dist/CjsResMan.js +3005 -740
  8. package/dist/CjsResMan.js.map +1 -1
  9. package/dist/CjsResManQueue.js +214 -214
  10. package/dist/CjsResource.js +566 -366
  11. package/dist/CjsResource.js.map +1 -1
  12. package/dist/CjsResourceSource.js +59 -59
  13. package/dist/_virtual/_rollupPluginBabelHelpers.js +153 -153
  14. package/dist/format/CjsBlueReader.js +269 -0
  15. package/dist/format/CjsBlueReader.js.map +1 -0
  16. package/dist/format/CjsFormat.js +192 -178
  17. package/dist/format/CjsFormat.js.map +1 -1
  18. package/dist/format/CjsReader.js +18 -0
  19. package/dist/format/CjsReader.js.map +1 -0
  20. package/dist/format/CjsResourceProbe.js +277 -269
  21. package/dist/format/CjsResourceProbe.js.map +1 -1
  22. package/dist/format/payloadContract.js +173 -173
  23. package/dist/formats/black/CjsBlackFormat.js +278 -272
  24. package/dist/formats/black/CjsBlackFormat.js.map +1 -1
  25. package/dist/formats/black/core/CjsBlackBinaryReader.js +153 -153
  26. package/dist/formats/black/core/CjsBlackPropertyReaders.js +382 -382
  27. package/dist/formats/black/core/CjsBlackReader.js +639 -760
  28. package/dist/formats/black/core/CjsBlackReader.js.map +1 -1
  29. package/dist/formats/black/core/CjsBlackSchemaRegistry.js +433 -433
  30. package/dist/formats/black/core/black-schema-v1-2026-07-11.json.js +4 -4
  31. package/dist/formats/black/core/blackConstants.js +7 -7
  32. package/dist/formats/black/core/blackDefinitions.js +10 -10
  33. package/dist/formats/black/core/blackEnums.js +6 -6
  34. package/dist/formats/black/core/blackSchema.js +3 -3
  35. package/dist/formats/black/core/blackVersion.js +22 -22
  36. package/dist/formats/black/core/helpers.js +182 -182
  37. package/dist/formats/black/core/schema.js +4 -4
  38. package/dist/formats/black/index.js +2 -2
  39. package/dist/formats/bnk/CjsBnkFormat.js +238 -245
  40. package/dist/formats/bnk/CjsBnkFormat.js.map +1 -1
  41. package/dist/formats/bnk/core/graph.js +140 -0
  42. package/dist/formats/bnk/core/graph.js.map +1 -0
  43. package/dist/formats/bnk/core/helpers.js +409 -354
  44. package/dist/formats/bnk/core/helpers.js.map +1 -1
  45. package/dist/formats/bnk/core/musicNodes.js +521 -0
  46. package/dist/formats/bnk/core/musicNodes.js.map +1 -0
  47. package/dist/formats/bnk/core/soundbanksInfo.js +246 -246
  48. package/dist/formats/bnk/index.js +2 -3
  49. package/dist/formats/bnk/index.js.map +1 -1
  50. package/dist/formats/cmf/CjsCmfFormat.js +497 -491
  51. package/dist/formats/cmf/CjsCmfFormat.js.map +1 -1
  52. package/dist/formats/cmf/core/binary.js +118 -118
  53. package/dist/formats/cmf/core/buffers.js +233 -233
  54. package/dist/formats/cmf/core/constants.js +47 -47
  55. package/dist/formats/cmf/core/gr2Anim.js +453 -453
  56. package/dist/formats/cmf/core/helpers.js +287 -287
  57. package/dist/formats/cmf/core/pack.js +276 -276
  58. package/dist/formats/cmf/core/schema.js +364 -364
  59. package/dist/formats/cmf/core/shared.js +268 -268
  60. package/dist/formats/cmf/core/writer.js +517 -517
  61. package/dist/formats/cmf/index.js +2 -2
  62. package/dist/formats/dds/CjsDdsFormat.js +193 -187
  63. package/dist/formats/dds/CjsDdsFormat.js.map +1 -1
  64. package/dist/formats/dds/core/bc6h.js +288 -0
  65. package/dist/formats/dds/core/bc6h.js.map +1 -0
  66. package/dist/formats/dds/core/bc7.js +251 -0
  67. package/dist/formats/dds/core/bc7.js.map +1 -0
  68. package/dist/formats/dds/core/helpers.js +815 -812
  69. package/dist/formats/dds/core/helpers.js.map +1 -1
  70. package/dist/formats/dds/index.js +2 -2
  71. package/dist/formats/fbx/CjsFbxFormat.js +266 -260
  72. package/dist/formats/fbx/CjsFbxFormat.js.map +1 -1
  73. package/dist/formats/fbx/core/helpers.js +3901 -3901
  74. package/dist/formats/fbx/index.js +2 -2
  75. package/dist/formats/flac/CjsFlacFormat.js +87 -81
  76. package/dist/formats/flac/CjsFlacFormat.js.map +1 -1
  77. package/dist/formats/flac/core/helpers.js +295 -295
  78. package/dist/formats/flac/index.js +2 -2
  79. package/dist/formats/gif/CjsGifFormat.js +87 -81
  80. package/dist/formats/gif/CjsGifFormat.js.map +1 -1
  81. package/dist/formats/gif/core/helpers.js +360 -360
  82. package/dist/formats/gif/index.js +2 -2
  83. package/dist/formats/gltf/CjsGltfFormat.js +290 -284
  84. package/dist/formats/gltf/CjsGltfFormat.js.map +1 -1
  85. package/dist/formats/gltf/core/helpers.js +292 -292
  86. package/dist/formats/gltf/core/json.js +79 -79
  87. package/dist/formats/gltf/core/parser.js +666 -666
  88. package/dist/formats/gltf/core/targets.js +163 -163
  89. package/dist/formats/gltf/index.js +2 -2
  90. package/dist/formats/index.js +23 -23
  91. package/dist/formats/jpeg/CjsJpegFormat.js +206 -200
  92. package/dist/formats/jpeg/CjsJpegFormat.js.map +1 -1
  93. package/dist/formats/jpeg/core/helpers.js +350 -350
  94. package/dist/formats/jpeg/core/jpeg.js +377 -377
  95. package/dist/formats/jpeg/index.js +2 -2
  96. package/dist/formats/mp3/CjsMp3Format.js +192 -186
  97. package/dist/formats/mp3/CjsMp3Format.js.map +1 -1
  98. package/dist/formats/mp3/core/helpers.js +338 -338
  99. package/dist/formats/mp3/index.js +2 -2
  100. package/dist/formats/mp4/CjsMp4Format.js +191 -185
  101. package/dist/formats/mp4/CjsMp4Format.js.map +1 -1
  102. package/dist/formats/mp4/core/helpers.js +449 -449
  103. package/dist/formats/mp4/index.js +2 -2
  104. package/dist/formats/obj/CjsObjFormat.js +253 -247
  105. package/dist/formats/obj/CjsObjFormat.js.map +1 -1
  106. package/dist/formats/obj/core/helpers.js +573 -573
  107. package/dist/formats/obj/core/json.js +64 -64
  108. package/dist/formats/obj/core/parser.js +321 -321
  109. package/dist/formats/obj/index.js +2 -2
  110. package/dist/formats/ogg/CjsOggFormat.js +88 -82
  111. package/dist/formats/ogg/CjsOggFormat.js.map +1 -1
  112. package/dist/formats/ogg/core/helpers.js +387 -387
  113. package/dist/formats/ogg/core/imdct.js +178 -178
  114. package/dist/formats/ogg/core/vorbis.js +999 -999
  115. package/dist/formats/ogg/index.js +2 -2
  116. package/dist/formats/png/CjsPngFormat.js +195 -189
  117. package/dist/formats/png/CjsPngFormat.js.map +1 -1
  118. package/dist/formats/png/core/helpers.js +583 -583
  119. package/dist/formats/png/index.js +2 -2
  120. package/dist/formats/red/CjsRedFormat.js +261 -247
  121. package/dist/formats/red/CjsRedFormat.js.map +1 -1
  122. package/dist/formats/red/core/CjsRedReader.js +194 -201
  123. package/dist/formats/red/core/CjsRedReader.js.map +1 -1
  124. package/dist/formats/red/core/blackDefinitions.js +3 -3
  125. package/dist/formats/red/core/helpers.js +139 -136
  126. package/dist/formats/red/core/helpers.js.map +1 -1
  127. package/dist/formats/red/core/redGraph.js +68 -68
  128. package/dist/formats/red/core/schema.js +4 -4
  129. package/dist/formats/red/index.js +2 -2
  130. package/dist/formats/stl/CjsStlFormat.js +365 -304
  131. package/dist/formats/stl/CjsStlFormat.js.map +1 -1
  132. package/dist/formats/stl/core/helpers.js +261 -257
  133. package/dist/formats/stl/core/helpers.js.map +1 -1
  134. package/dist/formats/stl/core/json.js +51 -51
  135. package/dist/formats/stl/core/stl.js +629 -544
  136. package/dist/formats/stl/core/stl.js.map +1 -1
  137. package/dist/formats/stl/core/targets.js +163 -163
  138. package/dist/formats/stl/index.js +2 -2
  139. package/dist/formats/tga/CjsTgaFormat.js +192 -186
  140. package/dist/formats/tga/CjsTgaFormat.js.map +1 -1
  141. package/dist/formats/tga/core/helpers.js +446 -446
  142. package/dist/formats/tga/index.js +2 -2
  143. package/dist/formats/wav/CjsWavFormat.js +192 -186
  144. package/dist/formats/wav/CjsWavFormat.js.map +1 -1
  145. package/dist/formats/wav/core/helpers.js +328 -328
  146. package/dist/formats/wav/index.js +2 -2
  147. package/dist/formats/webm/CjsWebmFormat.js +191 -185
  148. package/dist/formats/webm/CjsWebmFormat.js.map +1 -1
  149. package/dist/formats/webm/core/helpers.js +537 -537
  150. package/dist/formats/webm/index.js +2 -2
  151. package/dist/formats/webp/CjsWebpFormat.js +86 -80
  152. package/dist/formats/webp/CjsWebpFormat.js.map +1 -1
  153. package/dist/formats/webp/core/helpers.js +214 -214
  154. package/dist/formats/webp/index.js +2 -2
  155. package/dist/formats/wem/CjsWemFormat.js +241 -197
  156. package/dist/formats/wem/CjsWemFormat.js.map +1 -1
  157. package/dist/formats/wem/core/bitStream.js +259 -259
  158. package/dist/formats/wem/core/codebookLibrary.js +164 -164
  159. package/dist/formats/wem/core/helpers.js +417 -336
  160. package/dist/formats/wem/core/helpers.js.map +1 -1
  161. package/dist/formats/wem/core/packedCodebooksAotuv603.js +30 -30
  162. package/dist/formats/wem/core/ptadpcm.js +77 -0
  163. package/dist/formats/wem/core/ptadpcm.js.map +1 -0
  164. package/dist/formats/wem/core/resolve.js +121 -0
  165. package/dist/formats/wem/core/resolve.js.map +1 -0
  166. package/dist/formats/wem/core/wemToOgg.js +485 -485
  167. package/dist/formats/wem/index.js +2 -2
  168. package/dist/formats/yaml/CjsYamlFormat.js +83 -77
  169. package/dist/formats/yaml/CjsYamlFormat.js.map +1 -1
  170. package/dist/formats/yaml/core/CjsYamlReader.js +305 -304
  171. package/dist/formats/yaml/core/CjsYamlReader.js.map +1 -1
  172. package/dist/formats/yaml/core/helpers.js +160 -160
  173. package/dist/formats/yaml/index.js +2 -2
  174. package/dist/index.js +48 -52
  175. package/dist/index.js.map +1 -1
  176. package/dist/resourcePath.js +18 -18
  177. package/dist/resourceStates.js +9 -9
  178. package/dist/{generated/resources → resources}/AudioGeometryResData.js +47 -47
  179. package/dist/resources/AudioGeometryResData.js.map +1 -0
  180. package/dist/{generated/resources → resources}/GStateBindingCallbackData.js +31 -31
  181. package/dist/resources/GStateBindingCallbackData.js.map +1 -0
  182. package/dist/{generated/resources → resources}/MeshDecalData.js +37 -37
  183. package/dist/resources/MeshDecalData.js.map +1 -0
  184. package/dist/{generated/resources → resources}/MeshDecalLodData.js +34 -34
  185. package/dist/resources/MeshDecalLodData.js.map +1 -0
  186. package/dist/resources/Tr2EffectRes.js +71 -71
  187. package/dist/resources/Tr2GrannyIntersectionResult.js +60 -0
  188. package/dist/resources/Tr2GrannyIntersectionResult.js.map +1 -0
  189. package/dist/resources/Tr2GrannyStateRes.js +44 -44
  190. package/dist/resources/Tr2ImageRes.js +114 -114
  191. package/dist/resources/Tr2LightProfileRes.js +40 -40
  192. package/dist/{generated/resources → resources}/Tr2MaterialArea.js +34 -34
  193. package/dist/resources/Tr2MaterialArea.js.map +1 -0
  194. package/dist/{generated/resources → resources}/Tr2MaterialMesh.js +31 -31
  195. package/dist/resources/Tr2MaterialMesh.js.map +1 -0
  196. package/dist/{generated/resources → resources}/Tr2MaterialRes.js +34 -34
  197. package/dist/resources/Tr2MaterialRes.js.map +1 -0
  198. package/dist/{generated/resources → resources}/Tr2ShaderPermutation.js +43 -43
  199. package/dist/resources/Tr2ShaderPermutation.js.map +1 -0
  200. package/dist/{generated/resources → resources}/Tr2TextureLodManager.js +100 -60
  201. package/dist/resources/Tr2TextureLodManager.js.map +1 -0
  202. package/dist/{generated/resources → resources}/Tr2TextureLodUpdateRequest.js +37 -37
  203. package/dist/resources/Tr2TextureLodUpdateRequest.js.map +1 -0
  204. package/dist/{generated/resources → resources}/Tr2TexturePackChannel.js +37 -37
  205. package/dist/resources/Tr2TexturePackChannel.js.map +1 -0
  206. package/dist/resources/Tr2TexturePipeline.js +68 -0
  207. package/dist/resources/Tr2TexturePipeline.js.map +1 -0
  208. package/dist/{generated/resources → resources}/Tr2TexturePipelineParams.js +34 -34
  209. package/dist/resources/Tr2TexturePipelineParams.js.map +1 -0
  210. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepCompress.js +40 -40
  211. package/dist/resources/Tr2TexturePipelineStepCompress.js.map +1 -0
  212. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepLimitSize.js +34 -34
  213. package/dist/resources/Tr2TexturePipelineStepLimitSize.js.map +1 -0
  214. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepLoad.js +31 -31
  215. package/dist/resources/Tr2TexturePipelineStepLoad.js.map +1 -0
  216. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepPack.js +43 -43
  217. package/dist/resources/Tr2TexturePipelineStepPack.js.map +1 -0
  218. package/dist/resources/TriGeometryRes.js +239 -216
  219. package/dist/resources/TriGeometryRes.js.map +1 -1
  220. package/dist/{generated/resources → resources}/TriGeometryResAreaData.js +59 -59
  221. package/dist/resources/TriGeometryResAreaData.js.map +1 -0
  222. package/dist/{generated/resources → resources}/TriGeometryResJointData.js +38 -38
  223. package/dist/resources/TriGeometryResJointData.js.map +1 -0
  224. package/dist/{generated/resources → resources}/TriGeometryResLodData.js +88 -88
  225. package/dist/resources/TriGeometryResLodData.js.map +1 -0
  226. package/dist/{generated/resources → resources}/TriGeometryResMeshData.js +63 -63
  227. package/dist/resources/TriGeometryResMeshData.js.map +1 -0
  228. package/dist/{generated/resources → resources}/TriGeometryResSkeletonData.js +34 -34
  229. package/dist/resources/TriGeometryResSkeletonData.js.map +1 -0
  230. package/dist/resources/TriGrannyRes.js +43 -43
  231. package/dist/{generated/resources → resources}/TriJointBinding.js +38 -38
  232. package/dist/resources/TriJointBinding.js.map +1 -0
  233. package/dist/{generated/resources → resources}/TriMorphTargetGeometryConstants.js +46 -46
  234. package/dist/resources/TriMorphTargetGeometryConstants.js.map +1 -0
  235. package/dist/{generated/resources → resources}/TriRtGeometryConstants.js +88 -88
  236. package/dist/resources/TriRtGeometryConstants.js.map +1 -0
  237. package/dist/resources/TriTextureRes.js +304 -304
  238. package/dist/{generated/resources → resources}/enums.js +18 -18
  239. package/dist/resources/enums.js.map +1 -0
  240. package/dist/resources/resourceBoundary.js +47 -47
  241. package/dist/resources/texturePipelineBehavior.js +320 -0
  242. package/dist/resources/texturePipelineBehavior.js.map +1 -0
  243. package/dist/texture/CjsTextureArrayRes.js +406 -406
  244. package/dist/texture/CjsTextureParameterProxy.js +133 -133
  245. package/format-notices/black/LICENSE +21 -21
  246. package/format-notices/black/NOTICE +47 -47
  247. package/format-notices/bnk/LICENSE +21 -21
  248. package/format-notices/bnk/NOTICE +20 -17
  249. package/format-notices/cmf/LICENSE +21 -21
  250. package/format-notices/cmf/NOTICE +36 -36
  251. package/format-notices/dds/LICENSE +21 -21
  252. package/format-notices/dds/NOTICE +14 -14
  253. package/format-notices/fbx/LICENSE +21 -21
  254. package/format-notices/fbx/NOTICE +14 -14
  255. package/format-notices/flac/LICENSE +21 -21
  256. package/format-notices/flac/NOTICE +14 -14
  257. package/format-notices/gif/LICENSE +21 -21
  258. package/format-notices/gif/NOTICE +14 -14
  259. package/format-notices/gltf/LICENSE +21 -21
  260. package/format-notices/gltf/NOTICE +27 -27
  261. package/format-notices/jpeg/LICENSE +21 -21
  262. package/format-notices/jpeg/NOTICE +14 -14
  263. package/format-notices/mp3/LICENSE +21 -21
  264. package/format-notices/mp3/NOTICE +14 -14
  265. package/format-notices/mp4/LICENSE +21 -21
  266. package/format-notices/mp4/NOTICE +14 -14
  267. package/format-notices/obj/LICENSE +21 -21
  268. package/format-notices/obj/NOTICE +26 -26
  269. package/format-notices/ogg/LICENSE +21 -21
  270. package/format-notices/ogg/NOTICE +28 -28
  271. package/format-notices/png/LICENSE +21 -21
  272. package/format-notices/png/NOTICE +14 -14
  273. package/format-notices/red/LICENSE +21 -21
  274. package/format-notices/red/NOTICE +31 -31
  275. package/format-notices/stl/LICENSE +21 -21
  276. package/format-notices/stl/NOTICE +21 -21
  277. package/format-notices/tga/LICENSE +21 -21
  278. package/format-notices/tga/NOTICE +14 -14
  279. package/format-notices/wav/LICENSE +21 -21
  280. package/format-notices/wav/NOTICE +14 -14
  281. package/format-notices/webm/LICENSE +21 -21
  282. package/format-notices/webm/NOTICE +14 -14
  283. package/format-notices/webp/LICENSE +21 -21
  284. package/format-notices/webp/NOTICE +14 -14
  285. package/format-notices/wem/LICENSE +57 -57
  286. package/format-notices/wem/NOTICE +33 -33
  287. package/format-notices/yaml/LICENSE +21 -21
  288. package/format-notices/yaml/NOTICE +44 -44
  289. package/package.json +51 -51
  290. package/resource-lifecycle.md +679 -487
  291. package/dist/generated/resources/AudioGeometryResData.js.map +0 -1
  292. package/dist/generated/resources/CmfVertexReader.js +0 -46
  293. package/dist/generated/resources/CmfVertexReader.js.map +0 -1
  294. package/dist/generated/resources/GStateBindingCallbackData.js.map +0 -1
  295. package/dist/generated/resources/MeshDecalData.js.map +0 -1
  296. package/dist/generated/resources/MeshDecalLodData.js.map +0 -1
  297. package/dist/generated/resources/Tr2AsyncSave.js +0 -46
  298. package/dist/generated/resources/Tr2AsyncSave.js.map +0 -1
  299. package/dist/generated/resources/Tr2CmfContents.js +0 -37
  300. package/dist/generated/resources/Tr2CmfContents.js.map +0 -1
  301. package/dist/generated/resources/Tr2GrannyIntersectionResult.js +0 -58
  302. package/dist/generated/resources/Tr2GrannyIntersectionResult.js.map +0 -1
  303. package/dist/generated/resources/Tr2LoadPrepareFence.js +0 -37
  304. package/dist/generated/resources/Tr2LoadPrepareFence.js.map +0 -1
  305. package/dist/generated/resources/Tr2MaterialArea.js.map +0 -1
  306. package/dist/generated/resources/Tr2MaterialMesh.js.map +0 -1
  307. package/dist/generated/resources/Tr2MaterialRes.js.map +0 -1
  308. package/dist/generated/resources/Tr2ShaderPermutation.js.map +0 -1
  309. package/dist/generated/resources/Tr2TextureLodManager.js.map +0 -1
  310. package/dist/generated/resources/Tr2TextureLodUpdateRequest.js.map +0 -1
  311. package/dist/generated/resources/Tr2TexturePackChannel.js.map +0 -1
  312. package/dist/generated/resources/Tr2TexturePipeline.js +0 -44
  313. package/dist/generated/resources/Tr2TexturePipeline.js.map +0 -1
  314. package/dist/generated/resources/Tr2TexturePipelineParams.js.map +0 -1
  315. package/dist/generated/resources/Tr2TexturePipelineStepCompress.js.map +0 -1
  316. package/dist/generated/resources/Tr2TexturePipelineStepLimitSize.js.map +0 -1
  317. package/dist/generated/resources/Tr2TexturePipelineStepLoad.js.map +0 -1
  318. package/dist/generated/resources/Tr2TexturePipelineStepPack.js.map +0 -1
  319. package/dist/generated/resources/TriGeometryResAreaData.js.map +0 -1
  320. package/dist/generated/resources/TriGeometryResJointData.js.map +0 -1
  321. package/dist/generated/resources/TriGeometryResLodData.js.map +0 -1
  322. package/dist/generated/resources/TriGeometryResMeshData.js.map +0 -1
  323. package/dist/generated/resources/TriGeometryResSkeletonData.js.map +0 -1
  324. package/dist/generated/resources/TriJointBinding.js.map +0 -1
  325. package/dist/generated/resources/TriMorphTargetGeometryConstants.js.map +0 -1
  326. package/dist/generated/resources/TriRtGeometryConstants.js.map +0 -1
  327. package/dist/generated/resources/enums.js.map +0 -1
  328. package/dist/generated/resources/index.js +0 -32
  329. package/dist/generated/resources/index.js.map +0 -1
@@ -1,487 +1,679 @@
1
- # Runtime Resource Lifecycle
2
-
3
- This note records how Carbon/ccpwgl resource loading maps to
4
- `runtime-resource`.
5
-
6
- ## Carbon and ccpwgl
7
-
8
- Carbon's resource model separates load from prepare. The source schemas expose
9
- resource classes through `BlueAsyncRes`, and Carbon notes distinguish background
10
- load work from main-thread/device prepare work.
11
-
12
- ccpwgl makes that split visible in `Tw2Resource`:
13
-
14
- ```text
15
- NO_INIT -> REQUESTED -> LOADED -> PREPARED
16
- ```
17
-
18
- Additional terminal or cleanup states include `ERROR`, `UNLOADED`, and `PURGED`.
19
-
20
- The important behavior is:
21
-
22
- - `Tw2ResMan.LoadResource()` requests a resource.
23
- - The raw fetch resolves.
24
- - `Tw2Resource.OnLoaded()` marks bytes or source data as loaded.
25
- - The resource is queued for prepare.
26
- - The manager tick later calls `res.Prepare(data)`.
27
- - The concrete resource calls `OnPrepared()` after prepare work succeeds.
28
-
29
- Some ccpwgl concrete `Prepare()` implementations also create WebGL objects.
30
- That is a historical engine/runtime coupling, not the boundary we want in
31
- CarbonEngineJS.
32
-
33
- ## CarbonEngineJS
34
-
35
- `runtime-resource` owns the GPU-free half:
36
-
37
- ```text
38
- EMPTY -> REQUESTED/LOADING -> LOADED
39
- ```
40
-
41
- Engine adapters own device realization:
42
-
43
- ```text
44
- LOADED -> PREPARING -> PREPARED
45
- ```
46
-
47
- Current state meanings:
48
-
49
- - `EMPTY`: resource identity exists, but no payload has been read.
50
- - `REQUESTED`: the resource is waiting on a queued or shared source load.
51
- - `LOADING`: source bytes are available and staged object preparation is active.
52
- - `LOADED`: CPU payload or hydrated object graph exists.
53
- - `PREPARING`: an engine adapter is realizing backend-owned resources.
54
- - `PREPARED`: preparation completed successfully and the resource is usable.
55
- - `FAILED`: load or prepare failed.
56
- - `UNLOADED`: resource payload was released.
57
- - `PURGED`: resource was purged from active ownership.
58
-
59
- `CjsResMan.LoadObject()` now queues one deduplicated background source operation
60
- per source/path and limits active source operations with `maxConcurrentLoads`.
61
- After bytes arrive, object construction is split into separate main-queue
62
- items:
63
-
64
- ```text
65
- read -> registered/requested prepare stage 1 -> stage 2 -> ... -> publish
66
- ```
67
-
68
- `maxPrepareTime` limits seconds spent starting synchronous main-queue work in
69
- one pump, and `maxPrepareItemsPerTick` can add a count limit. Promise-returning
70
- stages remain in flight without blocking the JavaScript event loop. Publication
71
- moves the resource to `LOADED`, then stops. It must not mark the resource
72
- `PREPARED` or `GOOD` unless an explicitly supplied preparation stage has
73
- actually completed backend realization and marked it accordingly.
74
-
75
- Named pipelines are registration/configuration, not capability policy.
76
- `CjsLibrary` or a direct caller determines the required output and selects a
77
- registered `preparePipeline`; `CjsResMan` executes the supplied stages without
78
- probing device support. Per-request `prepareStages` are explicit overrides.
79
-
80
- The Blue method names remain the public queue vocabulary: `AddToQueue`,
81
- `CancelFromQueue`, `GetNextIdForQueue`, `PumpMainThreadQueue`, `PauseQueue`,
82
- `ResumeQueue`, `GetPendingLoads`, and `GetPendingPrepares`. `Update()`/`Tick()`
83
- pump queues. `Wait()` provides a method-level fence until both queues become
84
- idle, rather than introducing a runtime fence resource.
85
-
86
- ## CjsLibrary resource-path workflow
87
-
88
- The normal direct resource-path flow is:
89
-
90
- ```text
91
- Application / runtime object
92
- |
93
- | requests "res:/model/ship.gr2"
94
- | with optional per-request overrides
95
- v
96
- +-----------------------------------------------+
97
- | CjsLibrary |
98
- | |
99
- | - starts from registered default behavior |
100
- | - considers registered capability reports |
101
- | - chooses requirement / emit / pipeline |
102
- | - applies explicit request overrides |
103
- +-----------------------------------------------+
104
- |
105
- | path + resolved request options
106
- v
107
- +-----------------------------------------------+
108
- | CjsResMan.GetResource(path, options) |
109
- | |
110
- | - normalize path and extension |
111
- | - calculate requested resource variant |
112
- +-----------------------------------------------+
113
- |
114
- v
115
- +-----------------------------------------------+
116
- | CjsMotherLode.Lookup(path, variant) |
117
- +-----------------------------------------------+
118
- |
119
- +--- cache hit ------------------------------+
120
- | |
121
- | reuse the CjsResource and any active |
122
- | load/build operation |
123
- | |
124
- `--- cache miss -----------------------------+
125
- | |
126
- | resolve class from requirement |
127
- | construct + Initialize() |
128
- | insert into CjsMotherLode |
129
- v |
130
- new CjsResource -------------------------------+
131
- |
132
- | Ready() / GetObject()
133
- v
134
- +===============================================+
135
- | BACKGROUND LOAD QUEUE |
136
- | |
137
- | - mark resource REQUESTED |
138
- | - share one source operation per source/path |
139
- | - obey maxConcurrentLoads |
140
- | - source.Read(path) |
141
- +===============================================+
142
- |
143
- | source bytes
144
- v
145
- +===============================================+
146
- | MAIN PREPARE QUEUE |
147
- | |
148
- | every box below is a separately budgeted item |
149
- +===============================================+
150
- |
151
- v
152
- +-----------------------------------------------+
153
- | Read stage |
154
- | |
155
- | registered object loader for extension? |
156
- | yes -> call it |
157
- | no -> resolve registered format by |
158
- | extension + request options |
159
- +-----------------------------------------------+
160
- |
161
- | plain payload / hydrated object
162
- v
163
- +-----------------------------------------------+
164
- | Optional configured prepare stages |
165
- | |
166
- | stage 1 -> stage 2 -> ... |
167
- | examples: normalize, convert, adapt |
168
- +-----------------------------------------------+
169
- |
170
- | no configured stages skips this box
171
- v
172
- +-----------------------------------------------+
173
- | Publish stage |
174
- | |
175
- | semantic resource -> SetPayload(payload) |
176
- | generic resource -> resource.object = value |
177
- +-----------------------------------------------+
178
- |
179
- +--- validation failure --------------------+
180
- | |
181
- | resource -> FAILED |
182
- | Ready() rejects |
183
- | |
184
- `--- publication succeeds -----------------+
185
- |
186
- | resource -> LOADED
187
- v
188
- CjsLibrary returns
189
- the built CjsResource/object
190
- ```
191
-
192
- Device realization is a separate continuation selected outside ResMan:
193
-
194
- ```text
195
- CjsResource LOADED
196
- |
197
- | selected engine adapter
198
- v
199
- PREPARING -> attach opaque adapter resource -> PREPARED
200
- ```
201
-
202
- With no named or direct prepare stages, the main queue reduces to:
203
-
204
- ```text
205
- extension object-loader/format read -> validate -> publish
206
- ```
207
-
208
- ## Texture Array Generations
209
-
210
- `CjsTextureArrayRes` is a derived multi-source resource with an explicit
211
- requested/prepared revision boundary:
212
-
213
- ```text
214
- proxy/source change
215
- -> requested revision + dirty layer
216
- -> one scheduled next-frame snapshot
217
- -> consumed/in-flight request
218
- -> adapter candidate preparation
219
- -> guarded adapter + prepared-revision publication
220
- ```
221
-
222
- `ConsumeUpdateRequest()` produces an immutable snapshot and marks that revision
223
- in flight. A current consumed revision may be completed through
224
- `CommitPreparedAdapterRevision()`, failed through `FailUpdateRequest()`, or
225
- returned to the queue through `RetryUpdateRequest()`. Commit-before-consume and
226
- stale commits are rejected; rejected candidate allocations are destroyed by
227
- default.
228
-
229
- Publication installs the adapter allocation and prepared revision before
230
- completion events run. The result returns the displaced allocation to the
231
- adapter owner for post-publication destruction. A reentrant source change may
232
- therefore request a newer revision without allowing stale completion to replace
233
- it. The previous prepared allocation and `IsGood()` remain usable while a
234
- replacement is pending or if replacement preparation fails.
235
-
236
- `Ready()` is specialized for this derived resource: it resolves when the
237
- generation requested at call time has been published, rather than delegating
238
- to a single-source object loader. Initial preparation failure rejects it.
239
-
240
- Logical paths and resolved sources are independent. Attaching a redirected or
241
- LOD-specific source does not rewrite persistence. `TouchLayer()` represents an
242
- in-place source revision, and `HandleAdapterLoss()` invalidates the complete
243
- topology after destroying an unusable adapter allocation. Topology snapshots
244
- report an explicit `topologyChanged` flag and contain only valid current dirty
245
- layer indices.
246
-
247
- ## Remaining Runtime Manager Work
248
-
249
- The first load/prepare queue slice is implemented. `CjsResMan` and
250
- `CjsMotherLode` still do not cover several browser/runtime concerns that
251
- ccpwgl handles:
252
-
253
- - prepare priority and starvation policy
254
- - cancellation/abort propagation for work that has already started
255
- - queue-time and stage-time telemetry
256
- - `KeepAlive()` style active-frame updates
257
- - automatic purge windows for inactive resources
258
- - reload policy
259
- - browser-aware source behavior such as fetch response type selection
260
-
261
- ## Memory Retention and Purging
262
-
263
- ccpwgl keeps every resource in `Tw2MotherLode` until it is explicitly cleared or
264
- auto-purged. Each resource has:
265
-
266
- - `activeFrame`: last frame the resource was considered in use.
267
- - `doNotPurge`: a lock counter/flag for resources that must stay resident.
268
- - `KeepAlive()`: updates `activeFrame` and reloads an unloaded/purged resource
269
- if it is touched again.
270
-
271
- The manager advances an `activeFrame` counter on a cadence, and
272
- `Tw2MotherLode.PurgeInactive()` unloads resources whose `activeFrame` is old
273
- enough. It then marks them purged and removes them from the cache. In ccpwgl,
274
- `IsGood()` also calls `KeepAlive()`, so many read/check paths implicitly keep
275
- resources resident.
276
-
277
- That model works for ccpwgl, but CarbonEngineJS should be more explicit:
278
-
279
- - `IsGood()`, `IsPrepared()`, and `HasLoaded()` should stay pure state checks.
280
- - `KeepAlive()` or `Touch()` should be the explicit liveness operation.
281
- - `Lock()` / `Unlock()` should prevent automatic purge, probably with a lock
282
- count rather than a boolean.
283
- - resources should track `lastUsedFrame` or `lastUsedTime`, not rely on boolean
284
- state alone.
285
- - `CjsMotherLode` should support purge scanning without forcing all resources to
286
- be held forever by accidental cache references.
287
- - `Unload()` should release engine adapter resources and optionally CPU payloads.
288
- - `Purge()` should remove the resource from the cache after unload/cleanup.
289
-
290
- The JS/browser split adds one more axis that ccpwgl blurs: CPU payload memory and
291
- GPU/device memory are different budgets. A large decoded image, geometry buffer,
292
- or shader graph can be expensive even before an engine adapter prepares it.
293
- Runtime-resource should therefore support separate retention policy for:
294
-
295
- - resource identity: path, extension, state, error summary, lightweight metadata.
296
- - CPU payload: plain reader/converter objects, hydrated object graphs, decoded
297
- typed arrays.
298
- - adapter payload: WebGL/WebGPU textures, buffers, shader modules, pipelines.
299
-
300
- A sane default would keep resource identity and lightweight metadata while
301
- allowing CPU payloads and adapter payloads to be released independently. Engine
302
- adapters should own adapter-resource destruction, but `runtime-resource` can
303
- provide lifecycle hooks and opaque adapter slots so the adapter has a consistent
304
- place to clean up.
305
-
306
- ### Payload Retention Contract
307
-
308
- Reader and converter outputs are plain transient payload objects, not resource
309
- classes or DTO models. A payload may contain more decoded data than a particular
310
- resource or engine adapter needs. Each concrete resource validates the fields
311
- it requires before publishing the payload and retains the scalars and references
312
- it needs. An adapter may retain additional references in adapter-owned state.
313
- Referencing payload-owned typed arrays is valid and preferable to copying them
314
- merely to change ownership.
315
-
316
- The lifecycle treats resource residency and payload residency independently:
317
-
318
- ```text
319
- resource.KeepAlive()
320
- -> renew resource/cache residency
321
-
322
- resource.KeepPayloadAlive()
323
- -> renew the attached payload lease
324
-
325
- resource.ReleasePayload()
326
- -> explicitly release the full payload reference
327
- ```
328
-
329
- `ReleasePayload()` is implemented; `KeepPayloadAlive()` remains target
330
- behavior. `GetPayload()` and `HasPayload()` are pure queries; reading the payload
331
- must not implicitly renew its lease.
332
-
333
- The processor preparing a resource decides when the full payload can be
334
- released:
335
-
336
- ```text
337
- format reader -> plain payload -> resource validation + adapter prepare
338
- |
339
- +-> resource retains required values/references
340
- +-> adapter retains adapter-specific state/references
341
- +-> release payload after successful preparation
342
- `-> or renew its lease for deferred/further work
343
- ```
344
-
345
- A time-based lease is a fallback against abandoned payloads. An owner performing
346
- deferred work can renew the lease. If the payload has expired and is required
347
- again, the manager reloads the source and reconstructs it. Dynamic or
348
- non-reloadable resources must retain or be able to recreate any payload they
349
- still require.
350
-
351
- Payload references are shared read-only by default. Preparing WebGL and WebGPU
352
- adapters side by side should normally pass the same payload to both consumers
353
- and retain it until both have finished. Copying is an explicit consumer
354
- operation, justified when a consumer must mutate data, transfer and detach an
355
- `ArrayBuffer`, or retain an independently writable snapshot. The consumer
356
- should copy only the fields it requires; runtime-resource should not
357
- automatically deep-clone entire payload or typed-array bundles. Any full-copy
358
- operation should be format-aware rather than a generic resource-side clone.
359
-
360
- Open design questions:
361
-
362
- - Should `Unload()` drop only adapter payloads by default, or CPU payloads too?
363
- - Should there be explicit `UnloadAdapterResources()`, `UnloadPayload()`, and
364
- `Purge()` phases?
365
- - Should loaded CPU payloads have a byte-size estimate so the purge policy can
366
- be memory-budget based instead of only time/frame based?
367
- - Should manually attached/dynamic resources default to locked, like ccpwgl's
368
- manual shader resources use `doNotPurge`?
369
- - Should `KeepAlive()` reload purged resources, or should reload be an explicit
370
- `Reload()` call to avoid surprising browser/network work?
371
-
372
- ccpwgl's raw event emitter shape is useful. The part we should not copy is the
373
- separate resource notification/callback compatibility layer that sits beside
374
- events. CarbonEngineJS exposes one small event-emitter API and avoids short
375
- generic names such as `On`, `Once`, `Off`, and `Emit` on Carbon-shaped classes.
376
- `CjsEventEmitter` is a separate base class so non-model runtime services can
377
- extend it without extending `CjsModel`; `CjsResMan` already uses that path.
378
-
379
- The API stays deliberately direct:
380
-
381
- - `AddEvents(events)`
382
- - `OnEvent(eventName, listener, source?)`
383
- - `OnceEvent(eventName, listener, source?)`
384
- - `OffEvent(eventName = "*", listener?, source?)`
385
- - `EmitEvent(eventName, ...args)`
386
- - `HasEvent(eventName = "*", listener?, source?)`
387
- - `ClearEvent(eventName = "*")`
388
- - `GetEventNames()` and `GetEventListenerCount(eventName = "*")`
389
-
390
- The optional `source` is the callback's `this` value and an explicit matching
391
- identity for removal. Mutating event methods return the emitter for chaining.
392
- There are no listener scopes, subscription handles, owner-side `ListenTo()`
393
- helpers, or parallel resource notification callbacks.
394
-
395
- Event names are normalized to lowercase and dispatched by exact match. Resource
396
- classes may emit their own state or domain events, but the emitter does not
397
- invent a resource lifecycle contract.
398
-
399
- Event memory rules matter as much as event names. Event storage is the optional
400
- `events` member of the emitter's non-enumerable `__state` object. It is created
401
- only when the first listener is registered and deleted when the last record is
402
- removed. That does not make listeners weak. As long as an emitter is reachable,
403
- its event map strongly references listener functions and sources, and those
404
- listeners can keep whole scene/resource graphs alive.
405
-
406
- CarbonEngineJS event emitters should therefore follow these rules:
407
-
408
- - `OnceEvent()` should remove the listener before or immediately after the first
409
- callback, even if the callback throws.
410
- - `OffEvent(eventName, listener, source)` must remove the exact listener/source
411
- entry.
412
- - an external party that no longer observes a target must call `OffEvent()`;
413
- `target.OffEvent("*", null, source)` removes all of that source's records.
414
- - `Unload()` and `Purge()` are resource state/cache operations, not an implied
415
- listener-destruction lifecycle.
416
- - `OnEvent()` returns the emitter, ccpwgl-style. It does not return unsubscribe
417
- closures because those closures create another reference path.
418
- - event payload history is not stored.
419
- - we should prefer deterministic cleanup over `WeakRef`/`FinalizationRegistry`;
420
- those can help diagnostics, but they are not a lifecycle contract.
421
-
422
- The target is "easy to debug, hard to leak": clear ownership of who subscribed,
423
- who unsubscribes, and which cleanup phase clears all remaining listeners.
424
-
425
- The event data model is deliberately small:
426
-
427
- ```text
428
- emitter.__state (non-enumerable, allocated only when some subsystem needs it)
429
- events -> eventName -> Set<listenerRecord>
430
-
431
- listenerRecord
432
- emitter
433
- eventName
434
- listener
435
- source
436
- once
437
- ```
438
-
439
- The event map is allocated only when the first listener is registered. If the
440
- emitter becomes unreachable, its state and event records can be collected with
441
- it. While the emitter remains reachable, listener functions and sources are
442
- strongly retained until `OffEvent()`, `ClearEvent()`, once dispatch, or emitter
443
- collection. Multiple listeners on the same event are allowed because each event
444
- bucket is a set of records. A raw `CjsEventEmitter` does not gain model-owned
445
- `dirty` or `rebuild` state.
446
-
447
- This is as far as the event system should go for now:
448
-
449
- - lowercase exact event names only.
450
- - no wildcard listener dispatch.
451
- - wildcard names are accepted only by lookup and cleanup methods.
452
- - no listener scope or subscription-object layer.
453
- - no separate resource notification/callback compatibility layer.
454
- - no `family.event` or ancestor routing.
455
- - no event history by default.
456
- - no global master event manager.
457
- - debugging introspection should stay limited to counts and names unless a real
458
- use case appears.
459
-
460
- `CjsModel` has dirty-state helpers (`MarkDirty`, `ClearDirty`, `ConsumeDirty`,
461
- `GetDirtyNotifications`) for model invalidation. `SetValues()` compares incoming
462
- values with the current field values and only marks dirty when a value actually
463
- changes. A plain `MarkDirty()` means broad dirty invalidation; it does not request
464
- a rebuild. Deferred rebuild reasons belong to the independent
465
- `model.__state.rebuild` set. This is not a resource lifecycle event system.
466
- Resource lifecycle events should therefore remain a resource/resman concern.
467
-
468
- ## Why We Diverge
469
-
470
- The Carbon and ccpwgl resource classes live inside an engine that can prepare
471
- GPU objects directly. CarbonEngineJS keeps the format/resource layer reusable by
472
- stopping before GPU work:
473
-
474
- - `runtime-resource` selects and runs registered non-shader readers, then can
475
- directly hydrate the requested runtime class or return another requested
476
- outcome.
477
- - `runtime-resource` stores lifecycle state, cache entries, and loaded object
478
- payloads. Plain reader results are normally transient prepare inputs; the
479
- resource or adapter retains only what it requires, by reference or by
480
- explicit copy.
481
- - Frozen standalone non-shader `format-*` packages remain compatibility
482
- distributions. GR2 and all shader formats remain separate packages for now.
483
- - engine packages create WebGL/WebGPU textures, buffers, shader modules,
484
- pipelines, and bind groups from loaded resources.
485
-
486
- This gives us the Carbon lifecycle shape without forcing WebGL/WebGPU imports or
487
- device decisions into `runtime-resource`.
1
+ # Runtime Resource Lifecycle
2
+
3
+ This note records how Carbon/ccpwgl resource loading maps to
4
+ `runtime-resource`.
5
+
6
+ ## Carbon and ccpwgl
7
+
8
+ Carbon's resource model separates load from prepare. The source schemas expose
9
+ resource classes through `BlueAsyncRes`, and Carbon notes distinguish background
10
+ load work from main-thread/device prepare work.
11
+
12
+ ccpwgl makes that split visible in `Tw2Resource`:
13
+
14
+ ```text
15
+ NO_INIT -> REQUESTED -> LOADED -> PREPARED
16
+ ```
17
+
18
+ Additional terminal or cleanup states include `ERROR`, `UNLOADED`, and `PURGED`.
19
+
20
+ The important behavior is:
21
+
22
+ - `Tw2ResMan.LoadResource()` requests a resource.
23
+ - The raw fetch resolves.
24
+ - `Tw2Resource.OnLoaded()` marks bytes or source data as loaded.
25
+ - The resource is queued for prepare.
26
+ - The manager tick later calls `res.Prepare(data)`.
27
+ - The concrete resource calls `OnPrepared()` after prepare work succeeds.
28
+
29
+ Some ccpwgl concrete `Prepare()` implementations also create WebGL objects.
30
+ That is a historical engine/runtime coupling, not the boundary we want in
31
+ CarbonEngineJS.
32
+
33
+ ## CarbonEngineJS
34
+
35
+ `runtime-resource` owns the GPU-free half:
36
+
37
+ ```text
38
+ EMPTY -> REQUESTED/LOADING -> LOADED
39
+ ```
40
+
41
+ Engine adapters own device realization:
42
+
43
+ ```text
44
+ LOADED -> PREPARING -> PREPARED
45
+ ```
46
+
47
+ Current state meanings:
48
+
49
+ - `EMPTY`: resource identity exists, but no payload has been read.
50
+ - `REQUESTED`: the resource is waiting on a queued or shared source load.
51
+ - `LOADING`: source bytes are available and CPU reader/format work is active.
52
+ - `LOADED`: CPU payload or hydrated object graph exists.
53
+ - `PREPARING`: an engine adapter is realizing backend-owned resources.
54
+ - `PREPARED`: preparation completed successfully and the resource is usable.
55
+ - `FAILED`: CPU loading, conversion, validation, or publication failed before a valid payload was published.
56
+ - `UNLOADED`: resource payload was released.
57
+ - `PURGED`: an inactivity or recorded-byte cache policy evicted the resource
58
+ from active ownership. Ordinary replacement, `Delete()`, `Clear()`,
59
+ `ClearCached()`, and shutdown clean owned payloads/adapters but preserve the
60
+ detached handle's last valid state.
61
+
62
+ `CjsResMan.LoadObject()` now queues one deduplicated background source operation
63
+ per source/path and limits active source operations with `maxConcurrentLoads`.
64
+ After bytes arrive, object construction is split into separate main-queue
65
+ items:
66
+
67
+ ```text
68
+ reader/format conversion -> publish
69
+ ```
70
+
71
+ `maxPrepareTime` limits seconds spent starting synchronous main-queue work in
72
+ one pump, and `maxPrepareItemsPerTick` can add a count limit. Promise-returning
73
+ format work remains in flight without blocking the JavaScript event loop.
74
+ Publication moves the resource to `LOADED`, then stops. ResMan never performs
75
+ backend realization or marks the resource `PREPARED`/`GOOD`.
76
+
77
+ Format classes own conversion to the promised CPU output. Backend realization
78
+ is a separate explicit engine operation after publication. A realization
79
+ failure destroys its candidate and returns a current resource to `LOADED`
80
+ without discarding the valid CPU payload; an existing usable adapter may remain
81
+ `PREPARED`.
82
+
83
+ Every requested handle uses a normalized source path plus one promised output
84
+ tag. `variant` is explicit; `emit`, `requirement`, and `payload` are fallbacks.
85
+ The selected constructor, reader, and format defaults/options are setup-time
86
+ execution details, not MotherLode identity. A changed
87
+ registration does not create a hidden second resource; reset the affected
88
+ identity or create a new manager. A changed output contract uses a new tag.
89
+
90
+ Output matching is case-insensitive, then the canonical declaration spelling
91
+ is passed to the format reader. Direct object loaders expose only their
92
+ unforced default; multiple named outputs belong on a format class. Unsupported
93
+ output is rejected before a MotherLode cache hit can return.
94
+
95
+ Releasing an unlocked CPU payload keeps the lightweight handle and a small
96
+ reconstruction request containing its path/output and source provenance. Its
97
+ promised-output fields and retained source provenance remain pinned during
98
+ `Ready()`/`GetObject()` so another output/source cannot silently replace the
99
+ old identity. `Ready()` may re-read and rebuild it through current
100
+ registrations. Queries, lease renewal, sweeps, and adapter cleanup never
101
+ trigger that read. Engine adapters own their backend allocations and may
102
+ release those independently.
103
+
104
+ The Blue method names remain the public queue vocabulary: `AddToQueue`,
105
+ `CancelFromQueue`, `GetNextIdForQueue`, `PumpMainThreadQueue`, `PauseQueue`,
106
+ `ResumeQueue`, `GetPendingLoads`, and `GetPendingPrepares`. `Update()`/`Tick()`
107
+ pump queues. `Wait()` snapshots queued resource-operation roots and already
108
+ submitted low-level queue tasks before its first await. Captured roots remain
109
+ open through publication enqueued after an asynchronous read, failure, and
110
+ lock release; later unrelated roots/tasks are excluded. Failure and queued
111
+ cancellation cross the fence without making `Wait()` reject.
112
+
113
+ The default wait pumps only the two queues, never automatic retention sweeps,
114
+ and honors existing pause state and budgets. `{ pump: false }` requires an
115
+ external driver. A standalone canonical `PrepareResourceObjectQueued()` call
116
+ opens a queued root. Direct `LoadResourceObject()`, direct
117
+ `PrepareResourceObject()`, and standalone source/format reads bypass both
118
+ queues and are outside the fence. They are still tracked as active mutations
119
+ so synchronous MotherLode replacement cannot detach them. `WaitUrgent()`
120
+ remains open until real per-item priority and urgent membership exist.
121
+
122
+ ## CjsLibrary resource-path workflow
123
+
124
+ The normal direct resource-path flow is:
125
+
126
+ ```text
127
+ Application / runtime object
128
+ |
129
+ | requests "res:/model/ship.gr2"
130
+ | with optional per-request overrides
131
+ v
132
+ +-----------------------------------------------+
133
+ | CjsLibrary |
134
+ | |
135
+ | - starts from registered default behavior |
136
+ | - considers registered capability reports |
137
+ | - chooses requirement / emit / format |
138
+ | - applies explicit request overrides |
139
+ +-----------------------------------------------+
140
+ |
141
+ | path + resolved request options
142
+ v
143
+ +-----------------------------------------------+
144
+ | CjsResMan.GetResource(path, options) |
145
+ | |
146
+ | - normalize source path and extension |
147
+ | - resolve the promised output tag |
148
+ +-----------------------------------------------+
149
+ |
150
+ v
151
+ +-----------------------------------------------+
152
+ | CjsMotherLode.Lookup(resolved key) |
153
+ +-----------------------------------------------+
154
+ |
155
+ +--- cache hit ------------------------------+
156
+ | |
157
+ | reuse the CjsResource and any active |
158
+ | load/build operation |
159
+ | |
160
+ `--- cache miss -----------------------------+
161
+ | |
162
+ | resolve class from requirement |
163
+ | construct + Initialize() |
164
+ | insert into CjsMotherLode |
165
+ v |
166
+ new CjsResource -------------------------------+
167
+ |
168
+ | Ready() / GetObject()
169
+ v
170
+ +===============================================+
171
+ | BACKGROUND LOAD QUEUE |
172
+ | |
173
+ | - mark resource REQUESTED |
174
+ | - share one source operation per source/path |
175
+ | - obey maxConcurrentLoads |
176
+ | - source.Read(path) |
177
+ +===============================================+
178
+ |
179
+ | source bytes
180
+ v
181
+ +===============================================+
182
+ | MAIN PREPARE QUEUE |
183
+ | |
184
+ | every box below is a separately budgeted item |
185
+ +===============================================+
186
+ |
187
+ v
188
+ +-----------------------------------------------+
189
+ | Read stage |
190
+ | |
191
+ | current object loader for extension? |
192
+ | yes -> call it |
193
+ | no -> resolve registered formats by |
194
+ | bytes + request options |
195
+ +-----------------------------------------------+
196
+ |
197
+ | plain payload / hydrated object
198
+ v
199
+ +-----------------------------------------------+
200
+ | Publish stage |
201
+ | |
202
+ | semantic resource -> SetPayload(payload) |
203
+ | generic resource -> SetPayload(value) |
204
+ | object aliases payload |
205
+ +-----------------------------------------------+
206
+ |
207
+ +--- validation failure --------------------+
208
+ | |
209
+ | resource -> FAILED |
210
+ | Ready() rejects |
211
+ | |
212
+ `--- publication succeeds -----------------+
213
+ |
214
+ | resource -> LOADED
215
+ v
216
+ CjsLibrary returns
217
+ the built CjsResource/object
218
+ ```
219
+
220
+ Device realization is a separate continuation selected outside ResMan. It can
221
+ run again after adapter eviction or device loss while the CPU payload remains
222
+ resident:
223
+
224
+ ```text
225
+ CjsResource LOADED
226
+ |
227
+ | selected engine adapter
228
+ v
229
+ PREPARING
230
+ |
231
+ | create candidate -> verify current target -> synchronous attach
232
+ v
233
+ PREPARED
234
+
235
+ failure: destroy candidate -> LOADED (CPU payload retained)
236
+ ```
237
+
238
+ ## Texture Array Generations
239
+
240
+ `CjsTextureArrayRes` is a derived multi-source resource with an explicit
241
+ requested/prepared revision boundary:
242
+
243
+ ```text
244
+ proxy/source change
245
+ -> requested revision + dirty layer
246
+ -> one scheduled next-frame snapshot
247
+ -> consumed/in-flight request
248
+ -> adapter candidate preparation
249
+ -> guarded adapter + prepared-revision publication
250
+ ```
251
+
252
+ `ConsumeUpdateRequest()` produces an immutable snapshot and marks that revision
253
+ in flight. A current consumed revision may be completed through
254
+ `CommitPreparedAdapterRevision()`, failed through `FailUpdateRequest()`, or
255
+ returned to the queue through `RetryUpdateRequest()`. Commit-before-consume and
256
+ stale commits are rejected; rejected candidate allocations are destroyed by
257
+ default.
258
+
259
+ Publication installs the adapter allocation and prepared revision before
260
+ completion events run. The result returns the displaced allocation to the
261
+ adapter owner for post-publication destruction. A reentrant source change may
262
+ therefore request a newer revision without allowing stale completion to replace
263
+ it. The previous prepared allocation and `IsGood()` remain usable while a
264
+ replacement is pending or if replacement preparation fails.
265
+
266
+ `Ready()` is specialized for this derived resource: it resolves when the
267
+ generation requested at call time has been published, rather than delegating
268
+ to a single-source object loader. Initial preparation failure rejects it.
269
+
270
+ Logical paths and resolved sources are independent. Attaching a redirected or
271
+ LOD-specific source does not rewrite persistence. `TouchLayer()` represents an
272
+ in-place source revision, and `HandleAdapterLoss()` invalidates the complete
273
+ topology after destroying an unusable adapter allocation. Topology snapshots
274
+ report an explicit `topologyChanged` flag and contain only valid current dirty
275
+ layer indices.
276
+
277
+ ## Optional Runtime Manager Follow-up
278
+
279
+ The runtime-resource 0.6.0 ResMan/MotherLode contract is complete. Future
280
+ work should be driven by measured application needs rather than another
281
+ resource preparation abstraction:
282
+
283
+ - main-queue priority and starvation policy
284
+ - cancellation/abort propagation for work that has already started
285
+ - `WaitUrgent()` after real priority and bounded fairness
286
+ - queue-time and reader/format-time telemetry
287
+ - application-level default retention policy selection
288
+ - automatic resource/payload byte estimation and separate CPU/adapter budgets
289
+ - browser-aware source behavior such as fetch response type selection
290
+
291
+ Explicit `Ready()`/`GetObject()` reconstructs released CPU payloads. Backend
292
+ device-loss recovery belongs to the engine's realization operation.
293
+
294
+ ## Memory Retention and Purging
295
+
296
+ ccpwgl keeps every resource in `Tw2MotherLode` until it is explicitly cleared or
297
+ auto-purged. Each resource has:
298
+
299
+ - `activeFrame`: last frame the resource was considered in use.
300
+ - `doNotPurge`: a lock counter/flag for resources that must stay resident.
301
+ - `KeepAlive()`: updates `activeFrame` and reloads an unloaded/purged resource
302
+ if it is touched again.
303
+
304
+ The manager advances an `activeFrame` counter on a cadence, and
305
+ `Tw2MotherLode.PurgeInactive()` unloads resources whose `activeFrame` is old
306
+ enough. It then marks them purged and removes them from the cache. In ccpwgl,
307
+ `IsGood()` also calls `KeepAlive()`, so many read/check paths implicitly keep
308
+ resources resident.
309
+
310
+ That model works for ccpwgl, but CarbonEngineJS is deliberately more explicit:
311
+
312
+ - `IsGood()`, `IsPrepared()`, and `HasLoaded()` remain pure state checks.
313
+ - `KeepAlive()` and `KeepPayloadAlive()` are the explicit liveness operations.
314
+ - `Lock()` / `Unlock()` maintain a non-underflowing count and prevent identity
315
+ and payload eviction during a sweep.
316
+ - MotherLode tracks separate identity and CPU-payload frame/time observations.
317
+ - `CjsMotherLode.PurgeInactive()` itself scans only when explicitly requested
318
+ and never infers JavaScript reachability. `CjsResMan.Update()` may request it
319
+ only under an explicitly configured automatic policy.
320
+ - `Unload()` should release engine adapter resources and optionally CPU payloads.
321
+ - A resource-level `Purge()`/`Reload()` vocabulary remains future policy work.
322
+
323
+ ### Recorded-Byte Cache Contract
324
+
325
+ Carbon can infer when only its cache retains a resource through weak-reference
326
+ and refcount transitions. JavaScript cannot reproduce that ownership test
327
+ reliably, so `CjsMotherLode` budgets only entries that a caller explicitly
328
+ classifies with `{ cached: true, bytes }`. The byte value is an exact
329
+ caller-supplied safe-integer eviction weight; runtime-resource does not walk
330
+ arbitrary cyclic/shared object graphs or invoke payload getters to guess size.
331
+
332
+ Explicit cached entries receive a monotonic admission sequence.
333
+ With its default cleanup, `TrimCache(options)` destroys adapters, releases
334
+ payloads, detaches lifecycle callbacks, marks compatible handles `PURGED`, and
335
+ removes positive-byte entries oldest-first until `cacheBytes <= cacheSize`.
336
+ Live, locked, non-cacheable, and zero-byte entries do not create byte pressure.
337
+ `KeepAlive()` and `Lock()` promote a cached record to live; `Unlock()` does not
338
+ silently re-admit it.
339
+
340
+ `SetCacheSize(bytes, options)` installs and immediately enforces the new budget.
341
+ `CjsResMan.Update()` / `Tick()` also run cache housekeeping after queue pumping;
342
+ `{ cache: false }` skips one update without changing policy. Cleanup is
343
+ transactional per identity: a failed candidate remains canonical, later
344
+ candidates are still attempted, and the aggregate
345
+ `CJS_MOTHERLODE_CACHE_TRIM_FAILED` error carries successful evictions and any
346
+ remaining over-budget state. Trimming never reads, prepares, or reloads data.
347
+
348
+ The JS/browser split adds one more axis that ccpwgl blurs: CPU payload memory and
349
+ GPU/device memory are different budgets. A large decoded image, geometry buffer,
350
+ or shader graph can be expensive even before an engine adapter prepares it.
351
+ Runtime-resource should therefore support separate retention policy for:
352
+
353
+ - resource identity: path, extension, state, error summary, lightweight metadata.
354
+ - CPU payload: plain reader/converter objects, hydrated object graphs, decoded
355
+ typed arrays.
356
+ - adapter payload: WebGL/WebGPU textures, buffers, shader modules, pipelines.
357
+
358
+ A sane default would keep resource identity and lightweight metadata while
359
+ allowing CPU payloads and adapter payloads to be released independently. Engine
360
+ adapters should own adapter-resource destruction, but `runtime-resource` can
361
+ provide lifecycle hooks and opaque adapter slots so the adapter has a consistent
362
+ place to clean up.
363
+
364
+ ### Payload Retention Contract
365
+
366
+ Reader and converter outputs are plain transient payload objects, not resource
367
+ classes or DTO models. A payload may contain more decoded data than a particular
368
+ resource or engine adapter needs. Each concrete resource validates the fields
369
+ it requires before publishing the payload and retains the scalars and references
370
+ it needs. An adapter may retain additional references in adapter-owned state.
371
+ Referencing payload-owned typed arrays is valid and preferable to copying them
372
+ merely to change ownership.
373
+
374
+ The lifecycle treats resource residency and payload residency independently:
375
+
376
+ ```text
377
+ resource.KeepAlive()
378
+ -> renew resource/cache residency
379
+
380
+ resource.KeepPayloadAlive()
381
+ -> renew the attached payload lease
382
+
383
+ resource.ReleasePayload()
384
+ -> explicitly release the full payload reference
385
+ ```
386
+
387
+ `CjsResMan` binds resource-facing `KeepAlive()`, `KeepPayloadAlive()`, `Lock()`,
388
+ and `Unlock()` to the resource's canonical MotherLode key. `SetPayload()` renews
389
+ both identity and payload activity when it publishes a non-null payload.
390
+ `GetPayload()` and `HasPayload()` are pure queries; reading the payload does not
391
+ implicitly renew its lease. Detached and purged handles retain deterministic
392
+ no-op liveness methods rather than silently starting work.
393
+
394
+ Both semantic and generic/base resource results use that payload slot. A base
395
+ resource also mirrors its payload on the compatibility `object` property;
396
+ `ReleasePayload()` clears the alias only when it still references that exact
397
+ payload. Semantic resources continue to expose `object === resource` while
398
+ holding their validated plain payload privately.
399
+
400
+ Object-operation promises are retained only while in flight. Concurrent
401
+ `GetObject()`/`Ready()` calls share one operation; a resident result is returned
402
+ without source work and renews the explicit payload lease. Settlement removes
403
+ the operation record so its result graph can be reclaimed and a failure can be
404
+ retried. After payload release, only a new explicit object/readiness call
405
+ reconstructs it; queries, lease calls, and purge sweeps do not.
406
+
407
+ Read-cache provenance is explicit and separate from path/output resource identity.
408
+ For a selected source object and normalized path, `sourceRevision` is an opaque
409
+ caller/source-supplied string or finite-number content token. Source and format
410
+ records do not share across revisions. Format records are additionally isolated
411
+ by source object, frozen registration descriptor, and effective format options,
412
+ so another source or a re-registered default cannot reuse a stale parse.
413
+ Registered defaults are deeply snapshotted for supported plain-object/array
414
+ configuration. Requests containing option values that cannot be represented
415
+ safely bypass parsed-format sharing instead of accepting an ambiguous key.
416
+
417
+ `cacheSource` and `cacheFormat` use the same per-call tri-state contract:
418
+ omitted shares existing in-flight/retained work but drops a newly completed
419
+ record; `true` retains success and upgrades joined work; `false` bypasses both
420
+ sharing and retention. Failures are never retained. Resource loaders capture
421
+ the effective selected source (including the creation-time manager default)
422
+ and revision for later reconstruction, but do not capture cache flags or
423
+ one-shot reload.
424
+
425
+ `reload: true` detaches all queued/source/format records for the selected
426
+ source/path when fresh candidate work begins. `InvalidateReadCache()` exposes
427
+ explicit path invalidation, optionally restricted to one revision. Detachment
428
+ never aborts or rejects existing consumers and never touches MotherLode or the
429
+ already-published canonical payload. `Delete()` remains resource-identity-only;
430
+ `Clear()` resets all read ledgers. A failed reload preserves the former payload
431
+ but does not restore reusable read-cache entries detached by its explicit
432
+ freshness request.
433
+
434
+ ### Candidate-First Atomic Reload
435
+
436
+ When an owner already exists, `GetResource(path, { reload: true })` constructs
437
+ and initializes a distinct off-registry candidate. Ordinary `Lookup()` remains
438
+ on the former resource. Calling `Ready()` on the candidate, using
439
+ `GetObject()` / `FetchResource()` with `reload: true`, or calling the explicit
440
+ `ReloadObject()` / `ReloadResource()` helpers starts one shared candidate
441
+ operation.
442
+
443
+ The manager captures the exact MotherLode, key, former handle, former ownership
444
+ generation, and a newest-request token. It purge-locks the former owner and
445
+ tracks the candidate as a normal `Wait()` root. Reader, prepare, and publication
446
+ stages mutate only the detached candidate and validate candidate authority
447
+ before and after asynchronous boundaries. A fully loaded CPU candidate commits
448
+ through `CjsMotherLode.ReplaceExpected()` only if the exact former owner and
449
+ newest token still match. The final authority callback and exact-record check
450
+ run immediately before the synchronous map switch, with no user cleanup or
451
+ `await` between the comparison and publication.
452
+
453
+ After the switch, the displaced ownership generation is invalidated, the
454
+ candidate receives ordinary lifecycle/reconstruction callbacks, and the former
455
+ handle is cleaned exactly once. Existing JavaScript references are not
456
+ retargeted; fresh lookup returns the new handle. Source, format, or publication
457
+ failure leaves the exact former state, payload, and adapters canonical, retains
458
+ its original error, and cleans payload/adapters attached to the never-canonical
459
+ candidate. An otherwise-successful candidate that was superseded, deleted,
460
+ cleared, or replaced rejects with `CJS_RESMAN_STALE_RELOAD_CANDIDATE` and cannot
461
+ resurrect the key. Constructors that return the former singleton are rejected
462
+ before `Initialize()` can mutate it because staging requires a distinct handle.
463
+
464
+ Cleanup errors have explicit sides. Candidate cleanup failure aggregates with
465
+ the original preparation/stale error as
466
+ `CJS_RESMAN_RELOAD_CANDIDATE_CLEANUP_FAILED` while the former owner remains
467
+ canonical. A displaced-owner cleanup failure occurs after publication and
468
+ rejects as `CJS_MOTHERLODE_REPLACE_CLEANUP_FAILED` with a result where
469
+ `committed === true`; the already-good candidate remains canonical.
470
+ Started source/format work abort remains separate work.
471
+
472
+ This availability contract intentionally differs from Carbon.
473
+ `BlueAsyncRes::Reload` cancels/joins work, releases dependent cached data, and
474
+ reloads the same canonical object in place; failure can therefore leave that
475
+ stable handle bad. Carbon MotherLode replacement also switches immediately and
476
+ has no prepare-success gate or rollback. Runtime-resource instead preserves the
477
+ last published good handle until a distinct candidate has succeeded.
478
+
479
+ Every canonical queued/direct/standalone preparation captures an immutable
480
+ publication authority: exact MotherLode, canonical key, resource handle, and a
481
+ manager-local ownership generation. The manager validates that authority
482
+ before and after state changes, asynchronous reader/format work, and publication.
483
+ Delete, Clear, successful reload commit, or exact-handle reinsertion therefore makes
484
+ older work reject with `CJS_RESMAN_STALE_RESOURCE_OPERATION` before it can
485
+ publish. If stale work independently rejects, its original source/format error
486
+ is preserved and `SetError()` is suppressed on the detached handle.
487
+
488
+ `Register({ motherLode })` rejects with
489
+ `CJS_RESMAN_ACTIVE_RESOURCE_OPERATIONS` while queued or direct mutations are
490
+ active, including a reload candidate. Normal `Wait()` drains queued roots and
491
+ candidate lineages; a direct caller must await its own load/prepare promise
492
+ before retrying replacement. Canonical and candidate authority prevent late
493
+ publication, but started-work abort remains separate. Backend candidate
494
+ cleanup belongs to the explicit engine realization operation.
495
+
496
+ An explicit `PurgeInactive()` sweep accepts separate frame/time limits for
497
+ identity and payload residency. Identity expiry destroys adapter resources,
498
+ releases the payload, detaches lifecycle callbacks, marks compatible handles
499
+ `PURGED`, and removes the canonical key. Payload expiry calls
500
+ `ReleasePayload()` while retaining identity and adapter allocations. Locks skip
501
+ both operations, and candidate failures are aggregated after the sweep has
502
+ continued over other entries.
503
+
504
+ `CjsResMan` also exposes opt-in automatic scheduling through
505
+ `SetAutoPurgePolicy()` or the `autoPurgePolicy` registration option. It is
506
+ disabled by default. Automatic policy is time-only because MotherLode's current
507
+ activity frame counts observations rather than renderer frames. A policy must
508
+ set at least one of `maxIdleMilliseconds` or
509
+ `payloadMaxIdleMilliseconds`; `intervalMilliseconds` defaults to 1000. The
510
+ first `PumpAutoPurge()`/`Update()` after configuration sweeps immediately, then
511
+ the interval sets the minimum cadence. `Update({ purge: false })` suppresses a
512
+ sweep for one update without changing cadence. A regressing clock rebases and
513
+ skips one pump; custom deterministic clocks should be shared with MotherLode.
514
+ Recorded-byte cache trimming is separate from this opt-in inactivity policy and
515
+ runs on ordinary updates unless `{ cache: false }` is supplied.
516
+
517
+ Both queued `QueueResourceObject()` work and direct `LoadResourceObject()` work
518
+ hold one manager-owned lock from request/loading publication through success or
519
+ failure. The lock is balanced independently of caller locks, so automatic or
520
+ manual sweeps cannot detach a handle while its read/prepare operation is still
521
+ active. Lock release is conditional on the same captured ownership generation,
522
+ so stale work cannot decrement a newly rebound handle's lock. Scheduling and
523
+ active-work protection do not fetch or reload data.
524
+
525
+ The processor preparing a resource decides when the full payload can be
526
+ released:
527
+
528
+ ```text
529
+ format reader -> plain payload -> resource validation + adapter prepare
530
+ |
531
+ +-> resource retains required values/references
532
+ +-> adapter retains adapter-specific state/references
533
+ +-> release payload after successful preparation
534
+ `-> or renew its lease for deferred/further work
535
+ ```
536
+
537
+ A time- or frame-based lease is a fallback against abandoned payloads. An owner
538
+ performing deferred work can renew the lease. If an expired payload is required
539
+ again, the caller must explicitly request reconstruction; lease renewal and
540
+ purging never fetch or reload source data. Dynamic or non-reloadable resources
541
+ must remain locked, retain the required payload, or be able to recreate it.
542
+
543
+ Payload references are shared read-only by default. Preparing WebGL and WebGPU
544
+ adapters side by side should normally pass the same payload to both consumers
545
+ and retain it until both have finished. Copying is an explicit consumer
546
+ operation, justified when a consumer must mutate data, transfer and detach an
547
+ `ArrayBuffer`, or retain an independently writable snapshot. The consumer
548
+ should copy only the fields it requires; runtime-resource should not
549
+ automatically deep-clone entire payload or typed-array bundles. Any full-copy
550
+ operation should be format-aware rather than a generic resource-side clone.
551
+
552
+ Open design questions:
553
+
554
+ - Should `Unload()` drop only adapter payloads by default, or CPU payloads too?
555
+ - Should there be explicit `UnloadAdapterResources()`, `UnloadPayload()`, and
556
+ `Purge()` phases?
557
+ - Which format/resource-specific estimators should supply separate identity,
558
+ CPU-payload, and adapter byte weights without double-counting shared buffers?
559
+ - Should manually attached/dynamic resources default to locked, like ccpwgl's
560
+ manual shader resources use `doNotPurge`?
561
+ - What explicit `Reload()`/reconstruction API should restore purged resources
562
+ without introducing surprising browser or network work?
563
+
564
+ ccpwgl's raw event emitter shape is useful. The part we should not copy is the
565
+ separate resource notification/callback compatibility layer that sits beside
566
+ events. CarbonEngineJS exposes one small event-emitter API and avoids short
567
+ generic names such as `On`, `Once`, `Off`, and `Emit` on Carbon-shaped classes.
568
+ `CjsEventEmitter` is a separate base class so non-model runtime services can
569
+ extend it without extending `CjsModel`; `CjsResMan` already uses that path.
570
+
571
+ The API stays deliberately direct:
572
+
573
+ - `AddEvents(events)`
574
+ - `OnEvent(eventName, listener, source?)`
575
+ - `OnceEvent(eventName, listener, source?)`
576
+ - `OffEvent(eventName = "*", listener?, source?)`
577
+ - `EmitEvent(eventName, ...args)`
578
+ - `HasEvent(eventName = "*", listener?, source?)`
579
+ - `ClearEvent(eventName = "*")`
580
+ - `GetEventNames()` and `GetEventListenerCount(eventName = "*")`
581
+
582
+ The optional `source` is the callback's `this` value and an explicit matching
583
+ identity for removal. Mutating event methods return the emitter for chaining.
584
+ There are no listener scopes, subscription handles, owner-side `ListenTo()`
585
+ helpers, or parallel resource notification callbacks.
586
+
587
+ Event names are normalized to lowercase and dispatched by exact match. Resource
588
+ classes may emit their own state or domain events, but the emitter does not
589
+ invent a resource lifecycle contract.
590
+
591
+ Event memory rules matter as much as event names. Event storage is the optional
592
+ `events` member of the emitter's non-enumerable `__state` object. It is created
593
+ only when the first listener is registered and deleted when the last record is
594
+ removed. That does not make listeners weak. As long as an emitter is reachable,
595
+ its event map strongly references listener functions and sources, and those
596
+ listeners can keep whole scene/resource graphs alive.
597
+
598
+ CarbonEngineJS event emitters should therefore follow these rules:
599
+
600
+ - `OnceEvent()` should remove the listener before or immediately after the first
601
+ callback, even if the callback throws.
602
+ - `OffEvent(eventName, listener, source)` must remove the exact listener/source
603
+ entry.
604
+ - an external party that no longer observes a target must call `OffEvent()`;
605
+ `target.OffEvent("*", null, source)` removes all of that source's records.
606
+ - `Unload()` and `Purge()` are resource state/cache operations, not an implied
607
+ listener-destruction lifecycle.
608
+ - `OnEvent()` returns the emitter, ccpwgl-style. It does not return unsubscribe
609
+ closures because those closures create another reference path.
610
+ - event payload history is not stored.
611
+ - we should prefer deterministic cleanup over `WeakRef`/`FinalizationRegistry`;
612
+ those can help diagnostics, but they are not a lifecycle contract.
613
+
614
+ The target is "easy to debug, hard to leak": clear ownership of who subscribed,
615
+ who unsubscribes, and which cleanup phase clears all remaining listeners.
616
+
617
+ The event data model is deliberately small:
618
+
619
+ ```text
620
+ emitter.__state (non-enumerable, allocated only when some subsystem needs it)
621
+ events -> eventName -> Set<listenerRecord>
622
+
623
+ listenerRecord
624
+ emitter
625
+ eventName
626
+ listener
627
+ source
628
+ once
629
+ ```
630
+
631
+ The event map is allocated only when the first listener is registered. If the
632
+ emitter becomes unreachable, its state and event records can be collected with
633
+ it. While the emitter remains reachable, listener functions and sources are
634
+ strongly retained until `OffEvent()`, `ClearEvent()`, once dispatch, or emitter
635
+ collection. Multiple listeners on the same event are allowed because each event
636
+ bucket is a set of records. A raw `CjsEventEmitter` does not gain model-owned
637
+ `dirty` or `rebuild` state.
638
+
639
+ This is as far as the event system should go for now:
640
+
641
+ - lowercase exact event names only.
642
+ - no wildcard listener dispatch.
643
+ - wildcard names are accepted only by lookup and cleanup methods.
644
+ - no listener scope or subscription-object layer.
645
+ - no separate resource notification/callback compatibility layer.
646
+ - no `family.event` or ancestor routing.
647
+ - no event history by default.
648
+ - no global master event manager.
649
+ - debugging introspection should stay limited to counts and names unless a real
650
+ use case appears.
651
+
652
+ `CjsModel` has dirty-state helpers (`MarkDirty`, `ClearDirty`, `ConsumeDirty`,
653
+ `GetDirtyNotifications`) for model invalidation. `SetValues()` compares incoming
654
+ values with the current field values and only marks dirty when a value actually
655
+ changes. A plain `MarkDirty()` means broad dirty invalidation; it does not request
656
+ a rebuild. Deferred rebuild reasons belong to the independent
657
+ `model.__state.rebuild` set. This is not a resource lifecycle event system.
658
+ Resource lifecycle events should therefore remain a resource/resman concern.
659
+
660
+ ## Why We Diverge
661
+
662
+ The Carbon and ccpwgl resource classes live inside an engine that can prepare
663
+ GPU objects directly. CarbonEngineJS keeps the format/resource layer reusable by
664
+ stopping before GPU work:
665
+
666
+ - `runtime-resource` selects and runs registered non-shader readers, then can
667
+ directly hydrate the requested runtime class or return another requested
668
+ outcome.
669
+ - `runtime-resource` stores lifecycle state, cache entries, and loaded object
670
+ payloads. Plain reader results are normally transient prepare inputs; the
671
+ resource or adapter retains only what it requires, by reference or by
672
+ explicit copy.
673
+ - Frozen standalone non-shader `format-*` packages remain compatibility
674
+ distributions. GR2 and all shader formats remain separate packages for now.
675
+ - engine packages create WebGL/WebGPU textures, buffers, shader modules,
676
+ pipelines, and bind groups from loaded resources.
677
+
678
+ This gives us the Carbon lifecycle shape without forcing WebGL/WebGPU imports or
679
+ device decisions into `runtime-resource`.