diff --git a/data/resources/fullscreenui/backgrounds/Default.glsl b/data/resources/fullscreenui/backgrounds/Default.glsl new file mode 100644 index 000000000..6b30e6d1e --- /dev/null +++ b/data/resources/fullscreenui/backgrounds/Default.glsl @@ -0,0 +1,9 @@ +void main() +{ + // Radial gradient at (0.6, 0.4), moving horizontally slowly + float r1 = length(v_tex0 - vec2(0.6, 0.4)); + float r2 = length(v_tex0 - vec2(0.61, 0.41)); + float r = mix(r1, r2, sin(u_time / 10.0)); + vec3 bg_color = vec3(r * 0.33); + o_col0 = vec4(bg_color, 1.0); +} diff --git a/data/resources/fullscreenui/backgrounds/Trails.glsl b/data/resources/fullscreenui/backgrounds/Trails.glsl new file mode 100644 index 000000000..649565971 --- /dev/null +++ b/data/resources/fullscreenui/backgrounds/Trails.glsl @@ -0,0 +1,30 @@ +const float PI = 3.14159265359; +const float SCALE = 150.0; +const float LENGTH = 7.5; + +// https://www.shadertoy.com/view/Xt23Ry +float rand(float co) { + return fract(sin(co * 91.3458) * 47453.5453); +} + +vec3 background(vec2 pos) { + // Radial gradient at (0.6, 0.4). + float r = length(pos - vec2(0.6, 0.4)); + return vec3(r * 0.1); +} + +// Inspired by https://www.shadertoy.com/view/Wtl3D7 +vec3 trails(vec2 pos, vec3 bg_color, vec3 fg_color) { + float cdist = length(pos) * SCALE; + float rv = rand(ceil(cdist)); + float rotation = u_time * rv * 0.005; + float nangle = atan(pos.y, pos.x) / PI; + float intensity = smoothstep(rv, rv - 1.5, fract(nangle + rotation + rv * 0.1) * LENGTH) * step(0.1, cdist / SCALE); + return mix(bg_color, fg_color * rv, intensity); +} + +void main() { + vec3 bg_color = background(v_tex0); + vec3 fg_color = vec3(0.7, 0.7, 1.5); + o_col0 = vec4(trails(v_tex0, bg_color, fg_color), 1.0); +} diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index cdf0856ef..5a13f6fa8 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -24,6 +24,7 @@ #include "util/ini_settings_interface.h" #include "util/input_manager.h" #include "util/postprocessing.h" +#include "util/shadergen.h" #include "common/error.h" #include "common/file_system.h" @@ -224,6 +225,20 @@ static void OpenAboutWindow(); static void FixStateIfPaused(); static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel); +////////////////////////////////////////////////////////////////////////// +// Backgrounds +////////////////////////////////////////////////////////////////////////// + +static constexpr const char* DEFAULT_BACKGROUND_NAME = "Trails"; + +static bool HasBackground(); +static void LoadBackground(); +static bool LoadBackgroundShader(const std::string& path, Error* error); +static bool LoadBackgroundImage(const std::string& path, Error* error); +static void DrawBackground(); +static void DrawShaderBackgroundCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd); +static ChoiceDialogOptions GetBackgroundOptions(const TinyString& current_value); + ////////////////////////////////////////////////////////////////////////// // Resources ////////////////////////////////////////////////////////////////////////// @@ -282,7 +297,7 @@ static void DrawAdvancedSettingsPage(); static void DrawPatchesOrCheatsSettingsPage(bool cheats); static bool ShouldShowAdvancedSettings(); -static float GetSettingsWindowBgAlpha(); +static float GetBackgroundAlpha(); static bool IsEditingGameSettings(SettingsInterface* bsi); static SettingsInterface* GetEditingSettingsInterface(); static SettingsInterface* GetEditingSettingsInterface(bool game_settings); @@ -480,6 +495,11 @@ struct ALIGN_TO_CACHE_LINE UIState std::shared_ptr fallback_psf_texture; std::shared_ptr fallback_playlist_texture; + // Background + std::unique_ptr app_background_texture; + std::unique_ptr app_background_shader; + Timer::Value app_background_load_time = 0; + // Settings float settings_last_bg_alpha = 1.0f; SettingsPage settings_page = SettingsPage::Interface; @@ -649,6 +669,7 @@ bool FullscreenUI::Initialize() SwitchToLanding(); } + LoadBackground(); UpdateRunIdleState(); ForceKeyNavEnabled(); return true; @@ -885,6 +906,10 @@ void FullscreenUI::Render() ImGuiFullscreen::UploadAsyncTextures(); + // draw background before any overlays + if (!GPUThread::HasGPUBackend()) + DrawBackground(); + ImGuiFullscreen::BeginLayout(); // Primed achievements must come first, because we don't want the pause screen to be behind them. @@ -1021,7 +1046,6 @@ void FullscreenUI::ReturnToMainWindow() bool FullscreenUI::LoadResources() { s_state.app_icon_texture = LoadTexture("images/duck.png"); - s_state.fallback_disc_texture = LoadTexture("fullscreenui/media-cdrom.png"); s_state.fallback_exe_texture = LoadTexture("fullscreenui/applications-system.png"); s_state.fallback_psf_texture = LoadTexture("fullscreenui/multimedia-player.png"); @@ -1031,11 +1055,13 @@ bool FullscreenUI::LoadResources() void FullscreenUI::DestroyResources() { - s_state.app_icon_texture.reset(); s_state.fallback_playlist_texture.reset(); s_state.fallback_psf_texture.reset(); s_state.fallback_exe_texture.reset(); s_state.fallback_disc_texture.reset(); + s_state.app_background_texture.reset(); + s_state.app_background_shader.reset(); + s_state.app_icon_texture.reset(); } ////////////////////////////////////////////////////////////////////////// @@ -1386,6 +1412,208 @@ void FullscreenUI::DoToggleFullscreen() // Landing Window ////////////////////////////////////////////////////////////////////////// +bool FullscreenUI::HasBackground() +{ + return static_cast(s_state.app_background_texture || s_state.app_background_shader); +} + +void FullscreenUI::LoadBackground() +{ + if (!IsInitialized()) + return; + + g_gpu_device->RecycleTexture(std::move(s_state.app_background_texture)); + s_state.app_background_shader.reset(); + + const TinyString background_name = + Host::GetBaseTinyStringSettingValue("Main", "FullscreenUIBackground", DEFAULT_BACKGROUND_NAME); + if (background_name.empty() || background_name == "None") + return; + + static constexpr std::pair loaders[] = { + {"glsl", &FullscreenUI::LoadBackgroundShader}, + {"jpg", &FullscreenUI::LoadBackgroundImage}, + {"png", &FullscreenUI::LoadBackgroundImage}, + {"webp", &FullscreenUI::LoadBackgroundImage}, + }; + + for (const auto& [extension, loader] : loaders) + { + static constexpr auto get_path = [](const std::string& dir, const TinyString& name, const char* extension) { + return fmt::format("{}" FS_OSPATH_SEPARATOR_STR "fullscreenui" FS_OSPATH_SEPARATOR_STR + "backgrounds" FS_OSPATH_SEPARATOR_STR "{}.{}", + dir, name, extension); + }; + + // try user directory first + std::string path = get_path(EmuFolders::UserResources, background_name, extension); + if (!FileSystem::FileExists(path.c_str())) + { + path = get_path(EmuFolders::Resources, background_name, extension); + if (!FileSystem::FileExists(path.c_str())) + continue; + } + + Error error; + if (!loader(path, &error)) + { + ERROR_LOG("Failed to load background '{}' with {} loader: {}", background_name, extension, + error.GetDescription()); + return; + } + + INFO_LOG("Loaded background '{}' with {} loader", background_name, extension); + return; + } + + ERROR_LOG("No loader or file found for background '{}'", background_name); +} + +ChoiceDialogOptions FullscreenUI::GetBackgroundOptions(const TinyString& current_value) +{ + static constexpr const char* dir = FS_OSPATH_SEPARATOR_STR "fullscreenui" FS_OSPATH_SEPARATOR_STR "backgrounds"; + + ChoiceDialogOptions options; + options.emplace_back(FSUI_STR("None"), current_value.empty()); + + FileSystem::FindResultsArray results; + FileSystem::FindFiles(Path::Combine(EmuFolders::UserResources, dir).c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results); + FileSystem::FindFiles(Path::Combine(EmuFolders::Resources, dir).c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_KEEP_ARRAY, &results); + + for (const auto& it : results) + { + const std::string_view name = Path::GetFileTitle(it.FileName); + if (std::any_of(options.begin(), options.end(), [&name](const auto& it) { return it.first == name; })) + continue; + + options.emplace_back(name, current_value == name); + } + + return options; +} + +bool FullscreenUI::LoadBackgroundShader(const std::string& path, Error* error) +{ + std::optional shader_body = FileSystem::ReadFileToString(path.c_str(), error); + if (!shader_body.has_value()) + return false; + + const std::string::size_type main_pos = shader_body->find("void main()"); + if (main_pos == std::string::npos) + { + Error::SetStringView(error, "main() definition not found in shader."); + return false; + } + + const ShaderGen shadergen(g_gpu_device->GetRenderAPI(), GPUShaderLanguage::GLSLVK, false, false); + + std::stringstream shader; + shadergen.WriteHeader(shader); + shadergen.DeclareVertexEntryPoint(shader, {}, 0, 1, {}, true); + shader << R"( { + v_tex0 = vec2(float((v_id << 1) & 2u), float(v_id & 2u)); + v_pos = vec4(v_tex0 * vec2(2.0f, -2.0f) + vec2(-1.0f, 1.0f), 0.0f, 1.0f); + #if API_VULKAN + v_pos.y = -v_pos.y; + #endif + })"; + std::unique_ptr vs = + g_gpu_device->CreateShader(GPUShaderStage::Vertex, GPUShaderLanguage::GLSLVK, shader.str(), error); + if (!vs) + return false; + + shader.str(std::string()); + shadergen.WriteHeader(shader, false, false, false); + shadergen.DeclareUniformBuffer(shader, {"vec2 u_display_size", "vec2 u_rcp_display_size", "float u_time"}, true); + if (main_pos > 0) + shader << std::string_view(shader_body.value()).substr(0, main_pos); + shadergen.DeclareFragmentEntryPoint(shader, 0, 1); + shader << std::string_view(shader_body.value()).substr(main_pos + 11); + + std::unique_ptr fs = + g_gpu_device->CreateShader(GPUShaderStage::Fragment, GPUShaderLanguage::GLSLVK, shader.str(), error); + if (!fs) + return false; + + 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; + plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; + plconfig.SetTargetFormats(g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetFormat() : + GPUTexture::Format::RGBA8); + plconfig.vertex_shader = vs.get(); + plconfig.fragment_shader = fs.get(); + s_state.app_background_shader = g_gpu_device->CreatePipeline(plconfig, error); + if (!s_state.app_background_shader) + return false; + + return true; +} + +void FullscreenUI::DrawShaderBackgroundCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd) +{ + if (!g_gpu_device->HasMainSwapChain()) + return; + + struct alignas(16) Uniforms + { + float u_display_size[2]; + float u_rcp_display_size[2]; + float u_time; + }; + + const GSVector2 display_size = GSVector2(g_gpu_device->GetMainSwapChain()->GetSizeVec()); + Uniforms uniforms; + GSVector2::store(uniforms.u_display_size, display_size); + GSVector2::store(uniforms.u_rcp_display_size, GSVector2::cxpr(1.0f) / display_size); + uniforms.u_time = + static_cast(Timer::ConvertValueToSeconds(Timer::GetCurrentValue() - s_state.app_background_load_time)); + + g_gpu_device->SetPipeline(s_state.app_background_shader.get()); + g_gpu_device->PushUniformBuffer(&uniforms, sizeof(uniforms)); + g_gpu_device->Draw(3, 0); +} + +bool FullscreenUI::LoadBackgroundImage(const std::string& path, Error* error) +{ + Image image; + if (!image.LoadFromFile(path.c_str(), error)) + return false; + + s_state.app_background_texture = g_gpu_device->FetchAndUploadTextureImage(image, GPUTexture::Flags::None, error); + if (!s_state.app_background_texture) + return false; + + return true; +} + +void FullscreenUI::DrawBackground() +{ + if (s_state.app_background_shader) + { + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + dl->AddCallback(&FullscreenUI::DrawShaderBackgroundCallback, nullptr); + } + else if (s_state.app_background_texture) + { + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + const ImVec2 size = ImGui::GetIO().DisplaySize; + const ImRect uv_rect = + ImGuiFullscreen::FitImage(size, ImVec2(static_cast(s_state.app_background_texture->GetWidth()), + static_cast(s_state.app_background_texture->GetHeight()))); + dl->AddImage(s_state.app_background_texture.get(), ImVec2(0.0f, 0.0f), size, uv_rect.Min, uv_rect.Max); + } +} + void FullscreenUI::SwitchToLanding() { s_state.current_main_window = MainWindowType::Landing; @@ -1401,7 +1629,8 @@ void FullscreenUI::DrawLandingTemplate(ImVec2* menu_pos, ImVec2* menu_size) *menu_pos = ImVec2(0.0f, heading_size.y); *menu_size = ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)); - if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "landing_heading", UIStyle.PrimaryColor)) + if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "landing_heading", + ModAlpha(UIStyle.PrimaryColor, GetBackgroundAlpha()))) { ImFont* const heading_font = UIStyle.LargeFont; ImDrawList* const dl = ImGui::GetWindowDrawList(); @@ -1471,7 +1700,8 @@ void FullscreenUI::DrawLandingWindow() ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - if (BeginHorizontalMenu("landing_window", menu_pos, menu_size, 4)) + if (BeginHorizontalMenu("landing_window", menu_pos, menu_size, + ModAlpha(UIStyle.BackgroundColor, GetBackgroundAlpha()), 4)) { ResetFocusHere(); @@ -1543,7 +1773,8 @@ void FullscreenUI::DrawStartGameWindow() ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - if (BeginHorizontalMenu("start_game_window", menu_pos, menu_size, 4)) + if (BeginHorizontalMenu("start_game_window", menu_pos, menu_size, + ModAlpha(UIStyle.BackgroundColor, GetBackgroundAlpha()), 4)) { ResetFocusHere(); @@ -1607,7 +1838,8 @@ void FullscreenUI::DrawExitWindow() ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - if (BeginHorizontalMenu("exit_window", menu_pos, menu_size, 3)) + if (BeginHorizontalMenu("exit_window", menu_pos, menu_size, ModAlpha(UIStyle.BackgroundColor, GetBackgroundAlpha()), + 3)) { ResetFocusHere(); @@ -1644,9 +1876,10 @@ bool FullscreenUI::ShouldShowAdvancedSettings() return Host::GetBaseBoolSettingValue("Main", "ShowDebugMenu", false); } -float FullscreenUI::GetSettingsWindowBgAlpha() +float FullscreenUI::GetBackgroundAlpha() { - return GPUThread::HasGPUBackend() ? (s_state.settings_page == SettingsPage::PostProcessing ? 0.50f : 0.90f) : 1.0f; + return GPUThread::HasGPUBackend() ? (s_state.settings_page == SettingsPage::PostProcessing ? 0.50f : 0.90f) : + (HasBackground() ? 0.5f : 1.0f); } bool FullscreenUI::IsEditingGameSettings(SettingsInterface* bsi) @@ -2983,7 +3216,7 @@ void FullscreenUI::SwitchToSettings() s_state.current_main_window = MainWindowType::Settings; s_state.settings_page = SettingsPage::Interface; - s_state.settings_last_bg_alpha = GetSettingsWindowBgAlpha(); + s_state.settings_last_bg_alpha = GetBackgroundAlpha(); } void FullscreenUI::SwitchToGameSettingsForSerial(std::string_view serial) @@ -3103,7 +3336,7 @@ void FullscreenUI::DrawSettingsWindow() ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f)); - const float target_bg_alpha = GetSettingsWindowBgAlpha(); + const float target_bg_alpha = GetBackgroundAlpha(); s_state.settings_last_bg_alpha = (target_bg_alpha < s_state.settings_last_bg_alpha) ? std::max(s_state.settings_last_bg_alpha - io.DeltaTime * 2.0f, target_bg_alpha) : std::min(s_state.settings_last_bg_alpha + io.DeltaTime * 2.0f, target_bg_alpha); @@ -3420,6 +3653,30 @@ void FullscreenUI::DrawInterfaceSettingsPage() FSUI_CSTR("Prevents the screen saver from activating and the host from sleeping while emulation is running."), "Main", "InhibitScreensaver", true); + if (const TinyString current_value = + bsi->GetTinyStringValue("Main", "FullscreenUIBackground", DEFAULT_BACKGROUND_NAME); + MenuButtonWithValue(FSUI_ICONSTR(ICON_FA_IMAGE, "Menu Background"), + FSUI_CSTR("Shows a background image or shader when a game isn't running. Backgrounds are " + "located in resources/fullscreenui in the data directory."), + current_value.c_str())) + { + ChoiceDialogOptions options = GetBackgroundOptions(current_value); + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_IMAGE, "Menu Background"), false, std::move(options), + [](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + SettingsInterface* bsi = GetEditingSettingsInterface(); + bsi->SetStringValue("Main", "FullscreenUIBackground", (index == 0) ? "None" : title.c_str()); + SetSettingsChanged(bsi); + + // Have to defer the reload, because we've already drawn the bg for this frame. + Host::RunOnCPUThread([]() { GPUThread::RunOnThread(&FullscreenUI::LoadBackground); }); + } + + CloseChoiceDialog(); + }); + } + if (DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_PAINT_BRUSH, "Use Light Theme"), FSUI_CSTR("Uses a light coloured theme instead of the default dark theme."), "Main", "UseLightFullscreenUITheme", false)) @@ -7026,10 +7283,8 @@ void FullscreenUI::DrawGameListWindow() ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f)); - const float bg_alpha = GPUThread::HasGPUBackend() ? 0.90f : 1.0f; - if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", - MulAlpha(UIStyle.PrimaryColor, bg_alpha))) + MulAlpha(UIStyle.PrimaryColor, GetBackgroundAlpha()))) { static constexpr float ITEM_WIDTH = 25.0f; static constexpr const char* icons[] = {ICON_FA_BORDER_ALL, ICON_FA_LIST}; @@ -7118,7 +7373,8 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) if (IsFocusResetFromWindowChange()) ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f)); - if (BeginFullscreenColumnWindow(0.0f, -530.0f, "game_list_entries")) + if (BeginFullscreenColumnWindow(0.0f, -530.0f, "game_list_entries", + ModAlpha(UIStyle.BackgroundColor, GetBackgroundAlpha()))) { const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT)); @@ -7194,7 +7450,8 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) EndFullscreenColumnWindow(); static constexpr float info_window_width = 530.0f; - if (BeginFullscreenColumnWindow(-info_window_width, 0.0f, "game_list_info", UIStyle.PrimaryDarkColor)) + if (BeginFullscreenColumnWindow(-info_window_width, 0.0f, "game_list_info", + ModAlpha(UIStyle.PrimaryDarkColor, GetBackgroundAlpha()))) { static constexpr float info_top_margin = 20.0f; static constexpr float cover_size = 320.0f; @@ -7337,7 +7594,7 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size) if (!BeginFullscreenWindow( ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "game_grid", - UIStyle.BackgroundColor)) + ModAlpha(UIStyle.BackgroundColor, GetBackgroundAlpha()))) { EndFullscreenWindow(); return; @@ -7583,10 +7840,8 @@ void FullscreenUI::DrawGameListSettingsWindow() ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + LayoutScale(2.0f)); - const float bg_alpha = GPUThread::HasGPUBackend() ? 0.90f : 1.0f; - if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", - MulAlpha(UIStyle.PrimaryColor, bg_alpha))) + MulAlpha(UIStyle.PrimaryColor, GetBackgroundAlpha()))) { BeginNavBar(); @@ -7605,7 +7860,8 @@ void FullscreenUI::DrawGameListSettingsWindow() if (!BeginFullscreenWindow( ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), - "settings_parent", UIStyle.BackgroundColor, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f))) + "settings_parent", MulAlpha(UIStyle.PrimaryColor, GetBackgroundAlpha()), 0.0f, + ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f))) { EndFullscreenWindow(); return; diff --git a/src/util/gpu_device.cpp b/src/util/gpu_device.cpp index a70576f97..b3e50608c 100644 --- a/src/util/gpu_device.cpp +++ b/src/util/gpu_device.cpp @@ -758,10 +758,12 @@ void GPUDevice::RenderImGui(GPUSwapChain* swap_chain) for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; - DebugAssert(!pcmd->UserCallback); - if (pcmd->ElemCount == 0 || pcmd->ClipRect.z <= pcmd->ClipRect.x || pcmd->ClipRect.w <= pcmd->ClipRect.y) + 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)); clip = swap_chain->PreRotateClipRect(clip); @@ -770,7 +772,17 @@ void GPUDevice::RenderImGui(GPUSwapChain* swap_chain) SetScissor(clip); SetTextureSampler(0, reinterpret_cast(pcmd->TextureId), m_linear_sampler.get()); - DrawIndexed(pcmd->ElemCount, base_index + pcmd->IdxOffset, base_vertex + pcmd->VtxOffset); + + 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); + } } } } diff --git a/src/util/gpu_device.h b/src/util/gpu_device.h index 5ed91d7c1..c832c8e76 100644 --- a/src/util/gpu_device.h +++ b/src/util/gpu_device.h @@ -512,6 +512,10 @@ public: ALWAYS_INLINE float GetScale() const { return m_window_info.surface_scale; } ALWAYS_INLINE WindowInfo::PreRotation GetPreRotation() const { return m_window_info.surface_prerotation; } ALWAYS_INLINE GPUTexture::Format GetFormat() const { return m_window_info.surface_format; } + ALWAYS_INLINE GSVector2i GetSizeVec() const + { + return GSVector2i(m_window_info.surface_width, m_window_info.surface_height); + } ALWAYS_INLINE GPUVSyncMode GetVSyncMode() const { return m_vsync_mode; } ALWAYS_INLINE bool IsVSyncModeBlocking() const { return (m_vsync_mode == GPUVSyncMode::FIFO); } diff --git a/src/util/imgui_fullscreen.cpp b/src/util/imgui_fullscreen.cpp index 8fe8ee5ed..119ae2276 100644 --- a/src/util/imgui_fullscreen.cpp +++ b/src/util/imgui_fullscreen.cpp @@ -497,6 +497,33 @@ ImRect ImGuiFullscreen::CenterImage(const ImRect& fit_rect, const ImVec2& image_ return ret; } +ImRect ImGuiFullscreen::FitImage(const ImVec2& fit_size, const ImVec2& image_size) +{ + ImRect rect; + + const float image_aspect = image_size.x / image_size.y; + const float screen_aspect = fit_size.x / fit_size.y; + + if (screen_aspect < image_aspect) + { + // Screen is narrower than image - crop horizontally + float cropAmount = 1.0f - (screen_aspect / image_aspect); + float offset = cropAmount * 0.5f; + rect.Min = ImVec2(offset, 0.0f); + rect.Max = ImVec2(1.0f - offset, 1.0f); + } + else + { + // Screen is wider than image - crop vertically + float cropAmount = 1.0f - (image_aspect / screen_aspect); + float offset = cropAmount * 0.5f; + rect.Min = ImVec2(0.0f, offset); + rect.Max = ImVec2(1.0f, 1.0f - offset); + } + + return rect; +} + void ImGuiFullscreen::BeginLayout() { // we evict from the texture cache at the start of the frame, in case we go over mid-frame, @@ -695,13 +722,15 @@ bool ImGuiFullscreen::BeginFullscreenColumns(const char* title, float pos_y, boo if (title) { ImGui::PushFont(UIStyle.LargeFont); - clipped = ImGui::Begin(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + clipped = ImGui::Begin(title, nullptr, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground); ImGui::PopFont(); } else { clipped = ImGui::Begin("fullscreen_ui_columns_parent", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoBackground); } return clipped; @@ -2053,7 +2082,8 @@ bool ImGuiFullscreen::NavTab(const char* title, bool is_active, bool enabled /* return pressed; } -bool ImGuiFullscreen::BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, u32 num_items) +bool ImGuiFullscreen::BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, + const ImVec4& bg_color, u32 num_items) { s_state.menu_button_index = 0; @@ -2068,7 +2098,7 @@ bool ImGuiFullscreen::BeginHorizontalMenu(const char* name, const ImVec2& positi ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(item_spacing, 0.0f)); - if (!BeginFullscreenWindow(position, size, name, UIStyle.BackgroundColor, 0.0f, ImVec2())) + if (!BeginFullscreenWindow(position, size, name, bg_color, 0.0f, ImVec2())) return false; ImGui::SetCursorPos(ImVec2((size.x - menu_width) * 0.5f, (size.y - menu_height) * 0.5f)); diff --git a/src/util/imgui_fullscreen.h b/src/util/imgui_fullscreen.h index ba7bc8651..cdea06bcf 100644 --- a/src/util/imgui_fullscreen.h +++ b/src/util/imgui_fullscreen.h @@ -121,6 +121,9 @@ ALWAYS_INLINE static std::string_view RemoveHash(std::string_view s) ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size); ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size); +/// Fits an image to the specified bounds, cropping if needed. Returns UV coordinates. +ImRect FitImage(const ImVec2& fit_size, const ImVec2& image_size); + /// Initializes, setting up any state. bool Initialize(const char* placeholder_image_path); @@ -284,7 +287,8 @@ bool NavButton(const char* title, bool is_active, bool enabled = true, float wid bool NavTab(const char* title, bool is_active, bool enabled, float width, float height, const ImVec4& background, ImFont* font = UIStyle.LargeFont); -bool BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, u32 num_items); +bool BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, const ImVec4& bg_color, + u32 num_items); void EndHorizontalMenu(); bool HorizontalMenuItem(GPUTexture* icon, const char* title, const char* description); diff --git a/src/util/imgui_glyph_ranges.inl b/src/util/imgui_glyph_ranges.inl index d1d663b3e..1be15bc4d 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,0xf03d,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,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 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/shadergen.h b/src/util/shadergen.h index 8685ec728..af730b5b1 100644 --- a/src/util/shadergen.h +++ b/src/util/shadergen.h @@ -24,6 +24,8 @@ public: static TinyString GetGLSLVersionString(RenderAPI render_api, u32 version); ALWAYS_INLINE GPUShaderLanguage GetLanguage() const { return m_shader_language; } + 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; @@ -36,10 +38,6 @@ public: std::string GenerateImGuiVertexShader() const; std::string GenerateImGuiFragmentShader() const; -protected: - ALWAYS_INLINE bool IsVulkan() const { return (m_render_api == RenderAPI::Vulkan); } - ALWAYS_INLINE bool IsMetal() const { return (m_render_api == RenderAPI::Metal); } - const char* GetInterpolationQualifier(bool interface_block, bool centroid_interpolation, bool sample_interpolation, bool is_out) const; @@ -68,6 +66,7 @@ protected: bool declare_sample_id = false, bool noperspective_color = false, bool feedback_loop = false, bool rov = false) const; +protected: RenderAPI m_render_api; GPUShaderLanguage m_shader_language; bool m_glsl;