Merge pull request #5083 from zhaowenlan1779/video-dumping-update
video_core, citra_qt: Video dumping updates
This commit is contained in:
commit
9c7da35382
|
@ -38,14 +38,14 @@ void DspInterface::EnableStretching(bool enable) {
|
|||
perform_time_stretching = enable;
|
||||
}
|
||||
|
||||
void DspInterface::OutputFrame(StereoFrame16& frame) {
|
||||
void DspInterface::OutputFrame(StereoFrame16 frame) {
|
||||
if (!sink)
|
||||
return;
|
||||
|
||||
fifo.Push(frame.data(), frame.size());
|
||||
|
||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||
Core::System::GetInstance().VideoDumper().AddAudioFrame(frame);
|
||||
Core::System::GetInstance().VideoDumper().AddAudioFrame(std::move(frame));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ void DspInterface::OutputSample(std::array<s16, 2> sample) {
|
|||
fifo.Push(&sample, 1);
|
||||
|
||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||
Core::System::GetInstance().VideoDumper().AddAudioSample(sample);
|
||||
Core::System::GetInstance().VideoDumper().AddAudioSample(std::move(sample));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ public:
|
|||
void EnableStretching(bool enable);
|
||||
|
||||
protected:
|
||||
void OutputFrame(StereoFrame16& frame);
|
||||
void OutputFrame(StereoFrame16 frame);
|
||||
void OutputSample(std::array<s16, 2> sample);
|
||||
|
||||
private:
|
||||
|
|
|
@ -404,7 +404,7 @@ bool DspHle::Impl::Tick() {
|
|||
// shared memory region)
|
||||
current_frame = GenerateCurrentFrame();
|
||||
|
||||
parent.OutputFrame(current_frame);
|
||||
parent.OutputFrame(std::move(current_frame));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -483,7 +483,8 @@ DspLle::DspLle(Memory::MemorySystem& memory, bool multithread)
|
|||
*memory.GetFCRAMPointer(address - Memory::FCRAM_PADDR) = value;
|
||||
};
|
||||
impl->teakra.SetAHBMCallback(ahbm);
|
||||
impl->teakra.SetAudioCallback([this](std::array<s16, 2> sample) { OutputSample(sample); });
|
||||
impl->teakra.SetAudioCallback(
|
||||
[this](std::array<s16, 2> sample) { OutputSample(std::move(sample)); });
|
||||
}
|
||||
DspLle::~DspLle() = default;
|
||||
|
||||
|
|
|
@ -409,7 +409,7 @@ int main(int argc, char** argv) {
|
|||
if (!dump_video.empty()) {
|
||||
Layout::FramebufferLayout layout{
|
||||
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
||||
system.VideoDumper().StartDumping(dump_video, "webm", layout);
|
||||
system.VideoDumper().StartDumping(dump_video, layout);
|
||||
}
|
||||
|
||||
std::thread render_thread([&emu_window] { emu_window->Present(); });
|
||||
|
|
|
@ -268,6 +268,33 @@ void Config::ReadValues() {
|
|||
sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org");
|
||||
Settings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", "");
|
||||
Settings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", "");
|
||||
|
||||
// Video Dumping
|
||||
Settings::values.output_format =
|
||||
sdl2_config->GetString("Video Dumping", "output_format", "webm");
|
||||
Settings::values.format_options = sdl2_config->GetString("Video Dumping", "format_options", "");
|
||||
|
||||
Settings::values.video_encoder =
|
||||
sdl2_config->GetString("Video Dumping", "video_encoder", "libvpx-vp9");
|
||||
|
||||
// Options for variable bit rate live streaming taken from here:
|
||||
// https://developers.google.com/media/vp9/live-encoding
|
||||
std::string default_video_options;
|
||||
if (Settings::values.video_encoder == "libvpx-vp9") {
|
||||
default_video_options =
|
||||
"quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1";
|
||||
}
|
||||
Settings::values.video_encoder_options =
|
||||
sdl2_config->GetString("Video Dumping", "video_encoder_options", default_video_options);
|
||||
Settings::values.video_bitrate =
|
||||
sdl2_config->GetInteger("Video Dumping", "video_bitrate", 2500000);
|
||||
|
||||
Settings::values.audio_encoder =
|
||||
sdl2_config->GetString("Video Dumping", "audio_encoder", "libvorbis");
|
||||
Settings::values.audio_encoder_options =
|
||||
sdl2_config->GetString("Video Dumping", "audio_encoder_options", "");
|
||||
Settings::values.audio_bitrate =
|
||||
sdl2_config->GetInteger("Video Dumping", "audio_bitrate", 64000);
|
||||
}
|
||||
|
||||
void Config::Reload() {
|
||||
|
|
|
@ -304,5 +304,31 @@ web_api_url = https://api.citra-emu.org
|
|||
# See https://profile.citra-emu.org/ for more info
|
||||
citra_username =
|
||||
citra_token =
|
||||
|
||||
[Video Dumping]
|
||||
# Format of the video to output, default: webm
|
||||
output_format =
|
||||
|
||||
# Options passed to the muxer (optional)
|
||||
# This is a param package, format: [key1]:[value1],[key2]:[value2],...
|
||||
format_options =
|
||||
|
||||
# Video encoder used, default: libvpx-vp9
|
||||
video_encoder =
|
||||
|
||||
# Options passed to the video codec (optional)
|
||||
video_encoder_options =
|
||||
|
||||
# Video bitrate, default: 2500000
|
||||
video_bitrate =
|
||||
|
||||
# Audio encoder used, default: libvorbis
|
||||
audio_encoder =
|
||||
|
||||
# Options passed to the audio codec (optional)
|
||||
audio_encoder_options =
|
||||
|
||||
# Audio bitrate, default: 64000
|
||||
audio_bitrate =
|
||||
)";
|
||||
}
|
||||
|
|
|
@ -162,6 +162,20 @@ add_executable(citra-qt
|
|||
util/util.h
|
||||
)
|
||||
|
||||
if (ENABLE_FFMPEG_VIDEO_DUMPER)
|
||||
target_sources(citra-qt PRIVATE
|
||||
dumping/dumping_dialog.cpp
|
||||
dumping/dumping_dialog.h
|
||||
dumping/dumping_dialog.ui
|
||||
dumping/option_set_dialog.cpp
|
||||
dumping/option_set_dialog.h
|
||||
dumping/option_set_dialog.ui
|
||||
dumping/options_dialog.cpp
|
||||
dumping/options_dialog.h
|
||||
dumping/options_dialog.ui
|
||||
)
|
||||
endif()
|
||||
|
||||
file(GLOB COMPAT_LIST
|
||||
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
|
||||
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
|
||||
|
|
|
@ -93,6 +93,7 @@ void Config::ReadValues() {
|
|||
ReadMiscellaneousValues();
|
||||
ReadDebuggingValues();
|
||||
ReadWebServiceValues();
|
||||
ReadVideoDumpingValues();
|
||||
ReadUIValues();
|
||||
ReadUtilityValues();
|
||||
}
|
||||
|
@ -492,6 +493,49 @@ void Config::ReadSystemValues() {
|
|||
qt_config->endGroup();
|
||||
}
|
||||
|
||||
// Options for variable bit rate live streaming taken from here:
|
||||
// https://developers.google.com/media/vp9/live-encoding
|
||||
const QString DEFAULT_VIDEO_ENCODER_OPTIONS =
|
||||
QStringLiteral("quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1");
|
||||
const QString DEFAULT_AUDIO_ENCODER_OPTIONS = QString{};
|
||||
|
||||
void Config::ReadVideoDumpingValues() {
|
||||
qt_config->beginGroup(QStringLiteral("VideoDumping"));
|
||||
|
||||
Settings::values.output_format =
|
||||
ReadSetting(QStringLiteral("output_format"), QStringLiteral("webm"))
|
||||
.toString()
|
||||
.toStdString();
|
||||
Settings::values.format_options =
|
||||
ReadSetting(QStringLiteral("format_options")).toString().toStdString();
|
||||
|
||||
Settings::values.video_encoder =
|
||||
ReadSetting(QStringLiteral("video_encoder"), QStringLiteral("libvpx-vp9"))
|
||||
.toString()
|
||||
.toStdString();
|
||||
|
||||
Settings::values.video_encoder_options =
|
||||
ReadSetting(QStringLiteral("video_encoder_options"), DEFAULT_VIDEO_ENCODER_OPTIONS)
|
||||
.toString()
|
||||
.toStdString();
|
||||
|
||||
Settings::values.video_bitrate =
|
||||
ReadSetting(QStringLiteral("video_bitrate"), 2500000).toULongLong();
|
||||
|
||||
Settings::values.audio_encoder =
|
||||
ReadSetting(QStringLiteral("audio_encoder"), QStringLiteral("libvorbis"))
|
||||
.toString()
|
||||
.toStdString();
|
||||
Settings::values.audio_encoder_options =
|
||||
ReadSetting(QStringLiteral("audio_encoder_options"), DEFAULT_AUDIO_ENCODER_OPTIONS)
|
||||
.toString()
|
||||
.toStdString();
|
||||
Settings::values.audio_bitrate =
|
||||
ReadSetting(QStringLiteral("audio_bitrate"), 64000).toULongLong();
|
||||
|
||||
qt_config->endGroup();
|
||||
}
|
||||
|
||||
void Config::ReadUIValues() {
|
||||
qt_config->beginGroup(QStringLiteral("UI"));
|
||||
|
||||
|
@ -624,6 +668,7 @@ void Config::SaveValues() {
|
|||
SaveMiscellaneousValues();
|
||||
SaveDebuggingValues();
|
||||
SaveWebServiceValues();
|
||||
SaveVideoDumpingValues();
|
||||
SaveUIValues();
|
||||
SaveUtilityValues();
|
||||
}
|
||||
|
@ -928,6 +973,33 @@ void Config::SaveSystemValues() {
|
|||
qt_config->endGroup();
|
||||
}
|
||||
|
||||
void Config::SaveVideoDumpingValues() {
|
||||
qt_config->beginGroup(QStringLiteral("VideoDumping"));
|
||||
|
||||
WriteSetting(QStringLiteral("output_format"),
|
||||
QString::fromStdString(Settings::values.output_format), QStringLiteral("webm"));
|
||||
WriteSetting(QStringLiteral("format_options"),
|
||||
QString::fromStdString(Settings::values.format_options));
|
||||
WriteSetting(QStringLiteral("video_encoder"),
|
||||
QString::fromStdString(Settings::values.video_encoder),
|
||||
QStringLiteral("libvpx-vp9"));
|
||||
WriteSetting(QStringLiteral("video_encoder_options"),
|
||||
QString::fromStdString(Settings::values.video_encoder_options),
|
||||
DEFAULT_VIDEO_ENCODER_OPTIONS);
|
||||
WriteSetting(QStringLiteral("video_bitrate"),
|
||||
static_cast<unsigned long long>(Settings::values.video_bitrate), 2500000);
|
||||
WriteSetting(QStringLiteral("audio_encoder"),
|
||||
QString::fromStdString(Settings::values.audio_encoder),
|
||||
QStringLiteral("libvorbis"));
|
||||
WriteSetting(QStringLiteral("audio_encoder_options"),
|
||||
QString::fromStdString(Settings::values.audio_encoder_options),
|
||||
DEFAULT_AUDIO_ENCODER_OPTIONS);
|
||||
WriteSetting(QStringLiteral("audio_bitrate"),
|
||||
static_cast<unsigned long long>(Settings::values.audio_bitrate), 64000);
|
||||
|
||||
qt_config->endGroup();
|
||||
}
|
||||
|
||||
void Config::SaveUIValues() {
|
||||
qt_config->beginGroup(QStringLiteral("UI"));
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ private:
|
|||
void ReadUpdaterValues();
|
||||
void ReadUtilityValues();
|
||||
void ReadWebServiceValues();
|
||||
void ReadVideoDumpingValues();
|
||||
|
||||
void SaveValues();
|
||||
void SaveAudioValues();
|
||||
|
@ -65,6 +66,7 @@ private:
|
|||
void SaveUpdaterValues();
|
||||
void SaveUtilityValues();
|
||||
void SaveWebServiceValues();
|
||||
void SaveVideoDumpingValues();
|
||||
|
||||
QVariant ReadSetting(const QString& name) const;
|
||||
QVariant ReadSetting(const QString& name, const QVariant& default_value) const;
|
||||
|
|
|
@ -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>
|
|
@ -87,6 +87,10 @@
|
|||
#include "citra_qt/discord_impl.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
#include "citra_qt/dumping/dumping_dialog.h"
|
||||
#endif
|
||||
|
||||
#ifdef QT_STATICPLUGIN
|
||||
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
|
||||
#endif
|
||||
|
@ -679,9 +683,7 @@ void GMainWindow::ConnectMenuEvents() {
|
|||
connect(ui.action_Capture_Screenshot, &QAction::triggered, this,
|
||||
&GMainWindow::OnCaptureScreenshot);
|
||||
|
||||
#ifndef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
ui.action_Dump_Video->setEnabled(false);
|
||||
#endif
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
connect(ui.action_Dump_Video, &QAction::triggered, [this] {
|
||||
if (ui.action_Dump_Video->isChecked()) {
|
||||
OnStartVideoDumping();
|
||||
|
@ -689,6 +691,9 @@ void GMainWindow::ConnectMenuEvents() {
|
|||
OnStopVideoDumping();
|
||||
}
|
||||
});
|
||||
#else
|
||||
ui.action_Dump_Video->setEnabled(false);
|
||||
#endif
|
||||
|
||||
// Help
|
||||
connect(ui.action_Open_Citra_Folder, &QAction::triggered, this,
|
||||
|
@ -975,8 +980,14 @@ void GMainWindow::BootGame(const QString& filename) {
|
|||
if (video_dumping_on_start) {
|
||||
Layout::FramebufferLayout layout{
|
||||
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
||||
Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(),
|
||||
"webm", layout);
|
||||
if (!Core::System::GetInstance().VideoDumper().StartDumping(
|
||||
video_dumping_path.toStdString(), layout)) {
|
||||
|
||||
QMessageBox::critical(
|
||||
this, tr("Citra"),
|
||||
tr("Could not start video dumping.<br>Refer to the log for details."));
|
||||
ui.action_Dump_Video->setChecked(false);
|
||||
}
|
||||
video_dumping_on_start = false;
|
||||
video_dumping_path.clear();
|
||||
}
|
||||
|
@ -992,11 +1003,13 @@ void GMainWindow::ShutdownGame() {
|
|||
HideFullscreen();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||
game_shutdown_delayed = true;
|
||||
OnStopVideoDumping();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
AllowOSSleep();
|
||||
|
||||
|
@ -1804,18 +1817,23 @@ void GMainWindow::OnCaptureScreenshot() {
|
|||
OnStartGame();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
void GMainWindow::OnStartVideoDumping() {
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this, tr("Save Video"), UISettings::values.video_dumping_path, tr("WebM Videos (*.webm)"));
|
||||
if (path.isEmpty()) {
|
||||
DumpingDialog dialog(this);
|
||||
if (dialog.exec() != QDialog::DialogCode::Accepted) {
|
||||
ui.action_Dump_Video->setChecked(false);
|
||||
return;
|
||||
}
|
||||
UISettings::values.video_dumping_path = QFileInfo(path).path();
|
||||
const auto path = dialog.GetFilePath();
|
||||
if (emulation_running) {
|
||||
Layout::FramebufferLayout layout{
|
||||
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
|
||||
Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout);
|
||||
if (!Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout)) {
|
||||
QMessageBox::critical(
|
||||
this, tr("Citra"),
|
||||
tr("Could not start video dumping.<br>Refer to the log for details."));
|
||||
ui.action_Dump_Video->setChecked(false);
|
||||
}
|
||||
} else {
|
||||
video_dumping_on_start = true;
|
||||
video_dumping_path = path;
|
||||
|
@ -1832,6 +1850,8 @@ void GMainWindow::OnStopVideoDumping() {
|
|||
const bool was_dumping = Core::System::GetInstance().VideoDumper().IsDumping();
|
||||
if (!was_dumping)
|
||||
return;
|
||||
|
||||
game_paused_for_dumping = emu_thread->IsRunning();
|
||||
OnPauseGame();
|
||||
|
||||
auto future =
|
||||
|
@ -1841,13 +1861,15 @@ void GMainWindow::OnStopVideoDumping() {
|
|||
if (game_shutdown_delayed) {
|
||||
game_shutdown_delayed = false;
|
||||
ShutdownGame();
|
||||
} else {
|
||||
} else if (game_paused_for_dumping) {
|
||||
game_paused_for_dumping = false;
|
||||
OnStartGame();
|
||||
}
|
||||
});
|
||||
future_watcher->setFuture(future);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void GMainWindow::UpdateStatusBar() {
|
||||
if (emu_thread == nullptr) {
|
||||
|
|
|
@ -200,8 +200,10 @@ private slots:
|
|||
void OnPlayMovie();
|
||||
void OnStopRecordingPlayback();
|
||||
void OnCaptureScreenshot();
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
void OnStartVideoDumping();
|
||||
void OnStopVideoDumping();
|
||||
#endif
|
||||
void OnCoreError(Core::System::ResultStatus, std::string);
|
||||
/// Called whenever a user selects Help->About Citra
|
||||
void OnMenuAboutCitra();
|
||||
|
@ -256,6 +258,8 @@ private:
|
|||
QString video_dumping_path;
|
||||
// Whether game shutdown is delayed due to video dumping
|
||||
bool game_shutdown_delayed = false;
|
||||
// Whether game was paused due to stopping video dumping
|
||||
bool game_paused_for_dumping = false;
|
||||
|
||||
// Debugger panes
|
||||
ProfilerWidget* profilerWidget;
|
||||
|
|
|
@ -135,4 +135,20 @@ void ParamPackage::Clear() {
|
|||
data.clear();
|
||||
}
|
||||
|
||||
ParamPackage::DataType::iterator ParamPackage::begin() {
|
||||
return data.begin();
|
||||
}
|
||||
|
||||
ParamPackage::DataType::const_iterator ParamPackage::begin() const {
|
||||
return data.begin();
|
||||
}
|
||||
|
||||
ParamPackage::DataType::iterator ParamPackage::end() {
|
||||
return data.end();
|
||||
}
|
||||
|
||||
ParamPackage::DataType::const_iterator ParamPackage::end() const {
|
||||
return data.end();
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <initializer_list>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Common {
|
||||
|
||||
/// A string-based key-value container supporting serializing to and deserializing from a string
|
||||
class ParamPackage {
|
||||
public:
|
||||
using DataType = std::unordered_map<std::string, std::string>;
|
||||
using DataType = std::map<std::string, std::string>;
|
||||
|
||||
ParamPackage() = default;
|
||||
explicit ParamPackage(const std::string& serialized);
|
||||
|
@ -35,6 +35,12 @@ public:
|
|||
void Erase(const std::string& key);
|
||||
void Clear();
|
||||
|
||||
// For range-based for
|
||||
DataType::iterator begin();
|
||||
DataType::const_iterator begin() const;
|
||||
DataType::iterator end();
|
||||
DataType::const_iterator end() const;
|
||||
|
||||
private:
|
||||
DataType data;
|
||||
};
|
||||
|
|
|
@ -492,5 +492,5 @@ if (ARCHITECTURE_x86_64)
|
|||
endif()
|
||||
|
||||
if (ENABLE_FFMPEG_VIDEO_DUMPER)
|
||||
target_link_libraries(core PRIVATE FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil)
|
||||
target_link_libraries(core PUBLIC FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil)
|
||||
endif()
|
||||
|
|
|
@ -310,6 +310,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo
|
|||
Service::Init(*this);
|
||||
GDBStub::DeferStart();
|
||||
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
|
||||
#else
|
||||
video_dumper = std::make_unique<VideoDumper::NullBackend>();
|
||||
#endif
|
||||
|
||||
VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory);
|
||||
if (result != VideoCore::ResultStatus::Success) {
|
||||
switch (result) {
|
||||
|
@ -322,12 +328,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo
|
|||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
|
||||
video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
|
||||
#else
|
||||
video_dumper = std::make_unique<VideoDumper::NullBackend>();
|
||||
#endif
|
||||
|
||||
LOG_DEBUG(Core, "Initialized OK");
|
||||
|
||||
initalized = true;
|
||||
|
|
|
@ -8,17 +8,7 @@
|
|||
namespace VideoDumper {
|
||||
|
||||
VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_)
|
||||
: width(width_), height(height_), stride(width * 4), data(width * height * 4) {
|
||||
// While copying, rotate the image to put the pixels in correct order
|
||||
// (As OpenGL returns pixel data starting from the lowest position)
|
||||
for (std::size_t i = 0; i < height; i++) {
|
||||
for (std::size_t j = 0; j < width; j++) {
|
||||
for (std::size_t k = 0; k < 4; k++) {
|
||||
data[i * stride + j * 4 + k] = data_[(height - i - 1) * stride + j * 4 + k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: width(width_), height(height_), stride(width * 4), data(data_, data_ + width * height * 4) {}
|
||||
|
||||
Backend::~Backend() = default;
|
||||
NullBackend::~NullBackend() = default;
|
||||
|
|
|
@ -28,10 +28,9 @@ public:
|
|||
class Backend {
|
||||
public:
|
||||
virtual ~Backend();
|
||||
virtual bool StartDumping(const std::string& path, const std::string& format,
|
||||
const Layout::FramebufferLayout& layout) = 0;
|
||||
virtual void AddVideoFrame(const VideoFrame& frame) = 0;
|
||||
virtual void AddAudioFrame(const AudioCore::StereoFrame16& frame) = 0;
|
||||
virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0;
|
||||
virtual void AddVideoFrame(VideoFrame frame) = 0;
|
||||
virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0;
|
||||
virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0;
|
||||
virtual void StopDumping() = 0;
|
||||
virtual bool IsDumping() const = 0;
|
||||
|
@ -41,12 +40,12 @@ public:
|
|||
class NullBackend : public Backend {
|
||||
public:
|
||||
~NullBackend() override;
|
||||
bool StartDumping(const std::string& /*path*/, const std::string& /*format*/,
|
||||
bool StartDumping(const std::string& /*path*/,
|
||||
const Layout::FramebufferLayout& /*layout*/) override {
|
||||
return false;
|
||||
}
|
||||
void AddVideoFrame(const VideoFrame& /*frame*/) override {}
|
||||
void AddAudioFrame(const AudioCore::StereoFrame16& /*frame*/) override {}
|
||||
void AddVideoFrame(VideoFrame /*frame*/) override {}
|
||||
void AddAudioFrame(AudioCore::StereoFrame16 /*frame*/) override {}
|
||||
void AddAudioSample(const std::array<s16, 2>& /*sample*/) override {}
|
||||
void StopDumping() override {}
|
||||
bool IsDumping() const override {
|
||||
|
|
|
@ -2,15 +2,19 @@
|
|||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <unordered_set>
|
||||
#include "common/assert.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/param_package.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/dumping/ffmpeg_backend.h"
|
||||
#include "core/settings.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/video_core.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
namespace VideoDumper {
|
||||
|
@ -27,14 +31,25 @@ void InitializeFFmpegLibraries() {
|
|||
initialized = true;
|
||||
}
|
||||
|
||||
AVDictionary* ToAVDictionary(const std::string& serialized) {
|
||||
Common::ParamPackage param_package{serialized};
|
||||
AVDictionary* result = nullptr;
|
||||
for (const auto& [key, value] : param_package) {
|
||||
av_dict_set(&result, key.c_str(), value.c_str(), 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
FFmpegStream::~FFmpegStream() {
|
||||
Free();
|
||||
}
|
||||
|
||||
bool FFmpegStream::Init(AVFormatContext* format_context_) {
|
||||
bool FFmpegStream::Init(FFmpegMuxer& muxer) {
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
format_context = format_context_;
|
||||
format_context = muxer.format_context.get();
|
||||
format_context_mutex = &muxer.format_context_mutex;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -47,14 +62,12 @@ void FFmpegStream::Flush() {
|
|||
}
|
||||
|
||||
void FFmpegStream::WritePacket(AVPacket& packet) {
|
||||
if (packet.pts != static_cast<s64>(AV_NOPTS_VALUE)) {
|
||||
packet.pts = av_rescale_q(packet.pts, codec_context->time_base, stream->time_base);
|
||||
}
|
||||
if (packet.dts != static_cast<s64>(AV_NOPTS_VALUE)) {
|
||||
packet.dts = av_rescale_q(packet.dts, codec_context->time_base, stream->time_base);
|
||||
}
|
||||
av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base);
|
||||
packet.stream_index = stream->index;
|
||||
{
|
||||
std::lock_guard lock{*format_context_mutex};
|
||||
av_interleaved_write_frame(format_context, &packet);
|
||||
}
|
||||
}
|
||||
|
||||
void FFmpegStream::SendFrame(AVFrame* frame) {
|
||||
|
@ -88,21 +101,18 @@ FFmpegVideoStream::~FFmpegVideoStream() {
|
|||
Free();
|
||||
}
|
||||
|
||||
bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* output_format,
|
||||
const Layout::FramebufferLayout& layout_) {
|
||||
bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout_) {
|
||||
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
if (!FFmpegStream::Init(format_context))
|
||||
if (!FFmpegStream::Init(muxer))
|
||||
return false;
|
||||
|
||||
layout = layout_;
|
||||
frame_count = 0;
|
||||
|
||||
// Initialize video codec
|
||||
// Ensure VP9 codec here, also to avoid patent issues
|
||||
constexpr AVCodecID codec_id = AV_CODEC_ID_VP9;
|
||||
const AVCodec* codec = avcodec_find_encoder(codec_id);
|
||||
const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str());
|
||||
codec_context.reset(avcodec_alloc_context3(codec));
|
||||
if (!codec || !codec_context) {
|
||||
LOG_ERROR(Render, "Could not find video encoder or allocate video codec context");
|
||||
|
@ -111,23 +121,28 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
|
|||
|
||||
// Configure video codec context
|
||||
codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
codec_context->bit_rate = 2500000;
|
||||
codec_context->bit_rate = Settings::values.video_bitrate;
|
||||
codec_context->width = layout.width;
|
||||
codec_context->height = layout.height;
|
||||
codec_context->time_base.num = 1;
|
||||
codec_context->time_base.den = 60;
|
||||
codec_context->gop_size = 12;
|
||||
codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
codec_context->thread_count = 8;
|
||||
if (output_format->flags & AVFMT_GLOBALHEADER)
|
||||
codec_context->pix_fmt = codec->pix_fmts ? codec->pix_fmts[0] : AV_PIX_FMT_YUV420P;
|
||||
if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
|
||||
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||
av_opt_set_int(codec_context.get(), "cpu-used", 5, 0);
|
||||
|
||||
if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) {
|
||||
AVDictionary* options = ToAVDictionary(Settings::values.video_encoder_options);
|
||||
if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
|
||||
LOG_ERROR(Render, "Could not open video codec");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
|
||||
char* buf = nullptr;
|
||||
av_dict_get_string(options, &buf, ':', ';');
|
||||
LOG_WARNING(Render, "Video encoder options not found: {}", buf);
|
||||
}
|
||||
|
||||
// Create video stream
|
||||
stream = avformat_new_stream(format_context, codec);
|
||||
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
|
||||
|
@ -141,7 +156,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
|
|||
scaled_frame->format = codec_context->pix_fmt;
|
||||
scaled_frame->width = layout.width;
|
||||
scaled_frame->height = layout.height;
|
||||
if (av_frame_get_buffer(scaled_frame.get(), 1) < 0) {
|
||||
if (av_frame_get_buffer(scaled_frame.get(), 0) < 0) {
|
||||
LOG_ERROR(Render, "Could not allocate frame buffer");
|
||||
return false;
|
||||
}
|
||||
|
@ -177,6 +192,10 @@ void FFmpegVideoStream::ProcessFrame(VideoFrame& frame) {
|
|||
current_frame->height = layout.height;
|
||||
|
||||
// Scale the frame
|
||||
if (av_frame_make_writable(scaled_frame.get()) < 0) {
|
||||
LOG_ERROR(Render, "Video frame dropped: Could not prepare frame");
|
||||
return;
|
||||
}
|
||||
if (sws_context) {
|
||||
sws_scale(sws_context.get(), current_frame->data, current_frame->linesize, 0, layout.height,
|
||||
scaled_frame->data, scaled_frame->linesize);
|
||||
|
@ -191,17 +210,16 @@ FFmpegAudioStream::~FFmpegAudioStream() {
|
|||
Free();
|
||||
}
|
||||
|
||||
bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
||||
bool FFmpegAudioStream::Init(FFmpegMuxer& muxer) {
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
if (!FFmpegStream::Init(format_context))
|
||||
if (!FFmpegStream::Init(muxer))
|
||||
return false;
|
||||
|
||||
sample_count = 0;
|
||||
frame_count = 0;
|
||||
|
||||
// Initialize audio codec
|
||||
constexpr AVCodecID codec_id = AV_CODEC_ID_VORBIS;
|
||||
const AVCodec* codec = avcodec_find_encoder(codec_id);
|
||||
const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str());
|
||||
codec_context.reset(avcodec_alloc_context3(codec));
|
||||
if (!codec || !codec_context) {
|
||||
LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context");
|
||||
|
@ -210,17 +228,52 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
|||
|
||||
// Configure audio codec context
|
||||
codec_context->codec_type = AVMEDIA_TYPE_AUDIO;
|
||||
codec_context->bit_rate = 64000;
|
||||
codec_context->bit_rate = Settings::values.audio_bitrate;
|
||||
if (codec->sample_fmts) {
|
||||
codec_context->sample_fmt = codec->sample_fmts[0];
|
||||
} else {
|
||||
codec_context->sample_fmt = AV_SAMPLE_FMT_S16P;
|
||||
}
|
||||
|
||||
if (codec->supported_samplerates) {
|
||||
codec_context->sample_rate = codec->supported_samplerates[0];
|
||||
// Prefer native sample rate if supported
|
||||
const int* ptr = codec->supported_samplerates;
|
||||
while ((*ptr)) {
|
||||
if ((*ptr) == AudioCore::native_sample_rate) {
|
||||
codec_context->sample_rate = AudioCore::native_sample_rate;
|
||||
break;
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
} else {
|
||||
codec_context->sample_rate = AudioCore::native_sample_rate;
|
||||
}
|
||||
codec_context->time_base.num = 1;
|
||||
codec_context->time_base.den = codec_context->sample_rate;
|
||||
codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
codec_context->channels = 2;
|
||||
if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
|
||||
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||
|
||||
if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) {
|
||||
AVDictionary* options = ToAVDictionary(Settings::values.audio_encoder_options);
|
||||
if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
|
||||
LOG_ERROR(Render, "Could not open audio codec");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
|
||||
char* buf = nullptr;
|
||||
av_dict_get_string(options, &buf, ':', ';');
|
||||
LOG_WARNING(Render, "Audio encoder options not found: {}", buf);
|
||||
}
|
||||
|
||||
if (codec_context->frame_size) {
|
||||
frame_size = static_cast<u64>(codec_context->frame_size);
|
||||
} else { // variable frame size support
|
||||
frame_size = std::tuple_size<AudioCore::StereoFrame16>::value;
|
||||
}
|
||||
|
||||
// Create audio stream
|
||||
stream = avformat_new_stream(format_context, codec);
|
||||
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
|
||||
|
@ -234,6 +287,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
|||
audio_frame->format = codec_context->sample_fmt;
|
||||
audio_frame->channel_layout = codec_context->channel_layout;
|
||||
audio_frame->channels = codec_context->channels;
|
||||
audio_frame->sample_rate = codec_context->sample_rate;
|
||||
|
||||
// Allocate SWR context
|
||||
auto* context =
|
||||
|
@ -253,7 +307,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
|
|||
// Allocate resampled data
|
||||
int error =
|
||||
av_samples_alloc_array_and_samples(&resampled_data, nullptr, codec_context->channels,
|
||||
codec_context->frame_size, codec_context->sample_fmt, 0);
|
||||
frame_size, codec_context->sample_fmt, 0);
|
||||
if (error < 0) {
|
||||
LOG_ERROR(Render, "Could not allocate samples storage");
|
||||
return false;
|
||||
|
@ -274,39 +328,79 @@ void FFmpegAudioStream::Free() {
|
|||
av_freep(&resampled_data);
|
||||
}
|
||||
|
||||
void FFmpegAudioStream::ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) {
|
||||
void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0,
|
||||
const VariableAudioFrame& channel1) {
|
||||
ASSERT_MSG(channel0.size() == channel1.size(),
|
||||
"Frames of the two channels must have the same number of samples");
|
||||
std::array<const u8*, 2> src_data = {reinterpret_cast<u8*>(channel0.data()),
|
||||
reinterpret_cast<u8*>(channel1.data())};
|
||||
if (swr_convert(swr_context.get(), resampled_data, channel0.size(), src_data.data(),
|
||||
channel0.size()) < 0) {
|
||||
|
||||
const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt);
|
||||
std::array<const u8*, 2> src_data = {reinterpret_cast<const u8*>(channel0.data()),
|
||||
reinterpret_cast<const u8*>(channel1.data())};
|
||||
|
||||
std::array<u8*, 2> dst_data;
|
||||
if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
|
||||
dst_data = {resampled_data[0] + sample_size * offset,
|
||||
resampled_data[1] + sample_size * offset};
|
||||
} else {
|
||||
dst_data = {resampled_data[0] + sample_size * offset * 2}; // 2 channels
|
||||
}
|
||||
|
||||
auto resampled_count = swr_convert(swr_context.get(), dst_data.data(), frame_size - offset,
|
||||
src_data.data(), channel0.size());
|
||||
if (resampled_count < 0) {
|
||||
LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
|
||||
return;
|
||||
}
|
||||
|
||||
offset += resampled_count;
|
||||
if (offset < frame_size) { // Still not enough to form a frame
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// Prepare frame
|
||||
audio_frame->nb_samples = channel0.size();
|
||||
audio_frame->nb_samples = frame_size;
|
||||
audio_frame->data[0] = resampled_data[0];
|
||||
if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
|
||||
audio_frame->data[1] = resampled_data[1];
|
||||
audio_frame->pts = sample_count;
|
||||
sample_count += channel0.size();
|
||||
}
|
||||
audio_frame->pts = frame_count * frame_size;
|
||||
frame_count++;
|
||||
|
||||
SendFrame(audio_frame.get());
|
||||
|
||||
// swr_convert buffers input internally. Try to get more resampled data
|
||||
resampled_count = swr_convert(swr_context.get(), resampled_data, frame_size, nullptr, 0);
|
||||
if (resampled_count < 0) {
|
||||
LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
|
||||
return;
|
||||
}
|
||||
if (static_cast<u64>(resampled_count) < frame_size) {
|
||||
offset = resampled_count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t FFmpegAudioStream::GetAudioFrameSize() const {
|
||||
ASSERT_MSG(codec_context, "Codec context is not initialized yet!");
|
||||
return codec_context->frame_size;
|
||||
void FFmpegAudioStream::Flush() {
|
||||
// Send the last samples
|
||||
audio_frame->nb_samples = offset;
|
||||
audio_frame->data[0] = resampled_data[0];
|
||||
if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
|
||||
audio_frame->data[1] = resampled_data[1];
|
||||
}
|
||||
audio_frame->pts = frame_count * frame_size;
|
||||
|
||||
SendFrame(audio_frame.get());
|
||||
|
||||
FFmpegStream::Flush();
|
||||
}
|
||||
|
||||
FFmpegMuxer::~FFmpegMuxer() {
|
||||
Free();
|
||||
}
|
||||
|
||||
bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
|
||||
const Layout::FramebufferLayout& layout) {
|
||||
bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) {
|
||||
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
|
@ -315,9 +409,8 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
|
|||
}
|
||||
|
||||
// Get output format
|
||||
// Ensure webm here to avoid patent issues
|
||||
ASSERT_MSG(format == "webm", "Only webm is allowed for frame dumping");
|
||||
auto* output_format = av_guess_format(format.c_str(), path.c_str(), "video/webm");
|
||||
const auto format = Settings::values.output_format;
|
||||
auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr);
|
||||
if (!output_format) {
|
||||
LOG_ERROR(Render, "Could not get format {}", format);
|
||||
return false;
|
||||
|
@ -333,18 +426,24 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
|
|||
}
|
||||
format_context.reset(format_context_raw);
|
||||
|
||||
if (!video_stream.Init(format_context.get(), output_format, layout))
|
||||
if (!video_stream.Init(*this, layout))
|
||||
return false;
|
||||
if (!audio_stream.Init(format_context.get()))
|
||||
if (!audio_stream.Init(*this))
|
||||
return false;
|
||||
|
||||
AVDictionary* options = ToAVDictionary(Settings::values.format_options);
|
||||
// Open video file
|
||||
if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 ||
|
||||
avformat_write_header(format_context.get(), nullptr)) {
|
||||
avformat_write_header(format_context.get(), &options)) {
|
||||
|
||||
LOG_ERROR(Render, "Could not open {}", path);
|
||||
return false;
|
||||
}
|
||||
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
|
||||
char* buf = nullptr;
|
||||
av_dict_get_string(options, &buf, ':', ';');
|
||||
LOG_WARNING(Render, "Format options not found: {}", buf);
|
||||
}
|
||||
|
||||
LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height);
|
||||
return true;
|
||||
|
@ -360,7 +459,8 @@ void FFmpegMuxer::ProcessVideoFrame(VideoFrame& frame) {
|
|||
video_stream.ProcessFrame(frame);
|
||||
}
|
||||
|
||||
void FFmpegMuxer::ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) {
|
||||
void FFmpegMuxer::ProcessAudioFrame(const VariableAudioFrame& channel0,
|
||||
const VariableAudioFrame& channel1) {
|
||||
audio_stream.ProcessFrame(channel0, channel1);
|
||||
}
|
||||
|
||||
|
@ -372,11 +472,9 @@ void FFmpegMuxer::FlushAudio() {
|
|||
audio_stream.Flush();
|
||||
}
|
||||
|
||||
std::size_t FFmpegMuxer::GetAudioFrameSize() const {
|
||||
return audio_stream.GetAudioFrameSize();
|
||||
}
|
||||
|
||||
void FFmpegMuxer::WriteTrailer() {
|
||||
std::lock_guard lock{format_context_mutex};
|
||||
av_interleaved_write_frame(format_context.get(), nullptr);
|
||||
av_write_trailer(format_context.get());
|
||||
}
|
||||
|
||||
|
@ -392,12 +490,11 @@ FFmpegBackend::~FFmpegBackend() {
|
|||
ffmpeg.Free();
|
||||
}
|
||||
|
||||
bool FFmpegBackend::StartDumping(const std::string& path, const std::string& format,
|
||||
const Layout::FramebufferLayout& layout) {
|
||||
bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) {
|
||||
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
if (!ffmpeg.Init(path, format, layout)) {
|
||||
if (!ffmpeg.Init(path, layout)) {
|
||||
ffmpeg.Free();
|
||||
return false;
|
||||
}
|
||||
|
@ -450,31 +547,29 @@ bool FFmpegBackend::StartDumping(const std::string& path, const std::string& for
|
|||
return true;
|
||||
}
|
||||
|
||||
void FFmpegBackend::AddVideoFrame(const VideoFrame& frame) {
|
||||
void FFmpegBackend::AddVideoFrame(VideoFrame frame) {
|
||||
event1.Wait();
|
||||
video_frame_buffers[next_buffer] = std::move(frame);
|
||||
event2.Set();
|
||||
}
|
||||
|
||||
void FFmpegBackend::AddAudioFrame(const AudioCore::StereoFrame16& frame) {
|
||||
std::array<std::array<s16, 160>, 2> refactored_frame;
|
||||
void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) {
|
||||
std::array<VariableAudioFrame, 2> refactored_frame;
|
||||
for (auto& channel : refactored_frame) {
|
||||
channel.resize(frame.size());
|
||||
}
|
||||
for (std::size_t i = 0; i < frame.size(); i++) {
|
||||
refactored_frame[0][i] = frame[i][0];
|
||||
refactored_frame[1][i] = frame[i][1];
|
||||
}
|
||||
|
||||
for (auto i : {0, 1}) {
|
||||
audio_buffers[i].insert(audio_buffers[i].end(), refactored_frame[i].begin(),
|
||||
refactored_frame[i].end());
|
||||
}
|
||||
CheckAudioBuffer();
|
||||
audio_frame_queues[0].Push(std::move(refactored_frame[0]));
|
||||
audio_frame_queues[1].Push(std::move(refactored_frame[1]));
|
||||
}
|
||||
|
||||
void FFmpegBackend::AddAudioSample(const std::array<s16, 2>& sample) {
|
||||
for (auto i : {0, 1}) {
|
||||
audio_buffers[i].push_back(sample[i]);
|
||||
}
|
||||
CheckAudioBuffer();
|
||||
audio_frame_queues[0].Push(VariableAudioFrame{sample[0]});
|
||||
audio_frame_queues[1].Push(VariableAudioFrame{sample[1]});
|
||||
}
|
||||
|
||||
void FFmpegBackend::StopDumping() {
|
||||
|
@ -484,12 +579,6 @@ void FFmpegBackend::StopDumping() {
|
|||
// Flush the video processing queue
|
||||
AddVideoFrame(VideoFrame());
|
||||
for (auto i : {0, 1}) {
|
||||
// Add remaining data to audio queue
|
||||
if (audio_buffers[i].size() >= 0) {
|
||||
VariableAudioFrame buffer(audio_buffers[i].begin(), audio_buffers[i].end());
|
||||
audio_frame_queues[i].Push(std::move(buffer));
|
||||
audio_buffers[i].clear();
|
||||
}
|
||||
// Flush the audio processing queue
|
||||
audio_frame_queues[i].Push(VariableAudioFrame());
|
||||
}
|
||||
|
@ -513,18 +602,234 @@ void FFmpegBackend::EndDumping() {
|
|||
processing_ended.Set();
|
||||
}
|
||||
|
||||
void FFmpegBackend::CheckAudioBuffer() {
|
||||
for (auto i : {0, 1}) {
|
||||
const std::size_t frame_size = ffmpeg.GetAudioFrameSize();
|
||||
// Add audio data to the queue when there is enough to form a frame
|
||||
while (audio_buffers[i].size() >= frame_size) {
|
||||
VariableAudioFrame buffer(audio_buffers[i].begin(),
|
||||
audio_buffers[i].begin() + frame_size);
|
||||
audio_frame_queues[i].Push(std::move(buffer));
|
||||
// To std string, but handles nullptr
|
||||
std::string ToStdString(const char* str, const std::string& fallback = "") {
|
||||
return str ? std::string{str} : fallback;
|
||||
}
|
||||
|
||||
audio_buffers[i].erase(audio_buffers[i].begin(), audio_buffers[i].begin() + frame_size);
|
||||
std::string FormatDuration(s64 duration) {
|
||||
// The following is implemented according to libavutil code (opt.c)
|
||||
std::string out;
|
||||
if (duration < 0 && duration != std::numeric_limits<s64>::min()) {
|
||||
out.append("-");
|
||||
duration = -duration;
|
||||
}
|
||||
if (duration == std::numeric_limits<s64>::max()) {
|
||||
return "INT64_MAX";
|
||||
} else if (duration == std::numeric_limits<s64>::min()) {
|
||||
return "INT64_MIN";
|
||||
} else if (duration > 3600ll * 1000000ll) {
|
||||
out.append(fmt::format("{}:{:02d}:{:02d}.{:06d}", duration / 3600000000ll,
|
||||
((duration / 60000000ll) % 60), ((duration / 1000000ll) % 60),
|
||||
duration % 1000000));
|
||||
} else if (duration > 60ll * 1000000ll) {
|
||||
out.append(fmt::format("{}:{:02d}.{:06d}", duration / 60000000ll,
|
||||
((duration / 1000000ll) % 60), duration % 1000000));
|
||||
} else {
|
||||
out.append(fmt::format("{}.{:06d}", duration / 1000000ll, duration % 1000000));
|
||||
}
|
||||
while (out.back() == '0') {
|
||||
out.erase(out.size() - 1, 1);
|
||||
}
|
||||
if (out.back() == '.') {
|
||||
out.erase(out.size() - 1, 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string FormatDefaultValue(const AVOption* option,
|
||||
const std::vector<OptionInfo::NamedConstant>& named_constants) {
|
||||
// The following is taken and modified from libavutil code (opt.c)
|
||||
switch (option->type) {
|
||||
case AV_OPT_TYPE_BOOL: {
|
||||
const auto value = option->default_val.i64;
|
||||
if (value < 0) {
|
||||
return "auto";
|
||||
}
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
case AV_OPT_TYPE_FLAGS: {
|
||||
const auto value = option->default_val.i64;
|
||||
std::string out;
|
||||
for (const auto& constant : named_constants) {
|
||||
if (!(value & constant.value)) {
|
||||
continue;
|
||||
}
|
||||
if (!out.empty()) {
|
||||
out.append("+");
|
||||
}
|
||||
out.append(constant.name);
|
||||
}
|
||||
return out.empty() ? fmt::format("{}", value) : out;
|
||||
}
|
||||
case AV_OPT_TYPE_DURATION: {
|
||||
return FormatDuration(option->default_val.i64);
|
||||
}
|
||||
case AV_OPT_TYPE_INT:
|
||||
case AV_OPT_TYPE_UINT64:
|
||||
case AV_OPT_TYPE_INT64: {
|
||||
const auto value = option->default_val.i64;
|
||||
for (const auto& constant : named_constants) {
|
||||
if (constant.value == value) {
|
||||
return constant.name;
|
||||
}
|
||||
}
|
||||
return fmt::format("{}", value);
|
||||
}
|
||||
case AV_OPT_TYPE_DOUBLE:
|
||||
case AV_OPT_TYPE_FLOAT: {
|
||||
return fmt::format("{}", option->default_val.dbl);
|
||||
}
|
||||
case AV_OPT_TYPE_RATIONAL: {
|
||||
const auto q = av_d2q(option->default_val.dbl, std::numeric_limits<int>::max());
|
||||
return fmt::format("{}/{}", q.num, q.den);
|
||||
}
|
||||
case AV_OPT_TYPE_PIXEL_FMT: {
|
||||
const char* name = av_get_pix_fmt_name(static_cast<AVPixelFormat>(option->default_val.i64));
|
||||
return ToStdString(name, "none");
|
||||
}
|
||||
case AV_OPT_TYPE_SAMPLE_FMT: {
|
||||
const char* name =
|
||||
av_get_sample_fmt_name(static_cast<AVSampleFormat>(option->default_val.i64));
|
||||
return ToStdString(name, "none");
|
||||
}
|
||||
case AV_OPT_TYPE_COLOR:
|
||||
case AV_OPT_TYPE_IMAGE_SIZE:
|
||||
case AV_OPT_TYPE_STRING:
|
||||
case AV_OPT_TYPE_DICT:
|
||||
case AV_OPT_TYPE_VIDEO_RATE: {
|
||||
return ToStdString(option->default_val.str);
|
||||
}
|
||||
case AV_OPT_TYPE_CHANNEL_LAYOUT: {
|
||||
return fmt::format("{:#x}", option->default_val.i64);
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void GetOptionListSingle(std::vector<OptionInfo>& out, const AVClass* av_class) {
|
||||
if (av_class == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AVOption* current = nullptr;
|
||||
std::unordered_map<std::string, std::vector<OptionInfo::NamedConstant>> named_constants_map;
|
||||
// First iteration: find and place all named constants
|
||||
while ((current = av_opt_next(&av_class, current))) {
|
||||
if (current->type != AV_OPT_TYPE_CONST || !current->unit) {
|
||||
continue;
|
||||
}
|
||||
named_constants_map[current->unit].push_back(
|
||||
{current->name, ToStdString(current->help), current->default_val.i64});
|
||||
}
|
||||
// Second iteration: find all options
|
||||
current = nullptr;
|
||||
while ((current = av_opt_next(&av_class, current))) {
|
||||
// Currently we cannot handle binary options
|
||||
if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) {
|
||||
continue;
|
||||
}
|
||||
std::vector<OptionInfo::NamedConstant> named_constants;
|
||||
if (current->unit && named_constants_map.count(current->unit)) {
|
||||
named_constants = named_constants_map.at(current->unit);
|
||||
}
|
||||
const auto default_value = FormatDefaultValue(current, named_constants);
|
||||
out.push_back({current->name, ToStdString(current->help), current->type, default_value,
|
||||
std::move(named_constants), current->min, current->max});
|
||||
}
|
||||
}
|
||||
|
||||
void GetOptionList(std::vector<OptionInfo>& out, const AVClass* av_class, bool search_children) {
|
||||
if (av_class == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
GetOptionListSingle(out, av_class);
|
||||
|
||||
if (!search_children) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AVClass* child_class = nullptr;
|
||||
while ((child_class = av_opt_child_class_next(av_class, child_class))) {
|
||||
GetOptionListSingle(out, child_class);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<OptionInfo> GetOptionList(const AVClass* av_class, bool search_children) {
|
||||
std::vector<OptionInfo> out;
|
||||
GetOptionList(out, av_class, search_children);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<EncoderInfo> ListEncoders(AVMediaType type) {
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
std::vector<EncoderInfo> out;
|
||||
|
||||
const AVCodec* current = nullptr;
|
||||
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)
|
||||
while ((current = av_codec_next(current))) {
|
||||
#else
|
||||
void* data = nullptr; // For libavcodec to save the iteration state
|
||||
while ((current = av_codec_iterate(&data))) {
|
||||
#endif
|
||||
if (!av_codec_is_encoder(current) || current->type != type) {
|
||||
continue;
|
||||
}
|
||||
out.push_back({current->name, ToStdString(current->long_name), current->id,
|
||||
GetOptionList(current->priv_class, true)});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<OptionInfo> GetEncoderGenericOptions() {
|
||||
return GetOptionList(avcodec_get_class(), false);
|
||||
}
|
||||
|
||||
std::vector<FormatInfo> ListFormats() {
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
std::vector<FormatInfo> out;
|
||||
|
||||
const AVOutputFormat* current = nullptr;
|
||||
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
|
||||
while ((current = av_oformat_next(current))) {
|
||||
#else
|
||||
void* data = nullptr; // For libavformat to save the iteration state
|
||||
while ((current = av_muxer_iterate(&data))) {
|
||||
#endif
|
||||
std::vector<std::string> extensions;
|
||||
Common::SplitString(ToStdString(current->extensions), ',', extensions);
|
||||
|
||||
std::set<AVCodecID> supported_video_codecs;
|
||||
std::set<AVCodecID> supported_audio_codecs;
|
||||
// Go through all codecs
|
||||
const AVCodecDescriptor* codec = nullptr;
|
||||
while ((codec = avcodec_descriptor_next(codec))) {
|
||||
if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) {
|
||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||
supported_video_codecs.emplace(codec->id);
|
||||
} else if (codec->type == AVMEDIA_TYPE_AUDIO) {
|
||||
supported_audio_codecs.emplace(codec->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (supported_video_codecs.empty() || supported_audio_codecs.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push_back({current->name, ToStdString(current->long_name), std::move(extensions),
|
||||
std::move(supported_video_codecs), std::move(supported_audio_codecs),
|
||||
GetOptionList(current->priv_class, true)});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<OptionInfo> GetFormatGenericOptions() {
|
||||
return GetOptionList(avformat_get_class(), false);
|
||||
}
|
||||
|
||||
} // namespace VideoDumper
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
@ -19,6 +20,7 @@
|
|||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <libswscale/swscale.h>
|
||||
}
|
||||
|
@ -29,13 +31,15 @@ using VariableAudioFrame = std::vector<s16>;
|
|||
|
||||
void InitFFmpegLibraries();
|
||||
|
||||
class FFmpegMuxer;
|
||||
|
||||
/**
|
||||
* Wrapper around FFmpeg AVCodecContext + AVStream.
|
||||
* Rescales/Resamples, encodes and writes a frame.
|
||||
*/
|
||||
class FFmpegStream {
|
||||
public:
|
||||
bool Init(AVFormatContext* format_context);
|
||||
bool Init(FFmpegMuxer& muxer);
|
||||
void Free();
|
||||
void Flush();
|
||||
|
||||
|
@ -58,6 +62,7 @@ protected:
|
|||
};
|
||||
|
||||
AVFormatContext* format_context{};
|
||||
std::mutex* format_context_mutex{};
|
||||
std::unique_ptr<AVCodecContext, AVCodecContextDeleter> codec_context{};
|
||||
AVStream* stream{};
|
||||
};
|
||||
|
@ -70,8 +75,7 @@ class FFmpegVideoStream : public FFmpegStream {
|
|||
public:
|
||||
~FFmpegVideoStream();
|
||||
|
||||
bool Init(AVFormatContext* format_context, AVOutputFormat* output_format,
|
||||
const Layout::FramebufferLayout& layout);
|
||||
bool Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout);
|
||||
void Free();
|
||||
void ProcessFrame(VideoFrame& frame);
|
||||
|
||||
|
@ -96,15 +100,16 @@ private:
|
|||
/**
|
||||
* A FFmpegStream used for audio data.
|
||||
* Resamples (converts), encodes and writes a frame.
|
||||
* This also temporarily stores resampled audio data before there are enough to form a frame.
|
||||
*/
|
||||
class FFmpegAudioStream : public FFmpegStream {
|
||||
public:
|
||||
~FFmpegAudioStream();
|
||||
|
||||
bool Init(AVFormatContext* format_context);
|
||||
bool Init(FFmpegMuxer& muxer);
|
||||
void Free();
|
||||
void ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1);
|
||||
std::size_t GetAudioFrameSize() const;
|
||||
void ProcessFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1);
|
||||
void Flush();
|
||||
|
||||
private:
|
||||
struct SwrContextDeleter {
|
||||
|
@ -113,12 +118,14 @@ private:
|
|||
}
|
||||
};
|
||||
|
||||
u64 sample_count{};
|
||||
u64 frame_size{};
|
||||
u64 frame_count{};
|
||||
|
||||
std::unique_ptr<AVFrame, AVFrameDeleter> audio_frame{};
|
||||
std::unique_ptr<SwrContext, SwrContextDeleter> swr_context{};
|
||||
|
||||
u8** resampled_data{};
|
||||
u64 offset{}; // Number of output samples that are currently in resampled_data.
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -129,14 +136,12 @@ class FFmpegMuxer {
|
|||
public:
|
||||
~FFmpegMuxer();
|
||||
|
||||
bool Init(const std::string& path, const std::string& format,
|
||||
const Layout::FramebufferLayout& layout);
|
||||
bool Init(const std::string& path, const Layout::FramebufferLayout& layout);
|
||||
void Free();
|
||||
void ProcessVideoFrame(VideoFrame& frame);
|
||||
void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1);
|
||||
void ProcessAudioFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1);
|
||||
void FlushVideo();
|
||||
void FlushAudio();
|
||||
std::size_t GetAudioFrameSize() const;
|
||||
void WriteTrailer();
|
||||
|
||||
private:
|
||||
|
@ -150,28 +155,28 @@ private:
|
|||
FFmpegAudioStream audio_stream{};
|
||||
FFmpegVideoStream video_stream{};
|
||||
std::unique_ptr<AVFormatContext, AVFormatContextDeleter> format_context{};
|
||||
std::mutex format_context_mutex;
|
||||
|
||||
friend class FFmpegStream;
|
||||
};
|
||||
|
||||
/**
|
||||
* FFmpeg video dumping backend.
|
||||
* This class implements a double buffer, and an audio queue to keep audio data
|
||||
* before enough data is received to form a frame.
|
||||
* This class implements a double buffer.
|
||||
*/
|
||||
class FFmpegBackend : public Backend {
|
||||
public:
|
||||
FFmpegBackend();
|
||||
~FFmpegBackend() override;
|
||||
bool StartDumping(const std::string& path, const std::string& format,
|
||||
const Layout::FramebufferLayout& layout) override;
|
||||
void AddVideoFrame(const VideoFrame& frame) override;
|
||||
void AddAudioFrame(const AudioCore::StereoFrame16& frame) override;
|
||||
bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override;
|
||||
void AddVideoFrame(VideoFrame frame) override;
|
||||
void AddAudioFrame(AudioCore::StereoFrame16 frame) override;
|
||||
void AddAudioSample(const std::array<s16, 2>& sample) override;
|
||||
void StopDumping() override;
|
||||
bool IsDumping() const override;
|
||||
Layout::FramebufferLayout GetLayout() const override;
|
||||
|
||||
private:
|
||||
void CheckAudioBuffer();
|
||||
void EndDumping();
|
||||
|
||||
std::atomic_bool is_dumping = false; ///< Whether the backend is currently dumping
|
||||
|
@ -184,13 +189,51 @@ private:
|
|||
Common::Event event1, event2;
|
||||
std::thread video_processing_thread;
|
||||
|
||||
/// An audio buffer used to temporarily hold audio data, before the size is big enough
|
||||
/// to be sent to the encoder as a frame
|
||||
std::array<VariableAudioFrame, 2> audio_buffers;
|
||||
std::array<Common::SPSCQueue<VariableAudioFrame>, 2> audio_frame_queues;
|
||||
std::thread audio_processing_thread;
|
||||
|
||||
Common::Event processing_ended;
|
||||
};
|
||||
|
||||
/// Struct describing encoder/muxer options
|
||||
struct OptionInfo {
|
||||
std::string name;
|
||||
std::string description;
|
||||
AVOptionType type;
|
||||
std::string default_value;
|
||||
struct NamedConstant {
|
||||
std::string name;
|
||||
std::string description;
|
||||
s64 value;
|
||||
};
|
||||
std::vector<NamedConstant> named_constants;
|
||||
|
||||
// If this is a scalar type
|
||||
double min;
|
||||
double max;
|
||||
};
|
||||
|
||||
/// Struct describing an encoder
|
||||
struct EncoderInfo {
|
||||
std::string name;
|
||||
std::string long_name;
|
||||
AVCodecID codec;
|
||||
std::vector<OptionInfo> options;
|
||||
};
|
||||
|
||||
/// Struct describing a format
|
||||
struct FormatInfo {
|
||||
std::string name;
|
||||
std::string long_name;
|
||||
std::vector<std::string> extensions;
|
||||
std::set<AVCodecID> supported_video_codecs;
|
||||
std::set<AVCodecID> supported_audio_codecs;
|
||||
std::vector<OptionInfo> options;
|
||||
};
|
||||
|
||||
std::vector<EncoderInfo> ListEncoders(AVMediaType type);
|
||||
std::vector<OptionInfo> GetEncoderGenericOptions();
|
||||
std::vector<FormatInfo> ListFormats();
|
||||
std::vector<OptionInfo> GetFormatGenericOptions();
|
||||
|
||||
} // namespace VideoDumper
|
||||
|
|
|
@ -206,6 +206,18 @@ struct Values {
|
|||
std::string web_api_url;
|
||||
std::string citra_username;
|
||||
std::string citra_token;
|
||||
|
||||
// Video Dumping
|
||||
std::string output_format;
|
||||
std::string format_options;
|
||||
|
||||
std::string video_encoder;
|
||||
std::string video_encoder_options;
|
||||
u64 video_bitrate;
|
||||
|
||||
std::string audio_encoder;
|
||||
std::string audio_encoder_options;
|
||||
u64 audio_bitrate;
|
||||
} extern values;
|
||||
|
||||
// a special value for Values::region_value indicating that citra will automatically select a region
|
||||
|
|
|
@ -23,6 +23,8 @@ add_library(video_core STATIC
|
|||
regs_texturing.h
|
||||
renderer_base.cpp
|
||||
renderer_base.h
|
||||
renderer_opengl/frame_dumper_opengl.cpp
|
||||
renderer_opengl/frame_dumper_opengl.h
|
||||
renderer_opengl/gl_rasterizer.cpp
|
||||
renderer_opengl/gl_rasterizer.h
|
||||
renderer_opengl/gl_rasterizer_cache.cpp
|
||||
|
|
|
@ -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
|
|
@ -34,20 +34,6 @@
|
|||
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||
#include "video_core/video_core.h"
|
||||
|
||||
namespace Frontend {
|
||||
|
||||
struct Frame {
|
||||
u32 width{}; /// Width of the frame (to detect resize)
|
||||
u32 height{}; /// Height of the frame
|
||||
bool color_reloaded = false; /// Texture attachment was recreated (ie: resized)
|
||||
OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO
|
||||
OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread
|
||||
OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread
|
||||
GLsync render_fence{}; /// Fence created on the render thread
|
||||
GLsync present_fence{}; /// Fence created on the presentation thread
|
||||
};
|
||||
} // namespace Frontend
|
||||
|
||||
namespace OpenGL {
|
||||
|
||||
// If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have
|
||||
|
@ -78,6 +64,7 @@ public:
|
|||
std::queue<Frontend::Frame*>().swap(free_queue);
|
||||
present_queue.clear();
|
||||
present_cv.notify_all();
|
||||
free_cv.notify_all();
|
||||
}
|
||||
|
||||
void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override {
|
||||
|
@ -88,7 +75,7 @@ public:
|
|||
glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||
frame->color.handle);
|
||||
if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!");
|
||||
}
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo);
|
||||
|
@ -114,7 +101,7 @@ public:
|
|||
state.Apply();
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||
frame->color.handle);
|
||||
if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!");
|
||||
}
|
||||
prev_state.Apply();
|
||||
|
@ -144,19 +131,12 @@ public:
|
|||
present_cv.notify_one();
|
||||
}
|
||||
|
||||
Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
|
||||
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||
// wait for new entries in the present_queue
|
||||
present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
|
||||
[&] { return !present_queue.empty(); });
|
||||
if (present_queue.empty()) {
|
||||
// timed out waiting for a frame to draw so return the previous frame
|
||||
return previous_frame;
|
||||
}
|
||||
|
||||
// This is virtual as it is to be overriden in OGLVideoDumpingMailbox below.
|
||||
virtual void LoadPresentFrame() {
|
||||
// free the previous frame and add it back to the free queue
|
||||
if (previous_frame) {
|
||||
free_queue.push(previous_frame);
|
||||
free_cv.notify_one();
|
||||
}
|
||||
|
||||
// the newest entries are pushed to the front of the queue
|
||||
|
@ -168,8 +148,72 @@ public:
|
|||
}
|
||||
present_queue.clear();
|
||||
previous_frame = frame;
|
||||
}
|
||||
|
||||
Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
|
||||
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||
// wait for new entries in the present_queue
|
||||
present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
|
||||
[&] { return !present_queue.empty(); });
|
||||
if (present_queue.empty()) {
|
||||
// timed out waiting for a frame to draw so return the previous frame
|
||||
return previous_frame;
|
||||
}
|
||||
|
||||
LoadPresentFrame();
|
||||
return previous_frame;
|
||||
}
|
||||
};
|
||||
|
||||
/// This mailbox is different in that it will never discard rendered frames
|
||||
class OGLVideoDumpingMailbox : public OGLTextureMailbox {
|
||||
public:
|
||||
Frontend::Frame* GetRenderFrame() override {
|
||||
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||
|
||||
// If theres no free frames, we will wait until one shows up
|
||||
if (free_queue.empty()) {
|
||||
free_cv.wait(lock, [&] { return !free_queue.empty(); });
|
||||
}
|
||||
|
||||
if (free_queue.empty()) {
|
||||
LOG_CRITICAL(Render_OpenGL, "Could not get free frame");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Frontend::Frame* frame = free_queue.front();
|
||||
free_queue.pop();
|
||||
return frame;
|
||||
}
|
||||
|
||||
void LoadPresentFrame() override {
|
||||
// free the previous frame and add it back to the free queue
|
||||
if (previous_frame) {
|
||||
free_queue.push(previous_frame);
|
||||
free_cv.notify_one();
|
||||
}
|
||||
|
||||
Frontend::Frame* frame = present_queue.back();
|
||||
present_queue.pop_back();
|
||||
previous_frame = frame;
|
||||
|
||||
// Do not remove entries from the present_queue, as video dumping would require
|
||||
// that we preserve all frames
|
||||
}
|
||||
|
||||
Frontend::Frame* TryGetPresentFrame(int timeout_ms) override {
|
||||
std::unique_lock<std::mutex> lock(swap_chain_lock);
|
||||
// wait for new entries in the present_queue
|
||||
present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
|
||||
[&] { return !present_queue.empty(); });
|
||||
if (present_queue.empty()) {
|
||||
// timed out waiting for a frame
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LoadPresentFrame();
|
||||
return previous_frame;
|
||||
}
|
||||
};
|
||||
|
||||
static const char vertex_shader[] = R"(
|
||||
|
@ -278,21 +322,35 @@ struct ScreenRectVertex {
|
|||
*
|
||||
* The projection part of the matrix is trivial, hence these operations are represented
|
||||
* by a 3x2 matrix.
|
||||
*
|
||||
* @param flipped Whether the frame should be flipped upside down.
|
||||
*/
|
||||
static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, const float height) {
|
||||
static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, const float height,
|
||||
bool flipped) {
|
||||
|
||||
std::array<GLfloat, 3 * 2> matrix; // Laid out in column-major order
|
||||
|
||||
// Last matrix row is implicitly assumed to be [0, 0, 1].
|
||||
if (flipped) {
|
||||
// clang-format off
|
||||
matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f;
|
||||
matrix[1] = 0.f; matrix[3] = 2.f / height; matrix[5] = -1.f;
|
||||
// clang-format on
|
||||
} else {
|
||||
// clang-format off
|
||||
matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f;
|
||||
matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f;
|
||||
// Last matrix row is implicitly assumed to be [0, 0, 1].
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} {
|
||||
RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window)
|
||||
: RendererBase{window}, frame_dumper(Core::System::GetInstance().VideoDumper(), window) {
|
||||
|
||||
window.mailbox = std::make_unique<OGLTextureMailbox>();
|
||||
frame_dumper.mailbox = std::make_unique<OGLVideoDumpingMailbox>();
|
||||
}
|
||||
|
||||
RendererOpenGL::~RendererOpenGL() = default;
|
||||
|
@ -310,56 +368,14 @@ void RendererOpenGL::SwapBuffers() {
|
|||
|
||||
RenderScreenshot();
|
||||
|
||||
RenderVideoDumping();
|
||||
|
||||
const auto& layout = render_window.GetFramebufferLayout();
|
||||
RenderToMailbox(layout, render_window.mailbox, false);
|
||||
|
||||
Frontend::Frame* frame;
|
||||
{
|
||||
MICROPROFILE_SCOPE(OpenGL_WaitPresent);
|
||||
|
||||
frame = render_window.mailbox->GetRenderFrame();
|
||||
|
||||
// Clean up sync objects before drawing
|
||||
|
||||
// INTEL driver workaround. We can't delete the previous render sync object until we are
|
||||
// sure that the presentation is done
|
||||
if (frame->present_fence) {
|
||||
glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
||||
if (frame_dumper.IsDumping()) {
|
||||
RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true);
|
||||
}
|
||||
|
||||
// delete the draw fence if the frame wasn't presented
|
||||
if (frame->render_fence) {
|
||||
glDeleteSync(frame->render_fence);
|
||||
frame->render_fence = 0;
|
||||
}
|
||||
|
||||
// wait for the presentation to be done
|
||||
if (frame->present_fence) {
|
||||
glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(frame->present_fence);
|
||||
frame->present_fence = 0;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
MICROPROFILE_SCOPE(OpenGL_RenderFrame);
|
||||
// Recreate the frame if the size of the window has changed
|
||||
if (layout.width != frame->width || layout.height != frame->height) {
|
||||
LOG_DEBUG(Render_OpenGL, "Reloading render frame");
|
||||
render_window.mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
|
||||
}
|
||||
|
||||
GLuint render_texture = frame->color.handle;
|
||||
state.draw.draw_framebuffer = frame->render.handle;
|
||||
state.Apply();
|
||||
DrawScreens(layout);
|
||||
// Create a fence for the frontend to wait on and swap this frame to OffTex
|
||||
frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
render_window.mailbox->ReleaseRenderFrame(frame);
|
||||
m_current_frame++;
|
||||
}
|
||||
|
||||
Core::System::GetInstance().perf_stats->EndSystemFrame();
|
||||
|
||||
|
@ -395,7 +411,7 @@ void RendererOpenGL::RenderScreenshot() {
|
|||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||
renderbuffer);
|
||||
|
||||
DrawScreens(layout);
|
||||
DrawScreens(layout, false);
|
||||
|
||||
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
VideoCore::g_screenshot_bits);
|
||||
|
@ -448,33 +464,54 @@ void RendererOpenGL::PrepareRendertarget() {
|
|||
}
|
||||
}
|
||||
|
||||
void RendererOpenGL::RenderVideoDumping() {
|
||||
if (cleanup_video_dumping.exchange(false)) {
|
||||
ReleaseVideoDumpingGLObjects();
|
||||
void RendererOpenGL::RenderToMailbox(const Layout::FramebufferLayout& layout,
|
||||
std::unique_ptr<Frontend::TextureMailbox>& mailbox,
|
||||
bool flipped) {
|
||||
|
||||
Frontend::Frame* frame;
|
||||
{
|
||||
MICROPROFILE_SCOPE(OpenGL_WaitPresent);
|
||||
|
||||
frame = mailbox->GetRenderFrame();
|
||||
|
||||
// Clean up sync objects before drawing
|
||||
|
||||
// INTEL driver workaround. We can't delete the previous render sync object until we are
|
||||
// sure that the presentation is done
|
||||
if (frame->present_fence) {
|
||||
glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
||||
}
|
||||
|
||||
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
|
||||
if (prepare_video_dumping.exchange(false)) {
|
||||
InitVideoDumpingGLObjects();
|
||||
// delete the draw fence if the frame wasn't presented
|
||||
if (frame->render_fence) {
|
||||
glDeleteSync(frame->render_fence);
|
||||
frame->render_fence = 0;
|
||||
}
|
||||
|
||||
const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout();
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_dumping_framebuffer.handle);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle);
|
||||
DrawScreens(layout);
|
||||
// wait for the presentation to be done
|
||||
if (frame->present_fence) {
|
||||
glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(frame->present_fence);
|
||||
frame->present_fence = 0;
|
||||
}
|
||||
}
|
||||
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[current_pbo].handle);
|
||||
glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[next_pbo].handle);
|
||||
{
|
||||
MICROPROFILE_SCOPE(OpenGL_RenderFrame);
|
||||
// Recreate the frame if the size of the window has changed
|
||||
if (layout.width != frame->width || layout.height != frame->height) {
|
||||
LOG_DEBUG(Render_OpenGL, "Reloading render frame");
|
||||
mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
|
||||
}
|
||||
|
||||
GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
|
||||
VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
|
||||
Core::System::GetInstance().VideoDumper().AddVideoFrame(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;
|
||||
GLuint render_texture = frame->color.handle;
|
||||
state.draw.draw_framebuffer = frame->render.handle;
|
||||
state.Apply();
|
||||
DrawScreens(layout, flipped);
|
||||
// Create a fence for the frontend to wait on and swap this frame to OffTex
|
||||
frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
mailbox->ReleaseRenderFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -885,7 +922,7 @@ void RendererOpenGL::DrawSingleScreenStereo(const ScreenInfo& screen_info_l,
|
|||
/**
|
||||
* Draws the emulated screens to the emulator window.
|
||||
*/
|
||||
void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) {
|
||||
void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool flipped) {
|
||||
if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) {
|
||||
// Update background color before drawing
|
||||
glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
|
||||
|
@ -912,7 +949,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) {
|
|||
|
||||
// Set projection matrix
|
||||
std::array<GLfloat, 3 * 2> ortho_matrix =
|
||||
MakeOrthographicMatrix((float)layout.width, (float)layout.height);
|
||||
MakeOrthographicMatrix((float)layout.width, (float)layout.height, flipped);
|
||||
glUniformMatrix3x2fv(uniform_modelview_matrix, 1, GL_FALSE, ortho_matrix.data());
|
||||
|
||||
// Bind texture in Texture Unit 0
|
||||
|
@ -1051,41 +1088,11 @@ void RendererOpenGL::TryPresent(int timeout_ms) {
|
|||
void RendererOpenGL::UpdateFramerate() {}
|
||||
|
||||
void RendererOpenGL::PrepareVideoDumping() {
|
||||
prepare_video_dumping = true;
|
||||
frame_dumper.StartDumping();
|
||||
}
|
||||
|
||||
void RendererOpenGL::CleanupVideoDumping() {
|
||||
cleanup_video_dumping = true;
|
||||
}
|
||||
|
||||
void RendererOpenGL::InitVideoDumpingGLObjects() {
|
||||
const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout();
|
||||
|
||||
frame_dumping_framebuffer.Create();
|
||||
glGenRenderbuffers(1, &frame_dumping_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, frame_dumping_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle);
|
||||
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||
frame_dumping_renderbuffer);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
|
||||
for (auto& buffer : frame_dumping_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 RendererOpenGL::ReleaseVideoDumpingGLObjects() {
|
||||
frame_dumping_framebuffer.Release();
|
||||
glDeleteRenderbuffers(1, &frame_dumping_renderbuffer);
|
||||
|
||||
for (auto& buffer : frame_dumping_pbos) {
|
||||
buffer.Release();
|
||||
}
|
||||
frame_dumper.StopDumping();
|
||||
}
|
||||
|
||||
static const char* GetSource(GLenum source) {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "common/math_util.h"
|
||||
#include "core/hw/gpu.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/renderer_opengl/frame_dumper_opengl.h"
|
||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||
#include "video_core/renderer_opengl/gl_state.h"
|
||||
|
||||
|
@ -17,6 +18,20 @@ namespace Layout {
|
|||
struct FramebufferLayout;
|
||||
}
|
||||
|
||||
namespace Frontend {
|
||||
|
||||
struct Frame {
|
||||
u32 width{}; /// Width of the frame (to detect resize)
|
||||
u32 height{}; /// Height of the frame
|
||||
bool color_reloaded = false; /// Texture attachment was recreated (ie: resized)
|
||||
OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO
|
||||
OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread
|
||||
OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread
|
||||
GLsync render_fence{}; /// Fence created on the render thread
|
||||
GLsync present_fence{}; /// Fence created on the presentation thread
|
||||
};
|
||||
} // namespace Frontend
|
||||
|
||||
namespace OpenGL {
|
||||
|
||||
/// Structure used for storing information about the textures for each 3DS screen
|
||||
|
@ -72,10 +87,11 @@ private:
|
|||
void ReloadShader();
|
||||
void PrepareRendertarget();
|
||||
void RenderScreenshot();
|
||||
void RenderVideoDumping();
|
||||
void RenderToMailbox(const Layout::FramebufferLayout& layout,
|
||||
std::unique_ptr<Frontend::TextureMailbox>& mailbox, bool flipped);
|
||||
void ConfigureFramebufferTexture(TextureInfo& texture,
|
||||
const GPU::Regs::FramebufferConfig& framebuffer);
|
||||
void DrawScreens(const Layout::FramebufferLayout& layout);
|
||||
void DrawScreens(const Layout::FramebufferLayout& layout, bool flipped);
|
||||
void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h);
|
||||
void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h);
|
||||
void DrawSingleScreenStereoRotated(const ScreenInfo& screen_info_l,
|
||||
|
@ -91,9 +107,6 @@ private:
|
|||
// Fills active OpenGL texture with the given RGB color.
|
||||
void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture);
|
||||
|
||||
void InitVideoDumpingGLObjects();
|
||||
void ReleaseVideoDumpingGLObjects();
|
||||
|
||||
OpenGLState state;
|
||||
|
||||
// OpenGL object IDs
|
||||
|
@ -120,19 +133,7 @@ private:
|
|||
GLuint attrib_position;
|
||||
GLuint attrib_tex_coord;
|
||||
|
||||
// Frame dumping
|
||||
OGLFramebuffer frame_dumping_framebuffer;
|
||||
GLuint frame_dumping_renderbuffer;
|
||||
|
||||
// Whether prepare/cleanup video dumping has been requested.
|
||||
// They will be executed on next frame.
|
||||
std::atomic_bool prepare_video_dumping = false;
|
||||
std::atomic_bool cleanup_video_dumping = false;
|
||||
|
||||
// PBOs used to dump frames faster
|
||||
std::array<OGLBuffer, 2> frame_dumping_pbos;
|
||||
GLuint current_pbo = 1;
|
||||
GLuint next_pbo = 0;
|
||||
FrameDumperOpenGL frame_dumper;
|
||||
};
|
||||
|
||||
} // namespace OpenGL
|
||||
|
|
Loading…
Reference in New Issue