Merge branch 'master' into feature/savestates-2
commit
828f88d20a
@ -0,0 +1,220 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include "citra_qt/dumping/dumping_dialog.h"
|
||||||
|
#include "citra_qt/dumping/options_dialog.h"
|
||||||
|
#include "citra_qt/uisettings.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "ui_dumping_dialog.h"
|
||||||
|
|
||||||
|
DumpingDialog::DumpingDialog(QWidget* parent)
|
||||||
|
: QDialog(parent), ui(std::make_unique<Ui::DumpingDialog>()) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
format_generic_options = VideoDumper::GetFormatGenericOptions();
|
||||||
|
encoder_generic_options = VideoDumper::GetEncoderGenericOptions();
|
||||||
|
|
||||||
|
connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] {
|
||||||
|
if (ui->pathLineEdit->text().isEmpty()) {
|
||||||
|
QMessageBox::critical(this, tr("Citra"), tr("Please specify the output path."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApplyConfiguration();
|
||||||
|
accept();
|
||||||
|
});
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject);
|
||||||
|
connect(ui->formatOptionsButton, &QToolButton::clicked, [this] {
|
||||||
|
OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options,
|
||||||
|
format_generic_options, ui->formatOptionsLineEdit);
|
||||||
|
});
|
||||||
|
connect(ui->videoEncoderOptionsButton, &QToolButton::clicked, [this] {
|
||||||
|
OpenOptionsDialog(
|
||||||
|
video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options,
|
||||||
|
encoder_generic_options, ui->videoEncoderOptionsLineEdit);
|
||||||
|
});
|
||||||
|
connect(ui->audioEncoderOptionsButton, &QToolButton::clicked, [this] {
|
||||||
|
OpenOptionsDialog(
|
||||||
|
audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options,
|
||||||
|
encoder_generic_options, ui->audioEncoderOptionsLineEdit);
|
||||||
|
});
|
||||||
|
|
||||||
|
SetConfiguration();
|
||||||
|
|
||||||
|
connect(ui->formatComboBox, qOverload<int>(&QComboBox::currentIndexChanged), [this] {
|
||||||
|
ui->pathLineEdit->setText(QString{});
|
||||||
|
ui->formatOptionsLineEdit->clear();
|
||||||
|
PopulateEncoders();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ui->videoEncoderComboBox, qOverload<int>(&QComboBox::currentIndexChanged),
|
||||||
|
[this] { ui->videoEncoderOptionsLineEdit->clear(); });
|
||||||
|
connect(ui->audioEncoderComboBox, qOverload<int>(&QComboBox::currentIndexChanged),
|
||||||
|
[this] { ui->audioEncoderOptionsLineEdit->clear(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpingDialog::~DumpingDialog() = default;
|
||||||
|
|
||||||
|
QString DumpingDialog::GetFilePath() const {
|
||||||
|
return ui->pathLineEdit->text();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::Populate() {
|
||||||
|
formats = VideoDumper::ListFormats();
|
||||||
|
video_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_VIDEO);
|
||||||
|
audio_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_AUDIO);
|
||||||
|
|
||||||
|
// Check that these are not empty
|
||||||
|
QString missing;
|
||||||
|
if (formats.empty()) {
|
||||||
|
missing = tr("output formats");
|
||||||
|
}
|
||||||
|
if (video_encoders.empty()) {
|
||||||
|
missing = tr("video encoders");
|
||||||
|
}
|
||||||
|
if (audio_encoders.empty()) {
|
||||||
|
missing = tr("audio encoders");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missing.isEmpty()) {
|
||||||
|
QMessageBox::critical(this, tr("Citra"),
|
||||||
|
tr("Could not find any available %1.\nPlease check your FFmpeg "
|
||||||
|
"installation used for compilation.")
|
||||||
|
.arg(missing));
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate formats
|
||||||
|
for (std::size_t i = 0; i < formats.size(); ++i) {
|
||||||
|
const auto& format = formats[i];
|
||||||
|
|
||||||
|
// Check format: only formats that have video encoders and audio encoders are displayed
|
||||||
|
bool has_video = false;
|
||||||
|
for (const auto& video_encoder : video_encoders) {
|
||||||
|
if (format.supported_video_codecs.count(video_encoder.codec)) {
|
||||||
|
has_video = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!has_video)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool has_audio = false;
|
||||||
|
for (const auto& audio_encoder : audio_encoders) {
|
||||||
|
if (format.supported_audio_codecs.count(audio_encoder.codec)) {
|
||||||
|
has_audio = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!has_audio)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ui->formatComboBox->addItem(tr("%1 (%2)").arg(QString::fromStdString(format.long_name),
|
||||||
|
QString::fromStdString(format.name)),
|
||||||
|
static_cast<unsigned long long>(i));
|
||||||
|
if (format.name == Settings::values.output_format) {
|
||||||
|
ui->formatComboBox->setCurrentIndex(ui->formatComboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PopulateEncoders();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::PopulateEncoders() {
|
||||||
|
const auto& format = formats.at(ui->formatComboBox->currentData().toUInt());
|
||||||
|
|
||||||
|
ui->videoEncoderComboBox->clear();
|
||||||
|
for (std::size_t i = 0; i < video_encoders.size(); ++i) {
|
||||||
|
const auto& video_encoder = video_encoders[i];
|
||||||
|
if (!format.supported_video_codecs.count(video_encoder.codec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->videoEncoderComboBox->addItem(
|
||||||
|
tr("%1 (%2)").arg(QString::fromStdString(video_encoder.long_name),
|
||||||
|
QString::fromStdString(video_encoder.name)),
|
||||||
|
static_cast<unsigned long long>(i));
|
||||||
|
if (video_encoder.name == Settings::values.video_encoder) {
|
||||||
|
ui->videoEncoderComboBox->setCurrentIndex(ui->videoEncoderComboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->audioEncoderComboBox->clear();
|
||||||
|
for (std::size_t i = 0; i < audio_encoders.size(); ++i) {
|
||||||
|
const auto& audio_encoder = audio_encoders[i];
|
||||||
|
if (!format.supported_audio_codecs.count(audio_encoder.codec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->audioEncoderComboBox->addItem(
|
||||||
|
tr("%1 (%2)").arg(QString::fromStdString(audio_encoder.long_name),
|
||||||
|
QString::fromStdString(audio_encoder.name)),
|
||||||
|
static_cast<unsigned long long>(i));
|
||||||
|
if (audio_encoder.name == Settings::values.audio_encoder) {
|
||||||
|
ui->audioEncoderComboBox->setCurrentIndex(ui->audioEncoderComboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::OnToolButtonClicked() {
|
||||||
|
const auto& format = formats.at(ui->formatComboBox->currentData().toUInt());
|
||||||
|
|
||||||
|
QString extensions;
|
||||||
|
for (const auto& ext : format.extensions) {
|
||||||
|
if (!extensions.isEmpty()) {
|
||||||
|
extensions.append(QLatin1Char{' '});
|
||||||
|
}
|
||||||
|
extensions.append(QStringLiteral("*.%1").arg(QString::fromStdString(ext)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto path = QFileDialog::getSaveFileName(
|
||||||
|
this, tr("Select Video Output Path"), last_path,
|
||||||
|
tr("%1 (%2)").arg(QString::fromStdString(format.long_name), extensions));
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
last_path = QFileInfo(ui->pathLineEdit->text()).path();
|
||||||
|
ui->pathLineEdit->setText(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::OpenOptionsDialog(const std::vector<VideoDumper::OptionInfo>& specific_options,
|
||||||
|
const std::vector<VideoDumper::OptionInfo>& generic_options,
|
||||||
|
QLineEdit* line_edit) {
|
||||||
|
OptionsDialog dialog(this, specific_options, generic_options, line_edit->text().toStdString());
|
||||||
|
if (dialog.exec() != QDialog::DialogCode::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit->setText(QString::fromStdString(dialog.GetCurrentValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::SetConfiguration() {
|
||||||
|
Populate();
|
||||||
|
|
||||||
|
ui->formatOptionsLineEdit->setText(QString::fromStdString(Settings::values.format_options));
|
||||||
|
ui->videoEncoderOptionsLineEdit->setText(
|
||||||
|
QString::fromStdString(Settings::values.video_encoder_options));
|
||||||
|
ui->audioEncoderOptionsLineEdit->setText(
|
||||||
|
QString::fromStdString(Settings::values.audio_encoder_options));
|
||||||
|
last_path = UISettings::values.video_dumping_path;
|
||||||
|
ui->videoBitrateSpinBox->setValue(static_cast<int>(Settings::values.video_bitrate));
|
||||||
|
ui->audioBitrateSpinBox->setValue(static_cast<int>(Settings::values.audio_bitrate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpingDialog::ApplyConfiguration() {
|
||||||
|
Settings::values.output_format = formats.at(ui->formatComboBox->currentData().toUInt()).name;
|
||||||
|
Settings::values.format_options = ui->formatOptionsLineEdit->text().toStdString();
|
||||||
|
Settings::values.video_encoder =
|
||||||
|
video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).name;
|
||||||
|
Settings::values.video_encoder_options = ui->videoEncoderOptionsLineEdit->text().toStdString();
|
||||||
|
Settings::values.video_bitrate = ui->videoBitrateSpinBox->value();
|
||||||
|
Settings::values.audio_encoder =
|
||||||
|
audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).name;
|
||||||
|
Settings::values.audio_encoder_options = ui->audioEncoderOptionsLineEdit->text().toStdString();
|
||||||
|
Settings::values.audio_bitrate = ui->audioBitrateSpinBox->value();
|
||||||
|
UISettings::values.video_dumping_path = last_path;
|
||||||
|
Settings::Apply();
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class DumpingDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
|
||||||
|
class DumpingDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DumpingDialog(QWidget* parent);
|
||||||
|
~DumpingDialog() override;
|
||||||
|
|
||||||
|
QString GetFilePath() const;
|
||||||
|
void ApplyConfiguration();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Populate();
|
||||||
|
void PopulateEncoders();
|
||||||
|
void SetConfiguration();
|
||||||
|
void OnToolButtonClicked();
|
||||||
|
void OpenOptionsDialog(const std::vector<VideoDumper::OptionInfo>& specific_options,
|
||||||
|
const std::vector<VideoDumper::OptionInfo>& generic_options,
|
||||||
|
QLineEdit* line_edit);
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::DumpingDialog> ui;
|
||||||
|
|
||||||
|
QString last_path;
|
||||||
|
|
||||||
|
std::vector<VideoDumper::FormatInfo> formats;
|
||||||
|
std::vector<VideoDumper::OptionInfo> format_generic_options;
|
||||||
|
std::vector<VideoDumper::EncoderInfo> video_encoders;
|
||||||
|
std::vector<VideoDumper::EncoderInfo> audio_encoders;
|
||||||
|
std::vector<VideoDumper::OptionInfo> encoder_generic_options;
|
||||||
|
};
|
@ -0,0 +1,213 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>DumpingDialog</class>
|
||||||
|
<widget class="QDialog" name="DumpingDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>600</width>
|
||||||
|
<height>420</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dump Video</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Output</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Format:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="formatComboBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="formatOptionsLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="formatOptionsButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Path:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLineEdit" name="pathLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QToolButton" name="pathExplore">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Video</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Encoder:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="videoEncoderComboBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="videoEncoderOptionsLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="videoEncoderOptionsButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Bitrate:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QSpinBox" name="videoBitrateSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>10000000</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>1000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>bps</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Audio</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Encoder:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="audioEncoderComboBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="audioEncoderOptionsLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="audioEncoderOptionsButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Bitrate:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QSpinBox" name="audioBitrateSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1000000</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>bps</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
@ -0,0 +1,299 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QStringList>
|
||||||
|
#include "citra_qt/dumping/option_set_dialog.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "ui_option_set_dialog.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavutil/pixdesc.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::unordered_map<AVOptionType, const char*> TypeNameMap{{
|
||||||
|
{AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")},
|
||||||
|
{AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")},
|
||||||
|
{AV_OPT_TYPE_DURATION, QT_TR_NOOP("duration")},
|
||||||
|
{AV_OPT_TYPE_INT, QT_TR_NOOP("int")},
|
||||||
|
{AV_OPT_TYPE_UINT64, QT_TR_NOOP("uint64")},
|
||||||
|
{AV_OPT_TYPE_INT64, QT_TR_NOOP("int64")},
|
||||||
|
{AV_OPT_TYPE_DOUBLE, QT_TR_NOOP("double")},
|
||||||
|
{AV_OPT_TYPE_FLOAT, QT_TR_NOOP("float")},
|
||||||
|
{AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("rational")},
|
||||||
|
{AV_OPT_TYPE_PIXEL_FMT, QT_TR_NOOP("pixel format")},
|
||||||
|
{AV_OPT_TYPE_SAMPLE_FMT, QT_TR_NOOP("sample format")},
|
||||||
|
{AV_OPT_TYPE_COLOR, QT_TR_NOOP("color")},
|
||||||
|
{AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("image size")},
|
||||||
|
{AV_OPT_TYPE_STRING, QT_TR_NOOP("string")},
|
||||||
|
{AV_OPT_TYPE_DICT, QT_TR_NOOP("dictionary")},
|
||||||
|
{AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("video rate")},
|
||||||
|
{AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("channel layout")},
|
||||||
|
}};
|
||||||
|
|
||||||
|
static const std::unordered_map<AVOptionType, const char*> TypeDescriptionMap{{
|
||||||
|
{AV_OPT_TYPE_DURATION, QT_TR_NOOP("[<hours (integer)>:][<minutes (integer):]<seconds "
|
||||||
|
"(decimal)> e.g. 03:00.5 (3min 500ms)")},
|
||||||
|
{AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("<num>/<den>")},
|
||||||
|
{AV_OPT_TYPE_COLOR, QT_TR_NOOP("0xRRGGBBAA")},
|
||||||
|
{AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("<width>x<height>, or preset values like 'vga'.")},
|
||||||
|
{AV_OPT_TYPE_DICT,
|
||||||
|
QT_TR_NOOP("Comma-splitted list of <key>=<value>. Do not put spaces.")},
|
||||||
|
{AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("<num>/<den>, or preset values like 'pal'.")},
|
||||||
|
{AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("Hexadecimal channel layout mask starting with '0x'.")},
|
||||||
|
}};
|
||||||
|
|
||||||
|
/// Get the preset values of an option. returns {display value, real value}
|
||||||
|
std::vector<std::pair<QString, QString>> GetPresetValues(const VideoDumper::OptionInfo& option) {
|
||||||
|
switch (option.type) {
|
||||||
|
case AV_OPT_TYPE_BOOL: {
|
||||||
|
return {{QObject::tr("auto"), QStringLiteral("auto")},
|
||||||
|
{QObject::tr("true"), QStringLiteral("true")},
|
||||||
|
{QObject::tr("false"), QStringLiteral("false")}};
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_PIXEL_FMT: {
|
||||||
|
std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
|
||||||
|
// List all pixel formats
|
||||||
|
const AVPixFmtDescriptor* current = nullptr;
|
||||||
|
while ((current = av_pix_fmt_desc_next(current))) {
|
||||||
|
out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_SAMPLE_FMT: {
|
||||||
|
std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
|
||||||
|
// List all sample formats
|
||||||
|
int current = 0;
|
||||||
|
while (true) {
|
||||||
|
const char* name = av_get_sample_fmt_name(static_cast<AVSampleFormat>(current));
|
||||||
|
if (name == nullptr)
|
||||||
|
break;
|
||||||
|
out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case AV_OPT_TYPE_INT:
|
||||||
|
case AV_OPT_TYPE_INT64:
|
||||||
|
case AV_OPT_TYPE_UINT64: {
|
||||||
|
std::vector<std::pair<QString, QString>> out;
|
||||||
|
// Add in all named constants
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
out.emplace_back(QObject::tr("%1 (0x%2)")
|
||||||
|
.arg(QString::fromStdString(constant.name))
|
||||||
|
.arg(constant.value, 0, 16),
|
||||||
|
QString::fromStdString(constant.name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionSetDialog::InitializeUI(const std::string& initial_value) {
|
||||||
|
const QString type_name =
|
||||||
|
TypeNameMap.count(option.type) ? tr(TypeNameMap.at(option.type)) : tr("unknown");
|
||||||
|
ui->nameLabel->setText(tr("%1 <%2> %3")
|
||||||
|
.arg(QString::fromStdString(option.name), type_name,
|
||||||
|
QString::fromStdString(option.description)));
|
||||||
|
if (TypeDescriptionMap.count(option.type)) {
|
||||||
|
ui->formatLabel->setVisible(true);
|
||||||
|
ui->formatLabel->setText(tr(TypeDescriptionMap.at(option.type)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
|
||||||
|
option.type == AV_OPT_TYPE_UINT64 || option.type == AV_OPT_TYPE_FLOAT ||
|
||||||
|
option.type == AV_OPT_TYPE_DOUBLE || option.type == AV_OPT_TYPE_DURATION ||
|
||||||
|
option.type == AV_OPT_TYPE_RATIONAL) { // scalar types
|
||||||
|
|
||||||
|
ui->formatLabel->setVisible(true);
|
||||||
|
if (!ui->formatLabel->text().isEmpty()) {
|
||||||
|
ui->formatLabel->text().append(QStringLiteral("\n"));
|
||||||
|
}
|
||||||
|
ui->formatLabel->setText(
|
||||||
|
ui->formatLabel->text().append(tr("Range: %1 - %2").arg(option.min).arg(option.max)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide and initialize layout
|
||||||
|
if (option.type == AV_OPT_TYPE_BOOL || option.type == AV_OPT_TYPE_PIXEL_FMT ||
|
||||||
|
option.type == AV_OPT_TYPE_SAMPLE_FMT ||
|
||||||
|
((option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
|
||||||
|
option.type == AV_OPT_TYPE_UINT64) &&
|
||||||
|
!option.named_constants.empty())) { // Use the combobox layout
|
||||||
|
|
||||||
|
layout_type = 1;
|
||||||
|
ui->comboBox->setVisible(true);
|
||||||
|
ui->comboBoxHelpLabel->setVisible(true);
|
||||||
|
|
||||||
|
QString real_initial_value = QString::fromStdString(initial_value);
|
||||||
|
if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
|
||||||
|
option.type == AV_OPT_TYPE_UINT64) {
|
||||||
|
|
||||||
|
// Get the name of the initial value
|
||||||
|
try {
|
||||||
|
s64 initial_value_integer = std::stoll(initial_value, nullptr, 0);
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
if (constant.value == initial_value_integer) {
|
||||||
|
real_initial_value = QString::fromStdString(constant.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Not convertible to integer, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& [display, value] : GetPresetValues(option)) {
|
||||||
|
ui->comboBox->addItem(display, value);
|
||||||
|
if (value == real_initial_value) {
|
||||||
|
found = true;
|
||||||
|
ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui->comboBox->addItem(tr("custom"));
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1);
|
||||||
|
ui->lineEdit->setText(QString::fromStdString(initial_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUIDisplay();
|
||||||
|
|
||||||
|
connect(ui->comboBox, &QComboBox::currentTextChanged, this,
|
||||||
|
&OptionSetDialog::UpdateUIDisplay);
|
||||||
|
} else if (option.type == AV_OPT_TYPE_FLAGS &&
|
||||||
|
!option.named_constants.empty()) { // Use the check boxes layout
|
||||||
|
|
||||||
|
layout_type = 2;
|
||||||
|
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
auto* checkBox = new QCheckBox(tr("%1 (0x%2) %3")
|
||||||
|
.arg(QString::fromStdString(constant.name))
|
||||||
|
.arg(constant.value, 0, 16)
|
||||||
|
.arg(QString::fromStdString(constant.description)));
|
||||||
|
checkBox->setProperty("value", static_cast<unsigned long long>(constant.value));
|
||||||
|
checkBox->setProperty("name", QString::fromStdString(constant.name));
|
||||||
|
ui->checkBoxLayout->addWidget(checkBox);
|
||||||
|
}
|
||||||
|
SetCheckBoxDefaults(initial_value);
|
||||||
|
} else { // Use the line edit layout
|
||||||
|
layout_type = 0;
|
||||||
|
ui->lineEdit->setVisible(true);
|
||||||
|
ui->lineEdit->setText(QString::fromStdString(initial_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) {
|
||||||
|
if (initial_value.size() >= 2 &&
|
||||||
|
(initial_value.substr(0, 2) == "0x" || initial_value.substr(0, 2) == "0X")) {
|
||||||
|
// This is a hex mask
|
||||||
|
try {
|
||||||
|
u64 value = std::stoull(initial_value, nullptr, 16);
|
||||||
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
|
||||||
|
if (checkBox) {
|
||||||
|
checkBox->setChecked(value & checkBox->property("value").toULongLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
LOG_ERROR(Frontend, "Could not convert {} to number", initial_value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a combination of constants, splitted with + or |
|
||||||
|
std::vector<std::string> tmp;
|
||||||
|
Common::SplitString(initial_value, '+', tmp);
|
||||||
|
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::vector<std::string> tmp2;
|
||||||
|
for (const auto& str : tmp) {
|
||||||
|
Common::SplitString(str, '|', tmp2);
|
||||||
|
out.insert(out.end(), tmp2.begin(), tmp2.end());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
|
||||||
|
if (checkBox) {
|
||||||
|
checkBox->setChecked(
|
||||||
|
std::find(out.begin(), out.end(),
|
||||||
|
checkBox->property("name").toString().toStdString()) != out.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionSetDialog::UpdateUIDisplay() {
|
||||||
|
if (layout_type != 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { // custom
|
||||||
|
ui->comboBoxHelpLabel->setVisible(false);
|
||||||
|
ui->lineEdit->setVisible(true);
|
||||||
|
adjustSize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->lineEdit->setVisible(false);
|
||||||
|
for (const auto& constant : option.named_constants) {
|
||||||
|
if (constant.name == ui->comboBox->currentData().toString().toStdString()) {
|
||||||
|
ui->comboBoxHelpLabel->setVisible(true);
|
||||||
|
ui->comboBoxHelpLabel->setText(QString::fromStdString(constant.description));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> OptionSetDialog::GetCurrentValue() {
|
||||||
|
if (!is_set) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (layout_type) {
|
||||||
|
case 0: // line edit layout
|
||||||
|
return {true, ui->lineEdit->text().toStdString()};
|
||||||
|
case 1: // combo box layout
|
||||||
|
if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) {
|
||||||
|
return {true, ui->lineEdit->text().toStdString()}; // custom
|
||||||
|
}
|
||||||
|
return {true, ui->comboBox->currentData().toString().toStdString()};
|
||||||
|
case 2: { // check boxes layout
|
||||||
|
std::string out;
|
||||||
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
|
||||||
|
if (checkBox && checkBox->isChecked()) {
|
||||||
|
if (!out.empty()) {
|
||||||
|
out.append("+");
|
||||||
|
}
|
||||||
|
out.append(checkBox->property("name").toString().toStdString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out.empty()) {
|
||||||
|
out = "0x0";
|
||||||
|
}
|
||||||
|
return {true, out};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionSetDialog::OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option_,
|
||||||
|
const std::string& initial_value)
|
||||||
|
: QDialog(parent), ui(std::make_unique<Ui::OptionSetDialog>()), option(std::move(option_)) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
InitializeUI(initial_value);
|
||||||
|
|
||||||
|
connect(ui->unsetButton, &QPushButton::clicked, [this] {
|
||||||
|
is_set = false;
|
||||||
|
accept();
|
||||||
|
});
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionSetDialog::accept);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionSetDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionSetDialog::~OptionSetDialog() = default;
|
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class OptionSetDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionSetDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option,
|
||||||
|
const std::string& initial_value);
|
||||||
|
~OptionSetDialog() override;
|
||||||
|
|
||||||
|
// {is_set, value}
|
||||||
|
std::pair<bool, std::string> GetCurrentValue();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void InitializeUI(const std::string& initial_value);
|
||||||
|
void SetCheckBoxDefaults(const std::string& initial_value);
|
||||||
|
void UpdateUIDisplay();
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::OptionSetDialog> ui;
|
||||||
|
VideoDumper::OptionInfo option;
|
||||||
|
bool is_set = true;
|
||||||
|
int layout_type = -1; // 0 - line edit, 1 - combo box, 2 - flags (check boxes)
|
||||||
|
};
|
@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>OptionSetDialog</class>
|
||||||
|
<widget class="QDialog" name="OptionSetDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>600</width>
|
||||||
|
<height>150</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Options</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="nameLabel"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="formatLabel">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="comboBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="comboBox">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="comboBoxHelpLabel">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="lineEdit">
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="checkBoxLayout"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="unsetButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Unset</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
#include "citra_qt/dumping/option_set_dialog.h"
|
||||||
|
#include "citra_qt/dumping/options_dialog.h"
|
||||||
|
#include "ui_options_dialog.h"
|
||||||
|
|
||||||
|
constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]");
|
||||||
|
|
||||||
|
void OptionsDialog::PopulateOptions() {
|
||||||
|
const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options;
|
||||||
|
ui->main->clear();
|
||||||
|
ui->main->setSortingEnabled(false);
|
||||||
|
for (std::size_t i = 0; i < options.size(); ++i) {
|
||||||
|
const auto& option = options.at(i);
|
||||||
|
auto* item = new QTreeWidgetItem(
|
||||||
|
{QString::fromStdString(option.name), QString::fromStdString(current_values.Get(
|
||||||
|
option.name, tr(UNSET_TEXT).toStdString()))});
|
||||||
|
item->setData(1, Qt::UserRole, static_cast<unsigned long long>(i)); // ID
|
||||||
|
ui->main->addTopLevelItem(item);
|
||||||
|
}
|
||||||
|
ui->main->setSortingEnabled(true);
|
||||||
|
ui->main->sortItems(0, Qt::AscendingOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsDialog::OnSetOptionValue(QTreeWidgetItem* item) {
|
||||||
|
const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options;
|
||||||
|
const int id = item->data(1, Qt::UserRole).toInt();
|
||||||
|
OptionSetDialog dialog(this, options[id],
|
||||||
|
current_values.Get(options[id].name, options[id].default_value));
|
||||||
|
if (dialog.exec() != QDialog::DialogCode::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& [is_set, value] = dialog.GetCurrentValue();
|
||||||
|
if (is_set) {
|
||||||
|
current_values.Set(options[id].name, value);
|
||||||
|
} else {
|
||||||
|
current_values.Erase(options[id].name);
|
||||||
|
}
|
||||||
|
item->setText(1, is_set ? QString::fromStdString(value) : tr(UNSET_TEXT));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OptionsDialog::GetCurrentValue() const {
|
||||||
|
return current_values.Serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsDialog::OptionsDialog(QWidget* parent,
|
||||||
|
std::vector<VideoDumper::OptionInfo> specific_options_,
|
||||||
|
std::vector<VideoDumper::OptionInfo> generic_options_,
|
||||||
|
const std::string& current_value)
|
||||||
|
: QDialog(parent), ui(std::make_unique<Ui::OptionsDialog>()),
|
||||||
|
specific_options(std::move(specific_options_)), generic_options(std::move(generic_options_)),
|
||||||
|
current_values(current_value) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
PopulateOptions();
|
||||||
|
|
||||||
|
connect(ui->main, &QTreeWidget::itemDoubleClicked,
|
||||||
|
[this](QTreeWidgetItem* item, int column) { OnSetOptionValue(item); });
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionsDialog::accept);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionsDialog::reject);
|
||||||
|
connect(ui->specificRadioButton, &QRadioButton::toggled, this, &OptionsDialog::PopulateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsDialog::~OptionsDialog() = default;
|
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <QDialog>
|
||||||
|
#include "common/param_package.h"
|
||||||
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
|
|
||||||
|
class QTreeWidgetItem;
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class OptionsDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionsDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OptionsDialog(QWidget* parent, std::vector<VideoDumper::OptionInfo> specific_options,
|
||||||
|
std::vector<VideoDumper::OptionInfo> generic_options,
|
||||||
|
const std::string& current_value);
|
||||||
|
~OptionsDialog() override;
|
||||||
|
|
||||||
|
std::string GetCurrentValue() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void PopulateOptions();
|
||||||
|
void OnSetOptionValue(QTreeWidgetItem* item);
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::OptionsDialog> ui;
|
||||||
|
std::vector<VideoDumper::OptionInfo> specific_options;
|
||||||
|
std::vector<VideoDumper::OptionInfo> generic_options;
|
||||||
|
Common::ParamPackage current_values;
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>OptionsDialog</class>
|
||||||
|
<widget class="QDialog" name="OptionsDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>650</width>
|
||||||
|
<height>350</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Options</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel">
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Double click to see the description and change the values of the options.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="specificRadioButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Specific</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="genericRadioButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Generic</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeWidget" name="main">
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Name</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Value</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include "core/frontend/emu_window.h"
|
||||||
|
#include "core/frontend/scope_acquire_context.h"
|
||||||
|
#include "video_core/renderer_opengl/frame_dumper_opengl.h"
|
||||||
|
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
FrameDumperOpenGL::FrameDumperOpenGL(VideoDumper::Backend& video_dumper_,
|
||||||
|
Frontend::EmuWindow& emu_window)
|
||||||
|
: video_dumper(video_dumper_), context(emu_window.CreateSharedContext()) {}
|
||||||
|
|
||||||
|
FrameDumperOpenGL::~FrameDumperOpenGL() {
|
||||||
|
if (present_thread.joinable())
|
||||||
|
present_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FrameDumperOpenGL::IsDumping() const {
|
||||||
|
return video_dumper.IsDumping();
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout::FramebufferLayout FrameDumperOpenGL::GetLayout() const {
|
||||||
|
return video_dumper.GetLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::StartDumping() {
|
||||||
|
if (present_thread.joinable())
|
||||||
|
present_thread.join();
|
||||||
|
|
||||||
|
present_thread = std::thread(&FrameDumperOpenGL::PresentLoop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::StopDumping() {
|
||||||
|
stop_requested.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::PresentLoop() {
|
||||||
|
Frontend::ScopeAcquireContext scope{*context};
|
||||||
|
InitializeOpenGLObjects();
|
||||||
|
|
||||||
|
const auto& layout = GetLayout();
|
||||||
|
while (!stop_requested.exchange(false)) {
|
||||||
|
auto frame = mailbox->TryGetPresentFrame(200);
|
||||||
|
if (!frame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->color_reloaded) {
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Reloading present frame");
|
||||||
|
mailbox->ReloadPresentFrame(frame, layout.width, layout.height);
|
||||||
|
}
|
||||||
|
glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[current_pbo].handle);
|
||||||
|
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0);
|
||||||
|
|
||||||
|
// Insert fence for the main thread to block on
|
||||||
|
frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
glFlush();
|
||||||
|
|
||||||
|
// Bind the previous PBO and read the pixels
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle);
|
||||||
|
GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
|
||||||
|
VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
|
||||||
|
video_dumper.AddVideoFrame(std::move(frame_data));
|
||||||
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
current_pbo = (current_pbo + 1) % 2;
|
||||||
|
next_pbo = (current_pbo + 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanupOpenGLObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::InitializeOpenGLObjects() {
|
||||||
|
const auto& layout = GetLayout();
|
||||||
|
for (auto& buffer : pbos) {
|
||||||
|
buffer.Create();
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr,
|
||||||
|
GL_STREAM_READ);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameDumperOpenGL::CleanupOpenGLObjects() {
|
||||||
|
for (auto& buffer : pbos) {
|
||||||
|
buffer.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include "core/dumping/backend.h"
|
||||||
|
#include "core/frontend/framebuffer_layout.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
|
||||||
|
namespace Frontend {
|
||||||
|
class EmuWindow;
|
||||||
|
class GraphicsContext;
|
||||||
|
class TextureMailbox;
|
||||||
|
} // namespace Frontend
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class RendererOpenGL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the 'presentation' part in frame dumping.
|
||||||
|
* Processes frames/textures sent to its mailbox, downloads the pixels and sends the data
|
||||||
|
* to the video encoding backend.
|
||||||
|
*/
|
||||||
|
class FrameDumperOpenGL {
|
||||||
|
public:
|
||||||
|
explicit FrameDumperOpenGL(VideoDumper::Backend& video_dumper, Frontend::EmuWindow& emu_window);
|
||||||
|
~FrameDumperOpenGL();
|
||||||
|
|
||||||
|
bool IsDumping() const;
|
||||||
|
Layout::FramebufferLayout GetLayout() const;
|
||||||
|
void StartDumping();
|
||||||
|
void StopDumping();
|
||||||
|
|
||||||
|
std::unique_ptr<Frontend::TextureMailbox> mailbox;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void InitializeOpenGLObjects();
|
||||||
|
void CleanupOpenGLObjects();
|
||||||
|
void PresentLoop();
|
||||||
|
|
||||||
|
VideoDumper::Backend& video_dumper;
|
||||||
|
std::unique_ptr<Frontend::GraphicsContext> context;
|
||||||
|
std::thread present_thread;
|
||||||
|
std::atomic_bool stop_requested{false};
|
||||||
|
|
||||||
|
// PBOs used to dump frames faster
|
||||||
|
std::array<OGLBuffer, 2> pbos;
|
||||||
|
GLuint current_pbo = 1;
|
||||||
|
GLuint next_pbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,238 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
using PixelFormat = SurfaceParams::PixelFormat;
|
||||||
|
|
||||||
|
class RGBA4toRGB5A1 final : public FormatReinterpreterBase {
|
||||||
|
public:
|
||||||
|
RGBA4toRGB5A1() {
|
||||||
|
constexpr std::string_view vs_source = R"(
|
||||||
|
out vec2 dst_coord;
|
||||||
|
|
||||||
|
uniform mediump ivec2 dst_size;
|
||||||
|
|
||||||
|
const vec2 vertices[4] =
|
||||||
|
vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||||
|
dst_coord = (vertices[gl_VertexID] / 2.0 + 0.5) * vec2(dst_size);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr std::string_view fs_source = R"(
|
||||||
|
in mediump vec2 dst_coord;
|
||||||
|
|
||||||
|
out lowp vec4 frag_color;
|
||||||
|
|
||||||
|
uniform lowp sampler2D source;
|
||||||
|
uniform mediump ivec2 dst_size;
|
||||||
|
uniform mediump ivec2 src_size;
|
||||||
|
uniform mediump ivec2 src_offset;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
mediump ivec2 tex_coord;
|
||||||
|
if (src_size == dst_size) {
|
||||||
|
tex_coord = ivec2(dst_coord);
|
||||||
|
} else {
|
||||||
|
highp int tex_index = int(dst_coord.y) * dst_size.x + int(dst_coord.x);
|
||||||
|
mediump int y = tex_index / src_size.x;
|
||||||
|
tex_coord = ivec2(tex_index - y * src_size.x, y);
|
||||||
|
}
|
||||||
|
tex_coord -= src_offset;
|
||||||
|
|
||||||
|
lowp ivec4 rgba4 = ivec4(texelFetch(source, tex_coord, 0) * (exp2(4.0) - 1.0));
|
||||||
|
lowp ivec3 rgb5 =
|
||||||
|
((rgba4.rgb << ivec3(1, 2, 3)) | (rgba4.gba >> ivec3(3, 2, 1))) & 0x1F;
|
||||||
|
frag_color = vec4(vec3(rgb5) / (exp2(5.0) - 1.0), rgba4.a & 0x01);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
program.Create(vs_source.data(), fs_source.data());
|
||||||
|
dst_size_loc = glGetUniformLocation(program.handle, "dst_size");
|
||||||
|
src_size_loc = glGetUniformLocation(program.handle, "src_size");
|
||||||
|
src_offset_loc = glGetUniformLocation(program.handle, "src_offset");
|
||||||
|
vao.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
||||||
|
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
||||||
|
GLuint draw_fb_handle) override {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.texture_units[0].texture_2d = src_tex;
|
||||||
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
|
state.draw.shader_program = program.handle;
|
||||||
|
state.draw.vertex_array = vao.handle;
|
||||||
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
||||||
|
0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
||||||
|
0);
|
||||||
|
|
||||||
|
glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight());
|
||||||
|
glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight());
|
||||||
|
glUniform2i(src_offset_loc, src_rect.left, src_rect.bottom);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
OGLProgram program;
|
||||||
|
GLint dst_size_loc{-1}, src_size_loc{-1}, src_offset_loc{-1};
|
||||||
|
OGLVertexArray vao;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PixelBufferD24S8toABGR final : public FormatReinterpreterBase {
|
||||||
|
public:
|
||||||
|
PixelBufferD24S8toABGR() {
|
||||||
|
attributeless_vao.Create();
|
||||||
|
d24s8_abgr_buffer.Create();
|
||||||
|
d24s8_abgr_buffer_size = 0;
|
||||||
|
|
||||||
|
constexpr std::string_view vs_source = R"(
|
||||||
|
const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0),
|
||||||
|
vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string fs_source = GLES ? fragment_shader_precision_OES : "";
|
||||||
|
fs_source += R"(
|
||||||
|
uniform samplerBuffer tbo;
|
||||||
|
uniform vec2 tbo_size;
|
||||||
|
uniform vec4 viewport;
|
||||||
|
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw;
|
||||||
|
int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x);
|
||||||
|
color = texelFetch(tbo, tbo_offset).rabg;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
d24s8_abgr_shader.Create(vs_source.data(), fs_source.c_str());
|
||||||
|
|
||||||
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
|
GLuint old_program = state.draw.shader_program;
|
||||||
|
state.draw.shader_program = d24s8_abgr_shader.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo");
|
||||||
|
ASSERT(tbo_u_id != -1);
|
||||||
|
glUniform1i(tbo_u_id, 0);
|
||||||
|
|
||||||
|
state.draw.shader_program = old_program;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size");
|
||||||
|
ASSERT(d24s8_abgr_tbo_size_u_id != -1);
|
||||||
|
d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport");
|
||||||
|
ASSERT(d24s8_abgr_viewport_u_id != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~PixelBufferD24S8toABGR() {}
|
||||||
|
|
||||||
|
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
||||||
|
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
||||||
|
GLuint draw_fb_handle) override {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.draw.read_framebuffer = read_fb_handle;
|
||||||
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle);
|
||||||
|
|
||||||
|
GLsizeiptr target_pbo_size =
|
||||||
|
static_cast<GLsizeiptr>(src_rect.GetWidth()) * src_rect.GetHeight() * 4;
|
||||||
|
if (target_pbo_size > d24s8_abgr_buffer_size) {
|
||||||
|
d24s8_abgr_buffer_size = target_pbo_size * 2;
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY);
|
||||||
|
}
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
src_tex, 0);
|
||||||
|
glReadPixels(static_cast<GLint>(src_rect.left), static_cast<GLint>(src_rect.bottom),
|
||||||
|
static_cast<GLsizei>(src_rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(src_rect.GetHeight()), GL_DEPTH_STENCIL,
|
||||||
|
GL_UNSIGNED_INT_24_8, 0);
|
||||||
|
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
// PBO now contains src_tex in RABG format
|
||||||
|
state.draw.shader_program = d24s8_abgr_shader.handle;
|
||||||
|
state.draw.vertex_array = attributeless_vao.handle;
|
||||||
|
state.viewport.x = static_cast<GLint>(dst_rect.left);
|
||||||
|
state.viewport.y = static_cast<GLint>(dst_rect.bottom);
|
||||||
|
state.viewport.width = static_cast<GLsizei>(dst_rect.GetWidth());
|
||||||
|
state.viewport.height = static_cast<GLsizei>(dst_rect.GetHeight());
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
OGLTexture tbo;
|
||||||
|
tbo.Create();
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, tbo.handle);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle);
|
||||||
|
|
||||||
|
glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast<GLfloat>(src_rect.GetWidth()),
|
||||||
|
static_cast<GLfloat>(src_rect.GetHeight()));
|
||||||
|
glUniform4f(d24s8_abgr_viewport_u_id, static_cast<GLfloat>(state.viewport.x),
|
||||||
|
static_cast<GLfloat>(state.viewport.y),
|
||||||
|
static_cast<GLfloat>(state.viewport.width),
|
||||||
|
static_cast<GLfloat>(state.viewport.height));
|
||||||
|
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
||||||
|
0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
||||||
|
0);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
OGLVertexArray attributeless_vao;
|
||||||
|
OGLBuffer d24s8_abgr_buffer;
|
||||||
|
GLsizeiptr d24s8_abgr_buffer_size;
|
||||||
|
OGLProgram d24s8_abgr_shader;
|
||||||
|
GLint d24s8_abgr_tbo_size_u_id;
|
||||||
|
GLint d24s8_abgr_viewport_u_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormatReinterpreterOpenGL::FormatReinterpreterOpenGL() {
|
||||||
|
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGBA8, PixelFormat::D24S8},
|
||||||
|
std::make_unique<PixelBufferD24S8toABGR>());
|
||||||
|
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGB5A1, PixelFormat::RGBA4},
|
||||||
|
std::make_unique<RGBA4toRGB5A1>());
|
||||||
|
}
|
||||||
|
|
||||||
|
FormatReinterpreterOpenGL::~FormatReinterpreterOpenGL() = default;
|
||||||
|
|
||||||
|
std::pair<FormatReinterpreterOpenGL::ReinterpreterMap::iterator,
|
||||||
|
FormatReinterpreterOpenGL::ReinterpreterMap::iterator>
|
||||||
|
FormatReinterpreterOpenGL::GetPossibleReinterpretations(PixelFormat dst_format) {
|
||||||
|
return reinterpreters.equal_range(dst_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class RasterizerCacheOpenGL;
|
||||||
|
|
||||||
|
struct PixelFormatPair {
|
||||||
|
const SurfaceParams::PixelFormat dst_format, src_format;
|
||||||
|
struct less {
|
||||||
|
using is_transparent = void;
|
||||||
|
constexpr bool operator()(OpenGL::PixelFormatPair lhs, OpenGL::PixelFormatPair rhs) const {
|
||||||
|
return std::tie(lhs.dst_format, lhs.src_format) <
|
||||||
|
std::tie(rhs.dst_format, rhs.src_format);
|
||||||
|
}
|
||||||
|
constexpr bool operator()(OpenGL::SurfaceParams::PixelFormat lhs,
|
||||||
|
OpenGL::PixelFormatPair rhs) const {
|
||||||
|
return lhs < rhs.dst_format;
|
||||||
|
}
|
||||||
|
constexpr bool operator()(OpenGL::PixelFormatPair lhs,
|
||||||
|
OpenGL::SurfaceParams::PixelFormat rhs) const {
|
||||||
|
return lhs.dst_format < rhs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class FormatReinterpreterBase {
|
||||||
|
public:
|
||||||
|
virtual ~FormatReinterpreterBase() = default;
|
||||||
|
|
||||||
|
virtual void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect,
|
||||||
|
GLuint read_fb_handle, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, GLuint draw_fb_handle) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FormatReinterpreterOpenGL : NonCopyable {
|
||||||
|
using ReinterpreterMap =
|
||||||
|
std::map<PixelFormatPair, std::unique_ptr<FormatReinterpreterBase>, PixelFormatPair::less>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FormatReinterpreterOpenGL();
|
||||||
|
~FormatReinterpreterOpenGL();
|
||||||
|
|
||||||
|
std::pair<ReinterpreterMap::iterator, ReinterpreterMap::iterator> GetPossibleReinterpretations(
|
||||||
|
SurfaceParams::PixelFormat dst_format);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ReinterpreterMap reinterpreters;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,171 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const {
|
||||||
|
SurfaceParams params = *this;
|
||||||
|
const u32 tiled_size = is_tiled ? 8 : 1;
|
||||||
|
const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size);
|
||||||
|
PAddr aligned_start =
|
||||||
|
addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes);
|
||||||
|
PAddr aligned_end =
|
||||||
|
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes);
|
||||||
|
|
||||||
|
if (aligned_end - aligned_start > stride_tiled_bytes) {
|
||||||
|
params.addr = aligned_start;
|
||||||
|
params.height = (aligned_end - aligned_start) / BytesInPixels(stride);
|
||||||
|
} else {
|
||||||
|
// 1 row
|
||||||
|
ASSERT(aligned_end - aligned_start == stride_tiled_bytes);
|
||||||
|
const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1);
|
||||||
|
aligned_start =
|
||||||
|
addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment);
|
||||||
|
aligned_end =
|
||||||
|
addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment);
|
||||||
|
params.addr = aligned_start;
|
||||||
|
params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size;
|
||||||
|
params.stride = params.width;
|
||||||
|
params.height = tiled_size;
|
||||||
|
}
|
||||||
|
params.UpdateParams();
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle<u32> unscaled_rect) const {
|
||||||
|
if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_tiled) {
|
||||||
|
unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8;
|
||||||
|
unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8;
|
||||||
|
unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8;
|
||||||
|
unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 stride_tiled = !is_tiled ? stride : stride * 8;
|
||||||
|
|
||||||
|
const u32 pixel_offset =
|
||||||
|
stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) +
|
||||||
|
unscaled_rect.left;
|
||||||
|
|
||||||
|
const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth();
|
||||||
|
|
||||||
|
return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)};
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const {
|
||||||
|
SurfaceInterval result{};
|
||||||
|
const auto valid_regions =
|
||||||
|
SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions;
|
||||||
|
for (auto& valid_interval : valid_regions) {
|
||||||
|
const SurfaceInterval aligned_interval{
|
||||||
|
addr + Common::AlignUp(boost::icl::first(valid_interval) - addr,
|
||||||
|
BytesInPixels(is_tiled ? 8 * 8 : 1)),
|
||||||
|
addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr,
|
||||||
|
BytesInPixels(is_tiled ? 8 * 8 : 1))};
|
||||||
|
|
||||||
|
if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) ||
|
||||||
|
boost::icl::length(aligned_interval) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the rectangle within aligned_interval
|
||||||
|
const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1);
|
||||||
|
SurfaceInterval rect_interval{
|
||||||
|
addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes),
|
||||||
|
addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes),
|
||||||
|
};
|
||||||
|
if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) {
|
||||||
|
// 1 row
|
||||||
|
rect_interval = aligned_interval;
|
||||||
|
} else if (boost::icl::length(rect_interval) == 0) {
|
||||||
|
// 2 rows that do not make a rectangle, return the larger one
|
||||||
|
const SurfaceInterval row1{boost::icl::first(aligned_interval),
|
||||||
|
boost::icl::first(rect_interval)};
|
||||||
|
const SurfaceInterval row2{boost::icl::first(rect_interval),
|
||||||
|
boost::icl::last_next(aligned_interval)};
|
||||||
|
rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boost::icl::length(rect_interval) > boost::icl::length(result)) {
|
||||||
|
result = rect_interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const {
|
||||||
|
const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr);
|
||||||
|
|
||||||
|
if (is_tiled) {
|
||||||
|
const int x0 = (begin_pixel_index % (stride * 8)) / 8;
|
||||||
|
const int y0 = (begin_pixel_index / (stride * 8)) * 8;
|
||||||
|
// Top to bottom
|
||||||
|
return Common::Rectangle<u32>(x0, height - y0, x0 + sub_surface.width,
|
||||||
|
height - (y0 + sub_surface.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int x0 = begin_pixel_index % stride;
|
||||||
|
const int y0 = begin_pixel_index / stride;
|
||||||
|
// Bottom to top
|
||||||
|
return Common::Rectangle<u32>(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const {
|
||||||
|
auto rect = GetSubRect(sub_surface);
|
||||||
|
rect.left = rect.left * res_scale;
|
||||||
|
rect.right = rect.right * res_scale;
|
||||||
|
rect.top = rect.top * res_scale;
|
||||||
|
rect.bottom = rect.bottom * res_scale;
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const {
|
||||||
|
return std::tie(other_surface.addr, other_surface.width, other_surface.height,
|
||||||
|
other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) ==
|
||||||
|
std::tie(addr, width, height, stride, pixel_format, is_tiled) &&
|
||||||
|
pixel_format != PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const {
|
||||||
|
return sub_surface.addr >= addr && sub_surface.end <= end &&
|
||||||
|
sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid &&
|
||||||
|
sub_surface.is_tiled == is_tiled &&
|
||||||
|
(sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
||||||
|
(sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) &&
|
||||||
|
GetSubRect(sub_surface).right <= stride;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const {
|
||||||
|
return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format &&
|
||||||
|
addr <= expanded_surface.end && expanded_surface.addr <= end &&
|
||||||
|
is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride &&
|
||||||
|
(std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) %
|
||||||
|
BytesInPixels(stride * (is_tiled ? 8 : 1)) ==
|
||||||
|
0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const {
|
||||||
|
if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr ||
|
||||||
|
end < texcopy_params.end) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (texcopy_params.width != texcopy_params.stride) {
|
||||||
|
const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1));
|
||||||
|
return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
||||||
|
texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 &&
|
||||||
|
(texcopy_params.height == 1 || texcopy_params.stride == tile_stride) &&
|
||||||
|
((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride;
|
||||||
|
}
|
||||||
|
return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,270 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <climits>
|
||||||
|
#include <boost/icl/interval.hpp>
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "core/hw/gpu.h"
|
||||||
|
#include "video_core/regs_framebuffer.h"
|
||||||
|
#include "video_core/regs_texturing.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
struct CachedSurface;
|
||||||
|
using Surface = std::shared_ptr<CachedSurface>;
|
||||||
|
|
||||||
|
using SurfaceInterval = boost::icl::right_open_interval<PAddr>;
|
||||||
|
|
||||||
|
struct SurfaceParams {
|
||||||
|
private:
|
||||||
|
static constexpr std::array<unsigned int, 18> BPP_TABLE = {
|
||||||
|
32, // RGBA8
|
||||||
|
24, // RGB8
|
||||||
|
16, // RGB5A1
|
||||||
|
16, // RGB565
|
||||||
|
16, // RGBA4
|
||||||
|
16, // IA8
|
||||||
|
16, // RG8
|
||||||
|
8, // I8
|
||||||
|
8, // A8
|
||||||
|
8, // IA4
|
||||||
|
4, // I4
|
||||||
|
4, // A4
|
||||||
|
4, // ETC1
|
||||||
|
8, // ETC1A4
|
||||||
|
16, // D16
|
||||||
|
0,
|
||||||
|
24, // D24
|
||||||
|
32, // D24S8
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class PixelFormat {
|
||||||
|
// First 5 formats are shared between textures and color buffers
|
||||||
|
RGBA8 = 0,
|
||||||
|
RGB8 = 1,
|
||||||
|
RGB5A1 = 2,
|
||||||
|
RGB565 = 3,
|
||||||
|
RGBA4 = 4,
|
||||||
|
|
||||||
|
// Texture-only formats
|
||||||
|
IA8 = 5,
|
||||||
|
RG8 = 6,
|
||||||
|
I8 = 7,
|
||||||
|
A8 = 8,
|
||||||
|
IA4 = 9,
|
||||||
|
I4 = 10,
|
||||||
|
A4 = 11,
|
||||||
|
ETC1 = 12,
|
||||||
|
ETC1A4 = 13,
|
||||||
|
|
||||||
|
// Depth buffer-only formats
|
||||||
|
D16 = 14,
|
||||||
|
// gap
|
||||||
|
D24 = 16,
|
||||||
|
D24S8 = 17,
|
||||||
|
|
||||||
|
Invalid = 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SurfaceType {
|
||||||
|
Color = 0,
|
||||||
|
Texture = 1,
|
||||||
|
Depth = 2,
|
||||||
|
DepthStencil = 3,
|
||||||
|
Fill = 4,
|
||||||
|
Invalid = 5
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr unsigned int GetFormatBpp(PixelFormat format) {
|
||||||
|
const auto format_idx = static_cast<std::size_t>(format);
|
||||||
|
DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx);
|
||||||
|
return BPP_TABLE[format_idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int GetFormatBpp() const {
|
||||||
|
return GetFormatBpp(pixel_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string_view PixelFormatAsString(PixelFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case PixelFormat::RGBA8:
|
||||||
|
return "RGBA8";
|
||||||
|
case PixelFormat::RGB8:
|
||||||
|
return "RGB8";
|
||||||
|
case PixelFormat::RGB5A1:
|
||||||
|
return "RGB5A1";
|
||||||
|
case PixelFormat::RGB565:
|
||||||
|
return "RGB565";
|
||||||
|
case PixelFormat::RGBA4:
|
||||||
|
return "RGBA4";
|
||||||
|
case PixelFormat::IA8:
|
||||||
|
return "IA8";
|
||||||
|
case PixelFormat::RG8:
|
||||||
|
return "RG8";
|
||||||
|
case PixelFormat::I8:
|
||||||
|
return "I8";
|
||||||
|
case PixelFormat::A8:
|
||||||
|
return "A8";
|
||||||
|
case PixelFormat::IA4:
|
||||||
|
return "IA4";
|
||||||
|
case PixelFormat::I4:
|
||||||
|
return "I4";
|
||||||
|
case PixelFormat::A4:
|
||||||
|
return "A4";
|
||||||
|
case PixelFormat::ETC1:
|
||||||
|
return "ETC1";
|
||||||
|
case PixelFormat::ETC1A4:
|
||||||
|
return "ETC1A4";
|
||||||
|
case PixelFormat::D16:
|
||||||
|
return "D16";
|
||||||
|
case PixelFormat::D24:
|
||||||
|
return "D24";
|
||||||
|
case PixelFormat::D24S8:
|
||||||
|
return "D24S8";
|
||||||
|
default:
|
||||||
|
return "Not a real pixel format";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) {
|
||||||
|
return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) {
|
||||||
|
return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) {
|
||||||
|
return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14)
|
||||||
|
: PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
// RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat
|
||||||
|
case GPU::Regs::PixelFormat::RGB565:
|
||||||
|
return PixelFormat::RGB565;
|
||||||
|
case GPU::Regs::PixelFormat::RGB5A1:
|
||||||
|
return PixelFormat::RGB5A1;
|
||||||
|
default:
|
||||||
|
return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) {
|
||||||
|
SurfaceType a_type = GetFormatType(pixel_format_a);
|
||||||
|
SurfaceType b_type = GetFormatType(pixel_format_b);
|
||||||
|
|
||||||
|
if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) &&
|
||||||
|
(b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) {
|
||||||
|
if ((unsigned int)pixel_format < 5) {
|
||||||
|
return SurfaceType::Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((unsigned int)pixel_format < 14) {
|
||||||
|
return SurfaceType::Texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) {
|
||||||
|
return SurfaceType::Depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixel_format == PixelFormat::D24S8) {
|
||||||
|
return SurfaceType::DepthStencil;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SurfaceType::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the params "size", "end" and "type" from the already set "addr", "width", "height"
|
||||||
|
/// and "pixel_format"
|
||||||
|
void UpdateParams() {
|
||||||
|
if (stride == 0) {
|
||||||
|
stride = width;
|
||||||
|
}
|
||||||
|
type = GetFormatType(pixel_format);
|
||||||
|
size = !is_tiled ? BytesInPixels(stride * (height - 1) + width)
|
||||||
|
: BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8);
|
||||||
|
end = addr + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceInterval GetInterval() const {
|
||||||
|
return SurfaceInterval(addr, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the outer rectangle containing "interval"
|
||||||
|
SurfaceParams FromInterval(SurfaceInterval interval) const;
|
||||||
|
|
||||||
|
SurfaceInterval GetSubRectInterval(Common::Rectangle<u32> unscaled_rect) const;
|
||||||
|
|
||||||
|
// Returns the region of the biggest valid rectange within interval
|
||||||
|
SurfaceInterval GetCopyableInterval(const Surface& src_surface) const;
|
||||||
|
|
||||||
|
u32 GetScaledWidth() const {
|
||||||
|
return width * res_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetScaledHeight() const {
|
||||||
|
return height * res_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> GetRect() const {
|
||||||
|
return {0, height, width, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::Rectangle<u32> GetScaledRect() const {
|
||||||
|
return {0, GetScaledHeight(), GetScaledWidth(), 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 PixelsInBytes(u32 size) const {
|
||||||
|
return size * CHAR_BIT / GetFormatBpp(pixel_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 BytesInPixels(u32 pixels) const {
|
||||||
|
return pixels * GetFormatBpp(pixel_format) / CHAR_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExactMatch(const SurfaceParams& other_surface) const;
|
||||||
|
bool CanSubRect(const SurfaceParams& sub_surface) const;
|
||||||
|
bool CanExpand(const SurfaceParams& expanded_surface) const;
|
||||||
|
bool CanTexCopy(const SurfaceParams& texcopy_params) const;
|
||||||
|
|
||||||
|
Common::Rectangle<u32> GetSubRect(const SurfaceParams& sub_surface) const;
|
||||||
|
Common::Rectangle<u32> GetScaledSubRect(const SurfaceParams& sub_surface) const;
|
||||||
|
|
||||||
|
PAddr addr = 0;
|
||||||
|
PAddr end = 0;
|
||||||
|
u32 size = 0;
|
||||||
|
|
||||||
|
u32 width = 0;
|
||||||
|
u32 height = 0;
|
||||||
|
u32 stride = 0;
|
||||||
|
u16 res_scale = 1;
|
||||||
|
|
||||||
|
bool is_tiled = false;
|
||||||
|
PixelFormat pixel_format = PixelFormat::Invalid;
|
||||||
|
SurfaceType type = SurfaceType::Invalid;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class TextureFilterBase {
|
||||||
|
friend class TextureFilterer;
|
||||||
|
virtual void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TextureFilterBase(u16 scale_factor) : scale_factor{scale_factor} {};
|
||||||
|
virtual ~TextureFilterBase() = default;
|
||||||
|
|
||||||
|
const u16 scale_factor{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -1,38 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <string_view>
|
|
||||||
#include "common/common_types.h"
|
|
||||||
#include "common/math_util.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
|
||||||
|
|
||||||
struct CachedSurface;
|
|
||||||
struct Viewport;
|
|
||||||
|
|
||||||
class TextureFilterInterface {
|
|
||||||
public:
|
|
||||||
const u16 scale_factor{};
|
|
||||||
TextureFilterInterface(u16 scale_factor) : scale_factor{scale_factor} {}
|
|
||||||
virtual void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
|
|
||||||
std::size_t buffer_offset) = 0;
|
|
||||||
virtual ~TextureFilterInterface() = default;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
Viewport RectToViewport(const Common::Rectangle<u32>& rect);
|
|
||||||
};
|
|
||||||
|
|
||||||
// every texture filter should have a static GetInfo function
|
|
||||||
struct TextureFilterInfo {
|
|
||||||
std::string_view name;
|
|
||||||
struct {
|
|
||||||
u16 min, max;
|
|
||||||
} clamp_scale{1, 10};
|
|
||||||
std::function<std::unique_ptr<TextureFilterInterface>(u16 scale_factor)> constructor;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
|
@ -1,89 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
|
||||||
|
|
||||||
Viewport TextureFilterInterface::RectToViewport(const Common::Rectangle<u32>& rect) {
|
|
||||||
return {
|
|
||||||
static_cast<GLint>(rect.left) * scale_factor,
|
|
||||||
static_cast<GLint>(rect.top) * scale_factor,
|
|
||||||
static_cast<GLsizei>(rect.GetWidth()) * scale_factor,
|
|
||||||
static_cast<GLsizei>(rect.GetHeight()) * scale_factor,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
template <typename T>
|
|
||||||
std::pair<std::string_view, TextureFilterInfo> FilterMapPair() {
|
|
||||||
return {T::GetInfo().name, T::GetInfo()};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NoFilter {
|
|
||||||
static TextureFilterInfo GetInfo() {
|
|
||||||
TextureFilterInfo info;
|
|
||||||
info.name = TextureFilterManager::NONE;
|
|
||||||
info.clamp_scale = {1, 1};
|
|
||||||
info.constructor = [](u16) { return nullptr; };
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
const std::map<std::string_view, TextureFilterInfo, TextureFilterManager::FilterNameComp>&
|
|
||||||
TextureFilterManager::TextureFilterMap() {
|
|
||||||
static const std::map<std::string_view, TextureFilterInfo, FilterNameComp> filter_map{
|
|
||||||
FilterMapPair<NoFilter>(),
|
|
||||||
FilterMapPair<Anime4kUltrafast>(),
|
|
||||||
FilterMapPair<Bicubic>(),
|
|
||||||
FilterMapPair<XbrzFreescale>(),
|
|
||||||
};
|
|
||||||
return filter_map;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureFilterManager::SetTextureFilter(std::string filter_name, u16 new_scale_factor) {
|
|
||||||
if (name == filter_name && scale_factor == new_scale_factor)
|
|
||||||
return;
|
|
||||||
std::lock_guard<std::mutex> lock{mutex};
|
|
||||||
name = std::move(filter_name);
|
|
||||||
scale_factor = new_scale_factor;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureFilterInterface* TextureFilterManager::GetTextureFilter() const {
|
|
||||||
return filter.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TextureFilterManager::IsUpdated() const {
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureFilterManager::Reset() {
|
|
||||||
std::lock_guard<std::mutex> lock{mutex};
|
|
||||||
updated = false;
|
|
||||||
auto iter = TextureFilterMap().find(name);
|
|
||||||
if (iter == TextureFilterMap().end()) {
|
|
||||||
LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", name);
|
|
||||||
filter = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& filter_info = iter->second;
|
|
||||||
|
|
||||||
u16 clamped_scale =
|
|
||||||
std::clamp(scale_factor, filter_info.clamp_scale.min, filter_info.clamp_scale.max);
|
|
||||||
if (clamped_scale != scale_factor)
|
|
||||||
LOG_ERROR(Render_OpenGL, "Invalid scale factor {} for texture filter {}, clamped to {}",
|
|
||||||
scale_factor, filter_info.name, clamped_scale);
|
|
||||||
|
|
||||||
filter = filter_info.constructor(clamped_scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
|
@ -1,55 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string_view>
|
|
||||||
#include <tuple>
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
|
||||||
|
|
||||||
class TextureFilterManager {
|
|
||||||
public:
|
|
||||||
static constexpr std::string_view NONE = "none";
|
|
||||||
struct FilterNameComp {
|
|
||||||
bool operator()(const std::string_view a, const std::string_view b) const {
|
|
||||||
bool na = a == NONE;
|
|
||||||
bool nb = b == NONE;
|
|
||||||
if (na | nb)
|
|
||||||
return na & !nb;
|
|
||||||
return a < b;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// function ensures map is initialized before use
|
|
||||||
static const std::map<std::string_view, TextureFilterInfo, FilterNameComp>& TextureFilterMap();
|
|
||||||
|
|
||||||
static TextureFilterManager& GetInstance() {
|
|
||||||
static TextureFilterManager singleton;
|
|
||||||
return singleton;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Destroy() {
|
|
||||||
filter.reset();
|
|
||||||
}
|
|
||||||
void SetTextureFilter(std::string filter_name, u16 new_scale_factor);
|
|
||||||
TextureFilterInterface* GetTextureFilter() const;
|
|
||||||
// returns true if filter has been changed and a cache reset is needed
|
|
||||||
bool IsUpdated() const;
|
|
||||||
void Reset();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::atomic<bool> updated{false};
|
|
||||||
std::mutex mutex;
|
|
||||||
std::string name{"none"};
|
|
||||||
u16 scale_factor{1};
|
|
||||||
|
|
||||||
std::unique_ptr<TextureFilterInterface> filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
|
@ -0,0 +1,86 @@
|
|||||||
|
/// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using TextureFilterContructor = std::function<std::unique_ptr<TextureFilterBase>(u16)>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::pair<std::string_view, TextureFilterContructor> FilterMapPair() {
|
||||||
|
return {T::NAME, std::make_unique<T, u16>};
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::unordered_map<std::string_view, TextureFilterContructor> filter_map{
|
||||||
|
{TextureFilterer::NONE, [](u16) { return nullptr; }},
|
||||||
|
FilterMapPair<Anime4kUltrafast>(),
|
||||||
|
FilterMapPair<Bicubic>(),
|
||||||
|
FilterMapPair<XbrzFreescale>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TextureFilterer::TextureFilterer(std::string_view filter_name, u16 scale_factor) {
|
||||||
|
Reset(filter_name, scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureFilterer::Reset(std::string_view new_filter_name, u16 new_scale_factor) {
|
||||||
|
if (filter_name == new_filter_name && (IsNull() || filter->scale_factor == new_scale_factor))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto iter = filter_map.find(new_filter_name);
|
||||||
|
if (iter == filter_map.end()) {
|
||||||
|
LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", new_filter_name);
|
||||||
|
filter = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_name = iter->first;
|
||||||
|
filter = iter->second(new_scale_factor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureFilterer::IsNull() const {
|
||||||
|
return !filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureFilterer::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect,
|
||||||
|
SurfaceParams::SurfaceType type, GLuint read_fb_handle,
|
||||||
|
GLuint draw_fb_handle) {
|
||||||
|
// depth / stencil texture filtering is not supported for now
|
||||||
|
if (IsNull() ||
|
||||||
|
(type != SurfaceParams::SurfaceType::Color && type != SurfaceParams::SurfaceType::Texture))
|
||||||
|
return false;
|
||||||
|
filter->Filter(src_tex, src_rect, dst_tex, dst_rect, read_fb_handle, draw_fb_handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> TextureFilterer::GetFilterNames() {
|
||||||
|
std::vector<std::string_view> ret;
|
||||||
|
std::transform(filter_map.begin(), filter_map.end(), std::back_inserter(ret),
|
||||||
|
[](auto pair) { return pair.first; });
|
||||||
|
std::sort(ret.begin(), ret.end(), [](std::string_view lhs, std::string_view rhs) {
|
||||||
|
// sort lexicographically with none at the top
|
||||||
|
bool lhs_is_none{lhs == NONE};
|
||||||
|
bool rhs_is_none{rhs == NONE};
|
||||||
|
if (lhs_is_none || rhs_is_none)
|
||||||
|
return lhs_is_none && !rhs_is_none;
|
||||||
|
return lhs < rhs;
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2020 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_surface_params.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class TextureFilterer {
|
||||||
|
public:
|
||||||
|
static constexpr std::string_view NONE = "none";
|
||||||
|
|
||||||
|
explicit TextureFilterer(std::string_view filter_name, u16 scale_factor);
|
||||||
|
// returns true if the filter actually changed
|
||||||
|
bool Reset(std::string_view new_filter_name, u16 new_scale_factor);
|
||||||
|
// returns true if there is no active filter
|
||||||
|
bool IsNull() const;
|
||||||
|
// returns true if the texture was able to be filtered
|
||||||
|
bool Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
||||||
|
const Common::Rectangle<u32>& dst_rect, SurfaceParams::SurfaceType type,
|
||||||
|
GLuint read_fb_handle, GLuint draw_fb_handle);
|
||||||
|
|
||||||
|
static std::vector<std::string_view> GetFilterNames();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string_view filter_name = NONE;
|
||||||
|
std::unique_ptr<TextureFilterBase> filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
Loading…
Reference in New Issue