From 088f05726bdc28627e45342fd4b376694f3a6ab4 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 22 Feb 2026 17:07:43 +1000 Subject: [PATCH] FullscreenUI: Add background blur to notifications/menus --- dep/imgui/include/imconfig.h | 7 + src/core/fullscreenui.cpp | 27 +- src/core/fullscreenui_achievements.cpp | 146 +++++--- src/core/fullscreenui_settings.cpp | 38 +- src/core/fullscreenui_strings.h | 6 + src/core/fullscreenui_widgets.cpp | 425 ++++++++++++++++++++++- src/core/fullscreenui_widgets.h | 28 +- src/core/settings.cpp | 4 + src/core/settings.h | 2 + src/core/system.cpp | 2 + src/core/video_presenter.cpp | 121 ++++--- src/duckstation-qt/osdsettingswidget.cpp | 8 + src/duckstation-qt/osdsettingswidget.ui | 34 +- src/util/imgui_manager.cpp | 29 +- src/util/shadergen.cpp | 141 ++++++++ src/util/shadergen.h | 3 + 16 files changed, 872 insertions(+), 149 deletions(-) diff --git a/dep/imgui/include/imconfig.h b/dep/imgui/include/imconfig.h index 14f66b2d7..ff2c6c9b1 100644 --- a/dep/imgui/include/imconfig.h +++ b/dep/imgui/include/imconfig.h @@ -149,3 +149,10 @@ namespace ImGui // Use our texture type class GPUTexture; #define ImTextureID GPUTexture* + +// Add base vertex/offset to draw callback +struct ImDrawList; +struct ImDrawCmd; +typedef void (*ImDrawWithOffsetCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, unsigned int vertex_offset, + unsigned int index_offset); +#define ImDrawCallback ImDrawWithOffsetCallback diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index 2e560e78b..6b85131ce 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -63,7 +63,8 @@ static bool HasBackground(); static bool LoadBackgroundShader(const std::string& path, Error* error); static bool LoadBackgroundImage(const std::string& path, Error* error); static void DrawBackground(); -static void DrawShaderBackgroundCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd); +static void DrawShaderBackgroundCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd, u32 base_vertex, + u32 base_index); ////////////////////////////////////////////////////////////////////////// // Resources @@ -1151,7 +1152,8 @@ bool FullscreenUI::LoadBackgroundShader(const std::string& path, Error* error) return true; } -void FullscreenUI::DrawShaderBackgroundCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd) +void FullscreenUI::DrawShaderBackgroundCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd, u32 base_vertex, + u32 base_index) { if (!g_gpu_device->HasMainSwapChain()) return; @@ -1515,7 +1517,7 @@ void FullscreenUI::DrawPauseMenu() SmallString buffer; - ImDrawList* dl = ImGui::GetBackgroundDrawList(); + ImDrawList* const dl = ImGui::GetBackgroundDrawList(); const ImVec2 display_size(ImGui::GetIO().DisplaySize); const ImU32 title_text_color = ImGui::GetColorU32(UIStyle.BackgroundTextColor); const ImU32 text_color = ImGui::GetColorU32(DarkerColor(UIStyle.BackgroundTextColor, 0.85f)); @@ -1526,8 +1528,17 @@ void FullscreenUI::DrawPauseMenu() { const float scaled_text_spacing = LayoutScale(4.0f); const float scaled_top_bar_padding = LayoutScale(top_bar_padding); - dl->AddRectFilled(ImVec2(0.0f, 0.0f), ImVec2(display_size.x, scaled_top_bar_height), - ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 0.95f)), 0.0f); + const ImVec2 top_bar_min = ImVec2(0.0f, 0.0f); + const ImVec2 top_bar_max = ImVec2(display_size.x, scaled_top_bar_height); + if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, top_bar_min, top_bar_max)) + { + dl->AddRectFilled(top_bar_min, top_bar_max, ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 1.0f)), 0.0f); + EndBlurBackground(dl); + } + else + { + dl->AddRectFilled(top_bar_min, top_bar_max, ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 0.95f)), 0.0f); + } const std::string& game_title = VideoThread::GetGameTitle(); const std::string& game_serial = VideoThread::GetGameSerial(); @@ -1744,6 +1755,8 @@ void FullscreenUI::DrawPauseMenu() EndFullscreenWindow(); + SetFullscreenFooterBlur(false); + if (IsGamepadInputSource()) { SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")), @@ -1949,7 +1962,7 @@ void FullscreenUI::DrawSaveStateSelector() closed = true; if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "##save_state_selector_title", - ModAlpha(UIStyle.PrimaryColor, GetBackgroundAlpha()))) + ModAlpha(UIStyle.PrimaryColor, GetBackgroundAlpha()), 0.0f, ImVec2(), 0, true)) { BeginNavBar(); if (NavButton(ICON_PF_NAVIGATION_BACK, true, true)) @@ -1968,7 +1981,7 @@ void FullscreenUI::DrawSaveStateSelector() ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "##save_state_selector_list", ModAlpha(UIStyle.BackgroundColor, GetBackgroundAlpha()), 0.0f, - ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING))) + ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true)) { ResetFocusHere(); BeginMenuButtons(); diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index 3ee703dfc..bb3ec2c3a 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -347,6 +347,8 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) const float note_icon_size = ImCeil(LAYOUT_LARGE_FONT_SIZE * scale); const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f); const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f); + const bool blur_background = g_gpu_settings.display_blur_message_backgrounds && !FullscreenUI::HasActiveWindow() && + FullscreenUI::CanBlurBackground(); ImDrawList* const dl = ImGui::GetForegroundDrawList(); for (auto iter = s_achievements_locals.notifications.begin(); iter != s_achievements_locals.notifications.end();) @@ -468,15 +470,24 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) const ImVec2 box_min(expected_pos.x, actual_y); const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height); - const float background_opacity = opacity * 0.95f; - const float min_rounded_width = horizontal_padding * 2.0f; - DrawRoundedGradientRect( - dl, box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity)), - ImGui::GetColorU32(ModAlpha(ImLerp(left_background_color, right_background_color, - (box_width - min_rounded_width) / (max_width - min_rounded_width)), - background_opacity)), - horizontal_padding); + if (blur_background && BeginBlurBackground(dl, box_min, box_max)) + { + dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)), + horizontal_padding); + EndBlurBackground(dl); + } + else + { + const float background_opacity = opacity * 0.95f; + const float min_rounded_width = horizontal_padding * 2.0f; + DrawRoundedGradientRect( + dl, box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity)), + ImGui::GetColorU32(ModAlpha(ImLerp(left_background_color, right_background_color, + (box_width - min_rounded_width) / (max_width - min_rounded_width)), + background_opacity)), + horizontal_padding); + } const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding); const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size); @@ -561,7 +572,9 @@ void FullscreenUI::DrawIndicators(NotificationLayout& layout) const float image_size = ImCeil(50.0f * scale); const float font_size = ImCeil(LAYOUT_MEDIUM_FONT_SIZE * scale); const ImGuiIO& io = ImGui::GetIO(); - ImDrawList* dl = ImGui::GetBackgroundDrawList(); + const bool blur_background = g_gpu_settings.display_blur_message_backgrounds && !FullscreenUI::HasActiveWindow() && + FullscreenUI::CanBlurBackground(); + ImDrawList* const dl = ImGui::GetBackgroundDrawList(); if (std::vector& indicators = Achievements::GetActiveChallengeIndicators(); !indicators.empty() && @@ -628,9 +641,17 @@ void FullscreenUI::DrawIndicators(NotificationLayout& layout) Achievements::INDICATOR_FADE_OUT_TIME, INDICATOR_WIDTH_COEFF); const ImVec2 box_max = box_min + ImVec2(box_width, box_height); - DrawRoundedGradientRect(dl, box_min, box_max, - ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)), - ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding); + if (blur_background && BeginBlurBackground(dl, box_min, box_max)) + { + dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)), rounding); + EndBlurBackground(dl); + } + else + { + DrawRoundedGradientRect(dl, box_min, box_max, + ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)), + ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding); + } GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator->badge_path); if (badge) @@ -676,26 +697,34 @@ void FullscreenUI::DrawIndicators(NotificationLayout& layout) return UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(tstr)); }; - const auto draw_tracker = [&padding, &rounding, &font_size, &dl, &left_background_color, &right_background_color, - &tstr, &measure_tracker](Achievements::LeaderboardTrackerIndicator& indicator, - const ImVec2& pos, float opacity) { - const ImVec2 size = measure_tracker(indicator); - const float box_width = size.x + padding.x * 2.0f; - const float box_height = size.y + padding.y * 2.0f; - const ImRect box(pos, ImVec2(pos.x + box_width, pos.y + box_height)); + const auto draw_tracker = + [&padding, &rounding, &font_size, &blur_background, &dl, &left_background_color, &right_background_color, &tstr, + &measure_tracker](Achievements::LeaderboardTrackerIndicator& indicator, const ImVec2& pos, float opacity) { + const ImVec2 size = measure_tracker(indicator); + const float box_width = size.x + padding.x * 2.0f; + const float box_height = size.y + padding.y * 2.0f; + const ImRect box(pos, ImVec2(pos.x + box_width, pos.y + box_height)); - DrawRoundedGradientRect(dl, box.Min, box.Max, - ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)), - ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding); + if (blur_background && BeginBlurBackground(dl, box.Min, box.Max)) + { + dl->AddRectFilled(box.Min, box.Max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)), rounding); + EndBlurBackground(dl); + } + else + { + DrawRoundedGradientRect(dl, box.Min, box.Max, + ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)), + ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding); + } - tstr.format(ICON_FA_STOPWATCH " {}", indicator.text); + tstr.format(ICON_FA_STOPWATCH " {}", indicator.text); - const u32 text_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity)); - RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, box.Min + padding, box.Max, text_col, tstr, - nullptr, ImVec2(0.0f, 0.0f), 0.0f, &box); + const u32 text_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity)); + RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, box.Min + padding, box.Max, text_col, tstr, + nullptr, ImVec2(0.0f, 0.0f), 0.0f, &box); - return box_width; - }; + return box_width; + }; // animations are not currently handled for more than one tracker... but this should be rare if (trackers.size() > 1) @@ -938,7 +967,8 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) const float box_padding = LayoutScale(15.0f); const float box_content_width = box_width - box_padding - box_padding; const float box_rounding = LayoutScale(20.0f); - const u32 box_background_color = ImGui::GetColorU32(ModAlpha(UIStyle.PopupBackgroundColor, 0.8f)); + const u32 box_background_color = + ImGui::GetColorU32(ModAlpha(UIStyle.PopupBackgroundColor, UIStyle.BlurMenuBackground ? 1.0f : 0.8f)); const ImU32 box_title_text_color = ImGui::GetColorU32(DarkerColor(UIStyle.BackgroundTextColor, 0.9f)) | IM_COL32_A_MASK; const ImU32 title_text_color = ImGui::GetColorU32(UIStyle.BackgroundTextColor) | IM_COL32_A_MASK; @@ -965,7 +995,15 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) ImVec2 text_size; TinyString buffer; - dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max)) + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + EndBlurBackground(dl); + } + else + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + } // title { @@ -1037,7 +1075,15 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) box_max = ImVec2(box_min.x + box_width, box_min.y + box_height); text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding); - dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max)) + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + EndBlurBackground(dl); + } + else + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + } ImVec4 clip_rect = ImVec4(text_pos.x, text_pos.y, text_pos.x + box_content_width, box_max.y); dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, box_title_text_color, @@ -1227,7 +1273,15 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) box_max = ImVec2(box_min.x + box_width, box_min.y + box_height); text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding); - dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max)) + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + EndBlurBackground(dl); + } + else + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + } buffer.format(ICON_FA_HAND_FIST " {}", TRANSLATE_DISAMBIG_SV("Achievements", "Active Challenge Achievements", "Pause Menu")); @@ -1287,7 +1341,15 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) box_max = ImVec2(box_min.x + box_width, box_min.y + box_height); text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding); - dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max)) + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + EndBlurBackground(dl); + } + else + { + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + } buffer.format(ICON_FA_STOPWATCH " {}", TRANSLATE_DISAMBIG_SV("Achievements", "Active Leaderboard Attempts", "Pause Menu")); @@ -1627,15 +1689,15 @@ void FullscreenUI::DrawAchievementsWindow() ((summary.num_unsupported_achievements > 0) ? 20.0f : 0.0f); const ImVec4 background = ModAlpha(UIStyle.BackgroundColor, WINDOW_ALPHA); - const ImVec4 heading_background = ModAlpha(UIStyle.BackgroundColor, WINDOW_HEADING_ALPHA); + const ImVec4 heading_background = ModAlpha(DarkerColor(UIStyle.BackgroundColor, 1.5f), WINDOW_HEADING_ALPHA); const ImVec2 display_size = ImGui::GetIO().DisplaySize; const float heading_height = LayoutScale(heading_height_unscaled); bool close_window = false; if (BeginFullscreenWindow(ImVec2(), ImVec2(display_size.x, heading_height), "achievements_heading", heading_background, 0.0f, ImVec2(10.0f, 10.0f), - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoScrollWithMouse)) + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse, + true)) { const ImVec2 pos = ImGui::GetCursorScreenPos() + ImGui::GetStyle().FramePadding; const float spacing = LayoutScale(LAYOUT_MENU_ITEM_TITLE_SUMMARY_SPACING); @@ -1825,7 +1887,7 @@ void FullscreenUI::DrawAchievementsWindow() if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "achievements", background, 0.0f, - ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0)) + ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true)) { static constexpr std::pair bucket_names[] = { {ICON_FA_CIRCLE_QUESTION, TRANSLATE_NOOP("Achievements", "Unknown")}, @@ -2207,7 +2269,7 @@ void FullscreenUI::DrawLeaderboardsWindow() SmallString text; const ImVec4 background = ModAlpha(UIStyle.BackgroundColor, WINDOW_ALPHA); - const ImVec4 heading_background = ModAlpha(UIStyle.BackgroundColor, WINDOW_HEADING_ALPHA); + const ImVec4 heading_background = ModAlpha(DarkerColor(UIStyle.BackgroundColor, 1.5f), WINDOW_HEADING_ALPHA); const ImVec2 display_size = ImGui::GetIO().DisplaySize; const u32 text_color = ImGui::GetColorU32(ImGuiCol_Text); const float spacing = LayoutScale(10.0f); @@ -2216,8 +2278,8 @@ void FullscreenUI::DrawLeaderboardsWindow() if (BeginFullscreenWindow(ImVec2(), ImVec2(display_size.x, heading_height), "leaderboards_heading", heading_background, 0.0f, ImVec2(10.0f, 10.0f), - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoScrollWithMouse)) + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse, + true)) { const ImVec2 heading_pos = ImGui::GetCursorScreenPos() + ImGui::GetStyle().FramePadding; const float image_size = LayoutScale(75.0f); @@ -2369,7 +2431,7 @@ void FullscreenUI::DrawLeaderboardsWindow() if (BeginFullscreenWindow( ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "leaderboards", - background, 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0)) + background, 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true)) { ResetFocusHere(); BeginMenuButtons(); @@ -2430,7 +2492,7 @@ void FullscreenUI::DrawLeaderboardsWindow() if (BeginFullscreenWindow( ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "leaderboard", - background, 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0)) + background, 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true)) { const ImVec2 heading_start_pos = ImGui::GetCursorScreenPos(); ImVec2 column_heading_pos = heading_start_pos; diff --git a/src/core/fullscreenui_settings.cpp b/src/core/fullscreenui_settings.cpp index a4dba8222..5133dece6 100644 --- a/src/core/fullscreenui_settings.cpp +++ b/src/core/fullscreenui_settings.cpp @@ -194,7 +194,6 @@ static constexpr std::string_view COVER_DOWNLOADER_DIALOG_NAME = "##cover_downlo namespace { struct SettingsLocals { - float settings_last_bg_alpha = 1.0f; SettingsPage settings_page = SettingsPage::Interface; std::unique_ptr game_settings_interface; std::unique_ptr game_settings_entry; @@ -1654,7 +1653,6 @@ void FullscreenUI::SwitchToSettings() SwitchToMainWindow(MainWindowType::Settings); s_settings_locals.settings_page = SettingsPage::Interface; - s_settings_locals.settings_last_bg_alpha = GetBackgroundAlpha(); } bool FullscreenUI::SwitchToGameSettings(SettingsPage page) @@ -1818,12 +1816,8 @@ void FullscreenUI::DrawSettingsWindow() const ImVec2 heading_size = ImVec2( io.DisplaySize.x, UIStyle.LargeFontSize + (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f)); - const float target_bg_alpha = GetBackgroundAlpha(); - s_settings_locals.settings_last_bg_alpha = - (target_bg_alpha < s_settings_locals.settings_last_bg_alpha) ? - std::max(s_settings_locals.settings_last_bg_alpha - io.DeltaTime * 2.0f, target_bg_alpha) : - std::min(s_settings_locals.settings_last_bg_alpha + io.DeltaTime * 2.0f, target_bg_alpha); - + const float bg_alpha = GetBackgroundAlpha(); + const bool blur_background = CanBlurBackground() && s_settings_locals.settings_page != SettingsPage::PostProcessing; const bool show_localized_titles = GameList::ShouldShowLocalizedTitles(); static constexpr const SettingsPage global_pages[] = { @@ -1867,8 +1861,8 @@ void FullscreenUI::DrawSettingsWindow() } if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "settings_category", - ImVec4(UIStyle.PrimaryColor.x, UIStyle.PrimaryColor.y, UIStyle.PrimaryColor.z, - s_settings_locals.settings_last_bg_alpha))) + ModAlpha(UIStyle.PrimaryColor, bg_alpha), + 0.0f, ImVec2(), 0, blur_background)) { BeginNavBar(); @@ -1910,9 +1904,8 @@ void FullscreenUI::DrawSettingsWindow() ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), TinyString::from_format("settings_page_{}", static_cast(s_settings_locals.settings_page)).c_str(), - ImVec4(UIStyle.BackgroundColor.x, UIStyle.BackgroundColor.y, UIStyle.BackgroundColor.z, - s_settings_locals.settings_last_bg_alpha), - 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, 0.0f)); + ModAlpha(UIStyle.BackgroundColor, bg_alpha), 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0, + blur_background); if (ImGui::IsWindowFocused() && WantsToCloseMenu()) ReturnToPreviousWindow(); @@ -1924,8 +1917,7 @@ void FullscreenUI::DrawSettingsWindow() ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), TinyString::from_format("settings_page_{}", static_cast(s_settings_locals.settings_page)).c_str(), - ImVec4(UIStyle.BackgroundColor.x, UIStyle.BackgroundColor.y, UIStyle.BackgroundColor.z, - s_settings_locals.settings_last_bg_alpha)); + ModAlpha(UIStyle.BackgroundColor, bg_alpha), 0.0f, ImVec2(), 0, true); if (SplitWindowIsNavWindow() && WantsToCloseMenu()) ReturnToPreviousWindow(); @@ -2283,6 +2275,15 @@ void FullscreenUI::DrawInterfaceSettingsPage() FSUI_VSTR("Plays sound effects when navigating and activating menus."), "Main", "FullscreenUISoundEffects", true); + if (DrawToggleSetting( + bsi, FSUI_ICONVSTR(ICON_FA_GLASS_WATER, "Blur Backgrounds"), + FSUI_VSTR("Applies a blur effect to the background when a menu is open to improve readability."), "Main", + "FullscreenUIBlurMenuBackground", true)) + { + widgets_settings_changed = true; + BeginTransition({}); + } + // have to queue because we're holding the settings lock, and UpdateWidgetsSettings() reads it if (widgets_settings_changed) { @@ -2355,6 +2356,13 @@ void FullscreenUI::DrawInterfaceSettingsPage() FSUI_VSTR("Shows on-screen-display messages when events occur. Errors and warnings are still " "displayed regardless of this setting."), "Display", "ShowOSDMessages", true); + DrawToggleSetting( + bsi, FSUI_ICONVSTR(ICON_FA_GLASS_WATER, "Blur Message Backgrounds"), + FSUI_VSTR("Applies a blur effect to the background behind on-screen messages to improve readability."), "Display", + "BlurOSDMessageBackgrounds", true); + DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_WINDOW_MAXIMIZE, "Animate Messages"), + FSUI_VSTR("Enables animation for on-screen messages when they appear and disappear."), "Display", + "AnimateOSDMessages", true); DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_PLAY, "Show Status Indicators"), FSUI_VSTR("Shows persistent icons when turbo is active or when paused."), "Display", "ShowStatusIndicators", true); diff --git a/src/core/fullscreenui_strings.h b/src/core/fullscreenui_strings.h index 9a4c8cedb..cd84840d3 100644 --- a/src/core/fullscreenui_strings.h +++ b/src/core/fullscreenui_strings.h @@ -115,8 +115,11 @@ TRANSLATE_NOOP("FullscreenUI", "Alpha Blending"); TRANSLATE_NOOP("FullscreenUI", "Always Track Uploads"); TRANSLATE_NOOP("FullscreenUI", "An error occurred while deleting empty game settings:\n{}"); TRANSLATE_NOOP("FullscreenUI", "An error occurred while saving game settings:\n{}"); +TRANSLATE_NOOP("FullscreenUI", "Animate Messages"); TRANSLATE_NOOP("FullscreenUI", "Animates windows opening/closing and changes between views in the Big Picture UI."); TRANSLATE_NOOP("FullscreenUI", "Appearance"); +TRANSLATE_NOOP("FullscreenUI", "Applies a blur effect to the background behind on-screen messages to improve readability."); +TRANSLATE_NOOP("FullscreenUI", "Applies a blur effect to the background when a menu is open to improve readability."); TRANSLATE_NOOP("FullscreenUI", "Apply Image Patches"); TRANSLATE_NOOP("FullscreenUI", "Are you sure you want to clear all mappings for this controller?\n\nYou cannot undo this action."); TRANSLATE_NOOP("FullscreenUI", "Are you sure you want to clear the current post-processing chain? All configuration will be lost."); @@ -152,6 +155,8 @@ TRANSLATE_NOOP("FullscreenUI", "Back To Pause Menu"); TRANSLATE_NOOP("FullscreenUI", "Backend Settings"); TRANSLATE_NOOP("FullscreenUI", "Behavior"); TRANSLATE_NOOP("FullscreenUI", "Bindings"); +TRANSLATE_NOOP("FullscreenUI", "Blur Backgrounds"); +TRANSLATE_NOOP("FullscreenUI", "Blur Message Backgrounds"); TRANSLATE_NOOP("FullscreenUI", "Border Overlay"); TRANSLATE_NOOP("FullscreenUI", "Borderless Fullscreen"); TRANSLATE_NOOP("FullscreenUI", "Bottom: "); @@ -331,6 +336,7 @@ TRANSLATE_NOOP("FullscreenUI", "Enable/Disable the Player LED on DualSense contr TRANSLATE_NOOP("FullscreenUI", "Enables alignment and bus exceptions. Not needed for any known games."); TRANSLATE_NOOP("FullscreenUI", "Enables an additional 6MB of RAM to obtain a total of 2+6 = 8MB, usually present on dev consoles."); TRANSLATE_NOOP("FullscreenUI", "Enables an additional three controller slots on each port. Not supported in all games."); +TRANSLATE_NOOP("FullscreenUI", "Enables animation for on-screen messages when they appear and disappear."); TRANSLATE_NOOP("FullscreenUI", "Enables caching of guest textures, required for texture replacement."); TRANSLATE_NOOP("FullscreenUI", "Enables depth testing for semi-transparent polygons. Usually these include shadows, and tend to clip through the ground when depth testing is enabled."); TRANSLATE_NOOP("FullscreenUI", "Enables dumping of textures to image files, which can be replaced. Not compatible with all games."); diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index 066300e2d..3fe950f9b 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -21,6 +21,7 @@ #include "common/assert.h" #include "common/error.h" #include "common/file_system.h" +#include "common/gsvector_formatter.h" #include "common/log.h" #include "common/lru_cache.h" #include "common/path.h" @@ -59,6 +60,10 @@ static constexpr int MENU_BUTTON_SPLIT_LAYER_HIGHLIGHT = 1; static constexpr int MENU_BUTTON_SPLIT_LAYER_FOREGROUND = 2; static constexpr int NUM_MENU_BUTTON_SPLIT_LAYERS = 3; +// Render to a 720p-sized texture for consistent blur across all resolutions. +static constexpr u32 BLUR_TARGET_WIDTH = 1280; +static constexpr u32 BLUR_TARGET_HEIGHT = 720; + enum class SplitWindowFocusChange : u8 { None, @@ -69,7 +74,9 @@ enum class SplitWindowFocusChange : u8 static std::optional LoadTextureImage(std::string_view path, u32 svg_width, u32 svg_height); static std::shared_ptr UploadTexture(std::string_view path, const Image& image); -static bool CompileTransitionPipelines(Error* error); +static bool CompilePipelines(Error* error); + +static void DrawWithBlurTexture(const ImDrawList* parent_list, const ImDrawCmd* cmd, u32 base_vertex, u32 base_index); static void CreateFooterTextString(SmallStringBase& dest, std::span> items); @@ -338,6 +345,8 @@ struct WidgetsState FocusResetType focus_reset_queued = FocusResetType::None; TransitionState transition_state = TransitionState::Inactive; s8 has_pending_nav_move = static_cast(ImGuiDir_None); + bool blur_active = false; + bool blur_valid = false; ImVec2 horizontal_menu_button_size = {}; @@ -353,6 +362,16 @@ struct WidgetsState float transition_total_time = 0.0f; float transition_remaining_time = 0.0f; + // Blur resources + GSVector2 blur_texture_scale = GSVector2::cxpr(0); + GSVector4i blur_rect = GSVector4i::cxpr(0); + std::unique_ptr blur_source_texture; + std::unique_ptr blur_intermediate_texture; + std::unique_ptr blur_output_texture; + std::unique_ptr blur_render_pipeline; + std::unique_ptr blur_apply_pipeline; + std::unique_ptr present_copy_pipeline; + SmallString fullscreen_footer_text; SmallString last_fullscreen_footer_text; SmallString left_fullscreen_footer_text; @@ -369,8 +388,8 @@ struct WidgetsState bool has_hovered_menu_item = false; bool rendered_menu_item_border = false; bool had_focus_reset = false; - bool sound_effects_enabled = false; bool had_sound_effect = false; + bool fullscreen_footer_blur_allowed = false; ImAnimatedVec2 menu_button_frame_min_animated; ImAnimatedVec2 menu_button_frame_max_animated; @@ -414,13 +433,23 @@ ALIGN_TO_CACHE_LINE static WidgetsState s_state; } // namespace FullscreenUI +#if defined(_DEBUG) || defined(_DEVEL) + FullscreenUI::WidgetsState::~WidgetsState() { DebugAssert(!transition_prev_texture); DebugAssert(!transition_current_texture); DebugAssert(!transition_blend_pipeline); + DebugAssert(!blur_source_texture); + DebugAssert(!blur_intermediate_texture); + DebugAssert(!blur_output_texture); + DebugAssert(!blur_render_pipeline); + DebugAssert(!blur_apply_pipeline); + DebugAssert(!present_copy_pipeline); } +#endif + void FullscreenUI::SetFont(ImFont* ui_font) { UIStyle.Font = ui_font; @@ -476,7 +505,8 @@ void FullscreenUI::UpdateWidgetsSettings() UIStyle.Animations = Core::GetBaseBoolSettingValue("Main", "FullscreenUIAnimations", true); UIStyle.SmoothScrolling = Core::GetBaseBoolSettingValue("Main", "FullscreenUISmoothScrolling", true); UIStyle.MenuBorders = Core::GetBaseBoolSettingValue("Main", "FullscreenUIMenuBorders", false); - s_state.sound_effects_enabled = Core::GetBaseBoolSettingValue("Main", "FullscreenUISoundEffects", true); + UIStyle.BlurMenuBackground = Core::GetBaseBoolSettingValue("Main", "FullscreenUIBlurMenuBackground", true); + UIStyle.SoundEffects = Core::GetBaseBoolSettingValue("Main", "FullscreenUISoundEffects", true); const bool display_ps_icons = Core::GetBaseBoolSettingValue("Main", "FullscreenUIDisplayPSIcons", false); const bool swap_face_buttons = Core::GetBaseBoolSettingValue("Main", "FullscreenUISwapGamepadFaceButtons", false); @@ -503,7 +533,7 @@ bool FullscreenUI::CreateWidgetsGPUResources(Error* error) return false; } - if (!CompileTransitionPipelines(error)) + if (!CompilePipelines(error)) return false; return true; @@ -511,6 +541,15 @@ bool FullscreenUI::CreateWidgetsGPUResources(Error* error) void FullscreenUI::DestroyWidgetsGPUResources() { + g_gpu_device->RecycleTexture(std::move(s_state.blur_source_texture)); + g_gpu_device->RecycleTexture(std::move(s_state.blur_intermediate_texture)); + g_gpu_device->RecycleTexture(std::move(s_state.blur_output_texture)); + s_state.blur_render_pipeline.reset(); + s_state.blur_apply_pipeline.reset(); + s_state.present_copy_pipeline.reset(); + s_state.blur_active = false; + s_state.blur_valid = false; + s_state.transition_blend_pipeline.reset(); g_gpu_device->RecycleTexture(std::move(s_state.transition_prev_texture)); g_gpu_device->RecycleTexture(std::move(s_state.transition_current_texture)); @@ -520,6 +559,11 @@ void FullscreenUI::DestroyWidgetsGPUResources() s_state.texture_cache.Clear(); } +GPUPipeline* FullscreenUI::GetPresentCopyPipeline() +{ + return s_state.present_copy_pipeline.get(); +} + const std::shared_ptr& FullscreenUI::GetPlaceholderTexture() { return s_state.placeholder_texture; @@ -823,7 +867,7 @@ GPUTexture* FullscreenUI::GetTransitionRenderTexture(GPUSwapChain* swap_chain) return s_state.transition_current_texture.get(); } -bool FullscreenUI::CompileTransitionPipelines(Error* error) +bool FullscreenUI::CompilePipelines(Error* error) { const RenderAPI render_api = g_gpu_device->GetRenderAPI(); const ShaderGen shadergen(render_api, ShaderGen::GetShaderLanguageForAPI(render_api), false, false); @@ -860,6 +904,60 @@ bool FullscreenUI::CompileTransitionPipelines(Error* error) return false; } + fs = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), + shadergen.GenerateCopyFragmentShader(false), error); + if (!fs) + return false; + GL_OBJECT_NAME(fs, "Copy Fragment Shader"); + + plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; + plconfig.fragment_shader = fs.get(); + if (!(s_state.present_copy_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) + return false; + GL_OBJECT_NAME(s_state.present_copy_pipeline, "Present Copy Pipeline"); + + fs = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), + shadergen.GenerateGaussianBlurFragmentShader(), error); + if (!fs) + return false; + GL_OBJECT_NAME(fs, "Blur Fragment Shader"); + plconfig.fragment_shader = fs.get(); + if (!(s_state.blur_render_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) + return false; + GL_OBJECT_NAME(s_state.blur_render_pipeline, "Blur Render Pipeline"); + + vs = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), + shadergen.GenerateImGuiBlurVertexShader(), error); + if (!vs) + return false; + GL_OBJECT_NAME(vs, "Blur Apply Vertex Shader"); + + fs = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), + shadergen.GenerateImGuiBlurFragmentShader(), error); + if (!fs) + return false; + GL_OBJECT_NAME(fs, "Blur Apply Fragment Shader"); + + // Don't need texture coordinates, only pos+color. + static constexpr GPUPipeline::VertexAttribute imgui_attributes[] = { + GPUPipeline::VertexAttribute::Make(0, GPUPipeline::VertexAttribute::Semantic::Position, 0, + GPUPipeline::VertexAttribute::Type::Float, 2, OFFSETOF(ImDrawVert, pos)), + GPUPipeline::VertexAttribute::Make(1, GPUPipeline::VertexAttribute::Semantic::Color, 0, + GPUPipeline::VertexAttribute::Type::UNorm8, 4, OFFSETOF(ImDrawVert, col)), + }; + + plconfig.layout = GPUPipeline::Layout::MultiTextureAndUBOAndPushConstants; // SingleTextureAndUBOAndPushConstants + plconfig.input_layout.vertex_attributes = imgui_attributes; + plconfig.input_layout.vertex_stride = sizeof(ImDrawVert); + plconfig.blend = GPUPipeline::BlendState::GetAlphaBlendingState(); + plconfig.blend.write_mask = 0x7; + plconfig.vertex_shader = vs.get(); + plconfig.fragment_shader = fs.get(); + s_state.blur_apply_pipeline = g_gpu_device->CreatePipeline(plconfig, error); + if (!s_state.blur_apply_pipeline) + return false; + + GL_OBJECT_NAME(s_state.blur_apply_pipeline, "Blur Apply Pipeline"); return true; } @@ -923,6 +1021,237 @@ void FullscreenUI::UpdateTransitionState() } } +bool FullscreenUI::CanBlurBackground() +{ + // If there's no video presenter, we have no way to get the current backbuffer for blurring, so don't even try. + return VideoPresenter::HasDisplayTexture(); +} + +void FullscreenUI::InvalidateBlurBackground() +{ + s_state.blur_valid = false; +} + +GPUTexture* FullscreenUI::GetBlurRenderTexture() +{ + if (!s_state.blur_active) + return nullptr; + + // clear for next frame + s_state.blur_active = false; + + const GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); + if (!swap_chain) + return nullptr; + + const u32 swap_chain_width = swap_chain->GetPostRotatedWidth(); + const u32 swap_chain_height = swap_chain->GetPostRotatedHeight(); + u32 blur_width, blur_height; + if ((static_cast(swap_chain_width) / static_cast(swap_chain_height)) > + (static_cast(BLUR_TARGET_WIDTH) / static_cast(BLUR_TARGET_HEIGHT))) + { + blur_width = BLUR_TARGET_WIDTH; + blur_height = static_cast(BLUR_TARGET_WIDTH * swap_chain_height / swap_chain_width); + } + else + { + blur_height = BLUR_TARGET_HEIGHT; + blur_width = static_cast(BLUR_TARGET_HEIGHT * swap_chain_width / swap_chain_height); + } + + if (!s_state.blur_source_texture || s_state.blur_source_texture->GetWidth() != swap_chain_width || + s_state.blur_source_texture->GetHeight() != swap_chain_height) + { + if (!g_gpu_device->ResizeTexture(&s_state.blur_source_texture, swap_chain_width, swap_chain_height, + GPUTexture::Type::RenderTarget, swap_chain->GetFormat(), GPUTexture::Flags::None, + false) || + !g_gpu_device->ResizeTexture(&s_state.blur_intermediate_texture, blur_width, blur_height, + GPUTexture::Type::RenderTarget, swap_chain->GetFormat(), GPUTexture::Flags::None, + false) || + !g_gpu_device->ResizeTexture(&s_state.blur_output_texture, blur_width, blur_height, + GPUTexture::Type::RenderTarget, swap_chain->GetFormat(), GPUTexture::Flags::None, + false)) + { + ERROR_LOG("Failed to allocate {}x{}/{}x{} blur source texture.", swap_chain_width, swap_chain_height, blur_width, + blur_height); + g_gpu_device->RecycleTexture(std::move(s_state.blur_source_texture)); + g_gpu_device->RecycleTexture(std::move(s_state.blur_intermediate_texture)); + g_gpu_device->RecycleTexture(std::move(s_state.blur_output_texture)); + return nullptr; + } + + s_state.blur_texture_scale = + GSVector2(s_state.blur_output_texture->GetSizeVec()) / GSVector2(swap_chain->GetSizeVec()); + s_state.blur_valid = false; + } + + // skip rendering blur if it's unchanged + GL_INS_FMT("Blur active, needs render: {}, scale: {}", !s_state.blur_valid, s_state.blur_texture_scale); + return s_state.blur_valid ? nullptr : s_state.blur_source_texture.get(); +} + +void FullscreenUI::RenderBlur(GPUTexture* const blur_render_texture) +{ + DebugAssert(s_state.blur_source_texture && s_state.blur_source_texture.get() == blur_render_texture); + DebugAssert(s_state.blur_intermediate_texture); + DebugAssert(s_state.blur_output_texture); + DebugAssert(s_state.blur_render_pipeline); + + // We only blur the area of the screen where we need it for a background to save GPU cycles. + // But because the blur is over a wide range of pixels, we need to expand the are on the first pass. + static constexpr s32 BLUR_RADIUS = 30 + 1; + + // Scale the active area to the downsampled texture. + const GSVector2i source_size_vec = s_state.blur_source_texture->GetSizeVec(); + const GSVector2i size_vec = s_state.blur_output_texture->GetSizeVec(); + const GSVector4i viewport = GSVector4i::loadh(size_vec); + const GSVector4 scaled_blur_rectf = GSVector4(s_state.blur_rect) * GSVector4::xyxy(s_state.blur_texture_scale); + const GSVector4i scaled_blur_rect = GSVector4i(scaled_blur_rectf.floor().blend32<12>(scaled_blur_rectf.ceil())); + const GSVector2 texel_size = GSVector2::cxpr(1.0f) / GSVector2(size_vec); + + GL_SCOPE_FMT("RenderDisplayBlur() Rect={}, ScaledRect={}", s_state.blur_rect, scaled_blur_rect); + blur_render_texture->MakeReadyForSampling(); + g_gpu_device->SetViewport(viewport); + + GPUTexture* source_texture = s_state.blur_source_texture.get(); + if (!source_size_vec.eq(size_vec)) + { + // Downsample is pulling more pixels than the first blur. + const GSVector4i draw_rect = + scaled_blur_rect.add32(GSVector4i::cxpr(-BLUR_RADIUS * 2, -BLUR_RADIUS * 2, BLUR_RADIUS * 2, BLUR_RADIUS * 2)) + .rintersect(viewport); + + GSVector4 uv_rect = GSVector4(draw_rect) * GSVector4::xyxy(texel_size); + if (g_gpu_device->UsesLowerLeftOrigin()) + uv_rect = uv_rect.blend32<10>(GSVector4::cxpr(1.0f) - uv_rect); + + GL_SCOPE_FMT("Downsample Framebuffer: Rect={}", draw_rect); + g_gpu_device->InvalidateRenderTarget(s_state.blur_output_texture.get()); + g_gpu_device->SetRenderTarget(s_state.blur_output_texture.get()); + g_gpu_device->SetPipeline(s_state.present_copy_pipeline.get()); + g_gpu_device->SetTextureSampler(0, blur_render_texture, g_gpu_device->GetLinearSampler()); + VideoPresenter::DrawScreenQuad(draw_rect, uv_rect, size_vec, size_vec, DisplayRotation::Normal, + WindowInfoPrerotation::Identity, nullptr, 0); + s_state.blur_output_texture->MakeReadyForSampling(); + source_texture = s_state.blur_output_texture.get(); + } + + g_gpu_device->SetPipeline(s_state.blur_render_pipeline.get()); + + // We run the blur at a reduced number of taps but multiple passes for a stronger effect. + // Two full passes (H+V, H+V) gives a visually pleasing result. + { + // First pass needs to sample a wider range, since we'll be pulling from this for the vertical pass. + const GSVector4i draw_rect = + scaled_blur_rect.add32(GSVector4i::cxpr(-BLUR_RADIUS, -BLUR_RADIUS, BLUR_RADIUS, BLUR_RADIUS)) + .rintersect(viewport); + GSVector4 uv_rect = GSVector4(draw_rect) * GSVector4::xyxy(texel_size); + if (g_gpu_device->UsesLowerLeftOrigin()) + uv_rect = uv_rect.blend32<10>(GSVector4::cxpr(1.0f) - uv_rect); + + GL_SCOPE_FMT("Horizontal Blur: Rect={}", draw_rect); + s_state.blur_source_texture->MakeReadyForSampling(); + g_gpu_device->InvalidateRenderTarget(s_state.blur_intermediate_texture.get()); + g_gpu_device->SetRenderTarget(s_state.blur_intermediate_texture.get()); + g_gpu_device->SetTextureSampler(0, source_texture, g_gpu_device->GetLinearSampler()); + + const GSVector2 uniforms = texel_size.insert32<1, 1>(GSVector2::zero()); + VideoPresenter::DrawScreenQuad(draw_rect, uv_rect, size_vec, size_vec, DisplayRotation::Normal, + WindowInfoPrerotation::Identity, &uniforms, sizeof(uniforms)); + } + + { + // Second pass can be clamped to the actual sampled area. Add one for bilinear filtering. + const GSVector4i draw_rect = scaled_blur_rect.add32(GSVector4i::cxpr(-1, -1, 1, 1)).rintersect(viewport); + GSVector4 uv_rect = GSVector4(draw_rect) * GSVector4::xyxy(texel_size); + if (g_gpu_device->UsesLowerLeftOrigin()) + uv_rect = uv_rect.blend32<10>(GSVector4::cxpr(1.0f) - uv_rect); + + GL_SCOPE_FMT("Vertical Blur: Rect={}", draw_rect); + s_state.blur_intermediate_texture->MakeReadyForSampling(); + g_gpu_device->InvalidateRenderTarget(s_state.blur_output_texture.get()); + g_gpu_device->SetRenderTarget(s_state.blur_output_texture.get()); + g_gpu_device->SetTextureSampler(0, s_state.blur_intermediate_texture.get(), g_gpu_device->GetLinearSampler()); + + const GSVector2 uniforms = texel_size.insert32<0, 0>(GSVector2::zero()); + VideoPresenter::DrawScreenQuad(draw_rect, uv_rect, size_vec, size_vec, DisplayRotation::Normal, + WindowInfoPrerotation::Identity, &uniforms, sizeof(uniforms)); + } + + s_state.blur_output_texture->MakeReadyForSampling(); + s_state.blur_valid = true; +} + +bool FullscreenUI::BeginBlurBackground(ImDrawList* const dl, const ImVec2& bb_min, const ImVec2& bb_max) +{ + if (!CanBlurBackground()) + return false; + + const GSVector4i bounds = + GSVector4i(GSVector4::xyxy(GSVector2::load(&bb_min.x).floor(), GSVector2::load(&bb_max.x).ceil())); + + if (s_state.blur_valid) + { + // If the rectangle gets larger, invalidate it. + const GSVector4i prev_rect = s_state.blur_rect; + s_state.blur_rect = s_state.blur_rect.runion(bounds); + s_state.blur_valid &= s_state.blur_rect.eq(prev_rect); + } + + s_state.blur_rect = (s_state.blur_active || s_state.blur_valid) ? s_state.blur_rect.runion(bounds) : bounds; + s_state.blur_active = true; + + // should flush the current draw, if any + ImDrawCmd* curr_cmd = &dl->CmdBuffer.Data[dl->CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount > 0) + { + dl->AddDrawCmd(); + curr_cmd = &dl->CmdBuffer.Data[dl->CmdBuffer.Size - 1]; + } + + // mark it so we can check in EndPopBackgroundTexture + DebugAssert(curr_cmd->ElemCount == 0); + curr_cmd->UserCallback = &DrawWithBlurTexture; + return true; +} + +void FullscreenUI::EndBlurBackground(ImDrawList* const dl) +{ + ImDrawCmd* const curr_cmd = &dl->CmdBuffer.Data[dl->CmdBuffer.Size - 1]; + DebugAssert(curr_cmd->UserCallback == &DrawWithBlurTexture); + + // allow imgui to clear empty draw commands + if (curr_cmd->ElemCount == 0) + { + GL_INS("No vertices drawn with blur background, skipping."); + curr_cmd->UserCallback = nullptr; + } + else + { + // flush the blurred draw + dl->AddDrawCmd(); + } +} + +void FullscreenUI::DrawWithBlurTexture(const ImDrawList* parent_list, const ImDrawCmd* cmd, u32 base_vertex, + u32 base_index) +{ + struct Uniforms + { + float blur_texture_scale[2]; + float blur_background_weight; + float inv_blur_background_weight; + } uniforms; + uniforms.blur_background_weight = FullscreenUI::UIStyle.BlurBackgroundWeight; + uniforms.inv_blur_background_weight = 1.0f - FullscreenUI::UIStyle.BlurBackgroundWeight; + GSVector2::store(uniforms.blur_texture_scale, s_state.blur_texture_scale); + + g_gpu_device->SetPipeline(s_state.blur_apply_pipeline.get()); + g_gpu_device->SetTextureSampler(0, s_state.blur_output_texture.get(), g_gpu_device->GetLinearSampler()); + g_gpu_device->DrawIndexedWithPushConstants(cmd->ElemCount, base_index + cmd->IdxOffset, base_vertex + cmd->VtxOffset, + &uniforms, sizeof(uniforms)); +} + bool FullscreenUI::UpdateLayoutScale() { #ifndef __ANDROID__ @@ -1067,6 +1396,7 @@ void FullscreenUI::BeginLayout() // we evict from the texture cache at the start of the frame, in case we go over mid-frame, // we need to keep all those textures alive until the end of the frame s_state.texture_cache.ManualEvict(); + s_state.fullscreen_footer_blur_allowed = true; PushResetLayout(); } @@ -1113,7 +1443,7 @@ void FullscreenUI::EndLayout() void FullscreenUI::EnqueueSoundEffect(std::string_view sound_effect) { - if (s_state.had_sound_effect || !s_state.sound_effects_enabled) + if (s_state.had_sound_effect || !UIStyle.SoundEffects) return; SoundEffectManager::EnqueueSoundEffect(sound_effect); @@ -1499,7 +1829,7 @@ bool FullscreenUI::BeginFullscreenWindow(float left, float top, float width, flo bool FullscreenUI::BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const char* name, const ImVec4& background /* = HEX_TO_IMVEC4(0x212121, 0xFF) */, float rounding /*= 0.0f*/, const ImVec2& padding /*= 0.0f*/, - ImGuiWindowFlags flags /*= 0*/) + ImGuiWindowFlags flags /*= 0*/, bool blur /*= false*/) { ImGui::SetNextWindowPos(position); ImGui::SetNextWindowSize(size); @@ -1509,10 +1839,33 @@ bool FullscreenUI::BeginFullscreenWindow(const ImVec2& position, const ImVec2& s ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - return ImGui::Begin(name, nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ((background.w == 0.0f) ? ImGuiWindowFlags_NoBackground : 0) | flags); + const bool actually_blur = (blur && UIStyle.BlurMenuBackground && CanBlurBackground()); + const bool has_background = (background.w != 0.0f); + const bool res = ImGui::Begin(name, nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ((!has_background || actually_blur) ? ImGuiWindowFlags_NoBackground : 0) | flags); + + if (res && actually_blur) + { + ImDrawList* const dl = ImGui::GetWindowDrawList(); + const ImVec2 bg_min = position; + const ImVec2 bg_max = position + size; + if (BeginBlurBackground(dl, bg_min, bg_max)) + { + if (has_background) + dl->AddRectFilled(bg_min, bg_max, + ImGui::GetColorU32(ImVec4(background.x * background.w, background.y * background.w, + background.z * background.w, 1.0f))); + EndBlurBackground(dl); + } + else if (has_background) + { + dl->AddRectFilled(bg_min, bg_max, ImGui::GetColorU32(background)); + } + } + + return res; } void FullscreenUI::EndFullscreenWindow(bool allow_wrap_x, bool allow_wrap_y) @@ -1603,6 +1956,11 @@ void FullscreenUI::SetStandardSelectionFooterText(bool back_instead_of_cancel) } } +void FullscreenUI::SetFullscreenFooterBlur(bool allowed) +{ + s_state.fullscreen_footer_blur_allowed = allowed; +} + void FullscreenUI::DrawFullscreenFooter() { const ImGuiIO& io = ImGui::GetIO(); @@ -1621,9 +1979,20 @@ void FullscreenUI::DrawFullscreenFooter() const u32 text_color = ImGui::GetColorU32(UIStyle.PrimaryTextColor); const float bg_alpha = GetBackgroundAlpha(); - ImDrawList* dl = ImGui::GetForegroundDrawList(); - dl->AddRectFilled(ImVec2(0.0f, io.DisplaySize.y - height), io.DisplaySize, - ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryColor, bg_alpha)), 0.0f); + ImDrawList* const dl = ImGui::GetForegroundDrawList(); + const ImVec2 bb_min = ImVec2(0.0f, io.DisplaySize.y - height); + const ImVec2& bb_max = io.DisplaySize; + if (UIStyle.BlurMenuBackground && s_state.fullscreen_footer_blur_allowed && BeginBlurBackground(dl, bb_min, bb_max)) + { + dl->AddRectFilled(ImVec2(0.0f, io.DisplaySize.y - height), io.DisplaySize, + ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryColor, 1.0f)), 0.0f); + EndBlurBackground(dl); + } + else + { + dl->AddRectFilled(ImVec2(0.0f, io.DisplaySize.y - height), io.DisplaySize, + ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryColor, bg_alpha)), 0.0f); + } ImFont* const font = UIStyle.Font; const float font_size = UIStyle.MediumFontSize; @@ -5152,6 +5521,17 @@ void FullscreenUI::DrawLoadingScreen(std::string_view image, std::string_view ti const ImVec2 image_pos = ImVec2(ImCeil((io.DisplaySize.x - image_width) * 0.5f), ImCeil(((io.DisplaySize.y - total_height) * 0.5f))); ImDrawList* const dl = ImGui::GetBackgroundDrawList(); + + if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, ImVec2(), io.DisplaySize)) + { + dl->AddRectFilled(ImVec2(), io.DisplaySize, ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 0.9f))); + EndBlurBackground(dl); + } + else if (VideoPresenter::HasDisplayTexture()) + { + dl->AddRectFilled(ImVec2(), io.DisplaySize, ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 0.9f))); + } + GPUTexture* tex = GetCachedTexture(image); if (tex) { @@ -5502,7 +5882,7 @@ std::pair FullscreenUI::NotificationLayout::GetNextPosition(float ImVec2 pos; if (m_location == NotificationLocation::TopLeft || m_location == NotificationLocation::BottomLeft) { - if (anim_coeff != 1.0f) + if (anim_coeff != 1.0f && g_gpu_settings.display_animate_messages) { if (active) { @@ -5526,7 +5906,7 @@ std::pair FullscreenUI::NotificationLayout::GetNextPosition(float else { pos.x = m_current_position.x - width; - if (anim_coeff != 1.0f) + if (anim_coeff != 1.0f && g_gpu_settings.display_animate_messages) { if (active) { @@ -5568,7 +5948,7 @@ std::pair FullscreenUI::NotificationLayout::GetNextPosition(float pos.x = ImFloor((ImGui::GetIO().DisplaySize.x - width) * 0.5f); pos.y = m_current_position.y; - if (anim_coeff != 1.0f) + if (anim_coeff != 1.0f && g_gpu_settings.display_animate_messages) { if (active) { @@ -5602,7 +5982,7 @@ std::pair FullscreenUI::NotificationLayout::GetNextPosition(float pos.x = ImFloor((ImGui::GetIO().DisplaySize.x - width) * 0.5f); pos.y = m_current_position.y - height; - if (anim_coeff != 1.0f) + if (anim_coeff != 1.0f && g_gpu_settings.display_animate_messages) { if (active) { @@ -5786,6 +6166,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0x282828, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(0, 0, 0, 100); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = true; } else if (theme == "CobaltSky") @@ -5810,6 +6191,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0x2d4183, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(0, 0, 0, 100); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = true; } else if (theme == "GreyMatter") @@ -5834,6 +6216,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0x282828, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(0, 0, 0, 100); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = true; } else if (theme == "PinkyPals") @@ -5858,6 +6241,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0xd86a66, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(100, 100, 100, 50); + UIStyle.BlurBackgroundWeight = 0.5f; UIStyle.IsDarkTheme = false; } else if (theme == "GreenGiant") @@ -5881,6 +6265,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0xD5DE2E, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0x000000, 0xff); UIStyle.ShadowColor = IM_COL32(100, 100, 100, 50); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = false; } else if (theme == "DarkRuby") @@ -5905,6 +6290,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0x282828, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(0, 0, 0, 100); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = true; } else if (theme == "PurpleRain") @@ -5929,6 +6315,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0x8e65cb, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(100, 100, 100, 50); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = true; } else if (theme == "Light") @@ -5954,6 +6341,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0xf1f1f1, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0x000000, 0xff); UIStyle.ShadowColor = IM_COL32(100, 100, 100, 50); + UIStyle.BlurBackgroundWeight = 0.5f; UIStyle.IsDarkTheme = false; } else @@ -5979,6 +6367,7 @@ void FullscreenUI::UpdateTheme() UIStyle.ToastBackgroundColor = HEX_TO_IMVEC4(0x282828, 0xff); UIStyle.ToastTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); UIStyle.ShadowColor = IM_COL32(0, 0, 0, 100); + UIStyle.BlurBackgroundWeight = 0.25f; UIStyle.IsDarkTheme = true; } } diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index fdfeb01ba..006e3ed3b 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -28,9 +28,12 @@ class Error; class Image; +class ProgressCallbackWithPrompt; + +enum class GPUPresentResult : u8; +class GPUPipeline; class GPUTexture; class GPUSwapChain; -class ProgressCallbackWithPrompt; enum class OSDMessageType : u8; @@ -108,14 +111,17 @@ struct ALIGN_TO_CACHE_LINE UIStyles float MediumFontSize; float MediumLargeFontSize; float MediumSmallFontSize; + float BlurBackgroundWeight; static constexpr float NormalFontWeight = 0.0f; static constexpr float BoldFontWeight = 500.0f; - bool Animations; - bool SmoothScrolling; - bool MenuBorders; - bool IsDarkTheme; + bool Animations : 1; + bool SmoothScrolling : 1; + bool MenuBorders : 1; + bool BlurMenuBackground : 1; + bool SoundEffects : 1; + bool IsDarkTheme : 1; }; extern UIStyles UIStyle; @@ -229,6 +235,7 @@ void UpdateWidgetsSettings(); bool CreateWidgetsGPUResources(Error* error); void DestroyWidgetsGPUResources(); +GPUPipeline* GetPresentCopyPipeline(); std::span GetThemeNames(); std::span GetThemeDisplayNames(); @@ -273,6 +280,14 @@ GPUTexture* GetTransitionRenderTexture(GPUSwapChain* swap_chain); void RenderTransitionBlend(GPUSwapChain* swap_chain); void UpdateTransitionState(); +/// Screen blurring. +bool CanBlurBackground(); +void InvalidateBlurBackground(); +GPUTexture* GetBlurRenderTexture(); +void RenderBlur(GPUTexture* const blur_render_texture); +bool BeginBlurBackground(ImDrawList* const dl, const ImVec2& bb_min, const ImVec2& bb_max); +void EndBlurBackground(ImDrawList* const dl); + /// Layout helpers. void BeginLayout(); void EndLayout(); @@ -341,7 +356,7 @@ bool BeginFullscreenWindow(float left, float top, float width, float height, con const ImVec2& padding = ImVec2(), ImGuiWindowFlags flags = 0); bool BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const char* name, const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF), float rounding = 0.0f, - const ImVec2& padding = ImVec2(), ImGuiWindowFlags flags = 0); + const ImVec2& padding = ImVec2(), ImGuiWindowFlags flags = 0, bool blur = false); void EndFullscreenWindow(bool allow_wrap_x = false, bool allow_wrap_y = true); void SetWindowNavWrapping(bool allow_wrap_x = false, bool allow_wrap_y = true); @@ -350,6 +365,7 @@ std::string_view GetControllerIconMapping(std::string_view icon); void SetFullscreenFooterText(std::string_view text); void SetFullscreenFooterText(std::span> items); void SetStandardSelectionFooterText(bool back_instead_of_cancel); +void SetFullscreenFooterBlur(bool allowed); void SetFullscreenStatusText(std::string_view text); void SetFullscreenStatusText(std::span> items); void DrawFullscreenFooter(); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index af8c6e74e..9562773d9 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -374,6 +374,8 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro display_line_start_offset = static_cast(si.GetIntValue("Display", "LineStartOffset", 0)); display_line_end_offset = static_cast(si.GetIntValue("Display", "LineEndOffset", 0)); display_show_messages = si.GetBoolValue("Display", "ShowOSDMessages", true); + display_animate_messages = si.GetBoolValue("Display", "AnimateOSDMessages", true); + display_blur_message_backgrounds = si.GetBoolValue("Display", "BlurOSDMessageBackgrounds", true); display_show_fps = si.GetBoolValue("Display", "ShowFPS", false); display_show_speed = si.GetBoolValue("Display", "ShowSpeed", false); display_show_gpu_stats = si.GetBoolValue("Display", "ShowGPUStatistics", false); @@ -732,6 +734,8 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const if (!ignore_base) { si.SetBoolValue("Display", "ShowOSDMessages", display_show_messages); + si.SetBoolValue("Display", "AnimateOSDMessages", display_animate_messages); + si.SetBoolValue("Display", "BlurOSDMessageBackgrounds", display_blur_message_backgrounds); si.SetBoolValue("Display", "ShowFPS", display_show_fps); si.SetBoolValue("Display", "ShowSpeed", display_show_speed); si.SetBoolValue("Display", "ShowResolution", display_show_resolution); diff --git a/src/core/settings.h b/src/core/settings.h index c602204a9..b3e803231 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -106,6 +106,8 @@ struct GPUSettings bool display_force_4_3_for_24bit : 1 = false; bool display_24bit_chroma_smoothing : 1 = false; bool display_show_messages : 1 = true; + bool display_animate_messages : 1 = true; + bool display_blur_message_backgrounds : 1 = true; bool display_show_fps : 1 = false; bool display_show_speed : 1 = false; bool display_show_gpu_stats : 1 = false; diff --git a/src/core/system.cpp b/src/core/system.cpp index 9136ac960..b57926441 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -4816,6 +4816,8 @@ void System::CheckForSettingsChanges(const Settings& old_settings) } else if (const bool device_settings_changed = g_settings.AreGPUDeviceSettingsChanged(old_settings); device_settings_changed || g_settings.display_show_messages != old_settings.display_show_messages || + g_settings.display_animate_messages != old_settings.display_animate_messages || + g_settings.display_blur_message_backgrounds != old_settings.display_blur_message_backgrounds || g_settings.display_show_fps != old_settings.display_show_fps || g_settings.display_show_speed != old_settings.display_show_speed || g_settings.display_show_gpu_stats != old_settings.display_show_gpu_stats || diff --git a/src/core/video_presenter.cpp b/src/core/video_presenter.cpp index d712d7f5b..56bc2d547 100644 --- a/src/core/video_presenter.cpp +++ b/src/core/video_presenter.cpp @@ -67,6 +67,8 @@ static GPUTexture* GetDisplayPostProcessInputTexture(const GSVector4i source_rec DisplayRotation rotation); static GPUPresentResult ApplyDisplayPostProcess(GPUTexture* target, GPUTexture* input, const GSVector4i display_rect, const GSVector2i postfx_size); +static GPUPresentResult DrawDisplayCopy(GPUTexture* const source, GPUTexture* const target, + GPUSwapChain* const swap_chain); static bool DeinterlaceSetTargetSize(u32 width, u32 height, bool preserve); static void DestroyDeinterlaceTextures(); @@ -111,8 +113,6 @@ struct Locals bool border_overlay_alpha_blend = false; bool border_overlay_destination_alpha_blend = false; - std::unique_ptr present_copy_pipeline; - std::unique_ptr display_postfx; std::unique_ptr border_overlay_texture; @@ -151,7 +151,6 @@ VideoPresenter::Locals::~Locals() DebugAssert(!display_pipeline); DebugAssert(!display_24bit_pipeline); DebugAssert(!display_texture); - DebugAssert(!present_copy_blend_pipeline); DebugAssert(!display_postfx); DebugAssert(!border_overlay_texture); DebugAssert(!border_overlay_pipeline); @@ -167,14 +166,17 @@ const GSVector2i& VideoPresenter::GetVideoSize() { return s_locals.video_size; } + GPUTexture* VideoPresenter::GetDisplayTexture() { return s_locals.display_texture; } + const GSVector4i& VideoPresenter::GetDisplayTextureRect() { return s_locals.display_texture_rect; } + bool VideoPresenter::HasDisplayTexture() { return s_locals.display_texture; @@ -214,6 +216,8 @@ bool VideoPresenter::Initialize(Error* error) void VideoPresenter::Shutdown() { + FullscreenUI::InvalidateBlurBackground(); + DestroyDeinterlaceTextures(); g_gpu_device->RecycleTexture(std::move(s_locals.chroma_smoothing_texture)); g_gpu_device->RecycleTexture(std::move(s_locals.border_overlay_texture)); @@ -236,8 +240,6 @@ void VideoPresenter::Shutdown() s_locals.border_overlay_alpha_blend = false; s_locals.border_overlay_destination_alpha_blend = false; - s_locals.present_copy_pipeline.reset(); - s_locals.display_postfx.reset(); s_locals.border_overlay_pipeline.reset(); @@ -367,27 +369,21 @@ bool VideoPresenter::CompileDisplayPipelines(bool display, bool deinterlace, boo s_locals.display_24bit_pipeline.reset(); } - std::unique_ptr copy_fso = g_gpu_device->CreateShader( - GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateCopyFragmentShader(false), error); - if (!copy_fso) - return false; - GL_OBJECT_NAME(copy_fso, "Display Copy Fragment Shader"); - - plconfig.fragment_shader = copy_fso.get(); - if (!(s_locals.present_copy_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) - return false; - GL_OBJECT_NAME(s_locals.present_copy_pipeline, "Display Copy Pipeline"); - // blended variants if (s_locals.border_overlay_texture) { - std::unique_ptr clear_fso = g_gpu_device->CreateShader( + const std::unique_ptr clear_fso = g_gpu_device->CreateShader( GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateFillFragmentShader(GSVector4::cxpr(0.0f, 0.0f, 0.0f, 1.0f)), error); if (!clear_fso) return false; GL_OBJECT_NAME(clear_fso, "Display Clear Fragment Shader"); + const std::unique_ptr copy_fso = g_gpu_device->CreateShader( + GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateCopyFragmentShader(false), error); + if (!copy_fso) + return false; + GL_OBJECT_NAME(copy_fso, "Display Copy Fragment Shader"); plconfig.fragment_shader = copy_fso.get(); plconfig.blend = s_locals.border_overlay_alpha_blend ? GPUPipeline::BlendState::GetAlphaBlendingState() : GPUPipeline::BlendState::GetNoBlendingState(); @@ -572,6 +568,7 @@ void VideoPresenter::ClearDisplayTexture() { s_locals.display_texture = nullptr; s_locals.display_texture_rect = GSVector4i::zero(); + FullscreenUI::InvalidateBlurBackground(); } void VideoPresenter::SetDisplayParameters(const GSVector2i& video_size, const GSVector4i& video_active_rect, @@ -581,6 +578,7 @@ void VideoPresenter::SetDisplayParameters(const GSVector2i& video_size, const GS s_locals.video_active_rect = video_active_rect; s_locals.display_pixel_aspect_ratio = display_pixel_aspect_ratio; s_locals.display_texture_24bit = display_24bit; + FullscreenUI::InvalidateBlurBackground(); } void VideoPresenter::SetDisplayTexture(GPUTexture* texture, const GSVector4i& source_rect) @@ -592,6 +590,7 @@ void VideoPresenter::SetDisplayTexture(GPUTexture* texture, const GSVector4i& so s_locals.display_texture = texture; s_locals.display_texture_rect = source_rect; + FullscreenUI::InvalidateBlurBackground(); } GPUPresentResult VideoPresenter::RenderDisplay(GPUTexture* target, const GSVector2i target_size, bool postfx, @@ -777,7 +776,7 @@ GPUPresentResult VideoPresenter::RenderDisplay(GPUTexture* target, const GSVecto { // Otherwise, just copy the framebuffer. GL_SCOPE_FMT("Copy framebuffer for prerotation"); - g_gpu_device->SetPipeline(s_locals.present_copy_pipeline.get()); + g_gpu_device->SetPipeline(FullscreenUI::GetPresentCopyPipeline()); g_gpu_device->SetTextureSampler(0, postfx_output, g_gpu_device->GetNearestSampler()); DrawScreenQuad(GSVector4i::loadh(target_size), src_uv_rect, target_size, final_target_size, present_rotation, prerotation, nullptr, 0); @@ -1104,7 +1103,7 @@ GPUTexture* VideoPresenter::GetDisplayPostProcessInputTexture(const GSVector4i s g_gpu_device->ClearRenderTarget(postfx_input, GPUDevice::DEFAULT_CLEAR_COLOR); g_gpu_device->SetRenderTarget(postfx_input); g_gpu_device->SetViewportAndScissor(GSVector4i::loadh(postfx_input_size)); - g_gpu_device->SetPipeline(s_locals.present_copy_pipeline.get()); + g_gpu_device->SetPipeline(FullscreenUI::GetPresentCopyPipeline()); g_gpu_device->SetTextureSampler(0, s_locals.display_texture, g_gpu_device->GetNearestSampler()); DrawScreenQuad(input_draw_rect, src_uv_rect, postfx_input_size, postfx_input_size, rotation, WindowInfoPrerotation::Identity, nullptr, 0); @@ -1145,6 +1144,35 @@ GPUPresentResult VideoPresenter::ApplyDisplayPostProcess(GPUTexture* target, GPU s_locals.video_size.y); } +GPUPresentResult VideoPresenter::DrawDisplayCopy(GPUTexture* const source, GPUTexture* const target, + GPUSwapChain* const swap_chain) +{ + // write direct to final target + if (target) + { + g_gpu_device->InvalidateRenderTarget(target); + g_gpu_device->SetRenderTarget(target); + } + else + { + if (const GPUPresentResult pres = g_gpu_device->BeginPresent(swap_chain); pres != GPUPresentResult::OK) + return pres; + } + + const WindowInfoPrerotation prerotation = target ? WindowInfoPrerotation::Identity : swap_chain->GetPreRotation(); + const GSVector2i final_target_size = target ? target->GetSizeVec() : swap_chain->GetPostRotatedSizeVec(); + const GSVector2i target_size = target ? target->GetSizeVec() : swap_chain->GetSizeVec(); + const GSVector4i rect = GSVector4i::loadh(target_size); + const GSVector4 uv_rect = g_gpu_device->UsesLowerLeftOrigin() ? GSVector4::cxpr(0.0f, 1.0f, 1.0f, 0.0f) : + GSVector4::cxpr(0.0f, 0.0f, 1.0f, 1.0f); + g_gpu_device->SetViewportAndScissor(rect); + g_gpu_device->SetTextureSampler(0, source, g_gpu_device->GetNearestSampler()); + g_gpu_device->SetPipeline(FullscreenUI::GetPresentCopyPipeline()); + DrawScreenQuad(rect, uv_rect, target_size, final_target_size, DisplayRotation::Normal, prerotation, nullptr, 0); + + return GPUPresentResult::OK; +} + void VideoPresenter::SendDisplayToMediaCapture(MediaCapture* cap) { GPUTexture* target = cap->GetRenderTexture(); @@ -1479,35 +1507,46 @@ bool VideoPresenter::PresentFrame(GPUBackend* backend, u64 present_time) ImGuiManager::CreateDrawLists(); - // render offscreen for transitions - if (FullscreenUI::IsTransitionActive()) - { - GPUTexture* const rtex = FullscreenUI::GetTransitionRenderTexture(g_gpu_device->GetMainSwapChain()); - if (rtex) - { - if (backend) - RenderDisplay(rtex, rtex->GetSizeVec(), true, true); - else - g_gpu_device->ClearRenderTarget(rtex, GPUDevice::DEFAULT_CLEAR_COLOR); - - g_gpu_device->SetRenderTarget(rtex); - ImGuiManager::RenderDrawLists(rtex); - } - } - GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); DebugAssert(swap_chain); - const GPUPresentResult pres = - ((backend && !FullscreenUI::IsTransitionActive()) ? RenderDisplay(nullptr, swap_chain->GetSizeVec(), true, true) : - g_gpu_device->BeginPresent(swap_chain)); - if (pres == GPUPresentResult::OK) + GPUTexture* const blur_target = FullscreenUI::GetBlurRenderTexture(); + if (blur_target) { - if (FullscreenUI::IsTransitionActive()) - FullscreenUI::RenderTransitionBlend(swap_chain); + RenderDisplay(blur_target, blur_target->GetSizeVec(), true, true); + FullscreenUI::RenderBlur(blur_target); + } + + GPUTexture* const transition_target = FullscreenUI::IsTransitionActive() ? + FullscreenUI::GetTransitionRenderTexture(g_gpu_device->GetMainSwapChain()) : + nullptr; + GPUPresentResult pres; + if (transition_target) + { + if (blur_target) + DrawDisplayCopy(blur_target, transition_target, swap_chain); + else if (backend) + RenderDisplay(transition_target, transition_target->GetSizeVec(), true, true); else + g_gpu_device->ClearRenderTarget(transition_target, GPUDevice::DEFAULT_CLEAR_COLOR); + + g_gpu_device->SetRenderTarget(transition_target); + ImGuiManager::RenderDrawLists(transition_target); + + if ((pres = g_gpu_device->BeginPresent(swap_chain)) == GPUPresentResult::OK) + FullscreenUI::RenderTransitionBlend(swap_chain); + } + else + { + if ((pres = blur_target ? DrawDisplayCopy(blur_target, nullptr, swap_chain) : + RenderDisplay(nullptr, swap_chain->GetSizeVec(), true, true)) == GPUPresentResult::OK) + { ImGuiManager::RenderDrawLists(swap_chain); + } + } + if (pres == GPUPresentResult::OK) + { const GPUDevice::Features features = g_gpu_device->GetFeatures(); const bool scheduled_present = (present_time != 0); const bool explicit_present = (scheduled_present && (features.explicit_present && !features.timed_present)); diff --git a/src/duckstation-qt/osdsettingswidget.cpp b/src/duckstation-qt/osdsettingswidget.cpp index 6dd43a7a8..0108b4d2a 100644 --- a/src/duckstation-qt/osdsettingswidget.cpp +++ b/src/duckstation-qt/osdsettingswidget.cpp @@ -30,6 +30,9 @@ OSDSettingsWidget::OSDSettingsWidget(SettingsWindow* dialog, QWidget* parent) : &Settings::GetNotificationLocationName, &Settings::GetNotificationLocationDisplayName, Settings::DEFAULT_OSD_MESSAGE_LOCATION, NotificationLocation::MaxCount); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showMessages, "Display", "ShowOSDMessages", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.animateMessages, "Display", "AnimateOSDMessages", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.blurMessageBackgrounds, "Display", "BlurOSDMessageBackgrounds", + true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showFPS, "Display", "ShowFPS", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showSpeed, "Display", "ShowSpeed", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showResolution, "Display", "ShowResolution", false); @@ -77,6 +80,11 @@ OSDSettingsWidget::OSDSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_ui.showMessages, tr("Show Messages"), tr("Checked"), tr("Shows on-screen-display messages when events occur such as save states being created/loaded, screenshots being " "taken, etc. Errors and warnings are still displayed regardless of this setting.")); + dialog->registerWidgetHelp(m_ui.animateMessages, tr("Animate Messages"), tr("Checked"), + tr("Enables animation for on-screen messages when they appear and disappear.")); + dialog->registerWidgetHelp( + m_ui.blurMessageBackgrounds, tr("Blur Message Backgrounds"), tr("Checked"), + tr("Enables a blur effect on the background behind on-screen messages to improve readability.")); dialog->registerWidgetHelp(m_ui.showResolution, tr("Show Resolution"), tr("Unchecked"), tr("Shows the resolution of the game in the top-right corner of the display.")); dialog->registerWidgetHelp( diff --git a/src/duckstation-qt/osdsettingswidget.ui b/src/duckstation-qt/osdsettingswidget.ui index 36c542177..c609fe004 100644 --- a/src/duckstation-qt/osdsettingswidget.ui +++ b/src/duckstation-qt/osdsettingswidget.ui @@ -102,14 +102,28 @@ - + + + + Animate Messages + + + + + + + Blur Message Backgrounds + + + + Error Duration: - + seconds @@ -128,14 +142,14 @@ - + Warning Duration: - + seconds @@ -154,14 +168,14 @@ - + Information Duration: - + seconds @@ -180,14 +194,14 @@ - + Action Duration: - + seconds @@ -206,14 +220,14 @@ - + Display Location: - + diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index 5562df8f1..f9496f241 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -532,8 +532,7 @@ void ImGuiManager::RenderDrawLists(u32 window_width, u32 window_height, WindowIn if (pcmd->UserCallback) [[unlikely]] { - pcmd->UserCallback(cmd_list, pcmd); - g_gpu_device->UploadUniformBuffer(&mproj, sizeof(mproj)); + pcmd->UserCallback(cmd_list, pcmd, base_vertex, base_index); g_gpu_device->SetPipeline(s_state.imgui_pipeline.get()); } else @@ -1073,6 +1072,8 @@ void ImGuiManager::DrawOSDMessages(Timer::Value current_time) const float max_width_for_color = std::ceil(400.0f * scale); const float min_rounded_width = rounding * 2.0f; const bool show_messages = g_gpu_settings.display_show_messages; + const bool blur_background = g_gpu_settings.display_blur_message_backgrounds && !FullscreenUI::HasActiveWindow() && + FullscreenUI::CanBlurBackground(); const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f); const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f); @@ -1176,14 +1177,22 @@ void ImGuiManager::DrawOSDMessages(Timer::Value current_time) ImDrawList* const dl = ImGui::GetForegroundDrawList(); - const float background_opacity = opacity * 0.95f; - DrawRoundedGradientRect( - dl, pos, pos_max, ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity)), - ImGui::GetColorU32( - ModAlpha(ImLerp(left_background_color, right_background_color, - std::min((box_width - min_rounded_width) / (max_width_for_color - min_rounded_width), 1.0f)), - background_opacity)), - rounding); + if (blur_background && FullscreenUI::BeginBlurBackground(dl, pos, pos_max)) + { + dl->AddRectFilled(pos, pos_max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)), rounding); + FullscreenUI::EndBlurBackground(dl); + } + else + { + const float background_opacity = opacity * 0.95f; + DrawRoundedGradientRect( + dl, pos, pos_max, ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity)), + ImGui::GetColorU32( + ModAlpha(ImLerp(left_background_color, right_background_color, + std::min((box_width - min_rounded_width) / (max_width_for_color - min_rounded_width), 1.0f)), + background_opacity)), + rounding); + } const ImVec2 base_pos = ImVec2(pos.x + padding, pos.y + padding); const ImU32 color = ImGui::GetColorU32(ModAlpha(text_color, opacity)); diff --git a/src/util/shadergen.cpp b/src/util/shadergen.cpp index 60323e718..06d9231dc 100644 --- a/src/util/shadergen.cpp +++ b/src/util/shadergen.cpp @@ -977,6 +977,47 @@ std::string ShaderGen::GenerateImGuiFragmentShader() const ss << R"( { o_col0 = v_col0 * SAMPLE_TEXTURE(samp0, v_tex0); +} + )"; + + return std::move(ss).str(); +} + +std::string ShaderGen::GenerateImGuiBlurVertexShader() const +{ + std::stringstream ss; + WriteHeader(ss); + DeclareUniformBuffer(ss, {"float4x4 ProjectionMatrix"}, false); + DeclareUniformBuffer(ss, {"float2 BlurTextureScale", "float BlurBackgroundWeight", "float InvBlurBackgroundWeight"}, + true); + DeclareVertexEntryPoint(ss, {"float2 a_pos", "float4 a_col0"}, 1, 0, {}, false); + ss << R"( +{ + v_pos = mul(ProjectionMatrix, float4(a_pos, 0.f, 1.f)); + v_col0 = a_col0; + #if API_VULKAN + v_pos.y = -v_pos.y; + #endif +} +)"; + + return std::move(ss).str(); +} + +std::string ShaderGen::GenerateImGuiBlurFragmentShader() const +{ + std::stringstream ss; + WriteHeader(ss); + DeclareUniformBuffer(ss, {"float4x4 ProjectionMatrix"}, false); // needs the descriptor set defined + DeclareUniformBuffer(ss, {"float2 BlurTextureScale", "float BlurBackgroundWeight", "float InvBlurBackgroundWeight"}, + true); + DeclareTexture(ss, "samp0", 0); + DeclareFragmentEntryPoint(ss, 1, 0, {}, true); + + ss << R"( +{ + int2 blur_pos = int2(floor(v_pos.xy) * BlurTextureScale); + o_col0 = float4(LOAD_TEXTURE(samp0, blur_pos, 0).rgb * BlurBackgroundWeight + v_col0.rgb * InvBlurBackgroundWeight, v_col0.a); } )"; @@ -1002,3 +1043,103 @@ std::string ShaderGen::GenerateFadeFragmentShader() const return std::move(ss).str(); } + +std::string ShaderGen::GenerateGaussianBlurFragmentShader() const +{ + std::stringstream ss; + WriteHeader(ss); + + // Push constants: blur direction scaled by texel size (x_dir/width, y_dir/height) + DeclareUniformBuffer(ss, {"float2 u_blur_direction"}, true); + DeclareTexture(ss, "samp0", 0); + + // https://lisyarus.github.io/blog/posts/blur-coefficients-generator.html + // Radius: 30, sigma: 20 + ss << R"( +#define SAMPLE_COUNT 31 + +CONSTANT float OFFSETS[SAMPLE_COUNT] = BEGIN_ARRAY(float, SAMPLE_COUNT) +-29.481574788498758, +-27.482822966811984, +-25.48407133476425, +-23.48531987689748, +-21.4865685814993, +-19.487817441187733, +-17.4890664530808, +-15.49031561813869, +-13.491564939014534, +-11.492814415362423, +-9.49406403495946, +-7.495313758095638, +-5.496563491310285, +-3.4978130444607514, +-1.4990620619239194, +0.4996866407382734, +2.498437651462263, +4.497188307705211, +6.4959386324091755, +8.494688887763292, +10.493439208848661, +12.492189658097768, +14.490940258972138, +16.48969101629867, +18.48844192812288, +20.487192992240907, +22.485944209457497, +24.484695584886197, +26.48344712812872, +28.482198852858183, +30 +END_ARRAY; + +CONSTANT float WEIGHTS[SAMPLE_COUNT] = BEGIN_ARRAY(float, SAMPLE_COUNT) +0.01541016258836947, +0.017768151535171497, +0.020283267106540014, +0.022924205290094233, +0.025651423266747478, +0.02841773778937317, +0.03116939833169751, +0.03384762398431486, +0.03639055836034262, +0.03873556231203028, +0.040821733313367456, +0.042592516137640944, +0.043998254731464806, +0.0449985319608136, +0.04556415312900511, +0.04567825324468065, +0.04533701597951506, +0.04455118394212641, +0.043343783883772295, +0.04174984227341022, +0.03981466909233367, +0.03759167925377385, +0.03513992790902628, +0.032521509921346885, +0.029798976743363043, +0.027032914767164073, +0.024279809433072556, +0.02159029141425853, +0.019007828077837678, +0.016567888438120362, +0.007421145789225499 +END_ARRAY; +)"; + + DeclareFragmentEntryPoint(ss, 0, 1); + ss << R"( +{ + float3 result = float3(0.0f, 0.0f, 0.0f); + for (int i = 0; i < SAMPLE_COUNT; i++) + { + float2 offset = u_blur_direction * OFFSETS[i]; + float3 color = SAMPLE_TEXTURE(samp0, v_tex0 + offset).rgb; + result += color * WEIGHTS[i]; + } + + o_col0 = float4(result, 1.0); +})"; + + return std::move(ss).str(); +} diff --git a/src/util/shadergen.h b/src/util/shadergen.h index 9d92c024b..b3e1cf3b3 100644 --- a/src/util/shadergen.h +++ b/src/util/shadergen.h @@ -36,7 +36,10 @@ public: std::string GenerateImGuiVertexShader() const; std::string GenerateImGuiFragmentShader() const; + std::string GenerateImGuiBlurVertexShader() const; + std::string GenerateImGuiBlurFragmentShader() const; std::string GenerateFadeFragmentShader() const; + std::string GenerateGaussianBlurFragmentShader() const; const char* GetInterpolationQualifier(bool interface_block, bool centroid_interpolation, bool sample_interpolation, bool is_out) const;