diff options
21 files changed, 1464 insertions, 10 deletions
| diff --git a/src/core/core.h b/src/core/core.h index 552c8f5ee..ade456cfc 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -13,6 +13,7 @@  #include "core/memory.h"  #include "core/perf_stats.h"  #include "core/telemetry_session.h" +#include "video_core/debug_utils/debug_utils.h"  #include "video_core/gpu.h"  class EmuWindow; @@ -135,6 +136,14 @@ public:          return *app_loader;      } +    void SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context) { +        debug_context = std::move(context); +    } + +    std::shared_ptr<Tegra::DebugContext> GetGPUDebugContext() const { +        return debug_context; +    } +  private:      /**       * Initialize the emulated system. @@ -154,6 +163,8 @@ private:      std::unique_ptr<Kernel::Scheduler> scheduler;      std::unique_ptr<Tegra::GPU> gpu_core; +    std::shared_ptr<Tegra::DebugContext> debug_context; +      Kernel::SharedPtr<Kernel::Process> current_process;      /// When true, signals that a reschedule should happen diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index e56253c4c..3dab81769 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -1,6 +1,8 @@  add_library(video_core STATIC      command_processor.cpp      command_processor.h +    debug_utils/debug_utils.cpp +    debug_utils/debug_utils.h      engines/fermi_2d.cpp      engines/fermi_2d.h      engines/maxwell_3d.cpp @@ -31,6 +33,9 @@ add_library(video_core STATIC      renderer_opengl/gl_stream_buffer.h      renderer_opengl/renderer_opengl.cpp      renderer_opengl/renderer_opengl.h +    textures/decoders.cpp +    textures/decoders.h +    textures/texture.h      utils.h      video_core.cpp      video_core.h diff --git a/src/video_core/debug_utils/debug_utils.cpp b/src/video_core/debug_utils/debug_utils.cpp new file mode 100644 index 000000000..22d44aab2 --- /dev/null +++ b/src/video_core/debug_utils/debug_utils.cpp @@ -0,0 +1,64 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#include <algorithm> +#include <condition_variable> +#include <cstdint> +#include <cstring> +#include <fstream> +#include <map> +#include <mutex> +#include <string> + +#include "common/assert.h" +#include "common/bit_field.h" +#include "common/color.h" +#include "common/common_types.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/math_util.h" +#include "common/vector_math.h" +#include "video_core/debug_utils/debug_utils.h" + +namespace Tegra { + +void DebugContext::DoOnEvent(Event event, void* data) { +    { +        std::unique_lock<std::mutex> lock(breakpoint_mutex); + +        // TODO(Subv): Commit the rasterizer's caches so framebuffers, render targets, etc. will +        // show on debug widgets + +        // TODO: Should stop the CPU thread here once we multithread emulation. + +        active_breakpoint = event; +        at_breakpoint = true; + +        // Tell all observers that we hit a breakpoint +        for (auto& breakpoint_observer : breakpoint_observers) { +            breakpoint_observer->OnMaxwellBreakPointHit(event, data); +        } + +        // Wait until another thread tells us to Resume() +        resume_from_breakpoint.wait(lock, [&] { return !at_breakpoint; }); +    } +} + +void DebugContext::Resume() { +    { +        std::lock_guard<std::mutex> lock(breakpoint_mutex); + +        // Tell all observers that we are about to resume +        for (auto& breakpoint_observer : breakpoint_observers) { +            breakpoint_observer->OnMaxwellResume(); +        } + +        // Resume the waiting thread (i.e. OnEvent()) +        at_breakpoint = false; +    } + +    resume_from_breakpoint.notify_one(); +} + +} // namespace Tegra diff --git a/src/video_core/debug_utils/debug_utils.h b/src/video_core/debug_utils/debug_utils.h new file mode 100644 index 000000000..bbba8e380 --- /dev/null +++ b/src/video_core/debug_utils/debug_utils.h @@ -0,0 +1,163 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#pragma once + +#include <algorithm> +#include <array> +#include <condition_variable> +#include <iterator> +#include <list> +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <utility> +#include <vector> +#include "common/common_types.h" +#include "common/vector_math.h" + +namespace Tegra { + +class DebugContext { +public: +    enum class Event { +        FirstEvent = 0, + +        MaxwellCommandLoaded = FirstEvent, +        MaxwellCommandProcessed, +        IncomingPrimitiveBatch, +        FinishedPrimitiveBatch, + +        NumEvents +    }; + +    /** +     * Inherit from this class to be notified of events registered to some debug context. +     * Most importantly this is used for our debugger GUI. +     * +     * To implement event handling, override the OnMaxwellBreakPointHit and OnMaxwellResume methods. +     * @warning All BreakPointObservers need to be on the same thread to guarantee thread-safe state +     * access +     * @todo Evaluate an alternative interface, in which there is only one managing observer and +     * multiple child observers running (by design) on the same thread. +     */ +    class BreakPointObserver { +    public: +        /// Constructs the object such that it observes events of the given DebugContext. +        BreakPointObserver(std::shared_ptr<DebugContext> debug_context) +            : context_weak(debug_context) { +            std::unique_lock<std::mutex> lock(debug_context->breakpoint_mutex); +            debug_context->breakpoint_observers.push_back(this); +        } + +        virtual ~BreakPointObserver() { +            auto context = context_weak.lock(); +            if (context) { +                std::unique_lock<std::mutex> lock(context->breakpoint_mutex); +                context->breakpoint_observers.remove(this); + +                // If we are the last observer to be destroyed, tell the debugger context that +                // it is free to continue. In particular, this is required for a proper yuzu +                // shutdown, when the emulation thread is waiting at a breakpoint. +                if (context->breakpoint_observers.empty()) +                    context->Resume(); +            } +        } + +        /** +         * Action to perform when a breakpoint was reached. +         * @param event Type of event which triggered the breakpoint +         * @param data Optional data pointer (if unused, this is a nullptr) +         * @note This function will perform nothing unless it is overridden in the child class. +         */ +        virtual void OnMaxwellBreakPointHit(Event event, void* data) {} + +        /** +         * Action to perform when emulation is resumed from a breakpoint. +         * @note This function will perform nothing unless it is overridden in the child class. +         */ +        virtual void OnMaxwellResume() {} + +    protected: +        /** +         * Weak context pointer. This need not be valid, so when requesting a shared_ptr via +         * context_weak.lock(), always compare the result against nullptr. +         */ +        std::weak_ptr<DebugContext> context_weak; +    }; + +    /** +     * Simple structure defining a breakpoint state +     */ +    struct BreakPoint { +        bool enabled = false; +    }; + +    /** +     * Static constructor used to create a shared_ptr of a DebugContext. +     */ +    static std::shared_ptr<DebugContext> Construct() { +        return std::shared_ptr<DebugContext>(new DebugContext); +    } + +    /** +     * Used by the emulation core when a given event has happened. If a breakpoint has been set +     * for this event, OnEvent calls the event handlers of the registered breakpoint observers. +     * The current thread then is halted until Resume() is called from another thread (or until +     * emulation is stopped). +     * @param event Event which has happened +     * @param data Optional data pointer (pass nullptr if unused). Needs to remain valid until +     * Resume() is called. +     */ +    void OnEvent(Event event, void* data) { +        // This check is left in the header to allow the compiler to inline it. +        if (!breakpoints[(int)event].enabled) +            return; +        // For the rest of event handling, call a separate function. +        DoOnEvent(event, data); +    } + +    void DoOnEvent(Event event, void* data); + +    /** +     * Resume from the current breakpoint. +     * @warning Calling this from the same thread that OnEvent was called in will cause a deadlock. +     * Calling from any other thread is safe. +     */ +    void Resume(); + +    /** +     * Delete all set breakpoints and resume emulation. +     */ +    void ClearBreakpoints() { +        for (auto& bp : breakpoints) { +            bp.enabled = false; +        } +        Resume(); +    } + +    // TODO: Evaluate if access to these members should be hidden behind a public interface. +    std::array<BreakPoint, (int)Event::NumEvents> breakpoints; +    Event active_breakpoint; +    bool at_breakpoint = false; + +private: +    /** +     * Private default constructor to make sure people always construct this through Construct() +     * instead. +     */ +    DebugContext() = default; + +    /// Mutex protecting current breakpoint state and the observer list. +    std::mutex breakpoint_mutex; + +    /// Used by OnEvent to wait for resumption. +    std::condition_variable resume_from_breakpoint; + +    /// List of registered observers +    std::list<BreakPointObserver*> breakpoint_observers; +}; + +} // namespace Tegra diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 4d9745e48..986165c6d 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -2,8 +2,13 @@  // Licensed under GPLv2 or any later version  // Refer to the license.txt file included. +#include <cinttypes>  #include "common/assert.h" +#include "core/core.h" +#include "video_core/debug_utils/debug_utils.h"  #include "video_core/engines/maxwell_3d.h" +#include "video_core/textures/decoders.h" +#include "video_core/textures/texture.h"  namespace Tegra {  namespace Engines { @@ -46,6 +51,8 @@ void Maxwell3D::WriteReg(u32 method, u32 value, u32 remaining_params) {      ASSERT_MSG(method < Regs::NUM_REGS,                 "Invalid Maxwell3D register, increase the size of the Regs structure"); +    auto debug_context = Core::System::GetInstance().GetGPUDebugContext(); +      // It is an error to write to a register other than the current macro's ARG register before it      // has finished execution.      if (executing_macro != 0) { @@ -72,6 +79,10 @@ void Maxwell3D::WriteReg(u32 method, u32 value, u32 remaining_params) {          return;      } +    if (debug_context) { +        debug_context->OnEvent(Tegra::DebugContext::Event::MaxwellCommandLoaded, nullptr); +    } +      regs.reg_array[method] = value;  #define MAXWELL3D_REG_INDEX(field_name) (offsetof(Regs, field_name) / sizeof(u32)) @@ -137,6 +148,10 @@ void Maxwell3D::WriteReg(u32 method, u32 value, u32 remaining_params) {      }  #undef MAXWELL3D_REG_INDEX + +    if (debug_context) { +        debug_context->OnEvent(Tegra::DebugContext::Event::MaxwellCommandProcessed, nullptr); +    }  }  void Maxwell3D::ProcessQueryGet() { @@ -160,6 +175,15 @@ void Maxwell3D::ProcessQueryGet() {  void Maxwell3D::DrawArrays() {      LOG_WARNING(HW_GPU, "Game requested a DrawArrays, ignoring"); +    auto debug_context = Core::System::GetInstance().GetGPUDebugContext(); + +    if (debug_context) { +        debug_context->OnEvent(Tegra::DebugContext::Event::IncomingPrimitiveBatch, nullptr); +    } + +    if (debug_context) { +        debug_context->OnEvent(Tegra::DebugContext::Event::FinishedPrimitiveBatch, nullptr); +    }  }  void Maxwell3D::BindTextureInfoBuffer(const std::vector<u32>& parameters) { @@ -270,5 +294,50 @@ void Maxwell3D::ProcessCBData(u32 value) {      regs.const_buffer.cb_pos = regs.const_buffer.cb_pos + 4;  } +std::vector<Texture::TICEntry> Maxwell3D::GetStageTextures(Regs::ShaderStage stage) { +    std::vector<Texture::TICEntry> textures; + +    auto& fragment_shader = state.shader_stages[static_cast<size_t>(stage)]; +    auto& tex_info_buffer = fragment_shader.const_buffers[regs.tex_cb_index]; +    ASSERT(tex_info_buffer.enabled && tex_info_buffer.address != 0); + +    GPUVAddr tic_base_address = regs.tic.TICAddress(); + +    GPUVAddr tex_info_buffer_end = tex_info_buffer.address + tex_info_buffer.size; + +    // Offset into the texture constbuffer where the texture info begins. +    static constexpr size_t TextureInfoOffset = 0x20; + +    for (GPUVAddr current_texture = tex_info_buffer.address + TextureInfoOffset; +         current_texture < tex_info_buffer_end; current_texture += 4) { + +        Texture::TextureHandle tex_info{ +            Memory::Read32(memory_manager.PhysicalToVirtualAddress(current_texture))}; + +        if (tex_info.tic_id != 0 || tex_info.tsc_id != 0) { +            GPUVAddr tic_address_gpu = +                tic_base_address + tex_info.tic_id * sizeof(Texture::TICEntry); +            VAddr tic_address_cpu = memory_manager.PhysicalToVirtualAddress(tic_address_gpu); + +            Texture::TICEntry tic_entry; +            Memory::ReadBlock(tic_address_cpu, &tic_entry, sizeof(Texture::TICEntry)); + +            auto r_type = tic_entry.r_type.Value(); +            auto g_type = tic_entry.g_type.Value(); +            auto b_type = tic_entry.b_type.Value(); +            auto a_type = tic_entry.a_type.Value(); + +            // TODO(Subv): Different data types for separate components are not supported +            ASSERT(r_type == g_type && r_type == b_type && r_type == a_type); + +            auto format = tic_entry.format.Value(); + +            textures.push_back(tic_entry); +        } +    } + +    return textures; +} +  } // namespace Engines  } // namespace Tegra diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 545d7ff35..441cc0c19 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -12,6 +12,7 @@  #include "common/common_funcs.h"  #include "common/common_types.h"  #include "video_core/memory_manager.h" +#include "video_core/textures/texture.h"  namespace Tegra {  namespace Engines { @@ -21,12 +22,6 @@ public:      explicit Maxwell3D(MemoryManager& memory_manager);      ~Maxwell3D() = default; -    /// Write the value to the register identified by method. -    void WriteReg(u32 method, u32 value, u32 remaining_params); - -    /// Uploads the code for a GPU macro program associated with the specified entry. -    void SubmitMacroCode(u32 entry, std::vector<u32> code); -      /// Register structure of the Maxwell3D engine.      /// TODO(Subv): This structure will need to be made bigger as more registers are discovered.      struct Regs { @@ -430,6 +425,15 @@ public:      State state{}; +    /// Write the value to the register identified by method. +    void WriteReg(u32 method, u32 value, u32 remaining_params); + +    /// Uploads the code for a GPU macro program associated with the specified entry. +    void SubmitMacroCode(u32 entry, std::vector<u32> code); + +    /// Returns a list of enabled textures for the specified shader stage. +    std::vector<Texture::TICEntry> GetStageTextures(Regs::ShaderStage stage); +  private:      MemoryManager& memory_manager; diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index c384d236e..9463cd5d6 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -18,4 +18,8 @@ GPU::GPU() {  GPU::~GPU() = default; +const Tegra::Engines::Maxwell3D& GPU::Get3DEngine() const { +    return *maxwell_3d; +} +  } // namespace Tegra diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 206b3e05e..8183b12e9 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -13,6 +13,12 @@  namespace Tegra { +enum class RenderTargetFormat { +    RGBA8_UNORM = 0xD5, +}; + +class DebugContext; +  /**   * Struct describing framebuffer configuration   */ @@ -66,6 +72,9 @@ public:      /// Processes a command list stored at the specified address in GPU memory.      void ProcessCommandList(GPUVAddr address, u32 size); +    /// Returns a reference to the Maxwell3D GPU engine. +    const Engines::Maxwell3D& Get3DEngine() const; +      std::unique_ptr<MemoryManager> memory_manager;      Engines::Maxwell3D& Maxwell3D() { diff --git a/src/video_core/textures/decoders.cpp b/src/video_core/textures/decoders.cpp new file mode 100644 index 000000000..2e87281eb --- /dev/null +++ b/src/video_core/textures/decoders.cpp @@ -0,0 +1,105 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <cstring> +#include "common/assert.h" +#include "video_core/textures/decoders.h" +#include "video_core/textures/texture.h" + +namespace Tegra { +namespace Texture { + +/** + * Calculates the offset of an (x, y) position within a swizzled texture. + * Taken from the Tegra X1 TRM. + */ +static u32 GetSwizzleOffset(u32 x, u32 y, u32 image_width, u32 bytes_per_pixel, u32 block_height) { +    u32 image_width_in_gobs = image_width * bytes_per_pixel / 64; +    u32 GOB_address = 0 + (y / (8 * block_height)) * 512 * block_height * image_width_in_gobs + +                      (x * bytes_per_pixel / 64) * 512 * block_height + +                      (y % (8 * block_height) / 8) * 512; +    x *= bytes_per_pixel; +    u32 address = GOB_address + ((x % 64) / 32) * 256 + ((y % 8) / 2) * 64 + ((x % 32) / 16) * 32 + +                  (y % 2) * 16 + (x % 16); + +    return address; +} + +static void CopySwizzledData(u32 width, u32 height, u32 bytes_per_pixel, u32 out_bytes_per_pixel, +                             u8* swizzled_data, u8* unswizzled_data, bool unswizzle, +                             u32 block_height) { +    u8* data_ptrs[2]; +    for (unsigned y = 0; y < height; ++y) { +        for (unsigned x = 0; x < width; ++x) { +            u32 swizzle_offset = GetSwizzleOffset(x, y, width, bytes_per_pixel, block_height); +            u32 pixel_index = (x + y * width) * out_bytes_per_pixel; + +            data_ptrs[unswizzle] = swizzled_data + swizzle_offset; +            data_ptrs[!unswizzle] = &unswizzled_data[pixel_index]; + +            std::memcpy(data_ptrs[0], data_ptrs[1], bytes_per_pixel); +        } +    } +} + +u32 BytesPerPixel(TextureFormat format) { +    switch (format) { +    case TextureFormat::DXT1: +        // In this case a 'pixel' actually refers to a 4x4 tile. +        return 8; +    case TextureFormat::A8R8G8B8: +        return 4; +    default: +        UNIMPLEMENTED_MSG("Format not implemented"); +        break; +    } +} + +std::vector<u8> UnswizzleTexture(VAddr address, TextureFormat format, u32 width, u32 height) { +    u8* data = Memory::GetPointer(address); +    u32 bytes_per_pixel = BytesPerPixel(format); + +    static constexpr u32 DefaultBlockHeight = 16; + +    std::vector<u8> unswizzled_data(width * height * bytes_per_pixel); + +    switch (format) { +    case TextureFormat::DXT1: +        // In the DXT1 format, each 4x4 tile is swizzled instead of just individual pixel values. +        CopySwizzledData(width / 4, height / 4, bytes_per_pixel, bytes_per_pixel, data, +                         unswizzled_data.data(), true, DefaultBlockHeight); +        break; +    case TextureFormat::A8R8G8B8: +        CopySwizzledData(width, height, bytes_per_pixel, bytes_per_pixel, data, +                         unswizzled_data.data(), true, DefaultBlockHeight); +        break; +    default: +        UNIMPLEMENTED_MSG("Format not implemented"); +        break; +    } + +    return unswizzled_data; +} + +std::vector<u8> DecodeTexture(const std::vector<u8>& texture_data, TextureFormat format, u32 width, +                              u32 height) { +    std::vector<u8> rgba_data; + +    // TODO(Subv): Implement. +    switch (format) { +    case TextureFormat::DXT1: +    case TextureFormat::A8R8G8B8: +        // TODO(Subv): For the time being just forward the same data without any decoding. +        rgba_data = texture_data; +        break; +    default: +        UNIMPLEMENTED_MSG("Format not implemented"); +        break; +    } + +    return rgba_data; +} + +} // namespace Texture +} // namespace Tegra diff --git a/src/video_core/textures/decoders.h b/src/video_core/textures/decoders.h new file mode 100644 index 000000000..0c21694ff --- /dev/null +++ b/src/video_core/textures/decoders.h @@ -0,0 +1,26 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <vector> +#include "common/common_types.h" +#include "video_core/textures/texture.h" + +namespace Tegra { +namespace Texture { + +/** + * Unswizzles a swizzled texture without changing its format. + */ +std::vector<u8> UnswizzleTexture(VAddr address, TextureFormat format, u32 width, u32 height); + +/** + * Decodes an unswizzled texture into a A8R8G8B8 texture. + */ +std::vector<u8> DecodeTexture(const std::vector<u8>& texture_data, TextureFormat format, u32 width, +                              u32 height); + +} // namespace Texture +} // namespace Tegra diff --git a/src/video_core/textures/texture.h b/src/video_core/textures/texture.h new file mode 100644 index 000000000..d969bcdd9 --- /dev/null +++ b/src/video_core/textures/texture.h @@ -0,0 +1,61 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/bit_field.h" +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "video_core/memory_manager.h" + +namespace Tegra { +namespace Texture { + +enum class TextureFormat : u32 { +    A8R8G8B8 = 8, +    DXT1 = 0x24, +}; + +union TextureHandle { +    u32 raw; +    BitField<0, 20, u32> tic_id; +    BitField<20, 12, u32> tsc_id; +}; + +struct TICEntry { +    union { +        u32 raw; +        BitField<0, 7, TextureFormat> format; +        BitField<7, 3, u32> r_type; +        BitField<10, 3, u32> g_type; +        BitField<13, 3, u32> b_type; +        BitField<16, 3, u32> a_type; +    }; +    u32 address_low; +    u16 address_high; +    INSERT_PADDING_BYTES(6); +    u16 width_minus_1; +    INSERT_PADDING_BYTES(2); +    u16 height_minus_1; +    INSERT_PADDING_BYTES(10); + +    GPUVAddr Address() const { +        return static_cast<GPUVAddr>((static_cast<GPUVAddr>(address_high) << 32) | address_low); +    } + +    u32 Width() const { +        return width_minus_1 + 1; +    } + +    u32 Height() const { +        return height_minus_1 + 1; +    } +}; +static_assert(sizeof(TICEntry) == 0x20, "TICEntry has wrong size"); + +/// Returns the number of bytes per pixel of the input texture format. +u32 BytesPerPixel(TextureFormat format); + +} // namespace Texture +} // namespace Tegra diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 0c4056c49..5af3154d7 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -23,6 +23,13 @@ add_executable(yuzu      configuration/configure_input.h      configuration/configure_system.cpp      configuration/configure_system.h +    debugger/graphics/graphics_breakpoint_observer.cpp +    debugger/graphics/graphics_breakpoint_observer.h +    debugger/graphics/graphics_breakpoints.cpp +    debugger/graphics/graphics_breakpoints.h +    debugger/graphics/graphics_breakpoints_p.h +    debugger/graphics/graphics_surface.cpp +    debugger/graphics/graphics_surface.h      debugger/profiler.cpp      debugger/profiler.h      debugger/registers.cpp diff --git a/src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp b/src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp new file mode 100644 index 000000000..d6d61a739 --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp @@ -0,0 +1,27 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QMetaType> +#include "yuzu/debugger/graphics/graphics_breakpoint_observer.h" + +BreakPointObserverDock::BreakPointObserverDock(std::shared_ptr<Tegra::DebugContext> debug_context, +                                               const QString& title, QWidget* parent) +    : QDockWidget(title, parent), BreakPointObserver(debug_context) { +    qRegisterMetaType<Tegra::DebugContext::Event>("Tegra::DebugContext::Event"); + +    connect(this, SIGNAL(Resumed()), this, SLOT(OnResumed())); + +    // NOTE: This signal is emitted from a non-GUI thread, but connect() takes +    //       care of delaying its handling to the GUI thread. +    connect(this, SIGNAL(BreakPointHit(Tegra::DebugContext::Event, void*)), this, +            SLOT(OnBreakPointHit(Tegra::DebugContext::Event, void*)), Qt::BlockingQueuedConnection); +} + +void BreakPointObserverDock::OnMaxwellBreakPointHit(Tegra::DebugContext::Event event, void* data) { +    emit BreakPointHit(event, data); +} + +void BreakPointObserverDock::OnMaxwellResume() { +    emit Resumed(); +} diff --git a/src/yuzu/debugger/graphics/graphics_breakpoint_observer.h b/src/yuzu/debugger/graphics/graphics_breakpoint_observer.h new file mode 100644 index 000000000..9d05493cf --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_breakpoint_observer.h @@ -0,0 +1,33 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QDockWidget> +#include "video_core/debug_utils/debug_utils.h" + +/** + * Utility class which forwards calls to OnMaxwellBreakPointHit and OnMaxwellResume to public slots. + * This is because the Maxwell breakpoint callbacks are called from a non-GUI thread, while + * the widget usually wants to perform reactions in the GUI thread. + */ +class BreakPointObserverDock : public QDockWidget, +                               protected Tegra::DebugContext::BreakPointObserver { +    Q_OBJECT + +public: +    BreakPointObserverDock(std::shared_ptr<Tegra::DebugContext> debug_context, const QString& title, +                           QWidget* parent = nullptr); + +    void OnMaxwellBreakPointHit(Tegra::DebugContext::Event event, void* data) override; +    void OnMaxwellResume() override; + +private slots: +    virtual void OnBreakPointHit(Tegra::DebugContext::Event event, void* data) = 0; +    virtual void OnResumed() = 0; + +signals: +    void Resumed(); +    void BreakPointHit(Tegra::DebugContext::Event event, void* data); +}; diff --git a/src/yuzu/debugger/graphics/graphics_breakpoints.cpp b/src/yuzu/debugger/graphics/graphics_breakpoints.cpp new file mode 100644 index 000000000..f98cc8152 --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_breakpoints.cpp @@ -0,0 +1,212 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QLabel> +#include <QMetaType> +#include <QPushButton> +#include <QTreeView> +#include <QVBoxLayout> +#include "common/assert.h" +#include "yuzu/debugger/graphics/graphics_breakpoints.h" +#include "yuzu/debugger/graphics/graphics_breakpoints_p.h" + +BreakPointModel::BreakPointModel(std::shared_ptr<Tegra::DebugContext> debug_context, +                                 QObject* parent) +    : QAbstractListModel(parent), context_weak(debug_context), +      at_breakpoint(debug_context->at_breakpoint), +      active_breakpoint(debug_context->active_breakpoint) {} + +int BreakPointModel::columnCount(const QModelIndex& parent) const { +    return 1; +} + +int BreakPointModel::rowCount(const QModelIndex& parent) const { +    return static_cast<int>(Tegra::DebugContext::Event::NumEvents); +} + +QVariant BreakPointModel::data(const QModelIndex& index, int role) const { +    const auto event = static_cast<Tegra::DebugContext::Event>(index.row()); + +    switch (role) { +    case Qt::DisplayRole: { +        if (index.column() == 0) { +            static const std::map<Tegra::DebugContext::Event, QString> map = { +                {Tegra::DebugContext::Event::MaxwellCommandLoaded, tr("Maxwell command loaded")}, +                {Tegra::DebugContext::Event::MaxwellCommandProcessed, +                 tr("Maxwell command processed")}, +                {Tegra::DebugContext::Event::IncomingPrimitiveBatch, +                 tr("Incoming primitive batch")}, +                {Tegra::DebugContext::Event::FinishedPrimitiveBatch, +                 tr("Finished primitive batch")}, +            }; + +            DEBUG_ASSERT(map.size() == static_cast<size_t>(Tegra::DebugContext::Event::NumEvents)); +            return (map.find(event) != map.end()) ? map.at(event) : QString(); +        } + +        break; +    } + +    case Qt::CheckStateRole: { +        if (index.column() == 0) +            return data(index, Role_IsEnabled).toBool() ? Qt::Checked : Qt::Unchecked; +        break; +    } + +    case Qt::BackgroundRole: { +        if (at_breakpoint && index.row() == static_cast<int>(active_breakpoint)) { +            return QBrush(QColor(0xE0, 0xE0, 0x10)); +        } +        break; +    } + +    case Role_IsEnabled: { +        auto context = context_weak.lock(); +        return context && context->breakpoints[(int)event].enabled; +    } + +    default: +        break; +    } +    return QVariant(); +} + +Qt::ItemFlags BreakPointModel::flags(const QModelIndex& index) const { +    if (!index.isValid()) +        return 0; + +    Qt::ItemFlags flags = Qt::ItemIsEnabled; +    if (index.column() == 0) +        flags |= Qt::ItemIsUserCheckable; +    return flags; +} + +bool BreakPointModel::setData(const QModelIndex& index, const QVariant& value, int role) { +    const auto event = static_cast<Tegra::DebugContext::Event>(index.row()); + +    switch (role) { +    case Qt::CheckStateRole: { +        if (index.column() != 0) +            return false; + +        auto context = context_weak.lock(); +        if (!context) +            return false; + +        context->breakpoints[(int)event].enabled = value == Qt::Checked; +        QModelIndex changed_index = createIndex(index.row(), 0); +        emit dataChanged(changed_index, changed_index); +        return true; +    } +    } + +    return false; +} + +void BreakPointModel::OnBreakPointHit(Tegra::DebugContext::Event event) { +    auto context = context_weak.lock(); +    if (!context) +        return; + +    active_breakpoint = context->active_breakpoint; +    at_breakpoint = context->at_breakpoint; +    emit dataChanged(createIndex(static_cast<int>(event), 0), +                     createIndex(static_cast<int>(event), 0)); +} + +void BreakPointModel::OnResumed() { +    auto context = context_weak.lock(); +    if (!context) +        return; + +    at_breakpoint = context->at_breakpoint; +    emit dataChanged(createIndex(static_cast<int>(active_breakpoint), 0), +                     createIndex(static_cast<int>(active_breakpoint), 0)); +    active_breakpoint = context->active_breakpoint; +} + +GraphicsBreakPointsWidget::GraphicsBreakPointsWidget( +    std::shared_ptr<Tegra::DebugContext> debug_context, QWidget* parent) +    : QDockWidget(tr("Maxwell Breakpoints"), parent), Tegra::DebugContext::BreakPointObserver( +                                                          debug_context) { +    setObjectName("TegraBreakPointsWidget"); + +    status_text = new QLabel(tr("Emulation running")); +    resume_button = new QPushButton(tr("Resume")); +    resume_button->setEnabled(false); + +    breakpoint_model = new BreakPointModel(debug_context, this); +    breakpoint_list = new QTreeView; +    breakpoint_list->setRootIsDecorated(false); +    breakpoint_list->setHeaderHidden(true); +    breakpoint_list->setModel(breakpoint_model); + +    qRegisterMetaType<Tegra::DebugContext::Event>("Tegra::DebugContext::Event"); + +    connect(breakpoint_list, SIGNAL(doubleClicked(const QModelIndex&)), this, +            SLOT(OnItemDoubleClicked(const QModelIndex&))); + +    connect(resume_button, SIGNAL(clicked()), this, SLOT(OnResumeRequested())); + +    connect(this, SIGNAL(BreakPointHit(Tegra::DebugContext::Event, void*)), this, +            SLOT(OnBreakPointHit(Tegra::DebugContext::Event, void*)), Qt::BlockingQueuedConnection); +    connect(this, SIGNAL(Resumed()), this, SLOT(OnResumed())); + +    connect(this, SIGNAL(BreakPointHit(Tegra::DebugContext::Event, void*)), breakpoint_model, +            SLOT(OnBreakPointHit(Tegra::DebugContext::Event)), Qt::BlockingQueuedConnection); +    connect(this, SIGNAL(Resumed()), breakpoint_model, SLOT(OnResumed())); + +    connect(this, SIGNAL(BreakPointsChanged(const QModelIndex&, const QModelIndex&)), +            breakpoint_model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&))); + +    QWidget* main_widget = new QWidget; +    auto main_layout = new QVBoxLayout; +    { +        auto sub_layout = new QHBoxLayout; +        sub_layout->addWidget(status_text); +        sub_layout->addWidget(resume_button); +        main_layout->addLayout(sub_layout); +    } +    main_layout->addWidget(breakpoint_list); +    main_widget->setLayout(main_layout); + +    setWidget(main_widget); +} + +void GraphicsBreakPointsWidget::OnMaxwellBreakPointHit(Event event, void* data) { +    // Process in GUI thread +    emit BreakPointHit(event, data); +} + +void GraphicsBreakPointsWidget::OnBreakPointHit(Tegra::DebugContext::Event event, void* data) { +    status_text->setText(tr("Emulation halted at breakpoint")); +    resume_button->setEnabled(true); +} + +void GraphicsBreakPointsWidget::OnMaxwellResume() { +    // Process in GUI thread +    emit Resumed(); +} + +void GraphicsBreakPointsWidget::OnResumed() { +    status_text->setText(tr("Emulation running")); +    resume_button->setEnabled(false); +} + +void GraphicsBreakPointsWidget::OnResumeRequested() { +    if (auto context = context_weak.lock()) +        context->Resume(); +} + +void GraphicsBreakPointsWidget::OnItemDoubleClicked(const QModelIndex& index) { +    if (!index.isValid()) +        return; + +    QModelIndex check_index = breakpoint_list->model()->index(index.row(), 0); +    QVariant enabled = breakpoint_list->model()->data(check_index, Qt::CheckStateRole); +    QVariant new_state = Qt::Unchecked; +    if (enabled == Qt::Unchecked) +        new_state = Qt::Checked; +    breakpoint_list->model()->setData(check_index, new_state, Qt::CheckStateRole); +} diff --git a/src/yuzu/debugger/graphics/graphics_breakpoints.h b/src/yuzu/debugger/graphics/graphics_breakpoints.h new file mode 100644 index 000000000..ae0ede2e8 --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_breakpoints.h @@ -0,0 +1,46 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <QDockWidget> +#include "video_core/debug_utils/debug_utils.h" + +class QLabel; +class QPushButton; +class QTreeView; + +class BreakPointModel; + +class GraphicsBreakPointsWidget : public QDockWidget, Tegra::DebugContext::BreakPointObserver { +    Q_OBJECT + +    using Event = Tegra::DebugContext::Event; + +public: +    explicit GraphicsBreakPointsWidget(std::shared_ptr<Tegra::DebugContext> debug_context, +                                       QWidget* parent = nullptr); + +    void OnMaxwellBreakPointHit(Tegra::DebugContext::Event event, void* data) override; +    void OnMaxwellResume() override; + +public slots: +    void OnBreakPointHit(Tegra::DebugContext::Event event, void* data); +    void OnItemDoubleClicked(const QModelIndex&); +    void OnResumeRequested(); +    void OnResumed(); + +signals: +    void Resumed(); +    void BreakPointHit(Tegra::DebugContext::Event event, void* data); +    void BreakPointsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); + +private: +    QLabel* status_text; +    QPushButton* resume_button; + +    BreakPointModel* breakpoint_model; +    QTreeView* breakpoint_list; +}; diff --git a/src/yuzu/debugger/graphics/graphics_breakpoints_p.h b/src/yuzu/debugger/graphics/graphics_breakpoints_p.h new file mode 100644 index 000000000..35a6876ae --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_breakpoints_p.h @@ -0,0 +1,36 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <QAbstractListModel> +#include "video_core/debug_utils/debug_utils.h" + +class BreakPointModel : public QAbstractListModel { +    Q_OBJECT + +public: +    enum { +        Role_IsEnabled = Qt::UserRole, +    }; + +    BreakPointModel(std::shared_ptr<Tegra::DebugContext> context, QObject* parent); + +    int columnCount(const QModelIndex& parent = QModelIndex()) const override; +    int rowCount(const QModelIndex& parent = QModelIndex()) const override; +    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; +    Qt::ItemFlags flags(const QModelIndex& index) const override; + +    bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + +public slots: +    void OnBreakPointHit(Tegra::DebugContext::Event event); +    void OnResumed(); + +private: +    std::weak_ptr<Tegra::DebugContext> context_weak; +    bool at_breakpoint; +    Tegra::DebugContext::Event active_breakpoint; +}; diff --git a/src/yuzu/debugger/graphics/graphics_surface.cpp b/src/yuzu/debugger/graphics/graphics_surface.cpp new file mode 100644 index 000000000..8e6509adc --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_surface.cpp @@ -0,0 +1,452 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QBoxLayout> +#include <QComboBox> +#include <QDebug> +#include <QFileDialog> +#include <QLabel> +#include <QMouseEvent> +#include <QPushButton> +#include <QScrollArea> +#include <QSpinBox> +#include "core/core.h" +#include "video_core/engines/maxwell_3d.h" +#include "video_core/gpu.h" +#include "video_core/textures/decoders.h" +#include "video_core/textures/texture.h" +#include "video_core/utils.h" +#include "yuzu/debugger/graphics/graphics_surface.h" +#include "yuzu/util/spinbox.h" + +static Tegra::Texture::TextureFormat ConvertToTextureFormat( +    Tegra::RenderTargetFormat render_target_format) { +    switch (render_target_format) { +    case Tegra::RenderTargetFormat::RGBA8_UNORM: +        return Tegra::Texture::TextureFormat::A8R8G8B8; +    default: +        UNIMPLEMENTED_MSG("Unimplemented RT format"); +    } +} + +SurfacePicture::SurfacePicture(QWidget* parent, GraphicsSurfaceWidget* surface_widget_) +    : QLabel(parent), surface_widget(surface_widget_) {} +SurfacePicture::~SurfacePicture() {} + +void SurfacePicture::mousePressEvent(QMouseEvent* event) { +    // Only do something while the left mouse button is held down +    if (!(event->buttons() & Qt::LeftButton)) +        return; + +    if (pixmap() == nullptr) +        return; + +    if (surface_widget) +        surface_widget->Pick(event->x() * pixmap()->width() / width(), +                             event->y() * pixmap()->height() / height()); +} + +void SurfacePicture::mouseMoveEvent(QMouseEvent* event) { +    // We also want to handle the event if the user moves the mouse while holding down the LMB +    mousePressEvent(event); +} + +GraphicsSurfaceWidget::GraphicsSurfaceWidget(std::shared_ptr<Tegra::DebugContext> debug_context, +                                             QWidget* parent) +    : BreakPointObserverDock(debug_context, tr("Maxwell Surface Viewer"), parent), +      surface_source(Source::RenderTarget0) { +    setObjectName("MaxwellSurface"); + +    surface_source_list = new QComboBox; +    surface_source_list->addItem(tr("Render Target 0")); +    surface_source_list->addItem(tr("Render Target 1")); +    surface_source_list->addItem(tr("Render Target 2")); +    surface_source_list->addItem(tr("Render Target 3")); +    surface_source_list->addItem(tr("Render Target 4")); +    surface_source_list->addItem(tr("Render Target 5")); +    surface_source_list->addItem(tr("Render Target 6")); +    surface_source_list->addItem(tr("Render Target 7")); +    surface_source_list->addItem(tr("Z Buffer")); +    surface_source_list->addItem(tr("Custom")); +    surface_source_list->setCurrentIndex(static_cast<int>(surface_source)); + +    surface_address_control = new CSpinBox; +    surface_address_control->SetBase(16); +    surface_address_control->SetRange(0, 0x7FFFFFFFFFFFFFFF); +    surface_address_control->SetPrefix("0x"); + +    unsigned max_dimension = 16384; // TODO: Find actual maximum + +    surface_width_control = new QSpinBox; +    surface_width_control->setRange(0, max_dimension); + +    surface_height_control = new QSpinBox; +    surface_height_control->setRange(0, max_dimension); + +    surface_picker_x_control = new QSpinBox; +    surface_picker_x_control->setRange(0, max_dimension - 1); + +    surface_picker_y_control = new QSpinBox; +    surface_picker_y_control->setRange(0, max_dimension - 1); + +    surface_format_control = new QComboBox; + +    // Color formats sorted by Maxwell texture format index +    surface_format_control->addItem(tr("None")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("A8R8G8B8")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("Unknown")); +    surface_format_control->addItem(tr("DXT1")); +    surface_format_control->addItem(tr("DXT23")); +    surface_format_control->addItem(tr("DXT45")); +    surface_format_control->addItem(tr("DXN1")); +    surface_format_control->addItem(tr("DXN2")); + +    surface_info_label = new QLabel(); +    surface_info_label->setWordWrap(true); + +    surface_picture_label = new SurfacePicture(0, this); +    surface_picture_label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); +    surface_picture_label->setAlignment(Qt::AlignLeft | Qt::AlignTop); +    surface_picture_label->setScaledContents(false); + +    auto scroll_area = new QScrollArea(); +    scroll_area->setBackgroundRole(QPalette::Dark); +    scroll_area->setWidgetResizable(false); +    scroll_area->setWidget(surface_picture_label); + +    save_surface = new QPushButton(QIcon::fromTheme("document-save"), tr("Save")); + +    // Connections +    connect(this, SIGNAL(Update()), this, SLOT(OnUpdate())); +    connect(surface_source_list, SIGNAL(currentIndexChanged(int)), this, +            SLOT(OnSurfaceSourceChanged(int))); +    connect(surface_address_control, SIGNAL(ValueChanged(qint64)), this, +            SLOT(OnSurfaceAddressChanged(qint64))); +    connect(surface_width_control, SIGNAL(valueChanged(int)), this, +            SLOT(OnSurfaceWidthChanged(int))); +    connect(surface_height_control, SIGNAL(valueChanged(int)), this, +            SLOT(OnSurfaceHeightChanged(int))); +    connect(surface_format_control, SIGNAL(currentIndexChanged(int)), this, +            SLOT(OnSurfaceFormatChanged(int))); +    connect(surface_picker_x_control, SIGNAL(valueChanged(int)), this, +            SLOT(OnSurfacePickerXChanged(int))); +    connect(surface_picker_y_control, SIGNAL(valueChanged(int)), this, +            SLOT(OnSurfacePickerYChanged(int))); +    connect(save_surface, SIGNAL(clicked()), this, SLOT(SaveSurface())); + +    auto main_widget = new QWidget; +    auto main_layout = new QVBoxLayout; +    { +        auto sub_layout = new QHBoxLayout; +        sub_layout->addWidget(new QLabel(tr("Source:"))); +        sub_layout->addWidget(surface_source_list); +        main_layout->addLayout(sub_layout); +    } +    { +        auto sub_layout = new QHBoxLayout; +        sub_layout->addWidget(new QLabel(tr("GPU Address:"))); +        sub_layout->addWidget(surface_address_control); +        main_layout->addLayout(sub_layout); +    } +    { +        auto sub_layout = new QHBoxLayout; +        sub_layout->addWidget(new QLabel(tr("Width:"))); +        sub_layout->addWidget(surface_width_control); +        main_layout->addLayout(sub_layout); +    } +    { +        auto sub_layout = new QHBoxLayout; +        sub_layout->addWidget(new QLabel(tr("Height:"))); +        sub_layout->addWidget(surface_height_control); +        main_layout->addLayout(sub_layout); +    } +    { +        auto sub_layout = new QHBoxLayout; +        sub_layout->addWidget(new QLabel(tr("Format:"))); +        sub_layout->addWidget(surface_format_control); +        main_layout->addLayout(sub_layout); +    } +    main_layout->addWidget(scroll_area); + +    auto info_layout = new QHBoxLayout; +    { +        auto xy_layout = new QVBoxLayout; +        { +            { +                auto sub_layout = new QHBoxLayout; +                sub_layout->addWidget(new QLabel(tr("X:"))); +                sub_layout->addWidget(surface_picker_x_control); +                xy_layout->addLayout(sub_layout); +            } +            { +                auto sub_layout = new QHBoxLayout; +                sub_layout->addWidget(new QLabel(tr("Y:"))); +                sub_layout->addWidget(surface_picker_y_control); +                xy_layout->addLayout(sub_layout); +            } +        } +        info_layout->addLayout(xy_layout); +        surface_info_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); +        info_layout->addWidget(surface_info_label); +    } +    main_layout->addLayout(info_layout); + +    main_layout->addWidget(save_surface); +    main_widget->setLayout(main_layout); +    setWidget(main_widget); + +    // Load current data - TODO: Make sure this works when emulation is not running +    if (debug_context && debug_context->at_breakpoint) { +        emit Update(); +        widget()->setEnabled(debug_context->at_breakpoint); +    } else { +        widget()->setEnabled(false); +    } +} + +void GraphicsSurfaceWidget::OnBreakPointHit(Tegra::DebugContext::Event event, void* data) { +    emit Update(); +    widget()->setEnabled(true); +} + +void GraphicsSurfaceWidget::OnResumed() { +    widget()->setEnabled(false); +} + +void GraphicsSurfaceWidget::OnSurfaceSourceChanged(int new_value) { +    surface_source = static_cast<Source>(new_value); +    emit Update(); +} + +void GraphicsSurfaceWidget::OnSurfaceAddressChanged(qint64 new_value) { +    if (surface_address != new_value) { +        surface_address = static_cast<Tegra::GPUVAddr>(new_value); + +        surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); +        emit Update(); +    } +} + +void GraphicsSurfaceWidget::OnSurfaceWidthChanged(int new_value) { +    if (surface_width != static_cast<unsigned>(new_value)) { +        surface_width = static_cast<unsigned>(new_value); + +        surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); +        emit Update(); +    } +} + +void GraphicsSurfaceWidget::OnSurfaceHeightChanged(int new_value) { +    if (surface_height != static_cast<unsigned>(new_value)) { +        surface_height = static_cast<unsigned>(new_value); + +        surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); +        emit Update(); +    } +} + +void GraphicsSurfaceWidget::OnSurfaceFormatChanged(int new_value) { +    if (surface_format != static_cast<Tegra::Texture::TextureFormat>(new_value)) { +        surface_format = static_cast<Tegra::Texture::TextureFormat>(new_value); + +        surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); +        emit Update(); +    } +} + +void GraphicsSurfaceWidget::OnSurfacePickerXChanged(int new_value) { +    if (surface_picker_x != new_value) { +        surface_picker_x = new_value; +        Pick(surface_picker_x, surface_picker_y); +    } +} + +void GraphicsSurfaceWidget::OnSurfacePickerYChanged(int new_value) { +    if (surface_picker_y != new_value) { +        surface_picker_y = new_value; +        Pick(surface_picker_x, surface_picker_y); +    } +} + +void GraphicsSurfaceWidget::Pick(int x, int y) { +    surface_picker_x_control->setValue(x); +    surface_picker_y_control->setValue(y); + +    if (x < 0 || x >= static_cast<int>(surface_width) || y < 0 || +        y >= static_cast<int>(surface_height)) { +        surface_info_label->setText(tr("Pixel out of bounds")); +        surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); +        return; +    } + +    surface_info_label->setText(QString("Raw: <Unimplemented>\n(%1)").arg("<Unimplemented>")); +    surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); +} + +void GraphicsSurfaceWidget::OnUpdate() { +    auto& gpu = Core::System::GetInstance().GPU(); + +    QPixmap pixmap; + +    switch (surface_source) { +    case Source::RenderTarget0: +    case Source::RenderTarget1: +    case Source::RenderTarget2: +    case Source::RenderTarget3: +    case Source::RenderTarget4: +    case Source::RenderTarget5: +    case Source::RenderTarget6: +    case Source::RenderTarget7: { +        // TODO: Store a reference to the registers in the debug context instead of accessing them +        // directly... + +        auto& registers = gpu.Get3DEngine().regs; +        auto& rt = registers.rt[static_cast<size_t>(surface_source) - +                                static_cast<size_t>(Source::RenderTarget0)]; + +        surface_address = rt.Address(); +        surface_width = rt.horiz; +        surface_height = rt.vert; +        if (rt.format != 0) { +            surface_format = +                ConvertToTextureFormat(static_cast<Tegra::RenderTargetFormat>(rt.format)); +        } + +        break; +    } + +    case Source::Custom: { +        // Keep user-specified values +        break; +    } + +    default: +        qDebug() << "Unknown surface source " << static_cast<int>(surface_source); +        break; +    } + +    surface_address_control->SetValue(surface_address); +    surface_width_control->setValue(surface_width); +    surface_height_control->setValue(surface_height); +    surface_format_control->setCurrentIndex(static_cast<int>(surface_format)); + +    if (surface_address == 0) { +        surface_picture_label->hide(); +        surface_info_label->setText(tr("(invalid surface address)")); +        surface_info_label->setAlignment(Qt::AlignCenter); +        surface_picker_x_control->setEnabled(false); +        surface_picker_y_control->setEnabled(false); +        save_surface->setEnabled(false); +        return; +    } + +    // TODO: Implement a good way to visualize alpha components! + +    QImage decoded_image(surface_width, surface_height, QImage::Format_ARGB32); +    VAddr address = gpu.memory_manager->PhysicalToVirtualAddress(surface_address); + +    auto unswizzled_data = +        Tegra::Texture::UnswizzleTexture(address, surface_format, surface_width, surface_height); + +    auto texture_data = Tegra::Texture::DecodeTexture(unswizzled_data, surface_format, +                                                      surface_width, surface_height); + +    surface_picture_label->show(); + +    for (unsigned int y = 0; y < surface_height; ++y) { +        for (unsigned int x = 0; x < surface_width; ++x) { +            Math::Vec4<u8> color; +            color[0] = texture_data[x + y * surface_width + 0]; +            color[1] = texture_data[x + y * surface_width + 1]; +            color[2] = texture_data[x + y * surface_width + 2]; +            color[3] = texture_data[x + y * surface_width + 3]; +            decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), color.a())); +        } +    } + +    pixmap = QPixmap::fromImage(decoded_image); +    surface_picture_label->setPixmap(pixmap); +    surface_picture_label->resize(pixmap.size()); + +    // Update the info with pixel data +    surface_picker_x_control->setEnabled(true); +    surface_picker_y_control->setEnabled(true); +    Pick(surface_picker_x, surface_picker_y); + +    // Enable saving the converted pixmap to file +    save_surface->setEnabled(true); +} + +void GraphicsSurfaceWidget::SaveSurface() { +    QString png_filter = tr("Portable Network Graphic (*.png)"); +    QString bin_filter = tr("Binary data (*.bin)"); + +    QString selectedFilter; +    QString filename = QFileDialog::getSaveFileName( +        this, tr("Save Surface"), +        QString("texture-0x%1.png").arg(QString::number(surface_address, 16)), +        QString("%1;;%2").arg(png_filter, bin_filter), &selectedFilter); + +    if (filename.isEmpty()) { +        // If the user canceled the dialog, don't save anything. +        return; +    } + +    if (selectedFilter == png_filter) { +        const QPixmap* pixmap = surface_picture_label->pixmap(); +        ASSERT_MSG(pixmap != nullptr, "No pixmap set"); + +        QFile file(filename); +        file.open(QIODevice::WriteOnly); +        if (pixmap) +            pixmap->save(&file, "PNG"); +    } else if (selectedFilter == bin_filter) { +        auto& gpu = Core::System::GetInstance().GPU(); +        VAddr address = gpu.memory_manager->PhysicalToVirtualAddress(surface_address); + +        const u8* buffer = Memory::GetPointer(address); +        ASSERT_MSG(buffer != nullptr, "Memory not accessible"); + +        QFile file(filename); +        file.open(QIODevice::WriteOnly); +        int size = surface_width * surface_height * Tegra::Texture::BytesPerPixel(surface_format); +        QByteArray data(reinterpret_cast<const char*>(buffer), size); +        file.write(data); +    } else { +        UNREACHABLE_MSG("Unhandled filter selected"); +    } +} diff --git a/src/yuzu/debugger/graphics/graphics_surface.h b/src/yuzu/debugger/graphics/graphics_surface.h new file mode 100644 index 000000000..6a344bdfc --- /dev/null +++ b/src/yuzu/debugger/graphics/graphics_surface.h @@ -0,0 +1,97 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QLabel> +#include <QPushButton> +#include "video_core/memory_manager.h" +#include "video_core/textures/texture.h" +#include "yuzu/debugger/graphics/graphics_breakpoint_observer.h" + +class QComboBox; +class QSpinBox; +class CSpinBox; + +class GraphicsSurfaceWidget; + +class SurfacePicture : public QLabel { +    Q_OBJECT + +public: +    explicit SurfacePicture(QWidget* parent = nullptr, +                            GraphicsSurfaceWidget* surface_widget = nullptr); +    ~SurfacePicture(); + +protected slots: +    virtual void mouseMoveEvent(QMouseEvent* event); +    virtual void mousePressEvent(QMouseEvent* event); + +private: +    GraphicsSurfaceWidget* surface_widget; +}; + +class GraphicsSurfaceWidget : public BreakPointObserverDock { +    Q_OBJECT + +    using Event = Tegra::DebugContext::Event; + +    enum class Source { +        RenderTarget0 = 0, +        RenderTarget1 = 1, +        RenderTarget2 = 2, +        RenderTarget3 = 3, +        RenderTarget4 = 4, +        RenderTarget5 = 5, +        RenderTarget6 = 6, +        RenderTarget7 = 7, +        ZBuffer = 8, +        Custom = 9, +    }; + +public: +    explicit GraphicsSurfaceWidget(std::shared_ptr<Tegra::DebugContext> debug_context, +                                   QWidget* parent = nullptr); +    void Pick(int x, int y); + +public slots: +    void OnSurfaceSourceChanged(int new_value); +    void OnSurfaceAddressChanged(qint64 new_value); +    void OnSurfaceWidthChanged(int new_value); +    void OnSurfaceHeightChanged(int new_value); +    void OnSurfaceFormatChanged(int new_value); +    void OnSurfacePickerXChanged(int new_value); +    void OnSurfacePickerYChanged(int new_value); +    void OnUpdate(); + +private slots: +    void OnBreakPointHit(Tegra::DebugContext::Event event, void* data) override; +    void OnResumed() override; + +    void SaveSurface(); + +signals: +    void Update(); + +private: +    QComboBox* surface_source_list; +    CSpinBox* surface_address_control; +    QSpinBox* surface_width_control; +    QSpinBox* surface_height_control; +    QComboBox* surface_format_control; + +    SurfacePicture* surface_picture_label; +    QSpinBox* surface_picker_x_control; +    QSpinBox* surface_picker_y_control; +    QLabel* surface_info_label; +    QPushButton* save_surface; + +    Source surface_source; +    Tegra::GPUVAddr surface_address; +    unsigned surface_width; +    unsigned surface_height; +    Tegra::Texture::TextureFormat surface_format; +    int surface_picker_x = 0; +    int surface_picker_y = 0; +}; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index eb22a8ccf..bd323870b 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -25,10 +25,13 @@  #include "core/gdbstub/gdbstub.h"  #include "core/loader/loader.h"  #include "core/settings.h" +#include "video_core/debug_utils/debug_utils.h"  #include "yuzu/about_dialog.h"  #include "yuzu/bootmanager.h"  #include "yuzu/configuration/config.h"  #include "yuzu/configuration/configure_dialog.h" +#include "yuzu/debugger/graphics/graphics_breakpoints.h" +#include "yuzu/debugger/graphics/graphics_surface.h"  #include "yuzu/debugger/profiler.h"  #include "yuzu/debugger/registers.h"  #include "yuzu/debugger/wait_tree.h" @@ -68,6 +71,9 @@ static void ShowCalloutMessage(const QString& message, CalloutFlag flag) {  void GMainWindow::ShowCallouts() {}  GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { + +    debug_context = Tegra::DebugContext::Construct(); +      setAcceptDrops(true);      ui.setupUi(this);      statusBar()->hide(); @@ -160,6 +166,16 @@ void GMainWindow::InitializeDebugWidgets() {      connect(this, &GMainWindow::EmulationStopping, registersWidget,              &RegistersWidget::OnEmulationStopping); +    graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(debug_context, this); +    addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); +    graphicsBreakpointsWidget->hide(); +    debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + +    graphicsSurfaceWidget = new GraphicsSurfaceWidget(debug_context, this); +    addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceWidget); +    graphicsSurfaceWidget->hide(); +    debug_menu->addAction(graphicsSurfaceWidget->toggleViewAction()); +      waitTreeWidget = new WaitTreeWidget(this);      addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget);      waitTreeWidget->hide(); @@ -324,6 +340,8 @@ bool GMainWindow::LoadROM(const QString& filename) {      Core::System& system{Core::System::GetInstance()}; +    system.SetGPUDebugContext(debug_context); +      const Core::System::ResultStatus result{system.Load(render_window, filename.toStdString())};      Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "Qt"); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 4a0d912bb..2471caf83 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -15,17 +15,18 @@ class Config;  class EmuThread;  class GameList;  class GImageInfo; -class GPUCommandStreamWidget; -class GPUCommandListWidget;  class GraphicsBreakPointsWidget; -class GraphicsTracingWidget; -class GraphicsVertexShaderWidget; +class GraphicsSurfaceWidget;  class GRenderWindow;  class MicroProfileDialog;  class ProfilerWidget;  class RegistersWidget;  class WaitTreeWidget; +namespace Tegra { +class DebugContext; +} +  class GMainWindow : public QMainWindow {      Q_OBJECT @@ -138,6 +139,8 @@ private:      Ui::MainWindow ui; +    std::shared_ptr<Tegra::DebugContext> debug_context; +      GRenderWindow* render_window;      GameList* game_list; @@ -158,6 +161,8 @@ private:      ProfilerWidget* profilerWidget;      MicroProfileDialog* microProfileDialog;      RegistersWidget* registersWidget; +    GraphicsBreakPointsWidget* graphicsBreakpointsWidget; +    GraphicsSurfaceWidget* graphicsSurfaceWidget;      WaitTreeWidget* waitTreeWidget;      QAction* actions_recent_files[max_recent_files_item]; | 
