diff --git a/src/core/engine.cpp b/src/core/engine.cpp index a83d39a..a399e70 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -188,6 +188,23 @@ void VulkanEngine::init() window_flags ); + _windowMode = WindowMode::Windowed; + _windowDisplayIndex = SDL_GetWindowDisplayIndex(_window); + if (_windowDisplayIndex < 0) _windowDisplayIndex = 0; + { + int wx = 0, wy = 0, ww = 0, wh = 0; + SDL_GetWindowPosition(_window, &wx, &wy); + SDL_GetWindowSize(_window, &ww, &wh); + if (ww > 0 && wh > 0) + { + _windowedRect.x = wx; + _windowedRect.y = wy; + _windowedRect.w = ww; + _windowedRect.h = wh; + _windowedRect.valid = true; + } + } + _deviceManager = std::make_shared(); _deviceManager->init_vulkan(_window); @@ -332,6 +349,125 @@ void VulkanEngine::init() _isInitialized = true; } +void VulkanEngine::set_window_mode(WindowMode mode, int display_index) +{ + if (!_window) return; + + const int num_displays = SDL_GetNumVideoDisplays(); + int current_display = SDL_GetWindowDisplayIndex(_window); + if (current_display < 0) current_display = 0; + + if (num_displays <= 0) + { + display_index = 0; + } + else + { + if (display_index < 0) display_index = current_display; + display_index = std::clamp(display_index, 0, num_displays - 1); + } + + const WindowMode prev_mode = _windowMode; + const bool entering_fullscreen = (prev_mode == WindowMode::Windowed) && (mode != WindowMode::Windowed); + + if (entering_fullscreen) + { + int wx = 0, wy = 0, ww = 0, wh = 0; + SDL_GetWindowPosition(_window, &wx, &wy); + SDL_GetWindowSize(_window, &ww, &wh); + if (ww > 0 && wh > 0) + { + _windowedRect.x = wx; + _windowedRect.y = wy; + _windowedRect.w = ww; + _windowedRect.h = wh; + _windowedRect.valid = true; + } + } + + // If we are currently in any fullscreen mode, leave fullscreen first so we can safely + // move the window to the target display before re-entering fullscreen. + if (prev_mode != WindowMode::Windowed) + { + if (SDL_SetWindowFullscreen(_window, 0) != 0) + { + fmt::println("[Window] SDL_SetWindowFullscreen(0) failed: {}", SDL_GetError()); + } + } + + // Move the window to the selected display (applies to both windowed and fullscreen). + SDL_SetWindowPosition(_window, + SDL_WINDOWPOS_CENTERED_DISPLAY(display_index), + SDL_WINDOWPOS_CENTERED_DISPLAY(display_index)); + + if (mode == WindowMode::Windowed) + { + SDL_SetWindowBordered(_window, SDL_TRUE); + SDL_SetWindowResizable(_window, SDL_TRUE); + + int target_w = 0, target_h = 0; + if (_windowedRect.valid) + { + target_w = _windowedRect.w; + target_h = _windowedRect.h; + } + if (target_w <= 0 || target_h <= 0) + { + SDL_GetWindowSize(_window, &target_w, &target_h); + } + if (target_w > 0 && target_h > 0) + { + SDL_SetWindowSize(_window, target_w, target_h); + } + } + else + { + SDL_SetWindowBordered(_window, SDL_FALSE); + + const Uint32 fullscreen_flag = + (mode == WindowMode::FullscreenDesktop) ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN; + + if (mode == WindowMode::FullscreenExclusive) + { + SDL_DisplayMode desktop{}; + if (SDL_GetDesktopDisplayMode(display_index, &desktop) == 0) + { + // Request desktop mode by default. Future UI can add explicit mode selection. + SDL_SetWindowDisplayMode(_window, &desktop); + } + } + + if (SDL_SetWindowFullscreen(_window, fullscreen_flag) != 0) + { + fmt::println("[Window] SDL_SetWindowFullscreen({}) failed: {}", + static_cast(fullscreen_flag), + SDL_GetError()); + } + } + + _windowMode = mode; + _windowDisplayIndex = SDL_GetWindowDisplayIndex(_window); + if (_windowDisplayIndex < 0) _windowDisplayIndex = display_index; + + // Make sure SDL has processed the fullscreen/move requests before we query drawable size for swapchain recreation. + SDL_PumpEvents(); + + // Recreate swapchain immediately so the very next draw uses correct extent. + if (_swapchainManager) + { + _swapchainManager->resize_swapchain(_window); + if (_swapchainManager->resize_requested) + { + resize_requested = true; + _last_resize_event_ms = 0; + } + else + { + resize_requested = false; + } + } +} + void VulkanEngine::set_logical_render_extent(VkExtent2D extent) { extent = clamp_nonzero_extent(extent); diff --git a/src/core/engine.h b/src/core/engine.h index 08734d7..07096c9 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "vk_mem_alloc.h" #include #include @@ -62,6 +63,13 @@ struct MeshNode : public Node class VulkanEngine { public: + enum class WindowMode : uint8_t + { + Windowed = 0, + FullscreenDesktop = 1, // borderless fullscreen ("fullscreen windowed") + FullscreenExclusive = 2 // exclusive fullscreen (may change display mode) + }; + bool _isInitialized{false}; int _frameNumber{0}; @@ -80,6 +88,9 @@ public: struct SDL_Window *_window{nullptr}; + WindowMode _windowMode{WindowMode::Windowed}; + int _windowDisplayIndex{0}; + FrameResources _frames[FRAME_OVERLAP]; FrameResources &get_current_frame() { return _frames[_frameNumber % FRAME_OVERLAP]; }; @@ -204,6 +215,9 @@ public: //run main loop void run(); + // Window controls (runtime) + void set_window_mode(WindowMode mode, int display_index); + // Rendering resolution controls: // - logicalRenderExtent controls camera aspect and picking (letterboxed view). // - renderScale controls the internal render target pixel count (logical * scale). @@ -258,6 +272,15 @@ public: bool freeze_rendering{false}; private: + struct WindowedRect + { + int x{0}; + int y{0}; + int w{0}; + int h{0}; + bool valid{false}; + } _windowedRect; + void init_frame_resources(); void init_pipelines(); diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 181694c..afb4b03 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -6,6 +6,8 @@ #include "engine.h" +#include "SDL2/SDL.h" + #include "imgui.h" #include "ImGuizmo.h" @@ -23,12 +25,93 @@ #include "device/images.h" #include "context.h" #include +#include #include #include "mesh_bvh.h" namespace { + static void ui_window(VulkanEngine *eng) + { + if (!eng || !eng->_window) return; + + int num_displays = SDL_GetNumVideoDisplays(); + if (num_displays <= 0) + { + ImGui::Text("No displays reported by SDL (%s)", SDL_GetError()); + return; + } + + int current_display = SDL_GetWindowDisplayIndex(eng->_window); + if (current_display < 0) current_display = eng->_windowDisplayIndex; + current_display = std::clamp(current_display, 0, num_displays - 1); + + const char *cur_display_name = SDL_GetDisplayName(current_display); + if (!cur_display_name) cur_display_name = "Unknown"; + + ImGui::Text("Current: %s on display %d (%s)", + (eng->_windowMode == VulkanEngine::WindowMode::Windowed) + ? "Windowed" + : (eng->_windowMode == VulkanEngine::WindowMode::FullscreenDesktop) + ? "Borderless Fullscreen" + : "Exclusive Fullscreen", + current_display, + cur_display_name); + + static int pending_display = -1; + static int pending_mode = -1; // 0 windowed, 1 borderless, 2 exclusive + if (pending_display < 0) pending_display = current_display; + if (pending_mode < 0) pending_mode = static_cast(eng->_windowMode); + + ImGui::Separator(); + + if (ImGui::BeginCombo("Monitor", cur_display_name)) + { + for (int i = 0; i < num_displays; ++i) + { + const char *name = SDL_GetDisplayName(i); + if (!name) name = "Unknown"; + const bool selected = (pending_display == i); + if (ImGui::Selectable(name, selected)) + { + pending_display = i; + } + if (selected) + { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + const char *mode_labels[] = { + "Windowed", + "Borderless (Fullscreen Desktop)", + "Exclusive Fullscreen" + }; + ImGui::Combo("Mode", &pending_mode, mode_labels, 3); + + ImGui::TextUnformatted("Apply triggers immediate swapchain recreation."); + if (ImGui::Button("Apply")) + { + auto mode = static_cast(std::clamp(pending_mode, 0, 2)); + eng->set_window_mode(mode, pending_display); + + // Re-sync pending selections with what SDL actually applied. + pending_display = SDL_GetWindowDisplayIndex(eng->_window); + if (pending_display < 0) pending_display = eng->_windowDisplayIndex; + pending_display = std::clamp(pending_display, 0, num_displays - 1); + pending_mode = static_cast(eng->_windowMode); + } + ImGui::SameLine(); + if (ImGui::Button("Use Current")) + { + pending_display = current_display; + pending_mode = static_cast(eng->_windowMode); + } + } + // Background / compute playground static void ui_background(VulkanEngine *eng) { @@ -1364,6 +1447,11 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng) ui_overview(eng); ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Window")) + { + ui_window(eng); + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem("Background")) { ui_background(eng);