@forgeax/engine-rhi-debug 0.1.2

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 (231) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +139 -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__/recorder-bootstrap.unit.test.d.ts +2 -0
  43. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
  45. package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
  47. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
  49. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
  51. package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
  53. package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
  54. package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
  55. package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
  57. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
  58. package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
  59. package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
  61. package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
  62. package/dist/__tests__/replay-session.test-d.d.ts +2 -0
  63. package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
  64. package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
  65. package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
  67. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
  69. package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
  71. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
  72. package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
  73. package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
  75. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
  77. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
  79. package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
  81. package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/types.test.d.ts +2 -0
  83. package/dist/__tests__/types.test.d.ts.map +1 -0
  84. package/dist/browser.d.ts +45 -0
  85. package/dist/browser.d.ts.map +1 -0
  86. package/dist/browser.mjs +83 -0
  87. package/dist/browser.mjs.map +1 -0
  88. package/dist/errors.d.ts +85 -0
  89. package/dist/errors.d.ts.map +1 -0
  90. package/dist/frame-model.d.ts +126 -0
  91. package/dist/frame-model.d.ts.map +1 -0
  92. package/dist/index.d.ts +16 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.mjs +5810 -0
  95. package/dist/index.mjs.map +1 -0
  96. package/dist/protocol/codec.d.ts +6 -0
  97. package/dist/protocol/codec.d.ts.map +1 -0
  98. package/dist/protocol/event-semantics.d.ts +15 -0
  99. package/dist/protocol/event-semantics.d.ts.map +1 -0
  100. package/dist/protocol/tape-index.d.ts +30 -0
  101. package/dist/protocol/tape-index.d.ts.map +1 -0
  102. package/dist/protocol/types.d.ts +44 -0
  103. package/dist/protocol/types.d.ts.map +1 -0
  104. package/dist/protocol/validation.d.ts +7 -0
  105. package/dist/protocol/validation.d.ts.map +1 -0
  106. package/dist/readback.d.ts +156 -0
  107. package/dist/readback.d.ts.map +1 -0
  108. package/dist/recorder/assemble.d.ts +11 -0
  109. package/dist/recorder/assemble.d.ts.map +1 -0
  110. package/dist/recorder/closure.d.ts +39 -0
  111. package/dist/recorder/closure.d.ts.map +1 -0
  112. package/dist/recorder/core.d.ts +296 -0
  113. package/dist/recorder/core.d.ts.map +1 -0
  114. package/dist/recorder/device.d.ts +4 -0
  115. package/dist/recorder/device.d.ts.map +1 -0
  116. package/dist/recorder/encoder.d.ts +5 -0
  117. package/dist/recorder/encoder.d.ts.map +1 -0
  118. package/dist/recorder/lifecycle.d.ts +20 -0
  119. package/dist/recorder/lifecycle.d.ts.map +1 -0
  120. package/dist/recorder/pass.d.ts +6 -0
  121. package/dist/recorder/pass.d.ts.map +1 -0
  122. package/dist/recorder/proxy.d.ts +19 -0
  123. package/dist/recorder/proxy.d.ts.map +1 -0
  124. package/dist/recorder/queue.d.ts +4 -0
  125. package/dist/recorder/queue.d.ts.map +1 -0
  126. package/dist/recorder/resource-registry.d.ts +15 -0
  127. package/dist/recorder/resource-registry.d.ts.map +1 -0
  128. package/dist/recorder/session.d.ts +25 -0
  129. package/dist/recorder/session.d.ts.map +1 -0
  130. package/dist/recorder/shader.d.ts +3 -0
  131. package/dist/recorder/shader.d.ts.map +1 -0
  132. package/dist/recorder/snapshot.d.ts +10 -0
  133. package/dist/recorder/snapshot.d.ts.map +1 -0
  134. package/dist/recorder/wrap.d.ts +4 -0
  135. package/dist/recorder/wrap.d.ts.map +1 -0
  136. package/dist/recorder.d.ts +7 -0
  137. package/dist/recorder.d.ts.map +1 -0
  138. package/dist/replay/execute-support.d.ts +33 -0
  139. package/dist/replay/execute-support.d.ts.map +1 -0
  140. package/dist/replay/execute.d.ts +18 -0
  141. package/dist/replay/execute.d.ts.map +1 -0
  142. package/dist/replay/readback.d.ts +25 -0
  143. package/dist/replay/readback.d.ts.map +1 -0
  144. package/dist/replay/resources.d.ts +57 -0
  145. package/dist/replay/resources.d.ts.map +1 -0
  146. package/dist/replay/session.d.ts +32 -0
  147. package/dist/replay/session.d.ts.map +1 -0
  148. package/dist/replay/texture-format.d.ts +21 -0
  149. package/dist/replay/texture-format.d.ts.map +1 -0
  150. package/dist/texel-decode.d.ts +34 -0
  151. package/dist/texel-decode.d.ts.map +1 -0
  152. package/dist/texel-layout.d.ts +76 -0
  153. package/dist/texel-layout.d.ts.map +1 -0
  154. package/dist/types.d.ts +704 -0
  155. package/dist/types.d.ts.map +1 -0
  156. package/package.json +72 -0
  157. package/src/__tests__/bootstrap-integration.test.ts +268 -0
  158. package/src/__tests__/consumer-inventory.unit.test.ts +106 -0
  159. package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
  160. package/src/__tests__/coverage-invariant.test-d.ts +115 -0
  161. package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
  162. package/src/__tests__/e2e.browser.test.ts +234 -0
  163. package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
  164. package/src/__tests__/errors-v7.test-d.ts +19 -0
  165. package/src/__tests__/errors-v7.unit.test.ts +15 -0
  166. package/src/__tests__/errors.unit.test.ts +83 -0
  167. package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
  168. package/src/__tests__/format-info.unit.test.ts +85 -0
  169. package/src/__tests__/frame-model-parity.test-d.ts +8 -0
  170. package/src/__tests__/frame-model-parity.unit.test.ts +38 -0
  171. package/src/__tests__/guard-gates.test.ts +144 -0
  172. package/src/__tests__/public-surface.integration.test.ts +30 -0
  173. package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
  174. package/src/__tests__/readback-format-matrix.dawn.test.ts +155 -0
  175. package/src/__tests__/readback-format-matrix.unit.test.ts +96 -0
  176. package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
  177. package/src/__tests__/recorder-budget.unit.test.ts +42 -0
  178. package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
  179. package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
  180. package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
  181. package/src/__tests__/recorder-session.integration.test.ts +32 -0
  182. package/src/__tests__/recorder-session.unit.test.ts +63 -0
  183. package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
  184. package/src/__tests__/replay-fail-fast.unit.test.ts +143 -0
  185. package/src/__tests__/replay-session.dawn.test.ts +217 -0
  186. package/src/__tests__/replay-session.test-d.ts +21 -0
  187. package/src/__tests__/replay-session.unit.test.ts +155 -0
  188. package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
  189. package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
  190. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +176 -0
  191. package/src/__tests__/tape-index.unit.test.ts +53 -0
  192. package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
  193. package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
  194. package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
  195. package/src/__tests__/tree-shake.unit.test.ts +127 -0
  196. package/src/__tests__/types.test.ts +257 -0
  197. package/src/browser.ts +146 -0
  198. package/src/errors.ts +173 -0
  199. package/src/frame-model.ts +455 -0
  200. package/src/index.ts +85 -0
  201. package/src/pako.d.ts +8 -0
  202. package/src/protocol/codec.ts +220 -0
  203. package/src/protocol/event-semantics.ts +180 -0
  204. package/src/protocol/tape-index.ts +120 -0
  205. package/src/protocol/types.ts +60 -0
  206. package/src/protocol/validation.ts +173 -0
  207. package/src/readback.ts +804 -0
  208. package/src/recorder/assemble.ts +192 -0
  209. package/src/recorder/closure.ts +408 -0
  210. package/src/recorder/core.ts +587 -0
  211. package/src/recorder/device.ts +456 -0
  212. package/src/recorder/encoder.ts +234 -0
  213. package/src/recorder/lifecycle.ts +847 -0
  214. package/src/recorder/pass.ts +241 -0
  215. package/src/recorder/proxy.ts +59 -0
  216. package/src/recorder/queue.ts +109 -0
  217. package/src/recorder/resource-registry.ts +53 -0
  218. package/src/recorder/session.ts +152 -0
  219. package/src/recorder/shader.ts +47 -0
  220. package/src/recorder/snapshot.ts +38 -0
  221. package/src/recorder/wrap.ts +157 -0
  222. package/src/recorder.ts +15 -0
  223. package/src/replay/execute-support.ts +243 -0
  224. package/src/replay/execute.ts +870 -0
  225. package/src/replay/readback.ts +583 -0
  226. package/src/replay/resources.ts +154 -0
  227. package/src/replay/session.ts +464 -0
  228. package/src/replay/texture-format.ts +99 -0
  229. package/src/texel-decode.ts +255 -0
  230. package/src/texel-layout.ts +324 -0
  231. package/src/types.ts +864 -0
