MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TextureExecutionContext.hpp
Go to the documentation of this file.
1#pragma once
2
4
8
10
12
13namespace MayaFlux::Yantra {
14
15/**
16 * @class TextureExecutionContext
17 * @brief GpuExecutionContext specialisation for image compute shaders.
18 *
19 * Accepts DataIO (Datum<vector<DataVariant>>) as both input and output,
20 * matching the standard ComputeOperation contract. Image staging is driven
21 * by the optional container field on the input Datum:
22 *
23 * - When datum.container holds a TextureContainer, extract_inputs() stashes
24 * it and on_before_gpu_dispatch() uploads it to a VKImage staged at the
25 * declared image input binding. This is the preferred path: the container
26 * carries format, layer count, mip metadata, and sampler config.
27 *
28 * - When datum.container is absent or not a TextureContainer, no image
29 * staging occurs. The shader receives whatever bindings the caller
30 * declared via set_binding_data / stage_passthrough / stage_image_sampled
31 * directly. dispatch_async still works; it is a no-op on the image side.
32 *
33 * Output options (select one at construction):
34 *
35 * OutputMode::CONTAINER — the storage image at binding 0 is downloaded into
36 * a new TextureContainer and placed in the output Datum's container field.
37 * Primary float readback is empty. Mirrors the original behaviour for
38 * texture-transform pipelines (blur, colour grade, format convert).
39 *
40 * OutputMode::SCALAR — no image download. collect_result() returns the
41 * primary float readback and aux SSBOs from GpuChannelResult, exactly as
42 * ShaderExecutionContext does. Use for reduction shaders that write a small
43 * SSBO (spatial average, histogram, region sample) and feed the node graph.
44 *
45 * dispatch_async / collect_result mirror ShaderExecutionContext so
46 * GpuComputeNode can own this class without modification.
47 *
48 * calculate_dispatch_size() uses image dimensions when a TextureContainer is
49 * present, otherwise falls through to the standard element-count path.
50 *
51 * A context has one owner. Its image cache is accessed only after the previous
52 * dispatch has completed, so ImageCacheSet::acquire and
53 * TextureLoom::acquire_cached_image are not synchronized here. Do not invoke
54 * dispatch or image staging concurrently on the same context.
55 */
56class MAYAFLUX_API TextureExecutionContext
57 : public GpuExecutionContext<
58 std::vector<Kakshya::DataVariant>,
59 std::vector<Kakshya::DataVariant>> {
60public:
62 std::vector<Kakshya::DataVariant>,
63 std::vector<Kakshya::DataVariant>>;
64
65 enum class OutputMode : uint8_t {
66 CONTAINER, ///< Download storage image at binding 0 into a TextureContainer.
67 SCALAR, ///< Return SSBO readback via collect_result(); no image download.
68 IMAGE, ///< Transition output image to shader-read layout; no CPU download.
69 ///< Retrieve via get_output_image(0). Zero readback cost.
70 };
71
72 /**
73 * @param config Shader path, workgroup size, push constant size.
74 * @param output_format Pixel format of the storage image created per dispatch.
75 * Ignored when mode is SCALAR.
76 * @param mode Controls what collect_gpu_outputs / collect_result return.
77 * @param image_binding Binding index at which the input image is staged.
78 * Default 1.
79 * @param aux_bindings Additional buffer bindings to declare.
80 * @param image_access Access mode for the input image binding: IMAGE_SAMPLED
81 * (default) or IMAGE_STORAGE.
82 * @param output_binding Binding index for the output storage image.
83 * Ignored when mode is SCALAR. Default 0.
84 */
86 GpuComputeConfig config,
87 Portal::Graphics::ImageFormat output_format = Portal::Graphics::ImageFormat::RGBA8,
88 OutputMode mode = OutputMode::CONTAINER,
89 uint32_t image_binding = 1,
90 std::vector<GpuBufferBinding> aux_bindings = {},
91 GpuBufferBinding::ElementType image_access = GpuBufferBinding::ElementType::IMAGE_SAMPLED,
92 uint32_t output_binding = 0)
93 : Base(std::move(config))
94 , m_output_format(output_format)
95 , m_output_mode(mode)
96 , m_aux_bindings(std::move(aux_bindings))
97 {
98 m_image_slots.push_back({ .binding = { .set = 0,
99 .binding = image_binding,
100 .direction = GpuBufferBinding::Direction::INPUT,
101 .element_type = image_access } });
102 if (m_output_mode != OutputMode::SCALAR) {
103 m_image_slots.push_back({ .binding = { .set = 0,
104 .binding = output_binding,
105 .direction = GpuBufferBinding::Direction::OUTPUT,
106 .element_type = GpuBufferBinding::ElementType::IMAGE_STORAGE } });
107 }
108 }
109
110 /**
111 * @brief Construct from an explicit, fully-specified binding list.
112 *
113 * For dispatch shapes the positional constructor's image/aux split does
114 * not fit cleanly: any combination of input image, output image, and
115 * SSBOs declared directly as GpuBufferBinding entries. mode still governs
116 * collect_gpu_outputs / collect_result; the caller is responsible for the
117 * binding list matching that mode (e.g. an OUTPUT IMAGE_STORAGE entry
118 * present whenever mode != SCALAR).
119 *
120 * @param config Shader path, workgroup size, push constant size.
121 * @param bindings Full binding list, image and buffer entries mixed freely.
122 * @param mode Controls what collect_gpu_outputs / collect_result return.
123 */
125 GpuComputeConfig config,
126 const std::vector<GpuBufferBinding>& bindings,
127 OutputMode mode = OutputMode::SCALAR)
128 : Base(std::move(config))
129 , m_output_format(Portal::Graphics::ImageFormat::RGBA8)
130 , m_output_mode(mode)
131 {
132 for (const auto& b : bindings) {
133 const bool is_image = b.element_type == GpuBufferBinding::ElementType::IMAGE_STORAGE
134 || b.element_type == GpuBufferBinding::ElementType::IMAGE_SAMPLED;
135 if (is_image) {
136 m_image_slots.push_back({ .binding = b });
137 } else {
138 m_aux_bindings.push_back(b);
139 }
140 }
141 }
142
143 /**
144 * @brief Set the array layer staged from the input TextureContainer on the
145 * next dispatch_async call.
146 *
147 * Defaults to 0. Call before dispatch_async when iterating video frames,
148 * animation layers, or any multi-layer container where the layer index
149 * changes per dispatch. Has no effect when no TextureContainer is present
150 * on the input Datum.
151 *
152 * @param layer Array layer index. Must be < TextureContainer::get_layer_count().
153 */
154 void set_input_layer(uint32_t layer) { m_pending_layer = layer; }
155
156 /**
157 * @brief Override the output storage image dimensions for the next dispatch.
158 *
159 * When set, on_before_gpu_dispatch allocates the output image at these
160 * dimensions rather than the input container dimensions, and
161 * calculate_dispatch_size dispatches workgroups to cover this extent.
162 * Callers are responsible for supplying matching push constants so the
163 * shader maps output pixels to the correct source coordinates.
164 *
165 * Pass std::nullopt to restore container-derived sizing.
166 *
167 * @param w Output width in pixels.
168 * @param h Output height in pixels.
169 */
170 void set_output_dimensions(uint32_t w, uint32_t h)
171 {
172 m_output_dim_override = { w, h };
173 }
174
176 {
177 m_output_dim_override = std::nullopt;
178 }
179
180 /**
181 * @brief Select a distinct cached output image when the input is the
182 * current output image.
183 */
184 void set_avoid_output_alias(bool enabled)
185 {
186 m_avoid_output_alias = enabled;
187 }
188
189 /**
190 * @brief Direct access to an image slot's binding descriptor by index.
191 *
192 * Mutating .direction is used by callers driving dispatch_core_chained
193 * directly, where the batched-dispatch cross-pass barrier only fires for
194 * INPUT_OUTPUT. Caller is responsible for restoring OUTPUT afterward if
195 * the slot's normal dispatch_async path expects that.
196 */
197 GpuBufferBinding& slot_binding(uint32_t binding_index)
198 {
199 for (auto& s : m_image_slots) {
200 if (s.binding.binding == binding_index)
201 return s.binding;
202 }
203 error<std::runtime_error>(Journal::Component::Yantra, Journal::Context::BufferProcessing,
204 std::source_location::current(),
205 "TextureExecutionContext: no image slot at binding {}", binding_index);
206 }
207
208 // =========================================================================
209 // Async dispatch — mirrors ShaderExecutionContext
210 // =========================================================================
211
212 /**
213 * @brief Non-blocking dispatch.
214 *
215 * If datum.container holds a TextureContainer it is uploaded and staged
216 * before submission. Fence becomes signaled when GPU work completes.
217 * Call collect_result() (SCALAR mode) or retrieve the output Datum via
218 * execute() (CONTAINER mode) once signaled.
219 *
220 * @param input DataIO whose container field drives optional image staging.
221 * @return FenceID to poll with ShaderFoundry::is_fence_signaled.
222 */
224 {
225 if (!ensure_gpu_ready())
226 return Portal::Graphics::INVALID_FENCE;
227
228 auto [channels, structure_info] = extract_inputs(input);
229 return dispatch_core_async(channels, structure_info);
230 }
231
232 /**
233 * @brief Collect SSBO readback after a signaled async dispatch.
234 *
235 * Valid only in SCALAR mode. Mirrors ShaderExecutionContext::collect_result().
236 *
237 * @return GpuChannelResult with primary float data and aux buffers.
238 */
240 {
241 GpuChannelResult result;
242 result.primary = readback_primary(last_effective_element_count());
243 readback_aux(result);
244 return result;
245 }
246
247 /**
248 * @brief Collect the output TextureContainer after a signaled async dispatch.
249 *
250 * Valid only in CONTAINER mode. Mirrors collect_result() for SCALAR mode.
251 * Must be called only after ShaderFoundry::is_fence_signaled returns true
252 * for the FenceID returned by dispatch_async.
253 *
254 * @return DataIO with container field holding the output TextureContainer,
255 * or empty DataIO on failure.
256 */
258 {
260 readback_aux(raw);
261 return collect_gpu_outputs(raw, {}, {});
262 }
263
264 /**
265 * @brief Override for CHAINED mode: skips collect_gpu_outputs().
266 *
267 * Callers driving multi-pass image dispatch (e.g. ConnectedComponents
268 * merge loop) may flip an image slot to INPUT_OUTPUT for the cross-pass
269 * barrier in dispatch_batched, which leaves no OUTPUT-direction slot for
270 * collect_gpu_outputs' output_slot() lookup to find. Non-CHAINED modes
271 * are unaffected and fall through to the base implementation.
272 *
273 * @param input Ignored for CHAINED mode (image already staged manually).
274 * @param ctx ExecutionContext; CHAINED mode requires pass_count and pc_updater.
275 */
277 {
278 if (ctx.mode != ExecutionMode::CHAINED && ctx.mode != ExecutionMode::CHAINED_INDIRECT)
279 return Base::execute(input, ctx);
280
281 if (!ensure_gpu_ready()) {
282 error<std::runtime_error>(
283 Journal::Component::Yantra,
284 Journal::Context::BufferProcessing,
285 std::source_location::current(),
286 "TextureExecutionContext: GPU initialisation failed");
287 }
288
289 auto [ch_copies, structure_info] = extract_inputs(input);
290
291 if (ctx.mode == ExecutionMode::CHAINED_INDIRECT) {
292 dispatch_core_chained_indirect(ch_copies, structure_info, ctx);
293 } else {
294 dispatch_core_chained(ch_copies, structure_info, ctx);
295 }
296
297 return output_type {};
298 }
299
300 // =========================================================================
301 // Manual image staging — for callers that already hold a VKImage
302 // =========================================================================
303
304 /**
305 * @brief Stage an arbitrary VKImage at the declared input image binding.
306 *
307 * Bypasses container resolution. The image must be in
308 * eShaderReadOnlyOptimal layout for IMAGE_SAMPLED access, or eGeneral
309 * for IMAGE_STORAGE access. Use before dispatch_async when the source
310 * is a render-pass output or camera frame rather than a container.
311 *
312 * @param image Initialized VKImage.
313 * @param sampler Vulkan sampler. Defaults to the TextureLoom linear sampler
314 * when nullptr. Ignored for IMAGE_STORAGE access.
315 */
317 const std::shared_ptr<Core::VKImage>& image,
318 vk::Sampler sampler = nullptr)
319 {
320 auto& slot = input_slot();
321 if (slot.binding.element_type == GpuBufferBinding::ElementType::IMAGE_STORAGE) {
322 stage_image_at(slot.binding.binding, image, GpuBufferBinding::ElementType::IMAGE_STORAGE);
323 } else {
324 auto s = sampler
325 ? sampler
326 : Portal::Graphics::SamplerForge::instance().get_default_linear();
327 stage_image_at(slot.binding.binding, image, GpuBufferBinding::ElementType::IMAGE_SAMPLED, s);
328 }
329 slot.image = image;
330 m_pending_container = nullptr;
331 }
332
333 /**
334 * @brief Stage a TextureContainer layer at the declared input image binding.
335 *
336 * Convenience for callers that hold a container and want explicit control
337 * over layer and sampler rather than relying on Datum.container resolution.
338 * Always stages as IMAGE_SAMPLED regardless of the input slot's declared
339 * access mode.
340 *
341 * @param container Source TextureContainer.
342 * @param layer Array layer index (default 0).
343 * @param sampler Vulkan sampler. Defaults to linear when nullptr.
344 */
346 const Kakshya::TextureContainer& container,
347 uint32_t layer = 0,
348 vk::Sampler sampler = nullptr)
349 {
350 auto& slot = input_slot();
351 auto img = container_image(container, layer);
352 auto s = sampler
353 ? sampler
354 : Portal::Graphics::SamplerForge::instance().get_default_linear();
355 stage_image_at(slot.binding.binding, img, GpuBufferBinding::ElementType::IMAGE_SAMPLED, s);
356 slot.image = img;
357 m_pending_container = nullptr;
358 }
359
360 /**
361 * @brief Allocates the output storage image and stages it at the
362 * declared output binding.
363 *
364 * Only called in CONTAINER or IMAGE mode, from on_before_gpu_dispatch
365 * or per-step by callers chaining multi-pass sequences. The output slot
366 * keeps the images it has used, keyed by their creation parameters, so a
367 * caller that alternates between a few sizes, such as a sequence that
368 * downsamples every run, reuses them instead of allocating on every
369 * change. Images beyond a byte budget are dropped least recently used
370 * first, and asking for a dropped shape allocates again.
371 *
372 * TextureLoom retains every image it creates for the life of the process,
373 * so an image that is dropped from the cache is not freed.
374 */
375 void prepare_output_image(uint32_t width, uint32_t height)
376 {
377 auto& slot = output_slot();
378 if (m_avoid_output_alias) {
379 auto primary = acquire_output_image(slot, width, height);
380 const auto input = input_slot().image;
381 slot.image = primary && primary == input
382 ? acquire_output_image(slot, width, height, true)
383 : std::move(primary);
384 slot.width = width;
385 slot.height = height;
386 } else if (!slot.image || slot.width != width || slot.height != height) {
387 slot.image = acquire_output_image(slot, width, height);
388 slot.width = width;
389 slot.height = height;
390 }
391 stage_image_at(slot.binding.binding, slot.image, GpuBufferBinding::ElementType::IMAGE_STORAGE);
392 }
393
394protected:
395 // =========================================================================
396 // GpuExecutionContext overrides
397 // =========================================================================
398
399 /**
400 * @brief Declare the image bindings from m_image_slots, plus any
401 * aux SSBO bindings provided at construction.
402 */
403 [[nodiscard]] std::vector<GpuBufferBinding> declare_buffer_bindings() const override
404 {
405 std::vector<GpuBufferBinding> bindings;
406 for (const auto& slot : m_image_slots)
407 bindings.push_back(slot.binding);
408 bindings.insert(bindings.end(), m_aux_bindings.begin(), m_aux_bindings.end());
409 return bindings;
410 }
411
412 /**
413 * @brief Stashes the TextureContainer from datum.container for use in
414 * on_before_gpu_dispatch; returns empty channels (image shaders
415 * do not use the numeric channel path).
416 */
417 std::pair<std::vector<std::vector<double>>, DataStructureInfo>
419 {
420 m_pending_container = resolve_texture_container(input);
421 return { {}, {} };
422 }
423
424 /**
425 * @brief Stages the pending TextureContainer at the declared input image
426 * binding and, in CONTAINER or IMAGE mode, allocates the output
427 * storage image at the declared output binding.
428 */
430 const std::vector<std::vector<double>>& /*channels*/,
431 const DataStructureInfo& /*structure_info*/) override
432 {
433 if (m_pending_container) {
434 const auto sampler = Portal::Graphics::SamplerForge::instance().get_default_linear();
435 const auto img = container_image(*m_pending_container, m_pending_layer);
436
437 auto& in_slot = input_slot();
438 if (in_slot.binding.element_type == GpuBufferBinding::ElementType::IMAGE_STORAGE) {
439 stage_image_at(in_slot.binding.binding, img, GpuBufferBinding::ElementType::IMAGE_STORAGE);
440 } else {
441 stage_image_at(in_slot.binding.binding, img, GpuBufferBinding::ElementType::IMAGE_SAMPLED, sampler);
442 }
443 in_slot.image = img;
444
445 if (m_output_mode == OutputMode::CONTAINER
446 || m_output_mode == OutputMode::IMAGE) {
447 const uint32_t ow = m_output_dim_override
448 ? m_output_dim_override->first
449 : m_pending_container->get_width();
450 const uint32_t oh = m_output_dim_override
451 ? m_output_dim_override->second
452 : m_pending_container->get_height();
453 prepare_output_image(ow, oh);
454 }
455 }
456 }
457
458 /**
459 * @brief Derives dispatch size from the staged container dimensions when
460 * available; falls back to element-count dispatch otherwise.
461 */
462 [[nodiscard]] std::array<uint32_t, 3> calculate_dispatch_size(
463 size_t total_elements,
464 const DataStructureInfo& structure_info) const override
465 {
466 const auto& ws = gpu_config().workgroup_size;
467 if (m_output_dim_override) {
468 const uint32_t w = m_output_dim_override->first;
469 const uint32_t h = m_output_dim_override->second;
470 return {
471 (w + ws[0] - 1) / ws[0],
472 (h + ws[1] - 1) / ws[1],
473 1U
474 };
475 }
476 if (m_pending_container) {
477 const uint32_t w = m_pending_container->get_width();
478 const uint32_t h = m_pending_container->get_height();
479 return {
480 (w + ws[0] - 1) / ws[0],
481 (h + ws[1] - 1) / ws[1],
482 1U
483 };
484 }
485 return Base::calculate_dispatch_size(total_elements, structure_info);
486 }
487
488 /**
489 * @brief In CONTAINER mode: downloads the storage image at the declared
490 * output binding into a new TextureContainer placed in output.container.
491 * In SCALAR mode: returns an empty DataIO (use collect_result() instead).
492 */
494 const GpuChannelResult& /*raw*/,
495 const std::vector<std::vector<double>>& /*channels*/,
496 const DataStructureInfo& /*structure_info*/) override
497 {
498 if (m_output_mode == OutputMode::SCALAR)
499 return output_type {};
500
501 auto img = get_output_image(output_slot().binding.binding);
502 if (!img) {
503 error<std::runtime_error>(
504 Journal::Component::Yantra,
505 Journal::Context::BufferProcessing,
506 std::source_location::current(),
507 "TextureExecutionContext: no output image at declared output binding after dispatch");
508 }
509
510 Portal::Graphics::TextureLoom::instance().transition_layout(
511 img, vk::ImageLayout::eGeneral, vk::ImageLayout::eShaderReadOnlyOptimal);
512
513 if (m_output_mode == OutputMode::IMAGE)
514 return output_type {};
515
516 const uint32_t w = m_output_dim_override
517 ? m_output_dim_override->first
518 : (m_pending_container ? m_pending_container->get_width() : img->get_width());
519 const uint32_t h = m_output_dim_override
520 ? m_output_dim_override->second
521 : (m_pending_container ? m_pending_container->get_height() : img->get_height());
522
523 if (!m_output_container) {
524 m_output_container = std::make_shared<Kakshya::TextureContainer>(
525 w, h, m_output_format);
526 }
527
528 if (!m_download_staging) {
529 m_download_staging = Buffers::create_image_staging_buffer(
530 m_output_container->byte_size());
531 }
532
533 m_output_container->from_image(img, m_download_staging, 0);
534
535 output_type result;
536 result.container = std::static_pointer_cast<Kakshya::SignalSourceContainer>(
537 m_output_container);
538 return result;
539 }
540
541private:
542 /**
543 * @struct ImageKey
544 * @brief The creation parameters of an image.
545 *
546 * These are what decide whether an image can stand in for another. Its
547 * layout and contents change by transition and upload, so they are not
548 * part of the key and never cause a new image.
549 */
551
552 static constexpr size_t k_slot_cache_budget_bytes = size_t { 256 } << 20;
553
554 /**
555 * @struct ImageSlot
556 * @brief One declared image binding: its GpuBufferBinding descriptor,
557 * the currently staged VKImage, and cached dimensions.
558 *
559 * direction on the stored GpuBufferBinding distinguishes the input slot
560 * from the output slot; element_type distinguishes IMAGE_STORAGE from
561 * IMAGE_SAMPLED. Binding index is caller-configurable at construction,
562 * never hardcoded elsewhere in this class.
563 *
564 * cache retains images by allocation properties, so a slot that alternates
565 * between a few shapes reuses its images.
566 */
567 struct ImageSlot {
569 std::shared_ptr<Core::VKImage> image;
570 uint32_t width {};
571 uint32_t height {};
572 Portal::Graphics::ImageCacheSet cache { k_slot_cache_budget_bytes };
573 Portal::Graphics::ImageCacheSet alternate_cache { k_slot_cache_budget_bytes };
574 };
575
578 bool m_avoid_output_alias { false };
579 uint32_t m_pending_layer {};
580 std::vector<ImageSlot> m_image_slots;
581
582 std::shared_ptr<Kakshya::TextureContainer> m_pending_container;
583 std::shared_ptr<Kakshya::TextureContainer> m_output_container;
584 std::shared_ptr<Buffers::VKBuffer> m_download_staging;
585 std::shared_ptr<Buffers::VKBuffer> m_upload_staging;
586 std::vector<GpuBufferBinding> m_aux_bindings;
587
588 std::optional<std::pair<uint32_t, uint32_t>> m_output_dim_override;
589
591
592 // =========================================================================
593 // Helpers
594 // =========================================================================
595
596 /**
597 * @brief The container's pixels as a GPU image, without allocating one per
598 * call.
599 *
600 * The context retains its own image. TextureLoom creates one when its
601 * allocation properties change, then the container uploads current pixels
602 * into it on every call.
603 *
604 * The image is reused in place, so the previous dispatch on this context
605 * must have completed before the next call, as it must for the output
606 * image.
607 *
608 * @return The input image, or nullptr when the container has no usable data.
609 */
610 [[nodiscard]] std::shared_ptr<Core::VKImage> container_image(
611 const Kakshya::TextureContainer& container, uint32_t layer)
612 {
613 const uint32_t layers = container.get_layer_count();
614 const bool array = layers > 1;
615 const ImageKey key {
616 .width = container.get_width(),
617 .height = container.get_height(),
618 .layers = layers,
619 .format = container.get_format(),
620 .kind = array ? ImageKey::Kind::SAMPLED_ARRAY : ImageKey::Kind::SAMPLED_2D,
621 };
622
623 const size_t staging_bytes = container.byte_size() * (array ? layers : 1U);
624 if (!m_upload_staging) {
625 m_upload_staging = Buffers::create_image_staging_buffer(staging_bytes);
626 } else if (staging_bytes > m_upload_staging->get_size_bytes()) {
627 m_upload_staging->resize(
628 static_cast<size_t>(static_cast<float>(staging_bytes) * Buffers::k_buffer_growth_factor), false);
629 }
630
631 auto image = Portal::Graphics::TextureLoom::instance()
632 .acquire_cached_image(m_input_cache, key);
633 if (!image)
634 return nullptr;
635
636 const bool uploaded = array
637 ? container.upload_image_array(image, m_upload_staging)
638 : container.upload_image(image, layer, m_upload_staging);
639 return uploaded ? image : nullptr;
640 }
641
642 /**
643 * @brief Return this slot's cached image for these creation parameters,
644 * creating and caching one when there is none.
645 *
646 * The hashed cache keeps the current image on a direct fast path. Entries
647 * remain cached after the byte threshold is exceeded; the cache warns
648 * once because TextureLoom retains created images until shutdown.
649 */
650 [[nodiscard]] std::shared_ptr<Core::VKImage> acquire_output_image(
651 ImageSlot& slot, uint32_t width, uint32_t height, bool alternate = false)
652 {
653 const ImageKey key {
654 .width = width,
655 .height = height,
656 .layers = 1,
657 .format = m_output_format,
658 .kind = ImageKey::Kind::STORAGE_2D,
659 };
660 auto& cache = alternate ? slot.alternate_cache : slot.cache;
661 return cache.acquire(Portal::Graphics::TextureLoom::instance(), key);
662 }
663
664 /**
665 * @brief Find the declared input image slot.
666 * @return Reference to the ImageSlot with Direction::INPUT.
667 */
668 [[nodiscard]] ImageSlot& input_slot()
669 {
670 for (auto& s : m_image_slots) {
671 if (s.binding.direction == GpuBufferBinding::Direction::INPUT)
672 return s;
673 }
674
675 error<std::runtime_error>(
676 Journal::Component::Yantra,
677 Journal::Context::BufferProcessing,
678 std::source_location::current(),
679 "TextureExecutionContext: no input image slot declared");
680 }
681
682 /**
683 * @brief Find the declared output image slot.
684 * @return Reference to the ImageSlot with Direction::OUTPUT.
685 * @note Only present when constructed with mode != SCALAR.
686 */
687 [[nodiscard]] ImageSlot& output_slot()
688 {
689 for (auto& s : m_image_slots) {
690 if (s.binding.direction == GpuBufferBinding::Direction::OUTPUT)
691 return s;
692 }
693
694 error<std::runtime_error>(
695 Journal::Component::Yantra,
696 Journal::Context::BufferProcessing,
697 std::source_location::current(),
698 "TextureExecutionContext: no output image slot declared (SCALAR mode?)");
699 }
700
701 /**
702 * @brief Extracts a TextureContainer pointer from datum.container if present.
703 *
704 * Returns nullptr when the container field is absent or holds a different
705 * SignalSourceContainer subtype.
706 */
707 static std::shared_ptr<Kakshya::TextureContainer> resolve_texture_container(const input_type& input)
708 {
709 if (!input.container || !*input.container)
710 return nullptr;
711 return std::dynamic_pointer_cast<Kakshya::TextureContainer>(input.container.value());
712 }
713};
714
715} // namespace MayaFlux::Yantra
Core::GlobalInputConfig input
Definition Config.cpp:42
IO::ImageData image
Definition Decoder.cpp:47
uint32_t h
Definition InkPress.cpp:29
uint32_t mode
size_t b
uint32_t channels
uint32_t width
uint32_t height
bool upload_image(const std::shared_ptr< Core::VKImage > &image, uint32_t layer, const std::shared_ptr< Buffers::VKBuffer > &staging) const
Upload one layer into an existing VKImage instead of creating one.
bool upload_image_array(const std::shared_ptr< Core::VKImage > &image, const std::shared_ptr< Buffers::VKBuffer > &staging) const
Upload all layers into an existing VKImage instead of creating one.
size_t byte_size() const
Total byte count of the pixel buffer.
SignalSourceContainer wrapping GPU texture data as addressable pixel bytes.
std::shared_ptr< Core::VKImage > acquire(TextureLoom &loom, const ImageKey &key)
Cache images keyed by immutable allocation properties.
Type-parameterised shell over GpuDispatchCore.
void set_avoid_output_alias(bool enabled)
Select a distinct cached output image when the input is the current output image.
std::shared_ptr< Kakshya::TextureContainer > m_output_container
ImageSlot & output_slot()
Find the declared output image slot.
std::shared_ptr< Buffers::VKBuffer > m_download_staging
output_type execute(const input_type &input, const ExecutionContext &ctx) override
Override for CHAINED mode: skips collect_gpu_outputs().
TextureExecutionContext(GpuComputeConfig config, Portal::Graphics::ImageFormat output_format=Portal::Graphics::ImageFormat::RGBA8, OutputMode mode=OutputMode::CONTAINER, uint32_t image_binding=1, std::vector< GpuBufferBinding > aux_bindings={}, GpuBufferBinding::ElementType image_access=GpuBufferBinding::ElementType::IMAGE_SAMPLED, uint32_t output_binding=0)
output_type collect_container_result()
Collect the output TextureContainer after a signaled async dispatch.
std::shared_ptr< Buffers::VKBuffer > m_upload_staging
void on_before_gpu_dispatch(const std::vector< std::vector< double > > &, const DataStructureInfo &) override
Stages the pending TextureContainer at the declared input image binding and, in CONTAINER or IMAGE mo...
void set_output_dimensions(uint32_t w, uint32_t h)
Override the output storage image dimensions for the next dispatch.
void set_input_layer(uint32_t layer)
Set the array layer staged from the input TextureContainer on the next dispatch_async call.
std::shared_ptr< Kakshya::TextureContainer > m_pending_container
output_type collect_gpu_outputs(const GpuChannelResult &, const std::vector< std::vector< double > > &, const DataStructureInfo &) override
In CONTAINER mode: downloads the storage image at the declared output binding into a new TextureConta...
static std::shared_ptr< Kakshya::TextureContainer > resolve_texture_container(const input_type &input)
Extracts a TextureContainer pointer from datum.container if present.
void prepare_output_image(uint32_t width, uint32_t height)
Allocates the output storage image and stages it at the declared output binding.
Portal::Graphics::FenceID dispatch_async(const input_type &input)
Non-blocking dispatch.
ImageSlot & input_slot()
Find the declared input image slot.
void stage_container(const Kakshya::TextureContainer &container, uint32_t layer=0, vk::Sampler sampler=nullptr)
Stage a TextureContainer layer at the declared input image binding.
std::shared_ptr< Core::VKImage > acquire_output_image(ImageSlot &slot, uint32_t width, uint32_t height, bool alternate=false)
Return this slot's cached image for these creation parameters, creating and caching one when there is...
std::optional< std::pair< uint32_t, uint32_t > > m_output_dim_override
Portal::Graphics::ImageCacheEntry m_input_cache
GpuChannelResult collect_result()
Collect SSBO readback after a signaled async dispatch.
std::array< uint32_t, 3 > calculate_dispatch_size(size_t total_elements, const DataStructureInfo &structure_info) const override
Derives dispatch size from the staged container dimensions when available; falls back to element-coun...
void stage_image(const std::shared_ptr< Core::VKImage > &image, vk::Sampler sampler=nullptr)
Stage an arbitrary VKImage at the declared input image binding.
std::pair< std::vector< std::vector< double > >, DataStructureInfo > extract_inputs(const input_type &input) override
Stashes the TextureContainer from datum.container for use in on_before_gpu_dispatch; returns empty ch...
GpuBufferBinding & slot_binding(uint32_t binding_index)
Direct access to an image slot's binding descriptor by index.
TextureExecutionContext(GpuComputeConfig config, const std::vector< GpuBufferBinding > &bindings, OutputMode mode=OutputMode::SCALAR)
Construct from an explicit, fully-specified binding list.
std::vector< GpuBufferBinding > declare_buffer_bindings() const override
Declare the image bindings from m_image_slots, plus any aux SSBO bindings provided at construction.
std::shared_ptr< Core::VKImage > container_image(const Kakshya::TextureContainer &container, uint32_t layer)
The container's pixels as a GPU image, without allocating one per call.
GpuExecutionContext specialisation for image compute shaders.
ImageFormat
User-friendly image format enum.
Definition ImageData.hpp:12
bool is_image(const fs::path &filepath)
Definition Depot.cpp:136
ElementType
Element type the shader expects in this binding.
uint32_t binding
Binding index within the set.
Declares a single storage buffer or image binding a compute shader expects.
Plain-data description of the compute shader to dispatch.
Image retained by the producing owner.
Immutable properties that determine whether an image can be reused.
Dimensions, modality, source type, and optional Composite field schema.
std::optional< std::shared_ptr< Kakshya::SignalSourceContainer > > container
Optional reference to container, required for regions.
Definition DataIO.hpp:33
Input/Output container for computation pipeline data flow with structure preservation.
Definition DataIO.hpp:26
ExecutionMode mode
Execution mode controlling scheduling behavior.
Context information controlling how a compute operation executes.
Erased output of a GPU dispatch: reconstructed float data plus any raw auxiliary outputs keyed by bin...
One declared image binding: its GpuBufferBinding descriptor, the currently staged VKImage,...