@forgeax/engine-rhi-debug 0.0.0-dev.8d955ade1c79

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 (237) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +178 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/bootstrap-integration.test.d.ts +2 -0
  5. package/dist/__tests__/bootstrap-integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/consumer-inventory.unit.test.d.ts +2 -0
  7. package/dist/__tests__/consumer-inventory.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts +15 -0
  9. package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts.map +1 -0
  10. package/dist/__tests__/coverage-invariant.test-d.d.ts +2 -0
  11. package/dist/__tests__/coverage-invariant.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/coverage-invariant.unit.test.d.ts +2 -0
  13. package/dist/__tests__/coverage-invariant.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/e2e.browser.test.d.ts +2 -0
  15. package/dist/__tests__/e2e.browser.test.d.ts.map +1 -0
  16. package/dist/__tests__/error-code-narrowing.test-d.d.ts +2 -0
  17. package/dist/__tests__/error-code-narrowing.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/errors-v7.test-d.d.ts +2 -0
  19. package/dist/__tests__/errors-v7.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/errors-v7.unit.test.d.ts +2 -0
  21. package/dist/__tests__/errors-v7.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  23. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/event-semantics-coverage.unit.test.d.ts +2 -0
  25. package/dist/__tests__/event-semantics-coverage.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/format-info.unit.test.d.ts +2 -0
  27. package/dist/__tests__/format-info.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/frame-model-parity.test-d.d.ts +2 -0
  29. package/dist/__tests__/frame-model-parity.test-d.d.ts.map +1 -0
  30. package/dist/__tests__/frame-model-parity.unit.test.d.ts +2 -0
  31. package/dist/__tests__/frame-model-parity.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/guard-gates.test.d.ts +2 -0
  33. package/dist/__tests__/guard-gates.test.d.ts.map +1 -0
  34. package/dist/__tests__/public-surface.integration.test.d.ts +2 -0
  35. package/dist/__tests__/public-surface.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts +2 -0
  37. package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/readback-format-matrix.dawn.test.d.ts +2 -0
  39. package/dist/__tests__/readback-format-matrix.dawn.test.d.ts.map +1 -0
  40. package/dist/__tests__/readback-format-matrix.unit.test.d.ts +2 -0
  41. package/dist/__tests__/readback-format-matrix.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
  43. package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
  44. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts +2 -0
  45. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
  47. package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
  49. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
  51. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
  53. package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
  55. package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
  56. package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
  57. package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
  59. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
  60. package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
  61. package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
  63. package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
  64. package/dist/__tests__/replay-session.test-d.d.ts +2 -0
  65. package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
  66. package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
  67. package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
  69. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
  71. package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
  73. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
  74. package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
  75. package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
  77. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
  79. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
  81. package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
  83. package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/types.test.d.ts +2 -0
  85. package/dist/__tests__/types.test.d.ts.map +1 -0
  86. package/dist/browser.d.ts +45 -0
  87. package/dist/browser.d.ts.map +1 -0
  88. package/dist/browser.mjs +83 -0
  89. package/dist/browser.mjs.map +1 -0
  90. package/dist/errors.d.ts +85 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/frame-model.d.ts +158 -0
  93. package/dist/frame-model.d.ts.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.mjs +10415 -0
  97. package/dist/index.mjs.map +1 -0
  98. package/dist/protocol/codec.d.ts +6 -0
  99. package/dist/protocol/codec.d.ts.map +1 -0
  100. package/dist/protocol/event-semantics.d.ts +15 -0
  101. package/dist/protocol/event-semantics.d.ts.map +1 -0
  102. package/dist/protocol/tape-index.d.ts +30 -0
  103. package/dist/protocol/tape-index.d.ts.map +1 -0
  104. package/dist/protocol/types.d.ts +44 -0
  105. package/dist/protocol/types.d.ts.map +1 -0
  106. package/dist/protocol/validation.d.ts +7 -0
  107. package/dist/protocol/validation.d.ts.map +1 -0
  108. package/dist/readback.d.ts +156 -0
  109. package/dist/readback.d.ts.map +1 -0
  110. package/dist/recorder/assemble.d.ts +11 -0
  111. package/dist/recorder/assemble.d.ts.map +1 -0
  112. package/dist/recorder/closure.d.ts +39 -0
  113. package/dist/recorder/closure.d.ts.map +1 -0
  114. package/dist/recorder/core.d.ts +296 -0
  115. package/dist/recorder/core.d.ts.map +1 -0
  116. package/dist/recorder/device.d.ts +4 -0
  117. package/dist/recorder/device.d.ts.map +1 -0
  118. package/dist/recorder/encoder.d.ts +5 -0
  119. package/dist/recorder/encoder.d.ts.map +1 -0
  120. package/dist/recorder/lifecycle.d.ts +20 -0
  121. package/dist/recorder/lifecycle.d.ts.map +1 -0
  122. package/dist/recorder/pass.d.ts +6 -0
  123. package/dist/recorder/pass.d.ts.map +1 -0
  124. package/dist/recorder/proxy.d.ts +19 -0
  125. package/dist/recorder/proxy.d.ts.map +1 -0
  126. package/dist/recorder/queue.d.ts +4 -0
  127. package/dist/recorder/queue.d.ts.map +1 -0
  128. package/dist/recorder/resource-registry.d.ts +15 -0
  129. package/dist/recorder/resource-registry.d.ts.map +1 -0
  130. package/dist/recorder/session.d.ts +25 -0
  131. package/dist/recorder/session.d.ts.map +1 -0
  132. package/dist/recorder/shader.d.ts +3 -0
  133. package/dist/recorder/shader.d.ts.map +1 -0
  134. package/dist/recorder/snapshot.d.ts +10 -0
  135. package/dist/recorder/snapshot.d.ts.map +1 -0
  136. package/dist/recorder/wrap.d.ts +4 -0
  137. package/dist/recorder/wrap.d.ts.map +1 -0
  138. package/dist/recorder.d.ts +7 -0
  139. package/dist/recorder.d.ts.map +1 -0
  140. package/dist/replay/device-request.d.ts +8 -0
  141. package/dist/replay/device-request.d.ts.map +1 -0
  142. package/dist/replay/execute-support.d.ts +33 -0
  143. package/dist/replay/execute-support.d.ts.map +1 -0
  144. package/dist/replay/execute.d.ts +18 -0
  145. package/dist/replay/execute.d.ts.map +1 -0
  146. package/dist/replay/readback.d.ts +32 -0
  147. package/dist/replay/readback.d.ts.map +1 -0
  148. package/dist/replay/resources.d.ts +57 -0
  149. package/dist/replay/resources.d.ts.map +1 -0
  150. package/dist/replay/session.d.ts +49 -0
  151. package/dist/replay/session.d.ts.map +1 -0
  152. package/dist/replay/texture-format.d.ts +21 -0
  153. package/dist/replay/texture-format.d.ts.map +1 -0
  154. package/dist/texel-decode.d.ts +35 -0
  155. package/dist/texel-decode.d.ts.map +1 -0
  156. package/dist/texel-layout.d.ts +76 -0
  157. package/dist/texel-layout.d.ts.map +1 -0
  158. package/dist/types.d.ts +722 -0
  159. package/dist/types.d.ts.map +1 -0
  160. package/package.json +72 -0
  161. package/src/__tests__/bootstrap-integration.test.ts +268 -0
  162. package/src/__tests__/consumer-inventory.unit.test.ts +154 -0
  163. package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
  164. package/src/__tests__/coverage-invariant.test-d.ts +115 -0
  165. package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
  166. package/src/__tests__/e2e.browser.test.ts +239 -0
  167. package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
  168. package/src/__tests__/errors-v7.test-d.ts +19 -0
  169. package/src/__tests__/errors-v7.unit.test.ts +15 -0
  170. package/src/__tests__/errors.unit.test.ts +83 -0
  171. package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
  172. package/src/__tests__/format-info.unit.test.ts +85 -0
  173. package/src/__tests__/frame-model-parity.test-d.ts +20 -0
  174. package/src/__tests__/frame-model-parity.unit.test.ts +285 -0
  175. package/src/__tests__/guard-gates.test.ts +317 -0
  176. package/src/__tests__/public-surface.integration.test.ts +38 -0
  177. package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
  178. package/src/__tests__/readback-format-matrix.dawn.test.ts +378 -0
  179. package/src/__tests__/readback-format-matrix.unit.test.ts +120 -0
  180. package/src/__tests__/readback-matrix-fixture.ts +20 -0
  181. package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
  182. package/src/__tests__/recorder-budget.unit.test.ts +42 -0
  183. package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
  184. package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
  185. package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
  186. package/src/__tests__/recorder-session.integration.test.ts +32 -0
  187. package/src/__tests__/recorder-session.unit.test.ts +63 -0
  188. package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
  189. package/src/__tests__/replay-fail-fast.unit.test.ts +155 -0
  190. package/src/__tests__/replay-session.dawn.test.ts +220 -0
  191. package/src/__tests__/replay-session.test-d.ts +27 -0
  192. package/src/__tests__/replay-session.unit.test.ts +171 -0
  193. package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
  194. package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
  195. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +178 -0
  196. package/src/__tests__/tape-index.unit.test.ts +56 -0
  197. package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
  198. package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
  199. package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
  200. package/src/__tests__/tree-shake.unit.test.ts +151 -0
  201. package/src/__tests__/types.test.ts +257 -0
  202. package/src/browser.ts +146 -0
  203. package/src/errors.ts +173 -0
  204. package/src/frame-model.ts +753 -0
  205. package/src/index.ts +92 -0
  206. package/src/pako.d.ts +8 -0
  207. package/src/protocol/codec.ts +220 -0
  208. package/src/protocol/event-semantics.ts +180 -0
  209. package/src/protocol/tape-index.ts +120 -0
  210. package/src/protocol/types.ts +60 -0
  211. package/src/protocol/validation.ts +173 -0
  212. package/src/readback.ts +804 -0
  213. package/src/recorder/assemble.ts +192 -0
  214. package/src/recorder/closure.ts +408 -0
  215. package/src/recorder/core.ts +587 -0
  216. package/src/recorder/device.ts +474 -0
  217. package/src/recorder/encoder.ts +251 -0
  218. package/src/recorder/lifecycle.ts +847 -0
  219. package/src/recorder/pass.ts +241 -0
  220. package/src/recorder/proxy.ts +59 -0
  221. package/src/recorder/queue.ts +109 -0
  222. package/src/recorder/resource-registry.ts +53 -0
  223. package/src/recorder/session.ts +152 -0
  224. package/src/recorder/shader.ts +47 -0
  225. package/src/recorder/snapshot.ts +38 -0
  226. package/src/recorder/wrap.ts +157 -0
  227. package/src/recorder.ts +15 -0
  228. package/src/replay/device-request.ts +38 -0
  229. package/src/replay/execute-support.ts +243 -0
  230. package/src/replay/execute.ts +870 -0
  231. package/src/replay/readback.ts +671 -0
  232. package/src/replay/resources.ts +154 -0
  233. package/src/replay/session.ts +509 -0
  234. package/src/replay/texture-format.ts +99 -0
  235. package/src/texel-decode.ts +289 -0
  236. package/src/texel-layout.ts +324 -0
  237. package/src/types.ts +888 -0
