@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,99 @@
1
+ /// <reference types="@webgpu/types" />
2
+
3
+ export interface TextureReadbackDescriptor {
4
+ readonly format: string;
5
+ readonly dimension: string;
6
+ readonly aspect?: GPUTextureAspect | undefined;
7
+ }
8
+
9
+ export type TextureReadbackPlan =
10
+ | {
11
+ readonly supported: true;
12
+ readonly format: string;
13
+ readonly bytesPerTexel: number;
14
+ readonly blockWidth: 1;
15
+ readonly blockHeight: 1;
16
+ }
17
+ | {
18
+ readonly supported: false;
19
+ readonly format: string;
20
+ readonly reason: string;
21
+ };
22
+
23
+ const BYTES_PER_TEXEL: Readonly<Record<string, number>> = {
24
+ r8unorm: 1,
25
+ rg8unorm: 2,
26
+ rgba8unorm: 4,
27
+ 'rgba8unorm-srgb': 4,
28
+ bgra8unorm: 4,
29
+ 'bgra8unorm-srgb': 4,
30
+ r16float: 2,
31
+ rg16float: 4,
32
+ rgba16float: 8,
33
+ r32float: 4,
34
+ rg32float: 8,
35
+ rgba32float: 16,
36
+ rgb10a2unorm: 4,
37
+ rg11b10ufloat: 4,
38
+ };
39
+
40
+ const DEPTH_FORMATS = new Set([
41
+ 'depth16unorm',
42
+ 'depth24plus',
43
+ 'depth24plus-stencil8',
44
+ 'depth32float',
45
+ 'depth32float-stencil8',
46
+ ]);
47
+
48
+ export function isDepthTextureFormat(format: string): boolean {
49
+ return DEPTH_FORMATS.has(format);
50
+ }
51
+
52
+ export function isDepthStencilTextureFormat(format: string): boolean {
53
+ return format.endsWith('-stencil8');
54
+ }
55
+
56
+ export function getTextureReadbackPlan(descriptor: TextureReadbackDescriptor): TextureReadbackPlan {
57
+ if (descriptor.dimension === '3d') {
58
+ return {
59
+ supported: false,
60
+ format: descriptor.format,
61
+ reason: '3D texture volume readback is outside the v7 core matrix',
62
+ };
63
+ }
64
+ if (/^(bc|etc2|eac|astc)-/.test(descriptor.format)) {
65
+ return {
66
+ supported: false,
67
+ format: descriptor.format,
68
+ reason: 'compressed texture readback has no core decoder',
69
+ };
70
+ }
71
+ if (DEPTH_FORMATS.has(descriptor.format)) {
72
+ return {
73
+ supported: true,
74
+ format: descriptor.format,
75
+ bytesPerTexel: 4,
76
+ blockWidth: 1,
77
+ blockHeight: 1,
78
+ };
79
+ }
80
+ const bytesPerTexel = BYTES_PER_TEXEL[descriptor.format];
81
+ if (bytesPerTexel === undefined) {
82
+ return {
83
+ supported: false,
84
+ format: descriptor.format,
85
+ reason: 'texture format is not in the v7 readback matrix',
86
+ };
87
+ }
88
+ return {
89
+ supported: true,
90
+ format: descriptor.format,
91
+ bytesPerTexel,
92
+ blockWidth: 1,
93
+ blockHeight: 1,
94
+ };
95
+ }
96
+
97
+ export function textureBytesPerTexel(format: string): number | undefined {
98
+ return BYTES_PER_TEXEL[format] ?? (DEPTH_FORMATS.has(format) ? 4 : undefined);
99
+ }
@@ -0,0 +1,289 @@
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 a supported color or depth/stencil plane into
150
+ * RGBA8 the canvas can paint via putImageData. Returns null when the format has
151
+ * no {@link formatInfo} entry or the requested aspect is invalid.
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
+ * @param aspect - Explicit plane selected by readback for depth/stencil formats.
158
+ */
159
+ export function decodeToRgba8(
160
+ bytes: Uint8Array,
161
+ format: string,
162
+ width: number,
163
+ height: number,
164
+ aspect: 'all' | 'depth-only' | 'stencil-only' = 'all',
165
+ ): Uint8ClampedArray<ArrayBuffer> | null {
166
+ if (format.startsWith('depth')) {
167
+ return decodeDepthToRgba8(bytes, format, width, height, aspect);
168
+ }
169
+ const info = formatInfo(format);
170
+ const texBytes = bytesPerTexel(format as never);
171
+ if (!info || texBytes === undefined) return null;
172
+
173
+ // Per-channel byte width for plain formats (packed formats ignore this).
174
+ const channelBytes = info.packed ? texBytes : texBytes / info.channels;
175
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
176
+ // Explicit ArrayBuffer backing (not ArrayBufferLike) so ImageData accepts it.
177
+ const out = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));
178
+
179
+ for (let i = 0; i < width * height; i++) {
180
+ const [c0, c1, c2, c3] = readTexel(view, i * texBytes, info, channelBytes);
181
+ const di = i * 4;
182
+ if (info.channels === 1) {
183
+ const g = toByte(c0, info.channelType);
184
+ out[di] = g;
185
+ out[di + 1] = g;
186
+ out[di + 2] = g;
187
+ out[di + 3] = 255;
188
+ } else if (info.channels === 2) {
189
+ out[di] = toByte(c0, info.channelType);
190
+ out[di + 1] = toByte(c1, info.channelType);
191
+ out[di + 2] = 0;
192
+ out[di + 3] = 255;
193
+ } else {
194
+ // 3 or 4 channels. BGRA stores blue-first -> swizzle B<->R.
195
+ const r = info.bgra ? c2 : c0;
196
+ const b = info.bgra ? c0 : c2;
197
+ out[di] = toByte(r, info.channelType);
198
+ out[di + 1] = toByte(c1, info.channelType);
199
+ out[di + 2] = toByte(b, info.channelType);
200
+ out[di + 3] = info.channels === 4 ? toByte(c3, info.channelType) : 255;
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+
206
+ function decodeDepthToRgba8(
207
+ bytes: Uint8Array,
208
+ format: string,
209
+ width: number,
210
+ height: number,
211
+ aspect: 'all' | 'depth-only' | 'stencil-only',
212
+ ): Uint8ClampedArray<ArrayBuffer> | null {
213
+ const combined = format.endsWith('-stencil8');
214
+ if (combined && aspect === 'all') return null;
215
+ if (!combined && aspect === 'stencil-only') return null;
216
+ const stencil = aspect === 'stencil-only';
217
+ const bytesPerValue = stencil ? 1 : 4;
218
+ const texelCount = width * height;
219
+ if (bytes.byteLength < texelCount * bytesPerValue) return null;
220
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
221
+ const out = new Uint8ClampedArray(new ArrayBuffer(texelCount * 4));
222
+ for (let index = 0; index < texelCount; index++) {
223
+ const value = stencil
224
+ ? view.getUint8(index)
225
+ : Math.round(clamp01(view.getFloat32(index * 4, true)) * 255);
226
+ const target = index * 4;
227
+ out[target] = value;
228
+ out[target + 1] = value;
229
+ out[target + 2] = value;
230
+ out[target + 3] = 255;
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * Decode a single texel from tight raw GPU bytes into raw float RGBA values
237
+ * without the display clamp (D-4: raw byte bypass for HDR fidelity).
238
+ *
239
+ * Unlike `decodeToRgba8` which clamps float/ufloat to [0,1] for display, this
240
+ * returns the decoded per-channel values as-is: float formats keep true values
241
+ * (2.5 stays 2.5, negative stays negative), unorm/snorm stay in their native
242
+ * ranges, uint/sint stay as integers cast to number.
243
+ *
244
+ * Channels: 1ch grayscale (R=G=B, A=1), 2ch (RG, B=0, A=1), 3/4ch (RGBA or
245
+ * BGRA→swizzled). Returns null when the format has no {@link formatInfo} entry.
246
+ *
247
+ * @param bytes - Tight readback bytes, length = width*height*bytesPerTexel.
248
+ * @param format - The texture's real format string.
249
+ * @param width - Texture width in pixels.
250
+ * @param height - Texture height in pixels.
251
+ * @param texelX - 0-based column of the target texel.
252
+ * @param texelY - 0-based row of the target texel.
253
+ */
254
+ export function decodeTexelRaw(
255
+ bytes: Uint8Array,
256
+ format: string,
257
+ width: number,
258
+ height: number,
259
+ texelX: number,
260
+ texelY: number,
261
+ ): [number, number, number, number] | null {
262
+ if (texelX < 0 || texelX >= width || texelY < 0 || texelY >= height) return null;
263
+
264
+ const info = formatInfo(format);
265
+ const texBytes = bytesPerTexel(format as never);
266
+ if (!info || texBytes === undefined) return null;
267
+
268
+ const texelIdx = texelY * width + texelX;
269
+ const byteOffset = texelIdx * texBytes;
270
+
271
+ // Guard: the buffer must be large enough for this texel.
272
+ if (byteOffset + texBytes > bytes.byteLength) return null;
273
+
274
+ const channelBytes = info.packed ? texBytes : texBytes / info.channels;
275
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
276
+ const channels = readTexel(view, byteOffset, info, channelBytes);
277
+
278
+ if (info.channels === 1) {
279
+ return [channels[0], channels[0], channels[0], 1];
280
+ }
281
+ if (info.channels === 2) {
282
+ return [channels[0], channels[1], 0, 1];
283
+ }
284
+ // 3 or 4 channels. BGRA swizzle B<->R.
285
+ const r = info.bgra ? channels[2] : channels[0];
286
+ const b = info.bgra ? channels[0] : channels[2];
287
+ const a = info.channels === 4 ? channels[3] : 1;
288
+ return [r, channels[1], b, a];
289
+ }