Big Picture Mode (#4250)

* imguitest

* button tests

* fix gamepad nav

* placeholder hardcoded eboot path

* set focus correctly, move to own files

* get installed game information

* dynamically adjust rows

* use slider for ui scale

* launch big picture with CLI arg

* Use emulator settings for UI scale and window size

* center scrolling on focused item

* fix item focus on scrolling when navigating with keyboard or pad

* minor fixups and comments

* fix performance degradation

* adjust fonts to show TM symbol and higher overscale

* reuse and clang

* add exists check before iterator

* flatten navigation (gamepad navigation crosses child window container)

* cleanup and comments

* simplify update checker a bit

---------

Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
This commit is contained in:
rainmakerv2 2026-04-13 06:40:11 +08:00 committed by GitHub
parent 1c8ace6619
commit cead66d3c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 2094 additions and 14 deletions

3
.gitmodules vendored
View File

@ -129,6 +129,9 @@
[submodule "externals/openal-soft"]
path = externals/openal-soft
url = https://github.com/shadexternals/openal-soft.git
[submodule "externals/sdl3_image"]
path = externals/sdl3_image
url = https://github.com/libsdl-org/SDL_image
[submodule "externals/libusb"]
path = externals/libusb
url = https://github.com/shadexternals/libusb.git

View File

@ -234,6 +234,7 @@ find_package(PNG 1.6 MODULE)
find_package(OpenAL CONFIG)
find_package(RenderDoc 1.6.0 MODULE)
find_package(SDL3_mixer 2.8.1 CONFIG)
find_package(SDL3_image CONFIG)
if (SDL3_mixer_FOUND)
find_package(SDL3 3.1.2 CONFIG)
endif()
@ -1099,10 +1100,16 @@ set(IMGUI src/imgui/imgui_config.h
src/imgui/renderer/imgui_core.h
src/imgui/renderer/imgui_impl_sdl3.cpp
src/imgui/renderer/imgui_impl_sdl3.h
src/imgui/renderer/imgui_impl_sdl3_bpm.cpp
src/imgui/renderer/imgui_impl_sdl3_bpm.h
src/imgui/renderer/imgui_impl_sdlrenderer3.cpp
src/imgui/renderer/imgui_impl_sdlrenderer3.h
src/imgui/renderer/imgui_impl_vulkan.cpp
src/imgui/renderer/imgui_impl_vulkan.h
src/imgui/renderer/texture_manager.cpp
src/imgui/renderer/texture_manager.h
src/imgui/big_picture.cpp
src/imgui/big_picture.h
)
set(INPUT src/input/controller.cpp
@ -1140,7 +1147,7 @@ add_executable(shadps4
create_target_directory_groups(shadps4)
target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer pugixml::pugixml)
target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib)
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")

View File

@ -63,6 +63,29 @@ if (NOT TARGET SDL3::SDL3)
add_subdirectory(sdl3)
endif()
# SDL3_image
if (NOT TARGET SDL3_image::SDL3_image)
set(SDLIMAGE_VENDORED OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_ANI OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_AVIF OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_BMP OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_GIF OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_JPG OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_JXL OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_LBM OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_PCX OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_PNM OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_QOI OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_SVG OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_TGA OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_TIF OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_WEBP OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_XCF OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_XPM OFF CACHE BOOL "" FORCE)
set(SDLIMAGE_XV OFF CACHE BOOL "" FORCE)
add_subdirectory(sdl3_image)
endif()
# SDL3_mixer
if (NOT TARGET SDL3_mixer::SDL3_mixer)
set(SDLMIXER_FLAC OFF)

1
externals/sdl3_image vendored Submodule

@ -0,0 +1 @@
Subproject commit 1aedddcbd205c4e1ea0f99fdb2c785acc8e2489b

View File

@ -436,7 +436,7 @@ void L::Draw() {
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) {
SetWindowFontScale(1.5f);
TextCentered("Are you sure you want to quit?");
Overlay::TextCentered("Are you sure you want to quit?");
NewLine();
Text("Press Escape or Circle/B button to cancel");
Text("Press Enter or Cross/A button to quit");
@ -481,7 +481,9 @@ void L::Draw() {
PopID();
}
void L::TextCentered(const std::string& text) {
namespace Overlay {
void TextCentered(const std::string& text) {
float window_width = GetWindowSize().x;
float text_width = CalcTextSize(text.c_str()).x;
float text_indentation = (window_width - text_width) * 0.5f;
@ -490,8 +492,6 @@ void L::TextCentered(const std::string& text) {
Text("%s", text.c_str());
}
namespace Overlay {
void ToggleSimpleFps() {
show_simple_fps = !show_simple_fps;
visibility_toggled = true;

View File

@ -21,8 +21,6 @@ private:
static void DrawMenuBar();
static void DrawAdvanced();
static void DrawSimple();
static void TextCentered(const std::string& text);
};
} // namespace Core::Devtools
@ -34,4 +32,6 @@ void SetSimpleFps(bool enabled);
void ToggleQuitWindow();
void ShowVolume();
void TextCentered(const std::string& text);
} // namespace Overlay

View File

@ -187,6 +187,7 @@ struct GeneralSettings {
Setting<bool> discord_rpc_enabled{false};
Setting<bool> show_fps_counter{false};
Setting<int> console_language{1};
Setting<int> big_picture_scale{1000};
// return a vector of override descriptors (runtime, but tiny)
std::vector<OverrideItem> GetOverrideableFields() const {
@ -218,7 +219,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_
trophy_notification_duration, log_filter, log_type, show_splash,
identical_log_grouped, trophy_notification_side,
connected_to_network, discord_rpc_enabled, show_fps_counter,
console_language)
console_language, big_picture_scale)
// -------------------------------
// Debug settings
@ -557,6 +558,7 @@ public:
SETTING_FORWARD_BOOL(m_general, DiscordRPCEnabled, discord_rpc_enabled)
SETTING_FORWARD_BOOL(m_general, ShowFpsCounter, show_fps_counter)
SETTING_FORWARD(m_general, ConsoleLanguage, console_language)
SETTING_FORWARD(m_general, BigPictureScale, big_picture_scale)
// Audio settings
SETTING_FORWARD(m_audio, AudioBackend, audio_backend)

339
src/imgui/big_picture.cpp Normal file
View File

@ -0,0 +1,339 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <SDL3/SDL.h>
#include <imgui.h>
#include "big_picture.h"
#include "common/logging/log.h"
#include "core/devtools/layer.h"
#include "core/file_format/psf.h"
#include "emulator.h"
#include "imgui/renderer/imgui_impl_sdl3_bpm.h"
#include "imgui/renderer/imgui_impl_sdlrenderer3.h"
#include "imgui_fonts/notosansjp_regular.ttf.g.cpp"
#include "imgui_fonts/proggyvector_regular.ttf.g.cpp"
namespace BigPictureMode {
const float gameImageSize = 200.f;
static bool done = false;
static bool runGame = false;
static std::filesystem::path runEbootPath = "";
static std::vector<Game> gameVec = {};
static std::vector<bool> focusState = {};
static float uiScale = 1.0f;
static int scaleSelected = 1;
static SDL_Window* window = nullptr;
static SDL_Renderer* renderer = nullptr;
void Launch() {
if (!SDL_Init(SDL_INIT_VIDEO)) {
LOG_ERROR(ImGui, "SDL_INIT_VIDEO Error: {}", SDL_GetError());
SDL_Quit();
return;
}
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
LOG_ERROR(ImGui, "SDL_INIT_GAMEPAD Error: {}", SDL_GetError());
SDL_Quit();
return;
}
window = SDL_CreateWindow("shadPS4 Big Picture Mode", EmulatorSettings.GetWindowWidth(),
EmulatorSettings.GetWindowHeight(), SDL_WINDOW_RESIZABLE);
renderer = SDL_CreateRenderer(window, nullptr);
if (EmulatorSettings.IsFullScreen()) {
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
}
// Check if window creation failed
if (window == nullptr) {
LOG_ERROR(ImGui, "SDL Window Creation Error: {}", SDL_GetError());
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 config;
config.OversampleH = 3;
config.OversampleV = 3;
config.MergeMode = true;
ImFontConfig config2;
config.OversampleH = 3;
config.OversampleV = 3;
// tm symbol
static const ImWchar icon_ranges[] = {0x2122, 0x2122, 0x3000, 0x30FF, 0};
ImFontGlyphRangesBuilder rb{};
rb.AddRanges(io.Fonts->GetGlyphRangesDefault());
rb.AddRanges(io.Fonts->GetGlyphRangesGreek());
rb.AddRanges(io.Fonts->GetGlyphRangesKorean());
rb.AddRanges(io.Fonts->GetGlyphRangesJapanese());
rb.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
ImVector<ImWchar> ranges{};
rb.BuildRanges(&ranges);
ImFont* myFont = io.Fonts->AddFontFromMemoryCompressedTTF(
imgui_font_notosansjp_regular_compressed_data,
imgui_font_notosansjp_regular_compressed_size, 32.0f, &config2, icon_ranges);
io.Fonts->AddFontFromMemoryCompressedTTF(imgui_font_notosansjp_regular_compressed_data,
imgui_font_notosansjp_regular_compressed_size, 32.0f,
&config, ranges.Data);
io.Fonts->AddFontFromMemoryCompressedTTF(imgui_font_proggyvector_regular_compressed_data,
imgui_font_proggyvector_regular_compressed_size, 32.0f,
&config, io.Fonts->GetGlyphRangesDefault());
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 1.00f); // black
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.40f, 0.70f, 1.00f); // blue
colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.50f, 0.85f, 1.00f); // lighter blue
style.WindowRounding = 0.0f;
style.FrameRounding = 5.0f;
style.ItemSpacing = ImVec2(10.0f * uiScale, 10.0f * uiScale);
style.FramePadding = ImVec2(10.0f * uiScale, 10.0f * uiScale);
style.WindowBorderSize = 0.0f;
style.WindowPadding = ImVec2(20.0f * uiScale, 20.0f * uiScale);
ImGui_ImplSDL3_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer3_Init(renderer);
GetGameInfo();
uiScale = static_cast<float>(EmulatorSettings.GetBigPictureScale() / 1000.f);
float tempScale = uiScale;
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();
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration;
ImGui::PushFont(myFont);
ImGui::Begin("Game Window", &done, window_flags);
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();
ImGui::EndChild();
ImGui::Separator();
ImGui::SetNextItemWidth(300.0f * uiScale);
if (ImGui::SliderFloat("UI Scale", &tempScale, 0.25f, 3.0f)) {
// Dynamically changes UI scale
}
// Only update when user is not interacting with slider
if (ImGui::IsItemDeactivatedAfterEdit()) {
uiScale = tempScale;
tempScale = uiScale;
}
ImGui::SameLine();
// Align buttons right
float buttonsWidth =
ImGui::CalcTextSize("Settings (Under Construction)").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 (Under Construction)")) {
// Todo
}
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();
}
ImGui::PopFont();
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 (runGame) {
auto* emulator = Common::Singleton<Core::Emulator>::Instance();
emulator->Run(runEbootPath);
}
}
void SetGameIcons() {
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;
// Use same line if content fits horizontally, move to next line if not
for (int i = 0; i < gameVec.size(); i++) {
ImGui::BeginGroup();
std::string ButtonName = "Button" + std::to_string(i);
const char* ButtonNameChar = ButtonName.c_str();
if (ImGui::ImageButton(ButtonNameChar, (ImTextureID)gameVec[i].iconTexture,
ImVec2(gameImageSize * uiScale, gameImageSize * uiScale))) {
runGame = true;
done = true;
runEbootPath = gameVec[i].ebootPath;
}
// Scroll to item only when newly-focused
if (ImGui::IsItemFocused() && !focusState[i]) {
ImGui::SetScrollHereY(0.5f);
}
focusState[i] = ImGui::IsItemFocused();
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + gameImageSize * uiScale);
ImGui::TextWrapped("%s", gameVec[i].title.c_str());
ImGui::PopTextWrapPos();
ImGui::EndGroup();
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;
}
}
}
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 GetGameInfo() {
gameVec.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;
}
Game game;
game.ebootPath = entry.path() / "eboot.bin";
const std::string iconFileName = "icon0.png";
std::filesystem::path iconPath = UpdateChecker(iconFileName, entry.path());
game.iconTexture = IMG_LoadTexture(renderer, iconPath.string().c_str());
PSF psf;
const std::string sfoFileName = "param.sfo";
std::filesystem::path sfoPath = UpdateChecker(sfoFileName, entry.path());
if (psf.Open(sfoPath)) {
if (const auto title = psf.GetString("TITLE"); title.has_value()) {
game.title = *title;
}
} else {
continue;
}
gameVec.push_back(game);
focusState.push_back(false);
}
}
}
}
} // namespace BigPictureMode

