Revert some wip changes

This commit is contained in:
MrPurple666 2025-04-28 15:49:28 -03:00
parent 808276b48a
commit b695ca5a2a
17 changed files with 45 additions and 1135 deletions

View file

@ -18,7 +18,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_DEBUG("debug"),
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -644,21 +643,11 @@ struct Values {
// Add-Ons
std::map<u64, std::vector<std::string>> disabled_addons;
// Renderer Advanced Settings
SwitchableSetting<bool> use_enhanced_shader_building{linkage, false, "Enhanced Shader Building",
Category::RendererAdvanced};
// Add a new setting for shader compilation priority
SwitchableSetting<int> shader_compilation_priority{linkage, 0, "Shader Compilation Priority",
Category::RendererAdvanced};
};
extern Values values;
void UpdateGPUAccuracy();
// boold isGPULevelNormal();
// TODO: ZEP
bool IsGPULevelExtreme();
bool IsGPULevelHigh();

View file

@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 Citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(host_shaders)
@ -246,8 +245,6 @@ add_library(video_core STATIC
renderer_vulkan/vk_turbo_mode.h
renderer_vulkan/vk_update_descriptor.cpp
renderer_vulkan/vk_update_descriptor.h
renderer_vulkan/vk_texture_manager.cpp
renderer_vulkan/vk_texture_manager.h
shader_cache.cpp
shader_cache.h
shader_environment.cpp

View file

@ -1,13 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <string>
#include <vector>
#include <chrono>
#include <functional>
#include "common/settings.h" // for enum class Settings::ShaderBackend
#include "common/thread_worker.h"
@ -237,68 +234,26 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
shader_notify, backend, in_parallel,
force_context_flush](ShaderContext::Context*) mutable {
// Track time for shader compilation for possible performance tuning
const auto start_time = std::chrono::high_resolution_clock::now();
// Prepare compilation steps for all shader stages
std::vector<std::function<void()>> compilation_steps;
compilation_steps.reserve(5); // Maximum number of shader stages
// Prepare all compilation steps first to better distribute work
for (size_t stage = 0; stage < 5; ++stage) {
switch (backend) {
case Settings::ShaderBackend::Glsl:
if (!sources_[stage].empty()) {
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
source_programs[stage] = CreateProgram(source, Stage(stage));
});
source_programs[stage] = CreateProgram(sources_[stage], Stage(stage));
}
break;
case Settings::ShaderBackend::Glasm:
if (!sources_[stage].empty()) {
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
assembly_programs[stage] = CompileProgram(source, AssemblyStage(stage));
});
assembly_programs[stage] =
CompileProgram(sources_[stage], AssemblyStage(stage));
}
break;
case Settings::ShaderBackend::SpirV:
if (!sources_spirv_[stage].empty()) {
compilation_steps.emplace_back([this, stage, source = sources_spirv_[stage]]() {
source_programs[stage] = CreateProgram(source, Stage(stage));
});
source_programs[stage] = CreateProgram(sources_spirv_[stage], Stage(stage));
}
break;
}
}
// If we're running in parallel, use high-priority execution for vertex and fragment shaders
// as these are typically needed first by the renderer
if (in_parallel && compilation_steps.size() > 1) {
// Execute vertex (0) and fragment (4) shaders first if they exist
for (size_t priority_stage : {0, 4}) {
for (size_t i = 0; i < compilation_steps.size(); ++i) {
if ((i == priority_stage || (priority_stage == 0 && i <= 1)) && i < compilation_steps.size()) {
compilation_steps[i]();
compilation_steps[i] = [](){}; // Mark as executed
}
}
}
}
// Execute all remaining compilation steps
for (auto& step : compilation_steps) {
step(); // Will do nothing for already executed steps
}
// Performance measurement for possible logging or optimization
const auto end_time = std::chrono::high_resolution_clock::now();
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
if (compilation_time > 50) { // Only log slow compilations
LOG_DEBUG(Render_OpenGL, "Shader compilation took {}ms", compilation_time);
}
if (force_context_flush || in_parallel) {
std::scoped_lock lock{built_mutex};
built_fence.Create();
@ -668,41 +623,15 @@ void GraphicsPipeline::WaitForBuild() {
is_built = true;
}
bool GraphicsPipeline::IsBuilt() const noexcept {
bool GraphicsPipeline::IsBuilt() noexcept {
if (is_built) {
return true;
}
if (!built_fence.handle) {
if (built_fence.handle == 0) {
return false;
}
// Check if the async build has finished by polling the fence
const GLsync sync = built_fence.handle;
const GLuint result = glClientWaitSync(sync, 0, 0);
if (result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED) {
// Mark this as mutable even though we're in a const method - this is
// essentially a cached value update which is acceptable
const_cast<GraphicsPipeline*>(this)->is_built = true;
return true;
}
// For better performance tracking, capture time spent waiting for shaders
static thread_local std::chrono::high_resolution_clock::time_point last_shader_wait_log;
static thread_local u32 shader_wait_count = 0;
auto now = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - last_shader_wait_log).count();
// Log shader compilation status periodically to help diagnose performance issues
if (elapsed >= 5) { // Log every 5 seconds
shader_wait_count++;
LOG_DEBUG(Render_OpenGL, "Waiting for async shader compilation... (count={})",
shader_wait_count);
last_shader_wait_log = now;
}
return false;
is_built = built_fence.IsSignaled();
return is_built;
}
} // namespace OpenGL

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -103,7 +102,7 @@ public:
return uses_local_memory;
}
[[nodiscard]] bool IsBuilt() const noexcept;
[[nodiscard]] bool IsBuilt() noexcept;
template <typename Spec>
static auto MakeConfigureSpecFunc() {

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
@ -609,33 +608,9 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
}
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
// Calculate optimal number of workers based on available CPU cores
// Leave at least 1 core for main thread and other operations
// Use more cores for more parallelism in shader compilation
const u32 num_worker_threads = std::max(std::thread::hardware_concurrency(), 2U);
const u32 optimal_workers = num_worker_threads <= 3 ?
num_worker_threads - 1 : // On dual/quad core, leave 1 core free
num_worker_threads - 2; // On 6+ core systems, leave 2 cores free for other tasks
auto worker = std::make_unique<ShaderWorker>(
optimal_workers,
"GlShaderBuilder",
[this] {
auto context = Context{emu_window};
// Apply thread priority based on settings
// This allows users to control how aggressive shader compilation is
const int priority = Settings::values.shader_compilation_priority.GetValue();
if (priority != 0) {
Common::SetCurrentThreadPriority(
priority > 0 ? Common::ThreadPriority::High : Common::ThreadPriority::Low);
}
return context;
}
);
return worker;
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U) - 1,
"GlShaderBuilder",
[this] { return Context{emu_window}; });
}
} // namespace OpenGL

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -127,8 +126,6 @@ RendererVulkan::RendererVulkan(Core::Frontend::EmuWindow& emu_window,
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
scheduler),
hybrid_memory(std::make_unique<HybridMemory>(device, memory_allocator)),
texture_manager(device, memory_allocator),
shader_manager(device),
applet_frame() {
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
turbo_mode.emplace(instance, dld);
@ -178,41 +175,7 @@ RendererVulkan::RendererVulkan(Core::Frontend::EmuWindow& emu_window,
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
#endif
}
// 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<std::string> common_shaders;
// Add paths to common shaders that should be preloaded
// These will be compiled in parallel for faster startup
try {
if (std::filesystem::exists(shader_dir)) {
for (const auto& entry : std::filesystem::directory_iterator(shader_dir)) {
if (entry.is_regular_file() && 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);
}
} else {
LOG_INFO(Render_Vulkan, "Shader directory not found at {}", shader_dir);
}
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Error during shader preloading: {}", e.what());
}
}
Report();
InitializePlatformSpecific();
} catch (const vk::Exception& exception) {
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
@ -517,154 +480,4 @@ void RendererVulkan::RenderAppletCaptureLayer(
CaptureFormat);
}
void RendererVulkan::FixMSAADepthStencil(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer) {
if (framebuffer.Samples() == VK_SAMPLE_COUNT_1_BIT) {
return;
}
// Use the scheduler's command buffer wrapper
scheduler.Record([&](vk::CommandBuffer cmdbuf) {
// Find the depth/stencil image in the framebuffer's attachments
for (u32 i = 0; i < framebuffer.NumImages(); ++i) {
if (framebuffer.HasAspectDepthBit() && (framebuffer.ImageRanges()[i].aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT)) {
VkImageMemoryBarrier barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = framebuffer.Images()[i],
.subresourceRange = framebuffer.ImageRanges()[i]
};
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0, nullptr, nullptr, barrier);
break;
}
}
});
}
void RendererVulkan::ResolveMSAA(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer) {
if (framebuffer.Samples() == VK_SAMPLE_COUNT_1_BIT) {
return;
}
// Use the scheduler's command buffer wrapper
scheduler.Record([&](vk::CommandBuffer cmdbuf) {
// Find color attachments
for (u32 i = 0; i < framebuffer.NumColorBuffers(); ++i) {
if (framebuffer.HasAspectColorBit(i)) {
VkImageResolve resolve_region{
.srcSubresource{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
},
.srcOffset = {0, 0, 0},
.dstSubresource{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
},
.dstOffset = {0, 0, 0},
.extent{
.width = framebuffer.RenderArea().width,
.height = framebuffer.RenderArea().height,
.depth = 1
}
};
cmdbuf.ResolveImage(
framebuffer.Images()[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
framebuffer.Images()[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
resolve_region
);
}
}
});
}
bool RendererVulkan::HandleVulkanError(VkResult result, const std::string& operation) {
if (result == VK_SUCCESS) {
return true;
}
if (result == VK_ERROR_DEVICE_LOST) {
LOG_CRITICAL(Render_Vulkan, "Vulkan device lost during {}", operation);
RecoverFromError();
return false;
}
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
LOG_CRITICAL(Render_Vulkan, "Vulkan out of memory during {}", operation);
// Potential recovery: clear caches, reduce workload
texture_manager.CleanupTextureCache();
return false;
}
LOG_ERROR(Render_Vulkan, "Vulkan error during {}: {}", operation, result);
return false;
}
void RendererVulkan::RecoverFromError() {
LOG_INFO(Render_Vulkan, "Attempting to recover from Vulkan error");
// 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();
// Reset command buffers and pipelines
scheduler.Flush();
LOG_INFO(Render_Vulkan, "Recovery attempt completed");
}
void RendererVulkan::InitializePlatformSpecific() {
LOG_INFO(Render_Vulkan, "Initializing platform-specific Vulkan components");
#if defined(_WIN32) || defined(_WIN64)
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Windows");
// Windows-specific initialization
#elif defined(__linux__)
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Linux");
// Linux-specific initialization
#elif defined(__ANDROID__)
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Android");
// Android-specific initialization
#else
LOG_INFO(Render_Vulkan, "Platform-specific Vulkan initialization not implemented for this platform");
#endif
// Create a compute buffer using the HybridMemory system if enabled
if (Settings::values.use_gpu_memory_manager.GetValue()) {
try {
// Create a small compute buffer for testing
const VkDeviceSize buffer_size = 1 * 1024 * 1024; // 1 MB
ComputeBuffer compute_buffer = hybrid_memory->CreateComputeBuffer(
buffer_size,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
MemoryUsage::DeviceLocal);
LOG_INFO(Render_Vulkan, "Successfully created compute buffer using HybridMemory");
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Failed to create compute buffer: {}", e.what());
}
}
}
} // namespace Vulkan

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -7,7 +6,6 @@
#include <memory>
#include <string>
#include <variant>
#include <functional>
#include "common/dynamic_library.h"
#include "video_core/host1x/gpu_device_memory_manager.h"
@ -19,8 +17,6 @@
#include "video_core/renderer_vulkan/vk_state_tracker.h"
#include "video_core/renderer_vulkan/vk_swapchain.h"
#include "video_core/renderer_vulkan/vk_turbo_mode.h"
#include "video_core/renderer_vulkan/vk_texture_manager.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/hybrid_memory.h"
@ -58,9 +54,6 @@ public:
return device.GetDriverName();
}
void FixMSAADepthStencil(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer);
void ResolveMSAA(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer);
// Enhanced platform-specific initialization
void InitializePlatformSpecific();
@ -77,10 +70,6 @@ private:
void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers);
void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers);
// Enhanced error handling
bool HandleVulkanError(VkResult result, const std::string& operation);
void RecoverFromError();
Tegra::MaxwellDeviceMemoryManager& device_memory;
Tegra::GPU& gpu;
@ -106,10 +95,6 @@ private:
// HybridMemory for advanced memory management
std::unique_ptr<HybridMemory> hybrid_memory;
// Enhanced texture and shader management
TextureManager texture_manager;
ShaderManager shader_manager;
Frame applet_frame;
};

