Browse Source
Merge pull request #218 from neobrain/pica_debugger
Merge pull request #218 from neobrain/pica_debugger
Pica debugger improvementspull/15/merge
18 changed files with 1688 additions and 49 deletions
-
7src/citra_qt/CMakeLists.txt
-
13src/citra_qt/bootmanager.cpp
-
261src/citra_qt/debugger/graphics_breakpoints.cpp
-
53src/citra_qt/debugger/graphics_breakpoints.hxx
-
38src/citra_qt/debugger/graphics_breakpoints_p.hxx
-
241src/citra_qt/debugger/graphics_cmdlists.cpp
-
37src/citra_qt/debugger/graphics_cmdlists.hxx
-
282src/citra_qt/debugger/graphics_framebuffer.cpp
-
92src/citra_qt/debugger/graphics_framebuffer.hxx
-
16src/citra_qt/main.cpp
-
303src/citra_qt/util/spinbox.cpp
-
88src/citra_qt/util/spinbox.hxx
-
6src/common/common_funcs.h
-
3src/common/log.h
-
13src/video_core/command_processor.cpp
-
110src/video_core/debug_utils/debug_utils.cpp
-
146src/video_core/debug_utils/debug_utils.h
-
28src/video_core/pica.h
@ -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")); |
|||
} |
|||
} |
|||
@ -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; |
|||
}; |
|||
@ -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; |
|||
}; |
|||
@ -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); |
|||
} |
|||
@ -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; |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
}; |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue