From 25392ef6fd9ddd9ac605521cefc792ab40f30c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 23 Feb 2026 16:38:14 +0100 Subject: [PATCH 1/8] Add a workaround for boot-up crashes caused by switching away from the app while waiting for the boot-up thread to finish --- Core/System.cpp | 4 ++++ Core/System.h | 3 +++ UI/EmuScreen.cpp | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/Core/System.cpp b/Core/System.cpp index e0a16c0a42..a1895f53f0 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -720,6 +720,10 @@ BootState PSP_Init(const CoreParameter &coreParam, std::string *error_string) { } } +BootState PollBootState() { + return g_bootState; +} + void PSP_Shutdown(bool success) { // Reduce the risk for weird races with the Windows GE debugger. gpuDebug = nullptr; diff --git a/Core/System.h b/Core/System.h index 9b66e6f12e..a5dd39489d 100644 --- a/Core/System.h +++ b/Core/System.h @@ -75,6 +75,9 @@ BootState PSP_InitUpdate(std::string *error_string); // Returns either BootState::Complete or BootState::Failed. BootState PSP_Init(const CoreParameter &coreParam, std::string *error_string); +// The main thread needs to block on this in some situations, like losing the surface. +BootState PollBootState(); + void PSP_Shutdown(bool success); FileLoader *PSP_LoadedFile(); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index e113ff808f..85136e6a5e 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1368,6 +1368,13 @@ void EmuScreen::CreateViews() { } void EmuScreen::deviceLost() { + // If we are currently in the middle of boot, we have to block here! + // Otherwise the boot thread will encounter draw_ == nullptr and weird stuff like that. + // We're doing this in a very ugly way for now. + while (PollBootState() == BootState::Booting) { + sleep_ms(100, "device-lost-during-boot"); + } + UIScreen::deviceLost(); if (imguiInited_) { From 5dc169ecbec3c2d63e67a2e46d5e5233f4ed43ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 23 Feb 2026 20:46:14 +0100 Subject: [PATCH 2/8] Fix more race conditions if you exit during game bootup --- Core/System.cpp | 3 +++ GPU/Common/FramebufferManagerCommon.cpp | 2 +- UI/EmuScreen.cpp | 17 ++++++++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Core/System.cpp b/Core/System.cpp index a1895f53f0..9d16624c05 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -636,6 +636,9 @@ bool PSP_InitStart(const CoreParameter &coreParam) { } } + // Use this to test exit-during-boot and other exceptional cases. + // sleep_ms(6000, "test"); + g_CoreParameter.fileType = fileType; // TODO: The reason we pass in g_CoreParameter.errorString here is that it's persistent - diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 6e099732f1..b30b64178b 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -2874,7 +2874,7 @@ void FramebufferManagerCommon::NotifyRenderResized(const DisplayLayoutConfig &co PSP_CoreParameter().renderHeight = h; PSP_CoreParameter().renderScaleFactor = scaleFactor; - if (UpdateRenderSize(msaaLevel)) { + if (draw_ && UpdateRenderSize(msaaLevel)) { draw_->StopThreads(); DestroyAllFBOs(); draw_->StartThreads(); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 85136e6a5e..1836555efa 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -460,13 +460,20 @@ EmuScreen::~EmuScreen() { std::string gameID = g_paramSFO.GetValueString("DISC_ID"); g_Config.TimeTracker().Stop(gameID); - // Should not be able to quit during boot, as boot can't be cancelled. - _dbg_assert_(!bootPending_); - if (!bootPending_) { - Achievements::UnloadGame(); - PSP_Shutdown(true); + if (bootPending_) { + // We probably quit during boot, got blocked in lostdevice, and then didn't end up in update again to call PSP_InitUpdate. + // So we need to finish and join the boot thread before we can exit. + _dbg_assert_(PollBootState() != BootState::Booting); + // Make sure we join the boot thread, by calling PSP_InitUpdate. + std::string error_string = "(unknown error)"; + PSP_InitUpdate(&error_string); + ERROR_LOG(Log::G3D, "Quit during boot, not good. %s", error_string.c_str()); + bootPending_ = false; } + Achievements::UnloadGame(); + PSP_Shutdown(true); + // If achievements are disabled in the global config, let's shut it down here. if (!g_Config.bAchievementsEnable) { Achievements::Shutdown(); From 22f2e69d0ab28d052901f4fee4041009a2adb7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 24 Feb 2026 00:16:10 +0100 Subject: [PATCH 3/8] Integrate the exit reminder (you haven't saved..) in warnings from setting that require restart Part of #21223 --- UI/GameSettingsScreen.cpp | 12 +++++++++++- UI/PauseScreen.cpp | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 2dad8dae3e..7748b63167 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -1744,9 +1744,19 @@ void TriggerRestart(const char *why, bool editThenRestore, const Path &gamePath) System_RestartApp(param); } +// From PauseScreen +std::string GetConfirmExitMessage(); + void GameSettingsScreen::TriggerRestartOrDo(std::function callback) { auto di = GetI18NCategory(I18NCat::DIALOG); - screenManager()->push(new UI::MessagePopupScreen(di->T("Restart"), di->T("Changing this setting requires PPSSPP to restart."), di->T("Restart"), di->T("Cancel"), [=](bool yes) { + + std::string confirmExitMessage = GetConfirmExitMessage(); + if (!confirmExitMessage.empty()) { + confirmExitMessage = "\n\n" + confirmExitMessage; + } + confirmExitMessage = std::string(di->T("Changing this setting requires PPSSPP to restart.")) + confirmExitMessage; + + screenManager()->push(new UI::MessagePopupScreen(di->T("Restart"), confirmExitMessage, di->T("Restart"), di->T("Cancel"), [=](bool yes) { if (yes) { TriggerRestart("GameSettingsScreen::RenderingBackendYes", editGameSpecificThenRestore_, gamePath_); } else { diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 98336a6c87..05447bbc2e 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -782,6 +782,10 @@ int GetUnsavedProgressSeconds() { std::string GetConfirmExitMessage() { std::string confirmMessage; + if (!PSP_IsInited()) { + return std::string(); + } + int unsavedSeconds = GetUnsavedProgressSeconds(); // If RAIntegration has dirty info, ask for confirmation. From 7ea2265ff329d5ee0250d3be7ebbdfb07b1398ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 20 Feb 2026 10:08:07 +0100 Subject: [PATCH 4/8] Make the undo save/load buttons not scroll with the savestate list --- Common/UI/ScrollView.cpp | 17 ++++++----- Common/UI/ScrollView.h | 5 ++++ UI/PauseScreen.cpp | 65 +++++++++++++++++++++------------------- UI/PauseScreen.h | 6 +--- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/Common/UI/ScrollView.cpp b/Common/UI/ScrollView.cpp index 87505dcbbc..2e093d2029 100644 --- a/Common/UI/ScrollView.cpp +++ b/Common/UI/ScrollView.cpp @@ -55,6 +55,9 @@ void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec ver if (layoutParams_->height == WRAP_CONTENT) MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_); } + + // The below seems misguided and leads to wrong sized scrollviews. Need to investigate if it's needed somewhere... + /* if (orientation_ == ORIENT_VERTICAL && vert.type != EXACTLY) { float bestHeight = std::max(views_[0]->GetMeasuredHeight(), views_[0]->GetBounds().h); if (vert.type == AT_MOST) @@ -63,7 +66,7 @@ void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec ver if (measuredHeight_ < bestHeight && layoutParams_->height < 0.0f) { measuredHeight_ = bestHeight; } - } + }*/ } } @@ -276,8 +279,8 @@ void ScrollView::Draw(UIContext &dc) { // If not anchored at the top of the screen exactly, and not scrolled to the top, // draw a subtle drop shadow to indicate scrollability. - const float darkness = 0.4f; - if (bounds_.y > 0.0f && orientation_ == ORIENT_VERTICAL) { + constexpr float darkness = 0.4f; + if (shadows_ && bounds_.y > 0.0f && orientation_ == ORIENT_VERTICAL) { float radius = 20.0f; Bounds shadowBounds = bounds_; @@ -292,8 +295,8 @@ void ScrollView::Draw(UIContext &dc) { } // Same at the bottom. - float y2 = dc.GetLayoutBounds().y2(); - if (bounds_.y2() < y2 && orientation_ == ORIENT_VERTICAL) { + const float y2 = dc.GetLayoutBounds().y2(); + if (shadows_ && bounds_.y2() < y2 && orientation_ == ORIENT_VERTICAL) { float radius = 20.0f; Bounds shadowBounds = bounds_; @@ -483,10 +486,10 @@ float ScrollView::ClampedScrollPos(float pos) { float maxPull = bounds_.h * 0.1f; if (pos < 0.0f) { float dist = std::min(-pos * (1.0f / bounds_.h), 1.0f); - pull_ = -(sqrt(dist) * maxPull); + pull_ = -(sqrtf(dist) * maxPull); } else if (pos > scrollMax) { float dist = std::min((pos - scrollMax) * (1.0f / bounds_.h), 1.0f); - pull_ = sqrt(dist) * maxPull; + pull_ = sqrtf(dist) * maxPull; } else { pull_ = 0.0f; } diff --git a/Common/UI/ScrollView.h b/Common/UI/ScrollView.h index 8b491f09da..4a5020a805 100644 --- a/Common/UI/ScrollView.h +++ b/Common/UI/ScrollView.h @@ -44,6 +44,10 @@ public: alignOpposite_ = alignOpposite; } + void SetShadows(bool shadows) { + shadows_ = shadows; + } + NeighborResult FindScrollNeighbor(View *view, const Point2D &target, FocusDirection direction, NeighborResult best) override; private: @@ -81,6 +85,7 @@ private: bool alignOpposite_ = false; bool draggingBob_ = false; bool mouseHover_ = false; + bool shadows_ = true; float barDragStart_ = 0.0f; float barDragOffset_ = 0.0f; diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 05447bbc2e..cc08a91225 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -380,7 +380,7 @@ void GamePauseScreen::OnVKey(VirtKey virtualKeyCode, bool down) { } } -void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems) { +void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, UI::LinearLayout **extraRow) { auto pa = GetI18NCategory(I18NCat::PAUSE); using namespace UI; @@ -407,23 +407,39 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems) RecreateViews(); }); } - leftColumnItems->Add(new Spacer(0.0)); - LinearLayout *buttonRow = leftColumnItems->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(Margins(10, 0, 0, 0)))); + *extraRow = nullptr; + + LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(Margins(10, 0, 0, 0))); if (g_Config.bEnableStateUndo && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) { - UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"), ImageID("I_NAVIGATE_BACK"))); + UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); loadUndoButton->SetEnabled(SaveState::HasUndoLoad(saveStatePrefix_)); - loadUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLoadUndo); + loadUndoButton->OnClick.Add([this](UI::EventParams &e) { + SaveState::UndoLoad(saveStatePrefix_, &AfterSaveStateAction); + TriggerFinish(DR_CANCEL); + }); - UI::Choice *saveUndoButton = buttonRow->Add(new Choice(pa->T("Undo last save"), ImageID("I_NAVIGATE_BACK"))); + UI::Choice *saveUndoButton = buttonRow->Add(new Choice(pa->T("Undo last save"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); saveUndoButton->SetEnabled(SaveState::HasUndoLastSave(saveStatePrefix_)); - saveUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLastSaveUndo); + saveUndoButton->OnClick.Add([this](UI::EventParams &e) { + SaveState::UndoLastSave(saveStatePrefix_); + RecreateViews(); + }); } if (g_Config.iRewindSnapshotInterval > 0 && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) { - UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"), ImageID("I_REWIND"))); + UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"), ImageID("I_REWIND"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); rewindButton->SetEnabled(SaveState::CanRewind()); - rewindButton->OnClick.Handle(this, &GamePauseScreen::OnRewind); + rewindButton->OnClick.Add([this](UI::EventParams &e) { + SaveState::Rewind(&AfterSaveStateAction); + TriggerFinish(DR_CANCEL); + }); + } + + if (buttonRow->GetNumSubviews() == 0) { + delete buttonRow; + } else { + *extraRow = buttonRow; } } @@ -476,8 +492,11 @@ void GamePauseScreen::CreateViews() { }); } - ViewGroup *saveStateScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f, scrollMargins)); - root_->Add(saveStateScroll); + ViewGroup *saveScrollContainer = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f, scrollMargins)); + root_->Add(saveScrollContainer); + + ScrollView *saveStateScroll = saveScrollContainer->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f))); + saveStateScroll->SetShadows(false); LinearLayout *saveDataScrollItems = new LinearLayoutList(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); saveStateScroll->Add(saveDataScrollItems); @@ -547,7 +566,11 @@ void GamePauseScreen::CreateViews() { System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/troubleshooting/save-state-time-warps"); }); } - CreateSavestateControls(saveDataScrollItems); + LinearLayout *extraRow = nullptr; + CreateSavestateControls(saveDataScrollItems, &extraRow); + if (extraRow) { + saveScrollContainer->Add(extraRow); + } } else { // Let's show the active challenges. std::set ids = Achievements::GetActiveChallengeIDs(); @@ -846,24 +869,6 @@ void GamePauseScreen::OnReportFeedback(UI::EventParams &e) { screenManager()->push(new ReportScreen(gamePath_)); } -void GamePauseScreen::OnRewind(UI::EventParams &e) { - SaveState::Rewind(&AfterSaveStateAction); - - TriggerFinish(DR_CANCEL); -} - -void GamePauseScreen::OnLoadUndo(UI::EventParams &e) { - SaveState::UndoLoad(saveStatePrefix_, &AfterSaveStateAction); - - TriggerFinish(DR_CANCEL); -} - -void GamePauseScreen::OnLastSaveUndo(UI::EventParams &e) { - SaveState::UndoLastSave(saveStatePrefix_); - - RecreateViews(); -} - void GamePauseScreen::OnCreateConfig(UI::EventParams &e) { std::shared_ptr info = g_gameInfoCache->GetInfo(NULL, gamePath_, GameInfoFlags::PARAM_SFO); if (info->Ready(GameInfoFlags::PARAM_SFO)) { diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 60fe05fcf4..cc94f34680 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -49,16 +49,12 @@ protected: void OnVKey(VirtKey virtualKeyCode, bool down) override; private: - void CreateSavestateControls(UI::LinearLayout *viewGroup); + void CreateSavestateControls(UI::LinearLayout *viewGroup, UI::LinearLayout **extraRow); void OnGameSettings(UI::EventParams &e); void OnExit(UI::EventParams &e); void OnReportFeedback(UI::EventParams &e); - void OnRewind(UI::EventParams &e); - void OnLoadUndo(UI::EventParams &e); - void OnLastSaveUndo(UI::EventParams &e); - void OnCreateConfig(UI::EventParams &e); void OnDeleteConfig(UI::EventParams &e); From a3878c2148e9bd558f9449fef9ef715bc304ddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 24 Feb 2026 00:26:17 +0100 Subject: [PATCH 5/8] Update libchdr to latest --- ext/libchdr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/libchdr b/ext/libchdr index 8bba7745d7..7891cbee2e 160000 --- a/ext/libchdr +++ b/ext/libchdr @@ -1 +1 @@ -Subproject commit 8bba7745d758627258b315997a860039244cedaf +Subproject commit 7891cbee2e8d7967bf8b6c24f091d2c5a53d8cfc From f85902d161ae8c78f2defc2b587ac242a1d7f172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 24 Feb 2026 00:26:39 +0100 Subject: [PATCH 6/8] Some minor code style cleanup --- UI/Theme.cpp | 25 ++++++++++++++----------- UI/Theme.h | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/UI/Theme.cpp b/UI/Theme.cpp index 9ab5cc0b59..acd85e18de 100644 --- a/UI/Theme.cpp +++ b/UI/Theme.cpp @@ -88,29 +88,32 @@ static void LoadThemeInfo(const std::vector &directories) { themeInfos.push_back(info); }; - for (size_t d = 0; d < directories.size(); d++) { + for (const auto &d : directories) { std::vector fileInfo; - g_VFS.GetFileListing(directories[d].c_str(), &fileInfo, "ini:"); + g_VFS.GetFileListing(d.c_str(), &fileInfo, "ini:"); if (fileInfo.empty()) { - File::GetFilesInDir(directories[d], &fileInfo, "ini:"); + File::GetFilesInDir(d, &fileInfo, "ini:"); } - for (size_t f = 0; f < fileInfo.size(); f++) { + for (const auto &f : fileInfo) { IniFile ini; bool success = false; - if (fileInfo[f].isDirectory) + if (f.isDirectory) { continue; + } - Path name = fileInfo[f].fullName; - Path path = directories[d]; + Path name(f.fullName); + Path path = d; // Hack around Android VFS path bug. really need to redesign this. - if (name.ToString().substr(0, 7) == "assets/") + if (name.ToString().substr(0, 7) == "assets/") { name = Path(name.ToString().substr(7)); - if (path.ToString().substr(0, 7) == "assets/") + } + if (path.ToString().substr(0, 7) == "assets/") { path = Path(path.ToString().substr(7)); + } - if (ini.LoadFromVFS(g_VFS, name.ToString()) || ini.Load(fileInfo[f].fullName)) { + if (ini.LoadFromVFS(g_VFS, name.ToString()) || ini.Load(f.fullName)) { success = true; } @@ -245,7 +248,7 @@ void UpdateTheme() { ui_theme.popupSliderFocusedColor = themeInfo.uPopupSliderFocusedColor; } -UI::Theme *GetTheme() { +const UI::Theme *GetTheme() { return &ui_theme; } diff --git a/UI/Theme.h b/UI/Theme.h index cae6d1e95a..50fd82c506 100644 --- a/UI/Theme.h +++ b/UI/Theme.h @@ -27,4 +27,4 @@ void ReloadAllThemeInfo(); std::vector GetThemeInfoNames(); void UpdateTheme(); -UI::Theme *GetTheme(); +const UI::Theme *GetTheme(); From 733aee685daaada1f7bb1482caf0ed64b9077ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 24 Feb 2026 00:33:10 +0100 Subject: [PATCH 7/8] Fix window resizing issue on pause screen --- UI/EmuScreen.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 1836555efa..579b305012 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1648,11 +1648,15 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { 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; } @@ -1663,6 +1667,8 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { if (mode & ScreenRenderMode::TOP) { checkPowerDown(); } + draw->SetViewport(viewport); + draw->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres); renderUI(); return screenRenderFlags; } @@ -1672,8 +1678,6 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { screenRenderFlags = RunEmulation(true); } - // 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}; draw->SetViewport(viewport); ProcessQueuedVKeys(); From 31e032353aa038bfdec4bbb32e4e24e83f977d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 24 Feb 2026 10:45:54 +0100 Subject: [PATCH 8/8] Android: Make the native text input popup much nicer to use Thanks Gemini. --- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 330b3876e7..d8e94af9ad 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -48,6 +48,7 @@ import android.view.SurfaceView; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; @@ -1501,26 +1502,24 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList } private AlertDialog.Builder createDialogBuilderWithDeviceThemeAndUiVisibility() { - AlertDialog.Builder bld = new AlertDialog.Builder(this, AlertDialog.THEME_DEVICE_DEFAULT_DARK); - bld.setOnDismissListener(dialog -> updateSystemUiVisibility()); - return bld; + return new AlertDialog.Builder(this, AlertDialog.THEME_DEVICE_DEFAULT_DARK); } @RequiresApi(Build.VERSION_CODES.M) private AlertDialog.Builder createDialogBuilderNew() { - AlertDialog.Builder bld = new AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Dialog_Alert); - bld.setOnDismissListener(dialog -> updateSystemUiVisibility()); - return bld; + return new AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Dialog_Alert); + } + + private AlertDialog.Builder createDialogBuilder() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return createDialogBuilderNew(); + } else { + return createDialogBuilderWithDeviceThemeAndUiVisibility(); + } } // The return value is sent to C++ via requestID. public void inputBox(final int requestId, final String title, String defaultText, String defaultAction) { - // Workaround for issue #13363 to fix Split/Second game start - if (isVRDevice()) { - NativeApp.sendRequestResult(requestId, false, defaultText, 0); - return; - } - final FrameLayout fl = new FrameLayout(this); final EditText input = new EditText(this); input.setGravity(Gravity.CENTER); @@ -1530,39 +1529,52 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList fl.addView(input, editBoxLayout); input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setImeOptions(EditorInfo.IME_ACTION_DONE); input.setText(defaultText); + input.setFocusableInTouchMode(true); + input.requestFocus(); + input.selectAll(); + //input.setSelection(input.getText().length()); - AlertDialog.Builder bld; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - bld = createDialogBuilderNew(); - } else { - bld = createDialogBuilderWithDeviceThemeAndUiVisibility(); - } - - AlertDialog.Builder builder = bld + AlertDialog.Builder builder = createDialogBuilder() .setView(fl) .setTitle(title) .setPositiveButton(defaultAction, (d, which) -> { Log.i(TAG, "input box successful"); NativeApp.sendRequestResult(requestId, true, input.getText().toString(), 0); - d.dismiss(); // It's OK that this will cause an extra dismiss message. It'll be ignored since the request number has already been processed. + // Dismiss happens automatically. }) .setNegativeButton("Cancel", (d, which) -> { - Log.i(TAG, "input box cancelled"); - NativeApp.sendRequestResult(requestId, false, "", 0); d.cancel(); }); - builder.setOnDismissListener(d -> { + builder.setOnDismissListener( d -> { Log.i(TAG, "input box dismissed"); + // This will be ignored if we already sent a success. NativeApp.sendRequestResult(requestId, false, "", 0); updateSystemUiVisibility(); }); - AlertDialog dlg = builder.create(); + AlertDialog dlg = builder.create(); + input.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + Log.i(TAG, "input box successful via Keyboard Done"); + NativeApp.sendRequestResult(requestId, true, input.getText().toString(), 0); + + // We must dismiss the dialog manually here + dlg.dismiss(); + return true; // Consume the event + } + return false; + }); dlg.setCancelable(true); + Window wnd = dlg.getWindow(); + if (wnd != null) { + wnd.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } try { dlg.show(); + input.requestFocus(); } catch (Exception e) { NativeApp.reportException(e, "AlertDialog"); } @@ -1711,6 +1723,12 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList title = param[1]; if (param.length > 2) defString = param[2]; + // Workaround for issue #13363 to fix Split/Second game start - it requires text input + // but we don't support it on VR devices. + if (isVRDevice()) { + NativeApp.sendRequestResult(requestID, false, defString, 0); + return true; + } Log.i(TAG, "Launching inputbox: #" + requestID + " " + title + " " + defString); inputBox(requestID, title, defString, "OK"); return true;