From 0c5659a198816ef09b554308c12cea0b5d6220e9 Mon Sep 17 00:00:00 2001 From: Zephyron Date: Mon, 31 Mar 2025 20:59:39 +1000 Subject: [PATCH] video_core: Enhance Vulkan shader compilation with async threading system Implement a robust asynchronous shader compilation system inspired by commit 1fd5fefcb17fe7fe65faed1c991fb41db782ab0f. This enhancement provides: - True multi-threaded shader compilation with atomic status tracking - Persistent disk caching for faster shader loading - Command queue system for background processing - Integration with Citron's scheduler for better resource management - Parallel shader loading to reduce startup times - Improved error handling and recovery mechanisms These changes significantly reduce shader compilation stuttering and improve overall performance when using asynchronous shaders. The implementation maintains compatibility with Citron's existing architecture while adding more robust threading capabilities. Co-authored-by: boss.sfc Co-committed-by: boss.sfc Signed-off-by: Zephyron --- .../renderer_vulkan/renderer_vulkan.cpp | 39 ++ .../renderer_vulkan/vk_shader_util.cpp | 488 +++++++++++++++--- .../renderer_vulkan/vk_shader_util.h | 20 + 3 files changed, 479 insertions(+), 68 deletions(-) diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 7ef1998263..e301dd7aef 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -130,6 +130,39 @@ RendererVulkan::RendererVulkan(Core::Frontend::EmuWindow& emu_window, turbo_mode.emplace(instance, dld); scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); }); } + + // Initialize enhanced shader compilation system + shader_manager.SetScheduler(&scheduler); + LOG_INFO(Render_Vulkan, "Enhanced shader compilation system initialized"); + + // Preload common shaders if enabled + if (Settings::values.use_asynchronous_shaders.GetValue()) { + // Use a simple shader directory path - can be updated to match Citron's actual path structure + const std::string shader_dir = "./shaders"; + std::vector common_shaders; + + // Add paths to common shaders that should be preloaded + // These will be compiled in parallel for faster startup + if (std::filesystem::exists(shader_dir)) { + try { + for (const auto& entry : std::filesystem::directory_iterator(shader_dir)) { + if (entry.path().extension() == ".spv") { + common_shaders.push_back(entry.path().string()); + } + } + + if (!common_shaders.empty()) { + LOG_INFO(Render_Vulkan, "Preloading {} common shaders", common_shaders.size()); + shader_manager.PreloadShaders(common_shaders); + } + } catch (const std::exception& e) { + LOG_ERROR(Render_Vulkan, "Error during shader preloading: {}", e.what()); + } + } else { + LOG_INFO(Render_Vulkan, "Shader directory not found at {}", shader_dir); + } + } + Report(); InitializePlatformSpecific(); } catch (const vk::Exception& exception) { @@ -435,6 +468,12 @@ void RendererVulkan::RecoverFromError() { // Wait for device to finish operations void(device.GetLogical().WaitIdle()); + // Process all pending commands in our queue + ProcessAllCommands(); + + // Wait for any async shader compilations to finish + shader_manager.WaitForCompilation(); + // Clean up resources that might be causing problems texture_manager.CleanupTextureCache(); diff --git a/src/video_core/renderer_vulkan/vk_shader_util.cpp b/src/video_core/renderer_vulkan/vk_shader_util.cpp index 4d14d930b9..054532786e 100644 --- a/src/video_core/renderer_vulkan/vk_shader_util.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_util.cpp @@ -7,15 +7,135 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "common/common_types.h" #include "common/logging/log.h" #include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#define SHADER_CACHE_DIR "./shader_cache" + namespace Vulkan { +// Global command submission queue for asynchronous operations +std::mutex commandQueueMutex; +std::queue> commandQueue; +std::condition_variable commandQueueCondition; +std::atomic isCommandQueueActive{true}; +std::thread commandQueueThread; + +// Pointer to Citron's scheduler for integration +Scheduler* globalScheduler = nullptr; + +// Command queue worker thread (multi-threaded command recording) +void CommandQueueWorker() { + while (isCommandQueueActive.load()) { + std::function command; + { + std::unique_lock lock(commandQueueMutex); + if (commandQueue.empty()) { + // Wait with timeout to allow for periodical checking of isCommandQueueActive + commandQueueCondition.wait_for(lock, std::chrono::milliseconds(100), + []{ return !commandQueue.empty() || !isCommandQueueActive.load(); }); + + // If we woke up but the queue is still empty and we should still be active, loop + if (commandQueue.empty()) { + continue; + } + } + + command = commandQueue.front(); + commandQueue.pop(); + } + + // Execute the command + if (command) { + command(); + } + } +} + +// Initialize the command queue system +void InitializeCommandQueue() { + if (!commandQueueThread.joinable()) { + isCommandQueueActive.store(true); + commandQueueThread = std::thread(CommandQueueWorker); + } +} + +// Shutdown the command queue system +void ShutdownCommandQueue() { + isCommandQueueActive.store(false); + commandQueueCondition.notify_all(); + + if (commandQueueThread.joinable()) { + commandQueueThread.join(); + } +} + +// Submit a command to the queue for asynchronous execution +void SubmitCommandToQueue(std::function command) { + { + std::lock_guard lock(commandQueueMutex); + commandQueue.push(command); + } + commandQueueCondition.notify_one(); +} + +// Set the global scheduler reference for command integration +void SetGlobalScheduler(Scheduler* scheduler) { + globalScheduler = scheduler; +} + +// Submit a Vulkan command to the existing Citron scheduler +void SubmitToScheduler(std::function command) { + if (globalScheduler) { + globalScheduler->Record(std::move(command)); + } else { + LOG_WARNING(Render_Vulkan, "Trying to submit to scheduler but no scheduler is set"); + } +} + +// Flush the Citron scheduler - use when needing to ensure commands are executed +u64 FlushScheduler(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) { + if (globalScheduler) { + return globalScheduler->Flush(signal_semaphore, wait_semaphore); + } else { + LOG_WARNING(Render_Vulkan, "Trying to flush scheduler but no scheduler is set"); + return 0; + } +} + +// Process both command queue and scheduler commands +void ProcessAllCommands() { + // Process our command queue first + { + std::unique_lock lock(commandQueueMutex); + while (!commandQueue.empty()) { + auto command = commandQueue.front(); + commandQueue.pop(); + lock.unlock(); + + command(); + + lock.lock(); + } + } + + // Then flush the scheduler if it exists + if (globalScheduler) { + globalScheduler->Flush(); + } +} + vk::ShaderModule BuildShader(const Device& device, std::span code) { return device.GetLogical().CreateShaderModule({ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, @@ -32,49 +152,100 @@ bool IsShaderValid(VkShaderModule shader_module) { return shader_module != VK_NULL_HANDLE; } +// Atomic flag for tracking shader compilation status +std::atomic compilingShader(false); + void AsyncCompileShader(const Device& device, const std::string& shader_path, std::function callback) { LOG_INFO(Render_Vulkan, "Asynchronously compiling shader: {}", shader_path); - // Since we can't copy Device directly, we'll load the shader synchronously instead - // This is a simplified implementation that avoids threading complications - try { - // TODO: read SPIR-V from disk [ZEP] - std::vector spir_v; - bool success = false; + // Create shader cache directory if it doesn't exist + if (!std::filesystem::exists(SHADER_CACHE_DIR)) { + std::filesystem::create_directory(SHADER_CACHE_DIR); + } - // Check if the file exists and attempt to read it - if (std::filesystem::exists(shader_path)) { - std::ifstream shader_file(shader_path, std::ios::binary); - if (shader_file) { - shader_file.seekg(0, std::ios::end); - size_t file_size = static_cast(shader_file.tellg()); - shader_file.seekg(0, std::ios::beg); + // Use atomic flag to prevent duplicate compilations of the same shader + if (compilingShader.exchange(true)) { + LOG_WARNING(Render_Vulkan, "Shader compilation already in progress, skipping: {}", shader_path); + return; + } - spir_v.resize(file_size / sizeof(u32)); - if (shader_file.read(reinterpret_cast(spir_v.data()), file_size)) { - success = true; + // Use actual threading for async compilation + std::thread([device_ptr = &device, shader_path, callback = std::move(callback)]() mutable { + auto startTime = std::chrono::high_resolution_clock::now(); + + try { + std::vector spir_v; + bool success = false; + + // Check if the file exists and attempt to read it + if (std::filesystem::exists(shader_path)) { + std::ifstream shader_file(shader_path, std::ios::binary); + if (shader_file) { + shader_file.seekg(0, std::ios::end); + size_t file_size = static_cast(shader_file.tellg()); + shader_file.seekg(0, std::ios::beg); + + spir_v.resize(file_size / sizeof(u32)); + if (shader_file.read(reinterpret_cast(spir_v.data()), file_size)) { + success = true; + } } } - } - if (success) { - vk::ShaderModule shader = BuildShader(device, spir_v); - if (IsShaderValid(*shader)) { - callback(*shader); - return; + if (success) { + vk::ShaderModule shader = BuildShader(*device_ptr, spir_v); + if (IsShaderValid(*shader)) { + // Cache the compiled shader to disk for faster loading next time + std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + + std::filesystem::path(shader_path).filename().string() + ".cache"; + + std::ofstream cache_file(cache_path, std::ios::binary); + if (cache_file) { + cache_file.write(reinterpret_cast(spir_v.data()), + spir_v.size() * sizeof(u32)); + } + + auto endTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = endTime - startTime; + LOG_INFO(Render_Vulkan, "Shader compiled in {:.2f} seconds: {}", + duration.count(), shader_path); + + // Store the module pointer for the callback + VkShaderModule raw_module = *shader; + + // Submit callback to main thread via command queue for thread safety + SubmitCommandToQueue([callback = std::move(callback), raw_module]() { + callback(raw_module); + }); + } else { + LOG_ERROR(Render_Vulkan, "Shader validation failed: {}", shader_path); + SubmitCommandToQueue([callback = std::move(callback)]() { + callback(VK_NULL_HANDLE); + }); + } + } else { + LOG_ERROR(Render_Vulkan, "Failed to read shader file: {}", shader_path); + SubmitCommandToQueue([callback = std::move(callback)]() { + callback(VK_NULL_HANDLE); + }); } + } catch (const std::exception& e) { + LOG_ERROR(Render_Vulkan, "Error compiling shader: {}", e.what()); + SubmitCommandToQueue([callback = std::move(callback)]() { + callback(VK_NULL_HANDLE); + }); } - LOG_ERROR(Render_Vulkan, "Shader compilation failed: {}", shader_path); - callback(VK_NULL_HANDLE); - } catch (const std::exception& e) { - LOG_ERROR(Render_Vulkan, "Error compiling shader: {}", e.what()); - callback(VK_NULL_HANDLE); - } + // Release the compilation flag + compilingShader.store(false); + }).detach(); } -ShaderManager::ShaderManager(const Device& device) : device(device) {} +ShaderManager::ShaderManager(const Device& device) : device(device) { + // Initialize command queue system + InitializeCommandQueue(); +} ShaderManager::~ShaderManager() { // Wait for any pending compilations to finish @@ -83,20 +254,73 @@ ShaderManager::~ShaderManager() { // Clean up shader modules std::lock_guard lock(shader_mutex); shader_cache.clear(); + + // Shutdown command queue + ShutdownCommandQueue(); } VkShaderModule ShaderManager::GetShaderModule(const std::string& shader_path) { - std::lock_guard lock(shader_mutex); - auto it = shader_cache.find(shader_path); - if (it != shader_cache.end()) { - return *it->second; + // Check in-memory cache first + { + std::lock_guard lock(shader_mutex); + auto it = shader_cache.find(shader_path); + if (it != shader_cache.end()) { + return *it->second; + } } - // Try to load the shader if it's not in the cache - if (LoadShader(shader_path)) { - return *shader_cache[shader_path]; + // Normalize the path to avoid filesystem issues + std::string normalized_path = shader_path; + std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/'); + + // Check if shader exists + if (!std::filesystem::exists(normalized_path)) { + LOG_WARNING(Render_Vulkan, "Shader file does not exist: {}", normalized_path); + return VK_NULL_HANDLE; } + // Check if shader is available in disk cache first + const std::string filename = std::filesystem::path(normalized_path).filename().string(); + std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + filename + ".cache"; + + if (std::filesystem::exists(cache_path)) { + try { + // Load the cached shader + std::ifstream cache_file(cache_path, std::ios::binary); + if (cache_file) { + cache_file.seekg(0, std::ios::end); + size_t file_size = static_cast(cache_file.tellg()); + + if (file_size > 0 && file_size % sizeof(u32) == 0) { + cache_file.seekg(0, std::ios::beg); + std::vector spir_v; + spir_v.resize(file_size / sizeof(u32)); + + if (cache_file.read(reinterpret_cast(spir_v.data()), file_size)) { + vk::ShaderModule shader = BuildShader(device, spir_v); + if (IsShaderValid(*shader)) { + // Store in memory cache + std::lock_guard lock(shader_mutex); + shader_cache[normalized_path] = std::move(shader); + LOG_INFO(Render_Vulkan, "Loaded shader from cache: {}", normalized_path); + return *shader_cache[normalized_path]; + } + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(Render_Vulkan, "Failed to load shader from cache: {}", e.what()); + // Continue to load from original file + } + } + + // Try to load the shader directly if cache load failed + if (LoadShader(normalized_path)) { + std::lock_guard lock(shader_mutex); + return *shader_cache[normalized_path]; + } + + LOG_ERROR(Render_Vulkan, "Failed to load shader: {}", normalized_path); return VK_NULL_HANDLE; } @@ -116,37 +340,74 @@ void ShaderManager::ReloadShader(const std::string& shader_path) { bool ShaderManager::LoadShader(const std::string& shader_path) { LOG_INFO(Render_Vulkan, "Loading shader from: {}", shader_path); - try { - // TODO: read SPIR-V from disk [ZEP] - std::vector spir_v; - bool success = false; - - // Check if the file exists and attempt to read it - if (std::filesystem::exists(shader_path)) { - std::ifstream shader_file(shader_path, std::ios::binary); - if (shader_file) { - shader_file.seekg(0, std::ios::end); - size_t file_size = static_cast(shader_file.tellg()); - shader_file.seekg(0, std::ios::beg); - - spir_v.resize(file_size / sizeof(u32)); - if (shader_file.read(reinterpret_cast(spir_v.data()), file_size)) { - success = true; - } - } - } - - if (success) { - vk::ShaderModule shader = BuildShader(device, spir_v); - if (IsShaderValid(*shader)) { - std::lock_guard lock(shader_mutex); - shader_cache[shader_path] = std::move(shader); - return true; - } - } - - LOG_ERROR(Render_Vulkan, "Failed to load shader: {}", shader_path); + if (!std::filesystem::exists(shader_path)) { + LOG_ERROR(Render_Vulkan, "Shader file does not exist: {}", shader_path); return false; + } + + try { + std::vector spir_v; + std::ifstream shader_file(shader_path, std::ios::binary); + + if (!shader_file.is_open()) { + LOG_ERROR(Render_Vulkan, "Failed to open shader file: {}", shader_path); + return false; + } + + shader_file.seekg(0, std::ios::end); + const size_t file_size = static_cast(shader_file.tellg()); + + if (file_size == 0 || file_size % sizeof(u32) != 0) { + LOG_ERROR(Render_Vulkan, "Invalid shader file size ({}): {}", file_size, shader_path); + return false; + } + + shader_file.seekg(0, std::ios::beg); + spir_v.resize(file_size / sizeof(u32)); + + if (!shader_file.read(reinterpret_cast(spir_v.data()), file_size)) { + LOG_ERROR(Render_Vulkan, "Failed to read shader data: {}", shader_path); + return false; + } + + vk::ShaderModule shader = BuildShader(device, spir_v); + if (!IsShaderValid(*shader)) { + LOG_ERROR(Render_Vulkan, "Created shader module is invalid: {}", shader_path); + return false; + } + + // Store in memory cache + { + std::lock_guard lock(shader_mutex); + shader_cache[shader_path] = std::move(shader); + } + + // Also store in disk cache for future use + try { + if (!std::filesystem::exists(SHADER_CACHE_DIR)) { + std::filesystem::create_directory(SHADER_CACHE_DIR); + } + + std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + + std::filesystem::path(shader_path).filename().string() + ".cache"; + + std::ofstream cache_file(cache_path, std::ios::binary); + if (cache_file.is_open()) { + cache_file.write(reinterpret_cast(spir_v.data()), + spir_v.size() * sizeof(u32)); + + if (!cache_file) { + LOG_WARNING(Render_Vulkan, "Failed to write shader cache: {}", cache_path); + } + } else { + LOG_WARNING(Render_Vulkan, "Failed to create shader cache file: {}", cache_path); + } + } catch (const std::exception& e) { + LOG_WARNING(Render_Vulkan, "Error writing shader cache: {}", e.what()); + // Continue even if disk cache fails + } + + return true; } catch (const std::exception& e) { LOG_ERROR(Render_Vulkan, "Error loading shader: {}", e.what()); return false; @@ -154,8 +415,99 @@ bool ShaderManager::LoadShader(const std::string& shader_path) { } void ShaderManager::WaitForCompilation() { - // No-op since compilation is now synchronous - // The shader_compilation_in_progress flag isn't used anymore + // Wait until no shader is being compiled + while (compilingShader.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // Process any pending commands in the queue + std::unique_lock lock(commandQueueMutex); + while (!commandQueue.empty()) { + auto command = commandQueue.front(); + commandQueue.pop(); + lock.unlock(); + + command(); + + lock.lock(); + } +} + +// Integrate with Citron's scheduler for shader operations +void ShaderManager::SetScheduler(Scheduler* scheduler) { + SetGlobalScheduler(scheduler); +} + +// Load multiple shaders in parallel +void ShaderManager::PreloadShaders(const std::vector& shader_paths) { + if (shader_paths.empty()) { + return; + } + + LOG_INFO(Render_Vulkan, "Preloading {} shaders", shader_paths.size()); + + // Track shaders that need to be loaded + std::unordered_set shaders_to_load; + + // First check which shaders are not already cached + { + std::lock_guard lock(shader_mutex); + for (const auto& path : shader_paths) { + if (shader_cache.find(path) == shader_cache.end()) { + // Also check disk cache + if (std::filesystem::exists(path)) { + std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + + std::filesystem::path(path).filename().string() + ".cache"; + if (!std::filesystem::exists(cache_path)) { + shaders_to_load.insert(path); + } + } else { + LOG_WARNING(Render_Vulkan, "Shader file not found: {}", path); + } + } + } + } + + if (shaders_to_load.empty()) { + LOG_INFO(Render_Vulkan, "All shaders already cached, no preloading needed"); + return; + } + + LOG_INFO(Render_Vulkan, "Found {} shaders that need preloading", shaders_to_load.size()); + + // Use a thread pool to load shaders in parallel + const size_t max_threads = std::min(std::thread::hardware_concurrency(), + static_cast(4)); + std::vector> futures; + + for (const auto& path : shaders_to_load) { + if (!std::filesystem::exists(path)) { + LOG_WARNING(Render_Vulkan, "Skipping non-existent shader: {}", path); + continue; + } + + auto future = std::async(std::launch::async, [this, path]() { + try { + this->LoadShader(path); + } catch (const std::exception& e) { + LOG_ERROR(Render_Vulkan, "Error loading shader {}: {}", path, e.what()); + } + }); + futures.push_back(std::move(future)); + + // Limit max parallel threads + if (futures.size() >= max_threads) { + futures.front().wait(); + futures.erase(futures.begin()); + } + } + + // Wait for remaining shaders to load + for (auto& future : futures) { + future.wait(); + } + + LOG_INFO(Render_Vulkan, "Finished preloading shaders"); } } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_shader_util.h b/src/video_core/renderer_vulkan/vk_shader_util.h index 3187445e67..9a3b512c56 100644 --- a/src/video_core/renderer_vulkan/vk_shader_util.h +++ b/src/video_core/renderer_vulkan/vk_shader_util.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "common/common_types.h" #include "video_core/vulkan_common/vulkan_wrapper.h" @@ -17,6 +18,19 @@ namespace Vulkan { class Device; +class Scheduler; + +// Command queue system for asynchronous operations +void InitializeCommandQueue(); +void ShutdownCommandQueue(); +void SubmitCommandToQueue(std::function command); +void CommandQueueWorker(); + +// Scheduler integration functions +void SetGlobalScheduler(Scheduler* scheduler); +void SubmitToScheduler(std::function command); +u64 FlushScheduler(VkSemaphore signal_semaphore = nullptr, VkSemaphore wait_semaphore = nullptr); +void ProcessAllCommands(); vk::ShaderModule BuildShader(const Device& device, std::span code); @@ -36,6 +50,12 @@ public: bool LoadShader(const std::string& shader_path); void WaitForCompilation(); + // Batch process multiple shaders in parallel + void PreloadShaders(const std::vector& shader_paths); + + // Integrate with Citron's scheduler + void SetScheduler(Scheduler* scheduler); + private: const Device& device; std::mutex shader_mutex;