@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,474 @@
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 { module: _vertexModule, constants: vertexConstants, ...vertexFields } = desc.vertex;
307
+ const recordedVertex =
308
+ vertexConstants === undefined
309
+ ? vertexFields
310
+ : { ...vertexFields, constants: vertexConstants };
311
+ const recordedFragment =
312
+ desc.fragment === undefined
313
+ ? undefined
314
+ : (() => {
315
+ const {
316
+ module: _fragmentModule,
317
+ constants: fragmentConstants,
318
+ ...fragmentFields
319
+ } = desc.fragment;
320
+ return fragmentConstants === undefined
321
+ ? fragmentFields
322
+ : { ...fragmentFields, constants: fragmentConstants };
323
+ })();
324
+ const event: RhiCallEvent = {
325
+ kind: 'createRenderPipeline',
326
+ handleId: '' as HandleId,
327
+ desc: {
328
+ vertex: recordedVertex,
329
+ primitive: desc.primitive,
330
+ depthStencil: desc.depthStencil,
331
+ multisample: desc.multisample,
332
+ ...(recordedFragment === undefined ? {} : { fragment: recordedFragment }),
333
+ },
334
+ layoutHandleId: layoutId,
335
+ vertexShaderModuleHandleId,
336
+ fragmentShaderModuleHandleId,
337
+ };
338
+ registerHandle(s, res.value as object, 'renderPipeline', event);
339
+ pushEvent(s, event);
340
+ return res;
341
+ },
342
+
343
+ createComputePipeline(desc: ComputePipelineDescriptor) {
344
+ const res = realDevice.createComputePipeline(desc);
345
+ if (!res.ok) return res;
346
+ let layoutId: HandleId;
347
+ if (typeof desc.layout === 'string') {
348
+ layoutId = 'layout:auto';
349
+ } else {
350
+ layoutId = getHandleId(s, desc.layout as object, 'pipelineLayout');
351
+ }
352
+ const computeShaderModuleHandleId = getHandleId(
353
+ s,
354
+ desc.compute.module as object,
355
+ 'shaderModule',
356
+ );
357
+ const event: RhiCallEvent = {
358
+ kind: 'createComputePipeline',
359
+ handleId: '' as HandleId,
360
+ desc: { compute: JSON.parse(JSON.stringify(desc.compute)) },
361
+ layoutHandleId: layoutId,
362
+ computeShaderModuleHandleId,
363
+ };
364
+ registerHandle(s, res.value as object, 'computePipeline', event);
365
+ pushEvent(s, event);
366
+ return res;
367
+ },
368
+
369
+ createQuerySet(desc: QuerySetDescriptor) {
370
+ return realDevice.createQuerySet(desc);
371
+ },
372
+
373
+ destroyBuffer(buf: Buffer) {
374
+ const hId = s.handleMap.get(buf as object);
375
+ const res = realDevice.destroyBuffer(buf);
376
+ if (!res.ok) return res;
377
+ if (hId !== undefined) {
378
+ s.descriptorTable.delete(hId);
379
+ // Bound bootstrapCreates growth: an idle-destroyed resource can never
380
+ // be referenced by a future frame's events, so drop its create event.
381
+ // Only when NOT recording — mid-capture, an earlier frame event may
382
+ // still reference this handle, and the tape prefix needs its create
383
+ // event to stay self-contained (tape-invalid otherwise).
384
+ //
385
+ // Known bounded residual: a resource destroyed DURING a capture keeps
386
+ // its bootstrapCreates entry forever — the entry is only ever revisited
387
+ // by another destroy* of the SAME handle, which can't recur after the
388
+ // resource is gone. This leaks one create event per resource that is
389
+ // both created and destroyed inside a recording window (a narrow set;
390
+ // most resources are long-lived). Fixing it needs a defer-delete list
391
+ // swept at the next arm() — deliberately not added: the extra state
392
+ // costs more than the leak it plugs (see PR discussion).
393
+ //
394
+ // Separate, wider residual: bootstrapCreates entries for
395
+ // textureView / bindGroup / pipeline / sampler are NEVER pruned,
396
+ // because RhiDevice exposes destroy only for buffer / texture -- a
397
+ // faithful mirror of WebGPU, whose spec puts .destroy() solely on
398
+ // GPUBuffer / GPUTexture (the objects holding large, eagerly-freeable
399
+ // backing memory); views / bind groups / pipelines / samplers are
400
+ // lightweight reference objects left to GC. So there is no destroy
401
+ // signal to hook for them. An app that rebuilds these per frame grows
402
+ // bootstrapCreates under a long idle run. The GC-aligned fix is a
403
+ // WeakRef + FinalizationRegistry over the create-event objects (same
404
+ // spirit as handleMap's WeakMap, but keyed by handleId so it needs the
405
+ // WeakRef wrapper) -- a finalization-semantics change, out of scope here.
406
+ if (
407
+ !retainsCaptureBootstrap(s) &&
408
+ !s.snapshotSeededHandles.has(hId) &&
409
+ !hasBootstrapDependency(s, hId)
410
+ ) {
411
+ s.bootstrapCreates.delete(hId);
412
+ }
413
+ pushEvent(s, { kind: 'destroyBuffer', handleId: hId });
414
+ }
415
+ return res;
416
+ },
417
+
418
+ destroyQuerySet(querySet: QuerySet) {
419
+ return realDevice.destroyQuerySet(querySet);
420
+ },
421
+
422
+ destroyTexture(tex: Texture) {
423
+ const hId = s.handleMap.get(tex as object);
424
+ const res = realDevice.destroyTexture(tex);
425
+ if (!res.ok) return res;
426
+ if (hId !== undefined) {
427
+ s.descriptorTable.delete(hId);
428
+ // Same gate + same bounded residual as destroyBuffer above.
429
+ if (
430
+ !retainsCaptureBootstrap(s) &&
431
+ !s.snapshotSeededHandles.has(hId) &&
432
+ !hasBootstrapDependency(s, hId)
433
+ ) {
434
+ s.bootstrapCreates.delete(hId);
435
+ }
436
+ pushEvent(s, { kind: 'destroyTexture', handleId: hId });
437
+ }
438
+ return res;
439
+ },
440
+
441
+ createCommandEncoder(desc?: CommandEncoderDescriptor | undefined) {
442
+ const res = realDevice.createCommandEncoder(desc);
443
+ if (!res.ok) return res;
444
+ // Idle fast-path: return the real (un-proxied) encoder when not
445
+ // recording. One gate here elides the proxy wrapper AND every
446
+ // beginRenderPass/draw/setBindGroup/copy* call that would otherwise
447
+ // route through proxyCmdEncoder + proxyRenderPass only to be dropped
448
+ // by the pushEvent gate. Safe because a frame body runs synchronously
449
+ // in one rAF callback: createCommandEncoder -> passes -> queue.submit
450
+ // all observe the same recorder state (arm() only fires between frames),
451
+ // so an idle-created encoder's whole frame is consistently un-recorded,
452
+ // matching the now-gated queue.submit which skips its handle lookup.
453
+ if (!shouldRecord(s)) {
454
+ return res;
455
+ }
456
+ const cmdId = allocHandleId('commandEncoder');
457
+ pushEvent(s, {
458
+ kind: 'createCommandEncoder',
459
+ cmdHandleId: cmdId,
460
+ desc: desc as Partial<GPUCommandEncoderDescriptor> | undefined,
461
+ });
462
+ const proxyEnc = createCommandEncoderProxy(s, res.value, cmdId);
463
+ return makeOk(proxyEnc as RhiCommandEncoder) as Result<
464
+ RhiCommandEncoder,
465
+ import('@forgeax/engine-rhi').RhiError
466
+ >;
467
+ },
468
+ };
469
+ return d;
470
+ }
471
+
472
+ // --------------------------------------------------
473
+ // wrap RhiInstance
474
+ // --------------------------------------------------