shadPS4/src/imgui/big_picture/big_picture.cpp
2026-06-01 20:44:38 -05:00

395 lines
14 KiB
C++

// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <stb_image.h>
#include "big_picture.h"
#include "common/logging/log.h"
#include "core/devtools/layer.h"
#include "core/emulator_settings.h"
#include "core/file_format/psf.h"
#include "emulator.h"
#include "imgui/imgui_std.h"
#include "imgui/renderer/font_stack.h"
#include "imgui_impl_sdl3_big_picture.h"
#include "imgui_impl_sdlrenderer3.h"
#include "settings_dialog_imgui.h"
namespace BigPictureMode {
constexpr float gameImageSize = 200.f;
bool done = false;
bool showSettings = false;
std::filesystem::path runEbootPath = "";
std::vector<IconInfo> gameIcons = {};
float uiScale = 1.0f;
SDL_Renderer* renderer;
namespace {
std::filesystem::path UpdateChecker(const std::string sceItem, std::filesystem::path game_folder) {
std::filesystem::path outputPath;
auto update_folder = game_folder;
update_folder += "-UPDATE";
auto patch_folder = game_folder;
patch_folder += "-patch";
if (std::filesystem::exists(update_folder / "sce_sys" / sceItem)) {
outputPath = update_folder / "sce_sys" / sceItem;
} else if (std::filesystem::exists(patch_folder / "sce_sys" / sceItem)) {
outputPath = patch_folder / "sce_sys" / sceItem;
} else {
outputPath = game_folder / "sce_sys" / sceItem;
}
return outputPath;
}
void SetGameIcons(std::vector<IconInfo>& gameIcons) {
ImGuiStyle& style = ImGui::GetStyle();
const float maxAvailableWidth = ImGui::GetContentRegionAvail().x;
const float itemSpacing = style.ItemSpacing.x; // already scaled
const float padding = 10.0f * uiScale;
float rowContentWidth = gameImageSize * uiScale + itemSpacing;
for (int i = 0; i < gameIcons.size(); i++) {
ImGui::BeginGroup();
std::string ButtonName = "Button" + std::to_string(i);
const char* ButtonNameChar = ButtonName.c_str();
bool buttonFocused = (ImGui::GetID(ButtonNameChar) == ImGui::GetFocusID());
if (buttonFocused) {
ImGui::PushStyleColor(ImGuiCol_Button,
ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]);
}
ImTextureID id = gameIcons[i].textureId;
if (id != nullptr) {
if (ImGui::ImageButton(ButtonNameChar, id,
ImVec2(gameImageSize * uiScale, gameImageSize * uiScale))) {
done = true;
runEbootPath = gameIcons[i].ebootPath;
}
}
if (buttonFocused) {
ImGui::PopStyleColor();
}
// Scroll to item only when newly-focused
if (ImGui::IsItemFocused() && !gameIcons[i].focusState) {
ImGui::SetScrollHereY(0.5f);
}
if (ImGui::IsWindowFocused()) {
gameIcons[i].focusState = ImGui::IsItemFocused();
}
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + gameImageSize * uiScale);
ImGui::TextWrapped("%s", gameIcons[i].title.c_str());
ImGui::PopTextWrapPos();
ImGui::EndGroup();
// Use same line if content fits horizontally, move to next line if not
rowContentWidth += (gameImageSize * uiScale + itemSpacing * 2 + padding);
if (rowContentWidth < maxAvailableWidth) {
ImGui::SameLine(0.0f, padding);
} else {
ImGui::Dummy(ImVec2(0.0f, padding));
rowContentWidth = gameImageSize * uiScale + itemSpacing;
}
}
}
} // namespace
SDL_Texture* LoadSdlTextureData(std::vector<u8> data) {
int image_width = 0;
int image_height = 0;
int channels = 4;
unsigned char* image_data = stbi_load_from_memory(
(const unsigned char*)data.data(), (int)data.size(), &image_width, &image_height, NULL, 4);
if (image_data == nullptr) {
LOG_ERROR(ImGui, "Failed to load image: {}", stbi_failure_reason());
}
SDL_Surface* surface = SDL_CreateSurfaceFrom(image_width, image_height, SDL_PIXELFORMAT_RGBA32,
(void*)image_data, channels * image_width);
if (surface == nullptr) {
LOG_ERROR(ImGui, "Unable to create SDL surface: {}", SDL_GetError());
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
if (texture == nullptr) {
LOG_ERROR(ImGui, "Unable to create SDL texture: {}", SDL_GetError());
}
SDL_DestroySurface(surface);
stbi_image_free(image_data);
return texture;
}
SDL_Texture* LoadSdlTextureDataFromFile(std::filesystem::path filePath) {
std::ifstream file(filePath, std::ios::binary);
std::vector<u8> data =
std::vector<u8>(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
return LoadSdlTextureData(data);
}
void GetGameIconInfo(std::vector<IconInfo>& icons) {
icons.clear();
for (const auto& installLoc : EmulatorSettings.GetAllGameInstallDirs()) {
if (installLoc.enabled && std::filesystem::exists(installLoc.path)) {
for (const auto& entry : std::filesystem::directory_iterator(installLoc.path)) {
if (entry.path().filename().string().ends_with("-UPDATE") ||
entry.path().filename().string().ends_with("-patch") || !entry.is_directory()) {
continue;
}
IconInfo icon;
PSF psf;
const std::string sfoFileName = "param.sfo";
std::filesystem::path sfoPath = UpdateChecker(sfoFileName, entry.path());
if (std::filesystem::exists(sfoPath) && psf.Open(sfoPath)) {
if (const auto title = psf.GetString("TITLE"); title.has_value()) {
icon.title = *title;
}
if (const auto title_id = psf.GetString("TITLE_ID"); title_id.has_value()) {
icon.serial = *title_id;
}
} else {
continue;
}
const std::string iconFileName = "icon0.png";
std::filesystem::path iconPath = UpdateChecker(iconFileName, entry.path());
SDL_Texture* texture = LoadSdlTextureDataFromFile(iconPath);
icon.textureId = ImTextureID(texture);
icon.ebootPath = entry.path() / "eboot.bin";
icon.focusState = false;
icons.push_back(icon);
}
}
}
std::sort(icons.begin(), icons.end(), [](const IconInfo& a, const IconInfo& b) {
return a.title < b.title; // Alphabetical order
});
}
void Launch(char* executableName) {
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "BigPictureMode Launch: SDL_INIT_VIDEO Error: " << SDL_GetError() << std::endl;
SDL_Quit();
return;
}
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
std::cout << "BigPictureMode Launch: SDL_INIT_GAMEPAD Error: " << SDL_GetError()
<< std::endl;
}
SDL_Window* window =
SDL_CreateWindow("shadPS4 Big Picture Mode", 1280, 720, SDL_WINDOW_FULLSCREEN);
renderer = SDL_CreateRenderer(window, nullptr);
if (window == nullptr) {
std::cout << "BigPictureMode Launch: SDL Window Creation Error: " << SDL_GetError()
<< std::endl;
SDL_DestroyRenderer(renderer);
SDL_Quit();
return;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigNavCursorVisibleAlways = true;
ImFontConfig cfgBase;
cfgBase.OversampleH = 2;
cfgBase.OversampleV = 1;
io.FontDefault = ImGui::FontStack::AddPrimaryUiFont(
io.Fonts, 64.0f, EmulatorSettings.GetConsoleLanguage(), cfgBase, true);
io.FontGlobalScale = 0.5f;
io.Fonts->Build();
ImGui_ImplSDL3_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer3_Init(renderer);
GetGameIconInfo(gameIcons);
uiScale = static_cast<float>(EmulatorSettings.GetBigPictureScale() / 1000.f);
ImGuiEmuSettings::SettingsWindow settingsWindow(false);
while (!done) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL3_ProcessEvent(&event);
if (event.type == SDL_EVENT_QUIT) {
done = true;
}
}
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.06f, 1.00f)); // black
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.40f, 0.70f, 1.00f)); // blue
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
ImVec4(0.25f, 0.50f, 0.85f, 1.00f)); // lighter blue
ImGui::PushStyleColor(ImGuiCol_SliderGrabActive,
ImVec4(0.26f, 0.59f, 0.98f, 0.80f)); // another light blue
ImGui::PushStyleColor(ImGuiCol_SliderGrab,
ImVec4(0.26f, 0.59f, 0.98f, 0.80f)); // another light blue
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f * uiScale);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f * uiScale, 10.0f * uiScale));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f * uiScale, 10.0f * uiScale));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.5f * uiScale);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f * uiScale, 20.0f * uiScale));
ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, 20.0f * uiScale);
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGuiWindowFlags window_flags =
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse;
ImGui::Begin("Game Window", &done, window_flags);
ImGui::DrawPrettyBackground();
ImGui::SetWindowFontScale(uiScale);
ImGuiWindowFlags child_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NavFlattened;
if (ImGui::IsWindowAppearing()) {
ImGui::SetNextWindowFocus();
}
ImGui::BeginChild("ContentRegion", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), true,
child_flags);
Overlay::TextCentered("Select Game");
ImGui::Dummy(ImVec2(0.0f, 10.f * uiScale));
if (ImGui::IsWindowAppearing()) {
ImGui::SetKeyboardFocusHere();
}
SetGameIcons(gameIcons);
ImGui::EndChild();
ImGui::Separator();
ImGui::SetNextItemWidth(300.0f * uiScale);
static float sliderScale = 1.0f;
if (ImGui::IsWindowAppearing()) {
sliderScale = uiScale;
}
ImGui::SliderFloat("UI Scale", &sliderScale, 0.25f, 3.0f);
// Only update when user is not interacting with slider
if (ImGui::IsItemDeactivatedAfterEdit()) {
uiScale = sliderScale;
}
ImGui::SameLine();
// Align buttons right
float buttonsWidth = ImGui::CalcTextSize("Settings").x + ImGui::CalcTextSize("Exit").x +
ImGui::GetStyle().FramePadding.x * 4.0f +
ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - buttonsWidth);
if (ImGui::Button("Settings")) {
showSettings = true;
}
ImGui::SameLine();
if (ImGui::Button("Exit")) {
ImGui::OpenPopup("Confirm Exit");
}
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Confirm Exit", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("This will exit shadPS4!\nAre you sure?");
ImGui::Separator();
if (ImGui::Button("OK", ImVec2(120 * uiScale, 0))) {
ImGui::CloseCurrentPopup();
done = true;
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120 * uiScale, 0))) {
ImGui::CloseCurrentPopup();
}
if (ImGui::IsWindowAppearing()) {
ImGui::SetItemDefaultFocus();
}
ImGui::EndPopup();
}
if (showSettings) {
EmulatorSettings.SetBigPictureScale(static_cast<int>(uiScale * 1000));
EmulatorSettings.Save();
settingsWindow.DrawSettings(&showSettings);
// update when settings dialog closed
if (!showSettings) {
uiScale = static_cast<float>(EmulatorSettings.GetBigPictureScale() / 1000.f);
sliderScale = uiScale;
GetGameIconInfo(gameIcons);
}
}
ImGui::PopStyleVar(8);
ImGui::PopStyleColor(5);
ImGui::End();
ImGui::Render();
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), renderer);
SDL_RenderPresent(renderer);
}
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
SDL_DestroyWindow(window);
SDL_DestroyRenderer(renderer);
SDL_Quit();
EmulatorSettings.SetBigPictureScale(static_cast<int>(uiScale * 1000));
EmulatorSettings.Save();
if (runEbootPath != "") {
auto* emulator = Common::Singleton<Core::Emulator>::Instance();
emulator->executableName = executableName;
emulator->Run(runEbootPath);
}
}
} // namespace BigPictureMode