MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VisionGpuDispatch.cpp
Go to the documentation of this file.
2
6
7namespace MayaFlux::Yantra {
8
9using namespace Portal::Graphics;
10using namespace Kinesis::Vision;
11
12// ============================================================================
13// Internal push constant layouts
14// ============================================================================
15
16namespace {
17 constexpr uint32_t CC_BACKGROUND_HOST = 0xFFFFFFFFU;
18 constexpr uint32_t CC_UNCLAIMED_HOST = 0U;
19
20 /** Standard 2D workgroup used by all pixel-to-pixel vision shaders */
21 constexpr std::array<uint32_t, 3> k_wg2d { 8, 8, 1 };
22 /** Maximum number of connected components that can be labeled in a single pass */
23 constexpr uint32_t k_max_components = 4096;
24 /** Maximum number of points that can be stored in a single contour */
25 constexpr uint32_t k_max_points_per_contour = 4096;
26 /** Maximum number of contours that can be stored in a single pass */
27 constexpr uint32_t k_max_holes_per_label = 4;
28 /** Maximum number of trace slots that can be stored in a single pass (for contour tracing) */
29 constexpr uint32_t k_max_trace_slots = k_max_components * (1U + k_max_holes_per_label);
30
31 struct ThresholdPC {
32 float value;
33 };
34 struct ThresholdAdaptivePC {
35 uint32_t block_size;
36 float offset;
37 };
38 struct OtsuHistPC {
39 uint32_t width;
40 uint32_t height;
41 };
42 struct NormalizePC {
43 float scale;
44 float offset;
45 };
46 struct MorphPC {
47 uint32_t radius;
48 };
49 struct IngestPC {
50 uint32_t width;
51 uint32_t height;
52 };
53 struct HarrisPC {
54 float k;
55 uint32_t pass;
56 uint32_t width;
57 uint32_t height;
58 };
59 struct CannyPC {
60 float sigma;
61 float lo;
62 float hi;
63 };
64 struct RgbaToGrayPC {
65 float wr;
66 float wg;
67 float wb;
68 float wa;
69 };
70 struct GaussianPC {
71 uint32_t radius;
72 uint32_t width;
73 uint32_t height;
74 };
75 struct CompletedOp {
76 std::shared_ptr<Core::VKImage> output;
77 std::shared_ptr<Core::VKImage> input;
78 };
79 struct ClassifyPC {
80 float threshold;
81 float value;
82 };
83 struct HysteresisPC {
84 uint32_t width;
85 uint32_t height;
86 };
87 struct FinalizePC {
88 float threshold;
89 };
90 struct ExtractPeaksPC {
91 float threshold;
92 uint32_t nms_radius;
93 uint32_t width;
94 uint32_t height;
95 uint32_t max_keypoints;
96 };
97 struct CCBlockInitPC {
98 uint32_t width;
99 uint32_t height;
100 uint32_t block_width;
101 uint32_t block_height;
102 };
103 struct CCMergePC {
104 uint32_t width;
105 uint32_t height;
106 uint32_t block_width;
107 uint32_t block_height;
108 };
109 struct CCCompressPC {
110 uint32_t block_width;
111 uint32_t block_height;
112 };
113 struct CCFinalLabelPC {
114 uint32_t width;
115 uint32_t height;
116 uint32_t block_width;
117 uint32_t block_height;
120 };
121 struct CCResetPC {
122 uint32_t lut_size;
123 uint32_t max_components;
124 };
125 struct ContourSegmentsPC {
126 uint32_t width;
127 uint32_t height;
128 uint32_t max_segments;
129 };
130 struct ContourLinkPC {
131 uint32_t width;
132 uint32_t height;
133 uint32_t phase;
134 };
135 struct ContourClearPC {
136 uint32_t width;
137 uint32_t height;
138 };
139 struct ContourRenderPC {
140 uint32_t width;
141 uint32_t height;
142 uint32_t max_components;
144 uint32_t max_contours;
145 };
146 struct ContourMarchPC {
147 uint32_t width;
148 uint32_t height;
149 uint32_t max_components;
150 uint32_t max_points_per_contour;
152 uint32_t phase;
153 float min_area;
155 };
156 struct ContourCompactPC {
157 uint32_t max_components;
158 uint32_t max_holes_per_label;
159 };
160
161 /**
162 * @brief 2D Gaussian kernel for convolution, cached by (radius, sigma
163 * bit pattern).
164 *
165 * Sigma is a tuning parameter that rarely changes frame to frame;
166 * recomputing exp() over (2*radius+1)^2 taps and reallocating the
167 * kernel every call is pure repeated work for an identical result.
168 *
169 * @param radius Radius of the kernel in pixels. Kernel size is (2*radius + 1)^2.
170 * @param sigma Standard deviation of the Gaussian.
171 * @return Normalized kernel weights as a flat vector in row-major order.
172 */
173 const std::vector<float>& gaussian_kernel_2d(uint32_t radius, float sigma)
174 {
175 static std::unordered_map<uint64_t, std::vector<float>> cache;
176 const uint64_t key = (static_cast<uint64_t>(std::bit_cast<uint32_t>(sigma)) << 32)
177 | radius;
178 auto it = cache.find(key);
179 if (it != cache.end())
180 return it->second;
181
182 const uint32_t diam = 2 * radius + 1;
183 std::vector<float> k(static_cast<size_t>(diam) * diam);
184 float sum = 0.0F;
185 for (uint32_t y = 0; y < diam; ++y) {
186 for (uint32_t x = 0; x < diam; ++x) {
187 const float fx = static_cast<float>(x) - static_cast<float>(radius);
188 const float fy = static_cast<float>(y) - static_cast<float>(radius);
189 const float v = std::exp(-(fx * fx + fy * fy) / (2.0F * sigma * sigma));
190 k[y * diam + x] = v;
191 sum += v;
192 }
193 }
194 for (auto& v : k)
195 v /= sum;
196
197 return cache.emplace(key, std::move(k)).first->second;
198 }
199
200 /**
201 * @brief 1D Gaussian kernel for separable convolution, cached by
202 * (radius, sigma bit pattern).
203 *
204 * Sigma is a tuning parameter that rarely changes frame to frame;
205 * recomputing exp() per tap and reallocating the kernel every call
206 * is pure repeated work for an identical result. Mirrors
207 * VisionExecutor::gaussian_kernel's caching rationale for the CPU path.
208 *
209 * @param radius Radius of the kernel in pixels. Kernel size is (2*radius + 1).
210 * @param sigma Standard deviation of the Gaussian.
211 * @return Normalized kernel weights.
212 */
213 const std::vector<float>& gaussian_kernel_1d(uint32_t radius, float sigma)
214 {
215 static std::unordered_map<uint64_t, std::vector<float>> cache;
216 const uint64_t key = (static_cast<uint64_t>(std::bit_cast<uint32_t>(sigma)) << 32)
217 | radius;
218 auto it = cache.find(key);
219 if (it != cache.end())
220 return it->second;
221
222 const uint32_t size = 2 * radius + 1;
223 std::vector<float> k(size);
224 float sum = 0.0F;
225 for (uint32_t i = 0; i < size; ++i) {
226 const float x = static_cast<float>(i) - static_cast<float>(radius);
227 k[i] = std::exp(-(x * x) / (2.0F * sigma * sigma));
228 sum += k[i];
229 }
230 for (auto& v : k)
231 v /= sum;
232
233 return cache.emplace(key, std::move(k)).first->second;
234 }
235
236 GpuVisionPass::Completed op_threshold_otsu(VisionGpuContexts& contexts)
237 {
238 auto& pixel_ctx = contexts.pixel;
239 auto& structured_ctx = contexts.structured;
240 auto w = contexts.pass.w;
241 auto h = contexts.pass.h;
242 auto& foundry = Portal::Graphics::get_shader_foundry();
243
244 const auto otsu_input = contexts.pass.current;
245
246 structured_ctx.swap_shader({
247 .shader_path = "otsu_histogram.comp.spv",
248 .workgroup_size = k_wg2d,
249 .push_constant_size = sizeof(OtsuHistPC),
250 });
251 std::vector<uint32_t> zeros(256, 0);
252 structured_ctx.set_binding_data(3, std::span<const uint32_t>(zeros));
253 structured_ctx.stage_image(contexts.pass.current);
254 structured_ctx.set_push_constants(OtsuHistPC { .width = w, .height = h });
255 structured_ctx.set_output_dimensions(w, h);
256 {
257 const auto f = structured_ctx.dispatch_async({});
258 structured_ctx.clear_output_dimensions();
259 foundry.wait_for_fence(f);
260 foundry.release_fence(f);
261 }
262
263 const auto hist_check = structured_ctx.collect_result();
264 std::vector<uint32_t> hist_readback(256, 0);
265 if (auto it = hist_check.aux.find(3); it != hist_check.aux.end())
266 std::memcpy(hist_readback.data(), it->second.data(), 256 * sizeof(uint32_t));
267
268 structured_ctx.swap_shader({
269 .shader_path = "otsu_select.comp.spv",
270 .workgroup_size = { 256, 1, 1 },
271 });
272 structured_ctx.set_binding_data(3, std::span<const uint32_t>(hist_readback));
273 structured_ctx.set_output_dimensions(256, 1);
274
275 {
276 const auto f = structured_ctx.dispatch_async({});
277 structured_ctx.clear_output_dimensions();
278 foundry.wait_for_fence(f);
279 foundry.release_fence(f);
280 }
281
282 const auto sel_result = structured_ctx.collect_result();
283 uint32_t best_bin = 0;
284 if (auto it = sel_result.aux.find(4); it != sel_result.aux.end())
285 std::memcpy(&best_bin, it->second.data(), sizeof(uint32_t));
286 const float t_norm = static_cast<float>(best_bin) / 255.0F;
287
288 const auto apply_cfg = config_from_spec(
289 ShaderSpec::Assemble {}
290 .storage_image("out", BindingDirection::Output)
291 .storage_image("src", BindingDirection::Input)
292 .pc("threshold")
293 .op(KernelOp::CompareGE)
294 .workgroup(k_wg2d[0], k_wg2d[1])
295 .build());
296 pixel_ctx.swap_shader(apply_cfg);
297 pixel_ctx.stage_image(contexts.pass.current);
298 pixel_ctx.set_push_constants(ThresholdPC { .value = t_norm });
299 pixel_ctx.prepare_output_image(w, h);
300 {
301 const auto f = pixel_ctx.dispatch_async({});
302 foundry.wait_for_fence(f);
303 foundry.release_fence(f);
304 }
305 auto thresholded = pixel_ctx.get_output_image(0);
306
307 contexts.pass.result.debug_labels = thresholded;
308 contexts.pass.current = thresholded;
309 contexts.pass.result.structured = std::monostate {};
310
311 contexts.bound_config = apply_cfg;
312 contexts.bound_staged = otsu_input;
313
314 return { .output = thresholded, .input = otsu_input };
315 }
316
317 GpuVisionPass::Completed op_open_close(
318 VisionGpuContexts& contexts,
319 VisionOp op,
320 const MorphParams& p)
321 {
322 auto& pixel_ctx = contexts.pixel;
323 auto w = contexts.pass.w;
324 auto h = contexts.pass.h;
325 auto& foundry = Portal::Graphics::get_shader_foundry();
326
327 const auto morph_input = contexts.pass.current;
328 const auto radius = p.radius;
329 const bool is_open = (op == VisionOp::Open);
330
331 const GpuComputeConfig first_cfg {
332 .shader_path = is_open ? "erode.comp.spv" : "dilate.comp.spv",
333 .workgroup_size = k_wg2d,
334 .push_constant_size = sizeof(MorphPC),
335 };
336 pixel_ctx.swap_shader(first_cfg);
337 pixel_ctx.stage_image(contexts.pass.current);
338 pixel_ctx.set_push_constants(MorphPC { .radius = radius });
339 pixel_ctx.prepare_output_image(w, h);
340 pixel_ctx.set_output_dimensions(w, h);
341 {
342 const auto f = pixel_ctx.dispatch_async({});
343 foundry.wait_for_fence(f);
344 foundry.release_fence(f);
345 }
346 auto intermediate = pixel_ctx.get_output_image(0);
347
348 const GpuComputeConfig second_cfg {
349 .shader_path = is_open ? "dilate.comp.spv" : "erode.comp.spv",
350 .workgroup_size = k_wg2d,
351 .push_constant_size = sizeof(MorphPC),
352 };
353 pixel_ctx.swap_shader(second_cfg);
354 pixel_ctx.stage_image(intermediate);
355 pixel_ctx.set_push_constants(MorphPC { .radius = radius });
356 pixel_ctx.prepare_output_image(w, h);
357 pixel_ctx.set_output_dimensions(w, h);
358 {
359 const auto f = pixel_ctx.dispatch_async({});
360 foundry.wait_for_fence(f);
361 foundry.release_fence(f);
362 }
363
364 auto opened_closed = pixel_ctx.get_output_image(0);
365 contexts.pass.current = opened_closed;
366 contexts.pass.result.structured = std::monostate {};
367
368 contexts.bound_config = second_cfg;
369 contexts.bound_staged = intermediate;
370
371 return { .output = opened_closed, .input = morph_input };
372 }
373
374 GpuVisionPass::Completed op_canny(
375 VisionGpuContexts& contexts,
376 const VisionParams& params,
377 const CannyParams& p)
378 {
379 auto& pixel_ctx = contexts.pixel;
380 auto& label_ctx = contexts.labels;
381 auto w = contexts.pass.w;
382 auto h = contexts.pass.h;
383 auto& foundry = Portal::Graphics::get_shader_foundry();
384
385 const auto canny_input = contexts.pass.current;
386
387 const auto blur_key = Kinesis::Vision::hash_vision_step(
388 VisionOp::GaussianBlur, GaussianBlurParams { .sigma = p.sigma });
389 std::shared_ptr<Core::VKImage> blurred;
390
391 if (auto it = contexts.pass.completed.find(blur_key);
392 it != contexts.pass.completed.end() && it->second.input == canny_input) {
393 blurred = it->second.output;
394 } else {
395 const auto radius = static_cast<uint32_t>(std::ceil(p.sigma * 3.0F));
396 const auto& weights = gaussian_kernel_1d(radius, p.sigma);
397 const auto blur_cfg = VisionGpuExecutor::config(VisionOp::GaussianBlur, GaussianBlurParams { .sigma = p.sigma });
398 pixel_ctx.swap_shader(blur_cfg);
399 pixel_ctx.stage_image(canny_input);
400 pixel_ctx.set_binding_data(2, std::span<const float>(weights));
401 pixel_ctx.set_push_constants(GaussianPC { .radius = radius, .width = w, .height = h });
402 pixel_ctx.prepare_output_image(w, h);
403 {
404 const auto f = pixel_ctx.dispatch_async({});
405 foundry.wait_for_fence(f);
406 foundry.release_fence(f);
407 }
408 blurred = pixel_ctx.get_output_image(0);
409 contexts.pass.completed[blur_key] = { .output = blurred, .input = canny_input };
410 }
411
412 const auto sobel_key = Kinesis::Vision::hash_vision_step(VisionOp::Sobel, std::monostate {});
413 std::shared_ptr<Core::VKImage> grad;
414 if (auto it = contexts.pass.completed.find(sobel_key);
415 it != contexts.pass.completed.end() && it->second.input == blurred) {
416 grad = it->second.output;
417 } else {
418 const auto sobel_cfg = VisionGpuExecutor::config(VisionOp::Sobel, std::monostate {});
419 pixel_ctx.swap_shader(sobel_cfg);
420 pixel_ctx.stage_image(blurred);
421 pixel_ctx.prepare_output_image(w, h);
422 {
423 const auto f = pixel_ctx.dispatch_async({});
424 foundry.wait_for_fence(f);
425 foundry.release_fence(f);
426 }
427 grad = pixel_ctx.get_output_image(0);
428 contexts.pass.completed[sobel_key] = { .output = grad, .input = blurred };
429 }
430
431 const GpuComputeConfig nms_cfg {
432 .shader_path = "canny_nms.comp.spv",
433 .workgroup_size = k_wg2d,
434 };
435 pixel_ctx.swap_shader(nms_cfg);
436 pixel_ctx.stage_image(grad);
437 pixel_ctx.prepare_output_image(w, h);
438 {
439 const auto f = pixel_ctx.dispatch_async({});
440 foundry.wait_for_fence(f);
441 foundry.release_fence(f);
442 }
443 auto suppressed = pixel_ctx.get_output_image(0);
444
445 const auto classify_cfg = VisionGpuExecutor::config(VisionOp::Canny, params);
446 pixel_ctx.swap_shader(classify_cfg);
447 pixel_ctx.stage_image(suppressed);
448 pixel_ctx.set_push_constants(ClassifyPC { .threshold = p.low_threshold, .value = 0.5F });
449 pixel_ctx.prepare_output_image(w, h);
450 {
451 const auto f = pixel_ctx.dispatch_async({});
452 foundry.wait_for_fence(f);
453 foundry.release_fence(f);
454 }
455 auto classified_weak = pixel_ctx.get_output_image(0);
456
457 pixel_ctx.stage_image(classified_weak);
458 pixel_ctx.set_push_constants(ClassifyPC { .threshold = p.high_threshold, .value = 1.0F });
459 pixel_ctx.prepare_output_image(w, h);
460 {
461 const auto f = pixel_ctx.dispatch_async({});
462 foundry.wait_for_fence(f);
463 foundry.release_fence(f);
464 }
465 auto classified = pixel_ctx.get_output_image(0);
466
467 constexpr uint32_t k_max_hysteresis_rounds = 64;
468 label_ctx.set_output_size(2, sizeof(uint32_t));
469 label_ctx.set_output_dimensions(w, h);
470 label_ctx.swap_shader({
471 .shader_path = "canny_hysteresis.comp.spv",
472 .workgroup_size = k_wg2d,
473 .push_constant_size = sizeof(HysteresisPC),
474 });
475 label_ctx.stage_image_at(0, classified, GpuBufferBinding::ElementType::IMAGE_STORAGE);
476 label_ctx.slot_binding(0).direction = GpuBufferBinding::Direction::INPUT_OUTPUT;
477 {
478 uint32_t zero = 0;
479 label_ctx.set_binding_data(2, std::span<const uint32_t>(&zero, 1));
480 const HysteresisPC hpc { .width = w, .height = h };
481 ExecutionContext chained_ctx;
482 chained_ctx.mode = ExecutionMode::CHAINED;
483 chained_ctx.parameters = ChainedParams {
484 .pass_count = k_max_hysteresis_rounds,
485 .pc_updater = [hpc](uint32_t, void* dst) { std::memcpy(dst, &hpc, sizeof(HysteresisPC)); },
486 .passes_per_batch = k_max_hysteresis_rounds,
487 };
488 label_ctx.execute(Datum<> {}, chained_ctx);
489 }
490 label_ctx.slot_binding(0).direction = GpuBufferBinding::Direction::OUTPUT;
491 auto hysteresis_result = classified;
492
493 const auto finalize_cfg = config_from_spec(
494 ShaderSpec::Assemble {}
495 .storage_image("out", BindingDirection::Output)
496 .storage_image("src", BindingDirection::Input)
497 .pc("threshold")
498 .op(KernelOp::CompareGE)
499 .workgroup(k_wg2d[0], k_wg2d[1])
500 .build());
501 pixel_ctx.swap_shader(finalize_cfg);
502 pixel_ctx.stage_image(hysteresis_result);
503 pixel_ctx.set_push_constants(FinalizePC { .threshold = 1.0F });
504 pixel_ctx.prepare_output_image(w, h);
505 {
506 const auto f = pixel_ctx.dispatch_async({});
507 foundry.wait_for_fence(f);
508 foundry.release_fence(f);
509 }
510 auto finalized = pixel_ctx.get_output_image(0);
511
512 contexts.pass.result.debug_labels = finalized;
513 contexts.pass.current = finalized;
514 contexts.pass.result.structured = std::monostate {};
515
516 contexts.bound_config = finalize_cfg;
517 contexts.bound_staged = hysteresis_result;
518
519 return { .output = finalized, .input = canny_input };
520 }
521
522 GpuVisionPass::Completed op_harris_response(
523 VisionGpuContexts& contexts,
524 const HarrisParams& p)
525 {
526 auto& pixel_ctx = contexts.pixel;
527 auto w = contexts.pass.w;
528 auto h = contexts.pass.h;
529 auto& foundry = Portal::Graphics::get_shader_foundry();
530
531 const auto radius = static_cast<uint32_t>(std::ceil(p.sigma * 3.0F));
532 const auto& weights = gaussian_kernel_1d(radius, p.sigma);
533
534 const auto harris_input = contexts.pass.current;
535
536 pixel_ctx.swap_shader({ .shader_path = "harris_grad_pack.comp.spv", .workgroup_size = k_wg2d });
537 pixel_ctx.stage_image(harris_input);
538 pixel_ctx.prepare_output_image(w, h);
539 {
540 const auto f = pixel_ctx.dispatch_async({});
541 foundry.wait_for_fence(f);
542 foundry.release_fence(f);
543 }
544 auto packed = pixel_ctx.get_output_image(0);
545
546 const auto blur_cfg = VisionGpuExecutor::config(VisionOp::GaussianBlur, GaussianBlurParams { .sigma = p.sigma });
547 pixel_ctx.swap_shader(blur_cfg);
548 pixel_ctx.stage_image(packed);
549 pixel_ctx.set_binding_data(2, std::span<const float>(weights));
550 pixel_ctx.set_push_constants(GaussianPC { .radius = radius, .width = w, .height = h });
551 pixel_ctx.prepare_output_image(w, h);
552 {
553 const auto f = pixel_ctx.dispatch_async({});
554 foundry.wait_for_fence(f);
555 foundry.release_fence(f);
556 }
557 auto smoothed = pixel_ctx.get_output_image(0);
558
559 const GpuComputeConfig harris_resp_cfg {
560 .shader_path = "harris_response.comp.spv",
561 .workgroup_size = k_wg2d,
562 .push_constant_size = sizeof(HarrisPC),
563 };
564 pixel_ctx.swap_shader(harris_resp_cfg);
565 pixel_ctx.stage_image(smoothed);
566
567 /** Zero PeakBuf (binding 2) before pass 0 so its atomicMax starts clean;
568 * clear the staged bytes before pass 1 so the accumulated max survives. */
569 const uint32_t peak_reset = 0U;
570 pixel_ctx.set_binding_data(2, std::span<const uint32_t>(&peak_reset, 1));
571 pixel_ctx.set_push_constants(HarrisPC { .k = p.k, .pass = 0U, .width = w, .height = h });
572 pixel_ctx.prepare_output_image(w, h);
573 {
574 const auto f = pixel_ctx.dispatch_async({});
575 foundry.wait_for_fence(f);
576 foundry.release_fence(f);
577 }
578
579 pixel_ctx.set_binding_data(2, std::span<const uint32_t>(&peak_reset, 0));
580 pixel_ctx.set_push_constants(HarrisPC { .k = p.k, .pass = 1U, .width = w, .height = h });
581 {
582 const auto f = pixel_ctx.dispatch_async({});
583 foundry.wait_for_fence(f);
584 foundry.release_fence(f);
585 }
586
587 contexts.pass.current = pixel_ctx.get_output_image(0);
588 contexts.pass.result.structured = std::monostate {};
589
590 contexts.bound_config = harris_resp_cfg;
591 contexts.bound_staged = smoothed;
592
593 return { .output = contexts.pass.current, .input = harris_input };
594 }
595
596 void op_extract_peaks(
597 VisionGpuContexts& contexts,
598 const ExtractPeaksParams& p)
599 {
600 auto& structured_ctx = contexts.structured;
601 auto w = contexts.pass.w;
602 auto h = contexts.pass.h;
603 auto& foundry = Portal::Graphics::get_shader_foundry();
604
605 constexpr uint32_t k_max_kp = 4096;
606
607 structured_ctx.swap_shader({
608 .shader_path = "extract_peaks.comp.spv",
609 .workgroup_size = { 8, 8, 1 },
610 .push_constant_size = sizeof(ExtractPeaksPC),
611 });
612
613 structured_ctx.set_output_size(1, sizeof(uint32_t));
614 structured_ctx.set_output_size(2, static_cast<size_t>(k_max_kp) * 4 * sizeof(float));
615
616 structured_ctx.stage_image(contexts.pass.current);
617 structured_ctx.set_push_constants(ExtractPeaksPC {
618 .threshold = p.threshold,
619 .nms_radius = p.nms_radius,
620 .width = w,
621 .height = h,
622 .max_keypoints = k_max_kp,
623 });
624
625 structured_ctx.set_output_dimensions(w, h);
626 const auto fence = structured_ctx.dispatch_async({});
627 structured_ctx.clear_output_dimensions();
628 foundry.wait_for_fence(fence);
629 foundry.release_fence(fence);
630
631 const auto* next = contexts.pass.ahead();
632 if (next && next->op == VisionOp::TrackKeypoints) {
633 contexts.pass.result.structured = std::monostate {};
634 contexts.pass.result.w = w;
635 contexts.pass.result.h = h;
636 return;
637 }
638
639 const auto gpu_result = structured_ctx.collect_result();
640
641 uint32_t count = 0;
642 if (auto it = gpu_result.aux.find(1); it != gpu_result.aux.end())
643 std::memcpy(&count, it->second.data(), sizeof(uint32_t));
644 count = std::min(count, k_max_kp);
645
646 struct GpuKp {
647 float x, y, response, pad;
648 };
649 std::vector<GpuKp> raw(count);
650 if (count > 0) {
651 if (auto it = gpu_result.aux.find(2); it != gpu_result.aux.end())
652 std::memcpy(raw.data(), it->second.data(), count * sizeof(GpuKp));
653 }
654
655 std::vector<Kinesis::Vision::Keypoint> kpts;
656 kpts.reserve(count);
657 for (const auto& kp : raw) {
658 kpts.push_back({ .position = { kp.x, kp.y },
659 .response = kp.response,
660 .scale = 1.0F,
661 .angle = 0.0F });
662 }
663 std::ranges::sort(kpts, [](const auto& a, const auto& b) { return a.response > b.response; });
664
665 contexts.pass.result.structured = std::move(kpts);
666 contexts.pass.result.w = 0;
667 contexts.pass.result.h = 0;
668 }
669
670 void op_connected_components(
671 VisionGpuContexts& contexts,
672 const ConnectedComponentsParams& p)
673 {
674 auto& cc_pipeline = contexts.cc_pipeline;
675 auto w = contexts.pass.w;
676 auto h = contexts.pass.h;
677 auto& foundry = Portal::Graphics::get_shader_foundry();
678
679 const auto seed_input = contexts.pass.current;
680
681 const uint32_t block_width = (w + 1U) / 2U;
682 const uint32_t block_height = (h + 1U) / 2U;
683 const double block_diagonal = std::sqrt(
684 static_cast<double>(block_width) * block_width + static_cast<double>(block_height) * block_height);
685 const auto k_compress_passes = static_cast<uint32_t>(std::ceil(std::log2(std::max(2.0, block_diagonal))));
686
687 cc_pipeline.ensure_shared_buffer(0, 2, static_cast<size_t>(block_width) * block_height, GpuBufferBinding::ElementType::UINT32);
688 cc_pipeline.ensure_shared_buffer(0, 3, 1, GpuBufferBinding::ElementType::UINT32);
689 cc_pipeline.ensure_shared_buffer(0, 4, static_cast<size_t>(block_width) * block_height, GpuBufferBinding::ElementType::UINT32);
690 cc_pipeline.ensure_shared_buffer(0, 5, 1, GpuBufferBinding::ElementType::UINT32);
691 cc_pipeline.ensure_shared_buffer(0, 6, static_cast<size_t>(w) * h, GpuBufferBinding::ElementType::UINT32);
692 cc_pipeline.ensure_shared_buffer(0, 7, static_cast<size_t>(k_max_components) * 2, GpuBufferBinding::ElementType::UINT32);
693 cc_pipeline.ensure_shared_buffer(0, 8, static_cast<size_t>(k_max_components) * 2, GpuBufferBinding::ElementType::UINT32);
694 cc_pipeline.ensure_shared_buffer(0, 9, k_max_components, GpuBufferBinding::ElementType::UINT32);
695
696 const CCBlockInitPC init_pc { .width = w, .height = h, .block_width = block_width, .block_height = block_height };
697 const CCMergePC merge_pc { .width = w, .height = h, .block_width = block_width, .block_height = block_height };
698 const auto* next = contexts.pass.ahead();
699 const bool contours_follow = next && next->op == VisionOp::FindContours;
700 const uint32_t export_labels = (p.export_labels || contours_follow) ? 1U : 0U;
701
702 const CCFinalLabelPC final_pc {
703 .width = w,
704 .height = h,
705 .block_width = block_width,
706 .block_height = block_height,
707 .max_components = k_max_components,
708 .export_labels = export_labels
709 };
710
711 cc_pipeline.swap_shader({ .shader_path = "cc_reset.comp.spv", .workgroup_size = { 256, 1, 1 }, .push_constant_size = sizeof(CCResetPC) });
712 cc_pipeline.set_push_constants(CCResetPC {
713 .lut_size = block_width * block_height,
714 .max_components = k_max_components,
715 });
716 cc_pipeline.set_output_dimensions(std::max(block_width * block_height, k_max_components), 1);
717 {
718 const auto reset_fence = cc_pipeline.dispatch_async({});
719 foundry.wait_for_fence(reset_fence);
720 foundry.release_fence(reset_fence);
721 }
722 cc_pipeline.clear_output_dimensions();
723
724 const std::array<uint32_t, 3> block_groups {
725 (block_width + k_wg2d[0] - 1U) / k_wg2d[0],
726 (block_height + k_wg2d[1] - 1U) / k_wg2d[1],
727 1U
728 };
729
730 std::vector<DependencyStage> cc_stages;
731
732 cc_stages.push_back({
733 .config = { .shader_path = "cc_block_init.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(CCBlockInitPC) },
734 .stage_fn = [&](GpuDispatchCore& ctx) {
735 cc_pipeline.stage_image_at(1, seed_input, GpuBufferBinding::ElementType::IMAGE_STORAGE);
736 ctx.set_push_constants(init_pc);
737 cc_pipeline.set_output_dimensions(block_width, block_height); },
738 .hazard_fn = [&](GpuDispatchCore& ctx) -> std::vector<Portal::Graphics::HazardResource> {
739 return {
740 ctx.shared_buffer_hazard(
741 { .set = 0, .binding = 2, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 }),
742 };
743 },
744 .explicit_groups = block_groups,
745 });
746
747 cc_stages.push_back({
748 .config = { .shader_path = "cc_merge.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(CCMergePC) },
749 .stage_fn = [&](GpuDispatchCore& ctx) {
750 cc_pipeline.stage_image_at(1, seed_input, GpuBufferBinding::ElementType::IMAGE_STORAGE);
751 ctx.set_push_constants(merge_pc);
752 cc_pipeline.set_output_dimensions(block_width, block_height); },
753 .hazard_fn = [&](GpuDispatchCore& ctx) -> std::vector<Portal::Graphics::HazardResource> {
754 return {
755 ctx.shared_buffer_hazard(
756 { .set = 0, .binding = 2, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 }),
757 };
758 },
759 .explicit_groups = block_groups,
760 });
761
762 ExecutionContext cc_ctx;
763 cc_ctx.mode = ExecutionMode::DEPENDENCY;
764 DependencyParams params;
765 params.stages = cc_stages;
766 cc_ctx.parameters = params;
767 cc_pipeline.execute(Datum<> {}, cc_ctx);
768
769 cc_pipeline.ensure_shared_buffer(0, 10, 3, GpuBufferBinding::ElementType::UINT32,
771 const std::array<uint32_t, 3> full_grid_indirect {
772 (block_width + 7U) / 8U,
773 (block_height + 7U) / 8U,
774 1U
775 };
776 cc_pipeline.upload_shared_raw(0, 10, reinterpret_cast<const uint8_t*>(full_grid_indirect.data()), full_grid_indirect.size() * sizeof(uint32_t));
777
778 cc_pipeline.swap_shader({ .shader_path = "cc_compress.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(CCCompressPC) });
779 cc_pipeline.set_push_constants(CCCompressPC { .block_width = block_width, .block_height = block_height });
780 cc_pipeline.set_output_dimensions(block_width, block_height);
781 {
782 const auto fence = cc_pipeline.dispatch_async({});
783 foundry.wait_for_fence(fence);
784 foundry.release_fence(fence);
785 }
786 cc_pipeline.clear_output_dimensions();
787
788 cc_pipeline.swap_shader({ .shader_path = "cc_final_label.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(CCFinalLabelPC) });
789 cc_pipeline.stage_image_at(1, seed_input, GpuBufferBinding::ElementType::IMAGE_STORAGE);
790 cc_pipeline.set_push_constants(final_pc);
791 cc_pipeline.set_output_dimensions(w, h);
792 cc_pipeline.prepare_output_image(w, h);
793 {
794 const auto fence = cc_pipeline.dispatch_async({});
795 foundry.wait_for_fence(fence);
796 foundry.release_fence(fence);
797 }
798 cc_pipeline.clear_output_dimensions();
799
800 contexts.pass.result.debug_labels = p.with_colors ? cc_pipeline.get_output_image(0) : nullptr;
801
802 uint32_t compact_count = 0;
803 cc_pipeline.download_shared(0, 5, &compact_count, sizeof(uint32_t));
804 compact_count = std::min(compact_count, k_max_components);
805
806 Kinesis::Vision::ComponentResult cc_result;
807 cc_result.count = compact_count;
808 cc_result.boxes.reserve(compact_count);
809
810 if (compact_count > 0) {
811 std::vector<glm::uvec2> bmin(compact_count);
812 std::vector<glm::uvec2> bmax(compact_count);
813 std::vector<uint32_t> bcount(compact_count);
814 cc_pipeline.download_shared(0, 7, bmin.data(), bmin.size() * sizeof(glm::uvec2));
815 cc_pipeline.download_shared(0, 8, bmax.data(), bmax.size() * sizeof(glm::uvec2));
816 cc_pipeline.download_shared(0, 9, bcount.data(), bcount.size() * sizeof(uint32_t));
817
818 const float inv_w = 1.0F / static_cast<float>(w);
819 const float inv_h = 1.0F / static_cast<float>(h);
820
821 for (uint32_t i = 0; i < compact_count; ++i) {
822 if (bcount[i] == 0)
823 continue;
824 const float x = static_cast<float>(bmin[i].x) * inv_w;
825 const float y = static_cast<float>(bmin[i].y) * inv_h;
826 const float bw = static_cast<float>(bmax[i].x - bmin[i].x + 1) * inv_w;
827 const float bh = static_cast<float>(bmax[i].y - bmin[i].y + 1) * inv_h;
828 cc_result.boxes.push_back({ .x = x, .y = y, .w = bw, .h = bh, .confidence = 1.0F, .label_id = i + 1 });
829 }
830 }
831
832 contexts.pass.result.structured = std::move(cc_result);
833 contexts.pass.result.w = 0;
834 contexts.pass.result.h = 0;
835 }
836
837 bool op_find_contours(
838 VisionGpuContexts& contexts,
839 const FindContoursParams& p)
840 {
841 const auto* prev = contexts.pass.behind();
842 if (!prev || prev->op != VisionOp::ConnectedComponents) {
844 "run_gpu: FindContours requires ConnectedComponents as the immediately preceding step");
845 return false;
846 }
847
848 auto& cc_pipeline = contexts.cc_pipeline;
849 auto w = contexts.pass.w;
850 auto h = contexts.pass.h;
851 auto& foundry = Portal::Graphics::get_shader_foundry();
852
853 cc_pipeline.ensure_shared_buffer(1, 4, static_cast<size_t>(k_max_components) + 1U, GpuBufferBinding::ElementType::UINT32);
854 cc_pipeline.ensure_shared_buffer(1, 5, static_cast<size_t>(k_max_components) * k_max_holes_per_label, GpuBufferBinding::ElementType::UINT32);
855 cc_pipeline.ensure_shared_buffer(1, 6, static_cast<size_t>(k_max_trace_slots) * 2U, GpuBufferBinding::ElementType::UINT32);
856 cc_pipeline.ensure_shared_buffer(1, 7, 1U, GpuBufferBinding::ElementType::UINT32);
857 cc_pipeline.ensure_shared_buffer(1, 8, static_cast<size_t>(k_max_trace_slots) * k_max_points_per_contour * 2U, GpuBufferBinding::ElementType::FLOAT32);
858 cc_pipeline.ensure_shared_buffer(1, 9, 1U, GpuBufferBinding::ElementType::UINT32);
859 cc_pipeline.ensure_shared_buffer(1, 10, static_cast<size_t>(k_max_trace_slots) * 4U, GpuBufferBinding::ElementType::UINT32);
860 cc_pipeline.ensure_shared_buffer(1, 11, static_cast<size_t>(k_max_trace_slots) * 2U, GpuBufferBinding::ElementType::FLOAT32);
861 cc_pipeline.ensure_shared_buffer(2, 0, k_max_components, GpuBufferBinding::ElementType::FLOAT32);
862 cc_pipeline.ensure_shared_buffer(2, 1, k_max_components, GpuBufferBinding::ElementType::FLOAT32);
863
864 {
865 std::vector<uint32_t> owner_reset(static_cast<size_t>(k_max_components) + 1U, CC_UNCLAIMED_HOST);
866 cc_pipeline.upload_shared_raw(1, 4, reinterpret_cast<const uint8_t*>(owner_reset.data()), owner_reset.size() * sizeof(uint32_t));
867
868 std::vector<uint32_t> hole_owner_reset(static_cast<size_t>(k_max_components) * k_max_holes_per_label, CC_UNCLAIMED_HOST);
869 cc_pipeline.upload_shared_raw(1, 5, reinterpret_cast<const uint8_t*>(hole_owner_reset.data()), hole_owner_reset.size() * sizeof(uint32_t));
870
871 const uint32_t zero = 0;
872 cc_pipeline.upload_shared_raw(1, 7, reinterpret_cast<const uint8_t*>(&zero), sizeof(uint32_t));
873 cc_pipeline.upload_shared_raw(1, 9, reinterpret_cast<const uint8_t*>(&zero), sizeof(uint32_t));
874 }
875
876 auto max_points = p.max_points_per_contour > 0 ? std::min<uint32_t>(p.max_points_per_contour, k_max_points_per_contour) : k_max_points_per_contour;
877 cc_pipeline.swap_shader({ .shader_path = "contour_march.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(ContourMarchPC) });
878 cc_pipeline.stage_image_at(1, contexts.source, GpuBufferBinding::ElementType::IMAGE_SAMPLED);
879 cc_pipeline.prepare_output_image(w, h);
880 cc_pipeline.set_push_constants(ContourMarchPC {
881 .width = w,
882 .height = h,
883 .max_components = k_max_components,
884 .max_points_per_contour = max_points,
885 .max_holes_per_label = k_max_holes_per_label,
886 .phase = 0U,
887 .min_area = p.min_area,
888 .compacted_count = 0U });
889
890 cc_pipeline.set_output_dimensions(w, h);
891 {
892 const auto fence = cc_pipeline.dispatch_async({});
893 foundry.wait_for_fence(fence);
894 foundry.release_fence(fence);
895 }
896
897 cc_pipeline.set_push_constants(ContourMarchPC {
898 .width = w,
899 .height = h,
900 .max_components = k_max_components,
901 .max_points_per_contour = max_points,
902 .max_holes_per_label = k_max_holes_per_label,
903 .phase = 1U,
904 .min_area = p.min_area,
905 .compacted_count = 0U });
906 {
907 const auto fence = cc_pipeline.dispatch_async({});
908 foundry.wait_for_fence(fence);
909 foundry.release_fence(fence);
910 }
911
912 cc_pipeline.clear_output_dimensions();
913
914 cc_pipeline.swap_shader({ .shader_path = "contour_compact.comp.spv", .workgroup_size = { 256, 1, 1 }, .push_constant_size = sizeof(ContourCompactPC) });
915 cc_pipeline.set_push_constants(ContourCompactPC { .max_components = k_max_components, .max_holes_per_label = k_max_holes_per_label });
916 const uint32_t total_owner_slots = k_max_components * (1U + k_max_holes_per_label);
917 cc_pipeline.set_output_dimensions(total_owner_slots, 1);
918 {
919 const auto fence = cc_pipeline.dispatch_async({});
920 foundry.wait_for_fence(fence);
921 foundry.release_fence(fence);
922 }
923 cc_pipeline.clear_output_dimensions();
924
925 uint32_t compacted_count = 0;
926 cc_pipeline.download_shared(1, 7, &compacted_count, sizeof(uint32_t));
927
928 cc_pipeline.swap_shader({ .shader_path = "contour_march.comp.spv", .workgroup_size = { 256, 1, 1 }, .push_constant_size = sizeof(ContourMarchPC) });
929 cc_pipeline.set_push_constants(ContourMarchPC {
930 .width = w,
931 .height = h,
932 .max_components = k_max_components,
933 .max_points_per_contour = max_points,
934 .max_holes_per_label = k_max_holes_per_label,
935 .phase = 2U,
936 .min_area = p.min_area,
937 .compacted_count = compacted_count });
938 cc_pipeline.set_output_dimensions(std::max(compacted_count, 1U), 1);
939 {
940 const auto fence = cc_pipeline.dispatch_async({});
941 foundry.wait_for_fence(fence);
942 foundry.release_fence(fence);
943 }
944 cc_pipeline.clear_output_dimensions();
945
946 if (p.max_contours > 0U) {
947 constexpr uint32_t k = 12U;
948 constexpr uint32_t total_passes = k * (k + 1U) / 2U;
949
950 cc_pipeline.swap_shader(config_from_spec(
951 ShaderSpec::Assemble {}
952 .tmpl(KernelTemplate::BitonicSort)
953 .start_set(2)
954 .ssbo("keys", BindingDirection::InOut, Kakshya::GpuDataFormat::FLOAT32)
955 .ssbo("indices", BindingDirection::InOut, Kakshya::GpuDataFormat::FLOAT32)
959 .pc("descending", Kakshya::GpuDataFormat::UINT32)
960 .workgroup(256)
961 .build()));
962
963 cc_pipeline.set_output_dimensions(k_max_components, 1U);
964
965 ExecutionContext bitonic_ctx;
966 bitonic_ctx.mode = ExecutionMode::CHAINED;
967 bitonic_ctx.parameters = ChainedParams {
968 .pass_count = total_passes,
969 .pc_updater = [k](uint32_t p_idx, void* pc_ptr) {
970 uint32_t stage = 0, pass = 0, remaining = p_idx;
971 for (uint32_t s = 0; s < k; ++s) {
972 if (remaining <= s) {
973 stage = s;
974 pass = remaining;
975 break;
976 }
977 remaining -= (s + 1);
978 }
979 struct PC {
980 uint32_t stage, pass, count, descending;
981 };
982 *static_cast<PC*>(pc_ptr) = { .stage = stage, .pass = pass, .count = k_max_components, .descending = 1U };
983 },
984 };
985
986 cc_pipeline.execute(Datum<std::vector<Kakshya::DataVariant>> {}, bitonic_ctx);
987 cc_pipeline.clear_output_dimensions();
988 }
989
990 if (p.as_image) {
991 cc_pipeline.swap_shader({ .shader_path = "contour_render_clear.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(ContourClearPC) });
992 cc_pipeline.prepare_output_image(w, h);
993 cc_pipeline.set_push_constants(ContourClearPC { .width = w, .height = h });
994 cc_pipeline.set_output_dimensions(w, h);
995 {
996 const auto fence = cc_pipeline.dispatch_async({});
997 foundry.wait_for_fence(fence);
998 foundry.release_fence(fence);
999 }
1000 cc_pipeline.clear_output_dimensions();
1001
1002 cc_pipeline.swap_shader({ .shader_path = "contour_render.comp.spv", .workgroup_size = { 256, 1, 1 }, .push_constant_size = sizeof(ContourRenderPC) });
1003 cc_pipeline.set_push_constants(ContourRenderPC { .width = w, .height = h, .max_components = k_max_components, .max_points_per_contour = k_max_points_per_contour, .max_contours = p.max_contours });
1004 const uint32_t render_slots = p.max_contours > 0U ? std::min(p.max_contours, k_max_components) : k_max_trace_slots;
1005 cc_pipeline.set_output_dimensions(render_slots * k_max_points_per_contour, 1U);
1006 {
1007 const auto fence = cc_pipeline.dispatch_async({});
1008 foundry.wait_for_fence(fence);
1009 foundry.release_fence(fence);
1010 }
1011 cc_pipeline.clear_output_dimensions();
1012
1013 contexts.pass.result.debug_contours = cc_pipeline.get_output_image(0);
1014 contexts.pass.result.structured = std::monostate {};
1015 contexts.pass.result.w = 0;
1016 contexts.pass.result.h = 0;
1017 return true;
1018 }
1019
1020 std::vector<glm::uvec4> meta(compacted_count);
1021 cc_pipeline.download_shared(1, 10, meta.data(), compacted_count * sizeof(glm::uvec4));
1022 std::vector<glm::vec2> area_perim(compacted_count);
1023 cc_pipeline.download_shared(1, 11, area_perim.data(), compacted_count * sizeof(glm::vec2));
1024 uint32_t points_written = 0;
1025 cc_pipeline.download_shared(1, 9, &points_written, sizeof(uint32_t));
1026 std::vector<glm::vec2> flat_points_full(points_written);
1027
1028 if (points_written > 0)
1029 cc_pipeline.download_shared(1, 8, flat_points_full.data(), static_cast<size_t>(points_written) * sizeof(glm::vec2));
1030
1031 std::vector<uint32_t> order;
1032 if (p.max_contours > 0U) {
1033 const uint32_t take = std::min(p.max_contours, compacted_count);
1034 std::vector<float> sorted_indices(take);
1035 cc_pipeline.download_shared(2, 1, sorted_indices.data(), take * sizeof(float));
1036 order.reserve(take);
1037 for (float f : sorted_indices)
1038 order.push_back(static_cast<uint32_t>(f));
1039 } else {
1040 order.resize(compacted_count);
1041 for (uint32_t i = 0; i < compacted_count; ++i)
1042 order[i] = i;
1043 }
1044
1045 std::vector<Kinesis::Vision::Contour> out_contours;
1046 out_contours.reserve(order.size());
1047
1048 for (uint32_t idx : order) {
1049 if (idx >= compacted_count)
1050 continue;
1051 const auto& m = meta[idx];
1052 if (m.y < 3)
1053 continue;
1054 if (m.x > points_written || m.y > points_written - m.x)
1055 continue;
1056
1057 std::vector<glm::vec2> pts(
1058 flat_points_full.begin() + m.x,
1059 flat_points_full.begin() + m.x + m.y);
1060 const glm::vec2 ap = area_perim[idx];
1061 out_contours.push_back({ .points = std::move(pts), .area = ap.x, .perimeter = ap.y, .parent_label = m.z });
1062 }
1063
1064 contexts.pass.result.structured = std::move(out_contours);
1065 contexts.pass.result.w = 0;
1066 contexts.pass.result.h = 0;
1067 return true;
1068 }
1069
1070 /**
1071 * @brief Release the previous run's ingest fences (instant: long signalled).
1072 */
1073 void reap_ingest_fences(VisionGpuContexts& contexts)
1074 {
1075 auto& foundry = Portal::Graphics::get_shader_foundry();
1076 for (auto* slot : { &contexts.ingest_fence, &contexts.ingest_barrier_fence }) {
1077 if (*slot != Portal::Graphics::INVALID_FENCE) {
1078 foundry.wait_for_fence(*slot);
1079 foundry.release_fence(*slot);
1081 }
1082 }
1083 }
1084
1085 /**
1086 * @brief Convert a non-storage seed frame to an rgba32f storage image.
1087 *
1088 * Frames already carrying storage usage pass through. Otherwise
1089 * vision_ingest.comp samples the frame as a texture (sampler does the
1090 * unorm/sRGB decode) into contexts.ingest's rgba32f output. The dispatch
1091 * is not awaited — a trailing compute barrier gives the memory
1092 * dependency, fences are reaped on the next run.
1093 */
1094 std::shared_ptr<Core::VKImage> op_ingest(
1095 VisionGpuContexts& contexts,
1096 const std::shared_ptr<Core::VKImage>& frame,
1097 uint32_t w, uint32_t h)
1098 {
1099 if (!frame || !frame->is_initialized())
1100 return frame;
1101 if (static_cast<bool>(frame->get_usage_flags() & vk::ImageUsageFlagBits::eStorage))
1102 return frame;
1103
1104 auto& foundry = Portal::Graphics::get_shader_foundry();
1105 auto& ingest = contexts.ingest;
1106
1107 ingest.swap_shader({
1108 .shader_path = "vision_ingest.comp.spv",
1109 .workgroup_size = k_wg2d,
1110 .push_constant_size = sizeof(IngestPC),
1111 });
1112 ingest.stage_image(frame);
1113 ingest.set_push_constants(IngestPC { .width = w, .height = h });
1114 ingest.prepare_output_image(w, h);
1115 ingest.set_output_dimensions(w, h);
1116 contexts.ingest_fence = ingest.dispatch_async({});
1117 ingest.clear_output_dimensions();
1118
1119 auto out = ingest.get_output_image(0);
1120
1121 if (out) {
1122 const auto bcmd = foundry.begin_commands(
1124 foundry.image_barrier(bcmd, out->get_image(),
1125 vk::ImageLayout::eGeneral, vk::ImageLayout::eGeneral,
1126 vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eShaderRead,
1127 vk::PipelineStageFlagBits::eComputeShader,
1128 vk::PipelineStageFlagBits::eComputeShader);
1129 contexts.ingest_barrier_fence = foundry.submit_async(bcmd);
1130 }
1131
1132 return out;
1133 }
1134
1135} // namespace
1136
1138 : pixel {
1140 Portal::Graphics::ImageFormat::RGBA32F,
1141 TextureExecutionContext::OutputMode::IMAGE,
1142 1,
1143 std::vector<GpuBufferBinding> {
1144 { .set = 0, .binding = 2, .direction = GpuBufferBinding::Direction::INPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1145 },
1147 }
1148 , structured {
1149 GpuComputeConfig {},
1151 TextureExecutionContext::OutputMode::SCALAR,
1152 0,
1153 std::vector<GpuBufferBinding> {
1154 { .set = 0, .binding = 1, .direction = GpuBufferBinding::Direction::OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1155 { .set = 0, .binding = 2, .direction = GpuBufferBinding::Direction::OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1156 { .set = 0, .binding = 3, .direction = GpuBufferBinding::Direction::OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1157 { .set = 0, .binding = 4, .direction = GpuBufferBinding::Direction::OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1158 { .set = 1, .binding = 0, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1159 { .set = 1, .binding = 1, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1160 },
1161 GpuBufferBinding::ElementType::IMAGE_STORAGE,
1162 0,
1163 }
1164 , labels {
1167 TextureExecutionContext::OutputMode::IMAGE,
1168 1,
1169 std::vector<GpuBufferBinding> {
1170 { .set = 0, .binding = 2, .direction = GpuBufferBinding::Direction::OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1171 { .set = 0, .binding = 4, .direction = GpuBufferBinding::Direction::OUTPUT, .element_type = GpuBufferBinding::ElementType::IMAGE_STORAGE },
1172 },
1173 GpuBufferBinding::ElementType::IMAGE_STORAGE,
1174 }
1175 , cc_pipeline {
1178 TextureExecutionContext::OutputMode::IMAGE,
1179 1,
1180 std::vector<GpuBufferBinding> {
1181 { .set = 0, .binding = 2, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1182 { .set = 0, .binding = 3, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1183 { .set = 0, .binding = 4, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1184 { .set = 0, .binding = 5, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1185 { .set = 0, .binding = 6, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1186 { .set = 0, .binding = 7, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1187 { .set = 0, .binding = 8, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1188 { .set = 0, .binding = 9, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1189 { .set = 0, .binding = 10, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1190 { .set = 1, .binding = 0, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1191 { .set = 1, .binding = 1, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1192 { .set = 1, .binding = 2, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1193 { .set = 1, .binding = 3, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1194 { .set = 1, .binding = 4, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1195 { .set = 1, .binding = 5, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1196 { .set = 1, .binding = 6, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1197 { .set = 1, .binding = 7, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1198 { .set = 1, .binding = 8, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1199 { .set = 1, .binding = 9, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1200 { .set = 1, .binding = 10, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 },
1201 { .set = 1, .binding = 11, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1202 { .set = 2, .binding = 0, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1203 { .set = 2, .binding = 1, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::FLOAT32 },
1204 },
1205 GpuBufferBinding::ElementType::IMAGE_STORAGE,
1206 0,
1207 }
1208 , ingest {
1211 TextureExecutionContext::OutputMode::IMAGE,
1212 1,
1213 std::vector<GpuBufferBinding> {},
1214 GpuBufferBinding::ElementType::IMAGE_SAMPLED,
1215 0,
1216 }
1217{
1218 structured.set_output_size(1, sizeof(uint32_t));
1219 structured.set_output_size(2, static_cast<size_t>(4096) * 4 * sizeof(float));
1220 structured.set_output_size(3, static_cast<size_t>(256) * sizeof(uint32_t));
1221 structured.set_output_size(4, sizeof(uint32_t));
1222}
1223
1224// ============================================================================
1225// vision_gpu_config
1226// ============================================================================
1227
1228GpuComputeConfig VisionGpuExecutor::config(VisionOp op, const VisionParams& /*params*/)
1229{
1230 switch (op) {
1231 case VisionOp::Threshold: {
1232 const auto spec = ShaderSpec::Assemble {}
1233 .storage_image("out", BindingDirection::Output)
1234 .storage_image("src", BindingDirection::Input)
1235 .pc("threshold")
1236 .op(KernelOp::CompareGE)
1237 .workgroup(k_wg2d[0], k_wg2d[1])
1238 .build();
1239 return config_from_spec(spec);
1240 }
1241 case VisionOp::RgbaToGray: {
1242 const auto spec = ShaderSpec::Assemble {}
1243 .storage_image("out", BindingDirection::Output)
1244 .storage_image("src", BindingDirection::Input)
1245 .pc("wr")
1246 .pc("wg")
1247 .pc("wb")
1248 .pc("wa")
1249 .op(KernelOp::ChannelDot)
1250 .workgroup(k_wg2d[0], k_wg2d[1])
1251 .build();
1252 return config_from_spec(spec);
1253 }
1254 case VisionOp::GrayToRgba: {
1255 const auto spec = ShaderSpec::Assemble {}
1256 .storage_image("out", BindingDirection::Output)
1257 .storage_image("src", BindingDirection::Input)
1258 .op(KernelOp::ChannelReplicate)
1259 .workgroup(k_wg2d[0], k_wg2d[1])
1260 .build();
1261 return config_from_spec(spec);
1262 }
1263 case VisionOp::GaussianBlur: {
1264 const auto spec = ShaderSpec::Assemble {}
1265 .tmpl(KernelTemplate::Convolve2D)
1266 .storage_image("out", BindingDirection::Output)
1267 .storage_image("src", BindingDirection::Input)
1268 .ssbo("kernel", BindingDirection::Input, Kakshya::GpuDataFormat::FLOAT32)
1272 .workgroup(k_wg2d[0], k_wg2d[1])
1273 .build();
1274 return config_from_spec(spec);
1275 }
1276 case VisionOp::NormalizeRange: {
1277 const auto spec = ShaderSpec::Assemble {}
1278 .storage_image("out", BindingDirection::Output)
1279 .storage_image("src", BindingDirection::Input)
1280 .pc("scale")
1281 .pc("offset")
1282 .op(KernelOp::ScaleOffset)
1283 .workgroup(k_wg2d[0], k_wg2d[1])
1284 .build();
1285 return config_from_spec(spec);
1286 }
1287 case VisionOp::NormalizeInplace: {
1288 const auto spec = ShaderSpec::Assemble {}
1289 .storage_image("out", BindingDirection::Output)
1290 .storage_image("src", BindingDirection::Input)
1291 .op(KernelOp::Scale)
1292 .workgroup(k_wg2d[0], k_wg2d[1])
1293 .build();
1294 return config_from_spec(spec);
1295 }
1296 case VisionOp::Canny: {
1297 const auto spec = ShaderSpec::Assemble {}
1298 .storage_image("out", BindingDirection::Output)
1299 .storage_image("src", BindingDirection::Input)
1300 .pc("threshold")
1301 .pc("value")
1302 .op(KernelOp::CompareGEPreserve)
1303 .workgroup(k_wg2d[0], k_wg2d[1])
1304 .build();
1305 return config_from_spec(spec);
1306 }
1307 case VisionOp::RgbaToHsv:
1308 return { .shader_path = "rgba_to_hsv.comp.spv", .workgroup_size = k_wg2d };
1309 case VisionOp::Downsample2x:
1310 return { .shader_path = "downsample_2x.comp.spv", .workgroup_size = k_wg2d };
1311 case VisionOp::FilterSeparable:
1312 return { .shader_path = "filter_separable.comp.spv", .workgroup_size = k_wg2d };
1313 case VisionOp::Sobel:
1314 return { .shader_path = "sobel.comp.spv", .workgroup_size = k_wg2d };
1315 case VisionOp::Scharr:
1316 return { .shader_path = "scharr.comp.spv", .workgroup_size = k_wg2d };
1317 case VisionOp::Erode:
1318 return { .shader_path = "erode.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(MorphPC) };
1319 case VisionOp::Dilate:
1320 return { .shader_path = "dilate.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(MorphPC) };
1321 case VisionOp::Open:
1322 return { .shader_path = "open.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(MorphPC) };
1323 case VisionOp::Close:
1324 return { .shader_path = "close.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(MorphPC) };
1325 case VisionOp::MorphGradient:
1326 return { .shader_path = "morph_gradient.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(MorphPC) };
1327 case VisionOp::HarrisResponse:
1328 return { .shader_path = "harris_response.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(HarrisPC) };
1329 case VisionOp::ExtractPeaks:
1330 return { .shader_path = "extract_peaks.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(ExtractPeaksPC) };
1331 case VisionOp::ConnectedComponents:
1332 return { .shader_path = "cc_colorize.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(uint32_t) * 2 };
1333 case VisionOp::FindContours:
1334 return { .shader_path = "contour_segments.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(ContourSegmentsPC) };
1335 case VisionOp::ThresholdAdaptive:
1336 return { .shader_path = "threshold_adaptive.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(ThresholdAdaptivePC) };
1337 case VisionOp::ThresholdOtsu:
1338 return { .shader_path = "threshold_otsu.comp.spv", .workgroup_size = { 256, 1, 1 } };
1339 default:
1341 }
1342}
1343
1344void VisionGpuExecutor::reset()
1345{
1346 if (!m_contexts)
1347 return;
1348
1349 auto& contexts = *m_contexts;
1350
1351 reap_ingest_fences(contexts);
1352
1353 if (contexts.suspended.is_active()) {
1354 auto& foundry = Portal::Graphics::get_shader_foundry();
1355 foundry.wait_for_fence(contexts.suspended.fence);
1356 foundry.release_fence(contexts.suspended.fence);
1357 contexts.suspended.fence = Portal::Graphics::INVALID_FENCE;
1358 }
1359
1360 contexts.pass.sequence = nullptr;
1361 contexts.pass.index = 0;
1362 contexts.pass.result = Kinesis::Vision::VisionResult {};
1363 contexts.pass.current.reset();
1364 contexts.pass.forget();
1365 contexts.pass.completed.clear();
1366
1367 contexts.source.reset();
1368 contexts.bound_staged.reset();
1369}
1370
1371// ============================================================================
1372// run_gpu
1373// ============================================================================
1374
1375VisionResult VisionGpuExecutor::run(
1376 VisionGpuContexts& contexts,
1377 const VisionSequence& sequence,
1378 const std::shared_ptr<Core::VKImage>& image,
1379 uint32_t w, uint32_t h)
1380{
1381 auto& pixel_ctx = contexts.pixel;
1382 auto& structured_ctx = contexts.structured;
1383 auto& label_ctx = contexts.labels;
1384 auto& cc_pipeline = contexts.cc_pipeline;
1385
1386 auto& foundry = Portal::Graphics::get_shader_foundry();
1387 std::unordered_map<size_t, CompletedOp> completed_ops;
1388 size_t begin = 0;
1389
1390 if (contexts.suspended.is_active()) {
1391 if (&sequence != contexts.pass.sequence) {
1393 "run_gpu: polling a suspension with a different sequence; "
1394 "the walk continues on the sequence the run started from");
1395 }
1396
1397 if (!foundry.is_fence_signaled(contexts.suspended.fence)) {
1398 VisionResult pending;
1399 pending.status = VisionStatus::SUSPENDED;
1400 pending.suspended_at = contexts.pass.index;
1401 return pending;
1402 }
1403
1404 foundry.release_fence(contexts.suspended.fence);
1406
1407 begin = contexts.pass.index + 1;
1408 } else {
1409 reap_ingest_fences(contexts);
1410 contexts.pass.begin(sequence, w, h);
1411 const auto seed = op_ingest(contexts, image, w, h);
1412 contexts.pass.current = seed;
1413 contexts.source = seed;
1414 }
1415
1416 for (contexts.pass.index = begin; contexts.pass.index < sequence.steps.size(); ++contexts.pass.index) {
1417 const auto& step = sequence.steps[contexts.pass.index];
1418
1419 const uint32_t w = contexts.pass.w;
1420 const uint32_t h = contexts.pass.h;
1421
1422 const auto cfg = config(step.op, step.params);
1423
1424 if (cfg.shader_id == Portal::Graphics::INVALID_SHADER && cfg.shader_path.empty()) {
1426 "run_gpu: no GPU implementation for VisionOp {}",
1427 static_cast<int>(step.op));
1428 return VisionResult {};
1429 }
1430
1431 if (cfg.shader_id != contexts.bound_config.shader_id
1432 || cfg.shader_path != contexts.bound_config.shader_path) {
1433 pixel_ctx.swap_shader(cfg);
1434 contexts.bound_config = cfg;
1435 }
1436 if (contexts.pass.current != contexts.bound_staged) {
1437 pixel_ctx.stage_image(contexts.pass.current);
1438 contexts.bound_staged = contexts.pass.current;
1439 }
1440 if (w != contexts.pass.storage_w || h != contexts.pass.storage_h) {
1441 pixel_ctx.prepare_output_image(w, h);
1442 contexts.pass.storage_w = w;
1443 contexts.pass.storage_h = h;
1444 }
1445 pixel_ctx.set_output_dimensions(w, h);
1446
1447 switch (step.op) {
1448 case VisionOp::Downsample2x: {
1449 const uint32_t new_w = std::max(1U, w / 2);
1450 const uint32_t new_h = std::max(1U, h / 2);
1451
1452 pixel_ctx.prepare_output_image(new_w, new_h);
1453 pixel_ctx.set_output_dimensions(new_w, new_h);
1454 {
1455 const auto f = pixel_ctx.dispatch_async({});
1456 foundry.wait_for_fence(f);
1457 foundry.release_fence(f);
1458 }
1459 pixel_ctx.clear_output_dimensions();
1460
1461 auto downsampled = pixel_ctx.get_output_image(0);
1462 completed_ops[Kinesis::Vision::hash_vision_step(step.op, step.params)] = { .output = downsampled, .input = contexts.pass.current };
1463 contexts.pass.current = downsampled;
1464 contexts.bound_staged = contexts.pass.current;
1465
1466 contexts.pass.set_geometry(new_w, new_h);
1467 contexts.pass.storage_w = new_w;
1468 contexts.pass.storage_h = new_h;
1469
1470 continue;
1471 }
1472 case VisionOp::Threshold:
1473 pixel_ctx.set_push_constants(ThresholdPC {
1474 .value = std::get<ThresholdParams>(step.params).value });
1475 break;
1476 case VisionOp::NormalizeRange: {
1477 const auto& p = std::get<NormalizeRangeParams>(step.params);
1478 const float scale = (p.hi > p.lo) ? 1.0F / (p.hi - p.lo) : 1.0F;
1479 const float off = (p.hi > p.lo) ? -p.lo / (p.hi - p.lo) : 0.0F;
1480 pixel_ctx.set_push_constants(NormalizePC { .scale = scale, .offset = off });
1481 break;
1482 }
1483 case VisionOp::RgbaToGray:
1484 pixel_ctx.set_push_constants(RgbaToGrayPC {
1485 .wr = 0.299F, .wg = 0.587F, .wb = 0.114F, .wa = 0.0F });
1486 break;
1487 case VisionOp::GaussianBlur: {
1488 const auto& p = std::get<GaussianBlurParams>(step.params);
1489 const auto radius = static_cast<uint32_t>(std::ceil(p.sigma * 3.0F));
1490 const auto& weights = gaussian_kernel_1d(radius, p.sigma);
1491 pixel_ctx.set_binding_data(2, std::span<const float>(weights));
1492 pixel_ctx.set_push_constants(GaussianPC { .radius = radius, .width = w, .height = h });
1493 break;
1494 }
1495 case VisionOp::Erode:
1496 case VisionOp::Dilate:
1497 case VisionOp::MorphGradient:
1498 pixel_ctx.set_push_constants(MorphPC {
1499 .radius = std::get<MorphParams>(step.params).radius });
1500 break;
1501 case VisionOp::ThresholdAdaptive: {
1502 const auto& p = std::get<ThresholdAdaptiveParams>(step.params);
1503 pixel_ctx.set_push_constants(ThresholdAdaptivePC { .block_size = p.block_size, .offset = p.offset });
1504 break;
1505 }
1506 case VisionOp::ThresholdOtsu: {
1507 contexts.pass.completed[Kinesis::Vision::hash_vision_step(step.op, step.params)] = op_threshold_otsu(contexts);
1508 continue;
1509 }
1510 case VisionOp::Open:
1511 case VisionOp::Close: {
1512 contexts.pass.completed[Kinesis::Vision::hash_vision_step(step.op, step.params)] = op_open_close(contexts, step.op, std::get<MorphParams>(step.params));
1513 continue;
1514 }
1515 case VisionOp::Canny: {
1516 auto done = op_canny(contexts, step.params, std::get<CannyParams>(step.params));
1517 contexts.pass.completed[Kinesis::Vision::hash_vision_step(step.op, step.params)] = done;
1518 continue;
1519 }
1520 case VisionOp::HarrisResponse: {
1521 contexts.pass.completed[Kinesis::Vision::hash_vision_step(step.op, step.params)] = op_harris_response(contexts, std::get<HarrisParams>(step.params));
1522 continue;
1523 }
1524 case VisionOp::ExtractPeaks: {
1525 op_extract_peaks(contexts, std::get<ExtractPeaksParams>(step.params));
1526 continue;
1527 }
1528 case VisionOp::ConnectedComponents: {
1529 if (!contexts.pass.current) {
1530 continue;
1531 }
1532 op_connected_components(contexts, std::get<Kinesis::Vision::ConnectedComponentsParams>(step.params));
1533 continue;
1534 }
1535 case VisionOp::FindContours: {
1536 if (!op_find_contours(contexts,
1537 std::get<Kinesis::Vision::FindContoursParams>(step.params)))
1538 return VisionResult {};
1539 continue;
1540 }
1541 default:
1542 break;
1543 }
1544
1545 auto dispatch_input = contexts.pass.current;
1546 const auto fence = pixel_ctx.dispatch_async({});
1547
1548 if (step.deferred) {
1549 pixel_ctx.clear_output_dimensions();
1550 contexts.pass.current = pixel_ctx.get_output_image(0);
1551 contexts.bound_staged = contexts.pass.current;
1552
1553 const Kinesis::Vision::GpuVisionPass::Completed done {
1554 .output = contexts.pass.current,
1555 .input = dispatch_input
1556 };
1557 contexts.pass.completed[Kinesis::Vision::hash_vision_step(step.op, step.params)] = done;
1558
1559 contexts.suspended.fence = fence;
1560
1561 VisionResult pending;
1562 pending.status = VisionStatus::SUSPENDED;
1563 pending.suspended_at = contexts.pass.index;
1564 return pending;
1565 }
1566
1567 foundry.wait_for_fence(fence);
1568 foundry.release_fence(fence);
1569
1570 pixel_ctx.clear_output_dimensions();
1571 contexts.pass.current = pixel_ctx.get_output_image(0);
1572 contexts.bound_staged = contexts.pass.current;
1573 completed_ops[Kinesis::Vision::hash_vision_step(step.op, step.params)] = { .output = contexts.pass.current, .input = dispatch_input };
1574 }
1575
1576 return std::move(contexts.pass.result);
1577}
1578
1579VisionResult VisionGpuExecutor::run(
1580 const VisionSequence& sequence,
1581 const std::shared_ptr<Core::VKImage>& image,
1582 uint32_t w, uint32_t h)
1583{
1584 if (!m_contexts)
1585 m_contexts = std::make_unique<VisionGpuContexts>();
1586
1587 return run(*m_contexts, sequence, image, w, h);
1588}
1589
1590} // namespace MayaFlux::Yantra
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
vk::Fence fence
Core::GlobalInputConfig input
Definition Config.cpp:38
IO::ImageData image
Definition Decoder.cpp:64
float radius
uint32_t h
Definition InkPress.cpp:28
size_t a
size_t b
size_t count
uint32_t block_width
float value
float scale
float lo
uint32_t max_holes_per_label
std::shared_ptr< Core::VKImage > output
float threshold
float offset
uint32_t max_keypoints
uint32_t block_height
uint32_t phase
uint32_t nms_radius
float wb
uint32_t max_points_per_contour
uint32_t pass
float wg
uint32_t max_components
uint32_t block_size
float k
float min_area
uint32_t export_labels
float wa
uint32_t compacted_count
float sigma
uint32_t max_contours
float wr
float hi
uint32_t max_segments
uint32_t lut_size
GPU execution layer for Kinesis::Vision::VisionSequence.
uint32_t width
uint32_t height
Assemble & op(KernelOp o)
Set the named operation the emitter will lower to SPIR-V.
ShaderSpec build()
Finalise and return the ShaderSpec.
Assemble & pc(std::string name, Kakshya::GpuDataFormat format)
Declare a push constant field with explicit format.
Assemble & ssbo(std::string name, BindingDirection direction, Kakshya::GpuDataFormat format, Kakshya::DataModality modality=Kakshya::DataModality::SCALAR_F32)
Declare an SSBO binding.
Assemble & workgroup(uint32_t x, uint32_t y=1, uint32_t z=1)
Override workgroup size.
Assemble & tmpl(KernelTemplate t)
Set the kernel template.
Assemble & storage_image(std::string name, BindingDirection direction=BindingDirection::Output)
Declare a storage image binding (image2D).
Fluent assembler producing a ShaderSpec.
GpuExecutionContext specialisation for image compute shaders.
static GpuComputeConfig config(Kinesis::Vision::VisionOp op, const Kinesis::Vision::VisionParams &params)
GpuComputeConfig for a given VisionOp and its parameters.
void run()
Definition main.cpp:22
@ ComputeMatrix
Compute operations (Yantra - algorithms, matrices, DSP)
@ Yantra
DSP algorithms, computational units, matrix operations, Grammar.
std::vector< double > sum(std::span< const double > data, size_t n_windows, uint32_t hop_size, uint32_t window_size)
Sum per window.
Definition Analysis.cpp:469
size_t hash_vision_step(VisionOp op, const VisionParams &params)
Hash a VisionStep's op and parameters together.
Definition VisionOp.hpp:367
std::variant< std::monostate, ThresholdParams, ThresholdAdaptiveParams, NormalizeRangeParams, GaussianBlurParams, FilterSeparableParams, CannyParams, MorphParams, HarrisParams, ExtractPeaksParams, TrackKeypointsParams, ConnectedComponentsParams, FindContoursParams > VisionParams
Parameter variant covering all ops that carry parameters.
Definition VisionOp.hpp:148
VisionOp
Named operations available in a VisionSequence.
Definition VisionOp.hpp:29
constexpr ShaderID INVALID_SHADER
constexpr FenceID INVALID_FENCE
MAYAFLUX_API ShaderFoundry & get_shader_foundry()
Get the global shader compiler instance.
ImageFormat
User-friendly image format enum.
@ RGBA32F
Four channel 32-bit float.
@ INDIRECT
Indirect draw/dispatch buffer (device-local)
Portal::Graphics::GpuComputeConfig GpuComputeConfig
@ CHAINED
Part of a sequential chain.
@ DEPENDENCY
Part of dependency graph.
GpuComputeConfig config_from_spec(const Portal::Graphics::ShaderSpec &spec)
Derive a GpuComputeConfig from a ShaderSpec.
static constexpr DomainSpec Graphics
Domain constant for Graphics domain.
Definition Creator.hpp:318
void begin(const VisionSequence &seq, uint32_t width, uint32_t height)
Reset the walk band for a fresh run.
std::unordered_map< size_t, Completed > completed
void set_geometry(uint32_t width, uint32_t height) noexcept
Result of executing a VisionSequence on one frame.
Ordered sequence of VisionSteps describing a complete vision pipeline.
Definition VisionOp.hpp:169
enum MayaFlux::Portal::Graphics::GpuBufferBinding::Direction INPUT
enum MayaFlux::Portal::Graphics::GpuBufferBinding::ElementType FLOAT32
Declares a single storage buffer or image binding a compute shader expects.
Plain-data description of the compute shader to dispatch.
GpuComputeConfig bound_config
Shader currently bound on pixel, and the image staged into it.
TextureExecutionContext labels
Image + aux SSBO.
VisionGpuContexts()
Construct all three contexts in place with the one correct binding layout for every currently GPU-imp...
Kinesis::Vision::GpuVisionPass pass
Walk state for the current run: sequence position, geometry, working image, and the result under cons...
std::shared_ptr< Core::VKImage > source
Input image the current walk started from.
std::shared_ptr< Core::VKImage > bound_staged
TextureExecutionContext pixel
Image pipeline.
TextureExecutionContext structured
Buffer-only readback.
Fixed set of TextureExecutionContexts covering every GPU-implemented VisionOp shape.