mirror of
https://github.com/kamb205/KB-RPCS3.git
synced 2026-10-17 23:00:50 +02:00
282 lines
9.4 KiB
C++
282 lines
9.4 KiB
C++
/*
|
|
* PS5 Vulkan Template - the start screen: Samples, Designs or Themes, drawn with the UI kit.
|
|
*
|
|
* The samples title opens on it when it has the UI module. Samples leads to the
|
|
* menu of Vulkan samples, Designs to the kit's gallery of designs (uikit), Themes
|
|
* to the picker of the title's theme, and each comes back here. Like the menu,
|
|
* it is a program on the base class that ends once a choice is made, so what
|
|
* runs next starts on a device of its own. It wears the title's theme; its
|
|
* cards' artwork is drawn by the kit at start-up (render_to_texture).
|
|
*
|
|
* Copyright (C) 2026 Mihawk
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include "kit.hpp"
|
|
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
|
|
using hui::gfx::Color;
|
|
using hui::gfx::Rect;
|
|
|
|
enum Item { samplesItem, designsItem, themesItem, quitItem, itemCount };
|
|
constexpr int cardCount = 3;
|
|
|
|
class StartScreen : public ps5ui::KitExample
|
|
{
|
|
public:
|
|
int chosen{ -2 }; // -2: still choosing, -1: Quit, else samplesItem, designsItem or themesItem
|
|
int focus;
|
|
int lastCard; // where the focus goes back to from Quit
|
|
hui::tween::Spring shown[itemCount];
|
|
uint32_t art[cardCount]{};
|
|
const hui::ui::Theme &theme = ps5ui::active_theme();
|
|
hui::gfx::DrawList scene;
|
|
hui::ui::Feedback feedback;
|
|
float clock{ 0.0f };
|
|
|
|
explicit StartScreen(int selected)
|
|
: focus(selected >= samplesItem && selected < cardCount ? selected : samplesItem), lastCard(focus)
|
|
{
|
|
title = PS5_TITLE_NAME;
|
|
name = "start";
|
|
settings.overlay = false;
|
|
ps5.ownOverlay = true;
|
|
defaultClearColor = { { 0.0f, 0.0f, 0.0f, 1.0f } };
|
|
}
|
|
|
|
void prepare() override
|
|
{
|
|
VulkanExampleBase::prepare();
|
|
prepareKit({ .music = false, .covers = false });
|
|
makeArt();
|
|
for (int i = 0; i < itemCount; i++) {
|
|
shown[i].snap(i == focus ? 1.0f : 0.0f);
|
|
}
|
|
prepared = true;
|
|
}
|
|
|
|
uint32_t paint(const hui::gfx::BackdropSpec &backdrop, const hui::gfx::DrawList &list)
|
|
{
|
|
kit.renderer.begin();
|
|
kit.renderer.backdrop(backdrop);
|
|
kit.renderer.draw(list);
|
|
return kit.renderer.render_to_texture(800, 500, 800.0f, 500.0f);
|
|
}
|
|
|
|
// The cards' artwork: a procedural backdrop and a few shapes each
|
|
void makeArt()
|
|
{
|
|
hui::gfx::DrawList list;
|
|
|
|
// Samples: a warm synthwave floor under its sun
|
|
hui::gfx::BackdropSpec floor;
|
|
floor.mode = hui::gfx::BackdropMode::grid;
|
|
floor.colors[0] = Color::rgb(0x0e0707);
|
|
floor.colors[1] = Color::rgb(0x3a1610);
|
|
floor.colors[2] = Color::rgb(0xff7a45);
|
|
floor.colors[3] = Color::rgb(0xffc857);
|
|
floor.time = 3.0f;
|
|
art[samplesItem] = paint(floor, list);
|
|
|
|
// Designs: one card in a fan of design languages, on light paper
|
|
hui::gfx::BackdropSpec sheet;
|
|
sheet.mode = hui::gfx::BackdropMode::paper;
|
|
sheet.colors[0] = Color::rgb(0xf4efe4);
|
|
sheet.colors[1] = Color::rgb(0xe3dccb);
|
|
sheet.colors[2] = Color::rgb(0xffffff);
|
|
const uint32_t fills[] = { 0xffb3c7, 0xffd166, 0x7cf0c8, 0x8fb8ff, 0xffffff };
|
|
for (int i = 0; i < 5; i++) {
|
|
const float angle = -0.42f + 0.21f * (float)i;
|
|
const Rect card{ 250.0f + 40.0f * (float)i - 40.0f, 120.0f, 220.0f, 260.0f };
|
|
list.shadow(card, 26.0f, 30.0f, Color::rgb(0x000000, 0.35f));
|
|
list.rotated_rect(card, i == 4 ? 26.0f : 8.0f + 4.0f * (float)i, angle, Color::rgb(fills[i], i == 4 ? 1.0f : 0.9f));
|
|
}
|
|
list.rounded_rect({ 420.0f, 170.0f, 120.0f, 18.0f }, 9.0f, Color::rgb(0x2b2b33));
|
|
list.rounded_rect({ 420.0f, 204.0f, 150.0f, 12.0f }, 6.0f, Color::rgb(0x2b2b33, 0.45f));
|
|
list.arc(470.0f, 300.0f, 42.0f, 10.0f, 0.0f, 4.4f, Color::rgb(0xff5d8f));
|
|
art[designsItem] = paint(sheet, list);
|
|
|
|
// Themes: a swatch of every theme, its surface, its outline and its main colour
|
|
hui::gfx::BackdropSpec dusk;
|
|
dusk.mode = hui::gfx::BackdropMode::gradient;
|
|
dusk.colors[0] = Color::rgb(0x1a1b22);
|
|
dusk.colors[1] = Color::rgb(0x0b0b0e);
|
|
list.clear();
|
|
const auto all = hui::ui::themes();
|
|
for (size_t i = 0; i < all.size(); i++) {
|
|
const hui::ui::Theme &t = all[i];
|
|
const float x = 70.0f + 112.0f * (float)(i % 6);
|
|
const float y = 40.0f + 86.0f * (float)(i / 6);
|
|
const Rect swatch{ x, y, 100.0f, 74.0f };
|
|
list.rounded_rect(swatch.inset(-3.0f), 15.0f, t.page);
|
|
list.bordered_rect(swatch, 12.0f, t.surface, 2.0f, t.outline.a > 0.0f ? t.outline : t.surface_high);
|
|
list.rounded_rect({ x + 14.0f, y + 42.0f, 52.0f, 16.0f }, 8.0f, t.primary);
|
|
list.rounded_rect({ x + 14.0f, y + 18.0f, 70.0f, 8.0f }, 4.0f, t.text.with_alpha(0.8f));
|
|
}
|
|
art[themesItem] = paint(dusk, list);
|
|
}
|
|
|
|
void move(int to)
|
|
{
|
|
if (to == focus) {
|
|
return;
|
|
}
|
|
feedback.play(hui::audio::Cue::focus, 1.0f, to < cardCount ? 0.35f * (float)(to - 1) : 0.0f);
|
|
focus = to;
|
|
if (focus < cardCount) {
|
|
lastCard = focus;
|
|
}
|
|
}
|
|
|
|
void update(float dt)
|
|
{
|
|
feedback.clear();
|
|
hui::InputFrame input = kit.input();
|
|
if (ps5.frameBudget) {
|
|
input = kit.scripted("start", ps5.framesDrawn);
|
|
}
|
|
clock += dt;
|
|
const bool onCards = focus < cardCount;
|
|
if (input.nav == hui::Direction::left && onCards && focus > 0) {
|
|
move(focus - 1);
|
|
} else if (input.nav == hui::Direction::right && onCards && focus < cardCount - 1) {
|
|
move(focus + 1);
|
|
} else if (input.nav == hui::Direction::down && onCards) {
|
|
move(quitItem);
|
|
} else if (input.nav == hui::Direction::up && !onCards) {
|
|
move(lastCard);
|
|
} else if (input.nav != hui::Direction::none && !input.nav_repeat) {
|
|
feedback.play(hui::audio::Cue::error, 1.0f, 0.0f, 0.5f);
|
|
}
|
|
if (input.is_pressed(hui::Action::confirm)) {
|
|
feedback.play(hui::audio::Cue::select);
|
|
chosen = focus == quitItem ? -1 : focus;
|
|
}
|
|
for (int i = 0; i < itemCount; i++) {
|
|
shown[i].target = i == focus ? 1.0f : 0.0f;
|
|
shown[i].update(dt, theme.omega);
|
|
}
|
|
kit.play(feedback, theme.sounds);
|
|
kit.tick(dt);
|
|
}
|
|
|
|
void compose()
|
|
{
|
|
const uint32_t glass = kit.renderer.glass_texture();
|
|
scene.clear();
|
|
hui::ui::Canvas canvas{ scene, kit.fonts, glass, clock };
|
|
hui::ui::Painter painter(scene, kit.fonts, theme, glass);
|
|
const hui::ui::ComponentStyle style{ theme };
|
|
|
|
// The entrance: the title fades in, then the cards rise one after another
|
|
const auto arrive = [this](float delay, float length) {
|
|
return hui::tween::smoothstep(std::clamp((clock - delay) / length, 0.0f, 1.0f));
|
|
};
|
|
const float heading = arrive(0.0f, 0.5f);
|
|
scene.push_opacity(heading);
|
|
scene.push_transform(1.0f, 0.0f, 0.0f, 0.0f, (1.0f - heading) * 24.0f);
|
|
painter.label("PLAYSTATION 5 HOMEBREW", 160.0f, 150.0f, 22.0f, painter.page_text_muted());
|
|
painter.heading(PS5_TITLE_NAME, 156.0f, 252.0f, 96.0f, painter.page_text());
|
|
painter.body("Vulkan 1.4 on RADV, at 3840 x 2160: samples of the API, interface designs and thirty themes",
|
|
160.0f, 308.0f, 26.0f, painter.page_text_muted());
|
|
scene.pop_transform();
|
|
scene.pop_opacity();
|
|
|
|
hui::ui::CardLook look;
|
|
look.art_aspect = 1.6f;
|
|
look.title_size = 40.0f;
|
|
look.subtitle_size = 22.0f;
|
|
look.text_gap = 18.0f;
|
|
look.glow = true;
|
|
const char *titles[cardCount] = { "Samples", "Designs", "Themes" };
|
|
const char *subtitles[cardCount] = { "Vulkan techniques, from glTF to ray queries",
|
|
"Twenty-one complete interface designs", "The title's look, from thirty" };
|
|
const Color accents[cardCount] = { Color::rgb(0xff8a50), Color::rgb(0xffd166), theme.accent };
|
|
const float cardWidth = 500.0f;
|
|
const float gap = 50.0f;
|
|
const float cardHeight = hui::ui::card_height(look, cardWidth);
|
|
for (int i = 0; i < cardCount; i++) {
|
|
hui::ui::CardItem item;
|
|
item.title = titles[i];
|
|
item.subtitle = subtitles[i];
|
|
item.texture = art[i];
|
|
item.uv = hui::gfx::kCanvasUv;
|
|
item.image_aspect = 1.6f;
|
|
item.accent = accents[i];
|
|
hui::ui::CardState state;
|
|
state.focus = shown[i].value;
|
|
const Rect card{ 160.0f + (cardWidth + gap) * (float)i, 400.0f, cardWidth, cardHeight };
|
|
const float rise = arrive(0.25f + 0.12f * (float)i, 0.55f);
|
|
scene.push_opacity(rise);
|
|
scene.push_transform(1.0f, 0.0f, 0.0f, 0.0f, (1.0f - rise) * 120.0f);
|
|
hui::ui::draw_card(canvas, style, look, card, item, state);
|
|
scene.pop_transform();
|
|
scene.pop_opacity();
|
|
}
|
|
|
|
const float foot = arrive(0.75f, 0.4f);
|
|
scene.push_opacity(foot);
|
|
painter.button({ 860.0f, 940.0f, 200.0f, 60.0f }, "Quit", hui::ui::ButtonKind::secondary, { shown[quitItem].value, 0.0f, false });
|
|
ps5ui::draw_hints(scene, kit.fonts, theme,
|
|
{ { hui::ui::Button::dpad, "Move" }, { hui::ui::Button::cross, "Choose" }, { hui::ui::Button::circle, "Back here, from each" },
|
|
{ hui::ui::Button::options, "Hold: back here, always" } }, 960.0f, 1040.0f, 0);
|
|
scene.pop_opacity();
|
|
|
|
hui::gfx::BackdropSpec backdrop = theme.backdrop;
|
|
backdrop.time = clock;
|
|
kit.renderer.begin();
|
|
kit.renderer.backdrop(backdrop);
|
|
kit.renderer.glass();
|
|
kit.renderer.draw(scene);
|
|
}
|
|
|
|
void render() override
|
|
{
|
|
if (!prepared) {
|
|
return;
|
|
}
|
|
prepareFrame();
|
|
update(std::min(frameTimer, 0.05f));
|
|
compose();
|
|
buildKitCommandBuffer();
|
|
submitFrame();
|
|
if (chosen != -2) {
|
|
quit = true;
|
|
}
|
|
}
|
|
|
|
~StartScreen() override
|
|
{
|
|
if (device) {
|
|
vkDeviceWaitIdle(device);
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
int ps5_run_start(int selected, uint32_t frameBudget, const std::string &screenshotPath)
|
|
{
|
|
StartScreen *start = new StartScreen(selected);
|
|
start->benchmark.active = frameBudget > 0;
|
|
start->ps5.frameBudget = frameBudget;
|
|
start->ps5.screenshotPath = screenshotPath;
|
|
int chosen = -1;
|
|
try {
|
|
start->initVulkan();
|
|
start->prepare();
|
|
start->renderLoop();
|
|
chosen = frameBudget ? -1 : start->chosen;
|
|
} catch (const std::exception &e) {
|
|
say("start screen: %s", e.what());
|
|
}
|
|
if (start->vulkanDevice) {
|
|
vkDeviceWaitIdle(start->vulkanDevice->logicalDevice);
|
|
}
|
|
delete start;
|
|
return chosen;
|
|
}
|