@woosh/meep-engine 3.11.2 → 3.11.3

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