View file

@ -1,10 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#include <chrono>
#include <boost/container/small_vector.hpp>
@ -39,23 +37,10 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
if (shader_notify) {
shader_notify->MarkShaderBuilding();
}
// Track compilation start time for performance metrics
const auto start_time = std::chrono::high_resolution_clock::now();
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
uniform_buffer_sizes.begin());
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics, start_time] {
// Simplify the high priority determination - we can't use workgroup_size
// because it doesn't exist, so use a simpler heuristic
const bool is_high_priority = false; // Default to false until we can find a better criterion
if (is_high_priority) {
// Increase thread priority for small compute shaders that are likely part of critical path
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
}
uniform_buffer_sizes.begin());
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics] {
DescriptorLayoutBuilder builder{device};
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
@ -64,11 +49,15 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
descriptor_update_template =
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info);
const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT,
.pNext = nullptr,
.requiredSubgroupSize = GuestWarpSize,
};
VkPipelineCreateFlags flags{};
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
}
pipeline = device.GetLogical().CreateComputePipeline(
{
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
@ -76,7 +65,8 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
.flags = flags,
.stage{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = nullptr,
.pNext =
device.IsExtSubgroupSizeControlSupported() ? &subgroup_size_ci : nullptr,
.flags = 0,
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = *spv_module,
@ -89,15 +79,6 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
},
*pipeline_cache);
// Performance measurement
const auto end_time = std::chrono::high_resolution_clock::now();
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
if (compilation_time > 50) { // Only log slow compilations
LOG_DEBUG(Render_Vulkan, "Compiled compute shader in {}ms", compilation_time);
}
if (pipeline_statistics) {
pipeline_statistics->Collect(*pipeline);
}

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -260,16 +259,7 @@ GraphicsPipeline::GraphicsPipeline(
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
num_textures += Shader::NumDescriptors(info->texture_descriptors);
}
// Track compilation start time for performance metrics
const auto start_time = std::chrono::high_resolution_clock::now();
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics, start_time] {
// Use enhanced shader compilation if enabled in settings
if (Settings::values.use_enhanced_shader_building.GetValue()) {
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
}
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics] {
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
uses_push_descriptor = builder.CanUsePushDescriptor();
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
@ -284,17 +274,6 @@ GraphicsPipeline::GraphicsPipeline(
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
Validate();
MakePipeline(render_pass);
// Performance measurement
const auto end_time = std::chrono::high_resolution_clock::now();
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
// Log shader compilation time for slow shaders to help diagnose performance issues
if (compilation_time > 100) { // Only log very slow compilations
LOG_DEBUG(Render_Vulkan, "Compiled graphics pipeline in {}ms", compilation_time);
}
if (pipeline_statistics) {
pipeline_statistics->Collect(*pipeline);
}
@ -333,9 +312,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
const auto& regs{maxwell3d->regs};
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
// Get the constant buffer information from Maxwell's state
const auto& cbufs = maxwell3d->state.shader_stages[stage].const_buffers;
const Shader::Info& info{stage_infos[stage]};
buffer_cache.UnbindGraphicsStorageBuffers(stage);
if constexpr (Spec::has_storage_buffers) {
@ -347,7 +323,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
++ssbo_index;
}
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
@ -369,7 +345,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
}};
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)};

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -266,42 +265,18 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
}
size_t GetTotalPipelineWorkers() {
const size_t num_cores = std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL);
// Calculate optimal number of workers based on available CPU cores
size_t optimal_workers;
const size_t max_core_threads =
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
#ifdef ANDROID
// Mobile devices need more conservative threading to avoid thermal issues
// Leave more cores free on Android for system processes and other apps
constexpr size_t min_free_cores = 3ULL;
if (num_cores <= min_free_cores + 1) {
return 1ULL; // At least one worker
// Leave at least a few cores free in android
constexpr size_t free_cores = 3ULL;
if (max_core_threads <= free_cores) {
return 1ULL;
}
optimal_workers = num_cores - min_free_cores;
return max_core_threads - free_cores;
#else
// Desktop systems can use more aggressive threading
if (num_cores <= 3) {
optimal_workers = num_cores - 1; // Dual/triple core: leave 1 core free
} else if (num_cores <= 6) {
optimal_workers = num_cores - 2; // Quad/hex core: leave 2 cores free
} else {
// For 8+ core systems, use more workers but still leave some cores for other tasks
optimal_workers = num_cores - (num_cores / 4); // Leave ~25% of cores free
}
return max_core_threads;
#endif
// Apply threading priority via shader_compilation_priority setting if enabled
const int priority = Settings::values.shader_compilation_priority.GetValue();
if (priority > 0) {
// High priority - use more cores for shader compilation
optimal_workers = std::min(optimal_workers + 1, num_cores - 1);
} else if (priority < 0) {
// Low priority - use fewer cores for shader compilation
optimal_workers = (optimal_workers >= 2) ? optimal_workers - 1 : 1;
}
return optimal_workers;
}
} // Anonymous namespace
@ -619,35 +594,14 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
if (pipeline->IsBuilt()) {
return pipeline;
}
if (!use_asynchronous_shaders) {
return pipeline;
}
// Advanced heuristics for smarter async shader compilation
// Track stutter metrics for better debugging and performance tuning
static thread_local u32 async_shader_count = 0;
static thread_local std::chrono::high_resolution_clock::time_point last_async_shader_log;
auto now = std::chrono::high_resolution_clock::now();
// Simplify UI shader detection since we don't have access to clear_buffers
const bool is_ui_shader = !maxwell3d->regs.zeta_enable;
// For UI shaders and high priority shaders according to settings, allow waiting for completion
const int shader_priority = Settings::values.shader_compilation_priority.GetValue();
if ((is_ui_shader && shader_priority >= 0) || shader_priority > 1) {
// For UI/menu elements and critical visuals, let's wait for the shader to compile
// but only if high shader priority
return pipeline;
}
// If something is using depth, we can assume that games are not rendering anything which
// will be used one time.
if (maxwell3d->regs.zeta_enable) {
return nullptr;
}
// If games are using a small index count, we can assume these are full screen quads.
// Usually these shaders are only used once for building textures so we can assume they
// can't be built async
@ -655,23 +609,6 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
return pipeline;
}
// Track and log async shader statistics periodically
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - last_async_shader_log).count();
if (elapsed >= 10) { // Log every 10 seconds
async_shader_count = 0;
last_async_shader_log = now;
}
async_shader_count++;
// Log less frequently to avoid spamming log
if (async_shader_count % 100 == 1) {
LOG_DEBUG(Render_Vulkan, "Async shader compilation in progress (count={})",
async_shader_count);
}
return nullptr;
}

