mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-05-29 00:21:34 +08:00
2082 lines
67 KiB
C++
2082 lines
67 KiB
C++
// Copyright (c) 2012- 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 "ppsspp_config.h"
|
|
|
|
#include <functional>
|
|
|
|
using namespace std::placeholders;
|
|
|
|
#include "Common/Render/TextureAtlas.h"
|
|
#include "Common/GPU/OpenGL/GLFeatures.h"
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/File/VFS/VFS.h"
|
|
#include "Common/Log/LogManager.h"
|
|
#include "Common/UI/Root.h"
|
|
#include "Common/UI/UI.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/UI/Tween.h"
|
|
#include "Common/UI/View.h"
|
|
#include "Common/UI/AsyncImageFileView.h"
|
|
#include "Common/VR/PPSSPPVR.h"
|
|
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Input/InputState.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/System/Request.h"
|
|
#include "Common/System/OSD.h"
|
|
#include "Common/Profiler/Profiler.h"
|
|
#include "Common/Math/curves.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/TimeUtil.h"
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
#include "Core/AVIDump.h"
|
|
#endif
|
|
#include "Core/Config.h"
|
|
#include "Core/ConfigValues.h"
|
|
#include "Core/CoreTiming.h"
|
|
#include "Core/CoreParameter.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/KeyMap.h"
|
|
#include "Core/MemFault.h"
|
|
#include "Core/Reporting.h"
|
|
#include "Core/Util/PathUtil.h"
|
|
#include "Core/System.h"
|
|
#include "GPU/Common/PresentationCommon.h"
|
|
#include "GPU/GPUState.h"
|
|
#include "GPU/GPUCommon.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
#include "Core/HLE/sceCtrl.h"
|
|
#include "Core/HLE/sceSas.h"
|
|
#include "Core/HLE/sceNet.h"
|
|
#include "Core/HLE/sceDisplay.h"
|
|
#include "Core/HLE/sceNetAdhoc.h"
|
|
#include "Core/Debugger/SymbolMap.h"
|
|
#include "Core/RetroAchievements.h"
|
|
#include "Core/SaveState.h"
|
|
#include "Core/Screenshot.h"
|
|
#include "UI/ImDebugger/ImDebugger.h"
|
|
#include "Core/HLE/__sceAudio.h"
|
|
// #include "Core/HLE/proAdhoc.h"
|
|
#include "Core/HW/Display.h"
|
|
|
|
#include "UI/BackgroundAudio.h"
|
|
#include "UI/GamepadEmu.h"
|
|
#include "UI/PauseScreen.h"
|
|
#include "UI/MainScreen.h"
|
|
#include "UI/Background.h"
|
|
#include "UI/EmuScreen.h"
|
|
#include "UI/DevScreens.h"
|
|
#include "UI/GameInfoCache.h"
|
|
#include "UI/BaseScreens.h"
|
|
#include "UI/ControlMappingScreen.h"
|
|
#include "UI/DisplayLayoutScreen.h"
|
|
#include "UI/GameSettingsScreen.h"
|
|
#include "UI/MiscViews.h"
|
|
#include "UI/ProfilerDraw.h"
|
|
#include "UI/DiscordIntegration.h"
|
|
#include "UI/ChatScreen.h"
|
|
#include "UI/DebugOverlay.h"
|
|
|
|
#include "ext/imgui/imgui.h"
|
|
#include "ext/imgui/imgui_internal.h"
|
|
#include "ext/imgui/imgui_impl_thin3d.h"
|
|
#include "ext/imgui/imgui_impl_platform.h"
|
|
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
|
|
#include "Windows/MainWindow.h"
|
|
#endif
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
static AVIDump avi;
|
|
#endif
|
|
|
|
extern bool g_TakeScreenshot;
|
|
|
|
static void AssertCancelCallback(const char *message, void *userdata) {
|
|
NOTICE_LOG(Log::CPU, "Broke after assert: %s", message);
|
|
Core_Break(BreakReason::AssertChoice);
|
|
g_Config.bShowImDebugger = true;
|
|
|
|
EmuScreen *emuScreen = (EmuScreen *)userdata;
|
|
emuScreen->SendImDebuggerCommand(ImCommand{ ImCmd::SHOW_IN_CPU_DISASM, currentMIPS->pc });
|
|
}
|
|
|
|
// Handles control rotation due to internal screen rotation.
|
|
void EmuScreen::UpdatePSPButtons(uint32_t bitsToSet, uint32_t bitsToClear) {
|
|
if (!IsOnTop()) {
|
|
// Auto-release inputs
|
|
bitsToSet = 0;
|
|
}
|
|
__CtrlUpdateButtons(bitsToSet, bitsToClear);
|
|
}
|
|
|
|
void EmuScreen::SetPSPAnalog(int iInternalScreenRotation, int stick, float x, float y) {
|
|
if (!IsOnTop()) {
|
|
x = 0.0f;
|
|
y = 0.0f;
|
|
}
|
|
|
|
switch (iInternalScreenRotation) {
|
|
case ROTATION_LOCKED_HORIZONTAL:
|
|
// Standard rotation. No change.
|
|
break;
|
|
case ROTATION_LOCKED_HORIZONTAL180:
|
|
x = -x;
|
|
y = -y;
|
|
break;
|
|
case ROTATION_LOCKED_VERTICAL:
|
|
{
|
|
float new_y = y;
|
|
x = -y;
|
|
y = new_y;
|
|
break;
|
|
}
|
|
case ROTATION_LOCKED_VERTICAL180:
|
|
{
|
|
float new_y = -x;
|
|
x = y;
|
|
y = new_y;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
__CtrlSetAnalogXY(stick, x, y);
|
|
}
|
|
|
|
EmuScreen::EmuScreen(const Path &filename)
|
|
: gamePath_(filename) {
|
|
saveStateSlot_ = SaveState::GetCurrentSlot();
|
|
g_controlMapper.AddListener(this);
|
|
|
|
_dbg_assert_(coreState == CORE_POWERDOWN);
|
|
|
|
OnDevMenu.Handle(this, &EmuScreen::OnDevTools);
|
|
|
|
// Usually, we don't want focus movement enabled on this screen, so disable on start.
|
|
// Only if you open chat or dev tools do we want it to start working.
|
|
UI::EnableFocusMovement(false);
|
|
}
|
|
|
|
bool EmuScreen::bootAllowStorage(const Path &filename) {
|
|
// No permissions needed. The easy life.
|
|
if (filename.Type() == PathType::HTTP)
|
|
return true;
|
|
|
|
if (!System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS))
|
|
return true;
|
|
|
|
PermissionStatus status = System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE);
|
|
switch (status) {
|
|
case PERMISSION_STATUS_UNKNOWN:
|
|
System_AskForPermission(SYSTEM_PERMISSION_STORAGE);
|
|
return false;
|
|
|
|
case PERMISSION_STATUS_DENIED:
|
|
if (!bootPending_) {
|
|
screenManager()->switchScreen(new MainScreen());
|
|
}
|
|
return false;
|
|
|
|
case PERMISSION_STATUS_PENDING:
|
|
// Keep waiting.
|
|
return false;
|
|
|
|
case PERMISSION_STATUS_GRANTED:
|
|
return true;
|
|
}
|
|
|
|
_assert_(false);
|
|
return false;
|
|
}
|
|
|
|
void EmuScreen::ProcessGameBoot(const Path &filename) {
|
|
if (!bootPending_ && !readyToFinishBoot_) {
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
|
|
if (!root_) {
|
|
// Views not created yet, wait until they are. Not sure if this can actually happen
|
|
// but crash reports seem to indicate it.
|
|
return;
|
|
}
|
|
|
|
// Check permission status first, in case we came from a shortcut.
|
|
if (!bootAllowStorage(filename)) {
|
|
return;
|
|
}
|
|
|
|
if (Achievements::IsBlockingExecution()) {
|
|
// Keep waiting.
|
|
return;
|
|
}
|
|
|
|
if (readyToFinishBoot_) {
|
|
// Finish booting.
|
|
bootComplete();
|
|
readyToFinishBoot_ = false;
|
|
return;
|
|
}
|
|
|
|
std::string error_string = "(unknown error)";
|
|
const BootState state = PSP_InitUpdate(&error_string);
|
|
|
|
if (state == BootState::Off && screenManager()->topScreen() != this) {
|
|
// Don't kick off a new boot if we're not on top.
|
|
return;
|
|
}
|
|
|
|
switch (state) {
|
|
case BootState::Booting:
|
|
// Keep trying.
|
|
return;
|
|
case BootState::Failed:
|
|
// Failure.
|
|
_dbg_assert_(!error_string.empty());
|
|
g_BackgroundAudio.SetGame(Path());
|
|
bootPending_ = false;
|
|
errorMessage_ = error_string;
|
|
ERROR_LOG(Log::Boot, "Boot failed: %s", errorMessage_.c_str());
|
|
return;
|
|
case BootState::Complete:
|
|
// Done booting!
|
|
g_BackgroundAudio.SetGame(Path());
|
|
bootPending_ = false;
|
|
errorMessage_.clear();
|
|
|
|
if (PSP_CoreParameter().startBreak) {
|
|
coreState = CORE_STEPPING_CPU;
|
|
System_Notify(SystemNotification::DEBUG_MODE_CHANGE);
|
|
} else {
|
|
coreState = CORE_RUNNING_CPU;
|
|
}
|
|
|
|
Achievements::Initialize();
|
|
|
|
readyToFinishBoot_ = true;
|
|
return;
|
|
case BootState::Off:
|
|
// Gotta start the boot process! Continue below.
|
|
break;
|
|
}
|
|
|
|
SetAssertCancelCallback(&AssertCancelCallback, this);
|
|
|
|
if (!g_Config.bShaderCache) {
|
|
// Only developers should ever see this.
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, "Shader cache is disabled (developer)");
|
|
}
|
|
|
|
if (g_Config.bTiltInputEnabled && g_Config.iTiltInputType != 0 && System_GetPropertyBool(SYSPROP_HAS_ACCELEROMETER)) {
|
|
auto co = GetI18NCategory(I18NCat::CONTROLS);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions("%1: %2", co->T("Tilt control"), di->T("Enabled")), "", "I_CONTROLLER", 2.5f, "tilt");
|
|
}
|
|
|
|
currentMIPS = &mipsr4k;
|
|
|
|
CoreParameter coreParam{};
|
|
coreParam.cpuCore = (CPUCore)g_Config.iCpuCore;
|
|
coreParam.gpuCore = GPUCORE_GLES;
|
|
switch (GetGPUBackend()) {
|
|
case GPUBackend::DIRECT3D11:
|
|
coreParam.gpuCore = GPUCORE_DIRECTX11;
|
|
break;
|
|
#if !PPSSPP_PLATFORM(UWP)
|
|
#if PPSSPP_API(ANY_GL)
|
|
case GPUBackend::OPENGL:
|
|
coreParam.gpuCore = GPUCORE_GLES;
|
|
break;
|
|
#endif
|
|
case GPUBackend::VULKAN:
|
|
coreParam.gpuCore = GPUCORE_VULKAN;
|
|
break;
|
|
#endif
|
|
}
|
|
|
|
// Preserve the existing graphics context.
|
|
coreParam.graphicsContext = PSP_CoreParameter().graphicsContext;
|
|
coreParam.enableSound = g_Config.bEnableSound;
|
|
coreParam.fileToStart = filename;
|
|
coreParam.mountIso.clear();
|
|
coreParam.mountRoot = g_Config.mountRoot;
|
|
coreParam.startBreak = !g_Config.bAutoRun;
|
|
coreParam.headLess = false;
|
|
|
|
if (g_Config.iInternalResolution == 0) {
|
|
coreParam.renderWidth = g_display.pixel_xres;
|
|
coreParam.renderHeight = g_display.pixel_yres;
|
|
} else {
|
|
if (g_Config.iInternalResolution < 0)
|
|
g_Config.iInternalResolution = 1;
|
|
coreParam.renderWidth = 480 * g_Config.iInternalResolution;
|
|
coreParam.renderHeight = 272 * g_Config.iInternalResolution;
|
|
}
|
|
coreParam.pixelWidth = g_display.pixel_xres;
|
|
coreParam.pixelHeight = g_display.pixel_yres;
|
|
|
|
// PSP_InitStart can't really fail anymore, unless it's called at the wrong time. It just starts the loader thread.
|
|
if (!PSP_InitStart(coreParam)) {
|
|
bootPending_ = false;
|
|
ERROR_LOG(Log::Boot, "InitStart ProcessGameBoot error: %s", errorMessage_.c_str());
|
|
return;
|
|
}
|
|
|
|
_dbg_assert_(loadingViewVisible_);
|
|
_dbg_assert_(loadingViewColor_);
|
|
|
|
if (loadingViewColor_)
|
|
loadingViewColor_->Divert(0xFFFFFFFF, 0.75f);
|
|
if (loadingViewVisible_)
|
|
loadingViewVisible_->Divert(UI::V_VISIBLE, 0.75f);
|
|
|
|
screenManager()->getDrawContext()->ResetStats();
|
|
|
|
System_PostUIMessage(UIMessage::GAME_SELECTED, filename.c_str());
|
|
}
|
|
|
|
// Only call this on successful boot.
|
|
void EmuScreen::bootComplete() {
|
|
__DisplayListenFlip([](void *userdata) {
|
|
EmuScreen *scr = (EmuScreen *)userdata;
|
|
scr->HandleFlip();
|
|
}, (void *)this);
|
|
|
|
// Initialize retroachievements, now that we're on the right thread.
|
|
if (g_Config.bAchievementsEnable) {
|
|
std::string errorString;
|
|
Achievements::SetGame(PSP_CoreParameter().fileToStart, PSP_CoreParameter().fileType, PSP_LoadedFile());
|
|
}
|
|
|
|
// We don't want to boot with the wrong game specific config, so wait until info is ready.
|
|
// TODO: Actually, we read this info again during bootup, so this is not really necessary.
|
|
auto sc = GetI18NCategory(I18NCat::SCREEN);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
|
|
if (g_paramSFO.IsValid()) {
|
|
g_Discord.SetPresenceGame(SanitizeString(g_paramSFO.GetValueString("TITLE"), StringRestriction::NoLineBreaksOrSpecials));
|
|
std::string gameTitle = SanitizeString(g_paramSFO.GetValueString("TITLE"), StringRestriction::NoLineBreaksOrSpecials, 0, 32);
|
|
std::string id = g_paramSFO.GetValueString("DISC_ID");
|
|
extraAssertInfoStr_ = id + " " + gameTitle;
|
|
SetExtraAssertInfo(extraAssertInfoStr_.c_str());
|
|
SaveState::Rescan(SaveState::GetGamePrefix(g_paramSFO));
|
|
} else {
|
|
g_Discord.SetPresenceGame(sc->T("Untitled PSP game"));
|
|
}
|
|
|
|
UpdateUIState(UISTATE_INGAME);
|
|
System_Notify(SystemNotification::BOOT_DONE);
|
|
System_Notify(SystemNotification::DISASSEMBLY);
|
|
|
|
NOTICE_LOG(Log::Boot, "Booted %s...", PSP_CoreParameter().fileToStart.c_str());
|
|
if (!Achievements::HardcoreModeActive() && !bootIsReset_) {
|
|
// Don't auto-load savestates in hardcore mode.
|
|
AutoLoadSaveState();
|
|
}
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
if (g_Config.bFirstRun) {
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, sc->T("PressESC", "Press ESC to open the pause menu"));
|
|
}
|
|
#endif
|
|
|
|
#if !PPSSPP_PLATFORM(UWP)
|
|
if (GetGPUBackend() == GPUBackend::OPENGL) {
|
|
const char *renderer = gl_extensions.model;
|
|
if (strstr(renderer, "Chainfire3D") != 0) {
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("Chainfire3DWarning", "WARNING: Chainfire3D detected, may cause problems"), 10.0f);
|
|
} else if (strstr(renderer, "GLTools") != 0) {
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("GLToolsWarning", "WARNING: GLTools detected, may cause problems"), 10.0f);
|
|
}
|
|
|
|
if (g_Config.bGfxDebugOutput) {
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, "WARNING: GfxDebugOutput is enabled via ppsspp.ini. Things may be slow.", 10.0f);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (Core_GetPowerSaving()) {
|
|
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
|
#ifdef __ANDROID__
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, sy->T("WARNING: Android battery save mode is on"), 2.0f, "core_powerSaving");
|
|
#else
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, sy->T("WARNING: Battery save mode is on"), 2.0f, "core_powerSaving");
|
|
#endif
|
|
}
|
|
|
|
if (g_Config.bStereoRendering) {
|
|
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
// Stereo rendering is experimental, so let's notify the user it's being used.
|
|
// Carefully reuse translations for this rare warning.
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, std::string(gr->T("Stereo rendering")) + ": " + std::string(di->T("Enabled")));
|
|
}
|
|
|
|
saveStateSlot_ = SaveState::GetCurrentSlot();
|
|
|
|
if (loadingViewColor_)
|
|
loadingViewColor_->Divert(0x00FFFFFF, 0.2f);
|
|
if (loadingViewVisible_)
|
|
loadingViewVisible_->Divert(UI::V_INVISIBLE, 0.2f);
|
|
|
|
std::string gameID = g_paramSFO.GetValueString("DISC_ID");
|
|
g_Config.TimeTracker().Start(gameID);
|
|
|
|
bootIsReset_ = false;
|
|
|
|
// Reset views in case controls are in a different place.
|
|
RecreateViews();
|
|
}
|
|
|
|
EmuScreen::~EmuScreen() {
|
|
g_controlMapper.RemoveListener(this);
|
|
|
|
if (imguiInited_) {
|
|
ImGui_ImplThin3d_Shutdown();
|
|
ImGui::DestroyContext(ctx_);
|
|
}
|
|
|
|
std::string gameID = g_paramSFO.GetValueString("DISC_ID");
|
|
g_Config.TimeTracker().Stop(gameID);
|
|
|
|
if (bootPending_) {
|
|
// We probably quit during boot, got blocked in lostdevice, and then didn't end up in update again to call PSP_InitUpdate.
|
|
// So we need to finish and join the boot thread before we can exit.
|
|
_dbg_assert_(PollBootState() != BootState::Booting);
|
|
// Make sure we join the boot thread, by calling PSP_InitUpdate.
|
|
std::string error_string = "(unknown error)";
|
|
PSP_InitUpdate(&error_string);
|
|
ERROR_LOG(Log::G3D, "Quit during boot, not good. %s", error_string.c_str());
|
|
bootPending_ = false;
|
|
}
|
|
|
|
Achievements::UnloadGame();
|
|
PSP_Shutdown(true);
|
|
|
|
// If achievements are disabled in the global config, let's shut it down here.
|
|
if (!g_Config.bAchievementsEnable) {
|
|
Achievements::Shutdown();
|
|
}
|
|
|
|
_dbg_assert_(coreState == CORE_POWERDOWN);
|
|
|
|
System_PostUIMessage(UIMessage::GAME_SELECTED, "");
|
|
|
|
g_OSD.ClearAchievementStuff();
|
|
|
|
SetExtraAssertInfo(nullptr);
|
|
SetAssertCancelCallback(nullptr, nullptr);
|
|
|
|
g_logManager.DisableOutput(LogOutput::RingBuffer);
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
if (g_Config.bDumpFrames && startDumping_)
|
|
{
|
|
avi.Stop();
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, "AVI Dump stopped.", 2.0f);
|
|
startDumping_ = false;
|
|
}
|
|
#endif
|
|
|
|
if (GetUIState() == UISTATE_EXIT)
|
|
g_Discord.ClearPresence();
|
|
else
|
|
g_Discord.SetPresenceMenu();
|
|
|
|
// This makes sure that the recents list is updated, among other things.
|
|
g_Config.Save("exitGame");
|
|
}
|
|
|
|
void EmuScreen::dialogFinished(const Screen *dialog, DialogResult result) {
|
|
std::string_view tag = dialog->tag();
|
|
if (tag == "TextEditPopup") {
|
|
// Chat message finished.
|
|
return;
|
|
}
|
|
|
|
// Returning to the PauseScreen, unless we're stepping, means we should go back to controls.
|
|
UI::EnableFocusMovement(false);
|
|
|
|
// TODO: improve the way with which we got commands from PauseMenu.
|
|
// DR_CANCEL/DR_BACK means clicked on "continue", DR_OK means clicked on "back to menu",
|
|
// DR_YES means a message sent to PauseMenu by System_PostUIMessage.
|
|
if ((result == DR_OK || quit_) && !bootPending_) {
|
|
screenManager()->switchScreen(new MainScreen());
|
|
quit_ = false;
|
|
} else {
|
|
RecreateViews();
|
|
}
|
|
|
|
SetExtraAssertInfo(extraAssertInfoStr_.c_str());
|
|
|
|
// Make sure we re-enable keyboard mode if it was disabled by the dialog, and if needed.
|
|
lastImguiEnabled_ = false;
|
|
}
|
|
|
|
static void AfterSaveStateAction(SaveState::Status status, std::string_view message) {
|
|
if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) {
|
|
g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0);
|
|
}
|
|
}
|
|
|
|
void EmuScreen::focusChanged(ScreenFocusChange focusChange) {
|
|
Screen::focusChanged(focusChange);
|
|
|
|
std::string gameID = g_paramSFO.GetValueString("DISC_ID");
|
|
if (gameID.empty()) {
|
|
// startup or shutdown
|
|
return;
|
|
}
|
|
switch (focusChange) {
|
|
case ScreenFocusChange::FOCUS_LOST_TOP:
|
|
g_Config.TimeTracker().Stop(gameID);
|
|
break;
|
|
case ScreenFocusChange::FOCUS_BECAME_TOP:
|
|
g_Config.TimeTracker().Start(gameID);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EmuScreen::sendMessage(UIMessage message, const char *value) {
|
|
// External commands, like from the Windows UI.
|
|
// This happens on the main thread.
|
|
if (message == UIMessage::REQUEST_GAME_PAUSE && screenManager()->topScreen() == this) {
|
|
screenManager()->push(new GamePauseScreen(gamePath_, bootPending_));
|
|
} else if (message == UIMessage::REQUEST_GAME_STOP) {
|
|
// We will push MainScreen in update().
|
|
if (bootPending_) {
|
|
WARN_LOG(Log::Loader, "Can't stop during a pending boot");
|
|
return;
|
|
}
|
|
// The destructor will take care of shutting down.
|
|
screenManager()->switchScreen(new MainScreen());
|
|
} else if (message == UIMessage::REQUEST_GAME_RESET) {
|
|
if (bootPending_) {
|
|
WARN_LOG(Log::Loader, "Can't reset during a pending boot");
|
|
return;
|
|
}
|
|
Achievements::UnloadGame();
|
|
PSP_Shutdown(true);
|
|
UI::EnableFocusMovement(false);
|
|
|
|
// Restart the boot process
|
|
bootPending_ = true;
|
|
bootIsReset_ = true;
|
|
RecreateViews();
|
|
_dbg_assert_(coreState == CORE_POWERDOWN);
|
|
if (!PSP_InitStart(PSP_CoreParameter())) {
|
|
bootPending_ = false;
|
|
WARN_LOG(Log::Loader, "Error resetting");
|
|
screenManager()->switchScreen(new MainScreen());
|
|
return;
|
|
}
|
|
} else if (message == UIMessage::REQUEST_GAME_BOOT) {
|
|
INFO_LOG(Log::Loader, "EmuScreen received REQUEST_GAME_BOOT: %s", value);
|
|
|
|
if (bootPending_) {
|
|
ERROR_LOG(Log::Loader, "Can't boot a new game during a pending boot");
|
|
return;
|
|
}
|
|
// TODO: Ignore or not if it's the same game that's already running?
|
|
if (gamePath_ == Path(value)) {
|
|
WARN_LOG(Log::Loader, "Game already running, ignoring");
|
|
return;
|
|
}
|
|
|
|
// TODO: Create a path first and
|
|
Path newGamePath(value);
|
|
|
|
if (newGamePath.GetFileExtension() == ".ppst") {
|
|
// TODO: Should verify that it's for the correct game....
|
|
INFO_LOG(Log::Loader, "New game is a save state - just load it.");
|
|
SaveState::Load(newGamePath, -1, [](SaveState::Status status, std::string_view message) {
|
|
Core_Resume();
|
|
System_Notify(SystemNotification::DISASSEMBLY);
|
|
});
|
|
} else {
|
|
Achievements::UnloadGame();
|
|
PSP_Shutdown(true);
|
|
|
|
// OK, now pop any open settings screens and stuff that are running above us.
|
|
// Otherwise, we can get strange results with game-specific settings.
|
|
screenManager()->cancelScreensAbove(this);
|
|
|
|
bootPending_ = true;
|
|
bootIsReset_ = false;
|
|
gamePath_ = newGamePath;
|
|
}
|
|
} else if (message == UIMessage::CONFIG_LOADED) {
|
|
// In case we need to position touch controls differently.
|
|
RecreateViews();
|
|
} else if (message == UIMessage::SHOW_CONTROL_MAPPING && screenManager()->topScreen() == this) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new ControlMappingScreen(gamePath_));
|
|
} else if (message == UIMessage::SHOW_DISPLAY_LAYOUT_EDITOR && screenManager()->topScreen() == this) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new DisplayLayoutScreen(gamePath_));
|
|
} else if (message == UIMessage::SHOW_SETTINGS && screenManager()->topScreen() == this) {
|
|
UpdateUIState(UISTATE_PAUSEMENU);
|
|
screenManager()->push(new GameSettingsScreen(gamePath_));
|
|
} else if (message == UIMessage::REQUEST_GPU_DUMP_NEXT_FRAME) {
|
|
if (gpu)
|
|
gpu->DumpNextFrame();
|
|
} else if (message == UIMessage::REQUEST_CLEAR_JIT) {
|
|
if (!bootPending_) {
|
|
currentMIPS->ClearJitCache();
|
|
if (PSP_IsInited()) {
|
|
currentMIPS->UpdateCore((CPUCore)g_Config.iCpuCore);
|
|
}
|
|
}
|
|
} else if (message == UIMessage::WINDOW_MINIMIZED) {
|
|
if (!strcmp(value, "true")) {
|
|
gstate_c.skipDrawReason |= SKIPDRAW_WINDOW_MINIMIZED;
|
|
} else {
|
|
gstate_c.skipDrawReason &= ~SKIPDRAW_WINDOW_MINIMIZED;
|
|
}
|
|
} else if (message == UIMessage::SHOW_CHAT_SCREEN) {
|
|
if (g_Config.bEnableNetworkChat && !g_Config.bShowImDebugger) {
|
|
if (!chatButton_)
|
|
RecreateViews();
|
|
|
|
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_DESKTOP) {
|
|
// temporary workaround for hotkey its freeze the ui when open chat screen using hotkey and native keyboard is enable
|
|
if (g_Config.bBypassOSKWithKeyboard) {
|
|
// TODO: Make translatable.
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, "Disable \"Use system native keyboard\" to use chat hotkey", 2.0f);
|
|
} else {
|
|
OpenChat(true);
|
|
}
|
|
} else {
|
|
OpenChat(false);
|
|
}
|
|
} else if (!g_Config.bEnableNetworkChat) {
|
|
if (chatButton_) {
|
|
RecreateViews();
|
|
}
|
|
}
|
|
} else if (message == UIMessage::APP_RESUMED && screenManager()->topScreen() == this) {
|
|
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV) {
|
|
if (!KeyMap::IsKeyMapped(DEVICE_ID_PAD_0, VIRTKEY_PAUSE) || !KeyMap::IsKeyMapped(DEVICE_ID_PAD_1, VIRTKEY_PAUSE)) {
|
|
// If it's a TV (so no built-in back button), and there's no back button mapped to a pad,
|
|
// use this as the fallback way to get into the menu.
|
|
// Don't do it on the first resume though, in case we launch directly into emuscreen, like from a frontend - see #18926
|
|
if (!equals(value, "first")) {
|
|
screenManager()->push(new GamePauseScreen(gamePath_, bootPending_));
|
|
}
|
|
}
|
|
}
|
|
} else if (message == UIMessage::REQUEST_PLAY_SOUND) {
|
|
if (g_Config.bAchievementsSoundEffects && g_Config.bEnableSound) {
|
|
float achievementVolume = Volume100ToMultiplier(g_Config.iAchievementVolume);
|
|
// TODO: Handle this some nicer way.
|
|
if (!strcmp(value, "achievement_unlocked")) {
|
|
g_BackgroundAudio.SFX().Play(UI::UISound::ACHIEVEMENT_UNLOCKED, achievementVolume);
|
|
}
|
|
if (!strcmp(value, "leaderboard_submitted")) {
|
|
g_BackgroundAudio.SFX().Play(UI::UISound::LEADERBOARD_SUBMITTED, achievementVolume);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool EmuScreen::UnsyncTouch(const TouchInput &touch) {
|
|
System_Notify(SystemNotification::ACTIVITY);
|
|
|
|
bool ignoreGamepad = false;
|
|
|
|
if (chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE) {
|
|
// Avoid pressing touch button behind the chat
|
|
if (chatMenu_->Contains(touch.x, touch.y)) {
|
|
ignoreGamepad = true;
|
|
}
|
|
}
|
|
|
|
if (touch.flags & TouchInputFlags::DOWN) {
|
|
if (!(g_Config.bShowImDebugger && imguiInited_) && !ignoreGamepad) {
|
|
// This just prevents the gamepad from timing out.
|
|
GamepadTouch();
|
|
}
|
|
}
|
|
|
|
if (root_) {
|
|
UIScreen::UnsyncTouch(touch);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// TODO: We should replace the "fpsLimit" system with a speed factor.
|
|
static void ShowFpsLimitNotice() {
|
|
int fpsLimit = 60;
|
|
|
|
switch (PSP_CoreParameter().fpsLimit) {
|
|
case FPSLimit::CUSTOM1:
|
|
fpsLimit = g_Config.iFpsLimit1;
|
|
break;
|
|
case FPSLimit::CUSTOM2:
|
|
fpsLimit = g_Config.iFpsLimit2;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Now display it.
|
|
|
|
char temp[51];
|
|
snprintf(temp, sizeof(temp), "%d%%", (int)((float)fpsLimit / 60.0f * 100.0f));
|
|
g_OSD.Show(OSDType::STATUS_ICON, temp, "", "I_FAST_FORWARD", 1.5f, "altspeed");
|
|
g_OSD.SetFlags("altspeed", OSDMessageFlags::Transparent);
|
|
}
|
|
|
|
// NOTE: This is unsynchronized! We should have as little as possible in here.
|
|
void EmuScreen::OnVKey(VirtKey virtualKeyCode, bool down) {
|
|
if (!IsOnTop())
|
|
return;
|
|
|
|
auto sc = GetI18NCategory(I18NCat::SCREEN);
|
|
auto mc = GetI18NCategory(I18NCat::MAPPABLECONTROLS);
|
|
|
|
switch (virtualKeyCode) {
|
|
case VIRTKEY_FASTFORWARD:
|
|
if (down && !NetworkWarnUserIfOnlineAndCantSpeed() && !bootPending_) {
|
|
/*
|
|
// This seems like strange behavior. Commented it out.
|
|
if (coreState == CORE_STEPPING_CPU) {
|
|
Core_Resume();
|
|
}
|
|
*/
|
|
PSP_CoreParameter().fastForward = true;
|
|
} else {
|
|
PSP_CoreParameter().fastForward = false;
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_SPEED_CUSTOM1:
|
|
if (down && !NetworkWarnUserIfOnlineAndCantSpeed()) {
|
|
if (PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM1;
|
|
ShowFpsLimitNotice();
|
|
}
|
|
} else {
|
|
if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL;
|
|
ShowFpsLimitNotice();
|
|
}
|
|
}
|
|
break;
|
|
case VIRTKEY_SPEED_CUSTOM2:
|
|
if (down && !NetworkWarnUserIfOnlineAndCantSpeed()) {
|
|
if (PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM2;
|
|
ShowFpsLimitNotice();
|
|
}
|
|
} else {
|
|
if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM2) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL;
|
|
ShowFpsLimitNotice();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_RAPID_FIRE:
|
|
__CtrlSetRapidFire(down, g_Config.iRapidFireInterval);
|
|
break;
|
|
default:
|
|
// To make sure we're not in an async context.
|
|
if (down) {
|
|
queuedVirtKeys_.push_back(virtualKeyCode);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EmuScreen::ProcessQueuedVKeys() {
|
|
for (auto iter : queuedVirtKeys_) {
|
|
ProcessVKey(iter);
|
|
}
|
|
queuedVirtKeys_.clear();
|
|
}
|
|
|
|
// Synchronized processing of virtkeys.
|
|
void EmuScreen::ProcessVKey(VirtKey virtKey) {
|
|
auto mc = GetI18NCategory(I18NCat::MAPPABLECONTROLS);
|
|
auto sc = GetI18NCategory(I18NCat::SCREEN);
|
|
|
|
switch (virtKey) {
|
|
case VIRTKEY_PAUSE:
|
|
// Note: We don't check NetworkWarnUserIfOnlineAndCantSpeed, because we can keep
|
|
// running in the background of the menu.
|
|
pauseTrigger_ = true;
|
|
break;
|
|
|
|
case VIRTKEY_SCREENSHOT:
|
|
TakeUserScreenshot();
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_DEBUGGER:
|
|
g_Config.bShowImDebugger = !g_Config.bShowImDebugger;
|
|
break;
|
|
case VIRTKEY_TOGGLE_TILT:
|
|
g_Config.bTiltInputEnabled = !g_Config.bTiltInputEnabled;
|
|
if (!g_Config.bTiltInputEnabled) {
|
|
// Reset whatever got tilted.
|
|
switch (g_Config.iTiltInputType) {
|
|
case TILT_ANALOG:
|
|
__CtrlSetAnalogXY(0, 0, 0);
|
|
break;
|
|
case TILT_ACTION_BUTTON:
|
|
__CtrlUpdateButtons(0, CTRL_CROSS | CTRL_CIRCLE | CTRL_SQUARE | CTRL_TRIANGLE);
|
|
break;
|
|
case TILT_DPAD:
|
|
__CtrlUpdateButtons(0, CTRL_UP | CTRL_DOWN | CTRL_LEFT | CTRL_RIGHT);
|
|
break;
|
|
case TILT_TRIGGER_BUTTONS:
|
|
__CtrlUpdateButtons(0, CTRL_LTRIGGER | CTRL_RTRIGGER);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case VIRTKEY_OPENCHAT:
|
|
if (g_Config.bEnableNetworkChat && !g_Config.bShowImDebugger) {
|
|
UI::EventParams e{};
|
|
g_controlMapper.ForceReleaseVKey(VIRTKEY_OPENCHAT);
|
|
OpenChat(true);
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_AXIS_SWAP_TOGGLE:
|
|
g_controlMapper.ToggleSwapAxes();
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, mc->T("AxisSwap")); // best string we have.
|
|
break;
|
|
|
|
case VIRTKEY_DEVMENU:
|
|
{
|
|
UI::EventParams e{};
|
|
OnDevMenu.Trigger(e);
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_MOUSE:
|
|
g_Config.bMouseControl = !g_Config.bMouseControl;
|
|
break;
|
|
|
|
case VIRTKEY_TEXTURE_DUMP:
|
|
g_Config.bSaveNewTextures = !g_Config.bSaveNewTextures;
|
|
if (g_Config.bSaveNewTextures) {
|
|
g_OSD.Show(OSDType::MESSAGE_SUCCESS, sc->T("saveNewTextures_true", "Textures will now be saved to your storage"), 2.0, "savetexturechanged");
|
|
} else {
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, sc->T("saveNewTextures_false", "Texture saving was disabled"), 2.0, "savetexturechanged");
|
|
}
|
|
System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED);
|
|
break;
|
|
|
|
case VIRTKEY_TEXTURE_REPLACE:
|
|
g_Config.bReplaceTextures = !g_Config.bReplaceTextures;
|
|
if (g_Config.bReplaceTextures) {
|
|
g_OSD.Show(OSDType::MESSAGE_SUCCESS, sc->T("replaceTextures_true", "Texture replacement enabled"), 2.0, "replacetexturechanged");
|
|
} else {
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, sc->T("replaceTextures_false", "Textures are no longer being replaced"), 2.0, "replacetexturechanged");
|
|
}
|
|
System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED);
|
|
break;
|
|
|
|
case VIRTKEY_MUTE_TOGGLE:
|
|
g_Config.bEnableSound = !g_Config.bEnableSound;
|
|
break;
|
|
|
|
case VIRTKEY_SCREEN_ROTATION_VERTICAL:
|
|
{
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_VERTICAL;
|
|
break;
|
|
}
|
|
case VIRTKEY_SCREEN_ROTATION_VERTICAL180:
|
|
{
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_VERTICAL180;
|
|
break;
|
|
}
|
|
case VIRTKEY_SCREEN_ROTATION_HORIZONTAL:
|
|
{
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_HORIZONTAL;
|
|
break;
|
|
}
|
|
case VIRTKEY_SCREEN_ROTATION_HORIZONTAL180:
|
|
{
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_HORIZONTAL180;
|
|
break;
|
|
}
|
|
|
|
case VIRTKEY_TOGGLE_WLAN:
|
|
// Let's not allow the user to toggle wlan while connected, could get confusing.
|
|
if (!g_netInited) {
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
g_Config.bEnableWlan = !g_Config.bEnableWlan;
|
|
// Try to avoid adding more strings so we piece together a message from existing ones.
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, StringFromFormat(
|
|
"%s: %s", n->T_cstr("Enable networking"), g_Config.bEnableWlan ? di->T_cstr("Enabled") : di->T_cstr("Disabled")), 2.0, "toggle_wlan");
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_FULLSCREEN:
|
|
// TODO: Limit to platforms that can support fullscreen.
|
|
g_Config.bFullScreen = !g_Config.bFullScreen;
|
|
System_ApplyFullscreenState();
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_TOUCH_CONTROLS:
|
|
if (g_Config.bShowTouchControls) {
|
|
// This just messes with opacity if enabled, so you can touch the screen again to bring them back.
|
|
if (GamepadGetOpacity() < 0.01f) {
|
|
GamepadTouch();
|
|
} else {
|
|
GamepadResetTouch();
|
|
}
|
|
} else {
|
|
// If touch controls are disabled though, they'll get enabled.
|
|
g_Config.bShowTouchControls = true;
|
|
RecreateViews();
|
|
GamepadTouch();
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_REWIND:
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
|
if (SaveState::CanRewind()) {
|
|
SaveState::Rewind(&AfterSaveStateAction);
|
|
} else {
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_PAUSE_NO_MENU:
|
|
if (!NetworkWarnUserIfOnlineAndCantSpeed()) {
|
|
// We re-use debug break/resume to implement pause/resume without a menu.
|
|
if (coreState == CORE_STEPPING_CPU) { // should we check reason?
|
|
Core_Resume();
|
|
} else {
|
|
Core_Break(BreakReason::UIPause);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_EXIT_APP:
|
|
{
|
|
if (!bootPending_) {
|
|
std::string confirmExitMessage = GetConfirmExitMessage();
|
|
if (!confirmExitMessage.empty()) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto mm = GetI18NCategory(I18NCat::MAINMENU);
|
|
confirmExitMessage += '\n';
|
|
confirmExitMessage += di->T("Are you sure you want to exit?");
|
|
screenManager()->push(new UI::MessagePopupScreen(mm->T("Exit"), confirmExitMessage, di->T("Yes"), di->T("No"), [=](bool result) {
|
|
if (result) {
|
|
System_ExitApp();
|
|
}
|
|
}));
|
|
} else {
|
|
System_ExitApp();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case VIRTKEY_FRAME_ADVANCE:
|
|
// Can't do this reliably in an async fashion, so we just set a variable.
|
|
// Is this used by anyone? There's no user-friendly way to resume, other than PAUSE_NO_MENU or the debugger.
|
|
if (!NetworkWarnUserIfOnlineAndCantSpeed()) {
|
|
if (Core_IsStepping()) {
|
|
Core_Resume();
|
|
frameStep_ = true;
|
|
} else {
|
|
Core_Break(BreakReason::FrameAdvance);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_SPEED_TOGGLE:
|
|
if (!NetworkWarnUserIfOnlineAndCantSpeed()) {
|
|
// Cycle through enabled speeds.
|
|
if (PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL && g_Config.iFpsLimit1 >= 0) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM1;
|
|
} else if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1 && g_Config.iFpsLimit2 >= 0) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM2;
|
|
} else if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1 || PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM2) {
|
|
PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL;
|
|
}
|
|
|
|
ShowFpsLimitNotice();
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_RESET_EMULATION:
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_RESET);
|
|
break;
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
case VIRTKEY_RECORD:
|
|
if (g_Config.bDumpFrames == g_Config.bDumpAudio) {
|
|
g_Config.bDumpFrames = !g_Config.bDumpFrames;
|
|
g_Config.bDumpAudio = !g_Config.bDumpAudio;
|
|
} else {
|
|
// This hotkey should always toggle both audio and video together.
|
|
// So let's make sure that's the only outcome even if video OR audio was already being dumped.
|
|
if (g_Config.bDumpFrames) {
|
|
AVIDump::Stop();
|
|
AVIDump::Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
|
|
g_Config.bDumpAudio = true;
|
|
} else {
|
|
WAVDump::Reset();
|
|
g_Config.bDumpFrames = true;
|
|
}
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
case VIRTKEY_SAVE_STATE:
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
|
SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction);
|
|
}
|
|
break;
|
|
case VIRTKEY_LOAD_STATE:
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
|
SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction);
|
|
}
|
|
break;
|
|
case VIRTKEY_PREVIOUS_SLOT:
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
|
SaveState::PrevSlot();
|
|
System_PostUIMessage(UIMessage::SAVESTATE_DISPLAY_SLOT);
|
|
}
|
|
break;
|
|
case VIRTKEY_NEXT_SLOT:
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
|
SaveState::NextSlot();
|
|
System_PostUIMessage(UIMessage::SAVESTATE_DISPLAY_SLOT);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EmuScreen::OnVKeyAnalog(VirtKey virtualKeyCode, float value) {
|
|
if (!IsOnTop())
|
|
return;
|
|
|
|
if (virtualKeyCode != VIRTKEY_SPEED_ANALOG) {
|
|
return;
|
|
}
|
|
|
|
// We only handle VIRTKEY_SPEED_ANALOG here.
|
|
|
|
// Xbox controllers need a pretty big deadzone here to not leave behind small values
|
|
// on occasion when releasing the trigger. Still feels right.
|
|
static constexpr float DEADZONE_THRESHOLD = 0.2f;
|
|
static constexpr float DEADZONE_SCALE = 1.0f / (1.0f - DEADZONE_THRESHOLD);
|
|
|
|
FPSLimit &limitMode = PSP_CoreParameter().fpsLimit;
|
|
// If we're using an alternate speed already, let that win.
|
|
if (limitMode != FPSLimit::NORMAL && limitMode != FPSLimit::ANALOG)
|
|
return;
|
|
// Don't even try if the limit is invalid.
|
|
if (g_Config.iAnalogFpsLimit <= 0)
|
|
return;
|
|
|
|
// Apply a small deadzone (against the resting position.)
|
|
value = std::max(0.0f, (value - DEADZONE_THRESHOLD) * DEADZONE_SCALE);
|
|
|
|
// If target is above 60, value is how much to speed up over 60. Otherwise, it's how much slower.
|
|
// So normalize the target.
|
|
int target = g_Config.iAnalogFpsLimit - 60;
|
|
PSP_CoreParameter().analogFpsLimit = 60 + (int)(target * value);
|
|
|
|
// If we've reset back to normal, turn it off.
|
|
limitMode = PSP_CoreParameter().analogFpsLimit == 60 ? FPSLimit::NORMAL : FPSLimit::ANALOG;
|
|
}
|
|
|
|
bool EmuScreen::UnsyncKey(const KeyInput &key) {
|
|
System_Notify(SystemNotification::ACTIVITY);
|
|
|
|
// Update imgui modifier flags
|
|
if (key.flags & (KeyInputFlags::DOWN | KeyInputFlags::UP)) {
|
|
bool down = (key.flags & KeyInputFlags::DOWN) != 0;
|
|
switch (key.keyCode) {
|
|
case NKCODE_CTRL_LEFT: keyCtrlLeft_ = down; break;
|
|
case NKCODE_CTRL_RIGHT: keyCtrlRight_ = down; break;
|
|
case NKCODE_SHIFT_LEFT: keyShiftLeft_ = down; break;
|
|
case NKCODE_SHIFT_RIGHT: keyShiftRight_ = down; break;
|
|
case NKCODE_ALT_LEFT: keyAltLeft_ = down; break;
|
|
case NKCODE_ALT_RIGHT: keyAltRight_ = down; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
const bool chatMenuOpen = chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE;
|
|
|
|
if (chatMenuOpen || (g_Config.bShowImDebugger && imguiInited_)) {
|
|
// Note: Allow some Vkeys through, so we can toggle the imgui for example (since we actually block the control mapper otherwise in imgui mode).
|
|
// We need to manually implement it here :/
|
|
if (g_Config.bShowImDebugger && imguiInited_) {
|
|
if (key.flags & (KeyInputFlags::UP | KeyInputFlags::DOWN)) {
|
|
InputMapping mapping(key.deviceId, key.keyCode);
|
|
std::vector<int> pspButtons;
|
|
bool mappingFound = KeyMap::InputMappingToPspButton(mapping, &pspButtons);
|
|
if (mappingFound) {
|
|
for (auto b : pspButtons) {
|
|
if (b == VIRTKEY_TOGGLE_DEBUGGER || b == VIRTKEY_PAUSE) {
|
|
return g_controlMapper.Key(key, &pauseTrigger_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
UI::EnableFocusMovement(false);
|
|
// Enable gamepad controls while running imgui (but ignore mouse/keyboard).
|
|
switch (key.deviceId) {
|
|
case DEVICE_ID_KEYBOARD:
|
|
if (!ImGui::GetIO().WantCaptureKeyboard) {
|
|
g_controlMapper.Key(key, &pauseTrigger_);
|
|
}
|
|
break;
|
|
case DEVICE_ID_MOUSE:
|
|
if (!ImGui::GetIO().WantCaptureMouse) {
|
|
g_controlMapper.Key(key, &pauseTrigger_);
|
|
}
|
|
break;
|
|
default:
|
|
g_controlMapper.Key(key, &pauseTrigger_);
|
|
break;
|
|
}
|
|
} else {
|
|
// Let up-events through to the controlMapper_ so input doesn't get stuck.
|
|
if (key.flags & KeyInputFlags::UP) {
|
|
g_controlMapper.Key(key, &pauseTrigger_);
|
|
}
|
|
}
|
|
|
|
return UIScreen::UnsyncKey(key);
|
|
}
|
|
return g_controlMapper.Key(key, &pauseTrigger_);
|
|
}
|
|
|
|
void EmuScreen::UnsyncAxis(const AxisInput *axes, size_t count) {
|
|
System_Notify(SystemNotification::ACTIVITY);
|
|
|
|
if (UI::IsFocusMovementEnabled()) {
|
|
return UIScreen::UnsyncAxis(axes, count);
|
|
}
|
|
|
|
return g_controlMapper.Axis(axes, count);
|
|
}
|
|
|
|
bool EmuScreen::key(const KeyInput &key) {
|
|
bool retval = UIScreen::key(key);
|
|
|
|
if (!retval && g_Config.bShowImDebugger && imguiInited_) {
|
|
ImGui_ImplPlatform_KeyEvent(key);
|
|
}
|
|
|
|
if (!retval && (key.flags & KeyInputFlags::DOWN) != 0 && UI::IsEscapeKey(key)) {
|
|
if (chatMenu_)
|
|
chatMenu_->Close();
|
|
if (chatButton_)
|
|
chatButton_->SetVisibility(UI::V_VISIBLE);
|
|
UI::EnableFocusMovement(false);
|
|
return true;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
void EmuScreen::touch(const TouchInput &touch) {
|
|
if (g_Config.bShowImDebugger && imguiInited_) {
|
|
ImGui_ImplPlatform_TouchEvent(touch);
|
|
if (!ImGui::GetIO().WantCaptureMouse) {
|
|
UIScreen::touch(touch);
|
|
}
|
|
} else if (g_Config.bMouseControl && !(touch.flags & TouchInputFlags::UP) && (touch.flags & TouchInputFlags::MOUSE)) {
|
|
// don't do anything as the mouse pointer is hidden in this case.
|
|
// But we let touch-up events through to avoid getting stuck if the user toggles mouse control.
|
|
} else {
|
|
// Handle closing the chat menu if touched outside it.
|
|
if (chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE) {
|
|
// Avoid pressing touch button behind the chat
|
|
if (!chatMenu_->Contains(touch.x, touch.y)) {
|
|
if ((touch.flags & TouchInputFlags::DOWN) != 0) {
|
|
chatMenu_->Close();
|
|
if (chatButton_)
|
|
chatButton_->SetVisibility(UI::V_VISIBLE);
|
|
UI::EnableFocusMovement(false);
|
|
}
|
|
}
|
|
}
|
|
UIScreen::touch(touch);
|
|
}
|
|
}
|
|
|
|
// TODO: Shouldn't actually need bounds for this, Anchor can center too.
|
|
static UI::AnchorLayoutParams *AnchorInCorner(const Bounds &bounds, int corner, float xOffset, float yOffset) {
|
|
using namespace UI;
|
|
switch ((ScreenEdgePosition)g_Config.iChatButtonPosition) {
|
|
case ScreenEdgePosition::BOTTOM_LEFT: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, xOffset, NONE, NONE, yOffset, Centering::Both);
|
|
case ScreenEdgePosition::BOTTOM_CENTER: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, bounds.centerX(), NONE, NONE, yOffset, Centering::Both);
|
|
case ScreenEdgePosition::BOTTOM_RIGHT: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, NONE, NONE, xOffset, yOffset, Centering::Both);
|
|
case ScreenEdgePosition::TOP_LEFT: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, xOffset, yOffset, NONE, NONE, Centering::Both);
|
|
case ScreenEdgePosition::TOP_CENTER: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, bounds.centerX(), yOffset, NONE, NONE, Centering::Both);
|
|
case ScreenEdgePosition::TOP_RIGHT: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, NONE, yOffset, xOffset, NONE, Centering::Both);
|
|
case ScreenEdgePosition::CENTER_LEFT: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, xOffset, bounds.centerY(), NONE, NONE, Centering::Both);
|
|
case ScreenEdgePosition::CENTER_RIGHT: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, NONE, bounds.centerY(), xOffset, NONE, Centering::Both);
|
|
default: return new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, xOffset, NONE, NONE, yOffset, Centering::Both);
|
|
}
|
|
}
|
|
|
|
void EmuScreen::CreateViews() {
|
|
using namespace UI;
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
|
|
auto sc = GetI18NCategory(I18NCat::SCREEN);
|
|
|
|
const DeviceOrientation deviceOrientation = GetDeviceOrientation();
|
|
|
|
TouchControlConfig &touch = g_Config.GetTouchControlsConfig(deviceOrientation);
|
|
|
|
const Bounds &bounds = GetLayoutBounds(*screenManager()->getUIContext());
|
|
|
|
InitPadLayout(&touch, deviceOrientation, bounds.w, bounds.h);
|
|
|
|
root_ = CreatePadLayout(touch, bounds.w, bounds.h, &pauseTrigger_, &g_controlMapper);
|
|
if (g_Config.bShowDeveloperMenu) {
|
|
root_->Add(new Button(dev->T("DevMenu")))->OnClick.Handle(this, &EmuScreen::OnDevTools);
|
|
}
|
|
|
|
LinearLayout *buttons = new LinearLayout(Orientation::ORIENT_HORIZONTAL, new AnchorLayoutParams(bounds.centerX(), NONE, NONE, 60, Centering::Both));
|
|
buttons->SetSpacing(20.0f);
|
|
root_->Add(buttons);
|
|
|
|
resumeButton_ = buttons->Add(new Button(dev->T("Resume")));
|
|
resumeButton_->OnClick.Add([](UI::EventParams &) {
|
|
if (coreState == CoreState::CORE_RUNTIME_ERROR) {
|
|
// Force it!
|
|
Memory::MemFault_IgnoreLastCrash();
|
|
coreState = CoreState::CORE_RUNNING_CPU;
|
|
}
|
|
});
|
|
resumeButton_->SetVisibility(V_GONE);
|
|
|
|
resetButton_ = buttons->Add(new Button(di->T("Reset")));
|
|
resetButton_->OnClick.Add([](UI::EventParams &) {
|
|
if (coreState == CoreState::CORE_RUNTIME_ERROR) {
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_RESET);
|
|
}
|
|
});
|
|
resetButton_->SetVisibility(V_GONE);
|
|
|
|
backButton_ = buttons->Add(new Button(di->T("Back")));
|
|
backButton_->OnClick.Add([this](UI::EventParams &) {
|
|
this->pauseTrigger_ = true;
|
|
});
|
|
backButton_->SetVisibility(V_GONE);
|
|
|
|
cardboardDisableButton_ = root_->Add(new Button(sc->T("Cardboard VR OFF"), new AnchorLayoutParams(bounds.centerX(), NONE, NONE, 30, Centering::Both)));
|
|
DeviceOrientation orientation = GetDeviceOrientation();
|
|
cardboardDisableButton_->OnClick.Add([deviceOrientation](UI::EventParams &) {
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(deviceOrientation);
|
|
config.bEnableCardboardVR = false;
|
|
});
|
|
cardboardDisableButton_->SetVisibility(V_GONE);
|
|
cardboardDisableButton_->SetScale(0.65f); // make it smaller - this button can be in the way otherwise.
|
|
|
|
chatButton_ = nullptr;
|
|
chatMenu_ = nullptr;
|
|
if (g_Config.bEnableNetworkChat) {
|
|
if (g_Config.iChatButtonPosition != 8) {
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
AnchorLayoutParams *layoutParams = AnchorInCorner(bounds, g_Config.iChatButtonPosition, 80.0f, 50.0f);
|
|
chatButton_ = root_->Add(new ChoiceWithValueDisplay(&newChatMessages_, n->T("Chat"), layoutParams));
|
|
chatButton_->OnClick.Add([this](UI::EventParams &e) {
|
|
// Really, the check here should be "has a hard keyboard".
|
|
bool focus = System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_DESKTOP;
|
|
OpenChat(focus);
|
|
});
|
|
}
|
|
chatMenu_ = root_->Add(new ChatMenu(GetRequesterToken(), screenManager()->getUIContext()->GetBounds(), screenManager(), new LayoutParams(FILL_PARENT, FILL_PARENT)));
|
|
chatMenu_->SetVisibility(UI::V_GONE);
|
|
}
|
|
|
|
saveStatePreview_ = new AsyncImageFileView(Path(), IS_FIXED, new AnchorLayoutParams(bounds.centerX(), 100, NONE, NONE, Centering::Both));
|
|
saveStatePreview_->SetFixedSize(160, 90);
|
|
saveStatePreview_->SetColor(0x90FFFFFF);
|
|
saveStatePreview_->SetVisibility(V_GONE);
|
|
saveStatePreview_->SetCanBeFocused(false);
|
|
root_->Add(saveStatePreview_);
|
|
|
|
GameInfoBGView *loadingBG = root_->Add(new GameInfoBGView(gamePath_, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)));
|
|
|
|
static const ImageID symbols[4] = {
|
|
ImageID("I_CROSS"),
|
|
ImageID("I_CIRCLE"),
|
|
ImageID("I_SQUARE"),
|
|
ImageID("I_TRIANGLE"),
|
|
};
|
|
|
|
Spinner *loadingSpinner = root_->Add(new Spinner(symbols, ARRAY_SIZE(symbols), new AnchorLayoutParams(NONE, NONE, 70, 70, Centering::Both)));
|
|
loadingSpinner_ = loadingSpinner;
|
|
|
|
loadingBG->SetTag("LoadingBG");
|
|
loadingSpinner->SetTag("LoadingSpinner");
|
|
|
|
loadingViewColor_ = loadingSpinner->AddTween(new CallbackColorTween(0x00FFFFFF, 0x00FFFFFF, 0.2f, &bezierEaseInOut));
|
|
loadingViewColor_->SetCallback([loadingBG, loadingSpinner](View *v, uint32_t c) {
|
|
loadingBG->SetColor(c & 0xFFC0C0C0);
|
|
loadingSpinner->SetColor(alphaMul(c, 0.7f));
|
|
});
|
|
loadingViewColor_->Persist();
|
|
|
|
// We start invisible here, in case of recreated views.
|
|
loadingViewVisible_ = loadingSpinner->AddTween(new VisibilityTween(UI::V_INVISIBLE, UI::V_INVISIBLE, 0.2f, &bezierEaseInOut));
|
|
loadingViewVisible_->Persist();
|
|
loadingViewVisible_->Finish.Add([loadingBG, loadingSpinner](EventParams &p) {
|
|
loadingBG->SetVisibility(p.v->GetVisibility());
|
|
|
|
// If we just became invisible, flush BGs since we don't need them anymore.
|
|
// Saves some VRAM for the game, but don't do it before we fade out...
|
|
if (p.v->GetVisibility() == V_INVISIBLE) {
|
|
g_gameInfoCache->FlushBGs();
|
|
// And we can go away too. This means the tween will never run again.
|
|
loadingBG->SetVisibility(V_GONE);
|
|
loadingSpinner->SetVisibility(V_GONE);
|
|
}
|
|
});
|
|
// Will become visible along with the loadingView.
|
|
loadingBG->SetVisibility(V_INVISIBLE);
|
|
}
|
|
|
|
void EmuScreen::deviceLost() {
|
|
// If we are currently in the middle of boot, we have to block here!
|
|
// Otherwise the boot thread will encounter draw_ == nullptr and weird stuff like that.
|
|
// We're doing this in a very ugly way for now.
|
|
while (PollBootState() == BootState::Booting) {
|
|
sleep_ms(100, "device-lost-during-boot");
|
|
}
|
|
|
|
UIScreen::deviceLost();
|
|
|
|
if (imguiInited_) {
|
|
if (imDebugger_) {
|
|
imDebugger_->DeviceLost();
|
|
}
|
|
ImGui_ImplThin3d_DestroyDeviceObjects();
|
|
}
|
|
}
|
|
|
|
void EmuScreen::deviceRestored(Draw::DrawContext *draw) {
|
|
UIScreen::deviceRestored(draw);
|
|
if (imguiInited_) {
|
|
ImGui_ImplThin3d_CreateDeviceObjects(draw);
|
|
}
|
|
}
|
|
|
|
void EmuScreen::OnDevTools(UI::EventParams ¶ms) {
|
|
DevMenuScreen *devMenu = new DevMenuScreen(gamePath_, I18NCat::DEVELOPER);
|
|
if (params.v)
|
|
devMenu->SetPopupOrigin(params.v);
|
|
screenManager()->push(devMenu);
|
|
}
|
|
|
|
void EmuScreen::OpenChat(bool focus) {
|
|
if (chatButton_ != nullptr && chatButton_->GetVisibility() == UI::V_VISIBLE) {
|
|
chatButton_->SetVisibility(UI::V_GONE);
|
|
}
|
|
if (chatMenu_ != nullptr) {
|
|
chatMenu_->SetVisibility(UI::V_VISIBLE);
|
|
|
|
if (focus) {
|
|
UI::EnableFocusMovement(true);
|
|
root_->SetDefaultFocusView(chatMenu_);
|
|
|
|
chatMenu_->SetFocus(UI::FocusFlags::CAUSE_FORCED);
|
|
UI::View *focused = UI::GetFocusedView();
|
|
if (focused) {
|
|
root_->SubviewFocused(focused);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// To avoid including proAdhoc.h, which includes a lot of stuff.
|
|
int GetChatMessageCount();
|
|
|
|
ViewLayoutMode EmuScreen::LayoutMode() const {
|
|
return ViewLayoutMode::ApplyInsets;
|
|
}
|
|
|
|
void EmuScreen::update() {
|
|
using namespace UI;
|
|
|
|
// This is where views are recreated.
|
|
UIScreen::update();
|
|
|
|
resumeButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR && Memory::MemFault_MayBeResumable() ? V_VISIBLE : V_GONE);
|
|
resetButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR ? V_VISIBLE : V_GONE);
|
|
backButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR ? V_VISIBLE : V_GONE);
|
|
|
|
if (chatButton_ && chatMenu_) {
|
|
if (chatMenu_->GetVisibility() != V_GONE) {
|
|
chatMessages_ = GetChatMessageCount();
|
|
newChatMessages_ = 0;
|
|
} else {
|
|
int diff = GetChatMessageCount() - chatMessages_;
|
|
// Cap the count at 50.
|
|
newChatMessages_ = diff > 50 ? 50 : diff;
|
|
}
|
|
}
|
|
|
|
// Simply forcibly update to the current screen size every frame. Doesn't cost much.
|
|
// If bounds is set to be smaller than the actual pixel resolution of the display, respect that.
|
|
// TODO: Should be able to use g_dpi_scale here instead. Might want to store the dpi scale in the UI context too.
|
|
|
|
#ifndef _WIN32
|
|
const Bounds &bounds = screenManager()->getUIContext()->GetBounds();
|
|
PSP_CoreParameter().pixelWidth = g_display.pixel_xres * bounds.w / g_display.dp_xres;
|
|
PSP_CoreParameter().pixelHeight = g_display.pixel_yres * bounds.h / g_display.dp_yres;
|
|
#endif
|
|
|
|
if (PSP_IsInited()) {
|
|
UpdateUIState(coreState != CORE_RUNTIME_ERROR ? UISTATE_INGAME : UISTATE_EXCEPTION);
|
|
}
|
|
|
|
if (errorMessage_.size()) {
|
|
auto err = GetI18NCategory(I18NCat::ERRORS);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
std::string errLoadingFile = GetFriendlyPath(gamePath_) + "\n\n";
|
|
errLoadingFile.append(err->T("Error loading file", "Could not load game"));
|
|
errLoadingFile.append("\n");
|
|
errLoadingFile.append(errorMessage_);
|
|
|
|
// Can't really use the MessagePopupScreen here because we don't have a good looking background.
|
|
screenManager()->push(new PromptScreen(gamePath_, errLoadingFile, di->T("OK"), ""));
|
|
errorMessage_.clear();
|
|
quit_ = true;
|
|
return;
|
|
}
|
|
|
|
if (pauseTrigger_) {
|
|
pauseTrigger_ = false;
|
|
screenManager()->push(new GamePauseScreen(gamePath_, bootPending_));
|
|
}
|
|
|
|
if (!PSP_IsInited())
|
|
return;
|
|
|
|
double now = time_now_d();
|
|
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
g_controlMapper.UpdateConfig(config);
|
|
|
|
if (saveStatePreview_ && !bootPending_) {
|
|
int currentSlot = SaveState::GetCurrentSlot();
|
|
if (saveStateSlot_ != currentSlot) {
|
|
saveStateSlot_ = currentSlot;
|
|
|
|
const std::string gamePrefix = SaveState::GetGamePrefix(g_paramSFO);
|
|
|
|
Path fn;
|
|
if (SaveState::HasSaveInSlot(gamePrefix, currentSlot)) {
|
|
fn = SaveState::GenerateSaveSlotPath(gamePrefix, currentSlot, SaveState::SCREENSHOT_EXTENSION);
|
|
}
|
|
|
|
saveStatePreview_->SetFilename(fn);
|
|
if (!fn.empty()) {
|
|
saveStatePreview_->SetVisibility(UI::V_VISIBLE);
|
|
saveStatePreviewShownTime_ = now;
|
|
} else {
|
|
saveStatePreview_->SetVisibility(UI::V_GONE);
|
|
}
|
|
}
|
|
|
|
if (saveStatePreview_->GetVisibility() == UI::V_VISIBLE) {
|
|
double endTime = saveStatePreviewShownTime_ + 2.0;
|
|
float alpha = clamp_value((endTime - now) * 4.0, 0.0, 1.0);
|
|
saveStatePreview_->SetColor(colorAlpha(0x00FFFFFF, alpha));
|
|
|
|
if (now - saveStatePreviewShownTime_ > 2) {
|
|
saveStatePreview_->SetVisibility(UI::V_GONE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool EmuScreen::checkPowerDown() {
|
|
// This is for handling things like sceKernelExitGame().
|
|
// Also for REQUEST_STOP.
|
|
if (coreState == CORE_POWERDOWN && PSP_GetBootState() == BootState::Complete && !bootPending_) {
|
|
INFO_LOG(Log::System, "SELF-POWERDOWN!");
|
|
screenManager()->switchScreen(new MainScreen());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ScreenRenderRole EmuScreen::renderRole(bool isTop) const {
|
|
auto CanBeBackground = [&]() -> bool {
|
|
if (skipBufferEffects_) {
|
|
return isTop || (g_Config.bTransparentBackground && ShouldRunBehind());
|
|
}
|
|
|
|
if (!g_Config.bTransparentBackground && !isTop) {
|
|
if (ShouldRunBehind() || screenManager()->topScreen()->wantBrightBackground())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
if (!PSP_IsInited() && !bootPending_) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
ScreenRenderRole role = ScreenRenderRole::MUST_BE_FIRST;
|
|
if (CanBeBackground()) {
|
|
role |= ScreenRenderRole::CAN_BE_BACKGROUND;
|
|
}
|
|
return role;
|
|
}
|
|
|
|
void EmuScreen::darken() {
|
|
if (screenManager()->topScreen()->wantBrightBackground()) {
|
|
return;
|
|
}
|
|
|
|
UIContext &dc = *screenManager()->getUIContext();
|
|
uint32_t color = GetBackgroundColorWithAlpha(dc);
|
|
dc.Begin();
|
|
dc.RebindTexture();
|
|
dc.FillRect(UI::Drawable(color), dc.GetBounds());
|
|
dc.Flush();
|
|
}
|
|
|
|
void EmuScreen::HandleFlip() {
|
|
Achievements::FrameUpdate();
|
|
|
|
// This video dumping stuff is bad. Or at least completely broken with frameskip..
|
|
#ifndef MOBILE_DEVICE
|
|
if (g_Config.bDumpFrames && !startDumping_) {
|
|
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
|
avi.Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, sy->T("AVI Dump started."), 1.0f);
|
|
startDumping_ = true;
|
|
}
|
|
if (g_Config.bDumpFrames && startDumping_) {
|
|
avi.AddFrame();
|
|
} else if (!g_Config.bDumpFrames && startDumping_) {
|
|
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
|
avi.Stop();
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, sy->T("AVI Dump stopped."), 3.0f, "avi_dump");
|
|
Path lastFilename = avi.LastFilename();
|
|
g_OSD.SetClickCallback("avi_dump", [lastFilename]() {
|
|
System_ShowFileInFolder(lastFilename);
|
|
});
|
|
startDumping_ = false;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool EmuScreen::ShouldRunEmulation(ScreenRenderMode mode) const {
|
|
if (!(mode & ScreenRenderMode::TOP) && !ShouldRunBehind() && strcmp(screenManager()->topScreen()->tag(), "DevMenu") != 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ScreenRenderFlags EmuScreen::PreRender(ScreenRenderMode mode) {
|
|
// If a boot is in progress, update it.
|
|
ProcessGameBoot(gamePath_);
|
|
|
|
using namespace Draw;
|
|
skipBufferEffects_ = g_Config.bSkipBufferEffects;
|
|
if (!skipBufferEffects_ && PSP_IsInited()) {
|
|
if (ShouldRunEmulation(mode)) {
|
|
// We need to run emulation here, and perform all the normal render passes.
|
|
return RunEmulation(false);
|
|
} else if (gpu) {
|
|
// Need to check for gpu here, we might be in reset.
|
|
const DeviceOrientation orientation = GetDeviceOrientation();
|
|
const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(orientation);
|
|
// We run just the post shaders.
|
|
gpu->PrepareCopyDisplayToOutput(displayLayoutConfig);
|
|
// Screens on top like reporting might want to take screenshots of existing framebuffers.
|
|
ScreenshotNotifyPostGameRender(screenManager()->getDrawContext());
|
|
}
|
|
}
|
|
return ScreenRenderFlags::NONE;
|
|
}
|
|
|
|
ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) {
|
|
// Moved from update, because we want it to be possible for booting to happen even when the screen
|
|
// is in the background, like when choosing Reset from the pause menu.
|
|
|
|
using namespace Draw;
|
|
|
|
DrawContext *draw = screenManager()->getDrawContext();
|
|
if (!draw) {
|
|
return ScreenRenderFlags::NONE; // shouldn't really happen but I've seen a suspicious stack trace..
|
|
}
|
|
|
|
ScreenRenderFlags screenRenderFlags = ScreenRenderFlags::NONE;
|
|
|
|
if (mode & ScreenRenderMode::TOP) {
|
|
System_Notify(SystemNotification::KEEP_SCREEN_AWAKE);
|
|
}
|
|
|
|
const DeviceOrientation orientation = GetDeviceOrientation();
|
|
const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(orientation);
|
|
// We might have a bad viewport after RunEmulation, reset.
|
|
Viewport viewport{0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f};
|
|
|
|
if (!skipBufferEffects_ && !ShouldRunEmulation(mode)) {
|
|
if (gpu) {
|
|
gpu->CopyDisplayToOutput(displayLayoutConfig);
|
|
}
|
|
draw->SetViewport(viewport);
|
|
draw->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres);
|
|
darken();
|
|
return screenRenderFlags;
|
|
}
|
|
|
|
if (!PSP_IsInited() || readyToFinishBoot_) {
|
|
// It's possible this might be set outside PSP_RunLoopFor().
|
|
// In this case, we need to double check it here.
|
|
if (mode & ScreenRenderMode::TOP) {
|
|
checkPowerDown();
|
|
}
|
|
draw->SetViewport(viewport);
|
|
draw->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres);
|
|
renderUI();
|
|
return screenRenderFlags;
|
|
}
|
|
|
|
if (skipBufferEffects_) {
|
|
// In skip buffer effects mode, we run emulation *after* the backbuffer bind.
|
|
screenRenderFlags = RunEmulation(true);
|
|
}
|
|
|
|
draw->SetViewport(viewport);
|
|
|
|
ProcessQueuedVKeys();
|
|
|
|
const bool skipBufferEffects = skipBufferEffects_;
|
|
|
|
// Gotta copy the output at some point. Also this is where we take the screenshot if needed.
|
|
if (gpu) {
|
|
gpu->CopyDisplayToOutput(displayLayoutConfig);
|
|
}
|
|
|
|
// Reset the viewport. Needed in case Cardboard or something similar was enabled.
|
|
draw->SetViewport(viewport);
|
|
|
|
Draw::BackendState state = draw->GetCurrentBackendState();
|
|
|
|
if (!(mode & ScreenRenderMode::TOP)) {
|
|
renderImDebugger();
|
|
// We're in run-behind mode, but we don't want to draw chat, debug UI and stuff. We do draw the imdebugger though.
|
|
// So, darken and bail here.
|
|
// Reset viewport/scissor to be sure.
|
|
draw->SetViewport(viewport);
|
|
draw->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres);
|
|
darken();
|
|
return screenRenderFlags;
|
|
}
|
|
|
|
// NOTE: We don't check for powerdown if we're not the top screen.
|
|
checkPowerDown();
|
|
|
|
if (hasVisibleUI()) {
|
|
if (clearColor_) {
|
|
// This is used on the exception bluescreen for example.
|
|
draw->Clear(Draw::Aspect::COLOR_BIT, clearColor_, 0.0f, 0);
|
|
}
|
|
cardboardDisableButton_->SetVisibility(displayLayoutConfig.bEnableCardboardVR ? UI::V_VISIBLE : UI::V_GONE);
|
|
renderUI();
|
|
}
|
|
|
|
if (chatMenu_ && (chatMenu_->GetVisibility() == UI::V_VISIBLE)) {
|
|
SetVRAppMode(VRAppMode::VR_DIALOG_MODE);
|
|
} else {
|
|
SetVRAppMode(screenManager()->topScreen() == this ? VRAppMode::VR_GAME_MODE : VRAppMode::VR_DIALOG_MODE);
|
|
}
|
|
|
|
renderImDebugger();
|
|
return screenRenderFlags;
|
|
}
|
|
|
|
ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) {
|
|
using namespace Draw;
|
|
ScreenRenderFlags flags = ScreenRenderFlags::NONE;
|
|
|
|
g_OSD.NudgeIngameNotifications();
|
|
|
|
const DeviceOrientation orientation = GetDeviceOrientation();
|
|
const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(orientation);
|
|
__DisplaySetDisplayLayoutConfig(displayLayoutConfig);
|
|
|
|
DrawContext *draw = screenManager()->getDrawContext();
|
|
const Draw::Viewport viewport{0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f};
|
|
|
|
PSP_UpdateDebugStats((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS || g_Config.bLogFrameDrops);
|
|
clearColor_ = 0;
|
|
bool blockedExecution = Achievements::IsBlockingExecution();
|
|
if (!blockedExecution) {
|
|
// We process savestates before running the frame.
|
|
SaveState::Process();
|
|
|
|
if (gpu) {
|
|
gpu->BeginHostFrame(displayLayoutConfig);
|
|
}
|
|
|
|
// Freeze-frame functionality (loads a savestate on every frame).
|
|
if (PSP_CoreParameter().freezeNext) {
|
|
PSP_CoreParameter().frozen = true;
|
|
PSP_CoreParameter().freezeNext = false;
|
|
SaveState::SaveToRam(freezeState_);
|
|
} else if (PSP_CoreParameter().frozen) {
|
|
std::string errorString;
|
|
if (CChunkFileReader::ERROR_NONE != SaveState::LoadFromRam(freezeState_, &errorString)) {
|
|
ERROR_LOG(Log::SaveState, "Failed to load freeze state (%s). Unfreezing.", errorString.c_str());
|
|
PSP_CoreParameter().frozen = false;
|
|
}
|
|
}
|
|
|
|
PSP_RunLoopWhileState();
|
|
|
|
// Hopefully, after running, coreState is now CORE_NEXTFRAME
|
|
switch (coreState) {
|
|
case CORE_NEXTFRAME:
|
|
// Reached the end of the frame while running at full blast, all good. Set back to running for the next frame
|
|
coreState = frameStep_ ? CORE_STEPPING_CPU : CORE_RUNNING_CPU;
|
|
flags |= ScreenRenderFlags::HANDLED_THROTTLING;
|
|
break;
|
|
case CORE_STEPPING_CPU:
|
|
case CORE_STEPPING_GE:
|
|
case CORE_RUNTIME_ERROR:
|
|
{
|
|
// If there's an exception, display information.
|
|
const MIPSExceptionInfo &info = Core_GetExceptionInfo();
|
|
if (info.type != MIPSExceptionType::NONE) {
|
|
// Clear to blue background screen
|
|
bool dangerousSettings = !Reporting::IsSupported();
|
|
clearColor_ = dangerousSettings ? 0xFF900050 : 0xFF900000;
|
|
} else {
|
|
// If we're stepping, it's convenient not to clear the screen entirely, so we copy display to output.
|
|
// This won't work in non-buffered, but that's fine.
|
|
if (PSP_IsInited()) {
|
|
gpu->SetCurFramebufferDirty(true);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
// Didn't actually reach the end of the frame, ran out of the blockTicks cycles.
|
|
// In this case we need to bind and wipe the backbuffer, at least.
|
|
// It's possible we never ended up outputted anything - make sure we have the backbuffer cleared
|
|
// So, we don't set framebufferBound here.
|
|
|
|
// However, let's not cause a UI sleep in the mainloop.
|
|
flags |= ScreenRenderFlags::HANDLED_THROTTLING;
|
|
break;
|
|
}
|
|
|
|
if (gpu) {
|
|
// Run post processing and other passes.
|
|
gpu->PrepareCopyDisplayToOutput(displayLayoutConfig);
|
|
gpu->EndHostFrame();
|
|
|
|
// The right time to run this
|
|
if (ScreenshotNotifyPostGameRender(draw) && skipBufferEffects) {
|
|
// Need to restore the backbuffer render pass, it probably got destroyed.
|
|
draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer");
|
|
}
|
|
}
|
|
|
|
if (SaveState::PollRestartNeeded() && !bootPending_) {
|
|
Achievements::UnloadGame();
|
|
PSP_Shutdown(true);
|
|
|
|
// Restart the boot process
|
|
bootPending_ = true;
|
|
bootIsReset_ = true;
|
|
RecreateViews();
|
|
_dbg_assert_(coreState == CORE_POWERDOWN);
|
|
if (!PSP_InitStart(PSP_CoreParameter())) {
|
|
bootPending_ = false;
|
|
WARN_LOG(Log::Loader, "Error resetting");
|
|
screenManager()->switchScreen(new MainScreen());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (frameStep_) {
|
|
frameStep_ = false;
|
|
if (coreState == CORE_RUNNING_CPU) {
|
|
Core_Break(BreakReason::FrameAdvance, 0);
|
|
}
|
|
}
|
|
|
|
runImDebugger();
|
|
|
|
return flags;
|
|
}
|
|
|
|
void EmuScreen::runImDebugger() {
|
|
if (!lastImguiEnabled_ && g_Config.bShowImDebugger) {
|
|
#if !defined(MOBILE_DEVICE)
|
|
// On mobile devices (specifically iOS) we don't want to pop the keyboard
|
|
// on activating imgui. Instead, we should do it when a text edit field in imgui gets focus,
|
|
// although we'll still have ugly overlap problems.
|
|
System_NotifyUIEvent(UIEventNotification::TEXT_GOTFOCUS);
|
|
#endif
|
|
VERBOSE_LOG(Log::System, "activating keyboard");
|
|
} else if (lastImguiEnabled_ && !g_Config.bShowImDebugger) {
|
|
System_NotifyUIEvent(UIEventNotification::TEXT_LOSTFOCUS);
|
|
VERBOSE_LOG(Log::System, "deactivating keyboard");
|
|
}
|
|
lastImguiEnabled_ = g_Config.bShowImDebugger;
|
|
if (g_Config.bShowImDebugger) {
|
|
Draw::DrawContext *draw = screenManager()->getDrawContext();
|
|
if (!imguiInited_) {
|
|
// TODO: Do this only on demand.
|
|
IMGUI_CHECKVERSION();
|
|
ctx_ = ImGui::CreateContext();
|
|
|
|
ImGui_ImplPlatform_Init(GetSysDirectory(DIRECTORY_SYSTEM) / "imgui.ini");
|
|
imDebugger_ = std::make_unique<ImDebugger>();
|
|
|
|
// Read the TTF font
|
|
size_t propSize = 0;
|
|
const uint8_t *propFontData = g_VFS.ReadFile("Roboto_Condensed-Regular.ttf", &propSize);
|
|
size_t fixedSize = 0;
|
|
const uint8_t *fixedFontData = g_VFS.ReadFile("Inconsolata-Regular.ttf", &fixedSize);
|
|
// This call works even if fontData is nullptr, in which case the font just won't get loaded.
|
|
// This takes ownership of the font array.
|
|
ImGui_ImplThin3d_Init(draw, propFontData, propSize, fixedFontData, fixedSize);
|
|
imguiInited_ = true;
|
|
}
|
|
|
|
if (PSP_IsInited()) {
|
|
_dbg_assert_(imDebugger_);
|
|
|
|
ImGui_ImplPlatform_NewFrame();
|
|
ImGui_ImplThin3d_NewFrame(draw, ui_draw2d.GetDrawMatrix());
|
|
|
|
ImGui::NewFrame();
|
|
|
|
if (imCmd_.cmd != ImCmd::NONE) {
|
|
imDebugger_->PostCmd(imCmd_);
|
|
imCmd_.cmd = ImCmd::NONE;
|
|
}
|
|
|
|
// Update keyboard modifiers.
|
|
auto &io = ImGui::GetIO();
|
|
io.AddKeyEvent(ImGuiMod_Ctrl, keyCtrlLeft_ || keyCtrlRight_);
|
|
io.AddKeyEvent(ImGuiMod_Shift, keyShiftLeft_ || keyShiftRight_);
|
|
io.AddKeyEvent(ImGuiMod_Alt, keyAltLeft_ || keyAltRight_);
|
|
// io.AddKeyEvent(ImGuiMod_Super, e.key.super);
|
|
|
|
ImGuiID dockID = ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode | ImGuiDockNodeFlags_NoDockingOverCentralNode);
|
|
ImGuiDockNode* node = ImGui::DockBuilderGetCentralNode(dockID);
|
|
|
|
// Not elegant! But don't know how else to pass through the bounds, without making a mess.
|
|
Bounds centralNode(node->Pos.x, node->Pos.y, node->Size.x, node->Size.y);
|
|
SetOverrideScreenFrame(¢ralNode);
|
|
|
|
if (!io.WantCaptureKeyboard) {
|
|
// Draw a focus rectangle to indicate inputs will be passed through.
|
|
ImGui::GetBackgroundDrawList()->AddRect
|
|
(
|
|
node->Pos,
|
|
{ node->Pos.x + node->Size.x, node->Pos.y + node->Size.y },
|
|
IM_COL32(255, 255, 255, 90),
|
|
0.f,
|
|
ImDrawFlags_None,
|
|
1.f
|
|
);
|
|
}
|
|
imDebugger_->Frame(currentDebugMIPS, gpuDebug, draw);
|
|
|
|
// Convert to drawlists.
|
|
ImGui::Render();
|
|
}
|
|
}
|
|
}
|
|
|
|
void EmuScreen::renderImDebugger() {
|
|
if (g_Config.bShowImDebugger) {
|
|
Draw::DrawContext *draw = screenManager()->getDrawContext();
|
|
if (PSP_IsInited() && imDebugger_) {
|
|
ImGui_ImplThin3d_RenderDrawData(ImGui::GetDrawData(), draw);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool EmuScreen::hasVisibleUI() {
|
|
// Regular but uncommon UI.
|
|
if (saveStatePreview_->GetVisibility() != UI::V_GONE || loadingSpinner_->GetVisibility() == UI::V_VISIBLE)
|
|
return true;
|
|
if (!g_OSD.IsEmpty() || g_Config.bShowTouchControls || g_Config.iShowStatusFlags != 0)
|
|
return true;
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
if (config.bEnableCardboardVR || g_Config.bEnableNetworkChat)
|
|
return true;
|
|
if (g_Config.bShowGPOLEDs)
|
|
return true;
|
|
// Debug UI.
|
|
if ((DebugOverlay)g_Config.iDebugOverlay != DebugOverlay::OFF || g_Config.bShowDeveloperMenu)
|
|
return true;
|
|
|
|
// Exception information.
|
|
if (coreState == CORE_RUNTIME_ERROR || coreState == CORE_STEPPING_CPU) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void EmuScreen::renderUI() {
|
|
using namespace Draw;
|
|
|
|
DrawContext *draw = screenManager()->getDrawContext();
|
|
UIContext *ctx = screenManager()->getUIContext();
|
|
ctx->BeginFrame();
|
|
// This sets up some important states but not the viewport.
|
|
ctx->Begin();
|
|
|
|
if (root_) {
|
|
UI::LayoutViewHierarchy(*ctx, RootMargins(), root_, LayoutMode(), UseImmersiveMode());
|
|
root_->Draw(*ctx);
|
|
}
|
|
|
|
if (PSP_IsInited()) {
|
|
if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::CONTROL) {
|
|
DrawControlMapperOverlay(ctx, GetLayoutBounds(*ctx), g_controlMapper);
|
|
}
|
|
if (g_Config.iShowStatusFlags) {
|
|
DrawFPS(ctx, GetLayoutBounds(*ctx));
|
|
}
|
|
}
|
|
|
|
#ifdef USE_PROFILER
|
|
if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::FRAME_PROFILE && PSP_IsInited()) {
|
|
DrawProfile(*ctx);
|
|
}
|
|
#endif
|
|
|
|
if (g_Config.bShowGPOLEDs) {
|
|
// Draw a vertical strip of LEDs at the right side of the screen.
|
|
const float ledSize = 24.0f;
|
|
const float spacing = 4.0f;
|
|
const float height = 8 * ledSize + 7 * spacing;
|
|
const float x = ctx->GetBounds().w - spacing - ledSize;
|
|
const float y = (ctx->GetBounds().h - height) * 0.5f;
|
|
ctx->FillRect(UI::Drawable(0xFF000000), Bounds(x - spacing, y - spacing, ledSize + spacing * 2, height + spacing * 2));
|
|
for (int i = 0; i < 8; i++) {
|
|
int bit = (g_GPOBits >> i) & 1;
|
|
uint32_t color = 0xFF30FF30;
|
|
if (!bit) {
|
|
color = darkenColor(darkenColor(color));
|
|
}
|
|
Bounds ledBounds(x, y + (spacing + ledSize) * i, ledSize, ledSize);
|
|
ctx->FillRect(UI::Drawable(color), ledBounds);
|
|
}
|
|
ctx->Flush();
|
|
}
|
|
|
|
if (coreState == CORE_RUNTIME_ERROR || coreState == CORE_STEPPING_CPU) {
|
|
const MIPSExceptionInfo &info = Core_GetExceptionInfo();
|
|
if (info.type != MIPSExceptionType::NONE) {
|
|
DrawCrashDump(ctx, gamePath_);
|
|
} else {
|
|
// We're somehow in ERROR or STEPPING without a crash dump. This case is what lead
|
|
// to the bare "Resume" and "Reset" buttons without a crash dump before, in cases
|
|
// where we were unable to ignore memory errors.
|
|
}
|
|
}
|
|
|
|
ctx->Flush();
|
|
}
|
|
|
|
void EmuScreen::AutoLoadSaveState() {
|
|
if (autoLoadFailed_) {
|
|
return;
|
|
}
|
|
|
|
int autoSlot = -1;
|
|
|
|
std::string gamePrefix = SaveState::GetGamePrefix(g_paramSFO);
|
|
|
|
//check if save state has save, if so, load
|
|
switch (g_Config.iAutoLoadSaveState) {
|
|
case (int)AutoLoadSaveState::OFF: // "AutoLoad Off"
|
|
return;
|
|
case (int)AutoLoadSaveState::OLDEST: // "Oldest Save"
|
|
autoSlot = SaveState::GetOldestSlot(gamePrefix);
|
|
break;
|
|
case (int)AutoLoadSaveState::NEWEST: // "Newest Save"
|
|
autoSlot = SaveState::GetNewestSlot(gamePrefix);
|
|
break;
|
|
default: // try the specific save state slot specified
|
|
autoSlot = (SaveState::HasSaveInSlot(gamePrefix, g_Config.iAutoLoadSaveState - 3)) ? (g_Config.iAutoLoadSaveState - 3) : -1;
|
|
break;
|
|
}
|
|
|
|
if (g_Config.iAutoLoadSaveState && autoSlot != -1) {
|
|
SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) {
|
|
AfterSaveStateAction(status, message);
|
|
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
|
if (status == SaveState::Status::FAILURE) {
|
|
autoLoadFailed_ = true;
|
|
} else {
|
|
std::string msg = std::string(sy->T("Auto Load Savestate")) + ": " + StringFromFormat("%d", autoSlot);
|
|
g_OSD.Show(OSDType::MESSAGE_SUCCESS, msg, 2.0f, "autoload");
|
|
}
|
|
});
|
|
g_Config.iCurrentStateSlot = autoSlot;
|
|
}
|
|
}
|
|
|
|
void EmuScreen::resized() {
|
|
RecreateViews();
|
|
}
|
|
|
|
bool MustRunBehind() {
|
|
return IsNetworkConnected();
|
|
}
|
|
|
|
bool ShouldRunBehind() {
|
|
// Enforce run-behind if ad-hoc connected
|
|
return g_Config.bRunBehindPauseMenu || MustRunBehind();
|
|
}
|