22
src/imgui/big_picture.h Normal file
View File

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <SDL3_image/SDL_image.h>
namespace BigPictureMode {
struct Game {
SDL_Texture* iconTexture;
std::filesystem::path ebootPath;
std::string title;
};
void Launch();
void SetGameIcons();
void GetGameInfo();
std::filesystem::path UpdateChecker(const std::string sceItem, std::filesystem::path game_folder);
} // namespace BigPictureMode

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on imgui_impl_sdl3.h from Dear ImGui repository
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
#ifndef IMGUI_DISABLE
struct SDL_Window;
struct SDL_Renderer;
struct SDL_Gamepad;
typedef union SDL_Event SDL_Event;
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForOpenGL(SDL_Window* window, void* sdl_gl_context);
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForVulkan(SDL_Window* window);
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForD3D(SDL_Window* window);
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForMetal(SDL_Window* window);
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForSDLRenderer(SDL_Window* window, SDL_Renderer* renderer);
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForSDLGPU(SDL_Window* window);
IMGUI_IMPL_API bool ImGui_ImplSDL3_InitForOther(SDL_Window* window);
IMGUI_IMPL_API void ImGui_ImplSDL3_Shutdown();
IMGUI_IMPL_API void ImGui_ImplSDL3_NewFrame();
IMGUI_IMPL_API bool ImGui_ImplSDL3_ProcessEvent(const SDL_Event* event);
// Gamepad selection automatically starts in AutoFirst mode, picking first available SDL_Gamepad.You
// may override this. When using manual mode, caller is responsible for opening/closing gamepad.
enum ImGui_ImplSDL3_GamepadMode {
ImGui_ImplSDL3_GamepadMode_AutoFirst,
ImGui_ImplSDL3_GamepadMode_AutoAll,
ImGui_ImplSDL3_GamepadMode_Manual
};
IMGUI_IMPL_API void ImGui_ImplSDL3_SetGamepadMode(ImGui_ImplSDL3_GamepadMode mode,
SDL_Gamepad** manual_gamepads_array = nullptr,
int manual_gamepads_count = -1);
#endif // #ifndef IMGUI_DISABLE

