Browse Source
Merge pull request #273 from Subv/textures
Merge pull request #273 from Subv/textures
GPU: Added code to unswizzle textures and ported the surface viewer from citrapull/15/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1464 additions and 10 deletions
-
11src/core/core.h
-
5src/video_core/CMakeLists.txt
-
64src/video_core/debug_utils/debug_utils.cpp
-
163src/video_core/debug_utils/debug_utils.h
-
69src/video_core/engines/maxwell_3d.cpp
-
16src/video_core/engines/maxwell_3d.h
-
4src/video_core/gpu.cpp
-
9src/video_core/gpu.h
-
105src/video_core/textures/decoders.cpp
-
26src/video_core/textures/decoders.h
-
61src/video_core/textures/texture.h
-
7src/yuzu/CMakeLists.txt
-
27src/yuzu/debugger/graphics/graphics_breakpoint_observer.cpp
-
33src/yuzu/debugger/graphics/graphics_breakpoint_observer.h
-
212src/yuzu/debugger/graphics/graphics_breakpoints.cpp
-
46src/yuzu/debugger/graphics/graphics_breakpoints.h
-
36src/yuzu/debugger/graphics/graphics_breakpoints_p.h
-
452src/yuzu/debugger/graphics/graphics_surface.cpp
-
97src/yuzu/debugger/graphics/graphics_surface.h
-
18src/yuzu/main.cpp
-
13src/yuzu/main.h
@ -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
|
|||
@ -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 |
|||
@ -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
|
|||
@ -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 |
|||
@ -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 |
|||
@ -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(); |
|||
} |
|||
@ -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); |
|||
}; |
|||
@ -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); |
|||
} |
|||
@ -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; |
|||
}; |
|||
@ -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; |
|||
}; |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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; |
|||
}; |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue