diff --git a/src/duckstation-qt/displaywidget.cpp b/src/duckstation-qt/displaywidget.cpp index 6ffbe8c9e..b2e42fd66 100644 --- a/src/duckstation-qt/displaywidget.cpp +++ b/src/duckstation-qt/displaywidget.cpp @@ -465,3 +465,167 @@ bool DisplayContainer::event(QEvent* event) return res; } + +AuxiliaryDisplayWidget::AuxiliaryDisplayWidget(QWidget* parent, u32 width, u32 height, const QString& title, + void* userdata) + : QWidget(parent), m_userdata(userdata) +{ + // We want a native window for both D3D and OpenGL. + setAutoFillBackground(false); + setAttribute(Qt::WA_NativeWindow, true); + setAttribute(Qt::WA_NoSystemBackground, true); + setAttribute(Qt::WA_PaintOnScreen, true); + setAttribute(Qt::WA_KeyCompression, false); + setFocusPolicy(Qt::StrongFocus); + setMouseTracking(true); + setWindowTitle(title); + resize(width, height); +} + +AuxiliaryDisplayWidget::~AuxiliaryDisplayWidget() = default; + +QPaintEngine* AuxiliaryDisplayWidget::paintEngine() const +{ + return nullptr; +} + +bool AuxiliaryDisplayWidget::event(QEvent* event) +{ + const QEvent::Type type = event->type(); + switch (type) + { + case QEvent::KeyPress: + case QEvent::KeyRelease: + { + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, + (type == QEvent::KeyPress) ? Host::AuxiliaryRenderWindowEvent::KeyPressed : + Host::AuxiliaryRenderWindowEvent::KeyReleased, + Host::AuxiliaryRenderWindowEventParam{.uint_param = + static_cast(static_cast(event)->key())}); + + return true; + } + + case QEvent::MouseMove: + { + const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this); + const QPoint mouse_pos = static_cast(event)->pos(); + const float scaled_x = static_cast(static_cast(mouse_pos.x()) * dpr); + const float scaled_y = static_cast(static_cast(mouse_pos.y()) * dpr); + + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, Host::AuxiliaryRenderWindowEvent::MouseMoved, + Host::AuxiliaryRenderWindowEventParam{.float_param = scaled_x}, + Host::AuxiliaryRenderWindowEventParam{.float_param = scaled_y}); + + return true; + } + + case QEvent::MouseButtonPress: + case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonRelease: + { + const u32 button_index = CountTrailingZeros(static_cast(static_cast(event)->button())); + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, + (type == QEvent::MouseButtonRelease) ? Host::AuxiliaryRenderWindowEvent::MouseReleased : + Host::AuxiliaryRenderWindowEvent::MousePressed, + Host::AuxiliaryRenderWindowEventParam{.uint_param = button_index}, + Host::AuxiliaryRenderWindowEventParam{.uint_param = BoolToUInt32(type == QEvent::MouseButtonRelease)}); + + return true; + } + + case QEvent::Wheel: + { + const QWheelEvent* wheel_event = static_cast(event); + const QPoint delta = wheel_event->angleDelta(); + if (delta.x() != 0 || delta.y()) + { + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, Host::AuxiliaryRenderWindowEvent::MouseWheel, + Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast(delta.x())}, + Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast(delta.y())}); + } + + return true; + } + + case QEvent::Close: + { + if (m_destroying) + return QWidget::event(event); + + g_emu_thread->queueAuxiliaryRenderWindowInputEvent(m_userdata, Host::AuxiliaryRenderWindowEvent::CloseRequest); + event->ignore(); + return true; + } + + case QEvent::Paint: + case QEvent::Resize: + { + QWidget::event(event); + + const float dpr = QtUtils::GetDevicePixelRatioForWidget(this); + const u32 scaled_width = + static_cast(std::max(static_cast(std::ceil(static_cast(width()) * dpr)), 1)); + const u32 scaled_height = + static_cast(std::max(static_cast(std::ceil(static_cast(height()) * dpr)), 1)); + + // avoid spamming resize events for paint events (sent on move on windows) + if (m_last_window_width != scaled_width || m_last_window_height != scaled_height || m_last_window_scale != dpr) + { + m_last_window_width = scaled_width; + m_last_window_height = scaled_height; + m_last_window_scale = dpr; + g_emu_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, Host::AuxiliaryRenderWindowEvent::Resized, + Host::AuxiliaryRenderWindowEventParam{.uint_param = scaled_width}, + Host::AuxiliaryRenderWindowEventParam{.uint_param = scaled_height}, + Host::AuxiliaryRenderWindowEventParam{.float_param = dpr}); + } + + return true; + } + + default: + return QWidget::event(event); + } +} + +AuxiliaryDisplayWidget* AuxiliaryDisplayWidget::create(s32 pos_x, s32 pos_y, u32 width, u32 height, + const QString& title, const QString& icon_name, void* userdata) +{ + // TODO: Wayland shit + QWidget* parent = nullptr; + AuxiliaryDisplayWidget* widget = new AuxiliaryDisplayWidget(parent, width, height, title, userdata); + if (!icon_name.isEmpty()) + { + if (const QIcon icon(icon_name); !icon.isNull()) + widget->setWindowIcon(icon); + else + widget->setWindowIcon(QtHost::GetAppIcon()); + } + else + { + widget->setWindowIcon(QtHost::GetAppIcon()); + } + + if (pos_x != std::numeric_limits::min() && pos_y != std::numeric_limits::min()) + widget->move(pos_x, pos_y); + + widget->show(); + return widget; +} + +void AuxiliaryDisplayWidget::destroy() +{ + m_destroying = true; + + QWidget* container = static_cast(parent()); + if (!container) + container = this; + container->close(); + container->deleteLater(); +} diff --git a/src/duckstation-qt/displaywidget.h b/src/duckstation-qt/displaywidget.h index 6efc141cc..8b9fc1bdf 100644 --- a/src/duckstation-qt/displaywidget.h +++ b/src/duckstation-qt/displaywidget.h @@ -89,3 +89,28 @@ protected: private: DisplayWidget* m_display_widget = nullptr; }; + +class AuxiliaryDisplayWidget final : public QWidget +{ + Q_OBJECT + +public: + explicit AuxiliaryDisplayWidget(QWidget* parent, u32 width, u32 height, const QString& title, void* userdata); + ~AuxiliaryDisplayWidget(); + + QPaintEngine* paintEngine() const override; + + static AuxiliaryDisplayWidget* create(s32 pos_x, s32 pos_y, u32 width, u32 height, const QString& title, + const QString& icon_name, void* userdata); + void destroy(); + +protected: + bool event(QEvent* event) override; + +private: + void* m_userdata = nullptr; + u32 m_last_window_width = 0; + u32 m_last_window_height = 0; + float m_last_window_scale = 1.0f; + bool m_destroying = false; +}; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 7e98c9011..2c1ac5972 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2044,6 +2044,10 @@ void MainWindow::connectSignals() connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this, &MainWindow::onAchievementsChallengeModeChanged); connect(g_emu_thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered); + connect(g_emu_thread, &EmuThread::onCreateAuxiliaryRenderWindow, this, &MainWindow::onCreateAuxiliaryRenderWindow, + Qt::BlockingQueuedConnection); + connect(g_emu_thread, &EmuThread::onDestroyAuxiliaryRenderWindow, this, &MainWindow::onDestroyAuxiliaryRenderWindow, + Qt::BlockingQueuedConnection); // These need to be queued connections to stop crashing due to menus opening/closing and switching focus. connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress); @@ -2642,6 +2646,36 @@ void MainWindow::onAchievementsChallengeModeChanged(bool enabled) updateEmulationActions(false, System::IsValid(), enabled); } +bool MainWindow::onCreateAuxiliaryRenderWindow(qint32 x, qint32 y, quint32 width, quint32 height, const QString& title, + const QString& icon_name, Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error) +{ + AuxiliaryDisplayWidget* widget = AuxiliaryDisplayWidget::create(x, y, width, height, title, icon_name, userdata); + if (!widget) + return false; + + const std::optional owi = QtUtils::GetWindowInfoForWidget(widget, error); + if (!owi.has_value()) + { + widget->destroy(); + return false; + } + + *handle = widget; + *wi = owi.value(); + return true; +} + +void MainWindow::onDestroyAuxiliaryRenderWindow(Host::AuxiliaryRenderWindowHandle handle, QPoint* pos, QSize* size) +{ + AuxiliaryDisplayWidget* widget = static_cast(handle); + DebugAssert(widget); + + *pos = widget->pos(); + *size = widget->size(); + widget->destroy(); +} + void MainWindow::onToolsMemoryCardEditorTriggered() { openMemoryCardEditor(QString(), QString()); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 20861c114..31e3bb510 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -10,6 +10,7 @@ #include "core/types.h" +#include "util/imgui_manager.h" #include "util/window_info.h" #include @@ -144,6 +145,11 @@ private Q_SLOTS: void onMediaCaptureStopped(); void onAchievementsLoginRequested(Achievements::LoginRequestReason reason); void onAchievementsChallengeModeChanged(bool enabled); + bool onCreateAuxiliaryRenderWindow(qint32 x, qint32 y, quint32 width, quint32 height, const QString& title, + const QString& icon_name, Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error); + void onDestroyAuxiliaryRenderWindow(Host::AuxiliaryRenderWindowHandle handle, QPoint* pos, QSize* size); + void onApplicationStateChanged(Qt::ApplicationState state); void onStartFileActionTriggered(); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 77c07e3b2..674043214 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1700,6 +1700,58 @@ void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directo }); } +bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title, + std::string_view icon_name, AuxiliaryRenderWindowUserData userdata, + AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error) +{ + return emit g_emu_thread->onCreateAuxiliaryRenderWindow(x, y, width, height, QtUtils::StringViewToQString(title), + QtUtils::StringViewToQString(icon_name), userdata, handle, wi, + error); +} + +void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x, s32* pos_y, u32* width, + u32* height) +{ + QPoint pos; + QSize size; + emit g_emu_thread->onDestroyAuxiliaryRenderWindow(handle, &pos, &size); + + if (pos_x) + *pos_x = pos.x(); + if (pos_y) + *pos_y = pos.y(); + if (width) + *width = size.width(); + if (height) + *height = size.height(); + + // eat all pending events, to make sure we're not going to write input events back to a dead pointer + g_emu_thread->getEventLoop()->processEvents(QEventLoop::AllEvents); +} + +void EmuThread::queueAuxiliaryRenderWindowInputEvent(Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowEvent event, + Host::AuxiliaryRenderWindowEventParam param1, + Host::AuxiliaryRenderWindowEventParam param2, + Host::AuxiliaryRenderWindowEventParam param3) +{ + DebugAssert(isOnUIThread()); + QMetaObject::invokeMethod(this, "processAuxiliaryRenderWindowInputEvent", Qt::QueuedConnection, + Q_ARG(void*, userdata), Q_ARG(quint32, static_cast(event)), + Q_ARG(quint32, param1.uint_param), Q_ARG(quint32, param2.uint_param), + Q_ARG(quint32, param3.uint_param)); +} + +void EmuThread::processAuxiliaryRenderWindowInputEvent(void* userdata, quint32 event, quint32 param1, quint32 param2, + quint32 param3) +{ + DebugAssert(isOnThread()); + ImGuiManager::ProcessAuxiliaryRenderWindowInputEvent(userdata, static_cast(event), + Host::AuxiliaryRenderWindowEventParam{.uint_param = param1}, + Host::AuxiliaryRenderWindowEventParam{.uint_param = param2}, + Host::AuxiliaryRenderWindowEventParam{.uint_param = param3}); +} + void EmuThread::doBackgroundControllerPoll() { System::Internal::IdlePollUpdate(); diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index a44e8de65..b48ca36ce 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -11,6 +11,7 @@ #include "core/types.h" #include "util/gpu_device.h" +#include "util/imgui_manager.h" #include "util/input_manager.h" #include @@ -119,6 +120,13 @@ public: /// This version is **only** for the system thread. UI thread should use the MainWindow variant. SystemLock pauseAndLockSystem(); + /// Queues an input event for an additional render window to the emu thread. + void queueAuxiliaryRenderWindowInputEvent(Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowEvent event, + Host::AuxiliaryRenderWindowEventParam param1 = {}, + Host::AuxiliaryRenderWindowEventParam param2 = {}, + Host::AuxiliaryRenderWindowEventParam param3 = {}); + Q_SIGNALS: void errorReported(const QString& title, const QString& message); bool messageConfirmed(const QString& title, const QString& message); @@ -151,6 +159,11 @@ Q_SIGNALS: void mediaCaptureStarted(); void mediaCaptureStopped(); + bool onCreateAuxiliaryRenderWindow(qint32 x, qint32 y, quint32 width, quint32 height, const QString& title, + const QString& icon_name, Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error); + void onDestroyAuxiliaryRenderWindow(Host::AuxiliaryRenderWindowHandle handle, QPoint* pos, QSize* size); + /// Big Picture UI requests. void onCoverDownloaderOpenRequested(); @@ -210,6 +223,8 @@ private Q_SLOTS: void onDisplayWindowTextEntered(const QString& text); void doBackgroundControllerPoll(); void runOnEmuThread(std::function callback); + void processAuxiliaryRenderWindowInputEvent(void* userdata, quint32 event, quint32 param1, quint32 param2, + quint32 param3); protected: void run() override; diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index b3cdd2c25..02e3693ed 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -9,6 +9,9 @@ #include "imgui_glyph_ranges.inl" #include "input_manager.h" +// TODO: Remove me when GPUDevice config is also cleaned up. +#include "core/host.h" + #include "common/assert.h" #include "common/easing.h" #include "common/error.h" @@ -64,7 +67,7 @@ struct OSDMessage static_assert(std::is_same_v); static void UpdateScale(); -static void SetStyle(); +static void SetStyle(ImGuiStyle& style, float scale); static void SetKeyMap(); static bool LoadFontData(); static void ReloadFontDataIfActive(); @@ -91,6 +94,7 @@ static std::string s_font_path; static std::vector s_font_range; static std::vector s_emoji_range; +static ImGuiContext* s_imgui_context; static ImFont* s_standard_font; static ImFont* s_osd_font; static ImFont* s_fixed_font; @@ -235,9 +239,9 @@ bool ImGuiManager::Initialize(float global_scale, Error* error) (g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetScale() : 1.0f) * global_scale, 1.0f); s_scale_changed = false; - ImGui::CreateContext(); + s_imgui_context = ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); + ImGuiIO& io = s_imgui_context->IO; io.IniFilename = nullptr; io.BackendFlags |= ImGuiBackendFlags_HasGamepad | ImGuiBackendFlags_RendererHasVtxOffset; io.BackendUsingLegacyKeyArrays = 0; @@ -259,7 +263,7 @@ bool ImGuiManager::Initialize(float global_scale, Error* error) io.DisplaySize = ImVec2(s_window_width, s_window_height); SetKeyMap(); - SetStyle(); + SetStyle(s_imgui_context->Style, s_global_scale); if (!AddImGuiFonts(false) || !g_gpu_device->UpdateImGuiFontTexture()) { @@ -269,7 +273,7 @@ bool ImGuiManager::Initialize(float global_scale, Error* error) } // don't need the font data anymore, save some memory - ImGui::GetIO().Fonts->ClearTexData(); + io.Fonts->ClearTexData(); NewFrame(); @@ -281,8 +285,11 @@ void ImGuiManager::Shutdown() { DestroySoftwareCursorTextures(); - if (ImGui::GetCurrentContext()) - ImGui::DestroyContext(); + if (s_imgui_context) + { + ImGui::DestroyContext(s_imgui_context); + s_imgui_context = nullptr; + } s_standard_font = nullptr; s_fixed_font = nullptr; @@ -291,6 +298,11 @@ void ImGuiManager::Shutdown() ImGuiFullscreen::SetFonts(nullptr, nullptr); } +ImGuiContext* ImGuiManager::GetMainContext() +{ + return s_imgui_context; +} + float ImGuiManager::GetWindowWidth() { return s_window_width; @@ -327,11 +339,7 @@ void ImGuiManager::UpdateScale() return; s_global_scale = scale; - - ImGui::GetStyle() = ImGuiStyle(); - ImGui::GetStyle().WindowMinSize = ImVec2(1.0f, 1.0f); - SetStyle(); - ImGui::GetStyle().ScaleAllSizes(scale); + SetStyle(s_imgui_context->Style, s_global_scale); if (!AddImGuiFonts(HasFullscreenFonts())) Panic("Failed to create ImGui font text"); @@ -360,9 +368,8 @@ void ImGuiManager::NewFrame() s_imgui_wants_mouse.store(io.WantCaptureMouse, std::memory_order_release); } -void ImGuiManager::SetStyle() +void ImGuiManager::SetStyle(ImGuiStyle& style, float scale) { - ImGuiStyle& style = ImGui::GetStyle(); style = ImGuiStyle(); style.WindowMinSize = ImVec2(1.0f, 1.0f); @@ -416,7 +423,7 @@ void ImGuiManager::SetStyle() colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); - style.ScaleAllSizes(s_global_scale); + style.ScaleAllSizes(scale); } void ImGuiManager::SetKeyMap() @@ -713,7 +720,7 @@ bool ImGuiManager::AddImGuiFonts(bool fullscreen_fonts) void ImGuiManager::ReloadFontDataIfActive() { - if (!ImGui::GetCurrentContext()) + if (!s_imgui_context) return; ImGui::EndFrame(); @@ -1054,43 +1061,43 @@ bool ImGuiManager::WantsMouseInput() void ImGuiManager::AddTextInput(std::string str) { - if (!ImGui::GetCurrentContext()) + if (!s_imgui_context) return; if (!s_imgui_wants_keyboard.load(std::memory_order_acquire)) return; - ImGui::GetIO().AddInputCharactersUTF8(str.c_str()); + s_imgui_context->IO.AddInputCharactersUTF8(str.c_str()); } void ImGuiManager::UpdateMousePosition(float x, float y) { - if (!ImGui::GetCurrentContext()) + if (!s_imgui_context) return; - ImGui::GetIO().MousePos = ImVec2(x, y); + s_imgui_context->IO.MousePos = ImVec2(x, y); std::atomic_thread_fence(std::memory_order_release); } bool ImGuiManager::ProcessPointerButtonEvent(InputBindingKey key, float value) { - if (!ImGui::GetCurrentContext() || key.data >= std::size(ImGui::GetIO().MouseDown)) + if (!s_imgui_context || key.data >= std::size(ImGui::GetIO().MouseDown)) return false; // still update state anyway - ImGui::GetIO().AddMouseButtonEvent(key.data, value != 0.0f); + s_imgui_context->IO.AddMouseButtonEvent(key.data, value != 0.0f); return s_imgui_wants_mouse.load(std::memory_order_acquire); } bool ImGuiManager::ProcessPointerAxisEvent(InputBindingKey key, float value) { - if (!ImGui::GetCurrentContext() || key.data < static_cast(InputPointerAxis::WheelX)) + if (!s_imgui_context || key.data < static_cast(InputPointerAxis::WheelX)) return false; // still update state anyway const bool horizontal = (key.data == static_cast(InputPointerAxis::WheelX)); - ImGui::GetIO().AddMouseWheelEvent(horizontal ? value : 0.0f, horizontal ? 0.0f : value); + s_imgui_context->IO.AddMouseWheelEvent(horizontal ? value : 0.0f, horizontal ? 0.0f : value); return s_imgui_wants_mouse.load(std::memory_order_acquire); } @@ -1098,11 +1105,11 @@ bool ImGuiManager::ProcessPointerAxisEvent(InputBindingKey key, float value) bool ImGuiManager::ProcessHostKeyEvent(InputBindingKey key, float value) { decltype(s_imgui_key_map)::iterator iter; - if (!ImGui::GetCurrentContext() || (iter = s_imgui_key_map.find(key.data)) == s_imgui_key_map.end()) + if (!s_imgui_context || (iter = s_imgui_key_map.find(key.data)) == s_imgui_key_map.end()) return false; // still update state anyway - ImGui::GetIO().AddKeyEvent(iter->second, value != 0.0); + s_imgui_context->IO.AddKeyEvent(iter->second, value != 0.0); return s_imgui_wants_keyboard.load(std::memory_order_acquire); } @@ -1138,13 +1145,13 @@ bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value ImGuiKey_GamepadL2, // R2 }; - if (!ImGui::GetCurrentContext()) + if (!s_imgui_context) return false; if (static_cast(key) >= std::size(key_map) || key_map[static_cast(key)] == ImGuiKey_None) return false; - ImGui::GetIO().AddKeyAnalogEvent(key_map[static_cast(key)], (value > 0.0f), value); + s_imgui_context->IO.AddKeyAnalogEvent(key_map[static_cast(key)], (value > 0.0f), value); return s_imgui_wants_keyboard.load(std::memory_order_acquire); } @@ -1275,3 +1282,213 @@ std::string ImGuiManager::StripIconCharacters(std::string_view str) return result; } + +#ifndef __ANDROID__ + +bool ImGuiManager::CreateAuxiliaryRenderWindow(AuxiliaryRenderWindowState* state, std::string_view title, + std::string_view icon_name, const char* config_section, + const char* config_prefix, u32 default_width, u32 default_height, + Error* error) +{ + constexpr s32 DEFAULT_POSITION = std::numeric_limits::min(); + + // figure out where to position it + s32 pos_x = DEFAULT_POSITION; + s32 pos_y = DEFAULT_POSITION; + u32 width = default_width; + u32 height = default_height; + if (config_prefix) + { + pos_x = Host::GetBaseIntSettingValue(config_section, TinyString::from_format("{}PositionX", config_prefix), + DEFAULT_POSITION); + pos_y = Host::GetBaseIntSettingValue(config_section, TinyString::from_format("{}PositionY", config_prefix), + DEFAULT_POSITION); + width = + Host::GetBaseUIntSettingValue(config_section, TinyString::from_format("{}Width", config_prefix), default_width); + height = + Host::GetBaseUIntSettingValue(config_section, TinyString::from_format("{}Height", config_prefix), default_height); + } + + WindowInfo wi; + if (!Host::CreateAuxiliaryRenderWindow(pos_x, pos_y, width, height, title, icon_name, state, &state->window_handle, + &wi, error)) + { + return false; + } + + state->swap_chain = g_gpu_device->CreateSwapChain(wi, GPUVSyncMode::Disabled, false, nullptr, std::nullopt, error); + if (!state->swap_chain) + { + Host::DestroyAuxiliaryRenderWindow(state->window_handle); + state->window_handle = nullptr; + return false; + } + + state->imgui_context = ImGui::CreateContext(s_imgui_context->IO.Fonts); + state->imgui_context->IO.DisplaySize = + ImVec2(static_cast(state->swap_chain->GetWidth()), static_cast(state->swap_chain->GetHeight())); + state->imgui_context->IO.IniFilename = nullptr; + state->imgui_context->IO.BackendFlags |= ImGuiBackendFlags_HasGamepad; + state->imgui_context->IO.BackendUsingLegacyKeyArrays = 0; + state->imgui_context->IO.BackendUsingLegacyNavInputArray = 0; + state->imgui_context->IO.KeyRepeatDelay = 0.5f; + state->imgui_context->IO.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + + SetStyle(state->imgui_context->Style, state->swap_chain->GetScale()); + state->imgui_context->Style.WindowBorderSize = 0.0f; + + state->close_request = false; + return true; +} + +void ImGuiManager::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowState* state, const char* config_section, + const char* config_prefix) +{ + constexpr s32 DEFAULT_POSITION = std::numeric_limits::min(); + + if (!state->window_handle) + return; + + s32 old_pos_x = DEFAULT_POSITION, old_pos_y = DEFAULT_POSITION; + u32 old_width = 0, old_height = 0; + if (config_section) + { + old_pos_x = Host::GetBaseIntSettingValue(config_section, TinyString::from_format("{}PositionX", config_prefix), + DEFAULT_POSITION); + old_pos_y = Host::GetBaseIntSettingValue(config_section, TinyString::from_format("{}PositionY", config_prefix), + DEFAULT_POSITION); + old_width = Host::GetBaseUIntSettingValue(config_section, TinyString::from_format("{}Width", config_prefix), 0); + old_height = Host::GetBaseUIntSettingValue(config_section, TinyString::from_format("{}Height", config_prefix), 0); + } + + ImGui::DestroyContext(state->imgui_context); + state->imgui_context = nullptr; + state->swap_chain.reset(); + state->close_request = false; + + // store positioning for config + s32 new_pos_x = old_pos_x, new_pos_y = old_pos_y; + u32 new_width = old_width, new_height = old_height; + Host::DestroyAuxiliaryRenderWindow(std::exchange(state->window_handle, nullptr), &new_pos_x, &new_pos_y, &new_width, + &new_height); + + if (config_section) + { + // update config if the window was moved + if (old_pos_x != new_pos_x || old_pos_y != new_pos_y || old_width != new_width || old_height != new_height) + { + Host::SetBaseIntSettingValue(config_section, TinyString::from_format("{}PositionX", config_prefix), new_pos_x); + Host::SetBaseIntSettingValue(config_section, TinyString::from_format("{}PositionY", config_prefix), new_pos_y); + Host::SetBaseUIntSettingValue(config_section, TinyString::from_format("{}Width", config_prefix), new_width); + Host::SetBaseUIntSettingValue(config_section, TinyString::from_format("{}Height", config_prefix), new_height); + Host::CommitBaseSettingChanges(); + } + } +} + +bool ImGuiManager::RenderAuxiliaryRenderWindow(AuxiliaryRenderWindowState* state, void (*draw_callback)(float scale)) +{ + DebugAssert(state->window_handle); + if (state->close_request) + return false; + + ImGui::SetCurrentContext(state->imgui_context); + + ImGui::NewFrame(); + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(state->imgui_context->IO.DisplaySize, ImGuiCond_Always); + if (ImGui::Begin("AuxRenderWindowMain", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse)) + { + draw_callback(state->swap_chain->GetScale()); + } + + ImGui::End(); + + const GPUDevice::PresentResult pres = g_gpu_device->BeginPresent(state->swap_chain.get()); + if (pres == GPUDevice::PresentResult::OK) + { + g_gpu_device->RenderImGui(state->swap_chain.get()); + g_gpu_device->EndPresent(state->swap_chain.get(), false); + } + else + { + ImGui::EndFrame(); + } + + ImGui::SetCurrentContext(GetMainContext()); + return true; +} + +void ImGuiManager::ProcessAuxiliaryRenderWindowInputEvent(Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowEvent event, + Host::AuxiliaryRenderWindowEventParam param1, + Host::AuxiliaryRenderWindowEventParam param2, + Host::AuxiliaryRenderWindowEventParam param3) +{ + // we can get bogus events here after the user closes it, so check we're not being destroyed + AuxiliaryRenderWindowState* state = static_cast(userdata); + if (!state->window_handle) [[unlikely]] + return; + + ImGuiIO& io = state->imgui_context->IO; + + switch (event) + { + case Host::AuxiliaryRenderWindowEvent::CloseRequest: + { + state->close_request = true; + } + break; + + case Host::AuxiliaryRenderWindowEvent::Resized: + { + Error error; + if (!state->swap_chain->ResizeBuffers(param1.uint_param, param2.uint_param, param3.float_param, &error)) + { + ERROR_LOG("Failed to resize aux window swap chain to {}x{}: {}", param1.uint_param, param2.uint_param, + error.GetDescription()); + return; + } + + state->imgui_context->IO.DisplaySize.x = static_cast(param1.uint_param); + state->imgui_context->IO.DisplaySize.y = static_cast(param2.uint_param); + } + break; + + case Host::AuxiliaryRenderWindowEvent::KeyPressed: + case Host::AuxiliaryRenderWindowEvent::KeyReleased: + { + const auto iter = s_imgui_key_map.find(param1.uint_param); + if (iter != s_imgui_key_map.end()) + io.AddKeyEvent(iter->second, (event == Host::AuxiliaryRenderWindowEvent::KeyPressed)); + } + break; + + case Host::AuxiliaryRenderWindowEvent::MouseMoved: + { + io.MousePos.x = param1.float_param; + io.MousePos.y = param2.float_param; + } + break; + + case Host::AuxiliaryRenderWindowEvent::MousePressed: + case Host::AuxiliaryRenderWindowEvent::MouseReleased: + { + io.AddMouseButtonEvent(param1.uint_param, (event == Host::AuxiliaryRenderWindowEvent::MousePressed)); + } + break; + + case Host::AuxiliaryRenderWindowEvent::MouseWheel: + { + io.AddMouseWheelEvent(param1.float_param, param2.float_param); + } + break; + + default: + break; + } +} + +#endif // __ANDROID__ diff --git a/src/util/imgui_manager.h b/src/util/imgui_manager.h index e13a416d2..adab6d468 100644 --- a/src/util/imgui_manager.h +++ b/src/util/imgui_manager.h @@ -4,17 +4,51 @@ #pragma once #include "common/types.h" + +#include #include #include #include class Error; +struct WindowInfo; +class GPUSwapChain; + +struct ImGuiContext; struct ImFont; union InputBindingKey; enum class GenericInputBinding : u8; +namespace Host { + +/// Handle representing an auxiliary render window in the host. +using AuxiliaryRenderWindowHandle = void*; +using AuxiliaryRenderWindowUserData = void*; + +enum class AuxiliaryRenderWindowEvent : u8 +{ + CloseRequest, + Resized, + KeyPressed, + KeyReleased, + MouseMoved, + MousePressed, + MouseReleased, + MouseWheel, + MaxCount, +}; + +union AuxiliaryRenderWindowEventParam +{ + s32 int_param; + u32 uint_param; + float float_param; +}; + +} // namespace Host + namespace ImGuiManager { using WCharType = u32; @@ -42,6 +76,9 @@ bool Initialize(float global_scale, Error* error); /// Frees all ImGui resources. void Shutdown(); +/// Returns main ImGui context. +ImGuiContext* GetMainContext(); + /// Returns the size of the display window. Can be safely called from any thread. float GetWindowWidth(); float GetWindowHeight(); @@ -124,9 +161,45 @@ void RenderSoftwareCursors(); /// Strips icon characters from a string. std::string StripIconCharacters(std::string_view str); + +#ifndef __ANDROID__ + +/// Auxiliary imgui windows. +struct AuxiliaryRenderWindowState +{ + Host::AuxiliaryRenderWindowHandle window_handle = nullptr; + std::unique_ptr swap_chain; + ImGuiContext* imgui_context = nullptr; + bool close_request = false; +}; + +/// Create a new aux render window. This creates the window itself, swap chain, and imgui context. +/// Window position and dimensions are restored from the configuration file, under the specified section/key. +bool CreateAuxiliaryRenderWindow(AuxiliaryRenderWindowState* state, std::string_view title, std::string_view icon_name, + const char* config_section, const char* config_prefix, u32 default_width, + u32 default_height, Error* error); + +/// Destroys a previously-created aux render window, optionally saving its position information. +void DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowState* state, const char* config_section = nullptr, + const char* config_prefix = nullptr); + +/// Renders the specified aux render window. draw_callback will be invoked if the window is not hidden. +/// Returns false if the user has closed the window. +bool RenderAuxiliaryRenderWindow(AuxiliaryRenderWindowState* state, void (*draw_callback)(float scale)); + +/// Processes input events from the host. +void ProcessAuxiliaryRenderWindowInputEvent(Host::AuxiliaryRenderWindowUserData userdata, + Host::AuxiliaryRenderWindowEvent event, + Host::AuxiliaryRenderWindowEventParam param1, + Host::AuxiliaryRenderWindowEventParam param2, + Host::AuxiliaryRenderWindowEventParam param3); + +#endif + } // namespace ImGuiManager namespace Host { + /// Typical durations for OSD messages. static constexpr float OSD_CRITICAL_ERROR_DURATION = 20.0f; static constexpr float OSD_ERROR_DURATION = 15.0f; @@ -143,4 +216,16 @@ void AddIconOSDWarning(std::string key, const char* icon, std::string message, f void RemoveKeyedOSDMessage(std::string key); void RemoveKeyedOSDWarning(std::string key); void ClearOSDMessages(bool clear_warnings); + +#ifndef __ANDROID__ + +/// Auxiliary window management. +bool CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title, + std::string_view icon_name, AuxiliaryRenderWindowUserData userdata, + AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error); +void DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x = nullptr, s32* pos_y = nullptr, + u32* width = nullptr, u32* height = nullptr); + +#endif + } // namespace Host