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) :
mxvk::
VK_Window(
"-[ Fractal Zoom - MXVK ]-", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), reference_orbit_samples(static_cast<size_t>(reference_orbit_capacity)), shaderRoot(((path.empty() || path ==
".") ? std::string(
fractal_zoom_ASSET_DIR) : path) +
"/data") {}
60#if defined(MXWRITE_ENABLED)
63 if (
device != VK_NULL_HANDLE) {
66 destroyFractalResources();
69 void event(SDL_Event &e)
override {
70 if (e.type == SDL_EVENT_KEY_DOWN) {
71 if ((e.key.key == SDLK_F10 || e.key.scancode == SDL_SCANCODE_F10) && !e.key.repeat) {
72 saveFractalSnapshot();
75 if ((e.key.key == SDLK_P || e.key.scancode == SDL_SCANCODE_P) && !e.key.repeat) {
76#if defined(MXWRITE_ENABLED)
77 toggleVideoRecording();
79 std::cout <<
"fractal_zoom: MXWrite is unavailable; video recording disabled\n";
87 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
89 drag_start_mouse_x = e.button.x;
90 drag_start_mouse_y = e.button.y;
91 drag_start_center_x = center_x;
92 drag_start_center_y = center_y;
96 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
101 if (e.type == SDL_EVENT_MOUSE_MOTION && dragging) {
103 if (extent.width == 0U || extent.height == 0U) {
106 const int delta_x = e.motion.x - drag_start_mouse_x;
107 const int delta_y = e.motion.y - drag_start_mouse_y;
108 const ReferenceScalar scale = ReferenceScalar(2) / (zoom * ReferenceScalar(std::min(extent.width, extent.height)));
109 center_x = drag_start_center_x -
static_cast<ReferenceScalar
>(delta_x) * scale;
110 center_y = drag_start_center_y +
static_cast<ReferenceScalar
>(delta_y) * scale;
111 reference_orbit_dirty =
true;
115 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
116 applyWheelZoom(e.wheel.y);
120 void proc()
override { updateKeyboardNavigation(); }
123#if defined(MXWRITE_ENABLED)
124 serviceRecordingReadbacks();
127#if defined(MXWRITE_ENABLED)
128 recordPresentedFrame();
133#if defined(MXWRITE_ENABLED)
134 if (video_writer.is_open()) {
135 std::cerr <<
"fractal_zoom: swapchain is changing; closing current video recording\n";
139 destroyFractalResources();
145 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE) {
146 createFractalPipeline();
149 if (image_index >= fractal_descriptor_sets.size()) {
153 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE || fractal_descriptor_sets[image_index] == VK_NULL_HANDLE) {
158 if (extent.width == 0U || extent.height == 0U) {
162 updateReferenceOrbit(image_index, extent);
164 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fractal_pipeline);
165 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fractal_pipeline_layout, 0, 1, &fractal_descriptor_sets[image_index], 0,
nullptr);
167 const FractalPushConstants push_constants{center_x.convert_to<PushScalar>(), center_y.convert_to<PushScalar>(), (ReferenceScalar(1) / zoom).convert_to<PushScalar>(),
static_cast<PushScalar
>(std::chrono::duration<double>(std::chrono::steady_clock::now() - start_time).count()),
static_cast<PushScalar
>(extent.width),
static_cast<PushScalar
>(extent.height), max_iterations, palette_index, orbit_length, 0};
169 vkCmdPushConstants(cmd, fractal_pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0,
sizeof(push_constants), &push_constants);
171 vkCmdDraw(cmd, 3, 1, 0, 0);
175 static ReferenceScalar minReferenceScalar(
const ReferenceScalar &a,
const ReferenceScalar &b) {
return (a < b) ? a : b; }
177 static ReferenceScalar maxReferenceScalar(
const ReferenceScalar &a,
const ReferenceScalar &b) {
return (a > b) ? a : b; }
179 static ReferenceScalar clampReferenceScalar(
const ReferenceScalar &value,
const ReferenceScalar &minimum,
const ReferenceScalar &maximum) {
return maxReferenceScalar(minReferenceScalar(value, maximum), minimum); }
181 static float toOrbitSampleScalar(
const ReferenceScalar &value) {
return value.convert_to<
float>(); }
183 void handleKey(SDL_Keycode key) {
192 center_x = ReferenceScalar(
"-0.5");
193 center_y = ReferenceScalar(0);
194 zoom = ReferenceScalar(1);
195 max_iterations = 256;
196 reference_orbit_dirty =
true;
199 center_x = ReferenceScalar(
"-0.745");
200 center_y = ReferenceScalar(
"0.113");
201 zoom = ReferenceScalar(50);
202 max_iterations = 512;
203 reference_orbit_dirty =
true;
206 center_x = ReferenceScalar(
"-0.761574");
207 center_y = ReferenceScalar(
"-0.0847596");
208 zoom = ReferenceScalar(220);
209 max_iterations = 900;
210 reference_orbit_dirty =
true;
214 max_iterations = std::min(max_iterations + 64, max_reference_iterations);
215 reference_orbit_dirty =
true;
218 max_iterations = std::max(max_iterations - 64, 64);
219 reference_orbit_dirty =
true;
221 case SDLK_LEFTBRACKET:
222 palette_index = (palette_index + 2) % 3;
224 case SDLK_RIGHTBRACKET:
225 palette_index = (palette_index + 1) % 3;
232#if defined(MXWRITE_ENABLED)
233 void toggleVideoRecording() {
234 if (video_writer.is_open()) {
240 if (extent.width == 0U || extent.height == 0U) {
241 std::cerr <<
"fractal_zoom: cannot start video recording before the swapchain is ready\n";
245 constexpr float video_fps = 60.0f;
246 EncodeOptions encode_options{};
247 encode_options.
crf = 24;
248 encode_options.
preset =
"ultrafast";
249 encode_options.
tune =
"zerolatency";
252 if (!video_writer.open(video_output_path,
static_cast<int>(extent.width),
static_cast<int>(extent.height), video_fps, encode_options)) {
253 std::cerr <<
"fractal_zoom: failed to open MXWrite output file: " << video_output_path <<
"\n";
257 video_record_width = extent.width;
258 video_record_height = extent.height;
260 createRecordingReadbacks(extent);
261 }
catch (
const std::exception &ex) {
262 std::cerr <<
"fractal_zoom: failed to create async recording readback resources: " << ex.what() <<
"\n";
263 video_writer.close();
264 video_record_width = 0;
265 video_record_height = 0;
268 std::cout << std::format(
"fractal_zoom: recording video to {} at {}x{} 60 FPS\n", video_output_path, video_record_width, video_record_height);
271 void closeVideoWriter() {
272 if (!video_writer.is_open()) {
276 destroyRecordingReadbacks(
true);
277 video_writer.close();
278 std::cout <<
"fractal_zoom: saved video: " << video_output_path <<
"\n";
279 video_record_width = 0;
280 video_record_height = 0;
283 void serviceRecordingReadbacks() {
284 if (!video_writer.is_open()) {
289 pumpCompletedRecordingReadbacks(
false);
290 submitPendingRecordingReadbacks();
291 }
catch (
const std::exception &ex) {
292 std::cerr <<
"fractal_zoom: failed to service recording readback: " << ex.what() <<
"\n";
297 void recordPresentedFrame() {
298 if (!video_writer.is_open()) {
304 if (extent.width != video_record_width || extent.height != video_record_height) {
305 std::cerr <<
"fractal_zoom: swapchain size changed; closing current video recording\n";
309 const auto now = std::chrono::steady_clock::now();
310 if (now < next_recording_frame_time) {
313 next_recording_frame_time += recording_frame_interval;
314 if (next_recording_frame_time <= now) {
315 next_recording_frame_time = now + recording_frame_interval;
320 pumpCompletedRecordingReadbacks(
false);
321 submitPendingRecordingReadbacks();
322 }
catch (
const std::exception &ex) {
323 std::cerr <<
"fractal_zoom: failed to record video frame: " << ex.what() <<
"\n";
328 struct RecordingReadbackSlot {
329 VkBuffer buffer = VK_NULL_HANDLE;
330 VkDeviceMemory memory = VK_NULL_HANDLE;
331 VkCommandBuffer command_buffer = VK_NULL_HANDLE;
332 VkFence fence = VK_NULL_HANDLE;
333 std::vector<std::uint8_t> pixels{};
334 uint32_t image_index = std::numeric_limits<uint32_t>::max();
335 bool in_flight =
false;
339 void createRecordingReadbacks(VkExtent2D extent) {
340 destroyRecordingReadbacks(
false);
342 throw mxvk::Exception(
"recording requires initialized Vulkan render resources");
345 throw mxvk::Exception(
"recording requires swapchain transfer-source support");
350 if (!recording_format_is_bgra && !format_is_rgba) {
351 throw mxvk::Exception(std::format(
"unsupported recording swapchain format: {}",
static_cast<int>(
swapchain_format)));
354 recording_row_bytes =
static_cast<VkDeviceSize
>(extent.width) * 4U;
355 recording_image_bytes = recording_row_bytes *
static_cast<VkDeviceSize
>(extent.height);
356 recording_readbacks.resize(recording_readback_slot_count);
358 next_recording_frame_time = std::chrono::steady_clock::now();
360 VkCommandBufferAllocateInfo command_info{};
361 command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
363 command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
364 command_info.commandBufferCount = recording_readback_slot_count;
366 std::array<VkCommandBuffer, recording_readback_slot_count>
command_buffers{};
368 destroyRecordingReadbacks(
false);
369 throw mxvk::Exception(
"failed to allocate recording readback command buffers");
372 VkFenceCreateInfo fence_info{};
373 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
374 fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
376 for (
size_t i = 0; i < recording_readbacks.size(); ++i) {
377 RecordingReadbackSlot &slot = recording_readbacks[i];
379 createBuffer(recording_image_bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, slot.buffer, slot.memory);
380 if (vkCreateFence(
device, &fence_info,
nullptr, &slot.fence) != VK_SUCCESS) {
381 destroyRecordingReadbacks(
false);
382 throw mxvk::Exception(
"failed to create recording readback fence");
384 slot.pixels.resize(
static_cast<size_t>(recording_image_bytes));
387 recording_worker_stop =
false;
388 recording_worker = std::jthread([
this](std::stop_token stop_token) { recordingWorkerLoop(stop_token); });
389 recording_readbacks_ready =
true;
392 void destroyRecordingReadbacks(
bool drain) {
393 if (!recording_readbacks_ready && recording_readbacks.empty()) {
398 drainRecordingReadbacks();
402 std::lock_guard<std::mutex> lock(recording_mutex);
403 recording_worker_stop =
true;
405 recording_cv.notify_all();
406 if (recording_worker.joinable()) {
407 recording_worker.request_stop();
408 recording_worker.join();
411 if (
device != VK_NULL_HANDLE) {
412 for (RecordingReadbackSlot &slot : recording_readbacks) {
413 if (slot.in_flight && slot.fence != VK_NULL_HANDLE) {
414 vkWaitForFences(
device, 1, &slot.fence, VK_TRUE, UINT64_MAX);
419 if (slot.command_buffer != VK_NULL_HANDLE &&
command_pool != VK_NULL_HANDLE) {
421 slot.command_buffer = VK_NULL_HANDLE;
423 if (slot.fence != VK_NULL_HANDLE) {
424 vkDestroyFence(
device, slot.fence,
nullptr);
425 slot.fence = VK_NULL_HANDLE;
427 if (slot.buffer != VK_NULL_HANDLE) {
428 vkDestroyBuffer(
device, slot.buffer,
nullptr);
429 slot.buffer = VK_NULL_HANDLE;
431 if (slot.memory != VK_NULL_HANDLE) {
432 vkFreeMemory(
device, slot.memory,
nullptr);
433 slot.memory = VK_NULL_HANDLE;
438 recording_readbacks.clear();
439 recording_pending_images.clear();
441 std::lock_guard<std::mutex> lock(recording_mutex);
442 std::queue<size_t> empty_queue;
443 recording_ready_slots.swap(empty_queue);
444 recording_worker_stop =
false;
446 recording_readbacks_ready =
false;
447 recording_row_bytes = 0;
448 recording_image_bytes = 0;
449 recording_format_is_bgra =
false;
452 void submitPendingRecordingReadbacks() {
453 for (uint32_t image_index = 0; image_index < recording_pending_images.size(); ++image_index) {
454 if (!recording_pending_images[image_index]) {
457 if (submitRecordingReadback(image_index)) {
458 recording_pending_images[image_index] =
false;
463 void drainRecordingReadbacks() {
465 pumpCompletedRecordingReadbacks(
true);
466 std::unique_lock<std::mutex> lock(recording_mutex);
467 const bool idle = std::ranges::all_of(recording_readbacks, [](
const RecordingReadbackSlot &slot) {
return !slot.in_flight && !slot.queued; });
471 recording_idle_cv.wait(lock);
475 void pumpCompletedRecordingReadbacks(
bool wait_for_copy) {
476 for (
size_t i = 0; i < recording_readbacks.size(); ++i) {
477 RecordingReadbackSlot &slot = recording_readbacks[i];
478 VkFence slot_fence = VK_NULL_HANDLE;
480 std::lock_guard<std::mutex> lock(recording_mutex);
481 if (!slot.in_flight || slot.queued || slot.fence == VK_NULL_HANDLE) {
484 slot_fence = slot.fence;
487 VkResult fence_result = VK_SUCCESS;
489 fence_result = vkWaitForFences(
device, 1, &slot_fence, VK_TRUE, UINT64_MAX);
491 fence_result = vkGetFenceStatus(
device, slot_fence);
494 if (fence_result == VK_NOT_READY) {
497 if (fence_result != VK_SUCCESS) {
498 throw mxvk::Exception(std::format(
"recording readback fence failed: {}",
static_cast<int>(fence_result)));
502 std::lock_guard<std::mutex> lock(recording_mutex);
503 if (!slot.in_flight || slot.queued || slot.fence != slot_fence) {
510 recording_ready_slots.push(i);
512 recording_cv.notify_one();
516 bool submitRecordingReadback(uint32_t image_index) {
517 if (!recording_readbacks_ready || image_index == std::numeric_limits<uint32_t>::max()) {
525 if (source_fence != VK_NULL_HANDLE && vkGetFenceStatus(
device, source_fence) == VK_NOT_READY) {
529 RecordingReadbackSlot *free_slot =
nullptr;
531 std::lock_guard<std::mutex> lock(recording_mutex);
532 for (
size_t i = 0; i < recording_readbacks.size(); ++i) {
533 if (!recording_readbacks[i].in_flight && !recording_readbacks[i].queued) {
534 free_slot = &recording_readbacks[i];
539 if (free_slot ==
nullptr) {
540 ++recording_dropped_frames;
541 if (recording_dropped_frames % 60U == 0U) {
542 std::cerr <<
"fractal_zoom: dropped " << recording_dropped_frames <<
" recording frames (readback queue full)\n";
547 RecordingReadbackSlot &slot = *free_slot;
551 VkCommandBufferBeginInfo begin_info{};
552 begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
553 begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
554 VK_CHECK_RESULT(vkBeginCommandBuffer(slot.command_buffer, &begin_info));
556 VkImageMemoryBarrier2 to_transfer_barrier{};
557 to_transfer_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
558 to_transfer_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
559 to_transfer_barrier.srcAccessMask = VK_ACCESS_2_NONE;
560 to_transfer_barrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
561 to_transfer_barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
562 to_transfer_barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
563 to_transfer_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
564 to_transfer_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
565 to_transfer_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
567 to_transfer_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
568 to_transfer_barrier.subresourceRange.baseMipLevel = 0;
569 to_transfer_barrier.subresourceRange.levelCount = 1;
570 to_transfer_barrier.subresourceRange.baseArrayLayer = 0;
571 to_transfer_barrier.subresourceRange.layerCount = 1;
573 VkDependencyInfo to_transfer_dependency{};
574 to_transfer_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
575 to_transfer_dependency.imageMemoryBarrierCount = 1;
576 to_transfer_dependency.pImageMemoryBarriers = &to_transfer_barrier;
577 vkCmdPipelineBarrier2(slot.command_buffer, &to_transfer_dependency);
579 VkBufferImageCopy copy_region{};
580 copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
581 copy_region.imageSubresource.mipLevel = 0;
582 copy_region.imageSubresource.baseArrayLayer = 0;
583 copy_region.imageSubresource.layerCount = 1;
584 copy_region.imageExtent = {video_record_width, video_record_height, 1};
585 vkCmdCopyImageToBuffer(slot.command_buffer,
swapchain_images[image_index], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, slot.buffer, 1, ©_region);
587 VkImageMemoryBarrier2 to_present_barrier{};
588 to_present_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
589 to_present_barrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
590 to_present_barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
591 to_present_barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
592 to_present_barrier.dstAccessMask = VK_ACCESS_2_NONE;
593 to_present_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
594 to_present_barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
595 to_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
596 to_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
598 to_present_barrier.subresourceRange = to_transfer_barrier.subresourceRange;
600 VkDependencyInfo to_present_dependency{};
601 to_present_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
602 to_present_dependency.imageMemoryBarrierCount = 1;
603 to_present_dependency.pImageMemoryBarriers = &to_present_barrier;
604 vkCmdPipelineBarrier2(slot.command_buffer, &to_present_dependency);
608 VkCommandBufferSubmitInfo command_submit_info{};
609 command_submit_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
610 command_submit_info.commandBuffer = slot.command_buffer;
612 VkSubmitInfo2 submit_info{};
613 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
614 submit_info.commandBufferInfoCount = 1;
615 submit_info.pCommandBufferInfos = &command_submit_info;
617 slot.image_index = image_index;
618 const VkResult submit_result = vkQueueSubmit2(
graphics_queue, 1, &submit_info, slot.fence);
619 if (submit_result != VK_SUCCESS) {
620 throw mxvk::Exception(std::format(
"failed to submit recording readback: {}",
static_cast<int>(submit_result)));
623 std::lock_guard<std::mutex> lock(recording_mutex);
624 slot.in_flight =
true;
631 void recordingWorkerLoop(std::stop_token stop_token) {
632 std::vector<std::uint8_t> frame_pixels;
634 size_t slot_index = 0;
636 std::unique_lock<std::mutex> lock(recording_mutex);
637 recording_cv.wait(lock, [
this, &stop_token] {
return recording_worker_stop || stop_token.stop_requested() || !recording_ready_slots.empty(); });
638 if ((recording_worker_stop || stop_token.stop_requested()) && recording_ready_slots.empty()) {
641 slot_index = recording_ready_slots.front();
642 recording_ready_slots.pop();
645 if (slot_index >= recording_readbacks.size()) {
648 RecordingReadbackSlot &slot = recording_readbacks[slot_index];
649 void *mapped =
nullptr;
650 if (vkMapMemory(
device, slot.memory, 0, recording_image_bytes, 0, &mapped) == VK_SUCCESS) {
651 const auto *src =
static_cast<const std::uint8_t *
>(mapped);
652 frame_pixels.resize(
static_cast<size_t>(recording_image_bytes));
653 if (recording_format_is_bgra) {
654 for (
size_t i = 0; i < frame_pixels.size(); i += 4U) {
655 frame_pixels[i + 0U] = src[i + 2U];
656 frame_pixels[i + 1U] = src[i + 1U];
657 frame_pixels[i + 2U] = src[i + 0U];
658 frame_pixels[i + 3U] = src[i + 3U];
661 std::memcpy(frame_pixels.data(), src, frame_pixels.size());
663 vkUnmapMemory(
device, slot.memory);
665 std::cerr <<
"fractal_zoom: failed to map recording readback memory\n";
666 frame_pixels.clear();
670 std::lock_guard<std::mutex> lock(recording_mutex);
671 slot.in_flight =
false;
673 slot.image_index = std::numeric_limits<uint32_t>::max();
675 recording_idle_cv.notify_all();
677 if (!frame_pixels.empty()) {
678 video_writer.write(frame_pixels.data());
684 void saveFractalSnapshot() {
685 const char *home = std::getenv(
"HOME");
686 if (home ==
nullptr || std::strlen(home) == 0U) {
687 std::cerr <<
"fractal_zoom: HOME is not set; cannot save snapshot\n";
691 std::filesystem::path snapshot_dir = std::filesystem::path(home) /
"Pictures";
692 std::error_code error;
693 std::filesystem::create_directories(snapshot_dir, error);
695 std::cerr << std::format(
"fractal_zoom: failed to create snapshot directory '{}': {}\n", snapshot_dir.string(), error.message());
699 const std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
700 std::tm local_time{};
702 localtime_s(&local_time, &now);
704 localtime_r(&now, &local_time);
707 const std::string filename = std::format(
"fractal_zoom_snapshot.{:04d}-{:02d}-{:02d}.{:02d}-{:02d}-{:02d}-{:04d}.png", local_time.tm_year + 1900, local_time.tm_mon + 1, local_time.tm_mday, local_time.tm_hour, local_time.tm_min, local_time.tm_sec, snapshot_index++);
709 const std::filesystem::path snapshot_path = snapshot_dir / filename;
712 std::cout <<
"fractal_zoom: saved snapshot: " << snapshot_path.string() <<
"\n";
713 }
catch (
const std::exception &ex) {
714 std::cerr <<
"fractal_zoom: failed to save snapshot: " << ex.what() <<
"\n";
718 void applyWheelZoom(
float wheel_y) {
720 if (extent.width == 0U || extent.height == 0U ||
window ==
nullptr) {
724 float mouse_x = 0.0f;
725 float mouse_y = 0.0f;
726 SDL_GetMouseState(&mouse_x, &mouse_y);
728 const ReferenceScalar base_scale = ReferenceScalar(2) / (zoom * ReferenceScalar(std::min(extent.width, extent.height)));
729 const ReferenceScalar before_x = (
static_cast<ReferenceScalar
>(mouse_x) - ReferenceScalar(extent.width) * ReferenceScalar(
"0.5")) * base_scale + center_x;
730 const ReferenceScalar before_y = (ReferenceScalar(extent.height) * ReferenceScalar(
"0.5") -
static_cast<ReferenceScalar
>(mouse_y)) * base_scale + center_y;
732 const ReferenceScalar zoom_factor = (wheel_y > 0.0f) ? ReferenceScalar(
"1.2") : (ReferenceScalar(1) / ReferenceScalar(
"1.2"));
733 zoom = clampReferenceScalar(zoom * zoom_factor, ReferenceScalar(
"0.5"), MAX_ZOOM);
735 const ReferenceScalar new_scale = ReferenceScalar(2) / (zoom * ReferenceScalar(std::min(extent.width, extent.height)));
736 const ReferenceScalar after_x = (
static_cast<ReferenceScalar
>(mouse_x) - ReferenceScalar(extent.width) * ReferenceScalar(
"0.5")) * new_scale + center_x;
737 const ReferenceScalar after_y = (ReferenceScalar(extent.height) * ReferenceScalar(
"0.5") -
static_cast<ReferenceScalar
>(mouse_y)) * new_scale + center_y;
739 center_x += before_x - after_x;
740 center_y += before_y - after_y;
742 if (wheel_y > 0.0f && zoom > ReferenceScalar(10)) {
743 max_iterations = std::min(max_iterations + 12, max_reference_iterations);
745 reference_orbit_dirty =
true;
748 void updateKeyboardNavigation() {
749 const bool *keys = SDL_GetKeyboardState(
nullptr);
750 if (keys ==
nullptr) {
754 const auto now = std::chrono::steady_clock::now();
755 const ReferenceScalar dt = ReferenceScalar(std::chrono::duration<double>(now - last_tick).count());
758 const ReferenceScalar move_speed = ReferenceScalar(
"0.85") * minReferenceScalar(dt, ReferenceScalar(
"0.1")) / zoom;
760 if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) {
761 center_x -= move_speed;
764 if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) {
765 center_x += move_speed;
768 if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
769 center_y += move_speed;
772 if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
773 center_y -= move_speed;
776 if (keys[SDL_SCANCODE_Z]) {
777 zoom = minReferenceScalar(zoom * (ReferenceScalar(1) + ReferenceScalar(
"1.9") * dt), MAX_ZOOM);
780 if (keys[SDL_SCANCODE_X]) {
781 zoom = maxReferenceScalar(zoom * (ReferenceScalar(1) - ReferenceScalar(
"1.9") * dt), ReferenceScalar(
"0.5"));
785 reference_orbit_dirty =
true;
790 center_x = ReferenceScalar(
"-0.5");
791 center_y = ReferenceScalar(0);
792 zoom = ReferenceScalar(1);
793 max_iterations = 256;
794 reference_orbit_dirty =
true;
797 void destroyFractalResources() {
798 if (
device == VK_NULL_HANDLE) {
799 fractal_pipeline = VK_NULL_HANDLE;
800 fractal_pipeline_layout = VK_NULL_HANDLE;
801 fractal_descriptor_set_layout = VK_NULL_HANDLE;
802 fractal_descriptor_pool = VK_NULL_HANDLE;
803 fractal_descriptor_sets.clear();
804 reference_orbit_buffers.clear();
805 reference_orbit_memories.clear();
806 reference_orbit_mapped.clear();
807 reference_orbit_coherent.clear();
808 reference_orbit_uploaded_generations.clear();
812 if (fractal_pipeline != VK_NULL_HANDLE) {
813 vkDestroyPipeline(
device, fractal_pipeline,
nullptr);
814 fractal_pipeline = VK_NULL_HANDLE;
816 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
817 vkDestroyPipelineLayout(
device, fractal_pipeline_layout,
nullptr);
818 fractal_pipeline_layout = VK_NULL_HANDLE;
820 if (fractal_descriptor_pool != VK_NULL_HANDLE) {
821 vkDestroyDescriptorPool(
device, fractal_descriptor_pool,
nullptr);
822 fractal_descriptor_pool = VK_NULL_HANDLE;
823 fractal_descriptor_sets.clear();
825 if (fractal_descriptor_set_layout != VK_NULL_HANDLE) {
826 vkDestroyDescriptorSetLayout(
device, fractal_descriptor_set_layout,
nullptr);
827 fractal_descriptor_set_layout = VK_NULL_HANDLE;
829 for (
size_t i = 0; i < reference_orbit_memories.size(); ++i) {
830 if (reference_orbit_memories[i] != VK_NULL_HANDLE) {
831 if (i < reference_orbit_mapped.size() && reference_orbit_mapped[i] !=
nullptr) {
832 vkUnmapMemory(
device, reference_orbit_memories[i]);
834 vkFreeMemory(
device, reference_orbit_memories[i],
nullptr);
837 for (VkBuffer buffer : reference_orbit_buffers) {
838 if (buffer != VK_NULL_HANDLE) {
839 vkDestroyBuffer(
device, buffer,
nullptr);
842 reference_orbit_buffers.clear();
843 reference_orbit_memories.clear();
844 reference_orbit_mapped.clear();
845 reference_orbit_coherent.clear();
846 reference_orbit_uploaded_generations.clear();
849 void destroyFractalPipeline() {
850 if (
device == VK_NULL_HANDLE) {
851 fractal_pipeline = VK_NULL_HANDLE;
852 fractal_pipeline_layout = VK_NULL_HANDLE;
855 if (fractal_pipeline != VK_NULL_HANDLE) {
856 vkDestroyPipeline(
device, fractal_pipeline,
nullptr);
857 fractal_pipeline = VK_NULL_HANDLE;
859 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
860 vkDestroyPipelineLayout(
device, fractal_pipeline_layout,
nullptr);
861 fractal_pipeline_layout = VK_NULL_HANDLE;
865 void createFractalPipeline() {
866 if (
device == VK_NULL_HANDLE) {
872 if (color_format == VK_FORMAT_UNDEFINED || depth_attachment_format == VK_FORMAT_UNDEFINED) {
876 ensureFractalResources();
877 if (fractal_descriptor_set_layout == VK_NULL_HANDLE || fractal_descriptor_sets.empty()) {
881 destroyFractalPipeline();
883 const std::string vert_path = shaderRoot +
"/fractal.vert.spv";
884 const std::string frag_path =
885#if defined(MXVK_USE_MOLTENVK)
886 shaderRoot +
"/fractal_float.frag.spv";
888 shaderRoot +
"/fractal.frag.spv";
890 const std::vector<char> vert_bytes =
loadSpv(vert_path);
891 const std::vector<char> frag_bytes =
loadSpv(frag_path);
894 VkShaderModule frag_module = VK_NULL_HANDLE;
899 VkPipelineShaderStageCreateInfo vert_stage{};
900 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
901 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
902 vert_stage.module = vert_module;
903 vert_stage.pName =
"main";
905 VkPipelineShaderStageCreateInfo frag_stage{};
906 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
907 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
908 frag_stage.module = frag_module;
909 frag_stage.pName =
"main";
911 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
913 VkPipelineVertexInputStateCreateInfo vertex_input{};
914 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
916 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
917 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
918 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
919 input_assembly.primitiveRestartEnable = VK_FALSE;
921 VkPipelineViewportStateCreateInfo viewport_state{};
922 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
923 viewport_state.viewportCount = 1;
924 viewport_state.scissorCount = 1;
926 const VkDynamicState dynamic_states[] = {
927 VK_DYNAMIC_STATE_VIEWPORT,
928 VK_DYNAMIC_STATE_SCISSOR,
930 VkPipelineDynamicStateCreateInfo dynamic_state{};
931 dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
932 dynamic_state.dynamicStateCount = 2;
933 dynamic_state.pDynamicStates = dynamic_states;
935 VkPipelineRasterizationStateCreateInfo rasterizer{};
936 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
937 rasterizer.depthClampEnable = VK_FALSE;
938 rasterizer.rasterizerDiscardEnable = VK_FALSE;
939 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
940 rasterizer.lineWidth = 1.0f;
941 rasterizer.cullMode = VK_CULL_MODE_NONE;
942 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
944 VkPipelineMultisampleStateCreateInfo multisampling{};
945 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
946 multisampling.sampleShadingEnable = VK_FALSE;
947 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
949 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
950 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
951 depth_stencil.depthTestEnable = VK_FALSE;
952 depth_stencil.depthWriteEnable = VK_FALSE;
953 depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
954 depth_stencil.depthBoundsTestEnable = VK_FALSE;
955 depth_stencil.stencilTestEnable = VK_FALSE;
957 VkPipelineColorBlendAttachmentState color_blend_attachment{};
958 color_blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
959 color_blend_attachment.blendEnable = VK_FALSE;
961 VkPipelineColorBlendStateCreateInfo color_blending{};
962 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
963 color_blending.logicOpEnable = VK_FALSE;
964 color_blending.attachmentCount = 1;
965 color_blending.pAttachments = &color_blend_attachment;
967 VkPushConstantRange push_constant_range{};
968 push_constant_range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
969 push_constant_range.offset = 0;
970 push_constant_range.size =
static_cast<uint32_t
>(
sizeof(FractalPushConstants));
972 VkPipelineLayoutCreateInfo pipeline_layout_info{};
973 pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
974 pipeline_layout_info.setLayoutCount = 1;
975 pipeline_layout_info.pSetLayouts = &fractal_descriptor_set_layout;
976 pipeline_layout_info.pushConstantRangeCount = 1;
977 pipeline_layout_info.pPushConstantRanges = &push_constant_range;
979 if (vkCreatePipelineLayout(
device, &pipeline_layout_info,
nullptr, &fractal_pipeline_layout) != VK_SUCCESS) {
980 throw mxvk::Exception(
"Failed to create fractal pipeline layout");
983 VkPipelineRenderingCreateInfo pipeline_rendering_info{};
984 pipeline_rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
985 pipeline_rendering_info.viewMask = 0;
986 pipeline_rendering_info.colorAttachmentCount = 1;
987 pipeline_rendering_info.pColorAttachmentFormats = &color_format;
988 pipeline_rendering_info.depthAttachmentFormat = depth_attachment_format;
989 pipeline_rendering_info.stencilAttachmentFormat = VK_FORMAT_UNDEFINED;
991 VkGraphicsPipelineCreateInfo pipeline_info{};
992 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
993 pipeline_info.pNext = &pipeline_rendering_info;
994 pipeline_info.stageCount = 2;
995 pipeline_info.pStages = shader_stages;
996 pipeline_info.pVertexInputState = &vertex_input;
997 pipeline_info.pInputAssemblyState = &input_assembly;
998 pipeline_info.pViewportState = &viewport_state;
999 pipeline_info.pRasterizationState = &rasterizer;
1000 pipeline_info.pMultisampleState = &multisampling;
1001 pipeline_info.pDepthStencilState = &depth_stencil;
1002 pipeline_info.pColorBlendState = &color_blending;
1003 pipeline_info.pDynamicState = &dynamic_state;
1004 pipeline_info.layout = fractal_pipeline_layout;
1005 pipeline_info.renderPass = VK_NULL_HANDLE;
1006 pipeline_info.subpass = 0;
1008 if (vkCreateGraphicsPipelines(
device, VK_NULL_HANDLE, 1, &pipeline_info,
nullptr, &fractal_pipeline) != VK_SUCCESS) {
1009 throw mxvk::Exception(
"Failed to create fractal graphics pipeline");
1012 if (fractal_pipeline != VK_NULL_HANDLE) {
1013 vkDestroyPipeline(
device, fractal_pipeline,
nullptr);
1014 fractal_pipeline = VK_NULL_HANDLE;
1016 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
1017 vkDestroyPipelineLayout(
device, fractal_pipeline_layout,
nullptr);
1018 fractal_pipeline_layout = VK_NULL_HANDLE;
1020 if (frag_module != VK_NULL_HANDLE) {
1021 vkDestroyShaderModule(
device, frag_module,
nullptr);
1023 vkDestroyShaderModule(
device, vert_module,
nullptr);
1027 vkDestroyShaderModule(
device, frag_module,
nullptr);
1028 vkDestroyShaderModule(
device, vert_module,
nullptr);
1031 void ensureFractalResources() {
1033 if (reference_orbit_buffers.size() != required_count || fractal_descriptor_sets.size() != required_count) {
1034 destroyFractalResources();
1037 if (reference_orbit_buffers.empty()) {
1038 createReferenceOrbitBuffers(required_count);
1040 if (fractal_descriptor_set_layout == VK_NULL_HANDLE) {
1041 createDescriptorSetLayout();
1043 if (fractal_descriptor_pool == VK_NULL_HANDLE) {
1044 createDescriptorPool();
1046 if (fractal_descriptor_sets.empty()) {
1047 allocateDescriptorSets(required_count);
1048 writeDescriptorSets();
1052 void createReferenceOrbitBuffers(
size_t buffer_count) {
1053 const VkDeviceSize
buffer_size =
static_cast<VkDeviceSize
>(reference_orbit_capacity *
sizeof(OrbitSample));
1055 auto cleanup_reference_orbit_buffers = [&]() {
1056 for (
size_t i = 0; i < reference_orbit_memories.size(); ++i) {
1057 if (reference_orbit_memories[i] != VK_NULL_HANDLE) {
1058 if (i < reference_orbit_mapped.size() && reference_orbit_mapped[i] !=
nullptr) {
1059 vkUnmapMemory(
device, reference_orbit_memories[i]);
1061 vkFreeMemory(
device, reference_orbit_memories[i],
nullptr);
1064 for (VkBuffer buffer : reference_orbit_buffers) {
1065 if (buffer != VK_NULL_HANDLE) {
1066 vkDestroyBuffer(
device, buffer,
nullptr);
1069 reference_orbit_buffers.clear();
1070 reference_orbit_memories.clear();
1071 reference_orbit_mapped.clear();
1072 reference_orbit_coherent.clear();
1073 reference_orbit_uploaded_generations.clear();
1076 cleanup_reference_orbit_buffers();
1077 reference_orbit_buffers.assign(buffer_count, VK_NULL_HANDLE);
1078 reference_orbit_memories.assign(buffer_count, VK_NULL_HANDLE);
1079 reference_orbit_mapped.assign(buffer_count,
nullptr);
1080 reference_orbit_coherent.assign(buffer_count,
false);
1081 reference_orbit_uploaded_generations.assign(buffer_count, 0);
1084 for (
size_t i = 0; i < buffer_count; ++i) {
1086 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]);
1087 reference_orbit_coherent[i] =
true;
1089 createBuffer(buffer_size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, reference_orbit_buffers[i], reference_orbit_memories[i]);
1090 reference_orbit_coherent[i] =
false;
1093 if (vkMapMemory(
device, reference_orbit_memories[i], 0, buffer_size, 0, &reference_orbit_mapped[i]) != VK_SUCCESS) {
1094 throw mxvk::Exception(
"Failed to map fractal reference orbit buffer");
1098 cleanup_reference_orbit_buffers();
1103 void createDescriptorSetLayout() {
1104 VkDescriptorSetLayoutBinding orbit_binding{};
1105 orbit_binding.binding = 0;
1106 orbit_binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
1107 orbit_binding.descriptorCount = 1;
1108 orbit_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
1110 VkDescriptorSetLayoutCreateInfo layout_info{};
1111 layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
1112 layout_info.bindingCount = 1;
1113 layout_info.pBindings = &orbit_binding;
1115 if (vkCreateDescriptorSetLayout(
device, &layout_info,
nullptr, &fractal_descriptor_set_layout) != VK_SUCCESS) {
1116 throw mxvk::Exception(
"Failed to create fractal descriptor set layout");
1120 void createDescriptorPool() {
1121 VkDescriptorPoolSize pool_size{};
1122 pool_size.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
1123 pool_size.descriptorCount =
static_cast<uint32_t
>(reference_orbit_buffers.size());
1125 VkDescriptorPoolCreateInfo pool_info{};
1126 pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
1127 pool_info.maxSets =
static_cast<uint32_t
>(reference_orbit_buffers.size());
1128 pool_info.poolSizeCount = 1;
1129 pool_info.pPoolSizes = &pool_size;
1131 if (vkCreateDescriptorPool(
device, &pool_info,
nullptr, &fractal_descriptor_pool) != VK_SUCCESS) {
1132 throw mxvk::Exception(
"Failed to create fractal descriptor pool");
1136 void allocateDescriptorSets(
size_t descriptor_count) {
1137 std::vector<VkDescriptorSetLayout> layouts(descriptor_count, fractal_descriptor_set_layout);
1138 fractal_descriptor_sets.assign(descriptor_count, VK_NULL_HANDLE);
1140 VkDescriptorSetAllocateInfo alloc_info{};
1141 alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1142 alloc_info.descriptorPool = fractal_descriptor_pool;
1143 alloc_info.descriptorSetCount =
static_cast<uint32_t
>(descriptor_count);
1144 alloc_info.pSetLayouts = layouts.data();
1146 if (vkAllocateDescriptorSets(
device, &alloc_info, fractal_descriptor_sets.data()) != VK_SUCCESS) {
1147 throw mxvk::Exception(
"Failed to allocate fractal descriptor sets");
1151 void writeDescriptorSets() {
1152 std::vector<VkDescriptorBufferInfo> buffer_infos(fractal_descriptor_sets.size());
1153 std::vector<VkWriteDescriptorSet> writes(fractal_descriptor_sets.size());
1155 for (
size_t i = 0; i < fractal_descriptor_sets.size(); ++i) {
1156 buffer_infos[i].buffer = reference_orbit_buffers[i];
1157 buffer_infos[i].offset = 0;
1158 buffer_infos[i].range =
static_cast<VkDeviceSize
>(reference_orbit_capacity *
sizeof(OrbitSample));
1160 writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1161 writes[i].dstSet = fractal_descriptor_sets[i];
1162 writes[i].dstBinding = 0;
1163 writes[i].dstArrayElement = 0;
1164 writes[i].descriptorCount = 1;
1165 writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
1166 writes[i].pBufferInfo = &buffer_infos[i];
1169 vkUpdateDescriptorSets(
device,
static_cast<uint32_t
>(writes.size()), writes.data(), 0,
nullptr);
1172 void updateReferenceOrbit(uint32_t image_index, VkExtent2D extent) {
1173 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] ==
nullptr) {
1177 const bool needs_deep_references = zoom >= direct_reference_zoom_threshold;
1178 if (!needs_deep_references) {
1179 if (reference_orbit_dirty || cached_reference_count != 0) {
1180 std::fill(reference_orbit_samples.begin(), reference_orbit_samples.end(), OrbitSample{});
1181 reference_orbit_samples[0] = {0.0f,
static_cast<float>(reference_metadata_capacity),
static_cast<float>(reference_orbit_stride), 0.0f};
1182 cached_reference_count = 0;
1183 reference_orbit_dirty =
false;
1184 ++reference_orbit_generation;
1187 uploadReferenceOrbit(image_index);
1191 const auto now = std::chrono::steady_clock::now();
1192 const bool throttle_rebuild = reference_orbit_generation != 0 && (now - last_reference_rebuild_time) < reference_rebuild_interval;
1193 if (!reference_orbit_dirty || throttle_rebuild) {
1194 uploadReferenceOrbit(image_index);
1198 const int iteration_count = std::clamp(max_iterations, 1, max_reference_iterations);
1199 const ReferenceScalar min_dimension = ReferenceScalar(std::max<uint32_t>(std::min(extent.width, extent.height), 1U));
1200 const ReferenceScalar width = ReferenceScalar(std::max<uint32_t>(extent.width, 1U));
1201 const ReferenceScalar height = ReferenceScalar(std::max<uint32_t>(extent.height, 1U));
1202 const ReferenceScalar half_width_uv = width / (ReferenceScalar(2) * min_dimension);
1203 const ReferenceScalar half_height_uv = height / (ReferenceScalar(2) * min_dimension);
1205 std::fill(reference_orbit_samples.begin(), reference_orbit_samples.end(), OrbitSample{});
1206 std::vector<ReferenceTile> tiles;
1207 tiles.reserve(max_adaptive_references);
1209 for (
int root_y = 0; root_y < adaptive_root_rows; ++root_y) {
1210 for (
int root_x = 0; root_x < adaptive_root_cols; ++root_x) {
1211 const ReferenceScalar tile_min_x = -half_width_uv + ReferenceScalar(root_x) * (ReferenceScalar(2) * half_width_uv) / ReferenceScalar(adaptive_root_cols);
1212 const ReferenceScalar tile_max_x = -half_width_uv + ReferenceScalar(root_x + 1) * (ReferenceScalar(2) * half_width_uv) / ReferenceScalar(adaptive_root_cols);
1213 const ReferenceScalar tile_min_y = -half_height_uv + ReferenceScalar(root_y) * (ReferenceScalar(2) * half_height_uv) / ReferenceScalar(adaptive_root_rows);
1214 const ReferenceScalar tile_max_y = -half_height_uv + ReferenceScalar(root_y + 1) * (ReferenceScalar(2) * half_height_uv) / ReferenceScalar(adaptive_root_rows);
1216 tiles.push_back({tile_min_x, tile_min_y, tile_max_x, tile_max_y, 0});
1220 bool refined =
true;
1221 const int validation_iteration_count = std::min(iteration_count, max_validation_iterations);
1222 while (refined && tiles.size() + 3 <=
static_cast<size_t>(max_adaptive_references)) {
1225 for (
size_t tile_index = 0; tile_index < tiles.size(); ++tile_index) {
1226 const ReferenceTile tile = tiles[tile_index];
1227 if (tile.depth >= max_adaptive_depth || isAdaptiveReferenceTileStable(tile, validation_iteration_count)) {
1231 const ReferenceScalar mid_x = (tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5");
1232 const ReferenceScalar mid_y = (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5");
1233 const int child_depth = tile.depth + 1;
1234 const std::array<ReferenceTile, 4> children{ReferenceTile{tile.min_uv_x, tile.min_uv_y, mid_x, mid_y, child_depth}, ReferenceTile{mid_x, tile.min_uv_y, tile.max_uv_x, mid_y, child_depth}, ReferenceTile{tile.min_uv_x, mid_y, mid_x, tile.max_uv_y, child_depth}, ReferenceTile{mid_x, mid_y, tile.max_uv_x, tile.max_uv_y, child_depth}};
1236 tiles.erase(tiles.begin() +
static_cast<std::ptrdiff_t
>(tile_index));
1237 tiles.insert(tiles.begin() +
static_cast<std::ptrdiff_t
>(tile_index), children.begin(), children.end());
1243 const int reference_count =
static_cast<int>(std::min<size_t>(tiles.size(), max_adaptive_references));
1244 for (
int reference_index = 0; reference_index < reference_count; ++reference_index) {
1245 const ReferenceTile &tile = tiles[
static_cast<size_t>(reference_index)];
1246 const size_t orbit_base = referenceOrbitBase(reference_index);
1247 const ReferenceScalar ref_uv_x = (tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5");
1248 const ReferenceScalar ref_uv_y = (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5");
1249 const int sample_count = writeReferenceOrbit(orbit_base, ref_uv_x, ref_uv_y, iteration_count);
1250 writeReferenceMetadata(reference_index, tile, orbit_base, sample_count, ref_uv_x, ref_uv_y);
1253 reference_orbit_samples[0] = {
static_cast<float>(reference_count),
static_cast<float>(reference_metadata_capacity),
static_cast<float>(reference_orbit_stride), 0.0f};
1255 cached_reference_count = reference_count;
1256 reference_orbit_dirty =
false;
1257 last_reference_rebuild_time = now;
1258 ++reference_orbit_generation;
1259 orbit_length = iteration_count + 1;
1260 uploadReferenceOrbit(image_index);
1263 void uploadReferenceOrbit(uint32_t image_index) {
1264 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] ==
nullptr) {
1267 if (image_index < reference_orbit_uploaded_generations.size() && reference_orbit_uploaded_generations[image_index] == reference_orbit_generation) {
1271 const VkDeviceSize upload_size =
static_cast<VkDeviceSize
>(reference_orbit_samples.size() *
sizeof(OrbitSample));
1272 std::memcpy(reference_orbit_mapped[image_index], reference_orbit_samples.data(),
static_cast<size_t>(upload_size));
1274 if (!reference_orbit_coherent[image_index]) {
1275 VkMappedMemoryRange range{};
1276 range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
1277 range.memory = reference_orbit_memories[image_index];
1279 range.size = upload_size;
1280 vkFlushMappedMemoryRanges(
device, 1, &range);
1283 if (image_index < reference_orbit_uploaded_generations.size()) {
1284 reference_orbit_uploaded_generations[image_index] = reference_orbit_generation;
1288 bool isAdaptiveReferenceTileStable(
const ReferenceTile &tile,
int iteration_count) {
1289 const size_t orbit_base = referenceOrbitBase(max_adaptive_references);
1290 const ReferenceScalar ref_uv_x = (tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5");
1291 const ReferenceScalar ref_uv_y = (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5");
1292 const int sample_count = writeReferenceOrbit(orbit_base, ref_uv_x, ref_uv_y, iteration_count);
1293 return isReferenceTileStable(tile, orbit_base, sample_count, ref_uv_x, ref_uv_y, iteration_count);
1296 int writeReferenceOrbit(
size_t orbit_base,
const ReferenceScalar &ref_uv_x,
const ReferenceScalar &ref_uv_y,
int iteration_count) {
1297 const ReferenceScalar c_x = center_x + ref_uv_x / zoom;
1298 const ReferenceScalar c_y = center_y + ref_uv_y / zoom;
1300 ReferenceScalar z_x = 0;
1301 ReferenceScalar z_y = 0;
1302 int sample_count = 1;
1304 reference_orbit_samples[orbit_base] = {toOrbitSampleScalar(z_x), toOrbitSampleScalar(z_y), 1.0f, 0.0f};
1306 for (
int i = 0; i < iteration_count && sample_count < reference_orbit_stride; ++i) {
1307 const ReferenceScalar next_x = z_x * z_x - z_y * z_y + c_x;
1308 const ReferenceScalar next_y = ReferenceScalar(2) * z_x * z_y + c_y;
1313 reference_orbit_samples[orbit_base +
static_cast<size_t>(sample_count)] = {toOrbitSampleScalar(z_x), toOrbitSampleScalar(z_y), 0.0f, 0.0f};
1316 const ReferenceScalar mag2 = z_x * z_x + z_y * z_y;
1317 if (mag2 > ReferenceScalar(4)) {
1322 reference_orbit_samples[orbit_base].z =
static_cast<float>(sample_count);
1323 return sample_count;
1326 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) {
1327 const size_t metadata_base = 1 +
static_cast<size_t>(reference_index) * 2;
1328 reference_orbit_samples[metadata_base] = {toOrbitSampleScalar(tile.min_uv_x), toOrbitSampleScalar(tile.min_uv_y), toOrbitSampleScalar(tile.max_uv_x), toOrbitSampleScalar(tile.max_uv_y)};
1329 reference_orbit_samples[metadata_base + 1] = {toOrbitSampleScalar(ref_uv_x), toOrbitSampleScalar(ref_uv_y),
static_cast<float>(orbit_base),
static_cast<float>(sample_count)};
1332 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 {
1333 const std::array<std::pair<ReferenceScalar, ReferenceScalar>, 5> sample_points{std::pair{tile.min_uv_x, tile.min_uv_y}, std::pair{tile.max_uv_x, tile.min_uv_y}, std::pair{tile.min_uv_x, tile.max_uv_y}, std::pair{tile.max_uv_x, tile.max_uv_y}, std::pair{(tile.min_uv_x + tile.max_uv_x) * ReferenceScalar(
"0.5"), (tile.min_uv_y + tile.max_uv_y) * ReferenceScalar(
"0.5")}};
1335 for (
const auto &[sample_uv_x, sample_uv_y] : sample_points) {
1336 const float delta_c_x = ((sample_uv_x - ref_uv_x) / zoom).convert_to<float>();
1337 const float delta_c_y = ((sample_uv_y - ref_uv_y) / zoom).convert_to<float>();
1338 if (!isPerturbationSampleStable(orbit_base, sample_count, iteration_count, delta_c_x, delta_c_y)) {
1346 bool isPerturbationSampleStable(
size_t orbit_base,
int sample_count,
int iteration_count,
float delta_c_x,
float delta_c_y)
const {
1349 const int count = std::min(iteration_count, sample_count - 1);
1351 for (
int i = 0; i < count; ++i) {
1352 const OrbitSample &ref = reference_orbit_samples[orbit_base +
static_cast<size_t>(i)];
1353 const float z_dz_x = ref.x * dz_x - ref.y * dz_y;
1354 const float z_dz_y = ref.x * dz_y + ref.y * dz_x;
1355 const float dz_sq_x = dz_x * dz_x - dz_y * dz_y;
1356 const float dz_sq_y = 2.0f * dz_x * dz_y;
1358 dz_x = 2.0f * z_dz_x + dz_sq_x + delta_c_x;
1359 dz_y = 2.0f * z_dz_y + dz_sq_y + delta_c_y;
1361 if (!std::isfinite(dz_x) || !std::isfinite(dz_y)) {
1365 const OrbitSample &next_ref = reference_orbit_samples[orbit_base +
static_cast<size_t>(i + 1)];
1366 const float true_x = next_ref.x + dz_x;
1367 const float true_y = next_ref.y + dz_y;
1368 const float true_mag2 = true_x * true_x + true_y * true_y;
1369 if (!std::isfinite(true_mag2) || true_mag2 > 4.0f) {
1373 const float dz_mag2 = dz_x * dz_x + dz_y * dz_y;
1374 const float ref_mag2 = ref.x * ref.x + ref.y * ref.y;
1375 if (!std::isfinite(dz_mag2) || dz_mag2 > perturbation_breakdown_limit2 || (ref_mag2 > 0.0f && dz_mag2 > ref_mag2 * 0.25f)) {
1380 return sample_count > iteration_count;
1383 void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
1384 VkBufferCreateInfo buffer_info{};
1385 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1386 buffer_info.size = size;
1387 buffer_info.usage = usage;
1388 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1390 if (vkCreateBuffer(
device, &buffer_info,
nullptr, &buffer) != VK_SUCCESS) {
1391 throw mxvk::Exception(
"Failed to create fractal buffer");
1394 VkMemoryRequirements mem_requirements{};
1395 vkGetBufferMemoryRequirements(
device, buffer, &mem_requirements);
1397 VkMemoryAllocateInfo alloc_info{};
1398 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1399 alloc_info.allocationSize = mem_requirements.size;
1401 alloc_info.memoryTypeIndex = findMemoryType(mem_requirements.memoryTypeBits, properties);
1403 vkDestroyBuffer(
device, buffer,
nullptr);
1404 buffer = VK_NULL_HANDLE;
1408 if (vkAllocateMemory(
device, &alloc_info,
nullptr, &bufferMemory) != VK_SUCCESS) {
1409 vkDestroyBuffer(
device, buffer,
nullptr);
1410 buffer = VK_NULL_HANDLE;
1411 throw mxvk::Exception(
"Failed to allocate fractal buffer memory");
1414 if (vkBindBufferMemory(
device, buffer, bufferMemory, 0) != VK_SUCCESS) {
1415 vkFreeMemory(
device, bufferMemory,
nullptr);
1416 bufferMemory = VK_NULL_HANDLE;
1417 vkDestroyBuffer(
device, buffer,
nullptr);
1418 buffer = VK_NULL_HANDLE;
1419 throw mxvk::Exception(
"Failed to bind fractal buffer memory");
1423 uint32_t findMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties)
const {
1424 VkPhysicalDeviceMemoryProperties mem_properties{};
1425 vkGetPhysicalDeviceMemoryProperties(
physical_device, &mem_properties);
1427 for (uint32_t i = 0; i < mem_properties.memoryTypeCount; ++i) {
1428 const bool type_matches = (type_filter & (1U << i)) != 0U;
1429 const bool property_matches = (mem_properties.memoryTypes[i].propertyFlags & properties) == properties;
1430 if (type_matches && property_matches) {
1435 throw mxvk::Exception(
"Failed to find suitable memory type for fractal buffer");
1439#if defined(MXVK_USE_MOLTENVK)
1444 struct FractalPushConstants {
1445 PushScalar center_x;
1446 PushScalar center_y;
1447 PushScalar inverse_zoom;
1449 PushScalar resolution_x;
1450 PushScalar resolution_y;
1457 VkPipeline fractal_pipeline = VK_NULL_HANDLE;
1458 VkPipelineLayout fractal_pipeline_layout = VK_NULL_HANDLE;
1459 VkDescriptorSetLayout fractal_descriptor_set_layout = VK_NULL_HANDLE;
1460 VkDescriptorPool fractal_descriptor_pool = VK_NULL_HANDLE;
1461 std::vector<VkDescriptorSet> fractal_descriptor_sets{};
1462 std::vector<VkBuffer> reference_orbit_buffers{};
1463 std::vector<VkDeviceMemory> reference_orbit_memories{};
1464 std::vector<void *> reference_orbit_mapped{};
1465 std::vector<bool> reference_orbit_coherent{};
1466 std::vector<uint64_t> reference_orbit_uploaded_generations{};
1468 static constexpr int max_reference_iterations = 4096;
1469 static constexpr int adaptive_root_cols = 3;
1470 static constexpr int adaptive_root_rows = 2;
1471 static constexpr int max_adaptive_references = 32;
1472 static constexpr int max_adaptive_depth = 4;
1473 static constexpr int max_validation_iterations = 768;
1474 static constexpr int reference_metadata_capacity = 1 + max_adaptive_references * 2;
1475 static constexpr int reference_orbit_stride = max_reference_iterations + 1;
1476 static constexpr int reference_orbit_capacity = reference_metadata_capacity + (max_adaptive_references + 1) * reference_orbit_stride;
1477 static constexpr float perturbation_breakdown_limit2 = 0.0625f;
1478 static constexpr std::chrono::milliseconds reference_rebuild_interval{100};
1479#if defined(MXVK_USE_MOLTENVK)
1480 static inline const ReferenceScalar direct_reference_zoom_threshold = ReferenceScalar(4096);
1482 static inline const ReferenceScalar direct_reference_zoom_threshold = ReferenceScalar(
"1e15");
1484 static inline const ReferenceScalar MAX_ZOOM = ReferenceScalar(
"1e1000");
1485 std::vector<OrbitSample> reference_orbit_samples{};
1486 int orbit_length = 0;
1487 int cached_reference_count = -1;
1488 bool reference_orbit_dirty =
true;
1489 uint64_t reference_orbit_generation = 1;
1490 std::chrono::steady_clock::time_point last_reference_rebuild_time{};
1492 static constexpr size_t referenceOrbitBase(
int reference_index) {
return static_cast<size_t>(reference_metadata_capacity) +
static_cast<size_t>(reference_index) *
static_cast<size_t>(reference_orbit_stride); }
1494 ReferenceScalar center_x = ReferenceScalar(
"-0.5");
1495 ReferenceScalar center_y = ReferenceScalar(0);
1496 ReferenceScalar zoom = ReferenceScalar(1);
1497 int max_iterations = 256;
1498 int palette_index = 0;
1500 bool dragging =
false;
1501 int drag_start_mouse_x = 0;
1502 int drag_start_mouse_y = 0;
1503 ReferenceScalar drag_start_center_x = ReferenceScalar(0);
1504 ReferenceScalar drag_start_center_y = ReferenceScalar(0);
1506 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
1507 std::chrono::steady_clock::time_point last_tick{std::chrono::steady_clock::now()};
1508 std::string shaderRoot;
1509 uint32_t snapshot_index = 0;
1510#if defined(MXWRITE_ENABLED)
1511 Writer video_writer{};
1512 uint32_t video_record_width = 0;
1513 uint32_t video_record_height = 0;
1514 std::string video_output_path =
"output.mp4";
1515 static constexpr uint32_t recording_readback_slot_count = 4;
1516 std::vector<RecordingReadbackSlot> recording_readbacks{};
1517 std::vector<bool> recording_pending_images{};
1518 VkDeviceSize recording_row_bytes = 0;
1519 VkDeviceSize recording_image_bytes = 0;
1520 std::chrono::steady_clock::time_point next_recording_frame_time{};
1521 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));
1522 bool recording_readbacks_ready =
false;
1523 bool recording_format_is_bgra =
false;
1524 uint64_t recording_dropped_frames = 0;
1525 std::jthread recording_worker{};
1526 std::mutex recording_mutex{};
1527 std::condition_variable recording_cv{};
1528 std::condition_variable recording_idle_cv{};
1529 std::queue<size_t> recording_ready_slots{};
1530 bool recording_worker_stop =
false;