#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h" #include "common/assert.h" #include "common/file.h" #include "common/logging/log.h" #include "common/profiler.h" #include "graphics/guest_gpu/hardwareContext.h" #include "graphics/host_gpu/renderer/colorRenderTarget.h" #include "graphics/host_gpu/renderer/debug.h" #include "graphics/host_gpu/renderer/depthRenderTarget.h" #include "graphics/host_gpu/renderer/image/imageView.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/shader/shaderCompiler.h" #include "kytyGitVersion.h" #include "loader/systemContent.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { std::string DriverCacheSignature(const vk::PhysicalDeviceProperties& properties) { constexpr char hex[] = "0123456789abcdef"; std::string uuid(VK_UUID_SIZE * 2, '0'); for (size_t i = 0; i < VK_UUID_SIZE; i++) { uuid[i * 2] = hex[properties.pipelineCacheUUID[i] >> 4u]; uuid[i * 2 + 1] = hex[properties.pipelineCacheUUID[i] & 0xfu]; } return fmt::format("KytyPC1:{}:{:08x}:{:08x}:{:08x}:{}\n", KYTY_GIT_REVISION, properties.vendorID, properties.deviceID, properties.driverVersion, uuid); } std::string PipelineCacheTitleId() { std::string title_id; if ((!Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) || title_id.empty()) && (!Loader::SystemContentParamSfoGetString("CONTENT_ID", &title_id) || title_id.empty())) { return {}; } if (!std::ranges::all_of(title_id, [](unsigned char c) { return std::isalnum(c) != 0 || c == '-' || c == '_'; })) { return {}; } return title_id; } template void PipelineCacheLog(fmt::format_string format, Args&&... args) { auto message = fmt::format(format, std::forward(args)...); message += '\n'; if (Log::GetDirection() != Log::Direction::Console) { std::fwrite(message.data(), 1, message.size(), stdout); std::fflush(stdout); } Log::Write(message); Log::Flush(); } void NormalizeStaticParamsForDynamicState(PipelineStaticParameters& static_params) { static_params.viewport_scale[0] = 0.5f; static_params.viewport_scale[1] = 0.5f; static_params.viewport_scale[2] = 1.0f; static_params.viewport_offset[0] = 0.5f; static_params.viewport_offset[1] = 0.5f; static_params.viewport_offset[2] = 0.0f; static_params.scissor_ltrb[0] = 0; static_params.scissor_ltrb[1] = 0; static_params.scissor_ltrb[2] = 1; static_params.scissor_ltrb[3] = 1; } } // namespace struct PipelineCache::ProgramCache { struct SourceKey { ShaderType stage = ShaderType::Unknown; uint64_t hash = 0; bool operator==(const SourceKey&) const = default; }; struct Permutation { std::vector static_key; std::shared_ptr program; ShaderProgram handle; }; struct SourceKeyHash { std::size_t operator()(const SourceKey& key) const { std::size_t hash = static_cast(key.stage); PipelineKeyHash::Mix(hash, static_cast(key.hash)); if constexpr (sizeof(std::size_t) < sizeof(uint64_t)) { PipelineKeyHash::Mix(hash, static_cast(key.hash >> 32u)); } return hash; } }; static uint64_t MixId(uint64_t hash, uint64_t value) { return hash ^ (value + 0x9e3779b97f4a7c15ull + (hash << 6u) + (hash >> 2u)); } static constexpr std::size_t MaxStaticKeyWords = 5 + ShaderVertexInputInfo::RES_MAX * 17; template ShaderProgram Get(const ShaderParams& params, InputInfo& input_info) { constexpr ShaderType stage = [] { if constexpr (std::is_same_v) { return ShaderType::Vertex; } else if constexpr (std::is_same_v) { return ShaderType::Pixel; } else { static_assert(std::is_same_v); return ShaderType::Compute; } }(); BuildStageStaticKey(input_info, key_scratch); auto& permutations = programs[{stage, params.hash}]; for (const auto& permutation: permutations) { if (permutation.static_key == key_scratch && MaterializeProgram(permutation.program, params, input_info)) { return permutation.handle; } } const auto module = CompileProgram(device, params, input_info); EXIT_IF(module == nullptr || !input_info.stage); uint64_t id = MixId(MixId(params.hash, static_cast(stage)), permutations.size()); if (id == 0) { id = 1; } const ShaderProgram handle {.id = id, .module = module}; permutations.push_back({key_scratch, input_info.stage.program, handle}); std::printf("Num compiled %u shaders\n", ++num_compiled); return handle; } explicit ProgramCache(vk::Device device): device(device) { key_scratch.reserve(MaxStaticKeyWords); } std::unordered_map, SourceKeyHash> programs; std::vector key_scratch; vk::Device device; uint32_t num_compiled = 0; }; PipelineCache::PipelineCache(GraphicContext& graphics) : m_graphics(graphics), m_program_cache(std::make_unique(graphics.device)) { EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread()); InitializeDriverCache(); } PipelineCache::~PipelineCache() { Save(); auto destroy = [this](const auto& pipelines) { for (const auto& [key, pipeline]: pipelines) { (void)key; m_graphics.device.destroyPipeline(pipeline->pipeline, nullptr); m_graphics.device.destroyPipelineLayout(pipeline->pipeline_layout, nullptr); m_graphics.device.destroyDescriptorSetLayout(pipeline->descriptor_set_layout, nullptr); } }; destroy(m_graphics_pipelines); destroy(m_compute_pipelines); for (const auto& [key, permutations]: m_program_cache->programs) { (void)key; for (const auto& permutation: permutations) { m_graphics.device.destroyShaderModule(permutation.handle.module, nullptr); } } if (m_driver_cache != nullptr) { m_graphics.device.destroyPipelineCache(m_driver_cache, nullptr); } } void PipelineCache::InitializeDriverCache() { const auto title_id = PipelineCacheTitleId(); if (title_id.empty()) { return; } if (KYTY_BUILD != KYTY_BUILD_RELEASE) { PipelineCacheLog("Vulkan pipeline cache: disabled (non-Release build)"); return; } const std::string_view git_hash = KYTY_GIT_HASH; const std::string_view git_revision = KYTY_GIT_REVISION; if (git_hash == "unknown" || git_revision == "unknown") { PipelineCacheLog("Vulkan pipeline cache: disabled (unknown git revision)"); return; } if (git_hash.ends_with("-dirty")) { PipelineCacheLog("Vulkan pipeline cache: disabled (dirty build)"); return; } m_driver_cache_path = std::filesystem::path("_PipelineCache") / (title_id + ".bin"); const auto path = Common::PathToString(m_driver_cache_path); const bool cache_exists = Common::File::IsFileExisting(m_driver_cache_path); if (cache_exists) { PipelineCacheLog("Vulkan pipeline cache: loading {}", path); } else { PipelineCacheLog("Vulkan pipeline cache: initializing {}", path); } std::vector initial_data; if (cache_exists) { Common::File file(m_driver_cache_path, Common::File::Mode::Read); const auto file_size = file.IsInvalid() ? 0 : file.Size(); const auto signature = DriverCacheSignature(m_graphics.GetPhysicalDeviceProperties()); if (file_size >= signature.size() + sizeof(uint64_t) && file_size <= std::numeric_limits::max()) { std::string cached_signature(signature.size(), '\0'); uint64_t payload_hash = 0; initial_data.resize(file_size - signature.size() - sizeof(payload_hash)); uint32_t signature_read = 0; uint32_t hash_read = 0; uint32_t payload_read = 0; file.Read(cached_signature.data(), static_cast(cached_signature.size()), &signature_read); file.Read(&payload_hash, sizeof(payload_hash), &hash_read); file.Read(initial_data.data(), static_cast(initial_data.size()), &payload_read); file.Close(); if (signature_read != cached_signature.size() || hash_read != sizeof(payload_hash) || payload_read != initial_data.size() || cached_signature != signature || XXH3_64bits(initial_data.data(), initial_data.size()) != payload_hash) { initial_data.clear(); PipelineCacheLog( "Vulkan pipeline cache: invalidating {} (driver, emulator, or data mismatch)", path); } } else { file.Close(); PipelineCacheLog("Vulkan pipeline cache: invalidating {} (invalid file size)", path); } } vk::PipelineCacheCreateInfo create {}; create.sType = vk::StructureType::ePipelineCacheCreateInfo; create.initialDataSize = initial_data.size(); create.pInitialData = initial_data.empty() ? nullptr : initial_data.data(); auto result = m_graphics.device.createPipelineCache(&create, nullptr, &m_driver_cache); if (result != vk::Result::eSuccess && !initial_data.empty()) { PipelineCacheLog("Vulkan pipeline cache: driver rejected {} ({}); starting empty", path, VulkanToString(result)); initial_data.clear(); create.initialDataSize = 0; create.pInitialData = nullptr; result = m_graphics.device.createPipelineCache(&create, nullptr, &m_driver_cache); } if (result != vk::Result::eSuccess) { PipelineCacheLog("Vulkan pipeline cache: disabled ({})", VulkanToString(result)); m_driver_cache = nullptr; return; } if (!initial_data.empty()) { PipelineCacheLog("Vulkan pipeline cache: loaded {} bytes from {}", initial_data.size(), path); } else { PipelineCacheLog("Vulkan pipeline cache: initialized empty"); } } void PipelineCache::Save() { Common::LockGuard lock(m_mutex); if (m_driver_cache == nullptr) { return; } size_t size = 0; vk::Result result; std::vector payload; for (uint32_t attempt = 0; attempt < 3; attempt++) { size = 0; result = m_graphics.device.getPipelineCacheData(m_driver_cache, &size, nullptr); if (result != vk::Result::eSuccess || size == 0 || size > std::numeric_limits::max()) { break; } payload.resize(size); result = m_graphics.device.getPipelineCacheData(m_driver_cache, &size, payload.data()); if (result != vk::Result::eIncomplete) { break; } } if (result != vk::Result::eSuccess || size == 0 || size > std::numeric_limits::max()) { PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {} bytes)", VulkanToString(result), size); return; } payload.resize(size); auto prefix = DriverCacheSignature(m_graphics.GetPhysicalDeviceProperties()); const auto payload_hash = XXH3_64bits(payload.data(), payload.size()); prefix.append(reinterpret_cast(&payload_hash), sizeof(payload_hash)); if (!Common::File::CreateDirectories(m_driver_cache_path.parent_path())) { PipelineCacheLog("Vulkan pipeline cache: failed to create cache directory"); return; } auto temp_path = m_driver_cache_path; temp_path += ".tmp"; Common::File file; uint32_t prefix_written = 0; uint32_t payload_written = 0; if (file.Create(temp_path)) { file.Write(prefix.data(), static_cast(prefix.size()), &prefix_written); file.Write(payload.data(), static_cast(payload.size()), &payload_written); } const bool flushed = !file.IsInvalid() && file.Flush(); file.Close(); if (prefix_written != prefix.size() || payload_written != payload.size() || !flushed || !Common::File::RenameFile(temp_path, m_driver_cache_path)) { PipelineCacheLog("Vulkan pipeline cache: failed to write {}", Common::PathToString(m_driver_cache_path)); return; } PipelineCacheLog("Vulkan pipeline cache: saved {} bytes to {}", payload.size(), Common::PathToString(m_driver_cache_path)); m_graphics.device.destroyPipelineCache(m_driver_cache, nullptr); m_driver_cache = nullptr; } ShaderProgram PipelineCache::GetVertexProgram(const HW::VertexShaderInfo& regs, const HW::ShaderRegisters& sh, ShaderVertexInputInfo& input_info) { const auto params = PrepareProgram(regs, sh, input_info); Common::LockGuard lock(m_mutex); return m_program_cache->Get(params, input_info); } ShaderProgram PipelineCache::GetPixelProgram( const HW::PixelShaderInfo& regs, const HW::ShaderRegisters& sh, const ShaderVertexInputInfo& vertex_info, std::span target_export_mapping, ShaderPixelInputInfo& input_info) { const auto params = PrepareProgram(regs, sh, vertex_info, target_export_mapping, input_info); Common::LockGuard lock(m_mutex); return m_program_cache->Get(params, input_info); } ShaderProgram PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs, const HW::ShaderRegisters& sh, ShaderComputeInputInfo& input_info) { input_info.needs_lds_barriers = !m_graphics.compute_wave64_supported; const auto params = PrepareProgram(regs, sh, input_info); Common::LockGuard lock(m_mutex); return m_program_cache->Get(params, input_info); } bool PipelineStaticParameters::operator==(const PipelineStaticParameters& other) const noexcept { return std::memcmp(this, &other, sizeof(*this)) == 0; } PipelineCache::GraphicsPipeline& PipelineCache::CreateGraphicsPipeline( std::span colors, const RenderDepthInfo& depth, const ShaderVertexInputInfo& vs_input_info, CommandBuffer& command, const ShaderPixelInputInfo* ps_input_info, vk::PrimitiveTopology topology, bool primitive_restart_enable, const ShaderProgram& vertex_program, const ShaderProgram& pixel_program) { KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Gfx)", profiler::colors::DeepOrangeA200); EXIT_IF(colors.size() > RENDER_COLOR_ATTACHMENTS_MAX); EXIT_IF(!vertex_program); const bool ps_active = ps_input_info != nullptr; EXIT_IF(ps_active && !pixel_program); const auto color_count = static_cast(colors.size()); Common::LockGuard lock(m_mutex); auto& ctx = command.GetRegisters(); const HW::BlendColor& bclr = ctx.GetBlendColor(); uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {}; for (uint32_t i = 0; i < color_count; i++) { color_mask[i] = (colors[i].image_id ? colors[i].export_mapping.ApplyMask(render_target_mask_slot( ctx.GetRenderTargetMask(), colors[i].target_slot)) : 0); } const HW::ModeControl& mc = ctx.GetModeControl(); const auto vs_id = vertex_program.id; const auto ps_id = ps_active ? pixel_program.id : 0; PipelineStaticParameters static_params {}; GraphicsPipeline p {}; p.ps_shader_id = ps_id; p.vs_shader_id = vs_id; static_params.color_count = color_count; PipelineRenderingState rendering {}; rendering.color_count = color_count; uint32_t attachment_samples = 0; for (uint32_t i = 0; i < color_count; i++) { EXIT_IF(!colors[i].image_id || colors[i].format == vk::Format::eUndefined); rendering.color_formats[i] = colors[i].format; if (attachment_samples == 0) { attachment_samples = colors[i].samples; } else if (attachment_samples != colors[i].samples) { EXIT("mixed color attachment sample counts are unsupported: %u and %u\n", attachment_samples, colors[i].samples); } } const bool with_depth = depth.format != vk::Format::eUndefined && static_cast(depth.image_id); if (with_depth) { const auto aspects = ImageViewOps::DepthAspectMask(depth.format); rendering.depth_format = aspects & vk::ImageAspectFlagBits::eDepth ? depth.format : vk::Format::eUndefined; rendering.stencil_format = aspects & vk::ImageAspectFlagBits::eStencil ? depth.format : vk::Format::eUndefined; if (attachment_samples == 0) { attachment_samples = depth.samples; } else if (attachment_samples != depth.samples) { EXIT("mixed color/depth sample counts are unsupported: %u and %u\n", attachment_samples, depth.samples); } } if (color_count == 0 && !with_depth) { attachment_samples = render_sample_count(ctx.GetAaConfig().msaa_num_samples); EXIT_IF(!static_cast( m_graphics.GetPhysicalDeviceProperties().limits.framebufferNoAttachmentsSampleCounts & vulkan_sample_count(attachment_samples))); } EXIT_IF(attachment_samples == 0 || vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {}); if (ps_active && depth.depth_test_enable && ps_input_info->ps_execute_on_noop) { static std::atomic log_count {0}; if (log_count.fetch_add(1, std::memory_order_relaxed) < 16) { LOGF("Pipeline: temporary: accepting EXEC_ON_NOOP with depth test enabled\n"); } } const auto& clip_control = ctx.GetClipControl(); static_params.negative_one_to_one = !clip_control.dx_clip_space; static_params.depth_clip_enable = clip_control.IsZClipEnabled(); static_params.topology = topology; static_params.primitive_restart_enable = primitive_restart_enable; static_params.samples = attachment_samples; static_params.sample_shading_enable = ps_active && attachment_samples > 1 && ps_input_info->ps_sample_shading; if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) { EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n"); } static_params.with_depth = with_depth; static_params.depth_test_enable = depth.depth_test_enable; static_params.depth_write_enable = (depth.depth_write_enable && !depth.depth_clear_enable); static_params.depth_compare_op = depth.depth_compare_op; static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable; static_params.depth_min_bounds = depth.depth_min_bounds; static_params.depth_max_bounds = depth.depth_max_bounds; static_params.stencil_test_enable = depth.stencil_test_enable; static_params.stencil_front = depth.stencil_static_front; static_params.stencil_back = depth.stencil_static_back; for (uint32_t i = 0; i < RENDER_COLOR_ATTACHMENTS_MAX; i++) { static_params.color_mask[i] = color_mask[i]; } const bool rect_list = topology == vk::PrimitiveTopology::ePatchList; static_params.cull_back = !rect_list && mc.cull_back; static_params.cull_front = !rect_list && mc.cull_front; static_params.face = mc.face; for (uint32_t i = 0; i < color_count; i++) { const auto& rt = ctx.GetRenderTarget(colors[i].target_slot); const auto& bc = ctx.GetBlendControl(colors[i].target_slot); static_params.color_srcblend[i] = bc.color_srcblend; static_params.color_comb_fcn[i] = bc.color_comb_fcn; static_params.color_destblend[i] = bc.color_destblend; static_params.alpha_srcblend[i] = bc.alpha_srcblend; static_params.alpha_comb_fcn[i] = bc.alpha_comb_fcn; static_params.alpha_destblend[i] = bc.alpha_destblend; static_params.separate_alpha_blend[i] = bc.separate_alpha_blend; static_params.blend_enable[i] = bc.enable; static_params.blend_bypass[i] = rt.info.blend_bypass; } static_params.blend_color_red = bclr.red; static_params.blend_color_green = bclr.green; static_params.blend_color_blue = bclr.blue; static_params.blend_color_alpha = bclr.alpha; NormalizeStaticParamsForDynamicState(static_params); GraphicsPipelineKey key {}; key.rendering = rendering; key.vs_shader_id = p.vs_shader_id; key.ps_shader_id = p.ps_shader_id; key.static_params = static_params; if (auto iter = m_graphics_pipelines.find(key); iter != m_graphics_pipelines.end()) { return *iter->second; } if (graphics_debug_dump_enabled()) { ShaderDbgDumpInputInfo(vs_input_info); if (ps_active) { ShaderDbgDumpInputInfo(*ps_input_info); } LOGF("PipelineTrace: shader modules VS=%" PRIu64 " module=%p PS=%" PRIu64 " module=%p\n", vs_id, static_cast(vertex_program.module), ps_id, static_cast(pixel_program.module)); } auto cached = std::make_unique(p); LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id); CreatePipelineInternal(m_graphics, *cached, rendering, vs_input_info, vertex_program.module, ps_input_info, pixel_program.module, static_params, m_driver_cache); LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id); EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr); EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr); auto [iter, inserted] = m_graphics_pipelines.emplace(std::move(key), std::move(cached)); EXIT_IF(!inserted); return *iter->second; } PipelineCache::ComputePipeline& PipelineCache::CreateComputePipeline(ShaderComputeInputInfo& input_info, const ShaderProgram& compute_program) { KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Compute)", profiler::colors::RedA100); EXIT_IF(!compute_program); Common::LockGuard lock(m_mutex); ComputePipeline p {}; p.cs_shader_id = compute_program.id; ComputePipelineKey key {}; key.cs_shader_id = p.cs_shader_id; if (auto iter = m_compute_pipelines.find(key); iter != m_compute_pipelines.end()) { return *iter->second; } if (graphics_debug_dump_enabled()) { ShaderDbgDumpInputInfo(input_info); } auto cached = std::make_unique(p); CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module, m_driver_cache); EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr); EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr); auto [iter, inserted] = m_compute_pipelines.emplace(std::move(key), std::move(cached)); EXIT_IF(!inserted); return *iter->second; } } // namespace Libs::Graphics