diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp index b6e74b82c..1bb2a1d34 100644 --- a/src/audio_core/dsp_interface.cpp +++ b/src/audio_core/dsp_interface.cpp @@ -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 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)); } } diff --git a/src/audio_core/dsp_interface.h b/src/audio_core/dsp_interface.h index fc3d7cab2..5b83e684d 100644 --- a/src/audio_core/dsp_interface.h +++ b/src/audio_core/dsp_interface.h @@ -100,7 +100,7 @@ public: void EnableStretching(bool enable); protected: - void OutputFrame(StereoFrame16& frame); + void OutputFrame(StereoFrame16 frame); void OutputSample(std::array sample); private: diff --git a/src/audio_core/hle/hle.cpp b/src/audio_core/hle/hle.cpp index 513ca45f3..1725fdf78 100644 --- a/src/audio_core/hle/hle.cpp +++ b/src/audio_core/hle/hle.cpp @@ -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; } diff --git a/src/audio_core/lle/lle.cpp b/src/audio_core/lle/lle.cpp index e9948d1c2..0121bae27 100644 --- a/src/audio_core/lle/lle.cpp +++ b/src/audio_core/lle/lle.cpp @@ -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 sample) { OutputSample(sample); }); + impl->teakra.SetAudioCallback( + [this](std::array sample) { OutputSample(std::move(sample)); }); } DspLle::~DspLle() = default; diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index ce06b31b7..a1455d373 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -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(); }); diff --git a/src/citra/config.cpp b/src/citra/config.cpp index e18300824..400c655ff 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -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() { diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 28ca505c8..a6f7d5585 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -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 = )"; } diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 7ca0398fa..836e3c8ae 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -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) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 7de0002db..2901022da 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -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(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(Settings::values.audio_bitrate), 64000); + + qt_config->endGroup(); +} + void Config::SaveUIValues() { qt_config->beginGroup(QStringLiteral("UI")); diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 7d65d41b6..8b1cf8193 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -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; diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/citra_qt/dumping/dumping_dialog.cpp new file mode 100644 index 000000000..70d165c01 --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.cpp @@ -0,0 +1,220 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#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->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(&QComboBox::currentIndexChanged), [this] { + ui->pathLineEdit->setText(QString{}); + ui->formatOptionsLineEdit->clear(); + PopulateEncoders(); + }); + + connect(ui->videoEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), + [this] { ui->videoEncoderOptionsLineEdit->clear(); }); + connect(ui->audioEncoderComboBox, qOverload(&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(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(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(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& specific_options, + const std::vector& 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(Settings::values.video_bitrate)); + ui->audioBitrateSpinBox->setValue(static_cast(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(); +} diff --git a/src/citra_qt/dumping/dumping_dialog.h b/src/citra_qt/dumping/dumping_dialog.h new file mode 100644 index 000000000..284f215c3 --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.h @@ -0,0 +1,43 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#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& specific_options, + const std::vector& generic_options, + QLineEdit* line_edit); + + std::unique_ptr ui; + + QString last_path; + + std::vector formats; + std::vector format_generic_options; + std::vector video_encoders; + std::vector audio_encoders; + std::vector encoder_generic_options; +}; diff --git a/src/citra_qt/dumping/dumping_dialog.ui b/src/citra_qt/dumping/dumping_dialog.ui new file mode 100644 index 000000000..6e33d47d8 --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.ui @@ -0,0 +1,213 @@ + + + DumpingDialog + + + + 0 + 0 + 600 + 420 + + + + Dump Video + + + + + + Output + + + + + + Format: + + + + + + + + + + Options: + + + + + + + + + + ... + + + + + + + Path: + + + + + + + + + + ... + + + + + + + + + + Video + + + + + + Encoder: + + + + + + + + 0 + 0 + + + + + + + + Options: + + + + + + + + + + ... + + + + + + + Bitrate: + + + + + + + 10000000 + + + 1000 + + + + + + + bps + + + + + + + + + + Audio + + + + + + Encoder: + + + + + + + + 0 + 0 + + + + + + + + Options: + + + + + + + + + + ... + + + + + + + Bitrate: + + + + + + + 1000000 + + + 100 + + + + + + + bps + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + diff --git a/src/citra_qt/dumping/option_set_dialog.cpp b/src/citra_qt/dumping/option_set_dialog.cpp new file mode 100644 index 000000000..8dab0505e --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.cpp @@ -0,0 +1,299 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#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 +} + +static const std::unordered_map 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 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> 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> 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> 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(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> 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(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(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 tmp; + Common::SplitString(initial_value, '+', tmp); + + std::vector out; + std::vector 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(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 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(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()), 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; diff --git a/src/citra_qt/dumping/option_set_dialog.h b/src/citra_qt/dumping/option_set_dialog.h new file mode 100644 index 000000000..2c5d378d0 --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.h @@ -0,0 +1,33 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#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 GetCurrentValue(); + +private: + void InitializeUI(const std::string& initial_value); + void SetCheckBoxDefaults(const std::string& initial_value); + void UpdateUIDisplay(); + + std::unique_ptr ui; + VideoDumper::OptionInfo option; + bool is_set = true; + int layout_type = -1; // 0 - line edit, 1 - combo box, 2 - flags (check boxes) +}; diff --git a/src/citra_qt/dumping/option_set_dialog.ui b/src/citra_qt/dumping/option_set_dialog.ui new file mode 100644 index 000000000..dcf4bb572 --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.ui @@ -0,0 +1,89 @@ + + + OptionSetDialog + + + + 0 + 0 + 600 + 150 + + + + Options + + + + + + + + + false + + + + + + + + + false + + + + + + + false + + + + + + + + + false + + + + + + + + + + Qt::Vertical + + + + + + + + + Unset + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/src/citra_qt/dumping/options_dialog.cpp b/src/citra_qt/dumping/options_dialog.cpp new file mode 100644 index 000000000..e75fc61c0 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.cpp @@ -0,0 +1,68 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#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(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 specific_options_, + std::vector generic_options_, + const std::string& current_value) + : QDialog(parent), ui(std::make_unique()), + 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; diff --git a/src/citra_qt/dumping/options_dialog.h b/src/citra_qt/dumping/options_dialog.h new file mode 100644 index 000000000..d9a29e2dc --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.h @@ -0,0 +1,36 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#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 specific_options, + std::vector generic_options, + const std::string& current_value); + ~OptionsDialog() override; + + std::string GetCurrentValue() const; + +private: + void PopulateOptions(); + void OnSetOptionValue(QTreeWidgetItem* item); + + std::unique_ptr ui; + std::vector specific_options; + std::vector generic_options; + Common::ParamPackage current_values; +}; diff --git a/src/citra_qt/dumping/options_dialog.ui b/src/citra_qt/dumping/options_dialog.ui new file mode 100644 index 000000000..e8bd7fb41 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.ui @@ -0,0 +1,71 @@ + + + OptionsDialog + + + + 0 + 0 + 650 + 350 + + + + Options + + + + + + true + + + Double click to see the description and change the values of the options. + + + + + + + + + Specific + + + true + + + + + + + Generic + + + + + + + + + + Name + + + + + Value + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 1e7d0fdf0..fc7eb2522 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -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.
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.
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) { diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 75a1bbb3d..132fad821 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -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; diff --git a/src/common/param_package.cpp b/src/common/param_package.cpp index 433b34b36..3a218efbc 100644 --- a/src/common/param_package.cpp +++ b/src/common/param_package.cpp @@ -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 diff --git a/src/common/param_package.h b/src/common/param_package.h index 6a0a9b656..1fffb5035 100644 --- a/src/common/param_package.h +++ b/src/common/param_package.h @@ -5,15 +5,15 @@ #pragma once #include +#include #include -#include namespace Common { /// A string-based key-value container supporting serializing to and deserializing from a string class ParamPackage { public: - using DataType = std::unordered_map; + using DataType = std::map; 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; }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ab9f52e32..225a68796 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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() diff --git a/src/core/core.cpp b/src/core/core.cpp index 65b7e1cd3..c1d635afe 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -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(); +#else + video_dumper = std::make_unique(); +#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(); -#else - video_dumper = std::make_unique(); -#endif - LOG_DEBUG(Core, "Initialized OK"); initalized = true; diff --git a/src/core/dumping/backend.cpp b/src/core/dumping/backend.cpp index daf43c744..88686b7a2 100644 --- a/src/core/dumping/backend.cpp +++ b/src/core/dumping/backend.cpp @@ -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; diff --git a/src/core/dumping/backend.h b/src/core/dumping/backend.h index c2a4d532a..b0b63ba66 100644 --- a/src/core/dumping/backend.h +++ b/src/core/dumping/backend.h @@ -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& 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& /*sample*/) override {} void StopDumping() override {} bool IsDumping() const override { diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 811a5c99b..05cc25acf 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -2,15 +2,19 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #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 +#include } 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(AV_NOPTS_VALUE)) { - packet.pts = av_rescale_q(packet.pts, codec_context->time_base, stream->time_base); - } - if (packet.dts != static_cast(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; - av_interleaved_write_frame(format_context, &packet); + { + 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->sample_fmt = codec->sample_fmts[0]; - codec_context->sample_rate = AudioCore::native_sample_rate; + 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(codec_context->frame_size); + } else { // variable frame size support + frame_size = std::tuple_size::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 src_data = {reinterpret_cast(channel0.data()), - reinterpret_cast(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 src_data = {reinterpret_cast(channel0.data()), + reinterpret_cast(channel1.data())}; + + std::array 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; } - // Prepare frame - audio_frame->nb_samples = channel0.size(); - audio_frame->data[0] = resampled_data[0]; - audio_frame->data[1] = resampled_data[1]; - audio_frame->pts = sample_count; - sample_count += channel0.size(); + offset += resampled_count; + if (offset < frame_size) { // Still not enough to form a frame + return; + } - SendFrame(audio_frame.get()); + while (true) { + // Prepare frame + 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 = 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(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, 2> refactored_frame; +void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) { + std::array 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& 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::min()) { + out.append("-"); + duration = -duration; + } + if (duration == std::numeric_limits::max()) { + return "INT64_MAX"; + } else if (duration == std::numeric_limits::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& 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::max()); + return fmt::format("{}/{}", q.num, q.den); + } + case AV_OPT_TYPE_PIXEL_FMT: { + const char* name = av_get_pix_fmt_name(static_cast(option->default_val.i64)); + return ToStdString(name, "none"); + } + case AV_OPT_TYPE_SAMPLE_FMT: { + const char* name = + av_get_sample_fmt_name(static_cast(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& out, const AVClass* av_class) { + if (av_class == nullptr) { + return; + } + + const AVOption* current = nullptr; + std::unordered_map> 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 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& 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 GetOptionList(const AVClass* av_class, bool search_children) { + std::vector out; + GetOptionList(out, av_class, search_children); + return out; +} + +std::vector ListEncoders(AVMediaType type) { + InitializeFFmpegLibraries(); + + std::vector 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 GetEncoderGenericOptions() { + return GetOptionList(avcodec_get_class(), false); +} + +std::vector ListFormats() { + InitializeFFmpegLibraries(); + + std::vector 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 extensions; + Common::SplitString(ToStdString(current->extensions), ',', extensions); + + std::set supported_video_codecs; + std::set 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 GetFormatGenericOptions() { + return GetOptionList(avformat_get_class(), false); +} + } // namespace VideoDumper diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index 0208195d5..86a6e08cc 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include "common/common_types.h" @@ -19,6 +20,7 @@ extern "C" { #include #include +#include #include #include } @@ -29,13 +31,15 @@ using VariableAudioFrame = std::vector; 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 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 audio_frame{}; std::unique_ptr 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 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& 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 audio_buffers; std::array, 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 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 options; +}; + +/// Struct describing a format +struct FormatInfo { + std::string name; + std::string long_name; + std::vector extensions; + std::set supported_video_codecs; + std::set supported_audio_codecs; + std::vector options; +}; + +std::vector ListEncoders(AVMediaType type); +std::vector GetEncoderGenericOptions(); +std::vector ListFormats(); +std::vector GetFormatGenericOptions(); + } // namespace VideoDumper diff --git a/src/core/settings.h b/src/core/settings.h index 980156663..70bdd8c4c 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -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 diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 3553fe0a8..b402282af 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -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 diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp new file mode 100644 index 000000000..53985823c --- /dev/null +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.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 +#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(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 diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.h b/src/video_core/renderer_opengl/frame_dumper_opengl.h new file mode 100644 index 000000000..da6d96053 --- /dev/null +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.h @@ -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 +#include +#include +#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 mailbox; + +private: + void InitializeOpenGLObjects(); + void CleanupOpenGLObjects(); + void PresentLoop(); + + VideoDumper::Backend& video_dumper; + std::unique_ptr context; + std::thread present_thread; + std::atomic_bool stop_requested{false}; + + // PBOs used to dump frames faster + std::array pbos; + GLuint current_pbo = 1; + GLuint next_pbo = 0; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 5046895e0..b1fcfb592 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -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().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 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 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 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 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 MakeOrthographicMatrix(const float width, const float height) { +static std::array MakeOrthographicMatrix(const float width, const float height, + bool flipped) { + std::array matrix; // Laid out in column-major order - // 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 + 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; + // 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(); + frame_dumper.mailbox = std::make_unique(); } 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); - } - - // 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; - } + if (frame_dumper.IsDumping()) { + RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true); } - { - 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++; - } + 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& mailbox, + bool flipped) { - if (Core::System::GetInstance().VideoDumper().IsDumping()) { - if (prepare_video_dumping.exchange(false)) { - InitVideoDumpingGLObjects(); + 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); } - 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); + // delete the draw fence if the frame wasn't presented + if (frame->render_fence) { + glDeleteSync(frame->render_fence); + frame->render_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); + // 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; + } + } - GLubyte* pixels = static_cast(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)); - VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels}; - Core::System::GetInstance().VideoDumper().AddVideoFrame(frame_data); + { + 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); + } - 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 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) { diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 96df7f8ac..634d26ca4 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -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& 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 frame_dumping_pbos; - GLuint current_pbo = 1; - GLuint next_pbo = 0; + FrameDumperOpenGL frame_dumper; }; } // namespace OpenGL