summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/core.h11
-rw-r--r--src/video_core/CMakeLists.txt5
-rw-r--r--src/video_core/debug_utils/debug_utils.cpp64
-rw-r--r--src/video_core/debug_utils/debug_utils.h163
-rw-r--r--src/video_core/engines/maxwell_3d.cpp69
-rw-r--r--src/video_core/engines/maxwell_3d.h16
-rw-r--r--src/video_core/gpu.cpp4
-rw-r--r--src/video_core/gpu.h9
-rw-r--r--src/video_core/textures/decoders.cpp105
-rw-r--r--src/video_core/textures/decoders.h26
-rw-r--r--src/video_core/textures/texture.h61
-rw-r--r--src/yuzu/CMakeLists.txt7
-rw-r--r--src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp27
-rw-r--r--src/yuzu/debugger/graphics/graphics_breakpoint_observer.h33
-rw-r--r--src/yuzu/debugger/graphics/graphics_breakpoints.cpp212
-rw-r--r--src/yuzu/debugger/graphics/graphics_breakpoints.h46
-rw-r--r--src/yuzu/debugger/graphics/graphics_breakpoints_p.h36
-rw-r--r--src/yuzu/debugger/graphics/graphics_surface.cpp452
-rw-r--r--src/yuzu/debugger/graphics/graphics_surface.h97
-rw-r--r--src/yuzu/main.cpp18
-rw-r--r--src/yuzu/main.h13
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];