From 06a0d86e9cfb9c32377b6f4eba11666a393b90ff Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 28 Jan 2020 21:57:30 +0800 Subject: [PATCH 01/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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 aa433990119b9e466ce4cd6ff0b403a3840a985d Mon Sep 17 00:00:00 2001 From: Vitor Kiguchi Date: Sat, 22 Feb 2020 22:46:30 -0300 Subject: [PATCH 20/37] Bump bundled sdl2 and appveyor to sdl2.0.10 --- CMakeLists.txt | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41d55f375..7c0a7db29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,7 +142,7 @@ if (ENABLE_SDL2) if (CITRA_USE_BUNDLED_SDL2) # Detect toolchain and platform if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1930) AND ARCHITECTURE_x86_64) - set(SDL2_VER "SDL2-2.0.8") + set(SDL2_VER "SDL2-2.0.10") else() message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRA_USE_BUNDLED_SDL2 and provide your own.") endif() diff --git a/appveyor.yml b/appveyor.yml index d8cbfc35f..6eb4bccaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,7 +27,7 @@ install: - ps: | if ($env:BUILD_TYPE -eq 'mingw') { $dependencies = "mingw64/mingw-w64-x86_64-cmake mingw64/mingw-w64-x86_64-qt5 mingw64/mingw-w64-x86_64-ffmpeg" - C:\msys64\usr\bin\bash -lc "pacman --noconfirm -U http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-SDL2-2.0.5-2-any.pkg.tar.xz" + C:\msys64\usr\bin\bash -lc "pacman --noconfirm -U http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-SDL2-2.0.10-1-any.pkg.tar.xz" C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S $dependencies" # (HACK) ignore errors 0 From 8e3960b40984ae6b0d51cb6172f0907225b7d4c5 Mon Sep 17 00:00:00 2001 From: Vitor Kiguchi Date: Wed, 4 Mar 2020 21:08:26 -0300 Subject: [PATCH 21/37] sdl_joystick: disable the use of the hidapi drivers due to many problems caused by them. The main problem is the loss of compatibility with some controllers, but there are also unwanted changes to the behaviour of PS4 controllers (hardcoded lightbar color). --- src/input_common/sdl/sdl_impl.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 0b69bfede..f186787b8 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -472,6 +472,14 @@ SDLState::SDLState() { if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) { LOG_ERROR(Input, "Failed to set Hint for background events", SDL_GetError()); } +// these hints are only defined on sdl2.0.9 or higher +#if SDL_VERSION_ATLEAST(2, 0, 9) + // This can be set back to 1 when the compatibility problems with the controllers are + // solved. There are also hints to toggle the individual drivers. + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); + // This hint should probably stay as "0" as long as the hidapi PS4 led issue isn't fixed + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "0"); +#endif SDL_AddEventWatch(&SDLEventWatcher, this); From 22bfa7b5de708570cdb9b19e2b9fa63f383781cd Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 21 Mar 2020 12:02:21 +0800 Subject: [PATCH 22/37] 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 23/37] 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; From 09da1dcf55e3f76bafe3b803a1aa0e106c19770d Mon Sep 17 00:00:00 2001 From: Pengfei Zhu Date: Sat, 28 Mar 2020 23:10:45 +0800 Subject: [PATCH 24/37] flatpak: Update runtime version The docker image has been updated to 5.13 but the build script here wasn't. Maybe this was the issue preventing our flatpak from building recently --- .travis/linux-flatpak/generate-data.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux-flatpak/generate-data.sh b/.travis/linux-flatpak/generate-data.sh index aaecd0500..799f0f1b8 100644 --- a/.travis/linux-flatpak/generate-data.sh +++ b/.travis/linux-flatpak/generate-data.sh @@ -43,7 +43,7 @@ cat > /tmp/org.citra.$REPO_NAME.json < Date: Sat, 28 Mar 2020 09:20:48 -0600 Subject: [PATCH 25/37] Use the correct directory for Qt Plugins (#5148) --- CMakeModules/CopyCitraQt5Deps.cmake | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CMakeModules/CopyCitraQt5Deps.cmake b/CMakeModules/CopyCitraQt5Deps.cmake index 88f9b8af1..d9014354e 100644 --- a/CMakeModules/CopyCitraQt5Deps.cmake +++ b/CMakeModules/CopyCitraQt5Deps.cmake @@ -6,10 +6,10 @@ function(copy_citra_Qt5_deps target_dir) set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/") set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/") set(Qt5_IMAGEFORMATS_DIR "${Qt5_DIR}/../../../plugins/imageformats/") - set(PLATFORMS ${DLL_DEST}platforms/) - set(MEDIASERVICE ${DLL_DEST}mediaservice/) - set(STYLES ${DLL_DEST}styles/) - set(IMAGEFORMATS ${DLL_DEST}imageformats/) + set(PLATFORMS ${DLL_DEST}plugins/platforms/) + set(MEDIASERVICE ${DLL_DEST}plugins/mediaservice/) + set(STYLES ${DLL_DEST}plugins/styles/) + set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/) windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST} icudt*.dll icuin*.dll @@ -38,4 +38,10 @@ function(copy_citra_Qt5_deps target_dir) qwbmp$<$:d>.dll qwebp$<$:d>.dll ) + + # Create an empty qt.conf file. Qt will detect that this file exists, and use the folder that its in as the root folder. + # This way it'll look for plugins in the root/plugins/ folder + add_custom_command(TARGET citra-qt POST_BUILD + COMMAND ${CMAKE_COMMAND} -E touch ${DLL_DEST}qt.conf + ) endfunction(copy_citra_Qt5_deps) From a6ee1bf913abca29b5794adba8108af285d38925 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 28 Mar 2020 09:39:50 -0600 Subject: [PATCH 26/37] HLE Audio: Increase frame position by input buffer sample rate (#5049) * HLE Audio: Increase frame position by input buffer sample rate Currently the frame position moves ahead by the number of samples output, but thats a fixed number based on the 3ds native sample rate. Instead, based on a homebrew by cyuubi and looking at the lle audio, this sample position should be moved forward by the number of samples from the input buffer that was read, based on the buffer's sample rate. --- src/audio_core/hle/source.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/audio_core/hle/source.cpp b/src/audio_core/hle/source.cpp index c8ee3b618..a4a0f5412 100644 --- a/src/audio_core/hle/source.cpp +++ b/src/audio_core/hle/source.cpp @@ -289,7 +289,9 @@ void Source::GenerateFrame() { break; } } - state.next_sample_number += static_cast(frame_position); + // TODO(jroweboy): Keep track of frame_position independently so that it doesn't lose precision + // over time + state.next_sample_number += static_cast(frame_position * state.rate_multiplier); state.filters.ProcessFrame(current_frame); } From b4ac1c0559717b5f2998acb3330291bfef83896a Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Fri, 28 Feb 2020 02:56:24 +0100 Subject: [PATCH 27/37] citra_qt/system: Add N3DS mode checkbox and enable it by default --- src/citra/config.cpp | 2 +- src/citra/default_ini.h | 2 +- src/citra_qt/configuration/config.cpp | 4 +- .../configuration/configure_system.cpp | 4 + .../configuration/configure_system.ui | 257 +++++++++--------- src/core/hle/service/ptm/ptm.cpp | 7 +- 6 files changed, 141 insertions(+), 135 deletions(-) diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 47270ab36..7725666d7 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -201,7 +201,7 @@ void Config::ReadValues() { sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true); // System - Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", false); + Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", true); Settings::values.region_value = sdl2_config->GetInteger("System", "region_value", Settings::REGION_VALUE_AUTO_SELECT); Settings::values.init_clock = diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 881700913..6b4ba4172 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -244,7 +244,7 @@ use_virtual_sd = [System] # The system model that Citra will try to emulate -# 0: Old 3DS (default), 1: New 3DS +# 0: Old 3DS, 1: New 3DS (default) is_new_3ds = # The system region that Citra will use during emulation diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index d139a89e9..99142098c 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -482,7 +482,7 @@ void Config::ReadShortcutValues() { void Config::ReadSystemValues() { qt_config->beginGroup(QStringLiteral("System")); - Settings::values.is_new_3ds = ReadSetting(QStringLiteral("is_new_3ds"), false).toBool(); + Settings::values.is_new_3ds = ReadSetting(QStringLiteral("is_new_3ds"), true).toBool(); Settings::values.region_value = ReadSetting(QStringLiteral("region_value"), Settings::REGION_VALUE_AUTO_SELECT).toInt(); Settings::values.init_clock = static_cast( @@ -921,7 +921,7 @@ void Config::SaveShortcutValues() { void Config::SaveSystemValues() { qt_config->beginGroup(QStringLiteral("System")); - WriteSetting(QStringLiteral("is_new_3ds"), Settings::values.is_new_3ds, false); + WriteSetting(QStringLiteral("is_new_3ds"), Settings::values.is_new_3ds, true); WriteSetting(QStringLiteral("region_value"), Settings::values.region_value, Settings::REGION_VALUE_AUTO_SELECT); WriteSetting(QStringLiteral("init_clock"), static_cast(Settings::values.init_clock), diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index 139d92f7b..32ea7b3f3 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -277,6 +277,8 @@ void ConfigureSystem::SetConfiguration() { ui->slider_clock_speed->setValue(SettingsToSlider(Settings::values.cpu_clock_percentage)); ui->clock_display_label->setText( QStringLiteral("%1%").arg(Settings::values.cpu_clock_percentage)); + + ui->toggle_new_3ds->setChecked(Settings::values.is_new_3ds); } void ConfigureSystem::ReadSystemSettings() { @@ -374,6 +376,8 @@ void ConfigureSystem::ApplyConfiguration() { Settings::values.init_clock = static_cast(ui->combo_init_clock->currentIndex()); Settings::values.init_time = ui->edit_init_time->dateTime().toTime_t(); + + Settings::values.is_new_3ds = ui->toggle_new_3ds->isChecked(); } Settings::values.cpu_clock_percentage = SliderToSettings(ui->slider_clock_speed->value()); diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui index 554993990..c6f12060c 100644 --- a/src/citra_qt/configuration/configure_system.ui +++ b/src/citra_qt/configuration/configure_system.ui @@ -22,14 +22,74 @@ System Settings - - - - Username + + + + Note: this can be overridden when region setting is auto-select + + + Japanese (日本語) + + + + + English + + + + + French (français) + + + + + German (Deutsch) + + + + + Italian (italiano) + + + + + Spanish (español) + + + + + Simplified Chinese (简体中文) + + + + + Korean (한국어) + + + + + Dutch (Nederlands) + + + + + Portuguese (português) + + + + + Russian (Русский) + + + + + Traditional Chinese (正體中文) + + - + @@ -42,14 +102,33 @@ - - + + - Birthday + Username - + + + + + Mono + + + + + Stereo + + + + + Surround + + + + + @@ -120,124 +199,38 @@ - + Language - - - - Note: this can be overridden when region setting is auto-select + + + + Birthday - - - Japanese (日本語) - - - - - English - - - - - French (français) - - - - - German (Deutsch) - - - - - Italian (italiano) - - - - - Spanish (español) - - - - - Simplified Chinese (简体中文) - - - - - Korean (한국어) - - - - - Dutch (Nederlands) - - - - - Portuguese (português) - - - - - Russian (Русский) - - - - - Traditional Chinese (正體中文) - - - + Sound output mode - - - - - Mono - - - - - Stereo - - - - - Surround - - - + + - + Country - - - - - - - Clock - - - - + @@ -251,42 +244,35 @@ - + + + + Clock + + + + Startup time - - - - yyyy-MM-ddTHH:mm:ss - - - - - - - Play Coins: - - - - + 300 - - + + - Console ID: + Play Coins: - + @@ -302,6 +288,27 @@ + + + + Console ID: + + + + + + + yyyy-MM-ddTHH:mm:ss + + + + + + + Enable New 3DS mode + + + diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 7d30fcc5b..4af8291bf 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -117,15 +117,10 @@ void Module::Interface::GetSoftwareClosedFlag(Kernel::HLERequestContext& ctx) { void CheckNew3DS(IPC::RequestBuilder& rb) { const bool is_new_3ds = Settings::values.is_new_3ds; - if (is_new_3ds) { - LOG_CRITICAL(Service_PTM, "The option 'is_new_3ds' is enabled as part of the 'System' " - "settings. Citra does not fully support New 3DS emulation yet!"); - } - rb.Push(RESULT_SUCCESS); rb.Push(is_new_3ds); - LOG_WARNING(Service_PTM, "(STUBBED) called isNew3DS = 0x{:08x}", static_cast(is_new_3ds)); + LOG_DEBUG(Service_PTM, "called isNew3DS = 0x{:08x}", static_cast(is_new_3ds)); } void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) { From d26564d020460dd15f79b5e8e9229f2b99b6b50d Mon Sep 17 00:00:00 2001 From: Khangaroo Date: Thu, 2 Apr 2020 00:59:24 -0400 Subject: [PATCH 28/37] Don't dump textures that aren't a power of 2 (#5152) * don't dump textures that aren't a power of 2 * early return * include bitset * revert change to comment block * explain change --- .../renderer_opengl/gl_rasterizer_cache.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index cbf7d4926..56d65133f 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -884,6 +885,16 @@ bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_inf } void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) { + // Make sure the texture size is a power of 2 + // If not, the surface is actually a framebuffer + std::bitset<32> width_bits(width); + std::bitset<32> height_bits(height); + if (width_bits.count() != 1 || height_bits.count() != 1) { + LOG_WARNING(Render_OpenGL, "Not dumping {:016X} because size isn't a power of 2 ({}x{})", + tex_hash, width, height); + return; + } + // Dump texture to RGBA8 and encode as PNG const auto& image_interface = Core::System::GetInstance().GetImageInterface(); auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache(); From f14e973a271b1c2982bf88fce482f5c5d8b7c38a Mon Sep 17 00:00:00 2001 From: Marshall Mohror Date: Thu, 2 Apr 2020 22:42:50 -0500 Subject: [PATCH 29/37] Texture Filtering v2 (#5166) * video_core/renderer_opengl: Move SurfaceParams into its own file Some of its enums are needed outside of the rasterizer cache and trying to use it caused circular dependencies. * video_core/renderer_opengl: Overhaul the texture filter framework This should make it less intrusive. Now texture filtering doesn't have any mutable global state. The texture filters now always upscale to the internal rendering resolution. This simplifies the logic in UploadGLTexture and it simply takes the role of BlitTextures at the end of the function. This also prevent extra blitting required when uploading to a framebuffer surface with a mismatched size. * video_core/renderer_opengl: Use generated mipmaps for filtered textures The filtered guest mipmaps often looked terrible. * core/settings: Remove texture filter factor * sdl/config: Remove texture filter factor * qt/config: Remove texture filter factor --- src/citra/config.cpp | 2 - src/citra/default_ini.h | 3 +- src/citra_qt/configuration/config.cpp | 4 - .../configuration/configure_enhancements.cpp | 23 +- .../configuration/configure_enhancements.ui | 36 --- src/core/settings.cpp | 6 +- src/core/settings.h | 1 - src/video_core/CMakeLists.txt | 8 +- .../renderer_opengl/gl_rasterizer_cache.cpp | 217 ++--------------- .../renderer_opengl/gl_rasterizer_cache.h | 223 +---------------- src/video_core/renderer_opengl/gl_state.h | 14 +- .../renderer_opengl/gl_surface_params.cpp | 171 +++++++++++++ .../renderer_opengl/gl_surface_params.h | 229 ++++++++++++++++++ .../renderer_opengl/renderer_opengl.cpp | 7 +- .../anime4k/anime4k_ultrafast.cpp | 45 ++-- .../anime4k/anime4k_ultrafast.h | 22 +- .../texture_filters/anime4k/refine.frag | 4 +- .../texture_filters/bicubic/bicubic.cpp | 33 +-- .../texture_filters/bicubic/bicubic.h | 23 +- .../texture_filters/texture_filter_base.h | 26 ++ .../texture_filter_interface.h | 38 --- .../texture_filter_manager.cpp | 89 ------- .../texture_filters/texture_filter_manager.h | 55 ----- .../texture_filters/texture_filterer.cpp | 86 +++++++ .../texture_filters/texture_filterer.h | 39 +++ .../texture_filters/xbrz/xbrz_freescale.cpp | 30 +-- .../texture_filters/xbrz/xbrz_freescale.h | 19 +- src/video_core/video_core.cpp | 1 + src/video_core/video_core.h | 1 + 29 files changed, 676 insertions(+), 779 deletions(-) create mode 100644 src/video_core/renderer_opengl/gl_surface_params.cpp create mode 100644 src/video_core/renderer_opengl/gl_surface_params.h create mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_base.h delete mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h delete mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp delete mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h create mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp create mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filterer.h diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 47270ab36..f8c3a4f94 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -132,8 +132,6 @@ void Config::ReadValues() { static_cast(sdl2_config->GetInteger("Renderer", "use_vsync_new", 1)); Settings::values.texture_filter_name = sdl2_config->GetString("Renderer", "texture_filter_name", "none"); - Settings::values.texture_filter_factor = - sdl2_config->GetInteger("Renderer", "texture_filter_factor", 1); Settings::values.render_3d = static_cast( sdl2_config->GetInteger("Renderer", "render_3d", 0)); diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 881700913..b7996d1cb 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -132,9 +132,8 @@ use_disk_shader_cache = # factor for the 3DS resolution resolution_factor = -# Texture filter name and scale factor +# Texture filter name texture_filter_name = -texture_filter_factor = # Turns on the frame limiter, which will limit frames output to the target game speed # 0: Off, 1: On (default) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index d139a89e9..0bc28a43a 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -454,8 +454,6 @@ void Config::ReadRendererValues() { ReadSetting(QStringLiteral("texture_filter_name"), QStringLiteral("none")) .toString() .toStdString(); - Settings::values.texture_filter_factor = - ReadSetting(QStringLiteral("texture_filter_factor"), 1).toInt(); qt_config->endGroup(); } @@ -893,8 +891,6 @@ void Config::SaveRendererValues() { WriteSetting(QStringLiteral("texture_filter_name"), QString::fromStdString(Settings::values.texture_filter_name), QStringLiteral("none")); - WriteSetting(QStringLiteral("texture_filter_factor"), Settings::values.texture_filter_factor, - 1); qt_config->endGroup(); } diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index e1c6b6bdd..f25b9c1e1 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -8,17 +8,14 @@ #include "core/settings.h" #include "ui_configure_enhancements.h" #include "video_core/renderer_opengl/post_processing_opengl.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureEnhancements) { ui->setupUi(this); - for (const auto& filter : OpenGL::TextureFilterManager::TextureFilterMap()) - ui->texture_filter_combobox->addItem(QString::fromStdString(filter.first.data())); - - connect(ui->texture_filter_combobox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ConfigureEnhancements::updateTextureFilter); + for (const auto& filter : OpenGL::TextureFilterer::GetFilterNames()) + ui->texture_filter_combobox->addItem(QString::fromStdString(filter.data())); SetConfiguration(); @@ -60,7 +57,6 @@ void ConfigureEnhancements::SetConfiguration() { ui->factor_3d->setValue(Settings::values.factor_3d); updateShaders(Settings::values.render_3d); ui->toggle_linear_filter->setChecked(Settings::values.filter_mode); - ui->texture_scale_spinbox->setValue(Settings::values.texture_filter_factor); int tex_filter_idx = ui->texture_filter_combobox->findText( QString::fromStdString(Settings::values.texture_filter_name)); if (tex_filter_idx == -1) { @@ -68,7 +64,6 @@ void ConfigureEnhancements::SetConfiguration() { } else { ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx); } - updateTextureFilter(tex_filter_idx); ui->layout_combobox->setCurrentIndex(static_cast(Settings::values.layout_option)); ui->swap_screen->setChecked(Settings::values.swap_screen); ui->toggle_disk_shader_cache->setChecked(Settings::values.use_hw_shader && @@ -105,17 +100,6 @@ void ConfigureEnhancements::updateShaders(Settings::StereoRenderOption stereo_op } } -void ConfigureEnhancements::updateTextureFilter(int index) { - if (index == -1) - return; - ui->texture_filter_group->setEnabled(index != 0); - const auto& clamp = OpenGL::TextureFilterManager::TextureFilterMap() - .at(ui->texture_filter_combobox->currentText().toStdString()) - .clamp_scale; - ui->texture_scale_spinbox->setMinimum(clamp.min); - ui->texture_scale_spinbox->setMaximum(clamp.max); -} - void ConfigureEnhancements::RetranslateUI() { ui->retranslateUi(this); } @@ -130,7 +114,6 @@ void ConfigureEnhancements::ApplyConfiguration() { ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString(); Settings::values.filter_mode = ui->toggle_linear_filter->isChecked(); Settings::values.texture_filter_name = ui->texture_filter_combobox->currentText().toStdString(); - Settings::values.texture_filter_factor = ui->texture_scale_spinbox->value(); Settings::values.layout_option = static_cast(ui->layout_combobox->currentIndex()); Settings::values.swap_screen = ui->swap_screen->isChecked(); diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index d3193e9eb..909a98643 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -131,42 +131,6 @@ - - - - - 16 - - - 0 - - - 0 - - - 0 - - - - - - - Texture Scale Factor - - - - - - - 1 - - - - - - - - diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 13b1e965f..6af80205b 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -13,7 +13,6 @@ #include "core/hle/service/mic_u.h" #include "core/settings.h" #include "video_core/renderer_base.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" #include "video_core/video_core.h" namespace Settings { @@ -38,9 +37,7 @@ void Apply() { VideoCore::g_renderer_bg_color_update_requested = true; VideoCore::g_renderer_sampler_update_requested = true; VideoCore::g_renderer_shader_update_requested = true; - - OpenGL::TextureFilterManager::GetInstance().SetTextureFilter(values.texture_filter_name, - values.texture_filter_factor); + VideoCore::g_texture_filter_update_requested = true; auto& system = Core::System::GetInstance(); if (system.IsPoweredOn()) { @@ -88,7 +85,6 @@ void LogSettings() { LogSetting("Renderer_FrameLimit", Settings::values.frame_limit); LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name); LogSetting("Renderer_FilterMode", Settings::values.filter_mode); - LogSetting("Renderer_TextureFilterFactor", Settings::values.texture_filter_factor); LogSetting("Renderer_TextureFilterName", Settings::values.texture_filter_name); LogSetting("Stereoscopy_Render3d", static_cast(Settings::values.render_3d)); LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d); diff --git a/src/core/settings.h b/src/core/settings.h index aa7e781b5..980156663 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -148,7 +148,6 @@ struct Values { u16 resolution_factor; bool use_frame_limit; u16 frame_limit; - u16 texture_filter_factor; std::string texture_filter_name; LayoutOption layout_option; diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index a7dbc42de..3553fe0a8 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -43,6 +43,8 @@ add_library(video_core STATIC renderer_opengl/gl_state.h renderer_opengl/gl_stream_buffer.cpp renderer_opengl/gl_stream_buffer.h + renderer_opengl/gl_surface_params.cpp + renderer_opengl/gl_surface_params.h renderer_opengl/gl_vars.cpp renderer_opengl/gl_vars.h renderer_opengl/pica_to_gl.h @@ -54,9 +56,9 @@ add_library(video_core STATIC renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h renderer_opengl/texture_filters/bicubic/bicubic.cpp renderer_opengl/texture_filters/bicubic/bicubic.h - renderer_opengl/texture_filters/texture_filter_interface.h - renderer_opengl/texture_filters/texture_filter_manager.cpp - renderer_opengl/texture_filters/texture_filter_manager.h + renderer_opengl/texture_filters/texture_filter_base.h + renderer_opengl/texture_filters/texture_filterer.cpp + renderer_opengl/texture_filters/texture_filterer.h renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp renderer_opengl/texture_filters/xbrz/xbrz_freescale.h shader/debug_data.h diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index 56d65133f..868bcaecd 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -35,7 +35,7 @@ #include "video_core/renderer_opengl/gl_rasterizer_cache.h" #include "video_core/renderer_opengl/gl_state.h" #include "video_core/renderer_opengl/gl_vars.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" #include "video_core/utils.h" #include "video_core/video_core.h" @@ -494,125 +494,6 @@ static bool FillSurface(const Surface& surface, const u8* fill_data, return true; } -SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const { - SurfaceParams params = *this; - const u32 tiled_size = is_tiled ? 8 : 1; - const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size); - PAddr aligned_start = - addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes); - PAddr aligned_end = - addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes); - - if (aligned_end - aligned_start > stride_tiled_bytes) { - params.addr = aligned_start; - params.height = (aligned_end - aligned_start) / BytesInPixels(stride); - } else { - // 1 row - ASSERT(aligned_end - aligned_start == stride_tiled_bytes); - const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1); - aligned_start = - addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment); - aligned_end = - addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment); - params.addr = aligned_start; - params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size; - params.stride = params.width; - params.height = tiled_size; - } - params.UpdateParams(); - - return params; -} - -SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle unscaled_rect) const { - if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) { - return {}; - } - - if (is_tiled) { - unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8; - unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8; - unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8; - unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8; - } - - const u32 stride_tiled = !is_tiled ? stride : stride * 8; - - const u32 pixel_offset = - stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) + - unscaled_rect.left; - - const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth(); - - return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)}; -} - -Common::Rectangle SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const { - const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr); - - if (is_tiled) { - const int x0 = (begin_pixel_index % (stride * 8)) / 8; - const int y0 = (begin_pixel_index / (stride * 8)) * 8; - // Top to bottom - return Common::Rectangle(x0, height - y0, x0 + sub_surface.width, - height - (y0 + sub_surface.height)); - } - - const int x0 = begin_pixel_index % stride; - const int y0 = begin_pixel_index / stride; - // Bottom to top - return Common::Rectangle(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0); -} - -Common::Rectangle SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const { - auto rect = GetSubRect(sub_surface); - rect.left = rect.left * res_scale; - rect.right = rect.right * res_scale; - rect.top = rect.top * res_scale; - rect.bottom = rect.bottom * res_scale; - return rect; -} - -bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const { - return std::tie(other_surface.addr, other_surface.width, other_surface.height, - other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) == - std::tie(addr, width, height, stride, pixel_format, is_tiled) && - pixel_format != PixelFormat::Invalid; -} - -bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const { - return sub_surface.addr >= addr && sub_surface.end <= end && - sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid && - sub_surface.is_tiled == is_tiled && - (sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && - (sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) && - GetSubRect(sub_surface).right <= stride; -} - -bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const { - return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format && - addr <= expanded_surface.end && expanded_surface.addr <= end && - is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride && - (std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) % - BytesInPixels(stride * (is_tiled ? 8 : 1)) == - 0; -} - -bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const { - if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr || - end < texcopy_params.end) { - return false; - } - if (texcopy_params.width != texcopy_params.stride) { - const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1)); - return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && - texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 && - (texcopy_params.height == 1 || texcopy_params.stride == tile_stride) && - ((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride; - } - return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval(); -} - bool CachedSurface::CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const { if (type == SurfaceType::Fill && IsRegionValid(fill_interval) && @@ -654,47 +535,6 @@ bool CachedSurface::CanCopy(const SurfaceParams& dest_surface, return false; } -SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const { - SurfaceInterval result{}; - const auto valid_regions = - SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions; - for (auto& valid_interval : valid_regions) { - const SurfaceInterval aligned_interval{ - addr + Common::AlignUp(boost::icl::first(valid_interval) - addr, - BytesInPixels(is_tiled ? 8 * 8 : 1)), - addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr, - BytesInPixels(is_tiled ? 8 * 8 : 1))}; - - if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) || - boost::icl::length(aligned_interval) == 0) { - continue; - } - - // Get the rectangle within aligned_interval - const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1); - SurfaceInterval rect_interval{ - addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes), - addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes), - }; - if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) { - // 1 row - rect_interval = aligned_interval; - } else if (boost::icl::length(rect_interval) == 0) { - // 2 rows that do not make a rectangle, return the larger one - const SurfaceInterval row1{boost::icl::first(aligned_interval), - boost::icl::first(rect_interval)}; - const SurfaceInterval row2{boost::icl::first(rect_interval), - boost::icl::last_next(aligned_interval)}; - rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2; - } - - if (boost::icl::length(rect_interval) > boost::icl::length(result)) { - result = rect_interval; - } - } - return result; -} - MICROPROFILE_DEFINE(OpenGL_CopySurface, "OpenGL", "CopySurface", MP_RGB(128, 192, 64)); void RasterizerCacheOpenGL::CopySurface(const Surface& src_surface, const Surface& dst_surface, SurfaceInterval copy_interval) { @@ -956,10 +796,6 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ if (Settings::values.custom_textures) is_custom = LoadCustomTexture(tex_hash, custom_tex_info); - TextureFilterInterface* const texture_filter = - is_custom ? nullptr : TextureFilterManager::GetInstance().GetTextureFilter(); - const u16 default_scale = texture_filter ? texture_filter->scale_factor : 1; - // Load data from memory to the surface GLint x0 = static_cast(rect.left); GLint y0 = static_cast(rect.bottom); @@ -971,7 +807,7 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ // If not 1x scale, create 1x texture that we will blit from to replace texture subrect in // surface OGLTexture unscaled_tex; - if (res_scale != default_scale) { + if (res_scale != 1) { x0 = 0; y0 = 0; @@ -980,8 +816,7 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ AllocateSurfaceTexture(unscaled_tex.handle, GetFormatTuple(PixelFormat::RGBA8), custom_tex_info.width, custom_tex_info.height); } else { - AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth() * default_scale, - rect.GetHeight() * default_scale); + AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth(), rect.GetHeight()); } target_tex = unscaled_tex.handle; } @@ -1007,16 +842,6 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ glActiveTexture(GL_TEXTURE0); glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, custom_tex_info.width, custom_tex_info.height, GL_RGBA, GL_UNSIGNED_BYTE, custom_tex_info.tex.data()); - } else if (texture_filter) { - if (res_scale == default_scale) { - AllocateSurfaceTexture(texture.handle, GetFormatTuple(pixel_format), - rect.GetWidth() * default_scale, - rect.GetHeight() * default_scale); - cur_state.texture_units[0].texture_2d = texture.handle; - cur_state.Apply(); - } - texture_filter->scale(*this, {(u32)x0, (u32)y0, rect.GetWidth(), rect.GetHeight()}, - buffer_offset); } else { glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(stride)); @@ -1027,13 +852,13 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ } glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - if (Settings::values.dump_textures && !is_custom && !texture_filter) + if (Settings::values.dump_textures && !is_custom) DumpTexture(target_tex, tex_hash); cur_state.texture_units[0].texture_2d = old_tex; cur_state.Apply(); - if (res_scale != default_scale) { + if (res_scale != 1) { auto scaled_rect = rect; scaled_rect.left *= res_scale; scaled_rect.top *= res_scale; @@ -1042,8 +867,11 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ auto from_rect = is_custom ? Common::Rectangle{0, custom_tex_info.height, custom_tex_info.width, 0} : Common::Rectangle{0, rect.GetHeight(), rect.GetWidth(), 0}; - BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type, - read_fb_handle, draw_fb_handle); + if (!owner.texture_filterer->Filter(unscaled_tex.handle, from_rect, texture.handle, + scaled_rect, type, read_fb_handle, draw_fb_handle)) { + BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type, + read_fb_handle, draw_fb_handle); + } } InvalidateAllWatcher(); @@ -1232,6 +1060,10 @@ Surface FindMatch(const SurfaceCache& surface_cache, const SurfaceParams& params } RasterizerCacheOpenGL::RasterizerCacheOpenGL() { + resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); + texture_filterer = std::make_unique(Settings::values.texture_filter_name, + resolution_scale_factor); + read_framebuffer.Create(); draw_framebuffer.Create(); @@ -1508,11 +1340,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf params.height = info.height; params.is_tiled = true; params.pixel_format = SurfaceParams::PixelFormatFromTextureFormat(info.format); - TextureFilterInterface* filter{}; - - params.res_scale = (filter = TextureFilterManager::GetInstance().GetTextureFilter()) - ? filter->scale_factor - : 1; + params.res_scale = texture_filterer->IsNull() ? 1 : resolution_scale_factor; params.UpdateParams(); u32 min_width = info.width >> max_level; @@ -1565,7 +1393,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level, height >> level, 0, format_tuple.format, format_tuple.type, nullptr); } - if (surface->is_custom) { + if (surface->is_custom || !texture_filterer->IsNull()) { // TODO: proper mipmap support for custom textures glGenerateMipmap(GL_TEXTURE_2D); } @@ -1601,7 +1429,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf } state.ResetTexture(level_surface->texture.handle); state.Apply(); - if (!surface->is_custom) { + if (!surface->is_custom && texture_filterer->IsNull()) { glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, level_surface->texture.handle, 0); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, @@ -1725,10 +1553,9 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( const auto& config = regs.framebuffer.framebuffer; // update resolution_scale_factor and reset cache if changed - static u16 resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); - if (resolution_scale_factor != VideoCore::GetResolutionScaleFactor() || - TextureFilterManager::GetInstance().IsUpdated()) { - TextureFilterManager::GetInstance().Reset(); + if ((resolution_scale_factor != VideoCore::GetResolutionScaleFactor()) | + (VideoCore::g_texture_filter_update_requested.exchange(false) && + texture_filterer->Reset(Settings::values.texture_filter_name, resolution_scale_factor))) { resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); FlushAll(); while (!surface_cache.empty()) @@ -1813,7 +1640,7 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( } Surface RasterizerCacheOpenGL::GetFillSurface(const GPU::Regs::MemoryFillConfig& config) { - Surface new_surface = std::make_shared(); + Surface new_surface = std::make_shared(*this); new_surface->addr = config.GetStartAddress(); new_surface->end = config.GetEndAddress(); @@ -2041,7 +1868,7 @@ void RasterizerCacheOpenGL::InvalidateRegion(PAddr addr, u32 size, const Surface } Surface RasterizerCacheOpenGL::CreateSurface(const SurfaceParams& params) { - Surface surface = std::make_shared(); + Surface surface = std::make_shared(*this); static_cast(*surface) = params; surface->texture.Create(); diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h index 466ebb8d0..dffcbb05c 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h @@ -26,14 +26,15 @@ #include "common/common_types.h" #include "common/math_util.h" #include "core/custom_tex_cache.h" -#include "core/hw/gpu.h" -#include "video_core/regs_framebuffer.h" -#include "video_core/regs_texturing.h" #include "video_core/renderer_opengl/gl_resource_manager.h" +#include "video_core/renderer_opengl/gl_surface_params.h" #include "video_core/texture/texture_decode.h" namespace OpenGL { +class RasterizerCacheOpenGL; +class TextureFilterer; + struct TextureCubeConfig { PAddr px; PAddr nx; @@ -76,11 +77,8 @@ struct hash { namespace OpenGL { -struct CachedSurface; -using Surface = std::shared_ptr; using SurfaceSet = std::set; -using SurfaceInterval = boost::icl::right_open_interval; using SurfaceRegions = boost::icl::interval_set; using SurfaceMap = boost::icl::interval_map BPP_TABLE = { - 32, // RGBA8 - 24, // RGB8 - 16, // RGB5A1 - 16, // RGB565 - 16, // RGBA4 - 16, // IA8 - 16, // RG8 - 8, // I8 - 8, // A8 - 8, // IA4 - 4, // I4 - 4, // A4 - 4, // ETC1 - 8, // ETC1A4 - 16, // D16 - 0, - 24, // D24 - 32, // D24S8 - }; - -public: - enum class PixelFormat { - // First 5 formats are shared between textures and color buffers - RGBA8 = 0, - RGB8 = 1, - RGB5A1 = 2, - RGB565 = 3, - RGBA4 = 4, - - // Texture-only formats - IA8 = 5, - RG8 = 6, - I8 = 7, - A8 = 8, - IA4 = 9, - I4 = 10, - A4 = 11, - ETC1 = 12, - ETC1A4 = 13, - - // Depth buffer-only formats - D16 = 14, - // gap - D24 = 16, - D24S8 = 17, - - Invalid = 255, - }; - - enum class SurfaceType { - Color = 0, - Texture = 1, - Depth = 2, - DepthStencil = 3, - Fill = 4, - Invalid = 5 - }; - - static constexpr unsigned int GetFormatBpp(PixelFormat format) { - const auto format_idx = static_cast(format); - DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx); - return BPP_TABLE[format_idx]; - } - - unsigned int GetFormatBpp() const { - return GetFormatBpp(pixel_format); - } - - static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) { - return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid; - } - - static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) { - return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; - } - - static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) { - return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14) - : PixelFormat::Invalid; - } - - static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) { - switch (format) { - // RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat - case GPU::Regs::PixelFormat::RGB565: - return PixelFormat::RGB565; - case GPU::Regs::PixelFormat::RGB5A1: - return PixelFormat::RGB5A1; - default: - return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; - } - } - - static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) { - SurfaceType a_type = GetFormatType(pixel_format_a); - SurfaceType b_type = GetFormatType(pixel_format_b); - - if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) && - (b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) { - return true; - } - - if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) { - return true; - } - - if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) { - return true; - } - - return false; - } - - static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) { - if ((unsigned int)pixel_format < 5) { - return SurfaceType::Color; - } - - if ((unsigned int)pixel_format < 14) { - return SurfaceType::Texture; - } - - if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) { - return SurfaceType::Depth; - } - - if (pixel_format == PixelFormat::D24S8) { - return SurfaceType::DepthStencil; - } - - return SurfaceType::Invalid; - } - - /// Update the params "size", "end" and "type" from the already set "addr", "width", "height" - /// and "pixel_format" - void UpdateParams() { - if (stride == 0) { - stride = width; - } - type = GetFormatType(pixel_format); - size = !is_tiled ? BytesInPixels(stride * (height - 1) + width) - : BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8); - end = addr + size; - } - - SurfaceInterval GetInterval() const { - return SurfaceInterval(addr, end); - } - - // Returns the outer rectangle containing "interval" - SurfaceParams FromInterval(SurfaceInterval interval) const; - - SurfaceInterval GetSubRectInterval(Common::Rectangle unscaled_rect) const; - - // Returns the region of the biggest valid rectange within interval - SurfaceInterval GetCopyableInterval(const Surface& src_surface) const; - - u32 GetScaledWidth() const { - return width * res_scale; - } - - u32 GetScaledHeight() const { - return height * res_scale; - } - - Common::Rectangle GetRect() const { - return {0, height, width, 0}; - } - - Common::Rectangle GetScaledRect() const { - return {0, GetScaledHeight(), GetScaledWidth(), 0}; - } - - u32 PixelsInBytes(u32 size) const { - return size * CHAR_BIT / GetFormatBpp(pixel_format); - } - - u32 BytesInPixels(u32 pixels) const { - return pixels * GetFormatBpp(pixel_format) / CHAR_BIT; - } - - bool ExactMatch(const SurfaceParams& other_surface) const; - bool CanSubRect(const SurfaceParams& sub_surface) const; - bool CanExpand(const SurfaceParams& expanded_surface) const; - bool CanTexCopy(const SurfaceParams& texcopy_params) const; - - Common::Rectangle GetSubRect(const SurfaceParams& sub_surface) const; - Common::Rectangle GetScaledSubRect(const SurfaceParams& sub_surface) const; - - PAddr addr = 0; - PAddr end = 0; - u32 size = 0; - - u32 width = 0; - u32 height = 0; - u32 stride = 0; - u16 res_scale = 1; - - bool is_tiled = false; - PixelFormat pixel_format = PixelFormat::Invalid; - SurfaceType type = SurfaceType::Invalid; -}; - /** * A watcher that notifies whether a cached surface has been changed. This is useful for caching * surface collection objects, including texture cube and mipmap. @@ -345,6 +137,8 @@ private: }; struct CachedSurface : SurfaceParams, std::enable_shared_from_this { + CachedSurface(RasterizerCacheOpenGL& owner) : owner{owner} {} + bool CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const; bool CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const; @@ -422,6 +216,7 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this> watchers; }; @@ -519,8 +314,12 @@ private: OGLProgram d24s8_abgr_shader; GLint d24s8_abgr_tbo_size_u_id; GLint d24s8_abgr_viewport_u_id; + u16 resolution_scale_factor; std::unordered_map texture_cube_cache; + +public: + std::unique_ptr texture_filterer; }; struct FormatTuple { diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h index 5d655a127..cc7a864a2 100644 --- a/src/video_core/renderer_opengl/gl_state.h +++ b/src/video_core/renderer_opengl/gl_state.h @@ -38,13 +38,6 @@ constexpr GLuint ShadowTexturePZ = 5; constexpr GLuint ShadowTextureNZ = 6; } // namespace ImageUnits -struct Viewport { - GLint x; - GLint y; - GLsizei width; - GLsizei height; -}; - class OpenGLState { public: struct { @@ -142,7 +135,12 @@ public: GLsizei height; } scissor; - Viewport viewport; + struct { + GLint x; + GLint y; + GLsizei width; + GLsizei height; + } viewport; std::array clip_distance; // GL_CLIP_DISTANCE diff --git a/src/video_core/renderer_opengl/gl_surface_params.cpp b/src/video_core/renderer_opengl/gl_surface_params.cpp new file mode 100644 index 000000000..ceb0359ec --- /dev/null +++ b/src/video_core/renderer_opengl/gl_surface_params.cpp @@ -0,0 +1,171 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/alignment.h" +#include "video_core/renderer_opengl/gl_rasterizer_cache.h" +#include "video_core/renderer_opengl/gl_surface_params.h" + +namespace OpenGL { + +SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const { + SurfaceParams params = *this; + const u32 tiled_size = is_tiled ? 8 : 1; + const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size); + PAddr aligned_start = + addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes); + PAddr aligned_end = + addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes); + + if (aligned_end - aligned_start > stride_tiled_bytes) { + params.addr = aligned_start; + params.height = (aligned_end - aligned_start) / BytesInPixels(stride); + } else { + // 1 row + ASSERT(aligned_end - aligned_start == stride_tiled_bytes); + const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1); + aligned_start = + addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment); + aligned_end = + addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment); + params.addr = aligned_start; + params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size; + params.stride = params.width; + params.height = tiled_size; + } + params.UpdateParams(); + + return params; +} + +SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle unscaled_rect) const { + if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) { + return {}; + } + + if (is_tiled) { + unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8; + unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8; + unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8; + unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8; + } + + const u32 stride_tiled = !is_tiled ? stride : stride * 8; + + const u32 pixel_offset = + stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) + + unscaled_rect.left; + + const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth(); + + return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)}; +} + +SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const { + SurfaceInterval result{}; + const auto valid_regions = + SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions; + for (auto& valid_interval : valid_regions) { + const SurfaceInterval aligned_interval{ + addr + Common::AlignUp(boost::icl::first(valid_interval) - addr, + BytesInPixels(is_tiled ? 8 * 8 : 1)), + addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr, + BytesInPixels(is_tiled ? 8 * 8 : 1))}; + + if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) || + boost::icl::length(aligned_interval) == 0) { + continue; + } + + // Get the rectangle within aligned_interval + const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1); + SurfaceInterval rect_interval{ + addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes), + addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes), + }; + if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) { + // 1 row + rect_interval = aligned_interval; + } else if (boost::icl::length(rect_interval) == 0) { + // 2 rows that do not make a rectangle, return the larger one + const SurfaceInterval row1{boost::icl::first(aligned_interval), + boost::icl::first(rect_interval)}; + const SurfaceInterval row2{boost::icl::first(rect_interval), + boost::icl::last_next(aligned_interval)}; + rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2; + } + + if (boost::icl::length(rect_interval) > boost::icl::length(result)) { + result = rect_interval; + } + } + return result; +} + +Common::Rectangle SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const { + const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr); + + if (is_tiled) { + const int x0 = (begin_pixel_index % (stride * 8)) / 8; + const int y0 = (begin_pixel_index / (stride * 8)) * 8; + // Top to bottom + return Common::Rectangle(x0, height - y0, x0 + sub_surface.width, + height - (y0 + sub_surface.height)); + } + + const int x0 = begin_pixel_index % stride; + const int y0 = begin_pixel_index / stride; + // Bottom to top + return Common::Rectangle(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0); +} + +Common::Rectangle SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const { + auto rect = GetSubRect(sub_surface); + rect.left = rect.left * res_scale; + rect.right = rect.right * res_scale; + rect.top = rect.top * res_scale; + rect.bottom = rect.bottom * res_scale; + return rect; +} + +bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const { + return std::tie(other_surface.addr, other_surface.width, other_surface.height, + other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) == + std::tie(addr, width, height, stride, pixel_format, is_tiled) && + pixel_format != PixelFormat::Invalid; +} + +bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const { + return sub_surface.addr >= addr && sub_surface.end <= end && + sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid && + sub_surface.is_tiled == is_tiled && + (sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && + (sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) && + GetSubRect(sub_surface).right <= stride; +} + +bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const { + return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format && + addr <= expanded_surface.end && expanded_surface.addr <= end && + is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride && + (std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) % + BytesInPixels(stride * (is_tiled ? 8 : 1)) == + 0; +} + +bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const { + if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr || + end < texcopy_params.end) { + return false; + } + if (texcopy_params.width != texcopy_params.stride) { + const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1)); + return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && + texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 && + (texcopy_params.height == 1 || texcopy_params.stride == tile_stride) && + ((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride; + } + return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval(); +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_surface_params.h b/src/video_core/renderer_opengl/gl_surface_params.h new file mode 100644 index 000000000..56d33e0d4 --- /dev/null +++ b/src/video_core/renderer_opengl/gl_surface_params.h @@ -0,0 +1,229 @@ +// 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 "common/assert.h" +#include "common/math_util.h" +#include "core/hw/gpu.h" +#include "video_core/regs_framebuffer.h" +#include "video_core/regs_texturing.h" + +namespace OpenGL { + +struct CachedSurface; +using Surface = std::shared_ptr; + +using SurfaceInterval = boost::icl::right_open_interval; + +struct SurfaceParams { +private: + static constexpr std::array BPP_TABLE = { + 32, // RGBA8 + 24, // RGB8 + 16, // RGB5A1 + 16, // RGB565 + 16, // RGBA4 + 16, // IA8 + 16, // RG8 + 8, // I8 + 8, // A8 + 8, // IA4 + 4, // I4 + 4, // A4 + 4, // ETC1 + 8, // ETC1A4 + 16, // D16 + 0, + 24, // D24 + 32, // D24S8 + }; + +public: + enum class PixelFormat { + // First 5 formats are shared between textures and color buffers + RGBA8 = 0, + RGB8 = 1, + RGB5A1 = 2, + RGB565 = 3, + RGBA4 = 4, + + // Texture-only formats + IA8 = 5, + RG8 = 6, + I8 = 7, + A8 = 8, + IA4 = 9, + I4 = 10, + A4 = 11, + ETC1 = 12, + ETC1A4 = 13, + + // Depth buffer-only formats + D16 = 14, + // gap + D24 = 16, + D24S8 = 17, + + Invalid = 255, + }; + + enum class SurfaceType { + Color = 0, + Texture = 1, + Depth = 2, + DepthStencil = 3, + Fill = 4, + Invalid = 5 + }; + + static constexpr unsigned int GetFormatBpp(PixelFormat format) { + const auto format_idx = static_cast(format); + DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx); + return BPP_TABLE[format_idx]; + } + + unsigned int GetFormatBpp() const { + return GetFormatBpp(pixel_format); + } + + static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) { + return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid; + } + + static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) { + return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; + } + + static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) { + return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14) + : PixelFormat::Invalid; + } + + static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) { + switch (format) { + // RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat + case GPU::Regs::PixelFormat::RGB565: + return PixelFormat::RGB565; + case GPU::Regs::PixelFormat::RGB5A1: + return PixelFormat::RGB5A1; + default: + return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; + } + } + + static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) { + SurfaceType a_type = GetFormatType(pixel_format_a); + SurfaceType b_type = GetFormatType(pixel_format_b); + + if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) && + (b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) { + return true; + } + + if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) { + return true; + } + + if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) { + return true; + } + + return false; + } + + static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) { + if ((unsigned int)pixel_format < 5) { + return SurfaceType::Color; + } + + if ((unsigned int)pixel_format < 14) { + return SurfaceType::Texture; + } + + if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) { + return SurfaceType::Depth; + } + + if (pixel_format == PixelFormat::D24S8) { + return SurfaceType::DepthStencil; + } + + return SurfaceType::Invalid; + } + + /// Update the params "size", "end" and "type" from the already set "addr", "width", "height" + /// and "pixel_format" + void UpdateParams() { + if (stride == 0) { + stride = width; + } + type = GetFormatType(pixel_format); + size = !is_tiled ? BytesInPixels(stride * (height - 1) + width) + : BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8); + end = addr + size; + } + + SurfaceInterval GetInterval() const { + return SurfaceInterval(addr, end); + } + + // Returns the outer rectangle containing "interval" + SurfaceParams FromInterval(SurfaceInterval interval) const; + + SurfaceInterval GetSubRectInterval(Common::Rectangle unscaled_rect) const; + + // Returns the region of the biggest valid rectange within interval + SurfaceInterval GetCopyableInterval(const Surface& src_surface) const; + + u32 GetScaledWidth() const { + return width * res_scale; + } + + u32 GetScaledHeight() const { + return height * res_scale; + } + + Common::Rectangle GetRect() const { + return {0, height, width, 0}; + } + + Common::Rectangle GetScaledRect() const { + return {0, GetScaledHeight(), GetScaledWidth(), 0}; + } + + u32 PixelsInBytes(u32 size) const { + return size * CHAR_BIT / GetFormatBpp(pixel_format); + } + + u32 BytesInPixels(u32 pixels) const { + return pixels * GetFormatBpp(pixel_format) / CHAR_BIT; + } + + bool ExactMatch(const SurfaceParams& other_surface) const; + bool CanSubRect(const SurfaceParams& sub_surface) const; + bool CanExpand(const SurfaceParams& expanded_surface) const; + bool CanTexCopy(const SurfaceParams& texcopy_params) const; + + Common::Rectangle GetSubRect(const SurfaceParams& sub_surface) const; + Common::Rectangle GetScaledSubRect(const SurfaceParams& sub_surface) const; + + PAddr addr = 0; + PAddr end = 0; + u32 size = 0; + + u32 width = 0; + u32 height = 0; + u32 stride = 0; + u16 res_scale = 1; + + bool is_tiled = false; + PixelFormat pixel_format = PixelFormat::Invalid; + SurfaceType type = SurfaceType::Invalid; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index e7e0bb72f..5046895e0 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -32,7 +32,6 @@ #include "video_core/renderer_opengl/gl_vars.h" #include "video_core/renderer_opengl/post_processing_opengl.h" #include "video_core/renderer_opengl/renderer_opengl.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" #include "video_core/video_core.h" namespace Frontend { @@ -1179,14 +1178,10 @@ VideoCore::ResultStatus RendererOpenGL::Init() { RefreshRasterizerSetting(); - TextureFilterManager::GetInstance().Reset(); - return VideoCore::ResultStatus::Success; } /// Shutdown the renderer -void RendererOpenGL::ShutDown() { - TextureFilterManager::GetInstance().Destroy(); -} +void RendererOpenGL::ShutDown() {} } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp index 86b4a85b6..b70cc14f4 100644 --- a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp +++ b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp @@ -42,18 +42,17 @@ namespace OpenGL { -Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(scale_factor) { +Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_factor) { const OpenGLState cur_state = OpenGLState::GetCurState(); - const auto setup_temp_tex = [this, scale_factor](TempTex& texture, GLint internal_format, - GLint format) { + const auto setup_temp_tex = [this](TempTex& texture, GLint internal_format, GLint format) { texture.fbo.Create(); texture.tex.Create(); state.draw.draw_framebuffer = texture.fbo.handle; state.Apply(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_RECTANGLE, texture.tex.handle); - glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * scale_factor, - 1024 * scale_factor, 0, format, GL_HALF_FLOAT, nullptr); + glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * internal_scale_factor, + 1024 * internal_scale_factor, 0, format, GL_HALF_FLOAT, nullptr); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE, texture.tex.handle, 0); }; @@ -61,7 +60,6 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(sc setup_temp_tex(XY, GL_RG16F, GL_RG); vao.Create(); - out_fbo.Create(); for (std::size_t idx = 0; idx < samplers.size(); ++idx) { samplers[idx].Create(); @@ -86,30 +84,26 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(sc state.draw.shader_program = refine_program.handle; state.Apply(); glUniform1i(glGetUniformLocation(refine_program.handle, "LUMAD"), 1); + glUniform1f(glGetUniformLocation(refine_program.handle, "final_scale"), + static_cast(internal_scale_factor) / scale_factor); cur_state.Apply(); } -void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) { +void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle& src_rect, + GLuint dst_tex, const Common::Rectangle& dst_rect, + GLuint read_fb_handle, GLuint draw_fb_handle) { const OpenGLState cur_state = OpenGLState::GetCurState(); - OGLTexture src_tex; - src_tex.Create(); - - state.viewport = RectToViewport(rect); - - state.texture_units[0].texture_2d = src_tex.handle; + state.viewport = {static_cast(src_rect.left * internal_scale_factor), + static_cast(src_rect.bottom * internal_scale_factor), + static_cast(src_rect.GetWidth() * internal_scale_factor), + static_cast(src_rect.GetHeight() * internal_scale_factor)}; + state.texture_units[0].texture_2d = src_tex; state.draw.draw_framebuffer = XY.fbo.handle; state.draw.shader_program = gradient_x_program.handle; state.Apply(); - const FormatTuple tuple = GetFormatTuple(surface.pixel_format); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(surface.stride)); - glActiveTexture(GL_TEXTURE0); - glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0, - tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]); - glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_RECTANGLE, LUMAD.tex.handle); glActiveTexture(GL_TEXTURE2); @@ -124,14 +118,17 @@ void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; + state.draw.draw_framebuffer = draw_fb_handle; state.draw.shader_program = refine_program.handle; state.Apply(); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - cur_state.texture_units[0].texture_2d, 0); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); cur_state.Apply(); } diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h index 79a058fd5..9e89da816 100644 --- a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h +++ b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h @@ -6,29 +6,25 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" namespace OpenGL { -class Anime4kUltrafast : public TextureFilterInterface { +class Anime4kUltrafast : public TextureFilterBase { public: - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = "Anime4K Ultrafast"; - info.clamp_scale = {2, 2}; - info.constructor = std::make_unique; - return info; - } + static constexpr std::string_view NAME = "Anime4K Ultrafast"; - Anime4kUltrafast(u16 scale_factor); - void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) override; + explicit Anime4kUltrafast(u16 scale_factor); + void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) override; private: + static constexpr u8 internal_scale_factor = 2; + OpenGLState state{}; OGLVertexArray vao; - OGLFramebuffer out_fbo; struct TempTex { OGLTexture tex; diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag index 26f2526ba..4417b96f6 100644 --- a/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag +++ b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag @@ -8,6 +8,8 @@ uniform sampler2D HOOKED; uniform sampler2DRect LUMAD; uniform sampler2DRect LUMAG; +uniform float final_scale; + const float LINE_DETECT_THRESHOLD = 0.4; const float STRENGTH = 0.6; @@ -24,7 +26,7 @@ vec4 getAverage(vec4 cc, vec4 a, vec4 b, vec4 c) { #define GetRGBAL(offset) \ RGBAL(textureOffset(HOOKED, tex_coord, offset), \ - texture(LUMAD, clamp(gl_FragCoord.xy + offset, vec2(0.0), input_max)).x) + texture(LUMAD, clamp((gl_FragCoord.xy + offset) * final_scale, vec2(0.0), input_max)).x) float min3v(float a, float b, float c) { return min(min(a, b), c); diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp index 6c0bc1f82..ce039c211 100644 --- a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp +++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp @@ -10,45 +10,36 @@ namespace OpenGL { -Bicubic::Bicubic(u16 scale_factor) : TextureFilterInterface(scale_factor) { +Bicubic::Bicubic(u16 scale_factor) : TextureFilterBase(scale_factor) { program.Create(tex_coord_vert.data(), bicubic_frag.data()); vao.Create(); - draw_fbo.Create(); src_sampler.Create(); state.draw.shader_program = program.handle; state.draw.vertex_array = vao.handle; state.draw.shader_program = program.handle; - state.draw.draw_framebuffer = draw_fbo.handle; state.texture_units[0].sampler = src_sampler.handle; glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); -} +} // namespace OpenGL -void Bicubic::scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) { +void Bicubic::Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) { const OpenGLState cur_state = OpenGLState::GetCurState(); - - OGLTexture src_tex; - src_tex.Create(); - state.texture_units[0].texture_2d = src_tex.handle; - - state.viewport = RectToViewport(rect); + state.texture_units[0].texture_2d = src_tex; + state.draw.draw_framebuffer = draw_fb_handle; + state.viewport = {static_cast(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; state.Apply(); - const FormatTuple tuple = GetFormatTuple(surface.pixel_format); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(surface.stride)); - glActiveTexture(GL_TEXTURE0); - glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0, - tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - cur_state.texture_units[0].texture_2d, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); cur_state.Apply(); } diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h index a16bdafcf..b982cc973 100644 --- a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h +++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h @@ -6,27 +6,24 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" namespace OpenGL { -class Bicubic : public TextureFilterInterface { -public: - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = "Bicubic"; - info.constructor = std::make_unique; - return info; - } - Bicubic(u16 scale_factor); - void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) override; +class Bicubic : public TextureFilterBase { +public: + static constexpr std::string_view NAME = "Bicubic"; + + explicit Bicubic(u16 scale_factor); + void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) override; private: OpenGLState state{}; OGLProgram program{}; OGLVertexArray vao{}; - OGLFramebuffer draw_fbo{}; OGLSampler src_sampler{}; }; + } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_base.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_base.h new file mode 100644 index 000000000..126fdb108 --- /dev/null +++ b/src/video_core/renderer_opengl/texture_filters/texture_filter_base.h @@ -0,0 +1,26 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/common_types.h" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_surface_params.h" + +namespace OpenGL { + +class TextureFilterBase { + friend class TextureFilterer; + virtual void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) = 0; + +public: + explicit TextureFilterBase(u16 scale_factor) : scale_factor{scale_factor} {}; + virtual ~TextureFilterBase() = default; + + const u16 scale_factor{}; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h deleted file mode 100644 index c2bf2a686..000000000 --- a/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include "common/common_types.h" -#include "common/math_util.h" - -namespace OpenGL { - -struct CachedSurface; -struct Viewport; - -class TextureFilterInterface { -public: - const u16 scale_factor{}; - TextureFilterInterface(u16 scale_factor) : scale_factor{scale_factor} {} - virtual void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) = 0; - virtual ~TextureFilterInterface() = default; - -protected: - Viewport RectToViewport(const Common::Rectangle& rect); -}; - -// every texture filter should have a static GetInfo function -struct TextureFilterInfo { - std::string_view name; - struct { - u16 min, max; - } clamp_scale{1, 10}; - std::function(u16 scale_factor)> constructor; -}; - -} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp deleted file mode 100644 index 77d07111e..000000000 --- a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include "common/logging/log.h" -#include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h" -#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" -#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h" - -namespace OpenGL { - -Viewport TextureFilterInterface::RectToViewport(const Common::Rectangle& rect) { - return { - static_cast(rect.left) * scale_factor, - static_cast(rect.top) * scale_factor, - static_cast(rect.GetWidth()) * scale_factor, - static_cast(rect.GetHeight()) * scale_factor, - }; -} - -namespace { -template -std::pair FilterMapPair() { - return {T::GetInfo().name, T::GetInfo()}; -}; - -struct NoFilter { - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = TextureFilterManager::NONE; - info.clamp_scale = {1, 1}; - info.constructor = [](u16) { return nullptr; }; - return info; - } -}; -} // namespace - -const std::map& -TextureFilterManager::TextureFilterMap() { - static const std::map filter_map{ - FilterMapPair(), - FilterMapPair(), - FilterMapPair(), - FilterMapPair(), - }; - return filter_map; -} - -void TextureFilterManager::SetTextureFilter(std::string filter_name, u16 new_scale_factor) { - if (name == filter_name && scale_factor == new_scale_factor) - return; - std::lock_guard lock{mutex}; - name = std::move(filter_name); - scale_factor = new_scale_factor; - updated = true; -} - -TextureFilterInterface* TextureFilterManager::GetTextureFilter() const { - return filter.get(); -} - -bool TextureFilterManager::IsUpdated() const { - return updated; -} - -void TextureFilterManager::Reset() { - std::lock_guard lock{mutex}; - updated = false; - auto iter = TextureFilterMap().find(name); - if (iter == TextureFilterMap().end()) { - LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", name); - filter = nullptr; - return; - } - - const auto& filter_info = iter->second; - - u16 clamped_scale = - std::clamp(scale_factor, filter_info.clamp_scale.min, filter_info.clamp_scale.max); - if (clamped_scale != scale_factor) - LOG_ERROR(Render_OpenGL, "Invalid scale factor {} for texture filter {}, clamped to {}", - scale_factor, filter_info.name, clamped_scale); - - filter = filter_info.constructor(clamped_scale); -} - -} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h deleted file mode 100644 index 1299a9151..000000000 --- a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" - -namespace OpenGL { - -class TextureFilterManager { -public: - static constexpr std::string_view NONE = "none"; - struct FilterNameComp { - bool operator()(const std::string_view a, const std::string_view b) const { - bool na = a == NONE; - bool nb = b == NONE; - if (na | nb) - return na & !nb; - return a < b; - } - }; - // function ensures map is initialized before use - static const std::map& TextureFilterMap(); - - static TextureFilterManager& GetInstance() { - static TextureFilterManager singleton; - return singleton; - } - - void Destroy() { - filter.reset(); - } - void SetTextureFilter(std::string filter_name, u16 new_scale_factor); - TextureFilterInterface* GetTextureFilter() const; - // returns true if filter has been changed and a cache reset is needed - bool IsUpdated() const; - void Reset(); - -private: - std::atomic updated{false}; - std::mutex mutex; - std::string name{"none"}; - u16 scale_factor{1}; - - std::unique_ptr filter; -}; - -} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp b/src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp new file mode 100644 index 000000000..0afa6b876 --- /dev/null +++ b/src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp @@ -0,0 +1,86 @@ +/// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/logging/log.h" +#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h" +#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" +#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h" + +namespace OpenGL { + +namespace { + +using TextureFilterContructor = std::function(u16)>; + +template +std::pair FilterMapPair() { + return {T::NAME, std::make_unique}; +}; + +static const std::unordered_map filter_map{ + {TextureFilterer::NONE, [](u16) { return nullptr; }}, + FilterMapPair(), + FilterMapPair(), + FilterMapPair(), +}; + +} // namespace + +TextureFilterer::TextureFilterer(std::string_view filter_name, u16 scale_factor) { + Reset(filter_name, scale_factor); +} + +bool TextureFilterer::Reset(std::string_view new_filter_name, u16 new_scale_factor) { + if (filter_name == new_filter_name && (IsNull() || filter->scale_factor == new_scale_factor)) + return false; + + auto iter = filter_map.find(new_filter_name); + if (iter == filter_map.end()) { + LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", new_filter_name); + filter = nullptr; + return true; + } + + filter_name = iter->first; + filter = iter->second(new_scale_factor); + return true; +} + +bool TextureFilterer::IsNull() const { + return !filter; +} + +bool TextureFilterer::Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, + SurfaceParams::SurfaceType type, GLuint read_fb_handle, + GLuint draw_fb_handle) { + // depth / stencil texture filtering is not supported for now + if (IsNull() || + (type != SurfaceParams::SurfaceType::Color && type != SurfaceParams::SurfaceType::Texture)) + return false; + filter->Filter(src_tex, src_rect, dst_tex, dst_rect, read_fb_handle, draw_fb_handle); + return true; +} + +std::vector TextureFilterer::GetFilterNames() { + std::vector ret; + std::transform(filter_map.begin(), filter_map.end(), std::back_inserter(ret), + [](auto pair) { return pair.first; }); + std::sort(ret.begin(), ret.end(), [](std::string_view lhs, std::string_view rhs) { + // sort lexicographically with none at the top + bool lhs_is_none{lhs == NONE}; + bool rhs_is_none{rhs == NONE}; + if (lhs_is_none || rhs_is_none) + return lhs_is_none && !rhs_is_none; + return lhs < rhs; + }); + return ret; +} + +} // namespace OpenGL \ No newline at end of file diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filterer.h b/src/video_core/renderer_opengl/texture_filters/texture_filterer.h new file mode 100644 index 000000000..de3666356 --- /dev/null +++ b/src/video_core/renderer_opengl/texture_filters/texture_filterer.h @@ -0,0 +1,39 @@ +// 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 +#include "common/common_types.h" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_surface_params.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" + +namespace OpenGL { + +class TextureFilterer { +public: + static constexpr std::string_view NONE = "none"; + + explicit TextureFilterer(std::string_view filter_name, u16 scale_factor); + // returns true if the filter actually changed + bool Reset(std::string_view new_filter_name, u16 new_scale_factor); + // returns true if there is no active filter + bool IsNull() const; + // returns true if the texture was able to be filtered + bool Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, SurfaceParams::SurfaceType type, + GLuint read_fb_handle, GLuint draw_fb_handle); + + static std::vector GetFilterNames(); + +private: + std::string_view filter_name = NONE; + std::unique_ptr filter; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp index 5e7c34d1d..b1dcefc03 100644 --- a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp +++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp @@ -48,12 +48,11 @@ namespace OpenGL { -XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_factor) { +XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterBase(scale_factor) { const OpenGLState cur_state = OpenGLState::GetCurState(); program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data()); vao.Create(); - draw_fbo.Create(); src_sampler.Create(); state.draw.shader_program = program.handle; @@ -68,31 +67,24 @@ XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_fa cur_state.Apply(); state.draw.vertex_array = vao.handle; state.draw.shader_program = program.handle; - state.draw.draw_framebuffer = draw_fbo.handle; state.texture_units[0].sampler = src_sampler.handle; } -void XbrzFreescale::scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) { +void XbrzFreescale::Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) { const OpenGLState cur_state = OpenGLState::GetCurState(); - OGLTexture src_tex; - src_tex.Create(); - state.texture_units[0].texture_2d = src_tex.handle; - - state.viewport = RectToViewport(rect); + state.texture_units[0].texture_2d = src_tex; + state.draw.draw_framebuffer = draw_fb_handle; + state.viewport = {static_cast(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; state.Apply(); - const FormatTuple tuple = GetFormatTuple(surface.pixel_format); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(surface.stride)); - glActiveTexture(GL_TEXTURE0); - glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0, - tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - cur_state.texture_units[0].texture_2d, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); cur_state.Apply(); } diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h index aad10f308..02c6d5d7e 100644 --- a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h +++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h @@ -6,28 +6,23 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" namespace OpenGL { -class XbrzFreescale : public TextureFilterInterface { +class XbrzFreescale : public TextureFilterBase { public: - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = "xBRZ freescale"; - info.constructor = std::make_unique; - return info; - } + static constexpr std::string_view NAME = "xBRZ freescale"; - XbrzFreescale(u16 scale_factor); - void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) override; + explicit XbrzFreescale(u16 scale_factor); + void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) override; private: OpenGLState state{}; OGLProgram program{}; OGLVertexArray vao{}; - OGLFramebuffer draw_fbo{}; OGLSampler src_sampler{}; }; } // namespace OpenGL diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index a7ed5fb9e..1748efcc6 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -26,6 +26,7 @@ std::atomic g_use_disk_shader_cache; std::atomic g_renderer_bg_color_update_requested; std::atomic g_renderer_sampler_update_requested; std::atomic g_renderer_shader_update_requested; +std::atomic g_texture_filter_update_requested; // Screenshot std::atomic g_renderer_screenshot_requested; void* g_screenshot_bits; diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h index f11b67839..9951cd9a7 100644 --- a/src/video_core/video_core.h +++ b/src/video_core/video_core.h @@ -35,6 +35,7 @@ extern std::atomic g_use_disk_shader_cache; extern std::atomic g_renderer_bg_color_update_requested; extern std::atomic g_renderer_sampler_update_requested; extern std::atomic g_renderer_shader_update_requested; +extern std::atomic g_texture_filter_update_requested; // Screenshot extern std::atomic g_renderer_screenshot_requested; extern void* g_screenshot_bits; From d37b0476ad586e865aebab91b74eaef0e1a76c78 Mon Sep 17 00:00:00 2001 From: Marshall Mohror Date: Tue, 7 Apr 2020 09:12:32 -0500 Subject: [PATCH 30/37] video_core/renderer_opengl/gl_rasterizer_cache: Create Format Reinterpretation Framework (#5170) * video_core/renderer_opengl/gl_rasterizer_cache: Create Format Reinterpretation Framework Adds RGBA4 -> RGB5A1 reinterpretation commonly used by virtual console If no matching surface can be found, ValidateSurface checks for a surface in the cache which is reinterpretable to the requested format. If that fails, the cache is checked for any surface with a matching bit-width. If one is found, the region is flushed. If not, the region is checked against dirty_regions to see if it was created entirely on the GPU. If not, then the surface is flushed. Co-Authored-By: James Rowe Co-Authored-By: Ben temporary change to avoid merge conflicts with video dumping * re-add D24S8->RGBA8 res_scale hack * adress review comments * fix dirty region check * check for surfaces with invalid pixel format, and break logic into separate functions --- src/video_core/CMakeLists.txt | 3 + .../gl_format_reinterpreter.cpp | 238 +++++++++++++++++ .../renderer_opengl/gl_format_reinterpreter.h | 62 +++++ .../renderer_opengl/gl_rasterizer_cache.cpp | 246 +++++++++--------- .../renderer_opengl/gl_rasterizer_cache.h | 23 +- .../renderer_opengl/gl_surface_params.h | 41 +++ 6 files changed, 480 insertions(+), 133 deletions(-) create mode 100644 src/video_core/renderer_opengl/gl_format_reinterpreter.cpp create mode 100644 src/video_core/renderer_opengl/gl_format_reinterpreter.h diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index b402282af..38642935f 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -63,6 +63,9 @@ add_library(video_core STATIC renderer_opengl/texture_filters/texture_filterer.h renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp renderer_opengl/texture_filters/xbrz/xbrz_freescale.h + #temporary, move these back in alphabetical order before merging + renderer_opengl/gl_format_reinterpreter.cpp + renderer_opengl/gl_format_reinterpreter.h shader/debug_data.h shader/shader.cpp shader/shader.h diff --git a/src/video_core/renderer_opengl/gl_format_reinterpreter.cpp b/src/video_core/renderer_opengl/gl_format_reinterpreter.cpp new file mode 100644 index 000000000..2175c62bd --- /dev/null +++ b/src/video_core/renderer_opengl/gl_format_reinterpreter.cpp @@ -0,0 +1,238 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/assert.h" +#include "common/scope_exit.h" +#include "video_core/renderer_opengl/gl_format_reinterpreter.h" +#include "video_core/renderer_opengl/gl_rasterizer_cache.h" +#include "video_core/renderer_opengl/gl_state.h" +#include "video_core/renderer_opengl/gl_vars.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" + +namespace OpenGL { + +using PixelFormat = SurfaceParams::PixelFormat; + +class RGBA4toRGB5A1 final : public FormatReinterpreterBase { +public: + RGBA4toRGB5A1() { + constexpr std::string_view vs_source = R"( +out vec2 dst_coord; + +uniform mediump ivec2 dst_size; + +const vec2 vertices[4] = + vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)); + +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + dst_coord = (vertices[gl_VertexID] / 2.0 + 0.5) * vec2(dst_size); +} +)"; + + constexpr std::string_view fs_source = R"( +in mediump vec2 dst_coord; + +out lowp vec4 frag_color; + +uniform lowp sampler2D source; +uniform mediump ivec2 dst_size; +uniform mediump ivec2 src_size; +uniform mediump ivec2 src_offset; + +void main() { + mediump ivec2 tex_coord; + if (src_size == dst_size) { + tex_coord = ivec2(dst_coord); + } else { + highp int tex_index = int(dst_coord.y) * dst_size.x + int(dst_coord.x); + mediump int y = tex_index / src_size.x; + tex_coord = ivec2(tex_index - y * src_size.x, y); + } + tex_coord -= src_offset; + + lowp ivec4 rgba4 = ivec4(texelFetch(source, tex_coord, 0) * (exp2(4.0) - 1.0)); + lowp ivec3 rgb5 = + ((rgba4.rgb << ivec3(1, 2, 3)) | (rgba4.gba >> ivec3(3, 2, 1))) & 0x1F; + frag_color = vec4(vec3(rgb5) / (exp2(5.0) - 1.0), rgba4.a & 0x01); +} +)"; + + program.Create(vs_source.data(), fs_source.data()); + dst_size_loc = glGetUniformLocation(program.handle, "dst_size"); + src_size_loc = glGetUniformLocation(program.handle, "src_size"); + src_offset_loc = glGetUniformLocation(program.handle, "src_offset"); + vao.Create(); + } + + void Reinterpret(GLuint src_tex, const Common::Rectangle& src_rect, GLuint read_fb_handle, + GLuint dst_tex, const Common::Rectangle& dst_rect, + GLuint draw_fb_handle) override { + OpenGLState prev_state = OpenGLState::GetCurState(); + SCOPE_EXIT({ prev_state.Apply(); }); + + OpenGLState state; + state.texture_units[0].texture_2d = src_tex; + state.draw.draw_framebuffer = draw_fb_handle; + state.draw.shader_program = program.handle; + state.draw.vertex_array = vao.handle; + state.viewport = {static_cast(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; + state.Apply(); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, + 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, + 0); + + glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight()); + glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight()); + glUniform2i(src_offset_loc, src_rect.left, src_rect.bottom); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + } + +private: + OGLProgram program; + GLint dst_size_loc{-1}, src_size_loc{-1}, src_offset_loc{-1}; + OGLVertexArray vao; +}; + +class PixelBufferD24S8toABGR final : public FormatReinterpreterBase { +public: + PixelBufferD24S8toABGR() { + attributeless_vao.Create(); + d24s8_abgr_buffer.Create(); + d24s8_abgr_buffer_size = 0; + + constexpr std::string_view vs_source = R"( +const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), + vec2(-1.0, 1.0), vec2(1.0, 1.0)); +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); +} +)"; + + std::string fs_source = GLES ? fragment_shader_precision_OES : ""; + fs_source += R"( +uniform samplerBuffer tbo; +uniform vec2 tbo_size; +uniform vec4 viewport; + +out vec4 color; + +void main() { + vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw; + int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x); + color = texelFetch(tbo, tbo_offset).rabg; +} +)"; + d24s8_abgr_shader.Create(vs_source.data(), fs_source.c_str()); + + OpenGLState state = OpenGLState::GetCurState(); + GLuint old_program = state.draw.shader_program; + state.draw.shader_program = d24s8_abgr_shader.handle; + state.Apply(); + + GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo"); + ASSERT(tbo_u_id != -1); + glUniform1i(tbo_u_id, 0); + + state.draw.shader_program = old_program; + state.Apply(); + + d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size"); + ASSERT(d24s8_abgr_tbo_size_u_id != -1); + d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport"); + ASSERT(d24s8_abgr_viewport_u_id != -1); + } + + ~PixelBufferD24S8toABGR() {} + + void Reinterpret(GLuint src_tex, const Common::Rectangle& src_rect, GLuint read_fb_handle, + GLuint dst_tex, const Common::Rectangle& dst_rect, + GLuint draw_fb_handle) override { + OpenGLState prev_state = OpenGLState::GetCurState(); + SCOPE_EXIT({ prev_state.Apply(); }); + + OpenGLState state; + state.draw.read_framebuffer = read_fb_handle; + state.draw.draw_framebuffer = draw_fb_handle; + state.Apply(); + + glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle); + + GLsizeiptr target_pbo_size = + static_cast(src_rect.GetWidth()) * src_rect.GetHeight() * 4; + if (target_pbo_size > d24s8_abgr_buffer_size) { + d24s8_abgr_buffer_size = target_pbo_size * 2; + glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY); + } + + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, + src_tex, 0); + glReadPixels(static_cast(src_rect.left), static_cast(src_rect.bottom), + static_cast(src_rect.GetWidth()), + static_cast(src_rect.GetHeight()), GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, 0); + + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + // PBO now contains src_tex in RABG format + state.draw.shader_program = d24s8_abgr_shader.handle; + state.draw.vertex_array = attributeless_vao.handle; + state.viewport.x = static_cast(dst_rect.left); + state.viewport.y = static_cast(dst_rect.bottom); + state.viewport.width = static_cast(dst_rect.GetWidth()); + state.viewport.height = static_cast(dst_rect.GetHeight()); + state.Apply(); + + OGLTexture tbo; + tbo.Create(); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_BUFFER, tbo.handle); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle); + + glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast(src_rect.GetWidth()), + static_cast(src_rect.GetHeight())); + glUniform4f(d24s8_abgr_viewport_u_id, static_cast(state.viewport.x), + static_cast(state.viewport.y), + static_cast(state.viewport.width), + static_cast(state.viewport.height)); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, + 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, + 0); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + glBindTexture(GL_TEXTURE_BUFFER, 0); + } + +private: + OGLVertexArray attributeless_vao; + OGLBuffer d24s8_abgr_buffer; + GLsizeiptr d24s8_abgr_buffer_size; + OGLProgram d24s8_abgr_shader; + GLint d24s8_abgr_tbo_size_u_id; + GLint d24s8_abgr_viewport_u_id; +}; + +FormatReinterpreterOpenGL::FormatReinterpreterOpenGL() { + reinterpreters.emplace(PixelFormatPair{PixelFormat::RGBA8, PixelFormat::D24S8}, + std::make_unique()); + reinterpreters.emplace(PixelFormatPair{PixelFormat::RGB5A1, PixelFormat::RGBA4}, + std::make_unique()); +} + +FormatReinterpreterOpenGL::~FormatReinterpreterOpenGL() = default; + +std::pair +FormatReinterpreterOpenGL::GetPossibleReinterpretations(PixelFormat dst_format) { + return reinterpreters.equal_range(dst_format); +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_format_reinterpreter.h b/src/video_core/renderer_opengl/gl_format_reinterpreter.h new file mode 100644 index 000000000..d4b544096 --- /dev/null +++ b/src/video_core/renderer_opengl/gl_format_reinterpreter.h @@ -0,0 +1,62 @@ +// 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 "common/common_types.h" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_resource_manager.h" +#include "video_core/renderer_opengl/gl_surface_params.h" + +namespace OpenGL { + +class RasterizerCacheOpenGL; + +struct PixelFormatPair { + const SurfaceParams::PixelFormat dst_format, src_format; + struct less { + using is_transparent = void; + constexpr bool operator()(OpenGL::PixelFormatPair lhs, OpenGL::PixelFormatPair rhs) const { + return std::tie(lhs.dst_format, lhs.src_format) < + std::tie(rhs.dst_format, rhs.src_format); + } + constexpr bool operator()(OpenGL::SurfaceParams::PixelFormat lhs, + OpenGL::PixelFormatPair rhs) const { + return lhs < rhs.dst_format; + } + constexpr bool operator()(OpenGL::PixelFormatPair lhs, + OpenGL::SurfaceParams::PixelFormat rhs) const { + return lhs.dst_format < rhs; + } + }; +}; + +class FormatReinterpreterBase { +public: + virtual ~FormatReinterpreterBase() = default; + + virtual void Reinterpret(GLuint src_tex, const Common::Rectangle& src_rect, + GLuint read_fb_handle, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint draw_fb_handle) = 0; +}; + +class FormatReinterpreterOpenGL : NonCopyable { + using ReinterpreterMap = + std::map, PixelFormatPair::less>; + +public: + explicit FormatReinterpreterOpenGL(); + ~FormatReinterpreterOpenGL(); + + std::pair GetPossibleReinterpretations( + SurfaceParams::PixelFormat dst_format); + +private: + ReinterpreterMap reinterpreters; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index 868bcaecd..b8aaabf96 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -32,6 +32,7 @@ #include "core/settings.h" #include "video_core/pica_state.h" #include "video_core/renderer_base.h" +#include "video_core/renderer_opengl/gl_format_reinterpreter.h" #include "video_core/renderer_opengl/gl_rasterizer_cache.h" #include "video_core/renderer_opengl/gl_state.h" #include "video_core/renderer_opengl/gl_vars.h" @@ -1063,54 +1064,10 @@ RasterizerCacheOpenGL::RasterizerCacheOpenGL() { resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); texture_filterer = std::make_unique(Settings::values.texture_filter_name, resolution_scale_factor); + format_reinterpreter = std::make_unique(); read_framebuffer.Create(); draw_framebuffer.Create(); - - attributeless_vao.Create(); - - d24s8_abgr_buffer.Create(); - d24s8_abgr_buffer_size = 0; - - std::string vs_source = R"( -const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)); -void main() { - gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); -} -)"; - - std::string fs_source = GLES ? fragment_shader_precision_OES : ""; - fs_source += R"( -uniform samplerBuffer tbo; -uniform vec2 tbo_size; -uniform vec4 viewport; - -out vec4 color; - -void main() { - vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw; - int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x); - color = texelFetch(tbo, tbo_offset).rabg; -} -)"; - d24s8_abgr_shader.Create(vs_source.c_str(), fs_source.c_str()); - - OpenGLState state = OpenGLState::GetCurState(); - GLuint old_program = state.draw.shader_program; - state.draw.shader_program = d24s8_abgr_shader.handle; - state.Apply(); - - GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo"); - ASSERT(tbo_u_id != -1); - glUniform1i(tbo_u_id, 0); - - state.draw.shader_program = old_program; - state.Apply(); - - d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size"); - ASSERT(d24s8_abgr_tbo_size_u_id != -1); - d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport"); - ASSERT(d24s8_abgr_viewport_u_id != -1); } RasterizerCacheOpenGL::~RasterizerCacheOpenGL() { @@ -1136,64 +1093,6 @@ bool RasterizerCacheOpenGL::BlitSurfaces(const Surface& src_surface, draw_framebuffer.handle); } -void RasterizerCacheOpenGL::ConvertD24S8toABGR(GLuint src_tex, - const Common::Rectangle& src_rect, - GLuint dst_tex, - const Common::Rectangle& dst_rect) { - OpenGLState prev_state = OpenGLState::GetCurState(); - SCOPE_EXIT({ prev_state.Apply(); }); - - OpenGLState state; - state.draw.read_framebuffer = read_framebuffer.handle; - state.draw.draw_framebuffer = draw_framebuffer.handle; - state.Apply(); - - glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle); - - GLsizeiptr target_pbo_size = src_rect.GetWidth() * src_rect.GetHeight() * 4; - if (target_pbo_size > d24s8_abgr_buffer_size) { - d24s8_abgr_buffer_size = target_pbo_size * 2; - glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY); - } - - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, src_tex, - 0); - glReadPixels(static_cast(src_rect.left), static_cast(src_rect.bottom), - static_cast(src_rect.GetWidth()), - static_cast(src_rect.GetHeight()), GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, - 0); - - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - - // PBO now contains src_tex in RABG format - state.draw.shader_program = d24s8_abgr_shader.handle; - state.draw.vertex_array = attributeless_vao.handle; - state.viewport.x = static_cast(dst_rect.left); - state.viewport.y = static_cast(dst_rect.bottom); - state.viewport.width = static_cast(dst_rect.GetWidth()); - state.viewport.height = static_cast(dst_rect.GetHeight()); - state.Apply(); - - OGLTexture tbo; - tbo.Create(); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_BUFFER, tbo.handle); - glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle); - - glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast(src_rect.GetWidth()), - static_cast(src_rect.GetHeight())); - glUniform4f(d24s8_abgr_viewport_u_id, static_cast(state.viewport.x), - static_cast(state.viewport.y), static_cast(state.viewport.width), - static_cast(state.viewport.height)); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glBindTexture(GL_TEXTURE_BUFFER, 0); -} - Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale, bool load_if_create) { if (params.addr == 0 || params.height * params.width == 0) { @@ -1721,9 +1620,15 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr, return; } + auto validate_regions = surface->invalid_regions & validate_interval; + auto notify_validated = [&](SurfaceInterval interval) { + surface->invalid_regions.erase(interval); + validate_regions.erase(interval); + }; + while (true) { - const auto it = surface->invalid_regions.find(validate_interval); - if (it == surface->invalid_regions.end()) + const auto it = validate_regions.begin(); + if (it == validate_regions.end()) break; const auto interval = *it & validate_interval; @@ -1735,27 +1640,27 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr, if (copy_surface != nullptr) { SurfaceInterval copy_interval = params.GetCopyableInterval(copy_surface); CopySurface(copy_surface, surface, copy_interval); - surface->invalid_regions.erase(copy_interval); + notify_validated(copy_interval); continue; } - // D24S8 to RGBA8 - if (surface->pixel_format == PixelFormat::RGBA8) { - params.pixel_format = PixelFormat::D24S8; - Surface reinterpret_surface = - FindMatch(surface_cache, params, ScaleMatch::Ignore, interval); - if (reinterpret_surface != nullptr) { - ASSERT(reinterpret_surface->pixel_format == PixelFormat::D24S8); - - SurfaceInterval convert_interval = params.GetCopyableInterval(reinterpret_surface); - SurfaceParams convert_params = surface->FromInterval(convert_interval); - auto src_rect = reinterpret_surface->GetScaledSubRect(convert_params); - auto dest_rect = surface->GetScaledSubRect(convert_params); - - ConvertD24S8toABGR(reinterpret_surface->texture.handle, src_rect, - surface->texture.handle, dest_rect); - - surface->invalid_regions.erase(convert_interval); + // Try to find surface in cache with different format + // that can can be reinterpreted to the requested format. + if (ValidateByReinterpretation(surface, params, interval)) { + notify_validated(interval); + continue; + } + // Could not find a matching reinterpreter, check if we need to implement a + // reinterpreter + if (NoUnimplementedReinterpretations(surface, params, interval) && + !IntervalHasInvalidPixelFormat(params, interval)) { + // No surfaces were found in the cache that had a matching bit-width. + // If the region was created entirely on the GPU, + // assume it was a developer mistake and skip flushing. + if (boost::icl::contains(dirty_regions, interval)) { + LOG_DEBUG(Render_OpenGL, "Region created fully on GPU and reinterpretation is " + "invalid. Skipping validation"); + validate_regions.erase(interval); continue; } } @@ -1765,10 +1670,103 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr, surface->LoadGLBuffer(params.addr, params.end); surface->UploadGLTexture(surface->GetSubRect(params), read_framebuffer.handle, draw_framebuffer.handle); - surface->invalid_regions.erase(params.GetInterval()); + notify_validated(params.GetInterval()); } } +bool RasterizerCacheOpenGL::NoUnimplementedReinterpretations(const Surface& surface, + SurfaceParams& params, + const SurfaceInterval& interval) { + static constexpr std::array all_formats{ + PixelFormat::RGBA8, PixelFormat::RGB8, PixelFormat::RGB5A1, PixelFormat::RGB565, + PixelFormat::RGBA4, PixelFormat::IA8, PixelFormat::RG8, PixelFormat::I8, + PixelFormat::A8, PixelFormat::IA4, PixelFormat::I4, PixelFormat::A4, + PixelFormat::ETC1, PixelFormat::ETC1A4, PixelFormat::D16, PixelFormat::D24, + PixelFormat::D24S8, + }; + bool implemented = true; + for (PixelFormat format : all_formats) { + if (SurfaceParams::GetFormatBpp(format) == surface->GetFormatBpp()) { + params.pixel_format = format; + // This could potentially be expensive, + // although experimentally it hasn't been too bad + Surface test_surface = + FindMatch(surface_cache, params, ScaleMatch::Ignore, interval); + if (test_surface != nullptr) { + LOG_WARNING(Render_OpenGL, "Missing pixel_format reinterpreter: {} -> {}", + SurfaceParams::PixelFormatAsString(format), + SurfaceParams::PixelFormatAsString(surface->pixel_format)); + implemented = false; + } + } + } + return implemented; +} + +bool RasterizerCacheOpenGL::IntervalHasInvalidPixelFormat(SurfaceParams& params, + const SurfaceInterval& interval) { + params.pixel_format = PixelFormat::Invalid; + for (const auto& set : RangeFromInterval(surface_cache, interval)) + for (const auto& surface : set.second) + if (surface->pixel_format == PixelFormat::Invalid) { + LOG_WARNING(Render_OpenGL, "Surface found with invalid pixel format"); + return true; + } + return false; +} + +bool RasterizerCacheOpenGL::ValidateByReinterpretation(const Surface& surface, + SurfaceParams& params, + const SurfaceInterval& interval) { + auto [cvt_begin, cvt_end] = + format_reinterpreter->GetPossibleReinterpretations(surface->pixel_format); + for (auto reinterpreter = cvt_begin; reinterpreter != cvt_end; ++reinterpreter) { + PixelFormat format = reinterpreter->first.src_format; + params.pixel_format = format; + Surface reinterpret_surface = + FindMatch(surface_cache, params, ScaleMatch::Ignore, interval); + + if (reinterpret_surface != nullptr) { + SurfaceInterval reinterpret_interval = params.GetCopyableInterval(reinterpret_surface); + SurfaceParams reinterpret_params = surface->FromInterval(reinterpret_interval); + auto src_rect = reinterpret_surface->GetScaledSubRect(reinterpret_params); + auto dest_rect = surface->GetScaledSubRect(reinterpret_params); + + if (!texture_filterer->IsNull() && reinterpret_surface->res_scale == 1 && + surface->res_scale == resolution_scale_factor) { + // The destination surface is either a framebuffer, or a filtered texture. + OGLTexture tmp_tex; + tmp_tex.Create(); + // Create an intermediate surface to convert to before blitting to the + // destination. + Common::Rectangle tmp_rect{0, dest_rect.GetHeight() / resolution_scale_factor, + dest_rect.GetWidth() / resolution_scale_factor, 0}; + AllocateSurfaceTexture(tmp_tex.handle, + GetFormatTuple(reinterpreter->first.dst_format), + tmp_rect.right, tmp_rect.top); + reinterpreter->second->Reinterpret(reinterpret_surface->texture.handle, src_rect, + read_framebuffer.handle, tmp_tex.handle, + tmp_rect, draw_framebuffer.handle); + SurfaceParams::SurfaceType type = + SurfaceParams::GetFormatType(reinterpreter->first.dst_format); + + if (!texture_filterer->Filter(tmp_tex.handle, tmp_rect, surface->texture.handle, + dest_rect, type, read_framebuffer.handle, + draw_framebuffer.handle)) { + BlitTextures(tmp_tex.handle, tmp_rect, surface->texture.handle, dest_rect, type, + read_framebuffer.handle, draw_framebuffer.handle); + } + } else { + reinterpreter->second->Reinterpret(reinterpret_surface->texture.handle, src_rect, + read_framebuffer.handle, surface->texture.handle, + dest_rect, draw_framebuffer.handle); + } + return true; + } + } + return false; +} + void RasterizerCacheOpenGL::FlushRegion(PAddr addr, u32 size, Surface flush_surface) { if (size == 0) return; diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h index dffcbb05c..24dd9f594 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h @@ -34,6 +34,7 @@ namespace OpenGL { class RasterizerCacheOpenGL; class TextureFilterer; +class FormatReinterpreterOpenGL; struct TextureCubeConfig { PAddr px; @@ -240,9 +241,6 @@ public: bool BlitSurfaces(const Surface& src_surface, const Common::Rectangle& src_rect, const Surface& dst_surface, const Common::Rectangle& dst_rect); - void ConvertD24S8toABGR(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, - const Common::Rectangle& dst_rect); - /// Copy one surface's region to another void CopySurface(const Surface& src_surface, const Surface& dst_surface, SurfaceInterval copy_interval); @@ -288,6 +286,18 @@ private: /// Update surface's texture for given region when necessary void ValidateSurface(const Surface& surface, PAddr addr, u32 size); + // Returns false if there is a surface in the cache at the interval with the same bit-width, + bool NoUnimplementedReinterpretations(const OpenGL::Surface& surface, + OpenGL::SurfaceParams& params, + const OpenGL::SurfaceInterval& interval); + + // Return true if a surface with an invalid pixel format exists at the interval + bool IntervalHasInvalidPixelFormat(SurfaceParams& params, const SurfaceInterval& interval); + + // Attempt to find a reinterpretable surface in the cache and use it to copy for validation + bool ValidateByReinterpretation(const Surface& surface, SurfaceParams& params, + const SurfaceInterval& interval); + /// Create a new surface Surface CreateSurface(const SurfaceParams& params); @@ -308,18 +318,13 @@ private: OGLFramebuffer read_framebuffer; OGLFramebuffer draw_framebuffer; - OGLVertexArray attributeless_vao; - OGLBuffer d24s8_abgr_buffer; - GLsizeiptr d24s8_abgr_buffer_size; - OGLProgram d24s8_abgr_shader; - GLint d24s8_abgr_tbo_size_u_id; - GLint d24s8_abgr_viewport_u_id; u16 resolution_scale_factor; std::unordered_map texture_cube_cache; public: std::unique_ptr texture_filterer; + std::unique_ptr format_reinterpreter; }; struct FormatTuple { diff --git a/src/video_core/renderer_opengl/gl_surface_params.h b/src/video_core/renderer_opengl/gl_surface_params.h index 56d33e0d4..e5e5e56f3 100644 --- a/src/video_core/renderer_opengl/gl_surface_params.h +++ b/src/video_core/renderer_opengl/gl_surface_params.h @@ -91,6 +91,47 @@ public: return GetFormatBpp(pixel_format); } + static std::string_view PixelFormatAsString(PixelFormat format) { + switch (format) { + case PixelFormat::RGBA8: + return "RGBA8"; + case PixelFormat::RGB8: + return "RGB8"; + case PixelFormat::RGB5A1: + return "RGB5A1"; + case PixelFormat::RGB565: + return "RGB565"; + case PixelFormat::RGBA4: + return "RGBA4"; + case PixelFormat::IA8: + return "IA8"; + case PixelFormat::RG8: + return "RG8"; + case PixelFormat::I8: + return "I8"; + case PixelFormat::A8: + return "A8"; + case PixelFormat::IA4: + return "IA4"; + case PixelFormat::I4: + return "I4"; + case PixelFormat::A4: + return "A4"; + case PixelFormat::ETC1: + return "ETC1"; + case PixelFormat::ETC1A4: + return "ETC1A4"; + case PixelFormat::D16: + return "D16"; + case PixelFormat::D24: + return "D24"; + case PixelFormat::D24S8: + return "D24S8"; + default: + return "Not a real pixel format"; + } + } + static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) { return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid; } From 23921e32030256ee1a0e21a179de04410c6995cc Mon Sep 17 00:00:00 2001 From: Vitor K Date: Tue, 7 Apr 2020 11:58:51 -0300 Subject: [PATCH 31/37] Option to hide mouse on inactivity (#5094) * Add and implement option to hide mouse on iniactivity + clang format * Set mouse hide timeout as a constant * Address review comments, decrease mouse inactivity timeout to 2500ms * Hide mouse: fix menubar bugs squashable * Hide mouse: ensure status bar has the default pointer --- src/citra_qt/bootmanager.cpp | 4 ++ src/citra_qt/configuration/config.cpp | 3 + .../configuration/configure_general.cpp | 2 + .../configuration/configure_general.ui | 7 +++ src/citra_qt/main.cpp | 55 +++++++++++++++++++ src/citra_qt/main.h | 5 ++ src/citra_qt/uisettings.h | 1 + 7 files changed, 77 insertions(+) diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 78541e880..2f6ab6829 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -199,6 +199,8 @@ GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread) setLayout(layout); InputCommon::Init(); + this->setMouseTracking(true); + GMainWindow* parent = GetMainWindow(); connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete); } @@ -295,6 +297,7 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) { } else if (event->button() == Qt::RightButton) { InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y()); } + QWidget::mouseMoveEvent(event); } void GRenderWindow::mouseMoveEvent(QMouseEvent* event) { @@ -305,6 +308,7 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) { const auto [x, y] = ScaleTouch(pos); this->TouchMoved(x, y); InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y()); + QWidget::mouseMoveEvent(event); } void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) { diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 2901022da..9830f9000 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -570,6 +570,8 @@ void Config::ReadUIValues() { UISettings::values.show_console = ReadSetting(QStringLiteral("showConsole"), false).toBool(); UISettings::values.pause_when_in_background = ReadSetting(QStringLiteral("pauseWhenInBackground"), false).toBool(); + UISettings::values.hide_mouse = + ReadSetting(QStringLiteral("hideInactiveMouse"), false).toBool(); qt_config->endGroup(); } @@ -1028,6 +1030,7 @@ void Config::SaveUIValues() { WriteSetting(QStringLiteral("showConsole"), UISettings::values.show_console, false); WriteSetting(QStringLiteral("pauseWhenInBackground"), UISettings::values.pause_when_in_background, false); + WriteSetting(QStringLiteral("hideInactiveMouse"), UISettings::values.hide_mouse, false); qt_config->endGroup(); } diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index 47d559e3d..419331d13 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -27,6 +27,7 @@ ConfigureGeneral::~ConfigureGeneral() = default; void ConfigureGeneral::SetConfiguration() { ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background); + ui->toggle_hide_mouse->setChecked(UISettings::values.hide_mouse); ui->toggle_update_check->setChecked(UISettings::values.check_for_update_on_start); ui->toggle_auto_update->setChecked(UISettings::values.update_on_close); @@ -55,6 +56,7 @@ void ConfigureGeneral::ResetDefaults() { void ConfigureGeneral::ApplyConfiguration() { UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked(); + UISettings::values.hide_mouse = ui->toggle_hide_mouse->isChecked(); UISettings::values.check_for_update_on_start = ui->toggle_update_check->isChecked(); UISettings::values.update_on_close = ui->toggle_auto_update->isChecked(); diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui index 181455a64..6b2aa4416 100644 --- a/src/citra_qt/configuration/configure_general.ui +++ b/src/citra_qt/configuration/configure_general.ui @@ -36,6 +36,13 @@ + + + + Hide mouse on inactivity + + + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index fc7eb2522..f47e359a0 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -102,6 +102,8 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; } #endif +constexpr int default_mouse_timeout = 2500; + /** * "Callouts" are one-time instructional messages shown to the user. In the config settings, there * is a bitfield "callout_flags" options, used to track if a message has already been shown to the @@ -194,6 +196,14 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { // Show one-time "callout" messages to the user ShowTelemetryCallout(); + // make sure menubar has the arrow cursor instead of inheriting from this + ui.menubar->setCursor(QCursor()); + statusBar()->setCursor(QCursor()); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui.menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor); + if (UISettings::values.check_for_update_on_start) { CheckForUpdates(); } @@ -965,6 +975,13 @@ void GMainWindow::BootGame(const QString& filename) { } status_bar_update_timer.start(2000); + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + ui.centralwidget->setMouseTracking(true); + ui.menubar->setMouseTracking(true); + } + // show and hide the render_window to create the context render_window->show(); render_window->hide(); @@ -1063,6 +1080,10 @@ void GMainWindow::ShutdownGame() { game_list->show(); game_list->setFilterFocus(); + setMouseTracking(false); + ui.centralwidget->setMouseTracking(false); + ui.menubar->setMouseTracking(false); + // Disable status bar updates status_bar_update_timer.stop(); message_label->setVisible(false); @@ -1578,6 +1599,16 @@ void GMainWindow::OnConfigure() { SyncMenuUISettings(); game_list->RefreshGameDirectory(); config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + ui.centralwidget->setMouseTracking(true); + ui.menubar->setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + ui.centralwidget->setMouseTracking(false); + ui.menubar->setMouseTracking(false); + } } else { Settings::values.input_profiles = old_input_profiles; Settings::LoadProfile(old_input_profile_index); @@ -1894,6 +1925,30 @@ void GMainWindow::UpdateStatusBar() { emu_frametime_label->setVisible(true); } +void GMainWindow::HideMouseCursor() { + if (emu_thread == nullptr || UISettings::values.hide_mouse == false) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + setCursor(QCursor(Qt::BlankCursor)); +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + if (emu_thread != nullptr && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::mouseMoveEvent(QMouseEvent* event) { + ShowMouseCursor(); +} + +void GMainWindow::mousePressEvent(QMouseEvent* event) { + ShowMouseCursor(); +} + void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { QString status_message; diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 132fad821..4bdbd622f 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -220,6 +220,8 @@ private: void UpdateWindowTitle(); void RetranslateStatusBar(); void InstallCIA(QStringList filepaths); + void HideMouseCursor(); + void ShowMouseCursor(); Ui::MainWindow ui; @@ -248,6 +250,7 @@ private: QString game_path; bool auto_paused = false; + QTimer mouse_hide_timer; // Movie bool movie_record_on_start = false; @@ -291,6 +294,8 @@ protected: void dropEvent(QDropEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; }; Q_DECLARE_METATYPE(std::size_t); diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index 0736db6ff..a54bc3ee7 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -76,6 +76,7 @@ struct Values { bool confirm_before_closing; bool first_start; bool pause_when_in_background; + bool hide_mouse; bool updater_found; bool update_on_close; From 7903f6fbfca54f1a403e9e92fb4812cbe85f6621 Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 10 Apr 2020 18:35:14 -0500 Subject: [PATCH 32/37] Warnings/Network: Handle ENET_EVENT_TYPE_NONE and ENET_EVENT_TYPE_CONNECT in the network packet loop. ENET_EVENT_TYPE_NONE is basically a no-op. ENET_EVENT_TYPE_CONNECT should not happen since we are already connected. Assert in case we do receive it. --- src/network/room_member.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 5814fe12a..e43004027 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -242,6 +242,13 @@ void RoomMember::RoomMemberImpl::MemberLoop() { SetError(Error::LostConnection); } break; + case ENET_EVENT_TYPE_NONE: + break; + case ENET_EVENT_TYPE_CONNECT: + // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're + // already connected + ASSERT_MSG(false, "Received unexpected connect event while already connected"); + break; } } { From d17557c02ce23933c94c052d148213c2b54c4c8e Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 10 Apr 2020 18:35:53 -0500 Subject: [PATCH 33/37] Warnings/Network: Initialize the members of Room::RoomImpl in the order they are defined. --- src/network/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/room.cpp b/src/network/room.cpp index e1b6b77d3..342a2541b 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -53,7 +53,7 @@ public: mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists RoomImpl() - : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {} + : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {} /// Thread that receives and dispatches network packets std::unique_ptr room_thread; From 64b612bd6085ec848b82c6f59c96c7c3f48643c2 Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 10 Apr 2020 18:37:45 -0500 Subject: [PATCH 34/37] Warnings/GLWindow: Initialize the members of OpenGLWindow in the order they are defined. --- src/citra_qt/bootmanager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 2f6ab6829..ddabad13b 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -104,8 +104,8 @@ void EmuThread::run() { } OpenGLWindow::OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context) - : QWindow(parent), event_handler(event_handler), - context(new QOpenGLContext(shared_context->parent())) { + : QWindow(parent), context(new QOpenGLContext(shared_context->parent())), + event_handler(event_handler) { // disable vsync for any shared contexts auto format = shared_context->format(); From b80911162f6863335082786c46e51c39ad0ca1cc Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 10 Apr 2020 18:39:40 -0500 Subject: [PATCH 35/37] Warnings/QtMotionControls: There is no need to use std::move after std::make_unique. This fixes a Clang warning about the move preventing copy elision (-Wpessimizing-move) --- src/citra_qt/configuration/configure_motion_touch.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/citra_qt/configuration/configure_motion_touch.cpp b/src/citra_qt/configuration/configure_motion_touch.cpp index c1961d71e..a5457cdf7 100644 --- a/src/citra_qt/configuration/configure_motion_touch.cpp +++ b/src/citra_qt/configuration/configure_motion_touch.cpp @@ -30,7 +30,7 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent, setLayout(layout); using namespace InputCommon::CemuhookUDP; - job = std::move(std::make_unique( + job = std::make_unique( host, port, pad_index, client_id, [this](CalibrationConfigurationJob::Status status) { QString text; @@ -56,7 +56,7 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent, min_y = min_y_; max_x = max_x_; max_y = max_y_; - })); + }); } CalibrationConfigurationDialog::~CalibrationConfigurationDialog() = default; From 47417e762b3f217d578aebb42da1c4a19f06c75f Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 10 Apr 2020 18:40:56 -0500 Subject: [PATCH 36/37] Warnings/Qt: Removed unused variable from lambda capture list. --- src/citra_qt/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index f47e359a0..1f2a71286 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1232,7 +1232,7 @@ void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { using FutureWatcher = QFutureWatcher>; auto* future_watcher = new FutureWatcher(this); connect(future_watcher, &FutureWatcher::finished, - [this, program_id, dialog, base_path, update_path, future_watcher] { + [this, dialog, base_path, update_path, future_watcher] { dialog->hide(); const auto& [base, update] = future_watcher->result(); if (base != Loader::ResultStatus::Success) { From 0e88940df182a5e67510e891f90deb5e857faa7c Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 10 Apr 2020 18:42:57 -0500 Subject: [PATCH 37/37] Warnings/Thread: Added missing case for the Dormant thread status in ResumeFromWait. --- src/core/hle/kernel/thread.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index 47d8cb1df..c86a0fb86 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -194,6 +194,7 @@ void Thread::ResumeFromWait() { case ThreadStatus::WaitArb: case ThreadStatus::WaitSleep: case ThreadStatus::WaitIPC: + case ThreadStatus::Dormant: break; case ThreadStatus::Ready: