39 using ReferenceScalar = boost::multiprecision::cpp_dec_float_100;
48 struct ReferenceTile {
49 ReferenceScalar min_uv_x;
50 ReferenceScalar min_uv_y;
51 ReferenceScalar max_uv_x;
52 ReferenceScalar max_uv_y;
57 FractalWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync)
59 reference_orbit_samples(static_cast<size_t>(reference_orbit_capacity)),
64#if defined(MXWRITE_ENABLED)
67 if (
device != VK_NULL_HANDLE) {
70 destroyFractalResources();
73 void event(SDL_Event &e)
override {
74 if (e.type == SDL_EVENT_KEY_DOWN) {
75 if ((e.key.key == SDLK_F10 || e.key.scancode == SDL_SCANCODE_F10) && !e.key.repeat) {
76 saveFractalSnapshot();
79 if ((e.key.key == SDLK_P || e.key.scancode == SDL_SCANCODE_P) && !e.key.repeat) {
80#if defined(MXWRITE_ENABLED)
81 toggleVideoRecording();
83 std::cout <<
"fractal_zoom: MXWrite is unavailable; video recording disabled\n";
91 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
93 drag_start_mouse_x = e.button.x;
94 drag_start_mouse_y = e.button.y;
95 drag_start_center_x = center_x;
96 drag_start_center_y = center_y;
100 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
105 if (e.type == SDL_EVENT_MOUSE_MOTION && dragging) {
107 if (extent.width == 0U || extent.height == 0U) {
110 const int delta_x = e.motion.x - drag_start_mouse_x;
111 const int delta_y = e.motion.y - drag_start_mouse_y;
112 const ReferenceScalar scale = ReferenceScalar(2) / (zoom * ReferenceScalar(std::min(extent.width, extent.height)));
113 center_x = drag_start_center_x -
static_cast<ReferenceScalar
>(delta_x) * scale;
114 center_y = drag_start_center_y +
static_cast<ReferenceScalar
>(delta_y) * scale;
115 reference_orbit_dirty =
true;
119 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
120 applyWheelZoom(e.wheel.y);
125 updateKeyboardNavigation();
129#if defined(MXWRITE_ENABLED)
130 serviceRecordingReadbacks();
133#if defined(MXWRITE_ENABLED)
134 recordPresentedFrame();
139#if defined(MXWRITE_ENABLED)
140 if (video_writer.is_open()) {
141 std::cerr <<
"fractal_zoom: swapchain is changing; closing current video recording\n";
145 destroyFractalResources();
149 createFractalPipeline();
153 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE) {
154 createFractalPipeline();
157 if (image_index >= fractal_descriptor_sets.size()) {
161 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE || fractal_descriptor_sets[image_index] == VK_NULL_HANDLE) {
166 if (extent.width == 0U || extent.height == 0U) {
170 updateReferenceOrbit(image_index, extent);
172 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fractal_pipeline);
173 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fractal_pipeline_layout, 0, 1, &fractal_descriptor_sets[image_index], 0,
nullptr);
175 const FractalPushConstants push_constants{
176 center_x.convert_to<PushScalar>(),
177 center_y.convert_to<PushScalar>(),
178 (ReferenceScalar(1) / zoom).convert_to<PushScalar>(),
179 static_cast<PushScalar
>(std::chrono::duration<double>(std::chrono::steady_clock::now() - start_time).count()),
180 static_cast<PushScalar
>(extent.width),
181 static_cast<PushScalar
>(extent.height),
189 fractal_pipeline_layout,
190 VK_SHADER_STAGE_FRAGMENT_BIT,
192 sizeof(push_constants),
195 vkCmdDraw(cmd, 3, 1, 0, 0);
199 static ReferenceScalar minReferenceScalar(
const ReferenceScalar &a,
const ReferenceScalar &b) {
200 return (a < b) ? a : b;
203 static ReferenceScalar maxReferenceScalar(
const ReferenceScalar &a,
const ReferenceScalar &b) {
204 return (a > b) ? a : b;
207 static ReferenceScalar clampReferenceScalar(
const ReferenceScalar &value,
const ReferenceScalar &minimum,
const ReferenceScalar &maximum) {
208 return maxReferenceScalar(minReferenceScalar(value, maximum), minimum);
211 static float toOrbitSampleScalar(
const ReferenceScalar &value) {
212 return value.convert_to<
float>();
215 void handleKey(SDL_Keycode key) {
224 center_x = ReferenceScalar(
"-0.5");
225 center_y = ReferenceScalar(0);
226 zoom = ReferenceScalar(1);
227 max_iterations = 256;
228 reference_orbit_dirty =
true;
231 center_x = ReferenceScalar(
"-0.745");
232 center_y = ReferenceScalar(
"0.113");
233 zoom = ReferenceScalar(50);
234 max_iterations = 512;
235 reference_orbit_dirty =
true;
238 center_x = ReferenceScalar(
"-0.761574");
239 center_y = ReferenceScalar(
"-0.0847596");
240 zoom = ReferenceScalar(220);
241 max_iterations = 900;
242 reference_orbit_dirty =
true;
246 max_iterations = std::min(max_iterations + 64, max_reference_iterations);
247 reference_orbit_dirty =
true;
250 max_iterations = std::max(max_iterations - 64, 64);
251 reference_orbit_dirty =
true;
253 case SDLK_LEFTBRACKET:
254 palette_index = (palette_index + 2) % 3;
256 case SDLK_RIGHTBRACKET:
257 palette_index = (palette_index + 1) % 3;
264#if defined(MXWRITE_ENABLED)
265 void toggleVideoRecording() {
266 if (video_writer.is_open()) {
272 if (extent.width == 0U || extent.height == 0U) {
273 std::cerr <<
"fractal_zoom: cannot start video recording before the swapchain is ready\n";
277 constexpr float video_fps = 60.0f;
278 EncodeOptions encode_options{};
279 encode_options.
crf = 24;
280 encode_options.
preset =
"ultrafast";
281 encode_options.
tune =
"zerolatency";
284 if (!video_writer.open(video_output_path,
285 static_cast<int>(extent.width),
286 static_cast<int>(extent.height),
289 std::cerr <<
"fractal_zoom: failed to open MXWrite output file: " << video_output_path <<
"\n";
293 video_record_width = extent.width;
294 video_record_height = extent.height;
296 createRecordingReadbacks(extent);
297 }
catch (
const std::exception &ex) {
298 std::cerr <<
"fractal_zoom: failed to create async recording readback resources: " << ex.what() <<
"\n";
299 video_writer.close();
300 video_record_width = 0;
301 video_record_height = 0;
304 std::cout << std::format(
"fractal_zoom: recording video to {} at {}x{} 60 FPS\n",
307 video_record_height);
310 void closeVideoWriter() {
311 if (!video_writer.is_open()) {
315 destroyRecordingReadbacks(
true);
316 video_writer.close();
317 std::cout <<
"fractal_zoom: saved video: " << video_output_path <<
"\n";
318 video_record_width = 0;
319 video_record_height = 0;
322 void serviceRecordingReadbacks() {
323 if (!video_writer.is_open()) {
328 pumpCompletedRecordingReadbacks(
false);
329 submitPendingRecordingReadbacks();
330 }
catch (
const std::exception &ex) {
331 std::cerr <<
"fractal_zoom: failed to service recording readback: " << ex.what() <<
"\n";
336 void recordPresentedFrame() {
337 if (!video_writer.is_open()) {
343 if (extent.width != video_record_width || extent.height != video_record_height) {
344 std::cerr <<
"fractal_zoom: swapchain size changed; closing current video recording\n";
348 const auto now = std::chrono::steady_clock::now();
349 if (now < next_recording_frame_time) {
352 next_recording_frame_time += recording_frame_interval;
353 if (next_recording_frame_time <= now) {
354 next_recording_frame_time = now + recording_frame_interval;
359 pumpCompletedRecordingReadbacks(
false);
360 submitPendingRecordingReadbacks();
361 }
catch (
const std::exception &ex) {
362 std::cerr <<
"fractal_zoom: failed to record video frame: " << ex.what() <<
"\n";
367 struct RecordingReadbackSlot {
368 VkBuffer buffer = VK_NULL_HANDLE;
369 VkDeviceMemory memory = VK_NULL_HANDLE;
370 VkCommandBuffer command_buffer = VK_NULL_HANDLE;
371 VkFence fence = VK_NULL_HANDLE;
372 std::vector<std::uint8_t> pixels{};
373 uint32_t image_index = std::numeric_limits<uint32_t>::max();
374 bool in_flight =
false;
378 void createRecordingReadbacks(VkExtent2D extent) {
379 destroyRecordingReadbacks(
false);
381 throw mxvk::Exception(
"recording requires initialized Vulkan render resources");
384 throw mxvk::Exception(
"recording requires swapchain transfer-source support");
387 recording_format_is_bgra =
390 const bool format_is_rgba =
393 if (!recording_format_is_bgra && !format_is_rgba) {
394 throw mxvk::Exception(std::format(
"unsupported recording swapchain format: {}",
static_cast<int>(
swapchain_format)));
397 recording_row_bytes =
static_cast<VkDeviceSize
>(extent.width) * 4U;
398 recording_image_bytes = recording_row_bytes *
static_cast<VkDeviceSize
>(extent.height);
399 recording_readbacks.resize(recording_readback_slot_count);
401 next_recording_frame_time = std::chrono::steady_clock::now();
403 VkCommandBufferAllocateInfo command_info{};
404 command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
406 command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
407 command_info.commandBufferCount = recording_readback_slot_count;
409 std::array<VkCommandBuffer, recording_readback_slot_count>
command_buffers{};
411 destroyRecordingReadbacks(
false);
412 throw mxvk::Exception(
"failed to allocate recording readback command buffers");
415 VkFenceCreateInfo fence_info{};
416 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
417 fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
419 for (
size_t i = 0; i < recording_readbacks.size(); ++i) {
420 RecordingReadbackSlot &slot = recording_readbacks[i];
423 recording_image_bytes,
424 VK_BUFFER_USAGE_TRANSFER_DST_BIT,
425 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
428 if (vkCreateFence(
device, &fence_info,
nullptr, &slot.fence) != VK_SUCCESS) {
429 destroyRecordingReadbacks(
false);
430 throw mxvk::Exception(
"failed to create recording readback fence");
432 slot.pixels.resize(
static_cast<size_t>(recording_image_bytes));
435 recording_worker_stop =
false;
436 recording_worker = std::jthread([
this](std::stop_token stop_token) {
437 recordingWorkerLoop(stop_token);
439 recording_readbacks_ready =
true;
442 void destroyRecordingReadbacks(
bool drain) {
443 if (!recording_readbacks_ready && recording_readbacks.empty()) {
448 drainRecordingReadbacks();
452 std::lock_guard<std::mutex> lock(recording_mutex);
453 recording_worker_stop =
true;
455 recording_cv.notify_all();
456 if (recording_worker.joinable()) {
457 recording_worker.request_stop();
458 recording_worker.join();
461 if (
device != VK_NULL_HANDLE) {
462 for (RecordingReadbackSlot &slot : recording_readbacks) {
463 if (slot.in_flight && slot.fence != VK_NULL_HANDLE) {
464 vkWaitForFences(
device, 1, &slot.fence, VK_TRUE, UINT64_MAX);
469 if (slot.command_buffer != VK_NULL_HANDLE &&
command_pool != VK_NULL_HANDLE) {
471 slot.command_buffer = VK_NULL_HANDLE;
473 if (slot.fence != VK_NULL_HANDLE) {
474 vkDestroyFence(
device, slot.fence,
nullptr);
475 slot.fence = VK_NULL_HANDLE;
477 if (slot.buffer != VK_NULL_HANDLE) {
478 vkDestroyBuffer(
device, slot.buffer,
nullptr);
479 slot.buffer = VK_NULL_HANDLE;
481 if (slot.memory != VK_NULL_HANDLE) {
482 vkFreeMemory(
device, slot.memory,
nullptr);
483 slot.memory = VK_NULL_HANDLE;
488 recording_readbacks.clear();
489 recording_pending_images.clear();
491 std::lock_guard<std::mutex> lock(recording_mutex);
492 std::queue<size_t> empty_queue;
493 recording_ready_slots.swap(empty_queue);
494 recording_worker_stop =
false;
496 recording_readbacks_ready =
false;
497 recording_row_bytes = 0;
498 recording_image_bytes = 0;
499 recording_format_is_bgra =
false;
502 void submitPendingRecordingReadbacks() {
503 for (uint32_t image_index = 0; image_index < recording_pending_images.size(); ++image_index) {
504 if (!recording_pending_images[image_index]) {
507 if (submitRecordingReadback(image_index)) {
508 recording_pending_images[image_index] =
false;
513 void drainRecordingReadbacks() {
515 pumpCompletedRecordingReadbacks(
true);
516 std::unique_lock<std::mutex> lock(recording_mutex);
517 const bool idle = std::ranges::all_of(recording_readbacks, [](
const RecordingReadbackSlot &slot) {
518 return !slot.in_flight && !slot.queued;
523 recording_idle_cv.wait(lock);
527 void pumpCompletedRecordingReadbacks(
bool wait_for_copy) {
528 for (
size_t i = 0; i < recording_readbacks.size(); ++i) {
529 RecordingReadbackSlot &slot = recording_readbacks[i];
530 VkFence slot_fence = VK_NULL_HANDLE;
532 std::lock_guard<std::mutex> lock(recording_mutex);
533 if (!slot.in_flight || slot.queued || slot.fence == VK_NULL_HANDLE) {
536 slot_fence = slot.fence;
539 VkResult fence_result = VK_SUCCESS;
541 fence_result = vkWaitForFences(
device, 1, &slot_fence, VK_TRUE, UINT64_MAX);
543 fence_result = vkGetFenceStatus(
device, slot_fence);
546 if (fence_result == VK_NOT_READY) {
549 if (fence_result != VK_SUCCESS) {
550 throw mxvk::Exception(std::format(
"recording readback fence failed: {}",
static_cast<int>(fence_result)));
554 std::lock_guard<std::mutex> lock(recording_mutex);
555 if (!slot.in_flight || slot.queued || slot.fence != slot_fence) {
562 recording_ready_slots.push(i);
564 recording_cv.notify_one();
568 bool submitRecordingReadback(uint32_t image_index) {
569 if (!recording_readbacks_ready || image_index == std::numeric_limits<uint32_t>::max()) {
577 if (source_fence != VK_NULL_HANDLE && vkGetFenceStatus(
device, source_fence) == VK_NOT_READY) {
581 RecordingReadbackSlot *free_slot =
nullptr;
583 std::lock_guard<std::mutex> lock(recording_mutex);
584 for (
size_t i = 0; i < recording_readbacks.size(); ++i) {
585 if (!recording_readbacks[i].in_flight && !recording_readbacks[i].queued) {
586 free_slot = &recording_readbacks[i];
591 if (free_slot ==
nullptr) {
592 ++recording_dropped_frames;
593 if (recording_dropped_frames % 60U == 0U) {
594 std::cerr <<
"fractal_zoom: dropped " << recording_dropped_frames <<
" recording frames (readback queue full)\n";
599 RecordingReadbackSlot &slot = *free_slot;
603 VkCommandBufferBeginInfo begin_info{};
604 begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
605 begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
606 VK_CHECK_RESULT(vkBeginCommandBuffer(slot.command_buffer, &begin_info));
608 VkImageMemoryBarrier2 to_transfer_barrier{};
609 to_transfer_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
610 to_transfer_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
611 to_transfer_barrier.srcAccessMask = VK_ACCESS_2_NONE;
612 to_transfer_barrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
613 to_transfer_barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
614 to_transfer_barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
615 to_transfer_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
616 to_transfer_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
617 to_transfer_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
619 to_transfer_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
620 to_transfer_barrier.subresourceRange.baseMipLevel = 0;
621 to_transfer_barrier.subresourceRange.levelCount = 1;
622 to_transfer_barrier.subresourceRange.baseArrayLayer = 0;
623 to_transfer_barrier.subresourceRange.layerCount = 1;
625 VkDependencyInfo to_transfer_dependency{};
626 to_transfer_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
627 to_transfer_dependency.imageMemoryBarrierCount = 1;
628 to_transfer_dependency.pImageMemoryBarriers = &to_transfer_barrier;
629 vkCmdPipelineBarrier2(slot.command_buffer, &to_transfer_dependency);
631 VkBufferImageCopy copy_region{};
632 copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
633 copy_region.imageSubresource.mipLevel = 0;
634 copy_region.imageSubresource.baseArrayLayer = 0;
635 copy_region.imageSubresource.layerCount = 1;
636 copy_region.imageExtent = {video_record_width, video_record_height, 1};
637 vkCmdCopyImageToBuffer(
640 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
645 VkImageMemoryBarrier2 to_present_barrier{};
646 to_present_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
647 to_present_barrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
648 to_present_barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
649 to_present_barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
650 to_present_barrier.dstAccessMask = VK_ACCESS_2_NONE;
651 to_present_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
652 to_present_barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
653 to_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
654 to_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
656 to_present_barrier.subresourceRange = to_transfer_barrier.subresourceRange;
658 VkDependencyInfo to_present_dependency{};
659 to_present_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
660 to_present_dependency.imageMemoryBarrierCount = 1;
661 to_present_dependency.pImageMemoryBarriers = &to_present_barrier;
662 vkCmdPipelineBarrier2(slot.command_buffer, &to_present_dependency);
666 VkCommandBufferSubmitInfo command_submit_info{};
667 command_submit_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
668 command_submit_info.commandBuffer = slot.command_buffer;
670 VkSubmitInfo2 submit_info{};
671 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
672 submit_info.commandBufferInfoCount = 1;
673 submit_info.pCommandBufferInfos = &command_submit_info;
675 slot.image_index = image_index;
676 const VkResult submit_result = vkQueueSubmit2(
graphics_queue, 1, &submit_info, slot.fence);
677 if (submit_result != VK_SUCCESS) {
678 throw mxvk::Exception(std::format(
"failed to submit recording readback: {}",
static_cast<int>(submit_result)));
681 std::lock_guard<std::mutex> lock(recording_mutex);
682 slot.in_flight =
true;
689 void recordingWorkerLoop(std::stop_token stop_token) {
690 std::vector<std::uint8_t> frame_pixels;
692 size_t slot_index = 0;
694 std::unique_lock<std::mutex> lock(recording_mutex);
695 recording_cv.wait(lock, [
this, &stop_token] {
696 return recording_worker_stop || stop_token.stop_requested() || !recording_ready_slots.empty();
698 if ((recording_worker_stop || stop_token.stop_requested()) && recording_ready_slots.empty()) {
701 slot_index = recording_ready_slots.front();
702 recording_ready_slots.pop();
705 if (slot_index >= recording_readbacks.size()) {
708 RecordingReadbackSlot &slot = recording_readbacks[slot_index];
709 void *mapped =
nullptr;
710 if (vkMapMemory(
device, slot.memory, 0, recording_image_bytes, 0, &mapped) == VK_SUCCESS) {
711 const auto *src =
static_cast<const std::uint8_t *
>(mapped);
712 frame_pixels.resize(
static_cast<size_t>(recording_image_bytes));
713 if (recording_format_is_bgra) {
714 for (
size_t i = 0; i < frame_pixels.size(); i += 4U) {
715 frame_pixels[i + 0U] = src[i + 2U];
716 frame_pixels[i + 1U] = src[i + 1U];
717 frame_pixels[i + 2U] = src[i + 0U];
718 frame_pixels[i + 3U] = src[i + 3U];
721 std::memcpy(frame_pixels.data(), src, frame_pixels.size());
723 vkUnmapMemory(
device, slot.memory);
725 std::cerr <<
"fractal_zoom: failed to map recording readback memory\n";
726 frame_pixels.clear();
730 std::lock_guard<std::mutex> lock(recording_mutex);
731 slot.in_flight =
false;
733 slot.image_index = std::numeric_limits<uint32_t>::max();
735 recording_idle_cv.notify_all();
737 if (!frame_pixels.empty()) {
738 video_writer.write(frame_pixels.data());
744 void saveFractalSnapshot() {
745 const char *home = std::getenv(
"HOME");
746 if (home ==
nullptr || std::strlen(home) == 0U) {
747 std::cerr <<
"fractal_zoom: HOME is not set; cannot save snapshot\n";
751 std::filesystem::path snapshot_dir = std::filesystem::path(home) /
"Pictures";
752 std::error_code error;
753 std::filesystem::create_directories(snapshot_dir, error);
755 std::cerr << std::format(
"fractal_zoom: failed to create snapshot directory '{}': {}\n",
756 snapshot_dir.string(),
761 const std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
762 std::tm local_time{};
764 localtime_s(&local_time, &now);
766 localtime_r(&now, &local_time);
769 const std::string filename = std::format(
770 "fractal_zoom_snapshot.{:04d}-{:02d}-{:02d}.{:02d}-{:02d}-{:02d}-{:04d}.png",
771 local_time.tm_year + 1900,
772 local_time.tm_mon + 1,
779 const std::filesystem::path snapshot_path = snapshot_dir / filename;
782 std::cout <<
"fractal_zoom: saved snapshot: " << snapshot_path.string() <<
"\n";
783 }
catch (
const std::exception &ex) {
784 std::cerr <<
"fractal_zoom: failed to save snapshot: " << ex.what() <<
"\n";
788 void applyWheelZoom(
float wheel_y) {
790 if (extent.width == 0U || extent.height == 0U ||
window ==
nullptr) {
794 float mouse_x = 0.0f;
795 float mouse_y = 0.0f;
796 SDL_GetMouseState(&mouse_x, &mouse_y);
798 const ReferenceScalar base_scale = ReferenceScalar(2) / (zoom * ReferenceScalar(std::min(extent.width, extent.height)));
799 const ReferenceScalar before_x = (
static_cast<ReferenceScalar
>(mouse_x) - ReferenceScalar(extent.width) * ReferenceScalar(
"0.5")) * base_scale + center_x;
800 const ReferenceScalar before_y = (ReferenceScalar(extent.height) * ReferenceScalar(
"0.5") -
static_cast<ReferenceScalar
>(mouse_y)) * base_scale + center_y;
802 const ReferenceScalar zoom_factor = (wheel_y > 0.0f) ? ReferenceScalar(
"1.2") : (ReferenceScalar(1) / ReferenceScalar(
"1.2"));
803 zoom = clampReferenceScalar(zoom * zoom_factor, ReferenceScalar(
"0.5"), MAX_ZOOM);
805 const ReferenceScalar new_scale = ReferenceScalar(2) / (zoom * ReferenceScalar(std::min(extent.width, extent.height)));
806 const ReferenceScalar after_x = (
static_cast<ReferenceScalar
>(mouse_x) - ReferenceScalar(extent.width) * ReferenceScalar(
"0.5")) * new_scale + center_x;
807 const ReferenceScalar after_y = (ReferenceScalar(extent.height) * ReferenceScalar(
"0.5") -
static_cast<ReferenceScalar
>(mouse_y)) * new_scale + center_y;
809 center_x += before_x - after_x;
810 center_y += before_y - after_y;
812 if (wheel_y > 0.0f && zoom > ReferenceScalar(10)) {
813 max_iterations = std::min(max_iterations + 12, max_reference_iterations);
815 reference_orbit_dirty =
true;
818 void updateKeyboardNavigation() {
819 const bool *keys = SDL_GetKeyboardState(
nullptr);
820 if (keys ==
nullptr) {
824 const auto now = std::chrono::steady_clock::now();
825 const ReferenceScalar dt = ReferenceScalar(std::chrono::duration<double>(now - last_tick).count());
828 const ReferenceScalar move_speed = ReferenceScalar(
"0.85") * minReferenceScalar(dt, ReferenceScalar(
"0.1")) / zoom;
830 if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) {
831 center_x -= move_speed;
834 if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) {
835 center_x += move_speed;
838 if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
839 center_y += move_speed;
842 if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
843 center_y -= move_speed;
846 if (keys[SDL_SCANCODE_Z]) {
847 zoom = minReferenceScalar(zoom * (ReferenceScalar(1) + ReferenceScalar(
"1.9") * dt), MAX_ZOOM);
850 if (keys[SDL_SCANCODE_X]) {
851 zoom = maxReferenceScalar(zoom * (ReferenceScalar(1) - ReferenceScalar(
"1.9") * dt), ReferenceScalar(
"0.5"));
855 reference_orbit_dirty =
true;
860 center_x = ReferenceScalar(
"-0.5");
861 center_y = ReferenceScalar(0);
862 zoom = ReferenceScalar(1);
863 max_iterations = 256;
864 reference_orbit_dirty =
true;
867 void destroyFractalResources() {
868 if (
device == VK_NULL_HANDLE) {
869 fractal_pipeline = VK_NULL_HANDLE;
870 fractal_pipeline_layout = VK_NULL_HANDLE;
871 fractal_descriptor_set_layout = VK_NULL_HANDLE;
872 fractal_descriptor_pool = VK_NULL_HANDLE;
873 fractal_descriptor_sets.clear();
874 reference_orbit_buffers.clear();
875 reference_orbit_memories.clear();
876 reference_orbit_mapped.clear();
877 reference_orbit_coherent.clear();
878 reference_orbit_uploaded_generations.clear();
882 if (fractal_pipeline != VK_NULL_HANDLE) {
883 vkDestroyPipeline(
device, fractal_pipeline,
nullptr);
884 fractal_pipeline = VK_NULL_HANDLE;
886 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
887 vkDestroyPipelineLayout(
device, fractal_pipeline_layout,
nullptr);
888 fractal_pipeline_layout = VK_NULL_HANDLE;
890 if (fractal_descriptor_pool != VK_NULL_HANDLE) {
891 vkDestroyDescriptorPool(
device, fractal_descriptor_pool,
nullptr);
892 fractal_descriptor_pool = VK_NULL_HANDLE;
893 fractal_descriptor_sets.clear();
895 if (fractal_descriptor_set_layout != VK_NULL_HANDLE) {
896 vkDestroyDescriptorSetLayout(
device, fractal_descriptor_set_layout,
nullptr);
897 fractal_descriptor_set_layout = VK_NULL_HANDLE;
899 for (
size_t i = 0; i < reference_orbit_memories.size(); ++i) {
900 if (reference_orbit_memories[i] != VK_NULL_HANDLE) {
901 if (i < reference_orbit_mapped.size() && reference_orbit_mapped[i] !=
nullptr) {
902 vkUnmapMemory(
device, reference_orbit_memories[i]);
904 vkFreeMemory(
device, reference_orbit_memories[i],
nullptr);
907 for (VkBuffer buffer : reference_orbit_buffers) {
908 if (buffer != VK_NULL_HANDLE) {
909 vkDestroyBuffer(
device, buffer,
nullptr);
912 reference_orbit_buffers.clear();
913 reference_orbit_memories.clear();
914 reference_orbit_mapped.clear();
915 reference_orbit_coherent.clear();
916 reference_orbit_uploaded_generations.clear();
919 void destroyFractalPipeline() {
920 if (
device == VK_NULL_HANDLE) {
921 fractal_pipeline = VK_NULL_HANDLE;
922 fractal_pipeline_layout = VK_NULL_HANDLE;
925 if (fractal_pipeline != VK_NULL_HANDLE) {
926 vkDestroyPipeline(
device, fractal_pipeline,
nullptr);
927 fractal_pipeline = VK_NULL_HANDLE;
929 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
930 vkDestroyPipelineLayout(
device, fractal_pipeline_layout,
nullptr);
931 fractal_pipeline_layout = VK_NULL_HANDLE;
935 void createFractalPipeline() {
936 if (
device == VK_NULL_HANDLE) {
942 if (color_format == VK_FORMAT_UNDEFINED || depth_attachment_format == VK_FORMAT_UNDEFINED) {
946 ensureFractalResources();
947 if (fractal_descriptor_set_layout == VK_NULL_HANDLE || fractal_descriptor_sets.empty()) {
951 destroyFractalPipeline();
953 const std::string vert_path = shaderRoot +
"/fractal.vert.spv";
954 const std::string frag_path =
955#if defined(MXVK_USE_MOLTENVK)
956 shaderRoot +
"/fractal_float.frag.spv";
958 shaderRoot +
"/fractal.frag.spv";
960 const std::vector<char> vert_bytes =
loadSpv(vert_path);
961 const std::vector<char> frag_bytes =
loadSpv(frag_path);
964 VkShaderModule frag_module = VK_NULL_HANDLE;
969 VkPipelineShaderStageCreateInfo vert_stage{};
970 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
971 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
972 vert_stage.module = vert_module;
973 vert_stage.pName =
"main";
975 VkPipelineShaderStageCreateInfo frag_stage{};
976 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
977 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
978 frag_stage.module = frag_module;
979 frag_stage.pName =
"main";
981 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
983 VkPipelineVertexInputStateCreateInfo vertex_input{};
984 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
986 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
987 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
988 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
989 input_assembly.primitiveRestartEnable = VK_FALSE;
991 VkPipelineViewportStateCreateInfo viewport_state{};
992 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
993 viewport_state.viewportCount = 1;
994 viewport_state.scissorCount = 1;
996 const VkDynamicState dynamic_states[] = {
997 VK_DYNAMIC_STATE_VIEWPORT,
998 VK_DYNAMIC_STATE_SCISSOR,
1000 VkPipelineDynamicStateCreateInfo dynamic_state{};
1001 dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
1002 dynamic_state.dynamicStateCount = 2;
1003 dynamic_state.pDynamicStates = dynamic_states;
1005 VkPipelineRasterizationStateCreateInfo rasterizer{};
1006 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
1007 rasterizer.depthClampEnable = VK_FALSE;
1008 rasterizer.rasterizerDiscardEnable = VK_FALSE;
1009 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
1010 rasterizer.lineWidth = 1.0f;
1011 rasterizer.cullMode = VK_CULL_MODE_NONE;
1012 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
1014 VkPipelineMultisampleStateCreateInfo multisampling{};
1015 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
1016 multisampling.sampleShadingEnable = VK_FALSE;
1017 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
1019 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
1020 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
1021 depth_stencil.depthTestEnable = VK_FALSE;
1022 depth_stencil.depthWriteEnable = VK_FALSE;
1023 depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
1024 depth_stencil.depthBoundsTestEnable = VK_FALSE;
1025 depth_stencil.stencilTestEnable = VK_FALSE;
1027 VkPipelineColorBlendAttachmentState color_blend_attachment{};
1028 color_blend_attachment.colorWriteMask =
1029 VK_COLOR_COMPONENT_R_BIT |
1030 VK_COLOR_COMPONENT_G_BIT |
1031 VK_COLOR_COMPONENT_B_BIT |
1032 VK_COLOR_COMPONENT_A_BIT;
1033 color_blend_attachment.blendEnable = VK_FALSE;
1035 VkPipelineColorBlendStateCreateInfo color_blending{};
1036 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
1037 color_blending.logicOpEnable = VK_FALSE;
1038 color_blending.attachmentCount = 1;
1039 color_blending.pAttachments = &color_blend_attachment;
1041 VkPushConstantRange push_constant_range{};
1042 push_constant_range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
1043 push_constant_range.offset = 0;
1044 push_constant_range.size =
static_cast<uint32_t
>(
sizeof(FractalPushConstants));
1046 VkPipelineLayoutCreateInfo pipeline_layout_info{};
1047 pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
1048 pipeline_layout_info.setLayoutCount = 1;
1049 pipeline_layout_info.pSetLayouts = &fractal_descriptor_set_layout;
1050 pipeline_layout_info.pushConstantRangeCount = 1;
1051 pipeline_layout_info.pPushConstantRanges = &push_constant_range;
1053 if (vkCreatePipelineLayout(
device, &pipeline_layout_info,
nullptr, &fractal_pipeline_layout) != VK_SUCCESS) {
1054 throw mxvk::Exception(
"Failed to create fractal pipeline layout");
1057 VkPipelineRenderingCreateInfo pipeline_rendering_info{};
1058 pipeline_rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
1059 pipeline_rendering_info.viewMask = 0;
1060 pipeline_rendering_info.colorAttachmentCount = 1;
1061 pipeline_rendering_info.pColorAttachmentFormats = &color_format;
1062 pipeline_rendering_info.depthAttachmentFormat = depth_attachment_format;
1063 pipeline_rendering_info.stencilAttachmentFormat = VK_FORMAT_UNDEFINED;
1065 VkGraphicsPipelineCreateInfo pipeline_info{};
1066 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
1067 pipeline_info.pNext = &pipeline_rendering_info;
1068 pipeline_info.stageCount = 2;
1069 pipeline_info.pStages = shader_stages;
1070 pipeline_info.pVertexInputState = &vertex_input;
1071 pipeline_info.pInputAssemblyState = &input_assembly;
1072 pipeline_info.pViewportState = &viewport_state;
1073 pipeline_info.pRasterizationState = &rasterizer;
1074 pipeline_info.pMultisampleState = &multisampling;
1075 pipeline_info.pDepthStencilState = &depth_stencil;
1076 pipeline_info.pColorBlendState = &color_blending;
1077 pipeline_info.pDynamicState = &dynamic_state;
1078 pipeline_info.layout = fractal_pipeline_layout;
1079 pipeline_info.renderPass = VK_NULL_HANDLE;
1080 pipeline_info.subpass = 0;
1082 if (vkCreateGraphicsPipelines(
device, VK_NULL_HANDLE, 1, &pipeline_info,
nullptr, &fractal_pipeline) != VK_SUCCESS) {
1083 throw mxvk::Exception(
"Failed to create fractal graphics pipeline");
1086 if (fractal_pipeline != VK_NULL_HANDLE) {
1087 vkDestroyPipeline(
device, fractal_pipeline,
nullptr);
1088 fractal_pipeline = VK_NULL_HANDLE;
1090 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
1091 vkDestroyPipelineLayout(
device, fractal_pipeline_layout,
nullptr);
1092 fractal_pipeline_layout = VK_NULL_HANDLE;
1094 if (frag_module != VK_NULL_HANDLE) {
1095 vkDestroyShaderModule(
device, frag_module,
nullptr);
1097 vkDestroyShaderModule(
device, vert_module,
nullptr);
1101 vkDestroyShaderModule(
device, frag_module,
nullptr);
1102 vkDestroyShaderModule(
device, vert_module,
nullptr);
1105 void ensureFractalResources() {
1107 if (reference_orbit_buffers.size() != required_count || fractal_descriptor_sets.size() != required_count) {
1108 destroyFractalResources();
1111 if (reference_orbit_buffers.empty()) {
1112 createReferenceOrbitBuffers(required_count);
1114 if (fractal_descriptor_set_layout == VK_NULL_HANDLE) {
1115 createDescriptorSetLayout();
1117 if (fractal_descriptor_pool == VK_NULL_HANDLE) {
1118 createDescriptorPool();
1120 if (fractal_descriptor_sets.empty()) {
1121 allocateDescriptorSets(required_count);
1122 writeDescriptorSets();
1126 void createReferenceOrbitBuffers(
size_t buffer_count) {
1127 const VkDeviceSize
buffer_size =
static_cast<VkDeviceSize
>(reference_orbit_capacity *
sizeof(OrbitSample));
1129 auto cleanup_reference_orbit_buffers = [&]() {
1130 for (
size_t i = 0; i < reference_orbit_memories.size(); ++i) {
1131 if (reference_orbit_memories[i] != VK_NULL_HANDLE) {
1132 if (i < reference_orbit_mapped.size() && reference_orbit_mapped[i] !=
nullptr) {
1133 vkUnmapMemory(
device, reference_orbit_memories[i]);
1135 vkFreeMemory(
device, reference_orbit_memories[i],
nullptr);
1138 for (VkBuffer buffer : reference_orbit_buffers) {
1139 if (buffer != VK_NULL_HANDLE) {
1140 vkDestroyBuffer(
device, buffer,
nullptr);
1143 reference_orbit_buffers.clear();
1144 reference_orbit_memories.clear();
1145 reference_orbit_mapped.clear();
1146 reference_orbit_coherent.clear();
1147 reference_orbit_uploaded_generations.clear();
1150 cleanup_reference_orbit_buffers();
1151 reference_orbit_buffers.assign(buffer_count, VK_NULL_HANDLE);
1152 reference_orbit_memories.assign(buffer_count, VK_NULL_HANDLE);
1153 reference_orbit_mapped.assign(buffer_count,
nullptr);
1154 reference_orbit_coherent.assign(buffer_count,
false);
1155 reference_orbit_uploaded_generations.assign(buffer_count, 0);
1158 for (
size_t i = 0; i < buffer_count; ++i) {
1160 createBuffer(buffer_size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, reference_orbit_buffers[i], reference_orbit_memories[i]);
1161 reference_orbit_coherent[i] =
true;
1163 createBuffer(buffer_size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, reference_orbit_buffers[i], reference_orbit_memories[i]);
1164 reference_orbit_coherent[i] =
false;
1167 if (vkMapMemory(
device, reference_orbit_memories[i], 0, buffer_size, 0, &reference_orbit_mapped[i]) != VK_SUCCESS) {
1168 throw mxvk::Exception(
"Failed to map fractal reference orbit buffer");
1172 cleanup_reference_orbit_buffers();
1177 void createDescriptorSetLayout() {
1178 VkDescriptorSetLayoutBinding orbit_binding{};
1179 orbit_binding.binding = 0;
1180 orbit_binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
1181 orbit_binding.descriptorCount = 1;
1182 orbit_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
1184 VkDescriptorSetLayoutCreateInfo layout_info{};
1185 layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
1186 layout_info.bindingCount = 1;
1187 layout_info.pBindings = &orbit_binding;
1189 if (vkCreateDescriptorSetLayout(
device, &layout_info,
nullptr, &fractal_descriptor_set_layout) != VK_SUCCESS) {
1190 throw mxvk::Exception(
"Failed to create fractal descriptor set layout");
1194 void createDescriptorPool() {
1195 VkDescriptorPoolSize pool_size{};
1196 pool_size.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
1197 pool_size.descriptorCount =
static_cast<uint32_t
>(reference_orbit_buffers.size());
1199 VkDescriptorPoolCreateInfo pool_info{};
1200 pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
1201 pool_info.maxSets =
static_cast<uint32_t
>(reference_orbit_buffers.size());
1202 pool_info.poolSizeCount = 1;
1203 pool_info.pPoolSizes = &pool_size;
1205 if (vkCreateDescriptorPool(
device, &pool_info,
nullptr, &fractal_descriptor_pool) != VK_SUCCESS) {
1206 throw mxvk::Exception(
"Failed to create fractal descriptor pool");
1210 void allocateDescriptorSets(
size_t descriptor_count) {
1211 std::vector<VkDescriptorSetLayout> layouts(descriptor_count, fractal_descriptor_set_layout);
1212 fractal_descriptor_sets.assign(descriptor_count, VK_NULL_HANDLE);
1214 VkDescriptorSetAllocateInfo alloc_info{};
1215 alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1216 alloc_info.descriptorPool = fractal_descriptor_pool;
1217 alloc_info.descriptorSetCount =
static_cast<uint32_t
>(descriptor_count);
1218 alloc_info.pSetLayouts = layouts.data();
1220 if (vkAllocateDescriptorSets(
device, &alloc_info, fractal_descriptor_sets.data()) != VK_SUCCESS) {
1221 throw mxvk::Exception(
"Failed to allocate fractal descriptor sets");
1225 void writeDescriptorSets() {
1226 std::vector<VkDescriptorBufferInfo> buffer_infos(fractal_descriptor_sets.size());
1227 std::vector<VkWriteDescriptorSet> writes(fractal_descriptor_sets.size());
1229 for (
size_t i = 0; i < fractal_descriptor_sets.size(); ++i) {
1230 buffer_infos[i].buffer = reference_orbit_buffers[i];
1231 buffer_infos[i].offset = 0;
1232 buffer_infos[i].range =
static_cast<VkDeviceSize
>(reference_orbit_capacity *
sizeof(OrbitSample));
1234 writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1235 writes[i].dstSet = fractal_descriptor_sets[i];
1236 writes[i].dstBinding = 0;
1237 writes[i].dstArrayElement = 0;
1238 writes[i].descriptorCount = 1;
1239 writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
1240 writes[i].pBufferInfo = &buffer_infos[i];
1243 vkUpdateDescriptorSets(
device,
static_cast<uint32_t
>(writes.size()), writes.data(), 0,
nullptr);
1246 void updateReferenceOrbit(uint32_t image_index, VkExtent2D extent) {
1247 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] ==
nullptr) {
1251 const bool needs_deep_references = zoom >= direct_reference_zoom_threshold;
1252 if (!needs_deep_references) {
1253 if (reference_orbit_dirty || cached_reference_count != 0) {
1254 std::fill(reference_orbit_samples.begin(), reference_orbit_samples.end(), OrbitSample{});
1255 reference_orbit_samples[0] = {0.0f,
static_cast<float>(reference_metadata_capacity),
static_cast<float>(reference_orbit_stride), 0.0f};
1256 cached_reference_count = 0;
1257 reference_orbit_dirty =
false;
1258 ++reference_orbit_generation;
1261 uploadReferenceOrbit(image_index);
1265 const auto now = std::chrono::steady_clock::now();
1266 const bool throttle_rebuild = reference_orbit_generation != 0 && (now - last_reference_rebuild_time) < reference_rebuild_interval;
1267 if (!reference_orbit_dirty || throttle_rebuild) {
1268 uploadReferenceOrbit(image_index);
1272 const int iteration_count = std::clamp(max_iterations, 1, max_reference_iterations);
1273 const ReferenceScalar min_dimension = ReferenceScalar(std::max<uint32_t>(std::min(extent.width, extent.height), 1U));
1274 const ReferenceScalar width = ReferenceScalar(std::max<uint32_t>(extent.width, 1U));
1275 const ReferenceScalar height = ReferenceScalar(std::max<uint32_t>(extent.height, 1U));
1276 const ReferenceScalar half_width_uv = width / (ReferenceScalar(2) * min_dimension);
1277 const ReferenceScalar half_height_uv = height / (ReferenceScalar(2) * min_dimension);
1279 std::fill(reference_orbit_samples.begin(), reference_orbit_samples.end(), OrbitSample{});
1280 std::vector<ReferenceTile> tiles;
1281 tiles.reserve(max_adaptive_references);
1283 for (
int root_y = 0; root_y < adaptive_root_rows; ++root_y) {
1284 for (
int root_x = 0; root_x < adaptive_root_cols; ++root_x) {
1285 const ReferenceScalar tile_min_x = -half_width_uv + ReferenceScalar(root_x) * (ReferenceScalar(2) * half_width_uv) / ReferenceScalar(adaptive_root_cols);
1286 const ReferenceScalar tile_max_x = -half_width_uv + ReferenceScalar(root_x + 1) * (ReferenceScalar(2) * half_width_uv) / ReferenceScalar(adaptive_root_cols);
1287 const ReferenceScalar tile_min_y = -half_height_uv + ReferenceScalar(root_y) * (ReferenceScalar(2) * half_height_uv) / ReferenceScalar(adaptive_root_rows);
1288 const ReferenceScalar tile_max_y = -half_height_uv + ReferenceScalar(root_y + 1) * (ReferenceScalar(2) * half_height_uv) / ReferenceScalar(adaptive_root_rows);
1290 tiles.push_back({tile_min_x, tile_min_y, tile_max_x, tile_max_y, 0});
1294 bool refined =
true;
1295 const int validation_iteration_count = std::min(iteration_count, max_validation_iterations);
1296 while (refined && tiles.size() + 3 <=
static_cast<size_t>(max_adaptive_references)) {
1299 for (
size_t tile_index = 0; tile_index < tiles.size(); ++tile_index) {
1300 const ReferenceTile tile = tiles[tile_index];
1301 if (tile.depth >= max_adaptive_depth || isAdaptiveReferenceTileStable(tile, validation_iteration_count)) {
1305 const ReferenceScalar mid_x = (tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5");
1306 const ReferenceScalar mid_y = (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5");
1307 const int child_depth = tile.depth + 1;
1308 const std::array<ReferenceTile, 4> children{
1309 ReferenceTile{tile.min_uv_x, tile.min_uv_y, mid_x, mid_y, child_depth},
1310 ReferenceTile{mid_x, tile.min_uv_y, tile.max_uv_x, mid_y, child_depth},
1311 ReferenceTile{tile.min_uv_x, mid_y, mid_x, tile.max_uv_y, child_depth},
1312 ReferenceTile{mid_x, mid_y, tile.max_uv_x, tile.max_uv_y, child_depth}};
1314 tiles.erase(tiles.begin() +
static_cast<std::ptrdiff_t
>(tile_index));
1315 tiles.insert(tiles.begin() +
static_cast<std::ptrdiff_t
>(tile_index), children.begin(), children.end());
1321 const int reference_count =
static_cast<int>(std::min<size_t>(tiles.size(), max_adaptive_references));
1322 for (
int reference_index = 0; reference_index < reference_count; ++reference_index) {
1323 const ReferenceTile &tile = tiles[
static_cast<size_t>(reference_index)];
1324 const size_t orbit_base = referenceOrbitBase(reference_index);
1325 const ReferenceScalar ref_uv_x = (tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5");
1326 const ReferenceScalar ref_uv_y = (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5");
1327 const int sample_count = writeReferenceOrbit(orbit_base, ref_uv_x, ref_uv_y, iteration_count);
1328 writeReferenceMetadata(reference_index, tile, orbit_base, sample_count, ref_uv_x, ref_uv_y);
1331 reference_orbit_samples[0] = {
1332 static_cast<float>(reference_count),
1333 static_cast<float>(reference_metadata_capacity),
1334 static_cast<float>(reference_orbit_stride),
1337 cached_reference_count = reference_count;
1338 reference_orbit_dirty =
false;
1339 last_reference_rebuild_time = now;
1340 ++reference_orbit_generation;
1341 orbit_length = iteration_count + 1;
1342 uploadReferenceOrbit(image_index);
1345 void uploadReferenceOrbit(uint32_t image_index) {
1346 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] ==
nullptr) {
1349 if (image_index < reference_orbit_uploaded_generations.size() && reference_orbit_uploaded_generations[image_index] == reference_orbit_generation) {
1353 const VkDeviceSize upload_size =
static_cast<VkDeviceSize
>(reference_orbit_samples.size() *
sizeof(OrbitSample));
1354 std::memcpy(reference_orbit_mapped[image_index], reference_orbit_samples.data(),
static_cast<size_t>(upload_size));
1356 if (!reference_orbit_coherent[image_index]) {
1357 VkMappedMemoryRange range{};
1358 range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
1359 range.memory = reference_orbit_memories[image_index];
1361 range.size = upload_size;
1362 vkFlushMappedMemoryRanges(
device, 1, &range);
1365 if (image_index < reference_orbit_uploaded_generations.size()) {
1366 reference_orbit_uploaded_generations[image_index] = reference_orbit_generation;
1370 bool isAdaptiveReferenceTileStable(
const ReferenceTile &tile,
int iteration_count) {
1371 const size_t orbit_base = referenceOrbitBase(max_adaptive_references);
1372 const ReferenceScalar ref_uv_x = (tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5");
1373 const ReferenceScalar ref_uv_y = (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5");
1374 const int sample_count = writeReferenceOrbit(orbit_base, ref_uv_x, ref_uv_y, iteration_count);
1375 return isReferenceTileStable(tile, orbit_base, sample_count, ref_uv_x, ref_uv_y, iteration_count);
1378 int writeReferenceOrbit(
size_t orbit_base,
const ReferenceScalar &ref_uv_x,
const ReferenceScalar &ref_uv_y,
int iteration_count) {
1379 const ReferenceScalar c_x = center_x + ref_uv_x / zoom;
1380 const ReferenceScalar c_y = center_y + ref_uv_y / zoom;
1382 ReferenceScalar z_x = 0;
1383 ReferenceScalar z_y = 0;
1384 int sample_count = 1;
1386 reference_orbit_samples[orbit_base] = {
1387 toOrbitSampleScalar(z_x),
1388 toOrbitSampleScalar(z_y),
1392 for (
int i = 0; i < iteration_count && sample_count < reference_orbit_stride; ++i) {
1393 const ReferenceScalar next_x = z_x * z_x - z_y * z_y + c_x;
1394 const ReferenceScalar next_y = ReferenceScalar(2) * z_x * z_y + c_y;
1399 reference_orbit_samples[orbit_base +
static_cast<size_t>(sample_count)] = {
1400 toOrbitSampleScalar(z_x),
1401 toOrbitSampleScalar(z_y),
1406 const ReferenceScalar mag2 = z_x * z_x + z_y * z_y;
1407 if (mag2 > ReferenceScalar(4)) {
1412 reference_orbit_samples[orbit_base].z =
static_cast<float>(sample_count);
1413 return sample_count;
1416 void writeReferenceMetadata(
int reference_index,
const ReferenceTile &tile,
size_t orbit_base,
int sample_count,
const ReferenceScalar &ref_uv_x,
const ReferenceScalar &ref_uv_y) {
1417 const size_t metadata_base = 1 +
static_cast<size_t>(reference_index) * 2;
1418 reference_orbit_samples[metadata_base] = {
1419 toOrbitSampleScalar(tile.min_uv_x),
1420 toOrbitSampleScalar(tile.min_uv_y),
1421 toOrbitSampleScalar(tile.max_uv_x),
1422 toOrbitSampleScalar(tile.max_uv_y)};
1423 reference_orbit_samples[metadata_base + 1] = {
1424 toOrbitSampleScalar(ref_uv_x),
1425 toOrbitSampleScalar(ref_uv_y),
1426 static_cast<float>(orbit_base),
1427 static_cast<float>(sample_count)};
1430 bool isReferenceTileStable(
const ReferenceTile &tile,
size_t orbit_base,
int sample_count,
const ReferenceScalar &ref_uv_x,
const ReferenceScalar &ref_uv_y,
int iteration_count)
const {
1431 const std::array<std::pair<ReferenceScalar, ReferenceScalar>, 5> sample_points{
1432 std::pair{tile.min_uv_x, tile.min_uv_y},
1433 std::pair{tile.max_uv_x, tile.min_uv_y},
1434 std::pair{tile.min_uv_x, tile.max_uv_y},
1435 std::pair{tile.max_uv_x, tile.max_uv_y},
1436 std::pair{(tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5"), (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5")}};
1438 for (
const auto &[sample_uv_x, sample_uv_y] : sample_points) {
1439 const float delta_c_x = ((sample_uv_x - ref_uv_x) / zoom).convert_to<float>();
1440 const float delta_c_y = ((sample_uv_y - ref_uv_y) / zoom).convert_to<float>();
1441 if (!isPerturbationSampleStable(orbit_base, sample_count, iteration_count, delta_c_x, delta_c_y)) {
1449 bool isPerturbationSampleStable(
size_t orbit_base,
int sample_count,
int iteration_count,
float delta_c_x,
float delta_c_y)
const {
1452 const int count = std::min(iteration_count, sample_count - 1);
1454 for (
int i = 0; i < count; ++i) {
1455 const OrbitSample &ref = reference_orbit_samples[orbit_base +
static_cast<size_t>(i)];
1456 const float z_dz_x = ref.x * dz_x - ref.y * dz_y;
1457 const float z_dz_y = ref.x * dz_y + ref.y * dz_x;
1458 const float dz_sq_x = dz_x * dz_x - dz_y * dz_y;
1459 const float dz_sq_y = 2.0f * dz_x * dz_y;
1461 dz_x = 2.0f * z_dz_x + dz_sq_x + delta_c_x;
1462 dz_y = 2.0f * z_dz_y + dz_sq_y + delta_c_y;
1464 if (!std::isfinite(dz_x) || !std::isfinite(dz_y)) {
1468 const OrbitSample &next_ref = reference_orbit_samples[orbit_base +
static_cast<size_t>(i + 1)];
1469 const float true_x = next_ref.x + dz_x;
1470 const float true_y = next_ref.y + dz_y;
1471 const float true_mag2 = true_x * true_x + true_y * true_y;
1472 if (!std::isfinite(true_mag2) || true_mag2 > 4.0f) {
1476 const float dz_mag2 = dz_x * dz_x + dz_y * dz_y;
1477 const float ref_mag2 = ref.x * ref.x + ref.y * ref.y;
1478 if (!std::isfinite(dz_mag2) || dz_mag2 > perturbation_breakdown_limit2 || (ref_mag2 > 0.0f && dz_mag2 > ref_mag2 * 0.25f)) {
1483 return sample_count > iteration_count;
1486 void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
1487 VkBufferCreateInfo buffer_info{};
1488 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1489 buffer_info.size = size;
1490 buffer_info.usage = usage;
1491 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1493 if (vkCreateBuffer(
device, &buffer_info,
nullptr, &buffer) != VK_SUCCESS) {
1494 throw mxvk::Exception(
"Failed to create fractal buffer");
1497 VkMemoryRequirements mem_requirements{};
1498 vkGetBufferMemoryRequirements(
device, buffer, &mem_requirements);
1500 VkMemoryAllocateInfo alloc_info{};
1501 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1502 alloc_info.allocationSize = mem_requirements.size;
1504 alloc_info.memoryTypeIndex = findMemoryType(mem_requirements.memoryTypeBits, properties);
1506 vkDestroyBuffer(
device, buffer,
nullptr);
1507 buffer = VK_NULL_HANDLE;
1511 if (vkAllocateMemory(
device, &alloc_info,
nullptr, &bufferMemory) != VK_SUCCESS) {
1512 vkDestroyBuffer(
device, buffer,
nullptr);
1513 buffer = VK_NULL_HANDLE;
1514 throw mxvk::Exception(
"Failed to allocate fractal buffer memory");
1517 if (vkBindBufferMemory(
device, buffer, bufferMemory, 0) != VK_SUCCESS) {
1518 vkFreeMemory(
device, bufferMemory,
nullptr);
1519 bufferMemory = VK_NULL_HANDLE;
1520 vkDestroyBuffer(
device, buffer,
nullptr);
1521 buffer = VK_NULL_HANDLE;
1522 throw mxvk::Exception(
"Failed to bind fractal buffer memory");
1526 uint32_t findMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties)
const {
1527 VkPhysicalDeviceMemoryProperties mem_properties{};
1528 vkGetPhysicalDeviceMemoryProperties(
physical_device, &mem_properties);
1530 for (uint32_t i = 0; i < mem_properties.memoryTypeCount; ++i) {
1531 const bool type_matches = (type_filter & (1U << i)) != 0U;
1532 const bool property_matches = (mem_properties.memoryTypes[i].propertyFlags & properties) == properties;
1533 if (type_matches && property_matches) {
1538 throw mxvk::Exception(
"Failed to find suitable memory type for fractal buffer");
1542#if defined(MXVK_USE_MOLTENVK)
1547 struct FractalPushConstants {
1548 PushScalar center_x;
1549 PushScalar center_y;
1550 PushScalar inverse_zoom;
1552 PushScalar resolution_x;
1553 PushScalar resolution_y;
1560 VkPipeline fractal_pipeline = VK_NULL_HANDLE;
1561 VkPipelineLayout fractal_pipeline_layout = VK_NULL_HANDLE;
1562 VkDescriptorSetLayout fractal_descriptor_set_layout = VK_NULL_HANDLE;
1563 VkDescriptorPool fractal_descriptor_pool = VK_NULL_HANDLE;
1564 std::vector<VkDescriptorSet> fractal_descriptor_sets{};
1565 std::vector<VkBuffer> reference_orbit_buffers{};
1566 std::vector<VkDeviceMemory> reference_orbit_memories{};
1567 std::vector<void *> reference_orbit_mapped{};
1568 std::vector<bool> reference_orbit_coherent{};
1569 std::vector<uint64_t> reference_orbit_uploaded_generations{};
1571 static constexpr int max_reference_iterations = 4096;
1572 static constexpr int adaptive_root_cols = 3;
1573 static constexpr int adaptive_root_rows = 2;
1574 static constexpr int max_adaptive_references = 32;
1575 static constexpr int max_adaptive_depth = 4;
1576 static constexpr int max_validation_iterations = 768;
1577 static constexpr int reference_metadata_capacity = 1 + max_adaptive_references * 2;
1578 static constexpr int reference_orbit_stride = max_reference_iterations + 1;
1579 static constexpr int reference_orbit_capacity = reference_metadata_capacity + (max_adaptive_references + 1) * reference_orbit_stride;
1580 static constexpr float perturbation_breakdown_limit2 = 0.0625f;
1581 static constexpr std::chrono::milliseconds reference_rebuild_interval{100};
1582#if defined(MXVK_USE_MOLTENVK)
1583 static inline const ReferenceScalar direct_reference_zoom_threshold = ReferenceScalar(4096);
1585 static inline const ReferenceScalar direct_reference_zoom_threshold = ReferenceScalar(
"1e15");
1587 static inline const ReferenceScalar MAX_ZOOM = ReferenceScalar(
"1e1000");
1588 std::vector<OrbitSample> reference_orbit_samples{};
1589 int orbit_length = 0;
1590 int cached_reference_count = -1;
1591 bool reference_orbit_dirty =
true;
1592 uint64_t reference_orbit_generation = 1;
1593 std::chrono::steady_clock::time_point last_reference_rebuild_time{};
1595 static constexpr size_t referenceOrbitBase(
int reference_index) {
1596 return static_cast<size_t>(reference_metadata_capacity) +
static_cast<size_t>(reference_index) *
static_cast<size_t>(reference_orbit_stride);
1599 ReferenceScalar center_x = ReferenceScalar(
"-0.5");
1600 ReferenceScalar center_y = ReferenceScalar(0);
1601 ReferenceScalar zoom = ReferenceScalar(1);
1602 int max_iterations = 256;
1603 int palette_index = 0;
1605 bool dragging =
false;
1606 int drag_start_mouse_x = 0;
1607 int drag_start_mouse_y = 0;
1608 ReferenceScalar drag_start_center_x = ReferenceScalar(0);
1609 ReferenceScalar drag_start_center_y = ReferenceScalar(0);
1611 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
1612 std::chrono::steady_clock::time_point last_tick{std::chrono::steady_clock::now()};
1613 std::string shaderRoot;
1614 uint32_t snapshot_index = 0;
1615#if defined(MXWRITE_ENABLED)
1616 Writer video_writer{};
1617 uint32_t video_record_width = 0;
1618 uint32_t video_record_height = 0;
1619 std::string video_output_path =
"output.mp4";
1620 static constexpr uint32_t recording_readback_slot_count = 4;
1621 std::vector<RecordingReadbackSlot> recording_readbacks{};
1622 std::vector<bool> recording_pending_images{};
1623 VkDeviceSize recording_row_bytes = 0;
1624 VkDeviceSize recording_image_bytes = 0;
1625 std::chrono::steady_clock::time_point next_recording_frame_time{};
1626 std::chrono::steady_clock::duration recording_frame_interval = std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(1.0 / 60.0));
1627 bool recording_readbacks_ready =
false;
1628 bool recording_format_is_bgra =
false;
1629 uint64_t recording_dropped_frames = 0;
1630 std::jthread recording_worker{};
1631 std::mutex recording_mutex{};
1632 std::condition_variable recording_cv{};
1633 std::condition_variable recording_idle_cv{};
1634 std::queue<size_t> recording_ready_slots{};
1635 bool recording_worker_stop =
false;