MXVK Vulkan Framework 0.35.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 <cmath>
11#include <cstdlib>
12#include <cstring>
13#include <ctime>
14#include <exception>
15#include <filesystem>
16#include <format>
17#include <fstream>
18#include <iomanip>
19#include <iostream>
20#include <iterator>
21#include <optional>
22#include <sstream>
23#include <vector>
24
25#ifdef _WIN32
26#ifndef NOMINMAX
27#define NOMINMAX
28#endif
29#include <windows.h>
30#endif
31
32#ifndef MXVK_SPRITE_SHADER_DIR
33#define MXVK_SPRITE_SHADER_DIR "."
34#endif
35
36#ifndef MXVK_TEXT_SHADER_DIR
37#define MXVK_TEXT_SHADER_DIR "."
38#endif
39
40#ifndef MXVK_DEFAULT_FONT_DIR
41#define MXVK_DEFAULT_FONT_DIR "."
42#endif
43
44namespace mxvk {
45 namespace {
46 [[nodiscard]] float half_to_float(uint16_t value) noexcept {
47 const bool negative = (value & 0x8000U) != 0U;
48 const uint32_t exponent = (value >> 10U) & 0x1FU;
49 const uint32_t mantissa = value & 0x3FFU;
50 float result = 0.0F;
51 if (exponent == 0U) {
52 result = std::ldexp(static_cast<float>(mantissa), -24);
53 } else if (exponent == 31U) {
54 result = mantissa == 0U ? std::numeric_limits<float>::infinity() : std::numeric_limits<float>::quiet_NaN();
55 } else {
56 result = std::ldexp(1.0F + static_cast<float>(mantissa) / 1024.0F, static_cast<int>(exponent) - 15);
57 }
58 return negative ? -result : result;
59 }
60
62 const char *quiet_missing_validation = std::getenv("MXVK_QUIET_MISSING_VALIDATION");
63 return quiet_missing_validation == nullptr || std::strcmp(quiet_missing_validation, "1") != 0;
64 }
65 } // namespace
66
67 VKAPI_ATTR VkBool32 VKAPI_CALL VK_Window::debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT *callback_data, [[maybe_unused]] void *user_data) {
68 const char *message = (callback_data != nullptr && callback_data->pMessage != nullptr) ? callback_data->pMessage : "Unknown Vulkan validation message";
69
70 if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0U) {
71 std::cerr << std::format("vk validation error: {}\n", message);
72 } else if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) != 0U) {
73 std::cerr << std::format("vk validation warning: {}\n", message);
74 } else {
75 std::cout << std::format("vk validation: {}\n", message);
76 }
77 return VK_FALSE;
78 }
79
81 return VulkanContext{
82 .device = getDevice(),
83 .physical_device = getPhysicalDevice(),
84 .graphics_queue = getGraphicsQueue(),
85 .command_pool = getCommandPool(),
86 };
87 }
88
89 VK_Window::SwapchainSupport VK_Window::querySwapchainSupport(VkPhysicalDevice device, VkSurfaceKHR surface) {
90 std::cout << "vk: querying swapchain support details\n";
91 SwapchainSupport support{};
92
93 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &support.capabilities);
94
95 uint32_t format_count = 0;
96 vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, nullptr);
97 if (format_count > 0U) {
98 support.formats.resize(format_count);
99 vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, support.formats.data());
100 }
101
102 uint32_t present_mode_count = 0;
103 vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &present_mode_count, nullptr);
104 if (present_mode_count > 0U) {
105 support.present_modes.resize(present_mode_count);
106 vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &present_mode_count, support.present_modes.data());
107 }
108
109 return support;
110 }
111
112 VkSurfaceFormatKHR VK_Window::chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR> &available_formats) {
113 for (const VkSurfaceFormatKHR &format : available_formats) {
114 if (format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
115 return format;
116 }
117 }
118
119 return available_formats.front();
120 }
121
122 VkPresentModeKHR VK_Window::choosePresentMode(const std::vector<VkPresentModeKHR> &available_present_modes) const {
124 return VK_PRESENT_MODE_FIFO_KHR;
125 }
126
127 for (const VkPresentModeKHR present_mode : available_present_modes) {
128 if (present_mode == VK_PRESENT_MODE_MAILBOX_KHR) {
129 return present_mode;
130 }
131 }
132
133 return VK_PRESENT_MODE_FIFO_KHR;
134 }
135
136 VkExtent2D VK_Window::chooseExtent(const VkSurfaceCapabilitiesKHR &capabilities, SDL_Window *window) {
137 if (capabilities.currentExtent.width != UINT32_MAX) {
138 return capabilities.currentExtent;
139 }
140
141 int width = 1;
142 int height = 1;
143 SDL_GetWindowSizeInPixels(window, &width, &height);
144
145 VkExtent2D actual_extent{};
146 actual_extent.width = std::clamp(static_cast<uint32_t>(std::max(width, 1)), capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
147 actual_extent.height = std::clamp(static_cast<uint32_t>(std::max(height, 1)), capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
148
149 return actual_extent;
150 }
151
152 std::vector<char> VK_Window::loadSpv(const std::string &path) { return mxvk::load_spv(path); }
153
154 VkShaderModule VK_Window::createShaderModule(VkDevice device, const std::vector<char> &spv_bytes) { return mxvk::create_shader_module(device, spv_bytes); }
155
156 std::string VK_Window::resolveRuntimeShaderPath(const std::string &shaderFileName, const char *fallbackDir) const {
157 if (shaderFileName.empty()) {
158 throw mxvk::Exception("Shader file name is empty");
159 }
160
161 std::vector<std::filesystem::path> candidates{};
162
163 if (!defaultShaderDirectory().empty()) {
164 candidates.push_back(std::filesystem::path(defaultShaderDirectory()) / shaderFileName);
165 }
166
167 const auto add_executable_candidates = [&candidates, &shaderFileName](const std::filesystem::path &executableDir) {
168 candidates.push_back(executableDir / "data" / shaderFileName);
169 candidates.push_back(executableDir / shaderFileName);
170 candidates.push_back(executableDir.parent_path() / "share" / "mxvk" / "shaders" / shaderFileName);
171 };
172
173 if (const char *basePath = SDL_GetBasePath(); basePath != nullptr) {
174 add_executable_candidates(std::filesystem::path(basePath));
175 }
176
177#ifdef _WIN32
178 std::vector<wchar_t> executableName(MAX_PATH);
179 const DWORD executableLength = GetModuleFileNameW(nullptr, executableName.data(), static_cast<DWORD>(executableName.size()));
180 if (executableLength > 0U && executableLength < executableName.size() - 1U) {
181 add_executable_candidates(std::filesystem::path(std::wstring(executableName.data(), executableLength)).parent_path());
182 }
183#endif
184
185 candidates.push_back(std::filesystem::path("data") / shaderFileName);
186
187 if (!font_path.empty()) {
188 const std::filesystem::path fontPath(font_path);
189 if (fontPath.has_parent_path()) {
190 candidates.push_back(fontPath.parent_path() / shaderFileName);
191 }
192 }
193
194 if (fallbackDir != nullptr && fallbackDir[0] != '\0') {
195 candidates.push_back(std::filesystem::path(fallbackDir) / shaderFileName);
196 }
197
198 std::error_code existsError{};
199 for (const std::filesystem::path &candidate : candidates) {
200 if (std::filesystem::exists(candidate, existsError)) {
201 return candidate.string();
202 }
203 existsError.clear();
204 }
205
206 throw mxvk::Exception(std::format("Failed to locate shader file '{}'", shaderFileName));
207 }
208
209 VK_Window::VK_Window(const std::string &title, int width, int height, bool full, bool validiation, PresentModePreference presentModePreference, RuntimeMode runtimeMode) : runtime_mode(runtimeMode), requested_headless_extent{width > 0 ? static_cast<uint32_t>(width) : 0U, height > 0 ? static_cast<uint32_t>(height) : 0U}, present_mode_preference(presentModePreference) {
210 if (width <= 0 || height <= 0) {
211 throw mxvk::Exception("Render dimensions must be positive");
212 }
215 std::cout << std::format("mxvk: starting VK_Window construction (title='{}', width={}, height={}, fullscreen={}, validation={}, mode={})\n", title, width, height, full, validiation, headless() ? "headless" : "windowed");
216 SDL_WindowFlags flags = static_cast<SDL_WindowFlags>(SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
217 if (full) {
218 std::cout << "SDL3: enabling fullscreen window flag\n";
219 flags = static_cast<SDL_WindowFlags>(flags | SDL_WINDOW_FULLSCREEN);
220 }
221
222 if (!headless()) {
223 std::cout << "mxvk: initializing window subsystem and creating SDL window\n";
224 if (!initWindow(title, width, height, flags)) {
225 throw mxvk::Exception("Error on init of Window");
226 }
227 showCursor(!full);
228 } else {
229 std::cout << "mxvk: surface-free headless mode; SDL video and window creation skipped\n";
230 }
231
232 std::cout << "mxvk: initializing Vulkan runtime and rendering resources\n";
233 if (!initVulkan(validiation)) {
234 release();
235 throw mxvk::Exception("Error on init of Vulkan");
236 }
237 std::cout << "mxvk: VK_Window construction complete\n";
238 }
239
240 VK_Window::VK_Window(const std::string &title, int width, int height, bool full, bool validiation, bool enableVsync) : VK_Window(title, width, height, full, validiation, enableVsync ? PresentModePreference::Vsync : PresentModePreference::LowLatency) {}
241
243 std::cout << "mxvk: destructor invoked, releasing resources\n";
244 release();
245 }
246
248 std::cout << "mxvk: starting resource teardown\n";
249 stopScreenshotWorker();
250
251 if (device != VK_NULL_HANDLE) {
252 std::cout << "vk: waiting for device idle before teardown\n";
253 vkDeviceWaitIdle(device);
254 }
255
257
258 post_process_sprite = nullptr;
262 post_process_sprites.clear();
268
269 if (!sprites.empty()) {
270 std::cout << std::format("vk: releasing {} sprite(s)\n", sprites.size());
271 sprites.clear();
272 std::cout << "vk: sprite resources released\n";
273 }
274 if (!sprites3d.empty()) {
275 std::cout << std::format("vk: releasing {} 3D sprite batch(es)\n", sprites3d.size());
276 sprites3d.clear();
277 }
278 sprite_state_dirty = false;
279 destroySpritePipeline();
280 if (sprite_descriptor_set_layout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
281 vkDestroyDescriptorSetLayout(device, sprite_descriptor_set_layout, nullptr);
282 sprite_descriptor_set_layout = VK_NULL_HANDLE;
283 }
284
285 if (text_renderer) {
286 text_renderer.reset();
287 }
289 preview_text_renderer.reset();
290 }
291 preview_text_queued = false;
292 text_state_dirty = false;
293 destroyTextPipeline();
294 if (text_descriptor_set_layout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
295 vkDestroyDescriptorSetLayout(device, text_descriptor_set_layout, nullptr);
296 text_descriptor_set_layout = VK_NULL_HANDLE;
297 }
298
299 cleanupSyncObjects();
300
301 if (!retired_swapchains.empty()) {
302 for (VkSwapchainKHR retired : retired_swapchains) {
303 vkDestroySwapchainKHR(device, retired, nullptr);
304 }
305 retired_swapchains.clear();
306 }
307 std::cout << "vk: tearing down swapchain-dependent resources\n";
308 cleanupSwapchain(true);
309
310 if (command_pool != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
311 std::cout << "vk: destroying command pool\n";
312 vkDestroyCommandPool(device, command_pool, nullptr);
313 command_pool = VK_NULL_HANDLE;
314 }
315
316 if (device != VK_NULL_HANDLE) {
317 savePipelineCache();
318 destroyPipelineCache();
319 std::cout << "vk: destroying logical device\n";
320 vkDestroyDevice(device, nullptr);
321 device = VK_NULL_HANDLE;
322 graphics_queue = VK_NULL_HANDLE;
323 present_queue = VK_NULL_HANDLE;
324 }
325
326 graphics_queue_family = invalid_queue_index;
327 present_queue_family = invalid_queue_index;
328 physical_device = VK_NULL_HANDLE;
329
330 if (surface != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) {
331 std::cout << "vk: destroying presentation surface\n";
332 vkDestroySurfaceKHR(instance, surface, nullptr);
333 surface = VK_NULL_HANDLE;
334 }
335
336 cleanupDebugMessenger();
337
338 if (instance != VK_NULL_HANDLE) {
339 std::cout << "vk: destroying Vulkan instance\n";
340 vkDestroyInstance(instance, nullptr);
341 instance = VK_NULL_HANDLE;
342 }
343
344 if (window != nullptr) {
345 std::cout << "SDL3: destroying SDL window handle\n";
346 window.reset();
347 }
348
350 std::cout << "SDL3: unloading Vulkan loader library\n";
351 SDL_Vulkan_UnloadLibrary();
352 vulkan_library_loaded = false;
353 }
354
355 if (sdl_initialized) {
356 std::cout << "SDL3: shutting down SDL video/gamepad subsystems\n";
357 SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD);
358 sdl_initialized = false;
359 }
360
361 active = false;
362 std::cout << "mxvk: resource teardown complete\n";
363 }
364
365 bool VK_Window::initVulkan(bool validiation) {
366 std::cout << std::format("mxvk: entering initVulkan (validation={})\n", validiation);
367 validation_enabled = validiation;
368
369 if (!headless() && window == nullptr) {
370 std::cerr << "mxvk: Cannot initialize Vulkan without an SDL window\n";
371 return false;
372 }
373
374 if (instance != VK_NULL_HANDLE) {
375 std::cout << "vk: instance already initialized; skipping initVulkan\n";
376 return true;
377 }
378
379 std::vector<const char *> enabled_extensions{};
380 if (headless()) {
381 std::cout << "vk: initializing volk directly from the Vulkan loader\n";
382 if (volkInitialize() != VK_SUCCESS) {
383 std::cerr << "mxvk: failed to initialize the Vulkan loader\n";
384 return false;
385 }
386 } else {
387 std::cout << "vk: initializing volk from SDL's Vulkan loader\n";
388 const auto get_instance_proc_addr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr());
389 if (get_instance_proc_addr == nullptr) {
390 std::cerr << std::format("mxvk: SDL did not provide vkGetInstanceProcAddr: {}\n", SDL_GetError());
391 return false;
392 }
393 volkInitializeCustom(get_instance_proc_addr);
394
395 unsigned int extension_count = 0;
396 std::cout << "SDL3: querying Vulkan instance extensions required by SDL\n";
397 const char *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extension_count);
398 if (extensions == nullptr || extension_count == 0U) {
399 std::cerr << std::format("mxvk: Failed to get Vulkan instance extensions: {}\n", SDL_GetError());
400 return false;
401 }
402 std::cout << std::format("vk: SDL provided {} required instance extension(s)\n", extension_count);
403 enabled_extensions.assign(extensions, extensions + extension_count);
404 }
405
406#if defined(MXVK_USE_MOLTENVK)
407 const auto append_instance_extension_if_missing = [&enabled_extensions](const char *extension_name) {
408 const bool exists = std::ranges::any_of(enabled_extensions, [extension_name](const char *existing) { return std::strcmp(existing, extension_name) == 0; });
409 if (!exists) {
410 enabled_extensions.push_back(extension_name);
411 }
412 };
413 append_instance_extension_if_missing(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
414 append_instance_extension_if_missing(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
415#endif
416
417 std::vector<const char *> enabled_layers{};
418 VkDebugUtilsMessengerCreateInfoEXT debug_create_info{};
419 if (validation_enabled) {
420 if (!hasValidationLayerSupport()) {
421 if (shouldLogMissingValidationLayer()) {
422 std::cerr << std::format("mxvk: validation layer '{}' is not available; continuing without validation\n", validation_layer_name);
423 }
424 validation_enabled = false;
425 } else {
426 enabled_layers.push_back(validation_layer_name);
427 enabled_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
428 const std::optional<VkDebugUtilsMessengerCreateInfoEXT> maybe_debug_create_info = makeDebugMessengerCreateInfo();
429 if (!maybe_debug_create_info.has_value()) {
430 std::cerr << "mxvk: failed to construct debug messenger create info\n";
431 validation_enabled = false;
432 enabled_layers.clear();
433 } else {
434 debug_create_info = maybe_debug_create_info.value();
435 }
436 }
437 }
438
439 VkApplicationInfo app_info{};
440 app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
441 app_info.pApplicationName = "mxvk";
442 app_info.applicationVersion = VK_MAKE_VERSION(0, 1, 0);
443 app_info.pEngineName = "mxvk";
444 app_info.engineVersion = VK_MAKE_VERSION(MXVK_VERSION_CODE_MAJOR, MXVK_VERSION_CODE_MINOR, MXVK_VERSION_CODE_PATCH);
445 app_info.apiVersion = VK_API_VERSION_1_4;
446
447 VkInstanceCreateInfo create_info{};
448 create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
449 create_info.pApplicationInfo = &app_info;
450 create_info.enabledExtensionCount = static_cast<uint32_t>(enabled_extensions.size());
451 create_info.ppEnabledExtensionNames = enabled_extensions.data();
452 create_info.enabledLayerCount = static_cast<uint32_t>(enabled_layers.size());
453 create_info.ppEnabledLayerNames = enabled_layers.empty() ? nullptr : enabled_layers.data();
454 create_info.pNext = validation_enabled ? &debug_create_info : nullptr;
455
456 create_info.enabledExtensionCount = static_cast<uint32_t>(enabled_extensions.size());
457#if defined(MXVK_USE_MOLTENVK)
458 create_info.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
459#endif
460
461 std::cout << "vk: creating Vulkan instance\n";
462 if (vkCreateInstance(&create_info, nullptr, &instance) != VK_SUCCESS) {
463 std::cerr << "mxvk: Failed to create Vulkan instance\n";
464 instance = VK_NULL_HANDLE;
465 return false;
466 }
467
468 std::cout << "vk: loading Vulkan instance function pointers via volk\n";
469 volkLoadInstance(instance);
470
471 setupDebugMessenger();
472
473 uint32_t instanceVersion = 0;
474 if (vkEnumerateInstanceVersion != nullptr) {
475 vkEnumerateInstanceVersion(&instanceVersion);
476 }
477 std::cout << "vk: Vulkan instance version: " << VK_VERSION_MAJOR(instanceVersion) << "." << VK_VERSION_MINOR(instanceVersion) << "." << VK_VERSION_PATCH(instanceVersion) << "\n";
478 std::cout << "mxvk: engine version: " << MXVK_VERSION_CODE_MAJOR << "." << MXVK_VERSION_CODE_MINOR << "." << MXVK_VERSION_CODE_PATCH << "\n";
479
480 if (!headless()) {
481 std::cout << "SDL3: creating Vulkan presentation surface from SDL window\n";
482 if (!SDL_Vulkan_CreateSurface(window.get(), instance, nullptr, &surface)) {
483 std::cerr << std::format("mxvk: Failed to create Vulkan surface: {}\n", SDL_GetError());
484 cleanupDebugMessenger();
485 vkDestroyInstance(instance, nullptr);
486 instance = VK_NULL_HANDLE;
487 surface = VK_NULL_HANDLE;
488 return false;
489 }
490 }
491
492 std::cout << "mxvk: selecting suitable physical device\n";
493 pickDevice();
494 if (physical_device == VK_NULL_HANDLE) {
495 std::cerr << "mxvk: Failed to find a suitable Vulkan physical device\n";
496 return false;
497 }
498
499 std::cout << "mxvk: creating logical device and queues\n";
501 if (device == VK_NULL_HANDLE) {
502 std::cerr << "mxvk: Failed to create Vulkan logical device\n";
503 return false;
504 }
505 createPipelineCache();
506
507 std::cout << "mxvk: deferring swapchain/render/sync resource creation until first frame\n";
508
509 std::cout << "mxvk: initVulkan complete\n";
510 return true;
511 }
512
513 std::string VK_Window::pipelineCachePath() const {
514 if (physical_device == VK_NULL_HANDLE) {
515 return {};
516 }
517
518 VkPhysicalDeviceProperties properties{};
519 vkGetPhysicalDeviceProperties(physical_device, &properties);
520
521 char *pref_path = SDL_GetPrefPath("mxvk", "MXVK");
522 std::filesystem::path base_path;
523 if (pref_path != nullptr) {
524 base_path = pref_path;
525 SDL_free(pref_path);
526 } else {
527 base_path = std::filesystem::current_path();
528 }
529
530 std::error_code ec;
531 std::filesystem::create_directories(base_path, ec);
532 if (ec) {
533 return {};
534 }
535
536 std::ostringstream filename;
537 filename << "pipeline_cache_" << std::hex << std::setfill('0') << properties.vendorID << '_' << properties.deviceID << '_' << properties.driverVersion << '_';
538 for (uint8_t byte : properties.pipelineCacheUUID) {
539 filename << std::setw(2) << static_cast<unsigned>(byte);
540 }
541 filename << ".bin";
542
543 return (base_path / filename.str()).string();
544 }
545
546 void VK_Window::createPipelineCache() {
547 if (device == VK_NULL_HANDLE || pipeline_cache != VK_NULL_HANDLE) {
548 return;
549 }
550
551 const std::string cache_path = pipelineCachePath();
552 std::vector<char> initial_data{};
553 if (!cache_path.empty() && std::filesystem::exists(cache_path)) {
554 std::ifstream file(cache_path, std::ios::binary);
555 if (file) {
556 initial_data.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
557 }
558 }
559
560 VkPipelineCacheCreateInfo create_info{};
561 create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
562 create_info.initialDataSize = initial_data.size();
563 create_info.pInitialData = initial_data.empty() ? nullptr : initial_data.data();
564
565 VkResult result = vkCreatePipelineCache(device, &create_info, nullptr, &pipeline_cache);
566 if (result != VK_SUCCESS && !initial_data.empty()) {
567 std::cerr << "vk: cached pipeline data rejected; creating an empty pipeline cache\n";
568 create_info.initialDataSize = 0;
569 create_info.pInitialData = nullptr;
570 result = vkCreatePipelineCache(device, &create_info, nullptr, &pipeline_cache);
571 }
572
573 if (result != VK_SUCCESS) {
574 pipeline_cache = VK_NULL_HANDLE;
575 std::cerr << std::format("vk: failed to create pipeline cache ({})\n", static_cast<int>(result));
576 return;
577 }
578
579 if (!initial_data.empty()) {
580 std::cout << std::format("vk: loaded pipeline cache: {} bytes\n", initial_data.size());
581 }
582 }
583
584 void VK_Window::savePipelineCache() const {
585 if (device == VK_NULL_HANDLE || pipeline_cache == VK_NULL_HANDLE) {
586 return;
587 }
588
589 size_t data_size = 0;
590 VkResult result = vkGetPipelineCacheData(device, pipeline_cache, &data_size, nullptr);
591 if (result != VK_SUCCESS || data_size == 0) {
592 return;
593 }
594
595 std::vector<char> data(data_size);
596 result = vkGetPipelineCacheData(device, pipeline_cache, &data_size, data.data());
597 if (result != VK_SUCCESS || data_size == 0) {
598 return;
599 }
600 data.resize(data_size);
601
602 const std::string cache_path = pipelineCachePath();
603 if (cache_path.empty()) {
604 return;
605 }
606
607 std::ofstream file(cache_path, std::ios::binary | std::ios::trunc);
608 if (!file) {
609 std::cerr << std::format("vk: failed to open pipeline cache for writing: {}\n", cache_path);
610 return;
611 }
612
613 file.write(data.data(), static_cast<std::streamsize>(data.size()));
614 if (file) {
615 std::cout << std::format("vk: saved pipeline cache: {} bytes\n", data.size());
616 }
617 }
618
619 void VK_Window::destroyPipelineCache() {
620 if (device != VK_NULL_HANDLE && pipeline_cache != VK_NULL_HANDLE) {
621 vkDestroyPipelineCache(device, pipeline_cache, nullptr);
622 }
623 pipeline_cache = VK_NULL_HANDLE;
624 }
625
626 void VK_Window::event(SDL_Event &e) {
627 switch (e.type) {
628 case SDL_EVENT_KEY_DOWN:
629 switch (e.key.key) {
630 case SDLK_ESCAPE:
631 active = false;
632 break;
633 }
634 break;
635 }
636 }
637 void VK_Window::setClearColor(float r, float g, float b, float a) {
638 clear_color.float32[0] = std::clamp(r, 0.0f, 1.0f);
639 clear_color.float32[1] = std::clamp(g, 0.0f, 1.0f);
640 clear_color.float32[2] = std::clamp(b, 0.0f, 1.0f);
641 clear_color.float32[3] = std::clamp(a, 0.0f, 1.0f);
642 }
643
644 void VK_Window::setEnableScreenshot(bool enabled) noexcept {
645 screenshot_enabled = enabled;
646 if (enabled) {
648 }
649 }
650
652 if (headless()) {
653 active = true;
654 while (active) {
655 proc();
656 render();
657 maybeTrimMemory();
658 }
660 return;
661 }
662
663 SDL_Event e;
664 active = true;
665 while (active) {
666 while (SDL_PollEvent(&e)) {
667 switch (e.type) {
668 case SDL_EVENT_QUIT:
669 active = false;
670 break;
671 case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
672 if (window != nullptr) {
673 framebuffer_resized = true;
674 last_resize_event_ms = SDL_GetTicks();
676 }
677 break;
678 case SDL_EVENT_KEY_DOWN:
679 if ((e.key.key == SDLK_F12 || e.key.scancode == SDL_SCANCODE_F12) && !e.key.repeat) {
680 toggleFpsCounter();
681 }
682 if (screenshot_enabled && (e.key.key == SDLK_F10 || e.key.scancode == SDL_SCANCODE_F10) && !e.key.repeat) {
683 try {
684 saveScreenshot();
685 } catch (const mxvk::Exception &ex) {
686 std::cerr << std::format("mxvk: screenshot failed: {}\n", ex.text());
687 } catch (const std::exception &ex) {
688 std::cerr << std::format("mxvk: screenshot failed: {}\n", ex.what());
689 }
690 continue;
691 }
692 break;
693 default:
694 break;
695 }
696 event(e);
697 }
699 const uint64_t now_ms = SDL_GetTicks();
701 proc();
702 render();
703 SDL_Delay(1);
704 continue;
705 }
706 recreateSwapchain();
707 }
708 proc();
709 render();
710 maybeTrimMemory();
711 }
712 }
713
714 void VK_Window::saveScreenshot() {
715 const std::string path = makeScreenshotPath();
716 std::vector<std::uint8_t> rgba{};
717 uint32_t width = 0;
718 uint32_t height = 0;
719 captureSnapshotPixels(rgba, width, height);
720 enqueueScreenshotSave(path, std::move(rgba), width, height);
721 std::cout << std::format("mxvk: screenshot queued: {}\n", path);
722 }
723
724 void VK_Window::enqueueScreenshotSave(std::string path, std::vector<std::uint8_t> rgba, uint32_t width, uint32_t height) {
725 if (path.empty()) {
726 throw mxvk::Exception("enqueueScreenshotSave requires a non-empty output path");
727 }
728 if (rgba.empty() || width == 0U || height == 0U) {
729 throw mxvk::Exception("enqueueScreenshotSave requires non-empty pixel data");
730 }
731
732 startScreenshotWorker();
733
734 {
735 std::lock_guard<std::mutex> lock(screenshot_queue_mutex);
737 .path = std::move(path),
738 .rgba = std::move(rgba),
739 .width = width,
740 .height = height,
741 });
742 }
743 screenshot_queue_cv.notify_one();
744 }
745
746 void VK_Window::startScreenshotWorker() {
747 std::lock_guard<std::mutex> lock(screenshot_queue_mutex);
748 if (screenshot_worker.joinable()) {
749 return;
750 }
751
753 screenshot_worker = std::thread(&VK_Window::screenshotWorkerLoop, this);
754 }
755
756 void VK_Window::stopScreenshotWorker() {
757 {
758 std::lock_guard<std::mutex> lock(screenshot_queue_mutex);
760 }
761 screenshot_queue_cv.notify_one();
762
763 if (screenshot_worker.joinable()) {
764 screenshot_worker.join();
765 }
766
768 }
769
770 void VK_Window::screenshotWorkerLoop() {
771 while (true) {
772 ScreenshotSaveTask task{};
773 {
774 std::unique_lock<std::mutex> lock(screenshot_queue_mutex);
775 screenshot_queue_cv.wait(lock, [&]() { return screenshot_worker_stop || !screenshot_save_queue.empty(); });
776
778 return;
779 }
780
781 task = std::move(screenshot_save_queue.front());
782 screenshot_save_queue.pop_front();
783 }
784
785 if (!mxvk::SavePNG_RGBA(task.path.c_str(), task.rgba.data(), static_cast<int>(task.width), static_cast<int>(task.height))) {
786 std::cerr << std::format("mxvk: screenshot failed to write PNG: {}\n", task.path);
787 continue;
788 }
789 std::cout << std::format("mxvk: screenshot saved: {}\n", task.path);
790 }
791 }
792
793 std::string VK_Window::makeScreenshotPath() {
794 const char *home = std::getenv("HOME");
795 std::filesystem::path pictures_dir = (home != nullptr && home[0] != '\0') ? std::filesystem::path(home) / "Pictures" : std::filesystem::path("Pictures");
796 std::filesystem::create_directories(pictures_dir);
797
798 const std::time_t now = std::time(nullptr);
799 std::tm local_time{};
800#if defined(_WIN32)
801 localtime_s(&local_time, &now);
802#else
803 localtime_r(&now, &local_time);
804#endif
805
806 std::ostringstream date_stream;
807 date_stream << std::put_time(&local_time, "%Y.%m.%d");
808 std::ostringstream time_stream;
809 time_stream << std::put_time(&local_time, "%H.%M.%S");
810
811 const std::string prefix = std::format("{}.screenshot.{}.{}.{}x{}-", screenshot_prefix.empty() ? "mxvk" : screenshot_prefix, date_stream.str(), time_stream.str(), swapchain_extent.width, swapchain_extent.height);
812
813 for (uint32_t attempt = 0; attempt < 10000U; ++attempt) {
814 const uint32_t index = screenshot_index++;
815 std::filesystem::path path = pictures_dir / std::format("{}{}.png", prefix, index);
816 if (!std::filesystem::exists(path)) {
817 return path.string();
818 }
819 }
820
821 return (pictures_dir / std::format("{}{}.png", prefix, screenshot_index++)).string();
822 }
823
824 void VK_Window::render() { drawFrame(); }
825
826 void VK_Window::saveSnapshot(const std::string &path) {
827 if (path.empty()) {
828 throw mxvk::Exception("saveSnapshot requires a non-empty output path");
829 }
830
831 std::vector<std::uint8_t> rgba{};
832 uint32_t width = 0;
833 uint32_t height = 0;
834 captureSnapshotPixels(rgba, width, height);
835
836 if (!mxvk::SavePNG_RGBA(path.c_str(), rgba.data(), static_cast<int>(width), static_cast<int>(height))) {
837 throw mxvk::Exception("saveSnapshot failed to write PNG: " + path);
838 }
839 }
840
841 void VK_Window::captureSnapshotPixels(std::vector<std::uint8_t> &rgba_pixels, uint32_t &width, uint32_t &height) {
842 if (!latest_frame_readback_rgba.empty()) {
843 rgba_pixels = latest_frame_readback_rgba;
846 return;
847 }
848 if (!latest_frame_readback_rgba16.empty()) {
849 rgba_pixels.resize(latest_frame_readback_rgba16.size());
850 std::transform(latest_frame_readback_rgba16.begin(), latest_frame_readback_rgba16.end(), rgba_pixels.begin(), [](std::uint16_t value) { return static_cast<std::uint8_t>((static_cast<std::uint32_t>(value) + 128U) / 257U); });
853 return;
854 }
855 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || graphics_queue == VK_NULL_HANDLE) {
856 throw mxvk::Exception("captureSnapshotPixels called before Vulkan render resources are ready");
857 }
859 throw mxvk::Exception("captureSnapshotPixels called before a frame has been presented");
860 }
862 if (!capture_offscreen && !swapchain_supports_transfer_src) {
863 throw mxvk::Exception("captureSnapshotPixels requires swapchain transfer-source support");
864 }
865 const VkExtent2D source_extent = capture_offscreen ? getRenderExtent() : swapchain_extent;
866 const VkImage source_image = capture_offscreen ? post_process_images.front()[last_presented_image_index] : swapchain_images[last_presented_image_index];
867 const VkImageLayout source_layout = capture_offscreen ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : headless() ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
868 if (source_extent.width == 0U || source_extent.height == 0U) {
869 throw mxvk::Exception("captureSnapshotPixels cannot capture an empty swapchain extent");
870 }
871
872 const bool format_is_bgra = swapchain_format == VK_FORMAT_B8G8R8A8_UNORM || swapchain_format == VK_FORMAT_B8G8R8A8_SRGB;
873 const bool format_is_rgba = swapchain_format == VK_FORMAT_R8G8B8A8_UNORM || swapchain_format == VK_FORMAT_R8G8B8A8_SRGB;
874 if (!format_is_bgra && !format_is_rgba) {
875 throw mxvk::Exception(std::format("captureSnapshotPixels unsupported swapchain format: {}", static_cast<int>(swapchain_format)));
876 }
877
878 const VkDeviceSize row_bytes = static_cast<VkDeviceSize>(source_extent.width) * 4U;
879 const VkDeviceSize image_bytes = row_bytes * static_cast<VkDeviceSize>(source_extent.height);
880
881 VkBuffer readback_buffer = VK_NULL_HANDLE;
882 VkDeviceMemory readback_memory = VK_NULL_HANDLE;
883 VkCommandBuffer command_buffer = VK_NULL_HANDLE;
884 VkFence copy_fence = VK_NULL_HANDLE;
885
886 auto cleanup = [&]() {
887 if (copy_fence != VK_NULL_HANDLE) {
888 vkDestroyFence(device, copy_fence, nullptr);
889 }
890 if (command_buffer != VK_NULL_HANDLE) {
891 vkFreeCommandBuffers(device, command_pool, 1, &command_buffer);
892 }
893 if (readback_buffer != VK_NULL_HANDLE) {
894 vkDestroyBuffer(device, readback_buffer, nullptr);
895 }
896 if (readback_memory != VK_NULL_HANDLE) {
897 vkFreeMemory(device, readback_memory, nullptr);
898 }
899 };
900
901 try {
902 VkBufferCreateInfo buffer_info{};
903 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
904 buffer_info.size = image_bytes;
905 buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
906 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
907 if (vkCreateBuffer(device, &buffer_info, nullptr, &readback_buffer) != VK_SUCCESS) {
908 throw mxvk::Exception("captureSnapshotPixels failed to create readback buffer");
909 }
910
911 VkMemoryRequirements memory_requirements{};
912 vkGetBufferMemoryRequirements(device, readback_buffer, &memory_requirements);
913
914 VkPhysicalDeviceMemoryProperties memory_properties{};
915 vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties);
916 uint32_t memory_type_index = invalid_queue_index;
917 constexpr VkMemoryPropertyFlags REQUIRED_PROPERTIES = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
918 constexpr VkMemoryPropertyFlags PREFERRED_PROPERTIES = REQUIRED_PROPERTIES | VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
919 for (VkMemoryPropertyFlags properties : {PREFERRED_PROPERTIES, REQUIRED_PROPERTIES}) {
920 for (uint32_t index = 0; index < memory_properties.memoryTypeCount; ++index) {
921 const bool type_matches = (memory_requirements.memoryTypeBits & (1U << index)) != 0U;
922 const bool properties_match = (memory_properties.memoryTypes[index].propertyFlags & properties) == properties;
923 if (type_matches && properties_match) {
924 memory_type_index = index;
925 break;
926 }
927 }
928 if (memory_type_index != invalid_queue_index) {
929 break;
930 }
931 }
932 if (memory_type_index == invalid_queue_index) {
933 throw mxvk::Exception("captureSnapshotPixels failed to find host-visible coherent memory");
934 }
935
936 VkMemoryAllocateInfo allocation_info{};
937 allocation_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
938 allocation_info.allocationSize = memory_requirements.size;
939 allocation_info.memoryTypeIndex = memory_type_index;
940 if (vkAllocateMemory(device, &allocation_info, nullptr, &readback_memory) != VK_SUCCESS) {
941 throw mxvk::Exception("captureSnapshotPixels failed to allocate readback memory");
942 }
943 if (vkBindBufferMemory(device, readback_buffer, readback_memory, 0) != VK_SUCCESS) {
944 throw mxvk::Exception("captureSnapshotPixels failed to bind readback memory");
945 }
946
947 VkCommandBufferAllocateInfo command_buffer_info{};
948 command_buffer_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
949 command_buffer_info.commandPool = command_pool;
950 command_buffer_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
951 command_buffer_info.commandBufferCount = 1;
952 if (vkAllocateCommandBuffers(device, &command_buffer_info, &command_buffer) != VK_SUCCESS) {
953 throw mxvk::Exception("captureSnapshotPixels failed to allocate command buffer");
954 }
955
956 VkFenceCreateInfo fence_info{};
957 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
958 if (vkCreateFence(device, &fence_info, nullptr, &copy_fence) != VK_SUCCESS) {
959 throw mxvk::Exception("captureSnapshotPixels failed to create copy fence");
960 }
961
962 VkFence source_fence = image_fences[last_presented_image_index];
963 if (source_fence != VK_NULL_HANDLE) {
964 const VkResult wait_result = vkWaitForFences(device, 1, &source_fence, VK_TRUE, UINT64_MAX);
965 if (wait_result != VK_SUCCESS) {
966 throw mxvk::Exception(std::format("captureSnapshotPixels failed waiting for rendered frame: {}", static_cast<int>(wait_result)));
967 }
968 }
969
970 const VkResult idle_result = vkDeviceWaitIdle(device);
971 if (idle_result != VK_SUCCESS) {
972 throw mxvk::Exception(std::format("captureSnapshotPixels failed waiting for device idle: {}", static_cast<int>(idle_result)));
973 }
974
975 VkCommandBufferBeginInfo begin_info{};
976 begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
977 begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
978 if (vkBeginCommandBuffer(command_buffer, &begin_info) != VK_SUCCESS) {
979 throw mxvk::Exception("captureSnapshotPixels failed to begin copy command buffer");
980 }
981
982 VkImageMemoryBarrier2 to_transfer_barrier{};
983 to_transfer_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
984 to_transfer_barrier.srcStageMask = capture_offscreen ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT : headless() ? VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT : VK_PIPELINE_STAGE_2_NONE;
985 to_transfer_barrier.srcAccessMask = capture_offscreen ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : headless() ? VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT : VK_ACCESS_2_NONE;
986 to_transfer_barrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
987 to_transfer_barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
988 to_transfer_barrier.oldLayout = source_layout;
989 to_transfer_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
990 to_transfer_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
991 to_transfer_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
992 to_transfer_barrier.image = source_image;
993 to_transfer_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
994 to_transfer_barrier.subresourceRange.baseMipLevel = 0;
995 to_transfer_barrier.subresourceRange.levelCount = 1;
996 to_transfer_barrier.subresourceRange.baseArrayLayer = 0;
997 to_transfer_barrier.subresourceRange.layerCount = 1;
998
999 VkDependencyInfo to_transfer_dependency{};
1000 to_transfer_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1001 to_transfer_dependency.imageMemoryBarrierCount = 1;
1002 to_transfer_dependency.pImageMemoryBarriers = &to_transfer_barrier;
1003 vkCmdPipelineBarrier2(command_buffer, &to_transfer_dependency);
1004
1005 VkBufferImageCopy copy_region{};
1006 copy_region.bufferOffset = 0;
1007 copy_region.bufferRowLength = 0;
1008 copy_region.bufferImageHeight = 0;
1009 copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
1010 copy_region.imageSubresource.mipLevel = 0;
1011 copy_region.imageSubresource.baseArrayLayer = 0;
1012 copy_region.imageSubresource.layerCount = 1;
1013 copy_region.imageOffset = {0, 0, 0};
1014 copy_region.imageExtent = {source_extent.width, source_extent.height, 1};
1015 vkCmdCopyImageToBuffer(command_buffer, source_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback_buffer, 1, &copy_region);
1016
1017 VkImageMemoryBarrier2 to_present_barrier{};
1018 to_present_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
1019 to_present_barrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
1020 to_present_barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
1021 to_present_barrier.dstStageMask = capture_offscreen ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT : headless() ? VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT : VK_PIPELINE_STAGE_2_NONE;
1022 to_present_barrier.dstAccessMask = capture_offscreen ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : headless() ? VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT : VK_ACCESS_2_NONE;
1023 to_present_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
1024 to_present_barrier.newLayout = source_layout;
1025 to_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1026 to_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1027 to_present_barrier.image = source_image;
1028 to_present_barrier.subresourceRange = to_transfer_barrier.subresourceRange;
1029
1030 VkDependencyInfo to_present_dependency{};
1031 to_present_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1032 to_present_dependency.imageMemoryBarrierCount = 1;
1033 to_present_dependency.pImageMemoryBarriers = &to_present_barrier;
1034 vkCmdPipelineBarrier2(command_buffer, &to_present_dependency);
1035
1036 if (vkEndCommandBuffer(command_buffer) != VK_SUCCESS) {
1037 throw mxvk::Exception("captureSnapshotPixels failed to end copy command buffer");
1038 }
1039
1040 VkCommandBufferSubmitInfo command_submit_info{};
1041 command_submit_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
1042 command_submit_info.commandBuffer = command_buffer;
1043
1044 VkSubmitInfo2 submit_info{};
1045 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
1046 submit_info.commandBufferInfoCount = 1;
1047 submit_info.pCommandBufferInfos = &command_submit_info;
1048
1049 const VkResult submit_result = vkQueueSubmit2(graphics_queue, 1, &submit_info, copy_fence);
1050 if (submit_result != VK_SUCCESS) {
1051 throw mxvk::Exception(std::format("captureSnapshotPixels failed to submit copy command: {}", static_cast<int>(submit_result)));
1052 }
1053 const VkResult copy_wait_result = vkWaitForFences(device, 1, &copy_fence, VK_TRUE, UINT64_MAX);
1054 if (copy_wait_result != VK_SUCCESS) {
1055 throw mxvk::Exception(std::format("captureSnapshotPixels failed waiting for copy: {}", static_cast<int>(copy_wait_result)));
1056 }
1057
1058 void *mapped = nullptr;
1059 if (vkMapMemory(device, readback_memory, 0, image_bytes, 0, &mapped) != VK_SUCCESS) {
1060 throw mxvk::Exception("captureSnapshotPixels failed to map readback memory");
1061 }
1062
1063 const auto *src = static_cast<const std::uint8_t *>(mapped);
1064 rgba_pixels.resize(static_cast<std::size_t>(image_bytes));
1065 for (std::size_t i = 0; i < rgba_pixels.size(); i += 4U) {
1066 if (format_is_bgra) {
1067 rgba_pixels[i + 0U] = src[i + 2U];
1068 rgba_pixels[i + 1U] = src[i + 1U];
1069 rgba_pixels[i + 2U] = src[i + 0U];
1070 rgba_pixels[i + 3U] = src[i + 3U];
1071 } else {
1072 rgba_pixels[i + 0U] = src[i + 0U];
1073 rgba_pixels[i + 1U] = src[i + 1U];
1074 rgba_pixels[i + 2U] = src[i + 2U];
1075 rgba_pixels[i + 3U] = src[i + 3U];
1076 }
1077 }
1078 vkUnmapMemory(device, readback_memory);
1079 width = source_extent.width;
1080 height = source_extent.height;
1081 } catch (...) {
1082 cleanup();
1083 throw;
1084 }
1085
1086 cleanup();
1087 }
1088
1089 void VK_Window::onFrameReadback([[maybe_unused]] std::vector<std::uint8_t> &rgba_pixels, [[maybe_unused]] uint32_t width, [[maybe_unused]] uint32_t height) {}
1090
1091 void VK_Window::onFrameReadbackRgba16([[maybe_unused]] std::vector<std::uint16_t> &rgba_pixels, [[maybe_unused]] uint32_t width, [[maybe_unused]] uint32_t height) {}
1092
1094
1095 void VK_Window::dispatchFrameReadback(FrameReadbackSlot &slot) {
1096 if (!slot.pending) {
1097 return;
1098 }
1099 if (slot.mapped == nullptr || slot.width == 0U || slot.height == 0U) {
1100 slot.pending = false;
1101 return;
1102 }
1103
1104 if (slot.format == VK_FORMAT_R16G16B16A16_SFLOAT) {
1105 const auto *source = static_cast<const std::uint16_t *>(slot.mapped);
1106 const std::size_t component_count = static_cast<std::size_t>(slot.width) * slot.height * 4U;
1107 latest_frame_readback_rgba16.resize(component_count);
1108 for (std::size_t component = 0; component < component_count; ++component) {
1109 const float normalized = std::clamp(half_to_float(source[component]), 0.0F, 1.0F);
1110 latest_frame_readback_rgba16[component] = static_cast<std::uint16_t>(std::lround(normalized * 65535.0F));
1111 }
1112 latest_frame_readback_width = slot.width;
1113 latest_frame_readback_height = slot.height;
1114 slot.pending = false;
1116 return;
1117 }
1118
1119 const auto *source = static_cast<const std::uint8_t *>(slot.mapped);
1120 const std::size_t pixel_bytes = static_cast<std::size_t>(slot.width) * slot.height * 4U;
1121 latest_frame_readback_rgba.resize(pixel_bytes);
1122 const bool format_is_bgra = slot.format == VK_FORMAT_B8G8R8A8_UNORM || slot.format == VK_FORMAT_B8G8R8A8_SRGB;
1123 for (std::size_t offset = 0; offset < pixel_bytes; offset += 4U) {
1124 if (format_is_bgra) {
1125 latest_frame_readback_rgba[offset + 0U] = source[offset + 2U];
1126 latest_frame_readback_rgba[offset + 1U] = source[offset + 1U];
1127 latest_frame_readback_rgba[offset + 2U] = source[offset + 0U];
1128 latest_frame_readback_rgba[offset + 3U] = source[offset + 3U];
1129 } else {
1130 latest_frame_readback_rgba[offset + 0U] = source[offset + 0U];
1131 latest_frame_readback_rgba[offset + 1U] = source[offset + 1U];
1132 latest_frame_readback_rgba[offset + 2U] = source[offset + 2U];
1133 latest_frame_readback_rgba[offset + 3U] = source[offset + 3U];
1134 }
1135 }
1136
1137 latest_frame_readback_width = slot.width;
1138 latest_frame_readback_height = slot.height;
1139 slot.pending = false;
1141 }
1142
1144 if (device == VK_NULL_HANDLE) {
1145 return;
1146 }
1147 const bool has_pending = std::ranges::any_of(frame_readback_slots, [](const FrameReadbackSlot &slot) { return slot.pending; });
1148 if (!has_pending) {
1149 return;
1150 }
1151
1152 const VkResult wait_result = vkDeviceWaitIdle(device);
1153 if (wait_result != VK_SUCCESS) {
1154 throw mxvk::Exception(std::format("frame readback flush failed waiting for the device (VkResult={})", static_cast<int>(wait_result)));
1155 }
1156 for (uint32_t offset = 0; offset < max_frames_in_flight; ++offset) {
1157 const uint32_t slot_index = (current_frame + offset) % max_frames_in_flight;
1159 }
1160 }
1161
1164 return;
1165 }
1167 throw mxvk::Exception("frame readback requires swapchain transfer-source support");
1168 }
1169
1170 const VkExtent2D readback_extent = getRenderExtent();
1171 const VkDeviceSize required_size = static_cast<VkDeviceSize>(readback_extent.width) * readback_extent.height * (frame_readback_rgba16_enabled ? 8U : 4U);
1172 if (required_size == 0U) {
1173 throw mxvk::Exception("frame readback cannot use an empty swapchain extent");
1174 }
1175 const bool resources_ready = std::ranges::all_of(frame_readback_slots, [&](const FrameReadbackSlot &slot) { return slot.buffer != VK_NULL_HANDLE && slot.mapped != nullptr && slot.size >= required_size; });
1176 if (resources_ready) {
1177 return;
1178 }
1179
1180 if (device != VK_NULL_HANDLE) {
1182 vkDeviceWaitIdle(device);
1183 }
1185
1186 VkBufferCreateInfo buffer_info{};
1187 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1188 buffer_info.size = required_size;
1189 buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
1190 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1191 VkPhysicalDeviceMemoryProperties memory_properties{};
1192 vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties);
1193 constexpr VkMemoryPropertyFlags REQUIRED_PROPERTIES = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
1194 constexpr VkMemoryPropertyFlags PREFERRED_PROPERTIES = REQUIRED_PROPERTIES | VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
1195 bool memory_type_logged = false;
1196 try {
1197 for (FrameReadbackSlot &slot : frame_readback_slots) {
1198 if (vkCreateBuffer(device, &buffer_info, nullptr, &slot.buffer) != VK_SUCCESS) {
1199 throw mxvk::Exception("failed to create pipelined frame-readback buffer");
1200 }
1201
1202 VkMemoryRequirements memory_requirements{};
1203 vkGetBufferMemoryRequirements(device, slot.buffer, &memory_requirements);
1204 uint32_t memory_type_index = invalid_queue_index;
1205 for (VkMemoryPropertyFlags properties : {PREFERRED_PROPERTIES, REQUIRED_PROPERTIES}) {
1206 for (uint32_t index = 0; index < memory_properties.memoryTypeCount; ++index) {
1207 const bool type_matches = (memory_requirements.memoryTypeBits & (1U << index)) != 0U;
1208 const bool properties_match = (memory_properties.memoryTypes[index].propertyFlags & properties) == properties;
1209 if (type_matches && properties_match) {
1210 memory_type_index = index;
1211 break;
1212 }
1213 }
1214 if (memory_type_index != invalid_queue_index) {
1215 break;
1216 }
1217 }
1218 if (memory_type_index == invalid_queue_index) {
1219 throw mxvk::Exception("failed to find host-visible frame-readback memory");
1220 }
1221 if (!memory_type_logged) {
1222 const VkMemoryPropertyFlags selected_properties = memory_properties.memoryTypes[memory_type_index].propertyFlags;
1223 std::cout << std::format("mxvk: frame readback memory type {} ({})\n", memory_type_index, (selected_properties & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) != 0U ? "host cached" : "host coherent fallback");
1224 memory_type_logged = true;
1225 }
1226
1227 VkMemoryAllocateInfo allocation_info{};
1228 allocation_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1229 allocation_info.allocationSize = memory_requirements.size;
1230 allocation_info.memoryTypeIndex = memory_type_index;
1231 if (vkAllocateMemory(device, &allocation_info, nullptr, &slot.memory) != VK_SUCCESS) {
1232 throw mxvk::Exception("failed to allocate frame-readback memory");
1233 }
1234 if (vkBindBufferMemory(device, slot.buffer, slot.memory, 0) != VK_SUCCESS) {
1235 throw mxvk::Exception("failed to bind frame-readback memory");
1236 }
1237 if (vkMapMemory(device, slot.memory, 0, required_size, 0, &slot.mapped) != VK_SUCCESS) {
1238 throw mxvk::Exception("failed to map frame-readback memory");
1239 }
1240 slot.size = required_size;
1241 }
1242 } catch (...) {
1244 throw;
1245 }
1250 }
1251
1253 for (FrameReadbackSlot &slot : frame_readback_slots) {
1254 if (device != VK_NULL_HANDLE && slot.mapped != nullptr && slot.memory != VK_NULL_HANDLE) {
1255 vkUnmapMemory(device, slot.memory);
1256 }
1257 slot.mapped = nullptr;
1258 if (device != VK_NULL_HANDLE && slot.buffer != VK_NULL_HANDLE) {
1259 vkDestroyBuffer(device, slot.buffer, nullptr);
1260 }
1261 slot.buffer = VK_NULL_HANDLE;
1262 if (device != VK_NULL_HANDLE && slot.memory != VK_NULL_HANDLE) {
1263 vkFreeMemory(device, slot.memory, nullptr);
1264 }
1265 slot.memory = VK_NULL_HANDLE;
1266 slot.size = 0;
1267 slot.width = 0;
1268 slot.height = 0;
1269 slot.format = VK_FORMAT_UNDEFINED;
1270 slot.pending = false;
1271 }
1276 }
1277
1279
1281 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || vkTrimCommandPool == nullptr) {
1282 return;
1283 }
1284
1285 vkTrimCommandPool(device, command_pool, 0);
1286 }
1287
1288 void VK_Window::maybeTrimMemory() {
1289 const auto now = std::chrono::steady_clock::now();
1291 return;
1292 }
1293
1294 trimMemory();
1296 }
1297
1298 bool VK_Window::initWindow(const std::string &title, int width, int height, SDL_WindowFlags flags) {
1299 std::cout << std::format("mxvk: entering initWindow (title='{}', width={}, height={})\n", title, width, height);
1300 if (width <= 0 || height <= 0) {
1301 std::cerr << "mxvk: Window dimensions must be positive\n";
1302 return false;
1303 }
1304
1305 if (!sdl_initialized) {
1306 constexpr SDL_InitFlags sdlInitFlags = SDL_INIT_VIDEO | SDL_INIT_GAMEPAD;
1307 std::cout << "SDL3: initializing video/gamepad subsystems\n";
1308 if (!SDL_Init(sdlInitFlags)) {
1309 std::cerr << "mxvk: Error on SDL init: " << SDL_GetError() << "\n";
1310 return false;
1311 }
1312 sdl_initialized = true;
1313 SDL_SetGamepadEventsEnabled(true);
1314 SDL_SetJoystickEventsEnabled(true);
1315 std::cout << "SDL3: video/gamepad subsystems initialized\n";
1316 }
1317
1318 if (!vulkan_library_loaded) {
1319 std::cout << "SDL3: loading the Vulkan loader library\n";
1320 if (!SDL_Vulkan_LoadLibrary(nullptr)) {
1321 std::cerr << std::format("mxvk: SDL_Vulkan_LoadLibrary failed: {}\n", SDL_GetError());
1322 SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD);
1323 sdl_initialized = false;
1324 return false;
1325 }
1326 vulkan_library_loaded = true;
1327 }
1328
1329 const bool fullscreen = (flags & SDL_WINDOW_FULLSCREEN) != 0;
1330 const SDL_DisplayMode *fullscreen_mode = nullptr;
1331 if (fullscreen) {
1332 const SDL_DisplayID display = SDL_GetPrimaryDisplay();
1333 fullscreen_mode = display != 0 ? SDL_GetCurrentDisplayMode(display) : nullptr;
1334 if (fullscreen_mode != nullptr && fullscreen_mode->w > 0 && fullscreen_mode->h > 0) {
1335 width = fullscreen_mode->w;
1336 height = fullscreen_mode->h;
1337 std::cout << std::format("SDL3: using current display mode for fullscreen window: {}x{}\n", width, height);
1338 }
1339 }
1340
1341 std::cout << "SDL3: creating SDL window with Vulkan capability\n";
1342 SDL_Window *raw_window = SDL_CreateWindow(title.c_str(), width, height, flags);
1343 if (raw_window == nullptr) {
1344 std::cerr << std::format("Error creating window: {}\n", SDL_GetError());
1345 SDL_Vulkan_UnloadLibrary();
1346 vulkan_library_loaded = false;
1347 std::cout << "SDL3: rolling back SDL video/gamepad subsystems after window creation failure\n";
1348 SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD);
1349 sdl_initialized = false;
1350 return false;
1351 }
1352
1353 window.reset(raw_window);
1354 if (fullscreen && fullscreen_mode != nullptr && !SDL_SetWindowFullscreenMode(window.get(), fullscreen_mode)) {
1355 std::cerr << std::format("mxvk: SDL_SetWindowFullscreenMode failed: {}\n", SDL_GetError());
1356 }
1357 std::cout << "SDL3: showing created window\n";
1358 SDL_ShowWindow(window.get());
1359 if (fullscreen && !SDL_SyncWindow(window.get())) {
1360 std::cerr << std::format("mxvk: SDL_SyncWindow failed after fullscreen show: {}\n", SDL_GetError());
1361 }
1362 if (fullscreen) {
1363 for (int attempt = 0; attempt < 50; ++attempt) {
1364 int pixel_width = 0;
1365 int pixel_height = 0;
1366 SDL_GetWindowSizeInPixels(window.get(), &pixel_width, &pixel_height);
1367 if (pixel_width == width && pixel_height == height) {
1368 break;
1369 }
1370 SDL_PumpEvents();
1371 SDL_Delay(10);
1372 }
1373 }
1374 std::cout << "mxvk: initWindow complete\n";
1375 return true;
1376 }
1377
1378 void VK_Window::exit() { active = false; }
1379
1381
1383
1384 void VK_Window::onPrepareFrameRendering([[maybe_unused]] VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index) {}
1385
1386 void VK_Window::onRecordCustomRendering([[maybe_unused]] VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index) {}
1387
1388 void VK_Window::onRecordPostProcessingTexture([[maybe_unused]] VkCommandBuffer cmd, [[maybe_unused]] uint32_t image_index, [[maybe_unused]] VkImageView texture_view, [[maybe_unused]] VkExtent2D texture_extent) {}
1389
1390 void VK_Window::onConfigureDepthStencilAttachments([[maybe_unused]] VkRenderingAttachmentInfo &depth_attachment, [[maybe_unused]] VkRenderingAttachmentInfo &stencil_attachment, [[maybe_unused]] uint32_t image_index) {}
1391
1392 void VK_Window::renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd) { renderStandaloneSprite(sprite, cmd, swapchain_extent); }
1393
1394 void VK_Window::renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd, VkExtent2D extent) {
1395 if (device == VK_NULL_HANDLE || extent.width == 0U || extent.height == 0U) {
1396 return;
1397 }
1398
1399 if (sprite_pipeline == VK_NULL_HANDLE) {
1400 return;
1401 }
1402 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1403 sprite.renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1404 }
1405
1406 void VK_Window::setRenderExtent(uint32_t width, uint32_t height) {
1407 if ((width == 0U) != (height == 0U)) {
1408 throw mxvk::Exception("render extent requires two positive dimensions or two zeros");
1409 }
1410 if (render_extent_override.width == width && render_extent_override.height == height) {
1411 return;
1412 }
1413 if (headless()) {
1414 if (width == 0U && height == 0U) {
1415 return;
1416 }
1417 if (requested_headless_extent.width == width && requested_headless_extent.height == height) {
1418 return;
1419 }
1420 if (device != VK_NULL_HANDLE) {
1421 vkDeviceWaitIdle(device);
1424 cleanupSyncObjects();
1425 cleanupSwapchain(false);
1426 }
1427 requested_headless_extent = {width, height};
1429 sprite_state_dirty = true;
1430 text_state_dirty = true;
1431 if (device != VK_NULL_HANDLE) {
1432 createDevice();
1434 }
1435 std::cout << std::format("mxvk: surface-free render extent {}x{}\n", width, height);
1436 return;
1437 }
1438 if (device != VK_NULL_HANDLE && swapchain != VK_NULL_HANDLE) {
1439 vkDeviceWaitIdle(device);
1441 destroyPostProcessTargets();
1442 }
1443 render_extent_override = {width, height};
1444 sprite_state_dirty = true;
1445 if (device != VK_NULL_HANDLE && !swapchain_images.empty() && !post_process_sprites.empty()) {
1446 createPostProcessTargets();
1447 }
1448 std::cout << std::format("mxvk: fixed render extent {}\n", width > 0U ? std::format("{}x{}", width, height) : "disabled");
1449 }
1450
1452
1454 if (hdr_render_intermediates_enabled == enabled) {
1455 return;
1456 }
1457 if (device != VK_NULL_HANDLE) {
1458 vkDeviceWaitIdle(device);
1459 destroyPostProcessTargets();
1460 }
1462 sprite_state_dirty = true;
1463 if (device != VK_NULL_HANDLE && !swapchain_images.empty() && !post_process_sprites.empty()) {
1464 createPostProcessTargets();
1465 }
1466 std::cout << "mxvk: HDR render intermediates " << (enabled ? "enabled (RGBA16F)\n" : "disabled (RGBA8)\n");
1467 }
1468
1469 VK_Sprite *VK_Window::attachPostProcessingShader(const std::string &fragmentShaderPath, float p1, float p2, float p3, float p4) {
1470 const std::vector<VK_Sprite *> sprites = attachPostProcessingShaders({PostProcessingEffect{fragmentShaderPath, {p1, p2, p3, p4}, false}});
1471 return sprites.empty() ? nullptr : sprites.front();
1472 }
1473
1474 std::vector<VK_Sprite *> VK_Window::attachPostProcessingShaders(const std::vector<PostProcessingEffect> &effects) {
1476
1477 std::vector<VK_Sprite *> attachedSprites;
1478 attachedSprites.reserve(effects.size());
1479 for (size_t effect_index = 0; effect_index < effects.size(); ++effect_index) {
1480 const PostProcessingEffect &effect = effects[effect_index];
1481 if (effect.fragmentShaderPath.empty()) {
1482 throw mxvk::Exception("Cannot attach a post-processing pass with an empty shader path");
1483 }
1484
1485 const ShaderModuleInfo module_info = inspect_spirv(load_spv(effect.fragmentShaderPath));
1486 const ShaderStage stage = effect.stage == ShaderStage::Unknown ? module_info.stage : effect.stage;
1487 if (stage != ShaderStage::Fragment && stage != ShaderStage::Compute) {
1488 throw mxvk::Exception("Post-processing shaders must contain a fragment or compute entry point");
1489 }
1490 if (effect.stage != ShaderStage::Unknown && effect.stage != module_info.stage) {
1491 throw mxvk::Exception("Post-processing shader stage does not match its SPIR-V entry point");
1492 }
1493
1494 VK_Sprite *sprite = createSprite(1, 1, resolveRuntimeShaderPath("sprite.vert.spv", MXVK_SPRITE_SHADER_DIR), "", effect.spectrumBinCount, effect.spectrumHistoryLayerCount);
1495 sprite->enableExtendedUBO();
1496 if (effect.historySource != nullptr) {
1497 sprite->shareHistoryTexture(*effect.historySource);
1498 }
1499 const bool final_fragment = stage == ShaderStage::Fragment && effect_index + 1U == effects.size();
1500 if (stage == ShaderStage::Fragment) {
1501 if (!final_fragment || hdr_render_intermediates_enabled) {
1503 }
1505 } else if (stage == ShaderStage::Compute) {
1507 if (module_info.storageImageFormat != required_format) {
1508 throw mxvk::Exception(std::format("Compute shader '{}' must declare layout({}) for the "
1509 "active {} render path",
1510 effect.fragmentShaderPath,
1511 hdr_render_intermediates_enabled ? "rgba16f" : "rgba8",
1512 hdr_render_intermediates_enabled ? "HDR" : "SDR"));
1513 }
1514 sprite->enableComputeShader(effect.fragmentShaderPath, module_info.localSizeX, module_info.localSizeY, module_info.localSizeZ);
1515 }
1516 const uint32_t black_pixel = 0xFF000000u;
1517 sprite->updateTexture(&black_pixel, 1, 1);
1518 sprite->setShaderParams(effect.params[0], effect.params[1], effect.params[2], effect.params[3]);
1519
1520 attachedSprites.push_back(sprite);
1521 owned_post_process_sprites.push_back(sprite);
1522 post_process_sprites.push_back(sprite);
1523 post_process_effect_params.push_back(effect.params);
1525 post_process_effect_start_times.push_back(std::chrono::steady_clock::now());
1526 post_process_effect_stages.push_back(stage);
1527 }
1528
1529 if (!post_process_effect_stages.empty()) {
1530 post_process_present_sprite = createSprite(1, 1, resolveRuntimeShaderPath("sprite.vert.spv", MXVK_SPRITE_SHADER_DIR));
1531 const uint32_t black_pixel = 0xFF000000u;
1532 post_process_present_sprite->updateTexture(&black_pixel, 1, 1);
1533 if (!post_process_present_shader_path.empty()) {
1535 }
1537
1539 post_process_composite_sprite->updateTexture(&black_pixel, 1, 1);
1541 }
1542
1543 post_process_sprite = attachedSprites.empty() ? nullptr : attachedSprites.front();
1545 post_process_params = post_process_effect_params.empty() ? std::array<float, 4>{} : post_process_effect_params.front();
1547 post_process_start_time = post_process_effect_start_times.empty() ? std::chrono::steady_clock::now() : post_process_effect_start_times.front();
1548 if (device != VK_NULL_HANDLE && !swapchain_images.empty()) {
1549 createPostProcessTargets();
1550 }
1551 sprite_state_dirty = true;
1552 return attachedSprites;
1553 }
1554
1557 return;
1558 }
1560 if (post_process_present_sprite != nullptr) {
1562 sprite_state_dirty = true;
1563 }
1564 }
1565
1567 if (device != VK_NULL_HANDLE && !post_process_sprites.empty()) {
1568 vkDeviceWaitIdle(device);
1569 }
1570
1571 post_process_sprite = nullptr;
1572 destroyPostProcessTargets();
1576
1577 if (!owned_post_process_sprites.empty()) {
1578 const std::vector<VK_Sprite *> sprites_to_remove = owned_post_process_sprites;
1579 sprites.erase(std::remove_if(sprites.begin(), sprites.end(), [&sprites_to_remove](const std::unique_ptr<VK_Sprite> &sprite) { return std::ranges::find(sprites_to_remove, sprite.get()) != sprites_to_remove.end(); }), sprites.end());
1580 sprite_state_dirty = true;
1581 }
1582 owned_post_process_sprite = nullptr;
1584 post_process_sprites.clear();
1589 }
1590
1591 void VK_Window::setPostProcessingShaderParams(float p1, float p2, float p3, float p4) {
1592 post_process_params = {p1, p2, p3, p4};
1593 if (post_process_sprites.empty()) {
1594 return;
1595 }
1596 setPostProcessingShaderParams(0, p1, p2, p3, p4);
1597 }
1598
1599 void VK_Window::setPostProcessingShaderParams(size_t effectIndex, float p1, float p2, float p3, float p4) {
1600 if (effectIndex >= post_process_sprites.size()) {
1601 return;
1602 }
1603 post_process_params = {p1, p2, p3, p4};
1604 if (effectIndex < post_process_effect_params.size()) {
1606 }
1607 if (post_process_sprites[effectIndex] != nullptr) {
1608 post_process_sprites[effectIndex]->setShaderParams(p1, p2, p3, p4);
1609 }
1610 }
1611
1613 post_process_time_enabled = enabled;
1614 post_process_start_time = std::chrono::steady_clock::now();
1615 if (post_process_sprites.empty()) {
1616 return;
1617 }
1619 }
1620
1621 void VK_Window::setPostProcessingShaderTimeEnabled(size_t effectIndex, bool enabled) {
1622 if (effectIndex >= post_process_sprites.size()) {
1623 return;
1624 }
1625 post_process_time_enabled = enabled;
1626 post_process_start_time = std::chrono::steady_clock::now();
1627 if (effectIndex < post_process_effect_time_enabled.size()) {
1628 post_process_effect_time_enabled[effectIndex] = enabled;
1629 }
1630 if (effectIndex < post_process_effect_start_times.size()) {
1632 }
1633 }
1634
1636 if (owned_post_process_sprite != nullptr && sprite != owned_post_process_sprite) {
1637 std::vector<VK_Sprite *> sprites_to_remove = owned_post_process_sprites;
1638 if (sprites_to_remove.empty()) {
1639 sprites_to_remove.push_back(owned_post_process_sprite);
1640 }
1641 sprites.erase(std::remove_if(sprites.begin(), sprites.end(), [&sprites_to_remove](const std::unique_ptr<VK_Sprite> &existing_sprite) { return std::ranges::find(sprites_to_remove, existing_sprite.get()) != sprites_to_remove.end(); }), sprites.end());
1642 owned_post_process_sprite = nullptr;
1644 sprite_state_dirty = true;
1645 }
1646 post_process_sprite = sprite;
1647 post_process_sprites = sprite == nullptr ? std::vector<VK_Sprite *>{} : std::vector<VK_Sprite *>{sprite};
1651 if (device != VK_NULL_HANDLE && !swapchain_images.empty()) {
1652 createPostProcessTargets();
1653 }
1654 }
1655
1656 bool VK_Window::isPostProcessSprite(const VK_Sprite *sprite) const { return sprite != nullptr && (sprite == post_process_present_sprite || sprite == post_process_composite_sprite || std::ranges::find(post_process_sprites, sprite) != post_process_sprites.end()); }
1657
1658 void VK_Window::destroyPostProcessTargets() {
1659 for (VK_Sprite *sprite : post_process_sprites) {
1660 if (sprite != nullptr) {
1662 }
1663 }
1664 if (post_process_present_sprite != nullptr) {
1666 }
1667 if (post_process_composite_sprite != nullptr) {
1669 }
1670 for (const std::vector<VkImageView> &views : post_process_views) {
1671 for (VkImageView view : views) {
1672 if (view != VK_NULL_HANDLE) {
1673 vkDestroyImageView(device, view, nullptr);
1674 }
1675 }
1676 }
1677 for (const std::vector<VkImage> &images : post_process_images) {
1678 for (VkImage image : images) {
1679 if (image != VK_NULL_HANDLE) {
1680 vkDestroyImage(device, image, nullptr);
1681 }
1682 }
1683 }
1684 for (const std::vector<VkDeviceMemory> &memories : post_process_memories) {
1685 for (VkDeviceMemory memory : memories) {
1686 if (memory != VK_NULL_HANDLE) {
1687 vkFreeMemory(device, memory, nullptr);
1688 }
1689 }
1690 }
1691 post_process_views.clear();
1692 post_process_images.clear();
1693 post_process_memories.clear();
1695 }
1696
1697 void VK_Window::createPostProcessTargets() {
1698 destroyPostProcessTargets();
1699 if (post_process_sprites.empty() || swapchain_images.empty()) {
1700 return;
1701 }
1702 const bool has_compute = std::ranges::find(post_process_effect_stages, ShaderStage::Compute) != post_process_effect_stages.end();
1703 const bool detached_render = render_extent_override.width > 0U && render_extent_override.height > 0U;
1704 size_t target_count = 1U;
1706 target_count = 4U;
1707 } else if (detached_render || has_compute || post_process_sprites.size() > 1) {
1708 target_count = 3U;
1709 }
1710 const VkExtent2D render_extent = getRenderExtent();
1711 if (hdr_render_intermediates_enabled || has_compute) {
1712 VkFormatProperties format_properties{};
1713 vkGetPhysicalDeviceFormatProperties(physical_device, postProcessIntermediateFormat(), &format_properties);
1714 VkFormatFeatureFlags required_features = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
1715 if (has_compute) {
1716 required_features |= VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
1717 }
1718 if ((format_properties.optimalTilingFeatures & required_features) != required_features) {
1719 throw mxvk::Exception("The selected compute post-processing format is unsupported on this device");
1720 }
1721 }
1722 post_process_images.assign(target_count, std::vector<VkImage>(swapchain_images.size(), VK_NULL_HANDLE));
1723 post_process_memories.assign(target_count, std::vector<VkDeviceMemory>(swapchain_images.size(), VK_NULL_HANDLE));
1724 post_process_views.assign(target_count, std::vector<VkImageView>(swapchain_images.size(), VK_NULL_HANDLE));
1725 post_process_initialized.assign(target_count, std::vector<bool>(swapchain_images.size(), false));
1726 VkPhysicalDeviceMemoryProperties memory_properties{};
1727 vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties);
1728 try {
1729 for (size_t target = 0; target < target_count; ++target) {
1730 for (size_t i = 0; i < swapchain_images.size(); ++i) {
1731 VkImageCreateInfo image_info{};
1732 image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1733 image_info.imageType = VK_IMAGE_TYPE_2D;
1734 image_info.extent = {render_extent.width, render_extent.height, 1};
1735 image_info.mipLevels = 1;
1736 image_info.arrayLayers = 1;
1737 image_info.format = target == 0U ? swapchain_format : postProcessIntermediateFormat();
1738 image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
1739 image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1740 image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
1741 if (target != 0U && has_compute) {
1742 image_info.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
1743 }
1744 image_info.samples = VK_SAMPLE_COUNT_1_BIT;
1745 image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1746 if (vkCreateImage(device, &image_info, nullptr, &post_process_images[target][i]) != VK_SUCCESS) {
1747 throw mxvk::Exception("Failed to create post-process image");
1748 }
1749 VkMemoryRequirements requirements{};
1750 vkGetImageMemoryRequirements(device, post_process_images[target][i], &requirements);
1751 uint32_t memory_type = UINT32_MAX;
1752 for (uint32_t type = 0; type < memory_properties.memoryTypeCount; ++type) {
1753 if ((requirements.memoryTypeBits & (1U << type)) != 0U && (memory_properties.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0U) {
1754 memory_type = type;
1755 break;
1756 }
1757 }
1758 if (memory_type == UINT32_MAX) {
1759 throw mxvk::Exception("Failed to find post-process image memory type");
1760 }
1761 VkMemoryAllocateInfo allocation{};
1762 allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1763 allocation.allocationSize = requirements.size;
1764 allocation.memoryTypeIndex = memory_type;
1765 if (vkAllocateMemory(device, &allocation, nullptr, &post_process_memories[target][i]) != VK_SUCCESS || vkBindImageMemory(device, post_process_images[target][i], post_process_memories[target][i], 0) != VK_SUCCESS) {
1766 throw mxvk::Exception("Failed to allocate post-process image memory");
1767 }
1768 VkImageViewCreateInfo view_info{};
1769 view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1770 view_info.image = post_process_images[target][i];
1771 view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
1772 view_info.format = target == 0U ? swapchain_format : postProcessIntermediateFormat();
1773 view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
1774 view_info.subresourceRange.levelCount = 1;
1775 view_info.subresourceRange.layerCount = 1;
1776 if (vkCreateImageView(device, &view_info, nullptr, &post_process_views[target][i]) != VK_SUCCESS) {
1777 throw mxvk::Exception("Failed to create post-process image view");
1778 }
1779 }
1780 }
1781 } catch (...) {
1782 destroyPostProcessTargets();
1783 throw;
1784 }
1785 }
1786
1788 const auto sync_ready = [this]() { return std::ranges::all_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) && render_finished.size() == swapchain_images.size() && std::ranges::all_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) && std::ranges::all_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; }); };
1789
1790 if (device == VK_NULL_HANDLE) {
1791 return false;
1792 }
1793
1794 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE || command_buffers.empty() || !sync_ready() || image_fences.size() != swapchain_images.size()) {
1795 createDevice();
1796 }
1797
1798 return (renderTargetsReady() && command_pool != VK_NULL_HANDLE && !command_buffers.empty() && sync_ready() && image_fences.size() == swapchain_images.size());
1799 }
1800
1801 bool VK_Window::renderTargetsReady() const noexcept {
1802 if (headless()) {
1803 return !swapchain_images.empty() && swapchain_images.size() == swapchain_image_views.size() && swapchain_images.size() == headless_image_memories.size();
1804 }
1805 return swapchain != VK_NULL_HANDLE;
1806 }
1807
1809 std::cout << "mxvk: entering pickDevice\n";
1810 if (instance == VK_NULL_HANDLE || (!headless() && surface == VK_NULL_HANDLE)) {
1811 std::cout << "vk: cannot pick device because required instance state is missing\n";
1812 return;
1813 }
1814
1815 uint32_t device_count = 0;
1816 std::cout << "vk: enumerating physical devices\n";
1817 vkEnumeratePhysicalDevices(instance, &device_count, nullptr);
1818 if (device_count == 0U) {
1819 std::cout << "vk: no physical devices found\n";
1820 return;
1821 }
1822 std::cout << std::format("vk: found {} physical device(s)\n", device_count);
1823
1824 std::vector<VkPhysicalDevice> devices(device_count);
1825 vkEnumeratePhysicalDevices(instance, &device_count, devices.data());
1826
1827 for (const VkPhysicalDevice candidate : devices) {
1828 std::cout << "vk: evaluating candidate physical device\n";
1829 uint32_t queue_family_count = 0;
1830 vkGetPhysicalDeviceQueueFamilyProperties(candidate, &queue_family_count, nullptr);
1831 std::vector<VkQueueFamilyProperties> queue_families(queue_family_count);
1832 vkGetPhysicalDeviceQueueFamilyProperties(candidate, &queue_family_count, queue_families.data());
1833
1834 uint32_t candidate_graphics = invalid_queue_index;
1835 uint32_t candidate_present = invalid_queue_index;
1836
1837 for (uint32_t i = 0; i < queue_family_count; ++i) {
1838 if ((queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U) {
1839 candidate_graphics = i;
1840 }
1841
1842 if (!headless()) {
1843 VkBool32 present_support = VK_FALSE;
1844 vkGetPhysicalDeviceSurfaceSupportKHR(candidate, i, surface, &present_support);
1845 if (present_support == VK_TRUE) {
1846 candidate_present = i;
1847 }
1848 }
1849 }
1850
1851 if (headless()) {
1852 candidate_present = candidate_graphics;
1853 }
1854
1855 if (candidate_graphics == invalid_queue_index || candidate_present == invalid_queue_index) {
1856 std::cout << "vk: candidate rejected due to missing graphics/present queue support\n";
1857 continue;
1858 }
1859
1860 if (!headless()) {
1861 const SwapchainSupport swapchain_support = querySwapchainSupport(candidate, surface);
1862 if (swapchain_support.formats.empty() || swapchain_support.present_modes.empty()) {
1863 std::cout << "vk: candidate rejected due to incomplete swapchain support\n";
1864 continue;
1865 }
1866 }
1867
1868 VkPhysicalDeviceProperties properties{};
1869 vkGetPhysicalDeviceProperties(candidate, &properties);
1870
1871 const char *device_type = "unknown";
1872 switch (properties.deviceType) {
1873 case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
1874 device_type = "integrated";
1875 break;
1876 case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
1877 device_type = "discrete";
1878 break;
1879 case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
1880 device_type = "virtual";
1881 break;
1882 case VK_PHYSICAL_DEVICE_TYPE_CPU:
1883 device_type = "cpu";
1884 break;
1885 default:
1886 break;
1887 }
1888
1889 physical_device = candidate;
1890 graphics_queue_family = candidate_graphics;
1891 present_queue_family = candidate_present;
1892 std::cout << std::format("vk: selected GPU='{}' type={} vendor=0x{:04x} device=0x{:04x}\n", properties.deviceName, device_type, properties.vendorID, properties.deviceID);
1893 return;
1894 }
1895
1896 std::cout << "vk: no suitable physical device selected\n";
1897 }
1899 std::cout << "mxvk: entering createLogicalDevice\n";
1900 if (physical_device == VK_NULL_HANDLE) {
1901 std::cout << "vk: cannot create logical device without a selected physical device\n";
1902 return;
1903 }
1904
1905 std::vector<uint32_t> queue_families{};
1906 queue_families.push_back(graphics_queue_family);
1908 queue_families.push_back(present_queue_family);
1909 }
1910
1911 constexpr float queue_priority = 1.0f;
1912 std::vector<VkDeviceQueueCreateInfo> queue_create_infos{};
1913 queue_create_infos.reserve(queue_families.size());
1914 for (const uint32_t family : queue_families) {
1915 VkDeviceQueueCreateInfo queue_create_info{};
1916 queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
1917 queue_create_info.queueFamilyIndex = family;
1918 queue_create_info.queueCount = 1;
1919 queue_create_info.pQueuePriorities = &queue_priority;
1920 queue_create_infos.push_back(queue_create_info);
1921 }
1922 uint32_t device_extension_count = 0;
1923 vkEnumerateDeviceExtensionProperties(physical_device, nullptr, &device_extension_count, nullptr);
1924 std::vector<VkExtensionProperties> device_extensions(device_extension_count);
1925 if (device_extension_count > 0U) {
1926 vkEnumerateDeviceExtensionProperties(physical_device, nullptr, &device_extension_count, device_extensions.data());
1927 }
1928
1929 [[maybe_unused]] const auto has_device_extension = [&device_extensions](const char *extension_name) { return std::ranges::any_of(device_extensions, [extension_name](const VkExtensionProperties &ext) { return std::strcmp(ext.extensionName, extension_name) == 0; }); };
1930
1931 std::vector<const char *> required_device_extensions{};
1932 if (!headless()) {
1933 required_device_extensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
1934 }
1935#if defined(MXVK_CUDA) && defined(__linux__)
1936 if (has_device_extension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME)) {
1937 if (has_device_extension(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME)) {
1938 required_device_extensions.push_back(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME);
1939 }
1940 required_device_extensions.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
1941 std::cout << "vk: enabling external memory FD support for CUDA interop\n";
1942 } else {
1943 std::cout << "vk: external memory FD support unavailable; CUDA texture interop will fall back\n";
1944 }
1945#endif
1946
1947#if defined(MXVK_USE_MOLTENVK)
1948 const bool has_portability_subset = std::ranges::any_of(device_extensions, [](const VkExtensionProperties &ext) { return std::strcmp(ext.extensionName, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME) == 0; });
1949
1950 if (has_portability_subset) {
1951 required_device_extensions.push_back(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME);
1952 }
1953#endif
1954 VkPhysicalDeviceFeatures2 features2{};
1955 features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
1956 VkPhysicalDeviceVulkan13Features supported_vulkan13_features{};
1957 supported_vulkan13_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES;
1958 features2.pNext = &supported_vulkan13_features;
1959 vkGetPhysicalDeviceFeatures2(physical_device, &features2);
1960
1961 std::cout << std::format("vk: feature support - synchronization2={}, dynamicRendering={}, shaderFloat64={}, fillModeNonSolid={}, samplerAnisotropy={}\n", supported_vulkan13_features.synchronization2 == VK_TRUE ? "true" : "false", supported_vulkan13_features.dynamicRendering == VK_TRUE ? "true" : "false", features2.features.shaderFloat64 == VK_TRUE ? "true" : "false", features2.features.fillModeNonSolid == VK_TRUE ? "true" : "false", features2.features.samplerAnisotropy == VK_TRUE ? "true" : "false");
1962
1963 if (supported_vulkan13_features.synchronization2 != VK_TRUE) {
1964 std::cout << "vk: synchronization2 is unsupported on selected physical device\n";
1965 return;
1966 }
1967 if (supported_vulkan13_features.dynamicRendering != VK_TRUE) {
1968 std::cout << "vk: dynamic rendering is unsupported on selected physical device\n";
1969 return;
1970 }
1971 VkPhysicalDeviceVulkan13Features vulkan13_features{};
1972 vulkan13_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES;
1973 vulkan13_features.synchronization2 = VK_TRUE;
1974 vulkan13_features.dynamicRendering = VK_TRUE;
1975
1976 VkPhysicalDeviceFeatures2 enabled_features2{};
1977 enabled_features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
1978 enabled_features2.features.shaderFloat64 = features2.features.shaderFloat64;
1979 enabled_features2.features.fillModeNonSolid = features2.features.fillModeNonSolid;
1980 enabled_features2.features.samplerAnisotropy = features2.features.samplerAnisotropy;
1981
1982 enabled_features2.pNext = &vulkan13_features;
1983
1984 VkDeviceCreateInfo create_info{};
1985 create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
1986 create_info.pNext = &enabled_features2;
1987 create_info.queueCreateInfoCount = static_cast<uint32_t>(queue_create_infos.size());
1988 create_info.pQueueCreateInfos = queue_create_infos.data();
1989 create_info.enabledExtensionCount = static_cast<uint32_t>(required_device_extensions.size());
1990 create_info.ppEnabledExtensionNames = required_device_extensions.data();
1991 create_info.enabledLayerCount = 0;
1992 create_info.ppEnabledLayerNames = nullptr;
1993 create_info.pEnabledFeatures = nullptr;
1994
1995 std::cout << "vk: creating logical device\n";
1996 const VkResult create_device_result = vkCreateDevice(physical_device, &create_info, nullptr, &device);
1997 if (create_device_result != VK_SUCCESS) {
1998 std::cerr << std::format("vk: logical device creation failed (VkResult={})", static_cast<int>(create_device_result));
1999 if (!required_device_extensions.empty()) {
2000 std::cerr << "; requested extensions:";
2001 for (const char *extension_name : required_device_extensions) {
2002 std::cerr << ' ' << extension_name;
2003 }
2004 }
2005 std::cerr << '\n';
2006 device = VK_NULL_HANDLE;
2007 return;
2008 }
2009
2010 std::cout << "vk: loading device-level function pointers via volk\n";
2011 volkLoadDevice(device);
2012 if (vkQueueSubmit2 == nullptr || vkCmdBeginRendering == nullptr || vkCmdEndRendering == nullptr || vkCmdPipelineBarrier2 == nullptr) {
2013 std::cout << "vk: required dynamic rendering/synchronization function pointers are unavailable\n";
2014 vkDestroyDevice(device, nullptr);
2015 device = VK_NULL_HANDLE;
2016 return;
2017 }
2018 std::cout << (headless() ? "vk: retrieving graphics queue\n" : "vk: retrieving graphics and present queues\n");
2019 vkGetDeviceQueue(device, graphics_queue_family, 0, &graphics_queue);
2020 if (headless()) {
2022 } else {
2023 vkGetDeviceQueue(device, present_queue_family, 0, &present_queue);
2024 }
2025 std::cout << "mxvk: createLogicalDevice complete\n";
2026 }
2027
2029 std::cout << "mxvk: entering createDevice\n";
2030 if (device == VK_NULL_HANDLE) {
2031 std::cout << "vk: skipping createDevice because logical device is null\n";
2032 return;
2033 }
2034
2035 const bool sync_ready = std::ranges::all_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) && render_finished.size() == swapchain_images.size() && std::ranges::all_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) && std::ranges::all_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; });
2036
2037 if (renderTargetsReady() && command_pool != VK_NULL_HANDLE && !command_buffers.empty() && sync_ready && image_fences.size() == swapchain_images.size()) {
2038 std::cout << "vk: createDevice skipped because render resources are already initialized\n";
2039 return;
2040 }
2041
2042 if (headless()) {
2043 std::cout << "vk: creating surface-free color targets\n";
2044 if (!createHeadlessTargets()) {
2045 std::cout << "vk: createHeadlessTargets failed\n";
2046 return;
2047 }
2048 } else {
2049 std::cout << "vk: creating swapchain\n";
2050 if (!createSwapchain(VK_NULL_HANDLE)) {
2051 std::cout << "vk: createSwapchain failed\n";
2052 return;
2053 }
2054 }
2055
2056 std::cout << "vk: creating render resources\n";
2057 if (!createRenderResources()) {
2058 std::cout << "vk: createRenderResources failed\n";
2059 return;
2060 }
2061
2062 std::cout << "vk: creating synchronization objects\n";
2063 if (!createSyncObjects()) {
2064 std::cout << "vk: createSyncObjects failed\n";
2065 return;
2066 }
2067
2068 std::cout << "mxvk: createDevice complete\n";
2069 }
2070
2071 bool VK_Window::createSwapchain(VkSwapchainKHR old_swapchain) {
2072 std::cout << "vk: entering createSwapchain\n";
2073 SwapchainSupport support = querySwapchainSupport(physical_device, surface);
2074 if (support.formats.empty() || support.present_modes.empty()) {
2075 std::cout << "vk: cannot create swapchain because support is incomplete\n";
2076 return false;
2077 }
2078
2079 const VkSurfaceFormatKHR surface_format = chooseSurfaceFormat(support.formats);
2080 const VkPresentModeKHR present_mode = choosePresentMode(support.present_modes);
2081 auto clampedWindowPixelExtent = [this, &support]() -> std::optional<VkExtent2D> {
2082 if (window == nullptr) {
2083 return std::nullopt;
2084 }
2085 int pixel_w = 0;
2086 int pixel_h = 0;
2087 SDL_GetWindowSizeInPixels(window.get(), &pixel_w, &pixel_h);
2088 if (pixel_w <= 0 || pixel_h <= 0) {
2089 return std::nullopt;
2090 }
2091 return VkExtent2D{
2092 .width = std::clamp(static_cast<uint32_t>(pixel_w), support.capabilities.minImageExtent.width, support.capabilities.maxImageExtent.width),
2093 .height = std::clamp(static_cast<uint32_t>(pixel_h), support.capabilities.minImageExtent.height, support.capabilities.maxImageExtent.height),
2094 };
2095 };
2096 auto extentMatchesWindowPixels = [&clampedWindowPixelExtent](const VkExtent2D extent) {
2097 const std::optional<VkExtent2D> target_extent = clampedWindowPixelExtent();
2098 return !target_extent.has_value() || (extent.width == target_extent->width && extent.height == target_extent->height);
2099 };
2100
2101 VkExtent2D extent = chooseExtent(support.capabilities, window.get());
2102 for (int attempt = 0; attempt < 50 && !extentMatchesWindowPixels(extent); ++attempt) {
2103 SDL_PumpEvents();
2104 SDL_Delay(10);
2105 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &support.capabilities);
2106 extent = chooseExtent(support.capabilities, window.get());
2107 }
2108 if (!extentMatchesWindowPixels(extent)) {
2109 const std::optional<VkExtent2D> target_extent = clampedWindowPixelExtent();
2110 if (target_extent.has_value()) {
2111 std::cout << std::format("vk: delaying swapchain creation until surface extent catches up to window pixels (surface={}x{}, window={}x{})\n", extent.width, extent.height, target_extent->width, target_extent->height);
2112 }
2113 return false;
2114 }
2115
2116 uint32_t image_count = support.capabilities.minImageCount + 1;
2117 if (support.capabilities.maxImageCount > 0U && image_count > support.capabilities.maxImageCount) {
2118 image_count = support.capabilities.maxImageCount;
2119 }
2120
2121 VkSwapchainCreateInfoKHR create_info{};
2122 create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
2123 create_info.surface = surface;
2124 create_info.minImageCount = image_count;
2125 create_info.imageFormat = surface_format.format;
2126 create_info.imageColorSpace = surface_format.colorSpace;
2127 create_info.imageExtent = extent;
2128 create_info.imageArrayLayers = 1;
2129 create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
2130 swapchain_supports_transfer_src = (support.capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0U;
2132 create_info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
2133 } else {
2134 std::cerr << "mxvk: swapchain does not support transfer-source usage; saveSnapshot is unavailable\n";
2135 }
2136
2137 const uint32_t queue_family_indices[] = {graphics_queue_family, present_queue_family};
2139 create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
2140 create_info.queueFamilyIndexCount = 2;
2141 create_info.pQueueFamilyIndices = queue_family_indices;
2142 } else {
2143 create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
2144 create_info.queueFamilyIndexCount = 0;
2145 create_info.pQueueFamilyIndices = nullptr;
2146 }
2147
2148 create_info.preTransform = support.capabilities.currentTransform;
2149 create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
2150 create_info.presentMode = present_mode;
2151 create_info.clipped = VK_TRUE;
2152 create_info.oldSwapchain = old_swapchain;
2153
2154 std::cout << "vk: creating swapchain object\n";
2155 if (vkCreateSwapchainKHR(device, &create_info, nullptr, &swapchain) != VK_SUCCESS) {
2156 swapchain = VK_NULL_HANDLE;
2157 return false;
2158 }
2159
2160 std::cout << "vk: querying swapchain images\n";
2161 vkGetSwapchainImagesKHR(device, swapchain, &image_count, nullptr);
2162 swapchain_images.resize(image_count);
2163 vkGetSwapchainImagesKHR(device, swapchain, &image_count, swapchain_images.data());
2164 swapchain_image_initialized.assign(swapchain_images.size(), false);
2165
2166 swapchain_format = surface_format.format;
2167 swapchain_extent = extent;
2168 last_presented_image_index = invalid_queue_index;
2169
2171 for (size_t i = 0; i < swapchain_images.size(); ++i) {
2172 VkImageViewCreateInfo view_info{};
2173 view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
2174 view_info.image = swapchain_images[i];
2175 view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
2176 view_info.format = swapchain_format;
2177 view_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
2178 view_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
2179 view_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
2180 view_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
2181 view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2182 view_info.subresourceRange.baseMipLevel = 0;
2183 view_info.subresourceRange.levelCount = 1;
2184 view_info.subresourceRange.baseArrayLayer = 0;
2185 view_info.subresourceRange.layerCount = 1;
2186
2187 std::cout << std::format("vk: creating image view for swapchain image {}\n", i);
2188 if (vkCreateImageView(device, &view_info, nullptr, &swapchain_image_views[i]) != VK_SUCCESS) {
2189 for (VkImageView image_view : swapchain_image_views) {
2190 if (image_view != VK_NULL_HANDLE) {
2191 vkDestroyImageView(device, image_view, nullptr);
2192 }
2193 }
2194 swapchain_image_views.clear();
2195 swapchain_images.clear();
2197 last_presented_image_index = invalid_queue_index;
2198 vkDestroySwapchainKHR(device, swapchain, nullptr);
2199 swapchain = VK_NULL_HANDLE;
2200 swapchain_format = VK_FORMAT_UNDEFINED;
2201 swapchain_extent = {};
2203 return false;
2204 }
2205 }
2206
2207 std::cout << "vk: createSwapchain complete\n";
2208 return true;
2209 }
2210
2211 bool VK_Window::createHeadlessTargets() {
2212 if (!headless() || device == VK_NULL_HANDLE || physical_device == VK_NULL_HANDLE || requested_headless_extent.width == 0U || requested_headless_extent.height == 0U) {
2213 return false;
2214 }
2215 if (renderTargetsReady()) {
2216 return true;
2217 }
2218
2219 swapchain_format = VK_FORMAT_R8G8B8A8_UNORM;
2222 constexpr size_t HEADLESS_TARGET_COUNT = max_frames_in_flight;
2223 swapchain_images.assign(HEADLESS_TARGET_COUNT, VK_NULL_HANDLE);
2224 headless_image_memories.assign(HEADLESS_TARGET_COUNT, VK_NULL_HANDLE);
2225 swapchain_image_views.assign(HEADLESS_TARGET_COUNT, VK_NULL_HANDLE);
2226 swapchain_image_initialized.assign(HEADLESS_TARGET_COUNT, false);
2227
2228 VkPhysicalDeviceMemoryProperties memory_properties{};
2229 vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties);
2230 try {
2231 for (size_t index = 0; index < HEADLESS_TARGET_COUNT; ++index) {
2232 VkImageCreateInfo image_info{};
2233 image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
2234 image_info.imageType = VK_IMAGE_TYPE_2D;
2235 image_info.extent = {swapchain_extent.width, swapchain_extent.height, 1};
2236 image_info.mipLevels = 1;
2237 image_info.arrayLayers = 1;
2238 image_info.format = swapchain_format;
2239 image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
2240 image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
2241 image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
2242 image_info.samples = VK_SAMPLE_COUNT_1_BIT;
2243 image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
2244 if (vkCreateImage(device, &image_info, nullptr, &swapchain_images[index]) != VK_SUCCESS) {
2245 throw mxvk::Exception("Failed to create a headless color image");
2246 }
2247
2248 VkMemoryRequirements requirements{};
2249 vkGetImageMemoryRequirements(device, swapchain_images[index], &requirements);
2250 uint32_t memory_type = invalid_queue_index;
2251 for (uint32_t type = 0; type < memory_properties.memoryTypeCount; ++type) {
2252 const bool type_matches = (requirements.memoryTypeBits & (1U << type)) != 0U;
2253 const bool device_local = (memory_properties.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0U;
2254 if (type_matches && device_local) {
2255 memory_type = type;
2256 break;
2257 }
2258 }
2259 if (memory_type == invalid_queue_index) {
2260 throw mxvk::Exception("Failed to find headless image memory");
2261 }
2262
2263 VkMemoryAllocateInfo allocation_info{};
2264 allocation_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
2265 allocation_info.allocationSize = requirements.size;
2266 allocation_info.memoryTypeIndex = memory_type;
2267 if (vkAllocateMemory(device, &allocation_info, nullptr, &headless_image_memories[index]) != VK_SUCCESS) {
2268 throw mxvk::Exception("Failed to allocate headless image memory");
2269 }
2270 if (vkBindImageMemory(device, swapchain_images[index], headless_image_memories[index], 0) != VK_SUCCESS) {
2271 throw mxvk::Exception("Failed to bind headless image memory");
2272 }
2273
2274 VkImageViewCreateInfo view_info{};
2275 view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
2276 view_info.image = swapchain_images[index];
2277 view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
2278 view_info.format = swapchain_format;
2279 view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2280 view_info.subresourceRange.levelCount = 1;
2281 view_info.subresourceRange.layerCount = 1;
2282 if (vkCreateImageView(device, &view_info, nullptr, &swapchain_image_views[index]) != VK_SUCCESS) {
2283 throw mxvk::Exception("Failed to create a headless image view");
2284 }
2285 }
2286 } catch (const mxvk::Exception &ex) {
2287 std::cerr << std::format("mxvk: {}\n", ex.text());
2288 cleanupSwapchain(false);
2289 return false;
2290 }
2291
2293 last_presented_image_index = invalid_queue_index;
2294 std::cout << std::format("vk: created {} surface-free color targets ({}x{}, RGBA8)\n", HEADLESS_TARGET_COUNT, swapchain_extent.width, swapchain_extent.height);
2295 return true;
2296 }
2297
2298 bool VK_Window::createRenderResources() {
2299 std::cout << "vk: entering createRenderResources\n";
2300 const auto cleanup_render_resource_failure = [this]() {
2301 if (command_pool != VK_NULL_HANDLE && !command_buffers.empty()) {
2302 vkFreeCommandBuffers(device, command_pool, static_cast<uint32_t>(command_buffers.size()), command_buffers.data());
2303 command_buffers.clear();
2304 }
2305 if (!depth_image_views.empty()) {
2306 for (VkImageView &view : depth_image_views) {
2307 if (view != VK_NULL_HANDLE) {
2308 vkDestroyImageView(device, view, nullptr);
2309 view = VK_NULL_HANDLE;
2310 }
2311 }
2312 }
2313 if (!depth_images.empty()) {
2314 for (VkImage &image : depth_images) {
2315 if (image != VK_NULL_HANDLE) {
2316 vkDestroyImage(device, image, nullptr);
2317 image = VK_NULL_HANDLE;
2318 }
2319 }
2320 }
2321 if (!depth_image_memories.empty()) {
2322 for (VkDeviceMemory &memory : depth_image_memories) {
2323 if (memory != VK_NULL_HANDLE) {
2324 vkFreeMemory(device, memory, nullptr);
2325 memory = VK_NULL_HANDLE;
2326 }
2327 }
2328 }
2329 depth_image_views.clear();
2330 depth_images.clear();
2331 depth_image_memories.clear();
2333 depth_format = VK_FORMAT_UNDEFINED;
2334 destroyPostProcessTargets();
2335 };
2336
2337 if (command_pool == VK_NULL_HANDLE) {
2338 VkCommandPoolCreateInfo pool_info{};
2339 pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
2340 pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
2341 pool_info.queueFamilyIndex = graphics_queue_family;
2342
2343 std::cout << "vk: creating command pool\n";
2344 if (vkCreateCommandPool(device, &pool_info, nullptr, &command_pool) != VK_SUCCESS) {
2345 return false;
2346 }
2347 }
2348
2349 if (!command_buffers.empty()) {
2350 std::cout << "vk: freeing stale command buffers before reallocation\n";
2351 vkFreeCommandBuffers(device, command_pool, static_cast<uint32_t>(command_buffers.size()), command_buffers.data());
2352 command_buffers.clear();
2353 }
2354
2355 command_buffers.resize(swapchain_images.size());
2356 VkCommandBufferAllocateInfo alloc_info{};
2357 alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
2358 alloc_info.commandPool = command_pool;
2359 alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
2360 alloc_info.commandBufferCount = static_cast<uint32_t>(command_buffers.size());
2361
2362 std::cout << "vk: allocating command buffers\n";
2363 if (vkAllocateCommandBuffers(device, &alloc_info, command_buffers.data()) != VK_SUCCESS) {
2364 command_buffers.clear();
2365 return false;
2366 }
2367
2368 auto findDepthFormat = [&](VkPhysicalDevice gpu) -> VkFormat {
2369 const std::array<VkFormat, 2> candidates = {
2370 VK_FORMAT_D32_SFLOAT,
2371 VK_FORMAT_D16_UNORM,
2372 };
2373
2374 for (const VkFormat format : candidates) {
2375 VkFormatProperties props{};
2376 vkGetPhysicalDeviceFormatProperties(gpu, format, &props);
2377 if ((props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0U) {
2378 return format;
2379 }
2380 }
2381
2382 return VK_FORMAT_UNDEFINED;
2383 };
2384
2385 depth_format = findDepthFormat(physical_device);
2386 if (depth_format == VK_FORMAT_UNDEFINED) {
2387 std::cerr << "mxvk: failed to find a supported depth format\n";
2388 cleanup_render_resource_failure();
2389 return false;
2390 }
2391
2392 depth_images.resize(max_frames_in_flight, VK_NULL_HANDLE);
2393 depth_image_memories.resize(max_frames_in_flight, VK_NULL_HANDLE);
2394 depth_image_views.resize(max_frames_in_flight, VK_NULL_HANDLE);
2395 depth_image_initialized.assign(max_frames_in_flight, false);
2396
2397 const VkExtent2D render_extent = getRenderExtent();
2398 for (size_t i = 0; i < depth_images.size(); ++i) {
2399 std::cout << std::format("vk: creating depth image for frame {}\n", i);
2400 VkImageCreateInfo imageInfo{};
2401 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
2402 imageInfo.imageType = VK_IMAGE_TYPE_2D;
2403 imageInfo.extent.width = render_extent.width;
2404 imageInfo.extent.height = render_extent.height;
2405 imageInfo.extent.depth = 1;
2406 imageInfo.mipLevels = 1;
2407 imageInfo.arrayLayers = 1;
2408 imageInfo.format = depth_format;
2409 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
2410 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
2411 imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
2412 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
2413 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
2414
2415 if (vkCreateImage(device, &imageInfo, nullptr, &depth_images[i]) != VK_SUCCESS) {
2416 std::cerr << std::format("mxvk: failed to create depth image {}\n", i);
2417 cleanup_render_resource_failure();
2418 return false;
2419 }
2420
2421 VkMemoryRequirements memReq{};
2422 vkGetImageMemoryRequirements(device, depth_images[i], &memReq);
2423
2424 VkPhysicalDeviceMemoryProperties memProps{};
2425 vkGetPhysicalDeviceMemoryProperties(physical_device, &memProps);
2426 uint32_t memoryTypeIndex = UINT32_MAX;
2427 for (uint32_t t = 0; t < memProps.memoryTypeCount; ++t) {
2428 if (((memReq.memoryTypeBits & (1u << t)) != 0U) && ((memProps.memoryTypes[t].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0U)) {
2429 memoryTypeIndex = t;
2430 break;
2431 }
2432 }
2433 if (memoryTypeIndex == UINT32_MAX) {
2434 std::cerr << "mxvk: failed to find depth image memory type\n";
2435 cleanup_render_resource_failure();
2436 return false;
2437 }
2438
2439 std::cout << std::format("vk: allocating depth image memory for frame {}\n", i);
2440 VkMemoryAllocateInfo allocInfo{};
2441 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
2442 allocInfo.allocationSize = memReq.size;
2443 allocInfo.memoryTypeIndex = memoryTypeIndex;
2444
2445 if (vkAllocateMemory(device, &allocInfo, nullptr, &depth_image_memories[i]) != VK_SUCCESS) {
2446 std::cerr << std::format("mxvk: failed to allocate depth image memory {}\n", i);
2447 cleanup_render_resource_failure();
2448 return false;
2449 }
2450
2451 if (vkBindImageMemory(device, depth_images[i], depth_image_memories[i], 0) != VK_SUCCESS) {
2452 std::cerr << std::format("mxvk: failed to bind depth image memory {}\n", i);
2453 cleanup_render_resource_failure();
2454 return false;
2455 }
2456
2457 std::cout << std::format("vk: creating depth image view for frame {}\n", i);
2458 VkImageViewCreateInfo viewInfo{};
2459 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
2460 viewInfo.image = depth_images[i];
2461 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
2462 viewInfo.format = depth_format;
2463 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
2464 viewInfo.subresourceRange.baseMipLevel = 0;
2465 viewInfo.subresourceRange.levelCount = 1;
2466 viewInfo.subresourceRange.baseArrayLayer = 0;
2467 viewInfo.subresourceRange.layerCount = 1;
2468
2469 if (vkCreateImageView(device, &viewInfo, nullptr, &depth_image_views[i]) != VK_SUCCESS) {
2470 std::cerr << std::format("mxvk: failed to create depth image view {}\n", i);
2471 cleanup_render_resource_failure();
2472 return false;
2473 }
2474 }
2475
2476 if (!post_process_sprites.empty()) {
2477 try {
2478 createPostProcessTargets();
2479 } catch (const mxvk::Exception &ex) {
2480 std::cerr << std::format("mxvk: {}\n", ex.text());
2481 cleanup_render_resource_failure();
2482 return false;
2483 }
2484 }
2485
2486 std::cout << "vk: createRenderResources complete\n";
2487 return true;
2488 }
2489
2490 bool VK_Window::createSyncObjects() {
2491 std::cout << "vk: entering createSyncObjects\n";
2492 if (swapchain_images.empty()) {
2493 std::cerr << "mxvk: cannot create sync objects without swapchain images\n";
2494 return false;
2495 }
2496
2497 cleanupSyncObjects();
2498
2499 VkSemaphoreCreateInfo semaphore_info{};
2500 semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
2501
2502 VkFenceCreateInfo fence_info{};
2503 fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
2504 fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
2505
2506 render_finished.assign(swapchain_images.size(), VK_NULL_HANDLE);
2507
2508 for (uint32_t frame = 0; frame < max_frames_in_flight; ++frame) {
2509 std::cout << std::format("vk: creating image-available semaphore for frame {}\n", frame);
2510 if (vkCreateSemaphore(device, &semaphore_info, nullptr, &image_available[frame]) != VK_SUCCESS) {
2511 cleanupSyncObjects();
2512 return false;
2513 }
2514
2515 std::cout << std::format("vk: creating in-flight fence for frame {}\n", frame);
2516 if (vkCreateFence(device, &fence_info, nullptr, &in_flight_fences[frame]) != VK_SUCCESS) {
2517 cleanupSyncObjects();
2518 return false;
2519 }
2520 }
2521
2522 for (size_t image_index = 0; image_index < render_finished.size(); ++image_index) {
2523 std::cout << std::format("vk: creating render-finished semaphore for swapchain image {}\n", image_index);
2524 if (vkCreateSemaphore(device, &semaphore_info, nullptr, &render_finished[image_index]) != VK_SUCCESS) {
2525 cleanupSyncObjects();
2526 return false;
2527 }
2528 }
2529
2530 image_fences.assign(swapchain_images.size(), VK_NULL_HANDLE);
2531 current_frame = 0;
2532
2533 std::cout << "vk: createSyncObjects complete\n";
2534 return true;
2535 }
2536
2537 void VK_Window::cleanupSyncObjects() {
2538 if (device == VK_NULL_HANDLE) {
2539 return;
2540 }
2541
2542 const bool has_in_flight_fences = std::ranges::any_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; });
2543 const bool has_render_finished = std::ranges::any_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; });
2544 const bool has_image_available = std::ranges::any_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; });
2545
2546 if (!has_in_flight_fences && !has_render_finished && !has_image_available) {
2547 image_fences.clear();
2548 current_frame = 0;
2549 return;
2550 }
2551
2552 if (has_in_flight_fences) {
2553 std::cout << "vk: destroying in-flight fences\n";
2554 for (VkFence &fence : in_flight_fences) {
2555 if (fence != VK_NULL_HANDLE) {
2556 vkDestroyFence(device, fence, nullptr);
2557 fence = VK_NULL_HANDLE;
2558 }
2559 }
2560 }
2561
2562 if (has_render_finished) {
2563 std::cout << "vk: destroying render-finished semaphores\n";
2564 for (VkSemaphore &semaphore : render_finished) {
2565 if (semaphore != VK_NULL_HANDLE) {
2566 vkDestroySemaphore(device, semaphore, nullptr);
2567 semaphore = VK_NULL_HANDLE;
2568 }
2569 }
2570 }
2571
2572 if (has_image_available) {
2573 std::cout << "vk: destroying image-available semaphores\n";
2574 for (VkSemaphore &semaphore : image_available) {
2575 if (semaphore != VK_NULL_HANDLE) {
2576 vkDestroySemaphore(device, semaphore, nullptr);
2577 semaphore = VK_NULL_HANDLE;
2578 }
2579 }
2580 }
2581
2582 image_fences.clear();
2583 render_finished.clear();
2584 current_frame = 0;
2585 }
2586
2587 void VK_Window::recreateSwapchain() {
2588 if (device == VK_NULL_HANDLE || window == nullptr) {
2589 return;
2590 }
2591 int pixel_w = 0;
2592 int pixel_h = 0;
2593 SDL_GetWindowSizeInPixels(window.get(), &pixel_w, &pixel_h);
2594 VkSurfaceCapabilitiesKHR surface_capabilities{};
2595 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &surface_capabilities);
2596
2597 VkExtent2D new_extent{};
2598
2599 if (surface_capabilities.currentExtent.width != 0xFFFFFFFFU) {
2600 new_extent = surface_capabilities.currentExtent;
2601 } else {
2602 new_extent.width = std::clamp(static_cast<uint32_t>(pixel_w), surface_capabilities.minImageExtent.width, surface_capabilities.maxImageExtent.width);
2603
2604 new_extent.height = std::clamp(static_cast<uint32_t>(pixel_h), surface_capabilities.minImageExtent.height, surface_capabilities.maxImageExtent.height);
2605 }
2606
2607 if (new_extent.width == 0 || new_extent.height == 0) {
2608 return;
2609 }
2610
2611 const uint32_t target_w = std::clamp(static_cast<uint32_t>(pixel_w), surface_capabilities.minImageExtent.width, surface_capabilities.maxImageExtent.width);
2612 const uint32_t target_h = std::clamp(static_cast<uint32_t>(pixel_h), surface_capabilities.minImageExtent.height, surface_capabilities.maxImageExtent.height);
2613
2614 if (new_extent.width != target_w || new_extent.height != target_h) {
2615 return;
2616 }
2617
2618 if (!force_swapchain_recreate && swapchain_extent.width == new_extent.width && swapchain_extent.height == new_extent.height) {
2619 return;
2620 }
2621
2622 if (!force_swapchain_recreate && swapchain_extent.width == new_extent.width && swapchain_extent.height == new_extent.height) {
2623 return;
2624 }
2625
2626 std::cout << std::format("mxvk: recreating swapchain for {}x{} window\n", new_extent.width, new_extent.height);
2627 vkDeviceWaitIdle(device);
2629
2630 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2631 if (sprite) {
2632 sprite->releaseUploadResources();
2633 }
2634 }
2635
2636 sprite_state_dirty = true;
2637 text_state_dirty = true;
2638 destroySpritePipeline();
2639 destroyTextPipeline();
2640 cleanupSyncObjects();
2641 VkSwapchainKHR old_swapchain = swapchain;
2642 cleanupSwapchain(false);
2643 if (!createSwapchain(old_swapchain) || !createRenderResources() || !createSyncObjects()) {
2644 std::cerr << "mxvk: failed to recreate swapchain after resize\n";
2645 if (old_swapchain != VK_NULL_HANDLE) {
2646 retired_swapchains.push_back(old_swapchain);
2647 // vkDestroySwapchainKHR(device, old_swapchain, nullptr);
2648 }
2649 return;
2650 }
2651 if (old_swapchain != VK_NULL_HANDLE) {
2652 // vkDestroySwapchainKHR(device, old_swapchain, nullptr);
2653 retired_swapchains.push_back(old_swapchain);
2654 }
2655
2656 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2657 if (sprite) {
2659 }
2660 }
2661 for (const std::unique_ptr<VK_Sprite3D> &sprite : sprites3d) {
2662 if (sprite) {
2663 sprite->resize(this);
2664 }
2665 }
2666
2669 framebuffer_resized = false;
2670 std::cout << "mxvk: swapchain recreation complete\n";
2671 }
2672
2673 void VK_Window::cleanupSwapchain(bool destroy_swapchain_handle) {
2674 std::cout << "vk: entering cleanupSwapchain\n";
2675 if (device == VK_NULL_HANDLE) {
2676 return;
2677 }
2678
2679 destroyPostProcessTargets();
2680
2681 if (!swapchain_image_views.empty()) {
2682 std::cout << "vk: destroying swapchain image views\n";
2683 for (VkImageView image_view : swapchain_image_views) {
2684 if (image_view != VK_NULL_HANDLE) {
2685 vkDestroyImageView(device, image_view, nullptr);
2686 }
2687 }
2688 }
2689 swapchain_image_views.clear();
2690
2691 if (!headless_image_memories.empty()) {
2692 std::cout << "vk: destroying surface-free color images\n";
2693 for (VkImage image : swapchain_images) {
2694 if (image != VK_NULL_HANDLE) {
2695 vkDestroyImage(device, image, nullptr);
2696 }
2697 }
2698 std::cout << "vk: freeing surface-free color image memory\n";
2699 for (VkDeviceMemory memory : headless_image_memories) {
2700 if (memory != VK_NULL_HANDLE) {
2701 vkFreeMemory(device, memory, nullptr);
2702 }
2703 }
2705 }
2706
2707 if (!depth_image_views.empty()) {
2708 std::cout << "vk: destroying depth image views\n";
2709 for (VkImageView view : depth_image_views) {
2710 if (view != VK_NULL_HANDLE) {
2711 vkDestroyImageView(device, view, nullptr);
2712 }
2713 }
2714 }
2715 depth_image_views.clear();
2716
2717 if (!depth_images.empty()) {
2718 std::cout << "vk: destroying depth images\n";
2719 for (VkImage image : depth_images) {
2720 if (image != VK_NULL_HANDLE) {
2721 vkDestroyImage(device, image, nullptr);
2722 }
2723 }
2724 }
2725 depth_images.clear();
2726
2727 if (!depth_image_memories.empty()) {
2728 std::cout << "vk: freeing depth image memory\n";
2729 for (VkDeviceMemory memory : depth_image_memories) {
2730 if (memory != VK_NULL_HANDLE) {
2731 vkFreeMemory(device, memory, nullptr);
2732 }
2733 }
2734 }
2735 depth_image_memories.clear();
2737
2738 swapchain_images.clear();
2740 last_presented_image_index = invalid_queue_index;
2742 depth_format = VK_FORMAT_UNDEFINED;
2744
2745 // This guard prevents destroying the active swapchain during a live resize
2746 if (destroy_swapchain_handle && swapchain != VK_NULL_HANDLE) {
2747 std::cout << "vk: destroying swapchain\n";
2748 vkDestroySwapchainKHR(device, swapchain, nullptr);
2749 swapchain = VK_NULL_HANDLE;
2750 }
2751 std::cout << "vk: cleanupSwapchain complete\n";
2752 }
2753
2754 void VK_Window::drawFrame() {
2755 if (device == VK_NULL_HANDLE) {
2756 return;
2757 }
2758
2759 int pixel_w = 0;
2760 int pixel_h = 0;
2761 if (!headless()) {
2762 if (window != nullptr) {
2763 SDL_GetWindowSizeInPixels(window.get(), &pixel_w, &pixel_h);
2764 }
2765 if (pixel_w <= 0 || pixel_h <= 0) {
2766 framebuffer_resized = true;
2767 return;
2768 }
2769 }
2770
2771 if (!headless() && swapchain_extent.width != 0 && swapchain_extent.height != 0) {
2772 if (swapchain_extent.width != static_cast<uint32_t>(pixel_w) || swapchain_extent.height != static_cast<uint32_t>(pixel_h)) {
2773 std::cout << std::format("mxvk: requesting swapchain recreation because window pixels changed from {}x{} to {}x{}\n", swapchain_extent.width, swapchain_extent.height, pixel_w, pixel_h);
2774 framebuffer_resized = true;
2776 return;
2777 }
2778 }
2779
2780 if (!ensureRenderResources()) {
2781 std::cout << "mxvk: creating deferred swapchain/render/sync resources\n";
2782 if (!ensureRenderResources()) {
2783 std::cerr << "mxvk: deferred resource creation failed; skipping frame\n";
2784 return;
2785 }
2786 }
2787
2788 const bool sync_ready = std::ranges::all_of(image_available.begin(), image_available.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) && std::ranges::all_of(render_finished.begin(), render_finished.end(), [](VkSemaphore semaphore) { return semaphore != VK_NULL_HANDLE; }) && std::ranges::all_of(in_flight_fences.begin(), in_flight_fences.end(), [](VkFence fence) { return fence != VK_NULL_HANDLE; });
2789 if (!sync_ready) {
2790 return;
2791 }
2792
2793 if (sprite_state_dirty && !sprites.empty() && swapchain_format != VK_FORMAT_UNDEFINED) {
2794 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
2795 if (!sprite) {
2796 continue;
2797 }
2798 VkFormat sprite_color_format = swapchain_format;
2799 const auto post_process_match = std::ranges::find(post_process_sprites, sprite.get());
2800 if (post_process_match != post_process_sprites.end()) {
2801 const size_t effect_index = static_cast<size_t>(std::distance(post_process_sprites.begin(), post_process_match));
2802 if (hdr_render_intermediates_enabled || effect_index + 1U < post_process_sprites.size() || (render_extent_override.width > 0U && render_extent_override.height > 0U)) {
2803 sprite_color_format = postProcessIntermediateFormat();
2804 }
2805 } else if (hdr_render_intermediates_enabled && !post_process_sprites.empty() && !isPostProcessSprite(sprite.get())) {
2806 sprite_color_format = postProcessIntermediateFormat();
2807 }
2808 sprite->setColorAttachmentFormat(sprite_color_format);
2811 sprite->rebuildPipeline();
2812 sprite->rebuildInstancedPipeline();
2813 }
2814 try {
2815 createSpritePipeline();
2816 } catch (const std::exception &ex) {
2817 std::cerr << std::format("mxvk: sprite pipeline build skipped: {}\n", ex.what());
2818 }
2819 sprite_state_dirty = false;
2820 }
2821
2822 if (fps_counter_enabled) {
2823 updateFpsCounter();
2824 }
2825
2826 if (text_state_dirty && (text_renderer || preview_text_renderer) && swapchain_format != VK_FORMAT_UNDEFINED) {
2827 if (text_renderer) {
2828 text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
2829 }
2831 preview_text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
2832 }
2833 try {
2834 createTextPipeline();
2835 } catch (const std::exception &ex) {
2836 std::cerr << std::format("mxvk: text pipeline build skipped: {}\n", ex.what());
2837 }
2838 text_state_dirty = false;
2839 }
2840
2841 VkFence &frame_fence = in_flight_fences[current_frame];
2842 VkSemaphore &acquire_semaphore = image_available[current_frame];
2843 const size_t depth_slot = static_cast<size_t>(current_frame);
2844
2845 const VkResult wait_result = vkWaitForFences(device, 1, &frame_fence, VK_TRUE, UINT64_MAX);
2846 if (wait_result == VK_ERROR_DEVICE_LOST) {
2847 std::cerr << "mxvk: device lost while waiting for frame fence; stopping render loop\n";
2848 active = false;
2849 return;
2850 }
2851 if (wait_result != VK_SUCCESS) {
2852 std::cerr << std::format("mxvk: Failed waiting for frame fence (VkResult={})\n", static_cast<int>(wait_result));
2853 framebuffer_resized = true;
2854 return;
2855 }
2856
2858
2861 }
2862
2863 uint32_t image_index = 0;
2864 VkResult acquire_result = VK_SUCCESS;
2865 if (headless()) {
2866 image_index = next_headless_image % static_cast<uint32_t>(swapchain_images.size());
2867 next_headless_image = (next_headless_image + 1U) % static_cast<uint32_t>(swapchain_images.size());
2868 } else {
2869 const uint64_t acquire_timeout_ns = 100000000ULL; // 100 ms avoids UINT64_MAX forward-progress VUIDs.
2870 acquire_result = vkAcquireNextImageKHR(device, swapchain, acquire_timeout_ns, acquire_semaphore, VK_NULL_HANDLE, &image_index);
2871 }
2872
2873 static VkResult last_acquire_error = VK_SUCCESS;
2874 static uint32_t repeated_acquire_errors = 0;
2875
2876 if (acquire_result == VK_ERROR_OUT_OF_DATE_KHR) {
2877 last_acquire_error = VK_SUCCESS;
2878 repeated_acquire_errors = 0;
2879 std::cout << "mxvk: requesting swapchain recreation because acquire returned VK_ERROR_OUT_OF_DATE_KHR\n";
2881 framebuffer_resized = true;
2882 return;
2883 }
2884
2885 if (acquire_result == VK_ERROR_SURFACE_LOST_KHR) {
2886 if (last_acquire_error != acquire_result) {
2887 std::cerr << "mxvk: swapchain surface lost during acquire; requesting swapchain recreation\n";
2888 last_acquire_error = acquire_result;
2889 repeated_acquire_errors = 0;
2890 }
2891 std::cout << "mxvk: requesting swapchain recreation because acquire returned VK_ERROR_SURFACE_LOST_KHR\n";
2893 framebuffer_resized = true;
2894 return;
2895 }
2896
2897 if (acquire_result == VK_TIMEOUT || acquire_result == VK_NOT_READY) {
2898 // Non-fatal: no image available this frame.
2899 return;
2900 }
2901
2902 if (acquire_result != VK_SUCCESS && acquire_result != VK_SUBOPTIMAL_KHR) {
2903 if (last_acquire_error == acquire_result) {
2904 ++repeated_acquire_errors;
2905 if ((repeated_acquire_errors % 120U) == 0U) {
2906 std::cerr << std::format("mxvk: repeated swapchain acquire failures continue (VkResult={})\n", static_cast<int>(acquire_result));
2907 }
2908 } else {
2909 last_acquire_error = acquire_result;
2910 repeated_acquire_errors = 0;
2911 std::cerr << std::format("mxvk: Failed to acquire swapchain image (VkResult={})\n", static_cast<int>(acquire_result));
2912 }
2913
2914 if (acquire_result == VK_ERROR_DEVICE_LOST) {
2915 std::cerr << "mxvk: device lost; stopping render loop\n";
2916 active = false;
2917 }
2918 return;
2919 }
2920
2921 last_acquire_error = VK_SUCCESS;
2922 repeated_acquire_errors = 0;
2923
2924 if (image_index >= command_buffers.size() || image_index >= swapchain_images.size() || image_index >= swapchain_image_views.size()) {
2925 return;
2926 }
2927
2928 if (image_index >= image_fences.size()) {
2929 std::cerr << "mxvk: acquired image index exceeds tracked in-flight image count\n";
2930 return;
2931 }
2932
2933 if (image_index >= render_finished.size() || render_finished[image_index] == VK_NULL_HANDLE) {
2934 std::cerr << "mxvk: acquired image index exceeds render-finished semaphore count\n";
2935 return;
2936 }
2937
2938 VkSemaphore &present_semaphore = render_finished[image_index];
2939
2940 if (image_fences[image_index] != VK_NULL_HANDLE && image_fences[image_index] != frame_fence) {
2941 const VkResult image_wait_result = vkWaitForFences(device, 1, &image_fences[image_index], VK_TRUE, UINT64_MAX);
2942 if (image_wait_result == VK_ERROR_DEVICE_LOST) {
2943 std::cerr << "mxvk: device lost while waiting on acquired image fence; stopping render loop\n";
2944 active = false;
2945 return;
2946 }
2947 if (image_wait_result != VK_SUCCESS) {
2948 std::cerr << std::format("mxvk: Failed waiting for acquired image fence (VkResult={})\n", static_cast<int>(image_wait_result));
2949 framebuffer_resized = true;
2950 return;
2951 }
2952 }
2953
2954 const VkCommandBuffer cmd = command_buffers[image_index];
2955 vkResetCommandBuffer(cmd, 0);
2956
2957 VkCommandBufferBeginInfo begin_info{};
2958 begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
2959 if (vkBeginCommandBuffer(cmd, &begin_info) != VK_SUCCESS) {
2960 std::cerr << "mxvk: Failed to begin command buffer\n";
2961 framebuffer_resized = true;
2962 return;
2963 }
2964
2965 VkClearValue clear_value{};
2966 clear_value.color = clear_color;
2967 const bool detached_render = render_extent_override.width > 0U && render_extent_override.height > 0U;
2968 const VkExtent2D render_extent = getRenderExtent();
2969 const bool use_post_process = post_process_enabled && !post_process_sprites.empty() && !post_process_images.empty() && !post_process_views.empty() && !post_process_initialized.empty() && image_index < post_process_images.front().size() && image_index < post_process_views.front().size() && image_index < post_process_initialized.front().size();
2970 const bool use_offscreen_target = use_post_process || detached_render;
2971 const bool consume_post_process = use_post_process && post_process_texture_consumer_enabled;
2972 const size_t scene_target = hdr_render_intermediates_enabled ? 1U : 0U;
2973 VkImage rgba16_readback_image = VK_NULL_HANDLE;
2974
2975 VkImageMemoryBarrier2 to_color_barrier{};
2976 to_color_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
2977 to_color_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
2978 to_color_barrier.srcAccessMask = VK_ACCESS_2_NONE;
2979 to_color_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
2980 to_color_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
2981 to_color_barrier.oldLayout = swapchain_image_initialized[image_index] ? (headless() ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) : VK_IMAGE_LAYOUT_UNDEFINED;
2982 to_color_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
2983 to_color_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2984 to_color_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2985 to_color_barrier.image = swapchain_images[image_index];
2986 to_color_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2987 to_color_barrier.subresourceRange.baseMipLevel = 0;
2988 to_color_barrier.subresourceRange.levelCount = 1;
2989 to_color_barrier.subresourceRange.baseArrayLayer = 0;
2990 to_color_barrier.subresourceRange.layerCount = 1;
2991
2992 VkDependencyInfo pre_render_dependency{};
2993 pre_render_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
2994 pre_render_dependency.imageMemoryBarrierCount = 1;
2995 pre_render_dependency.pImageMemoryBarriers = &to_color_barrier;
2996 vkCmdPipelineBarrier2(cmd, &pre_render_dependency);
2997
2998 if (use_offscreen_target) {
2999 VkImageMemoryBarrier2 post_target_barrier{};
3000 post_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3001 post_target_barrier.srcStageMask = post_process_initialized[scene_target][image_index] ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_NONE;
3002 post_target_barrier.srcAccessMask = post_process_initialized[scene_target][image_index] ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_NONE;
3003 post_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3004 post_target_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3005 post_target_barrier.oldLayout = post_process_initialized[scene_target][image_index] ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
3006 post_target_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3007 post_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3008 post_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3009 post_target_barrier.image = post_process_images[scene_target][image_index];
3010 post_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3011 post_target_barrier.subresourceRange.levelCount = 1;
3012 post_target_barrier.subresourceRange.layerCount = 1;
3013 VkDependencyInfo post_target_dependency{};
3014 post_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3015 post_target_dependency.imageMemoryBarrierCount = 1;
3016 post_target_dependency.pImageMemoryBarriers = &post_target_barrier;
3017 vkCmdPipelineBarrier2(cmd, &post_target_dependency);
3018 }
3019
3020 VkImageMemoryBarrier2 to_depth_barrier{};
3021 to_depth_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3022 to_depth_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
3023 to_depth_barrier.srcAccessMask = VK_ACCESS_2_NONE;
3024 to_depth_barrier.dstStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT;
3025 to_depth_barrier.dstAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
3026 to_depth_barrier.oldLayout = (depth_slot < depth_image_initialized.size() && depth_image_initialized[depth_slot]) ? VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
3027 to_depth_barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
3028 to_depth_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3029 to_depth_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3030 if (depth_slot < depth_images.size()) {
3031 to_depth_barrier.image = depth_images[depth_slot];
3032 }
3033 to_depth_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
3034 to_depth_barrier.subresourceRange.baseMipLevel = 0;
3035 to_depth_barrier.subresourceRange.levelCount = 1;
3036 to_depth_barrier.subresourceRange.baseArrayLayer = 0;
3037 to_depth_barrier.subresourceRange.layerCount = 1;
3038
3039 if (to_depth_barrier.image != VK_NULL_HANDLE) {
3040 VkDependencyInfo pre_depth_dependency{};
3041 pre_depth_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3042 pre_depth_dependency.imageMemoryBarrierCount = 1;
3043 pre_depth_dependency.pImageMemoryBarriers = &to_depth_barrier;
3044 vkCmdPipelineBarrier2(cmd, &pre_depth_dependency);
3045 }
3046
3047 onPrepareFrameRendering(cmd, image_index);
3048 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
3049 if (sprite) {
3050 sprite->prepareForRendering(cmd);
3051 }
3052 }
3053
3054 VkRenderingAttachmentInfo color_attachment{};
3055 color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3056 color_attachment.imageView = use_offscreen_target ? post_process_views[scene_target][image_index] : swapchain_image_views[image_index];
3057 color_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3058 color_attachment.resolveMode = VK_RESOLVE_MODE_NONE;
3059 color_attachment.resolveImageView = VK_NULL_HANDLE;
3060 color_attachment.resolveImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
3061 color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
3062 color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3063 color_attachment.clearValue = clear_value;
3064
3065 VkClearValue depth_clear_value{};
3066 depth_clear_value.depthStencil = {1.0f, 0};
3067
3068 VkRenderingAttachmentInfo depth_attachment{};
3069 depth_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3070 if (depth_slot < depth_image_views.size()) {
3071 depth_attachment.imageView = depth_image_views[depth_slot];
3072 }
3073 depth_attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
3074 depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
3075 depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
3076 depth_attachment.clearValue = depth_clear_value;
3077
3078 VkRenderingAttachmentInfo stencil_attachment{};
3079 stencil_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3080 onConfigureDepthStencilAttachments(depth_attachment, stencil_attachment, image_index);
3081
3082 VkRenderingInfo rendering_info{};
3083 rendering_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3084 rendering_info.renderArea.offset = {0, 0};
3085 rendering_info.renderArea.extent = use_offscreen_target ? render_extent : swapchain_extent;
3086 rendering_info.layerCount = 1;
3087 rendering_info.viewMask = 0;
3088 rendering_info.colorAttachmentCount = 1;
3089 rendering_info.pColorAttachments = &color_attachment;
3090 rendering_info.pDepthAttachment = (depth_attachment.imageView != VK_NULL_HANDLE) ? &depth_attachment : nullptr;
3091 rendering_info.pStencilAttachment = (stencil_attachment.imageView != VK_NULL_HANDLE) ? &stencil_attachment : nullptr;
3092
3093 vkCmdBeginRendering(cmd, &rendering_info);
3094
3095 VkViewport viewport{};
3096 viewport.x = 0.0F;
3097 viewport.y = 0.0F;
3098 viewport.width = static_cast<float>(rendering_info.renderArea.extent.width);
3099 viewport.height = static_cast<float>(rendering_info.renderArea.extent.height);
3100 viewport.minDepth = 0.0F;
3101 viewport.maxDepth = 1.0F;
3102 vkCmdSetViewport(cmd, 0, 1, &viewport);
3103
3104 VkRect2D scissor{};
3105 scissor.offset = {0, 0};
3106 scissor.extent = rendering_info.renderArea.extent;
3107 vkCmdSetScissor(cmd, 0, 1, &scissor);
3108
3109 onRecordCustomRendering(cmd, image_index);
3110
3111 // Draw 2D overlays after custom scene rendering so HUD/text stays on top.
3112 if (!sprites.empty()) {
3113 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
3114 if (!sprite || isPostProcessSprite(sprite.get())) {
3115 continue;
3116 }
3117 if (sprite_pipeline != VK_NULL_HANDLE) {
3118 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
3119 }
3120 sprite->renderSprites(cmd, sprite_pipeline_layout, rendering_info.renderArea.extent.width, rendering_info.renderArea.extent.height);
3121 }
3122 }
3123
3124 if (!use_post_process && text_renderer && text_pipeline != VK_NULL_HANDLE) {
3125 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, text_pipeline);
3126 text_renderer->renderText(cmd, text_pipeline_layout, rendering_info.renderArea.extent.width, rendering_info.renderArea.extent.height);
3127 }
3128
3129 vkCmdEndRendering(cmd);
3130 if (depth_slot < depth_image_initialized.size()) {
3131 depth_image_initialized[depth_slot] = true;
3132 }
3133
3134 if (use_post_process) {
3135 VkImageMemoryBarrier2 post_target_barrier{};
3136 post_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3137 post_target_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3138 post_target_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3139 post_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3140 post_target_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3141 post_target_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3142 post_target_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3143 post_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3144 post_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3145 post_target_barrier.image = post_process_images[scene_target][image_index];
3146 post_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3147 post_target_barrier.subresourceRange.levelCount = 1;
3148 post_target_barrier.subresourceRange.layerCount = 1;
3149 VkDependencyInfo post_target_dependency{};
3150 post_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3151 post_target_dependency.imageMemoryBarrierCount = 1;
3152 post_target_dependency.pImageMemoryBarriers = &post_target_barrier;
3153 vkCmdPipelineBarrier2(cmd, &post_target_dependency);
3154 post_process_initialized[scene_target][image_index] = true;
3155
3156 size_t source_target = scene_target;
3157 for (size_t effect_index = 0; effect_index < post_process_sprites.size(); ++effect_index) {
3158 VK_Sprite *effect_sprite = post_process_sprites[effect_index];
3159 if (effect_sprite == nullptr) {
3160 continue;
3161 }
3162
3163 if (effect_index < post_process_effect_time_enabled.size() && post_process_effect_time_enabled[effect_index]) {
3164 post_process_effect_params[effect_index][0] = std::chrono::duration<float>(std::chrono::steady_clock::now() - post_process_effect_start_times[effect_index]).count();
3165 }
3166 if (effect_index < post_process_effect_params.size()) {
3167 const std::array<float, 4> &params = post_process_effect_params[effect_index];
3168 effect_sprite->setShaderParams(params[0], params[1], params[2], params[3]);
3169 }
3170
3171 const bool compute_effect = effect_index < post_process_effect_stages.size() && post_process_effect_stages[effect_index] == ShaderStage::Compute;
3172 const bool final_effect = (effect_index + 1U) == post_process_sprites.size();
3173 const size_t destination_target = hdr_render_intermediates_enabled ? (source_target == 2U ? 3U : 2U) : (source_target == 1U ? 2U : 1U);
3174
3175 if (compute_effect) {
3176 VkImageMemoryBarrier2 storage_target_barrier{};
3177 storage_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3178 storage_target_barrier.srcStageMask = post_process_initialized[destination_target][image_index] ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_NONE;
3179 storage_target_barrier.srcAccessMask = post_process_initialized[destination_target][image_index] ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_NONE;
3180 storage_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3181 storage_target_barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
3182 storage_target_barrier.oldLayout = post_process_initialized[destination_target][image_index] ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
3183 storage_target_barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
3184 storage_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3185 storage_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3186 storage_target_barrier.image = post_process_images[destination_target][image_index];
3187 storage_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3188 storage_target_barrier.subresourceRange.levelCount = 1;
3189 storage_target_barrier.subresourceRange.layerCount = 1;
3190 VkDependencyInfo storage_target_dependency{};
3191 storage_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3192 storage_target_dependency.imageMemoryBarrierCount = 1;
3193 storage_target_dependency.pImageMemoryBarriers = &storage_target_barrier;
3194 vkCmdPipelineBarrier2(cmd, &storage_target_dependency);
3195
3196 effect_sprite->dispatchCompute(cmd, post_process_views[source_target][image_index], post_process_views[destination_target][image_index], render_extent.width, render_extent.height);
3197
3198 VkImageMemoryBarrier2 sampled_target_barrier{};
3199 sampled_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3200 sampled_target_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3201 sampled_target_barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
3202 sampled_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3203 sampled_target_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3204 sampled_target_barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
3205 sampled_target_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3206 sampled_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3207 sampled_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3208 sampled_target_barrier.image = post_process_images[destination_target][image_index];
3209 sampled_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3210 sampled_target_barrier.subresourceRange.levelCount = 1;
3211 sampled_target_barrier.subresourceRange.layerCount = 1;
3212 VkDependencyInfo sampled_target_dependency{};
3213 sampled_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3214 sampled_target_dependency.imageMemoryBarrierCount = 1;
3215 sampled_target_dependency.pImageMemoryBarriers = &sampled_target_barrier;
3216 vkCmdPipelineBarrier2(cmd, &sampled_target_dependency);
3217 post_process_initialized[destination_target][image_index] = true;
3218 source_target = destination_target;
3219
3220 if (final_effect && !hdr_render_intermediates_enabled && !detached_render && !consume_post_process && post_process_present_sprite != nullptr) {
3221 VkRenderingAttachmentInfo post_attachment{};
3222 post_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3223 post_attachment.imageView = swapchain_image_views[image_index];
3224 post_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3225 post_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
3226 post_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3227 VkRenderingInfo post_info{};
3228 post_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3229 post_info.renderArea.extent = swapchain_extent;
3230 post_info.layerCount = 1;
3231 post_info.colorAttachmentCount = 1;
3232 post_info.pColorAttachments = &post_attachment;
3233 vkCmdBeginRendering(cmd, &post_info);
3234 post_process_present_sprite->setExternalTexture(post_process_views[source_target][image_index], static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
3235 post_process_present_sprite->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
3237 vkCmdEndRendering(cmd);
3238 }
3239 continue;
3240 }
3241
3242 const VkImageView destination_view = final_effect && !hdr_render_intermediates_enabled && !detached_render && !consume_post_process ? swapchain_image_views[image_index] : post_process_views[destination_target][image_index];
3243 if (!final_effect || hdr_render_intermediates_enabled || detached_render || consume_post_process) {
3244 VkImageMemoryBarrier2 next_target_barrier{};
3245 next_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3246 next_target_barrier.srcStageMask = post_process_initialized[destination_target][image_index] ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_NONE;
3247 next_target_barrier.srcAccessMask = post_process_initialized[destination_target][image_index] ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_NONE;
3248 next_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3249 next_target_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3250 next_target_barrier.oldLayout = post_process_initialized[destination_target][image_index] ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
3251 next_target_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3252 next_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3253 next_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3254 next_target_barrier.image = post_process_images[destination_target][image_index];
3255 next_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3256 next_target_barrier.subresourceRange.levelCount = 1;
3257 next_target_barrier.subresourceRange.layerCount = 1;
3258 VkDependencyInfo next_target_dependency{};
3259 next_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3260 next_target_dependency.imageMemoryBarrierCount = 1;
3261 next_target_dependency.pImageMemoryBarriers = &next_target_barrier;
3262 vkCmdPipelineBarrier2(cmd, &next_target_dependency);
3263 }
3264
3265 VkRenderingAttachmentInfo post_attachment{};
3266 post_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3267 post_attachment.imageView = destination_view;
3268 post_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3269 post_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
3270 post_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3271 VkRenderingInfo post_info{};
3272 post_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3273 post_info.renderArea.extent = detached_render ? render_extent : swapchain_extent;
3274 post_info.layerCount = 1;
3275 post_info.colorAttachmentCount = 1;
3276 post_info.pColorAttachments = &post_attachment;
3277 vkCmdBeginRendering(cmd, &post_info);
3278
3279 effect_sprite->setExternalTexture(post_process_views[source_target][image_index], static_cast<int>(render_extent.width), static_cast<int>(render_extent.height));
3280 effect_sprite->drawSpriteRect(0, 0, static_cast<int>(render_extent.width), static_cast<int>(render_extent.height));
3281 renderStandaloneSprite(*effect_sprite, cmd, detached_render ? render_extent : swapchain_extent);
3282 vkCmdEndRendering(cmd);
3283
3284 if (!final_effect || hdr_render_intermediates_enabled || detached_render || consume_post_process) {
3285 VkImageMemoryBarrier2 sampled_target_barrier{};
3286 sampled_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3287 sampled_target_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3288 sampled_target_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3289 sampled_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3290 sampled_target_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3291 sampled_target_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3292 sampled_target_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3293 sampled_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3294 sampled_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3295 sampled_target_barrier.image = post_process_images[destination_target][image_index];
3296 sampled_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3297 sampled_target_barrier.subresourceRange.levelCount = 1;
3298 sampled_target_barrier.subresourceRange.layerCount = 1;
3299 VkDependencyInfo sampled_target_dependency{};
3300 sampled_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3301 sampled_target_dependency.imageMemoryBarrierCount = 1;
3302 sampled_target_dependency.pImageMemoryBarriers = &sampled_target_barrier;
3303 vkCmdPipelineBarrier2(cmd, &sampled_target_dependency);
3304 post_process_initialized[destination_target][image_index] = true;
3305 source_target = destination_target;
3306 }
3307 }
3308
3309 bool hdr_consumer_rendered = false;
3310 if (consume_post_process && hdr_render_intermediates_enabled) {
3311 const size_t consumer_target = source_target == 2U ? 3U : 2U;
3312 VkImageMemoryBarrier2 consumer_target_barrier{};
3313 consumer_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3314 consumer_target_barrier.srcStageMask = post_process_initialized[consumer_target][image_index] ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_NONE;
3315 consumer_target_barrier.srcAccessMask = post_process_initialized[consumer_target][image_index] ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_NONE;
3316 consumer_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3317 consumer_target_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3318 consumer_target_barrier.oldLayout = post_process_initialized[consumer_target][image_index] ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
3319 consumer_target_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3320 consumer_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3321 consumer_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3322 consumer_target_barrier.image = post_process_images[consumer_target][image_index];
3323 consumer_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3324 consumer_target_barrier.subresourceRange.levelCount = 1;
3325 consumer_target_barrier.subresourceRange.layerCount = 1;
3326 VkDependencyInfo consumer_target_dependency{};
3327 consumer_target_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3328 consumer_target_dependency.imageMemoryBarrierCount = 1;
3329 consumer_target_dependency.pImageMemoryBarriers = &consumer_target_barrier;
3330 vkCmdPipelineBarrier2(cmd, &consumer_target_dependency);
3331
3332 VkRenderingAttachmentInfo consumer_attachment{};
3333 consumer_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3334 consumer_attachment.imageView = post_process_views[consumer_target][image_index];
3335 consumer_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3336 consumer_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
3337 consumer_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3338 VkRenderingInfo consumer_info{};
3339 consumer_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3340 consumer_info.renderArea.extent = render_extent;
3341 consumer_info.layerCount = 1;
3342 consumer_info.colorAttachmentCount = 1;
3343 consumer_info.pColorAttachments = &consumer_attachment;
3344 consumer_info.pDepthAttachment = depth_attachment.imageView != VK_NULL_HANDLE ? &depth_attachment : nullptr;
3345 vkCmdBeginRendering(cmd, &consumer_info);
3346 vkCmdSetViewport(cmd, 0, 1, &viewport);
3347 vkCmdSetScissor(cmd, 0, 1, &scissor);
3348 onRecordPostProcessingTexture(cmd, image_index, post_process_views[source_target][image_index], render_extent);
3349 vkCmdEndRendering(cmd);
3350
3351 VkImageMemoryBarrier2 sampled_consumer_barrier = consumer_target_barrier;
3352 sampled_consumer_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3353 sampled_consumer_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3354 sampled_consumer_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3355 sampled_consumer_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3356 sampled_consumer_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3357 sampled_consumer_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3358 VkDependencyInfo sampled_consumer_dependency{};
3359 sampled_consumer_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3360 sampled_consumer_dependency.imageMemoryBarrierCount = 1;
3361 sampled_consumer_dependency.pImageMemoryBarriers = &sampled_consumer_barrier;
3362 vkCmdPipelineBarrier2(cmd, &sampled_consumer_dependency);
3363 post_process_initialized[consumer_target][image_index] = true;
3364 source_target = consumer_target;
3365 hdr_consumer_rendered = true;
3366 }
3367
3369 rgba16_readback_image = post_process_images[source_target][image_index];
3370 }
3371
3372 if (hdr_render_intermediates_enabled && !detached_render && (!consume_post_process || hdr_consumer_rendered) && post_process_present_sprite != nullptr) {
3373 VkRenderingAttachmentInfo present_attachment{};
3374 present_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3375 present_attachment.imageView = swapchain_image_views[image_index];
3376 present_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3377 present_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
3378 present_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3379 VkRenderingInfo present_info{};
3380 present_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3381 present_info.renderArea.extent = swapchain_extent;
3382 present_info.layerCount = 1;
3383 present_info.colorAttachmentCount = 1;
3384 present_info.pColorAttachments = &present_attachment;
3385 vkCmdBeginRendering(cmd, &present_info);
3386 post_process_present_sprite->setExternalTexture(post_process_views[source_target][image_index], static_cast<int>(render_extent.width), static_cast<int>(render_extent.height));
3387 post_process_present_sprite->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
3390 vkCmdEndRendering(cmd);
3391 }
3392
3393 if (detached_render && post_process_composite_sprite != nullptr) {
3394 VkImageMemoryBarrier2 composite_target_barrier{};
3395 composite_target_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3396 composite_target_barrier.srcStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3397 composite_target_barrier.srcAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3398 composite_target_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3399 composite_target_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3400 composite_target_barrier.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3401 composite_target_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3402 composite_target_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3403 composite_target_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3404 composite_target_barrier.image = post_process_images[0][image_index];
3405 composite_target_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3406 composite_target_barrier.subresourceRange.levelCount = 1;
3407 composite_target_barrier.subresourceRange.layerCount = 1;
3408 VkDependencyInfo composite_dependency{};
3409 composite_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3410 composite_dependency.imageMemoryBarrierCount = 1;
3411 composite_dependency.pImageMemoryBarriers = &composite_target_barrier;
3412 vkCmdPipelineBarrier2(cmd, &composite_dependency);
3413
3414 VkRenderingAttachmentInfo composite_attachment{};
3415 composite_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3416 composite_attachment.imageView = post_process_views[0][image_index];
3417 composite_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3418 composite_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
3419 composite_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3420 VkRenderingInfo composite_info{};
3421 composite_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3422 composite_info.renderArea.extent = render_extent;
3423 composite_info.layerCount = 1;
3424 composite_info.colorAttachmentCount = 1;
3425 composite_info.pColorAttachments = &composite_attachment;
3426 composite_info.pDepthAttachment = consume_post_process && !hdr_consumer_rendered && depth_attachment.imageView != VK_NULL_HANDLE ? &depth_attachment : nullptr;
3427 vkCmdBeginRendering(cmd, &composite_info);
3428 if (consume_post_process && !hdr_consumer_rendered) {
3429 vkCmdSetViewport(cmd, 0, 1, &viewport);
3430 vkCmdSetScissor(cmd, 0, 1, &scissor);
3431 onRecordPostProcessingTexture(cmd, image_index, post_process_views[source_target][image_index], render_extent);
3432 } else {
3433 post_process_composite_sprite->setExternalTexture(post_process_views[source_target][image_index], static_cast<int>(render_extent.width), static_cast<int>(render_extent.height));
3434 post_process_composite_sprite->drawSpriteRect(0, 0, static_cast<int>(render_extent.width), static_cast<int>(render_extent.height));
3437 }
3438 vkCmdEndRendering(cmd);
3439 } else if (consume_post_process && !hdr_consumer_rendered) {
3440 VkRenderingAttachmentInfo consumer_attachment{};
3441 consumer_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3442 consumer_attachment.imageView = swapchain_image_views[image_index];
3443 consumer_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3444 consumer_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
3445 consumer_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3446 VkRenderingInfo consumer_info{};
3447 consumer_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3448 consumer_info.renderArea.extent = swapchain_extent;
3449 consumer_info.layerCount = 1;
3450 consumer_info.colorAttachmentCount = 1;
3451 consumer_info.pColorAttachments = &consumer_attachment;
3452 consumer_info.pDepthAttachment = depth_attachment.imageView != VK_NULL_HANDLE ? &depth_attachment : nullptr;
3453 vkCmdBeginRendering(cmd, &consumer_info);
3454 vkCmdSetViewport(cmd, 0, 1, &viewport);
3455 vkCmdSetScissor(cmd, 0, 1, &scissor);
3456 onRecordPostProcessingTexture(cmd, image_index, post_process_views[source_target][image_index], render_extent);
3457 vkCmdEndRendering(cmd);
3458 }
3459 }
3460
3461 // Post-processing owns the final swapchain color until this point. Draw
3462 // queued text in a load-preserving pass so HUD and watermark text stay
3463 // crisp and are included in the final readback without being sampled by
3464 // the effect chain.
3465 if (use_post_process && text_renderer && text_pipeline != VK_NULL_HANDLE) {
3466 VkImageMemoryBarrier2 to_text_barrier{};
3467 to_text_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3468 to_text_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3469 to_text_barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3470 to_text_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3471 to_text_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3472 to_text_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3473 to_text_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3474 to_text_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3475 to_text_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3476 to_text_barrier.image = detached_render ? post_process_images[0][image_index] : swapchain_images[image_index];
3477 to_text_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3478 to_text_barrier.subresourceRange.levelCount = 1;
3479 to_text_barrier.subresourceRange.layerCount = 1;
3480 VkDependencyInfo to_text_dependency{};
3481 to_text_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3482 to_text_dependency.imageMemoryBarrierCount = 1;
3483 to_text_dependency.pImageMemoryBarriers = &to_text_barrier;
3484 vkCmdPipelineBarrier2(cmd, &to_text_dependency);
3485
3486 VkRenderingAttachmentInfo text_attachment{};
3487 text_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3488 text_attachment.imageView = detached_render ? post_process_views[0][image_index] : swapchain_image_views[image_index];
3489 text_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3490 text_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
3491 text_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3492 VkRenderingInfo text_info{};
3493 text_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3494 text_info.renderArea.extent = detached_render ? render_extent : swapchain_extent;
3495 text_info.layerCount = 1;
3496 text_info.colorAttachmentCount = 1;
3497 text_info.pColorAttachments = &text_attachment;
3498 vkCmdBeginRendering(cmd, &text_info);
3499 vkCmdSetViewport(cmd, 0, 1, &viewport);
3500 vkCmdSetScissor(cmd, 0, 1, &scissor);
3501 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, text_pipeline);
3502 text_renderer->renderText(cmd, text_pipeline_layout, text_info.renderArea.extent.width, text_info.renderArea.extent.height);
3503 vkCmdEndRendering(cmd);
3504 }
3505
3506 const bool use_rgba16_readback = frame_readback_enabled && rgba16_readback_image != VK_NULL_HANDLE;
3507 const bool use_output_readback = frame_readback_enabled && !use_rgba16_readback;
3509 const VkImage readback_image = use_rgba16_readback ? rgba16_readback_image : detached_render ? post_process_images[0][image_index] : swapchain_images[image_index];
3510 VkImageMemoryBarrier2 to_readback_barrier{};
3511 to_readback_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3512 to_readback_barrier.srcStageMask = use_rgba16_readback ? VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3513 to_readback_barrier.srcAccessMask = use_rgba16_readback ? VK_ACCESS_2_SHADER_SAMPLED_READ_BIT : VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3514 to_readback_barrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
3515 to_readback_barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
3516 to_readback_barrier.oldLayout = use_rgba16_readback ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3517 to_readback_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
3518 to_readback_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3519 to_readback_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3520 to_readback_barrier.image = readback_image;
3521 to_readback_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3522 to_readback_barrier.subresourceRange.baseMipLevel = 0;
3523 to_readback_barrier.subresourceRange.levelCount = 1;
3524 to_readback_barrier.subresourceRange.baseArrayLayer = 0;
3525 to_readback_barrier.subresourceRange.layerCount = 1;
3526
3527 VkDependencyInfo to_readback_dependency{};
3528 to_readback_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3529 to_readback_dependency.imageMemoryBarrierCount = 1;
3530 to_readback_dependency.pImageMemoryBarriers = &to_readback_barrier;
3531 vkCmdPipelineBarrier2(cmd, &to_readback_dependency);
3532
3533 VkBufferImageCopy readback_region{};
3534 readback_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3535 readback_region.imageSubresource.mipLevel = 0;
3536 readback_region.imageSubresource.baseArrayLayer = 0;
3537 readback_region.imageSubresource.layerCount = 1;
3538 readback_region.imageExtent = {use_rgba16_readback || detached_render ? render_extent.width : swapchain_extent.width, use_rgba16_readback || detached_render ? render_extent.height : swapchain_extent.height, 1};
3539 vkCmdCopyImageToBuffer(cmd, readback_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, frame_readback_slots[current_frame].buffer, 1, &readback_region);
3540
3541 if (use_rgba16_readback) {
3542 VkImageMemoryBarrier2 restore_readback_barrier = to_readback_barrier;
3543 restore_readback_barrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
3544 restore_readback_barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT;
3545 restore_readback_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
3546 restore_readback_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3547 restore_readback_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
3548 restore_readback_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3549 VkDependencyInfo restore_readback_dependency{};
3550 restore_readback_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3551 restore_readback_dependency.imageMemoryBarrierCount = 1;
3552 restore_readback_dependency.pImageMemoryBarriers = &restore_readback_barrier;
3553 vkCmdPipelineBarrier2(cmd, &restore_readback_dependency);
3554 }
3555 }
3556
3557 if (!headless() && detached_render && post_process_present_sprite != nullptr) {
3558 VkImageMemoryBarrier2 to_sample_barrier{};
3559 to_sample_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3560 to_sample_barrier.srcStageMask = use_output_readback ? VK_PIPELINE_STAGE_2_TRANSFER_BIT : VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3561 to_sample_barrier.srcAccessMask = use_output_readback ? VK_ACCESS_2_TRANSFER_READ_BIT : VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3562 to_sample_barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
3563 to_sample_barrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
3564 to_sample_barrier.oldLayout = use_output_readback ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3565 to_sample_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
3566 to_sample_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3567 to_sample_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3568 to_sample_barrier.image = post_process_images[0][image_index];
3569 to_sample_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3570 to_sample_barrier.subresourceRange.levelCount = 1;
3571 to_sample_barrier.subresourceRange.layerCount = 1;
3572 VkDependencyInfo to_sample_dependency{};
3573 to_sample_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3574 to_sample_dependency.imageMemoryBarrierCount = 1;
3575 to_sample_dependency.pImageMemoryBarriers = &to_sample_barrier;
3576 vkCmdPipelineBarrier2(cmd, &to_sample_dependency);
3577
3578 VkRenderingAttachmentInfo present_attachment{};
3579 present_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3580 present_attachment.imageView = swapchain_image_views[image_index];
3581 present_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3582 present_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
3583 present_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3584 present_attachment.clearValue = clear_value;
3585 VkRenderingInfo present_info{};
3586 present_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3587 present_info.renderArea.extent = swapchain_extent;
3588 present_info.layerCount = 1;
3589 present_info.colorAttachmentCount = 1;
3590 present_info.pColorAttachments = &present_attachment;
3591 vkCmdBeginRendering(cmd, &present_info);
3592
3593 VkViewport present_viewport{};
3594 present_viewport.width = static_cast<float>(swapchain_extent.width);
3595 present_viewport.height = static_cast<float>(swapchain_extent.height);
3596 present_viewport.maxDepth = 1.0F;
3597 vkCmdSetViewport(cmd, 0, 1, &present_viewport);
3598 VkRect2D present_scissor{};
3599 present_scissor.extent = swapchain_extent;
3600 vkCmdSetScissor(cmd, 0, 1, &present_scissor);
3601
3602 const double preview_scale = std::min(static_cast<double>(swapchain_extent.width) / render_extent.width, static_cast<double>(swapchain_extent.height) / render_extent.height);
3603 const int preview_width = std::max(1, static_cast<int>(std::lround(render_extent.width * preview_scale)));
3604 const int preview_height = std::max(1, static_cast<int>(std::lround(render_extent.height * preview_scale)));
3605 const int preview_x = (static_cast<int>(swapchain_extent.width) - preview_width) / 2;
3606 const int preview_y = (static_cast<int>(swapchain_extent.height) - preview_height) / 2;
3607 post_process_present_sprite->setExternalTexture(post_process_views[0][image_index], static_cast<int>(render_extent.width), static_cast<int>(render_extent.height));
3608 post_process_present_sprite->drawSpriteRect(preview_x, preview_y, preview_width, preview_height);
3611 vkCmdEndRendering(cmd);
3612 }
3613
3614 const bool render_preview_text = !headless() && preview_text_queued && preview_text_renderer && text_pipeline != VK_NULL_HANDLE;
3615 if (render_preview_text) {
3616 VkImageMemoryBarrier2 to_preview_barrier{};
3617 to_preview_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3618 to_preview_barrier.srcStageMask = use_output_readback && !detached_render ? VK_PIPELINE_STAGE_2_TRANSFER_BIT : VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3619 to_preview_barrier.srcAccessMask = use_output_readback && !detached_render ? VK_ACCESS_2_TRANSFER_READ_BIT : VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3620 to_preview_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3621 to_preview_barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3622 to_preview_barrier.oldLayout = use_output_readback && !detached_render ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3623 to_preview_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3624 to_preview_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3625 to_preview_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3626 to_preview_barrier.image = swapchain_images[image_index];
3627 to_preview_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3628 to_preview_barrier.subresourceRange.levelCount = 1;
3629 to_preview_barrier.subresourceRange.layerCount = 1;
3630 VkDependencyInfo to_preview_dependency{};
3631 to_preview_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3632 to_preview_dependency.imageMemoryBarrierCount = 1;
3633 to_preview_dependency.pImageMemoryBarriers = &to_preview_barrier;
3634 vkCmdPipelineBarrier2(cmd, &to_preview_dependency);
3635
3636 VkRenderingAttachmentInfo preview_attachment{};
3637 preview_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
3638 preview_attachment.imageView = swapchain_image_views[image_index];
3639 preview_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3640 preview_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
3641 preview_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
3642 VkRenderingInfo preview_info{};
3643 preview_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
3644 preview_info.renderArea.extent = swapchain_extent;
3645 preview_info.layerCount = 1;
3646 preview_info.colorAttachmentCount = 1;
3647 preview_info.pColorAttachments = &preview_attachment;
3648 vkCmdBeginRendering(cmd, &preview_info);
3649 viewport.width = static_cast<float>(swapchain_extent.width);
3650 viewport.height = static_cast<float>(swapchain_extent.height);
3651 scissor.extent = swapchain_extent;
3652 vkCmdSetViewport(cmd, 0, 1, &viewport);
3653 vkCmdSetScissor(cmd, 0, 1, &scissor);
3654 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, text_pipeline);
3656 vkCmdEndRendering(cmd);
3657 }
3658
3659 VkImageMemoryBarrier2 to_present_barrier{};
3660 to_present_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
3661 to_present_barrier.srcStageMask = render_preview_text ? VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT : use_output_readback && !detached_render ? VK_PIPELINE_STAGE_2_TRANSFER_BIT : VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3662 to_present_barrier.srcAccessMask = render_preview_text ? VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT : use_output_readback && !detached_render ? VK_ACCESS_2_TRANSFER_READ_BIT : VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
3663 to_present_barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
3664 to_present_barrier.dstAccessMask = VK_ACCESS_2_NONE;
3665 to_present_barrier.oldLayout = render_preview_text ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : use_output_readback && !detached_render ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
3666 to_present_barrier.newLayout = headless() ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
3667 to_present_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3668 to_present_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
3669 to_present_barrier.image = swapchain_images[image_index];
3670 to_present_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3671 to_present_barrier.subresourceRange.baseMipLevel = 0;
3672 to_present_barrier.subresourceRange.levelCount = 1;
3673 to_present_barrier.subresourceRange.baseArrayLayer = 0;
3674 to_present_barrier.subresourceRange.layerCount = 1;
3675
3676 VkDependencyInfo post_render_dependency{};
3677 post_render_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
3678 post_render_dependency.imageMemoryBarrierCount = 1;
3679 post_render_dependency.pImageMemoryBarriers = &to_present_barrier;
3680 vkCmdPipelineBarrier2(cmd, &post_render_dependency);
3681
3682 if (vkEndCommandBuffer(cmd) != VK_SUCCESS) {
3683 std::cerr << "mxvk: Failed to end command buffer\n";
3684 framebuffer_resized = true;
3685 return;
3686 }
3687
3688 VkSemaphoreSubmitInfo wait_semaphore_info{};
3689 wait_semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
3690 wait_semaphore_info.semaphore = acquire_semaphore;
3691 wait_semaphore_info.value = 0;
3692 wait_semaphore_info.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
3693 wait_semaphore_info.deviceIndex = 0;
3694
3695 VkCommandBufferSubmitInfo command_buffer_info{};
3696 command_buffer_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
3697 command_buffer_info.commandBuffer = cmd;
3698 command_buffer_info.deviceMask = 0;
3699
3700 VkSemaphoreSubmitInfo signal_semaphore_info{};
3701 signal_semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
3702 signal_semaphore_info.semaphore = present_semaphore;
3703 signal_semaphore_info.value = 0;
3704 signal_semaphore_info.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
3705 signal_semaphore_info.deviceIndex = 0;
3706
3707 VkSubmitInfo2 submit_info{};
3708 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
3709 submit_info.waitSemaphoreInfoCount = headless() ? 0U : 1U;
3710 submit_info.pWaitSemaphoreInfos = headless() ? nullptr : &wait_semaphore_info;
3711 submit_info.commandBufferInfoCount = 1;
3712 submit_info.pCommandBufferInfos = &command_buffer_info;
3713 submit_info.signalSemaphoreInfoCount = headless() ? 0U : 1U;
3714 submit_info.pSignalSemaphoreInfos = headless() ? nullptr : &signal_semaphore_info;
3715
3716 const VkResult fence_reset_result = vkResetFences(device, 1, &frame_fence);
3717 if (fence_reset_result == VK_ERROR_DEVICE_LOST) {
3718 std::cerr << "mxvk: device lost while resetting frame fence; stopping render loop\n";
3719 active = false;
3720 return;
3721 }
3722 if (fence_reset_result != VK_SUCCESS) {
3723 std::cerr << std::format("mxvk: Failed to reset frame fence (VkResult={})\n", static_cast<int>(fence_reset_result));
3724 framebuffer_resized = true;
3725 return;
3726 }
3727
3728 const VkResult submit_result = vkQueueSubmit2(graphics_queue, 1, &submit_info, frame_fence);
3729 if (submit_result == VK_ERROR_DEVICE_LOST) {
3730 std::cerr << "mxvk: device lost during queue submit; stopping render loop\n";
3731 active = false;
3732 return;
3733 }
3734 if (submit_result != VK_SUCCESS) {
3735 std::cerr << std::format("mxvk: Failed to submit draw command (VkResult={})\n", static_cast<int>(submit_result));
3736 // We already acquired an image this frame. Force swapchain recreation so we do not reuse
3737 // a signaled acquire semaphore that never got consumed by a successful submit.
3738 framebuffer_resized = true;
3739 return;
3740 }
3741 image_fences[image_index] = frame_fence;
3742 swapchain_image_initialized[image_index] = true;
3743
3744 VkResult present_result = VK_SUCCESS;
3745 if (!headless()) {
3746 VkPresentInfoKHR present_info{};
3747 present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
3748 present_info.waitSemaphoreCount = 1;
3749 present_info.pWaitSemaphores = &present_semaphore;
3750 present_info.swapchainCount = 1;
3751 present_info.pSwapchains = &swapchain;
3752 present_info.pImageIndices = &image_index;
3753 present_result = vkQueuePresentKHR(present_queue, &present_info);
3754 }
3755 if (present_result == VK_SUCCESS || present_result == VK_SUBOPTIMAL_KHR) {
3756 last_presented_image_index = image_index;
3758 FrameReadbackSlot &slot = frame_readback_slots[current_frame];
3759 slot.width = use_rgba16_readback || detached_render ? render_extent.width : swapchain_extent.width;
3760 slot.height = use_rgba16_readback || detached_render ? render_extent.height : swapchain_extent.height;
3761 slot.format = use_rgba16_readback ? VK_FORMAT_R16G16B16A16_SFLOAT : swapchain_format;
3762 slot.pending = true;
3764 }
3765 }
3766
3767 if (present_result == VK_ERROR_OUT_OF_DATE_KHR) {
3768 std::cout << "mxvk: requesting swapchain recreation because present returned VK_ERROR_OUT_OF_DATE_KHR\n";
3770 last_resize_event_ms = SDL_GetTicks();
3771 framebuffer_resized = true;
3772 } else if (present_result == VK_SUBOPTIMAL_KHR) {
3773 int present_pixel_w = 0;
3774 int present_pixel_h = 0;
3775 if (window != nullptr) {
3776 SDL_GetWindowSizeInPixels(window.get(), &present_pixel_w, &present_pixel_h);
3777 }
3778 const bool known_present_extent = present_pixel_w > 0 && present_pixel_h > 0;
3779 const bool present_extent_changed = known_present_extent && (swapchain_extent.width != static_cast<uint32_t>(present_pixel_w) || swapchain_extent.height != static_cast<uint32_t>(present_pixel_h));
3780 if (!known_present_extent || present_extent_changed) {
3781 std::cout << "mxvk: requesting swapchain recreation because present returned VK_SUBOPTIMAL_KHR with changed window extent\n";
3782 last_resize_event_ms = SDL_GetTicks();
3783 framebuffer_resized = true;
3785 }
3786 } else if (present_result != VK_SUCCESS) {
3787 std::cerr << "mxvk: Failed to present swapchain image\n";
3788 }
3789
3790 for (const std::unique_ptr<VK_Sprite> &sprite : sprites) {
3791 if (sprite) {
3792 sprite->clearQueue();
3793 }
3794 }
3795 if (text_renderer) {
3796 text_renderer->clearQueue();
3797 }
3799 preview_text_renderer->clearQueue();
3800 }
3801 preview_text_queued = false;
3802
3803 current_frame = (current_frame + 1U) % max_frames_in_flight;
3804 if (!retired_swapchains.empty() && (present_result == VK_SUCCESS || present_result == VK_SUBOPTIMAL_KHR)) {
3805 for (VkSwapchainKHR retired : retired_swapchains) {
3806 vkDestroySwapchainKHR(device, retired, nullptr);
3807 }
3808 retired_swapchains.clear();
3809 }
3810 }
3811
3813
3814 bool VK_Window::hasValidationLayerSupport() {
3815 uint32_t layer_count = 0;
3816 const VkResult count_result = vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
3817 if (count_result != VK_SUCCESS) {
3818 std::cerr << std::format("mxvk: Failed to query validation layer count (VkResult={})\n", static_cast<int>(count_result));
3819 return false;
3820 }
3821
3822 if (layer_count == 0U) {
3823 return false;
3824 }
3825
3826 std::vector<VkLayerProperties> available_layers(layer_count);
3827 const VkResult layers_result = vkEnumerateInstanceLayerProperties(&layer_count, available_layers.data());
3828 if (layers_result != VK_SUCCESS) {
3829 std::cerr << std::format("mxvk: Failed to enumerate validation layers (VkResult={})\n", static_cast<int>(layers_result));
3830 return false;
3831 }
3832
3833 return std::ranges::any_of(available_layers, [](const VkLayerProperties &layer) { return std::strcmp(layer.layerName, validation_layer_name) == 0; });
3834 }
3835
3836 std::optional<VkDebugUtilsMessengerCreateInfoEXT> VK_Window::makeDebugMessengerCreateInfo() {
3837 VkDebugUtilsMessengerCreateInfoEXT create_info{};
3838 create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
3839 create_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
3840 create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
3841 create_info.pfnUserCallback = debugCallback;
3842 create_info.pUserData = nullptr;
3843 return create_info;
3844 }
3845
3846 void VK_Window::setupDebugMessenger() {
3847 if (!validation_enabled || instance == VK_NULL_HANDLE) {
3848 return;
3849 }
3850
3851 const std::optional<VkDebugUtilsMessengerCreateInfoEXT> maybe_create_info = makeDebugMessengerCreateInfo();
3852 if (!maybe_create_info.has_value()) {
3853 std::cerr << "mxvk: unable to build debug messenger create info\n";
3854 return;
3855 }
3856
3857 const VkResult result = vkCreateDebugUtilsMessengerEXT(instance, &maybe_create_info.value(), nullptr, &debug_messenger);
3858 if (result != VK_SUCCESS) {
3859 std::cerr << "mxvk: failed to create Vulkan debug messenger\n";
3860 debug_messenger = VK_NULL_HANDLE;
3861 }
3862 }
3863
3864 void VK_Window::cleanupDebugMessenger() {
3865 if (debug_messenger != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) {
3866 std::cout << "vk: destroying debug messenger\n";
3867 vkDestroyDebugUtilsMessengerEXT(instance, debug_messenger, nullptr);
3868 debug_messenger = VK_NULL_HANDLE;
3869 }
3870 }
3871
3872 void VK_Window::setFont(const std::string &fontPath, int fontSize) {
3873 if (fontPath.empty() || fontSize <= 0) {
3874 throw mxvk::Exception("setFont requires a non-empty path and positive font size");
3875 }
3876
3877 font_path = fontPath;
3878 font_size = fontSize;
3879 font_configured = true;
3880
3881 if (device == VK_NULL_HANDLE) {
3882 throw mxvk::Exception("Cannot set font before Vulkan device initialization");
3883 }
3884 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
3885 createDevice();
3886 }
3887 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
3888 throw mxvk::Exception("Cannot initialize text renderer before swapchain and command resources are available");
3889 }
3890
3891 if (!text_renderer) {
3892 ensureTextRenderer();
3893 } else {
3895 }
3898 }
3899 text_state_dirty = true;
3900 }
3901
3902 void VK_Window::setPreviewFont(const std::string &fontPath, int fontSize) {
3903 if (fontPath.empty() || fontSize <= 0) {
3904 throw mxvk::Exception("setPreviewFont requires a non-empty path and positive font size");
3905 }
3906 if (device == VK_NULL_HANDLE) {
3907 throw mxvk::Exception("Cannot set preview font before Vulkan device initialization");
3908 }
3909 if (!preview_text_renderer) {
3910 ensurePreviewTextRenderer(fontPath, fontSize);
3911 }
3912 if (!preview_text_renderer) {
3913 throw mxvk::Exception("Cannot initialize preview text renderer");
3914 }
3915 preview_text_renderer->setFont(fontPath, fontSize);
3916 text_state_dirty = true;
3917 }
3918
3919 void VK_Window::printText(const std::string &text, int x, int y, const SDL_Color &col) {
3920 if (text.empty()) {
3921 return;
3922 }
3923
3924 if (!font_configured) {
3925 throw mxvk::Exception("printText requires setFont() to be called first");
3926 }
3927
3928 ensureTextRenderer();
3929 text_renderer->printTextG_Solid(text, x, y, col);
3930 }
3931
3932 void VK_Window::printText(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *font) {
3933 if (text.empty()) {
3934 return;
3935 }
3936
3937 if (font == nullptr) {
3938 throw mxvk::Exception("printText requires a non-null TTF_Font");
3939 }
3940
3941 ensureTextRenderer(resolveDefaultFontPath(), font_size);
3942 if (!text_renderer) {
3943 throw mxvk::Exception("printText with an explicit font could not initialize the text renderer");
3944 }
3945 text_renderer->printTextG_Solid(text, x, y, col, font);
3946 }
3947
3948 void VK_Window::printText(const std::string &text, int x, int y, const SDL_Color &col, const Font &font) {
3949 if (text.empty()) {
3950 return;
3951 }
3952
3953 if (!font) {
3954 throw mxvk::Exception("printText requires a valid mxvk::Font");
3955 }
3956
3957 ensureTextRenderer(font.path(), font.size());
3958 if (!text_renderer) {
3959 throw mxvk::Exception("printText with an explicit font could not initialize the text renderer");
3960 }
3961 text_renderer->printTextG_Solid(text, x, y, col, font);
3962 }
3963
3965 if (text_renderer) {
3966 text_renderer->clearQueue();
3967 }
3969 preview_text_renderer->clearQueue();
3970 }
3971 preview_text_queued = false;
3972 }
3973
3974 void VK_Window::printPreviewText(const std::string &text, int x, int y, const SDL_Color &col) {
3975 if (text.empty()) {
3976 return;
3977 }
3978 if (!font_configured) {
3979 throw mxvk::Exception("printPreviewText requires setFont() to be called first");
3980 }
3981 ensurePreviewTextRenderer();
3982 if (!preview_text_renderer) {
3983 throw mxvk::Exception("printPreviewText could not initialize the text renderer");
3984 }
3985 preview_text_renderer->printTextG_Solid(text, x, y, col);
3986 preview_text_queued = true;
3987 }
3988
3989 void VK_Window::printPreviewText(const std::string &text, int x, int y, const SDL_Color &col, const Font &font) {
3990 if (text.empty()) {
3991 return;
3992 }
3993 if (!font) {
3994 throw mxvk::Exception("printPreviewText requires a valid mxvk::Font");
3995 }
3996 ensurePreviewTextRenderer(font.path(), font.size());
3997 if (!preview_text_renderer) {
3998 throw mxvk::Exception("printPreviewText could not initialize the text renderer");
3999 }
4000 preview_text_renderer->printTextG_Solid(text, x, y, col, font);
4001 preview_text_queued = true;
4002 }
4003
4004 bool VK_Window::getTextDimensions(const std::string &text, int &width, int &height) {
4005 if (!font_configured) {
4006 throw mxvk::Exception("getTextDimensions requires setFont() to be called first");
4007 }
4008
4009 if (!text_renderer) {
4010 ensureTextRenderer();
4011 }
4012 if (!text_renderer) {
4013 width = 0;
4014 height = 0;
4015 return false;
4016 }
4017 return text_renderer->getTextDimensions(text, width, height);
4018 }
4019
4020 bool VK_Window::getTextDimensions(const std::string &text, int &width, int &height, TTF_Font *font) {
4021 if (font == nullptr) {
4022 width = 0;
4023 height = 0;
4024 return false;
4025 }
4026
4027 ensureTextRenderer(resolveDefaultFontPath(), font_size);
4028 if (!text_renderer) {
4029 width = 0;
4030 height = 0;
4031 return false;
4032 }
4033 return text_renderer->getTextDimensions(text, width, height, font);
4034 }
4035
4036 bool VK_Window::getTextDimensions(const std::string &text, int &width, int &height, const Font &font) {
4037 if (!font) {
4038 width = 0;
4039 height = 0;
4040 return false;
4041 }
4042
4043 ensureTextRenderer(font.path(), font.size());
4044 if (!text_renderer) {
4045 width = 0;
4046 height = 0;
4047 return false;
4048 }
4049 return text_renderer->getTextDimensions(text, width, height, font);
4050 }
4051
4052 void VK_Window::ensureTextRenderer() {
4053 if (text_renderer) {
4054 return;
4055 }
4056
4057 if (!font_configured || font_path.empty() || font_size <= 0) {
4058 return;
4059 }
4060
4061 if (device == VK_NULL_HANDLE) {
4062 return;
4063 }
4064 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4065 createDevice();
4066 }
4067 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4068 return;
4069 }
4070
4071 if (text_descriptor_set_layout == VK_NULL_HANDLE) {
4072 createTextDescriptorSetLayout();
4073 }
4074
4076 text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
4077 text_state_dirty = true;
4078 }
4079
4080 void VK_Window::ensurePreviewTextRenderer() {
4082 return;
4083 }
4084 if (!font_configured || font_path.empty() || font_size <= 0 || device == VK_NULL_HANDLE) {
4085 return;
4086 }
4087 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4088 createDevice();
4089 }
4090 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4091 return;
4092 }
4093 if (text_descriptor_set_layout == VK_NULL_HANDLE) {
4094 createTextDescriptorSetLayout();
4095 }
4097 preview_text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
4098 text_state_dirty = true;
4099 }
4100
4101 void VK_Window::ensurePreviewTextRenderer(const std::string &fallbackFontPath, int fallbackFontSize) {
4103 return;
4104 }
4105 if (device == VK_NULL_HANDLE) {
4106 return;
4107 }
4108 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4109 createDevice();
4110 }
4111 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4112 return;
4113 }
4114 const std::string renderer_font_path = font_configured ? font_path : fallbackFontPath;
4115 const int renderer_font_size = font_configured ? font_size : fallbackFontSize;
4116 if (renderer_font_path.empty() || renderer_font_size <= 0) {
4117 return;
4118 }
4119 if (text_descriptor_set_layout == VK_NULL_HANDLE) {
4120 createTextDescriptorSetLayout();
4121 }
4122 preview_text_renderer = std::make_unique<VK_Text>(device, physical_device, graphics_queue, command_pool, renderer_font_path, renderer_font_size);
4123 preview_text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
4124 text_state_dirty = true;
4125 }
4126
4127 void VK_Window::ensureTextRenderer(const std::string &fallbackFontPath, int fallbackFontSize) {
4128 if (text_renderer) {
4129 return;
4130 }
4131
4132 if (device == VK_NULL_HANDLE) {
4133 return;
4134 }
4135 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4136 createDevice();
4137 }
4138 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4139 return;
4140 }
4141
4142 const std::string renderer_font_path = font_configured ? font_path : fallbackFontPath;
4143 const int renderer_font_size = font_configured ? font_size : fallbackFontSize;
4144 if (renderer_font_path.empty() || renderer_font_size <= 0) {
4145 return;
4146 }
4147
4148 if (text_descriptor_set_layout == VK_NULL_HANDLE) {
4149 createTextDescriptorSetLayout();
4150 }
4151
4152 text_renderer = std::make_unique<VK_Text>(device, physical_device, graphics_queue, command_pool, renderer_font_path, renderer_font_size);
4153 text_renderer->setDescriptorSetLayout(text_descriptor_set_layout);
4154 text_state_dirty = true;
4155 }
4156
4157 std::string VK_Window::resolveDefaultFontPath() const {
4158 std::vector<std::filesystem::path> candidates{};
4159
4160 if (const char *basePath = SDL_GetBasePath(); basePath != nullptr) {
4161 const std::filesystem::path executableDir(basePath);
4162 candidates.push_back(executableDir / "data" / "default.ttf");
4163 candidates.push_back(executableDir / "default.ttf");
4164 }
4165
4166 candidates.push_back(std::filesystem::path("data") / "default.ttf");
4167
4168 if (!font_path.empty()) {
4169 const std::filesystem::path configured_font_path(font_path);
4170 if (configured_font_path.has_parent_path()) {
4171 candidates.push_back(configured_font_path.parent_path() / "default.ttf");
4172 }
4173 }
4174
4175 candidates.push_back(std::filesystem::path(MXVK_DEFAULT_FONT_DIR) / "default.ttf");
4176
4177 std::error_code exists_error{};
4178 for (const std::filesystem::path &candidate : candidates) {
4179 if (std::filesystem::exists(candidate, exists_error)) {
4180 return candidate.string();
4181 }
4182 exists_error.clear();
4183 }
4184
4185 return {};
4186 }
4187
4188 void VK_Window::toggleFpsCounter() {
4191 fps_counter_sample_time = std::chrono::steady_clock::now();
4192 fps_counter_text = "FPS: --";
4193 std::cout << std::format("mxvk: FPS counter {}\n", fps_counter_enabled ? "enabled" : "disabled");
4194
4195 if (!fps_counter_enabled) {
4196 return;
4197 }
4198
4200 const std::string default_font_path = resolveDefaultFontPath();
4201 if (default_font_path.empty()) {
4202 std::cerr << "mxvk: F12 FPS counter could not locate data/default.ttf\n";
4203 fps_counter_enabled = false;
4204 return;
4205 }
4206 fps_counter_font.reset(default_font_path, 18);
4208 }
4209 }
4210
4211 void VK_Window::updateFpsCounter() {
4212 if (!fps_counter_enabled) {
4213 return;
4214 }
4215
4217 const std::string default_font_path = resolveDefaultFontPath();
4218 if (default_font_path.empty()) {
4219 std::cerr << "mxvk: F12 FPS counter could not locate data/default.ttf\n";
4220 fps_counter_enabled = false;
4221 return;
4222 }
4223 fps_counter_font.reset(default_font_path, 18);
4225 }
4226
4228 const auto now = std::chrono::steady_clock::now();
4229 const double elapsed = std::chrono::duration<double>(now - fps_counter_sample_time).count();
4230 if (elapsed >= 0.5) {
4231 const double fps = static_cast<double>(fps_counter_frame_count) / elapsed;
4234 fps_counter_text = std::format("FPS: {:.1f}", fps);
4235 }
4236
4237 printPreviewText(fps_counter_text, 12, 10, SDL_Color{255, 255, 255, 255}, fps_counter_font);
4238 }
4239
4240 void VK_Window::createTextDescriptorSetLayout() {
4241 if (device == VK_NULL_HANDLE || text_descriptor_set_layout != VK_NULL_HANDLE) {
4242 return;
4243 }
4244
4245 VkDescriptorSetLayoutBinding sampler_binding{};
4246 sampler_binding.binding = 0;
4247 sampler_binding.descriptorCount = 1;
4248 sampler_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
4249 sampler_binding.pImmutableSamplers = nullptr;
4250 sampler_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
4251
4252 VkDescriptorSetLayoutCreateInfo layout_info{};
4253 layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
4254 layout_info.bindingCount = 1;
4255 layout_info.pBindings = &sampler_binding;
4256
4257 if (vkCreateDescriptorSetLayout(device, &layout_info, nullptr, &text_descriptor_set_layout) != VK_SUCCESS) {
4258 throw mxvk::Exception("Failed to create text descriptor set layout");
4259 }
4260 }
4261
4262 void VK_Window::destroyTextPipeline() {
4263 if (device == VK_NULL_HANDLE) {
4264 text_pipeline = VK_NULL_HANDLE;
4265 text_pipeline_layout = VK_NULL_HANDLE;
4266 return;
4267 }
4268
4269 if (text_pipeline != VK_NULL_HANDLE) {
4270 std::cout << "vk: destroying text pipeline\n";
4271 vkDestroyPipeline(device, text_pipeline, nullptr);
4272 text_pipeline = VK_NULL_HANDLE;
4273 }
4274 if (text_pipeline_layout != VK_NULL_HANDLE) {
4275 std::cout << "vk: destroying text pipeline layout\n";
4276 vkDestroyPipelineLayout(device, text_pipeline_layout, nullptr);
4277 text_pipeline_layout = VK_NULL_HANDLE;
4278 }
4279 }
4280
4281 void VK_Window::createTextPipeline() {
4282 if (device == VK_NULL_HANDLE || swapchain_format == VK_FORMAT_UNDEFINED || text_descriptor_set_layout == VK_NULL_HANDLE) {
4283 return;
4284 }
4285
4286 destroyTextPipeline();
4287
4288 const std::vector<char> vert_shader = loadSpv(resolveRuntimeShaderPath("text.vert.spv", MXVK_TEXT_SHADER_DIR));
4289 const std::vector<char> frag_shader = loadSpv(resolveRuntimeShaderPath("text.frag.spv", MXVK_TEXT_SHADER_DIR));
4290
4291 const VkShaderModule vert_module = createShaderModule(device, vert_shader);
4292 VkShaderModule frag_module = VK_NULL_HANDLE;
4293
4294 try {
4295 frag_module = createShaderModule(device, frag_shader);
4296
4297 VkPipelineShaderStageCreateInfo vert_stage{};
4298 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
4299 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
4300 vert_stage.module = vert_module;
4301 vert_stage.pName = "main";
4302
4303 VkPipelineShaderStageCreateInfo frag_stage{};
4304 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
4305 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
4306 frag_stage.module = frag_module;
4307 frag_stage.pName = "main";
4308
4309 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
4310
4311 VkVertexInputBindingDescription binding_description{};
4312 binding_description.binding = 0;
4313 binding_description.stride = sizeof(float) * 4;
4314 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
4315
4316 std::array<VkVertexInputAttributeDescription, 2> attributes{};
4317 attributes[0].binding = 0;
4318 attributes[0].location = 0;
4319 attributes[0].format = VK_FORMAT_R32G32_SFLOAT;
4320 attributes[0].offset = 0;
4321 attributes[1].binding = 0;
4322 attributes[1].location = 1;
4323 attributes[1].format = VK_FORMAT_R32G32_SFLOAT;
4324 attributes[1].offset = sizeof(float) * 2;
4325
4326 VkPipelineVertexInputStateCreateInfo vertex_input{};
4327 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
4328 vertex_input.vertexBindingDescriptionCount = 1;
4329 vertex_input.pVertexBindingDescriptions = &binding_description;
4330 vertex_input.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributes.size());
4331 vertex_input.pVertexAttributeDescriptions = attributes.data();
4332
4333 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
4334 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
4335 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
4336 input_assembly.primitiveRestartEnable = VK_FALSE;
4337
4338 const std::array<VkDynamicState, 2> dynamic_states = {
4339 VK_DYNAMIC_STATE_VIEWPORT,
4340 VK_DYNAMIC_STATE_SCISSOR,
4341 };
4342 VkPipelineDynamicStateCreateInfo dynamic_state{};
4343 dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
4344 dynamic_state.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
4345 dynamic_state.pDynamicStates = dynamic_states.data();
4346
4347 VkPipelineViewportStateCreateInfo viewport_state{};
4348 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
4349 viewport_state.viewportCount = 1;
4350 viewport_state.scissorCount = 1;
4351
4352 VkPipelineRasterizationStateCreateInfo rasterizer{};
4353 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
4354 rasterizer.depthClampEnable = VK_FALSE;
4355 rasterizer.rasterizerDiscardEnable = VK_FALSE;
4356 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
4357 rasterizer.lineWidth = 1.0F;
4358 rasterizer.cullMode = VK_CULL_MODE_NONE;
4359 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
4360 rasterizer.depthBiasEnable = VK_FALSE;
4361
4362 VkPipelineMultisampleStateCreateInfo multisampling{};
4363 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
4364 multisampling.sampleShadingEnable = VK_FALSE;
4365 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
4366
4367 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
4368 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
4369 depth_stencil.depthTestEnable = VK_FALSE;
4370 depth_stencil.depthWriteEnable = VK_FALSE;
4371
4372 VkPipelineColorBlendAttachmentState color_attachment{};
4373 color_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
4374 color_attachment.blendEnable = VK_TRUE;
4375 color_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
4376 color_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
4377 color_attachment.colorBlendOp = VK_BLEND_OP_ADD;
4378 color_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
4379 color_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
4380 color_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
4381
4382 VkPipelineColorBlendStateCreateInfo color_blending{};
4383 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
4384 color_blending.logicOpEnable = VK_FALSE;
4385 color_blending.attachmentCount = 1;
4386 color_blending.pAttachments = &color_attachment;
4387
4388 VkPushConstantRange push_constant_range{};
4389 push_constant_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
4390 push_constant_range.offset = 0;
4391 push_constant_range.size = sizeof(float) * 4;
4392
4393 VkPipelineLayoutCreateInfo pipeline_layout_info{};
4394 pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
4395 pipeline_layout_info.setLayoutCount = 1;
4396 pipeline_layout_info.pSetLayouts = &text_descriptor_set_layout;
4397 pipeline_layout_info.pushConstantRangeCount = 1;
4398 pipeline_layout_info.pPushConstantRanges = &push_constant_range;
4399
4400 if (vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &text_pipeline_layout) != VK_SUCCESS) {
4401 throw mxvk::Exception("Failed to create text pipeline layout");
4402 }
4403
4404 VkPipelineRenderingCreateInfo rendering_info{};
4405 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
4406 rendering_info.viewMask = 0;
4407 rendering_info.colorAttachmentCount = 1;
4408 rendering_info.pColorAttachmentFormats = &swapchain_format;
4409 if (depth_format != VK_FORMAT_UNDEFINED) {
4410 rendering_info.depthAttachmentFormat = depth_format;
4411 }
4412
4413 VkGraphicsPipelineCreateInfo pipeline_info{};
4414 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
4415 pipeline_info.pNext = &rendering_info;
4416 pipeline_info.stageCount = 2;
4417 pipeline_info.pStages = shader_stages;
4418 pipeline_info.pVertexInputState = &vertex_input;
4419 pipeline_info.pInputAssemblyState = &input_assembly;
4420 pipeline_info.pViewportState = &viewport_state;
4421 pipeline_info.pRasterizationState = &rasterizer;
4422 pipeline_info.pMultisampleState = &multisampling;
4423 pipeline_info.pDepthStencilState = &depth_stencil;
4424 pipeline_info.pColorBlendState = &color_blending;
4425 pipeline_info.pDynamicState = &dynamic_state;
4426 pipeline_info.layout = text_pipeline_layout;
4427 pipeline_info.renderPass = VK_NULL_HANDLE;
4428 pipeline_info.subpass = 0;
4429 pipeline_info.basePipelineHandle = VK_NULL_HANDLE;
4430 pipeline_info.basePipelineIndex = -1;
4431
4432 if (vkCreateGraphicsPipelines(device, pipeline_cache, 1, &pipeline_info, nullptr, &text_pipeline) != VK_SUCCESS) {
4433 throw mxvk::Exception("Failed to create text graphics pipeline");
4434 }
4435 } catch (...) {
4436 if (text_pipeline != VK_NULL_HANDLE) {
4437 vkDestroyPipeline(device, text_pipeline, nullptr);
4438 text_pipeline = VK_NULL_HANDLE;
4439 }
4440 if (text_pipeline_layout != VK_NULL_HANDLE) {
4441 vkDestroyPipelineLayout(device, text_pipeline_layout, nullptr);
4442 text_pipeline_layout = VK_NULL_HANDLE;
4443 }
4444 if (frag_module != VK_NULL_HANDLE) {
4445 vkDestroyShaderModule(device, frag_module, nullptr);
4446 }
4447 vkDestroyShaderModule(device, vert_module, nullptr);
4448 throw;
4449 }
4450
4451 vkDestroyShaderModule(device, frag_module, nullptr);
4452 vkDestroyShaderModule(device, vert_module, nullptr);
4453 }
4454
4455 VK_Sprite *VK_Window::createSprite(const std::string &pngPath, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
4456 if (device == VK_NULL_HANDLE) {
4457 throw mxvk::Exception("Cannot create sprite before Vulkan device initialization");
4458 }
4459 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4460 createDevice();
4461 }
4462 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4463 throw mxvk::Exception("Cannot create sprite before swapchain and command resources are available");
4464 }
4465
4466 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
4467 createSpriteDescriptorSetLayout();
4468 }
4469
4470 auto sprite = std::make_unique<VK_Sprite>(device, physical_device, graphics_queue, command_pool);
4475
4476 if (!vertexShaderPath.empty()) {
4477 sprite->setVertexShaderPath(vertexShaderPath);
4478 }
4479
4480 sprite->loadSprite(pngPath, fragmentShaderPath);
4481
4482 VK_Sprite *const sprite_ptr = sprite.get();
4483 sprites.push_back(std::move(sprite));
4484 sprite_state_dirty = true;
4485 return sprite_ptr;
4486 }
4487
4488 VK_Sprite *VK_Window::createSprite(SDL_Surface *surface, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
4489 if (surface == nullptr) {
4490 throw mxvk::Exception("Cannot create sprite from a null SDL surface");
4491 }
4492 if (device == VK_NULL_HANDLE) {
4493 throw mxvk::Exception("Cannot create sprite before Vulkan device initialization");
4494 }
4495 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4496 createDevice();
4497 }
4498 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4499 throw mxvk::Exception("Cannot create sprite before swapchain and command resources are available");
4500 }
4501
4502 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
4503 createSpriteDescriptorSetLayout();
4504 }
4505
4506 auto sprite = std::make_unique<VK_Sprite>(device, physical_device, graphics_queue, command_pool);
4511
4512 if (!vertexShaderPath.empty()) {
4513 sprite->setVertexShaderPath(vertexShaderPath);
4514 }
4515 if (!fragmentShaderPath.empty()) {
4516 sprite->enableExtendedUBO();
4517 }
4518
4519 sprite->loadSprite(surface, fragmentShaderPath);
4520
4521 VK_Sprite *const sprite_ptr = sprite.get();
4522 sprites.push_back(std::move(sprite));
4523 sprite_state_dirty = true;
4524 return sprite_ptr;
4525 }
4526
4527 VK_Sprite *VK_Window::createSprite(int width, int height, const std::string &vertexShaderPath, const std::string &fragmentShaderPath, uint32_t spectrumBinCount) { return createSprite(width, height, vertexShaderPath, fragmentShaderPath, spectrumBinCount, 0); }
4528
4529 VK_Sprite *VK_Window::createSprite(int width, int height, const std::string &vertexShaderPath, const std::string &fragmentShaderPath, uint32_t spectrumBinCount, uint32_t spectrumHistoryLayerCount) {
4530 if (width <= 0 || height <= 0) {
4531 throw mxvk::Exception("Sprite dimensions must be positive");
4532 }
4533 if (device == VK_NULL_HANDLE) {
4534 throw mxvk::Exception("Cannot create sprite before Vulkan device initialization");
4535 }
4536 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4537 createDevice();
4538 }
4539 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4540 throw mxvk::Exception("Cannot create sprite before swapchain and command resources are available");
4541 }
4542
4543 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
4544 createSpriteDescriptorSetLayout();
4545 }
4546
4547 auto sprite = std::make_unique<VK_Sprite>(device, physical_device, graphics_queue, command_pool);
4552 if (!fragmentShaderPath.empty()) {
4553 sprite->enableExtendedUBO();
4554 }
4555 if (spectrumBinCount > 0) {
4556 sprite->enableSpectrumTexture(spectrumBinCount);
4557 }
4558 if (spectrumHistoryLayerCount > 0) {
4559 if (spectrumBinCount == 0) {
4560 throw mxvk::Exception("Spectrum history requires a positive spectrum bin count");
4561 }
4562 sprite->enableSpectrumHistoryTexture(spectrumBinCount, spectrumHistoryLayerCount);
4563 }
4564
4565 sprite->createEmptySprite(width, height, vertexShaderPath, fragmentShaderPath);
4566
4567 VK_Sprite *const sprite_ptr = sprite.get();
4568 sprites.push_back(std::move(sprite));
4569 sprite_state_dirty = true;
4570 return sprite_ptr;
4571 }
4572
4573 VK_Sprite3D *VK_Window::createSprite3D(const std::string &pngPath, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
4574 if (device == VK_NULL_HANDLE) {
4575 throw mxvk::Exception("Cannot create 3D sprite before Vulkan device initialization");
4576 }
4577 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4578 createDevice();
4579 }
4580 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4581 throw mxvk::Exception("Cannot create 3D sprite before swapchain and command resources are available");
4582 }
4583
4584 const std::string vertPath = vertexShaderPath.empty() ? resolveRuntimeShaderPath("sprite3d.vert.spv", MXVK_SPRITE3D_SHADER_DIR) : vertexShaderPath;
4585 const std::string fragPath = fragmentShaderPath.empty() ? resolveRuntimeShaderPath("sprite3d.frag.spv", MXVK_SPRITE3D_SHADER_DIR) : fragmentShaderPath;
4586
4587 auto sprite = std::make_unique<VK_Sprite3D>();
4588 sprite->load(this, pngPath, vertPath, fragPath);
4589
4590 VK_Sprite3D *const sprite_ptr = sprite.get();
4591 sprites3d.push_back(std::move(sprite));
4592 return sprite_ptr;
4593 }
4594
4595 VK_Sprite3D *VK_Window::createSprite3D(SDL_Surface *surface, const std::string &vertexShaderPath, const std::string &fragmentShaderPath) {
4596 if (surface == nullptr) {
4597 throw mxvk::Exception("Cannot create 3D sprite from a null SDL surface");
4598 }
4599 if (device == VK_NULL_HANDLE) {
4600 throw mxvk::Exception("Cannot create 3D sprite before Vulkan device initialization");
4601 }
4602 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4603 createDevice();
4604 }
4605 if (!renderTargetsReady() || command_pool == VK_NULL_HANDLE) {
4606 throw mxvk::Exception("Cannot create 3D sprite before swapchain and command resources are available");
4607 }
4608
4609 const std::string vertPath = vertexShaderPath.empty() ? resolveRuntimeShaderPath("sprite3d.vert.spv", MXVK_SPRITE3D_SHADER_DIR) : vertexShaderPath;
4610 const std::string fragPath = fragmentShaderPath.empty() ? resolveRuntimeShaderPath("sprite3d.frag.spv", MXVK_SPRITE3D_SHADER_DIR) : fragmentShaderPath;
4611
4612 auto sprite = std::make_unique<VK_Sprite3D>();
4613 sprite->load(this, surface, vertPath, fragPath);
4614
4615 VK_Sprite3D *const sprite_ptr = sprite.get();
4616 sprites3d.push_back(std::move(sprite));
4617 return sprite_ptr;
4618 }
4619
4621 if (on)
4622 SDL_ShowCursor();
4623 else
4624 SDL_HideCursor();
4625 }
4626
4627 void VK_Window::createSpriteDescriptorSetLayout() {
4628 if (device == VK_NULL_HANDLE || sprite_descriptor_set_layout != VK_NULL_HANDLE) {
4629 return;
4630 }
4631
4632 VkDescriptorSetLayoutBinding sampler_binding{};
4633 sampler_binding.binding = 0;
4634 sampler_binding.descriptorCount = 1;
4635 sampler_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
4636 sampler_binding.pImmutableSamplers = nullptr;
4637 sampler_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
4638
4639 VkDescriptorSetLayoutCreateInfo layout_info{};
4640 layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
4641 layout_info.bindingCount = 1;
4642 layout_info.pBindings = &sampler_binding;
4643
4644 if (vkCreateDescriptorSetLayout(device, &layout_info, nullptr, &sprite_descriptor_set_layout) != VK_SUCCESS) {
4645 throw mxvk::Exception("Failed to create sprite descriptor set layout");
4646 }
4647 }
4648
4649 void VK_Window::destroySpritePipeline() {
4650 if (device == VK_NULL_HANDLE) {
4651 sprite_pipeline = VK_NULL_HANDLE;
4652 sprite_pipeline_layout = VK_NULL_HANDLE;
4653 return;
4654 }
4655
4656 if (sprite_pipeline != VK_NULL_HANDLE) {
4657 std::cout << "vk: destroying sprite pipeline\n";
4658 vkDestroyPipeline(device, sprite_pipeline, nullptr);
4659 sprite_pipeline = VK_NULL_HANDLE;
4660 }
4661 if (sprite_pipeline_layout != VK_NULL_HANDLE) {
4662 std::cout << "vk: destroying sprite pipeline layout\n";
4663 vkDestroyPipelineLayout(device, sprite_pipeline_layout, nullptr);
4664 sprite_pipeline_layout = VK_NULL_HANDLE;
4665 }
4666 }
4667
4668 void VK_Window::createSpritePipeline() {
4669 if (device == VK_NULL_HANDLE || swapchain_format == VK_FORMAT_UNDEFINED) {
4670 return;
4671 }
4672 if (sprite_descriptor_set_layout == VK_NULL_HANDLE) {
4673 createSpriteDescriptorSetLayout();
4674 }
4675
4676 destroySpritePipeline();
4677
4678 const std::vector<char> vert_shader = loadSpv(resolveRuntimeShaderPath("sprite.vert.spv", MXVK_SPRITE_SHADER_DIR));
4679 const std::vector<char> frag_shader = loadSpv(resolveRuntimeShaderPath("sprite.frag.spv", MXVK_SPRITE_SHADER_DIR));
4680
4681 const VkShaderModule vert_module = createShaderModule(device, vert_shader);
4682 VkShaderModule frag_module = VK_NULL_HANDLE;
4683
4684 try {
4685 frag_module = createShaderModule(device, frag_shader);
4686
4687 VkPipelineShaderStageCreateInfo vert_stage{};
4688 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
4689 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
4690 vert_stage.module = vert_module;
4691 vert_stage.pName = "main";
4692
4693 VkPipelineShaderStageCreateInfo frag_stage{};
4694 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
4695 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
4696 frag_stage.module = frag_module;
4697 frag_stage.pName = "main";
4698
4699 const VkPipelineShaderStageCreateInfo shader_stages[] = {vert_stage, frag_stage};
4700
4701 VkVertexInputBindingDescription binding_description{};
4702 binding_description.binding = 0;
4703 binding_description.stride = sizeof(float) * 4;
4704 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
4705
4706 std::array<VkVertexInputAttributeDescription, 2> attributes{};
4707 attributes[0].binding = 0;
4708 attributes[0].location = 0;
4709 attributes[0].format = VK_FORMAT_R32G32_SFLOAT;
4710 attributes[0].offset = 0;
4711 attributes[1].binding = 0;
4712 attributes[1].location = 1;
4713 attributes[1].format = VK_FORMAT_R32G32_SFLOAT;
4714 attributes[1].offset = sizeof(float) * 2;
4715
4716 VkPipelineVertexInputStateCreateInfo vertex_input{};
4717 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
4718 vertex_input.vertexBindingDescriptionCount = 1;
4719 vertex_input.pVertexBindingDescriptions = &binding_description;
4720 vertex_input.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributes.size());
4721 vertex_input.pVertexAttributeDescriptions = attributes.data();
4722
4723 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
4724 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
4725 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
4726 input_assembly.primitiveRestartEnable = VK_FALSE;
4727
4728 const std::array<VkDynamicState, 2> dynamic_states = {
4729 VK_DYNAMIC_STATE_VIEWPORT,
4730 VK_DYNAMIC_STATE_SCISSOR,
4731 };
4732 VkPipelineDynamicStateCreateInfo dynamic_state{};
4733 dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
4734 dynamic_state.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
4735 dynamic_state.pDynamicStates = dynamic_states.data();
4736
4737 VkPipelineViewportStateCreateInfo viewport_state{};
4738 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
4739 viewport_state.viewportCount = 1;
4740 viewport_state.scissorCount = 1;
4741
4742 VkPipelineRasterizationStateCreateInfo rasterizer{};
4743 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
4744 rasterizer.depthClampEnable = VK_FALSE;
4745 rasterizer.rasterizerDiscardEnable = VK_FALSE;
4746 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
4747 rasterizer.lineWidth = 1.0F;
4748 rasterizer.cullMode = VK_CULL_MODE_NONE;
4749 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
4750 rasterizer.depthBiasEnable = VK_FALSE;
4751
4752 VkPipelineMultisampleStateCreateInfo multisampling{};
4753 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
4754 multisampling.sampleShadingEnable = VK_FALSE;
4755 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
4756
4757 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
4758 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
4759 depth_stencil.depthTestEnable = VK_FALSE;
4760 depth_stencil.depthWriteEnable = VK_FALSE;
4761
4762 VkPipelineColorBlendAttachmentState color_attachment{};
4763 color_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
4764 color_attachment.blendEnable = VK_TRUE;
4765 color_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
4766 color_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
4767 color_attachment.colorBlendOp = VK_BLEND_OP_ADD;
4768 color_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
4769 color_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
4770 color_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
4771
4772 VkPipelineColorBlendStateCreateInfo color_blending{};
4773 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
4774 color_blending.logicOpEnable = VK_FALSE;
4775 color_blending.attachmentCount = 1;
4776 color_blending.pAttachments = &color_attachment;
4777
4778 VkPushConstantRange push_constant_range{};
4779 push_constant_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
4780 push_constant_range.offset = 0;
4781 push_constant_range.size = sizeof(float) * 12;
4782
4783 VkPipelineLayoutCreateInfo pipeline_layout_info{};
4784 pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
4785 pipeline_layout_info.setLayoutCount = 1;
4786 pipeline_layout_info.pSetLayouts = &sprite_descriptor_set_layout;
4787 pipeline_layout_info.pushConstantRangeCount = 1;
4788 pipeline_layout_info.pPushConstantRanges = &push_constant_range;
4789
4790 if (vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, &sprite_pipeline_layout) != VK_SUCCESS) {
4791 throw mxvk::Exception("Failed to create sprite pipeline layout");
4792 }
4793
4794 VkPipelineRenderingCreateInfo rendering_info{};
4795 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
4796 rendering_info.viewMask = 0;
4797 rendering_info.colorAttachmentCount = 1;
4798 rendering_info.pColorAttachmentFormats = &swapchain_format;
4799 if (depth_format != VK_FORMAT_UNDEFINED) {
4800 rendering_info.depthAttachmentFormat = depth_format;
4801 }
4802
4803 VkGraphicsPipelineCreateInfo pipeline_info{};
4804 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
4805 pipeline_info.pNext = &rendering_info;
4806 pipeline_info.stageCount = 2;
4807 pipeline_info.pStages = shader_stages;
4808 pipeline_info.pVertexInputState = &vertex_input;
4809 pipeline_info.pInputAssemblyState = &input_assembly;
4810 pipeline_info.pViewportState = &viewport_state;
4811 pipeline_info.pRasterizationState = &rasterizer;
4812 pipeline_info.pMultisampleState = &multisampling;
4813 pipeline_info.pDepthStencilState = &depth_stencil;
4814 pipeline_info.pColorBlendState = &color_blending;
4815 pipeline_info.pDynamicState = &dynamic_state;
4816 pipeline_info.layout = sprite_pipeline_layout;
4817 pipeline_info.renderPass = VK_NULL_HANDLE;
4818 pipeline_info.subpass = 0;
4819 pipeline_info.basePipelineHandle = VK_NULL_HANDLE;
4820 pipeline_info.basePipelineIndex = -1;
4821
4822 if (vkCreateGraphicsPipelines(device, pipeline_cache, 1, &pipeline_info, nullptr, &sprite_pipeline) != VK_SUCCESS) {
4823 throw mxvk::Exception("Failed to create sprite graphics pipeline");
4824 }
4825 } catch (...) {
4826 if (sprite_pipeline != VK_NULL_HANDLE) {
4827 vkDestroyPipeline(device, sprite_pipeline, nullptr);
4828 sprite_pipeline = VK_NULL_HANDLE;
4829 }
4830 if (sprite_pipeline_layout != VK_NULL_HANDLE) {
4831 vkDestroyPipelineLayout(device, sprite_pipeline_layout, nullptr);
4832 sprite_pipeline_layout = VK_NULL_HANDLE;
4833 }
4834 if (frag_module != VK_NULL_HANDLE) {
4835 vkDestroyShaderModule(device, frag_module, nullptr);
4836 }
4837 vkDestroyShaderModule(device, vert_module, nullptr);
4838 throw;
4839 }
4840
4841 vkDestroyShaderModule(device, frag_module, nullptr);
4842 vkDestroyShaderModule(device, vert_module, nullptr);
4843 }
4844} // 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
void reset()
Definition mxvk_text.cpp:27
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 shareHistoryTexture(const VK_Sprite &source)
Bind another sprite's history array without taking ownership.
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 enableComputeShader(const std::string &path, uint32_t localSizeX, uint32_t localSizeY, uint32_t localSizeZ=1)
Build a compute image-effect pipeline for this sprite.
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 enableExtendedUBO()
Allocate and initialise the extended uniform buffer object.
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.
uint32_t enableSpectrumHistoryTexture(uint32_t bins, uint32_t layers)
Allocate a shader-readable FFT spectrum-history array.
void setExternalTexture(VkImageView image_view, int width, int height)
void setPipelineCache(VkPipelineCache cache)
Use the shared pipeline cache for custom/instanced pipeline creation.
void setFragmentShaderPath(const std::string &path)
Replace the fragment shader path and rebuild the custom pipeline.
void clearQueue()
Discard all pending draw commands without rendering.
void enableSpectrumTexture(uint32_t bins)
Allocate a shader-readable 1-D floating-point spectrum texture.
void rebuildPipeline()
Destroy and recreate the custom graphics pipeline.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
std::vector< ShaderStage > post_process_effect_stages
Definition mxvk.hpp:695
VkPipelineLayout sprite_pipeline_layout
Definition mxvk.hpp:680
bool post_process_enabled
Definition mxvk.hpp:685
bool force_swapchain_recreate
Definition mxvk.hpp:671
std::mutex screenshot_queue_mutex
Definition mxvk.hpp:650
void showCursor(bool on)
Definition mxvk.cpp:4620
VkSwapchainKHR swapchain
Definition mxvk.hpp:613
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:841
virtual void event(SDL_Event &e)
Handle one SDL event.
Definition mxvk.cpp:626
virtual ~VK_Window()
Destroy owned Vulkan and SDL resources.
Definition mxvk.cpp:242
void pickDevice()
Pick a suitable Vulkan physical device.
Definition mxvk.cpp:1808
void dispatchFrameReadback(FrameReadbackSlot &slot)
Definition mxvk.cpp:1095
static constexpr std::chrono::seconds MEMORY_TRIM_INTERVAL
Definition mxvk.hpp:675
VkDescriptorSetLayout sprite_descriptor_set_layout
Definition mxvk.hpp:679
std::vector< std::vector< bool > > post_process_initialized
Definition mxvk.hpp:702
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
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:4573
std::condition_variable screenshot_queue_cv
Definition mxvk.hpp:651
VkDevice getDevice() const noexcept
Get the Vulkan logical device handle.
Definition mxvk.hpp:198
VkDebugUtilsMessengerEXT debug_messenger
Definition mxvk.hpp:603
VkDevice device
Definition mxvk.hpp:606
static VkBool32 debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT *callback_data, void *user_data)
Definition mxvk.cpp:67
std::chrono::steady_clock::time_point fps_counter_sample_time
Definition mxvk.hpp:719
uint32_t screenshot_index
Definition mxvk.hpp:643
void createLogicalDevice()
Create a logical Vulkan device from the selected physical device.
Definition mxvk.cpp:1898
uint32_t fps_counter_frame_count
Definition mxvk.hpp:720
virtual bool initVulkan(bool validiation)
Initialize Vulkan state.
Definition mxvk.cpp:365
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:4455
VkClearColorValue clear_color
Definition mxvk.hpp:722
VkQueue getGraphicsQueue() const noexcept
Get the graphics queue handle.
Definition mxvk.hpp:204
std::string screenshot_prefix
Definition mxvk.hpp:642
virtual void onSwapchainRecreated()
Called after swapchain and render resources are recreated.
Definition mxvk.cpp:1382
VkInstance instance
Definition mxvk.hpp:602
VkFormat swapchain_format
Definition mxvk.hpp:614
VkCommandPool getCommandPool() const noexcept
Get the command pool used for graphics/upload work.
Definition mxvk.hpp:207
VkExtent2D swapchain_extent
Definition mxvk.hpp:616
virtual void onSwapchainAboutToRecreate()
Called right before swapchain-dependent resources are recreated.
Definition mxvk.cpp:1380
std::vector< std::uint16_t > latest_frame_readback_rgba16
Definition mxvk.hpp:659
std::vector< bool > post_process_effect_time_enabled
Definition mxvk.hpp:697
VulkanContext context() const
Definition mxvk.cpp:80
bool vulkan_library_loaded
Definition mxvk.hpp:639
bool validation_enabled
Definition mxvk.hpp:665
VkExtent2D requested_headless_extent
Definition mxvk.hpp:667
bool screenshot_worker_stop
Definition mxvk.hpp:654
std::vector< std::array< float, 4 > > post_process_effect_params
Definition mxvk.hpp:696
std::vector< std::vector< VkDeviceMemory > > post_process_memories
Definition mxvk.hpp:700
VkDescriptorSetLayout text_descriptor_set_layout
Definition mxvk.hpp:708
static constexpr uint64_t resize_settle_delay_ms
Definition mxvk.hpp:673
std::vector< std::vector< VkImageView > > post_process_views
Definition mxvk.hpp:701
VkPipeline text_pipeline
Definition mxvk.hpp:710
std::unique_ptr< VK_Text > preview_text_renderer
Definition mxvk.hpp:707
std::chrono::steady_clock::time_point post_process_start_time
Definition mxvk.hpp:689
VkFormat depth_format
Definition mxvk.hpp:615
uint32_t present_queue_family
Definition mxvk.hpp:608
void enablePostProcessing(VK_Sprite *sprite)
Definition mxvk.cpp:1635
uint32_t last_presented_image_index
Definition mxvk.hpp:635
std::vector< VkImageView > swapchain_image_views
Definition mxvk.hpp:620
std::vector< VK_Sprite * > owned_post_process_sprites
Definition mxvk.hpp:691
static VkShaderModule createShaderModule(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
Definition mxvk.cpp:154
std::chrono::steady_clock::time_point last_memory_trim_time
Definition mxvk.hpp:674
RuntimeMode runtime_mode
Definition mxvk.hpp:666
void setRenderExtent(uint32_t width, uint32_t height)
Render post-processing at a fixed extent independent of the preview window.
Definition mxvk.cpp:1406
VkQueue present_queue
Definition mxvk.hpp:610
void ensureFrameReadbackResources()
Definition mxvk.cpp:1162
void createDevice()
Create final device resources.
Definition mxvk.cpp:2028
bool text_state_dirty
Definition mxvk.hpp:711
Font fps_counter_font
Definition mxvk.hpp:718
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4004
std::vector< bool > swapchain_image_initialized
Definition mxvk.hpp:621
std::vector< VkImage > swapchain_images
Definition mxvk.hpp:618
std::vector< VkFence > image_fences
Definition mxvk.hpp:633
std::string font_path
Definition mxvk.hpp:714
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:3964
void flushFrameReadbacks()
Wait for and deliver all pending pipelined frame readbacks.
Definition mxvk.cpp:1143
std::vector< VkSwapchainKHR > retired_swapchains
Definition mxvk.hpp:723
std::vector< VkImageView > depth_image_views
Definition mxvk.hpp:624
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:1390
bool post_process_time_enabled
Definition mxvk.hpp:687
std::unique_ptr< VK_Text > text_renderer
Definition mxvk.hpp:706
bool screenshot_enabled
Definition mxvk.hpp:641
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
Definition mxvk.cpp:826
uint32_t graphics_queue_family
Definition mxvk.hpp:607
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
bool preview_text_queued
Definition mxvk.hpp:712
std::vector< VK_Sprite * > attachPostProcessingShaders(const std::vector< PostProcessingEffect > &effects)
Definition mxvk.cpp:1474
virtual void onPrepareFrameRendering(VkCommandBuffer cmd, uint32_t image_index)
Record resource transitions that must happen before dynamic rendering begins.
Definition mxvk.cpp:1384
std::vector< VkCommandBuffer > command_buffers
Definition mxvk.hpp:628
void renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd)
Render one standalone sprite using the window's shared sprite pipeline.
Definition mxvk.cpp:1392
std::thread screenshot_worker
Definition mxvk.hpp:653
uint32_t current_frame
Definition mxvk.hpp:634
std::vector< VkSemaphore > render_finished
Definition mxvk.hpp:631
void setHdrRenderIntermediatesEnabled(bool enabled)
Select RGBA16F scene/effect intermediates while retaining the presentation format.
Definition mxvk.cpp:1453
VkPipelineCache pipeline_cache
Definition mxvk.hpp:611
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:601
bool sdl_initialized
Definition mxvk.hpp:638
VK_Sprite * post_process_composite_sprite
Definition mxvk.hpp:694
std::vector< std::chrono::steady_clock::time_point > post_process_effect_start_times
Definition mxvk.hpp:698
VkPipeline sprite_pipeline
Definition mxvk.hpp:681
void exit()
Request loop termination.
Definition mxvk.cpp:1378
virtual void onRecordPostProcessingTexture(VkCommandBuffer cmd, uint32_t image_index, VkImageView texture_view, VkExtent2D texture_extent)
Render a completed post-processing chain as custom scene input.
Definition mxvk.cpp:1388
VkCommandPool command_pool
Definition mxvk.hpp:627
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:1469
uint32_t latest_frame_readback_width
Definition mxvk.hpp:660
bool frame_readback_enabled
Definition mxvk.hpp:655
void printPreviewText(const std::string &text, int x, int y, const SDL_Color &col)
Queue preview-only text after frame readback.
Definition mxvk.cpp:3974
bool hdr_render_intermediates_enabled
Definition mxvk.hpp:704
bool frame_readback_rgba16_enabled
Definition mxvk.hpp:656
bool ensureRenderResources()
Ensure deferred render resources are initialized.
Definition mxvk.cpp:1787
VkSurfaceKHR surface
Definition mxvk.hpp:604
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:605
VK_Sprite * post_process_present_sprite
Definition mxvk.hpp:692
VkPipelineLayout text_pipeline_layout
Definition mxvk.hpp:709
virtual void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index)
Optional hook for derived classes to record extra draw commands.
Definition mxvk.cpp:1386
std::array< float, 4 > post_process_params
Definition mxvk.hpp:688
void detachPostProcessingShader()
Detach the current post-processing shader and return to direct swapchain rendering.
Definition mxvk.cpp:1566
std::vector< bool > depth_image_initialized
Definition mxvk.hpp:625
bool fps_counter_font_ready
Definition mxvk.hpp:717
uint32_t next_headless_image
Definition mxvk.hpp:668
std::deque< ScreenshotSaveTask > screenshot_save_queue
Definition mxvk.hpp:652
void release()
Release Vulkan and SDL resources.
Definition mxvk.cpp:247
std::array< FrameReadbackSlot, max_frames_in_flight > frame_readback_slots
Definition mxvk.hpp:657
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
std::vector< VkDeviceMemory > depth_image_memories
Definition mxvk.hpp:623
std::vector< std::uint8_t > latest_frame_readback_rgba
Definition mxvk.hpp:658
PresentModePreference present_mode_preference
Definition mxvk.hpp:669
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:3919
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:152
std::vector< std::unique_ptr< VK_Sprite3D > > sprites3d
Definition mxvk.hpp:678
VkExtent2D render_extent_override
Definition mxvk.hpp:617
virtual void onFrameReadbackRgba16(std::vector< std::uint16_t > &rgba_pixels, uint32_t width, uint32_t height)
Receive normalized RGBA16 pixels from an RGBA16F HDR target.
Definition mxvk.cpp:1091
std::vector< VK_Sprite * > post_process_sprites
Definition mxvk.hpp:690
bool sprite_state_dirty
Definition mxvk.hpp:682
void trimMemory()
Return transient Vulkan command-pool allocations to the driver when supported.
Definition mxvk.cpp:1280
virtual void onFrameReadback(std::vector< std::uint8_t > &rgba_pixels, uint32_t width, uint32_t height)
Receive one tightly packed RGBA8 copy of the rendered frame.
Definition mxvk.cpp:1089
VK_Sprite * post_process_sprite
Definition mxvk.hpp:683
bool framebuffer_resized
Definition mxvk.hpp:670
VkQueue graphics_queue
Definition mxvk.hpp:609
bool font_configured
Definition mxvk.hpp:713
uint32_t latest_frame_readback_height
Definition mxvk.hpp:661
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:1591
void destroyFrameReadbackResources()
Definition mxvk.cpp:1252
std::string fps_counter_text
Definition mxvk.hpp:721
virtual void render()
Render one frame.
Definition mxvk.cpp:824
bool initWindow(const std::string &title, int width, int height, SDL_WindowFlags flags)
Initialize SDL window resources.
Definition mxvk.cpp:1298
bool headless() const noexcept
Return true when rendering without SDL video, a surface, or presentation.
Definition mxvk.hpp:195
std::vector< VkImage > depth_images
Definition mxvk.hpp:622
std::vector< VkDeviceMemory > headless_image_memories
Definition mxvk.hpp:619
bool post_process_texture_consumer_enabled
Definition mxvk.hpp:686
void setPostProcessingPresentFragmentShader(const std::string &path)
Set an optional fragment shader used only when presenting the final post-processing texture to a wind...
Definition mxvk.cpp:1555
virtual void proc()
Execute one processing/update step.
Definition mxvk.cpp:1278
bool validationEnabled() const
Check whether Vulkan validation layers are currently enabled.
Definition mxvk.cpp:3812
virtual void onFrameReadbackScheduled()
Called after a presented frame has scheduled a readback.
Definition mxvk.cpp:1093
std::vector< std::unique_ptr< VK_Sprite > > sprites
Definition mxvk.hpp:677
uint64_t last_resize_event_ms
Definition mxvk.hpp:672
VkExtent2D getRenderExtent() const noexcept
Get the active scene/post-processing extent.
Definition mxvk.cpp:1451
VkPhysicalDevice getPhysicalDevice() const noexcept
Get the Vulkan physical device handle.
Definition mxvk.hpp:201
void setPostProcessingShaderTimeEnabled(bool enabled)
Keep shader param 1 updated with elapsed render time in seconds.
Definition mxvk.cpp:1612
std::vector< std::vector< VkImage > > post_process_images
Definition mxvk.hpp:699
void setEnableScreenshot(bool enabled) noexcept
Enable or disable F10 screenshot capture.
Definition mxvk.cpp:644
std::string post_process_present_shader_path
Definition mxvk.hpp:693
void setPreviewFont(const std::string &fontPath, int fontSize=24)
Set the preview-only HUD font independently of output text.
Definition mxvk.cpp:3902
VK_Sprite * owned_post_process_sprite
Definition mxvk.hpp:684
std::array< VkSemaphore, max_frames_in_flight > image_available
Definition mxvk.hpp:630
bool fps_counter_enabled
Definition mxvk.hpp:716
bool swapchain_supports_transfer_src
Definition mxvk.hpp:636
std::array< VkFence, max_frames_in_flight > in_flight_fences
Definition mxvk.hpp:632
VkFormat postProcessIntermediateFormat() const noexcept
Definition mxvk.hpp:703
#define MXVK_TEXT_SHADER_DIR
Definition mxvk.cpp:37
#define MXVK_SPRITE_SHADER_DIR
Definition mxvk.cpp:33
#define MXVK_DEFAULT_FONT_DIR
Definition mxvk.cpp:41
PNG image loading and saving utilities via SDL3.
float half_to_float(uint16_t value) noexcept
Definition mxvk.cpp:46
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
bool SavePNG_RGBA(const char *filename, void *buffer, int w, int h, int compression_level=6)
Save a raw RGBA pixel buffer to a PNG file.
Definition mxvk_png.cpp:279
VkShaderModule create_shader_module(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
const std::string & defaultShaderDirectory()
bool defaultEnableScreenshot()
ShaderModuleInfo inspect_spirv(const std::vector< char > &spv_bytes)
Inspect a SPIR-V entry point and compute local workgroup size.
const std::string & defaultExecutableName()
std::vector< char > load_spv(const std::string &path)
Load a SPIR-V file from disk.
StorageImageFormat storageImageFormat
std::array< float, 4 > params
Definition mxvk.hpp:310
Minimal Vulkan handles required by MXVK resource helpers.