mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-06 20:55:23 +02:00
Freeze-frame restores a savestate every frame, straight through SaveState::LoadFromRam(), so it never touched the operation queue and neither hardcore check saw it. Blocked at the toggle in the dev menu, and again in the render loop, since hardcore mode can come up after the fact once the game has been identified. Enqueue also just dropped operations silently, so a load that arrived through a path with no check of its own (--state, auto-load) did nothing with no explanation. Both it and Process now go through WarnUserIfHardcoreModeActive, which is the same predicate plus the standard message. Callers that already ask it themselves return before reaching Enqueue, so nothing shows the message twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
908 lines
31 KiB
C++
908 lines
31 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/.
|
|
|
|
// Hack around name collisions between UI and xlib
|
|
// Only affects this file.
|
|
#undef VK_USE_PLATFORM_XLIB_KHR
|
|
#undef VK_USE_PLATFORM_XCB_KHR
|
|
#undef VK_USE_PLATFORM_DIRECTFB_EXT
|
|
#undef VK_USE_PLATFORM_XLIB_XRANDR_EXT
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include "Common/Common.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/System/OSD.h"
|
|
#include "Common/Audio/AudioBackend.h"
|
|
|
|
#include "Common/File/AndroidStorage.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Data/Text/Parsers.h"
|
|
#include "Common/Data/Encoding/Utf8.h"
|
|
#include "Common/Net/HTTPClient.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/UI/PopupScreens.h"
|
|
#include "Common/UI/View.h"
|
|
#include "Common/UI/ViewGroup.h"
|
|
#include "Common/UI/UI.h"
|
|
#include "Common/UI/IconCache.h"
|
|
#include "Common/UI/ScreenManager.h"
|
|
#include "Common/Render/Text/draw_text.h"
|
|
#include "Common/Profiler/Profiler.h"
|
|
|
|
#include "Common/Log/LogManager.h"
|
|
#include "Common/CPUDetect.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/GPU/ShaderWriter.h"
|
|
|
|
#include "Core/WebServer.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/ConfigValues.h"
|
|
#include "Core/System.h"
|
|
#include "Core/Reporting.h"
|
|
#include "Core/RetroAchievements.h"
|
|
#include "Core/CoreParameter.h"
|
|
#include "Core/HLE/sceKernel.h" // GPI/GPO
|
|
#include "Core/MIPS/MIPSTables.h"
|
|
#include "Core/MIPS/JitCommon/JitBlockCache.h"
|
|
#include "Core/MIPS/JitCommon/JitCommon.h"
|
|
#include "Core/MIPS/JitCommon/JitState.h"
|
|
#include "GPU/Debugger/Record.h"
|
|
#include "GPU/GPUCommon.h"
|
|
#include "GPU/GPUState.h"
|
|
|
|
// These two are for pipeline analysis, maybe should move elsewhere.
|
|
#include "GPU/Vulkan/PipelineManagerVulkan.h"
|
|
#include "GPU/Vulkan/GPU_Vulkan.h"
|
|
#include "UI/BaseScreens.h"
|
|
#include "UI/DevScreens.h"
|
|
#include "UI/MainScreen.h"
|
|
#include "UI/EmuScreen.h"
|
|
#include "UI/OnScreenDisplay.h"
|
|
#include "UI/ControlMappingScreen.h"
|
|
#include "UI/DeveloperToolsScreen.h"
|
|
#include "UI/JitCompareScreen.h"
|
|
#include "android/jni/app-android.h"
|
|
|
|
|
|
static const char *logLevelList[] = {
|
|
"Notice",
|
|
"Error",
|
|
"Warn",
|
|
"Info",
|
|
"Debug",
|
|
"Verb."
|
|
};
|
|
|
|
static const char *g_debugOverlayList[] = {
|
|
"Off",
|
|
"Debug stats",
|
|
"Draw Frametimes Graph",
|
|
"Frame timing",
|
|
#ifdef USE_PROFILER
|
|
"Frame profile",
|
|
#endif
|
|
"Control Debug",
|
|
"Audio Debug",
|
|
"GPU Profile",
|
|
"GPU Allocator Viewer",
|
|
"Framebuffer list",
|
|
};
|
|
|
|
void AddOverlayList(UI::ViewGroup *items, ScreenManager *screenManager) {
|
|
using namespace UI;
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
int numOverlays = ARRAY_SIZE(g_debugOverlayList);
|
|
if (!(g_Config.iGPUBackend == (int)GPUBackend::VULKAN || g_Config.iGPUBackend == (int)GPUBackend::OPENGL)) {
|
|
numOverlays -= 2; // skip the last 2.
|
|
}
|
|
items->Add(new PopupMultiChoice((int *)&g_Config.iDebugOverlay, dev->T("Debug overlay"), g_debugOverlayList, 0, numOverlays, I18NCat::DEVELOPER, screenManager));
|
|
}
|
|
|
|
void SaveFrameDump() {
|
|
if (!gpu) {
|
|
return;
|
|
}
|
|
gpu->GetRecorder()->RecordNextFrame([](const Path &dumpPath) {
|
|
NOTICE_LOG(Log::System, "Frame dump created at '%s'", dumpPath.c_str());
|
|
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
|
|
System_ShowFileInFolder(dumpPath);
|
|
} else {
|
|
g_OSD.Show(OSDType::MESSAGE_SUCCESS, GetFriendlyPath(dumpPath), 7.0f);
|
|
}
|
|
});
|
|
}
|
|
|
|
void DevMenuScreen::CreatePopupContents(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
|
|
|
ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
|
|
LinearLayout *items = new LinearLayout(ORIENT_VERTICAL);
|
|
|
|
items->SetSpacing(0.0f);
|
|
|
|
items->Add(new Choice(dev->T("Log View")))->OnClick.Add([this](UI::EventParams & e) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new LogViewScreen());
|
|
});
|
|
|
|
items->Add(new Choice(dev->T("Logging Channels")))->OnClick.Add([this](UI::EventParams & e) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new LogConfigScreen());
|
|
});
|
|
|
|
items->Add(new Choice(dev->T("Debugger")))->OnClick.Add([](UI::EventParams &e) {
|
|
g_Config.bShowImDebugger = !g_Config.bShowImDebugger;
|
|
});
|
|
|
|
if (WebServerRunning(WebServerFlags::DEBUGGER)) {
|
|
items->Add(new Choice(dev->T("Remote debugger")))->OnClick.Add([](UI::EventParams &e) {
|
|
int port = g_Config.iRemoteISOPort; // Also used for serving a local remote debugger.
|
|
if (g_Config.bRemoteDebuggerLocal) {
|
|
// TODO: Need to modify this URL to add /cpu when we upgrade to the latest version of the web debugger.
|
|
char uri[64];
|
|
snprintf(uri, sizeof(uri), "http://localhost:%d/debugger/", port);
|
|
System_LaunchUrl(LaunchUrlType::BROWSER_URL, uri);
|
|
} else {
|
|
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "http://ppsspp-debugger.unknownbrackets.org/cpu"); // NOTE: https doesn't work
|
|
}
|
|
});
|
|
}
|
|
|
|
items->Add(new Choice(sy->T("Developer Tools")))->OnClick.Add([this](UI::EventParams &e) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new DeveloperToolsScreen(gamePath_));
|
|
});
|
|
|
|
// Debug overlay
|
|
AddOverlayList(items, screenManager());
|
|
|
|
items->Add(new Choice(dev->T("Jit Compare")))->OnClick.Add([this](UI::EventParams &e) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new JitCompareScreen());
|
|
});
|
|
items->Add(new Choice(dev->T("Shader Viewer")))->OnClick.Add([this](UI::EventParams &e) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
if (gpu) // Avoid crashing if chosen while the game is being loaded.
|
|
screenManager()->push(new ShaderListScreen());
|
|
});
|
|
|
|
items->Add(new Choice(dev->T("Toggle Freeze")))->OnClick.Add([](UI::EventParams &e) {
|
|
// Freezing restores a savestate every frame, so it's not allowed in hardcore mode.
|
|
if (Achievements::WarnUserIfHardcoreModeActive(false)) {
|
|
return;
|
|
}
|
|
if (PSP_CoreParameter().frozen) {
|
|
PSP_CoreParameter().frozen = false;
|
|
} else {
|
|
PSP_CoreParameter().freezeNext = true;
|
|
}
|
|
});
|
|
|
|
items->Add(new Choice(dev->T("Reset limited logging")))->OnClick.Add([](UI::EventParams &e) {
|
|
Reporting::ResetCounts();
|
|
});
|
|
|
|
items->Add(new Choice(dev->T("GPI/GPO switches/LEDs")))->OnClick.Add([this, dev](UI::EventParams &e) {
|
|
screenManager()->push(new GPIGPOScreen(dev->T("GPI/GPO switches/LEDs")));
|
|
});
|
|
|
|
if (PSP_CoreParameter().fileType != IdentifiedFileType::PPSSPP_GE_DUMP) {
|
|
items->Add(new Choice(dev->T("Create frame dump")))->OnClick.Add([](UI::EventParams &e) {
|
|
SaveFrameDump();
|
|
});
|
|
}
|
|
|
|
// This one is not very useful these days, and only really on desktop. Hide it on other platforms.
|
|
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_DESKTOP) {
|
|
items->Add(new Choice(dev->T("Dump next frame to log")))->OnClick.Add([](UI::EventParams &e) {
|
|
gpu->DumpNextFrame();
|
|
});
|
|
}
|
|
|
|
items->Add(new Choice(dev->T("Compatibility flags")))->OnClick.Add([this](UI::EventParams &e) {
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
std::string activeFlags = PSP_CoreParameter().compat.GetActiveFlagsString();
|
|
std::string compatText = join(di->T("Enabled"), "\n");
|
|
compatText += activeFlags.empty() ? "None" : activeFlags;
|
|
compatText += "\n\n";
|
|
std::string ignored = g_Config.sIgnoreCompatSettings;
|
|
compatText += join(di->T("Ignored"), ":");
|
|
if (ignored.empty()) {
|
|
compatText += " None";
|
|
} else {
|
|
compatText += "\n" + ignored;
|
|
}
|
|
compatText += "\n\n";
|
|
compatText += "Loaded files: ";
|
|
for (const auto &file : PSP_CoreParameter().compat.filesLoaded()) {
|
|
compatText += "\n" + file;
|
|
}
|
|
screenManager()->push(new MessagePopupScreen(dev->T("Compatibility flags"), compatText, di->T("OK"), ""));
|
|
});
|
|
|
|
scroll->Add(items);
|
|
parent->Add(scroll);
|
|
|
|
g_logManager.EnableOutput(LogOutput::RingBuffer);
|
|
}
|
|
|
|
void DevMenuScreen::dialogFinished(const Screen *dialog, DialogResult result) {
|
|
UpdateUIState(UISTATE_INGAME);
|
|
// Close when a subscreen got closed.
|
|
// TODO: a bug in screenmanager causes this not to work here.
|
|
// TriggerFinish(DR_OK);
|
|
}
|
|
|
|
void GPIGPOScreen::CreatePopupContents(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
parent->Add(new CheckBox(&g_Config.bShowGPOLEDs, dev->T("Show GPO LEDs")));
|
|
for (int i = 0; i < 8; i++) {
|
|
std::string name = ApplySafeSubstitutions(dev->T("GPI switch %1"), i);
|
|
parent->Add(new BitCheckBox(&g_GPIBits, 1 << i, name));
|
|
}
|
|
}
|
|
|
|
void LogViewScreen::UpdateLog() {
|
|
using namespace UI;
|
|
const RingbufferLog &ring = g_logManager.GetRingbuffer();
|
|
vert_->Clear();
|
|
|
|
// TODO: Direct rendering without TextViews.
|
|
for (int i = ring.GetCount() - 1; i >= 0; i--) {
|
|
TextView *v = vert_->Add(new TextView(StripSpaces(ring.TextAt(i)), FLAG_DYNAMIC_ASCII, true));
|
|
uint32_t color = LogManager::GetLevelColor(ring.LevelAt(i));
|
|
v->SetTextColor(0xFF000000 | color);
|
|
}
|
|
toBottom_ = true;
|
|
}
|
|
|
|
void LogViewScreen::update() {
|
|
UIBaseDialogScreen::update();
|
|
if (toBottom_) {
|
|
toBottom_ = false;
|
|
scroll_->ScrollToBottom();
|
|
}
|
|
}
|
|
|
|
void LogViewScreen::CreateViews() {
|
|
using namespace UI;
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
|
|
LinearLayout *outer = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
root_ = outer;
|
|
|
|
scroll_ = outer->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0)));
|
|
LinearLayout *bottom = outer->Add(new LinearLayout(ORIENT_HORIZONTAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
bottom->Add(new Button(di->T("Back")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
|
|
|
|
vert_ = scroll_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
vert_->SetSpacing(0);
|
|
|
|
UpdateLog();
|
|
}
|
|
|
|
std::string_view LogConfigScreen::GetTitle() const {
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
return dev->T("Logging Channels");
|
|
}
|
|
|
|
void LogConfigScreen::CreateSettingsViews(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
parent->Add(new Choice(di->T("Toggle All")))->OnClick.Handle(this, &LogConfigScreen::OnToggleAll);
|
|
parent->Add(new Choice(di->T("Enable All")))->OnClick.Handle(this, &LogConfigScreen::OnEnableAll);
|
|
parent->Add(new Choice(di->T("Disable All")))->OnClick.Handle(this, &LogConfigScreen::OnDisableAll);
|
|
parent->Add(new Choice(dev->T("Log Level")))->OnClick.Handle(this, &LogConfigScreen::OnLogLevel);
|
|
}
|
|
|
|
void LogConfigScreen::CreateContentViews(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
int cellSize = 350;
|
|
|
|
UI::GridLayoutSettings gridsettings(cellSize, 64, 5);
|
|
gridsettings.fillCells = true;
|
|
GridLayout *grid = parent->Add(new GridLayoutList(gridsettings, new LayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
|
|
for (int i = 0; i < LogManager::GetNumChannels(); i++) {
|
|
Log type = (Log)i;
|
|
LogChannel *chan = g_logManager.GetLogChannel(type);
|
|
LinearLayout *row = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(cellSize - 50, WRAP_CONTENT));
|
|
row->SetSpacing(0);
|
|
row->Add(new CheckBox(&chan->enabled, "", "", new LinearLayoutParams(50, WRAP_CONTENT)));
|
|
row->Add(new PopupMultiChoice((int *)&chan->level, LogManager::GetLogTypeName(type), logLevelList, 1, 6, I18NCat::NONE, screenManager(), new LinearLayoutParams(1.0)));
|
|
grid->Add(row);
|
|
}
|
|
}
|
|
|
|
void LogConfigScreen::OnToggleAll(UI::EventParams &e) {
|
|
for (int i = 0; i < LogManager::GetNumChannels(); i++) {
|
|
LogChannel *chan = g_logManager.GetLogChannel((Log)i);
|
|
chan->enabled = !chan->enabled;
|
|
}
|
|
}
|
|
|
|
void LogConfigScreen::OnEnableAll(UI::EventParams &e) {
|
|
for (int i = 0; i < LogManager::GetNumChannels(); i++) {
|
|
LogChannel *chan = g_logManager.GetLogChannel((Log)i);
|
|
chan->enabled = true;
|
|
}
|
|
}
|
|
|
|
void LogConfigScreen::OnDisableAll(UI::EventParams &e) {
|
|
for (int i = 0; i < LogManager::GetNumChannels(); i++) {
|
|
LogChannel *chan = g_logManager.GetLogChannel((Log)i);
|
|
chan->enabled = false;
|
|
}
|
|
}
|
|
|
|
void LogConfigScreen::OnLogLevelChange(UI::EventParams &e) {
|
|
RecreateViews();
|
|
}
|
|
|
|
void LogConfigScreen::OnLogLevel(UI::EventParams &e) {
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
auto logLevelScreen = new LogLevelScreen(dev->T("Log Level"));
|
|
logLevelScreen->OnChoice.Handle(this, &LogConfigScreen::OnLogLevelChange);
|
|
if (e.v)
|
|
logLevelScreen->SetPopupOrigin(e.v);
|
|
screenManager()->push(logLevelScreen);
|
|
}
|
|
|
|
LogLevelScreen::LogLevelScreen(std::string_view title) : ListPopupScreen(title) {
|
|
int NUMLOGLEVEL = 6;
|
|
std::vector<std::string> list;
|
|
for (int i = 0; i < NUMLOGLEVEL; ++i) {
|
|
list.push_back(logLevelList[i]);
|
|
}
|
|
adaptor_ = UI::StringVectorListAdaptor(list, -1);
|
|
|
|
// CreateViews takes care of, well, that.
|
|
}
|
|
|
|
void LogLevelScreen::OnCompleted(DialogResult result) {
|
|
if (result != DR_OK)
|
|
return;
|
|
int selected = listView_->GetSelected();
|
|
|
|
for (int i = 0; i < LogManager::GetNumChannels(); ++i) {
|
|
Log type = (Log)i;
|
|
LogChannel *chan = g_logManager.GetLogChannel(type);
|
|
if (chan->enabled)
|
|
chan->level = (LogLevel)(selected + 1);
|
|
}
|
|
}
|
|
|
|
struct JitDisableFlag {
|
|
MIPSComp::JitDisable flag;
|
|
const char *name;
|
|
};
|
|
|
|
// Please do not try to translate these :)
|
|
static const JitDisableFlag jitDisableFlags[] = {
|
|
{ MIPSComp::JitDisable::ALU, "ALU" },
|
|
{ MIPSComp::JitDisable::ALU_IMM, "ALU_IMM" },
|
|
{ MIPSComp::JitDisable::ALU_BIT, "ALU_BIT" },
|
|
{ MIPSComp::JitDisable::MULDIV, "MULDIV" },
|
|
{ MIPSComp::JitDisable::FPU, "FPU" },
|
|
{ MIPSComp::JitDisable::FPU_COMP, "FPU_COMP" },
|
|
{ MIPSComp::JitDisable::FPU_XFER, "FPU_XFER" },
|
|
{ MIPSComp::JitDisable::VFPU_VEC, "VFPU_VEC" },
|
|
{ MIPSComp::JitDisable::VFPU_MTX_VTFM, "VFPU_MTX_VTFM" },
|
|
{ MIPSComp::JitDisable::VFPU_MTX_VMSCL, "VFPU_MTX_VMSCL" },
|
|
{ MIPSComp::JitDisable::VFPU_MTX_VMMUL, "VFPU_MTX_VMMUL" },
|
|
{ MIPSComp::JitDisable::VFPU_MTX_VMMOV, "VFPU_MTX_VMMOV" },
|
|
{ MIPSComp::JitDisable::VFPU_COMP, "VFPU_COMP" },
|
|
{ MIPSComp::JitDisable::VFPU_XFER, "VFPU_XFER" },
|
|
{ MIPSComp::JitDisable::LSU, "LSU" },
|
|
{ MIPSComp::JitDisable::LSU_UNALIGNED, "LSU_UNALIGNED" },
|
|
{ MIPSComp::JitDisable::LSU_FPU, "LSU_FPU" },
|
|
{ MIPSComp::JitDisable::LSU_VFPU, "LSU_VFPU" },
|
|
{ MIPSComp::JitDisable::SIMD, "SIMD" },
|
|
{ MIPSComp::JitDisable::BLOCKLINK, "Block Linking" },
|
|
{ MIPSComp::JitDisable::POINTERIFY, "Pointerify" },
|
|
{ MIPSComp::JitDisable::STATIC_ALLOC, "Static regalloc" },
|
|
{ MIPSComp::JitDisable::CACHE_POINTERS, "Cached pointers" },
|
|
{ MIPSComp::JitDisable::REGALLOC_GPR, "GPR Regalloc across instructions" },
|
|
{ MIPSComp::JitDisable::REGALLOC_FPR, "FPR Regalloc across instructions" },
|
|
};
|
|
|
|
void JitDebugScreen::CreateViews() {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
root_ = new ScrollView(ORIENT_VERTICAL);
|
|
|
|
LinearLayout *vert = root_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
vert->SetSpacing(0);
|
|
|
|
LinearLayout *topbar = new LinearLayout(ORIENT_HORIZONTAL);
|
|
topbar->Add(new Choice(ImageID("I_NAVIGATE_BACK")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
|
|
topbar->Add(new Choice(di->T("Disable All")))->OnClick.Handle(this, &JitDebugScreen::OnDisableAll);
|
|
topbar->Add(new Choice(di->T("Enable All")))->OnClick.Handle(this, &JitDebugScreen::OnEnableAll);
|
|
|
|
vert->Add(topbar);
|
|
vert->Add(new ItemHeader(dev->T("Disabled JIT functionality")));
|
|
|
|
for (auto flag : jitDisableFlags) {
|
|
// Do not add translation of these.
|
|
vert->Add(new BitCheckBox(&g_Config.uJitDisableFlags, (uint32_t)flag.flag, flag.name));
|
|
}
|
|
}
|
|
|
|
void JitDebugScreen::OnEnableAll(UI::EventParams &e) {
|
|
g_Config.uJitDisableFlags &= ~(uint32_t)MIPSComp::JitDisable::ALL_FLAGS;
|
|
}
|
|
|
|
void JitDebugScreen::OnDisableAll(UI::EventParams &e) {
|
|
g_Config.uJitDisableFlags |= (uint32_t)MIPSComp::JitDisable::ALL_FLAGS;
|
|
}
|
|
|
|
int ShaderListScreen::ListShaders(DebugShaderType shaderType, UI::LinearLayout *view) {
|
|
using namespace UI;
|
|
std::vector<std::string> shaderIds_ = gpu->DebugGetShaderIDs(shaderType);
|
|
int count = 0;
|
|
for (const auto &id : shaderIds_) {
|
|
Choice *choice = view->Add(new Choice(gpu->DebugGetShaderString(id, shaderType, SHADER_STRING_SHORT_DESC)));
|
|
choice->SetTag(id);
|
|
choice->SetDrawTextFlags(FLAG_DYNAMIC_ASCII);
|
|
choice->OnClick.Handle(this, &ShaderListScreen::OnShaderClick);
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static constexpr struct { DebugShaderType type; const char *name; } shaderTypes[] = {
|
|
{ SHADER_TYPE_VERTEX, "Vertex" },
|
|
{ SHADER_TYPE_FRAGMENT, "Fragment" },
|
|
{ SHADER_TYPE_VERTEXLOADER, "VertexLoader" },
|
|
{ SHADER_TYPE_PIPELINE, "Pipeline" },
|
|
{ SHADER_TYPE_TEXTURE, "Texture" },
|
|
{ SHADER_TYPE_SAMPLER, "Sampler" },
|
|
};
|
|
|
|
void ShaderListScreen::CreateTabs() {
|
|
using namespace UI;
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(shaderTypes); i++) {
|
|
int count = (int)gpu->DebugGetShaderIDs(shaderTypes[i].type).size();
|
|
AddTab(shaderTypes[i].name, StringFromFormat("%s (%d)", shaderTypes[i].name, count), [this, i](UI::LinearLayout *tabContent) {
|
|
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN && shaderTypes[i].type == SHADER_TYPE_PIPELINE) {
|
|
CollapsibleSection *pipelineAnalysisSection = tabContent->Add(new CollapsibleSection("Pipeline Analysis"));
|
|
AddPipelineAnalysis(pipelineAnalysisSection);
|
|
pipelineAnalysisSection->SetOpen(false);
|
|
}
|
|
|
|
LinearLayout *shaderList = new LinearLayoutList(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
int count = ListShaders(shaderTypes[i].type, shaderList);
|
|
tabContent->Add(shaderList);
|
|
});
|
|
}
|
|
}
|
|
|
|
const bool IsSameExceptForShaders(const VulkanPipelineKey &a, const VulkanPipelineKey &b) {
|
|
return a.useHWTransform == b.useHWTransform && a.raster == b.raster && a.vtxFmtId == b.vtxFmtId;
|
|
}
|
|
|
|
const bool IsSameExceptForVertexFormat(const VulkanPipelineKey &a, const VulkanPipelineKey &b) {
|
|
return a.useHWTransform == b.useHWTransform && a.raster == b.raster && a.vid == b.vid && a.fid == b.fid;
|
|
}
|
|
|
|
void ShaderListScreen::AddPipelineAnalysis(UI::LinearLayout *tabContent) {
|
|
#if !PPSSPP_PLATFORM(UWP)
|
|
GPU_Vulkan *vgpu = dynamic_cast<GPU_Vulkan *>(gpu);
|
|
const PipelineManagerVulkan *pipelineManager = vgpu->GetPipelineManager();
|
|
// Collect all pipeline keys in a vector.
|
|
std::vector<VulkanPipelineKey> keys;
|
|
pipelineManager->GetPipelines().Iterate([&keys](const VulkanPipelineKey &key, VulkanPipeline *pipeline) {
|
|
keys.push_back(key);
|
|
});
|
|
|
|
using namespace UI;
|
|
|
|
// Now, see if there are two pipelines that are identical except for the shader IDs. If so, we may be able to
|
|
// merge them by making the shaders a bit more "uber".
|
|
for (size_t i = 0; i < keys.size(); i++) {
|
|
std::string toBeMerged;
|
|
for (size_t j = i + 1; j < keys.size(); j++) {
|
|
const VulkanPipelineKey &keyA = keys[i];
|
|
const VulkanPipelineKey &keyB = keys[j];
|
|
if (IsSameExceptForShaders(keyA, keyB)) {
|
|
// Found a pair of pipelines that are identical except for the shaders.
|
|
if (keyA.vid != keyB.vid) {
|
|
toBeMerged += "VShader: " + keyA.vid.Description(false) + " vs " + keyB.vid.Description(false) + "\n";
|
|
}
|
|
if (keyA.fid != keyB.fid) {
|
|
toBeMerged += "FShader: " + keyA.fid.Description(false) + " vs " + keyB.fid.Description(false) + "\n";
|
|
}
|
|
}
|
|
if (IsSameExceptForVertexFormat(keyA, keyB)) {
|
|
if (keyA.vtxFmtId != 0 && keyB.vtxFmtId != 0 && keyA.vtxFmtId != keyB.vtxFmtId) {
|
|
DecVtxFormat fmtA;
|
|
DecVtxFormat fmtB;
|
|
fmtA.InitializeFromID(keyA.vtxFmtId);
|
|
fmtB.InitializeFromID(keyB.vtxFmtId);
|
|
toBeMerged += "VertexFmt: " + fmtA.ToString() + " vs " + fmtB.ToString() + "\n";
|
|
}
|
|
}
|
|
}
|
|
if (!toBeMerged.empty()) {
|
|
tabContent->Add(new UI::TextView(keys[i].GetDescription(SHADER_STRING_SHORT_DESC), FLAG_DYNAMIC_ASCII, true));
|
|
tabContent->Add(new UI::TextView(toBeMerged, FLAG_DYNAMIC_ASCII, true));
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ShaderListScreen::OnShaderClick(UI::EventParams &e) {
|
|
using namespace UI;
|
|
std::string id = e.v->Tag();
|
|
DebugShaderType type = shaderTypes[GetCurrentTab()].type;
|
|
screenManager()->push(new ShaderViewScreen(id, type));
|
|
}
|
|
|
|
void ShaderViewScreen::CreateViews() {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
LinearLayout *layout = new LinearLayout(ORIENT_VERTICAL);
|
|
root_ = layout;
|
|
|
|
LinearLayout *topbar = new LinearLayout(ORIENT_HORIZONTAL);
|
|
topbar->Add(new Choice(ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams()))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
|
|
topbar->Add(new Choice(ImageID("I_FILE_COPY"), new LinearLayoutParams()))->OnClick.Add([this](UI::EventParams &e) {
|
|
System_CopyStringToClipboard(gpu->DebugGetShaderString(id_, type_, SHADER_STRING_SHORT_DESC));
|
|
});
|
|
topbar->Add(new Choice(dev->T("Copy source code"), ImageID("I_FILE_COPY"), new LinearLayoutParams()))->OnClick.Add([this](UI::EventParams &e) {
|
|
System_CopyStringToClipboard(gpu->DebugGetShaderString(id_, type_, SHADER_STRING_SOURCE_CODE));
|
|
});
|
|
topbar->Add(new TextView(gpu->DebugGetShaderString(id_, type_, SHADER_STRING_SHORT_DESC), FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT, false));
|
|
layout->Add(topbar);
|
|
|
|
ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0));
|
|
scroll->SetTag("DevShaderView");
|
|
layout->Add(scroll);
|
|
|
|
LinearLayout *lineLayout = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
lineLayout->SetSpacing(0.0);
|
|
scroll->Add(lineLayout);
|
|
|
|
std::vector<std::string> lines;
|
|
SplitString(gpu->DebugGetShaderString(id_, type_, SHADER_STRING_SOURCE_CODE), '\n', lines);
|
|
|
|
for (const auto &line : lines) {
|
|
lineLayout->Add(new TextView(line, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT, true));
|
|
}
|
|
}
|
|
|
|
bool ShaderViewScreen::key(const KeyInput &ki) {
|
|
if (ki.flags & KeyInputFlags::CHAR) {
|
|
if (ki.unicodeChar == 'C' || ki.unicodeChar == 'c') {
|
|
System_CopyStringToClipboard(gpu->DebugGetShaderString(id_, type_, SHADER_STRING_SHORT_DESC));
|
|
}
|
|
}
|
|
return UIBaseDialogScreen::key(ki);
|
|
}
|
|
|
|
|
|
const std::string framedumpsBaseUrl = "http://framedump.ppsspp.org/repro/";
|
|
|
|
FrameDumpTestScreen::~FrameDumpTestScreen() {
|
|
if (listing_) {
|
|
listing_->Cancel();
|
|
}
|
|
if (dumpDownload_) {
|
|
dumpDownload_->Cancel();
|
|
}
|
|
}
|
|
|
|
void FrameDumpTestScreen::CreateTabs() {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
AddTab("General", dev->T("Dumps"), [this](UI::LinearLayout *parent) {
|
|
parent->Add(new ItemHeader("GE Frame Dumps"));
|
|
|
|
for (auto &file : files_) {
|
|
std::string url = framedumpsBaseUrl + file;
|
|
Choice *c = parent->Add(new Choice(file));
|
|
c->SetTag(url);
|
|
c->OnClick.Add([this, url = Path(url)](UI::EventParams &e) {
|
|
INFO_LOG(Log::Common, "Trying to launch '%s'", url.c_str());
|
|
// Our disc streaming functionality detects the URL and takes over and handles loading framedumps well,
|
|
// except for some reason the game ID.
|
|
// TODO: Fix that since it can be important for compat settings.
|
|
screenManager()->switchScreen(new EmuScreen(url));
|
|
});
|
|
}
|
|
});
|
|
|
|
EnsureTabs();
|
|
}
|
|
|
|
void FrameDumpTestScreen::update() {
|
|
UIScreen::update();
|
|
|
|
if (!listing_) {
|
|
const char *acceptMime = "text/html, */*; q=0.8";
|
|
listing_ = g_DownloadManager.StartDownload(framedumpsBaseUrl, Path(), http::RequestFlags::ProgressBar | http::RequestFlags::ProgressBarDelayed, acceptMime);
|
|
}
|
|
|
|
if (listing_ && listing_->Done() && files_.empty()) {
|
|
if (listing_->ResultCode() == 200) {
|
|
std::string listingHtml;
|
|
listing_->buffer().TakeAll(&listingHtml);
|
|
|
|
std::vector<std::string> lines;
|
|
// We rely slightly on nginx listing format here. Not great.
|
|
SplitString(listingHtml, '\n', lines);
|
|
for (auto &line : lines) {
|
|
std::string trimmed(StripSpaces(line));
|
|
if (startsWith(trimmed, "<a href=\"")) {
|
|
trimmed = trimmed.substr(strlen("<a href=\""));
|
|
size_t offset = trimmed.find('\"');
|
|
if (offset != std::string::npos) {
|
|
trimmed = trimmed.substr(0, offset);
|
|
if (endsWith(trimmed, ".ppdmp")) {
|
|
INFO_LOG(Log::Common, "Found ppdmp: '%s'", trimmed.c_str());
|
|
files_.push_back(trimmed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// something went bad. Too lazy to make UI, so let's just finish this screen.
|
|
TriggerFinish(DialogResult::DR_CANCEL);
|
|
}
|
|
RecreateViews();
|
|
}
|
|
}
|
|
|
|
bool TouchTestScreen::touch(const TouchInput &touch) {
|
|
UIBaseDialogScreen::touch(touch);
|
|
if (touch.flags & TouchInputFlags::DOWN) {
|
|
bool found = false;
|
|
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
|
|
if (touches_[i].id == touch.id) {
|
|
WARN_LOG(Log::System, "Double touch");
|
|
touches_[i].x = touch.x;
|
|
touches_[i].y = touch.y;
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) {
|
|
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
|
|
if (touches_[i].id == -1) {
|
|
touches_[i].id = touch.id;
|
|
touches_[i].x = touch.x;
|
|
touches_[i].y = touch.y;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (touch.flags & TouchInputFlags::MOVE) {
|
|
bool found = false;
|
|
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
|
|
if (touches_[i].id == touch.id) {
|
|
touches_[i].x = touch.x;
|
|
touches_[i].y = touch.y;
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found && touch.buttons) {
|
|
WARN_LOG(Log::System, "Move with buttons %d without touch down: %d", touch.buttons, touch.id);
|
|
}
|
|
}
|
|
if (touch.flags & TouchInputFlags::UP) {
|
|
bool found = false;
|
|
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
|
|
if (touches_[i].id == touch.id) {
|
|
found = true;
|
|
touches_[i].id = -1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
WARN_LOG(Log::System, "Touch release without touch down");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// TODO: Move this screen out into its own file.
|
|
void TouchTestScreen::CreateViews() {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
|
|
root_ = new LinearLayout(ORIENT_VERTICAL);
|
|
LinearLayout *theTwo = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f));
|
|
|
|
// TODO: This one should use DYNAMIC_ASCII. Though doesn't matter much.
|
|
lastKeyEvents_ = theTwo->Add(new TextView("-", new LayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
|
|
root_->Add(theTwo);
|
|
|
|
#if !PPSSPP_PLATFORM(UWP)
|
|
static const char *renderingBackend[] = { "OpenGL", "(n/a)", "Direct3D 11", "Vulkan" };
|
|
PopupMultiChoice *renderingBackendChoice = root_->Add(new PopupMultiChoice(&g_Config.iGPUBackend, gr->T("Backend"), renderingBackend, (int)GPUBackend::OPENGL, ARRAY_SIZE(renderingBackend), I18NCat::GRAPHICS, screenManager()));
|
|
renderingBackendChoice->OnChoice.Handle(this, &TouchTestScreen::OnRenderingBackend);
|
|
|
|
if (!g_Config.IsBackendEnabled(GPUBackend::OPENGL))
|
|
renderingBackendChoice->HideChoice((int)GPUBackend::OPENGL);
|
|
renderingBackendChoice->HideChoice(1); // previously D3D9
|
|
if (!g_Config.IsBackendEnabled(GPUBackend::DIRECT3D11))
|
|
renderingBackendChoice->HideChoice((int)GPUBackend::DIRECT3D11);
|
|
if (!g_Config.IsBackendEnabled(GPUBackend::VULKAN))
|
|
renderingBackendChoice->HideChoice((int)GPUBackend::VULKAN);
|
|
#endif
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
root_->Add(new Choice(gr->T("Recreate Activity")))->OnClick.Handle(this, &TouchTestScreen::OnRecreateActivity);
|
|
#endif
|
|
root_->Add(new Button(di->T("Back"), new LinearLayoutParams(FILL_PARENT, 64, Margins(10, 0))))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
|
|
}
|
|
|
|
void TouchTestScreen::UpdateLogView() {
|
|
while (keyEventLog_.size() > 8) {
|
|
keyEventLog_.erase(keyEventLog_.begin());
|
|
}
|
|
|
|
std::string text;
|
|
for (auto &iter : keyEventLog_) {
|
|
text += iter + "\n";
|
|
}
|
|
|
|
if (lastKeyEvents_) {
|
|
lastKeyEvents_->SetText(text);
|
|
}
|
|
}
|
|
|
|
bool TouchTestScreen::key(const KeyInput &key) {
|
|
UIScreen::key(key);
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "%s (%d) Device ID: %d [%s%s%s%s%s%s%s%s]", KeyMap::GetKeyName(key.keyCode).c_str(), key.keyCode, key.deviceId,
|
|
(key.flags & KeyInputFlags::IS_REPEAT) ? "REP " : "",
|
|
(key.flags & KeyInputFlags::UP) ? "UP " : "",
|
|
(key.flags & KeyInputFlags::DOWN) ? "DOWN " : "",
|
|
(key.flags & KeyInputFlags::CHAR) ? "CHAR " : "",
|
|
(key.flags & KeyInputFlags::ModCtrl) ? "CTRL " : "",
|
|
(key.flags & KeyInputFlags::ModShift) ? "SHIFT " : "",
|
|
(key.flags & KeyInputFlags::ModAlt) ? "ALT " : "",
|
|
(key.flags & KeyInputFlags::ModMeta) ? "META " : "");
|
|
keyEventLog_.push_back(buf);
|
|
UpdateLogView();
|
|
return true;
|
|
}
|
|
|
|
void TouchTestScreen::axis(const AxisInput &axis) {
|
|
if (axis.deviceId == DEVICE_ID_MOUSE && (axis.axisId == JOYSTICK_AXIS_MOUSE_REL_X || axis.axisId == JOYSTICK_AXIS_MOUSE_REL_Y)) {
|
|
// These spam a lot, don't log for now.
|
|
return;
|
|
}
|
|
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "Axis: %s (%d) (value %1.3f) Device ID: %d",
|
|
KeyMap::GetAxisName(axis.axisId).c_str(), axis.axisId, axis.value, axis.deviceId);
|
|
|
|
keyEventLog_.push_back(buf);
|
|
if (keyEventLog_.size() > 8) {
|
|
keyEventLog_.erase(keyEventLog_.begin());
|
|
}
|
|
UpdateLogView();
|
|
}
|
|
|
|
#undef DrawText
|
|
|
|
void TouchTestScreen::DrawForeground(UIContext &dc) {
|
|
Bounds bounds = GetLayoutBounds(dc);
|
|
|
|
double now = dc.FrameStartTime();
|
|
double delta = now - lastFrameTime_;
|
|
lastFrameTime_ = now;
|
|
|
|
dc.BeginNoTex();
|
|
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
|
|
if (touches_[i].id != -1) {
|
|
dc.Draw()->Circle(touches_[i].x, touches_[i].y, 100.0, 3.0, 80, 0.0f, 0xFFFFFFFF, 1.0);
|
|
}
|
|
}
|
|
dc.Flush();
|
|
|
|
dc.Begin();
|
|
|
|
char buffer[4096];
|
|
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
|
|
if (touches_[i].id != -1) {
|
|
dc.Draw()->Circle(touches_[i].x, touches_[i].y, 100.0, 3.0, 80, 0.0f, 0xFFFFFFFF, 1.0);
|
|
snprintf(buffer, sizeof(buffer), "%0.1fx%0.1f", touches_[i].x, touches_[i].y);
|
|
dc.DrawText(buffer, touches_[i].x, touches_[i].y + (touches_[i].y > g_display.dp_yres - 100.0f ? -135.0f : 95.0f), 0xFFFFFFFF, ALIGN_HCENTER | FLAG_DYNAMIC_ASCII);
|
|
}
|
|
}
|
|
|
|
char extra_debug[2048]{};
|
|
truncate_cpy(extra_debug, Android_GetInputDeviceDebugString());
|
|
|
|
snprintf(buffer, sizeof(buffer),
|
|
"display_res: %dx%d\n"
|
|
"dp_res: %dx%d pixel_res: %dx%d\n"
|
|
"dpi_scale: %0.3fx%0.3f\n"
|
|
"dpi_scale_real: %0.3fx%0.3f\n"
|
|
"delta: %0.2f ms fps: %0.3f\n%s",
|
|
(int)System_GetPropertyInt(SYSPROP_DISPLAY_XRES), (int)System_GetPropertyInt(SYSPROP_DISPLAY_YRES),
|
|
g_display.dp_xres, g_display.dp_yres, g_display.pixel_xres, g_display.pixel_yres,
|
|
g_display.dpi_scale_x, g_display.dpi_scale_y,
|
|
g_display.dpi_scale_real_x, g_display.dpi_scale_real_y,
|
|
delta * 1000.0, 1.0 / delta,
|
|
extra_debug);
|
|
|
|
// On Android, also add joystick debug data.
|
|
dc.DrawTextShadow(buffer, bounds.centerX(), bounds.y + 20.0f, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII);
|
|
dc.Flush();
|
|
}
|
|
|
|
void RecreateActivity() {
|
|
const int SYSTEM_JELLYBEAN = 16;
|
|
if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= SYSTEM_JELLYBEAN) {
|
|
INFO_LOG(Log::System, "Sending recreate");
|
|
System_Notify(SystemNotification::FORCE_RECREATE_ACTIVITY);
|
|
INFO_LOG(Log::System, "Got back from recreate");
|
|
} else {
|
|
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
|
|
System_Toast(gr->T_cstr("Must Restart", "You must restart PPSSPP for this change to take effect"));
|
|
}
|
|
}
|
|
|
|
void TouchTestScreen::OnImmersiveModeChange(UI::EventParams &e) {
|
|
System_Notify(SystemNotification::IMMERSIVE_MODE_CHANGE);
|
|
if (g_Config.iAndroidHwScale != 0) {
|
|
RecreateActivity();
|
|
}
|
|
}
|
|
|
|
void TouchTestScreen::OnRenderingBackend(UI::EventParams &e) {
|
|
g_Config.Save("GameSettingsScreen::RenderingBackend");
|
|
System_RestartApp("--start-screen=touchscreentest");
|
|
}
|
|
|
|
void TouchTestScreen::OnRecreateActivity(UI::EventParams &e) {
|
|
RecreateActivity();
|
|
}
|