View file

@ -1,141 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <thread>
#include <filesystem>
#include <fstream>
#include <vector>
#include <atomic>
#include <queue>
#include <condition_variable>
#include <future>
#include <chrono>
#include <unordered_set>
#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<std::function<void()>> commandQueue;
std::condition_variable commandQueueCondition;
std::atomic<bool> 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<void()> command;
{
std::unique_lock<std::mutex> 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<void()> command) {
{
std::lock_guard<std::mutex> 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<void(vk::CommandBuffer)> 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<std::mutex> 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<const u32> code) {
return device.GetLogical().CreateShaderModule({
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
@ -146,368 +20,4 @@ vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
});
}
bool IsShaderValid(VkShaderModule shader_module) {
// TODO: validate the shader by checking if it's null
// or by examining SPIR-V data for correctness [ZEP]
return shader_module != VK_NULL_HANDLE;
}
// Atomic flag for tracking shader compilation status
std::atomic<bool> compilingShader(false);
void AsyncCompileShader(const Device& device, const std::string& shader_path,
std::function<void(VkShaderModule)> callback) {
LOG_INFO(Render_Vulkan, "Asynchronously compiling shader: {}", shader_path);
// Create shader cache directory if it doesn't exist
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
std::filesystem::create_directory(SHADER_CACHE_DIR);
}
// 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;
}
// 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<u32> 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<size_t>(shader_file.tellg());
shader_file.seekg(0, std::ios::beg);
spir_v.resize(file_size / sizeof(u32));
if (shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
success = true;
}
}
}
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<const char*>(spir_v.data()),
spir_v.size() * sizeof(u32));
}
auto endTime = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> 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);
});
}
// Release the compilation flag
compilingShader.store(false);
}).detach();
}
ShaderManager::ShaderManager(const Device& device_) : device(device_) {
// Initialize command queue system
InitializeCommandQueue();
}
ShaderManager::~ShaderManager() {
// Wait for any pending compilations to finish
WaitForCompilation();
// Clean up shader modules
std::lock_guard<std::mutex> lock(shader_mutex);
shader_cache.clear();
// Shutdown command queue
ShutdownCommandQueue();
}
VkShaderModule ShaderManager::GetShaderModule(const std::string& shader_path) {
// Check in-memory cache first
{
std::lock_guard<std::mutex> lock(shader_mutex);
auto it = shader_cache.find(shader_path);
if (it != shader_cache.end()) {
return *it->second;
}
}
// 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<size_t>(cache_file.tellg());
if (file_size > 0 && file_size % sizeof(u32) == 0) {
cache_file.seekg(0, std::ios::beg);
std::vector<u32> spir_v;
spir_v.resize(file_size / sizeof(u32));
if (cache_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
vk::ShaderModule shader = BuildShader(device, spir_v);
if (IsShaderValid(*shader)) {
// Store in memory cache
std::lock_guard<std::mutex> 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<std::mutex> lock(shader_mutex);
return *shader_cache[normalized_path];
}
LOG_ERROR(Render_Vulkan, "Failed to load shader: {}", normalized_path);
return VK_NULL_HANDLE;
}
void ShaderManager::ReloadShader(const std::string& shader_path) {
LOG_INFO(Render_Vulkan, "Reloading shader: {}", shader_path);
// Remove the old shader from cache
{
std::lock_guard<std::mutex> lock(shader_mutex);
shader_cache.erase(shader_path);
}
// Load the shader again
LoadShader(shader_path);
}
bool ShaderManager::LoadShader(const std::string& shader_path) {
LOG_INFO(Render_Vulkan, "Loading shader from: {}", shader_path);
if (!std::filesystem::exists(shader_path)) {
LOG_ERROR(Render_Vulkan, "Shader file does not exist: {}", shader_path);
return false;
}
try {
std::vector<u32> 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<size_t>(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<char*>(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<std::mutex> 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<const char*>(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;
}
}
void ShaderManager::WaitForCompilation() {
// 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<std::mutex> 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<std::string>& 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<std::string> shaders_to_load;
// First check which shaders are not already cached
{
std::lock_guard<std::mutex> 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<unsigned>(4));
std::vector<std::future<void>> 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

View file

@ -1,16 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include <string>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <functional>
#include <vector>
#include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
@ -18,48 +11,7 @@
namespace Vulkan {
class Device;
class Scheduler;
// Command queue system for asynchronous operations
void InitializeCommandQueue();
void ShutdownCommandQueue();
void SubmitCommandToQueue(std::function<void()> command);
void CommandQueueWorker();
// Scheduler integration functions
void SetGlobalScheduler(Scheduler* scheduler);
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command);
u64 FlushScheduler(VkSemaphore signal_semaphore = nullptr, VkSemaphore wait_semaphore = nullptr);
void ProcessAllCommands();
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code);
// Enhanced shader functionality
bool IsShaderValid(VkShaderModule shader_module);
void AsyncCompileShader(const Device& device, const std::string& shader_path,
std::function<void(VkShaderModule)> callback);
class ShaderManager {
public:
explicit ShaderManager(const Device& device);
~ShaderManager();
VkShaderModule GetShaderModule(const std::string& shader_path);
void ReloadShader(const std::string& shader_path);
bool LoadShader(const std::string& shader_path);
void WaitForCompilation();
// Batch process multiple shaders in parallel
void PreloadShaders(const std::vector<std::string>& shader_paths);
// Integrate with Citron's scheduler
void SetScheduler(Scheduler* scheduler);
private:
const Device& device;
std::mutex shader_mutex;
std::unordered_map<std::string, vk::ShaderModule> shader_cache;
};
} // namespace Vulkan

View file

@ -30,10 +30,6 @@
namespace Vulkan {
// TextureCacheManager implementations to fix linker errors
TextureCacheManager::TextureCacheManager() = default;
TextureCacheManager::~TextureCacheManager() = default;
using Tegra::Engines::Fermi2D;
using Tegra::Texture::SwizzleSource;
using Tegra::Texture::TextureMipmapFilter;

View file

@ -5,10 +5,6 @@
#pragma once
#include <span>
#include <mutex>
#include <atomic>
#include <string>
#include <unordered_map>
#include "video_core/texture_cache/texture_cache_base.h"
@ -41,22 +37,6 @@ class RenderPassCache;
class StagingBufferPool;
class Scheduler;
// Enhanced texture management for better error handling and thread safety
class TextureCacheManager {
public:
explicit TextureCacheManager();
~TextureCacheManager();
VkImage GetTextureFromCache(const std::string& texture_path);
void ReloadTexture(const std::string& texture_path);
bool IsTextureLoadedCorrectly(VkImage texture);
void HandleTextureCache();
private:
std::mutex texture_mutex;
std::unordered_map<std::string, VkImage> texture_cache;
};
class TextureCacheRuntime {
public:
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
@ -137,10 +117,6 @@ public:
VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const;
// Enhanced texture error handling
bool IsTextureLoadedCorrectly(VkImage texture);
void HandleTextureError(const std::string& texture_path);
const Device& device;
Scheduler& scheduler;
MemoryAllocator& memory_allocator;
@ -152,9 +128,6 @@ public:
const Settings::ResolutionScalingInfo& resolution;
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
// Enhanced texture management
TextureCacheManager texture_cache_manager;
static constexpr size_t indexing_slots = 8 * sizeof(size_t);
std::array<vk::Buffer, indexing_slots> buffers{};
};

View file

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <mutex>
#include <string>
#include <unordered_map>
#include <functional>
#include <atomic>
#include <optional>
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
class Device;
class MemoryAllocator;
// Enhanced texture manager for better error handling and thread safety
class TextureManager {
public:
explicit TextureManager(const Device& device, MemoryAllocator& memory_allocator);
~TextureManager();
// Get a texture from the cache, loading it if necessary
VkImage GetTexture(const std::string& texture_path);
// Force a texture to reload from disk
void ReloadTexture(const std::string& texture_path);
// Check if a texture is loaded correctly
bool IsTextureLoadedCorrectly(VkImage texture);
// Remove old textures from the cache
void CleanupTextureCache();
// Handle texture rendering, with automatic reload if needed
void HandleTextureRendering(const std::string& texture_path,
std::function<void(VkImage)> render_callback);
private:
// Load a texture from disk and create a Vulkan image
vk::Image LoadTexture(const std::string& texture_path);
// Create a default texture to use in case of errors
vk::Image CreateDefaultTexture();
const Device& device;
MemoryAllocator& memory_allocator;
std::mutex texture_mutex;
std::unordered_map<std::string, vk::Image> texture_cache;
std::optional<vk::Image> default_texture;
VkFormat texture_format = VK_FORMAT_B8G8R8A8_SRGB;
};
} // namespace Vulkan

View file

@ -140,10 +140,6 @@ public:
return (flags & property_flags) == flags && (type_mask & shifted_memory_type) != 0;
}
[[nodiscard]] bool IsEmpty() const noexcept {
return commits.empty();
}
private:
[[nodiscard]] static constexpr u32 ShiftType(u32 type) {
return 1U << type;
@ -288,78 +284,39 @@ MemoryCommit MemoryAllocator::Commit(const VkMemoryRequirements& requirements, M
const u32 type_mask = requirements.memoryTypeBits;
const VkMemoryPropertyFlags usage_flags = MemoryUsagePropertyFlags(usage);
const VkMemoryPropertyFlags flags = MemoryPropertyFlags(type_mask, usage_flags);
// First attempt
if (std::optional<MemoryCommit> commit = TryCommit(requirements, flags)) {
return std::move(*commit);
}
// Commit has failed, allocate more memory
// Commit has failed, allocate more memory.
const u64 chunk_size = AllocationChunkSize(requirements.size);
if (TryAllocMemory(flags, type_mask, chunk_size)) {
return TryCommit(requirements, flags).value();
if (!TryAllocMemory(flags, type_mask, chunk_size)) {
// TODO(Rodrigo): Handle out of memory situations in some way like flushing to guest memory.
throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY);
}
// Memory allocation failed - try to recover by releasing empty allocations
for (auto it = allocations.begin(); it != allocations.end();) {
if ((*it)->IsEmpty()) {
it = allocations.erase(it);
} else {
++it;
}
}
// Try allocating again after cleanup
if (TryAllocMemory(flags, type_mask, chunk_size)) {
return TryCommit(requirements, flags).value();
}
// If still failing, try with non-device-local memory as a last resort
if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
const VkMemoryPropertyFlags fallback_flags = flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
if (TryAllocMemory(fallback_flags, type_mask, chunk_size)) {
if (auto commit = TryCommit(requirements, fallback_flags)) {
LOG_WARNING(Render_Vulkan, "Falling back to non-device-local memory due to OOM");
return std::move(*commit);
}
}
}
LOG_CRITICAL(Render_Vulkan, "Vulkan memory allocation failed - out of device memory");
throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY);
// Commit again, this time it won't fail since there's a fresh allocation above.
// If it does, there's a bug.
return TryCommit(requirements, flags).value();
}
bool MemoryAllocator::TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size) {
const auto type_opt = FindType(flags, type_mask);
if (!type_opt) {
if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) {
// Try to allocate non device local memory
return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size);
}
return false;
}
const u64 aligned_size = (device.GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) ?
Common::AlignUp(size, 4096) : // Adreno requires 4KB alignment
size; // Others (NVIDIA, AMD, Intel, etc)
const u32 type = FindType(flags, type_mask).value();
vk::DeviceMemory memory = device.GetLogical().TryAllocateMemory({
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.pNext = nullptr,
.allocationSize = aligned_size,
.memoryTypeIndex = *type_opt,
.allocationSize = size,
.memoryTypeIndex = type,
});
if (!memory) {
if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) {
// Try to allocate non device local memory
return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size);
} else {
// RIP
return false;
}
return false;
}
allocations.push_back(
std::make_unique<MemoryAllocation>(this, std::move(memory), flags, aligned_size, *type_opt));
std::make_unique<MemoryAllocation>(this, std::move(memory), flags, size, type));
return true;
}