/* * RPCS3 PS5 - its program, made from the PS5 Vulkan Template's UI starter. * * One complete design of PS5_VKHomebrewUI's kit (ps5/ui/kit.hpp) fills the * screen: its procedural backdrop, its scene, its frosted overlay and its post * overlay, its sounds on the console's audio output, its rumble and its light * bar colour. There is no switcher: every button is the design's, and holding * OPTIONS for a second leaves (back to the menu, or out of a title of one * program). The design is this title's copy of the kit's "aurora", in * kit/screen.cpp, to change at will. * * Copyright (C) 2026 Mihawk * SPDX-License-Identifier: GPL-3.0-or-later */ #include "kit.hpp" #include "kit/library.hpp" #include "kit/requests.hpp" #include "kit/core.hpp" #include "kit/tiles.hpp" #include #include #include // The design on screen: this title's own, in kit/screen.cpp std::unique_ptr makeScreen(hui::app::Context &context) { return ps5ui::make_screen(context); } class VulkanExample : public ps5ui::KitExample { public: // What a design receives: the fonts, the sample content, the live numbers // it may show and the settings it may change hui::Settings choices; hui::app::Telemetry telemetry; std::unique_ptr context; std::unique_ptr screen; hui::app::Frame frame; hui::ui::Feedback feedback; ps5ui::HoldToLeave leaving; hui::gfx::DrawList leavingList; double fpsSeconds{ 0.0 }; int fpsFrames{ 0 }; // Started from a game's PS5 home-screen tile: its launch screen, then the game (main.cpp) bool splash{ false }; float splashTime{ 0.0f }; std::uint32_t splashBackground{ 0 }, splashCover{ 0 }; int splashCoverW{ 0 }, splashCoverH{ 0 }; std::string splashTitle; VulkanExample() : KitExample() { title = "KB-RPCS3"; name = "rpcs3ps5"; // The kit draws the whole screen and owns the pad settings.overlay = false; ps5.ownOverlay = true; defaultClearColor = { { 0.0f, 0.0f, 0.0f, 1.0f } }; } void prepare() override { VulkanExampleBase::prepare(); prepareKit(); rpcs3ps5::library().scan(&kit.renderer); reportScan(); context = std::make_unique(hui::app::Context{ kit.fonts, kit.catalog, telemetry, choices }); screen = makeScreen(*context); screen->enter(); kit.apply(choices); if (!rpcs3ps5::launch_splash().empty()) { prepareSplash(rpcs3ps5::launch_splash()); } else { feedback.play(hui::audio::Cue::welcome); } prepared = true; } // An image file as a texture (0 when it can't be read) std::uint32_t loadTexture(const std::vector &file, int *width = nullptr, int *height = nullptr) { int w = 0, h = 0, channels = 0; stbi_uc *rgba = file.empty() ? nullptr : stbi_load_from_memory(file.data(), (int)file.size(), &w, &h, &channels, 4); if (!rgba) { return 0; } const std::uint32_t texture = kit.renderer.create_texture(w, h, rgba); stbi_image_free(rgba); if (width) *width = w; if (height) *height = h; return texture; } void prepareSplash(const std::string &boot) { splash = true; for (const rpcs3ps5::Game &game : rpcs3ps5::library().games()) { if (game.boot != boot) { continue; } splashTitle = game.title; std::vector file; if (rpcs3ps5::game_file(game, "PIC1.PNG", file)) { splashBackground = loadTexture(file); } std::ifstream cover(rpcs3ps5::cover_path(game.title_id), std::ios::binary); file.assign(std::istreambuf_iterator(cover), {}); splashCover = loadTexture(file, &splashCoverW, &splashCoverH); if (!splashCover && game_file(game, "ICON0.PNG", file)) { splashCover = loadTexture(file, &splashCoverW, &splashCoverH); } break; } rpcs3ps5::trace("splash: %s (background %u, cover %ux%u)", splashTitle.c_str(), splashBackground, splashCoverW, splashCoverH); } // The launch screen: the game's background, dimmed, its box art on the left and its name void drawSplash(float dt) { using hui::gfx::Color; splashTime += dt; if (splashTime > 2.6f) { quit = true; // main.cpp starts the game } const float fade = std::min(1.0f, splashTime / 0.5f) * std::min(1.0f, std::max(0.0f, (2.6f - splashTime) / 0.3f)); const float W = hui::gfx::kVirtualWidth, H = hui::gfx::kVirtualHeight; frame.reset(); frame.backdrop.mode = hui::gfx::BackdropMode::gradient; frame.backdrop.colors[0] = Color::rgb(0x000000); frame.backdrop.colors[1] = Color::rgb(0x05070c); frame.backdrop.colors[2] = Color::rgb(0x10131c); hui::gfx::DrawList &list = frame.scene; list.push_opacity(fade); if (splashBackground) { // a slow push-in const float grow = 1.0f + 0.03f * std::min(1.0f, splashTime / 2.6f); const float w = W * grow, h = H * grow; list.image(splashBackground, { (W - w) * 0.5f, (H - h) * 0.5f, w, h }, hui::gfx::kFullUv, Color::rgb(0xffffff)); } list.gradient_rect_h({ 0, 0, W, H }, 0, Color::rgb(0x000000, 0.78f), Color::rgb(0x000000, 0.25f)); float textX = 160.0f; if (splashCover && splashCoverH > 0) { const float h = 720.0f; const float w = std::min(620.0f, h * (float)splashCoverW / (float)splashCoverH); const float y = (H - h) * 0.5f; list.glow({ 160, y, w, h }, 12, 60, Color::rgb(0x000000, 0.6f)); list.image(splashCover, { 160, y, w, h }, hui::gfx::kFullUv, Color::rgb(0xffffff), 12); textX = 160.0f + w + 90.0f; } hui::ui::text(list, kit.fonts.semibold, splashTitle, textX, H * 0.5f - 10, 64, Color::rgb(0xffffff)); const int dots = 1 + (int)(splashTime * 3.0f) % 3; hui::ui::text(list, kit.fonts.regular, std::string("Starting") + std::string(dots, '.'), textX, H * 0.5f + 60, 30, Color::rgb(0xffffff, 0.75f)); list.pop_opacity(); kit.renderer.begin(); kit.renderer.backdrop(frame.backdrop); kit.renderer.draw(frame.scene); buildKitCommandBuffer(); submitFrame(); } void reportScan() { const auto &lib = rpcs3ps5::library(); rpcs3ps5::set_status(std::to_string(lib.games().size()) + " games found" + (lib.firmware_installed() ? "" : " \xC2\xB7 PS3 system software not installed yet")); } // What the home screen asked for this frame void handleRequests() { // A game was deleted (on the tiles thread): list the library again if (rpcs3ps5::take_rescan_wanted()) { vkDeviceWaitIdle(device); rpcs3ps5::library().scan(&kit.renderer); } const rpcs3ps5::Pending pending = rpcs3ps5::take_request(); const auto &games = rpcs3ps5::library().games(); const bool valid_game = pending.argument >= 0 && pending.argument < (int)games.size(); switch (pending.what) { case rpcs3ps5::Request::tile_add: if (valid_game) rpcs3ps5::tile_add_async(games[pending.argument]); break; case rpcs3ps5::Request::tile_remove: if (valid_game) rpcs3ps5::tile_remove_async(games[pending.argument]); break; case rpcs3ps5::Request::delete_game: case rpcs3ps5::Request::delete_game_and_saves: if (valid_game) rpcs3ps5::game_delete_async(games[pending.argument], pending.what == rpcs3ps5::Request::delete_game_and_saves); break; case rpcs3ps5::Request::none: break; case rpcs3ps5::Request::rescan: vkDeviceWaitIdle(device); rpcs3ps5::library().scan(&kit.renderer); reportScan(); break; case rpcs3ps5::Request::boot: if (pending.argument >= 0 && pending.argument < (int)games.size()) { if (!rpcs3ps5::core_linked()) { rpcs3ps5::set_status("Booting " + games[pending.argument].title + ": the emulator core is not linked into this build yet"); } else if (!rpcs3ps5::library().firmware_installed()) { rpcs3ps5::set_status("Install the PS3 system software first (Install tab)"); } else if (rpcs3ps5::core_busy() || rpcs3ps5::tiles_busy()) { rpcs3ps5::set_status("Wait for the install to finish"); } else { // The home screen closes; main.cpp runs the game and brings it back after rpcs3ps5::set_boot_path(games[pending.argument].boot); quit = true; } } break; case rpcs3ps5::Request::game_settings: rpcs3ps5::set_status("Per-game settings: coming with the emulator core"); break; case rpcs3ps5::Request::install_firmware: if (rpcs3ps5::library().firmware_pup_present()) { rpcs3ps5::core_install_firmware_async(rpcs3ps5::Library::root() + "/PS3UPDAT.PUP"); } else { rpcs3ps5::set_status("No PS3UPDAT.PUP in " + rpcs3ps5::Library::root()); } break; case rpcs3ps5::Request::install_packages: rpcs3ps5::core_install_packages_async(); break; case rpcs3ps5::Request::install_licenses: { const int count = rpcs3ps5::core_install_licenses(); rpcs3ps5::set_status(count ? std::to_string(count) + " licence(s) (.rap) installed \xC2\xB7 PSN games that need them can start now" : "No .rap files in " + rpcs3ps5::Library::root() + "/exdata: copy them there over FTP, then choose this again"); break; } case rpcs3ps5::Request::toggle_setting: // The row past the settings is a game's "use the global settings" if (pending.argument >= rpcs3ps5::setting_count()) { rpcs3ps5::reset_game_settings(); rpcs3ps5::set_status(rpcs3ps5::settings_game_title() + " uses the global settings"); } else { rpcs3ps5::cycle_setting(pending.argument); rpcs3ps5::set_status(std::string(rpcs3ps5::setting_label(pending.argument)) + ": " + rpcs3ps5::setting_text(pending.argument) + (rpcs3ps5::settings_for_game() ? " \xC2\xB7 saved for " + rpcs3ps5::settings_game_title() : " \xC2\xB7 saved for all games")); } break; } } // L3 + R3 (both sticks pressed): the screen to /screenshots/home-