View File

@ -0,0 +1,289 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on imgui_impl_sdlrenderer3.cpp from Dear ImGui repository
#include "imgui.h"
#ifndef IMGUI_DISABLE
#include <stdint.h> // intptr_t
#include "imgui_impl_sdlrenderer3.h"
// Clang warnings with -Weverything
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored \
"-Wsign-conversion" // warning: implicit conversion changes signedness
#endif
// SDL
#include <SDL3/SDL.h>
#if !SDL_VERSION_ATLEAST(3, 0, 0)
#error This backend requires SDL 3.0.0+
#endif
// SDL_Renderer data
struct ImGui_ImplSDLRenderer3_Data {
SDL_Renderer* Renderer; // Main viewport's renderer
SDL_Texture* FontTexture;
ImVector<SDL_FColor> ColorBuffer;
ImGui_ImplSDLRenderer3_Data() {
memset((void*)this, 0, sizeof(*this));
}
};
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui
// contexts It is STRONGLY preferred that you use docking branch with multi-viewports (==single Dear
// ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
static ImGui_ImplSDLRenderer3_Data* ImGui_ImplSDLRenderer3_GetBackendData() {
return ImGui::GetCurrentContext()
? (ImGui_ImplSDLRenderer3_Data*)ImGui::GetIO().BackendRendererUserData
: nullptr;
}
// Functions
bool ImGui_ImplSDLRenderer3_Init(SDL_Renderer* renderer) {
ImGuiIO& io = ImGui::GetIO();
IMGUI_CHECKVERSION();
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
IM_ASSERT(renderer != nullptr && "SDL_Renderer not initialized!");
// Setup backend capabilities flags
ImGui_ImplSDLRenderer3_Data* bd = IM_NEW(ImGui_ImplSDLRenderer3_Data)();
io.BackendRendererUserData = (void*)bd;
io.BackendRendererName = "imgui_impl_sdlrenderer3";
io.BackendFlags |=
ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset
// field, allowing for large meshes.
bd->Renderer = renderer;
return true;
}
void ImGui_ImplSDLRenderer3_Shutdown() {
ImGui_ImplSDLRenderer3_Data* bd = ImGui_ImplSDLRenderer3_GetBackendData();
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplSDLRenderer3_DestroyDeviceObjects();
io.BackendRendererName = nullptr;
io.BackendRendererUserData = nullptr;
io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset;
IM_DELETE(bd);
}
static void ImGui_ImplSDLRenderer3_SetupRenderState(SDL_Renderer* renderer) {
// Clear out any viewports and cliprect set by the user
// FIXME: Technically speaking there are lots of other things we could backup/setup/restore
// during our render process.
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderClipRect(renderer, nullptr);
}
void ImGui_ImplSDLRenderer3_NewFrame() {
ImGui_ImplSDLRenderer3_Data* bd = ImGui_ImplSDLRenderer3_GetBackendData();
IM_ASSERT(bd != nullptr &&
"Context or backend not initialized! Did you call ImGui_ImplSDLRenderer3_Init()?");
if (!bd->FontTexture)
ImGui_ImplSDLRenderer3_CreateDeviceObjects();
}
// https://github.com/libsdl-org/SDL/issues/9009
static int SDL_RenderGeometryRaw8BitColor(SDL_Renderer* renderer, ImVector<SDL_FColor>& colors_out,
SDL_Texture* texture, const float* xy, int xy_stride,
const SDL_Color* color, int color_stride, const float* uv,
int uv_stride, int num_vertices, const void* indices,
int num_indices, int size_indices) {
const Uint8* color2 = (const Uint8*)color;
colors_out.resize(num_vertices);
SDL_FColor* color3 = colors_out.Data;
for (int i = 0; i < num_vertices; i++) {
color3[i].r = color->r / 255.0f;
color3[i].g = color->g / 255.0f;
color3[i].b = color->b / 255.0f;
color3[i].a = color->a / 255.0f;
color2 += color_stride;
color = (const SDL_Color*)color2;
}
return SDL_RenderGeometryRaw(renderer, texture, xy, xy_stride, color3, sizeof(*color3), uv,
uv_stride, num_vertices, indices, num_indices, size_indices);
}
void ImGui_ImplSDLRenderer3_RenderDrawData(ImDrawData* draw_data, SDL_Renderer* renderer) {
ImGui_ImplSDLRenderer3_Data* bd = ImGui_ImplSDLRenderer3_GetBackendData();
// If there's a scale factor set by the user, use that instead
// If the user has specified a scale factor to SDL_Renderer already via SDL_RenderSetScale(),
// SDL will scale whatever we pass to SDL_RenderGeometryRaw() by that scale factor. In that case
// we don't want to be also scaling it ourselves here.
float rsx = 1.0f;
float rsy = 1.0f;
SDL_GetRenderScale(renderer, &rsx, &rsy);
ImVec2 render_scale;
render_scale.x = (rsx == 1.0f) ? draw_data->FramebufferScale.x : 1.0f;
render_scale.y = (rsy == 1.0f) ? draw_data->FramebufferScale.y : 1.0f;
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates !=
// framebuffer coordinates)
int fb_width = (int)(draw_data->DisplaySize.x * render_scale.x);
int fb_height = (int)(draw_data->DisplaySize.y * render_scale.y);
if (fb_width == 0 || fb_height == 0)
return;
// Backup SDL_Renderer state that will be modified to restore it afterwards
struct BackupSDLRendererState {
SDL_Rect Viewport;
bool ViewportEnabled;
bool ClipEnabled;
SDL_Rect ClipRect;
};
BackupSDLRendererState old = {};
old.ViewportEnabled = SDL_RenderViewportSet(renderer);
old.ClipEnabled = SDL_RenderClipEnabled(renderer);
SDL_GetRenderViewport(renderer, &old.Viewport);
SDL_GetRenderClipRect(renderer, &old.ClipRect);
// Setup desired state
ImGui_ImplSDLRenderer3_SetupRenderState(renderer);
// Setup render state structure (for callbacks and custom texture bindings)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplSDLRenderer3_RenderState render_state;
render_state.Renderer = renderer;
platform_io.Renderer_RenderState = &render_state;
// Will project scissor/clipping rectangles into framebuffer space
ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
ImVec2 clip_scale = render_scale;
// Render command lists
for (int n = 0; n < draw_data->CmdListsCount; n++) {
const ImDrawList* draw_list = draw_data->CmdLists[n];
const ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data;
const ImDrawIdx* idx_buffer = draw_list->IdxBuffer.Data;
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) {
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
if (pcmd->UserCallback) {
// User callback, registered via ImDrawList::AddCallback()
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to
// request the renderer to reset render state.)
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
ImGui_ImplSDLRenderer3_SetupRenderState(renderer);
else
pcmd->UserCallback(draw_list, pcmd);
} else {
// Project scissor/clipping rectangles into framebuffer space
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x,
(pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x,
(pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
if (clip_min.x < 0.0f) {
clip_min.x = 0.0f;
}
if (clip_min.y < 0.0f) {
clip_min.y = 0.0f;
}
if (clip_max.x > (float)fb_width) {
clip_max.x = (float)fb_width;
}
if (clip_max.y > (float)fb_height) {
clip_max.y = (float)fb_height;
}
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
continue;
SDL_Rect r = {(int)(clip_min.x), (int)(clip_min.y), (int)(clip_max.x - clip_min.x),
(int)(clip_max.y - clip_min.y)};
SDL_SetRenderClipRect(renderer, &r);
const float* xy =
(const float*)(const void*)((const char*)(vtx_buffer + pcmd->VtxOffset) +
offsetof(ImDrawVert, pos));
const float* uv =
(const float*)(const void*)((const char*)(vtx_buffer + pcmd->VtxOffset) +
offsetof(ImDrawVert, uv));
const SDL_Color* color =
(const SDL_Color*)(const void*)((const char*)(vtx_buffer + pcmd->VtxOffset) +
offsetof(ImDrawVert, col)); // SDL 2.0.19+
// Bind texture, Draw
SDL_Texture* tex = (SDL_Texture*)pcmd->GetTexID();
SDL_RenderGeometryRaw8BitColor(
renderer, bd->ColorBuffer, tex, xy, (int)sizeof(ImDrawVert), color,
(int)sizeof(ImDrawVert), uv, (int)sizeof(ImDrawVert),
draw_list->VtxBuffer.Size - pcmd->VtxOffset, idx_buffer + pcmd->IdxOffset,
pcmd->ElemCount, sizeof(ImDrawIdx));
}
}
}
platform_io.Renderer_RenderState = nullptr;
// Restore modified SDL_Renderer state
SDL_SetRenderViewport(renderer, old.ViewportEnabled ? &old.Viewport : nullptr);
SDL_SetRenderClipRect(renderer, old.ClipEnabled ? &old.ClipRect : nullptr);
}
// Called by Init/NewFrame/Shutdown
bool ImGui_ImplSDLRenderer3_CreateFontsTexture() {
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplSDLRenderer3_Data* bd = ImGui_ImplSDLRenderer3_GetBackendData();
// Build texture atlas
unsigned char* pixels;
int width, height;
io.Fonts->GetTexDataAsRGBA32(
&pixels, &width,
&height); // Load as RGBA 32-bit (75% of the memory is wasted, but default font is so small)
// because it is more likely to be compatible with user's existing shaders. If
// your ImTextureId represent a higher-level concept than just a GL texture id,
// consider calling GetTexDataAsAlpha8() instead to save on GPU memory.
// Upload texture to graphics system
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |=
// ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow
// point/nearest sampling)
bd->FontTexture = SDL_CreateTexture(bd->Renderer, SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STATIC, width, height);
if (bd->FontTexture == nullptr) {
SDL_Log("error creating texture");
return false;
}
SDL_UpdateTexture(bd->FontTexture, nullptr, pixels, 4 * width);
SDL_SetTextureBlendMode(bd->FontTexture, SDL_BLENDMODE_BLEND);
SDL_SetTextureScaleMode(bd->FontTexture, SDL_SCALEMODE_LINEAR);
// Store our identifier
io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->FontTexture);
return true;
}
void ImGui_ImplSDLRenderer3_DestroyFontsTexture() {
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplSDLRenderer3_Data* bd = ImGui_ImplSDLRenderer3_GetBackendData();
if (bd->FontTexture) {
io.Fonts->SetTexID(0);
SDL_DestroyTexture(bd->FontTexture);
bd->FontTexture = nullptr;
}
}
bool ImGui_ImplSDLRenderer3_CreateDeviceObjects() {
return ImGui_ImplSDLRenderer3_CreateFontsTexture();
}
void ImGui_ImplSDLRenderer3_DestroyDeviceObjects() {
ImGui_ImplSDLRenderer3_DestroyFontsTexture();
}
//-----------------------------------------------------------------------------
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
#endif // #ifndef IMGUI_DISABLE

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on imgui_impl_sdlrenderer3.h from Dear ImGui repository
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
#ifndef IMGUI_DISABLE
struct SDL_Renderer;
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer3_Init(SDL_Renderer* renderer);
IMGUI_IMPL_API void ImGui_ImplSDLRenderer3_Shutdown();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer3_NewFrame();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer3_RenderDrawData(ImDrawData* draw_data,
SDL_Renderer* renderer);
// Called by Init/NewFrame/Shutdown
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer3_CreateFontsTexture();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer3_DestroyFontsTexture();
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer3_CreateDeviceObjects();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer3_DestroyDeviceObjects();
// [BETA] Selected render state data shared with callbacks.
// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the
// ImGui_ImplSDLRenderer3_RenderDrawData() call. (Please open an issue if you feel you need access
// to more data)
struct ImGui_ImplSDLRenderer3_RenderState {
SDL_Renderer* Renderer;
};
#endif // #ifndef IMGUI_DISABLE

