MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VisionFlowDispatch.cpp
Go to the documentation of this file.
1#include "DispatchLayouts.hpp"
2
3namespace MayaFlux::Yantra {
4
5using namespace VisionInternal;
6
7using namespace Portal::Graphics;
8using namespace Kinesis::Vision;
9
10namespace {
11
12 constexpr FlowBufferSpec k_flow_det_count { .set = 0, .binding = 1, .type = GpuBufferBinding::ElementType::UINT32 };
13 constexpr FlowBufferSpec k_flow_det_points { .set = 0, .binding = 2, .type = GpuBufferBinding::ElementType::FLOAT32 };
14 constexpr FlowBufferSpec k_flow_prev_count { .set = 1, .binding = 0, .type = GpuBufferBinding::ElementType::UINT32 };
15 constexpr FlowBufferSpec k_flow_prev_points { .set = 1, .binding = 1, .type = GpuBufferBinding::ElementType::FLOAT32 };
16 constexpr FlowBufferSpec k_flow_tracks { .set = 1, .binding = 2, .type = GpuBufferBinding::ElementType::FLOAT32 };
17 constexpr FlowBufferSpec k_flow_meta { .set = 1, .binding = 3, .type = GpuBufferBinding::ElementType::UINT32 };
18 constexpr FlowBufferSpec k_flow_next_points { .set = 1, .binding = 4, .type = GpuBufferBinding::ElementType::FLOAT32 };
19 constexpr FlowBufferSpec k_flow_counters { .set = 1, .binding = 5, .type = GpuBufferBinding::ElementType::UINT32 };
20 constexpr FlowBufferSpec k_flow_histogram { .set = 1, .binding = 6, .type = GpuBufferBinding::ElementType::UINT32 };
21 constexpr FlowBufferSpec k_flow_grid { .set = 1, .binding = 7, .type = GpuBufferBinding::ElementType::UINT32 };
22 constexpr std::array<FlowBufferSpec, 2> k_flow_export {
23 FlowBufferSpec { .set = 1, .binding = 8, .type = GpuBufferBinding::ElementType::FLOAT32 },
24 FlowBufferSpec { .set = 1, .binding = 9, .type = GpuBufferBinding::ElementType::FLOAT32 },
25 };
26 constexpr FlowBufferSpec k_flow_args { .set = 1, .binding = 10, .type = GpuBufferBinding::ElementType::UINT32 };
27
28 /**
29 * @brief Dispatch triples written by the args phase of flow_select.comp,
30 * in the order they are stored.
31 */
32 enum class FlowArgs : uint8_t {
33 LK = 0,
34 RETAINED = 1,
35 DETECTED = 2,
36 PUBLISH = 3,
37 };
38
39 /**
40 * @brief True when the frame just processed is a repeat of the one before.
41 *
42 * The pyramid build accumulates the total intensity change of the new
43 * frame against the previous one into meta[1]. Without a previous frame
44 * there is nothing to repeat.
45 */
46 bool frame_is_duplicate(const VisionGpuContexts& contexts, const std::array<uint32_t, 4>& meta)
47 {
48 return contexts.flow_state.have_prev && meta[1] <= k_flow_duplicate_energy;
49 }
50
51 /**
52 * @brief Read the flow meta words the GPU finished writing: the live track
53 * count in [0] and the frame change energy in [1].
54 *
55 * One sixteen byte host read after the fence. Zeros, without a read, when
56 * there is no previous frame, since nothing was tracked or compared.
57 */
58 std::array<uint32_t, 4> read_flow_meta(VisionGpuContexts& contexts)
59 {
60 std::array<uint32_t, 4> meta {};
61 if (contexts.flow_state.have_prev)
62 contexts.flow.download_shared(1, 3, meta.data(), sizeof(meta));
63 return meta;
64 }
65
66 /** Location of one dispatch triple in the flow context's args buffer */
67 IndirectGroupsSource flow_args_source(FlowArgs slot)
68 {
69 return { .set = k_flow_args.set, .binding = k_flow_args.binding, .offset_bytes = static_cast<uint64_t>(slot) * k_flow_args_stride };
70 }
71
72 /**
73 * @brief Hazard list covering the named shared buffers of the flow
74 * context, for the barrier a stage owes the stages after it.
75 */
76 std::vector<Portal::Graphics::HazardResource> flow_hazards(GpuDispatchCore& ctx, const std::vector<FlowBufferSpec>& specs)
77 {
78 std::vector<Portal::Graphics::HazardResource> hazards;
79 hazards.reserve(specs.size());
80 for (const auto& s : specs) {
81 hazards.push_back(ctx.shared_buffer_hazard({
82 .set = s.set,
83 .binding = s.binding,
84 .direction = GpuBufferBinding::Direction::INPUT_OUTPUT,
85 .element_type = s.type,
86 }));
87 }
88 return hazards;
89 }
90
91 /**
92 * @brief Decode count track records, two vec4 each, into host results.
93 *
94 * Shared by the tracks buffer readback and the exported buffer reader, so
95 * the two layouts cannot drift apart.
96 */
97 std::vector<Kinesis::Vision::TrackResult> decode_track_records(const glm::vec4* records, uint32_t count)
98 {
99 std::vector<Kinesis::Vision::TrackResult> tracks;
100 tracks.reserve(count);
101 for (uint32_t i = 0; i < count; ++i) {
102 const auto& pose = records[static_cast<size_t>(i) * 2U];
103 const auto& status = records[static_cast<size_t>(i) * 2U + 1U];
104 tracks.push_back({
105 .position = { pose.x, pose.y },
106 .error = status.x,
107 .tracked = status.y > 0.5F,
108 .previous = { pose.z, pose.w },
109 .id = std::bit_cast<uint32_t>(status.z),
110 .age = std::bit_cast<uint32_t>(status.w),
111 });
112 }
113 return tracks;
114 }
115
116 /**
117 * @brief True when a flow step remains after the one being finished.
118 *
119 * The atlas parity flips once per frame, after the last flow step, so a
120 * sequence holding both TrackKeypoints and OpticalFlowDense reads the
121 * same frame pair in each.
122 */
123 bool flow_step_ahead(const VisionGpuContexts& contexts)
124 {
125 const auto& steps = contexts.pass.sequence->steps;
126 for (size_t i = contexts.pass.index + 1; i < steps.size(); ++i) {
127 if (steps[i].op == VisionOp::TrackKeypoints || steps[i].op == VisionOp::OpticalFlowDense)
128 return true;
129 }
130 return false;
131 }
132
133 /**
134 * @brief End the frame for the flow context: the current atlas becomes
135 * the previous one. Held back while a later flow step in the same
136 * sequence still needs this frame pair.
137 */
138 void commit_flow_frame(VisionGpuContexts& contexts)
139 {
140 if (flow_step_ahead(contexts))
141 return;
142
143 auto& state = contexts.flow_state;
144 state.have_prev = true;
145 state.curr ^= 1U;
146 state.curr_ready = false;
147 }
148
149 /**
150 * @brief Publish a finished tracking sequence: deliver the tracks, flip
151 * the atlas parity, and mark the flow context as having a previous
152 * frame.
153 *
154 * The host readback of the tracks region is the only host transfer in the
155 * feature, and only happens when the step asked for host tracks. The
156 * detection promotion already happened on the GPU inside the sequence, so
157 * a step that only exports skips the transfer entirely and reads just the
158 * 16 byte meta block. The repeated frame decision uses the track count of
159 * the last distinct frame rather than the host list, so it holds without
160 * a host result.
161 */
162 void finish_track(VisionGpuContexts& contexts)
163 {
164 auto& state = contexts.flow_state;
165 auto& flow = contexts.flow;
166 std::vector<Kinesis::Vision::TrackResult> tracks;
167 const auto meta = read_flow_meta(contexts);
168 const bool duplicate = frame_is_duplicate(contexts, meta) && state.last_track_count > 0;
169 const uint32_t count = state.have_prev ? std::min(meta[0], k_flow_max_points) : 0U;
170
171 if (state.host_pending) {
172 if (duplicate && !state.last_tracks.empty()) {
173 tracks = state.last_tracks;
174 } else {
175 if (count > 0) {
176 std::vector<glm::vec4> records(static_cast<size_t>(count) * 2U);
177 flow.download_shared(1, 2, records.data(), records.size() * sizeof(glm::vec4));
178 tracks = decode_track_records(records.data(), count);
179 }
180 state.last_tracks = tracks;
181 }
182 }
183
184 if (!duplicate)
185 state.last_track_count = count;
186
187 if (state.export_pending) {
188 if (!duplicate || !state.last_export) {
189 const auto& slot = k_flow_export[state.export_slot];
190 auto& view = state.export_view[state.export_slot];
191 if (!view)
192 view = std::make_shared<Portal::Graphics::GpuBufferHandle>(flow.shared_buffer_handle(slot.set, slot.binding));
193 state.last_export = view;
194 state.export_slot ^= 1U;
195 }
196 contexts.pass.result.buffers.tracks = state.last_export;
197 state.export_pending = false;
198 }
199 commit_flow_frame(contexts);
200
201 contexts.pass.result.structured = std::move(tracks);
202 contexts.pass.result.w = 0;
203 contexts.pass.result.h = 0;
204 }
205
206 /**
207 * @brief Allocate the dense flow images for the current atlas layout.
208 *
209 * The frame sized outputs are alternated by parity; everything else shares
210 * the atlas layout. Allocated once per layout, like the atlases.
211 *
212 * @return False when any image could not be created.
213 */
214 bool ensure_dense_images(VisionGpuContexts& contexts)
215 {
216 auto& state = contexts.flow_state;
217 const auto& layout = state.layout;
218
219 if (state.flow_lvl && state.dense_layout == layout)
220 return true;
221
223 const auto make = [&loom](uint32_t w, uint32_t h) {
224 return loom.create_storage_image(w, h, ImageFormat::RGBA32F);
225 };
226
227 state.flow_out[0] = make(layout.level[0].w, layout.level[0].h);
228 state.flow_out[1] = make(layout.level[0].w, layout.level[0].h);
229 state.flow_lvl = make(layout.atlas_w, layout.atlas_h);
230 state.dense_a = make(layout.atlas_w, layout.atlas_h);
231 state.dense_b = make(layout.atlas_w, layout.atlas_h);
232 state.dense_tensor = make(layout.atlas_w, layout.atlas_h);
233 state.flow_vis = make(layout.level[0].w, layout.level[0].h);
234 state.dense_layout = layout;
235
236 return state.flow_out[0] && state.flow_out[1] && state.flow_lvl
237 && state.dense_a && state.dense_b && state.dense_tensor && state.flow_vis;
238 }
239
240 /**
241 * @brief Publish a finished dense flow sequence: hand the flow image, and
242 * the colour wheel image when one was rendered, to the result and
243 * end the frame for the flow context.
244 *
245 * Nothing is read back. The images stay on the GPU for the consumer.
246 */
247 void finish_dense(VisionGpuContexts& contexts, bool visualized)
248 {
249 auto& state = contexts.flow_state;
250 const auto meta = read_flow_meta(contexts);
251 const bool duplicate = frame_is_duplicate(contexts, meta) && state.last_flow;
252 if (!duplicate)
253 state.last_flow = state.flow_out[state.curr];
254
255 contexts.pass.result.flow = state.last_flow;
256 const auto pixel_count = static_cast<float>(contexts.pass.w) * static_cast<float>(contexts.pass.h);
257 contexts.pass.result.motion_energy = pixel_count > 0.0F
258 ? static_cast<float>(meta[1]) / 1024.0F / pixel_count
259 : 0.0F;
260 if (visualized && state.last_flow)
261 contexts.pass.result.images.flow_visualization = state.flow_vis;
262 commit_flow_frame(contexts);
263 }
264
265 /**
266 * @brief Allocate the flow context's shared buffers and zero its counters
267 * with a GPU dispatch. Runs once per executor.
268 *
269 * Buffers only shaders touch (detections, retained points, counters,
270 * histogram, occupancy grid) request device local memory. The tracks and
271 * meta buffers stay in host cached memory because the host reads them back
272 * every frame. The two export buffers are device local and always
273 * allocated, so every unit of the context can bind the full table and the
274 * export never reallocates.
275 */
276 void ensure_flow_resources(VisionGpuContexts& contexts)
277 {
278 auto& state = contexts.flow_state;
279 if (state.buffers_ready)
280 return;
281
282 auto& flow = contexts.flow;
283 auto& foundry = Portal::Graphics::get_shader_foundry();
284
286 flow.ensure_shared_buffer(0, 2, static_cast<size_t>(k_flow_max_points) * 4, GpuBufferBinding::ElementType::FLOAT32, Portal::Graphics::BufferUsageHint::COMPUTE);
288 flow.ensure_shared_buffer(1, 1, static_cast<size_t>(k_flow_max_points) * 8, GpuBufferBinding::ElementType::FLOAT32, Portal::Graphics::BufferUsageHint::COMPUTE);
289 flow.ensure_shared_buffer(1, 2, static_cast<size_t>(k_flow_max_points) * 8, GpuBufferBinding::ElementType::FLOAT32);
290 flow.ensure_shared_buffer(1, 3, 4, GpuBufferBinding::ElementType::UINT32);
291 flow.ensure_shared_buffer(1, 4, static_cast<size_t>(k_flow_max_points) * 8, GpuBufferBinding::ElementType::FLOAT32, Portal::Graphics::BufferUsageHint::COMPUTE);
295 for (const auto& spec : k_flow_export)
296 flow.ensure_shared_buffer(spec.set, spec.binding, k_flow_export_vec4 * 4, spec.type, Portal::Graphics::BufferUsageHint::COMPUTE);
297 flow.ensure_shared_buffer(k_flow_args.set, k_flow_args.binding, k_flow_args_count * 3, k_flow_args.type, Portal::Graphics::BufferUsageHint::INDIRECT);
298
299 flow.swap_shader({ .shader_path = "flow_reset.comp.spv", .workgroup_size = { 64, 1, 1 } });
300 flow.set_output_dimensions(1, 1);
301 const auto fence = flow.dispatch_async({});
302 flow.clear_output_dimensions();
303 foundry.wait_for_fence(fence);
304 foundry.release_fence(fence);
305
306 state.buffers_ready = true;
307 }
308
309}
310
312{
313 auto& state = contexts.flow_state;
314 auto& flow = contexts.flow;
315 auto& foundry = Portal::Graphics::get_shader_foundry();
316
317 contexts.pass.result.structured = std::vector<Kinesis::Vision::TrackResult> {};
318 contexts.pass.result.w = 0;
319 contexts.pass.result.h = 0;
320
321 const auto* peaks = contexts.pass.behind();
322 if (!peaks || peaks->op != VisionOp::ExtractPeaks) {
324 "run_gpu: TrackKeypoints requires ExtractPeaks as the immediately preceding step");
325 return false;
326 }
327
328 if (!state.curr_ready || !state.atlas[0]) {
330 "run_gpu: TrackKeypoints requires an earlier RgbaToGray step to supply the gray frame");
331 return false;
332 }
333
334 const auto& p = std::get<TrackKeypointsParams>(step.params);
335 const auto& pk = std::get<ExtractPeaksParams>(peaks->params);
336 const auto& layout = state.layout;
337 const uint32_t w = contexts.pass.w;
338 const uint32_t h = contexts.pass.h;
339 const uint32_t max_points = std::clamp(p.max_points, 1U, k_flow_max_points);
340
341 if (w != layout.level[0].w || h != layout.level[0].h) {
343 "run_gpu: TrackKeypoints image is {}x{} but the gray frame was captured at {}x{}",
344 w, h, layout.level[0].w, layout.level[0].h);
345 return false;
346 }
347
348 const bool export_tracks = p.export_tracks;
349 const uint32_t duplicate_cutoff = (state.last_track_count > 0 && state.last_export)
351 : 0U;
352
353 const auto response = contexts.pass.current;
354 const auto atlas_a = state.atlas[0];
355 const auto atlas_b = state.atlas[1];
356
357 const ExtractPeaksPC peaks_pc {
358 .threshold = pk.threshold,
359 .nms_radius = pk.nms_radius,
360 .width = w,
361 .height = h,
362 .max_keypoints = k_flow_max_points,
363 .rect = region_rect(pk.region, w, h),
364 };
365
366 std::vector<DependencyStage> stages;
367 stages.reserve(layout.levels * (p.forward_backward_threshold > 0.0F ? 2U : 1U) + 8U);
368
369 stages.push_back({
370 .config = { .shader_path = "extract_peaks.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(ExtractPeaksPC) },
371 .stage_fn = [response, peaks_pc](GpuDispatchCore& ctx) {
372 ctx.stage_image_at(0, response, GpuBufferBinding::ElementType::IMAGE_STORAGE);
373 ctx.set_push_constants(peaks_pc); },
374 .hazard_fn = [](GpuDispatchCore& ctx) { return flow_hazards(ctx, { k_flow_det_count, k_flow_det_points }); },
375 .explicit_groups = std::array<uint32_t, 3> { (w + k_wg2d[0] - 1U) / k_wg2d[0], (h + k_wg2d[1] - 1U) / k_wg2d[1], 1U },
376 });
377
378 const auto base_w = static_cast<float>(layout.level[0].w);
379 const auto base_h = static_cast<float>(layout.level[0].h);
380 float cell = std::max(p.min_distance, 1.0F);
381 auto grid_w = static_cast<uint32_t>(std::ceil(base_w / cell));
382 auto grid_h = static_cast<uint32_t>(std::ceil(base_h / cell));
383 while (static_cast<uint64_t>(grid_w) * grid_h > k_flow_grid_capacity) {
384 cell *= 1.1F;
385 grid_w = static_cast<uint32_t>(std::ceil(base_w / cell));
386 grid_h = static_cast<uint32_t>(std::ceil(base_h / cell));
387 }
388 const uint32_t grid_cells = grid_w * grid_h;
389 const uint32_t clear_groups = std::max(1U, (grid_cells + k_flow_select_local - 1U) / k_flow_select_local);
390 const uint32_t candidate_groups = (k_flow_max_points + k_flow_select_local - 1U) / k_flow_select_local;
391 const uint32_t have_prev = state.have_prev ? 1U : 0U;
392
393 const auto add_select = [&](FlowSelectPhase phase, uint32_t groups, std::vector<FlowBufferSpec> touched, std::optional<FlowArgs> args = std::nullopt) {
394 const FlowSelectPC pc {
395 .phase = static_cast<uint32_t>(phase),
396 .max_points = max_points,
397 .have_prev = have_prev,
398 .grid_w = grid_w,
399 .grid_h = grid_h,
400 .grid_cells = grid_cells,
401 .cell = cell,
402 .base_w = base_w,
403 .base_h = base_h,
404 .publish_slot = state.export_slot,
405 .duplicate_cutoff = duplicate_cutoff,
406 };
407 stages.push_back({
408 .config = { .shader_path = "flow_select.comp.spv", .workgroup_size = { k_flow_select_local, 1, 1 }, .push_constant_size = sizeof(FlowSelectPC) },
409 .stage_fn = [pc](GpuDispatchCore& ctx) { ctx.set_push_constants(pc); },
410 .hazard_fn = [touched = std::move(touched)](GpuDispatchCore& ctx) { return flow_hazards(ctx, touched); },
411 .explicit_groups = std::array<uint32_t, 3> { groups, 1U, 1U },
412 .indirect_groups = args ? std::optional<IndirectGroupsSource> { flow_args_source(*args) } : std::nullopt,
413 });
414 };
415
416 add_select(FlowSelectPhase::ARGS, 1U, {});
417
418 const auto add_lk = [&](bool backward) {
419 for (uint32_t level = layout.levels; level-- > 0;) {
420 const PyramidLevel lv = layout.level[level];
421 const bool finest = level == 0;
422
423 const FlowLkPC pc {
424 .curr_atlas = state.curr,
425 .level = level,
426 .coarsest = level + 1U == layout.levels ? 1U : 0U,
427 .pad0 = 0,
428 .lvl_ox = lv.ox,
429 .lvl_oy = lv.oy,
430 .lvl_w = lv.w,
431 .lvl_h = lv.h,
432 .base_w = layout.level[0].w,
433 .base_h = layout.level[0].h,
434 .window_radius = std::min(p.window_radius, k_flow_max_radius),
435 .max_iterations = p.max_iterations,
436 .eigen_threshold = p.eigen_threshold,
437 .error_threshold = p.error_threshold,
438 .max_points = max_points,
439 .pad1 = 0,
440 .backward = backward ? 1U : 0U,
441 .forward_backward_threshold = p.forward_backward_threshold,
442 };
443
444 stages.push_back({
445 .config = { .shader_path = "flow_lk.comp.spv", .workgroup_size = { 64, 1, 1 }, .push_constant_size = sizeof(FlowLkPC) },
446 .stage_fn = [atlas_a, atlas_b, pc](GpuDispatchCore& ctx) {
447 ctx.stage_image_at(3, atlas_a, GpuBufferBinding::ElementType::IMAGE_STORAGE);
448 ctx.stage_image_at(4, atlas_b, GpuBufferBinding::ElementType::IMAGE_STORAGE);
449 ctx.set_push_constants(pc); },
450 .hazard_fn = [finest, backward](GpuDispatchCore& ctx) {
451 if (finest || backward)
452 return flow_hazards(ctx, { k_flow_tracks, k_flow_meta, k_flow_prev_count, k_flow_prev_points });
453 return flow_hazards(ctx, { k_flow_tracks, k_flow_meta }); },
454 .explicit_groups = std::array<uint32_t, 3> { k_flow_max_points, 1U, 1U },
455 .indirect_groups = flow_args_source(FlowArgs::LK),
456 });
457 }
458 };
459 if (state.have_prev) {
460 add_lk(false);
461 if (p.forward_backward_threshold > 0.0F)
462 add_lk(true);
463 }
464
465 add_select(FlowSelectPhase::CLEAR, clear_groups, { k_flow_grid, k_flow_histogram, k_flow_counters });
466 add_select(FlowSelectPhase::SURVIVORS, candidate_groups, { k_flow_next_points, k_flow_counters, k_flow_grid }, FlowArgs::RETAINED);
467 add_select(FlowSelectPhase::HISTOGRAM, candidate_groups, { k_flow_histogram }, FlowArgs::DETECTED);
468 add_select(FlowSelectPhase::THRESHOLD, 1U, { k_flow_counters });
469 add_select(FlowSelectPhase::ADD_STRONG, candidate_groups, { k_flow_next_points, k_flow_counters, k_flow_grid }, FlowArgs::DETECTED);
470 add_select(FlowSelectPhase::ADD_MARGINAL, candidate_groups, { k_flow_next_points, k_flow_counters, k_flow_grid }, FlowArgs::DETECTED);
471 add_select(FlowSelectPhase::COMMIT, 1U, {});
472 if (export_tracks)
473 add_select(FlowSelectPhase::PUBLISH, candidate_groups, { k_flow_export[0], k_flow_export[1] }, FlowArgs::PUBLISH);
474
475 ExecutionContext track_ctx;
477 DependencyParams track_params;
478 track_params.stages = stages;
479 track_params.async = true;
480 track_ctx.parameters = track_params;
481 const auto track_dispatch_result = flow.execute(Datum<> {}, track_ctx);
482 const auto fence = track_dispatch_result.get_metadata<Portal::Graphics::FenceID>("gpu_fence").value_or(Portal::Graphics::INVALID_FENCE);
485 "run_gpu: TrackKeypoints failed to submit its dispatch sequence");
486 return false;
487 }
488 state.export_pending = export_tracks;
489 state.host_pending = p.host_tracks;
490
491 if (step.deferred) {
492 contexts.suspended.fence = fence;
493 contexts.suspended.finalize = [](VisionGpuContexts& c) { finish_track(c); };
494 return true;
495 }
496
497 foundry.wait_for_fence(fence);
498 foundry.release_fence(fence);
499 finish_track(contexts);
500 return false;
501}
502
504{
505 auto& state = contexts.flow_state;
506 auto& flow = contexts.flow;
507 auto& foundry = Portal::Graphics::get_shader_foundry();
508
509 if (!state.curr_ready || !state.atlas[0]) {
511 "run_gpu: OpticalFlowDense requires an earlier RgbaToGray step to supply the gray frame");
512 return false;
513 }
514
515 const auto& p = std::get<OpticalFlowDenseParams>(step.params);
516 const auto& layout = state.layout;
517 const uint32_t w = contexts.pass.w;
518 const uint32_t h = contexts.pass.h;
519
520 if (w != layout.level[0].w || h != layout.level[0].h) {
522 "run_gpu: OpticalFlowDense image is {}x{} but the gray frame was captured at {}x{}",
523 w, h, layout.level[0].w, layout.level[0].h);
524 return false;
525 }
526
527 if (!state.have_prev) {
528 commit_flow_frame(contexts);
529 return false;
530 }
531
532 if (!ensure_dense_images(contexts)) {
534 "run_gpu: OpticalFlowDense failed to allocate its flow images");
535 return false;
536 }
537
538 struct DenseImages {
539 std::shared_ptr<Core::VKImage> atlas_a;
540 std::shared_ptr<Core::VKImage> atlas_b;
541 std::shared_ptr<Core::VKImage> flow_out_a;
542 std::shared_ptr<Core::VKImage> flow_out_b;
543 std::shared_ptr<Core::VKImage> flow_lvl;
544 std::shared_ptr<Core::VKImage> scratch_a;
545 std::shared_ptr<Core::VKImage> scratch_b;
546 std::shared_ptr<Core::VKImage> tensor;
547 std::shared_ptr<Core::VKImage> vis;
548 };
549
550 const auto images = std::make_shared<const DenseImages>(DenseImages {
551 .atlas_a = state.atlas[0],
552 .atlas_b = state.atlas[1],
553 .flow_out_a = state.flow_out[0],
554 .flow_out_b = state.flow_out[1],
555 .flow_lvl = state.flow_lvl,
556 .scratch_a = state.dense_a,
557 .scratch_b = state.dense_b,
558 .tensor = state.dense_tensor,
559 .vis = state.flow_vis,
560 });
561
562 const uint32_t out_parity = state.curr;
563 const uint32_t radius = std::clamp(p.window_radius, 1U, k_flow_dense_max_radius);
564 const uint32_t iterations = std::max(p.iterations, 1U);
565
566 const auto image_hazard = [](const std::shared_ptr<Core::VKImage>& image, uint32_t binding) {
567 return HazardResource {
568 .binding = { .set = 0, .binding = binding, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::IMAGE_STORAGE },
569 .image = image->get_image(),
570 };
571 };
572
573 const auto scratch_a_hazard = image_hazard(images->scratch_a, 8U);
574 const auto scratch_b_hazard = image_hazard(images->scratch_b, 9U);
575 const auto tensor_hazard = image_hazard(images->tensor, 10U);
576 const auto coarse_hazard = image_hazard(images->flow_lvl, 7U);
577 const auto out_hazard = image_hazard(out_parity == 0 ? images->flow_out_a : images->flow_out_b, 5U + out_parity);
578
579 std::vector<DependencyStage> stages;
580 stages.reserve(static_cast<size_t>(layout.levels) * (2U + 3U * iterations) + 1U);
581
582 const auto push_stage = [&](const FlowDensePC& stage_pc, const PyramidLevel& region, std::vector<HazardResource> hazards) {
583 stages.push_back({
584 .config = { .shader_path = "flow_dense.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(FlowDensePC) },
585 .stage_fn = [images, stage_pc](GpuDispatchCore& ctx) {
587 ctx.stage_image_at(3, images->atlas_a, Kind::IMAGE_STORAGE);
588 ctx.stage_image_at(4, images->atlas_b, Kind::IMAGE_STORAGE);
589 ctx.stage_image_at(5, images->flow_out_a, Kind::IMAGE_STORAGE);
590 ctx.stage_image_at(6, images->flow_out_b, Kind::IMAGE_STORAGE);
591 ctx.stage_image_at(7, images->flow_lvl, Kind::IMAGE_STORAGE);
592 ctx.stage_image_at(8, images->scratch_a, Kind::IMAGE_STORAGE);
593 ctx.stage_image_at(9, images->scratch_b, Kind::IMAGE_STORAGE);
594 ctx.stage_image_at(10, images->tensor, Kind::IMAGE_STORAGE);
595 ctx.stage_image_at(11, images->vis, Kind::IMAGE_STORAGE);
596 ctx.set_push_constants(stage_pc); },
597 .hazard_fn = [hazards = std::move(hazards)](GpuDispatchCore&) { return hazards; },
598 .explicit_groups = std::array<uint32_t, 3> { (region.w + k_wg2d[0] - 1U) / k_wg2d[0], (region.h + k_wg2d[1] - 1U) / k_wg2d[1], 1U },
599 });
600 };
601
602 for (uint32_t level = layout.levels; level-- > 0;) {
603 const PyramidLevel lv = layout.level[level];
604 const bool coarsest = level + 1U == layout.levels;
605 const PyramidLevel coarse = coarsest ? PyramidLevel {} : layout.level[level + 1];
606 const auto& flow_hazard = level == 0 ? out_hazard : coarse_hazard;
607
608 FlowDensePC pc {
609 .phase = 0,
610 .curr_atlas = state.curr,
611 .out_parity = out_parity,
612 .level = level,
613 .lvl_ox = lv.ox,
614 .lvl_oy = lv.oy,
615 .lvl_w = lv.w,
616 .lvl_h = lv.h,
617 .crs_ox = coarse.ox,
618 .crs_oy = coarse.oy,
619 .crs_w = coarse.w,
620 .crs_h = coarse.h,
621 .has_coarse = coarsest ? 0U : 1U,
622 .radius = radius,
623 .eigen_threshold = p.eigen_threshold,
624 .max_step = p.max_step,
625 .visual_range = p.visual_range,
626 .visual_min_motion = p.visual_min_motion,
627 .last_iteration = 0,
628 };
629
630 const auto add = [&](FlowDensePhase phase, std::vector<HazardResource> hazards) {
631 pc.phase = static_cast<uint32_t>(phase);
632 push_stage(pc, lv, std::move(hazards));
633 };
634
635 add(FlowDensePhase::TENSOR_H, { scratch_a_hazard });
636 add(FlowDensePhase::TENSOR_V, { tensor_hazard, scratch_a_hazard });
637 for (uint32_t iteration = 0; iteration < iterations; ++iteration) {
638 add(iteration == 0 ? FlowDensePhase::WARP_INIT : FlowDensePhase::WARP, { scratch_a_hazard, flow_hazard });
639 add(FlowDensePhase::BOX_H, { scratch_b_hazard, scratch_a_hazard });
640 pc.last_iteration = iteration + 1U == iterations ? 1U : 0U;
641 add(FlowDensePhase::SOLVE, { flow_hazard, scratch_b_hazard });
642 }
643 }
644
645 if (p.visualize) {
646 const PyramidLevel finest = layout.level[0];
647 const FlowDensePC vis_pc {
648 .phase = static_cast<uint32_t>(FlowDensePhase::VISUALIZE),
649 .curr_atlas = state.curr,
650 .out_parity = out_parity,
651 .level = 0,
652 .lvl_ox = finest.ox,
653 .lvl_oy = finest.oy,
654 .lvl_w = finest.w,
655 .lvl_h = finest.h,
656 .crs_ox = 0,
657 .crs_oy = 0,
658 .crs_w = 0,
659 .crs_h = 0,
660 .has_coarse = 0,
661 .radius = radius,
662 .eigen_threshold = p.eigen_threshold,
663 .max_step = p.max_step,
664 .visual_range = p.visual_range,
665 .visual_min_motion = p.visual_min_motion,
666 .last_iteration = 0,
667 };
668 push_stage(vis_pc, finest, { out_hazard, image_hazard(images->vis, 11U) });
669 }
670
671 ExecutionContext dense_ctx;
673 DependencyParams dense_params;
674 dense_params.stages = stages;
675 dense_params.async = true;
676 dense_ctx.parameters = dense_params;
677 const auto dense_dispatch_result = flow.execute(Datum<> {}, dense_ctx);
678 const auto fence = dense_dispatch_result.get_metadata<Portal::Graphics::FenceID>("gpu_fence").value_or(Portal::Graphics::INVALID_FENCE);
681 "run_gpu: OpticalFlowDense failed to submit its dispatch sequence");
682 return false;
683 }
684
685 if (step.deferred) {
686 contexts.suspended.fence = fence;
687 contexts.suspended.finalize = [visualize = p.visualize](VisionGpuContexts& c) { finish_dense(c, visualize); };
688 return true;
689 }
690
691 foundry.wait_for_fence(fence);
692 foundry.release_fence(fence);
693 finish_dense(contexts, p.visualize);
694 return false;
695}
696
697std::vector<Kinesis::Vision::TrackResult> VisionGpuExecutor::read_exported_tracks(const Kinesis::Vision::VisionResult& result)
698{
699 const auto& handle = result.buffers.tracks;
700 const auto count = exported_record_count(handle, 2);
701
702 if (!count)
703 return {};
704
705 const auto* header = static_cast<const glm::vec4*>(handle->mapped_ptr);
706 return decode_track_records(header + 1, *count);
707}
708
709void VisionGpuExecutor::build_flow_pyramid(VisionGpuContexts& contexts, uint32_t requested_levels, uint32_t src_channel)
710{
711 auto& flow = contexts.flow;
712 auto& state = contexts.flow_state;
713 const auto gray = contexts.pass.current;
714
715 if (!gray || !gray->is_initialized())
716 return;
717
718 const uint32_t w = contexts.pass.w;
719 const uint32_t h = contexts.pass.h;
720 const auto layout = pyramid_layout(w, h, requested_levels, k_flow_min_level_extent);
721
722 ensure_flow_resources(contexts);
723
724 if (!state.atlas[0] || !(state.layout == layout)) {
726 state.atlas[0] = loom.create_storage_image(layout.atlas_w, layout.atlas_h, ImageFormat::RGBA16F);
727 state.atlas[1] = loom.create_storage_image(layout.atlas_w, layout.atlas_h, ImageFormat::RGBA16F);
728 state.layout = layout;
729 state.have_prev = false;
730 state.curr = 0;
731 }
732
733 const auto atlas_a = state.atlas[0];
734 const auto atlas_b = state.atlas[1];
735 const auto target = state.atlas[state.curr];
736
737 std::vector<DependencyStage> stages;
738 stages.reserve(layout.levels);
739
740 for (uint32_t level = 0; level < layout.levels; ++level) {
741 const PyramidLevel dst = layout.level[level];
742 const PyramidLevel src = level == 0 ? PyramidLevel { .ox = 0, .oy = 0, .w = w, .h = h } : layout.level[level - 1];
743
744 const FlowPyramidPC pc {
745 .target_atlas = state.curr,
746 .level = level,
747 .src_w = src.w,
748 .src_h = src.h,
749 .src_ox = src.ox,
750 .src_oy = src.oy,
751 .dst_w = dst.w,
752 .dst_h = dst.h,
753 .dst_ox = dst.ox,
754 .dst_oy = dst.oy,
755 .src_channel = src_channel,
756 };
757
758 stages.push_back({
759 .config = { .shader_path = "flow_pyramid.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(FlowPyramidPC) },
760 .stage_fn = [gray, atlas_a, atlas_b, pc](GpuDispatchCore& ctx) {
761 ctx.stage_image_at(0, gray, GpuBufferBinding::ElementType::IMAGE_STORAGE);
762 ctx.stage_image_at(3, atlas_a, GpuBufferBinding::ElementType::IMAGE_STORAGE);
763 ctx.stage_image_at(4, atlas_b, GpuBufferBinding::ElementType::IMAGE_STORAGE);
764 ctx.set_push_constants(pc); },
765 .hazard_fn = [target, gray, level](GpuDispatchCore&) {
766 std::vector<HazardResource> hazards;
767 hazards.push_back({
768 .binding = { .set = 0, .binding = 3, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::IMAGE_STORAGE },
769 .image = target->get_image(),
770 });
771 if (level == 0) {
772 hazards.push_back({
773 .binding = { .set = 0, .binding = 0, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::IMAGE_STORAGE },
774 .image = gray->get_image(),
775 });
776 }
777 return hazards; },
778 .explicit_groups = std::array<uint32_t, 3> { (dst.w + k_wg2d[0] - 1U) / k_wg2d[0], (dst.h + k_wg2d[1] - 1U) / k_wg2d[1], 1U },
779 });
780 }
781
782 auto& foundry = Portal::Graphics::get_shader_foundry();
783 if (state.build_fence != Portal::Graphics::INVALID_FENCE) {
784 foundry.wait_for_fence(state.build_fence);
785 foundry.release_fence(state.build_fence);
786 state.build_fence = Portal::Graphics::INVALID_FENCE;
787 }
788
789 const std::array<uint32_t, 4> cleared {};
790 flow.upload_shared_raw(1, 3, reinterpret_cast<const uint8_t*>(cleared.data()), sizeof(cleared));
791
792 ExecutionContext pyramid_ctx;
793 pyramid_ctx.mode = ExecutionMode::DEPENDENCY;
794 DependencyParams pyramid_params;
795 pyramid_params.stages = stages;
796 pyramid_params.async = true;
797 pyramid_ctx.parameters = pyramid_params;
798 const auto pyramid_dispatch_result = flow.execute(Datum<> {}, pyramid_ctx);
799 state.build_fence = pyramid_dispatch_result.get_metadata<Portal::Graphics::FenceID>("gpu_fence").value_or(Portal::Graphics::INVALID_FENCE);
800 state.curr_ready = state.build_fence != Portal::Graphics::INVALID_FENCE;
801}
802
803} // namespace MayaFlux::Yantra
#define MF_ERROR(comp, ctx,...)
vk::Fence fence
IO::ImageData image
Definition Decoder.cpp:47
float radius
uint32_t h
Definition InkPress.cpp:29
uint32_t * dst
const uint32_t * src
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
size_t count
Non-template base that owns all type-independent GPU dispatch logic.
static void build_flow_pyramid(VisionGpuContexts &contexts, uint32_t requested_levels, uint32_t src_channel)
Build the current frame's pyramid atlas from the working image.
bool op_dense_flow(VisionGpuContexts &contexts, const Kinesis::Vision::VisionStep &step)
Dense optical flow from the previous frame to the current one.
bool op_track_keypoints(VisionGpuContexts &contexts, const Kinesis::Vision::VisionStep &step)
Track the previous frame's keypoints into the current frame.
static std::vector< Kinesis::Vision::TrackResult > read_exported_tracks(const Kinesis::Vision::VisionResult &result)
Host tracks decoded from result.buffers.tracks, or empty.
@ ComputeMatrix
Compute operations (Yantra - algorithms, matrices, DSP)
@ Yantra
DSP algorithms, computational units, matrix operations, Grammar.
@ Portal
High-level user-facing API layer.
PyramidLayout pyramid_layout(uint32_t w, uint32_t h, uint32_t requested, uint32_t min_extent=16)
Place up to requested pyramid levels for a w x h base image.
Definition Pyramid.hpp:63
constexpr FenceID INVALID_FENCE
MAYAFLUX_API ShaderFoundry & get_shader_foundry()
Get the global shader compiler instance.
BufferUsageHint
Semantic usage hint for buffer allocation and memory properties.
@ INDIRECT
Indirect draw/dispatch buffer (device-local)
@ COMPUTE
Storage buffer for compute shaders (device-local)
constexpr size_t k_flow_export_vec4
Export buffer size in vec4: one header plus two per track.
constexpr uint32_t k_flow_min_level_extent
Smallest pyramid level edge worth building.
constexpr uint32_t k_flow_duplicate_energy
Frame change, in 1/1024 intensity summed over pixels, at or below which a frame counts as a repeat.
constexpr uint32_t k_flow_max_radius
Largest window radius the tracker's shared template cache holds.
std::optional< uint32_t > exported_record_count(const std::shared_ptr< Portal::Graphics::GpuBufferHandle > &handle, size_t stride_vec4)
Clamped record count from an exported buffer's vec4 header, or nullopt when the buffer isn't readable...
constexpr uint32_t k_flow_grid_capacity
Occupancy grid capacity in cells; the cell size grows to fit large frames.
PixelRect region_rect(const std::optional< Kinesis::Vision::BoundingBox > &region, uint32_t w, uint32_t h)
Pixel rectangle of a normalised region over a w x h image, clamped to the image.
constexpr uint32_t k_flow_max_points
Upper bound on tracked keypoints, matching extract_peaks' buffer capacity.
constexpr std::array< uint32_t, 3 > k_wg2d
Standard 2D workgroup used by all pixel-to-pixel vision shaders.
constexpr uint32_t k_flow_dense_max_radius
Largest summation window radius of the dense solver.
@ DEPENDENCY
Part of dependency graph.
static constexpr DomainSpec Graphics
Domain constant for Graphics domain.
Definition Creator.hpp:444
std::shared_ptr< Portal::Graphics::GpuBufferHandle > tracks
Extent and position of one pyramid level inside an atlas.
Definition Pyramid.hpp:17
const VisionStep * behind(size_t offset=1) const noexcept
Op at offset steps back, or nullptr before the start.
BufferOutputs buffers
GPU-resident export buffers, one named slot per exporting op.
Result of executing a VisionSequence on one frame.
One step in a VisionSequence: an op and its parameters.
Definition VisionOp.hpp:368
ElementType
Element type the shader expects in this binding.
enum MayaFlux::Portal::Graphics::GpuBufferBinding::ElementType FLOAT32
GpuBufferBinding binding
Direction/element_type describing this resource.
One resource this stage's dispatch reads/writes that a later stage in the sequence depends on,...
Input/Output container for computation pipeline data flow with structure preservation.
Definition DataIO.hpp:26
bool async
Submit the sequence without waiting for it to complete.
std::vector< DependencyStage > stages
Parameters for ExecutionMode::DEPENDENCY.
ExecutionMode mode
Execution mode controlling scheduling behavior.
ExecutionParams parameters
Optional parameters specific to the execution mode.
Context information controlling how a compute operation executes.
std::function< void(VisionGpuContexts &)> finalize
Work to run once the fence has signaled, before the walk resumes.
Kinesis::Vision::GpuVisionPass pass
Walk state for the current run: sequence position, geometry, working image, and the result under cons...
TextureExecutionContext flow
Pyramidal optical flow.
Fixed set of TextureExecutionContexts covering every GPU-implemented VisionOp shape.