From e0a0d736c4df546cdf250f534d5b912f239134ae Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 10 Mar 2026 01:45:05 +0100 Subject: [PATCH 1/5] cellGem: remove redundant check --- rpcs3/Emu/Cell/Modules/cellGem.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index f87b4349a1..2ed6d8911d 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -1609,12 +1609,6 @@ public: return false; } - if (!m_camera_info.bytesize) - { - cellGem.error("gem_tracker: unexpected image size: %d", m_camera_info.bytesize); - return false; - } - m_tracker.set_image_data(m_camera_info.buffer.get_ptr(), m_camera_info.bytesize, m_camera_info.width, m_camera_info.height, m_camera_info.format); m_framenumber++; // using framenumber instead of timestamp since the timestamp could be identical return true; From 0f92ea2578a48ee21aff2430be2d28baefde8c48 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 10 Mar 2026 01:50:56 +0100 Subject: [PATCH 2/5] cellGem: fix memcpy in cellGemReadExternalPortDeviceInfo It was copying from dst to src, and the wrong size at that --- rpcs3/Emu/Cell/Modules/cellGem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index 2ed6d8911d..11cc18e547 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -3619,7 +3619,7 @@ error_code cellGemReadExternalPortDeviceInfo(u32 gem_num, vm::ptr ext_id, v if (!pad->move_data.external_device_read_requested) { *ext_id = controller.ext_id = pad->move_data.external_device_id; - std::memcpy(pad->move_data.external_device_read.data(), ext_info.get_ptr(), CELL_GEM_EXTERNAL_PORT_OUTPUT_SIZE); + std::memcpy(ext_info.get_ptr(), pad->move_data.external_device_read.data(), CELL_GEM_EXTERNAL_PORT_DEVICE_INFO_SIZE); break; } } From 763001ee91c515fc77c050258bba383acb75f25e Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 10 Mar 2026 01:53:51 +0100 Subject: [PATCH 3/5] cellGem: fix gain channels --- rpcs3/Emu/Cell/Modules/cellGem.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index 11cc18e547..9731474b51 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -770,8 +770,8 @@ namespace gem if constexpr (use_gain) { dst0[0] = static_cast(std::clamp(r * gain_r, 0.0f, 255.0f)); - dst0[1] = static_cast(std::clamp(b * gain_b, 0.0f, 255.0f)); - dst0[2] = static_cast(std::clamp(g * gain_g, 0.0f, 255.0f)); + dst0[1] = static_cast(std::clamp(g * gain_g, 0.0f, 255.0f)); + dst0[2] = static_cast(std::clamp(b * gain_b, 0.0f, 255.0f)); } else { @@ -822,8 +822,8 @@ namespace gem if constexpr (use_gain) { dst0[0] = static_cast(std::clamp(r * gain_r, 0.0f, 255.0f)); - dst0[1] = static_cast(std::clamp(b * gain_b, 0.0f, 255.0f)); - dst0[2] = static_cast(std::clamp(g * gain_g, 0.0f, 255.0f)); + dst0[1] = static_cast(std::clamp(g * gain_g, 0.0f, 255.0f)); + dst0[2] = static_cast(std::clamp(b * gain_b, 0.0f, 255.0f)); } else { @@ -881,9 +881,9 @@ namespace gem const u8* src_data = video_data_in.data(); const u8 alpha = vc.alpha; - const f32 gain_r = vc.gain * vc.blue_gain; + const f32 gain_r = vc.gain * vc.red_gain; const f32 gain_g = vc.gain * vc.green_gain; - const f32 gain_b = vc.gain * vc.red_gain; + const f32 gain_b = vc.gain * vc.blue_gain; // Only RAW8 should be relevant for cellGem unless I'm mistaken if (input_format == CELL_CAMERA_RAW8) From 71f0d5c60233494e7fe2c3f2ff416a00c87480fb Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 10 Mar 2026 03:38:24 +0100 Subject: [PATCH 4/5] cellGem: fix RAW8 to RGBA_320x240 We were basically writing two rows into dst for each other src line. This means we were writing 480 lines in total instead of 240, overwriting one of the lines written in the previous iteration. This led to writing one line out of bounds last iteration. Let's just use a simple debayer technique which perfectly matches here. This also applies the previously missing gain factors. I also tried to first demosaic and then drop every other pixel. The result was comparatively blurred and the performance worse. --- rpcs3/Emu/Cell/Modules/cellGem.cpp | 76 +++++++++++++++++++----------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index 9731474b51..d45dace1ca 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -845,6 +845,53 @@ namespace gem debayer_raw8_impl(src, dst, alpha, gain_r, gain_g, gain_b); } + template + static inline void debayer_raw8_downscale_impl(const u8* src, u8* dst, u8 alpha, f32 gain_r, f32 gain_g, f32 gain_b) + { + constexpr u32 in_pitch = 640; + constexpr u32 out_pitch = 320 * 4; + + // Simple debayer + for (s32 y = 0; y < 240; y++) + { + const u8* src0 = src + y * 2 * in_pitch; + const u8* src1 = src0 + in_pitch; + + u8* dst0 = dst + y * out_pitch; + + for (s32 x = 0; x < 320; x++, dst0 += 4, src0 += 2, src1 += 2) + { + const u8 b = src0[0]; + const u8 g0 = src0[1]; + const u8 g1 = src1[0]; + const u8 r = src1[1]; + const u8 g = (g0 + g1) >> 1; + + if constexpr (use_gain) + { + dst0[0] = static_cast(std::clamp(r * gain_r, 0.0f, 255.0f)); + dst0[1] = static_cast(std::clamp(g * gain_g, 0.0f, 255.0f)); + dst0[2] = static_cast(std::clamp(b * gain_b, 0.0f, 255.0f)); + } + else + { + dst0[0] = r; + dst0[1] = g; + dst0[2] = b; + } + dst0[3] = alpha; + } + } + } + + static void debayer_raw8_downscale(const u8* src, u8* dst, u8 alpha, f32 gain_r, f32 gain_g, f32 gain_b) + { + if (gain_r != 1.0f || gain_g != 1.0f || gain_b != 1.0f) + debayer_raw8_downscale_impl(src, dst, alpha, gain_r, gain_g, gain_b); + else + debayer_raw8_downscale_impl(src, dst, alpha, gain_r, gain_g, gain_b); + } + bool convert_image_format(CellCameraFormat input_format, const CellGemVideoConvertAttribute& vc, const std::vector& video_data_in, u32 width, u32 height, u8* video_data_out, u32 video_data_out_size, u8* buffer_memory, @@ -1183,34 +1230,7 @@ namespace gem { case CELL_CAMERA_RAW8: { - const u32 in_pitch = width; - const u32 out_pitch = width * 4 / 2; - - for (u32 y = 0; y < height - 1; y += 2) - { - const u8* src0 = src_data + y * in_pitch; - const u8* src1 = src0 + in_pitch; - - u8* dst0 = video_data_out + (y / 2) * out_pitch; - u8* dst1 = dst0 + out_pitch; - - for (u32 x = 0; x < width - 1; x += 2, src0 += 2, src1 += 2, dst0 += 4, dst1 += 4) - { - const u8 b = src0[0]; - const u8 g0 = src0[1]; - const u8 g1 = src1[0]; - const u8 r = src1[1]; - - const u8 top[4] = { r, g0, b, alpha }; - const u8 bottom[4] = { r, g1, b, alpha }; - - // Top-Left - std::memcpy(dst0, top, 4); - - // Bottom-Left Pixel - std::memcpy(dst1, bottom, 4); - } - } + debayer_raw8_downscale(src_data, video_data_out, alpha, gain_r, gain_g, gain_b); break; } case CELL_CAMERA_RGBA: From 0603d24a911013051a29b3794567ec75b760de61 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 10 Mar 2026 05:33:55 +0100 Subject: [PATCH 5/5] Qt: play SND0.AT3 in game lists when a movie would play --- rpcs3/Emu/GameInfo.h | 1 + rpcs3/rpcs3qt/game_list_frame.cpp | 6 ++ rpcs3/rpcs3qt/game_list_grid.cpp | 18 +++++- rpcs3/rpcs3qt/game_list_table.cpp | 18 +++++- rpcs3/rpcs3qt/gui_game_info.h | 1 + rpcs3/rpcs3qt/qt_video_source.cpp | 89 ++++++++++++++++++++++++++- rpcs3/rpcs3qt/qt_video_source.h | 7 +++ rpcs3/rpcs3qt/save_manager_dialog.cpp | 6 ++ rpcs3/util/video_source.h | 1 + 9 files changed, 140 insertions(+), 7 deletions(-) diff --git a/rpcs3/Emu/GameInfo.h b/rpcs3/Emu/GameInfo.h index 3efca1410a..da8b2638ba 100644 --- a/rpcs3/Emu/GameInfo.h +++ b/rpcs3/Emu/GameInfo.h @@ -8,6 +8,7 @@ struct GameInfo std::string path; std::string icon_path; std::string movie_path; + std::string audio_path; std::string name; std::string serial; diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index ea7b87ff6d..5ba31d9d77 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -637,6 +637,12 @@ void game_list_frame::OnParsingFinished() game.has_hover_pam = true; } + if (std::string audio_path = sfo_dir + "/SND0.AT3"; file_exists(audio_path)) + { + game.info.audio_path = std::move(audio_path); + game.has_audio_file = true; + } + const QString serial = QString::fromStdString(game.info.serial); m_games_mutex.lock(); diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index a33755ff54..4dfcf9744a 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -109,11 +109,23 @@ void game_list_grid::populate( } }); - if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam)) + if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam || game->has_audio_file)) { - item->set_video_path(game->info.movie_path); + bool check_iso = false; - if (!fs::exists(game->info.movie_path) && is_file_iso(game->info.path)) + if (game->has_hover_gif || game->has_hover_pam) + { + item->set_video_path(game->info.movie_path); + check_iso |= !fs::exists(game->info.movie_path); + } + + if (game->has_audio_file) + { + item->set_audio_path(game->info.audio_path); + check_iso |= !fs::exists(game->info.audio_path); + } + + if (check_iso && is_file_iso(game->info.path)) { item->set_iso_path(game->info.path); } diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index 98b9ef344d..737709dabb 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -299,11 +299,23 @@ void game_list_table::populate( } }); - if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam)) + if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam || game->has_audio_file)) { - icon_item->set_video_path(game->info.movie_path); + bool check_iso = false; - if (!fs::exists(game->info.movie_path) && is_file_iso(game->info.path)) + if (game->has_hover_gif || game->has_hover_pam) + { + icon_item->set_video_path(game->info.movie_path); + check_iso |= !fs::exists(game->info.movie_path); + } + + if (game->has_audio_file) + { + icon_item->set_audio_path(game->info.audio_path); + check_iso |= !fs::exists(game->info.audio_path); + } + + if (check_iso && is_file_iso(game->info.path)) { icon_item->set_iso_path(game->info.path); } diff --git a/rpcs3/rpcs3qt/gui_game_info.h b/rpcs3/rpcs3qt/gui_game_info.h index 08483fa7fb..693483dd6a 100644 --- a/rpcs3/rpcs3qt/gui_game_info.h +++ b/rpcs3/rpcs3qt/gui_game_info.h @@ -21,6 +21,7 @@ struct gui_game_info bool has_custom_icon = false; bool has_hover_gif = false; bool has_hover_pam = false; + bool has_audio_file = false; bool icon_in_archive = false; movie_item_base* item = nullptr; diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp index d74d395c58..2d4ce34aa3 100644 --- a/rpcs3/rpcs3qt/qt_video_source.cpp +++ b/rpcs3/rpcs3qt/qt_video_source.cpp @@ -1,11 +1,19 @@ #include "stdafx.h" #include "Emu/System.h" +#include "Emu/system_config.h" #include "qt_video_source.h" #include "Loader/ISO.h" +#include #include +static video_source* s_audio_source = nullptr; +static std::unique_ptr s_audio_player = nullptr; +static std::unique_ptr s_audio_output = nullptr; +static std::unique_ptr s_audio_buffer = nullptr; +static std::unique_ptr s_audio_data = nullptr; + qt_video_source::qt_video_source() : video_source() { @@ -21,6 +29,11 @@ void qt_video_source::set_video_path(const std::string& video_path) m_video_path = QString::fromStdString(video_path); } +void qt_video_source::set_audio_path(const std::string& audio_path) +{ + m_audio_path = QString::fromStdString(audio_path); +} + void qt_video_source::set_iso_path(const std::string& iso_path) { m_iso_path = iso_path; @@ -89,7 +102,6 @@ void qt_video_source::init_movie() m_video_buffer = std::make_unique(&m_video_data); m_video_buffer->open(QIODevice::ReadOnly); m_movie = std::make_unique(m_video_buffer.get()); - } if (!m_movie->isValid()) @@ -179,6 +191,8 @@ void qt_video_source::start_movie() m_media_player->play(); } + start_audio(); + m_active = true; } @@ -196,6 +210,71 @@ void qt_video_source::stop_movie() m_media_player.reset(); m_video_buffer.reset(); m_video_data.clear(); + + stop_audio(); +} + +void qt_video_source::start_audio() +{ + if (m_audio_path.isEmpty() || s_audio_source == this) return; + + if (!s_audio_player) + { + s_audio_output = std::make_unique(); + s_audio_player = std::make_unique(); + s_audio_player->setAudioOutput(s_audio_output.get()); + } + + if (m_iso_path.empty()) + { + s_audio_player->setSource(QUrl::fromLocalFile(m_audio_path)); + } + else + { + iso_archive archive(m_iso_path); + auto audio_file = archive.open(m_audio_path.toStdString()); + const auto audio_size = audio_file.size(); + if (audio_size == 0) return; + + std::unique_ptr old_audio_data = std::move(s_audio_data); + s_audio_data = std::make_unique(audio_size, 0); + audio_file.read(s_audio_data->data(), audio_size); + + if (!s_audio_buffer) + { + s_audio_buffer = std::make_unique(); + } + + s_audio_buffer->setBuffer(s_audio_data.get()); + s_audio_buffer->open(QIODevice::ReadOnly); + s_audio_player->setSourceDevice(s_audio_buffer.get()); + + if (old_audio_data) + { + old_audio_data.reset(); + } + } + + s_audio_output->setVolume(g_cfg.audio.volume.get() / 100.0f); + s_audio_player->play(); + s_audio_source = this; +} + +void qt_video_source::stop_audio() +{ + if (s_audio_source != this) return; + + s_audio_source = nullptr; + + if (s_audio_player) + { + s_audio_player->stop(); + s_audio_player.reset(); + } + + s_audio_output.reset(); + s_audio_buffer.reset(); + s_audio_data.reset(); } QPixmap qt_video_source::get_movie_image(const QVideoFrame& frame) const @@ -288,6 +367,14 @@ void qt_video_source_wrapper::set_video_path(const std::string& video_path) }); } +void qt_video_source_wrapper::set_audio_path(const std::string& audio_path) +{ + Emu.CallFromMainThread([this, path = audio_path]() + { + // TODO + }); +} + void qt_video_source_wrapper::set_active(bool active) { Emu.CallFromMainThread([this, active]() diff --git a/rpcs3/rpcs3qt/qt_video_source.h b/rpcs3/rpcs3qt/qt_video_source.h index ce43d593d7..cda92671a2 100644 --- a/rpcs3/rpcs3qt/qt_video_source.h +++ b/rpcs3/rpcs3qt/qt_video_source.h @@ -19,7 +19,9 @@ public: void set_iso_path(const std::string& iso_path); void set_video_path(const std::string& video_path) override; + void set_audio_path(const std::string& audio_path) override; const QString& video_path() const { return m_video_path; } + const QString& audio_path() const { return m_audio_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; } @@ -30,6 +32,9 @@ public: void start_movie(); void stop_movie(); + void start_audio(); + void stop_audio(); + QPixmap get_movie_image(const QVideoFrame& frame) const; void image_change_callback(const QVideoFrame& frame = {}) const; @@ -44,6 +49,7 @@ protected: atomic_t m_has_new = false; QString m_video_path; + QString m_audio_path; std::string m_iso_path; // path of the source archive QByteArray m_video_data{}; QImage m_image{}; @@ -67,6 +73,7 @@ public: virtual ~qt_video_source_wrapper(); void set_video_path(const std::string& video_path) override; + void set_audio_path(const std::string& audio_path) override; void set_active(bool active) override; bool get_active() const override; bool has_new() const override { return m_qt_video_source && m_qt_video_source->has_new(); } diff --git a/rpcs3/rpcs3qt/save_manager_dialog.cpp b/rpcs3/rpcs3qt/save_manager_dialog.cpp index 2dd2a14e86..002df8d527 100644 --- a/rpcs3/rpcs3qt/save_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/save_manager_dialog.cpp @@ -360,6 +360,11 @@ void save_manager_dialog::UpdateList() icon_item->set_video_path(movie_path); } + if (const std::string audio_path = dir_path + "SND0.AT3"; fs::is_file(audio_path)) + { + icon_item->set_audio_path(audio_path); + } + icon_item->set_image_change_callback([this, icon_item](const QVideoFrame& frame) { if (!icon_item) @@ -686,6 +691,7 @@ void save_manager_dialog::UpdateDetails() const SaveDataEntry& save = ::at32(m_save_entries, idx); m_details_icon->set_video_path(icon_item->video_path().toStdString()); + m_details_icon->set_audio_path(icon_item->audio_path().toStdString()); m_details_icon->set_thumbnail(icon_item->data(SaveUserRole::Pixmap).value()); m_details_icon->set_active(false); diff --git a/rpcs3/util/video_source.h b/rpcs3/util/video_source.h index 9449ed238e..a18b564209 100644 --- a/rpcs3/util/video_source.h +++ b/rpcs3/util/video_source.h @@ -9,6 +9,7 @@ public: video_source() {}; virtual ~video_source() {}; virtual void set_video_path(const std::string& video_path) = 0; + virtual void set_audio_path(const std::string& audio_path) = 0; virtual void set_active(bool active) = 0; virtual bool get_active() const = 0; virtual bool has_new() const = 0;