From f58ee3f15f7427a8b834286384931bcf821ed771 Mon Sep 17 00:00:00 2001
From: Fernando Sahmkow <fsahmkow27@gmail.com>
Date: Wed, 17 Nov 2021 04:19:29 +0100
Subject: [PATCH] ShaderDecompiler: Add a debug option to dump the game's
 shaders.

---
 src/common/settings.h                         |  1 +
 src/shader_recompiler/environment.h           |  2 +
 .../renderer_opengl/gl_shader_cache.cpp       | 11 +++-
 .../renderer_vulkan/vk_pipeline_cache.cpp     |  9 ++++
 src/video_core/shader_environment.cpp         | 54 +++++++++++++++++++
 src/video_core/shader_environment.h           |  6 +++
 src/yuzu/configuration/configure_debug.cpp    |  3 ++
 src/yuzu/configuration/configure_debug.ui     | 13 +++++
 8 files changed, 98 insertions(+), 1 deletion(-)

diff --git a/src/common/settings.h b/src/common/settings.h
index 313f1fa7f7..d01c0448c7 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -597,6 +597,7 @@ struct Values {
     BasicSetting<std::string> program_args{std::string(), "program_args"};
     BasicSetting<bool> dump_exefs{false, "dump_exefs"};
     BasicSetting<bool> dump_nso{false, "dump_nso"};
+    BasicSetting<bool> dump_shaders{false, "dump_shaders"};
     BasicSetting<bool> enable_fs_access_log{false, "enable_fs_access_log"};
     BasicSetting<bool> reporting_services{false, "reporting_services"};
     BasicSetting<bool> quest_flag{false, "quest_flag"};
diff --git a/src/shader_recompiler/environment.h b/src/shader_recompiler/environment.h
index b4df73e8af..db16429d46 100644
--- a/src/shader_recompiler/environment.h
+++ b/src/shader_recompiler/environment.h
@@ -31,6 +31,8 @@ public:
 
     [[nodiscard]] virtual std::array<u32, 3> WorkgroupSize() const = 0;
 
+    virtual void Dump(u64 hash) = 0;
+
     [[nodiscard]] const ProgramHeader& SPH() const noexcept {
         return sph;
     }
diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp
index ec558a9afa..f71e01a342 100644
--- a/src/video_core/renderer_opengl/gl_shader_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp
@@ -425,6 +425,11 @@ std::unique_ptr<GraphicsPipeline> ShaderCache::CreateGraphicsPipeline(
 
         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 (Settings::values.dump_shaders) {
+            env.Dump(key.unique_hashes[index]);
+        }
+
         if (!uses_vertex_a || index != 1) {
             // Normal path
             programs[index] = TranslateProgram(pools.inst, pools.block, env, cfg, host_info);
@@ -511,8 +516,12 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
     LOG_INFO(Render_OpenGL, "0x{:016x}", key.Hash());
 
     Shader::Maxwell::Flow::CFG cfg{env, pools.flow_block, env.StartAddress()};
-    auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)};
 
+    if (Settings::values.dump_shaders) {
+        env.Dump(key.Hash());
+    }
+
+    auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)};
     const u32 num_storage_buffers{Shader::NumDescriptors(program.info.storage_buffers_descriptors)};
     Shader::RuntimeInfo info;
     info.glasm_use_storage_buffers = num_storage_buffers <= device.GetMaxGLASMStorageBufferBlocks();
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 2728353c8a..a633b73e5e 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -517,6 +517,9 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
 
         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 (Settings::values.dump_shaders) {
+            env.Dump(key.unique_hashes[index]);
+        }
         if (!uses_vertex_a || index != 1) {
             // Normal path
             programs[index] = TranslateProgram(pools.inst, pools.block, env, cfg, host_info);
@@ -613,6 +616,12 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
     LOG_INFO(Render_Vulkan, "0x{:016x}", key.Hash());
 
     Shader::Maxwell::Flow::CFG cfg{env, pools.flow_block, env.StartAddress()};
+
+    // Dump it before error.
+    if (Settings::values.dump_shaders) {
+        env.Dump(key.Hash());
+    }
+
     auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)};
     const std::vector<u32> code{EmitSPIRV(profile, program)};
     device.SaveShader(code);
diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp
index 05850afd0d..7d3ae0de47 100644
--- a/src/video_core/shader_environment.cpp
+++ b/src/video_core/shader_environment.cpp
@@ -3,6 +3,7 @@
 // Refer to the license.txt file included.
 
 #include <algorithm>
+#include <bit>
 #include <filesystem>
 #include <fstream>
 #include <memory>
@@ -14,6 +15,7 @@
 #include "common/common_types.h"
 #include "common/div_ceil.h"
 #include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "shader_recompiler/environment.h"
 #include "video_core/engines/kepler_compute.h"
@@ -57,6 +59,47 @@ static Shader::TextureType ConvertType(const Tegra::Texture::TICEntry& entry) {
     }
 }
 
+static std::string_view StageToPrefix(Shader::Stage stage) {
+    switch (stage) {
+    case Shader::Stage::VertexB:
+        return "VB";
+    case Shader::Stage::TessellationControl:
+        return "TC";
+    case Shader::Stage::TessellationEval:
+        return "TE";
+    case Shader::Stage::Geometry:
+        return "GS";
+    case Shader::Stage::Fragment:
+        return "FS";
+    case Shader::Stage::Compute:
+        return "CS";
+    case Shader::Stage::VertexA:
+        return "VA";
+    default:
+        return "UK";
+    }
+}
+
+static void DumpImpl(u64 hash, const u64* code, u32 read_highest, u32 read_lowest,
+                     u32 initial_offset, Shader::Stage stage) {
+    const auto shader_dir{Common::FS::GetYuzuPath(Common::FS::YuzuPath::DumpDir)};
+    const auto base_dir{shader_dir / "shaders"};
+    if (!Common::FS::CreateDir(shader_dir) || !Common::FS::CreateDir(base_dir)) {
+        LOG_ERROR(Common_Filesystem, "Failed to create shader dump directories");
+        return;
+    }
+    const auto prefix = StageToPrefix(stage);
+    const auto name{base_dir / fmt::format("{}{:016x}.ash", prefix, hash)};
+    const size_t real_size = read_highest - read_lowest + initial_offset;
+    const size_t padding_needed = ((32 - (real_size % 32)) % 32);
+    std::fstream shader_file(name, std::ios::out | std::ios::binary);
+    const size_t jump_index = initial_offset / sizeof(u64);
+    shader_file.write(reinterpret_cast<const char*>(code + jump_index), real_size);
+    for (size_t i = 0; i < padding_needed; i++) {
+        shader_file.put(0);
+    }
+}
+
 GenericEnvironment::GenericEnvironment(Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_,
                                        u32 start_address_)
     : gpu_memory{&gpu_memory_}, program_base{program_base_} {
@@ -128,6 +171,10 @@ u64 GenericEnvironment::CalculateHash() const {
     return Common::CityHash64(data.get(), size);
 }
 
+void GenericEnvironment::Dump(u64 hash) {
+    DumpImpl(hash, code.data(), read_highest, read_lowest, initial_offset, stage);
+}
+
 void GenericEnvironment::Serialize(std::ofstream& file) const {
     const u64 code_size{static_cast<u64>(CachedSize())};
     const u64 num_texture_types{static_cast<u64>(texture_types.size())};
@@ -207,6 +254,7 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_,
                                          u32 start_address_)
     : GenericEnvironment{gpu_memory_, program_base_, start_address_}, maxwell3d{&maxwell3d_} {
     gpu_memory->ReadBlock(program_base + start_address, &sph, sizeof(sph));
+    initial_offset = sizeof(sph);
     gp_passthrough_mask = maxwell3d->regs.gp_passthrough_mask;
     switch (program) {
     case Maxwell::ShaderProgram::VertexA:
@@ -323,14 +371,20 @@ void FileEnvironment::Deserialize(std::ifstream& file) {
     if (stage == Shader::Stage::Compute) {
         file.read(reinterpret_cast<char*>(&workgroup_size), sizeof(workgroup_size))
             .read(reinterpret_cast<char*>(&shared_memory_size), sizeof(shared_memory_size));
+        initial_offset = 0;
     } else {
         file.read(reinterpret_cast<char*>(&sph), sizeof(sph));
+        initial_offset = sizeof(sph);
         if (stage == Shader::Stage::Geometry) {
             file.read(reinterpret_cast<char*>(&gp_passthrough_mask), sizeof(gp_passthrough_mask));
         }
     }
 }
 
+void FileEnvironment::Dump(u64 [[maybe_unused]] hash) {
+    DumpImpl(hash, code.get(), read_highest, read_lowest, initial_offset, stage);
+}
+
 u64 FileEnvironment::ReadInstruction(u32 address) {
     if (address < read_lowest || address > read_highest) {
         throw Shader::LogicError("Out of bounds address {}", address);
diff --git a/src/video_core/shader_environment.h b/src/video_core/shader_environment.h
index 6640e53d08..aae762b270 100644
--- a/src/video_core/shader_environment.h
+++ b/src/video_core/shader_environment.h
@@ -57,6 +57,8 @@ public:
 
     [[nodiscard]] u64 CalculateHash() const;
 
+    void Dump(u64 hash) override;
+
     void Serialize(std::ofstream& file) const;
 
 protected:
@@ -82,6 +84,7 @@ protected:
 
     u32 cached_lowest = std::numeric_limits<u32>::max();
     u32 cached_highest = 0;
+    u32 initial_offset = 0;
 
     bool has_unbound_instructions = false;
 };
@@ -149,6 +152,8 @@ public:
 
     [[nodiscard]] std::array<u32, 3> WorkgroupSize() const override;
 
+    void Dump(u64 hash) override;
+
 private:
     std::unique_ptr<u64[]> code;
     std::unordered_map<u32, Shader::TextureType> texture_types;
@@ -159,6 +164,7 @@ private:
     u32 texture_bound{};
     u32 read_lowest{};
     u32 read_highest{};
+    u32 initial_offset{};
 };
 
 void SerializePipeline(std::span<const char> key, std::span<const GenericEnvironment* const> envs,
diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp
index 633fc295b6..c1cf4050c1 100644
--- a/src/yuzu/configuration/configure_debug.cpp
+++ b/src/yuzu/configuration/configure_debug.cpp
@@ -51,6 +51,8 @@ void ConfigureDebug::SetConfiguration() {
     ui->enable_cpu_debugging->setChecked(Settings::values.cpu_debug_mode.GetValue());
     ui->enable_nsight_aftermath->setEnabled(runtime_lock);
     ui->enable_nsight_aftermath->setChecked(Settings::values.enable_nsight_aftermath.GetValue());
+    ui->dump_shaders->setEnabled(runtime_lock);
+    ui->dump_shaders->setChecked(Settings::values.dump_shaders.GetValue());
     ui->disable_macro_jit->setEnabled(runtime_lock);
     ui->disable_macro_jit->setChecked(Settings::values.disable_macro_jit.GetValue());
     ui->disable_loop_safety_checks->setEnabled(runtime_lock);
@@ -73,6 +75,7 @@ void ConfigureDebug::ApplyConfiguration() {
     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.dump_shaders = ui->dump_shaders->isChecked();
     Settings::values.disable_shader_loop_safety_checks =
         ui->disable_loop_safety_checks->isChecked();
     Settings::values.disable_macro_jit = ui->disable_macro_jit->isChecked();
diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui
index 0f3b51c8df..4dd870855d 100644
--- a/src/yuzu/configuration/configure_debug.ui
+++ b/src/yuzu/configuration/configure_debug.ui
@@ -105,6 +105,19 @@
         </property>
        </widget>
       </item>
+      <item row="2" column="1">
+       <widget class="QCheckBox" name="dump_shaders">
+        <property name="enabled">
+         <bool>true</bool>
+        </property>
+        <property name="toolTip">
+         <string>When checked, it will dump all the original assembler shaders from the disk shader cache or game as found</string>
+        </property>
+        <property name="text">
+         <string>Dump Game Shaders</string>
+        </property>
+       </widget>
+      </item>
       <item row="0" column="1">
        <widget class="QCheckBox" name="disable_macro_jit">
         <property name="enabled">