Files
ppsspp/UI/DeveloperToolsScreen.cpp
T
Henrik RydgårdandClaude Opus 5 812d8f4c09 Android GL: Delete the JavaGL path, leaving only native EGL
The EGL path from the previous commit is now the only one, so GLSurfaceView
and everything that existed to serve it are gone:

- NativeRenderer.java and NativeGLSurfaceView.java, deleted.
- The javaGL branches in PpssppActivity. GL and Vulkan now share one
  lifecycle: a plain SurfaceView, startRenderLoopThread() when the surface
  arrives, joinRenderLoopThread() when it goes away.
- The "androidJavaGL" queryConfig, and the AndroidNativeEGL setting and its
  developer-tools checkbox.
- displayInit/displayRender, and with them the "Second time, joining the
  emuthread and starting it up again" dance - restarting the emu thread from
  inside a JNI callback because a lost context was only observable as a
  second onSurfaceCreated.
- GraphicsContext::NotifyContextLost, which existed solely because that join
  drained the render queue on a thread with no current context. The render
  thread now joins the emu thread with the context still current, so the
  drain is real work and nothing has to be skipped.
- g_emuThread, now that nothing starts an emu thread outside the render loop.

Three things displayInit/displayRender did that nothing else did, moved
rather than dropped:

- EnterVR and the per-frame StartVRRender/UpdateVRInput/FinishVRRender hooks
  now live in RenderLoopThread's GL branch. Java's GL thread could simply
  return early and wait for the next vsync when the VR session was inactive;
  this loop drives itself, so that case sleeps 16ms instead of spinning.
  Not tested - no headset here.
- RECREATE_VIEWS after the emu thread starts, as before. Vulkan never did
  this, so it's a candidate for removal when the two paths converge.
- The thin3d error callback, which surfaces GL errors as OSD messages. That
  one moved into AndroidEGLGraphicsContext::InitSurface, next to the rest of
  the context's own setup, matching WindowsGLContext.

Tested on a nubia Z2464N (Unisoc UMS9632, Mali-G57, Android 15): cold start,
UI, booting a homebrew, backgrounding and resuming both in the menu and
in-game, and a density change, which destroys and recreates the activity the
way a rotation does. Every cycle tears the EGL context down and builds a new
one, and the renderer comes back with FBOs and textures intact. The config
ladder's first choice, RGBA8888 D24S8, is what the Java chooser landed on
here too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:22:25 +02:00

859 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
// Only the interpreter raises these, but don't hide it behind the CPU core choice - it'd just
// look like the setting had vanished.
list->Add(new CheckBox(&g_Config.bEnableFPUExceptionTraps, dev->T("Enable FPU exceptions")));
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")));
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);
procScreen->OnChoice.Add([this](EventParams &) {
RecreateViews();
});
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);
});
AddTab("HLE", dev->T("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);
}
}