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);
154 window->set_size(
width, height);
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);
327 vk::ImageLayout::ePresentSrcKHR,
328 vk::PipelineStageFlagBits::eBottomOfPipe);
333 display_service->ensure_depth_attachment = [
this](
const std::shared_ptr<void>& window_ptr) {
334 auto window = std::static_pointer_cast<Window>(window_ptr);
338 "ensure_depth_attachment: window '{}' not registered",
339 window->get_create_info().title);
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);
348 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
351 static thread_local vk::ImageView view;
352 view = ctx->depth_image->get_image_view();
353 return static_cast<void*
>(&view);
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);
359 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
360 return static_cast<uint32_t
>(vk::Format::eUndefined);
362 return static_cast<uint32_t
>(ctx->depth_image->get_format());
368 display_service->get_last_frame = [
this](
const std::shared_ptr<void>& window_ptr)
369 -> std::shared_ptr<std::vector<uint8_t>> {
371 if (!ctx || !ctx->capture)
374#ifdef MAYAFLUX_PLATFORM_MACOS
375 auto& state = *ctx->capture;
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)) {
385 if (slot == CaptureState::LAST_FRAME_MAX_READERS)
388 const std::vector<uint8_t>* raw;
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));
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);
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);
407 return ctx->capture->last_frame.load(std::memory_order_acquire);
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)>&
423 "register_frame_observer: window not registered with graphics backend");
430 auto& state = *ctx->capture;
431 uint32_t
id = state.next_observer_id.fetch_add(1, std::memory_order_relaxed);
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)) {
442 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
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));
460 }
while (!state.observers.compare_exchange_weak(
current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
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);
466 state.retire_observers(
current);
468 auto current = state.observers.load(std::memory_order_acquire);
469 std::shared_ptr<CaptureState::ObserverMap> next;
471 next = std::make_shared<CaptureState::ObserverMap>(*
current);
473 }
while (!state.observers.compare_exchange_weak(
475 std::memory_order_release, std::memory_order_acquire));
483 display_service->unregister_frame_observer = [
this](
484 const std::shared_ptr<void>& window_ptr, uint32_t id) {
488 "unregister_frame_observer: window not registered with graphics backend");
493 "unregister_frame_observer: capture not active for '{}'",
494 ctx->window->get_create_info().title);
498 auto& state = *ctx->capture;
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)) {
509 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
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));
527 }
while (!state.observers.compare_exchange_weak(
current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
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);
533 state.retire_observers(
current);
535 auto current = state.observers.load(std::memory_order_acquire);
536 std::shared_ptr<CaptureState::ObserverMap> next;
538 next = std::make_shared<CaptureState::ObserverMap>(*
current);
540 }
while (!state.observers.compare_exchange_weak(
542 std::memory_order_release, std::memory_order_acquire));
788 if (!window || !window->get_state().is_visible || !window->get_rendering_buffers().empty()) {
793 auto device = m_context.get_device();
796 auto& in_flight = ctx.
in_flight[frame_index];
803 "Clear command buffer not allocated for window '{}'",
804 window->get_create_info().title);
807 cmd_buffer.reset({});
809 if (device.waitForFences(1, &in_flight, VK_TRUE, UINT64_MAX) == vk::Result::eTimeout) {
813 auto image_index_opt = ctx.
swapchain->acquire_next_image(image_available);
814 if (!image_index_opt.has_value()) {
820 if (device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
824 vk::CommandBufferBeginInfo begin_info {};
825 begin_info.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
826 cmd_buffer.begin(begin_info);
828 const auto& image_views = ctx.
swapchain->get_image_views();
829 auto extent = ctx.
swapchain->get_extent();
831 vk::RenderingAttachmentInfo color_attachment {};
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);
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;
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;
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;
857 cmd_buffer.pipelineBarrier(
858 vk::PipelineStageFlagBits::eTopOfPipe,
859 vk::PipelineStageFlagBits::eColorAttachmentOutput,
860 {}, {}, {}, barrier);
862 cmd_buffer.beginRendering(rendering_info);
863 cmd_buffer.endRendering();
865 barrier.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
866 barrier.newLayout = vk::ImageLayout::ePresentSrcKHR;
867 barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
868 barrier.dstAccessMask = {};
870 cmd_buffer.pipelineBarrier(
871 vk::PipelineStageFlagBits::eColorAttachmentOutput,
872 vk::PipelineStageFlagBits::eBottomOfPipe,
873 {}, {}, {}, barrier);
877 submit_and_present(window, cmd_buffer);
881 }
catch (
const std::exception& e) {
883 "Failed to render empty window '{}': {}",
884 window->get_create_info().title, e.what());
917 || !ctx.
window->is_capture_enabled())
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();
927 ensure_capture_state(ctx);
932 if (slot.pending.load(std::memory_order_acquire))
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 {};
943 vk::ImageCreateInfo ici {};
944 ici.imageType = vk::ImageType::e2D;
946 ici.extent = vk::Extent3D { ext.width, ext.height, 1 };
949 ici.samples = vk::SampleCountFlagBits::e1;
950 ici.tiling = vk::ImageTiling::eLinear;
951 ici.usage = vk::ImageUsageFlagBits::eTransferDst;
953 slot.image = dev.createImage(ici);
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(
960 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
962 slot.mem = dev.allocateMemory(mai);
963 dev.bindImageMemory(slot.image, slot.mem, 0);
968 vk::CommandBufferBeginInfo bi {};
969 bi.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
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 };
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);
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);
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, ©);
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);
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);
1018 (void)dev.resetFences(1, &slot.fence);
1020 vk::SubmitInfo si {};
1021 si.commandBufferCount = 1;
1022 si.pCommandBuffers = &slot.cmd;
1024 slot.pending.store(
true, std::memory_order_release);
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);
1037void BackendWindowHandler::start_readback_thread(
CaptureState& state, vk::Device dev)
1045 for (auto& slot_ptr : state.slots) {
1046 auto& slot = *slot_ptr;
1048 if (!slot.pending.load(std::memory_order_acquire))
1051 if (dev.getFenceStatus(slot.fence) != vk::Result::eSuccess)
1054 const size_t nb = static_cast<size_t>(slot.extent.width)
1055 * slot.extent.height * state.bpp;
1057 vk::SubresourceLayout layout = dev.getImageSubresourceLayout(
1058 slot.image, { vk::ImageAspectFlagBits::eColor, 0, 0 });
1060 const auto* mapped = static_cast<const uint8_t*>(
1061 dev.mapMemory(slot.mem, 0, VK_WHOLE_SIZE));
1062 mapped += layout.offset;
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,
1072 dev.unmapMemory(slot.mem);
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);
1081 state.retire_last_frame(old);
1083 state.last_frame.store(buf, std::memory_order_release);
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)) {
1099 if (obs_slot != CaptureState::OBSERVERS_MAX_READERS) {
1100 const CaptureState::ObserverMap* obs_current;
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));
1107 for (const auto& [id, cb] : *obs_current) {
1108 cb(buf, slot.extent.width, slot.extent.height,
1109 static_cast<uint32_t>(state.format));
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);
1117 auto obs = state.observers.load(std::memory_order_acquire);
1119 for (const auto& [id, cb] : *obs) {
1120 cb(buf, slot.extent.width, slot.extent.height,
1121 static_cast<uint32_t>(state.format));
1126 slot.pending.store(false, std::memory_order_release);
1131 std::this_thread::sleep_for(std::chrono::microseconds(200));