MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
fractal.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
4#if defined(MXWRITE_ENABLED)
5#include "mxwrite.hpp"
6#endif
7
8#include <SDL3/SDL.h>
9
10#include <boost/multiprecision/cpp_dec_float.hpp>
11
12#include <algorithm>
13#include <array>
14#include <chrono>
15#include <cmath>
16#include <condition_variable>
17#include <cstdint>
18#include <cstdlib>
19#include <cstring>
20#include <ctime>
21#include <filesystem>
22#include <format>
23#include <iostream>
24#include <mutex>
25#include <queue>
26#include <string>
27#include <system_error>
28#include <thread>
29#include <utility>
30#include <vector>
31
32#ifndef fractal_zoom_ASSET_DIR
33#define fractal_zoom_ASSET_DIR "."
34#endif
35
36namespace example {
37
39 using ReferenceScalar = boost::multiprecision::cpp_dec_float_100;
40
41 struct OrbitSample {
42 float x;
43 float y;
44 float z;
45 float w;
46 };
47
48 struct ReferenceTile {
49 ReferenceScalar min_uv_x;
50 ReferenceScalar min_uv_y;
51 ReferenceScalar max_uv_x;
52 ReferenceScalar max_uv_y;
53 int depth;
54 };
55
56 public:
57 FractalWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
58 : mxvk::VK_Window("-[ Fractal Zoom - MXVK ]-", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
59 reference_orbit_samples(static_cast<size_t>(reference_orbit_capacity)),
60 shaderRoot(((path.empty() || path == ".") ? std::string(fractal_zoom_ASSET_DIR) : path) + "/data") {
61 }
62
63 ~FractalWindow() override {
64#if defined(MXWRITE_ENABLED)
65 closeVideoWriter();
66#endif
67 if (device != VK_NULL_HANDLE) {
68 vkDeviceWaitIdle(device);
69 }
70 destroyFractalResources();
71 }
72
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();
77 return;
78 }
79 if ((e.key.key == SDLK_P || e.key.scancode == SDL_SCANCODE_P) && !e.key.repeat) {
80#if defined(MXWRITE_ENABLED)
81 toggleVideoRecording();
82#else
83 std::cout << "fractal_zoom: MXWrite is unavailable; video recording disabled\n";
84#endif
85 return;
86 }
87 handleKey(e.key.key);
88 return;
89 }
90
91 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
92 dragging = true;
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;
97 return;
98 }
99
100 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
101 dragging = false;
102 return;
103 }
104
105 if (e.type == SDL_EVENT_MOUSE_MOTION && dragging) {
106 const VkExtent2D extent = getSwapchainExtent();
107 if (extent.width == 0U || extent.height == 0U) {
108 return;
109 }
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;
116 return;
117 }
118
119 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
120 applyWheelZoom(e.wheel.y);
121 }
122 }
123
124 void proc() override {
125 updateKeyboardNavigation();
126 }
127
128 void render() override {
129#if defined(MXWRITE_ENABLED)
130 serviceRecordingReadbacks();
131#endif
133#if defined(MXWRITE_ENABLED)
134 recordPresentedFrame();
135#endif
136 }
137
139#if defined(MXWRITE_ENABLED)
140 if (video_writer.is_open()) {
141 std::cerr << "fractal_zoom: swapchain is changing; closing current video recording\n";
142 closeVideoWriter();
143 }
144#endif
145 destroyFractalResources();
146 }
147
148 void onSwapchainRecreated() override {
149 createFractalPipeline();
150 }
151
152 void onRecordCustomRendering(VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index) override {
153 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE) {
154 createFractalPipeline();
155 }
156
157 if (image_index >= fractal_descriptor_sets.size()) {
158 return;
159 }
160
161 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE || fractal_descriptor_sets[image_index] == VK_NULL_HANDLE) {
162 return;
163 }
164
165 const VkExtent2D extent = getSwapchainExtent();
166 if (extent.width == 0U || extent.height == 0U) {
167 return;
168 }
169
170 updateReferenceOrbit(image_index, extent);
171
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);
174
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),
182 max_iterations,
183 palette_index,
184 orbit_length,
185 0};
186
187 vkCmdPushConstants(
188 cmd,
189 fractal_pipeline_layout,
190 VK_SHADER_STAGE_FRAGMENT_BIT,
191 0,
192 sizeof(push_constants),
193 &push_constants);
194
195 vkCmdDraw(cmd, 3, 1, 0, 0);
196 }
197
198 private:
199 static ReferenceScalar minReferenceScalar(const ReferenceScalar &a, const ReferenceScalar &b) {
200 return (a < b) ? a : b;
201 }
202
203 static ReferenceScalar maxReferenceScalar(const ReferenceScalar &a, const ReferenceScalar &b) {
204 return (a > b) ? a : b;
205 }
206
207 static ReferenceScalar clampReferenceScalar(const ReferenceScalar &value, const ReferenceScalar &minimum, const ReferenceScalar &maximum) {
208 return maxReferenceScalar(minReferenceScalar(value, maximum), minimum);
209 }
210
211 static float toOrbitSampleScalar(const ReferenceScalar &value) {
212 return value.convert_to<float>();
213 }
214
215 void handleKey(SDL_Keycode key) {
216 switch (key) {
217 case SDLK_ESCAPE:
218 exit();
219 break;
220 case SDLK_R:
221 resetView();
222 break;
223 case SDLK_1:
224 center_x = ReferenceScalar("-0.5");
225 center_y = ReferenceScalar(0);
226 zoom = ReferenceScalar(1);
227 max_iterations = 256;
228 reference_orbit_dirty = true;
229 break;
230 case SDLK_2:
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;
236 break;
237 case SDLK_3:
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;
243 break;
244 case SDLK_EQUALS:
245 case SDLK_PLUS:
246 max_iterations = std::min(max_iterations + 64, max_reference_iterations);
247 reference_orbit_dirty = true;
248 break;
249 case SDLK_MINUS:
250 max_iterations = std::max(max_iterations - 64, 64);
251 reference_orbit_dirty = true;
252 break;
253 case SDLK_LEFTBRACKET:
254 palette_index = (palette_index + 2) % 3;
255 break;
256 case SDLK_RIGHTBRACKET:
257 palette_index = (palette_index + 1) % 3;
258 break;
259 default:
260 break;
261 }
262 }
263
264#if defined(MXWRITE_ENABLED)
265 void toggleVideoRecording() {
266 if (video_writer.is_open()) {
267 closeVideoWriter();
268 return;
269 }
270
271 const VkExtent2D extent = getSwapchainExtent();
272 if (extent.width == 0U || extent.height == 0U) {
273 std::cerr << "fractal_zoom: cannot start video recording before the swapchain is ready\n";
274 return;
275 }
276
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";
282 encode_options.realtime = true;
283 encode_options.block_when_full = false;
284 if (!video_writer.open(video_output_path,
285 static_cast<int>(extent.width),
286 static_cast<int>(extent.height),
287 video_fps,
288 encode_options)) {
289 std::cerr << "fractal_zoom: failed to open MXWrite output file: " << video_output_path << "\n";
290 return;
291 }
292
293 video_record_width = extent.width;
294 video_record_height = extent.height;
295 try {
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;
302 return;
303 }
304 std::cout << std::format("fractal_zoom: recording video to {} at {}x{} 60 FPS\n",
305 video_output_path,
306 video_record_width,
307 video_record_height);
308 }
309
310 void closeVideoWriter() {
311 if (!video_writer.is_open()) {
312 return;
313 }
314
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;
320 }
321
322 void serviceRecordingReadbacks() {
323 if (!video_writer.is_open()) {
324 return;
325 }
326
327 try {
328 pumpCompletedRecordingReadbacks(false);
329 submitPendingRecordingReadbacks();
330 } catch (const std::exception &ex) {
331 std::cerr << "fractal_zoom: failed to service recording readback: " << ex.what() << "\n";
332 closeVideoWriter();
333 }
334 }
335
336 void recordPresentedFrame() {
337 if (!video_writer.is_open()) {
338 return;
339 }
340
341 try {
342 const VkExtent2D extent = getSwapchainExtent();
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";
345 closeVideoWriter();
346 return;
347 }
348 const auto now = std::chrono::steady_clock::now();
349 if (now < next_recording_frame_time) {
350 return;
351 }
352 next_recording_frame_time += recording_frame_interval;
353 if (next_recording_frame_time <= now) {
354 next_recording_frame_time = now + recording_frame_interval;
355 }
356 if (last_presented_image_index < recording_pending_images.size()) {
357 recording_pending_images[last_presented_image_index] = true;
358 }
359 pumpCompletedRecordingReadbacks(false);
360 submitPendingRecordingReadbacks();
361 } catch (const std::exception &ex) {
362 std::cerr << "fractal_zoom: failed to record video frame: " << ex.what() << "\n";
363 closeVideoWriter();
364 }
365 }
366
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;
375 bool queued = false;
376 };
377
378 void createRecordingReadbacks(VkExtent2D extent) {
379 destroyRecordingReadbacks(false);
380 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || graphics_queue == VK_NULL_HANDLE) {
381 throw mxvk::Exception("recording requires initialized Vulkan render resources");
382 }
384 throw mxvk::Exception("recording requires swapchain transfer-source support");
385 }
386
387 recording_format_is_bgra =
388 swapchain_format == VK_FORMAT_B8G8R8A8_UNORM ||
389 swapchain_format == VK_FORMAT_B8G8R8A8_SRGB;
390 const bool format_is_rgba =
391 swapchain_format == VK_FORMAT_R8G8B8A8_UNORM ||
392 swapchain_format == VK_FORMAT_R8G8B8A8_SRGB;
393 if (!recording_format_is_bgra && !format_is_rgba) {
394 throw mxvk::Exception(std::format("unsupported recording swapchain format: {}", static_cast<int>(swapchain_format)));
395 }
396
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);
400 recording_pending_images.assign(swapchain_images.size(), false);
401 next_recording_frame_time = std::chrono::steady_clock::now();
402
403 VkCommandBufferAllocateInfo command_info{};
404 command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
405 command_info.commandPool = command_pool;
406 command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
407 command_info.commandBufferCount = recording_readback_slot_count;
408
409 std::array<VkCommandBuffer, recording_readback_slot_count> command_buffers{};
410 if (vkAllocateCommandBuffers(device, &command_info, command_buffers.data()) != VK_SUCCESS) {
411 destroyRecordingReadbacks(false);
412 throw mxvk::Exception("failed to allocate recording readback command buffers");
413 }
414
415 VkFenceCreateInfo fence_info{};
416 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
417 fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
418
419 for (size_t i = 0; i < recording_readbacks.size(); ++i) {
420 RecordingReadbackSlot &slot = recording_readbacks[i];
421 slot.command_buffer = command_buffers[i];
422 createBuffer(
423 recording_image_bytes,
424 VK_BUFFER_USAGE_TRANSFER_DST_BIT,
425 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
426 slot.buffer,
427 slot.memory);
428 if (vkCreateFence(device, &fence_info, nullptr, &slot.fence) != VK_SUCCESS) {
429 destroyRecordingReadbacks(false);
430 throw mxvk::Exception("failed to create recording readback fence");
431 }
432 slot.pixels.resize(static_cast<size_t>(recording_image_bytes));
433 }
434
435 recording_worker_stop = false;
436 recording_worker = std::jthread([this](std::stop_token stop_token) {
437 recordingWorkerLoop(stop_token);
438 });
439 recording_readbacks_ready = true;
440 }
441
442 void destroyRecordingReadbacks(bool drain) {
443 if (!recording_readbacks_ready && recording_readbacks.empty()) {
444 return;
445 }
446
447 if (drain) {
448 drainRecordingReadbacks();
449 }
450
451 {
452 std::lock_guard<std::mutex> lock(recording_mutex);
453 recording_worker_stop = true;
454 }
455 recording_cv.notify_all();
456 if (recording_worker.joinable()) {
457 recording_worker.request_stop();
458 recording_worker.join();
459 }
460
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);
465 }
466 if (slot.image_index < image_fences.size() && image_fences[slot.image_index] == slot.fence) {
467 image_fences[slot.image_index] = VK_NULL_HANDLE;
468 }
469 if (slot.command_buffer != VK_NULL_HANDLE && command_pool != VK_NULL_HANDLE) {
470 vkFreeCommandBuffers(device, command_pool, 1, &slot.command_buffer);
471 slot.command_buffer = VK_NULL_HANDLE;
472 }
473 if (slot.fence != VK_NULL_HANDLE) {
474 vkDestroyFence(device, slot.fence, nullptr);
475 slot.fence = VK_NULL_HANDLE;
476 }
477 if (slot.buffer != VK_NULL_HANDLE) {
478 vkDestroyBuffer(device, slot.buffer, nullptr);
479 slot.buffer = VK_NULL_HANDLE;
480 }
481 if (slot.memory != VK_NULL_HANDLE) {
482 vkFreeMemory(device, slot.memory, nullptr);
483 slot.memory = VK_NULL_HANDLE;
484 }
485 }
486 }
487
488 recording_readbacks.clear();
489 recording_pending_images.clear();
490 {
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;
495 }
496 recording_readbacks_ready = false;
497 recording_row_bytes = 0;
498 recording_image_bytes = 0;
499 recording_format_is_bgra = false;
500 }
501
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]) {
505 continue;
506 }
507 if (submitRecordingReadback(image_index)) {
508 recording_pending_images[image_index] = false;
509 }
510 }
511 }
512
513 void drainRecordingReadbacks() {
514 while (true) {
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;
519 });
520 if (idle) {
521 return;
522 }
523 recording_idle_cv.wait(lock);
524 }
525 }
526
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;
531 {
532 std::lock_guard<std::mutex> lock(recording_mutex);
533 if (!slot.in_flight || slot.queued || slot.fence == VK_NULL_HANDLE) {
534 continue;
535 }
536 slot_fence = slot.fence;
537 }
538
539 VkResult fence_result = VK_SUCCESS;
540 if (wait_for_copy) {
541 fence_result = vkWaitForFences(device, 1, &slot_fence, VK_TRUE, UINT64_MAX);
542 } else {
543 fence_result = vkGetFenceStatus(device, slot_fence);
544 }
545
546 if (fence_result == VK_NOT_READY) {
547 continue;
548 }
549 if (fence_result != VK_SUCCESS) {
550 throw mxvk::Exception(std::format("recording readback fence failed: {}", static_cast<int>(fence_result)));
551 }
552
553 {
554 std::lock_guard<std::mutex> lock(recording_mutex);
555 if (!slot.in_flight || slot.queued || slot.fence != slot_fence) {
556 continue;
557 }
558 if (slot.image_index < image_fences.size() && image_fences[slot.image_index] == slot.fence) {
559 image_fences[slot.image_index] = VK_NULL_HANDLE;
560 }
561 slot.queued = true;
562 recording_ready_slots.push(i);
563 }
564 recording_cv.notify_one();
565 }
566 }
567
568 bool submitRecordingReadback(uint32_t image_index) {
569 if (!recording_readbacks_ready || image_index == std::numeric_limits<uint32_t>::max()) {
570 return false;
571 }
572 if (image_index >= swapchain_images.size() || image_index >= image_fences.size()) {
573 return false;
574 }
575
576 VkFence source_fence = image_fences[image_index];
577 if (source_fence != VK_NULL_HANDLE && vkGetFenceStatus(device, source_fence) == VK_NOT_READY) {
578 return false;
579 }
580
581 RecordingReadbackSlot *free_slot = nullptr;
582 {
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];
587 break;
588 }
589 }
590 }
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";
595 }
596 return true;
597 }
598
599 RecordingReadbackSlot &slot = *free_slot;
600 VK_CHECK_RESULT(vkResetFences(device, 1, &slot.fence));
601 VK_CHECK_RESULT(vkResetCommandBuffer(slot.command_buffer, 0));
602
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));
607
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;
618 to_transfer_barrier.image = swapchain_images[image_index];
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;
624
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);
630
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(
638 slot.command_buffer,
639 swapchain_images[image_index],
640 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
641 slot.buffer,
642 1,
643 &copy_region);
644
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;
655 to_present_barrier.image = swapchain_images[image_index];
656 to_present_barrier.subresourceRange = to_transfer_barrier.subresourceRange;
657
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);
663
664 VK_CHECK_RESULT(vkEndCommandBuffer(slot.command_buffer));
665
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;
669
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;
674
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)));
679 }
680 {
681 std::lock_guard<std::mutex> lock(recording_mutex);
682 slot.in_flight = true;
683 slot.queued = false;
684 }
685 image_fences[slot.image_index] = slot.fence;
686 return true;
687 }
688
689 void recordingWorkerLoop(std::stop_token stop_token) {
690 std::vector<std::uint8_t> frame_pixels;
691 while (true) {
692 size_t slot_index = 0;
693 {
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();
697 });
698 if ((recording_worker_stop || stop_token.stop_requested()) && recording_ready_slots.empty()) {
699 break;
700 }
701 slot_index = recording_ready_slots.front();
702 recording_ready_slots.pop();
703 }
704
705 if (slot_index >= recording_readbacks.size()) {
706 continue;
707 }
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];
719 }
720 } else {
721 std::memcpy(frame_pixels.data(), src, frame_pixels.size());
722 }
723 vkUnmapMemory(device, slot.memory);
724 } else {
725 std::cerr << "fractal_zoom: failed to map recording readback memory\n";
726 frame_pixels.clear();
727 }
728
729 {
730 std::lock_guard<std::mutex> lock(recording_mutex);
731 slot.in_flight = false;
732 slot.queued = false;
733 slot.image_index = std::numeric_limits<uint32_t>::max();
734 }
735 recording_idle_cv.notify_all();
736
737 if (!frame_pixels.empty()) {
738 video_writer.write(frame_pixels.data());
739 }
740 }
741 }
742#endif
743
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";
748 return;
749 }
750
751 std::filesystem::path snapshot_dir = std::filesystem::path(home) / "Pictures";
752 std::error_code error;
753 std::filesystem::create_directories(snapshot_dir, error);
754 if (error) {
755 std::cerr << std::format("fractal_zoom: failed to create snapshot directory '{}': {}\n",
756 snapshot_dir.string(),
757 error.message());
758 return;
759 }
760
761 const std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
762 std::tm local_time{};
763#if defined(_WIN32)
764 localtime_s(&local_time, &now);
765#else
766 localtime_r(&now, &local_time);
767#endif
768
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,
773 local_time.tm_mday,
774 local_time.tm_hour,
775 local_time.tm_min,
776 local_time.tm_sec,
777 snapshot_index++);
778
779 const std::filesystem::path snapshot_path = snapshot_dir / filename;
780 try {
781 saveSnapshot(snapshot_path.string());
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";
785 }
786 }
787
788 void applyWheelZoom(float wheel_y) {
789 const VkExtent2D extent = getSwapchainExtent();
790 if (extent.width == 0U || extent.height == 0U || window == nullptr) {
791 return;
792 }
793
794 float mouse_x = 0.0f;
795 float mouse_y = 0.0f;
796 SDL_GetMouseState(&mouse_x, &mouse_y);
797
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;
801
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);
804
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;
808
809 center_x += before_x - after_x;
810 center_y += before_y - after_y;
811
812 if (wheel_y > 0.0f && zoom > ReferenceScalar(10)) {
813 max_iterations = std::min(max_iterations + 12, max_reference_iterations);
814 }
815 reference_orbit_dirty = true;
816 }
817
818 void updateKeyboardNavigation() {
819 const bool *keys = SDL_GetKeyboardState(nullptr);
820 if (keys == nullptr) {
821 return;
822 }
823
824 const auto now = std::chrono::steady_clock::now();
825 const ReferenceScalar dt = ReferenceScalar(std::chrono::duration<double>(now - last_tick).count());
826 last_tick = now;
827
828 const ReferenceScalar move_speed = ReferenceScalar("0.85") * minReferenceScalar(dt, ReferenceScalar("0.1")) / zoom;
829 bool moved = false;
830 if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) {
831 center_x -= move_speed;
832 moved = true;
833 }
834 if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) {
835 center_x += move_speed;
836 moved = true;
837 }
838 if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
839 center_y += move_speed;
840 moved = true;
841 }
842 if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
843 center_y -= move_speed;
844 moved = true;
845 }
846 if (keys[SDL_SCANCODE_Z]) {
847 zoom = minReferenceScalar(zoom * (ReferenceScalar(1) + ReferenceScalar("1.9") * dt), MAX_ZOOM);
848 moved = true;
849 }
850 if (keys[SDL_SCANCODE_X]) {
851 zoom = maxReferenceScalar(zoom * (ReferenceScalar(1) - ReferenceScalar("1.9") * dt), ReferenceScalar("0.5"));
852 moved = true;
853 }
854 if (moved) {
855 reference_orbit_dirty = true;
856 }
857 }
858
859 void resetView() {
860 center_x = ReferenceScalar("-0.5");
861 center_y = ReferenceScalar(0);
862 zoom = ReferenceScalar(1);
863 max_iterations = 256;
864 reference_orbit_dirty = true;
865 }
866
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();
879 return;
880 }
881
882 if (fractal_pipeline != VK_NULL_HANDLE) {
883 vkDestroyPipeline(device, fractal_pipeline, nullptr);
884 fractal_pipeline = VK_NULL_HANDLE;
885 }
886 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
887 vkDestroyPipelineLayout(device, fractal_pipeline_layout, nullptr);
888 fractal_pipeline_layout = VK_NULL_HANDLE;
889 }
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();
894 }
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;
898 }
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]);
903 }
904 vkFreeMemory(device, reference_orbit_memories[i], nullptr);
905 }
906 }
907 for (VkBuffer buffer : reference_orbit_buffers) {
908 if (buffer != VK_NULL_HANDLE) {
909 vkDestroyBuffer(device, buffer, nullptr);
910 }
911 }
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();
917 }
918
919 void destroyFractalPipeline() {
920 if (device == VK_NULL_HANDLE) {
921 fractal_pipeline = VK_NULL_HANDLE;
922 fractal_pipeline_layout = VK_NULL_HANDLE;
923 return;
924 }
925 if (fractal_pipeline != VK_NULL_HANDLE) {
926 vkDestroyPipeline(device, fractal_pipeline, nullptr);
927 fractal_pipeline = VK_NULL_HANDLE;
928 }
929 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
930 vkDestroyPipelineLayout(device, fractal_pipeline_layout, nullptr);
931 fractal_pipeline_layout = VK_NULL_HANDLE;
932 }
933 }
934
935 void createFractalPipeline() {
936 if (device == VK_NULL_HANDLE) {
937 return;
938 }
939
940 const VkFormat color_format = getSwapchainFormat();
941 const VkFormat depth_attachment_format = getDepthFormat();
942 if (color_format == VK_FORMAT_UNDEFINED || depth_attachment_format == VK_FORMAT_UNDEFINED) {
943 return;
944 }
945
946 ensureFractalResources();
947 if (fractal_descriptor_set_layout == VK_NULL_HANDLE || fractal_descriptor_sets.empty()) {
948 return;
949 }
950
951 destroyFractalPipeline();
952
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";
957#else
958 shaderRoot + "/fractal.frag.spv";
959#endif
960 const std::vector<char> vert_bytes = loadSpv(vert_path);
961 const std::vector<char> frag_bytes = loadSpv(frag_path);
962
963 const VkShaderModule vert_module = createShaderModule(device, vert_bytes);
964 VkShaderModule frag_module = VK_NULL_HANDLE;
965
966 try {
967 frag_module = createShaderModule(device, frag_bytes);
968
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";
974
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";
980
981 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
982
983 VkPipelineVertexInputStateCreateInfo vertex_input{};
984 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
985
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;
990
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;
995
996 const VkDynamicState dynamic_states[] = {
997 VK_DYNAMIC_STATE_VIEWPORT,
998 VK_DYNAMIC_STATE_SCISSOR,
999 };
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;
1004
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;
1013
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;
1018
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;
1026
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;
1034
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;
1040
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));
1045
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;
1052
1053 if (vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &fractal_pipeline_layout) != VK_SUCCESS) {
1054 throw mxvk::Exception("Failed to create fractal pipeline layout");
1055 }
1056
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;
1064
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;
1081
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");
1084 }
1085 } catch (...) {
1086 if (fractal_pipeline != VK_NULL_HANDLE) {
1087 vkDestroyPipeline(device, fractal_pipeline, nullptr);
1088 fractal_pipeline = VK_NULL_HANDLE;
1089 }
1090 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
1091 vkDestroyPipelineLayout(device, fractal_pipeline_layout, nullptr);
1092 fractal_pipeline_layout = VK_NULL_HANDLE;
1093 }
1094 if (frag_module != VK_NULL_HANDLE) {
1095 vkDestroyShaderModule(device, frag_module, nullptr);
1096 }
1097 vkDestroyShaderModule(device, vert_module, nullptr);
1098 throw;
1099 }
1100
1101 vkDestroyShaderModule(device, frag_module, nullptr);
1102 vkDestroyShaderModule(device, vert_module, nullptr);
1103 }
1104
1105 void ensureFractalResources() {
1106 const size_t required_count = std::max<size_t>(getSwapchainImageCount(), 1);
1107 if (reference_orbit_buffers.size() != required_count || fractal_descriptor_sets.size() != required_count) {
1108 destroyFractalResources();
1109 }
1110
1111 if (reference_orbit_buffers.empty()) {
1112 createReferenceOrbitBuffers(required_count);
1113 }
1114 if (fractal_descriptor_set_layout == VK_NULL_HANDLE) {
1115 createDescriptorSetLayout();
1116 }
1117 if (fractal_descriptor_pool == VK_NULL_HANDLE) {
1118 createDescriptorPool();
1119 }
1120 if (fractal_descriptor_sets.empty()) {
1121 allocateDescriptorSets(required_count);
1122 writeDescriptorSets();
1123 }
1124 }
1125
1126 void createReferenceOrbitBuffers(size_t buffer_count) {
1127 const VkDeviceSize buffer_size = static_cast<VkDeviceSize>(reference_orbit_capacity * sizeof(OrbitSample));
1128
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]);
1134 }
1135 vkFreeMemory(device, reference_orbit_memories[i], nullptr);
1136 }
1137 }
1138 for (VkBuffer buffer : reference_orbit_buffers) {
1139 if (buffer != VK_NULL_HANDLE) {
1140 vkDestroyBuffer(device, buffer, nullptr);
1141 }
1142 }
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();
1148 };
1149
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);
1156
1157 try {
1158 for (size_t i = 0; i < buffer_count; ++i) {
1159 try {
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;
1162 } catch (...) {
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;
1165 }
1166
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");
1169 }
1170 }
1171 } catch (...) {
1172 cleanup_reference_orbit_buffers();
1173 throw;
1174 }
1175 }
1176
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;
1183
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;
1188
1189 if (vkCreateDescriptorSetLayout(device, &layout_info, nullptr, &fractal_descriptor_set_layout) != VK_SUCCESS) {
1190 throw mxvk::Exception("Failed to create fractal descriptor set layout");
1191 }
1192 }
1193
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());
1198
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;
1204
1205 if (vkCreateDescriptorPool(device, &pool_info, nullptr, &fractal_descriptor_pool) != VK_SUCCESS) {
1206 throw mxvk::Exception("Failed to create fractal descriptor pool");
1207 }
1208 }
1209
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);
1213
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();
1219
1220 if (vkAllocateDescriptorSets(device, &alloc_info, fractal_descriptor_sets.data()) != VK_SUCCESS) {
1221 throw mxvk::Exception("Failed to allocate fractal descriptor sets");
1222 }
1223 }
1224
1225 void writeDescriptorSets() {
1226 std::vector<VkDescriptorBufferInfo> buffer_infos(fractal_descriptor_sets.size());
1227 std::vector<VkWriteDescriptorSet> writes(fractal_descriptor_sets.size());
1228
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));
1233
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];
1241 }
1242
1243 vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1244 }
1245
1246 void updateReferenceOrbit(uint32_t image_index, VkExtent2D extent) {
1247 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] == nullptr) {
1248 return;
1249 }
1250
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;
1259 orbit_length = 0;
1260 }
1261 uploadReferenceOrbit(image_index);
1262 return;
1263 }
1264
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);
1269 return;
1270 }
1271
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);
1278
1279 std::fill(reference_orbit_samples.begin(), reference_orbit_samples.end(), OrbitSample{});
1280 std::vector<ReferenceTile> tiles;
1281 tiles.reserve(max_adaptive_references);
1282
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);
1289
1290 tiles.push_back({tile_min_x, tile_min_y, tile_max_x, tile_max_y, 0});
1291 }
1292 }
1293
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)) {
1297 refined = false;
1298
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)) {
1302 continue;
1303 }
1304
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}};
1313
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());
1316 refined = true;
1317 break;
1318 }
1319 }
1320
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);
1329 }
1330
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),
1335 0.0f};
1336
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);
1343 }
1344
1345 void uploadReferenceOrbit(uint32_t image_index) {
1346 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] == nullptr) {
1347 return;
1348 }
1349 if (image_index < reference_orbit_uploaded_generations.size() && reference_orbit_uploaded_generations[image_index] == reference_orbit_generation) {
1350 return;
1351 }
1352
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));
1355
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];
1360 range.offset = 0;
1361 range.size = upload_size;
1362 vkFlushMappedMemoryRanges(device, 1, &range);
1363 }
1364
1365 if (image_index < reference_orbit_uploaded_generations.size()) {
1366 reference_orbit_uploaded_generations[image_index] = reference_orbit_generation;
1367 }
1368 }
1369
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);
1376 }
1377
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;
1381
1382 ReferenceScalar z_x = 0;
1383 ReferenceScalar z_y = 0;
1384 int sample_count = 1;
1385
1386 reference_orbit_samples[orbit_base] = {
1387 toOrbitSampleScalar(z_x),
1388 toOrbitSampleScalar(z_y),
1389 1.0f,
1390 0.0f};
1391
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;
1395
1396 z_x = next_x;
1397 z_y = next_y;
1398
1399 reference_orbit_samples[orbit_base + static_cast<size_t>(sample_count)] = {
1400 toOrbitSampleScalar(z_x),
1401 toOrbitSampleScalar(z_y),
1402 0.0f,
1403 0.0f};
1404 ++sample_count;
1405
1406 const ReferenceScalar mag2 = z_x * z_x + z_y * z_y;
1407 if (mag2 > ReferenceScalar(4)) {
1408 break;
1409 }
1410 }
1411
1412 reference_orbit_samples[orbit_base].z = static_cast<float>(sample_count);
1413 return sample_count;
1414 }
1415
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)};
1428 }
1429
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")}};
1437
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)) {
1442 return false;
1443 }
1444 }
1445
1446 return true;
1447 }
1448
1449 bool isPerturbationSampleStable(size_t orbit_base, int sample_count, int iteration_count, float delta_c_x, float delta_c_y) const {
1450 float dz_x = 0.0f;
1451 float dz_y = 0.0f;
1452 const int count = std::min(iteration_count, sample_count - 1);
1453
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;
1460
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;
1463
1464 if (!std::isfinite(dz_x) || !std::isfinite(dz_y)) {
1465 return false;
1466 }
1467
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) {
1473 return true;
1474 }
1475
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)) {
1479 return false;
1480 }
1481 }
1482
1483 return sample_count > iteration_count;
1484 }
1485
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;
1492
1493 if (vkCreateBuffer(device, &buffer_info, nullptr, &buffer) != VK_SUCCESS) {
1494 throw mxvk::Exception("Failed to create fractal buffer");
1495 }
1496
1497 VkMemoryRequirements mem_requirements{};
1498 vkGetBufferMemoryRequirements(device, buffer, &mem_requirements);
1499
1500 VkMemoryAllocateInfo alloc_info{};
1501 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1502 alloc_info.allocationSize = mem_requirements.size;
1503 try {
1504 alloc_info.memoryTypeIndex = findMemoryType(mem_requirements.memoryTypeBits, properties);
1505 } catch (...) {
1506 vkDestroyBuffer(device, buffer, nullptr);
1507 buffer = VK_NULL_HANDLE;
1508 throw;
1509 }
1510
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");
1515 }
1516
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");
1523 }
1524 }
1525
1526 uint32_t findMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties) const {
1527 VkPhysicalDeviceMemoryProperties mem_properties{};
1528 vkGetPhysicalDeviceMemoryProperties(physical_device, &mem_properties);
1529
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) {
1534 return i;
1535 }
1536 }
1537
1538 throw mxvk::Exception("Failed to find suitable memory type for fractal buffer");
1539 }
1540
1541 using PushScalar =
1542#if defined(MXVK_USE_MOLTENVK)
1543 float;
1544#else
1545 double;
1546#endif
1547 struct FractalPushConstants {
1548 PushScalar center_x;
1549 PushScalar center_y;
1550 PushScalar inverse_zoom;
1551 PushScalar time;
1552 PushScalar resolution_x;
1553 PushScalar resolution_y;
1554 int max_iterations;
1555 int palette;
1556 int orbit_length;
1557 int reserved;
1558 };
1559
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{};
1570
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);
1584#else
1585 static inline const ReferenceScalar direct_reference_zoom_threshold = ReferenceScalar("1e15");
1586#endif
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{};
1594
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);
1597 }
1598
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;
1604
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);
1610
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;
1636#endif
1637 };
1638
1639} // namespace example
1640
1641int main(int argc, char **argv) {
1642 try {
1643 const Arguments args = proc_args(argc, argv);
1644 example::FractalWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
1645 window.loop();
1646 } catch (mxvk::Exception &e) {
1647 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1648 return EXIT_FAILURE;
1649 } catch (ArgException<std::string> &e) {
1650 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1651 return EXIT_FAILURE;
1652 }
1653 return EXIT_SUCCESS;
1654}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:872
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:178
void event(SDL_Event &e) override
Handle one SDL event.
Definition fractal.cpp:73
void proc() override
Execute one processing/update step.
Definition fractal.cpp:124
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
Definition fractal.cpp:152
void render() override
Render one frame.
Definition fractal.cpp:128
~FractalWindow() override
Definition fractal.cpp:63
void onSwapchainAboutToRecreate() override
Called right before swapchain-dependent resources are recreated.
Definition fractal.cpp:138
FractalWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
Definition fractal.cpp:57
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition fractal.cpp:148
std::string text() const
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:186
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
VkDevice device
Definition mxvk.hpp:485
VkFormat swapchain_format
Definition mxvk.hpp:493
uint32_t last_presented_image_index
Definition mxvk.hpp:512
static VkShaderModule createShaderModule(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
Definition mxvk.cpp:145
std::vector< VkImage > swapchain_images
Definition mxvk.hpp:496
std::vector< VkFence > image_fences
Definition mxvk.hpp:510
size_t getSwapchainImageCount() const noexcept
Get the number of swapchain images currently allocated.
Definition mxvk.hpp:192
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
Definition mxvk.cpp:782
std::vector< VkCommandBuffer > command_buffers
Definition mxvk.hpp:505
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:480
void exit()
Request loop termination.
Definition mxvk.cpp:1126
VkCommandPool command_pool
Definition mxvk.hpp:504
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:484
VkFormat getDepthFormat() const noexcept
Get the depth format used for dynamic rendering attachments.
Definition mxvk.hpp:189
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:141
VkQueue graphics_queue
Definition mxvk.hpp:488
virtual void render()
Render one frame.
Definition mxvk.cpp:778
bool swapchain_supports_transfer_src
Definition mxvk.hpp:513
VkFormat getSwapchainFormat() const noexcept
Get the swapchain color format.
Definition mxvk.hpp:183
int main(void)
Definition main.cpp:7
#define fractal_zoom_ASSET_DIR
Definition fractal.cpp:33
#define MXVK_VALIDATION
Definition mxvk.hpp:27
#define VK_CHECK_RESULT(f)
FFmpeg-based video writer used by MXWrite.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:750
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:735
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732
std::string tune
Optional tuning mode.
Definition mxwrite.hpp:58
bool realtime
Enable low-latency settings.
Definition mxwrite.hpp:61
int crf
Constant Rate Factor.
Definition mxwrite.hpp:59
bool block_when_full
Block producer threads instead of dropping when the encoder queue is full.
Definition mxwrite.hpp:62
std::string preset
Encoder preset name.
Definition mxwrite.hpp:57