Merge branch 'master' into feature/savestates-2
This commit is contained in:
		| @@ -495,5 +495,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() | ||||
|   | ||||
| @@ -377,6 +377,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo | ||||
|     Service::Init(*this); | ||||
|     GDBStub::DeferStart(); | ||||
|  | ||||
| #ifdef ENABLE_FFMPEG_VIDEO_DUMPER | ||||
|     video_dumper = std::make_unique<VideoDumper::FFmpegBackend>(); | ||||
| #else | ||||
|     video_dumper = std::make_unique<VideoDumper::NullBackend>(); | ||||
| #endif | ||||
|  | ||||
|     VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory); | ||||
|     if (result != VideoCore::ResultStatus::Success) { | ||||
|         switch (result) { | ||||
| @@ -389,12 +395,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo | ||||
|         } | ||||
|     } | ||||
|  | ||||
| #ifdef ENABLE_FFMPEG_VIDEO_DUMPER | ||||
|     video_dumper = std::make_unique<VideoDumper::FFmpegBackend>(); | ||||
| #else | ||||
|     video_dumper = std::make_unique<VideoDumper::NullBackend>(); | ||||
| #endif | ||||
|  | ||||
|     LOG_DEBUG(Core, "Initialized OK"); | ||||
|  | ||||
|     initalized = true; | ||||
|   | ||||
| @@ -8,17 +8,7 @@ | ||||
| namespace VideoDumper { | ||||
|  | ||||
| VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_) | ||||
|     : width(width_), height(height_), stride(width * 4), data(width * height * 4) { | ||||
|     // While copying, rotate the image to put the pixels in correct order | ||||
|     // (As OpenGL returns pixel data starting from the lowest position) | ||||
|     for (std::size_t i = 0; i < height; i++) { | ||||
|         for (std::size_t j = 0; j < width; j++) { | ||||
|             for (std::size_t k = 0; k < 4; k++) { | ||||
|                 data[i * stride + j * 4 + k] = data_[(height - i - 1) * stride + j * 4 + k]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|     : width(width_), height(height_), stride(width * 4), data(data_, data_ + width * height * 4) {} | ||||
|  | ||||
| Backend::~Backend() = default; | ||||
| NullBackend::~NullBackend() = default; | ||||
|   | ||||
| @@ -28,10 +28,9 @@ public: | ||||
| class Backend { | ||||
| public: | ||||
|     virtual ~Backend(); | ||||
|     virtual bool StartDumping(const std::string& path, const std::string& format, | ||||
|                               const Layout::FramebufferLayout& layout) = 0; | ||||
|     virtual void AddVideoFrame(const VideoFrame& frame) = 0; | ||||
|     virtual void AddAudioFrame(const AudioCore::StereoFrame16& frame) = 0; | ||||
|     virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0; | ||||
|     virtual void AddVideoFrame(VideoFrame frame) = 0; | ||||
|     virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0; | ||||
|     virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0; | ||||
|     virtual void StopDumping() = 0; | ||||
|     virtual bool IsDumping() const = 0; | ||||
| @@ -41,12 +40,12 @@ public: | ||||
| class NullBackend : public Backend { | ||||
| public: | ||||
|     ~NullBackend() override; | ||||
|     bool StartDumping(const std::string& /*path*/, const std::string& /*format*/, | ||||
|     bool StartDumping(const std::string& /*path*/, | ||||
|                       const Layout::FramebufferLayout& /*layout*/) override { | ||||
|         return false; | ||||
|     } | ||||
|     void AddVideoFrame(const VideoFrame& /*frame*/) override {} | ||||
|     void AddAudioFrame(const AudioCore::StereoFrame16& /*frame*/) override {} | ||||
|     void AddVideoFrame(VideoFrame /*frame*/) override {} | ||||
|     void AddAudioFrame(AudioCore::StereoFrame16 /*frame*/) override {} | ||||
|     void AddAudioSample(const std::array<s16, 2>& /*sample*/) override {} | ||||
|     void StopDumping() override {} | ||||
|     bool IsDumping() const override { | ||||
|   | ||||
| @@ -2,15 +2,19 @@ | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <unordered_set> | ||||
| #include "common/assert.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/param_package.h" | ||||
| #include "common/string_util.h" | ||||
| #include "core/dumping/ffmpeg_backend.h" | ||||
| #include "core/settings.h" | ||||
| #include "video_core/renderer_base.h" | ||||
| #include "video_core/video_core.h" | ||||
|  | ||||
| extern "C" { | ||||
| #include <libavutil/opt.h> | ||||
| #include <libavutil/pixdesc.h> | ||||
| } | ||||
|  | ||||
| namespace VideoDumper { | ||||
| @@ -27,14 +31,25 @@ void InitializeFFmpegLibraries() { | ||||
|     initialized = true; | ||||
| } | ||||
|  | ||||
| AVDictionary* ToAVDictionary(const std::string& serialized) { | ||||
|     Common::ParamPackage param_package{serialized}; | ||||
|     AVDictionary* result = nullptr; | ||||
|     for (const auto& [key, value] : param_package) { | ||||
|         av_dict_set(&result, key.c_str(), value.c_str(), 0); | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| FFmpegStream::~FFmpegStream() { | ||||
|     Free(); | ||||
| } | ||||
|  | ||||
| bool FFmpegStream::Init(AVFormatContext* format_context_) { | ||||
| bool FFmpegStream::Init(FFmpegMuxer& muxer) { | ||||
|     InitializeFFmpegLibraries(); | ||||
|  | ||||
|     format_context = format_context_; | ||||
|     format_context = muxer.format_context.get(); | ||||
|     format_context_mutex = &muxer.format_context_mutex; | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| @@ -47,14 +62,12 @@ void FFmpegStream::Flush() { | ||||
| } | ||||
|  | ||||
| void FFmpegStream::WritePacket(AVPacket& packet) { | ||||
|     if (packet.pts != static_cast<s64>(AV_NOPTS_VALUE)) { | ||||
|         packet.pts = av_rescale_q(packet.pts, codec_context->time_base, stream->time_base); | ||||
|     } | ||||
|     if (packet.dts != static_cast<s64>(AV_NOPTS_VALUE)) { | ||||
|         packet.dts = av_rescale_q(packet.dts, codec_context->time_base, stream->time_base); | ||||
|     } | ||||
|     av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base); | ||||
|     packet.stream_index = stream->index; | ||||
|     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<u64>(codec_context->frame_size); | ||||
|     } else { // variable frame size support | ||||
|         frame_size = std::tuple_size<AudioCore::StereoFrame16>::value; | ||||
|     } | ||||
|  | ||||
|     // Create audio stream | ||||
|     stream = avformat_new_stream(format_context, codec); | ||||
|     if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { | ||||
| @@ -234,6 +287,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { | ||||
|     audio_frame->format = codec_context->sample_fmt; | ||||
|     audio_frame->channel_layout = codec_context->channel_layout; | ||||
|     audio_frame->channels = codec_context->channels; | ||||
|     audio_frame->sample_rate = codec_context->sample_rate; | ||||
|  | ||||
|     // Allocate SWR context | ||||
|     auto* context = | ||||
| @@ -253,7 +307,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { | ||||
|     // Allocate resampled data | ||||
|     int error = | ||||
|         av_samples_alloc_array_and_samples(&resampled_data, nullptr, codec_context->channels, | ||||
|                                            codec_context->frame_size, codec_context->sample_fmt, 0); | ||||
|                                            frame_size, codec_context->sample_fmt, 0); | ||||
|     if (error < 0) { | ||||
|         LOG_ERROR(Render, "Could not allocate samples storage"); | ||||
|         return false; | ||||
| @@ -274,39 +328,79 @@ void FFmpegAudioStream::Free() { | ||||
|     av_freep(&resampled_data); | ||||
| } | ||||
|  | ||||
| void FFmpegAudioStream::ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) { | ||||
| void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0, | ||||
|                                      const VariableAudioFrame& channel1) { | ||||
|     ASSERT_MSG(channel0.size() == channel1.size(), | ||||
|                "Frames of the two channels must have the same number of samples"); | ||||
|     std::array<const u8*, 2> src_data = {reinterpret_cast<u8*>(channel0.data()), | ||||
|                                          reinterpret_cast<u8*>(channel1.data())}; | ||||
|     if (swr_convert(swr_context.get(), resampled_data, channel0.size(), src_data.data(), | ||||
|                     channel0.size()) < 0) { | ||||
|  | ||||
|     const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt); | ||||
|     std::array<const u8*, 2> src_data = {reinterpret_cast<const u8*>(channel0.data()), | ||||
|                                          reinterpret_cast<const u8*>(channel1.data())}; | ||||
|  | ||||
|     std::array<u8*, 2> dst_data; | ||||
|     if (av_sample_fmt_is_planar(codec_context->sample_fmt)) { | ||||
|         dst_data = {resampled_data[0] + sample_size * offset, | ||||
|                     resampled_data[1] + sample_size * offset}; | ||||
|     } else { | ||||
|         dst_data = {resampled_data[0] + sample_size * offset * 2}; // 2 channels | ||||
|     } | ||||
|  | ||||
|     auto resampled_count = swr_convert(swr_context.get(), dst_data.data(), frame_size - offset, | ||||
|                                        src_data.data(), channel0.size()); | ||||
|     if (resampled_count < 0) { | ||||
|         LOG_ERROR(Render, "Audio frame dropped: Could not resample data"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // 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<u64>(resampled_count) < frame_size) { | ||||
|             offset = resampled_count; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| std::size_t FFmpegAudioStream::GetAudioFrameSize() const { | ||||
|     ASSERT_MSG(codec_context, "Codec context is not initialized yet!"); | ||||
|     return codec_context->frame_size; | ||||
| void FFmpegAudioStream::Flush() { | ||||
|     // Send the last samples | ||||
|     audio_frame->nb_samples = offset; | ||||
|     audio_frame->data[0] = resampled_data[0]; | ||||
|     if (av_sample_fmt_is_planar(codec_context->sample_fmt)) { | ||||
|         audio_frame->data[1] = resampled_data[1]; | ||||
|     } | ||||
|     audio_frame->pts = frame_count * frame_size; | ||||
|  | ||||
|     SendFrame(audio_frame.get()); | ||||
|  | ||||
|     FFmpegStream::Flush(); | ||||
| } | ||||
|  | ||||
| FFmpegMuxer::~FFmpegMuxer() { | ||||
|     Free(); | ||||
| } | ||||
|  | ||||
| bool FFmpegMuxer::Init(const std::string& path, const std::string& format, | ||||
|                        const Layout::FramebufferLayout& layout) { | ||||
| bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) { | ||||
|  | ||||
|     InitializeFFmpegLibraries(); | ||||
|  | ||||
| @@ -315,9 +409,8 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format, | ||||
|     } | ||||
|  | ||||
|     // Get output format | ||||
|     // Ensure webm here to avoid patent issues | ||||
|     ASSERT_MSG(format == "webm", "Only webm is allowed for frame dumping"); | ||||
|     auto* output_format = av_guess_format(format.c_str(), path.c_str(), "video/webm"); | ||||
|     const auto format = Settings::values.output_format; | ||||
|     auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr); | ||||
|     if (!output_format) { | ||||
|         LOG_ERROR(Render, "Could not get format {}", format); | ||||
|         return false; | ||||
| @@ -333,18 +426,24 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format, | ||||
|     } | ||||
|     format_context.reset(format_context_raw); | ||||
|  | ||||
|     if (!video_stream.Init(format_context.get(), output_format, layout)) | ||||
|     if (!video_stream.Init(*this, layout)) | ||||
|         return false; | ||||
|     if (!audio_stream.Init(format_context.get())) | ||||
|     if (!audio_stream.Init(*this)) | ||||
|         return false; | ||||
|  | ||||
|     AVDictionary* options = ToAVDictionary(Settings::values.format_options); | ||||
|     // Open video file | ||||
|     if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 || | ||||
|         avformat_write_header(format_context.get(), nullptr)) { | ||||
|         avformat_write_header(format_context.get(), &options)) { | ||||
|  | ||||
|         LOG_ERROR(Render, "Could not open {}", path); | ||||
|         return false; | ||||
|     } | ||||
|     if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict | ||||
|         char* buf = nullptr; | ||||
|         av_dict_get_string(options, &buf, ':', ';'); | ||||
|         LOG_WARNING(Render, "Format options not found: {}", buf); | ||||
|     } | ||||
|  | ||||
|     LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height); | ||||
|     return true; | ||||
| @@ -360,7 +459,8 @@ void FFmpegMuxer::ProcessVideoFrame(VideoFrame& frame) { | ||||
|     video_stream.ProcessFrame(frame); | ||||
| } | ||||
|  | ||||
| void FFmpegMuxer::ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) { | ||||
| void FFmpegMuxer::ProcessAudioFrame(const VariableAudioFrame& channel0, | ||||
|                                     const VariableAudioFrame& channel1) { | ||||
|     audio_stream.ProcessFrame(channel0, channel1); | ||||
| } | ||||
|  | ||||
| @@ -372,11 +472,9 @@ void FFmpegMuxer::FlushAudio() { | ||||
|     audio_stream.Flush(); | ||||
| } | ||||
|  | ||||
| std::size_t FFmpegMuxer::GetAudioFrameSize() const { | ||||
|     return audio_stream.GetAudioFrameSize(); | ||||
| } | ||||
|  | ||||
| void FFmpegMuxer::WriteTrailer() { | ||||
|     std::lock_guard lock{format_context_mutex}; | ||||
|     av_interleaved_write_frame(format_context.get(), nullptr); | ||||
|     av_write_trailer(format_context.get()); | ||||
| } | ||||
|  | ||||
| @@ -392,12 +490,11 @@ FFmpegBackend::~FFmpegBackend() { | ||||
|     ffmpeg.Free(); | ||||
| } | ||||
|  | ||||
| bool FFmpegBackend::StartDumping(const std::string& path, const std::string& format, | ||||
|                                  const Layout::FramebufferLayout& layout) { | ||||
| bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) { | ||||
|  | ||||
|     InitializeFFmpegLibraries(); | ||||
|  | ||||
|     if (!ffmpeg.Init(path, format, layout)) { | ||||
|     if (!ffmpeg.Init(path, layout)) { | ||||
|         ffmpeg.Free(); | ||||
|         return false; | ||||
|     } | ||||
| @@ -450,31 +547,29 @@ bool FFmpegBackend::StartDumping(const std::string& path, const std::string& for | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| void FFmpegBackend::AddVideoFrame(const VideoFrame& frame) { | ||||
| void FFmpegBackend::AddVideoFrame(VideoFrame frame) { | ||||
|     event1.Wait(); | ||||
|     video_frame_buffers[next_buffer] = std::move(frame); | ||||
|     event2.Set(); | ||||
| } | ||||
|  | ||||
| void FFmpegBackend::AddAudioFrame(const AudioCore::StereoFrame16& frame) { | ||||
|     std::array<std::array<s16, 160>, 2> refactored_frame; | ||||
| void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) { | ||||
|     std::array<VariableAudioFrame, 2> refactored_frame; | ||||
|     for (auto& channel : refactored_frame) { | ||||
|         channel.resize(frame.size()); | ||||
|     } | ||||
|     for (std::size_t i = 0; i < frame.size(); i++) { | ||||
|         refactored_frame[0][i] = frame[i][0]; | ||||
|         refactored_frame[1][i] = frame[i][1]; | ||||
|     } | ||||
|  | ||||
|     for (auto i : {0, 1}) { | ||||
|         audio_buffers[i].insert(audio_buffers[i].end(), refactored_frame[i].begin(), | ||||
|                                 refactored_frame[i].end()); | ||||
|     } | ||||
|     CheckAudioBuffer(); | ||||
|     audio_frame_queues[0].Push(std::move(refactored_frame[0])); | ||||
|     audio_frame_queues[1].Push(std::move(refactored_frame[1])); | ||||
| } | ||||
|  | ||||
| void FFmpegBackend::AddAudioSample(const std::array<s16, 2>& sample) { | ||||
|     for (auto i : {0, 1}) { | ||||
|         audio_buffers[i].push_back(sample[i]); | ||||
|     } | ||||
|     CheckAudioBuffer(); | ||||
|     audio_frame_queues[0].Push(VariableAudioFrame{sample[0]}); | ||||
|     audio_frame_queues[1].Push(VariableAudioFrame{sample[1]}); | ||||
| } | ||||
|  | ||||
| void FFmpegBackend::StopDumping() { | ||||
| @@ -484,12 +579,6 @@ void FFmpegBackend::StopDumping() { | ||||
|     // Flush the video processing queue | ||||
|     AddVideoFrame(VideoFrame()); | ||||
|     for (auto i : {0, 1}) { | ||||
|         // Add remaining data to audio queue | ||||
|         if (audio_buffers[i].size() >= 0) { | ||||
|             VariableAudioFrame buffer(audio_buffers[i].begin(), audio_buffers[i].end()); | ||||
|             audio_frame_queues[i].Push(std::move(buffer)); | ||||
|             audio_buffers[i].clear(); | ||||
|         } | ||||
|         // Flush the audio processing queue | ||||
|         audio_frame_queues[i].Push(VariableAudioFrame()); | ||||
|     } | ||||
| @@ -513,18 +602,234 @@ void FFmpegBackend::EndDumping() { | ||||
|     processing_ended.Set(); | ||||
| } | ||||
|  | ||||
| void FFmpegBackend::CheckAudioBuffer() { | ||||
|     for (auto i : {0, 1}) { | ||||
|         const std::size_t frame_size = ffmpeg.GetAudioFrameSize(); | ||||
|         // Add audio data to the queue when there is enough to form a frame | ||||
|         while (audio_buffers[i].size() >= frame_size) { | ||||
|             VariableAudioFrame buffer(audio_buffers[i].begin(), | ||||
|                                       audio_buffers[i].begin() + frame_size); | ||||
|             audio_frame_queues[i].Push(std::move(buffer)); | ||||
| // To std string, but handles nullptr | ||||
| std::string ToStdString(const char* str, const std::string& fallback = "") { | ||||
|     return str ? std::string{str} : fallback; | ||||
| } | ||||
|  | ||||
|             audio_buffers[i].erase(audio_buffers[i].begin(), audio_buffers[i].begin() + frame_size); | ||||
| std::string FormatDuration(s64 duration) { | ||||
|     // The following is implemented according to libavutil code (opt.c) | ||||
|     std::string out; | ||||
|     if (duration < 0 && duration != std::numeric_limits<s64>::min()) { | ||||
|         out.append("-"); | ||||
|         duration = -duration; | ||||
|     } | ||||
|     if (duration == std::numeric_limits<s64>::max()) { | ||||
|         return "INT64_MAX"; | ||||
|     } else if (duration == std::numeric_limits<s64>::min()) { | ||||
|         return "INT64_MIN"; | ||||
|     } else if (duration > 3600ll * 1000000ll) { | ||||
|         out.append(fmt::format("{}:{:02d}:{:02d}.{:06d}", duration / 3600000000ll, | ||||
|                                ((duration / 60000000ll) % 60), ((duration / 1000000ll) % 60), | ||||
|                                duration % 1000000)); | ||||
|     } else if (duration > 60ll * 1000000ll) { | ||||
|         out.append(fmt::format("{}:{:02d}.{:06d}", duration / 60000000ll, | ||||
|                                ((duration / 1000000ll) % 60), duration % 1000000)); | ||||
|     } else { | ||||
|         out.append(fmt::format("{}.{:06d}", duration / 1000000ll, duration % 1000000)); | ||||
|     } | ||||
|     while (out.back() == '0') { | ||||
|         out.erase(out.size() - 1, 1); | ||||
|     } | ||||
|     if (out.back() == '.') { | ||||
|         out.erase(out.size() - 1, 1); | ||||
|     } | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| std::string FormatDefaultValue(const AVOption* option, | ||||
|                                const std::vector<OptionInfo::NamedConstant>& named_constants) { | ||||
|     // The following is taken and modified from libavutil code (opt.c) | ||||
|     switch (option->type) { | ||||
|     case AV_OPT_TYPE_BOOL: { | ||||
|         const auto value = option->default_val.i64; | ||||
|         if (value < 0) { | ||||
|             return "auto"; | ||||
|         } | ||||
|         return value ? "true" : "false"; | ||||
|     } | ||||
|     case AV_OPT_TYPE_FLAGS: { | ||||
|         const auto value = option->default_val.i64; | ||||
|         std::string out; | ||||
|         for (const auto& constant : named_constants) { | ||||
|             if (!(value & constant.value)) { | ||||
|                 continue; | ||||
|             } | ||||
|             if (!out.empty()) { | ||||
|                 out.append("+"); | ||||
|             } | ||||
|             out.append(constant.name); | ||||
|         } | ||||
|         return out.empty() ? fmt::format("{}", value) : out; | ||||
|     } | ||||
|     case AV_OPT_TYPE_DURATION: { | ||||
|         return FormatDuration(option->default_val.i64); | ||||
|     } | ||||
|     case AV_OPT_TYPE_INT: | ||||
|     case AV_OPT_TYPE_UINT64: | ||||
|     case AV_OPT_TYPE_INT64: { | ||||
|         const auto value = option->default_val.i64; | ||||
|         for (const auto& constant : named_constants) { | ||||
|             if (constant.value == value) { | ||||
|                 return constant.name; | ||||
|             } | ||||
|         } | ||||
|         return fmt::format("{}", value); | ||||
|     } | ||||
|     case AV_OPT_TYPE_DOUBLE: | ||||
|     case AV_OPT_TYPE_FLOAT: { | ||||
|         return fmt::format("{}", option->default_val.dbl); | ||||
|     } | ||||
|     case AV_OPT_TYPE_RATIONAL: { | ||||
|         const auto q = av_d2q(option->default_val.dbl, std::numeric_limits<int>::max()); | ||||
|         return fmt::format("{}/{}", q.num, q.den); | ||||
|     } | ||||
|     case AV_OPT_TYPE_PIXEL_FMT: { | ||||
|         const char* name = av_get_pix_fmt_name(static_cast<AVPixelFormat>(option->default_val.i64)); | ||||
|         return ToStdString(name, "none"); | ||||
|     } | ||||
|     case AV_OPT_TYPE_SAMPLE_FMT: { | ||||
|         const char* name = | ||||
|             av_get_sample_fmt_name(static_cast<AVSampleFormat>(option->default_val.i64)); | ||||
|         return ToStdString(name, "none"); | ||||
|     } | ||||
|     case AV_OPT_TYPE_COLOR: | ||||
|     case AV_OPT_TYPE_IMAGE_SIZE: | ||||
|     case AV_OPT_TYPE_STRING: | ||||
|     case AV_OPT_TYPE_DICT: | ||||
|     case AV_OPT_TYPE_VIDEO_RATE: { | ||||
|         return ToStdString(option->default_val.str); | ||||
|     } | ||||
|     case AV_OPT_TYPE_CHANNEL_LAYOUT: { | ||||
|         return fmt::format("{:#x}", option->default_val.i64); | ||||
|     } | ||||
|     default: | ||||
|         return ""; | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GetOptionListSingle(std::vector<OptionInfo>& out, const AVClass* av_class) { | ||||
|     if (av_class == nullptr) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const AVOption* current = nullptr; | ||||
|     std::unordered_map<std::string, std::vector<OptionInfo::NamedConstant>> named_constants_map; | ||||
|     // First iteration: find and place all named constants | ||||
|     while ((current = av_opt_next(&av_class, current))) { | ||||
|         if (current->type != AV_OPT_TYPE_CONST || !current->unit) { | ||||
|             continue; | ||||
|         } | ||||
|         named_constants_map[current->unit].push_back( | ||||
|             {current->name, ToStdString(current->help), current->default_val.i64}); | ||||
|     } | ||||
|     // Second iteration: find all options | ||||
|     current = nullptr; | ||||
|     while ((current = av_opt_next(&av_class, current))) { | ||||
|         // Currently we cannot handle binary options | ||||
|         if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) { | ||||
|             continue; | ||||
|         } | ||||
|         std::vector<OptionInfo::NamedConstant> named_constants; | ||||
|         if (current->unit && named_constants_map.count(current->unit)) { | ||||
|             named_constants = named_constants_map.at(current->unit); | ||||
|         } | ||||
|         const auto default_value = FormatDefaultValue(current, named_constants); | ||||
|         out.push_back({current->name, ToStdString(current->help), current->type, default_value, | ||||
|                        std::move(named_constants), current->min, current->max}); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GetOptionList(std::vector<OptionInfo>& out, const AVClass* av_class, bool search_children) { | ||||
|     if (av_class == nullptr) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     GetOptionListSingle(out, av_class); | ||||
|  | ||||
|     if (!search_children) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const AVClass* child_class = nullptr; | ||||
|     while ((child_class = av_opt_child_class_next(av_class, child_class))) { | ||||
|         GetOptionListSingle(out, child_class); | ||||
|     } | ||||
| } | ||||
|  | ||||
| std::vector<OptionInfo> GetOptionList(const AVClass* av_class, bool search_children) { | ||||
|     std::vector<OptionInfo> out; | ||||
|     GetOptionList(out, av_class, search_children); | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| std::vector<EncoderInfo> ListEncoders(AVMediaType type) { | ||||
|     InitializeFFmpegLibraries(); | ||||
|  | ||||
|     std::vector<EncoderInfo> out; | ||||
|  | ||||
|     const AVCodec* current = nullptr; | ||||
| #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) | ||||
|     while ((current = av_codec_next(current))) { | ||||
| #else | ||||
|     void* data = nullptr; // For libavcodec to save the iteration state | ||||
|     while ((current = av_codec_iterate(&data))) { | ||||
| #endif | ||||
|         if (!av_codec_is_encoder(current) || current->type != type) { | ||||
|             continue; | ||||
|         } | ||||
|         out.push_back({current->name, ToStdString(current->long_name), current->id, | ||||
|                        GetOptionList(current->priv_class, true)}); | ||||
|     } | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| std::vector<OptionInfo> GetEncoderGenericOptions() { | ||||
|     return GetOptionList(avcodec_get_class(), false); | ||||
| } | ||||
|  | ||||
| std::vector<FormatInfo> ListFormats() { | ||||
|     InitializeFFmpegLibraries(); | ||||
|  | ||||
|     std::vector<FormatInfo> out; | ||||
|  | ||||
|     const AVOutputFormat* current = nullptr; | ||||
| #if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) | ||||
|     while ((current = av_oformat_next(current))) { | ||||
| #else | ||||
|     void* data = nullptr; // For libavformat to save the iteration state | ||||
|     while ((current = av_muxer_iterate(&data))) { | ||||
| #endif | ||||
|         std::vector<std::string> extensions; | ||||
|         Common::SplitString(ToStdString(current->extensions), ',', extensions); | ||||
|  | ||||
|         std::set<AVCodecID> supported_video_codecs; | ||||
|         std::set<AVCodecID> supported_audio_codecs; | ||||
|         // Go through all codecs | ||||
|         const AVCodecDescriptor* codec = nullptr; | ||||
|         while ((codec = avcodec_descriptor_next(codec))) { | ||||
|             if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) { | ||||
|                 if (codec->type == AVMEDIA_TYPE_VIDEO) { | ||||
|                     supported_video_codecs.emplace(codec->id); | ||||
|                 } else if (codec->type == AVMEDIA_TYPE_AUDIO) { | ||||
|                     supported_audio_codecs.emplace(codec->id); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (supported_video_codecs.empty() || supported_audio_codecs.empty()) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         out.push_back({current->name, ToStdString(current->long_name), std::move(extensions), | ||||
|                        std::move(supported_video_codecs), std::move(supported_audio_codecs), | ||||
|                        GetOptionList(current->priv_class, true)}); | ||||
|     } | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| std::vector<OptionInfo> GetFormatGenericOptions() { | ||||
|     return GetOptionList(avformat_get_class(), false); | ||||
| } | ||||
|  | ||||
| } // namespace VideoDumper | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| #include <limits> | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
| #include <set> | ||||
| #include <thread> | ||||
| #include <vector> | ||||
| #include "common/common_types.h" | ||||
| @@ -19,6 +20,7 @@ | ||||
| extern "C" { | ||||
| #include <libavcodec/avcodec.h> | ||||
| #include <libavformat/avformat.h> | ||||
| #include <libavutil/opt.h> | ||||
| #include <libswresample/swresample.h> | ||||
| #include <libswscale/swscale.h> | ||||
| } | ||||
| @@ -29,13 +31,15 @@ using VariableAudioFrame = std::vector<s16>; | ||||
|  | ||||
| void InitFFmpegLibraries(); | ||||
|  | ||||
| class FFmpegMuxer; | ||||
|  | ||||
| /** | ||||
|  * Wrapper around FFmpeg AVCodecContext + AVStream. | ||||
|  * Rescales/Resamples, encodes and writes a frame. | ||||
|  */ | ||||
| class FFmpegStream { | ||||
| public: | ||||
|     bool Init(AVFormatContext* format_context); | ||||
|     bool Init(FFmpegMuxer& muxer); | ||||
|     void Free(); | ||||
|     void Flush(); | ||||
|  | ||||
| @@ -58,6 +62,7 @@ protected: | ||||
|     }; | ||||
|  | ||||
|     AVFormatContext* format_context{}; | ||||
|     std::mutex* format_context_mutex{}; | ||||
|     std::unique_ptr<AVCodecContext, AVCodecContextDeleter> codec_context{}; | ||||
|     AVStream* stream{}; | ||||
| }; | ||||
| @@ -70,8 +75,7 @@ class FFmpegVideoStream : public FFmpegStream { | ||||
| public: | ||||
|     ~FFmpegVideoStream(); | ||||
|  | ||||
|     bool Init(AVFormatContext* format_context, AVOutputFormat* output_format, | ||||
|               const Layout::FramebufferLayout& layout); | ||||
|     bool Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout); | ||||
|     void Free(); | ||||
|     void ProcessFrame(VideoFrame& frame); | ||||
|  | ||||
| @@ -96,15 +100,16 @@ private: | ||||
| /** | ||||
|  * A FFmpegStream used for audio data. | ||||
|  * Resamples (converts), encodes and writes a frame. | ||||
|  * This also temporarily stores resampled audio data before there are enough to form a frame. | ||||
|  */ | ||||
| class FFmpegAudioStream : public FFmpegStream { | ||||
| public: | ||||
|     ~FFmpegAudioStream(); | ||||
|  | ||||
|     bool Init(AVFormatContext* format_context); | ||||
|     bool Init(FFmpegMuxer& muxer); | ||||
|     void Free(); | ||||
|     void ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1); | ||||
|     std::size_t GetAudioFrameSize() const; | ||||
|     void ProcessFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1); | ||||
|     void Flush(); | ||||
|  | ||||
| private: | ||||
|     struct SwrContextDeleter { | ||||
| @@ -113,12 +118,14 @@ private: | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     u64 sample_count{}; | ||||
|     u64 frame_size{}; | ||||
|     u64 frame_count{}; | ||||
|  | ||||
|     std::unique_ptr<AVFrame, AVFrameDeleter> audio_frame{}; | ||||
|     std::unique_ptr<SwrContext, SwrContextDeleter> swr_context{}; | ||||
|  | ||||
|     u8** resampled_data{}; | ||||
|     u64 offset{}; // Number of output samples that are currently in resampled_data. | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -129,14 +136,12 @@ class FFmpegMuxer { | ||||
| public: | ||||
|     ~FFmpegMuxer(); | ||||
|  | ||||
|     bool Init(const std::string& path, const std::string& format, | ||||
|               const Layout::FramebufferLayout& layout); | ||||
|     bool Init(const std::string& path, const Layout::FramebufferLayout& layout); | ||||
|     void Free(); | ||||
|     void ProcessVideoFrame(VideoFrame& frame); | ||||
|     void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1); | ||||
|     void ProcessAudioFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1); | ||||
|     void FlushVideo(); | ||||
|     void FlushAudio(); | ||||
|     std::size_t GetAudioFrameSize() const; | ||||
|     void WriteTrailer(); | ||||
|  | ||||
| private: | ||||
| @@ -150,28 +155,28 @@ private: | ||||
|     FFmpegAudioStream audio_stream{}; | ||||
|     FFmpegVideoStream video_stream{}; | ||||
|     std::unique_ptr<AVFormatContext, AVFormatContextDeleter> format_context{}; | ||||
|     std::mutex format_context_mutex; | ||||
|  | ||||
|     friend class FFmpegStream; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * FFmpeg video dumping backend. | ||||
|  * This class implements a double buffer, and an audio queue to keep audio data | ||||
|  * before enough data is received to form a frame. | ||||
|  * This class implements a double buffer. | ||||
|  */ | ||||
| class FFmpegBackend : public Backend { | ||||
| public: | ||||
|     FFmpegBackend(); | ||||
|     ~FFmpegBackend() override; | ||||
|     bool StartDumping(const std::string& path, const std::string& format, | ||||
|                       const Layout::FramebufferLayout& layout) override; | ||||
|     void AddVideoFrame(const VideoFrame& frame) override; | ||||
|     void AddAudioFrame(const AudioCore::StereoFrame16& frame) override; | ||||
|     bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override; | ||||
|     void AddVideoFrame(VideoFrame frame) override; | ||||
|     void AddAudioFrame(AudioCore::StereoFrame16 frame) override; | ||||
|     void AddAudioSample(const std::array<s16, 2>& sample) override; | ||||
|     void StopDumping() override; | ||||
|     bool IsDumping() const override; | ||||
|     Layout::FramebufferLayout GetLayout() const override; | ||||
|  | ||||
| private: | ||||
|     void CheckAudioBuffer(); | ||||
|     void EndDumping(); | ||||
|  | ||||
|     std::atomic_bool is_dumping = false; ///< Whether the backend is currently dumping | ||||
| @@ -184,13 +189,51 @@ private: | ||||
|     Common::Event event1, event2; | ||||
|     std::thread video_processing_thread; | ||||
|  | ||||
|     /// An audio buffer used to temporarily hold audio data, before the size is big enough | ||||
|     /// to be sent to the encoder as a frame | ||||
|     std::array<VariableAudioFrame, 2> audio_buffers; | ||||
|     std::array<Common::SPSCQueue<VariableAudioFrame>, 2> audio_frame_queues; | ||||
|     std::thread audio_processing_thread; | ||||
|  | ||||
|     Common::Event processing_ended; | ||||
| }; | ||||
|  | ||||
| /// Struct describing encoder/muxer options | ||||
| struct OptionInfo { | ||||
|     std::string name; | ||||
|     std::string description; | ||||
|     AVOptionType type; | ||||
|     std::string default_value; | ||||
|     struct NamedConstant { | ||||
|         std::string name; | ||||
|         std::string description; | ||||
|         s64 value; | ||||
|     }; | ||||
|     std::vector<NamedConstant> named_constants; | ||||
|  | ||||
|     // If this is a scalar type | ||||
|     double min; | ||||
|     double max; | ||||
| }; | ||||
|  | ||||
| /// Struct describing an encoder | ||||
| struct EncoderInfo { | ||||
|     std::string name; | ||||
|     std::string long_name; | ||||
|     AVCodecID codec; | ||||
|     std::vector<OptionInfo> options; | ||||
| }; | ||||
|  | ||||
| /// Struct describing a format | ||||
| struct FormatInfo { | ||||
|     std::string name; | ||||
|     std::string long_name; | ||||
|     std::vector<std::string> extensions; | ||||
|     std::set<AVCodecID> supported_video_codecs; | ||||
|     std::set<AVCodecID> supported_audio_codecs; | ||||
|     std::vector<OptionInfo> options; | ||||
| }; | ||||
|  | ||||
| std::vector<EncoderInfo> ListEncoders(AVMediaType type); | ||||
| std::vector<OptionInfo> GetEncoderGenericOptions(); | ||||
| std::vector<FormatInfo> ListFormats(); | ||||
| std::vector<OptionInfo> GetFormatGenericOptions(); | ||||
|  | ||||
| } // namespace VideoDumper | ||||
|   | ||||
| @@ -223,6 +223,7 @@ void Thread::ResumeFromWait() { | ||||
|     case ThreadStatus::WaitArb: | ||||
|     case ThreadStatus::WaitSleep: | ||||
|     case ThreadStatus::WaitIPC: | ||||
|     case ThreadStatus::Dormant: | ||||
|         break; | ||||
|  | ||||
|     case ThreadStatus::Ready: | ||||
|   | ||||
| @@ -120,15 +120,10 @@ void Module::Interface::GetSoftwareClosedFlag(Kernel::HLERequestContext& ctx) { | ||||
| void CheckNew3DS(IPC::RequestBuilder& rb) { | ||||
|     const bool is_new_3ds = Settings::values.is_new_3ds; | ||||
|  | ||||
|     if (is_new_3ds) { | ||||
|         LOG_CRITICAL(Service_PTM, "The option 'is_new_3ds' is enabled as part of the 'System' " | ||||
|                                   "settings. Citra does not fully support New 3DS emulation yet!"); | ||||
|     } | ||||
|  | ||||
|     rb.Push(RESULT_SUCCESS); | ||||
|     rb.Push(is_new_3ds); | ||||
|  | ||||
|     LOG_WARNING(Service_PTM, "(STUBBED) called isNew3DS = 0x{:08x}", static_cast<u32>(is_new_3ds)); | ||||
|     LOG_DEBUG(Service_PTM, "called isNew3DS = 0x{:08x}", static_cast<u32>(is_new_3ds)); | ||||
| } | ||||
|  | ||||
| void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) { | ||||
|   | ||||
| @@ -13,7 +13,6 @@ | ||||
| #include "core/hle/service/mic_u.h" | ||||
| #include "core/settings.h" | ||||
| #include "video_core/renderer_base.h" | ||||
| #include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" | ||||
| #include "video_core/video_core.h" | ||||
|  | ||||
| namespace Settings { | ||||
| @@ -38,9 +37,7 @@ void Apply() { | ||||
|     VideoCore::g_renderer_bg_color_update_requested = true; | ||||
|     VideoCore::g_renderer_sampler_update_requested = true; | ||||
|     VideoCore::g_renderer_shader_update_requested = true; | ||||
|  | ||||
|     OpenGL::TextureFilterManager::GetInstance().SetTextureFilter(values.texture_filter_name, | ||||
|                                                                  values.texture_filter_factor); | ||||
|     VideoCore::g_texture_filter_update_requested = true; | ||||
|  | ||||
|     auto& system = Core::System::GetInstance(); | ||||
|     if (system.IsPoweredOn()) { | ||||
| @@ -88,7 +85,6 @@ void LogSettings() { | ||||
|     LogSetting("Renderer_FrameLimit", Settings::values.frame_limit); | ||||
|     LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name); | ||||
|     LogSetting("Renderer_FilterMode", Settings::values.filter_mode); | ||||
|     LogSetting("Renderer_TextureFilterFactor", Settings::values.texture_filter_factor); | ||||
|     LogSetting("Renderer_TextureFilterName", Settings::values.texture_filter_name); | ||||
|     LogSetting("Stereoscopy_Render3d", static_cast<int>(Settings::values.render_3d)); | ||||
|     LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d); | ||||
|   | ||||
| @@ -148,7 +148,6 @@ struct Values { | ||||
|     u16 resolution_factor; | ||||
|     bool use_frame_limit; | ||||
|     u16 frame_limit; | ||||
|     u16 texture_filter_factor; | ||||
|     std::string texture_filter_name; | ||||
|  | ||||
|     LayoutOption layout_option; | ||||
| @@ -207,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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user