@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,456 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/device -- device proxy owner.
2
+
3
+ /// <reference types="@webgpu/types" />
4
+
5
+ import type {
6
+ BindGroupDescriptor,
7
+ BindGroupLayoutDescriptor,
8
+ Buffer,
9
+ BufferDescriptor,
10
+ CommandEncoderDescriptor,
11
+ ComputePipelineDescriptor,
12
+ PipelineLayoutDescriptor,
13
+ QuerySet,
14
+ QuerySetDescriptor,
15
+ RenderPipelineDescriptor,
16
+ RhiCommandEncoder,
17
+ RhiDevice,
18
+ SamplerDescriptor,
19
+ Texture,
20
+ TextureDescriptor,
21
+ TextureViewDescriptor,
22
+ } from '@forgeax/engine-rhi';
23
+ import { RhiError } from '@forgeax/engine-rhi';
24
+ import { err as makeErr, ok as makeOk, type Result } from '@forgeax/engine-types';
25
+ import type { HandleId, RhiBindResourceKind, RhiCallEvent } from '../types';
26
+ import type { RecorderInternal } from './core';
27
+ import {
28
+ allocHandleId,
29
+ ensureTextureCreateEvent,
30
+ getHandleId,
31
+ hasBootstrapDependency,
32
+ isDepthOrStencilFormat,
33
+ promoteBufferUsage,
34
+ pushEvent,
35
+ registerHandle,
36
+ retainsCaptureBootstrap,
37
+ shouldRecord,
38
+ TEXTURE_USAGE_BINDING,
39
+ TEXTURE_USAGE_COPY_DST,
40
+ TEXTURE_USAGE_COPY_SRC,
41
+ } from './core';
42
+ import { createCommandEncoderProxy } from './encoder';
43
+ import { createQueueProxy } from './queue';
44
+
45
+ export function createDeviceProxy(s: RecorderInternal, realDevice: RhiDevice): RhiDevice {
46
+ const proxiedQueue = createQueueProxy(s, realDevice.queue);
47
+
48
+ // Expose the real RhiDevice for standalone createShaderModule calls.
49
+ // engine-rhi-webgpu's createShaderModule uses RAW_DEVICE_MAP (WeakMap)
50
+ // to reverse-lookup the GPUDevice. The proxy is a different JS object
51
+ // from the RhiDevice that makeRhiDevice registered, so WeakMap.get(proxy)
52
+ // returns undefined and createShaderModule returns shader-compile-failed.
53
+ // The _realDevice property lets callers pass the real RhiDevice directly.
54
+ type RhiDeviceWithReal = RhiDevice & { _realDevice: RhiDevice };
55
+
56
+ const d: RhiDeviceWithReal = {
57
+ _realDevice: realDevice,
58
+
59
+ get caps() {
60
+ return realDevice.caps;
61
+ },
62
+ get features() {
63
+ return realDevice.features;
64
+ },
65
+ get limits() {
66
+ return realDevice.limits;
67
+ },
68
+ get queue() {
69
+ return proxiedQueue;
70
+ },
71
+ get lost() {
72
+ return realDevice.lost;
73
+ },
74
+
75
+ createBuffer(desc: BufferDescriptor) {
76
+ // COPY_SRC promotion (D-5): every recorded resource must be readable
77
+ // back via copyBufferToBuffer so snapshotResource can capture its GPU
78
+ // bytes at frame-header time. Promote the live resource's usage too,
79
+ // not just the recorded event — a buffer created without COPY_SRC is
80
+ // an invalid copy source on the real device. Mappable buffers are
81
+ // skipped (MAP_READ|COPY_SRC is invalid) — they are staging buffers,
82
+ // never snapshot targets.
83
+ const promotedUsage = promoteBufferUsage(desc.usage ?? 0);
84
+ const res = realDevice.createBuffer({ ...desc, usage: promotedUsage });
85
+ if (!res.ok) return res;
86
+ const event: RhiCallEvent = {
87
+ kind: 'createBuffer',
88
+ handleId: '' as HandleId,
89
+ desc: {
90
+ size: desc.size ?? 0,
91
+ usage: promotedUsage,
92
+ mappedAtCreation: desc.mappedAtCreation,
93
+ },
94
+ };
95
+ const bufResource = res.value as object;
96
+ const bufHandleId = registerHandle(s, bufResource, 'buffer', event);
97
+ s.descriptorTable.set(bufHandleId, {
98
+ kind: 'buffer',
99
+ size: desc.size ?? 0,
100
+ usage: promotedUsage,
101
+ resource: bufResource,
102
+ });
103
+ pushEvent(s, event);
104
+ return res;
105
+ },
106
+
107
+ createTexture(desc: TextureDescriptor) {
108
+ // Snapshot promotion (D-5): a texture needs COPY_SRC for GPU readback
109
+ // and COPY_DST for queue.writeTexture bootstrap seeding. Promote the
110
+ // live resource's usage as well as the recorded event's so replay can
111
+ // restore the captured color bytes on a fresh device.
112
+ const promotedUsage =
113
+ (desc.usage ?? 0) |
114
+ TEXTURE_USAGE_COPY_SRC |
115
+ TEXTURE_USAGE_COPY_DST |
116
+ (isDepthOrStencilFormat(desc.format) ? TEXTURE_USAGE_BINDING : 0);
117
+ const res = realDevice.createTexture({ ...desc, usage: promotedUsage });
118
+ if (!res.ok) return res;
119
+ const event: RhiCallEvent = {
120
+ kind: 'createTexture',
121
+ handleId: '' as HandleId,
122
+ desc: {
123
+ size: desc.size ?? { width: 1, height: 1 },
124
+ mipLevelCount: desc.mipLevelCount,
125
+ sampleCount: desc.sampleCount,
126
+ dimension: desc.dimension,
127
+ format: desc.format ?? ('bgra8unorm' as GPUTextureFormat),
128
+ usage: promotedUsage,
129
+ viewFormats: desc.viewFormats,
130
+ textureBindingViewDimension: desc.textureBindingViewDimension,
131
+ },
132
+ };
133
+ const texResource = res.value as object;
134
+ const texHandleId = registerHandle(s, texResource, 'texture', event);
135
+ s.descriptorTable.set(texHandleId, {
136
+ kind: 'texture',
137
+ size: desc.size ?? { width: 1, height: 1 },
138
+ format: desc.format ?? ('bgra8unorm' as GPUTextureFormat),
139
+ ...(desc.sampleCount !== undefined ? { sampleCount: desc.sampleCount } : {}),
140
+ ...(desc.mipLevelCount !== undefined ? { mipLevelCount: desc.mipLevelCount } : {}),
141
+ usage: promotedUsage,
142
+ resource: texResource,
143
+ });
144
+ pushEvent(s, event);
145
+ return res;
146
+ },
147
+
148
+ createTextureView(texture: Texture, desc: TextureViewDescriptor) {
149
+ const res = realDevice.createTextureView(texture, desc);
150
+ if (!res.ok) return res;
151
+ const srcId = getHandleId(s, texture as object, 'texture');
152
+ const textureError = ensureTextureCreateEvent(s, texture as object, srcId, desc.format);
153
+ if (textureError !== undefined) {
154
+ return makeErr(
155
+ new RhiError({
156
+ code: 'webgpu-runtime-error',
157
+ expected: 'texture-view creation to remain representable in the capture graph',
158
+ hint: textureError.hint,
159
+ detail: {
160
+ error: {
161
+ code: textureError.code,
162
+ message: textureError.hint,
163
+ name: 'RhiDebugError',
164
+ },
165
+ },
166
+ }),
167
+ );
168
+ }
169
+ const viewId = registerHandle(s, res.value as object, 'textureView');
170
+ s.textureViewHandleMap.set(res.value, viewId);
171
+
172
+ const event: RhiCallEvent = {
173
+ kind: 'createTextureView',
174
+ sourceHandleId: srcId,
175
+ resultHandleId: viewId,
176
+ desc: {
177
+ format: desc.format,
178
+ dimension: desc.dimension,
179
+ usage: desc.usage,
180
+ aspect: desc.aspect,
181
+ baseMipLevel: desc.baseMipLevel,
182
+ mipLevelCount: desc.mipLevelCount,
183
+ baseArrayLayer: desc.baseArrayLayer,
184
+ arrayLayerCount: desc.arrayLayerCount,
185
+ },
186
+ };
187
+ s.bootstrapCreates.set(viewId, event);
188
+ pushEvent(s, event);
189
+ return res;
190
+ },
191
+
192
+ createSampler(desc?: SamplerDescriptor | undefined) {
193
+ const res = realDevice.createSampler(desc);
194
+ if (!res.ok) return res;
195
+ const event: RhiCallEvent = {
196
+ kind: 'createSampler',
197
+ handleId: '' as HandleId,
198
+ desc: desc as Partial<GPUSamplerDescriptor> | undefined,
199
+ };
200
+ registerHandle(s, res.value as object, 'sampler', event);
201
+ pushEvent(s, event);
202
+ return res;
203
+ },
204
+
205
+ createBindGroupLayout(desc: BindGroupLayoutDescriptor) {
206
+ const res = realDevice.createBindGroupLayout(desc);
207
+ if (!res.ok) return res;
208
+ const event: RhiCallEvent = {
209
+ kind: 'createBindGroupLayout',
210
+ handleId: '' as HandleId,
211
+ desc: { label: desc.label, entries: desc.entries ?? [] },
212
+ };
213
+ registerHandle(s, res.value as object, 'bindGroupLayout', event);
214
+ pushEvent(s, event);
215
+ return res;
216
+ },
217
+
218
+ createBindGroup(desc: BindGroupDescriptor) {
219
+ const res = realDevice.createBindGroup(desc);
220
+ if (!res.ok) return res;
221
+ const layoutId = getHandleId(s, desc.layout as object, 'bindGroupLayout');
222
+ const entries = Array.from(desc.entries);
223
+ const resourceKinds: RhiBindResourceKind[] = entries.map((e) => e.resource.kind);
224
+ const resourceHandleIds: HandleId[] = entries.map((e) => {
225
+ const r = e.resource;
226
+ switch (r.kind) {
227
+ case 'sampler':
228
+ return getHandleId(s, r.value as object, 'sampler');
229
+ case 'buffer':
230
+ return getHandleId(s, r.value.buffer as object, 'buffer');
231
+ case 'textureView':
232
+ return getHandleId(s, r.value as object, 'textureView');
233
+ case 'externalTexture':
234
+ return 'externalTexture:unknown';
235
+ }
236
+ return 'externalTexture:unknown' as HandleId;
237
+ });
238
+ const event: RhiCallEvent = {
239
+ kind: 'createBindGroup',
240
+ handleId: '' as HandleId,
241
+ layoutHandleId: layoutId,
242
+ entries: entries.map((e, idx) => {
243
+ const entry: {
244
+ binding: number;
245
+ resourceKind: RhiBindResourceKind;
246
+ bufferOffset?: number;
247
+ bufferSize?: number;
248
+ } = {
249
+ binding: e.binding,
250
+ resourceKind: resourceKinds[idx] as RhiBindResourceKind,
251
+ };
252
+ // Capture the bound sub-range for buffer entries so a
253
+ // dynamic-offset slice (e.g. a 256 B view of a 256 KiB pool)
254
+ // replays as that slice, not the whole buffer.
255
+ if (e.resource.kind === 'buffer') {
256
+ const { offset, size } = e.resource.value;
257
+ if (offset !== undefined) entry.bufferOffset = offset;
258
+ if (size !== undefined) entry.bufferSize = size;
259
+ }
260
+ return entry;
261
+ }),
262
+ resourceHandleIds,
263
+ };
264
+ registerHandle(s, res.value as object, 'bindGroup', event);
265
+ pushEvent(s, event);
266
+ return res;
267
+ },
268
+
269
+ createPipelineLayout(desc: PipelineLayoutDescriptor) {
270
+ const res = realDevice.createPipelineLayout(desc);
271
+ if (!res.ok) return res;
272
+ const bglIds = Array.from(desc.bindGroupLayouts).map((bgl) =>
273
+ getHandleId(s, bgl as object, 'bindGroupLayout'),
274
+ );
275
+ const event: RhiCallEvent = {
276
+ kind: 'createPipelineLayout',
277
+ handleId: '' as HandleId,
278
+ bglHandleIds: bglIds,
279
+ };
280
+ registerHandle(s, res.value as object, 'pipelineLayout', event);
281
+ pushEvent(s, event);
282
+ return res;
283
+ },
284
+
285
+ createRenderPipeline(desc: RenderPipelineDescriptor) {
286
+ const res = realDevice.createRenderPipeline(desc);
287
+ if (!res.ok) return res;
288
+ let layoutId: HandleId;
289
+ if (typeof desc.layout === 'string') {
290
+ layoutId = 'layout:auto';
291
+ } else {
292
+ layoutId = getHandleId(s, desc.layout as object, 'pipelineLayout');
293
+ }
294
+ let vertexShaderModuleHandleId: HandleId | undefined;
295
+ if (desc.vertex !== undefined) {
296
+ vertexShaderModuleHandleId = getHandleId(s, desc.vertex.module as object, 'shaderModule');
297
+ }
298
+ let fragmentShaderModuleHandleId: HandleId | undefined;
299
+ if (desc.fragment !== undefined) {
300
+ fragmentShaderModuleHandleId = getHandleId(
301
+ s,
302
+ desc.fragment.module as object,
303
+ 'shaderModule',
304
+ );
305
+ }
306
+ const event: RhiCallEvent = {
307
+ kind: 'createRenderPipeline',
308
+ handleId: '' as HandleId,
309
+ desc: {
310
+ vertex: desc.vertex,
311
+ primitive: desc.primitive,
312
+ depthStencil: desc.depthStencil,
313
+ multisample: desc.multisample,
314
+ fragment: desc.fragment,
315
+ },
316
+ layoutHandleId: layoutId,
317
+ vertexShaderModuleHandleId,
318
+ fragmentShaderModuleHandleId,
319
+ };
320
+ registerHandle(s, res.value as object, 'renderPipeline', event);
321
+ pushEvent(s, event);
322
+ return res;
323
+ },
324
+
325
+ createComputePipeline(desc: ComputePipelineDescriptor) {
326
+ const res = realDevice.createComputePipeline(desc);
327
+ if (!res.ok) return res;
328
+ let layoutId: HandleId;
329
+ if (typeof desc.layout === 'string') {
330
+ layoutId = 'layout:auto';
331
+ } else {
332
+ layoutId = getHandleId(s, desc.layout as object, 'pipelineLayout');
333
+ }
334
+ const computeShaderModuleHandleId = getHandleId(
335
+ s,
336
+ desc.compute.module as object,
337
+ 'shaderModule',
338
+ );
339
+ const event: RhiCallEvent = {
340
+ kind: 'createComputePipeline',
341
+ handleId: '' as HandleId,
342
+ desc: { compute: JSON.parse(JSON.stringify(desc.compute)) },
343
+ layoutHandleId: layoutId,
344
+ computeShaderModuleHandleId,
345
+ };
346
+ registerHandle(s, res.value as object, 'computePipeline', event);
347
+ pushEvent(s, event);
348
+ return res;
349
+ },
350
+
351
+ createQuerySet(desc: QuerySetDescriptor) {
352
+ return realDevice.createQuerySet(desc);
353
+ },
354
+
355
+ destroyBuffer(buf: Buffer) {
356
+ const hId = s.handleMap.get(buf as object);
357
+ const res = realDevice.destroyBuffer(buf);
358
+ if (!res.ok) return res;
359
+ if (hId !== undefined) {
360
+ s.descriptorTable.delete(hId);
361
+ // Bound bootstrapCreates growth: an idle-destroyed resource can never
362
+ // be referenced by a future frame's events, so drop its create event.
363
+ // Only when NOT recording — mid-capture, an earlier frame event may
364
+ // still reference this handle, and the tape prefix needs its create
365
+ // event to stay self-contained (tape-invalid otherwise).
366
+ //
367
+ // Known bounded residual: a resource destroyed DURING a capture keeps
368
+ // its bootstrapCreates entry forever — the entry is only ever revisited
369
+ // by another destroy* of the SAME handle, which can't recur after the
370
+ // resource is gone. This leaks one create event per resource that is
371
+ // both created and destroyed inside a recording window (a narrow set;
372
+ // most resources are long-lived). Fixing it needs a defer-delete list
373
+ // swept at the next arm() — deliberately not added: the extra state
374
+ // costs more than the leak it plugs (see PR discussion).
375
+ //
376
+ // Separate, wider residual: bootstrapCreates entries for
377
+ // textureView / bindGroup / pipeline / sampler are NEVER pruned,
378
+ // because RhiDevice exposes destroy only for buffer / texture -- a
379
+ // faithful mirror of WebGPU, whose spec puts .destroy() solely on
380
+ // GPUBuffer / GPUTexture (the objects holding large, eagerly-freeable
381
+ // backing memory); views / bind groups / pipelines / samplers are
382
+ // lightweight reference objects left to GC. So there is no destroy
383
+ // signal to hook for them. An app that rebuilds these per frame grows
384
+ // bootstrapCreates under a long idle run. The GC-aligned fix is a
385
+ // WeakRef + FinalizationRegistry over the create-event objects (same
386
+ // spirit as handleMap's WeakMap, but keyed by handleId so it needs the
387
+ // WeakRef wrapper) -- a finalization-semantics change, out of scope here.
388
+ if (
389
+ !retainsCaptureBootstrap(s) &&
390
+ !s.snapshotSeededHandles.has(hId) &&
391
+ !hasBootstrapDependency(s, hId)
392
+ ) {
393
+ s.bootstrapCreates.delete(hId);
394
+ }
395
+ pushEvent(s, { kind: 'destroyBuffer', handleId: hId });
396
+ }
397
+ return res;
398
+ },
399
+
400
+ destroyQuerySet(querySet: QuerySet) {
401
+ return realDevice.destroyQuerySet(querySet);
402
+ },
403
+
404
+ destroyTexture(tex: Texture) {
405
+ const hId = s.handleMap.get(tex as object);
406
+ const res = realDevice.destroyTexture(tex);
407
+ if (!res.ok) return res;
408
+ if (hId !== undefined) {
409
+ s.descriptorTable.delete(hId);
410
+ // Same gate + same bounded residual as destroyBuffer above.
411
+ if (
412
+ !retainsCaptureBootstrap(s) &&
413
+ !s.snapshotSeededHandles.has(hId) &&
414
+ !hasBootstrapDependency(s, hId)
415
+ ) {
416
+ s.bootstrapCreates.delete(hId);
417
+ }
418
+ pushEvent(s, { kind: 'destroyTexture', handleId: hId });
419
+ }
420
+ return res;
421
+ },
422
+
423
+ createCommandEncoder(desc?: CommandEncoderDescriptor | undefined) {
424
+ const res = realDevice.createCommandEncoder(desc);
425
+ if (!res.ok) return res;
426
+ // Idle fast-path: return the real (un-proxied) encoder when not
427
+ // recording. One gate here elides the proxy wrapper AND every
428
+ // beginRenderPass/draw/setBindGroup/copy* call that would otherwise
429
+ // route through proxyCmdEncoder + proxyRenderPass only to be dropped
430
+ // by the pushEvent gate. Safe because a frame body runs synchronously
431
+ // in one rAF callback: createCommandEncoder -> passes -> queue.submit
432
+ // all observe the same recorder state (arm() only fires between frames),
433
+ // so an idle-created encoder's whole frame is consistently un-recorded,
434
+ // matching the now-gated queue.submit which skips its handle lookup.
435
+ if (!shouldRecord(s)) {
436
+ return res;
437
+ }
438
+ const cmdId = allocHandleId('commandEncoder');
439
+ pushEvent(s, {
440
+ kind: 'createCommandEncoder',
441
+ cmdHandleId: cmdId,
442
+ desc: desc as Partial<GPUCommandEncoderDescriptor> | undefined,
443
+ });
444
+ const proxyEnc = createCommandEncoderProxy(s, res.value, cmdId);
445
+ return makeOk(proxyEnc as RhiCommandEncoder) as Result<
446
+ RhiCommandEncoder,
447
+ import('@forgeax/engine-rhi').RhiError
448
+ >;
449
+ },
450
+ };
451
+ return d;
452
+ }
453
+
454
+ // --------------------------------------------------
455
+ // wrap RhiInstance
456
+ // --------------------------------------------------
@@ -0,0 +1,234 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/encoder -- command encoder proxy owner.
2
+
3
+ import type { Buffer, ComputePassDescriptor, RhiCommandEncoder } from '@forgeax/engine-rhi';
4
+ import type { HandleId } from '../types';
5
+ import type { RecorderInternal } from './core';
6
+ import { allocHandleId, getHandleId, pushEvent } from './core';
7
+ import { createComputePassProxy, createRenderPassProxy } from './pass';
8
+
9
+ export function createCommandEncoderProxy(
10
+ s: RecorderInternal,
11
+ realEnc: RhiCommandEncoder,
12
+ cmdHId: HandleId,
13
+ ): RhiCommandEncoder {
14
+ return {
15
+ beginRenderPass(desc: GPURenderPassDescriptor) {
16
+ const passHId = allocHandleId('renderPass');
17
+ // I-2 fix-up (round 1, dawn smoke): walk colorAttachments +
18
+ // depthStencilAttachment to extract the textureView handleIds
19
+ // recorded under createTextureView. Without this the replayer
20
+ // would dereference the original GPUTextureView brand directly,
21
+ // which fails on a fresh device (cross-device GPU object reuse).
22
+ const colorAttachmentViewHandleIds: (HandleId | undefined)[] = [];
23
+ const colorAttachmentResolveTargetHandleIds: (HandleId | undefined)[] = [];
24
+ for (const att of desc.colorAttachments) {
25
+ if (att === null || att === undefined) {
26
+ colorAttachmentViewHandleIds.push(undefined);
27
+ colorAttachmentResolveTargetHandleIds.push(undefined);
28
+ } else {
29
+ const view = (att as GPURenderPassColorAttachment).view;
30
+ const id =
31
+ view !== undefined && view !== null ? s.handleMap.get(view as object) : undefined;
32
+ colorAttachmentViewHandleIds.push(id);
33
+ const resolveTarget = (att as GPURenderPassColorAttachment).resolveTarget;
34
+ const resolveTargetId =
35
+ resolveTarget !== undefined && resolveTarget !== null
36
+ ? s.handleMap.get(resolveTarget as object)
37
+ : undefined;
38
+ colorAttachmentResolveTargetHandleIds.push(resolveTargetId);
39
+ }
40
+ }
41
+ let depthStencilViewHandleId: HandleId | undefined;
42
+ if (desc.depthStencilAttachment !== undefined) {
43
+ const dsView = desc.depthStencilAttachment.view;
44
+ if (dsView !== undefined && dsView !== null) {
45
+ depthStencilViewHandleId = s.handleMap.get(dsView as object);
46
+ }
47
+ }
48
+ pushEvent(s, {
49
+ kind: 'beginRenderPass',
50
+ cmdHandleId: cmdHId,
51
+ passHandleId: passHId,
52
+ desc: desc as Omit<GPURenderPassDescriptor, 'label'>,
53
+ colorAttachmentViewHandleIds,
54
+ colorAttachmentResolveTargetHandleIds,
55
+ depthStencilViewHandleId,
56
+ });
57
+ const realPass = realEnc.beginRenderPass(desc);
58
+ return createRenderPassProxy(s, realPass, passHId);
59
+ },
60
+ beginComputePass(desc?: ComputePassDescriptor | undefined) {
61
+ const passHId = allocHandleId('computePass');
62
+ pushEvent(s, {
63
+ kind: 'beginComputePass',
64
+ cmdHandleId: cmdHId,
65
+ passHandleId: passHId,
66
+ desc: desc as Partial<GPUComputePassDescriptor> | undefined,
67
+ });
68
+ const realPass = realEnc.beginComputePass(desc);
69
+ return createComputePassProxy(s, realPass, passHId);
70
+ },
71
+ // Passthrough copy/clear methods (event recording added where types permit)
72
+ copyBufferToBuffer(...args: unknown[]) {
73
+ // Overloaded: 3-arg or 5-arg
74
+ if (typeof args[1] === 'number') {
75
+ // 5-arg form: (source, sourceOffset, destination, destinationOffset, size)
76
+ const sourceId = getHandleId(s, args[0] as object, 'buffer');
77
+ const destinationId = getHandleId(s, args[2] as object, 'buffer');
78
+ pushEvent(s, {
79
+ kind: 'copyBufferToBuffer',
80
+ cmdHandleId: cmdHId,
81
+ sourceHandleId: sourceId,
82
+ sourceOffset: args[1] as number,
83
+ destinationHandleId: destinationId,
84
+ destinationOffset: args[3] as number,
85
+ size: args[4] as number,
86
+ });
87
+ (realEnc.copyBufferToBuffer as (...args: unknown[]) => void)(...args);
88
+ } else {
89
+ // 3-arg form: (source, destination, size?)
90
+ const sourceId = getHandleId(s, args[0] as object, 'buffer');
91
+ const destinationId = getHandleId(s, args[1] as object, 'buffer');
92
+ const size = args[2] as number | undefined;
93
+ pushEvent(s, {
94
+ kind: 'copyBufferToBuffer',
95
+ cmdHandleId: cmdHId,
96
+ sourceHandleId: sourceId,
97
+ sourceOffset: 0,
98
+ destinationHandleId: destinationId,
99
+ destinationOffset: 0,
100
+ size: (size ?? 0) as number,
101
+ });
102
+ realEnc.copyBufferToBuffer(args[0] as Buffer, args[1] as Buffer, size);
103
+ }
104
+ },
105
+ copyBufferToTexture(source, destination, copySize) {
106
+ const bufId = getHandleId(s, (source as { buffer: object }).buffer as object, 'buffer');
107
+ const texId = getHandleId(
108
+ s,
109
+ (destination as { texture: object }).texture as object,
110
+ 'texture',
111
+ );
112
+ const dstPayload: Record<string, unknown> = { textureHandleId: texId };
113
+ if (destination.mipLevel !== undefined) dstPayload.mipLevel = destination.mipLevel;
114
+ if (destination.origin !== undefined) dstPayload.origin = destination.origin;
115
+ if (destination.aspect !== undefined) dstPayload.aspect = destination.aspect;
116
+ pushEvent(s, {
117
+ kind: 'copyBufferToTexture',
118
+ cmdHandleId: cmdHId,
119
+ source: {
120
+ bufferHandleId: bufId,
121
+ offset: source.offset ?? 0,
122
+ bytesPerRow: source.bytesPerRow ?? 0,
123
+ rowsPerImage: source.rowsPerImage ?? 0,
124
+ },
125
+ destination: dstPayload as Omit<GPUTexelCopyTextureInfo, 'texture'> & {
126
+ readonly textureHandleId: HandleId;
127
+ },
128
+ copySize,
129
+ });
130
+ realEnc.copyBufferToTexture(source, destination, copySize);
131
+ },
132
+ copyTextureToBuffer(source, destination, copySize) {
133
+ const texId = getHandleId(s, (source as { texture: object }).texture as object, 'texture');
134
+ const bufId = getHandleId(s, (destination as { buffer: object }).buffer as object, 'buffer');
135
+ const srcPayload: Record<string, unknown> = { textureHandleId: texId };
136
+ if (source.mipLevel !== undefined) srcPayload.mipLevel = source.mipLevel;
137
+ if (source.origin !== undefined) srcPayload.origin = source.origin;
138
+ if (source.aspect !== undefined) srcPayload.aspect = source.aspect;
139
+ pushEvent(s, {
140
+ kind: 'copyTextureToBuffer',
141
+ cmdHandleId: cmdHId,
142
+ source: srcPayload as Omit<GPUTexelCopyTextureInfo, 'texture'> & {
143
+ readonly textureHandleId: HandleId;
144
+ },
145
+ destination: {
146
+ bufferHandleId: bufId,
147
+ offset: destination.offset ?? 0,
148
+ bytesPerRow: destination.bytesPerRow ?? 0,
149
+ rowsPerImage: destination.rowsPerImage ?? 0,
150
+ },
151
+ copySize,
152
+ });
153
+ realEnc.copyTextureToBuffer(source, destination, copySize);
154
+ },
155
+ copyTextureToTexture(source, destination, copySize) {
156
+ const srcTexId = getHandleId(s, (source as { texture: object }).texture as object, 'texture');
157
+ const dstTexId = getHandleId(
158
+ s,
159
+ (destination as { texture: object }).texture as object,
160
+ 'texture',
161
+ );
162
+ const srcPayload: Record<string, unknown> = { textureHandleId: srcTexId };
163
+ if (source.mipLevel !== undefined) srcPayload.mipLevel = source.mipLevel;
164
+ if (source.origin !== undefined) srcPayload.origin = source.origin;
165
+ if (source.aspect !== undefined) srcPayload.aspect = source.aspect;
166
+ const dstPayload: Record<string, unknown> = { textureHandleId: dstTexId };
167
+ if (destination.mipLevel !== undefined) dstPayload.mipLevel = destination.mipLevel;
168
+ if (destination.origin !== undefined) dstPayload.origin = destination.origin;
169
+ if (destination.aspect !== undefined) dstPayload.aspect = destination.aspect;
170
+ pushEvent(s, {
171
+ kind: 'copyTextureToTexture',
172
+ cmdHandleId: cmdHId,
173
+ source: srcPayload as Omit<GPUTexelCopyTextureInfo, 'texture'> & {
174
+ readonly textureHandleId: HandleId;
175
+ },
176
+ destination: dstPayload as Omit<GPUTexelCopyTextureInfo, 'texture'> & {
177
+ readonly textureHandleId: HandleId;
178
+ },
179
+ copySize,
180
+ });
181
+ realEnc.copyTextureToTexture(source, destination, copySize);
182
+ },
183
+ clearBuffer(buffer, offset, size) {
184
+ const bufId = getHandleId(s, buffer as object, 'buffer');
185
+ pushEvent(s, {
186
+ kind: 'clearBuffer',
187
+ cmdHandleId: cmdHId,
188
+ handleId: bufId,
189
+ offset,
190
+ size,
191
+ });
192
+ realEnc.clearBuffer(buffer, offset, size);
193
+ },
194
+ resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset) {
195
+ return realEnc.resolveQuerySet(
196
+ querySet,
197
+ firstQuery,
198
+ queryCount,
199
+ destination,
200
+ destinationOffset,
201
+ );
202
+ },
203
+ writeTimestamp(querySet, queryIndex) {
204
+ realEnc.writeTimestamp(querySet, queryIndex);
205
+ },
206
+ pushDebugGroup(groupLabel) {
207
+ pushEvent(s, { kind: 'pushDebugGroup', cmdHandleId: cmdHId, groupLabel });
208
+ realEnc.pushDebugGroup(groupLabel);
209
+ },
210
+ popDebugGroup() {
211
+ pushEvent(s, { kind: 'popDebugGroup', cmdHandleId: cmdHId });
212
+ realEnc.popDebugGroup();
213
+ },
214
+ insertDebugMarker(markerLabel) {
215
+ pushEvent(s, { kind: 'insertDebugMarker', cmdHandleId: cmdHId, markerLabel });
216
+ realEnc.insertDebugMarker(markerLabel);
217
+ },
218
+ finish() {
219
+ pushEvent(s, { kind: 'finish', cmdHandleId: cmdHId });
220
+ const res = realEnc.finish();
221
+ // I-2 fix-up (round 1, dawn smoke handle-graph integrity):
222
+ // alias the resulting CommandBuffer object to the encoder's
223
+ // cmdHandleId so subsequent queue.submit() handle lookups resolve
224
+ // to the SAME id that the createCommandEncoder/finish event pair
225
+ // declared. Without this, submit would register a fresh handleId
226
+ // for the CommandBuffer object and the tape's handle graph
227
+ // integrity check would reject the tape on deserialize.
228
+ if (res.ok) {
229
+ s.handleMap.set(res.value as object, cmdHId);
230
+ }
231
+ return res;
232
+ },
233
+ };
234
+ }