@carbonenginejs/runtime-resource 0.13.0 → 0.15.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 (454) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +32 -32
  3. package/README.md +129 -129
  4. package/dist/CjsMotherLode.js +1271 -1271
  5. package/dist/CjsResMan.js +3858 -3858
  6. package/dist/CjsResManFetchProvider.js +94 -94
  7. package/dist/CjsResManWorkQueue.js +301 -301
  8. package/dist/_virtual/_rollupPluginBabelHelpers.js +150 -150
  9. package/dist/format/CjsBlueReader.js +358 -358
  10. package/dist/format/CjsByteReader.js +310 -310
  11. package/dist/format/CjsByteWriter.js +242 -242
  12. package/dist/format/CjsFormat.js +223 -223
  13. package/dist/format/CjsFormatError.js +41 -41
  14. package/dist/format/CjsReader.js +22 -22
  15. package/dist/format/CjsResourceProbe.js +279 -279
  16. package/dist/format/CjsStringTable.js +268 -268
  17. package/dist/format/carbonEffect/CjsCarbonEffectReader.js +265 -265
  18. package/dist/format/carbonEffect/CjsCarbonEffectWriter.js +373 -373
  19. package/dist/format/carbonEffect/buildCarbonEffectContainer.js +182 -182
  20. package/dist/format/carbonEffect/carbonEffectBackendBlock.js +321 -321
  21. package/dist/format/carbonEffect/carbonEffectRecords.js +1147 -1147
  22. package/dist/format/carbonEffect/carbonEffectResourceTransform.js +197 -197
  23. package/dist/format/compareUtf8.js +36 -36
  24. package/dist/format/effect/effectBodyInventory.js +146 -146
  25. package/dist/format/effect/effectPermutationGraph.js +257 -257
  26. package/dist/format/effect/sha256.js +114 -114
  27. package/dist/format/index.js +11 -11
  28. package/dist/format/payloadContract.js +193 -193
  29. package/dist/formats/black/CjsBlackFormat.js +310 -310
  30. package/dist/formats/black/core/CjsBlackBinaryReader.js +260 -260
  31. package/dist/formats/black/core/CjsBlackPropertyReaders.js +448 -448
  32. package/dist/formats/black/core/CjsBlackReader.js +764 -764
  33. package/dist/formats/black/core/CjsBlackSchemaRegistry.js +540 -540
  34. package/dist/formats/black/core/black-schema-v1-2026-07-23.json.js +4 -4
  35. package/dist/formats/black/core/blackConstants.js +7 -7
  36. package/dist/formats/black/core/blackDefinitions.js +10 -10
  37. package/dist/formats/black/core/blackEnums.js +6 -6
  38. package/dist/formats/black/core/blackSchema.js +3 -3
  39. package/dist/formats/black/core/blackVersion.js +22 -22
  40. package/dist/formats/black/core/helpers.js +199 -199
  41. package/dist/formats/black/core/schema.js +4 -4
  42. package/dist/formats/black/index.js +2 -2
  43. package/dist/formats/bnk/CjsBnkFormat.js +175 -175
  44. package/dist/formats/bnk/core/busNodes.js +252 -252
  45. package/dist/formats/bnk/core/effectNodes.js +147 -147
  46. package/dist/formats/bnk/core/eventAction.js +416 -416
  47. package/dist/formats/bnk/core/globalSettings.js +215 -215
  48. package/dist/formats/bnk/core/graph.js +137 -137
  49. package/dist/formats/bnk/core/helpers.js +512 -512
  50. package/dist/formats/bnk/core/musicNodes.js +540 -540
  51. package/dist/formats/bnk/core/nodeBase.js +553 -553
  52. package/dist/formats/bnk/core/sfxNodes.js +632 -632
  53. package/dist/formats/bnk/core/soundbanksInfo.js +209 -209
  54. package/dist/formats/bnk/index.js +2 -2
  55. package/dist/formats/cmf/CjsCmfFormat.js +497 -497
  56. package/dist/formats/cmf/core/binary.js +194 -194
  57. package/dist/formats/cmf/core/buffers.js +237 -237
  58. package/dist/formats/cmf/core/constants.js +47 -47
  59. package/dist/formats/cmf/core/gr2Anim.js +453 -453
  60. package/dist/formats/cmf/core/helpers.js +318 -318
  61. package/dist/formats/cmf/core/pack.js +276 -276
  62. package/dist/formats/cmf/core/schema.js +374 -374
  63. package/dist/formats/cmf/core/shared.js +277 -277
  64. package/dist/formats/cmf/core/writer.js +571 -571
  65. package/dist/formats/cmf/index.js +2 -2
  66. package/dist/formats/dds/CjsDdsFormat.js +200 -200
  67. package/dist/formats/dds/core/bc6h.js +298 -298
  68. package/dist/formats/dds/core/bc7.js +272 -272
  69. package/dist/formats/dds/core/helpers.js +901 -862
  70. package/dist/formats/dds/core/helpers.js.map +1 -1
  71. package/dist/formats/dds/index.js +2 -2
  72. package/dist/formats/dxbc/CjsDxbcFormat.js +187 -142
  73. package/dist/formats/dxbc/CjsDxbcFormat.js.map +1 -1
  74. package/dist/formats/dxbc/core/DxbcReader.js +266 -266
  75. package/dist/formats/dxbc/core/container.js +169 -169
  76. package/dist/formats/dxbc/core/decoder.js +789 -789
  77. package/dist/formats/dxbc/core/disassemble.js +240 -0
  78. package/dist/formats/dxbc/core/disassemble.js.map +1 -0
  79. package/dist/formats/dxbc/core/errors.js +19 -19
  80. package/dist/formats/dxbc/core/helpers.js +220 -220
  81. package/dist/formats/dxbc/core/opcodes.js +45 -45
  82. package/dist/formats/dxbc/core/program.js +91 -91
  83. package/dist/formats/dxbc/core/signature.js +172 -172
  84. package/dist/formats/dxbc/index.js +2 -2
  85. package/dist/formats/fbx/CjsFbxFormat.js +266 -266
  86. package/dist/formats/fbx/core/helpers.js +3932 -3932
  87. package/dist/formats/fbx/index.js +2 -2
  88. package/dist/formats/flac/CjsFlacFormat.js +142 -142
  89. package/dist/formats/flac/core/helpers.js +315 -315
  90. package/dist/formats/flac/index.js +2 -2
  91. package/dist/formats/gif/CjsGifFormat.js +141 -141
  92. package/dist/formats/gif/core/helpers.js +380 -380
  93. package/dist/formats/gif/index.js +2 -2
  94. package/dist/formats/gltf/CjsGltfFormat.js +252 -290
  95. package/dist/formats/gltf/CjsGltfFormat.js.map +1 -1
  96. package/dist/formats/gltf/core/helpers.js +287 -307
  97. package/dist/formats/gltf/core/helpers.js.map +1 -1
  98. package/dist/formats/gltf/core/json.js +79 -79
  99. package/dist/formats/gltf/core/parser.js +679 -679
  100. package/dist/formats/gltf/core/targets.js +173 -173
  101. package/dist/formats/gltf/index.js +2 -2
  102. package/dist/formats/gr2/CjsGr2Format.js +289 -289
  103. package/dist/formats/gr2/core/bitknit2.js +282 -282
  104. package/dist/formats/gr2/core/curves.js +1047 -1047
  105. package/dist/formats/gr2/core/gsf.js +72 -72
  106. package/dist/formats/gr2/core/helpers.js +352 -352
  107. package/dist/formats/gr2/core/json.js +622 -622
  108. package/dist/formats/gr2/core/oodle1.js +388 -388
  109. package/dist/formats/gr2/core/tangents.js +48 -48
  110. package/dist/formats/gr2/core/targets.js +361 -361
  111. package/dist/formats/gr2/index.js +2 -2
  112. package/dist/formats/hlsl/CjsHlslFormat.js +233 -254
  113. package/dist/formats/hlsl/CjsHlslFormat.js.map +1 -1
  114. package/dist/formats/hlsl/core/HlslBinaryUtils.js +15 -15
  115. package/dist/formats/hlsl/core/HlslEffectReadError.js +19 -19
  116. package/dist/formats/hlsl/core/HlslEffectStateManager.js +130 -130
  117. package/dist/formats/hlsl/core/HlslRenderStateSetup.js +37 -37
  118. package/dist/formats/hlsl/core/HlslResourceSetDescription.js +94 -94
  119. package/dist/formats/hlsl/core/HlslShaderBytecode.js +44 -44
  120. package/dist/formats/hlsl/core/analysis.js +51 -51
  121. package/dist/formats/hlsl/core/carbonDescriptionToRuntime.js +850 -850
  122. package/dist/formats/hlsl/core/detailMapFamily.js +128 -128
  123. package/dist/formats/hlsl/core/helpers.js +270 -270
  124. package/dist/formats/hlsl/core/json.js +284 -284
  125. package/dist/formats/hlsl/core/localLightFamily.js +133 -133
  126. package/dist/formats/hlsl/core/metadata.js +327 -327
  127. package/dist/formats/hlsl/core/render-states.js +280 -280
  128. package/dist/formats/hlsl/core/tr2/HlslRenderContextEnum.js +43 -43
  129. package/dist/formats/hlsl/core/tr2/resources/HlslEffectRes.js +320 -320
  130. package/dist/formats/hlsl/core/tr2/resources/HlslShaderPermutation.js +33 -33
  131. package/dist/formats/hlsl/core/tr2/shader/HlslEffectBindingManifest.js +423 -416
  132. package/dist/formats/hlsl/core/tr2/shader/HlslEffectBindingManifest.js.map +1 -1
  133. package/dist/formats/hlsl/core/tr2/shader/HlslEffectConstant.js +40 -40
  134. package/dist/formats/hlsl/core/tr2/shader/HlslEffectDescription.js +62 -62
  135. package/dist/formats/hlsl/core/tr2/shader/HlslEffectLibrary.js +52 -52
  136. package/dist/formats/hlsl/core/tr2/shader/HlslEffectParameterAnnotation.js +38 -38
  137. package/dist/formats/hlsl/core/tr2/shader/HlslEffectResource.js +50 -50
  138. package/dist/formats/hlsl/core/tr2/shader/HlslEffectStageInput.js +86 -86
  139. package/dist/formats/hlsl/core/tr2/shader/HlslEffectTechnique.js +31 -31
  140. package/dist/formats/hlsl/core/tr2/shader/HlslPass.js +42 -42
  141. package/dist/formats/hlsl/core/tr2/shader/HlslSamplerDescription.js +55 -55
  142. package/dist/formats/hlsl/core/tr2/shader/HlslSamplerSetup.js +29 -29
  143. package/dist/formats/hlsl/core/tr2/shader/HlslShader.js +220 -220
  144. package/dist/formats/hlsl/core/tr2/shader/HlslShaderOption.js +30 -30
  145. package/dist/formats/hlsl/index.js +3 -3
  146. package/dist/formats/index.js +33 -33
  147. package/dist/formats/jpeg/CjsJpegFormat.js +212 -212
  148. package/dist/formats/jpeg/core/helpers.js +402 -402
  149. package/dist/formats/jpeg/core/jpeg.js +480 -480
  150. package/dist/formats/jpeg/index.js +2 -2
  151. package/dist/formats/mp3/CjsMp3Format.js +197 -197
  152. package/dist/formats/mp3/core/helpers.js +375 -375
  153. package/dist/formats/mp3/index.js +2 -2
  154. package/dist/formats/mp4/CjsMp4Format.js +197 -197
  155. package/dist/formats/mp4/core/helpers.js +484 -484
  156. package/dist/formats/mp4/index.js +2 -2
  157. package/dist/formats/obj/CjsObjFormat.js +232 -253
  158. package/dist/formats/obj/CjsObjFormat.js.map +1 -1
  159. package/dist/formats/obj/core/helpers.js +553 -573
  160. package/dist/formats/obj/core/helpers.js.map +1 -1
  161. package/dist/formats/obj/core/json.js +64 -64
  162. package/dist/formats/obj/core/parser.js +321 -321
  163. package/dist/formats/obj/index.js +2 -2
  164. package/dist/formats/ogg/CjsOggFormat.js +143 -143
  165. package/dist/formats/ogg/core/helpers.js +410 -410
  166. package/dist/formats/ogg/core/imdct.js +178 -178
  167. package/dist/formats/ogg/core/vorbis.js +1017 -1017
  168. package/dist/formats/ogg/index.js +2 -2
  169. package/dist/formats/pickle/CjsPickleFormat.js +214 -214
  170. package/dist/formats/pickle/core/CjsPickleProtocol0Reader.js +551 -551
  171. package/dist/formats/pickle/index.js +2 -2
  172. package/dist/formats/png/CjsPngFormat.js +201 -201
  173. package/dist/formats/png/core/helpers.js +635 -635
  174. package/dist/formats/png/index.js +2 -2
  175. package/dist/formats/red/CjsRedFormat.js +263 -263
  176. package/dist/formats/red/core/CjsRedReader.js +246 -246
  177. package/dist/formats/red/core/blackDefinitions.js +3 -3
  178. package/dist/formats/red/core/helpers.js +158 -158
  179. package/dist/formats/red/core/redGraph.js +71 -71
  180. package/dist/formats/red/core/schema.js +4 -4
  181. package/dist/formats/red/index.js +2 -2
  182. package/dist/formats/stl/CjsStlFormat.js +320 -365
  183. package/dist/formats/stl/CjsStlFormat.js.map +1 -1
  184. package/dist/formats/stl/core/helpers.js +241 -261
  185. package/dist/formats/stl/core/helpers.js.map +1 -1
  186. package/dist/formats/stl/core/json.js +51 -51
  187. package/dist/formats/stl/core/stl.js +642 -642
  188. package/dist/formats/stl/core/targets.js +173 -173
  189. package/dist/formats/stl/index.js +2 -2
  190. package/dist/formats/tga/CjsTgaFormat.js +197 -197
  191. package/dist/formats/tga/core/helpers.js +493 -493
  192. package/dist/formats/tga/index.js +2 -2
  193. package/dist/formats/wav/CjsWavFormat.js +198 -198
  194. package/dist/formats/wav/core/helpers.js +365 -365
  195. package/dist/formats/wav/index.js +2 -2
  196. package/dist/formats/webgl/CjsWebglFormat.js +199 -199
  197. package/dist/formats/webgl/core/buildGlslEffectContainer.js +70 -70
  198. package/dist/formats/webgl/core/effectPackage.js +900 -900
  199. package/dist/formats/webgl/core/errors.js +27 -27
  200. package/dist/formats/webgl/core/glsl/DxbcGlslEmitter.js +2820 -2820
  201. package/dist/formats/webgl/core/glsl/DxbcGlslHelpers.js +89 -89
  202. package/dist/formats/webgl/core/glsl/DxbcGlslOperandFormatter.js +486 -486
  203. package/dist/formats/webgl/core/glsl/packedLightFixups.js +98 -98
  204. package/dist/formats/webgl/core/glslBackendBlock.js +552 -552
  205. package/dist/formats/webgl/core/glslBackendBodySet.js +243 -243
  206. package/dist/formats/webgl/core/glslEffectCompleteness.js +85 -85
  207. package/dist/formats/webgl/core/glslEffectCompleteness.js.map +1 -1
  208. package/dist/formats/webgl/core/helpers.js +167 -167
  209. package/dist/formats/webgl/core/inspectGlslEffectContainer.js +122 -122
  210. package/dist/formats/webgl/core/readGlslEffectContainer.js +202 -256
  211. package/dist/formats/webgl/core/readGlslEffectContainer.js.map +1 -1
  212. package/dist/formats/webgl/index.js +2 -2
  213. package/dist/formats/webgpu/CjsWebgpuFormat.js +357 -357
  214. package/dist/formats/webgpu/core/buildCarbonEffectContainer.js +89 -89
  215. package/dist/formats/webgpu/core/carbonWebgpu/CarbonWebgpuContainer.js +354 -354
  216. package/dist/formats/webgpu/core/carbonWebgpu/containerViews.js +355 -355
  217. package/dist/formats/webgpu/core/carbonWebgpu/validateContainer.js +90 -90
  218. package/dist/formats/webgpu/core/effectAnalysis.js +82 -82
  219. package/dist/formats/webgpu/core/effectBackendBodySet.js +306 -306
  220. package/dist/formats/webgpu/core/errors.js +20 -20
  221. package/dist/formats/webgpu/core/helpers.js +443 -443
  222. package/dist/formats/webgpu/core/ir/analyzeRegisterValues.js +212 -212
  223. package/dist/formats/webgpu/core/ir/buildControlFlow.js +220 -220
  224. package/dist/formats/webgpu/core/ir/indexableTemps.js +137 -137
  225. package/dist/formats/webgpu/core/ir/inferValueTypes.js +449 -449
  226. package/dist/formats/webgpu/core/ir/lowerDxbcToIr.js +494 -494
  227. package/dist/formats/webgpu/core/ir/resolveRegisterFlow.js +177 -177
  228. package/dist/formats/webgpu/core/ir/sourceLanes.js +61 -61
  229. package/dist/formats/webgpu/core/packageEffect.js +381 -381
  230. package/dist/formats/webgpu/core/packageEffectSelection.js +164 -164
  231. package/dist/formats/webgpu/core/packageMetadata.js +17 -17
  232. package/dist/formats/webgpu/core/schema.js +4 -4
  233. package/dist/formats/webgpu/core/wgsl/buildResourceTransformPlan.js +263 -263
  234. package/dist/formats/webgpu/core/wgsl/buildWgslBindingPlan.js +172 -172
  235. package/dist/formats/webgpu/core/wgsl/buildWgslSet.js +325 -325
  236. package/dist/formats/webgpu/core/wgsl/emitWgsl.js +356 -356
  237. package/dist/formats/webgpu/core/wgsl/hoistEscapingValues.js +77 -77
  238. package/dist/formats/webgpu/core/wgsl/lowerBindingLayout.js +451 -451
  239. package/dist/formats/webgpu/core/wgsl/lowerComputeProgram.js +735 -735
  240. package/dist/formats/webgpu/core/wgsl/lowerCreateHistogramsComputeProgram.js +457 -457
  241. package/dist/formats/webgpu/core/wgsl/lowerFragmentProgram.js +1572 -1572
  242. package/dist/formats/webgpu/core/wgsl/lowerMergeHistogramsComputeProgram.js +659 -659
  243. package/dist/formats/webgpu/core/wgsl/lowerParticleClearComputePrograms.js +734 -734
  244. package/dist/formats/webgpu/core/wgsl/lowerParticleEmitComputeProgram.js +583 -583
  245. package/dist/formats/webgpu/core/wgsl/lowerSkinVerticesComputeProgram.js +621 -621
  246. package/dist/formats/webgpu/core/wgsl/lowerSortComputeProgram.js +824 -824
  247. package/dist/formats/webgpu/core/wgsl/lowerSortInnerComputeProgram.js +697 -697
  248. package/dist/formats/webgpu/core/wgsl/lowerSortStepComputeProgram.js +559 -559
  249. package/dist/formats/webgpu/core/wgsl/lowerVertexProgram.js +1328 -1328
  250. package/dist/formats/webgpu/core/wgsl/particleEmitSemanticDigest.js +110 -110
  251. package/dist/formats/webgpu/core/wgsl/precisionControls.js +55 -55
  252. package/dist/formats/webgpu/core/wgsl/selectionPlans.js +717 -717
  253. package/dist/formats/webgpu/core/wgsl/uniformity.js +78 -78
  254. package/dist/formats/webgpu/core/wgsl/validateExactComputeIr.js +196 -196
  255. package/dist/formats/webgpu/core/wgsl/validateHandleOperand.js +37 -37
  256. package/dist/formats/webgpu/index.js +2 -2
  257. package/dist/formats/webm/CjsWebmFormat.js +197 -197
  258. package/dist/formats/webm/core/helpers.js +572 -572
  259. package/dist/formats/webm/index.js +2 -2
  260. package/dist/formats/webp/CjsWebpFormat.js +140 -140
  261. package/dist/formats/webp/core/helpers.js +237 -237
  262. package/dist/formats/webp/index.js +2 -2
  263. package/dist/formats/wem/CjsWemFormat.js +250 -250
  264. package/dist/formats/wem/core/bitStream.js +261 -261
  265. package/dist/formats/wem/core/codebookLibrary.js +164 -164
  266. package/dist/formats/wem/core/helpers.js +437 -437
  267. package/dist/formats/wem/core/packedCodebooksAotuv603.js +30 -30
  268. package/dist/formats/wem/core/ptadpcm.js +77 -77
  269. package/dist/formats/wem/core/resolve.js +121 -121
  270. package/dist/formats/wem/core/wemToOgg.js +485 -485
  271. package/dist/formats/wem/index.js +2 -2
  272. package/dist/formats/yaml/CjsYamlFormat.js +134 -134
  273. package/dist/formats/yaml/core/CjsYamlReader.js +400 -400
  274. package/dist/formats/yaml/core/helpers.js +196 -196
  275. package/dist/formats/yaml/index.js +2 -2
  276. package/dist/index.js +64 -64
  277. package/dist/resource/CjsLoadingObject.js +19 -19
  278. package/dist/resource/CjsResource.js +801 -801
  279. package/dist/resource/ResourceHandlerMode.js +14 -14
  280. package/dist/resource/Tr2LightProfileRes.js +32 -32
  281. package/dist/resource/audio/AudioGeometryResData.js +47 -47
  282. package/dist/resource/audio/CjsAudioBufferRes.js +86 -86
  283. package/dist/resource/audio/CjsAudioRes.js +213 -213
  284. package/dist/resource/audio/index.js +4 -4
  285. package/dist/resource/geometry/MeshDecalData.js +37 -37
  286. package/dist/resource/geometry/MeshDecalLodData.js +34 -34
  287. package/dist/resource/geometry/TriGeometryRes.js +570 -467
  288. package/dist/resource/geometry/TriGeometryRes.js.map +1 -1
  289. package/dist/resource/geometry/TriGeometryResAreaData.js +59 -59
  290. package/dist/resource/geometry/TriGeometryResJointData.js +38 -38
  291. package/dist/resource/geometry/TriGeometryResLodData.js +88 -88
  292. package/dist/resource/geometry/TriGeometryResMeshData.js +63 -63
  293. package/dist/resource/geometry/TriGeometryResSkeletonData.js +34 -34
  294. package/dist/resource/geometry/TriJointBinding.js +38 -38
  295. package/dist/resource/geometry/TriMorphTargetGeometryConstants.js +46 -46
  296. package/dist/resource/geometry/TriRtGeometryConstants.js +88 -88
  297. package/dist/resource/geometry/granny/GStateBindingCallbackData.js +31 -31
  298. package/dist/resource/geometry/granny/Tr2GrannyIntersectionResult.js +60 -60
  299. package/dist/resource/geometry/granny/Tr2GrannyStateRes.js +36 -36
  300. package/dist/resource/geometry/granny/TriGrannyRes.js +35 -35
  301. package/dist/resource/geometry/granny/enums.js +10 -10
  302. package/dist/resource/geometry/granny/index.js +6 -6
  303. package/dist/resource/geometry/index.js +17 -17
  304. package/dist/resource/index.js +54 -54
  305. package/dist/resource/resourceBoundary.js +64 -64
  306. package/dist/resource/shader/Tr2EffectRes.js +336 -336
  307. package/dist/resource/shader/Tr2MaterialArea.js +31 -31
  308. package/dist/resource/shader/Tr2MaterialMesh.js +27 -27
  309. package/dist/resource/shader/Tr2MaterialRes.js +31 -31
  310. package/dist/resource/shader/Tr2Shader.js +283 -283
  311. package/dist/resource/shader/Tr2ShaderPermutation.js +43 -43
  312. package/dist/resource/shader/index.js +17 -17
  313. package/dist/resource/shader/reflection/Tr2EffectConstant.js +143 -143
  314. package/dist/resource/shader/reflection/Tr2EffectDefine.js +30 -30
  315. package/dist/resource/shader/reflection/Tr2EffectDescription.js +114 -114
  316. package/dist/resource/shader/reflection/Tr2EffectLibrary.js +168 -168
  317. package/dist/resource/shader/reflection/Tr2EffectParameterAnnotation.js +125 -125
  318. package/dist/resource/shader/reflection/Tr2EffectResource.js +120 -120
  319. package/dist/resource/shader/reflection/Tr2EffectStageInput.js +372 -372
  320. package/dist/resource/shader/reflection/Tr2EffectTechnique.js +78 -78
  321. package/dist/resource/shader/reflection/Tr2Pass.js +168 -168
  322. package/dist/resource/shader/reflection/carbonRecordFields.js +159 -159
  323. package/dist/resource/shader/reflection/shaderStage.js +22 -22
  324. package/dist/resource/shader/sampler/Tr2SamplerSetup.js +135 -135
  325. package/dist/resource/texture/CjsTextureArrayRes.js +472 -472
  326. package/dist/resource/texture/CjsTextureArrayResParameterProxy.js +179 -179
  327. package/dist/resource/texture/Tr2ImageRes.js +120 -120
  328. package/dist/resource/texture/Tr2TextureLodManager.js +82 -82
  329. package/dist/resource/texture/Tr2TextureLodUpdateRequest.js +37 -37
  330. package/dist/resource/texture/Tr2TexturePackChannel.js +37 -37
  331. package/dist/resource/texture/Tr2TexturePipeline.js +54 -54
  332. package/dist/resource/texture/Tr2TexturePipelineParams.js +34 -34
  333. package/dist/resource/texture/Tr2TexturePipelineStepCompress.js +40 -40
  334. package/dist/resource/texture/Tr2TexturePipelineStepGenerateMips.js +22 -22
  335. package/dist/resource/texture/Tr2TexturePipelineStepLimitSize.js +34 -34
  336. package/dist/resource/texture/Tr2TexturePipelineStepLoad.js +31 -31
  337. package/dist/resource/texture/Tr2TexturePipelineStepPack.js +43 -43
  338. package/dist/resource/texture/TriTextureRes.js +359 -359
  339. package/dist/resource/texture/index.js +15 -15
  340. package/dist/resource/texture/texturePipelineBehavior.js +308 -308
  341. package/dist/worker/CjsResManMainThreadLoader.js +89 -89
  342. package/dist/worker/CjsResManWorker.js +218 -218
  343. package/dist/worker/CjsResManWorkerLoader.js +437 -437
  344. package/dist/worker/protocol.js +12 -12
  345. package/docs/README.md +98 -98
  346. package/docs/architecture.md +118 -118
  347. package/docs/concepts/resource-lifecycle.md +226 -226
  348. package/docs/concepts/shader-resource-model.md +111 -111
  349. package/docs/concepts/writing-an-engine-adapter.md +115 -115
  350. package/docs/formats/README.md +138 -138
  351. package/docs/formats/carbon-effect-container.md +553 -553
  352. package/docs/formats/dxbc/README.md +68 -68
  353. package/docs/formats/dxbc/architecture.md +80 -80
  354. package/docs/formats/dxbc/reference/api.md +105 -77
  355. package/docs/formats/dxbc/reference/classes/README.md +9 -9
  356. package/docs/formats/dxbc/reference/decoded-output.md +122 -122
  357. package/docs/formats/gr2.md +160 -160
  358. package/docs/formats/hlsl/README.md +54 -54
  359. package/docs/formats/hlsl/architecture.md +66 -65
  360. package/docs/formats/hlsl/guides/hydrating-json-output.md +60 -60
  361. package/docs/formats/hlsl/guides/reading-effects.md +68 -64
  362. package/docs/formats/hlsl/reference/advanced-analysis.md +61 -61
  363. package/docs/formats/hlsl/reference/api.md +91 -92
  364. package/docs/formats/hlsl/reference/classes/README.md +11 -11
  365. package/docs/formats/hlsl/reference/json-graph.md +97 -97
  366. package/docs/formats/pickle.md +82 -82
  367. package/docs/formats/provenance.md +196 -196
  368. package/docs/formats/stl.md +37 -37
  369. package/docs/formats/webgl/README.md +115 -115
  370. package/docs/formats/webgl/architecture.md +69 -69
  371. package/docs/formats/webgl/carbon-constant-layouts.md +326 -326
  372. package/docs/formats/webgl/decl-io.md +1234 -1234
  373. package/docs/formats/webgl/memory-structured.md +890 -890
  374. package/docs/formats/webgl/reference/classes/README.md +9 -9
  375. package/docs/formats/webgl/texture-sample.md +964 -964
  376. package/docs/formats/webgpu/README.md +84 -84
  377. package/docs/formats/webgpu/architecture.md +95 -95
  378. package/docs/formats/webgpu/formats/carbon-webgpu.md +215 -215
  379. package/docs/formats/webgpu/guides/effect-packaging.md +189 -189
  380. package/docs/formats/webgpu/reference/api.md +196 -196
  381. package/docs/formats/webgpu/reference/classes/README.md +9 -9
  382. package/docs/formats/webgpu/reference/wgsl-compatibility.md +1546 -1546
  383. package/docs/formats/wwise.md +146 -146
  384. package/docs/reference/classes/README.md +35 -35
  385. package/docs/reference/classes/audio.md +30 -30
  386. package/docs/reference/classes/core.md +216 -216
  387. package/docs/reference/classes/dropped.md +46 -46
  388. package/docs/reference/classes/formats.md +944 -944
  389. package/docs/reference/classes/resources.md +456 -456
  390. package/docs/reference/classes/texture.md +26 -26
  391. package/docs/reference/events.md +117 -117
  392. package/docs/reference/motherlode-cache.md +275 -275
  393. package/docs/reference/queues.md +194 -194
  394. package/docs/reference/reload.md +107 -107
  395. package/docs/reference/texture-arrays.md +113 -113
  396. package/docs/reference/texture-pipeline.md +53 -53
  397. package/docs/reference/workers.md +142 -142
  398. package/docs/roadmap.md +150 -150
  399. package/format-notices/black/LICENSE +21 -21
  400. package/format-notices/black/NOTICE +47 -47
  401. package/format-notices/bnk/LICENSE +21 -21
  402. package/format-notices/bnk/NOTICE +21 -21
  403. package/format-notices/cmf/LICENSE +21 -21
  404. package/format-notices/cmf/NOTICE +36 -36
  405. package/format-notices/dds/LICENSE +21 -21
  406. package/format-notices/dds/NOTICE +14 -14
  407. package/format-notices/dxbc/LICENSE +21 -21
  408. package/format-notices/dxbc/NOTICE +20 -20
  409. package/format-notices/fbx/LICENSE +21 -21
  410. package/format-notices/fbx/NOTICE +14 -14
  411. package/format-notices/flac/LICENSE +21 -21
  412. package/format-notices/flac/NOTICE +14 -14
  413. package/format-notices/gif/LICENSE +21 -21
  414. package/format-notices/gif/NOTICE +14 -14
  415. package/format-notices/gltf/LICENSE +21 -21
  416. package/format-notices/gltf/NOTICE +27 -27
  417. package/format-notices/gr2/LICENSE +21 -21
  418. package/format-notices/gr2/NOTICE +60 -60
  419. package/format-notices/gr2/THIRD-PARTY-NOTICES.md +93 -93
  420. package/format-notices/hlsl/LICENSE +21 -21
  421. package/format-notices/hlsl/NOTICE +25 -25
  422. package/format-notices/jpeg/LICENSE +21 -21
  423. package/format-notices/jpeg/NOTICE +14 -14
  424. package/format-notices/mp3/LICENSE +21 -21
  425. package/format-notices/mp3/NOTICE +14 -14
  426. package/format-notices/mp4/LICENSE +21 -21
  427. package/format-notices/mp4/NOTICE +14 -14
  428. package/format-notices/obj/LICENSE +21 -21
  429. package/format-notices/obj/NOTICE +26 -26
  430. package/format-notices/ogg/LICENSE +21 -21
  431. package/format-notices/ogg/NOTICE +28 -28
  432. package/format-notices/png/LICENSE +21 -21
  433. package/format-notices/png/NOTICE +14 -14
  434. package/format-notices/red/LICENSE +21 -21
  435. package/format-notices/red/NOTICE +31 -31
  436. package/format-notices/stl/LICENSE +21 -21
  437. package/format-notices/stl/NOTICE +21 -21
  438. package/format-notices/tga/LICENSE +21 -21
  439. package/format-notices/tga/NOTICE +14 -14
  440. package/format-notices/wav/LICENSE +21 -21
  441. package/format-notices/wav/NOTICE +14 -14
  442. package/format-notices/webgl/LICENSE +21 -21
  443. package/format-notices/webgl/NOTICE +35 -35
  444. package/format-notices/webgpu/LICENSE +21 -21
  445. package/format-notices/webgpu/NOTICE +31 -31
  446. package/format-notices/webm/LICENSE +21 -21
  447. package/format-notices/webm/NOTICE +14 -14
  448. package/format-notices/webp/LICENSE +21 -21
  449. package/format-notices/webp/NOTICE +14 -14
  450. package/format-notices/wem/LICENSE +57 -57
  451. package/format-notices/wem/NOTICE +33 -33
  452. package/format-notices/yaml/LICENSE +21 -21
  453. package/format-notices/yaml/NOTICE +44 -44
  454. package/package.json +63 -63
