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;