diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index d5d9f992b2..7a5234d01a 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -760,6 +760,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index d08488e266..50e3e666a7 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -2716,6 +2716,9 @@ Emu\GPU\RSX\Program + + Utilities + diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 7c39b0a88c..acee714f24 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -807,6 +807,7 @@ + @@ -1514,6 +1515,7 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index a2e4afd1a6..0bb0890b41 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -196,6 +196,9 @@ {9b51636c-b371-425b-86d3-be003774a1b7} + + {2bb5cec5-5acb-40c0-a388-68db05dff305} + @@ -1176,6 +1179,9 @@ Gui\game list + + Io\video + @@ -1385,6 +1391,9 @@ Gui\game list + + Io\video + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index ffe2a96d9f..66c4fa3144 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -73,6 +73,7 @@ add_library(rpcs3_ui STATIC qt_camera_video_sink.cpp qt_music_handler.cpp qt_utils.cpp + qt_video_source.cpp raw_mouse_settings_dialog.cpp register_editor_dialog.cpp recvmessage_dialog_frame.cpp diff --git a/rpcs3/rpcs3qt/game_list_base.cpp b/rpcs3/rpcs3qt/game_list_base.cpp index 631bf4c066..72b45d33bf 100644 --- a/rpcs3/rpcs3qt/game_list_base.cpp +++ b/rpcs3/rpcs3qt/game_list_base.cpp @@ -33,7 +33,7 @@ void game_list_base::repaint_icons(std::vector& game_data, const QCol IconLoadFunction(game, device_pixel_ratio, cancel); }); - item->call_icon_func(); + item->image_change_callback(); } } } diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index 30b8cd0745..5d4b90203f 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -21,7 +21,7 @@ game_list_grid::game_list_grid() connect(this, &game_list_grid::IconReady, this, [this](const movie_item_base* item) { - if (item) item->call_icon_func(); + if (item) item->image_change_callback(); }, Qt::QueuedConnection); // The default 'AutoConnection' doesn't seem to work in this specific case... connect(this, &flow_widget::ItemSelectionChanged, this, [this](int index) @@ -81,7 +81,7 @@ void game_list_grid::populate( item->setToolTip(tr("%0 [%1]").arg(title).arg(serial)); } - item->set_icon_func([this, item, game](const QVideoFrame& frame) + item->set_image_change_callback([this, item, game](const QVideoFrame& frame) { if (!item || !game) { @@ -109,7 +109,7 @@ void game_list_grid::populate( if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam)) { - item->set_movie_path(QString::fromStdString(game->info.movie_path)); + item->set_video_path(game->info.movie_path); } if (selected_item_id == game->info.path + game->info.icon_path) @@ -154,7 +154,7 @@ void game_list_grid::repaint_icons(std::vector& game_data, const QCol { // We don't have an icon. Set a placeholder to initialize the layout. game->pxmap = placeholder; - item->call_icon_func(); + item->image_change_callback(); } item->set_icon_load_func([this, game, device_pixel_ratio, cancel = item->icon_loading_aborted()](int) diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index 3aed99dcf9..f526bcea43 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -54,7 +54,7 @@ game_list_table::game_list_table(game_list_frame* frame, std::shared_ptrcall_icon_func(); + if (item) item->image_change_callback(); }); } @@ -242,7 +242,7 @@ void game_list_table::populate( custom_table_widget_item* icon_item = new custom_table_widget_item; game->item = icon_item; - icon_item->set_icon_func([this, icon_item, game](const QVideoFrame& frame) + icon_item->set_image_change_callback([this, icon_item, game](const QVideoFrame& frame) { if (!icon_item || !game) { @@ -292,7 +292,7 @@ void game_list_table::populate( if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam)) { - icon_item->set_movie_path(QString::fromStdString(game->info.movie_path)); + icon_item->set_video_path(game->info.movie_path); } icon_item->setData(Qt::UserRole, index, true); diff --git a/rpcs3/rpcs3qt/movie_item_base.cpp b/rpcs3/rpcs3qt/movie_item_base.cpp index d413f4d0b3..fdf820ae5f 100644 --- a/rpcs3/rpcs3qt/movie_item_base.cpp +++ b/rpcs3/rpcs3qt/movie_item_base.cpp @@ -1,24 +1,14 @@ #include "stdafx.h" #include "movie_item_base.h" -#include - -movie_item_base::movie_item_base() +movie_item_base::movie_item_base() : qt_video_source() { init_pointers(); } movie_item_base::~movie_item_base() { - if (m_movie) - { - m_movie->stop(); - } - - if (m_media_player) - { - m_media_player->stop(); - } + stop_movie(); wait_for_icon_loading(true); wait_for_size_on_disk_loading(true); @@ -30,146 +20,6 @@ void movie_item_base::init_pointers() m_size_on_disk_loading_aborted.reset(new atomic_t(false)); } -void movie_item_base::set_active(bool active) -{ - if (!std::exchange(m_active, active) && active) - { - init_movie(); - - if (m_movie) - { - m_movie->jumpToFrame(1); - m_movie->start(); - } - - if (m_media_player) - { - m_media_player->play(); - } - } -} - -void movie_item_base::init_movie() -{ - if (m_movie || m_media_player) - { - // Already initialized - return; - } - - if (!m_icon_callback || m_movie_path.isEmpty() || !QFile::exists(m_movie_path)) - { - m_movie_path.clear(); - return; - } - - const QString lower = m_movie_path.toLower(); - - if (lower.endsWith(".gif")) - { - m_movie.reset(new QMovie(m_movie_path)); - m_movie_path.clear(); - - if (!m_movie->isValid()) - { - m_movie.reset(); - return; - } - - QObject::connect(m_movie.get(), &QMovie::frameChanged, m_movie.get(), [this](int) - { - m_icon_callback({}); - }); - return; - } - - if (lower.endsWith(".pam")) - { - // We can't set PAM files as source of the video player, so we have to feed them as raw data. - QFile file(m_movie_path); - if (!file.open(QFile::OpenModeFlag::ReadOnly)) - { - return; - } - - // TODO: Decode the pam properly before pushing it to the player - m_movie_data = file.readAll(); - if (m_movie_data.isEmpty()) - { - return; - } - - m_movie_buffer.reset(new QBuffer(&m_movie_data)); - m_movie_buffer->open(QIODevice::ReadOnly); - } - - m_video_sink.reset(new QVideoSink()); - QObject::connect(m_video_sink.get(), &QVideoSink::videoFrameChanged, m_video_sink.get(), [this](const QVideoFrame& frame) - { - m_icon_callback(frame); - }); - - m_media_player.reset(new QMediaPlayer()); - m_media_player->setVideoSink(m_video_sink.get()); - m_media_player->setLoops(QMediaPlayer::Infinite); - - if (m_movie_buffer) - { - m_media_player->setSourceDevice(m_movie_buffer.get()); - } - else - { - m_media_player->setSource(m_movie_path); - } -} - -void movie_item_base::stop_movie() -{ - if (m_movie) - { - m_movie->stop(); - } - - m_video_sink.reset(); - m_media_player.reset(); - m_movie_buffer.reset(); - m_movie_data.clear(); -} - -QPixmap movie_item_base::get_movie_image(const QVideoFrame& frame) const -{ - if (!m_active) - { - return {}; - } - - if (m_movie) - { - return m_movie->currentPixmap(); - } - - if (!frame.isValid()) - { - return {}; - } - - // Get image. This usually also converts the image to ARGB32. - return QPixmap::fromImage(frame.toImage()); -} - -void movie_item_base::call_icon_func() const -{ - if (m_icon_callback) - { - m_icon_callback({}); - } -} - -void movie_item_base::set_icon_func(const icon_callback_t& func) -{ - m_icon_callback = func; -} - void movie_item_base::call_icon_load_func(int index) { if (!m_icon_load_callback || m_icon_loading || m_icon_loading_aborted->load()) diff --git a/rpcs3/rpcs3qt/movie_item_base.h b/rpcs3/rpcs3qt/movie_item_base.h index f13bce91b2..3784de4e1f 100644 --- a/rpcs3/rpcs3qt/movie_item_base.h +++ b/rpcs3/rpcs3qt/movie_item_base.h @@ -1,25 +1,16 @@ #pragma once -#include "movie_item_base.h" -#include "util/atomic.hpp" -#include "Utilities/mutex.h" +#include "qt_video_source.h" -#include #include -#include -#include -#include -#include -#include #include #include -using icon_callback_t = std::function; using icon_load_callback_t = std::function; using size_calc_callback_t = std::function; -class movie_item_base +class movie_item_base : public qt_video_source { public: movie_item_base(); @@ -27,25 +18,6 @@ public: void init_pointers(); - void set_active(bool active); - - [[nodiscard]] bool get_active() const - { - return m_active; - } - - void set_movie_path(QString path) - { - m_movie_path = std::move(path); - } - - void init_movie(); - void stop_movie(); - QPixmap get_movie_image(const QVideoFrame& frame) const; - - void call_icon_func() const; - void set_icon_func(const icon_callback_t& func); - void call_icon_load_func(int index); void set_icon_load_func(const icon_load_callback_t& func); @@ -77,23 +49,13 @@ public: shared_mutex pixmap_mutex; -protected: - QString m_movie_path; - QByteArray m_movie_data{}; - std::unique_ptr m_movie_buffer; - std::unique_ptr m_media_player; - std::shared_ptr m_video_sink; - std::shared_ptr m_movie; - private: std::unique_ptr m_icon_load_thread; std::unique_ptr m_size_calc_thread; - bool m_active = false; atomic_t m_size_on_disk_loading = false; atomic_t m_icon_loading = false; size_calc_callback_t m_size_calc_callback = nullptr; icon_load_callback_t m_icon_load_callback = nullptr; - icon_callback_t m_icon_callback = nullptr; std::shared_ptr> m_icon_loading_aborted; std::shared_ptr> m_size_on_disk_loading_aborted; diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp new file mode 100644 index 0000000000..263c1f2121 --- /dev/null +++ b/rpcs3/rpcs3qt/qt_video_source.cpp @@ -0,0 +1,244 @@ +#include "stdafx.h" +#include "Emu/System.h" +#include "qt_video_source.h" + +#include + +qt_video_source::qt_video_source() + : video_source() +{ +} + +qt_video_source::~qt_video_source() +{ + stop_movie(); +} + +void qt_video_source::set_video_path(const std::string& path) +{ + m_video_path = QString::fromStdString(path); +} + +void qt_video_source::set_active(bool active) +{ + if (!m_active.exchange(active) && active) + { + start_movie(); + } +} + +void qt_video_source::image_change_callback() const +{ + if (m_image_change_callback) + { + m_image_change_callback({}); + } +} + +void qt_video_source::set_image_change_callback(const std::function& func) +{ + m_image_change_callback = func; +} + +void qt_video_source::init_movie() +{ + if (m_movie || m_media_player) + { + // Already initialized + return; + } + + if (!m_image_change_callback || m_video_path.isEmpty() || !QFile::exists(m_video_path)) + { + m_video_path.clear(); + return; + } + + const QString lower = m_video_path.toLower(); + + if (lower.endsWith(".gif")) + { + m_movie.reset(new QMovie(m_video_path)); + m_video_path.clear(); + + if (!m_movie->isValid()) + { + m_movie.reset(); + return; + } + + QObject::connect(m_movie.get(), &QMovie::frameChanged, m_movie.get(), [this](int) + { + m_image_change_callback({}); + m_has_new = true; + }); + return; + } + + if (lower.endsWith(".pam")) + { + // We can't set PAM files as source of the video player, so we have to feed them as raw data. + QFile file(m_video_path); + if (!file.open(QFile::OpenModeFlag::ReadOnly)) + { + return; + } + + // TODO: Decode the pam properly before pushing it to the player + m_video_data = file.readAll(); + if (m_video_data.isEmpty()) + { + return; + } + + m_video_buffer.reset(new QBuffer(&m_video_data)); + m_video_buffer->open(QIODevice::ReadOnly); + } + + m_video_sink.reset(new QVideoSink()); + QObject::connect(m_video_sink.get(), &QVideoSink::videoFrameChanged, m_video_sink.get(), [this](const QVideoFrame& frame) + { + m_image_change_callback(frame); + m_has_new = true; + }); + + m_media_player.reset(new QMediaPlayer()); + m_media_player->setVideoSink(m_video_sink.get()); + m_media_player->setLoops(QMediaPlayer::Infinite); + + if (m_video_buffer) + { + m_media_player->setSourceDevice(m_video_buffer.get()); + } + else + { + m_media_player->setSource(m_video_path); + } +} + +void qt_video_source::start_movie() +{ + init_movie(); + + if (m_movie) + { + m_movie->jumpToFrame(1); + m_movie->start(); + } + + if (m_media_player) + { + m_media_player->play(); + } + + m_active = true; +} + +void qt_video_source::stop_movie() +{ + m_active = false; + + if (m_movie) + { + m_movie->stop(); + } + + m_video_sink.reset(); + m_media_player.reset(); + m_video_buffer.reset(); + m_video_data.clear(); +} + +QPixmap qt_video_source::get_movie_image(const QVideoFrame& frame) const +{ + if (!m_active) + { + return {}; + } + + if (m_movie) + { + return m_movie->currentPixmap(); + } + + if (!frame.isValid()) + { + return {}; + } + + // Get image. This usually also converts the image to ARGB32. + return QPixmap::fromImage(frame.toImage()); +} + +void qt_video_source::get_image(std::vector& data, int& w, int& h, int& ch, int& bpp) +{ + if (!m_has_new.exchange(false)) + { + return; + } + + std::lock_guard lock(m_image_mutex); + + if (m_image.isNull()) + { + w = h = ch = bpp = 0; + data.clear(); + return; + } + + w = m_image.width(); + h = m_image.height(); + ch = m_image.colorCount(); + bpp = m_image.depth(); + + data.resize(m_image.height() * m_image.bytesPerLine()); + std::memcpy(data.data(), m_image.constBits(), data.size()); +} + +qt_video_source_wrapper::~qt_video_source_wrapper() +{ + Emu.BlockingCallFromMainThread([this]() + { + m_qt_video_source.reset(); + }); +} + +void qt_video_source_wrapper::set_video_path(const std::string& path) +{ + Emu.BlockingCallFromMainThread([this, &path]() + { + m_qt_video_source = std::make_unique(); + m_qt_video_source->m_image_change_callback = [this](const QVideoFrame& frame) + { + std::lock_guard lock(m_qt_video_source->m_image_mutex); + + if (m_qt_video_source->m_movie) + { + m_qt_video_source->m_image = m_qt_video_source->m_movie->currentImage(); + } + else if (frame.isValid()) + { + // Get image. This usually also converts the image to ARGB32. + m_qt_video_source->m_image = frame.toImage(); + } + else + { + return; + } + + if (m_qt_video_source->m_image.format() != QImage::Format_RGBA8888) + { + m_qt_video_source->m_image.convertTo(QImage::Format_RGBA8888); + } + }; + m_qt_video_source->set_video_path(path); + m_qt_video_source->set_active(true); + }); +} + +void qt_video_source_wrapper::get_image(std::vector& data, int& w, int& h, int& ch, int& bpp) +{ + ensure(m_qt_video_source); + + m_qt_video_source->get_image(data, w, h, ch, bpp); +} diff --git a/rpcs3/rpcs3qt/qt_video_source.h b/rpcs3/rpcs3qt/qt_video_source.h new file mode 100644 index 0000000000..44cdebc034 --- /dev/null +++ b/rpcs3/rpcs3qt/qt_video_source.h @@ -0,0 +1,76 @@ +#pragma once + +#include "util/video_source.h" +#include "util/atomic.hpp" +#include "Utilities/mutex.h" + +#include +#include +#include +#include +#include +#include + +class qt_video_source : public video_source +{ +public: + qt_video_source(); + virtual ~qt_video_source(); + + void set_video_path(const std::string& path) override; + const QString& video_path() const { return m_video_path; } + + void get_image(std::vector& data, int& w, int& h, int& ch, int& bpp) override; + bool has_new() const override { return m_has_new; } + + void set_active(bool active); + [[nodiscard]] bool get_active() const + { + return m_active; + } + + void start_movie(); + void stop_movie(); + + QPixmap get_movie_image(const QVideoFrame& frame) const; + + void image_change_callback() const; + void set_image_change_callback(const std::function& func); + +protected: + void init_movie(); + + shared_mutex m_image_mutex; + + atomic_t m_active = false; + atomic_t m_has_new = false; + + QString m_video_path; + QByteArray m_video_data{}; + QImage m_image{}; + std::vector m_image_path; + + std::unique_ptr m_video_buffer; + std::unique_ptr m_media_player; + std::shared_ptr m_video_sink; + std::shared_ptr m_movie; + + std::function m_image_change_callback = nullptr; + + friend class qt_video_source_wrapper; +}; + +// Wrapper for emulator usage +class qt_video_source_wrapper : public video_source +{ +public: + qt_video_source_wrapper() : video_source() {} + virtual ~qt_video_source_wrapper(); + + void set_video_path(const std::string& path) override; + void get_image(std::vector& data, int& w, int& h, int& ch, int& bpp) override; + bool has_new() const override { return m_qt_video_source && m_qt_video_source->has_new(); } + +private: + std::unique_ptr m_qt_video_source; +}; diff --git a/rpcs3/rpcs3qt/save_manager_dialog.cpp b/rpcs3/rpcs3qt/save_manager_dialog.cpp index 1485d12ec0..bdb085d7df 100644 --- a/rpcs3/rpcs3qt/save_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/save_manager_dialog.cpp @@ -208,7 +208,7 @@ void save_manager_dialog::Init() if (movie_item* item = static_cast(m_list->item(index, SaveColumns::Icon))) { item->setData(SaveUserRole::PixmapScaled, pixmap); - item->call_icon_func(); + item->image_change_callback(); } }); connect(search_bar, &QLineEdit::textChanged, this, &save_manager_dialog::text_changed); @@ -344,14 +344,14 @@ void save_manager_dialog::UpdateList() if (const std::string movie_path = dir_path + localized_movie; fs::is_file(movie_path)) { - icon_item->set_movie_path(QString::fromStdString(movie_path)); + icon_item->set_video_path(movie_path); } else if (const std::string movie_path = dir_path + "ICON1.PAM"; fs::is_file(movie_path)) { - icon_item->set_movie_path(QString::fromStdString(movie_path)); + icon_item->set_video_path(movie_path); } - icon_item->set_icon_func([this, icon_item](const QVideoFrame& frame) + icon_item->set_image_change_callback([this, icon_item](const QVideoFrame& frame) { if (!icon_item) { diff --git a/rpcs3/util/video_source.h b/rpcs3/util/video_source.h new file mode 100644 index 0000000000..b5737bccc0 --- /dev/null +++ b/rpcs3/util/video_source.h @@ -0,0 +1,20 @@ +#pragma once + +#include "types.hpp" + +class video_source +{ +public: + video_source() {}; + virtual ~video_source() {}; + virtual void set_video_path(const std::string& path) { static_cast(path); } + virtual bool has_new() const { return false; }; + virtual void get_image(std::vector& data, int& w, int& h, int& ch, int& bpp) + { + static_cast(data); + static_cast(w); + static_cast(h); + static_cast(ch); + static_cast(bpp); + } +};