@@ -0,0 +1,324 @@
1
+ /// <reference types="@webgpu/types" />
2
+ // @forgeax/engine-rhi-debug/texel-layout -- SSOT for how a color texture's GPU
3
+ // bytes are laid out, shared by the snapshot readback (recorder) and the seed
4
+ // write-back (replayer) so both agree on byte layout without duplicating it in
5
+ // the tape (architecture-principles #1 SSOT / #2 Derive: format + size +
6
+ // mipLevelCount already live in the createTexture event; layout is derived, not
7
+ // re-stored in the initialData event).
8
+ //
9
+ // Why this exists: the original frame-header snapshot hardcoded bytesPerRow =
10
+ // width*4, depthOrArrayLayers = 1, mipLevel = 0 -- correct only for a single-mip
11
+ // single-layer 4-byte texture (rgba8). IBL resources break all three: the
12
+ // irradiance/prefilter cubemaps are rgba16float (8 B/texel), 6 array layers, and
13
+ // the prefilter map has a 5-mip roughness chain. Snapshotting them with the old
14
+ // assumptions either skipped them (-> replay renders unlit/black) or would have
15
+ // seeded corrupt bytes. This module computes the real per-subresource layout so a
16
+ // full snapshot + faithful seed round-trips each color format with a known texel
17
+ // block footprint, including block-compressed formats.
18
+
19
+ /**
20
+ * Bytes per texel for uncompressed color formats. Block-compressed formats use
21
+ * {@link textureBlockLayout} because their bytes are addressed by blocks.
22
+ */
23
+ export function bytesPerTexel(format: GPUTextureFormat | undefined): number | undefined {
24
+ if (format === undefined) return undefined;
25
+ return TEXEL_BYTES[format];
26
+ }
27
+
28
+ // Uncompressed color formats only. Keyed to the W3C WebGPU GPUTextureFormat
29
+ // names. Depth/stencil and block-compressed formats are deliberately absent.
30
+ const TEXEL_BYTES: Partial<Record<GPUTextureFormat, number>> = {
31
+ // 8-bit channels
32
+ r8unorm: 1,
33
+ r8snorm: 1,
34
+ r8uint: 1,
35
+ r8sint: 1,
36
+ rg8unorm: 2,
37
+ rg8snorm: 2,
38
+ rg8uint: 2,
39
+ rg8sint: 2,
40
+ rgba8unorm: 4,
41
+ 'rgba8unorm-srgb': 4,
42
+ rgba8snorm: 4,
43
+ rgba8uint: 4,
44
+ rgba8sint: 4,
45
+ bgra8unorm: 4,
46
+ 'bgra8unorm-srgb': 4,
47
+ // 16-bit channels
48
+ r16uint: 2,
49
+ r16sint: 2,
50
+ r16float: 2,
51
+ rg16uint: 4,
52
+ rg16sint: 4,
53
+ rg16float: 4,
54
+ rgba16uint: 8,
55
+ rgba16sint: 8,
56
+ rgba16float: 8,
57
+ // 32-bit channels
58
+ r32uint: 4,
59
+ r32sint: 4,
60
+ r32float: 4,
61
+ rg32uint: 8,
62
+ rg32sint: 8,
63
+ rg32float: 8,
64
+ rgba32uint: 16,
65
+ rgba32sint: 16,
66
+ rgba32float: 16,
67
+ // packed
68
+ rgb10a2unorm: 4,
69
+ rg11b10ufloat: 4,
70
+ };
71
+
72
+ export interface TextureBlockLayout {
73
+ readonly blockWidth: number;
74
+ readonly blockHeight: number;
75
+ readonly bytesPerBlock: number;
76
+ }
77
+
78
+ const COMPRESSED_BLOCKS: Partial<Record<GPUTextureFormat, TextureBlockLayout>> = {
79
+ // BCn: all formats use 4x4 blocks; BC1/BC4 are 8 bytes, the rest 16.
80
+ 'bc1-rgba-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
81
+ 'bc1-rgba-unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
82
+ 'bc2-rgba-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
83
+ 'bc2-rgba-unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
84
+ 'bc3-rgba-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
85
+ 'bc3-rgba-unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
86
+ 'bc4-r-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
87
+ 'bc4-r-snorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
88
+ 'bc5-rg-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
89
+ 'bc5-rg-snorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
90
+ 'bc6h-rgb-ufloat': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
91
+ 'bc6h-rgb-float': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
92
+ 'bc7-rgba-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
93
+ 'bc7-rgba-unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
94
+ // ETC2/EAC: all formats use 4x4 blocks; one or two 64-bit blocks.
95
+ 'etc2-rgb8unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
96
+ 'etc2-rgb8unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
97
+ 'etc2-rgb8a1unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
98
+ 'etc2-rgb8a1unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
99
+ 'etc2-rgba8unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
100
+ 'etc2-rgba8unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
101
+ 'eac-r11unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
102
+ 'eac-r11snorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 8 },
103
+ 'eac-rg11unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
104
+ 'eac-rg11snorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
105
+ // ASTC uses 16-byte blocks with a format-specific footprint.
106
+ 'astc-4x4-unorm': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
107
+ 'astc-4x4-unorm-srgb': { blockWidth: 4, blockHeight: 4, bytesPerBlock: 16 },
108
+ 'astc-5x4-unorm': { blockWidth: 5, blockHeight: 4, bytesPerBlock: 16 },
109
+ 'astc-5x4-unorm-srgb': { blockWidth: 5, blockHeight: 4, bytesPerBlock: 16 },
110
+ 'astc-5x5-unorm': { blockWidth: 5, blockHeight: 5, bytesPerBlock: 16 },
111
+ 'astc-5x5-unorm-srgb': { blockWidth: 5, blockHeight: 5, bytesPerBlock: 16 },
112
+ 'astc-6x5-unorm': { blockWidth: 6, blockHeight: 5, bytesPerBlock: 16 },
113
+ 'astc-6x5-unorm-srgb': { blockWidth: 6, blockHeight: 5, bytesPerBlock: 16 },
114
+ 'astc-6x6-unorm': { blockWidth: 6, blockHeight: 6, bytesPerBlock: 16 },
115
+ 'astc-6x6-unorm-srgb': { blockWidth: 6, blockHeight: 6, bytesPerBlock: 16 },
116
+ 'astc-8x5-unorm': { blockWidth: 8, blockHeight: 5, bytesPerBlock: 16 },
117
+ 'astc-8x5-unorm-srgb': { blockWidth: 8, blockHeight: 5, bytesPerBlock: 16 },
118
+ 'astc-8x6-unorm': { blockWidth: 8, blockHeight: 6, bytesPerBlock: 16 },
119
+ 'astc-8x6-unorm-srgb': { blockWidth: 8, blockHeight: 6, bytesPerBlock: 16 },
120
+ 'astc-8x8-unorm': { blockWidth: 8, blockHeight: 8, bytesPerBlock: 16 },
121
+ 'astc-8x8-unorm-srgb': { blockWidth: 8, blockHeight: 8, bytesPerBlock: 16 },
122
+ 'astc-10x5-unorm': { blockWidth: 10, blockHeight: 5, bytesPerBlock: 16 },
123
+ 'astc-10x5-unorm-srgb': { blockWidth: 10, blockHeight: 5, bytesPerBlock: 16 },
124
+ 'astc-10x6-unorm': { blockWidth: 10, blockHeight: 6, bytesPerBlock: 16 },
125
+ 'astc-10x6-unorm-srgb': { blockWidth: 10, blockHeight: 6, bytesPerBlock: 16 },
126
+ 'astc-10x8-unorm': { blockWidth: 10, blockHeight: 8, bytesPerBlock: 16 },
127
+ 'astc-10x8-unorm-srgb': { blockWidth: 10, blockHeight: 8, bytesPerBlock: 16 },
128
+ 'astc-10x10-unorm': { blockWidth: 10, blockHeight: 10, bytesPerBlock: 16 },
129
+ 'astc-10x10-unorm-srgb': { blockWidth: 10, blockHeight: 10, bytesPerBlock: 16 },
130
+ 'astc-12x10-unorm': { blockWidth: 12, blockHeight: 10, bytesPerBlock: 16 },
131
+ 'astc-12x10-unorm-srgb': { blockWidth: 12, blockHeight: 10, bytesPerBlock: 16 },
132
+ 'astc-12x12-unorm': { blockWidth: 12, blockHeight: 12, bytesPerBlock: 16 },
133
+ 'astc-12x12-unorm-srgb': { blockWidth: 12, blockHeight: 12, bytesPerBlock: 16 },
134
+ };
135
+
136
+ /** Return the WebGPU texel-block footprint for each supported color format. */
137
+ export function textureBlockLayout(
138
+ format: GPUTextureFormat | undefined,
139
+ ): TextureBlockLayout | undefined {
140
+ if (format === undefined) return undefined;
141
+ return (
142
+ COMPRESSED_BLOCKS[format] ??
143
+ (TEXEL_BYTES[format] === undefined
144
+ ? undefined
145
+ : { blockWidth: 1, blockHeight: 1, bytesPerBlock: TEXEL_BYTES[format] })
146
+ );
147
+ }
148
+
149
+ // ============================================================================
150
+ // formatInfo -- per-channel semantics for CPU-side decode (viewer preview)
151
+ // ============================================================================
152
+
153
+ /** How a channel's bits are interpreted (drives the host-side decode-to-RGBA8). */
154
+ export type ChannelType = 'unorm' | 'snorm' | 'uint' | 'sint' | 'float' | 'ufloat';
155
+
156
+ /**
157
+ * Per-channel layout of an uncompressed color format, enough to decode its raw
158
+ * GPU bytes into displayable RGBA8 on the host (no GPU). Sits beside
159
+ * {@link bytesPerTexel} (which stays the SSOT for total texel size); the
160
+ * per-channel byte width is derived as `bytesPerTexel / channels` for plain
161
+ * formats, so no byte count is re-stored here. `packed` formats pack all
162
+ * channels into a single 4-byte word (channels read by bit-field, not by a
163
+ * uniform per-channel width).
164
+ */
165
+ export interface FormatInfo {
166
+ readonly channels: 1 | 2 | 3 | 4;
167
+ readonly channelType: ChannelType;
168
+ /** Channels stored blue-first in memory (swizzle B<->R when decoding). */
169
+ readonly bgra?: boolean;
170
+ /** Bit-packed layout (channels share one 32-bit word) needing special unpack. */
171
+ readonly packed?: 'rgb10a2unorm' | 'rg11b10ufloat';
172
+ }
173
+
174
+ // Keyed to the same uncompressed color formats as TEXEL_BYTES. Depth/stencil and
175
+ // block-compressed formats are absent (return undefined -> caller falls back).
176
+ const FORMAT_INFO: Partial<Record<GPUTextureFormat, FormatInfo>> = {
177
+ // 8-bit channels
178
+ r8unorm: { channels: 1, channelType: 'unorm' },
179
+ r8snorm: { channels: 1, channelType: 'snorm' },
180
+ r8uint: { channels: 1, channelType: 'uint' },
181
+ r8sint: { channels: 1, channelType: 'sint' },
182
+ rg8unorm: { channels: 2, channelType: 'unorm' },
183
+ rg8snorm: { channels: 2, channelType: 'snorm' },
184
+ rg8uint: { channels: 2, channelType: 'uint' },
185
+ rg8sint: { channels: 2, channelType: 'sint' },
186
+ rgba8unorm: { channels: 4, channelType: 'unorm' },
187
+ 'rgba8unorm-srgb': { channels: 4, channelType: 'unorm' },
188
+ rgba8snorm: { channels: 4, channelType: 'snorm' },
189
+ rgba8uint: { channels: 4, channelType: 'uint' },
190
+ rgba8sint: { channels: 4, channelType: 'sint' },
191
+ bgra8unorm: { channels: 4, channelType: 'unorm', bgra: true },
192
+ 'bgra8unorm-srgb': { channels: 4, channelType: 'unorm', bgra: true },
193
+ // 16-bit channels
194
+ r16uint: { channels: 1, channelType: 'uint' },
195
+ r16sint: { channels: 1, channelType: 'sint' },
196
+ r16float: { channels: 1, channelType: 'float' },
197
+ rg16uint: { channels: 2, channelType: 'uint' },
198
+ rg16sint: { channels: 2, channelType: 'sint' },
199
+ rg16float: { channels: 2, channelType: 'float' },
200
+ rgba16uint: { channels: 4, channelType: 'uint' },
201
+ rgba16sint: { channels: 4, channelType: 'sint' },
202
+ rgba16float: { channels: 4, channelType: 'float' },
203
+ // 32-bit channels
204
+ r32uint: { channels: 1, channelType: 'uint' },
205
+ r32sint: { channels: 1, channelType: 'sint' },
206
+ r32float: { channels: 1, channelType: 'float' },
207
+ rg32uint: { channels: 2, channelType: 'uint' },
208
+ rg32sint: { channels: 2, channelType: 'sint' },
209
+ rg32float: { channels: 2, channelType: 'float' },
210
+ rgba32uint: { channels: 4, channelType: 'uint' },
211
+ rgba32sint: { channels: 4, channelType: 'sint' },
212
+ rgba32float: { channels: 4, channelType: 'float' },
213
+ // packed (channels share one 32-bit word; decoded by bit-field)
214
+ rgb10a2unorm: { channels: 4, channelType: 'unorm', packed: 'rgb10a2unorm' },
215
+ rg11b10ufloat: { channels: 3, channelType: 'ufloat', packed: 'rg11b10ufloat' },
216
+ };
217
+
218
+ /**
219
+ * Per-channel layout of an uncompressed color format for host-side decode.
220
+ * Returns `undefined` for depth/stencil, block-compressed, or unknown formats
221
+ * (the viewer's preview path treats `undefined` as "not directly previewable").
222
+ */
223
+ export function formatInfo(format: GPUTextureFormat | string | undefined): FormatInfo | undefined {
224
+ if (format === undefined) return undefined;
225
+ return FORMAT_INFO[format as GPUTextureFormat];
226
+ }
227
+
228
+ /** One mip level of one array layer: its extent + where its tight bytes live in the blob. */
229
+ export interface SubresourceSlice {
230
+ readonly layer: number;
231
+ readonly mip: number;
232
+ readonly width: number;
233
+ readonly height: number;
234
+ readonly byteOffset: number;
235
+ readonly byteLength: number;
236
+ }
237
+
238
+ /** Full subresource layout of a texture snapshot blob. */
239
+ export interface TextureLayout {
240
+ readonly blockWidth: number;
241
+ readonly blockHeight: number;
242
+ readonly bytesPerBlock: number;
243
+ readonly layerCount: number;
244
+ readonly mipLevelCount: number;
245
+ readonly slices: readonly SubresourceSlice[];
246
+ /** Total tight byte length across every subresource (the blob size). */
247
+ readonly totalBytes: number;
248
+ }
249
+
250
+ export interface TextureExtent {
251
+ readonly width: number;
252
+ readonly height: number;
253
+ readonly layerCount: number;
254
+ }
255
+
256
+ /** Project a serialized GPU texture extent onto the dimensions used by snapshots. */
257
+ export function projectTextureExtent(size: unknown): TextureExtent {
258
+ if (typeof size === 'number') return { width: size, height: 1, layerCount: 1 };
259
+ if (Array.isArray(size)) {
260
+ const width = typeof size[0] === 'number' ? size[0] : 1;
261
+ const height = typeof size[1] === 'number' ? size[1] : width;
262
+ const layerCount = typeof size[2] === 'number' ? size[2] : 1;
263
+ return { width, height, layerCount };
264
+ }
265
+ if (size !== null && typeof size === 'object') {
266
+ const record = size as {
267
+ readonly width?: unknown;
268
+ readonly height?: unknown;
269
+ readonly depthOrArrayLayers?: unknown;
270
+ };
271
+ const width = typeof record.width === 'number' ? record.width : 1;
272
+ const height = typeof record.height === 'number' ? record.height : width;
273
+ const layerCount =
274
+ typeof record.depthOrArrayLayers === 'number' ? record.depthOrArrayLayers : 1;
275
+ return { width, height, layerCount };
276
+ }
277
+ return { width: 1, height: 1, layerCount: 1 };
278
+ }
279
+
280
+ /**
281
+ * Compute the canonical tight byte layout for a texture snapshot: every array
282
+ * layer, every mip level, packed in layer-major then mip-minor order with no row
283
+ * padding (tight bytesPerRow = blockCountX * bytesPerBlock). Mip dimensions halve
284
+ * per level (floor, min 1), matching the WebGPU mip size rule.
285
+ *
286
+ * Returns `undefined` when the format has no known block footprint. The
287
+ * recorder reads each slice into this exact offset; the replayer reads each
288
+ * slice back out and writeTexture's it to (layer, mip).
289
+ */
290
+ export function computeTextureLayout(
291
+ format: GPUTextureFormat | undefined,
292
+ width: number,
293
+ height: number,
294
+ layerCount: number,
295
+ mipLevelCount: number,
296
+ ): TextureLayout | undefined {
297
+ const block = textureBlockLayout(format);
298
+ if (block === undefined) return undefined;
299
+
300
+ const layers = Math.max(1, layerCount);
301
+ const mips = Math.max(1, mipLevelCount);
302
+ const slices: SubresourceSlice[] = [];
303
+ let offset = 0;
304
+ for (let layer = 0; layer < layers; layer++) {
305
+ for (let mip = 0; mip < mips; mip++) {
306
+ const mw = Math.max(1, width >> mip);
307
+ const mh = Math.max(1, height >> mip);
308
+ const blockCountX = Math.ceil(mw / block.blockWidth);
309
+ const blockCountY = Math.ceil(mh / block.blockHeight);
310
+ const byteLength = blockCountX * blockCountY * block.bytesPerBlock;
311
+ slices.push({ layer, mip, width: mw, height: mh, byteOffset: offset, byteLength });
312
+ offset += byteLength;
313
+ }
314
+ }
315
+ return {
316
+ blockWidth: block.blockWidth,
317
+ blockHeight: block.blockHeight,
318
+ bytesPerBlock: block.bytesPerBlock,
319
+ layerCount: layers,
320
+ mipLevelCount: mips,
321
+ slices,
322
+ totalBytes: offset,
323
+ };
324
+ }