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);
148 display_service->wait_idle = [
this]() {
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);
156 if (ctx.window == window) {
157 ctx.needs_recreation =
true;
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);
166 if (ctx.window == window) {
167 return static_cast<uint32_t
>(ctx.swapchain->get_image_count());
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);
176 if (ctx.window == window) {
177 return static_cast<int>(ctx.swapchain->get_image_format());
183 display_service->get_swapchain_extent = [
this](
184 const std::shared_ptr<void>& window_ptr,
186 uint32_t& out_height) {
187 auto window = std::static_pointer_cast<Window>(window_ptr);
190 if (context && context->swapchain) {
191 auto extent = context->swapchain->get_extent();
192 out_width = extent.width;
193 out_height = extent.height;
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);
205 "Window '{}' not registered for swapchain acquisition",
206 window->get_create_info().title);
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];
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);
222 auto image_index_opt = ctx->swapchain->acquire_next_image(image_available);
223 if (!image_index_opt.has_value()) {
224 ctx->needs_recreation =
true;
228 ctx->current_image_index = image_index_opt.value();
230 if (device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
232 "Failed to reset fence for window '{}'",
233 window->get_create_info().title);
237 const auto& images = ctx->swapchain->get_images();
238 VkImage raw = images[ctx->current_image_index];
239 return reinterpret_cast<uint64_t
>(raw);
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);
246 if (!context || !context->swapchain) {
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,
260 static thread_local vk::ImageView view;
261 view = image_views[context->current_image_index];
262 return static_cast<void*
>(&view);
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);
268 if (!ctx || !ctx->swapchain)
271 const auto& images = ctx->swapchain->get_images();
272 if (ctx->current_image_index >= images.size())
275 return reinterpret_cast<uint64_t
>(
static_cast<VkImage
>(images[ctx->current_image_index]));
278 display_service->readback_swapchain_region = [
this](
279 const std::shared_ptr<void>& window_ptr,
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);
289 if (!ctx || !ctx->swapchain) {
291 "readback_swapchain_region: window '{}' has no swapchain",
292 window->get_create_info().title);
298 "readback_swapchain_region: resource manager not set");
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);
310 auto proxy = std::make_shared<VKImage>(
311 pixel_width, pixel_height, 1U,
312 ctx->swapchain->get_image_format(),
319 res.
image = images[ctx->current_image_index];
320 proxy->set_image_resources(res);
321 proxy->set_current_layout(vk::ImageLayout::ePresentSrcKHR);
329 vk::ImageLayout::ePresentSrcKHR,
330 vk::PipelineStageFlagBits::eBottomOfPipe);
335 display_service->ensure_depth_attachment = [
this](
const std::shared_ptr<void>& window_ptr) {
336 auto window = std::static_pointer_cast<Window>(window_ptr);
340 "ensure_depth_attachment: window '{}' not registered",
341 window->get_create_info().title);
347 display_service->get_depth_image_view = [
this](
const std::shared_ptr<void>& window_ptr) ->
void* {
348 auto window = std::static_pointer_cast<Window>(window_ptr);
350 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
353 static thread_local vk::ImageView view;
354 view = ctx->depth_image->get_image_view();
355 return static_cast<void*
>(&view);
358 display_service->get_depth_format = [
this](
const std::shared_ptr<void>& window_ptr) -> uint32_t {
359 auto window = std::static_pointer_cast<Window>(window_ptr);
361 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
362 return static_cast<uint32_t
>(vk::Format::eUndefined);
364 return static_cast<uint32_t
>(ctx->depth_image->get_format());
370 display_service->get_last_frame = [
this](
const std::shared_ptr<void>& window_ptr)
371 -> std::shared_ptr<std::vector<uint8_t>> {
373 if (!ctx || !ctx->capture)
376#ifdef MAYAFLUX_PLATFORM_MACOS
377 auto& state = *ctx->capture;
379 size_t slot = CaptureState::LAST_FRAME_MAX_READERS;
380 for (
size_t i = 0; i < CaptureState::LAST_FRAME_MAX_READERS; ++i) {
381 bool expected =
false;
382 if (state.last_frame_slot_active[i].compare_exchange_strong(expected,
true, std::memory_order_acquire)) {
387 if (slot == CaptureState::LAST_FRAME_MAX_READERS)
390 const std::vector<uint8_t>* raw;
392 raw = state.last_frame.load(std::memory_order_acquire);
393 state.last_frame_hazard_ptrs[slot].store(raw, std::memory_order_release);
394 }
while (raw != state.last_frame.load(std::memory_order_acquire));
397 state.last_frame_hazard_ptrs[slot].store(
nullptr, std::memory_order_release);
398 state.last_frame_slot_active[slot].store(
false, std::memory_order_release);
402 return std::shared_ptr<std::vector<uint8_t>>(
403 const_cast<std::vector<uint8_t>*
>(raw),
404 [&state, slot](std::vector<uint8_t>*) {
405 state.last_frame_hazard_ptrs[slot].store(
nullptr, std::memory_order_release);
406 state.last_frame_slot_active[slot].store(
false, std::memory_order_release);
409 return ctx->capture->last_frame.load(std::memory_order_acquire);
416 display_service->register_frame_observer = [
this](
417 const std::shared_ptr<void>& window_ptr,
418 const std::function<void(
419 const std::shared_ptr<std::vector<uint8_t>>&,
420 uint32_t, uint32_t, uint32_t)>&
425 "register_frame_observer: window not registered with graphics backend");
432 auto& state = *ctx->capture;
433 uint32_t
id = state.next_observer_id.fetch_add(1, std::memory_order_relaxed);
435#ifdef MAYAFLUX_PLATFORM_MACOS
436 size_t obs_slot = CaptureState::OBSERVERS_MAX_READERS;
437 for (
size_t i = 0; i < CaptureState::OBSERVERS_MAX_READERS; ++i) {
438 bool expected =
false;
439 if (state.observers_slot_active[i].compare_exchange_strong(expected,
true, std::memory_order_acquire)) {
444 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
455 current = state.observers.load(std::memory_order_acquire);
456 state.observers_hazard_ptrs[obs_slot].store(
current, std::memory_order_release);
457 }
while (
current != state.observers.load(std::memory_order_acquire));
462 }
while (!state.observers.compare_exchange_weak(
current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
464 state.observers_hazard_ptrs[obs_slot].store(
nullptr, std::memory_order_release);
465 state.observers_slot_active[obs_slot].store(
false, std::memory_order_release);
468 state.retire_observers(
current);
470 auto current = state.observers.load(std::memory_order_acquire);
471 std::shared_ptr<CaptureState::ObserverMap> next;
473 next = std::make_shared<CaptureState::ObserverMap>(*
current);
475 }
while (!state.observers.compare_exchange_weak(
477 std::memory_order_release, std::memory_order_acquire));
485 display_service->unregister_frame_observer = [
this](
486 const std::shared_ptr<void>& window_ptr, uint32_t id) {
490 "unregister_frame_observer: window not registered with graphics backend");
495 "unregister_frame_observer: capture not active for '{}'",
496 ctx->window->get_create_info().title);
500 auto& state = *ctx->capture;
502#ifdef MAYAFLUX_PLATFORM_MACOS
503 size_t obs_slot = CaptureState::OBSERVERS_MAX_READERS;
504 for (
size_t i = 0; i < CaptureState::OBSERVERS_MAX_READERS; ++i) {
505 bool expected =
false;
506 if (state.observers_slot_active[i].compare_exchange_strong(expected,
true, std::memory_order_acquire)) {
511 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
522 current = state.observers.load(std::memory_order_acquire);
523 state.observers_hazard_ptrs[obs_slot].store(
current, std::memory_order_release);
524 }
while (
current != state.observers.load(std::memory_order_acquire));
529 }
while (!state.observers.compare_exchange_weak(
current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
531 state.observers_hazard_ptrs[obs_slot].store(
nullptr, std::memory_order_release);
532 state.observers_slot_active[obs_slot].store(
false, std::memory_order_release);
535 state.retire_observers(
current);
537 auto current = state.observers.load(std::memory_order_acquire);
538 std::shared_ptr<CaptureState::ObserverMap> next;
540 next = std::make_shared<CaptureState::ObserverMap>(*
current);
542 }
while (!state.observers.compare_exchange_weak(
544 std::memory_order_release, std::memory_order_acquire));
790 if (!window || !window->get_state().is_visible || !window->get_rendering_buffers().empty()) {
795 auto device = m_context.get_device();
798 auto& in_flight = ctx.
in_flight[frame_index];
805 "Clear command buffer not allocated for window '{}'",
806 window->get_create_info().title);
809 cmd_buffer.reset({});
811 if (device.waitForFences(1, &in_flight, VK_TRUE, UINT64_MAX) == vk::Result::eTimeout) {
815 auto image_index_opt = ctx.
swapchain->acquire_next_image(image_available);
816 if (!image_index_opt.has_value()) {
822 if (device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
826 vk::CommandBufferBeginInfo begin_info {};
827 begin_info.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
828 cmd_buffer.begin(begin_info);
830 const auto& image_views = ctx.
swapchain->get_image_views();
831 auto extent = ctx.
swapchain->get_extent();
833 vk::RenderingAttachmentInfo color_attachment {};
835 color_attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
836 color_attachment.loadOp = vk::AttachmentLoadOp::eClear;
837 color_attachment.storeOp = vk::AttachmentStoreOp::eStore;
838 color_attachment.clearValue.color = vk::ClearColorValue(
839 window->get_create_info().clear_color);
841 vk::RenderingInfo rendering_info {};
842 rendering_info.renderArea = vk::Rect2D { { 0, 0 }, extent };
843 rendering_info.layerCount = 1;
844 rendering_info.colorAttachmentCount = 1;
845 rendering_info.pColorAttachments = &color_attachment;
847 vk::ImageMemoryBarrier barrier {};
848 barrier.oldLayout = vk::ImageLayout::eUndefined;
849 barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal;
850 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
851 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
853 barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
854 barrier.subresourceRange.levelCount = 1;
855 barrier.subresourceRange.layerCount = 1;
856 barrier.srcAccessMask = {};
857 barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
859 cmd_buffer.pipelineBarrier(
860 vk::PipelineStageFlagBits::eTopOfPipe,
861 vk::PipelineStageFlagBits::eColorAttachmentOutput,
862 {}, {}, {}, barrier);
864 cmd_buffer.beginRendering(rendering_info);
865 cmd_buffer.endRendering();
867 barrier.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
868 barrier.newLayout = vk::ImageLayout::ePresentSrcKHR;
869 barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
870 barrier.dstAccessMask = {};
872 cmd_buffer.pipelineBarrier(
873 vk::PipelineStageFlagBits::eColorAttachmentOutput,
874 vk::PipelineStageFlagBits::eBottomOfPipe,
875 {}, {}, {}, barrier);
879 submit_and_present(window, cmd_buffer);
883 }
catch (
const std::exception& e) {
885 "Failed to render empty window '{}': {}",
886 window->get_create_info().title, e.what());
919 || !ctx.
window->is_capture_enabled())
922 auto dev = m_context.get_device();
923 const auto ext = ctx.
swapchain->get_extent();
924 const vk::Format fmt = ctx.
swapchain->get_image_format();
929 ensure_capture_state(ctx);
934 if (slot.pending.load(std::memory_order_acquire))
937 if (slot.image && slot.extent != ext) {
938 dev.destroyImage(slot.image);
939 dev.freeMemory(slot.mem);
940 slot.image = vk::Image {};
941 slot.mem = vk::DeviceMemory {};
945 vk::ImageCreateInfo ici {};
946 ici.imageType = vk::ImageType::e2D;
948 ici.extent = vk::Extent3D { ext.width, ext.height, 1 };
951 ici.samples = vk::SampleCountFlagBits::e1;
952 ici.tiling = vk::ImageTiling::eLinear;
953 ici.usage = vk::ImageUsageFlagBits::eTransferDst;
955 slot.image = dev.createImage(ici);
957 auto req = dev.getImageMemoryRequirements(slot.image);
958 vk::MemoryAllocateInfo mai {};
959 mai.allocationSize = req.size;
960 mai.memoryTypeIndex = m_resource_manager->find_memory_type(
962 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
964 slot.mem = dev.allocateMemory(mai);
965 dev.bindImageMemory(slot.image, slot.mem, 0);
970 vk::CommandBufferBeginInfo bi {};
971 bi.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
974 vk::ImageMemoryBarrier
b {};
975 b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
976 b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
977 b.subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 };
979 b.image = slot.image;
980 b.oldLayout = vk::ImageLayout::eUndefined;
981 b.newLayout = vk::ImageLayout::eTransferDstOptimal;
982 b.srcAccessMask = {};
983 b.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
984 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
985 vk::PipelineStageFlagBits::eTransfer, {}, {}, {},
b);
988 b.oldLayout = vk::ImageLayout::ePresentSrcKHR;
989 b.newLayout = vk::ImageLayout::eTransferSrcOptimal;
990 b.srcAccessMask = vk::AccessFlagBits::eMemoryRead;
991 b.dstAccessMask = vk::AccessFlagBits::eTransferRead;
992 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
993 vk::PipelineStageFlagBits::eTransfer, {}, {}, {},
b);
995 vk::ImageCopy copy {};
996 copy.srcSubresource = { vk::ImageAspectFlagBits::eColor, 0, 0, 1 };
997 copy.dstSubresource = { vk::ImageAspectFlagBits::eColor, 0, 0, 1 };
998 copy.extent = vk::Extent3D { ext.width, ext.height, 1 };
999 slot.cmd.copyImage(src, vk::ImageLayout::eTransferSrcOptimal,
1000 slot.image, vk::ImageLayout::eTransferDstOptimal, 1, ©);
1002 b.image = slot.image;
1003 b.oldLayout = vk::ImageLayout::eTransferDstOptimal;
1004 b.newLayout = vk::ImageLayout::eGeneral;
1005 b.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
1006 b.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
1007 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1008 vk::PipelineStageFlagBits::eTransfer, {}, {}, {},
b);
1011 b.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
1012 b.newLayout = vk::ImageLayout::ePresentSrcKHR;
1013 b.srcAccessMask = vk::AccessFlagBits::eTransferRead;
1014 b.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
1015 slot.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1016 vk::PipelineStageFlagBits::eTransfer, {}, {}, {},
b);
1020 (void)dev.resetFences(1, &slot.fence);
1022 vk::SubmitInfo si {};
1023 si.commandBufferCount = 1;
1024 si.pCommandBuffers = &slot.cmd;
1026 slot.pending.store(
true, std::memory_order_release);
1028 if (
auto result = m_context.get_graphics_queue().submit(1, &si, slot.fence); result != vk::Result::eSuccess) {
1030 "Failed to submit capture command buffer for window '{}': {}",
1031 ctx.
window->get_create_info().title, vk::to_string(result));
1032 slot.pending.store(
false, std::memory_order_release);
1039void BackendWindowHandler::start_readback_thread(
CaptureState& state, vk::Device dev)
1047 for (auto& slot_ptr : state.slots) {
1048 auto& slot = *slot_ptr;
1050 if (!slot.pending.load(std::memory_order_acquire))
1053 if (dev.getFenceStatus(slot.fence) != vk::Result::eSuccess)
1056 const size_t nb = static_cast<size_t>(slot.extent.width)
1057 * slot.extent.height * state.bpp;
1059 vk::SubresourceLayout layout = dev.getImageSubresourceLayout(
1060 slot.image, { vk::ImageAspectFlagBits::eColor, 0, 0 });
1062 const auto* mapped = static_cast<const uint8_t*>(
1063 dev.mapMemory(slot.mem, 0, VK_WHOLE_SIZE));
1064 mapped += layout.offset;
1066 auto buf = std::make_shared<std::vector<uint8_t>>(nb);
1067 const uint32_t row_bytes = slot.extent.width * state.bpp;
1068 for (uint32_t y = 0; y < slot.extent.height; ++y) {
1069 std::memcpy(buf->data() + static_cast<size_t>(y * row_bytes),
1070 mapped + y * layout.rowPitch,
1074 dev.unmapMemory(slot.mem);
1079#ifdef MAYAFLUX_PLATFORM_MACOS
1080 auto* raw_frame = new std::vector<uint8_t>(std::move(*buf));
1081 auto* old = state.last_frame.exchange(raw_frame, std::memory_order_acq_rel);
1083 state.retire_last_frame(old);
1085 state.last_frame.store(buf, std::memory_order_release);
1091#ifdef MAYAFLUX_PLATFORM_MACOS
1092 size_t obs_slot = CaptureState::OBSERVERS_MAX_READERS;
1093 for (size_t i = 0; i < CaptureState::OBSERVERS_MAX_READERS; ++i) {
1094 bool expected = false;
1095 if (state.observers_slot_active[i].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
1101 if (obs_slot != CaptureState::OBSERVERS_MAX_READERS) {
1102 const CaptureState::ObserverMap* obs_current;
1104 obs_current = state.observers.load(std::memory_order_acquire);
1105 state.observers_hazard_ptrs[obs_slot].store(obs_current, std::memory_order_release);
1106 } while (obs_current != state.observers.load(std::memory_order_acquire));
1109 for (const auto& [id, cb] : *obs_current) {
1110 cb(buf, slot.extent.width, slot.extent.height,
1111 static_cast<uint32_t>(state.format));
1115 state.observers_hazard_ptrs[obs_slot].store(nullptr, std::memory_order_release);
1116 state.observers_slot_active[obs_slot].store(false, std::memory_order_release);
1119 auto obs = state.observers.load(std::memory_order_acquire);
1121 for (const auto& [id, cb] : *obs) {
1122 cb(buf, slot.extent.width, slot.extent.height,
1123 static_cast<uint32_t>(state.format));
1128 slot.pending.store(false, std::memory_order_release);
1133 std::this_thread::sleep_for(std::chrono::microseconds(200));