Browse Source
Merge pull request #1765 from JayFoxRox/debug-surface-viewer
Merge pull request #1765 from JayFoxRox/debug-surface-viewer
Debugger: Pica surface viewernce_cpp
9 changed files with 876 additions and 583 deletions
-
4src/citra_qt/CMakeLists.txt
-
125src/citra_qt/debugger/graphics_cmdlists.cpp
-
22src/citra_qt/debugger/graphics_cmdlists.h
-
356src/citra_qt/debugger/graphics_framebuffer.cpp
-
76src/citra_qt/debugger/graphics_framebuffer.h
-
736src/citra_qt/debugger/graphics_surface.cpp
-
120src/citra_qt/debugger/graphics_surface.h
-
19src/citra_qt/main.cpp
-
1src/citra_qt/main.h
@ -1,356 +0,0 @@ |
|||
// Copyright 2014 Citra Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <QBoxLayout>
|
|||
#include <QComboBox>
|
|||
#include <QDebug>
|
|||
#include <QLabel>
|
|||
#include <QPushButton>
|
|||
#include <QSpinBox>
|
|||
|
|||
#include "citra_qt/debugger/graphics_framebuffer.h"
|
|||
#include "citra_qt/util/spinbox.h"
|
|||
|
|||
#include "common/color.h"
|
|||
|
|||
#include "core/memory.h"
|
|||
#include "core/hw/gpu.h"
|
|||
|
|||
#include "video_core/pica.h"
|
|||
#include "video_core/pica_state.h"
|
|||
#include "video_core/utils.h"
|
|||
|
|||
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("Active Depth Buffer")); |
|||
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("RGB5A1")); |
|||
framebuffer_format_control->addItem(tr("RGB565")); |
|||
framebuffer_format_control->addItem(tr("RGBA4")); |
|||
framebuffer_format_control->addItem(tr("D16")); |
|||
framebuffer_format_control->addItem(tr("D24")); |
|||
framebuffer_format_control->addItem(tr("D24X8")); |
|||
framebuffer_format_control->addItem(tr("X24S8")); |
|||
framebuffer_format_control->addItem(tr("(unknown)")); |
|||
|
|||
// 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
|
|||
if (debug_context && debug_context->at_breakpoint) |
|||
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 != static_cast<unsigned>(new_value)) { |
|||
framebuffer_width = static_cast<unsigned>(new_value); |
|||
|
|||
framebuffer_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); |
|||
emit Update(); |
|||
} |
|||
} |
|||
|
|||
void GraphicsFramebufferWidget::OnFramebufferHeightChanged(int new_value) |
|||
{ |
|||
if (framebuffer_height != static_cast<unsigned>(new_value)) { |
|||
framebuffer_height = static_cast<unsigned>(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...
|
|||
|
|||
const auto& framebuffer = Pica::g_state.regs.framebuffer; |
|||
|
|||
framebuffer_address = framebuffer.GetColorBufferPhysicalAddress(); |
|||
framebuffer_width = framebuffer.GetWidth(); |
|||
framebuffer_height = framebuffer.GetHeight(); |
|||
|
|||
switch (framebuffer.color_format) { |
|||
case Pica::Regs::ColorFormat::RGBA8: |
|||
framebuffer_format = Format::RGBA8; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGB8: |
|||
framebuffer_format = Format::RGB8; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGB5A1: |
|||
framebuffer_format = Format::RGB5A1; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGB565: |
|||
framebuffer_format = Format::RGB565; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGBA4: |
|||
framebuffer_format = Format::RGBA4; |
|||
break; |
|||
|
|||
default: |
|||
framebuffer_format = Format::Unknown; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case Source::DepthBuffer: |
|||
{ |
|||
const auto& framebuffer = Pica::g_state.regs.framebuffer; |
|||
|
|||
framebuffer_address = framebuffer.GetDepthBufferPhysicalAddress(); |
|||
framebuffer_width = framebuffer.GetWidth(); |
|||
framebuffer_height = framebuffer.GetHeight(); |
|||
|
|||
switch (framebuffer.depth_format) { |
|||
case Pica::Regs::DepthFormat::D16: |
|||
framebuffer_format = Format::D16; |
|||
break; |
|||
|
|||
case Pica::Regs::DepthFormat::D24: |
|||
framebuffer_format = Format::D24; |
|||
break; |
|||
|
|||
case Pica::Regs::DepthFormat::D24S8: |
|||
framebuffer_format = Format::D24X8; |
|||
break; |
|||
|
|||
default: |
|||
framebuffer_format = Format::Unknown; |
|||
break; |
|||
} |
|||
|
|||
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
|
|||
u32 bytes_per_pixel = GraphicsFramebufferWidget::BytesPerPixel(framebuffer_format); |
|||
|
|||
QImage decoded_image(framebuffer_width, framebuffer_height, QImage::Format_ARGB32); |
|||
u8* buffer = Memory::GetPhysicalPointer(framebuffer_address); |
|||
|
|||
for (unsigned int y = 0; y < framebuffer_height; ++y) { |
|||
for (unsigned int x = 0; x < framebuffer_width; ++x) { |
|||
const u32 coarse_y = y & ~7; |
|||
u32 offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * framebuffer_width * bytes_per_pixel; |
|||
const u8* pixel = buffer + offset; |
|||
Math::Vec4<u8> color = { 0, 0, 0, 0 }; |
|||
|
|||
switch (framebuffer_format) { |
|||
case Format::RGBA8: |
|||
color = Color::DecodeRGBA8(pixel); |
|||
break; |
|||
case Format::RGB8: |
|||
color = Color::DecodeRGB8(pixel); |
|||
break; |
|||
case Format::RGB5A1: |
|||
color = Color::DecodeRGB5A1(pixel); |
|||
break; |
|||
case Format::RGB565: |
|||
color = Color::DecodeRGB565(pixel); |
|||
break; |
|||
case Format::RGBA4: |
|||
color = Color::DecodeRGBA4(pixel); |
|||
break; |
|||
case Format::D16: |
|||
{ |
|||
u32 data = Color::DecodeD16(pixel); |
|||
color.r() = data & 0xFF; |
|||
color.g() = (data >> 8) & 0xFF; |
|||
break; |
|||
} |
|||
case Format::D24: |
|||
{ |
|||
u32 data = Color::DecodeD24(pixel); |
|||
color.r() = data & 0xFF; |
|||
color.g() = (data >> 8) & 0xFF; |
|||
color.b() = (data >> 16) & 0xFF; |
|||
break; |
|||
} |
|||
case Format::D24X8: |
|||
{ |
|||
Math::Vec2<u32> data = Color::DecodeD24S8(pixel); |
|||
color.r() = data.x & 0xFF; |
|||
color.g() = (data.x >> 8) & 0xFF; |
|||
color.b() = (data.x >> 16) & 0xFF; |
|||
break; |
|||
} |
|||
case Format::X24S8: |
|||
{ |
|||
Math::Vec2<u32> data = Color::DecodeD24S8(pixel); |
|||
color.r() = color.g() = color.b() = data.y; |
|||
break; |
|||
} |
|||
default: |
|||
qDebug() << "Unknown fb color format " << static_cast<int>(framebuffer_format); |
|||
break; |
|||
} |
|||
|
|||
decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), 255)); |
|||
} |
|||
} |
|||
pixmap = QPixmap::fromImage(decoded_image); |
|||
|
|||
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); |
|||
} |
|||
|
|||
u32 GraphicsFramebufferWidget::BytesPerPixel(GraphicsFramebufferWidget::Format format) { |
|||
switch (format) { |
|||
case Format::RGBA8: |
|||
case Format::D24X8: |
|||
case Format::X24S8: |
|||
return 4; |
|||
case Format::RGB8: |
|||
case Format::D24: |
|||
return 3; |
|||
case Format::RGB5A1: |
|||
case Format::RGB565: |
|||
case Format::RGBA4: |
|||
case Format::D16: |
|||
return 2; |
|||
default: |
|||
UNREACHABLE_MSG("GraphicsFramebufferWidget::BytesPerPixel: this " |
|||
"should not be reached as this function should " |
|||
"be given a format which is in " |
|||
"GraphicsFramebufferWidget::Format. Instead got %i", |
|||
static_cast<int>(format)); |
|||
} |
|||
} |
|||
@ -1,76 +0,0 @@ |
|||
// Copyright 2014 Citra Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include "citra_qt/debugger/graphics_breakpoint_observer.h" |
|||
|
|||
class QComboBox; |
|||
class QLabel; |
|||
class QSpinBox; |
|||
|
|||
class CSpinBox; |
|||
|
|||
class GraphicsFramebufferWidget : public BreakPointObserverDock { |
|||
Q_OBJECT |
|||
|
|||
using Event = Pica::DebugContext::Event; |
|||
|
|||
enum class Source { |
|||
PicaTarget = 0, |
|||
DepthBuffer = 1, |
|||
Custom = 2, |
|||
|
|||
// TODO: Add GPU framebuffer sources! |
|||
}; |
|||
|
|||
enum class Format { |
|||
RGBA8 = 0, |
|||
RGB8 = 1, |
|||
RGB5A1 = 2, |
|||
RGB565 = 3, |
|||
RGBA4 = 4, |
|||
D16 = 5, |
|||
D24 = 6, |
|||
D24X8 = 7, |
|||
X24S8 = 8, |
|||
Unknown = 9 |
|||
}; |
|||
|
|||
static u32 BytesPerPixel(Format format); |
|||
|
|||
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,736 @@ |
|||
// Copyright 2014 Citra Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <QBoxLayout>
|
|||
#include <QComboBox>
|
|||
#include <QDebug>
|
|||
#include <QFileDialog>
|
|||
#include <QLabel>
|
|||
#include <QMouseEvent>
|
|||
#include <QPushButton>
|
|||
#include <QScrollArea>
|
|||
#include <QSpinBox>
|
|||
|
|||
#include "citra_qt/debugger/graphics_surface.h"
|
|||
#include "citra_qt/util/spinbox.h"
|
|||
|
|||
#include "common/color.h"
|
|||
|
|||
#include "core/memory.h"
|
|||
#include "core/hw/gpu.h"
|
|||
|
|||
#include "video_core/pica.h"
|
|||
#include "video_core/pica_state.h"
|
|||
#include "video_core/utils.h"
|
|||
|
|||
SurfacePicture::SurfacePicture(QWidget* parent, GraphicsSurfaceWidget* surface_widget_) : QLabel(parent), surface_widget(surface_widget_) {} |
|||
SurfacePicture::~SurfacePicture() {} |
|||
|
|||
void SurfacePicture::mousePressEvent(QMouseEvent* event) |
|||
{ |
|||
// Only do something while the left mouse button is held down
|
|||
if (!(event->buttons() & Qt::LeftButton)) |
|||
return; |
|||
|
|||
if (pixmap() == nullptr) |
|||
return; |
|||
|
|||
if (surface_widget) |
|||
surface_widget->Pick(event->x() * pixmap()->width() / width(), |
|||
event->y() * pixmap()->height() / height()); |
|||
} |
|||
|
|||
void SurfacePicture::mouseMoveEvent(QMouseEvent* event) |
|||
{ |
|||
// We also want to handle the event if the user moves the mouse while holding down the LMB
|
|||
mousePressEvent(event); |
|||
} |
|||
|
|||
|
|||
GraphicsSurfaceWidget::GraphicsSurfaceWidget(std::shared_ptr<Pica::DebugContext> debug_context, |
|||
QWidget* parent) |
|||
: BreakPointObserverDock(debug_context, tr("Pica Surface Viewer"), parent), |
|||
surface_source(Source::ColorBuffer) |
|||
{ |
|||
setObjectName("PicaSurface"); |
|||
|
|||
surface_source_list = new QComboBox; |
|||
surface_source_list->addItem(tr("Color Buffer")); |
|||
surface_source_list->addItem(tr("Depth Buffer")); |
|||
surface_source_list->addItem(tr("Stencil Buffer")); |
|||
surface_source_list->addItem(tr("Texture 0")); |
|||
surface_source_list->addItem(tr("Texture 1")); |
|||
surface_source_list->addItem(tr("Texture 2")); |
|||
surface_source_list->addItem(tr("Custom")); |
|||
surface_source_list->setCurrentIndex(static_cast<int>(surface_source)); |
|||
|
|||
surface_address_control = new CSpinBox; |
|||
surface_address_control->SetBase(16); |
|||
surface_address_control->SetRange(0, 0xFFFFFFFF); |
|||
surface_address_control->SetPrefix("0x"); |
|||
|
|||
unsigned max_dimension = 16384; // TODO: Find actual maximum
|
|||
|
|||
surface_width_control = new QSpinBox; |
|||
surface_width_control->setRange(0, max_dimension); |
|||
|
|||
surface_height_control = new QSpinBox; |
|||
surface_height_control->setRange(0, max_dimension); |
|||
|
|||
surface_picker_x_control = new QSpinBox; |
|||
surface_picker_x_control->setRange(0, max_dimension - 1); |
|||
|
|||
surface_picker_y_control = new QSpinBox; |
|||
surface_picker_y_control->setRange(0, max_dimension - 1); |
|||
|
|||
surface_format_control = new QComboBox; |
|||
|
|||
// Color formats sorted by Pica texture format index
|
|||
surface_format_control->addItem(tr("RGBA8")); |
|||
surface_format_control->addItem(tr("RGB8")); |
|||
surface_format_control->addItem(tr("RGB5A1")); |
|||
surface_format_control->addItem(tr("RGB565")); |
|||
surface_format_control->addItem(tr("RGBA4")); |
|||
surface_format_control->addItem(tr("IA8")); |
|||
surface_format_control->addItem(tr("RG8")); |
|||
surface_format_control->addItem(tr("I8")); |
|||
surface_format_control->addItem(tr("A8")); |
|||
surface_format_control->addItem(tr("IA4")); |
|||
surface_format_control->addItem(tr("I4")); |
|||
surface_format_control->addItem(tr("A4")); |
|||
surface_format_control->addItem(tr("ETC1")); |
|||
surface_format_control->addItem(tr("ETC1A4")); |
|||
surface_format_control->addItem(tr("D16")); |
|||
surface_format_control->addItem(tr("D24")); |
|||
surface_format_control->addItem(tr("D24X8")); |
|||
surface_format_control->addItem(tr("X24S8")); |
|||
surface_format_control->addItem(tr("Unknown")); |
|||
|
|||
surface_info_label = new QLabel(); |
|||
surface_info_label->setWordWrap(true); |
|||
|
|||
surface_picture_label = new SurfacePicture(0, this); |
|||
surface_picture_label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); |
|||
surface_picture_label->setAlignment(Qt::AlignLeft | Qt::AlignTop); |
|||
surface_picture_label->setScaledContents(false); |
|||
|
|||
auto scroll_area = new QScrollArea(); |
|||
scroll_area->setBackgroundRole(QPalette::Dark); |
|||
scroll_area->setWidgetResizable(false); |
|||
scroll_area->setWidget(surface_picture_label); |
|||
|
|||
save_surface = new QPushButton(QIcon::fromTheme("document-save"), tr("Save")); |
|||
|
|||
// Connections
|
|||
connect(this, SIGNAL(Update()), this, SLOT(OnUpdate())); |
|||
connect(surface_source_list, SIGNAL(currentIndexChanged(int)), this, SLOT(OnSurfaceSourceChanged(int))); |
|||
connect(surface_address_control, SIGNAL(ValueChanged(qint64)), this, SLOT(OnSurfaceAddressChanged(qint64))); |
|||
connect(surface_width_control, SIGNAL(valueChanged(int)), this, SLOT(OnSurfaceWidthChanged(int))); |
|||
connect(surface_height_control, SIGNAL(valueChanged(int)), this, SLOT(OnSurfaceHeightChanged(int))); |
|||
connect(surface_format_control, SIGNAL(currentIndexChanged(int)), this, SLOT(OnSurfaceFormatChanged(int))); |
|||
connect(surface_picker_x_control, SIGNAL(valueChanged(int)), this, SLOT(OnSurfacePickerXChanged(int))); |
|||
connect(surface_picker_y_control, SIGNAL(valueChanged(int)), this, SLOT(OnSurfacePickerYChanged(int))); |
|||
connect(save_surface, SIGNAL(clicked()), this, SLOT(SaveSurface())); |
|||
|
|||
auto main_widget = new QWidget; |
|||
auto main_layout = new QVBoxLayout; |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("Source:"))); |
|||
sub_layout->addWidget(surface_source_list); |
|||
main_layout->addLayout(sub_layout); |
|||
} |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("Physical Address:"))); |
|||
sub_layout->addWidget(surface_address_control); |
|||
main_layout->addLayout(sub_layout); |
|||
} |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("Width:"))); |
|||
sub_layout->addWidget(surface_width_control); |
|||
main_layout->addLayout(sub_layout); |
|||
} |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("Height:"))); |
|||
sub_layout->addWidget(surface_height_control); |
|||
main_layout->addLayout(sub_layout); |
|||
} |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("Format:"))); |
|||
sub_layout->addWidget(surface_format_control); |
|||
main_layout->addLayout(sub_layout); |
|||
} |
|||
main_layout->addWidget(scroll_area); |
|||
|
|||
auto info_layout = new QHBoxLayout; |
|||
{ |
|||
auto xy_layout = new QVBoxLayout; |
|||
{ |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("X:"))); |
|||
sub_layout->addWidget(surface_picker_x_control); |
|||
xy_layout->addLayout(sub_layout); |
|||
} |
|||
{ |
|||
auto sub_layout = new QHBoxLayout; |
|||
sub_layout->addWidget(new QLabel(tr("Y:"))); |
|||
sub_layout->addWidget(surface_picker_y_control); |
|||
xy_layout->addLayout(sub_layout); |
|||
} |
|||
} |
|||
info_layout->addLayout(xy_layout); |
|||
surface_info_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); |
|||
info_layout->addWidget(surface_info_label); |
|||
} |
|||
main_layout->addLayout(info_layout); |
|||
|
|||
main_layout->addWidget(save_surface); |
|||
main_widget->setLayout(main_layout); |
|||
setWidget(main_widget); |
|||
|
|||
// Load current data - TODO: Make sure this works when emulation is not running
|
|||
if (debug_context && debug_context->at_breakpoint) { |
|||
emit Update(); |
|||
widget()->setEnabled(debug_context->at_breakpoint); |
|||
} else { |
|||
widget()->setEnabled(false); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnBreakPointHit(Pica::DebugContext::Event event, void* data) |
|||
{ |
|||
emit Update(); |
|||
widget()->setEnabled(true); |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnResumed() |
|||
{ |
|||
widget()->setEnabled(false); |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfaceSourceChanged(int new_value) |
|||
{ |
|||
surface_source = static_cast<Source>(new_value); |
|||
emit Update(); |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfaceAddressChanged(qint64 new_value) |
|||
{ |
|||
if (surface_address != new_value) { |
|||
surface_address = static_cast<unsigned>(new_value); |
|||
|
|||
surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); |
|||
emit Update(); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfaceWidthChanged(int new_value) |
|||
{ |
|||
if (surface_width != static_cast<unsigned>(new_value)) { |
|||
surface_width = static_cast<unsigned>(new_value); |
|||
|
|||
surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); |
|||
emit Update(); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfaceHeightChanged(int new_value) |
|||
{ |
|||
if (surface_height != static_cast<unsigned>(new_value)) { |
|||
surface_height = static_cast<unsigned>(new_value); |
|||
|
|||
surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); |
|||
emit Update(); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfaceFormatChanged(int new_value) |
|||
{ |
|||
if (surface_format != static_cast<Format>(new_value)) { |
|||
surface_format = static_cast<Format>(new_value); |
|||
|
|||
surface_source_list->setCurrentIndex(static_cast<int>(Source::Custom)); |
|||
emit Update(); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfacePickerXChanged(int new_value) |
|||
{ |
|||
if (surface_picker_x != new_value) { |
|||
surface_picker_x = new_value; |
|||
Pick(surface_picker_x, surface_picker_y); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnSurfacePickerYChanged(int new_value) |
|||
{ |
|||
if (surface_picker_y != new_value) { |
|||
surface_picker_y = new_value; |
|||
Pick(surface_picker_x, surface_picker_y); |
|||
} |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::Pick(int x, int y) |
|||
{ |
|||
surface_picker_x_control->setValue(x); |
|||
surface_picker_y_control->setValue(y); |
|||
|
|||
if (x < 0 || x >= surface_width || y < 0 || y >= surface_height) { |
|||
surface_info_label->setText(tr("Pixel out of bounds")); |
|||
surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); |
|||
return; |
|||
} |
|||
|
|||
u8* buffer = Memory::GetPhysicalPointer(surface_address); |
|||
if (buffer == nullptr) { |
|||
surface_info_label->setText(tr("(unable to access pixel data)")); |
|||
surface_info_label->setAlignment(Qt::AlignCenter); |
|||
return; |
|||
} |
|||
|
|||
unsigned nibbles_per_pixel = GraphicsSurfaceWidget::NibblesPerPixel(surface_format); |
|||
unsigned stride = nibbles_per_pixel * surface_width / 2; |
|||
|
|||
unsigned bytes_per_pixel; |
|||
bool nibble_mode = (nibbles_per_pixel == 1); |
|||
if (nibble_mode) { |
|||
// As nibbles are contained in a byte we still need to access one byte per nibble
|
|||
bytes_per_pixel = 1; |
|||
} else { |
|||
bytes_per_pixel = nibbles_per_pixel / 2; |
|||
} |
|||
|
|||
const u32 coarse_y = y & ~7; |
|||
u32 offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * stride; |
|||
const u8* pixel = buffer + (nibble_mode ? (offset / 2) : offset); |
|||
|
|||
auto GetText = [offset](Format format, const u8* pixel) { |
|||
switch (format) { |
|||
case Format::RGBA8: |
|||
{ |
|||
auto value = Color::DecodeRGBA8(pixel) / 255.0f; |
|||
return QString("Red: %1, Green: %2, Blue: %3, Alpha: %4") |
|||
.arg(QString::number(value.r(), 'f', 2)) |
|||
.arg(QString::number(value.g(), 'f', 2)) |
|||
.arg(QString::number(value.b(), 'f', 2)) |
|||
.arg(QString::number(value.a(), 'f', 2)); |
|||
} |
|||
case Format::RGB8: |
|||
{ |
|||
auto value = Color::DecodeRGB8(pixel) / 255.0f; |
|||
return QString("Red: %1, Green: %2, Blue: %3") |
|||
.arg(QString::number(value.r(), 'f', 2)) |
|||
.arg(QString::number(value.g(), 'f', 2)) |
|||
.arg(QString::number(value.b(), 'f', 2)); |
|||
} |
|||
case Format::RGB5A1: |
|||
{ |
|||
auto value = Color::DecodeRGB5A1(pixel) / 255.0f; |
|||
return QString("Red: %1, Green: %2, Blue: %3, Alpha: %4") |
|||
.arg(QString::number(value.r(), 'f', 2)) |
|||
.arg(QString::number(value.g(), 'f', 2)) |
|||
.arg(QString::number(value.b(), 'f', 2)) |
|||
.arg(QString::number(value.a(), 'f', 2)); |
|||
} |
|||
case Format::RGB565: |
|||
{ |
|||
auto value = Color::DecodeRGB565(pixel) / 255.0f; |
|||
return QString("Red: %1, Green: %2, Blue: %3") |
|||
.arg(QString::number(value.r(), 'f', 2)) |
|||
.arg(QString::number(value.g(), 'f', 2)) |
|||
.arg(QString::number(value.b(), 'f', 2)); |
|||
} |
|||
case Format::RGBA4: |
|||
{ |
|||
auto value = Color::DecodeRGBA4(pixel) / 255.0f; |
|||
return QString("Red: %1, Green: %2, Blue: %3, Alpha: %4") |
|||
.arg(QString::number(value.r(), 'f', 2)) |
|||
.arg(QString::number(value.g(), 'f', 2)) |
|||
.arg(QString::number(value.b(), 'f', 2)) |
|||
.arg(QString::number(value.a(), 'f', 2)); |
|||
} |
|||
case Format::IA8: |
|||
return QString("Index: %1, Alpha: %2") |
|||
.arg(pixel[0]) |
|||
.arg(pixel[1]); |
|||
case Format::RG8: { |
|||
auto value = Color::DecodeRG8(pixel) / 255.0f; |
|||
return QString("Red: %1, Green: %2") |
|||
.arg(QString::number(value.r(), 'f', 2)) |
|||
.arg(QString::number(value.g(), 'f', 2)); |
|||
} |
|||
case Format::I8: |
|||
return QString("Index: %1").arg(*pixel); |
|||
case Format::A8: |
|||
return QString("Alpha: %1").arg(QString::number(*pixel / 255.0f, 'f', 2)); |
|||
case Format::IA4: |
|||
return QString("Index: %1, Alpha: %2") |
|||
.arg(*pixel & 0xF) |
|||
.arg((*pixel & 0xF0) >> 4); |
|||
case Format::I4: |
|||
{ |
|||
u8 i = (*pixel >> ((offset % 2) ? 4 : 0)) & 0xF; |
|||
return QString("Index: %1").arg(i); |
|||
} |
|||
case Format::A4: |
|||
{ |
|||
u8 a = (*pixel >> ((offset % 2) ? 4 : 0)) & 0xF; |
|||
return QString("Alpha: %1").arg(QString::number(a / 15.0f, 'f', 2)); |
|||
} |
|||
case Format::ETC1: |
|||
case Format::ETC1A4: |
|||
// TODO: Display block information or channel values?
|
|||
return QString("Compressed data"); |
|||
case Format::D16: |
|||
{ |
|||
auto value = Color::DecodeD16(pixel); |
|||
return QString("Depth: %1").arg(QString::number(value / (float)0xFFFF, 'f', 4)); |
|||
} |
|||
case Format::D24: |
|||
{ |
|||
auto value = Color::DecodeD24(pixel); |
|||
return QString("Depth: %1").arg(QString::number(value / (float)0xFFFFFF, 'f', 4)); |
|||
} |
|||
case Format::D24X8: |
|||
case Format::X24S8: |
|||
{ |
|||
auto values = Color::DecodeD24S8(pixel); |
|||
return QString("Depth: %1, Stencil: %2").arg(QString::number(values[0] / (float)0xFFFFFF, 'f', 4)).arg(values[1]); |
|||
} |
|||
case Format::Unknown: |
|||
return QString("Unknown format"); |
|||
default: |
|||
return QString("Unhandled format"); |
|||
} |
|||
return QString(""); |
|||
}; |
|||
|
|||
QString nibbles = ""; |
|||
for (unsigned i = 0; i < nibbles_per_pixel; i++) { |
|||
unsigned nibble_index = i; |
|||
if (nibble_mode) { |
|||
nibble_index += (offset % 2) ? 0 : 1; |
|||
} |
|||
u8 byte = pixel[nibble_index / 2]; |
|||
u8 nibble = (byte >> ((nibble_index % 2) ? 0 : 4)) & 0xF; |
|||
nibbles.append(QString::number(nibble, 16).toUpper()); |
|||
} |
|||
|
|||
surface_info_label->setText(QString("Raw: 0x%3\n(%4)").arg(nibbles).arg(GetText(surface_format, pixel))); |
|||
surface_info_label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::OnUpdate() |
|||
{ |
|||
QPixmap pixmap; |
|||
|
|||
switch (surface_source) { |
|||
case Source::ColorBuffer: |
|||
{ |
|||
// TODO: Store a reference to the registers in the debug context instead of accessing them directly...
|
|||
|
|||
const auto& framebuffer = Pica::g_state.regs.framebuffer; |
|||
|
|||
surface_address = framebuffer.GetColorBufferPhysicalAddress(); |
|||
surface_width = framebuffer.GetWidth(); |
|||
surface_height = framebuffer.GetHeight(); |
|||
|
|||
switch (framebuffer.color_format) { |
|||
case Pica::Regs::ColorFormat::RGBA8: |
|||
surface_format = Format::RGBA8; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGB8: |
|||
surface_format = Format::RGB8; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGB5A1: |
|||
surface_format = Format::RGB5A1; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGB565: |
|||
surface_format = Format::RGB565; |
|||
break; |
|||
|
|||
case Pica::Regs::ColorFormat::RGBA4: |
|||
surface_format = Format::RGBA4; |
|||
break; |
|||
|
|||
default: |
|||
surface_format = Format::Unknown; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case Source::DepthBuffer: |
|||
{ |
|||
const auto& framebuffer = Pica::g_state.regs.framebuffer; |
|||
|
|||
surface_address = framebuffer.GetDepthBufferPhysicalAddress(); |
|||
surface_width = framebuffer.GetWidth(); |
|||
surface_height = framebuffer.GetHeight(); |
|||
|
|||
switch (framebuffer.depth_format) { |
|||
case Pica::Regs::DepthFormat::D16: |
|||
surface_format = Format::D16; |
|||
break; |
|||
|
|||
case Pica::Regs::DepthFormat::D24: |
|||
surface_format = Format::D24; |
|||
break; |
|||
|
|||
case Pica::Regs::DepthFormat::D24S8: |
|||
surface_format = Format::D24X8; |
|||
break; |
|||
|
|||
default: |
|||
surface_format = Format::Unknown; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case Source::StencilBuffer: |
|||
{ |
|||
const auto& framebuffer = Pica::g_state.regs.framebuffer; |
|||
|
|||
surface_address = framebuffer.GetDepthBufferPhysicalAddress(); |
|||
surface_width = framebuffer.GetWidth(); |
|||
surface_height = framebuffer.GetHeight(); |
|||
|
|||
switch (framebuffer.depth_format) { |
|||
case Pica::Regs::DepthFormat::D24S8: |
|||
surface_format = Format::X24S8; |
|||
break; |
|||
|
|||
default: |
|||
surface_format = Format::Unknown; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case Source::Texture0: |
|||
case Source::Texture1: |
|||
case Source::Texture2: |
|||
{ |
|||
unsigned texture_index; |
|||
if (surface_source == Source::Texture0) texture_index = 0; |
|||
else if (surface_source == Source::Texture1) texture_index = 1; |
|||
else if (surface_source == Source::Texture2) texture_index = 2; |
|||
else { |
|||
qDebug() << "Unknown texture source " << static_cast<int>(surface_source); |
|||
break; |
|||
} |
|||
|
|||
const auto texture = Pica::g_state.regs.GetTextures()[texture_index]; |
|||
auto info = Pica::DebugUtils::TextureInfo::FromPicaRegister(texture.config, texture.format); |
|||
|
|||
surface_address = info.physical_address; |
|||
surface_width = info.width; |
|||
surface_height = info.height; |
|||
surface_format = static_cast<Format>(info.format); |
|||
|
|||
if (surface_format > Format::MaxTextureFormat) { |
|||
qDebug() << "Unknown texture format " << static_cast<int>(info.format); |
|||
} |
|||
break; |
|||
} |
|||
|
|||
case Source::Custom: |
|||
{ |
|||
// Keep user-specified values
|
|||
break; |
|||
} |
|||
|
|||
default: |
|||
qDebug() << "Unknown surface source " << static_cast<int>(surface_source); |
|||
break; |
|||
} |
|||
|
|||
surface_address_control->SetValue(surface_address); |
|||
surface_width_control->setValue(surface_width); |
|||
surface_height_control->setValue(surface_height); |
|||
surface_format_control->setCurrentIndex(static_cast<int>(surface_format)); |
|||
|
|||
// TODO: Implement a good way to visualize alpha components!
|
|||
|
|||
QImage decoded_image(surface_width, surface_height, QImage::Format_ARGB32); |
|||
u8* buffer = Memory::GetPhysicalPointer(surface_address); |
|||
|
|||
if (buffer == nullptr) { |
|||
surface_picture_label->hide(); |
|||
surface_info_label->setText(tr("(invalid surface address)")); |
|||
surface_info_label->setAlignment(Qt::AlignCenter); |
|||
surface_picker_x_control->setEnabled(false); |
|||
surface_picker_y_control->setEnabled(false); |
|||
save_surface->setEnabled(false); |
|||
return; |
|||
} |
|||
|
|||
if (surface_format == Format::Unknown) { |
|||
surface_picture_label->hide(); |
|||
surface_info_label->setText(tr("(unknown surface format)")); |
|||
surface_info_label->setAlignment(Qt::AlignCenter); |
|||
surface_picker_x_control->setEnabled(false); |
|||
surface_picker_y_control->setEnabled(false); |
|||
save_surface->setEnabled(false); |
|||
return; |
|||
} |
|||
|
|||
surface_picture_label->show(); |
|||
|
|||
unsigned nibbles_per_pixel = GraphicsSurfaceWidget::NibblesPerPixel(surface_format); |
|||
unsigned stride = nibbles_per_pixel * surface_width / 2; |
|||
|
|||
// We handle depth formats here because DebugUtils only supports TextureFormats
|
|||
if (surface_format <= Format::MaxTextureFormat) { |
|||
|
|||
// Generate a virtual texture
|
|||
Pica::DebugUtils::TextureInfo info; |
|||
info.physical_address = surface_address; |
|||
info.width = surface_width; |
|||
info.height = surface_height; |
|||
info.format = static_cast<Pica::Regs::TextureFormat>(surface_format); |
|||
info.stride = stride; |
|||
|
|||
for (unsigned int y = 0; y < surface_height; ++y) { |
|||
for (unsigned int x = 0; x < surface_width; ++x) { |
|||
Math::Vec4<u8> color = Pica::DebugUtils::LookupTexture(buffer, x, y, info, true); |
|||
decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), color.a())); |
|||
} |
|||
} |
|||
|
|||
} else { |
|||
|
|||
ASSERT_MSG(nibbles_per_pixel >= 2, "Depth decoder only supports formats with at least one byte per pixel"); |
|||
unsigned bytes_per_pixel = nibbles_per_pixel / 2; |
|||
|
|||
for (unsigned int y = 0; y < surface_height; ++y) { |
|||
for (unsigned int x = 0; x < surface_width; ++x) { |
|||
const u32 coarse_y = y & ~7; |
|||
u32 offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * stride; |
|||
const u8* pixel = buffer + offset; |
|||
Math::Vec4<u8> color = { 0, 0, 0, 0 }; |
|||
|
|||
switch(surface_format) { |
|||
case Format::D16: |
|||
{ |
|||
u32 data = Color::DecodeD16(pixel); |
|||
color.r() = data & 0xFF; |
|||
color.g() = (data >> 8) & 0xFF; |
|||
break; |
|||
} |
|||
case Format::D24: |
|||
{ |
|||
u32 data = Color::DecodeD24(pixel); |
|||
color.r() = data & 0xFF; |
|||
color.g() = (data >> 8) & 0xFF; |
|||
color.b() = (data >> 16) & 0xFF; |
|||
break; |
|||
} |
|||
case Format::D24X8: |
|||
{ |
|||
Math::Vec2<u32> data = Color::DecodeD24S8(pixel); |
|||
color.r() = data.x & 0xFF; |
|||
color.g() = (data.x >> 8) & 0xFF; |
|||
color.b() = (data.x >> 16) & 0xFF; |
|||
break; |
|||
} |
|||
case Format::X24S8: |
|||
{ |
|||
Math::Vec2<u32> data = Color::DecodeD24S8(pixel); |
|||
color.r() = color.g() = color.b() = data.y; |
|||
break; |
|||
} |
|||
default: |
|||
qDebug() << "Unknown surface format " << static_cast<int>(surface_format); |
|||
break; |
|||
} |
|||
|
|||
decoded_image.setPixel(x, y, qRgba(color.r(), color.g(), color.b(), 255)); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
pixmap = QPixmap::fromImage(decoded_image); |
|||
surface_picture_label->setPixmap(pixmap); |
|||
surface_picture_label->resize(pixmap.size()); |
|||
|
|||
// Update the info with pixel data
|
|||
surface_picker_x_control->setEnabled(true); |
|||
surface_picker_y_control->setEnabled(true); |
|||
Pick(surface_picker_x, surface_picker_y); |
|||
|
|||
// Enable saving the converted pixmap to file
|
|||
save_surface->setEnabled(true); |
|||
} |
|||
|
|||
void GraphicsSurfaceWidget::SaveSurface() { |
|||
QString png_filter = tr("Portable Network Graphic (*.png)"); |
|||
QString bin_filter = tr("Binary data (*.bin)"); |
|||
|
|||
QString selectedFilter; |
|||
QString filename = QFileDialog::getSaveFileName(this, tr("Save Surface"), QString("texture-0x%1.png").arg(QString::number(surface_address, 16)), |
|||
QString("%1;;%2").arg(png_filter, bin_filter), &selectedFilter); |
|||
|
|||
if (filename.isEmpty()) { |
|||
// If the user canceled the dialog, don't save anything.
|
|||
return; |
|||
} |
|||
|
|||
if (selectedFilter == png_filter) { |
|||
const QPixmap* pixmap = surface_picture_label->pixmap(); |
|||
ASSERT_MSG(pixmap != nullptr, "No pixmap set"); |
|||
|
|||
QFile file(filename); |
|||
file.open(QIODevice::WriteOnly); |
|||
if (pixmap) |
|||
pixmap->save(&file, "PNG"); |
|||
} else if (selectedFilter == bin_filter) { |
|||
const u8* buffer = Memory::GetPhysicalPointer(surface_address); |
|||
ASSERT_MSG(buffer != nullptr, "Memory not accessible"); |
|||
|
|||
QFile file(filename); |
|||
file.open(QIODevice::WriteOnly); |
|||
int size = surface_width * surface_height * NibblesPerPixel(surface_format) / 2; |
|||
QByteArray data(reinterpret_cast<const char*>(buffer), size); |
|||
file.write(data); |
|||
} else { |
|||
UNREACHABLE_MSG("Unhandled filter selected"); |
|||
} |
|||
} |
|||
|
|||
unsigned int GraphicsSurfaceWidget::NibblesPerPixel(GraphicsSurfaceWidget::Format format) { |
|||
if (format <= Format::MaxTextureFormat) { |
|||
return Pica::Regs::NibblesPerPixel(static_cast<Pica::Regs::TextureFormat>(format)); |
|||
} |
|||
|
|||
switch (format) { |
|||
case Format::D24X8: |
|||
case Format::X24S8: |
|||
return 4 * 2; |
|||
case Format::D24: |
|||
return 3 * 2; |
|||
case Format::D16: |
|||
return 2 * 2; |
|||
default: |
|||
UNREACHABLE_MSG("GraphicsSurfaceWidget::BytesPerPixel: this " |
|||
"should not be reached as this function should " |
|||
"be given a format which is in " |
|||
"GraphicsSurfaceWidget::Format. Instead got %i", |
|||
static_cast<int>(format)); |
|||
return 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
// Copyright 2014 Citra Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include "citra_qt/debugger/graphics_breakpoint_observer.h" |
|||
|
|||
#include <QLabel> |
|||
#include <QPushButton> |
|||
|
|||
class QComboBox; |
|||
class QSpinBox; |
|||
class CSpinBox; |
|||
|
|||
class GraphicsSurfaceWidget; |
|||
|
|||
class SurfacePicture : public QLabel |
|||
{ |
|||
Q_OBJECT |
|||
|
|||
public: |
|||
SurfacePicture(QWidget* parent = 0, GraphicsSurfaceWidget* surface_widget = nullptr); |
|||
~SurfacePicture(); |
|||
|
|||
protected slots: |
|||
virtual void mouseMoveEvent(QMouseEvent* event); |
|||
virtual void mousePressEvent(QMouseEvent* event); |
|||
|
|||
private: |
|||
GraphicsSurfaceWidget* surface_widget; |
|||
|
|||
}; |
|||
|
|||
class GraphicsSurfaceWidget : public BreakPointObserverDock { |
|||
Q_OBJECT |
|||
|
|||
using Event = Pica::DebugContext::Event; |
|||
|
|||
enum class Source { |
|||
ColorBuffer = 0, |
|||
DepthBuffer = 1, |
|||
StencilBuffer = 2, |
|||
Texture0 = 3, |
|||
Texture1 = 4, |
|||
Texture2 = 5, |
|||
Custom = 6, |
|||
}; |
|||
|
|||
enum class Format { |
|||
// These must match the TextureFormat type! |
|||
RGBA8 = 0, |
|||
RGB8 = 1, |
|||
RGB5A1 = 2, |
|||
RGB565 = 3, |
|||
RGBA4 = 4, |
|||
IA8 = 5, |
|||
RG8 = 6, ///< @note Also called HILO8 in 3DBrew. |
|||
I8 = 7, |
|||
A8 = 8, |
|||
IA4 = 9, |
|||
I4 = 10, |
|||
A4 = 11, |
|||
ETC1 = 12, // compressed |
|||
ETC1A4 = 13, |
|||
MaxTextureFormat = 13, |
|||
D16 = 14, |
|||
D24 = 15, |
|||
D24X8 = 16, |
|||
X24S8 = 17, |
|||
Unknown = 18, |
|||
}; |
|||
|
|||
static unsigned int NibblesPerPixel(Format format); |
|||
|
|||
public: |
|||
GraphicsSurfaceWidget(std::shared_ptr<Pica::DebugContext> debug_context, QWidget* parent = nullptr); |
|||
void Pick(int x, int y); |
|||
|
|||
public slots: |
|||
void OnSurfaceSourceChanged(int new_value); |
|||
void OnSurfaceAddressChanged(qint64 new_value); |
|||
void OnSurfaceWidthChanged(int new_value); |
|||
void OnSurfaceHeightChanged(int new_value); |
|||
void OnSurfaceFormatChanged(int new_value); |
|||
void OnSurfacePickerXChanged(int new_value); |
|||
void OnSurfacePickerYChanged(int new_value); |
|||
void OnUpdate(); |
|||
|
|||
private slots: |
|||
void OnBreakPointHit(Pica::DebugContext::Event event, void* data) override; |
|||
void OnResumed() override; |
|||
|
|||
void SaveSurface(); |
|||
|
|||
signals: |
|||
void Update(); |
|||
|
|||
private: |
|||
|
|||
QComboBox* surface_source_list; |
|||
CSpinBox* surface_address_control; |
|||
QSpinBox* surface_width_control; |
|||
QSpinBox* surface_height_control; |
|||
QComboBox* surface_format_control; |
|||
|
|||
SurfacePicture* surface_picture_label; |
|||
QSpinBox* surface_picker_x_control; |
|||
QSpinBox* surface_picker_y_control; |
|||
QLabel* surface_info_label; |
|||
QPushButton* save_surface; |
|||
|
|||
Source surface_source; |
|||
unsigned surface_address; |
|||
unsigned surface_width; |
|||
unsigned surface_height; |
|||
Format surface_format; |
|||
int surface_picker_x = 0; |
|||
int surface_picker_y = 0; |
|||
}; |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue