Merge branch 'master' into feature/savestates-2
This commit is contained in:
commit
828f88d20a
76 changed files with 3507 additions and 1369 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue