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