mirror of
https://github.com/kamb205/KB-RPCS3.git
synced 2026-10-17 16:01:41 +02:00
370 lines
13 KiB
C++
370 lines
13 KiB
C++
/*
|
|
* 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 <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// The design on screen: this title's own, in kit/screen.cpp
|
|
std::unique_ptr<hui::app::Concept> 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<hui::app::Context> context;
|
|
std::unique_ptr<hui::app::Concept> 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>(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<std::uint8_t> &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<std::uint8_t> 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<char>(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 <data>/screenshots/home-<time>.ppm
|
|
bool screenshotCombo = false;
|
|
void screenshotOnCombo()
|
|
{
|
|
const bool both = (ps5_pad().held & (PAD_L3 | PAD_R3)) == (PAD_L3 | PAD_R3);
|
|
if (both && !screenshotCombo && !ps5.captureNow) {
|
|
const std::string dir = rpcs3ps5::Library::root() + "/screenshots";
|
|
mkdir(dir.c_str(), 0777);
|
|
chmod(dir.c_str(), 0777);
|
|
char name[64];
|
|
std::snprintf(name, sizeof(name), "/home-%ld.ppm", (long)time(nullptr));
|
|
ps5.screenshotPath = dir + name;
|
|
ps5.captureNow = true;
|
|
rpcs3ps5::set_status("Screenshot saved: rpcs3/screenshots" + std::string(name));
|
|
feedback.play(hui::audio::Cue::select);
|
|
}
|
|
screenshotCombo = both;
|
|
}
|
|
|
|
// Holding OPTIONS for a second leaves; a press is the design's
|
|
void holdOptions(float dt)
|
|
{
|
|
leaving.label = ps5.optionsEnds ? "Back to the menu" : "Close";
|
|
if (!ps5.frameBudget && leaving.update(dt, (ps5_pad().held & PAD_OPTIONS) != 0)) {
|
|
quit = true;
|
|
}
|
|
}
|
|
|
|
void updateTelemetry()
|
|
{
|
|
if (benchmark.active) {
|
|
// A test run's pictures must not depend on the console's timing
|
|
telemetry.push(16.7f);
|
|
telemetry.fps = 59.9f;
|
|
telemetry.average_ms = 16.7f;
|
|
} else {
|
|
telemetry.push(frameTimer * 1000.0f);
|
|
fpsSeconds += frameTimer;
|
|
fpsFrames++;
|
|
if (fpsSeconds >= 0.5) {
|
|
telemetry.fps = (float)(fpsFrames / fpsSeconds);
|
|
telemetry.average_ms = (float)(fpsSeconds * 1000.0 / fpsFrames);
|
|
fpsSeconds = 0.0;
|
|
fpsFrames = 0;
|
|
}
|
|
telemetry.voices = kit.mixer.active_voices();
|
|
}
|
|
telemetry.draw_calls = kit.renderer.last_draw_calls();
|
|
telemetry.instances = kit.renderer.last_instances();
|
|
}
|
|
|
|
void render() override
|
|
{
|
|
if (!prepared) {
|
|
return;
|
|
}
|
|
prepareFrame();
|
|
const float dt = std::min(frameTimer, 0.05f);
|
|
if (splash) {
|
|
kit.tick(dt);
|
|
drawSplash(dt);
|
|
return;
|
|
}
|
|
// A test run ignores the pad: the design shows its entrance and rests
|
|
hui::InputFrame input = kit.input();
|
|
if (ps5.frameBudget) {
|
|
input = hui::InputFrame{};
|
|
input.connected = true;
|
|
}
|
|
holdOptions(dt);
|
|
screenshotOnCombo();
|
|
updateTelemetry();
|
|
screen->update(input, dt, feedback);
|
|
// The home screen never ends on CIRCLE: holding OPTIONS closes the title
|
|
handleRequests();
|
|
if (context->settings_changed) {
|
|
context->settings_changed = false;
|
|
kit.apply(choices);
|
|
}
|
|
kit.play(feedback, screen->info().sounds, choices.haptics);
|
|
feedback.clear();
|
|
if (choices.light_bar) {
|
|
kit.light_bar(screen->info().accent);
|
|
}
|
|
kit.tick(dt);
|
|
|
|
// backdrop, scene, [the glass copy], overlay, post: the order a design is drawn in
|
|
frame.reset();
|
|
frame.glass_texture = kit.renderer.glass_texture();
|
|
screen->draw(frame);
|
|
kit.renderer.begin();
|
|
kit.renderer.backdrop(frame.backdrop);
|
|
kit.renderer.draw(frame.scene);
|
|
if (frame.glass) {
|
|
kit.renderer.glass();
|
|
}
|
|
kit.renderer.draw(frame.overlay);
|
|
kit.renderer.backdrop(frame.post);
|
|
leavingList.clear();
|
|
leaving.draw(leavingList, kit.fonts, ps5ui::active_theme());
|
|
kit.renderer.draw(leavingList);
|
|
buildKitCommandBuffer();
|
|
submitFrame();
|
|
}
|
|
|
|
~VulkanExample() override
|
|
{
|
|
if (device) {
|
|
vkDeviceWaitIdle(device);
|
|
rpcs3ps5::library().release(kit.renderer);
|
|
if (splashBackground) kit.renderer.destroy_texture(splashBackground);
|
|
if (splashCover) kit.renderer.destroy_texture(splashCover);
|
|
}
|
|
}
|
|
};
|
|
|
|
VULKAN_EXAMPLE_MAIN()
|