diff --git a/src/common/gsvector.cpp b/src/common/gsvector.cpp index a617f3924..146411240 100644 --- a/src/common/gsvector.cpp +++ b/src/common/gsvector.cpp @@ -5,6 +5,34 @@ #include +GSVector4i GSVector4i::rfit(const GSVector4i& fit_rect, const GSVector2i& image_size) +{ + const GSVector2 ffit_size = GSVector2(fit_rect.rsize()); + const GSVector2 fimage_size = GSVector2(image_size); + const float fit_ar = ffit_size.x / ffit_size.y; + const float image_ar = fimage_size.x / fimage_size.y; + + GSVector4i ret; + if (fit_ar > image_ar) + { + // center horizontally + const float width = ffit_size.y * image_ar; + const float offset = (ffit_size.x - width) / 2.0f; + const float height = ffit_size.y; + ret = GSVector4i(GSVector4(offset, 0.0f, offset + width, height)); + } + else + { + // center vertically + const float height = ffit_size.x / image_ar; + const float offset = (ffit_size.y - height) / 2.0f; + const float width = ffit_size.x; + ret = GSVector4i(GSVector4(0.0f, offset, width, offset + height)); + } + + return ret; +} + GSMatrix2x2::GSMatrix2x2(float e00, float e01, float e10, float e11) { E[0][0] = e00; diff --git a/src/common/gsvector_neon.h b/src/common/gsvector_neon.h index 9d8487e0f..42d5c5fcb 100644 --- a/src/common/gsvector_neon.h +++ b/src/common/gsvector_neon.h @@ -2345,6 +2345,8 @@ public: ALWAYS_INLINE static GSVector4i xyxy(const GSVector2i& xyzw) { return GSVector4i(vcombine_s32(xyzw.v2s, xyzw.v2s)); } + static GSVector4i rfit(const GSVector4i& fit_rect, const GSVector2i& image_size); + ALWAYS_INLINE GSVector2i xy() const { return GSVector2i(vget_low_s32(v4s)); } ALWAYS_INLINE GSVector2i zw() const { return GSVector2i(vget_high_s32(v4s)); } diff --git a/src/common/gsvector_nosimd.h b/src/common/gsvector_nosimd.h index 8977670bb..8146738cf 100644 --- a/src/common/gsvector_nosimd.h +++ b/src/common/gsvector_nosimd.h @@ -1565,7 +1565,6 @@ public: return ret; } - template ALWAYS_INLINE static GSVector4i loadh(const GSVector2i& v) { return loadh(&v); @@ -1668,6 +1667,8 @@ public: ALWAYS_INLINE static GSVector4i xyxy(const GSVector2i& xyzw) { return GSVector4i(xyzw.x, xyzw.y, xyzw.x, xyzw.y); } + static GSVector4i rfit(const GSVector4i& fit_rect, const GSVector2i& image_size); + ALWAYS_INLINE GSVector2i xy() const { return GSVector2i(x, y); } ALWAYS_INLINE GSVector2i zw() const { return GSVector2i(z, w); } diff --git a/src/common/gsvector_sse.h b/src/common/gsvector_sse.h index 986c4b6fe..f51f17ec7 100644 --- a/src/common/gsvector_sse.h +++ b/src/common/gsvector_sse.h @@ -1745,7 +1745,6 @@ public: return GSVector4i(_mm_castps_si128(_mm_loadh_pi(_mm_setzero_ps(), static_cast(p)))); } - template ALWAYS_INLINE static GSVector4i loadh(const GSVector2i& v) { return GSVector4i(_mm_unpacklo_epi64(_mm_setzero_si128(), v.m)); @@ -1842,6 +1841,8 @@ public: return GSVector4i(_mm_unpacklo_epi64(xy.m, zw.m)); } + static GSVector4i rfit(const GSVector4i& fit_rect, const GSVector2i& image_size); + ALWAYS_INLINE GSVector2i xy() const { return GSVector2i(m); } ALWAYS_INLINE GSVector2i zw() const { return GSVector2i(_mm_shuffle_epi32(m, _MM_SHUFFLE(3, 2, 3, 2))); } diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index e61b824d6..c68df1d26 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -8,6 +8,7 @@ #include "controller.h" #include "game_list.h" #include "gpu.h" +#include "gpu_presenter.h" #include "gpu_thread.h" #include "host.h" #include "imgui_overlays.h" @@ -355,14 +356,12 @@ static void DrawFloatSpinBoxSetting(SettingsInterface* bsi, const char* title, c float step_value, float multiplier, const char* format = "%f", bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = UIStyle.LargeFont, ImFont* summary_font = UIStyle.MediumFont); -#if 0 static void DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* left_key, int default_left, const char* top_key, int default_top, const char* right_key, int default_right, const char* bottom_key, int default_bottom, int min_value, int max_value, const char* format = "%d", bool enabled = true, - float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, - ImFont* summary_font = g_medium_font); -#endif + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = UIStyle.LargeFont, ImFont* summary_font = UIStyle.MediumFont); static void DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, const char* default_value, std::span options, std::span option_values, bool enabled = true, @@ -2763,7 +2762,6 @@ void FullscreenUI::DrawFloatSpinBoxSetting(SettingsInterface* bsi, const char* t ImGui::PopFont(); } -#if 0 void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* left_key, int default_left, const char* top_key, int default_top, const char* right_key, int default_right, const char* bottom_key, @@ -2784,7 +2782,8 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, left_value.has_value() ? TinyString::from_sprintf(format, left_value.value()) : TinyString(FSUI_VSTR("Default")), top_value.has_value() ? TinyString::from_sprintf(format, top_value.value()) : TinyString(FSUI_VSTR("Default")), right_value.has_value() ? TinyString::from_sprintf(format, right_value.value()) : TinyString(FSUI_VSTR("Default")), - bottom_value.has_value() ? TinyString::from_sprintf(format, bottom_value.value()) : TinyString(FSUI_VSTR("Default"))); + bottom_value.has_value() ? TinyString::from_sprintf(format, bottom_value.value()) : + TinyString(FSUI_VSTR("Default"))); if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) ImGui::OpenPopup(title); @@ -2792,7 +2791,7 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, ImGui::SetNextWindowSize(LayoutScale(500.0f, 370.0f)); ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::PushFont(g_large_font); + ImGui::PushFont(font); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); @@ -2868,7 +2867,8 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, if (left_modified || top_modified || right_modified || bottom_modified) SetSettingsChanged(bsi); - if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f))) + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) { ImGui::CloseCurrentPopup(); } @@ -2880,7 +2880,6 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, ImGui::PopStyleVar(4); ImGui::PopFont(); } -#endif void FullscreenUI::DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, int default_value, int min_value, @@ -5677,6 +5676,86 @@ void FullscreenUI::DrawPostProcessingSettingsPage() break; } + MenuHeading(FSUI_CSTR("Border Overlay")); + + { + const std::optional preset_name = bsi->GetOptionalTinyStringValue("BorderOverlay", "PresetName"); + const bool is_null = !preset_name.has_value(); + const bool is_none = (!is_null && preset_name->empty()); + const bool is_custom = (!is_null && preset_name.value() == "Custom"); + const char* visible_value = + is_null ? FSUI_CSTR("Use Global Setting") : + (is_none ? FSUI_CSTR("None") : (is_custom ? FSUI_CSTR("Custom") : preset_name->c_str())); + if (MenuButtonWithValue( + FSUI_ICONSTR(ICON_FA_BORDER_ALL, "Selected Preset"), + FSUI_CSTR("Select from the list of preset borders, or manually specify a custom configuration."), + visible_value)) + { + std::vector preset_names = GPUPresenter::EnumerateBorderOverlayPresets(); + ChoiceDialogOptions options; + options.reserve(preset_names.size() + 2 + BoolToUInt32(IsEditingGameSettings(bsi))); + if (IsEditingGameSettings(bsi)) + options.emplace_back(FSUI_STR("Use Global Setting"), is_null); + options.emplace_back(FSUI_STR("None"), is_none); + options.emplace_back(FSUI_STR("Custom"), is_custom); + for (std::string& name : preset_names) + { + const bool is_selected = (preset_name.has_value() && preset_name.value() == name); + options.emplace_back(std::move(name), is_selected); + } + + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_BORDER_ALL, "Border Overlay"), false, std::move(options), + [game_settings = IsEditingGameSettings(bsi)](s32 index, const std::string& title, bool) mutable { + if (index < 0) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* const bsi = GetEditingSettingsInterface(game_settings); + const s32 offset = static_cast(BoolToUInt32(game_settings)); + if (game_settings && index == 0) + { + bsi->DeleteValue("BorderOverlay", "PresetName"); + } + else + { + const char* new_value = + (index == (offset + 0)) ? "" : ((index == (offset + 1)) ? "Custom" : title.c_str()); + bsi->SetStringValue("BorderOverlay", "PresetName", new_value); + } + SetSettingsChanged(bsi); + }); + } + + if (is_custom) + { + if (MenuButton(FSUI_ICONSTR(ICON_FA_IMAGE, "Image Path"), + GetEffectiveTinyStringSetting(bsi, "BorderOverlay", "ImagePath", ""))) + { + OpenFileSelector( + FSUI_ICONSTR(ICON_FA_IMAGE, "Image Path"), false, + [game_settings = IsEditingGameSettings(bsi)](const std::string& path) { + if (path.empty()) + return; + + SettingsInterface* const bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("BorderOverlay", "ImagePath", path.c_str()); + SetSettingsChanged(bsi); + }, + GetImageFilters()); + } + + DrawIntRectSetting(bsi, FSUI_ICONSTR(ICON_FA_BORDER_STYLE, "Display Area"), + FSUI_CSTR("Determines the area of the overlay image that the display will be drawn within."), + "BorderOverlay", "DisplayStartX", 0, "DisplayStartY", 0, "DisplayEndX", 0, "DisplayEndY", 0, 0, + 65535, "%dpx"); + + DrawToggleSetting( + bsi, FSUI_ICONSTR(ICON_FA_BLENDER, "Destination Alpha Blending"), + FSUI_CSTR("If enabled, the display will be blended with the transparency of the overlay image."), + "BorderOverlay", "AlphaBlend", false); + } + } + EndMenuButtons(); } @@ -8590,6 +8669,7 @@ TRANSLATE_NOOP("FullscreenUI", "Back"); TRANSLATE_NOOP("FullscreenUI", "Back To Pause Menu"); TRANSLATE_NOOP("FullscreenUI", "Backend Settings"); TRANSLATE_NOOP("FullscreenUI", "Behavior"); +TRANSLATE_NOOP("FullscreenUI", "Border Overlay"); TRANSLATE_NOOP("FullscreenUI", "Borderless Fullscreen"); TRANSLATE_NOOP("FullscreenUI", "Buffer Size"); TRANSLATE_NOOP("FullscreenUI", "Buttons"); @@ -8650,6 +8730,7 @@ TRANSLATE_NOOP("FullscreenUI", "Create Save State Backups"); TRANSLATE_NOOP("FullscreenUI", "Crop Mode"); TRANSLATE_NOOP("FullscreenUI", "Culling Correction"); TRANSLATE_NOOP("FullscreenUI", "Current Game"); +TRANSLATE_NOOP("FullscreenUI", "Custom"); TRANSLATE_NOOP("FullscreenUI", "Deadzone"); TRANSLATE_NOOP("FullscreenUI", "Debugging Settings"); TRANSLATE_NOOP("FullscreenUI", "Default"); @@ -8663,6 +8744,7 @@ TRANSLATE_NOOP("FullscreenUI", "Delete State"); TRANSLATE_NOOP("FullscreenUI", "Depth Clear Threshold"); TRANSLATE_NOOP("FullscreenUI", "Depth Test Transparent Polygons"); TRANSLATE_NOOP("FullscreenUI", "Desktop Mode"); +TRANSLATE_NOOP("FullscreenUI", "Destination Alpha Blending"); TRANSLATE_NOOP("FullscreenUI", "Details"); TRANSLATE_NOOP("FullscreenUI", "Details unavailable for game not scanned in game list."); TRANSLATE_NOOP("FullscreenUI", "Determines how large the on-screen messages and monitor are."); @@ -8675,6 +8757,7 @@ TRANSLATE_NOOP("FullscreenUI", "Determines how the emulated console's output is TRANSLATE_NOOP("FullscreenUI", "Determines quality of audio when not running at 100% speed."); TRANSLATE_NOOP("FullscreenUI", "Determines that field that the game list will be sorted by."); TRANSLATE_NOOP("FullscreenUI", "Determines the amount of audio buffered before being pulled by the host API."); +TRANSLATE_NOOP("FullscreenUI", "Determines the area of the overlay image that the display will be drawn within."); TRANSLATE_NOOP("FullscreenUI", "Determines the emulated hardware type."); TRANSLATE_NOOP("FullscreenUI", "Determines the format that screenshots will be saved/compressed with."); TRANSLATE_NOOP("FullscreenUI", "Determines the frequency at which the macro will toggle the buttons on and off (aka auto fire)."); @@ -8692,6 +8775,7 @@ TRANSLATE_NOOP("FullscreenUI", "Disabled"); TRANSLATE_NOOP("FullscreenUI", "Disables dithering and uses the full 8 bits per channel of color information."); TRANSLATE_NOOP("FullscreenUI", "Disc {} | {}"); TRANSLATE_NOOP("FullscreenUI", "Discord Server"); +TRANSLATE_NOOP("FullscreenUI", "Display Area"); TRANSLATE_NOOP("FullscreenUI", "Displays DualShock/DualSense button icons in the footer and input binding, instead of Xbox buttons."); TRANSLATE_NOOP("FullscreenUI", "Displays popup messages on events such as achievement unlocks and leaderboard submissions."); TRANSLATE_NOOP("FullscreenUI", "Displays popup messages when starting, submitting, or failing a leaderboard challenge."); @@ -8815,7 +8899,9 @@ TRANSLATE_NOOP("FullscreenUI", "Hotkey Settings"); TRANSLATE_NOOP("FullscreenUI", "How many saves will be kept for rewinding. Higher values have greater memory requirements."); TRANSLATE_NOOP("FullscreenUI", "How often a rewind state will be created. Higher frequencies have greater system requirements."); TRANSLATE_NOOP("FullscreenUI", "Identifies any new files added to the game directories."); +TRANSLATE_NOOP("FullscreenUI", "If enabled, the display will be blended with the transparency of the overlay image."); TRANSLATE_NOOP("FullscreenUI", "If not enabled, the current post processing chain will be ignored."); +TRANSLATE_NOOP("FullscreenUI", "Image Path"); TRANSLATE_NOOP("FullscreenUI", "Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games."); TRANSLATE_NOOP("FullscreenUI", "Increases the precision of polygon culling, reducing the number of holes in geometry."); TRANSLATE_NOOP("FullscreenUI", "Inhibit Screensaver"); @@ -9030,6 +9116,8 @@ TRANSLATE_NOOP("FullscreenUI", "Select Disc Image"); TRANSLATE_NOOP("FullscreenUI", "Select Game"); TRANSLATE_NOOP("FullscreenUI", "Select Macro {} Binds"); TRANSLATE_NOOP("FullscreenUI", "Select State"); +TRANSLATE_NOOP("FullscreenUI", "Select from the list of preset borders, or manually specify a custom configuration."); +TRANSLATE_NOOP("FullscreenUI", "Selected Preset"); TRANSLATE_NOOP("FullscreenUI", "Selects the GPU to use for rendering."); TRANSLATE_NOOP("FullscreenUI", "Selects the percentage of the normal clock speed the emulated hardware will run at."); TRANSLATE_NOOP("FullscreenUI", "Selects the quality at which screenshots will be compressed."); diff --git a/src/core/gpu_presenter.cpp b/src/core/gpu_presenter.cpp index 7590c7969..dcd0d0ea9 100644 --- a/src/core/gpu_presenter.cpp +++ b/src/core/gpu_presenter.cpp @@ -29,6 +29,7 @@ #include "common/gsvector_formatter.h" #include "common/log.h" #include "common/path.h" +#include "common/settings_interface.h" #include "common/small_string.h" #include "common/string_util.h" #include "common/threading.h" @@ -38,6 +39,8 @@ LOG_CHANNEL(GPU); +#include "common/ryml_helpers.h" + static constexpr GPUTexture::Format DISPLAY_INTERNAL_POSTFX_FORMAT = GPUTexture::Format::RGBA8; GPUPresenter::GPUPresenter() = default; @@ -50,6 +53,15 @@ GPUPresenter::~GPUPresenter() bool GPUPresenter::Initialize(Error* error) { + // we can't change the format after compiling shaders + m_present_format = + g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetFormat() : GPUTexture::Format::RGBA8; + VERBOSE_LOG("Presentation format is {}", GPUTexture::GetFormatName(m_present_format)); + + // overlay has to come first, because it sets the alpha blending on the display pipeline + if (LoadOverlaySettings()) + LoadOverlayTexture(); + if (!CompileDisplayPipelines(true, true, g_gpu_settings.display_24bit_chroma_smoothing, error)) return false; @@ -79,6 +91,22 @@ bool GPUPresenter::UpdateSettings(const GPUSettings& old_settings, Error* error) return true; } +bool GPUPresenter::UpdatePostProcessingSettings(Error* error) +{ + if (LoadOverlaySettings()) + { + // something changed, need to recompile pipelines + if (LoadOverlayTexture() && m_border_overlay_alpha_blend && + (!m_present_copy_blend_pipeline || !m_display_blend_pipeline) && + !CompileDisplayPipelines(true, false, false, error)) + { + return false; + } + } + + return true; +} + bool GPUPresenter::CompileDisplayPipelines(bool display, bool deinterlace, bool chroma_smoothing, Error* error) { const GPUShaderGen shadergen(g_gpu_device->GetRenderAPI(), g_gpu_device->GetFeatures().dual_source_blend, @@ -100,10 +128,14 @@ bool GPUPresenter::CompileDisplayPipelines(bool display, bool deinterlace, bool GPUBackend::SetScreenQuadInputLayout(plconfig); plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; - plconfig.SetTargetFormats(g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetFormat() : - GPUTexture::Format::RGBA8); + plconfig.SetTargetFormats(m_present_format); + + std::unique_ptr vso = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), + shadergen.GenerateDisplayVertexShader(), error); + if (!vso) + return false; + GL_OBJECT_NAME(vso, "Display Vertex Shader"); - std::string vs = shadergen.GenerateDisplayVertexShader(); std::string fs; switch (g_gpu_settings.display_scaling) { @@ -123,25 +155,59 @@ bool GPUPresenter::CompileDisplayPipelines(bool display, bool deinterlace, bool break; } - std::unique_ptr vso = - g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), vs, error); std::unique_ptr fso = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), fs, error); - if (!vso || !fso) + if (!fso) return false; - GL_OBJECT_NAME(vso, "Display Vertex Shader"); GL_OBJECT_NAME_FMT(fso, "Display Fragment Shader [{}]", Settings::GetDisplayScalingName(g_gpu_settings.display_scaling)); + plconfig.vertex_shader = vso.get(); plconfig.fragment_shader = fso.get(); if (!(m_display_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) return false; GL_OBJECT_NAME_FMT(m_display_pipeline, "Display Pipeline [{}]", Settings::GetDisplayScalingName(g_gpu_settings.display_scaling)); + + std::unique_ptr rotate_copy_fso = g_gpu_device->CreateShader( + GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateCopyFragmentShader(false), error); + if (!rotate_copy_fso) + return false; + GL_OBJECT_NAME(rotate_copy_fso, "Display Rotate/Copy Fragment Shader"); + + plconfig.fragment_shader = rotate_copy_fso.get(); + if (!(m_present_copy_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) + return false; + GL_OBJECT_NAME(m_present_copy_pipeline, "Display Rotate/Copy Pipeline"); + + // blended variants + if (m_border_overlay_texture && m_border_overlay_alpha_blend) + { + // destination blend the main present, not source + plconfig.blend.enable = true; + plconfig.blend.src_blend = GPUPipeline::BlendFunc::InvDstAlpha; + plconfig.blend.blend_op = GPUPipeline::BlendOp::Add; + plconfig.blend.dst_blend = GPUPipeline::BlendFunc::One; + plconfig.blend.src_alpha_blend = GPUPipeline::BlendFunc::One; + plconfig.blend.alpha_blend_op = GPUPipeline::BlendOp::Add; + plconfig.blend.dst_alpha_blend = GPUPipeline::BlendFunc::Zero; + + plconfig.fragment_shader = fso.get(); + if (!(m_display_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) + return false; + GL_OBJECT_NAME_FMT(m_display_blend_pipeline, "Display Pipeline [Blended, {}]", + Settings::GetDisplayScalingName(g_gpu_settings.display_scaling)); + + plconfig.fragment_shader = rotate_copy_fso.get(); + if (!(m_present_copy_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) + return false; + GL_OBJECT_NAME(m_present_copy_blend_pipeline, "Display Rotate/Copy Pipeline [Blended]"); + } } plconfig.input_layout = {}; plconfig.primitive = GPUPipeline::Primitive::Triangles; + plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState(); if (deinterlace) { @@ -304,18 +370,38 @@ void GPUPresenter::SetDisplayTexture(GPUTexture* texture, GPUTexture* depth_buff GPUDevice::PresentResult GPUPresenter::PresentDisplay() { - if (!g_gpu_device->HasMainSwapChain()) - return GPUDevice::PresentResult::SkipPresent; + DebugAssert(g_gpu_device->HasMainSwapChain()); + + u32 display_area_width = g_gpu_device->GetMainSwapChain()->GetWidth(); + u32 display_area_height = g_gpu_device->GetMainSwapChain()->GetHeight(); + GSVector4i overlay_display_rect = GSVector4i::zero(); + GSVector4i overlay_rect = GSVector4i::zero(); + if (m_border_overlay_texture) + { + overlay_rect = GSVector4i::rfit(GSVector4i(0, 0, display_area_width, display_area_height), + m_border_overlay_texture->GetSizeVec()); + + const GSVector2 scale = GSVector2(overlay_rect.rsize()) / GSVector2(m_border_overlay_texture->GetSizeVec()); + overlay_display_rect = + GSVector4i(GSVector4(m_border_overlay_display_rect) * GSVector4::xyxy(scale)).add32(overlay_rect.xyxy()); + display_area_width = overlay_display_rect.width(); + display_area_height = overlay_display_rect.height(); + } GSVector4i display_rect; GSVector4i draw_rect; - CalculateDrawRect(g_gpu_device->GetMainSwapChain()->GetWidth(), g_gpu_device->GetMainSwapChain()->GetHeight(), - !g_gpu_settings.gpu_show_vram, true, &display_rect, &draw_rect); - return RenderDisplay(nullptr, display_rect, draw_rect, !g_gpu_settings.gpu_show_vram); + CalculateDrawRect(display_area_width, display_area_height, !g_gpu_settings.gpu_show_vram, true, &display_rect, + &draw_rect); + + display_rect = display_rect.add32(overlay_display_rect.xyxy()); + draw_rect = draw_rect.add32(overlay_display_rect.xyxy()); + + return RenderDisplay(nullptr, overlay_rect, display_rect, draw_rect, !g_gpu_settings.gpu_show_vram); } -GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const GSVector4i display_rect, - const GSVector4i draw_rect, bool postfx) +GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const GSVector4i overlay_rect, + const GSVector4i display_rect, const GSVector4i draw_rect, + bool postfx) { GL_SCOPE_FMT("RenderDisplay: {}", draw_rect); @@ -332,10 +418,6 @@ GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const G PostProcessing::InternalChain.CheckTargets(DISPLAY_INTERNAL_POSTFX_FORMAT, display_texture_view_width, display_texture_view_height)) { - DebugAssert(display_texture_view_x == 0 && display_texture_view_y == 0 && - static_cast(display_texture->GetWidth()) == display_texture_view_width && - static_cast(display_texture->GetHeight()) == display_texture_view_height); - // Now we can apply the post chain. GPUTexture* post_output_texture = PostProcessing::InternalChain.GetOutputTexture(); if (PostProcessing::InternalChain.Apply(display_texture, m_display_depth_buffer, post_output_texture, @@ -350,178 +432,272 @@ GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const G } } - const GPUTexture::Format hdformat = target ? target->GetFormat() : g_gpu_device->GetMainSwapChain()->GetFormat(); - const u32 target_width = target ? target->GetWidth() : g_gpu_device->GetMainSwapChain()->GetWidth(); - const u32 target_height = target ? target->GetHeight() : g_gpu_device->GetMainSwapChain()->GetHeight(); - const bool really_postfx = (postfx && PostProcessing::DisplayChain.IsActive() && g_gpu_device->HasMainSwapChain() && - hdformat != GPUTexture::Format::Unknown && target_width > 0 && target_height > 0 && - PostProcessing::DisplayChain.CheckTargets(hdformat, target_width, target_height)); - const u32 real_target_width = - (target || really_postfx) ? target_width : g_gpu_device->GetMainSwapChain()->GetPostRotatedWidth(); - const u32 real_target_height = - (target || really_postfx) ? target_height : g_gpu_device->GetMainSwapChain()->GetPostRotatedHeight(); - GSVector4i real_draw_rect = - (target || really_postfx) ? draw_rect : g_gpu_device->GetMainSwapChain()->PreRotateClipRect(draw_rect); - if (really_postfx) - { - g_gpu_device->ClearRenderTarget(PostProcessing::DisplayChain.GetInputTexture(), GPUDevice::DEFAULT_CLEAR_COLOR); - g_gpu_device->SetRenderTarget(PostProcessing::DisplayChain.GetInputTexture()); - } - else - { + // There's a bunch of scenarios where we need to use intermediate buffers. + // If we have post-processing and overlays enabled, postfx needs to happen on an intermediate buffer first. + // If pre-rotation is enabled with post-processing, we need to draw to an intermediate buffer, and apply the + // rotation at the end. + GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); + const WindowInfo::PreRotation prerotation = target ? WindowInfo::PreRotation::Identity : swap_chain->GetPreRotation(); + const bool have_overlay = static_cast(m_border_overlay_texture); + const bool have_prerotation = (prerotation != WindowInfo::PreRotation::Identity); + const GSVector2i target_size = target ? target->GetSizeVec() : swap_chain->GetSizeVec(); + GL_INS(have_overlay ? "Overlay is ENABLED" : "Overlay is disabled"); + GL_INS_FMT("Prerotation: {}", static_cast(prerotation)); + GL_INS_FMT("Final target size: {}x{}", target_size.x, target_size.y); + + // Postfx active? + const GSVector2i postfx_size = have_overlay ? display_rect.rsize() : target_size; + const bool really_postfx = + (postfx && PostProcessing::DisplayChain.IsActive() && + PostProcessing::DisplayChain.CheckTargets(m_present_format, postfx_size.x, postfx_size.y)); + GL_INS(really_postfx ? "Post-processing is ENABLED" : "Post-processing is disabled"); + GL_INS_FMT("Post-processing render target size: {}x{}", postfx_size.x, postfx_size.y); + + // Helper to bind swap chain/final target. + const auto bind_final_target = [target, swap_chain](bool clear) { if (target) { + if (clear) + g_gpu_device->ClearRenderTarget(target, GPUDevice::DEFAULT_CLEAR_COLOR); + else + g_gpu_device->InvalidateRenderTarget(target); g_gpu_device->SetRenderTarget(target); + return GPUDevice::PresentResult::OK; } else { - const GPUDevice::PresentResult pres = g_gpu_device->BeginPresent(g_gpu_device->GetMainSwapChain()); - if (pres != GPUDevice::PresentResult::OK) - return pres; + return g_gpu_device->BeginPresent(swap_chain); } - } - - if (display_texture) - { - bool texture_filter_linear = false; - - struct alignas(16) Uniforms - { - float src_size[4]; - float clamp_rect[4]; - float params[4]; - } uniforms; - std::memset(uniforms.params, 0, sizeof(uniforms.params)); - - switch (g_gpu_settings.display_scaling) - { - case DisplayScalingMode::Nearest: - case DisplayScalingMode::NearestInteger: - break; - - case DisplayScalingMode::BilinearSmooth: - case DisplayScalingMode::BilinearInteger: - texture_filter_linear = true; - break; - - case DisplayScalingMode::BilinearSharp: - { - texture_filter_linear = true; - uniforms.params[0] = std::max( - std::floor(static_cast(draw_rect.width()) / static_cast(m_display_texture_view_width)), 1.0f); - uniforms.params[1] = std::max( - std::floor(static_cast(draw_rect.height()) / static_cast(m_display_texture_view_height)), 1.0f); - uniforms.params[2] = 0.5f - 0.5f / uniforms.params[0]; - uniforms.params[3] = 0.5f - 0.5f / uniforms.params[1]; - } - break; - - default: - UnreachableCode(); - break; - } - - g_gpu_device->SetPipeline(m_display_pipeline.get()); - g_gpu_device->SetTextureSampler( - 0, display_texture, texture_filter_linear ? g_gpu_device->GetLinearSampler() : g_gpu_device->GetNearestSampler()); - - // For bilinear, clamp to 0.5/SIZE-0.5 to avoid bleeding from the adjacent texels in VRAM. This is because - // 1.0 in UV space is not the bottom-right texel, but a mix of the bottom-right and wrapped/next texel. - const GSVector2 display_texture_size = GSVector2(display_texture->GetSizeVec()); - const GSVector4 display_texture_size4 = GSVector4::xyxy(display_texture_size); - const GSVector4 uv_rect = GSVector4(GSVector4i(display_texture_view_x, display_texture_view_y, - display_texture_view_x + display_texture_view_width, - display_texture_view_y + display_texture_view_height)) / - display_texture_size4; - GSVector4::store(uniforms.clamp_rect, - GSVector4(static_cast(display_texture_view_x) + 0.5f, - static_cast(display_texture_view_y) + 0.5f, - static_cast(display_texture_view_x + display_texture_view_width) - 0.5f, - static_cast(display_texture_view_y + display_texture_view_height) - 0.5f) / - display_texture_size4); - GSVector4::store(uniforms.src_size, - GSVector4::xyxy(display_texture_size, GSVector2::cxpr(1.0f) / display_texture_size)); - - g_gpu_device->PushUniformBuffer(&uniforms, sizeof(uniforms)); - - g_gpu_device->SetViewport(0, 0, real_target_width, real_target_height); - g_gpu_device->SetScissor(g_gpu_device->UsesLowerLeftOrigin() ? - GPUDevice::FlipToLowerLeft(real_draw_rect, real_target_height) : - real_draw_rect); - - GPUBackend::ScreenVertex* vertices; - u32 space; - u32 base_vertex; - g_gpu_device->MapVertexBuffer(sizeof(GPUBackend::ScreenVertex), 4, reinterpret_cast(&vertices), &space, - &base_vertex); - - const WindowInfo::PreRotation surface_prerotation = (target || really_postfx) ? - WindowInfo::PreRotation::Identity : - g_gpu_device->GetMainSwapChain()->GetPreRotation(); - - const DisplayRotation uv_rotation = static_cast( - (static_cast(g_gpu_settings.display_rotation) + static_cast(surface_prerotation)) % - static_cast(DisplayRotation::Count)); - - const GSVector4 xy = - GPUBackend::GetScreenQuadClipSpaceCoordinates(real_draw_rect, GSVector2i(real_target_width, real_target_height)); - switch (uv_rotation) - { - case DisplayRotation::Normal: - vertices[0].Set(xy.xy(), uv_rect.xy()); - vertices[1].Set(xy.zyzw().xy(), uv_rect.zyzw().xy()); - vertices[2].Set(xy.xwzw().xy(), uv_rect.xwzw().xy()); - vertices[3].Set(xy.zw(), uv_rect.zw()); - break; - case DisplayRotation::Rotate90: - vertices[0].Set(xy.xy(), uv_rect.xwzw().xy()); - vertices[1].Set(xy.zyzw().xy(), uv_rect.xy()); - vertices[2].Set(xy.xwzw().xy(), uv_rect.zw()); - vertices[3].Set(xy.zw(), uv_rect.zyzw().xy()); - break; - case DisplayRotation::Rotate180: - vertices[0].Set(xy.xy(), uv_rect.xwzw().xy()); - vertices[1].Set(xy.zyzw().xy(), uv_rect.zw()); - vertices[2].Set(xy.xwzw().xy(), uv_rect.xy()); - vertices[3].Set(xy.zw(), uv_rect.zyzw().xy()); - break; - case DisplayRotation::Rotate270: - vertices[0].Set(xy.xy(), uv_rect.zyzw().xy()); - vertices[1].Set(xy.zyzw().xy(), uv_rect.zw()); - vertices[2].Set(xy.xwzw().xy(), uv_rect.xy()); - vertices[3].Set(xy.zw(), uv_rect.xwzw().xy()); - break; - - DefaultCaseIsUnreachable(); - } - - g_gpu_device->UnmapVertexBuffer(sizeof(GPUBackend::ScreenVertex), 4); - g_gpu_device->Draw(4, base_vertex); - } + }; + // If postfx is enabled, we need to draw to an intermediate buffer first. if (really_postfx) { - DebugAssert(!g_gpu_settings.gpu_show_vram); + // Remove draw offset if we're using an overlay. + const GSVector4i real_draw_rect = have_overlay ? draw_rect.sub32(display_rect.xyxy()) : draw_rect; - // "original size" in postfx includes padding. - const float upscale_x = - m_display_texture ? static_cast(m_display_texture_view_width) / static_cast(m_display_vram_width) : - 1.0f; - const float upscale_y = m_display_texture ? static_cast(m_display_texture_view_height) / - static_cast(m_display_vram_height) : - 1.0f; - const s32 orig_width = static_cast(std::ceil(static_cast(m_display_width) * upscale_x)); - const s32 orig_height = static_cast(std::ceil(static_cast(m_display_height) * upscale_y)); + // Display is always drawn to the postfx input. + GPUTexture* const postfx_input = PostProcessing::DisplayChain.GetInputTexture(); + g_gpu_device->ClearRenderTarget(postfx_input, GPUDevice::DEFAULT_CLEAR_COLOR); + g_gpu_device->SetRenderTarget(postfx_input); + if (display_texture) + { + DrawDisplay(postfx_size, display_texture, display_texture_view_x, display_texture_view_y, + display_texture_view_width, display_texture_view_height, real_draw_rect, false, + g_gpu_settings.display_rotation, WindowInfo::PreRotation::Identity); + } + postfx_input->MakeReadyForSampling(); - return PostProcessing::DisplayChain.Apply(PostProcessing::DisplayChain.GetInputTexture(), nullptr, target, - display_rect, orig_width, orig_height, m_display_width, m_display_height); + // Apply postprocessing to an intermediate texture if we're prerotating or have an overlay. + if (have_prerotation || have_overlay) + { + GPUTexture* const postfx_output = PostProcessing::DisplayChain.GetTextureUnusedAtEndOfChain(); + const GSVector4i real_display_rect = have_overlay ? display_rect.sub32(display_rect.xyxy()) : display_rect; + ApplyDisplayPostProcess(postfx_output, postfx_input, real_display_rect); + postfx_output->MakeReadyForSampling(); + + // Start draw to final buffer. + if (const GPUDevice::PresentResult pres = bind_final_target(have_overlay); pres != GPUDevice::PresentResult::OK) + return pres; + + // If we have an overlay, draw it, and then copy the postprocessed framebuffer in. + if (have_overlay) + { + DrawTextureCopy(target_size, overlay_rect, m_border_overlay_texture.get(), false, true, prerotation); + DrawTextureCopy(target_size, draw_rect, postfx_output, m_border_overlay_alpha_blend, false, prerotation); + } + else + { + // Ohterwise, just copy the framebuffer. + DrawTextureCopy(target_size, draw_rect, postfx_output, false, false, prerotation); + } + + // All done + return GPUDevice::PresentResult::OK; + } + else + { + // Otherwise apply postprocessing directly to swap chain. + return ApplyDisplayPostProcess(target, postfx_input, display_rect); + } } else { + // The non-postprocessing cases are much simpler. We always optionally draw the overlay, then draw the display. + // The only tricky bit is we have to combine the display rotation and prerotation for the latter. + if (const GPUDevice::PresentResult pres = bind_final_target(true); pres != GPUDevice::PresentResult::OK) + return pres; + + if (have_overlay) + DrawTextureCopy(target_size, overlay_rect, m_border_overlay_texture.get(), false, true, prerotation); + + if (display_texture) + { + DrawDisplay(target_size, display_texture, display_texture_view_x, display_texture_view_y, + display_texture_view_width, display_texture_view_height, display_rect, m_border_overlay_alpha_blend, + g_gpu_settings.display_rotation, prerotation); + } + return GPUDevice::PresentResult::OK; } } +void GPUPresenter::DrawDisplay(const GSVector2i target_size, GPUTexture* display_texture, s32 display_texture_view_x, + s32 display_texture_view_y, s32 display_texture_view_width, + s32 display_texture_view_height, const GSVector4i display_rect, bool dst_alpha_blend, + DisplayRotation rotation, WindowInfo::PreRotation prerotation) +{ + bool texture_filter_linear = false; + + struct alignas(16) Uniforms + { + float src_size[4]; + float clamp_rect[4]; + float params[4]; + } uniforms; + std::memset(uniforms.params, 0, sizeof(uniforms.params)); + + switch (g_gpu_settings.display_scaling) + { + case DisplayScalingMode::Nearest: + case DisplayScalingMode::NearestInteger: + break; + + case DisplayScalingMode::BilinearSmooth: + case DisplayScalingMode::BilinearInteger: + texture_filter_linear = true; + break; + + case DisplayScalingMode::BilinearSharp: + { + texture_filter_linear = true; + uniforms.params[0] = std::max( + std::floor(static_cast(display_rect.width()) / static_cast(m_display_texture_view_width)), 1.0f); + uniforms.params[1] = std::max( + std::floor(static_cast(display_rect.height()) / static_cast(m_display_texture_view_height)), + 1.0f); + uniforms.params[2] = 0.5f - 0.5f / uniforms.params[0]; + uniforms.params[3] = 0.5f - 0.5f / uniforms.params[1]; + } + break; + + default: + UnreachableCode(); + break; + } + + g_gpu_device->SetPipeline(dst_alpha_blend ? m_display_blend_pipeline.get() : m_display_pipeline.get()); + g_gpu_device->SetTextureSampler( + 0, display_texture, texture_filter_linear ? g_gpu_device->GetLinearSampler() : g_gpu_device->GetNearestSampler()); + + // For bilinear, clamp to 0.5/SIZE-0.5 to avoid bleeding from the adjacent texels in VRAM. This is because + // 1.0 in UV space is not the bottom-right texel, but a mix of the bottom-right and wrapped/next texel. + const GSVector2 display_texture_size = GSVector2(display_texture->GetSizeVec()); + const GSVector4 display_texture_size4 = GSVector4::xyxy(display_texture_size); + const GSVector4 uv_rect = GSVector4(GSVector4i(display_texture_view_x, display_texture_view_y, + display_texture_view_x + display_texture_view_width, + display_texture_view_y + display_texture_view_height)) / + display_texture_size4; + GSVector4::store(uniforms.clamp_rect, + GSVector4(static_cast(display_texture_view_x) + 0.5f, + static_cast(display_texture_view_y) + 0.5f, + static_cast(display_texture_view_x + display_texture_view_width) - 0.5f, + static_cast(display_texture_view_y + display_texture_view_height) - 0.5f) / + display_texture_size4); + GSVector4::store(uniforms.src_size, + GSVector4::xyxy(display_texture_size, GSVector2::cxpr(1.0f) / display_texture_size)); + + g_gpu_device->PushUniformBuffer(&uniforms, sizeof(uniforms)); + DrawScreenQuad(display_rect, uv_rect, target_size, rotation, prerotation); +} + +void GPUPresenter::DrawScreenQuad(const GSVector4i rect, const GSVector4 uv_rect, const GSVector2i target_size, + DisplayRotation rotation, WindowInfo::PreRotation prerotation) +{ + const GSVector4i real_rect = GPUSwapChain::PreRotateClipRect(prerotation, target_size, rect); + g_gpu_device->SetViewport(GSVector4i::loadh(target_size)); + g_gpu_device->SetScissor(g_gpu_device->UsesLowerLeftOrigin() ? GPUDevice::FlipToLowerLeft(real_rect, target_size.y) : + real_rect); + + GPUBackend::ScreenVertex* vertices; + u32 space; + u32 base_vertex; + g_gpu_device->MapVertexBuffer(sizeof(GPUBackend::ScreenVertex), 4, reinterpret_cast(&vertices), &space, + &base_vertex); + + const GSVector4 xy = GPUBackend::GetScreenQuadClipSpaceCoordinates(real_rect, target_size); + + // Combine display rotation and prerotation together, since the rectangle has already been adjusted. + const DisplayRotation effective_rotation = static_cast( + (static_cast(rotation) + static_cast(prerotation)) % static_cast(DisplayRotation::Count)); + switch (effective_rotation) + { + case DisplayRotation::Normal: + vertices[0].Set(xy.xy(), uv_rect.xy()); + vertices[1].Set(xy.zyzw().xy(), uv_rect.zyzw().xy()); + vertices[2].Set(xy.xwzw().xy(), uv_rect.xwzw().xy()); + vertices[3].Set(xy.zw(), uv_rect.zw()); + break; + + case DisplayRotation::Rotate90: + vertices[0].Set(xy.xy(), uv_rect.xwzw().xy()); + vertices[1].Set(xy.zyzw().xy(), uv_rect.xy()); + vertices[2].Set(xy.xwzw().xy(), uv_rect.zw()); + vertices[3].Set(xy.zw(), uv_rect.zyzw().xy()); + break; + + case DisplayRotation::Rotate180: + vertices[0].Set(xy.xy(), uv_rect.xwzw().xy()); + vertices[1].Set(xy.zyzw().xy(), uv_rect.zw()); + vertices[2].Set(xy.xwzw().xy(), uv_rect.xy()); + vertices[3].Set(xy.zw(), uv_rect.zyzw().xy()); + break; + + case DisplayRotation::Rotate270: + vertices[0].Set(xy.xy(), uv_rect.zyzw().xy()); + vertices[1].Set(xy.zyzw().xy(), uv_rect.zw()); + vertices[2].Set(xy.xwzw().xy(), uv_rect.xy()); + vertices[3].Set(xy.zw(), uv_rect.xwzw().xy()); + break; + + DefaultCaseIsUnreachable(); + } + + g_gpu_device->UnmapVertexBuffer(sizeof(GPUBackend::ScreenVertex), 4); + g_gpu_device->Draw(4, base_vertex); +} + +GPUDevice::PresentResult GPUPresenter::ApplyDisplayPostProcess(GPUTexture* target, GPUTexture* input, + const GSVector4i display_rect) +{ + DebugAssert(!g_gpu_settings.gpu_show_vram); + + // "original size" in postfx includes padding. + const float upscale_x = + m_display_texture ? static_cast(m_display_texture_view_width) / static_cast(m_display_vram_width) : + 1.0f; + const float upscale_y = + m_display_texture ? static_cast(m_display_texture_view_height) / static_cast(m_display_vram_height) : + 1.0f; + const s32 orig_width = static_cast(std::ceil(static_cast(m_display_width) * upscale_x)); + const s32 orig_height = static_cast(std::ceil(static_cast(m_display_height) * upscale_y)); + + return PostProcessing::DisplayChain.Apply(PostProcessing::DisplayChain.GetInputTexture(), nullptr, target, + display_rect, orig_width, orig_height, m_display_width, m_display_height); +} + +void GPUPresenter::DrawTextureCopy(const GSVector2i target_size, const GSVector4i draw_rect, GPUTexture* input, + bool dst_alpha_blend, bool linear, WindowInfo::PreRotation prerotation) +{ + GL_SCOPE_FMT("DrawTextureCopy({}, blend={}, linear={}, prerotation={})", draw_rect, dst_alpha_blend, draw_rect, + static_cast(prerotation)); + + g_gpu_device->SetPipeline(dst_alpha_blend ? m_present_copy_blend_pipeline.get() : m_present_copy_pipeline.get()); + g_gpu_device->SetTextureSampler(0, input, g_gpu_device->GetNearestSampler()); + + DrawScreenQuad(draw_rect, GSVector4::cxpr(0.0f, 0.0f, 1.0f, 1.0f), target_size, DisplayRotation::Normal, prerotation); +} + void GPUPresenter::SendDisplayToMediaCapture(MediaCapture* cap) { GPUTexture* target = cap->GetRenderTexture(); @@ -542,7 +718,7 @@ void GPUPresenter::SendDisplayToMediaCapture(MediaCapture* cap) // Not cleared by RenderDisplay(). g_gpu_device->ClearRenderTarget(target, GPUDevice::DEFAULT_CLEAR_COLOR); - if (RenderDisplay(target, display_rect, draw_rect, postfx) != GPUDevice::PresentResult::OK || + if (RenderDisplay(target, GSVector4i::zero(), display_rect, draw_rect, postfx) != GPUDevice::PresentResult::OK || !cap->DeliverVideoFrame(target)) [[unlikely]] { WARNING_LOG("Failed to render/deliver video capture frame."); @@ -857,21 +1033,19 @@ void GPUPresenter::SleepUntilPresentTime(u64 present_time) bool GPUPresenter::RenderScreenshotToBuffer(u32 width, u32 height, const GSVector4i display_rect, const GSVector4i draw_rect, bool postfx, Image* out_image) { - const GPUTexture::Format hdformat = - g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetFormat() : GPUTexture::Format::RGBA8; - const ImageFormat image_format = GPUTexture::GetImageFormatForTextureFormat(hdformat); + const ImageFormat image_format = GPUTexture::GetImageFormatForTextureFormat(m_present_format); if (image_format == ImageFormat::None) return false; auto render_texture = g_gpu_device->FetchAutoRecycleTexture(width, height, 1, 1, 1, GPUTexture::Type::RenderTarget, - hdformat, GPUTexture::Flags::None); + m_present_format, GPUTexture::Flags::None); if (!render_texture) return false; g_gpu_device->ClearRenderTarget(render_texture.get(), GPUDevice::DEFAULT_CLEAR_COLOR); // TODO: this should use copy shader instead. - RenderDisplay(render_texture.get(), display_rect, draw_rect, postfx); + RenderDisplay(render_texture.get(), GSVector4i::zero(), display_rect, draw_rect, postfx); Image image(width, height, image_format); @@ -879,12 +1053,12 @@ bool GPUPresenter::RenderScreenshotToBuffer(u32 width, u32 height, const GSVecto std::unique_ptr dltex; if (g_gpu_device->GetFeatures().memory_import) { - dltex = g_gpu_device->CreateDownloadTexture(width, height, hdformat, image.GetPixels(), image.GetStorageSize(), - image.GetPitch(), &error); + dltex = g_gpu_device->CreateDownloadTexture(width, height, m_present_format, image.GetPixels(), + image.GetStorageSize(), image.GetPitch(), &error); } if (!dltex) { - if (!(dltex = g_gpu_device->CreateDownloadTexture(width, height, hdformat, &error))) + if (!(dltex = g_gpu_device->CreateDownloadTexture(width, height, m_present_format, &error))) { ERROR_LOG("Failed to create {}x{} download texture: {}", width, height, error.GetDescription()); return false; @@ -945,3 +1119,157 @@ void GPUPresenter::CalculateScreenshotSize(DisplayScreenshotMode mode, u32* widt CalculateDrawRect(*width, *height, true, !g_settings.gpu_show_vram, display_rect, draw_rect); } } + +bool GPUPresenter::LoadOverlaySettings() +{ + std::string preset_name = Host::GetStringSettingValue("BorderOverlay", "PresetName"); + std::string image_path; + GSVector4i display_rect = m_border_overlay_display_rect; + bool alpha_blend = m_border_overlay_alpha_blend; + if (preset_name == "Custom") + { + image_path = Host::GetStringSettingValue("BorderOverlay", "ImagePath"); + display_rect = GSVector4i(Host::GetIntSettingValue("BorderOverlay", "DisplayStartX", 0), + Host::GetIntSettingValue("BorderOverlay", "DisplayStartY", 0), + Host::GetIntSettingValue("BorderOverlay", "DisplayEndX", 0), + Host::GetIntSettingValue("BorderOverlay", "DisplayEndY", 0)); + alpha_blend = Host::GetBoolSettingValue("BorderOverlay", "AlphaBlend", false); + } + + // check rect validity.. ignore everything if it's bogus + if (!image_path.empty() && display_rect.rempty()) + { + ERROR_LOG("Border overlay rectangle {} is invalid.", display_rect); + image_path = {}; + } + if (image_path.empty()) + { + // using preset? + if (!preset_name.empty()) + { + // don't worry about the other settings, the loader will fix them up + if (m_border_overlay_image_path == preset_name) + return false; + + image_path = std::move(preset_name); + } + + display_rect = GSVector4i::zero(); + alpha_blend = false; + } + + // display rect can be updated without issue + m_border_overlay_display_rect = display_rect; + + // but images and alphablend require pipeline/texture changes + if (m_border_overlay_image_path == image_path && (image_path.empty() || alpha_blend == m_border_overlay_alpha_blend)) + { + m_border_overlay_alpha_blend = alpha_blend; + return false; + } + + m_border_overlay_image_path = std::move(image_path); + m_border_overlay_alpha_blend = alpha_blend; + return true; +} + +bool GPUPresenter::LoadOverlayTexture() +{ + g_gpu_device->RecycleTexture(std::move(m_border_overlay_texture)); + if (m_border_overlay_image_path.empty()) + { + m_border_overlay_display_rect = GSVector4i::zero(); + m_border_overlay_image_path = {}; + m_border_overlay_alpha_blend = false; + return true; + } + + Image image; + Error error; + + bool image_load_result; + if (Path::IsAbsolute(m_border_overlay_image_path)) + image_load_result = image.LoadFromFile(m_border_overlay_image_path.c_str(), &error); + else + image_load_result = LoadOverlayPreset(&error, &image); + if (!image_load_result || + !(m_border_overlay_texture = g_gpu_device->FetchAndUploadTextureImage(image, GPUTexture::Flags::None, &error))) + { + ERROR_LOG("Failed to load overlay '{}': {}", Path::GetFileName(m_border_overlay_image_path), + error.GetDescription()); + m_border_overlay_display_rect = GSVector4i::zero(); + m_border_overlay_image_path = {}; + m_border_overlay_alpha_blend = false; + return false; + } + + INFO_LOG("Loaded overlay image {}: {}x{}", Path::GetFileName(m_border_overlay_image_path), + m_border_overlay_texture->GetWidth(), m_border_overlay_texture->GetHeight()); + return true; +} + +std::vector GPUPresenter::EnumerateBorderOverlayPresets() +{ + static constexpr const char* pattern = "*.yml"; + + std::vector ret; + + FileSystem::FindResultsArray files; + FileSystem::FindFiles(Path::Combine(EmuFolders::Resources, "overlays").c_str(), pattern, + FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_FILES, &files); + FileSystem::FindFiles(Path::Combine(EmuFolders::UserResources, "overlays").c_str(), pattern, + FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_KEEP_ARRAY, &files); + + ret.reserve(files.size()); + for (FILESYSTEM_FIND_DATA& fd : files) + { + const std::string_view name = Path::GetFileTitle(fd.FileName); + if (StringUtil::IsInStringList(ret, name)) + continue; + + ret.emplace_back(name); + } + + std::sort(ret.begin(), ret.end()); + return ret; +} + +bool GPUPresenter::LoadOverlayPreset(Error* error, Image* image) +{ + SmallString path = SmallString::from_format("overlays/{}.yml", m_border_overlay_image_path); + std::optional yaml_data = Host::ReadResourceFileToString(path, true, error); + if (!yaml_data.has_value()) + return false; + + const ryml::Tree yaml = + ryml::parse_in_place(to_csubstr(path), c4::substr(reinterpret_cast(yaml_data->data()), yaml_data->size())); + const ryml::ConstNodeRef root = yaml.rootref(); + if (root.empty()) + { + Error::SetStringView(error, "Configuration is empty."); + return false; + } + + std::string_view image_filename; + GSVector4i display_area = GSVector4i::zero(); + bool display_alpha_blend = false; + if (!GetStringFromObject(root, "image", &image_filename) || + !GetUIntFromObject(root, "displayStartX", &display_area.x) || + !GetUIntFromObject(root, "displayStartY", &display_area.y) || + !GetUIntFromObject(root, "displayEndX", &display_area.z) || + !GetUIntFromObject(root, "displayEndY", &display_area.w) || + !GetUIntFromObject(root, "alphaBlend", &display_alpha_blend)) + { + Error::SetStringView(error, "One or more parameters is missing."); + return false; + } + + path.format("overlays/{}", image_filename); + std::optional> image_data = Host::ReadResourceFile(path, true, error); + if (!image_data.has_value() || !image->LoadFromBuffer(image_filename, image_data.value(), error)) + return false; + + m_border_overlay_display_rect = display_area; + m_border_overlay_alpha_blend = display_alpha_blend; + return true; +} diff --git a/src/core/gpu_presenter.h b/src/core/gpu_presenter.h index ccd84d3af..a51053b9f 100644 --- a/src/core/gpu_presenter.h +++ b/src/core/gpu_presenter.h @@ -3,9 +3,13 @@ #pragma once +#include "types.h" + #include "util/gpu_device.h" #include +#include +#include class Error; class Image; @@ -28,6 +32,9 @@ public: /// Main frame presenter - used both when a game is and is not running. static bool PresentFrame(GPUPresenter* presenter, GPUBackend* backend, bool allow_skip_present, u64 present_time); + /// Returns a list of border overlay presets. + static std::vector EnumerateBorderOverlayPresets(); + ALWAYS_INLINE s32 GetDisplayWidth() const { return m_display_width; } ALWAYS_INLINE s32 GetDisplayHeight() const { return m_display_height; } ALWAYS_INLINE s32 GetDisplayVRAMWidth() const { return m_display_vram_width; } @@ -44,6 +51,8 @@ public: bool UpdateSettings(const GPUSettings& old_settings, Error* error); + bool UpdatePostProcessingSettings(Error* error); + void ClearDisplay(); void ClearDisplayTexture(); void SetDisplayParameters(u16 display_width, u16 display_height, u16 display_origin_left, u16 display_origin_top, @@ -82,12 +91,28 @@ private: bool CompileDisplayPipelines(bool display, bool deinterlace, bool chroma_smoothing, Error* error); - GPUDevice::PresentResult RenderDisplay(GPUTexture* target, const GSVector4i display_rect, const GSVector4i draw_rect, - bool postfx); + GPUDevice::PresentResult RenderDisplay(GPUTexture* target, const GSVector4i overlay_rect, + const GSVector4i display_rect, const GSVector4i draw_rect, bool postfx); + + void DrawDisplay(const GSVector2i target_size, GPUTexture* display_texture, s32 display_texture_view_x, + s32 display_texture_view_y, s32 display_texture_view_width, s32 display_texture_view_height, + const GSVector4i display_rect, bool dst_alpha_blend, DisplayRotation rotation, + WindowInfo::PreRotation prerotation); + GPUDevice::PresentResult ApplyDisplayPostProcess(GPUTexture* target, GPUTexture* input, + const GSVector4i display_rect); + void DrawTextureCopy(const GSVector2i target_size, const GSVector4i draw_rect, GPUTexture* input, + bool dst_alpha_blend, bool linear, WindowInfo::PreRotation prerotation); + void DrawScreenQuad(const GSVector4i rect, const GSVector4 uv_rect, const GSVector2i target_size, + DisplayRotation uv_rotation, WindowInfo::PreRotation prerotation); bool DeinterlaceSetTargetSize(u32 width, u32 height, bool preserve); void DestroyDeinterlaceTextures(); + /// Returns true if the image path or alpha blend option has changed. + bool LoadOverlaySettings(); + bool LoadOverlayTexture(); + bool LoadOverlayPreset(Error* error, Image* image); + s32 m_display_width = 0; s32 m_display_height = 0; @@ -114,6 +139,20 @@ private: s32 m_display_texture_view_height = 0; u32 m_skipped_present_count = 0; + GPUTexture::Format m_present_format = GPUTexture::Format::Unknown; + + std::unique_ptr m_present_copy_pipeline; + + // blended variants of pipelines, used when overlays are enabled + std::unique_ptr m_display_blend_pipeline; + std::unique_ptr m_present_copy_blend_pipeline; + + std::unique_ptr m_border_overlay_texture; + GSVector4i m_border_overlay_display_rect = GSVector4i::zero(); + + // Low-traffic variables down here. + std::string m_border_overlay_image_path; + bool m_border_overlay_alpha_blend = false; }; namespace Host { diff --git a/src/core/gpu_thread.cpp b/src/core/gpu_thread.cpp index 675dd0ebd..399a290c7 100644 --- a/src/core/gpu_thread.cpp +++ b/src/core/gpu_thread.cpp @@ -973,7 +973,8 @@ void GPUThread::UpdateSettingsOnThread(const GPUSettings& old_settings) PostProcessing::UpdateSettings(); Error error; - if (!s_state.gpu_presenter->UpdateSettings(old_settings, &error) || + if (!s_state.gpu_presenter->UpdatePostProcessingSettings(&error) || + !s_state.gpu_presenter->UpdateSettings(old_settings, &error) || !s_state.gpu_backend->UpdateSettings(old_settings, &error)) [[unlikely]] { ReportFatalErrorAndShutdown(fmt::format("Failed to update settings: {}", error.GetDescription())); @@ -1071,6 +1072,14 @@ void GPUThread::UpdateSettings(bool gpu_settings_changed, bool device_settings_c if (s_state.gpu_backend) { PostProcessing::UpdateSettings(); + + Error error; + if (!s_state.gpu_presenter->UpdatePostProcessingSettings(&error)) + { + ReportFatalErrorAndShutdown(fmt::format("Failed to update settings: {}", error.GetDescription())); + return; + } + if (ImGuiManager::UpdateDebugWindowConfig() || (PostProcessing::DisplayChain.IsActive() && !IsSystemPaused())) PresentFrameAndRestoreContext(); } diff --git a/src/core/settings.h b/src/core/settings.h index 6c790d50b..475ee86a4 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -205,6 +205,9 @@ struct GPUSettings bool operator!=(const TextureReplacementSettings& rhs) const; } texture_replacements; + std::string overlay_image_path; + s16 mingus2[4]; + float GetDisplayAspectRatioValue() const; ALWAYS_INLINE bool IsUsingSoftwareRenderer() const { return (gpu_renderer == GPURenderer::Software); } @@ -256,7 +259,7 @@ struct Settings : public GPUSettings { Settings(); - u32 cpu_overclock_numerator = 1; + ALIGN_TO_CACHE_LINE u32 cpu_overclock_numerator = 1; u32 cpu_overclock_denominator = 1; TickCount dma_max_slice_ticks = DEFAULT_DMA_MAX_SLICE_TICKS; diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index f11917df0..9a4e242a0 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -360,6 +360,9 @@ Document + + Document + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 2e0b9024a..8fac7166a 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -295,6 +295,7 @@ + diff --git a/src/duckstation-qt/postprocessingoverlayconfigwidget.ui b/src/duckstation-qt/postprocessingoverlayconfigwidget.ui new file mode 100644 index 000000000..f84fd65b0 --- /dev/null +++ b/src/duckstation-qt/postprocessingoverlayconfigwidget.ui @@ -0,0 +1,190 @@ + + + PostProcessingOverlayConfigWidget + + + + 0 + 0 + 540 + 355 + + + + Form + + + + + + Basic Configuration + + + + + + Selected Preset: + + + + + + + + + + <html><head/><body><p>A border overlay is an image that is drawn around the system display. Border overlays are applied after post-processing. You can choose from the border list below, or manually configure a custom border.</p><p>Additional preset borders can be added to the <span style=" font-weight:700;">resources\overlays</span> folder within in the data directory.</p></body></html> + + + true + + + + + + + + + + Custom Configuration + + + + + + Image Path: + + + + + + + + + + + + Browse... + + + + + + + + + Display Start: + + + + + + + + + X: + + + + + + + pixels + + + 65535 + + + + + + + Y: + + + + + + + pixels + + + 65535 + + + + + + + + + Destination Alpha Blending + + + + + + + + + X: + + + + + + + pixels + + + 65535 + + + + + + + Y: + + + + + + + pixels + + + 65535 + + + + + + + + + Display End: + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 0 + + + + + + + + + diff --git a/src/duckstation-qt/postprocessingsettingswidget.cpp b/src/duckstation-qt/postprocessingsettingswidget.cpp index 960530f96..94bfe3f85 100644 --- a/src/duckstation-qt/postprocessingsettingswidget.cpp +++ b/src/duckstation-qt/postprocessingsettingswidget.cpp @@ -5,13 +5,17 @@ #include "qthost.h" #include "settingwidgetbinder.h" +#include "core/gpu_presenter.h" + #include "util/postprocessing.h" #include "common/error.h" +#include #include #include #include +#include #include #include #include @@ -22,6 +26,7 @@ PostProcessingSettingsWidget::PostProcessingSettingsWidget(SettingsWindow* dialo tr("Display")); addTab(new PostProcessingChainConfigWidget(dialog, this, PostProcessing::Config::INTERNAL_CHAIN_SECTION), tr("Internal")); + addTab(new PostProcessingOverlayConfigWidget(dialog, this), tr("Border Overlay")); setDocumentMode(true); } @@ -477,3 +482,53 @@ void PostProcessingShaderConfigWidget::createUi() row++; m_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), row, 0, 1, 3); } + +PostProcessingOverlayConfigWidget::PostProcessingOverlayConfigWidget(SettingsWindow* dialog, QWidget* parent) + : QWidget(parent), m_dialog(dialog) +{ + SettingsInterface* sif = dialog->getSettingsInterface(); + + m_ui.setupUi(this); + + m_ui.overlayName->addItem(tr("None"), QString()); + m_ui.overlayName->addItem(tr("Custom..."), QStringLiteral("Custom")); + for (const std::string& name : GPUPresenter::EnumerateBorderOverlayPresets()) + { + const QString qname = QString::fromStdString(name); + m_ui.overlayName->addItem(qname, qname); + } + + SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.overlayName, "BorderOverlay", "PresetName"); + SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.imagePath, "BorderOverlay", "ImagePath"); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.displayStartX, "BorderOverlay", "DisplayStartX", 0); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.displayStartY, "BorderOverlay", "DisplayStartY", 0); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.displayEndX, "BorderOverlay", "DisplayEndX", 0); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.displayEndY, "BorderOverlay", "DisplayEndY", 0); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.alphaBlend, "BorderOverlay", "AlphaBlend", false); + + connect(m_ui.overlayName, &QComboBox::currentIndexChanged, this, + &PostProcessingOverlayConfigWidget::onOverlayNameCurrentIndexChanged); + connect(m_ui.imagePathBrowse, &QPushButton::clicked, this, + &PostProcessingOverlayConfigWidget::onImagePathBrowseClicked); + + onOverlayNameCurrentIndexChanged(m_ui.overlayName->currentIndex()); +} + +PostProcessingOverlayConfigWidget::~PostProcessingOverlayConfigWidget() = default; + +void PostProcessingOverlayConfigWidget::onOverlayNameCurrentIndexChanged(int index) +{ + const int custom_idx = m_dialog->isPerGameSettings() ? 2 : 1; + const bool enable_custom = (index == custom_idx); + m_ui.customConfiguration->setEnabled(enable_custom); +} + +void PostProcessingOverlayConfigWidget::onImagePathBrowseClicked() +{ + const QString path = QFileDialog::getOpenFileName(QtUtils::GetRootWidget(this), tr("Select Image"), QString(), + tr("All Cover Image Types (*.jpg *.jpeg *.png *.webp)")); + if (path.isEmpty()) + return; + + m_ui.imagePath->setText(QDir::toNativeSeparators(path)); +} diff --git a/src/duckstation-qt/postprocessingsettingswidget.h b/src/duckstation-qt/postprocessingsettingswidget.h index f648ca63a..30c6d8d3c 100644 --- a/src/duckstation-qt/postprocessingsettingswidget.h +++ b/src/duckstation-qt/postprocessingsettingswidget.h @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once #include "ui_postprocessingchainconfigwidget.h" +#include "ui_postprocessingoverlayconfigwidget.h" #include "util/postprocessing.h" @@ -13,7 +14,7 @@ class SettingsWindow; class PostProcessingShaderConfigWidget; -class PostProcessingSettingsWidget : public QTabWidget +class PostProcessingSettingsWidget final : public QTabWidget { Q_OBJECT @@ -61,7 +62,7 @@ private: PostProcessingShaderConfigWidget* m_shader_config = nullptr; }; -class PostProcessingShaderConfigWidget : public QWidget +class PostProcessingShaderConfigWidget final : public QWidget { Q_OBJECT @@ -73,7 +74,7 @@ public: private Q_SLOTS: void onResetDefaultsClicked(); -protected: +private: void createUi(); void updateConfigForOption(const PostProcessing::ShaderOption& option); @@ -86,3 +87,20 @@ protected: u32 m_stage_index; std::vector m_options; }; + +class PostProcessingOverlayConfigWidget final : public QWidget +{ + Q_OBJECT + +public: + PostProcessingOverlayConfigWidget(SettingsWindow* dialog, QWidget* parent); + ~PostProcessingOverlayConfigWidget(); + +private Q_SLOTS: + void onOverlayNameCurrentIndexChanged(int index); + void onImagePathBrowseClicked(); + +private: + Ui::PostProcessingOverlayConfigWidget m_ui; + SettingsWindow* m_dialog; +}; diff --git a/src/util/gpu_device.cpp b/src/util/gpu_device.cpp index b3e50608c..2b4fcea85 100644 --- a/src/util/gpu_device.cpp +++ b/src/util/gpu_device.cpp @@ -233,10 +233,11 @@ GPUSwapChain::GPUSwapChain(const WindowInfo& wi, GPUVSyncMode vsync_mode, bool a GPUSwapChain::~GPUSwapChain() = default; -GSVector4i GPUSwapChain::PreRotateClipRect(const GSVector4i& v) +GSVector4i GPUSwapChain::PreRotateClipRect(WindowInfo::PreRotation prerotation, const GSVector2i surface_size, + const GSVector4i& v) { GSVector4i new_clip; - switch (m_window_info.surface_prerotation) + switch (prerotation) { case WindowInfo::PreRotation::Identity: new_clip = v; @@ -245,7 +246,7 @@ GSVector4i GPUSwapChain::PreRotateClipRect(const GSVector4i& v) case WindowInfo::PreRotation::Rotate90Clockwise: { const s32 height = (v.w - v.y); - const s32 y = m_window_info.surface_height - v.y - height; + const s32 y = surface_size.y - v.y - height; new_clip = GSVector4i(y, v.x, y + height, v.z); } break; @@ -254,8 +255,8 @@ GSVector4i GPUSwapChain::PreRotateClipRect(const GSVector4i& v) { const s32 width = (v.z - v.x); const s32 height = (v.w - v.y); - const s32 x = m_window_info.surface_width - v.x - width; - const s32 y = m_window_info.surface_height - v.y - height; + const s32 x = surface_size.x - v.x - width; + const s32 y = surface_size.y - v.y - height; new_clip = GSVector4i(x, y, x + width, y + height); } break; @@ -263,7 +264,7 @@ GSVector4i GPUSwapChain::PreRotateClipRect(const GSVector4i& v) case WindowInfo::PreRotation::Rotate270Clockwise: { const s32 width = (v.z - v.x); - const s32 x = m_window_info.surface_width - v.x - width; + const s32 x = surface_size.x - v.x - width; new_clip = GSVector4i(v.y, x, v.w, x + width); } break; @@ -736,12 +737,11 @@ void GPUDevice::RenderImGui(GPUSwapChain* swap_chain) SetPipeline(m_imgui_pipeline.get()); SetViewport(0, 0, swap_chain->GetPostRotatedWidth(), post_rotated_height); + const bool prerotated = (swap_chain->GetPreRotation() != WindowInfo::PreRotation::Identity); GSMatrix4x4 mproj = GSMatrix4x4::OffCenterOrthographicProjection( 0.0f, 0.0f, static_cast(swap_chain->GetWidth()), static_cast(swap_chain->GetHeight()), 0.0f, 1.0f); - if (swap_chain->GetPreRotation() != WindowInfo::PreRotation::Identity) - { + if (prerotated) mproj = GSMatrix4x4::RotationZ(WindowInfo::GetZRotationForPreRotation(swap_chain->GetPreRotation())) * mproj; - } PushUniformBuffer(&mproj, sizeof(mproj)); // Render command lists @@ -766,7 +766,9 @@ void GPUDevice::RenderImGui(GPUSwapChain* swap_chain) } GSVector4i clip = GSVector4i(GSVector4::load(&pcmd->ClipRect.x)); - clip = swap_chain->PreRotateClipRect(clip); + + if (prerotated) + clip = GPUSwapChain::PreRotateClipRect(swap_chain->GetPreRotation(), swap_chain->GetSizeVec(), clip); if (flip) clip = FlipToLowerLeft(clip, post_rotated_height); diff --git a/src/util/gpu_device.h b/src/util/gpu_device.h index c832c8e76..eda9dd9d2 100644 --- a/src/util/gpu_device.h +++ b/src/util/gpu_device.h @@ -516,6 +516,10 @@ public: { return GSVector2i(m_window_info.surface_width, m_window_info.surface_height); } + ALWAYS_INLINE GSVector2i GetPostRotatedSizeVec() const + { + return GSVector2i(m_window_info.GetPostRotatedWidth(), m_window_info.GetPostRotatedHeight()); + } ALWAYS_INLINE GPUVSyncMode GetVSyncMode() const { return m_vsync_mode; } ALWAYS_INLINE bool IsVSyncModeBlocking() const { return (m_vsync_mode == GPUVSyncMode::FIFO); } @@ -524,11 +528,12 @@ public: virtual bool ResizeBuffers(u32 new_width, u32 new_height, float new_scale, Error* error) = 0; virtual bool SetVSyncMode(GPUVSyncMode mode, bool allow_present_throttle, Error* error) = 0; - GSVector4i PreRotateClipRect(const GSVector4i& v); - bool ShouldSkipPresentingFrame(); void ThrottlePresentation(); + static GSVector4i PreRotateClipRect(WindowInfo::PreRotation prerotation, const GSVector2i surface_size, + const GSVector4i& v); + protected: // TODO: Merge WindowInfo into this struct... WindowInfo m_window_info; diff --git a/src/util/imgui_glyph_ranges.inl b/src/util/imgui_glyph_ranges.inl index 1be15bc4d..0410ceebc 100644 --- a/src/util/imgui_glyph_ranges.inl +++ b/src/util/imgui_glyph_ranges.inl @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 -static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe070,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf026,0xf028,0xf028,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03e,0xf04a,0xf04c,0xf050,0xf050,0xf056,0xf056,0xf05e,0xf05e,0xf062,0xf063,0xf065,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf083,0xf085,0xf091,0xf091,0xf0ac,0xf0ae,0xf0b2,0xf0b2,0xf0c3,0xf0c3,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e0,0xf0e0,0xf0e2,0xf0e2,0xf0e7,0xf0e8,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf11b,0xf11c,0xf140,0xf140,0xf144,0xf144,0xf146,0xf146,0xf14a,0xf14a,0xf15b,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1c0,0xf1c0,0xf1c5,0xf1c5,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fb,0xf1fc,0xf201,0xf201,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2c1,0xf2c1,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f1,0xf2f2,0xf302,0xf302,0xf31e,0xf31e,0xf338,0xf338,0xf35d,0xf35d,0xf360,0xf360,0xf362,0xf362,0xf3fd,0xf3fd,0xf410,0xf410,0xf422,0xf422,0xf424,0xf424,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf51f,0xf51f,0xf538,0xf538,0xf53f,0xf53f,0xf545,0xf545,0xf547,0xf548,0xf54c,0xf54c,0xf55b,0xf55b,0xf55d,0xf55d,0xf565,0xf565,0xf56e,0xf570,0xf575,0xf575,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5ae,0xf5ae,0xf5c7,0xf5c7,0xf5cb,0xf5cb,0xf5e7,0xf5e7,0xf5ee,0xf5ee,0xf61f,0xf61f,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf70e,0xf70e,0xf78c,0xf78c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7a4,0xf7a5,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf87d,0xf87d,0xf8cc,0xf8cc,0x0,0x0 }; +static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe070,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf026,0xf028,0xf028,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03e,0xf04a,0xf04c,0xf050,0xf050,0xf056,0xf056,0xf05e,0xf05e,0xf062,0xf063,0xf065,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf083,0xf085,0xf091,0xf091,0xf0ac,0xf0ae,0xf0b2,0xf0b2,0xf0c3,0xf0c3,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e0,0xf0e0,0xf0e2,0xf0e2,0xf0e7,0xf0e8,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf11b,0xf11c,0xf140,0xf140,0xf144,0xf144,0xf146,0xf146,0xf14a,0xf14a,0xf15b,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1c0,0xf1c0,0xf1c5,0xf1c5,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fb,0xf1fc,0xf201,0xf201,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2c1,0xf2c1,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f1,0xf2f2,0xf302,0xf302,0xf31e,0xf31e,0xf338,0xf338,0xf35d,0xf35d,0xf360,0xf360,0xf362,0xf362,0xf3fd,0xf3fd,0xf410,0xf410,0xf422,0xf422,0xf424,0xf424,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf517,0xf517,0xf51f,0xf51f,0xf538,0xf538,0xf53f,0xf53f,0xf545,0xf545,0xf547,0xf548,0xf54c,0xf54c,0xf55b,0xf55b,0xf55d,0xf55d,0xf565,0xf565,0xf56e,0xf570,0xf575,0xf575,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5ae,0xf5ae,0xf5c7,0xf5c7,0xf5cb,0xf5cb,0xf5e7,0xf5e7,0xf5ee,0xf5ee,0xf61f,0xf61f,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf70e,0xf70e,0xf78c,0xf78c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7a4,0xf7a5,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf853,0xf853,0xf87d,0xf87d,0xf8cc,0xf8cc,0x0,0x0 }; static constexpr ImWchar PF_ICON_RANGE[] = { 0x2196,0x2199,0x219e,0x21a3,0x21b0,0x21b3,0x21ba,0x21c3,0x21c7,0x21ca,0x21d0,0x21d4,0x21e0,0x21e3,0x21e6,0x21e8,0x21ed,0x21ee,0x21f7,0x21f8,0x21fa,0x21fb,0x221a,0x221b,0x227a,0x227f,0x2284,0x2284,0x22bf,0x22c8,0x2349,0x2349,0x235e,0x235e,0x2360,0x2361,0x2364,0x2366,0x23b2,0x23b4,0x23cc,0x23cc,0x23ce,0x23ce,0x23f4,0x23f7,0x2427,0x243a,0x243c,0x243e,0x2446,0x2446,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x2753,0x2753,0x278a,0x278e,0x27fc,0x27fc,0xe000,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x0,0x0 }; diff --git a/src/util/postprocessing.cpp b/src/util/postprocessing.cpp index 24e927fbe..b29bf7893 100644 --- a/src/util/postprocessing.cpp +++ b/src/util/postprocessing.cpp @@ -577,45 +577,6 @@ bool PostProcessing::Chain::CheckTargets(GPUTexture::Format target_format, u32 t Error error; - if (!IsInternalChain() && (!m_rotated_copy_pipeline || m_target_format != target_format)) - { - const RenderAPI rapi = g_gpu_device->GetRenderAPI(); - const ShaderGen shadergen(rapi, ShaderGen::GetShaderLanguageForAPI(rapi), false, false); - const std::unique_ptr vso = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), - shadergen.GenerateRotateVertexShader(), &error); - const std::unique_ptr fso = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), - shadergen.GenerateRotateFragmentShader(), &error); - if (!vso || !fso) - { - ERROR_LOG("Failed to compile post-processing rotate shaders: {}", error.GetDescription()); - return false; - } - GL_OBJECT_NAME(vso, "Post-processing rotate blit VS"); - GL_OBJECT_NAME(vso, "Post-processing rotate blit FS"); - - const GPUPipeline::GraphicsConfig config = {.layout = GPUPipeline::Layout::SingleTextureAndPushConstants, - .primitive = GPUPipeline::Primitive::Triangles, - .input_layout = {}, - .rasterization = GPUPipeline::RasterizationState::GetNoCullState(), - .depth = GPUPipeline::DepthState::GetNoTestsState(), - .blend = GPUPipeline::BlendState::GetNoBlendingState(), - .vertex_shader = vso.get(), - .geometry_shader = nullptr, - .fragment_shader = fso.get(), - .color_formats = {target_format}, - .depth_format = GPUTexture::Format::Unknown, - .samples = 1, - .per_sample_shading = false, - .render_pass_flags = GPUPipeline::NoRenderPassFlags}; - m_rotated_copy_pipeline = g_gpu_device->CreatePipeline(config, &error); - if (!m_rotated_copy_pipeline) - { - ERROR_LOG("Failed to compile post-processing rotate pipeline: {}", error.GetDescription()); - return false; - } - GL_OBJECT_NAME(m_rotated_copy_pipeline, "Post-processing rotate pipeline"); - } - // In case any allocs fail. DestroyTextures(); @@ -677,11 +638,6 @@ void PostProcessing::Chain::DestroyTextures() g_gpu_device->RecycleTexture(std::move(m_input_texture)); } -void PostProcessing::Chain::DestroyPipelines() -{ - m_rotated_copy_pipeline.reset(); -} - GPUDevice::PresentResult PostProcessing::Chain::Apply(GPUTexture* input_color, GPUTexture* input_depth, GPUTexture* final_target, GSVector4i final_rect, s32 orig_width, s32 orig_height, s32 native_width, s32 native_height) @@ -693,25 +649,14 @@ GPUDevice::PresentResult PostProcessing::Chain::Apply(GPUTexture* input_color, G if (input_depth) input_depth->MakeReadyForSampling(); - GPUTexture* draw_final_target = final_target; - const WindowInfo::PreRotation prerotation = - final_target ? WindowInfo::PreRotation::Identity : g_gpu_device->GetMainSwapChain()->GetPreRotation(); - if (prerotation != WindowInfo::PreRotation::Identity) - { - // We have prerotation and post processing. This is messy, since we need to run the shader on the "real" size, - // then copy it across to the rotated image. We can use the input or output texture from the chain, whichever - // was not the last that was drawn to. - draw_final_target = GetTextureUnusedAtEndOfChain(); - } - const float time = static_cast(Timer::ConvertValueToSeconds(Timer::GetCurrentValue() - s_start_time)); for (const std::unique_ptr& stage : m_stages) { const bool is_final = (stage.get() == m_stages.back().get()); if (const GPUDevice::PresentResult pres = - stage->Apply(input_color, input_depth, is_final ? draw_final_target : output, final_rect, orig_width, - orig_height, native_width, native_height, m_target_width, m_target_height, time); + stage->Apply(input_color, input_depth, is_final ? final_target : output, final_rect, orig_width, orig_height, + native_width, native_height, m_target_width, m_target_height, time); pres != GPUDevice::PresentResult::OK) { return pres; @@ -725,30 +670,6 @@ GPUDevice::PresentResult PostProcessing::Chain::Apply(GPUTexture* input_color, G } } - if (prerotation != WindowInfo::PreRotation::Identity) - { - draw_final_target->MakeReadyForSampling(); - - // Rotate and blit to final swap chain. - GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); - if (const GPUDevice::PresentResult pres = g_gpu_device->BeginPresent(swap_chain); - pres != GPUDevice::PresentResult::OK) - { - return pres; - } - - GL_PUSH_FMT("Apply swap chain pre-rotation"); - - const GSMatrix2x2 rotmat = GSMatrix2x2::Rotation(WindowInfo::GetZRotationForPreRotation(prerotation)); - g_gpu_device->SetPipeline(m_rotated_copy_pipeline.get()); - g_gpu_device->PushUniformBuffer(&rotmat, sizeof(rotmat)); - g_gpu_device->SetTextureSampler(0, draw_final_target, g_gpu_device->GetNearestSampler()); - g_gpu_device->SetViewportAndScissor(0, 0, swap_chain->GetPostRotatedWidth(), swap_chain->GetPostRotatedHeight()); - g_gpu_device->Draw(3, 0); - - GL_POP(); - } - return GPUDevice::PresentResult::OK; } @@ -771,7 +692,6 @@ void PostProcessing::Shutdown() s_samplers.clear(); ForAllChains([](Chain& chain) { chain.ClearStages(); - chain.DestroyPipelines(); chain.DestroyTextures(); }); } @@ -788,7 +708,6 @@ bool PostProcessing::ReloadShaders() ForAllChains([](Chain& chain) { chain.ClearStages(); - chain.DestroyPipelines(); chain.DestroyTextures(); chain.LoadStages(); }); diff --git a/src/util/postprocessing.h b/src/util/postprocessing.h index 873308bec..fe2f5f1c9 100644 --- a/src/util/postprocessing.h +++ b/src/util/postprocessing.h @@ -107,7 +107,7 @@ void UnsetStageOption(SettingsInterface& si, const char* section, u32 index, con void ClearStages(SettingsInterface& si, const char* section); } // namespace Config -class Chain +class Chain final { public: Chain(const char* section); @@ -129,7 +129,6 @@ public: void LoadStages(); void ClearStages(); void DestroyTextures(); - void DestroyPipelines(); /// Temporarily toggles post-processing on/off. void Toggle(); @@ -156,7 +155,6 @@ private: std::vector> m_stages; std::unique_ptr m_input_texture; std::unique_ptr m_output_texture; - std::unique_ptr m_rotated_copy_pipeline; }; // [display_name, filename] diff --git a/src/util/shadergen.cpp b/src/util/shadergen.cpp index c77826c0d..97af04d6b 100644 --- a/src/util/shadergen.cpp +++ b/src/util/shadergen.cpp @@ -793,40 +793,6 @@ void ShaderGen::DeclareFragmentEntryPoint( } } -std::string ShaderGen::GenerateRotateVertexShader() const -{ - std::stringstream ss; - WriteHeader(ss); - DeclareUniformBuffer(ss, { "float2 u_rotation_matrix0", "float2 u_rotation_matrix1" }, true); - DeclareVertexEntryPoint(ss, {}, 0, 1, {}, true); - ss << "{\n"; - ss << " v_tex0 = float2(float((v_id << 1) & 2u), float(v_id & 2u));\n"; - ss << " v_pos = float4(v_tex0 * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f);\n"; - ss << " v_pos.xy = float2(dot(u_rotation_matrix0, v_pos.xy), dot(u_rotation_matrix1, v_pos.xy));\n"; - ss << " #if API_OPENGL || API_OPENGL_ES || API_VULKAN\n"; - ss << " v_pos.y = -v_pos.y;\n"; - ss << " #endif\n"; - ss << "}\n"; - - return ss.str(); -} - -std::string ShaderGen::GenerateRotateFragmentShader() const -{ - std::stringstream ss; - WriteHeader(ss); - DeclareTexture(ss, "samp0", 0); - DeclareFragmentEntryPoint(ss, 0, 1); - - ss << R"( -{ - o_col0 = SAMPLE_TEXTURE(samp0, v_tex0); -} -)"; - - return ss.str(); -} - std::string ShaderGen::GenerateScreenQuadVertexShader(float z /* = 0.0f */) const { std::stringstream ss; @@ -879,20 +845,33 @@ std::string ShaderGen::GenerateFillFragmentShader() const return ss.str(); } -std::string ShaderGen::GenerateCopyFragmentShader() const +std::string ShaderGen::GenerateCopyFragmentShader(bool offset) const { std::stringstream ss; WriteHeader(ss); - DeclareUniformBuffer(ss, {"float4 u_src_rect"}, true); + if (offset) + DeclareUniformBuffer(ss, {"float4 u_src_rect"}, true); + DeclareTexture(ss, "samp0", 0); DeclareFragmentEntryPoint(ss, 0, 1); - ss << R"( + if (offset) + { + ss << R"( { float2 coords = u_src_rect.xy + v_tex0 * u_src_rect.zw; o_col0 = SAMPLE_TEXTURE(samp0, coords); } )"; + } + else + { + ss << R"( +{ + o_col0 = SAMPLE_TEXTURE(samp0, v_tex0); +} +)"; + } return ss.str(); } diff --git a/src/util/shadergen.h b/src/util/shadergen.h index af730b5b1..09bf49d9d 100644 --- a/src/util/shadergen.h +++ b/src/util/shadergen.h @@ -27,13 +27,10 @@ public: ALWAYS_INLINE bool IsVulkan() const { return (m_render_api == RenderAPI::Vulkan); } ALWAYS_INLINE bool IsMetal() const { return (m_render_api == RenderAPI::Metal); } - std::string GenerateRotateVertexShader() const; - std::string GenerateRotateFragmentShader() const; - std::string GenerateScreenQuadVertexShader(float z = 0.0f) const; std::string GenerateUVQuadVertexShader() const; std::string GenerateFillFragmentShader() const; - std::string GenerateCopyFragmentShader() const; + std::string GenerateCopyFragmentShader(bool offset = true) const; std::string GenerateImGuiVertexShader() const; std::string GenerateImGuiFragmentShader() const;