From 519f282f9982cfc2e9a94faf6210d0c8ed5b397e Mon Sep 17 00:00:00 2001 From: LunaMoo Date: Thu, 22 Oct 2026 12:41:36 +0200 Subject: [PATCH] Overlay: follow display offset/scale, avoid needless reloads - Apply the display layout's scale and offset to the overlay itself instead of always centering it, so moving/resizing the display in Display Layout moves the overlay with the game inside it, and the portrait default (screen high up) is respected. - Rescanning overlays (done whenever the display layout screen recreates its views, e.g. after closing the picker popup) no longer unloads the active overlay unless its definition changed. - Re-picking the current overlay no longer posts GPU config change notifications, which caused an audio glitch with a game running behind. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QGBg32EFPsTXjAFu7dnTcz --- GPU/Common/Overlay.cpp | 26 +++++++++++++++++++++----- GPU/Common/Overlay.h | 11 ++++++----- GPU/Common/PresentationCommon.cpp | 4 ++-- UI/DisplayLayoutScreen.cpp | 6 +++++- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/GPU/Common/Overlay.cpp b/GPU/Common/Overlay.cpp index 734c594b96..8445f1ad82 100644 --- a/GPU/Common/Overlay.cpp +++ b/GPU/Common/Overlay.cpp @@ -172,7 +172,19 @@ void ReloadAllOverlayInfo() { std::lock_guard guard(g_overlayLock); ScanOverlays(&g_overlayInfo); g_overlayInfoLoaded = true; - UnloadActiveLocked(); + + // Keep the loaded overlay if its definition didn't change, so that rescanning (which the + // display layout screen does every time its views are recreated) doesn't cause a reload. + if (!g_activeId.empty()) { + auto iter = std::find_if(g_overlayInfo.begin(), g_overlayInfo.end(), [](const OverlayInfo &info) { return info.id == g_activeId; }); + const bool unchanged = iter != g_overlayInfo.end() && !g_activeFailed && + iter->imagePath == g_activeInfo.imagePath && iter->layer == g_activeInfo.layer && + iter->holeFromIni == g_activeInfo.holeFromIni && iter->holeX == g_activeInfo.holeX && + iter->holeY == g_activeInfo.holeY && iter->holeW == g_activeInfo.holeW && iter->holeH == g_activeInfo.holeH; + if (!unchanged) { + UnloadActiveLocked(); + } + } } std::vector GetAllOverlayInfo() { @@ -216,6 +228,7 @@ bool ImportOverlayImage(const Path &source, std::string *id) { return false; } INFO_LOG(Log::G3D, "Imported overlay %s as %s", source.c_str(), filename.c_str()); + // The file name is new, so this can't affect the loaded overlay. ReloadAllOverlayInfo(); *id = filename; return true; @@ -305,12 +318,15 @@ bool GetOverlayLayout(const DisplayLayoutConfig &config, float frameX, float fra const float imgW = (float)g_activeImage->width; const float imgH = (float)g_activeImage->height; - // Fit the overlay to the frame, keeping the aspect ratio, centered. - const float scale = std::min(frameW / imgW, frameH / imgH); + // Fit the overlay to the frame, keeping the aspect ratio. Then apply the display layout's scale + // and offset to the overlay, the same way they'd apply to the game without one, so moving and + // resizing the display moves the overlay (with the game inside it). This also keeps the portrait + // default of having the screen high up. + const float scale = std::min(frameW / imgW, frameH / imgH) * config.fDisplayScale; layout->overlayW = imgW * scale; layout->overlayH = imgH * scale; - layout->overlayX = frameX + (frameW - layout->overlayW) * 0.5f; - layout->overlayY = frameY + (frameH - layout->overlayH) * 0.5f; + layout->overlayX = frameX + frameW * config.fDisplayOffsetX - layout->overlayW * 0.5f; + layout->overlayY = frameY + frameH * config.fDisplayOffsetY - layout->overlayH * 0.5f; layout->layer = g_activeInfo.layer; layout->hasHole = g_activeHasHole; diff --git a/GPU/Common/Overlay.h b/GPU/Common/Overlay.h index ed44d033c6..9ac32ddebd 100644 --- a/GPU/Common/Overlay.h +++ b/GPU/Common/Overlay.h @@ -33,10 +33,11 @@ // Layer=Foreground ; or Background (drawn behind the game) // Hole=0,0,640,362 ; optional, in image pixels. Auto-detected if missing. // -// Placement: the overlay is fit to the screen (aspect preserved, centered). The hole is mapped -// to the screen with the same scale and offset, and the PSP screen is then fit into the hole -// using the normal display layout options (stretch, integer scale, aspect ratio). -// The display offset and scale options are ignored while an overlay with a hole is active. +// Placement: the overlay is fit to the screen (aspect preserved), then the display layout's scale +// and offset are applied to it, like they normally are to the game. So moving or resizing the +// display moves the overlay, and the portrait default (screen high up) works. The hole is mapped +// with the same scale and offset, and the PSP screen is fit into the hole using the remaining +// display options (stretch, integer scale, aspect ratio). #pragma once @@ -86,7 +87,7 @@ struct OverlayLayout { uint32_t generation = 0; }; -// Rescans the overlays folder. Also unloads the active overlay, so it gets reloaded from disk. +// Rescans the overlays folder. The loaded overlay is kept unless its definition changed or it's gone. void ReloadAllOverlayInfo(); std::vector GetAllOverlayInfo(); diff --git a/GPU/Common/PresentationCommon.cpp b/GPU/Common/PresentationCommon.cpp index 5ef649757d..b504f5d98b 100644 --- a/GPU/Common/PresentationCommon.cpp +++ b/GPU/Common/PresentationCommon.cpp @@ -108,8 +108,8 @@ void CalculateDisplayOutputRect(const DisplayLayoutConfig &config, FRect *rc, fl float scale = config.fDisplayScale; - // With an overlay that has a hole, the overlay is fit to the screen and the game is fit into - // the hole instead. The offset and scale options don't apply then, the overlay decides those. + // With an overlay that has a hole, the game is fit into the hole instead. The offset and scale + // options were already applied to the overlay itself, see GetOverlayLayout. FRect frame = screenFrame; OverlayLayout overlay; const bool inOverlayHole = GetOverlayLayout(config, screenFrame.x, screenFrame.y, screenFrame.w, screenFrame.h, &overlay) && overlay.hasHole; diff --git a/UI/DisplayLayoutScreen.cpp b/UI/DisplayLayoutScreen.cpp index 63c99b4105..40423197e6 100644 --- a/UI/DisplayLayoutScreen.cpp +++ b/UI/DisplayLayoutScreen.cpp @@ -370,7 +370,11 @@ void DisplayLayoutScreen::CreateViews() { } if (overlayIds.size() > 1) { auto overlayChoice = leftColumn->Add(new PopupMultiChoiceDynamic(&config.sOverlay, gr->T("Overlay"), overlayNames, I18NCat::NONE, screenManager(), &overlayIds)); - overlayChoice->OnChoice.Add([](UI::EventParams &) { + overlayChoice->OnChoice.Add([](UI::EventParams &e) { + // e.b is set if the value actually changed. Re-picking the current one shouldn't + // cause a reload, since that glitches the audio of a game running in the background. + if (!e.b) + return; // The game rect may have moved, which matters for post shaders that run at output resolution. NotifyPostChanges(); });