MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
GpuDispatchCore.hpp
Go to the documentation of this file.
1#pragma once
2
4
6
8
9namespace MayaFlux::Core {
10class VKImage;
11}
12
13namespace MayaFlux::Yantra {
14
15class GpuDispatchCore;
16
17/**
18 * @struct GpuChannelResult
19 * @brief Erased output of a GPU dispatch: reconstructed float data plus
20 * any raw auxiliary outputs keyed by binding index.
21 */
23 std::vector<float> primary;
24 std::unordered_map<size_t, std::vector<uint8_t>> aux;
25};
26
27/**
28 * @struct IndirectGroupsSource
29 * @brief Location of GPU-resident workgroup counts for a DependencyStage.
30 *
31 * Names a shared buffer of the dispatching context, allocated with
32 * BufferUsageHint::INDIRECT, that holds {x, y, z} as three uint32 values at
33 * offset_bytes. An earlier stage of the same call writes it, so the dispatch
34 * size never reaches the host.
35 */
37 uint32_t set {};
38 size_t binding {};
39 uint64_t offset_bytes { 0 };
40};
41
42/**
43 * @struct DependencyStage
44 * @brief One stage in a dispatch_core_dependency call.
45 *
46 * stage_fn is invoked with this context as the active target, exactly as
47 * if the caller had just called swap_shader(config) and were about to call
48 * dispatch_core — stage_image_at, set_binding_data, stage_passthrough,
49 * set_push_constants all apply normally inside stage_fn. Works identically
50 * for image-bearing and buffer-only shaders; nothing about this struct is
51 * image-specific.
52 *
53 * hazard_fn runs immediately after stage_fn, once this stage's resources
54 * (e.g. an image just allocated inside stage_fn) actually exist. Returns
55 * the hazard list for this stage; may be empty if nothing downstream in
56 * the same dependency call depends on this stage's output.
57 */
60 std::function<void(GpuDispatchCore&)> stage_fn;
61 std::function<std::vector<Portal::Graphics::HazardResource>(GpuDispatchCore&)> hazard_fn;
62 std::optional<std::array<uint32_t, 3>> explicit_groups;
63
64 /**
65 * @brief Shared buffer holding this stage's workgroup counts, when they
66 * come from GPU data. explicit_groups is then ignored.
67 */
68 std::optional<IndirectGroupsSource> indirect_groups;
69};
70
71/**
72 * @class GpuDispatchCore
73 * @brief Non-template base that owns all type-independent GPU dispatch logic.
74 *
75 * Separates resource management, buffer staging, and dispatch orchestration
76 * from the type-parameterised boundary in GpuExecutionContext. All virtual
77 * override points that do not reference InputType/OutputType live here so
78 * that implementations can be placed in a .cpp file.
79 *
80 * Subclasses (including GpuExecutionContext) implement the two remaining
81 * type-dependent steps -- channel extraction and output reconstruction --
82 * without duplicating anything that is type-independent.
83 */
84class MAYAFLUX_API GpuDispatchCore {
85public:
86 explicit GpuDispatchCore(GpuComputeConfig config);
87 virtual ~GpuDispatchCore() = default;
88
93
94 /**
95 * @brief Set push constant data from a raw byte pointer.
96 * @param data Pointer to trivially-copyable push constant struct.
97 * @param bytes Size in bytes.
98 */
99 void set_push_constants(const void* data, size_t bytes);
100
101 /**
102 * @brief Typed convenience wrapper for set_push_constants(const void*, size_t).
103 * @tparam T Trivially copyable type matching shader push constant layout.
104 */
105 template <typename T>
106 void set_push_constants(const T& data)
107 {
108 set_push_constants(&data, sizeof(T));
109 }
110
111 /**
112 * @brief Pre-stage typed data for a specific binding slot, bypassing
113 * the default channel-flattening path in prepare_gpu_inputs.
114 * @tparam T Trivially copyable element type.
115 * @param index Binding index matching declare_buffer_bindings order.
116 * @param data Elements to upload.
117 */
118 template <typename T>
119 void set_binding_data(size_t index, std::span<const T> data)
120 {
121 if (index >= m_binding_data.size())
122 m_binding_data.resize(index + 1);
123 auto& slot = m_binding_data[index];
124 slot.resize(data.size_bytes());
125 std::memcpy(slot.data(), data.data(), data.size_bytes());
126 }
127
128 template <typename T>
129 void set_binding_data(size_t index, const std::vector<T>& data)
130 {
131 set_binding_data(index, std::span<const T>(data));
132 }
133
134 /**
135 * @brief Ensure a shared storage buffer of at least element_count elements
136 * exists at (set, binding_index).
137 *
138 * The default hint allocates host cached memory, suited to buffers the
139 * host reads back. DEVICE and COMPUTE request device local, host visible
140 * memory instead, so buffers only shaders touch avoid crossing the bus;
141 * the request falls back to host cached memory when none is available.
142 * Host reads of device local memory are uncached, so keep bulk readback
143 * buffers on the default hint.
144 */
145 void ensure_shared_buffer(uint32_t set, size_t binding_index, size_t element_count,
147 Portal::Graphics::BufferUsageHint usage_hint = Portal::Graphics::BufferUsageHint::COMPUTE_STORAGE)
148 {
149 m_resources.ensure_shared_buffer(set, binding_index, element_count, element_type, usage_hint);
150 }
151
152 void upload_shared_raw(uint32_t set, size_t binding_index, const uint8_t* data, size_t byte_size)
153 {
154 m_resources.upload_shared_raw(set, binding_index, data, byte_size);
155 }
156
157 void download_shared(uint32_t set, size_t binding_index, void* dest, size_t byte_size)
158 {
159 m_resources.download_shared(set, binding_index, dest, byte_size);
160 }
161
163 const GpuBufferBinding& spec) const
164 {
165 return m_resources.make_shared_buffer_hazard(spec);
166 }
167
168 /**
169 * @brief Declare the byte capacity of an output binding independently
170 * of input data. Required for edge lists, histograms, count
171 * buffers, and any output whose size cannot be derived from input.
172 * @param index Binding index.
173 * @param byte_size Required allocation in bytes.
174 */
175 void set_output_size(size_t index, size_t byte_size);
176
177 /**
178 * @brief Ensure GPU resources are initialised. Safe to call repeatedly.
179 * @return True if GPU is ready after this call.
180 */
181 bool ensure_gpu_ready();
182
183 /**
184 * @brief Query GPU readiness without attempting initialisation.
185 */
186 [[nodiscard]] bool is_gpu_ready() const;
187
188 /**
189 * @brief Return the image registered at an IMAGE_STORAGE output binding.
190 *
191 * Valid after dispatch_core completes (dispatch is synchronous via
192 * submit_and_wait). Callers may then bind it directly to a render pass
193 * or read it back via TextureLoom.
194 *
195 * @param binding_index Index of the IMAGE_STORAGE binding.
196 * @return Shared pointer to the VKImage, or nullptr if not registered.
197 */
198 [[nodiscard]] std::shared_ptr<Core::VKImage> get_output_image(size_t binding_index) const;
199
200 /**
201 * @brief Read back a specific binding into a caller-provided destination.
202 *
203 * @param index Binding index to read back.
204 * @param dest Pointer to caller-allocated memory for the data.
205 * @param byte_size Size in bytes to read back (must not exceed allocated size).
206 */
207 void download_binding(size_t index, void* dest, size_t byte_size);
208
209 /**
210 * @brief Switch which shader subsequent dispatch_core calls target.
211 *
212 * No longer destroys any GpuResourceManager state: each shader path
213 * is a persistent key in GpuResourceManager, created once and reused.
214 * Switching back to a previously-used shader is free. Preserves all
215 * staged image and buffer bindings, same as before.
216 *
217 * Use to drive a multi-op sequence through one context: stage the input
218 * image, call swap_shader + dispatch_core per op, pipe get_output_image(0)
219 * back in via stage_image between steps.
220 *
221 * @param config Shader config for the next dispatch.
222 */
224 {
225 m_gpu_config = std::move(config);
226 update_dispatch_key_cache();
227 }
228
229 /**
230 * @brief Register a VKImage at an explicit binding index.
231 *
232 * Dispatches to the storage or sampled path based on kind. The image
233 * will be transitioned to eGeneral (STORAGE) or eShaderReadOnlyOptimal
234 * (SAMPLED) if not already there.
235 *
236 * @param binding_index Index matching the declared image binding.
237 * @param image Initialised VKImage.
238 * @param kind IMAGE_STORAGE or IMAGE_SAMPLED.
239 * @param sampler Vulkan sampler handle. Required for IMAGE_SAMPLED,
240 * ignored for IMAGE_STORAGE.
241 */
242 void stage_image_at(size_t binding_index,
243 std::shared_ptr<Core::VKImage> image,
245 vk::Sampler sampler = nullptr);
246
247 /**
248 * @brief Non-owning view of the shared buffer at (set, binding_index).
249 *
250 * Lets a consumer read a context's GPU output in place instead of through
251 * a host readback. The context keeps ownership, so the view is only valid
252 * while the context lives and until the buffer is grown.
253 */
254 [[nodiscard]] Portal::Graphics::GpuBufferHandle shared_buffer_handle(uint32_t set, size_t binding_index) const
255 {
256 return m_resources.shared_buffer_handle(set, binding_index);
257 }
258
259 /**
260 * @brief The key used for this context's GpuResourceManager unit.
261 *
262 * shader_path when non-empty (named .comp files). Falls back to the
263 * numeric shader_id when shader_path is empty, which is always the case
264 * for assembled ShaderSpec configs (config_from_spec never sets
265 * shader_path — only shader_id, via ShaderFoundry's content-hash cache).
266 * Without this fallback, every assembled shader collapses onto the same
267 * empty-string key and silently overwrites the previous assembled
268 * shader's pipeline/bindings.
269 */
270 [[nodiscard]] const std::string& dispatch_key() const { return m_cached_dispatch_key; }
271
272protected:
273 /**
274 * @brief Declare the storage buffers the shader expects.
275 *
276 * Default: INPUT at (0,0) FLOAT32, OUTPUT at (0,1) FLOAT32.
277 */
278 [[nodiscard]] virtual std::vector<GpuBufferBinding> declare_buffer_bindings() const;
279
280 /**
281 * @brief Called immediately before dispatch. Override to write push
282 * constants or perform any per-dispatch reconfiguration.
283 */
284 virtual void on_before_gpu_dispatch(
285 const std::vector<std::vector<double>>& channels,
286 const DataStructureInfo& structure_info);
287
288 /**
289 * @brief Marshal channel data into GPU input buffers.
290 *
291 * Handles FLOAT32, UINT32, INT32, PASSTHROUGH, IMAGE_STORAGE, and
292 * IMAGE_SAMPLED binding kinds. Called after flatten_channels_to_staging.
293 */
294 virtual void prepare_gpu_inputs(
295 const std::vector<std::vector<double>>& channels,
296 const DataStructureInfo& structure_info);
297
298 /**
299 * @brief Calculate workgroup dispatch counts from structure dimensions.
300 *
301 * Reads SPATIAL_X/Y/Z roles for 2D/3D shaders; falls back to 1D
302 * element-count dispatch when no spatial dimensions exist.
303 *
304 * @param total_elements Flat element count for the 1D fallback.
305 * @param structure_info Dimension metadata.
306 */
307 [[nodiscard]] virtual std::array<uint32_t, 3> calculate_dispatch_size(
308 size_t total_elements,
309 const DataStructureInfo& structure_info) const;
310
311 /**
312 * @brief Stage raw bytes for a PASSTHROUGH binding before dispatch.
313 * @param binding_index Index matching declare_buffer_bindings order.
314 * @param data Raw byte pointer.
315 * @param byte_size Size in bytes.
316 */
317 void stage_passthrough(size_t binding_index, const void* data, size_t byte_size);
318
319 /**
320 * @brief Stage a flat native-typed byte buffer for FLOAT32 bindings,
321 * bypassing the double-to-float cast in flatten_channels_to_staging.
322 *
323 * Intended for pixel data (uint8_t, uint16_t) and pre-converted float
324 * buffers that should reach the GPU without an intermediate double widening.
325 * Once staged, prepare_gpu_inputs will upload these bytes directly via
326 * upload_raw rather than the float staging path.
327 *
328 * Calling this clears any previously staged native bytes for the slot.
329 * It does not affect m_binding_data (PASSTHROUGH) or m_staging_floats.
330 *
331 * @param binding_index Binding slot. Must match a FLOAT32 INPUT binding.
332 * @param data Raw bytes in the element type the shader expects.
333 * @param byte_size Total byte count.
334 */
335 void stage_native_bytes(size_t binding_index, const void* data, size_t byte_size);
336
337 [[nodiscard]] const GpuComputeConfig& gpu_config() const;
338
339 /**
340 * @brief Full single-pass dispatch. Drives prepare_gpu_inputs,
341 * on_before_gpu_dispatch, bind_descriptor, and GpuResourceManager::dispatch.
342 *
343 * @param channels Extracted double channels from the input Datum.
344 * @param structure_info Dimension/modality metadata from OperationHelper.
345 * @return GpuChannelResult containing primary float readback and aux buffers.
346 */
347 GpuChannelResult dispatch_core(
348 const std::vector<std::vector<double>>& channels,
349 const DataStructureInfo& structure_info);
350
351 /**
352 * @brief Multi-pass (chained) dispatch. Calls dispatch_batched on
353 * GpuResourceManager and reads back once after all passes.
354 *
355 * @param channels Extracted double channels.
356 * @param structure_info Dimension/modality metadata.
357 * @param ctx ExecutionContext carrying pass_count and pc_updater.
358 * @return GpuChannelResult containing primary float readback and aux buffers.
359 */
360 GpuChannelResult dispatch_core_chained(
361 const std::vector<std::vector<double>>& channels,
362 const DataStructureInfo& structure_info,
363 const ExecutionContext& ctx);
364
365 /**
366 * @brief Multi-pass dispatch where a GPU-resident indirect buffer gates
367 * each pass's workgroup count instead of a fixed pass_count.
368 *
369 * Calls dispatch_batched_indirect on GpuResourceManager and reads back
370 * once after all passes.
371 *
372 * @param channels Extracted double channels.
373 * @param structure_info Dimension/modality metadata.
374 * @param ctx ExecutionContext carrying pass_count, pc_updater,
375 * and indirect_dispatch_binding.
376 * @return GpuChannelResult containing primary float readback and aux buffers.
377 */
378 GpuChannelResult dispatch_core_chained_indirect(
379 const std::vector<std::vector<double>>& channels,
380 const DataStructureInfo& structure_info,
381 const ExecutionContext& ctx);
382
383 /**
384 * @brief Non-blocking variant of dispatch_core.
385 *
386 * Performs the full setup (on_before_gpu_dispatch, prepare_gpu_inputs,
387 * bind_descriptor) then calls GpuResourceManager::dispatch_async.
388 * Returns immediately with a FenceID. The caller must poll
389 * ShaderFoundry::is_fence_signaled on the returned ID, and once
390 * signaled call readback_primary / readback_aux to collect results.
391 *
392 * @param channels Extracted double channels from the input Datum.
393 * @param structure_info Dimension/modality metadata from OperationHelper.
394 * @return FenceID to poll. INVALID_FENCE if dispatch fails.
395 */
396 [[nodiscard]] Portal::Graphics::FenceID dispatch_core_async(
397 const std::vector<std::vector<double>>& channels,
398 const DataStructureInfo& structure_info);
399
400 /**
401 * @brief Multi-pipeline dependency dispatch.
402 *
403 * For each stage, in order: applies its shader config (swap_shader
404 * equivalent), invokes stage_fn to perform whatever staging that stage
405 * needs, ensures its GpuResourceManager unit exists, binds its
406 * descriptors, invokes hazard_fn to resolve hazard resources now that
407 * this stage's data exists, then accumulates a Portal::Graphics::ComputeStage.
408 * After every stage is prepared, records and submits the full sequence in
409 * one command buffer via GpuResourceManager::dispatch_sequence.
410 *
411 * Restores the context's original shader config, bindings, and staged
412 * data after the sequence runs, so a subsequent unrelated dispatch_core
413 * call on this context is unaffected.
414 *
415 * @param stages Ordered list of stage descriptions.
416 */
417 void dispatch_core_dependency(const std::vector<DependencyStage>& stages);
418
419 /**
420 * @brief Non-blocking variant of dispatch_core_dependency.
421 *
422 * Stages are prepared exactly as in dispatch_core_dependency, then the
423 * whole sequence is recorded into one command buffer and submitted
424 * without waiting. The context's own shader config, bindings and staged
425 * data are restored before this returns, so the caller may reconfigure
426 * the context immediately. Descriptor sets already written for the
427 * stages' shader keys must not be rebound until the returned fence has
428 * signaled.
429 *
430 * The barrier owed to the final stage's INPUT_OUTPUT hazards is still
431 * recorded, which orders anything submitted afterwards behind this
432 * sequence's writes.
433 *
434 * Unlike dispatch_core_async, no shader-write to host-read barrier is
435 * recorded for the stages' buffers. As with dispatch_core_dependency,
436 * host reads after the fence has signaled rely on the fence's own
437 * visibility guarantee and on the buffers being host coherent.
438 *
439 * @param stages Ordered stage descriptions, as for DependencyParams.
440 * @return FenceID to poll with ShaderFoundry::is_fence_signaled and to
441 * release once signaled. INVALID_FENCE on submission failure.
442 */
443 [[nodiscard]] Portal::Graphics::FenceID dispatch_core_dependency_async(
444 const std::vector<DependencyStage>& stages);
445
446 /**
447 * @brief Effective element count used by the last dispatch_core or
448 * dispatch_core_async call.
449 *
450 * Cached after each dispatch so callers can pass the correct count to
451 * readback_primary without re-deriving it.
452 */
453 [[nodiscard]] size_t last_effective_element_count() const
454 {
455 return m_last_effective_element_count;
456 }
457
458 /**
459 * @brief Read back the primary output buffer into a float vector.
460 *
461 * Selects the first OUTPUT or INPUT_OUTPUT binding. Caps readback to
462 * the lesser of the requested float count and the allocated buffer size.
463 *
464 * @param float_count Number of float elements to attempt to read.
465 * @return Float vector of length min(float_count, allocated / sizeof(float)).
466 */
467 [[nodiscard]] std::vector<float> readback_primary(size_t float_count);
468
469 /**
470 * @brief Read back all OUTPUT bindings that have explicit size overrides
471 * into the aux map of a GpuChannelResult.
472 *
473 * @param result GpuChannelResult to write aux entries into.
474 */
475 void readback_aux(GpuChannelResult& result);
476
477 /**
478 * @brief Flatten planar double channels into m_staging_floats.
479 *
480 * Skipped for structured modalities (glm::vec3 etc.) since those are
481 * handled per-binding via PASSTHROUGH or integer paths.
482 */
483 void flatten_channels_to_staging(
484 const std::vector<std::vector<double>>& channels,
485 const DataStructureInfo& structure_info);
486
487 /**
488 * @brief Flatten native-typed DataVariant channels into m_native_staging_bytes
489 * without any conversion.
490 *
491 * Called by prepare_gpu_inputs when structure_info.original_type indicates
492 * a non-double native type and no explicit stage_native_bytes call has been
493 * made. Visits the variant's active alternative and memcpys bytes directly.
494 *
495 * No-ops when channels is empty or the modality is structured.
496 *
497 * @param variants Per-channel DataVariants from the container.
498 * @param structure_info Dimension/modality metadata.
499 */
500 void flatten_native_variants_to_staging(
501 const std::vector<Kakshya::DataVariant>& variants,
502 const DataStructureInfo& structure_info);
503
504 [[nodiscard]] size_t find_first_output_index() const;
505 [[nodiscard]] size_t largest_binding_data_element_count() const;
506
508 std::vector<GpuBufferBinding> m_bindings;
509 std::vector<float> m_staging_floats;
510 std::vector<uint8_t> m_push_constants;
511 std::vector<size_t> m_output_size_overrides;
512 std::vector<std::vector<uint8_t>> m_passthrough_bytes;
513 std::vector<std::vector<uint8_t>> m_binding_data;
514
516 std::shared_ptr<Core::VKImage> image;
517 vk::Sampler sampler;
519 };
520 std::vector<ImageBinding> m_image_bindings;
521
522private:
524 std::vector<std::string> keys;
525 std::vector<std::array<uint32_t, 3>> groups;
526 std::vector<std::vector<uint8_t>> push_constants;
527 std::vector<std::vector<Portal::Graphics::HazardResource>> hazards;
528 std::vector<Portal::Graphics::IndirectDispatch> indirect;
529 };
530
531 [[nodiscard]] DependencyPlan prepare_dependency(const std::vector<DependencyStage>& stages);
532
535
536 size_t m_last_effective_element_count {};
537
538 /// Native-typed staging buffer. Non-empty when stage_native_bytes() has
539 /// been called or flatten_native_variants_to_staging() produced output.
540 /// Takes precedence over m_staging_floats in prepare_gpu_inputs.
541 std::vector<uint8_t> m_native_staging_bytes;
542
543 void update_dispatch_key_cache();
544 void bind_all_descriptors();
545};
546
547} // namespace MayaFlux::Yantra
IO::ImageData image
Definition Decoder.cpp:47
AttributeKind kind
uint32_t index
Definition VKDevice.cpp:122
uint32_t channels
std::vector< ImageBinding > m_image_bindings
std::vector< uint8_t > m_native_staging_bytes
Native-typed staging buffer.
GpuDispatchCore(GpuDispatchCore &&)=delete
Portal::Graphics::HazardResource shared_buffer_hazard(const GpuBufferBinding &spec) const
void ensure_shared_buffer(uint32_t set, size_t binding_index, size_t element_count, GpuBufferBinding::ElementType element_type, Portal::Graphics::BufferUsageHint usage_hint=Portal::Graphics::BufferUsageHint::COMPUTE_STORAGE)
Ensure a shared storage buffer of at least element_count elements exists at (set, binding_index).
const std::string & dispatch_key() const
The key used for this context's GpuResourceManager unit.
void download_shared(uint32_t set, size_t binding_index, void *dest, size_t byte_size)
std::vector< uint8_t > m_push_constants
void upload_shared_raw(uint32_t set, size_t binding_index, const uint8_t *data, size_t byte_size)
void set_binding_data(size_t index, const std::vector< T > &data)
std::vector< std::vector< uint8_t > > m_binding_data
Portal::Graphics::GpuBufferHandle shared_buffer_handle(uint32_t set, size_t binding_index) const
Non-owning view of the shared buffer at (set, binding_index).
void swap_shader(GpuComputeConfig config)
Switch which shader subsequent dispatch_core calls target.
std::vector< GpuBufferBinding > m_bindings
std::vector< size_t > m_output_size_overrides
virtual ~GpuDispatchCore()=default
void set_push_constants(const T &data)
Typed convenience wrapper for set_push_constants(const void*, size_t).
size_t last_effective_element_count() const
Effective element count used by the last dispatch_core or dispatch_core_async call.
GpuDispatchCore & operator=(GpuDispatchCore &&)=delete
std::vector< std::vector< uint8_t > > m_passthrough_bytes
GpuDispatchCore(const GpuDispatchCore &)=delete
GpuDispatchCore & operator=(const GpuDispatchCore &)=delete
void set_binding_data(size_t index, std::span< const T > data)
Pre-stage typed data for a specific binding slot, bypassing the default channel-flattening path in pr...
Non-template base that owns all type-independent GPU dispatch logic.
Encapsulates all Vulkan resource lifecycle behind Portal facades.
BufferUsageHint
Semantic usage hint for buffer allocation and memory properties.
ElementType
Element type the shader expects in this binding.
Declares a single storage buffer or image binding a compute shader expects.
Non-owning view of a GPU storage buffer produced by a compute path.
Plain-data description of the compute shader to dispatch.
One resource this stage's dispatch reads/writes that a later stage in the sequence depends on,...
Dimensions, modality, source type, and optional Composite field schema.
std::optional< IndirectGroupsSource > indirect_groups
Shared buffer holding this stage's workgroup counts, when they come from GPU data.
std::function< void(GpuDispatchCore &)> stage_fn
std::function< std::vector< Portal::Graphics::HazardResource >(GpuDispatchCore &)> hazard_fn
std::optional< std::array< uint32_t, 3 > > explicit_groups
One stage in a dispatch_core_dependency call.
Context information controlling how a compute operation executes.
std::unordered_map< size_t, std::vector< uint8_t > > aux
Erased output of a GPU dispatch: reconstructed float data plus any raw auxiliary outputs keyed by bin...
std::vector< std::vector< uint8_t > > push_constants
std::vector< Portal::Graphics::IndirectDispatch > indirect
std::vector< std::vector< Portal::Graphics::HazardResource > > hazards
std::vector< std::array< uint32_t, 3 > > groups
Location of GPU-resident workgroup counts for a DependencyStage.