From f97a3d09bd2d6f5dcae62f25344c0c46d995c1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 18 Feb 2026 11:24:28 +0100 Subject: [PATCH 1/4] Savedata: Fix rare crash bug found in Google Play reports --- Common/StringUtils.h | 5 +++++ Core/Dialog/SavedataParam.cpp | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Common/StringUtils.h b/Common/StringUtils.h index a7d518da23..d48ad12993 100644 --- a/Common/StringUtils.h +++ b/Common/StringUtils.h @@ -136,6 +136,11 @@ inline size_t truncate_cpy(char(&out)[Count], std::string_view src) { return truncate_cpy(out, Count, src); } +template +inline std::string_view StringViewFromFixedSizeField(const char(&field)[Count]) { + return std::string_view(field, strnlen(field, Count)); +} + inline std::string join(std::string_view a, std::string_view b) { std::string result; result.reserve(a.size() + b.size()); diff --git a/Core/Dialog/SavedataParam.cpp b/Core/Dialog/SavedataParam.cpp index f5f887cf33..2d65188a3f 100644 --- a/Core/Dialog/SavedataParam.cpp +++ b/Core/Dialog/SavedataParam.cpp @@ -96,7 +96,7 @@ namespace return result == dataSize; } - PSPFileInfo FileFromListing(const std::vector &listing, const std::string &filename) { + static PSPFileInfo FileFromListing(const std::vector &listing, std::string_view filename) { for (const PSPFileInfo &sub : listing) { if (sub.name == filename) return sub; @@ -1426,12 +1426,12 @@ bool SavedataParam::GetSize(SceUtilitySavedataParam *param) { s64 writeBytes = 0; for (int i = 0; i < param->sizeInfo->numNormalEntries; ++i) { const auto &entry = param->sizeInfo->normalEntries[i]; - overwriteBytes += FileFromListing(listing, entry.name).size; + overwriteBytes += FileFromListing(listing, StringViewFromFixedSizeField(entry.name)).size; writeBytes += entry.size; } for (int i = 0; i < param->sizeInfo->numSecureEntries; ++i) { const auto &entry = param->sizeInfo->secureEntries[i]; - overwriteBytes += FileFromListing(listing, entry.name).size; + overwriteBytes += FileFromListing(listing, StringViewFromFixedSizeField(entry.name)).size; writeBytes += entry.size + 0x10; } From 0dd77f896a70970057bc61b55ba4051a60951483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 18 Feb 2026 11:24:55 +0100 Subject: [PATCH 2/4] A few more string length safety fixes in SavedataParam.cpp --- Core/Dialog/SavedataParam.cpp | 37 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/Core/Dialog/SavedataParam.cpp b/Core/Dialog/SavedataParam.cpp index 2d65188a3f..c5273a889c 100644 --- a/Core/Dialog/SavedataParam.cpp +++ b/Core/Dialog/SavedataParam.cpp @@ -266,31 +266,22 @@ std::string SavedataParam::GetSaveFilePath(const SceUtilitySavedataParam *param, return GetSaveFilePath(param, GetSaveDir(param, saveId)); } -inline static std::string FixedToString(const char *str, size_t n) -{ - if (!str) { - return std::string(); - } else { - return std::string(str, strnlen(str, n)); - } -} - std::string SavedataParam::GetGameName(const SceUtilitySavedataParam *param) const { - return FixedToString(param->gameName, ARRAY_SIZE(param->gameName)); + return std::string(StringViewFromFixedSizeField(param->gameName)); } std::string SavedataParam::GetSaveName(const SceUtilitySavedataParam *param) const { - const std::string saveName = FixedToString(param->saveName, ARRAY_SIZE(param->saveName)); + const std::string_view saveName = StringViewFromFixedSizeField(param->saveName); if (saveName == "<>") return ""; - return saveName; + return std::string(saveName); } std::string SavedataParam::GetFileName(const SceUtilitySavedataParam *param) const { - return FixedToString(param->fileName, ARRAY_SIZE(param->fileName)); + return std::string(StringViewFromFixedSizeField(param->fileName)); } std::string SavedataParam::GetKey(const SceUtilitySavedataParam *param) const @@ -507,9 +498,9 @@ int SavedataParam::Save(SceUtilitySavedataParam* param, const std::string &saveD // Update values. NOTE! #18430 made this conditional on !subWrite, but this is not correct, as it causes #18687. // So now we do a hacky trick and just check for a valid title before we proceed with updating the sfoFile. if (strnlen(param->sfoParam.title, sizeof(param->sfoParam.title)) > 0) { - sfoFile->SetValue("TITLE", param->sfoParam.title, 128); - sfoFile->SetValue("SAVEDATA_TITLE", param->sfoParam.savedataTitle, 128); - sfoFile->SetValue("SAVEDATA_DETAIL", param->sfoParam.detail, 1024); + sfoFile->SetValue("TITLE", StringViewFromFixedSizeField(param->sfoParam.title), 128); + sfoFile->SetValue("SAVEDATA_TITLE", StringViewFromFixedSizeField(param->sfoParam.savedataTitle), 128); + sfoFile->SetValue("SAVEDATA_DETAIL", StringViewFromFixedSizeField(param->sfoParam.detail), 1024); sfoFile->SetValue("PARENTAL_LEVEL", param->sfoParam.parentalLevel, 4); sfoFile->SetValue("CATEGORY", "MS", 4); sfoFile->SetValue("SAVEDATA_DIRECTORY", GetSaveDir(param, saveDirName), 64); @@ -598,28 +589,24 @@ int SavedataParam::Save(SceUtilitySavedataParam* param, const std::string &saveD } // SAVE ICON0 - if (param->icon0FileData.buf.IsValid()) - { + if (param->icon0FileData.buf.IsValid()) { std::string icon0path = dirPath + "/" + ICON0_FILENAME; WritePSPFile(icon0path, param->icon0FileData.buf, param->icon0FileData.size); } // SAVE ICON1 - if (param->icon1FileData.buf.IsValid()) - { + if (param->icon1FileData.buf.IsValid()) { std::string icon1path = dirPath + "/" + ICON1_FILENAME; if (param->icon1FileData.size > 0) { WritePSPFile(icon1path, param->icon1FileData.buf, param->icon1FileData.size); } } // SAVE PIC1 - if (param->pic1FileData.buf.IsValid()) - { + if (param->pic1FileData.buf.IsValid()) { std::string pic1path = dirPath + "/" + PIC1_FILENAME; WritePSPFile(pic1path, param->pic1FileData.buf, param->pic1FileData.size); } // Save SND - if (param->snd0FileData.buf.IsValid()) - { + if (param->snd0FileData.buf.IsValid()) { std::string snd0path = dirPath + "/" + SND0_FILENAME; if (param->snd0FileData.size > 0) { WritePSPFile(snd0path, param->snd0FileData.buf, param->snd0FileData.size); @@ -1592,7 +1579,7 @@ int SavedataParam::SetPspParam(SceUtilitySavedataParam *param) continue; } - const std::string thisSaveName = FixedToString(saveNameListData[i], ARRAY_SIZE(saveNameListData[i])); + const std::string thisSaveName(StringViewFromFixedSizeField(saveNameListData[i])); const std::string folderName = gameName + thisSaveName; From 5b33918ac41f7b30f39260a88b5dcc33975e1a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 18 Feb 2026 11:35:55 +0100 Subject: [PATCH 3/4] VulkanBarrier: Change a hard assert to a debug assert, add fallback code. --- Common/GPU/Vulkan/VulkanBarrier.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Common/GPU/Vulkan/VulkanBarrier.cpp b/Common/GPU/Vulkan/VulkanBarrier.cpp index 8fc5665215..8a6bb0ef1a 100644 --- a/Common/GPU/Vulkan/VulkanBarrier.cpp +++ b/Common/GPU/Vulkan/VulkanBarrier.cpp @@ -88,7 +88,10 @@ void VulkanBarrierBatch::TransitionColorImageAuto( srcStageMask_ |= VK_PIPELINE_STAGE_TRANSFER_BIT; break; default: - _assert_msg_(false, "Unexpected oldLayout: %s", VulkanImageLayoutToString(*imageLayout)); + _dbg_assert_msg_(false, "Unexpected oldLayout: %s", VulkanImageLayoutToString(*imageLayout)); + // Sync hard. + srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_SHADER_READ_BIT; + srcStageMask_ = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; break; } @@ -115,7 +118,10 @@ void VulkanBarrierBatch::TransitionColorImageAuto( dstStageMask_ |= VK_PIPELINE_STAGE_TRANSFER_BIT; break; default: - _assert_msg_(false, "Unexpected newLayout: %s", VulkanImageLayoutToString(newImageLayout)); + _dbg_assert_msg_(false, "Unexpected newLayout: %s", VulkanImageLayoutToString(newImageLayout)); + // Sync hard. + dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_SHADER_READ_BIT; + dstStageMask_ = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; break; } @@ -167,7 +173,10 @@ void VulkanBarrierBatch::TransitionDepthStencilImageAuto( srcStageMask_ |= VK_PIPELINE_STAGE_TRANSFER_BIT; break; default: - _assert_msg_(false, "Unexpected oldLayout: %s", VulkanImageLayoutToString(*imageLayout)); + _dbg_assert_msg_(false, "Unexpected oldLayout: %s", VulkanImageLayoutToString(*imageLayout)); + // Sync hard. + srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_SHADER_READ_BIT; + srcStageMask_ = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; break; } @@ -189,7 +198,10 @@ void VulkanBarrierBatch::TransitionDepthStencilImageAuto( dstStageMask_ |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; break; default: - _assert_msg_(false, "Unexpected newLayout: %s", VulkanImageLayoutToString(newImageLayout)); + _dbg_assert_msg_(false, "Unexpected newLayout: %s", VulkanImageLayoutToString(newImageLayout)); + // Sync hard. + dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_SHADER_READ_BIT; + dstStageMask_ = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; break; } From 341e034f0b32bd7ab2c7dfdfc9a271ba8399fa56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 18 Feb 2026 11:54:13 +0100 Subject: [PATCH 4/4] Handle savedata files better on the game browser tab --- UI/MainScreen.cpp | 27 +++++++++-- UI/SavedataScreen.cpp | 102 +++++++++++++++++++----------------------- UI/SavedataScreen.h | 19 ++++++++ 3 files changed, 87 insertions(+), 61 deletions(-) diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 63cf9123fc..a4dee96819 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -85,12 +85,21 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons // Check if we already know that this game isn't playable. auto info = g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::FILE_TYPE); - if (info->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO) { + switch (info->fileType) { + case IdentifiedFileType::PSP_UMD_VIDEO_ISO: // We show info about it. screenManager->push(new GameScreen(path, false)); return; + case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: + { + // Show the savedata popup, why not? + std::string title = SanitizeString(info->GetTitle(), StringRestriction::NoLineBreaksOrSpecials, 0, 200); + screenManager->push(new SavedataPopupScreen(Path(), path, title)); + return; + } + default: + break; } - if (currentScreen) { screenManager->cancelScreensAbove(currentScreen); } @@ -398,9 +407,14 @@ void GameButton::Draw(UIContext &dc) { } if (gridStyle_ && g_Config.bShowIDOnGameIcon && ginfo->fileType != IdentifiedFileType::PSP_ELF && !ginfo->id_version.empty()) { + std::string_view idStr = ginfo->id_version; + if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { + auto ga = GetI18NCategory(I18NCat::GAME); + idStr = ga->T("SaveData"); + } dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); - dc.DrawText(ginfo->id_version, bounds_.x + 5, y + 1, 0xFF000000, ALIGN_TOPLEFT); - dc.DrawText(ginfo->id_version, bounds_.x + 4, y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 5, y + 1, 0xFF000000, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 4, y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); dc.SetFontScale(1.0f, 1.0f); } @@ -1728,6 +1742,11 @@ void MainScreen::dialogFinished(const Screen *dialog, DialogResult result) { } else if (tag == "Upload") { // Files may have been uploaded. RecreateViews(); + } else if (tag == "SavedataPopup") { + // We must have come from the file browser tab. + if (gameBrowsers_.size() >= 2) { + gameBrowsers_[1]->RequestRefresh(); + } } } diff --git a/UI/SavedataScreen.cpp b/UI/SavedataScreen.cpp index 98a9a0988b..1790271207 100644 --- a/UI/SavedataScreen.cpp +++ b/UI/SavedataScreen.cpp @@ -132,75 +132,63 @@ SavedataView::SavedataView(UIContext &dc, GameInfo *ginfo, IdentifiedFileType ty } } -class SavedataPopupScreen : public UI::PopupScreen { -public: - SavedataPopupScreen(Path gamePath, Path savePath, std::string_view title) : PopupScreen(StripSpaces(title)), savePath_(savePath), gamePath_(gamePath) { } +SavedataPopupScreen::SavedataPopupScreen(Path gamePath, Path savePath, std::string_view title) : PopupScreen(StripSpaces(title)), savePath_(savePath), gamePath_(gamePath) {} - const char *tag() const override { return "SavedataPopup"; } - void update() override { - PopupScreen::update(); - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); - if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { - // Hm, this is no good. But hopefully the previous screen loaded it. - return; - } - if (savedataView_) { - savedataView_->UpdateGame(ginfo.get()); - } +void SavedataPopupScreen::update() { + PopupScreen::update(); + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { + // Hm, this is no good. But hopefully the previous screen loaded it. + return; + } + if (savedataView_) { + savedataView_->UpdateGame(ginfo.get()); + } +} + +void SavedataPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { + using namespace UI; + UIContext &dc = *screenManager()->getUIContext(); + + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { + // This is OK, handled in Update. Though most likely, the previous screen loaded it. } - void CreatePopupContents(UI::ViewGroup *parent) override { - using namespace UI; - UIContext &dc = *screenManager()->getUIContext(); + ScrollView *contentScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, UI::Margins(0, 3))); + parent->Add(contentScroll); - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); - if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { - // This is OK, handled in Update. Though most likely, the previous screen loaded it. - } + // TODO: If the game info wasn't already loaded, we'll get a bogus fileType here. + savedataView_ = contentScroll->Add(new SavedataView(dc, ginfo.get(), ginfo->fileType, true)); - ScrollView *contentScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, UI::Margins(0, 3))); - parent->Add(contentScroll); + auto di = GetI18NCategory(I18NCat::DIALOG); - // TODO: If the game info wasn't already loaded, we'll get a bogus fileType here. - savedataView_ = contentScroll->Add(new SavedataView(dc, ginfo.get(), ginfo->fileType, true)); + LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + buttonRow->SetSpacing(0); + Margins buttonMargins(5, 5, 5, 13); // not sure why we need more at the bottom to make it look right + buttonRow->Add(new Choice(di->T("Back"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Handle(this, &UIScreen::OnBack); + buttonRow->Add(new Choice(di->T("Delete"), ImageID("I_TRASHCAN"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Add([this](UI::EventParams &e) { auto di = GetI18NCategory(I18NCat::DIALOG); + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GameInfoFlags::PARAM_SFO); - LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - buttonRow->SetSpacing(0); - Margins buttonMargins(5, 5, 5, 13); // not sure why we need more at the bottom to make it look right + const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN); - buttonRow->Add(new Choice(di->T("Back"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Handle(this, &UIScreen::OnBack); - buttonRow->Add(new Choice(di->T("Delete"), ImageID("I_TRASHCAN"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Add([this](UI::EventParams &e) { - auto di = GetI18NCategory(I18NCat::DIALOG); - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GameInfoFlags::PARAM_SFO); - - const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN); - - std::string_view confirmMessage = di->T("Are you sure you want to delete the file?"); - screenManager()->push(new MessagePopupScreen(di->T("Delete"), confirmMessage, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"), [=](bool result) { - if (result) { - ginfo->Delete(); - TriggerFinish(DR_NO); - } - })); + std::string_view confirmMessage = di->T("Are you sure you want to delete the file?"); + screenManager()->push(new MessagePopupScreen(di->T("Delete"), confirmMessage, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"), [=](bool result) { + if (result) { + ginfo->Delete(); + TriggerFinish(DR_NO); + } + })); + }); + if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) { + buttonRow->Add(new Choice(di->T("Show in folder"), ImageID("I_FOLDER"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Add([this](UI::EventParams &e) { + System_ShowFileInFolder(savePath_); }); - if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) { - buttonRow->Add(new Choice(di->T("Show in folder"), ImageID("I_FOLDER"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Add([this](UI::EventParams &e) { - System_ShowFileInFolder(savePath_); - }); - } - parent->Add(buttonRow); } - -protected: - UI::Size PopupWidth() const override { return 600; } - -private: - SavedataView *savedataView_ = nullptr; - Path savePath_; - Path gamePath_; -}; + parent->Add(buttonRow); +} class SortedLinearLayout : public UI::LinearLayoutList { public: diff --git a/UI/SavedataScreen.h b/UI/SavedataScreen.h index e0ac8efdb0..df519835c6 100644 --- a/UI/SavedataScreen.h +++ b/UI/SavedataScreen.h @@ -67,6 +67,25 @@ private: bool searchPending_ = false; }; +class SavedataView; + +class SavedataPopupScreen : public UI::PopupScreen { +public: + SavedataPopupScreen(Path gamePath, Path savePath, std::string_view title); + + const char *tag() const override { return "SavedataPopup"; } + void update() override; + void CreatePopupContents(UI::ViewGroup *parent) override; + +protected: + UI::Size PopupWidth() const override { return 600; } + +private: + SavedataView *savedataView_ = nullptr; + Path savePath_; + Path gamePath_; +}; + class SavedataScreen : public UITabbedBaseDialogScreen { public: // gamePath can be empty, in that case this screen will show all savedata in the save directory.