@woosh/meep-engine 3.10.0 → 3.11.0

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 (142) hide show
  1. package/README.md +168 -152
  2. package/package.json +1 -1
  3. package/src/REVIEW_2026_08_06.md +610 -610
  4. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.js +28 -4
  6. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts +26 -0
  7. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
  8. package/src/engine/graphics/render/frame_graph/FrameGraph.js +47 -0
  9. package/src/engine/physics/ecs/PhysicsSystem.d.ts +0 -3
  10. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  11. package/src/engine/physics/ecs/PhysicsSystem.js +0 -3
  12. package/src/shade/device/ShadeGPUCommandContext.d.ts +21 -0
  13. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  14. package/src/shade/device/ShadeGPUCommandContext.js +940 -749
  15. package/src/shade/device/timing/GPUTimerArray.d.ts +22 -4
  16. package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
  17. package/src/shade/device/timing/GPUTimerArray.js +88 -14
  18. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +349 -98
  19. package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts +93 -0
  20. package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts.map +1 -0
  21. package/src/shade/device/timing/profile/GPUFrameRecorder.js +297 -0
  22. package/src/shade/device/timing/profile/GPUProfileCapture.d.ts +62 -0
  23. package/src/shade/device/timing/profile/GPUProfileCapture.d.ts.map +1 -0
  24. package/src/shade/device/timing/profile/GPUProfileCapture.js +77 -0
  25. package/src/shade/device/timing/profile/GPUProfileFrame.d.ts +73 -0
  26. package/src/shade/device/timing/profile/GPUProfileFrame.d.ts.map +1 -0
  27. package/src/shade/device/timing/profile/GPUProfileFrame.js +88 -0
  28. package/src/shade/device/timing/profile/GPUProfileLevel.d.ts +20 -0
  29. package/src/shade/device/timing/profile/GPUProfileLevel.d.ts.map +1 -0
  30. package/src/shade/device/timing/profile/GPUProfileLevel.js +50 -0
  31. package/src/shade/device/timing/profile/GPUProfileMeta.d.ts +63 -0
  32. package/src/shade/device/timing/profile/GPUProfileMeta.d.ts.map +1 -0
  33. package/src/shade/device/timing/profile/GPUProfileMeta.js +73 -0
  34. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +173 -0
  35. package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -0
  36. package/src/shade/device/timing/profile/GPUProfileSession.js +470 -0
  37. package/src/shade/device/timing/profile/GPUProfileSpan.d.ts +60 -0
  38. package/src/shade/device/timing/profile/GPUProfileSpan.d.ts.map +1 -0
  39. package/src/shade/device/timing/profile/GPUProfileSpan.js +65 -0
  40. package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts +19 -0
  41. package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts.map +1 -0
  42. package/src/shade/device/timing/profile/GPUProfileSpanKind.js +25 -0
  43. package/src/shade/device/timing/profile/GPUProfileTopology.d.ts +159 -0
  44. package/src/shade/device/timing/profile/GPUProfileTopology.d.ts.map +1 -0
  45. package/src/shade/device/timing/profile/GPUProfileTopology.js +202 -0
  46. package/src/shade/device/timing/profile/GPUProfileWork.d.ts +90 -0
  47. package/src/shade/device/timing/profile/GPUProfileWork.d.ts.map +1 -0
  48. package/src/shade/device/timing/profile/GPUProfileWork.js +115 -0
  49. package/src/shade/device/timing/profile/SGPTDefect.d.ts +43 -0
  50. package/src/shade/device/timing/profile/SGPTDefect.d.ts.map +1 -0
  51. package/src/shade/device/timing/profile/SGPTDefect.js +65 -0
  52. package/src/shade/device/timing/profile/SGPTHeader.d.ts +38 -0
  53. package/src/shade/device/timing/profile/SGPTHeader.d.ts.map +1 -0
  54. package/src/shade/device/timing/profile/SGPTHeader.js +46 -0
  55. package/src/shade/device/timing/profile/SGPTRecordRef.d.ts +37 -0
  56. package/src/shade/device/timing/profile/SGPTRecordRef.d.ts.map +1 -0
  57. package/src/shade/device/timing/profile/SGPTRecordRef.js +45 -0
  58. package/src/shade/device/timing/profile/SGPTWriteStream.d.ts +103 -0
  59. package/src/shade/device/timing/profile/SGPTWriteStream.d.ts.map +1 -0
  60. package/src/shade/device/timing/profile/SGPTWriteStream.js +287 -0
  61. package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts +19 -0
  62. package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts.map +1 -0
  63. package/src/shade/device/timing/profile/SGPT_DEFECT.js +26 -0
  64. package/src/shade/device/timing/profile/SGPT_FLAG.d.ts +8 -0
  65. package/src/shade/device/timing/profile/SGPT_FLAG.d.ts.map +1 -0
  66. package/src/shade/device/timing/profile/SGPT_FLAG.js +15 -0
  67. package/src/shade/device/timing/profile/SGPT_FORMAT.md +345 -0
  68. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts +10 -0
  69. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts.map +1 -0
  70. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.js +9 -0
  71. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts +17 -0
  72. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts.map +1 -0
  73. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.js +16 -0
  74. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts +16 -0
  75. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts.map +1 -0
  76. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.js +15 -0
  77. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts +15 -0
  78. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts.map +1 -0
  79. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.js +29 -0
  80. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts +7 -0
  81. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts.map +1 -0
  82. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.js +6 -0
  83. package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts +7 -0
  84. package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts.map +1 -0
  85. package/src/shade/device/timing/profile/SGPT_MAGIC.js +6 -0
  86. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts +16 -0
  87. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts.map +1 -0
  88. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.js +15 -0
  89. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts +7 -0
  90. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts.map +1 -0
  91. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.js +6 -0
  92. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts +13 -0
  93. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts.map +1 -0
  94. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.js +23 -0
  95. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts +12 -0
  96. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts.map +1 -0
  97. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.js +11 -0
  98. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts +18 -0
  99. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts.map +1 -0
  100. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.js +46 -0
  101. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts +20 -0
  102. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -0
  103. package/src/shade/device/timing/profile/frame_graph_extract_topology.js +174 -0
  104. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +21 -0
  105. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +1 -0
  106. package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +114 -0
  107. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +20 -0
  108. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +1 -0
  109. package/src/shade/device/timing/profile/parse_workgroup_size.js +43 -0
  110. package/src/shade/device/timing/profile/sgpt_fourcc.d.ts +18 -0
  111. package/src/shade/device/timing/profile/sgpt_fourcc.d.ts.map +1 -0
  112. package/src/shade/device/timing/profile/sgpt_fourcc.js +34 -0
  113. package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts +42 -0
  114. package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts.map +1 -0
  115. package/src/shade/device/timing/profile/sgpt_frame_codec.js +172 -0
  116. package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts +20 -0
  117. package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts.map +1 -0
  118. package/src/shade/device/timing/profile/sgpt_meta_codec.js +65 -0
  119. package/src/shade/device/timing/profile/sgpt_read_capture.d.ts +19 -0
  120. package/src/shade/device/timing/profile/sgpt_read_capture.d.ts.map +1 -0
  121. package/src/shade/device/timing/profile/sgpt_read_capture.js +134 -0
  122. package/src/shade/device/timing/profile/sgpt_read_header.d.ts +22 -0
  123. package/src/shade/device/timing/profile/sgpt_read_header.d.ts.map +1 -0
  124. package/src/shade/device/timing/profile/sgpt_read_header.js +94 -0
  125. package/src/shade/device/timing/profile/sgpt_read_records.d.ts +27 -0
  126. package/src/shade/device/timing/profile/sgpt_read_records.d.ts.map +1 -0
  127. package/src/shade/device/timing/profile/sgpt_read_records.js +153 -0
  128. package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts +54 -0
  129. package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts.map +1 -0
  130. package/src/shade/device/timing/profile/sgpt_topology_codec.js +329 -0
  131. package/src/shade/device/timing/profile/sgpt_write_header.d.ts +22 -0
  132. package/src/shade/device/timing/profile/sgpt_write_header.d.ts.map +1 -0
  133. package/src/shade/device/timing/profile/sgpt_write_header.js +47 -0
  134. package/src/shade/device/timing/profile/sgpt_write_record.d.ts +23 -0
  135. package/src/shade/device/timing/profile/sgpt_write_record.d.ts.map +1 -0
  136. package/src/shade/device/timing/profile/sgpt_write_record.js +62 -0
  137. package/src/shade/renderer/Renderer.d.ts +21 -0
  138. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  139. package/src/shade/renderer/Renderer.js +2513 -2455
  140. package/src/engine/save/GameStateLoader.d.ts +0 -49
  141. package/src/engine/save/GameStateLoader.d.ts.map +0 -1
  142. package/src/engine/save/GameStateLoader.js +0 -168
