Files
ppsspp/UI/DeveloperToolsScreen.cpp
Henrik RydgårdandClaude Opus 5 05f5668dfe Save symbols outside any module to a per-game file, and only save real names
Module symbols are keyed by module+crc so they're shared by every game that
loads that module. But symbols the user attaches to addresses that aren't in
any module - the heap, the stack, scratchpad, a hardware register, typically
after a memory.search - describe one game's own memory layout and are worthless
to any other game. Those now go to PSP/SYSTEM/SYMBOLS/<gameID>_syms.ppsym.

They're module index 0 ("absolute"), which already round-trips through the
existing per-module code: GetModuleRelativeAddr/GetModuleAbsoluteAddr are
identity for it, so the file format is unchanged, just with absolute addresses.
SaveModuleSymbols only needed to stop requiring a ModuleEntry. Auto-load/save
hang off CPU_Init/CPU_Shutdown rather than module load/unload, gated on the same
bAutoSaveLoadSymbols setting - and deliberately not on SYSPROP_HAS_DEBUGGER,
which only the Windows port reports true for, so LoadSymbolsIfSupported next to
it does nothing at all on headless. hle.game.saveSymbols/loadSymbols expose it.

Four things found while doing it:

- Symbols outside any module were being dropped on the floor. AddFunction/
  AddData/AddLabel take moduleIndex -1 as "work it out", pass it to
  GetModuleIndex(), and store whatever comes back - but that returns -1 when no
  module contains the address, and -1 is never an active module, so the symbol
  never reached the active maps: invisible to every lookup and to any save.
  hle.data.add had spotted this and normalized -1 to 0 locally; nothing else
  did, so e.g. hle.func.add outside a module silently did nothing. Fixed
  centrally in a new ResolveModuleIndex() the three of them share.
  (This only became reachable with the GetModuleIndex() fix in 29a38af37e -
  before that it returned a wrong-but-valid module index instead.)

- The saved files were almost entirely noise. Every function the analyzer finds
  is named z_un_<addr> and every import stub zz_<name>, both regenerated from
  scratch on each load. One real module wrote 13KB - 443 unnamed functions and
  64 stub names - for the four names a human had actually chosen. Worse, on the
  next run those were loaded back as authoritative and would beat the module's
  own symbols to the address. Now only names that aren't regenerated get saved,
  and a module with none writes no file at all (and removes a stale one, so
  deleting a symbol sticks). That module's file went 13020 -> 81 bytes.

- LoadModuleSymbols trusted the addresses in the file. It's meant to be
  hand-edited and can outlive the build it came from, so relative addresses past
  the end of the module are now skipped with a warning instead of landing at
  nonsense addresses.

- AddFunction and AddData both erased the map entry they were updating and then
  read back through the now-dangling iterator to refresh the active copy. Only
  latent: the refresh is guarded on the active copy's module matching the new
  one, which is false exactly when the erase happens. Re-point the iterator at
  the entry's new home instead, so it can't rot if that guard ever changes.
  AddLabel already did the equivalent correctly, via a local copy.

Filename sanitizing goes through SanitizeString with a new FileName restriction
rather than being open-coded; unlike the existing restrictions it substitutes
'_' instead of dropping, so two module names can't collapse onto one file.

Verified end to end on headless with cpu_alu.prx: named a function inside the
module and data/functions in scratchpad and the heap, let it exit, checked both
files, rebooted and confirmed all of it came back at the right addresses.
Unit tests 51/51, pspautotests 314/314.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 16:53:48 +02:00

853 lines
34 KiB
C++

