diff --git a/.gitmodules b/.gitmodules index 95b0fc0b..be4c1851 100644 --- a/.gitmodules +++ b/.gitmodules @@ -85,3 +85,8 @@ [submodule "externals/half"] path = externals/half url = https://github.com/ROCm/half.git +[submodule "externals/dear_imgui"] + path = externals/dear_imgui + url = https://github.com/shadps4-emu/ext-imgui.git + shallow = true + branch = docking diff --git a/CMakeLists.txt b/CMakeLists.txt index 2da9465c..4987b96e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -561,6 +561,18 @@ set(VIDEO_CORE src/video_core/amdgpu/liverpool.cpp src/video_core/renderdoc.h ) +set(IMGUI src/imgui/imgui_config.h + src/imgui/imgui_layer.h + src/imgui/layer/video_info.cpp + src/imgui/layer/video_info.h + src/imgui/renderer/imgui_core.cpp + src/imgui/renderer/imgui_core.h + src/imgui/renderer/imgui_impl_sdl3.cpp + src/imgui/renderer/imgui_impl_sdl3.h + src/imgui/renderer/imgui_impl_vulkan.cpp + src/imgui/renderer/imgui_impl_vulkan.h +) + set(INPUT src/input/controller.cpp src/input/controller.h ) @@ -617,6 +629,7 @@ endif() if (ENABLE_QT_GUI) qt_add_executable(shadps4 ${AUDIO_CORE} + ${IMGUI} ${INPUT} ${QT_GUI} ${COMMON} @@ -629,6 +642,7 @@ if (ENABLE_QT_GUI) else() add_executable(shadps4 ${AUDIO_CORE} + ${IMGUI} ${INPUT} ${COMMON} ${CORE} @@ -645,9 +659,12 @@ endif() create_target_directory_groups(shadps4) -target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg) +target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui) target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3) +target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") +target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") + if (APPLE) option(USE_SYSTEM_VULKAN_LOADER "Enables using the system Vulkan loader instead of directly linking with MoltenVK. Useful for loading validation layers." OFF) if (USE_SYSTEM_VULKAN_LOADER) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index de0317ff..b3ba2134 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -155,6 +155,17 @@ if (APPLE) endif() endif() +# Dear ImGui +add_library(Dear_ImGui + dear_imgui/imgui.cpp + dear_imgui/imgui_demo.cpp + dear_imgui/imgui_draw.cpp + dear_imgui/imgui_internal.h + dear_imgui/imgui_tables.cpp + dear_imgui/imgui_widgets.cpp +) +target_include_directories(Dear_ImGui INTERFACE dear_imgui/) + # Tracy option(TRACY_ENABLE "" ON) option(TRACY_NO_CRASH_HANDLER "" ON) # Otherwise texture cache exceptions will be treaten as a crash diff --git a/externals/dear_imgui b/externals/dear_imgui new file mode 160000 index 00000000..636cd4a7 --- /dev/null +++ b/externals/dear_imgui @@ -0,0 +1 @@ +Subproject commit 636cd4a7d623a2bc9bf59bb3acbb4ca075befba3 diff --git a/src/common/assert.cpp b/src/common/assert.cpp index 3a49c939..78c6ec07 100644 --- a/src/common/assert.cpp +++ b/src/common/assert.cpp @@ -18,3 +18,8 @@ void assert_fail_impl() { Crash(); throw std::runtime_error("Unreachable code"); } + +void assert_fail_debug_msg(const char* msg) { + LOG_CRITICAL(Debug, "Assertion Failed!\n{}", msg); + assert_fail_impl(); +} diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index ab3468ca..3257a601 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -117,6 +117,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Render) \ SUB(Render, Vulkan) \ SUB(Render, Recompiler) \ + CLS(ImGui) \ CLS(Input) \ CLS(Tty) \ CLS(Loader) diff --git a/src/common/logging/types.h b/src/common/logging/types.h index dd2376ea..dbae836c 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -84,6 +84,7 @@ enum class Class : u8 { Render, ///< Video Core Render_Vulkan, ///< Vulkan backend Render_Recompiler, ///< Shader recompiler + ImGui, ///< ImGui Loader, ///< ROM loader Input, ///< Input emulation Tty, ///< Debug output from emu diff --git a/src/core/libraries/videoout/driver.cpp b/src/core/libraries/videoout/driver.cpp index 91694cfa..27fe773b 100644 --- a/src/core/libraries/videoout/driver.cpp +++ b/src/core/libraries/videoout/driver.cpp @@ -161,10 +161,6 @@ int VideoOutDriver::UnregisterBuffers(VideoOutPort* port, s32 attributeIndex) { } std::chrono::microseconds VideoOutDriver::Flip(const Request& req) { - if (!req) { - return std::chrono::microseconds{0}; - } - const auto start = std::chrono::high_resolution_clock::now(); // Whatever the game is rendering show splash if it is active @@ -207,6 +203,11 @@ std::chrono::microseconds VideoOutDriver::Flip(const Request& req) { return std::chrono::duration_cast(end - start); } +void VideoOutDriver::DrawBlankFrame() { + const auto empty_frame = renderer->PrepareBlankFrame(false); + renderer->Present(empty_frame); +} + bool VideoOutDriver::SubmitFlip(VideoOutPort* port, s32 index, s64 flip_arg, bool is_eop /*= false*/) { { @@ -283,7 +284,14 @@ void VideoOutDriver::PresentThread(std::stop_token token) { auto& vblank_status = main_port.vblank_status; if (vblank_status.count % (main_port.flip_rate + 1) == 0) { const auto request = receive_request(); - delay = Flip(request); + if (!request) { + delay = std::chrono::microseconds{0}; + if (!main_port.is_open) { + DrawBlankFrame(); + } + } else { + delay = Flip(request); + } FRAME_END; } diff --git a/src/core/libraries/videoout/driver.h b/src/core/libraries/videoout/driver.h index 6fc74e01..141294bf 100644 --- a/src/core/libraries/videoout/driver.h +++ b/src/core/libraries/videoout/driver.h @@ -102,6 +102,7 @@ private: }; std::chrono::microseconds Flip(const Request& req); + void DrawBlankFrame(); // Used when there is no flip request to keep ImGui up to date void SubmitFlipInternal(VideoOutPort* port, s32 index, s64 flip_arg, bool is_eop = false); void PresentThread(std::stop_token token); diff --git a/src/imgui/imgui_config.h b/src/imgui/imgui_config.h new file mode 100644 index 00000000..4602382e --- /dev/null +++ b/src/imgui/imgui_config.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +// WARNING: All includes from this file must be relative to allow Dear_ImGui project to compile +// without having this project include paths. + +#include + +extern void assert_fail_debug_msg(const char* msg); + +#define ImDrawIdx std::uint32_t + +#define IM_STRINGIZE(x) IM_STRINGIZE2(x) +#define IM_STRINGIZE2(x) #x +#define IM_ASSERT(_EXPR) \ + ([&]() { \ + if (!(_EXPR)) [[unlikely]] { \ + assert_fail_debug_msg(#_EXPR " at " __FILE__ ":" IM_STRINGIZE(__LINE__)); \ + } \ + }()) + +#define IMGUI_USE_WCHAR32 +#define IMGUI_ENABLE_STB_TRUETYPE +#define IMGUI_DEFINE_MATH_OPERATORS + +#define IM_VEC2_CLASS_EXTRA \ + constexpr ImVec2(float _v) : x(_v), y(_v) {} \ No newline at end of file diff --git a/src/imgui/imgui_layer.h b/src/imgui/imgui_layer.h new file mode 100644 index 00000000..a2ec7fd2 --- /dev/null +++ b/src/imgui/imgui_layer.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace ImGui { + +class Layer { +public: + virtual ~Layer() = default; + static void AddLayer(Layer* layer); + static void RemoveLayer(Layer* layer); + + virtual void Draw() = 0; + + virtual bool ShouldGrabGamepad() { + return false; + } +}; + +} // namespace ImGui \ No newline at end of file diff --git a/src/imgui/layer/video_info.cpp b/src/imgui/layer/video_info.cpp new file mode 100644 index 00000000..15226cd8 --- /dev/null +++ b/src/imgui/layer/video_info.cpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "video_info.h" + +void ImGui::Layers::VideoInfo::Draw() { + const ImGuiIO& io = GetIO(); + + m_show = IsKeyPressed(ImGuiKey_F10, false) ^ m_show; + + if (m_show && Begin("Video Info")) { + Text("Frame time: %.3f ms (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + End(); + } +} diff --git a/src/imgui/layer/video_info.h b/src/imgui/layer/video_info.h new file mode 100644 index 00000000..8eec972a --- /dev/null +++ b/src/imgui/layer/video_info.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "imgui/imgui_layer.h" + +namespace Vulkan { +class RendererVulkan; +} +namespace ImGui::Layers { + +class VideoInfo : public Layer { + bool m_show = false; + ::Vulkan::RendererVulkan* renderer{}; + +public: + explicit VideoInfo(::Vulkan::RendererVulkan* renderer) : renderer(renderer) {} + + void Draw() override; +}; + +} // namespace ImGui::Layers diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp new file mode 100644 index 00000000..26b732c1 --- /dev/null +++ b/src/imgui/renderer/imgui_core.cpp @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/config.h" +#include "common/path_util.h" +#include "imgui/imgui_layer.h" +#include "imgui_core.h" +#include "imgui_impl_sdl3.h" +#include "imgui_impl_vulkan.h" +#include "sdl_window.h" +#include "video_core/renderer_vulkan/renderer_vulkan.h" + +static void CheckVkResult(const vk::Result err) { + LOG_ERROR(ImGui, "Vulkan error {}", vk::to_string(err)); +} + +static std::vector layers; + +// Update layers before rendering to allow layer changes to be applied during rendering. +// Using deque to keep the order of changes in case a Layer is removed then added again between +// frames. +static std::deque> change_layers; +static std::mutex change_layers_mutex{}; + +namespace ImGui { + +namespace Core { + +void Initialize(const ::Vulkan::Instance& instance, const Frontend::WindowSDL& window, + const u32 image_count, vk::Format surface_format, + const vk::AllocationCallbacks* allocator) { + + const auto config_path = GetUserPath(Common::FS::PathType::UserDir) / "imgui.ini"; + const auto log_path = GetUserPath(Common::FS::PathType::LogDir) / "imgui_log.txt"; + + CreateContext(); + ImGuiIO& io = GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.DisplaySize = ImVec2((float)window.getWidth(), (float)window.getHeight()); + io.IniFilename = SDL_strdup(config_path.string().c_str()); + io.LogFilename = SDL_strdup(log_path.string().c_str()); + StyleColorsDark(); + + Sdl::Init(window.GetSdlWindow()); + + const Vulkan::InitInfo vk_info{ + .instance = instance.GetInstance(), + .physical_device = instance.GetPhysicalDevice(), + .device = instance.GetDevice(), + .queue_family = instance.GetPresentQueueFamilyIndex(), + .queue = instance.GetPresentQueue(), + .image_count = image_count, + .min_allocation_size = 1024 * 1024, + .pipeline_rendering_create_info{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &surface_format, + }, + .allocator = allocator, + .check_vk_result_fn = &CheckVkResult, + }; + Vulkan::Init(vk_info); +} + +void OnResize() { + Sdl::OnResize(); +} + +void Shutdown(const vk::Device& device) { + device.waitIdle(); + + const ImGuiIO& io = GetIO(); + const auto ini_filename = (void*)io.IniFilename; + const auto log_filename = (void*)io.LogFilename; + + Vulkan::Shutdown(); + Sdl::Shutdown(); + DestroyContext(); + + SDL_free(ini_filename); + SDL_free(log_filename); +} + +bool ProcessEvent(SDL_Event* event) { + Sdl::ProcessEvent(event); + switch (event->type) { + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_WHEEL: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + return GetIO().WantCaptureMouse; + case SDL_EVENT_TEXT_INPUT: + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + return GetIO().WantCaptureKeyboard; + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + case SDL_EVENT_GAMEPAD_ADDED: + case SDL_EVENT_GAMEPAD_REMOVED: + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + return (GetIO().BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + default: + return false; + } +} + +void NewFrame() { + { + std::scoped_lock lock{change_layers_mutex}; + while (!change_layers.empty()) { + const auto [to_be_added, layer] = change_layers.front(); + if (to_be_added) { + layers.push_back(layer); + } else { + const auto [begin, end] = std::ranges::remove(layers, layer); + layers.erase(begin, end); + } + change_layers.pop_front(); + } + } + + Vulkan::NewFrame(); + Sdl::NewFrame(); + ImGui::NewFrame(); + + bool capture_gamepad = false; + for (auto* layer : layers) { + layer->Draw(); + if (layer->ShouldGrabGamepad()) { + capture_gamepad = true; + } + } + if (capture_gamepad) { + GetIO().BackendFlags |= ImGuiBackendFlags_HasGamepad; + } else { + GetIO().BackendFlags &= ~ImGuiBackendFlags_HasGamepad; + } +} + +void Render(const vk::CommandBuffer& cmdbuf, ::Vulkan::Frame* frame) { + ImGui::Render(); + ImDrawData* draw_data = GetDrawData(); + if (draw_data->CmdListsCount == 0) { + return; + } + + if (Config::vkMarkersEnabled()) { + cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ + .pLabelName = "ImGui Render", + }); + } + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, + {vk::ImageMemoryBarrier{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = frame->image, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }, + }}); + + vk::RenderingAttachmentInfo color_attachments[1]{ + { + .imageView = frame->image_view, + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eLoad, + .storeOp = vk::AttachmentStoreOp::eStore, + }, + }; + vk::RenderingInfo render_info = {}; + render_info.renderArea = { + .offset = {0, 0}, + .extent = {frame->width, frame->height}, + }; + render_info.layerCount = 1; + render_info.colorAttachmentCount = 1; + render_info.pColorAttachments = color_attachments; + cmdbuf.beginRendering(render_info); + Vulkan::RenderDrawData(*draw_data, cmdbuf); + cmdbuf.endRendering(); + if (Config::vkMarkersEnabled()) { + cmdbuf.endDebugUtilsLabelEXT(); + } +} + +} // namespace Core + +void Layer::AddLayer(Layer* layer) { + std::scoped_lock lock{change_layers_mutex}; + change_layers.emplace_back(true, layer); +} + +void Layer::RemoveLayer(Layer* layer) { + std::scoped_lock lock{change_layers_mutex}; + change_layers.emplace_back(false, layer); +} + +} // namespace ImGui diff --git a/src/imgui/renderer/imgui_core.h b/src/imgui/renderer/imgui_core.h new file mode 100644 index 00000000..9ad708f8 --- /dev/null +++ b/src/imgui/renderer/imgui_core.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "video_core/renderer_vulkan/vk_instance.h" +#include "vulkan/vulkan_handles.hpp" + +union SDL_Event; + +namespace Vulkan { +struct Frame; +} + +namespace ImGui::Core { + +void Initialize(const Vulkan::Instance& instance, const Frontend::WindowSDL& window, + u32 image_count, vk::Format surface_format, + const vk::AllocationCallbacks* allocator = nullptr); + +void OnResize(); + +void Shutdown(const vk::Device& device); + +bool ProcessEvent(SDL_Event* event); + +void NewFrame(); + +void Render(const vk::CommandBuffer& cmdbuf, Vulkan::Frame* frame); + +} // namespace ImGui::Core diff --git a/src/imgui/renderer/imgui_impl_sdl3.cpp b/src/imgui/renderer/imgui_impl_sdl3.cpp new file mode 100644 index 00000000..2a7d801e --- /dev/null +++ b/src/imgui/renderer/imgui_impl_sdl3.cpp @@ -0,0 +1,789 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on imgui_impl_sdl3.cpp from Dear ImGui repository + +#include +#include "imgui_impl_sdl3.h" + +// SDL +#include +#if defined(__APPLE__) +#include +#endif +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +namespace ImGui::Sdl { + +// SDL Data +struct SdlData { + SDL_Window* window{}; + SDL_WindowID window_id{}; + Uint64 time{}; + const char* clipboard_text_data{}; + + // IME handling + SDL_Window* ime_window{}; + + // Mouse handling + Uint32 mouse_window_id{}; + int mouse_buttons_down{}; + SDL_Cursor* mouse_cursors[ImGuiMouseCursor_COUNT]{}; + SDL_Cursor* mouse_last_cursor{}; + int mouse_pending_leave_frame{}; + + // Gamepad handling + ImVector gamepads{}; + GamepadMode gamepad_mode{}; + bool want_update_gamepads_list{}; +}; + +// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui +// contexts It is STRONGLY preferred that you use docking branch with multi-viewports (== single +// Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static SdlData* GetBackendData() { + return ImGui::GetCurrentContext() ? (SdlData*)ImGui::GetIO().BackendPlatformUserData : nullptr; +} + +static const char* GetClipboardText(ImGuiContext*) { + SdlData* bd = GetBackendData(); + if (bd->clipboard_text_data) + SDL_free((void*)bd->clipboard_text_data); + const char* sdl_clipboard_text = SDL_GetClipboardText(); + bd->clipboard_text_data = sdl_clipboard_text; + return bd->clipboard_text_data; +} + +static void SetClipboardText(ImGuiContext*, const char* text) { + SDL_SetClipboardText(text); +} + +static void PlatformSetImeData(ImGuiContext*, ImGuiViewport* viewport, ImGuiPlatformImeData* data) { + SdlData* bd = GetBackendData(); + auto window_id = (SDL_WindowID)(intptr_t)viewport->PlatformHandle; + SDL_Window* window = SDL_GetWindowFromID(window_id); + if ((!data->WantVisible || bd->ime_window != window) && bd->ime_window != nullptr) { + SDL_StopTextInput(bd->ime_window); + bd->ime_window = nullptr; + } + if (data->WantVisible) { + SDL_Rect r; + r.x = (int)data->InputPos.x; + r.y = (int)data->InputPos.y; + r.w = 1; + r.h = (int)data->InputLineHeight; + SDL_SetTextInputArea(window, &r, 0); + SDL_StartTextInput(window); + bd->ime_window = window; + } +} + +static ImGuiKey KeyEventToImGuiKey(SDL_Keycode keycode, SDL_Scancode scancode) { + // Keypad doesn't have individual key values in SDL3 + switch (scancode) { + case SDL_SCANCODE_KP_0: + return ImGuiKey_Keypad0; + case SDL_SCANCODE_KP_1: + return ImGuiKey_Keypad1; + case SDL_SCANCODE_KP_2: + return ImGuiKey_Keypad2; + case SDL_SCANCODE_KP_3: + return ImGuiKey_Keypad3; + case SDL_SCANCODE_KP_4: + return ImGuiKey_Keypad4; + case SDL_SCANCODE_KP_5: + return ImGuiKey_Keypad5; + case SDL_SCANCODE_KP_6: + return ImGuiKey_Keypad6; + case SDL_SCANCODE_KP_7: + return ImGuiKey_Keypad7; + case SDL_SCANCODE_KP_8: + return ImGuiKey_Keypad8; + case SDL_SCANCODE_KP_9: + return ImGuiKey_Keypad9; + case SDL_SCANCODE_KP_PERIOD: + return ImGuiKey_KeypadDecimal; + case SDL_SCANCODE_KP_DIVIDE: + return ImGuiKey_KeypadDivide; + case SDL_SCANCODE_KP_MULTIPLY: + return ImGuiKey_KeypadMultiply; + case SDL_SCANCODE_KP_MINUS: + return ImGuiKey_KeypadSubtract; + case SDL_SCANCODE_KP_PLUS: + return ImGuiKey_KeypadAdd; + case SDL_SCANCODE_KP_ENTER: + return ImGuiKey_KeypadEnter; + case SDL_SCANCODE_KP_EQUALS: + return ImGuiKey_KeypadEqual; + default: + break; + } + switch (keycode) { + case SDLK_TAB: + return ImGuiKey_Tab; + case SDLK_LEFT: + return ImGuiKey_LeftArrow; + case SDLK_RIGHT: + return ImGuiKey_RightArrow; + case SDLK_UP: + return ImGuiKey_UpArrow; + case SDLK_DOWN: + return ImGuiKey_DownArrow; + case SDLK_PAGEUP: + return ImGuiKey_PageUp; + case SDLK_PAGEDOWN: + return ImGuiKey_PageDown; + case SDLK_HOME: + return ImGuiKey_Home; + case SDLK_END: + return ImGuiKey_End; + case SDLK_INSERT: + return ImGuiKey_Insert; + case SDLK_DELETE: + return ImGuiKey_Delete; + case SDLK_BACKSPACE: + return ImGuiKey_Backspace; + case SDLK_SPACE: + return ImGuiKey_Space; + case SDLK_RETURN: + return ImGuiKey_Enter; + case SDLK_ESCAPE: + return ImGuiKey_Escape; + case SDLK_APOSTROPHE: + return ImGuiKey_Apostrophe; + case SDLK_COMMA: + return ImGuiKey_Comma; + case SDLK_MINUS: + return ImGuiKey_Minus; + case SDLK_PERIOD: + return ImGuiKey_Period; + case SDLK_SLASH: + return ImGuiKey_Slash; + case SDLK_SEMICOLON: + return ImGuiKey_Semicolon; + case SDLK_EQUALS: + return ImGuiKey_Equal; + case SDLK_LEFTBRACKET: + return ImGuiKey_LeftBracket; + case SDLK_BACKSLASH: + return ImGuiKey_Backslash; + case SDLK_RIGHTBRACKET: + return ImGuiKey_RightBracket; + case SDLK_GRAVE: + return ImGuiKey_GraveAccent; + case SDLK_CAPSLOCK: + return ImGuiKey_CapsLock; + case SDLK_SCROLLLOCK: + return ImGuiKey_ScrollLock; + case SDLK_NUMLOCKCLEAR: + return ImGuiKey_NumLock; + case SDLK_PRINTSCREEN: + return ImGuiKey_PrintScreen; + case SDLK_PAUSE: + return ImGuiKey_Pause; + case SDLK_LCTRL: + return ImGuiKey_LeftCtrl; + case SDLK_LSHIFT: + return ImGuiKey_LeftShift; + case SDLK_LALT: + return ImGuiKey_LeftAlt; + case SDLK_LGUI: + return ImGuiKey_LeftSuper; + case SDLK_RCTRL: + return ImGuiKey_RightCtrl; + case SDLK_RSHIFT: + return ImGuiKey_RightShift; + case SDLK_RALT: + return ImGuiKey_RightAlt; + case SDLK_RGUI: + return ImGuiKey_RightSuper; + case SDLK_APPLICATION: + return ImGuiKey_Menu; + case SDLK_0: + return ImGuiKey_0; + case SDLK_1: + return ImGuiKey_1; + case SDLK_2: + return ImGuiKey_2; + case SDLK_3: + return ImGuiKey_3; + case SDLK_4: + return ImGuiKey_4; + case SDLK_5: + return ImGuiKey_5; + case SDLK_6: + return ImGuiKey_6; + case SDLK_7: + return ImGuiKey_7; + case SDLK_8: + return ImGuiKey_8; + case SDLK_9: + return ImGuiKey_9; + case SDLK_A: + return ImGuiKey_A; + case SDLK_B: + return ImGuiKey_B; + case SDLK_C: + return ImGuiKey_C; + case SDLK_D: + return ImGuiKey_D; + case SDLK_E: + return ImGuiKey_E; + case SDLK_F: + return ImGuiKey_F; + case SDLK_G: + return ImGuiKey_G; + case SDLK_H: + return ImGuiKey_H; + case SDLK_I: + return ImGuiKey_I; + case SDLK_J: + return ImGuiKey_J; + case SDLK_K: + return ImGuiKey_K; + case SDLK_L: + return ImGuiKey_L; + case SDLK_M: + return ImGuiKey_M; + case SDLK_N: + return ImGuiKey_N; + case SDLK_O: + return ImGuiKey_O; + case SDLK_P: + return ImGuiKey_P; + case SDLK_Q: + return ImGuiKey_Q; + case SDLK_R: + return ImGuiKey_R; + case SDLK_S: + return ImGuiKey_S; + case SDLK_T: + return ImGuiKey_T; + case SDLK_U: + return ImGuiKey_U; + case SDLK_V: + return ImGuiKey_V; + case SDLK_W: + return ImGuiKey_W; + case SDLK_X: + return ImGuiKey_X; + case SDLK_Y: + return ImGuiKey_Y; + case SDLK_Z: + return ImGuiKey_Z; + case SDLK_F1: + return ImGuiKey_F1; + case SDLK_F2: + return ImGuiKey_F2; + case SDLK_F3: + return ImGuiKey_F3; + case SDLK_F4: + return ImGuiKey_F4; + case SDLK_F5: + return ImGuiKey_F5; + case SDLK_F6: + return ImGuiKey_F6; + case SDLK_F7: + return ImGuiKey_F7; + case SDLK_F8: + return ImGuiKey_F8; + case SDLK_F9: + return ImGuiKey_F9; + case SDLK_F10: + return ImGuiKey_F10; + case SDLK_F11: + return ImGuiKey_F11; + case SDLK_F12: + return ImGuiKey_F12; + case SDLK_F13: + return ImGuiKey_F13; + case SDLK_F14: + return ImGuiKey_F14; + case SDLK_F15: + return ImGuiKey_F15; + case SDLK_F16: + return ImGuiKey_F16; + case SDLK_F17: + return ImGuiKey_F17; + case SDLK_F18: + return ImGuiKey_F18; + case SDLK_F19: + return ImGuiKey_F19; + case SDLK_F20: + return ImGuiKey_F20; + case SDLK_F21: + return ImGuiKey_F21; + case SDLK_F22: + return ImGuiKey_F22; + case SDLK_F23: + return ImGuiKey_F23; + case SDLK_F24: + return ImGuiKey_F24; + case SDLK_AC_BACK: + return ImGuiKey_AppBack; + case SDLK_AC_FORWARD: + return ImGuiKey_AppForward; + default: + break; + } + return ImGuiKey_None; +} + +static void UpdateKeyModifiers(SDL_Keymod sdl_key_mods) { + ImGuiIO& io = ImGui::GetIO(); + io.AddKeyEvent(ImGuiMod_Ctrl, (sdl_key_mods & SDL_KMOD_CTRL) != 0); + io.AddKeyEvent(ImGuiMod_Shift, (sdl_key_mods & SDL_KMOD_SHIFT) != 0); + io.AddKeyEvent(ImGuiMod_Alt, (sdl_key_mods & SDL_KMOD_ALT) != 0); + io.AddKeyEvent(ImGuiMod_Super, (sdl_key_mods & SDL_KMOD_GUI) != 0); +} + +static ImGuiViewport* GetViewportForWindowId(SDL_WindowID window_id) { + SdlData* bd = GetBackendData(); + return (window_id == bd->window_id) ? ImGui::GetMainViewport() : nullptr; +} + +// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to +// use your inputs. +// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or +// clear/overwrite your copy of the mouse data. +// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main +// application, or clear/overwrite your copy of the keyboard data. Generally you may always pass all +// inputs to dear imgui, and hide them from your application based on those two flags. If you have +// multiple SDL events and some of them are not meant to be used by dear imgui, you may need to +// filter events based on their windowID field. +bool ProcessEvent(const SDL_Event* event) { + SdlData* bd = GetBackendData(); + IM_ASSERT(bd != nullptr && + "Context or backend not initialized! Did you call ImGui_ImplSDL3_Init()?"); + ImGuiIO& io = ImGui::GetIO(); + + switch (event->type) { + case SDL_EVENT_MOUSE_MOTION: { + if (GetViewportForWindowId(event->motion.windowID) == NULL) + return false; + ImVec2 mouse_pos((float)event->motion.x, (float)event->motion.y); + io.AddMouseSourceEvent(event->motion.which == SDL_TOUCH_MOUSEID + ? ImGuiMouseSource_TouchScreen + : ImGuiMouseSource_Mouse); + io.AddMousePosEvent(mouse_pos.x, mouse_pos.y); + return true; + } + case SDL_EVENT_MOUSE_WHEEL: { + if (GetViewportForWindowId(event->wheel.windowID) == NULL) + return false; + // IMGUI_DEBUG_LOG("wheel %.2f %.2f, precise %.2f %.2f\n", (float)event->wheel.x, + // (float)event->wheel.y, event->wheel.preciseX, event->wheel.preciseY); + float wheel_x = -event->wheel.x; + float wheel_y = event->wheel.y; +#ifdef __EMSCRIPTEN__ + wheel_x /= 100.0f; +#endif + io.AddMouseSourceEvent(event->wheel.which == SDL_TOUCH_MOUSEID + ? ImGuiMouseSource_TouchScreen + : ImGuiMouseSource_Mouse); + io.AddMouseWheelEvent(wheel_x, wheel_y); + return true; + } + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: { + if (GetViewportForWindowId(event->button.windowID) == NULL) + return false; + int mouse_button = -1; + if (event->button.button == SDL_BUTTON_LEFT) { + mouse_button = 0; + } + if (event->button.button == SDL_BUTTON_RIGHT) { + mouse_button = 1; + } + if (event->button.button == SDL_BUTTON_MIDDLE) { + mouse_button = 2; + } + if (event->button.button == SDL_BUTTON_X1) { + mouse_button = 3; + } + if (event->button.button == SDL_BUTTON_X2) { + mouse_button = 4; + } + if (mouse_button == -1) + break; + io.AddMouseSourceEvent(event->button.which == SDL_TOUCH_MOUSEID + ? ImGuiMouseSource_TouchScreen + : ImGuiMouseSource_Mouse); + io.AddMouseButtonEvent(mouse_button, (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)); + bd->mouse_buttons_down = (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) + ? (bd->mouse_buttons_down | (1 << mouse_button)) + : (bd->mouse_buttons_down & ~(1 << mouse_button)); + return true; + } + case SDL_EVENT_TEXT_INPUT: { + if (GetViewportForWindowId(event->text.windowID) == NULL) + return false; + io.AddInputCharactersUTF8(event->text.text); + return true; + } + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: { + if (GetViewportForWindowId(event->key.windowID) == NULL) + return false; + // IMGUI_DEBUG_LOG("SDL_EVENT_KEY_%d: key=%d, scancode=%d, mod=%X\n", (event->type == + // SDL_EVENT_KEY_DOWN) ? "DOWN" : "UP", event->key.key, event->key.scancode, + // event->key.mod); + UpdateKeyModifiers((SDL_Keymod)event->key.mod); + ImGuiKey key = KeyEventToImGuiKey(event->key.key, event->key.scancode); + io.AddKeyEvent(key, (event->type == SDL_EVENT_KEY_DOWN)); + io.SetKeyEventNativeData( + key, event->key.key, event->key.scancode, + event->key.scancode); // To support legacy indexing (<1.87 user code). Legacy backend + // uses SDLK_*** as indices to IsKeyXXX() functions. + return true; + } + case SDL_EVENT_WINDOW_MOUSE_ENTER: { + if (GetViewportForWindowId(event->window.windowID) == NULL) + return false; + bd->mouse_window_id = event->window.windowID; + bd->mouse_pending_leave_frame = 0; + return true; + } + // - In some cases, when detaching a window from main viewport SDL may send + // SDL_WINDOWEVENT_ENTER one frame too late, + // causing SDL_WINDOWEVENT_LEAVE on previous frame to interrupt drag operation by clear mouse + // position. This is why we delay process the SDL_WINDOWEVENT_LEAVE events by one frame. See + // issue #5012 for details. + // FIXME: Unconfirmed whether this is still needed with SDL3. + case SDL_EVENT_WINDOW_MOUSE_LEAVE: { + if (GetViewportForWindowId(event->window.windowID) == NULL) + return false; + bd->mouse_pending_leave_frame = ImGui::GetFrameCount() + 1; + return true; + } + case SDL_EVENT_WINDOW_FOCUS_GAINED: + case SDL_EVENT_WINDOW_FOCUS_LOST: { + if (GetViewportForWindowId(event->window.windowID) == NULL) + return false; + io.AddFocusEvent(event->type == SDL_EVENT_WINDOW_FOCUS_GAINED); + return true; + } + case SDL_EVENT_GAMEPAD_ADDED: + case SDL_EVENT_GAMEPAD_REMOVED: { + bd->want_update_gamepads_list = true; + return true; + } + } + return false; +} + +static void SetupPlatformHandles(ImGuiViewport* viewport, SDL_Window* window) { + viewport->PlatformHandle = (void*)(intptr_t)SDL_GetWindowID(window); + viewport->PlatformHandleRaw = nullptr; +#if defined(_WIN32) && !defined(__WINRT__) + viewport->PlatformHandleRaw = (HWND)SDL_GetPointerProperty( + SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); +#elif defined(__APPLE__) && defined(SDL_VIDEO_DRIVER_COCOA) + viewport->PlatformHandleRaw = SDL_GetPointerProperty( + SDL_GetWindowProperties(window), SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr); +#endif +} + +bool Init(SDL_Window* window) { + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!"); + + // Setup backend capabilities flags + SdlData* bd = IM_NEW(SdlData)(); + io.BackendPlatformUserData = (void*)bd; + io.BackendPlatformName = "imgui_impl_sdl3_shadps4"; + io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values + io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests + // (optional, rarely used) + + bd->window = window; + bd->window_id = SDL_GetWindowID(window); + + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Platform_SetClipboardTextFn = SetClipboardText; + platform_io.Platform_GetClipboardTextFn = GetClipboardText; + platform_io.Platform_SetImeDataFn = PlatformSetImeData; + + // Gamepad handling + bd->gamepad_mode = ImGui_ImplSDL3_GamepadMode_AutoFirst; + bd->want_update_gamepads_list = true; + + // Load mouse cursors +#define CURSOR(left, right) \ + bd->mouse_cursors[ImGuiMouseCursor_##left] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_##right) + CURSOR(Arrow, DEFAULT); + CURSOR(TextInput, TEXT); + CURSOR(ResizeAll, MOVE); + CURSOR(ResizeNS, NS_RESIZE); + CURSOR(ResizeEW, EW_RESIZE); + CURSOR(ResizeNESW, NESW_RESIZE); + CURSOR(ResizeNWSE, NWSE_RESIZE); + CURSOR(Hand, POINTER); + CURSOR(NotAllowed, NOT_ALLOWED); +#undef CURSOR + + // Set platform dependent data in viewport + // Our mouse update function expect PlatformHandle to be filled for the main viewport + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + SetupPlatformHandles(main_viewport, window); + + // From 2.0.5: Set SDL hint to receive mouse click events on window focus, otherwise SDL doesn't + // emit the event. Without this, when clicking to gain focus, our widgets wouldn't activate even + // though they showed as hovered. (This is unfortunately a global SDL setting, so enabling it + // might have a side-effect on your application. It is unlikely to make a difference, but if + // your app absolutely needs to ignore the initial on-focus click: you can ignore + // SDL_EVENT_MOUSE_BUTTON_DOWN events coming right after a SDL_WINDOWEVENT_FOCUS_GAINED) +#ifdef SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH + SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); +#endif + + // From 2.0.22: Disable auto-capture, this is preventing drag and drop across multiple windows + // (see #5710) +#ifdef SDL_HINT_MOUSE_AUTO_CAPTURE + SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0"); +#endif + + return true; +} + +static void CloseGamepads(); + +void Shutdown() { + SdlData* bd = GetBackendData(); + IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + + if (bd->clipboard_text_data) { + SDL_free((void*)bd->clipboard_text_data); + } + for (ImGuiMouseCursor cursor_n = 0; cursor_n < ImGuiMouseCursor_COUNT; cursor_n++) + SDL_DestroyCursor(bd->mouse_cursors[cursor_n]); + CloseGamepads(); + + io.BackendPlatformName = nullptr; + io.BackendPlatformUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | + ImGuiBackendFlags_HasGamepad); + IM_DELETE(bd); +} + +static void UpdateMouseData() { + SdlData* bd = GetBackendData(); + ImGuiIO& io = ImGui::GetIO(); + + // We forward mouse input when hovered or captured (via SDL_EVENT_MOUSE_MOTION) or when focused + // (below) + // SDL_CaptureMouse() let the OS know e.g. that our imgui drag outside the SDL window boundaries + // shouldn't e.g. trigger other operations outside + SDL_CaptureMouse((bd->mouse_buttons_down != 0) ? SDL_TRUE : SDL_FALSE); + SDL_Window* focused_window = SDL_GetKeyboardFocus(); + const bool is_app_focused = (bd->window == focused_window); + + if (is_app_focused) { + // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when + // ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) + if (io.WantSetMousePos) + SDL_WarpMouseInWindow(bd->window, io.MousePos.x, io.MousePos.y); + + // (Optional) Fallback to provide mouse position when focused (SDL_EVENT_MOUSE_MOTION + // already provides this when hovered or captured) + if (bd->mouse_buttons_down == 0) { + // Single-viewport mode: mouse position in client window coordinates (io.MousePos is + // (0,0) when the mouse is on the upper-left corner of the app window) + float mouse_x_global, mouse_y_global; + int window_x, window_y; + SDL_GetGlobalMouseState(&mouse_x_global, &mouse_y_global); + SDL_GetWindowPosition(focused_window, &window_x, &window_y); + io.AddMousePosEvent(mouse_x_global - (float)window_x, mouse_y_global - (float)window_y); + } + } +} + +static void UpdateMouseCursor() { + ImGuiIO& io = ImGui::GetIO(); + if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) + return; + SdlData* bd = GetBackendData(); + + ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); + if (io.MouseDrawCursor || imgui_cursor == ImGuiMouseCursor_None) { + // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor + SDL_HideCursor(); + } else { + // Show OS mouse cursor + SDL_Cursor* expected_cursor = bd->mouse_cursors[imgui_cursor] + ? bd->mouse_cursors[imgui_cursor] + : bd->mouse_cursors[ImGuiMouseCursor_Arrow]; + if (bd->mouse_last_cursor != expected_cursor) { + SDL_SetCursor(expected_cursor); // SDL function doesn't have an early out (see #6113) + bd->mouse_last_cursor = expected_cursor; + } + SDL_ShowCursor(); + } +} + +static void CloseGamepads() { + SdlData* bd = GetBackendData(); + if (bd->gamepad_mode != ImGui_ImplSDL3_GamepadMode_Manual) + for (SDL_Gamepad* gamepad : bd->gamepads) + SDL_CloseGamepad(gamepad); + bd->gamepads.resize(0); +} + +void SetGamepadMode(GamepadMode mode, SDL_Gamepad** manual_gamepads_array, + int manual_gamepads_count) { + SdlData* bd = GetBackendData(); + CloseGamepads(); + if (mode == ImGui_ImplSDL3_GamepadMode_Manual) { + IM_ASSERT(manual_gamepads_array != nullptr && manual_gamepads_count > 0); + for (int n = 0; n < manual_gamepads_count; n++) + bd->gamepads.push_back(manual_gamepads_array[n]); + } else { + IM_ASSERT(manual_gamepads_array == nullptr && manual_gamepads_count <= 0); + bd->want_update_gamepads_list = true; + } + bd->gamepad_mode = mode; +} + +static void UpdateGamepadButton(SdlData* bd, ImGuiIO& io, ImGuiKey key, + SDL_GamepadButton button_no) { + bool merged_value = false; + for (SDL_Gamepad* gamepad : bd->gamepads) + merged_value |= SDL_GetGamepadButton(gamepad, button_no) != 0; + io.AddKeyEvent(key, merged_value); +} + +static inline float Saturate(float v) { + return v < 0.0f ? 0.0f : v > 1.0f ? 1.0f : v; +} +static void UpdateGamepadAnalog(SdlData* bd, ImGuiIO& io, ImGuiKey key, SDL_GamepadAxis axis_no, + float v0, float v1) { + float merged_value = 0.0f; + for (SDL_Gamepad* gamepad : bd->gamepads) { + float vn = Saturate((float)(SDL_GetGamepadAxis(gamepad, axis_no) - v0) / (float)(v1 - v0)); + if (merged_value < vn) + merged_value = vn; + } + io.AddKeyAnalogEvent(key, merged_value > 0.1f, merged_value); +} + +static void UpdateGamepads() { + ImGuiIO& io = ImGui::GetIO(); + SdlData* bd = GetBackendData(); + + // Update list of gamepads to use + if (bd->want_update_gamepads_list && bd->gamepad_mode != ImGui_ImplSDL3_GamepadMode_Manual) { + CloseGamepads(); + int sdl_gamepads_count = 0; + const SDL_JoystickID* sdl_gamepads = SDL_GetGamepads(&sdl_gamepads_count); + for (int n = 0; n < sdl_gamepads_count; n++) + if (SDL_Gamepad* gamepad = SDL_OpenGamepad(sdl_gamepads[n])) { + bd->gamepads.push_back(gamepad); + if (bd->gamepad_mode == ImGui_ImplSDL3_GamepadMode_AutoFirst) + break; + } + bd->want_update_gamepads_list = false; + } + + // FIXME: Technically feeding gamepad shouldn't depend on this now that they are regular inputs. + if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) + return; + io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; + if (bd->gamepads.Size == 0) + return; + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + // Update gamepad inputs + const int thumb_dead_zone = 8000; // SDL_gamepad.h suggests using this value. + UpdateGamepadButton(bd, io, ImGuiKey_GamepadStart, SDL_GAMEPAD_BUTTON_START); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadBack, SDL_GAMEPAD_BUTTON_BACK); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceLeft, + SDL_GAMEPAD_BUTTON_WEST); // Xbox X, PS Square + UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceRight, + SDL_GAMEPAD_BUTTON_EAST); // Xbox B, PS Circle + UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceUp, + SDL_GAMEPAD_BUTTON_NORTH); // Xbox Y, PS Triangle + UpdateGamepadButton(bd, io, ImGuiKey_GamepadFaceDown, + SDL_GAMEPAD_BUTTON_SOUTH); // Xbox A, PS Cross + UpdateGamepadButton(bd, io, ImGuiKey_GamepadDpadLeft, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadDpadRight, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadDpadUp, SDL_GAMEPAD_BUTTON_DPAD_UP); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadDpadDown, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadL1, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadR1, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadL2, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 0.0f, 32767); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadR2, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 0.0f, 32767); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadL3, SDL_GAMEPAD_BUTTON_LEFT_STICK); + UpdateGamepadButton(bd, io, ImGuiKey_GamepadR3, SDL_GAMEPAD_BUTTON_RIGHT_STICK); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadLStickLeft, SDL_GAMEPAD_AXIS_LEFTX, + -thumb_dead_zone, -32768); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadLStickRight, SDL_GAMEPAD_AXIS_LEFTX, + +thumb_dead_zone, +32767); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadLStickUp, SDL_GAMEPAD_AXIS_LEFTY, -thumb_dead_zone, + -32768); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadLStickDown, SDL_GAMEPAD_AXIS_LEFTY, + +thumb_dead_zone, +32767); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadRStickLeft, SDL_GAMEPAD_AXIS_RIGHTX, + -thumb_dead_zone, -32768); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadRStickRight, SDL_GAMEPAD_AXIS_RIGHTX, + +thumb_dead_zone, +32767); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadRStickUp, SDL_GAMEPAD_AXIS_RIGHTY, -thumb_dead_zone, + -32768); + UpdateGamepadAnalog(bd, io, ImGuiKey_GamepadRStickDown, SDL_GAMEPAD_AXIS_RIGHTY, + +thumb_dead_zone, +32767); +} + +void NewFrame() { + SdlData* bd = GetBackendData(); + IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + + // Setup time step (we don't use SDL_GetTicks() because it is using millisecond resolution) + // (Accept SDL_GetPerformanceCounter() not returning a monotonically increasing value. Happens + // in VMs and Emscripten, see #6189, #6114, #3644) + static Uint64 frequency = SDL_GetPerformanceFrequency(); + Uint64 current_time = SDL_GetPerformanceCounter(); + if (current_time <= bd->time) + current_time = bd->time + 1; + io.DeltaTime = bd->time > 0 ? (float)((double)(current_time - bd->time) / (double)frequency) + : (float)(1.0f / 60.0f); + bd->time = current_time; + + if (bd->mouse_pending_leave_frame && bd->mouse_pending_leave_frame >= ImGui::GetFrameCount() && + bd->mouse_buttons_down == 0) { + bd->mouse_window_id = 0; + bd->mouse_pending_leave_frame = 0; + io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + } + + UpdateMouseData(); + UpdateMouseCursor(); + + // Update game controllers (if enabled and available) + UpdateGamepads(); +} + +void OnResize() { + SdlData* bd = GetBackendData(); + ImGuiIO& io = ImGui::GetIO(); + + int w, h; + int display_w, display_h; + SDL_GetWindowSize(bd->window, &w, &h); + if (SDL_GetWindowFlags(bd->window) & SDL_WINDOW_MINIMIZED) { + w = h = 0; + } + SDL_GetWindowSizeInPixels(bd->window, &display_w, &display_h); + io.DisplaySize = ImVec2((float)w, (float)h); + if (w > 0 && h > 0) { + io.DisplayFramebufferScale = {(float)display_w / (float)w, (float)display_h / (float)h}; + } +} + +} // namespace ImGui::Sdl diff --git a/src/imgui/renderer/imgui_impl_sdl3.h b/src/imgui/renderer/imgui_impl_sdl3.h new file mode 100644 index 00000000..59b1a685 --- /dev/null +++ b/src/imgui/renderer/imgui_impl_sdl3.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on imgui_impl_sdl3.h from Dear ImGui repository + +#pragma once + +struct SDL_Window; +struct SDL_Renderer; +struct SDL_Gamepad; +typedef union SDL_Event SDL_Event; + +namespace ImGui::Sdl { + +bool Init(SDL_Window* window); +void Shutdown(); +void NewFrame(); +bool ProcessEvent(const SDL_Event* event); +void OnResize(); + +// Gamepad selection automatically starts in AutoFirst mode, picking first available SDL_Gamepad. +// You may override this. When using manual mode, caller is responsible for opening/closing gamepad. +enum GamepadMode { + ImGui_ImplSDL3_GamepadMode_AutoFirst, + ImGui_ImplSDL3_GamepadMode_AutoAll, + ImGui_ImplSDL3_GamepadMode_Manual +}; +void SetGamepadMode(GamepadMode mode, SDL_Gamepad** manual_gamepads_array = NULL, + int manual_gamepads_count = -1); + +}; // namespace ImGui::Sdl diff --git a/src/imgui/renderer/imgui_impl_vulkan.cpp b/src/imgui/renderer/imgui_impl_vulkan.cpp new file mode 100644 index 00000000..2c1c135f --- /dev/null +++ b/src/imgui/renderer/imgui_impl_vulkan.cpp @@ -0,0 +1,1107 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on imgui_impl_vulkan.cpp from Dear ImGui repository + +#include +#include + +#include "imgui_impl_vulkan.h" + +#ifndef IM_MAX +#define IM_MAX(A, B) (((A) >= (B)) ? (A) : (B)) +#endif + +#define IDX_SIZE sizeof(ImDrawIdx) + +namespace ImGui::Vulkan { + +struct RenderBuffer { + vk::DeviceMemory buffer_memory{}; + vk::DeviceSize buffer_size{}; + vk::Buffer buffer{}; +}; + +// Reusable buffers used for rendering 1 current in-flight frame, for RenderDrawData() +struct FrameRenderBuffers { + RenderBuffer vertex; + RenderBuffer index; +}; + +// Each viewport will hold 1 WindowRenderBuffers +struct WindowRenderBuffers { + uint32_t index{}; + uint32_t count{}; + std::vector frame_render_buffers{}; +}; + +// Vulkan data +struct VkData { + const InitInfo init_info; + vk::DeviceSize buffer_memory_alignment = 256; + vk::PipelineCreateFlags pipeline_create_flags{}; + vk::DescriptorPool descriptor_pool{}; + vk::DescriptorSetLayout descriptor_set_layout{}; + vk::PipelineLayout pipeline_layout{}; + vk::Pipeline pipeline{}; + vk::ShaderModule shader_module_vert{}; + vk::ShaderModule shader_module_frag{}; + + // Font data + vk::Sampler font_sampler{}; + vk::DeviceMemory font_memory{}; + vk::Image font_image{}; + vk::ImageView font_view{}; + vk::DescriptorSet font_descriptor_set{}; + vk::CommandPool font_command_pool{}; + vk::CommandBuffer font_command_buffer{}; + + // Render buffers + WindowRenderBuffers render_buffers{}; + + VkData(const InitInfo init_info) : init_info(init_info) { + render_buffers.count = init_info.image_count; + render_buffers.frame_render_buffers.resize(render_buffers.count); + } +}; + +//----------------------------------------------------------------------------- +// SHADERS +//----------------------------------------------------------------------------- + +// backends/vulkan/glsl_shader.vert, compiled with: +// # glslangValidator -V -x -o glsl_shader.vert.u32 glsl_shader.vert +/* +#version 450 core +layout(location = 0) in vec2 aPos; +layout(location = 1) in vec2 aUV; +layout(location = 2) in vec4 aColor; +layout(push_constant) uniform uPushConstant { vec2 uScale; vec2 uTranslate; } pc; + +out gl_PerVertex { vec4 gl_Position; }; +layout(location = 0) out struct { vec4 Color; vec2 UV; } Out; + +void main() +{ + Out.Color = aColor; + Out.UV = aUV; + gl_Position = vec4(aPos * pc.uScale + pc.uTranslate, 0, 1); +} +*/ +static uint32_t glsl_shader_vert_spv[] = { + 0x07230203, 0x00010000, 0x00080001, 0x0000002e, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x000a000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000000f, 0x00000015, + 0x0000001b, 0x0000001c, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, + 0x00000000, 0x00030005, 0x00000009, 0x00000000, 0x00050006, 0x00000009, 0x00000000, 0x6f6c6f43, + 0x00000072, 0x00040006, 0x00000009, 0x00000001, 0x00005655, 0x00030005, 0x0000000b, 0x0074754f, + 0x00040005, 0x0000000f, 0x6c6f4361, 0x0000726f, 0x00030005, 0x00000015, 0x00565561, 0x00060005, + 0x00000019, 0x505f6c67, 0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x00000019, 0x00000000, + 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00030005, 0x0000001b, 0x00000000, 0x00040005, 0x0000001c, + 0x736f5061, 0x00000000, 0x00060005, 0x0000001e, 0x73755075, 0x6e6f4368, 0x6e617473, 0x00000074, + 0x00050006, 0x0000001e, 0x00000000, 0x61635375, 0x0000656c, 0x00060006, 0x0000001e, 0x00000001, + 0x61725475, 0x616c736e, 0x00006574, 0x00030005, 0x00000020, 0x00006370, 0x00040047, 0x0000000b, + 0x0000001e, 0x00000000, 0x00040047, 0x0000000f, 0x0000001e, 0x00000002, 0x00040047, 0x00000015, + 0x0000001e, 0x00000001, 0x00050048, 0x00000019, 0x00000000, 0x0000000b, 0x00000000, 0x00030047, + 0x00000019, 0x00000002, 0x00040047, 0x0000001c, 0x0000001e, 0x00000000, 0x00050048, 0x0000001e, + 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001e, 0x00000001, 0x00000023, 0x00000008, + 0x00030047, 0x0000001e, 0x00000002, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, + 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040017, + 0x00000008, 0x00000006, 0x00000002, 0x0004001e, 0x00000009, 0x00000007, 0x00000008, 0x00040020, + 0x0000000a, 0x00000003, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000003, 0x00040015, + 0x0000000c, 0x00000020, 0x00000001, 0x0004002b, 0x0000000c, 0x0000000d, 0x00000000, 0x00040020, + 0x0000000e, 0x00000001, 0x00000007, 0x0004003b, 0x0000000e, 0x0000000f, 0x00000001, 0x00040020, + 0x00000011, 0x00000003, 0x00000007, 0x0004002b, 0x0000000c, 0x00000013, 0x00000001, 0x00040020, + 0x00000014, 0x00000001, 0x00000008, 0x0004003b, 0x00000014, 0x00000015, 0x00000001, 0x00040020, + 0x00000017, 0x00000003, 0x00000008, 0x0003001e, 0x00000019, 0x00000007, 0x00040020, 0x0000001a, + 0x00000003, 0x00000019, 0x0004003b, 0x0000001a, 0x0000001b, 0x00000003, 0x0004003b, 0x00000014, + 0x0000001c, 0x00000001, 0x0004001e, 0x0000001e, 0x00000008, 0x00000008, 0x00040020, 0x0000001f, + 0x00000009, 0x0000001e, 0x0004003b, 0x0000001f, 0x00000020, 0x00000009, 0x00040020, 0x00000021, + 0x00000009, 0x00000008, 0x0004002b, 0x00000006, 0x00000028, 0x00000000, 0x0004002b, 0x00000006, + 0x00000029, 0x3f800000, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, + 0x00000005, 0x0004003d, 0x00000007, 0x00000010, 0x0000000f, 0x00050041, 0x00000011, 0x00000012, + 0x0000000b, 0x0000000d, 0x0003003e, 0x00000012, 0x00000010, 0x0004003d, 0x00000008, 0x00000016, + 0x00000015, 0x00050041, 0x00000017, 0x00000018, 0x0000000b, 0x00000013, 0x0003003e, 0x00000018, + 0x00000016, 0x0004003d, 0x00000008, 0x0000001d, 0x0000001c, 0x00050041, 0x00000021, 0x00000022, + 0x00000020, 0x0000000d, 0x0004003d, 0x00000008, 0x00000023, 0x00000022, 0x00050085, 0x00000008, + 0x00000024, 0x0000001d, 0x00000023, 0x00050041, 0x00000021, 0x00000025, 0x00000020, 0x00000013, + 0x0004003d, 0x00000008, 0x00000026, 0x00000025, 0x00050081, 0x00000008, 0x00000027, 0x00000024, + 0x00000026, 0x00050051, 0x00000006, 0x0000002a, 0x00000027, 0x00000000, 0x00050051, 0x00000006, + 0x0000002b, 0x00000027, 0x00000001, 0x00070050, 0x00000007, 0x0000002c, 0x0000002a, 0x0000002b, + 0x00000028, 0x00000029, 0x00050041, 0x00000011, 0x0000002d, 0x0000001b, 0x0000000d, 0x0003003e, + 0x0000002d, 0x0000002c, 0x000100fd, 0x00010038}; + +// backends/vulkan/glsl_shader.frag, compiled with: +// # glslangValidator -V -x -o glsl_shader.frag.u32 glsl_shader.frag +/* +#version 450 core +layout(location = 0) out vec4 fColor; +layout(set=0, binding=0) uniform sampler2D sTexture; +layout(location = 0) in struct { vec4 Color; vec2 UV; } In; +void main() +{ + fColor = In.Color * texture(sTexture, In.UV.st); +} +*/ +static uint32_t glsl_shader_frag_spv[] = { + 0x07230203, 0x00010000, 0x00080001, 0x0000001e, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000d, 0x00030010, + 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, + 0x00000000, 0x00040005, 0x00000009, 0x6c6f4366, 0x0000726f, 0x00030005, 0x0000000b, 0x00000000, + 0x00050006, 0x0000000b, 0x00000000, 0x6f6c6f43, 0x00000072, 0x00040006, 0x0000000b, 0x00000001, + 0x00005655, 0x00030005, 0x0000000d, 0x00006e49, 0x00050005, 0x00000016, 0x78655473, 0x65727574, + 0x00000000, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000d, 0x0000001e, + 0x00000000, 0x00040047, 0x00000016, 0x00000022, 0x00000000, 0x00040047, 0x00000016, 0x00000021, + 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, + 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, + 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040017, 0x0000000a, 0x00000006, + 0x00000002, 0x0004001e, 0x0000000b, 0x00000007, 0x0000000a, 0x00040020, 0x0000000c, 0x00000001, + 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000001, 0x00040015, 0x0000000e, 0x00000020, + 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040020, 0x00000010, 0x00000001, + 0x00000007, 0x00090019, 0x00000013, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x0003001b, 0x00000014, 0x00000013, 0x00040020, 0x00000015, 0x00000000, + 0x00000014, 0x0004003b, 0x00000015, 0x00000016, 0x00000000, 0x0004002b, 0x0000000e, 0x00000018, + 0x00000001, 0x00040020, 0x00000019, 0x00000001, 0x0000000a, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000010, 0x00000011, 0x0000000d, + 0x0000000f, 0x0004003d, 0x00000007, 0x00000012, 0x00000011, 0x0004003d, 0x00000014, 0x00000017, + 0x00000016, 0x00050041, 0x00000019, 0x0000001a, 0x0000000d, 0x00000018, 0x0004003d, 0x0000000a, + 0x0000001b, 0x0000001a, 0x00050057, 0x00000007, 0x0000001c, 0x00000017, 0x0000001b, 0x00050085, + 0x00000007, 0x0000001d, 0x00000012, 0x0000001c, 0x0003003e, 0x00000009, 0x0000001d, 0x000100fd, + 0x00010038}; + +// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui +// contexts It is STRONGLY preferred that you use docking branch with multi-viewports (== single +// Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static VkData* GetBackendData() { + return ImGui::GetCurrentContext() ? (VkData*)ImGui::GetIO().BackendRendererUserData : nullptr; +} + +static uint32_t FindMemoryType(vk::MemoryPropertyFlags properties, uint32_t type_bits) { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + const auto prop = v.physical_device.getMemoryProperties(); + for (uint32_t i = 0; i < prop.memoryTypeCount; i++) + if ((prop.memoryTypes[i].propertyFlags & properties) == properties && type_bits & (1 << i)) + return i; + return 0xFFFFFFFF; // Unable to find memoryType +} + +template +static T CheckVkResult(vk::ResultValue res) { + if (res.result == vk::Result::eSuccess) { + return res.value; + } + const VkData* bd = GetBackendData(); + if (!bd) { + return res.value; + } + const InitInfo& v = bd->init_info; + if (v.check_vk_result_fn) { + v.check_vk_result_fn(res.result); + } + return res.value; +} + +static void CheckVkErr(vk::Result res) { + if (res == vk::Result::eSuccess) { + return; + } + const VkData* bd = GetBackendData(); + if (!bd) { + return; + } + const InitInfo& v = bd->init_info; + if (v.check_vk_result_fn) { + v.check_vk_result_fn(res); + } +} + +// Same as IM_MEMALIGN(). 'alignment' must be a power of two. +static inline vk::DeviceSize AlignBufferSize(vk::DeviceSize size, vk::DeviceSize alignment) { + return (size + alignment - 1) & ~(alignment - 1); +} + +// Register a texture +vk::DescriptorSet AddTexture(vk::Sampler sampler, vk::ImageView image_view, + vk::ImageLayout image_layout) { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + // Create Descriptor Set: + vk::DescriptorSet descriptor_set; + { + vk::DescriptorSetAllocateInfo alloc_info{ + .sType = vk::StructureType::eDescriptorSetAllocateInfo, + .descriptorPool = bd->descriptor_pool, + .descriptorSetCount = 1, + .pSetLayouts = &bd->descriptor_set_layout, + }; + descriptor_set = CheckVkResult(v.device.allocateDescriptorSets(alloc_info)).front(); + } + + // Update the Descriptor Set: + { + vk::DescriptorImageInfo desc_image[1]{ + { + .sampler = sampler, + .imageView = image_view, + .imageLayout = image_layout, + }, + }; + vk::WriteDescriptorSet write_desc[1]{ + { + .sType = vk::StructureType::eWriteDescriptorSet, + .dstSet = descriptor_set, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = desc_image, + }, + }; + v.device.updateDescriptorSets({write_desc}, {}); + } + return descriptor_set; +} + +void RemoveTexture(vk::DescriptorSet descriptor_set) { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + v.device.freeDescriptorSets(bd->descriptor_pool, {descriptor_set}); +} + +static void CreateOrResizeBuffer(RenderBuffer& rb, size_t new_size, vk::BufferUsageFlagBits usage) { + VkData* bd = GetBackendData(); + IM_ASSERT(bd != nullptr); + const InitInfo& v = bd->init_info; + if (rb.buffer != VK_NULL_HANDLE) { + v.device.destroyBuffer(rb.buffer, v.allocator); + } + if (rb.buffer_memory != VK_NULL_HANDLE) { + v.device.freeMemory(rb.buffer_memory, v.allocator); + } + + const vk::DeviceSize buffer_size_aligned = + AlignBufferSize(IM_MAX(v.min_allocation_size, new_size), bd->buffer_memory_alignment); + vk::BufferCreateInfo buffer_info{ + .sType = vk::StructureType::eBufferCreateInfo, + .size = buffer_size_aligned, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + }; + rb.buffer = CheckVkResult(v.device.createBuffer(buffer_info, v.allocator)); + + const vk::MemoryRequirements req = v.device.getBufferMemoryRequirements(rb.buffer); + bd->buffer_memory_alignment = IM_MAX(bd->buffer_memory_alignment, req.alignment); + vk::MemoryAllocateInfo alloc_info{ + .sType = vk::StructureType::eMemoryAllocateInfo, + .allocationSize = req.size, + .memoryTypeIndex = + FindMemoryType(vk::MemoryPropertyFlagBits::eHostVisible, req.memoryTypeBits), + }; + rb.buffer_memory = CheckVkResult(v.device.allocateMemory(alloc_info, v.allocator)); + + CheckVkErr(v.device.bindBufferMemory(rb.buffer, rb.buffer_memory, 0)); + rb.buffer_size = buffer_size_aligned; +} + +static void SetupRenderState(ImDrawData& draw_data, vk::Pipeline pipeline, vk::CommandBuffer cmdbuf, + FrameRenderBuffers& frb, int fb_width, int fb_height) { + VkData* bd = GetBackendData(); + + // Bind pipeline: + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind Vertex And Index Buffer: + if (draw_data.TotalVtxCount > 0) { + vk::Buffer vertex_buffers[1] = {frb.vertex.buffer}; + vk::DeviceSize vertex_offset[1] = {0}; + cmdbuf.bindVertexBuffers(0, {vertex_buffers}, vertex_offset); + cmdbuf.bindIndexBuffer(frb.index.buffer, 0, + IDX_SIZE == 2 ? vk::IndexType::eUint16 : vk::IndexType::eUint32); + } + + // Setup viewport: + { + vk::Viewport viewport{ + .x = 0, + .y = 0, + .width = (float)fb_width, + .height = (float)fb_height, + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + cmdbuf.setViewport(0, {viewport}); + } + + // Setup scale and translation: + // Our visible imgui space lies from draw_data->DisplayPps (top left) to + // draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single + // viewport apps. + { + float scale[2]; + scale[0] = 2.0f / draw_data.DisplaySize.x; + scale[1] = 2.0f / draw_data.DisplaySize.y; + float translate[2]; + translate[0] = -1.0f - draw_data.DisplayPos.x * scale[0]; + translate[1] = -1.0f - draw_data.DisplayPos.y * scale[1]; + cmdbuf.pushConstants(bd->pipeline_layout, vk::ShaderStageFlagBits::eVertex, + sizeof(float) * 0, sizeof(float) * 2, scale); + cmdbuf.pushConstants(bd->pipeline_layout, vk::ShaderStageFlagBits::eVertex, + sizeof(float) * 2, sizeof(float) * 2, translate); + } +} + +// Render function +void RenderDrawData(ImDrawData& draw_data, vk::CommandBuffer command_buffer, + vk::Pipeline pipeline) { + // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != + // framebuffer coordinates) + int fb_width = (int)(draw_data.DisplaySize.x * draw_data.FramebufferScale.x); + int fb_height = (int)(draw_data.DisplaySize.y * draw_data.FramebufferScale.y); + if (fb_width <= 0 || fb_height <= 0) { + return; + } + + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + if (pipeline == VK_NULL_HANDLE) { + pipeline = bd->pipeline; + } + + // Allocate array to store enough vertex/index buffers + WindowRenderBuffers& wrb = bd->render_buffers; + wrb.index = (wrb.index + 1) % wrb.count; + FrameRenderBuffers& frb = wrb.frame_render_buffers[wrb.index]; + + if (draw_data.TotalVtxCount > 0) { + // Create or resize the vertex/index buffers + size_t vertex_size = AlignBufferSize(draw_data.TotalVtxCount * sizeof(ImDrawVert), + bd->buffer_memory_alignment); + size_t index_size = + AlignBufferSize(draw_data.TotalIdxCount * IDX_SIZE, bd->buffer_memory_alignment); + if (frb.vertex.buffer == VK_NULL_HANDLE || frb.vertex.buffer_size < vertex_size) { + CreateOrResizeBuffer(frb.vertex, vertex_size, vk::BufferUsageFlagBits::eVertexBuffer); + } + if (frb.index.buffer == VK_NULL_HANDLE || frb.index.buffer_size < index_size) { + CreateOrResizeBuffer(frb.index, index_size, vk::BufferUsageFlagBits::eIndexBuffer); + } + + // Upload vertex/index data into a single contiguous GPU buffer + ImDrawVert* vtx_dst = nullptr; + ImDrawIdx* idx_dst = nullptr; + vtx_dst = (ImDrawVert*)CheckVkResult( + v.device.mapMemory(frb.vertex.buffer_memory, 0, vertex_size, vk::MemoryMapFlags{})); + idx_dst = (ImDrawIdx*)CheckVkResult( + v.device.mapMemory(frb.index.buffer_memory, 0, index_size, vk::MemoryMapFlags{})); + for (int n = 0; n < draw_data.CmdListsCount; n++) { + const ImDrawList* cmd_list = draw_data.CmdLists[n]; + memcpy(vtx_dst, cmd_list->VtxBuffer.Data, + cmd_list->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy(idx_dst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * IDX_SIZE); + vtx_dst += cmd_list->VtxBuffer.Size; + idx_dst += cmd_list->IdxBuffer.Size; + } + vk::MappedMemoryRange range[2]{ + { + .sType = vk::StructureType::eMappedMemoryRange, + .memory = frb.vertex.buffer_memory, + .size = VK_WHOLE_SIZE, + }, + { + .sType = vk::StructureType::eMappedMemoryRange, + .memory = frb.index.buffer_memory, + .size = VK_WHOLE_SIZE, + }, + }; + CheckVkErr(v.device.flushMappedMemoryRanges({range})); + v.device.unmapMemory(frb.vertex.buffer_memory); + v.device.unmapMemory(frb.index.buffer_memory); + } + + // Setup desired Vulkan state + SetupRenderState(draw_data, pipeline, command_buffer, frb, fb_width, fb_height); + + // Will project scissor/clipping rectangles into framebuffer space + + // (0,0) unless using multi-viewports + ImVec2 clip_off = draw_data.DisplayPos; + // (1,1) unless using retina display which are often (2,2) + ImVec2 clip_scale = draw_data.FramebufferScale; + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + int global_vtx_offset = 0; + int global_idx_offset = 0; + for (int n = 0; n < draw_data.CmdListsCount; n++) { + const ImDrawList* cmd_list = draw_data.CmdLists[n]; + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback != nullptr) { + // User callback, registered via ImDrawList::AddCallback() + // (ImDrawCallback_ResetRenderState is a special callback value used by the user to + // request the renderer to reset render state.) + if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) { + SetupRenderState(draw_data, pipeline, command_buffer, frb, fb_width, fb_height); + } else { + pcmd->UserCallback(cmd_list, pcmd); + } + } else { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, + (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, + (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + + // Clamp to viewport as vk::CmdSetScissor() won't accept values that are off bounds + if (clip_min.x < 0.0f) { + clip_min.x = 0.0f; + } + if (clip_min.y < 0.0f) { + clip_min.y = 0.0f; + } + if (clip_max.x > fb_width) { + clip_max.x = (float)fb_width; + } + if (clip_max.y > fb_height) { + clip_max.y = (float)fb_height; + } + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // Apply scissor/clipping rectangle + vk::Rect2D scissor{ + .offset{ + .x = (int32_t)(clip_min.x), + .y = (int32_t)(clip_min.y), + }, + .extent{ + .width = (uint32_t)(clip_max.x - clip_min.x), + .height = (uint32_t)(clip_max.y - clip_min.y), + }, + }; + command_buffer.setScissor(0, 1, &scissor); + + // Bind DescriptorSet with font or user texture + vk::DescriptorSet desc_set[1]{(VkDescriptorSet)pcmd->TextureId}; + if (sizeof(ImTextureID) < sizeof(ImU64)) { + // We don't support texture switches if ImTextureID hasn't been redefined to be + // 64-bit. Do a flaky check that other textures haven't been used. + IM_ASSERT(pcmd->TextureId == (ImTextureID)bd->font_descriptor_set); + desc_set[0] = bd->font_descriptor_set; + } + command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + bd->pipeline_layout, 0, {desc_set}, {}); + + // Draw + command_buffer.drawIndexed(pcmd->ElemCount, 1, pcmd->IdxOffset + global_idx_offset, + pcmd->VtxOffset + global_vtx_offset, 0); + } + } + global_idx_offset += cmd_list->IdxBuffer.Size; + global_vtx_offset += cmd_list->VtxBuffer.Size; + } + // vk::Rect2D scissor = {{0, 0}, {(uint32_t)fb_width, (uint32_t)fb_height}}; + // command_buffer.setScissor(0, 1, &scissor); +} + +static void DestroyFontsTexture(); + +static bool CreateFontsTexture() { + ImGuiIO& io = ImGui::GetIO(); + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + // Destroy existing texture (if any) + if (bd->font_view || bd->font_image || bd->font_memory || bd->font_descriptor_set) { + CheckVkErr(v.queue.waitIdle()); + DestroyFontsTexture(); + } + + // Create command pool/buffer + if (bd->font_command_pool == VK_NULL_HANDLE) { + vk::CommandPoolCreateInfo info{ + .sType = vk::StructureType::eCommandPoolCreateInfo, + .flags = vk::CommandPoolCreateFlags{}, + .queueFamilyIndex = v.queue_family, + }; + bd->font_command_pool = CheckVkResult(v.device.createCommandPool(info, v.allocator)); + } + if (bd->font_command_buffer == VK_NULL_HANDLE) { + vk::CommandBufferAllocateInfo info{ + .sType = vk::StructureType::eCommandBufferAllocateInfo, + .commandPool = bd->font_command_pool, + .commandBufferCount = 1, + }; + bd->font_command_buffer = CheckVkResult(v.device.allocateCommandBuffers(info)).front(); + } + + // Start command buffer + { + CheckVkErr(v.device.resetCommandPool(bd->font_command_pool, vk::CommandPoolResetFlags{})); + vk::CommandBufferBeginInfo begin_info{}; + begin_info.sType = vk::StructureType::eCommandBufferBeginInfo; + begin_info.flags |= vk::CommandBufferUsageFlagBits::eOneTimeSubmit; + CheckVkErr(bd->font_command_buffer.begin(&begin_info)); + } + + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + size_t upload_size = width * height * 4 * sizeof(char); + + // Create the Image: + { + vk::ImageCreateInfo info{ + .sType = vk::StructureType::eImageCreateInfo, + .imageType = vk::ImageType::e2D, + .format = vk::Format::eR8G8B8A8Unorm, + .extent{ + .width = static_cast(width), + .height = static_cast(height), + .depth = 1, + }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + bd->font_image = CheckVkResult(v.device.createImage(info, v.allocator)); + vk::MemoryRequirements req = v.device.getImageMemoryRequirements(bd->font_image); + vk::MemoryAllocateInfo alloc_info{ + .sType = vk::StructureType::eMemoryAllocateInfo, + .allocationSize = IM_MAX(v.min_allocation_size, req.size), + .memoryTypeIndex = + FindMemoryType(vk::MemoryPropertyFlagBits::eDeviceLocal, req.memoryTypeBits), + }; + bd->font_memory = CheckVkResult(v.device.allocateMemory(alloc_info, v.allocator)); + CheckVkErr(v.device.bindImageMemory(bd->font_image, bd->font_memory, 0)); + } + + // Create the Image View: + { + vk::ImageViewCreateInfo info{ + .sType = vk::StructureType::eImageViewCreateInfo, + .image = bd->font_image, + .viewType = vk::ImageViewType::e2D, + .format = vk::Format::eR8G8B8A8Unorm, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1, + }, + }; + bd->font_view = CheckVkResult(v.device.createImageView(info, v.allocator)); + } + + // Create the Descriptor Set: + bd->font_descriptor_set = + AddTexture(bd->font_sampler, bd->font_view, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Create the Upload Buffer: + vk::DeviceMemory upload_buffer_memory{}; + vk::Buffer upload_buffer{}; + { + vk::BufferCreateInfo buffer_info{ + .sType = vk::StructureType::eBufferCreateInfo, + .size = upload_size, + .usage = vk::BufferUsageFlagBits::eTransferSrc, + .sharingMode = vk::SharingMode::eExclusive, + }; + upload_buffer = CheckVkResult(v.device.createBuffer(buffer_info, v.allocator)); + vk::MemoryRequirements req = v.device.getBufferMemoryRequirements(upload_buffer); + bd->buffer_memory_alignment = IM_MAX(bd->buffer_memory_alignment, req.alignment); + vk::MemoryAllocateInfo alloc_info{ + .sType = vk::StructureType::eMemoryAllocateInfo, + .allocationSize = IM_MAX(v.min_allocation_size, req.size), + .memoryTypeIndex = + FindMemoryType(vk::MemoryPropertyFlagBits::eHostVisible, req.memoryTypeBits), + }; + upload_buffer_memory = CheckVkResult(v.device.allocateMemory(alloc_info, v.allocator)); + CheckVkErr(v.device.bindBufferMemory(upload_buffer, upload_buffer_memory, 0)); + } + + // Upload to Buffer: + { + char* map = (char*)CheckVkResult(v.device.mapMemory(upload_buffer_memory, 0, upload_size)); + memcpy(map, pixels, upload_size); + vk::MappedMemoryRange range[1]{ + { + .sType = vk::StructureType::eMappedMemoryRange, + .memory = upload_buffer_memory, + .size = upload_size, + }, + }; + CheckVkErr(v.device.flushMappedMemoryRanges({range})); + v.device.unmapMemory(upload_buffer_memory); + } + + // Copy to Image: + { + vk::ImageMemoryBarrier copy_barrier[1]{ + { + .sType = vk::StructureType::eImageMemoryBarrier, + .dstAccessMask = vk::AccessFlagBits::eTransferWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = bd->font_image, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1, + }, + }, + }; + bd->font_command_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eHost, + vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, + {copy_barrier}); + + vk::BufferImageCopy region{ + .imageSubresource{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .layerCount = 1, + }, + .imageExtent{ + .width = static_cast(width), + .height = static_cast(height), + .depth = 1, + }, + }; + bd->font_command_buffer.copyBufferToImage(upload_buffer, bd->font_image, + vk::ImageLayout::eTransferDstOptimal, {region}); + + vk::ImageMemoryBarrier use_barrier[1]{{ + .sType = vk::StructureType::eImageMemoryBarrier, + .srcAccessMask = vk::AccessFlagBits::eTransferWrite, + .dstAccessMask = vk::AccessFlagBits::eShaderRead, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = bd->font_image, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1, + }, + }}; + bd->font_command_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eFragmentShader, {}, {}, + {}, {use_barrier}); + } + + // Store our identifier + io.Fonts->SetTexID((ImTextureID)bd->font_descriptor_set); + + // End command buffer + vk::SubmitInfo end_info = {}; + end_info.sType = vk::StructureType::eSubmitInfo; + end_info.commandBufferCount = 1; + end_info.pCommandBuffers = &bd->font_command_buffer; + CheckVkErr(bd->font_command_buffer.end()); + CheckVkErr(v.queue.submit({end_info})); + + CheckVkErr(v.queue.waitIdle()); + + v.device.destroyBuffer(upload_buffer, v.allocator); + v.device.freeMemory(upload_buffer_memory, v.allocator); + + return true; +} + +// You probably never need to call this, as it is called by CreateFontsTexture() +// and Shutdown(). +static void DestroyFontsTexture() { + ImGuiIO& io = ImGui::GetIO(); + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + if (bd->font_descriptor_set) { + RemoveTexture(bd->font_descriptor_set); + bd->font_descriptor_set = VK_NULL_HANDLE; + io.Fonts->SetTexID(nullptr); + } + + if (bd->font_view) { + v.device.destroyImageView(bd->font_view, v.allocator); + bd->font_view = VK_NULL_HANDLE; + } + if (bd->font_image) { + v.device.destroyImage(bd->font_image, v.allocator); + bd->font_image = VK_NULL_HANDLE; + } + if (bd->font_memory) { + v.device.freeMemory(bd->font_memory, v.allocator); + bd->font_memory = VK_NULL_HANDLE; + } +} + +static void DestroyFrameRenderBuffers(vk::Device device, RenderBuffer& rb, + const vk::AllocationCallbacks* allocator) { + if (rb.buffer) { + device.destroyBuffer(rb.buffer, allocator); + rb.buffer = VK_NULL_HANDLE; + } + if (rb.buffer_memory) { + device.freeMemory(rb.buffer_memory, allocator); + rb.buffer_memory = VK_NULL_HANDLE; + } + rb.buffer_size = 0; +} + +static void DestroyWindowRenderBuffers(vk::Device device, WindowRenderBuffers& buffers, + const vk::AllocationCallbacks* allocator) { + for (uint32_t n = 0; n < buffers.count; n++) { + auto& frb = buffers.frame_render_buffers[n]; + DestroyFrameRenderBuffers(device, frb.index, allocator); + DestroyFrameRenderBuffers(device, frb.vertex, allocator); + } + buffers = {}; +} + +static void CreateShaderModules(vk::Device device, const vk::AllocationCallbacks* allocator) { + // Create the shader modules + VkData* bd = GetBackendData(); + if (bd->shader_module_vert == VK_NULL_HANDLE) { + vk::ShaderModuleCreateInfo vert_info{ + .sType = vk::StructureType::eShaderModuleCreateInfo, + .codeSize = sizeof(glsl_shader_vert_spv), + .pCode = (uint32_t*)glsl_shader_vert_spv, + }; + bd->shader_module_vert = CheckVkResult(device.createShaderModule(vert_info, allocator)); + } + if (bd->shader_module_frag == VK_NULL_HANDLE) { + vk::ShaderModuleCreateInfo frag_info{ + .sType = vk::StructureType::eShaderModuleCreateInfo, + .codeSize = sizeof(glsl_shader_frag_spv), + .pCode = (uint32_t*)glsl_shader_frag_spv, + }; + bd->shader_module_frag = CheckVkResult(device.createShaderModule(frag_info, allocator)); + } +} + +static void CreatePipeline(vk::Device device, const vk::AllocationCallbacks* allocator, + vk::PipelineCache pipeline_cache, vk::RenderPass render_pass, + vk::Pipeline* pipeline, uint32_t subpass) { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + CreateShaderModules(device, allocator); + + vk::PipelineShaderStageCreateInfo stage[2]{ + { + .sType = vk::StructureType::ePipelineShaderStageCreateInfo, + .stage = vk::ShaderStageFlagBits::eVertex, + .module = bd->shader_module_vert, + .pName = "main", + }, + { + .sType = vk::StructureType::ePipelineShaderStageCreateInfo, + .stage = vk::ShaderStageFlagBits::eFragment, + .module = bd->shader_module_frag, + .pName = "main", + }, + }; + + vk::VertexInputBindingDescription binding_desc[1]{ + { + .stride = sizeof(ImDrawVert), + .inputRate = vk::VertexInputRate::eVertex, + }, + }; + + vk::VertexInputAttributeDescription attribute_desc[3]{ + { + .location = 0, + .binding = binding_desc[0].binding, + .format = vk::Format::eR32G32Sfloat, + .offset = offsetof(ImDrawVert, pos), + }, + { + .location = 1, + .binding = binding_desc[0].binding, + .format = vk::Format::eR32G32Sfloat, + .offset = offsetof(ImDrawVert, uv), + }, + { + .location = 2, + .binding = binding_desc[0].binding, + .format = vk::Format::eR8G8B8A8Unorm, + .offset = offsetof(ImDrawVert, col), + }, + }; + + vk::PipelineVertexInputStateCreateInfo vertex_info{ + .sType = vk::StructureType::ePipelineVertexInputStateCreateInfo, + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = binding_desc, + .vertexAttributeDescriptionCount = 3, + .pVertexAttributeDescriptions = attribute_desc, + }; + + vk::PipelineInputAssemblyStateCreateInfo ia_info{ + .sType = vk::StructureType::ePipelineInputAssemblyStateCreateInfo, + .topology = vk::PrimitiveTopology::eTriangleList, + }; + + vk::PipelineViewportStateCreateInfo viewport_info{ + .sType = vk::StructureType::ePipelineViewportStateCreateInfo, + .viewportCount = 1, + .scissorCount = 1, + }; + + vk::PipelineRasterizationStateCreateInfo raster_info{ + .sType = vk::StructureType::ePipelineRasterizationStateCreateInfo, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eNone, + .frontFace = vk::FrontFace::eCounterClockwise, + .lineWidth = 1.0f, + }; + + vk::PipelineMultisampleStateCreateInfo ms_info{ + .sType = vk::StructureType::ePipelineMultisampleStateCreateInfo, + .rasterizationSamples = vk::SampleCountFlagBits::e1, + }; + + vk::PipelineColorBlendAttachmentState color_attachment[1]{ + { + .blendEnable = VK_TRUE, + .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, + .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, + }, + }; + + vk::PipelineDepthStencilStateCreateInfo depth_info{ + .sType = vk::StructureType::ePipelineDepthStencilStateCreateInfo, + }; + + vk::PipelineColorBlendStateCreateInfo blend_info{ + .sType = vk::StructureType::ePipelineColorBlendStateCreateInfo, + .attachmentCount = 1, + .pAttachments = color_attachment, + }; + + vk::DynamicState dynamic_states[2]{ + vk::DynamicState::eViewport, + vk::DynamicState::eScissor, + }; + vk::PipelineDynamicStateCreateInfo dynamic_state{ + .sType = vk::StructureType::ePipelineDynamicStateCreateInfo, + .dynamicStateCount = (uint32_t)IM_ARRAYSIZE(dynamic_states), + .pDynamicStates = dynamic_states, + }; + + vk::GraphicsPipelineCreateInfo info{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &v.pipeline_rendering_create_info, + .flags = bd->pipeline_create_flags, + .stageCount = 2, + .pStages = stage, + .pVertexInputState = &vertex_info, + .pInputAssemblyState = &ia_info, + .pViewportState = &viewport_info, + .pRasterizationState = &raster_info, + .pMultisampleState = &ms_info, + .pDepthStencilState = &depth_info, + .pColorBlendState = &blend_info, + .pDynamicState = &dynamic_state, + .layout = bd->pipeline_layout, + .renderPass = render_pass, + .subpass = subpass, + }; + + *pipeline = + CheckVkResult(device.createGraphicsPipelines(pipeline_cache, {info}, allocator)).front(); +} + +bool CreateDeviceObjects() { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + vk::Result err; + + if (!bd->descriptor_pool) { + // large enough descriptor pool + vk::DescriptorPoolSize pool_sizes[]{ + {vk::DescriptorType::eSampler, 1000}, + {vk::DescriptorType::eCombinedImageSampler, 1000}, + {vk::DescriptorType::eSampledImage, 1000}, + {vk::DescriptorType::eStorageImage, 1000}, + {vk::DescriptorType::eUniformTexelBuffer, 1000}, + {vk::DescriptorType::eStorageTexelBuffer, 1000}, + {vk::DescriptorType::eUniformBuffer, 1000}, + {vk::DescriptorType::eStorageBuffer, 1000}, + {vk::DescriptorType::eUniformBufferDynamic, 1000}, + {vk::DescriptorType::eStorageBufferDynamic, 1000}, + {vk::DescriptorType::eInputAttachment, 1000}, + }; + + vk::DescriptorPoolCreateInfo pool_info{ + .sType = vk::StructureType::eDescriptorPoolCreateInfo, + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = 1000, + .poolSizeCount = std::size(pool_sizes), + .pPoolSizes = pool_sizes, + }; + + bd->descriptor_pool = CheckVkResult(v.device.createDescriptorPool(pool_info)); + } + + if (!bd->font_sampler) { + // Bilinear sampling is required by default. Set 'io.Fonts->Flags |= + // ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow + // point/nearest sampling. + vk::SamplerCreateInfo info{ + .sType = vk::StructureType::eSamplerCreateInfo, + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .maxAnisotropy = 1.0f, + .minLod = -1000, + .maxLod = 1000, + }; + bd->font_sampler = CheckVkResult(v.device.createSampler(info, v.allocator)); + } + + if (!bd->descriptor_set_layout) { + vk::DescriptorSetLayoutBinding binding[1]{ + { + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + }, + }; + vk::DescriptorSetLayoutCreateInfo info{ + .sType = vk::StructureType::eDescriptorSetLayoutCreateInfo, + .bindingCount = 1, + .pBindings = binding, + }; + bd->descriptor_set_layout = + CheckVkResult(v.device.createDescriptorSetLayout(info, v.allocator)); + } + + if (!bd->pipeline_layout) { + // Constants: we are using 'vec2 offset' and 'vec2 scale' instead of a full 3d projection + // matrix + vk::PushConstantRange push_constants[1]{ + { + .stageFlags = vk::ShaderStageFlagBits::eVertex, + .offset = sizeof(float) * 0, + .size = sizeof(float) * 4, + }, + }; + vk::DescriptorSetLayout set_layout[1] = {bd->descriptor_set_layout}; + vk::PipelineLayoutCreateInfo layout_info{ + .sType = vk::StructureType::ePipelineLayoutCreateInfo, + .setLayoutCount = 1, + .pSetLayouts = set_layout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = push_constants, + }; + bd->pipeline_layout = + CheckVkResult(v.device.createPipelineLayout(layout_info, v.allocator)); + } + + CreatePipeline(v.device, v.allocator, v.pipeline_cache, nullptr, &bd->pipeline, v.subpass); + + return true; +} + +void ImGuiImplVulkanDestroyDeviceObjects() { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + DestroyWindowRenderBuffers(v.device, bd->render_buffers, v.allocator); + DestroyFontsTexture(); + + if (bd->font_command_buffer) { + v.device.freeCommandBuffers(bd->font_command_pool, {bd->font_command_buffer}); + bd->font_command_buffer = VK_NULL_HANDLE; + } + if (bd->font_command_pool) { + v.device.destroyCommandPool(bd->font_command_pool, v.allocator); + bd->font_command_pool = VK_NULL_HANDLE; + } + if (bd->shader_module_vert) { + v.device.destroyShaderModule(bd->shader_module_vert, v.allocator); + bd->shader_module_vert = VK_NULL_HANDLE; + } + if (bd->shader_module_frag) { + v.device.destroyShaderModule(bd->shader_module_frag, v.allocator); + bd->shader_module_frag = VK_NULL_HANDLE; + } + if (bd->font_sampler) { + v.device.destroySampler(bd->font_sampler, v.allocator); + bd->font_sampler = VK_NULL_HANDLE; + } + if (bd->descriptor_set_layout) { + v.device.destroyDescriptorSetLayout(bd->descriptor_set_layout, v.allocator); + bd->descriptor_set_layout = VK_NULL_HANDLE; + } + if (bd->pipeline_layout) { + v.device.destroyPipelineLayout(bd->pipeline_layout, v.allocator); + bd->pipeline_layout = VK_NULL_HANDLE; + } + if (bd->pipeline) { + v.device.destroyPipeline(bd->pipeline, v.allocator); + bd->pipeline = VK_NULL_HANDLE; + } +} + +bool Init(InitInfo info) { + + IM_ASSERT(info.instance != VK_NULL_HANDLE); + IM_ASSERT(info.physical_device != VK_NULL_HANDLE); + IM_ASSERT(info.device != VK_NULL_HANDLE); + IM_ASSERT(info.image_count >= 2); + + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + + // Setup backend capabilities flags + auto* bd = IM_NEW(VkData)(info); + io.BackendRendererUserData = (void*)bd; + io.BackendRendererName = "imgui_impl_vulkan_shadps4"; + // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + + CreateDeviceObjects(); + CreateFontsTexture(); + + return true; +} + +void Shutdown() { + VkData* bd = GetBackendData(); + IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + + ImGuiImplVulkanDestroyDeviceObjects(); + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset; + IM_DELETE(bd); +} + +void NewFrame() { + VkData* bd = GetBackendData(); + IM_ASSERT(bd != nullptr && + "Context or backend not initialized! Did you call ImGuiImplVulkanInit()?"); + + if (!bd->font_descriptor_set) + CreateFontsTexture(); +} + +} // namespace ImGui::Vulkan diff --git a/src/imgui/renderer/imgui_impl_vulkan.h b/src/imgui/renderer/imgui_impl_vulkan.h new file mode 100644 index 00000000..e68b8723 --- /dev/null +++ b/src/imgui/renderer/imgui_impl_vulkan.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on imgui_impl_vulkan.h from Dear ImGui repository + +#pragma once + +#define VULKAN_HPP_NO_EXCEPTIONS +#include "video_core/renderer_vulkan/vk_common.h" + +struct ImDrawData; + +namespace ImGui::Vulkan { + +struct InitInfo { + vk::Instance instance; + vk::PhysicalDevice physical_device; + vk::Device device; + uint32_t queue_family; + vk::Queue queue; + uint32_t image_count; // >= 2 + vk::DeviceSize min_allocation_size; // Minimum allocation size + vk::PipelineCache pipeline_cache; + uint32_t subpass; + vk::PipelineRenderingCreateInfoKHR pipeline_rendering_create_info; + + // (Optional) Allocation, Logging + const vk::AllocationCallbacks* allocator{}; + void (*check_vk_result_fn)(vk::Result err); +}; + +vk::DescriptorSet AddTexture(vk::Sampler sampler, vk::ImageView image_view, + vk::ImageLayout image_layout); + +void RemoveTexture(vk::DescriptorSet descriptor_set); + +bool Init(InitInfo info); +void Shutdown(); +void NewFrame(); +void RenderDrawData(ImDrawData& draw_data, vk::CommandBuffer command_buffer, + vk::Pipeline pipeline = VK_NULL_HANDLE); + +} // namespace ImGui::Vulkan \ No newline at end of file diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 31460d07..f3418c8f 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -9,6 +9,7 @@ #include "common/config.h" #include "common/version.h" #include "core/libraries/pad/pad.h" +#include "imgui/renderer/imgui_core.h" #include "input/controller.h" #include "sdl_window.h" #include "video_core/renderdoc.h" @@ -80,6 +81,10 @@ void WindowSDL::waitEvent() { return; } + if (ImGui::Core::ProcessEvent(&event)) { + return; + } + switch (event.type) { case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_MAXIMIZED: @@ -115,6 +120,7 @@ void WindowSDL::waitEvent() { void WindowSDL::onResize() { SDL_GetWindowSizeInPixels(window, &width, &height); + ImGui::Core::OnResize(); } void WindowSDL::onKeyPress(const SDL_Event* event) { diff --git a/src/sdl_window.h b/src/sdl_window.h index 11ee9289..2a5aeb38 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -58,6 +58,10 @@ public: return is_open; } + [[nodiscard]] SDL_Window* GetSdlWindow() const { + return window; + } + WindowSystemInfo getWindowInfo() const { return window_info; } diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index f1c81b6e..6416acfb 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -6,6 +6,7 @@ #include "common/singleton.h" #include "core/file_format/splash.h" #include "core/libraries/system/systemservice.h" +#include "imgui/renderer/imgui_core.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/renderer_vulkan/vk_rasterizer.h" @@ -73,7 +74,7 @@ RendererVulkan::RendererVulkan(Frontend::WindowSDL& window_, AmdGpu::Liverpool* draw_scheduler{instance}, present_scheduler{instance}, flip_scheduler{instance}, swapchain{instance, window}, rasterizer{std::make_unique(instance, draw_scheduler, liverpool)}, - texture_cache{rasterizer->GetTextureCache()} { + texture_cache{rasterizer->GetTextureCache()}, video_info_ui{this} { const u32 num_images = swapchain.GetImageCount(); const vk::Device device = instance.GetDevice(); @@ -84,9 +85,14 @@ RendererVulkan::RendererVulkan(Frontend::WindowSDL& window_, AmdGpu::Liverpool* frame.present_done = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled}); free_queue.push(&frame); } + + // Setup ImGui + ImGui::Core::Initialize(instance, window, num_images, swapchain.GetSurfaceFormat().format); + ImGui::Layer::AddLayer(&video_info_ui); } RendererVulkan::~RendererVulkan() { + ImGui::Layer::RemoveLayer(&video_info_ui); draw_scheduler.Finish(); const vk::Device device = instance.GetDevice(); for (auto& frame : present_frames) { @@ -94,6 +100,7 @@ RendererVulkan::~RendererVulkan() { device.destroyImageView(frame.image_view); device.destroyFence(frame.present_done); } + ImGui::Core::Shutdown(device); } void RendererVulkan::RecreateFrame(Frame* frame, u32 width, u32 height) { @@ -254,6 +261,8 @@ Frame* RendererVulkan::PrepareFrameInternal(VideoCore::Image& image, bool is_eop } void RendererVulkan::Present(Frame* frame) { + ImGui::Core::NewFrame(); + swapchain.AcquireNextImage(); const vk::Image swapchain_image = swapchain.Image(); @@ -286,7 +295,7 @@ void RendererVulkan::Present(Frame* frame) { vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .dstAccessMask = vk::AccessFlagBits::eTransferRead, - .oldLayout = vk::ImageLayout::eGeneral, + .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eTransferSrcOptimal, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, @@ -317,6 +326,8 @@ void RendererVulkan::Present(Frame* frame) { }, }; + ImGui::Core::Render(cmdbuf, frame); + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlagBits::eByRegion, {}, {}, pre_barriers); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index eab9d527..c8e56641 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -4,6 +4,8 @@ #pragma once #include + +#include "imgui/layer/video_info.h" #include "video_core/amdgpu/liverpool.h" #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_scheduler.h" @@ -103,6 +105,8 @@ private: std::condition_variable_any frame_cv; std::optional splash_img; std::vector vo_buffers_addr; + + ImGui::Layers::VideoInfo video_info_ui; }; } // namespace Vulkan