// Copyright 2009 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/OnScreenDisplay.h" #include #include #include #include #include #include #include #include "Common/CommonTypes.h" #include "Common/Config/Config.h" #include "Common/Timer.h" #include "Core/Config/MainSettings.h" #include "VideoCommon/AbstractGfx.h" #include "VideoCommon/AbstractTexture.h" #include "VideoCommon/Assets/CustomTextureData.h" #include "VideoCommon/TextureConfig.h" namespace OSD { constexpr float LEFT_MARGIN = 10.0f; // Pixels to the left of OSD messages. constexpr float TOP_MARGIN = 10.0f; // Pixels above the first OSD message. constexpr float WINDOW_PADDING = 4.0f; // Pixels between subsequent OSD messages. constexpr float MESSAGE_FADE_TIME = 1000.f; // Ms to fade OSD messages at the end of their life. constexpr float MESSAGE_DROP_TIME = 5000.f; // Ms to drop OSD messages that has yet to ever render. static std::atomic s_obscured_pixels_left = 0; static std::atomic s_obscured_pixels_top = 0; struct Message { Message() = default; Message(std::string text_, u32 duration_, u32 color_, const VideoCommon::CustomTextureData::ArraySlice::Level* icon_ = nullptr) : text(std::move(text_)), duration(duration_), color(color_), icon(icon_) { timer.Start(); } s64 TimeRemaining() const { return duration - timer.ElapsedMs(); } std::string text; Common::Timer timer; u32 duration = 0; bool ever_drawn = false; bool should_discard = false; u32 color = 0; const VideoCommon::CustomTextureData::ArraySlice::Level* icon; std::unique_ptr texture; }; static std::multimap s_messages; static std::mutex s_messages_mutex; static ImVec4 ARGBToImVec4(const u32 argb) { return ImVec4(static_cast((argb >> 16) & 0xFF) / 255.0f, static_cast((argb >> 8) & 0xFF) / 255.0f, static_cast((argb >> 0) & 0xFF) / 255.0f, static_cast((argb >> 24) & 0xFF) / 255.0f); } static float DrawMessage(int index, Message& msg, const ImVec2& position, int time_left) { // We have to provide a window name, and these shouldn't be duplicated. // So instead, we generate a name based on the number of messages drawn. const std::string window_name = fmt::format("osd_{}", index); // The size must be reset, otherwise the length of old messages could influence new ones. ImGui::SetNextWindowPos(position); ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f)); // Gradually fade old messages away (except in their first frame) const float fade_time = std::max(std::min(MESSAGE_FADE_TIME, (float)msg.duration), 1.f); const float alpha = std::clamp(time_left / fade_time, 0.f, 1.f); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, msg.ever_drawn ? alpha : 1.0); float window_height = 0.0f; if (ImGui::Begin(window_name.c_str(), nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing)) { if (msg.icon) { if (!msg.texture) { const u32 width = msg.icon->width; const u32 height = msg.icon->height; TextureConfig tex_config(width, height, 1, 1, 1, AbstractTextureFormat::RGBA8, 0, AbstractTextureType::Texture_2DArray); msg.texture = g_gfx->CreateTexture(tex_config); if (msg.texture) { msg.texture->Load(0, width, height, width, msg.icon->data.data(), sizeof(u32) * width * height); } else { // don't try again next time msg.icon = nullptr; } } if (msg.texture) { ImGui::Image(msg.texture.get(), ImVec2(static_cast(msg.icon->width), static_cast(msg.icon->height))); ImGui::SameLine(); } } // Use %s in case message contains %. if (msg.text.size() > 0) ImGui::TextColored(ARGBToImVec4(msg.color), "%s", msg.text.c_str()); window_height = ImGui::GetWindowSize().y + (WINDOW_PADDING * ImGui::GetIO().DisplayFramebufferScale.y); } ImGui::End(); ImGui::PopStyleVar(); msg.ever_drawn = true; return window_height; } void AddTypedMessage(MessageType type, std::string message, u32 ms, u32 argb, const VideoCommon::CustomTextureData::ArraySlice::Level* icon) { std::lock_guard lock{s_messages_mutex}; // A message may hold a reference to a texture that can only be destroyed on the video thread, so // only mark the old typed message (if any) for removal. It will be discarded on the next call to // DrawMessages(). auto range = s_messages.equal_range(type); for (auto it = range.first; it != range.second; ++it) it->second.should_discard = true; s_messages.emplace(type, Message(std::move(message), ms, argb, std::move(icon))); } void AddMessage(std::string message, u32 ms, u32 argb, const VideoCommon::CustomTextureData::ArraySlice::Level* icon) { std::lock_guard lock{s_messages_mutex}; s_messages.emplace(MessageType::Typeless, Message(std::move(message), ms, argb, std::move(icon))); } void DrawMessages() { const bool draw_messages = Config::Get(Config::MAIN_OSD_MESSAGES); const float current_x = LEFT_MARGIN * ImGui::GetIO().DisplayFramebufferScale.x + s_obscured_pixels_left; float current_y = TOP_MARGIN * ImGui::GetIO().DisplayFramebufferScale.y + s_obscured_pixels_top; int index = 0; std::lock_guard lock{s_messages_mutex}; for (auto it = s_messages.begin(); it != s_messages.end();) { Message& msg = it->second; if (msg.should_discard) { it = s_messages.erase(it); continue; } const s64 time_left = msg.TimeRemaining(); // Make sure we draw them at least once if they were printed with 0ms, // unless enough time has expired, in that case, we drop them if (time_left <= 0 && (msg.ever_drawn || -time_left >= MESSAGE_DROP_TIME)) { it = s_messages.erase(it); continue; } else { ++it; } if (draw_messages) current_y += DrawMessage(index++, msg, ImVec2(current_x, current_y), time_left); } } void ClearMessages() { std::lock_guard lock{s_messages_mutex}; s_messages.clear(); } void SetObscuredPixelsLeft(int width) { s_obscured_pixels_left = width; } void SetObscuredPixelsTop(int height) { s_obscured_pixels_top = height; } } // namespace OSD