@@ -1,750 +1,941 @@
1
- import { assert } from "../../core/assert.js";
2
- import { array_push_if_unique } from "../../core/collection/array/array_push_if_unique.js";
3
- import { array_remove_first } from "../../core/collection/array/array_remove_first.js";
4
- import { array_buffer_copy } from "../../core/collection/array/typed/array_buffer_copy.js";
5
- import Signal from "../../core/events/signal/Signal.js";
6
- import { noop } from "../../core/function/noop.js";
7
- import { GPUTimerArray } from "./timing/GPUTimerArray.js";
8
- import { warn_limited } from "../util/warn_limited.js";
9
- import { write_gpu_typed_buffer } from "../renderer/shader/type/serde/write_gpu_typed_buffer.js";
10
- import { BufferResourceDescriptor } from "./graph/BufferResourceDescriptor.js";
11
- import { GraphicsExecutionContext } from "./graph/GraphicsExecutionContext.js";
12
-
13
- /**
14
- * Keep track of how many contexts are currently open (unfinished)
15
- * Important because contexts can have transient resources allocated, and those are not cleaned up until the context is `finished`
16
- * @type {number}
17
- */
18
- let debug_open_context_count = 0;
19
-
20
- /**
21
- *
22
- * @type {ShadeGPUCommandContext[]}
23
- */
24
- const debug_open_contexts = [];
25
-
26
- const scratch_buffer_descriptor = new BufferResourceDescriptor();
27
-
28
- let id_counter = 0;
29
-
30
- /**
31
- * Abstraction on top of {@link GPUCommandEncoder}.
32
- *
33
- * Inspired by Intel's "MiniEngine" CommandContext
34
- * @see https://github.com/GameTechDev/TAA/blob/39786709cf70a1e0906196c600f6079571a33ceb/MiniEngine/Core/CommandContext.h
35
- * @see https://www.w3.org/TR/webgpu/#gpucommandencoder
36
- */
37
- export class ShadeGPUCommandContext {
38
-
39
- #id = id_counter++;
40
-
41
- /**
42
- * Unique identifier for this context, useful for debugging
43
- *
44
- * @return {number}
45
- */
46
- get id() {
47
- return this.#id;
48
- }
49
-
50
- /**
51
- *
52
- * @returns {boolean}
53
- */
54
- get isGPUCommandContext() {
55
- return true;
56
- }
57
-
58
-
59
- /**
60
- * Actual underlying encoder
61
- * @type {GPUCommandEncoder}
62
- */
63
- #gpu_encoder
64
-
65
- /**
66
- * Don't circumvent {@link ShadeGPUCommandContext} unless you know what you're doing.
67
- * {@link ShadeGPUCommandContext} is a wrapper around {@link GPUCommandEncoder} that provides additional spec-compliance asserts to help find errors sooner.
68
- * @returns {GPUCommandEncoder}
69
- */
70
- get gpu_encoder() {
71
- return this.#gpu_encoder;
72
- }
73
-
74
- /**
75
- * @type {GPUTimerArray|undefined}
76
- */
77
- #debug_timers;
78
-
79
- /**
80
- *
81
- * @type {function}
82
- */
83
- #debug_timers_callback = noop;
84
-
85
- /**
86
- *
87
- * @param {function} debug_timers_callback
88
- */
89
- enable_debug_timers(debug_timers_callback) {
90
- if (this.#debug_timers !== undefined) {
91
- // already enabled
92
- return;
93
- }
94
-
95
- this.#debug_timers = new GPUTimerArray(this.device);
96
- this.#debug_timers_callback = debug_timers_callback;
97
- }
98
-
99
- /**
100
- * @type {GraphicsContext}
101
- */
102
- #graphics
103
-
104
- /**
105
- *
106
- * @return {GPUDevice}
107
- */
108
- get device() {
109
- return this.#graphics.device;
110
- }
111
-
112
- /**
113
- *
114
- * @return {TextureManager}
115
- */
116
- get textures() {
117
- return this.#graphics.textures;
118
- }
119
-
120
- /**
121
- * These are to be destroyed at the end of the context's lifetime.
122
- * Need to be released at the end of the lifetime.
123
- * @type {GPUBuffer[]}
124
- */
125
- #transient_buffers = [];
126
-
127
- /**
128
- *
129
- * @type {GPUBuffer[]}
130
- */
131
- #staging_buffers = [];
132
-
133
- /**
134
- * Once finished, the encoder becomes inactive, we use this flag to track potential misuse and inform the user
135
- * @type {boolean}
136
- */
137
- #finished = false;
138
-
139
- /**
140
- * @readonly
141
- * @type {Signal<this>}
142
- */
143
- onFinished = new Signal();
144
-
145
- /**
146
- * @readonly
147
- * @type {Signal<this>}
148
- */
149
- onBeforeFinish = new Signal();
150
-
151
- /**
152
- *
153
- * @return {Promise<void>}
154
- */
155
- get done() {
156
- if (this.#finished) {
157
- return Promise.resolve();
158
- }
159
-
160
- return new Promise(resolve => {
161
- this.onFinished.addOne(resolve);
162
-
163
- if (this.#finished) {
164
- // special case, double-checking to avoid race conditions
165
- resolve();
166
- }
167
-
168
- });
169
- }
170
-
171
- /**
172
- * Do not call directly, always use {@link create}
173
- * @private
174
- */
175
- constructor() {
176
- // private, do not call from the outside
177
- }
178
-
179
- /**
180
- *
181
- * @param {GraphicsContext} graphics
182
- * @param {string} [label]
183
- * @returns {ShadeGPUCommandContext}
184
- */
185
- static create(
186
- graphics,
187
- label = ""
188
- ) {
189
-
190
- assert.defined(graphics, 'graphics');
191
- assert.equal(graphics.isGraphicsContext, true, "graphics.isGraphicsContext !== true");
192
-
193
- assert.isString(label, 'label');
194
-
195
- const r = new ShadeGPUCommandContext();
196
-
197
- r.#graphics = graphics;
198
- r.#gpu_encoder = graphics.device.createCommandEncoder({
199
- label
200
- });
201
-
202
- // track how many we have open
203
- debug_open_context_count++;
204
- array_push_if_unique(debug_open_contexts, r);
205
-
206
- if (debug_open_context_count > 1024) {
207
- warn_limited(`Too many open GPU contexts`, 20);
208
- }
209
-
210
- return r;
211
-
212
- }
213
-
214
- /**
215
- *
216
- * @returns {GraphicsExecutionContext}
217
- */
218
- createFrameGraphContext() {
219
- return new GraphicsExecutionContext(this.#graphics, this);
220
- }
221
-
222
- /**
223
- *
224
- * @param {FrameGraph} graph
225
- */
226
- encodeGraph(graph) {
227
- assert.defined(graph, 'graph');
228
- assert.isObject(graph, 'graph');
229
- assert.equal(graph.isFrameGraph, true, "graph.isFrameGraph !== true");
230
-
231
- // TODO check if the graph is empty, if so, we can skip the whole thing
232
-
233
- const ctx = this.createFrameGraphContext();
234
-
235
- graph.compile();
236
- graph.execute(ctx);
237
- }
238
-
239
- /**
240
- * @see https://www.w3.org/TR/webgpu/#dom-gpucommandencoder-clearbuffer
241
- * @param {GPUBuffer} buffer
242
- * @param {number} [offset=0]
243
- * @param {number} [size]
244
- */
245
- clearBuffer(buffer, offset = 0, size) {
246
- assert.defined(buffer, 'buffer');
247
- assert.equal(buffer.usage & GPUBufferUsage.COPY_DST, GPUBufferUsage.COPY_DST, 'buffer must include COPY_DST usage flag');
248
- assert.isNonNegativeInteger(offset, 'offset');
249
- assert.equal(offset % 4, 0, 'offset must be a multiple of 4');
250
-
251
- // console.log(`cmd.clearBuffer(${buffer.label}, ${offset}, ${size})`);
252
-
253
- this.#gpu_encoder.clearBuffer(buffer, offset, size);
254
- }
255
-
256
- /**
257
- * Not supported, only here to offer documentation hint
258
- * see {@link texture_fill_rectangle}
259
- * @throws {Error} Always, documentation hint only
260
- */
261
- clearTexture() {
262
- throw new Error(`Documentation hint only, use texture_fill_rectangle instead`);
263
- }
264
-
265
- /**
266
- * NOTE: only the explicit verbose form of the API is supported, shorthand such as `copyBufferToBuffer(source, destination, size)` are not supported.
267
- * see https://www.w3.org/TR/webgpu/#gpucommandencoder-copybuffertobuffer
268
- * @see GPUCommandEncoder.copyBufferToBuffer
269
- * @param {GPUBuffer} source The GPUBuffer to copy from.
270
- * @param {number} sourceOffset Offset in bytes into the source to begin copying from.
271
- * @param {GPUBuffer} destination The GPUBuffer to copy to.
272
- * @param {number} destinationOffset Offset in bytes into destination to place the copied data.
273
- * @param {number} [size] Bytes to copy. In not specified, defaults to `source.size - sourceOffset`
274
- */
275
- copyBufferToBuffer(
276
- source,
277
- sourceOffset,
278
- destination,
279
- destinationOffset,
280
- size
281
- ) {
282
-
283
- assert.notEqual(source.usage & GPUBufferUsage.COPY_SRC, 0, 'source must include COPY_SRC usage flag');
284
- assert.isNonNegativeInteger(sourceOffset, 'sourceOffset');
285
- assert.equal(sourceOffset % 4, 0, 'sourceOffset must be a multiple of 4');
286
-
287
- assert.notEqual(destination.usage & GPUBufferUsage.COPY_DST, 0, 'destination must include COPY_DST usage flag');
288
- assert.isNonNegativeInteger(destinationOffset, 'destinationOffset');
289
- assert.equal(destinationOffset % 4, 0, 'destinationOffset must be a multiple of 4');
290
-
291
- if (size !== undefined) {
292
- assert.isNonNegativeInteger(size, 'size');
293
- assert.equal(size % 4, 0, 'size must be a multiple of 4');
294
-
295
- assert.greaterThanOrEqual(source.size, sourceOffset + size, 'source underflow');
296
- assert.greaterThanOrEqual(destination.size, destinationOffset + size, 'destination underflow');
297
- }
298
-
299
- assert.notEqual(source, destination, 'source and destination must be different');
300
-
301
- // console.log(`cmd.copyBufferToBuffer("${source.label}", ${sourceOffset}, "${destination.label}", ${destinationOffset}, ${size})`);
302
-
303
- this.#gpu_encoder.copyBufferToBuffer(
304
- source,
305
- sourceOffset,
306
- destination,
307
- destinationOffset,
308
- size
309
- );
310
- }
311
-
312
-
313
- /**
314
- *
315
- * @param {GPUImageCopyTexture} source
316
- * @param {GPUImageCopyTexture} destination
317
- * @param {GPUExtent3DStrict} copySize
318
- */
319
- copyTextureToTexture(source,
320
- destination,
321
- copySize
322
- ) {
323
- this.#gpu_encoder.copyTextureToTexture(source, destination, copySize);
324
- }
325
-
326
- /**
327
- *
328
- * @param {GPUComputePassDescriptor} [descriptor]
329
- * @returns {GPUComputePassEncoder}
330
- */
331
- beginComputePass(descriptor) {
332
- const encoder = this.#gpu_encoder;
333
-
334
- let _descriptor = descriptor;
335
-
336
- const timers = this.#debug_timers;
337
- const graphics = this.#graphics;
338
-
339
- if (
340
- timers !== undefined
341
- && graphics.device.features.has('timestamp-query')
342
- ) {
343
-
344
- _descriptor.timestampWrites = timers.getComputeWrites(descriptor.label);
345
-
346
- }
347
-
348
- // console.log(`cmd.beginComputePass(${descriptor.label})`);
349
-
350
- return encoder.beginComputePass(_descriptor)
351
- }
352
-
353
- /**
354
- *
355
- * @param {ComputePipelineDescriptor} pipeline
356
- * @param {string} [label]
357
- * @param {GPUBindingResource[][]} [bindings]
358
- * @returns {GPUComputePassEncoder}
359
- */
360
- constructComputePass({
361
- pipeline,
362
- label,
363
- bindings = []
364
- }) {
365
-
366
- const pass_descriptor = {
367
- label
368
- };
369
-
370
- if (label === undefined) {
371
- pass_descriptor.label = pipeline.label;
372
- }
373
-
374
- const graphics = this.#graphics;
375
-
376
- const pass = this.beginComputePass(pass_descriptor);
377
-
378
- const gpu_pipeline = graphics.compute_pipelines.obtain(pipeline);
379
-
380
- pass.setPipeline(gpu_pipeline);
381
-
382
- graphics.setPipelineBindings(pass, pipeline, bindings);
383
-
384
- // console.warn(`cmd.constructComputePass(${pass_descriptor.label})`)
385
-
386
- return pass;
387
- }
388
-
389
- /**
390
- * Begins a labeled debug group containing subsequent commands.
391
- * @see https://gpuweb.github.io/gpuweb/#debug-markers
392
- * @param {string} label
393
- */
394
- pushDebugGroup(label) {
395
- assert.isString(label, 'label');
396
- this.#gpu_encoder.pushDebugGroup(label);
397
-
398
- // console.group("cmd/debug_group=" + label)
399
- }
400
-
401
- /**
402
- * Ends the labeled debug group most recently started by pushDebugGroup().
403
- * @see https://gpuweb.github.io/gpuweb/#debug-markers
404
- */
405
- popDebugGroup() {
406
- this.#gpu_encoder.popDebugGroup();
407
-
408
- // console.groupEnd();
409
- }
410
-
411
- /**
412
- * Marks a point in a stream of commands with a label.
413
- * @see https://gpuweb.github.io/gpuweb/#debug-markers
414
- * @param {string} label
415
- */
416
- insertDebugMarker(label) {
417
- assert.isString(label, 'label');
418
- this.#gpu_encoder.insertDebugMarker(label);
419
- }
420
-
421
- /**
422
- *
423
- * @param {string} [label]
424
- * @param {RenderPipelineDescriptor} pipeline
425
- * @param {GPUBindingResource[][]} [bindings]
426
- * @param {GPURenderPassColorAttachment[]} colorAttachments
427
- * @param {GPURenderPassDepthStencilAttachment} [depthStencilAttachment]
428
- * @returns {GPURenderPassEncoder}
429
- */
430
- constructRenderPass({
431
- label,
432
- pipeline,
433
- bindings,
434
- colorAttachments,
435
- depthStencilAttachment,
436
- }) {
437
- assert.defined(pipeline, 'pipeline');
438
- assert.equal(pipeline.isRenderPipelineDescriptor, true, "pipeline.isRenderPipelineDescriptor !== true");
439
-
440
- const pass_descriptor = {
441
- label,
442
- colorAttachments,
443
- depthStencilAttachment
444
- };
445
-
446
- if (label === undefined) {
447
- pass_descriptor.label = pipeline.label;
448
- }
449
-
450
- const graphics = this.#graphics;
451
-
452
- // TODO rewrite the pipeline if necessary to achieve compatibility with colorAttachment formats
453
- const gpu_pipeline = graphics.render_pipelines.obtain(pipeline);
454
-
455
- const pass = this.beginRenderPass(pass_descriptor);
456
-
457
- pass.setPipeline(gpu_pipeline);
458
-
459
- graphics.setPipelineBindings(pass, pipeline, bindings);
460
-
461
- // console.warn(`cmd.constructRenderPass(${pass_descriptor.label})`)
462
-
463
- return pass;
464
- }
465
-
466
- /**
467
- *
468
- * @param {GPURenderPassDescriptor} descriptor
469
- * @returns {GPURenderPassEncoder}
470
- */
471
- beginRenderPass(descriptor) {
472
-
473
- let _descriptor = descriptor;
474
-
475
- const timers = this.#debug_timers;
476
- const graphics = this.#graphics;
477
-
478
- if (
479
- timers !== undefined
480
- && graphics.device.features.has('timestamp-query')
481
- ) {
482
-
483
- _descriptor.timestampWrites = timers.getRenderWrites(descriptor.label);
484
-
485
- }
486
-
487
- // console.log(`cmd.beginRenderPass(${descriptor.label})`);
488
-
489
- return this.#gpu_encoder.beginRenderPass(_descriptor)
490
- }
491
-
492
- /**
493
- *
494
- * @param {GPUQuerySet} querySet
495
- * @param {number} firstQuery
496
- * @param {number} queryCount
497
- * @param {GPUBuffer} destination
498
- * @param {number} destinationOffset
499
- */
500
- resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset) {
501
- this.#gpu_encoder.resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset);
502
- }
503
-
504
- /**
505
- * Transient buffers are automatically released at the end of the context's lifetime;
506
- * however, we can explicitly release it earlier to allow for reuse.
507
- *
508
- * @param {GPUBuffer} buffer
509
- * @return {boolean} true if buffer was released, false if it was not found
510
- */
511
- releaseTransientBuffer(buffer) {
512
- assert.defined(buffer, 'buffer');
513
-
514
- if (!array_remove_first(this.#transient_buffers, buffer)) {
515
- return false;
516
- }
517
-
518
- const allocator = this.#transient_allocator;
519
-
520
- return allocator.release(buffer);
521
- }
522
-
523
- /**
524
- *
525
- * @return {GPUBufferAllocator}
526
- */
527
- get #transient_allocator() {
528
- return this.#graphics.buffer_allocator_main;
529
- }
530
-
531
- /**
532
- * Allocated buffer will only be valid until the context is finished.
533
- *
534
- * @param {number|GPUBufferUsage} usage
535
- * @param {number} size
536
- * @returns {GPUBuffer} managed buffer
537
- */
538
- allocateTransientBuffer(
539
- usage = GPUBufferUsage.UNIFORM,
540
- size
541
- ) {
542
- assert.isNonNegativeInteger(size, 'size');
543
- assert.isInteger(usage, 'usage');
544
-
545
- assert.notOk(this.#finished, 'already finished');
546
-
547
- scratch_buffer_descriptor.size = size;
548
- // COPY_DST is needed to write the data
549
- scratch_buffer_descriptor.usage = usage | GPUBufferUsage.COPY_DST;
550
-
551
- const allocator = this.#transient_allocator;
552
- const buffer = allocator.get(scratch_buffer_descriptor, this);
553
-
554
- // remember so we can clean up at the end
555
- this.#transient_buffers.push(buffer);
556
-
557
- return buffer;
558
- }
559
-
560
- /**
561
- *
562
- * @param {ArrayBuffer} data CPU-side data to place into the GPU buffer
563
- * @param {number|GPUBufferUsage} [usage=GPUBufferUsage.UNIFORM]
564
- * @param {number} [offset] offset into source data where to start copying from
565
- * @param {number} [size] in bytes
566
- * @returns {GPUBuffer} managed buffer, will be automatically destroyed when context is finished
567
- */
568
- allocateTransientBufferAndLoad(
569
- data,
570
- usage = GPUBufferUsage.UNIFORM,
571
- offset = 0,
572
- size = data.byteLength
573
- ) {
574
- assert.isInstanceOf(data, ArrayBuffer, 'data');
575
- assert.isInteger(usage, 'usage');
576
- assert.isNonNegativeInteger(offset, 'offset');
577
- assert.isNonNegativeInteger(size, 'size');
578
-
579
- assert.notOk(this.#finished, 'already finished');
580
-
581
- const buffer = this.allocateTransientBuffer(
582
- usage,
583
- size
584
- );
585
-
586
- this.writeBuffer(buffer, 0, data, offset, size);
587
-
588
- return buffer;
589
- }
590
-
591
- /**
592
- * @template T
593
- * @param {WebGPUType} type
594
- * @param {T} value
595
- * @param {number|GPUBufferUsage} [usage]
596
- * @return {GPUBuffer}
597
- */
598
- allocateTransientValueBuffer(
599
- type,
600
- value,
601
- usage
602
- ) {
603
- assert.defined(type, 'type');
604
- assert.notNull(type, 'type');
605
-
606
- const buffer = this.allocateTransientBuffer(usage, type.aligned_size);
607
-
608
- this.writeValueBuffer(buffer, 0, type, value);
609
-
610
- return buffer;
611
- }
612
-
613
- /**
614
- * @template T
615
- * @param {GPUBuffer} buffer
616
- * @param {number} buffer_offset
617
- * @param {WebGPUType} type
618
- * @param {T} value
619
- */
620
- writeValueBuffer(buffer, buffer_offset, type, value) {
621
-
622
- assert.notOk(this.#finished, 'already finished');
623
-
624
- const gfx = this.#graphics;
625
-
626
- const size = type.aligned_size;
627
-
628
- const stage = gfx.buffer_allocator_staging.get(size);
629
-
630
- const mapped = stage.getMappedRange(0, size);
631
-
632
- write_gpu_typed_buffer(value, type, mapped, 0);
633
-
634
- stage.unmap();
635
-
636
- this.#staging_buffers.push(stage);
637
-
638
- this.copyBufferToBuffer(stage, 0, buffer, buffer_offset, size);
639
- }
640
-
641
- /**
642
- * Very similar to {@link GPUQueue.writeBuffer}, but instead of doing the operation direction on the queue - uses a staging buffer under the hood and performs the actual write operation as part of the commmand buffer
643
- * @param {GPUBuffer} buffer
644
- * @param {number} buffer_offset
645
- * @param {ArrayBuffer} data
646
- * @param {number} data_offset
647
- * @param {number} size
648
- */
649
- writeBuffer(buffer, buffer_offset, data, data_offset, size) {
650
- assert.defined(buffer, 'buffer');
651
- assert.isNonNegativeInteger(buffer_offset, 'buffer_offset');
652
- assert.defined(data, 'data');
653
- assert.isNonNegativeInteger(data_offset, 'data_offset');
654
- assert.isNonNegativeInteger(size, 'size');
655
-
656
- const gfx = this.#graphics;
657
-
658
- const stage = gfx.buffer_allocator_staging.get(size);
659
-
660
- const mapped = stage.getMappedRange(0, size);
661
-
662
- array_buffer_copy(data, data_offset, mapped, 0, size);
663
-
664
- stage.unmap();
665
-
666
- this.#staging_buffers.push(stage);
667
-
668
- this.copyBufferToBuffer(stage, 0, buffer, buffer_offset, size);
669
-
670
- }
671
-
672
- #cleanup() {
673
-
674
- // console.warn('cmd.cleanup()');
675
-
676
- const transient_buffers = this.#transient_buffers;
677
- const transient_buffer_count = transient_buffers.length;
678
-
679
- const buffer_allocator_transient = this.#transient_allocator;
680
-
681
- // release in reverse order to make it more likely that we'd end up in the same acquisition order next time, which is desirable for caching
682
- for (let i = transient_buffer_count - 1; i >= 0; i--) {
683
- const buffer = transient_buffers[i];
684
-
685
- buffer_allocator_transient.release(buffer);
686
-
687
- }
688
-
689
- // staging buffers
690
- const buffer_allocator_staging = this.#graphics.buffer_allocator_staging;
691
- for (const staging of this.#staging_buffers) {
692
- buffer_allocator_staging.release(staging);
693
- }
694
- }
695
-
696
- /**
697
- * Performs both {@link GPUCommandEncoder.finish} and flushed command buffer
698
- */
699
- finish() {
700
- if (this.#finished) {
701
- console.warn('Context already finished');
702
- return;
703
- }
704
-
705
- this.onBeforeFinish.send1(this);
706
-
707
- debug_open_context_count--;
708
- array_remove_first(debug_open_contexts, this);
709
-
710
- const encoder = this.#gpu_encoder;
711
-
712
- const debug_timers = this.#debug_timers;
713
- if (debug_timers !== undefined) {
714
- debug_timers.resolve(encoder);
715
- }
716
-
717
- this.#finished = true;
718
-
719
- // release encoder reference
720
- this.#gpu_encoder = undefined;
721
-
722
- const command_buffer = encoder.finish();
723
-
724
- const graphics = this.#graphics;
725
- const queue = graphics.device.queue;
726
-
727
- queue.submit([command_buffer]);
728
-
729
- // cleanup allocated memory
730
- this.#cleanup();
731
-
732
- if (debug_timers !== undefined) {
733
- debug_timers
734
- .download_results()
735
- .then(() => {
736
- const table = debug_timers.results_to_console_table();
737
-
738
- this.#debug_timers_callback(table);
739
- })
740
- .finally(() => {
741
- debug_timers.destroy();
742
- });
743
-
744
- this.#debug_timers = undefined;
745
- }
746
-
747
- this.onFinished.send1(this);
748
-
749
- }
1
+ import { assert } from "../../core/assert.js";
2
+ import { array_push_if_unique } from "../../core/collection/array/array_push_if_unique.js";
3
+ import { array_remove_first } from "../../core/collection/array/array_remove_first.js";
4
+ import { array_buffer_copy } from "../../core/collection/array/typed/array_buffer_copy.js";
5
+ import Signal from "../../core/events/signal/Signal.js";
6
+ import { noop } from "../../core/function/noop.js";
7
+ import { GPUTimerArray } from "./timing/GPUTimerArray.js";
8
+ import { make_profiling_pass_encoder } from "./timing/profile/make_profiling_pass_encoder.js";
9
+ import { parse_workgroup_size } from "./timing/profile/parse_workgroup_size.js";
10
+ import { warn_limited } from "../util/warn_limited.js";
11
+ import { write_gpu_typed_buffer } from "../renderer/shader/type/serde/write_gpu_typed_buffer.js";
12
+ import { BufferResourceDescriptor } from "./graph/BufferResourceDescriptor.js";
13
+ import { GraphicsExecutionContext } from "./graph/GraphicsExecutionContext.js";
14
+
15
+ /**
16
+ * Keep track of how many contexts are currently open (unfinished)
17
+ * Important because contexts can have transient resources allocated, and those are not cleaned up until the context is `finished`
18
+ * @type {number}
19
+ */
20
+ let debug_open_context_count = 0;
21
+
22
+ /**
23
+ *
24
+ * @type {ShadeGPUCommandContext[]}
25
+ */
26
+ const debug_open_contexts = [];
27
+
28
+ const scratch_buffer_descriptor = new BufferResourceDescriptor();
29
+
30
+ let id_counter = 0;
31
+
32
+ /**
33
+ * Abstraction on top of {@link GPUCommandEncoder}.
34
+ *
35
+ * Inspired by Intel's "MiniEngine" CommandContext
36
+ * @see https://github.com/GameTechDev/TAA/blob/39786709cf70a1e0906196c600f6079571a33ceb/MiniEngine/Core/CommandContext.h
37
+ * @see https://www.w3.org/TR/webgpu/#gpucommandencoder
38
+ */
39
+ export class ShadeGPUCommandContext {
40
+
41
+ #id = id_counter++;
42
+
43
+ /**
44
+ * Unique identifier for this context, useful for debugging
45
+ *
46
+ * @return {number}
47
+ */
48
+ get id() {
49
+ return this.#id;
50
+ }
51
+
52
+ /**
53
+ *
54
+ * @returns {boolean}
55
+ */
56
+ get isGPUCommandContext() {
57
+ return true;
58
+ }
59
+
60
+
61
+ /**
62
+ * Actual underlying encoder
63
+ * @type {GPUCommandEncoder}
64
+ */
65
+ #gpu_encoder
66
+
67
+ /**
68
+ * Don't circumvent {@link ShadeGPUCommandContext} unless you know what you're doing.
69
+ * {@link ShadeGPUCommandContext} is a wrapper around {@link GPUCommandEncoder} that provides additional spec-compliance asserts to help find errors sooner.
70
+ * @returns {GPUCommandEncoder}
71
+ */
72
+ get gpu_encoder() {
73
+ return this.#gpu_encoder;
74
+ }
75
+
76
+ /**
77
+ * @type {GPUTimerArray|undefined}
78
+ */
79
+ #debug_timers;
80
+
81
+ /**
82
+ *
83
+ * @type {function}
84
+ */
85
+ #debug_timers_callback = noop;
86
+
87
+ /**
88
+ *
89
+ * @param {function} debug_timers_callback
90
+ */
91
+ enable_debug_timers(debug_timers_callback) {
92
+ if (this.#debug_timers !== undefined) {
93
+ // already enabled
94
+ return;
95
+ }
96
+
97
+ this.#debug_timers = new GPUTimerArray(this.device);
98
+ this.#debug_timers_callback = debug_timers_callback;
99
+ }
100
+
101
+ /**
102
+ * The frame recorder collecting this context's spans, or null.
103
+ *
104
+ * @type {GPUFrameRecorder|null}
105
+ */
106
+ #profile_recorder = null;
107
+
108
+ /**
109
+ * Which query set this context is, within its frame. Carried into every span it records —
110
+ * timestamps from two query sets are not specified to share a clock domain, and a frame has
111
+ * one set per context.
112
+ *
113
+ * @type {number}
114
+ */
115
+ #profile_query_set_id = 0;
116
+
117
+ /**
118
+ * The work record for the pass currently being opened.
119
+ *
120
+ * Set by {@link #with_timestamp_writes} and consumed immediately by the `begin*Pass` that asked
121
+ * for it — the two are one operation, split only because the descriptor has to be built before
122
+ * the encoder exists.
123
+ *
124
+ * @type {GPUProfileWork|null}
125
+ */
126
+ #pending_work = null;
127
+
128
+ /**
129
+ * Wrap `encoder` so its dispatches and draws are recorded, when a capture asked for them.
130
+ *
131
+ * @param {GPUComputePassEncoder|GPURenderPassEncoder} encoder
132
+ * @returns {GPUComputePassEncoder|GPURenderPassEncoder}
133
+ */
134
+ #instrument_pass(encoder) {
135
+ const work = this.#pending_work;
136
+
137
+ if (work === null) {
138
+ this.#last_work = null;
139
+
140
+ return encoder;
141
+ }
142
+
143
+ this.#pending_work = null;
144
+ this.#last_work = work;
145
+
146
+ return make_profiling_pass_encoder(encoder, work);
147
+ }
148
+
149
+ /**
150
+ * Resolves once this context's timings have reached its recorder.
151
+ *
152
+ * @type {Promise<void>}
153
+ */
154
+ #profiling_absorbed = Promise.resolve();
155
+
156
+ /**
157
+ * @type {function}
158
+ */
159
+ #profiling_absorbed_resolve = noop;
160
+
161
+ /**
162
+ * Resolves after this context's timings have been read back and handed to its recorder.
163
+ *
164
+ * **Not the same moment as {@link done}.** `done` resolves from `onFinished`, which fires
165
+ * synchronously at submit — before any timestamp has been read. A caller that closes its frame
166
+ * on `done` commits an empty one, every time. Already-resolved when nothing is profiling.
167
+ *
168
+ * @returns {Promise<void>}
169
+ */
170
+ get profiling_absorbed() {
171
+ return this.#profiling_absorbed;
172
+ }
173
+
174
+ /**
175
+ * Record this context's pass timings into `recorder`.
176
+ *
177
+ * Independent of {@link enable_debug_timers}, which is the console-shaped one-off; both share
178
+ * the underlying {@link GPUTimerArray} and either may enable it.
179
+ *
180
+ * @param {GPUFrameRecorder} recorder
181
+ * @param {number} query_set_id this context's index within the frame
182
+ * @returns {void}
183
+ */
184
+ enable_profiling(recorder, query_set_id) {
185
+ assert.defined(recorder, 'recorder');
186
+ assert.isNonNegativeInteger(query_set_id, 'query_set_id');
187
+ assert.notOk(this.#finished, 'already finished');
188
+
189
+ if (this.#debug_timers === undefined) {
190
+ this.#debug_timers = new GPUTimerArray(this.device);
191
+ }
192
+
193
+ this.#profile_recorder = recorder;
194
+ this.#profile_query_set_id = query_set_id;
195
+
196
+ this.#profiling_absorbed = new Promise(resolve => {
197
+ this.#profiling_absorbed_resolve = resolve;
198
+ });
199
+ }
200
+
201
+ /**
202
+ * @type {GraphicsContext}
203
+ */
204
+ #graphics
205
+
206
+ /**
207
+ *
208
+ * @return {GPUDevice}
209
+ */
210
+ get device() {
211
+ return this.#graphics.device;
212
+ }
213
+
214
+ /**
215
+ *
216
+ * @return {TextureManager}
217
+ */
218
+ get textures() {
219
+ return this.#graphics.textures;
220
+ }
221
+
222
+ /**
223
+ * These are to be destroyed at the end of the context's lifetime.
224
+ * Need to be released at the end of the lifetime.
225
+ * @type {GPUBuffer[]}
226
+ */
227
+ #transient_buffers = [];
228
+
229
+ /**
230
+ *
231
+ * @type {GPUBuffer[]}
232
+ */
233
+ #staging_buffers = [];
234
+
235
+ /**
236
+ * Once finished, the encoder becomes inactive, we use this flag to track potential misuse and inform the user
237
+ * @type {boolean}
238
+ */
239
+ #finished = false;
240
+
241
+ /**
242
+ * @readonly
243
+ * @type {Signal<this>}
244
+ */
245
+ onFinished = new Signal();
246
+
247
+ /**
248
+ * @readonly
249
+ * @type {Signal<this>}
250
+ */
251
+ onBeforeFinish = new Signal();
252
+
253
+ /**
254
+ *
255
+ * @return {Promise<void>}
256
+ */
257
+ get done() {
258
+ if (this.#finished) {
259
+ return Promise.resolve();
260
+ }
261
+
262
+ return new Promise(resolve => {
263
+ this.onFinished.addOne(resolve);
264
+
265
+ if (this.#finished) {
266
+ // special case, double-checking to avoid race conditions
267
+ resolve();
268
+ }
269
+
270
+ });
271
+ }
272
+
273
+ /**
274
+ * Do not call directly, always use {@link create}
275
+ * @private
276
+ */
277
+ constructor() {
278
+ // private, do not call from the outside
279
+ }
280
+
281
+ /**
282
+ *
283
+ * @param {GraphicsContext} graphics
284
+ * @param {string} [label]
285
+ * @returns {ShadeGPUCommandContext}
286
+ */
287
+ static create(
288
+ graphics,
289
+ label = ""
290
+ ) {
291
+
292
+ assert.defined(graphics, 'graphics');
293
+ assert.equal(graphics.isGraphicsContext, true, "graphics.isGraphicsContext !== true");
294
+
295
+ assert.isString(label, 'label');
296
+
297
+ const r = new ShadeGPUCommandContext();
298
+
299
+ r.#graphics = graphics;
300
+ r.#gpu_encoder = graphics.device.createCommandEncoder({
301
+ label
302
+ });
303
+
304
+ // track how many we have open
305
+ debug_open_context_count++;
306
+ array_push_if_unique(debug_open_contexts, r);
307
+
308
+ if (debug_open_context_count > 1024) {
309
+ warn_limited(`Too many open GPU contexts`, 20);
310
+ }
311
+
312
+ return r;
313
+
314
+ }
315
+
316
+ /**
317
+ *
318
+ * @returns {GraphicsExecutionContext}
319
+ */
320
+ createFrameGraphContext() {
321
+ return new GraphicsExecutionContext(this.#graphics, this);
322
+ }
323
+
324
+ /**
325
+ *
326
+ * @param {FrameGraph} graph
327
+ */
328
+ encodeGraph(graph) {
329
+ assert.defined(graph, 'graph');
330
+ assert.isObject(graph, 'graph');
331
+ assert.equal(graph.isFrameGraph, true, "graph.isFrameGraph !== true");
332
+
333
+ // TODO check if the graph is empty, if so, we can skip the whole thing
334
+
335
+ const ctx = this.createFrameGraphContext();
336
+
337
+ graph.compile();
338
+ graph.execute(ctx);
339
+ }
340
+
341
+ /**
342
+ * @see https://www.w3.org/TR/webgpu/#dom-gpucommandencoder-clearbuffer
343
+ * @param {GPUBuffer} buffer
344
+ * @param {number} [offset=0]
345
+ * @param {number} [size]
346
+ */
347
+ clearBuffer(buffer, offset = 0, size) {
348
+ assert.defined(buffer, 'buffer');
349
+ assert.equal(buffer.usage & GPUBufferUsage.COPY_DST, GPUBufferUsage.COPY_DST, 'buffer must include COPY_DST usage flag');
350
+ assert.isNonNegativeInteger(offset, 'offset');
351
+ assert.equal(offset % 4, 0, 'offset must be a multiple of 4');
352
+
353
+ // console.log(`cmd.clearBuffer(${buffer.label}, ${offset}, ${size})`);
354
+
355
+ this.#gpu_encoder.clearBuffer(buffer, offset, size);
356
+ }
357
+
358
+ /**
359
+ * Not supported, only here to offer documentation hint
360
+ * see {@link texture_fill_rectangle}
361
+ * @throws {Error} Always, documentation hint only
362
+ */
363
+ clearTexture() {
364
+ throw new Error(`Documentation hint only, use texture_fill_rectangle instead`);
365
+ }
366
+
367
+ /**
368
+ * NOTE: only the explicit verbose form of the API is supported, shorthand such as `copyBufferToBuffer(source, destination, size)` are not supported.
369
+ * see https://www.w3.org/TR/webgpu/#gpucommandencoder-copybuffertobuffer
370
+ * @see GPUCommandEncoder.copyBufferToBuffer
371
+ * @param {GPUBuffer} source The GPUBuffer to copy from.
372
+ * @param {number} sourceOffset Offset in bytes into the source to begin copying from.
373
+ * @param {GPUBuffer} destination The GPUBuffer to copy to.
374
+ * @param {number} destinationOffset Offset in bytes into destination to place the copied data.
375
+ * @param {number} [size] Bytes to copy. In not specified, defaults to `source.size - sourceOffset`
376
+ */
377
+ copyBufferToBuffer(
378
+ source,
379
+ sourceOffset,
380
+ destination,
381
+ destinationOffset,
382
+ size
383
+ ) {
384
+
385
+ assert.notEqual(source.usage & GPUBufferUsage.COPY_SRC, 0, 'source must include COPY_SRC usage flag');
386
+ assert.isNonNegativeInteger(sourceOffset, 'sourceOffset');
387
+ assert.equal(sourceOffset % 4, 0, 'sourceOffset must be a multiple of 4');
388
+
389
+ assert.notEqual(destination.usage & GPUBufferUsage.COPY_DST, 0, 'destination must include COPY_DST usage flag');
390
+ assert.isNonNegativeInteger(destinationOffset, 'destinationOffset');
391
+ assert.equal(destinationOffset % 4, 0, 'destinationOffset must be a multiple of 4');
392
+
393
+ if (size !== undefined) {
394
+ assert.isNonNegativeInteger(size, 'size');
395
+ assert.equal(size % 4, 0, 'size must be a multiple of 4');
396
+
397
+ assert.greaterThanOrEqual(source.size, sourceOffset + size, 'source underflow');
398
+ assert.greaterThanOrEqual(destination.size, destinationOffset + size, 'destination underflow');
399
+ }
400
+
401
+ assert.notEqual(source, destination, 'source and destination must be different');
402
+
403
+ // console.log(`cmd.copyBufferToBuffer("${source.label}", ${sourceOffset}, "${destination.label}", ${destinationOffset}, ${size})`);
404
+
405
+ this.#gpu_encoder.copyBufferToBuffer(
406
+ source,
407
+ sourceOffset,
408
+ destination,
409
+ destinationOffset,
410
+ size
411
+ );
412
+ }
413
+
414
+
415
+ /**
416
+ *
417
+ * @param {GPUImageCopyTexture} source
418
+ * @param {GPUImageCopyTexture} destination
419
+ * @param {GPUExtent3DStrict} copySize
420
+ */
421
+ copyTextureToTexture(source,
422
+ destination,
423
+ copySize
424
+ ) {
425
+ this.#gpu_encoder.copyTextureToTexture(source, destination, copySize);
426
+ }
427
+
428
+ /**
429
+ *
430
+ * @param {GPUComputePassDescriptor} [descriptor]
431
+ * @returns {GPUComputePassEncoder}
432
+ */
433
+ beginComputePass(descriptor) {
434
+ const encoder = this.#gpu_encoder;
435
+
436
+ const _descriptor = this.#with_timestamp_writes(descriptor, 'compute');
437
+
438
+ // console.log(`cmd.beginComputePass(${descriptor.label})`);
439
+
440
+ return this.#instrument_pass(encoder.beginComputePass(_descriptor))
441
+ }
442
+
443
+ /**
444
+ * The descriptor a pass should actually be begun with, once timing has had its say.
445
+ *
446
+ * Copies rather than writing through. Several call sites hand in a descriptor they reuse across
447
+ * passes, and stamping `timestampWrites` onto it left the next pass carrying the previous
448
+ * pass's query indices. The copy is one shallow spread per pass and only when timers are on.
449
+ *
450
+ * Returns the descriptor unchanged when timing is off, when the device withheld the feature, or
451
+ * when the timer array has no slot left ({@link GPUTimerArray#dropped_count}). An untimed pass
452
+ * is the correct outcome in all three; a pass carrying indices the query set does not have is a
453
+ * validation error.
454
+ *
455
+ * @param {GPUComputePassDescriptor|GPURenderPassDescriptor} [descriptor]
456
+ * @param {string} kind one of 'compute', 'render'
457
+ * @returns {GPUComputePassDescriptor|GPURenderPassDescriptor}
458
+ */
459
+ #with_timestamp_writes(descriptor, kind) {
460
+ const timers = this.#debug_timers;
461
+
462
+ if (timers === undefined) {
463
+ return descriptor;
464
+ }
465
+
466
+ if (!this.#graphics.device.features.has('timestamp-query')) {
467
+ return descriptor;
468
+ }
469
+
470
+ // `beginComputePass()` is legal with no descriptor at all; reading `.label` off nothing is
471
+ // not, and used to throw here the moment timers were switched on
472
+ const source = descriptor !== undefined ? descriptor : {};
473
+
474
+ const writes = kind === 'compute'
475
+ ? timers.getComputeWrites(source.label)
476
+ : timers.getRenderWrites(source.label);
477
+
478
+ if (writes === undefined) {
479
+ return descriptor;
480
+ }
481
+
482
+ const recorder = this.#profile_recorder;
483
+
484
+ if (recorder !== null) {
485
+ /*
486
+ The one moment the join can be made. The recorder knows which frame graph pass is
487
+ executing right now; the slot is what the timings will come back keyed by. Neither
488
+ fact is recoverable later — see GPUFrameRecorder.
489
+ */
490
+ this.#pending_work = recorder.note_pass_slot(
491
+ this.#profile_query_set_id,
492
+ writes.beginningOfPassWriteIndex / 2
493
+ );
494
+ } else {
495
+ this.#pending_work = null;
496
+ }
497
+
498
+ return {
499
+ ...source,
500
+ timestampWrites: writes
501
+ };
502
+ }
503
+
504
+ /**
505
+ *
506
+ * @param {ComputePipelineDescriptor} pipeline
507
+ * @param {string} [label]
508
+ * @param {GPUBindingResource[][]} [bindings]
509
+ * @returns {GPUComputePassEncoder}
510
+ */
511
+ constructComputePass({
512
+ pipeline,
513
+ label,
514
+ bindings = []
515
+ }) {
516
+
517
+ const pass_descriptor = {
518
+ label
519
+ };
520
+
521
+ if (label === undefined) {
522
+ pass_descriptor.label = pipeline.label;
523
+ }
524
+
525
+ const graphics = this.#graphics;
526
+
527
+ const pass = this.beginComputePass(pass_descriptor);
528
+
529
+ const gpu_pipeline = graphics.compute_pipelines.obtain(pipeline);
530
+
531
+ pass.setPipeline(gpu_pipeline);
532
+
533
+ this.#note_pipeline(pipeline, true);
534
+
535
+ graphics.setPipelineBindings(pass, pipeline, bindings);
536
+
537
+ // console.warn(`cmd.constructComputePass(${pass_descriptor.label})`)
538
+
539
+ return pass;
540
+ }
541
+
542
+ /**
543
+ * Record which pipeline a pass ran, and for a compute pipeline its workgroup size.
544
+ *
545
+ * Only reachable from `constructComputePass` / `constructRenderPass`, which is the only place a
546
+ * *descriptor* is in hand. `setPipeline` receives the compiled `GPUComputePipeline`, and the
547
+ * WGSL that carries `@workgroup_size` is not on it — so a pass begun the long way records its
548
+ * counts but not what ran them, which the model represents as an unknown rather than a zero.
549
+ *
550
+ * @param {ComputePipelineDescriptor|RenderPipelineDescriptor} pipeline
551
+ * @param {boolean} is_compute
552
+ * @returns {void}
553
+ */
554
+ #note_pipeline(pipeline, is_compute) {
555
+ const work = this.#last_work;
556
+
557
+ if (work === null) {
558
+ return;
559
+ }
560
+
561
+ work.pipeline = pipeline.label ?? "";
562
+
563
+ if (is_compute) {
564
+ work.workgroup_size = parse_workgroup_size(pipeline.compute?.module?.code);
565
+ }
566
+ }
567
+
568
+ /**
569
+ * The work record of the most recently opened pass, so `construct*Pass` can annotate it after
570
+ * the encoder has been made.
571
+ *
572
+ * @type {GPUProfileWork|null}
573
+ */
574
+ #last_work = null;
575
+
576
+ /**
577
+ * Begins a labeled debug group containing subsequent commands.
578
+ * @see https://gpuweb.github.io/gpuweb/#debug-markers
579
+ * @param {string} label
580
+ */
581
+ pushDebugGroup(label) {
582
+ assert.isString(label, 'label');
583
+ this.#gpu_encoder.pushDebugGroup(label);
584
+
585
+ // console.group("cmd/debug_group=" + label)
586
+ }
587
+
588
+ /**
589
+ * Ends the labeled debug group most recently started by pushDebugGroup().
590
+ * @see https://gpuweb.github.io/gpuweb/#debug-markers
591
+ */
592
+ popDebugGroup() {
593
+ this.#gpu_encoder.popDebugGroup();
594
+
595
+ // console.groupEnd();
596
+ }
597
+
598
+ /**
599
+ * Marks a point in a stream of commands with a label.
600
+ * @see https://gpuweb.github.io/gpuweb/#debug-markers
601
+ * @param {string} label
602
+ */
603
+ insertDebugMarker(label) {
604
+ assert.isString(label, 'label');
605
+ this.#gpu_encoder.insertDebugMarker(label);
606
+ }
607
+
608
+ /**
609
+ *
610
+ * @param {string} [label]
611
+ * @param {RenderPipelineDescriptor} pipeline
612
+ * @param {GPUBindingResource[][]} [bindings]
613
+ * @param {GPURenderPassColorAttachment[]} colorAttachments
614
+ * @param {GPURenderPassDepthStencilAttachment} [depthStencilAttachment]
615
+ * @returns {GPURenderPassEncoder}
616
+ */
617
+ constructRenderPass({
618
+ label,
619
+ pipeline,
620
+ bindings,
621
+ colorAttachments,
622
+ depthStencilAttachment,
623
+ }) {
624
+ assert.defined(pipeline, 'pipeline');
625
+ assert.equal(pipeline.isRenderPipelineDescriptor, true, "pipeline.isRenderPipelineDescriptor !== true");
626
+
627
+ const pass_descriptor = {
628
+ label,
629
+ colorAttachments,
630
+ depthStencilAttachment
631
+ };
632
+
633
+ if (label === undefined) {
634
+ pass_descriptor.label = pipeline.label;
635
+ }
636
+
637
+ const graphics = this.#graphics;
638
+
639
+ // TODO rewrite the pipeline if necessary to achieve compatibility with colorAttachment formats
640
+ const gpu_pipeline = graphics.render_pipelines.obtain(pipeline);
641
+
642
+ const pass = this.beginRenderPass(pass_descriptor);
643
+
644
+ pass.setPipeline(gpu_pipeline);
645
+
646
+ this.#note_pipeline(pipeline, false);
647
+
648
+ graphics.setPipelineBindings(pass, pipeline, bindings);
649
+
650
+ // console.warn(`cmd.constructRenderPass(${pass_descriptor.label})`)
651
+
652
+ return pass;
653
+ }
654
+
655
+ /**
656
+ *
657
+ * @param {GPURenderPassDescriptor} descriptor
658
+ * @returns {GPURenderPassEncoder}
659
+ */
660
+ beginRenderPass(descriptor) {
661
+
662
+ const _descriptor = this.#with_timestamp_writes(descriptor, 'render');
663
+
664
+ // console.log(`cmd.beginRenderPass(${descriptor.label})`);
665
+
666
+ return this.#instrument_pass(this.#gpu_encoder.beginRenderPass(_descriptor))
667
+ }
668
+
669
+ /**
670
+ *
671
+ * @param {GPUQuerySet} querySet
672
+ * @param {number} firstQuery
673
+ * @param {number} queryCount
674
+ * @param {GPUBuffer} destination
675
+ * @param {number} destinationOffset
676
+ */
677
+ resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset) {
678
+ this.#gpu_encoder.resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset);
679
+ }
680
+
681
+ /**
682
+ * Transient buffers are automatically released at the end of the context's lifetime;
683
+ * however, we can explicitly release it earlier to allow for reuse.
684
+ *
685
+ * @param {GPUBuffer} buffer
686
+ * @return {boolean} true if buffer was released, false if it was not found
687
+ */
688
+ releaseTransientBuffer(buffer) {
689
+ assert.defined(buffer, 'buffer');
690
+
691
+ if (!array_remove_first(this.#transient_buffers, buffer)) {
692
+ return false;
693
+ }
694
+
695
+ const allocator = this.#transient_allocator;
696
+
697
+ return allocator.release(buffer);
698
+ }
699
+
700
+ /**
701
+ *
702
+ * @return {GPUBufferAllocator}
703
+ */
704
+ get #transient_allocator() {
705
+ return this.#graphics.buffer_allocator_main;
706
+ }
707
+
708
+ /**
709
+ * Allocated buffer will only be valid until the context is finished.
710
+ *
711
+ * @param {number|GPUBufferUsage} usage
712
+ * @param {number} size
713
+ * @returns {GPUBuffer} managed buffer
714
+ */
715
+ allocateTransientBuffer(
716
+ usage = GPUBufferUsage.UNIFORM,
717
+ size
718
+ ) {
719
+ assert.isNonNegativeInteger(size, 'size');
720
+ assert.isInteger(usage, 'usage');
721
+
722
+ assert.notOk(this.#finished, 'already finished');
723
+
724
+ scratch_buffer_descriptor.size = size;
725
+ // COPY_DST is needed to write the data
726
+ scratch_buffer_descriptor.usage = usage | GPUBufferUsage.COPY_DST;
727
+
728
+ const allocator = this.#transient_allocator;
729
+ const buffer = allocator.get(scratch_buffer_descriptor, this);
730
+
731
+ // remember so we can clean up at the end
732
+ this.#transient_buffers.push(buffer);
733
+
734
+ return buffer;
735
+ }
736
+
737
+ /**
738
+ *
739
+ * @param {ArrayBuffer} data CPU-side data to place into the GPU buffer
740
+ * @param {number|GPUBufferUsage} [usage=GPUBufferUsage.UNIFORM]
741
+ * @param {number} [offset] offset into source data where to start copying from
742
+ * @param {number} [size] in bytes
743
+ * @returns {GPUBuffer} managed buffer, will be automatically destroyed when context is finished
744
+ */
745
+ allocateTransientBufferAndLoad(
746
+ data,
747
+ usage = GPUBufferUsage.UNIFORM,
748
+ offset = 0,
749
+ size = data.byteLength
750
+ ) {
751
+ assert.isInstanceOf(data, ArrayBuffer, 'data');
752
+ assert.isInteger(usage, 'usage');
753
+ assert.isNonNegativeInteger(offset, 'offset');
754
+ assert.isNonNegativeInteger(size, 'size');
755
+
756
+ assert.notOk(this.#finished, 'already finished');
757
+
758
+ const buffer = this.allocateTransientBuffer(
759
+ usage,
760
+ size
761
+ );
762
+
763
+ this.writeBuffer(buffer, 0, data, offset, size);
764
+
765
+ return buffer;
766
+ }
767
+
768
+ /**
769
+ * @template T
770
+ * @param {WebGPUType} type
771
+ * @param {T} value
772
+ * @param {number|GPUBufferUsage} [usage]
773
+ * @return {GPUBuffer}
774
+ */
775
+ allocateTransientValueBuffer(
776
+ type,
777
+ value,
778
+ usage
779
+ ) {
780
+ assert.defined(type, 'type');
781
+ assert.notNull(type, 'type');
782
+
783
+ const buffer = this.allocateTransientBuffer(usage, type.aligned_size);
784
+
785
+ this.writeValueBuffer(buffer, 0, type, value);
786
+
787
+ return buffer;
788
+ }
789
+
790
+ /**
791
+ * @template T
792
+ * @param {GPUBuffer} buffer
793
+ * @param {number} buffer_offset
794
+ * @param {WebGPUType} type
795
+ * @param {T} value
796
+ */
797
+ writeValueBuffer(buffer, buffer_offset, type, value) {
798
+
799
+ assert.notOk(this.#finished, 'already finished');
800
+
801
+ const gfx = this.#graphics;
802
+
803
+ const size = type.aligned_size;
804
+
805
+ const stage = gfx.buffer_allocator_staging.get(size);
806
+
807
+ const mapped = stage.getMappedRange(0, size);
808
+
809
+ write_gpu_typed_buffer(value, type, mapped, 0);
810
+
811
+ stage.unmap();
812
+
813
+ this.#staging_buffers.push(stage);
814
+
815
+ this.copyBufferToBuffer(stage, 0, buffer, buffer_offset, size);
816
+ }
817
+
818
+ /**
819
+ * Very similar to {@link GPUQueue.writeBuffer}, but instead of doing the operation direction on the queue - uses a staging buffer under the hood and performs the actual write operation as part of the commmand buffer
820
+ * @param {GPUBuffer} buffer
821
+ * @param {number} buffer_offset
822
+ * @param {ArrayBuffer} data
823
+ * @param {number} data_offset
824
+ * @param {number} size
825
+ */
826
+ writeBuffer(buffer, buffer_offset, data, data_offset, size) {
827
+ assert.defined(buffer, 'buffer');
828
+ assert.isNonNegativeInteger(buffer_offset, 'buffer_offset');
829
+ assert.defined(data, 'data');
830
+ assert.isNonNegativeInteger(data_offset, 'data_offset');
831
+ assert.isNonNegativeInteger(size, 'size');
832
+
833
+ const gfx = this.#graphics;
834
+
835
+ const stage = gfx.buffer_allocator_staging.get(size);
836
+
837
+ const mapped = stage.getMappedRange(0, size);
838
+
839
+ array_buffer_copy(data, data_offset, mapped, 0, size);
840
+
841
+ stage.unmap();
842
+
843
+ this.#staging_buffers.push(stage);
844
+
845
+ this.copyBufferToBuffer(stage, 0, buffer, buffer_offset, size);
846
+
847
+ }
848
+
849
+ #cleanup() {
850
+
851
+ // console.warn('cmd.cleanup()');
852
+
853
+ const transient_buffers = this.#transient_buffers;
854
+ const transient_buffer_count = transient_buffers.length;
855
+
856
+ const buffer_allocator_transient = this.#transient_allocator;
857
+
858
+ // release in reverse order to make it more likely that we'd end up in the same acquisition order next time, which is desirable for caching
859
+ for (let i = transient_buffer_count - 1; i >= 0; i--) {
860
+ const buffer = transient_buffers[i];
861
+
862
+ buffer_allocator_transient.release(buffer);
863
+
864
+ }
865
+
866
+ // staging buffers
867
+ const buffer_allocator_staging = this.#graphics.buffer_allocator_staging;
868
+ for (const staging of this.#staging_buffers) {
869
+ buffer_allocator_staging.release(staging);
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Performs both {@link GPUCommandEncoder.finish} and flushed command buffer
875
+ */
876
+ finish() {
877
+ if (this.#finished) {
878
+ console.warn('Context already finished');
879
+ return;
880
+ }
881
+
882
+ this.onBeforeFinish.send1(this);
883
+
884
+ debug_open_context_count--;
885
+ array_remove_first(debug_open_contexts, this);
886
+
887
+ const encoder = this.#gpu_encoder;
888
+
889
+ const debug_timers = this.#debug_timers;
890
+ if (debug_timers !== undefined) {
891
+ debug_timers.resolve(encoder);
892
+ }
893
+
894
+ this.#finished = true;
895
+
896
+ // release encoder reference
897
+ this.#gpu_encoder = undefined;
898
+
899
+ const command_buffer = encoder.finish();
900
+
901
+ const graphics = this.#graphics;
902
+ const queue = graphics.device.queue;
903
+
904
+ queue.submit([command_buffer]);
905
+
906
+ // cleanup allocated memory
907
+ this.#cleanup();
908
+
909
+ if (debug_timers !== undefined) {
910
+ const recorder = this.#profile_recorder;
911
+ const query_set_id = this.#profile_query_set_id;
912
+
913
+ debug_timers
914
+ .download_results()
915
+ .then(() => {
916
+ if (recorder !== null) {
917
+ recorder.absorb(debug_timers, query_set_id);
918
+ }
919
+
920
+ // The console-shaped path stays exactly as it was; a context can be doing both.
921
+ const table = debug_timers.results_to_console_table();
922
+
923
+ this.#debug_timers_callback(table);
924
+ })
925
+ .finally(() => {
926
+ debug_timers.destroy();
927
+
928
+ // In `finally` so a failed readback still releases whoever is waiting on the
929
+ // frame. A profile session that hangs because one download rejected is worse
930
+ // than one missing a frame.
931
+ this.#profiling_absorbed_resolve();
932
+ });
933
+
934
+ this.#debug_timers = undefined;
935
+ this.#profile_recorder = null;
936
+ }
937
+
938
+ this.onFinished.send1(this);
939
+
940
+ }
750
941
  }