From 3b006f4fe28006d320c60fd2b4393fd3f27eacd7 Mon Sep 17 00:00:00 2001
From: ReinUsesLisp <reinuseslisp@airmail.cc>
Date: Tue, 27 Jul 2021 19:15:32 -0300
Subject: [PATCH] renderer_vulkan: Add setting to log pipeline statistics

Use VK_KHR_pipeline_executable_properties when enabled and available to
log statistics about the pipeline cache in a game.

For example, this is on Turing GPUs when generating a pipeline cache
from Super Smash Bros. Ultimate:

Average pipeline statistics
==========================================
Code size:       6433.167
Register count:    32.939

More advanced results could be presented, at the moment it's just an
average of all 3D and compute pipelines.
---
 src/common/settings.h                         |   1 +
 src/video_core/CMakeLists.txt                 |   2 +
 .../renderer_vulkan/pipeline_statistics.cpp   | 100 ++++++++++++++++++
 .../renderer_vulkan/pipeline_statistics.h     |  40 +++++++
 .../renderer_vulkan/vk_compute_pipeline.cpp   |  13 ++-
 .../renderer_vulkan/vk_compute_pipeline.h     |   2 +
 .../renderer_vulkan/vk_graphics_pipeline.cpp  |  16 ++-
 .../renderer_vulkan/vk_graphics_pipeline.h    |   6 +-
 .../renderer_vulkan/vk_pipeline_cache.cpp     |  30 ++++--
 .../renderer_vulkan/vk_pipeline_cache.h       |   7 +-
 .../vulkan_common/vulkan_device.cpp           |  29 +++++
 src/video_core/vulkan_common/vulkan_device.h  |   6 ++
 .../vulkan_common/vulkan_wrapper.cpp          |  38 +++++++
 src/video_core/vulkan_common/vulkan_wrapper.h |   8 ++
 src/yuzu/configuration/config.cpp             |   2 +
 src/yuzu/configuration/configure_debug.cpp    |   3 +
 src/yuzu/configuration/configure_debug.ui     |  23 +++-
 src/yuzu_cmd/config.cpp                       |   1 +
 src/yuzu_cmd/default_ini.h                    |   4 +
 19 files changed, 307 insertions(+), 24 deletions(-)
 create mode 100644 src/video_core/renderer_vulkan/pipeline_statistics.cpp
 create mode 100644 src/video_core/renderer_vulkan/pipeline_statistics.h

