// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include "common/cityhash.h" #include "common/scope_exit.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" #include "common/thread_worker.h" #include "core/core.h" #include "shader_recompiler/backend/spirv/emit_spirv.h" #include "shader_recompiler/environment.h" #include "shader_recompiler/frontend/maxwell/control_flow.h" #include "shader_recompiler/frontend/maxwell/translate_program.h" #include "shader_recompiler/ir_opt/passes.h" #include "shader_recompiler/program_header.h" #include "video_core/engines/kepler_compute.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/memory_manager.h" #include "video_core/renderer_vulkan/fixed_pipeline_state.h" #include "video_core/renderer_vulkan/maxwell_to_vk.h" #include "video_core/renderer_vulkan/pipeline_helper.h" #include "video_core/renderer_vulkan/pipeline_statistics.h" #include "video_core/renderer_vulkan/vertex_repack.h" #include "video_core/renderer_vulkan/vk_compute_pipeline.h" #include "video_core/renderer_vulkan/vk_descriptor_pool.h" #include "video_core/renderer_vulkan/vk_pipeline_cache.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/renderer_vulkan/vk_shader_util.h" #include "video_core/renderer_vulkan/vk_update_descriptor.h" #include "video_core/shader_cache.h" #include "video_core/shader_environment.h" #include "video_core/shader_notify.h" #include "video_core/surface.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/gpu_logging/gpu_logging.h" #ifdef __ANDROID__ #include "../../android/app/src/main/jni/android_settings.h" #endif namespace Vulkan { namespace { using Shader::Backend::SPIRV::EmitSPIRV; using Shader::Maxwell::ConvertLegacyToGeneric; using Shader::Maxwell::GenerateGeometryPassthrough; using Shader::Maxwell::MergeDualVertexPrograms; using Shader::Maxwell::TranslateProgram; using VideoCommon::ComputeEnvironment; using VideoCommon::FileEnvironment; using VideoCommon::GenericEnvironment; using VideoCommon::GraphicsEnvironment; constexpr u32 CACHE_VERSION = 19; constexpr size_t VULKAN_CACHE_FLUSH_PIPELINES = 128; constexpr size_t VULKAN_CACHE_FLUSH_MIN_SECONDS = 30; constexpr std::array VULKAN_CACHE_MAGIC_NUMBER{'y', 'u', 'z', 'u', 'v', 'k', 'c', 'h'}; template auto MakeSpan(Container& container) { return std::span(container.data(), container.size()); } Shader::OutputTopology MaxwellToOutputTopology(Maxwell::PrimitiveTopology topology) { switch (topology) { case Maxwell::PrimitiveTopology::Points: return Shader::OutputTopology::PointList; case Maxwell::PrimitiveTopology::LineStrip: return Shader::OutputTopology::LineStrip; default: return Shader::OutputTopology::TriangleStrip; } } Shader::CompareFunction MaxwellToCompareFunction(Maxwell::ComparisonOp comparison) { switch (comparison) { case Maxwell::ComparisonOp::Never_D3D: case Maxwell::ComparisonOp::Never_GL: return Shader::CompareFunction::Never; case Maxwell::ComparisonOp::Less_D3D: case Maxwell::ComparisonOp::Less_GL: return Shader::CompareFunction::Less; case Maxwell::ComparisonOp::Equal_D3D: case Maxwell::ComparisonOp::Equal_GL: return Shader::CompareFunction::Equal; case Maxwell::ComparisonOp::LessEqual_D3D: case Maxwell::ComparisonOp::LessEqual_GL: return Shader::CompareFunction::LessThanEqual; case Maxwell::ComparisonOp::Greater_D3D: case Maxwell::ComparisonOp::Greater_GL: return Shader::CompareFunction::Greater; case Maxwell::ComparisonOp::NotEqual_D3D: case Maxwell::ComparisonOp::NotEqual_GL: return Shader::CompareFunction::NotEqual; case Maxwell::ComparisonOp::GreaterEqual_D3D: case Maxwell::ComparisonOp::GreaterEqual_GL: return Shader::CompareFunction::GreaterThanEqual; case Maxwell::ComparisonOp::Always_D3D: case Maxwell::ComparisonOp::Always_GL: return Shader::CompareFunction::Always; } UNIMPLEMENTED_MSG("Unimplemented comparison op={}", comparison); return {}; } Shader::AttributeType CastAttributeType(const FixedPipelineState::VertexAttribute& attr) { if (attr.enabled == 0) { return Shader::AttributeType::Disabled; } switch (attr.Type()) { case Maxwell::VertexAttribute::Type::UnusedEnumDoNotUseBecauseItWillGoAway: ASSERT_MSG(false, "Invalid vertex attribute type!"); return Shader::AttributeType::Disabled; case Maxwell::VertexAttribute::Type::SNorm: case Maxwell::VertexAttribute::Type::UNorm: case Maxwell::VertexAttribute::Type::Float: return Shader::AttributeType::Float; case Maxwell::VertexAttribute::Type::SInt: return Shader::AttributeType::SignedInt; case Maxwell::VertexAttribute::Type::UInt: return Shader::AttributeType::UnsignedInt; case Maxwell::VertexAttribute::Type::UScaled: return Shader::AttributeType::UnsignedScaled; case Maxwell::VertexAttribute::Type::SScaled: return Shader::AttributeType::SignedScaled; } return Shader::AttributeType::Float; } Shader::AttributeType AttributeType(const FixedPipelineState& state, size_t index) { switch (state.DynamicAttributeType(index)) { case 0: return Shader::AttributeType::Disabled; case 1: return Shader::AttributeType::Float; case 2: return Shader::AttributeType::SignedInt; case 3: return Shader::AttributeType::UnsignedInt; } return Shader::AttributeType::Disabled; } Shader::RuntimeInfo MakeRuntimeInfo(std::span programs, const GraphicsPipelineCacheKey& key, const Shader::IR::Program& program, const Shader::IR::Program* previous_program, const Vulkan::Device& device) { Shader::RuntimeInfo info; if (previous_program) { info.previous_stage_stores = previous_program->info.stores; info.previous_stage_legacy_stores_mapping = previous_program->info.legacy_stores_mapping; if (previous_program->is_geometry_passthrough) { info.previous_stage_stores.mask |= previous_program->info.passthrough.mask; } } else { info.previous_stage_stores.mask.set(); } if (previous_program && previous_program->stage == Shader::Stage::Mesh) { for (const u32 line : previous_program->mesh.primitive_lines) { const auto attribute{static_cast(line)}; if (Shader::IR::IsGeneric(attribute)) { info.per_primitive_generics.set(Shader::IR::GenericAttributeIndex(attribute)); } } } const Shader::Stage stage{program.stage}; const bool has_geometry{key.unique_hashes[4] != 0 && !programs[4].is_geometry_passthrough}; const bool gl_ndc{key.state.ndc_minus_one_to_one != 0}; const float point_size{std::bit_cast(key.state.point_size)}; switch (stage) { case Shader::Stage::VertexB: if (!has_geometry) { if (key.state.topology == Maxwell::PrimitiveTopology::Points) { info.fixed_state_point_size = point_size; } if (key.state.xfb_enabled) { if (device.IsExtTransformFeedbackSupported()) { auto [varyings, count] = VideoCommon::MakeTransformFeedbackVaryings(key.state.xfb_state); info.xfb_varyings = varyings; info.xfb_count = count; } else { LOG_WARNING(Render_Vulkan, "XFB requested in pipeline key but device lacks VK_EXT_transform_feedback; ignoring XFB decorations"); } } info.convert_depth_mode = gl_ndc; } if (key.state.dynamic_vertex_input) { for (size_t index = 0; index < Maxwell::NumVertexAttributes; ++index) { info.generic_input_types[index] = AttributeType(key.state, index); } } else { std::ranges::transform(key.state.attributes, info.generic_input_types.begin(), &CastAttributeType); } break; case Shader::Stage::TessellationEval: info.tess_clockwise = key.state.tessellation_clockwise != 0; info.tess_primitive = [&key] { const u32 raw{key.state.tessellation_primitive.Value()}; switch (static_cast(raw)) { case Maxwell::Tessellation::DomainType::Isolines: return Shader::TessPrimitive::Isolines; case Maxwell::Tessellation::DomainType::Triangles: return Shader::TessPrimitive::Triangles; case Maxwell::Tessellation::DomainType::Quads: return Shader::TessPrimitive::Quads; } ASSERT(false); return Shader::TessPrimitive::Triangles; }(); info.tess_spacing = [&] { const u32 raw{key.state.tessellation_spacing}; switch (static_cast(raw)) { case Maxwell::Tessellation::Spacing::Integer: return Shader::TessSpacing::Equal; case Maxwell::Tessellation::Spacing::FractionalOdd: return Shader::TessSpacing::FractionalOdd; case Maxwell::Tessellation::Spacing::FractionalEven: return Shader::TessSpacing::FractionalEven; } ASSERT(false); return Shader::TessSpacing::Equal; }(); break; case Shader::Stage::Mesh: info.convert_depth_mode = gl_ndc; break; case Shader::Stage::Geometry: if (program.output_topology == Shader::OutputTopology::PointList) { info.fixed_state_point_size = point_size; } if (key.state.xfb_enabled != 0) { if (device.IsExtTransformFeedbackSupported()) { auto [varyings, count] = VideoCommon::MakeTransformFeedbackVaryings(key.state.xfb_state); info.xfb_varyings = varyings; info.xfb_count = count; } else { LOG_WARNING(Render_Vulkan, "XFB requested in pipeline key but device lacks VK_EXT_transform_feedback; ignoring XFB decorations"); } } info.convert_depth_mode = gl_ndc; break; case Shader::Stage::Fragment: { info.alpha_test_func = MaxwellToCompareFunction( key.state.UnpackComparisonOp(key.state.alpha_test_func.Value())); info.alpha_test_reference = std::bit_cast(key.state.alpha_test_ref); info.dual_source_blend = key.state.attachment0_dual_source_blend != 0; if (device.IsMoltenVK()) { for (size_t i = 0; i < 8; ++i) { const auto format = static_cast(key.state.color_formats[i]); if (format == Tegra::RenderTargetFormat::NONE) { info.color_output_types[i] = Shader::AttributeType::Float; continue; } const auto pixel_format = VideoCore::Surface::PixelFormatFromRenderTargetFormat(format); if (VideoCore::Surface::IsPixelFormatInteger(pixel_format)) { if (VideoCore::Surface::IsPixelFormatSignedInteger(pixel_format)) { info.color_output_types[i] = Shader::AttributeType::SignedInt; } else { info.color_output_types[i] = Shader::AttributeType::UnsignedInt; } } else { info.color_output_types[i] = Shader::AttributeType::Float; } } } break; } default: break; } switch (key.state.topology) { case Maxwell::PrimitiveTopology::Points: info.input_topology = Shader::InputTopology::Points; break; case Maxwell::PrimitiveTopology::Lines: case Maxwell::PrimitiveTopology::LineLoop: case Maxwell::PrimitiveTopology::LineStrip: info.input_topology = Shader::InputTopology::Lines; break; case Maxwell::PrimitiveTopology::Triangles: case Maxwell::PrimitiveTopology::TriangleStrip: case Maxwell::PrimitiveTopology::TriangleFan: case Maxwell::PrimitiveTopology::Quads: case Maxwell::PrimitiveTopology::QuadStrip: case Maxwell::PrimitiveTopology::Polygon: case Maxwell::PrimitiveTopology::Patches: info.input_topology = Shader::InputTopology::Triangles; break; case Maxwell::PrimitiveTopology::LinesAdjacency: case Maxwell::PrimitiveTopology::LineStripAdjacency: info.input_topology = Shader::InputTopology::LinesAdjacency; break; case Maxwell::PrimitiveTopology::TrianglesAdjacency: case Maxwell::PrimitiveTopology::TriangleStripAdjacency: info.input_topology = Shader::InputTopology::TrianglesAdjacency; break; } info.force_early_z = key.state.early_z != 0; info.y_negate = key.state.y_negate != 0; return info; } size_t GetTotalPipelineWorkers() { const size_t max_core_threads = std::max(static_cast(std::thread::hardware_concurrency()), 2ULL) - 1ULL; #ifdef __ANDROID__ const int configured = AndroidSettings::values.pipeline_worker_count.GetValue(); const int clamped = std::clamp(configured, 2, 8); const size_t desired = static_cast(clamped); if (desired == 0) { return 1ULL; } return std::min(max_core_threads, desired); #else return max_core_threads; #endif } } // Anonymous namespace size_t ComputePipelineCacheKey::Hash() const noexcept { const u64 hash = Common::CityHash64(reinterpret_cast(this), sizeof *this); return static_cast(hash); } bool ComputePipelineCacheKey::operator==(const ComputePipelineCacheKey& rhs) const noexcept { return std::memcmp(&rhs, this, sizeof *this) == 0; } size_t GraphicsPipelineCacheKey::Hash() const noexcept { const u64 hash = Common::CityHash64(reinterpret_cast(this), Size()); return static_cast(hash); } bool GraphicsPipelineCacheKey::operator==(const GraphicsPipelineCacheKey& rhs) const noexcept { return std::memcmp(&rhs, this, Size()) == 0; } namespace { // NVIDIA's and AMD's drivers keep a disk cache of built pipelines of their own. Eden's copy on top // only kept every pipeline the title ever built in host memory (title 1: a 245 MB file, ~0.35 GB // of RAM for the session) and copied it whole at each save, which stalled frames; pipelines built // no slower without it. bool DriverCachesPipelines(const Device& device) { switch (device.GetDriverID()) { case VK_DRIVER_ID_NVIDIA_PROPRIETARY: case VK_DRIVER_ID_AMD_PROPRIETARY: return true; default: return false; } } } // Anonymous namespace PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_, GuestDescriptorQueue& guest_descriptor_queue_, DescriptorBufferRing& descriptor_buffer_ring_, RenderPassCache& render_pass_cache_, BufferCache& buffer_cache_, TextureCache& texture_cache_, VideoCore::ShaderNotify& shader_notify_) : VideoCommon::ShaderCache{device_memory_}, device{device_}, scheduler{scheduler_}, descriptor_pool{descriptor_pool_}, guest_descriptor_queue{guest_descriptor_queue_}, descriptor_buffer_ring{descriptor_buffer_ring_}, render_pass_cache{render_pass_cache_}, buffer_cache{buffer_cache_}, texture_cache{texture_cache_}, shader_notify{shader_notify_}, use_asynchronous_shaders{Settings::values.use_asynchronous_shaders.GetValue()}, use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue() && !DriverCachesPipelines(device_)}, workers(device.HasBrokenParallelShaderCompiling() ? 1ULL : GetTotalPipelineWorkers(), "VkPipelineBuilder", {}, Common::ThreadPlacement::Background), serialization_thread(1, "VkPipelineSerialization", {}, Common::ThreadPlacement::Background) { const auto& float_control{device.FloatControlProperties()}; const VkDriverId driver_id{device.GetDriverID()}; const VkShaderStageFlags subgroup_stages{device.GetSubgroupSupportedStages()}; const auto subgroup_stage_bit{[subgroup_stages](VkShaderStageFlags flag, Shader::Stage stage) { return (subgroup_stages & flag) != 0 ? (1u << static_cast(stage)) : 0u; }}; u32 supported_subgroup_stages{ subgroup_stage_bit(VK_SHADER_STAGE_VERTEX_BIT, Shader::Stage::VertexA) | subgroup_stage_bit(VK_SHADER_STAGE_VERTEX_BIT, Shader::Stage::VertexB) | subgroup_stage_bit(VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT, Shader::Stage::TessellationControl) | subgroup_stage_bit(VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT, Shader::Stage::TessellationEval) | subgroup_stage_bit(VK_SHADER_STAGE_GEOMETRY_BIT, Shader::Stage::Geometry) | subgroup_stage_bit(VK_SHADER_STAGE_FRAGMENT_BIT, Shader::Stage::Fragment) | subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute) | subgroup_stage_bit(VK_SHADER_STAGE_TASK_BIT_EXT, Shader::Stage::Task) | subgroup_stage_bit(VK_SHADER_STAGE_MESH_BIT_EXT, Shader::Stage::Mesh)}; if (driver_id == VK_DRIVER_ID_MESA_TURNIP) { supported_subgroup_stages &= ~(1u << static_cast(Shader::Stage::Geometry)); } profile = Shader::Profile{ .supported_spirv = device.SupportedSpirvVersion(), .unified_descriptor_binding = true, .support_descriptor_aliasing = device.IsDescriptorAliasingSupported(), .support_int8 = device.IsInt8Supported(), .support_uniform_and_storage_buffer_8bit = device.IsUniformAndStorageBuffer8BitAccessSupported(), .support_storage_buffer_8bit = device.IsStorageBuffer8BitAccessSupported(), .support_int16 = device.IsShaderInt16Supported(), .support_uniform_and_storage_buffer_16bit = device.IsUniformAndStorageBuffer16BitAccessSupported(), .support_storage_buffer_16bit = device.IsStorageBuffer16BitAccessSupported(), .support_int64 = device.IsShaderInt64Supported(), .support_vertex_instance_id = false, .support_float_controls = device.IsKhrShaderFloatControlsSupported(), .support_separate_denorm_behavior = float_control.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, .support_separate_rounding_mode = float_control.roundingModeIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, .support_fp16_denorm_preserve = float_control.shaderDenormPreserveFloat16 != VK_FALSE, .support_fp32_denorm_preserve = float_control.shaderDenormPreserveFloat32 != VK_FALSE, .support_fp16_denorm_flush = float_control.shaderDenormFlushToZeroFloat16 != VK_FALSE, .support_fp32_denorm_flush = float_control.shaderDenormFlushToZeroFloat32 != VK_FALSE, .support_fp16_signed_zero_nan_preserve = float_control.shaderSignedZeroInfNanPreserveFloat16 != VK_FALSE, .support_fp32_signed_zero_nan_preserve = float_control.shaderSignedZeroInfNanPreserveFloat32 != VK_FALSE, .support_fp64_signed_zero_nan_preserve = float_control.shaderSignedZeroInfNanPreserveFloat64 != VK_FALSE, .support_explicit_workgroup_layout = device.IsKhrWorkgroupMemoryExplicitLayoutSupported(), .support_workgroup_layout_8bit_access = device.IsWorkgroupMemoryExplicitLayout8BitAccessSupported(), .support_workgroup_layout_16bit_access = device.IsWorkgroupMemoryExplicitLayout16BitAccessSupported(), .support_shader_quad_control = device.IsKhrShaderQuadControlSupported(), .support_quad_shuffles = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_QUAD_BIT), .support_vote = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_VOTE_BIT), .support_shuffle_relative = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT), .supported_subgroup_stages = supported_subgroup_stages, .support_viewport_index_layer_non_geometry = device.IsExtShaderViewportIndexLayerSupported(), .support_viewport_mask = device.IsNvViewportArray2Supported(), .support_typeless_image_loads = device.IsFormatlessImageLoadSupported(), .support_demote_to_helper_invocation = device.IsExtShaderDemoteToHelperInvocationSupported(), .support_int64_atomics = device.IsExtShaderAtomicInt64Supported(), .support_shared_int64_atomics = device.IsSharedInt64AtomicsSupported(), .support_derivative_control = true, .support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(), .support_native_ndc = device.IsExtDepthClipControlSupported(), .support_scaled_attributes = !device.MustEmulateScaledFormats(), .support_multi_viewport = device.SupportsMultiViewport(), .support_geometry_streams = device.AreTransformFeedbackGeometryStreamsSupported(), .support_sampled_image_array_nonuniform_indexing = device.IsSampledImageArrayNonUniformIndexingSupported(), .support_storage_image_array_nonuniform_indexing = device.IsStorageImageArrayNonUniformIndexingSupported(), .support_uniform_texel_buffer_array_nonuniform_indexing = device.IsUniformTexelBufferArrayNonUniformIndexingSupported(), .support_storage_texel_buffer_array_nonuniform_indexing = device.IsStorageTexelBufferArrayNonUniformIndexingSupported(), .warp_size_potentially_larger_than_guest = device.IsWarpSizePotentiallyBiggerThanGuest(), .lower_left_origin_mode = false, .need_declared_frag_colors = false, .need_gather_subpixel_offset = driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE || driver_id == VK_DRIVER_ID_MESA_RADV || driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS || driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA || driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY || driver_id == VK_DRIVER_ID_MESA_TURNIP, .has_broken_spirv_clamp = driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, .has_broken_spirv_position_input = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY, .has_broken_unsigned_image_offsets = false, .has_broken_signed_operations = false, .has_broken_fp16_float_controls = driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY, .has_broken_fp32_denorm_flush = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY, .ignore_nan_fp_comparisons = false, .has_broken_spirv_subgroup_mask_vector_extract_dynamic = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY && device.GetDriverVersion() < VK_MAKE_VERSION(512, 672, 0), .has_broken_spirv_subgroup_shuffle = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY, .max_subgroup_size = device.GetMaxSubgroupSize(), .has_broken_robust = device.IsNvidia() && device.GetNvidiaArch() <= NvidiaArchitecture::Arch_Pascal, .min_ssbo_alignment = device.GetStorageBufferAlignment(), .max_user_clip_distances = device.GetMaxUserClipDistances() }; host_info = Shader::HostTranslateInfo{ .min_ssbo_alignment = device.GetStorageBufferAlignment(), .max_per_stage_descriptor_sampled_images = device.GetMaxPerStageDescriptorSampledImages(), .max_per_stage_resources = device.GetMaxPerStageResources(), .max_descriptor_set_samplers = device.GetMaxDescriptorSetSamplers(), .max_descriptor_set_uniform_buffers = device.GetMaxDescriptorSetUniformBuffers(), .max_descriptor_set_uniform_buffers_dynamic = device.GetMaxDescriptorSetUniformBuffersDynamic(), .max_descriptor_set_storage_buffers = device.GetMaxDescriptorSetStorageBuffers(), .max_descriptor_set_storage_buffers_dynamic = device.GetMaxDescriptorSetStorageBuffersDynamic(), .max_descriptor_set_sampled_images = device.GetMaxDescriptorSetSampledImages(), .max_descriptor_set_storage_images = device.GetMaxDescriptorSetStorageImages(), .max_descriptor_set_input_attachements = device.GetMaxDescriptorSetInputAttachments(), .support_float64 = device.IsFloat64Supported(), .support_float16 = device.IsFloat16Supported(), .support_int64 = device.IsShaderInt64Supported(), .needs_demote_reorder = driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE || driver_id == VK_DRIVER_ID_SAMSUNG_PROPRIETARY, .support_snorm_render_buffer = true, .support_viewport_index_layer = device.IsExtShaderViewportIndexLayerSupported(), .support_viewport_mask = device.IsNvViewportArray2Supported(), .support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(), .support_conditional_barrier = device.SupportsConditionalBarriers(), .single_lane_geometry_subgroups = !profile.SupportsSubgroupStage(Shader::Stage::Geometry), }; host_info.ApplyDescriptorLimitPolicy(); if (device.GetMaxVertexInputAttributes() < Maxwell::NumVertexAttributes) { LOG_WARNING(Render_Vulkan, "maxVertexInputAttributes is too low: {} < {}", device.GetMaxVertexInputAttributes(), Maxwell::NumVertexAttributes); } if (device.GetMaxVertexInputBindings() < Maxwell::NumVertexArrays) { LOG_WARNING(Render_Vulkan, "maxVertexInputBindings is too low: {} < {}", device.GetMaxVertexInputBindings(), Maxwell::NumVertexArrays); } LOG_INFO(Render_Vulkan, "DynamicState setting value: {}", u32(Settings::values.dyna_state.GetValue())); dynamic_features = {}; dynamic_features.driver_id = device.GetDriverID(); dynamic_features.driver_version = device.GetDriverVersion(); // User granularity enforced in vulkan_device.cpp switch statement: // Level 0: Core Dynamic States only // Level 1: Core + EDS1 // Level 2: Core + EDS1 + EDS2 (accumulative) // Level 3: Core + EDS1 + EDS2 + EDS3 (accumulative) // Here we only verify if extensions were successfully loaded by the device dynamic_features.has_extended_dynamic_state = device.IsExtExtendedDynamicStateSupported(); dynamic_features.has_extended_dynamic_state_2 = device.IsExtExtendedDynamicState2Supported(); dynamic_features.has_extended_dynamic_state_2_logic_op = device.IsExtExtendedDynamicState2ExtrasSupported(); dynamic_features.has_extended_dynamic_state_2_patch_control_points = false; dynamic_features.has_extended_dynamic_state_3_blend = device.IsExtExtendedDynamicState3BlendingSupported(); dynamic_features.has_extended_dynamic_state_3_enables = device.IsExtExtendedDynamicState3EnablesSupported(); dynamic_features.has_color_write_enable = device.IsExtColorWriteEnableSupported(); dynamic_features.has_dynamic_state3_depth_clamp_enable = dynamic_features.has_extended_dynamic_state_3_enables && device.SupportsDynamicState3DepthClampEnable(); dynamic_features.has_dynamic_state3_logic_op_enable = dynamic_features.has_extended_dynamic_state_3_enables && device.SupportsDynamicState3LogicOpEnable(); dynamic_features.has_dynamic_state3_line_stipple_enable = dynamic_features.has_extended_dynamic_state_3_enables && device.SupportsDynamicState3LineStippleEnable(); // VIDS: Independent toggle (not affected by dyna_state levels) dynamic_features.has_dynamic_vertex_input = device.IsExtVertexInputDynamicStateSupported() && Settings::values.vertex_input_dynamic_state.GetValue(); dynamic_features.has_static_vertex_repack = StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment()); dynamic_features.has_provoking_vertex = device.IsExtProvokingVertexSupported(); dynamic_features.has_provoking_vertex_first_mode = device.SupportsProvokingVertexFirstMode(); dynamic_features.has_provoking_vertex_last_mode = device.SupportsProvokingVertexLastMode(); dynamic_features.has_provoking_vertex_tf_preserve = device.SupportsTransformFeedbackProvokingVertexPreservation(); } namespace { // /vulkan_used.bin: the graphics pipelines recent sessions used, as key hash + sessions // since the last use. constexpr u32 UsedPipelinesMagic = 0x31505545; // "EUP1" // Pipelines the last two sessions used are built at boot. Each holds ~0.45 MB of driver memory: // title 1 built 4,800 from eight sessions, 1.5 GB more than building them on first use (but that // stalled frames 4x as often), while one session of its title screen uses ~1,500. // MoltenVK keeps the previous eight sessions: a pipeline built while playing goes through // SPIR-V -> MSL -> Metal, far more than the 2-3 ms the shorter window assumes, so each one // built at boot is a stutter avoided. constexpr u8 UsedPipelinesMaxAge = 2; constexpr u8 UsedPipelinesMaxAgeMoltenVK = 8; u8 MaxUsedPipelineAge(const Device& device) { return device.IsMoltenVK() ? UsedPipelinesMaxAgeMoltenVK : UsedPipelinesMaxAge; } std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path, u8 max_age) { std::unordered_map<u64, u8> used; std::ifstream file{path, std::ios::binary}; u32 magic{}, count{}; if (!file.read(reinterpret_cast<char*>(&magic), sizeof(magic)) || magic != UsedPipelinesMagic || !file.read(reinterpret_cast<char*>(&count), sizeof(count))) { return used; } used.reserve(count); for (u32 i = 0; i < count; ++i) { u64 hash{}; u8 age{}; if (!file.read(reinterpret_cast<char*>(&hash), sizeof(hash)) || !file.read(reinterpret_cast<char*>(&age), sizeof(age))) { break; } if (age < max_age) { // lists saved with a longer window used.emplace(hash, age); } } return used; } // Without a list of the pipelines recent sessions used (the first boot with this cache, or a session // that ended before saving it), this many cached graphics pipelines at most are built at boot and the // rest when first needed. A cache that grew over many builds held 48k pipelines, ~21 GB of driver // memory built before the game started; the list is only saved at exit or after two minutes, so a // boot that ran out of memory never saved one and the next boot did the same. constexpr size_t MaxPipelinesWithoutUsedList = 3000; // EDEN_NX2_PRELOAD_ALL_PIPELINES=1 builds every cached pipeline at boot, as before. bool PreloadAllPipelines() { static const bool all = [] { const char* value = std::getenv("EDEN_NX2_PRELOAD_ALL_PIPELINES"); return value != nullptr && value[0] == '1'; }(); return all; } } // Anonymous namespace PipelineCache::~PipelineCache() { SaveUsedPipelines(); if (use_vulkan_pipeline_cache && !vulkan_pipeline_cache_filename.empty()) { SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, CACHE_VERSION); } } void PipelineCache::SaveUsedPipelines() { used_pipelines_saved = std::chrono::steady_clock::now(); // A session too short to say what the game uses (a boot to the menu) keeps the old list. constexpr size_t MinimumUsed = 64; if (used_pipelines_filename.empty() || used_this_session.size() + used_and_evicted.size() < MinimumUsed) { return; } std::unordered_map<u64, u8> next; next.reserve(used_pipelines.size() + used_this_session.size()); for (const auto& [key, pipeline] : graphics_cache) { if (used_this_session.contains(pipeline.get())) { next.emplace(key.Hash(), u8{0}); } } for (const u64 hash : used_and_evicted) { next.emplace(hash, u8{0}); } for (const auto& [hash, age] : used_pipelines) { if (age + 1 < MaxUsedPipelineAge(device)) { next.try_emplace(hash, static_cast<u8>(age + 1)); } } std::ofstream file{used_pipelines_filename, std::ios::binary | std::ios::trunc}; const u32 count = static_cast<u32>(next.size()); file.write(reinterpret_cast<const char*>(&UsedPipelinesMagic), sizeof(UsedPipelinesMagic)); file.write(reinterpret_cast<const char*>(&count), sizeof(count)); for (const auto& [hash, age] : next) { file.write(reinterpret_cast<const char*>(&hash), sizeof(hash)); file.write(reinterpret_cast<const char*>(&age), sizeof(age)); } } GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() { if (!RefreshStages(graphics_key.unique_hashes)) { current_pipeline = nullptr; return nullptr; } graphics_key.state.Refresh(*maxwell3d, dynamic_features); if (current_pipeline) { GraphicsPipeline* const next{current_pipeline->Next(graphics_key)}; if (next) { current_pipeline = next; current_pipeline->MarkUsed(frame_count); return BuiltPipeline(current_pipeline); } } return CurrentGraphicsPipelineSlowPath(); } ComputePipeline* PipelineCache::CurrentComputePipeline() { const ShaderInfo* const shader{ComputeShader()}; if (!shader) { return nullptr; } const auto& qmd{kepler_compute->launch_description}; const ComputePipelineCacheKey key{ .unique_hash = shader->unique_hash, .shared_memory_size = qmd.shared_alloc, .workgroup_size{qmd.block_dim_x, qmd.block_dim_y, qmd.block_dim_z}, }; const auto [pair, is_new]{compute_cache.try_emplace(key)}; auto& pipeline{pair->second}; if (!is_new) { return pipeline.get(); } pipeline = CreateComputePipeline(key, shader); return pipeline.get(); } void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading, const VideoCore::DiskResourceLoadCallback& callback) { if (title_id == 0) { return; } if (!pipeline_cache_filename.empty()) { serialization_thread.WaitForRequests(); if (use_vulkan_pipeline_cache && !vulkan_pipeline_cache_filename.empty()) { SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, CACHE_VERSION); } } const auto shader_dir{Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir)}; const auto base_dir{shader_dir / fmt::format("{:016x}", title_id)}; if (!Common::FS::CreateDir(shader_dir) || !Common::FS::CreateDir(base_dir)) { LOG_ERROR(Common_Filesystem, "Failed to create pipeline cache directories"); return; } pipeline_cache_filename = base_dir / "vulkan.bin"; used_pipelines_filename = base_dir / "vulkan_used.bin"; used_pipelines = LoadUsedPipelines(used_pipelines_filename, MaxUsedPipelineAge(device)); used_pipelines_saved = std::chrono::steady_clock::now(); size_t skipped_pipelines = 0; size_t queued_graphics = 0; if (use_vulkan_pipeline_cache) { vulkan_pipeline_cache_filename = base_dir / "vulkan_pipelines.bin"; vulkan_pipeline_cache = LoadVulkanPipelineCache(vulkan_pipeline_cache_filename, CACHE_VERSION); } struct { std::mutex mutex; size_t total{}; size_t built{}; bool has_loaded{}; std::unique_ptr<PipelineStatistics> statistics; } state; if (device.IsKhrPipelineExecutablePropertiesEnabled()) { state.statistics = std::make_unique<PipelineStatistics>(device); } const auto load_compute{[&](std::ifstream& file, FileEnvironment env) { ComputePipelineCacheKey key; file.read(reinterpret_cast<char*>(&key), sizeof(key)); workers.QueueWork([this, key, env_ = std::move(env), &state, &callback]() mutable { ShaderPools pools; auto pipeline{CreateComputePipeline(pools, key, env_, state.statistics.get(), false)}; std::scoped_lock lock{state.mutex}; if (pipeline) { compute_cache.emplace(key, std::move(pipeline)); } ++state.built; if (state.has_loaded) { callback(VideoCore::LoadCallbackStage::Build, state.built, state.total); } }); ++state.total; }}; const auto load_graphics{[&](std::ifstream& file, std::vector<FileEnvironment> envs) { GraphicsPipelineCacheKey key; file.read(reinterpret_cast<char*>(&key), sizeof(key)); // Task/mesh pipelines are not stored (their mesh registers are not in the format); skip // any that were. if (std::ranges::any_of(envs, [](const FileEnvironment& env) { return env.ShaderStage() == Shader::Stage::Task || env.ShaderStage() == Shader::Stage::Mesh; })) { return; } if ((key.state.extended_dynamic_state != 0) != dynamic_features.has_extended_dynamic_state || (key.state.extended_dynamic_state_2 != 0) != dynamic_features.has_extended_dynamic_state_2 || (key.state.extended_dynamic_state_2_logic_op != 0) != dynamic_features.has_extended_dynamic_state_2_logic_op || (key.state.extended_dynamic_state_3_blend != 0) != dynamic_features.has_extended_dynamic_state_3_blend || (key.state.extended_dynamic_state_3_enables != 0) != dynamic_features.has_extended_dynamic_state_3_enables || (key.state.color_write_enable_dynamic != 0) != dynamic_features.has_color_write_enable || (key.state.dynamic_vertex_input != 0) != dynamic_features.has_dynamic_vertex_input) { return; } const bool key_requests_provoking_last = key.state.provoking_vertex_last != 0; if (key_requests_provoking_last && !dynamic_features.has_provoking_vertex_last_mode) { return; } const bool key_uses_transform_feedback = key.state.xfb_enabled != 0; if (key_uses_transform_feedback && key_requests_provoking_last && !dynamic_features.has_provoking_vertex_tf_preserve) { return; } const u64 hash = key.Hash(); disk_pipelines.insert(hash); if (!PreloadAllPipelines() && (used_pipelines.empty() ? queued_graphics >= MaxPipelinesWithoutUsedList : !used_pipelines.contains(hash))) { ++skipped_pipelines; return; } ++queued_graphics; workers.QueueWork([this, key, envs_ = std::move(envs), &state, &callback]() mutable { ShaderPools pools; boost::container::static_vector<Shader::Environment*, 5> env_ptrs; for (auto& env : envs_) { env_ptrs.push_back(&env); } auto pipeline{CreateGraphicsPipeline(pools, key, MakeSpan(env_ptrs), state.statistics.get(), false)}; std::scoped_lock lock{state.mutex}; if (pipeline) { graphics_cache.emplace(key, std::move(pipeline)); } ++state.built; if (state.has_loaded) { callback(VideoCore::LoadCallbackStage::Build, state.built, state.total); } }); ++state.total; }}; VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute, load_graphics); LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total); if (skipped_pipelines != 0) { LOG_INFO(Render_Vulkan, "{} cached graphics pipelines are built when first needed", skipped_pipelines); } std::unique_lock lock{state.mutex}; callback(VideoCore::LoadCallbackStage::Build, 0, state.total); state.has_loaded = true; lock.unlock(); workers.WaitForRequests(stop_loading); if (use_vulkan_pipeline_cache) { SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, CACHE_VERSION); size_t size = 0; vulkan_pipeline_cache.Read(&size, nullptr); last_cache_size.store(size, std::memory_order_relaxed); last_flush = std::chrono::steady_clock::now(); } if (state.statistics) { state.statistics->Report(); } } void PipelineCache::QueueVulkanPipelineCacheFlush() { if (!use_vulkan_pipeline_cache || vulkan_pipeline_cache_filename.empty()) { return; } if (++pipelines_since_flush < VULKAN_CACHE_FLUSH_PIPELINES) { return; } const auto now = std::chrono::steady_clock::now(); const auto megabytes = last_cache_size.load(std::memory_order_relaxed) / (1024 * 1024); const std::chrono::seconds interval{ std::max<size_t>(VULKAN_CACHE_FLUSH_MIN_SECONDS, megabytes)}; if (last_flush.time_since_epoch().count() != 0 && now - last_flush < interval) { return; } if (flush_in_flight.exchange(true, std::memory_order_acq_rel)) { return; } pipelines_since_flush = 0; last_flush = now; serialization_thread.QueueWork([this] { SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, CACHE_VERSION); size_t size = 0; vulkan_pipeline_cache.Read(&size, nullptr); last_cache_size.store(size, std::memory_order_relaxed); flush_in_flight.store(false, std::memory_order_release); }); } namespace { // EDEN_NX2_PIPELINE_DIFF=1: for a new graphics pipeline whose shaders an earlier one uses too, // log which parts of the fixed state differ from that one (finds state that splits the same // draws into many pipelines, each translated on the GPU thread). void ReportNewPipelineKey(const GraphicsPipelineCacheKey& key) { static const bool enabled = std::getenv("EDEN_NX2_PIPELINE_DIFF") != nullptr; if (!enabled) { return; } static std::unordered_map<u64, GraphicsPipelineCacheKey> last_by_shaders; static u32 logged{}; const u64 shaders = Common::CityHash64(reinterpret_cast<const char*>(key.unique_hashes.data()), sizeof(key.unique_hashes)); const auto [it, first] = last_by_shaders.try_emplace(shaders, key); if (first) { return; } const FixedPipelineState& now = key.state; const FixedPipelineState& before = it->second.state; std::string diffs; if (now.raw1 != before.raw1) diffs += fmt::format(" raw1^{:#x}", now.raw1 ^ before.raw1); if (now.raw2 != before.raw2) diffs += fmt::format(" raw2^{:#x}", now.raw2 ^ before.raw2); if (now.color_formats != before.color_formats) { diffs += fmt::format(" color_formats {:02x} -> {:02x}", fmt::join(before.color_formats, ""), fmt::join(now.color_formats, "")); } if (now.alpha_test_ref != before.alpha_test_ref) { diffs += fmt::format(" alpha_test_ref {:#x} -> {:#x} (func {})", before.alpha_test_ref, now.alpha_test_ref, now.alpha_test_func.Value()); } if (now.point_size != before.point_size) { diffs += fmt::format(" point_size {:#x} -> {:#x}", before.point_size, now.point_size); } if (now.viewport_swizzles != before.viewport_swizzles) diffs += " viewport_swizzles"; if (now.attribute_types != before.attribute_types) { diffs += fmt::format(" attribute_types^{:#x}", now.attribute_types ^ before.attribute_types); } if (std::memcmp(&now.dynamic_state, &before.dynamic_state, sizeof(now.dynamic_state)) != 0) { diffs += " dynamic_state"; } if (now.driver_id != before.driver_id || now.driver_version != before.driver_version) { diffs += " driver"; } const size_t size = now.Size(); if (diffs.empty() && size > offsetof(FixedPipelineState, attachments)) { diffs = fmt::format(" past attachments (key size {})", size); } it->second = key; if (logged++ < 400) { LOG_INFO(Render_Vulkan, "New pipeline for known shaders {:016x} differs in:{}", shaders, diffs.empty() ? std::string{" nothing in the key prefix"} : diffs); } } } // Anonymous namespace GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() { const auto [pair, is_new]{graphics_cache.try_emplace(graphics_key)}; auto& pipeline{pair->second}; if (is_new) { ReportNewPipelineKey(graphics_key); pipeline = CreateGraphicsPipeline(); } if (!pipeline) { return nullptr; } if (used_this_session.insert(pipeline.get()).second && std::chrono::steady_clock::now() - used_pipelines_saved > std::chrono::minutes{2}) { SaveUsedPipelines(); } if (current_pipeline) { current_pipeline->AddTransition(pipeline.get()); } current_pipeline = pipeline.get(); current_pipeline->MarkUsed(frame_count); return BuiltPipeline(current_pipeline); } void PipelineCache::TickFrame() { ++frame_count; while (!evicted_pipelines.empty() && scheduler.IsFree(evicted_pipelines.front().first)) { evicted_pipelines.pop_front(); } constexpr u64 EvictCheckFrames = 600; if (frame_count % EvictCheckFrames == 0) { EvictUnusedPipelines(); } } void PipelineCache::EvictUnusedPipelines() { // Building a pipeline again on MoltenVK means a Metal compile mid-game, far dearer than the // 2-3 ms below, so none are evicted there. if (device.IsMoltenVK()) { return; } // Each built pipeline holds ~0.45 MB of driver memory and none was ever destroyed, so it grew // with every place a session went (title 1: 1,500 on its title screen alone). One unused for // this long is destroyed; it takes 2-3 ms to build again from the driver's pipeline cache. constexpr u64 EvictAfterFrames = 9000; if (frame_count <= EvictAfterFrames) { return; } const u64 cutoff = frame_count - EvictAfterFrames; const u64 tick = scheduler.CurrentTick(); size_t evicted = 0; for (auto it = graphics_cache.begin(); it != graphics_cache.end();) { GraphicsPipeline* const pipeline = it->second.get(); if (pipeline == nullptr || pipeline == current_pipeline || !pipeline->IsBuilt() || pipeline->LastUse() >= cutoff) { ++it; continue; } if (used_this_session.erase(pipeline) != 0) { used_and_evicted.insert(it->first.Hash()); } evicted_pipelines.emplace_back(tick, std::move(it->second)); it = graphics_cache.erase(it); ++evicted; } if (evicted == 0) { return; } for (auto& [key, pipeline] : graphics_cache) { if (pipeline) { pipeline->ClearTransitions(); } } LOG_INFO(Render_Vulkan, "Evicted {} graphics pipelines unused for {} frames, {} left", evicted, EvictAfterFrames, graphics_cache.size()); } GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const noexcept { if (pipeline->IsBuilt()) { return pipeline; } if (!use_asynchronous_shaders) { 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 const auto& draw_state = maxwell3d->draw_manager.draw_state; if (draw_state.draw_element_count <= 6) { return pipeline; } return nullptr; } std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline( ShaderPools& pools, const GraphicsPipelineCacheKey& key, std::span<Shader::Environment* const> envs, PipelineStatistics* statistics, bool build_in_parallel) try { auto hash = key.Hash(); LOG_DEBUG(Render_Vulkan, "{:#016x}", hash); size_t env_index{0}; std::array<Shader::IR::Program, Maxwell::MaxShaderProgram> programs; const bool uses_vertex_a{key.unique_hashes[0] != 0}; const bool uses_vertex_b{key.unique_hashes[1] != 0}; // Layer passthrough generation for devices without VK_EXT_shader_viewport_index_layer Shader::IR::Program* layer_source_program{}; for (size_t index = 0; index < Maxwell::MaxShaderProgram; ++index) { const bool is_emulated_stage = layer_source_program != nullptr && index == static_cast<u32>(Maxwell::ShaderType::Geometry); if (key.unique_hashes[index] == 0 && is_emulated_stage) { auto topology = MaxwellToOutputTopology(key.state.topology); programs[index] = GenerateGeometryPassthrough(pools.inst, pools.block, host_info, *layer_source_program, topology); continue; } if (key.unique_hashes[index] == 0) { continue; } Shader::Environment& env{*envs[env_index]}; ++env_index; const u32 cfg_offset{static_cast<u32>(env.StartAddress() + sizeof(Shader::ProgramHeader))}; Shader::Maxwell::Flow::CFG cfg(env, pools.flow_block, cfg_offset, index == 0); if (!uses_vertex_a || index != 1) { // Normal path programs[index] = TranslateProgram(pools.inst, pools.block, env, cfg, host_info); } else { // VertexB path when VertexA is present. auto& program_va{programs[0]}; auto program_vb{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)}; programs[index] = MergeDualVertexPrograms(program_va, program_vb, env); } if (device.GetDriverID() != VK_DRIVER_ID_NVIDIA_PROPRIETARY && Shader::Optimization::HasBrokenPattern(programs[index])) { LOG_WARNING(Render_Vulkan, "0x{:016x}: Skipping shader with known issues", key.unique_hashes[index]); return nullptr; } if (Settings::values.dump_guest_shaders) { env.Dump(hash, key.unique_hashes[index]); } if (programs[index].info.requires_layer_emulation) { layer_source_program = &programs[index]; } } std::array<const Shader::Info*, Maxwell::MaxShaderStage> infos{}; std::array<vk::ShaderModule, Maxwell::MaxShaderStage> modules; const Shader::IR::Program* previous_stage{}; Shader::Backend::Bindings binding; for (size_t index = uses_vertex_a && uses_vertex_b ? 1 : 0; index < Maxwell::MaxShaderProgram; ++index) { const bool is_emulated_stage = layer_source_program != nullptr && index == static_cast<u32>(Maxwell::ShaderType::Geometry); if (key.unique_hashes[index] == 0 && !is_emulated_stage) { continue; } UNIMPLEMENTED_IF(index == 0); Shader::IR::Program& program{programs[index]}; const size_t stage_index{index - 1}; infos[stage_index] = &program.info; const auto runtime_info{MakeRuntimeInfo(programs, key, program, previous_stage, device)}; ConvertLegacyToGeneric(program, runtime_info); const std::vector<u32> code{EmitSPIRV(profile, runtime_info, program, binding)}; device.SaveShader(code); modules[stage_index] = BuildShader(device, code); // Text log + .spv dump. Text log is gated by gpu_log_level != Off; .spv dump // is independent and gated only by gpu_log_shader_dumps. const bool should_log = GPU::Logging::IsActive(); const bool should_dump = Settings::values.gpu_log_shader_dumps.GetValue(); if (should_log || should_dump) { static constexpr std::array stage_names{"vertex", "tess_control", "tess_eval", "geometry", "fragment"}; const std::string shader_name = fmt::format("shader_{:016x}_{}", key.unique_hashes[index], stage_names[stage_index]); if (should_log) { const std::string shader_info = fmt::format("SPIR-V size: {} bytes, hash: {:016x}", code.size() * sizeof(u32), key.unique_hashes[index]); GPU::Logging::GPULogger::GetInstance().LogShaderCompilation(shader_name, shader_info); } if (should_dump) { GPU::Logging::DumpSpirvShader(key.unique_hashes[index], std::span<const u32>(code.data(), code.size())); } } if (device.HasDebuggingToolAttached()) { const std::string name{fmt::format("Shader {:016x}", key.unique_hashes[index])}; modules[stage_index].SetObjectNameEXT(name.c_str()); } previous_stage = &program; } Common::ThreadWorker* const thread_worker{build_in_parallel ? &workers : nullptr}; return std::make_unique<GraphicsPipeline>( scheduler, buffer_cache, texture_cache, vulkan_pipeline_cache, &shader_notify, device, descriptor_pool, guest_descriptor_queue, descriptor_buffer_ring, thread_worker, statistics, render_pass_cache, key, std::move(modules), infos); } catch (const Shader::Exception& exception) { auto hash = key.Hash(); size_t env_index{0}; for (size_t index = 0; index < Maxwell::MaxShaderProgram; ++index) { if (key.unique_hashes[index] == 0) { continue; } Shader::Environment& env{*envs[env_index]}; ++env_index; const u32 cfg_offset{static_cast<u32>(env.StartAddress() + sizeof(Shader::ProgramHeader))}; try { // Walks the program for the dump; the one that failed may fail here again, and an // exception leaving this handler would end the process. Shader::Maxwell::Flow::CFG cfg(env, pools.flow_block, cfg_offset, index == 0); } catch (const Shader::Exception&) { } env.Dump(hash, key.unique_hashes[index]); } LOG_ERROR(Render_Vulkan, "{}", exception.what()); return nullptr; } namespace { // Pipelines built on the GPU thread at run time (shader translation; the driver compile is on // workers): every one over 2 ms is logged, and the total every 10 s, to tell how much of the // stutter on new content is translation. void ReportRuntimePipeline(const char* kind, std::chrono::steady_clock::time_point begin) { static std::chrono::steady_clock::duration total{}; static u32 count{}, logged{}; static auto window = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now(); const auto elapsed = now - begin; total += elapsed; ++count; const auto ms = [](auto d) { return std::chrono::duration<double, std::milli>(d).count(); }; if (elapsed > std::chrono::milliseconds{2} && logged < 400) { ++logged; LOG_INFO(Render_Vulkan, "{} pipeline built on the GPU thread in {:.1f} ms", kind, ms(elapsed)); } if (now - window > std::chrono::seconds{10}) { LOG_INFO(Render_Vulkan, "Runtime pipelines in the last 10 s: {} built, {:.1f} ms on the GPU thread", count, ms(total)); total = {}; count = 0; window = now; } } } // Anonymous namespace std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() { const auto begin = std::chrono::steady_clock::now(); SCOPE_EXIT { ReportRuntimePipeline("Graphics", begin); }; GraphicsEnvironments environments; GetGraphicsEnvironments(environments, graphics_key.unique_hashes); main_pools.ReleaseContents(); auto pipeline{ CreateGraphicsPipeline(main_pools, graphics_key, environments.Span(), nullptr, true)}; if (!pipeline || pipeline_cache_filename.empty()) { return pipeline; } if (pipeline->IsMeshPipeline()) { // Not stored: task and mesh programs are translated with the mesh registers, which the // cache format does not carry. The few of them rebuild quickly from the driver cache. QueueVulkanPipelineCacheFlush(); return pipeline; } if (disk_pipelines.contains(graphics_key.Hash())) { // In the disk cache already, only not built at boot. QueueVulkanPipelineCacheFlush(); return pipeline; } disk_pipelines.insert(graphics_key.Hash()); serialization_thread.QueueWork([this, key = graphics_key, envs = std::move(environments.envs)] { boost::container::static_vector<const GenericEnvironment*, Maxwell::MaxShaderProgram> env_ptrs; for (size_t index = 0; index < Maxwell::MaxShaderProgram; ++index) { if (key.unique_hashes[index] != 0) { env_ptrs.push_back(&envs[index]); } } SerializePipeline(key, env_ptrs, pipeline_cache_filename, CACHE_VERSION); }); QueueVulkanPipelineCacheFlush(); return pipeline; } std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline( const ComputePipelineCacheKey& key, const ShaderInfo* shader) { const auto begin = std::chrono::steady_clock::now(); SCOPE_EXIT { ReportRuntimePipeline("Compute", begin); }; const GPUVAddr program_base{kepler_compute->regs.code_loc.Address()}; const auto& qmd{kepler_compute->launch_description}; ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start, shader->lowered_native}; env.SetCachedSize(shader->size_bytes); main_pools.ReleaseContents(); auto pipeline{CreateComputePipeline(main_pools, key, env, nullptr, true)}; if (!pipeline || pipeline_cache_filename.empty()) { return pipeline; } serialization_thread.QueueWork([this, key, env_ = std::move(env)] { SerializePipeline(key, std::array<const GenericEnvironment*, 1>{&env_}, pipeline_cache_filename, CACHE_VERSION); }); QueueVulkanPipelineCacheFlush(); return pipeline; } std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline( ShaderPools& pools, const ComputePipelineCacheKey& key, Shader::Environment& env, PipelineStatistics* statistics, bool build_in_parallel) try { auto hash = key.Hash(); if (device.HasBrokenCompute()) { LOG_ERROR(Render_Vulkan, "Skipping {:#016x}", hash); return nullptr; } LOG_DEBUG(Render_Vulkan, "{:#016x}", hash); Shader::Maxwell::Flow::CFG cfg{env, pools.flow_block, env.StartAddress()}; // Dump it before error. if (Settings::values.dump_guest_shaders) { env.Dump(hash, key.unique_hash); } auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)}; const VkDriverIdKHR driver_id = device.GetDriverID(); const bool needs_shared_mem_clamp = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY || driver_id == VK_DRIVER_ID_ARM_PROPRIETARY || driver_id == VK_DRIVER_ID_MOLTENVK; const u32 max_shared_memory = device.GetMaxComputeSharedMemorySize(); if (needs_shared_mem_clamp && program.shared_memory_size > max_shared_memory) { LOG_WARNING(Render_Vulkan, "Compute shader {:#016x} requests {}KB shared memory but device max is {}KB - clamping", key.unique_hash, program.shared_memory_size / 1024, max_shared_memory / 1024); program.shared_memory_size = max_shared_memory; } const std::vector<u32> code{EmitSPIRV(profile, program)}; device.SaveShader(code); vk::ShaderModule spv_module{BuildShader(device, code)}; // Text log + .spv dump. Same split as the graphics path. const bool should_log = GPU::Logging::IsActive(); const bool should_dump = Settings::values.gpu_log_shader_dumps.GetValue(); if (should_log || should_dump) { const std::string shader_name = fmt::format("shader_{:016x}_compute", key.unique_hash); if (should_log) { const std::string shader_info = fmt::format("SPIR-V size: {} bytes, hash: {:016x}", code.size() * sizeof(u32), key.unique_hash); GPU::Logging::GPULogger::GetInstance().LogShaderCompilation(shader_name, shader_info); } if (should_dump) { GPU::Logging::DumpSpirvShader(key.unique_hash, std::span<const u32>(code.data(), code.size())); } } if (device.HasDebuggingToolAttached()) { const auto name{fmt::format("Shader {:016x}", key.unique_hash)}; spv_module.SetObjectNameEXT(name.c_str()); } Common::ThreadWorker* const thread_worker{build_in_parallel ? &workers : nullptr}; return std::make_unique<ComputePipeline>(device, scheduler, vulkan_pipeline_cache, descriptor_pool, guest_descriptor_queue, descriptor_buffer_ring, thread_worker, statistics, &shader_notify, program.info, std::move(spv_module), key.unique_hash); } catch (const Shader::Exception& exception) { LOG_ERROR(Render_Vulkan, "{}", exception.what()); return nullptr; } void PipelineCache::SerializeVulkanPipelineCache(const std::filesystem::path& filename, const vk::PipelineCache& pipeline_cache, u32 cache_version) try { // Written beside the cache and renamed over it: the emulator stopped mid-write left the cache // empty, and every later boot failed to read it. auto temporary = filename; temporary += ".tmp"; std::ofstream file(temporary, std::ios::binary); file.exceptions(std::ifstream::failbit); if (!file.is_open()) { LOG_ERROR(Common_Filesystem, "Failed to open Vulkan driver pipeline cache file {}", Common::FS::PathToUTF8String(filename)); return; } file.write(VULKAN_CACHE_MAGIC_NUMBER.data(), VULKAN_CACHE_MAGIC_NUMBER.size()) .write(reinterpret_cast<const char*>(&cache_version), sizeof(cache_version)); size_t cache_size = 0; std::vector<char> cache_data; if (pipeline_cache) { pipeline_cache.Read(&cache_size, nullptr); cache_data.resize(cache_size); pipeline_cache.Read(&cache_size, cache_data.data()); } file.write(cache_data.data(), cache_size); file.close(); std::error_code error; std::filesystem::rename(temporary, filename, error); if (error) { LOG_ERROR(Common_Filesystem, "Failed to replace Vulkan driver pipeline cache file {}: {}", Common::FS::PathToUTF8String(filename), error.message()); std::filesystem::remove(temporary, error); return; } LOG_INFO(Render_Vulkan, "Vulkan driver pipelines cached at: {}", Common::FS::PathToUTF8String(filename)); } catch (const std::ios_base::failure& e) { LOG_ERROR(Common_Filesystem, "{}", e.what()); if (!Common::FS::RemoveFile(filename)) { LOG_ERROR(Common_Filesystem, "Failed to delete Vulkan driver pipeline cache file {}", Common::FS::PathToUTF8String(filename)); } } vk::PipelineCache PipelineCache::LoadVulkanPipelineCache(const std::filesystem::path& filename, u32 expected_cache_version) { const auto create_pipeline_cache = [this](size_t data_size, const void* data) { VkPipelineCacheCreateInfo pipeline_cache_ci = { .sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO, .pNext = nullptr, .flags = 0, .initialDataSize = data_size, .pInitialData = data}; return device.GetLogical().CreatePipelineCache(pipeline_cache_ci); }; try { std::ifstream file(filename, std::ios::binary | std::ios::ate); if (!file.is_open()) { return create_pipeline_cache(0, nullptr); } file.exceptions(std::ifstream::failbit); const auto end{file.tellg()}; file.seekg(0, std::ios::beg); std::array<char, 8> magic_number; u32 cache_version; // A file cut short (the emulator stopped while writing it) would throw from the reads // below, and macOS's libc++ throws an ios_base::failure the handler doesn't match. if (static_cast<size_t>(end) < magic_number.size() + sizeof(cache_version)) { file.close(); LOG_ERROR(Common_Filesystem, "Truncated Vulkan driver pipeline cache file, deleting it"); Common::FS::RemoveFile(filename); return create_pipeline_cache(0, nullptr); } file.read(magic_number.data(), magic_number.size()) .read(reinterpret_cast<char*>(&cache_version), sizeof(cache_version)); if (magic_number != VULKAN_CACHE_MAGIC_NUMBER || cache_version != expected_cache_version) { file.close(); if (Common::FS::RemoveFile(filename)) { if (magic_number != VULKAN_CACHE_MAGIC_NUMBER) { LOG_ERROR(Common_Filesystem, "Invalid Vulkan driver pipeline cache file"); } if (cache_version != expected_cache_version) { LOG_INFO(Common_Filesystem, "Deleting old Vulkan driver pipeline cache"); } } else { LOG_ERROR(Common_Filesystem, "Invalid Vulkan pipeline cache file and failed to delete it in \"{}\"", Common::FS::PathToUTF8String(filename)); } return create_pipeline_cache(0, nullptr); } static constexpr size_t header_size = magic_number.size() + sizeof(cache_version); const size_t cache_size = static_cast<size_t>(end) - header_size; std::vector<char> cache_data(cache_size); file.read(cache_data.data(), cache_size); LOG_INFO(Render_Vulkan, "Loaded Vulkan driver pipeline cache: ", Common::FS::PathToUTF8String(filename)); return create_pipeline_cache(cache_size, cache_data.data()); } catch (const std::ios_base::failure& e) { LOG_ERROR(Common_Filesystem, "{}", e.what()); if (!Common::FS::RemoveFile(filename)) { LOG_ERROR(Common_Filesystem, "Failed to delete Vulkan driver pipeline cache file {}", Common::FS::PathToUTF8String(filename)); } return create_pipeline_cache(0, nullptr); } } } // namespace Vulkan