mirror of
				https://git.zaroz.cloud/nintendo-back-up/yuzu/yuzu.git
				synced 2025-05-12 00:45:25 +00:00 
			
		
		
		
	Merge pull request #218 from neobrain/pica_debugger
Pica debugger improvements
This commit is contained in:
		
						commit
						360f68419d
					
				| @ -8,9 +8,12 @@ set(SRCS | ||||
|             debugger/callstack.cpp | ||||
|             debugger/disassembler.cpp | ||||
|             debugger/graphics.cpp | ||||
|             debugger/graphics_breakpoints.cpp | ||||
|             debugger/graphics_cmdlists.cpp | ||||
|             debugger/graphics_framebuffer.cpp | ||||
|             debugger/ramview.cpp | ||||
|             debugger/registers.cpp | ||||
|             util/spinbox.cpp | ||||
|             bootmanager.cpp | ||||
|             hotkeys.cpp | ||||
|             main.cpp | ||||
| @ -23,9 +26,13 @@ set(HEADERS | ||||
|             debugger/callstack.hxx | ||||
|             debugger/disassembler.hxx | ||||
|             debugger/graphics.hxx | ||||
|             debugger/graphics_breakpoints.hxx | ||||
|             debugger/graphics_breakpoints_p.hxx | ||||
|             debugger/graphics_cmdlists.hxx | ||||
|             debugger/graphics_framebuffer.hxx | ||||
|             debugger/ramview.hxx | ||||
|             debugger/registers.hxx | ||||
|             util/spinbox.hxx | ||||
|             bootmanager.hxx | ||||
|             hotkeys.hxx | ||||
|             main.hxx | ||||
|  | ||||
| @ -14,6 +14,8 @@ | ||||
| #include "core/core.h" | ||||
| #include "core/settings.h" | ||||
| 
 | ||||
| #include "video_core/debug_utils/debug_utils.h" | ||||
| 
 | ||||
| #include "video_core/video_core.h" | ||||
| 
 | ||||
| #include "citra_qt/version.h" | ||||
| @ -65,14 +67,21 @@ void EmuThread::Stop() | ||||
|     } | ||||
|     stop_run = true; | ||||
| 
 | ||||
|     // Release emu threads from any breakpoints, so that this doesn't hang forever.
 | ||||
|     Pica::g_debug_context->ClearBreakpoints(); | ||||
| 
 | ||||
|     //core::g_state = core::SYS_DIE;
 | ||||
| 
 | ||||
|     wait(500); | ||||
|     // TODO: Waiting here is just a bad workaround for retarded shutdown logic.
 | ||||
|     wait(1000); | ||||
|     if (isRunning()) | ||||
|     { | ||||
|         WARN_LOG(MASTER_LOG, "EmuThread still running, terminating..."); | ||||
|         quit(); | ||||
|         wait(1000); | ||||
| 
 | ||||
|         // TODO: Waiting 50 seconds can be necessary if the logging subsystem has a lot of spam
 | ||||
|         // queued... This should be fixed.
 | ||||
|         wait(50000); | ||||
|         if (isRunning()) | ||||
|         { | ||||
|             WARN_LOG(MASTER_LOG, "EmuThread STILL running, something is wrong here..."); | ||||
|  | ||||
							
								
								
									
										261
									
								
								src/citra_qt/debugger/graphics_breakpoints.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								src/citra_qt/debugger/graphics_breakpoints.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,261 @@ | ||||
| // Copyright 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QMetaType> | ||||
| #include <QPushButton> | ||||
| #include <QTreeWidget> | ||||
| #include <QVBoxLayout> | ||||
| #include <QLabel> | ||||
| 
 | ||||
| #include "graphics_breakpoints.hxx" | ||||
| #include "graphics_breakpoints_p.hxx" | ||||
| 
 | ||||
| BreakPointModel::BreakPointModel(std::shared_ptr<Pica::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 2; | ||||
| } | ||||
| 
 | ||||
| int BreakPointModel::rowCount(const QModelIndex& parent) const | ||||
| { | ||||
|     return static_cast<int>(Pica::DebugContext::Event::NumEvents); | ||||
| } | ||||
| 
 | ||||
| QVariant BreakPointModel::data(const QModelIndex& index, int role) const | ||||
| { | ||||
|     const auto event = static_cast<Pica::DebugContext::Event>(index.row()); | ||||
| 
 | ||||
|     switch (role) { | ||||
|     case Qt::DisplayRole: | ||||
|     { | ||||
|         switch (index.column()) { | ||||
|         case 0: | ||||
|         { | ||||
|             std::map<Pica::DebugContext::Event, QString> map; | ||||
|             map.insert({Pica::DebugContext::Event::CommandLoaded, tr("Pica command loaded")}); | ||||
|             map.insert({Pica::DebugContext::Event::CommandProcessed, tr("Pica command processed")}); | ||||
|             map.insert({Pica::DebugContext::Event::IncomingPrimitiveBatch, tr("Incoming primitive batch")}); | ||||
|             map.insert({Pica::DebugContext::Event::FinishedPrimitiveBatch, tr("Finished primitive batch")}); | ||||
| 
 | ||||
|             _dbg_assert_(GUI, map.size() == static_cast<size_t>(Pica::DebugContext::Event::NumEvents)); | ||||
| 
 | ||||
|             return map[event]; | ||||
|         } | ||||
| 
 | ||||
|         case 1: | ||||
|             return data(index, Role_IsEnabled).toBool() ? tr("Enabled") : tr("Disabled"); | ||||
| 
 | ||||
|         default: | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         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[event].enabled; | ||||
|     } | ||||
| 
 | ||||
|     default: | ||||
|         break; | ||||
|     } | ||||
|     return QVariant(); | ||||
| } | ||||
| 
 | ||||
| QVariant BreakPointModel::headerData(int section, Qt::Orientation orientation, int role) const | ||||
| { | ||||
|     switch(role) { | ||||
|     case Qt::DisplayRole: | ||||
|     { | ||||
|         if (section == 0) { | ||||
|             return tr("Event"); | ||||
|         } else if (section == 1) { | ||||
|             return tr("Status"); | ||||
|         } | ||||
| 
 | ||||
|         break; | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     return QVariant(); | ||||
| } | ||||
| 
 | ||||
| bool BreakPointModel::setData(const QModelIndex& index, const QVariant& value, int role) | ||||
| { | ||||
|     const auto event = static_cast<Pica::DebugContext::Event>(index.row()); | ||||
| 
 | ||||
|     switch (role) { | ||||
|     case Role_IsEnabled: | ||||
|     { | ||||
|         auto context = context_weak.lock(); | ||||
|         if (!context) | ||||
|             return false; | ||||
| 
 | ||||
|         context->breakpoints[event].enabled = value.toBool(); | ||||
|         QModelIndex changed_index = createIndex(index.row(), 1); | ||||
|         emit dataChanged(changed_index, changed_index); | ||||
|         return true; | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| void BreakPointModel::OnBreakPointHit(Pica::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), 1)); | ||||
| } | ||||
| 
 | ||||
| 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), 1)); | ||||
|     active_breakpoint = context->active_breakpoint; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| GraphicsBreakPointsWidget::GraphicsBreakPointsWidget(std::shared_ptr<Pica::DebugContext> debug_context, | ||||
|                                                      QWidget* parent) | ||||
|     : QDockWidget(tr("Pica Breakpoints"), parent), | ||||
|       Pica::DebugContext::BreakPointObserver(debug_context) | ||||
| { | ||||
|     setObjectName("PicaBreakPointsWidget"); | ||||
| 
 | ||||
|     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->setModel(breakpoint_model); | ||||
| 
 | ||||
|     toggle_breakpoint_button = new QPushButton(tr("Enable")); | ||||
|     toggle_breakpoint_button->setEnabled(false); | ||||
| 
 | ||||
|     qRegisterMetaType<Pica::DebugContext::Event>("Pica::DebugContext::Event"); | ||||
| 
 | ||||
|     connect(resume_button, SIGNAL(clicked()), this, SLOT(OnResumeRequested())); | ||||
| 
 | ||||
|     connect(this, SIGNAL(BreakPointHit(Pica::DebugContext::Event,void*)), | ||||
|             this, SLOT(OnBreakPointHit(Pica::DebugContext::Event,void*)), | ||||
|             Qt::BlockingQueuedConnection); | ||||
|     connect(this, SIGNAL(Resumed()), this, SLOT(OnResumed())); | ||||
| 
 | ||||
|     connect(this, SIGNAL(BreakPointHit(Pica::DebugContext::Event,void*)), | ||||
|             breakpoint_model, SLOT(OnBreakPointHit(Pica::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&))); | ||||
| 
 | ||||
|     connect(breakpoint_list->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), | ||||
|             this, SLOT(OnBreakpointSelectionChanged(QModelIndex))); | ||||
| 
 | ||||
|     connect(toggle_breakpoint_button, SIGNAL(clicked()), this, SLOT(OnToggleBreakpointEnabled())); | ||||
| 
 | ||||
|     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_layout->addWidget(toggle_breakpoint_button); | ||||
|     main_widget->setLayout(main_layout); | ||||
| 
 | ||||
|     setWidget(main_widget); | ||||
| } | ||||
| 
 | ||||
| void GraphicsBreakPointsWidget::OnPicaBreakPointHit(Event event, void* data) | ||||
| { | ||||
|     // Process in GUI thread
 | ||||
|     emit BreakPointHit(event, data); | ||||
| } | ||||
| 
 | ||||
| void GraphicsBreakPointsWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) | ||||
| { | ||||
|     status_text->setText(tr("Emulation halted at breakpoint")); | ||||
|     resume_button->setEnabled(true); | ||||
| } | ||||
| 
 | ||||
| void GraphicsBreakPointsWidget::OnPicaResume() | ||||
| { | ||||
|     // 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::OnBreakpointSelectionChanged(const QModelIndex& index) | ||||
| { | ||||
|     if (!index.isValid()) { | ||||
|         toggle_breakpoint_button->setEnabled(false); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     toggle_breakpoint_button->setEnabled(true); | ||||
|     UpdateToggleBreakpointButton(index); | ||||
| } | ||||
| 
 | ||||
| void GraphicsBreakPointsWidget::OnToggleBreakpointEnabled() | ||||
| { | ||||
|     QModelIndex index = breakpoint_list->selectionModel()->currentIndex(); | ||||
|     bool new_state = !(breakpoint_model->data(index, BreakPointModel::Role_IsEnabled).toBool()); | ||||
| 
 | ||||
|     breakpoint_model->setData(index, new_state, | ||||
|                               BreakPointModel::Role_IsEnabled); | ||||
|     UpdateToggleBreakpointButton(index); | ||||
| } | ||||
| 
 | ||||
| void GraphicsBreakPointsWidget::UpdateToggleBreakpointButton(const QModelIndex& index) | ||||
| { | ||||
|     if (true == breakpoint_model->data(index, BreakPointModel::Role_IsEnabled).toBool()) { | ||||
|         toggle_breakpoint_button->setText(tr("Disable")); | ||||
|     } else { | ||||
|         toggle_breakpoint_button->setText(tr("Enable")); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										53
									
								
								src/citra_qt/debugger/graphics_breakpoints.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/citra_qt/debugger/graphics_breakpoints.hxx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| // Copyright 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <memory> | ||||
| 
 | ||||
| #include <QAbstractListModel> | ||||
| #include <QDockWidget> | ||||
| 
 | ||||
| #include "video_core/debug_utils/debug_utils.h" | ||||
| 
 | ||||
| class QLabel; | ||||
| class QPushButton; | ||||
| class QTreeView; | ||||
| 
 | ||||
| class BreakPointModel; | ||||
| 
 | ||||
| class GraphicsBreakPointsWidget : public QDockWidget, Pica::DebugContext::BreakPointObserver { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
|     using Event = Pica::DebugContext::Event; | ||||
| 
 | ||||
| public: | ||||
|     GraphicsBreakPointsWidget(std::shared_ptr<Pica::DebugContext> debug_context, | ||||
|                               QWidget* parent = nullptr); | ||||
| 
 | ||||
|     void OnPicaBreakPointHit(Pica::DebugContext::Event event, void* data) override; | ||||
|     void OnPicaResume() override; | ||||
| 
 | ||||
| public slots: | ||||
|     void OnBreakPointHit(Pica::DebugContext::Event event, void* data); | ||||
|     void OnResumeRequested(); | ||||
|     void OnResumed(); | ||||
|     void OnBreakpointSelectionChanged(const QModelIndex&); | ||||
|     void OnToggleBreakpointEnabled(); | ||||
| 
 | ||||
| signals: | ||||
|     void Resumed(); | ||||
|     void BreakPointHit(Pica::DebugContext::Event event, void* data); | ||||
|     void BreakPointsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); | ||||
| 
 | ||||
| private: | ||||
|     void UpdateToggleBreakpointButton(const QModelIndex& index); | ||||
| 
 | ||||
|     QLabel* status_text; | ||||
|     QPushButton* resume_button; | ||||
|     QPushButton* toggle_breakpoint_button; | ||||
| 
 | ||||
|     BreakPointModel* breakpoint_model; | ||||
|     QTreeView* breakpoint_list; | ||||
| }; | ||||
							
								
								
									
										38
									
								
								src/citra_qt/debugger/graphics_breakpoints_p.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/citra_qt/debugger/graphics_breakpoints_p.hxx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| // Copyright 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2
 | ||||
| // 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<Pica::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; | ||||
|     QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; | ||||
| 
 | ||||
|     bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); | ||||
| 
 | ||||
| public slots: | ||||
|     void OnBreakPointHit(Pica::DebugContext::Event event); | ||||
|     void OnResumed(); | ||||
| 
 | ||||
| private: | ||||
|     std::weak_ptr<Pica::DebugContext> context_weak; | ||||
|     bool at_breakpoint; | ||||
|     Pica::DebugContext::Event active_breakpoint; | ||||
| }; | ||||
| @ -2,30 +2,171 @@ | ||||
| // Licensed under GPLv2
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QLabel> | ||||
| #include <QListView> | ||||
| #include <QMainWindow> | ||||
| #include <QPushButton> | ||||
| #include <QVBoxLayout> | ||||
| #include <QTreeView> | ||||
| #include <QSpinBox> | ||||
| #include <QComboBox> | ||||
| 
 | ||||
| #include "video_core/pica.h" | ||||
| #include "video_core/math.h" | ||||
| 
 | ||||
| #include "video_core/debug_utils/debug_utils.h" | ||||
| 
 | ||||
| #include "graphics_cmdlists.hxx" | ||||
| 
 | ||||
| GPUCommandListModel::GPUCommandListModel(QObject* parent) : QAbstractListModel(parent) | ||||
| { | ||||
| #include "util/spinbox.hxx" | ||||
| 
 | ||||
| QImage LoadTexture(u8* src, const Pica::DebugUtils::TextureInfo& info) { | ||||
|     QImage decoded_image(info.width, info.height, QImage::Format_ARGB32); | ||||
|     for (int y = 0; y < info.height; ++y) { | ||||
|         for (int x = 0; x < info.width; ++x) { | ||||
|             Math::Vec4<u8> color = Pica::DebugUtils::LookupTexture(src, x, y, info); | ||||
|             decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), color.a())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return decoded_image; | ||||
| } | ||||
| 
 | ||||
| class TextureInfoWidget : public QWidget { | ||||
| public: | ||||
|     TextureInfoWidget(u8* src, const Pica::DebugUtils::TextureInfo& info, QWidget* parent = nullptr) : QWidget(parent) { | ||||
|         QLabel* image_widget = new QLabel; | ||||
|         QPixmap image_pixmap = QPixmap::fromImage(LoadTexture(src, info)); | ||||
|         image_pixmap = image_pixmap.scaled(200, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation); | ||||
|         image_widget->setPixmap(image_pixmap); | ||||
| 
 | ||||
|         QVBoxLayout* layout = new QVBoxLayout; | ||||
|         layout->addWidget(image_widget); | ||||
|         setLayout(layout); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| TextureInfoDockWidget::TextureInfoDockWidget(const Pica::DebugUtils::TextureInfo& info, QWidget* parent) | ||||
|     : QDockWidget(tr("Texture 0x%1").arg(info.address, 8, 16, QLatin1Char('0'))), | ||||
|       info(info) { | ||||
| 
 | ||||
|     QWidget* main_widget = new QWidget; | ||||
| 
 | ||||
|     QLabel* image_widget = new QLabel; | ||||
| 
 | ||||
|     connect(this, SIGNAL(UpdatePixmap(const QPixmap&)), image_widget, SLOT(setPixmap(const QPixmap&))); | ||||
| 
 | ||||
|     CSpinBox* phys_address_spinbox = new CSpinBox; | ||||
|     phys_address_spinbox->SetBase(16); | ||||
|     phys_address_spinbox->SetRange(0, 0xFFFFFFFF); | ||||
|     phys_address_spinbox->SetPrefix("0x"); | ||||
|     phys_address_spinbox->SetValue(info.address); | ||||
|     connect(phys_address_spinbox, SIGNAL(ValueChanged(qint64)), this, SLOT(OnAddressChanged(qint64))); | ||||
| 
 | ||||
|     QComboBox* format_choice = new QComboBox; | ||||
|     format_choice->addItem(tr("RGBA8")); | ||||
|     format_choice->addItem(tr("RGB8")); | ||||
|     format_choice->addItem(tr("RGBA5551")); | ||||
|     format_choice->addItem(tr("RGB565")); | ||||
|     format_choice->addItem(tr("RGBA4")); | ||||
|     format_choice->setCurrentIndex(static_cast<int>(info.format)); | ||||
|     connect(format_choice, SIGNAL(currentIndexChanged(int)), this, SLOT(OnFormatChanged(int))); | ||||
| 
 | ||||
|     QSpinBox* width_spinbox = new QSpinBox; | ||||
|     width_spinbox->setMaximum(65535); | ||||
|     width_spinbox->setValue(info.width); | ||||
|     connect(width_spinbox, SIGNAL(valueChanged(int)), this, SLOT(OnWidthChanged(int))); | ||||
| 
 | ||||
|     QSpinBox* height_spinbox = new QSpinBox; | ||||
|     height_spinbox->setMaximum(65535); | ||||
|     height_spinbox->setValue(info.height); | ||||
|     connect(height_spinbox, SIGNAL(valueChanged(int)), this, SLOT(OnHeightChanged(int))); | ||||
| 
 | ||||
|     QSpinBox* stride_spinbox = new QSpinBox; | ||||
|     stride_spinbox->setMaximum(65535 * 4); | ||||
|     stride_spinbox->setValue(info.stride); | ||||
|     connect(stride_spinbox, SIGNAL(valueChanged(int)), this, SLOT(OnStrideChanged(int))); | ||||
| 
 | ||||
|     QVBoxLayout* main_layout = new QVBoxLayout; | ||||
|     main_layout->addWidget(image_widget); | ||||
| 
 | ||||
|     { | ||||
|         QHBoxLayout* sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Source Address:"))); | ||||
|         sub_layout->addWidget(phys_address_spinbox); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
| 
 | ||||
|     { | ||||
|         QHBoxLayout* sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Format"))); | ||||
|         sub_layout->addWidget(format_choice); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
| 
 | ||||
|     { | ||||
|         QHBoxLayout* sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Width:"))); | ||||
|         sub_layout->addWidget(width_spinbox); | ||||
|         sub_layout->addStretch(); | ||||
|         sub_layout->addWidget(new QLabel(tr("Height:"))); | ||||
|         sub_layout->addWidget(height_spinbox); | ||||
|         sub_layout->addStretch(); | ||||
|         sub_layout->addWidget(new QLabel(tr("Stride:"))); | ||||
|         sub_layout->addWidget(stride_spinbox); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
| 
 | ||||
|     main_widget->setLayout(main_layout); | ||||
| 
 | ||||
|     emit UpdatePixmap(ReloadPixmap()); | ||||
| 
 | ||||
|     setWidget(main_widget); | ||||
| } | ||||
| 
 | ||||
| void TextureInfoDockWidget::OnAddressChanged(qint64 value) { | ||||
|     info.address = value; | ||||
|     emit UpdatePixmap(ReloadPixmap()); | ||||
| } | ||||
| 
 | ||||
| void TextureInfoDockWidget::OnFormatChanged(int value) { | ||||
|     info.format = static_cast<Pica::Regs::TextureFormat>(value); | ||||
|     emit UpdatePixmap(ReloadPixmap()); | ||||
| } | ||||
| 
 | ||||
| void TextureInfoDockWidget::OnWidthChanged(int value) { | ||||
|     info.width = value; | ||||
|     emit UpdatePixmap(ReloadPixmap()); | ||||
| } | ||||
| 
 | ||||
| void TextureInfoDockWidget::OnHeightChanged(int value) { | ||||
|     info.height = value; | ||||
|     emit UpdatePixmap(ReloadPixmap()); | ||||
| } | ||||
| 
 | ||||
| void TextureInfoDockWidget::OnStrideChanged(int value) { | ||||
|     info.stride = value; | ||||
|     emit UpdatePixmap(ReloadPixmap()); | ||||
| } | ||||
| 
 | ||||
| QPixmap TextureInfoDockWidget::ReloadPixmap() const { | ||||
|     u8* src = Memory::GetPointer(info.address); | ||||
|     return QPixmap::fromImage(LoadTexture(src, info)); | ||||
| } | ||||
| 
 | ||||
| GPUCommandListModel::GPUCommandListModel(QObject* parent) : QAbstractListModel(parent) { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| int GPUCommandListModel::rowCount(const QModelIndex& parent) const | ||||
| { | ||||
| int GPUCommandListModel::rowCount(const QModelIndex& parent) const { | ||||
|     return pica_trace.writes.size(); | ||||
| } | ||||
| 
 | ||||
| int GPUCommandListModel::columnCount(const QModelIndex& parent) const | ||||
| { | ||||
| int GPUCommandListModel::columnCount(const QModelIndex& parent) const { | ||||
|     return 2; | ||||
| } | ||||
| 
 | ||||
| QVariant GPUCommandListModel::data(const QModelIndex& index, int role) const | ||||
| { | ||||
| QVariant GPUCommandListModel::data(const QModelIndex& index, int role) const { | ||||
|     if (!index.isValid()) | ||||
|         return QVariant(); | ||||
| 
 | ||||
| @ -36,21 +177,39 @@ QVariant GPUCommandListModel::data(const QModelIndex& index, int role) const | ||||
|     if (role == Qt::DisplayRole) { | ||||
|         QString content; | ||||
|         if (index.column() == 0) { | ||||
|             content = QString::fromLatin1(Pica::Regs::GetCommandName(cmd.cmd_id).c_str()); | ||||
|             QString content = QString::fromLatin1(Pica::Regs::GetCommandName(cmd.cmd_id).c_str()); | ||||
|             content.append(" "); | ||||
|             return content; | ||||
|         } else if (index.column() == 1) { | ||||
|             content.append(QString("%1 ").arg(cmd.hex, 8, 16, QLatin1Char('0'))); | ||||
|             QString content = QString("%1 ").arg(cmd.hex, 8, 16, QLatin1Char('0')); | ||||
|             content.append(QString("%1 ").arg(val, 8, 16, QLatin1Char('0'))); | ||||
|             return content; | ||||
|         } | ||||
| 
 | ||||
|         return QVariant(content); | ||||
|     } else if (role == CommandIdRole) { | ||||
|         return QVariant::fromValue<int>(cmd.cmd_id.Value()); | ||||
|     } | ||||
| 
 | ||||
|     return QVariant(); | ||||
| } | ||||
| 
 | ||||
| void GPUCommandListModel::OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace& trace) | ||||
| { | ||||
| QVariant GPUCommandListModel::headerData(int section, Qt::Orientation orientation, int role) const { | ||||
|     switch(role) { | ||||
|     case Qt::DisplayRole: | ||||
|     { | ||||
|         if (section == 0) { | ||||
|             return tr("Command Name"); | ||||
|         } else if (section == 1) { | ||||
|             return tr("Data"); | ||||
|         } | ||||
| 
 | ||||
|         break; | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     return QVariant(); | ||||
| } | ||||
| 
 | ||||
| void GPUCommandListModel::OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace& trace) { | ||||
|     beginResetModel(); | ||||
| 
 | ||||
|     pica_trace = trace; | ||||
| @ -58,38 +217,82 @@ void GPUCommandListModel::OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace& | ||||
|     endResetModel(); | ||||
| } | ||||
| 
 | ||||
| #define COMMAND_IN_RANGE(cmd_id, reg_name)   \ | ||||
|     (cmd_id >= PICA_REG_INDEX(reg_name) &&   \ | ||||
|      cmd_id < PICA_REG_INDEX(reg_name) + sizeof(decltype(Pica::registers.reg_name)) / 4) | ||||
| 
 | ||||
| GPUCommandListWidget::GPUCommandListWidget(QWidget* parent) : QDockWidget(tr("Pica Command List"), parent) | ||||
| { | ||||
| void GPUCommandListWidget::OnCommandDoubleClicked(const QModelIndex& index) { | ||||
|     const int command_id = list_widget->model()->data(index, GPUCommandListModel::CommandIdRole).toInt(); | ||||
|     if (COMMAND_IN_RANGE(command_id, texture0)) { | ||||
|         auto info = Pica::DebugUtils::TextureInfo::FromPicaRegister(Pica::registers.texture0, | ||||
|                                                                     Pica::registers.texture0_format); | ||||
| 
 | ||||
|         // TODO: Instead, emit a signal here to be caught by the main window widget.
 | ||||
|         auto main_window = static_cast<QMainWindow*>(parent()); | ||||
|         main_window->tabifyDockWidget(this, new TextureInfoDockWidget(info, main_window)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GPUCommandListWidget::SetCommandInfo(const QModelIndex& index) { | ||||
|     QWidget* new_info_widget; | ||||
| 
 | ||||
|     const int command_id = list_widget->model()->data(index, GPUCommandListModel::CommandIdRole).toInt(); | ||||
|     if (COMMAND_IN_RANGE(command_id, texture0)) { | ||||
|         u8* src = Memory::GetPointer(Pica::registers.texture0.GetPhysicalAddress()); | ||||
|         auto info = Pica::DebugUtils::TextureInfo::FromPicaRegister(Pica::registers.texture0, | ||||
|                                                                     Pica::registers.texture0_format); | ||||
|         new_info_widget = new TextureInfoWidget(src, info); | ||||
|     } else { | ||||
|         new_info_widget = new QWidget; | ||||
|     } | ||||
| 
 | ||||
|     widget()->layout()->removeWidget(command_info_widget); | ||||
|     delete command_info_widget; | ||||
|     widget()->layout()->addWidget(new_info_widget); | ||||
|     command_info_widget = new_info_widget; | ||||
| } | ||||
| #undef COMMAND_IN_RANGE | ||||
| 
 | ||||
| GPUCommandListWidget::GPUCommandListWidget(QWidget* parent) : QDockWidget(tr("Pica Command List"), parent) { | ||||
|     setObjectName("Pica Command List"); | ||||
|     GPUCommandListModel* model = new GPUCommandListModel(this); | ||||
| 
 | ||||
|     QWidget* main_widget = new QWidget; | ||||
| 
 | ||||
|     QTreeView* list_widget = new QTreeView; | ||||
|     list_widget = new QTreeView; | ||||
|     list_widget->setModel(model); | ||||
|     list_widget->setFont(QFont("monospace")); | ||||
|     list_widget->setRootIsDecorated(false); | ||||
| 
 | ||||
|     QPushButton* toggle_tracing = new QPushButton(tr("Start Tracing")); | ||||
|     connect(list_widget->selectionModel(), SIGNAL(currentChanged(const QModelIndex&,const QModelIndex&)), | ||||
|             this, SLOT(SetCommandInfo(const QModelIndex&))); | ||||
|     connect(list_widget, SIGNAL(doubleClicked(const QModelIndex&)), | ||||
|             this, SLOT(OnCommandDoubleClicked(const QModelIndex&))); | ||||
| 
 | ||||
|     toggle_tracing = new QPushButton(tr("Start Tracing")); | ||||
| 
 | ||||
|     connect(toggle_tracing, SIGNAL(clicked()), this, SLOT(OnToggleTracing())); | ||||
|     connect(this, SIGNAL(TracingFinished(const Pica::DebugUtils::PicaTrace&)), | ||||
|             model, SLOT(OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace&))); | ||||
| 
 | ||||
|     command_info_widget = new QWidget; | ||||
| 
 | ||||
|     QVBoxLayout* main_layout = new QVBoxLayout; | ||||
|     main_layout->addWidget(list_widget); | ||||
|     main_layout->addWidget(toggle_tracing); | ||||
|     main_layout->addWidget(command_info_widget); | ||||
|     main_widget->setLayout(main_layout); | ||||
| 
 | ||||
|     setWidget(main_widget); | ||||
| } | ||||
| 
 | ||||
| void GPUCommandListWidget::OnToggleTracing() | ||||
| { | ||||
| void GPUCommandListWidget::OnToggleTracing() { | ||||
|     if (!Pica::DebugUtils::IsPicaTracing()) { | ||||
|         Pica::DebugUtils::StartPicaTracing(); | ||||
|         toggle_tracing->setText(tr("Finish Tracing")); | ||||
|     } else { | ||||
|         pica_trace = Pica::DebugUtils::FinishPicaTracing(); | ||||
|         emit TracingFinished(*pica_trace); | ||||
|         toggle_tracing->setText(tr("Start Tracing")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,16 +10,24 @@ | ||||
| #include "video_core/gpu_debugger.h" | ||||
| #include "video_core/debug_utils/debug_utils.h" | ||||
| 
 | ||||
| class QPushButton; | ||||
| class QTreeView; | ||||
| 
 | ||||
| class GPUCommandListModel : public QAbstractListModel | ||||
| { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|     enum { | ||||
|         CommandIdRole = Qt::UserRole, | ||||
|     }; | ||||
| 
 | ||||
|     GPUCommandListModel(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; | ||||
|     QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; | ||||
| 
 | ||||
| public slots: | ||||
|     void OnPicaTraceFinished(const Pica::DebugUtils::PicaTrace& trace); | ||||
| @ -37,10 +45,39 @@ public: | ||||
| 
 | ||||
| public slots: | ||||
|     void OnToggleTracing(); | ||||
|     void OnCommandDoubleClicked(const QModelIndex&); | ||||
| 
 | ||||
|     void SetCommandInfo(const QModelIndex&); | ||||
| 
 | ||||
| signals: | ||||
|     void TracingFinished(const Pica::DebugUtils::PicaTrace&); | ||||
| 
 | ||||
| private: | ||||
|     std::unique_ptr<Pica::DebugUtils::PicaTrace> pica_trace; | ||||
| 
 | ||||
|     QTreeView* list_widget; | ||||
|     QWidget* command_info_widget; | ||||
|     QPushButton* toggle_tracing; | ||||
| }; | ||||
| 
 | ||||
| class TextureInfoDockWidget : public QDockWidget { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|     TextureInfoDockWidget(const Pica::DebugUtils::TextureInfo& info, QWidget* parent = nullptr); | ||||
| 
 | ||||
| signals: | ||||
|     void UpdatePixmap(const QPixmap& pixmap); | ||||
| 
 | ||||
| private slots: | ||||
|     void OnAddressChanged(qint64 value); | ||||
|     void OnFormatChanged(int value); | ||||
|     void OnWidthChanged(int value); | ||||
|     void OnHeightChanged(int value); | ||||
|     void OnStrideChanged(int value); | ||||
| 
 | ||||
| private: | ||||
|     QPixmap ReloadPixmap() const; | ||||
| 
 | ||||
|     Pica::DebugUtils::TextureInfo info; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										282
									
								
								src/citra_qt/debugger/graphics_framebuffer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								src/citra_qt/debugger/graphics_framebuffer.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,282 @@ | ||||
| // Copyright 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QBoxLayout> | ||||
| #include <QComboBox> | ||||
| #include <QDebug> | ||||
| #include <QLabel> | ||||
| #include <QMetaType> | ||||
| #include <QPushButton> | ||||
| #include <QSpinBox> | ||||
| 
 | ||||
| #include "video_core/pica.h" | ||||
| 
 | ||||
| #include "graphics_framebuffer.hxx" | ||||
| 
 | ||||
| #include "util/spinbox.hxx" | ||||
| 
 | ||||
| BreakPointObserverDock::BreakPointObserverDock(std::shared_ptr<Pica::DebugContext> debug_context, | ||||
|                                                const QString& title, QWidget* parent) | ||||
|     : QDockWidget(title, parent), BreakPointObserver(debug_context) | ||||
| { | ||||
|     qRegisterMetaType<Pica::DebugContext::Event>("Pica::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(Pica::DebugContext::Event,void*)), | ||||
|             this, SLOT(OnBreakPointHit(Pica::DebugContext::Event,void*)), | ||||
|             Qt::BlockingQueuedConnection); | ||||
| } | ||||
| 
 | ||||
| void BreakPointObserverDock::OnPicaBreakPointHit(Pica::DebugContext::Event event, void* data) | ||||
| { | ||||
|     emit BreakPointHit(event, data); | ||||
| } | ||||
| 
 | ||||
| void BreakPointObserverDock::OnPicaResume() | ||||
| { | ||||
|     emit Resumed(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| GraphicsFramebufferWidget::GraphicsFramebufferWidget(std::shared_ptr<Pica::DebugContext> debug_context, | ||||
|                                                      QWidget* parent) | ||||
|     : BreakPointObserverDock(debug_context, tr("Pica Framebuffer"), parent), | ||||
|       framebuffer_source(Source::PicaTarget) | ||||
| { | ||||
|     setObjectName("PicaFramebuffer"); | ||||
| 
 | ||||
|     framebuffer_source_list = new QComboBox; | ||||
|     framebuffer_source_list->addItem(tr("Active Render Target")); | ||||
|     framebuffer_source_list->addItem(tr("Custom")); | ||||
|     framebuffer_source_list->setCurrentIndex(static_cast<int>(framebuffer_source)); | ||||
| 
 | ||||
|     framebuffer_address_control = new CSpinBox; | ||||
|     framebuffer_address_control->SetBase(16); | ||||
|     framebuffer_address_control->SetRange(0, 0xFFFFFFFF); | ||||
|     framebuffer_address_control->SetPrefix("0x"); | ||||
| 
 | ||||
|     framebuffer_width_control = new QSpinBox; | ||||
|     framebuffer_width_control->setMinimum(1); | ||||
|     framebuffer_width_control->setMaximum(std::numeric_limits<int>::max()); // TODO: Find actual maximum
 | ||||
| 
 | ||||
|     framebuffer_height_control = new QSpinBox; | ||||
|     framebuffer_height_control->setMinimum(1); | ||||
|     framebuffer_height_control->setMaximum(std::numeric_limits<int>::max()); // TODO: Find actual maximum
 | ||||
| 
 | ||||
|     framebuffer_format_control = new QComboBox; | ||||
|     framebuffer_format_control->addItem(tr("RGBA8")); | ||||
|     framebuffer_format_control->addItem(tr("RGB8")); | ||||
|     framebuffer_format_control->addItem(tr("RGBA5551")); | ||||
|     framebuffer_format_control->addItem(tr("RGB565")); | ||||
|     framebuffer_format_control->addItem(tr("RGBA4")); | ||||
| 
 | ||||
|     // TODO: This QLabel should shrink the image to the available space rather than just expanding...
 | ||||
|     framebuffer_picture_label = new QLabel; | ||||
| 
 | ||||
|     auto enlarge_button = new QPushButton(tr("Enlarge")); | ||||
| 
 | ||||
|     // Connections
 | ||||
|     connect(this, SIGNAL(Update()), this, SLOT(OnUpdate())); | ||||
|     connect(framebuffer_source_list, SIGNAL(currentIndexChanged(int)), this, SLOT(OnFramebufferSourceChanged(int))); | ||||
|     connect(framebuffer_address_control, SIGNAL(ValueChanged(qint64)), this, SLOT(OnFramebufferAddressChanged(qint64))); | ||||
|     connect(framebuffer_width_control, SIGNAL(valueChanged(int)), this, SLOT(OnFramebufferWidthChanged(int))); | ||||
|     connect(framebuffer_height_control, SIGNAL(valueChanged(int)), this, SLOT(OnFramebufferHeightChanged(int))); | ||||
|     connect(framebuffer_format_control, SIGNAL(currentIndexChanged(int)), this, SLOT(OnFramebufferFormatChanged(int))); | ||||
| 
 | ||||
|     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(framebuffer_source_list); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
|     { | ||||
|         auto sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Virtual Address:"))); | ||||
|         sub_layout->addWidget(framebuffer_address_control); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
|     { | ||||
|         auto sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Width:"))); | ||||
|         sub_layout->addWidget(framebuffer_width_control); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
|     { | ||||
|         auto sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Height:"))); | ||||
|         sub_layout->addWidget(framebuffer_height_control); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
|     { | ||||
|         auto sub_layout = new QHBoxLayout; | ||||
|         sub_layout->addWidget(new QLabel(tr("Format:"))); | ||||
|         sub_layout->addWidget(framebuffer_format_control); | ||||
|         main_layout->addLayout(sub_layout); | ||||
|     } | ||||
|     main_layout->addWidget(framebuffer_picture_label); | ||||
|     main_layout->addWidget(enlarge_button); | ||||
|     main_widget->setLayout(main_layout); | ||||
|     setWidget(main_widget); | ||||
| 
 | ||||
|     // Load current data - TODO: Make sure this works when emulation is not running
 | ||||
|     emit Update(); | ||||
|     widget()->setEnabled(false); // TODO: Only enable if currently at breakpoint
 | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) | ||||
| { | ||||
|     emit Update(); | ||||
|     widget()->setEnabled(true); | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnResumed() | ||||
| { | ||||
|     widget()->setEnabled(false); | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnFramebufferSourceChanged(int new_value) | ||||
| { | ||||
|     framebuffer_source = static_cast<Source>(new_value); | ||||
|     emit Update(); | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnFramebufferAddressChanged(qint64 new_value) | ||||
| { | ||||
|     if (framebuffer_address != new_value) { | ||||
|         framebuffer_address = static_cast<unsigned>(new_value); | ||||
| 
 | ||||
|         framebuffer_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); | ||||
|         emit Update(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnFramebufferWidthChanged(int new_value) | ||||
| { | ||||
|     if (framebuffer_width != new_value) { | ||||
|         framebuffer_width = new_value; | ||||
| 
 | ||||
|         framebuffer_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); | ||||
|         emit Update(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnFramebufferHeightChanged(int new_value) | ||||
| { | ||||
|     if (framebuffer_height != new_value) { | ||||
|         framebuffer_height = new_value; | ||||
| 
 | ||||
|         framebuffer_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); | ||||
|         emit Update(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnFramebufferFormatChanged(int new_value) | ||||
| { | ||||
|     if (framebuffer_format != static_cast<Format>(new_value)) { | ||||
|         framebuffer_format = static_cast<Format>(new_value); | ||||
| 
 | ||||
|         framebuffer_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); | ||||
|         emit Update(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GraphicsFramebufferWidget::OnUpdate() | ||||
| { | ||||
|     QPixmap pixmap; | ||||
| 
 | ||||
|     switch (framebuffer_source) { | ||||
|     case Source::PicaTarget: | ||||
|     { | ||||
|         // TODO: Store a reference to the registers in the debug context instead of accessing them directly...
 | ||||
| 
 | ||||
|         auto framebuffer = Pica::registers.framebuffer; | ||||
|         using Framebuffer = decltype(framebuffer); | ||||
| 
 | ||||
|         framebuffer_address = framebuffer.GetColorBufferAddress(); | ||||
|         framebuffer_width = framebuffer.GetWidth(); | ||||
|         framebuffer_height = framebuffer.GetHeight(); | ||||
|         framebuffer_format = static_cast<Format>(framebuffer.color_format); | ||||
| 
 | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     case Source::Custom: | ||||
|     { | ||||
|         // Keep user-specified values
 | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     default: | ||||
|         qDebug() << "Unknown framebuffer source " << static_cast<int>(framebuffer_source); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     // TODO: Implement a good way to visualize alpha components!
 | ||||
|     // TODO: Unify this decoding code with the texture decoder
 | ||||
|     switch (framebuffer_format) { | ||||
|     case Format::RGBA8: | ||||
|     { | ||||
|         QImage decoded_image(framebuffer_width, framebuffer_height, QImage::Format_ARGB32); | ||||
|         u32* color_buffer = (u32*)Memory::GetPointer(framebuffer_address); | ||||
|         for (int y = 0; y < framebuffer_height; ++y) { | ||||
|             for (int x = 0; x < framebuffer_width; ++x) { | ||||
|                 u32 value = *(color_buffer + x + y * framebuffer_width); | ||||
| 
 | ||||
|                 decoded_image.setPixel(x, y, qRgba((value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF, 255/*value >> 24*/)); | ||||
|             } | ||||
|         } | ||||
|         pixmap = QPixmap::fromImage(decoded_image); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     case Format::RGB8: | ||||
|     { | ||||
|         QImage decoded_image(framebuffer_width, framebuffer_height, QImage::Format_ARGB32); | ||||
|         u8* color_buffer = Memory::GetPointer(framebuffer_address); | ||||
|         for (int y = 0; y < framebuffer_height; ++y) { | ||||
|             for (int x = 0; x < framebuffer_width; ++x) { | ||||
|                 u8* pixel_pointer = color_buffer + x * 3 + y * 3 * framebuffer_width; | ||||
| 
 | ||||
|                 decoded_image.setPixel(x, y, qRgba(pixel_pointer[0], pixel_pointer[1], pixel_pointer[2], 255/*value >> 24*/)); | ||||
|             } | ||||
|         } | ||||
|         pixmap = QPixmap::fromImage(decoded_image); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     case Format::RGBA5551: | ||||
|     { | ||||
|         QImage decoded_image(framebuffer_width, framebuffer_height, QImage::Format_ARGB32); | ||||
|         u32* color_buffer = (u32*)Memory::GetPointer(framebuffer_address); | ||||
|         for (int y = 0; y < framebuffer_height; ++y) { | ||||
|             for (int x = 0; x < framebuffer_width; ++x) { | ||||
|                 u16 value = *(u16*)(((u8*)color_buffer) + x * 2 + y * framebuffer_width * 2); | ||||
|                 u8 r = (value >> 11) & 0x1F; | ||||
|                 u8 g = (value >> 6) & 0x1F; | ||||
|                 u8 b = (value >> 1) & 0x1F; | ||||
|                 u8 a = value & 1; | ||||
| 
 | ||||
|                 decoded_image.setPixel(x, y, qRgba(r, g, b, 255/*a*/)); | ||||
|             } | ||||
|         } | ||||
|         pixmap = QPixmap::fromImage(decoded_image); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     default: | ||||
|         qDebug() << "Unknown fb color format " << static_cast<int>(framebuffer_format); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     framebuffer_address_control->SetValue(framebuffer_address); | ||||
|     framebuffer_width_control->setValue(framebuffer_width); | ||||
|     framebuffer_height_control->setValue(framebuffer_height); | ||||
|     framebuffer_format_control->setCurrentIndex(static_cast<int>(framebuffer_format)); | ||||
|     framebuffer_picture_label->setPixmap(pixmap); | ||||
| } | ||||
							
								
								
									
										92
									
								
								src/citra_qt/debugger/graphics_framebuffer.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/citra_qt/debugger/graphics_framebuffer.hxx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| // Copyright 2014 Citra Emulator Project
 | ||||
| // Licensed under GPLv2
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <QDockWidget> | ||||
| 
 | ||||
| #include "video_core/debug_utils/debug_utils.h" | ||||
| 
 | ||||
| class QComboBox; | ||||
| class QLabel; | ||||
| class QSpinBox; | ||||
| 
 | ||||
| class CSpinBox; | ||||
| 
 | ||||
| // Utility class which forwards calls to OnPicaBreakPointHit and OnPicaResume to public slots.
 | ||||
| // This is because the Pica 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, Pica::DebugContext::BreakPointObserver { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|     BreakPointObserverDock(std::shared_ptr<Pica::DebugContext> debug_context, const QString& title, | ||||
|                            QWidget* parent = nullptr); | ||||
| 
 | ||||
|     void OnPicaBreakPointHit(Pica::DebugContext::Event event, void* data) override; | ||||
|     void OnPicaResume() override; | ||||
| 
 | ||||
| private slots: | ||||
|     virtual void OnBreakPointHit(Pica::DebugContext::Event event, void* data) = 0; | ||||
|     virtual void OnResumed() = 0; | ||||
| 
 | ||||
| signals: | ||||
|     void Resumed(); | ||||
|     void BreakPointHit(Pica::DebugContext::Event event, void* data); | ||||
| }; | ||||
| 
 | ||||
| class GraphicsFramebufferWidget : public BreakPointObserverDock { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
|     using Event = Pica::DebugContext::Event; | ||||
| 
 | ||||
|     enum class Source { | ||||
|         PicaTarget = 0, | ||||
|         Custom = 1, | ||||
| 
 | ||||
|         // TODO: Add GPU framebuffer sources!
 | ||||
|     }; | ||||
| 
 | ||||
|     enum class Format { | ||||
|         RGBA8    = 0, | ||||
|         RGB8     = 1, | ||||
|         RGBA5551 = 2, | ||||
|         RGB565   = 3, | ||||
|         RGBA4    = 4, | ||||
|     }; | ||||
| 
 | ||||
| public: | ||||
|     GraphicsFramebufferWidget(std::shared_ptr<Pica::DebugContext> debug_context, QWidget* parent = nullptr); | ||||
| 
 | ||||
| public slots: | ||||
|     void OnFramebufferSourceChanged(int new_value); | ||||
|     void OnFramebufferAddressChanged(qint64 new_value); | ||||
|     void OnFramebufferWidthChanged(int new_value); | ||||
|     void OnFramebufferHeightChanged(int new_value); | ||||
|     void OnFramebufferFormatChanged(int new_value); | ||||
|     void OnUpdate(); | ||||
| 
 | ||||
| private slots: | ||||
|     void OnBreakPointHit(Pica::DebugContext::Event event, void* data) override; | ||||
|     void OnResumed() override; | ||||
| 
 | ||||
| signals: | ||||
|     void Update(); | ||||
| 
 | ||||
| private: | ||||
| 
 | ||||
|     QComboBox* framebuffer_source_list; | ||||
|     CSpinBox* framebuffer_address_control; | ||||
|     QSpinBox* framebuffer_width_control; | ||||
|     QSpinBox* framebuffer_height_control; | ||||
|     QComboBox* framebuffer_format_control; | ||||
| 
 | ||||
|     QLabel* framebuffer_picture_label; | ||||
| 
 | ||||
|     Source framebuffer_source; | ||||
|     unsigned framebuffer_address; | ||||
|     unsigned framebuffer_width; | ||||
|     unsigned framebuffer_height; | ||||
|     Format framebuffer_format; | ||||
| }; | ||||
| @ -20,7 +20,9 @@ | ||||
| #include "debugger/callstack.hxx" | ||||
| #include "debugger/ramview.hxx" | ||||
| #include "debugger/graphics.hxx" | ||||
| #include "debugger/graphics_breakpoints.hxx" | ||||
| #include "debugger/graphics_cmdlists.hxx" | ||||
| #include "debugger/graphics_framebuffer.hxx" | ||||
| 
 | ||||
| #include "core/settings.h" | ||||
| #include "core/system.h" | ||||
| @ -36,6 +38,8 @@ GMainWindow::GMainWindow() | ||||
| { | ||||
|     LogManager::Init(); | ||||
| 
 | ||||
|     Pica::g_debug_context = Pica::DebugContext::Construct(); | ||||
| 
 | ||||
|     Config config; | ||||
| 
 | ||||
|     if (!Settings::values.enable_log) | ||||
| @ -67,12 +71,22 @@ GMainWindow::GMainWindow() | ||||
|     addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); | ||||
|     graphicsCommandsWidget->hide(); | ||||
| 
 | ||||
|     auto graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); | ||||
|     addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); | ||||
|     graphicsBreakpointsWidget->hide(); | ||||
| 
 | ||||
|     auto graphicsFramebufferWidget = new GraphicsFramebufferWidget(Pica::g_debug_context, this); | ||||
|     addDockWidget(Qt::RightDockWidgetArea, graphicsFramebufferWidget); | ||||
|     graphicsFramebufferWidget->hide(); | ||||
| 
 | ||||
|     QMenu* debug_menu = ui.menu_View->addMenu(tr("Debugging")); | ||||
|     debug_menu->addAction(disasmWidget->toggleViewAction()); | ||||
|     debug_menu->addAction(registersWidget->toggleViewAction()); | ||||
|     debug_menu->addAction(callstackWidget->toggleViewAction()); | ||||
|     debug_menu->addAction(graphicsWidget->toggleViewAction()); | ||||
|     debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); | ||||
|     debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); | ||||
|     debug_menu->addAction(graphicsFramebufferWidget->toggleViewAction()); | ||||
| 
 | ||||
|     // Set default UI state
 | ||||
|     // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
 | ||||
| @ -133,6 +147,8 @@ GMainWindow::~GMainWindow() | ||||
|     // will get automatically deleted otherwise
 | ||||
|     if (render_window->parent() == nullptr) | ||||
|         delete render_window; | ||||
| 
 | ||||
|     Pica::g_debug_context.reset(); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::BootGame(std::string filename) | ||||
|  | ||||
							
								
								
									
										303
									
								
								src/citra_qt/util/spinbox.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/citra_qt/util/spinbox.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,303 @@ | ||||
| // Licensed under GPLv2+
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| 
 | ||||
| // Copyright 2014 Tony Wasserka
 | ||||
| // All rights reserved.
 | ||||
| //
 | ||||
| // Redistribution and use in source and binary forms, with or without
 | ||||
| // modification, are permitted provided that the following conditions are met:
 | ||||
| //
 | ||||
| //     * Redistributions of source code must retain the above copyright
 | ||||
| //       notice, this list of conditions and the following disclaimer.
 | ||||
| //     * Redistributions in binary form must reproduce the above copyright
 | ||||
| //       notice, this list of conditions and the following disclaimer in the
 | ||||
| //       documentation and/or other materials provided with the distribution.
 | ||||
| //     * Neither the name of the owner nor the names of its contributors may
 | ||||
| //       be used to endorse or promote products derived from this software
 | ||||
| //       without specific prior written permission.
 | ||||
| //
 | ||||
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 | ||||
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 | ||||
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | ||||
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 | ||||
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 | ||||
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 | ||||
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 | ||||
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 | ||||
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | ||||
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 | ||||
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | ||||
| 
 | ||||
| #include <QLineEdit> | ||||
| #include <QRegExpValidator> | ||||
| 
 | ||||
| #include "common/log.h" | ||||
| 
 | ||||
| #include "spinbox.hxx" | ||||
| 
 | ||||
| CSpinBox::CSpinBox(QWidget* parent) : QAbstractSpinBox(parent), base(10), min_value(-100), max_value(100), value(0), num_digits(0) | ||||
| { | ||||
|     // TODO: Might be nice to not immediately call the slot.
 | ||||
|     //       Think of an address that is being replaced by a different one, in which case a lot
 | ||||
|     //       invalid intermediate addresses would be read from during editing.
 | ||||
|     connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(OnEditingFinished())); | ||||
| 
 | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::SetValue(qint64 val) | ||||
| { | ||||
|     auto old_value = value; | ||||
|     value = std::max(std::min(val, max_value), min_value); | ||||
| 
 | ||||
|     if (old_value != value) { | ||||
|         UpdateText(); | ||||
|         emit ValueChanged(value); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::SetRange(qint64 min, qint64 max) | ||||
| { | ||||
|     min_value = min; | ||||
|     max_value = max; | ||||
| 
 | ||||
|     SetValue(value); | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::stepBy(int steps) | ||||
| { | ||||
|     auto new_value = value; | ||||
|     // Scale number of steps by the currently selected digit
 | ||||
|     // TODO: Move this code elsewhere and enable it.
 | ||||
|     // TODO: Support for num_digits==0, too
 | ||||
|     // TODO: Support base!=16, too
 | ||||
|     // TODO: Make the cursor not jump back to the end of the line...
 | ||||
|     /*if (base == 16 && num_digits > 0) {
 | ||||
|         int digit = num_digits - (lineEdit()->cursorPosition() - prefix.length()) - 1; | ||||
|         digit = std::max(0, std::min(digit, num_digits - 1)); | ||||
|         steps <<= digit * 4; | ||||
|     }*/ | ||||
| 
 | ||||
|     // Increment "new_value" by "steps", and perform annoying overflow checks, too.
 | ||||
|     if (steps < 0 && new_value + steps > new_value) { | ||||
|         new_value = std::numeric_limits<qint64>::min(); | ||||
|     } else if (steps > 0 && new_value + steps < new_value) { | ||||
|         new_value = std::numeric_limits<qint64>::max(); | ||||
|     } else { | ||||
|         new_value += steps; | ||||
|     } | ||||
| 
 | ||||
|     SetValue(new_value); | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| QAbstractSpinBox::StepEnabled CSpinBox::stepEnabled() const | ||||
| { | ||||
|     StepEnabled ret = StepNone; | ||||
| 
 | ||||
|     if (value > min_value) | ||||
|         ret |= StepDownEnabled; | ||||
| 
 | ||||
|     if (value < max_value) | ||||
|         ret |= StepUpEnabled; | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::SetBase(int base) | ||||
| { | ||||
|     this->base = base; | ||||
| 
 | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::SetNumDigits(int num_digits) | ||||
| { | ||||
|     this->num_digits = num_digits; | ||||
| 
 | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::SetPrefix(const QString& prefix) | ||||
| { | ||||
|     this->prefix = prefix; | ||||
| 
 | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::SetSuffix(const QString& suffix) | ||||
| { | ||||
|     this->suffix = suffix; | ||||
| 
 | ||||
|     UpdateText(); | ||||
| } | ||||
| 
 | ||||
| static QString StringToInputMask(const QString& input) { | ||||
|     QString mask = input; | ||||
| 
 | ||||
|     // ... replace any special characters by their escaped counterparts ...
 | ||||
|     mask.replace("\\", "\\\\"); | ||||
|     mask.replace("A", "\\A"); | ||||
|     mask.replace("a", "\\a"); | ||||
|     mask.replace("N", "\\N"); | ||||
|     mask.replace("n", "\\n"); | ||||
|     mask.replace("X", "\\X"); | ||||
|     mask.replace("x", "\\x"); | ||||
|     mask.replace("9", "\\9"); | ||||
|     mask.replace("0", "\\0"); | ||||
|     mask.replace("D", "\\D"); | ||||
|     mask.replace("d", "\\d"); | ||||
|     mask.replace("#", "\\#"); | ||||
|     mask.replace("H", "\\H"); | ||||
|     mask.replace("h", "\\h"); | ||||
|     mask.replace("B", "\\B"); | ||||
|     mask.replace("b", "\\b"); | ||||
|     mask.replace(">", "\\>"); | ||||
|     mask.replace("<", "\\<"); | ||||
|     mask.replace("!", "\\!"); | ||||
| 
 | ||||
|     return mask; | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::UpdateText() | ||||
| { | ||||
|     // If a fixed number of digits is used, we put the line edit in insertion mode by setting an
 | ||||
|     // input mask.
 | ||||
|     QString mask; | ||||
|     if (num_digits != 0) { | ||||
|         mask += StringToInputMask(prefix); | ||||
| 
 | ||||
|         // For base 10 and negative range, demand a single sign character
 | ||||
|         if (HasSign()) | ||||
|             mask += "X"; // identified as "-" or "+" in the validator
 | ||||
| 
 | ||||
|         // Uppercase digits greater than 9.
 | ||||
|         mask += ">"; | ||||
| 
 | ||||
|         // The greatest signed 64-bit number has 19 decimal digits.
 | ||||
|         // TODO: Could probably make this more generic with some logarithms.
 | ||||
|         // For reference, unsigned 64-bit can have up to 20 decimal digits.
 | ||||
|         int digits = (num_digits != 0) ? num_digits | ||||
|                      : (base == 16) ? 16 | ||||
|                      : (base == 10) ? 19 | ||||
|                      : 0xFF; // fallback case...
 | ||||
| 
 | ||||
|         // Match num_digits digits
 | ||||
|         // Digits irrelevant to the chosen number base are filtered in the validator
 | ||||
|         mask += QString("H").repeated(std::max(num_digits, 1)); | ||||
| 
 | ||||
|         // Switch off case conversion
 | ||||
|         mask += "!"; | ||||
| 
 | ||||
|         mask += StringToInputMask(suffix); | ||||
|     } | ||||
|     lineEdit()->setInputMask(mask); | ||||
| 
 | ||||
|     // Set new text without changing the cursor position. This will cause the cursor to briefly
 | ||||
|     // appear at the end of the line and then to jump back to its original position. That's
 | ||||
|     // a bit ugly, but better than having setText() move the cursor permanently all the time.
 | ||||
|     int cursor_position = lineEdit()->cursorPosition(); | ||||
|     lineEdit()->setText(TextFromValue()); | ||||
|     lineEdit()->setCursorPosition(cursor_position); | ||||
| } | ||||
| 
 | ||||
| QString CSpinBox::TextFromValue() | ||||
| { | ||||
|     return prefix | ||||
|            + QString(HasSign() ? ((value < 0) ? "-" : "+") : "") | ||||
|            + QString("%1").arg(abs(value), num_digits, base, QLatin1Char('0')).toUpper() | ||||
|            + suffix; | ||||
| } | ||||
| 
 | ||||
| qint64 CSpinBox::ValueFromText() | ||||
| { | ||||
|     unsigned strpos = prefix.length(); | ||||
| 
 | ||||
|     QString num_string = text().mid(strpos, text().length() - strpos - suffix.length()); | ||||
|     return num_string.toLongLong(nullptr, base); | ||||
| } | ||||
| 
 | ||||
| bool CSpinBox::HasSign() const | ||||
| { | ||||
|     return base == 10 && min_value < 0; | ||||
| } | ||||
| 
 | ||||
| void CSpinBox::OnEditingFinished() | ||||
| { | ||||
|     // Only update for valid input
 | ||||
|     QString input = lineEdit()->text(); | ||||
|     int pos = 0; | ||||
|     if (QValidator::Acceptable == validate(input, pos)) | ||||
|         SetValue(ValueFromText()); | ||||
| } | ||||
| 
 | ||||
| QValidator::State CSpinBox::validate(QString& input, int& pos) const | ||||
| { | ||||
|     if (!prefix.isEmpty() && input.left(prefix.length()) != prefix) | ||||
|         return QValidator::Invalid; | ||||
| 
 | ||||
|     unsigned strpos = prefix.length(); | ||||
| 
 | ||||
|     // Empty "numbers" allowed as intermediate values
 | ||||
|     if (strpos >= input.length() - HasSign() - suffix.length()) | ||||
|         return QValidator::Intermediate; | ||||
| 
 | ||||
|     _dbg_assert_(GUI, base <= 10 || base == 16); | ||||
|     QString regexp; | ||||
| 
 | ||||
|     // Demand sign character for negative ranges
 | ||||
|     if (HasSign()) | ||||
|         regexp += "[+\\-]"; | ||||
| 
 | ||||
|     // Match digits corresponding to the chosen number base.
 | ||||
|     regexp += QString("[0-%1").arg(std::min(base, 9)); | ||||
|     if (base == 16) { | ||||
|         regexp += "a-fA-F"; | ||||
|     } | ||||
|     regexp += "]"; | ||||
| 
 | ||||
|     // Specify number of digits
 | ||||
|     if (num_digits > 0) { | ||||
|         regexp += QString("{%1}").arg(num_digits); | ||||
|     } else { | ||||
|         regexp += "+"; | ||||
|     } | ||||
| 
 | ||||
|     // Match string
 | ||||
|     QRegExp num_regexp(regexp); | ||||
|     int num_pos = strpos; | ||||
|     QString sub_input = input.mid(strpos, input.length() - strpos - suffix.length()); | ||||
| 
 | ||||
|     if (!num_regexp.exactMatch(sub_input) && num_regexp.matchedLength() == 0) | ||||
|         return QValidator::Invalid; | ||||
| 
 | ||||
|     sub_input = sub_input.left(num_regexp.matchedLength()); | ||||
|     bool ok; | ||||
|     qint64 val = sub_input.toLongLong(&ok, base); | ||||
| 
 | ||||
|     if (!ok) | ||||
|         return QValidator::Invalid; | ||||
| 
 | ||||
|     // Outside boundaries => don't accept
 | ||||
|     if (val < min_value || val > max_value) | ||||
|         return QValidator::Invalid; | ||||
| 
 | ||||
|     // Make sure we are actually at the end of this string...
 | ||||
|     strpos += num_regexp.matchedLength(); | ||||
| 
 | ||||
|     if (!suffix.isEmpty() && input.mid(strpos) != suffix) { | ||||
|         return QValidator::Invalid; | ||||
|     } else { | ||||
|         strpos += suffix.length(); | ||||
|     } | ||||
| 
 | ||||
|     if (strpos != input.length()) | ||||
|         return QValidator::Invalid; | ||||
| 
 | ||||
|     // At this point we can say for sure that the input is fine. Let's fix it up a bit though
 | ||||
|     input.replace(num_pos, sub_input.length(), sub_input.toUpper()); | ||||
| 
 | ||||
|     return QValidator::Acceptable; | ||||
| } | ||||
							
								
								
									
										88
									
								
								src/citra_qt/util/spinbox.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/citra_qt/util/spinbox.hxx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| // Licensed under GPLv2+
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| 
 | ||||
| // Copyright 2014 Tony Wasserka
 | ||||
| // All rights reserved.
 | ||||
| //
 | ||||
| // Redistribution and use in source and binary forms, with or without
 | ||||
| // modification, are permitted provided that the following conditions are met:
 | ||||
| //
 | ||||
| //     * Redistributions of source code must retain the above copyright
 | ||||
| //       notice, this list of conditions and the following disclaimer.
 | ||||
| //     * Redistributions in binary form must reproduce the above copyright
 | ||||
| //       notice, this list of conditions and the following disclaimer in the
 | ||||
| //       documentation and/or other materials provided with the distribution.
 | ||||
| //     * Neither the name of the owner nor the names of its contributors may
 | ||||
| //       be used to endorse or promote products derived from this software
 | ||||
| //       without specific prior written permission.
 | ||||
| //
 | ||||
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 | ||||
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 | ||||
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | ||||
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 | ||||
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 | ||||
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 | ||||
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 | ||||
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 | ||||
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | ||||
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 | ||||
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | ||||
| 
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <QAbstractSpinBox> | ||||
| #include <QtGlobal> | ||||
| 
 | ||||
| class QVariant; | ||||
| 
 | ||||
| /**
 | ||||
|  * A custom spin box widget with enhanced functionality over Qt's QSpinBox | ||||
|  */ | ||||
| class CSpinBox : public QAbstractSpinBox { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|     CSpinBox(QWidget* parent = nullptr); | ||||
| 
 | ||||
|     void stepBy(int steps) override; | ||||
|     StepEnabled stepEnabled() const override; | ||||
| 
 | ||||
|     void SetValue(qint64 val); | ||||
| 
 | ||||
|     void SetRange(qint64 min, qint64 max); | ||||
| 
 | ||||
|     void SetBase(int base); | ||||
| 
 | ||||
|     void SetPrefix(const QString& prefix); | ||||
|     void SetSuffix(const QString& suffix); | ||||
| 
 | ||||
|     void SetNumDigits(int num_digits); | ||||
| 
 | ||||
|     QValidator::State validate(QString& input, int& pos) const override; | ||||
| 
 | ||||
| signals: | ||||
|     void ValueChanged(qint64 val); | ||||
| 
 | ||||
| private slots: | ||||
|     void OnEditingFinished(); | ||||
| 
 | ||||
| private: | ||||
|     void UpdateText(); | ||||
| 
 | ||||
|     bool HasSign() const; | ||||
| 
 | ||||
|     QString TextFromValue(); | ||||
|     qint64 ValueFromText(); | ||||
| 
 | ||||
|     qint64 min_value, max_value; | ||||
| 
 | ||||
|     qint64 value; | ||||
| 
 | ||||
|     QString prefix, suffix; | ||||
| 
 | ||||
|     int base; | ||||
| 
 | ||||
|     int num_digits; | ||||
| }; | ||||
| @ -4,6 +4,8 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "common_types.h" | ||||
| 
 | ||||
| #ifdef _WIN32 | ||||
| #define SLEEP(x) Sleep(x) | ||||
| #else | ||||
| @ -37,6 +39,8 @@ template<> struct CompileTimeAssert<true> {}; | ||||
| #include <sys/endian.h> | ||||
| #endif | ||||
| 
 | ||||
| #include "common_types.h" | ||||
| 
 | ||||
| // go to debugger mode
 | ||||
|     #ifdef GEKKO | ||||
|         #define Crash() | ||||
| @ -73,6 +77,8 @@ inline u64 _rotr64(u64 x, unsigned int shift){ | ||||
| } | ||||
| 
 | ||||
| #else // WIN32
 | ||||
| #include <locale.h> | ||||
| 
 | ||||
| // Function Cross-Compatibility
 | ||||
|     #define strcasecmp _stricmp | ||||
|     #define strncasecmp _strnicmp | ||||
|  | ||||
| @ -4,6 +4,9 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "common/common_funcs.h" | ||||
| #include "common/msg_handler.h" | ||||
| 
 | ||||
| #ifndef LOGGING | ||||
| #define LOGGING | ||||
| #endif | ||||
|  | ||||
| @ -34,6 +34,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | ||||
|     u32 old_value = registers[id]; | ||||
|     registers[id] = (old_value & ~mask) | (value & mask); | ||||
| 
 | ||||
|     if (g_debug_context) | ||||
|         g_debug_context->OnEvent(DebugContext::Event::CommandLoaded, reinterpret_cast<void*>(&id)); | ||||
| 
 | ||||
|     DebugUtils::OnPicaRegWrite(id, registers[id]); | ||||
| 
 | ||||
|     switch(id) { | ||||
| @ -43,6 +46,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | ||||
|         { | ||||
|             DebugUtils::DumpTevStageConfig(registers.GetTevStages()); | ||||
| 
 | ||||
|             if (g_debug_context) | ||||
|                 g_debug_context->OnEvent(DebugContext::Event::IncomingPrimitiveBatch, nullptr); | ||||
| 
 | ||||
|             const auto& attribute_config = registers.vertex_attributes; | ||||
|             const u8* const base_address = Memory::GetPointer(attribute_config.GetBaseAddress()); | ||||
| 
 | ||||
| @ -132,6 +138,10 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | ||||
|                 clipper_primitive_assembler.SubmitVertex(output, Clipper::ProcessTriangle); | ||||
|             } | ||||
|             geometry_dumper.Dump(); | ||||
| 
 | ||||
|             if (g_debug_context) | ||||
|                 g_debug_context->OnEvent(DebugContext::Event::FinishedPrimitiveBatch, nullptr); | ||||
| 
 | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
| @ -229,6 +239,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     if (g_debug_context) | ||||
|         g_debug_context->OnEvent(DebugContext::Event::CommandProcessed, reinterpret_cast<void*>(&id)); | ||||
| } | ||||
| 
 | ||||
| static std::ptrdiff_t ExecuteCommandBlock(const u32* first_command_word) { | ||||
|  | ||||
| @ -3,6 +3,8 @@ | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <condition_variable> | ||||
| #include <list> | ||||
| #include <map> | ||||
| #include <fstream> | ||||
| #include <mutex> | ||||
| @ -12,14 +14,56 @@ | ||||
| #include <png.h> | ||||
| #endif | ||||
| 
 | ||||
| #include "common/log.h" | ||||
| #include "common/file_util.h" | ||||
| 
 | ||||
| #include "video_core/math.h" | ||||
| #include "video_core/pica.h" | ||||
| 
 | ||||
| #include "debug_utils.h" | ||||
| 
 | ||||
| namespace Pica { | ||||
| 
 | ||||
| void DebugContext::OnEvent(Event event, void* data) { | ||||
|     if (!breakpoints[event].enabled) | ||||
|         return; | ||||
| 
 | ||||
|     { | ||||
|         std::unique_lock<std::mutex> lock(breakpoint_mutex); | ||||
| 
 | ||||
|         // 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->OnPicaBreakPointHit(event, data); | ||||
|         } | ||||
| 
 | ||||
|         // Wait until another thread tells us to Resume()
 | ||||
|         resume_from_breakpoint.wait(lock, [&]{ return !at_breakpoint; }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void DebugContext::Resume() { | ||||
|     { | ||||
|         std::unique_lock<std::mutex> lock(breakpoint_mutex); | ||||
| 
 | ||||
|         // Tell all observers that we are about to resume
 | ||||
|         for (auto& breakpoint_observer : breakpoint_observers) { | ||||
|             breakpoint_observer->OnPicaResume(); | ||||
|         } | ||||
| 
 | ||||
|         // Resume the waiting thread (i.e. OnEvent())
 | ||||
|         at_breakpoint = false; | ||||
|     } | ||||
| 
 | ||||
|     resume_from_breakpoint.notify_one(); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<DebugContext> g_debug_context; // TODO: Get rid of this global
 | ||||
| 
 | ||||
| namespace DebugUtils { | ||||
| 
 | ||||
| void GeometryDumper::AddTriangle(Vertex& v0, Vertex& v1, Vertex& v2) { | ||||
| @ -312,6 +356,42 @@ std::unique_ptr<PicaTrace> FinishPicaTracing() | ||||
|     return std::move(ret); | ||||
| } | ||||
| 
 | ||||
| const Math::Vec4<u8> LookupTexture(const u8* source, int x, int y, const TextureInfo& info) { | ||||
|     _dbg_assert_(GPU, info.format == Pica::Regs::TextureFormat::RGB8); | ||||
| 
 | ||||
|     // Cf. rasterizer code for an explanation of this algorithm.
 | ||||
|     int texel_index_within_tile = 0; | ||||
|     for (int block_size_index = 0; block_size_index < 3; ++block_size_index) { | ||||
|         int sub_tile_width = 1 << block_size_index; | ||||
|         int sub_tile_height = 1 << block_size_index; | ||||
| 
 | ||||
|         int sub_tile_index = (x & sub_tile_width) << block_size_index; | ||||
|         sub_tile_index += 2 * ((y & sub_tile_height) << block_size_index); | ||||
|         texel_index_within_tile += sub_tile_index; | ||||
|     } | ||||
| 
 | ||||
|     const int block_width = 8; | ||||
|     const int block_height = 8; | ||||
| 
 | ||||
|     int coarse_x = (x / block_width) * block_width; | ||||
|     int coarse_y = (y / block_height) * block_height; | ||||
| 
 | ||||
|     const u8* source_ptr = source + coarse_x * block_height * 3 + coarse_y * info.stride + texel_index_within_tile * 3; | ||||
|     return { source_ptr[2], source_ptr[1], source_ptr[0], 255 }; | ||||
| } | ||||
| 
 | ||||
| TextureInfo TextureInfo::FromPicaRegister(const Regs::TextureConfig& config, | ||||
|                                           const Regs::TextureFormat& format) | ||||
| { | ||||
|     TextureInfo info; | ||||
|     info.address = config.GetPhysicalAddress(); | ||||
|     info.width = config.width; | ||||
|     info.height = config.height; | ||||
|     info.format = format; | ||||
|     info.stride = Pica::Regs::BytesPerPixel(info.format) * info.width; | ||||
|     return info; | ||||
| } | ||||
| 
 | ||||
| void DumpTexture(const Pica::Regs::TextureConfig& texture_config, u8* data) { | ||||
|     // NOTE: Permanently enabling this just trashes hard disks for no reason.
 | ||||
|     //       Hence, this is currently disabled.
 | ||||
| @ -377,27 +457,15 @@ void DumpTexture(const Pica::Regs::TextureConfig& texture_config, u8* data) { | ||||
|     buf = new u8[row_stride * texture_config.height]; | ||||
|     for (unsigned y = 0; y < texture_config.height; ++y) { | ||||
|         for (unsigned x = 0; x < texture_config.width; ++x) { | ||||
|             // Cf. rasterizer code for an explanation of this algorithm.
 | ||||
|             int texel_index_within_tile = 0; | ||||
|             for (int block_size_index = 0; block_size_index < 3; ++block_size_index) { | ||||
|                 int sub_tile_width = 1 << block_size_index; | ||||
|                 int sub_tile_height = 1 << block_size_index; | ||||
| 
 | ||||
|                 int sub_tile_index = (x & sub_tile_width) << block_size_index; | ||||
|                 sub_tile_index += 2 * ((y & sub_tile_height) << block_size_index); | ||||
|                 texel_index_within_tile += sub_tile_index; | ||||
|             } | ||||
| 
 | ||||
|             const int block_width = 8; | ||||
|             const int block_height = 8; | ||||
| 
 | ||||
|             int coarse_x = (x / block_width) * block_width; | ||||
|             int coarse_y = (y / block_height) * block_height; | ||||
| 
 | ||||
|             u8* source_ptr = (u8*)data + coarse_x * block_height * 3 + coarse_y * row_stride + texel_index_within_tile * 3; | ||||
|             buf[3 * x + y * row_stride    ] = source_ptr[2]; | ||||
|             buf[3 * x + y * row_stride + 1] = source_ptr[1]; | ||||
|             buf[3 * x + y * row_stride + 2] = source_ptr[0]; | ||||
|             TextureInfo info; | ||||
|             info.width = texture_config.width; | ||||
|             info.height = texture_config.height; | ||||
|             info.stride = row_stride; | ||||
|             info.format = registers.texture0_format; | ||||
|             Math::Vec4<u8> texture_color = LookupTexture(data, x, y, info); | ||||
|             buf[3 * x + y * row_stride    ] = texture_color.r(); | ||||
|             buf[3 * x + y * row_stride + 1] = texture_color.g(); | ||||
|             buf[3 * x + y * row_stride + 2] = texture_color.b(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -5,13 +5,147 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <condition_variable> | ||||
| #include <list> | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
| #include <vector> | ||||
| 
 | ||||
| #include "video_core/math.h" | ||||
| #include "video_core/pica.h" | ||||
| 
 | ||||
| namespace Pica { | ||||
| 
 | ||||
| class DebugContext { | ||||
| public: | ||||
|     enum class Event { | ||||
|         FirstEvent = 0, | ||||
| 
 | ||||
|         CommandLoaded = FirstEvent, | ||||
|         CommandProcessed, | ||||
|         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 OnPicaBreakPointHit and OnPicaResume 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 Citra
 | ||||
|                 // 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 OnPicaBreakPointHit(Event, void*) { | ||||
|         } | ||||
| 
 | ||||
|         /**
 | ||||
|          * 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 OnPicaResume() { | ||||
|         } | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|     /**
 | ||||
|      * 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() { | ||||
|         breakpoints.clear(); | ||||
|         Resume(); | ||||
|     } | ||||
| 
 | ||||
|     // TODO: Evaluate if access to these members should be hidden behind a public interface.
 | ||||
|     std::map<Event, BreakPoint> 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; | ||||
| }; | ||||
| 
 | ||||
| extern std::shared_ptr<DebugContext> g_debug_context; // TODO: Get rid of this global
 | ||||
| 
 | ||||
| namespace DebugUtils { | ||||
| 
 | ||||
| // Simple utility class for dumping geometry data to an OBJ file
 | ||||
| @ -57,6 +191,18 @@ bool IsPicaTracing(); | ||||
| void OnPicaRegWrite(u32 id, u32 value); | ||||
| std::unique_ptr<PicaTrace> FinishPicaTracing(); | ||||
| 
 | ||||
| struct TextureInfo { | ||||
|     unsigned int address; | ||||
|     int width; | ||||
|     int height; | ||||
|     int stride; | ||||
|     Pica::Regs::TextureFormat format; | ||||
| 
 | ||||
|     static TextureInfo FromPicaRegister(const Pica::Regs::TextureConfig& config, | ||||
|                                         const Pica::Regs::TextureFormat& format); | ||||
| }; | ||||
| 
 | ||||
| const Math::Vec4<u8> LookupTexture(const u8* source, int x, int y, const TextureInfo& info); | ||||
| void DumpTexture(const Pica::Regs::TextureConfig& texture_config, u8* data); | ||||
| 
 | ||||
| void DumpTevStageConfig(const std::array<Pica::Regs::TevStageConfig,6>& stages); | ||||
|  | ||||
| @ -109,7 +109,7 @@ struct Regs { | ||||
| 
 | ||||
|         u32 address; | ||||
| 
 | ||||
|         u32 GetPhysicalAddress() { | ||||
|         u32 GetPhysicalAddress() const { | ||||
|             return DecodeAddressRegister(address) - Memory::FCRAM_PADDR + Memory::HEAP_GSP_VADDR; | ||||
|         } | ||||
| 
 | ||||
| @ -130,7 +130,26 @@ struct Regs { | ||||
|         // Seems like they are luminance formats and compressed textures.
 | ||||
|     }; | ||||
| 
 | ||||
|     BitField<0, 1, u32> texturing_enable; | ||||
|     static unsigned BytesPerPixel(TextureFormat format) { | ||||
|         switch (format) { | ||||
|         case TextureFormat::RGBA8: | ||||
|             return 4; | ||||
| 
 | ||||
|         case TextureFormat::RGB8: | ||||
|             return 3; | ||||
| 
 | ||||
|         case TextureFormat::RGBA5551: | ||||
|         case TextureFormat::RGB565: | ||||
|         case TextureFormat::RGBA4: | ||||
|             return 2; | ||||
| 
 | ||||
|         default: | ||||
|             // placeholder for yet unknown formats
 | ||||
|             return 1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     BitField< 0, 1, u32> texturing_enable; | ||||
|     TextureConfig texture0; | ||||
|     INSERT_PADDING_WORDS(0x8); | ||||
|     BitField<0, 4, TextureFormat> texture0_format; | ||||
| @ -517,10 +536,6 @@ struct Regs { | ||||
|     static std::string GetCommandName(int index) { | ||||
|         std::map<u32, std::string> map; | ||||
| 
 | ||||
|         // TODO: MSVC does not support using offsetof() on non-static data members even though this
 | ||||
|         //       is technically allowed since C++11. Hence, this functionality is disabled until
 | ||||
|         //       MSVC properly supports it.
 | ||||
|         #ifndef _MSC_VER | ||||
|         Regs regs; | ||||
|         #define ADD_FIELD(name)                                                                               \ | ||||
|             do {                                                                                              \ | ||||
| @ -557,7 +572,6 @@ struct Regs { | ||||
|         ADD_FIELD(vs_swizzle_patterns); | ||||
| 
 | ||||
|         #undef ADD_FIELD | ||||
|         #endif // _MSC_VER
 | ||||
| 
 | ||||
|         // Return empty string if no match is found
 | ||||
|         return map[index]; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 bunnei
						bunnei