From fdcfded8dfd3060af66bda0a3ac4635458980038 Mon Sep 17 00:00:00 2001 From: BeezBumba <130922034+BeezBumba@users.noreply.github.com> Date: Tue, 25 Aug 2026 06:11:08 -0500 Subject: [PATCH] Add Big Picture Mode using RSX overlay UI --- rpcs3/Emu/CMakeLists.txt | 5 + .../BigPicture/overlay_big_picture.cpp | 174 +++++++++ .../Overlays/BigPicture/overlay_big_picture.h | 39 ++ .../overlay_big_picture_game_details.cpp | 146 +++++++ .../overlay_big_picture_game_details.h | 53 +++ .../overlay_big_picture_game_grid.cpp | 358 ++++++++++++++++++ .../overlay_big_picture_game_grid.h | 72 ++++ .../overlay_big_picture_main_menu.cpp | 28 ++ .../overlay_big_picture_main_menu.h | 18 + .../HomeMenu/overlay_home_menu_main_menu.cpp | 148 +------- .../HomeMenu/overlay_home_menu_main_menu.h | 25 +- .../overlay_home_menu_sidebar_page.cpp | 159 ++++++++ .../HomeMenu/overlay_home_menu_sidebar_page.h | 37 ++ rpcs3/Emu/System.cpp | 92 ++++- rpcs3/Emu/System.h | 5 + rpcs3/Emu/localized_string_id.h | 9 + rpcs3/Emu/system_config.h | 1 + rpcs3/emucore.vcxproj | 10 + rpcs3/emucore.vcxproj.filters | 30 ++ rpcs3/rpcs3.cpp | 4 + rpcs3/rpcs3qt/emu_settings_type.cpp | 1 + rpcs3/rpcs3qt/emu_settings_type.h | 1 + rpcs3/rpcs3qt/localized_emu.h | 8 + rpcs3/rpcs3qt/main_window.cpp | 28 ++ rpcs3/rpcs3qt/main_window.ui | 10 + rpcs3/rpcs3qt/settings_dialog.cpp | 3 + rpcs3/rpcs3qt/settings_dialog.ui | 7 + rpcs3/rpcs3qt/tooltips.h | 1 + 28 files changed, 1300 insertions(+), 172 deletions(-) create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.cpp create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.h create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.cpp create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.h create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_grid.cpp create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_grid.h create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_main_menu.cpp create mode 100644 rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_main_menu.h create mode 100644 rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_sidebar_page.cpp create mode 100644 rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_sidebar_page.h diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 8293047ccc..42e363665d 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -518,6 +518,11 @@ target_sources(rpcs3_emu PRIVATE RSX/Overlays/HomeMenu/overlay_home_menu_page.cpp RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp RSX/Overlays/HomeMenu/overlay_home_menu_savestate.cpp + RSX/Overlays/HomeMenu/overlay_home_menu_sidebar_page.cpp + RSX/Overlays/BigPicture/overlay_big_picture.cpp + RSX/Overlays/BigPicture/overlay_big_picture_game_details.cpp + RSX/Overlays/BigPicture/overlay_big_picture_game_grid.cpp + RSX/Overlays/BigPicture/overlay_big_picture_main_menu.cpp RSX/Overlays/Network/overlay_recvmessage_dialog.cpp RSX/Overlays/Network/overlay_sendmessage_dialog.cpp RSX/Overlays/Trophies/overlay_trophy_list_dialog.cpp diff --git a/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.cpp b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.cpp new file mode 100644 index 0000000000..89f329d000 --- /dev/null +++ b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.cpp @@ -0,0 +1,174 @@ +#include "stdafx.h" +#include "overlay_big_picture.h" +#include "../overlay_manager.h" +#include "Emu/System.h" + +atomic_t g_big_picture_mode_active = false; + +namespace rsx +{ + namespace overlays + { + namespace + { + void boot_game_from_big_picture_mode(std::string path, std::string title_id) + { + rsx_log.notice("Big Picture Mode: starting boot handoff for '%s' (path='%s')", title_id, path); + + Emu.CallFromMainThread([path, title_id]() + { + // Keep the game window alive across the handoff instead of closing and reopening it. + Emu.SetContinuousMode(true); + rsx_log.notice("Big Picture Mode: shutting down shell before boot"); + Emu.GracefulShutdown(false); + rsx_log.notice("Big Picture Mode: shell shut down, is_stopped=%d", Emu.IsStopped()); + + // Only mark the session as BPM-launched now, so the transitional shutdown above doesn't + // itself get treated as "the game closed" and re-trigger Big Picture Mode. + g_big_picture_mode_active = true; + + const game_boot_result result = Emu.BootGame(path, title_id); + rsx_log.notice("Big Picture Mode: BootGame result=%s", result); + + if (is_error(result)) + { + g_big_picture_mode_active = false; + } + }); + } + } + + big_picture_dialog::big_picture_dialog() + : m_main_menu(20, 85, virtual_width - 2 * 20, 540, nullptr, &boot_game_from_big_picture_mode) + { + m_allow_input_on_pause = true; + + m_dim_background.set_size(virtual_width, virtual_height); + m_dim_background.back_color.a = 0.85f; + + m_description.set_font("Arial", 20); + m_description.set_pos(20, 37); + m_description.set_text(get_localized_string(localized_string_id::BIG_PICTURE_MODE_TITLE)); + m_description.auto_resize(); + m_description.back_color.a = 0.f; + + fade_animation.duration_sec = 0.15f; + + return_code = selection_code::canceled; + } + + void big_picture_dialog::update(u64 timestamp_us) + { + if (fade_animation.active) + { + fade_animation.update(timestamp_us); + } + + m_main_menu.update(timestamp_us); + } + + void big_picture_dialog::on_button_pressed(pad_button button_press, bool is_auto_repeat) + { + if (fade_animation.active) return; + + switch (button_press) + { + case pad_button::dpad_left: + case pad_button::dpad_right: + case pad_button::ls_left: + case pad_button::ls_right: + m_auto_repeat_ms_interval = 10; + break; + default: + m_auto_repeat_ms_interval = m_auto_repeat_ms_interval_default; + break; + } + + const page_navigation navigation = m_main_menu.handle_button_press(button_press, is_auto_repeat, m_auto_repeat_ms_interval); + + switch (navigation) + { + case page_navigation::back: + case page_navigation::next: + { + if (home_menu_page* page = m_main_menu.get_current_page(true)) + { + std::string path = page->title; + for (home_menu_page* parent = page->parent; parent; parent = parent->parent) + { + if (parent->title.empty()) + { + break; + } + + path = parent->title + " > " + path; + } + m_description.set_text(path.empty() ? get_localized_string(localized_string_id::BIG_PICTURE_MODE_TITLE) : path); + m_description.auto_resize(); + } + break; + } + case page_navigation::exit: + { + // Don't call close() synchronously from the input thread - just like the pause menu's own + // "Exit Game", tearing down the shell on the main thread takes this dialog down as a side effect. + g_big_picture_mode_active = false; + + Emu.CallFromMainThread([]() + { + Emu.GracefulShutdown(true, true); + }); + break; + } + default: + break; + } + } + + compiled_resource big_picture_dialog::get_compiled() + { + if (!visible) + { + return {}; + } + + compiled_resource result; + result.add(m_dim_background.get_compiled()); + result.add(m_main_menu.get_compiled()); + result.add(m_description.get_compiled()); + + fade_animation.apply(result); + + return result; + } + + void big_picture_dialog::show() + { + visible = false; + + fade_animation.current = color4f(0.f); + fade_animation.end = color4f(1.f); + fade_animation.active = true; + + visible = true; + + auto& overlayman = g_fxo->get(); + overlayman.attach_thread_input(uid, "Big Picture Mode"); + } + + void open_big_picture_mode() + { + auto& overlayman = g_fxo->get(); + const auto dialog = overlayman.create(); + dialog->show(); + + // Both exit paths (starting a game or leaving Big Picture Mode) tear down this shell via + // Emu.GracefulShutdown on the main thread, which destroys the display_manager (and this dialog + // with it) as a side effect. Just wait for that to happen instead of waiting on the dialog itself. + while (!Emu.IsStopped()) + { + thread_ctrl::wait_for(50'000); + } + } + } // namespace overlays +} // namespace rsx diff --git a/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.h b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.h new file mode 100644 index 0000000000..0e4f7abbbb --- /dev/null +++ b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture.h @@ -0,0 +1,39 @@ +#pragma once + +#include "Emu/RSX/Overlays/overlays.h" +#include "Emu/Cell/ErrorCodes.h" +#include "overlay_big_picture_main_menu.h" + +namespace rsx +{ + namespace overlays + { + struct big_picture_dialog : public user_interface + { + public: + big_picture_dialog(); + + void update(u64 timestamp_us) override; + void on_button_pressed(pad_button button_press, bool is_auto_repeat) override; + + compiled_resource get_compiled() override; + + void show(); + + private: + big_picture_main_menu m_main_menu; + overlay_element m_dim_background{}; + label m_description{}; + + animation_color_interpolate fade_animation{}; + }; + + // Entry point run on the dedicated thread started by Emulator::BootBigPictureMode(). + // Blocks until Big Picture Mode's shell is torn down (either the user exited it, or picked a game to start). + void open_big_picture_mode(); + } +} + +// True while the currently running/booting game was launched from Big Picture Mode. +// Checked by the Qt front-end so exiting the game re-enters Big Picture Mode instead of the bare game list. +extern atomic_t g_big_picture_mode_active; diff --git a/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.cpp b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.cpp new file mode 100644 index 0000000000..32f2e81319 --- /dev/null +++ b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.cpp @@ -0,0 +1,146 @@ +#include "stdafx.h" +#include "overlay_big_picture_game_details.h" +#include "Emu/RSX/Overlays/HomeMenu/overlay_home_menu_components.h" + +namespace rsx +{ + namespace overlays + { + big_picture_game_details::big_picture_game_details(s16 x, s16 y, u16 width, u16 height) + : m_x(x), m_y(y), m_w(width), m_h(height) + { + m_pic_background.set_pos(0, 0); + m_pic_background.set_size(overlay::virtual_width, overlay::virtual_height); + m_pic_background.back_color.a = 0.f; + + m_dim_background.set_pos(0, 0); + m_dim_background.set_size(overlay::virtual_width, overlay::virtual_height); + m_dim_background.back_color.a = 0.75f; + + m_icon.set_pos(x + 40, y + 40); + m_icon.set_size(320, 176); + m_icon.back_color.a = 0.f; + + m_title.set_font("Arial", 28); + m_title.set_pos(x + 40, y + 240); + m_title.back_color.a = 0.f; + m_title.set_wrap_text(true); + m_title.set_size(width - 80, 80); + + m_info_text.set_font("Arial", 16); + m_info_text.set_pos(x + 40, y + 330); + m_info_text.back_color.a = 0.f; + m_info_text.set_wrap_text(true); + m_info_text.set_size(width - 80, 200); + + m_start_btn.set_image_resource(resource_config::standard_image_resource::cross); + m_start_btn.set_text(localized_string_id::BIG_PICTURE_GAME_DETAILS_START); + m_start_btn.set_font("Arial", 16); + m_start_btn.set_pos(x + 40, y + height - 80); + + m_back_hint.set_image_resource(resource_config::standard_image_resource::circle); + m_back_hint.set_text(localized_string_id::BIG_PICTURE_HINT_BACK); + m_back_hint.set_font("Arial", 16); + m_back_hint.set_pos(x + 40 + 120 + 20, y + height - 80); + } + + void big_picture_game_details::show(const GameInfo& info, const image_info* icon_data) + { + m_title.set_text(info.name.empty() ? info.serial : info.name); + m_title.auto_resize(false, m_w - 80); + + m_info_text.set_text(fmt::format("%s\n%s\n%s", info.serial, info.category, info.app_ver)); + + const std::string game_dir = fs::get_parent_dir(info.icon_path); + + if (const std::string pic1_path = game_dir + "/PIC1.PNG"; fs::is_file(pic1_path)) + { + m_pic_data = std::make_unique(pic1_path); + // The renderer's texture cache is keyed by this object's address, which can be reused by an + // unrelated image after the old one is freed - force a re-upload instead of trusting the cache. + m_pic_data->dirty = true; + m_pic_background.set_raw_image(m_pic_data.get()); + m_pic_background.back_color.a = 0.35f; + } + else + { + m_pic_data.reset(); + m_pic_background.clear_image(); + m_pic_background.back_color.a = 0.f; + } + + m_pic_background.refresh(); + + if (const std::string icon1_path = game_dir + "/ICON1.PAM"; fs::is_file(icon1_path)) + { + m_video = std::make_unique(icon1_path, "", info.icon_path); + m_video->set_pos(m_icon.x, m_icon.y); + m_video->set_size(m_icon.w, m_icon.h); + m_video->set_active(true); + } + else + { + m_video.reset(); + + if (icon_data) + { + m_icon.set_raw_image(icon_data); + } + else + { + m_icon.set_image_resource(resource_config::standard_image_resource::new_entry); + } + + // set_raw_image()/set_image_resource() don't invalidate the compiled vertex cache themselves. + m_icon.refresh(); + } + + m_visible = true; + } + + void big_picture_game_details::hide() + { + m_visible = false; + + if (m_video) + { + m_video->set_active(false); + } + } + + big_picture_game_details::result big_picture_game_details::handle_button_press(pad_button button_press) + { + switch (button_press) + { + case pad_button::cross: + play_sound(sound_effect::accept); + return result::start; + case pad_button::circle: + play_sound(sound_effect::cancel); + return result::back; + default: + return result::stay; + } + } + + compiled_resource& big_picture_game_details::get_compiled() + { + m_compiled = {}; + + if (!m_visible) + { + return m_compiled; + } + + m_compiled.add(m_pic_background.get_compiled()); + m_compiled.add(m_dim_background.get_compiled()); + m_compiled.add(m_video ? m_video->get_compiled() : m_icon.get_compiled()); + m_compiled.add(m_title.get_compiled()); + m_compiled.add(m_info_text.get_compiled()); + m_compiled.add(m_start_btn.get_compiled()); + m_compiled.add(m_back_hint.get_compiled()); + + return m_compiled; + } + } +} diff --git a/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.h b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.h new file mode 100644 index 0000000000..3dc87b2339 --- /dev/null +++ b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_details.h @@ -0,0 +1,53 @@ +#pragma once + +#include "Emu/RSX/Overlays/overlays.h" +#include "Emu/RSX/Overlays/overlay_controls.h" +#include "Emu/RSX/Overlays/overlay_video.h" +#include "Emu/GameInfo.h" + +namespace rsx +{ + namespace overlays + { + // A full-screen panel shown on top of the game grid, describing the highlighted game with a "Start" prompt. + struct big_picture_game_details + { + public: + big_picture_game_details(s16 x, s16 y, u16 width, u16 height); + + enum class result + { + stay, + back, + start + }; + + void show(const GameInfo& info, const image_info* icon_data); + void hide(); + bool is_visible() const { return m_visible; } + + result handle_button_press(pad_button button_press); + + compiled_resource& get_compiled(); + + private: + s16 m_x = 0; + s16 m_y = 0; + u16 m_w = 0; + u16 m_h = 0; + + bool m_visible = false; + compiled_resource m_compiled; + + overlay_element m_dim_background{}; + image_view m_pic_background{}; + std::unique_ptr m_pic_data; + image_view m_icon{}; + std::unique_ptr m_video; + label m_title{}; + label m_info_text{}; + image_button m_start_btn{ 120, 30 }; + image_button m_back_hint{ 120, 30 }; + }; + } +} diff --git a/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_grid.cpp b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_grid.cpp new file mode 100644 index 0000000000..3b417d0684 --- /dev/null +++ b/rpcs3/Emu/RSX/Overlays/BigPicture/overlay_big_picture_game_grid.cpp @@ -0,0 +1,358 @@ +#include "stdafx.h" +#include "overlay_big_picture_game_grid.h" +#include "overlay_big_picture_game_details.h" +#include "Emu/System.h" +#include "Emu/system_utils.hpp" +#include "Loader/PSF.h" + +#include + +namespace rsx +{ + namespace overlays + { + big_picture_game_tile::big_picture_game_tile(const big_picture_game_entry& entry, u16 tile_width) + { + pack_padding = 8; + back_color.a = 0.f; + + const u16 icon_h = icon_height(tile_width); + + std::unique_ptr icon = std::make_unique(); + icon->set_size(tile_width, icon_h); + icon->back_color = color4f(1.f, 1.f, 1.f, 0.08f); + + if (fs::exists(entry.info.icon_path)) + { + m_icon_data = std::make_unique(entry.info.icon_path); + // The renderer's texture cache is keyed by this object's address, which can be reused by an + // unrelated image after the old one is freed - force a re-upload instead of trusting the cache. + m_icon_data->dirty = true; + static_cast(icon.get())->set_raw_image(m_icon_data.get()); + } + else + { + static_cast(icon.get())->set_image_resource(resource_config::standard_image_resource::new_entry); + } + + std::unique_ptr title = std::make_unique