From e8f70594a46aa8bc116237eed47261d7af2cb744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 11 Dec 2023 12:41:44 +0100 Subject: [PATCH 1/4] Implement running the game in the background on the pause screen. Fix some bugs. --- Common/UI/Screen.cpp | 17 ++++++------- Common/UI/Screen.h | 11 ++++++--- Common/UI/UIScreen.cpp | 23 ++++++++++------- Common/UI/UIScreen.h | 4 ++- Core/Config.cpp | 2 ++ Core/Config.h | 3 ++- UI/BackgroundAudio.cpp | 4 --- UI/ControlMappingScreen.cpp | 25 +++++++++---------- UI/ControlMappingScreen.h | 2 +- UI/EmuScreen.cpp | 49 ++++++++++++++++++++++--------------- UI/EmuScreen.h | 2 +- UI/GPUDriverTestScreen.cpp | 16 ++++-------- UI/GPUDriverTestScreen.h | 6 ++--- UI/GameScreen.cpp | 9 ++++--- UI/GameScreen.h | 2 +- UI/MemStickScreen.h | 5 ++-- UI/MiscScreens.cpp | 18 ++++---------- UI/MiscScreens.h | 7 +++--- UI/NativeApp.cpp | 15 +++++------- UI/OnScreenDisplay.cpp | 4 +-- UI/OnScreenDisplay.h | 2 +- UI/PauseScreen.cpp | 2 ++ UI/PauseScreen.h | 2 ++ UI/ReportScreen.cpp | 5 ++-- UI/ReportScreen.h | 2 +- 25 files changed, 120 insertions(+), 117 deletions(-) diff --git a/Common/UI/Screen.cpp b/Common/UI/Screen.cpp index b1fe507cfe4a..1bf1d32f95f4 100644 --- a/Common/UI/Screen.cpp +++ b/Common/UI/Screen.cpp @@ -154,7 +154,8 @@ void ScreenManager::resized() { } } -void ScreenManager::render() { +ScreenRenderFlags ScreenManager::render() { + ScreenRenderFlags flags = ScreenRenderFlags::NONE; if (!stack_.empty()) { // Collect the screens to render TinySet layers; @@ -169,12 +170,12 @@ void ScreenManager::render() { Screen *backgroundScreen = nullptr; do { --iter; - if (!coveringScreen) { - layers.push_back(iter->screen); - } else if (!backgroundScreen && iter->screen->canBeBackground()) { + if (!backgroundScreen && iter->screen->canBeBackground()) { // There still might be a screen that wants to be background - generally the EmuScreen if present. layers.push_back(iter->screen); backgroundScreen = iter->screen; + } else if (!coveringScreen) { + layers.push_back(iter->screen); } if (iter->flags != LAYER_TRANSPARENT) { coveringScreen = iter->screen; @@ -194,9 +195,6 @@ void ScreenManager::render() { if (i == (int)layers.size() - 1) { // Bottom. mode = ScreenRenderMode::FIRST; - if (layers[i] == backgroundScreen && coveringScreen != layers[i]) { - mode |= ScreenRenderMode::BACKGROUND; - } if (i == 0) { mode |= ScreenRenderMode::TOP; } @@ -205,12 +203,12 @@ void ScreenManager::render() { } else { mode = ScreenRenderMode::BEHIND; } - layers[i]->render(mode); + flags |= layers[i]->render(mode); } if (overlayScreen_) { // It doesn't care about mode. - overlayScreen_->render(ScreenRenderMode::TOP); + flags |= overlayScreen_->render(ScreenRenderMode::TOP); } getUIContext()->Flush(); @@ -224,6 +222,7 @@ void ScreenManager::render() { } processFinishDialog(); + return flags; } void ScreenManager::getFocusPosition(float &x, float &y, float &z) { diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index ed5b2a957aa7..e65b0d8d7993 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -50,12 +50,17 @@ enum class ScreenFocusChange { enum class ScreenRenderMode { DEFAULT = 0, FIRST = 1, - BACKGROUND = 2, BEHIND = 4, TOP = 8, }; ENUM_CLASS_BITOPS(ScreenRenderMode); +enum class ScreenRenderFlags { + NONE = 0, + HANDLED_THROTTLING = 1, +}; +ENUM_CLASS_BITOPS(ScreenRenderFlags); + class Screen { public: Screen() : screenManager_(nullptr) { } @@ -65,7 +70,7 @@ class Screen { virtual void onFinish(DialogResult reason) {} virtual void update() {} - virtual void render(ScreenRenderMode mode) {} + virtual ScreenRenderFlags render(ScreenRenderMode mode) = 0; virtual void resized() {} virtual void dialogFinished(const Screen *dialog, DialogResult result) {} virtual void sendMessage(UIMessage message, const char *value) {} @@ -128,7 +133,7 @@ class ScreenManager { postRenderUserdata_ = userdata; } - void render(); + ScreenRenderFlags render(); void resized(); void shutdown(); diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index 99bb251c31df..927d8bdb4a24 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -212,27 +212,32 @@ void UIScreen::SetupViewport() { draw->SetTargetSize(g_display.pixel_xres, g_display.pixel_yres); } -void UIScreen::render(ScreenRenderMode mode) { +ScreenRenderFlags UIScreen::render(ScreenRenderMode mode) { if (mode & ScreenRenderMode::FIRST) { SetupViewport(); } DoRecreateViews(); + UIContext &uiContext = *screenManager()->getUIContext(); if (root_) { - UIContext &uiContext = *screenManager()->getUIContext(); - UI::LayoutViewHierarchy(uiContext, root_, ignoreInsets_); + } - uiContext.PushTransform({translation_, scale_, alpha_}); + uiContext.PushTransform({translation_, scale_, alpha_}); - uiContext.Begin(); - DrawBackground(uiContext); + uiContext.Begin(); + DrawBackground(uiContext); + if (root_) { root_->Draw(uiContext); - uiContext.Flush(); - - uiContext.PopTransform(); } + uiContext.Flush(); + DrawForeground(uiContext); + uiContext.Flush(); + + uiContext.PopTransform(); + + return ScreenRenderFlags::NONE; } TouchInput UIScreen::transformTouch(const TouchInput &touch) { diff --git a/Common/UI/UIScreen.h b/Common/UI/UIScreen.h index a5fb9604ad70..d7487bee717d 100644 --- a/Common/UI/UIScreen.h +++ b/Common/UI/UIScreen.h @@ -36,7 +36,7 @@ class UIScreen : public Screen { ~UIScreen(); void update() override; - void render(ScreenRenderMode mode) override; + ScreenRenderFlags render(ScreenRenderMode mode) override; void deviceLost() override; void deviceRestored() override; @@ -72,6 +72,8 @@ class UIScreen : public Screen { protected: virtual void DrawBackground(UIContext &ui) {} + virtual void DrawForeground(UIContext &ui) {} + void SetupViewport(); void DoRecreateViews(); diff --git a/Core/Config.cpp b/Core/Config.cpp index eec83a23e87c..c7b194a3a35f 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -297,6 +297,8 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("EnablePlugins", &g_Config.bLoadPlugins, true, CfgFlag::PER_GAME), ConfigSetting("IgnoreCompatSettings", &g_Config.sIgnoreCompatSettings, "", CfgFlag::PER_GAME | CfgFlag::REPORT), + + ConfigSetting("RunBehindPauseMenu", &g_Config.bRunBehindPauseMenu, false, CfgFlag::DEFAULT), }; static bool DefaultSasThread() { diff --git a/Core/Config.h b/Core/Config.h index 1b5a737e3020..de2d6971cbfe 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -99,9 +99,10 @@ struct Config { // Not used on mobile devices. bool bPauseExitsEmulator; - bool bPauseMenuExitsEmulator; + bool bRunBehindPauseMenu; + // Core bool bIgnoreBadMemAccess; diff --git a/UI/BackgroundAudio.cpp b/UI/BackgroundAudio.cpp index c990da8a68bc..08090f88e496 100644 --- a/UI/BackgroundAudio.cpp +++ b/UI/BackgroundAudio.cpp @@ -302,10 +302,6 @@ void BackgroundAudio::SetGame(const Path &path) { } bool BackgroundAudio::Play() { - if (GetUIState() == UISTATE_INGAME) { - return false; - } - std::lock_guard lock(mutex_); // Immediately stop the sound if it is turned off while playing. diff --git a/UI/ControlMappingScreen.cpp b/UI/ControlMappingScreen.cpp index 4df546bb4ca8..6181a9f429ca 100644 --- a/UI/ControlMappingScreen.cpp +++ b/UI/ControlMappingScreen.cpp @@ -713,27 +713,25 @@ void TouchTestScreen::axis(const AxisInput &axis) { UpdateLogView(); } -void TouchTestScreen::render(ScreenRenderMode mode) { - UIDialogScreenWithGameBackground::render(mode); - UIContext *ui_context = screenManager()->getUIContext(); - Bounds bounds = ui_context->GetLayoutBounds(); +void TouchTestScreen::DrawForeground(UIContext &dc) { + Bounds bounds = dc.GetLayoutBounds(); - ui_context->BeginNoTex(); + dc.BeginNoTex(); for (int i = 0; i < MAX_TOUCH_POINTS; i++) { if (touches_[i].id != -1) { - ui_context->Draw()->Circle(touches_[i].x, touches_[i].y, 100.0, 3.0, 80, 0.0f, 0xFFFFFFFF, 1.0); + dc.Draw()->Circle(touches_[i].x, touches_[i].y, 100.0, 3.0, 80, 0.0f, 0xFFFFFFFF, 1.0); } } - ui_context->Flush(); + dc.Flush(); - ui_context->Begin(); + dc.Begin(); char buffer[4096]; for (int i = 0; i < MAX_TOUCH_POINTS; i++) { if (touches_[i].id != -1) { - ui_context->Draw()->Circle(touches_[i].x, touches_[i].y, 100.0, 3.0, 80, 0.0f, 0xFFFFFFFF, 1.0); + dc.Draw()->Circle(touches_[i].x, touches_[i].y, 100.0, 3.0, 80, 0.0f, 0xFFFFFFFF, 1.0); snprintf(buffer, sizeof(buffer), "%0.1fx%0.1f", touches_[i].x, touches_[i].y); - ui_context->DrawText(buffer, touches_[i].x, touches_[i].y + (touches_[i].y > g_display.dp_yres - 100.0f ? -135.0f : 95.0f), 0xFFFFFFFF, ALIGN_HCENTER | FLAG_DYNAMIC_ASCII); + dc.DrawText(buffer, touches_[i].x, touches_[i].y + (touches_[i].y > g_display.dp_yres - 100.0f ? -135.0f : 95.0f), 0xFFFFFFFF, ALIGN_HCENTER | FLAG_DYNAMIC_ASCII); } } @@ -762,8 +760,8 @@ void TouchTestScreen::render(ScreenRenderMode mode) { // On Android, also add joystick debug data. - ui_context->DrawTextShadow(buffer, bounds.centerX(), bounds.y + 20.0f, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); - ui_context->Flush(); + dc.DrawTextShadow(buffer, bounds.centerX(), bounds.y + 20.0f, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); + dc.Flush(); } void RecreateActivity() { @@ -799,8 +797,7 @@ UI::EventReturn TouchTestScreen::OnRecreateActivity(UI::EventParams &e) { class Backplate : public UI::InertView { public: - Backplate(float scale, UI::LayoutParams *layoutParams = nullptr) : InertView(layoutParams), scale_(scale) { - } + Backplate(float scale, UI::LayoutParams *layoutParams = nullptr) : InertView(layoutParams), scale_(scale) {} void Draw(UIContext &dc) override { for (float dy = 0.0f; dy <= 4.0f; dy += 1.0f) { diff --git a/UI/ControlMappingScreen.h b/UI/ControlMappingScreen.h index 69c8a4d449d2..d096fdec531d 100644 --- a/UI/ControlMappingScreen.h +++ b/UI/ControlMappingScreen.h @@ -147,7 +147,7 @@ class TouchTestScreen : public UIDialogScreenWithGameBackground { } void touch(const TouchInput &touch) override; - void render(ScreenRenderMode mode) override; + void DrawForeground(UIContext &dc) override; bool key(const KeyInput &key) override; void axis(const AxisInput &axis) override; diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index ec7542c22d93..5620f90639e9 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1438,12 +1438,17 @@ void EmuScreen::darken() { } } -void EmuScreen::render(ScreenRenderMode mode) { +ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { + ScreenRenderFlags flags = ScreenRenderFlags::NONE; + using namespace Draw; + + DrawContext *draw = screenManager()->getDrawContext(); + if (!draw) + return flags; // shouldn't really happen but I've seen a suspicious stack trace.. + if (mode & ScreenRenderMode::FIRST) { // Actually, always gonna be first when it exists (?) - using namespace Draw; - DrawContext *draw = screenManager()->getDrawContext(); // Here we do NOT bind the backbuffer or clear the screen, unless non-buffered. // The emuscreen is different than the others - we really want to allow the game to render to framebuffers // before we ever bind the backbuffer for rendering. On mobile GPUs, switching back and forth between render @@ -1471,19 +1476,13 @@ void EmuScreen::render(ScreenRenderMode mode) { draw->SetTargetSize(g_display.pixel_xres, g_display.pixel_yres); } - using namespace Draw; - - DrawContext *thin3d = screenManager()->getDrawContext(); - if (!thin3d) - return; // shouldn't really happen but I've seen a suspicious stack trace.. - g_OSD.NudgeSidebar(); if (mode & ScreenRenderMode::TOP) { System_Notify(SystemNotification::KEEP_SCREEN_AWAKE); - } else { + } else if (!g_Config.bRunBehindPauseMenu) { // Not on top. Let's not execute, only draw the image. - thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_Stepping"); + draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_Stepping"); // Just to make sure. if (PSP_IsInited() && !g_Config.bSkipBufferEffects) { PSP_BeginHostFrame(); @@ -1491,7 +1490,7 @@ void EmuScreen::render(ScreenRenderMode mode) { PSP_EndHostFrame(); darken(); } - return; + return flags; } if (invalid_) { @@ -1502,9 +1501,9 @@ void EmuScreen::render(ScreenRenderMode mode) { // It's possible this might be set outside PSP_RunLoopFor(). // In this case, we need to double check it here. checkPowerDown(); - thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_Invalid"); + draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_Invalid"); renderUI(); - return; + return flags; } // Freeze-frame functionality (loads a savestate on every frame). @@ -1528,6 +1527,8 @@ void EmuScreen::render(ScreenRenderMode mode) { PSP_BeginHostFrame(); PSP_RunLoopWhileState(); + flags |= ScreenRenderFlags::HANDLED_THROTTLING; + // Hopefully coreState is now CORE_NEXTFRAME switch (coreState) { case CORE_NEXTFRAME: @@ -1543,12 +1544,12 @@ void EmuScreen::render(ScreenRenderMode mode) { // Clear to blue background screen bool dangerousSettings = !Reporting::IsSupported(); uint32_t color = dangerousSettings ? 0xFF900050 : 0xFF900000; - thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE, color }, "EmuScreen_RuntimeError"); + draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE, color }, "EmuScreen_RuntimeError"); // The info is drawn later in renderUI } 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. - thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_Stepping"); + draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_Stepping"); // Just to make sure. if (PSP_IsInited()) { gpu->CopyDisplayToOutput(true); @@ -1570,12 +1571,19 @@ void EmuScreen::render(ScreenRenderMode mode) { // This must happen after PSP_EndHostFrame so that things like push buffers are end-frame'd before we start destroying stuff. if (checkPowerDown() || rebind) { // Shutting down can end up ending the current render pass - thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_NoFrame"); + draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_NoFrame"); + } + + if (!(mode & ScreenRenderMode::TOP)) { + // We're in run-behind mode, but we don't want to draw chat, debug UI and stuff. + // So, darken and bail here. + darken(); + return flags; } if (hasVisibleUI()) { // In most cases, this should already be bound and a no-op. - thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::KEEP, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_UI"); + draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::KEEP, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_UI"); cardboardDisableButton_->SetVisibility(g_Config.bEnableCardboardVR ? UI::V_VISIBLE : UI::V_GONE); screenManager()->getUIContext()->BeginFrame(); renderUI(); @@ -1590,10 +1598,11 @@ void EmuScreen::render(ScreenRenderMode mode) { if (mode & ScreenRenderMode::TOP) { // TODO: Replace this with something else. if (stopRender_) - thin3d->WipeQueue(); - } else if (!screenManager()->topScreen()->wantBrightBackground()) { + draw->WipeQueue(); + } else { darken(); } + return flags; } bool EmuScreen::hasVisibleUI() { diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index a050fd92660c..a97273027450 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -42,7 +42,7 @@ class EmuScreen : public UIScreen { const char *tag() const override { return "Emu"; } void update() override; - void render(ScreenRenderMode mode) override; + ScreenRenderFlags render(ScreenRenderMode mode) override; void dialogFinished(const Screen *dialog, DialogResult result) override; void sendMessage(UIMessage message, const char *value) override; void resized() override; diff --git a/UI/GPUDriverTestScreen.cpp b/UI/GPUDriverTestScreen.cpp index c10faf17ba85..326383dd1d03 100644 --- a/UI/GPUDriverTestScreen.cpp +++ b/UI/GPUDriverTestScreen.cpp @@ -311,7 +311,7 @@ void GPUDriverTestScreen::CreateViews() { anchor->Add(back); } -void GPUDriverTestScreen::DiscardTest() { +void GPUDriverTestScreen::DiscardTest(UIContext &dc) { using namespace UI; using namespace Draw; if (!discardWriteDepthStencil_) { @@ -440,7 +440,6 @@ void GPUDriverTestScreen::DiscardTest() { rasterNoCull->Release(); } - UIContext &dc = *screenManager()->getUIContext(); Draw::DrawContext *draw = dc.GetDrawContext(); static const char * const writeModeNames[] = { "Stencil+Depth", "Stencil", "Depth" }; @@ -529,10 +528,9 @@ void GPUDriverTestScreen::DiscardTest() { dc.Flush(); } -void GPUDriverTestScreen::ShaderTest() { +void GPUDriverTestScreen::ShaderTest(UIContext &dc) { using namespace Draw; - UIContext &dc = *screenManager()->getUIContext(); Draw::DrawContext *draw = dc.GetDrawContext(); if (!adrenoLogicDiscardPipeline_) { @@ -629,17 +627,13 @@ void GPUDriverTestScreen::ShaderTest() { dc.Flush(); } - -void GPUDriverTestScreen::render(ScreenRenderMode mode) { - using namespace Draw; - UIScreen::render(mode); - +void GPUDriverTestScreen::DrawForeground(UIContext &dc) { switch (tabHolder_->GetCurrentTab()) { case 0: - DiscardTest(); + DiscardTest(dc); break; case 1: - ShaderTest(); + ShaderTest(dc); break; } } diff --git a/UI/GPUDriverTestScreen.h b/UI/GPUDriverTestScreen.h index 16c1f8d7c63c..5c8b67cfa460 100644 --- a/UI/GPUDriverTestScreen.h +++ b/UI/GPUDriverTestScreen.h @@ -15,13 +15,13 @@ class GPUDriverTestScreen : public UIDialogScreenWithBackground { ~GPUDriverTestScreen(); void CreateViews() override; - void render(ScreenRenderMode mode) override; + void DrawForeground(UIContext &dc) override; const char *tag() const override { return "GPUDriverTest"; } private: - void DiscardTest(); - void ShaderTest(); + void DiscardTest(UIContext &dc); + void ShaderTest(UIContext &dc); // Common objects Draw::SamplerState *samplerNearest_ = nullptr; diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index aae67e8be313..76ebc4efc6c7 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -276,14 +276,14 @@ UI::EventReturn GameScreen::OnDeleteConfig(UI::EventParams &e) return UI::EVENT_DONE; } -void GameScreen::render(ScreenRenderMode mode) { - UIScreen::render(mode); +ScreenRenderFlags GameScreen::render(ScreenRenderMode mode) { + ScreenRenderFlags flags = UIScreen::render(mode); auto ga = GetI18NCategory(I18NCat::GAME); - Draw::DrawContext *thin3d = screenManager()->getDrawContext(); + Draw::DrawContext *draw = screenManager()->getDrawContext(); - std::shared_ptr info = g_gameInfoCache->GetInfo(thin3d, gamePath_, GAMEINFO_WANTBG | GAMEINFO_WANTSIZE | GAMEINFO_WANTUNCOMPRESSEDSIZE); + std::shared_ptr info = g_gameInfoCache->GetInfo(draw, gamePath_, GAMEINFO_WANTBG | GAMEINFO_WANTSIZE | GAMEINFO_WANTUNCOMPRESSEDSIZE); if (tvTitle_) { tvTitle_->SetText(info->GetTitle()); @@ -416,6 +416,7 @@ void GameScreen::render(ScreenRenderMode mode) { choice->SetVisibility(UI::V_VISIBLE); } } + return flags; } UI::EventReturn GameScreen::OnShowInFolder(UI::EventParams &e) { diff --git a/UI/GameScreen.h b/UI/GameScreen.h index 4340fae811bd..b8fd92fe784f 100644 --- a/UI/GameScreen.h +++ b/UI/GameScreen.h @@ -38,7 +38,7 @@ class GameScreen : public UIDialogScreenWithGameBackground { void update() override; - void render(ScreenRenderMode mode) override; + ScreenRenderFlags render(ScreenRenderMode mode) override; const char *tag() const override { return "Game"; } diff --git a/UI/MemStickScreen.h b/UI/MemStickScreen.h index f409e958153a..cf0eecbc1b6a 100644 --- a/UI/MemStickScreen.h +++ b/UI/MemStickScreen.h @@ -52,14 +52,15 @@ class MemStickScreen : public UIDialogScreenWithBackground { void dialogFinished(const Screen *dialog, DialogResult result) override; void update() override; - void render(ScreenRenderMode mode) override { + ScreenRenderFlags render(ScreenRenderMode mode) override { // Simple anti-flicker due to delayed finish. if (!done_) { // render as usual. - UIDialogScreenWithBackground::render(mode); + return UIDialogScreenWithBackground::render(mode); } else { // no render. black frame insertion is better than flicker. } + return ScreenRenderFlags::NONE; } private: diff --git a/UI/MiscScreens.cpp b/UI/MiscScreens.cpp index 76218bf27a07..a5dce8b7935a 100644 --- a/UI/MiscScreens.cpp +++ b/UI/MiscScreens.cpp @@ -429,7 +429,7 @@ void HandleCommonMessages(UIMessage message, const char *value, ScreenManager *m } } -void BackgroundScreen::render(ScreenRenderMode mode) { +ScreenRenderFlags BackgroundScreen::render(ScreenRenderMode mode) { if (mode & ScreenRenderMode::FIRST) { SetupViewport(); } else { @@ -453,6 +453,8 @@ void BackgroundScreen::render(ScreenRenderMode mode) { uiContext->Flush(); uiContext->PopTransform(); + + return ScreenRenderFlags::NONE; } void BackgroundScreen::sendMessage(UIMessage message, const char *value) { @@ -732,12 +734,9 @@ void LogoScreen::touch(const TouchInput &touch) { } } -void LogoScreen::render(ScreenRenderMode mode) { +void LogoScreen::DrawForeground(UIContext &dc) { using namespace Draw; - UIScreen::render(mode); - UIContext &dc = *screenManager()->getUIContext(); - const Bounds &bounds = dc.GetBounds(); dc.Begin(); @@ -752,10 +751,6 @@ void LogoScreen::render(ScreenRenderMode mode) { alphaText = 3.0f - t; uint32_t textColor = colorAlpha(dc.theme->infoStyle.fgColor, alphaText); - float x, y, z; - screenManager()->getFocusPosition(x, y, z); - ::DrawBackground(dc, alpha, x, y, z); - auto cr = GetI18NCategory(I18NCat::PSPCREDITS); auto gr = GetI18NCategory(I18NCat::GRAPHICS); char temp[256]; @@ -871,9 +866,7 @@ void CreditsScreen::update() { UpdateUIState(UISTATE_MENU); } -void CreditsScreen::render(ScreenRenderMode mode) { - UIScreen::render(mode); - +void CreditsScreen::DrawForeground(UIContext &dc) { auto cr = GetI18NCategory(I18NCat::PSPCREDITS); std::string specialthanksMaxim = "Maxim "; @@ -1020,7 +1013,6 @@ void CreditsScreen::render(ScreenRenderMode mode) { } credits[0] = (const char *)temp; - UIContext &dc = *screenManager()->getUIContext(); dc.Begin(); const Bounds &bounds = dc.GetLayoutBounds(); diff --git a/UI/MiscScreens.h b/UI/MiscScreens.h index cc4a6b595f77..da816c686156 100644 --- a/UI/MiscScreens.h +++ b/UI/MiscScreens.h @@ -38,10 +38,9 @@ inline void NoOpVoidBool(bool) {} class BackgroundScreen : public UIScreen { public: - void render(ScreenRenderMode mode) override; + ScreenRenderFlags render(ScreenRenderMode mode) override; void sendMessage(UIMessage message, const char *value) override; - private: void CreateViews() override {} const char *tag() const override { return "bg"; } @@ -146,7 +145,7 @@ class LogoScreen : public UIScreen { bool key(const KeyInput &key) override; void touch(const TouchInput &touch) override; void update() override; - void render(ScreenRenderMode mode) override; + void DrawForeground(UIContext &ui) override; void sendMessage(UIMessage message, const char *value) override; void CreateViews() override {} @@ -164,7 +163,7 @@ class CreditsScreen : public UIDialogScreenWithBackground { public: CreditsScreen(); void update() override; - void render(ScreenRenderMode mode) override; + void DrawForeground(UIContext &ui) override; void CreateViews() override; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 05e7ec97d82c..ad47de968baa 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1077,12 +1077,7 @@ static void SendMouseDeltaAxis(); void NativeFrame(GraphicsContext *graphicsContext) { PROFILE_END_FRAME(); - bool menuThrottle = (GetUIState() != UISTATE_INGAME || !PSP_IsInited()) && GetUIState() != UISTATE_EXIT; - - double startTime; - if (menuThrottle) { - startTime = time_now_d(); - } + double startTime = time_now_d(); std::vector toProcess; { @@ -1107,7 +1102,6 @@ void NativeFrame(GraphicsContext *graphicsContext) { g_DownloadManager.Update(); g_Discord.Update(); - g_BackgroundAudio.Play(); g_OSD.Update(); @@ -1147,7 +1141,7 @@ void NativeFrame(GraphicsContext *graphicsContext) { g_screenManager->getUIContext()->SetTintSaturation(g_Config.fUITint, g_Config.fUISaturation); // All actual rendering happen in here. - g_screenManager->render(); + ScreenRenderFlags renderFlags = g_screenManager->render(); if (g_screenManager->getUIContext()->Text()) { g_screenManager->getUIContext()->Text()->OncePerFrame(); } @@ -1200,7 +1194,7 @@ void NativeFrame(GraphicsContext *graphicsContext) { graphicsContext->Poll(); } - if (menuThrottle) { + if (!(renderFlags & ScreenRenderFlags::HANDLED_THROTTLING)) { float refreshRate = System_GetPropertyFloat(SYSPROP_DISPLAY_REFRESH_RATE); // Simple throttling to not burn the GPU in the menu. // TODO: This should move into NativeFrame. Also, it's only necessary in MAILBOX or IMMEDIATE presentation modes. @@ -1208,6 +1202,9 @@ void NativeFrame(GraphicsContext *graphicsContext) { int sleepTime = (int)(1000.0 / refreshRate) - (int)(diffTime * 1000.0); if (sleepTime > 0) sleep_ms(sleepTime); + + // TODO: We should ideally mix this with game audio. + g_BackgroundAudio.Play(); } SendMouseDeltaAxis(); diff --git a/UI/OnScreenDisplay.cpp b/UI/OnScreenDisplay.cpp index 0edaa36b0203..9781a251a604 100644 --- a/UI/OnScreenDisplay.cpp +++ b/UI/OnScreenDisplay.cpp @@ -517,9 +517,7 @@ void OSDOverlayScreen::CreateViews() { osmView_ = root_->Add(new OnScreenMessagesView(new UI::AnchorLayoutParams(0.0f, 0.0f, 0.0f, 0.0f))); } -void OSDOverlayScreen::render(ScreenRenderMode mode) { - UIScreen::render(mode); - +void OSDOverlayScreen::DrawForeground(UIContext &ui) { DebugOverlay debugOverlay = (DebugOverlay)g_Config.iDebugOverlay; // Special case control for now, since it uses the control mapper that's owned by EmuScreen. diff --git a/UI/OnScreenDisplay.h b/UI/OnScreenDisplay.h index 1440fdca635b..85159e0d50fa 100644 --- a/UI/OnScreenDisplay.h +++ b/UI/OnScreenDisplay.h @@ -41,7 +41,7 @@ class OSDOverlayScreen : public UIScreen { bool UnsyncTouch(const TouchInput &touch) override; void CreateViews() override; - void render(ScreenRenderMode mode) override; + void DrawForeground(UIContext &ui) override; void update() override; private: diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index d2bf2e78b5b9..bf329710eb2d 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -414,6 +414,8 @@ void GamePauseScreen::CreateViews() { } else { rightColumnItems->Add(new Choice(pa->T("Exit to menu")))->OnClick.Handle(this, &GamePauseScreen::OnExitToMenu); } + rightColumnItems->Add(new Spacer(25.0f)); + rightColumnItems->Add(new CheckBox(&g_Config.bRunBehindPauseMenu, "Run Behind")); } UI::EventReturn GamePauseScreen::OnGameSettings(UI::EventParams &e) { diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index c7e32dc180bc..ec8d1d2bdc28 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -65,4 +65,6 @@ class GamePauseScreen : public UIDialogScreenWithGameBackground { // hack bool finishNextFrame_ = false; PauseScreenMode mode_ = PauseScreenMode::MAIN; + + UI::Button *pauseButton_ = nullptr; }; diff --git a/UI/ReportScreen.cpp b/UI/ReportScreen.cpp index 5ec3415785ef..cff056929065 100644 --- a/UI/ReportScreen.cpp +++ b/UI/ReportScreen.cpp @@ -166,8 +166,8 @@ ReportScreen::ReportScreen(const Path &gamePath) ratingEnabled_ = enableReporting_; } -void ReportScreen::render(ScreenRenderMode mode) { - UIScreen::render(mode); +ScreenRenderFlags ReportScreen::render(ScreenRenderMode mode) { + ScreenRenderFlags flags = UIScreen::render(mode); if (mode & ScreenRenderMode::TOP) { @@ -189,6 +189,7 @@ void ReportScreen::render(ScreenRenderMode mode) { tookScreenshot_ = true; } } + return flags; } void ReportScreen::update() { diff --git a/UI/ReportScreen.h b/UI/ReportScreen.h index cd7c55ab10d6..c287042dead4 100644 --- a/UI/ReportScreen.h +++ b/UI/ReportScreen.h @@ -40,7 +40,7 @@ class ReportScreen : public UIDialogScreenWithGameBackground { const char *tag() const override { return "Report"; } protected: - void render(ScreenRenderMode mode) override; + ScreenRenderFlags render(ScreenRenderMode mode) override; void update() override; void resized() override; void CreateViews() override; From 8d8ff5886bdc9cebed7b02e546f23c699a553b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 11 Dec 2023 13:06:15 +0100 Subject: [PATCH 2/4] Fix issue where nothing had started a render pass when we wanted to clear the screen. --- GPU/Common/FramebufferManagerCommon.cpp | 6 +++++- GPU/Common/FramebufferManagerCommon.h | 2 ++ GPU/Common/PresentationCommon.cpp | 1 + GPU/Common/PresentationCommon.h | 8 ++++++++ GPU/GPUCommon.cpp | 4 ++++ GPU/GPUCommon.h | 2 ++ GPU/GPUCommonHW.cpp | 1 - GPU/GPUInterface.h | 1 + UI/EmuScreen.cpp | 5 ++--- 9 files changed, 25 insertions(+), 5 deletions(-) diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index b06df186c0aa..bce9ec3872f8 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -123,10 +123,14 @@ void FramebufferManagerCommon::CheckPostShaders() { void FramebufferManagerCommon::BeginFrame() { DecimateFBOs(); - + presentation_->BeginFrame(); currentRenderVfb_ = nullptr; } +bool FramebufferManagerCommon::PresentedThisFrame() const { + return presentation_->PresentedThisFrame(); +} + void FramebufferManagerCommon::SetDisplayFramebuffer(u32 framebuf, u32 stride, GEBufferFormat format) { displayFramebufPtr_ = framebuf & 0x3FFFFFFF; if (Memory::IsVRAMAddress(displayFramebufPtr_)) diff --git a/GPU/Common/FramebufferManagerCommon.h b/GPU/Common/FramebufferManagerCommon.h index 2f3bf5095483..02b0c467fc0a 100644 --- a/GPU/Common/FramebufferManagerCommon.h +++ b/GPU/Common/FramebufferManagerCommon.h @@ -485,6 +485,8 @@ class FramebufferManagerCommon { currentFramebufferCopy_ = nullptr; } + bool PresentedThisFrame() const; + protected: virtual void ReadbackFramebuffer(VirtualFramebuffer *vfb, int x, int y, int w, int h, RasterChannel channel, Draw::ReadbackMode mode); // Used for when a shader is required, such as GLES. diff --git a/GPU/Common/PresentationCommon.cpp b/GPU/Common/PresentationCommon.cpp index d86db4bd9e6d..2494c48ac8e0 100644 --- a/GPU/Common/PresentationCommon.cpp +++ b/GPU/Common/PresentationCommon.cpp @@ -931,6 +931,7 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u draw_->Invalidate(InvalidationFlags::CACHED_RENDER_STATE); previousUniforms_ = uniforms; + presentedThisFrame_ = true; } void PresentationCommon::CalculateRenderResolution(int *width, int *height, int *scaleFactor, bool *upscaling, bool *ssaa) const { diff --git a/GPU/Common/PresentationCommon.h b/GPU/Common/PresentationCommon.h index 9b840c669e83..21be2359b3f5 100644 --- a/GPU/Common/PresentationCommon.h +++ b/GPU/Common/PresentationCommon.h @@ -98,6 +98,13 @@ class PresentationCommon { bool UpdatePostShader(); + void BeginFrame() { + presentedThisFrame_ = false; + } + bool PresentedThisFrame() const { + return presentedThisFrame_; + } + void DeviceLost(); void DeviceRestore(Draw::DrawContext *draw); @@ -159,6 +166,7 @@ class PresentationCommon { bool usePostShader_ = false; bool restorePostShader_ = false; + bool presentedThisFrame_ = false; ShaderLanguage lang_; struct PrevFBO { diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index 4e19b0f5177c..ac8c557156d3 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -722,6 +722,10 @@ void GPUCommon::BeginFrame() { GPURecord::NotifyBeginFrame(); } +bool GPUCommon::PresentedThisFrame() const { + return framebufferManager_ ? framebufferManager_->PresentedThisFrame() : true; +} + void GPUCommon::SlowRunLoop(DisplayList &list) { const bool dumpThisFrame = dumpThisFrame_; while (downcount > 0) { diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index 012f299d335c..96cdf5d1988d 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -226,6 +226,8 @@ class GPUCommon : public GPUInterface, public GPUDebugInterface { fullInfo = reportingFullInfo_; } + bool PresentedThisFrame() const override; + protected: void ClearCacheNextFrame() override {} diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index 66906f4b1b47..3bce75f53b3f 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -503,7 +503,6 @@ void GPUCommonHW::UpdateCmdInfo() { void GPUCommonHW::BeginFrame() { GPUCommon::BeginFrame(); - if (drawEngineCommon_->EverUsedExactEqualDepth() && !sawExactEqualDepth_) { sawExactEqualDepth_ = true; gstate_c.SetUseFlags(CheckGPUFeatures()); diff --git a/GPU/GPUInterface.h b/GPU/GPUInterface.h index 496b23057f4b..8e526b541cb4 100644 --- a/GPU/GPUInterface.h +++ b/GPU/GPUInterface.h @@ -253,6 +253,7 @@ class GPUInterface { virtual bool FramebufferDirty() = 0; virtual bool FramebufferReallyDirty() = 0; virtual bool BusyDrawing() = 0; + virtual bool PresentedThisFrame() const = 0; // If any jit is being used inside the GPU. virtual bool DescribeCodePtr(const u8 *ptr, std::string &name) = 0; diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 5620f90639e9..51f1fc47c44b 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1568,15 +1568,14 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { PSP_EndHostFrame(); } - // This must happen after PSP_EndHostFrame so that things like push buffers are end-frame'd before we start destroying stuff. - if (checkPowerDown() || rebind) { - // Shutting down can end up ending the current render pass + if (gpu && !gpu->PresentedThisFrame()) { draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_NoFrame"); } if (!(mode & ScreenRenderMode::TOP)) { // We're in run-behind mode, but we don't want to draw chat, debug UI and stuff. // So, darken and bail here. + darken(); return flags; } From 6e369e5188b4206ff00507e2877d16d244d345a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 11 Dec 2023 13:56:16 +0100 Subject: [PATCH 3/4] Add play/pause button to the bottom right of the pause screen --- Common/UI/View.h | 12 ++++++++++ Common/UI/ViewGroup.cpp | 10 ++++++++- UI/PauseScreen.cpp | 21 ++++++++++++++---- UI/PauseScreen.h | 2 +- assets/ui_atlas.meta | Bin 1643 -> 1670 bytes assets/ui_atlas.zim | Bin 72411 -> 72624 bytes buildatlas.sh | 8 +++---- .../image/{pausepng.png => pause.png} | Bin ui_atlasscript.txt | 1 + 9 files changed, 44 insertions(+), 10 deletions(-) rename source_assets/image/{pausepng.png => pause.png} (100%) diff --git a/Common/UI/View.h b/Common/UI/View.h index 190da0b115b1..87941167ccf5 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -903,6 +903,18 @@ class Spacer : public InertView { void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { w = size_; h = size_; } + + void GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const override { + if (horiz.type == AT_MOST || horiz.type == EXACTLY) + w = horiz.size; + else + w = size_; + if (vert.type == AT_MOST || vert.type == EXACTLY) + h = vert.size; + else + h = size_; + } + void Draw(UIContext &dc) override {} std::string DescribeText() const override { return ""; } diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp index 229ef8206268..b78781f20fcd 100644 --- a/Common/UI/ViewGroup.cpp +++ b/Common/UI/ViewGroup.cpp @@ -25,7 +25,7 @@ namespace UI { static constexpr Size ITEM_HEIGHT = 64.f; -void ApplyGravity(const Bounds outer, const Margins &margins, float w, float h, int gravity, Bounds &inner) { +void ApplyGravity(const Bounds &outer, const Margins &margins, float w, float h, int gravity, Bounds &inner) { inner.w = w; inner.h = h; @@ -495,6 +495,10 @@ void LinearLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec v if (views_.empty()) return; + if (tag_ == "debug") { + tag_ = "debug"; + } + float sum = 0.0f; float maxOther = 0.0f; float totalWeight = 0.0f; @@ -666,6 +670,10 @@ void LinearLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec v void LinearLayout::Layout() { const Bounds &bounds = bounds_; + if (tag_ == "debug") { + tag_ = "debug"; + } + Bounds itemBounds; float pos; diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index bf329710eb2d..c94f9abcd737 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -356,8 +356,12 @@ void GamePauseScreen::CreateViews() { leftColumnItems->Add(new NoticeView(NoticeLevel::INFO, notAvailable, "")); } - ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(vertical ? 200 : 300, FILL_PARENT, actionMenuMargins)); - root_->Add(rightColumn); + ViewGroup *rightColumnHolder = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(vertical ? 200 : 300, FILL_PARENT, actionMenuMargins)); + + ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)); + rightColumnHolder->Add(rightColumn); + + root_->Add(rightColumnHolder); LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL); rightColumn->Add(rightColumnItems); @@ -414,8 +418,17 @@ void GamePauseScreen::CreateViews() { } else { rightColumnItems->Add(new Choice(pa->T("Exit to menu")))->OnClick.Handle(this, &GamePauseScreen::OnExitToMenu); } - rightColumnItems->Add(new Spacer(25.0f)); - rightColumnItems->Add(new CheckBox(&g_Config.bRunBehindPauseMenu, "Run Behind")); + + ViewGroup *playControls = rightColumnHolder->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + playControls->SetTag("debug"); + playControls->Add(new Spacer(new LinearLayoutParams(1.0f))); + playButton_ = playControls->Add(new Button("", g_Config.bRunBehindPauseMenu ? ImageID("I_PAUSE") : ImageID("I_PLAY"), new LinearLayoutParams(0.0f, G_RIGHT))); + playButton_->OnClick.Add([=](UI::EventParams &e) { + g_Config.bRunBehindPauseMenu = !g_Config.bRunBehindPauseMenu; + playButton_->SetImageID(g_Config.bRunBehindPauseMenu ? ImageID("I_PAUSE") : ImageID("I_PLAY")); + return UI::EVENT_DONE; + }); + rightColumnHolder->Add(new Spacer(10.0f)); } UI::EventReturn GamePauseScreen::OnGameSettings(UI::EventParams &e) { diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index ec8d1d2bdc28..ee4aaf2cdcbf 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -66,5 +66,5 @@ class GamePauseScreen : public UIDialogScreenWithGameBackground { bool finishNextFrame_ = false; PauseScreenMode mode_ = PauseScreenMode::MAIN; - UI::Button *pauseButton_ = nullptr; + UI::Button *playButton_ = nullptr; }; diff --git a/assets/ui_atlas.meta b/assets/ui_atlas.meta index 83517b60b9910dba4db4bbe7dbc82f67e044610b..aa5453eab386215f97b87628d79f4a495869a95b 100644 GIT binary patch literal 1670 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 diff --git a/assets/ui_atlas.zim b/assets/ui_atlas.zim index 15b934c5d31596a42fc503cfb170f9d20208062f..d7ba96a849553e37052d830a9dc63dfe79744bef 100644 GIT binary patch delta 16742 zcmWif^;?q-8-|}T-01Ea0s9kW`RvR8YE`jqZ}}(cN9! z_kMrE{mXS6_j#SyR5@``C2`T{BzW})l4R@1UJEIQR6^9_nOam;2WHC*K0q*~ky^C0 zBMa6p5#j@jpQLSSd-Hvgy-L|@ug0T}>PFX+tnM&X$?L*yt-t3dr{}@+dB3HlNUJ5` z@=3PTQ)2gJ7N_E<{@T$8n_~}2Fg-d;Z#n@WgdW@7I+%r-v%^P7&>#^E3}gAn80Vg- zz%`N8+bxj(aMJ8&4&{u(n{T!v8&#qH0Rb6CHhD~(uX=2)WL~*^f9X$ghlORUP%65% zV{f5}Yf_SbrwV!kMorc%G8N(?NkG^bSX|;hMyX$25x_UHK zOAOnKIhdEa;QVlW39$i-f&U$Vmv+$Z%KoaR!2vhj_=^Z}+~H>!xune_^OYkk0!Yt` zsgz~kD}-}kT0Lie-UlVdjm8jj~TX`;oG^x3WZ^$iu{l;ecQ-)mc8ydq!hb z{k@Y*v9YO>t-65i&wgRxr5oD8ty1q%$$F0QIkWaGhrnxcg&HV5?eZY+$ofc&|HaU3|3NK%- zFsS^AuO)hnEhC9GUa6(`G#Od-E

DY{^amT#^nyrF9Yh-@**1IBXxv)^T`%d~H8m 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 RjiU=A(>sV)#fWhrF9E;3<5>Uz delta 16528 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 MkIDwj?BXFhCrE>5v;Y7A diff --git a/buildatlas.sh b/buildatlas.sh index 4e20f19b814c..2866f9959d61 100755 --- a/buildatlas.sh +++ b/buildatlas.sh @@ -1,9 +1,9 @@ # Note that we do not copy the big font atlas to Android assets. No longer needed! ./ext/native/tools/build/atlastool ui_atlasscript.txt ui 8888 && cp ui_atlas.zim ui_atlas.meta assets && rm ui_atlas.cpp ui_atlas.h -./ext/native/tools/build/atlastool font_atlasscript.txt font 8888 && cp font_atlas.zim font_atlas.meta assets && rm font_atlas.cpp font_atlas.h -./ext/native/tools/build/atlastool asciifont_atlasscript.txt asciifont 8888 && cp asciifont_atlas.zim asciifont_atlas.meta assets && rm asciifont_atlas.cpp asciifont_atlas.h +#./ext/native/tools/build/atlastool font_atlasscript.txt font 8888 && cp font_atlas.zim font_atlas.meta assets && rm font_atlas.cpp font_atlas.h +#./ext/native/tools/build/atlastool asciifont_atlasscript.txt asciifont 8888 && cp asciifont_atlas.zim asciifont_atlas.meta assets && rm asciifont_atlas.cpp asciifont_atlas.h rm ui_atlas.zim ui_atlas.meta -rm font_atlas.zim font_atlas.meta -rm asciifont_atlas.zim asciifont_atlas.meta +#rm font_atlas.zim font_atlas.meta +#rm asciifont_atlas.zim asciifont_atlas.meta diff --git a/source_assets/image/pausepng.png b/source_assets/image/pause.png similarity index 100% rename from source_assets/image/pausepng.png rename to source_assets/image/pause.png diff --git a/ui_atlasscript.txt b/ui_atlasscript.txt index ded4b2314307..1bcd79bef39c 100644 --- a/ui_atlasscript.txt +++ b/ui_atlasscript.txt @@ -74,6 +74,7 @@ image I_RETROACHIEVEMENTS_LOGO source_assets/image/retroachievements_logo.png co image I_CHECKMARK source_assets/image/checkmark.png copy image I_PLAY source_assets/image/play.png copy image I_STOP source_assets/image/stop.png copy +image I_PAUSE source_assets/image/pause.png copy image I_FASTFORWARD source_assets/image/fast_forward.png copy image I_RECORD source_assets/image/record.png copy image I_SPEAKER source_assets/image/speaker.png copy From 25ab7b917037979a5aa430121582bb1eb66abe0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 11 Dec 2023 15:58:08 +0100 Subject: [PATCH 4/4] Fix a bunch of edge cases --- Common/UI/Screen.cpp | 5 +++-- Common/UI/Screen.h | 2 +- UI/EmuScreen.cpp | 23 ++++++++++++++++++----- UI/EmuScreen.h | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Common/UI/Screen.cpp b/Common/UI/Screen.cpp index 1bf1d32f95f4..bf626ce653bd 100644 --- a/Common/UI/Screen.cpp +++ b/Common/UI/Screen.cpp @@ -168,9 +168,10 @@ ScreenRenderFlags ScreenManager::render() { auto iter = stack_.end(); Screen *coveringScreen = nullptr; Screen *backgroundScreen = nullptr; + bool first = true; do { --iter; - if (!backgroundScreen && iter->screen->canBeBackground()) { + if (!backgroundScreen && iter->screen->canBeBackground(first)) { // There still might be a screen that wants to be background - generally the EmuScreen if present. layers.push_back(iter->screen); backgroundScreen = iter->screen; @@ -180,6 +181,7 @@ ScreenRenderFlags ScreenManager::render() { if (iter->flags != LAYER_TRANSPARENT) { coveringScreen = iter->screen; } + first = false; } while (iter != stack_.begin()); // Confusing-looking expression, argh! Note the '_' @@ -189,7 +191,6 @@ ScreenRenderFlags ScreenManager::render() { } // OK, now we iterate backwards over our little pile of collected screens. - bool first = true; for (int i = (int)layers.size() - 1; i >= 0; i--) { ScreenRenderMode mode = ScreenRenderMode::DEFAULT; if (i == (int)layers.size() - 1) { diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index e65b0d8d7993..e7232a84009e 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -76,7 +76,7 @@ class Screen { virtual void sendMessage(UIMessage message, const char *value) {} virtual void deviceLost() {} virtual void deviceRestored() {} - virtual bool canBeBackground() const { return false; } + virtual bool canBeBackground(bool isTop) const { return false; } virtual bool wantBrightBackground() const { return false; } // special hack for DisplayLayoutScreen. virtual void focusChanged(ScreenFocusChange focusChange); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 51f1fc47c44b..74d34a2c0c04 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1415,9 +1415,10 @@ static void DrawFPS(UIContext *ctx, const Bounds &bounds) { ctx->RebindTexture(); } -bool EmuScreen::canBeBackground() const { - if (g_Config.bSkipBufferEffects) - return false; +bool EmuScreen::canBeBackground(bool isTop) const { + if (g_Config.bSkipBufferEffects) { + return isTop || (g_Config.bTransparentBackground && g_Config.bRunBehindPauseMenu); + } bool forceTransparent = false; // this needs to be true somehow on the display layout screen. @@ -1446,6 +1447,8 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { if (!draw) return flags; // shouldn't really happen but I've seen a suspicious stack trace.. + bool skipBufferEffects = g_Config.bSkipBufferEffects; + if (mode & ScreenRenderMode::FIRST) { // Actually, always gonna be first when it exists (?) @@ -1472,6 +1475,8 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { viewport.MaxDepth = 1.0; viewport.MinDepth = 0.0; draw->SetViewport(viewport); + + skipBufferEffects = true; } draw->SetTargetSize(g_display.pixel_xres, g_display.pixel_yres); } @@ -1568,14 +1573,22 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { PSP_EndHostFrame(); } - if (gpu && !gpu->PresentedThisFrame()) { + if (gpu && !gpu->PresentedThisFrame() && !skipBufferEffects) { draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_NoFrame"); + Viewport viewport{ 0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f }; + draw->SetViewport(viewport); + draw->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres); } if (!(mode & ScreenRenderMode::TOP)) { // We're in run-behind mode, but we don't want to draw chat, debug UI and stuff. // So, darken and bail here. - + if (skipBufferEffects) { + // Need to reset viewport/scissor. + Viewport viewport{ 0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f }; + draw->SetViewport(viewport); + draw->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres); + } darken(); return flags; } diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index a97273027450..bcf3171a3e48 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -46,7 +46,7 @@ class EmuScreen : public UIScreen { void dialogFinished(const Screen *dialog, DialogResult result) override; void sendMessage(UIMessage message, const char *value) override; void resized() override; - bool canBeBackground() const override; + bool canBeBackground(bool isTop) const override; // Note: Unlike your average boring UIScreen, here we override the Unsync* functions // to get minimal latency and full control. We forward to UIScreen when needed.