MXVK Vulkan Framework 0.35.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) : 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") {}
58
59 ~FractalWindow() override {
60#if defined(MXWRITE_ENABLED)
61 closeVideoWriter();
62#endif
63 if (device != VK_NULL_HANDLE) {
64 vkDeviceWaitIdle(device);
65 }
66 destroyFractalResources();
67 }
68
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();
73 return;
74 }
75 if ((e.key.key == SDLK_P || e.key.scancode == SDL_SCANCODE_P) && !e.key.repeat) {
76#if defined(MXWRITE_ENABLED)
77 toggleVideoRecording();
78#else
79 std::cout << "fractal_zoom: MXWrite is unavailable; video recording disabled\n";
80#endif
81 return;
82 }
83 handleKey(e.key.key);
84 return;
85 }
86
87 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
88 dragging = true;
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;
93 return;
94 }
95
96 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
97 dragging = false;
98 return;
99 }
100
101 if (e.type == SDL_EVENT_MOUSE_MOTION && dragging) {
102 const VkExtent2D extent = getSwapchainExtent();
103 if (extent.width == 0U || extent.height == 0U) {
104 return;
105 }
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;
112 return;
113 }
114
115 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
116 applyWheelZoom(e.wheel.y);
117 }
118 }
119
120 void proc() override { updateKeyboardNavigation(); }
121
122 void render() override {
123#if defined(MXWRITE_ENABLED)
124 serviceRecordingReadbacks();
125#endif
127#if defined(MXWRITE_ENABLED)
128 recordPresentedFrame();
129#endif
130 }
131
133#if defined(MXWRITE_ENABLED)
134 if (video_writer.is_open()) {
135 std::cerr << "fractal_zoom: swapchain is changing; closing current video recording\n";
136 closeVideoWriter();
137 }
138#endif
139 destroyFractalResources();
140 }
141
142 void onSwapchainRecreated() override { createFractalPipeline(); }
143
144 void onRecordCustomRendering(VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index) override {
145 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE) {
146 createFractalPipeline();
147 }
148
149 if (image_index >= fractal_descriptor_sets.size()) {
150 return;
151 }
152
153 if (fractal_pipeline == VK_NULL_HANDLE || fractal_pipeline_layout == VK_NULL_HANDLE || fractal_descriptor_sets[image_index] == VK_NULL_HANDLE) {
154 return;
155 }
156
157 const VkExtent2D extent = getSwapchainExtent();
158 if (extent.width == 0U || extent.height == 0U) {
159 return;
160 }
161
162 updateReferenceOrbit(image_index, extent);
163
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);
166
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};
168
169 vkCmdPushConstants(cmd, fractal_pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push_constants), &push_constants);
170
171 vkCmdDraw(cmd, 3, 1, 0, 0);
172 }
173
174 private:
175 static ReferenceScalar minReferenceScalar(const ReferenceScalar &a, const ReferenceScalar &b) { return (a < b) ? a : b; }
176
177 static ReferenceScalar maxReferenceScalar(const ReferenceScalar &a, const ReferenceScalar &b) { return (a > b) ? a : b; }
178
179 static ReferenceScalar clampReferenceScalar(const ReferenceScalar &value, const ReferenceScalar &minimum, const ReferenceScalar &maximum) { return maxReferenceScalar(minReferenceScalar(value, maximum), minimum); }
180
181 static float toOrbitSampleScalar(const ReferenceScalar &value) { return value.convert_to<float>(); }
182
183 void handleKey(SDL_Keycode key) {
184 switch (key) {
185 case SDLK_ESCAPE:
186 exit();
187 break;
188 case SDLK_R:
189 resetView();
190 break;
191 case SDLK_1:
192 center_x = ReferenceScalar("-0.5");
193 center_y = ReferenceScalar(0);
194 zoom = ReferenceScalar(1);
195 max_iterations = 256;
196 reference_orbit_dirty = true;
197 break;
198 case SDLK_2:
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;
204 break;
205 case SDLK_3:
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;
211 break;
212 case SDLK_EQUALS:
213 case SDLK_PLUS:
214 max_iterations = std::min(max_iterations + 64, max_reference_iterations);
215 reference_orbit_dirty = true;
216 break;
217 case SDLK_MINUS:
218 max_iterations = std::max(max_iterations - 64, 64);
219 reference_orbit_dirty = true;
220 break;
221 case SDLK_LEFTBRACKET:
222 palette_index = (palette_index + 2) % 3;
223 break;
224 case SDLK_RIGHTBRACKET:
225 palette_index = (palette_index + 1) % 3;
226 break;
227 default:
228 break;
229 }
230 }
231
232#if defined(MXWRITE_ENABLED)
233 void toggleVideoRecording() {
234 if (video_writer.is_open()) {
235 closeVideoWriter();
236 return;
237 }
238
239 const VkExtent2D extent = getSwapchainExtent();
240 if (extent.width == 0U || extent.height == 0U) {
241 std::cerr << "fractal_zoom: cannot start video recording before the swapchain is ready\n";
242 return;
243 }
244
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";
250 encode_options.realtime = true;
251 encode_options.block_when_full = false;
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";
254 return;
255 }
256
257 video_record_width = extent.width;
258 video_record_height = extent.height;
259 try {
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;
266 return;
267 }
268 std::cout << std::format("fractal_zoom: recording video to {} at {}x{} 60 FPS\n", video_output_path, video_record_width, video_record_height);
269 }
270
271 void closeVideoWriter() {
272 if (!video_writer.is_open()) {
273 return;
274 }
275
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;
281 }
282
283 void serviceRecordingReadbacks() {
284 if (!video_writer.is_open()) {
285 return;
286 }
287
288 try {
289 pumpCompletedRecordingReadbacks(false);
290 submitPendingRecordingReadbacks();
291 } catch (const std::exception &ex) {
292 std::cerr << "fractal_zoom: failed to service recording readback: " << ex.what() << "\n";
293 closeVideoWriter();
294 }
295 }
296
297 void recordPresentedFrame() {
298 if (!video_writer.is_open()) {
299 return;
300 }
301
302 try {
303 const VkExtent2D extent = getSwapchainExtent();
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";
306 closeVideoWriter();
307 return;
308 }
309 const auto now = std::chrono::steady_clock::now();
310 if (now < next_recording_frame_time) {
311 return;
312 }
313 next_recording_frame_time += recording_frame_interval;
314 if (next_recording_frame_time <= now) {
315 next_recording_frame_time = now + recording_frame_interval;
316 }
317 if (last_presented_image_index < recording_pending_images.size()) {
318 recording_pending_images[last_presented_image_index] = true;
319 }
320 pumpCompletedRecordingReadbacks(false);
321 submitPendingRecordingReadbacks();
322 } catch (const std::exception &ex) {
323 std::cerr << "fractal_zoom: failed to record video frame: " << ex.what() << "\n";
324 closeVideoWriter();
325 }
326 }
327
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;
336 bool queued = false;
337 };
338
339 void createRecordingReadbacks(VkExtent2D extent) {
340 destroyRecordingReadbacks(false);
341 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || graphics_queue == VK_NULL_HANDLE) {
342 throw mxvk::Exception("recording requires initialized Vulkan render resources");
343 }
345 throw mxvk::Exception("recording requires swapchain transfer-source support");
346 }
347
348 recording_format_is_bgra = swapchain_format == VK_FORMAT_B8G8R8A8_UNORM || swapchain_format == VK_FORMAT_B8G8R8A8_SRGB;
349 const bool format_is_rgba = swapchain_format == VK_FORMAT_R8G8B8A8_UNORM || swapchain_format == VK_FORMAT_R8G8B8A8_SRGB;
350 if (!recording_format_is_bgra && !format_is_rgba) {
351 throw mxvk::Exception(std::format("unsupported recording swapchain format: {}", static_cast<int>(swapchain_format)));
352 }
353
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);
357 recording_pending_images.assign(swapchain_images.size(), false);
358 next_recording_frame_time = std::chrono::steady_clock::now();
359
360 VkCommandBufferAllocateInfo command_info{};
361 command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
362 command_info.commandPool = command_pool;
363 command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
364 command_info.commandBufferCount = recording_readback_slot_count;
365
366 std::array<VkCommandBuffer, recording_readback_slot_count> command_buffers{};
367 if (vkAllocateCommandBuffers(device, &command_info, command_buffers.data()) != VK_SUCCESS) {
368 destroyRecordingReadbacks(false);
369 throw mxvk::Exception("failed to allocate recording readback command buffers");
370 }
371
372 VkFenceCreateInfo fence_info{};
373 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
374 fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
375
376 for (size_t i = 0; i < recording_readbacks.size(); ++i) {
377 RecordingReadbackSlot &slot = recording_readbacks[i];
378 slot.command_buffer = command_buffers[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");
383 }
384 slot.pixels.resize(static_cast<size_t>(recording_image_bytes));
385 }
386
387 recording_worker_stop = false;
388 recording_worker = std::jthread([this](std::stop_token stop_token) { recordingWorkerLoop(stop_token); });
389 recording_readbacks_ready = true;
390 }
391
392 void destroyRecordingReadbacks(bool drain) {
393 if (!recording_readbacks_ready && recording_readbacks.empty()) {
394 return;
395 }
396
397 if (drain) {
398 drainRecordingReadbacks();
399 }
400
401 {
402 std::lock_guard<std::mutex> lock(recording_mutex);
403 recording_worker_stop = true;
404 }
405 recording_cv.notify_all();
406 if (recording_worker.joinable()) {
407 recording_worker.request_stop();
408 recording_worker.join();
409 }
410
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);
415 }
416 if (slot.image_index < image_fences.size() && image_fences[slot.image_index] == slot.fence) {
417 image_fences[slot.image_index] = VK_NULL_HANDLE;
418 }
419 if (slot.command_buffer != VK_NULL_HANDLE && command_pool != VK_NULL_HANDLE) {
420 vkFreeCommandBuffers(device, command_pool, 1, &slot.command_buffer);
421 slot.command_buffer = VK_NULL_HANDLE;
422 }
423 if (slot.fence != VK_NULL_HANDLE) {
424 vkDestroyFence(device, slot.fence, nullptr);
425 slot.fence = VK_NULL_HANDLE;
426 }
427 if (slot.buffer != VK_NULL_HANDLE) {
428 vkDestroyBuffer(device, slot.buffer, nullptr);
429 slot.buffer = VK_NULL_HANDLE;
430 }
431 if (slot.memory != VK_NULL_HANDLE) {
432 vkFreeMemory(device, slot.memory, nullptr);
433 slot.memory = VK_NULL_HANDLE;
434 }
435 }
436 }
437
438 recording_readbacks.clear();
439 recording_pending_images.clear();
440 {
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;
445 }
446 recording_readbacks_ready = false;
447 recording_row_bytes = 0;
448 recording_image_bytes = 0;
449 recording_format_is_bgra = false;
450 }
451
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]) {
455 continue;
456 }
457 if (submitRecordingReadback(image_index)) {
458 recording_pending_images[image_index] = false;
459 }
460 }
461 }
462
463 void drainRecordingReadbacks() {
464 while (true) {
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; });
468 if (idle) {
469 return;
470 }
471 recording_idle_cv.wait(lock);
472 }
473 }
474
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;
479 {
480 std::lock_guard<std::mutex> lock(recording_mutex);
481 if (!slot.in_flight || slot.queued || slot.fence == VK_NULL_HANDLE) {
482 continue;
483 }
484 slot_fence = slot.fence;
485 }
486
487 VkResult fence_result = VK_SUCCESS;
488 if (wait_for_copy) {
489 fence_result = vkWaitForFences(device, 1, &slot_fence, VK_TRUE, UINT64_MAX);
490 } else {
491 fence_result = vkGetFenceStatus(device, slot_fence);
492 }
493
494 if (fence_result == VK_NOT_READY) {
495 continue;
496 }
497 if (fence_result != VK_SUCCESS) {
498 throw mxvk::Exception(std::format("recording readback fence failed: {}", static_cast<int>(fence_result)));
499 }
500
501 {
502 std::lock_guard<std::mutex> lock(recording_mutex);
503 if (!slot.in_flight || slot.queued || slot.fence != slot_fence) {
504 continue;
505 }
506 if (slot.image_index < image_fences.size() && image_fences[slot.image_index] == slot.fence) {
507 image_fences[slot.image_index] = VK_NULL_HANDLE;
508 }
509 slot.queued = true;
510 recording_ready_slots.push(i);
511 }
512 recording_cv.notify_one();
513 }
514 }
515
516 bool submitRecordingReadback(uint32_t image_index) {
517 if (!recording_readbacks_ready || image_index == std::numeric_limits<uint32_t>::max()) {
518 return false;
519 }
520 if (image_index >= swapchain_images.size() || image_index >= image_fences.size()) {
521 return false;
522 }
523
524 VkFence source_fence = image_fences[image_index];
525 if (source_fence != VK_NULL_HANDLE && vkGetFenceStatus(device, source_fence) == VK_NOT_READY) {
526 return false;
527 }
528
529 RecordingReadbackSlot *free_slot = nullptr;
530 {
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];
535 break;
536 }
537 }
538 }
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";
543 }
544 return true;
545 }
546
547 RecordingReadbackSlot &slot = *free_slot;
548 VK_CHECK_RESULT(vkResetFences(device, 1, &slot.fence));
549 VK_CHECK_RESULT(vkResetCommandBuffer(slot.command_buffer, 0));
550
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));
555
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;
566 to_transfer_barrier.image = swapchain_images[image_index];
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;
572
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);
578
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, &copy_region);
586
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;
597 to_present_barrier.image = swapchain_images[image_index];
598 to_present_barrier.subresourceRange = to_transfer_barrier.subresourceRange;
599
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);
605
606 VK_CHECK_RESULT(vkEndCommandBuffer(slot.command_buffer));
607
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;
611
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;
616
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)));
621 }
622 {
623 std::lock_guard<std::mutex> lock(recording_mutex);
624 slot.in_flight = true;
625 slot.queued = false;
626 }
627 image_fences[slot.image_index] = slot.fence;
628 return true;
629 }
630
631 void recordingWorkerLoop(std::stop_token stop_token) {
632 std::vector<std::uint8_t> frame_pixels;
633 while (true) {
634 size_t slot_index = 0;
635 {
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()) {
639 break;
640 }
641 slot_index = recording_ready_slots.front();
642 recording_ready_slots.pop();
643 }
644
645 if (slot_index >= recording_readbacks.size()) {
646 continue;
647 }
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];
659 }
660 } else {
661 std::memcpy(frame_pixels.data(), src, frame_pixels.size());
662 }
663 vkUnmapMemory(device, slot.memory);
664 } else {
665 std::cerr << "fractal_zoom: failed to map recording readback memory\n";
666 frame_pixels.clear();
667 }
668
669 {
670 std::lock_guard<std::mutex> lock(recording_mutex);
671 slot.in_flight = false;
672 slot.queued = false;
673 slot.image_index = std::numeric_limits<uint32_t>::max();
674 }
675 recording_idle_cv.notify_all();
676
677 if (!frame_pixels.empty()) {
678 video_writer.write(frame_pixels.data());
679 }
680 }
681 }
682#endif
683
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";
688 return;
689 }
690
691 std::filesystem::path snapshot_dir = std::filesystem::path(home) / "Pictures";
692 std::error_code error;
693 std::filesystem::create_directories(snapshot_dir, error);
694 if (error) {
695 std::cerr << std::format("fractal_zoom: failed to create snapshot directory '{}': {}\n", snapshot_dir.string(), error.message());
696 return;
697 }
698
699 const std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
700 std::tm local_time{};
701#if defined(_WIN32)
702 localtime_s(&local_time, &now);
703#else
704 localtime_r(&now, &local_time);
705#endif
706
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++);
708
709 const std::filesystem::path snapshot_path = snapshot_dir / filename;
710 try {
711 saveSnapshot(snapshot_path.string());
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";
715 }
716 }
717
718 void applyWheelZoom(float wheel_y) {
719 const VkExtent2D extent = getSwapchainExtent();
720 if (extent.width == 0U || extent.height == 0U || window == nullptr) {
721 return;
722 }
723
724 float mouse_x = 0.0f;
725 float mouse_y = 0.0f;
726 SDL_GetMouseState(&mouse_x, &mouse_y);
727
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;
731
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);
734
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;
738
739 center_x += before_x - after_x;
740 center_y += before_y - after_y;
741
742 if (wheel_y > 0.0f && zoom > ReferenceScalar(10)) {
743 max_iterations = std::min(max_iterations + 12, max_reference_iterations);
744 }
745 reference_orbit_dirty = true;
746 }
747
748 void updateKeyboardNavigation() {
749 const bool *keys = SDL_GetKeyboardState(nullptr);
750 if (keys == nullptr) {
751 return;
752 }
753
754 const auto now = std::chrono::steady_clock::now();
755 const ReferenceScalar dt = ReferenceScalar(std::chrono::duration<double>(now - last_tick).count());
756 last_tick = now;
757
758 const ReferenceScalar move_speed = ReferenceScalar("0.85") * minReferenceScalar(dt, ReferenceScalar("0.1")) / zoom;
759 bool moved = false;
760 if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) {
761 center_x -= move_speed;
762 moved = true;
763 }
764 if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) {
765 center_x += move_speed;
766 moved = true;
767 }
768 if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
769 center_y += move_speed;
770 moved = true;
771 }
772 if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
773 center_y -= move_speed;
774 moved = true;
775 }
776 if (keys[SDL_SCANCODE_Z]) {
777 zoom = minReferenceScalar(zoom * (ReferenceScalar(1) + ReferenceScalar("1.9") * dt), MAX_ZOOM);
778 moved = true;
779 }
780 if (keys[SDL_SCANCODE_X]) {
781 zoom = maxReferenceScalar(zoom * (ReferenceScalar(1) - ReferenceScalar("1.9") * dt), ReferenceScalar("0.5"));
782 moved = true;
783 }
784 if (moved) {
785 reference_orbit_dirty = true;
786 }
787 }
788
789 void resetView() {
790 center_x = ReferenceScalar("-0.5");
791 center_y = ReferenceScalar(0);
792 zoom = ReferenceScalar(1);
793 max_iterations = 256;
794 reference_orbit_dirty = true;
795 }
796
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();
809 return;
810 }
811
812 if (fractal_pipeline != VK_NULL_HANDLE) {
813 vkDestroyPipeline(device, fractal_pipeline, nullptr);
814 fractal_pipeline = VK_NULL_HANDLE;
815 }
816 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
817 vkDestroyPipelineLayout(device, fractal_pipeline_layout, nullptr);
818 fractal_pipeline_layout = VK_NULL_HANDLE;
819 }
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();
824 }
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;
828 }
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]);
833 }
834 vkFreeMemory(device, reference_orbit_memories[i], nullptr);
835 }
836 }
837 for (VkBuffer buffer : reference_orbit_buffers) {
838 if (buffer != VK_NULL_HANDLE) {
839 vkDestroyBuffer(device, buffer, nullptr);
840 }
841 }
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();
847 }
848
849 void destroyFractalPipeline() {
850 if (device == VK_NULL_HANDLE) {
851 fractal_pipeline = VK_NULL_HANDLE;
852 fractal_pipeline_layout = VK_NULL_HANDLE;
853 return;
854 }
855 if (fractal_pipeline != VK_NULL_HANDLE) {
856 vkDestroyPipeline(device, fractal_pipeline, nullptr);
857 fractal_pipeline = VK_NULL_HANDLE;
858 }
859 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
860 vkDestroyPipelineLayout(device, fractal_pipeline_layout, nullptr);
861 fractal_pipeline_layout = VK_NULL_HANDLE;
862 }
863 }
864
865 void createFractalPipeline() {
866 if (device == VK_NULL_HANDLE) {
867 return;
868 }
869
870 const VkFormat color_format = getSwapchainFormat();
871 const VkFormat depth_attachment_format = getDepthFormat();
872 if (color_format == VK_FORMAT_UNDEFINED || depth_attachment_format == VK_FORMAT_UNDEFINED) {
873 return;
874 }
875
876 ensureFractalResources();
877 if (fractal_descriptor_set_layout == VK_NULL_HANDLE || fractal_descriptor_sets.empty()) {
878 return;
879 }
880
881 destroyFractalPipeline();
882
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";
887#else
888 shaderRoot + "/fractal.frag.spv";
889#endif
890 const std::vector<char> vert_bytes = loadSpv(vert_path);
891 const std::vector<char> frag_bytes = loadSpv(frag_path);
892
893 const VkShaderModule vert_module = createShaderModule(device, vert_bytes);
894 VkShaderModule frag_module = VK_NULL_HANDLE;
895
896 try {
897 frag_module = createShaderModule(device, frag_bytes);
898
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";
904
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";
910
911 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
912
913 VkPipelineVertexInputStateCreateInfo vertex_input{};
914 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
915
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;
920
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;
925
926 const VkDynamicState dynamic_states[] = {
927 VK_DYNAMIC_STATE_VIEWPORT,
928 VK_DYNAMIC_STATE_SCISSOR,
929 };
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;
934
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;
943
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;
948
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;
956
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;
960
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;
966
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));
971
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;
978
979 if (vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &fractal_pipeline_layout) != VK_SUCCESS) {
980 throw mxvk::Exception("Failed to create fractal pipeline layout");
981 }
982
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;
990
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;
1007
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");
1010 }
1011 } catch (...) {
1012 if (fractal_pipeline != VK_NULL_HANDLE) {
1013 vkDestroyPipeline(device, fractal_pipeline, nullptr);
1014 fractal_pipeline = VK_NULL_HANDLE;
1015 }
1016 if (fractal_pipeline_layout != VK_NULL_HANDLE) {
1017 vkDestroyPipelineLayout(device, fractal_pipeline_layout, nullptr);
1018 fractal_pipeline_layout = VK_NULL_HANDLE;
1019 }
1020 if (frag_module != VK_NULL_HANDLE) {
1021 vkDestroyShaderModule(device, frag_module, nullptr);
1022 }
1023 vkDestroyShaderModule(device, vert_module, nullptr);
1024 throw;
1025 }
1026
1027 vkDestroyShaderModule(device, frag_module, nullptr);
1028 vkDestroyShaderModule(device, vert_module, nullptr);
1029 }
1030
1031 void ensureFractalResources() {
1032 const size_t required_count = std::max<size_t>(getSwapchainImageCount(), 1);
1033 if (reference_orbit_buffers.size() != required_count || fractal_descriptor_sets.size() != required_count) {
1034 destroyFractalResources();
1035 }
1036
1037 if (reference_orbit_buffers.empty()) {
1038 createReferenceOrbitBuffers(required_count);
1039 }
1040 if (fractal_descriptor_set_layout == VK_NULL_HANDLE) {
1041 createDescriptorSetLayout();
1042 }
1043 if (fractal_descriptor_pool == VK_NULL_HANDLE) {
1044 createDescriptorPool();
1045 }
1046 if (fractal_descriptor_sets.empty()) {
1047 allocateDescriptorSets(required_count);
1048 writeDescriptorSets();
1049 }
1050 }
1051
1052 void createReferenceOrbitBuffers(size_t buffer_count) {
1053 const VkDeviceSize buffer_size = static_cast<VkDeviceSize>(reference_orbit_capacity * sizeof(OrbitSample));
1054
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]);
1060 }
1061 vkFreeMemory(device, reference_orbit_memories[i], nullptr);
1062 }
1063 }
1064 for (VkBuffer buffer : reference_orbit_buffers) {
1065 if (buffer != VK_NULL_HANDLE) {
1066 vkDestroyBuffer(device, buffer, nullptr);
1067 }
1068 }
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();
1074 };
1075
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);
1082
1083 try {
1084 for (size_t i = 0; i < buffer_count; ++i) {
1085 try {
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;
1088 } catch (...) {
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;
1091 }
1092
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");
1095 }
1096 }
1097 } catch (...) {
1098 cleanup_reference_orbit_buffers();
1099 throw;
1100 }
1101 }
1102
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;
1109
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;
1114
1115 if (vkCreateDescriptorSetLayout(device, &layout_info, nullptr, &fractal_descriptor_set_layout) != VK_SUCCESS) {
1116 throw mxvk::Exception("Failed to create fractal descriptor set layout");
1117 }
1118 }
1119
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());
1124
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;
1130
1131 if (vkCreateDescriptorPool(device, &pool_info, nullptr, &fractal_descriptor_pool) != VK_SUCCESS) {
1132 throw mxvk::Exception("Failed to create fractal descriptor pool");
1133 }
1134 }
1135
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);
1139
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();
1145
1146 if (vkAllocateDescriptorSets(device, &alloc_info, fractal_descriptor_sets.data()) != VK_SUCCESS) {
1147 throw mxvk::Exception("Failed to allocate fractal descriptor sets");
1148 }
1149 }
1150
1151 void writeDescriptorSets() {
1152 std::vector<VkDescriptorBufferInfo> buffer_infos(fractal_descriptor_sets.size());
1153 std::vector<VkWriteDescriptorSet> writes(fractal_descriptor_sets.size());
1154
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));
1159
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];
1167 }
1168
1169 vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1170 }
1171
1172 void updateReferenceOrbit(uint32_t image_index, VkExtent2D extent) {
1173 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] == nullptr) {
1174 return;
1175 }
1176
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;
1185 orbit_length = 0;
1186 }
1187 uploadReferenceOrbit(image_index);
1188 return;
1189 }
1190
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);
1195 return;
1196 }
1197
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);
1204
1205 std::fill(reference_orbit_samples.begin(), reference_orbit_samples.end(), OrbitSample{});
1206 std::vector<ReferenceTile> tiles;
1207 tiles.reserve(max_adaptive_references);
1208
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);
1215
1216 tiles.push_back({tile_min_x, tile_min_y, tile_max_x, tile_max_y, 0});
1217 }
1218 }
1219
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)) {
1223 refined = false;
1224
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)) {
1228 continue;
1229 }
1230
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}};
1235
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());
1238 refined = true;
1239 break;
1240 }
1241 }
1242
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);
1251 }
1252
1253 reference_orbit_samples[0] = {static_cast<float>(reference_count), static_cast<float>(reference_metadata_capacity), static_cast<float>(reference_orbit_stride), 0.0f};
1254
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);
1261 }
1262
1263 void uploadReferenceOrbit(uint32_t image_index) {
1264 if (image_index >= reference_orbit_mapped.size() || reference_orbit_mapped[image_index] == nullptr) {
1265 return;
1266 }
1267 if (image_index < reference_orbit_uploaded_generations.size() && reference_orbit_uploaded_generations[image_index] == reference_orbit_generation) {
1268 return;
1269 }
1270
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));
1273
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];
1278 range.offset = 0;
1279 range.size = upload_size;
1280 vkFlushMappedMemoryRanges(device, 1, &range);
1281 }
1282
1283 if (image_index < reference_orbit_uploaded_generations.size()) {
1284 reference_orbit_uploaded_generations[image_index] = reference_orbit_generation;
1285 }
1286 }
1287
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);
1294 }
1295
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;
1299
1300 ReferenceScalar z_x = 0;
1301 ReferenceScalar z_y = 0;
1302 int sample_count = 1;
1303
1304 reference_orbit_samples[orbit_base] = {toOrbitSampleScalar(z_x), toOrbitSampleScalar(z_y), 1.0f, 0.0f};
1305
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;
1309
1310 z_x = next_x;
1311 z_y = next_y;
1312
1313 reference_orbit_samples[orbit_base + static_cast<size_t>(sample_count)] = {toOrbitSampleScalar(z_x), toOrbitSampleScalar(z_y), 0.0f, 0.0f};
1314 ++sample_count;
1315
1316 const ReferenceScalar mag2 = z_x * z_x + z_y * z_y;
1317 if (mag2 > ReferenceScalar(4)) {
1318 break;
1319 }
1320 }
1321
1322 reference_orbit_samples[orbit_base].z = static_cast<float>(sample_count);
1323 return sample_count;
1324 }
1325
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)};
1330 }
1331
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")}};
1334
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)) {
1339 return false;
1340 }
1341 }
1342
1343 return true;
1344 }
1345
1346 bool isPerturbationSampleStable(size_t orbit_base, int sample_count, int iteration_count, float delta_c_x, float delta_c_y) const {
1347 float dz_x = 0.0f;
1348 float dz_y = 0.0f;
1349 const int count = std::min(iteration_count, sample_count - 1);
1350
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;
1357
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;
1360
1361 if (!std::isfinite(dz_x) || !std::isfinite(dz_y)) {
1362 return false;
1363 }
1364
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) {
1370 return true;
1371 }
1372
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)) {
1376 return false;
1377 }
1378 }
1379
1380 return sample_count > iteration_count;
1381 }
1382
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;
1389
1390 if (vkCreateBuffer(device, &buffer_info, nullptr, &buffer) != VK_SUCCESS) {
1391 throw mxvk::Exception("Failed to create fractal buffer");
1392 }
1393
1394 VkMemoryRequirements mem_requirements{};
1395 vkGetBufferMemoryRequirements(device, buffer, &mem_requirements);
1396
1397 VkMemoryAllocateInfo alloc_info{};
1398 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1399 alloc_info.allocationSize = mem_requirements.size;
1400 try {
1401 alloc_info.memoryTypeIndex = findMemoryType(mem_requirements.memoryTypeBits, properties);
1402 } catch (...) {
1403 vkDestroyBuffer(device, buffer, nullptr);
1404 buffer = VK_NULL_HANDLE;
1405 throw;
1406 }
1407
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");
1412 }
1413
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");
1420 }
1421 }
1422
1423 uint32_t findMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties) const {
1424 VkPhysicalDeviceMemoryProperties mem_properties{};
1425 vkGetPhysicalDeviceMemoryProperties(physical_device, &mem_properties);
1426
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) {
1431 return i;
1432 }
1433 }
1434
1435 throw mxvk::Exception("Failed to find suitable memory type for fractal buffer");
1436 }
1437
1438 using PushScalar =
1439#if defined(MXVK_USE_MOLTENVK)
1440 float;
1441#else
1442 double;
1443#endif
1444 struct FractalPushConstants {
1445 PushScalar center_x;
1446 PushScalar center_y;
1447 PushScalar inverse_zoom;
1448 PushScalar time;
1449 PushScalar resolution_x;
1450 PushScalar resolution_y;
1451 int max_iterations;
1452 int palette;
1453 int orbit_length;
1454 int reserved;
1455 };
1456
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{};
1467
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);
1481#else
1482 static inline const ReferenceScalar direct_reference_zoom_threshold = ReferenceScalar("1e15");
1483#endif
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{};
1491
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); }
1493
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;
1499
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);
1505
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;
1531#endif
1532 };
1533
1534} // namespace example
1535
1536int main(int argc, char **argv) {
1537 try {
1538 const Arguments args = proc_args(argc, argv);
1539 example::FractalWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
1540 window.loop();
1541 } catch (mxvk::Exception &e) {
1542 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1543 return EXIT_FAILURE;
1544 } catch (ArgException<std::string> &e) {
1545 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1546 return EXIT_FAILURE;
1547 }
1548 return EXIT_SUCCESS;
1549}
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:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
void event(SDL_Event &e) override
Handle one SDL event.
Definition fractal.cpp:69
void proc() override
Execute one processing/update step.
Definition fractal.cpp:120
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
Definition fractal.cpp:144
void render() override
Render one frame.
Definition fractal.cpp:122
~FractalWindow() override
Definition fractal.cpp:59
void onSwapchainAboutToRecreate() override
Called right before swapchain-dependent resources are recreated.
Definition fractal.cpp:132
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:142
std::string text() const
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:222
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
VkDevice device
Definition mxvk.hpp:606
VkFormat swapchain_format
Definition mxvk.hpp:614
uint32_t last_presented_image_index
Definition mxvk.hpp:635
static VkShaderModule createShaderModule(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
Definition mxvk.cpp:154
std::vector< VkImage > swapchain_images
Definition mxvk.hpp:618
std::vector< VkFence > image_fences
Definition mxvk.hpp:633
size_t getSwapchainImageCount() const noexcept
Get the number of swapchain images currently allocated.
Definition mxvk.hpp:239
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
Definition mxvk.cpp:826
std::vector< VkCommandBuffer > command_buffers
Definition mxvk.hpp:628
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:601
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VkCommandPool command_pool
Definition mxvk.hpp:627
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:605
VkFormat getDepthFormat() const noexcept
Get the depth format used for dynamic rendering attachments.
Definition mxvk.hpp:236
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:152
VkQueue graphics_queue
Definition mxvk.hpp:609
virtual void render()
Render one frame.
Definition mxvk.cpp:824
bool swapchain_supports_transfer_src
Definition mxvk.hpp:636
VkFormat getSwapchainFormat() const noexcept
Get the swapchain color format.
Definition mxvk.hpp:213
#define fractal_zoom_ASSET_DIR
Definition fractal.cpp:33
#define MXVK_VALIDATION
Definition mxvk.hpp:28
#define VK_CHECK_RESULT(f)
FFmpeg-based video writer used by MXWrite.
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
std::string tune
Optional tuning mode.
Definition mxwrite.hpp:96
bool realtime
Enable low-latency settings.
Definition mxwrite.hpp:101
int crf
Constant Rate Factor.
Definition mxwrite.hpp:97
bool block_when_full
Pace producers to encoder throughput instead of dropping frames.
Definition mxwrite.hpp:102
std::string preset
Encoder preset name.
Definition mxwrite.hpp:95