From e6e6313219d13ac0acd6a34c36b81cb2dd39c649 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 30 Mar 2025 12:57:17 +1000 Subject: [PATCH] FullscreenUI: Add offscreen-based screen fade --- src/core/fullscreen_ui.cpp | 188 ++++++++++++++++++++++++++++++++++++- src/core/fullscreen_ui.h | 22 +++++ src/core/gpu_presenter.cpp | 32 +++++-- src/core/gpu_presenter.h | 8 +- src/core/gpu_shadergen.cpp | 21 ----- src/util/gpu_device.cpp | 59 ++++++++++++ src/util/gpu_device.h | 1 + src/util/shadergen.cpp | 60 ++++++++---- src/util/shadergen.h | 3 +- 9 files changed, 342 insertions(+), 52 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 80300a8ca..711d12420 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_backend.h" #include "gpu_presenter.h" #include "gpu_thread.h" #include "gte_types.h" @@ -237,6 +238,8 @@ static void CopyTextToClipboard(std::string title, std::string_view text); static void DrawAboutWindow(); static void FixStateIfPaused(); static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel); +static bool CompileTransitionPipelines(); +static void UpdateTransitionState(); ////////////////////////////////////////////////////////////////////////// // Backgrounds @@ -528,6 +531,7 @@ private: struct ALIGN_TO_CACHE_LINE UIState { // Main + TransitionState transition_state = TransitionState::Inactive; MainWindowType current_main_window = MainWindowType::None; PauseSubMenu current_pause_submenu = PauseSubMenu::None; bool initialized = false; @@ -552,6 +556,14 @@ struct ALIGN_TO_CACHE_LINE UIState std::unique_ptr app_background_shader; Timer::Value app_background_load_time = 0; + // Transition Resources + TransitionStartCallback transition_start_callback; + std::unique_ptr transition_prev_texture; + std::unique_ptr transition_current_texture; + std::unique_ptr transition_blend_pipeline; + float transition_total_time = 0.0f; + float transition_remaining_time = 0.0f; + // Settings float settings_last_bg_alpha = 1.0f; SettingsPage settings_page = SettingsPage::Interface; @@ -764,7 +776,8 @@ bool FullscreenUI::IsInitialized() bool FullscreenUI::HasActiveWindow() { - return s_state.initialized && (s_state.current_main_window != MainWindowType::None || AreAnyDialogsOpen()); + return s_state.initialized && (s_state.current_main_window != MainWindowType::None || + s_state.transition_state != TransitionState::Inactive || AreAnyDialogsOpen()); } bool FullscreenUI::AreAnyDialogsOpen() @@ -786,6 +799,169 @@ void FullscreenUI::UpdateRunIdleState() GPUThread::SetRunIdleReason(GPUThread::RunIdleReason::FullscreenUIActive, new_run_idle); } +void FullscreenUI::BeginTransition(TransitionStartCallback func, float time) +{ + if (s_state.transition_state == TransitionState::Starting) + { + WARNING_LOG("More than one transition started"); + if (s_state.transition_start_callback) + std::move(s_state.transition_start_callback)(); + } + + s_state.transition_state = TransitionState::Starting; + s_state.transition_total_time = time; + s_state.transition_remaining_time = time; + s_state.transition_start_callback = func; + UpdateRunIdleState(); +} + +void FullscreenUI::CancelTransition() +{ + if (s_state.transition_state != TransitionState::Active) + return; + + if (s_state.transition_state == TransitionState::Starting && s_state.transition_start_callback) + std::move(s_state.transition_start_callback)(); + + s_state.transition_state = TransitionState::Inactive; + s_state.transition_start_callback = {}; + s_state.transition_remaining_time = 0.0f; +} + +void FullscreenUI::BeginTransition(float time, TransitionStartCallback func) +{ + BeginTransition(std::move(func), time); +} + +bool FullscreenUI::IsTransitionActive() +{ + return (s_state.transition_state != TransitionState::Inactive); +} + +FullscreenUI::TransitionState FullscreenUI::GetTransitionState() +{ + return s_state.transition_state; +} + +GPUTexture* FullscreenUI::GetTransitionRenderTexture(GPUSwapChain* swap_chain) +{ + if (!g_gpu_device->ResizeTexture(&s_state.transition_current_texture, swap_chain->GetWidth(), swap_chain->GetHeight(), + GPUTexture::Type::RenderTarget, swap_chain->GetFormat(), GPUTexture::Flags::None, + false)) + { + ERROR_LOG("Failed to allocate {}x{} texture for transition, cancelling.", swap_chain->GetWidth(), + swap_chain->GetHeight()); + s_state.transition_state = TransitionState::Inactive; + return nullptr; + } + + return s_state.transition_current_texture.get(); +} + +bool FullscreenUI::CompileTransitionPipelines() +{ + const RenderAPI render_api = g_gpu_device->GetRenderAPI(); + const ShaderGen shadergen(render_api, ShaderGen::GetShaderLanguageForAPI(render_api), false, false); + GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); + + Error error; + std::unique_ptr vs = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), + shadergen.GeneratePassthroughVertexShader(), &error); + std::unique_ptr fs = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(), + shadergen.GenerateFadeFragmentShader(), &error); + if (!vs || !fs) + { + ERROR_LOG("Failed to compile transition shaders: {}", error.GetDescription()); + return false; + } + GL_OBJECT_NAME(vs, "Transition Vertex Shader"); + GL_OBJECT_NAME(fs, "Transition Fragment Shader"); + + GPUPipeline::GraphicsConfig plconfig; + GPUBackend::SetScreenQuadInputLayout(plconfig); + plconfig.layout = GPUPipeline::Layout::MultiTextureAndPushConstants; + plconfig.rasterization = GPUPipeline::RasterizationState::GetNoCullState(); + plconfig.depth = GPUPipeline::DepthState::GetNoTestsState(); + plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState(); + plconfig.SetTargetFormats(swap_chain ? swap_chain->GetFormat() : GPUTexture::Format::RGBA8); + plconfig.samples = 1; + plconfig.per_sample_shading = false; + plconfig.render_pass_flags = GPUPipeline::NoRenderPassFlags; + plconfig.vertex_shader = vs.get(); + plconfig.geometry_shader = nullptr; + plconfig.fragment_shader = fs.get(); + + s_state.transition_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, &error); + if (!s_state.transition_blend_pipeline) + { + ERROR_LOG("Failed to create transition blend pipeline: {}", error.GetDescription()); + return false; + } + + return true; +} + +void FullscreenUI::RenderTransitionBlend(GPUSwapChain* swap_chain) +{ + GPUTexture* const curr = s_state.transition_current_texture.get(); + DebugAssert(curr); + + if (s_state.transition_state == TransitionState::Starting) + { + // copy current frame + if (!g_gpu_device->ResizeTexture(&s_state.transition_prev_texture, curr->GetWidth(), curr->GetHeight(), + GPUTexture::Type::RenderTarget, curr->GetFormat(), GPUTexture::Flags::None, false)) + { + ERROR_LOG("Failed to allocate {}x{} texture for transition, cancelling.", curr->GetWidth(), curr->GetHeight()); + s_state.transition_state = TransitionState::Inactive; + return; + } + + g_gpu_device->CopyTextureRegion(s_state.transition_prev_texture.get(), 0, 0, 0, 0, curr, 0, 0, 0, 0, + curr->GetWidth(), curr->GetHeight()); + + s_state.transition_state = TransitionState::Active; + } + + const float transition_alpha = s_state.transition_remaining_time / s_state.transition_total_time; + const float uniforms[2] = {1.0f - transition_alpha, transition_alpha}; + g_gpu_device->PushUniformBuffer(uniforms, sizeof(uniforms)); + g_gpu_device->SetPipeline(s_state.transition_blend_pipeline.get()); + g_gpu_device->SetViewportAndScissor(0, 0, swap_chain->GetPostRotatedWidth(), swap_chain->GetPostRotatedHeight()); + g_gpu_device->SetTextureSampler(0, curr, g_gpu_device->GetNearestSampler()); + g_gpu_device->SetTextureSampler(1, s_state.transition_prev_texture.get(), g_gpu_device->GetNearestSampler()); + + const GSVector2i size = swap_chain->GetSizeVec(); + const GSVector2i postrotated_size = swap_chain->GetPostRotatedSizeVec(); + const GSVector4 uv_rect = g_gpu_device->UsesLowerLeftOrigin() ? GSVector4::cxpr(0.0f, 1.0f, 1.0f, 0.0f) : + GSVector4::cxpr(0.0f, 0.0f, 1.0f, 1.0f); + GPUPresenter::DrawScreenQuad(GSVector4i::loadh(size), uv_rect, size, postrotated_size, DisplayRotation::Normal, + swap_chain->GetPreRotation()); +} + +void FullscreenUI::UpdateTransitionState() +{ + if (s_state.transition_state == TransitionState::Inactive) + { + return; + } + else if (s_state.transition_state == TransitionState::Starting) + { + // starting is cleared in render + if (s_state.transition_start_callback) + std::move(s_state.transition_start_callback)(); + } + + s_state.transition_remaining_time -= ImGui::GetIO().DeltaTime; + if (s_state.transition_remaining_time <= 0.0f) + { + // At 1080p we're only talking 2MB of VRAM, 16MB at 4K.. saves reallocating it on the next transition. + // g_gpu_device->RecycleTexture(std::move(s_state.transition_current_texture)); + // g_gpu_device->RecycleTexture(std::move(s_state.transition_prev_texture)); + s_state.transition_state = TransitionState::Inactive; + } +} + void FullscreenUI::OnSystemStarting() { // NOTE: Called on CPU thread. @@ -1116,6 +1292,7 @@ void FullscreenUI::Render() } ImGuiFullscreen::ResetCloseMenuIfNeeded(); + UpdateTransitionState(); } void FullscreenUI::InvalidateCoverCache() @@ -1175,11 +1352,20 @@ bool FullscreenUI::LoadResources() s_state.fallback_exe_texture = LoadTexture("fullscreenui/exe-file.png"); s_state.fallback_psf_texture = LoadTexture("fullscreenui/psf-file.png"); s_state.fallback_playlist_texture = LoadTexture("fullscreenui/playlist-file.png"); + + if (!CompileTransitionPipelines()) + return false; + return true; } void FullscreenUI::DestroyResources() { + s_state.transition_blend_pipeline.reset(); + g_gpu_device->RecycleTexture(std::move(s_state.transition_prev_texture)); + g_gpu_device->RecycleTexture(std::move(s_state.transition_current_texture)); + s_state.transition_state = TransitionState::Inactive; + s_state.transition_start_callback = {}; s_state.fallback_playlist_texture.reset(); s_state.fallback_psf_texture.reset(); s_state.fallback_exe_texture.reset(); diff --git a/src/core/fullscreen_ui.h b/src/core/fullscreen_ui.h index ddcc20d4f..e536a7a87 100644 --- a/src/core/fullscreen_ui.h +++ b/src/core/fullscreen_ui.h @@ -16,6 +16,9 @@ class SmallStringBase; +class GPUSwapChain; +class GPUTexture; + struct GPUSettings; namespace FullscreenUI { @@ -46,6 +49,25 @@ void SetTheme(); #ifndef __ANDROID__ +static constexpr float SHORT_TRANSITION_TIME = 0.08f; +static constexpr float DEFAULT_TRANSITION_TIME = 0.15f; + +enum class TransitionState : u8 +{ + Inactive, + Starting, + Active, +}; + +using TransitionStartCallback = std::function; +void BeginTransition(TransitionStartCallback func, float time = DEFAULT_TRANSITION_TIME); +void BeginTransition(float time, TransitionStartCallback func); +void CancelTransition(); +bool IsTransitionActive(); +TransitionState GetTransitionState(); +GPUTexture* GetTransitionRenderTexture(GPUSwapChain* swap_chain); +void RenderTransitionBlend(GPUSwapChain* swap_chain); + std::vector GetThemeNames(); std::span GetThemeConfigNames(); diff --git a/src/core/gpu_presenter.cpp b/src/core/gpu_presenter.cpp index 2cadfd0de..2985cf076 100644 --- a/src/core/gpu_presenter.cpp +++ b/src/core/gpu_presenter.cpp @@ -115,7 +115,7 @@ bool GPUPresenter::CompileDisplayPipelines(bool display, bool deinterlace, bool plconfig.SetTargetFormats(m_present_format); std::unique_ptr vso = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), - shadergen.GenerateDisplayVertexShader(), error); + shadergen.GeneratePassthroughVertexShader(), error); if (!vso) return false; GL_OBJECT_NAME(vso, "Display Vertex Shader"); @@ -1047,19 +1047,39 @@ bool GPUPresenter::PresentFrame(GPUPresenter* presenter, GPUBackend* backend, bo ImGuiManager::RenderSoftwareCursors(); ImGuiManager::RenderDebugWindows(); + + // render offscreen for transitions + if (FullscreenUI::IsTransitionActive()) + { + GPUTexture* const rtex = FullscreenUI::GetTransitionRenderTexture(g_gpu_device->GetMainSwapChain()); + if (rtex) + { + if (presenter) + presenter->RenderDisplay(rtex, rtex->GetSizeVec(), true, true); + else + g_gpu_device->ClearRenderTarget(rtex, GPUDevice::DEFAULT_CLEAR_COLOR); + + g_gpu_device->SetRenderTarget(rtex); + g_gpu_device->RenderImGui(rtex); + } + } } GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); - const GPUDevice::PresentResult pres = - skip_present ? GPUDevice::PresentResult::SkipPresent : - (presenter ? presenter->RenderDisplay(nullptr, swap_chain->GetSizeVec(), true, true) : - g_gpu_device->BeginPresent(swap_chain)); + const GPUDevice::PresentResult pres = skip_present ? + GPUDevice::PresentResult::SkipPresent : + ((presenter && !FullscreenUI::IsTransitionActive()) ? + presenter->RenderDisplay(nullptr, swap_chain->GetSizeVec(), true, true) : + g_gpu_device->BeginPresent(swap_chain)); if (pres == GPUDevice::PresentResult::OK) { if (presenter) presenter->m_skipped_present_count = 0; - g_gpu_device->RenderImGui(swap_chain); + if (FullscreenUI::IsTransitionActive()) + FullscreenUI::RenderTransitionBlend(swap_chain); + else + g_gpu_device->RenderImGui(swap_chain); const GPUDevice::Features features = g_gpu_device->GetFeatures(); const bool scheduled_present = (present_time != 0); diff --git a/src/core/gpu_presenter.h b/src/core/gpu_presenter.h index 95e539b25..cfee3472b 100644 --- a/src/core/gpu_presenter.h +++ b/src/core/gpu_presenter.h @@ -89,6 +89,11 @@ public: /// Reloads post-processing settings. Only callable from the CPU thread. static void ReloadPostProcessingSettings(bool display, bool internal, bool reload_shaders); + // Draws the specified bounding box with display rotation and pre-rotation. + static void DrawScreenQuad(const GSVector4i rect, const GSVector4 uv_rect, const GSVector2i target_size, + const GSVector2i final_target_size, DisplayRotation uv_rotation, + WindowInfo::PreRotation prerotation); + private: enum : u32 { @@ -109,9 +114,6 @@ private: bool dst_alpha_blend, DisplayRotation rotation, WindowInfo::PreRotation prerotation); GPUDevice::PresentResult ApplyDisplayPostProcess(GPUTexture* target, GPUTexture* input, const GSVector4i display_rect); - void DrawScreenQuad(const GSVector4i rect, const GSVector4 uv_rect, const GSVector2i target_size, - const GSVector2i final_target_size, DisplayRotation uv_rotation, - WindowInfo::PreRotation prerotation); bool DeinterlaceSetTargetSize(u32 width, u32 height, bool preserve); void DestroyDeinterlaceTextures(); diff --git a/src/core/gpu_shadergen.cpp b/src/core/gpu_shadergen.cpp index aea2f1cb0..7fc0cf5de 100644 --- a/src/core/gpu_shadergen.cpp +++ b/src/core/gpu_shadergen.cpp @@ -20,27 +20,6 @@ float2 ClampUV(float2 uv) { })"; } -std::string GPUShaderGen::GenerateDisplayVertexShader() const -{ - std::stringstream ss; - WriteHeader(ss); - WriteDisplayUniformBuffer(ss); - DeclareVertexEntryPoint(ss, {"float2 a_pos", "float2 a_tex0"}, 0, 1, {}, false, "", false, false, false); - ss << R"( -{ - v_pos = float4(a_pos, 0.0f, 1.0f); - v_tex0 = a_tex0; - - // NDC space Y flip in Vulkan. - #if API_VULKAN - v_pos.y = -v_pos.y; - #endif -} -)"; - - return std::move(ss).str(); -} - std::string GPUShaderGen::GenerateDisplayFragmentShader(bool clamp_uv, bool nearest) const { std::stringstream ss; diff --git a/src/util/gpu_device.cpp b/src/util/gpu_device.cpp index 50d2bdc07..261e0da5f 100644 --- a/src/util/gpu_device.cpp +++ b/src/util/gpu_device.cpp @@ -804,6 +804,65 @@ void GPUDevice::RenderImGui(GPUSwapChain* swap_chain) } } +void GPUDevice::RenderImGui(GPUTexture* texture) +{ + GL_SCOPE("RenderImGui"); + + ImGui::Render(); + + const ImDrawData* draw_data = ImGui::GetDrawData(); + if (draw_data->CmdListsCount == 0) + return; + + SetPipeline(m_imgui_pipeline.get()); + SetViewport(0, 0, texture->GetWidth(), texture->GetHeight()); + + const GSMatrix4x4 mproj = GSMatrix4x4::OffCenterOrthographicProjection( + 0.0f, 0.0f, static_cast(texture->GetWidth()), static_cast(texture->GetHeight()), 0.0f, 1.0f); + PushUniformBuffer(&mproj, sizeof(mproj)); + + // Render command lists + const bool flip = UsesLowerLeftOrigin(); + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + static_assert(sizeof(ImDrawIdx) == sizeof(DrawIndex)); + + u32 base_vertex, base_index; + UploadVertexBuffer(cmd_list->VtxBuffer.Data, sizeof(ImDrawVert), cmd_list->VtxBuffer.Size, &base_vertex); + UploadIndexBuffer(cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size, &base_index); + + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + + if ((pcmd->ElemCount == 0 && !pcmd->UserCallback) || pcmd->ClipRect.z <= pcmd->ClipRect.x || + pcmd->ClipRect.w <= pcmd->ClipRect.y) + { + continue; + } + + GSVector4i clip = GSVector4i(GSVector4::load(&pcmd->ClipRect.x)); + if (flip) + clip = FlipToLowerLeft(clip, texture->GetHeight()); + + SetScissor(clip); + SetTextureSampler(0, reinterpret_cast(pcmd->TextureId), m_linear_sampler); + + if (pcmd->UserCallback) [[unlikely]] + { + pcmd->UserCallback(cmd_list, pcmd); + PushUniformBuffer(&mproj, sizeof(mproj)); + SetPipeline(m_imgui_pipeline.get()); + } + else + { + DrawIndexed(pcmd->ElemCount, base_index + pcmd->IdxOffset, base_vertex + pcmd->VtxOffset); + } + } + } +} + void GPUDevice::UploadVertexBuffer(const void* vertices, u32 vertex_size, u32 vertex_count, u32* base_vertex) { void* map; diff --git a/src/util/gpu_device.h b/src/util/gpu_device.h index c7b9b2a33..fc61a1def 100644 --- a/src/util/gpu_device.h +++ b/src/util/gpu_device.h @@ -861,6 +861,7 @@ public: /// Renders ImGui screen elements. Call before EndPresent(). void RenderImGui(GPUSwapChain* swap_chain); + void RenderImGui(GPUTexture* texture); ALWAYS_INLINE bool IsDebugDevice() const { return m_debug_device; } ALWAYS_INLINE size_t GetVRAMUsage() const { return s_total_vram_usage; } diff --git a/src/util/shadergen.cpp b/src/util/shadergen.cpp index 008494cfe..282ef9d7a 100644 --- a/src/util/shadergen.cpp +++ b/src/util/shadergen.cpp @@ -793,6 +793,26 @@ void ShaderGen::DeclareFragmentEntryPoint( } } +std::string ShaderGen::GeneratePassthroughVertexShader() const +{ + std::stringstream ss; + WriteHeader(ss); + DeclareVertexEntryPoint(ss, {"float2 a_pos", "float2 a_tex0"}, 0, 1, {}, false, "", false, false, false); + ss << R"( +{ + v_pos = float4(a_pos, 0.0f, 1.0f); + v_tex0 = a_tex0; + + // NDC space Y flip in Vulkan. + #if API_VULKAN + v_pos.y = -v_pos.y; + #endif +} +)"; + + return std::move(ss).str(); +} + std::string ShaderGen::GenerateScreenQuadVertexShader(float z /* = 0.0f */) const { std::stringstream ss; @@ -809,26 +829,6 @@ std::string ShaderGen::GenerateScreenQuadVertexShader(float z /* = 0.0f */) cons return std::move(ss).str(); } -std::string ShaderGen::GenerateUVQuadVertexShader() const -{ - std::stringstream ss; - WriteHeader(ss); - DeclareUniformBuffer(ss, {"float2 u_uv_min", "float2 u_uv_max"}, true); - DeclareVertexEntryPoint(ss, {}, 0, 1, {}, true); - ss << R"( -{ - v_tex0 = float2(float((v_id << 1) & 2u), float(v_id & 2u)); - v_pos = float4(v_tex0 * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f); - v_tex0 = u_uv_min + (u_uv_max - u_uv_min) * v_tex0; - #if API_OPENGL || API_OPENGL_ES || API_VULKAN - v_pos.y = -v_pos.y; - #endif -} -)"; - - return std::move(ss).str(); -} - std::string ShaderGen::GenerateFillFragmentShader() const { std::stringstream ss; @@ -925,3 +925,23 @@ std::string ShaderGen::GenerateImGuiFragmentShader() const return std::move(ss).str(); } + +std::string ShaderGen::GenerateFadeFragmentShader() const +{ + std::stringstream ss; + WriteHeader(ss); + DeclareUniformBuffer(ss, {"float u_tex0_weight", "float u_tex1_weight"}, true); + DeclareTexture(ss, "samp0", 0); + DeclareTexture(ss, "samp1", 1); + DeclareFragmentEntryPoint(ss, 0, 1); + + ss << R"( +{ + o_col0 = SAMPLE_TEXTURE(samp0, v_tex0) * u_tex0_weight; + o_col0 += SAMPLE_TEXTURE(samp1, v_tex0) * u_tex1_weight; + o_col0.a = 1.0f; +} +)"; + + return std::move(ss).str(); +} diff --git a/src/util/shadergen.h b/src/util/shadergen.h index 478cf530e..b5d3356af 100644 --- a/src/util/shadergen.h +++ b/src/util/shadergen.h @@ -27,14 +27,15 @@ 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 GeneratePassthroughVertexShader() const; std::string GenerateScreenQuadVertexShader(float z = 0.0f) const; - std::string GenerateUVQuadVertexShader() const; std::string GenerateFillFragmentShader() const; std::string GenerateFillFragmentShader(const GSVector4 fixed_color) const; std::string GenerateCopyFragmentShader(bool offset = true) const; std::string GenerateImGuiVertexShader() const; std::string GenerateImGuiFragmentShader() const; + std::string GenerateFadeFragmentShader() const; const char* GetInterpolationQualifier(bool interface_block, bool centroid_interpolation, bool sample_interpolation, bool is_out) const;