// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "gpu_presenter.h" #include "fullscreen_ui.h" #include "gpu.h" #include "gpu_backend.h" #include "gpu_shadergen.h" #include "gpu_thread.h" #include "gpu_thread_commands.h" #include "host.h" #include "imgui_overlays.h" #include "performance_counters.h" #include "save_state_version.h" #include "settings.h" #include "system.h" #include "util/gpu_device.h" #include "util/image.h" #include "util/imgui_fullscreen.h" #include "util/imgui_manager.h" #include "util/media_capture.h" #include "util/postprocessing.h" #include "util/state_wrapper.h" #include "common/align.h" #include "common/assert.h" #include "common/error.h" #include "common/file_system.h" #include "common/gsvector_formatter.h" #include "common/log.h" #include "common/path.h" #include "common/ryml_helpers.h" #include "common/settings_interface.h" #include "common/small_string.h" #include "common/string_util.h" #include "common/threading.h" #include "common/timer.h" #include LOG_CHANNEL(GPU); GPUPresenter::GPUPresenter() = default; GPUPresenter::~GPUPresenter() { DestroyDeinterlaceTextures(); g_gpu_device->RecycleTexture(std::move(m_chroma_smoothing_texture)); } 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; LoadPostProcessingSettings(false); return true; } bool GPUPresenter::UpdateSettings(const GPUSettings& old_settings, Error* error) { if (g_gpu_settings.display_scaling != old_settings.display_scaling || g_gpu_settings.display_deinterlacing_mode != old_settings.display_deinterlacing_mode || g_gpu_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing) { // Toss buffers on mode change. if (g_gpu_settings.display_deinterlacing_mode != old_settings.display_deinterlacing_mode) DestroyDeinterlaceTextures(); if (!CompileDisplayPipelines( g_gpu_settings.display_scaling != old_settings.display_scaling, g_gpu_settings.display_deinterlacing_mode != old_settings.display_deinterlacing_mode, g_gpu_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing, error)) { Error::AddPrefix(error, "Failed to compile display pipeline on settings change:\n"); return false; } } return true; } bool GPUPresenter::IsDisplayPostProcessingActive() const { return (m_display_postfx && m_display_postfx->IsActive()); } 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, g_gpu_device->GetFeatures().framebuffer_fetch); GPUPipeline::GraphicsConfig plconfig; plconfig.primitive = GPUPipeline::Primitive::Triangles; plconfig.rasterization = GPUPipeline::RasterizationState::GetNoCullState(); plconfig.depth = GPUPipeline::DepthState::GetNoTestsState(); plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState(); plconfig.geometry_shader = nullptr; plconfig.depth_format = GPUTexture::Format::Unknown; plconfig.samples = 1; plconfig.per_sample_shading = false; plconfig.render_pass_flags = GPUPipeline::NoRenderPassFlags; if (display) { GPUBackend::SetScreenQuadInputLayout(plconfig); plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; 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 fs; switch (g_gpu_settings.display_scaling) { case DisplayScalingMode::BilinearSharp: fs = shadergen.GenerateDisplaySharpBilinearFragmentShader(); break; case DisplayScalingMode::BilinearSmooth: case DisplayScalingMode::BilinearInteger: fs = shadergen.GenerateDisplayFragmentShader(true, false); break; case DisplayScalingMode::Nearest: case DisplayScalingMode::NearestInteger: default: fs = shadergen.GenerateDisplayFragmentShader(false, true); break; } std::unique_ptr fso = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), fs, error); if (!fso) return false; 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) { std::unique_ptr vso = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), shadergen.GenerateScreenQuadVertexShader(), error); if (!vso) return false; GL_OBJECT_NAME(vso, "Deinterlace Vertex Shader"); plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; plconfig.vertex_shader = vso.get(); plconfig.SetTargetFormats(GPUTexture::Format::RGBA8); switch (g_gpu_settings.display_deinterlacing_mode) { case DisplayDeinterlacingMode::Disabled: case DisplayDeinterlacingMode::Progressive: break; case DisplayDeinterlacingMode::Weave: { std::unique_ptr fso = g_gpu_device->CreateShader( GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateDeinterlaceWeaveFragmentShader(), error); if (!fso) return false; GL_OBJECT_NAME(fso, "Weave Deinterlace Fragment Shader"); plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; plconfig.vertex_shader = vso.get(); plconfig.fragment_shader = fso.get(); if (!(m_deinterlace_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) return false; GL_OBJECT_NAME(m_deinterlace_pipeline, "Weave Deinterlace Pipeline"); } break; case DisplayDeinterlacingMode::Blend: { std::unique_ptr fso = g_gpu_device->CreateShader( GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateDeinterlaceBlendFragmentShader(), error); if (!fso) return false; GL_OBJECT_NAME(fso, "Blend Deinterlace Fragment Shader"); plconfig.layout = GPUPipeline::Layout::MultiTextureAndPushConstants; plconfig.vertex_shader = vso.get(); plconfig.fragment_shader = fso.get(); if (!(m_deinterlace_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) return false; GL_OBJECT_NAME(m_deinterlace_pipeline, "Blend Deinterlace Pipeline"); } break; case DisplayDeinterlacingMode::Adaptive: { std::unique_ptr fso = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateFastMADReconstructFragmentShader(), error); if (!fso) return false; GL_OBJECT_NAME(fso, "FastMAD Reconstruct Fragment Shader"); plconfig.layout = GPUPipeline::Layout::MultiTextureAndPushConstants; plconfig.fragment_shader = fso.get(); if (!(m_deinterlace_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) return false; GL_OBJECT_NAME(m_deinterlace_pipeline, "FastMAD Reconstruct Pipeline"); } break; default: UnreachableCode(); } } if (chroma_smoothing) { m_chroma_smoothing_pipeline.reset(); g_gpu_device->RecycleTexture(std::move(m_chroma_smoothing_texture)); if (g_gpu_settings.display_24bit_chroma_smoothing) { plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; plconfig.SetTargetFormats(GPUTexture::Format::RGBA8); std::unique_ptr vso = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), shadergen.GenerateScreenQuadVertexShader(), error); std::unique_ptr fso = g_gpu_device->CreateShader( GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateChromaSmoothingFragmentShader(), error); if (!vso || !fso) return false; GL_OBJECT_NAME(vso, "Chroma Smoothing Vertex Shader"); GL_OBJECT_NAME(fso, "Chroma Smoothing Fragment Shader"); plconfig.vertex_shader = vso.get(); plconfig.fragment_shader = fso.get(); if (!(m_chroma_smoothing_pipeline = g_gpu_device->CreatePipeline(plconfig, error))) return false; GL_OBJECT_NAME(m_chroma_smoothing_pipeline, "Chroma Smoothing Pipeline"); } } return true; } void GPUPresenter::ClearDisplay() { ClearDisplayTexture(); // Just recycle the textures, it'll get re-fetched. DestroyDeinterlaceTextures(); } void GPUPresenter::ClearDisplayTexture() { m_display_texture = nullptr; m_display_texture_view_x = 0; m_display_texture_view_y = 0; m_display_texture_view_width = 0; m_display_texture_view_height = 0; } void GPUPresenter::SetDisplayParameters(u16 display_width, u16 display_height, u16 display_origin_left, u16 display_origin_top, u16 display_vram_width, u16 display_vram_height, float display_pixel_aspect_ratio) { m_display_width = display_width; m_display_height = display_height; m_display_origin_left = display_origin_left; m_display_origin_top = display_origin_top; m_display_vram_width = display_vram_width; m_display_vram_height = display_vram_height; m_display_pixel_aspect_ratio = display_pixel_aspect_ratio; } void GPUPresenter::SetDisplayTexture(GPUTexture* texture, s32 view_x, s32 view_y, s32 view_width, s32 view_height) { DebugAssert(texture); if (g_gpu_settings.display_auto_resize_window && (view_width != m_display_texture_view_width || view_height != m_display_texture_view_height)) { Host::RunOnCPUThread([]() { System::RequestDisplaySize(); }); } m_display_texture = texture; m_display_texture_view_x = view_x; m_display_texture_view_y = view_y; m_display_texture_view_width = view_width; m_display_texture_view_height = view_height; } GPUDevice::PresentResult GPUPresenter::PresentDisplay() { 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(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 overlay_rect, const GSVector4i display_rect, const GSVector4i draw_rect, bool postfx) { GL_SCOPE_FMT("RenderDisplay: {}", draw_rect); if (m_display_texture) m_display_texture->MakeReadyForSampling(); // 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 && m_display_postfx && m_display_postfx->IsActive() && m_display_postfx && m_display_postfx->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 { return g_gpu_device->BeginPresent(swap_chain); } }; // If postfx is enabled, we need to draw to an intermediate buffer first. if (really_postfx) { // Remove draw offset if we're using an overlay. const GSVector4i real_draw_rect = have_overlay ? draw_rect.sub32(display_rect.xyxy()) : draw_rect; // Display is always drawn to the postfx input. GPUTexture* const postfx_input = m_display_postfx->GetInputTexture(); g_gpu_device->ClearRenderTarget(postfx_input, GPUDevice::DEFAULT_CLEAR_COLOR); g_gpu_device->SetRenderTarget(postfx_input); if (m_display_texture) { DrawDisplay(postfx_size, real_draw_rect, false, g_gpu_settings.display_rotation, WindowInfo::PreRotation::Identity); } postfx_input->MakeReadyForSampling(); // Apply postprocessing to an intermediate texture if we're prerotating or have an overlay. if (have_prerotation || have_overlay) { GPUTexture* const postfx_output = m_display_postfx->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 (m_display_texture) { DrawDisplay(target_size, display_rect, m_border_overlay_alpha_blend, g_gpu_settings.display_rotation, prerotation); } return GPUDevice::PresentResult::OK; } } void GPUPresenter::DrawDisplay(const GSVector2i target_size, 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, m_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(m_display_texture->GetSizeVec()); const GSVector4 display_texture_size4 = GSVector4::xyxy(display_texture_size); const GSVector4 uv_rect = GSVector4(GSVector4i(m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_x + m_display_texture_view_width, m_display_texture_view_y + m_display_texture_view_height)) / display_texture_size4; GSVector4::store( uniforms.clamp_rect, GSVector4(static_cast(m_display_texture_view_x) + 0.5f, static_cast(m_display_texture_view_y) + 0.5f, static_cast(m_display_texture_view_x + m_display_texture_view_width) - 0.5f, static_cast(m_display_texture_view_y + m_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 m_display_postfx->Apply(input, 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(); if (!target) [[unlikely]] { WARNING_LOG("Failed to get video capture render texture."); Host::RunOnCPUThread(&System::StopMediaCapture); return; } const bool apply_aspect_ratio = (g_gpu_settings.display_screenshot_mode != DisplayScreenshotMode::UncorrectedInternalResolution); const bool postfx = (g_gpu_settings.display_screenshot_mode != DisplayScreenshotMode::InternalResolution); GSVector4i display_rect, draw_rect; CalculateDrawRect(target->GetWidth(), target->GetHeight(), !g_gpu_settings.gpu_show_vram, apply_aspect_ratio, &display_rect, &draw_rect); // Not cleared by RenderDisplay(). g_gpu_device->ClearRenderTarget(target, GPUDevice::DEFAULT_CLEAR_COLOR); 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."); Host::RunOnCPUThread(&System::StopMediaCapture); return; } } void GPUPresenter::DestroyDeinterlaceTextures() { for (std::unique_ptr& tex : m_deinterlace_buffers) g_gpu_device->RecycleTexture(std::move(tex)); g_gpu_device->RecycleTexture(std::move(m_deinterlace_texture)); m_current_deinterlace_buffer = 0; } bool GPUPresenter::Deinterlace(u32 field) { GPUTexture* src = m_display_texture; const u32 x = m_display_texture_view_x; const u32 y = m_display_texture_view_y; const u32 width = m_display_texture_view_width; const u32 height = m_display_texture_view_height; const auto copy_to_field_buffer = [&](u32 buffer) { if (!g_gpu_device->ResizeTexture(&m_deinterlace_buffers[buffer], width, height, GPUTexture::Type::Texture, src->GetFormat(), GPUTexture::Flags::None, false)) [[unlikely]] { return false; } GL_OBJECT_NAME_FMT(m_deinterlace_buffers[buffer], "Blend Deinterlace Buffer {}", buffer); GL_INS_FMT("Copy {}x{} from {},{} to field buffer {}", width, height, x, y, buffer); g_gpu_device->CopyTextureRegion(m_deinterlace_buffers[buffer].get(), 0, 0, 0, 0, m_display_texture, x, y, 0, 0, width, height); return true; }; src->MakeReadyForSampling(); switch (g_gpu_settings.display_deinterlacing_mode) { case DisplayDeinterlacingMode::Disabled: { GL_INS("Deinterlacing disabled, displaying field texture"); return true; } case DisplayDeinterlacingMode::Weave: { GL_SCOPE_FMT("DeinterlaceWeave({{{},{}}}, {}x{}, field={})", x, y, width, height, field); const u32 full_height = height * 2; if (!DeinterlaceSetTargetSize(width, full_height, true)) [[unlikely]] { ClearDisplayTexture(); return false; } src->MakeReadyForSampling(); g_gpu_device->SetRenderTarget(m_deinterlace_texture.get()); g_gpu_device->SetPipeline(m_deinterlace_pipeline.get()); g_gpu_device->SetTextureSampler(0, src, g_gpu_device->GetNearestSampler()); const u32 uniforms[4] = {x, y, field, 0}; g_gpu_device->PushUniformBuffer(uniforms, sizeof(uniforms)); g_gpu_device->SetViewportAndScissor(0, 0, width, full_height); g_gpu_device->Draw(3, 0); m_deinterlace_texture->MakeReadyForSampling(); SetDisplayTexture(m_deinterlace_texture.get(), 0, 0, width, full_height); return true; } case DisplayDeinterlacingMode::Blend: { constexpr u32 NUM_BLEND_BUFFERS = 2; GL_SCOPE_FMT("DeinterlaceBlend({{{},{}}}, {}x{}, field={})", x, y, width, height, field); const u32 this_buffer = m_current_deinterlace_buffer; m_current_deinterlace_buffer = (m_current_deinterlace_buffer + 1u) % NUM_BLEND_BUFFERS; GL_INS_FMT("Current buffer: {}", this_buffer); if (!DeinterlaceSetTargetSize(width, height, false) || !copy_to_field_buffer(this_buffer)) [[unlikely]] { ClearDisplayTexture(); return false; } copy_to_field_buffer(this_buffer); // TODO: could be implemented with alpha blending instead.. g_gpu_device->InvalidateRenderTarget(m_deinterlace_texture.get()); g_gpu_device->SetRenderTarget(m_deinterlace_texture.get()); g_gpu_device->SetPipeline(m_deinterlace_pipeline.get()); g_gpu_device->SetTextureSampler(0, m_deinterlace_buffers[this_buffer].get(), g_gpu_device->GetNearestSampler()); g_gpu_device->SetTextureSampler(1, m_deinterlace_buffers[(this_buffer - 1) % NUM_BLEND_BUFFERS].get(), g_gpu_device->GetNearestSampler()); g_gpu_device->SetViewportAndScissor(0, 0, width, height); g_gpu_device->Draw(3, 0); m_deinterlace_texture->MakeReadyForSampling(); SetDisplayTexture(m_deinterlace_texture.get(), 0, 0, width, height); return true; } case DisplayDeinterlacingMode::Adaptive: { GL_SCOPE_FMT("DeinterlaceAdaptive({{{},{}}}, {}x{}, field={})", x, y, width, height, field); const u32 this_buffer = m_current_deinterlace_buffer; const u32 full_height = height * 2; m_current_deinterlace_buffer = (m_current_deinterlace_buffer + 1u) % DEINTERLACE_BUFFER_COUNT; GL_INS_FMT("Current buffer: {}", this_buffer); if (!DeinterlaceSetTargetSize(width, full_height, false) || !copy_to_field_buffer(this_buffer)) [[unlikely]] { ClearDisplayTexture(); return false; } g_gpu_device->SetRenderTarget(m_deinterlace_texture.get()); g_gpu_device->SetPipeline(m_deinterlace_pipeline.get()); g_gpu_device->SetTextureSampler(0, m_deinterlace_buffers[this_buffer].get(), g_gpu_device->GetNearestSampler()); g_gpu_device->SetTextureSampler(1, m_deinterlace_buffers[(this_buffer - 1) % DEINTERLACE_BUFFER_COUNT].get(), g_gpu_device->GetNearestSampler()); g_gpu_device->SetTextureSampler(2, m_deinterlace_buffers[(this_buffer - 2) % DEINTERLACE_BUFFER_COUNT].get(), g_gpu_device->GetNearestSampler()); g_gpu_device->SetTextureSampler(3, m_deinterlace_buffers[(this_buffer - 3) % DEINTERLACE_BUFFER_COUNT].get(), g_gpu_device->GetNearestSampler()); const u32 uniforms[] = {field, full_height}; g_gpu_device->PushUniformBuffer(uniforms, sizeof(uniforms)); g_gpu_device->SetViewportAndScissor(0, 0, width, full_height); g_gpu_device->Draw(3, 0); m_deinterlace_texture->MakeReadyForSampling(); SetDisplayTexture(m_deinterlace_texture.get(), 0, 0, width, full_height); return true; } default: UnreachableCode(); } } bool GPUPresenter::DeinterlaceSetTargetSize(u32 width, u32 height, bool preserve) { if (!g_gpu_device->ResizeTexture(&m_deinterlace_texture, width, height, GPUTexture::Type::RenderTarget, GPUTexture::Format::RGBA8, GPUTexture::Flags::None, preserve)) [[unlikely]] { return false; } GL_OBJECT_NAME(m_deinterlace_texture, "Deinterlace target texture"); return true; } bool GPUPresenter::ApplyChromaSmoothing() { const u32 x = m_display_texture_view_x; const u32 y = m_display_texture_view_y; const u32 width = m_display_texture_view_width; const u32 height = m_display_texture_view_height; if (!g_gpu_device->ResizeTexture(&m_chroma_smoothing_texture, width, height, GPUTexture::Type::RenderTarget, GPUTexture::Format::RGBA8, GPUTexture::Flags::None, false)) { ClearDisplayTexture(); return false; } GL_OBJECT_NAME(m_chroma_smoothing_texture, "Chroma smoothing texture"); GL_SCOPE_FMT("ApplyChromaSmoothing({{{},{}}}, {}x{})", x, y, width, height); m_display_texture->MakeReadyForSampling(); g_gpu_device->InvalidateRenderTarget(m_chroma_smoothing_texture.get()); g_gpu_device->SetRenderTarget(m_chroma_smoothing_texture.get()); g_gpu_device->SetPipeline(m_chroma_smoothing_pipeline.get()); g_gpu_device->SetTextureSampler(0, m_display_texture, g_gpu_device->GetNearestSampler()); const u32 uniforms[] = {x, y, width - 1, height - 1}; g_gpu_device->PushUniformBuffer(uniforms, sizeof(uniforms)); g_gpu_device->SetViewportAndScissor(0, 0, width, height); g_gpu_device->Draw(3, 0); m_chroma_smoothing_texture->MakeReadyForSampling(); SetDisplayTexture(m_chroma_smoothing_texture.get(), 0, 0, width, height); return true; } void GPUPresenter::CalculateDrawRect(s32 window_width, s32 window_height, bool apply_rotation, bool apply_aspect_ratio, GSVector4i* display_rect, GSVector4i* draw_rect) const { const bool integer_scale = (g_gpu_settings.display_scaling == DisplayScalingMode::NearestInteger || g_gpu_settings.display_scaling == DisplayScalingMode::BilinearInteger); const bool show_vram = g_gpu_settings.gpu_show_vram; const u32 display_width = show_vram ? VRAM_WIDTH : m_display_width; const u32 display_height = show_vram ? VRAM_HEIGHT : m_display_height; const s32 display_origin_left = show_vram ? 0 : m_display_origin_left; const s32 display_origin_top = show_vram ? 0 : m_display_origin_top; const u32 display_vram_width = show_vram ? VRAM_WIDTH : m_display_vram_width; const u32 display_vram_height = show_vram ? VRAM_HEIGHT : m_display_vram_height; const float display_pixel_aspect_ratio = show_vram ? 1.0f : m_display_pixel_aspect_ratio; GPU::CalculateDrawRect(window_width, window_height, display_width, display_height, display_origin_left, display_origin_top, display_vram_width, display_vram_height, g_gpu_settings.display_rotation, g_gpu_settings.display_alignment, display_pixel_aspect_ratio, g_gpu_settings.display_stretch_vertically, integer_scale, display_rect, draw_rect); } bool GPUPresenter::PresentFrame(GPUPresenter* presenter, GPUBackend* backend, bool allow_skip_present, u64 present_time) { const bool skip_present = (!g_gpu_device->HasMainSwapChain() || (allow_skip_present && g_gpu_device->GetMainSwapChain()->ShouldSkipPresentingFrame() && presenter && presenter->m_skipped_present_count < MAX_SKIPPED_PRESENT_COUNT)); if (!skip_present) { // acquire for IO.MousePos and system state. std::atomic_thread_fence(std::memory_order_acquire); FullscreenUI::Render(); if (backend && System::IsValid()) ImGuiManager::RenderTextOverlays(backend); ImGuiManager::RenderOverlayWindows(); ImGuiManager::RenderOSDMessages(); ImGuiFullscreen::RenderOverlays(); if (backend && System::GetState() == System::State::Running) ImGuiManager::RenderSoftwareCursors(); ImGuiManager::RenderDebugWindows(); } const GPUDevice::PresentResult pres = skip_present ? GPUDevice::PresentResult::SkipPresent : (presenter ? presenter->PresentDisplay() : g_gpu_device->BeginPresent(g_gpu_device->GetMainSwapChain())); if (pres == GPUDevice::PresentResult::OK) { if (presenter) presenter->m_skipped_present_count = 0; g_gpu_device->RenderImGui(g_gpu_device->GetMainSwapChain()); 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)); const bool timed_present = (scheduled_present && features.timed_present); if (scheduled_present && !explicit_present) { // No explicit present support, simulate it with Flush. g_gpu_device->FlushCommands(); SleepUntilPresentTime(present_time); } g_gpu_device->EndPresent(g_gpu_device->GetMainSwapChain(), explicit_present, timed_present ? present_time : 0); if (g_gpu_device->IsGPUTimingEnabled()) PerformanceCounters::AccumulateGPUTime(); if (explicit_present) { SleepUntilPresentTime(present_time); g_gpu_device->SubmitPresent(g_gpu_device->GetMainSwapChain()); } } else { if (presenter) presenter->m_skipped_present_count++; if (pres == GPUDevice::PresentResult::DeviceLost) [[unlikely]] { ERROR_LOG("GPU device lost during present."); GPUThread::ReportFatalErrorAndShutdown("GPU device lost. The log may contain more information."); return false; } if (pres == GPUDevice::PresentResult::ExclusiveFullscreenLost) [[unlikely]] { WARNING_LOG("Lost exclusive fullscreen."); Host::SetFullscreen(false); } if (!skip_present) g_gpu_device->FlushCommands(); // Still need to kick ImGui or it gets cranky. ImGui::EndFrame(); } ImGuiManager::NewFrame(); return true; } void GPUPresenter::SleepUntilPresentTime(u64 present_time) { // Use a spinwait if we undersleep for all platforms except android.. don't want to burn battery. // Linux also seems to do a much better job of waking up at the requested time. #if !defined(__linux__) && !defined(__ANDROID__) Timer::SleepUntil(present_time, true); #else Timer::SleepUntil(present_time, false); #endif } bool GPUPresenter::RenderScreenshotToBuffer(u32 width, u32 height, const GSVector4i display_rect, const GSVector4i draw_rect, bool postfx, Image* out_image) { 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, 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(), GSVector4i::zero(), display_rect, draw_rect, postfx); Image image(width, height, image_format); Error error; std::unique_ptr dltex; if (g_gpu_device->GetFeatures().memory_import) { 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, m_present_format, &error))) { ERROR_LOG("Failed to create {}x{} download texture: {}", width, height, error.GetDescription()); return false; } } dltex->CopyFromTexture(0, 0, render_texture.get(), 0, 0, width, height, 0, 0, false); if (!dltex->ReadTexels(0, 0, width, height, image.GetPixels(), image.GetPitch())) return false; *out_image = std::move(image); return true; } void GPUPresenter::CalculateScreenshotSize(DisplayScreenshotMode mode, u32* width, u32* height, GSVector4i* display_rect, GSVector4i* draw_rect) const { const bool internal_resolution = (mode != DisplayScreenshotMode::ScreenResolution || g_gpu_settings.gpu_show_vram); if (internal_resolution && m_display_texture_view_width != 0 && m_display_texture_view_height != 0) { if (mode == DisplayScreenshotMode::InternalResolution) { float f_width = static_cast(m_display_texture_view_width); float f_height = static_cast(m_display_texture_view_height); if (!g_gpu_settings.gpu_show_vram) GPU::ApplyPixelAspectRatioToSize(m_display_pixel_aspect_ratio, &f_width, &f_height); // DX11 won't go past 16K texture size. const float max_texture_size = static_cast(g_gpu_device->GetMaxTextureSize()); if (f_width > max_texture_size) { f_height = f_height / (f_width / max_texture_size); f_width = max_texture_size; } if (f_height > max_texture_size) { f_height = max_texture_size; f_width = f_width / (f_height / max_texture_size); } *width = static_cast(std::ceil(f_width)); *height = static_cast(std::ceil(f_height)); } else // if (mode == DisplayScreenshotMode::UncorrectedInternalResolution) { *width = m_display_texture_view_width; *height = m_display_texture_view_height; } // Remove padding, it's not part of the framebuffer. *draw_rect = GSVector4i(0, 0, static_cast(*width), static_cast(*height)); *display_rect = *draw_rect; } else { *width = g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetWidth() : 1; *height = g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetHeight() : 1; CalculateDrawRect(*width, *height, true, !g_settings.gpu_show_vram, display_rect, draw_rect); } } void GPUPresenter::LoadPostProcessingSettings(bool force_load) { static constexpr const char* section = PostProcessing::Config::DISPLAY_CHAIN_SECTION; auto lock = Host::GetSettingsLock(); const SettingsInterface& si = GetPostProcessingSettingsInterface(section); // This is the initial load, defer creating the chain until it's actually enabled if disabled. if (!force_load && (!PostProcessing::Config::IsEnabled(si, section) || PostProcessing::Config::GetStageCount(si, section) == 0)) { return; } m_display_postfx = std::make_unique(section); m_display_postfx->LoadStages(lock, si, true); } bool GPUPresenter::UpdatePostProcessingSettings(bool force_reload, 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; } } // Update postfx settings { static constexpr const char* section = PostProcessing::Config::DISPLAY_CHAIN_SECTION; auto lock = Host::GetSettingsLock(); const SettingsInterface& si = *Host::GetSettingsInterface(); // Don't delete the chain if we're just temporarily disabling. if (PostProcessing::Config::GetStageCount(si, section) == 0) { m_display_postfx.reset(); } else { if (!m_display_postfx || force_reload) { if (!m_display_postfx) m_display_postfx = std::make_unique(section); m_display_postfx->LoadStages(lock, si, true); } else if (!force_reload) { m_display_postfx->UpdateSettings(lock, si); } } } return true; } SettingsInterface& GPUPresenter::GetPostProcessingSettingsInterface(const char* section) { // If PostProcessing/Enable is set in the game settings interface, use that. // Otherwise, use the base settings. SettingsInterface* game_si = Host::Internal::GetGameSettingsLayer(); if (game_si && game_si->ContainsValue(section, "Enabled")) return *game_si; else return *Host::Internal::GetBaseSettingsLayer(); } void GPUPresenter::TogglePostProcessing() { DebugAssert(!GPUThread::IsOnThread()); GPUThread::RunOnBackend( [](GPUBackend* backend) { if (!backend) return; GPUPresenter& presenter = backend->GetPresenter(); // if it is being lazy loaded, we have to load it here if (!presenter.m_display_postfx) { presenter.LoadPostProcessingSettings(true); if (presenter.m_display_postfx && presenter.m_display_postfx->IsActive()) return; } if (presenter.m_display_postfx) presenter.m_display_postfx->Toggle(); }, false, true); } void GPUPresenter::ReloadPostProcessingSettings(bool display, bool internal, bool reload_shaders) { DebugAssert(!GPUThread::IsOnThread()); GPUThread::RunOnBackend( [display, internal, reload_shaders](GPUBackend* backend) { if (!backend) return; // OSD message first in case any errors occur. if (reload_shaders) { Host::AddIconOSDMessage("PostProcessing", ICON_FA_PAINT_ROLLER, TRANSLATE_STR("OSDMessage", "Post-processing shaders reloaded."), Host::OSD_QUICK_DURATION); } if (display) { Error error; if (!backend->GetPresenter().UpdatePostProcessingSettings(reload_shaders, &error)) { GPUThread::ReportFatalErrorAndShutdown(fmt::format("Failed to update settings: {}", error.GetDescription())); return; } } if (internal) backend->UpdatePostProcessingSettings(reload_shaders); // trigger represent of frame if (GPUThread::IsSystemPaused()) GPUThread::Internal::PresentFrameAndRestoreContext(); }, false, true); } 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; }