@@ -1,553 +1,553 @@
1
- # Carbon compiled-effect container
2
-
3
- Status: Stable
4
- Visibility: Public
5
- Scope: `@carbonenginejs/runtime-resource` — `src/format/carbonEffect/`, `src/format/CjsByteReader.js`, `src/format/CjsByteWriter.js`, `src/format/CjsStringTable.js`
6
- Audience: Anyone reading or writing compiled shader effect bytes, or extending `.carbonwebgpu`
7
- Summary: The v15 binary layout of Carbon's compiled effect files, the shared byte reader and writer that implement it, and the arena offset policy a byte-exact re-emit depends on.
8
-
9
- ## What this is
10
-
11
- Carbon's shader compiler emits one file per effect containing **every permutation**,
12
- selected at read time through an offset table. The format has three parts: a header,
13
- a deduplicated blob arena ("string table"), one dense offset-table row per
14
- permutation, and one stored description blob per distinct encoded body.
15
-
16
- This package implements it as a shared reader and writer, verified byte-exact against
17
- CCP's own shipped files. It is the wire foundation for **both** browser backends:
18
- `.carbonwebgpu` and `.carbonwebgl` are the same Carbon v15 container, differing
19
- only in the program text occupying each stage's slot and in the optional
20
- per-pass backend block. Neither carries a private magic or chunk layout.
21
-
22
- | module | role |
23
- |---|---|
24
- | `src/format/CjsByteReader.js` | little-endian cursor plus arena resolution primitives; shared by the HLSL, Carbon-effect, and WebGL readers |
25
- | `src/format/CjsByteWriter.js` | growable append cursor with reserve-and-patch |
26
- | `src/format/CjsStringTable.js` | the blob arena, with Carbon's bytewise-sorted offset assignment |
27
- | `src/format/carbonEffect/carbonEffectRecords.js` | the v15 description-blob record codec |
28
- | `src/format/carbonEffect/CjsCarbonEffectReader.js` | container reader and structural checks |
29
- | `src/format/carbonEffect/CjsCarbonEffectWriter.js` | container writer, offset arithmetic and alias dedupe |
30
-
31
- ## Versioning: how Carbon does it, and what this reader does instead
32
-
33
- ### Carbon validates the version, then threads it through parsing
34
-
35
- Carbon reads the version dword once, in `Tr2EffectRes`, and rejects anything
36
- outside 2..15 (`Tr2EffectRes.cpp:209`). Inside that range it never branches to
37
- accept or refuse again — the version becomes a *parsing input*. It is first used
38
- by the container reader itself, then **passed as a parameter** into the effect
39
- description's read call (`Tr2EffectRes.cpp:128`), alongside the buffer, its
40
- size, and the string table.
41
-
42
- From there it is threaded through every record reader in
43
- `Tr2EffectDescription.cpp` — `ReadConstant`, `ReadResource`,
44
- `ReadPipelineInputs`, `ReadRegisters`, `ReadInput`, and the pass/technique loop
45
- — which branch on it at 22 conditional sites using 16 distinct comparisons:
46
-
47
- | Threshold | What it decides |
48
- | --- | --- |
49
- | `> 8` | Whether a stage carries a register signature **at all** |
50
- | `< 14` | Where pipeline inputs are read, and whether registers are read beside them |
51
- | `< 11` | How a constant's type byte is decoded — older files remap a legacy enum, so the byte count is unchanged but its meaning is not |
52
- | `>= 13` | Whether a resource carries `arrayElements` at all; below 13 it is absent and defaults to 1 |
53
- | `< 5`, `< 4`, `>= 3`, `>= 4` | Stage-input presence and ordering |
54
- | `> 6`, `> 9`, `> 10`, `> 12`, `>= 8` | Per-record field presence |
55
- | `> 13`, `>= 14` | Library and pipeline-input placement |
56
-
57
- The consequence worth internalising: **the version determines the byte layout at
58
- nearly every level of the reflection graph**, not just the header. A v8 file and
59
- a v15 file are not the same records with a different preamble.
60
-
61
- Note where the version does *not* reach: `Tr2Shader` itself has no version field
62
- and no version-dependent branch. Container/header parsing and description
63
- parsing between them absorb every version difference before the graph reaches
64
- `Tr2Shader` — the container reader has its own branches (`Tr2EffectRes.cpp`
65
- handles `< 5` for the legacy header size, `>= 15` for the compiler version and
66
- source hash, and `> 5` for the permutation type), and the description reader
67
- handles the rest. What emerges carries no version, so consumers above that
68
- boundary are version-free. A port that preserves the same separation stays
69
- consistent with Carbon.
70
-
71
- ### What this reader does: constant-folded to 15
72
-
73
- `readEffectDescription` takes no version parameter. This reader is
74
- constant-folded to version 15 and rejects everything else outright.
75
-
76
- That is a deliberate narrowing, and it is the missing piece if an older version
77
- is ever needed. Supporting v8 does **not** mean writing a second reader; it
78
- means restoring the parameter Carbon always had and implementing the branches
79
- above. The writer is unaffected — reading an old version does not imply emitting
80
- one, and this package emits v15 only.
81
-
82
- Rejecting is the correct failure mode rather than a limitation to route around.
83
- Applying v15 rules to an older layout can misalign fields — the comparisons
84
- above change which fields are present and how their bytes are interpreted — and
85
- must not be attempted. How such a read would fail in practice is not documented
86
- here, because no legacy container has been examined to establish it.
87
-
88
- The reader rejects with the version it read, and does not classify it further.
89
- A leading dword of 2..14 is a *recognized legacy version number*, not evidence
90
- that the bytes are an effect container: arbitrary data can begin with a small
91
- integer, and the version dword alone carries no magic or checksum to confirm it.
92
- Anything that reaches this rejection needs identifying by other means before it
93
- is treated as a porting question rather than a wrong file.
94
-
95
- ### Why 15 is the version that got implemented
96
-
97
- Version 15 is the one with an authoritative writer to check against, and the
98
- entire audited shipped corpus at build 3444265 is v15 — 3222 files across
99
- `effect.dx11` and `effect.dx12`, plus the same 537 shaders again under
100
- `effect.metal`. Nothing older appears in that audited corpus. Carbon's own
101
- v13/v14 branches additionally mark their field-order boundaries as uncertain.
102
-
103
- The v15 body is byte-identical to v14. Version 15 differs from 14 only by the 36
104
- extra header bytes: the compiler version and the source hash.
105
-
106
- ## Layout
107
-
108
- ### Header
109
-
110
- ```
111
- u32 version = 15
112
- u8[4] shaderCompilerVersion {major, minor, patch, tweak}
113
- char[32] sourceHash ASCII hex MD5 of the HLSL source inputs
114
- u32 stringTableSize | arena payload
115
- u8 permutationCount | permutation records
116
- u32 recordCount | recordCount x { u32 index, u32 offset, u32 size }
117
- description blobs
118
- ```
119
-
120
- The compiler writes this order and the runtime reads the same order.
121
-
122
- The compiler version is **four bytes, not a `u32`**, and Carbon's rebuild check
123
- compares only the first three. A shipped v15 header reads `01 02 06 00` —
124
- compiler 1.2.6.0, matching the ShaderCompiler project version. As a `u32` those
125
- bytes are `0x00060201`, which means nothing. `HlslEffectRes` historically read
126
- the field as a dword; it now also exposes `m_compilerVersionBytes`, which is the
127
- truthful reading and what new code should use.
128
-
129
- The compiler's rebuild check reads the 32-byte hash, while the runtime skips it.
130
- It is provenance, not integrity.
131
-
132
- A permutation record is:
133
-
134
- ```
135
- u32 nameOffset | u8 defaultOption | u32 descriptionOffset | u8 type | u8 optionCount | u32 optionOffset[optionCount]
136
- ```
137
-
138
- Note the field order: `defaultOption` sits between the name and the description.
139
- Carbon writes that byte inside a conditional loop with no `else`, so a
140
- permutation whose declared default matches no option would emit a record one
141
- byte short and desynchronise the entire rest of the header. Our writer always
142
- emits it.
143
-
144
- ### Body-offset arithmetic
145
-
146
- ```
147
- base = 4 + 4 + 32 + headerSize + stringTable.GetSize()
148
- headerSize = (recordCount * 3 + 1) * 4 + permutationBytes
149
- permutationBytes = 1 + Σ (11 + optionCount * 4)
150
- ```
151
-
152
- `GetSize()` **includes** the arena's own `u32` length prefix, so the prefix is
153
- counted exactly once. Row offsets are absolute from byte 0 of the file.
154
- `CjsCarbonEffectWriter` asserts that the bytes it actually wrote before the
155
- first body equal this computed base, so an arithmetic error fails loudly rather
156
- than shifting every body.
157
-
158
- ### The arena
159
-
160
- `CjsStringTable` is Carbon's `StringTable`. Three properties matter:
161
-
162
- 1. **Offsets are assigned by a bytewise sort, not by insertion order.**
163
- The comparison is `memcmp` over the shorter length, then shorter-wins on a
164
- tie, before cumulative offsets are assigned. Any writer that assigns
165
- first-seen offsets produces a valid file that is not byte-identical to
166
- Carbon's.
167
- 2. **Dedupe is on exact bytes with no suffix merging.** `"red"` gets its own
168
- entry even though it is a suffix of `"shared"`.
169
- 3. **There are two kinds of entry and no manifest.** A NUL-terminated string is
170
- added with its terminator and referenced by a bare `u32` offset. A sized blob
171
- — shader bytecode, program source, default constant values — is added with
172
- exactly its own bytes and referenced by a `{u32 size, u32 offset}` pair. The
173
- arena writes a `u32` payload size and the payload, nothing else; every
174
- reference site resolves its own entry.
175
-
176
- ## Two rules for anything added later
177
-
178
- Neither is visible from Carbon's own code, and both were found by implementing
179
- rather than by reading. They constrain every future addition to this format.
180
-
181
- **Rule 1: every sized record must parse to exactly its declared end.** Trailing
182
- bytes mean one of two things and both are fatal — the writer knew fields this reader
183
- does not, or the writer miscounted. Enforced for the description blob
184
- (`readEffectDescription`), for the per-pass backend block (`readBackendBlock`), and
185
- for the header, whose end must equal where the body region begins.
186
-
187
- This rule carries weight that used to live elsewhere. The chunk container it replaces
188
- spent roughly 600 lines asserting that its several projections of one effect still
189
- agreed with each other, and those checks caught a malformed *tree* — our writer
190
- emitting something structurally wrong — not only a malformed file. A record layout
191
- makes most of that question unaskable, because containment replaces reference and
192
- position replaces key. What remains is this: a writer bug either fails to parse,
193
- which announces itself, or it parses and leaves the cursor somewhere other than the
194
- declared end. Applying the rule to some sized records and not others is a gap that
195
- stays invisible until a writer bug hides in one of the others.
196
-
197
- **Rule 2: anything placed in the arena must be arena-independent.** An arena entry
198
- cannot contain an arena offset. Offsets are assigned by the content sort, the sort
199
- depends on every entry's bytes, so an entry that referred to the arena would have to
200
- be interned before its own contents could be computed — a circular dependency with
201
- no fixed point. This is invisible in Carbon's own code because no Carbon arena blob
202
- refers to the arena: strings, bytecode and default constant values are all leaves.
203
- Our per-pass backend block is the first non-leaf candidate, and it is why that block
204
- carries inline length-prefixed strings instead of references. A test pins the
205
- property directly — the block's bytes must be identical whichever arena it is
206
- interned into. Any future arena entry must satisfy the same rule.
207
-
208
- ### One field the container cannot round-trip
209
-
210
- For a **non-dynamic sampler, the name is not preserved.** The file stores one, but
211
- Carbon's reader nulls it before any producer sees it, so a package built from
212
- our reflection carries the empty string.
213
-
214
- This is a property of the input, not a bug in the mapping: the name is unrecoverable
215
- by the time we receive the data, rather than dropped on the way out. Carbon nulls it
216
- precisely because a non-dynamic sampler is never looked up by name — `FindSamplerByName`
217
- only matters for the dynamic case. Recorded here because it will otherwise be
218
- rediscovered as a bug: a diff against the source effect will always show it.
219
-
220
- **Corollary: the container admits all six of Carbon's stage types.** `stages` is
221
- capped at `SHADER_TYPE_COUNT` = 6, and the stage-type byte uses Carbon's
222
- `InputStageType` numbering: vertex, pixel, compute, geometry, hull, domain. A
223
- backend that can only express three of those rejects the rest in its own layer;
224
- the container does not narrow on its behalf. The Carbon region is
225
- backend-invariant, and restrictions belong to the backend.
226
-
227
- `0xffffffff` is the null reference. It is legal at **exactly one wire
228
- position**: a stage's default-constant-value offset when the accompanying size
229
- is zero, which the optional-value reader consumes without dereferencing.
230
- Everywhere else a `0xffffffff` offset fails the load.
231
-
232
- Two deliberate departures from Carbon, both of which make byte-identical output
233
- more likely rather than less:
234
-
235
- - **`m_size` is initialised.** Carbon's constructor leaves it indeterminate and
236
- gets away with it only because the one instance is a zero-initialised global.
237
- - **Adding after an offset has been handed out is an error.** In Carbon,
238
- `GetOffset` re-sorts a dirty table, which reassigns *every* offset — including
239
- offsets already baked into packed bodies. Carbon avoids the corruption by
240
- interning all late strings before the packing pass. `CjsCarbonEffectWriter`
241
- reproduces that discipline structurally: it runs the record walk twice, once
242
- with `collectArena` to intern and once with `internArena` to emit. Because both
243
- passes drive the same `writeEffectDescription`, they cannot drift apart.
244
-
245
- ### Description blob, v15 field order
246
-
247
- Derived independently from the writer's save order and the reader's load order,
248
- then confirmed to agree field for field. Counts are `u8` unless marked.
249
-
250
- ```
251
- u8 techniqueCount
252
- u32 name
253
- u8 passCount cap 64
254
- u8 stageCount cap 6 (SHADER_TYPE_COUNT)
255
- u8 stageType
256
- u32 shaderSize | u32 shaderDataOffset program payload, arena blob
257
- u32 threadGroupSize[3]
258
- u8 pipelineInputCount cap 64
259
- u8 usage, registerIndex, usageIndex, usedMask, type, dimension
260
- -- StageData --
261
- u8 registerCount
262
- u8 registerType | u32 registerIndex | u32 registerCount | u8 registerSpace
263
- u8 staticSamplerCount
264
- u32 registerIndex | u8 registerSpace | u8 x7 filters/address
265
- f32 mipLODBias | u8 maxAnisotropy | u8 comparisonFunc
266
- u8 borderColor enum, NOT four floats
267
- f32 minLOD | f32 maxLOD
268
- u32 constantCount u32, not u8
269
- u32 name | u32 offset | u32 size | u8 type | u8 dimension
270
- u32 elements | u8 isSRGB | u8 isAutoregister
271
- u32 defaultValuesSize | u32 defaultValuesOffset 0xffffffff legal when size 0
272
- u8 textureCount cap 64
273
- u8 registerIndex | u32 name | u8 type | u32 count | u8 isSRGB | u8 isAutoregister
274
- u8 samplerCount cap 64
275
- u8 registerIndex | u32 name | u8 x7 | f32 mipLODBias | u8 maxAnisotropy
276
- u8 comparisonFunc | f32 borderColor[4] | f32 minLOD | f32 maxLOD | u8 isDynamic
277
- u8 uavCount cap 64
278
- u8 registerIndex | u32 name | u8 type | u32 count | u8 isAutoregister
279
- u8 annotationCount
280
- u32 name | u8 type | (u32 stringOffset if type == STRING else 4 raw bytes)
281
- u8 renderStateCount cap 64
282
- u32 state | u32 value
283
- u8 libraryCount
284
- u32 payloadSize | u32 shaderSize | u32 shaderDataOffset
285
- u32 exportCount u32, not u8
286
- u8 type | u32 name
287
- u32 hitGroupName
288
- StageData globalInputs
289
- StageData localInputs
290
- u16 parameterCount cap 256
291
- u32 name | annotation map as above
292
- ```
293
-
294
- Four places this is easy to get wrong:
295
-
296
- - **At v15 the program payload comes first and the signature tables follow.**
297
- `pipelineInputs` and `registers` sit *after* `shaderCode` and `threadGroupSize`.
298
- Before v14 it was the other way round; v14 moved them, and Carbon marks its own
299
- v14 branch as uncertain.
300
- - **A UAV record is one byte shorter than a texture record** — it has no `isSRGB`.
301
- Carbon's reader hardcodes `isSRGB = false` and the writer omits it. Sharing one
302
- "resource" codec between the two silently corrupts every subsequent field.
303
- - **`borderColor` is four floats on a sampler and one byte on a static sampler**
304
- because the two records mirror different D3D binding models.
305
- - **A non-string annotation value is four raw bytes.** Carbon writes it through the
306
- `float` member of a `{float,int32_t}` union and reads it back through a different
307
- union. The bytes round-trip; applying an int/float conversion does not. The codec
308
- keeps `rawValue` as bytes for exactly this reason.
309
-
310
- Carbon writes `textures`, `samplers`, `uavs` and render states in ascending key
311
- order and sorts annotation keys by bytewise string comparison.
312
- `compareAnnotationNames` implements that comparison over UTF-8 bytes, which is
313
- *not* the same as JavaScript's UTF-16 code-unit order for names outside ASCII
314
- — `"Z"` sorts before `"a"`.
315
-
316
- ### The optional trailing block
317
-
318
- Our own containers add exactly one optional block per pass, after the render-state
319
- table, referenced by a `{u32 size, u32 offset}` pair into the arena. A Carbon file
320
- ends the pass at the render states, so the reader and writer gate it on
321
- `{ backend: true }` and produce Carbon's bytes unchanged when it is closed.
322
-
323
- The block carries the two sections that are not derivable from Carbon reflection —
324
- WebGPU bind-group layouts and resource transforms — in **one** unit, because they
325
- are mutually required and because "the Carbon region is backend-invariant, with
326
- exactly one optional trailing block" is the invariant worth keeping.
327
-
328
- It lives in the arena so identical layouts dedupe across bodies the way program
329
- source does; measured sharing is 30.5:1 at `(body, pass)` granularity, 22 distinct
330
- blocks across 672 pairs. That forces one property: **the block contains no arena
331
- offsets.** An offset is only known after the arena's content sort, which depends on
332
- every blob's bytes including this one, so a block referencing the arena could not be
333
- built before it was interned. Strings inside it are inline and length-prefixed.
334
-
335
- ```
336
- u8 blobVersion = 1
337
- u8 bindGroupCount
338
- u8 group | u8 bindingCount
339
- u8 resourceKind | u8 registerSpace | u8 binding | u8 visibilityMask
340
- u32 registerIndex | u32 structureStride (0xffffffff absent) | u8 arrayLayerCount (0 absent)
341
- str type | str generatedSymbol | str transformId (empty = none)
342
- u8 transformCount
343
- u8 familyCode | str id | u8 inputCount
344
- u8 registerSpace | u8 registerIndex | str parameter
345
- ```
346
-
347
- `identity` and `group` on each binding, and a transform's `kind`,
348
- `stage`, `representation`, `missingLayer`, `viewDimension`, `layerCount`,
349
- `output.identity`, `output.scopeIdentity`, `output.name`, `layoutKey` and every
350
- input's `layer` are restored on read, not stored. The family byte is what keeps them
351
- derivable without pinning the format to one recognizer. `id` and each input's
352
- `parameter` stay on the wire deliberately — `id` because a caller may supply it,
353
- `parameter` because it keeps layer identity cross-checkable rather than asserted by
354
- position.
355
-
356
- Backend-block version 1 stores visibility but not the original
357
- `scopeIdentity`. The reader reconstructs `${identity}@${visibility[0]}`. A
358
- multi-stage shared binding therefore rereads as stage-qualified rather than
359
- recovering its original bare scope. Callers must not infer that original
360
- sharing decision from the wire view.
361
-
362
- An unknown `blobVersion` reports the pass as having no backend data rather than
363
- misparsing it; the enclosing size makes it skippable.
364
-
365
- ### Count caps
366
-
367
- `CARBON_EFFECT_COUNT_CAPS` mirrors the runtime's inclusive limits. Carbon's
368
- compiler enforces none of them while its runtime rejects anything above them,
369
- so an over-large effect compiles and then fails to load; our writer checks on
370
- the way out. The caps Carbon does *not* have — techniques, registers, static
371
- samplers, constants, libraries, exports, annotation counts — are deliberately
372
- not invented here.
373
-
374
- ### The alias path
375
-
376
- Carbon compares packed bodies pairwise and points a duplicate's row at the
377
- surviving twin. The row is **kept**, so the offset table stays dense while the
378
- file stores each distinct body once. Across the shipped corpus 22% of files
379
- alias, at roughly 2.1 rows per distinct body.
380
-
381
- `enumerateUniqueEffectBodies` recovers that grouping without decoding a single
382
- body. It inspects source records and bytes only, and returns
383
- first-occurrence-ordered groups, each holding one canonical
384
- `permutationIndex`/`sourceRecord` plus every byte-identical alias. Exact range
385
- aliases are the fast path; distinct ranges are fingerprinted and then compared
386
- byte for byte. It caps the Cartesian body table at 65,536 records
387
- (`EFFECT_BODY_COUNT_MAX`) and rejects partial overlaps, because a partial
388
- overlap means two bodies claim the same bytes. It decodes nothing: it reads
389
- `m_offsets` and slices `m_data` directly, so a backend packager can inventory an
390
- effect without disturbing anything a later read depends on. It is internal
391
- (`src/format/effect/`), not a published export; the backend body set is its one
392
- consumer.
393
-
394
- ## Offset-table density
395
-
396
- Carbon indexes the offset table **positionally** and never reads each row's
397
- stored `index` field. A sparse or misordered table therefore does not fail — it
398
- silently returns the wrong shader body.
399
-
400
- Density is incidental in Carbon: it falls out of `g_compiledEffects` being a
401
- `std::map` densely keyed by the work-queue builder, and is promised nowhere.
402
-
403
- Measured twice at build 3444265. A header-only sweep of every `.sm_hi`, `.sm_lo`
404
- and `.sm_depth` under `effect.dx11` and `effect.dx12` — 3222 files, 52,332 rows —
405
- and the full round-trip run below across all three backends — 4833 files, 78,498
406
- rows, 40,645 distinct bodies. **Every file is dense and positionally indexed**, and
407
- every row's byte range lies inside the file and clear of the header. In every
408
- single file the body region tiles the post-header space exactly, with no leading or
409
- trailing slack.
410
-
411
- Given that, the checks are implemented as follows:
412
-
413
- - `CjsCarbonEffectReader` **always** collects `dense` and `indicesMatchPosition` as
414
- diagnostics, and **always** fails closed on an out-of-range row.
415
- - Density and positional indexing **fail closed on read by default**.
416
- `{ permissive: true }` skips the check and leaves the diagnostics in place, for
417
- forensic inspection of a file already known to be malformed. It is not a load
418
- option.
419
- - `writeCarbonEffectFile` **always** fails closed: it refuses to emit bodies that
420
- are not dense from index 0. Where we own the bytes there is no reason to be
421
- lenient.
422
-
423
- `--ignore-permutations` does make CCP's compiler emit only key 0 while declaring
424
- every axis, so a sparse file is producible. That argues for the escape hatch, not
425
- for permissive defaults: Carbon does not reject such a file, it returns the wrong
426
- permutation's shader silently, which is the failure class this port exists to
427
- close.
428
-
429
- ## Backend selection and envelope removal
430
-
431
- **Historical compatibility.** Flat-package builds once prepended
432
- `magic | containerVersion | payloadKind` before Carbon's byte-compatible
433
- layout. Current packaging emits no such prefix and the former helper is no
434
- longer part of the package.
435
-
436
- **Current WebGPU wire.** Carbon WebGPU bytes have no envelope, magic, `payloadKind`,
437
- or independent container version. They are bare Carbon v15 records resolved
438
- from `effect.webgpu/`, with one optional per-pass backend block. `.carbonwebgl`
439
- remains a separate Carbon WebGL chunk format rather than this Carbon-record wire.
440
-
441
- `CarbonWebgpuContainer` reads the optional blocks, and the shared record reader can
442
- auto-detect them from a description's declared size. There is no adapter
443
- boundary left between them and the runtime: `Tr2EffectRes.DoLoad` retains a
444
- `CjsCarbonEffectReader` over the same bytes and `Tr2Shader.fromCarbonBinary`
445
- builds the device-free graph from one description record.
446
-
447
- Versioning remains local to what it versions: Carbon's version dword governs
448
- the Carbon region, while `blobVersion` governs the optional backend block. An
449
- unknown block version is skipped rather than misparsed. The package does not
450
- claim a version in CCP's namespace.
451
-
452
- Loose program bytes without a resource path can be identified from their
453
- payload: DXBC opens with `"DXBC"`, AIR is bitcode (`BC 0xC0DE`), and WGSL and
454
- GLSL have distinct text syntax. The stage record itself carries no language
455
- tag; program interpretation remains a backend/path responsibility.
456
-
457
- ## Verification
458
-
459
- `node --test` in this package. Two gates.
460
-
461
- **Always green.** `test/format/byte-primitives.test.js` and
462
- `test/format/carbon-effect.test.js` build a synthetic four-permutation v15
463
- container exercising every record type — static samplers, UAVs, annotations of
464
- every value type, render states, a raytracing library with both stage-data blocks
465
- — and assert a byte-exact write → read → write round trip, the arena sort order,
466
- the caps, the structural checks and, until the switchover, the legacy envelope's
467
- disjointness.
468
-
469
- **Env-gated real-file proof.** `test/format/carbon-effect-corpus.test.js`, enabled
470
- with `CARBON_EFFECT_CORPUS_DIR`. Game bytes are never committed. Supply a
471
- separately acquired corpus at pinned build 3444265.
472
-
473
- `CARBON_EFFECT_CORPUS_DIR` must point at a **materialised tree of source
474
- effects** — real `.sm_hi` / `.sm_lo` / `.sm_depth` filenames under
475
- `effect.dx11`, `effect.dx12`, or `effect.gles2`, which is what the walker
476
- filters on. Two nearby directories look like corpora and are not:
477
-
478
- - a content-addressed resource store (hash-named files, no extensions) matches
479
- nothing and fails as `no compiled effect files found` — the walker is
480
- extension-driven, so an unextracted cache silently yields zero files;
481
- - a directory of **our own translated output** keeps the `.sm_*` names but
482
- holds `Carbon WebGL`/`Carbon WebGPU` containers, and fails with
483
- `Unsupported Carbon effect version 1196901699` — that number is the ASCII
484
- `Carbon WebGL` magic read as a version dword.
485
-
486
- Both failures are loud, which is the intended behaviour; neither is a defect.
487
- The test re-emits each file three ways:
488
-
489
- 1. every description blob through the file's own arena — proves the field order;
490
- 2. the whole container from raw bodies and the source arena — proves the header
491
- order, the base arithmetic and the alias path;
492
- 3. the whole container with the arena rebuilt from the references found — proves
493
- the sorted-offset policy.
494
-
495
- Only the third can legitimately differ, because an arena may retain blobs the file
496
- no longer references. When it does differ the divergence is reported exactly and
497
- asserted to be unreferenced-blob retention; it is never downgraded to a weaker
498
- comparison such as "same strings, any order", which would look green and prove
499
- nothing.
500
-
501
- Measured result over the complete corpus — 4833 files (537 shaders × 3 variants ×
502
- 3 backends), 78,498 offset-table rows, 40,645 distinct description bodies:
503
- **all three modes byte-exact, with zero arena-rebuild divergences, zero sparse
504
- tables and zero misordered tables.** Not one shipped file retains an unreferenced
505
- arena blob, so the sorted-offset policy reproduces CCP's arena exactly.
506
- Supply a separately acquired corpus and set `CARBON_EFFECT_CORPUS_DIR`; no
507
- corpus data ships with the package.
508
-
509
- **The same proof, one level up.** `test/resource/effect-res-corpus.test.js`
510
- closes the loop through the resource classes rather than the records: bytes →
511
- `Tr2EffectRes.DoLoad` → `Tr2Shader.fromCarbonBinary` → the device-free graph →
512
- `toCarbonBinary()` → records → bytes. Running the record-level round trip on the
513
- same files with no classes involved is the negative control; when the two
514
- disagree, the difference is the class layer's, and that is how the three
515
- fidelity losses it once had were found and closed — non-dynamic sampler names
516
- (1,631 files), authored pass-stage order (21 files), and the offset word of a
517
- zero-size blob (150 files), which is not always the `0xffffffff` sentinel.
518
- Carbon's *runtime* discards the first two; the file does not, so re-emitting the
519
- file it came from means keeping them.
520
-
521
- That result is the container port's central evidence. The same reader and the same
522
- writer reproduce, byte for byte, files whose program payloads are DXBC in two
523
- dialects and AIR — with no language field anywhere in the format. The metadata
524
- region is backend-invariant as a measured fact rather than an argument from the
525
- writer. Backend selection therefore belongs at the resource-path boundary, not
526
- in an envelope or per-stage record.
527
-
528
- `effect.gles2` is deliberately not a validation target for **this package**: those
529
- shaders are v8, and nothing in the container port reads or writes them.
530
-
531
- **Do not read that as "obsolete".** `effect.gles2` is the shader tree ccpwgl
532
- actually renders with today — it is the only one that currently works end to end.
533
-
534
- The two statements coexist because **v15-only constrains what we write and
535
- validate against, not what a reader may accept.** Version-branching is the
536
- format's own mechanism. A reader that wants all supported generations branches
537
- on the version dword — v2..8 legacy gles2, v15 everything current — which is one
538
- reader, not a bespoke path per format.
539
-
540
- **Our containers are v15, not a version of our own.** A "v16" was considered for
541
- the variant carrying the per-pass backend block and **rejected**: CCP owns that
542
- number space, so claiming 16 would collide with any real v16 they ship, in the one
543
- field whose entire job is telling a reader how to parse. It also failed the rule
544
- the rest of this format is held to — invent something only because it *has to*
545
- exist, never because we think it should.
546
-
547
- The container needs no new version. Each description blob carries a declared
548
- size in the offset table, and [Rule 1](#two-rules-for-anything-added-later)
549
- already requires it to parse to exactly that end. A reader parses a blob without
550
- blocks and re-parses with them if the cursor misses the declared end, so the
551
- presence of the block is **self-describing** with no new field, no container
552
- version and no out-of-band flag. `blobVersion` inside the block versions the
553
- extension itself.
1
+ # Carbon compiled-effect container
2
+
3
+ Status: Stable
4
+ Visibility: Public
5
+ Scope: `@carbonenginejs/runtime-resource` — `src/format/carbonEffect/`, `src/format/CjsByteReader.js`, `src/format/CjsByteWriter.js`, `src/format/CjsStringTable.js`
6
+ Audience: Anyone reading or writing compiled shader effect bytes, or extending `.carbonwebgpu`
7
+ Summary: The v15 binary layout of Carbon's compiled effect files, the shared byte reader and writer that implement it, and the arena offset policy a byte-exact re-emit depends on.
8
+
9
+ ## What this is
10
+
11
+ Carbon's shader compiler emits one file per effect containing **every permutation**,
12
+ selected at read time through an offset table. The format has three parts: a header,
13
+ a deduplicated blob arena ("string table"), one dense offset-table row per
14
+ permutation, and one stored description blob per distinct encoded body.
15
+
16
+ This package implements it as a shared reader and writer, verified byte-exact against
17
+ CCP's own shipped files. It is the wire foundation for **both** browser backends:
18
+ `.carbonwebgpu` and `.carbonwebgl` are the same Carbon v15 container, differing
19
+ only in the program text occupying each stage's slot and in the optional
20
+ per-pass backend block. Neither carries a private magic or chunk layout.
21
+
22
+ | module | role |
23
+ |---|---|
24
+ | `src/format/CjsByteReader.js` | little-endian cursor plus arena resolution primitives; shared by the HLSL, Carbon-effect, and WebGL readers |
25
+ | `src/format/CjsByteWriter.js` | growable append cursor with reserve-and-patch |
26
+ | `src/format/CjsStringTable.js` | the blob arena, with Carbon's bytewise-sorted offset assignment |
27
+ | `src/format/carbonEffect/carbonEffectRecords.js` | the v15 description-blob record codec |
28
+ | `src/format/carbonEffect/CjsCarbonEffectReader.js` | container reader and structural checks |
29
+ | `src/format/carbonEffect/CjsCarbonEffectWriter.js` | container writer, offset arithmetic and alias dedupe |
30
+
31
+ ## Versioning: how Carbon does it, and what this reader does instead
32
+
33
+ ### Carbon validates the version, then threads it through parsing
34
+
35
+ Carbon reads the version dword once, in `Tr2EffectRes`, and rejects anything
36
+ outside 2..15 (`Tr2EffectRes.cpp:209`). Inside that range it never branches to
37
+ accept or refuse again — the version becomes a *parsing input*. It is first used
38
+ by the container reader itself, then **passed as a parameter** into the effect
39
+ description's read call (`Tr2EffectRes.cpp:128`), alongside the buffer, its
40
+ size, and the string table.
41
+
42
+ From there it is threaded through every record reader in
43
+ `Tr2EffectDescription.cpp` — `ReadConstant`, `ReadResource`,
44
+ `ReadPipelineInputs`, `ReadRegisters`, `ReadInput`, and the pass/technique loop
45
+ — which branch on it at 22 conditional sites using 16 distinct comparisons:
46
+
47
+ | Threshold | What it decides |
48
+ | --- | --- |
49
+ | `> 8` | Whether a stage carries a register signature **at all** |
50
+ | `< 14` | Where pipeline inputs are read, and whether registers are read beside them |
51
+ | `< 11` | How a constant's type byte is decoded — older files remap a legacy enum, so the byte count is unchanged but its meaning is not |
52
+ | `>= 13` | Whether a resource carries `arrayElements` at all; below 13 it is absent and defaults to 1 |
53
+ | `< 5`, `< 4`, `>= 3`, `>= 4` | Stage-input presence and ordering |
54
+ | `> 6`, `> 9`, `> 10`, `> 12`, `>= 8` | Per-record field presence |
55
+ | `> 13`, `>= 14` | Library and pipeline-input placement |
56
+
57
+ The consequence worth internalising: **the version determines the byte layout at
58
+ nearly every level of the reflection graph**, not just the header. A v8 file and
59
+ a v15 file are not the same records with a different preamble.
60
+
61
+ Note where the version does *not* reach: `Tr2Shader` itself has no version field
62
+ and no version-dependent branch. Container/header parsing and description
63
+ parsing between them absorb every version difference before the graph reaches
64
+ `Tr2Shader` — the container reader has its own branches (`Tr2EffectRes.cpp`
65
+ handles `< 5` for the legacy header size, `>= 15` for the compiler version and
66
+ source hash, and `> 5` for the permutation type), and the description reader
67
+ handles the rest. What emerges carries no version, so consumers above that
68
+ boundary are version-free. A port that preserves the same separation stays
69
+ consistent with Carbon.
70
+
71
+ ### What this reader does: constant-folded to 15
72
+
73
+ `readEffectDescription` takes no version parameter. This reader is
74
+ constant-folded to version 15 and rejects everything else outright.
75
+
76
+ That is a deliberate narrowing, and it is the missing piece if an older version
77
+ is ever needed. Supporting v8 does **not** mean writing a second reader; it
78
+ means restoring the parameter Carbon always had and implementing the branches
79
+ above. The writer is unaffected — reading an old version does not imply emitting
80
+ one, and this package emits v15 only.
81
+
82
+ Rejecting is the correct failure mode rather than a limitation to route around.
83
+ Applying v15 rules to an older layout can misalign fields — the comparisons
84
+ above change which fields are present and how their bytes are interpreted — and
85
+ must not be attempted. How such a read would fail in practice is not documented
86
+ here, because no legacy container has been examined to establish it.
87
+
88
+ The reader rejects with the version it read, and does not classify it further.
89
+ A leading dword of 2..14 is a *recognized legacy version number*, not evidence
90
+ that the bytes are an effect container: arbitrary data can begin with a small
91
+ integer, and the version dword alone carries no magic or checksum to confirm it.
92
+ Anything that reaches this rejection needs identifying by other means before it
93
+ is treated as a porting question rather than a wrong file.
94
+
95
+ ### Why 15 is the version that got implemented
96
+
97
+ Version 15 is the one with an authoritative writer to check against, and the
98
+ entire audited shipped corpus at build 3444265 is v15 — 3222 files across
99
+ `effect.dx11` and `effect.dx12`, plus the same 537 shaders again under
100
+ `effect.metal`. Nothing older appears in that audited corpus. Carbon's own
101
+ v13/v14 branches additionally mark their field-order boundaries as uncertain.
102
+
103
+ The v15 body is byte-identical to v14. Version 15 differs from 14 only by the 36
104
+ extra header bytes: the compiler version and the source hash.
105
+
106
+ ## Layout
107
+
108
+ ### Header
109
+
110
+ ```
111
+ u32 version = 15
112
+ u8[4] shaderCompilerVersion {major, minor, patch, tweak}
113
+ char[32] sourceHash ASCII hex MD5 of the HLSL source inputs
114
+ u32 stringTableSize | arena payload
115
+ u8 permutationCount | permutation records
116
+ u32 recordCount | recordCount x { u32 index, u32 offset, u32 size }
117
+ description blobs
118
+ ```
119
+
120
+ The compiler writes this order and the runtime reads the same order.
121
+
122
+ The compiler version is **four bytes, not a `u32`**, and Carbon's rebuild check
123
+ compares only the first three. A shipped v15 header reads `01 02 06 00` —
124
+ compiler 1.2.6.0, matching the ShaderCompiler project version. As a `u32` those
125
+ bytes are `0x00060201`, which means nothing. `HlslEffectRes` historically read
126
+ the field as a dword; it now also exposes `m_compilerVersionBytes`, which is the
127
+ truthful reading and what new code should use.
128
+
129
+ The compiler's rebuild check reads the 32-byte hash, while the runtime skips it.
130
+ It is provenance, not integrity.
131
+
132
+ A permutation record is:
133
+
134
+ ```
135
+ u32 nameOffset | u8 defaultOption | u32 descriptionOffset | u8 type | u8 optionCount | u32 optionOffset[optionCount]
136
+ ```
137
+
138
+ Note the field order: `defaultOption` sits between the name and the description.
139
+ Carbon writes that byte inside a conditional loop with no `else`, so a
140
+ permutation whose declared default matches no option would emit a record one
141
+ byte short and desynchronise the entire rest of the header. Our writer always
142
+ emits it.
143
+
144
+ ### Body-offset arithmetic
145
+
146
+ ```
147
+ base = 4 + 4 + 32 + headerSize + stringTable.GetSize()
148
+ headerSize = (recordCount * 3 + 1) * 4 + permutationBytes
149
+ permutationBytes = 1 + Σ (11 + optionCount * 4)
150
+ ```
151
+
152
+ `GetSize()` **includes** the arena's own `u32` length prefix, so the prefix is
153
+ counted exactly once. Row offsets are absolute from byte 0 of the file.
154
+ `CjsCarbonEffectWriter` asserts that the bytes it actually wrote before the
155
+ first body equal this computed base, so an arithmetic error fails loudly rather
156
+ than shifting every body.
157
+
158
+ ### The arena
159
+
160
+ `CjsStringTable` is Carbon's `StringTable`. Three properties matter:
161
+
162
+ 1. **Offsets are assigned by a bytewise sort, not by insertion order.**
163
+ The comparison is `memcmp` over the shorter length, then shorter-wins on a
164
+ tie, before cumulative offsets are assigned. Any writer that assigns
165
+ first-seen offsets produces a valid file that is not byte-identical to
166
+ Carbon's.
167
+ 2. **Dedupe is on exact bytes with no suffix merging.** `"red"` gets its own
168
+ entry even though it is a suffix of `"shared"`.
169
+ 3. **There are two kinds of entry and no manifest.** A NUL-terminated string is
170
+ added with its terminator and referenced by a bare `u32` offset. A sized blob
171
+ — shader bytecode, program source, default constant values — is added with
172
+ exactly its own bytes and referenced by a `{u32 size, u32 offset}` pair. The
173
+ arena writes a `u32` payload size and the payload, nothing else; every
174
+ reference site resolves its own entry.
175
+
176
+ ## Two rules for anything added later
177
+
178
+ Neither is visible from Carbon's own code, and both were found by implementing
179
+ rather than by reading. They constrain every future addition to this format.
180
+
181
+ **Rule 1: every sized record must parse to exactly its declared end.** Trailing
182
+ bytes mean one of two things and both are fatal — the writer knew fields this reader
183
+ does not, or the writer miscounted. Enforced for the description blob
184
+ (`readEffectDescription`), for the per-pass backend block (`readBackendBlock`), and
185
+ for the header, whose end must equal where the body region begins.
186
+
187
+ This rule carries weight that used to live elsewhere. The chunk container it replaces
188
+ spent roughly 600 lines asserting that its several projections of one effect still
189
+ agreed with each other, and those checks caught a malformed *tree* — our writer
190
+ emitting something structurally wrong — not only a malformed file. A record layout
191
+ makes most of that question unaskable, because containment replaces reference and
192
+ position replaces key. What remains is this: a writer bug either fails to parse,
193
+ which announces itself, or it parses and leaves the cursor somewhere other than the
194
+ declared end. Applying the rule to some sized records and not others is a gap that
195
+ stays invisible until a writer bug hides in one of the others.
196
+
197
+ **Rule 2: anything placed in the arena must be arena-independent.** An arena entry
198
+ cannot contain an arena offset. Offsets are assigned by the content sort, the sort
199
+ depends on every entry's bytes, so an entry that referred to the arena would have to
200
+ be interned before its own contents could be computed — a circular dependency with
201
+ no fixed point. This is invisible in Carbon's own code because no Carbon arena blob
202
+ refers to the arena: strings, bytecode and default constant values are all leaves.
203
+ Our per-pass backend block is the first non-leaf candidate, and it is why that block
204
+ carries inline length-prefixed strings instead of references. A test pins the
205
+ property directly — the block's bytes must be identical whichever arena it is
206
+ interned into. Any future arena entry must satisfy the same rule.
207
+
208
+ ### One field the container cannot round-trip
209
+
210
+ For a **non-dynamic sampler, the name is not preserved.** The file stores one, but
211
+ Carbon's reader nulls it before any producer sees it, so a package built from
212
+ our reflection carries the empty string.
213
+
214
+ This is a property of the input, not a bug in the mapping: the name is unrecoverable
215
+ by the time we receive the data, rather than dropped on the way out. Carbon nulls it
216
+ precisely because a non-dynamic sampler is never looked up by name — `FindSamplerByName`
217
+ only matters for the dynamic case. Recorded here because it will otherwise be
218
+ rediscovered as a bug: a diff against the source effect will always show it.
219
+
220
+ **Corollary: the container admits all six of Carbon's stage types.** `stages` is
221
+ capped at `SHADER_TYPE_COUNT` = 6, and the stage-type byte uses Carbon's
222
+ `InputStageType` numbering: vertex, pixel, compute, geometry, hull, domain. A
223
+ backend that can only express three of those rejects the rest in its own layer;
224
+ the container does not narrow on its behalf. The Carbon region is
225
+ backend-invariant, and restrictions belong to the backend.
226
+
227
+ `0xffffffff` is the null reference. It is legal at **exactly one wire
228
+ position**: a stage's default-constant-value offset when the accompanying size
229
+ is zero, which the optional-value reader consumes without dereferencing.
230
+ Everywhere else a `0xffffffff` offset fails the load.
231
+
232
+ Two deliberate departures from Carbon, both of which make byte-identical output
233
+ more likely rather than less:
234
+
235
+ - **`m_size` is initialised.** Carbon's constructor leaves it indeterminate and
236
+ gets away with it only because the one instance is a zero-initialised global.
237
+ - **Adding after an offset has been handed out is an error.** In Carbon,
238
+ `GetOffset` re-sorts a dirty table, which reassigns *every* offset — including
239
+ offsets already baked into packed bodies. Carbon avoids the corruption by
240
+ interning all late strings before the packing pass. `CjsCarbonEffectWriter`
241
+ reproduces that discipline structurally: it runs the record walk twice, once
242
+ with `collectArena` to intern and once with `internArena` to emit. Because both
243
+ passes drive the same `writeEffectDescription`, they cannot drift apart.
244
+
245
+ ### Description blob, v15 field order
246
+
247
+ Derived independently from the writer's save order and the reader's load order,
248
+ then confirmed to agree field for field. Counts are `u8` unless marked.
249
+
250
+ ```
251
+ u8 techniqueCount
252
+ u32 name
253
+ u8 passCount cap 64
254
+ u8 stageCount cap 6 (SHADER_TYPE_COUNT)
255
+ u8 stageType
256
+ u32 shaderSize | u32 shaderDataOffset program payload, arena blob
257
+ u32 threadGroupSize[3]
258
+ u8 pipelineInputCount cap 64
259
+ u8 usage, registerIndex, usageIndex, usedMask, type, dimension
260
+ -- StageData --
261
+ u8 registerCount
262
+ u8 registerType | u32 registerIndex | u32 registerCount | u8 registerSpace
263
+ u8 staticSamplerCount
264
+ u32 registerIndex | u8 registerSpace | u8 x7 filters/address
265
+ f32 mipLODBias | u8 maxAnisotropy | u8 comparisonFunc
266
+ u8 borderColor enum, NOT four floats
267
+ f32 minLOD | f32 maxLOD
268
+ u32 constantCount u32, not u8
269
+ u32 name | u32 offset | u32 size | u8 type | u8 dimension
270
+ u32 elements | u8 isSRGB | u8 isAutoregister
271
+ u32 defaultValuesSize | u32 defaultValuesOffset 0xffffffff legal when size 0
272
+ u8 textureCount cap 64
273
+ u8 registerIndex | u32 name | u8 type | u32 count | u8 isSRGB | u8 isAutoregister
274
+ u8 samplerCount cap 64
275
+ u8 registerIndex | u32 name | u8 x7 | f32 mipLODBias | u8 maxAnisotropy
276
+ u8 comparisonFunc | f32 borderColor[4] | f32 minLOD | f32 maxLOD | u8 isDynamic
277
+ u8 uavCount cap 64
278
+ u8 registerIndex | u32 name | u8 type | u32 count | u8 isAutoregister
279
+ u8 annotationCount
280
+ u32 name | u8 type | (u32 stringOffset if type == STRING else 4 raw bytes)
281
+ u8 renderStateCount cap 64
282
+ u32 state | u32 value
283
+ u8 libraryCount
284
+ u32 payloadSize | u32 shaderSize | u32 shaderDataOffset
285
+ u32 exportCount u32, not u8
286
+ u8 type | u32 name
287
+ u32 hitGroupName
288
+ StageData globalInputs
289
+ StageData localInputs
290
+ u16 parameterCount cap 256
291
+ u32 name | annotation map as above
292
+ ```
293
+
294
+ Four places this is easy to get wrong:
295
+
296
+ - **At v15 the program payload comes first and the signature tables follow.**
297
+ `pipelineInputs` and `registers` sit *after* `shaderCode` and `threadGroupSize`.
298
+ Before v14 it was the other way round; v14 moved them, and Carbon marks its own
299
+ v14 branch as uncertain.
300
+ - **A UAV record is one byte shorter than a texture record** — it has no `isSRGB`.
301
+ Carbon's reader hardcodes `isSRGB = false` and the writer omits it. Sharing one
302
+ "resource" codec between the two silently corrupts every subsequent field.
303
+ - **`borderColor` is four floats on a sampler and one byte on a static sampler**
304
+ because the two records mirror different D3D binding models.
305
+ - **A non-string annotation value is four raw bytes.** Carbon writes it through the
306
+ `float` member of a `{float,int32_t}` union and reads it back through a different
307
+ union. The bytes round-trip; applying an int/float conversion does not. The codec
308
+ keeps `rawValue` as bytes for exactly this reason.
309
+
310
+ Carbon writes `textures`, `samplers`, `uavs` and render states in ascending key
311
+ order and sorts annotation keys by bytewise string comparison.
312
+ `compareAnnotationNames` implements that comparison over UTF-8 bytes, which is
313
+ *not* the same as JavaScript's UTF-16 code-unit order for names outside ASCII
314
+ — `"Z"` sorts before `"a"`.
315
+
316
+ ### The optional trailing block
317
+
318
+ Our own containers add exactly one optional block per pass, after the render-state
319
+ table, referenced by a `{u32 size, u32 offset}` pair into the arena. A Carbon file
320
+ ends the pass at the render states, so the reader and writer gate it on
321
+ `{ backend: true }` and produce Carbon's bytes unchanged when it is closed.
322
+
323
+ The block carries the two sections that are not derivable from Carbon reflection —
324
+ WebGPU bind-group layouts and resource transforms — in **one** unit, because they
325
+ are mutually required and because "the Carbon region is backend-invariant, with
326
+ exactly one optional trailing block" is the invariant worth keeping.
327
+
328
+ It lives in the arena so identical layouts dedupe across bodies the way program
329
+ source does; measured sharing is 30.5:1 at `(body, pass)` granularity, 22 distinct
330
+ blocks across 672 pairs. That forces one property: **the block contains no arena
331
+ offsets.** An offset is only known after the arena's content sort, which depends on
332
+ every blob's bytes including this one, so a block referencing the arena could not be
333
+ built before it was interned. Strings inside it are inline and length-prefixed.
334
+
335
+ ```
336
+ u8 blobVersion = 1
337
+ u8 bindGroupCount
338
+ u8 group | u8 bindingCount
339
+ u8 resourceKind | u8 registerSpace | u8 binding | u8 visibilityMask
340
+ u32 registerIndex | u32 structureStride (0xffffffff absent) | u8 arrayLayerCount (0 absent)
341
+ str type | str generatedSymbol | str transformId (empty = none)
342
+ u8 transformCount
343
+ u8 familyCode | str id | u8 inputCount
344
+ u8 registerSpace | u8 registerIndex | str parameter
345
+ ```
346
+
347
+ `identity` and `group` on each binding, and a transform's `kind`,
348
+ `stage`, `representation`, `missingLayer`, `viewDimension`, `layerCount`,
349
+ `output.identity`, `output.scopeIdentity`, `output.name`, `layoutKey` and every
350
+ input's `layer` are restored on read, not stored. The family byte is what keeps them
351
+ derivable without pinning the format to one recognizer. `id` and each input's
352
+ `parameter` stay on the wire deliberately — `id` because a caller may supply it,
353
+ `parameter` because it keeps layer identity cross-checkable rather than asserted by
354
+ position.
355
+
356
+ Backend-block version 1 stores visibility but not the original
357
+ `scopeIdentity`. The reader reconstructs `${identity}@${visibility[0]}`. A
358
+ multi-stage shared binding therefore rereads as stage-qualified rather than
359
+ recovering its original bare scope. Callers must not infer that original
360
+ sharing decision from the wire view.
361
+
362
+ An unknown `blobVersion` reports the pass as having no backend data rather than
363
+ misparsing it; the enclosing size makes it skippable.
364
+
365
+ ### Count caps
366
+
367
+ `CARBON_EFFECT_COUNT_CAPS` mirrors the runtime's inclusive limits. Carbon's
368
+ compiler enforces none of them while its runtime rejects anything above them,
369
+ so an over-large effect compiles and then fails to load; our writer checks on
370
+ the way out. The caps Carbon does *not* have — techniques, registers, static
371
+ samplers, constants, libraries, exports, annotation counts — are deliberately
372
+ not invented here.
373
+
374
+ ### The alias path
375
+
376
+ Carbon compares packed bodies pairwise and points a duplicate's row at the
377
+ surviving twin. The row is **kept**, so the offset table stays dense while the
378
+ file stores each distinct body once. Across the shipped corpus 22% of files
379
+ alias, at roughly 2.1 rows per distinct body.
380
+
381
+ `enumerateUniqueEffectBodies` recovers that grouping without decoding a single
382
+ body. It inspects source records and bytes only, and returns
383
+ first-occurrence-ordered groups, each holding one canonical
384
+ `permutationIndex`/`sourceRecord` plus every byte-identical alias. Exact range
385
+ aliases are the fast path; distinct ranges are fingerprinted and then compared
386
+ byte for byte. It caps the Cartesian body table at 65,536 records
387
+ (`EFFECT_BODY_COUNT_MAX`) and rejects partial overlaps, because a partial
388
+ overlap means two bodies claim the same bytes. It decodes nothing: it reads
389
+ `m_offsets` and slices `m_data` directly, so a backend packager can inventory an
390
+ effect without disturbing anything a later read depends on. It is internal
391
+ (`src/format/effect/`), not a published export; the backend body set is its one
392
+ consumer.
393
+
394
+ ## Offset-table density
395
+
396
+ Carbon indexes the offset table **positionally** and never reads each row's
397
+ stored `index` field. A sparse or misordered table therefore does not fail — it
398
+ silently returns the wrong shader body.
399
+
400
+ Density is incidental in Carbon: it falls out of `g_compiledEffects` being a
401
+ `std::map` densely keyed by the work-queue builder, and is promised nowhere.
402
+
403
+ Measured twice at build 3444265. A header-only sweep of every `.sm_hi`, `.sm_lo`
404
+ and `.sm_depth` under `effect.dx11` and `effect.dx12` — 3222 files, 52,332 rows —
405
+ and the full round-trip run below across all three backends — 4833 files, 78,498
406
+ rows, 40,645 distinct bodies. **Every file is dense and positionally indexed**, and
407
+ every row's byte range lies inside the file and clear of the header. In every
408
+ single file the body region tiles the post-header space exactly, with no leading or
409
+ trailing slack.
410
+
411
+ Given that, the checks are implemented as follows:
412
+
413
+ - `CjsCarbonEffectReader` **always** collects `dense` and `indicesMatchPosition` as
414
+ diagnostics, and **always** fails closed on an out-of-range row.
415
+ - Density and positional indexing **fail closed on read by default**.
416
+ `{ permissive: true }` skips the check and leaves the diagnostics in place, for
417
+ forensic inspection of a file already known to be malformed. It is not a load
418
+ option.
419
+ - `writeCarbonEffectFile` **always** fails closed: it refuses to emit bodies that
420
+ are not dense from index 0. Where we own the bytes there is no reason to be
421
+ lenient.
422
+
423
+ `--ignore-permutations` does make CCP's compiler emit only key 0 while declaring
424
+ every axis, so a sparse file is producible. That argues for the escape hatch, not
425
+ for permissive defaults: Carbon does not reject such a file, it returns the wrong
426
+ permutation's shader silently, which is the failure class this port exists to
427
+ close.
428
+
429
+ ## Backend selection and envelope removal
430
+
431
+ **Historical compatibility.** Flat-package builds once prepended
432
+ `magic | containerVersion | payloadKind` before Carbon's byte-compatible
433
+ layout. Current packaging emits no such prefix and the former helper is no
434
+ longer part of the package.
435
+
436
+ **Current WebGPU wire.** Carbon WebGPU bytes have no envelope, magic, `payloadKind`,
437
+ or independent container version. They are bare Carbon v15 records resolved
438
+ from `effect.webgpu/`, with one optional per-pass backend block. `.carbonwebgl`
439
+ remains a separate Carbon WebGL chunk format rather than this Carbon-record wire.
440
+
441
+ `CarbonWebgpuContainer` reads the optional blocks, and the shared record reader can
442
+ auto-detect them from a description's declared size. There is no adapter
443
+ boundary left between them and the runtime: `Tr2EffectRes.DoLoad` retains a
444
+ `CjsCarbonEffectReader` over the same bytes and `Tr2Shader.fromCarbonBinary`
445
+ builds the device-free graph from one description record.
446
+
447
+ Versioning remains local to what it versions: Carbon's version dword governs
448
+ the Carbon region, while `blobVersion` governs the optional backend block. An
449
+ unknown block version is skipped rather than misparsed. The package does not
450
+ claim a version in CCP's namespace.
451
+
452
+ Loose program bytes without a resource path can be identified from their
453
+ payload: DXBC opens with `"DXBC"`, AIR is bitcode (`BC 0xC0DE`), and WGSL and
454
+ GLSL have distinct text syntax. The stage record itself carries no language
455
+ tag; program interpretation remains a backend/path responsibility.
456
+
457
+ ## Verification
458
+
459
+ `node --test` in this package. Two gates.
460
+
461
+ **Always green.** `test/format/byte-primitives.test.js` and
462
+ `test/format/carbon-effect.test.js` build a synthetic four-permutation v15
463
+ container exercising every record type — static samplers, UAVs, annotations of
464
+ every value type, render states, a raytracing library with both stage-data blocks
465
+ — and assert a byte-exact write → read → write round trip, the arena sort order,
466
+ the caps, the structural checks and, until the switchover, the legacy envelope's
467
+ disjointness.
468
+
469
+ **Env-gated real-file proof.** `test/format/carbon-effect-corpus.test.js`, enabled
470
+ with `CARBON_EFFECT_CORPUS_DIR`. Game bytes are never committed. Supply a
471
+ separately acquired corpus at pinned build 3444265.
472
+
473
+ `CARBON_EFFECT_CORPUS_DIR` must point at a **materialised tree of source
474
+ effects** — real `.sm_hi` / `.sm_lo` / `.sm_depth` filenames under
475
+ `effect.dx11`, `effect.dx12`, or `effect.gles2`, which is what the walker
476
+ filters on. Two nearby directories look like corpora and are not:
477
+
478
+ - a content-addressed resource store (hash-named files, no extensions) matches
479
+ nothing and fails as `no compiled effect files found` — the walker is
480
+ extension-driven, so an unextracted cache silently yields zero files;
481
+ - a directory of **our own translated output** keeps the `.sm_*` names but
482
+ holds `Carbon WebGL`/`Carbon WebGPU` containers, and fails with
483
+ `Unsupported Carbon effect version 1196901699` — that number is the ASCII
484
+ `Carbon WebGL` magic read as a version dword.
485
+
486
+ Both failures are loud, which is the intended behaviour; neither is a defect.
487
+ The test re-emits each file three ways:
488
+
489
+ 1. every description blob through the file's own arena — proves the field order;
490
+ 2. the whole container from raw bodies and the source arena — proves the header
491
+ order, the base arithmetic and the alias path;
492
+ 3. the whole container with the arena rebuilt from the references found — proves
493
+ the sorted-offset policy.
494
+
495
+ Only the third can legitimately differ, because an arena may retain blobs the file
496
+ no longer references. When it does differ the divergence is reported exactly and
497
+ asserted to be unreferenced-blob retention; it is never downgraded to a weaker
498
+ comparison such as "same strings, any order", which would look green and prove
499
+ nothing.
500
+
501
+ Measured result over the complete corpus — 4833 files (537 shaders × 3 variants ×
502
+ 3 backends), 78,498 offset-table rows, 40,645 distinct description bodies:
503
+ **all three modes byte-exact, with zero arena-rebuild divergences, zero sparse
504
+ tables and zero misordered tables.** Not one shipped file retains an unreferenced
505
+ arena blob, so the sorted-offset policy reproduces CCP's arena exactly.
506
+ Supply a separately acquired corpus and set `CARBON_EFFECT_CORPUS_DIR`; no
507
+ corpus data ships with the package.
508
+
509
+ **The same proof, one level up.** `test/resource/effect-res-corpus.test.js`
510
+ closes the loop through the resource classes rather than the records: bytes →
511
+ `Tr2EffectRes.DoLoad` → `Tr2Shader.fromCarbonBinary` → the device-free graph →
512
+ `toCarbonBinary()` → records → bytes. Running the record-level round trip on the
513
+ same files with no classes involved is the negative control; when the two
514
+ disagree, the difference is the class layer's, and that is how the three
515
+ fidelity losses it once had were found and closed — non-dynamic sampler names
516
+ (1,631 files), authored pass-stage order (21 files), and the offset word of a
517
+ zero-size blob (150 files), which is not always the `0xffffffff` sentinel.
518
+ Carbon's *runtime* discards the first two; the file does not, so re-emitting the
519
+ file it came from means keeping them.
520
+
521
+ That result is the container port's central evidence. The same reader and the same
522
+ writer reproduce, byte for byte, files whose program payloads are DXBC in two
523
+ dialects and AIR — with no language field anywhere in the format. The metadata
524
+ region is backend-invariant as a measured fact rather than an argument from the
525
+ writer. Backend selection therefore belongs at the resource-path boundary, not
526
+ in an envelope or per-stage record.
527
+
528
+ `effect.gles2` is deliberately not a validation target for **this package**: those
529
+ shaders are v8, and nothing in the container port reads or writes them.
530
+
531
+ **Do not read that as "obsolete".** `effect.gles2` is the shader tree ccpwgl
532
+ actually renders with today — it is the only one that currently works end to end.
533
+
534
+ The two statements coexist because **v15-only constrains what we write and
535
+ validate against, not what a reader may accept.** Version-branching is the
536
+ format's own mechanism. A reader that wants all supported generations branches
537
+ on the version dword — v2..8 legacy gles2, v15 everything current — which is one
538
+ reader, not a bespoke path per format.
539
+
540
+ **Our containers are v15, not a version of our own.** A "v16" was considered for
541
+ the variant carrying the per-pass backend block and **rejected**: CCP owns that
542
+ number space, so claiming 16 would collide with any real v16 they ship, in the one
543
+ field whose entire job is telling a reader how to parse. It also failed the rule
544
+ the rest of this format is held to — invent something only because it *has to*
545
+ exist, never because we think it should.
546
+
547
+ The container needs no new version. Each description blob carries a declared
548
+ size in the offset table, and [Rule 1](#two-rules-for-anything-added-later)
549
+ already requires it to parse to exactly that end. A reader parses a blob without
550
+ blocks and re-parses with them if the cursor misses the declared end, so the
551
+ presence of the block is **self-describing** with no new field, no container
552
+ version and no out-of-band flag. `blobVersion` inside the block versions the
553
+ extension itself.