@@ -0,0 +1,255 @@
1
+ // texel-decode.ts -- CPU decode of each uncompressed color format to RGBA8 for preview.
2
+ //
3
+ // The recorder promotes COPY_SRC onto every createTexture, so the replayer's
4
+ // copyTextureToBuffer reads back each uncompressed color texture's raw bytes
5
+ // faithfully (no format change -- replay fidelity intact). This module turns
6
+ // those raw bytes into displayable RGBA8 entirely on the host:
7
+ //
8
+ // raw bytes --formatInfo--> per-channel values --[display map]--> RGBA8
9
+ //
10
+ // Display map (RenderDoc's default initial state, confirmed with the user):
11
+ // float/ufloat -> clamp(v, 0, 1) * 255 (HDR highlights >1 saturate to white)
12
+ // unorm -> v * 255 (already 0..1)
13
+ // snorm -> clamp(v, 0, 1) * 255 (negatives clip to black)
14
+ // uint/sint -> clamp(v, 0, 255) (small integer ids stay visible)
15
+ //
16
+ // Channels -> RGBA: 1ch grayscale (R=G=B), 2ch (R,G,B=0), 3ch (RGB,A=255), 4ch.
17
+ //
18
+ // Related: plan "all-format color preview"; reuses formatInfo (rhi-debug texel-layout).
19
+
20
+ import { bytesPerTexel, type FormatInfo, formatInfo } from './texel-layout';
21
+
22
+ /** Decode a single IEEE half-float (16-bit) bit pattern to a JS number. */
23
+ export function halfToFloat(h: number): number {
24
+ const sign = (h & 0x8000) >> 15;
25
+ const exp = (h & 0x7c00) >> 10;
26
+ const frac = h & 0x03ff;
27
+ const s = sign === 0 ? 1 : -1;
28
+ if (exp === 0) {
29
+ // Subnormal (or zero): 2^-14 * (frac / 1024).
30
+ return s * 2 ** -14 * (frac / 1024);
31
+ }
32
+ if (exp === 0x1f) {
33
+ // Inf / NaN.
34
+ return frac === 0 ? s * Number.POSITIVE_INFINITY : Number.NaN;
35
+ }
36
+ // Normal: 2^(exp-15) * (1 + frac/1024).
37
+ return s * 2 ** (exp - 15) * (1 + frac / 1024);
38
+ }
39
+
40
+ /**
41
+ * Decode an unsigned-int bit pattern of an 11- or 10-bit unsigned float
42
+ * (rg11b10ufloat layout: no sign bit). 11-bit = 5 exp + 6 mantissa; 10-bit =
43
+ * 5 exp + 5 mantissa. Bias 15, matching the f16 exponent.
44
+ */
45
+ function smallUFloatToFloat(bits: number, mantissaBits: number): number {
46
+ const expBits = 5;
47
+ const exp = (bits >> mantissaBits) & ((1 << expBits) - 1);
48
+ const mantMax = 1 << mantissaBits;
49
+ const frac = bits & (mantMax - 1);
50
+ if (exp === 0) return 2 ** -14 * (frac / mantMax);
51
+ if (exp === 0x1f) return frac === 0 ? Number.POSITIVE_INFINITY : Number.NaN;
52
+ return 2 ** (exp - 15) * (1 + frac / mantMax);
53
+ }
54
+
55
+ const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
56
+
57
+ /** Map one decoded channel value to a 0..255 display byte by channel type. */
58
+ function toByte(v: number, channelType: FormatInfo['channelType']): number {
59
+ // uint/sint: small integer ids stay visible (clamp 0..255, no normalize).
60
+ if (channelType === 'uint' || channelType === 'sint') {
61
+ return Math.round(v < 0 ? 0 : v > 255 ? 255 : v);
62
+ }
63
+ // float/ufloat clamp HDR to [0,1]; unorm already 0..1; snorm clips negatives.
64
+ return Math.round(clamp01(v) * 255);
65
+ }
66
+
67
+ /**
68
+ * Read the `channels` raw channel values of one texel at byte offset `off`.
69
+ * Plain formats read each channel at a uniform per-channel byte width; packed
70
+ * formats unpack the single 32-bit word by bit-field. Values are returned in the
71
+ * format's native channel order (BGRA swizzle handled by the caller).
72
+ */
73
+ function readTexel(
74
+ view: DataView,
75
+ off: number,
76
+ info: FormatInfo,
77
+ channelBytes: number,
78
+ ): [number, number, number, number] {
79
+ const out: [number, number, number, number] = [0, 0, 0, 1];
80
+
81
+ if (info.packed === 'rgb10a2unorm') {
82
+ const word = view.getUint32(off, true);
83
+ out[0] = (word & 0x3ff) / 1023;
84
+ out[1] = ((word >> 10) & 0x3ff) / 1023;
85
+ out[2] = ((word >> 20) & 0x3ff) / 1023;
86
+ out[3] = ((word >> 30) & 0x3) / 3;
87
+ return out;
88
+ }
89
+ if (info.packed === 'rg11b10ufloat') {
90
+ const word = view.getUint32(off, true);
91
+ out[0] = smallUFloatToFloat(word & 0x7ff, 6);
92
+ out[1] = smallUFloatToFloat((word >> 11) & 0x7ff, 6);
93
+ out[2] = smallUFloatToFloat((word >> 22) & 0x3ff, 5);
94
+ out[3] = 1;
95
+ return out;
96
+ }
97
+
98
+ for (let c = 0; c < info.channels; c++) {
99
+ const co = off + c * channelBytes;
100
+ let v: number;
101
+ switch (info.channelType) {
102
+ case 'float':
103
+ v = channelBytes === 2 ? halfToFloat(view.getUint16(co, true)) : view.getFloat32(co, true);
104
+ break;
105
+ case 'unorm':
106
+ v =
107
+ channelBytes === 1
108
+ ? view.getUint8(co) / 255
109
+ : channelBytes === 2
110
+ ? view.getUint16(co, true) / 65535
111
+ : view.getUint32(co, true) / 4294967295;
112
+ break;
113
+ case 'snorm': {
114
+ const raw =
115
+ channelBytes === 1
116
+ ? view.getInt8(co)
117
+ : channelBytes === 2
118
+ ? view.getInt16(co, true)
119
+ : view.getInt32(co, true);
120
+ const denom = channelBytes === 1 ? 127 : channelBytes === 2 ? 32767 : 2147483647;
121
+ v = Math.max(-1, raw / denom);
122
+ break;
123
+ }
124
+ case 'uint':
125
+ v =
126
+ channelBytes === 1
127
+ ? view.getUint8(co)
128
+ : channelBytes === 2
129
+ ? view.getUint16(co, true)
130
+ : view.getUint32(co, true);
131
+ break;
132
+ case 'sint':
133
+ v =
134
+ channelBytes === 1
135
+ ? view.getInt8(co)
136
+ : channelBytes === 2
137
+ ? view.getInt16(co, true)
138
+ : view.getInt32(co, true);
139
+ break;
140
+ default:
141
+ v = 0;
142
+ }
143
+ out[c] = v;
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /**
149
+ * Decode tight raw GPU bytes of an uncompressed color texture into RGBA8 the
150
+ * canvas can paint via putImageData. Returns null when the format has no
151
+ * {@link formatInfo} entry (compressed / depth / unknown -> caller falls back).
152
+ *
153
+ * @param bytes - Tight readback bytes (no row padding), length = w*h*bytesPerTexel.
154
+ * @param format - The texture's real format string.
155
+ * @param width - Texture width in pixels.
156
+ * @param height - Texture height in pixels.
157
+ */
158
+ export function decodeToRgba8(
159
+ bytes: Uint8Array,
160
+ format: string,
161
+ width: number,
162
+ height: number,
163
+ ): Uint8ClampedArray<ArrayBuffer> | null {
164
+ const info = formatInfo(format);
165
+ const texBytes = bytesPerTexel(format as never);
166
+ if (!info || texBytes === undefined) return null;
167
+
168
+ // Per-channel byte width for plain formats (packed formats ignore this).
169
+ const channelBytes = info.packed ? texBytes : texBytes / info.channels;
170
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
171
+ // Explicit ArrayBuffer backing (not ArrayBufferLike) so ImageData accepts it.
172
+ const out = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));
173
+
174
+ for (let i = 0; i < width * height; i++) {
175
+ const [c0, c1, c2, c3] = readTexel(view, i * texBytes, info, channelBytes);
176
+ const di = i * 4;
177
+ if (info.channels === 1) {
178
+ const g = toByte(c0, info.channelType);
179
+ out[di] = g;
180
+ out[di + 1] = g;
181
+ out[di + 2] = g;
182
+ out[di + 3] = 255;
183
+ } else if (info.channels === 2) {
184
+ out[di] = toByte(c0, info.channelType);
185
+ out[di + 1] = toByte(c1, info.channelType);
186
+ out[di + 2] = 0;
187
+ out[di + 3] = 255;
188
+ } else {
189
+ // 3 or 4 channels. BGRA stores blue-first -> swizzle B<->R.
190
+ const r = info.bgra ? c2 : c0;
191
+ const b = info.bgra ? c0 : c2;
192
+ out[di] = toByte(r, info.channelType);
193
+ out[di + 1] = toByte(c1, info.channelType);
194
+ out[di + 2] = toByte(b, info.channelType);
195
+ out[di + 3] = info.channels === 4 ? toByte(c3, info.channelType) : 255;
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+
201
+ /**
202
+ * Decode a single texel from tight raw GPU bytes into raw float RGBA values
203
+ * without the display clamp (D-4: raw byte bypass for HDR fidelity).
204
+ *
205
+ * Unlike `decodeToRgba8` which clamps float/ufloat to [0,1] for display, this
206
+ * returns the decoded per-channel values as-is: float formats keep true values
207
+ * (2.5 stays 2.5, negative stays negative), unorm/snorm stay in their native
208
+ * ranges, uint/sint stay as integers cast to number.
209
+ *
210
+ * Channels: 1ch grayscale (R=G=B, A=1), 2ch (RG, B=0, A=1), 3/4ch (RGBA or
211
+ * BGRA→swizzled). Returns null when the format has no {@link formatInfo} entry.
212
+ *
213
+ * @param bytes - Tight readback bytes, length = width*height*bytesPerTexel.
214
+ * @param format - The texture's real format string.
215
+ * @param width - Texture width in pixels.
216
+ * @param height - Texture height in pixels.
217
+ * @param texelX - 0-based column of the target texel.
218
+ * @param texelY - 0-based row of the target texel.
219
+ */
220
+ export function decodeTexelRaw(
221
+ bytes: Uint8Array,
222
+ format: string,
223
+ width: number,
224
+ height: number,
225
+ texelX: number,
226
+ texelY: number,
227
+ ): [number, number, number, number] | null {
228
+ if (texelX < 0 || texelX >= width || texelY < 0 || texelY >= height) return null;
229
+
230
+ const info = formatInfo(format);
231
+ const texBytes = bytesPerTexel(format as never);
232
+ if (!info || texBytes === undefined) return null;
233
+
234
+ const texelIdx = texelY * width + texelX;
235
+ const byteOffset = texelIdx * texBytes;
236
+
237
+ // Guard: the buffer must be large enough for this texel.
238
+ if (byteOffset + texBytes > bytes.byteLength) return null;
239
+
240
+ const channelBytes = info.packed ? texBytes : texBytes / info.channels;
241
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
242
+ const channels = readTexel(view, byteOffset, info, channelBytes);
243
+
244
+ if (info.channels === 1) {
245
+ return [channels[0], channels[0], channels[0], 1];
246
+ }
247
+ if (info.channels === 2) {
248
+ return [channels[0], channels[1], 0, 1];
249
+ }
250
+ // 3 or 4 channels. BGRA swizzle B<->R.
251
+ const r = info.bgra ? channels[2] : channels[0];
252
+ const b = info.bgra ? channels[0] : channels[2];
253
+ const a = info.channels === 4 ? channels[3] : 1;
254
+ return [r, channels[1], b, a];
255
+ }
@@ -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
+ }