From 06a0d86e9cfb9c32377b6f4eba11666a393b90ff Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 28 Jan 2020 21:57:30 +0800 Subject: [PATCH 01/21] video_core, core: Move pixel download to its own thread This uses the mailbox model to move pixel downloading to its own thread, eliminating Nvidia's warnings and (possibly) making use of GPU copy engine. To achieve this, we created a new mailbox type that is different from the presentation mailbox in that it never discards a rendered frame. Also, I tweaked the projection matrix thing so that it can just draw the frame upside down instead of having the CPU flip it. --- src/core/core.cpp | 12 +- src/core/dumping/backend.cpp | 12 +- src/video_core/CMakeLists.txt | 2 + .../renderer_opengl/frame_dumper_opengl.cpp | 98 +++++++ .../renderer_opengl/frame_dumper_opengl.h | 57 ++++ .../renderer_opengl/renderer_opengl.cpp | 275 +++++++++--------- .../renderer_opengl/renderer_opengl.h | 37 +-- 7 files changed, 324 insertions(+), 169 deletions(-) create mode 100644 src/video_core/renderer_opengl/frame_dumper_opengl.cpp create mode 100644 src/video_core/renderer_opengl/frame_dumper_opengl.h diff --git a/src/core/core.cpp b/src/core/core.cpp index 01ab8481c..cde0daa94 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -308,6 +308,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo Service::Init(*this); GDBStub::Init(); +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER + video_dumper = std::make_unique(); +#else + video_dumper = std::make_unique(); +#endif + VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory); if (result != VideoCore::ResultStatus::Success) { switch (result) { @@ -320,12 +326,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo } } -#ifdef ENABLE_FFMPEG_VIDEO_DUMPER - video_dumper = std::make_unique(); -#else - video_dumper = std::make_unique(); -#endif - LOG_DEBUG(Core, "Initialized OK"); initalized = true; diff --git a/src/core/dumping/backend.cpp b/src/core/dumping/backend.cpp index daf43c744..88686b7a2 100644 --- a/src/core/dumping/backend.cpp +++ b/src/core/dumping/backend.cpp @@ -8,17 +8,7 @@ namespace VideoDumper { VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_) - : width(width_), height(height_), stride(width * 4), data(width * height * 4) { - // While copying, rotate the image to put the pixels in correct order - // (As OpenGL returns pixel data starting from the lowest position) - for (std::size_t i = 0; i < height; i++) { - for (std::size_t j = 0; j < width; j++) { - for (std::size_t k = 0; k < 4; k++) { - data[i * stride + j * 4 + k] = data_[(height - i - 1) * stride + j * 4 + k]; - } - } - } -} + : width(width_), height(height_), stride(width * 4), data(data_, data_ + width * height * 4) {} Backend::~Backend() = default; NullBackend::~NullBackend() = default; diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 4cb976354..245b0c49d 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -23,6 +23,8 @@ add_library(video_core STATIC regs_texturing.h renderer_base.cpp renderer_base.h + renderer_opengl/frame_dumper_opengl.cpp + renderer_opengl/frame_dumper_opengl.h renderer_opengl/gl_rasterizer.cpp renderer_opengl/gl_rasterizer.h renderer_opengl/gl_rasterizer_cache.cpp diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp new file mode 100644 index 000000000..7be4cc8ef --- /dev/null +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp @@ -0,0 +1,98 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "core/frontend/emu_window.h" +#include "core/frontend/scope_acquire_context.h" +#include "video_core/renderer_opengl/frame_dumper_opengl.h" +#include "video_core/renderer_opengl/renderer_opengl.h" + +namespace OpenGL { + +FrameDumperOpenGL::FrameDumperOpenGL(VideoDumper::Backend& video_dumper_, + Frontend::EmuWindow& emu_window) + : video_dumper(video_dumper_), context(emu_window.CreateSharedContext()) {} + +FrameDumperOpenGL::~FrameDumperOpenGL() { + if (present_thread.joinable()) + present_thread.join(); +} + +bool FrameDumperOpenGL::IsDumping() const { + return video_dumper.IsDumping(); +} + +Layout::FramebufferLayout FrameDumperOpenGL::GetLayout() const { + return video_dumper.GetLayout(); +} + +void FrameDumperOpenGL::StartDumping() { + if (present_thread.joinable()) + present_thread.join(); + + present_thread = std::thread(&FrameDumperOpenGL::PresentLoop, this); +} + +void FrameDumperOpenGL::StopDumping() { + stop_requested.store(true, std::memory_order_relaxed); +} + +void FrameDumperOpenGL::PresentLoop() { + Frontend::ScopeAcquireContext scope{*context}; + InitializeOpenGLObjects(); + + const auto& layout = GetLayout(); + while (!stop_requested.exchange(false)) { + auto frame = mailbox->TryGetPresentFrame(200); + if (!frame) { + continue; + } + + if (frame->color_reloaded) { + LOG_DEBUG(Render_OpenGL, "Reloading present frame"); + mailbox->ReloadPresentFrame(frame, layout.width, layout.height); + } + glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle); + glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[current_pbo].handle); + glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0); + + // Insert fence for the main thread to block on + frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + + // Bind the previous PBO and read the pixels + glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle); + GLubyte* pixels = static_cast(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)); + VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels}; + video_dumper.AddVideoFrame(frame_data); + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + current_pbo = (current_pbo + 1) % 2; + next_pbo = (current_pbo + 1) % 2; + } + + CleanupOpenGLObjects(); +} + +void FrameDumperOpenGL::InitializeOpenGLObjects() { + const auto& layout = GetLayout(); + for (auto& buffer : pbos) { + buffer.Create(); + glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle); + glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr, + GL_STREAM_READ); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + } +} + +void FrameDumperOpenGL::CleanupOpenGLObjects() { + for (auto& buffer : pbos) { + buffer.Release(); + } +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.h b/src/video_core/renderer_opengl/frame_dumper_opengl.h new file mode 100644 index 000000000..da6d96053 --- /dev/null +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.h @@ -0,0 +1,57 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "core/dumping/backend.h" +#include "core/frontend/framebuffer_layout.h" +#include "video_core/renderer_opengl/gl_resource_manager.h" + +namespace Frontend { +class EmuWindow; +class GraphicsContext; +class TextureMailbox; +} // namespace Frontend + +namespace OpenGL { + +class RendererOpenGL; + +/** + * This is the 'presentation' part in frame dumping. + * Processes frames/textures sent to its mailbox, downloads the pixels and sends the data + * to the video encoding backend. + */ +class FrameDumperOpenGL { +public: + explicit FrameDumperOpenGL(VideoDumper::Backend& video_dumper, Frontend::EmuWindow& emu_window); + ~FrameDumperOpenGL(); + + bool IsDumping() const; + Layout::FramebufferLayout GetLayout() const; + void StartDumping(); + void StopDumping(); + + std::unique_ptr mailbox; + +private: + void InitializeOpenGLObjects(); + void CleanupOpenGLObjects(); + void PresentLoop(); + + VideoDumper::Backend& video_dumper; + std::unique_ptr context; + std::thread present_thread; + std::atomic_bool stop_requested{false}; + + // PBOs used to dump frames faster + std::array pbos; + GLuint current_pbo = 1; + GLuint next_pbo = 0; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 5046895e0..b1fcfb592 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -34,20 +34,6 @@ #include "video_core/renderer_opengl/renderer_opengl.h" #include "video_core/video_core.h" -namespace Frontend { - -struct Frame { - u32 width{}; /// Width of the frame (to detect resize) - u32 height{}; /// Height of the frame - bool color_reloaded = false; /// Texture attachment was recreated (ie: resized) - OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO - OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread - OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread - GLsync render_fence{}; /// Fence created on the render thread - GLsync present_fence{}; /// Fence created on the presentation thread -}; -} // namespace Frontend - namespace OpenGL { // If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have @@ -78,6 +64,7 @@ public: std::queue().swap(free_queue); present_queue.clear(); present_cv.notify_all(); + free_cv.notify_all(); } void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override { @@ -88,7 +75,7 @@ public: glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, frame->color.handle); - if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!"); } glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo); @@ -114,7 +101,7 @@ public: state.Apply(); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, frame->color.handle); - if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!"); } prev_state.Apply(); @@ -144,19 +131,12 @@ public: present_cv.notify_one(); } - Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { - std::unique_lock lock(swap_chain_lock); - // wait for new entries in the present_queue - present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), - [&] { return !present_queue.empty(); }); - if (present_queue.empty()) { - // timed out waiting for a frame to draw so return the previous frame - return previous_frame; - } - + // This is virtual as it is to be overriden in OGLVideoDumpingMailbox below. + virtual void LoadPresentFrame() { // free the previous frame and add it back to the free queue if (previous_frame) { free_queue.push(previous_frame); + free_cv.notify_one(); } // the newest entries are pushed to the front of the queue @@ -168,8 +148,72 @@ public: } present_queue.clear(); previous_frame = frame; + } + + Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { + std::unique_lock lock(swap_chain_lock); + // wait for new entries in the present_queue + present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), + [&] { return !present_queue.empty(); }); + if (present_queue.empty()) { + // timed out waiting for a frame to draw so return the previous frame + return previous_frame; + } + + LoadPresentFrame(); + return previous_frame; + } +}; + +/// This mailbox is different in that it will never discard rendered frames +class OGLVideoDumpingMailbox : public OGLTextureMailbox { +public: + Frontend::Frame* GetRenderFrame() override { + std::unique_lock lock(swap_chain_lock); + + // If theres no free frames, we will wait until one shows up + if (free_queue.empty()) { + free_cv.wait(lock, [&] { return !free_queue.empty(); }); + } + + if (free_queue.empty()) { + LOG_CRITICAL(Render_OpenGL, "Could not get free frame"); + return nullptr; + } + + Frontend::Frame* frame = free_queue.front(); + free_queue.pop(); return frame; } + + void LoadPresentFrame() override { + // free the previous frame and add it back to the free queue + if (previous_frame) { + free_queue.push(previous_frame); + free_cv.notify_one(); + } + + Frontend::Frame* frame = present_queue.back(); + present_queue.pop_back(); + previous_frame = frame; + + // Do not remove entries from the present_queue, as video dumping would require + // that we preserve all frames + } + + Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { + std::unique_lock lock(swap_chain_lock); + // wait for new entries in the present_queue + present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), + [&] { return !present_queue.empty(); }); + if (present_queue.empty()) { + // timed out waiting for a frame + return nullptr; + } + + LoadPresentFrame(); + return previous_frame; + } }; static const char vertex_shader[] = R"( @@ -278,21 +322,35 @@ struct ScreenRectVertex { * * The projection part of the matrix is trivial, hence these operations are represented * by a 3x2 matrix. + * + * @param flipped Whether the frame should be flipped upside down. */ -static std::array MakeOrthographicMatrix(const float width, const float height) { +static std::array MakeOrthographicMatrix(const float width, const float height, + bool flipped) { + std::array matrix; // Laid out in column-major order - // clang-format off - matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f; - matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f; // Last matrix row is implicitly assumed to be [0, 0, 1]. - // clang-format on + if (flipped) { + // clang-format off + matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f; + matrix[1] = 0.f; matrix[3] = 2.f / height; matrix[5] = -1.f; + // clang-format on + } else { + // clang-format off + matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f; + matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f; + // clang-format on + } return matrix; } -RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} { +RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) + : RendererBase{window}, frame_dumper(Core::System::GetInstance().VideoDumper(), window) { + window.mailbox = std::make_unique(); + frame_dumper.mailbox = std::make_unique(); } RendererOpenGL::~RendererOpenGL() = default; @@ -310,56 +368,14 @@ void RendererOpenGL::SwapBuffers() { RenderScreenshot(); - RenderVideoDumping(); - const auto& layout = render_window.GetFramebufferLayout(); + RenderToMailbox(layout, render_window.mailbox, false); - Frontend::Frame* frame; - { - MICROPROFILE_SCOPE(OpenGL_WaitPresent); - - frame = render_window.mailbox->GetRenderFrame(); - - // Clean up sync objects before drawing - - // INTEL driver workaround. We can't delete the previous render sync object until we are - // sure that the presentation is done - if (frame->present_fence) { - glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); - } - - // delete the draw fence if the frame wasn't presented - if (frame->render_fence) { - glDeleteSync(frame->render_fence); - frame->render_fence = 0; - } - - // wait for the presentation to be done - if (frame->present_fence) { - glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(frame->present_fence); - frame->present_fence = 0; - } + if (frame_dumper.IsDumping()) { + RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true); } - { - MICROPROFILE_SCOPE(OpenGL_RenderFrame); - // Recreate the frame if the size of the window has changed - if (layout.width != frame->width || layout.height != frame->height) { - LOG_DEBUG(Render_OpenGL, "Reloading render frame"); - render_window.mailbox->ReloadRenderFrame(frame, layout.width, layout.height); - } - - GLuint render_texture = frame->color.handle; - state.draw.draw_framebuffer = frame->render.handle; - state.Apply(); - DrawScreens(layout); - // Create a fence for the frontend to wait on and swap this frame to OffTex - frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - render_window.mailbox->ReleaseRenderFrame(frame); - m_current_frame++; - } + m_current_frame++; Core::System::GetInstance().perf_stats->EndSystemFrame(); @@ -395,7 +411,7 @@ void RendererOpenGL::RenderScreenshot() { glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); - DrawScreens(layout); + DrawScreens(layout, false); glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, VideoCore::g_screenshot_bits); @@ -448,33 +464,54 @@ void RendererOpenGL::PrepareRendertarget() { } } -void RendererOpenGL::RenderVideoDumping() { - if (cleanup_video_dumping.exchange(false)) { - ReleaseVideoDumpingGLObjects(); - } +void RendererOpenGL::RenderToMailbox(const Layout::FramebufferLayout& layout, + std::unique_ptr& mailbox, + bool flipped) { - if (Core::System::GetInstance().VideoDumper().IsDumping()) { - if (prepare_video_dumping.exchange(false)) { - InitVideoDumpingGLObjects(); + Frontend::Frame* frame; + { + MICROPROFILE_SCOPE(OpenGL_WaitPresent); + + frame = mailbox->GetRenderFrame(); + + // Clean up sync objects before drawing + + // INTEL driver workaround. We can't delete the previous render sync object until we are + // sure that the presentation is done + if (frame->present_fence) { + glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); } - const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout(); - glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_dumping_framebuffer.handle); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle); - DrawScreens(layout); + // delete the draw fence if the frame wasn't presented + if (frame->render_fence) { + glDeleteSync(frame->render_fence); + frame->render_fence = 0; + } - glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[current_pbo].handle); - glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0); - glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[next_pbo].handle); + // wait for the presentation to be done + if (frame->present_fence) { + glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(frame->present_fence); + frame->present_fence = 0; + } + } - GLubyte* pixels = static_cast(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)); - VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels}; - Core::System::GetInstance().VideoDumper().AddVideoFrame(frame_data); + { + MICROPROFILE_SCOPE(OpenGL_RenderFrame); + // Recreate the frame if the size of the window has changed + if (layout.width != frame->width || layout.height != frame->height) { + LOG_DEBUG(Render_OpenGL, "Reloading render frame"); + mailbox->ReloadRenderFrame(frame, layout.width, layout.height); + } - glUnmapBuffer(GL_PIXEL_PACK_BUFFER); - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - current_pbo = (current_pbo + 1) % 2; - next_pbo = (current_pbo + 1) % 2; + GLuint render_texture = frame->color.handle; + state.draw.draw_framebuffer = frame->render.handle; + state.Apply(); + DrawScreens(layout, flipped); + // Create a fence for the frontend to wait on and swap this frame to OffTex + frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + mailbox->ReleaseRenderFrame(frame); } } @@ -885,7 +922,7 @@ void RendererOpenGL::DrawSingleScreenStereo(const ScreenInfo& screen_info_l, /** * Draws the emulated screens to the emulator window. */ -void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) { +void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool flipped) { if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) { // Update background color before drawing glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue, @@ -912,7 +949,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) { // Set projection matrix std::array ortho_matrix = - MakeOrthographicMatrix((float)layout.width, (float)layout.height); + MakeOrthographicMatrix((float)layout.width, (float)layout.height, flipped); glUniformMatrix3x2fv(uniform_modelview_matrix, 1, GL_FALSE, ortho_matrix.data()); // Bind texture in Texture Unit 0 @@ -1051,41 +1088,11 @@ void RendererOpenGL::TryPresent(int timeout_ms) { void RendererOpenGL::UpdateFramerate() {} void RendererOpenGL::PrepareVideoDumping() { - prepare_video_dumping = true; + frame_dumper.StartDumping(); } void RendererOpenGL::CleanupVideoDumping() { - cleanup_video_dumping = true; -} - -void RendererOpenGL::InitVideoDumpingGLObjects() { - const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout(); - - frame_dumping_framebuffer.Create(); - glGenRenderbuffers(1, &frame_dumping_renderbuffer); - glBindRenderbuffer(GL_RENDERBUFFER, frame_dumping_renderbuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle); - glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - frame_dumping_renderbuffer); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - - for (auto& buffer : frame_dumping_pbos) { - buffer.Create(); - glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle); - glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr, - GL_STREAM_READ); - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - } -} - -void RendererOpenGL::ReleaseVideoDumpingGLObjects() { - frame_dumping_framebuffer.Release(); - glDeleteRenderbuffers(1, &frame_dumping_renderbuffer); - - for (auto& buffer : frame_dumping_pbos) { - buffer.Release(); - } + frame_dumper.StopDumping(); } static const char* GetSource(GLenum source) { diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 96df7f8ac..634d26ca4 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -10,6 +10,7 @@ #include "common/math_util.h" #include "core/hw/gpu.h" #include "video_core/renderer_base.h" +#include "video_core/renderer_opengl/frame_dumper_opengl.h" #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" @@ -17,6 +18,20 @@ namespace Layout { struct FramebufferLayout; } +namespace Frontend { + +struct Frame { + u32 width{}; /// Width of the frame (to detect resize) + u32 height{}; /// Height of the frame + bool color_reloaded = false; /// Texture attachment was recreated (ie: resized) + OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO + OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread + OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread + GLsync render_fence{}; /// Fence created on the render thread + GLsync present_fence{}; /// Fence created on the presentation thread +}; +} // namespace Frontend + namespace OpenGL { /// Structure used for storing information about the textures for each 3DS screen @@ -72,10 +87,11 @@ private: void ReloadShader(); void PrepareRendertarget(); void RenderScreenshot(); - void RenderVideoDumping(); + void RenderToMailbox(const Layout::FramebufferLayout& layout, + std::unique_ptr& mailbox, bool flipped); void ConfigureFramebufferTexture(TextureInfo& texture, const GPU::Regs::FramebufferConfig& framebuffer); - void DrawScreens(const Layout::FramebufferLayout& layout); + void DrawScreens(const Layout::FramebufferLayout& layout, bool flipped); void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h); void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h); void DrawSingleScreenStereoRotated(const ScreenInfo& screen_info_l, @@ -91,9 +107,6 @@ private: // Fills active OpenGL texture with the given RGB color. void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture); - void InitVideoDumpingGLObjects(); - void ReleaseVideoDumpingGLObjects(); - OpenGLState state; // OpenGL object IDs @@ -120,19 +133,7 @@ private: GLuint attrib_position; GLuint attrib_tex_coord; - // Frame dumping - OGLFramebuffer frame_dumping_framebuffer; - GLuint frame_dumping_renderbuffer; - - // Whether prepare/cleanup video dumping has been requested. - // They will be executed on next frame. - std::atomic_bool prepare_video_dumping = false; - std::atomic_bool cleanup_video_dumping = false; - - // PBOs used to dump frames faster - std::array frame_dumping_pbos; - GLuint current_pbo = 1; - GLuint next_pbo = 0; + FrameDumperOpenGL frame_dumper; }; } // namespace OpenGL From 3c6765e87c78d151712f7c74859fad91b27db4a5 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 28 Jan 2020 22:19:36 +0800 Subject: [PATCH 02/21] core: Properly std::move things around --- src/audio_core/dsp_interface.cpp | 6 +++--- src/audio_core/dsp_interface.h | 2 +- src/audio_core/hle/hle.cpp | 2 +- src/audio_core/lle/lle.cpp | 3 ++- src/core/dumping/backend.h | 8 ++++---- src/core/dumping/ffmpeg_backend.cpp | 4 ++-- src/core/dumping/ffmpeg_backend.h | 4 ++-- src/video_core/renderer_opengl/frame_dumper_opengl.cpp | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp index b6e74b82c..1bb2a1d34 100644 --- a/src/audio_core/dsp_interface.cpp +++ b/src/audio_core/dsp_interface.cpp @@ -38,14 +38,14 @@ void DspInterface::EnableStretching(bool enable) { perform_time_stretching = enable; } -void DspInterface::OutputFrame(StereoFrame16& frame) { +void DspInterface::OutputFrame(StereoFrame16 frame) { if (!sink) return; fifo.Push(frame.data(), frame.size()); if (Core::System::GetInstance().VideoDumper().IsDumping()) { - Core::System::GetInstance().VideoDumper().AddAudioFrame(frame); + Core::System::GetInstance().VideoDumper().AddAudioFrame(std::move(frame)); } } @@ -56,7 +56,7 @@ void DspInterface::OutputSample(std::array sample) { fifo.Push(&sample, 1); if (Core::System::GetInstance().VideoDumper().IsDumping()) { - Core::System::GetInstance().VideoDumper().AddAudioSample(sample); + Core::System::GetInstance().VideoDumper().AddAudioSample(std::move(sample)); } } diff --git a/src/audio_core/dsp_interface.h b/src/audio_core/dsp_interface.h index fc3d7cab2..5b83e684d 100644 --- a/src/audio_core/dsp_interface.h +++ b/src/audio_core/dsp_interface.h @@ -100,7 +100,7 @@ public: void EnableStretching(bool enable); protected: - void OutputFrame(StereoFrame16& frame); + void OutputFrame(StereoFrame16 frame); void OutputSample(std::array sample); private: diff --git a/src/audio_core/hle/hle.cpp b/src/audio_core/hle/hle.cpp index 052e507c5..9c79a7537 100644 --- a/src/audio_core/hle/hle.cpp +++ b/src/audio_core/hle/hle.cpp @@ -400,7 +400,7 @@ bool DspHle::Impl::Tick() { // shared memory region) current_frame = GenerateCurrentFrame(); - parent.OutputFrame(current_frame); + parent.OutputFrame(std::move(current_frame)); return true; } diff --git a/src/audio_core/lle/lle.cpp b/src/audio_core/lle/lle.cpp index e9948d1c2..0121bae27 100644 --- a/src/audio_core/lle/lle.cpp +++ b/src/audio_core/lle/lle.cpp @@ -483,7 +483,8 @@ DspLle::DspLle(Memory::MemorySystem& memory, bool multithread) *memory.GetFCRAMPointer(address - Memory::FCRAM_PADDR) = value; }; impl->teakra.SetAHBMCallback(ahbm); - impl->teakra.SetAudioCallback([this](std::array sample) { OutputSample(sample); }); + impl->teakra.SetAudioCallback( + [this](std::array sample) { OutputSample(std::move(sample)); }); } DspLle::~DspLle() = default; diff --git a/src/core/dumping/backend.h b/src/core/dumping/backend.h index c2a4d532a..3dc89082c 100644 --- a/src/core/dumping/backend.h +++ b/src/core/dumping/backend.h @@ -30,8 +30,8 @@ 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 void AddVideoFrame(VideoFrame frame) = 0; + virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0; virtual void AddAudioSample(const std::array& sample) = 0; virtual void StopDumping() = 0; virtual bool IsDumping() const = 0; @@ -45,8 +45,8 @@ public: const Layout::FramebufferLayout& /*layout*/) override { return false; } - void AddVideoFrame(const VideoFrame& /*frame*/) override {} - void AddAudioFrame(const AudioCore::StereoFrame16& /*frame*/) override {} + void AddVideoFrame(VideoFrame /*frame*/) override {} + void AddAudioFrame(AudioCore::StereoFrame16 /*frame*/) override {} void AddAudioSample(const std::array& /*sample*/) override {} void StopDumping() override {} bool IsDumping() const override { diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 811a5c99b..9becaff7b 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -450,13 +450,13 @@ 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) { +void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) { std::array, 2> refactored_frame; for (std::size_t i = 0; i < frame.size(); i++) { refactored_frame[0][i] = frame[i][0]; diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index 0208195d5..8af74b0a8 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -163,8 +163,8 @@ public: ~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; + void AddVideoFrame(VideoFrame frame) override; + void AddAudioFrame(AudioCore::StereoFrame16 frame) override; void AddAudioSample(const std::array& sample) override; void StopDumping() override; bool IsDumping() const override; diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp index 7be4cc8ef..53985823c 100644 --- a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp @@ -67,7 +67,7 @@ void FrameDumperOpenGL::PresentLoop() { glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle); GLubyte* pixels = static_cast(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)); VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels}; - video_dumper.AddVideoFrame(frame_data); + video_dumper.AddVideoFrame(std::move(frame_data)); glUnmapBuffer(GL_PIXEL_PACK_BUFFER); glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); From 016f8be0b8a5778a2ead3fbecda4e5b4065a7591 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 29 Jan 2020 14:54:39 +0800 Subject: [PATCH 03/21] core/dumping: Allow format/encoder selection+configuration The ParamPackage got modified so that we can use range-based for on it --- src/citra/citra.cpp | 2 +- src/citra_qt/main.cpp | 4 +- src/common/param_package.cpp | 16 ++++++++ src/common/param_package.h | 10 ++++- src/core/dumping/backend.h | 5 +-- src/core/dumping/ffmpeg_backend.cpp | 63 ++++++++++++++++++++--------- src/core/dumping/ffmpeg_backend.h | 6 +-- src/core/settings.h | 12 ++++++ 8 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index ce06b31b7..a1455d373 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -409,7 +409,7 @@ int main(int argc, char** argv) { if (!dump_video.empty()) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - system.VideoDumper().StartDumping(dump_video, "webm", layout); + system.VideoDumper().StartDumping(dump_video, layout); } std::thread render_thread([&emu_window] { emu_window->Present(); }); diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 0c87e98e1..e106bf170 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -976,7 +976,7 @@ void GMainWindow::BootGame(const QString& filename) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(), - "webm", layout); + layout); video_dumping_on_start = false; video_dumping_path.clear(); } @@ -1815,7 +1815,7 @@ void GMainWindow::OnStartVideoDumping() { if (emulation_running) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout); + Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout); } else { video_dumping_on_start = true; video_dumping_path = path; diff --git a/src/common/param_package.cpp b/src/common/param_package.cpp index 433b34b36..3a218efbc 100644 --- a/src/common/param_package.cpp +++ b/src/common/param_package.cpp @@ -135,4 +135,20 @@ void ParamPackage::Clear() { data.clear(); } +ParamPackage::DataType::iterator ParamPackage::begin() { + return data.begin(); +} + +ParamPackage::DataType::const_iterator ParamPackage::begin() const { + return data.begin(); +} + +ParamPackage::DataType::iterator ParamPackage::end() { + return data.end(); +} + +ParamPackage::DataType::const_iterator ParamPackage::end() const { + return data.end(); +} + } // namespace Common diff --git a/src/common/param_package.h b/src/common/param_package.h index 6a0a9b656..1fffb5035 100644 --- a/src/common/param_package.h +++ b/src/common/param_package.h @@ -5,15 +5,15 @@ #pragma once #include +#include #include -#include namespace Common { /// A string-based key-value container supporting serializing to and deserializing from a string class ParamPackage { public: - using DataType = std::unordered_map; + using DataType = std::map; ParamPackage() = default; explicit ParamPackage(const std::string& serialized); @@ -35,6 +35,12 @@ public: void Erase(const std::string& key); void Clear(); + // For range-based for + DataType::iterator begin(); + DataType::const_iterator begin() const; + DataType::iterator end(); + DataType::const_iterator end() const; + private: DataType data; }; diff --git a/src/core/dumping/backend.h b/src/core/dumping/backend.h index 3dc89082c..b0b63ba66 100644 --- a/src/core/dumping/backend.h +++ b/src/core/dumping/backend.h @@ -28,8 +28,7 @@ public: class Backend { public: virtual ~Backend(); - virtual bool StartDumping(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout) = 0; + virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0; virtual void AddVideoFrame(VideoFrame frame) = 0; virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0; virtual void AddAudioSample(const std::array& sample) = 0; @@ -41,7 +40,7 @@ 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; } diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 9becaff7b..2e7ecbba5 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -5,7 +5,9 @@ #include "common/assert.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "common/param_package.h" #include "core/dumping/ffmpeg_backend.h" +#include "core/settings.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" @@ -27,6 +29,15 @@ 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(); } @@ -100,9 +111,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou 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 +120,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->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) { @@ -200,8 +214,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { sample_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 +223,24 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { // Configure audio codec context codec_context->codec_type = AVMEDIA_TYPE_AUDIO; - codec_context->bit_rate = 64000; + codec_context->bit_rate = Settings::values.audio_bitrate; codec_context->sample_fmt = codec->sample_fmts[0]; codec_context->sample_rate = AudioCore::native_sample_rate; codec_context->channel_layout = AV_CH_LAYOUT_STEREO; codec_context->channels = 2; - 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); + } + // Create audio stream stream = avformat_new_stream(format_context, codec); if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { @@ -305,8 +325,7 @@ 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 +334,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; @@ -338,13 +356,19 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format, if (!audio_stream.Init(format_context.get())) 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; @@ -392,12 +416,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; } diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index 8af74b0a8..f08f31d3d 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -129,8 +129,7 @@ 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); @@ -161,8 +160,7 @@ class FFmpegBackend : public Backend { public: FFmpegBackend(); ~FFmpegBackend() override; - bool StartDumping(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout) override; + bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override; void AddVideoFrame(VideoFrame frame) override; void AddAudioFrame(AudioCore::StereoFrame16 frame) override; void AddAudioSample(const std::array& sample) override; diff --git a/src/core/settings.h b/src/core/settings.h index 78b11912c..8739bcd1c 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -204,6 +204,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 From 834da14329e43d891fd4d8e2087a6e5b346f0aa3 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 29 Jan 2020 14:57:16 +0800 Subject: [PATCH 04/21] citra, citra_qt: Add video dumping config read/write The default values are VP9/libvorbis just like before. The default configuration is provided for VP9 --- src/citra/config.cpp | 27 ++++++++++ src/citra/default_ini.h | 26 ++++++++++ src/citra_qt/configuration/config.cpp | 72 +++++++++++++++++++++++++++ src/citra_qt/configuration/config.h | 2 + 4 files changed, 127 insertions(+) diff --git a/src/citra/config.cpp b/src/citra/config.cpp index b2c878ddf..92efb310b 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -264,6 +264,33 @@ void Config::ReadValues() { sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); Settings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); Settings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); + + // Video Dumping + Settings::values.output_format = + sdl2_config->GetString("Video Dumping", "output_format", "webm"); + Settings::values.format_options = sdl2_config->GetString("Video Dumping", "format_options", ""); + + Settings::values.video_encoder = + sdl2_config->GetString("Video Dumping", "video_encoder", "libvpx-vp9"); + + // Options for variable bit rate live streaming taken from here: + // https://developers.google.com/media/vp9/live-encoding + std::string default_video_options; + if (Settings::values.video_encoder == "libvpx-vp9") { + default_video_options = + "quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1"; + } + Settings::values.video_encoder_options = + sdl2_config->GetString("Video Dumping", "video_encoder_options", default_video_options); + Settings::values.video_bitrate = + sdl2_config->GetInteger("Video Dumping", "video_bitrate", 2500000); + + Settings::values.audio_encoder = + sdl2_config->GetString("Video Dumping", "audio_encoder", "libvorbis"); + Settings::values.audio_encoder_options = + sdl2_config->GetString("Video Dumping", "audio_encoder_options", ""); + Settings::values.audio_bitrate = + sdl2_config->GetInteger("Video Dumping", "audio_bitrate", 64000); } void Config::Reload() { diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 9c441e354..6af827c09 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -295,5 +295,31 @@ web_api_url = https://api.citra-emu.org # See https://profile.citra-emu.org/ for more info citra_username = citra_token = + +[Video Dumping] +# Format of the video to output, default: webm +output_format = + +# Options passed to the muxer (optional) +# This is a param package, format: [key1]:[value1],[key2]:[value2],... +format_options = + +# Video encoder used, default: libvpx-vp9 +video_encoder = + +# Options passed to the video codec (optional) +video_encoder_options = + +# Video bitrate, default: 2500000 +video_bitrate = + +# Audio encoder used, default: libvorbis +audio_encoder = + +# Options passed to the audio codec (optional) +audio_encoder_options = + +# Audio bitrate, default: 64000 +audio_bitrate = )"; } diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 82274bff0..416d05f09 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -93,6 +93,7 @@ void Config::ReadValues() { ReadMiscellaneousValues(); ReadDebuggingValues(); ReadWebServiceValues(); + ReadVideoDumpingValues(); ReadUIValues(); ReadUtilityValues(); } @@ -485,6 +486,49 @@ void Config::ReadSystemValues() { qt_config->endGroup(); } +// Options for variable bit rate live streaming taken from here: +// https://developers.google.com/media/vp9/live-encoding +const QString DEFAULT_VIDEO_ENCODER_OPTIONS = + QStringLiteral("quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1"); +const QString DEFAULT_AUDIO_ENCODER_OPTIONS = QString{}; + +void Config::ReadVideoDumpingValues() { + qt_config->beginGroup(QStringLiteral("VideoDumping")); + + Settings::values.output_format = + ReadSetting(QStringLiteral("output_format"), QStringLiteral("webm")) + .toString() + .toStdString(); + Settings::values.format_options = + ReadSetting(QStringLiteral("format_options")).toString().toStdString(); + + Settings::values.video_encoder = + ReadSetting(QStringLiteral("video_encoder"), QStringLiteral("libvpx-vp9")) + .toString() + .toStdString(); + + Settings::values.video_encoder_options = + ReadSetting(QStringLiteral("video_encoder_options"), DEFAULT_VIDEO_ENCODER_OPTIONS) + .toString() + .toStdString(); + + Settings::values.video_bitrate = + ReadSetting(QStringLiteral("video_bitrate"), 2500000).toULongLong(); + + Settings::values.audio_encoder = + ReadSetting(QStringLiteral("audio_encoder"), QStringLiteral("libvorbis")) + .toString() + .toStdString(); + Settings::values.audio_encoder_options = + ReadSetting(QStringLiteral("audio_encoder_options"), DEFAULT_AUDIO_ENCODER_OPTIONS) + .toString() + .toStdString(); + Settings::values.audio_bitrate = + ReadSetting(QStringLiteral("audio_bitrate"), 64000).toULongLong(); + + qt_config->endGroup(); +} + void Config::ReadUIValues() { qt_config->beginGroup(QStringLiteral("UI")); @@ -617,6 +661,7 @@ void Config::SaveValues() { SaveMiscellaneousValues(); SaveDebuggingValues(); SaveWebServiceValues(); + SaveVideoDumpingValues(); SaveUIValues(); SaveUtilityValues(); } @@ -915,6 +960,33 @@ void Config::SaveSystemValues() { qt_config->endGroup(); } +void Config::SaveVideoDumpingValues() { + qt_config->beginGroup(QStringLiteral("VideoDumping")); + + WriteSetting(QStringLiteral("output_format"), + QString::fromStdString(Settings::values.output_format), QStringLiteral("webm")); + WriteSetting(QStringLiteral("format_options"), + QString::fromStdString(Settings::values.format_options)); + WriteSetting(QStringLiteral("video_encoder"), + QString::fromStdString(Settings::values.video_encoder), + QStringLiteral("libvpx-vp9")); + WriteSetting(QStringLiteral("video_encoder_options"), + QString::fromStdString(Settings::values.video_encoder_options), + DEFAULT_VIDEO_ENCODER_OPTIONS); + WriteSetting(QStringLiteral("video_bitrate"), + static_cast(Settings::values.video_bitrate), 2500000); + WriteSetting(QStringLiteral("audio_encoder"), + QString::fromStdString(Settings::values.audio_encoder), + QStringLiteral("libvorbis")); + WriteSetting(QStringLiteral("audio_encoder_options"), + QString::fromStdString(Settings::values.audio_encoder_options), + DEFAULT_AUDIO_ENCODER_OPTIONS); + WriteSetting(QStringLiteral("audio_bitrate"), + static_cast(Settings::values.audio_bitrate), 64000); + + qt_config->endGroup(); +} + void Config::SaveUIValues() { qt_config->beginGroup(QStringLiteral("UI")); diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 7d65d41b6..8b1cf8193 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -44,6 +44,7 @@ private: void ReadUpdaterValues(); void ReadUtilityValues(); void ReadWebServiceValues(); + void ReadVideoDumpingValues(); void SaveValues(); void SaveAudioValues(); @@ -65,6 +66,7 @@ private: void SaveUpdaterValues(); void SaveUtilityValues(); void SaveWebServiceValues(); + void SaveVideoDumpingValues(); QVariant ReadSetting(const QString& name) const; QVariant ReadSetting(const QString& name, const QVariant& default_value) const; From 17461b5d114ffe6bdeb3251d2591f7e7e8589c47 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 11:22:41 +0800 Subject: [PATCH 05/21] ffmpeg: Correctly set pixel format While YUV420P is widely used, not all encoders accept it (e.g. Intel QSV only accepts NV12). We should use the codec's preferred pixel format instead as we need to rescale the frame anyway. --- src/core/dumping/ffmpeg_backend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 2e7ecbba5..f3c3e236a 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -126,7 +126,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou 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->pix_fmt = codec->pix_fmts ? codec->pix_fmts[0] : AV_PIX_FMT_YUV420P; if (output_format->flags & AVFMT_GLOBALHEADER) codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; From 8b9c01ded97e752790430fa86e113c25665daafa Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 11:28:57 +0800 Subject: [PATCH 06/21] ffmpeg: Correctly handle sample format We previously assumed that the first preferred sample format is planar, but that may not be true for all codecs. Instead we should find a supported sample format that is planar. --- src/core/dumping/ffmpeg_backend.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index f3c3e236a..ef7843ffc 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -224,7 +224,25 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { // Configure audio codec context codec_context->codec_type = AVMEDIA_TYPE_AUDIO; codec_context->bit_rate = Settings::values.audio_bitrate; - codec_context->sample_fmt = codec->sample_fmts[0]; + if (codec->sample_fmts) { + codec_context->sample_fmt = AV_SAMPLE_FMT_NONE; + // Use any planar format + const AVSampleFormat* ptr = codec->sample_fmts; + while ((*ptr) != -1) { + if (av_sample_fmt_is_planar((*ptr))) { + codec_context->sample_fmt = (*ptr); + break; + } + ptr++; + } + if (codec_context->sample_fmt == AV_SAMPLE_FMT_NONE) { + LOG_ERROR(Render, "Specified audio encoder does not support any planar format"); + return false; + } + } else { + codec_context->sample_fmt = AV_SAMPLE_FMT_S16P; + } + codec_context->sample_rate = AudioCore::native_sample_rate; codec_context->channel_layout = AV_CH_LAYOUT_STEREO; codec_context->channels = 2; From 4161163d9c99a1cf4c4ea6f22ab32ab66a593ad4 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 12:23:07 +0800 Subject: [PATCH 07/21] ffmpeg: Correctly handle sample rates Previously, we just used the native sample rate for encoding. However, some encoders like libmp3lame doesn't support it. Therefore, we now use a supported sample rate (preferring the native one if possible). FFmpeg requires audio data to be sent in a sequence of frames, each containing the same specific number of samples. Previously, we buffered input samples in FFmpegBackend. However, as the source and destination sample rates can now be different, we should buffer resampled data instead. swresample have an internal input buffer, so we now just forward all data to it and 'gradually' receive resampled data, at most one frame_size at a time. When there is not enough resampled data to form a frame, we will record the current offset and request for less data on the next call. Additionally, this commit also fixes a flaw. When an encoder supports variable frame sizes, its frame size is reported to be 0, which breaks our buffering system. Now we treat variable frame size encoders as having a frame size of 160 (the size of a HLE audio frame). --- src/core/dumping/ffmpeg_backend.cpp | 129 ++++++++++++++++------------ src/core/dumping/ffmpeg_backend.h | 19 ++-- 2 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index ef7843ffc..3c34c8440 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -211,7 +211,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { if (!FFmpegStream::Init(format_context)) return false; - sample_count = 0; + frame_count = 0; // Initialize audio codec const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str()); @@ -243,7 +243,20 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { codec_context->sample_fmt = AV_SAMPLE_FMT_S16P; } - codec_context->sample_rate = AudioCore::native_sample_rate; + 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->channel_layout = AV_CH_LAYOUT_STEREO; codec_context->channels = 2; @@ -259,6 +272,12 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { LOG_WARNING(Render, "Audio encoder options not found: {}", buf); } + if (codec_context->frame_size) { + frame_size = static_cast(codec_context->frame_size); + } else { // variable frame size support + frame_size = std::tuple_size::value; + } + // Create audio stream stream = avformat_new_stream(format_context, codec); if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { @@ -291,7 +310,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; @@ -312,31 +331,62 @@ void FFmpegAudioStream::Free() { av_freep(&resampled_data); } -void FFmpegAudioStream::ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) { +void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0, + const VariableAudioFrame& channel1) { ASSERT_MSG(channel0.size() == channel1.size(), "Frames of the two channels must have the same number of samples"); - std::array src_data = {reinterpret_cast(channel0.data()), - reinterpret_cast(channel1.data())}; - if (swr_convert(swr_context.get(), resampled_data, channel0.size(), src_data.data(), - channel0.size()) < 0) { + const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt); + std::array src_data = {reinterpret_cast(channel0.data()), + reinterpret_cast(channel1.data())}; + std::array dst_data = {resampled_data[0] + sample_size * offset, + resampled_data[1] + sample_size * offset}; + + 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]; + audio_frame->data[1] = resampled_data[1]; + audio_frame->pts = frame_count * frame_size; + frame_count++; + + SendFrame(audio_frame.get()); + + // swr_convert buffers input internally. Try to get more resampled data + resampled_count = swr_convert(swr_context.get(), resampled_data, frame_size, nullptr, 0); + if (resampled_count < 0) { + LOG_ERROR(Render, "Audio frame dropped: Could not resample data"); + return; + } + if (static_cast(resampled_count) < frame_size) { + offset = resampled_count; + break; + } + } } -std::size_t FFmpegAudioStream::GetAudioFrameSize() const { - ASSERT_MSG(codec_context, "Codec context is not initialized yet!"); - return codec_context->frame_size; +void FFmpegAudioStream::Flush() { + // Send the last samples + audio_frame->nb_samples = offset; + audio_frame->data[0] = resampled_data[0]; + audio_frame->data[1] = resampled_data[1]; + audio_frame->pts = frame_count * frame_size; + + SendFrame(audio_frame.get()); + + FFmpegStream::Flush(); } FFmpegMuxer::~FFmpegMuxer() { @@ -402,7 +452,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); } @@ -414,10 +465,6 @@ void FFmpegMuxer::FlushAudio() { audio_stream.Flush(); } -std::size_t FFmpegMuxer::GetAudioFrameSize() const { - return audio_stream.GetAudioFrameSize(); -} - void FFmpegMuxer::WriteTrailer() { av_write_trailer(format_context.get()); } @@ -498,24 +545,20 @@ void FFmpegBackend::AddVideoFrame(VideoFrame frame) { } void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) { - std::array, 2> refactored_frame; + std::array refactored_frame; + for (auto& channel : refactored_frame) { + channel.resize(frame.size()); + } for (std::size_t i = 0; i < frame.size(); i++) { refactored_frame[0][i] = frame[i][0]; refactored_frame[1][i] = frame[i][1]; } - for (auto i : {0, 1}) { - audio_buffers[i].insert(audio_buffers[i].end(), refactored_frame[i].begin(), - refactored_frame[i].end()); - } - CheckAudioBuffer(); + ffmpeg.ProcessAudioFrame(refactored_frame[0], refactored_frame[1]); } void FFmpegBackend::AddAudioSample(const std::array& sample) { - for (auto i : {0, 1}) { - audio_buffers[i].push_back(sample[i]); - } - CheckAudioBuffer(); + ffmpeg.ProcessAudioFrame({sample[0]}, {sample[1]}); } void FFmpegBackend::StopDumping() { @@ -525,12 +568,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()); } @@ -554,18 +591,4 @@ 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)); - - audio_buffers[i].erase(audio_buffers[i].begin(), audio_buffers[i].begin() + frame_size); - } - } -} - } // namespace VideoDumper diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index f08f31d3d..f0962189e 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -96,6 +96,7 @@ 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: @@ -103,8 +104,8 @@ public: bool Init(AVFormatContext* format_context); 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 +114,14 @@ private: } }; - u64 sample_count{}; + u64 frame_size{}; + u64 frame_count{}; std::unique_ptr audio_frame{}; std::unique_ptr swr_context{}; u8** resampled_data{}; + u64 offset{}; // Number of output samples that are currently in resampled_data. }; /** @@ -132,10 +135,9 @@ public: 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: @@ -153,8 +155,7 @@ private: /** * 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: @@ -169,7 +170,6 @@ public: Layout::FramebufferLayout GetLayout() const override; private: - void CheckAudioBuffer(); void EndDumping(); std::atomic_bool is_dumping = false; ///< Whether the backend is currently dumping @@ -182,9 +182,6 @@ private: Common::Event event1, event2; std::thread video_processing_thread; - /// An audio buffer used to temporarily hold audio data, before the size is big enough - /// to be sent to the encoder as a frame - std::array audio_buffers; std::array, 2> audio_frame_queues; std::thread audio_processing_thread; From 8c4bcf9f5987a1b92ab65547dcabeee90e2b3eb4 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 12:28:13 +0800 Subject: [PATCH 08/21] ffmpeg: Add ListFormats and ListEncoders These two functions allow the frontend to get a list of encoders/formats and their specific options. Retrieving the options is harder than it sounds due to FFmpeg's strange AVClass and AVOption system. For example, for integer and flags options, 'named constants' can be set. They are of type `AV_OPT_TYPE_CONST` and are categoried according to the `unit` field. An option can recognize all constants of the same `unit`. --- src/core/CMakeLists.txt | 2 +- src/core/dumping/ffmpeg_backend.cpp | 243 +++++++++++++++++++++++++++- src/core/dumping/ffmpeg_backend.h | 41 +++++ 3 files changed, 284 insertions(+), 2 deletions(-) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ab9f52e32..225a68796 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -492,5 +492,5 @@ if (ARCHITECTURE_x86_64) endif() if (ENABLE_FFMPEG_VIDEO_DUMPER) - target_link_libraries(core PRIVATE FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil) + target_link_libraries(core PUBLIC FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil) endif() diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 3c34c8440..68b148bd3 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -2,17 +2,19 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include "common/assert.h" #include "common/file_util.h" #include "common/logging/log.h" #include "common/param_package.h" +#include "common/string_util.h" #include "core/dumping/ffmpeg_backend.h" #include "core/settings.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" extern "C" { -#include +#include } namespace VideoDumper { @@ -591,4 +593,243 @@ void FFmpegBackend::EndDumping() { processing_ended.Set(); } +// To std string, but handles nullptr +std::string ToStdString(const char* str, const std::string& fallback = "") { + return str ? std::string{str} : fallback; +} + +std::string FormatDuration(s64 duration) { + // The following is implemented according to libavutil code (opt.c) + std::string out; + if (duration < 0 && duration != std::numeric_limits::min()) { + out.append("-"); + duration = -duration; + } + if (duration == std::numeric_limits::max()) { + return "INT64_MAX"; + } else if (duration == std::numeric_limits::min()) { + return "INT64_MIN"; + } else if (duration > 3600ll * 1000000ll) { + out.append(fmt::format("{}:{:02d}:{:02d}.{:06d}", duration / 3600000000ll, + ((duration / 60000000ll) % 60), ((duration / 1000000ll) % 60), + duration % 1000000)); + } else if (duration > 60ll * 1000000ll) { + out.append(fmt::format("{}:{:02d}.{:06d}", duration / 60000000ll, + ((duration / 1000000ll) % 60), duration % 1000000)); + } else { + out.append(fmt::format("{}.{:06d}", duration / 1000000ll, duration % 1000000)); + } + while (out.back() == '0') { + out.erase(out.size() - 1, 1); + } + if (out.back() == '.') { + out.erase(out.size() - 1, 1); + } + return out; +} + +std::string FormatDefaultValue(const AVOption* option, + const std::vector& named_constants) { + // The following is taken and modified from libavutil code (opt.c) + switch (option->type) { + case AV_OPT_TYPE_BOOL: { + const auto value = option->default_val.i64; + if (value < 0) { + return "auto"; + } + return value ? "true" : "false"; + } + case AV_OPT_TYPE_FLAGS: { + const auto value = option->default_val.i64; + std::string out; + for (const auto& constant : named_constants) { + if (!(value & constant.value)) { + continue; + } + if (!out.empty()) { + out.append("+"); + } + out.append(constant.name); + } + return out.empty() ? fmt::format("{}", value) : out; + } + case AV_OPT_TYPE_DURATION: { + return FormatDuration(option->default_val.i64); + } + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_UINT64: + case AV_OPT_TYPE_INT64: { + const auto value = option->default_val.i64; + for (const auto& constant : named_constants) { + if (constant.value == value) { + return constant.name; + } + } + return fmt::format("{}", value); + } + case AV_OPT_TYPE_DOUBLE: + case AV_OPT_TYPE_FLOAT: { + return fmt::format("{}", option->default_val.dbl); + } + case AV_OPT_TYPE_RATIONAL: { + const auto q = av_d2q(option->default_val.dbl, std::numeric_limits::max()); + return fmt::format("{}/{}", q.num, q.den); + } + case AV_OPT_TYPE_PIXEL_FMT: { + const char* name = av_get_pix_fmt_name(static_cast(option->default_val.i64)); + return ToStdString(name, "none"); + } + case AV_OPT_TYPE_SAMPLE_FMT: { + const char* name = + av_get_sample_fmt_name(static_cast(option->default_val.i64)); + return ToStdString(name, "none"); + } + case AV_OPT_TYPE_COLOR: + case AV_OPT_TYPE_IMAGE_SIZE: + case AV_OPT_TYPE_STRING: + case AV_OPT_TYPE_DICT: + case AV_OPT_TYPE_VIDEO_RATE: { + return ToStdString(option->default_val.str); + } + case AV_OPT_TYPE_CHANNEL_LAYOUT: { + return fmt::format("{:#x}", option->default_val.i64); + } + default: + return ""; + } +} + +void GetOptionListSingle(std::vector& out, const AVClass* av_class) { + if (av_class == nullptr) { + return; + } + + const AVOption* current = nullptr; + std::unordered_map> named_constants_map; + // First iteration: find and place all named constants + while ((current = av_opt_next(&av_class, current))) { + if (current->type != AV_OPT_TYPE_CONST || !current->unit) { + continue; + } + named_constants_map[current->unit].push_back( + {current->name, ToStdString(current->help), current->default_val.i64}); + } + // Second iteration: find all options + current = nullptr; + while ((current = av_opt_next(&av_class, current))) { + // Currently we cannot handle binary options + if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) { + continue; + } + std::vector named_constants; + if (current->unit && named_constants_map.count(current->unit)) { + named_constants = named_constants_map.at(current->unit); + } + const auto default_value = FormatDefaultValue(current, named_constants); + out.push_back({current->name, ToStdString(current->help), current->type, default_value, + std::move(named_constants), current->min, current->max}); + } +} + +void GetOptionList(std::vector& out, const AVClass* av_class) { + if (av_class == nullptr) { + return; + } + + GetOptionListSingle(out, av_class); + + const AVClass* child_class = nullptr; + while ((child_class = av_opt_child_class_next(av_class, child_class))) { + GetOptionListSingle(out, child_class); + } +} + +std::vector GetOptionList(const AVClass* av_class) { + std::vector out; + GetOptionList(out, av_class); + + // Filter out identical options (why do they exist in the first place?) + std::unordered_set option_name_set; + std::vector final_out; + for (auto& option : out) { + if (option_name_set.count(option.name)) { + continue; + } + option_name_set.emplace(option.name); + final_out.emplace_back(std::move(option)); + } + + return final_out; +} + +std::vector ListEncoders(AVMediaType type) { + InitializeFFmpegLibraries(); + + const auto general_options = GetOptionList(avcodec_get_class()); + + std::vector out; + + const AVCodec* current = nullptr; +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) + while ((current = av_codec_next(current))) { +#else + void* data = nullptr; // For libavcodec to save the iteration state + while ((current = av_codec_iterate(&data))) { +#endif + if (!av_codec_is_encoder(current) || current->type != type) { + continue; + } + auto options = GetOptionList(current->priv_class); + options.insert(options.end(), general_options.begin(), general_options.end()); + out.push_back( + {current->name, ToStdString(current->long_name), current->id, std::move(options)}); + } + return out; +} + +std::vector ListFormats() { + InitializeFFmpegLibraries(); + + const auto general_options = GetOptionList(avformat_get_class()); + + std::vector out; + + const AVOutputFormat* current = nullptr; +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) + while ((current = av_oformat_next(current))) { +#else + void* data = nullptr; // For libavformat to save the iteration state + while ((current = av_muxer_iterate(&data))) { +#endif + auto options = GetOptionList(current->priv_class); + options.insert(options.end(), general_options.begin(), general_options.end()); + + std::vector extensions; + Common::SplitString(ToStdString(current->extensions), ',', extensions); + + std::set supported_video_codecs; + std::set supported_audio_codecs; + // Go through all codecs + const AVCodecDescriptor* codec = nullptr; + while ((codec = avcodec_descriptor_next(codec))) { + if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) { + if (codec->type == AVMEDIA_TYPE_VIDEO) { + supported_video_codecs.emplace(codec->id); + } else if (codec->type == AVMEDIA_TYPE_AUDIO) { + supported_audio_codecs.emplace(codec->id); + } + } + } + + if (supported_video_codecs.empty() || supported_audio_codecs.empty()) { + continue; + } + + out.push_back({current->name, ToStdString(current->long_name), std::move(extensions), + std::move(supported_video_codecs), std::move(supported_audio_codecs), + std::move(options)}); + } + return out; +} + } // namespace VideoDumper diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index f0962189e..e2c605c8c 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include "common/common_types.h" @@ -19,6 +20,7 @@ extern "C" { #include #include +#include #include #include } @@ -188,4 +190,43 @@ private: Common::Event processing_ended; }; +/// Struct describing encoder/muxer options +struct OptionInfo { + std::string name; + std::string description; + AVOptionType type; + std::string default_value; + struct NamedConstant { + std::string name; + std::string description; + s64 value; + }; + std::vector named_constants; + + // If this is a scalar type + double min; + double max; +}; + +/// Struct describing an encoder +struct EncoderInfo { + std::string name; + std::string long_name; + AVCodecID codec; + std::vector options; +}; + +/// Struct describing a format +struct FormatInfo { + std::string name; + std::string long_name; + std::vector extensions; + std::set supported_video_codecs; + std::set supported_audio_codecs; + std::vector options; +}; + +std::vector ListEncoders(AVMediaType type); +std::vector ListFormats(); + } // namespace VideoDumper From 94bc09d3ae244012b447ed1776ea10a2f7b1f75e Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 12:34:20 +0800 Subject: [PATCH 09/21] citra_qt/dumping: Add option set dialog This dialog allows changing the value and unsetting one option. There are three possible variants of this dialog: 1. The LineEdit layout. This is used for normal options like string and duration, and just features a textbox for the user to type in whatever they want to set. 2. The ComboBox layout. This is used when there are named constants for an option, or when the option accepts an enum value like sample_format or pixel_format. A description will be displayed for the currently selected named constant. The user can also select 'custom' and type in their own value. 3. The CheckBox-es layout. This is used for flags options. A checkbox will be displayed for each named constant and the user can tick the flags they want to set. --- src/citra_qt/dumping/option_set_dialog.cpp | 299 +++++++++++++++++++++ src/citra_qt/dumping/option_set_dialog.h | 33 +++ src/citra_qt/dumping/option_set_dialog.ui | 89 ++++++ 3 files changed, 421 insertions(+) create mode 100644 src/citra_qt/dumping/option_set_dialog.cpp create mode 100644 src/citra_qt/dumping/option_set_dialog.h create mode 100644 src/citra_qt/dumping/option_set_dialog.ui diff --git a/src/citra_qt/dumping/option_set_dialog.cpp b/src/citra_qt/dumping/option_set_dialog.cpp new file mode 100644 index 000000000..8dab0505e --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.cpp @@ -0,0 +1,299 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/dumping/option_set_dialog.h" +#include "common/logging/log.h" +#include "common/string_util.h" +#include "ui_option_set_dialog.h" + +extern "C" { +#include +} + +static const std::unordered_map TypeNameMap{{ + {AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")}, + {AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")}, + {AV_OPT_TYPE_DURATION, QT_TR_NOOP("duration")}, + {AV_OPT_TYPE_INT, QT_TR_NOOP("int")}, + {AV_OPT_TYPE_UINT64, QT_TR_NOOP("uint64")}, + {AV_OPT_TYPE_INT64, QT_TR_NOOP("int64")}, + {AV_OPT_TYPE_DOUBLE, QT_TR_NOOP("double")}, + {AV_OPT_TYPE_FLOAT, QT_TR_NOOP("float")}, + {AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("rational")}, + {AV_OPT_TYPE_PIXEL_FMT, QT_TR_NOOP("pixel format")}, + {AV_OPT_TYPE_SAMPLE_FMT, QT_TR_NOOP("sample format")}, + {AV_OPT_TYPE_COLOR, QT_TR_NOOP("color")}, + {AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("image size")}, + {AV_OPT_TYPE_STRING, QT_TR_NOOP("string")}, + {AV_OPT_TYPE_DICT, QT_TR_NOOP("dictionary")}, + {AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("video rate")}, + {AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("channel layout")}, +}}; + +static const std::unordered_map TypeDescriptionMap{{ + {AV_OPT_TYPE_DURATION, QT_TR_NOOP("[<hours (integer)>:][<minutes (integer):]<seconds " + "(decimal)> e.g. 03:00.5 (3min 500ms)")}, + {AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("<num>/<den>")}, + {AV_OPT_TYPE_COLOR, QT_TR_NOOP("0xRRGGBBAA")}, + {AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("<width>x<height>, or preset values like 'vga'.")}, + {AV_OPT_TYPE_DICT, + QT_TR_NOOP("Comma-splitted list of <key>=<value>. Do not put spaces.")}, + {AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("<num>/<den>, or preset values like 'pal'.")}, + {AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("Hexadecimal channel layout mask starting with '0x'.")}, +}}; + +/// Get the preset values of an option. returns {display value, real value} +std::vector> GetPresetValues(const VideoDumper::OptionInfo& option) { + switch (option.type) { + case AV_OPT_TYPE_BOOL: { + return {{QObject::tr("auto"), QStringLiteral("auto")}, + {QObject::tr("true"), QStringLiteral("true")}, + {QObject::tr("false"), QStringLiteral("false")}}; + } + case AV_OPT_TYPE_PIXEL_FMT: { + std::vector> out{{QObject::tr("none"), QStringLiteral("none")}}; + // List all pixel formats + const AVPixFmtDescriptor* current = nullptr; + while ((current = av_pix_fmt_desc_next(current))) { + out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name)); + } + return out; + } + case AV_OPT_TYPE_SAMPLE_FMT: { + std::vector> out{{QObject::tr("none"), QStringLiteral("none")}}; + // List all sample formats + int current = 0; + while (true) { + const char* name = av_get_sample_fmt_name(static_cast(current)); + if (name == nullptr) + break; + out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name)); + } + return out; + } + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_INT64: + case AV_OPT_TYPE_UINT64: { + std::vector> out; + // Add in all named constants + for (const auto& constant : option.named_constants) { + out.emplace_back(QObject::tr("%1 (0x%2)") + .arg(QString::fromStdString(constant.name)) + .arg(constant.value, 0, 16), + QString::fromStdString(constant.name)); + } + return out; + } + default: + return {}; + } +} + +void OptionSetDialog::InitializeUI(const std::string& initial_value) { + const QString type_name = + TypeNameMap.count(option.type) ? tr(TypeNameMap.at(option.type)) : tr("unknown"); + ui->nameLabel->setText(tr("%1 <%2> %3") + .arg(QString::fromStdString(option.name), type_name, + QString::fromStdString(option.description))); + if (TypeDescriptionMap.count(option.type)) { + ui->formatLabel->setVisible(true); + ui->formatLabel->setText(tr(TypeDescriptionMap.at(option.type))); + } + + if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || + option.type == AV_OPT_TYPE_UINT64 || option.type == AV_OPT_TYPE_FLOAT || + option.type == AV_OPT_TYPE_DOUBLE || option.type == AV_OPT_TYPE_DURATION || + option.type == AV_OPT_TYPE_RATIONAL) { // scalar types + + ui->formatLabel->setVisible(true); + if (!ui->formatLabel->text().isEmpty()) { + ui->formatLabel->text().append(QStringLiteral("\n")); + } + ui->formatLabel->setText( + ui->formatLabel->text().append(tr("Range: %1 - %2").arg(option.min).arg(option.max))); + } + + // Decide and initialize layout + if (option.type == AV_OPT_TYPE_BOOL || option.type == AV_OPT_TYPE_PIXEL_FMT || + option.type == AV_OPT_TYPE_SAMPLE_FMT || + ((option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || + option.type == AV_OPT_TYPE_UINT64) && + !option.named_constants.empty())) { // Use the combobox layout + + layout_type = 1; + ui->comboBox->setVisible(true); + ui->comboBoxHelpLabel->setVisible(true); + + QString real_initial_value = QString::fromStdString(initial_value); + if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || + option.type == AV_OPT_TYPE_UINT64) { + + // Get the name of the initial value + try { + s64 initial_value_integer = std::stoll(initial_value, nullptr, 0); + for (const auto& constant : option.named_constants) { + if (constant.value == initial_value_integer) { + real_initial_value = QString::fromStdString(constant.name); + break; + } + } + } catch (...) { + // Not convertible to integer, ignore + } + } + + bool found = false; + for (const auto& [display, value] : GetPresetValues(option)) { + ui->comboBox->addItem(display, value); + if (value == real_initial_value) { + found = true; + ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1); + } + } + ui->comboBox->addItem(tr("custom")); + + if (!found) { + ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1); + ui->lineEdit->setText(QString::fromStdString(initial_value)); + } + + UpdateUIDisplay(); + + connect(ui->comboBox, &QComboBox::currentTextChanged, this, + &OptionSetDialog::UpdateUIDisplay); + } else if (option.type == AV_OPT_TYPE_FLAGS && + !option.named_constants.empty()) { // Use the check boxes layout + + layout_type = 2; + + for (const auto& constant : option.named_constants) { + auto* checkBox = new QCheckBox(tr("%1 (0x%2) %3") + .arg(QString::fromStdString(constant.name)) + .arg(constant.value, 0, 16) + .arg(QString::fromStdString(constant.description))); + checkBox->setProperty("value", static_cast(constant.value)); + checkBox->setProperty("name", QString::fromStdString(constant.name)); + ui->checkBoxLayout->addWidget(checkBox); + } + SetCheckBoxDefaults(initial_value); + } else { // Use the line edit layout + layout_type = 0; + ui->lineEdit->setVisible(true); + ui->lineEdit->setText(QString::fromStdString(initial_value)); + } + + adjustSize(); +} + +void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) { + if (initial_value.size() >= 2 && + (initial_value.substr(0, 2) == "0x" || initial_value.substr(0, 2) == "0X")) { + // This is a hex mask + try { + u64 value = std::stoull(initial_value, nullptr, 16); + for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { + auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); + if (checkBox) { + checkBox->setChecked(value & checkBox->property("value").toULongLong()); + } + } + } catch (...) { + LOG_ERROR(Frontend, "Could not convert {} to number", initial_value); + } + } else { + // This is a combination of constants, splitted with + or | + std::vector tmp; + Common::SplitString(initial_value, '+', tmp); + + std::vector out; + std::vector tmp2; + for (const auto& str : tmp) { + Common::SplitString(str, '|', tmp2); + out.insert(out.end(), tmp2.begin(), tmp2.end()); + } + for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { + auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); + if (checkBox) { + checkBox->setChecked( + std::find(out.begin(), out.end(), + checkBox->property("name").toString().toStdString()) != out.end()); + } + } + } +} + +void OptionSetDialog::UpdateUIDisplay() { + if (layout_type != 1) + return; + + if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { // custom + ui->comboBoxHelpLabel->setVisible(false); + ui->lineEdit->setVisible(true); + adjustSize(); + return; + } + + ui->lineEdit->setVisible(false); + for (const auto& constant : option.named_constants) { + if (constant.name == ui->comboBox->currentData().toString().toStdString()) { + ui->comboBoxHelpLabel->setVisible(true); + ui->comboBoxHelpLabel->setText(QString::fromStdString(constant.description)); + return; + } + } +} + +std::pair OptionSetDialog::GetCurrentValue() { + if (!is_set) { + return {}; + } + + switch (layout_type) { + case 0: // line edit layout + return {true, ui->lineEdit->text().toStdString()}; + case 1: // combo box layout + if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { + return {true, ui->lineEdit->text().toStdString()}; // custom + } + return {true, ui->comboBox->currentData().toString().toStdString()}; + case 2: { // check boxes layout + std::string out; + for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { + auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); + if (checkBox && checkBox->isChecked()) { + if (!out.empty()) { + out.append("+"); + } + out.append(checkBox->property("name").toString().toStdString()); + } + } + if (out.empty()) { + out = "0x0"; + } + return {true, out}; + } + default: + return {}; + } +} + +OptionSetDialog::OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option_, + const std::string& initial_value) + : QDialog(parent), ui(std::make_unique()), option(std::move(option_)) { + + ui->setupUi(this); + InitializeUI(initial_value); + + connect(ui->unsetButton, &QPushButton::clicked, [this] { + is_set = false; + accept(); + }); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionSetDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionSetDialog::reject); +} + +OptionSetDialog::~OptionSetDialog() = default; diff --git a/src/citra_qt/dumping/option_set_dialog.h b/src/citra_qt/dumping/option_set_dialog.h new file mode 100644 index 000000000..2c5d378d0 --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.h @@ -0,0 +1,33 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "core/dumping/ffmpeg_backend.h" + +namespace Ui { +class OptionSetDialog; +} + +class OptionSetDialog : public QDialog { + Q_OBJECT + +public: + explicit OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option, + const std::string& initial_value); + ~OptionSetDialog() override; + + // {is_set, value} + std::pair GetCurrentValue(); + +private: + void InitializeUI(const std::string& initial_value); + void SetCheckBoxDefaults(const std::string& initial_value); + void UpdateUIDisplay(); + + std::unique_ptr ui; + VideoDumper::OptionInfo option; + bool is_set = true; + int layout_type = -1; // 0 - line edit, 1 - combo box, 2 - flags (check boxes) +}; diff --git a/src/citra_qt/dumping/option_set_dialog.ui b/src/citra_qt/dumping/option_set_dialog.ui new file mode 100644 index 000000000..dcf4bb572 --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.ui @@ -0,0 +1,89 @@ + + + OptionSetDialog + + + + 0 + 0 + 600 + 150 + + + + Options + + + + + + + + + false + + + + + + + + + false + + + + + + + false + + + + + + + + + false + + + + + + + + + + Qt::Vertical + + + + + + + + + Unset + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + From e769d90aa8fbf73fff7a710f0e2cf8c494115d24 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 12:35:00 +0800 Subject: [PATCH 10/21] citra_qt/dumping: Add options dialog This is a simple list of name-value pairs of options. Users can double-click on an option to set or modify its value. --- src/citra_qt/dumping/options_dialog.cpp | 59 +++++++++++++++++++++++++ src/citra_qt/dumping/options_dialog.h | 32 ++++++++++++++ src/citra_qt/dumping/options_dialog.ui | 50 +++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/citra_qt/dumping/options_dialog.cpp create mode 100644 src/citra_qt/dumping/options_dialog.h create mode 100644 src/citra_qt/dumping/options_dialog.ui diff --git a/src/citra_qt/dumping/options_dialog.cpp b/src/citra_qt/dumping/options_dialog.cpp new file mode 100644 index 000000000..419272dd1 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.cpp @@ -0,0 +1,59 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "citra_qt/dumping/option_set_dialog.h" +#include "citra_qt/dumping/options_dialog.h" +#include "ui_options_dialog.h" + +constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]"); + +void OptionsDialog::PopulateOptions(const std::string& current_value) { + for (std::size_t i = 0; i < options.size(); ++i) { + const auto& option = options.at(i); + auto* item = new QTreeWidgetItem( + {QString::fromStdString(option.name), QString::fromStdString(current_values.Get( + option.name, tr(UNSET_TEXT).toStdString()))}); + item->setData(1, Qt::UserRole, static_cast(i)); // ID + ui->main->addTopLevelItem(item); + } +} + +void OptionsDialog::OnSetOptionValue(int id) { + OptionSetDialog dialog(this, options[id], + current_values.Get(options[id].name, options[id].default_value)); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + return; + } + + const auto& [is_set, value] = dialog.GetCurrentValue(); + if (is_set) { + current_values.Set(options[id].name, value); + } else { + current_values.Erase(options[id].name); + } + ui->main->invisibleRootItem()->child(id)->setText(1, is_set ? QString::fromStdString(value) + : tr(UNSET_TEXT)); +} + +std::string OptionsDialog::GetCurrentValue() const { + return current_values.Serialize(); +} + +OptionsDialog::OptionsDialog(QWidget* parent, std::vector options_, + const std::string& current_value) + : QDialog(parent), ui(std::make_unique()), options(std::move(options_)), + current_values(current_value) { + + ui->setupUi(this); + PopulateOptions(current_value); + + connect(ui->main, &QTreeWidget::itemDoubleClicked, [this](QTreeWidgetItem* item, int column) { + OnSetOptionValue(item->data(1, Qt::UserRole).toInt()); + }); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionsDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionsDialog::reject); +} + +OptionsDialog::~OptionsDialog() = default; diff --git a/src/citra_qt/dumping/options_dialog.h b/src/citra_qt/dumping/options_dialog.h new file mode 100644 index 000000000..9c6f92112 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.h @@ -0,0 +1,32 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/param_package.h" +#include "core/dumping/ffmpeg_backend.h" + +namespace Ui { +class OptionsDialog; +} + +class OptionsDialog : public QDialog { + Q_OBJECT + +public: + explicit OptionsDialog(QWidget* parent, std::vector options, + const std::string& current_value); + ~OptionsDialog() override; + + std::string GetCurrentValue() const; + +private: + void PopulateOptions(const std::string& current_value); + void OnSetOptionValue(int id); + + std::unique_ptr ui; + std::vector options; + Common::ParamPackage current_values; +}; diff --git a/src/citra_qt/dumping/options_dialog.ui b/src/citra_qt/dumping/options_dialog.ui new file mode 100644 index 000000000..21c7c6676 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.ui @@ -0,0 +1,50 @@ + + + OptionsDialog + + + + 0 + 0 + 600 + 300 + + + + Options + + + + + + true + + + Double click to see the description and change the values of the options. + + + + + + + + Name + + + + + Value + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + From f82ba41fe0ccbfbb4db3cbfdbde54453b42bbf4d Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 12:38:13 +0800 Subject: [PATCH 11/21] citra_qt/dumping: Add dumping dialog This is the main dialog of video dumping. This dialog allows the user to set output format, output path, video/audio encoder and video/audio bitrate. When a format is selected, the list of video and audio encoders are updated. Only encoders of codecs that can be contained in the format is shown. --- src/citra_qt/dumping/dumping_dialog.cpp | 210 ++++++++++++++++++++++++ src/citra_qt/dumping/dumping_dialog.h | 41 +++++ src/citra_qt/dumping/dumping_dialog.ui | 183 +++++++++++++++++++++ 3 files changed, 434 insertions(+) create mode 100644 src/citra_qt/dumping/dumping_dialog.cpp create mode 100644 src/citra_qt/dumping/dumping_dialog.h create mode 100644 src/citra_qt/dumping/dumping_dialog.ui diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/citra_qt/dumping/dumping_dialog.cpp new file mode 100644 index 000000000..ee5fe6b2a --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.cpp @@ -0,0 +1,210 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/dumping/dumping_dialog.h" +#include "citra_qt/dumping/options_dialog.h" +#include "citra_qt/uisettings.h" +#include "core/settings.h" +#include "ui_dumping_dialog.h" + +DumpingDialog::DumpingDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + + ui->setupUi(this); + + connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked); + connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { + ApplyConfiguration(); + accept(); + }); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject); + connect(ui->formatOptionsButton, &QPushButton::clicked, [this] { + OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options, + format_options); + }); + connect(ui->videoEncoderOptionsButton, &QPushButton::clicked, [this] { + OpenOptionsDialog( + video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options, + video_encoder_options); + }); + connect(ui->audioEncoderOptionsButton, &QPushButton::clicked, [this] { + OpenOptionsDialog( + audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options, + audio_encoder_options); + }); + + SetConfiguration(); + + connect(ui->formatComboBox, qOverload(&QComboBox::currentIndexChanged), [this] { + ui->pathLineEdit->setText(QString{}); + format_options.clear(); + PopulateEncoders(); + }); + + connect(ui->videoEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), + [this] { video_encoder_options.clear(); }); + connect(ui->audioEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), + [this] { audio_encoder_options.clear(); }); +} + +DumpingDialog::~DumpingDialog() = default; + +QString DumpingDialog::GetFilePath() const { + return ui->pathLineEdit->text(); +} + +void DumpingDialog::Populate() { + formats = VideoDumper::ListFormats(); + video_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_VIDEO); + audio_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_AUDIO); + + // Check that these are not empty + QString missing; + if (formats.empty()) { + missing = tr("output formats"); + } + if (video_encoders.empty()) { + missing = tr("video encoders"); + } + if (audio_encoders.empty()) { + missing = tr("audio encoders"); + } + + if (!missing.isEmpty()) { + QMessageBox::critical(this, tr("Citra"), + tr("Could not find any available %1.\nPlease check your FFmpeg " + "installation used for compilation.") + .arg(missing)); + reject(); + return; + } + + // Populate formats + for (std::size_t i = 0; i < formats.size(); ++i) { + const auto& format = formats[i]; + + // Check format: only formats that have video encoders and audio encoders are displayed + bool has_video = false; + for (const auto& video_encoder : video_encoders) { + if (format.supported_video_codecs.count(video_encoder.codec)) { + has_video = true; + break; + } + } + if (!has_video) + continue; + + bool has_audio = false; + for (const auto& audio_encoder : audio_encoders) { + if (format.supported_audio_codecs.count(audio_encoder.codec)) { + has_audio = true; + break; + } + } + if (!has_audio) + continue; + + ui->formatComboBox->addItem(tr("%1 (%2)").arg(QString::fromStdString(format.long_name), + QString::fromStdString(format.name)), + static_cast(i)); + if (format.name == Settings::values.output_format) { + ui->formatComboBox->setCurrentIndex(ui->formatComboBox->count() - 1); + } + } + PopulateEncoders(); +} + +void DumpingDialog::PopulateEncoders() { + const auto& format = formats.at(ui->formatComboBox->currentData().toUInt()); + + ui->videoEncoderComboBox->clear(); + for (std::size_t i = 0; i < video_encoders.size(); ++i) { + const auto& video_encoder = video_encoders[i]; + if (!format.supported_video_codecs.count(video_encoder.codec)) { + continue; + } + + ui->videoEncoderComboBox->addItem( + tr("%1 (%2)").arg(QString::fromStdString(video_encoder.long_name), + QString::fromStdString(video_encoder.name)), + static_cast(i)); + if (video_encoder.name == Settings::values.video_encoder) { + ui->videoEncoderComboBox->setCurrentIndex(ui->videoEncoderComboBox->count() - 1); + } + } + + ui->audioEncoderComboBox->clear(); + for (std::size_t i = 0; i < audio_encoders.size(); ++i) { + const auto& audio_encoder = audio_encoders[i]; + if (!format.supported_audio_codecs.count(audio_encoder.codec)) { + continue; + } + + ui->audioEncoderComboBox->addItem( + tr("%1 (%2)").arg(QString::fromStdString(audio_encoder.long_name), + QString::fromStdString(audio_encoder.name)), + static_cast(i)); + if (audio_encoder.name == Settings::values.audio_encoder) { + ui->audioEncoderComboBox->setCurrentIndex(ui->audioEncoderComboBox->count() - 1); + } + } +} + +void DumpingDialog::OnToolButtonClicked() { + const auto& format = formats.at(ui->formatComboBox->currentData().toUInt()); + + QString extensions; + for (const auto& ext : format.extensions) { + if (!extensions.isEmpty()) { + extensions.append(QLatin1Char{' '}); + } + extensions.append(QStringLiteral("*.%1").arg(QString::fromStdString(ext))); + } + + const auto path = QFileDialog::getSaveFileName( + this, tr("Select Video Output Path"), last_path, + tr("%1 (%2)").arg(QString::fromStdString(format.long_name), extensions)); + if (!path.isEmpty()) { + last_path = QFileInfo(ui->pathLineEdit->text()).path(); + ui->pathLineEdit->setText(path); + } +} + +void DumpingDialog::OpenOptionsDialog(const std::vector& options, + std::string& current_value) { + OptionsDialog dialog(this, options, current_value); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + return; + } + + current_value = dialog.GetCurrentValue(); +} + +void DumpingDialog::SetConfiguration() { + Populate(); + + format_options = Settings::values.format_options; + video_encoder_options = Settings::values.video_encoder_options; + audio_encoder_options = Settings::values.audio_encoder_options; + last_path = UISettings::values.video_dumping_path; + ui->videoBitrateSpinBox->setValue(static_cast(Settings::values.video_bitrate)); + ui->audioBitrateSpinBox->setValue(static_cast(Settings::values.audio_bitrate)); +} + +void DumpingDialog::ApplyConfiguration() { + Settings::values.output_format = formats.at(ui->formatComboBox->currentData().toUInt()).name; + Settings::values.format_options = format_options; + Settings::values.video_encoder = + video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).name; + Settings::values.video_encoder_options = video_encoder_options; + Settings::values.video_bitrate = ui->videoBitrateSpinBox->value(); + Settings::values.audio_encoder = + audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).name; + Settings::values.audio_encoder_options = audio_encoder_options; + Settings::values.audio_bitrate = ui->audioBitrateSpinBox->value(); + UISettings::values.video_dumping_path = last_path; + Settings::Apply(); +} diff --git a/src/citra_qt/dumping/dumping_dialog.h b/src/citra_qt/dumping/dumping_dialog.h new file mode 100644 index 000000000..7476770ba --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.h @@ -0,0 +1,41 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "core/dumping/ffmpeg_backend.h" + +namespace Ui { +class DumpingDialog; +} + +class DumpingDialog : public QDialog { + Q_OBJECT + +public: + explicit DumpingDialog(QWidget* parent); + ~DumpingDialog() override; + + QString GetFilePath() const; + void ApplyConfiguration(); + +private: + void Populate(); + void PopulateEncoders(); + void SetConfiguration(); + void OnToolButtonClicked(); + void OpenOptionsDialog(const std::vector& options, + std::string& current_value); + + std::unique_ptr ui; + std::string format_options; + std::string video_encoder_options; + std::string audio_encoder_options; + + QString last_path; + + std::vector formats; + std::vector video_encoders; + std::vector audio_encoders; +}; diff --git a/src/citra_qt/dumping/dumping_dialog.ui b/src/citra_qt/dumping/dumping_dialog.ui new file mode 100644 index 000000000..a3468ed3f --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.ui @@ -0,0 +1,183 @@ + + + DumpingDialog + + + + 0 + 0 + 600 + 360 + + + + Dump Video + + + + + + Output + + + + + + Format: + + + + + + + + + + Options... + + + + + + + Path: + + + + + + + + + + ... + + + + + + + + + + Video + + + + + + Encoder: + + + + + + + + 0 + 0 + + + + + + + + Options... + + + + + + + Bitrate: + + + + + + + 10000000 + + + 1000 + + + + + + + bps + + + + + + + + + + Audio + + + + + + Encoder: + + + + + + + + 0 + 0 + + + + + + + + Options... + + + + + + + Bitrate: + + + + + + + 1000000 + + + 100 + + + + + + + bps + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + From 71c64c261719ccf2a0255b734b442e9cc8fbe642 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 12:38:46 +0800 Subject: [PATCH 12/21] citra_qt: Use the new dumping dialog Note that it is only compiled in for FFmpeg video dumper enabled builds --- src/citra_qt/CMakeLists.txt | 14 ++++++++++++++ src/citra_qt/main.cpp | 23 +++++++++++++++-------- src/citra_qt/main.h | 2 ++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 7ca0398fa..836e3c8ae 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -162,6 +162,20 @@ add_executable(citra-qt util/util.h ) +if (ENABLE_FFMPEG_VIDEO_DUMPER) + target_sources(citra-qt PRIVATE + dumping/dumping_dialog.cpp + dumping/dumping_dialog.h + dumping/dumping_dialog.ui + dumping/option_set_dialog.cpp + dumping/option_set_dialog.h + dumping/option_set_dialog.ui + dumping/options_dialog.cpp + dumping/options_dialog.h + dumping/options_dialog.ui + ) +endif() + file(GLOB COMPAT_LIST ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index e106bf170..fe0ef4f35 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -87,6 +87,10 @@ #include "citra_qt/discord_impl.h" #endif +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER +#include "citra_qt/dumping/dumping_dialog.h" +#endif + #ifdef QT_STATICPLUGIN Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); #endif @@ -679,9 +683,7 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Capture_Screenshot, &QAction::triggered, this, &GMainWindow::OnCaptureScreenshot); -#ifndef ENABLE_FFMPEG_VIDEO_DUMPER - ui.action_Dump_Video->setEnabled(false); -#endif +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER connect(ui.action_Dump_Video, &QAction::triggered, [this] { if (ui.action_Dump_Video->isChecked()) { OnStartVideoDumping(); @@ -689,6 +691,9 @@ void GMainWindow::ConnectMenuEvents() { OnStopVideoDumping(); } }); +#else + ui.action_Dump_Video->setEnabled(false); +#endif // Help connect(ui.action_Open_Citra_Folder, &QAction::triggered, this, @@ -992,11 +997,13 @@ void GMainWindow::ShutdownGame() { HideFullscreen(); } +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER if (Core::System::GetInstance().VideoDumper().IsDumping()) { game_shutdown_delayed = true; OnStopVideoDumping(); return; } +#endif AllowOSSleep(); @@ -1804,14 +1811,13 @@ void GMainWindow::OnCaptureScreenshot() { OnStartGame(); } +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER void GMainWindow::OnStartVideoDumping() { - const QString path = QFileDialog::getSaveFileName( - this, tr("Save Video"), UISettings::values.video_dumping_path, tr("WebM Videos (*.webm)")); - if (path.isEmpty()) { - ui.action_Dump_Video->setChecked(false); + DumpingDialog dialog(this); + if (dialog.exec() != QDialog::DialogCode::Accepted) { return; } - UISettings::values.video_dumping_path = QFileInfo(path).path(); + const auto path = dialog.GetFilePath(); if (emulation_running) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; @@ -1848,6 +1854,7 @@ void GMainWindow::OnStopVideoDumping() { future_watcher->setFuture(future); } } +#endif void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 75a1bbb3d..995983e5a 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -200,8 +200,10 @@ private slots: void OnPlayMovie(); void OnStopRecordingPlayback(); void OnCaptureScreenshot(); +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER void OnStartVideoDumping(); void OnStopVideoDumping(); +#endif void OnCoreError(Core::System::ResultStatus, std::string); /// Called whenever a user selects Help->About Citra void OnMenuAboutCitra(); From 0a4be71913883ec835d6af8d753e0101fc6f5fc7 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Feb 2020 22:17:08 +0800 Subject: [PATCH 13/21] citra_qt: Add simple video dumping error reporting This is just a simple message that tells the user to refer to the log --- src/citra_qt/main.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index fe0ef4f35..323e638a4 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -980,8 +980,14 @@ void GMainWindow::BootGame(const QString& filename) { if (video_dumping_on_start) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(), - layout); + if (!Core::System::GetInstance().VideoDumper().StartDumping( + video_dumping_path.toStdString(), layout)) { + + QMessageBox::critical( + this, tr("Citra"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui.action_Dump_Video->setChecked(false); + } video_dumping_on_start = false; video_dumping_path.clear(); } @@ -1821,7 +1827,12 @@ void GMainWindow::OnStartVideoDumping() { if (emulation_running) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout); + if (!Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout)) { + QMessageBox::critical( + this, tr("Citra"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui.action_Dump_Video->setChecked(false); + } } else { video_dumping_on_start = true; video_dumping_path = path; From c38202bd3067772625b735a2cc5a42a6e2ece13f Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 22 Feb 2020 11:18:55 +0800 Subject: [PATCH 14/21] dumping_dialog: Add a line edit for the options So that users can just paste a set of parameters they found elsewhere. --- src/citra_qt/dumping/dumping_dialog.cpp | 38 +++++++------- src/citra_qt/dumping/dumping_dialog.h | 7 ++- src/citra_qt/dumping/dumping_dialog.ui | 68 ++++++++++++++++++------- 3 files changed, 72 insertions(+), 41 deletions(-) diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/citra_qt/dumping/dumping_dialog.cpp index ee5fe6b2a..0c39f86e7 100644 --- a/src/citra_qt/dumping/dumping_dialog.cpp +++ b/src/citra_qt/dumping/dumping_dialog.cpp @@ -21,33 +21,33 @@ DumpingDialog::DumpingDialog(QWidget* parent) accept(); }); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject); - connect(ui->formatOptionsButton, &QPushButton::clicked, [this] { + connect(ui->formatOptionsButton, &QToolButton::clicked, [this] { OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options, - format_options); + ui->formatOptionsLineEdit); }); - connect(ui->videoEncoderOptionsButton, &QPushButton::clicked, [this] { + connect(ui->videoEncoderOptionsButton, &QToolButton::clicked, [this] { OpenOptionsDialog( video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options, - video_encoder_options); + ui->videoEncoderOptionsLineEdit); }); - connect(ui->audioEncoderOptionsButton, &QPushButton::clicked, [this] { + connect(ui->audioEncoderOptionsButton, &QToolButton::clicked, [this] { OpenOptionsDialog( audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options, - audio_encoder_options); + ui->audioEncoderOptionsLineEdit); }); SetConfiguration(); connect(ui->formatComboBox, qOverload(&QComboBox::currentIndexChanged), [this] { ui->pathLineEdit->setText(QString{}); - format_options.clear(); + ui->formatOptionsLineEdit->clear(); PopulateEncoders(); }); connect(ui->videoEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), - [this] { video_encoder_options.clear(); }); + [this] { ui->videoEncoderOptionsLineEdit->clear(); }); connect(ui->audioEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), - [this] { audio_encoder_options.clear(); }); + [this] { ui->audioEncoderOptionsLineEdit->clear(); }); } DumpingDialog::~DumpingDialog() = default; @@ -174,21 +174,23 @@ void DumpingDialog::OnToolButtonClicked() { } void DumpingDialog::OpenOptionsDialog(const std::vector& options, - std::string& current_value) { - OptionsDialog dialog(this, options, current_value); + QLineEdit* line_edit) { + OptionsDialog dialog(this, options, line_edit->text().toStdString()); if (dialog.exec() != QDialog::DialogCode::Accepted) { return; } - current_value = dialog.GetCurrentValue(); + line_edit->setText(QString::fromStdString(dialog.GetCurrentValue())); } void DumpingDialog::SetConfiguration() { Populate(); - format_options = Settings::values.format_options; - video_encoder_options = Settings::values.video_encoder_options; - audio_encoder_options = Settings::values.audio_encoder_options; + ui->formatOptionsLineEdit->setText(QString::fromStdString(Settings::values.format_options)); + ui->videoEncoderOptionsLineEdit->setText( + QString::fromStdString(Settings::values.video_encoder_options)); + ui->audioEncoderOptionsLineEdit->setText( + QString::fromStdString(Settings::values.audio_encoder_options)); last_path = UISettings::values.video_dumping_path; ui->videoBitrateSpinBox->setValue(static_cast(Settings::values.video_bitrate)); ui->audioBitrateSpinBox->setValue(static_cast(Settings::values.audio_bitrate)); @@ -196,14 +198,14 @@ void DumpingDialog::SetConfiguration() { void DumpingDialog::ApplyConfiguration() { Settings::values.output_format = formats.at(ui->formatComboBox->currentData().toUInt()).name; - Settings::values.format_options = format_options; + Settings::values.format_options = ui->formatOptionsLineEdit->text().toStdString(); Settings::values.video_encoder = video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).name; - Settings::values.video_encoder_options = video_encoder_options; + Settings::values.video_encoder_options = ui->videoEncoderOptionsLineEdit->text().toStdString(); Settings::values.video_bitrate = ui->videoBitrateSpinBox->value(); Settings::values.audio_encoder = audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).name; - Settings::values.audio_encoder_options = audio_encoder_options; + Settings::values.audio_encoder_options = ui->audioEncoderOptionsLineEdit->text().toStdString(); Settings::values.audio_bitrate = ui->audioBitrateSpinBox->value(); UISettings::values.video_dumping_path = last_path; Settings::Apply(); diff --git a/src/citra_qt/dumping/dumping_dialog.h b/src/citra_qt/dumping/dumping_dialog.h index 7476770ba..c2b0999ea 100644 --- a/src/citra_qt/dumping/dumping_dialog.h +++ b/src/citra_qt/dumping/dumping_dialog.h @@ -10,6 +10,8 @@ namespace Ui { class DumpingDialog; } +class QLineEdit; + class DumpingDialog : public QDialog { Q_OBJECT @@ -26,12 +28,9 @@ private: void SetConfiguration(); void OnToolButtonClicked(); void OpenOptionsDialog(const std::vector& options, - std::string& current_value); + QLineEdit* line_edit); std::unique_ptr ui; - std::string format_options; - std::string video_encoder_options; - std::string audio_encoder_options; QString last_path; diff --git a/src/citra_qt/dumping/dumping_dialog.ui b/src/citra_qt/dumping/dumping_dialog.ui index a3468ed3f..6e33d47d8 100644 --- a/src/citra_qt/dumping/dumping_dialog.ui +++ b/src/citra_qt/dumping/dumping_dialog.ui @@ -7,7 +7,7 @@ 0 0 600 - 360 + 420 @@ -30,24 +30,34 @@ - - + + - Options... + Options: - + + + + + + + ... + + + + Path: - + - + ... @@ -80,21 +90,31 @@ - - + + - Options... + Options: - + + + + + + + ... + + + + Bitrate: - + 10000000 @@ -104,7 +124,7 @@ - + bps @@ -137,21 +157,31 @@ - - + + - Options... + Options: - + + + + + + + ... + + + + Bitrate: - + 1000000 @@ -161,7 +191,7 @@ - + bps From 8868d4db8654601ef917dae05d0916a1cc2713b1 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 23 Feb 2020 12:25:48 +0800 Subject: [PATCH 15/21] citra_qt: Fixed a bug when dumping dialog is cancelled or closed --- src/citra_qt/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 323e638a4..e225bb5a1 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1821,6 +1821,7 @@ void GMainWindow::OnCaptureScreenshot() { void GMainWindow::OnStartVideoDumping() { DumpingDialog dialog(this); if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui.action_Dump_Video->setChecked(false); return; } const auto path = dialog.GetFilePath(); From c9c26955d2191e1122c9073ee1089a6f095fc6f4 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 25 Feb 2020 18:58:46 +0800 Subject: [PATCH 16/21] dumping_dialog: Prompt when path is empty --- src/citra_qt/dumping/dumping_dialog.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/citra_qt/dumping/dumping_dialog.cpp index 0c39f86e7..e728747bc 100644 --- a/src/citra_qt/dumping/dumping_dialog.cpp +++ b/src/citra_qt/dumping/dumping_dialog.cpp @@ -17,6 +17,10 @@ DumpingDialog::DumpingDialog(QWidget* parent) connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked); connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { + if (ui->pathLineEdit->text().isEmpty()) { + QMessageBox::critical(this, tr("Citra"), tr("Please specify the output path.")); + return; + } ApplyConfiguration(); accept(); }); From a28eac08ae1fb3f381361eef5f5875ae57a5e8c7 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Thu, 27 Feb 2020 16:37:06 +0800 Subject: [PATCH 17/21] ffmpeg: Properly handle non-planar formats For non-planar formats, only the first data plane is used. Therefore, they need to be handled differently in certain places. --- src/core/dumping/ffmpeg_backend.cpp | 33 +++++++++++++---------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 68b148bd3..5a81bed78 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -227,20 +227,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { codec_context->codec_type = AVMEDIA_TYPE_AUDIO; codec_context->bit_rate = Settings::values.audio_bitrate; if (codec->sample_fmts) { - codec_context->sample_fmt = AV_SAMPLE_FMT_NONE; - // Use any planar format - const AVSampleFormat* ptr = codec->sample_fmts; - while ((*ptr) != -1) { - if (av_sample_fmt_is_planar((*ptr))) { - codec_context->sample_fmt = (*ptr); - break; - } - ptr++; - } - if (codec_context->sample_fmt == AV_SAMPLE_FMT_NONE) { - LOG_ERROR(Render, "Specified audio encoder does not support any planar format"); - return false; - } + codec_context->sample_fmt = codec->sample_fmts[0]; } else { codec_context->sample_fmt = AV_SAMPLE_FMT_S16P; } @@ -341,8 +328,14 @@ void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0, const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt); std::array src_data = {reinterpret_cast(channel0.data()), reinterpret_cast(channel1.data())}; - std::array dst_data = {resampled_data[0] + sample_size * offset, - resampled_data[1] + sample_size * offset}; + + std::array dst_data; + if (av_sample_fmt_is_planar(codec_context->sample_fmt)) { + dst_data = {resampled_data[0] + sample_size * offset, + resampled_data[1] + sample_size * offset}; + } else { + dst_data = {resampled_data[0] + sample_size * offset * 2}; // 2 channels + } auto resampled_count = swr_convert(swr_context.get(), dst_data.data(), frame_size - offset, src_data.data(), channel0.size()); @@ -360,7 +353,9 @@ void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0, // Prepare frame audio_frame->nb_samples = frame_size; audio_frame->data[0] = resampled_data[0]; - audio_frame->data[1] = resampled_data[1]; + 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++; @@ -383,7 +378,9 @@ void FFmpegAudioStream::Flush() { // Send the last samples audio_frame->nb_samples = offset; audio_frame->data[0] = resampled_data[0]; - audio_frame->data[1] = resampled_data[1]; + 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()); From f3e9780d100d4b975ed3685c9d2a2b23daf56674 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 28 Feb 2020 16:40:13 +0800 Subject: [PATCH 18/21] ffmpeg: Avoid listing child classes for AVFormatContext and AVCodecContext As they actually have every encoder/format as their child classes, this will result in a lot of extra options being added. --- src/core/dumping/ffmpeg_backend.cpp | 32 +++++++++++------------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 5a81bed78..17a2c4cd7 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -728,41 +728,33 @@ void GetOptionListSingle(std::vector& out, const AVClass* av_class) } } -void GetOptionList(std::vector& out, const AVClass* av_class) { +void GetOptionList(std::vector& out, const AVClass* av_class, bool search_children) { if (av_class == nullptr) { return; } GetOptionListSingle(out, av_class); + if (!search_children) { + return; + } + const AVClass* child_class = nullptr; while ((child_class = av_opt_child_class_next(av_class, child_class))) { GetOptionListSingle(out, child_class); } } -std::vector GetOptionList(const AVClass* av_class) { +std::vector GetOptionList(const AVClass* av_class, bool search_children) { std::vector out; - GetOptionList(out, av_class); - - // Filter out identical options (why do they exist in the first place?) - std::unordered_set option_name_set; - std::vector final_out; - for (auto& option : out) { - if (option_name_set.count(option.name)) { - continue; - } - option_name_set.emplace(option.name); - final_out.emplace_back(std::move(option)); - } - - return final_out; + GetOptionList(out, av_class, search_children); + return out; } std::vector ListEncoders(AVMediaType type) { InitializeFFmpegLibraries(); - const auto general_options = GetOptionList(avcodec_get_class()); + const auto general_options = GetOptionList(avcodec_get_class(), false); std::vector out; @@ -776,7 +768,7 @@ std::vector ListEncoders(AVMediaType type) { if (!av_codec_is_encoder(current) || current->type != type) { continue; } - auto options = GetOptionList(current->priv_class); + auto options = GetOptionList(current->priv_class, true); options.insert(options.end(), general_options.begin(), general_options.end()); out.push_back( {current->name, ToStdString(current->long_name), current->id, std::move(options)}); @@ -787,7 +779,7 @@ std::vector ListEncoders(AVMediaType type) { std::vector ListFormats() { InitializeFFmpegLibraries(); - const auto general_options = GetOptionList(avformat_get_class()); + const auto general_options = GetOptionList(avformat_get_class(), false); std::vector out; @@ -798,7 +790,7 @@ std::vector ListFormats() { void* data = nullptr; // For libavformat to save the iteration state while ((current = av_muxer_iterate(&data))) { #endif - auto options = GetOptionList(current->priv_class); + auto options = GetOptionList(current->priv_class, true); options.insert(options.end(), general_options.begin(), general_options.end()); std::vector extensions; From a50ba7192be317af9f372f11b4682331350cf854 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 28 Feb 2020 18:26:20 +0800 Subject: [PATCH 19/21] citra_qt: Split options into 'Specific' and 'Generic' For easier usage. Also made the option list sortable. --- src/citra_qt/dumping/dumping_dialog.cpp | 14 +++++++----- src/citra_qt/dumping/dumping_dialog.h | 5 ++++- src/citra_qt/dumping/options_dialog.cpp | 29 ++++++++++++++++--------- src/citra_qt/dumping/options_dialog.h | 12 ++++++---- src/citra_qt/dumping/options_dialog.ui | 25 +++++++++++++++++++-- src/core/dumping/ffmpeg_backend.cpp | 23 ++++++++++---------- src/core/dumping/ffmpeg_backend.h | 2 ++ 7 files changed, 76 insertions(+), 34 deletions(-) diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/citra_qt/dumping/dumping_dialog.cpp index e728747bc..70d165c01 100644 --- a/src/citra_qt/dumping/dumping_dialog.cpp +++ b/src/citra_qt/dumping/dumping_dialog.cpp @@ -15,6 +15,9 @@ DumpingDialog::DumpingDialog(QWidget* parent) ui->setupUi(this); + format_generic_options = VideoDumper::GetFormatGenericOptions(); + encoder_generic_options = VideoDumper::GetEncoderGenericOptions(); + connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked); connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { if (ui->pathLineEdit->text().isEmpty()) { @@ -27,17 +30,17 @@ DumpingDialog::DumpingDialog(QWidget* parent) connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject); connect(ui->formatOptionsButton, &QToolButton::clicked, [this] { OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options, - ui->formatOptionsLineEdit); + format_generic_options, ui->formatOptionsLineEdit); }); connect(ui->videoEncoderOptionsButton, &QToolButton::clicked, [this] { OpenOptionsDialog( video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options, - ui->videoEncoderOptionsLineEdit); + encoder_generic_options, ui->videoEncoderOptionsLineEdit); }); connect(ui->audioEncoderOptionsButton, &QToolButton::clicked, [this] { OpenOptionsDialog( audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options, - ui->audioEncoderOptionsLineEdit); + encoder_generic_options, ui->audioEncoderOptionsLineEdit); }); SetConfiguration(); @@ -177,9 +180,10 @@ void DumpingDialog::OnToolButtonClicked() { } } -void DumpingDialog::OpenOptionsDialog(const std::vector& options, +void DumpingDialog::OpenOptionsDialog(const std::vector& specific_options, + const std::vector& generic_options, QLineEdit* line_edit) { - OptionsDialog dialog(this, options, line_edit->text().toStdString()); + OptionsDialog dialog(this, specific_options, generic_options, line_edit->text().toStdString()); if (dialog.exec() != QDialog::DialogCode::Accepted) { return; } diff --git a/src/citra_qt/dumping/dumping_dialog.h b/src/citra_qt/dumping/dumping_dialog.h index c2b0999ea..284f215c3 100644 --- a/src/citra_qt/dumping/dumping_dialog.h +++ b/src/citra_qt/dumping/dumping_dialog.h @@ -27,7 +27,8 @@ private: void PopulateEncoders(); void SetConfiguration(); void OnToolButtonClicked(); - void OpenOptionsDialog(const std::vector& options, + void OpenOptionsDialog(const std::vector& specific_options, + const std::vector& generic_options, QLineEdit* line_edit); std::unique_ptr ui; @@ -35,6 +36,8 @@ private: QString last_path; std::vector formats; + std::vector format_generic_options; std::vector video_encoders; std::vector audio_encoders; + std::vector encoder_generic_options; }; diff --git a/src/citra_qt/dumping/options_dialog.cpp b/src/citra_qt/dumping/options_dialog.cpp index 419272dd1..e75fc61c0 100644 --- a/src/citra_qt/dumping/options_dialog.cpp +++ b/src/citra_qt/dumping/options_dialog.cpp @@ -9,7 +9,10 @@ constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]"); -void OptionsDialog::PopulateOptions(const std::string& current_value) { +void OptionsDialog::PopulateOptions() { + const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options; + ui->main->clear(); + ui->main->setSortingEnabled(false); for (std::size_t i = 0; i < options.size(); ++i) { const auto& option = options.at(i); auto* item = new QTreeWidgetItem( @@ -18,9 +21,13 @@ void OptionsDialog::PopulateOptions(const std::string& current_value) { item->setData(1, Qt::UserRole, static_cast(i)); // ID ui->main->addTopLevelItem(item); } + ui->main->setSortingEnabled(true); + ui->main->sortItems(0, Qt::AscendingOrder); } -void OptionsDialog::OnSetOptionValue(int id) { +void OptionsDialog::OnSetOptionValue(QTreeWidgetItem* item) { + const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options; + const int id = item->data(1, Qt::UserRole).toInt(); OptionSetDialog dialog(this, options[id], current_values.Get(options[id].name, options[id].default_value)); if (dialog.exec() != QDialog::DialogCode::Accepted) { @@ -33,27 +40,29 @@ void OptionsDialog::OnSetOptionValue(int id) { } else { current_values.Erase(options[id].name); } - ui->main->invisibleRootItem()->child(id)->setText(1, is_set ? QString::fromStdString(value) - : tr(UNSET_TEXT)); + item->setText(1, is_set ? QString::fromStdString(value) : tr(UNSET_TEXT)); } std::string OptionsDialog::GetCurrentValue() const { return current_values.Serialize(); } -OptionsDialog::OptionsDialog(QWidget* parent, std::vector options_, +OptionsDialog::OptionsDialog(QWidget* parent, + std::vector specific_options_, + std::vector generic_options_, const std::string& current_value) - : QDialog(parent), ui(std::make_unique()), options(std::move(options_)), + : QDialog(parent), ui(std::make_unique()), + specific_options(std::move(specific_options_)), generic_options(std::move(generic_options_)), current_values(current_value) { ui->setupUi(this); - PopulateOptions(current_value); + PopulateOptions(); - connect(ui->main, &QTreeWidget::itemDoubleClicked, [this](QTreeWidgetItem* item, int column) { - OnSetOptionValue(item->data(1, Qt::UserRole).toInt()); - }); + connect(ui->main, &QTreeWidget::itemDoubleClicked, + [this](QTreeWidgetItem* item, int column) { OnSetOptionValue(item); }); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionsDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionsDialog::reject); + connect(ui->specificRadioButton, &QRadioButton::toggled, this, &OptionsDialog::PopulateOptions); } OptionsDialog::~OptionsDialog() = default; diff --git a/src/citra_qt/dumping/options_dialog.h b/src/citra_qt/dumping/options_dialog.h index 9c6f92112..d9a29e2dc 100644 --- a/src/citra_qt/dumping/options_dialog.h +++ b/src/citra_qt/dumping/options_dialog.h @@ -8,6 +8,8 @@ #include "common/param_package.h" #include "core/dumping/ffmpeg_backend.h" +class QTreeWidgetItem; + namespace Ui { class OptionsDialog; } @@ -16,17 +18,19 @@ class OptionsDialog : public QDialog { Q_OBJECT public: - explicit OptionsDialog(QWidget* parent, std::vector options, + explicit OptionsDialog(QWidget* parent, std::vector specific_options, + std::vector generic_options, const std::string& current_value); ~OptionsDialog() override; std::string GetCurrentValue() const; private: - void PopulateOptions(const std::string& current_value); - void OnSetOptionValue(int id); + void PopulateOptions(); + void OnSetOptionValue(QTreeWidgetItem* item); std::unique_ptr ui; - std::vector options; + std::vector specific_options; + std::vector generic_options; Common::ParamPackage current_values; }; diff --git a/src/citra_qt/dumping/options_dialog.ui b/src/citra_qt/dumping/options_dialog.ui index 21c7c6676..e8bd7fb41 100644 --- a/src/citra_qt/dumping/options_dialog.ui +++ b/src/citra_qt/dumping/options_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 600 - 300 + 650 + 350 @@ -24,6 +24,27 @@ + + + + + + Specific + + + true + + + + + + + Generic + + + + + diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 17a2c4cd7..6829b2d45 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -754,8 +754,6 @@ std::vector GetOptionList(const AVClass* av_class, bool search_child std::vector ListEncoders(AVMediaType type) { InitializeFFmpegLibraries(); - const auto general_options = GetOptionList(avcodec_get_class(), false); - std::vector out; const AVCodec* current = nullptr; @@ -768,19 +766,19 @@ std::vector ListEncoders(AVMediaType type) { if (!av_codec_is_encoder(current) || current->type != type) { continue; } - auto options = GetOptionList(current->priv_class, true); - options.insert(options.end(), general_options.begin(), general_options.end()); - out.push_back( - {current->name, ToStdString(current->long_name), current->id, std::move(options)}); + out.push_back({current->name, ToStdString(current->long_name), current->id, + GetOptionList(current->priv_class, true)}); } return out; } +std::vector GetEncoderGenericOptions() { + return GetOptionList(avcodec_get_class(), false); +} + std::vector ListFormats() { InitializeFFmpegLibraries(); - const auto general_options = GetOptionList(avformat_get_class(), false); - std::vector out; const AVOutputFormat* current = nullptr; @@ -790,9 +788,6 @@ std::vector ListFormats() { void* data = nullptr; // For libavformat to save the iteration state while ((current = av_muxer_iterate(&data))) { #endif - auto options = GetOptionList(current->priv_class, true); - options.insert(options.end(), general_options.begin(), general_options.end()); - std::vector extensions; Common::SplitString(ToStdString(current->extensions), ',', extensions); @@ -816,9 +811,13 @@ std::vector ListFormats() { out.push_back({current->name, ToStdString(current->long_name), std::move(extensions), std::move(supported_video_codecs), std::move(supported_audio_codecs), - std::move(options)}); + GetOptionList(current->priv_class, true)}); } return out; } +std::vector GetFormatGenericOptions() { + return GetOptionList(avformat_get_class(), false); +} + } // namespace VideoDumper diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index e2c605c8c..d9f8d8807 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -227,6 +227,8 @@ struct FormatInfo { }; std::vector ListEncoders(AVMediaType type); +std::vector GetEncoderGenericOptions(); std::vector ListFormats(); +std::vector GetFormatGenericOptions(); } // namespace VideoDumper From 22bfa7b5de708570cdb9b19e2b9fa63f383781cd Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 21 Mar 2020 12:02:21 +0800 Subject: [PATCH 20/21] ffmpeg: Misc fixes The most important one being adding a mutex to protect the format_context. Apparently it wasn't thread safe (as one'd expect) but I didn't think about that. Should fix some of the strange issues happening with MP4 muxers, etc. --- src/core/dumping/ffmpeg_backend.cpp | 52 ++++++++++++++++++----------- src/core/dumping/ffmpeg_backend.h | 13 +++++--- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 6829b2d45..05cc25acf 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -44,10 +44,12 @@ 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; } @@ -60,14 +62,12 @@ void FFmpegStream::Flush() { } void FFmpegStream::WritePacket(AVPacket& packet) { - if (packet.pts != static_cast(AV_NOPTS_VALUE)) { - packet.pts = av_rescale_q(packet.pts, codec_context->time_base, stream->time_base); - } - if (packet.dts != static_cast(AV_NOPTS_VALUE)) { - packet.dts = av_rescale_q(packet.dts, codec_context->time_base, stream->time_base); - } + av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base); packet.stream_index = stream->index; - av_interleaved_write_frame(format_context, &packet); + { + std::lock_guard lock{*format_context_mutex}; + av_interleaved_write_frame(format_context, &packet); + } } void FFmpegStream::SendFrame(AVFrame* frame) { @@ -101,12 +101,11 @@ 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_; @@ -129,7 +128,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou codec_context->time_base.den = 60; codec_context->gop_size = 12; codec_context->pix_fmt = codec->pix_fmts ? codec->pix_fmts[0] : AV_PIX_FMT_YUV420P; - if (output_format->flags & AVFMT_GLOBALHEADER) + if (format_context->oformat->flags & AVFMT_GLOBALHEADER) codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; AVDictionary* options = ToAVDictionary(Settings::values.video_encoder_options); @@ -157,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; } @@ -193,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); @@ -207,10 +210,10 @@ 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; frame_count = 0; @@ -246,8 +249,12 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { } 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; AVDictionary* options = ToAVDictionary(Settings::values.audio_encoder_options); if (avcodec_open2(codec_context.get(), codec, &options) < 0) { @@ -280,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 = @@ -418,9 +426,9 @@ bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& } 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); @@ -465,6 +473,8 @@ void FFmpegMuxer::FlushAudio() { } void FFmpegMuxer::WriteTrailer() { + std::lock_guard lock{format_context_mutex}; + av_interleaved_write_frame(format_context.get(), nullptr); av_write_trailer(format_context.get()); } @@ -553,11 +563,13 @@ void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) { refactored_frame[1][i] = frame[i][1]; } - ffmpeg.ProcessAudioFrame(refactored_frame[0], refactored_frame[1]); + audio_frame_queues[0].Push(std::move(refactored_frame[0])); + audio_frame_queues[1].Push(std::move(refactored_frame[1])); } void FFmpegBackend::AddAudioSample(const std::array& sample) { - ffmpeg.ProcessAudioFrame({sample[0]}, {sample[1]}); + audio_frame_queues[0].Push(VariableAudioFrame{sample[0]}); + audio_frame_queues[1].Push(VariableAudioFrame{sample[1]}); } void FFmpegBackend::StopDumping() { diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index d9f8d8807..86a6e08cc 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -31,13 +31,15 @@ using VariableAudioFrame = std::vector; void InitFFmpegLibraries(); +class FFmpegMuxer; + /** * Wrapper around FFmpeg AVCodecContext + AVStream. * Rescales/Resamples, encodes and writes a frame. */ class FFmpegStream { public: - bool Init(AVFormatContext* format_context); + bool Init(FFmpegMuxer& muxer); void Free(); void Flush(); @@ -60,6 +62,7 @@ protected: }; AVFormatContext* format_context{}; + std::mutex* format_context_mutex{}; std::unique_ptr codec_context{}; AVStream* stream{}; }; @@ -72,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); @@ -104,7 +106,7 @@ class FFmpegAudioStream : public FFmpegStream { public: ~FFmpegAudioStream(); - bool Init(AVFormatContext* format_context); + bool Init(FFmpegMuxer& muxer); void Free(); void ProcessFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1); void Flush(); @@ -153,6 +155,9 @@ private: FFmpegAudioStream audio_stream{}; FFmpegVideoStream video_stream{}; std::unique_ptr format_context{}; + std::mutex format_context_mutex; + + friend class FFmpegStream; }; /** From b87a15c6b2dd93baa49312d64879e051333890bf Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 22 Mar 2020 00:56:57 +0800 Subject: [PATCH 21/21] citra_qt: Only resume the game if it wasn't paused When dumping was stopped, the game will be paused and then resumed. However when the game was already paused this will result in the game being unexpectedly resumed, which isn't what we want. --- src/citra_qt/main.cpp | 5 ++++- src/citra_qt/main.h | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index e225bb5a1..55d963335 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1850,6 +1850,8 @@ void GMainWindow::OnStopVideoDumping() { const bool was_dumping = Core::System::GetInstance().VideoDumper().IsDumping(); if (!was_dumping) return; + + game_paused_for_dumping = emu_thread->IsRunning(); OnPauseGame(); auto future = @@ -1859,7 +1861,8 @@ void GMainWindow::OnStopVideoDumping() { if (game_shutdown_delayed) { game_shutdown_delayed = false; ShutdownGame(); - } else { + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; OnStartGame(); } }); diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 995983e5a..132fad821 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -258,6 +258,8 @@ private: QString video_dumping_path; // Whether game shutdown is delayed due to video dumping bool game_shutdown_delayed = false; + // Whether game was paused due to stopping video dumping + bool game_paused_for_dumping = false; // Debugger panes ProfilerWidget* profilerWidget;