Files
ppsspp/Core/Dialog/PSPDialog.cpp
T
Henrik RydgårdandClaude Opus 5.5 41b5074a98 Utility: Release volatile memory on a forced shutdown, fix dialog savestates
- The Yugioh savedata workaround force-stopped the other dialog without
  releasing the volatile memory it held, so the savedata helper then waited
  for it forever.
- Screenshot's ShutdownStart and Update succeeded when no screenshot was
  running, putting it back into SHUTDOWN.
- Savestates: Netconf didn't keep its request address, and a DNS json
  download going on was gone after a load, so it could wait for it forever;
  it now fetches the json again (it's cached). NpSignin didn't keep its
  request address either. Both restart their timeouts instead of timing out
  at once. With states from before, they keep the current address as they
  used to.
- Loading a state from before NpSignin, GameSharing or HtmlViewer were saved
  resets them rather than keeping this session's state (including the
  HtmlViewer's memory block), and without Shutdown's side effects, which
  would write to the loaded memory and release its volatile lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 12:50:41 -06:00

559 lines
17 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 <algorithm>
#include "Common/Data/Text/I18n.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/StringUtils.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/CoreTiming.h"
#include "Core/Dialog/PSPDialog.h"
#include "Core/HLE/sceCtrl.h"
#include "Core/HLE/sceKernelThread.h"
#include "Core/HLE/scePower.h"
#include "Core/HLE/sceUtility.h"
#include "Core/MemMapHelpers.h"
#include "Core/Util/PPGeDraw.h"
// In seconds. On a PSP, dialog animations advance by animSpeed frames per Update and take about
// this long.
#define FADE_TIME 0.2
constexpr float FONT_SCALE = 0.55f;
const char *UtilityDialogTypeToString(UtilityDialogType type) {
switch (type) {
case UtilityDialogType::NONE: return "NONE";
case UtilityDialogType::SAVEDATA: return "SAVEDATA";
case UtilityDialogType::MSG: return "MSG";
case UtilityDialogType::OSK: return "OSK";
case UtilityDialogType::NET: return "NET";
case UtilityDialogType::SCREENSHOT: return "SCREENSHOT";
case UtilityDialogType::GAMESHARING: return "GAMESHARING";
case UtilityDialogType::GAMEDATAINSTALL: return "GAMEDATAINSTALL";
case UtilityDialogType::NPSIGNIN: return "NPSIGNIN";
case UtilityDialogType::HTMLVIEWER: return "HTMLVIEWER";
default: return "(unknown)";
}
}
const char *UtilityDialogStatusToString(PSPDialog::DialogStatus status) {
switch (status) {
case PSPDialog::SCE_UTILITY_STATUS_NONE: return "STATUS_NONE";
case PSPDialog::SCE_UTILITY_STATUS_INITIALIZE: return "STATUS_INITIALIZE";
case PSPDialog::SCE_UTILITY_STATUS_RUNNING: return "STATUS_RUNNING";
case PSPDialog::SCE_UTILITY_STATUS_FINISHED: return "STATUS_FINISHED";
case PSPDialog::SCE_UTILITY_STATUS_SHUTDOWN: return "STATUS_SHUTDOWN";
case PSPDialog::SCE_UTILITY_STATUS_SCREENSHOT_UNKNOWN: return "STATUS_SCREENSHOT_UNKNOWN";
default: return "(unknown)";
}
}
void PSPDialog::InitCommon() {
UpdateCommon();
if (const pspUtilityDialogCommon *common = GetCommonParam()) {
// How these compare with the calling thread decides what the game sees from GetStatus right
// after InitStart/ShutdownStart.
DEBUG_LOG(Log::sceUtility, "%s thread priorities: graphics=%d access=%d font=%d sound=%d (caller %d)",
UtilityDialogTypeToString(DialogType()), common->graphicsThread, common->accessThread,
common->fontThread, common->soundThread, KernelCurThreadPriority());
}
if (GetCommonParam() && GetCommonParam()->language != GetPSPLanguage()) {
WARN_LOG(Log::sceUtility, "Game requested language %d, ignoring and using user language", GetCommonParam()->language);
}
}
void PSPDialog::UpdateCommon() {
okButtonImg = ImageID("I_CIRCLE");
cancelButtonImg = ImageID("I_CROSS");
okButtonFlag = CTRL_CIRCLE;
cancelButtonFlag = CTRL_CROSS;
if (GetCommonParam() && GetCommonParam()->buttonSwap == 1) {
okButtonImg = ImageID("I_CROSS");
cancelButtonImg = ImageID("I_CIRCLE");
okButtonFlag = CTRL_CROSS;
cancelButtonFlag = CTRL_CIRCLE;
}
}
void PSPDialog::UpdatePendingStatus() {
if (pendingStatusTicks != 0 && CoreTiming::GetTicks(currentMIPS) >= pendingStatusTicks) {
bool changeAllowed = true;
if (pendingStatus == SCE_UTILITY_STATUS_NONE && status == SCE_UTILITY_STATUS_SHUTDOWN) {
FinishVolatile();
} else if (pendingStatus == SCE_UTILITY_STATUS_RUNNING && status == SCE_UTILITY_STATUS_INITIALIZE) {
if (!volatileLocked_ && LocksVolatileMemory()) {
volatileLocked_ = KernelVolatileMemLock(0, 0, 0) == 0;
changeAllowed = volatileLocked_;
}
}
if (changeAllowed) {
status = pendingStatus;
pendingStatusTicks = 0;
}
}
}
PSPDialog::DialogStatus PSPDialog::GetStatus() {
UpdatePendingStatus();
PSPDialog::DialogStatus retval = status;
if (UseAutoStatus()) {
if (status == SCE_UTILITY_STATUS_SHUTDOWN) {
FinishVolatile();
status = SCE_UTILITY_STATUS_NONE;
}
if (status == SCE_UTILITY_STATUS_INITIALIZE)
status = SCE_UTILITY_STATUS_RUNNING;
}
return retval;
}
bool PSPDialog::IsBusy() {
UpdatePendingStatus();
// An auto status dialog in SHUTDOWN is only waiting for the game to see that (FinishAutoShutdown).
if (status == SCE_UTILITY_STATUS_SHUTDOWN && UseAutoStatus()) {
return false;
}
return status != SCE_UTILITY_STATUS_NONE;
}
void PSPDialog::FinishAutoShutdown() {
if (status == SCE_UTILITY_STATUS_SHUTDOWN && UseAutoStatus()) {
FinishVolatile();
status = SCE_UTILITY_STATUS_NONE;
}
}
void PSPDialog::ChangeStatus(DialogStatus newStatus, int delayUs) {
if (delayUs <= 0) {
if (newStatus == SCE_UTILITY_STATUS_NONE && status == SCE_UTILITY_STATUS_SHUTDOWN) {
FinishVolatile();
} else if (newStatus == SCE_UTILITY_STATUS_RUNNING && status == SCE_UTILITY_STATUS_INITIALIZE) {
if (!volatileLocked_ && LocksVolatileMemory()) {
// TODO: Should probably make the status pending instead?
volatileLocked_ = KernelVolatileMemLock(0, 0, 0) == 0;
}
}
status = newStatus;
pendingStatus = newStatus;
pendingStatusTicks = 0;
} else {
pendingStatus = newStatus;
pendingStatusTicks = CoreTiming::GetTicks(currentMIPS) + usToCycles(delayUs);
}
}
void PSPDialog::FinishVolatile() {
if (!volatileLocked_)
return;
if (KernelVolatileMemUnlock(0) == 0) {
volatileLocked_ = false;
// Simulate modifications to volatile memory.
Memory::Memset(PSP_GetVolatileMemoryStart(), 0, PSP_GetVolatileMemoryEnd() - PSP_GetVolatileMemoryStart());
}
}
int PSPDialog::FinishInit() {
// The thread has locked volatile memory. An auto status dialog may be past INITIALIZE already,
// and must still let go of it on shutdown.
if (ReadStatus() == SCE_UTILITY_STATUS_NONE) {
KernelVolatileMemUnlock(0);
return -1;
}
volatileLocked_ = true;
if (ReadStatus() == SCE_UTILITY_STATUS_INITIALIZE)
ChangeStatus(SCE_UTILITY_STATUS_RUNNING, 0);
return 0;
}
int PSPDialog::FinishShutdown() {
if (ReadStatus() != SCE_UTILITY_STATUS_SHUTDOWN)
return -1;
ChangeStatus(SCE_UTILITY_STATUS_NONE, 0);
return 0;
}
void PSPDialog::ChangeStatusInit(int delayUs) {
ChangeStatus(SCE_UTILITY_STATUS_INITIALIZE, 0);
auto params = GetCommonParam();
if (params)
UtilityDialogInitialize(DialogType(), delayUs, params->accessThread, params->graphicsThread);
else
ChangeStatus(SCE_UTILITY_STATUS_RUNNING, delayUs);
}
void PSPDialog::ChangeStatusShutdown(int delayUs) {
// If we're doing shutdown right away and skipped start, we don't run the dialog thread.
bool skipDialogShutdown = status == SCE_UTILITY_STATUS_NONE && pendingStatus == SCE_UTILITY_STATUS_NONE;
ChangeStatus(SCE_UTILITY_STATUS_SHUTDOWN, 0);
auto params = GetCommonParam();
if (params && !skipDialogShutdown)
UtilityDialogShutdown(DialogType(), delayUs, params->accessThread, params->graphicsThread);
else
ChangeStatus(SCE_UTILITY_STATUS_NONE, delayUs);
}
void PSPDialog::StartDraw()
{
PPGeBegin();
PPGeDrawRect(0, 0, 480, 272, CalcFadedColor(0x20000000));
}
void PSPDialog::EndDraw()
{
PPGeEnd();
}
int PSPDialog::Shutdown(bool force)
{
if (force) {
ChangeStatus(SCE_UTILITY_STATUS_NONE, 0);
} else {
ChangeStatus(SCE_UTILITY_STATUS_SHUTDOWN, 0);
}
return 0;
}
void PSPDialog::StartFade(bool fadeIn_)
{
isFading = true;
fadeTimer = 0;
fadeIn = fadeIn_;
}
void PSPDialog::UpdateFade(int animSpeed) {
if (isFading) {
fadeTimer += animSpeed / 60.0f;
if (fadeTimer < FADE_TIME) {
if (fadeIn)
fadeValue = (u32) (fadeTimer / FADE_TIME * 255);
else
fadeValue = 255 - (u32) (fadeTimer / FADE_TIME * 255);
} else {
fadeValue = (fadeIn ? 255 : 0);
isFading = false;
if (!fadeIn) {
FinishFadeOut();
}
}
}
}
void PSPDialog::FinishFadeOut() {
ChangeStatus(SCE_UTILITY_STATUS_FINISHED, 0);
}
u32 PSPDialog::CalcFadedColor(u32 inColor) const {
u32 alpha = inColor >> 24;
alpha = alpha * fadeValue / 255;
return (inColor & 0x00FFFFFF) | (alpha << 24);
}
void PSPDialog::ResetState() {
status = SCE_UTILITY_STATUS_NONE;
pendingStatus = SCE_UTILITY_STATUS_NONE;
pendingStatusTicks = 0;
volatileLocked_ = false;
isFading = false;
fadeValue = 0;
}
void PSPDialog::DoState(PointerWrap &p) {
auto s = p.Section("PSPDialog", 1, 3);
if (!s)
return;
Do(p, status);
Do(p, lastButtons);
Do(p, buttons);
Do(p, fadeTimer);
Do(p, isFading);
Do(p, fadeIn);
Do(p, fadeValue);
// I don't think we should save these two... Let's just ignore them for now for compat.
int okButtonImg = 0;
Do(p, okButtonImg);
int cancelButtonImg = 0;
Do(p, cancelButtonImg);
Do(p, okButtonFlag);
Do(p, cancelButtonFlag);
if (s >= 2) {
Do(p, pendingStatus);
Do(p, pendingStatusTicks);
} else {
pendingStatusTicks = 0;
}
if (s >= 3) {
Do(p, volatileLocked_);
} else {
volatileLocked_ = false;
}
}
void PSPDialog::UpdateButtons()
{
lastButtons = __CtrlPeekButtons();
buttons = __CtrlReadLatch();
}
// Input is fine while fading in, but not while fading out: the choice has already been made.
bool PSPDialog::IsFadingOut() const {
return isFading && !fadeIn;
}
bool PSPDialog::IsButtonPressed(int checkButton)
{
return !IsFadingOut() && (buttons & checkButton);
}
bool PSPDialog::IsButtonHeld(int checkButton, int &framesHeld, int framesHeldThreshold, int framesHeldRepeatRate)
{
bool btnWasHeldLastFrame = (lastButtons & checkButton) && (__CtrlPeekButtons() & checkButton);
if (!IsFadingOut() && btnWasHeldLastFrame) {
framesHeld++;
}
else {
framesHeld = 0;
return false;
}
// It's considered held for dialog purposes after 30 frames (~0.5 seconds),
// and set to repeat every 10 frames, by default.
if (framesHeld >= framesHeldThreshold && ((framesHeld % framesHeldRepeatRate) == 0))
return true;
return false;
}
PPGeStyle PSPDialog::FadedStyle(PPGeAlign align, float scale) {
PPGeStyle textStyle;
textStyle.align = align;
textStyle.scale = scale;
textStyle.color = CalcFadedColor(textStyle.color);
textStyle.hasShadow = true;
textStyle.shadowColor = CalcFadedColor(textStyle.shadowColor);
return textStyle;
}
PPGeImageStyle PSPDialog::FadedImageStyle() {
PPGeImageStyle style;
style.color = CalcFadedColor(style.color);
return style;
}
void PSPDialog::DisplayButtons(int flags, std::string_view caption) {
bool useCaption = false;
char safeCaption[65] = {0};
if (!caption.empty()) {
useCaption = true;
truncate_cpy(safeCaption, sizeof(safeCaption), caption);
}
PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_LEFT, FONT_SCALE);
auto di = GetI18NCategory(I18NCat::DIALOG);
float x1 = 183.5f, x2 = 261.5f;
const pspUtilityDialogCommon *commonParams = GetCommonParam();
if (!commonParams) {
return;
}
if (commonParams->buttonSwap == 1) {
x1 = 261.5f;
x2 = 183.5f;
}
if (flags & DS_BUTTON_OK) {
std::string_view text = useCaption ? safeCaption : di->T("Enter");
PPGeDrawImage(okButtonImg, x2, 256, 11.5f, 11.5f, textStyle);
PPGeDrawText(text, x2 + 14.5f, 252, textStyle);
}
if (flags & DS_BUTTON_CANCEL) {
std::string_view text = useCaption ? safeCaption : di->T("Back");
PPGeDrawImage(cancelButtonImg, x1, 256, 11.5f, 11.5f, textStyle);
PPGeDrawText(text, x1 + 14.5f, 252, textStyle);
}
}
int PSPDialog::CheckRequest(u32 addr, std::initializer_list<u32> sizes) {
if (!Memory::IsValidRange(addr, sizeof(pspUtilityDialogCommon))) {
return SCE_ERROR_UTILITY_INVALID_ADDRESS;
}
const u32 size = Memory::ReadUnchecked_U32(addr);
if (std::find(sizes.begin(), sizes.end(), size) == sizes.end()) {
return SCE_ERROR_UTILITY_INVALID_PARAM_SIZE;
}
if (!Memory::IsValidRange(addr, size)) {
return SCE_ERROR_UTILITY_INVALID_ADDRESS;
}
return 0;
}
int PSPDialog::GetConfirmButton() {
if (PSP_CoreParameter().compat.flags().ForceCircleButtonConfirm) {
return CTRL_CIRCLE;
}
return g_Config.iButtonPreference == PSP_SYSTEMPARAM_BUTTON_CROSS ? CTRL_CROSS : CTRL_CIRCLE;
}
int PSPDialog::GetCancelButton() {
if (PSP_CoreParameter().compat.flags().ForceCircleButtonConfirm) {
return CTRL_CROSS;
}
return g_Config.iButtonPreference == PSP_SYSTEMPARAM_BUTTON_CROSS ? CTRL_CIRCLE : CTRL_CROSS;
}
void PSPDialog::DisplayMessage2(std::string_view text1, std::string_view text2a, std::string_view text2b, std::string_view text3a, std::string_view text3b, bool hasYesNo, bool hasOK) {
auto di = GetI18NCategory(I18NCat::DIALOG);
PPGeStyle buttonStyle = FadedStyle(PPGeAlign::BOX_CENTER, FONT_SCALE);
PPGeStyle messageStyle = FadedStyle(PPGeAlign::BOX_HCENTER, FONT_SCALE);
PPGeStyle messageStyleRight = FadedStyle(PPGeAlign::BOX_RIGHT, FONT_SCALE);
PPGeStyle messageStyleLeft = FadedStyle(PPGeAlign::BOX_LEFT, FONT_SCALE);
std::string text2 = std::string(text2a) + " " + std::string(text2b);
std::string text3 = std::string(text3a) + " " + std::string(text3b);
// Without the scrollbar, we have 350 total pixels.
float WRAP_WIDTH = 300.0f;
if (UTF8StringNonASCIICount(text1) >= (int)text1.size() / 4) {
WRAP_WIDTH = 336.0f;
if (text1.size() > 12) {
messageStyle.scale = 0.6f;
}
}
float totalHeight1 = 0.0f;
PPGeMeasureText(nullptr, &totalHeight1, text1, FONT_SCALE, PPGE_LINE_WRAP_WORD, WRAP_WIDTH);
float totalHeight2 = 0.0f;
if (text2 != " ")
PPGeMeasureText(nullptr, &totalHeight2, text2, FONT_SCALE, PPGE_LINE_USE_ELLIPSIS, WRAP_WIDTH);
float totalHeight3 = 0.0f;
if (text3 != " ")
PPGeMeasureText(nullptr, &totalHeight3, text3, FONT_SCALE, PPGE_LINE_USE_ELLIPSIS, WRAP_WIDTH);
float marginTop = 0.0f;
if (text2 != " " || text3 != " ")
marginTop = 11.0f;
float totalHeight = totalHeight1 + totalHeight2 + totalHeight3 + marginTop;
// The PSP normally only shows about 8 lines at a time.
// For improved UX, we intentionally show part of the next line.
float visibleHeight = std::min(totalHeight, 175.0f);
float h2 = visibleHeight / 2.0f;
float centerY = 135.0f;
float sy = centerY - h2 - 15.0f;
float ey = centerY + h2 + 20.0f;
float buttonY = centerY + h2 + 5.0f;
auto drawSelectionBoxAndAdjust = [&](float x) {
// Box has a fixed size.
float w = 15.0f;
float h = 8.0f;
PPGeDrawRect(x - w, buttonY - h, x + w, buttonY + h, CalcFadedColor(0x6DCFCFCF));
centerY -= h + 5.0f;
sy -= h + 5.0f;
ey = buttonY + h * 2.0f + 5.0f;
};
if (hasYesNo) {
if (yesnoChoice == 1) {
drawSelectionBoxAndAdjust(204.0f);
} else {
drawSelectionBoxAndAdjust(273.0f);
}
PPGeDrawText(di->T("Yes"), 203.0f, buttonY - 1.0f, buttonStyle);
PPGeDrawText(di->T("No"), 272.0f, buttonY - 1.0f, buttonStyle);
if (IsButtonPressed(CTRL_LEFT) && yesnoChoice == 0) {
yesnoChoice = 1;
} else if (IsButtonPressed(CTRL_RIGHT) && yesnoChoice == 1) {
yesnoChoice = 0;
}
buttonY += 8.0f + 5.0f;
}
if (hasOK) {
drawSelectionBoxAndAdjust(240.0f);
PPGeDrawText(di->T("OK"), 239.0f, buttonY - 1.0f, buttonStyle);
buttonY += 8.0f + 5.0f;
}
PPGeScissor(0, (int)(centerY - h2 - 2), 480, (int)(centerY + h2 + 2));
PPGeDrawTextWrapped(text1, 240.0f, centerY - h2 - scrollPos_, WRAP_WIDTH, 0, messageStyle);
if (!text2a.empty()) {
if (!text2b.empty())
PPGeDrawTextWrapped(text2a, 240.0f - 5.0f, centerY - h2 - scrollPos_ + totalHeight1 + marginTop, WRAP_WIDTH, 0, messageStyleRight);
else
PPGeDrawTextWrapped(text2a, 240.0f, centerY - h2 - scrollPos_ + totalHeight1 + marginTop, WRAP_WIDTH, 0, messageStyle);
}
if (!text2b.empty())
PPGeDrawTextWrapped(text2b, 240.0f + 5.0f, centerY - h2 - scrollPos_ + totalHeight1 + marginTop, WRAP_WIDTH, 0, messageStyleLeft);
if (!text3a.empty()) {
if (!text3b.empty())
PPGeDrawTextWrapped(text3a, 240.0f - 5.0f, centerY - h2 - scrollPos_ + totalHeight1 + totalHeight2 + marginTop, WRAP_WIDTH, 0, messageStyleRight);
else
PPGeDrawTextWrapped(text3a, 240.0f, centerY - h2 - scrollPos_ + totalHeight1 + totalHeight2 + marginTop, WRAP_WIDTH, 0, messageStyle);
}
if (!text3b.empty())
PPGeDrawTextWrapped(text3b, 240.0f + 5.0f, centerY - h2 - scrollPos_ + totalHeight1 + totalHeight2 + marginTop, WRAP_WIDTH, 0, messageStyleLeft);
PPGeScissorReset();
// Do we need a scrollbar?
if (visibleHeight < totalHeight) {
float scrollSpeed = 5.0f;
float scrollMax = totalHeight - visibleHeight;
float bobHeight = (visibleHeight / totalHeight) * visibleHeight;
float bobOffset = (scrollPos_ / scrollMax) * (visibleHeight - bobHeight);
float bobY1 = centerY - h2 + bobOffset;
PPGeDrawRect(415.0f, bobY1, 420.0f, bobY1 + bobHeight, CalcFadedColor(0xFFCCCCCC));
auto buttonDown = [this](int btn, int& held) {
if (IsButtonPressed(btn)) {
held = 0;
return true;
}
return IsButtonHeld(btn, held, 1, 1);
};
if (buttonDown(CTRL_DOWN, framesDownHeld_) && scrollPos_ < scrollMax) {
scrollPos_ = std::min(scrollMax, scrollPos_ + scrollSpeed);
}
if (buttonDown(CTRL_UP, framesUpHeld_) && scrollPos_ > 0.0f) {
scrollPos_ = std::max(0.0f, scrollPos_ - scrollSpeed);
}
}
PPGeDrawRect(60.0f, sy, 420.0f, sy + 1.0f, CalcFadedColor(0xFFFFFFFF));
PPGeDrawRect(60.0f, ey, 420.0f, ey + 1.0f, CalcFadedColor(0xFFFFFFFF));
}