diff --git a/src/common/settings.h b/src/common/settings.h
index d8730f515c..375569450c 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -314,6 +314,7 @@ struct Values {
     // Renderer
     Setting<RendererBackend> renderer_backend{RendererBackend::OpenGL, "backend"};
     BasicSetting<bool> renderer_debug{false, "debug"};
+    BasicSetting<bool> renderer_shader_feedback{false, "shader_feedback"};
     BasicSetting<bool> enable_nsight_aftermath{false, "nsight_aftermath"};
     BasicSetting<bool> disable_shader_loop_safety_checks{false,
                                                          "disable_shader_loop_safety_checks"};
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 007ecc13ee..333f6f35fd 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -106,6 +106,8 @@ add_library(video_core STATIC
     renderer_vulkan/maxwell_to_vk.cpp
     renderer_vulkan/maxwell_to_vk.h
     renderer_vulkan/pipeline_helper.h
+    renderer_vulkan/pipeline_statistics.cpp
+    renderer_vulkan/pipeline_statistics.h
     renderer_vulkan/renderer_vulkan.h
     renderer_vulkan/renderer_vulkan.cpp
     renderer_vulkan/vk_blit_screen.cpp
diff --git a/src/video_core/renderer_vulkan/pipeline_statistics.cpp b/src/video_core/renderer_vulkan/pipeline_statistics.cpp
new file mode 100644
index 0000000000..bfec931a66
--- /dev/null
+++ b/src/video_core/renderer_vulkan/pipeline_statistics.cpp
@@ -0,0 +1,100 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <string_view>
+
+#include <fmt/format.h>
+
+#include "common/common_types.h"
+#include "common/logging/log.h"
+#include "video_core/renderer_vulkan/pipeline_statistics.h"
+#include "video_core/vulkan_common/vulkan_device.h"
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+namespace Vulkan {
+
+using namespace std::string_view_literals;
+
+static u64 GetUint64(const VkPipelineExecutableStatisticKHR& statistic) {
+    switch (statistic.format) {
+    case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_INT64_KHR:
+        return static_cast<u64>(statistic.value.i64);
+    case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_UINT64_KHR:
+        return statistic.value.u64;
+    case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_FLOAT64_KHR:
+        return static_cast<u64>(statistic.value.f64);
+    default:
+        return 0;
+    }
+}
+
+PipelineStatistics::PipelineStatistics(const Device& device_) : device{device_} {}
+
+void PipelineStatistics::Collect(VkPipeline pipeline) {
+    const auto& dev{device.GetLogical()};
+    const std::vector properties{dev.GetPipelineExecutablePropertiesKHR(pipeline)};
+    const u32 num_executables{static_cast<u32>(properties.size())};
+    for (u32 executable = 0; executable < num_executables; ++executable) {
+        const auto statistics{dev.GetPipelineExecutableStatisticsKHR(pipeline, executable)};
+        if (statistics.empty()) {
+            continue;
+        }
+        Stats stage_stats;
+        for (const auto& statistic : statistics) {
+            const char* const name{statistic.name};
+            if (name == "Binary Size"sv || name == "Code size"sv || name == "Instruction Count"sv) {
+                stage_stats.code_size = GetUint64(statistic);
+            } else if (name == "Register Count"sv) {
+                stage_stats.register_count = GetUint64(statistic);
+            } else if (name == "SGPRs"sv || name == "numUsedSgprs"sv) {
+                stage_stats.sgpr_count = GetUint64(statistic);
+            } else if (name == "VGPRs"sv || name == "numUsedVgprs"sv) {
+                stage_stats.vgpr_count = GetUint64(statistic);
+            } else if (name == "Branches"sv) {
+                stage_stats.branches_count = GetUint64(statistic);
+            } else if (name == "Basic Block Count"sv) {
+                stage_stats.basic_block_count = GetUint64(statistic);
+            }
+        }
+        std::lock_guard lock{mutex};
+        collected_stats.push_back(stage_stats);
+    }
+}
+
+void PipelineStatistics::Report() const {
+    double num{};
+    Stats total;
+    {
+        std::lock_guard lock{mutex};
+        for (const Stats& stats : collected_stats) {
+            total.code_size += stats.code_size;
+            total.register_count += stats.register_count;
+            total.sgpr_count += stats.sgpr_count;
+            total.vgpr_count += stats.vgpr_count;
+            total.branches_count += stats.branches_count;
+            total.basic_block_count += stats.basic_block_count;
+        }
+        num = static_cast<double>(collected_stats.size());
+    }
+    std::string report;
+    const auto add = [&](const char* fmt, u64 value) {
+        if (value > 0) {
+            report += fmt::format(fmt::runtime(fmt), static_cast<double>(value) / num);
+        }
+    };
+    add("Code size:      {:9.03f}\n", total.code_size);
+    add("Register count: {:9.03f}\n", total.register_count);
+    add("SGPRs:          {:9.03f}\n", total.sgpr_count);
+    add("VGPRs:          {:9.03f}\n", total.vgpr_count);
+    add("Branches count: {:9.03f}\n", total.branches_count);
+    add("Basic blocks:   {:9.03f}\n", total.basic_block_count);
+
+    LOG_INFO(Render_Vulkan,
+             "\nAverage pipeline statistics\n"
+             "==========================================\n"
+             "{}\n",
+             report);
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/pipeline_statistics.h b/src/video_core/renderer_vulkan/pipeline_statistics.h
new file mode 100644
index 0000000000..b618401079
--- /dev/null
+++ b/src/video_core/renderer_vulkan/pipeline_statistics.h
@@ -0,0 +1,40 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <mutex>
+#include <vector>
+
+#include "common/common_types.h"
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+namespace Vulkan {
+
+class Device;
+
+class PipelineStatistics {
+public:
+    explicit PipelineStatistics(const Device& device_);
+
+    void Collect(VkPipeline pipeline);
+
+    void Report() const;
+
+private:
+    struct Stats {
+        u64 code_size{};
+        u64 register_count{};
+        u64 sgpr_count{};
+        u64 vgpr_count{};
+        u64 branches_count{};
+        u64 basic_block_count{};
+    };
+
+    const Device& device;
+    mutable std::mutex mutex;
+    std::vector<Stats> collected_stats;
+};
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
index 70b84c7a6b..44faf626a7 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
@@ -8,6 +8,7 @@
 #include <boost/container/small_vector.hpp>
 
 #include "video_core/renderer_vulkan/pipeline_helper.h"
+#include "video_core/renderer_vulkan/pipeline_statistics.h"
 #include "video_core/renderer_vulkan/vk_buffer_cache.h"
 #include "video_core/renderer_vulkan/vk_compute_pipeline.h"
 #include "video_core/renderer_vulkan/vk_descriptor_pool.h"
@@ -26,6 +27,7 @@ using Tegra::Texture::TexturePair;
 ComputePipeline::ComputePipeline(const Device& device_, DescriptorPool& descriptor_pool,
                                  VKUpdateDescriptorQueue& update_descriptor_queue_,
                                  Common::ThreadWorker* thread_worker,
+                                 PipelineStatistics* pipeline_statistics,
                                  VideoCore::ShaderNotify* shader_notify, const Shader::Info& info_,
                                  vk::ShaderModule spv_module_)
     : device{device_}, update_descriptor_queue{update_descriptor_queue_}, info{info_},
@@ -36,7 +38,7 @@ ComputePipeline::ComputePipeline(const Device& device_, DescriptorPool& descript
     std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
                 uniform_buffer_sizes.begin());
 
-    auto func{[this, &descriptor_pool, shader_notify] {
+    auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics] {
         DescriptorLayoutBuilder builder{device};
         builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
 
@@ -50,10 +52,14 @@ ComputePipeline::ComputePipeline(const Device& device_, DescriptorPool& descript
             .pNext = nullptr,
             .requiredSubgroupSize = GuestWarpSize,
         };
+        VkPipelineCreateFlags flags{};
+        if (device.IsKhrPipelineEexecutablePropertiesEnabled()) {
+            flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
+        }
         pipeline = device.GetLogical().CreateComputePipeline({
             .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
             .pNext = nullptr,
-            .flags = 0,
+            .flags = flags,
             .stage{
                 .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                 .pNext = device.IsExtSubgroupSizeControlSupported() ? &subgroup_size_ci : nullptr,
@@ -67,6 +73,9 @@ ComputePipeline::ComputePipeline(const Device& device_, DescriptorPool& descript
             .basePipelineHandle = 0,
             .basePipelineIndex = 0,
         });
+        if (pipeline_statistics) {
+            pipeline_statistics->Collect(*pipeline);
+        }
         std::lock_guard lock{build_mutex};
         is_built = true;
         build_condvar.notify_one();
diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.h b/src/video_core/renderer_vulkan/vk_compute_pipeline.h
index 52fec04d3a..8c4b0a301e 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pipeline.h
+++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.h
@@ -25,6 +25,7 @@ class ShaderNotify;
 namespace Vulkan {
 
 class Device;
+class PipelineStatistics;
 class VKScheduler;
 
 class ComputePipeline {
@@ -32,6 +33,7 @@ public:
     explicit ComputePipeline(const Device& device, DescriptorPool& descriptor_pool,
                              VKUpdateDescriptorQueue& update_descriptor_queue,
                              Common::ThreadWorker* thread_worker,
+                             PipelineStatistics* pipeline_statistics,
                              VideoCore::ShaderNotify* shader_notify, const Shader::Info& info,
                              vk::ShaderModule spv_module);
 
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
index 18482e1d0e..7c0f910077 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
@@ -11,6 +11,7 @@
 #include "common/bit_field.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/vk_buffer_cache.h"
 #include "video_core/renderer_vulkan/vk_graphics_pipeline.h"
 #include "video_core/renderer_vulkan/vk_render_pass_cache.h"
@@ -217,8 +218,8 @@ GraphicsPipeline::GraphicsPipeline(
     VKScheduler& scheduler_, BufferCache& buffer_cache_, TextureCache& texture_cache_,
     VideoCore::ShaderNotify* shader_notify, const Device& device_, DescriptorPool& descriptor_pool,
     VKUpdateDescriptorQueue& update_descriptor_queue_, Common::ThreadWorker* worker_thread,
-    RenderPassCache& render_pass_cache, const GraphicsPipelineCacheKey& key_,
-    std::array<vk::ShaderModule, NUM_STAGES> stages,
+    PipelineStatistics* pipeline_statistics, RenderPassCache& render_pass_cache,
+    const GraphicsPipelineCacheKey& key_, std::array<vk::ShaderModule, NUM_STAGES> stages,
     const std::array<const Shader::Info*, NUM_STAGES>& infos)
     : key{key_}, maxwell3d{maxwell3d_}, gpu_memory{gpu_memory_}, device{device_},
       texture_cache{texture_cache_}, buffer_cache{buffer_cache_}, scheduler{scheduler_},
@@ -235,7 +236,7 @@ GraphicsPipeline::GraphicsPipeline(
         enabled_uniform_buffer_masks[stage] = info->constant_buffer_mask;
         std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
     }
-    auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool] {
+    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);
@@ -250,6 +251,9 @@ GraphicsPipeline::GraphicsPipeline(
         const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
         Validate();
         MakePipeline(render_pass);
+        if (pipeline_statistics) {
+            pipeline_statistics->Collect(*pipeline);
+        }
 
         std::lock_guard lock{build_mutex};
         is_built = true;
@@ -782,10 +786,14 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
         }
         */
     }
+    VkPipelineCreateFlags flags{};
+    if (device.IsKhrPipelineEexecutablePropertiesEnabled()) {
+        flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
+    }
     pipeline = device.GetLogical().CreateGraphicsPipeline({
         .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
         .pNext = nullptr,
-        .flags = 0,
+        .flags = flags,
         .stageCount = static_cast<u32>(shader_stages.size()),
         .pStages = shader_stages.data(),
         .pVertexInputState = &vertex_input_ci,
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
index 2bd48d6971..1c780e9445 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
@@ -60,6 +60,7 @@ struct hash<Vulkan::GraphicsPipelineCacheKey> {
 namespace Vulkan {
 
 class Device;
+class PipelineStatistics;
 class RenderPassCache;
 class VKScheduler;
 class VKUpdateDescriptorQueue;
@@ -73,8 +74,9 @@ public:
         VKScheduler& scheduler, BufferCache& buffer_cache, TextureCache& texture_cache,
         VideoCore::ShaderNotify* shader_notify, const Device& device,
         DescriptorPool& descriptor_pool, VKUpdateDescriptorQueue& update_descriptor_queue,
-        Common::ThreadWorker* worker_thread, RenderPassCache& render_pass_cache,
-        const GraphicsPipelineCacheKey& key, std::array<vk::ShaderModule, NUM_STAGES> stages,
+        Common::ThreadWorker* worker_thread, PipelineStatistics* pipeline_statistics,
+        RenderPassCache& render_pass_cache, const GraphicsPipelineCacheKey& key,
+        std::array<vk::ShaderModule, NUM_STAGES> stages,
         const std::array<const Shader::Info*, NUM_STAGES>& infos);
 
     GraphicsPipeline& operator=(GraphicsPipeline&&) noexcept = delete;
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 57b1632470..a37ca1fdf1 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -29,6 +29,7 @@
 #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/vk_compute_pipeline.h"
 #include "video_core/renderer_vulkan/vk_descriptor_pool.h"
 #include "video_core/renderer_vulkan/vk_pipeline_cache.h"
@@ -389,15 +390,19 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
         size_t total{};
         size_t built{};
         bool has_loaded{};
+        std::unique_ptr<PipelineStatistics> statistics;
     } state;
 
+    if (device.IsKhrPipelineEexecutablePropertiesEnabled()) {
+        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, false)};
+            auto pipeline{CreateComputePipeline(pools, key, env, state.statistics.get(), false)};
             std::lock_guard lock{state.mutex};
             if (pipeline) {
                 compute_cache.emplace(key, std::move(pipeline));
@@ -425,7 +430,8 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
             for (auto& env : envs) {
                 env_ptrs.push_back(&env);
             }
-            auto pipeline{CreateGraphicsPipeline(pools, key, MakeSpan(env_ptrs), false)};
+            auto pipeline{CreateGraphicsPipeline(pools, key, MakeSpan(env_ptrs),
+                                                 state.statistics.get(), false)};
 
             std::lock_guard lock{state.mutex};
             graphics_cache.emplace(key, std::move(pipeline));
@@ -445,6 +451,10 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
     lock.unlock();
 
     workers.WaitForRequests();
+
+    if (state.statistics) {
+        state.statistics->Report();
+    }
 }
 
 GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() {
@@ -486,7 +496,8 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
 
 std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
     ShaderPools& pools, const GraphicsPipelineCacheKey& key,
-    std::span<Shader::Environment* const> envs, bool build_in_parallel) try {
+    std::span<Shader::Environment* const> envs, PipelineStatistics* statistics,
+    bool build_in_parallel) try {
     LOG_INFO(Render_Vulkan, "0x{:016x}", key.Hash());
     size_t env_index{0};
     std::array<Shader::IR::Program, Maxwell::MaxShaderProgram> programs;
@@ -540,7 +551,7 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
     Common::ThreadWorker* const thread_worker{build_in_parallel ? &workers : nullptr};
     return std::make_unique<GraphicsPipeline>(
         maxwell3d, gpu_memory, scheduler, buffer_cache, texture_cache, &shader_notify, device,
-        descriptor_pool, update_descriptor_queue, thread_worker, render_pass_cache, key,
+        descriptor_pool, update_descriptor_queue, thread_worker, statistics, render_pass_cache, key,
         std::move(modules), infos);
 
 } catch (const Shader::Exception& exception) {
@@ -553,7 +564,8 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() {
     GetGraphicsEnvironments(environments, graphics_key.unique_hashes);
 
     main_pools.ReleaseContents();
-    auto pipeline{CreateGraphicsPipeline(main_pools, graphics_key, environments.Span(), true)};
+    auto pipeline{
+        CreateGraphicsPipeline(main_pools, graphics_key, environments.Span(), nullptr, true)};
     if (!pipeline || pipeline_cache_filename.empty()) {
         return pipeline;
     }
@@ -578,7 +590,7 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
     env.SetCachedSize(shader->size_bytes);
 
     main_pools.ReleaseContents();
-    auto pipeline{CreateComputePipeline(main_pools, key, env, true)};
+    auto pipeline{CreateComputePipeline(main_pools, key, env, nullptr, true)};
     if (!pipeline || pipeline_cache_filename.empty()) {
         return pipeline;
     }
@@ -591,7 +603,7 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
 
 std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
     ShaderPools& pools, const ComputePipelineCacheKey& key, Shader::Environment& env,
-    bool build_in_parallel) try {
+    PipelineStatistics* statistics, bool build_in_parallel) try {
     LOG_INFO(Render_Vulkan, "0x{:016x}", key.Hash());
 
     Shader::Maxwell::Flow::CFG cfg{env, pools.flow_block, env.StartAddress()};
@@ -605,8 +617,8 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
     }
     Common::ThreadWorker* const thread_worker{build_in_parallel ? &workers : nullptr};
     return std::make_unique<ComputePipeline>(device, descriptor_pool, update_descriptor_queue,
-                                             thread_worker, &shader_notify, program.info,
-                                             std::move(spv_module));
+                                             thread_worker, statistics, &shader_notify,
+                                             program.info, std::move(spv_module));
 
 } catch (const Shader::Exception& exception) {
     LOG_ERROR(Render_Vulkan, "{}", exception.what());
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h
index efe5a7ed82..4c135b5ddb 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h
@@ -80,8 +80,9 @@ struct hash<Vulkan::ComputePipelineCacheKey> {
 namespace Vulkan {
 
 class ComputePipeline;
-class Device;
 class DescriptorPool;
+class Device;
+class PipelineStatistics;
 class RasterizerVulkan;
 class RenderPassCache;
 class VKScheduler;
@@ -128,7 +129,8 @@ private:
 
     std::unique_ptr<GraphicsPipeline> CreateGraphicsPipeline(
         ShaderPools& pools, const GraphicsPipelineCacheKey& key,
-        std::span<Shader::Environment* const> envs, bool build_in_parallel);
+        std::span<Shader::Environment* const> envs, PipelineStatistics* statistics,
+        bool build_in_parallel);
 
     std::unique_ptr<ComputePipeline> CreateComputePipeline(const ComputePipelineCacheKey& key,
                                                            const ShaderInfo* shader);
@@ -136,6 +138,7 @@ private:
     std::unique_ptr<ComputePipeline> CreateComputePipeline(ShaderPools& pools,
                                                            const ComputePipelineCacheKey& key,
                                                            Shader::Environment& env,
+                                                           PipelineStatistics* statistics,
                                                            bool build_in_parallel);
 
     const Device& device;
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 44afdc1cdd..8e56a89e10 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -526,6 +526,17 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
         SetNext(next, workgroup_layout);
     }
 
+    VkPhysicalDevicePipelineExecutablePropertiesFeaturesKHR executable_properties;
+    if (khr_pipeline_executable_properties) {
+        LOG_INFO(Render_Vulkan, "Enabling shader feedback, expect slower shader build times");
+        executable_properties = {
+            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_EXECUTABLE_PROPERTIES_FEATURES_KHR,
+            .pNext = nullptr,
+            .pipelineExecutableInfo = VK_TRUE,
+        };
+        SetNext(next, executable_properties);
+    }
+
     if (!ext_depth_range_unrestricted) {
         LOG_INFO(Render_Vulkan, "Device doesn't support depth range unrestricted");
     }
@@ -824,6 +835,7 @@ std::vector<const char*> Device::LoadExtensions(bool requires_surface) {
 
     bool has_khr_shader_float16_int8{};
     bool has_khr_workgroup_memory_explicit_layout{};
+    bool has_khr_pipeline_executable_properties{};
     bool has_ext_subgroup_size_control{};
     bool has_ext_transform_feedback{};
     bool has_ext_custom_border_color{};
@@ -878,6 +890,10 @@ std::vector<const char*> Device::LoadExtensions(bool requires_surface) {
             test(nv_device_diagnostics_config, VK_NV_DEVICE_DIAGNOSTICS_CONFIG_EXTENSION_NAME,
                  true);
         }
+        if (Settings::values.renderer_shader_feedback) {
+            test(has_khr_pipeline_executable_properties,
+                 VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME, false);
+        }
     }
     VkPhysicalDeviceFeatures2KHR features{};
     features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR;
@@ -1033,6 +1049,19 @@ std::vector<const char*> Device::LoadExtensions(bool requires_surface) {
             khr_workgroup_memory_explicit_layout = true;
         }
     }
+    if (has_khr_pipeline_executable_properties) {
+        VkPhysicalDevicePipelineExecutablePropertiesFeaturesKHR executable_properties;
+        executable_properties.sType =
+            VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_EXECUTABLE_PROPERTIES_FEATURES_KHR;
+        executable_properties.pNext = nullptr;
+        features.pNext = &executable_properties;
+        physical.GetFeatures2KHR(features);
+
+        if (executable_properties.pipelineExecutableInfo) {
+            extensions.push_back(VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME);
+            khr_pipeline_executable_properties = true;
+        }
+    }
     if (khr_push_descriptor) {
         VkPhysicalDevicePushDescriptorPropertiesKHR push_descriptor;
         push_descriptor.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PUSH_DESCRIPTOR_PROPERTIES_KHR;
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index df394e384e..c19f40746d 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -214,6 +214,11 @@ public:
         return khr_push_descriptor;
     }
 
+    /// Returns true if VK_KHR_pipeline_executable_properties is enabled.
+    bool IsKhrPipelineEexecutablePropertiesEnabled() const {
+        return khr_pipeline_executable_properties;
+    }
+
     /// Returns true if the device supports VK_KHR_workgroup_memory_explicit_layout.
     bool IsKhrWorkgroupMemoryExplicitLayoutSupported() const {
         return khr_workgroup_memory_explicit_layout;
@@ -378,6 +383,7 @@ private:
     bool khr_spirv_1_4{};                       ///< Support for VK_KHR_spirv_1_4.
     bool khr_workgroup_memory_explicit_layout{}; ///< Support for explicit workgroup layouts.
     bool khr_push_descriptor{};                  ///< Support for VK_KHR_push_descritor.
+    bool khr_pipeline_executable_properties{};   ///< Support for executable properties.
     bool ext_index_type_uint8{};                 ///< Support for VK_EXT_index_type_uint8.
     bool ext_sampler_filter_minmax{};            ///< Support for VK_EXT_sampler_filter_minmax.
     bool ext_depth_range_unrestricted{};         ///< Support for VK_EXT_depth_range_unrestricted.
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp
index 70898004a9..a9faa48071 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.cpp
+++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp
@@ -181,6 +181,8 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept {
     X(vkGetMemoryWin32HandleKHR);
 #endif
     X(vkGetQueryPoolResults);
+    X(vkGetPipelineExecutablePropertiesKHR);
+    X(vkGetPipelineExecutableStatisticsKHR);
     X(vkGetSemaphoreCounterValueKHR);
     X(vkMapMemory);
     X(vkQueueSubmit);
@@ -809,6 +811,42 @@ VkMemoryRequirements Device::GetImageMemoryRequirements(VkImage image) const noe
     return requirements;
 }
 
+std::vector<VkPipelineExecutablePropertiesKHR> Device::GetPipelineExecutablePropertiesKHR(
+    VkPipeline pipeline) const {
+    const VkPipelineInfoKHR info{
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_INFO_KHR,
+        .pNext = nullptr,
+        .pipeline = pipeline,
+    };
+    u32 num{};
+    dld->vkGetPipelineExecutablePropertiesKHR(handle, &info, &num, nullptr);
+    std::vector<VkPipelineExecutablePropertiesKHR> properties(num);
+    for (auto& property : properties) {
+        property.sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_PROPERTIES_KHR;
+    }
+    Check(dld->vkGetPipelineExecutablePropertiesKHR(handle, &info, &num, properties.data()));
+    return properties;
+}
+
+std::vector<VkPipelineExecutableStatisticKHR> Device::GetPipelineExecutableStatisticsKHR(
+    VkPipeline pipeline, u32 executable_index) const {
+    const VkPipelineExecutableInfoKHR executable_info{
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_INFO_KHR,
+        .pNext = nullptr,
+        .pipeline = pipeline,
+        .executableIndex = executable_index,
+    };
+    u32 num{};
+    dld->vkGetPipelineExecutableStatisticsKHR(handle, &executable_info, &num, nullptr);
+    std::vector<VkPipelineExecutableStatisticKHR> statistics(num);
+    for (auto& statistic : statistics) {
+        statistic.sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_STATISTIC_KHR;
+    }
+    Check(dld->vkGetPipelineExecutableStatisticsKHR(handle, &executable_info, &num,
+                                                    statistics.data()));
+    return statistics;
+}
+
 void Device::UpdateDescriptorSets(Span<VkWriteDescriptorSet> writes,
                                   Span<VkCopyDescriptorSet> copies) const noexcept {
     dld->vkUpdateDescriptorSets(handle, writes.size(), writes.data(), copies.size(), copies.data());
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h
index d76bb43240..b7ae01c6ca 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.h
+++ b/src/video_core/vulkan_common/vulkan_wrapper.h
@@ -295,6 +295,8 @@ struct DeviceDispatch : InstanceDispatch {
 #ifdef _WIN32
     PFN_vkGetMemoryWin32HandleKHR vkGetMemoryWin32HandleKHR{};
 #endif
+    PFN_vkGetPipelineExecutablePropertiesKHR vkGetPipelineExecutablePropertiesKHR{};
+    PFN_vkGetPipelineExecutableStatisticsKHR vkGetPipelineExecutableStatisticsKHR{};
     PFN_vkGetQueryPoolResults vkGetQueryPoolResults{};
     PFN_vkGetSemaphoreCounterValueKHR vkGetSemaphoreCounterValueKHR{};
     PFN_vkMapMemory vkMapMemory{};
@@ -879,6 +881,12 @@ public:
 
     VkMemoryRequirements GetImageMemoryRequirements(VkImage image) const noexcept;
 
+    std::vector<VkPipelineExecutablePropertiesKHR> GetPipelineExecutablePropertiesKHR(
+        VkPipeline pipeline) const;
+
+    std::vector<VkPipelineExecutableStatisticKHR> GetPipelineExecutableStatisticsKHR(
+        VkPipeline pipeline, u32 executable_index) const;
+
     void UpdateDescriptorSets(Span<VkWriteDescriptorSet> writes,
                               Span<VkCopyDescriptorSet> copies) const noexcept;
 
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 72027e773c..91b6217dbb 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -825,6 +825,7 @@ void Config::ReadRendererValues() {
     if (global) {
         ReadBasicSetting(Settings::values.fps_cap);
         ReadBasicSetting(Settings::values.renderer_debug);
+        ReadBasicSetting(Settings::values.renderer_shader_feedback);
         ReadBasicSetting(Settings::values.enable_nsight_aftermath);
         ReadBasicSetting(Settings::values.disable_shader_loop_safety_checks);
     }
@@ -1360,6 +1361,7 @@ void Config::SaveRendererValues() {
     if (global) {
         WriteBasicSetting(Settings::values.fps_cap);
         WriteBasicSetting(Settings::values.renderer_debug);
+        WriteBasicSetting(Settings::values.renderer_shader_feedback);
         WriteBasicSetting(Settings::values.enable_nsight_aftermath);
         WriteBasicSetting(Settings::values.disable_shader_loop_safety_checks);
     }
diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp
index f7e29dbd7a..c0b240c1e7 100644
--- a/src/yuzu/configuration/configure_debug.cpp
+++ b/src/yuzu/configuration/configure_debug.cpp
@@ -43,6 +43,8 @@ void ConfigureDebug::SetConfiguration() {
     ui->use_auto_stub->setChecked(Settings::values.use_auto_stub.GetValue());
     ui->enable_graphics_debugging->setEnabled(runtime_lock);
     ui->enable_graphics_debugging->setChecked(Settings::values.renderer_debug.GetValue());
+    ui->enable_shader_feedback->setEnabled(runtime_lock);
+    ui->enable_shader_feedback->setChecked(Settings::values.renderer_shader_feedback.GetValue());
     ui->enable_cpu_debugging->setEnabled(runtime_lock);
     ui->enable_cpu_debugging->setChecked(Settings::values.cpu_debug_mode.GetValue());
     ui->enable_nsight_aftermath->setEnabled(runtime_lock);
@@ -65,6 +67,7 @@ void ConfigureDebug::ApplyConfiguration() {
     Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked();
     Settings::values.use_auto_stub = ui->use_auto_stub->isChecked();
     Settings::values.renderer_debug = ui->enable_graphics_debugging->isChecked();
+    Settings::values.renderer_shader_feedback = ui->enable_shader_feedback->isChecked();
     Settings::values.cpu_debug_mode = ui->enable_cpu_debugging->isChecked();
     Settings::values.enable_nsight_aftermath = ui->enable_nsight_aftermath->isChecked();
     Settings::values.disable_shader_loop_safety_checks =
diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui
index c8baf2921d..3fe9ff7dec 100644
--- a/src/yuzu/configuration/configure_debug.ui
+++ b/src/yuzu/configuration/configure_debug.ui
@@ -111,8 +111,8 @@
      <property name="title">
       <string>Graphics</string>
      </property>
-     <layout class="QVBoxLayout" name="verticalLayout_6">
-      <item>
+     <layout class="QGridLayout" name="gridLayout_3">
+      <item row="0" column="0">
        <widget class="QCheckBox" name="enable_graphics_debugging">
         <property name="enabled">
          <bool>true</bool>
@@ -125,7 +125,7 @@
         </property>
        </widget>
       </item>
-      <item>
+      <item row="2" column="0">
        <widget class="QCheckBox" name="enable_nsight_aftermath">
         <property name="toolTip">
          <string>When checked, it enables Nsight Aftermath crash dumps</string>
@@ -135,7 +135,7 @@
         </property>
        </widget>
       </item>
-      <item>
+      <item row="0" column="1">
        <widget class="QCheckBox" name="disable_macro_jit">
         <property name="enabled">
          <bool>true</bool>
@@ -148,7 +148,17 @@
         </property>
        </widget>
       </item>
-      <item>
+      <item row="1" column="0">
+       <widget class="QCheckBox" name="enable_shader_feedback">
+        <property name="toolTip">
+         <string>When checked, yuzu will log statistics about the compiled pipeline cache</string>
+        </property>
+        <property name="text">
+         <string>Enable Shader Feedback</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
        <widget class="QCheckBox" name="disable_loop_safety_checks">
         <property name="toolTip">
          <string>When checked, it executes shaders without loop logic changes</string>
@@ -276,11 +286,14 @@
   <tabstop>open_log_button</tabstop>
   <tabstop>homebrew_args_edit</tabstop>
   <tabstop>enable_graphics_debugging</tabstop>
+  <tabstop>enable_shader_feedback</tabstop>
   <tabstop>enable_nsight_aftermath</tabstop>
   <tabstop>disable_macro_jit</tabstop>
   <tabstop>disable_loop_safety_checks</tabstop>
+  <tabstop>fs_access_log</tabstop>
   <tabstop>reporting_services</tabstop>
   <tabstop>quest_flag</tabstop>
+  <tabstop>enable_cpu_debugging</tabstop>
   <tabstop>use_debug_asserts</tabstop>
   <tabstop>use_auto_stub</tabstop>
  </tabstops>
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index 5af1ee6a8d..064ecaafa3 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -444,6 +444,7 @@ void Config::ReadValues() {
     // Renderer
     ReadSetting("Renderer", Settings::values.renderer_backend);
     ReadSetting("Renderer", Settings::values.renderer_debug);
+    ReadSetting("Renderer", Settings::values.renderer_shader_feedback);
     ReadSetting("Renderer", Settings::values.enable_nsight_aftermath);
     ReadSetting("Renderer", Settings::values.disable_shader_loop_safety_checks);
     ReadSetting("Renderer", Settings::values.vulkan_device);
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index e646e2d2f6..0c0c128aea 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -221,6 +221,10 @@ backend =
 # 0 (default): Disabled, 1: Enabled
 debug =
 
+# Enable shader feedback.
+# 0 (default): Disabled, 1: Enabled
+renderer_shader_feedback =
+
 # Enable Nsight Aftermath crash dumps
 # 0 (default): Disabled, 1: Enabled
 nsight_aftermath =