View File

@ -20,6 +20,8 @@
#include "core/file_sys/fs.h"
#include "core/ipc/ipc.h"
#include "emulator.h"
#include "imgui/big_picture.h"
#ifdef _WIN32
#include <windows.h>
#endif
@ -73,6 +75,7 @@ int main(int argc, char* argv[]) {
bool configClean = false;
bool configGlobal = false;
bool logAppend = false;
bool bigPicture = false;
std::optional<std::filesystem::path> addGameFolder;
std::optional<std::filesystem::path> setAddonFolder;
@ -84,6 +87,8 @@ int main(int argc, char* argv[]) {
app.add_flag("-i,--ignore-game-patch", ignoreGamePatch,
"Disable automatic loading of game patches");
app.add_flag("-b,--big-picture", bigPicture, "Start in Big Picture Mode");
// FULLSCREEN: behavior-identical
app.add_option("-f,--fullscreen", fullscreenStr, "Fullscreen mode (true|false)");
@ -140,7 +145,7 @@ int main(int argc, char* argv[]) {
return 0;
}
if (!gamePath.has_value()) {
if (!gamePath.has_value() && !bigPicture) {
if (!gameArgs.empty()) {
gamePath = gameArgs.front();
gameArgs.erase(gameArgs.begin());
@ -200,7 +205,7 @@ int main(int argc, char* argv[]) {
break;
}
}
if (!found) {
if (!found && !bigPicture) {
std::cerr << "Error: Game ID or file path not found: " << *gamePath << "\n";
return 1;
}
@ -209,10 +214,14 @@ int main(int argc, char* argv[]) {
if (waitPid)
Core::Debugger::WaitForPid(*waitPid);
auto* emulator = Common::Singleton<Core::Emulator>::Instance();
emulator->executableName = argv[0];
emulator->waitForDebuggerBeforeRun = waitForDebugger;
emulator->Run(ebootPath, gameArgs, overrideRoot);
if (bigPicture) {
BigPictureMode::Launch();
} else {
auto* emulator = Common::Singleton<Core::Emulator>::Instance();
emulator->executableName = argv[0];
emulator->waitForDebuggerBeforeRun = waitForDebugger;
emulator->Run(ebootPath, gameArgs, overrideRoot);
}
return 0;
}