From ac8440e35dd7ab345f87d5e47f4965c65b7d101d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 26 Oct 2026 15:00:41 +0100 Subject: [PATCH 1/3] Settings: Don't keep the draw and UI contexts in page callbacks Android replaces both when it loses the surface (a recreate, which changing the display resolution does, or switching away), but the settings screen and its views live on. The external display setting's enabled check, which runs on every draw, then called into the deleted DrawContext ("Pure virtual function called!"). The UI scale and "Reload UI atlas" handlers had the same problem with the UIContext. Look them up through the screen manager when needed instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- UI/DeveloperSettingsPages.cpp | 6 +++--- UI/Settings/GeneralPage.cpp | 6 +++--- UI/Settings/GraphicsPage.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/UI/DeveloperSettingsPages.cpp b/UI/DeveloperSettingsPages.cpp index 56531eb815..55ba3a65fc 100644 --- a/UI/DeveloperSettingsPages.cpp +++ b/UI/DeveloperSettingsPages.cpp @@ -403,9 +403,9 @@ static void BuildUIPage(SettingsPage *page) { using namespace UI; SettingsPageHost *host = page->Host(); - UIContext *uiContext = host->PageScreenManager()->getUIContext(); - page->Choice("Reload UI atlas").OnClick([uiContext](UI::EventParams &) { - uiContext->InvalidateAtlas(); + // Looked up when clicked, the UI context is recreated along with the graphics. + page->Choice("Reload UI atlas").OnClick([host](UI::EventParams &) { + host->PageScreenManager()->getUIContext()->InvalidateAtlas(); }); // TODO: Move most of these strings out of the SysInfo category. diff --git a/UI/Settings/GeneralPage.cpp b/UI/Settings/GeneralPage.cpp index 9f548f13bf..9664efbadc 100644 --- a/UI/Settings/GeneralPage.cpp +++ b/UI/Settings/GeneralPage.cpp @@ -154,11 +154,11 @@ static void BuildUISettingsPage(SettingsPage *page) { using namespace UI; SettingsPageHost *host = page->Host(); - UIContext *ctx = host->PageScreenManager()->getUIContext(); - page->Setting(&g_Config.iUIScaleFactor).OnChange([ctx](UI::EventParams &e) { + // The UI context is looked up when used, since it's recreated along with the graphics. + page->Setting(&g_Config.iUIScaleFactor).OnChange([host](UI::EventParams &e) { const float dpiMul = UIScaleFactorToMultiplier(g_Config.iUIScaleFactor); g_display.Recalculate(-1, -1, -1, -1, dpiMul); - ctx->InvalidateAtlas(); + host->PageScreenManager()->getUIContext()->InvalidateAtlas(); NativeResized(); }); diff --git a/UI/Settings/GraphicsPage.cpp b/UI/Settings/GraphicsPage.cpp index 00e2b65232..d6c36a5ca4 100644 --- a/UI/Settings/GraphicsPage.cpp +++ b/UI/Settings/GraphicsPage.cpp @@ -175,8 +175,10 @@ void BuildGraphicsSettings(SettingsPage *page) { #if (PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(TVOS)) || PPSSPP_PLATFORM(ANDROID) // Showing the game there needs a second swapchain. Otherwise the system mirrors the screen. - page->Setting(&g_Config.iExternalDisplay).EnabledFunc([draw] { - return draw->GetDeviceCaps().multipleSwapchains; + // Looked up each time: the draw context is replaced when Android loses the surface, and this page isn't rebuilt. + page->Setting(&g_Config.iExternalDisplay).EnabledFunc([host] { + Draw::DrawContext *draw = host->PageScreenManager()->getDrawContext(); + return draw && draw->GetDeviceCaps().multipleSwapchains; }).OnChange([previous = g_Config.iExternalDisplay](UI::EventParams &e) mutable { // Picking the current value again would rebuild the TV's output for nothing (iOS starts over on a change). if (g_Config.iExternalDisplay == previous) { From 247e764b97a45e2d34adb4fb6f3fce8f48ffa3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 26 Oct 2026 15:04:00 +0100 Subject: [PATCH 2/3] UI: Don't use draw objects from before a device loss The system info screen's "Copy summary" kept the draw context it was created with, and the GPU driver test screen kept its pipelines and sampler, using and finally releasing them into a deleted context after Android lost the surface. Look up the draw context when clicked, and release the test objects on device loss (they're recreated on draw). Co-Authored-By: Claude Opus 5.5 (1M context) --- UI/GPUDriverTestScreen.cpp | 85 +++++++++++++++++--------------------- UI/GPUDriverTestScreen.h | 2 + UI/SystemInfoScreen.cpp | 2 +- 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/UI/GPUDriverTestScreen.cpp b/UI/GPUDriverTestScreen.cpp index 8113f73507..673d20becb 100644 --- a/UI/GPUDriverTestScreen.cpp +++ b/UI/GPUDriverTestScreen.cpp @@ -239,61 +239,48 @@ GPUDriverTestScreen::GPUDriverTestScreen() { } GPUDriverTestScreen::~GPUDriverTestScreen() { - if (discardWriteDepthStencil_) - discardWriteDepthStencil_->Release(); - if (discardWriteDepth_) - discardWriteDepth_->Release(); - if (discardWriteStencil_) - discardWriteStencil_->Release(); + DestroyDeviceObjects(); +} - if (drawTestStencilEqualDepthAlways_) - drawTestStencilEqualDepthAlways_->Release(); - if (drawTestStencilNotEqualDepthAlways_) - drawTestStencilNotEqualDepthAlways_->Release(); - if (drawTestStencilEqual_) - drawTestStencilEqual_->Release(); - if (drawTestStencilNotEqual_) - drawTestStencilNotEqual_->Release(); - if (drawTestStencilAlwaysDepthLessEqual_) - drawTestStencilAlwaysDepthLessEqual_->Release(); - if (drawTestStencilAlwaysDepthGreater_) - drawTestStencilAlwaysDepthGreater_->Release(); - if (drawTestDepthLessEqual_) - drawTestDepthLessEqual_->Release(); - if (drawTestDepthGreater_) - drawTestDepthGreater_->Release(); +template +static void ReleaseAndClear(T *&obj) { + if (obj) { + obj->Release(); + obj = nullptr; + } +} - if (discardFragShader_) - discardFragShader_->Release(); +void GPUDriverTestScreen::deviceLost() { + UIBaseDialogScreen::deviceLost(); + DestroyDeviceObjects(); +} - // Shader test - if (adrenoLogicDiscardPipeline_) - adrenoLogicDiscardPipeline_->Release(); - if (flatShadingPipeline_) - flatShadingPipeline_->Release(); - - if (adrenoLogicDiscardFragShader_) - adrenoLogicDiscardFragShader_->Release(); - if (adrenoLogicDiscardVertShader_) - adrenoLogicDiscardVertShader_->Release(); - if (flatFragShader_) - flatFragShader_->Release(); - if (flatVertShader_) - flatVertShader_->Release(); - - if (samplerNearest_) - samplerNearest_->Release(); +// Everything is created again on the next draw, from the new draw context. +void GPUDriverTestScreen::DestroyDeviceObjects() { + ReleaseAndClear(discardWriteDepthStencil_); + ReleaseAndClear(discardWriteDepth_); + ReleaseAndClear(discardWriteStencil_); + ReleaseAndClear(drawTestStencilEqualDepthAlways_); + ReleaseAndClear(drawTestStencilNotEqualDepthAlways_); + ReleaseAndClear(drawTestStencilEqual_); + ReleaseAndClear(drawTestStencilNotEqual_); + ReleaseAndClear(drawTestStencilAlwaysDepthLessEqual_); + ReleaseAndClear(drawTestStencilAlwaysDepthGreater_); + ReleaseAndClear(drawTestDepthLessEqual_); + ReleaseAndClear(drawTestDepthGreater_); + ReleaseAndClear(discardFragShader_); + ReleaseAndClear(adrenoLogicDiscardPipeline_); + ReleaseAndClear(flatShadingPipeline_); + ReleaseAndClear(adrenoLogicDiscardFragShader_); + ReleaseAndClear(adrenoLogicDiscardVertShader_); + ReleaseAndClear(flatFragShader_); + ReleaseAndClear(flatVertShader_); + ReleaseAndClear(samplerNearest_); } void GPUDriverTestScreen::CreateViews() { using namespace Draw; - if (!samplerNearest_) { - DrawContext *draw = screenManager()->getDrawContext(); - SamplerStateDesc nearestDesc{}; - samplerNearest_ = draw->CreateSamplerState(nearestDesc); - } - // Don't bother with views for now. using namespace UI; auto di = GetI18NCategory(I18NCat::DIALOG); @@ -631,6 +618,10 @@ void GPUDriverTestScreen::ShaderTest(UIContext &dc) { } void GPUDriverTestScreen::DrawForeground(UIContext &dc) { + if (!samplerNearest_) { + Draw::SamplerStateDesc nearestDesc{}; + samplerNearest_ = screenManager()->getDrawContext()->CreateSamplerState(nearestDesc); + } switch (tabHolder_->GetCurrentTab()) { case 0: DiscardTest(dc); diff --git a/UI/GPUDriverTestScreen.h b/UI/GPUDriverTestScreen.h index 95ee4f1099..f4070b9def 100644 --- a/UI/GPUDriverTestScreen.h +++ b/UI/GPUDriverTestScreen.h @@ -18,12 +18,14 @@ public: void CreateViews() override; void DrawForeground(UIContext &dc) override; + void deviceLost() override; const char *tag() const override { return "GPUDriverTest"; } private: void DiscardTest(UIContext &dc); void ShaderTest(UIContext &dc); + void DestroyDeviceObjects(); // Common objects Draw::SamplerState *samplerNearest_ = nullptr; diff --git a/UI/SystemInfoScreen.cpp b/UI/SystemInfoScreen.cpp index ed6637ef5c..ca3960adec 100644 --- a/UI/SystemInfoScreen.cpp +++ b/UI/SystemInfoScreen.cpp @@ -129,7 +129,7 @@ void SystemInfoScreen::CreateDeviceInfoTab(UI::LinearLayout *deviceSpecs) { Draw::DrawContext *draw = screenManager()->getDrawContext(); - systemInfo->Add(new Choice(si->T("Copy summary to clipboard"), ImageID("I_FILE_COPY")))->OnClick.Add([draw](UI::EventParams &e) { CopySummaryToClipboard(draw); }); + systemInfo->Add(new Choice(si->T("Copy summary to clipboard"), ImageID("I_FILE_COPY")))->OnClick.Add([this](UI::EventParams &e) { CopySummaryToClipboard(screenManager()->getDrawContext()); }); systemInfo->Add(new InfoItem(si->T("System Name"), System_GetProperty(SYSPROP_NAME))); #if PPSSPP_PLATFORM(ANDROID) systemInfo->Add(new InfoItem(si->T("System Version"), StringFromInt(System_GetPropertyInt(SYSPROP_SYSTEMVERSION)))); From b43e119ea11f171a5140b024c7941196069e4a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 26 Oct 2026 15:16:33 +0100 Subject: [PATCH 3/3] GPU: Don't keep state from before a device loss - Metal: Load the shader cache again in DeviceRestore, like Vulkan. DeviceLost saves it and throws everything away, so the next save (the next app switch, or quitting) overwrote it with only what had been drawn since. - Software renderer: Bring the post-shader back after a device loss. DestroyDeviceObjects asks for it with restorePostShader_, but CreateDeviceObjects cleared the flag again before BeginFrame could act on it. The hardware backends recover through renderResized_. - Forget pointers into the old device's objects in DeviceLost, rather than relying on BeginFrame to come before the next flush: Vulkan's last pipeline and bound views, Metal's UBO allocations, the GLES last linked shader and bound texture. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Common/PresentationCommon.cpp | 3 +-- GPU/GLES/ShaderManagerGLES.cpp | 1 + GPU/GLES/TextureCacheGLES.cpp | 1 + GPU/Metal/DrawEngineMetal.mm | 4 ++++ GPU/Metal/GPU_Metal.mm | 6 ++++++ GPU/Vulkan/DrawEngineVulkan.cpp | 4 ++++ 6 files changed, 17 insertions(+), 2 deletions(-) diff --git a/GPU/Common/PresentationCommon.cpp b/GPU/Common/PresentationCommon.cpp index 6bc3179b85..392ea6d045 100644 --- a/GPU/Common/PresentationCommon.cpp +++ b/GPU/Common/PresentationCommon.cpp @@ -599,8 +599,7 @@ void PresentationCommon::CreateDeviceObjects() { texColorRBSwizzle_ = CreatePipeline({ draw_->GetVshaderPreset(VS_TEXTURE_COLOR_2D), draw_->GetFshaderPreset(FS_TEXTURE_COLOR_2D_RB_SWIZZLE) }, false, &vsTexColBufDesc); texColorBlend_ = CreatePipeline({ draw_->GetVshaderPreset(VS_TEXTURE_COLOR_2D), draw_->GetFshaderPreset(FS_TEXTURE_COLOR_2D) }, false, &vsTexColBufDesc, true); overlayVdata_ = draw_->CreateBuffer(sizeof(Vertex) * 4, BufferUsageFlag::DYNAMIC | BufferUsageFlag::VERTEXDATA); - - restorePostShader_ = false; + // Not resetting restorePostShader_: after a device loss, BeginFrame uses it to bring the post-shader back. } template diff --git a/GPU/GLES/ShaderManagerGLES.cpp b/GPU/GLES/ShaderManagerGLES.cpp index ea3439b176..17cd36cb05 100644 --- a/GPU/GLES/ShaderManagerGLES.cpp +++ b/GPU/GLES/ShaderManagerGLES.cpp @@ -612,6 +612,7 @@ void ShaderManagerGLES::Clear() { linkedShaderCache_.clear(); fsCache_.Clear(); vsCache_.Clear(); + lastShader_ = nullptr; lastFSID_.set_invalid(); lastVSID_.set_invalid(); diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index 3050cf55dd..5f5e81883e 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -407,6 +407,7 @@ bool TextureCacheGLES::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, void TextureCacheGLES::DeviceLost() { TextureCacheCommon::DeviceLost(); + lastBoundTexture_ = nullptr; draw_ = nullptr; render_ = nullptr; } diff --git a/GPU/Metal/DrawEngineMetal.mm b/GPU/Metal/DrawEngineMetal.mm index 905a1b8e4d..1dbd5276d7 100644 --- a/GPU/Metal/DrawEngineMetal.mm +++ b/GPU/Metal/DrawEngineMetal.mm @@ -178,6 +178,10 @@ void DrawEngineMetal::DeviceLost() { } boundSecondary_ = nil; boundDepal_ = nil; + // Allocated from the old render manager's push buffer. + baseUBO_ = MetalPushAlloc{}; + lightUBO_ = MetalPushAlloc{}; + uboFrame_ = (uint64_t)-1; draw_ = nullptr; rm_ = nullptr; } diff --git a/GPU/Metal/GPU_Metal.mm b/GPU/Metal/GPU_Metal.mm index 43e07d0971..0deea8a6eb 100644 --- a/GPU/Metal/GPU_Metal.mm +++ b/GPU/Metal/GPU_Metal.mm @@ -214,6 +214,12 @@ void GPU_Metal::DeviceLost() { void GPU_Metal::DeviceRestore(Draw::DrawContext *draw) { GPUCommonHW::DeviceRestore(draw); + + // DeviceLost saved the cache and then threw everything away. Load it again, or the next save would + // overwrite the file with only what gets drawn from now on. + if (shaderCachePath_.Valid()) { + LoadCache(shaderCachePath_); + } } void GPU_Metal::GetStats(StringWriter &w) { diff --git a/GPU/Vulkan/DrawEngineVulkan.cpp b/GPU/Vulkan/DrawEngineVulkan.cpp index aadc9edee9..163420f617 100644 --- a/GPU/Vulkan/DrawEngineVulkan.cpp +++ b/GPU/Vulkan/DrawEngineVulkan.cpp @@ -133,6 +133,10 @@ void DrawEngineVulkan::DestroyDeviceObjects() { void DrawEngineVulkan::DeviceLost() { DestroyDeviceObjects(); DirtyAllUBOs(); + // The pipelines and views these point to are gone (BeginFrame resets them too, but a flush could come first). + lastPipeline_ = nullptr; + boundDepal_ = VK_NULL_HANDLE; + boundSecondary_ = VK_NULL_HANDLE; draw_ = nullptr; }