// Copyright (c) 2013- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <string>
#include "android/jni/app-android.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Common/System/OSD.h"
#include "Common/GPU/OpenGL/GLFeatures.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/File/FileUtil.h"
#include "Common/Render/Text/draw_text.h"
#include "Common/StringUtils.h"
#include "Common/UI/ScreenManager.h"
#include "GPU/Common/TextureReplacer.h"
#include "GPU/Common/PostShader.h"
#include "Core/MIPS/MIPSTracer.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/Config.h"
#include "Core/HLE/HLE.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "Core/WebServer.h"
#include "Core/Util/PathUtil.h"
#include "Core/FileSystems/VirtualDiscFileSystem.h"
#include "UI/GPUDriverTestScreen.h"
#include "UI/DeveloperToolsScreen.h"
#include "UI/DevScreens.h"
#include "UI/DriverManagerScreen.h"
#include "UI/DisplayLayoutScreen.h"
#include "UI/GameSettingsScreen.h"
#include "UI/OnScreenDisplay.h"
#include "UI/IconCache.h"
#include "UI/MiscViews.h"
#if PPSSPP_PLATFORM(ANDROID)
static bool CheckKgslPresent() {
constexpr auto KgslPath{ "/dev/kgsl-3d0" };
return access(KgslPath, F_OK) == 0;
}
static bool SupportsCustomDriver() {
return android_get_device_api_level() >= 28 && CheckKgslPresent();
}
#else
static bool SupportsCustomDriver() {
#ifdef _DEBUG
return false; // change to true to debug driver installation on other platforms
#else
return false;
#endif
}
#endif
static std::string PostShaderTranslateName(std::string_view value) {
const ShaderInfo *info = GetPostShaderInfo(value);
if (info) {
auto ps = GetI18NCategory(I18NCat::POSTSHADERS);
return std::string(ps->T(value, info->name));
} else {
return std::string(value);
}
}
DeveloperToolsScreen::DeveloperToolsScreen(const Path &gamePath)
: UITabbedBaseDialogScreen(gamePath, &g_Config.iDeveloperSettingsCurrentTab, TabDialogFlags::AddAutoTitles) {}
void DeveloperToolsScreen::CreateTextureReplacementTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
auto di = GetI18NCategory(I18NCat::DIALOG);
list->Add(new ItemHeader(dev->T("Texture Replacement")));
list->Add(new CheckBox(&g_Config.bSaveNewTextures, dev->T("Save new textures")));
list->Add(new CheckBox(&g_Config.bReplaceTextures, dev->T("Replace textures")));
Choice *createTextureIni = list->Add(new Choice(dev->T("Create/Open textures.ini file for current game")));
createTextureIni->OnClick.Handle(this, &DeveloperToolsScreen::OnOpenTexturesIniFile);
createTextureIni->SetEnabledFunc([&] {
if (!PSP_IsInited())
return false;
// Disable the choice to Open/Create if the textures.ini file already exists, and we can't open it due to platform support limitations.
if (!System_GetPropertyBool(SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR)) {
if (hasTexturesIni_ == HasIni::MAYBE)
hasTexturesIni_ = TextureReplacer::IniExists(g_paramSFO.GetDiscID()) ? HasIni::YES : HasIni::NO;
return hasTexturesIni_ != HasIni::YES;
}
return true;
});
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
// Best string we have
list->Add(new Choice(di->T("Show in folder")))->OnClick.Add([=](UI::EventParams &) {
Path path;
if (PSP_IsInited()) {
std::string gameID = g_paramSFO.GetDiscID();
path = GetSysDirectory(DIRECTORY_TEXTURES) / gameID;
} else {
// Just show the root textures directory.
path = GetSysDirectory(DIRECTORY_TEXTURES);
}
System_ShowFileInFolder(path);
});
}
static const char *texLoadSpeeds[] = { "Slow (smooth)", "Medium", "Fast", "Instant (may stutter)" };
PopupMultiChoice *texLoadSpeed = list->Add(new PopupMultiChoice(&g_Config.iReplacementTextureLoadSpeed, dev->T("Replacement texture load speed"), texLoadSpeeds, 0, ARRAY_SIZE(texLoadSpeeds), I18NCat::DEVELOPER, screenManager()));
texLoadSpeed->SetChoiceIcon(3, ImageID("I_WARNING"));
}
void DeveloperToolsScreen::CreateGeneralTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
auto ms = GetI18NCategory(I18NCat::MAINSETTINGS);
list->Add(new ItemHeader(sy->T("CPU Core")));
bool canUseJit = System_GetPropertyBool(SYSPROP_CAN_JIT);
// iOS can now use JIT on all modes, apparently.
// The bool may come in handy for future non-jit platforms though (UWP XB1?)
// In iOS App Store builds, we disable the JIT.
static const char *cpuCores[] = { "Interpreter", "Dynarec/JIT (recommended)", "IR Interpreter", "JIT using IR" };
PopupMultiChoice *core = list->Add(new PopupMultiChoice(&g_Config.iCpuCore, sy->T("CPU Core"), cpuCores, 0, ARRAY_SIZE(cpuCores), I18NCat::SYSTEM, screenManager()));
core->OnChoice.Add([=](UI::EventParams &e) {
OnJitAffectingSetting(e);
g_Config.NotifyUpdatedCpuCore();
});
if (!canUseJit) {
core->HideChoice(1);
core->HideChoice(3);
}
// TODO: Enable "JIT using IR" on more architectures. ARM32 needs more testing.
// Note also that Loongarch and RISC-V only have a jit-ir backend, and it's used for the JIT option, so the fourth option isn't shown.
#if !PPSSPP_ARCH(X86) && !PPSSPP_ARCH(AMD64) && !PPSSPP_ARCH(ARM64)
core->HideChoice(3);
#endif
list->Add(new Choice(dev->T("JIT debug tools")))->OnClick.Handle(this, &DeveloperToolsScreen::OnJitDebugTools);
list->Add(new CheckBox(&g_Config.bShowDeveloperMenu, dev->T("Show in-game developer menu")));
AddOverlayList(list, screenManager());
list->Add(new ItemHeader(sy->T("General")));
list->Add(new CheckBox(&g_Config.bEnableLogging, dev->T("Enable Logging")))->OnClick.Handle(this, &DeveloperToolsScreen::OnLoggingChanged);
list->Add(new Choice(dev->T("Logging Channels")))->OnClick.Add([this](UI::EventParams &e) {
screenManager()->push(new LogConfigScreen());
});
list->Add(new CheckBox(&g_Config.bEnableFileLogging, dev->T("Log to file")))->SetEnabledPtr(&g_Config.bEnableLogging);
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_DESKTOP) {
list->Add(new Choice(dev->T("Show log file in folder")))->OnClick.Add([](UI::EventParams &e) {
Path logFilePath = g_logManager.GetLogFilePath();
if (logFilePath.empty()) {
ERROR_LOG(Log::System, "No log file path configured.");
return;
}
if (File::Exists(logFilePath)) {
System_ShowFileInFolder(logFilePath);
} else {
System_LaunchUrl(LaunchUrlType::LOCAL_FILE, logFilePath.NavigateUp().ToString());
}
});
}
list->Add(new CheckBox(&g_Config.bLogFrameDrops, dev->T("Log Dropped Frame Statistics")));
if (GetGPUBackend() == GPUBackend::VULKAN) {
list->Add(new CheckBox(&g_Config.bGpuLogProfiler, dev->T("GPU log profiler")));
}
allowDebugger_ = !WebServerStopped(WebServerFlags::DEBUGGER);
canAllowDebugger_ = !WebServerStopping(WebServerFlags::DEBUGGER);
CheckBox *allowDebugger = new CheckBox(&allowDebugger_, dev->T("Allow remote debugger"));
list->Add(allowDebugger)->OnClick.Handle(this, &DeveloperToolsScreen::OnRemoteDebugger);
allowDebugger->SetEnabledPtr(&canAllowDebugger_);
CheckBox *localDebugger = list->Add(new CheckBox(&g_Config.bRemoteDebuggerLocal, dev->T("Use locally hosted remote debugger")));
localDebugger->SetEnabledPtr(&allowDebugger_);
list->Add(new CheckBox(&g_Config.bAutoSaveLoadSymbols, dev->T("Auto save/load symbols")));
list->Add(new Choice(dev->T("GPI/GPO switches/LEDs")))->OnClick.Add([=](UI::EventParams &e) {
screenManager()->push(new GPIGPOScreen(dev->T("GPI/GPO switches/LEDs")));
});
list->Add(new CheckBox(&g_Config.bShowSaveLoadIndicator, dev->T("Show indicator when saving/loading")));
#if PPSSPP_PLATFORM(ANDROID)
static const char *framerateModes[] = { "Default", "Request 60 Hz", "Force 60Hz" };
PopupMultiChoice *framerateMode = list->Add(new PopupMultiChoice(&g_Config.iDisplayFramerateMode, gr->T("Framerate mode"), framerateModes, 0, ARRAY_SIZE(framerateModes), I18NCat::GRAPHICS, screenManager()));
framerateMode->SetEnabledFunc([]() { return System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30; });
framerateMode->OnChoice.Add([](UI::EventParams &e) {
System_Notify(SystemNotification::FORCE_RECREATE_ACTIVITY);
});
#endif
#if PPSSPP_PLATFORM(IOS)
list->Add(new NoticeView(NoticeLevel::WARN, ms->T("Moving the memstick directory is NOT recommended on iOS"), ""))->SetWrapText(true);
list->Add(new Choice(sy->T("Set Memory Stick folder")))->OnClick.Add(
[=](UI::EventParams &) {
SetMemStickDirDarwin(GetRequesterToken());
});
#endif
// Makes it easy to get savestates out of an iOS device. The file listing shown in MacOS doesn't allow
// you to descend into directories.
#if PPSSPP_PLATFORM(IOS)
list->Add(new Choice(dev->T("Copy savestates to memstick root")))->OnClick.Handle(this, &DeveloperToolsScreen::OnCopyStatesToRoot);
#endif
auto di = GetI18NCategory(I18NCat::DIALOG);
// Reuse strings to the max, heh.
list->Add(new Choice(ApplySafeSubstitutions("%1: ppsspp.ini", di->T("Copy to clipboard"))))->OnClick.Add([=](UI::EventParams &) {
auto di = GetI18NCategory(I18NCat::DIALOG);
std::string configStr = g_Config.GetConfigAsString();
if (!configStr.empty()) {
System_CopyStringToClipboard(configStr);
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), "ppsspp.ini"), 0.0f, "copyToClip");
}
});
#if PLATFORM_SUPPORTS_FILE_HANDLER_PLUGINS
list->Add(new CheckBox(&g_Config.bEnableFileHandlerPlugins, dev->T("Enable file handler plugins (insecure)")));
#endif
}
void DeveloperToolsScreen::CreateTestsTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
list->Add(new Choice(dev->T("Touchscreen Test")))->OnClick.Add([this](UI::EventParams &e) {
screenManager()->push(new TouchTestScreen(gamePath_));
// Handle touchscreen test event
});
// list->Add(new Choice(dev->T("Memstick Test")))->OnClick.Handle(this, &DeveloperToolsScreen::OnMemstickTest);
Choice *frameDumpTests = list->Add(new Choice(dev->T("Framedump tests")));
frameDumpTests->OnClick.Add([this](UI::EventParams &e) {
screenManager()->push(new FrameDumpTestScreen());
});
frameDumpTests->SetEnabled(!PSP_IsInited());
// For now, we only implement GPU driver tests for Vulkan and OpenGL. This is simply
// because the D3D drivers are generally solid enough to not need this type of investigation.
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN || g_Config.iGPUBackend == (int)GPUBackend::OPENGL) {
list->Add(new Choice(dev->T("GPU Driver Test")))->OnClick.Handle(this, &DeveloperToolsScreen::OnGPUDriverTest);
}
// Not useful enough to be made visible.
/*
auto memmapTest = list->Add(new Choice(dev->T("Memory map test")));
memmapTest->OnClick.Add([this](UI::EventParams &e) {
MemoryMapTest();
});
memmapTest->SetEnabled(PSP_IsInited());
*/
}
void DeveloperToolsScreen::CreateDumpFileTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
list->Add(new ItemHeader(dev->T("Dump files")));
list->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::EBOOT, dev->T("Dump Decrypted Eboot", "Dump Decrypted EBOOT.BIN (If Encrypted) When Booting Game")));
list->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::PRX, dev->T("PRX")));
list->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::Atrac3, dev->T("Atrac3/3+")));
list->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::PBP_ISO, dev->T("ISO from PBP")));
}
void DeveloperToolsScreen::CreateHLETab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
list->Add(new CheckBox(&g_Config.bUseOldAtrac, dev->T("Use the old sceAtrac implementation")));
list->Add(new ItemHeader(dev->T("Disable HLE")));
for (int i = 0; i < (int)DisableHLEFlags::Count; i++) {
DisableHLEFlags flag = (DisableHLEFlags)(1 << i);
// Show a checkbox, unless the setting has graduated to always disabled.
if (!(flag & AlwaysDisableHLEFlags())) {
const HLEModuleMeta *meta = GetHLEModuleMetaByFlag(flag);
if (meta) {
BitCheckBox *checkBox = list->Add(new BitCheckBox(&g_Config.iDisableHLE, (int)flag, meta->modname));
checkBox->SetEnabled(!PSP_IsInited());
}
}
}
list->Add(new ItemHeader(dev->T("Force-enable HLE")));
for (int i = 0; i < (int)DisableHLEFlags::Count; i++) {
DisableHLEFlags flag = (DisableHLEFlags)(1 << i);
// Show a checkbox, only if the setting has graduated to always disabled (and thus it makes sense to force-enable it).
if (flag & AlwaysDisableHLEFlags()) {
const HLEModuleMeta *meta = GetHLEModuleMetaByFlag(flag);
if (meta) {
BitCheckBox *checkBox = list->Add(new BitCheckBox(&g_Config.iForceEnableHLE, (int)flag, meta->modname));
checkBox->SetEnabled(!PSP_IsInited());
}
}
}
}
void DeveloperToolsScreen::CreateMIPSTracerTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
list->Add(new ItemHeader(dev->T("MIPSTracer")));
MIPSTracerEnabled_ = mipsTracer.tracing_enabled;
CheckBox *MIPSTracerEnabled = new CheckBox(&MIPSTracerEnabled_, dev->T("MIPSTracer enabled"));
list->Add(MIPSTracerEnabled)->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerEnabled);
MIPSTracerEnabled->SetEnabledFunc([]() {
bool temp = g_Config.iCpuCore == static_cast<int>(CPUCore::IR_INTERPRETER) && PSP_IsInited();
return temp && Core_IsStepping() && coreState != CORE_POWERDOWN;
});
Choice *TraceDumpPath = list->Add(new Choice(dev->T("Select the file path for the trace")));
TraceDumpPath->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerPathChanged);
TraceDumpPath->SetEnabledFunc([]() {
if (!PSP_IsInited())
return false;
return true;
});
MIPSTracerPath_ = mipsTracer.get_logging_path();
MIPSTracerPath = list->Add(new InfoItem(dev->T("Current log file"), MIPSTracerPath_));
PopupSliderChoice* storage_capacity = list->Add(
new PopupSliderChoice(
&mipsTracer.in_storage_capacity, 0x4'0000, 0x40'0000, 0x10'0000, dev->T("Storage capacity"), 0x10000, screenManager()
)
);
storage_capacity->SetFormat("0x%x asm opcodes");
storage_capacity->OnChange.Add([&](UI::EventParams &) {
INFO_LOG(Log::JIT, "User changed the tracer's storage capacity to 0x%x", mipsTracer.in_storage_capacity);
});
PopupSliderChoice* trace_max_size = list->Add(
new PopupSliderChoice(
&mipsTracer.in_max_trace_size, 0x1'0000, 0x40'0000, 0x10'0000, dev->T("Max allowed trace size"), 0x10000, screenManager()
)
);
trace_max_size->SetFormat("%d basic blocks");
trace_max_size->OnChange.Add([&](UI::EventParams &) {
INFO_LOG(Log::JIT, "User changed the tracer's max trace size to %d", mipsTracer.in_max_trace_size);
});
list->Add(new ItemHeader(dev->T("MIPSTracer actions")));
Choice *FlushTrace = list->Add(new Choice(dev->T("Flush the trace")));
FlushTrace->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerFlushTrace);
Choice *InvalidateJitCache = list->Add(new Choice(dev->T("Clear the JIT cache")));
InvalidateJitCache->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerClearJitCache);
Choice *ClearMIPSTracer = list->Add(new Choice(dev->T("Clear the MIPSTracer")));
ClearMIPSTracer->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerClearTracer);
}
void DeveloperToolsScreen::CreateAudioTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
list->Add(new CheckBox(&g_Config.bForceFfmpegForAudioDec, dev->T("Use FFMPEG for all compressed audio")));
}
void DeveloperToolsScreen::CreateUITab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
UIContext *uiContext = screenManager()->getUIContext();
list->Add(new Choice(dev->T("Reload UI atlas")))->OnClick.Add([uiContext](UI::EventParams &) {
uiContext->InvalidateAtlas();
});
auto di = GetI18NCategory(I18NCat::DIALOG);
auto si = GetI18NCategory(I18NCat::SYSINFO);
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS);
// TODO: Move most of these strings out of the SysInfo category.
list->Add(new ItemHeader(si->T("Icon cache")));
IconCacheStats iconStats = g_iconCache.GetStats();
list->Add(new InfoItem(si->T("Image data count"), StringFromFormat("%d", iconStats.cachedCount)));
list->Add(new InfoItem(si->T("Texture count"), StringFromFormat("%d", iconStats.textureCount)));
list->Add(new InfoItem(si->T("Data size"), NiceSizeFormat(iconStats.dataSize)));
list->Add(new Choice(di->T("Clear")))->OnClick.Add([&](UI::EventParams &) {
g_iconCache.ClearData();
RecreateViews();
});
list->Add(new ItemHeader(si->T("Font cache")));
const TextDrawer *text = screenManager()->getUIContext()->Text();
if (text) {
list->Add(new InfoItem(si->T("Texture count"), StringFromFormat("%d", text->GetStringCacheSize())));
list->Add(new InfoItem(si->T("Data size"), NiceSizeFormat(text->GetCacheDataSize())));
}
list->Add(new ItemHeader(si->T("Slider test")));
list->Add(new Slider(&testSliderValue_, 0, 100, 1, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
static const char *positions[] = {"Bottom Left", "Bottom Center", "Bottom Right", "Top Left", "Top Center", "Top Right", "Center Left", "Center Right", "None"};
list->Add(new ItemHeader(si->T("Notification tests")));
list->Add(new Choice(si->T("Error")))->OnClick.Add([&](UI::EventParams &) {
std::string str = "Error " + CodepointToUTF8(0x1F41B) + CodepointToUTF8(0x1F41C) + CodepointToUTF8(0x1F914);
g_OSD.Show(OSDType::MESSAGE_ERROR, str);
});
list->Add(new Choice(si->T("Warning")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.Show(OSDType::MESSAGE_WARNING, "Warning, a pretty long warning heading", "Some\nAdditional\nDetail, some of which is very, very long and wide and will need line wrapping on most screens.");
});
list->Add(new Choice(si->T("Info")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.Show(OSDType::MESSAGE_INFO, "Info, info info info info info info info info info info");
});
// This one is clickable
list->Add(new Choice(si->T("Success")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.Show(OSDType::MESSAGE_SUCCESS, "Success", 0.0f, "clickable");
g_OSD.SetClickCallback("clickable", []() {
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.google.com/");
});
});
list->Add(new Choice(sy->T("RetroAchievements")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.Show(OSDType::MESSAGE_WARNING, "RetroAchievements warning", "", "I_RETROACHIEVEMENTS_LOGO");
});
list->Add(new ItemHeader(si->T("Progress tests")));
list->Add(new Choice(si->T("30%")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.SetProgressBar("testprogress", "Test Progress", 1, 100, 30, 0.0f);
});
list->Add(new Choice(si->T("100%")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.SetProgressBar("testprogress", "Test Progress", 1, 100, 100, 1.0f);
});
list->Add(new Choice(si->T("N/A%")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.SetProgressBar("testprogress", "Test Progress", 0, 0, 0, 0.0f);
});
list->Add(new Choice(si->T("Success")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.RemoveProgressBar("testprogress", true, 0.5f);
});
list->Add(new Choice(si->T("Failure")))->OnClick.Add([&](UI::EventParams &) {
g_OSD.RemoveProgressBar("testprogress", false, 0.5f);
});
list->Add(new ItemHeader(si->T("Achievement tests")));
list->Add(new Choice(si->T("Leaderboard tracker: Show")))->OnClick.Add([=](UI::EventParams &) {
g_OSD.ShowLeaderboardTracker(1, "My leaderboard tracker", true);
});
list->Add(new Choice(si->T("Leaderboard tracker: Update")))->OnClick.Add([=](UI::EventParams &) {
g_OSD.ShowLeaderboardTracker(1, "Updated tracker", true);
});
list->Add(new Choice(si->T("Leaderboard tracker: Hide")))->OnClick.Add([=](UI::EventParams &) {
g_OSD.ShowLeaderboardTracker(1, "", false);
});
list->Add(new ItemHeader(ac->T("Notifications")));
list->Add(new PopupMultiChoice(&g_Config.iNotificationPos, sy->T("Notification screen position"), positions, 0, ARRAY_SIZE(positions), I18NCat::DIALOG, screenManager()));
list->Add(new PopupMultiChoice(&g_Config.iAchievementsLeaderboardTrackerPos, ac->T("Leaderboard tracker"), positions, 0, ARRAY_SIZE(positions), I18NCat::DIALOG, screenManager()));
#ifdef _DEBUG
list->Add(new CheckBox(&pretendIngame_, ac->T("Pretend to be in-game (for testing)")));
// Untranslated string because this is debug mode only, only for PPSSPP developers.
list->Add(new ItemHeader(ac->T("Assert")));
list->Add(new Choice("Assert"))->OnClick.Add([=](UI::EventParams &) {
_dbg_assert_msg_(false, "Test assert message");
});
#endif
#if PPSSPP_PLATFORM(ANDROID)
list->Add(new Choice(si->T("Exception")))->OnClick.Add([&](UI::EventParams &) {
System_Notify(SystemNotification::TEST_JAVA_EXCEPTION);
});
#endif
}
void DeveloperToolsScreen::CreateNetworkTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
auto ms = GetI18NCategory(I18NCat::MAINSETTINGS);
auto ri = GetI18NCategory(I18NCat::REMOTEISO);
auto nw = GetI18NCategory(I18NCat::NETWORKING);
list->Add(new ItemHeader(ms->T("Networking")));
list->Add(new CheckBox(&g_Config.bDontDownloadInfraJson, dev->T("Don't download infra-dns.json")));
// This is shared between RemoteISO and the remote debugger.
list->Add(new PopupSliderChoice(&g_Config.iRemoteISOPort, 0, 65535, 0, ri->T("Local Server Port", "Local Server Port"), 100, screenManager()));
list->Add(new ItemHeader(nw->T("AdHoc server")));
list->Add(new CheckBox(&g_Config.bAdhocServerShowPlayerPorts, nw->T("Show player port numbers")));
}
// TODO: Make this generic
extern int DefaultDepthRaster();
void DeveloperToolsScreen::CreateGraphicsTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
auto ps = GetI18NCategory(I18NCat::POSTSHADERS);
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto si = GetI18NCategory(I18NCat::SYSINFO);
Draw::DrawContext *draw = screenManager()->getDrawContext();
list->Add(new ItemHeader(sy->T("General")));
list->Add(new CheckBox(&g_Config.bVendorBugChecksEnabled, dev->T("Enable driver bug workarounds")));
list->Add(new CheckBox(&g_Config.bShaderCache, dev->T("Enable shader cache")));
auto displayRefreshRate = list->Add(new PopupSliderChoice(&g_Config.iDisplayRefreshRate, 60, 1000, 60, dev->T("Display refresh rate"), 1, screenManager()));
displayRefreshRate->SetFormat(si->T("%d Hz"));
list->Add(new ItemHeader(dev->T("Vulkan")));
list->Add(new CheckBox(&g_Config.bVulkanDisableImplicitLayers, dev->T("Prevent loading overlays")));
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
list->Add(new CheckBox(&g_Config.bRenderMultiThreading, dev->T("Multi-threaded rendering"), ""))->OnClick.Add([](UI::EventParams &e) {
// TODO: Not translating yet. Will combine with other translations of settings that need restart.
g_OSD.Show(OSDType::MESSAGE_WARNING, "Restart required");
});
}
if (GetGPUBackend() == GPUBackend::VULKAN && SupportsCustomDriver()) {
auto driverChoice = list->Add(new Choice(gr->T("AdrenoTools driver manager")));
driverChoice->OnClick.Add([=](UI::EventParams &e) {
screenManager()->push(new DriverManagerScreen(gamePath_));
});
}
static const char *depthRasterModes[] = { "Auto", "Low", "Off", "Always on" };
PopupMultiChoice *depthRasterMode = list->Add(new PopupMultiChoice(&g_Config.iDepthRasterMode, gr->T("Lens flare occlusion"), depthRasterModes, 0, ARRAY_SIZE(depthRasterModes), I18NCat::GRAPHICS, screenManager()));
depthRasterMode->SetDisabledPtr(&g_Config.bSoftwareRendering);
depthRasterMode->SetDefault(DefaultDepthRaster());
depthRasterMode->SetChoiceIcon(3, ImageID("I_WARNING")); // It's a performance trap.
list->Add(new ItemHeader(dev->T("Ubershaders")));
if (draw->GetShaderLanguageDesc().bitwiseOps && !draw->GetBugs().Has(Draw::Bugs::UNIFORM_INDEXING_BROKEN)) {
// If the above if fails, the checkbox is redundant since it'll be force disabled anyway.
list->Add(new CheckBox(&g_Config.bUberShaderVertex, dev->T("Vertex")));
}
// Experimental, allow some VR features without OpenXR
if (GetGPUBackend() == GPUBackend::OPENGL) {
auto vr = GetI18NCategory(I18NCat::VR);
list->Add(new ItemHeader(vr->T("Virtual reality")));
list->Add(new CheckBox(&g_Config.bForceVR, vr->T("VR camera")));
}
// Experimental, will move to main graphics settings later.
bool multiViewSupported = draw->GetDeviceCaps().multiViewSupported;
auto enableStereo = [=]() -> bool {
return g_Config.bStereoRendering && multiViewSupported;
};
if (multiViewSupported) {
list->Add(new ItemHeader(gr->T("Stereo rendering")));
list->Add(new CheckBox(&g_Config.bStereoRendering, gr->T("Stereo rendering")));
std::vector<std::string> stereoShaderNames;
ChoiceWithValueDisplay *stereoShaderChoice = list->Add(new ChoiceWithValueDisplay(&g_Config.sStereoToMonoShader, gr->T("Stereo display shader"), &PostShaderTranslateName));
stereoShaderChoice->SetEnabledFunc(enableStereo);
stereoShaderChoice->OnClick.Add([=](EventParams &e) {
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
auto procScreen = new PostProcScreen(gr->T("Stereo display shader"), 0, true);
if (e.v)
procScreen->SetPopupOrigin(e.v);
screenManager()->push(procScreen);
});
const ShaderInfo *shaderInfo = GetPostShaderInfo(g_Config.sStereoToMonoShader);
if (shaderInfo) {
for (size_t i = 0; i < ARRAY_SIZE(shaderInfo->settings); ++i) {
auto &setting = shaderInfo->settings[i];
if (!setting.name.empty()) {
std::string key = StringFromFormat("%sSettingCurrentValue%d", shaderInfo->section.c_str(), i + 1);
bool keyExisted = g_Config.mPostShaderSetting.find(key) != g_Config.mPostShaderSetting.end();
auto &value = g_Config.mPostShaderSetting[key];
if (!keyExisted)
value = setting.value;
PopupSliderChoiceFloat *settingValue = list->Add(new PopupSliderChoiceFloat(&value, setting.minValue, setting.maxValue, setting.value, ps->T(setting.name), setting.step, screenManager()));
settingValue->SetEnabledFunc([=] {
return !g_Config.bSkipBufferEffects && enableStereo();
});
}
}
}
}
}
void DeveloperToolsScreen::CreateCrashHistoryTab(UI::LinearLayout *list) {
using namespace UI;
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
auto di = GetI18NCategory(I18NCat::DIALOG);
std::vector<std::string> reports = Android_GetNativeCrashHistory(20);
list->Add(new ItemHeader(dev->T("Crash history")));
if (reports.empty()) {
list->Add(new TextView(di->T("None")));
return;
}
for (size_t i = 0; i < reports.size(); i++) {
std::string name = StringFromFormat("Crash %d", (int)i);
CollapsibleSection *section = list->Add(new CollapsibleSection(name));
const std::string report = reports[i];
if (report.size() > 150) {
section->Add(new Choice(di->T("Copy to clipboard"), ImageID("I_FILE_COPY")))->OnClick.Add([report](UI::EventParams&) {
System_CopyStringToClipboard(report);
});
}
section->Add(new TextView(report, FLAG_WRAP_TEXT | FLAG_DYNAMIC_ASCII, true));
}
}
void DeveloperToolsScreen::CreateTabs() {
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto ms = GetI18NCategory(I18NCat::MAINSETTINGS);
AddTab("General", sy->T("General"), [this](UI::LinearLayout *parent) {
CreateGeneralTab(parent);
});
AddTab("TextureReplacement", dev->T("Texture Replacement"), [this](UI::LinearLayout *parent) {
CreateTextureReplacementTab(parent);
});
AddTab("Graphics", ms->T("Graphics"), [this](UI::LinearLayout *parent) {
CreateGraphicsTab(parent);
});
AddTab("Networking", ms->T("Networking"), [this](UI::LinearLayout *parent) {
CreateNetworkTab(parent);
});
AddTab("Audio", ms->T("Audio"), [this](UI::LinearLayout *parent) {
CreateAudioTab(parent);
});
AddTab("Tests", dev->T("Tests"), [this](UI::LinearLayout *parent) {
CreateTestsTab(parent);
});
AddTab("UI", dev->T("UI"), [this](UI::LinearLayout *parent) {
CreateUITab(parent);
});
AddTab("DumpFiles", dev->T("Dump files"), [this](UI::LinearLayout *parent) {
CreateDumpFileTab(parent);
});
// Need a better title string.
AddTab("HLE", dev->T("Disable HLE"), [this](UI::LinearLayout *parent) {
CreateHLETab(parent);
});
#if !PPSSPP_PLATFORM(ANDROID) && !PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(SWITCH)
AddTab("MIPSTracer", dev->T("MIPSTracer"), [this](UI::LinearLayout *parent) {
CreateMIPSTracerTab(parent);
});
#endif
//#if PPSSPP_PLATFORM(ANDROID)
if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30) {
AddTab("Crash history", dev->T("Crash history"), [this](UI::LinearLayout *parent) {
CreateCrashHistoryTab(parent);
});
}
//#endif
// Reconsider whenever recreating views.
hasTexturesIni_ = HasIni::MAYBE;
}
void DeveloperToolsScreen::onFinish(DialogResult result) {
UIScreen::onFinish(result);
g_Config.Save("DeveloperToolsScreen::onFinish");
System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED);
}
void DeveloperToolsScreen::OnLoggingChanged(UI::EventParams &e) {
System_Notify(SystemNotification::TOGGLE_DEBUG_CONSOLE);
}
void DeveloperToolsScreen::OnOpenTexturesIniFile(UI::EventParams &e) {
std::string gameID = g_paramSFO.GetDiscID();
Path generatedFilename;
if (TextureReplacer::GenerateIni(gameID, generatedFilename)) {
if (System_GetPropertyBool(SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR)) {
File::OpenFileInEditor(generatedFilename);
} else {
// Can't do much here, let's send a "toast" so the user sees that something happened.
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
System_Toast((GetFriendlyPath(generatedFilename) + ": " + dev->T_cstr("Texture ini file created")).c_str());
}
hasTexturesIni_ = HasIni::YES;
}
}
void DeveloperToolsScreen::OnJitDebugTools(UI::EventParams &e) {
screenManager()->push(new JitDebugScreen());
}
void DeveloperToolsScreen::OnGPUDriverTest(UI::EventParams &e) {
screenManager()->push(new GPUDriverTestScreen());
}
void DeveloperToolsScreen::OnJitAffectingSetting(UI::EventParams &e) {
System_PostUIMessage(UIMessage::REQUEST_CLEAR_JIT);
}
void DeveloperToolsScreen::OnCopyStatesToRoot(UI::EventParams &e) {
Path savestate_dir = GetSysDirectory(DIRECTORY_SAVESTATE);
Path root_dir = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT);
std::vector<File::FileInfo> files;
GetFilesInDir(savestate_dir, &files, nullptr, 0);
for (const File::FileInfo &file : files) {
Path src = file.fullName;
Path dst = root_dir / file.name;
INFO_LOG(Log::System, "Copying file '%s' to '%s'", src.c_str(), dst.c_str());
File::Copy(src, dst);
}
}
void DeveloperToolsScreen::OnRemoteDebugger(UI::EventParams &e) {
if (allowDebugger_) {
StartWebServer(WebServerFlags::DEBUGGER);
} else {
StopWebServer(WebServerFlags::DEBUGGER);
}
// Persist the setting. Maybe should separate?
g_Config.bRemoteDebuggerOnStartup = allowDebugger_;
}
void DeveloperToolsScreen::OnMIPSTracerEnabled(UI::EventParams &e) {
if (MIPSTracerEnabled_) {
u32 capacity = mipsTracer.in_storage_capacity;
u32 trace_size = mipsTracer.in_max_trace_size;
mipsTracer.initialize(capacity, trace_size);
mipsTracer.start_tracing();
} else {
mipsTracer.stop_tracing();
}
}
void DeveloperToolsScreen::OnMIPSTracerPathChanged(UI::EventParams &e) {
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
System_BrowseForFileSave(
GetRequesterToken(),
dev->T("Select the log file"),
"trace.txt",
BrowseFileType::ANY,
[this](std::string_view value, int) {
mipsTracer.set_logging_path(std::string(value));
MIPSTracerPath_ = value;
MIPSTracerPath->SetRightText(MIPSTracerPath_);
}
);
}
void DeveloperToolsScreen::OnMIPSTracerFlushTrace(UI::EventParams &e) {
mipsTracer.flush_to_file();
// The error logs are emitted inside the tracer
}
void DeveloperToolsScreen::OnMIPSTracerClearJitCache(UI::EventParams &e) {
INFO_LOG(Log::JIT, "Clearing the jit cache...");
System_PostUIMessage(UIMessage::REQUEST_CLEAR_JIT);
}
void DeveloperToolsScreen::OnMIPSTracerClearTracer(UI::EventParams &e) {
INFO_LOG(Log::JIT, "Clearing the MIPSTracer...");
mipsTracer.clear();
}
void DeveloperToolsScreen::update() {
UIBaseDialogScreen::update();
allowDebugger_ = !WebServerStopped(WebServerFlags::DEBUGGER);
canAllowDebugger_ = !WebServerStopping(WebServerFlags::DEBUGGER);
// For the UI tab's notification tests.
if (pretendIngame_) {
g_OSD.NudgeIngameNotifications();
}
}
void DeveloperToolsScreen::MemoryMapTest() {
int sum = 0;
for (uint64_t addr = 0; addr < 0x100000000ULL; addr += 0x1000) {
const u32 addr32 = (u32)addr;
if (Memory::IsValidAddress(addr32)) {
sum += Memory::ReadUnchecked_U32(addr32);
}
}
// Just to force the compiler to do things properly.
INFO_LOG(Log::JIT, "Total sum: %08x", sum);
}
static bool RunMemstickTest(std::string *error) {
Path testRoot = GetSysDirectory(PSPDirectories::DIRECTORY_CACHE) / "test";
*error = "N/A";
File::CreateDir(testRoot);
if (!File::Exists(testRoot)) {
return false;
}
Path testFilePath = testRoot / "temp.txt";
File::CreateEmptyFile(testFilePath);
// Attempt to delete the test root. This should fail since it still contains files.
File::DeleteDir(testRoot);
if (!File::Exists(testRoot)) {
*error = "testroot was deleted with a file in it!";
return false;
}
File::Delete(testFilePath);
if (File::Exists(testFilePath)) {
*error = "testfile wasn't deleted";
return false;
}
File::DeleteDir(testRoot);
if (File::Exists(testRoot)) {
*error = "testroot wasn't deleted, even when empty";
return false;
}
*error = "passed";
return true;
}
void DeveloperToolsScreen::OnMemstickTest(UI::EventParams &e) {
std::string error;
if (RunMemstickTest(&error)) {
g_OSD.Show(OSDType::MESSAGE_SUCCESS, "Memstick test passed", error, 6.0f);
} else {
g_OSD.Show(OSDType::MESSAGE_ERROR, "Memstick test failed", error, 6.0f);
}
}