MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
BackendWindowHandler.cpp
Go to the documentation of this file.
2
5#include "VKEnumUtils.hpp"
6#include "VKImage.hpp"
7#include "VKSwapchain.hpp"
8
12
13namespace MayaFlux::Core {
14
15// ---------------------------------------------------------------------------
16// CaptureState retirement implementations (macOS only)
17// ---------------------------------------------------------------------------
18#ifdef MAYAFLUX_PLATFORM_MACOS
19
20void CaptureState::retire_last_frame(const std::vector<uint8_t>* ptr)
21{
22 for (;;) {
23 bool in_use = false;
24 for (size_t i = 0; i < LAST_FRAME_MAX_READERS; ++i) {
25 if (last_frame_hazard_ptrs[i].load(std::memory_order_acquire) == ptr) {
26 in_use = true;
27 break;
28 }
29 }
30 if (!in_use)
31 break;
32 std::this_thread::yield();
33 }
34 delete ptr;
35}
36
37void CaptureState::retire_observers(const ObserverMap* ptr)
38{
39 for (;;) {
40 bool in_use = false;
41 for (size_t i = 0; i < OBSERVERS_MAX_READERS; ++i) {
42 if (observers_hazard_ptrs[i].load(std::memory_order_acquire) == ptr) {
43 in_use = true;
44 break;
45 }
46 }
47 if (!in_use)
48 break;
49 std::this_thread::yield();
50 }
51 delete ptr;
52}
53#endif
54
56{
57 vk::Device device = context.get_device();
58
59 if (capture) {
60 capture->readback_running.store(false, std::memory_order_release);
61 device.waitIdle();
62
63 if (capture->readback_thread.joinable())
64 capture->readback_thread.join();
65
66 for (auto& slot : capture->slots) {
67 if (slot->fence) {
68 (void)device.waitForFences(1, &slot->fence, VK_TRUE, UINT64_MAX);
69 device.destroyFence(slot->fence);
70 }
71 if (slot->image)
72 device.destroyImage(slot->image);
73 if (slot->mem)
74 device.freeMemory(slot->mem);
75 }
76 capture.reset();
77 }
78
79 device.waitIdle();
80
81 for (auto& img : image_available) {
82 if (img) {
83 device.destroySemaphore(img);
84 }
85 }
86 image_available.clear();
87
88 for (auto& render : render_finished) {
89 if (render) {
90 device.destroySemaphore(render);
91 }
92 }
93 render_finished.clear();
94
96
97 for (auto& fence : in_flight) {
98 if (fence) {
99 device.destroyFence(fence);
100 }
101 }
102 in_flight.clear();
103 slot_submission.clear();
104
105 if (depth_image && depth_image->is_initialized()) {
106 const auto& res = depth_image->get_image_resources();
107 if (res.image_view) {
108 device.destroyImageView(res.image_view);
109 }
110 if (res.memory) {
111 device.freeMemory(res.memory);
112 }
113 if (res.image) {
114 device.destroyImage(res.image);
115 }
116 depth_image.reset();
117 }
118
119 if (swapchain) {
120 swapchain->cleanup();
121 swapchain.reset();
122 }
123
124 if (surface) {
125 context.destroy_surface(surface);
126 surface = nullptr;
127 }
128
129 window->set_graphics_registered(false);
130}
131
133 : m_context(context)
134 , m_command_manager(command_manager)
135{
136}
137
138void BackendWindowHandler::setup_backend_service(const std::shared_ptr<Registry::Service::DisplayService>& display_service)
139{
140 display_service->submit_and_present = [this](
141 const std::shared_ptr<void>& window_ptr,
142 uint64_t primary_cmd_bits) {
143 auto window = std::static_pointer_cast<Window>(window_ptr);
144 vk::CommandBuffer primary_cmd = *reinterpret_cast<vk::CommandBuffer*>(&primary_cmd_bits);
145
146 this->submit_and_present(window, primary_cmd);
147 };
148
149 display_service->wait_idle = [this]() {
150 m_context.get_device().waitIdle();
151 };
152
153 display_service->resize_surface = [this](const std::shared_ptr<void>& window_ptr, uint32_t width, uint32_t height) {
154 auto window = std::static_pointer_cast<Window>(window_ptr);
155 window->set_size(width, height);
156 for (auto& ctx : m_window_contexts) {
157 if (ctx.window == window) {
158 ctx.needs_recreation = true;
159 break;
160 }
161 }
162 };
163
164 display_service->get_swapchain_image_count = [this](const std::shared_ptr<void>& window_ptr) -> uint32_t {
165 auto window = std::static_pointer_cast<Window>(window_ptr);
166 for (const auto& ctx : m_window_contexts) {
167 if (ctx.window == window) {
168 return static_cast<uint32_t>(ctx.swapchain->get_image_count());
169 }
170 }
171 return 0;
172 };
173
174 display_service->get_submitted_frame_count = [this](const std::shared_ptr<void>& window_ptr) -> uint64_t {
175 const auto* ctx = find_window_context(std::static_pointer_cast<Window>(window_ptr));
176 return ctx ? ctx->submitted_frames : 0;
177 };
178
179 display_service->get_completed_frame_count = [this](const std::shared_ptr<void>& window_ptr) -> uint64_t {
180 const auto* ctx = find_window_context(std::static_pointer_cast<Window>(window_ptr));
181 return ctx ? completed_frame_count(*ctx) : 0;
182 };
183
184 display_service->get_swapchain_format = [this](const std::shared_ptr<void>& window_ptr) -> uint32_t {
185 auto window = std::static_pointer_cast<Window>(window_ptr);
186 for (const auto& ctx : m_window_contexts) {
187 if (ctx.window == window) {
188 return static_cast<int>(ctx.swapchain->get_image_format());
189 }
190 }
191 return 0;
192 };
193
194 display_service->get_swapchain_extent = [this](
195 const std::shared_ptr<void>& window_ptr,
196 uint32_t& out_width,
197 uint32_t& out_height) {
198 auto window = std::static_pointer_cast<Window>(window_ptr);
199 auto* context = find_window_context(window);
200
201 if (context && context->swapchain) {
202 auto extent = context->swapchain->get_extent();
203 out_width = extent.width;
204 out_height = extent.height;
205 } else {
206 out_width = 0;
207 out_height = 0;
208 }
209 };
210
211 display_service->acquire_next_swapchain_image = [this](const std::shared_ptr<void>& window_ptr) -> uint64_t {
212 auto window = std::static_pointer_cast<Window>(window_ptr);
213 auto* ctx = find_window_context(window);
214 if (!ctx) {
216 "Window '{}' not registered for swapchain acquisition",
217 window->get_create_info().title);
218 return 0;
219 }
220
221 auto device = m_context.get_device();
222 size_t frame_index = ctx->current_frame;
223 auto& in_flight = ctx->in_flight[frame_index];
224 auto& image_available = ctx->image_available[frame_index];
225
226 if (device.waitForFences(1, &in_flight, VK_TRUE, UINT64_MAX) == vk::Result::eTimeout) {
228 "Fence timeout during swapchain acquisition for window '{}'",
229 window->get_create_info().title);
230 return 0;
231 }
232
233 auto image_index_opt = ctx->swapchain->acquire_next_image(image_available);
234 if (!image_index_opt.has_value()) {
235 ctx->needs_recreation = true;
236 return 0;
237 }
238
239 ctx->current_image_index = image_index_opt.value();
240
241 if (device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
243 "Failed to reset fence for window '{}'",
244 window->get_create_info().title);
245 return 0;
246 }
247
248 const auto& images = ctx->swapchain->get_images();
249 VkImage raw = images[ctx->current_image_index];
250 return reinterpret_cast<uint64_t>(raw);
251 };
252
253 display_service->get_current_image_view = [this](const std::shared_ptr<void>& window_ptr) -> void* {
254 auto window = std::static_pointer_cast<Window>(window_ptr);
255 auto* context = find_window_context(window);
256
257 if (!context || !context->swapchain) {
258 return nullptr;
259 }
260
261 const auto& image_views = context->swapchain->get_image_views();
262 if (context->current_image_index >= image_views.size()) {
264 "Invalid current_image_index {} for window '{}' (swapchain has {} images)",
265 context->current_image_index,
266 window->get_create_info().title,
267 image_views.size());
268 return nullptr;
269 }
270
271 static thread_local vk::ImageView view;
272 view = image_views[context->current_image_index];
273 return static_cast<void*>(&view);
274 };
275
276 display_service->get_current_swapchain_image = [this](const std::shared_ptr<void>& window_ptr) -> uint64_t {
277 auto window = std::static_pointer_cast<Window>(window_ptr);
278 auto* ctx = find_window_context(window);
279 if (!ctx || !ctx->swapchain)
280 return 0;
281
282 const auto& images = ctx->swapchain->get_images();
283 if (ctx->current_image_index >= images.size())
284 return 0;
285
286 return reinterpret_cast<uint64_t>(static_cast<VkImage>(images[ctx->current_image_index]));
287 };
288
289 display_service->readback_swapchain_region = [this](
290 const std::shared_ptr<void>& window_ptr,
291 void* dst,
292 uint32_t /*x_offset*/,
293 uint32_t /*y_offset*/,
294 uint32_t pixel_width,
295 uint32_t pixel_height,
296 size_t byte_count) -> bool {
297 auto window = std::static_pointer_cast<Window>(window_ptr);
298 auto* ctx = find_window_context(window);
299
300 if (!ctx || !ctx->swapchain) {
302 "readback_swapchain_region: window '{}' has no swapchain",
303 window->get_create_info().title);
304 return false;
305 }
306
307 if (!m_resource_manager) {
309 "readback_swapchain_region: resource manager not set");
310 return false;
311 }
312
313 const auto& images = ctx->swapchain->get_images();
314 if (ctx->current_image_index >= images.size()) {
316 "readback_swapchain_region: invalid image index {} for '{}'",
317 ctx->current_image_index, window->get_create_info().title);
318 return false;
319 }
320
321 auto proxy = std::make_shared<VKImage>(
322 pixel_width, pixel_height, 1U,
323 ctx->swapchain->get_image_format(),
326 1U, 1U,
328
329 VKImageResources res {};
330 res.image = images[ctx->current_image_index];
331 proxy->set_image_resources(res);
332 proxy->set_current_layout(vk::ImageLayout::ePresentSrcKHR);
333
335 proxy,
336 dst,
337 byte_count,
338 nullptr,
339 false,
340 vk::ImageLayout::ePresentSrcKHR,
341 vk::PipelineStageFlagBits::eBottomOfPipe);
342
343 return true;
344 };
345
346 display_service->ensure_depth_attachment = [this](const std::shared_ptr<void>& window_ptr) {
347 auto window = std::static_pointer_cast<Window>(window_ptr);
348 auto* ctx = find_window_context(window);
349 if (!ctx) {
351 "ensure_depth_attachment: window '{}' not registered",
352 window->get_create_info().title);
353 return;
354 }
355 ensure_depth_image(*ctx);
356 };
357
358 display_service->get_depth_image_view = [this](const std::shared_ptr<void>& window_ptr) -> void* {
359 auto window = std::static_pointer_cast<Window>(window_ptr);
360 auto* ctx = find_window_context(window);
361 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
362 return nullptr;
363 }
364 static thread_local vk::ImageView view;
365 view = ctx->depth_image->get_image_view();
366 return static_cast<void*>(&view);
367 };
368
369 display_service->get_depth_format = [this](const std::shared_ptr<void>& window_ptr) -> uint32_t {
370 auto window = std::static_pointer_cast<Window>(window_ptr);
371 auto* ctx = find_window_context(window);
372 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
373 return static_cast<uint32_t>(vk::Format::eUndefined);
374 }
375 return static_cast<uint32_t>(ctx->depth_image->get_format());
376 };
377
378 // ======================================================================
379 // get_last_frame – hazard‑pointer protected read + copy
380 // ======================================================================
381 display_service->get_last_frame = [this](const std::shared_ptr<void>& window_ptr)
382 -> std::shared_ptr<std::vector<uint8_t>> {
383 auto* ctx = find_window_context(std::static_pointer_cast<Window>(window_ptr));
384 if (!ctx || !ctx->capture)
385 return nullptr;
386
387#ifdef MAYAFLUX_PLATFORM_MACOS
388 auto& state = *ctx->capture;
389
390 size_t slot = CaptureState::LAST_FRAME_MAX_READERS;
391 for (size_t i = 0; i < CaptureState::LAST_FRAME_MAX_READERS; ++i) {
392 bool expected = false;
393 if (state.last_frame_slot_active[i].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
394 slot = i;
395 break;
396 }
397 }
398 if (slot == CaptureState::LAST_FRAME_MAX_READERS)
399 return nullptr;
400
401 const std::vector<uint8_t>* raw;
402 do {
403 raw = state.last_frame.load(std::memory_order_acquire);
404 state.last_frame_hazard_ptrs[slot].store(raw, std::memory_order_release);
405 } while (raw != state.last_frame.load(std::memory_order_acquire));
406
407 if (!raw) {
408 state.last_frame_hazard_ptrs[slot].store(nullptr, std::memory_order_release);
409 state.last_frame_slot_active[slot].store(false, std::memory_order_release);
410 return nullptr;
411 }
412
413 return std::shared_ptr<std::vector<uint8_t>>(
414 const_cast<std::vector<uint8_t>*>(raw),
415 [&state, slot](std::vector<uint8_t>*) {
416 state.last_frame_hazard_ptrs[slot].store(nullptr, std::memory_order_release);
417 state.last_frame_slot_active[slot].store(false, std::memory_order_release);
418 });
419#else
420 return ctx->capture->last_frame.load(std::memory_order_acquire);
421#endif
422 };
423
424 // ======================================================================
425 // register_frame_observer – hazard‑pointer update of observer map
426 // ======================================================================
427 display_service->register_frame_observer = [this](
428 const std::shared_ptr<void>& window_ptr,
429 const std::function<void(
430 const std::shared_ptr<std::vector<uint8_t>>&,
431 uint32_t, uint32_t, uint32_t)>&
432 cb) -> uint32_t {
433 auto* ctx = find_window_context(std::static_pointer_cast<Window>(window_ptr));
434 if (!ctx) {
436 "register_frame_observer: window not registered with graphics backend");
437 return 0;
438 }
439 if (!ctx->capture) {
441 }
442 if (!ctx->capture) {
444 "register_frame_observer: capture is not enabled for '{}'",
445 ctx->window->get_create_info().title);
446 return 0;
447 }
448
449 auto& state = *ctx->capture;
450 uint32_t id = state.next_observer_id.fetch_add(1, std::memory_order_relaxed);
451
452#ifdef MAYAFLUX_PLATFORM_MACOS
453 size_t obs_slot = CaptureState::OBSERVERS_MAX_READERS;
454 for (size_t i = 0; i < CaptureState::OBSERVERS_MAX_READERS; ++i) {
455 bool expected = false;
456 if (state.observers_slot_active[i].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
457 obs_slot = i;
458 break;
459 }
460 }
461 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
462 return 0;
463
465 CaptureState::ObserverMap* new_map = nullptr;
466
467 do {
468 if (new_map)
469 delete new_map;
470
471 do {
472 current = state.observers.load(std::memory_order_acquire);
473 state.observers_hazard_ptrs[obs_slot].store(current, std::memory_order_release);
474 } while (current != state.observers.load(std::memory_order_acquire));
475
477 (*new_map)[id] = cb;
478
479 } while (!state.observers.compare_exchange_weak(current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
480
481 state.observers_hazard_ptrs[obs_slot].store(nullptr, std::memory_order_release);
482 state.observers_slot_active[obs_slot].store(false, std::memory_order_release);
483
484 if (current)
485 state.retire_observers(current);
486#else
487 auto current = state.observers.load(std::memory_order_acquire);
488 std::shared_ptr<CaptureState::ObserverMap> next;
489 do {
490 next = std::make_shared<CaptureState::ObserverMap>(*current);
491 (*next)[id] = cb;
492 } while (!state.observers.compare_exchange_weak(
493 current, next,
494 std::memory_order_release, std::memory_order_acquire));
495#endif
496 return id;
497 };
498
499 // ======================================================================
500 // unregister_frame_observer – hazard‑pointer update
501 // ======================================================================
502 display_service->unregister_frame_observer = [this](
503 const std::shared_ptr<void>& window_ptr, uint32_t id) {
504 auto* ctx = find_window_context(std::static_pointer_cast<Window>(window_ptr));
505 if (!ctx) {
507 "unregister_frame_observer: window not registered with graphics backend");
508 return;
509 }
510 if (!ctx->capture) {
512 "unregister_frame_observer: capture not active for '{}'",
513 ctx->window->get_create_info().title);
514 return;
515 }
516
517 auto& state = *ctx->capture;
518
519#ifdef MAYAFLUX_PLATFORM_MACOS
520 size_t obs_slot = CaptureState::OBSERVERS_MAX_READERS;
521 for (size_t i = 0; i < CaptureState::OBSERVERS_MAX_READERS; ++i) {
522 bool expected = false;
523 if (state.observers_slot_active[i].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
524 obs_slot = i;
525 break;
526 }
527 }
528 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
529 return;
530
532 CaptureState::ObserverMap* new_map = nullptr;
533
534 do {
535 if (new_map)
536 delete new_map;
537
538 do {
539 current = state.observers.load(std::memory_order_acquire);
540 state.observers_hazard_ptrs[obs_slot].store(current, std::memory_order_release);
541 } while (current != state.observers.load(std::memory_order_acquire));
542
544 new_map->erase(id);
545
546 } while (!state.observers.compare_exchange_weak(current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
547
548 state.observers_hazard_ptrs[obs_slot].store(nullptr, std::memory_order_release);
549 state.observers_slot_active[obs_slot].store(false, std::memory_order_release);
550
551 if (current)
552 state.retire_observers(current);
553#else
554 auto current = state.observers.load(std::memory_order_acquire);
555 std::shared_ptr<CaptureState::ObserverMap> next;
556 do {
557 next = std::make_shared<CaptureState::ObserverMap>(*current);
558 next->erase(id);
559 } while (!state.observers.compare_exchange_weak(
560 current, next,
561 std::memory_order_release, std::memory_order_acquire));
562#endif
563 };
564}
565
567{
568 auto it = std::ranges::find_if(m_window_contexts,
569 [window](const auto& config) { return config.window == window; });
570 return it != m_window_contexts.end() ? &(*it) : nullptr;
571}
572
573const WindowRenderContext* BackendWindowHandler::find_window_context(const std::shared_ptr<Window>& window) const
574{
575 auto it = std::ranges::find_if(m_window_contexts,
576 [window](const auto& config) { return config.window == window; });
577 return it != m_window_contexts.end() ? &(*it) : nullptr;
578}
579
580bool BackendWindowHandler::register_window(const std::shared_ptr<Window>& window)
581{
582 if (window->is_graphics_registered() || find_window_context(window)) {
583 return false;
584 }
585
586 vk::SurfaceKHR surface = m_context.create_surface(window);
587 if (!surface) {
589 "Failed to create Vulkan surface for window '{}'",
590 window->get_create_info().title);
591 return false;
592 }
593
596 "Graphics queue family cannot present to window '{}'", window->get_create_info().title);
597 m_context.destroy_surface(surface);
598 return false;
599 }
600
601 WindowRenderContext config;
602 config.window = window;
603 config.surface = surface;
604 config.swapchain = std::make_unique<VKSwapchain>();
605
606 if (!config.swapchain->create(m_context, surface, window->get_create_info())) {
608 "Failed to create swapchain for window '{}'", window->get_create_info().title);
609 m_context.destroy_surface(surface);
610 return false;
611 }
612
613 if (!create_sync_objects(config)) {
615 "Failed to create sync objects for window '{}'",
616 window->get_create_info().title);
617 config.swapchain->cleanup();
618 m_context.destroy_surface(surface);
619 return false;
620 }
621
622 m_window_contexts.emplace_back(std::move(config));
623 window->set_graphics_registered(true);
624
625 window->set_event_callback([this, window_ptr = window](const WindowEvent& event) {
628 auto* config = find_window_context(window_ptr);
629 if (config) {
630 config->needs_recreation = true;
631 }
632 }
633 });
634
636 "Registered window '{}' for graphics processing", window->get_create_info().title);
637
638 return true;
639}
640
641bool BackendWindowHandler::create_sync_objects(WindowRenderContext& config)
642{
643 auto device = m_context.get_device();
644 uint32_t image_count = config.swapchain->get_image_count();
645
646 try {
647 config.image_available.resize(image_count);
648 config.render_finished.resize(image_count);
649 config.in_flight.resize(image_count);
650 config.slot_submission.assign(image_count, 0);
651 config.submitted_frames = 0;
652 config.clear_command_buffers.resize(image_count);
653
654 vk::SemaphoreCreateInfo semaphore_info {};
655 vk::FenceCreateInfo fence_info {};
656 fence_info.flags = vk::FenceCreateFlagBits::eSignaled;
657
658 for (uint32_t i = 0; i < image_count; ++i) {
659 config.image_available[i] = device.createSemaphore(semaphore_info);
660 config.render_finished[i] = device.createSemaphore(semaphore_info);
661
662 config.clear_command_buffers[i] = m_command_manager.allocate_command_buffer(
663 vk::CommandBufferLevel::ePrimary);
664 }
665
666 for (auto& i : config.in_flight) {
667 i = device.createFence(fence_info);
668 }
669
670 config.current_frame = 0;
671
672 return true;
673 } catch (const vk::SystemError& e) {
675 "Failed to create sync objects: {}", e.what());
676 return false;
677 }
678}
679
680void BackendWindowHandler::recreate_swapchain_for_context(WindowRenderContext& context)
681{
682 m_context.wait_idle();
683
684 if (recreate_swapchain_internal(context)) {
685 context.needs_recreation = false;
687 "Recreated swapchain for window '{}' ({}x{})",
688 context.window->get_create_info().title,
689 context.window->get_state().current_width,
690 context.window->get_state().current_height);
691 }
692}
693
694bool BackendWindowHandler::recreate_swapchain_internal(WindowRenderContext& context)
695{
696 const auto& state = context.window->get_state();
697
698 if (!context.swapchain->recreate(state.current_width, state.current_height)) {
700 "Failed to recreate swapchain for window '{}'",
701 context.window->get_create_info().title);
702 return false;
703 }
704
705 if (context.depth_image) {
706 auto device = m_context.get_device();
707 const auto& res = context.depth_image->get_image_resources();
708 if (res.image_view) {
709 device.destroyImageView(res.image_view);
710 }
711 if (res.memory) {
712 device.freeMemory(res.memory);
713 }
714 if (res.image) {
715 device.destroyImage(res.image);
716 }
717 context.depth_image.reset();
718 }
719
720 return true;
721}
722
723void BackendWindowHandler::ensure_depth_image(WindowRenderContext& ctx)
724{
725 if (!ctx.swapchain) {
726 return;
727 }
728
729 auto extent = ctx.swapchain->get_extent();
730
731 if (ctx.depth_image && ctx.depth_image->is_initialized()
732 && ctx.depth_image->get_width() == extent.width
733 && ctx.depth_image->get_height() == extent.height) {
734 return;
735 }
736
737 auto device = m_context.get_device();
738
739 if (ctx.depth_image && ctx.depth_image->is_initialized()) {
740 device.waitIdle();
741 const auto& res = ctx.depth_image->get_image_resources();
742 if (res.image_view) {
743 device.destroyImageView(res.image_view);
744 }
745 if (res.memory) {
746 device.freeMemory(res.memory);
747 }
748 if (res.image) {
749 device.destroyImage(res.image);
750 }
751 ctx.depth_image.reset();
752 }
753
754 ctx.depth_image = std::make_shared<VKImage>(
755 extent.width, extent.height, 1,
756 vk::Format::eD32Sfloat,
757 VKImage::Usage::DEPTH_STENCIL,
758 VKImage::Type::TYPE_2D,
759 1, 1,
761
762 m_resource_manager->initialize_image(ctx.depth_image);
763
764 if (!ctx.depth_image->is_initialized()) {
766 "Failed to create depth image for window '{}'",
767 ctx.window->get_create_info().title);
768 ctx.depth_image.reset();
769 return;
770 }
771
772 m_resource_manager->transition_image_layout(
773 ctx.depth_image->get_image(),
774 vk::ImageLayout::eUndefined,
775 vk::ImageLayout::eDepthStencilAttachmentOptimal,
776 1, 1,
777 vk::ImageAspectFlagBits::eDepth);
778 ctx.depth_image->set_current_layout(vk::ImageLayout::eDepthStencilAttachmentOptimal);
779
781 "Created depth image for window '{}': {}x{}, D32_SFLOAT",
782 ctx.window->get_create_info().title,
783 extent.width, extent.height);
784}
785
786void BackendWindowHandler::render_window(const std::shared_ptr<Window>& window)
787{
788 auto ctx = find_window_context(window);
789 if (!ctx) {
791 "Window '{}' not registered for rendering",
792 window->get_create_info().title);
793 return;
794 }
795
796 if (ctx->window && ctx->window->get_rendering_buffers().empty()) {
797 render_empty_window(*ctx);
798 }
799}
800
801void BackendWindowHandler::render_all_windows()
802{
804 "render_all_windows() is not implemented in BackendWindowHandler. Dynamic rendering is expected to be handled externally.");
805}
806
807void BackendWindowHandler::render_empty_window(WindowRenderContext& ctx)
808{
809 auto window = ctx.window;
810
811 if (!window || !window->get_state().is_visible || !window->get_rendering_buffers().empty()) {
812 return;
813 }
814
815 try {
816 auto device = m_context.get_device();
817
818 size_t frame_index = ctx.current_frame;
819 auto& in_flight = ctx.in_flight[frame_index];
820 auto& image_available = ctx.image_available[frame_index];
821 auto& render_finished = ctx.render_finished[frame_index];
822
823 auto cmd_buffer = ctx.clear_command_buffers[frame_index];
824 if (!cmd_buffer) {
826 "Clear command buffer not allocated for window '{}'",
827 window->get_create_info().title);
828 return;
829 }
830
831 if (device.waitForFences(1, &in_flight, VK_TRUE, UINT64_MAX) == vk::Result::eTimeout) {
832 return;
833 }
834 cmd_buffer.reset({});
835
836 auto image_index_opt = ctx.swapchain->acquire_next_image(image_available);
837 if (!image_index_opt.has_value()) {
838 ctx.needs_recreation = true;
839 return;
840 }
841 ctx.current_image_index = image_index_opt.value();
842
843 if (device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
844 return;
845 }
846
847 vk::CommandBufferBeginInfo begin_info {};
848 begin_info.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
849 cmd_buffer.begin(begin_info);
850
851 const auto& image_views = ctx.swapchain->get_image_views();
852 auto extent = ctx.swapchain->get_extent();
853
854 vk::RenderingAttachmentInfo color_attachment {};
855 color_attachment.imageView = image_views[ctx.current_image_index];
856 color_attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
857 color_attachment.loadOp = vk::AttachmentLoadOp::eClear;
858 color_attachment.storeOp = vk::AttachmentStoreOp::eStore;
859 color_attachment.clearValue.color = vk::ClearColorValue(
860 window->get_create_info().clear_color);
861
862 vk::RenderingInfo rendering_info {};
863 rendering_info.renderArea = vk::Rect2D { { 0, 0 }, extent };
864 rendering_info.layerCount = 1;
865 rendering_info.colorAttachmentCount = 1;
866 rendering_info.pColorAttachments = &color_attachment;
867
868 vk::ImageMemoryBarrier barrier {};
869 barrier.oldLayout = vk::ImageLayout::eUndefined;
870 barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal;
871 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
872 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
873 barrier.image = ctx.swapchain->get_images()[ctx.current_image_index];
874 barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
875 barrier.subresourceRange.levelCount = 1;
876 barrier.subresourceRange.layerCount = 1;
877 barrier.srcAccessMask = {};
878 barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
879
880 cmd_buffer.pipelineBarrier(
881 vk::PipelineStageFlagBits::eTopOfPipe,
882 vk::PipelineStageFlagBits::eColorAttachmentOutput,
883 {}, {}, {}, barrier);
884
885 cmd_buffer.beginRendering(rendering_info);
886 cmd_buffer.endRendering();
887
888 barrier.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
889 barrier.newLayout = vk::ImageLayout::ePresentSrcKHR;
890 barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
891 barrier.dstAccessMask = {};
892
893 cmd_buffer.pipelineBarrier(
894 vk::PipelineStageFlagBits::eColorAttachmentOutput,
895 vk::PipelineStageFlagBits::eBottomOfPipe,
896 {}, {}, {}, barrier);
897
898 cmd_buffer.end();
899
900 submit_and_present(window, cmd_buffer);
901
902 ctx.current_frame = (ctx.current_frame + 1) % ctx.in_flight.size();
903
904 } catch (const std::exception& e) {
906 "Failed to render empty window '{}': {}",
907 window->get_create_info().title, e.what());
908 }
909}
910
911void BackendWindowHandler::ensure_capture_state(WindowRenderContext& ctx)
912{
913 if (ctx.capture || !ctx.swapchain || !ctx.window->is_capture_enabled())
914 return;
915
916 auto dev = m_context.get_device();
917 const vk::Format fmt = ctx.swapchain->get_image_format();
918 const uint32_t bpp = Core::vk_format_bytes_per_pixel(fmt);
919
920 ctx.capture = std::make_unique<CaptureState>();
921 ctx.capture->format = fmt;
922 ctx.capture->bpp = bpp;
923
924 const uint32_t count = ctx.swapchain->get_image_count();
925 ctx.capture->slots.reserve(count);
926 for (uint32_t i = 0; i < count; ++i) {
927 auto slot = std::make_unique<CaptureSlot>();
928 slot->cmd = m_command_manager.allocate_command_buffer(
929 vk::CommandBufferLevel::ePrimary);
930 slot->fence = dev.createFence(vk::FenceCreateInfo { vk::FenceCreateFlagBits::eSignaled });
931 ctx.capture->slots.push_back(std::move(slot));
932 }
933
934 start_readback_thread(*ctx.capture, dev);
935}
936
937void BackendWindowHandler::capture_frame(WindowRenderContext& ctx)
938{
939 if (!ctx.swapchain || ctx.window->get_rendering_buffers().empty()
940 || !ctx.window->is_capture_enabled())
941 return;
942
943 auto dev = m_context.get_device();
944 const auto ext = ctx.swapchain->get_extent();
945 const vk::Format fmt = ctx.swapchain->get_image_format();
946 const uint32_t bpp = Core::vk_format_bytes_per_pixel(fmt);
947 const vk::Image src = ctx.swapchain->get_images()[ctx.current_image_index];
948
949 if (!ctx.capture) {
950 ensure_capture_state(ctx);
951 }
952
953 auto& slot = *ctx.capture->slots[ctx.capture->slot_index];
954
955 if (slot.pending.load(std::memory_order_acquire))
956 return;
957
958 if (slot.image && slot.extent != ext) {
959 dev.destroyImage(slot.image);
960 dev.freeMemory(slot.mem);
961 slot.image = vk::Image {};
962 slot.mem = vk::DeviceMemory {};
963 }
964
965 if (!slot.image) {
966 vk::ImageCreateInfo ici {};
967 ici.imageType = vk::ImageType::e2D;
968 ici.format = fmt;
969 ici.extent = vk::Extent3D { ext.width, ext.height, 1 };
970 ici.arrayLayers = 1;
971 ici.mipLevels = 1;
972 ici.samples = vk::SampleCountFlagBits::e1;
973 ici.tiling = vk::ImageTiling::eLinear;
974 ici.usage = vk::ImageUsageFlagBits::eTransferDst;
975
976 slot.image = dev.createImage(ici);
977
978 auto req = dev.getImageMemoryRequirements(slot.image);
979 vk::MemoryAllocateInfo mai {};
980 mai.allocationSize = req.size;
981 mai.memoryTypeIndex = m_resource_manager->find_memory_type(
982 req.memoryTypeBits,
983 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
984
985 slot.mem = dev.allocateMemory(mai);
986 dev.bindImageMemory(slot.image, slot.mem, 0);
987 slot.extent = ext;
988 }
989
990 slot.cmd.reset({});
991 vk::CommandBufferBeginInfo bi {};
992 bi.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
993 slot.cmd.begin(bi);
994
995 vk::ImageMemoryBarrier b {};
996 b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
997 b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
998 b.subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 };
999
1000 b.image = slot.image;
1001 b.oldLayout = vk::ImageLayout::eUndefined;
1002 b.newLayout = vk::ImageLayout::eTransferDstOptimal;
1003 b.srcAccessMask = {};
1004 b.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
1005 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1006 vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, b);
1007
1008 b.image = src;
1009 b.oldLayout = vk::ImageLayout::ePresentSrcKHR;
1010 b.newLayout = vk::ImageLayout::eTransferSrcOptimal;
1011 b.srcAccessMask = vk::AccessFlagBits::eMemoryRead;
1012 b.dstAccessMask = vk::AccessFlagBits::eTransferRead;
1013 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1014 vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, b);
1015
1016 vk::ImageCopy copy {};
1017 copy.srcSubresource = { vk::ImageAspectFlagBits::eColor, 0, 0, 1 };
1018 copy.dstSubresource = { vk::ImageAspectFlagBits::eColor, 0, 0, 1 };
1019 copy.extent = vk::Extent3D { ext.width, ext.height, 1 };
1020 slot.cmd.copyImage(src, vk::ImageLayout::eTransferSrcOptimal,
1021 slot.image, vk::ImageLayout::eTransferDstOptimal, 1, &copy);
1022
1023 b.image = slot.image;
1024 b.oldLayout = vk::ImageLayout::eTransferDstOptimal;
1025 b.newLayout = vk::ImageLayout::eGeneral;
1026 b.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
1027 b.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
1028 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1029 vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, b);
1030
1031 b.image = src;
1032 b.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
1033 b.newLayout = vk::ImageLayout::ePresentSrcKHR;
1034 b.srcAccessMask = vk::AccessFlagBits::eTransferRead;
1035 b.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
1036 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1037 vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, b);
1038
1039 slot.cmd.end();
1040
1041 (void)dev.resetFences(1, &slot.fence);
1042
1043 vk::SubmitInfo si {};
1044 si.commandBufferCount = 1;
1045 si.pCommandBuffers = &slot.cmd;
1046
1047 slot.pending.store(true, std::memory_order_release);
1048
1049 if (auto result = m_context.get_graphics_queue().submit(1, &si, slot.fence); result != vk::Result::eSuccess) {
1051 "Failed to submit capture command buffer for window '{}': {}",
1052 ctx.window->get_create_info().title, vk::to_string(result));
1053 slot.pending.store(false, std::memory_order_release);
1054 return;
1055 }
1056
1057 ctx.capture->slot_index = (ctx.capture->slot_index + 1) % ctx.capture->slots.size();
1058}
1059
1060void BackendWindowHandler::start_readback_thread(CaptureState& state, vk::Device dev)
1061{
1062 state.readback_running.store(true, std::memory_order_release);
1063
1064 state.readback_thread = std::thread([&state, dev]() {
1065 while (state.readback_running.load(std::memory_order_acquire)) {
1066 bool any = false;
1067
1068 for (auto& slot_ptr : state.slots) {
1069 auto& slot = *slot_ptr;
1070
1071 if (!slot.pending.load(std::memory_order_acquire))
1072 continue;
1073
1074 if (dev.getFenceStatus(slot.fence) != vk::Result::eSuccess)
1075 continue;
1076
1077 const size_t nb = static_cast<size_t>(slot.extent.width)
1078 * slot.extent.height * state.bpp;
1079
1080 vk::SubresourceLayout layout = dev.getImageSubresourceLayout(
1081 slot.image, { vk::ImageAspectFlagBits::eColor, 0, 0 });
1082
1083 const auto* mapped = static_cast<const uint8_t*>(
1084 dev.mapMemory(slot.mem, 0, VK_WHOLE_SIZE));
1085 mapped += layout.offset;
1086
1087 auto buf = std::make_shared<std::vector<uint8_t>>(nb);
1088 const uint32_t row_bytes = slot.extent.width * state.bpp;
1089 for (uint32_t y = 0; y < slot.extent.height; ++y) {
1090 std::memcpy(buf->data() + static_cast<size_t>(y * row_bytes),
1091 mapped + y * layout.rowPitch,
1092 row_bytes);
1093 }
1094
1095 dev.unmapMemory(slot.mem);
1096
1097 // ---------------------------------------------------------------
1098 // Store new frame into last_frame (same pattern as InputManager)
1099 // ---------------------------------------------------------------
1100#ifdef MAYAFLUX_PLATFORM_MACOS
1101 auto* raw_frame = new std::vector<uint8_t>(*buf);
1102 auto* old = state.last_frame.exchange(raw_frame, std::memory_order_acq_rel);
1103 if (old)
1104 state.retire_last_frame(old);
1105#else
1106 state.last_frame.store(buf, std::memory_order_release);
1107#endif
1108
1109// ---------------------------------------------------------------
1110// Read observers and invoke callbacks
1111// ---------------------------------------------------------------
1112#ifdef MAYAFLUX_PLATFORM_MACOS
1113 size_t obs_slot = CaptureState::OBSERVERS_MAX_READERS;
1114 for (size_t i = 0; i < CaptureState::OBSERVERS_MAX_READERS; ++i) {
1115 bool expected = false;
1116 if (state.observers_slot_active[i].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
1117 obs_slot = i;
1118 break;
1119 }
1120 }
1121
1122 if (obs_slot != CaptureState::OBSERVERS_MAX_READERS) {
1123 const CaptureState::ObserverMap* obs_current;
1124 do {
1125 obs_current = state.observers.load(std::memory_order_acquire);
1126 state.observers_hazard_ptrs[obs_slot].store(obs_current, std::memory_order_release);
1127 } while (obs_current != state.observers.load(std::memory_order_acquire));
1128
1129 if (obs_current) {
1130 for (const auto& [id, cb] : *obs_current) {
1131 cb(buf, slot.extent.width, slot.extent.height,
1132 static_cast<uint32_t>(state.format));
1133 }
1134 }
1135
1136 state.observers_hazard_ptrs[obs_slot].store(nullptr, std::memory_order_release);
1137 state.observers_slot_active[obs_slot].store(false, std::memory_order_release);
1138 }
1139#else
1140 auto obs = state.observers.load(std::memory_order_acquire);
1141 if (obs) {
1142 for (const auto& [id, cb] : *obs) {
1143 cb(buf, slot.extent.width, slot.extent.height,
1144 static_cast<uint32_t>(state.format));
1145 }
1146 }
1147#endif
1148
1149 slot.pending.store(false, std::memory_order_release);
1150 any = true;
1151 }
1152
1153 if (!any)
1154 std::this_thread::sleep_for(std::chrono::microseconds(200));
1155 }
1156 });
1157}
1158
1159void BackendWindowHandler::submit_and_present(
1160 const std::shared_ptr<Window>& window,
1161 const vk::CommandBuffer& command_buffer)
1162{
1163 auto* ctx = find_window_context(window);
1164 if (!ctx) {
1166 "Window not registered for submit_and_present");
1167 return;
1168 }
1169
1170 auto graphics_queue = m_context.get_graphics_queue();
1171
1172 size_t frame_index = ctx->current_frame;
1173 auto& in_flight = ctx->in_flight[frame_index];
1174 auto& image_available = ctx->image_available[frame_index];
1175 auto& render_finished = ctx->render_finished[frame_index];
1176
1177 vk::Semaphore deferred_sem = m_resource_manager
1178 ? m_resource_manager->flush_deferred_commands()
1179 : nullptr;
1180
1181 std::vector<vk::Semaphore> wait_sems { image_available };
1182 std::vector<vk::PipelineStageFlags> wait_stages {
1183 vk::PipelineStageFlagBits::eColorAttachmentOutput
1184 };
1185
1186 if (deferred_sem) {
1187 wait_sems.push_back(deferred_sem);
1188 wait_stages.emplace_back(vk::PipelineStageFlagBits::eFragmentShader);
1189 }
1190
1191 vk::SubmitInfo submit_info {};
1192 submit_info.waitSemaphoreCount = static_cast<uint32_t>(wait_sems.size());
1193 submit_info.pWaitSemaphores = wait_sems.data();
1194 submit_info.pWaitDstStageMask = wait_stages.data();
1195 submit_info.commandBufferCount = 1;
1196 submit_info.pCommandBuffers = &command_buffer;
1197 submit_info.signalSemaphoreCount = 1;
1198 submit_info.pSignalSemaphores = &render_finished;
1199
1200 try {
1201 auto result = graphics_queue.submit(1, &submit_info, in_flight);
1202 ctx->slot_submission[frame_index] = ++ctx->submitted_frames;
1203 } catch (const vk::SystemError& e) {
1205 "Failed to submit primary command buffer: {}", e.what());
1206 return;
1207 }
1208
1209 bool present_success = ctx->swapchain->present(
1210 ctx->current_image_index, render_finished, graphics_queue);
1211
1212 if (!present_success) {
1213 ctx->needs_recreation = true;
1214 }
1215
1216 if (present_success)
1217 capture_frame(*ctx);
1218
1219 ctx->current_frame = (frame_index + 1) % ctx->in_flight.size();
1220
1222 "Window '{}': frame submitted and presented",
1223 window->get_create_info().title);
1224}
1225
1226uint64_t BackendWindowHandler::completed_frame_count(const WindowRenderContext& ctx) const
1227{
1228 const auto device = m_context.get_device();
1229
1230 uint64_t oldest_pending = ctx.submitted_frames + 1;
1231 for (size_t slot = 0; slot < ctx.in_flight.size(); ++slot) {
1232 const uint64_t submission = ctx.slot_submission[slot];
1233 if (submission == 0) {
1234 continue;
1235 }
1236 if (device.getFenceStatus(ctx.in_flight[slot]) == vk::Result::eNotReady) {
1237 oldest_pending = std::min(oldest_pending, submission);
1238 }
1239 }
1240
1241 return oldest_pending - 1;
1242}
1243
1244bool BackendWindowHandler::is_window_registered(const std::shared_ptr<Window>& window) const
1245{
1246 return find_window_context(window) != nullptr;
1247}
1248
1249void BackendWindowHandler::unregister_window(const std::shared_ptr<Window>& window)
1250{
1251 auto it = m_window_contexts.begin();
1252 while (it != m_window_contexts.end()) {
1253 if (it->window == window) {
1254 it->cleanup(m_context);
1256 "Unregistered window '{}'", it->window->get_create_info().title);
1257 it = m_window_contexts.erase(it);
1258 return;
1259 }
1260 ++it;
1261 }
1262}
1263
1264void BackendWindowHandler::handle_window_resize()
1265{
1266 for (auto& context : m_window_contexts) {
1267 if (context.needs_recreation) {
1268 recreate_swapchain_for_context(context);
1269 }
1270 }
1271}
1272
1273void BackendWindowHandler::cleanup()
1274{
1275 for (auto& config : m_window_contexts) {
1276 config.cleanup(m_context);
1277 }
1278 m_window_contexts.clear();
1279}
1280
1281} // namespace MayaFlux::Core
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_RT_WARN(comp, ctx,...)
#define MF_RT_ERROR(comp, ctx,...)
#define MF_RT_TRACE(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
vk::Fence fence
glm::vec2 current
std::weak_ptr< Core::Window > window
Definition Picker.cpp:23
uint32_t * dst
const uint32_t * src
size_t b
vk::PhysicalDevice device
Definition VKDevice.cpp:121
size_t count
const uint8_t * ptr
uint32_t width
uint32_t height
void download_image_data(std::shared_ptr< VKImage > image, void *data, size_t size, const std::shared_ptr< Buffers::VKBuffer > &staging=nullptr, bool deferred=false, vk::ImageLayout restore_layout=vk::ImageLayout::eShaderReadOnlyOptimal, vk::PipelineStageFlags restore_stage=vk::PipelineStageFlagBits::eFragmentShader)
Download image data to a host pointer.
void ensure_depth_image(WindowRenderContext &ctx)
Ensure depth image exists at current swapchain extent.
bool register_window(const std::shared_ptr< Window > &window)
uint64_t completed_frame_count(const WindowRenderContext &ctx) const
Submitted frames of a window whose GPU work has finished.
std::vector< WindowRenderContext > m_window_contexts
void setup_backend_service(const std::shared_ptr< Registry::Service::DisplayService > &display_service)
BackendWindowHandler(VKContext &context, VKCommandManager &command_manager)
void submit_and_present(const std::shared_ptr< Window > &window, const vk::CommandBuffer &command_buffer)
bool create_sync_objects(WindowRenderContext &config)
Create synchronization objects for a window's swapchain.
WindowRenderContext * find_window_context(const std::shared_ptr< Window > &window)
void ensure_capture_state(WindowRenderContext &ctx)
Ensure capture state is initialized for a window context.
Manages Vulkan command pools and command buffers.
bool graphics_family_can_present(vk::SurfaceKHR surface) const
Confirm the graphics family presents to a surface.
Definition VKContext.hpp:79
vk::Device get_device() const
Get logical device.
Definition VKContext.hpp:49
vk::SurfaceKHR create_surface(std::shared_ptr< Window > window)
Create surface from window's native handles.
Definition VKContext.cpp:74
void destroy_surface(vk::SurfaceKHR surface)
Destroy a specific surface Called when window is unregistered.
High-level wrapper for Vulkan instance and device.
Definition VKContext.hpp:16
@ TEXTURE_2D
Sampled texture (shader read)
uint32_t vk_format_bytes_per_pixel(vk::Format fmt)
Byte width of a single pixel for a given Vulkan format.
@ GraphicsBackend
Graphics/visual rendering backend (Vulkan, OpenGL)
@ GraphicsSubsystem
Graphics subsystem operations (Vulkan, rendering pipeline)
@ GraphicsCallback
Graphics/visual rendering callback - frame-rate real-time.
@ Core
Core engine, backend, subsystems.
@ IMAGE_COLOR
2D RGB/RGBA image
@ IMAGE_2D
2D image (grayscale or single channel)
std::unordered_map< uint32_t, FrameObserver > ObserverMap
Vulkan image resource handles.
Definition VKImage.hpp:15
Event data for window and input events.
std::vector< vk::Semaphore > image_available
std::unique_ptr< CaptureState > capture
std::unique_ptr< VKSwapchain > swapchain
std::vector< vk::Semaphore > render_finished
std::vector< vk::CommandBuffer > clear_command_buffers