mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-06 12:45:21 +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
1946 lines
63 KiB
C++
1946 lines
63 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/System/NativeApp.h"
|
|
#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/UI/ScreenManager.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/sceKernelThread.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 "Core/HLE/__sceAudio.h"
|
|
#include "Core/HW/Display.h"
|
|
|
|
#include "UI/BackgroundAudio.h"
|
|
#include "UI/GamepadEmu.h"
|
|
#include "UI/PauseScreen.h"
|
|
#include "UI/LoadStateConfirmScreen.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"
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
|
|
#include "Windows/MainWindow.h"
|
|
#endif
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
static AVIDump avi;
|
|
#endif
|
|
|
|
extern bool g_TakeScreenshot;
|
|
|
|
// 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;
|
|
}
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
|
|
// TODO: We need to somehow handle exit callbacks here, too.
|
|
|
|
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());
|
|
}
|
|
|
|
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;
|
|
}
|
|
// Give the game a chance to run its registered exit callback before tearing things down.
|
|
// If the dispatch took, we keep running emulation; ActionAfterExitCallback (or a direct
|
|
// sceKernelExitGame from inside the callback) flips coreState to CORE_POWERDOWN, and
|
|
// checkPowerDown() then switches us to MainScreen as usual. Pressing stop again while
|
|
// the callback is in flight, or stopping a non-running core, falls through to immediate shutdown.
|
|
if (coreState == CORE_RUNNING_CPU && !__KernelIsExitCallbackPending() && __KernelInvokeRegisteredExitCallback()) {
|
|
INFO_LOG(Log::Loader, "REQUEST_GAME_STOP: running exit callback before shutdown.");
|
|
} else {
|
|
// No callback (or already in flight, or core not running) - 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, std::string_view metadata) {
|
|
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->ClearJitCacheDeferred();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
std::lock_guard<std::mutex> lock(queuedVirtKeysLock_);
|
|
queuedVirtKeys_.push_back(std::make_pair(virtualKeyCode, down));
|
|
}
|
|
|
|
void EmuScreen::ProcessQueuedVKeys() {
|
|
std::vector<std::pair<VirtKey, bool>> dequeued;
|
|
{
|
|
std::lock_guard<std::mutex> lock(queuedVirtKeysLock_);
|
|
dequeued = std::move(queuedVirtKeys_);
|
|
queuedVirtKeys_.clear();
|
|
}
|
|
|
|
for (auto iter : dequeued) {
|
|
ProcessVKey(iter.first, iter.second);
|
|
}
|
|
}
|
|
|
|
// Synchronized processing of virtkeys.
|
|
void EmuScreen::ProcessVKey(VirtKey virtKey, bool down) {
|
|
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.
|
|
if (down) {
|
|
pauseTrigger_ = true;
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_SCREENSHOT:
|
|
if (down) {
|
|
TakeUserScreenshot();
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_TILT:
|
|
if (down) {
|
|
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 (down) {
|
|
if (g_Config.bEnableNetworkChat && !g_Config.bShowImDebugger) {
|
|
UI::EventParams e{};
|
|
g_controlMapper.ForceReleaseVKey(VIRTKEY_OPENCHAT);
|
|
OpenChat(true);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_AXIS_SWAP_TOGGLE:
|
|
if (down) {
|
|
g_controlMapper.ToggleSwapAxes();
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, mc->T("AxisSwap")); // best string we have.
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_DEVMENU:
|
|
if (down) {
|
|
UI::EventParams e{};
|
|
OnDevMenu.Trigger(e);
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_MOUSE:
|
|
if (down) {
|
|
g_Config.bMouseControl = !g_Config.bMouseControl;
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TEXTURE_DUMP:
|
|
if (down) {
|
|
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:
|
|
if (down) {
|
|
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:
|
|
if (down) {
|
|
g_Config.bEnableSound = !g_Config.bEnableSound;
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_SCREEN_ROTATION_VERTICAL:
|
|
if (down) {
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_VERTICAL;
|
|
}
|
|
break;
|
|
case VIRTKEY_SCREEN_ROTATION_VERTICAL180:
|
|
if (down) {
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_VERTICAL180;
|
|
}
|
|
break;
|
|
case VIRTKEY_SCREEN_ROTATION_HORIZONTAL:
|
|
if (down) {
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_HORIZONTAL;
|
|
}
|
|
break;
|
|
case VIRTKEY_SCREEN_ROTATION_HORIZONTAL180:
|
|
if (down) {
|
|
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
|
|
config.iInternalScreenRotation = ROTATION_LOCKED_HORIZONTAL180;
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_WLAN:
|
|
if (down) {
|
|
// 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:
|
|
if (down) {
|
|
// TODO: Limit to platforms that can support fullscreen.
|
|
g_Config.bFullScreen = !g_Config.bFullScreen;
|
|
System_ApplyFullscreenState();
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_TOGGLE_TOUCH_CONTROLS:
|
|
if (down) {
|
|
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 (down) {
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
|
if (SaveState::CanRewind()) {
|
|
SaveState::Rewind(&ShowMessageAfterSaveStateAction);
|
|
} else {
|
|
g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_PAUSE_NO_MENU:
|
|
if (down) {
|
|
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 (down) {
|
|
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(di->T("Exit"), confirmExitMessage, di->T("Yes"), di->T("No"), [](bool result) {
|
|
if (result) {
|
|
System_ExitApp();
|
|
}
|
|
}));
|
|
} else {
|
|
System_ExitApp();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VIRTKEY_FRAME_ADVANCE:
|
|
if (down) {
|
|
// 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 (down) {
|
|
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:
|
|
if (down) {
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_RESET);
|
|
}
|
|
break;
|
|
|
|
#ifndef MOBILE_DEVICE
|
|
case VIRTKEY_RECORD:
|
|
if (down) {
|
|
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 (down) {
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
|
SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &ShowMessageAfterSaveStateAction);
|
|
}
|
|
}
|
|
break;
|
|
case VIRTKEY_LOAD_STATE:
|
|
if (down) {
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
|
if (g_Config.bConfirmLoadState) {
|
|
std::string prefix = SaveState::GetGamePrefix(g_paramSFO);
|
|
int slot = g_Config.iCurrentStateSlot;
|
|
screenManager()->push(new LoadStateConfirmScreen(prefix, slot, [prefix, slot](bool result) {
|
|
if (result) {
|
|
SaveState::LoadSlot(prefix, slot, &ShowMessageAfterSaveStateAction);
|
|
}
|
|
}));
|
|
} else {
|
|
SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &ShowMessageAfterSaveStateAction);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case VIRTKEY_PREVIOUS_SLOT:
|
|
if (down) {
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
|
SaveState::PrevSlot();
|
|
System_PostUIMessage(UIMessage::SAVESTATE_DISPLAY_SLOT);
|
|
}
|
|
}
|
|
break;
|
|
case VIRTKEY_NEXT_SLOT:
|
|
if (down) {
|
|
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
|
SaveState::NextSlot();
|
|
System_PostUIMessage(UIMessage::SAVESTATE_DISPLAY_SLOT);
|
|
}
|
|
}
|
|
break;
|
|
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:
|
|
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;
|
|
}
|
|
|
|
InputMode EmuScreen::PassInputToMapper() const {
|
|
const bool chatMenuOpen = chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE;
|
|
if (chatMenuOpen) {
|
|
return InputMode::None;
|
|
}
|
|
|
|
return InputMode::Keyboard | InputMode::Mouse | InputMode::Other | InputMode::ImDebuggerToggle;
|
|
}
|
|
|
|
bool EmuScreen::key(const KeyInput &key) {
|
|
bool retval = UIScreen::key(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;
|
|
}
|
|
|
|
bool EmuScreen::touch(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 && !ignoreGamepad) {
|
|
// This just prevents the gamepad from timing out.
|
|
GamepadTouch();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
return UIScreen::touch(touch);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
void EmuScreen::deviceRestored(Draw::DrawContext *draw) {
|
|
UIScreen::deviceRestored(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_.empty()) {
|
|
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 (g_controlMapper.PollPauseTrigger()) {
|
|
pauseTrigger_ = true;
|
|
}
|
|
|
|
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 = [this, isTop]() -> 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)) {
|
|
// 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;
|
|
}
|
|
|
|
bool EmuScreen::AllowFocusMovement() const {
|
|
if (chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE) {
|
|
return true;
|
|
}
|
|
// We have just a game controller and possibly a chat and debug button. Don't allow focus movement.
|
|
return false;
|
|
}
|
|
|
|
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). It's a savestate load
|
|
// like any other, so it's banned in hardcore mode - and checked here rather than only
|
|
// where it's toggled, since hardcore mode can come up after the fact, once the game
|
|
// has been identified.
|
|
if (Achievements::HardcoreModeActive()) {
|
|
PSP_CoreParameter().freezeNext = false;
|
|
PSP_CoreParameter().frozen = false;
|
|
} else 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);
|
|
}
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
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, std::string_view metadata) {
|
|
ShowMessageAfterSaveStateAction(status, message, metadata);
|
|
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();
|
|
}
|