FullscreenUI: Add offscreen-based screen fade

This commit is contained in:
Stenzek 2025-03-30 12:57:17 +10:00
parent 4e2872f248
commit e6e6313219
No known key found for this signature in database
9 changed files with 342 additions and 52 deletions

View File

@ -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<GPUPipeline> app_background_shader;
Timer::Value app_background_load_time = 0;
// Transition Resources
TransitionStartCallback transition_start_callback;
std::unique_ptr<GPUTexture> transition_prev_texture;
std::unique_ptr<GPUTexture> transition_current_texture;
std::unique_ptr<GPUPipeline> 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<GPUShader> vs = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(),
shadergen.GeneratePassthroughVertexShader(), &error);
std::unique_ptr<GPUShader> 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();

View File

@ -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()>;
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<std::string_view> GetThemeNames();
std::span<const char* const> GetThemeConfigNames();

View File

@ -115,7 +115,7 @@ bool GPUPresenter::CompileDisplayPipelines(bool display, bool deinterlace, bool
plconfig.SetTargetFormats(m_present_format);
std::unique_ptr<GPUShader> 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);

View File

@ -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();

View File

@ -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;

View File

@ -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<float>(texture->GetWidth()), static_cast<float>(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<false>(&pcmd->ClipRect.x));
if (flip)
clip = FlipToLowerLeft(clip, texture->GetHeight());
SetScissor(clip);
SetTextureSampler(0, reinterpret_cast<GPUTexture*>(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;

View File

@ -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; }

View File

@ -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();
}

View File

@ -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;