From ac83f8dc48739054cba97fb272a64e50606bf28a Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 16 Dec 2025 15:56:51 +0900 Subject: [PATCH] ADD: UI/picking manager --- .gitignore | 3 + src/CMakeLists.txt | 4 + src/core/device/swapchain.cpp | 20 ++ src/core/device/swapchain.h | 1 + src/core/engine.cpp | 359 +++++++----------------- src/core/engine.h | 74 ++--- src/core/engine_ui.cpp | 200 +++++++++----- src/core/game_api.cpp | 32 ++- src/core/picking/picking_system.cpp | 405 ++++++++++++++++++++++++++++ src/core/picking/picking_system.h | 107 ++++++++ src/core/ui/imgui_system.cpp | 309 +++++++++++++++++++++ src/core/ui/imgui_system.h | 46 ++++ src/render/passes/imgui_pass.cpp | 61 ----- src/render/passes/imgui_pass.h | 1 - src/scene/vk_scene_picking.cpp | 4 +- 15 files changed, 1174 insertions(+), 452 deletions(-) create mode 100644 src/core/picking/picking_system.cpp create mode 100644 src/core/picking/picking_system.h create mode 100644 src/core/ui/imgui_system.cpp create mode 100644 src/core/ui/imgui_system.h diff --git a/.gitignore b/.gitignore index fd17bc9..d189575 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ /bin /assets /.idea +/cmake-build-debug +/cmake-build-debug_win +/cmake-build-release_win *.spv \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 21de98b..201727a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,10 @@ add_executable (vulkan_engine core/engine.h core/engine.cpp core/engine_ui.cpp + core/ui/imgui_system.h + core/ui/imgui_system.cpp + core/picking/picking_system.h + core/picking/picking_system.cpp core/game_api.h core/game_api.cpp # core/device diff --git a/src/core/device/swapchain.cpp b/src/core/device/swapchain.cpp index 072eda2..88b1c30 100644 --- a/src/core/device/swapchain.cpp +++ b/src/core/device/swapchain.cpp @@ -27,6 +27,26 @@ void SwapchainManager::init_swapchain() resize_render_targets(_renderExtent); } +void SwapchainManager::set_window_extent_from_window(struct SDL_Window *window) +{ + if (!window) + { + return; + } + + int w = 0, h = 0; + SDL_Vulkan_GetDrawableSize(window, &w, &h); + if (w <= 0 || h <= 0) + { + SDL_GetWindowSize(window, &w, &h); + } + if (w > 0 && h > 0) + { + _windowExtent.width = static_cast(w); + _windowExtent.height = static_cast(h); + } +} + void SwapchainManager::cleanup() { _deletionQueue.flush(); diff --git a/src/core/device/swapchain.h b/src/core/device/swapchain.h index dcbc1aa..c7c382d 100644 --- a/src/core/device/swapchain.h +++ b/src/core/device/swapchain.h @@ -14,6 +14,7 @@ public: void cleanup(); + void set_window_extent_from_window(struct SDL_Window *window); void init_swapchain(); void create_swapchain(uint32_t width, uint32_t height); void destroy_swapchain() const; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index a399e70..b23fe1f 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -16,6 +16,7 @@ #include "engine.h" #include "SDL2/SDL.h" +#include "SDL2/SDL_vulkan.h" #include #include @@ -37,9 +38,8 @@ #include "render/primitives.h" #include "vk_mem_alloc.h" -#include "imgui.h" -#include "imgui_impl_sdl2.h" -#include "imgui_impl_vulkan.h" +#include "core/ui/imgui_system.h" +#include "core/picking/picking_system.h" #include "render/passes/geometry.h" #include "render/passes/imgui_pass.h" #include "render/passes/lighting.h" @@ -59,6 +59,8 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng); VulkanEngine *loadedEngine = nullptr; +VulkanEngine::~VulkanEngine() = default; + static VkExtent2D clamp_nonzero_extent(VkExtent2D extent) { if (extent.width == 0) extent.width = 1; @@ -166,6 +168,13 @@ size_t VulkanEngine::query_texture_budget_bytes() const void VulkanEngine::init() { + // DPI awareness and HiDPI behavior must be configured before initializing the video subsystem. +#if defined(_WIN32) +#ifdef SDL_HINT_WINDOWS_DPI_AWARENESS + SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); +#endif +#endif + // We initialize SDL and create a window with it. SDL_Init(SDL_INIT_VIDEO); @@ -175,7 +184,11 @@ void VulkanEngine::init() _logicalRenderExtent = clamp_nonzero_extent(_logicalRenderExtent); _drawExtent = scaled_extent(_logicalRenderExtent, renderScale); - constexpr auto window_flags = static_cast(SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); + SDL_WindowFlags window_flags = static_cast(SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); + if (_hiDpiEnabled) + { + window_flags = static_cast(window_flags | SDL_WINDOW_ALLOW_HIGHDPI); + } _swapchainManager = std::make_unique(); @@ -188,6 +201,11 @@ void VulkanEngine::init() window_flags ); + if (_swapchainManager) + { + _swapchainManager->set_window_extent_from_window(_window); + } + _windowMode = WindowMode::Windowed; _windowDisplayIndex = SDL_GetWindowDisplayIndex(_window); if (_windowDisplayIndex < 0) _windowDisplayIndex = 0; @@ -283,6 +301,9 @@ void VulkanEngine::init() _context->window = _window; _context->stats = &stats; + _picking = std::make_unique(); + _picking->init(_context.get()); + // Render graph skeleton _renderGraph = std::make_unique(); _renderGraph->init(_context.get()); @@ -341,6 +362,10 @@ void VulkanEngine::init() auto imguiPass = std::make_unique(); _renderPassManager->setImGuiPass(std::move(imguiPass)); + _ui = std::make_unique(); + _ui->init(_context.get()); + _ui->add_draw_callback([this]() { vk_engine_draw_debug_ui(this); }); + _resourceManager->set_deferred_uploads(true); _context->enableSSR = true; @@ -456,6 +481,10 @@ void VulkanEngine::set_window_mode(WindowMode mode, int display_index) if (_swapchainManager) { _swapchainManager->resize_swapchain(_window); + if (_ui) + { + _ui->on_swapchain_recreated(); + } if (_swapchainManager->resize_requested) { resize_requested = true; @@ -804,6 +833,17 @@ void VulkanEngine::cleanup() //make sure the gpu has stopped doing its things vkDeviceWaitIdle(_deviceManager->device()); + if (_ui) + { + _ui->cleanup(); + _ui.reset(); + } + if (_picking) + { + _picking->cleanup(); + _picking.reset(); + } + // Flush all frame deletion queues first while VMA allocator is still alive for (int i = 0; i < FRAME_OVERLAP; i++) { @@ -861,13 +901,6 @@ void VulkanEngine::cleanup() print_vma_stats(_deviceManager.get(), "after RTManager"); dump_vma_json(_deviceManager.get(), "after_RTManager"); - // Destroy pick readback buffer before resource manager cleanup - if (_pickReadbackBuffer.buffer) - { - _resourceManager->destroy_buffer(_pickReadbackBuffer); - _pickReadbackBuffer = {}; - } - _resourceManager->cleanup(); print_vma_stats(_deviceManager.get(), "after ResourceManager"); dump_vma_json(_deviceManager.get(), "after_ResourceManager"); @@ -962,31 +995,9 @@ void VulkanEngine::draw() } } - // Per-frame hover raycast based on last mouse position. - if (_sceneManager && _mousePosPixels.x >= 0.0f && _mousePosPixels.y >= 0.0f) + if (_picking) { - RenderObject hoverObj{}; - WorldVec3 hoverPos{}; - if (_sceneManager->pick(_mousePosPixels, hoverObj, hoverPos)) - { - _hoverPick.mesh = hoverObj.sourceMesh; - _hoverPick.scene = hoverObj.sourceScene; - _hoverPick.node = hoverObj.sourceNode; - _hoverPick.ownerType = hoverObj.ownerType; - _hoverPick.ownerName = hoverObj.ownerName; - _hoverPick.worldPos = hoverPos; - _hoverPick.worldTransform = hoverObj.transform; - _hoverPick.firstIndex = hoverObj.firstIndex; - _hoverPick.indexCount = hoverObj.indexCount; - _hoverPick.surfaceIndex = hoverObj.surfaceIndex; - _hoverPick.valid = true; - } - else - { - _hoverPick.valid = false; - _hoverPick.ownerName.clear(); - _hoverPick.ownerType = RenderObject::OwnerType::None; - } + _picking->update_hover(); } // Compute desired internal render-target extent from logical extent + render scale. @@ -1124,69 +1135,12 @@ void VulkanEngine::draw() RGImageHandle hID = _renderGraph->import_id_buffer(); geometry->register_graph(_renderGraph.get(), hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hGBufferExtra, hID, hDepth); - // If ID-buffer picking is enabled and a pick was requested this frame, - // add a small transfer pass to read back 1 pixel from the ID buffer. - if (_useIdBufferPicking && _pendingPick.active && hID.valid() && _pickReadbackBuffer.buffer) + if (_picking && _swapchainManager) { - VkExtent2D swapExt = _swapchainManager->swapchainExtent(); - VkExtent2D drawExt = _drawExtent; - - glm::vec2 logicalPos{}; - if (!vkutil::map_window_to_letterbox_src(_pendingPick.windowPos, drawExt, swapExt, logicalPos)) - { - // Click landed outside the active letterboxed region. - _pendingPick.active = false; - } - else - { - uint32_t idX = uint32_t(glm::clamp(logicalPos.x, 0.0f, float(drawExt.width - 1))); - uint32_t idY = uint32_t(glm::clamp(logicalPos.y, 0.0f, float(drawExt.height - 1))); - _pendingPick.idCoords = {idX, idY}; - - RGImportedBufferDesc bd{}; - bd.name = "pick.readback"; - bd.buffer = _pickReadbackBuffer.buffer; - bd.size = sizeof(uint32_t); - bd.currentStage = VK_PIPELINE_STAGE_2_NONE; - bd.currentAccess = 0; - RGBufferHandle hPickBuf = _renderGraph->import_buffer(bd); - - _renderGraph->add_pass( - "PickReadback", - RGPassType::Transfer, - [hID, hPickBuf](RGPassBuilder &builder, EngineContext *) - { - builder.read(hID, RGImageUsage::TransferSrc); - builder.write_buffer(hPickBuf, RGBufferUsage::TransferDst); - }, - [this, hID, hPickBuf](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *) - { - VkImage idImage = res.image(hID); - VkBuffer dst = res.buffer(hPickBuf); - if (idImage == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return; - - VkBufferImageCopy region{}; - region.bufferOffset = 0; - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.imageSubresource.mipLevel = 0; - region.imageSubresource.baseArrayLayer = 0; - region.imageSubresource.layerCount = 1; - region.imageOffset = { static_cast(_pendingPick.idCoords.x), - static_cast(_pendingPick.idCoords.y), - 0 }; - region.imageExtent = {1, 1, 1}; - - vkCmdCopyImageToBuffer(cmd, - idImage, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - dst, - 1, - ®ion); - }); - - _pickResultPending = true; - _pendingPick.active = false; - } + _picking->register_id_buffer_readback(*_renderGraph, + hID, + _drawExtent, + _swapchainManager->swapchainExtent()); } } if (auto *lighting = _renderPassManager->getPass()) @@ -1323,7 +1277,11 @@ void VulkanEngine::run() while (SDL_PollEvent(&e) != 0) { //close the window when user alt-f4s or clicks the X button - if (e.type == SDL_QUIT) bQuit = true; + if (e.type == SDL_QUIT) + { + bQuit = true; + } + if (e.type == SDL_WINDOWEVENT) { switch (e.window.event) @@ -1341,119 +1299,40 @@ void VulkanEngine::run() resize_requested = true; _last_resize_event_ms = SDL_GetTicks(); break; + case SDL_WINDOWEVENT_MOVED: + // Moving between monitors can change DPI scale; ensure swapchain is refreshed. + resize_requested = true; + _last_resize_event_ms = SDL_GetTicks(); + break; default: break; } } - if (e.type == SDL_MOUSEMOTION) + + const bool ui_capture_mouse = _ui && _ui->want_capture_mouse(); + const bool ui_capture_keyboard = _ui && _ui->want_capture_keyboard(); + + if (_ui) { - _mousePosPixels = glm::vec2{static_cast(e.motion.x), - static_cast(e.motion.y)}; - if (_dragState.buttonDown) + _ui->process_event(e); + } + if (_picking) + { + _picking->process_event(e, ui_capture_mouse); + } + if (_sceneManager) + { + const bool key_event = (e.type == SDL_KEYDOWN) || (e.type == SDL_KEYUP); + const bool mouse_event = (e.type == SDL_MOUSEBUTTONDOWN) || + (e.type == SDL_MOUSEBUTTONUP) || + (e.type == SDL_MOUSEMOTION) || + (e.type == SDL_MOUSEWHEEL); + + if (!(ui_capture_keyboard && key_event) && !(ui_capture_mouse && mouse_event)) { - _dragState.current = _mousePosPixels; - // Consider any motion as dragging for now; can add threshold if desired. - _dragState.dragging = true; + _sceneManager->getMainCamera().processSDLEvent(e); } } - if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) - { - if (!ImGui::GetIO().WantCaptureMouse) - { - _dragState.buttonDown = true; - _dragState.dragging = false; - _dragState.start = glm::vec2{static_cast(e.button.x), - static_cast(e.button.y)}; - _dragState.current = _dragState.start; - } - } - if (e.type == SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT) - { - glm::vec2 releasePos{static_cast(e.button.x), - static_cast(e.button.y)}; - _dragState.buttonDown = false; - - constexpr float clickThreshold = 3.0f; - glm::vec2 delta = releasePos - _dragState.start; - bool treatAsClick = !_dragState.dragging && - std::abs(delta.x) < clickThreshold && - std::abs(delta.y) < clickThreshold; - - if (treatAsClick) - { - if (_useIdBufferPicking) - { - // Asynchronous ID-buffer clicking: queue a pick request for this position. - // The result will be resolved at the start of a future frame from the ID buffer. - _pendingPick.active = true; - _pendingPick.windowPos = releasePos; - } - else - { - // Raycast click selection (CPU side) when ID-buffer picking is disabled. - if (_sceneManager) - { - RenderObject hitObject{}; - WorldVec3 hitPos{}; - if (_sceneManager->pick(releasePos, hitObject, hitPos)) - { - _lastPick.mesh = hitObject.sourceMesh; - _lastPick.scene = hitObject.sourceScene; - _lastPick.node = hitObject.sourceNode; - _lastPick.ownerType = hitObject.ownerType; - _lastPick.ownerName = hitObject.ownerName; - _lastPick.worldPos = hitPos; - _lastPick.worldTransform = hitObject.transform; - _lastPick.firstIndex = hitObject.firstIndex; - _lastPick.indexCount = hitObject.indexCount; - _lastPick.surfaceIndex = hitObject.surfaceIndex; - _lastPick.valid = true; - _lastPickObjectID = hitObject.objectID; - } - else - { - _lastPick.valid = false; - _lastPick.ownerName.clear(); - _lastPick.ownerType = RenderObject::OwnerType::None; - _lastPickObjectID = 0; - } - } - } - } - else - { - // Drag selection completed; compute selection based on screen-space rectangle. - _dragSelection.clear(); - if (_sceneManager) - { - std::vector selected; - _sceneManager->selectRect(_dragState.start, releasePos, selected); - _dragSelection.reserve(selected.size()); - for (const RenderObject &obj : selected) - { - PickInfo info{}; - info.mesh = obj.sourceMesh; - info.scene = obj.sourceScene; - info.node = obj.sourceNode; - info.ownerType = obj.ownerType; - info.ownerName = obj.ownerName; - // Use bounds origin transformed to world as a representative point. - glm::vec3 centerLocal = glm::vec3(obj.transform * glm::vec4(obj.bounds.origin, 1.0f)); - info.worldPos = local_to_world(centerLocal, _sceneManager->get_world_origin()); - info.worldTransform = obj.transform; - info.firstIndex = obj.firstIndex; - info.indexCount = obj.indexCount; - info.surfaceIndex = obj.surfaceIndex; - info.valid = true; - _dragSelection.push_back(info); - } - } - } - - _dragState.dragging = false; - } - _sceneManager->getMainCamera().processSDLEvent(e); - ImGui_ImplSDL2_ProcessEvent(&e); } if (freeze_rendering) @@ -1468,6 +1347,10 @@ void VulkanEngine::run() if (now_ms - _last_resize_event_ms >= RESIZE_DEBOUNCE_MS) { _swapchainManager->resize_swapchain(_window); + if (_ui) + { + _ui->on_swapchain_recreated(); + } resize_requested = false; } } @@ -1510,53 +1393,9 @@ void VulkanEngine::run() } } - if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager) + if (_picking) { - vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t)); - uint32_t pickedID = 0; - if (_pickReadbackBuffer.info.pMappedData) - { - pickedID = *reinterpret_cast(_pickReadbackBuffer.info.pMappedData); - } - - if (pickedID == 0) - { - // No object under cursor in ID buffer: clear last pick. - _lastPick.valid = false; - _lastPick.ownerName.clear(); - _lastPick.ownerType = RenderObject::OwnerType::None; - _lastPickObjectID = 0; - } - else - { - _lastPickObjectID = pickedID; - RenderObject picked{}; - if (_sceneManager->resolveObjectID(pickedID, picked)) - { - // Fallback hit position: object origin in world space (can refine later) - glm::vec3 fallbackLocal = glm::vec3(picked.transform[3]); - WorldVec3 fallbackPos = local_to_world(fallbackLocal, _sceneManager->get_world_origin()); - _lastPick.mesh = picked.sourceMesh; - _lastPick.scene = picked.sourceScene; - _lastPick.node = picked.sourceNode; - _lastPick.ownerType = picked.ownerType; - _lastPick.ownerName = picked.ownerName; - _lastPick.worldPos = fallbackPos; - _lastPick.worldTransform = picked.transform; - _lastPick.firstIndex = picked.firstIndex; - _lastPick.indexCount = picked.indexCount; - _lastPick.surfaceIndex = picked.surfaceIndex; - _lastPick.valid = true; - } - else - { - _lastPick.valid = false; - _lastPick.ownerName.clear(); - _lastPick.ownerType = RenderObject::OwnerType::None; - _lastPickObjectID = 0; - } - } - _pickResultPending = false; + _picking->begin_frame(); } get_current_frame()._deletionQueue.flush(); @@ -1566,17 +1405,11 @@ void VulkanEngine::run() } get_current_frame()._frameDescriptors.clear_pools(_deviceManager->device()); - - // imgui new frame - ImGui_ImplVulkan_NewFrame(); - ImGui_ImplSDL2_NewFrame(); - - ImGui::NewFrame(); - - // Build the engine debug UI (tabs, inspectors, etc.). - vk_engine_draw_debug_ui(this); - - ImGui::Render(); + if (_ui) + { + _ui->begin_frame(); + _ui->end_frame(); + } draw(); auto end = std::chrono::system_clock::now(); @@ -1602,12 +1435,6 @@ void VulkanEngine::init_frame_resources() { _frames[i].init(_deviceManager.get(), frame_sizes); } - - // Allocate a small readback buffer for ID-buffer picking (single uint32 pixel). - _pickReadbackBuffer = _resourceManager->create_buffer( - sizeof(uint32_t), - VK_BUFFER_USAGE_TRANSFER_DST_BIT, - VMA_MEMORY_USAGE_CPU_TO_GPU); } void VulkanEngine::init_pipelines() diff --git a/src/core/engine.h b/src/core/engine.h index 07096c9..3e48c1b 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -37,6 +37,8 @@ #include "core/raytracing/raytracing.h" #include "core/assets/texture_cache.h" #include "core/assets/ibl_manager.h" +#include "core/ui/imgui_system.h" +#include "core/picking/picking_system.h" // Number of frames-in-flight. Affects per-frame command buffers, fences, // semaphores, and transient descriptor pools in FrameResources. @@ -70,17 +72,21 @@ public: FullscreenExclusive = 2 // exclusive fullscreen (may change display mode) }; + ~VulkanEngine(); + bool _isInitialized{false}; int _frameNumber{0}; - std::shared_ptr _deviceManager; - std::unique_ptr _swapchainManager; - std::shared_ptr _resourceManager; - std::unique_ptr _renderPassManager; - std::unique_ptr _sceneManager; - std::unique_ptr _pipelineManager; - std::unique_ptr _assetManager; - std::unique_ptr _asyncLoader; + std::shared_ptr _deviceManager; + std::unique_ptr _swapchainManager; + std::shared_ptr _resourceManager; + std::unique_ptr _renderPassManager; + std::unique_ptr _ui; + std::unique_ptr _sceneManager; + std::unique_ptr _picking; + std::unique_ptr _pipelineManager; + std::unique_ptr _assetManager; + std::unique_ptr _asyncLoader; std::unique_ptr _renderGraph; std::unique_ptr _rayManager; std::unique_ptr _textureCache; @@ -91,6 +97,10 @@ public: WindowMode _windowMode{WindowMode::Windowed}; int _windowDisplayIndex{0}; + // HiDPI: allow high-DPI drawables (Retina / Windows scaling). + // Window coordinates from SDL events may be in "points" while the drawable/swapchain is in pixels. + bool _hiDpiEnabled{true}; + FrameResources _frames[FRAME_OVERLAP]; FrameResources &get_current_frame() { return _frames[_frameNumber % FRAME_OVERLAP]; }; @@ -154,51 +164,11 @@ public: IBLPaths paths{}; } _pendingIBLRequest; - struct PickInfo - { - MeshAsset *mesh = nullptr; - LoadedGLTF *scene = nullptr; - Node *node = nullptr; - RenderObject::OwnerType ownerType = RenderObject::OwnerType::None; - std::string ownerName; - WorldVec3 worldPos{0.0, 0.0, 0.0}; - glm::mat4 worldTransform{1.0f}; - uint32_t indexCount = 0; - uint32_t firstIndex = 0; - uint32_t surfaceIndex = 0; - bool valid = false; - } _lastPick; - uint32_t _lastPickObjectID = 0; + ImGuiSystem *ui() { return _ui.get(); } + const ImGuiSystem *ui() const { return _ui.get(); } - struct PickRequest - { - bool active = false; - glm::vec2 windowPos{0.0f}; - glm::uvec2 idCoords{0, 0}; - } _pendingPick; - bool _pickResultPending = false; - AllocatedBuffer _pickReadbackBuffer{}; - - // Hover and drag-selection state (raycast-based) - PickInfo _hoverPick{}; - glm::vec2 _mousePosPixels{-1.0f, -1.0f}; - struct DragState - { - bool dragging = false; - bool buttonDown = false; - glm::vec2 start{0.0f}; - glm::vec2 current{0.0f}; - } _dragState; - // Optional list of last drag-selected objects (for future editing UI) - std::vector _dragSelection; - - // Toggle to enable/disable ID-buffer picking in addition to raycast - bool _useIdBufferPicking = false; - // Debug: draw mesh BVH boxes for last pick - bool _debugDrawBVH = false; - - // Last click selection (CPU ray or ID-buffer). Useful for game/editor code. - const PickInfo &get_last_pick() const { return _lastPick; } + PickingSystem *picking() { return _picking.get(); } + const PickingSystem *picking() const { return _picking.get(); } // Debug: persistent pass enable overrides (by pass name) std::unordered_map _rgPassToggles; diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index afb4b03..1021b6e 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -5,8 +5,10 @@ // The main frame loop in vk_engine.cpp simply calls vk_engine_draw_debug_ui(). #include "engine.h" +#include "core/picking/picking_system.h" #include "SDL2/SDL.h" +#include "SDL2/SDL_vulkan.h" #include "imgui.h" #include "ImGuizmo.h" @@ -110,6 +112,75 @@ namespace pending_display = current_display; pending_mode = static_cast(eng->_windowMode); } + + ImGui::Separator(); + ImGui::TextUnformatted("HiDPI / Sizes"); + ImGui::Text("HiDPI enabled: %s", eng->_hiDpiEnabled ? "yes" : "no"); + + int winW = 0, winH = 0; + SDL_GetWindowSize(eng->_window, &winW, &winH); + int drawW = 0, drawH = 0; + SDL_Vulkan_GetDrawableSize(eng->_window, &drawW, &drawH); + ImGui::Text("Window size: %d x %d", winW, winH); + ImGui::Text("Drawable size: %d x %d", drawW, drawH); + if (winW > 0 && winH > 0 && drawW > 0 && drawH > 0) + { + ImGui::Text("Drawable scale: %.3f x %.3f", + static_cast(drawW) / static_cast(winW), + static_cast(drawH) / static_cast(winH)); + } + if (eng->_swapchainManager) + { + VkExtent2D sw = eng->_swapchainManager->swapchainExtent(); + ImGui::Text("Swapchain extent: %u x %u", sw.width, sw.height); + } + + ImGui::Separator(); + ImGui::TextUnformatted("GPU"); + if (!eng->_deviceManager || !eng->_deviceManager->physicalDevice()) + { + ImGui::TextUnformatted("No Vulkan device initialized."); + return; + } + + VkPhysicalDevice gpu = eng->_deviceManager->physicalDevice(); + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(gpu, &props); + VkPhysicalDeviceMemoryProperties mem{}; + vkGetPhysicalDeviceMemoryProperties(gpu, &mem); + + auto type_str = [](VkPhysicalDeviceType t) -> const char* + { + switch (t) + { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "Discrete"; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "Integrated"; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "Virtual"; + case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU"; + default: return "Other"; + } + }; + + uint64_t device_local_bytes = 0; + for (uint32_t i = 0; i < mem.memoryHeapCount; ++i) + { + if (mem.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + { + device_local_bytes += mem.memoryHeaps[i].size; + } + } + const double vram_gib = static_cast(device_local_bytes) / (1024.0 * 1024.0 * 1024.0); + + const uint32_t api = props.apiVersion; + ImGui::Text("Name: %s", props.deviceName); + ImGui::Text("Type: %s", type_str(props.deviceType)); + ImGui::Text("Vendor: 0x%04x Device: 0x%04x", props.vendorID, props.deviceID); + ImGui::Text("Vulkan API: %u.%u.%u", VK_VERSION_MAJOR(api), VK_VERSION_MINOR(api), VK_VERSION_PATCH(api)); + ImGui::Text("Driver: %u (0x%08x)", props.driverVersion, props.driverVersion); + ImGui::Text("Device-local memory: %.2f GiB", vram_gib); + ImGui::Text("RayQuery: %s AccelStruct: %s", + eng->_deviceManager->supportsRayQuery() ? "yes" : "no", + eng->_deviceManager->supportsAccelerationStructure() ? "yes" : "no"); } // Background / compute playground @@ -962,10 +1033,27 @@ namespace const DrawContext &dc = eng->_context->getMainDrawContext(); ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size()); ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size()); - ImGui::Checkbox("Use ID-buffer picking", &eng->_useIdBufferPicking); - ImGui::Text("Picking mode: %s", - eng->_useIdBufferPicking ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); - ImGui::Checkbox("Debug draw mesh BVH (last pick)", &eng->_debugDrawBVH); + PickingSystem *picking = eng->picking(); + if (picking) + { + bool use_id = picking->use_id_buffer_picking(); + if (ImGui::Checkbox("Use ID-buffer picking", &use_id)) + { + picking->set_use_id_buffer_picking(use_id); + } + ImGui::Text("Picking mode: %s", + use_id ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); + + bool debug_bvh = picking->debug_draw_bvh(); + if (ImGui::Checkbox("Debug draw mesh BVH (last pick)", &debug_bvh)) + { + picking->set_debug_draw_bvh(debug_bvh); + } + } + else + { + ImGui::TextUnformatted("Picking system not available"); + } ImGui::Separator(); // Spawn glTF instances (runtime) @@ -1119,8 +1207,13 @@ namespace if (ImGui::Button("Delete selected")) { deleteStatus.clear(); - const auto *pick = eng->_lastPick.valid ? &eng->_lastPick - : (eng->_hoverPick.valid ? &eng->_hoverPick : nullptr); + const PickingSystem::PickInfo *pick = nullptr; + if (picking) + { + const auto &last = picking->last_pick(); + const auto &hover = picking->hover_pick(); + pick = last.valid ? &last : (hover.valid ? &hover : nullptr); + } if (!pick || pick->ownerName.empty()) { deleteStatus = "No selection to delete."; @@ -1128,6 +1221,10 @@ namespace else if (pick->ownerType == RenderObject::OwnerType::MeshInstance) { bool ok = eng->_sceneManager->removeMeshInstance(pick->ownerName); + if (ok && picking) + { + picking->clear_owner_picks(RenderObject::OwnerType::MeshInstance, pick->ownerName); + } deleteStatus = ok ? "Removed mesh instance: " + pick->ownerName : "Mesh instance not found: " + pick->ownerName; } @@ -1137,35 +1234,9 @@ namespace if (ok) { deleteStatus = "Removed glTF instance: " + pick->ownerName; - - // Debug: log and clear any picks that still reference the deleted instance. - fmt::println("[Debug] GLTF delete requested for '{}'; clearing picks if they match.", - pick->ownerName); - - if (eng->_lastPick.valid && - eng->_lastPick.ownerType == RenderObject::OwnerType::GLTFInstance && - eng->_lastPick.ownerName == pick->ownerName) + if (picking) { - fmt::println("[Debug] Clearing _lastPick for deleted GLTF instance '{}'", pick->ownerName); - eng->_lastPick.valid = false; - eng->_lastPick.ownerName.clear(); - eng->_lastPick.ownerType = RenderObject::OwnerType::None; - eng->_lastPick.mesh = nullptr; - eng->_lastPick.scene = nullptr; - eng->_lastPick.node = nullptr; - } - - if (eng->_hoverPick.valid && - eng->_hoverPick.ownerType == RenderObject::OwnerType::GLTFInstance && - eng->_hoverPick.ownerName == pick->ownerName) - { - fmt::println("[Debug] Clearing _hoverPick for deleted GLTF instance '{}'", pick->ownerName); - eng->_hoverPick.valid = false; - eng->_hoverPick.ownerName.clear(); - eng->_hoverPick.ownerType = RenderObject::OwnerType::None; - eng->_hoverPick.mesh = nullptr; - eng->_hoverPick.scene = nullptr; - eng->_hoverPick.node = nullptr; + picking->clear_owner_picks(RenderObject::OwnerType::GLTFInstance, pick->ownerName); } } else @@ -1184,36 +1255,37 @@ namespace } ImGui::Separator(); - if (eng->_lastPick.valid) + if (picking && picking->last_pick().valid) { - const char *meshName = eng->_lastPick.mesh ? eng->_lastPick.mesh->name.c_str() : ""; + const auto &last = picking->last_pick(); + const char *meshName = last.mesh ? last.mesh->name.c_str() : ""; const char *sceneName = ""; - if (eng->_lastPick.scene && !eng->_lastPick.scene->debugName.empty()) + if (last.scene && !last.scene->debugName.empty()) { - sceneName = eng->_lastPick.scene->debugName.c_str(); + sceneName = last.scene->debugName.c_str(); } ImGui::Text("Last pick scene: %s", sceneName); ImGui::Text("Last pick source: %s", - eng->_useIdBufferPicking ? "ID buffer" : "CPU raycast"); - ImGui::Text("Last pick object ID: %u", eng->_lastPickObjectID); - ImGui::Text("Last pick mesh: %s (surface %u)", meshName, eng->_lastPick.surfaceIndex); + picking->use_id_buffer_picking() ? "ID buffer" : "CPU raycast"); + ImGui::Text("Last pick object ID: %u", picking->last_pick_object_id()); + ImGui::Text("Last pick mesh: %s (surface %u)", meshName, last.surfaceIndex); ImGui::Text("World pos: (%.3f, %.3f, %.3f)", - eng->_lastPick.worldPos.x, - eng->_lastPick.worldPos.y, - eng->_lastPick.worldPos.z); + last.worldPos.x, + last.worldPos.y, + last.worldPos.z); const char *ownerTypeStr = "none"; - switch (eng->_lastPick.ownerType) + switch (last.ownerType) { case RenderObject::OwnerType::MeshInstance: ownerTypeStr = "mesh instance"; break; case RenderObject::OwnerType::GLTFInstance: ownerTypeStr = "glTF instance"; break; case RenderObject::OwnerType::StaticGLTF: ownerTypeStr = "glTF scene"; break; default: break; } - const char *ownerName = eng->_lastPick.ownerName.empty() ? "" : eng->_lastPick.ownerName.c_str(); + const char *ownerName = last.ownerName.empty() ? "" : last.ownerName.c_str(); ImGui::Text("Owner: %s (%s)", ownerName, ownerTypeStr); ImGui::Text("Indices: first=%u count=%u", - eng->_lastPick.firstIndex, - eng->_lastPick.indexCount); + last.firstIndex, + last.indexCount); if (eng->_sceneManager) { @@ -1235,28 +1307,29 @@ namespace ImGui::TextUnformatted("Last pick: "); } ImGui::Separator(); - if (eng->_hoverPick.valid) + if (picking && picking->hover_pick().valid) { - const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : ""; - ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex); + const auto &hover = picking->hover_pick(); + const char *meshName = hover.mesh ? hover.mesh->name.c_str() : ""; + ImGui::Text("Hover mesh: %s (surface %u)", meshName, hover.surfaceIndex); const char *ownerTypeStr = "none"; - switch (eng->_hoverPick.ownerType) + switch (hover.ownerType) { case RenderObject::OwnerType::MeshInstance: ownerTypeStr = "mesh instance"; break; case RenderObject::OwnerType::GLTFInstance: ownerTypeStr = "glTF instance"; break; case RenderObject::OwnerType::StaticGLTF: ownerTypeStr = "glTF scene"; break; default: break; } - const char *ownerName = eng->_hoverPick.ownerName.empty() ? "" : eng->_hoverPick.ownerName.c_str(); + const char *ownerName = hover.ownerName.empty() ? "" : hover.ownerName.c_str(); ImGui::Text("Hover owner: %s (%s)", ownerName, ownerTypeStr); } else { ImGui::TextUnformatted("Hover: "); } - if (!eng->_dragSelection.empty()) + if (picking && !picking->drag_selection().empty()) { - ImGui::Text("Drag selection: %zu objects", eng->_dragSelection.size()); + ImGui::Text("Drag selection: %zu objects", picking->drag_selection().size()); } ImGui::Separator(); @@ -1271,14 +1344,17 @@ namespace SceneManager *sceneMgr = eng->_sceneManager.get(); // Choose a pick to edit: prefer last pick, then hover. - VulkanEngine::PickInfo *pick = nullptr; - if (eng->_lastPick.valid) + PickingSystem::PickInfo *pick = nullptr; + if (picking) { - pick = &eng->_lastPick; - } - else if (eng->_hoverPick.valid) - { - pick = &eng->_hoverPick; + if (picking->last_pick().valid) + { + pick = picking->mutable_last_pick(); + } + else if (picking->hover_pick().valid) + { + pick = picking->mutable_hover_pick(); + } } if (!pick || pick->ownerName.empty()) diff --git a/src/core/game_api.cpp b/src/core/game_api.cpp index 175911d..5d87b4c 100644 --- a/src/core/game_api.cpp +++ b/src/core/game_api.cpp @@ -7,6 +7,7 @@ #include "render/passes/tonemap.h" #include "render/passes/fxaa.h" #include "render/renderpass.h" +#include "core/picking/picking_system.h" #include "scene/vk_scene.h" #include "scene/camera.h" @@ -1160,29 +1161,44 @@ Stats Engine::get_stats() const Engine::PickResult Engine::get_last_pick() const { PickResult r; - r.valid = _engine->_lastPick.valid; - r.ownerName = _engine->_lastPick.ownerName; - r.worldPosition = glm::vec3(_engine->_lastPick.worldPos); + const PickingSystem *picking = _engine ? _engine->picking() : nullptr; + if (picking) + { + const auto &pick = picking->last_pick(); + r.valid = pick.valid; + r.ownerName = pick.ownerName; + r.worldPosition = glm::vec3(pick.worldPos); + } return r; } Engine::PickResultD Engine::get_last_pick_d() const { PickResultD r; - r.valid = _engine->_lastPick.valid; - r.ownerName = _engine->_lastPick.ownerName; - r.worldPosition = _engine->_lastPick.worldPos; + const PickingSystem *picking = _engine ? _engine->picking() : nullptr; + if (picking) + { + const auto &pick = picking->last_pick(); + r.valid = pick.valid; + r.ownerName = pick.ownerName; + r.worldPosition = pick.worldPos; + } return r; } void Engine::set_use_id_buffer_picking(bool use) { - _engine->_useIdBufferPicking = use; + if (!_engine) return; + PickingSystem *picking = _engine->picking(); + if (!picking) return; + picking->set_use_id_buffer_picking(use); } bool Engine::get_use_id_buffer_picking() const { - return _engine->_useIdBufferPicking; + const PickingSystem *picking = _engine ? _engine->picking() : nullptr; + if (!picking) return false; + return picking->use_id_buffer_picking(); } } // namespace GameAPI diff --git a/src/core/picking/picking_system.cpp b/src/core/picking/picking_system.cpp new file mode 100644 index 0000000..12b885c --- /dev/null +++ b/src/core/picking/picking_system.cpp @@ -0,0 +1,405 @@ +#include "picking_system.h" + +#include "core/context.h" +#include "core/device/device.h" +#include "core/device/images.h" +#include "core/device/swapchain.h" +#include "render/graph/graph.h" + +#include "SDL2/SDL.h" +#include "SDL2/SDL_vulkan.h" + +#include +#include + +void PickingSystem::init(EngineContext *context) +{ + _context = context; + _last_pick = {}; + _hover_pick = {}; + _drag_selection.clear(); + _mouse_pos_window = glm::vec2{-1.0f, -1.0f}; + _drag_state = {}; + _pending_pick = {}; + _pick_result_pending = false; + _last_pick_object_id = 0; + + _pick_readback_buffer = {}; + if (_context && _context->getResources()) + { + _pick_readback_buffer = _context->getResources()->create_buffer( + sizeof(uint32_t), + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + } +} + +void PickingSystem::cleanup() +{ + if (_pick_readback_buffer.buffer && _context && _context->getResources()) + { + _context->getResources()->destroy_buffer(_pick_readback_buffer); + } + _pick_readback_buffer = {}; + + _context = nullptr; + _last_pick = {}; + _hover_pick = {}; + _drag_selection.clear(); + _pending_pick = {}; + _pick_result_pending = false; + _last_pick_object_id = 0; +} + +void PickingSystem::process_event(const SDL_Event &event, bool ui_want_capture_mouse) +{ + if (_context == nullptr) + { + return; + } + + if (event.type == SDL_MOUSEMOTION) + { + _mouse_pos_window = glm::vec2{static_cast(event.motion.x), + static_cast(event.motion.y)}; + if (_drag_state.button_down) + { + _drag_state.current = _mouse_pos_window; + _drag_state.dragging = true; + } + return; + } + + if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) + { + if (ui_want_capture_mouse) + { + return; + } + + _drag_state.button_down = true; + _drag_state.dragging = false; + _drag_state.start = glm::vec2{static_cast(event.button.x), + static_cast(event.button.y)}; + _drag_state.current = _drag_state.start; + return; + } + + if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) + { + const bool was_down = _drag_state.button_down; + _drag_state.button_down = false; + if (!was_down) + { + _drag_state.dragging = false; + return; + } + + glm::vec2 release_pos{static_cast(event.button.x), + static_cast(event.button.y)}; + + constexpr float click_threshold = 3.0f; + glm::vec2 delta = release_pos - _drag_state.start; + bool treat_as_click = !_drag_state.dragging && + std::abs(delta.x) < click_threshold && + std::abs(delta.y) < click_threshold; + + SceneManager *scene = _context->scene; + + if (treat_as_click) + { + if (_use_id_buffer_picking) + { + _pending_pick.active = true; + _pending_pick.window_pos_swapchain = window_to_swapchain_pixels(release_pos); + } + else if (scene) + { + RenderObject hit_object{}; + WorldVec3 hit_pos{}; + if (scene->pick(window_to_swapchain_pixels(release_pos), hit_object, hit_pos)) + { + set_pick_from_hit(hit_object, hit_pos, _last_pick); + _last_pick_object_id = hit_object.objectID; + } + else + { + clear_pick(_last_pick); + _last_pick_object_id = 0; + } + } + } + else + { + _drag_selection.clear(); + if (scene) + { + std::vector selected; + scene->selectRect(window_to_swapchain_pixels(_drag_state.start), + window_to_swapchain_pixels(release_pos), + selected); + _drag_selection.reserve(selected.size()); + for (const RenderObject &obj : selected) + { + PickInfo info{}; + info.mesh = obj.sourceMesh; + info.scene = obj.sourceScene; + info.node = obj.sourceNode; + info.ownerType = obj.ownerType; + info.ownerName = obj.ownerName; + glm::vec3 center_local = glm::vec3(obj.transform * glm::vec4(obj.bounds.origin, 1.0f)); + info.worldPos = local_to_world(center_local, scene->get_world_origin()); + info.worldTransform = obj.transform; + info.firstIndex = obj.firstIndex; + info.indexCount = obj.indexCount; + info.surfaceIndex = obj.surfaceIndex; + info.valid = true; + _drag_selection.push_back(std::move(info)); + } + } + } + + _drag_state.dragging = false; + } +} + +void PickingSystem::update_hover() +{ + if (_context == nullptr || _context->scene == nullptr) + { + return; + } + + if (_mouse_pos_window.x < 0.0f || _mouse_pos_window.y < 0.0f) + { + clear_pick(_hover_pick); + return; + } + + RenderObject hover_obj{}; + WorldVec3 hover_pos{}; + if (_context->scene->pick(window_to_swapchain_pixels(_mouse_pos_window), hover_obj, hover_pos)) + { + set_pick_from_hit(hover_obj, hover_pos, _hover_pick); + } + else + { + clear_pick(_hover_pick); + } +} + +void PickingSystem::begin_frame() +{ + if (!_pick_result_pending || !_pick_readback_buffer.buffer || _context == nullptr || _context->scene == nullptr) + { + return; + } + + DeviceManager *dev = _context->getDevice(); + if (!dev) + { + return; + } + + vmaInvalidateAllocation(dev->allocator(), _pick_readback_buffer.allocation, 0, sizeof(uint32_t)); + + uint32_t picked_id = 0; + if (_pick_readback_buffer.info.pMappedData) + { + picked_id = *reinterpret_cast(_pick_readback_buffer.info.pMappedData); + } + + if (picked_id == 0) + { + clear_pick(_last_pick); + _last_pick_object_id = 0; + } + else + { + _last_pick_object_id = picked_id; + RenderObject picked{}; + if (_context->scene->resolveObjectID(picked_id, picked)) + { + glm::vec3 fallback_local = glm::vec3(picked.transform[3]); + WorldVec3 fallback_pos = local_to_world(fallback_local, _context->scene->get_world_origin()); + set_pick_from_hit(picked, fallback_pos, _last_pick); + } + else + { + clear_pick(_last_pick); + _last_pick_object_id = 0; + } + } + + _pick_result_pending = false; +} + +void PickingSystem::register_id_buffer_readback(RenderGraph &graph, + RGImageHandle id_buffer, + VkExtent2D draw_extent, + VkExtent2D swapchain_extent) +{ + if (!_use_id_buffer_picking || !_pending_pick.active || !id_buffer.valid() || !_pick_readback_buffer.buffer) + { + return; + } + + if (draw_extent.width == 0 || draw_extent.height == 0 || swapchain_extent.width == 0 || swapchain_extent.height == 0) + { + _pending_pick.active = false; + return; + } + + glm::vec2 logical_pos{}; + if (!vkutil::map_window_to_letterbox_src(_pending_pick.window_pos_swapchain, draw_extent, swapchain_extent, logical_pos)) + { + _pending_pick.active = false; + return; + } + + const uint32_t id_x = static_cast(std::clamp(logical_pos.x, 0.0f, float(draw_extent.width - 1))); + const uint32_t id_y = static_cast(std::clamp(logical_pos.y, 0.0f, float(draw_extent.height - 1))); + _pending_pick.id_coords = {id_x, id_y}; + + RGImportedBufferDesc bd{}; + bd.name = "pick.readback"; + bd.buffer = _pick_readback_buffer.buffer; + bd.size = sizeof(uint32_t); + bd.currentStage = VK_PIPELINE_STAGE_2_NONE; + bd.currentAccess = 0; + RGBufferHandle pick_buf = graph.import_buffer(bd); + + const glm::uvec2 coords = _pending_pick.id_coords; + graph.add_pass( + "PickReadback", + RGPassType::Transfer, + [id_buffer, pick_buf](RGPassBuilder &builder, EngineContext *) + { + builder.read(id_buffer, RGImageUsage::TransferSrc); + builder.write_buffer(pick_buf, RGBufferUsage::TransferDst); + }, + [coords, id_buffer, pick_buf](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *) + { + VkImage id_image = res.image(id_buffer); + VkBuffer dst = res.buffer(pick_buf); + if (id_image == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return; + + VkBufferImageCopy region{}; + region.bufferOffset = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = {static_cast(coords.x), + static_cast(coords.y), + 0}; + region.imageExtent = {1, 1, 1}; + + vkCmdCopyImageToBuffer(cmd, + id_image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + dst, + 1, + ®ion); + }); + + _pick_result_pending = true; + _pending_pick.active = false; +} + +void PickingSystem::clear_owner_picks(RenderObject::OwnerType owner_type, const std::string &owner_name) +{ + if (_last_pick.valid && _last_pick.ownerType == owner_type && _last_pick.ownerName == owner_name) + { + clear_pick(_last_pick); + _last_pick_object_id = 0; + } + if (_hover_pick.valid && _hover_pick.ownerType == owner_type && _hover_pick.ownerName == owner_name) + { + clear_pick(_hover_pick); + } + + if (!_drag_selection.empty()) + { + _drag_selection.erase(std::remove_if(_drag_selection.begin(), + _drag_selection.end(), + [&](const PickInfo &p) { + return p.valid && p.ownerType == owner_type && p.ownerName == owner_name; + }), + _drag_selection.end()); + } +} + +glm::vec2 PickingSystem::window_to_swapchain_pixels(const glm::vec2 &window_pos) const +{ + if (_context == nullptr || _context->window == nullptr || _context->getSwapchain() == nullptr) + { + return window_pos; + } + + int win_w = 0, win_h = 0; + SDL_GetWindowSize(_context->window, &win_w, &win_h); + + int draw_w = 0, draw_h = 0; + SDL_Vulkan_GetDrawableSize(_context->window, &draw_w, &draw_h); + + glm::vec2 scale{1.0f, 1.0f}; + if (win_w > 0 && win_h > 0 && draw_w > 0 && draw_h > 0) + { + scale.x = static_cast(draw_w) / static_cast(win_w); + scale.y = static_cast(draw_h) / static_cast(win_h); + } + + glm::vec2 drawable_pos{window_pos.x * scale.x, window_pos.y * scale.y}; + + VkExtent2D drawable_extent{0, 0}; + if (draw_w > 0 && draw_h > 0) + { + drawable_extent.width = static_cast(draw_w); + drawable_extent.height = static_cast(draw_h); + } + if ((drawable_extent.width == 0 || drawable_extent.height == 0) && _context->getSwapchain()) + { + drawable_extent = _context->getSwapchain()->windowExtent(); + } + + VkExtent2D swap = _context->getSwapchain()->swapchainExtent(); + if (drawable_extent.width == 0 || drawable_extent.height == 0 || swap.width == 0 || swap.height == 0) + { + return drawable_pos; + } + + const float sx = static_cast(swap.width) / static_cast(drawable_extent.width); + const float sy = static_cast(swap.height) / static_cast(drawable_extent.height); + return glm::vec2{drawable_pos.x * sx, drawable_pos.y * sy}; +} + +void PickingSystem::set_pick_from_hit(const RenderObject &hit_object, const WorldVec3 &hit_pos, PickInfo &out_pick) +{ + out_pick.mesh = hit_object.sourceMesh; + out_pick.scene = hit_object.sourceScene; + out_pick.node = hit_object.sourceNode; + out_pick.ownerType = hit_object.ownerType; + out_pick.ownerName = hit_object.ownerName; + out_pick.worldPos = hit_pos; + out_pick.worldTransform = hit_object.transform; + out_pick.firstIndex = hit_object.firstIndex; + out_pick.indexCount = hit_object.indexCount; + out_pick.surfaceIndex = hit_object.surfaceIndex; + out_pick.valid = true; +} + +void PickingSystem::clear_pick(PickInfo &pick) +{ + pick.mesh = nullptr; + pick.scene = nullptr; + pick.node = nullptr; + pick.ownerType = RenderObject::OwnerType::None; + pick.ownerName.clear(); + pick.worldPos = WorldVec3{0.0, 0.0, 0.0}; + pick.worldTransform = glm::mat4(1.0f); + pick.indexCount = 0; + pick.firstIndex = 0; + pick.surfaceIndex = 0; + pick.valid = false; +} diff --git a/src/core/picking/picking_system.h b/src/core/picking/picking_system.h new file mode 100644 index 0000000..111aade --- /dev/null +++ b/src/core/picking/picking_system.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include + +#include +#include + +union SDL_Event; +class EngineContext; +class RenderGraph; +struct RGImageHandle; + +class PickingSystem +{ +public: + struct PickInfo + { + MeshAsset *mesh = nullptr; + LoadedGLTF *scene = nullptr; + Node *node = nullptr; + RenderObject::OwnerType ownerType = RenderObject::OwnerType::None; + std::string ownerName; + WorldVec3 worldPos{0.0, 0.0, 0.0}; + glm::mat4 worldTransform{1.0f}; + uint32_t indexCount = 0; + uint32_t firstIndex = 0; + uint32_t surfaceIndex = 0; + bool valid = false; + }; + + void init(EngineContext *context); + void cleanup(); + + void process_event(const SDL_Event &event, bool ui_want_capture_mouse); + void update_hover(); + + // Called after the per-frame fence is waited to resolve async ID-buffer picks. + void begin_frame(); + + // Called during RenderGraph build after the ID buffer is available. + void register_id_buffer_readback(RenderGraph &graph, + RGImageHandle id_buffer, + VkExtent2D draw_extent, + VkExtent2D swapchain_extent); + + const PickInfo &last_pick() const { return _last_pick; } + const PickInfo &hover_pick() const { return _hover_pick; } + const std::vector &drag_selection() const { return _drag_selection; } + + uint32_t last_pick_object_id() const { return _last_pick_object_id; } + + bool use_id_buffer_picking() const { return _use_id_buffer_picking; } + void set_use_id_buffer_picking(bool enabled) { _use_id_buffer_picking = enabled; } + + bool debug_draw_bvh() const { return _debug_draw_bvh; } + void set_debug_draw_bvh(bool enabled) { _debug_draw_bvh = enabled; } + + void clear_owner_picks(RenderObject::OwnerType owner_type, const std::string &owner_name); + + PickInfo *mutable_last_pick() { return &_last_pick; } + PickInfo *mutable_hover_pick() { return &_hover_pick; } + +private: + struct PickRequest + { + bool active = false; + glm::vec2 window_pos_swapchain{0.0f}; + glm::uvec2 id_coords{0, 0}; + }; + + struct DragState + { + bool dragging = false; + bool button_down = false; + glm::vec2 start{0.0f}; + glm::vec2 current{0.0f}; + }; + + glm::vec2 window_to_swapchain_pixels(const glm::vec2 &window_pos) const; + void set_pick_from_hit(const RenderObject &hit_object, const WorldVec3 &hit_pos, PickInfo &out_pick); + void clear_pick(PickInfo &pick); + + EngineContext *_context = nullptr; + + PickInfo _last_pick{}; + PickInfo _hover_pick{}; + std::vector _drag_selection{}; + + glm::vec2 _mouse_pos_window{-1.0f, -1.0f}; + DragState _drag_state{}; + + bool _use_id_buffer_picking = false; + bool _debug_draw_bvh = false; + + uint32_t _last_pick_object_id = 0; + PickRequest _pending_pick{}; + bool _pick_result_pending = false; + + AllocatedBuffer _pick_readback_buffer{}; +}; diff --git a/src/core/ui/imgui_system.cpp b/src/core/ui/imgui_system.cpp new file mode 100644 index 0000000..90abf38 --- /dev/null +++ b/src/core/ui/imgui_system.cpp @@ -0,0 +1,309 @@ +#include "imgui_system.h" + +#include "core/context.h" +#include "core/device/swapchain.h" + +#include "SDL2/SDL.h" +#include "SDL2/SDL_vulkan.h" + +#include "imgui.h" +#include "imgui_impl_sdl2.h" +#include "imgui_impl_vulkan.h" + +#include +#include + +#include "device.h" + +namespace +{ + VkDescriptorPool create_imgui_descriptor_pool(VkDevice device) + { + VkDescriptorPoolSize pool_sizes[] = { + {VK_DESCRIPTOR_TYPE_SAMPLER, 1000}, + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000}, + {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000}, + {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000} + }; + + VkDescriptorPoolCreateInfo pool_info{}; + pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + pool_info.maxSets = 1000; + pool_info.poolSizeCount = static_cast(std::size(pool_sizes)); + pool_info.pPoolSizes = pool_sizes; + + VkDescriptorPool pool = VK_NULL_HANDLE; + VK_CHECK(vkCreateDescriptorPool(device, &pool_info, nullptr, &pool)); + return pool; + } + + uint32_t clamp_imgui_image_count(uint32_t count) + { + if (count < 2) return 2; + if (count > 8) return 8; + return count; + } +} // namespace + +void ImGuiSystem::init(EngineContext *context) +{ + if (_initialized) + { + return; + } + + _context = context; + if (_context == nullptr || _context->getDevice() == nullptr || _context->getSwapchain() == nullptr || _context->window == nullptr) + { + fmt::println("[ImGuiSystem] init skipped (missing context/device/swapchain/window)"); + return; + } + + VkDevice device = _context->getDevice()->device(); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + + ImGuiIO &io = ImGui::GetIO(); + + _swapchain_format = _context->getSwapchain()->swapchainImageFormat(); + + _dpi_scale = std::clamp(compute_dpi_scale(), 0.5f, 4.0f); + rebuild_fonts(_dpi_scale); + + _imgui_pool = create_imgui_descriptor_pool(device); + + ImGui_ImplSDL2_InitForVulkan(_context->window); + + ImGui_ImplVulkan_InitInfo init_info{}; + init_info.Instance = _context->getDevice()->instance(); + init_info.PhysicalDevice = _context->getDevice()->physicalDevice(); + init_info.Device = _context->getDevice()->device(); + init_info.QueueFamily = _context->getDevice()->graphicsQueueFamily(); + init_info.Queue = _context->getDevice()->graphicsQueue(); + init_info.DescriptorPool = _imgui_pool; + + const auto &images = _context->getSwapchain()->swapchainImages(); + uint32_t image_count = images.empty() ? 3u : clamp_imgui_image_count(static_cast(images.size())); + init_info.MinImageCount = image_count; + init_info.ImageCount = image_count; + + init_info.UseDynamicRendering = true; + init_info.PipelineRenderingCreateInfo = {}; + init_info.PipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; + init_info.PipelineRenderingCreateInfo.colorAttachmentCount = 1; + init_info.PipelineRenderingCreateInfo.pColorAttachmentFormats = &_swapchain_format; + init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + + ImGui_ImplVulkan_Init(&init_info); + + if (!ImGui_ImplVulkan_CreateFontsTexture()) + { + fmt::println("[ImGuiSystem] Warning: ImGui_ImplVulkan_CreateFontsTexture() failed"); + } + + io.FontGlobalScale = (_dpi_scale > 0.0f) ? (1.0f / _dpi_scale) : 1.0f; + + _initialized = true; +} + +void ImGuiSystem::cleanup() +{ + if (!_initialized) + { + return; + } + + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplSDL2_Shutdown(); + + if (_context && _context->getDevice() && _imgui_pool != VK_NULL_HANDLE) + { + vkDestroyDescriptorPool(_context->getDevice()->device(), _imgui_pool, nullptr); + } + _imgui_pool = VK_NULL_HANDLE; + + ImGui::DestroyContext(); + + _draw_callbacks.clear(); + _context = nullptr; + _initialized = false; +} + +void ImGuiSystem::process_event(const SDL_Event &event) +{ + if (!_initialized) + { + return; + } + ImGui_ImplSDL2_ProcessEvent(const_cast(&event)); +} + +void ImGuiSystem::begin_frame() +{ + if (!_initialized) + { + return; + } + + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplSDL2_NewFrame(); + + update_framebuffer_scale(); + + const float new_scale = std::clamp(compute_dpi_scale(), 0.5f, 4.0f); + if (std::isfinite(new_scale) && std::abs(new_scale - _dpi_scale) > 0.05f) + { + rebuild_fonts(new_scale); + _dpi_scale = new_scale; + } + + ImGui::NewFrame(); + + for (auto &cb : _draw_callbacks) + { + if (cb) cb(); + } +} + +void ImGuiSystem::end_frame() +{ + if (!_initialized) + { + return; + } + ImGui::Render(); +} + +void ImGuiSystem::add_draw_callback(DrawCallback callback) +{ + _draw_callbacks.push_back(std::move(callback)); +} + +void ImGuiSystem::clear_draw_callbacks() +{ + _draw_callbacks.clear(); +} + +bool ImGuiSystem::want_capture_mouse() const +{ + if (!_initialized) return false; + return ImGui::GetIO().WantCaptureMouse; +} + +bool ImGuiSystem::want_capture_keyboard() const +{ + if (!_initialized) return false; + return ImGui::GetIO().WantCaptureKeyboard; +} + +void ImGuiSystem::on_swapchain_recreated() +{ + if (!_initialized || _context == nullptr || _context->getSwapchain() == nullptr) + { + return; + } + + const auto &images = _context->getSwapchain()->swapchainImages(); + uint32_t image_count = images.empty() ? 3u : clamp_imgui_image_count(static_cast(images.size())); + ImGui_ImplVulkan_SetMinImageCount(image_count); + + update_framebuffer_scale(); +} + +float ImGuiSystem::compute_dpi_scale() const +{ + if (_context == nullptr || _context->window == nullptr || _context->getSwapchain() == nullptr) + { + return 1.0f; + } + + int win_w = 0, win_h = 0; + SDL_GetWindowSize(_context->window, &win_w, &win_h); + if (win_w <= 0 || win_h <= 0) + { + return _dpi_scale > 0.0f ? _dpi_scale : 1.0f; + } + + VkExtent2D swap = _context->getSwapchain()->swapchainExtent(); + if (swap.width == 0 || swap.height == 0) + { + return _dpi_scale > 0.0f ? _dpi_scale : 1.0f; + } + + float sx = static_cast(swap.width) / static_cast(win_w); + float sy = static_cast(swap.height) / static_cast(win_h); + + if (!std::isfinite(sx) || !std::isfinite(sy)) + { + return 1.0f; + } + + return 0.5f * (sx + sy); +} + +void ImGuiSystem::update_framebuffer_scale() +{ + if (_context == nullptr || _context->window == nullptr || _context->getSwapchain() == nullptr) + { + return; + } + + int win_w = 0, win_h = 0; + SDL_GetWindowSize(_context->window, &win_w, &win_h); + if (win_w <= 0 || win_h <= 0) + { + return; + } + + VkExtent2D swap = _context->getSwapchain()->swapchainExtent(); + if (swap.width == 0 || swap.height == 0) + { + return; + } + + ImGuiIO &io = ImGui::GetIO(); + io.DisplayFramebufferScale = ImVec2(static_cast(swap.width) / static_cast(win_w), + static_cast(swap.height) / static_cast(win_h)); + + const float scale = std::clamp(compute_dpi_scale(), 0.5f, 4.0f); + io.FontGlobalScale = (scale > 0.0f) ? (1.0f / scale) : 1.0f; +} + +void ImGuiSystem::rebuild_fonts(float dpi_scale) +{ + if (_initialized) + { + ImGui_ImplVulkan_DestroyFontsTexture(); + } + + ImGuiIO &io = ImGui::GetIO(); + io.Fonts->Clear(); + + ImFontConfig cfg{}; + cfg.SizePixels = _base_font_size * dpi_scale; + cfg.OversampleH = 3; + cfg.OversampleV = 2; + cfg.PixelSnapH = false; + + io.Fonts->AddFontDefault(&cfg); + + io.FontGlobalScale = (dpi_scale > 0.0f) ? (1.0f / dpi_scale) : 1.0f; + + if (_initialized) + { + if (!ImGui_ImplVulkan_CreateFontsTexture()) + { + fmt::println("[ImGuiSystem] Warning: ImGui_ImplVulkan_CreateFontsTexture() failed after DPI change"); + } + } +} diff --git a/src/core/ui/imgui_system.h b/src/core/ui/imgui_system.h new file mode 100644 index 0000000..da9a4ee --- /dev/null +++ b/src/core/ui/imgui_system.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include + +union SDL_Event; +class EngineContext; + +class ImGuiSystem +{ +public: + using DrawCallback = std::function; + + void init(EngineContext *context); + void cleanup(); + + void process_event(const SDL_Event &event); + + void begin_frame(); + void end_frame(); + + void add_draw_callback(DrawCallback callback); + void clear_draw_callbacks(); + + bool want_capture_mouse() const; + bool want_capture_keyboard() const; + + void on_swapchain_recreated(); + +private: + float compute_dpi_scale() const; + void update_framebuffer_scale(); + void rebuild_fonts(float dpi_scale); + + EngineContext *_context = nullptr; + std::vector _draw_callbacks; + + VkDescriptorPool _imgui_pool = VK_NULL_HANDLE; + VkFormat _swapchain_format = VK_FORMAT_UNDEFINED; + + float _dpi_scale = 1.0f; + float _base_font_size = 16.0f; + bool _initialized = false; +}; diff --git a/src/render/passes/imgui_pass.cpp b/src/render/passes/imgui_pass.cpp index fafccac..fe8c07d 100644 --- a/src/render/passes/imgui_pass.cpp +++ b/src/render/passes/imgui_pass.cpp @@ -1,78 +1,17 @@ #include "imgui_pass.h" #include "imgui.h" -#include "imgui_impl_sdl2.h" #include "imgui_impl_vulkan.h" -#include "core/device/device.h" -#include "core/device/swapchain.h" -#include "core/util/initializers.h" #include "core/context.h" #include "render/graph/graph.h" void ImGuiPass::init(EngineContext *context) { _context = context; - - VkDescriptorPoolSize pool_sizes[] = { - {VK_DESCRIPTOR_TYPE_SAMPLER, 1000}, - {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000}, - {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000}, - {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000}, - {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000}, - {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000}, - {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000}, - {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000}, - {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000} - }; - - VkDescriptorPoolCreateInfo pool_info = {}; - pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; - pool_info.maxSets = 1000; - pool_info.poolSizeCount = (uint32_t) std::size(pool_sizes); - pool_info.pPoolSizes = pool_sizes; - - VkDescriptorPool imguiPool; - VK_CHECK(vkCreateDescriptorPool(_context->device->device(), &pool_info, nullptr, &imguiPool)); - - ImGui::CreateContext(); - - ImGui_ImplSDL2_InitForVulkan(_context->window); - - ImGui_ImplVulkan_InitInfo init_info = {}; - init_info.Instance = _context->getDevice()->instance(); - init_info.PhysicalDevice = _context->getDevice()->physicalDevice(); - init_info.Device = _context->getDevice()->device(); - init_info.Queue = _context->getDevice()->graphicsQueue(); - init_info.DescriptorPool = imguiPool; - init_info.MinImageCount = 3; - init_info.ImageCount = 3; - init_info.UseDynamicRendering = true; - - init_info.PipelineRenderingCreateInfo = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO}; - init_info.PipelineRenderingCreateInfo.colorAttachmentCount = 1; - auto _swapchainImageFormat = _context->getSwapchain()->swapchainImageFormat(); - init_info.PipelineRenderingCreateInfo.pColorAttachmentFormats = &_swapchainImageFormat; - - init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; - - ImGui_ImplVulkan_Init(&init_info); - - ImGui_ImplVulkan_CreateFontsTexture(); - - // add the destroy the imgui created structures - _deletionQueue.push_function([=]() { - ImGui_ImplVulkan_Shutdown(); - vkDestroyDescriptorPool(_context->getDevice()->device(), imguiPool, nullptr); - }); } void ImGuiPass::cleanup() { - fmt::print("ImGuiPass::cleanup()\n"); - _deletionQueue.flush(); } void ImGuiPass::execute(VkCommandBuffer) diff --git a/src/render/passes/imgui_pass.h b/src/render/passes/imgui_pass.h index 066e21b..c3608a1 100644 --- a/src/render/passes/imgui_pass.h +++ b/src/render/passes/imgui_pass.h @@ -25,5 +25,4 @@ private: const class RGPassResources &resources, RGImageHandle targetHandle) const; - DeletionQueue _deletionQueue; }; diff --git a/src/scene/vk_scene_picking.cpp b/src/scene/vk_scene_picking.cpp index 5990f66..9eb4fea 100644 --- a/src/scene/vk_scene_picking.cpp +++ b/src/scene/vk_scene_picking.cpp @@ -434,7 +434,7 @@ bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject return false; } - VkExtent2D dstExtent = swapchain->windowExtent(); + VkExtent2D dstExtent = swapchain->swapchainExtent(); if (dstExtent.width == 0 || dstExtent.height == 0) { return false; @@ -584,7 +584,7 @@ void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vec } SwapchainManager *swapchain = _context->getSwapchain(); - VkExtent2D dstExtent = swapchain->windowExtent(); + VkExtent2D dstExtent = swapchain->swapchainExtent(); if (dstExtent.width == 0 || dstExtent.height == 0) { return;