MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxvk.cpp
Go to the documentation of this file.
1#include "mxvk/mxvk.hpp"
3#include "mxvk/mxvk_png.hpp"
5#include <SDL3/SDL_oldnames.h>
6#include <SDL3/SDL_video.h>
7#include <SDL3/SDL_vulkan.h>
8#include <algorithm>
9#include <array>
10#include <cstdlib>
11#include <cstring>
12#include <ctime>
13#include <exception>
14#include <filesystem>
15#include <format>
16#include <fstream>
17#include <iomanip>
18#include <iostream>
19#include <iterator>
20#include <optional>
21#include <sstream>
22#include <vector>
23
24#ifndef MXVK_SPRITE_SHADER_DIR
25#define MXVK_SPRITE_SHADER_DIR "."
26#endif
27
28#ifndef MXVK_TEXT_SHADER_DIR
29#define MXVK_TEXT_SHADER_DIR "."
30#endif
31
32#ifndef MXVK_DEFAULT_FONT_DIR
33#define MXVK_DEFAULT_FONT_DIR "."
34#endif
35
36namespace mxvk {
37 namespace {
39 const char *quiet_missing_validation = std::getenv("MXVK_QUIET_MISSING_VALIDATION");
40 return quiet_missing_validation == nullptr || std::strcmp(quiet_missing_validation, "1") != 0;
41 }
42 } // namespace
43
44 VKAPI_ATTR VkBool32 VKAPI_CALL VK_Window::debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT *callback_data, [[maybe_unused]] void *user_data) {
45 const char *message = (callback_data != nullptr && callback_data->pMessage != nullptr)
46 ? callback_data->pMessage
47 : "Unknown Vulkan validation message";
48
49 if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0U) {
50 std::cerr << std::format("vk validation error: {}\n", message);
51 } else if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) != 0U) {
52 std::cerr << std::format("vk validation warning: {}\n", message);
53 } else {
54 std::cout << std::format("vk validation: {}\n", message);
55 }
56 return VK_FALSE;
57 }
58
60 return VulkanContext{
61 .device = getDevice(),
62 .physical_device = getPhysicalDevice(),
63 .graphics_queue = getGraphicsQueue(),
64 .command_pool = getCommandPool(),
65 };
66 }
67
68 VK_Window::SwapchainSupport VK_Window::querySwapchainSupport(VkPhysicalDevice device, VkSurfaceKHR surface) {
69 std::cout << "vk: querying swapchain support details\n";
70 SwapchainSupport support{};
71
72 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &support.capabilities);
73
74 uint32_t format_count = 0;
75 vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, nullptr);
76 if (format_count > 0U) {
77 support.formats.resize(format_count);
78 vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, support.formats.data());
79 }
80
81 uint32_t present_mode_count = 0;
82 vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &present_mode_count, nullptr);
83 if (present_mode_count > 0U) {
84 support.present_modes.resize(present_mode_count);
85 vkGetPhysicalDeviceSurfacePresentModesKHR(
86 device,
87 surface,
88 &present_mode_count,
89 support.present_modes.data());
90 }
91
92 return support;
93 }
94
95 VkSurfaceFormatKHR VK_Window::chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR> &available_formats) {
96 for (const VkSurfaceFormatKHR &format : available_formats) {
97 if (format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
98 return format;
99 }
100 }
101
102 return available_formats.front();
103 }
104
105 VkPresentModeKHR VK_Window::choosePresentMode(const std::vector<VkPresentModeKHR> &available_present_modes) const {
107 return VK_PRESENT_MODE_FIFO_KHR;
108 }
109
110 for (const VkPresentModeKHR present_mode : available_present_modes) {
111 if (present_mode == VK_PRESENT_MODE_MAILBOX_KHR) {
112 return present_mode;
113 }
114 }
115
116 return VK_PRESENT_MODE_FIFO_KHR;
117 }
118
119 VkExtent2D VK_Window::chooseExtent(const VkSurfaceCapabilitiesKHR &capabilities, SDL_Window *window) {
120 if (capabilities.currentExtent.width != UINT32_MAX) {
121 return capabilities.currentExtent;
122 }
123
124 int width = 1;
125 int height = 1;
126 SDL_GetWindowSizeInPixels(window, &width, &height);
127
128 VkExtent2D actual_extent{};
129 actual_extent.width = std::clamp(
130 static_cast<uint32_t>(std::max(width, 1)),
131 capabilities.minImageExtent.width,
132 capabilities.maxImageExtent.width);
133 actual_extent.height = std::clamp(
134 static_cast<uint32_t>(std::max(height, 1)),
135 capabilities.minImageExtent.height,
136 capabilities.maxImageExtent.height);
137
138 return actual_extent;
139 }
140
141 std::vector<char> VK_Window::loadSpv(const std::string &path) {
142 return mxvk::load_spv(path);
143 }
144
145 VkShaderModule VK_Window::createShaderModule(VkDevice device, const std::vector<char> &spv_bytes) {
146 return mxvk::create_shader_module(device, spv_bytes);
147 }
148
149 std::string VK_Window::resolveRuntimeShaderPath(const std::string &shaderFileName, const char *fallbackDir) const {
150 if (shaderFileName.empty()) {
151 throw mxvk::Exception("Shader file name is empty");
152 }
153
154 std::vector<std::filesystem::path> candidates{};
155
156 if (const char *basePath = SDL_GetBasePath(); basePath != nullptr) {
157 const std::filesystem::path executableDir(basePath);
158 candidates.push_back(executableDir / "data" / shaderFileName);
159 candidates.push_back(executableDir / shaderFileName);
160 }
161
162 candidates.push_back(std::filesystem::path("data") / shaderFileName);
163
164 if (!font_path.empty()) {
165 const std::filesystem::path fontPath(font_path);
166 if (fontPath.has_parent_path()) {
167 candidates.push_back(fontPath.parent_path() / shaderFileName);
168 }
169 }
170
171 if (fallbackDir != nullptr && fallbackDir[0] != '\0') {
172 candidates.push_back(std::filesystem::path(fallbackDir) / shaderFileName);
173 }
174
175 std::error_code existsError{};
176 for (const std::filesystem::path &candidate : candidates) {
177 if (std::filesystem::exists(candidate, existsError)) {
178 return candidate.string();
179 }
180 existsError.clear();
181 }
182
183 throw mxvk::Exception(std::format("Failed to locate shader file '{}'", shaderFileName));
184 }
185
186 VK_Window::VK_Window(const std::string &title, int width, int height, bool full, bool validiation, PresentModePreference presentModePreference) : present_mode_preference(presentModePreference) {
189 std::cout << std::format("mxvk: starting VK_Window construction (title='{}', width={}, height={}, fullscreen={}, validation={})\n", title, width, height, full, validiation);
190 SDL_WindowFlags flags = static_cast<SDL_WindowFlags>(SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
191 if (full) {
192 std::cout << "SDL3: enabling fullscreen window flag\n";
193 flags = static_cast<SDL_WindowFlags>(flags | SDL_WINDOW_FULLSCREEN);
194 }
195
196 std::cout << "mxvk: initializing window subsystem and creating SDL window\n";
197 if (!initWindow(title, width, height, flags)) {
198 throw mxvk::Exception("Error on init of Window");
199 }
200 showCursor(!full);
201
202 std::cout << "mxvk: initializing Vulkan runtime and rendering resources\n";
203 if (!initVulkan(validiation)) {
204 release();
205 throw mxvk::Exception("Error on init of Vulkan");
206 }
207 std::cout << "mxvk: VK_Window construction complete\n";
208 }
209
210 VK_Window::VK_Window(const std::string &title, int width, int height, bool full, bool validiation, bool enableVsync)
211 : VK_Window(title, width, height, full, validiation, enableVsync ? PresentModePreference::Vsync : PresentModePreference::LowLatency) {}
212
214 std::cout << "mxvk: destructor invoked, releasing resources\n";
215 release();
216 }
217
219 std::cout << "mxvk: starting resource teardown\n";
220 stopScreenshotWorker();
221
222 if (device != VK_NULL_HANDLE) {
223 std::cout << "vk: waiting for device idle before teardown\n";
224 vkDeviceWaitIdle(device);
225 }
226
227 post_process_sprite = nullptr;
229 post_process_sprites.clear();
234
235 if (!sprites.empty()) {
236 std::cout << std::format("vk: releasing {} sprite(s)\n", sprites.size());
237 sprites.clear();
238 }
239 if (!sprites3d.empty()) {
240 std::cout << std::format("vk: releasing {} 3D sprite batch(es)\n", sprites3d.size());
241 sprites3d.clear();
242 }
243 sprite_state_dirty = false;
244 destroySpritePipeline();
245 if (sprite_descriptor_set_layout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
246 vkDestroyDescriptorSetLayout(device, sprite_descriptor_set_layout, nullptr);
247 sprite_descriptor_set_layout = VK_NULL_HANDLE;
248 }
249
250 if (text_renderer) {
251 text_renderer.reset();
252 }
253 text_state_dirty = false;
254 destroyTextPipeline();
255 if (text_descriptor_set_layout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
256 vkDestroyDescriptorSetLayout(device, text_descriptor_set_layout, nullptr);
257 text_descriptor_set_layout = VK_NULL_HANDLE;
258 }
259
260 cleanupSyncObjects();
261
262 if (!retired_swapchains.empty()) {
263 for (VkSwapchainKHR retired : retired_swapchains) {
264 vkDestroySwapchainKHR(device, retired, nullptr);
265 }
266 retired_swapchains.clear();
267 }
268 std::cout << "vk: tearing down swapchain-dependent resources\n";
269 cleanupSwapchain(true);
270
271 if (command_pool != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
272 std::cout << "vk: destroying command pool\n";
273 vkDestroyCommandPool(device, command_pool, nullptr);
274 command_pool = VK_NULL_HANDLE;
275 }
276
277 if (device != VK_NULL_HANDLE) {
278 savePipelineCache();
279 destroyPipelineCache();
280 std::cout << "vk: destroying logical device\n";
281 vkDestroyDevice(device, nullptr);
282 device = VK_NULL_HANDLE;
283 graphics_queue = VK_NULL_HANDLE;
284 present_queue = VK_NULL_HANDLE;
285 }
286
287 graphics_queue_family = invalid_queue_index;
288 present_queue_family = invalid_queue_index;
289 physical_device = VK_NULL_HANDLE;
290
291 if (surface != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) {
292 std::cout << "vk: destroying presentation surface\n";
293 vkDestroySurfaceKHR(instance, surface, nullptr);
294 surface = VK_NULL_HANDLE;
295 }
296
297 cleanupDebugMessenger();
298
299 if (instance != VK_NULL_HANDLE) {
300 std::cout << "vk: destroying Vulkan instance\n";
301 vkDestroyInstance(instance, nullptr);
302 instance = VK_NULL_HANDLE;
303 }
304
305 std::cout << "SDL3: destroying SDL window handle\n";
306 window.reset();
307
308 if (sdl_initialized) {
309 std::cout << "SDL3: shutting down SDL video/gamepad subsystems\n";
310 SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD);
311 sdl_initialized = false;
312 }
313
314 active = false;
315 std::cout << "mxvk: resource teardown complete\n";
316 }
317
318 bool VK_Window::initVulkan(bool validiation) {
319 std::cout << std::format("mxvk: entering initVulkan (validation={})\n", validiation);
320 validation_enabled = validiation;
321
322 if (window == nullptr) {
323 std::cerr << "mxvk: Cannot initialize Vulkan without an SDL window\n";
324 return false;
325 }
326
327 if (instance != VK_NULL_HANDLE) {
328 std::cout << "vk: instance already initialized; skipping initVulkan\n";
329 return true;
330 }
331
332 std::cout << "vk: initializing volk loader\n";
333 if (volkInitialize() != VK_SUCCESS) {
334 std::cerr << "mxvk: Failed to initialize volk\n";
335 return false;
336 }
337
338 unsigned int extension_count = 0;
339 std::cout << "SDL3: querying Vulkan instance extensions required by SDL\n";
340 const char *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extension_count);
341 if (extensions == nullptr || extension_count == 0U) {
342 std::cerr << std::format("mxvk: Failed to get Vulkan instance extensions: {}\n", SDL_GetError());
343 return false;
344 }
345 std::cout << std::format("vk: SDL provided {} required instance extension(s)\n", extension_count);
346
347 std::vector<const char *> enabled_extensions(extensions, extensions + extension_count);
348
349#if defined(MXVK_USE_MOLTENVK)
350 const auto append_instance_extension_if_missing = [&enabled_extensions](const char *extension_name) {
351 const bool exists = std::ranges::any_of(
352 enabled_extensions,
353 [extension_name](const char *existing) { return std::strcmp(existing, extension_name) == 0; });
354 if (!exists) {
355 enabled_extensions.push_back(extension_name);
356 }
357 };
358 append_instance_extension_if_missing(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
359 append_instance_extension_if_missing(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
360#endif
361
362 std::vector<const char *> enabled_layers{};
363 VkDebugUtilsMessengerCreateInfoEXT debug_create_info{};
364 if (validation_enabled) {
365 if (!hasValidationLayerSupport()) {
366 if (shouldLogMissingValidationLayer()) {
367 std::cerr << std::format(
368 "mxvk: validation layer '{}' is not available; continuing without validation\n",
369 validation_layer_name);
370 }
371 validation_enabled = false;
372 } else {
373 enabled_layers.push_back(validation_layer_name);
374 enabled_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
375 const std::optional<VkDebugUtilsMessengerCreateInfoEXT> maybe_debug_create_info =
376 makeDebugMessengerCreateInfo();
377 if (!maybe_debug_create_info.has_value()) {
378 std::cerr << "mxvk: failed to construct debug messenger create info\n";
379 validation_enabled = false;
380 enabled_layers.clear();
381 } else {
382 debug_create_info = maybe_debug_create_info.value();
383 }
384 }
385 }
386
387 VkApplicationInfo app_info{};
388 app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
389 app_info.pApplicationName = "mxvk";
390 app_info.applicationVersion = VK_MAKE_VERSION(0, 1, 0);
391 app_info.pEngineName = "mxvk";
392 app_info.engineVersion = VK_MAKE_VERSION(MXVK_VERSION_CODE_MAJOR, MXVK_VERSION_CODE_MINOR, MXVK_VERSION_CODE_PATCH);
393 app_info.apiVersion = VK_API_VERSION_1_4;
394
395 VkInstanceCreateInfo create_info{};
396 create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
397 create_info.pApplicationInfo = &app_info;
398 create_info.enabledExtensionCount = extension_count;
399 create_info.ppEnabledExtensionNames = enabled_extensions.data();
400 create_info.enabledLayerCount = static_cast<uint32_t>(enabled_layers.size());
401 create_info.ppEnabledLayerNames = enabled_layers.empty() ? nullptr : enabled_layers.data();
402 create_info.pNext = validation_enabled ? &debug_create_info : nullptr;
403
404 create_info.enabledExtensionCount = static_cast<uint32_t>(enabled_extensions.size());
405#if defined(MXVK_USE_MOLTENVK)
406 create_info.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
407#endif
408
409 std::cout << "vk: creating Vulkan instance\n";
410 if (vkCreateInstance(&create_info, nullptr, &instance) != VK_SUCCESS) {
411 std::cerr << "mxvk: Failed to create Vulkan instance\n";
412 instance = VK_NULL_HANDLE;
413 return false;
414 }
415
416 std::cout << "vk: loading Vulkan instance function pointers via volk\n";
417 volkLoadInstance(instance);
418
419 setupDebugMessenger();
420
421 uint32_t instanceVersion = 0;
422 if (vkEnumerateInstanceVersion != nullptr) {
423 vkEnumerateInstanceVersion(&instanceVersion);
424 }
425 std::cout << "vk: Vulkan instance version: "
426 << VK_VERSION_MAJOR(instanceVersion) << "."
427 << VK_VERSION_MINOR(instanceVersion) << "."
428 << VK_VERSION_PATCH(instanceVersion) << "\n";
429 std::cout << "mxvk: engine version: "
430 << MXVK_VERSION_CODE_MAJOR << "."
431 << MXVK_VERSION_CODE_MINOR << "."
432 << MXVK_VERSION_CODE_PATCH << "\n";
433
434 std::cout << "SDL3: creating Vulkan presentation surface from SDL window\n";
435 if (!SDL_Vulkan_CreateSurface(window.get(), instance, nullptr, &surface)) {
436 std::cerr << std::format("mxvk: Failed to create Vulkan surface: {}\n", SDL_GetError());
437 cleanupDebugMessenger();
438 vkDestroyInstance(instance, nullptr);
439 instance = VK_NULL_HANDLE;
440 surface = VK_NULL_HANDLE;
441 return false;
442 }
443
444 std::cout << "mxvk: selecting suitable physical device\n";
445 pickDevice();
446 if (physical_device == VK_NULL_HANDLE) {
447 std::cerr << "mxvk: Failed to find a Vulkan physical device with present support\n";
448 return false;
449 }
450
451 std::cout << "mxvk: creating logical device and queues\n";
453 if (device == VK_NULL_HANDLE) {
454 std::cerr << "mxvk: Failed to create Vulkan logical device\n";
455 return false;
456 }
457 createPipelineCache();
458
459 std::cout << "mxvk: deferring swapchain/render/sync resource creation until first frame\n";
460
461 std::cout << "mxvk: initVulkan complete\n";
462 return true;
463 }
464
465 std::string VK_Window::pipelineCachePath() const {
466 if (physical_device == VK_NULL_HANDLE) {
467 return {};
468 }
469
470 VkPhysicalDeviceProperties properties{};
471 vkGetPhysicalDeviceProperties(physical_device, &properties);
472
473 char *pref_path = SDL_GetPrefPath("mxvk", "MXVK");
474 std::filesystem::path base_path;
475 if (pref_path != nullptr) {
476 base_path = pref_path;
477 SDL_free(pref_path);
478 } else {
479 base_path = std::filesystem::current_path();
480 }
481
482 std::error_code ec;
483 std::filesystem::create_directories(base_path, ec);
484 if (ec) {
485 return {};
486 }
487
488 std::ostringstream filename;
489 filename << "pipeline_cache_"
490 << std::hex << std::setfill('0')
491 << properties.vendorID << '_'
492 << properties.deviceID << '_'
493 << properties.driverVersion << '_';
494 for (uint8_t byte : properties.pipelineCacheUUID) {
495 filename << std::setw(2) << static_cast<unsigned>(byte);
496 }
497 filename << ".bin";
498
499 return (base_path / filename.str()).string();
500 }
501
502 void VK_Window::createPipelineCache() {
503 if (device == VK_NULL_HANDLE || pipeline_cache != VK_NULL_HANDLE) {
504 return;
505 }
506
507 const std::string cache_path = pipelineCachePath();
508 std::vector<char> initial_data{};
509 if (!cache_path.empty() && std::filesystem::exists(cache_path)) {
510 std::ifstream file(cache_path, std::ios::binary);
511 if (file) {
512 initial_data.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
513 }
514 }
515
516 VkPipelineCacheCreateInfo create_info{};
517 create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
518 create_info.initialDataSize = initial_data.size();
519 create_info.pInitialData = initial_data.empty() ? nullptr : initial_data.data();
520
521 VkResult result = vkCreatePipelineCache(device, &create_info, nullptr, &pipeline_cache);
522 if (result != VK_SUCCESS && !initial_data.empty()) {
523 std::cerr << "vk: cached pipeline data rejected; creating an empty pipeline cache\n";
524 create_info.initialDataSize = 0;
525 create_info.pInitialData = nullptr;
526 result = vkCreatePipelineCache(device, &create_info, nullptr, &pipeline_cache);
527 }
528
529 if (result != VK_SUCCESS) {
530 pipeline_cache = VK_NULL_HANDLE;
531 std::cerr << std::format("vk: failed to create pipeline cache ({})\n", static_cast<int>(result));
532 return;
533 }
534
535 if (!initial_data.empty()) {
536 std::cout << std::format("vk: loaded pipeline cache: {} bytes\n", initial_data.size());
537 }
538 }
539
540 void VK_Window::savePipelineCache() const {
541 if (device == VK_NULL_HANDLE || pipeline_cache == VK_NULL_HANDLE) {
542 return;
543 }
544
545 size_t data_size = 0;
546 VkResult result = vkGetPipelineCacheData(device, pipeline_cache, &data_size, nullptr);
547 if (result != VK_SUCCESS || data_size == 0) {
548 return;
549 }
550
551 std::vector<char> data(data_size);
552 result = vkGetPipelineCacheData(device, pipeline_cache, &data_size, data.data());
553 if (result != VK_SUCCESS || data_size == 0) {
554 return;
555 }
556 data.resize(data_size);
557
558 const std::string cache_path = pipelineCachePath();
559 if (cache_path.empty()) {
560 return;
561 }
562
563 std::ofstream file(cache_path, std::ios::binary | std::ios::trunc);
564 if (!file) {
565 std::cerr << std::format("vk: failed to open pipeline cache for writing: {}\n", cache_path);
566 return;
567 }
568
569 file.write(data.data(), static_cast<std::streamsize>(data.size()));
570 if (file) {
571 std::cout << std::format("vk: saved pipeline cache: {} bytes\n", data.size());
572 }
573 }
574
575 void VK_Window::destroyPipelineCache() {
576 if (device != VK_NULL_HANDLE && pipeline_cache != VK_NULL_HANDLE) {
577 vkDestroyPipelineCache(device, pipeline_cache, nullptr);
578 }
579 pipeline_cache = VK_NULL_HANDLE;
580 }
581
582 void VK_Window::event(SDL_Event &e) {
583 switch (e.type) {
584 case SDL_EVENT_KEY_DOWN:
585 switch (e.key.key) {
586 case SDLK_ESCAPE:
587 active = false;
588 break;
589 }
590 break;
591 }
592 }
593 void VK_Window::setClearColor(float r, float g, float b, float a) {
594 clear_color.float32[0] = std::clamp(r, 0.0f, 1.0f);
595 clear_color.float32[1] = std::clamp(g, 0.0f, 1.0f);
596 clear_color.float32[2] = std::clamp(b, 0.0f, 1.0f);
597 clear_color.float32[3] = std::clamp(a, 0.0f, 1.0f);
598 }
599
601 SDL_Event e;
602 active = true;
603 while (active) {
604 while (SDL_PollEvent(&e)) {
605 switch (e.type) {
606 case SDL_EVENT_QUIT:
607 active = false;
608 break;
609 case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
610 if (window != nullptr) {
611 framebuffer_resized = true;
612 last_resize_event_ms = SDL_GetTicks();
614 }
615 break;
616 case SDL_EVENT_KEY_DOWN:
617 if ((e.key.key == SDLK_F12 || e.key.scancode == SDL_SCANCODE_F12) && !e.key.repeat) {
618 toggleFpsCounter();
619 }
620 if (screenshot_enabled &&
621 (e.key.key == SDLK_F10 || e.key.scancode == SDL_SCANCODE_F10) &&
622 !e.key.repeat) {
623 try {
624 saveScreenshot();
625 } catch (const mxvk::Exception &ex) {
626 std::cerr << std::format("mxvk: screenshot failed: {}\n", ex.text());
627 } catch (const std::exception &ex) {
628 std::cerr << std::format("mxvk: screenshot failed: {}\n", ex.what());
629 }
630 continue;
631 }
632 break;
633 default:
634 break;
635 }
636 event(e);
637 }
639 const uint64_t now_ms = SDL_GetTicks();
643 proc();
644 render();
645 SDL_Delay(1);
646 continue;
647 }
648 recreateSwapchain();
649 }
650 proc();
651 render();
652 maybeTrimMemory();
653 }
654 }
655
656 void VK_Window::saveScreenshot() {
657 const std::string path = makeScreenshotPath();
658 std::vector<std::uint8_t> rgba{};
659 uint32_t width = 0;
660 uint32_t height = 0;
661 captureSnapshotPixels(rgba, width, height);
662 enqueueScreenshotSave(path, std::move(rgba), width, height);
663 std::cout << std::format("mxvk: screenshot queued: {}\n", path);
664 }
665
666 void VK_Window::enqueueScreenshotSave(std::string path, std::vector<std::uint8_t> rgba, uint32_t width, uint32_t height) {
667 if (path.empty()) {
668 throw mxvk::Exception("enqueueScreenshotSave requires a non-empty output path");
669 }
670 if (rgba.empty() || width == 0U || height == 0U) {
671 throw mxvk::Exception("enqueueScreenshotSave requires non-empty pixel data");
672 }
673
674 startScreenshotWorker();
675
676 {
677 std::lock_guard<std::mutex> lock(screenshot_queue_mutex);
679 .path = std::move(path),
680 .rgba = std::move(rgba),
681 .width = width,
682 .height = height,
683 });
684 }
685 screenshot_queue_cv.notify_one();
686 }
687
688 void VK_Window::startScreenshotWorker() {
689 std::lock_guard<std::mutex> lock(screenshot_queue_mutex);
690 if (screenshot_worker.joinable()) {
691 return;
692 }
693
695 screenshot_worker = std::thread(&VK_Window::screenshotWorkerLoop, this);
696 }
697
698 void VK_Window::stopScreenshotWorker() {
699 {
700 std::lock_guard<std::mutex> lock(screenshot_queue_mutex);
702 }
703 screenshot_queue_cv.notify_one();
704
705 if (screenshot_worker.joinable()) {
706 screenshot_worker.join();
707 }
708
710 }
711
712 void VK_Window::screenshotWorkerLoop() {
713 while (true) {
714 ScreenshotSaveTask task{};
715 {
716 std::unique_lock<std::mutex> lock(screenshot_queue_mutex);
717 screenshot_queue_cv.wait(lock, [&]() {
719 });
720
722 return;
723 }
724
725 task = std::move(screenshot_save_queue.front());
726 screenshot_save_queue.pop_front();
727 }
728
729 if (!mxvk::SavePNG_RGBA(task.path.c_str(),
730 task.rgba.data(),
731 static_cast<int>(task.width),
732 static_cast<int>(task.height))) {
733 std::cerr << std::format("mxvk: screenshot failed to write PNG: {}\n", task.path);
734 continue;
735 }
736 std::cout << std::format("mxvk: screenshot saved: {}\n", task.path);
737 }
738 }
739
740 std::string VK_Window::makeScreenshotPath() {
741 const char *home = std::getenv("HOME");
742 std::filesystem::path pictures_dir = (home != nullptr && home[0] != '\0')
743 ? std::filesystem::path(home) / "Pictures"
744 : std::filesystem::path("Pictures");
745 std::filesystem::create_directories(pictures_dir);
746
747 const std::time_t now = std::time(nullptr);
748 std::tm local_time{};
749#if defined(_WIN32)
750 localtime_s(&local_time, &now);
751#else
752 localtime_r(&now, &local_time);
753#endif
754
755 std::ostringstream date_stream;
756 date_stream << std::put_time(&local_time, "%Y.%m.%d");
757 std::ostringstream time_stream;
758 time_stream << std::put_time(&local_time, "%H.%M.%S");
759
760 const std::string prefix = std::format("{}.screenshot.{}.{}.{}x{}-",
761 screenshot_prefix.empty() ? "mxvk" : screenshot_prefix,
762 date_stream.str(),
763 time_stream.str(),
764 swapchain_extent.width,
765 swapchain_extent.height);
766
767 for (uint32_t attempt = 0; attempt < 10000U; ++attempt) {
768 const uint32_t index = screenshot_index++;
769 std::filesystem::path path = pictures_dir / std::format("{}{}.png", prefix, index);
770 if (!std::filesystem::exists(path)) {
771 return path.string();
772 }
773 }
774
775 return (pictures_dir / std::format("{}{}.png", prefix, screenshot_index++)).string();
776 }
777
779 drawFrame();
780 }
781
782 void VK_Window::saveSnapshot(const std::string &path) {
783 if (path.empty()) {
784 throw mxvk::Exception("saveSnapshot requires a non-empty output path");
785 }
786
787 std::vector<std::uint8_t> rgba{};
788 uint32_t width = 0;
789 uint32_t height = 0;
790 captureSnapshotPixels(rgba, width, height);
791
792 if (!mxvk::SavePNG_RGBA(path.c_str(),
793 rgba.data(),
794 static_cast<int>(width),
795 static_cast<int>(height))) {
796 throw mxvk::Exception("saveSnapshot failed to write PNG: " + path);
797 }
798 }
799
800 void VK_Window::captureSnapshotPixels(std::vector<std::uint8_t> &rgba_pixels, uint32_t &width, uint32_t &height) {
801 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || graphics_queue == VK_NULL_HANDLE) {
802 throw mxvk::Exception("captureSnapshotPixels called before Vulkan render resources are ready");
803 }
805 throw mxvk::Exception("captureSnapshotPixels requires swapchain transfer-source support");
806 }
807 if (last_presented_image_index == invalid_queue_index ||
810 throw mxvk::Exception("captureSnapshotPixels called before a frame has been presented");
811 }
812 if (swapchain_extent.width == 0U || swapchain_extent.height == 0U) {
813 throw mxvk::Exception("captureSnapshotPixels cannot capture an empty swapchain extent");
814 }
815
816 const bool format_is_bgra =
817 swapchain_format == VK_FORMAT_B8G8R8A8_UNORM ||
818 swapchain_format == VK_FORMAT_B8G8R8A8_SRGB;
819 const bool format_is_rgba =
820 swapchain_format == VK_FORMAT_R8G8B8A8_UNORM ||
821 swapchain_format == VK_FORMAT_R8G8B8A8_SRGB;
822 if (!format_is_bgra && !format_is_rgba) {
823 throw mxvk::Exception(std::format("captureSnapshotPixels unsupported swapchain format: {}", static_cast<int>(swapchain_format)));
824 }
825
826 const VkDeviceSize row_bytes = static_cast<VkDeviceSize>(swapchain_extent.width) * 4U;
827 const VkDeviceSize image_bytes = row_bytes * static_cast<VkDeviceSize>(swapchain_extent.height);
828
829 VkBuffer readback_buffer = VK_NULL_HANDLE;
830 VkDeviceMemory readback_memory = VK_NULL_HANDLE;
831 VkCommandBuffer command_buffer = VK_NULL_HANDLE;
832 VkFence copy_fence = VK_NULL_HANDLE;
833
834 auto cleanup = [&]() {
835 if (copy_fence != VK_NULL_HANDLE) {
836 vkDestroyFence(device, copy_fence, nullptr);
837 }
838 if (command_buffer != VK_NULL_HANDLE) {
839 vkFreeCommandBuffers(device, command_pool, 1, &command_buffer);
840 }
841 if (readback_buffer != VK_NULL_HANDLE) {
842 vkDestroyBuffer(device, readback_buffer, nullptr);
843 }
844 if (readback_memory != VK_NULL_HANDLE) {
845 vkFreeMemory(device, readback_memory, nullptr);
846 }
847 };
848
849 try {
850 VkBufferCreateInfo buffer_info{};
851 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
852 buffer_info.size = image_bytes;
853 buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
854 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
855 if (vkCreateBuffer(device, &buffer_info, nullptr, &readback_buffer) != VK_SUCCESS) {
856 throw mxvk::Exception("captureSnapshotPixels failed to create readback buffer");
857 }
858
859 VkMemoryRequirements memory_requirements{};
860 vkGetBufferMemoryRequirements(device, readback_buffer, &memory_requirements);
861
862 VkPhysicalDeviceMemoryProperties memory_properties{};
863 vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties);
864 uint32_t memory_type_index = invalid_queue_index;
865 constexpr VkMemoryPropertyFlags required_properties =
866 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
867 for (uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) {
868 const bool type_matches = (memory_requirements.memoryTypeBits & (1U << i)) != 0U;
869 const bool properties_match =
870 (memory_properties.memoryTypes[i].propertyFlags & required_properties) == required_properties;
871 if (type_matches && properties_match) {
872 memory_type_index = i;
873 break;
874 }
875 }
876 if (memory_type_index == invalid_queue_index) {
877 throw mxvk::Exception("captureSnapshotPixels failed to find host-visible coherent memory");
878 }
879
880 VkMemoryAllocateInfo allocation_info{};
881 allocation_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
882 allocation_info.allocationSize = memory_requirements.size;
883 allocation_info.memoryTypeIndex = memory_type_index;
884 if (vkAllocateMemory(device, &allocation_info, nullptr, &readback_memory) != VK_SUCCESS) {
885 throw mxvk::Exception("captureSnapshotPixels failed to allocate readback memory");
886 }
887 if (vkBindBufferMemory(device, readback_buffer, readback_memory, 0) != VK_SUCCESS) {
888 throw mxvk::Exception("captureSnapshotPixels failed to bind readback memory");
889 }
890
891 VkCommandBufferAllocateInfo command_buffer_info{};
892 command_buffer_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
893 command_buffer_info.commandPool = command_pool;
894 command_buffer_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
895 command_buffer_info.commandBufferCount = 1;
896 if (vkAllocateCommandBuffers(device, &command_buffer_info, &command_buffer) != VK_SUCCESS) {
897 throw mxvk::Exception("captureSnapshotPixels failed to allocate command buffer");
898 }
899
900 VkFenceCreateInfo fence_info{};
901 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
902 if (vkCreateFence(device, &fence_info, nullptr, &copy_fence) != VK_SUCCESS) {
903 throw mxvk::Exception("captureSnapshotPixels failed to create copy fence");
904 }
905
906 VkFence source_fence = image_fences[last_presented_image_index];
907 if (source_fence != VK_NULL_HANDLE) {
908 const VkResult wait_result = vkWaitForFences(device, 1, &source_fence, VK_TRUE, UINT64_MAX);
909 if (wait_result != VK_SUCCESS) {
910 throw mxvk::Exception(std::format("captureSnapshotPixels failed waiting for rendered frame: {}", static_cast<int>(wait_result)));
911 }
912 }
913
914 const VkResult idle_result = vkDeviceWaitIdle(device);
915 if (idle_result != VK_SUCCESS) {
916 throw mxvk::Exception(std::format("captureSnapshotPixels failed waiting for device idle: {}", static_cast<int>(idle_result)));
917 }
918
919 VkCommandBufferBeginInfo begin_info{};
920 begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
921 begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
922 if (vkBeginCommandBuffer(command_buffer, &begin_info) != VK_SUCCESS) {
923 throw mxvk::Exception("captureSnapshotPixels failed to begin copy command buffer");
924 }
925
926 VkImageMemoryBarrier2 to_transfer_barrier{};
927 to_transfer_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
928 to_transfer_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
929 to_transfer_barrier.srcAccessMask = VK_ACCESS_2_NONE;
930 to_transfer_barrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
931 to_transfer_barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
932 to_transfer_barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
933 to_transfer_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
934 to_transfer_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
935 to_transfer_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
936 to_transfer_barrier.image = swapchain_images[last_presented_image_index];
937 to_transfer_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
938 to_transfer_barrier.subresourceRange.baseMipLevel = 0;
939 to_transfer_barrier.subresourceRange.levelCount = 1;
940 to_transfer_barrier.subresourceRange.baseArrayLayer = 0;
941 to_transfer_barrier.subresourceRange.layerCount = 1;
942
943 VkDependencyInfo to_transfer_dependency{};
944 to_transfer_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
945 to_transfer_dependency.imageMemoryBarrierCount = 1;
946 to_transfer_dependency.pImageMemoryBarriers = &to_transfer_barrier;
947 vkCmdPipelineBarrier2(command_buffer, &to_transfer_dependency);
948
949 VkBufferImageCopy copy_region{};
950 copy_region.bufferOffset = 0;
951 copy_region.bufferRowLength = 0;
952 copy_region.bufferImageHeight = 0;
953 copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
954 copy_region.imageSubresource.mipLevel = 0;
955 copy_region.imageSubresource.baseArrayLayer = 0;
956 copy_region.imageSubresource.layerCount = 1;
957 copy_region.imageOffset = {0, 0, 0};
958 copy_region.imageExtent = {swapchain_extent.width, swapchain_extent.height, 1};
959 vkCmdCopyImageToBuffer(command_buffer,
961 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
962 readback_buffer,
963 1,
964 &copy_region);
965
966 VkImageMemoryBarrier2 to_present_barrier{};
967 to_present_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
968 to_present_barrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
969 to_present_barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
970 to_present_barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
971 to_present_barrier.dstAccessMask = VK_ACCESS_2_NONE;
972 to_present_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
973 to_present_barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
974 to_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
975 to_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
976 to_present_barrier.image = swapchain_images[last_presented_image_index];
977 to_present_barrier.subresourceRange = to_transfer_barrier.subresourceRange;
978
979 VkDependencyInfo to_present_dependency{};
980 to_present_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
981 to_present_dependency.imageMemoryBarrierCount = 1;
982 to_present_dependency.pImageMemoryBarriers = &to_present_barrier;
983 vkCmdPipelineBarrier2(command_buffer, &to_present_dependency);
984
985 if (vkEndCommandBuffer(command_buffer) != VK_SUCCESS) {
986 throw mxvk::Exception("captureSnapshotPixels failed to end copy command buffer");
987 }
988
989 VkCommandBufferSubmitInfo command_submit_info{};
990 command_submit_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
991 command_submit_info.commandBuffer = command_buffer;
992
993 VkSubmitInfo2 submit_info{};
994 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
995 submit_info.commandBufferInfoCount = 1;
996 submit_info.pCommandBufferInfos = &command_submit_info;
997
998 const VkResult submit_result = vkQueueSubmit2(graphics_queue, 1, &submit_info, copy_fence);
999 if (submit_result != VK_SUCCESS) {
1000 throw mxvk::Exception(std::format("captureSnapshotPixels failed to submit copy command: {}", static_cast<int>(submit_result)));
1001 }
1002 const VkResult copy_wait_result = vkWaitForFences(device, 1, &copy_fence, VK_TRUE, UINT64_MAX);
1003 if (copy_wait_result != VK_SUCCESS) {
1004 throw mxvk::Exception(std::format("captureSnapshotPixels failed waiting for copy: {}", static_cast<int>(copy_wait_result)));
1005 }
1006
1007 void *mapped = nullptr;
1008 if (vkMapMemory(device, readback_memory, 0, image_bytes, 0, &mapped) != VK_SUCCESS) {
1009 throw mxvk::Exception("captureSnapshotPixels failed to map readback memory");
1010 }
1011
1012 const auto *src = static_cast<const std::uint8_t *>(mapped);
1013 rgba_pixels.resize(static_cast<std::size_t>(image_bytes));
1014 for (std::size_t i = 0; i < rgba_pixels.size(); i += 4U) {
1015 if (format_is_bgra) {
1016 rgba_pixels[i + 0U] = src[i + 2U];
1017 rgba_pixels[i + 1U] = src[i + 1U];
1018 rgba_pixels[i + 2U] = src[i + 0U];
1019 rgba_pixels[i + 3U] = src[i + 3U];
1020 } else {
1021 rgba_pixels[i + 0U] = src[i + 0U];
1022 rgba_pixels[i + 1U] = src[i + 1U];
1023 rgba_pixels[i + 2U] = src[i + 2U];
1024 rgba_pixels[i + 3U] = src[i + 3U];
1025 }
1026 }
1027 vkUnmapMemory(device, readback_memory);
1028 width = swapchain_extent.width;
1029 height = swapchain_extent.height;
1030 } catch (...) {
1031 cleanup();
1032 throw;
1033 }
1034
1035 cleanup();
1036 }
1037
1039 }
1040
1042 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || vkTrimCommandPool == nullptr) {
1043 return;
1044 }
1045
1046 vkTrimCommandPool(device, command_pool, 0);
1047 }
1048
1049 void VK_Window::maybeTrimMemory() {
1050 const auto now = std::chrono::steady_clock::now();
1052 return;
1053 }
1054
1055 trimMemory();
1057 }
1058
1059 bool VK_Window::initWindow(const std::string &title, int width, int height, SDL_WindowFlags flags) {
1060 std::cout << std::format("mxvk: entering initWindow (title='{}', width={}, height={})\n", title, width, height);
1061 if (width <= 0 || height <= 0) {
1062 std::cerr << "mxvk: Window dimensions must be positive\n";
1063 return false;
1064 }
1065
1066 if (!sdl_initialized) {
1067 constexpr SDL_InitFlags sdlInitFlags = SDL_INIT_VIDEO | SDL_INIT_GAMEPAD;
1068 std::cout << "SDL3: initializing video/gamepad subsystems\n";
1069 if (!SDL_Init(sdlInitFlags)) {
1070 std::cerr << "mxvk: Error on SDL init: " << SDL_GetError() << "\n";
1071 return false;
1072 }
1073 sdl_initialized = true;
1074 SDL_SetGamepadEventsEnabled(true);
1075 SDL_SetJoystickEventsEnabled(true);
1076 std::cout << "SDL3: video/gamepad subsystems initialized\n";
1077 }
1078
1079 const bool fullscreen = (flags & SDL_WINDOW_FULLSCREEN) != 0;
1080 const SDL_DisplayMode *fullscreen_mode = nullptr;
1081 if (fullscreen) {
1082 const SDL_DisplayID display = SDL_GetPrimaryDisplay();
1083 fullscreen_mode = display != 0 ? SDL_GetCurrentDisplayMode(display) : nullptr;
1084 if (fullscreen_mode != nullptr && fullscreen_mode->w > 0 && fullscreen_mode->h > 0) {
1085 width = fullscreen_mode->w;
1086 height = fullscreen_mode->h;
1087 std::cout << std::format("SDL3: using current display mode for fullscreen window: {}x{}\n", width, height);
1088 }
1089 }
1090
1091 std::cout << "SDL3: creating SDL window with Vulkan capability\n";
1092 SDL_Window *raw_window = SDL_CreateWindow(title.c_str(), width, height, flags);
1093 if (raw_window == nullptr) {
1094 std::cerr << std::format("Error creating window: {}\n", SDL_GetError());
1095 std::cout << "SDL3: rolling back SDL video/gamepad subsystems after window creation failure\n";
1096 SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD);
1097 sdl_initialized = false;
1098 return false;
1099 }
1100
1101 window.reset(raw_window);
1102 if (fullscreen && fullscreen_mode != nullptr && !SDL_SetWindowFullscreenMode(window.get(), fullscreen_mode)) {
1103 std::cerr << std::format("mxvk: SDL_SetWindowFullscreenMode failed: {}\n", SDL_GetError());
1104 }
1105 std::cout << "SDL3: showing created window\n";
1106 SDL_ShowWindow(window.get());
1107 if (fullscreen && !SDL_SyncWindow(window.get())) {
1108 std::cerr << std::format("mxvk: SDL_SyncWindow failed after fullscreen show: {}\n", SDL_GetError());
1109 }
1110 if (fullscreen) {
1111 for (int attempt = 0; attempt < 50; ++attempt) {
1112 int pixel_width = 0;
1113 int pixel_height = 0;
1114 SDL_GetWindowSizeInPixels(window.get(), &pixel_width, &pixel_height);
1115 if (pixel_width == width && pixel_height == height) {
1116 break;
1117 }
1118 SDL_PumpEvents();
1119 SDL_Delay(10);
1120 }
1121 }
1122 std::cout << "mxvk: initWindow complete\n";
1123 return true;
1124 }
1125
1127 active = false;
1128 }
1129
1131
1133
1134 void VK_Window::onPrepareFrameRendering([[maybe_unused]] VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index) {}
1135
1136 void VK_Window::onRecordCustomRendering([[maybe_unused]] VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index) {}
1137
1138 void VK_Window::onConfigureDepthStencilAttachments([[maybe_unused]] VkRenderingAttachmentInfo &depth_attachment,
1139 [[maybe_unused]] VkRenderingAttachmentInfo &stencil_attachment,
1140 [[maybe_unused]] uint32_t image_index) {}
1141
1142 void VK_Window::renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd) {
1143 if (device == VK_NULL_HANDLE || swapchain_extent.width == 0U || swapchain_extent.height == 0U) {
1144 return;
1145 }
1146
1147 if (sprite_pipeline == VK_NULL_HANDLE) {
1148 return;
1149 }
1150 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1152 }
1153
1154 VK_Sprite *VK_Window::attachPostProcessingShader(const std::string &fragmentShaderPath, float p1, float p2, float p3, float p4) {
1155 const std::vector<VK_Sprite *> sprites = attachPostProcessingShaders({PostProcessingEffect{fragmentShaderPath, {p1, p2, p3, p4}, false}});
1156 return sprites.empty() ? nullptr : sprites.front();
1157 }
1158
1159 std::vector<VK_Sprite *> VK_Window::attachPostProcessingShaders(const std::vector<PostProcessingEffect> &effects) {
1161
1162 std::vector<VK_Sprite *> attachedSprites;
1163 attachedSprites.reserve(effects.size());
1164 for (const PostProcessingEffect &effect : effects) {
1165 if (effect.fragmentShaderPath.empty()) {
1166 throw mxvk::Exception("Cannot attach post-processing shader with an empty fragment shader path");
1167 }
1168
1169 VK_Sprite *sprite = createSprite(
1170 1,
1171 1,
1172 resolveRuntimeShaderPath("sprite.vert.spv", MXVK_SPRITE_SHADER_DIR),
1173 effect.fragmentShaderPath);
1174 const uint32_t black_pixel = 0xFF000000u;
1175 sprite->updateTexture(&black_pixel, 1, 1);
1176 sprite->setShaderParams(effect.params[0], effect.params[1], effect.params[2], effect.params[3]);
1177
1178 attachedSprites.push_back(sprite);
1179 owned_post_process_sprites.push_back(sprite);
1180 post_process_sprites.push_back(sprite);
1181 post_process_effect_params.push_back(effect.params);
1182 post_process_effect_time_enabled.push_back(effect.timeEnabled);
1183 post_process_effect_start_times.push_back(std::chrono::steady_clock::now());
1184 }
1185
1186 post_process_sprite = attachedSprites.empty() ? nullptr : attachedSprites.front();
1188 post_process_params = post_process_effect_params.empty() ? std::array<float, 4>{} : post_process_effect_params.front();
1190 post_process_start_time = post_process_effect_start_times.empty() ? std::chrono::steady_clock::now() : post_process_effect_start_times.front();
1191 if (device != VK_NULL_HANDLE && !swapchain_images.empty()) {
1192 createPostProcessTargets();
1193 }
1194 sprite_state_dirty = true;
1195 return attachedSprites;
1196 }
1197
1199 if (device != VK_NULL_HANDLE && !post_process_sprites.empty()) {
1200 vkDeviceWaitIdle(device);
1201 }
1202
1203 post_process_sprite = nullptr;
1204 destroyPostProcessTargets();
1206
1207 if (!owned_post_process_sprites.empty()) {
1208 const std::vector<VK_Sprite *> sprites_to_remove = owned_post_process_sprites;
1209 sprites.erase(
1210 std::remove_if(
1211 sprites.begin(),
1212 sprites.end(),
1213 [&sprites_to_remove](const std::unique_ptr<VK_Sprite> &sprite) {
1214 return std::ranges::find(sprites_to_remove, sprite.get()) != sprites_to_remove.end();
1215 }),
1216 sprites.end());
1217 sprite_state_dirty = true;
1218 }
1219 owned_post_process_sprite = nullptr;
1221 post_process_sprites.clear();
1225 }
1226
1227 void VK_Window::setPostProcessingShaderParams(float p1, float p2, float p3, float p4) {
1228 post_process_params = {p1, p2, p3, p4};
1229 if (post_process_sprites.empty()) {
1230 return;
1231 }
1232 setPostProcessingShaderParams(0, p1, p2, p3, p4);
1233 }
1234
1235 void VK_Window::setPostProcessingShaderParams(size_t effectIndex, float p1, float p2, float p3, float p4) {
1236 if (effectIndex >= post_process_sprites.size()) {
1237 return;
1238 }
1239 post_process_params = {p1, p2, p3, p4};
1240 if (effectIndex < post_process_effect_params.size()) {
1242 }
1243 if (post_process_sprites[effectIndex] != nullptr) {
1244 post_process_sprites[effectIndex]->setShaderParams(p1, p2, p3, p4);
1245 }
1246 }
1247
1249 post_process_time_enabled = enabled;
1250 post_process_start_time = std::chrono::steady_clock::now();
1251 if (post_process_sprites.empty()) {
1252 return;
1253 }
1255 }
1256
1257 void VK_Window::setPostProcessingShaderTimeEnabled(size_t effectIndex, bool enabled) {
1258 if (effectIndex >= post_process_sprites.size()) {
1259 return;
1260 }
1261 post_process_time_enabled = enabled;
1262 post_process_start_time = std::chrono::steady_clock::now();
1263 if (effectIndex < post_process_effect_time_enabled.size()) {
1264 post_process_effect_time_enabled[effectIndex] = enabled;
1265 }
1266 if (effectIndex < post_process_effect_start_times.size()) {
1268 }
1269 }
1270
1272 if (owned_post_process_sprite != nullptr && sprite != owned_post_process_sprite) {
1273 std::vector<VK_Sprite *> sprites_to_remove = owned_post_process_sprites;
1274 if (sprites_to_remove.empty()) {
1275 sprites_to_remove.push_back(owned_post_process_sprite);
1276 }
1277 sprites.erase(
1278 std::remove_if(
1279 sprites.begin(),
1280 sprites.end(),
1281 [&sprites_to_remove](const std::unique_ptr<VK_Sprite> &existing_sprite) {
1282 return std::ranges::find(sprites_to_remove, existing_sprite.get()) != sprites_to_remove.end();
1283 }),
1284 sprites.end());
1285 owned_post_process_sprite = nullptr;
1287 sprite_state_dirty = true;
1288 }
1289 post_process_sprite = sprite;
1290 post_process_sprites = sprite == nullptr ? std::vector<VK_Sprite *>{} : std::vector<VK_Sprite *>{sprite};
1294 if (device != VK_NULL_HANDLE && !swapchain_images.empty()) {
1295 createPostProcessTargets();
1296 }
1297 }
1298
1299 bool VK_Window::isPostProcessSprite(const VK_Sprite *sprite) const {
1300 return sprite != nullptr && std::ranges::find(post_process_sprites, sprite) != post_process_sprites.end();
1301 }
1302
1303 void VK_Window::destroyPostProcessTargets() {
1304 for (VK_Sprite *sprite : post_process_sprites) {
1305 if (sprite != nullptr) {
1307 }
1308 }
1309 for (const std::vector<VkImageView> &views : post_process_views) {
1310 for (VkImageView view : views) {
1311 if (view != VK_NULL_HANDLE) {
1312 vkDestroyImageView(device, view, nullptr);
1313 }
1314 }
1315 }
1316 for (const std::vector<VkImage> &images : post_process_images) {
1317 for (VkImage image : images) {
1318 if (image != VK_NULL_HANDLE) {
1319 vkDestroyImage(device, image, nullptr);
1320 }
1321 }
1322 }
1323 for (const std::vector<VkDeviceMemory> &memories : post_process_memories) {
1324 for (VkDeviceMemory memory : memories) {
1325 if (memory != VK_NULL_HANDLE) {
1326 vkFreeMemory(device, memory, nullptr);
1327 }
1328 }
1329 }
1330 post_process_views.clear();
1331 post_process_images.clear();
1332 post_process_memories.clear();
1334 }
1335
1336 void VK_Window::createPostProcessTargets() {
1337 destroyPostProcessTargets();
1338 if (post_process_sprites.empty() || swapchain_images.empty()) {
1339 return;
1340 }
1341 const size_t target_count = post_process_sprites.size() > 1 ? 2U : 1U;
1342 post_process_images.assign(target_count, std::vector<VkImage>(swapchain_images.size(), VK_NULL_HANDLE));
1343 post_process_memories.assign(target_count, std::vector<VkDeviceMemory>(swapchain_images.size(), VK_NULL_HANDLE));
1344 post_process_views.assign(target_count, std::vector<VkImageView>(swapchain_images.size(), VK_NULL_HANDLE));
1345 post_process_initialized.assign(target_count, std::vector<bool>(swapchain_images.size(), false));
1346 VkPhysicalDeviceMemoryProperties memory_properties{};
1347 vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties);
1348 try {
1349 for (size_t target = 0; target < target_count; ++target) {
1350 for (size_t i = 0; i < swapchain_images.size(); ++i) {
1351 VkImageCreateInfo image_info{};
1352 image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1353 image_info.imageType = VK_IMAGE_TYPE_2D;
1354 image_info.extent = {swapchain_extent.width, swapchain_extent.height, 1};
1355 image_info.mipLevels = 1;
1356 image_info.arrayLayers = 1;
1357 image_info.format = swapchain_format;
1358 image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
1359 image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1360 image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
1361 image_info.samples = VK_SAMPLE_COUNT_1_BIT;
1362 image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1363 if (vkCreateImage(device, &image_info, nullptr, &post_process_images[target][i]) != VK_SUCCESS) {
1364 throw mxvk::Exception("Failed to create post-process image");
1365 }
1366 VkMemoryRequirements requirements{};
1367 vkGetImageMemoryRequirements(device, post_process_images[target][i], &requirements);
1368 uint32_t memory_type = UINT32_MAX;
1369 for (uint32_t type = 0; type < memory_properties.memoryTypeCount; ++type) {
1370 if ((requirements.memoryTypeBits & (1U << type)) != 0U &&
1371 (memory_properties.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0U) {
1372 memory_type = type;
1373 break;
1374 }
1375 }
1376 if (memory_type == UINT32_MAX) {
1377 throw mxvk::Exception("Failed to find post-process image memory type");
1378 }
1379 VkMemoryAllocateInfo allocation{};
1380 allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1381 allocation.allocationSize = requirements.size;
1382 allocation.memoryTypeIndex = memory_type;
1383 if (vkAllocateMemory(device, &allocation, nullptr, &post_process_memories[target][i]) != VK_SUCCESS ||
1384 vkBindImageMemory(device, post_process_images[target][i], post_process_memories[target][i], 0) != VK_SUCCESS) {
1385 throw mxvk::Exception("Failed to allocate post-process image memory");
1386 }
1387 VkImageViewCreateInfo view_info{};
1388 view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1389 view_info.image = post_process_images[target][i];
1390 view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
1391 view_info.format = swapchain_format;
1392 view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
1393 view_info.subresourceRange.levelCount = 1;
1394 view_info.subresourceRange.layerCount = 1;
1395 if (vkCreateImageView(device, &view_info, nullptr, &post_process_views[target][i]) != VK_SUCCESS) {
1396 throw mxvk::Exception("Failed to create post-process image view");
1397 }
1398 }
1399 }
1400 } catch (...) {
1401 destroyPostProcessTargets();
1402 throw;
1403 }
1404 }
1405
1407 const auto sync_ready = [this]() {
1408 return std::ranges::all_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) &&
1409 render_finished.size() == swapchain_images.size() &&
1410 std::ranges::all_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) &&
1411 std::ranges::all_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; });
1412 };
1413
1414 if (device == VK_NULL_HANDLE) {
1415 return false;
1416 }
1417
1418 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || command_buffers.empty() || !sync_ready() ||
1419 image_fences.size() != swapchain_images.size()) {
1420 createDevice();
1421 }
1422
1423 return (swapchain != VK_NULL_HANDLE && command_pool != VK_NULL_HANDLE && !command_buffers.empty() && sync_ready() &&
1424 image_fences.size() == swapchain_images.size());
1425 }
1426
1428 std::cout << "mxvk: entering pickDevice\n";
1429 if (instance == VK_NULL_HANDLE || surface == VK_NULL_HANDLE) {
1430 std::cout << "vk: cannot pick device because instance or surface is missing\n";
1431 return;
1432 }
1433
1434 uint32_t device_count = 0;
1435 std::cout << "vk: enumerating physical devices\n";
1436 vkEnumeratePhysicalDevices(instance, &device_count, nullptr);
1437 if (device_count == 0U) {
1438 std::cout << "vk: no physical devices found\n";
1439 return;
1440 }
1441 std::cout << std::format("vk: found {} physical device(s)\n", device_count);
1442
1443 std::vector<VkPhysicalDevice> devices(device_count);
1444 vkEnumeratePhysicalDevices(instance, &device_count, devices.data());
1445
1446 for (const VkPhysicalDevice candidate : devices) {
1447 std::cout << "vk: evaluating candidate physical device\n";
1448 uint32_t queue_family_count = 0;
1449 vkGetPhysicalDeviceQueueFamilyProperties(candidate, &queue_family_count, nullptr);
1450 std::vector<VkQueueFamilyProperties> queue_families(queue_family_count);
1451 vkGetPhysicalDeviceQueueFamilyProperties(candidate, &queue_family_count, queue_families.data());
1452
1453 uint32_t candidate_graphics = invalid_queue_index;
1454 uint32_t candidate_present = invalid_queue_index;
1455
1456 for (uint32_t i = 0; i < queue_family_count; ++i) {
1457 if ((queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U) {
1458 candidate_graphics = i;
1459 }
1460
1461 VkBool32 present_support = VK_FALSE;
1462 vkGetPhysicalDeviceSurfaceSupportKHR(candidate, i, surface, &present_support);
1463 if (present_support == VK_TRUE) {
1464 candidate_present = i;
1465 }
1466 }
1467
1468 if (candidate_graphics == invalid_queue_index || candidate_present == invalid_queue_index) {
1469 std::cout << "vk: candidate rejected due to missing graphics/present queue support\n";
1470 continue;
1471 }
1472
1473 const SwapchainSupport swapchain_support = querySwapchainSupport(candidate, surface);
1474 if (swapchain_support.formats.empty() || swapchain_support.present_modes.empty()) {
1475 std::cout << "vk: candidate rejected due to incomplete swapchain support\n";
1476 continue;
1477 }
1478
1479 VkPhysicalDeviceProperties properties{};
1480 vkGetPhysicalDeviceProperties(candidate, &properties);
1481
1482 const char *device_type = "unknown";
1483 switch (properties.deviceType) {
1484 case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
1485 device_type = "integrated";
1486 break;
1487 case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
1488 device_type = "discrete";
1489 break;
1490 case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
1491 device_type = "virtual";
1492 break;
1493 case VK_PHYSICAL_DEVICE_TYPE_CPU:
1494 device_type = "cpu";
1495 break;
1496 default:
1497 break;
1498 }
1499
1500 physical_device = candidate;
1501 graphics_queue_family = candidate_graphics;
1502 present_queue_family = candidate_present;
1503 std::cout << std::format(
1504 "vk: selected GPU='{}' type={} vendor=0x{:04x} device=0x{:04x}\n",
1505 properties.deviceName,
1506 device_type,
1507 properties.vendorID,
1508 properties.deviceID);
1509 return;
1510 }
1511
1512 std::cout << "vk: no suitable physical device selected\n";
1513 }
1515 std::cout << "mxvk: entering createLogicalDevice\n";
1516 if (physical_device == VK_NULL_HANDLE) {
1517 std::cout << "vk: cannot create logical device without a selected physical device\n";
1518 return;
1519 }
1520
1521 std::vector<uint32_t> queue_families{};
1522 queue_families.push_back(graphics_queue_family);
1524 queue_families.push_back(present_queue_family);
1525 }
1526
1527 constexpr float queue_priority = 1.0f;
1528 std::vector<VkDeviceQueueCreateInfo> queue_create_infos{};
1529 queue_create_infos.reserve(queue_families.size());
1530 for (const uint32_t family : queue_families) {
1531 VkDeviceQueueCreateInfo queue_create_info{};
1532 queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
1533 queue_create_info.queueFamilyIndex = family;
1534 queue_create_info.queueCount = 1;
1535 queue_create_info.pQueuePriorities = &queue_priority;
1536 queue_create_infos.push_back(queue_create_info);
1537 }
1538 uint32_t device_extension_count = 0;
1539 vkEnumerateDeviceExtensionProperties(physical_device, nullptr, &device_extension_count, nullptr);
1540 std::vector<VkExtensionProperties> device_extensions(device_extension_count);
1541 if (device_extension_count > 0U) {
1542 vkEnumerateDeviceExtensionProperties(physical_device, nullptr, &device_extension_count, device_extensions.data());
1543 }
1544
1545 [[maybe_unused]] const auto has_device_extension = [&device_extensions](const char *extension_name) {
1546 return std::ranges::any_of(
1547 device_extensions,
1548 [extension_name](const VkExtensionProperties &ext) {
1549 return std::strcmp(ext.extensionName, extension_name) == 0;
1550 });
1551 };
1552
1553 std::vector<const char *> required_device_extensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
1554#if defined(MXVK_CUDA) && defined(__linux__)
1555 if (has_device_extension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME)) {
1556 if (has_device_extension(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME)) {
1557 required_device_extensions.push_back(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME);
1558 }
1559 required_device_extensions.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
1560 std::cout << "vk: enabling external memory FD support for CUDA interop\n";
1561 } else {
1562 std::cout << "vk: external memory FD support unavailable; CUDA texture interop will fall back\n";
1563 }
1564#endif
1565
1566#if defined(MXVK_USE_MOLTENVK)
1567 const bool has_portability_subset = std::ranges::any_of(
1568 device_extensions,
1569 [](const VkExtensionProperties &ext) {
1570 return std::strcmp(ext.extensionName, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME) == 0;
1571 });
1572
1573 if (has_portability_subset) {
1574 required_device_extensions.push_back(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME);
1575 }
1576#endif
1577 VkPhysicalDeviceFeatures2 features2{};
1578 features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
1579 VkPhysicalDeviceVulkan13Features supported_vulkan13_features{};
1580 supported_vulkan13_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES;
1581 features2.pNext = &supported_vulkan13_features;
1582 vkGetPhysicalDeviceFeatures2(physical_device, &features2);
1583
1584 std::cout << std::format(
1585 "vk: feature support - synchronization2={}, dynamicRendering={}, shaderFloat64={}, fillModeNonSolid={}, samplerAnisotropy={}\n",
1586 supported_vulkan13_features.synchronization2 == VK_TRUE ? "true" : "false",
1587 supported_vulkan13_features.dynamicRendering == VK_TRUE ? "true" : "false",
1588 features2.features.shaderFloat64 == VK_TRUE ? "true" : "false",
1589 features2.features.fillModeNonSolid == VK_TRUE ? "true" : "false",
1590 features2.features.samplerAnisotropy == VK_TRUE ? "true" : "false");
1591
1592 if (supported_vulkan13_features.synchronization2 != VK_TRUE) {
1593 std::cout << "vk: synchronization2 is unsupported on selected physical device\n";
1594 return;
1595 }
1596 if (supported_vulkan13_features.dynamicRendering != VK_TRUE) {
1597 std::cout << "vk: dynamic rendering is unsupported on selected physical device\n";
1598 return;
1599 }
1600 VkPhysicalDeviceVulkan13Features vulkan13_features{};
1601 vulkan13_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES;
1602 vulkan13_features.synchronization2 = VK_TRUE;
1603 vulkan13_features.dynamicRendering = VK_TRUE;
1604
1605 VkPhysicalDeviceFeatures2 enabled_features2{};
1606 enabled_features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
1607 enabled_features2.features.shaderFloat64 = features2.features.shaderFloat64;
1608 enabled_features2.features.fillModeNonSolid = features2.features.fillModeNonSolid;
1609 enabled_features2.features.samplerAnisotropy = features2.features.samplerAnisotropy;
1610
1611 enabled_features2.pNext = &vulkan13_features;
1612
1613 VkDeviceCreateInfo create_info{};
1614 create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
1615 create_info.pNext = &enabled_features2;
1616 create_info.queueCreateInfoCount = static_cast<uint32_t>(queue_create_infos.size());
1617 create_info.pQueueCreateInfos = queue_create_infos.data();
1618 create_info.enabledExtensionCount = static_cast<uint32_t>(required_device_extensions.size());
1619 create_info.ppEnabledExtensionNames = required_device_extensions.data();
1620 create_info.enabledLayerCount = 0;
1621 create_info.ppEnabledLayerNames = nullptr;
1622 create_info.pEnabledFeatures = nullptr;
1623
1624 std::cout << "vk: creating logical device\n";
1625 if (vkCreateDevice(physical_device, &create_info, nullptr, &device) != VK_SUCCESS) {
1626 std::cout << "vk: logical device creation failed\n";
1627 device = VK_NULL_HANDLE;
1628 return;
1629 }
1630
1631 std::cout << "vk: loading device-level function pointers via volk\n";
1632 volkLoadDevice(device);
1633 if (vkQueueSubmit2 == nullptr || vkCmdBeginRendering == nullptr || vkCmdEndRendering == nullptr || vkCmdPipelineBarrier2 == nullptr) {
1634 std::cout << "vk: required dynamic rendering/synchronization function pointers are unavailable\n";
1635 vkDestroyDevice(device, nullptr);
1636 device = VK_NULL_HANDLE;
1637 return;
1638 }
1639 std::cout << "vk: retrieving graphics and present queues\n";
1640 vkGetDeviceQueue(device, graphics_queue_family, 0, &graphics_queue);
1641 vkGetDeviceQueue(device, present_queue_family, 0, &present_queue);
1642 std::cout << "mxvk: createLogicalDevice complete\n";
1643 }
1644
1646 std::cout << "mxvk: entering createDevice\n";
1647 if (device == VK_NULL_HANDLE) {
1648 std::cout << "vk: skipping createDevice because logical device is null\n";
1649 return;
1650 }
1651
1652 const bool sync_ready =
1653 std::ranges::all_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) &&
1654 render_finished.size() == swapchain_images.size() &&
1655 std::ranges::all_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) &&
1656 std::ranges::all_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; });
1657
1658 if (swapchain != VK_NULL_HANDLE && command_pool != VK_NULL_HANDLE && !command_buffers.empty() &&
1659 sync_ready && image_fences.size() == swapchain_images.size()) {
1660 std::cout << "vk: createDevice skipped because render resources are already initialized\n";
1661 return;
1662 }
1663
1664 std::cout << "vk: creating swapchain\n";
1665 if (!createSwapchain(VK_NULL_HANDLE)) {
1666 std::cout << "vk: createSwapchain failed\n";
1667 return;
1668 }
1669
1670 std::cout << "vk: creating render resources\n";
1671 if (!createRenderResources()) {
1672 std::cout << "vk: createRenderResources failed\n";
1673 return;
1674 }
1675
1676 std::cout << "vk: creating synchronization objects\n";
1677 if (!createSyncObjects()) {
1678 std::cout << "vk: createSyncObjects failed\n";
1679 return;
1680 }
1681
1682 std::cout << "mxvk: createDevice complete\n";
1683 }
1684
1685 bool VK_Window::createSwapchain(VkSwapchainKHR old_swapchain) {
1686 std::cout << "vk: entering createSwapchain\n";
1687 SwapchainSupport support = querySwapchainSupport(physical_device, surface);
1688 if (support.formats.empty() || support.present_modes.empty()) {
1689 std::cout << "vk: cannot create swapchain because support is incomplete\n";
1690 return false;
1691 }
1692
1693 const VkSurfaceFormatKHR surface_format = chooseSurfaceFormat(support.formats);
1694 const VkPresentModeKHR present_mode = choosePresentMode(support.present_modes);
1695 auto clampedWindowPixelExtent = [this, &support]() -> std::optional<VkExtent2D> {
1696 if (window == nullptr) {
1697 return std::nullopt;
1698 }
1699 int pixel_w = 0;
1700 int pixel_h = 0;
1701 SDL_GetWindowSizeInPixels(window.get(), &pixel_w, &pixel_h);
1702 if (pixel_w <= 0 || pixel_h <= 0) {
1703 return std::nullopt;
1704 }
1705 return VkExtent2D{
1706 .width = std::clamp(static_cast<uint32_t>(pixel_w),
1707 support.capabilities.minImageExtent.width,
1708 support.capabilities.maxImageExtent.width),
1709 .height = std::clamp(static_cast<uint32_t>(pixel_h),
1710 support.capabilities.minImageExtent.height,
1711 support.capabilities.maxImageExtent.height),
1712 };
1713 };
1714 auto extentMatchesWindowPixels = [&clampedWindowPixelExtent](const VkExtent2D extent) {
1715 const std::optional<VkExtent2D> target_extent = clampedWindowPixelExtent();
1716 return !target_extent.has_value() ||
1717 (extent.width == target_extent->width && extent.height == target_extent->height);
1718 };
1719
1720 VkExtent2D extent = chooseExtent(support.capabilities, window.get());
1721 for (int attempt = 0; attempt < 50 && !extentMatchesWindowPixels(extent); ++attempt) {
1722 SDL_PumpEvents();
1723 SDL_Delay(10);
1724 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &support.capabilities);
1725 extent = chooseExtent(support.capabilities, window.get());
1726 }
1727 if (!extentMatchesWindowPixels(extent)) {
1728 const std::optional<VkExtent2D> target_extent = clampedWindowPixelExtent();
1729 if (target_extent.has_value()) {
1730 std::cout << std::format("vk: delaying swapchain creation until surface extent catches up to window pixels (surface={}x{}, window={}x{})\n",
1731 extent.width,
1732 extent.height,
1733 target_extent->width,
1734 target_extent->height);
1735 }
1736 return false;
1737 }
1738
1739 uint32_t image_count = support.capabilities.minImageCount + 1;
1740 if (support.capabilities.maxImageCount > 0U && image_count > support.capabilities.maxImageCount) {
1741 image_count = support.capabilities.maxImageCount;
1742 }
1743
1744 VkSwapchainCreateInfoKHR create_info{};
1745 create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
1746 create_info.surface = surface;
1747 create_info.minImageCount = image_count;
1748 create_info.imageFormat = surface_format.format;
1749 create_info.imageColorSpace = surface_format.colorSpace;
1750 create_info.imageExtent = extent;
1751 create_info.imageArrayLayers = 1;
1752 create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
1753 swapchain_supports_transfer_src = (support.capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0U;
1755 create_info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
1756 } else {
1757 std::cerr << "mxvk: swapchain does not support transfer-source usage; saveSnapshot is unavailable\n";
1758 }
1759
1760 const uint32_t queue_family_indices[] = {graphics_queue_family, present_queue_family};
1762 create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
1763 create_info.queueFamilyIndexCount = 2;
1764 create_info.pQueueFamilyIndices = queue_family_indices;
1765 } else {
1766 create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
1767 create_info.queueFamilyIndexCount = 0;
1768 create_info.pQueueFamilyIndices = nullptr;
1769 }
1770
1771 create_info.preTransform = support.capabilities.currentTransform;
1772 create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
1773 create_info.presentMode = present_mode;
1774 create_info.clipped = VK_TRUE;
1775 create_info.oldSwapchain = old_swapchain;
1776
1777 std::cout << "vk: creating swapchain object\n";
1778 if (vkCreateSwapchainKHR(device, &create_info, nullptr, &swapchain) != VK_SUCCESS) {
1779 swapchain = VK_NULL_HANDLE;
1780 return false;
1781 }
1782
1783 std::cout << "vk: querying swapchain images\n";
1784 vkGetSwapchainImagesKHR(device, swapchain, &image_count, nullptr);
1785 swapchain_images.resize(image_count);
1786 vkGetSwapchainImagesKHR(device, swapchain, &image_count, swapchain_images.data());
1787 swapchain_image_initialized.assign(swapchain_images.size(), false);
1788
1789 swapchain_format = surface_format.format;
1790 swapchain_extent = extent;
1791 last_presented_image_index = invalid_queue_index;
1792
1794 for (size_t i = 0; i < swapchain_images.size(); ++i) {
1795 VkImageViewCreateInfo view_info{};
1796 view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1797 view_info.image = swapchain_images[i];
1798 view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
1799 view_info.format = swapchain_format;
1800 view_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
1801 view_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
1802 view_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
1803 view_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
1804 view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
1805 view_info.subresourceRange.baseMipLevel = 0;
1806 view_info.subresourceRange.levelCount = 1;
1807 view_info.subresourceRange.baseArrayLayer = 0;
1808 view_info.subresourceRange.layerCount = 1;
1809
1810 std::cout << std::format("vk: creating image view for swapchain image {}\n", i);
1811 if (vkCreateImageView(device, &view_info, nullptr, &swapchain_image_views[i]) != VK_SUCCESS) {
1812 for (VkImageView image_view : swapchain_image_views) {
1813 if (image_view != VK_NULL_HANDLE) {
1814 vkDestroyImageView(device, image_view, nullptr);
1815 }
1816 }
1817 swapchain_image_views.clear();
1818 swapchain_images.clear();
1820 last_presented_image_index = invalid_queue_index;
1821 vkDestroySwapchainKHR(device, swapchain, nullptr);
1822 swapchain = VK_NULL_HANDLE;
1823 swapchain_format = VK_FORMAT_UNDEFINED;
1824 swapchain_extent = {};
1826 return false;
1827 }
1828 }
1829
1830 std::cout << "vk: createSwapchain complete\n";
1831 return true;
1832 }
1833
1834 bool VK_Window::createRenderResources() {
1835 std::cout << "vk: entering createRenderResources\n";
1836 const auto cleanup_render_resource_failure = [this]() {
1837 if (command_pool != VK_NULL_HANDLE && !command_buffers.empty()) {
1838 vkFreeCommandBuffers(device, command_pool, static_cast<uint32_t>(command_buffers.size()), command_buffers.data());
1839 command_buffers.clear();
1840 }
1841 if (!depth_image_views.empty()) {
1842 for (VkImageView &view : depth_image_views) {
1843 if (view != VK_NULL_HANDLE) {
1844 vkDestroyImageView(device, view, nullptr);
1845 view = VK_NULL_HANDLE;
1846 }
1847 }
1848 }
1849 if (!depth_images.empty()) {
1850 for (VkImage &image : depth_images) {
1851 if (image != VK_NULL_HANDLE) {
1852 vkDestroyImage(device, image, nullptr);
1853 image = VK_NULL_HANDLE;
1854 }
1855 }
1856 }
1857 if (!depth_image_memories.empty()) {
1858 for (VkDeviceMemory &memory : depth_image_memories) {
1859 if (memory != VK_NULL_HANDLE) {
1860 vkFreeMemory(device, memory, nullptr);
1861 memory = VK_NULL_HANDLE;
1862 }
1863 }
1864 }
1865 depth_image_views.clear();
1866 depth_images.clear();
1867 depth_image_memories.clear();
1869 depth_format = VK_FORMAT_UNDEFINED;
1870 destroyPostProcessTargets();
1871 };
1872
1873 if (command_pool == VK_NULL_HANDLE) {
1874 VkCommandPoolCreateInfo pool_info{};
1875 pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
1876 pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
1877 pool_info.queueFamilyIndex = graphics_queue_family;
1878
1879 std::cout << "vk: creating command pool\n";
1880 if (vkCreateCommandPool(device, &pool_info, nullptr, &command_pool) != VK_SUCCESS) {
1881 return false;
1882 }
1883 }
1884
1885 if (!command_buffers.empty()) {
1886 std::cout << "vk: freeing stale command buffers before reallocation\n";
1887 vkFreeCommandBuffers(device, command_pool, static_cast<uint32_t>(command_buffers.size()), command_buffers.data());
1888 command_buffers.clear();
1889 }
1890
1891 command_buffers.resize(swapchain_images.size());
1892 VkCommandBufferAllocateInfo alloc_info{};
1893 alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
1894 alloc_info.commandPool = command_pool;
1895 alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
1896 alloc_info.commandBufferCount = static_cast<uint32_t>(command_buffers.size());
1897
1898 std::cout << "vk: allocating command buffers\n";
1899 if (vkAllocateCommandBuffers(device, &alloc_info, command_buffers.data()) != VK_SUCCESS) {
1900 command_buffers.clear();
1901 return false;
1902 }
1903
1904 auto findDepthFormat = [&](VkPhysicalDevice gpu) -> VkFormat {
1905 const std::array<VkFormat, 2> candidates = {
1906 VK_FORMAT_D32_SFLOAT,
1907 VK_FORMAT_D16_UNORM,
1908 };
1909
1910 for (const VkFormat format : candidates) {
1911 VkFormatProperties props{};
1912 vkGetPhysicalDeviceFormatProperties(gpu, format, &props);
1913 if ((props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0U) {
1914 return format;
1915 }
1916 }
1917
1918 return VK_FORMAT_UNDEFINED;
1919 };
1920
1921 depth_format = findDepthFormat(physical_device);
1922 if (depth_format == VK_FORMAT_UNDEFINED) {
1923 std::cerr << "mxvk: failed to find a supported depth format\n";
1924 cleanup_render_resource_failure();
1925 return false;
1926 }
1927
1928 depth_images.resize(max_frames_in_flight, VK_NULL_HANDLE);
1929 depth_image_memories.resize(max_frames_in_flight, VK_NULL_HANDLE);
1930 depth_image_views.resize(max_frames_in_flight, VK_NULL_HANDLE);
1931 depth_image_initialized.assign(max_frames_in_flight, false);
1932
1933 for (size_t i = 0; i < depth_images.size(); ++i) {
1934 std::cout << std::format("vk: creating depth image for frame {}\n", i);
1935 VkImageCreateInfo imageInfo{};
1936 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1937 imageInfo.imageType = VK_IMAGE_TYPE_2D;
1938 imageInfo.extent.width = swapchain_extent.width;
1939 imageInfo.extent.height = swapchain_extent.height;
1940 imageInfo.extent.depth = 1;
1941 imageInfo.mipLevels = 1;
1942 imageInfo.arrayLayers = 1;
1943 imageInfo.format = depth_format;
1944 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1945 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1946 imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
1947 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1948 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1949
1950 if (vkCreateImage(device, &imageInfo, nullptr, &depth_images[i]) != VK_SUCCESS) {
1951 std::cerr << std::format("mxvk: failed to create depth image {}\n", i);
1952 cleanup_render_resource_failure();
1953 return false;
1954 }
1955
1956 VkMemoryRequirements memReq{};
1957 vkGetImageMemoryRequirements(device, depth_images[i], &memReq);
1958
1959 VkPhysicalDeviceMemoryProperties memProps{};
1960 vkGetPhysicalDeviceMemoryProperties(physical_device, &memProps);
1961 uint32_t memoryTypeIndex = UINT32_MAX;
1962 for (uint32_t t = 0; t < memProps.memoryTypeCount; ++t) {
1963 if (((memReq.memoryTypeBits & (1u << t)) != 0U) &&
1964 ((memProps.memoryTypes[t].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0U)) {
1965 memoryTypeIndex = t;
1966 break;
1967 }
1968 }
1969 if (memoryTypeIndex == UINT32_MAX) {
1970 std::cerr << "mxvk: failed to find depth image memory type\n";
1971 cleanup_render_resource_failure();
1972 return false;
1973 }
1974
1975 std::cout << std::format("vk: allocating depth image memory for frame {}\n", i);
1976 VkMemoryAllocateInfo allocInfo{};
1977 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1978 allocInfo.allocationSize = memReq.size;
1979 allocInfo.memoryTypeIndex = memoryTypeIndex;
1980
1981 if (vkAllocateMemory(device, &allocInfo, nullptr, &depth_image_memories[i]) != VK_SUCCESS) {
1982 std::cerr << std::format("mxvk: failed to allocate depth image memory {}\n", i);
1983 cleanup_render_resource_failure();
1984 return false;
1985 }
1986
1987 if (vkBindImageMemory(device, depth_images[i], depth_image_memories[i], 0) != VK_SUCCESS) {
1988 std::cerr << std::format("mxvk: failed to bind depth image memory {}\n", i);
1989 cleanup_render_resource_failure();
1990 return false;
1991 }
1992
1993 std::cout << std::format("vk: creating depth image view for frame {}\n", i);
1994 VkImageViewCreateInfo viewInfo{};
1995 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1996 viewInfo.image = depth_images[i];
1997 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1998 viewInfo.format = depth_format;
1999 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
2000 viewInfo.subresourceRange.baseMipLevel = 0;
2001 viewInfo.subresourceRange.levelCount = 1;
2002 viewInfo.subresourceRange.baseArrayLayer = 0;
2003 viewInfo.subresourceRange.layerCount = 1;
2004
2005 if (vkCreateImageView(device, &viewInfo, nullptr, &depth_image_views[i]) != VK_SUCCESS) {
2006 std::cerr << std::format("mxvk: failed to create depth image view {}\n", i);
2007 cleanup_render_resource_failure();
2008 return false;
2009 }
2010 }
2011
2012 if (!post_process_sprites.empty()) {
2013 try {
2014 createPostProcessTargets();
2015 } catch (const mxvk::Exception &ex) {
2016 std::cerr << std::format("mxvk: {}\n", ex.text());
2017 cleanup_render_resource_failure();
2018 return false;
2019 }
2020 }
2021
2022 std::cout << "vk: createRenderResources complete\n";
2023 return true;
2024 }
2025
2026 bool VK_Window::createSyncObjects() {
2027 std::cout << "vk: entering createSyncObjects\n";
2028 if (swapchain_images.empty()) {
2029 std::cerr << "mxvk: cannot create sync objects without swapchain images\n";
2030 return false;
2031 }
2032
2033 cleanupSyncObjects();
2034
2035 VkSemaphoreCreateInfo semaphore_info{};
2036 semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
2037
2038 VkFenceCreateInfo fence_info{};
2039 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
2040 fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
2041
2042 render_finished.assign(swapchain_images.size(), VK_NULL_HANDLE);
2043
2044 for (uint32_t frame = 0; frame < max_frames_in_flight; ++frame) {
2045 std::cout << std::format("vk: creating image-available semaphore for frame {}\n", frame);
2046 if (vkCreateSemaphore(device, &semaphore_info, nullptr, &image_available[frame]) != VK_SUCCESS) {
2047 cleanupSyncObjects();
2048 return false;
2049 }
2050
2051 std::cout << std::format("vk: creating in-flight fence for frame {}\n", frame);
2052 if (vkCreateFence(device, &fence_info, nullptr, &in_flight_fences[frame]) != VK_SUCCESS) {
2053 cleanupSyncObjects();
2054 return false;
2055 }
2056 }
2057
2058 for (size_t image_index = 0; image_index < render_finished.size(); ++image_index) {
2059 std::cout << std::format("vk: creating render-finished semaphore for swapchain image {}\n", image_index);
2060 if (vkCreateSemaphore(device, &semaphore_info, nullptr, &render_finished[image_index]) != VK_SUCCESS) {
2061 cleanupSyncObjects();
2062 return false;
2063 }
2064 }
2065
2066 image_fences.assign(swapchain_images.size(), VK_NULL_HANDLE);
2067 current_frame = 0;
2068
2069 std::cout << "vk: createSyncObjects complete\n";
2070 return true;
2071 }
2072
2073 void VK_Window::cleanupSyncObjects() {
2074 if (device == VK_NULL_HANDLE) {
2075 return;
2076 }
2077
2078 const bool has_in_flight_fences = std::ranges::any_of(
2079 in_flight_fences.begin(),
2080 in_flight_fences.end(),
2081 [](VkFence fence) { return fence != VK_NULL_HANDLE; });
2082 const bool has_render_finished = std::ranges::any_of(
2083 render_finished.begin(),
2084 render_finished.end(),
2085 [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; });
2086 const bool has_image_available = std::ranges::any_of(
2087 image_available.begin(),
2088 image_available.end(),
2089 [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; });
2090
2091 if (!has_in_flight_fences && !has_render_finished && !has_image_available) {
2092 image_fences.clear();
2093 current_frame = 0;
2094 return;
2095 }
2096
2097 if (has_in_flight_fences) {
2098 std::cout << "vk: destroying in-flight fences\n";
2099 for (VkFence &fence : in_flight_fences) {
2100 if (fence != VK_NULL_HANDLE) {
2101 vkDestroyFence(device, fence, nullptr);
2102 fence = VK_NULL_HANDLE;
2103 }
2104 }
2105 }
2106
2107 if (has_render_finished) {
2108 std::cout << "vk: destroying render-finished semaphores\n";
2109 for (VkSemaphore &semaphore : render_finished) {
2110 if (semaphore != VK_NULL_HANDLE) {
2111 vkDestroySemaphore(device, semaphore, nullptr);
2112 semaphore = VK_NULL_HANDLE;
2113 }
2114 }
2115 }
2116
2117 if (has_image_available) {
2118 std::cout << "vk: destroying image-available semaphores\n";
2119 for (VkSemaphore &semaphore : image_available) {
2120 if (semaphore != VK_NULL_HANDLE) {
2121 vkDestroySemaphore(device, semaphore, nullptr);
2122 semaphore = VK_NULL_HANDLE;
2123 }
2124 }
2125 }
2126
2127 image_fences.clear();
2128 render_finished.clear();
2129 current_frame = 0;
2130 }
2131
2132 void VK_Window::recreateSwapchain() {
2133 if (device == VK_NULL_HANDLE || window == nullptr) {
2134 return;
2135 }
2136 int pixel_w = 0;
2137 int pixel_h = 0;
2138 SDL_GetWindowSizeInPixels(window.get(), &pixel_w, &pixel_h);
2139 VkSurfaceCapabilitiesKHR surface_capabilities{};
2140 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &surface_capabilities);
2141
2142 VkExtent2D new_extent{};
2143
2144 if (surface_capabilities.currentExtent.width != 0xFFFFFFFFU) {
2145 new_extent = surface_capabilities.currentExtent;
2146 } else {
2147 new_extent.width = std::clamp(static_cast<uint32_t>(pixel_w),
2148 surface_capabilities.minImageExtent.width,
2149 surface_capabilities.maxImageExtent.width);
2150
2151 new_extent.height = std::clamp(static_cast<uint32_t>(pixel_h),
2152 surface_capabilities.minImageExtent.height,
2153 surface_capabilities.maxImageExtent.height);
2154 }
2155
2156 if (new_extent.width == 0 || new_extent.height == 0) {
2157 return;
2158 }
2159
2160 const uint32_t target_w = std::clamp(static_cast<uint32_t>(pixel_w),
2161 surface_capabilities.minImageExtent.width,
2162 surface_capabilities.maxImageExtent.width);
2163 const uint32_t target_h = std::clamp(static_cast<uint32_t>(pixel_h),
2164 surface_capabilities.minImageExtent.height,
2165 surface_capabilities.maxImageExtent.height);
2166
2167 if (new_extent.width != target_w || new_extent.height != target_h) {
2168 return;
2169 }
2170
2171 if (!force_swapchain_recreate && swapchain_extent.width == new_extent.width &&
2172 swapchain_extent.height == new_extent.height) {
2173 return;
2174 }
2175
2176 if (!force_swapchain_recreate && swapchain_extent.width == new_extent.width &&
2177 swapchain_extent.height == new_extent.height) {
2178 return;
2179 }
2180
2181 std::cout << std::format("mxvk: recreating swapchain for {}x{} window\n", new_extent.width, new_extent.height);
2182 vkDeviceWaitIdle(device);
2184
2185 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2186 if (sprite) {
2187 sprite->releaseUploadResources();
2188 }
2189 }
2190
2191 sprite_state_dirty = true;
2192 text_state_dirty = true;
2193 destroySpritePipeline();
2194 destroyTextPipeline();
2195 cleanupSyncObjects();
2196 VkSwapchainKHR old_swapchain = swapchain;
2197 cleanupSwapchain(false);
2198 if (!createSwapchain(old_swapchain) || !createRenderResources() || !createSyncObjects()) {
2199 std::cerr << "mxvk: failed to recreate swapchain after resize\n";
2200 if (old_swapchain != VK_NULL_HANDLE) {
2201 retired_swapchains.push_back(old_swapchain);
2202 // vkDestroySwapchainKHR(device, old_swapchain, nullptr);
2203 }
2204 return;
2205 }
2206 if (old_swapchain != VK_NULL_HANDLE) {
2207 // vkDestroySwapchainKHR(device, old_swapchain, nullptr);
2208 retired_swapchains.push_back(old_swapchain);
2209 }
2210
2211 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2212 if (sprite) {
2214 }
2215 }
2216 for (const std::unique_ptr<VK_Sprite3D> &sprite : sprites3d) {
2217 if (sprite) {
2218 sprite->resize(this);
2219 }
2220 }
2221
2224 framebuffer_resized = false;
2225 std::cout << "mxvk: swapchain recreation complete\n";
2226 }
2227
2228 void VK_Window::cleanupSwapchain(bool destroy_swapchain_handle) {
2229 std::cout << "vk: entering cleanupSwapchain\n";
2230 if (device == VK_NULL_HANDLE) {
2231 return;
2232 }
2233
2234 destroyPostProcessTargets();
2235
2236 if (!swapchain_image_views.empty()) {
2237 std::cout << "vk: destroying swapchain image views\n";
2238 for (VkImageView image_view : swapchain_image_views) {
2239 if (image_view != VK_NULL_HANDLE) {
2240 vkDestroyImageView(device, image_view, nullptr);
2241 }
2242 }
2243 }
2244 swapchain_image_views.clear();
2245
2246 if (!depth_image_views.empty()) {
2247 std::cout << "vk: destroying depth image views\n";
2248 for (VkImageView view : depth_image_views) {
2249 if (view != VK_NULL_HANDLE) {
2250 vkDestroyImageView(device, view, nullptr);
2251 }
2252 }
2253 }
2254 depth_image_views.clear();
2255
2256 if (!depth_images.empty()) {
2257 std::cout << "vk: destroying depth images\n";
2258 for (VkImage image : depth_images) {
2259 if (image != VK_NULL_HANDLE) {
2260 vkDestroyImage(device, image, nullptr);
2261 }
2262 }
2263 }
2264 depth_images.clear();
2265
2266 if (!depth_image_memories.empty()) {
2267 std::cout << "vk: freeing depth image memory\n";
2268 for (VkDeviceMemory memory : depth_image_memories) {
2269 if (memory != VK_NULL_HANDLE) {
2270 vkFreeMemory(device, memory, nullptr);
2271 }
2272 }
2273 }
2274 depth_image_memories.clear();
2276
2277 swapchain_images.clear();
2279 last_presented_image_index = invalid_queue_index;
2281 depth_format = VK_FORMAT_UNDEFINED;
2282
2283 // This guard prevents destroying the active swapchain during a live resize
2284 if (destroy_swapchain_handle && swapchain != VK_NULL_HANDLE) {
2285 std::cout << "vk: destroying swapchain\n";
2286 vkDestroySwapchainKHR(device, swapchain, nullptr);
2287 swapchain = VK_NULL_HANDLE;
2288 }
2289 std::cout << "vk: cleanupSwapchain complete\n";
2290 }
2291
2292 void VK_Window::drawFrame() {
2293 if (device == VK_NULL_HANDLE) {
2294 return;
2295 }
2296
2297 int pixel_w = 0;
2298 int pixel_h = 0;
2299 if (window != nullptr) {
2300 SDL_GetWindowSizeInPixels(window.get(), &pixel_w, &pixel_h);
2301 }
2302 if (pixel_w <= 0 || pixel_h <= 0) {
2303 framebuffer_resized = true;
2304 return;
2305 }
2306
2307 if (swapchain_extent.width != 0 && swapchain_extent.height != 0) {
2308 if (swapchain_extent.width != static_cast<uint32_t>(pixel_w) ||
2309 swapchain_extent.height != static_cast<uint32_t>(pixel_h)) {
2310 std::cout << std::format("mxvk: requesting swapchain recreation because window pixels changed from {}x{} to {}x{}\n",
2311 swapchain_extent.width,
2312 swapchain_extent.height,
2313 pixel_w,
2314 pixel_h);
2315 framebuffer_resized = true;
2317 return;
2318 }
2319 }
2320
2321 if (!ensureRenderResources()) {
2322 std::cout << "mxvk: creating deferred swapchain/render/sync resources\n";
2323 if (!ensureRenderResources()) {
2324 std::cerr << "mxvk: deferred resource creation failed; skipping frame\n";
2325 return;
2326 }
2327 }
2328
2329 const bool sync_ready =
2330 std::ranges::all_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) &&
2331 std::ranges::all_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) &&
2332 std::ranges::all_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; });
2333 if (!sync_ready) {
2334 return;
2335 }
2336
2337 if (sprite_state_dirty && !sprites.empty() && swapchain_format != VK_FORMAT_UNDEFINED) {
2338 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2339 if (!sprite) {
2340 continue;
2341 }
2345 sprite->rebuildPipeline();
2346 sprite->rebuildInstancedPipeline();
2347 }
2348 try {
2349 createSpritePipeline();
2350 } catch (const std::exception &ex) {
2351 std::cerr << std::format("mxvk: sprite pipeline build skipped: {}\n", ex.what());
2352 }
2353 sprite_state_dirty = false;
2354 }
2355
2356 if (fps_counter_enabled) {
2357 updateFpsCounter();
2358 }
2359
2360 if (text_state_dirty && text_renderer && swapchain_format != VK_FORMAT_UNDEFINED) {
2361 text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
2362 try {
2363 createTextPipeline();
2364 } catch (const std::exception &ex) {
2365 std::cerr << std::format("mxvk: text pipeline build skipped: {}\n", ex.what());
2366 }
2367 text_state_dirty = false;
2368 }
2369
2370 VkFence &frame_fence = in_flight_fences[current_frame];
2371 VkSemaphore &acquire_semaphore = image_available[current_frame];
2372 const size_t depth_slot = static_cast<size_t>(current_frame);
2373
2374 const VkResult wait_result = vkWaitForFences(device, 1, &frame_fence, VK_TRUE, UINT64_MAX);
2375 if (wait_result == VK_ERROR_DEVICE_LOST) {
2376 std::cerr << "mxvk: device lost while waiting for frame fence; stopping render loop\n";
2377 active = false;
2378 return;
2379 }
2380 if (wait_result != VK_SUCCESS) {
2381 std::cerr << std::format("mxvk: Failed waiting for frame fence (VkResult={})\n", static_cast<int>(wait_result));
2382 framebuffer_resized = true;
2383 return;
2384 }
2385
2386 uint32_t image_index = 0;
2387 const uint64_t acquire_timeout_ns = 100000000ULL; // 100 ms avoids UINT64_MAX forward-progress VUIDs.
2388 const VkResult acquire_result =
2389 vkAcquireNextImageKHR(device, swapchain, acquire_timeout_ns, acquire_semaphore, VK_NULL_HANDLE, &image_index);
2390
2391 static VkResult last_acquire_error = VK_SUCCESS;
2392 static uint32_t repeated_acquire_errors = 0;
2393
2394 if (acquire_result == VK_ERROR_OUT_OF_DATE_KHR) {
2395 last_acquire_error = VK_SUCCESS;
2396 repeated_acquire_errors = 0;
2397 std::cout << "mxvk: requesting swapchain recreation because acquire returned VK_ERROR_OUT_OF_DATE_KHR\n";
2399 framebuffer_resized = true;
2400 return;
2401 }
2402
2403 if (acquire_result == VK_ERROR_SURFACE_LOST_KHR) {
2404 if (last_acquire_error != acquire_result) {
2405 std::cerr << "mxvk: swapchain surface lost during acquire; requesting swapchain recreation\n";
2406 last_acquire_error = acquire_result;
2407 repeated_acquire_errors = 0;
2408 }
2409 std::cout << "mxvk: requesting swapchain recreation because acquire returned VK_ERROR_SURFACE_LOST_KHR\n";
2411 framebuffer_resized = true;
2412 return;
2413 }
2414
2415 if (acquire_result == VK_TIMEOUT || acquire_result == VK_NOT_READY) {
2416 // Non-fatal: no image available this frame.
2417 return;
2418 }
2419
2420 if (acquire_result != VK_SUCCESS && acquire_result != VK_SUBOPTIMAL_KHR) {
2421 if (last_acquire_error == acquire_result) {
2422 ++repeated_acquire_errors;
2423 if ((repeated_acquire_errors % 120U) == 0U) {
2424 std::cerr << std::format(
2425 "mxvk: repeated swapchain acquire failures continue (VkResult={})\n",
2426 static_cast<int>(acquire_result));
2427 }
2428 } else {
2429 last_acquire_error = acquire_result;
2430 repeated_acquire_errors = 0;
2431 std::cerr << std::format(
2432 "mxvk: Failed to acquire swapchain image (VkResult={})\n",
2433 static_cast<int>(acquire_result));
2434 }
2435
2436 if (acquire_result == VK_ERROR_DEVICE_LOST) {
2437 std::cerr << "mxvk: device lost; stopping render loop\n";
2438 active = false;
2439 }
2440 return;
2441 }
2442
2443 last_acquire_error = VK_SUCCESS;
2444 repeated_acquire_errors = 0;
2445
2446 if (image_index >= command_buffers.size() || image_index >= swapchain_images.size() || image_index >= swapchain_image_views.size()) {
2447 return;
2448 }
2449
2450 if (image_index >= image_fences.size()) {
2451 std::cerr << "mxvk: acquired image index exceeds tracked in-flight image count\n";
2452 return;
2453 }
2454
2455 if (image_index >= render_finished.size() || render_finished[image_index] == VK_NULL_HANDLE) {
2456 std::cerr << "mxvk: acquired image index exceeds render-finished semaphore count\n";
2457 return;
2458 }
2459
2460 VkSemaphore &present_semaphore = render_finished[image_index];
2461
2462 if (image_fences[image_index] != VK_NULL_HANDLE && image_fences[image_index] != frame_fence) {
2463 const VkResult image_wait_result = vkWaitForFences(device, 1, &image_fences[image_index], VK_TRUE, UINT64_MAX);
2464 if (image_wait_result == VK_ERROR_DEVICE_LOST) {
2465 std::cerr << "mxvk: device lost while waiting on acquired image fence; stopping render loop\n";
2466 active = false;
2467 return;
2468 }
2469 if (image_wait_result != VK_SUCCESS) {
2470 std::cerr << std::format("mxvk: Failed waiting for acquired image fence (VkResult={})\n", static_cast<int>(image_wait_result));
2471 framebuffer_resized = true;
2472 return;
2473 }
2474 }
2475
2476 const VkCommandBuffer cmd = command_buffers[image_index];
2477 vkResetCommandBuffer(cmd, 0);
2478
2479 VkCommandBufferBeginInfo begin_info{};
2480 begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
2481 if (vkBeginCommandBuffer(cmd, &begin_info) != VK_SUCCESS) {
2482 std::cerr << "mxvk: Failed to begin command buffer\n";
2483 framebuffer_resized = true;
2484 return;
2485 }
2486
2487 VkClearValue clear_value{};
2488 clear_value.color = clear_color;
2489 const bool use_post_process = post_process_enabled &&
2490 !post_process_sprites.empty() &&
2491 !post_process_images.empty() &&
2492 !post_process_views.empty() &&
2493 !post_process_initialized.empty() &&
2494 image_index < post_process_images.front().size() &&
2495 image_index < post_process_views.front().size() &&
2496 image_index < post_process_initialized.front().size();
2497
2498 VkImageMemoryBarrier2 to_color_barrier{};
2499 to_color_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2500 to_color_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
2501 to_color_barrier.srcAccessMask = VK_ACCESS_2_NONE;
2502 to_color_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2503 to_color_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2504 to_color_barrier.oldLayout =
2505 swapchain_image_initialized[image_index] ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_UNDEFINED;
2506 to_color_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2507 to_color_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2508 to_color_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2509 to_color_barrier.image = swapchain_images[image_index];
2510 to_color_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2511 to_color_barrier.subresourceRange.baseMipLevel = 0;
2512 to_color_barrier.subresourceRange.levelCount = 1;
2513 to_color_barrier.subresourceRange.baseArrayLayer = 0;
2514 to_color_barrier.subresourceRange.layerCount = 1;
2515
2516 VkDependencyInfo pre_render_dependency{};
2517 pre_render_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2518 pre_render_dependency.imageMemoryBarrierCount = 1;
2519 pre_render_dependency.pImageMemoryBarriers = &to_color_barrier;
2520 vkCmdPipelineBarrier2(cmd, &pre_render_dependency);
2521
2522 if (use_post_process) {
2523 VkImageMemoryBarrier2 post_target_barrier{};
2524 post_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2525 post_target_barrier.srcStageMask = post_process_initialized[0][image_index] ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_2_NONE;
2526 post_target_barrier.srcAccessMask = post_process_initialized[0][image_index] ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_NONE;
2527 post_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2528 post_target_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2529 post_target_barrier.oldLayout = post_process_initialized[0][image_index] ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
2530 post_target_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2531 post_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2532 post_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2533 post_target_barrier.image = post_process_images[0][image_index];
2534 post_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2535 post_target_barrier.subresourceRange.levelCount = 1;
2536 post_target_barrier.subresourceRange.layerCount = 1;
2537 VkDependencyInfo post_target_dependency{};
2538 post_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2539 post_target_dependency.imageMemoryBarrierCount = 1;
2540 post_target_dependency.pImageMemoryBarriers = &post_target_barrier;
2541 vkCmdPipelineBarrier2(cmd, &post_target_dependency);
2542 }
2543
2544 VkImageMemoryBarrier2 to_depth_barrier{};
2545 to_depth_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2546 to_depth_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
2547 to_depth_barrier.srcAccessMask = VK_ACCESS_2_NONE;
2548 to_depth_barrier.dstStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT;
2549 to_depth_barrier.dstAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
2550 to_depth_barrier.oldLayout =
2551 (depth_slot < depth_image_initialized.size() && depth_image_initialized[depth_slot])
2552 ? VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL
2553 : VK_IMAGE_LAYOUT_UNDEFINED;
2554 to_depth_barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
2555 to_depth_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2556 to_depth_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2557 if (depth_slot < depth_images.size()) {
2558 to_depth_barrier.image = depth_images[depth_slot];
2559 }
2560 to_depth_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
2561 to_depth_barrier.subresourceRange.baseMipLevel = 0;
2562 to_depth_barrier.subresourceRange.levelCount = 1;
2563 to_depth_barrier.subresourceRange.baseArrayLayer = 0;
2564 to_depth_barrier.subresourceRange.layerCount = 1;
2565
2566 if (to_depth_barrier.image != VK_NULL_HANDLE) {
2567 VkDependencyInfo pre_depth_dependency{};
2568 pre_depth_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2569 pre_depth_dependency.imageMemoryBarrierCount = 1;
2570 pre_depth_dependency.pImageMemoryBarriers = &to_depth_barrier;
2571 vkCmdPipelineBarrier2(cmd, &pre_depth_dependency);
2572 }
2573
2574 onPrepareFrameRendering(cmd, image_index);
2575 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2576 if (sprite) {
2577 sprite->prepareForRendering(cmd);
2578 }
2579 }
2580
2581 VkRenderingAttachmentInfo color_attachment{};
2582 color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
2583 color_attachment.imageView = use_post_process ? post_process_views[0][image_index] : swapchain_image_views[image_index];
2584 color_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2585 color_attachment.resolveMode = VK_RESOLVE_MODE_NONE;
2586 color_attachment.resolveImageView = VK_NULL_HANDLE;
2587 color_attachment.resolveImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
2588 color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
2589 color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
2590 color_attachment.clearValue = clear_value;
2591
2592 VkClearValue depth_clear_value{};
2593 depth_clear_value.depthStencil = {1.0f, 0};
2594
2595 VkRenderingAttachmentInfo depth_attachment{};
2596 depth_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
2597 if (depth_slot < depth_image_views.size()) {
2598 depth_attachment.imageView = depth_image_views[depth_slot];
2599 }
2600 depth_attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
2601 depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
2602 depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
2603 depth_attachment.clearValue = depth_clear_value;
2604
2605 VkRenderingAttachmentInfo stencil_attachment{};
2606 stencil_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
2607 onConfigureDepthStencilAttachments(depth_attachment, stencil_attachment, image_index);
2608
2609 VkRenderingInfo rendering_info{};
2610 rendering_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
2611 rendering_info.renderArea.offset = {0, 0};
2612 rendering_info.renderArea.extent = swapchain_extent;
2613 rendering_info.layerCount = 1;
2614 rendering_info.viewMask = 0;
2615 rendering_info.colorAttachmentCount = 1;
2616 rendering_info.pColorAttachments = &color_attachment;
2617 rendering_info.pDepthAttachment = (depth_attachment.imageView != VK_NULL_HANDLE) ? &depth_attachment : nullptr;
2618 rendering_info.pStencilAttachment = (stencil_attachment.imageView != VK_NULL_HANDLE) ? &stencil_attachment : nullptr;
2619
2620 vkCmdBeginRendering(cmd, &rendering_info);
2621
2622 VkViewport viewport{};
2623 viewport.x = 0.0F;
2624 viewport.y = 0.0F;
2625 viewport.width = static_cast<float>(swapchain_extent.width);
2626 viewport.height = static_cast<float>(swapchain_extent.height);
2627 viewport.minDepth = 0.0F;
2628 viewport.maxDepth = 1.0F;
2629 vkCmdSetViewport(cmd, 0, 1, &viewport);
2630
2631 VkRect2D scissor{};
2632 scissor.offset = {0, 0};
2633 scissor.extent = swapchain_extent;
2634 vkCmdSetScissor(cmd, 0, 1, &scissor);
2635
2636 onRecordCustomRendering(cmd, image_index);
2637
2638 // Draw 2D overlays after custom scene rendering so HUD/text stays on top.
2639 if (!sprites.empty()) {
2640 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2641 if (!sprite || isPostProcessSprite(sprite.get())) {
2642 continue;
2643 }
2644 if (sprite_pipeline != VK_NULL_HANDLE) {
2645 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
2646 }
2648 }
2649 }
2650
2651 if (text_renderer && text_pipeline != VK_NULL_HANDLE) {
2652 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, text_pipeline);
2654 }
2655
2656 vkCmdEndRendering(cmd);
2657 if (depth_slot < depth_image_initialized.size()) {
2658 depth_image_initialized[depth_slot] = true;
2659 }
2660
2661 if (use_post_process) {
2662 VkImageMemoryBarrier2 post_target_barrier{};
2663 post_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2664 post_target_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2665 post_target_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2666 post_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
2667 post_target_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
2668 post_target_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2669 post_target_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
2670 post_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2671 post_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2672 post_target_barrier.image = post_process_images[0][image_index];
2673 post_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2674 post_target_barrier.subresourceRange.levelCount = 1;
2675 post_target_barrier.subresourceRange.layerCount = 1;
2676 VkDependencyInfo post_target_dependency{};
2677 post_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2678 post_target_dependency.imageMemoryBarrierCount = 1;
2679 post_target_dependency.pImageMemoryBarriers = &post_target_barrier;
2680 vkCmdPipelineBarrier2(cmd, &post_target_dependency);
2681 post_process_initialized[0][image_index] = true;
2682
2683 size_t source_target = 0;
2684 for (size_t effect_index = 0; effect_index < post_process_sprites.size(); ++effect_index) {
2685 VK_Sprite *effect_sprite = post_process_sprites[effect_index];
2686 if (effect_sprite == nullptr) {
2687 continue;
2688 }
2689
2690 const bool final_effect = (effect_index + 1U) == post_process_sprites.size();
2691 const size_t destination_target = source_target == 0U ? 1U : 0U;
2692 VkImageView destination_view = final_effect ? swapchain_image_views[image_index] : post_process_views[destination_target][image_index];
2693
2694 if (!final_effect) {
2695 VkImageMemoryBarrier2 next_target_barrier{};
2696 next_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2697 next_target_barrier.srcStageMask = post_process_initialized[destination_target][image_index] ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_2_NONE;
2698 next_target_barrier.srcAccessMask = post_process_initialized[destination_target][image_index] ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_NONE;
2699 next_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2700 next_target_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2701 next_target_barrier.oldLayout = post_process_initialized[destination_target][image_index] ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
2702 next_target_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2703 next_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2704 next_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2705 next_target_barrier.image = post_process_images[destination_target][image_index];
2706 next_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2707 next_target_barrier.subresourceRange.levelCount = 1;
2708 next_target_barrier.subresourceRange.layerCount = 1;
2709 VkDependencyInfo next_target_dependency{};
2710 next_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2711 next_target_dependency.imageMemoryBarrierCount = 1;
2712 next_target_dependency.pImageMemoryBarriers = &next_target_barrier;
2713 vkCmdPipelineBarrier2(cmd, &next_target_dependency);
2714 }
2715
2716 VkRenderingAttachmentInfo post_attachment{};
2717 post_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
2718 post_attachment.imageView = destination_view;
2719 post_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2720 post_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
2721 post_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
2722 VkRenderingInfo post_info{};
2723 post_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
2724 post_info.renderArea.extent = swapchain_extent;
2725 post_info.layerCount = 1;
2726 post_info.colorAttachmentCount = 1;
2727 post_info.pColorAttachments = &post_attachment;
2728 vkCmdBeginRendering(cmd, &post_info);
2729
2730 if (effect_index < post_process_effect_time_enabled.size() && post_process_effect_time_enabled[effect_index]) {
2731 post_process_effect_params[effect_index][0] = std::chrono::duration<float>(std::chrono::steady_clock::now() - post_process_effect_start_times[effect_index]).count();
2732 }
2733 if (effect_index < post_process_effect_params.size()) {
2734 const std::array<float, 4> &params = post_process_effect_params[effect_index];
2735 effect_sprite->setShaderParams(params[0], params[1], params[2], params[3]);
2736 }
2737
2738 effect_sprite->setExternalTexture(post_process_views[source_target][image_index], static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
2739 effect_sprite->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
2740 renderStandaloneSprite(*effect_sprite, cmd);
2741 vkCmdEndRendering(cmd);
2742
2743 if (!final_effect) {
2744 VkImageMemoryBarrier2 sampled_target_barrier{};
2745 sampled_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2746 sampled_target_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2747 sampled_target_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2748 sampled_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
2749 sampled_target_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
2750 sampled_target_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2751 sampled_target_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
2752 sampled_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2753 sampled_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2754 sampled_target_barrier.image = post_process_images[destination_target][image_index];
2755 sampled_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2756 sampled_target_barrier.subresourceRange.levelCount = 1;
2757 sampled_target_barrier.subresourceRange.layerCount = 1;
2758 VkDependencyInfo sampled_target_dependency{};
2759 sampled_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2760 sampled_target_dependency.imageMemoryBarrierCount = 1;
2761 sampled_target_dependency.pImageMemoryBarriers = &sampled_target_barrier;
2762 vkCmdPipelineBarrier2(cmd, &sampled_target_dependency);
2763 post_process_initialized[destination_target][image_index] = true;
2764 source_target = destination_target;
2765 }
2766 }
2767 }
2768
2769 VkImageMemoryBarrier2 to_present_barrier{};
2770 to_present_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2771 to_present_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2772 to_present_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2773 to_present_barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
2774 to_present_barrier.dstAccessMask = VK_ACCESS_2_NONE;
2775 to_present_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2776 to_present_barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
2777 to_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2778 to_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2779 to_present_barrier.image = swapchain_images[image_index];
2780 to_present_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2781 to_present_barrier.subresourceRange.baseMipLevel = 0;
2782 to_present_barrier.subresourceRange.levelCount = 1;
2783 to_present_barrier.subresourceRange.baseArrayLayer = 0;
2784 to_present_barrier.subresourceRange.layerCount = 1;
2785
2786 VkDependencyInfo post_render_dependency{};
2787 post_render_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2788 post_render_dependency.imageMemoryBarrierCount = 1;
2789 post_render_dependency.pImageMemoryBarriers = &to_present_barrier;
2790 vkCmdPipelineBarrier2(cmd, &post_render_dependency);
2791
2792 if (vkEndCommandBuffer(cmd) != VK_SUCCESS) {
2793 std::cerr << "mxvk: Failed to end command buffer\n";
2794 framebuffer_resized = true;
2795 return;
2796 }
2797
2798 VkSemaphoreSubmitInfo wait_semaphore_info{};
2799 wait_semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
2800 wait_semaphore_info.semaphore = acquire_semaphore;
2801 wait_semaphore_info.value = 0;
2802 wait_semaphore_info.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2803 wait_semaphore_info.deviceIndex = 0;
2804
2805 VkCommandBufferSubmitInfo command_buffer_info{};
2806 command_buffer_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
2807 command_buffer_info.commandBuffer = cmd;
2808 command_buffer_info.deviceMask = 0;
2809
2810 VkSemaphoreSubmitInfo signal_semaphore_info{};
2811 signal_semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
2812 signal_semaphore_info.semaphore = present_semaphore;
2813 signal_semaphore_info.value = 0;
2814 signal_semaphore_info.stageMask = VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT;
2815 signal_semaphore_info.deviceIndex = 0;
2816
2817 VkSubmitInfo2 submit_info{};
2818 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
2819 submit_info.waitSemaphoreInfoCount = 1;
2820 submit_info.pWaitSemaphoreInfos = &wait_semaphore_info;
2821 submit_info.commandBufferInfoCount = 1;
2822 submit_info.pCommandBufferInfos = &command_buffer_info;
2823 submit_info.signalSemaphoreInfoCount = 1;
2824 submit_info.pSignalSemaphoreInfos = &signal_semaphore_info;
2825
2826 const VkResult fence_reset_result = vkResetFences(device, 1, &frame_fence);
2827 if (fence_reset_result == VK_ERROR_DEVICE_LOST) {
2828 std::cerr << "mxvk: device lost while resetting frame fence; stopping render loop\n";
2829 active = false;
2830 return;
2831 }
2832 if (fence_reset_result != VK_SUCCESS) {
2833 std::cerr << std::format("mxvk: Failed to reset frame fence (VkResult={})\n", static_cast<int>(fence_reset_result));
2834 framebuffer_resized = true;
2835 return;
2836 }
2837
2838 const VkResult submit_result = vkQueueSubmit2(graphics_queue, 1, &submit_info, frame_fence);
2839 if (submit_result == VK_ERROR_DEVICE_LOST) {
2840 std::cerr << "mxvk: device lost during queue submit; stopping render loop\n";
2841 active = false;
2842 return;
2843 }
2844 if (submit_result != VK_SUCCESS) {
2845 std::cerr << std::format("mxvk: Failed to submit draw command (VkResult={})\n", static_cast<int>(submit_result));
2846 // We already acquired an image this frame. Force swapchain recreation so we do not reuse
2847 // a signaled acquire semaphore that never got consumed by a successful submit.
2848 framebuffer_resized = true;
2849 return;
2850 }
2851 image_fences[image_index] = frame_fence;
2852 swapchain_image_initialized[image_index] = true;
2853
2854 VkPresentInfoKHR present_info{};
2855 present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
2856 present_info.waitSemaphoreCount = 1;
2857 present_info.pWaitSemaphores = &present_semaphore;
2858 present_info.swapchainCount = 1;
2859 present_info.pSwapchains = &swapchain;
2860 present_info.pImageIndices = &image_index;
2861
2862 const VkResult present_result = vkQueuePresentKHR(present_queue, &present_info);
2863 if (present_result == VK_SUCCESS || present_result == VK_SUBOPTIMAL_KHR) {
2864 last_presented_image_index = image_index;
2865 }
2866
2867 if (present_result == VK_ERROR_OUT_OF_DATE_KHR) {
2868 std::cout << "mxvk: requesting swapchain recreation because present returned VK_ERROR_OUT_OF_DATE_KHR\n";
2870 last_resize_event_ms = SDL_GetTicks();
2871 framebuffer_resized = true;
2872 } else if (present_result == VK_SUBOPTIMAL_KHR) {
2873 int present_pixel_w = 0;
2874 int present_pixel_h = 0;
2875 if (window != nullptr) {
2876 SDL_GetWindowSizeInPixels(window.get(), &present_pixel_w, &present_pixel_h);
2877 }
2878 const bool known_present_extent = present_pixel_w > 0 && present_pixel_h > 0;
2879 const bool present_extent_changed =
2880 known_present_extent &&
2881 (swapchain_extent.width != static_cast<uint32_t>(present_pixel_w) ||
2882 swapchain_extent.height != static_cast<uint32_t>(present_pixel_h));
2883 if (!known_present_extent || present_extent_changed) {
2884 std::cout << "mxvk: requesting swapchain recreation because present returned VK_SUBOPTIMAL_KHR with changed window extent\n";
2885 last_resize_event_ms = SDL_GetTicks();
2886 framebuffer_resized = true;
2888 }
2889 } else if (present_result != VK_SUCCESS) {
2890 std::cerr << "mxvk: Failed to present swapchain image\n";
2891 }
2892
2893 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2894 if (sprite) {
2895 sprite->clearQueue();
2896 }
2897 }
2898 if (text_renderer) {
2899 text_renderer->clearQueue();
2900 }
2901
2902 current_frame = (current_frame + 1U) % max_frames_in_flight;
2903 if (!retired_swapchains.empty() && (present_result == VK_SUCCESS || present_result == VK_SUBOPTIMAL_KHR)) {
2904 for (VkSwapchainKHR retired : retired_swapchains) {
2905 vkDestroySwapchainKHR(device, retired, nullptr);
2906 }
2907 retired_swapchains.clear();
2908 }
2909 }
2910
2912 return validation_enabled;
2913 }
2914
2915 bool VK_Window::hasValidationLayerSupport() {
2916 uint32_t layer_count = 0;
2917 const VkResult count_result = vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
2918 if (count_result != VK_SUCCESS) {
2919 std::cerr << std::format(
2920 "mxvk: Failed to query validation layer count (VkResult={})\n",
2921 static_cast<int>(count_result));
2922 return false;
2923 }
2924
2925 if (layer_count == 0U) {
2926 return false;
2927 }
2928
2929 std::vector<VkLayerProperties> available_layers(layer_count);
2930 const VkResult layers_result = vkEnumerateInstanceLayerProperties(&layer_count, available_layers.data());
2931 if (layers_result != VK_SUCCESS) {
2932 std::cerr << std::format(
2933 "mxvk: Failed to enumerate validation layers (VkResult={})\n",
2934 static_cast<int>(layers_result));
2935 return false;
2936 }
2937
2938 return std::ranges::any_of(
2939 available_layers,
2940 [](const VkLayerProperties &layer) {
2941 return std::strcmp(layer.layerName, validation_layer_name) == 0;
2942 });
2943 }
2944
2945 std::optional<VkDebugUtilsMessengerCreateInfoEXT> VK_Window::makeDebugMessengerCreateInfo() {
2946 VkDebugUtilsMessengerCreateInfoEXT create_info{};
2947 create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
2948 create_info.messageSeverity =
2949 VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
2950 VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
2951 VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
2952 create_info.messageType =
2953 VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
2954 VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
2955 VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
2956 create_info.pfnUserCallback = debugCallback;
2957 create_info.pUserData = nullptr;
2958 return create_info;
2959 }
2960
2961 void VK_Window::setupDebugMessenger() {
2962 if (!validation_enabled || instance == VK_NULL_HANDLE) {
2963 return;
2964 }
2965
2966 const std::optional<VkDebugUtilsMessengerCreateInfoEXT> maybe_create_info = makeDebugMessengerCreateInfo();
2967 if (!maybe_create_info.has_value()) {
2968 std::cerr << "mxvk: unable to build debug messenger create info\n";
2969 return;
2970 }
2971
2972 const VkResult result = vkCreateDebugUtilsMessengerEXT(
2973 instance,
2974 &maybe_create_info.value(),
2975 nullptr,
2977 if (result != VK_SUCCESS) {
2978 std::cerr << "mxvk: failed to create Vulkan debug messenger\n";
2979 debug_messenger = VK_NULL_HANDLE;
2980 }
2981 }
2982
2983 void VK_Window::cleanupDebugMessenger() {
2984 if (debug_messenger != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) {
2985 std::cout << "vk: destroying debug messenger\n";
2986 vkDestroyDebugUtilsMessengerEXT(instance, debug_messenger, nullptr);
2987 debug_messenger = VK_NULL_HANDLE;
2988 }
2989 }
2990
2991 void VK_Window::setFont(const std::string &fontPath, int fontSize) {
2992 if (fontPath.empty() || fontSize <= 0) {
2993 throw mxvk::Exception("setFont requires a non-empty path and positive font size");
2994 }
2995
2996 font_path = fontPath;
2997 font_size = fontSize;
2998 font_configured = true;
2999
3000 if (device == VK_NULL_HANDLE) {
3001 throw mxvk::Exception("Cannot set font before Vulkan device initialization");
3002 }
3003 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3004 createDevice();
3005 }
3006 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3007 throw mxvk::Exception("Cannot initialize text renderer before swapchain and command resources are available");
3008 }
3009
3010 if (!text_renderer) {
3011 ensureTextRenderer();
3012 } else {
3014 }
3015 text_state_dirty = true;
3016 }
3017
3018 void VK_Window::printText(const std::string &text, int x, int y, const SDL_Color &col) {
3019 if (text.empty()) {
3020 return;
3021 }
3022
3023 if (!font_configured) {
3024 throw mxvk::Exception("printText requires setFont() to be called first");
3025 }
3026
3027 ensureTextRenderer();
3028 text_renderer->printTextG_Solid(text, x, y, col);
3029 }
3030
3031 void VK_Window::printText(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *font) {
3032 if (text.empty()) {
3033 return;
3034 }
3035
3036 if (font == nullptr) {
3037 throw mxvk::Exception("printText requires a non-null TTF_Font");
3038 }
3039
3040 ensureTextRenderer(resolveDefaultFontPath(), font_size);
3041 if (!text_renderer) {
3042 throw mxvk::Exception("printText with an explicit font could not initialize the text renderer");
3043 }
3044 text_renderer->printTextG_Solid(text, x, y, col, font);
3045 }
3046
3047 void VK_Window::printText(const std::string &text, int x, int y, const SDL_Color &col, const Font &font) {
3048 if (text.empty()) {
3049 return;
3050 }
3051
3052 if (!font) {
3053 throw mxvk::Exception("printText requires a valid mxvk::Font");
3054 }
3055
3056 ensureTextRenderer(font.path(), font.size());
3057 if (!text_renderer) {
3058 throw mxvk::Exception("printText with an explicit font could not initialize the text renderer");
3059 }
3060 text_renderer->printTextG_Solid(text, x, y, col, font);
3061 }
3062
3064 if (text_renderer) {
3065 text_renderer->clearQueue();
3066 }
3067 }
3068
3069 bool VK_Window::getTextDimensions(const std::string &text, int &width, int &height) {
3070 if (!font_configured) {
3071 throw mxvk::Exception("getTextDimensions requires setFont() to be called first");
3072 }
3073
3074 if (!text_renderer) {
3075 ensureTextRenderer();
3076 }
3077 if (!text_renderer) {
3078 width = 0;
3079 height = 0;
3080 return false;
3081 }
3082 return text_renderer->getTextDimensions(text, width, height);
3083 }
3084
3085 bool VK_Window::getTextDimensions(const std::string &text, int &width, int &height, TTF_Font *font) {
3086 if (font == nullptr) {
3087 width = 0;
3088 height = 0;
3089 return false;
3090 }
3091
3092 ensureTextRenderer(resolveDefaultFontPath(), font_size);
3093 if (!text_renderer) {
3094 width = 0;
3095 height = 0;
3096 return false;
3097 }
3098 return text_renderer->getTextDimensions(text, width, height, font);
3099 }
3100
3101 bool VK_Window::getTextDimensions(const std::string &text, int &width, int &height, const Font &font) {
3102 if (!font) {
3103 width = 0;
3104 height = 0;
3105 return false;
3106 }
3107
3108 ensureTextRenderer(font.path(), font.size());
3109 if (!text_renderer) {
3110 width = 0;
3111 height = 0;
3112 return false;
3113 }
3114 return text_renderer->getTextDimensions(text, width, height, font);
3115 }
3116
3117 void VK_Window::ensureTextRenderer() {
3118 if (text_renderer) {
3119 return;
3120 }
3121
3122 if (!font_configured || font_path.empty() || font_size <= 0) {
3123 return;
3124 }
3125
3126 if (device == VK_NULL_HANDLE) {
3127 return;
3128 }
3129 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3130 createDevice();
3131 }
3132 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3133 return;
3134 }
3135
3136 if (text_descriptor_set_layout == VK_NULL_HANDLE) {
3137 createTextDescriptorSetLayout();
3138 }
3139
3141 text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
3142 text_state_dirty = true;
3143 }
3144
3145 void VK_Window::ensureTextRenderer(const std::string &fallbackFontPath, int fallbackFontSize) {
3146 if (text_renderer) {
3147 return;
3148 }
3149
3150 if (device == VK_NULL_HANDLE) {
3151 return;
3152 }
3153 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3154 createDevice();
3155 }
3156 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3157 return;
3158 }
3159
3160 const std::string renderer_font_path = font_configured ? font_path : fallbackFontPath;
3161 const int renderer_font_size = font_configured ? font_size : fallbackFontSize;
3162 if (renderer_font_path.empty() || renderer_font_size <= 0) {
3163 return;
3164 }
3165
3166 if (text_descriptor_set_layout == VK_NULL_HANDLE) {
3167 createTextDescriptorSetLayout();
3168 }
3169
3170 text_renderer = std::make_unique<VK_Text>(device, physical_device, graphics_queue, command_pool, renderer_font_path, renderer_font_size);
3171 text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
3172 text_state_dirty = true;
3173 }
3174
3175 std::string VK_Window::resolveDefaultFontPath() const {
3176 std::vector<std::filesystem::path> candidates{};
3177
3178 if (const char *basePath = SDL_GetBasePath(); basePath != nullptr) {
3179 const std::filesystem::path executableDir(basePath);
3180 candidates.push_back(executableDir / "data" / "default.ttf");
3181 candidates.push_back(executableDir / "default.ttf");
3182 }
3183
3184 candidates.push_back(std::filesystem::path("data") / "default.ttf");
3185
3186 if (!font_path.empty()) {
3187 const std::filesystem::path configured_font_path(font_path);
3188 if (configured_font_path.has_parent_path()) {
3189 candidates.push_back(configured_font_path.parent_path() / "default.ttf");
3190 }
3191 }
3192
3193 candidates.push_back(std::filesystem::path(MXVK_DEFAULT_FONT_DIR) / "default.ttf");
3194
3195 std::error_code exists_error{};
3196 for (const std::filesystem::path &candidate : candidates) {
3197 if (std::filesystem::exists(candidate, exists_error)) {
3198 return candidate.string();
3199 }
3200 exists_error.clear();
3201 }
3202
3203 return {};
3204 }
3205
3206 void VK_Window::toggleFpsCounter() {
3209 fps_counter_sample_time = std::chrono::steady_clock::now();
3210 fps_counter_text = "FPS: --";
3211 std::cout << std::format("mxvk: FPS counter {}\n", fps_counter_enabled ? "enabled" : "disabled");
3212
3213 if (!fps_counter_enabled) {
3214 return;
3215 }
3216
3218 const std::string default_font_path = resolveDefaultFontPath();
3219 if (default_font_path.empty()) {
3220 std::cerr << "mxvk: F12 FPS counter could not locate data/default.ttf\n";
3221 fps_counter_enabled = false;
3222 return;
3223 }
3224 fps_counter_font.reset(default_font_path, 18);
3226 }
3227 }
3228
3229 void VK_Window::updateFpsCounter() {
3230 if (!fps_counter_enabled) {
3231 return;
3232 }
3233
3235 const std::string default_font_path = resolveDefaultFontPath();
3236 if (default_font_path.empty()) {
3237 std::cerr << "mxvk: F12 FPS counter could not locate data/default.ttf\n";
3238 fps_counter_enabled = false;
3239 return;
3240 }
3241 fps_counter_font.reset(default_font_path, 18);
3243 }
3244
3246 const auto now = std::chrono::steady_clock::now();
3247 const double elapsed = std::chrono::duration<double>(now - fps_counter_sample_time).count();
3248 if (elapsed >= 0.5) {
3249 const double fps = static_cast<double>(fps_counter_frame_count) / elapsed;
3252 fps_counter_text = std::format("FPS: {:.1f}", fps);
3253 }
3254
3255 printText(fps_counter_text, 12, 10, SDL_Color{255, 255, 255, 255}, fps_counter_font);
3256 }
3257
3258 void VK_Window::createTextDescriptorSetLayout() {
3259 if (device == VK_NULL_HANDLE || text_descriptor_set_layout != VK_NULL_HANDLE) {
3260 return;
3261 }
3262
3263 VkDescriptorSetLayoutBinding sampler_binding{};
3264 sampler_binding.binding = 0;
3265 sampler_binding.descriptorCount = 1;
3266 sampler_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
3267 sampler_binding.pImmutableSamplers = nullptr;
3268 sampler_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
3269
3270 VkDescriptorSetLayoutCreateInfo layout_info{};
3271 layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
3272 layout_info.bindingCount = 1;
3273 layout_info.pBindings = &sampler_binding;
3274
3275 if (vkCreateDescriptorSetLayout(device, &layout_info, nullptr, &text_descriptor_set_layout) != VK_SUCCESS) {
3276 throw mxvk::Exception("Failed to create text descriptor set layout");
3277 }
3278 }
3279
3280 void VK_Window::destroyTextPipeline() {
3281 if (device == VK_NULL_HANDLE) {
3282 text_pipeline = VK_NULL_HANDLE;
3283 text_pipeline_layout = VK_NULL_HANDLE;
3284 return;
3285 }
3286
3287 if (text_pipeline != VK_NULL_HANDLE) {
3288 std::cout << "vk: destroying text pipeline\n";
3289 vkDestroyPipeline(device, text_pipeline, nullptr);
3290 text_pipeline = VK_NULL_HANDLE;
3291 }
3292 if (text_pipeline_layout != VK_NULL_HANDLE) {
3293 std::cout << "vk: destroying text pipeline layout\n";
3294 vkDestroyPipelineLayout(device, text_pipeline_layout, nullptr);
3295 text_pipeline_layout = VK_NULL_HANDLE;
3296 }
3297 }
3298
3299 void VK_Window::createTextPipeline() {
3300 if (device == VK_NULL_HANDLE || swapchain_format == VK_FORMAT_UNDEFINED || text_descriptor_set_layout == VK_NULL_HANDLE) {
3301 return;
3302 }
3303
3304 destroyTextPipeline();
3305
3306 const std::vector<char> vert_shader = loadSpv(resolveRuntimeShaderPath("text.vert.spv", MXVK_TEXT_SHADER_DIR));
3307 const std::vector<char> frag_shader = loadSpv(resolveRuntimeShaderPath("text.frag.spv", MXVK_TEXT_SHADER_DIR));
3308
3309 const VkShaderModule vert_module = createShaderModule(device, vert_shader);
3310 VkShaderModule frag_module = VK_NULL_HANDLE;
3311
3312 try {
3313 frag_module = createShaderModule(device, frag_shader);
3314
3315 VkPipelineShaderStageCreateInfo vert_stage{};
3316 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3317 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
3318 vert_stage.module = vert_module;
3319 vert_stage.pName = "main";
3320
3321 VkPipelineShaderStageCreateInfo frag_stage{};
3322 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3323 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
3324 frag_stage.module = frag_module;
3325 frag_stage.pName = "main";
3326
3327 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
3328
3329 VkVertexInputBindingDescription binding_description{};
3330 binding_description.binding = 0;
3331 binding_description.stride = sizeof(float) * 4;
3332 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
3333
3334 std::array<VkVertexInputAttributeDescription, 2> attributes{};
3335 attributes[0].binding = 0;
3336 attributes[0].location = 0;
3337 attributes[0].format = VK_FORMAT_R32G32_SFLOAT;
3338 attributes[0].offset = 0;
3339 attributes[1].binding = 0;
3340 attributes[1].location = 1;
3341 attributes[1].format = VK_FORMAT_R32G32_SFLOAT;
3342 attributes[1].offset = sizeof(float) * 2;
3343
3344 VkPipelineVertexInputStateCreateInfo vertex_input{};
3345 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3346 vertex_input.vertexBindingDescriptionCount = 1;
3347 vertex_input.pVertexBindingDescriptions = &binding_description;
3348 vertex_input.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributes.size());
3349 vertex_input.pVertexAttributeDescriptions = attributes.data();
3350
3351 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
3352 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3353 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
3354 input_assembly.primitiveRestartEnable = VK_FALSE;
3355
3356 const std::array<VkDynamicState, 2> dynamic_states = {
3357 VK_DYNAMIC_STATE_VIEWPORT,
3358 VK_DYNAMIC_STATE_SCISSOR,
3359 };
3360 VkPipelineDynamicStateCreateInfo dynamic_state{};
3361 dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
3362 dynamic_state.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
3363 dynamic_state.pDynamicStates = dynamic_states.data();
3364
3365 VkPipelineViewportStateCreateInfo viewport_state{};
3366 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
3367 viewport_state.viewportCount = 1;
3368 viewport_state.scissorCount = 1;
3369
3370 VkPipelineRasterizationStateCreateInfo rasterizer{};
3371 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3372 rasterizer.depthClampEnable = VK_FALSE;
3373 rasterizer.rasterizerDiscardEnable = VK_FALSE;
3374 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
3375 rasterizer.lineWidth = 1.0F;
3376 rasterizer.cullMode = VK_CULL_MODE_NONE;
3377 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
3378 rasterizer.depthBiasEnable = VK_FALSE;
3379
3380 VkPipelineMultisampleStateCreateInfo multisampling{};
3381 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3382 multisampling.sampleShadingEnable = VK_FALSE;
3383 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
3384
3385 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
3386 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3387 depth_stencil.depthTestEnable = VK_FALSE;
3388 depth_stencil.depthWriteEnable = VK_FALSE;
3389
3390 VkPipelineColorBlendAttachmentState color_attachment{};
3391 color_attachment.colorWriteMask =
3392 VK_COLOR_COMPONENT_R_BIT |
3393 VK_COLOR_COMPONENT_G_BIT |
3394 VK_COLOR_COMPONENT_B_BIT |
3395 VK_COLOR_COMPONENT_A_BIT;
3396 color_attachment.blendEnable = VK_TRUE;
3397 color_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3398 color_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
3399 color_attachment.colorBlendOp = VK_BLEND_OP_ADD;
3400 color_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
3401 color_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
3402 color_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
3403
3404 VkPipelineColorBlendStateCreateInfo color_blending{};
3405 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3406 color_blending.logicOpEnable = VK_FALSE;
3407 color_blending.attachmentCount = 1;
3408 color_blending.pAttachments = &color_attachment;
3409
3410 VkPushConstantRange push_constant_range{};
3411 push_constant_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
3412 push_constant_range.offset = 0;
3413 push_constant_range.size = sizeof(float) * 4;
3414
3415 VkPipelineLayoutCreateInfo pipeline_layout_info{};
3416 pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3417 pipeline_layout_info.setLayoutCount = 1;
3418 pipeline_layout_info.pSetLayouts = &text_descriptor_set_layout;
3419 pipeline_layout_info.pushConstantRangeCount = 1;
3420 pipeline_layout_info.pPushConstantRanges = &push_constant_range;
3421
3422 if (vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &text_pipeline_layout) != VK_SUCCESS) {
3423 throw mxvk::Exception("Failed to create text pipeline layout");
3424 }
3425
3426 VkPipelineRenderingCreateInfo rendering_info{};
3427 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
3428 rendering_info.viewMask = 0;
3429 rendering_info.colorAttachmentCount = 1;
3430 rendering_info.pColorAttachmentFormats = &swapchain_format;
3431 if (depth_format != VK_FORMAT_UNDEFINED) {
3432 rendering_info.depthAttachmentFormat = depth_format;
3433 }
3434
3435 VkGraphicsPipelineCreateInfo pipeline_info{};
3436 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3437 pipeline_info.pNext = &rendering_info;
3438 pipeline_info.stageCount = 2;
3439 pipeline_info.pStages = shader_stages;
3440 pipeline_info.pVertexInputState = &vertex_input;
3441 pipeline_info.pInputAssemblyState = &input_assembly;
3442 pipeline_info.pViewportState = &viewport_state;
3443 pipeline_info.pRasterizationState = &rasterizer;
3444 pipeline_info.pMultisampleState = &multisampling;
3445 pipeline_info.pDepthStencilState = &depth_stencil;
3446 pipeline_info.pColorBlendState = &color_blending;
3447 pipeline_info.pDynamicState = &dynamic_state;
3448 pipeline_info.layout = text_pipeline_layout;
3449 pipeline_info.renderPass = VK_NULL_HANDLE;
3450 pipeline_info.subpass = 0;
3451 pipeline_info.basePipelineHandle = VK_NULL_HANDLE;
3452 pipeline_info.basePipelineIndex = -1;
3453
3454 if (vkCreateGraphicsPipelines(device, pipeline_cache, 1, &pipeline_info, nullptr, &text_pipeline) != VK_SUCCESS) {
3455 throw mxvk::Exception("Failed to create text graphics pipeline");
3456 }
3457 } catch (...) {
3458 if (text_pipeline != VK_NULL_HANDLE) {
3459 vkDestroyPipeline(device, text_pipeline, nullptr);
3460 text_pipeline = VK_NULL_HANDLE;
3461 }
3462 if (text_pipeline_layout != VK_NULL_HANDLE) {
3463 vkDestroyPipelineLayout(device, text_pipeline_layout, nullptr);
3464 text_pipeline_layout = VK_NULL_HANDLE;
3465 }
3466 if (frag_module != VK_NULL_HANDLE) {
3467 vkDestroyShaderModule(device, frag_module, nullptr);
3468 }
3469 vkDestroyShaderModule(device, vert_module, nullptr);
3470 throw;
3471 }
3472
3473 vkDestroyShaderModule(device, frag_module, nullptr);
3474 vkDestroyShaderModule(device, vert_module, nullptr);
3475 }
3476
3477 VK_Sprite *VK_Window::createSprite(const std::string &pngPath, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
3478 if (device == VK_NULL_HANDLE) {
3479 throw mxvk::Exception("Cannot create sprite before Vulkan device initialization");
3480 }
3481 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3482 createDevice();
3483 }
3484 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3485 throw mxvk::Exception("Cannot create sprite before swapchain and command resources are available");
3486 }
3487
3488 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
3489 createSpriteDescriptorSetLayout();
3490 }
3491
3492 auto sprite = std::make_unique<VK_Sprite>(device, physical_device, graphics_queue, command_pool);
3497
3498 if (!vertexShaderPath.empty()) {
3499 sprite->setVertexShaderPath(vertexShaderPath);
3500 }
3501
3502 sprite->loadSprite(pngPath, fragmentShaderPath);
3503
3504 VK_Sprite *const sprite_ptr = sprite.get();
3505 sprites.push_back(std::move(sprite));
3506 sprite_state_dirty = true;
3507 return sprite_ptr;
3508 }
3509
3510 VK_Sprite *VK_Window::createSprite(SDL_Surface *surface, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
3511 if (surface == nullptr) {
3512 throw mxvk::Exception("Cannot create sprite from a null SDL surface");
3513 }
3514 if (device == VK_NULL_HANDLE) {
3515 throw mxvk::Exception("Cannot create sprite before Vulkan device initialization");
3516 }
3517 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3518 createDevice();
3519 }
3520 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3521 throw mxvk::Exception("Cannot create sprite before swapchain and command resources are available");
3522 }
3523
3524 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
3525 createSpriteDescriptorSetLayout();
3526 }
3527
3528 auto sprite = std::make_unique<VK_Sprite>(device, physical_device, graphics_queue, command_pool);
3533
3534 if (!vertexShaderPath.empty()) {
3535 sprite->setVertexShaderPath(vertexShaderPath);
3536 }
3537
3538 sprite->loadSprite(surface, fragmentShaderPath);
3539
3540 VK_Sprite *const sprite_ptr = sprite.get();
3541 sprites.push_back(std::move(sprite));
3542 sprite_state_dirty = true;
3543 return sprite_ptr;
3544 }
3545
3546 VK_Sprite *VK_Window::createSprite(int width, int height, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
3547 if (width <= 0 || height <= 0) {
3548 throw mxvk::Exception("Sprite dimensions must be positive");
3549 }
3550 if (device == VK_NULL_HANDLE) {
3551 throw mxvk::Exception("Cannot create sprite before Vulkan device initialization");
3552 }
3553 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3554 createDevice();
3555 }
3556 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3557 throw mxvk::Exception("Cannot create sprite before swapchain and command resources are available");
3558 }
3559
3560 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
3561 createSpriteDescriptorSetLayout();
3562 }
3563
3564 auto sprite = std::make_unique<VK_Sprite>(device, physical_device, graphics_queue, command_pool);
3569
3570 sprite->createEmptySprite(width, height, vertexShaderPath, fragmentShaderPath);
3571
3572 VK_Sprite *const sprite_ptr = sprite.get();
3573 sprites.push_back(std::move(sprite));
3574 sprite_state_dirty = true;
3575 return sprite_ptr;
3576 }
3577
3578 VK_Sprite3D *VK_Window::createSprite3D(const std::string &pngPath, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
3579 if (device == VK_NULL_HANDLE) {
3580 throw mxvk::Exception("Cannot create 3D sprite before Vulkan device initialization");
3581 }
3582 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3583 createDevice();
3584 }
3585 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3586 throw mxvk::Exception("Cannot create 3D sprite before swapchain and command resources are available");
3587 }
3588
3589 const std::string vertPath = vertexShaderPath.empty()
3590 ? resolveRuntimeShaderPath("sprite3d.vert.spv", MXVK_SPRITE3D_SHADER_DIR)
3591 : vertexShaderPath;
3592 const std::string fragPath = fragmentShaderPath.empty()
3593 ? resolveRuntimeShaderPath("sprite3d.frag.spv", MXVK_SPRITE3D_SHADER_DIR)
3594 : fragmentShaderPath;
3595
3596 auto sprite = std::make_unique<VK_Sprite3D>();
3597 sprite->load(this, pngPath, vertPath, fragPath);
3598
3599 VK_Sprite3D *const sprite_ptr = sprite.get();
3600 sprites3d.push_back(std::move(sprite));
3601 return sprite_ptr;
3602 }
3603
3604 VK_Sprite3D *VK_Window::createSprite3D(SDL_Surface *surface, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
3605 if (surface == nullptr) {
3606 throw mxvk::Exception("Cannot create 3D sprite from a null SDL surface");
3607 }
3608 if (device == VK_NULL_HANDLE) {
3609 throw mxvk::Exception("Cannot create 3D sprite before Vulkan device initialization");
3610 }
3611 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3612 createDevice();
3613 }
3614 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
3615 throw mxvk::Exception("Cannot create 3D sprite before swapchain and command resources are available");
3616 }
3617
3618 const std::string vertPath = vertexShaderPath.empty()
3619 ? resolveRuntimeShaderPath("sprite3d.vert.spv", MXVK_SPRITE3D_SHADER_DIR)
3620 : vertexShaderPath;
3621 const std::string fragPath = fragmentShaderPath.empty()
3622 ? resolveRuntimeShaderPath("sprite3d.frag.spv", MXVK_SPRITE3D_SHADER_DIR)
3623 : fragmentShaderPath;
3624
3625 auto sprite = std::make_unique<VK_Sprite3D>();
3626 sprite->load(this, surface, vertPath, fragPath);
3627
3628 VK_Sprite3D *const sprite_ptr = sprite.get();
3629 sprites3d.push_back(std::move(sprite));
3630 return sprite_ptr;
3631 }
3632
3634 if (on)
3635 SDL_ShowCursor();
3636 else
3637 SDL_HideCursor();
3638 }
3639
3640 void VK_Window::createSpriteDescriptorSetLayout() {
3641 if (device == VK_NULL_HANDLE || sprite_descriptor_set_layout != VK_NULL_HANDLE) {
3642 return;
3643 }
3644
3645 VkDescriptorSetLayoutBinding sampler_binding{};
3646 sampler_binding.binding = 0;
3647 sampler_binding.descriptorCount = 1;
3648 sampler_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
3649 sampler_binding.pImmutableSamplers = nullptr;
3650 sampler_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
3651
3652 VkDescriptorSetLayoutCreateInfo layout_info{};
3653 layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
3654 layout_info.bindingCount = 1;
3655 layout_info.pBindings = &sampler_binding;
3656
3657 if (vkCreateDescriptorSetLayout(device, &layout_info, nullptr, &sprite_descriptor_set_layout) != VK_SUCCESS) {
3658 throw mxvk::Exception("Failed to create sprite descriptor set layout");
3659 }
3660 }
3661
3662 void VK_Window::destroySpritePipeline() {
3663 if (device == VK_NULL_HANDLE) {
3664 sprite_pipeline = VK_NULL_HANDLE;
3665 sprite_pipeline_layout = VK_NULL_HANDLE;
3666 return;
3667 }
3668
3669 if (sprite_pipeline != VK_NULL_HANDLE) {
3670 std::cout << "vk: destroying sprite pipeline\n";
3671 vkDestroyPipeline(device, sprite_pipeline, nullptr);
3672 sprite_pipeline = VK_NULL_HANDLE;
3673 }
3674 if (sprite_pipeline_layout != VK_NULL_HANDLE) {
3675 std::cout << "vk: destroying sprite pipeline layout\n";
3676 vkDestroyPipelineLayout(device, sprite_pipeline_layout, nullptr);
3677 sprite_pipeline_layout = VK_NULL_HANDLE;
3678 }
3679 }
3680
3681 void VK_Window::createSpritePipeline() {
3682 if (device == VK_NULL_HANDLE || swapchain_format == VK_FORMAT_UNDEFINED) {
3683 return;
3684 }
3685 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
3686 createSpriteDescriptorSetLayout();
3687 }
3688
3689 destroySpritePipeline();
3690
3691 const std::vector<char> vert_shader = loadSpv(resolveRuntimeShaderPath("sprite.vert.spv", MXVK_SPRITE_SHADER_DIR));
3692 const std::vector<char> frag_shader = loadSpv(resolveRuntimeShaderPath("sprite.frag.spv", MXVK_SPRITE_SHADER_DIR));
3693
3694 const VkShaderModule vert_module = createShaderModule(device, vert_shader);
3695 VkShaderModule frag_module = VK_NULL_HANDLE;
3696
3697 try {
3698 frag_module = createShaderModule(device, frag_shader);
3699
3700 VkPipelineShaderStageCreateInfo vert_stage{};
3701 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3702 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
3703 vert_stage.module = vert_module;
3704 vert_stage.pName = "main";
3705
3706 VkPipelineShaderStageCreateInfo frag_stage{};
3707 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3708 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
3709 frag_stage.module = frag_module;
3710 frag_stage.pName = "main";
3711
3712 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
3713
3714 VkVertexInputBindingDescription binding_description{};
3715 binding_description.binding = 0;
3716 binding_description.stride = sizeof(float) * 4;
3717 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
3718
3719 std::array<VkVertexInputAttributeDescription, 2> attributes{};
3720 attributes[0].binding = 0;
3721 attributes[0].location = 0;
3722 attributes[0].format = VK_FORMAT_R32G32_SFLOAT;
3723 attributes[0].offset = 0;
3724 attributes[1].binding = 0;
3725 attributes[1].location = 1;
3726 attributes[1].format = VK_FORMAT_R32G32_SFLOAT;
3727 attributes[1].offset = sizeof(float) * 2;
3728
3729 VkPipelineVertexInputStateCreateInfo vertex_input{};
3730 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3731 vertex_input.vertexBindingDescriptionCount = 1;
3732 vertex_input.pVertexBindingDescriptions = &binding_description;
3733 vertex_input.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributes.size());
3734 vertex_input.pVertexAttributeDescriptions = attributes.data();
3735
3736 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
3737 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3738 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
3739 input_assembly.primitiveRestartEnable = VK_FALSE;
3740
3741 const std::array<VkDynamicState, 2> dynamic_states = {
3742 VK_DYNAMIC_STATE_VIEWPORT,
3743 VK_DYNAMIC_STATE_SCISSOR,
3744 };
3745 VkPipelineDynamicStateCreateInfo dynamic_state{};
3746 dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
3747 dynamic_state.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
3748 dynamic_state.pDynamicStates = dynamic_states.data();
3749
3750 VkPipelineViewportStateCreateInfo viewport_state{};
3751 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
3752 viewport_state.viewportCount = 1;
3753 viewport_state.scissorCount = 1;
3754
3755 VkPipelineRasterizationStateCreateInfo rasterizer{};
3756 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3757 rasterizer.depthClampEnable = VK_FALSE;
3758 rasterizer.rasterizerDiscardEnable = VK_FALSE;
3759 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
3760 rasterizer.lineWidth = 1.0F;
3761 rasterizer.cullMode = VK_CULL_MODE_NONE;
3762 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
3763 rasterizer.depthBiasEnable = VK_FALSE;
3764
3765 VkPipelineMultisampleStateCreateInfo multisampling{};
3766 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3767 multisampling.sampleShadingEnable = VK_FALSE;
3768 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
3769
3770 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
3771 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3772 depth_stencil.depthTestEnable = VK_FALSE;
3773 depth_stencil.depthWriteEnable = VK_FALSE;
3774
3775 VkPipelineColorBlendAttachmentState color_attachment{};
3776 color_attachment.colorWriteMask =
3777 VK_COLOR_COMPONENT_R_BIT |
3778 VK_COLOR_COMPONENT_G_BIT |
3779 VK_COLOR_COMPONENT_B_BIT |
3780 VK_COLOR_COMPONENT_A_BIT;
3781 color_attachment.blendEnable = VK_TRUE;
3782 color_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3783 color_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
3784 color_attachment.colorBlendOp = VK_BLEND_OP_ADD;
3785 color_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
3786 color_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
3787 color_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
3788
3789 VkPipelineColorBlendStateCreateInfo color_blending{};
3790 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3791 color_blending.logicOpEnable = VK_FALSE;
3792 color_blending.attachmentCount = 1;
3793 color_blending.pAttachments = &color_attachment;
3794
3795 VkPushConstantRange push_constant_range{};
3796 push_constant_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
3797 push_constant_range.offset = 0;
3798 push_constant_range.size = sizeof(float) * 12;
3799
3800 VkPipelineLayoutCreateInfo pipeline_layout_info{};
3801 pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3802 pipeline_layout_info.setLayoutCount = 1;
3803 pipeline_layout_info.pSetLayouts = &sprite_descriptor_set_layout;
3804 pipeline_layout_info.pushConstantRangeCount = 1;
3805 pipeline_layout_info.pPushConstantRanges = &push_constant_range;
3806
3807 if (vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &sprite_pipeline_layout) != VK_SUCCESS) {
3808 throw mxvk::Exception("Failed to create sprite pipeline layout");
3809 }
3810
3811 VkPipelineRenderingCreateInfo rendering_info{};
3812 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
3813 rendering_info.viewMask = 0;
3814 rendering_info.colorAttachmentCount = 1;
3815 rendering_info.pColorAttachmentFormats = &swapchain_format;
3816 if (depth_format != VK_FORMAT_UNDEFINED) {
3817 rendering_info.depthAttachmentFormat = depth_format;
3818 }
3819
3820 VkGraphicsPipelineCreateInfo pipeline_info{};
3821 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3822 pipeline_info.pNext = &rendering_info;
3823 pipeline_info.stageCount = 2;
3824 pipeline_info.pStages = shader_stages;
3825 pipeline_info.pVertexInputState = &vertex_input;
3826 pipeline_info.pInputAssemblyState = &input_assembly;
3827 pipeline_info.pViewportState = &viewport_state;
3828 pipeline_info.pRasterizationState = &rasterizer;
3829 pipeline_info.pMultisampleState = &multisampling;
3830 pipeline_info.pDepthStencilState = &depth_stencil;
3831 pipeline_info.pColorBlendState = &color_blending;
3832 pipeline_info.pDynamicState = &dynamic_state;
3833 pipeline_info.layout = sprite_pipeline_layout;
3834 pipeline_info.renderPass = VK_NULL_HANDLE;
3835 pipeline_info.subpass = 0;
3836 pipeline_info.basePipelineHandle = VK_NULL_HANDLE;
3837 pipeline_info.basePipelineIndex = -1;
3838
3839 if (vkCreateGraphicsPipelines(device, pipeline_cache, 1, &pipeline_info, nullptr, &sprite_pipeline) != VK_SUCCESS) {
3840 throw mxvk::Exception("Failed to create sprite graphics pipeline");
3841 }
3842 } catch (...) {
3843 if (sprite_pipeline != VK_NULL_HANDLE) {
3844 vkDestroyPipeline(device, sprite_pipeline, nullptr);
3845 sprite_pipeline = VK_NULL_HANDLE;
3846 }
3847 if (sprite_pipeline_layout != VK_NULL_HANDLE) {
3848 vkDestroyPipelineLayout(device, sprite_pipeline_layout, nullptr);
3849 sprite_pipeline_layout = VK_NULL_HANDLE;
3850 }
3851 if (frag_module != VK_NULL_HANDLE) {
3852 vkDestroyShaderModule(device, frag_module, nullptr);
3853 }
3854 vkDestroyShaderModule(device, vert_module, nullptr);
3855 throw;
3856 }
3857
3858 vkDestroyShaderModule(device, frag_module, nullptr);
3859 vkDestroyShaderModule(device, vert_module, nullptr);
3860 }
3861} // namespace mxvk
std::string text() const
Small RAII wrapper for an SDL_ttf font handle.
Definition mxvk_text.hpp:46
int size() const noexcept
Definition mxvk_text.hpp:61
const std::string & path() const noexcept
Definition mxvk_text.hpp:60
Depth-tested 3D billboard sprite batch.
void renderSprites(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight)
Record all queued draw commands into the given command buffer.
void setVertexShaderPath(const std::string &path)
Override the vertex shader path (used when rebuilding the pipeline).
void releaseUploadResources()
Release upload/staging resources tied to the current command pool.
void setDepthAttachmentFormat(VkFormat format)
Assign dynamic-rendering depth attachment format used to build pipelines.
void setCommandPool(VkCommandPool pool)
Rebind the command pool used for upload/staging operations.
void setShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set up to four custom shader float parameters.
void setDescriptorSetLayout(VkDescriptorSetLayout layout)
Assign an external descriptor-set layout.
void prepareForRendering(VkCommandBuffer cmdBuffer)
Record texture barriers that must happen before dynamic rendering begins.
void updateTexture(SDL_Surface *surface)
Replace the sprite texture from an SDL_Surface.
void rebuildInstancedPipeline()
Destroy and recreate the instanced graphics pipeline.
void setColorAttachmentFormat(VkFormat format)
Assign dynamic-rendering color attachment format used to build pipelines.
void loadSprite(const std::string &pngPath, const std::string &fragmentShaderPath="")
Load sprite texture from a PNG file.
void clearExternalTextureDescriptors()
void createEmptySprite(int width, int height, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a blank (un-initialised) sprite texture.
void setPipelineCache(VkPipelineCache cache)
Use the shared pipeline cache for custom/instanced pipeline creation.
void clearQueue()
Discard all pending draw commands without rendering.
void rebuildPipeline()
Destroy and recreate the custom graphics pipeline.
VkPipelineLayout sprite_pipeline_layout
Definition mxvk.hpp:543
bool post_process_enabled
Definition mxvk.hpp:548
bool force_swapchain_recreate
Definition mxvk.hpp:534
std::mutex screenshot_queue_mutex
Definition mxvk.hpp:526
void showCursor(bool on)
Definition mxvk.cpp:3633
VkSwapchainKHR swapchain
Definition mxvk.hpp:492
void captureSnapshotPixels(std::vector< std::uint8_t > &rgba_pixels, uint32_t &width, uint32_t &height)
Capture the most recently presented swapchain image as tightly packed RGBA8 pixels.
Definition mxvk.cpp:800
virtual void event(SDL_Event &e)
Handle one SDL event.
Definition mxvk.cpp:582
virtual ~VK_Window()
Destroy owned Vulkan and SDL resources.
Definition mxvk.cpp:213
void pickDevice()
Pick a suitable Vulkan physical device.
Definition mxvk.cpp:1427
static constexpr std::chrono::seconds MEMORY_TRIM_INTERVAL
Definition mxvk.hpp:538
VkDescriptorSetLayout sprite_descriptor_set_layout
Definition mxvk.hpp:542
std::vector< std::vector< bool > > post_process_initialized
Definition mxvk.hpp:560
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
VK_Sprite3D * createSprite3D(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a world-space billboard sprite from a PNG file.
Definition mxvk.cpp:3578
std::condition_variable screenshot_queue_cv
Definition mxvk.hpp:527
VkDevice getDevice() const noexcept
Get the Vulkan logical device handle.
Definition mxvk.hpp:168
VkDebugUtilsMessengerEXT debug_messenger
Definition mxvk.hpp:482
VkDevice device
Definition mxvk.hpp:485
static VkBool32 debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT *callback_data, void *user_data)
Definition mxvk.cpp:44
std::chrono::steady_clock::time_point fps_counter_sample_time
Definition mxvk.hpp:573
uint32_t screenshot_index
Definition mxvk.hpp:519
void createLogicalDevice()
Create a logical Vulkan device from the selected physical device.
Definition mxvk.cpp:1514
uint32_t fps_counter_frame_count
Definition mxvk.hpp:574
virtual bool initVulkan(bool validiation)
Initialize Vulkan state.
Definition mxvk.cpp:318
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:3477
VkClearColorValue clear_color
Definition mxvk.hpp:576
VkQueue getGraphicsQueue() const noexcept
Get the graphics queue handle.
Definition mxvk.hpp:174
std::string screenshot_prefix
Definition mxvk.hpp:518
virtual void onSwapchainRecreated()
Called after swapchain and render resources are recreated.
Definition mxvk.cpp:1132
VkInstance instance
Definition mxvk.hpp:481
VkFormat swapchain_format
Definition mxvk.hpp:493
VkCommandPool getCommandPool() const noexcept
Get the command pool used for graphics/upload work.
Definition mxvk.hpp:177
VkExtent2D swapchain_extent
Definition mxvk.hpp:495
virtual void onSwapchainAboutToRecreate()
Called right before swapchain-dependent resources are recreated.
Definition mxvk.cpp:1130
std::vector< bool > post_process_effect_time_enabled
Definition mxvk.hpp:555
VulkanContext context() const
Definition mxvk.cpp:59
bool validation_enabled
Definition mxvk.hpp:531
bool screenshot_worker_stop
Definition mxvk.hpp:530
std::vector< std::array< float, 4 > > post_process_effect_params
Definition mxvk.hpp:554
std::vector< std::vector< VkDeviceMemory > > post_process_memories
Definition mxvk.hpp:558
VkDescriptorSetLayout text_descriptor_set_layout
Definition mxvk.hpp:563
static constexpr uint64_t resize_settle_delay_ms
Definition mxvk.hpp:536
std::vector< std::vector< VkImageView > > post_process_views
Definition mxvk.hpp:559
VkPipeline text_pipeline
Definition mxvk.hpp:565
std::chrono::steady_clock::time_point post_process_start_time
Definition mxvk.hpp:551
VkFormat depth_format
Definition mxvk.hpp:494
uint32_t present_queue_family
Definition mxvk.hpp:487
void enablePostProcessing(VK_Sprite *sprite)
Definition mxvk.cpp:1271
uint32_t last_presented_image_index
Definition mxvk.hpp:512
std::vector< VkImageView > swapchain_image_views
Definition mxvk.hpp:497
std::vector< VK_Sprite * > owned_post_process_sprites
Definition mxvk.hpp:553
static VkShaderModule createShaderModule(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
Definition mxvk.cpp:145
std::chrono::steady_clock::time_point last_memory_trim_time
Definition mxvk.hpp:537
VkQueue present_queue
Definition mxvk.hpp:489
void createDevice()
Create final device resources.
Definition mxvk.cpp:1645
bool text_state_dirty
Definition mxvk.hpp:566
Font fps_counter_font
Definition mxvk.hpp:572
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:3069
std::vector< bool > swapchain_image_initialized
Definition mxvk.hpp:498
std::vector< VkImage > swapchain_images
Definition mxvk.hpp:496
std::vector< VkFence > image_fences
Definition mxvk.hpp:510
std::string font_path
Definition mxvk.hpp:568
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:3063
std::vector< VkSwapchainKHR > retired_swapchains
Definition mxvk.hpp:577
std::vector< VkImageView > depth_image_views
Definition mxvk.hpp:501
virtual void onConfigureDepthStencilAttachments(VkRenderingAttachmentInfo &depth_attachment, VkRenderingAttachmentInfo &stencil_attachment, uint32_t image_index)
Allow derived classes to customize depth/stencil attachments for the main dynamic rendering pass.
Definition mxvk.cpp:1138
bool post_process_time_enabled
Definition mxvk.hpp:549
std::unique_ptr< VK_Text > text_renderer
Definition mxvk.hpp:562
bool screenshot_enabled
Definition mxvk.hpp:517
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
Definition mxvk.cpp:782
uint32_t graphics_queue_family
Definition mxvk.hpp:486
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:593
std::vector< VK_Sprite * > attachPostProcessingShaders(const std::vector< PostProcessingEffect > &effects)
Definition mxvk.cpp:1159
virtual void onPrepareFrameRendering(VkCommandBuffer cmd, uint32_t image_index)
Record resource transitions that must happen before dynamic rendering begins.
Definition mxvk.cpp:1134
std::vector< VkCommandBuffer > command_buffers
Definition mxvk.hpp:505
void renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd)
Render one standalone sprite using the window's shared sprite pipeline.
Definition mxvk.cpp:1142
std::thread screenshot_worker
Definition mxvk.hpp:529
uint32_t current_frame
Definition mxvk.hpp:511
std::vector< VkSemaphore > render_finished
Definition mxvk.hpp:508
VkPipelineCache pipeline_cache
Definition mxvk.hpp:490
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:480
bool sdl_initialized
Definition mxvk.hpp:515
std::vector< std::chrono::steady_clock::time_point > post_process_effect_start_times
Definition mxvk.hpp:556
VkPipeline sprite_pipeline
Definition mxvk.hpp:544
void exit()
Request loop termination.
Definition mxvk.cpp:1126
VkCommandPool command_pool
Definition mxvk.hpp:504
VK_Sprite * attachPostProcessingShader(const std::string &fragmentShaderPath, float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Attach a full-screen post-processing fragment shader.
Definition mxvk.cpp:1154
bool ensureRenderResources()
Ensure deferred render resources are initialized.
Definition mxvk.cpp:1406
VkSurfaceKHR surface
Definition mxvk.hpp:483
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:484
VkPipelineLayout text_pipeline_layout
Definition mxvk.hpp:564
virtual void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index)
Optional hook for derived classes to record extra draw commands.
Definition mxvk.cpp:1136
std::array< float, 4 > post_process_params
Definition mxvk.hpp:550
void detachPostProcessingShader()
Detach the current post-processing shader and return to direct swapchain rendering.
Definition mxvk.cpp:1198
std::vector< bool > depth_image_initialized
Definition mxvk.hpp:502
bool fps_counter_font_ready
Definition mxvk.hpp:571
std::deque< ScreenshotSaveTask > screenshot_save_queue
Definition mxvk.hpp:528
void release()
Release Vulkan and SDL resources.
Definition mxvk.cpp:218
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:2991
std::vector< VkDeviceMemory > depth_image_memories
Definition mxvk.hpp:500
PresentModePreference present_mode_preference
Definition mxvk.hpp:532
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3018
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:141
std::vector< std::unique_ptr< VK_Sprite3D > > sprites3d
Definition mxvk.hpp:541
std::vector< VK_Sprite * > post_process_sprites
Definition mxvk.hpp:552
bool sprite_state_dirty
Definition mxvk.hpp:545
void trimMemory()
Return transient Vulkan command-pool allocations to the driver when supported.
Definition mxvk.cpp:1041
VK_Sprite * post_process_sprite
Definition mxvk.hpp:546
bool framebuffer_resized
Definition mxvk.hpp:533
VkQueue graphics_queue
Definition mxvk.hpp:488
bool font_configured
Definition mxvk.hpp:567
void setPostProcessingShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set the post-processing shader params passed as a vec4 push constant.
Definition mxvk.cpp:1227
std::string fps_counter_text
Definition mxvk.hpp:575
virtual void render()
Render one frame.
Definition mxvk.cpp:778
bool initWindow(const std::string &title, int width, int height, SDL_WindowFlags flags)
Initialize SDL window resources.
Definition mxvk.cpp:1059
std::vector< VkImage > depth_images
Definition mxvk.hpp:499
virtual void proc()
Execute one processing/update step.
Definition mxvk.cpp:1038
bool validationEnabled() const
Check whether Vulkan validation layers are currently enabled.
Definition mxvk.cpp:2911
std::vector< std::unique_ptr< VK_Sprite > > sprites
Definition mxvk.hpp:540
uint64_t last_resize_event_ms
Definition mxvk.hpp:535
VkPhysicalDevice getPhysicalDevice() const noexcept
Get the Vulkan physical device handle.
Definition mxvk.hpp:171
void setPostProcessingShaderTimeEnabled(bool enabled)
Keep shader param 1 updated with elapsed render time in seconds.
Definition mxvk.cpp:1248
std::vector< std::vector< VkImage > > post_process_images
Definition mxvk.hpp:557
void setEnableScreenshot(bool enabled) noexcept
Enable or disable F10 screenshot capture.
Definition mxvk.hpp:159
VK_Sprite * owned_post_process_sprite
Definition mxvk.hpp:547
std::array< VkSemaphore, max_frames_in_flight > image_available
Definition mxvk.hpp:507
bool fps_counter_enabled
Definition mxvk.hpp:570
bool swapchain_supports_transfer_src
Definition mxvk.hpp:513
std::array< VkFence, max_frames_in_flight > in_flight_fences
Definition mxvk.hpp:509
#define MXVK_TEXT_SHADER_DIR
Definition mxvk.cpp:29
#define MXVK_SPRITE_SHADER_DIR
Definition mxvk.cpp:25
#define MXVK_DEFAULT_FONT_DIR
Definition mxvk.cpp:33
PNG image loading and saving utilities via SDL3.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
VkShaderModule create_shader_module(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
bool SavePNG_RGBA(const char *filename, void *buffer, int w, int h)
Save a raw RGBA pixel buffer to a PNG file.
Definition mxvk_png.cpp:300
bool defaultEnableScreenshot()
const std::string & defaultExecutableName()
std::vector< char > load_spv(const std::string &path)
Load a SPIR-V file from disk.
Minimal Vulkan handles required by MXVK resource helpers.