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);
149 display_service->wait_idle = [
this]() {
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);
157 if (ctx.window ==
window) {
158 ctx.needs_recreation =
true;
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);
167 if (ctx.window ==
window) {
168 return static_cast<uint32_t
>(ctx.swapchain->get_image_count());
174 display_service->get_submitted_frame_count = [
this](
const std::shared_ptr<void>& window_ptr) -> uint64_t {
176 return ctx ? ctx->submitted_frames : 0;
179 display_service->get_completed_frame_count = [
this](
const std::shared_ptr<void>& window_ptr) -> uint64_t {
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);
187 if (ctx.window ==
window) {
188 return static_cast<int>(ctx.swapchain->get_image_format());
194 display_service->get_swapchain_extent = [
this](
195 const std::shared_ptr<void>& window_ptr,
197 uint32_t& out_height) {
198 auto window = std::static_pointer_cast<Window>(window_ptr);
201 if (context && context->swapchain) {
202 auto extent = context->swapchain->get_extent();
203 out_width = extent.width;
204 out_height = extent.height;
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);
216 "Window '{}' not registered for swapchain acquisition",
217 window->get_create_info().title);
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];
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);
233 auto image_index_opt = ctx->swapchain->acquire_next_image(image_available);
234 if (!image_index_opt.has_value()) {
235 ctx->needs_recreation =
true;
239 ctx->current_image_index = image_index_opt.value();
241 if (
device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
243 "Failed to reset fence for window '{}'",
244 window->get_create_info().title);
248 const auto& images = ctx->swapchain->get_images();
249 VkImage raw = images[ctx->current_image_index];
250 return reinterpret_cast<uint64_t
>(raw);
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);
257 if (!context || !context->swapchain) {
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,
271 static thread_local vk::ImageView view;
272 view = image_views[context->current_image_index];
273 return static_cast<void*
>(&view);
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);
279 if (!ctx || !ctx->swapchain)
282 const auto& images = ctx->swapchain->get_images();
283 if (ctx->current_image_index >= images.size())
286 return reinterpret_cast<uint64_t
>(
static_cast<VkImage
>(images[ctx->current_image_index]));
289 display_service->readback_swapchain_region = [
this](
290 const std::shared_ptr<void>& window_ptr,
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);
300 if (!ctx || !ctx->swapchain) {
302 "readback_swapchain_region: window '{}' has no swapchain",
303 window->get_create_info().title);
309 "readback_swapchain_region: resource manager not set");
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);
321 auto proxy = std::make_shared<VKImage>(
322 pixel_width, pixel_height, 1U,
323 ctx->swapchain->get_image_format(),
330 res.
image = images[ctx->current_image_index];
331 proxy->set_image_resources(res);
332 proxy->set_current_layout(vk::ImageLayout::ePresentSrcKHR);
340 vk::ImageLayout::ePresentSrcKHR,
341 vk::PipelineStageFlagBits::eBottomOfPipe);
346 display_service->ensure_depth_attachment = [
this](
const std::shared_ptr<void>& window_ptr) {
347 auto window = std::static_pointer_cast<Window>(window_ptr);
351 "ensure_depth_attachment: window '{}' not registered",
352 window->get_create_info().title);
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);
361 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
364 static thread_local vk::ImageView view;
365 view = ctx->depth_image->get_image_view();
366 return static_cast<void*
>(&view);
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);
372 if (!ctx || !ctx->depth_image || !ctx->depth_image->is_initialized()) {
373 return static_cast<uint32_t
>(vk::Format::eUndefined);
375 return static_cast<uint32_t
>(ctx->depth_image->get_format());
381 display_service->get_last_frame = [
this](
const std::shared_ptr<void>& window_ptr)
382 -> std::shared_ptr<std::vector<uint8_t>> {
384 if (!ctx || !ctx->capture)
387#ifdef MAYAFLUX_PLATFORM_MACOS
388 auto& state = *ctx->capture;
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)) {
398 if (slot == CaptureState::LAST_FRAME_MAX_READERS)
401 const std::vector<uint8_t>* raw;
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));
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);
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);
420 return ctx->capture->last_frame.load(std::memory_order_acquire);
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)>&
436 "register_frame_observer: window not registered with graphics backend");
444 "register_frame_observer: capture is not enabled for '{}'",
445 ctx->window->get_create_info().title);
449 auto& state = *ctx->capture;
450 uint32_t
id = state.next_observer_id.fetch_add(1, std::memory_order_relaxed);
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)) {
461 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
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));
479 }
while (!state.observers.compare_exchange_weak(
current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
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);
485 state.retire_observers(
current);
487 auto current = state.observers.load(std::memory_order_acquire);
488 std::shared_ptr<CaptureState::ObserverMap> next;
490 next = std::make_shared<CaptureState::ObserverMap>(*
current);
492 }
while (!state.observers.compare_exchange_weak(
494 std::memory_order_release, std::memory_order_acquire));
502 display_service->unregister_frame_observer = [
this](
503 const std::shared_ptr<void>& window_ptr, uint32_t id) {
507 "unregister_frame_observer: window not registered with graphics backend");
512 "unregister_frame_observer: capture not active for '{}'",
513 ctx->window->get_create_info().title);
517 auto& state = *ctx->capture;
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)) {
528 if (obs_slot == CaptureState::OBSERVERS_MAX_READERS)
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));
546 }
while (!state.observers.compare_exchange_weak(
current, new_map, std::memory_order_acq_rel, std::memory_order_acquire));
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);
552 state.retire_observers(
current);
554 auto current = state.observers.load(std::memory_order_acquire);
555 std::shared_ptr<CaptureState::ObserverMap> next;
557 next = std::make_shared<CaptureState::ObserverMap>(*
current);
559 }
while (!state.observers.compare_exchange_weak(
561 std::memory_order_release, std::memory_order_acquire));
811 if (!
window || !
window->get_state().is_visible || !
window->get_rendering_buffers().empty()) {
816 auto device = m_context.get_device();
819 auto& in_flight = ctx.
in_flight[frame_index];
826 "Clear command buffer not allocated for window '{}'",
827 window->get_create_info().title);
831 if (
device.waitForFences(1, &in_flight, VK_TRUE, UINT64_MAX) == vk::Result::eTimeout) {
834 cmd_buffer.reset({});
836 auto image_index_opt = ctx.
swapchain->acquire_next_image(image_available);
837 if (!image_index_opt.has_value()) {
843 if (
device.resetFences(1, &in_flight) != vk::Result::eSuccess) {
847 vk::CommandBufferBeginInfo begin_info {};
848 begin_info.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
849 cmd_buffer.begin(begin_info);
851 const auto& image_views = ctx.
swapchain->get_image_views();
852 auto extent = ctx.
swapchain->get_extent();
854 vk::RenderingAttachmentInfo color_attachment {};
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);
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;
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;
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;
880 cmd_buffer.pipelineBarrier(
881 vk::PipelineStageFlagBits::eTopOfPipe,
882 vk::PipelineStageFlagBits::eColorAttachmentOutput,
883 {}, {}, {}, barrier);
885 cmd_buffer.beginRendering(rendering_info);
886 cmd_buffer.endRendering();
888 barrier.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
889 barrier.newLayout = vk::ImageLayout::ePresentSrcKHR;
890 barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
891 barrier.dstAccessMask = {};
893 cmd_buffer.pipelineBarrier(
894 vk::PipelineStageFlagBits::eColorAttachmentOutput,
895 vk::PipelineStageFlagBits::eBottomOfPipe,
896 {}, {}, {}, barrier);
900 submit_and_present(
window, cmd_buffer);
904 }
catch (
const std::exception& e) {
906 "Failed to render empty window '{}': {}",
907 window->get_create_info().title, e.what());
940 || !ctx.
window->is_capture_enabled())
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();
950 ensure_capture_state(ctx);
955 if (slot.pending.load(std::memory_order_acquire))
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 {};
966 vk::ImageCreateInfo ici {};
967 ici.imageType = vk::ImageType::e2D;
969 ici.extent = vk::Extent3D { ext.width, ext.height, 1 };
972 ici.samples = vk::SampleCountFlagBits::e1;
973 ici.tiling = vk::ImageTiling::eLinear;
974 ici.usage = vk::ImageUsageFlagBits::eTransferDst;
976 slot.image = dev.createImage(ici);
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(
983 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
985 slot.mem = dev.allocateMemory(mai);
986 dev.bindImageMemory(slot.image, slot.mem, 0);
991 vk::CommandBufferBeginInfo bi {};
992 bi.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
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 };
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);
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);
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, ©);
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);
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);
1041 (void)dev.resetFences(1, &slot.fence);
1043 vk::SubmitInfo si {};
1044 si.commandBufferCount = 1;
1045 si.pCommandBuffers = &slot.cmd;
1047 slot.pending.store(
true, std::memory_order_release);
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);
1060void BackendWindowHandler::start_readback_thread(
CaptureState& state, vk::Device dev)
1068 for (auto& slot_ptr : state.slots) {
1069 auto& slot = *slot_ptr;
1071 if (!slot.pending.load(std::memory_order_acquire))
1074 if (dev.getFenceStatus(slot.fence) != vk::Result::eSuccess)
1077 const size_t nb = static_cast<size_t>(slot.extent.width)
1078 * slot.extent.height * state.bpp;
1080 vk::SubresourceLayout layout = dev.getImageSubresourceLayout(
1081 slot.image, { vk::ImageAspectFlagBits::eColor, 0, 0 });
1083 const auto* mapped = static_cast<const uint8_t*>(
1084 dev.mapMemory(slot.mem, 0, VK_WHOLE_SIZE));
1085 mapped += layout.offset;
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,
1095 dev.unmapMemory(slot.mem);
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);
1104 state.retire_last_frame(old);
1106 state.last_frame.store(buf, std::memory_order_release);
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)) {
1122 if (obs_slot != CaptureState::OBSERVERS_MAX_READERS) {
1123 const CaptureState::ObserverMap* obs_current;
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));
1130 for (const auto& [id, cb] : *obs_current) {
1131 cb(buf, slot.extent.width, slot.extent.height,
1132 static_cast<uint32_t>(state.format));
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);
1140 auto obs = state.observers.load(std::memory_order_acquire);
1142 for (const auto& [id, cb] : *obs) {
1143 cb(buf, slot.extent.width, slot.extent.height,
1144 static_cast<uint32_t>(state.format));
1149 slot.pending.store(false, std::memory_order_release);
1154 std::this_thread::sleep_for(std::chrono::microseconds(200));